You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

206 lines
6.8 KiB

3 years ago
  1. <?php
  2. /**
  3. * @copyright Copyright (c) 2017 Joas Schilling <coding@schilljs.com>
  4. *
  5. * @author Christoph Wurst <christoph@winzerhof-wurst.at>
  6. * @author Joas Schilling <coding@schilljs.com>
  7. * @author Lukas Reschke <lukas@statuscode.ch>
  8. *
  9. * @license GNU AGPL version 3 or any later version
  10. *
  11. * This program is free software: you can redistribute it and/or modify
  12. * it under the terms of the GNU Affero General Public License as
  13. * published by the Free Software Foundation, either version 3 of the
  14. * License, or (at your option) any later version.
  15. *
  16. * This program is distributed in the hope that it will be useful,
  17. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  18. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  19. * GNU Affero General Public License for more details.
  20. *
  21. * You should have received a copy of the GNU Affero General Public License
  22. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  23. *
  24. */
  25. namespace OC\App\CodeChecker;
  26. use PhpParser\Node;
  27. use PhpParser\Node\Name;
  28. use PhpParser\NodeVisitorAbstract;
  29. class MigrationSchemaChecker extends NodeVisitorAbstract {
  30. /** @var string */
  31. protected $schemaVariableName = null;
  32. /** @var array */
  33. protected $tableVariableNames = [];
  34. /** @var array */
  35. public $errors = [];
  36. /**
  37. * @param Node $node
  38. * @return void
  39. *
  40. * @suppress PhanUndeclaredProperty
  41. */
  42. public function enterNode(Node $node) {
  43. /**
  44. * Check tables
  45. */
  46. if ($this->schemaVariableName !== null &&
  47. $node instanceof Node\Expr\Assign &&
  48. $node->var instanceof Node\Expr\Variable &&
  49. $node->expr instanceof Node\Expr\MethodCall &&
  50. $node->expr->var instanceof Node\Expr\Variable &&
  51. $node->expr->var->name === $this->schemaVariableName) {
  52. if ($node->expr->name === 'createTable') {
  53. if (isset($node->expr->args[0]) && $node->expr->args[0]->value instanceof Node\Scalar\String_) {
  54. if (!$this->checkNameLength($node->expr->args[0]->value->value)) {
  55. $this->errors[] = [
  56. 'line' => $node->getLine(),
  57. 'disallowedToken' => $node->expr->args[0]->value->value,
  58. 'reason' => 'Table name is too long (max. 27)',
  59. ];
  60. } else {
  61. $this->tableVariableNames[$node->var->name] = $node->expr->args[0]->value->value;
  62. }
  63. }
  64. } elseif ($node->expr->name === 'getTable') {
  65. if (isset($node->expr->args[0]) && $node->expr->args[0]->value instanceof Node\Scalar\String_) {
  66. $this->tableVariableNames[$node->var->name] = $node->expr->args[0]->value->value;
  67. }
  68. }
  69. } elseif ($this->schemaVariableName !== null &&
  70. $node instanceof Node\Expr\MethodCall &&
  71. $node->var instanceof Node\Expr\Variable &&
  72. $node->var->name === $this->schemaVariableName) {
  73. if ($node->name === 'renameTable') {
  74. $this->errors[] = [
  75. 'line' => $node->getLine(),
  76. 'disallowedToken' => 'Deprecated method',
  77. 'reason' => sprintf(
  78. '`$%s->renameTable()` must not be used',
  79. $node->var->name
  80. ),
  81. ];
  82. }
  83. /**
  84. * Check columns and Indexes
  85. */
  86. } elseif (!empty($this->tableVariableNames) &&
  87. $node instanceof Node\Expr\MethodCall &&
  88. $node->var instanceof Node\Expr\Variable &&
  89. isset($this->tableVariableNames[$node->var->name])) {
  90. if ($node->name === 'addColumn' || $node->name === 'changeColumn') {
  91. if (isset($node->args[0]) && $node->args[0]->value instanceof Node\Scalar\String_) {
  92. if (!$this->checkNameLength($node->args[0]->value->value)) {
  93. $this->errors[] = [
  94. 'line' => $node->getLine(),
  95. 'disallowedToken' => $node->args[0]->value->value,
  96. 'reason' => sprintf(
  97. 'Column name is too long on table `%s` (max. 27)',
  98. $this->tableVariableNames[$node->var->name]
  99. ),
  100. ];
  101. }
  102. // On autoincrement the max length of the table name is 21 instead of 27
  103. if (isset($node->args[2]) && $node->args[2]->value instanceof Node\Expr\Array_) {
  104. /** @var Node\Expr\Array_ $options */
  105. $options = $node->args[2]->value;
  106. if ($this->checkColumnForAutoincrement($options)) {
  107. if (!$this->checkNameLength($this->tableVariableNames[$node->var->name], true)) {
  108. $this->errors[] = [
  109. 'line' => $node->getLine(),
  110. 'disallowedToken' => $this->tableVariableNames[$node->var->name],
  111. 'reason' => 'Table name is too long because of autoincrement (max. 21)',
  112. ];
  113. }
  114. }
  115. }
  116. }
  117. } elseif ($node->name === 'addIndex' ||
  118. $node->name === 'addUniqueIndex' ||
  119. $node->name === 'renameIndex' ||
  120. $node->name === 'setPrimaryKey') {
  121. if (isset($node->args[1]) && $node->args[1]->value instanceof Node\Scalar\String_) {
  122. if (!$this->checkNameLength($node->args[1]->value->value)) {
  123. $this->errors[] = [
  124. 'line' => $node->getLine(),
  125. 'disallowedToken' => $node->args[1]->value->value,
  126. 'reason' => sprintf(
  127. 'Index name is too long on table `%s` (max. 27)',
  128. $this->tableVariableNames[$node->var->name]
  129. ),
  130. ];
  131. }
  132. }
  133. } elseif ($node->name === 'addForeignKeyConstraint') {
  134. if (isset($node->args[4]) && $node->args[4]->value instanceof Node\Scalar\String_) {
  135. if (!$this->checkNameLength($node->args[4]->value->value)) {
  136. $this->errors[] = [
  137. 'line' => $node->getLine(),
  138. 'disallowedToken' => $node->args[4]->value->value,
  139. 'reason' => sprintf(
  140. 'Constraint name is too long on table `%s` (max. 27)',
  141. $this->tableVariableNames[$node->var->name]
  142. ),
  143. ];
  144. }
  145. }
  146. } elseif ($node->name === 'renameColumn') {
  147. $this->errors[] = [
  148. 'line' => $node->getLine(),
  149. 'disallowedToken' => 'Deprecated method',
  150. 'reason' => sprintf(
  151. '`$%s->renameColumn()` must not be used',
  152. $node->var->name
  153. ),
  154. ];
  155. }
  156. /**
  157. * Find the schema
  158. */
  159. } elseif ($node instanceof Node\Expr\Assign &&
  160. $node->expr instanceof Node\Expr\FuncCall &&
  161. $node->var instanceof Node\Expr\Variable &&
  162. $node->expr->name instanceof Node\Expr\Variable &&
  163. $node->expr->name->name === 'schemaClosure') {
  164. // E.g. $schema = $schemaClosure();
  165. $this->schemaVariableName = $node->var->name;
  166. }
  167. }
  168. protected function checkNameLength($tableName, $hasAutoincrement = false) {
  169. if ($hasAutoincrement) {
  170. return strlen($tableName) <= 21;
  171. }
  172. return strlen($tableName) <= 27;
  173. }
  174. /**
  175. * @param Node\Expr\Array_ $optionsArray
  176. * @return bool Whether the column is an autoincrement column
  177. */
  178. protected function checkColumnForAutoincrement(Node\Expr\Array_ $optionsArray) {
  179. foreach ($optionsArray->items as $option) {
  180. if ($option->key instanceof Node\Scalar\String_) {
  181. if ($option->key->value === 'autoincrement' &&
  182. $option->value instanceof Node\Expr\ConstFetch) {
  183. /** @var Node\Expr\ConstFetch $const */
  184. $const = $option->value;
  185. if ($const->name instanceof Name &&
  186. $const->name->parts === ['true']) {
  187. return true;
  188. }
  189. }
  190. }
  191. }
  192. return false;
  193. }
  194. }