ReplaceController.php 23 KB

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