SproutCMS

This is the code documentation for the SproutCMS project

source of /sprout/Controllers/Admin/PageAdminController.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 DOMDocument;
  17. use Exception;
  18.  
  19. use Kohana;
  20.  
  21. use Sprout\Controllers\PageController;
  22. use karmabunny\pdb\Exceptions\QueryException;
  23. use karmabunny\pdb\Exceptions\RowMissingException;
  24. use Sprout\Exceptions\ValidationException;
  25. use Sprout\Exceptions\WorkerJobException;
  26. use Sprout\Helpers\Admin;
  27. use Sprout\Helpers\AdminAuth;
  28. use Sprout\Helpers\AdminError;
  29. use Sprout\Helpers\AdminPerms;
  30. use Sprout\Helpers\AdminSeo;
  31. use Sprout\Helpers\Category;
  32. use Sprout\Helpers\ColModifierDate;
  33. use Sprout\Helpers\Constants;
  34. use Sprout\Helpers\Cron;
  35. use Sprout\Helpers\Csrf;
  36. use Sprout\Helpers\DocImport\DocImport;
  37. use Sprout\Helpers\Email;
  38. use Sprout\Helpers\Enc;
  39. use Sprout\Helpers\File;
  40. use Sprout\Helpers\FileConstants;
  41. use Sprout\Helpers\FileUpload;
  42. use Sprout\Helpers\Form;
  43. use Sprout\Helpers\FrontEndEntrance;
  44. use Sprout\Helpers\Inflector;
  45. use Sprout\Helpers\Itemlist;
  46. use Sprout\Helpers\Json;
  47. use Sprout\Helpers\Lnk;
  48. use Sprout\Helpers\MultiEdit;
  49. use Sprout\Helpers\Navigation;
  50. use Sprout\Helpers\NavigationGroups;
  51. use Sprout\Helpers\Notification;
  52. use Sprout\Helpers\Page;
  53. use Sprout\Helpers\Pdb;
  54. use Sprout\Helpers\Preview;
  55. use Sprout\Helpers\RefineBar;
  56. use Sprout\Helpers\RefineWidgetTextbox;
  57. use Sprout\Helpers\Register;
  58. use Sprout\Helpers\Search;
  59. use Sprout\Helpers\Security;
  60. use Sprout\Helpers\Slug;
  61. use Sprout\Helpers\Sprout;
  62. use Sprout\Helpers\Subsites;
  63. use Sprout\Helpers\TinyMCE4RichText;
  64. use Sprout\Helpers\Treenode;
  65. use Sprout\Helpers\Upload;
  66. use Sprout\Helpers\Url;
  67. use Sprout\Helpers\UserPerms;
  68. use Sprout\Helpers\Validator;
  69. use Sprout\Helpers\View;
  70. use Sprout\Helpers\WidgetArea;
  71. use Sprout\Helpers\WorkerCtrl;
  72.  
  73.  
  74. /**
  75.  * Handles admin processing for pages
  76.  */
  77. class PageAdminController extends TreeAdminController
  78. {
  79. protected $controller_name = 'page';
  80. protected $friendly_name = 'Pages';
  81. protected $main_delete = true;
  82.  
  83.  
  84. /**
  85.   * Constructor
  86.   **/
  87. public function __construct()
  88. {
  89. $this->refine_bar = new RefineBar();
  90. $this->refine_bar->setGroup('Pages');
  91. $this->refine_bar->addWidget(new RefineWidgetTextbox('name', 'Name'));
  92.  
  93. $this->refine_bar->setGroup('Page content');
  94. $this->refine_bar->addWidget(new RefineWidgetTextbox('_keyword', 'Keyword search'));
  95. $this->refine_bar->addWidget(new RefineWidgetTextbox('_phrase', 'Exact phrase'));
  96.  
  97. parent::__construct();
  98.  
  99. $this->main_columns = [
  100. 'Name' => 'name',
  101. 'Added' => [new ColModifierDate('g:ia d/m/Y'), 'date_added']
  102. ];
  103. }
  104.  
  105.  
  106. /**
  107.   * Return the WHERE clause to use for a given key which is provided by the RefineBar
  108.   *
  109.   * Allows custom non-table clauses to be added.
  110.   * Is only called for key names which begin with an underscore.
  111.   * The base table is aliased to 'item'.
  112.   *
  113.   * @param string $key The key name, including underscore
  114.   * @param string $val The value which is being refined.
  115.   * @param array &$query_params Parameters to add to the query which will use the WHERE clause
  116.   * @return string WHERE clause, e.g. "item.name LIKE CONCAT('%', ?, '%')", "item.status IN (?, ?, ?)"
  117.   */
  118. protected function _getRefineClause($key, $val, array &$query_params)
  119. {
  120.  
  121. switch ($key) {
  122. case '_keyword':
  123. $query_params[] = Pdb::likeEscape($val);
  124. return "item.id IN (SELECT record_id FROM ~page_keywords AS pk
  125. INNER JOIN ~search_keywords AS k ON pk.keyword_id = k.id WHERE k.name LIKE ?)";
  126.  
  127. case '_phrase':
  128. $query_params[] = 'live';
  129. $query_params[] = Pdb::likeEscape($val);
  130. return "item.id IN (SELECT page_id FROM ~page_revisions AS rev
  131. WHERE rev.status = ? AND rev.text LIKE CONCAT('%', ?, '%'))";
  132. }
  133.  
  134. return parent::_getRefineClause($key, $val, $query_params);
  135. }
  136.  
  137.  
  138. /**
  139.   * This is called after every add, edit and delete, as well as other (i.e. bulk) actions.
  140.   * Use it to clear any frontend caches. The default is an empty method.
  141.   *
  142.   * @param string $action The name of the action (e.g. 'add', 'edit', 'delete', etc)
  143.   * @param int $item_id The item which was affected. Bulk actions (e.g. reorders) will have this set to NULL.
  144.   **/
  145. public function _invalidateCaches($action, $item_id = null)
  146. {
  147. Navigation::clearCache();
  148. }
  149.  
  150.  
  151. /**
  152.   * Returns the contents of the navigation pane for the tree
  153.   **/
  154. public function _getNavigation()
  155. {
  156. $nodes_string = '';
  157. if (!empty($_SESSION['admin'][$this->controller_name . '_nav'])) {
  158. $nodes_string = "'" . implode ("', '", $_SESSION['admin'][$this->controller_name . '_nav']) . "'";
  159. }
  160.  
  161. $q = "SELECT id FROM ~homepages WHERE subsite_id = ?";
  162.  
  163. $view = new View('sprout/admin/page_navigation');
  164. $view->home_page_id = Pdb::q($q, [$_SESSION['admin']['active_subsite']], 'val');
  165. $view->nodes_string = $nodes_string;
  166. $view->controller_name = $this->controller_name;
  167. $view->friendly_name = $this->friendly_name;
  168. $view->record_id = Admin::getRecordId();
  169. $view->root = Navigation::getRootNode();
  170.  
  171. return $view->render();
  172. }
  173.  
  174.  
  175. public function _getCustomAddSaveHTML()
  176. {
  177. $view = new View('sprout/admin/page_add_save');
  178. return $view->render();
  179. }
  180.  
  181.  
  182. /**
  183.   * Return the sub-actions for adding; for spec {@see AdminController::renderSubActions}
  184.   * @return array
  185.   */
  186. public function _getAddSubActions()
  187. {
  188. $actions = parent::_getAddSubActions();
  189. // Add your actions here, like this: $actions[] = [ ... ];
  190.  
  191. if (@$_GET['type'] == 'tool') {
  192. $actions[] = [
  193. 'url' => 'admin/add/page',
  194. 'name' => 'Add a standard page',
  195. ];
  196. } else {
  197. $actions[] = [
  198. 'url' => 'admin/add/page?type=tool',
  199. 'name' => 'Add a tool page',
  200. ];
  201. }
  202.  
  203. return $actions;
  204. }
  205.  
  206.  
  207. /**
  208.   * Returns the add form for adding a page
  209.   *
  210.   * @return string The HTML code which represents the add form
  211.   **/
  212. public function _getAddForm()
  213. {
  214. // Defaults
  215. $data = array(
  216. 'active' => 1,
  217. 'show_in_nav' => 1,
  218. 'admin_perm_type' => Constants::PERM_INHERIT,
  219. 'parent_id' => (int) @$_GET['parent_id'],
  220. 'status' => 'live',
  221. 'admin_perm_specific' => 0,
  222. 'admin_permissions' => [],
  223. 'user_perm_specific' => 0,
  224. 'user_permissions' => [],
  225. );
  226.  
  227. if (! AdminPerms::canAccess('access_noapproval')) {
  228. $data['status'] = 'need_approval';
  229. }
  230.  
  231. // Fields in session
  232. if (!empty($_SESSION['admin']['field_values'])) {
  233. $data = $_SESSION['admin']['field_values'];
  234. unset ($_SESSION['admin']['field_values']);
  235. }
  236.  
  237. $errors = [];
  238. if (!empty($_SESSION['admin']['field_errors'])) {
  239. $errors = $_SESSION['admin']['field_errors'];
  240. unset($_SESSION['admin']['field_errors']);
  241. }
  242.  
  243. $templates = Subsites::getConfigAdmin('skin_views');
  244. if (! $templates) $templates = array('skin/inner' => 'Inner');
  245.  
  246. // Controller list for entrance dropdown
  247. $front_end_controllers = Register::getFrontEndControllers();
  248. asort($front_end_controllers);
  249.  
  250. // Load entrance arguments
  251. $controller_arguments = [];
  252. if (!empty($data['controller_entrance'])) {
  253. $inst = Sprout::instance($data['controller_entrance']);
  254. if ($inst instanceof FrontEndEntrance) {
  255. $controller_arguments = $inst->_getEntranceArguments();
  256. if (empty($controller_arguments)) {
  257. $controller_arguments = ['' => '- Nothing available -'];
  258. }
  259. }
  260. }
  261.  
  262. $title = 'Add a page';
  263. if (@$_GET['type'] == 'tool') {
  264. $data['type'] = 'tool';
  265. $title = 'Add a tool page';
  266. } else {
  267. $data['type'] = 'standard';
  268. }
  269. $view = new View('sprout/admin/page_add');
  270.  
  271. $view->data = $data;
  272. $view->errors = $errors;
  273. $view->admin_category_options = AdminAuth::getAllCategories();
  274. $view->user_category_options = UserPerms::getAllCategories();
  275. $view->front_end_controllers = $front_end_controllers;
  276. $view->controller_arguments = $controller_arguments;
  277. $view->templates = $templates;
  278.  
  279. return array(
  280. 'title' => $title,
  281. 'content' => $view->render()
  282. );
  283. }
  284.  
  285. /**
  286.   * Saves the provided POST data into a new page in the database
  287.   *
  288.   * @param int $page_id After saving, the new record id will be returned in this parameter
  289.   * @param bool True on success, false on failure
  290.   **/
  291. public function _addSave(&$page_id)
  292. {
  293. // Boolean values
  294. $_POST['admin_perm_specific'] = (empty($_POST['admin_perm_specific']) ? 0 : 1);
  295. $_POST['active'] = (empty($_POST['active']) ? 0 : 1);
  296. $_POST['show_in_nav'] = (empty($_POST['show_in_nav']) ? 0 : 1);
  297.  
  298. // Checkbox sets
  299. if (!isset($_POST['user_permissions'])) $_POST['user_permissions'] = [];
  300.  
  301. $valid = new Validator($_POST);
  302. $valid->required(['name']);
  303. $valid->check('name', 'Validity::length', 1, 200);
  304. $valid->check('meta_description', 'Validity::length', 0, 200);
  305.  
  306. // HACK: we should attempt to generate a unique slug rather than just failing out
  307. try {
  308. $conds = [
  309. 'subsite_id' => $_SESSION['admin']['active_subsite'],
  310. 'parent_id' => (int)@$_POST['parent_id']
  311. ];
  312. Slug::unique(Enc::urlname((string)@$_POST['name'], '-'), 'pages', $conds);
  313. } catch (ValidationException $exp) {
  314. $valid->addFieldError('name', 'this will result in a conflicting URL, please try another.');
  315. }
  316.  
  317. if ($_POST['type'] == 'tool') {
  318. // Validate fields specific to tool pages
  319. $valid->required(['controller_entrance']);
  320. $valid->check('controller_entrance', 'Validity::length', 1, 200);
  321. if (!self::checkControllerEntrance($_POST['controller_entrance'], $page_id)) {
  322. $valid->addFieldError('controller_entrance', 'Invalid value');
  323. }
  324.  
  325. // Tell core AdminController the right URL to redirect to upon validation error
  326. $_POST['current_url'] = 'admin/add/page?type=tool';
  327. }
  328.  
  329. if ($valid->hasErrors()) {
  330. $_SESSION['admin']['field_values'] = $_POST;
  331. $_SESSION['admin']['field_errors'] = $valid->getFieldErrors();
  332. $valid->createNotifications();
  333. return false;
  334. }
  335.  
  336. if (!in_array($_POST['type'], Pdb::extractEnumArr('page_revisions', 'type'))) {
  337. return false;
  338. }
  339.  
  340. $operator = AdminAuth::getDetails();
  341. if (! $operator) return false;
  342.  
  343. // Start transaction
  344. Pdb::transact();
  345.  
  346. // Add page
  347. $update_fields = [];
  348. $update_fields['name'] = $_POST['name'];
  349. $update_fields['meta_description'] = $_POST['meta_description'];
  350. $update_fields['parent_id'] = $_POST['parent_id'];
  351. $update_fields['active'] = $_POST['active'];
  352. $update_fields['slug'] = Enc::urlname($_POST['name'], '-');
  353. $update_fields['show_in_nav'] = $_POST['show_in_nav'];
  354. $update_fields['subsite_id'] = $_SESSION['admin']['active_subsite'];
  355. $update_fields['modified_editor'] = $operator['name'];
  356. $update_fields['alt_template'] = trim(preg_replace('![^-_a-z0-9/]!i', '', @$_POST['alt_template']));
  357. $update_fields['menu_group'] = (int) @$_POST['menu_group'];
  358. $update_fields['date_added'] = Pdb::now();
  359. $update_fields['date_modified'] = Pdb::now();
  360.  
  361. if ($_POST['admin_perm_specific'] == 1) {
  362. $update_fields['admin_perm_type'] = Constants::PERM_SPECIFIC;
  363. } else {
  364. $update_fields['admin_perm_type'] = Constants::PERM_INHERIT;
  365. }
  366.  
  367. if (Register::hasFeature('users')) {
  368. if (@$_POST['user_perm_specific'] == 1) {
  369. $update_fields['user_perm_type'] = Constants::PERM_SPECIFIC;
  370. } else {
  371. $update_fields['user_perm_type'] = Constants::PERM_INHERIT;
  372. }
  373. }
  374.  
  375. $page_id = Pdb::insert('pages', $update_fields);
  376.  
  377. $this->fixRecordOrder($page_id);
  378.  
  379. // History item
  380. $res = $this->addHistoryItem($page_id, 'Created empty new page');
  381. if (! $res) return false;
  382.  
  383. // Add first (blank) revision
  384. $update_fields = array();
  385. $update_fields['page_id'] = $page_id;
  386. $update_fields['type'] = $_POST['type'];
  387. $update_fields['changes_made'] = 'New page';
  388.  
  389.  
  390. // Normal pages have more data to add on the edit form, but tool pages go live straight away
  391. if ($_POST['type'] == 'tool') {
  392. $update_fields['status'] = 'live';
  393.  
  394. $update_fields['controller_entrance'] = $_POST['controller_entrance'];
  395. $update_fields['controller_argument'] = $_POST['controller_argument'];
  396. } else {
  397. $update_fields['status'] = 'wip';
  398. }
  399.  
  400. $update_fields['modified_editor'] = $operator['name'];
  401. $update_fields['date_added'] = Pdb::now();
  402. $update_fields['date_modified'] = Pdb::now();
  403. Pdb::insert('page_revisions', $update_fields);
  404.  
  405. // Admin permissions
  406. if ($_POST['admin_perm_specific'] == 1 and @count($_POST['admin_permissions'])) {
  407. foreach ($_POST['admin_permissions'] as $id) {
  408. $id = (int) $id;
  409. if ($id == 0) continue;
  410.  
  411. // Create a new permission record
  412. $update_fields = array();
  413. $update_fields['item_id'] = $page_id;
  414. $update_fields['category_id'] = $id;
  415.  
  416. Pdb::insert('page_admin_permissions', $update_fields);
  417. }
  418. }
  419.  
  420. // User permissions
  421. if (Register::hasFeature('users')) {
  422. if (@$_POST['user_perm_specific'] == 1 and @count($_POST['user_permissions'])) {
  423. foreach ($_POST['user_permissions'] as $id) {
  424. $id = (int) $id;
  425. if ($id == 0) continue;
  426.  
  427. // Create a new permission record
  428. $update_fields = array();
  429. $update_fields['item_id'] = $page_id;
  430. $update_fields['category_id'] = $id;
  431.  
  432. Pdb::insert('page_user_permissions', $update_fields);
  433. }
  434. }
  435. }
  436.  
  437. // Commit
  438. Pdb::commit();
  439.  
  440. Navigation::clearCache();
  441.  
  442. Notification::confirm('Your page has been created. Add your content below.');
  443.  
  444. return 'admin/edit/' . $this->controller_name . '/' . $page_id . '?suppress=true';
  445. }
  446.  
  447.  
  448. /**
  449.   * Return HTML for the import upload form
  450.   **/
  451. public function _importUploadForm()
  452. {
  453. $types = [];
  454. foreach (Register::getDocImports() as $ext => $details) {
  455. $types[$details[1]] = ['name' => $details[1], 'ext' => $ext];
  456. }
  457. ksort($types);
  458.  
  459. $list = new Itemlist();
  460. $list->main_columns = ['Type' => 'name', 'File extension' => 'ext'];
  461. $list->items = $types;
  462.  
  463. $view = new View('sprout/admin/page_import_upload');
  464. $view->list = $list->render();
  465.  
  466. return $view;
  467. }
  468.  
  469.  
  470. /**
  471.   * Upload and do initial processing on the file
  472.   **/
  473. public function importUploadAction()
  474. {
  475. AdminAuth::checkLogin();
  476. Csrf::checkOrDie();
  477. $timestamp = time();
  478.  
  479. // Validate upload
  480. if (! is_array($_FILES['import'])) {
  481. $error = 'No file provided';
  482. } else if (! Upload::required($_FILES['import'])) {
  483. $error = 'No file provided';
  484. } else if (! Upload::valid($_FILES['import'])) {
  485. $error = 'File upload error';
  486. } else if (! FileUpload::checkFilename($_FILES['import']['name'])) {
  487. $error = 'File type not allowed';
  488. } else {
  489. $error = null;
  490. }
  491.  
  492. // Instantiate the importer library
  493. if (! $error) {
  494. try {
  495. $inst = DocImport::instance($_FILES['import']['name']);
  496. $ext = File::getExt($_FILES['import']['name']);
  497. } catch (Exception $ex) {
  498. $error = $ex->getMessage();
  499. }
  500. }
  501.  
  502. // Upload file to temp dir
  503. if (! $error) {
  504. $temporig = APPPATH . "temp/import_{$timestamp}.{$ext}";
  505.  
  506. $res = @copy($_FILES['import']['tmp_name'], $temporig);
  507. if (! $res) {
  508. $error = 'Unable to copy file to temporary directory';
  509. }
  510. }
  511.  
  512. // Do file processing
  513. if (! $error) {
  514. try {
  515. $result = $inst->load($temporig);
  516. } catch (Exception $ex) {
  517. $error = $ex->getMessage();
  518. }
  519.  
  520. @unlink($temporig);
  521. }
  522.  
  523. // Check the result is valid XML
  524. if (! $error) {
  525. if (!($result instanceof DOMDocument)) {
  526. $dom = new DOMDocument();
  527. $success = @$dom->loadXML($result);
  528. if (! $success) {
  529. $error = 'Conversion failed';
  530.  
  531. foreach (libxml_get_errors() as $err) {
  532. Notification::error($err->message . ' (' . $err->code . ') at ' . $err->line . ':' . $err->column);
  533. }
  534.  
  535. file_put_contents(APPPATH . "temp/conversion_failure.xml", $result);
  536. chmod(APPPATH . "temp/conversion_failure.xml", 0666);
  537. }
  538. unset($dom);
  539. }
  540. }
  541.  
  542. // Save XML file
  543. if (! $error) {
  544. $tempxml = APPPATH . "temp/import_{$timestamp}.xml";
  545.  
  546. if ($result instanceof DOMDocument) {
  547. $result->save($tempxml);
  548.  
  549. } else if (is_string($result)) {
  550. file_put_contents($tempxml, $result);
  551.  
  552. } else {
  553. $error = 'Unable to save temp xml';
  554. }
  555. }
  556.  
  557. // Report error
  558. if ($error) {
  559. Notification::error($error);
  560. Url::redirect("admin/import_upload/page");
  561. }
  562.  
  563. Url::redirect("admin/import_options/page?timestamp={$timestamp}&ext=xml");
  564. }
  565.  
  566.  
  567. /**
  568.   * Preview the pages which will be created
  569.   **/
  570. public function importPreviewAjax()
  571. {
  572. AdminAuth::checkLogin();
  573. $_GET['timestamp'] = (int) $_GET['timestamp'];
  574. $filename = APPPATH . "temp/import_{$_GET['timestamp']}.xml";
  575.  
  576. echo '<div class="info highlight-confirm">This is a preview of the pages which will be created</div>';
  577.  
  578. switch ($_GET['import_type']) {
  579. case 'none':
  580. echo '<ul>';
  581. echo '<li>', ($_GET['page_name'] ? Enc::html($_GET['page_name']) : '<i>Enter a page name into the field above</i>'), '</li>';
  582. echo '</ul>';
  583. break;
  584.  
  585. case 'heading':
  586. $tree = DocImport::getHeadingsTree($filename, $_GET['heading_level']);
  587.  
  588. if (trim($_GET['top_page_name'])) {
  589. $tree['name'] = trim($_GET['top_page_name']);
  590. $new_root = new Treenode();
  591. $new_root->children = array($tree);
  592. $tree->parent = $new_root;
  593. $tree = $new_root;
  594. }
  595.  
  596. echo '<ul>';
  597. foreach ($tree->children as $child) {
  598. self::renderPreviewTreenode($child);
  599. }
  600. echo '</ul>';
  601. break;
  602.  
  603. default:
  604. echo '<p><i>Invalid import type</i></p>';
  605. }
  606. }
  607.  
  608.  
  609. /**
  610.   * Render a single node for the preview tree we return in `import_preview_ajax`
  611.   **/
  612. private static function renderPreviewTreenode($node)
  613. {
  614. echo '<li>', Enc::html($node['name']);
  615.  
  616. if (count($node->children)) {
  617. echo '<ul>';
  618. foreach ($node->children as $child) {
  619. self::renderPreviewTreenode($child);
  620. }
  621. echo '</ul>';
  622. }
  623.  
  624. echo '</li>';
  625. }
  626.  
  627.  
  628. /**
  629.   * Returns a form which contains options for doing an import
  630.   **/
  631. public function _getImport($filename)
  632. {
  633. $view = new View('sprout/admin/page_import_options');
  634.  
  635. return array(
  636. 'title' => 'Document import',
  637. 'content' => $view->render(),
  638. );
  639. }
  640.  
  641.  
  642. /**
  643.   * Facebox info box
  644.   **/
  645. public function importNotes($view_name)
  646. {
  647. AdminAuth::checkLogin();
  648. $view = new View('sprout/doc_import_notes/' . $view_name);
  649.  
  650. echo '<div class="import-notes">';
  651. echo $view->render();
  652. echo '</div>';
  653. }
  654.  
  655.  
  656. /**
  657.   * Does the actual import
  658.   *
  659.   * @param string $filename The location of the import data, in a temporary directory
  660.   **/
  661. public function _importData($filename)
  662. {
  663. $images = array();
  664. $headings = array();
  665.  
  666. $operator = AdminAuth::getDetails();
  667. if (! $operator) return false;
  668.  
  669. // Basic validation
  670. if ($_POST['import_type'] != 'none' and $_POST['import_type'] != 'heading') {
  671. return false;
  672. }
  673.  
  674. if ($_POST['import_type'] == 'none' and !$_POST['page_name']) {
  675. return false;
  676. }
  677.  
  678. // Load images, save mapping into $images
  679. $cat_id = Category::lookupOrCreate('files', 'Imported document images');
  680. $resources = DocImport::getResources($filename);
  681. foreach ($resources as $resname => $blob) {
  682. $image_filename = File::filenameMakeSane('doc_' . $resname);
  683.  
  684.  
  685. Pdb::transact();
  686.  
  687. // Add file record
  688. $update_fields = array();
  689. $update_fields['name'] = $resname;
  690. $update_fields['type'] = FileConstants::TYPE_IMAGE;
  691. $update_fields['date_added'] = Pdb::now();
  692. $update_fields['date_modified'] = Pdb::now();
  693. $update_fields['date_file_modified'] = Pdb::now();
  694. $update_fields['sha1'] = hash('sha1', $blob, false);
  695.  
  696. $file_id = Pdb::insert('files', $update_fields);
  697. $image_filename = $file_id . '_' . $image_filename;
  698.  
  699. // Set filename to contain file id
  700. $update_fields = array();
  701. $update_fields['filename'] = $image_filename;
  702. Pdb::update('files', $update_fields, ['id' => $file_id]);
  703.  
  704. // categorise
  705. Category::insertInto('files', $file_id, $cat_id);
  706.  
  707. // insert the blob of data
  708. File::putString($image_filename, $blob);
  709.  
  710. File::createDefaultSizes($image_filename);
  711.  
  712. Pdb::commit();
  713.  
  714.  
  715. // If the image is large, auto-switch in the medium size
  716. $size = File::imageSize($image_filename);
  717. $small = File::getResizeFilename($image_filename, 'medium');
  718. if ($size[0] > 300 and File::exists($small)) {
  719. $image_filename = $small;
  720. }
  721.  
  722. $images[$resname] = File::relUrl($image_filename);
  723. }
  724.  
  725. // Split into pages based on options
  726. switch ($_POST['import_type']) {
  727. case 'none':
  728. $dom = new DOMDocument();
  729. $dom->loadXML(file_get_contents($filename));
  730. $body = $dom->saveXML($dom->getElementsByTagName('body')->item(0));
  731.  
  732. $tree = new Treenode();
  733. $node = new Treenode();
  734. $node['name'] = $_POST['page_name'];
  735. $node['body'] = $body;
  736. $tree->children[] = $node;
  737.  
  738. $headings[1] = 2;
  739. break;
  740.  
  741. case 'heading':
  742. $tree = DocImport::getHeadingsTree($filename, $_POST['heading_level'], true);
  743.  
  744. if ($_POST['heading_level'] > 1) {
  745. for ($i = $_POST['heading_level'] + 1; $i < 6; $i++) {
  746. $headings[$i] = $i - 1;
  747. }
  748. }
  749.  
  750. if (trim($_POST['top_page_name'])) {
  751. $tree['name'] = trim($_POST['top_page_name']);
  752. $new_root = new Treenode();
  753. $new_root->children = array($tree);
  754. $tree->parent = $new_root;
  755. $tree = $new_root;
  756. }
  757. break;
  758.  
  759. default:
  760. throw new Exception("Invalid import type '{$_POST['import_type']}'.");
  761. }
  762.  
  763. // Walk page tree and create pages
  764. $count = 0;
  765. Pdb::transact();
  766. foreach ($tree->children as $child) {
  767. $result = $this->createPageTreenode($child, (int)$_POST['parent_id'], $images, $headings, $operator);
  768. if ($result === false) return false;
  769. $count += $result;
  770. }
  771. Pdb::commit();
  772.  
  773. Notification::confirm('Imported ' . $count . ' ' . Inflector::plural('page', $count));
  774. return true;
  775. }
  776.  
  777.  
  778. /**
  779.   * Create a page
  780.   **/
  781. private function createPageTreenode($node, $parent_id, $images, $headings, $operator)
  782. {
  783. $dom = new DOMDocument();
  784. $success = $dom->loadXML('<doc><body>' . $node['body'] . '</body></doc>');
  785. $html = DocImport::getHtml($dom, $images, $headings);
  786.  
  787. // Add page
  788. $update_fields = [];
  789. $update_fields['name'] = trim($node['name']);
  790. $update_fields['slug'] = Enc::urlname(trim($node['name']), '-');
  791. $update_fields['active'] = 1;
  792. $update_fields['show_in_nav'] = 1;
  793. $update_fields['parent_id'] = $parent_id;
  794. $update_fields['subsite_id'] = $_SESSION['admin']['active_subsite'];
  795. $update_fields['modified_editor'] = $operator['name'];
  796. $update_fields['date_added'] = Pdb::now();
  797. $update_fields['date_modified'] = Pdb::now();
  798. $page_id = Pdb::insert('pages', $update_fields);
  799.  
  800. $this->fixRecordOrder($page_id);
  801.  
  802. // Add revision
  803. $update_fields = array();
  804. $update_fields['page_id'] = $page_id;
  805. $update_fields['type'] = 'standard';
  806. $update_fields['changes_made'] = 'Imported page from uploaded file';
  807. $update_fields['status'] = 'live';
  808. $update_fields['modified_editor'] = $operator['name'];
  809. $update_fields['date_added'] = Pdb::now();
  810. $update_fields['date_modified'] = Pdb::now();
  811. $rev_id = Pdb::insert('page_revisions', $update_fields);
  812.  
  813. // Add content block
  814. $settings = [
  815. 'text' => $html,
  816. ];
  817. $update_fields = array();
  818. $update_fields['page_revision_id'] = $rev_id;
  819. $update_fields['area_id'] = 1;
  820. $update_fields['active'] = 1;
  821. $update_fields['type'] = 'RichText';
  822. $update_fields['settings'] = json_encode($settings);
  823. $update_fields['record_order'] = 1;
  824. Pdb::insert('page_widgets', $update_fields);
  825.  
  826. // History page
  827. $res = $this->addHistoryItem($page_id, "Imported page from uploaded file");
  828. if (! $res) return false;
  829.  
  830. // Do indexing on the page text
  831. $res = $this->reindexItem($page_id, $node['name'], $html);
  832. if (! $res) return false;
  833.  
  834. // Children pages
  835. $count = 1;
  836. foreach ($node->children as $child) {
  837. $count += $this->createPageTreenode($child, $page_id, $images, $headings, $operator);
  838. }
  839.  
  840. return $count;
  841. }
  842.  
  843.  
  844. public function _getCustomEditSaveHTML($item_id)
  845. {
  846. // N.B. this is called after the edit form has been rendered
  847.  
  848. $user_id = AdminAuth::getId();
  849. $q = "SELECT operators.id, operators.name
  850. FROM ~operators AS operators
  851. INNER JOIN ~operators_cat_join AS joiner ON joiner.operator_id = operators.id
  852. AND operators.id != ?
  853. INNER JOIN ~operators_cat_list AS cat ON joiner.cat_id = cat.id
  854. WHERE cat.access_noapproval = 1
  855. ORDER BY operators.name";
  856. $approval_admins = Pdb::q($q, [$user_id], 'map');
  857.  
  858. $view = new View('sprout/admin/page_edit_save');
  859. $view->id = (int) $item_id;
  860. $view->preview_url = Subsites::getAbsRootAdmin() . 'admin/call/page/preview/' . $item_id;
  861. $view->approval_admins = $approval_admins;
  862. $view->allow_delete = $this->_isDeleteSaved($item_id);
  863.  
  864. $view->type = $this->edit_type;
  865.  
  866. return $view->render();
  867. }
  868.  
  869.  
  870. /**
  871.   * Return the URL to use for the 'view live site' button, when editing a given record
  872.   *
  873.   * @param int $item_id Record which is being editied
  874.   * @return string URL, either absolute or relative
  875.   * @return null Default url should be used
  876.   */
  877. public function _getEditLiveUrl($item_id)
  878. {
  879. return Page::url($item_id);
  880. }
  881.  
  882.  
  883. /**
  884.   * Returns the edit form for editing the specified page
  885.   *
  886.   * @param int $id The record to show the edit form for
  887.   * @return string The HTML code which represents the edit form
  888.   **/
  889. public function _getEditForm($id)
  890. {
  891. $id = (int) $id;
  892.  
  893. // Get subsite info
  894. $q = "SELECT * FROM ~subsites WHERE id = ?";
  895. try {
  896. $subsite = Pdb::q($q, [$_SESSION['admin']['active_subsite']], 'row');
  897. } catch (QueryException $ex) {
  898. return new AdminError("Invalid id specified - current subsite does not exist");
  899. }
  900.  
  901. // Load the page
  902. $q = "SELECT * FROM ~pages WHERE id = ?";
  903. try {
  904. $page = Pdb::q($q, [$id], 'row');
  905. } catch (QueryException $ex) {
  906. return new AdminError("Invalid id specified - page does not exist");
  907. }
  908.  
  909. TinyMCE4RichText::needs();
  910.  
  911. // If no slug set, create one
  912. if (empty($page['slug'])) $page['slug'] = Enc::urlname($page['name']);
  913.  
  914. // Check the permissions of the page - can the operator edit this page?
  915. $res = AdminPerms::checkPermissionsTree('pages', $id);
  916. if (! $res) return new AdminError("Access denied to modify this page");
  917.  
  918. // Load the revisions
  919. $q = "SELECT page_revisions.*, DATE_FORMAT(date_modified, '%d/%m/%Y %h:%i %p') AS date_modified
  920. FROM ~page_revisions AS page_revisions
  921. WHERE page_id = ?
  922. ORDER BY page_revisions.date_modified DESC";
  923. $revs = Pdb::q($q, [$id], 'arr');
  924. if (count($revs) == 0) {
  925. return new AdminError("Invalid id specified - page does not have any revisions");
  926. }
  927.  
  928. // Select the revision to edit
  929. // If there is a rev in the session, use it.
  930. // Otherwise, get the latest live revision.
  931. // If all else fails, use the latest revision
  932. $sel_rev = ['id' => 0];
  933. $rev_num = 0;
  934. if (!empty($_SESSION['admin']['field_values']['rev_id'])) {
  935. foreach ($revs as $i => $rev) {
  936. if ($rev['id'] == $_SESSION['admin']['field_values']['rev_id']) {
  937. $sel_rev = $rev;
  938. $rev_num = count($revs) - $i;
  939. break;
  940. }
  941. }
  942.  
  943. } else if (!empty($_GET['revision'])) {
  944. $rev_id = (int)$_GET['revision'];
  945. foreach ($revs as $i => $rev) {
  946. if ($rev['id'] == $rev_id) {
  947. $sel_rev = $rev;
  948. $rev_num = count($revs) - $i;
  949. break;
  950. }
  951. }
  952.  
  953. } else {
  954. $sel_rev = $revs[0];
  955. foreach ($revs as $rev) {
  956. if ($rev['status'] == 'live') {
  957. $sel_rev = $rev;
  958. break;
  959. }
  960. }
  961. }
  962.  
  963. // If there's no live revision, inform the user
  964. $has_live_rev = false;
  965. foreach ($revs as $rev) {
  966. if ($rev['status'] == 'live') {
  967. $has_live_rev = true;
  968. break;
  969. }
  970. }
  971.  
  972. $data = array_merge($page, $sel_rev);
  973.  
  974. // Type override caused by clicking a sidebar 'change to' option
  975. if (in_array(@$_GET['type'], Pdb::extractEnumArr('page_revisions', 'type'))) {
  976. $data['type'] = $_GET['type'];
  977. }
  978.  
  979. // Remember the edit type for use in sidebar; i.e. _getCustomEditSaveHTML
  980. $this->edit_type = $data['type'];
  981.  
  982. if ($sel_rev['status'] != 'wip') {
  983. $data['changes_made'] = '';
  984. }
  985.  
  986. if ($data['status'] != 'wip' and $data['status'] != 'auto_launch') {
  987. if (AdminPerms::canAccess('access_noapproval')) {
  988. $data['status'] = 'live';
  989. } else {
  990. $data['status'] = 'need_approval';
  991. }
  992. }
  993.  
  994. if ($data['type'] == 'standard') {
  995. // Load widgets and collate rich text as page text
  996. $text = '';
  997. $widgets = [];
  998. $q = "SELECT area_id, type, settings, conditions, active, heading, template
  999. FROM ~page_widgets
  1000. WHERE page_revision_id = ?
  1001. ORDER BY area_id, record_order";
  1002. $wids = Pdb::q($q, [$sel_rev['id']], 'arr');
  1003.  
  1004. foreach ($wids as $widget) {
  1005. $widgets[$widget['area_id']][] = $widget;
  1006.  
  1007. // Embedded rich text widgets
  1008. if ($widget['area_id'] == 1 and $widget['type'] == 'RichText') {
  1009. $settings = json_decode($widget['settings'], true);
  1010. if ($text) $text .= "\n";
  1011. $text .= $settings['text'];
  1012. }
  1013. }
  1014.  
  1015.  
  1016. // Load media
  1017. $media = [];
  1018. preg_match_all('/<img.*?src="(.*?)"/', $text, $matches);
  1019. foreach ($matches[1] as $match) {
  1020. $media[] = $match;
  1021. }
  1022.  
  1023. AdminSeo::setTopic($page['name']);
  1024. AdminSeo::setSlug($data['slug']);
  1025. AdminSeo::addContent($text);
  1026. AdminSeo::addLinks(Page::determineRelatedLinks($id));
  1027.  
  1028. } else if (in_array($data['type'], ['tool', 'redirect'])) {
  1029. $widgets = [];
  1030.  
  1031. } else {
  1032. return new AdminError("Invalid page type");
  1033. }
  1034.  
  1035. // Load permissions for page
  1036. $admin_permissions = AdminPerms::getAccessableGroups('pages', $id);
  1037. if ($data['admin_perm_type'] == Constants::PERM_SPECIFIC) {
  1038. $data['admin_perm_specific'] = 1;
  1039. } else {
  1040. $data['admin_perm_specific'] = 0;
  1041. }
  1042.  
  1043. $data['admin_permissions'] = $admin_permissions;
  1044.  
  1045. $user_permissions = UserPerms::getAccessableGroups('pages', $id);
  1046. if ($data['user_perm_type'] == Constants::PERM_SPECIFIC) {
  1047. $data['user_perm_specific'] = 1;
  1048. } else {
  1049. $data['user_perm_specific'] = 0;
  1050. }
  1051.  
  1052. $data['user_permissions'] = $user_permissions;
  1053.  
  1054.  
  1055. // Overlay session data
  1056. if (!empty($_SESSION['admin']['field_values'])) {
  1057. $data = array_merge($data, $_SESSION['admin']['field_values']);
  1058. unset ($_SESSION['admin']['field_values']);
  1059. }
  1060.  
  1061. // Load history
  1062. $q = "SELECT modified_editor, changes_made,
  1063. DATE_FORMAT(date_added, '%d/%m/%Y %h:%i %p') AS date_added
  1064. FROM ~page_history_items AS page_history_items
  1065. WHERE page_id = ?
  1066. ORDER BY page_history_items.date_added DESC";
  1067. $history = Pdb::q($q, [$id], 'arr');
  1068.  
  1069. // Controller list for entrance dropdown
  1070. $front_end_controllers = Register::getFrontEndControllers();
  1071. asort($front_end_controllers);
  1072.  
  1073. // Load entrance arguments
  1074. $controller_arguments = array();
  1075. if ($data['controller_entrance']) {
  1076. $inst = Sprout::instance($data['controller_entrance']);
  1077. if ($inst instanceof FrontEndEntrance) {
  1078. $controller_arguments = $inst->_getEntranceArguments();
  1079. if (empty($controller_arguments)) {
  1080. $controller_arguments = array('' => '- Nothing available -');
  1081. }
  1082. }
  1083. }
  1084.  
  1085. $templates = Subsites::getConfigAdmin('skin_views');
  1086. if (! $templates) $templates = array('skin/inner' => 'Inner');
  1087.  
  1088.  
  1089. // Custom attributes
  1090. $attributes = MultiEdit::load('page_attributes', ['page_id' => $id]);
  1091. if (! isset($data['multiedit_attrs'])) {
  1092. $data['multiedit_attrs'] = $attributes;
  1093. }
  1094.  
  1095. // Load admin notes, if found
  1096. $admin_notes = null;
  1097. foreach ($attributes as $row) {
  1098. if ($row['name'] == 'sprout.admin_notes') {
  1099. $admin_notes = trim($row['value']);
  1100. break;
  1101. }
  1102. }
  1103.  
  1104. // Special case for redirect pages
  1105. if (!$admin_notes and $data['type'] == 'redirect') {
  1106. if (!empty($data['redirect'])) {
  1107. $typename = Lnk::typename($data['redirect']);
  1108. if (preg_match('/^[aeiou]/i', $typename)) {
  1109. $admin_notes = 'This page redirects to an ' . $typename . '. Content blocks on this page have been disabled.';
  1110. } else {
  1111. $admin_notes = 'This page redirects to a ' . $typename . '. Content blocks on this page have been disabled.';
  1112. }
  1113. } else {
  1114. $admin_notes = 'This page will redirect. Content blocks on this page have been disabled.';
  1115. }
  1116. }
  1117.  
  1118. // Richtext width and height
  1119. $richtext_width = Kohana::config('sprout.admin_richtext_width');
  1120. $richtext_height = Kohana::config('sprout.admin_richtext_height');
  1121. if (!$richtext_width) $richtext_width = 700;
  1122. if (!$richtext_height) $richtext_height = 500;
  1123.  
  1124. // View
  1125. $view = new View('sprout/admin/page_edit');
  1126.  
  1127. $errors = [];
  1128. if (!empty($_SESSION['admin']['field_errors'])) {
  1129. $errors = $_SESSION['admin']['field_errors'];
  1130. unset($_SESSION['admin']['field_errors']);
  1131. }
  1132.  
  1133. $view->id = $id;
  1134. $view->page = $page;
  1135. $view->subsite = $subsite;
  1136. $view->data = $data;
  1137. $view->errors = $errors;
  1138. $view->widgets = $widgets;
  1139. $view->history = $history;
  1140. $view->admin_category_options = AdminAuth::getAllCategories();
  1141. $view->admin_permissions = $admin_permissions;
  1142. $view->user_category_options = UserPerms::getAllCategories();
  1143. $view->user_permissions = $user_permissions;
  1144. $view->can_approve_revisions = AdminPerms::canAccess('access_noapproval');
  1145. $view->front_end_controllers = $front_end_controllers;
  1146. $view->controller_arguments = $controller_arguments;
  1147. $view->templates = $templates;
  1148. $view->admin_notes = $admin_notes;
  1149. $view->richtext_width = $richtext_width;
  1150. $view->richtext_height = $richtext_height;
  1151.  
  1152. $view->revs = $revs;
  1153. $view->sel_rev_id = $sel_rev['id'];
  1154. $view->has_live_rev = $has_live_rev;
  1155.  
  1156. if ($data['type'] == 'standard') {
  1157. $view->text = $text;
  1158. $view->media = $media;
  1159. }
  1160.  
  1161. $view->show_tour = !Admin::isTourCompleted('page_edit');
  1162.  
  1163. if ($rev_num) {
  1164. $title = 'Editing revision ' . $rev_num . ' of page <strong>' . Enc::html($page['name']) . '</strong>';
  1165. } else {
  1166. $title = 'Editing page <strong>' . Enc::html($page['name']) . '</strong>';
  1167. }
  1168.  
  1169. return array(
  1170. 'title' => $title,
  1171. 'content' => $view->render()
  1172. );
  1173. }
  1174.  
  1175. /**
  1176.   * Gets the text of a single revision
  1177.   *
  1178.   * @param int $id The revision to get the text of
  1179.   **/
  1180. public function ajaxGetRev($id)
  1181. {
  1182. $id = (int) $id;
  1183.  
  1184. $rev = Pdb::get('page_revisions', $id);
  1185.  
  1186. $out = array();
  1187. $out['text'] = $rev['text'];
  1188. $out['date_launch'] = '';
  1189.  
  1190. switch ($rev['status']) {
  1191. case 'wip':
  1192. case 'auto_launch':
  1193. $out['changes_made'] = $rev['changes_made'];
  1194. $out['status'] = $rev['status'];
  1195. $out['date_launch'] = $rev['date_launch'];
  1196. break;
  1197.  
  1198. case 'need_approval':
  1199. $out['changes_made'] = $rev['changes_made'];
  1200. $out['status'] = 'live';
  1201. break;
  1202.  
  1203. case 'live':
  1204. $out['changes_made'] = '';
  1205. $out['status'] = 'live';
  1206. break;
  1207.  
  1208. case 'old':
  1209. case 'rejected':
  1210. $changes = preg_replace('/ ?\(based.+/', '', $rev['changes_made']);
  1211. $out['changes_made'] = "...... (based on revision {$rev['id']}: '{$changes}')";
  1212. $out['status'] = 'live';
  1213. break;
  1214. }
  1215.  
  1216. Json::out($out);
  1217. }
  1218.  
  1219.  
  1220. /**
  1221.   * Return a list of menu groups for a selected parent page
  1222.   **/
  1223. public function ajaxGetMenuGroups($parent_page_id)
  1224. {
  1225. AdminAuth::checkLogin();
  1226. $parent_page_id = (int) $parent_page_id;
  1227.  
  1228. // Special case for top-level
  1229. if ($parent_page_id === 0) {
  1230. Json::confirm(array('groups' => array()));
  1231. return;
  1232. }
  1233.  
  1234. // Find the page
  1235. $root = Navigation::loadPageTree($_SESSION['admin']['active_subsite'], true);
  1236. $node = $root->findNodeValue('id', $parent_page_id);
  1237. if ($node === null) Json::error('Invalid parent page');
  1238.  
  1239. // Get the groups
  1240. $anc = $node->findAncestors();
  1241. $top_parent = $anc[0];
  1242. $groups = NavigationGroups::getGroupsAdmin($top_parent['id']);
  1243.  
  1244. $names = array();
  1245. foreach ($groups as $id => $row) {
  1246. $names[$id] = $row['name'];
  1247. }
  1248.  
  1249. Json::confirm(array('groups' => $names));
  1250. }
  1251.  
  1252.  
  1253. /**
  1254.   * Makes the provided html text be in a standard format to ensure the integrity of the change check
  1255.   **/
  1256. private function convertForChangeCheck($text)
  1257. {
  1258. $text = preg_replace('/^<!-- .+ -->/', '', trim($text));
  1259. return md5($text);
  1260. }
  1261.  
  1262. /**
  1263.   * Saves the provided POST data into this page in the database
  1264.   *
  1265.   * @param int $page_id The record to update
  1266.   * @param bool True on success, false on failure
  1267.   **/
  1268. public function _editSave($page_id)
  1269. {
  1270. $res = AdminPerms::checkPermissionsTree('pages', $page_id);
  1271. if (! $res) return false;
  1272.  
  1273. $page_id = (int) $page_id;
  1274. $rev_id = (int) $_POST['rev_id'];
  1275.  
  1276. $q = "SELECT page.*, rev.type, rev.controller_entrance, rev.controller_argument, rev.redirect
  1277. FROM ~pages AS page
  1278. INNER JOIN ~page_revisions AS rev ON page.id = rev.page_id
  1279. AND rev.id = ?
  1280. WHERE page.id = ?";
  1281. $orig_page = Pdb::q($q, [$rev_id, $page_id], 'row');
  1282.  
  1283. $revision_changed = false;
  1284. $page_type = (string) @$_POST['type'];
  1285. if (!in_array($page_type, Pdb::extractEnumArr('page_revisions', 'type'))) {
  1286. $page_type = $orig_page['type'];
  1287. }
  1288.  
  1289. if ($page_type != $orig_page['type']) $revision_changed = true;
  1290.  
  1291. // Collate POSTed widgets.
  1292. $new_widgets = [];
  1293. if (@count($_POST['widgets'])) {
  1294. foreach ($_POST['widgets'] as $area_name => $widgets) {
  1295. $area = WidgetArea::findAreaByName($area_name);
  1296. if ($area == null) continue;
  1297.  
  1298. $order = 0;
  1299. foreach ($widgets as $info) {
  1300. list ($index, $type) = explode(',', $info, 2);
  1301.  
  1302. // If it's been deleted, then skip over all other processing
  1303. if ($_POST['widget_deleted'][$area_name][$index] == '1') {
  1304. continue;
  1305. }
  1306.  
  1307. $settings = @$_POST['widget_settings_' . $index];
  1308. if (!is_array($settings)) $settings = [];
  1309.  
  1310. $settings = json_encode($settings);
  1311.  
  1312. $active = 1;
  1313. if (isset($_POST['widget_active'][$area_name][$index])) {
  1314. $active = (int) (bool) $_POST['widget_active'][$area_name][$index];
  1315. }
  1316.  
  1317. $conditions = '';
  1318. if (isset($_POST['widget_conds'][$area_name][$index])) {
  1319. $conditions = $_POST['widget_conds'][$area_name][$index];
  1320. }
  1321.  
  1322. $heading = '';
  1323. if (isset($_POST['widget_heading'][$area_name][$index])) {
  1324. $heading = $_POST['widget_heading'][$area_name][$index];
  1325. }
  1326.  
  1327. $template = '';
  1328. if (isset($_POST['widget_template'][$area_name][$index])) {
  1329. $template = $_POST['widget_template'][$area_name][$index];
  1330. }
  1331.  
  1332. $new_widgets[] = [
  1333. 'area_id' => $area->getIndex(),
  1334. 'active' => $active,
  1335. 'type' => $type,
  1336. 'settings' => $settings,
  1337. 'conditions' => $conditions,
  1338. 'heading' => $heading,
  1339. 'template' => $template,
  1340. 'record_order' => $order++,
  1341. ];
  1342. }
  1343. }
  1344. }
  1345.  
  1346. // Compare new widgets with old ones -- if changed, need a new revision
  1347. $q = "SELECT area_id, active, type, settings, conditions, heading, template, record_order
  1348. FROM ~page_widgets
  1349. WHERE page_revision_id = ?
  1350. ORDER BY area_id, record_order";
  1351. $old_widgets = Pdb::query($q, [$rev_id], 'arr');
  1352. if (count($new_widgets) != count($old_widgets)) {
  1353. $revision_changed = true;
  1354. } else {
  1355. foreach ($old_widgets as $key => $widget) {
  1356. if ($widget != @$new_widgets[$key]) {
  1357. $revision_changed = true;
  1358. break;
  1359. }
  1360. }
  1361. }
  1362.  
  1363. // Get the original revision, see if it has changed
  1364. try {
  1365. $q = "SELECT rev.*, page.parent_id
  1366. FROM ~page_revisions AS rev
  1367. INNER JOIN ~pages AS page ON page.id = rev.page_id
  1368. WHERE rev.id = ?";
  1369. $orig_rev = Pdb::q($q, [$rev_id], 'row');
  1370. } catch (RowMissingException $ex) {
  1371. // Pretend there's an existing revision when going a page preview
  1372. $orig_rev = ['status' => null, 'date_launch' => null];
  1373. }
  1374.  
  1375. if ($_POST['status'] != $orig_rev['status']) $revision_changed = true;
  1376.  
  1377. if ($_POST['status'] == 'auto_launch') {
  1378. if ($_POST['date_launch'] != $orig_rev['date_launch']) $revision_changed = true;
  1379. } else {
  1380. $_POST['date_launch'] = null;
  1381. }
  1382.  
  1383. if ($page_type == 'standard') {
  1384. if (@count($_POST['mediareplace_fr'])) {
  1385. foreach($_POST['mediareplace_fr'] as $idx => $replace_from) {
  1386. $replace_to = $_POST['mediareplace_to'][$idx];
  1387.  
  1388. if (! $replace_to) continue;
  1389. if ($replace_to == $replace_from) continue;
  1390.  
  1391. $resized = File::getResizeFilename($replace_to, 'medium');
  1392. if (File::exists($resized)) $replace_to = $resized;
  1393. $replace_to = File::relUrl($replace_to);
  1394.  
  1395. foreach ($new_widgets as &$widget) {
  1396. if ($widget['area_id'] != 1 or $widget['type'] != 'RichText') continue;
  1397. $settings = json_decode($widget['settings'], true);
  1398. $new_text = preg_replace('/<img(.*?)src="' . preg_quote($replace_from, '/') . '"(.*?)>/', "<img\$1src=\"{$replace_to}\"\$2/>", $settings['text']);
  1399. if ($new_text != $settings['text']) {
  1400. $settings['text'] = $new_text;
  1401. $widget['settings'] = json_encode($settings);
  1402. $revision_changed = true;
  1403. }
  1404. }
  1405. unset($widget);
  1406. }
  1407. }
  1408. } else if ($page_type == 'tool') {
  1409. if ($orig_page['controller_entrance'] != $_POST['controller_entrance']) $revision_changed = true;
  1410. if ($orig_page['controller_argument'] != $_POST['controller_argument']) $revision_changed = true;
  1411. } else if ($page_type == 'redirect') {
  1412. if ($orig_page['redirect'] != $_POST['redirect']) $revision_changed = true;
  1413. }
  1414.  
  1415. // Check if the parent changed
  1416. $parent_changed = false;
  1417. if ((int) $orig_page['parent_id'] != (int) $_POST['parent_id']) {
  1418. $parent_changed = true;
  1419. }
  1420.  
  1421. if (empty($_POST['controller_argument'])) {
  1422. $_POST['controller_argument'] = '';
  1423. }
  1424.  
  1425. // Set up validation rules
  1426. $valid = new Validator($_POST);
  1427. $valid->setLabels([
  1428. 'parent_id' => 'Parent page',
  1429. 'slug' => 'URL slug',
  1430. 'gallery_thumb' => 'Gallery thumbnail',
  1431. ]);
  1432.  
  1433. $valid->required(['name', 'slug']);
  1434. $valid->check('name', 'Validity::length', 1, 200);
  1435. $valid->check('slug', 'Validity::length', 1, 200);
  1436. $valid->check('slug', 'Slug::valid');
  1437.  
  1438. $slug_conditions = [
  1439. 'subsite_id' => $_SESSION['admin']['active_subsite'],
  1440. 'parent_id' => (int)$_POST['parent_id'],
  1441. ['id', '!=', $page_id],
  1442. ];
  1443. $valid->check('slug', 'Slug::unique', 'pages', $slug_conditions);
  1444.  
  1445. $valid->check('meta_keywords', 'Validity::length', 0, 200);
  1446. $valid->check('meta_description', 'Validity::length', 0, 200);
  1447. $valid->check('alt_browser_title', 'Validity::length', 0, 200);
  1448. $valid->check('alt_nav_title', 'Validity::length', 0, 200);
  1449.  
  1450. if ($page_type == 'standard') {
  1451. $valid->check('redirect', 'Validity::length', 0, 200);
  1452.  
  1453. if ($revision_changed) {
  1454. $valid->check('changes_made', 'Validity::length', 0, 250);
  1455. }
  1456.  
  1457. if ($_POST['status'] == 'need_approval') {
  1458. $valid->required(['approval_operator_id']);
  1459.  
  1460. try {
  1461. $q = "SELECT * FROM ~operators WHERE ID = ?";
  1462. $approval_operator = Pdb::query($q, [(int) $_POST['approval_operator_id']], 'row');
  1463. } catch (RowMissingException $ex) {
  1464. $valid->addFieldError('approval_operator_id', 'Invalid value');
  1465. }
  1466. }
  1467.  
  1468. if ($_POST['status'] == 'auto_launch') {
  1469. $valid->required(['date_launch']);
  1470. }
  1471.  
  1472. } else if ($page_type == 'tool') {
  1473. $valid->required(['controller_entrance', 'controller_argument']);
  1474. $valid->check('controller_entrance', 'Validity::length', 1, 200);
  1475. if (!self::checkControllerEntrance($_POST['controller_entrance'], $page_id)) {
  1476. $valid->addFieldError('controller_entrance', 'Invalid value');
  1477. }
  1478. } else if ($page_type == 'redirect') {
  1479. $valid->required(['redirect']);
  1480. $valid->check('redirect', function($value) {
  1481. if (!Lnk::valid($_POST['redirect'])) {
  1482. throw new ValidationException('Invalid redirect target');
  1483. }
  1484. });
  1485. }
  1486.  
  1487. if ($orig_page['id'] == $_POST['parent_id']) {
  1488. $valid->addFieldError('parent_id', 'A page can\'t be its own parent.');
  1489. }
  1490.  
  1491. if ($parent_changed) {
  1492. $root_node = Navigation::loadPageTree($_SESSION['admin']['active_subsite'], true);
  1493. $new_parent = $root_node->findNodeValue('id', $_POST['parent_id']);
  1494. $ancestors = $new_parent->findAncestors();
  1495.  
  1496. foreach ($ancestors as $anc) {
  1497. if ($anc['id'] == $page_id) {
  1498. $valid->addFieldError('parent_id', 'You can\'t set a descendent of this page as its parent');
  1499.  
  1500. break;
  1501. }
  1502. }
  1503. }
  1504.  
  1505. // Check validation
  1506. if ($valid->hasErrors()) {
  1507. $_SESSION['admin']['field_values'] = $_POST;
  1508. $_SESSION['admin']['field_errors'] = $valid->getFieldErrors();
  1509. $valid->createNotifications();
  1510. return false;
  1511. }
  1512.  
  1513. $operator = AdminAuth::getDetails();
  1514. if (! $operator) return false;
  1515.  
  1516. // Start transaction
  1517. Pdb::transact();
  1518.  
  1519. // Update page
  1520. $update_fields = [];
  1521. $update_fields['date_modified'] = Pdb::now();
  1522. $update_fields['name'] = $_POST['name'];
  1523. $update_fields['slug'] = Enc::urlname($_POST['slug'], '-');
  1524. $update_fields['active'] = (int) (bool) @$_POST['active'];
  1525. $update_fields['show_in_nav'] = (int) (bool) @$_POST['show_in_nav'];
  1526. $update_fields['meta_keywords'] = $_POST['meta_keywords'];
  1527. $update_fields['meta_description'] = $_POST['meta_description'];
  1528.  
  1529. if ($page_type == 'standard') {
  1530. if ($_POST['stale_age'] === '') {
  1531. $update_fields['stale_age'] = null;
  1532. } else {
  1533. $update_fields['stale_age'] = (int) $_POST['stale_age'];
  1534. }
  1535. }
  1536.  
  1537. $update_fields['alt_browser_title'] = $_POST['alt_browser_title'];
  1538. $update_fields['alt_nav_title'] = $_POST['alt_nav_title'];
  1539. $update_fields['date_expire'] = $_POST['date_expire'];
  1540. $update_fields['modified_editor'] = $operator['name'];
  1541. $update_fields['menu_group'] = (int) @$_POST['menu_group'];
  1542. if (Kohana::config('page.enable_banners')) {
  1543. $update_fields['banner'] = !empty($_POST['banner']) ? $_POST['banner'] : null;
  1544. }
  1545.  
  1546. $update_fields['gallery_thumb'] = !empty($_POST['gallery_thumb']) ? $_POST['gallery_thumb'] : null;
  1547.  
  1548. if ($_POST['type'] == 'standard') {
  1549. $update_fields['alt_template'] = trim(preg_replace('![^-_a-z0-9/]!i', '', $_POST['alt_template']));
  1550.  
  1551.  
  1552. if (Kohana::config('sprout.tweak_skin')) {
  1553. $update_fields['additional_css'] = trim($_POST['additional_css']);
  1554. }
  1555.  
  1556. }
  1557.  
  1558. if (@$_POST['admin_perm_specific'] == 1) {
  1559. $update_fields['admin_perm_type'] = Constants::PERM_SPECIFIC;
  1560. } else {
  1561. $update_fields['admin_perm_type'] = Constants::PERM_INHERIT;
  1562. }
  1563.  
  1564. if (Register::hasFeature('users')) {
  1565. if (@$_POST['user_perm_specific'] == 1) {
  1566. $update_fields['user_perm_type'] = Constants::PERM_SPECIFIC;
  1567. } else {
  1568. $update_fields['user_perm_type'] = Constants::PERM_INHERIT;
  1569. }
  1570. }
  1571.  
  1572. if ($parent_changed) {
  1573. $update_fields['parent_id'] = (int) $_POST['parent_id'];
  1574. $update_fields['record_order'] = 0;
  1575. }
  1576.  
  1577. Pdb::update('pages', $update_fields, ['id' => $page_id]);
  1578.  
  1579. if ($parent_changed) $this->fixRecordOrder($page_id);
  1580.  
  1581. // Update revision - if the text actually changed
  1582. if ($revision_changed) {
  1583.  
  1584. $update_fields = [];
  1585. $update_fields['type'] = $_POST['type'];
  1586. $update_fields['status'] = $_POST['status'];
  1587. $update_fields['operator_id'] = $operator['id'];
  1588. $update_fields['modified_editor'] = $operator['name'];
  1589. $update_fields['date_launch'] = $_POST['date_launch'];
  1590. $update_fields['date_modified'] = Pdb::now();
  1591. $update_fields['changes_made'] = $_POST['changes_made'];
  1592.  
  1593. if ($_POST['type'] == 'redirect') {
  1594. $update_fields['redirect'] = $_POST['redirect'];
  1595. } else if ($_POST['type'] == 'tool') {
  1596. $update_fields['controller_entrance'] = $_POST['controller_entrance'];
  1597. $update_fields['controller_argument'] = $_POST['controller_argument'];
  1598. }
  1599.  
  1600. if ($orig_rev['status'] == 'wip' or $orig_rev['status'] == 'auto_launch') {
  1601. // Update the selected revision
  1602. Pdb::update('page_revisions', $update_fields, ['page_id' => $page_id, 'id' => $rev_id]);
  1603.  
  1604. $res = $this->addHistoryItem($page_id, "Updated revision {$rev_id}");
  1605. if (! $res) return false;
  1606.  
  1607. } else {
  1608. // Create a new revision
  1609. $update_fields['page_id'] = $page_id;
  1610. $update_fields['approval_operator_id'] = (int) @$_POST['approval_operator_id'];
  1611. $update_fields['date_added'] = Pdb::now();
  1612.  
  1613. $rev_id = Pdb::insert('page_revisions', $update_fields);
  1614.  
  1615. $res = $this->addHistoryItem($page_id, "Created new revision {$rev_id}");
  1616. if (! $res) return false;
  1617. }
  1618.  
  1619. // Mark all other live revisions as being old
  1620. if ($_POST['status'] == 'live') {
  1621. Page::activateRevision($rev_id);
  1622. }
  1623.  
  1624. // Widgets
  1625. Pdb::delete('page_widgets', ['page_revision_id' => $rev_id]);
  1626.  
  1627. foreach ($new_widgets as $widget) {
  1628. $update_fields = $widget;
  1629. $update_fields['page_revision_id'] = $rev_id;
  1630. Pdb::insert('page_widgets', $update_fields);
  1631. }
  1632. }
  1633.  
  1634. // If the save is also requesting approval, generate an approval code
  1635. if ($_POST['status'] == 'need_approval') {
  1636. $approval_code = Security::randStr(12);
  1637. $update_fields = [];
  1638. $update_fields['approval_code'] = $approval_code;
  1639. Pdb::update('page_revisions', $update_fields, ['page_id' => $page_id, 'id' => $rev_id]);
  1640. }
  1641.  
  1642. // Notification
  1643. if (empty($this->in_preview)) {
  1644. switch ($_POST['status']) {
  1645. case 'need_approval':
  1646. Notification::confirm('Your new revision has been saved, and is pending approval');
  1647. Notification::confirm('Now showing current live revision');
  1648. break;
  1649. case 'auto_launch':
  1650. $msg = 'Your new revision has been saved and will be published on ';
  1651. $msg .= date('l, j/n/Y', strtotime($_POST['date_launch']));
  1652. Notification::confirm($msg);
  1653. Notification::confirm('Now showing current live revision');
  1654. break;
  1655. case 'live':
  1656. Notification::confirm('Your changes have been saved and are now live');
  1657. break;
  1658. default:
  1659. Notification::confirm('Your changes have been saved');
  1660. }
  1661. }
  1662.  
  1663.  
  1664. // Admin permissions
  1665. Pdb::delete('page_admin_permissions', ['item_id' => $page_id]);
  1666.  
  1667. if (@$_POST['admin_perm_specific'] == 1 and @count($_POST['admin_permissions'])) {
  1668. foreach ($_POST['admin_permissions'] as $id) {
  1669. $id = (int) $id;
  1670. if ($id == 0) continue;
  1671.  
  1672. // Create a new permission record
  1673. $update_fields = array();
  1674. $update_fields['item_id'] = $page_id;
  1675. $update_fields['category_id'] = $id;
  1676.  
  1677. Pdb::insert('page_admin_permissions', $update_fields);
  1678. }
  1679. }
  1680.  
  1681.  
  1682. // User permissions
  1683. if (Register::hasFeature('users')) {
  1684. Pdb::delete('page_user_permissions', ['item_id' => $page_id]);
  1685.  
  1686. if (@$_POST['user_perm_specific'] == 1 and @count($_POST['user_permissions'])) {
  1687. foreach ($_POST['user_permissions'] as $id) {
  1688. $id = (int) $id;
  1689. if ($id == 0) continue;
  1690.  
  1691. // Create a new permission record
  1692. $update_fields = array();
  1693. $update_fields['item_id'] = $page_id;
  1694. $update_fields['category_id'] = $id;
  1695.  
  1696. Pdb::insert('page_user_permissions', $update_fields);
  1697. }
  1698. }
  1699. }
  1700.  
  1701.  
  1702. // Custom attributes
  1703. Pdb::delete('page_attributes', ['page_id' => $page_id]);
  1704.  
  1705. if (@count($_POST['multiedit_attrs'])) {
  1706. foreach ($_POST['multiedit_attrs'] as $idx => $data) {
  1707. if (MultiEdit::recordEmpty($data)) continue;
  1708.  
  1709. $update_fields = array();
  1710. $update_fields['page_id'] = (int) $page_id;
  1711. $update_fields['name'] = $data['name'];
  1712. $update_fields['value'] = $data['value'];
  1713.  
  1714. Pdb::insert('page_attributes', $update_fields);
  1715. }
  1716. }
  1717.  
  1718.  
  1719. // Do indexing on the page text, which is found in the embedded widgets
  1720. $text = Page::getText($page_id, $rev_id, $_SESSION['admin']['active_subsite']);
  1721.  
  1722. if ($revision_changed) {
  1723. if ($_POST['status'] == 'need_approval') {
  1724. // An email to the operator who is checking the revision
  1725. $view = new View('sprout/email/page_need_check');
  1726. $view->page = $_POST;
  1727. $view->approval_operator = $approval_operator;
  1728. $view->request_operator = $operator;
  1729. $view->url = Sprout::absRoot() . "page/view_specific_rev/{$page_id}/{$rev_id}/{$approval_code}";
  1730. $view->changes_made = $_POST['changes_made'];
  1731.  
  1732. $mail = new Email();
  1733. $mail->AddAddress($approval_operator['email']);
  1734. $mail->Subject = 'Page change approval required for ' . Kohana::config('sprout.site_title');
  1735. $mail->SkinnedHTML($view->render());
  1736. $mail->Send();
  1737.  
  1738. Notification::confirm("An email has been sent to {$approval_operator['name']}");
  1739.  
  1740. } else if ($_POST['status'] == 'live' and Kohana::config('sprout.update_notify')) {
  1741. // A notification to all operators
  1742. $view = new View('sprout/email/page_notify');
  1743. $view->page = $_POST;
  1744. $view->request_operator = $operator;
  1745. $view->url = Sprout::absRoot() . "page/view_specific_rev/{$page_id}/{$rev_id}";
  1746. $view->changes_made = $_POST['changes_made'];
  1747.  
  1748. $email_sql = $operator['email'];
  1749. $q = "SELECT email FROM ~operators WHERE email != '' AND email != ?";
  1750. $res = Pdb::q($q, [$email_sql], 'pdo');
  1751. foreach ($res as $row) {
  1752. $mail = new Email();
  1753. $mail->AddAddress($row['email']);
  1754. $mail->Subject = 'Page updated on site ' . Kohana::config('sprout.site_title') . ': ' . $_POST['name'];
  1755. $mail->SkinnedHTML($view->render());
  1756. $mail->Send();
  1757. }
  1758. $res->closeCursor();
  1759. }
  1760. }
  1761.  
  1762. $res = $this->reindexItem($page_id, $_POST['name'], $text);
  1763. if (!$res) Notification::error('Failed to index page text');
  1764.  
  1765.  
  1766. if ($page_type != $orig_page['type']) {
  1767. $this->addHistoryItem($page_id, "Changed the page type");
  1768. if (empty($this->in_preview)) Notification::confirm('Page type has been changed.');
  1769. }
  1770.  
  1771.  
  1772. // Commit
  1773. Pdb::commit();
  1774.  
  1775. Navigation::clearCache();
  1776.  
  1777. // Make sure operator is sent to revision they just modified,
  1778. // instead of going to the current live revision
  1779. if (empty($this->in_preview)) {
  1780. return "admin/edit/page/{$page_id}?revision={$rev_id}";
  1781. }
  1782.  
  1783. return true;
  1784. }
  1785.  
  1786.  
  1787. /**
  1788.   * Page organisation tool
  1789.   * Bulk renaming, reordering and reparenting
  1790.   **/
  1791. public function _extraOrganise()
  1792. {
  1793. $view = new View('sprout/admin/tree_organise');
  1794. $view->root = Navigation::getRootNode();
  1795. $view->controller_name = $this->controller_name;
  1796.  
  1797. return array(
  1798. 'title' => 'Organise pages',
  1799. 'content' => $view->render()
  1800. );
  1801. }
  1802.  
  1803.  
  1804. /**
  1805.   * Page organisation tool
  1806.   * Bulk renaming, reordering and reparenting
  1807.   **/
  1808. public function _extraMenuGroups()
  1809. {
  1810. $view = new View('sprout/admin/page_menu_groups');
  1811. $view->all_groups = NavigationGroups::getAllGroupsAdmin();
  1812. $view->all_extras = NavigationGroups::getAllExtrasAdmin();
  1813.  
  1814. $enabled_extras = Subsites::getConfigAdmin('nav_extras');
  1815. if ($enabled_extras === null) {
  1816. $enabled_extras = [];
  1817. }
  1818. $view->enabled_extras = $enabled_extras;
  1819.  
  1820. return array(
  1821. 'title' => 'Manage menu groups',
  1822. 'content' => $view->render()
  1823. );
  1824. }
  1825.  
  1826.  
  1827. /**
  1828.   * Save the menu groups
  1829.   **/
  1830. public function menuGroupsAction()
  1831. {
  1832. AdminAuth::checkLogin();
  1833. Csrf::checkOrDie();
  1834.  
  1835. $enabled_extras = Subsites::getConfigAdmin('nav_extras');
  1836. if ($enabled_extras === null) {
  1837. $enabled_extras = array();
  1838. }
  1839.  
  1840. $all_groups = NavigationGroups::getAllGroupsAdmin();
  1841. foreach ($all_groups as $page_id => $groups) {
  1842. foreach ($groups as $id => $name) {
  1843. $update_data = array();
  1844. $update_data['name'] = @$_POST['groups'][$id]['name'];
  1845. $update_data['date_modified'] = Pdb::now();
  1846.  
  1847. $conditions = array();
  1848. $conditions['subsite_id'] = (int)$_SESSION['admin']['active_subsite'];
  1849. $conditions['page_id'] = (int)$page_id;
  1850. $conditions['id'] = (int)$id;
  1851.  
  1852. Pdb::update('menu_groups', $update_data, $conditions);
  1853. }
  1854.  
  1855. if (array_sum($enabled_extras)) {
  1856. $update_data = array();
  1857. $update_data['subsite_id'] = (int)$_SESSION['admin']['active_subsite'];
  1858. $update_data['page_id'] = (int)$page_id;
  1859.  
  1860. if (!empty($enabled_extras['text'])) {
  1861. $update_data['text'] = $_POST['extras'][$page_id]['text'];
  1862. }
  1863. if (!empty($enabled_extras['image'])) {
  1864. if (empty($_POST['extras'][$page_id]['image'])) {
  1865. $_POST['extras'][$page_id]['image'] = null;
  1866. }
  1867. $update_data['image'] = $_POST['extras'][$page_id]['image'];
  1868. }
  1869.  
  1870. try {
  1871. Pdb::insert('menu_extras', $update_data);
  1872. } catch (Exception $ex) {
  1873. if (strpos($ex, 'Duplicate entry') === false) throw $ex;
  1874. unset($update_data['page_id']);
  1875. Pdb::update('menu_extras', $update_data, array('page_id' => $page_id));
  1876. }
  1877. }
  1878. }
  1879.  
  1880. Notification::confirm('Your changes have been saved');
  1881. Url::redirect('admin/extra/page/menu_groups');
  1882. }
  1883.  
  1884.  
  1885. /**
  1886.   * Adds a history item for a page
  1887.   **/
  1888. private function addHistoryItem($page_id, $changes_made, $editor = null)
  1889. {
  1890. if ($editor == null) {
  1891. $operator = AdminAuth::getDetails();
  1892. if (! $operator) return false;
  1893. $editor = $operator['name'];
  1894. }
  1895.  
  1896. // Create a new revision
  1897. $update_fields = array();
  1898. $update_fields['page_id'] = $page_id;
  1899. $update_fields['modified_editor'] = $editor;
  1900. $update_fields['changes_made'] = $changes_made;
  1901. $update_fields['date_added'] = Pdb::now();
  1902.  
  1903. $res = Pdb::insert('page_history_items', $update_fields);
  1904.  
  1905. return true;
  1906. }
  1907.  
  1908. /**
  1909.   * Returns the edit form for editing the specified page
  1910.   *
  1911.   * @param int $id The record to show the edit form for
  1912.   * @return string The HTML code which represents the edit form
  1913.   **/
  1914. public function _getDeleteForm($id)
  1915. {
  1916. $id = (int) $id;
  1917.  
  1918. $view = new View('sprout/admin/page_delete');
  1919. $view->id = $id;
  1920.  
  1921. // Load page details
  1922. $q = "SELECT * FROM ~pages WHERE id = ?";
  1923. try {
  1924. $view->page = Pdb::q($q, [$id], 'row');
  1925. } catch (QueryException $ex) {
  1926. return new AdminError("Invalid id specified - page does not exist");
  1927. }
  1928.  
  1929. // Check permissions
  1930. $res = AdminPerms::checkPermissionsTree('pages', $id);
  1931. if (! $res) return new AdminError("Access denied to delete this page");
  1932.  
  1933. // Children pages
  1934. $root = Navigation::getRootNode();
  1935. $node = $root->findNodeValue('id', $id);
  1936. $child_pages = ($node ? $node->children : []);
  1937. foreach ($child_pages as $child) {
  1938. if (!$child->children) continue;
  1939. foreach ($child->children as $descendent) {
  1940. $child_pages[] = $descendent;
  1941. }
  1942. }
  1943. $view->child_pages = $child_pages;
  1944.  
  1945. return array(
  1946. 'title' => 'Deleting page <strong>' . Enc::html($view->page['name']) . '</strong>',
  1947. 'content' => $view->render()
  1948. );
  1949. }
  1950.  
  1951.  
  1952. /**
  1953.   * Does custom actions before _deleteSave method is called, e.g. extra security checks
  1954.   * @param int $item_id The record to delete
  1955.   * @return void
  1956.   * @throws Exception if the deletion shouldn't proceed for some reason
  1957.   */
  1958. public function _deletePreSave($item_id)
  1959. {
  1960. if (!AdminPerms::checkPermissionsTree('pages', $item_id)) {
  1961. throw new Exception('Permission denied');
  1962. }
  1963. }
  1964.  
  1965.  
  1966. /**
  1967.   * Does custom actions after the _deleteSave method is called, e.g. clearing cache data
  1968.   * @param int $item_id The record to delete
  1969.   * @return void
  1970.   */
  1971. public function _deletePostSave($item_id)
  1972. {
  1973. Navigation::clearCache();
  1974. }
  1975.  
  1976.  
  1977. /**
  1978.   * Shows the links (incoming and outgoing) for a page
  1979.   **/
  1980. public function _extraLinks($id)
  1981. {
  1982. $id = (int) $id;
  1983.  
  1984. $res = AdminPerms::checkPermissionsTree('pages', $id);
  1985. if (! $res) return "Access denied to view this page";
  1986.  
  1987. $view = new View('sprout/admin/page_linklist');
  1988. $view->id = $id;
  1989.  
  1990. // Load page details
  1991. $q = "SELECT pages.name, revs.text
  1992. FROM ~pages AS pages
  1993. INNER JOIN ~page_revisions AS revs
  1994. ON revs.page_id = pages.id AND revs.status = ?
  1995. WHERE pages.id = ?";
  1996. $view->page = Pdb::q($q, ['live', $id], 'row');
  1997.  
  1998. $root = Navigation::getRootNode();
  1999. $node = $root->findNodeValue('id', $id);
  2000. $url = $node->getFriendlyUrl();
  2001.  
  2002. // Incoming links
  2003. $q = "SELECT pages.id, pages.name, revs.text
  2004. FROM ~pages AS pages
  2005. INNER JOIN ~page_revisions AS revs
  2006. ON revs.page_id = pages.id
  2007. AND revs.status = ?
  2008. WHERE revs.text LIKE CONCAT('%', ?, '%')";
  2009. $res = Pdb::q($q, ['live', Pdb::likeEscape($url)], 'pdo');
  2010.  
  2011. $items = array();
  2012. foreach ($res as $row) {
  2013. $matches = array();
  2014. $match = preg_match('/<a.*?href="' . preg_quote($url, '/') . '".*?>(.+?)<\/a>/', $row->text, $matches);
  2015.  
  2016. if ($match) {
  2017. $items[] = array('id' => $row['id'], 'name' => $row['name'], 'text' => $matches[1]);
  2018. }
  2019. }
  2020. $res->closeCursor();
  2021.  
  2022. $list = new Itemlist();
  2023. $list->items = $items;
  2024. $list->main_columns = array(
  2025. 'Page' => 'name',
  2026. 'Link text' => 'text',
  2027. );
  2028. $list->addAction('edit', "SITE/admin/edit/{$this->controller_name}/%%");
  2029. $view->incoming = $list->render();
  2030.  
  2031.  
  2032. // Outgoing links
  2033. $matches = array();
  2034. $res = preg_match_all('/<a.*?href="(.+?)".*?>(.+?)<\/a>/i', $view->page->text, $matches, PREG_SET_ORDER);
  2035.  
  2036. $items = array();
  2037. foreach ($matches as $match) {
  2038. $items[] = array('id' => $match[1], 'url' => $match[1], 'text' => $match[2]);
  2039. }
  2040.  
  2041. $list = new Itemlist();
  2042. $list->items = $items;
  2043. $list->main_columns = array(
  2044. 'URL' => 'url',
  2045. 'Link text' => 'text',
  2046. );
  2047. $list->addAction('edit', '%ne%');
  2048. $view->outgoing = $list->render();
  2049.  
  2050.  
  2051. return array(
  2052. 'title' => 'Links for page <strong>' . Enc::html($view->page->name) . '</strong>',
  2053. 'content' => $view->render()
  2054. );
  2055. }
  2056.  
  2057.  
  2058.  
  2059. /**
  2060.   * Returns the intro HTML for this controller.
  2061.   * Looks for a view named "admin/<controller-name>_intro", and loads it if found.
  2062.   * Otherwise, loads the view, "admin/generic_intro".
  2063.   **/
  2064. public function _intro()
  2065. {
  2066. $intro = new View("sprout/admin/page_intro");
  2067. $intro->controller_name = $this->controller_name;
  2068. $intro->friendly_name = $this->friendly_name;
  2069.  
  2070.  
  2071. // Recently updated pages
  2072. $q = "SELECT pages.id, pages.name, DATE_FORMAT(pages.date_modified, '%d/%m/%Y') AS date_modified,
  2073. pages.modified_editor
  2074. FROM ~pages AS pages
  2075. WHERE subsite_id = ?
  2076. ORDER BY pages.date_modified DESC
  2077. LIMIT 5";
  2078. $res = Pdb::q($q, [$_SESSION['admin']['active_subsite']], 'arr');
  2079.  
  2080. // Create the itemlist
  2081. $itemlist = new Itemlist();
  2082. $itemlist->main_columns = array('Name' => 'name', 'Date modified' => 'date_modified', 'Editor' => 'modified_editor');
  2083. $itemlist->items = $res;
  2084. $itemlist->addAction('edit', "SITE/admin/edit/{$this->controller_name}/%%");
  2085.  
  2086. $intro->recently_updated = $itemlist->render();
  2087.  
  2088.  
  2089. // Changes needing approval
  2090. if (AdminPerms::canAccess('access_noapproval')) {
  2091. $q = "SELECT pages.id, pages.name, DATE_FORMAT(page_revisions.date_modified, '%d/%m/%Y') AS date_modified,
  2092. page_revisions.modified_editor
  2093. FROM ~page_revisions AS page_revisions
  2094. INNER JOIN ~pages AS pages ON page_revisions.page_id = pages.id
  2095. WHERE page_revisions.status = ?
  2096. AND subsite_id = ?
  2097. ORDER BY page_revisions.date_modified DESC
  2098. LIMIT 5";
  2099. $res = Pdb::q($q, ['need_approval', $_SESSION['admin']['active_subsite']], 'arr');
  2100.  
  2101. // Create the itemlist
  2102. $itemlist = new Itemlist();
  2103. $itemlist->main_columns = [
  2104. 'Name' => 'name',
  2105. 'Date modified' => 'date_modified',
  2106. 'Editor' => 'modified_editor'
  2107. ];
  2108. $itemlist->items = $res;
  2109. $itemlist->addAction('edit', 'SITE/admin/edit/page/%%#main-tabs-revs');
  2110.  
  2111. $intro->need_approval = $itemlist->render();
  2112. }
  2113.  
  2114.  
  2115. return $intro->render();
  2116. }
  2117.  
  2118.  
  2119. /**
  2120.   * Does a re-index for a page
  2121.   **/
  2122. private function reindexItem($item_id, $name, $text)
  2123. {
  2124. Search::selectIndex('page_keywords', $item_id);
  2125.  
  2126. $res = Search::clearIndex();
  2127. if (! $res) return false;
  2128.  
  2129. $res = Search::indexHtml($text, 1);
  2130. if (! $res) return false;
  2131.  
  2132. $res = Search::indexText($name, 4);
  2133. if (! $res) return false;
  2134.  
  2135. $res = Search::cleanup('pages');
  2136. if (! $res) return false;
  2137.  
  2138. return true;
  2139. }
  2140.  
  2141. /**
  2142.   * Does a complete re-index of all pages
  2143.   **/
  2144. public function reindexAll()
  2145. {
  2146. AdminAuth::checkLogin();
  2147.  
  2148. Pdb::transact();
  2149.  
  2150. $q = "SELECT pages.id, pages.name, pages.active, widget.settings
  2151. FROM ~pages AS pages
  2152. INNER JOIN ~page_revisions AS rev
  2153. ON rev.page_id = pages.id AND rev.status = ?
  2154. LEFT JOIN ~page_widgets AS widget ON rev.id = widget.page_revision_id
  2155. AND widget.area_id = 1 AND widget.active = 1 AND widget.type = 'RichText'
  2156. ORDER BY widget.record_order";
  2157. $res = Pdb::q($q, ['live'], 'pdo');
  2158.  
  2159. $pages = [];
  2160. foreach ($res as $row) {
  2161. if ($row['settings'] == null or $row['active'] == 0) {
  2162. $pages[$row['id']] = ['name' => $row['name'], 'text' => ''];
  2163. continue;
  2164. }
  2165. $settings = json_decode($row['settings'], true);
  2166. if (!isset($pages[$row['id']])) {
  2167. $pages[$row['id']] = ['name' => $row['name'], 'text' => $settings['text']];
  2168. } else {
  2169. $pages[$row['id']]['text'] .= "\n" . $settings['text'];
  2170. }
  2171. }
  2172. $res->closeCursor();
  2173.  
  2174. if (count($pages) == 0) {
  2175. echo '<p>Nothing to index</p>';
  2176. Pdb::rollback();
  2177. return;
  2178. }
  2179.  
  2180. foreach ($pages as $id => $page) {
  2181. $this->reindexItem($id, $page['name'], $page['text']);
  2182. }
  2183.  
  2184. Pdb::commit();
  2185.  
  2186. echo '<p>Success</p>';
  2187. }
  2188.  
  2189.  
  2190. /**
  2191.   * Validate given controller implements FrontEndEntrance
  2192.   *
  2193.   * @param string $controller Controller class
  2194.   * @param int $page_id Page record ID
  2195.   * @return bool True on success. False on failure
  2196.   */
  2197. private static function checkControllerEntrance($controller, $page_id)
  2198. {
  2199. $page_id = (int) $page_id;
  2200.  
  2201. $front_end_controllers = Register::getFrontEndControllers();
  2202. if (empty($front_end_controllers[$controller])) return false;
  2203.  
  2204. $inst = Sprout::instance($controller);
  2205. if (!($inst instanceof FrontEndEntrance)) return false;
  2206.  
  2207. return true;
  2208. }
  2209.  
  2210.  
  2211. /**
  2212.   * Returns the children pages for a specific page, in a format required by jqueryFileTree.
  2213.   * Uses the POST param 'dir', and is usually run through an AJAX call.
  2214.   **/
  2215. public function filetreeOpen()
  2216. {
  2217. AdminAuth::checkLogin();
  2218.  
  2219. $_POST['dir'] = trim($_POST['dir']);
  2220. $_GET['record_id'] = (int) $_GET['record_id'];
  2221.  
  2222. Navigation::loadPageTree($_SESSION['admin']['active_subsite'], true);
  2223.  
  2224. $root = Navigation::getRootNode();
  2225. $top_node = $root->findNodeValue('id', basename($_POST['dir']));
  2226.  
  2227. $nav_limit = Subsites::getConfigAdmin('nav_limit');
  2228. if (! $nav_limit) $nav_limit = 99999;
  2229. if (Subsites::getConfigAdmin('nav_home')) $nav_limit--;
  2230.  
  2231. echo "<ul class=\"jqueryFileTree\" style=\"display: none;\">";
  2232.  
  2233. // This item
  2234. $dir_item_path = preg_replace('!^/(.+)/$!', '$1', $_POST['dir']);
  2235. if ($dir_item_path != '/') {
  2236. $dir_item_name = basename($_POST['dir']);
  2237.  
  2238. $name = $top_node['name'];
  2239. if (strlen($name) > 25) $name = substr($name, 0, 25) . '...';
  2240. $name = Enc::html($name);
  2241.  
  2242. $rel = Enc::html('/' . $dir_item_path);
  2243.  
  2244. $admin_perms = AdminPerms::checkPermissionsTree('pages', $top_node['id']);
  2245.  
  2246. $class = ($admin_perms ? ' allow-access' : ' no-access');
  2247. if ($top_node['id'] == $_GET['record_id']) $class .= ' current-edit';
  2248. echo "<li class=\"file ext_txt{$class} directory-item\"><a href=\"#\" rel=\"{$rel}\">{$name}</a></li>";
  2249. }
  2250.  
  2251. // Children of this item
  2252. foreach ($top_node->children as $child) {
  2253. $name = $child['name'];
  2254. if (strlen($name) > 25) $name = substr($name, 0, 25) . '...';
  2255. $name = Enc::html($name);
  2256.  
  2257. $rel = Enc::html($_POST['dir'] . $child['id']);
  2258.  
  2259. $admin_perms = AdminPerms::checkPermissionsTree('pages', $child['id']);
  2260.  
  2261. $class = ($admin_perms ? ' allow-access' : ' no-access');
  2262. if (count($child->children) > 0 and $admin_perms) {
  2263. echo "<li class=\"directory collapsed{$class}\"><a href=\"#\" rel=\"{$rel}/\">{$name}</a></li>";
  2264. } else {
  2265. if ($child['id'] == $_GET['record_id']) $class .= ' current-edit';
  2266. echo "<li class=\"file ext_txt{$class}\"><a href=\"#\" rel=\"{$rel}\">{$name}</a></li>";
  2267. }
  2268.  
  2269. if ($dir_item_path == '/') {
  2270. $nav_limit--;
  2271. if ($nav_limit == 0) {
  2272. echo "<li class=\"nav-limit\">&nbsp;</li>";
  2273. }
  2274. }
  2275. }
  2276.  
  2277. echo "</ul>";
  2278.  
  2279. if ($dir_item_path != '/') {
  2280. echo "<p class=\"tree-extras\">";
  2281. echo "&#43; <a href=\"SITE/admin/add/page?parent_id={$top_node['id']}\">Add Child</a>";
  2282. echo " &nbsp; ";
  2283. echo "&#8597; <a href=\"SITE/page/reorder/{$top_node['id']}\" onclick=\"$.facebox({'ajax':this.href}); return false;\">Re-order</a>";
  2284. echo "</p>";
  2285. }
  2286.  
  2287. if ($_SESSION['admin'][$this->controller_name . '_nav'] == null) $_SESSION['admin'][$this->controller_name . '_nav'] = array();
  2288. if ($_POST['dir'] != '/' and !in_array ($_POST['dir'], $_SESSION['admin'][$this->controller_name . '_nav'])) {
  2289. $_SESSION['admin'][$this->controller_name . '_nav'][] = $_POST['dir'];
  2290. }
  2291. }
  2292.  
  2293. /**
  2294.   * Saves in the session data the currently open pages in the pages tree (navigation pane)
  2295.   * Uses the POST param 'pages', and is usually run through an AJAX call.
  2296.   **/
  2297. public function filetreeClose()
  2298. {
  2299. AdminAuth::checkLogin();
  2300.  
  2301. if (empty($_SESSION['admin'][$this->controller_name . '_nav'])) return;
  2302.  
  2303. $index = array_search ($_POST['dir'], $_SESSION['admin'][$this->controller_name . '_nav']);
  2304. unset ($_SESSION['admin'][$this->controller_name . '_nav'][$index]);
  2305. }
  2306.  
  2307.  
  2308. /**
  2309.   * Shows the reorder screen (which is shown in a popup box) for re-ordering the top-level stuff
  2310.   * This custom version adds subsite support
  2311.   **/
  2312. public function reorderTop()
  2313. {
  2314. AdminAuth::checkLogin();
  2315.  
  2316. // Get children
  2317. $q = "SELECT id, name
  2318. FROM ~{$this->table_name}
  2319. WHERE subsite_id = ? AND parent_id = 0
  2320. ORDER BY record_order";
  2321. $children = Pdb::q($q, [$_SESSION['admin']['active_subsite']], 'arr');
  2322.  
  2323. // If there is only one child, complain that it's impossible to re-order
  2324. if (count($children) == 1) {
  2325. echo "<p>This site does not have enough top-level items for ordering.</p>";
  2326. return;
  2327. }
  2328.  
  2329. // View
  2330. $view = new View('sprout/admin/categories_reorder');
  2331. $view->id = 0;
  2332. $view->items = $children;
  2333. $view->controller_name = $this->controller_name;
  2334. $view->friendly_name = $this->friendly_name;
  2335.  
  2336. echo $view->render();
  2337. }
  2338.  
  2339. /**
  2340.   * If the specified item needs a record number to be set,
  2341.   * Puts this item at the end of the list.
  2342.   *
  2343.   * This custom version adds subsite support
  2344.   *
  2345.   * @param int $item_id Record-id to update
  2346.   */
  2347. protected function fixRecordOrder($item_id)
  2348. {
  2349. $q = "SELECT record_order, subsite_id, parent_id FROM ~{$this->table_name} WHERE id = ?";
  2350. $item = Pdb::q($q, [$item_id], 'row');
  2351.  
  2352. if ($item['record_order'] != 0) return;
  2353.  
  2354. $q = "SELECT MAX(record_order) AS m
  2355. FROM ~{$this->table_name}
  2356. WHERE subsite_id = ?
  2357. AND parent_id = ?";
  2358. try {
  2359. $max = Pdb::q($q, [$item['subsite_id'], $item['parent_id']], 'val');
  2360. } catch (QueryException $ex) {
  2361. return;
  2362. }
  2363.  
  2364. $q = "UPDATE ~{$this->table_name} SET record_order = ? WHERE id = ?";
  2365. Pdb::q($q, [$max + 1, $item_id], 'count');
  2366. }
  2367.  
  2368.  
  2369. /**
  2370.   * Activates AutoLaunch revisions
  2371.   **/
  2372. public function cronPageActivate()
  2373. {
  2374. Cron::start('Activate pages');
  2375.  
  2376. try {
  2377. Pdb::transact();
  2378.  
  2379. // Find revisions needing launch
  2380. $q = "SELECT id, page_id
  2381. FROM ~page_revisions
  2382. WHERE status = ? AND date_launch <= NOW()";
  2383. $res = Pdb::q($q, ['auto_launch'], 'arr');
  2384.  
  2385. foreach ($res as $row) {
  2386. Cron::message("Launching revision {$row['id']} on page {$row['page_id']}");
  2387.  
  2388. // Launch revision
  2389. Pdb::update('page_revisions', ['status' => 'live'], ['id' => $row['id']]);
  2390.  
  2391. $this->addHistoryItem($row['page_id'], "AutoLaunched revision {$row['id']}", 'n/a');
  2392.  
  2393. $where = [
  2394. 'page_id' => $row['page_id'],
  2395. ['id', '!=', $row['id']],
  2396. 'status' => 'live',
  2397. ];
  2398. Pdb::update('page_revisions', ['status' => 'old'], $where);
  2399. }
  2400.  
  2401. Pdb::commit();
  2402. } catch (QueryException $ex) {
  2403. return Cron::failure('Database error');
  2404. }
  2405.  
  2406. Navigation::clearCache();
  2407.  
  2408. Cron::success();
  2409. }
  2410.  
  2411.  
  2412. /**
  2413.   * Deactivates pages after set date
  2414.   **/
  2415. public function cronPageDeactivate()
  2416. {
  2417. Cron::start('De-activate pages');
  2418.  
  2419. try {
  2420. Pdb::transact();
  2421.  
  2422. // Find revisions needing launch
  2423. $q = "SELECT id
  2424. FROM ~pages
  2425. WHERE active = 1
  2426. AND date_expire != '0000-00-00'
  2427. AND date_expire IS NOT NULL
  2428. AND date_expire <= NOW()";
  2429. $res = Pdb::q($q, [], 'arr');
  2430.  
  2431. foreach ($res as $row) {
  2432. Cron::message("De-activating page {$row['id']}");
  2433.  
  2434. // Deactivate page
  2435. Pdb::update('pages', ['active' => 0], ['id' => $row['id']]);
  2436.  
  2437. $this->addHistoryItem($row['id'], "Deactivated page {$row['id']}", 'n/a');
  2438. }
  2439.  
  2440. Pdb::commit();
  2441. } catch (QueryException $ex) {
  2442. return Cron::failure('Database error');
  2443. }
  2444.  
  2445. Navigation::clearCache();
  2446.  
  2447. Cron::success();
  2448. }
  2449.  
  2450.  
  2451. /**
  2452.   * Returns the tools to show in the left navigation
  2453.   **/
  2454. public function _getTools()
  2455. {
  2456. $items = array();
  2457.  
  2458. if (count(Register::getDocImports()) > 0) {
  2459. $items[] = "<li class=\"import\"><a href=\"SITE/admin/import_upload/page\">Document import</a></li>";
  2460. }
  2461.  
  2462. $items[] = "<li class=\"action-log\"><a href=\"SITE/admin/search/{$this->controller_name}\">Search pages</a></li>";
  2463.  
  2464. if (AdminPerms::canAccess('access_noapproval')) {
  2465. $items[] = "<li class=\"action-log\"><a href=\"admin/extra/page/need_approval\">Pages needing approval</a></li>";
  2466. }
  2467.  
  2468. if (AdminAuth::isSuper() or Subsites::getConfigAdmin('nav_reorder')) {
  2469.  
  2470. $items[] = "<li class=\"reorder\"><a href=\"admin/call/{$this->controller_name}/reorderTop\" onclick=\"$.facebox({'ajax':this.href}); return false;\">Reorder top-level</a></li>";
  2471. }
  2472.  
  2473. if (AdminAuth::isSuper()) {
  2474. $items[] = "<li class=\"config\"><a href=\"admin/extra/page/organise\">Sitemap manager</a></li>";
  2475. }
  2476.  
  2477. if (Subsites::getConfigAdmin('nav_groups') !== null) {
  2478. $items[] = "<li class=\"config\"><a href=\"admin/extra/page/menu_groups\">Manage menu groups</a></li>";
  2479. }
  2480.  
  2481. $items[] = "<li class=\"config\"><a href=\"admin/extra/page/link_checker\">Link checker</a></li>";
  2482.  
  2483. if (Kohana::config('cache.enabled')) {
  2484. $items[] = "<li class=\"config\"><a href=\"page/clear_navigation_cache\">Clear navigation cache</a></li>";
  2485. }
  2486.  
  2487. if ($this->_actionLog()) {
  2488. $tools[] = '<li class="action-log"><a href="SITE/admin/contents/action_log?record_table=' . $this->getTableName() . '">View action log</a></li>';
  2489. }
  2490.  
  2491. return $items;
  2492. }
  2493.  
  2494.  
  2495. /**
  2496.   * Starts the link checker
  2497.   **/
  2498. public function _extraLinkChecker()
  2499. {
  2500. $view = new View('sprout/admin/link_checker');
  2501. $view->ops = AdminPerms::getOperatorsWithAccess('access_reportemail');
  2502.  
  2503. $details = AdminAuth::getDetails();
  2504. $view->email = $details['email'];
  2505.  
  2506. return array(
  2507. 'title' => 'Link Checker',
  2508. 'content' => $view->render(),
  2509. );
  2510. }
  2511.  
  2512.  
  2513. /**
  2514.   * Starts the link checker
  2515.   **/
  2516. public function linkCheckerAction()
  2517. {
  2518. AdminAuth::checkLogin();
  2519. Csrf::checkOrDie();
  2520.  
  2521. $_POST['send_to'] = trim($_POST['send_to']);
  2522. $_POST['email'] = trim($_POST['email']);
  2523.  
  2524. if ($_POST['send_to'] != 'admins' and $_POST['send_to'] != 'specific') {
  2525. Notification::error("Invalid 'send_to' argument");
  2526. Url::redirect('admin/extra/page/link_checker');
  2527. }
  2528.  
  2529. if ($_POST['send_to'] == 'specific' and $_POST['email'] == '') {
  2530. Notification::error("You didn't enter an email address");
  2531. Url::redirect('admin/extra/page/link_checker');
  2532. }
  2533.  
  2534. try {
  2535. if ($_POST['send_to'] == 'admins') {
  2536. $info = WorkerCtrl::start('Sprout\\Helpers\\WorkerLinkChecker');
  2537. } else if ($_POST['send_to'] == 'specific') {
  2538. $info = WorkerCtrl::start('Sprout\\Helpers\\WorkerLinkChecker', $_POST['email']);
  2539. }
  2540. } catch (WorkerJobException $ex) {
  2541. Notification::error("Unable to start background process: {$ex->getMessage()}");
  2542. Url::redirect('admin/extra/page/link_checker');
  2543. }
  2544.  
  2545. Notification::confirm("Background process started");
  2546. Url::redirect('admin/extra/page/link_checker_info/' . $info['job_id']);
  2547. }
  2548.  
  2549.  
  2550. /**
  2551.   * Tells the user the link checker is running
  2552.   **/
  2553. public function _extraLinkCheckerInfo($id)
  2554. {
  2555. $id = (int) $id;
  2556.  
  2557. $out = '';
  2558. $out .= '<h3>Job started</h3>';
  2559. $out .= '<p>The link checker background process has been started.</p>';
  2560. $out .= '<p>An email will be sent once it is complete.</p>';
  2561.  
  2562. return array(
  2563. 'title' => 'Link Checker',
  2564. 'content' => $out,
  2565. );
  2566. }
  2567.  
  2568.  
  2569. /**
  2570.   * List of pages which need approval
  2571.   */
  2572. public function _extraNeedApproval()
  2573. {
  2574. if (!AdminPerms::canAccess('access_noapproval')) {
  2575. return 'Access denied';
  2576. }
  2577.  
  2578. $q = "SELECT pages.id, pages.name, revs.date_modified, revs.modified_editor
  2579. FROM ~page_revisions AS revs
  2580. INNER JOIN ~pages AS pages ON revs.page_id = pages.id
  2581. WHERE revs.status = 'need_approval'
  2582. AND pages.subsite_id = ?
  2583. GROUP BY pages.id
  2584. ORDER BY revs.date_modified DESC
  2585. LIMIT 25";
  2586. $res = Pdb::query($q, [$_SESSION['admin']['active_subsite']], 'arr');
  2587.  
  2588. $itemlist = new Itemlist();
  2589. $itemlist->main_columns = [
  2590. 'Name' => 'name',
  2591. 'Date modified' => [new ColModifierDate(), 'date_modified'],
  2592. 'Editor' => 'modified_editor'
  2593. ];
  2594. $itemlist->items = $res;
  2595. $itemlist->addAction('edit', 'admin/edit/page/%%');
  2596.  
  2597. return array(
  2598. 'title' => 'Pages needing approval',
  2599. 'content' => $itemlist->render(),
  2600. );
  2601. }
  2602.  
  2603.  
  2604. /**
  2605.   * Cron version of the link checker
  2606.   **/
  2607. public function cronLinkChecker()
  2608. {
  2609. Cron::start('Link Checker');
  2610. WorkerCtrl::start('Sprout\\Helpers\\WorkerLinkChecker');
  2611. Cron::success();
  2612. }
  2613.  
  2614.  
  2615. /**
  2616.   * Check for stale page content and send emails regarding stale pages; to be run via cron
  2617.   *
  2618.   * @return void
  2619.   */
  2620. public function cronCheckStale()
  2621. {
  2622. Cron::start('Stale page checker');
  2623.  
  2624. $email = Kohana::config('sprout.stale_page_email');
  2625. $default_max_age = Kohana::config('sprout.stale_page_age');
  2626. $resend_interval = (int) Kohana::config('sprout.stale_page_resend_after');
  2627. if ($resend_interval <= 0) $resend_interval = 7;
  2628.  
  2629. $q = "SELECT page.id, page.subsite_id, page.name,
  2630. rev.modified_editor, DATEDIFF(NOW(), rev.date_modified) AS age,
  2631. op.id AS op_id, op.name AS operator, op.email
  2632. FROM ~pages AS page
  2633. INNER JOIN ~page_revisions AS rev
  2634. ON page.id = rev.page_id AND rev.status = 'live' AND rev.type = 'standard'
  2635. LEFT JOIN ~operators AS op
  2636. ON rev.operator_id = op.id
  2637. WHERE page.active = 1
  2638. AND DATE_SUB(CURDATE(), INTERVAL ? DAY) >= page.stale_reminder_sent
  2639. AND IFNULL(page.stale_age, ?) > 0
  2640. AND DATEDIFF(NOW(), rev.date_modified) >= IFNULL(page.stale_age, ?)
  2641. ORDER BY age DESC, page.id";
  2642. $res = Pdb::q($q, [$resend_interval, $default_max_age, $default_max_age], 'map-arr');
  2643.  
  2644. if (count($res) == 0) {
  2645. Cron::message('No stale pages found');
  2646. Cron::success();
  2647. return;
  2648. }
  2649.  
  2650. $op_emails = [];
  2651.  
  2652. if ($email and !preg_match('/example\.com$/', $email)) {
  2653. $op_emails[0] = ['email' => $email, 'pages' => []];
  2654. }
  2655.  
  2656. foreach ($res as $id => $row) {
  2657. $url = Page::url($id);
  2658. if (!$url) continue;
  2659.  
  2660. $url = Subsites::getAbsRoot($row['subsite_id']) . $url;
  2661.  
  2662. $op_emails[0]['pages'][] = [
  2663. 'id' => $row['id'],
  2664. 'name' => $row['name'],
  2665. 'age' => $row['age'],
  2666. 'editor' => $row['modified_editor'],
  2667. 'url' => $url,
  2668. ];
  2669.  
  2670. if (!$row['email']) continue;
  2671.  
  2672. $op = $row['op_id'];
  2673. if (!isset($op_emails[$op])) {
  2674. $op_emails[$op] = [
  2675. 'email' => $row['email'],
  2676. 'operator' => $row['operator'],
  2677. 'pages' => [],
  2678. ];
  2679. }
  2680.  
  2681. $op_emails[$op]['pages'][] = [
  2682. 'id' => $row['id'],
  2683. 'name' => $row['name'],
  2684. 'age' => $row['age'],
  2685. 'url' => $url,
  2686. ];
  2687. }
  2688.  
  2689. foreach ($op_emails as $id => $details) {
  2690. if (empty($details['email'])) continue;
  2691. if (count($details['pages']) == 0) continue;
  2692.  
  2693. $msg = "Sending email to {$details['email']} about ";
  2694. $msg .= Inflector::numPlural(count($details['pages']), 'page');
  2695. Cron::message($msg);
  2696.  
  2697. $view = new View('sprout/email/pages_stale');
  2698. $view->show_op = ($id == 0);
  2699. $view->pages = $details['pages'];
  2700. $view->base = Sprout::absRoot();
  2701.  
  2702. $mail = new Email();
  2703. $mail->Subject = 'Stale content warning';
  2704. if (!empty($details['operator'])) {
  2705. $mail->addAddress($details['email'], $details['operator']);
  2706. } else {
  2707. $mail->addAddress($details['email']);
  2708. }
  2709. $mail->skinnedHTML($view);
  2710. $mail->send();
  2711. }
  2712.  
  2713. Cron::message("Marking pages as sent");
  2714.  
  2715. $params = [date('Y-m-d')];
  2716. $conds = [['id', 'IN', array_keys($res)]];
  2717. $where = Pdb::buildClause($conds, $params);
  2718.  
  2719. $count = Pdb::q("UPDATE ~pages SET stale_reminder_sent = ? WHERE {$where}", $params, 'count');
  2720. Cron::message(Inflector::numPlural($count, 'page') . " marked as sent");
  2721.  
  2722. Cron::success();
  2723. }
  2724.  
  2725.  
  2726. /**
  2727.   * Forces a clear of the pagecache
  2728.   **/
  2729. public function clearNavigationCache()
  2730. {
  2731. AdminAuth::checkLogin();
  2732.  
  2733. Navigation::clearCache();
  2734.  
  2735. Notification::confirm('Page cache has been cleared');
  2736. Url::redirect('admin/intro/page');
  2737. }
  2738.  
  2739.  
  2740. public function preview($item_id = 0) {
  2741. $item_id = (int) $item_id;
  2742.  
  2743. $tables = [
  2744. 'pages' => 1,
  2745. 'page_revisions' => ['id' => $_POST['rev_id']],
  2746. 'page_widgets' => 0,
  2747. 'page_history_items' => 0,
  2748. 'page_admin_permissions' => 1,
  2749. 'page_user_permissions' => 1,
  2750. 'page_attributes' => 0,
  2751. 'page_keywords' => 0,
  2752. 'search_keywords' => 0,
  2753. ];
  2754.  
  2755. // Make sure the resulting revision is live so it's the revision displayed in the preview, even though it might
  2756. // be saved under a different status once the current administrator is happy with the preview
  2757. $_POST['status'] = 'live';
  2758.  
  2759. $this->in_preview = true;
  2760. $item_id = Preview::load($this, $tables, $item_id);
  2761.  
  2762. $ctlr = new PageController();
  2763. Preview::run($ctlr, 'viewById', [$item_id]);
  2764. }
  2765.  
  2766.  
  2767. /**
  2768.   * Return JSON list of custom widget templates as defined by skin config
  2769.   * AJAX called
  2770.   *
  2771.   * @param string $_GET['template'] Template filename
  2772.   * @return void Echos HTML directly
  2773.   */
  2774. public function ajaxListWidgetTemplates()
  2775. {
  2776. $templates = Kohana::config('sprout.widget_templates');
  2777. Form::setData(['template' => @$_GET['template']]);
  2778. $out = '';
  2779.  
  2780. Form::nextFieldDetails('Template', false);
  2781. $out .= Form::dropdown('template', [], $templates);
  2782.  
  2783. // Render Save button
  2784. $out .= '<div class="-clearfix"><button class="save-changes-save-button button button-green icon-after icon-save" type="submit">Save changes</button></div>';
  2785.  
  2786. echo $out;
  2787. }
  2788. }
  2789.