PageRenderTime 46ms CodeModel.GetById 14ms RepoModel.GetById 1ms app.codeStats 0ms

/libraries/classes/Controllers/Table/ReplaceController.php

http://github.com/phpmyadmin/phpmyadmin
PHP | 672 lines | 502 code | 91 blank | 79 comment | 60 complexity | 4f6a2cd4cbec42e404c556bba9480da4 MD5 | raw file
Possible License(s): GPL-2.0, MIT, LGPL-3.0
  1. <?php
  2. declare(strict_types=1);
  3. namespace PhpMyAdmin\Controllers\Table;
  4. use PhpMyAdmin\Controllers\Database\SqlController as DatabaseSqlController;
  5. use PhpMyAdmin\Controllers\Sql\SqlController;
  6. use PhpMyAdmin\Controllers\Table\SqlController as TableSqlController;
  7. use PhpMyAdmin\Core;
  8. use PhpMyAdmin\DatabaseInterface;
  9. use PhpMyAdmin\File;
  10. use PhpMyAdmin\Html\Generator;
  11. use PhpMyAdmin\InsertEdit;
  12. use PhpMyAdmin\Message;
  13. use PhpMyAdmin\Plugins\IOTransformationsPlugin;
  14. use PhpMyAdmin\Relation;
  15. use PhpMyAdmin\ResponseRenderer;
  16. use PhpMyAdmin\Table;
  17. use PhpMyAdmin\Template;
  18. use PhpMyAdmin\Transformations;
  19. use PhpMyAdmin\Util;
  20. use function __;
  21. use function array_values;
  22. use function class_exists;
  23. use function count;
  24. use function implode;
  25. use function in_array;
  26. use function is_file;
  27. use function is_numeric;
  28. use function method_exists;
  29. use function parse_str;
  30. use function sprintf;
  31. /**
  32. * Manipulation of table data like inserting, replacing and updating.
  33. */
  34. final class ReplaceController extends AbstractController
  35. {
  36. /** @var InsertEdit */
  37. private $insertEdit;
  38. /** @var Transformations */
  39. private $transformations;
  40. /** @var Relation */
  41. private $relation;
  42. /** @var DatabaseInterface */
  43. private $dbi;
  44. public function __construct(
  45. ResponseRenderer $response,
  46. Template $template,
  47. string $db,
  48. string $table,
  49. InsertEdit $insertEdit,
  50. Transformations $transformations,
  51. Relation $relation,
  52. DatabaseInterface $dbi
  53. ) {
  54. parent::__construct($response, $template, $db, $table);
  55. $this->insertEdit = $insertEdit;
  56. $this->transformations = $transformations;
  57. $this->relation = $relation;
  58. $this->dbi = $dbi;
  59. }
  60. public function __invoke(): void
  61. {
  62. global $containerBuilder, $db, $table, $urlParams, $message;
  63. global $errorUrl, $mime_map, $unsaved_values, $active_page, $disp_query, $disp_message;
  64. global $goto_include, $loop_array, $using_key, $is_insert, $is_insertignore, $query;
  65. global $value_sets, $func_no_param, $func_optional_param, $gis_from_text_functions, $gis_from_wkb_functions;
  66. global $query_fields, $insert_errors, $row_skipped, $query_values;
  67. global $total_affected_rows, $last_messages, $warning_messages, $error_messages, $return_to_sql_query;
  68. Util::checkParameters(['db', 'table', 'goto']);
  69. $this->dbi->selectDb($db);
  70. /**
  71. * Initializes some variables
  72. */
  73. $goto_include = false;
  74. $this->addScriptFiles([
  75. 'makegrid.js',
  76. 'vendor/stickyfill.min.js',
  77. 'sql.js',
  78. 'indexes.js',
  79. 'gis_data_editor.js',
  80. ]);
  81. $insertRows = $_POST['insert_rows'] ?? null;
  82. if (is_numeric($insertRows) && $insertRows != $GLOBALS['cfg']['InsertRows']) {
  83. // check whether insert row mode, if so include /table/change
  84. $this->addScriptFiles([
  85. 'vendor/jquery/additional-methods.js',
  86. 'table/change.js',
  87. ]);
  88. $GLOBALS['cfg']['InsertRows'] = $_POST['insert_rows'];
  89. /** @var ChangeController $controller */
  90. $controller = $containerBuilder->get(ChangeController::class);
  91. $controller();
  92. return;
  93. }
  94. $after_insert_actions = [
  95. 'new_insert',
  96. 'same_insert',
  97. 'edit_next',
  98. ];
  99. if (isset($_POST['after_insert']) && in_array($_POST['after_insert'], $after_insert_actions)) {
  100. $urlParams['after_insert'] = $_POST['after_insert'];
  101. if (isset($_POST['where_clause'])) {
  102. foreach ($_POST['where_clause'] as $one_where_clause) {
  103. if ($_POST['after_insert'] === 'same_insert') {
  104. $urlParams['where_clause'][] = $one_where_clause;
  105. } elseif ($_POST['after_insert'] === 'edit_next') {
  106. $this->insertEdit->setSessionForEditNext($one_where_clause);
  107. }
  108. }
  109. }
  110. }
  111. //get $goto_include for different cases
  112. $goto_include = $this->insertEdit->getGotoInclude($goto_include);
  113. // Defines the url to return in case of failure of the query
  114. $errorUrl = $this->insertEdit->getErrorUrl($urlParams);
  115. /**
  116. * Prepares the update/insert of a row
  117. */
  118. [
  119. $loop_array,
  120. $using_key,
  121. $is_insert,
  122. $is_insertignore,
  123. ] = $this->insertEdit->getParamsForUpdateOrInsert();
  124. $query = [];
  125. $value_sets = [];
  126. $func_no_param = [
  127. 'CONNECTION_ID',
  128. 'CURRENT_USER',
  129. 'CURDATE',
  130. 'CURTIME',
  131. 'CURRENT_DATE',
  132. 'CURRENT_TIME',
  133. 'DATABASE',
  134. 'LAST_INSERT_ID',
  135. 'NOW',
  136. 'PI',
  137. 'RAND',
  138. 'SYSDATE',
  139. 'UNIX_TIMESTAMP',
  140. 'USER',
  141. 'UTC_DATE',
  142. 'UTC_TIME',
  143. 'UTC_TIMESTAMP',
  144. 'UUID',
  145. 'UUID_SHORT',
  146. 'VERSION',
  147. ];
  148. $func_optional_param = [
  149. 'RAND',
  150. 'UNIX_TIMESTAMP',
  151. ];
  152. $gis_from_text_functions = [
  153. 'GeomFromText',
  154. 'GeomCollFromText',
  155. 'LineFromText',
  156. 'MLineFromText',
  157. 'PointFromText',
  158. 'MPointFromText',
  159. 'PolyFromText',
  160. 'MPolyFromText',
  161. ];
  162. $gis_from_wkb_functions = [
  163. 'GeomFromWKB',
  164. 'GeomCollFromWKB',
  165. 'LineFromWKB',
  166. 'MLineFromWKB',
  167. 'PointFromWKB',
  168. 'MPointFromWKB',
  169. 'PolyFromWKB',
  170. 'MPolyFromWKB',
  171. ];
  172. if ($this->dbi->getVersion() >= 50600) {
  173. $gis_from_text_functions = [
  174. 'ST_GeomFromText',
  175. 'ST_GeomCollFromText',
  176. 'ST_LineFromText',
  177. 'ST_MLineFromText',
  178. 'ST_PointFromText',
  179. 'ST_MPointFromText',
  180. 'ST_PolyFromText',
  181. 'ST_MPolyFromText',
  182. ];
  183. $gis_from_wkb_functions = [
  184. 'ST_GeomFromWKB',
  185. 'ST_GeomCollFromWKB',
  186. 'ST_LineFromWKB',
  187. 'ST_MLineFromWKB',
  188. 'ST_PointFromWKB',
  189. 'ST_MPointFromWKB',
  190. 'ST_PolyFromWKB',
  191. 'ST_MPolyFromWKB',
  192. ];
  193. }
  194. $mime_map = $this->transformations->getMime($db, $table);
  195. if ($mime_map === null) {
  196. $mime_map = [];
  197. }
  198. $query_fields = [];
  199. $insert_errors = [];
  200. $row_skipped = false;
  201. $unsaved_values = [];
  202. foreach ($loop_array as $rownumber => $where_clause) {
  203. // skip fields to be ignored
  204. if (! $using_key && isset($_POST['insert_ignore_' . $where_clause])) {
  205. continue;
  206. }
  207. // Defines the SET part of the sql query
  208. $query_values = [];
  209. // Map multi-edit keys to single-level arrays, dependent on how we got the fields
  210. $multi_edit_columns = $_POST['fields']['multi_edit'][$rownumber] ?? [];
  211. $multi_edit_columns_name = $_POST['fields_name']['multi_edit'][$rownumber] ?? [];
  212. $multi_edit_columns_prev = $_POST['fields_prev']['multi_edit'][$rownumber] ?? null;
  213. $multi_edit_funcs = $_POST['funcs']['multi_edit'][$rownumber] ?? null;
  214. $multi_edit_salt = $_POST['salt']['multi_edit'][$rownumber] ?? null;
  215. $multi_edit_columns_type = $_POST['fields_type']['multi_edit'][$rownumber] ?? null;
  216. $multi_edit_columns_null = $_POST['fields_null']['multi_edit'][$rownumber] ?? null;
  217. $multi_edit_columns_null_prev = $_POST['fields_null_prev']['multi_edit'][$rownumber] ?? null;
  218. $multi_edit_auto_increment = $_POST['auto_increment']['multi_edit'][$rownumber] ?? null;
  219. $multi_edit_virtual = $_POST['virtual']['multi_edit'][$rownumber] ?? null;
  220. // When a select field is nullified, it's not present in $_POST
  221. // so initialize it; this way, the foreach($multi_edit_columns) will process it
  222. foreach ($multi_edit_columns_name as $key => $val) {
  223. if (isset($multi_edit_columns[$key])) {
  224. continue;
  225. }
  226. $multi_edit_columns[$key] = '';
  227. }
  228. // Iterate in the order of $multi_edit_columns_name,
  229. // not $multi_edit_columns, to avoid problems
  230. // when inserting multiple entries
  231. $insert_fail = false;
  232. foreach ($multi_edit_columns_name as $key => $column_name) {
  233. $current_value = $multi_edit_columns[$key];
  234. // Note: $key is an md5 of the fieldname. The actual fieldname is
  235. // available in $multi_edit_columns_name[$key]
  236. $file_to_insert = new File();
  237. $file_to_insert->checkTblChangeForm((string) $key, (string) $rownumber);
  238. $possibly_uploaded_val = $file_to_insert->getContent();
  239. if ($possibly_uploaded_val !== false) {
  240. $current_value = $possibly_uploaded_val;
  241. }
  242. // Apply Input Transformation if defined
  243. if (! empty($mime_map[$column_name]) && ! empty($mime_map[$column_name]['input_transformation'])) {
  244. $filename = 'libraries/classes/Plugins/Transformations/'
  245. . $mime_map[$column_name]['input_transformation'];
  246. if (is_file(ROOT_PATH . $filename)) {
  247. $classname = $this->transformations->getClassName($filename);
  248. if (class_exists($classname)) {
  249. /** @var IOTransformationsPlugin $transformation_plugin */
  250. $transformation_plugin = new $classname();
  251. $transformation_options = $this->transformations->getOptions(
  252. $mime_map[$column_name]['input_transformation_options']
  253. );
  254. $current_value = $transformation_plugin->applyTransformation(
  255. $current_value,
  256. $transformation_options
  257. );
  258. // check if transformation was successful or not
  259. // and accordingly set error messages & insert_fail
  260. if (
  261. method_exists($transformation_plugin, 'isSuccess')
  262. && ! $transformation_plugin->isSuccess()
  263. ) {
  264. $insert_fail = true;
  265. $row_skipped = true;
  266. $insert_errors[] = sprintf(
  267. __('Row: %1$s, Column: %2$s, Error: %3$s'),
  268. $rownumber,
  269. $column_name,
  270. $transformation_plugin->getError()
  271. );
  272. }
  273. }
  274. }
  275. }
  276. if ($file_to_insert->isError()) {
  277. $insert_errors[] = $file_to_insert->getError();
  278. }
  279. // delete $file_to_insert temporary variable
  280. $file_to_insert->cleanUp();
  281. $current_value = $this->insertEdit->getCurrentValueForDifferentTypes(
  282. $possibly_uploaded_val,
  283. $key,
  284. $multi_edit_columns_type,
  285. $current_value,
  286. $multi_edit_auto_increment,
  287. $rownumber,
  288. $multi_edit_columns_name,
  289. $multi_edit_columns_null,
  290. $multi_edit_columns_null_prev,
  291. $is_insert,
  292. $using_key,
  293. $where_clause,
  294. $table,
  295. $multi_edit_funcs
  296. );
  297. $current_value_as_an_array = $this->insertEdit->getCurrentValueAsAnArrayForMultipleEdit(
  298. $multi_edit_funcs,
  299. $multi_edit_salt,
  300. $gis_from_text_functions,
  301. $current_value,
  302. $gis_from_wkb_functions,
  303. $func_optional_param,
  304. $func_no_param,
  305. $key
  306. );
  307. if (! isset($multi_edit_virtual, $multi_edit_virtual[$key])) {
  308. [
  309. $query_values,
  310. $query_fields,
  311. ] = $this->insertEdit->getQueryValuesForInsertAndUpdateInMultipleEdit(
  312. $multi_edit_columns_name,
  313. $multi_edit_columns_null,
  314. $current_value,
  315. $multi_edit_columns_prev,
  316. $multi_edit_funcs,
  317. $is_insert,
  318. $query_values,
  319. $query_fields,
  320. $current_value_as_an_array,
  321. $value_sets,
  322. $key,
  323. $multi_edit_columns_null_prev
  324. );
  325. }
  326. if (! isset($multi_edit_columns_null[$key])) {
  327. continue;
  328. }
  329. $multi_edit_columns[$key] = null;
  330. }
  331. // temporarily store rows not inserted
  332. // so that they can be populated again.
  333. if ($insert_fail) {
  334. $unsaved_values[$rownumber] = $multi_edit_columns;
  335. }
  336. if ($insert_fail || count($query_values) <= 0) {
  337. continue;
  338. }
  339. if ($is_insert) {
  340. $value_sets[] = implode(', ', $query_values);
  341. } else {
  342. // build update query
  343. $clauseIsUnique = $_POST['clause_is_unique'] ?? '';// Should contain 0 or 1
  344. $query[] = 'UPDATE ' . Util::backquote($table)
  345. . ' SET ' . implode(', ', $query_values)
  346. . ' WHERE ' . $where_clause
  347. . ($clauseIsUnique ? '' : ' LIMIT 1');
  348. }
  349. }
  350. unset(
  351. $multi_edit_columns_name,
  352. $multi_edit_columns_prev,
  353. $multi_edit_funcs,
  354. $multi_edit_columns_type,
  355. $multi_edit_columns_null,
  356. $func_no_param,
  357. $multi_edit_auto_increment,
  358. $current_value_as_an_array,
  359. $key,
  360. $current_value,
  361. $loop_array,
  362. $where_clause,
  363. $using_key,
  364. $multi_edit_columns_null_prev,
  365. $insert_fail
  366. );
  367. // Builds the sql query
  368. if ($is_insert && count($value_sets) > 0) {
  369. $query = $this->insertEdit->buildSqlQuery($is_insertignore, $query_fields, $value_sets);
  370. } elseif (empty($query) && ! isset($_POST['preview_sql']) && ! $row_skipped) {
  371. // No change -> move back to the calling script
  372. //
  373. // Note: logic passes here for inline edit
  374. $message = Message::success(__('No change'));
  375. // Avoid infinite recursion
  376. if ($goto_include === '/table/replace') {
  377. $goto_include = '/table/change';
  378. }
  379. $active_page = $goto_include;
  380. if ($goto_include === '/sql') {
  381. /** @var SqlController $controller */
  382. $controller = $containerBuilder->get(SqlController::class);
  383. $controller();
  384. return;
  385. }
  386. if ($goto_include === '/database/sql') {
  387. /** @var DatabaseSqlController $controller */
  388. $controller = $containerBuilder->get(DatabaseSqlController::class);
  389. $controller();
  390. return;
  391. }
  392. if ($goto_include === '/table/change') {
  393. /** @var ChangeController $controller */
  394. $controller = $containerBuilder->get(ChangeController::class);
  395. $controller();
  396. return;
  397. }
  398. if ($goto_include === '/table/sql') {
  399. /** @var TableSqlController $controller */
  400. $controller = $containerBuilder->get(TableSqlController::class);
  401. $controller();
  402. return;
  403. }
  404. /** @psalm-suppress UnresolvableInclude */
  405. include ROOT_PATH . Core::securePath((string) $goto_include);
  406. return;
  407. }
  408. unset($multi_edit_columns, $is_insertignore);
  409. // If there is a request for SQL previewing.
  410. if (isset($_POST['preview_sql'])) {
  411. Core::previewSQL($query);
  412. return;
  413. }
  414. /**
  415. * Executes the sql query and get the result, then move back to the calling
  416. * page
  417. */
  418. [
  419. $urlParams,
  420. $total_affected_rows,
  421. $last_messages,
  422. $warning_messages,
  423. $error_messages,
  424. $return_to_sql_query,
  425. ] = $this->insertEdit->executeSqlQuery($urlParams, $query);
  426. if ($is_insert && (count($value_sets) > 0 || $row_skipped)) {
  427. $message = Message::getMessageForInsertedRows($total_affected_rows);
  428. $unsaved_values = array_values($unsaved_values);
  429. } else {
  430. $message = Message::getMessageForAffectedRows($total_affected_rows);
  431. }
  432. if ($row_skipped) {
  433. $goto_include = '/table/change';
  434. $message->addMessagesString($insert_errors, '<br>');
  435. $message->isError(true);
  436. }
  437. $message->addMessages($last_messages, '<br>');
  438. if (! empty($warning_messages)) {
  439. $message->addMessagesString($warning_messages, '<br>');
  440. $message->isError(true);
  441. }
  442. if (! empty($error_messages)) {
  443. $message->addMessagesString($error_messages);
  444. $message->isError(true);
  445. }
  446. unset(
  447. $error_messages,
  448. $warning_messages,
  449. $total_affected_rows,
  450. $last_messages,
  451. $row_skipped,
  452. $insert_errors
  453. );
  454. /**
  455. * The following section only applies to grid editing.
  456. * However, verifying isAjax() is not enough to ensure we are coming from
  457. * grid editing. If we are coming from the Edit or Copy link in Browse mode,
  458. * ajax_page_request is present in the POST parameters.
  459. */
  460. if ($this->response->isAjax() && ! isset($_POST['ajax_page_request'])) {
  461. /**
  462. * If we are in grid editing, we need to process the relational and
  463. * transformed fields, if they were edited. After that, output the correct
  464. * link/transformed value and exit
  465. */
  466. if (isset($_POST['rel_fields_list']) && $_POST['rel_fields_list'] != '') {
  467. $map = $this->relation->getForeigners($db, $table, '', 'both');
  468. /** @var array<int,array> $relation_fields */
  469. $relation_fields = [];
  470. parse_str($_POST['rel_fields_list'], $relation_fields);
  471. // loop for each relation cell
  472. foreach ($relation_fields as $cell_index => $curr_rel_field) {
  473. foreach ($curr_rel_field as $relation_field => $relation_field_value) {
  474. $where_comparison = "='" . $relation_field_value . "'";
  475. $dispval = $this->insertEdit->getDisplayValueForForeignTableColumn(
  476. $where_comparison,
  477. $map,
  478. $relation_field
  479. );
  480. $extra_data['relations'][$cell_index] = $this->insertEdit->getLinkForRelationalDisplayField(
  481. $map,
  482. $relation_field,
  483. $where_comparison,
  484. $dispval,
  485. $relation_field_value
  486. );
  487. }
  488. }
  489. }
  490. if (isset($_POST['do_transformations']) && $_POST['do_transformations'] == true) {
  491. $edited_values = [];
  492. parse_str($_POST['transform_fields_list'], $edited_values);
  493. if (! isset($extra_data)) {
  494. $extra_data = [];
  495. }
  496. $transformation_types = [
  497. 'input_transformation',
  498. 'transformation',
  499. ];
  500. foreach ($mime_map as $transformation) {
  501. $column_name = $transformation['column_name'];
  502. foreach ($transformation_types as $type) {
  503. $file = Core::securePath($transformation[$type]);
  504. $extra_data = $this->insertEdit->transformEditedValues(
  505. $db,
  506. $table,
  507. $transformation,
  508. $edited_values,
  509. $file,
  510. $column_name,
  511. $extra_data,
  512. $type
  513. );
  514. }
  515. }
  516. }
  517. // Need to check the inline edited value can be truncated by MySQL
  518. // without informing while saving
  519. $column_name = $_POST['fields_name']['multi_edit'][0][0];
  520. $this->insertEdit->verifyWhetherValueCanBeTruncatedAndAppendExtraData(
  521. $db,
  522. $table,
  523. $column_name,
  524. $extra_data
  525. );
  526. /**Get the total row count of the table*/
  527. $_table = new Table($_POST['table'], $_POST['db']);
  528. $extra_data['row_count'] = $_table->countRecords();
  529. $extra_data['sql_query'] = Generator::getMessage($message, $GLOBALS['display_query']);
  530. $this->response->setRequestStatus($message->isSuccess());
  531. $this->response->addJSON('message', $message);
  532. $this->response->addJSON($extra_data);
  533. return;
  534. }
  535. if (! empty($return_to_sql_query)) {
  536. $disp_query = $GLOBALS['sql_query'];
  537. $disp_message = $message;
  538. unset($message);
  539. $GLOBALS['sql_query'] = $return_to_sql_query;
  540. }
  541. $this->addScriptFiles(['vendor/jquery/additional-methods.js', 'table/change.js']);
  542. $active_page = $goto_include;
  543. /**
  544. * If user asked for "and then Insert another new row" we have to remove
  545. * WHERE clause information so that /table/change does not go back
  546. * to the current record
  547. */
  548. if (isset($_POST['after_insert']) && $_POST['after_insert'] === 'new_insert') {
  549. unset($_POST['where_clause']);
  550. }
  551. if ($goto_include === '/sql') {
  552. /** @var SqlController $controller */
  553. $controller = $containerBuilder->get(SqlController::class);
  554. $controller();
  555. return;
  556. }
  557. if ($goto_include === '/database/sql') {
  558. /** @var DatabaseSqlController $controller */
  559. $controller = $containerBuilder->get(DatabaseSqlController::class);
  560. $controller();
  561. return;
  562. }
  563. if ($goto_include === '/table/change') {
  564. /** @var ChangeController $controller */
  565. $controller = $containerBuilder->get(ChangeController::class);
  566. $controller();
  567. return;
  568. }
  569. if ($goto_include === '/table/sql') {
  570. /** @var TableSqlController $controller */
  571. $controller = $containerBuilder->get(TableSqlController::class);
  572. $controller();
  573. return;
  574. }
  575. /**
  576. * Load target page.
  577. */
  578. /** @psalm-suppress UnresolvableInclude */
  579. require ROOT_PATH . Core::securePath((string) $goto_include);
  580. }
  581. }