SproutCMS

This is the code documentation for the SproutCMS project

source of /sprout/Controllers/Admin/HasCategoriesAdminController.php

  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.  
  14. namespace Sprout\Controllers\Admin;
  15.  
  16. use InvalidArgumentException;
  17.  
  18. use Sprout\Exceptions\FileMissingException;
  19. use karmabunny\pdb\Exceptions\RowMissingException;
  20. use Sprout\Helpers\Admin;
  21. use Sprout\Helpers\AdminError;
  22. use Sprout\Helpers\AdminPerms;
  23. use Sprout\Helpers\Category;
  24. use Sprout\Helpers\Constants;
  25. use Sprout\Helpers\Csrf;
  26. use Sprout\Helpers\Enc;
  27. use Sprout\Helpers\Inflector;
  28. use Sprout\Helpers\Itemlist;
  29. use Sprout\Helpers\JsonForm;
  30. use Sprout\Helpers\Notification;
  31. use Sprout\Helpers\Pdb;
  32. use Sprout\Helpers\RefineWidgetSelect;
  33. use Sprout\Helpers\Router;
  34. use Sprout\Helpers\Url;
  35. use Sprout\Helpers\View;
  36.  
  37.  
  38. /**
  39. * An abstract class for controllers of things which have categories.
  40. **/
  41. abstract class HasCategoriesAdminController extends ManagedAdminController {
  42. protected $controller_name;
  43. protected $friendly_name;
  44. protected $add_defaults = array();
  45. protected $db;
  46. protected $main_columns;
  47.  
  48.  
  49. /**
  50.   * Enables re-ordering for categories.
  51.   * You will need a "record_order" column on the category table.
  52.   **/
  53. protected $category_reorder = false;
  54.  
  55. /**
  56.   * Enables single-cat mode.
  57.   * This uses radiobuttons instead of checkboxes in the category selection UI.
  58.   **/
  59. protected $category_single = false;
  60.  
  61. /**
  62.   * Do we have the 'archive' feature for categories?
  63.   * You will need a "show_admin TINYINT UNSIGNED DEFAULT 1" column on the category table.
  64.   **/
  65. protected $category_archive = false;
  66.  
  67.  
  68. /**
  69.   * Constructor
  70.   **/
  71. public function __construct()
  72. {
  73. if (! $this->main_columns) {
  74. $this->main_columns = array('Name' => 'name');
  75. }
  76.  
  77. // Add refine fields
  78. $this->initTableName();
  79. $records = Pdb::lookup("{$this->table_name}_cat_list");
  80. $records[0] = 'Uncategorised';
  81.  
  82. $this->initRefineBar();
  83. $this->refine_bar->addWidget(new RefineWidgetSelect('_category_id', 'Category', $records));
  84.  
  85. parent::__construct();
  86. }
  87.  
  88.  
  89. /**
  90.   * Returns TRUE if category archive is enabled, FALSE otherwise
  91.   **/
  92. public final function getCategoryArchive() {
  93. return $this->category_archive;
  94. }
  95.  
  96.  
  97. /**
  98.   * Return true if categories are allowed to be added.
  99.   **/
  100. public function catAllowAdd()
  101. {
  102. return true;
  103. }
  104.  
  105.  
  106. /**
  107.   * Return true if categories are allowed to be edited.
  108.   **/
  109. public function catAllowEdit($category_id)
  110. {
  111. return true;
  112. }
  113.  
  114.  
  115. /**
  116.   * Return true if categories are allowed to be deleted.
  117.   **/
  118. public function catAllowDelete($category_id)
  119. {
  120. return true;
  121. }
  122.  
  123.  
  124. /**
  125.   * Returns the contents of the navigation pane
  126.   **/
  127. public function _getNavigation()
  128. {
  129. $joiner_ref_col = Category::columnMain2joiner($this->table_name);
  130.  
  131. $where = '1';
  132. $columns = [];
  133. $columns[] = 'categories.id';
  134. $columns[] = 'categories.name';
  135. $columns[] = "COUNT(joiner.{$joiner_ref_col}) AS num_items";
  136.  
  137. if ($this->category_archive) {
  138. // GET param or SESSION param
  139. $columns[] = 'categories.show_admin';
  140.  
  141. $_GET['category_type'] = (int) @$_GET['category_type'];
  142. if (! $_GET['category_type']) {
  143. $_GET['category_type'] = @$_SESSION['admin']['category_type'];
  144. }
  145.  
  146. // Default
  147. if (! $_GET['category_type']) {
  148. $_GET['category_type'] = Constants::CATEGORIES_CURRENT;
  149. }
  150.  
  151. // Where clause and set session
  152. $clause = Constants::$category_admin_where[$_GET['category_type']];
  153. if ($clause) {
  154. $where = $clause;
  155. $_SESSION['admin']['category_type'] = $_GET['category_type'];
  156. }
  157. }
  158.  
  159. // Get the category names, and the number of items in each
  160. $columns = implode(', ', $columns);
  161. $q = "SELECT {$columns}
  162. FROM ~{$this->table_name}_cat_list AS categories
  163. LEFT JOIN ~{$this->table_name}_cat_join AS joiner
  164. ON categories.id = joiner.cat_id
  165. WHERE {$where}
  166. GROUP BY categories.id
  167. ORDER BY " . ($this->category_reorder ? 'categories.record_order, categories.name' : 'categories.name');
  168. $res = Pdb::q($q, [], 'arr');
  169.  
  170. // Get the number of items which don't have any categories
  171. $where = implode(' AND ', $this->main_where);
  172. if (!$where) $where = '1';
  173. $q = "SELECT COUNT(item.id) AS c
  174. FROM ~{$this->table_name} AS item
  175. LEFT JOIN ~{$this->table_name}_cat_join AS joiner ON joiner.{$joiner_ref_col} = item.id
  176. WHERE joiner.{$joiner_ref_col} IS NULL
  177. AND {$where}";
  178. $uncat = (int) Pdb::q($q, [], 'val');
  179.  
  180. // If there were any, create an 'uncategorised' meta-category
  181. if ($uncat) {
  182. $res[] = array(
  183. 'id' => '0',
  184. 'name' => 'Uncategorised',
  185. 'num_items' => $uncat
  186. );
  187. }
  188.  
  189. // Create the view and populate it with data
  190. $view = new View('sprout/admin/categories_navigation');
  191. $view->controller_name = $this->controller_name;
  192. $view->friendly_name = $this->friendly_name;
  193. $view->category_reorder = $this->category_reorder;
  194. $view->category_archive = $this->category_archive;
  195. $view->categories = $res;
  196. $view->main_add = $this->main_add;
  197.  
  198. if ($this->category_archive) {
  199. $view->category_archive_type = $_GET['category_type'];
  200. }
  201.  
  202. if (Router::$method == 'contents' or Router::$method == 'export') {
  203. $view->export_refine = '?' . $_SERVER['QUERY_STRING'];
  204. }
  205.  
  206. return $view->render();
  207. }
  208.  
  209.  
  210. /**
  211.   * Additional categry tools
  212.   **/
  213. public function _getTools()
  214. {
  215. $tools = parent::_getTools();
  216.  
  217. if ($this->category_reorder) {
  218. $tools['reorder'] = "<li class=\"reorder\"><a href=\"admin/extra/{$this->controller_name}_category/reorder_categories\">Reorder Categories</a></li>";
  219. }
  220.  
  221. return $tools;
  222. }
  223.  
  224.  
  225. /**
  226.   * Return the WHERE clause to use for a given key which is provided by the RefineBar
  227.   *
  228.   * Allows custom non-table clauses to be added.
  229.   * Is only called for key names which begin with an underscore.
  230.   * The base table is aliased to 'item'.
  231.   *
  232.   * @param string $key The key name, including underscore
  233.   * @param string $val The value which is being refined.
  234.   * @param array &$query_params Parameters to add to the query which will use the WHERE clause
  235.   * @return string WHERE clause, e.g. "item.name LIKE CONCAT('%', ?, '%')", "item.status IN (?, ?, ?)"
  236.   */
  237. protected function _getRefineClause($key, $val, array &$query_params)
  238. {
  239. $joiner_ref_col = Category::columnMain2joiner($this->table_name);
  240.  
  241. switch ($key) {
  242. case '_category_id':
  243. if ($val == 0) {
  244. // Uncategoried
  245. return "(SELECT 1 FROM ~{$this->table_name}_cat_join AS joiner
  246. WHERE joiner.{$joiner_ref_col} = item.id LIMIT 1) IS NULL";
  247. }
  248.  
  249. $query_params[] = $val;
  250. return "(SELECT 1 FROM ~{$this->table_name}_cat_join AS joiner
  251. WHERE joiner.{$joiner_ref_col} = item.id AND joiner.cat_id = ? LIMIT 1) = 1";
  252. }
  253.  
  254. return parent::_getRefineClause($key, $val, $query_params);
  255. }
  256.  
  257.  
  258. /**
  259.   * Returns the main list of records for this controller
  260.   **/
  261. public function _getContents()
  262. {
  263. if (! isset($_GET['page'])) $_GET['page'] = '1';
  264. $_GET['page'] = (int) $_GET['page'];
  265.  
  266. // Apply filter
  267. list($where, $params) = $this->applyRefineFilter();
  268.  
  269. // Apply category filter
  270. if (@$_GET['_category_id'] == '0') {
  271. $title = 'Uncategorised ' . $this->friendly_name;
  272. $category = null;
  273.  
  274. } else if (empty($_GET['_category_id'])) {
  275. $title = 'All ' . $this->friendly_name;
  276. $category = null;
  277.  
  278. } else {
  279. // All regular categories
  280. $_GET['_category_id'] = (int) $_GET['_category_id'];
  281. $q = "SELECT * FROM ~{$this->table_name}_cat_list WHERE id = ?";
  282. $category = Pdb::q($q, [$_GET['_category_id']], 'row');
  283. $title = $this->friendly_name . ' category <strong>' . Enc::html($category['name']) . '</strong>';
  284. }
  285.  
  286. // Build the where clause
  287. $has_refine = (bool) count($where);
  288. if ($this->main_where) $where = array_merge($where, $this->main_where);
  289. $where = implode(' AND ', $where);
  290. if ($where == '') $where = '1';
  291.  
  292. // Determine record order
  293. $_GET['order'] = preg_replace('/[^_a-z0-9]/', '', @$_GET['order']);
  294. if (!empty($_GET['order'])) {
  295. Pdb::validateIdentifier($_GET['order']);
  296. $order = "item.{$_GET['order']}";
  297. if (@$_GET['dir'] == 'asc' or @$_GET['dir'] == 'desc') {
  298. $order .= ' ' . $_GET['dir'];
  299. } else {
  300. $_GET['dir'] = 'asc';
  301. }
  302.  
  303. } else if (isset($_GET['_category_id'])) {
  304. $joiner_table = Category::tableMain2joiner($this->table_name);
  305. $joiner_ref_col = Category::columnMain2joiner($this->table_name);
  306.  
  307. $cat_id = (int) $_GET['_category_id'];
  308. $order = "(SELECT record_order
  309. FROM ~{$joiner_table}
  310. WHERE {$joiner_ref_col} = item.id AND cat_id = ?
  311. LIMIT 1)";
  312. $params[] = $cat_id;
  313.  
  314. } else {
  315. $order = $this->main_order;
  316. preg_match('/(item\.)?([_a-z]+)( asc| desc)?/i', $this->main_order, $matches);
  317. $_GET['order'] = trim($matches[2]);
  318. $_GET['dir'] = trim(!empty($matches[3]) ? strtolower($matches[3]) : 'asc');
  319. }
  320.  
  321. // Get the actual records
  322. $offset = $this->records_per_page * ($_GET['page'] - 1);
  323. $q = $this->_getContentsQuery($where, $order, $params);
  324. $q .= " LIMIT {$this->records_per_page} OFFSET {$offset}";
  325. $items = Pdb::q($q, $params, 'arr');
  326.  
  327. // Get the total number of records
  328. $total_row_count = Pdb::q("SELECT FOUND_ROWS()", [], 'val');
  329.  
  330.  
  331. // If no mode set, use the session
  332. // If a mode is set and valid, save in the session
  333. if (empty($_GET['main_mode'])) {
  334. $_GET['main_mode'] = @$_SESSION['admin'][$this->controller_name]['main_mode'];
  335. } else if ($this->main_modes[$_GET['main_mode']]) {
  336. $_SESSION['admin'][$this->controller_name]['main_mode'] = $_GET['main_mode'];
  337. }
  338.  
  339. // If no valid mode set, use a default
  340. if (empty($this->main_modes[$_GET['main_mode']])) {
  341. $_GET['main_mode'] = key($this->main_modes);
  342. }
  343.  
  344. // Build the refine bar
  345. if ($this->refine_bar) {
  346. $refine = $this->refine_bar->get();
  347. } else {
  348. $refine = '';
  349. }
  350.  
  351. // Build the mode selector ui
  352. if (count($this->main_modes) > 1) {
  353. $mode_sel = $this->_modeSelector($_GET['main_mode']);
  354. } else {
  355. $mode_sel = '';
  356. }
  357.  
  358. // If there is no records, tell the user
  359. if ($total_row_count == 0) {
  360. if ($has_refine) {
  361. $items_view = '<p>No records were found which match the specified refinements.</p>';
  362. } else {
  363. $items_view = '<p>No records currently exist in the database.</p>';
  364. }
  365. } else {
  366. $items_view = $this->_getContentsView($items, $_GET['main_mode'], $category);
  367. }
  368.  
  369. // Build the pagination bar
  370. if ($total_row_count > $this->records_per_page) {
  371. $paginate = $this->_paginationBar($_GET['page'], $total_row_count);
  372. } else {
  373. $paginate = '';
  374. }
  375.  
  376. return array(
  377. 'title' => $title,
  378. 'content' => $refine . $mode_sel . $items_view . $paginate,
  379. );
  380. }
  381.  
  382.  
  383. /**
  384.   * Return HTML for a resultset of items
  385.   * The returned HTML will be sandwiched between the refinebar and the pagination bar.
  386.   *
  387.   * @param Traversable $items The items to render.
  388.   * @param string $mode The mode of the display.
  389.   * @param StdClass $category Category details if a category has been selected.
  390.   **/
  391. public function _getContentsView($items, $mode, $category)
  392. {
  393. return $this->_getContentsViewList($items, $category);
  394. }
  395.  
  396.  
  397. /**
  398.   * Formats a resultset of items into an Itemlist
  399.   *
  400.   * @param Traversable $items The items to render.
  401.   * @param StdClass $category Category details if a category has been selected.
  402.   **/
  403. public function _getContentsViewList($items, $category)
  404. {
  405. // Create the itemlist
  406. $itemlist = new Itemlist();
  407. $itemlist->main_columns = $this->main_columns;
  408. $itemlist->items = $items;
  409. $itemlist->setCheckboxes(true);
  410. $itemlist->setOrdering(true);
  411. $itemlist->setActionsClasses('button button-small');
  412.  
  413. // Add the actions
  414. $itemlist->addAction('edit', "SITE/admin/edit/{$this->controller_name}/%%");
  415. foreach ($this->main_actions as $name => $url) {
  416. $itemlist->addAction($name, $url, 'button-grey');
  417. }
  418. if ($this->getDuplicateEnabled()) {
  419. $itemlist->addAction('Duplicate', "SITE/admin/duplicate/{$this->controller_name}/%%", 'button-grey icon-before icon-add');
  420. }
  421. if ($this->main_delete) {
  422. $itemlist->addAction('Delete', "SITE/admin/delete/{$this->controller_name}/%%", 'button button-red icon-before icon-delete');
  423. }
  424.  
  425. // Add classes based on visibility fields
  426. $visibility = $this->_getVisibilityFields();
  427. $itemlist->setRowClassesFunc(function($row) use($visibility) {
  428. $out = '';
  429. foreach ($visibility as $name => $label) {
  430. $out .= "main-list--{$name}-{$row[$name]} ";
  431. }
  432. return rtrim($out);
  433. });
  434.  
  435. // Prepare view which renders the main content area
  436. $outer = new View("sprout/admin/categories_itemlist_outer");
  437.  
  438. // Build the outer view
  439. $outer->controller_name = $this->controller_name;
  440. $outer->friendly_name = $this->friendly_name;
  441. $outer->category_reorder = $this->category_reorder;
  442. $outer->itemlist = $itemlist->render();
  443. $outer->allow_add = $this->main_add;
  444. $outer->allow_del = $this->main_delete;
  445. $outer->category = $category;
  446.  
  447. return $outer->render();
  448. }
  449.  
  450.  
  451. /**
  452.   * Called when the import form is being built.
  453.   *
  454.   * Returns HTML of extra options to display, or null if no extra options.
  455.   **/
  456. protected function _importExtraOptions()
  457. {
  458. $view = new View('sprout/admin/categories_import_options');
  459.  
  460. // Get the categories
  461. $cats_table = Category::tableMain2cat($this->table_name);
  462. $q = "SELECT category.id, category.name
  463. FROM ~{$cats_table} AS category
  464. ORDER BY category.name";
  465. $view->cats = Pdb::q($q, [], 'map');
  466.  
  467. Admin::setCategoryTablename($cats_table);
  468. Admin::setCategorySinglecat($this->category_single);
  469.  
  470. return $view;
  471. }
  472.  
  473.  
  474. /**
  475.   * Called after a record has been inserted or updated.
  476.   *
  477.   * @param int $record_id The id of the record that was inserted or updated.
  478.   * @param array $new_data The new data of the record.
  479.   * @param array $existing_record The old data of the record, which has now been replaced.
  480.   * @param string $type One of 'insert' or 'update'.
  481.   * @param boolean False if any errors are encountered; will cancel the entire import process.
  482.   **/
  483. protected function _importPostRecord($record_id, $new_data, $existing_record, $type, $raw_data)
  484. {
  485. if (! parent::_importPostRecord ($record_id, $new_data, $existing_record, $type, $raw_data)) return false;
  486.  
  487. if (@count($_POST['categories'])) {
  488. foreach ($_POST['categories'] as $cat_id) {
  489. Category::insertInto($this->table_name, $record_id, $cat_id);
  490. }
  491. }
  492.  
  493. return true;
  494. }
  495.  
  496. /**
  497.   * Returns a page title and HTML for a form to add a record
  498.   * @return array Two elements: 'title' and 'content'
  499.   */
  500. public function _getAddForm()
  501. {
  502. if (is_array($this->add_defaults)) {
  503. $data = $this->add_defaults;
  504. } else {
  505. $data = [];
  506. }
  507.  
  508. if (!empty($_SESSION['admin']['field_values'])) {
  509. $data = $_SESSION['admin']['field_values'];
  510. unset($_SESSION['admin']['field_values']);
  511. }
  512.  
  513. $errors = [];
  514. if (!empty($_SESSION['admin']['field_errors'])) {
  515. $errors = $_SESSION['admin']['field_errors'];
  516. unset($_SESSION['admin']['field_errors']);
  517. }
  518.  
  519. Admin::setCategoryTablename("{$this->table_name}_cat_list");
  520. Admin::setCategorySinglecat($this->category_single);
  521.  
  522. // Get the categories
  523. $q = "SELECT category.id, category.name
  524. FROM ~{$this->table_name}_cat_list AS category
  525. ORDER BY category.name";
  526. $cats = Pdb::q($q, [], 'map');
  527.  
  528. if (!isset($data['categories'])) $data['categories'] = [];
  529. if (!empty($_GET['category_id'])) {
  530. $data['categories'][] = (int) $_GET['category_id'];
  531. }
  532.  
  533. // Auto-generate form from JSON where possible
  534. $conf = false;
  535. try {
  536. $conf = $this->loadEditJson();
  537. $view = new View('sprout/auto_edit');
  538. $view->id = 0;
  539. $view->config = $conf;
  540.  
  541. } catch (FileMissingException $ex) {
  542. $view_dir = $this->getModulePath();
  543. $view = new View("{$view_dir}/admin/{$this->controller_name}_add");
  544. }
  545.  
  546. $view->controller_name = $this->controller_name;
  547. $view->friendly_name = $this->friendly_name;
  548. $view->data = $data;
  549. $view->errors = $errors;
  550. $view->cats = $cats;
  551.  
  552. $this->_addPreRender($view);
  553.  
  554. return array(
  555. 'title' => 'Adding ' . Enc::html(Inflector::singular($this->friendly_name)),
  556. 'content' => $view->render()
  557. );
  558. }
  559.  
  560.  
  561. /**
  562.   * Returns the edit form for adding a record
  563.   *
  564.   * @param int $id The id of the record to get the edit form of
  565.   **/
  566. public function _getEditForm($id)
  567. {
  568. $id = (int) $id;
  569.  
  570. $joiner_ref_col = Category::columnMain2joiner($this->table_name);
  571.  
  572. // Get the item
  573. $q = "SELECT * FROM ~{$this->table_name} WHERE id = ?";
  574. try {
  575. $item = Pdb::q($q, [$id], 'row');
  576. } catch (RowMissingException $ex) {
  577. $single = Inflector::singular($this->friendly_name);
  578. return new AdminError("Invalid id specified - {$single} does not exist");
  579. }
  580.  
  581. $data = $item;
  582.  
  583. Admin::setCategoryTablename("{$this->table_name}_cat_list");
  584. Admin::setCategorySinglecat($this->category_single);
  585.  
  586. // Get the categories
  587. $q = "SELECT category.id, category.name
  588. FROM ~{$this->table_name}_cat_list AS category
  589. ORDER BY category.name";
  590. $cats = Pdb::q($q, [], 'map');
  591.  
  592. // Get the selected categories
  593. if (!isset($data['categories'])) {
  594. $q = "SELECT cat_id
  595. FROM ~{$this->table_name}_cat_join
  596. WHERE {$joiner_ref_col} = ?";
  597. $data['categories'] = Pdb::q($q, [$id], 'col');
  598. }
  599.  
  600. // Auto-generate form from JSON where possible
  601. $conf = false;
  602. try {
  603. $conf = $this->loadEditJson();
  604. $view = new View('sprout/auto_edit');
  605. $view->config = $conf;
  606.  
  607. $default_link = Inflector::singular($this->table_name) . '_id';
  608. $data = array_merge($data, JsonForm::loadMultiEditData($conf, $default_link, $id, []));
  609. $data = array_merge($data, JsonForm::loadAutofillListData($conf, $this->table_name, $id, []));
  610. } catch (FileMissingException $ex) {
  611. $view_dir = $this->getModulePath();
  612. $view = new View("{$view_dir}/admin/{$this->controller_name}_edit");
  613. }
  614.  
  615. // Overlay session data
  616. if (!empty($_SESSION['admin']['field_values'])) {
  617. $data = array_merge($data, $_SESSION['admin']['field_values']);
  618. unset ($_SESSION['admin']['field_values']);
  619. }
  620.  
  621. $errors = [];
  622. if (!empty($_SESSION['admin']['field_errors'])) {
  623. $errors = $_SESSION['admin']['field_errors'];
  624. unset($_SESSION['admin']['field_errors']);
  625. }
  626.  
  627. $view->controller_name = $this->controller_name;
  628. $view->friendly_name = $this->friendly_name;
  629. $view->id = $id;
  630. $view->item = $item;
  631. $view->data = $data;
  632. $view->errors = $errors;
  633. $view->cats = $cats;
  634.  
  635. $this->_editPreRender($view, $id);
  636.  
  637. $title = 'Editing ' . Enc::html(Inflector::singular($this->friendly_name));
  638. return array(
  639. 'title' => $title . ' <strong>' . Enc::html($this->_identifier($item)) . '</strong>',
  640. 'content' => $view->render()
  641. );
  642. }
  643.  
  644. /**
  645.   * Returns the edit form for duplicating a record
  646.   *
  647.   * @param int $id The id of the record to get the data from
  648.   **/
  649. public function _getDuplicateForm($id)
  650. {
  651. $id = (int) $id;
  652. if ($id <= 0) throw new InvalidArgumentException('$id must be greater than 0');
  653.  
  654. // Get the item
  655. $q = "SELECT * FROM ~{$this->table_name} WHERE id = ?";
  656. $data = $item = Pdb::q($q, [$id], 'row');
  657.  
  658. Admin::setCategoryTablename("{$this->table_name}_cat_list");
  659. Admin::setCategorySinglecat($this->category_single);
  660.  
  661. // Get the categories
  662. $cat_table = Category::tableMain2cat($this->table_name);
  663. $q = "SELECT category.id, category.name
  664. FROM ~{$cat_table} AS category
  665. ORDER BY category.name";
  666. $cats = Pdb::q($q, [], 'map');
  667.  
  668. // Clobber duplication fields with any defaults defined in controller
  669. if (@count($this->duplicate_defaults)) {
  670. foreach ($this->duplicate_defaults as $key => $val) {
  671. $data[$key] = $val;
  672. }
  673. }
  674.  
  675. // Overlay session data
  676. if (!empty($_SESSION['admin']['field_values'])) {
  677. $data = array_merge($data, $_SESSION['admin']['field_values']);
  678. unset ($_SESSION['admin']['field_values']);
  679. }
  680.  
  681. $errors = [];
  682. if (!empty($_SESSION['admin']['field_errors'])) {
  683. $errors = $_SESSION['admin']['field_errors'];
  684. unset($_SESSION['admin']['field_errors']);
  685. }
  686.  
  687. // Get the selected categories
  688. if (empty($data['categories'])) {
  689. $data['categories'] = array();
  690. $q = "SELECT cat_id
  691. FROM ~{$this->table_name}_cat_join
  692. WHERE {$this->controller_name}_id = ?";
  693. $data['categories'] = Pdb::q($q, [$id], 'col');
  694. }
  695.  
  696. // Auto-generate form from JSON where possible
  697. $conf = false;
  698. try {
  699. $conf = $this->loadEditJson();
  700. $view = new View('sprout/auto_edit');
  701. $view->config = $conf;
  702.  
  703. $default_link = Inflector::singular($this->table_name) . '_id';
  704. $data = array_merge($data, JsonForm::loadMultiEditData($conf, $default_link, $id, []));
  705. $data = array_merge($data, JsonForm::loadAutofillListData($conf, $this->table_name, $id, []));
  706. } catch (FileMissingException $ex) {
  707. $view_dir = $this->getModulePath();
  708. $view = new View("{$view_dir}/admin/{$this->controller_name}_edit");
  709. }
  710. $view->controller_name = $this->controller_name;
  711. $view->friendly_name = $this->friendly_name;
  712. $view->id = $id;
  713. $view->item = $item;
  714. $view->data = $data;
  715. $view->errors = $errors;
  716. $view->cats = $cats;
  717.  
  718. $this->_duplicatePreRender($view, $id);
  719.  
  720. $title = 'Duplicating ' . Enc::html(Inflector::singular($this->friendly_name));
  721. return array(
  722. 'title' => $title . ' <strong>' . Enc::html($this->_identifier($item)) . '</strong>',
  723. 'content' => $view->render()
  724. );
  725. }
  726.  
  727.  
  728. /**
  729.   * Deletes an item and logs the deleted data
  730.   *
  731.   * This method should not be overridden unless absolutely necessary.
  732.   *
  733.   * @param int $item_id The record to delete
  734.   * @return bool True on success, false on failure
  735.   * @throws QueryException
  736.   */
  737. public function _deleteSave($item_id)
  738. {
  739. $item_id = (int) $item_id;
  740.  
  741. if (!$this->_isDeleteSaved($item_id)) return false;
  742.  
  743. // Start transaction
  744. $extant_transaction = Pdb::inTransaction();
  745. if (!$extant_transaction) Pdb::transact();
  746.  
  747. // Delete record
  748. $this->deleteRecord($this->table_name, $item_id);
  749.  
  750. // Delete categories
  751. // N.B. these will already have been deleted if the foreign keys are correctly defined
  752. $cat_table = Category::tableMain2joiner($this->table_name);
  753. $record_col = Category::columnMain2joiner($this->table_name);
  754. Pdb::delete($cat_table, [$record_col => $item_id]);
  755.  
  756. // Commit
  757. if (!$extant_transaction) Pdb::commit();
  758.  
  759. return true;
  760. }
  761.  
  762.  
  763. /**
  764.   * Updates the category table for this controller (so for articles, the updated table will be articles_cat_join)
  765.   * so that the records for the specified item match the category ids provided.
  766.   *
  767.   * @param array $categories A list of category-ids which the specified item should be associated with
  768.   * @param int $id The id of the item to set the categories for
  769.   * @return boolean True on success, false on failure
  770.   *
  771.   * @api
  772.   * @module-api
  773.   **/
  774. protected function updateCategories($item_id, $categories)
  775. {
  776. $item_id = (int) $item_id;
  777. if (! is_array($categories)) $categories = array();
  778.  
  779. $table_name = $this->table_name . '_cat_join';
  780. $item_column = $this->controller_name . '_id';
  781.  
  782. // Find out what is in the db
  783. $q = "SELECT cat_id FROM ~{$table_name} WHERE {$item_column} = ?";
  784. $res = Pdb::q($q, [$item_id], 'arr');
  785.  
  786. // If it's in the list, remove it from the list, otherwise mark for removal from the db
  787. $delete = array();
  788. foreach ($res as $row) {
  789. $idx = array_search($row['cat_id'], $categories);
  790. if ($idx === false) {
  791. $delete[] = $row['cat_id'];
  792. } else {
  793. unset ($categories[$idx]);
  794. }
  795. }
  796.  
  797. // Add everything that is in the list into the db
  798. if (@count($categories)) {
  799. foreach ($categories as $cat_id) {
  800. $cat_id = (int) $cat_id;
  801.  
  802. $update_fields = array();
  803. $update_fields[$item_column] = $item_id;
  804. $update_fields['cat_id'] = $cat_id;
  805.  
  806. Pdb::insert($table_name, $update_fields);
  807.  
  808. $this->logAddCategory(Inflector::plural($this->controller_name), $item_id, $cat_id);
  809. }
  810. }
  811.  
  812. // Remove everything from the delete list
  813. foreach ($delete as $cat_id) {
  814. $cat_id = (int) $cat_id;
  815. $q = "DELETE FROM ~{$table_name} WHERE {$item_column} = ? AND cat_id = ?";
  816. Pdb::q($q, [$item_id, $cat_id], 'count');
  817.  
  818. $this->logDeleteCategory(Inflector::plural($this->controller_name), $item_id, $cat_id);
  819. }
  820.  
  821. return true;
  822. }
  823.  
  824.  
  825. /**
  826.   * Form to change the categories for a number of records
  827.   **/
  828. public function _extraMultiCategorise()
  829. {
  830.  
  831. if (! AdminPerms::controllerAccess($this->getControllerName(), 'edit')) {
  832. return new AdminError('Access denied');
  833. }
  834.  
  835. if (empty($_GET['ids'])) {
  836. Notification::error('No items selected for categorisation');
  837. Url::redirect('admin/contents/' . $this->controller_name);
  838. }
  839.  
  840. Admin::setCategoryTablename("{$this->table_name}_cat_list");
  841. Admin::setCategorySinglecat($this->category_single);
  842.  
  843. $view = new View('sprout/admin/categories_multi_categorise');
  844. $view->controller_name = $this->controller_name;
  845. $view->friendly_name = $this->friendly_name;
  846. $view->ids = $_GET['ids'];
  847.  
  848. // Get the categories
  849. $cat_table = Category::tableMain2cat($this->table_name);
  850. $q = "SELECT category.id, category.name
  851. FROM ~{$cat_table} AS category
  852. ORDER BY category.name";
  853. $view->cats = Pdb::q($q, [], 'map');
  854.  
  855. // Get the items
  856. $params = [];
  857. $where = Pdb::buildClause([['item.id', 'IN', $_GET['ids']]], $params);
  858. $q = $this->_getContentsQuery($where, 'item.id', $params);
  859. $items = Pdb::q($q, $params, 'arr');
  860.  
  861. // Create the itemlist
  862. $itemlist = new Itemlist();
  863. $itemlist->main_columns = $this->main_columns;
  864. $itemlist->items = $items;
  865. $view->itemlist = $itemlist->render();
  866.  
  867.  
  868. return $view;
  869. }
  870.  
  871. /**
  872.   * Change the categories for a number of records
  873.   **/
  874. public function postMultiCategorise()
  875. {
  876. Csrf::checkOrDie();
  877.  
  878. if (! AdminPerms::controllerAccess($this->getControllerName(), 'edit')) {
  879. Notification::error('Access denied');
  880. Url::redirect('admin/contents/' . $this->controller_name);
  881. }
  882.  
  883. if (empty($_POST['ids'])) {
  884. Notification::error('No items selected for categorisation');
  885. Url::redirect('admin/contents/' . $this->controller_name);
  886. }
  887.  
  888. if (!@is_array($_POST['categories'])) {
  889. Notification::error('No categories specified for addition');
  890. Url::redirect('admin/extra/' . $this->controller_name . '/multi_categorise' . '?' . http_build_query($_POST));
  891. }
  892.  
  893. Pdb::transact();
  894.  
  895. if (!empty($_POST['mode']) and $_POST['mode'] == 'mod') {
  896. // Modify categories mode
  897. foreach ($_POST['ids'] as $item_id) {
  898. $res = $this->updateCategories($item_id, $_POST['categories']);
  899. if (! $res) {
  900. Notification::error('Database error');
  901. Url::redirect('admin/contents/' . $this->controller_name);
  902. }
  903. }
  904.  
  905. } else {
  906. // Add categories mode by default
  907. foreach ($_POST['ids'] as $item_id) {
  908. foreach ($_POST['categories'] as $cat_id) {
  909. Category::insertInto($this->table_name, $item_id, $cat_id);
  910. }
  911. }
  912.  
  913. }
  914.  
  915. Pdb::commit();
  916.  
  917. $this->_invalidateCaches('multi_categorise');
  918.  
  919. Notification::confirm('Categorisation was successful');
  920. Url::redirect('admin/contents/' . $this->controller_name);
  921. }
  922.  
  923. }
  924.  
  925.  
  926.