SproutCMS

This is the code documentation for the SproutCMS project

source of /sprout/Controllers/Controller.php

Copyright (C) 2017 Karmabunny Pty Ltd.

This file is a part of SproutCMS.

SproutCMS is free software: you can redistribute it and/or modify it under the terms
of the GNU General Public License as published by the Free Software Foundation, either
version 2 of the License, or (at your option) any later version.

For more information, visit <http://getsproutcms.com>.

This class was originally from Kohana 2.3.4
Copyright 2007-2008 Kohana Team
  1. <?php
  2. /**
  3.  * Copyright (C) 2017 Karmabunny Pty Ltd.
  4.  *
  5.  * This file is a part of SproutCMS.
  6.  *
  7.  * SproutCMS is free software: you can redistribute it and/or modify it under the terms
  8.  * of the GNU General Public License as published by the Free Software Foundation, either
  9.  * version 2 of the License, or (at your option) any later version.
  10.  *
  11.  * For more information, visit <http://getsproutcms.com>.
  12.  *
  13.  * This class was originally from Kohana 2.3.4
  14.  * Copyright 2007-2008 Kohana Team
  15.  */
  16. namespace Sprout\Controllers;
  17.  
  18. use Exception;
  19. use InvalidArgumentException;
  20.  
  21. use Sprout\Controllers\Admin\ManagedAdminController;
  22. use Sprout\Exceptions\FileMissingException;
  23. use karmabunny\pdb\Exceptions\QueryException;
  24. use Sprout\Helpers\AdminAuth;
  25. use Sprout\Helpers\Inflector;
  26. use Sprout\Helpers\JsonForm;
  27. use Sprout\Helpers\MultiEdit;
  28. use Sprout\Helpers\Pdb;
  29. use Sprout\Helpers\Request;
  30. use Sprout\Helpers\Sprout;
  31. use Sprout\Helpers\Text;
  32. use Sprout\Helpers\Validator;
  33. use Sprout\Helpers\View;
  34.  
  35.  
  36. /**
  37.  * Kohana Controller class. The controller class must be extended to work
  38.  * properly, so this class is defined as abstract.
  39.  */
  40. abstract class Controller extends BaseController
  41. {
  42.  
  43. /** Should this controller log add/edit/delete actions? */
  44. protected $action_log = false;
  45.  
  46.  
  47. /**
  48.   * Stores a history item in the database, recording an add (i.e. insert), edit, or delete.
  49.   * Should be called AFTER the action has been made.
  50.   * N.B. This is a low-level method; the friendlier wrapper methods are preferred;
  51.   * e.g. {@see Controller::logAdd}, {@see Controller::logEdit}
  52.   *
  53.   * @param string $table The table which had data modified by the query
  54.   * @param int $record_id The id of the added/edited/deleted record
  55.   * @param int $type ENUM value from history_items.type
  56.   * @param array $data Data associated with an add/edit.
  57.   * The handling differs based on the $type parameter:
  58.   * 'Add' ignored
  59.   * 'Edit' should be an array of the pre-update data, used to build a diff
  60.   * 'Delete' should contain the complete data of the deleted record
  61.   * 'Add category' should contain ['cat_id' => value]
  62.   * 'Delete category' should contain ['cat_id' => value]
  63.   * 'Change password' should be an empty array
  64.   * @param int $parent_log_id ID of a parent log entry in the history_items table.
  65.   * This is used when multiple records are deleted by a single action.
  66.   * Undoing that action can then restore all of the deleted data.
  67.   * N.B. This is only relevant for deletes
  68.   * @return int ID of the record inserted into the history_items table;
  69.   * 0 if no insert was done; -1 if it failed
  70.   * @throws QueryException
  71.   */
  72. protected function logAction($table, $record_id, $type, array $data = [], $parent_log_id = 0)
  73. {
  74. static $types = null;
  75. static $insert_query = null;
  76. static $pdo = null;
  77.  
  78. if (!$this->action_log) return 0;
  79.  
  80. $record_id = (int) $record_id;
  81. $parent_log_id = (int) $parent_log_id;
  82. if (!$types) $types = Pdb::extractEnumArr('history_items', 'type');
  83.  
  84. if (!in_array($type, $types)) {
  85. throw new InvalidArgumentException('Invalid action type: ' . $type);
  86. }
  87.  
  88. switch ($type) {
  89. case 'Add':
  90. $data = $this->loadRecord($table, $record_id);
  91. if (!$data) return 0;
  92. break;
  93.  
  94. case 'Edit':
  95. $row = $this->loadRecord($table, $record_id);
  96. if (!$row) return 0;
  97. unset ($row['date_modified']);
  98.  
  99. foreach ($row as $name => $val) {
  100. if ($data[$name] == $val) {
  101. unset ($row[$name]);
  102. continue;
  103. }
  104.  
  105. $row[$name] = trim(strip_tags($row[$name]));
  106. $row[$name] = str_replace("\n", ' ', $row[$name]);
  107.  
  108. if (strlen($row[$name]) > 50) {
  109. $row[$name] = substr($row[$name], 0, 50) . '...';
  110. }
  111. }
  112.  
  113. $data = $row;
  114. if (count($data) == 0) return 0;
  115. break;
  116. }
  117.  
  118. $user_details = AdminAuth::getDetails();
  119.  
  120. if (!$insert_query) {
  121. $pdo = Pdb::getConnection('RW');
  122. $q = "INSERT INTO ~history_items
  123. (record_table, record_id, type, modified_editor, ip_address, user_agent, controller,
  124. data, date_added, date_modified, parent_id)
  125. VALUES
  126. (:record_table, :record_id, :type, :modified_editor, :ip_address, :user_agent, :controller,
  127. :data, :date_added, :date_modified, :parent_id)";
  128. $insert_query = Pdb::query($q, [], 'prep');
  129. }
  130.  
  131. $insert_data = array();
  132. $insert_data['record_table'] = $table;
  133. $insert_data['record_id'] = $record_id;
  134. $insert_data['type'] = $type;
  135. $insert_data['modified_editor'] = $user_details['name'];
  136. $insert_data['ip_address'] = bin2hex(inet_pton(trim(Request::userIp())));
  137. $insert_data['user_agent'] = (string) @$_SERVER['HTTP_USER_AGENT'];
  138. $insert_data['controller'] = get_class($this);
  139. $insert_data['data'] = json_encode($data);
  140. $insert_data['date_added'] = Pdb::now();
  141. $insert_data['date_modified'] = Pdb::now();
  142. $insert_data['parent_id'] = $parent_log_id;
  143.  
  144. if (!$insert_query->execute($insert_data)) return -1;
  145.  
  146. return $pdo->lastInsertId();
  147. }
  148.  
  149.  
  150. /**
  151.   * Fetches the pre-update record from the database.
  152.   * Used by the action log system, and disabled if the action log system has been turned off
  153.   * @param string $table Name of table to load record from
  154.   * @param int $record_id ID of record to load
  155.   * @return array|bool False if not using logging for this controller, else the record row
  156.   * @throws RowMissingException If record doesn't exist
  157.   */
  158. protected function loadRecord($table, $record_id)
  159. {
  160. $record_id = (int) $record_id;
  161. Pdb::validateIdentifier($table);
  162. if (!$this->action_log) return false;
  163.  
  164. $q = "SELECT * FROM ~{$table} WHERE id = ?";
  165. return Pdb::query($q, [$record_id], 'row');
  166. }
  167.  
  168.  
  169. /**
  170.   * Logs an add action. This is a wrapper for {@see Controller::logAction}
  171.   * @param string $table The table which had data modified by the query
  172.   * @param int $record_id The id of the added record
  173.   * @return int ID of the record inserted into the history_items table
  174.   * @throws QueryException
  175.   */
  176. protected function logAdd($table, $record_id)
  177. {
  178. return $this->logAction($table, $record_id, 'Add');
  179. }
  180.  
  181.  
  182. /**
  183.   * Logs the adding of a record to a category. This is a wrapper for {@see Controller::logAction}
  184.   * @param string $table The table which had data modified by the query
  185.   * @param int $record_id The ID of the record which was added to the category
  186.   * @param int $cat_id The ID of the category which the record was added to
  187.   * @return int ID of the record inserted into the history_items table
  188.   * @throws QueryException
  189.   */
  190. protected function logAddCategory($table, $record_id, $cat_id)
  191. {
  192. return $this->logAction($table, $record_id, 'Add category', ['cat_id' => $cat_id]);
  193. }
  194.  
  195.  
  196. /**
  197.   * Logs an edit action. This is a wrapper for {@see Controller::logAction}
  198.   * @param string $table The table which had data modified by the query
  199.   * @param int $record_id The id of the edited record
  200.   * @param array $data Pre-update data, used to build a diff
  201.   * @return int ID of the record inserted into the history_items table
  202.   * @throws QueryException
  203.   */
  204. protected function logEdit($table, $record_id, array $data)
  205. {
  206. return $this->logAction($table, $record_id, 'Edit', $data);
  207. }
  208.  
  209.  
  210. /**
  211.   * Logs a delete action. This is a wrapper for {@see Controller::logAction}
  212.   * @param string $table The table which contained the deleted record
  213.   * @param int $record_id The id of the deleted record
  214.   * @param array $data The complete contents of the record which was deleted, i.e. [field => value],
  215.   * so it can be restored if necessary
  216.   * @param int $parent_log_id ID of a parent log entry in the history_items table.
  217.   * This is used when multiple records are deleted by a single action.
  218.   * Undoing that action can then restore all of the deleted data.
  219.   * @return int ID of the record inserted into the history_items table
  220.   * @throws QueryException
  221.   */
  222. protected function logDelete($table, $record_id, array $data, $parent_log_id = 0)
  223. {
  224. return $this->logAction($table, $record_id, 'Delete', $data, $parent_log_id);
  225. }
  226.  
  227.  
  228. /**
  229.   * Logs the removal of a record from a category. This is a wrapper for {@see Controller::logAction}
  230.   * @param string $table The table which contains the record which was removed from the category
  231.   * @param int $record_id The ID of the record which was removed from the category
  232.   * @param int $cat_id The ID of the category which the record was removed from
  233.   * @return int ID of the record inserted into the history_items table
  234.   * @throws QueryException
  235.   */
  236. protected function logDeleteCategory($table, $record_id, $cat_id)
  237. {
  238. return $this->logAction($table, $record_id, 'Delete category', ['cat_id' => $cat_id]);
  239. }
  240.  
  241.  
  242. /**
  243.   * Deletes a record, and logs the deletion
  244.   * This should be used by all _deleteSave methods
  245.   * Starts a transaction and commits it if not already in a transaction when called
  246.   * @param string $table Table name
  247.   * @param int $record_id
  248.   * @param int $parent_log_id ID of a parent entry in the history_items table.
  249.   * This is used when multiple records are deleted by a single action.
  250.   * Undoing that action can then restore all of the deleted data.
  251.   * @return int ID of the record inserted into the history_items table (0 if no logging)
  252.   */
  253. protected function deleteRecord($table, $record_id, $parent_log_id = 0)
  254. {
  255. $record_id = (int) $record_id;
  256.  
  257. $extant_transaction = Pdb::inTransaction();
  258. if (!$extant_transaction) Pdb::transact();
  259.  
  260. if ($this->action_log) {
  261. static $table_dep_cache = [];
  262.  
  263. $record = Pdb::get($table, $record_id);
  264.  
  265. // Look up all dependent foreign key relationships
  266. $deps = [];
  267. $base_tables = [$table];
  268. do {
  269. $new_base_tables = [];
  270. foreach ($base_tables as $base_table) {
  271. if (array_key_exists($base_table, $table_dep_cache)) {
  272. $table_deps = $table_dep_cache[$base_table];
  273. } else {
  274. $table_deps = Pdb::getDependentKeys($base_table);
  275. $table_dep_cache[$base_table] = $table_deps;
  276. }
  277.  
  278. foreach ($table_deps as $dep) {
  279. $new_base_tables[] = $dep['table'];
  280. $deps[$base_table][] = $dep;
  281. }
  282. }
  283. $base_tables = array_unique($new_base_tables);
  284. } while (!empty($base_tables));
  285.  
  286. // Look up all dependent data
  287. $data = [$table => [$record_id => $record]];
  288. foreach ($deps as $base_table => $table_deps) {
  289. $ids = @array_keys($data[$base_table]);
  290. if (empty($ids)) continue;
  291.  
  292. foreach ($table_deps as $dep) {
  293. if (!isset($data[$dep['table']])) $data[$dep['table']] = [];
  294.  
  295. $params = [];
  296. $where = Pdb::buildClause([[$dep['column'], 'IN', $ids]], $params);
  297. $q = "SELECT * FROM ~{$dep['table']} WHERE {$where}";
  298. $res = Pdb::q($q, $params, 'pdo');
  299. foreach ($res as $row) {
  300. // N.B. some tables (e.g. *_cat_join) don't have an id column
  301. // Such tables can't have subrecords (since the dependency relationship works from
  302. // the id column), so it's fine to just use numeric array indexing on their records.
  303. // The restore/undelete function should ignore the value in the record_id column in
  304. // history_items, and just use what's saved in the data column.
  305. if (isset($row['id'])) {
  306. $data[$dep['table']][$row['id']] = $row;
  307. } else {
  308. $data[$dep['table']][] = $row;
  309. }
  310. }
  311. $res->closeCursor();
  312. }
  313. }
  314.  
  315. Pdb::delete($table, ['id' => $record_id]);
  316. $log_id = $this->logDelete($table, $record_id, $record, $parent_log_id);
  317.  
  318. if ($parent_log_id == 0) $parent_log_id = $log_id;
  319.  
  320. // Log deletion of per-record permissions
  321. if ($this instanceof ManagedAdminController) {
  322. $params = [];
  323. $conds = ['controller' => $this->getControllerName(), 'item_id' => $record_id];
  324. $where = Pdb::buildClause($conds, $params);
  325.  
  326. $q = "SELECT * FROM ~per_record_permissions WHERE {$where}";
  327. $perms = Pdb::q($q, $params, 'arr');
  328. if (count($perms) > 0) {
  329. $perms = Sprout::iterableFirstValue($perms);
  330. Pdb::delete('per_record_permissions', ['id' => $perms['id']]);
  331. $this->logDelete('per_record_permissions', $perms['id'], $perms, $parent_log_id);
  332. }
  333. }
  334.  
  335. // Log all deleted dependent data
  336. foreach ($data as $data_table => $data_rows) {
  337. if ($data_table == $table) continue;
  338.  
  339. foreach ($data_rows as $data_id => $data_row) {
  340. $this->logDelete($data_table, $data_id, $data_row, $parent_log_id);
  341. }
  342. }
  343.  
  344. } else {
  345. $log_id = 0;
  346. Pdb::delete($table, ['id' => $record_id]);
  347.  
  348. if ($this instanceof ManagedAdminController) {
  349. $where = ['controller' => $this->getControllerName(), 'item_id' => $record_id];
  350. Pdb::delete('per_record_permissions', $where);
  351. }
  352. }
  353.  
  354. if (!$extant_transaction) Pdb::commit();
  355.  
  356. return $log_id;
  357. }
  358.  
  359.  
  360. /**
  361.   * Loads a config file for a JsonForm associated with this controller
  362.   * @param string $file_name The config file name, e.g. 'register.json'
  363.   * @return array
  364.   * @throws FileMissingException If the file is missing
  365.   * @throws Exception If the file is invalid
  366.   */
  367. protected function loadFormJson($file_name)
  368. {
  369. $conf_file = $this->getModulePath() . '/' . $file_name;
  370.  
  371. if (!file_exists($conf_file)) {
  372. throw new FileMissingException("Missing JSON file: {$conf_file}");
  373. } else if (filesize($conf_file) == 0) {
  374. throw new Exception("Empty JSON file");
  375. }
  376.  
  377. $conf = file_get_contents($conf_file);
  378. $conf = json_decode($conf, true);
  379. if ($conf === null) {
  380. throw new Exception("Invalid JSON -- " . json_last_error_msg());
  381. }
  382. return $conf;
  383. }
  384.  
  385.  
  386. /**
  387.   * Loads a JSON config file for an automated edit-type form for this controller
  388.   * @return array
  389.   * @throws Exception If the file is missing or invalid
  390.   */
  391. protected function loadEditJson()
  392. {
  393. $full_class = get_called_class();
  394. $class = Sprout::removeNs($full_class);
  395. $class = preg_replace('/Controller$/', '', $class);
  396. $class = Text::camel2lc($class);
  397.  
  398. return $this->loadFormJson("{$class}_edit.json");
  399. }
  400.  
  401.  
  402. /**
  403.   * Generates a form view from a JSON config file
  404.   * @return array
  405.   * @throws Exception If the file is missing or invalid
  406.   */
  407. protected function generateFormView($file_name)
  408. {
  409. $conf = $this->loadFormJson($file_name);
  410. $view = new View('sprout/auto_edit');
  411. $view->config = $conf;
  412. return $view;
  413. }
  414.  
  415.  
  416. /**
  417.   * Do any additional validation prior to saving the record
  418.   *
  419.   * @param int $id Record ID or 0 for adds
  420.   * @param Validator $validator Validator instance to attach your errors to
  421.   * @return void
  422.   */
  423. protected function jsonExtraValidate($id, Validator $validator)
  424. {
  425. // The default implementation is empty
  426. }
  427.  
  428.  
  429. /**
  430.   * Auto-set the "empty" param for fields with foreign keys to be NULL
  431.   *
  432.   * This greatly reduces the number of foreign-key constraints hit in day-to-day use,
  433.   * especially with file fields.
  434.   *
  435.   * @param array $conf Json form configuration
  436.   * @return null Array $conf is altered in-place
  437.   */
  438. protected function autoSetEmptyParam(array &$conf)
  439. {
  440. // Find FKs on main table, set empty to null
  441. $fk_cols = [];
  442. $fks = Pdb::getForeignKeys($this->table_name);
  443. foreach ($fks as $row) {
  444. $fk_cols[] = $row['source_column'];
  445. }
  446.  
  447. // Iterate and do two tasks: Set empty params; collate multiedits for processing below
  448. $multiedits = [];
  449. foreach ($conf as $tab => &$items) {
  450. if (is_array($items)) {
  451. if (count($fk_cols)) {
  452. JsonForm::setParameterForColumns($items, $fk_cols, 'empty', null);
  453. }
  454. foreach ($items as &$item) {
  455. if (isset($item['multiedit'])) {
  456. $multiedits[] = &$item['multiedit'];
  457. }
  458. }
  459. }
  460. }
  461. unset($items);
  462.  
  463. // Iterate multiedits and do the same processing
  464. foreach ($multiedits as &$multi) {
  465. $fk_cols = [];
  466. $fks = Pdb::getForeignKeys($multi['table']);
  467. foreach ($fks as $row) {
  468. $fk_cols[] = $row['source_column'];
  469. }
  470. if (count($fk_cols)) {
  471. JsonForm::setParameterForColumns($multi['items'], $fk_cols, 'empty', null);
  472. }
  473. }
  474. }
  475.  
  476.  
  477. /**
  478.   * Automatically saves the data associated with a submission on a JSON-generated form
  479.   * @param array $conf Config loaded from a JSON file
  480.   * @param int $item_id Database ID of record to store data in. If zero, a new record will be inserted, and as this
  481.   * argument is a reference, it will be updated with the auto-increment ID generated by the insert.
  482.   * @param string $mode Mode: 'add', 'edit', or something custom (e.g. 'duplicate', 'verify'). If blank,
  483.   * 'add' or 'edit' will be automatically determined, based on $item_id
  484.   * @return bool True if the save succeeded. If false is returned, errors will be saved in $_SESSION
  485.   */
  486. protected function saveJsonData(array $conf, &$item_id, $mode = '')
  487. {
  488. $item_id = (int) $item_id;
  489.  
  490. $session_key = 'public';
  491. if ($this instanceof ManagedAdminController) {
  492. $session_key = 'admin';
  493. }
  494.  
  495. $_SESSION[$session_key]['field_values'] = Validator::trim($_POST);
  496.  
  497. if ($mode == '') $mode = ($item_id == 0 ? 'add' : 'edit');
  498. $validator = new Validator($_POST);
  499. $this->autoSetEmptyParam($conf);
  500. list($data, $errs) = JsonForm::collateData($conf, $mode, $validator, $item_id);
  501.  
  502. $this->jsonExtraValidate($item_id, $validator);
  503. $errs = array_merge($errs, $validator->getFieldErrors());
  504.  
  505. $_SESSION[$session_key]['field_errors'] = $errs;
  506. if (count($errs) > 0) {
  507. $validator->createNotifications();
  508. return false;
  509. }
  510.  
  511. $this->_preSave($item_id, $data);
  512.  
  513. $was_in_transaction = true;
  514. if (!Pdb::inTransaction()) {
  515. $was_in_transaction = false;
  516. Pdb::transact();
  517. }
  518.  
  519. // Main insert/update, then log the action
  520. $base_data = [];
  521. foreach ($data as $key => $val) {
  522. if ($key == 'categories') continue;
  523. if (substr($key, 0, 9) == 'multiedit') continue;
  524. $base_data[$key] = $val;
  525. }
  526. if ($item_id <= 0) {
  527. $item_id = Pdb::insert($this->table_name, $base_data);
  528. $this->logAdd($this->table_name, $item_id);
  529. } else {
  530. $log_data = $this->loadRecord($this->table_name, $item_id);
  531. Pdb::update($this->table_name, $base_data, ['id' => $item_id]);
  532. $this->logEdit($this->table_name, $item_id, $log_data);
  533. }
  534.  
  535. // Update the categories
  536. if (isset($data['categories'])) {
  537. $this->updateCategories($item_id, $data['categories']);
  538. }
  539.  
  540. // Update multiedits
  541. $id_field = Inflector::singular($this->table_name) . '_id';
  542. foreach ($conf as $tab_name => $tab) {
  543. if (!is_array($tab)) continue;
  544. foreach ($tab as $item) {
  545. if (!isset($item['multiedit'])) continue;
  546.  
  547. $multed = $item['multiedit'];
  548. $table = $multed['table'];
  549. $multi_data_key = 'multiedit_' . $multed['id'];
  550. $link_column = !empty($multed['link']) ? $multed['link'] : $id_field;
  551.  
  552. $conditions = [];
  553. if (isset($multed['where'])) $conditions = $multed['where'];
  554. $conditions[] = [$link_column, '=', $item_id];
  555.  
  556. if (isset($data[$multi_data_key])) {
  557. $defaults = [];
  558. $field_defns = JsonForm::flattenGroups($multed['items']);
  559. foreach ($field_defns as $field) {
  560. if (array_key_exists('default', $field)) {
  561. $defaults[$field['name']] = $field['default'];
  562. }
  563. }
  564.  
  565. $record_order = 0;
  566. $new_set = $data[$multi_data_key];
  567. foreach ($new_set as $key => &$new_rec) {
  568. // Skip blank records where user hasn't entered any data
  569. if (MultiEdit::recordEmpty($new_rec, $defaults)) {
  570. unset($new_set[$key]);
  571. continue;
  572. }
  573.  
  574. if ($multed['reorder']) {
  575. $new_rec['record_order'] = $record_order++;
  576. }
  577.  
  578. if (!isset($new_rec[$link_column])) {
  579. $new_rec[$link_column] = $item_id;
  580. }
  581. }
  582. } else {
  583. $new_set = [];
  584. }
  585.  
  586. $this->replaceSet($table, $new_set, $conditions);
  587. }
  588. }
  589.  
  590. // Update autofill_lists
  591. foreach ($conf as $tab_name => $tab) {
  592. if (!is_array($tab)) continue;
  593. foreach ($tab as $item) {
  594. if (!isset($item['autofill_list'])) continue;
  595.  
  596. $auto = $item['autofill_list'];
  597. $auto = JsonForm::autofillOptionDefaults($auto, $this->table_name);
  598.  
  599. Pdb::validateIdentifier($auto['joiner_local_col']);
  600. Pdb::validateIdentifier($auto['joiner_foreign_col']);
  601. Pdb::validateIdentifier($auto['joiner_table']);
  602.  
  603. // Post data for this field may be empty if nothing selected
  604. if (isset($_POST[$auto['name']])) {
  605. $postdata = $_POST[$auto['name']];
  606. } else {
  607. $postdata = [];
  608. }
  609.  
  610. // It's safe to do a nuke-then-insert as there isn't IDs to keep stable
  611. // and this code runs within a transaction
  612. $conditions = [
  613. $auto['joiner_local_col'] => $item_id
  614. ];
  615. Pdb::delete($auto['joiner_table'], $conditions);
  616.  
  617. $record_order = 0;
  618. $foreign_ids = [];
  619. foreach ($postdata as $postrow) {
  620. if (in_array($postrow['id'], $foreign_ids)) {
  621. continue; // ignore duplicates
  622. }
  623.  
  624. $data = [];
  625. $data[$auto['joiner_local_col']] = $item_id;
  626. $data[$auto['joiner_foreign_col']] = $postrow['id'];
  627. if ($auto['reorder']) {
  628. $data['record_order'] = ++$record_order;
  629. }
  630. Pdb::insert($auto['joiner_table'], $data);
  631. $foreign_ids[] = $postrow['id'];
  632. }
  633. }
  634. }
  635.  
  636. if (!$was_in_transaction) Pdb::commit();
  637.  
  638. unset($_SESSION[$session_key]['field_values']);
  639. unset($_SESSION[$session_key]['field_errors']);
  640.  
  641. return true;
  642. }
  643.  
  644.  
  645. /**
  646.   * Inserts records which are in $new_records, but are not in the specified table
  647.   * Deletes records which are in the specified table, but not in $new_records
  648.   * Updates records which are in both
  649.   *
  650.   * Matching is done on the 'id' field, as a result the $new_records arrays MUST contain an id field.
  651.   *
  652.   * @param string $table The table name. Do not include prefix.
  653.   * @param array $new_records The new records for the table. Should be an array of arrays,
  654.   * With each sub-array being the arguments which would normally be passed to Pdb::insert or Pdb::update
  655.   * @param string $conditions A where clause to use when looking to see what records already exist. {@see Pdb::buildClause}
  656.   * @return void
  657.   * @throws QueryException
  658.   */
  659. protected function replaceSet($table, $new_records, array $conditions)
  660. {
  661. Pdb::validateIdentifier($table);
  662.  
  663. $values = [];
  664. $select_conditions = $conditions;
  665. $q = "SELECT id FROM ~{$table} WHERE " . Pdb::buildClause($select_conditions, $values);
  666. $existing = Pdb::q($q, $values, 'arr');
  667.  
  668. // If existing record found, update, otherwise delete
  669. $delete_list = [];
  670. foreach ($existing as $old) {
  671. $found = null;
  672. foreach ($new_records as $new_idx => $new) {
  673. if ($old['id'] == @$new['id']) {
  674. $found = $new;
  675. unset($new_records[$new_idx]);
  676. break;
  677. }
  678. }
  679.  
  680. $replace_conditions = $conditions;
  681. $replace_conditions['id'] = $old['id'];
  682. if ($found) {
  683. unset($found['id']);
  684. $found['date_modified'] = Pdb::now();
  685. try {
  686. Pdb::update($table, $found, $replace_conditions);
  687. } catch (QueryException $ex) {
  688. unset($found['date_modified']);
  689. Pdb::update($table, $found, $replace_conditions);
  690. }
  691. } else {
  692. $this->deleteRecord($table, $replace_conditions['id']);
  693. }
  694. }
  695.  
  696. // Anything not updated or deleted gets added
  697. foreach ($new_records as $fields) {
  698. $fields['date_added'] = Pdb::now();
  699. $fields['date_modified'] = Pdb::now();
  700. try {
  701. Pdb::insert($table, $fields);
  702. } catch (QueryException $ex) {
  703. unset($fields['date_added']);
  704. unset($fields['date_modified']);
  705. Pdb::insert($table, $fields);
  706. }
  707. }
  708. }
  709.  
  710.  
  711. } // End Controller Class
  712.