SproutCMS

This is the code documentation for the SproutCMS project

source of /sprout/Controllers/AdminController.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;
  15.  
  16. use Exception;
  17. use InvalidArgumentException;
  18. use ReflectionException;
  19. use ReflectionMethod;
  20.  
  21. use Kohana;
  22. use Kohana_404_Exception;
  23.  
  24. use Sprout\Controllers\Admin\CategoryAdminController;
  25. use Sprout\Controllers\Admin\ManagedAdminController;
  26. use Sprout\Controllers\Admin\PageAdminController;
  27. use karmabunny\pdb\Exceptions\ConstraintQueryException;
  28. use karmabunny\pdb\Exceptions\QueryException;
  29. use karmabunny\pdb\Exceptions\RowMissingException;
  30. use Sprout\Helpers\Admin;
  31. use Sprout\Helpers\AdminAuth;
  32. use Sprout\Helpers\AdminDashboard;
  33. use Sprout\Helpers\AdminError;
  34. use Sprout\Helpers\AdminPerms;
  35. use Sprout\Helpers\AdminSeo;
  36. use Sprout\Helpers\Category;
  37. use Sprout\Helpers\Constants;
  38. use Sprout\Helpers\Cron;
  39. use Sprout\Helpers\Csrf;
  40. use Sprout\Helpers\Enc;
  41. use Sprout\Helpers\FileIndexing;
  42. use Sprout\Helpers\Form;
  43. use Sprout\Helpers\Inflector;
  44. use Sprout\Helpers\Navigation;
  45. use Sprout\Helpers\Notification;
  46. use Sprout\Helpers\Pdb;
  47. use Sprout\Helpers\PerRecordPerms;
  48. use Sprout\Helpers\Register;
  49. use Sprout\Helpers\Replication;
  50. use Sprout\Helpers\Request;
  51. use Sprout\Helpers\Router;
  52. use Sprout\Helpers\Session;
  53. use Sprout\Helpers\Sprout;
  54. use Sprout\Helpers\Subsites;
  55. use Sprout\Helpers\Tags;
  56. use Sprout\Helpers\Text;
  57. use Sprout\Helpers\TwoFactor\GoogleAuthenticator;
  58. use Sprout\Helpers\Upload;
  59. use Sprout\Helpers\Url;
  60. use Sprout\Helpers\UserAgent;
  61. use Sprout\Helpers\View;
  62.  
  63.  
  64. /**
  65.  * Main class to handle admin processing.
  66.  * This delegates processing to controllers registered with {@see Register::adminControllers}
  67.  */
  68. class AdminController extends Controller
  69. {
  70.  
  71. /**
  72.   * Does some general admin loading
  73.   **/
  74. public function __construct()
  75. {
  76. parent::__construct();
  77. Session::instance();
  78.  
  79. // Check the IP whitelist
  80. if (PHP_SAPI != 'cli') {
  81. $whitelist = Kohana::config('sprout.admin_ips');
  82. if ($whitelist and count($whitelist) > 0) {
  83. if (! Sprout::ipaddressInArray(Request::userIp(), $whitelist)) {
  84. throw new Kohana_404_Exception();
  85. }
  86. }
  87. }
  88.  
  89. // If it's the wrong server, switch to the right one.
  90. $admin_url = Replication::adminUrl();
  91. if ($admin_url) {
  92. Url::redirect($admin_url);
  93. }
  94.  
  95. AdminPerms::loadAccessFlags();
  96.  
  97. // A little domain-name check for multi-site installs
  98. $domain = Kohana::config('sprout.admin_domain');
  99. if ($domain and $domain != $_SERVER['HTTP_HOST']) {
  100. Url::redirect('http://' . $domain . Kohana::config('config.site_domain') . Url::current());
  101. }
  102.  
  103. Register::docImport('csv', 'Sprout\\Helpers\\DocImport\\DocImportCSV', 'CSV');
  104. Register::docImport('txt', 'Sprout\\Helpers\\DocImport\\DocImportPlaintext', 'Plain text');
  105. Register::docImport('docx', 'Sprout\\Helpers\\DocImport\\DocImportDOCX', 'Microsoft Word 2007 and later');
  106. Register::coreContentControllers();
  107.  
  108. // Most methods require auth, but a few do not
  109. $methods_no_auth = ['login', 'loginAction', 'loginTwoFactor', 'loginTwoFactorAction', 'logout', 'userAgent'];
  110.  
  111. // Also, some initalisation doesn't work properly when not authenticated
  112. if (!in_array(Router::$method, $methods_no_auth) and PHP_SAPI !== 'cli') {
  113. AdminAuth::checkLogin();
  114.  
  115. // Load page tree
  116. Navigation::loadPageTree(@$_SESSION['admin']['active_subsite'], true);
  117.  
  118. // Execute some code for each module
  119. // This usually just loads some menu items
  120. $module_paths = Register::getModuleDirs();
  121. foreach ($module_paths as $path) {
  122. $path .= '/admin_load.php';
  123. if (file_exists($path)) include_once $path;
  124. }
  125. }
  126.  
  127. // Default config
  128. if (! Kohana::config('sprout.admin_intro')) {
  129. Kohana::configSet('sprout.admin_intro', 'admin/dashboard');
  130. }
  131. }
  132.  
  133. /**
  134.   * Home page of admin area
  135.   **/
  136. public function index()
  137. {
  138. AdminAuth::checkLogin();
  139. Url::redirect(Kohana::config('sprout.admin_intro'));
  140. }
  141.  
  142. /**
  143.   * Shows a login form
  144.   **/
  145. public function login()
  146. {
  147. if (AdminAuth::isLoggedIn()) {
  148. Url::redirect(Kohana::config('sprout.admin_intro'));
  149. }
  150.  
  151. $view = new View('sprout/admin/login_layout');
  152. $this->setDefaultMainviewParams($view);
  153.  
  154. $view->nav = null;
  155. $view->admin_authenticated = false;
  156. $view->browser_title = 'Login';
  157. $view->main_title = 'Login';
  158.  
  159. $msg = Sprout::extraPage(Constants::EXTRAPAGES_ADMIN_LOGIN);
  160. if ($msg and empty($_GET['nomsg'])) {
  161. $view->main_content = new View('sprout/admin/login_message');
  162. $view->main_content->msg = $msg;
  163.  
  164. } else {
  165. $view->main_content = new View('sprout/admin/login_form');
  166. }
  167.  
  168. if (!empty($_GET['username'])) {
  169. $view->main_content->username = trim($_GET['username']);
  170. }
  171.  
  172. echo $view->render();
  173. }
  174.  
  175. /**
  176.   * Processes a user login
  177.   **/
  178. public function loginAction()
  179. {
  180. Csrf::checkOrDie();
  181.  
  182. Session::instance();
  183. Session::regenerate();
  184.  
  185. $_POST['Username'] = trim($_POST['Username']);
  186. $_POST['Password'] = trim($_POST['Password']);
  187. $_POST['redirect'] = trim($_POST['redirect']);
  188.  
  189. if ($_POST['Username'] == '' or $_POST['Password'] == '') {
  190. Notification::error("Username or password not specified.");
  191. Url::redirect('admin/login?username=' . Enc::url($_POST['Username']) . '&redirect=' . Enc::url($_POST['redirect']) . '&nomsg=1');
  192. }
  193.  
  194. $result = AdminAuth::checkRateLimit($_POST['Username'], Request::userIp());
  195.  
  196. if ($result !== true) {
  197. list($aspect, $limit) = $result;
  198. Notification::error('Login rate limit exceeded.');
  199. Notification::error("Limit: {$aspect}, {$limit}");
  200. Url::redirect('admin/login&redirect=' . Enc::url($_POST['redirect']) . '&nomsg=1');
  201. }
  202.  
  203. $result = AdminAuth::processLogin($_POST['Username'], $_POST['Password']);
  204.  
  205. if (! $result) {
  206. $result = AdminAuth::processRemote($_POST['Username'], $_POST['Password']);
  207. }
  208.  
  209. if (! $result) {
  210. $result = AdminAuth::processLocal($_POST['Username'], $_POST['Password']);
  211. }
  212.  
  213. AdminAuth::saveLoginAttempt($_POST['Username'], Request::userIp(), $result === true ? 1 : 0);
  214.  
  215. if (! $result) {
  216. Notification::error('Incorrect username or password specified');
  217. Url::redirect('admin/login?username=' . Enc::url($_POST['Username']) . '&redirect=' . Enc::url($_POST['redirect']) . '&nomsg=1');
  218. }
  219.  
  220. // Login requires two-factor auth
  221. if (isset($_SESSION['admin']['tfa_id'])) {
  222. Url::redirect('admin/login-two-factor?redirect=' . $_POST['redirect']);
  223. }
  224.  
  225. $this->loginComplete();
  226. }
  227.  
  228.  
  229. /**
  230.   * Show the two-factor-auth ui for a half-logged-in operator
  231.   */
  232. public function loginTwoFactor()
  233. {
  234. if (!isset($_SESSION['admin']['tfa_id'])) {
  235. Url::redirect('admin/login');
  236. }
  237.  
  238. try {
  239. $q = "SELECT tfa_method FROM ~operators WHERE id = ?";
  240. $tfa_method = Pdb::query($q, [$_SESSION['admin']['tfa_id']], 'val');
  241. } catch (RowMissingException $ex) {
  242. Url::redirect('admin/login');
  243. }
  244.  
  245. switch ($tfa_method) {
  246. case 'none':
  247. $this->loginComplete();
  248. break;
  249.  
  250. case 'totp':
  251. $view = new View('sprout/tfa/totp_login');
  252. $view->action_url = 'admin/login-two-factor-action';
  253. break;
  254.  
  255. default:
  256. throw new Exception('Unknown TFA method');
  257. }
  258.  
  259. $skin = new View('sprout/admin/login_layout');
  260. $skin->browser_title = 'Login';
  261. $skin->main_title = 'Login';
  262. $skin->main_content = $view->render();
  263. echo $skin->render();
  264. }
  265.  
  266.  
  267. /**
  268.   * Process the result of a two-factor-auth for a half-logged-in operator
  269.   */
  270. public function loginTwoFactorAction()
  271. {
  272. if (!isset($_SESSION['admin']['tfa_id'])) {
  273. Url::redirect('admin/login');
  274. }
  275.  
  276. $_POST['redirect'] = trim(@$_POST['redirect']);
  277.  
  278. $q = "SELECT tfa_method, tfa_secret FROM ~operators WHERE id = ?";
  279. $operator = Pdb::query($q, [$_SESSION['admin']['tfa_id']], 'row');
  280.  
  281. switch ($operator['tfa_method']) {
  282. case 'totp':
  283. $goog = new GoogleAuthenticator();
  284. $success = $goog->checkCode($operator['tfa_secret'], $_POST['code']);
  285. break;
  286.  
  287. default:
  288. throw new Exception('Unknown TFA method');
  289. }
  290.  
  291. if (!$success) {
  292. Notification::error('Two-factor authentication failed - please try again');
  293. Url::redirect('admin/login-two-factor?redirect=' . $_POST['redirect']);
  294. }
  295.  
  296. $_SESSION['admin']['login_id'] = $_SESSION['admin']['tfa_id'];
  297. unset($_SESSION['admin']['tfa_id']);
  298. $this->loginComplete();
  299. }
  300.  
  301.  
  302. /**
  303.   * Set up various login params and redirect into admin
  304.   *
  305.   * Called after a successful login (either one-factor or two-factor)
  306.   */
  307. private function loginComplete()
  308. {
  309. if (empty($_POST['Username'])) $_POST['Username'] = '';
  310. if (empty($_POST['redirect'])) $_POST['redirect'] = '';
  311.  
  312. $subsite = Subsites::getFirstAccessable();
  313. if (! $subsite) {
  314. Notification::error('No subsites are accessible by your user account');
  315. Url::redirect('admin/login?username=' . Enc::url($_POST['Username']) . '&redirect=' . Enc::url($_POST['redirect']) . '&nomsg=1');
  316. }
  317.  
  318. // Permissions system requires users to be in a category
  319. if (!AdminAuth::isSuper()) {
  320. $cats = Category::categoryList('operators', AdminAuth::getId());
  321. if (count($cats) == 0) {
  322. Notification::error('Your user account isn\'t in any categories.');
  323. Url::redirect('admin/login?username=' . Enc::url($_POST['Username']) . '&redirect=' . Enc::url($_POST['redirect']) . '&nomsg=1');
  324. }
  325. }
  326.  
  327. $_SESSION['admin']['active_subsite'] = $subsite;
  328.  
  329. Notification::confirm('You are now logged in to the admin control panel');
  330. if (!empty($_POST['redirect']) and Url::checkRedirect($_POST['redirect'], true)) {
  331. Url::redirect($_POST['redirect']);
  332. }
  333.  
  334. Url::redirect(Kohana::config('sprout.admin_intro'));
  335. }
  336.  
  337.  
  338. /**
  339.   * Processes a user logout
  340.   **/
  341. public function logout()
  342. {
  343. try {
  344. Admin::unlock();
  345. } catch (QueryException $ex) {
  346. // Assume DB has no tables
  347. }
  348. AdminAuth::logout();
  349.  
  350. Session::instance();
  351. Session::regenerate();
  352.  
  353. Notification::confirm('You are now logged out');
  354. Url::redirect('admin/login');
  355. }
  356.  
  357.  
  358. /**
  359.   * View the various styles available in the admin area
  360.   **/
  361. public function styleGuide($section)
  362. {
  363. $section = preg_replace('![^_a-z]!', '', $section);
  364. AdminAuth::checkLogin();
  365.  
  366. $buttons = new View('sprout/admin/style_guide/index');
  367.  
  368. if ($section != 'index') {
  369. $inner_view = new View('sprout/admin/style_guide/' . $section);
  370. } else {
  371. $inner_view = '';
  372. }
  373.  
  374. $view = new View('sprout/admin/main_layout');
  375. $ctlr = $this->getController('Sprout\Controllers\Admin\PageAdminController');
  376. $this->setDefaultMainviewParams($view);
  377. $this->setNavigation($view, $ctlr);
  378. $view->controller_name = '_style_guide';
  379. $view->browser_title = 'Style guide';
  380. $view->main_title = 'SproutCMS Style Guide';
  381. $view->main_content = $buttons . $inner_view;
  382.  
  383. echo $view->render();
  384. }
  385.  
  386.  
  387. /**
  388.   * Dashboard shown when a user first logs in to the admin
  389.   */
  390. public function dashboard()
  391. {
  392. AdminAuth::checkLogin();
  393.  
  394. $first = AdminPerms::getFirstAccessable();
  395. if ($first === null) {
  396. Url::redirect('admin/intro/my_settings');
  397. } else if ($first != 'page') {
  398. Url::redirect('admin/intro/' . $first);
  399. }
  400.  
  401. $ctlr = $this->getController('Sprout\Controllers\Admin\PageAdminController');
  402. if (! $ctlr) return;
  403.  
  404. $dash_html = AdminDashboard::render();
  405.  
  406. $view = new View('sprout/admin/main_layout');
  407. $this->setDefaultMainviewParams($view);
  408. $this->setNavigation($view, $ctlr);
  409. $view->controller_name = '_dashboard';
  410. $view->browser_title = 'Dashboard';
  411. $view->main_title = 'SproutCMS Administration';
  412. $view->main_content = $dash_html;
  413. echo $view->render();
  414. }
  415.  
  416.  
  417. /**
  418.   * Closes the 'first run' box, which is shown on the admin dashboard
  419.   *
  420.   * @return void Redirects to the admin dashboard
  421.   */
  422. public function closeFirstrun()
  423. {
  424. AdminAuth::checkLogin();
  425.  
  426. Pdb::update(
  427. 'operators',
  428. ['firstrun' => 0],
  429. ['id' => AdminAuth::getId()]
  430. );
  431.  
  432. Url::redirect('admin/dashboard');
  433. }
  434.  
  435.  
  436. /**
  437.   * Shows an introduction for a specified type.
  438.   *
  439.   * @param string $type The type to show an intro for.
  440.   **/
  441. public function intro($type)
  442. {
  443. AdminAuth::checkLogin();
  444.  
  445. $ctlr = $this->getController($type);
  446. if (! $ctlr) return;
  447.  
  448. $view = new View('sprout/admin/main_layout');
  449. $this->setDefaultMainviewParams($view);
  450.  
  451. $this->setNavigation($view, $ctlr);
  452.  
  453. $main = $ctlr->_intro();
  454. if (! is_array($main)) {
  455. $main = array('title' => $ctlr->getFriendlyName(), 'content' => $main);
  456. }
  457.  
  458. $view->browser_title = $ctlr->getFriendlyName();
  459. $view->main_title = $ctlr->getFriendlyName();
  460. $view->main_title = $main['title'];
  461. $view->main_content = $main['content'];
  462. echo $view->render();
  463. }
  464.  
  465.  
  466. /**
  467.   * Shows a search form for the specified item
  468.   *
  469.   * @param string $type The type of item to show the search form for
  470.   **/
  471. public function search($type)
  472. {
  473. AdminAuth::checkLogin();
  474.  
  475. $this->unlock($type);
  476.  
  477. $view = new View('sprout/admin/main_layout');
  478. $this->setDefaultMainviewParams($view);
  479.  
  480. $ctlr = $this->getController($type);
  481. if (! $ctlr) return;
  482. if (! $this->checkAccess($ctlr, 'contents', false)) return;
  483.  
  484. $this->setNavigation($view, $ctlr);
  485.  
  486. $main = $ctlr->_getSearchForm();
  487. if ($main instanceof AdminError) {
  488. $this->error($main->getMessage(), $ctlr);
  489. return;
  490. }
  491.  
  492. if (! is_array($main)) $main = array('title' => $ctlr->getFriendlyName(), 'content' => $main);
  493.  
  494. $view->browser_title = strip_tags($main['title']);
  495. $view->main_title = $main['title'];
  496. $view->main_content = $main['content'];
  497.  
  498. echo $view->render();
  499. }
  500.  
  501.  
  502. /**
  503.   * Shows an edit form for the specified item
  504.   *
  505.   * @param string $type The type of item to show the edit form of
  506.   * @param int $id The id of the record to edit
  507.   **/
  508. public function contents($type)
  509. {
  510. AdminAuth::checkLogin();
  511.  
  512. $this->unlock($type);
  513.  
  514. $view = new View('sprout/admin/main_layout');
  515. $this->setDefaultMainviewParams($view);
  516.  
  517. $ctlr = $this->getController($type);
  518. if (! $ctlr) return;
  519. if (! $this->checkAccess($ctlr, 'contents', false)) return;
  520.  
  521. $this->setNavigation($view, $ctlr);
  522.  
  523. $main = $ctlr->_getContents();
  524. if ($main instanceof AdminError) {
  525. $this->error($main->getMessage(), $ctlr);
  526. return;
  527. }
  528.  
  529. if (! is_array($main)) $main = array('title' => $ctlr->getFriendlyName(), 'content' => $main);
  530.  
  531. $view->browser_title = strip_tags($main['title']);
  532. $view->main_title = $main['title'];
  533. $view->main_content = $main['content'];
  534.  
  535. echo $view->render();
  536. }
  537.  
  538.  
  539. /**
  540.   * Shows an edit form for the specified item
  541.   *
  542.   * @param string $type The type of item to show the edit form of
  543.   * @param int $id The id of the record to edit
  544.   **/
  545. public function export($type)
  546. {
  547. AdminAuth::checkLogin();
  548.  
  549. $view = new View('sprout/admin/main_layout');
  550. $this->setDefaultMainviewParams($view);
  551.  
  552. $ctlr = $this->getController($type);
  553. if (! $ctlr) return;
  554. if (! $this->checkAccess($ctlr, 'export', false)) return;
  555.  
  556. $this->setNavigation($view, $ctlr);
  557.  
  558. $main = $ctlr->_getExport();
  559. if ($main instanceof AdminError) {
  560. $this->error($main->getMessage(), $ctlr);
  561. return;
  562. }
  563.  
  564. if (! is_array($main)) $main = array('title' => $ctlr->getFriendlyName(), 'content' => $main);
  565.  
  566. $view->browser_title = strip_tags($main['title']);
  567. $view->main_title = $main['title'];
  568. $view->main_content = $main['content'];
  569.  
  570. echo $view->render();
  571. }
  572.  
  573. /**
  574.   * Executes the save action for a specific item
  575.   *
  576.   * @param string $type The type of item to save
  577.   * @param int $id The id of the record to save
  578.   **/
  579. public function exportAction($type)
  580. {
  581. AdminAuth::checkLogin();
  582. Csrf::checkOrDie();
  583.  
  584. $ctlr = $this->getController($type);
  585. if (! $ctlr) return;
  586. if (! $this->checkAccess($ctlr, 'export', true)) return;
  587.  
  588. $result = $ctlr->_exportData();
  589.  
  590. if ($result == false) {
  591. Notification::error('There was an error performing the export');
  592. Url::redirect("admin/export/{$type}");
  593. }
  594.  
  595. $length = strlen($result['data']);
  596. header("Content-type: {$result['type']}");
  597. header("Content-disposition: attachment; filename={$result['filename']}");
  598. header("Content-length: {$length}");
  599.  
  600. // MSIE needs "public" when under SSL - http://support.microsoft.com/kb/316431
  601. header('Pragma: public');
  602. header('Cache-Control: public, max-age=1');
  603.  
  604. echo $result['data'];
  605. }
  606.  
  607.  
  608. /**
  609.   * File upload box for importing, options are the next step
  610.   *
  611.   * @param string $type The type of item to show the import form of
  612.   **/
  613. public function importUpload($type)
  614. {
  615. AdminAuth::checkLogin();
  616.  
  617. $view = new View('sprout/admin/main_layout');
  618. $this->setDefaultMainviewParams($view);
  619.  
  620. $ctlr = $this->getController($type);
  621. if (! $ctlr) return;
  622. if (! $this->checkAccess($ctlr, 'import', false)) return;
  623.  
  624. $this->setNavigation($view, $ctlr);
  625.  
  626. if ($type == 'page') {
  627. $title = 'Document import';
  628. $main = $ctlr->_importUploadForm();
  629.  
  630. } else {
  631. $title = 'Import ' . strtolower($ctlr->getFriendlyName());
  632. $main = new View('sprout/admin/import_upload');
  633. $main->type = $type;
  634. $main->xls = FileIndexing::isExtSupported('xls');
  635. }
  636.  
  637. $view->browser_title = strip_tags($title);
  638. $view->main_title = $title;
  639. $view->main_content = $main;
  640.  
  641. echo $view->render();
  642. }
  643.  
  644. /**
  645.   * Copies the file to a temporary directory
  646.   **/
  647. public function importUploadAction($type)
  648. {
  649. AdminAuth::checkLogin();
  650. Csrf::checkOrDie();
  651.  
  652. $formats = array('csv');
  653. if (FileIndexing::isExtSupported('xls')) {
  654. $formats[] = 'xls';
  655. }
  656.  
  657. // validate upload
  658. $error = false;
  659. if (! Upload::required($_FILES['import'])) {
  660. $error = 'No file provided';
  661. } else if (! Upload::valid($_FILES['import'])) {
  662. $error = 'File upload error';
  663. } else if (! Upload::type($_FILES['import'], $formats)) {
  664. $error = 'Incorrect file type, accepted types are: ' . implode(', ', $formats);
  665. }
  666.  
  667. if (! $error) {
  668. $timestamp = time();
  669. $tempname = APPPATH . "temp/import_{$timestamp}.csv";
  670.  
  671. if (preg_match('/\.xls$/', $_FILES['import']['name'])) {
  672. // Load XLS from fileindexing tool
  673. $plaintext = FileIndexing::getPlaintext($_FILES['import']['tmp_name'], 'xls');
  674. if (! $plaintext) {
  675. $error = 'Unable to copy file to temporary directory (read)';
  676. }
  677.  
  678. $res = @file_put_contents($tempname, $plaintext);
  679. if ($res === false) {
  680. $error = 'Unable to copy file to temporary directory (write)';
  681. }
  682.  
  683. } else if (preg_match('/\.csv$/', $_FILES['import']['name'])) {
  684. // Copy the CSV directly
  685. $res = @copy($_FILES['import']['tmp_name'], $tempname);
  686. if (! $res) {
  687. $error = 'Unable to copy file to temporary directory';
  688. }
  689.  
  690. } else {
  691. $error = 'Unknown file type';
  692. }
  693. }
  694.  
  695. if ($error) {
  696. Notification::error($error);
  697. Url::redirect("admin/import_upload/{$type}");
  698. }
  699.  
  700. Url::redirect("admin/import_options/{$type}?timestamp={$timestamp}");
  701. }
  702.  
  703. /**
  704.   * Shows the import form for the specified item
  705.   *
  706.   * @param string $type The type of item to show the import form of
  707.   **/
  708. public function importOptions($type)
  709. {
  710. AdminAuth::checkLogin();
  711.  
  712. $_GET['timestamp'] = (int)@$_GET['timestamp'];
  713.  
  714. $_GET['ext'] = trim(@$_GET['ext']);
  715. if (! $_GET['ext']) $_GET['ext'] = 'csv';
  716.  
  717. $filename = APPPATH . "temp/import_{$_GET['timestamp']}.{$_GET['ext']}";
  718. if (! file_exists($filename)) {
  719. $this->error("Uploaded import file not found on server");
  720. return;
  721. }
  722.  
  723. $view = new View('sprout/admin/main_layout');
  724. $this->setDefaultMainviewParams($view);
  725.  
  726. $ctlr = $this->getController($type);
  727. if (! $ctlr) return;
  728. if (! $this->checkAccess($ctlr, 'import', false)) return;
  729.  
  730. $this->setNavigation($view, $ctlr);
  731.  
  732. $main = $ctlr->_getImport($filename);
  733. if ($main instanceof AdminError) {
  734. $this->error($main->getMessage(), $ctlr);
  735. return;
  736. }
  737.  
  738. if (! is_array($main)) $main = array('title' => $ctlr->getFriendlyName(), 'content' => $main);
  739.  
  740. $view->browser_title = strip_tags($main['title']);
  741. $view->main_title = $main['title'];
  742. $view->main_content = $main['content'];
  743.  
  744. echo $view->render();
  745. }
  746.  
  747. /**
  748.   * Executes the import action
  749.   *
  750.   * @param string $type The type of item to import
  751.   **/
  752. public function importAction($type)
  753. {
  754. AdminAuth::checkLogin();
  755. Csrf::checkOrDie();
  756.  
  757. $_POST['timestamp'] = (int) @$_POST['timestamp'];
  758.  
  759. $_POST['ext'] = trim(@$_POST['ext']);
  760. if (! $_POST['ext']) $_POST['ext'] = 'csv';
  761.  
  762. $filename = APPPATH . "temp/import_{$_POST['timestamp']}.{$_POST['ext']}";
  763. if (! file_exists($filename)) {
  764. $this->error("Uploaded import file not found on server");
  765. return;
  766. }
  767.  
  768. $ctlr = $this->getController($type);
  769. if (! $ctlr) return;
  770. if (! $this->checkAccess($ctlr, 'import', true)) return;
  771.  
  772. $result = $ctlr->_importData($filename);
  773.  
  774. if ($result == false) {
  775. Notification::error('There was an error performing the import');
  776. Url::redirect("admin/import_options/{$type}?timestamp={$_POST['timestamp']}&ext={$_POST['ext']}");
  777. }
  778.  
  779. $ctlr->_invalidateCaches('import');
  780.  
  781. @unlink($filename);
  782.  
  783. Notification::confirm('Import has been completed successfully');
  784. Url::redirect("admin/contents/{$type}");
  785. }
  786.  
  787.  
  788. /**
  789.   * Shows an error message in the admin skin
  790.   *
  791.   * @param string $message The message to show. Should be plain-text.
  792.   * @param ManagedAdminController $ctlr A controller to show the navigation of.
  793.   **/
  794. private function error($message, ManagedAdminController $ctlr = null)
  795. {
  796. AdminAuth::checkLogin();
  797.  
  798. $content = new View('sprout/admin/error');
  799. $content->message = $message;
  800.  
  801. $view = new View('sprout/admin/main_layout');
  802. $this->setDefaultMainviewParams($view);
  803.  
  804. if ($ctlr) {
  805. $this->setNavigation($view, $ctlr);
  806. }
  807.  
  808. $view->browser_title = 'Error';
  809. $view->main_title = 'Error';
  810. $view->main_content = $content;
  811.  
  812. echo $view->render();
  813. }
  814.  
  815.  
  816. /**
  817.   * Check access for a given controller/access flag combo
  818.   * If this returns false, it will echo out an error message; the calling code should just return.
  819.  
  820.   * @param ManagedAdminController $ctlr A controller to check
  821.   * @param string $access_flag The access flag to check, e.g. 'add', 'edit', etc
  822.   * @param bool $action True if it's an action method, false if it's a form method.
  823.   * @return True if auth is okay, false if it is not.
  824.   **/
  825. private function checkAccess(ManagedAdminController $ctlr, $access_flag, $action)
  826. {
  827. AdminAuth::checkLogin();
  828.  
  829. if ($ctlr instanceof CategoryAdminController) {
  830. $ctlr = $ctlr->getParentInst();
  831. $access_flag = 'categories';
  832. }
  833.  
  834. if (AdminPerms::controllerAccess($ctlr->getControllerName(), $access_flag)) {
  835. return true;
  836. }
  837.  
  838. if ($action) {
  839. Notification::error('Access Denied; Section: ' . $ctlr->getFriendlyName() . '; Action: ' . $access_flag);
  840. Url::redirect('admin');
  841.  
  842. } else {
  843. $content = new View('sprout/admin/access_denied');
  844. $content->friendly_name = $ctlr->getFriendlyName();
  845. $content->access_flag = $access_flag;
  846.  
  847. $view = new View('sprout/admin/main_layout');
  848. $this->setDefaultMainviewParams($view);
  849.  
  850. if ($ctlr) {
  851. $this->setNavigation($view, $ctlr);
  852. }
  853.  
  854. $view->browser_title = 'Access denied';
  855. $view->main_title = 'Access denied';
  856. $view->main_content = $content;
  857. echo $view->render();
  858. }
  859.  
  860. return false;
  861. }
  862.  
  863.  
  864. /**
  865.   * Checks that an admin user has access to an individual record
  866.   *
  867.   * @param ManagedAdminController $ctlr The controller which manages the table containing the record
  868.   * @param int $item_id The id of the row to be edited/deleted
  869.   * @return bool True if access allowed
  870.   */
  871. function checkRecordAccess(ManagedAdminController $ctlr, $item_id)
  872. {
  873. $restrict = PerRecordPerms::controllerRestricted($ctlr);
  874. if (!$restrict) return true;
  875.  
  876. $params = [];
  877. $cat_clause = PerRecordPerms::getCategoryClause();
  878.  
  879. $params[] = $ctlr->getControllerName();
  880. $params[] = $item_id;
  881.  
  882. $q = "SELECT {$cat_clause}
  883. FROM ~per_record_permissions
  884. WHERE controller = ? AND item_id = ?";
  885. $res = Pdb::q($q, $params, 'col');
  886.  
  887. if (count($res) == 0) return true;
  888.  
  889. return (bool) Sprout::iterableFirstValue($res);
  890. }
  891.  
  892.  
  893. /**
  894.   * Ensure 'active' flag and 'tags' POST fields have values set, if they are nonexistant
  895.   *
  896.   * @param ManagedAdminController $ctlr
  897.   */
  898. private function cleanupCommonPostData(ManagedAdminController $ctlr)
  899. {
  900. if (!isset($_POST['tags'])) {
  901. $_POST['tags'] = '';
  902. }
  903.  
  904. $visibility = $ctlr->_getVisibilityFields();
  905. foreach ($visibility as $name => $label) {
  906. if (empty($_POST[$name])) {
  907. $_POST[$name] = 0;
  908. } elseif ($_POST[$name] != '0' and $_POST[$name] != '1') {
  909. $_POST[$name] = 0;
  910. }
  911. }
  912.  
  913. // Ensure that the session always has data, so that the initial lookup is treated
  914. // differently from a form submission with no categories selected
  915. if (!isset($_POST['_prm_categories'])) {
  916. $_POST['_prm_categories'] = [];
  917. }
  918. }
  919.  
  920.  
  921. /**
  922.   * Render a list of sub-actions into HTML as links
  923.   *
  924.   * Each sub-action should be an array, with the following keys:
  925.   * url Link URL
  926.   * name Link text
  927.   * class Optional class
  928.   * new_tab Optional bool to show in new window/tab
  929.   *
  930.   * Use a special entry with a key of "_preview" and a value of the
  931.   * preview URL to set up a preview button
  932.   *
  933.   * @param array $list Sub-actions to render
  934.   * @return HTML
  935.   */
  936. private function renderSubActions(array $list)
  937. {
  938. $out = '<ul class="list-style-1">';
  939.  
  940. foreach ($list as $key => $item) {
  941. if ($key === '_preview') continue;
  942.  
  943. $class = 'sub-action';
  944. if (isset($item['class'])) $class .= ' ' . $item['class'];
  945.  
  946. $out .= '<li>';
  947. $out .= '<a href="' . Enc::html($item['url']) . '" class="' . Enc::html($class) . '"';
  948. if (isset($item['new_tab']) and $item['new_tab'] === true) {
  949. $out .= ' target="_blank"';
  950. }
  951. $out .= '>';
  952. $out .= Enc::html($item['name']);
  953. $out .= '</a>';
  954. $out .= '</li>';
  955. }
  956.  
  957. $out .= '</ul>';
  958.  
  959. return $out;
  960. }
  961.  
  962.  
  963. /**
  964.   * Generates HTML for fields relating to per-record permissions in the 'save changes' box
  965.   *
  966.   * @param string ManagedAdminController $ctlr The controller to check permissions for
  967.   * @param int $item_id The ID of the record being edited (0 when adding a new record)
  968.   * @return string HTML
  969.   */
  970. protected function perRecordPermissionsFields(ManagedAdminController $ctlr, $item_id)
  971. {
  972. if (!PerRecordPerms::controllerRestricted($ctlr)) {
  973. return '';
  974. }
  975.  
  976. // Preload operator categories for per-user permissions
  977. if ($item_id > 0) {
  978. $q = "SELECT operator_categories
  979. FROM ~per_record_permissions
  980. WHERE controller = ? AND item_id = ?";
  981. $access = Pdb::q($q, [$ctlr->getControllerName(), $item_id], 'arr');
  982. if (count($access) > 0) {
  983. $access = Sprout::iterableFirstValue($access);
  984.  
  985. if (Form::getData('_prm_categories') === null) {
  986. $cat_ids = array_filter(explode(',', trim($access['operator_categories'], ',')));
  987. Form::setFieldValue('_prm_categories', $cat_ids);
  988. }
  989. }
  990. }
  991.  
  992. $out = '';
  993. if (AdminPerms::canAccess('access_operators')) {
  994. $cat_list = AdminAuth::getAllCategories();
  995. } else {
  996. $cat_list = [];
  997. $cat_ids = AdminAuth::getOperatorCategories();
  998.  
  999. if (count($cat_ids) > 0) {
  1000. $params = [];
  1001. $conds = [
  1002. ['id', 'IN', $cat_ids],
  1003. ];
  1004. $where = Pdb::buildClause($conds, $params);
  1005.  
  1006. $q = "SELECT id, name
  1007. FROM ~operators_cat_list
  1008. WHERE {$where}
  1009. ORDER BY name";
  1010. $cat_list = Pdb::q($q, $params, 'map');
  1011. }
  1012. }
  1013.  
  1014. // Don't display primary administrators category; they always get access
  1015. $primary_cat_id = AdminAuth::getPrimaryCategoryId();
  1016. unset($cat_list[$primary_cat_id]);
  1017.  
  1018. $checked_cats = Form::getData('_prm_categories');
  1019.  
  1020. // Pre-tick all categories if on add form
  1021. // N.B. primary admins don't have any categories ticked because they belong to ALL categories,
  1022. // and it defeats the purpose of per-record controls if everyone has access by default.
  1023. if ($item_id == 0 and count($checked_cats) == 0) {
  1024. if (!AdminAuth::inCategory($primary_cat_id)) {
  1025. $checked_cats = array_keys($cat_list);
  1026. Form::setFieldValue('_prm_categories', $checked_cats);
  1027. }
  1028. }
  1029.  
  1030. $allow_cats = '';
  1031. if ($item_id == 0 or AdminAuth::inCategory($primary_cat_id)) {
  1032. Form::nextFieldDetails('Allow changes by', false);
  1033. $allow_cats = Form::checkboxSet('_prm_categories', [], $cat_list);
  1034.  
  1035. // Hack in 'all operators' option for primary admins
  1036. if (AdminAuth::inCategory($primary_cat_id)) {
  1037. $all = '<div class="field-element__input-set">';
  1038. $all .= '<div class="fieldset-input"><input type="checkbox" value="1" name="_prm_all_cats" id="_prm_all"';
  1039. if ($checked_cats == ['*'] or ($item_id == 0 and Form::getData('_prm_all_cats'))) {
  1040. $all .= ' checked';
  1041. }
  1042. $all .= '><label for="_prm_all">All operators</label></div>';
  1043. $allow_cats = str_replace('<div class="field-element__input-set">', $all, $allow_cats);
  1044. }
  1045. }
  1046.  
  1047. $out .= $allow_cats;
  1048.  
  1049. return $out;
  1050. }
  1051.  
  1052.  
  1053. /**
  1054.   * Shows an add form for the specified item
  1055.   *
  1056.   * @param string $type The type of item to show the add form of
  1057.   **/
  1058. public function add($type)
  1059. {
  1060. AdminAuth::checkLogin();
  1061.  
  1062. $ctlr = $this->getController($type);
  1063. if (! $ctlr) return;
  1064. if (! $this->checkAccess($ctlr, 'add', false)) return;
  1065.  
  1066. $view = new View('sprout/admin/main_layout');
  1067. $this->setDefaultMainviewParams($view);
  1068.  
  1069. $this->setNavigation($view, $ctlr);
  1070.  
  1071. $main = $ctlr->_getAddForm();
  1072. if ($main instanceof AdminError) {
  1073. $this->error($main->getMessage(), $ctlr);
  1074. return;
  1075. }
  1076. if (!is_array($main)) {
  1077. throw new InvalidArgumentException('Return value from _getAddForm must be an array');
  1078. }
  1079.  
  1080. if ($ctlr->_isAddSaved() and Text::containsFormTag($main['content'])) {
  1081. throw new Exception("Add view must not include the form tag");
  1082. }
  1083.  
  1084. if (Request::isAjax()) {
  1085. $class = 'admin-ajax action-add type-' . Enc::id($type);
  1086. echo '<h2 class="popup-title">', $main['title'], '</h2>';
  1087. echo '<div class="', $class, '">';
  1088. echo '<form action="admin/add_save/' . Enc::html($ctlr->getControllerName()) . '" method="post">';
  1089. echo Csrf::token();
  1090. echo $main['content'];
  1091. echo '<div class="action-bar"><button type="submit" class="button button-regular button-green icon-after icon-save">Save changes</button></div>';
  1092. echo '</form>';
  1093. echo '</div>';
  1094. return;
  1095. }
  1096.  
  1097. // Create tags area, and inject it into content after the <form> tag
  1098. $tags = new View('sprout/admin/main_tags');
  1099. $tags->type = $type;
  1100. $tags->suggestions = Tags::suggestTags($ctlr->getTableName());
  1101. $tags->table = $ctlr->getTableName();
  1102.  
  1103. $tags->current_tags = @$_SESSION['admin']['tags'];
  1104. unset ($_SESSION['admin']['tags']);
  1105.  
  1106. if ($ctlr->_isAddSaved()) {
  1107. $single = Inflector::singular($ctlr->getFriendlyName());
  1108. $content = '<form action="admin/add_save/' . Enc::html($ctlr->getControllerName()) . '" method="post" id="edit-form" class="-clearfix">';
  1109.  
  1110. $content .= Csrf::token();
  1111. $content .= '<div class="mainbar-with-right-sidebar">';
  1112. $content .= $tags->render();
  1113. $content .= $main['content'];
  1114. $content .= '</div>';
  1115.  
  1116. $content .= '<div class="right-sidebar">';
  1117. $content .= '<div class="right-sidebar-inner">';
  1118. $content .= '<div class="save-changes-box">';
  1119.  
  1120. $html = $ctlr->_getCustomAddSaveHTML();
  1121. if ($html) {
  1122. $content .= $html;
  1123. } else {
  1124. $visibility = $ctlr->_getVisibilityFields();
  1125. $sub_actions = $ctlr->_getAddSubActions();
  1126.  
  1127. $content .= '<h2 class="icon-before icon-add">Add ' . Enc::html($single) . '</h2>';
  1128. if (!empty($visibility)) {
  1129. Form::nextFieldDetails('Visibility', false);
  1130. $content .= Form::checkboxBoolList(null, [], $visibility);
  1131. }
  1132.  
  1133. $content .= $this->perRecordPermissionsFields($ctlr, 0);
  1134.  
  1135. if ($ctlr->isPerSubsite()) {
  1136. $subsites = Pdb::lookup('subsites');
  1137. Form::nextFieldDetails('Subsite', false);
  1138. Form::setFieldValue('subsite_id', $_SESSION['admin']['active_subsite']);
  1139. $content .= Form::dropdown('subsite_id', ['-dropdown-top' => 'Show on all sites'], $subsites);
  1140. }
  1141.  
  1142. $content .= $this->renderSubActions($sub_actions);
  1143. $content .= '<div class="save-changes-box-bottom -clearfix">';
  1144. if (!empty($sub_actions['_preview'])) {
  1145. $content .= '<a href="' . Enc::html($sub_actions['_preview']) . '" class="save-changes-preview-button button button-regular button-blue icon-after icon-remove_red_eye">Preview</a>';
  1146. }
  1147. $content .= '<button type="submit" class="save-changes-save-button button button-regular button-green icon-after icon-add">Save changes</button>';
  1148. $content .= '</div>';
  1149. }
  1150.  
  1151. $content .= '</div>';
  1152. $content .= '</div>';
  1153. $content .= '</div>';
  1154.  
  1155. $content .= '</form>';
  1156. } else {
  1157. $content = $main['content'];
  1158. }
  1159.  
  1160. $view->browser_title = strip_tags($main['title']);
  1161. $view->main_title = $main['title'];
  1162. $view->main_content = $content;
  1163. $view->has_tags = true;
  1164. $view->main_class = 'do-action-box';
  1165.  
  1166. echo $view->render();
  1167. }
  1168.  
  1169. /**
  1170.   * Executes the save action for a specific item
  1171.   *
  1172.   * @param string $type The type of item to add
  1173.   **/
  1174. public function addSave($type)
  1175. {
  1176. AdminAuth::checkLogin();
  1177. Csrf::checkOrDie();
  1178.  
  1179. $ctlr = $this->getController($type);
  1180. if (! $ctlr) return;
  1181. if (! $this->checkAccess($ctlr, 'add', true)) return;
  1182.  
  1183. $this->cleanupCommonPostData($ctlr);
  1184.  
  1185. $_SESSION['admin']['tags'] = $_POST['tags'];
  1186.  
  1187. $id = 0;
  1188. $result = $ctlr->_addSave($id);
  1189.  
  1190. // Set per-record permissions
  1191. if ($result) {
  1192. PerRecordPerms::save($ctlr, $id);
  1193. }
  1194.  
  1195. if (Request::isAjax()) {
  1196. $result = (int) $result;
  1197. echo json_encode(array('result' => $result));
  1198. }
  1199.  
  1200. if ($result == false) {
  1201. Notification::error('There was an error saving your changes');
  1202. if (!empty($_POST['current_url'])) {
  1203. Url::redirect($_POST['current_url']);
  1204. }
  1205. Url::redirect("admin/add/{$type}");
  1206. }
  1207.  
  1208. $new_tags = Tags::splitupTags($_POST['tags']);
  1209. $tag_result = Tags::update($ctlr->getTableName(), $id, $new_tags);
  1210. unset ($_SESSION['admin']['tags']);
  1211.  
  1212. if ($tag_result == false) {
  1213. Notification::error('There was an error updating the tags for this item');
  1214. }
  1215.  
  1216. $ctlr->_invalidateCaches('add', $id);
  1217.  
  1218. unset ($_SESSION['admin']['field_values']);
  1219.  
  1220. $single = strtolower(Inflector::singular($ctlr->getFriendlyName()));
  1221. $message = "Your {$single} has been added";
  1222.  
  1223. if (!Notification::has(Notification::TYPE_CONFIRM)) {
  1224. Notification::confirm($message, []);
  1225. }
  1226.  
  1227. if (is_string($result)) {
  1228. Url::redirect($result);
  1229. } else {
  1230. Url::redirect("admin/edit/{$type}/{$id}");
  1231. }
  1232. }
  1233.  
  1234. /**
  1235.   * Shows an edit form for the specified item
  1236.   *
  1237.   * @param string $type The type of item to show the edit form of
  1238.   * @param int $id The id of the record to edit
  1239.   **/
  1240. public function edit($type, $id)
  1241. {
  1242. AdminAuth::checkLogin();
  1243. $id = (int) $id;
  1244.  
  1245. $view = new View('sprout/admin/main_layout');
  1246. $this->setDefaultMainviewParams($view);
  1247. $view->has_tags = true;
  1248.  
  1249. $ctlr = $this->getController($type);
  1250. if (! $ctlr) return;
  1251. if (! $this->checkAccess($ctlr, 'edit', false)) return;
  1252. if (! $this->checkRecordAccess($ctlr, $id)) return;
  1253.  
  1254. $this->setNavigation($view, $ctlr);
  1255.  
  1256. $main = $ctlr->_getEditForm($id);
  1257. if ($main instanceof AdminError) {
  1258. $this->error($main->getMessage(), $ctlr);
  1259. return;
  1260. }
  1261. if (!is_array($main)) {
  1262. throw new InvalidArgumentException('Return value from _getEditForm must be an array');
  1263. }
  1264.  
  1265. // Disallow view if it contains a <FORM> tag or output will contain nested-forms and that doesn't work
  1266. if ($ctlr->_isEditSaved($id) and Text::containsFormTag($main['content'])) {
  1267. throw new Exception("Edit view must not include the form tag");
  1268. }
  1269.  
  1270. // Create tags area, and inject it into content after the <form> tag
  1271. $tags = new View('sprout/admin/main_tags');
  1272. $tags->suggestions = Tags::suggestTags($ctlr->getTableName());
  1273. $tags->table = $ctlr->getTableName();
  1274.  
  1275. $tags->current_tags = @$_SESSION['admin']['tags'];
  1276. if (empty($_SESSION['admin']['tags'])) {
  1277. $tags->current_tags = implode(', ', Tags::byRecord($ctlr->getTableName(), $id));
  1278. }
  1279. unset ($_SESSION['admin']['tags']);
  1280.  
  1281. // Check for SEO enabled content
  1282. $view->enable_seo = !empty(AdminSeo::$content)? true : false;
  1283.  
  1284. if ($ctlr->_isEditSaved($id)) {
  1285. $content = '<form action="admin/edit_save/' . Enc::html($ctlr->getControllerName()) . '/' . $id;
  1286. $content .= '" method="post" id="edit-form" class="-clearfix" enctype="multipart/form-data">';
  1287. $content .= Csrf::token();
  1288. $content .= '<div class="mainbar-with-right-sidebar">';
  1289. $content .= $tags->render();
  1290. $content .= AdminSeo::getAnalysis();
  1291. $content .= $main['content'];
  1292. $content .= '</div>';
  1293.  
  1294. $content .= '<div class="right-sidebar">';
  1295. $content .= '<div class="right-sidebar-inner">';
  1296. $content .= '<div class="save-changes-box">';
  1297.  
  1298. $html = $ctlr->_getCustomEditSaveHTML($id);
  1299. if ($html) {
  1300. $content .= $html;
  1301. } else {
  1302. $visibility = $ctlr->_getVisibilityFields();
  1303. $sub_actions = $ctlr->_getEditSubActions($id);
  1304.  
  1305. $content .= '<h2 class="icon-before icon-save">Save changes</h2>';
  1306. if (!empty($visibility)) {
  1307. Form::nextFieldDetails('Visibility', false);
  1308. $content .= Form::checkboxBoolList(null, [], $visibility);
  1309. }
  1310.  
  1311. $content .= $this->perRecordPermissionsFields($ctlr, $id);
  1312.  
  1313. if ($ctlr->isPerSubsite()) {
  1314. $subsites = Pdb::lookup('subsites');
  1315. Form::nextFieldDetails('Subsite', false);
  1316. $content .= Form::dropdown('subsite_id', ['-dropdown-top' => 'Show on all sites'], $subsites);
  1317. }
  1318. $content .= $this->renderSubActions($sub_actions);
  1319. $content .= '<div class="save-changes-box-bottom -clearfix">';
  1320. if (!empty($sub_actions['_preview'])) {
  1321. $content .= '<a href="' . Enc::html($sub_actions['_preview']) . '" class="save-changes-preview-button button button-regular button-blue icon-after icon-remove_red_eye">Preview</a>';
  1322. }
  1323. $content .= '<button type="submit" class="save-changes-save-button button button-regular button-green icon-after icon-save">Save changes</button>';
  1324. $content .= '</div>';
  1325. }
  1326.  
  1327. $content .= '</div>';
  1328. $content .= '</div>';
  1329. $content .= '</div>';
  1330.  
  1331. $content .= '</form>';
  1332. } else {
  1333. $content = $main['content'];
  1334. }
  1335.  
  1336. $this->lock($type, $id, $view);
  1337.  
  1338. // Render the main view
  1339. $view->browser_title = Text::limitChars(strip_tags($main['title']), 50, '...');
  1340. $view->main_title = $main['title'];
  1341. $view->main_content = $content;
  1342. $view->main_class = 'do-action-box';
  1343.  
  1344. $url = $ctlr->_getEditLiveUrl($id);
  1345. if ($url) {
  1346. $view->live_url = Admin::ensureUrlAbsolute($url);
  1347. }
  1348.  
  1349. echo $view->render();
  1350. }
  1351.  
  1352. /**
  1353.   * Executes the save action for a specific item
  1354.   *
  1355.   * @param string $type The type of item to save
  1356.   * @param int $id The id of the record to save
  1357.   **/
  1358. public function editSave($type, $id)
  1359. {
  1360. AdminAuth::checkLogin();
  1361. Csrf::checkOrDie();
  1362.  
  1363. $id = (int) $id;
  1364.  
  1365. $ctlr = $this->getController($type);
  1366. if (! $ctlr) return;
  1367. if (! $this->checkAccess($ctlr, 'edit', true)) return;
  1368. if (! $this->checkRecordAccess($ctlr, $id)) return;
  1369.  
  1370. $this->unlock($type, $id);
  1371. $this->cleanupCommonPostData($ctlr);
  1372.  
  1373. $_SESSION['admin']['tags'] = $_POST['tags'];
  1374.  
  1375. $result = $ctlr->_editSave($id);
  1376.  
  1377. if (Request::isAjax()) {
  1378. $result = (int) $result;
  1379. echo json_encode(array('result' => $result));
  1380. return;
  1381. }
  1382.  
  1383. if ($result == false) {
  1384. Notification::error('There was an error saving your changes');
  1385. Url::redirect("admin/edit/{$type}/{$id}");
  1386. }
  1387.  
  1388. // Update per-record permissions
  1389. if ($result and AdminPerms::canAccess('access_operators')) {
  1390. PerRecordPerms::save($ctlr, $id);
  1391. }
  1392.  
  1393. $new_tags = Tags::splitupTags($_POST['tags']);
  1394. $tag_result = Tags::update($ctlr->getTableName(), $id, $new_tags);
  1395. unset ($_SESSION['admin']['tags']);
  1396.  
  1397. if ($tag_result == false) {
  1398. Notification::error('There was an error updating the tags for this item');
  1399. }
  1400.  
  1401. $ctlr->_invalidateCaches('edit', $id);
  1402.  
  1403. unset ($_SESSION['admin']['field_values']);
  1404. if (!Notification::has(Notification::TYPE_CONFIRM)) {
  1405. Notification::confirm('Your changes have been saved');
  1406. }
  1407.  
  1408. if (is_string($result)) {
  1409. Url::redirect($result);
  1410. } else {
  1411. Url::redirect("admin/edit/{$type}/{$id}");
  1412. }
  1413. }
  1414.  
  1415.  
  1416. /**
  1417.   * Shows a delete form for the specified item
  1418.   * @param string $type Shorthand controller name; see {@see Register::adminControllers}
  1419.   * @param int $id The id of the record to show
  1420.   */
  1421. public function delete($type, $id)
  1422. {
  1423. AdminAuth::checkLogin();
  1424.  
  1425. $ctlr = $this->getController($type);
  1426. if (!$ctlr) return;
  1427. if (!$this->checkAccess($ctlr, 'delete', false)) return;
  1428. if (!$this->checkRecordAccess($ctlr, $id)) return;
  1429. if (!$ctlr->_isDeleteSaved($id)) return;
  1430.  
  1431. $main = $ctlr->_getDeleteForm($id);
  1432. if ($main instanceof AdminError) {
  1433. $this->error($main->getMessage(), $ctlr);
  1434. return;
  1435. }
  1436.  
  1437. if (!is_array($main)) {
  1438. throw new InvalidArgumentException('Return value from _getDeleteForm must be an array');
  1439. }
  1440.  
  1441. if ($ctlr->_isDeleteSaved($id) and Text::containsFormTag($main['content'])) {
  1442. throw new Exception("Delete view must not include the form tag");
  1443. }
  1444.  
  1445. $view = new View('sprout/admin/main_layout');
  1446. $this->setDefaultMainviewParams($view);
  1447. $this->setNavigation($view, $ctlr);
  1448.  
  1449. $single = Inflector::singular($ctlr->getFriendlyName());
  1450. $content = '<form action="admin/delete_save/' . Enc::html($ctlr->getControllerName()) . '/' . $id . '" method="post" id="edit-form">';
  1451.  
  1452. $content .= Csrf::token();
  1453.  
  1454. $content .= '<div class="mainbar-with-right-sidebar">';
  1455. $content .= $main['content'];
  1456. $content .= '</div>';
  1457.  
  1458. $content .= '<div class="right-sidebar">';
  1459. $content .= '<div class="right-sidebar-inner">';
  1460. $content .= '<div class="save-changes-box">';
  1461.  
  1462. $content .= '<h2 class="icon-before icon-delete">Delete ' . Enc::html($single) . '</h2>';
  1463. $content .= $this->renderSubActions($ctlr->_getDeleteSubActions($id));
  1464. $content .= '<div class="save-changes-box-bottom -clearfix">';
  1465. $content .= '<button type="submit" class="save-changes-save-button button button-regular button-red button-ref icon-after icon-delete">Delete ' . Enc::html($single) . '</button>';
  1466. $content .= '</div>';
  1467.  
  1468. $content .= '</div>';
  1469. $content .= '</div>';
  1470. $content .= '</div>';
  1471.  
  1472. $content .= '</form>';
  1473.  
  1474. $view->browser_title = Text::limitChars(strip_tags($main['title']), 50, '...');
  1475. $view->main_title = $main['title'];
  1476. $view->main_content = $content;
  1477. $view->main_class = 'delete';
  1478.  
  1479. echo $view->render();
  1480. }
  1481.  
  1482. /**
  1483.   * Executes the delete action for a specific item
  1484.   *
  1485.   * @param string $type The Type of the item to delete
  1486.   * @param int $id The id of the record to delete
  1487.   **/
  1488. public function deleteSave($type, $id)
  1489. {
  1490. AdminAuth::checkLogin();
  1491. Csrf::checkOrDie();
  1492.  
  1493. $ctlr = $this->getController($type);
  1494. if (! $ctlr) return;
  1495. if (! $this->checkAccess($ctlr, 'delete', true)) return;
  1496. if (! $this->checkRecordAccess($ctlr, $id)) return;
  1497.  
  1498. try {
  1499. $ctlr->_deletePreSave($id);
  1500. } catch (Exception $ex) {
  1501. Notification::error($ex->getMessage());
  1502. Url::redirect("admin/delete/{$type}/{$id}");
  1503. }
  1504.  
  1505. $result = false;
  1506. try {
  1507. $result = $ctlr->_deleteSave($id);
  1508. } catch (ConstraintQueryException $ex) {
  1509. $item_name = Inflector::singular($ctlr->getFriendlyName());
  1510. Notification::error("This {$item_name} is in use and can't be deleted");
  1511. Url::redirect("admin/edit/{$type}/{$id}");
  1512. }
  1513. if ($result) $ctlr->_deletePostSave($id);
  1514.  
  1515. $tag_result = Tags::update($ctlr->getTableName(), $id, array());
  1516. if (! $tag_result) $result = false;
  1517.  
  1518. if (Request::isAjax()) {
  1519. $result = (int) $result;
  1520. echo json_encode(array('result' => $result));
  1521. }
  1522.  
  1523. if ($result == false) {
  1524. Notification::error('There was a database error deleting the specified item');
  1525. Url::redirect("admin/delete/{$type}/{$id}");
  1526. }
  1527.  
  1528. $ctlr->_invalidateCaches('delete', $id);
  1529.  
  1530. Notification::confirm('Deletion was successful');
  1531.  
  1532. if (is_string($result)) {
  1533. Url::redirect($result);
  1534. } else {
  1535. Url::redirect("admin/contents/{$type}");
  1536. }
  1537. }
  1538.  
  1539.  
  1540. /**
  1541.   * Shows a duplication form for the specified item
  1542.   * This uses the edit form with some string replacements
  1543.   *
  1544.   * @param string $type The type of item to show the duplication form of
  1545.   * @param int $id The id of the record to duplicate
  1546.   **/
  1547. public function duplicate($type, $id)
  1548. {
  1549. AdminAuth::checkLogin();
  1550. $id = (int) $id;
  1551.  
  1552. $view = new View('sprout/admin/main_layout');
  1553. $this->setDefaultMainviewParams($view);
  1554. $view->has_tags = true;
  1555.  
  1556. $ctlr = $this->getController($type);
  1557. if (! $ctlr) return;
  1558. if (! $this->checkAccess($ctlr, 'edit', false)) return;
  1559. if (! $this->checkRecordAccess($ctlr, $id)) return;
  1560.  
  1561. if (! $ctlr->getDuplicateEnabled()) {
  1562. $this->error("Duplication is not enabled for this controller");
  1563. return;
  1564. }
  1565.  
  1566. $this->setNavigation($view, $ctlr);
  1567.  
  1568. $main = $ctlr->_getDuplicateForm($id);
  1569. if ($main instanceof AdminError) {
  1570. $this->error($main->getMessage(), $ctlr);
  1571. return;
  1572. }
  1573.  
  1574. if (! is_array($main)) $main = array('title' => $ctlr->getFriendlyName(), 'content' => $main);
  1575.  
  1576. if (Text::containsFormTag($main['content'])) {
  1577. throw new Exception("Duplicate view must not include the form tag");
  1578. }
  1579.  
  1580. // Create tags area, and inject it into content after the <form> tag
  1581. $tags = new View('sprout/admin/main_tags');
  1582. $tags->suggestions = Tags::suggestTags($ctlr->getTableName());
  1583. $tags->table = $ctlr->getTableName();
  1584.  
  1585. // Inject tags UI
  1586. $tags->current_tags = @$_SESSION['admin']['tags'];
  1587. if (empty($_SESSION['admin']['tags'])) {
  1588. $tags->current_tags = implode(', ', Tags::byRecord($ctlr->getTableName(), $id));
  1589. }
  1590. unset ($_SESSION['admin']['tags']);
  1591.  
  1592. // Rejig the edit form to be about duplication instead of editing
  1593. $name_find = array ('edit_save', 'editing', 'Editing', 'Save changes');
  1594. $name_replace = array ('duplicate_save', 'duplicating', 'Duplicating', 'Duplicate');
  1595. $main['content'] = str_replace($name_find, $name_replace, $main['content']);
  1596. $main['title'] = str_replace($name_find, $name_replace, $main['title']);
  1597.  
  1598. if ($ctlr->_isEditSaved($id)) {
  1599. $single = Inflector::singular($ctlr->getFriendlyName());
  1600. $content = '<form action="admin/duplicate_save/' . Enc::html($ctlr->getControllerName()) . '/' . $id . '" method="post" id="edit-form">';
  1601.  
  1602. $content .= Csrf::token();
  1603. $content .= '<div class="mainbar-with-right-sidebar">';
  1604. $content .= $tags->render();
  1605. $content .= $main['content'];
  1606. $content .= '</div>';
  1607.  
  1608. $content .= '<div class="right-sidebar">';
  1609. $content .= '<div class="right-sidebar-inner">';
  1610. $content .= '<div class="save-changes-box">';
  1611.  
  1612. $html = $ctlr->_getCustomDuplicateSaveHTML($id);
  1613. if ($html) {
  1614. $content .= $html;
  1615. } else {
  1616. $visibility = $ctlr->_getVisibilityFields();
  1617. $sub_actions = $ctlr->_getDuplicateSubActions($id);
  1618.  
  1619. $content .= '<h2 class="icon-before icon-save">Duplicate ' . Enc::html($single) . '</h2>';
  1620. if (!empty($visibility)) {
  1621. Form::nextFieldDetails('Visibility', false);
  1622. $content .= Form::checkboxBoolList(null, [], $visibility);
  1623. }
  1624. $content .= '<div class="save-changes-box-bottom -clearfix">';
  1625. if (!empty($sub_actions['_preview'])) {
  1626. $content .= '<a href="' . Enc::html($sub_actions['_preview']) . '" class="save-changes-preview-button button button-regular button-blue icon-after icon-remove_red_eye">Preview</a>';
  1627. }
  1628. $content .= '<button type="submit" class="save-changes-save-button button button-regular button-green icon-after icon-save">Save changes</button>';
  1629. $content .= '</div>';
  1630. }
  1631.  
  1632. $content .= '</div>';
  1633. $content .= '</div>';
  1634. $content .= '</div>';
  1635.  
  1636. $content .= '</form>';
  1637. } else {
  1638. $content = $main['content'];
  1639. }
  1640.  
  1641.  
  1642. $this->lock($type, $id, $view);
  1643.  
  1644. // Render the main view
  1645. $view->browser_title = Text::limitChars(strip_tags($main['title']), 50, '...');
  1646. $view->main_title = $main['title'];
  1647. $view->main_content = $content;
  1648. $view->main_class = 'do-action-box';
  1649.  
  1650. echo $view->render();
  1651. }
  1652.  
  1653. /**
  1654.   * Executes the save action for a record duplication
  1655.   *
  1656.   * @param string $type The type of item to save
  1657.   * @param int $id The id of the record to save
  1658.   **/
  1659. public function duplicateSave($type, $orig_id)
  1660. {
  1661. AdminAuth::checkLogin();
  1662. Csrf::checkOrDie();
  1663.  
  1664. $orig_id = (int) $orig_id;
  1665.  
  1666. $ctlr = $this->getController($type);
  1667. if (! $ctlr) return;
  1668. if (! $this->checkAccess($ctlr, 'edit', true)) return;
  1669. if (! $this->checkRecordAccess($ctlr, $orig_id)) return;
  1670.  
  1671. $this->cleanupCommonPostData($ctlr);
  1672.  
  1673. $_SESSION['admin']['tags'] = $_POST['tags'];
  1674.  
  1675. // Start transaction
  1676. Pdb::transact();
  1677.  
  1678. // Create new id
  1679. // Nasty hack to prevent errors with null values in fields with foreign key constraints
  1680. // This shouldn't be an issue as the actual data from the POST submission should comply with the constraints
  1681. Pdb::q('SET foreign_key_checks = 0', [], 'count');
  1682. $id = Pdb::insert($ctlr->getTableName(), array('date_added' => Pdb::now()));
  1683.  
  1684. // Set "id" columns from multiedit records to zero for force an insert
  1685. foreach ($_POST as $key => &$val) {
  1686. if (is_array($val) and strpos($key, 'multiedit_') === 0) {
  1687. foreach ($val as &$multiedit_row) {
  1688. $multiedit_row['id'] = 0;
  1689. }
  1690. unset($multiedit_row);
  1691. }
  1692. }
  1693. unset($val);
  1694.  
  1695. $result = $ctlr->_duplicateSave($id);
  1696.  
  1697. // Re-enable foreign key constraints now that the real data has been saved
  1698. Pdb::q('SET foreign_key_checks = 1', [], 'count');
  1699.  
  1700. // Commit
  1701. if ($result == true) {
  1702. // Copy across per-record permissions, or create new ones if none exist for the original record
  1703. try {
  1704. $perms = PerRecordPerms::fetchDetails($ctlr, $orig_id);
  1705.  
  1706. $_POST['_prm_categories'] = $perms['categories'];
  1707.  
  1708. PerRecordPerms::save($ctlr, $id);
  1709. } catch (RowMissingException $ex) {
  1710. $_POST['_prm_categories'] = '*';
  1711.  
  1712. PerRecordPerms::save($ctlr, $id);
  1713. }
  1714.  
  1715. Pdb::commit();
  1716. }
  1717.  
  1718. if (Request::isAjax()) {
  1719. $result = (int) $result;
  1720. echo json_encode(array('result' => $result));
  1721. return;
  1722. }
  1723.  
  1724. if ($result == false) {
  1725. Notification::error('There was an error saving your changes');
  1726. Url::redirect("admin/duplicate/{$type}/{$orig_id}");
  1727. }
  1728.  
  1729. $new_tags = Tags::splitupTags($_POST['tags']);
  1730. $tag_result = Tags::update($ctlr->getTableName(), $id, $new_tags);
  1731. unset ($_SESSION['admin']['tags']);
  1732.  
  1733. if ($tag_result == false) {
  1734. Notification::error('There was an error updating the tags for this item');
  1735. }
  1736.  
  1737. $ctlr->_invalidateCaches('duplicate', $id);
  1738.  
  1739. unset ($_SESSION['admin']['field_values']);
  1740. Notification::confirm('Your changes have been saved');
  1741. Url::redirect("admin/edit/{$type}/{$id}");
  1742. }
  1743.  
  1744.  
  1745. /**
  1746.   * Moderation
  1747.   **/
  1748. public function moderate()
  1749. {
  1750. AdminAuth::checkLogin();
  1751.  
  1752. $view = new View('sprout/admin/main_layout');
  1753. $this->setDefaultMainviewParams($view);
  1754. $this->setNavigation($view, new PageAdminController());
  1755.  
  1756.  
  1757. $moderators = Register::getModerators();
  1758.  
  1759. if (count($moderators) == 0) {
  1760. $this->error("No moderation classes are registered.");
  1761. return;
  1762. }
  1763.  
  1764. $out = '<form action="SITE/admin/moderate_action" method="post">';
  1765. $out .= Csrf::token();
  1766.  
  1767. $idx = 0;
  1768. foreach ($moderators as $class) {
  1769. $inst = Sprout::instance($class, 'Sprout\Helpers\Moderate');
  1770.  
  1771. $out .= '<h3>' . Enc::html($inst->getFriendlyName()) . '</h3>';
  1772.  
  1773. $list = $inst->getList();
  1774. if ($list === null) {
  1775. $out .= '<p><i>Error: Unable to load record list for moderation.</i></p>';
  1776. continue;
  1777. }
  1778.  
  1779. if (count($list) == 0) {
  1780. $out .= '<p><i>Nothing needs moderation.</i></p>';
  1781. continue;
  1782. }
  1783.  
  1784. $out .= '<table class="main-list main-list-no-js moderation">';
  1785. $out .= '<thead>';
  1786. $out .= '<tr><th>Item details</th><th class="mod">Approve</th><th class="mod">Delete</th><th class="mod">Do nothing</th></tr>';
  1787. $out .= '</thead><tbody>';
  1788. foreach ($list as $id => $html) {
  1789. $idx++;
  1790. $out .= '<tr>';
  1791. $out .= '<td>' . $html . '</td>';
  1792. $out .= "<td class=\"mod\"><input type=\"radio\" name=\"moderate[{$class}][{$id}]\" value=\"app\" checked></td>";
  1793. $out .= "<td class=\"mod\"><input type=\"radio\" name=\"moderate[{$class}][{$id}]\" value=\"del\"></td>";
  1794. $out .= "<td class=\"mod\"><input type=\"radio\" name=\"moderate[{$class}][{$id}]\" value=\"\"></td>";
  1795. $out .= '</tr>';
  1796. }
  1797. $out .= '</tbody></table>';
  1798. }
  1799.  
  1800. $out .= '<div class="action-bar">';
  1801. $out .= '<button type="submit" class="button button-regular button-green icon-after icon-save">Save changes</button>';
  1802. $out .= '</div>';
  1803. $out .= '</form>';
  1804.  
  1805. $view->browser_title = 'Content Moderation';
  1806. $view->main_title = 'Content Moderation';
  1807. $view->main_content = $out;
  1808.  
  1809. echo $view->render();
  1810. }
  1811.  
  1812.  
  1813. /**
  1814.   * Processes the moderation form
  1815.   **/
  1816. public function moderateAction()
  1817. {
  1818. AdminAuth::checkLogin();
  1819. Csrf::checkOrDie();
  1820.  
  1821. if (@!is_array($_POST['moderate'])) $_POST['moderate'] = array();
  1822.  
  1823. Pdb::transact();
  1824.  
  1825. $approve = 0;
  1826. $delete = 0;
  1827. foreach ($_POST['moderate'] as $class => $records) {
  1828. if (! is_array($records)) continue;
  1829.  
  1830. $inst = Sprout::instance($class, 'Sprout\Helpers\Moderate');
  1831.  
  1832. foreach ($records as $id => $do) {
  1833. $id = (int) $id;
  1834.  
  1835. if ($do == 'app') {
  1836. $inst->approve($id);
  1837. $approve++;
  1838.  
  1839. } else if ($do == 'del') {
  1840. $inst->delete($id);
  1841. $delete++;
  1842.  
  1843. }
  1844. }
  1845. }
  1846.  
  1847. Pdb::commit();
  1848.  
  1849. if ($approve) Notification::confirm('Approved ' . $approve . ' record' . ($approve != 1 ? 's.' : '.'));
  1850. if ($delete) Notification::confirm('Deleted ' . $delete . ' record' . ($delete != 1 ? 's.' : '.'));
  1851. Url::redirect('admin/moderate');
  1852. }
  1853.  
  1854.  
  1855. /**
  1856.   * Calls any 'extra' commands that might be provided by the controller
  1857.   *
  1858.   * Method names will be prefixed with '_extra' and must be public
  1859.   * Names which are lower_cased will be converted to camelCase
  1860.   *
  1861.   * Supports varargs - additional args are passed to the underlying function
  1862.   * Called method should return an array, with two keys:
  1863.   * title string Main title
  1864.   * content string HTML for the main content area
  1865.   *
  1866.   * @example
  1867.   * class BookingAdminController extends ManagedAdminController {
  1868.   * // Call with url admin/extra/booking/send_email/:record_id
  1869.   * public function _extraSendEmail($record_id) {
  1870.   * return ['title' => '...', 'content' => '...'];
  1871.   * }
  1872.   * }
  1873.   *
  1874.   * @param string $type The class name of the method to call (must extend ManagedAdminController)
  1875.   * @param string $method The method name to call
  1876.   * @return void Outputs HTML
  1877.   **/
  1878. public function extra($class, $method)
  1879. {
  1880. AdminAuth::checkLogin();
  1881.  
  1882. $method = preg_replace('/[^a-zA-Z0-9_]/', '', $method);
  1883. if (empty($method)) {
  1884. throw new InvalidArgumentException('Invalid method specified');
  1885. }
  1886.  
  1887. $method = '_extra' . ucfirst(Text::lc2camelCase($method));
  1888.  
  1889. $ctlr = $this->getController($class);
  1890.  
  1891. try {
  1892. $reflect = new ReflectionMethod($ctlr, $method);
  1893. } catch (ReflectionException $ex) {
  1894. throw new InvalidArgumentException('Method "' . $method . '" does not exist');
  1895. }
  1896. if (!$reflect->isPublic()) {
  1897. throw new InvalidArgumentException('Method "' . $method . '" does not exist');
  1898. }
  1899.  
  1900. $view = new View('sprout/admin/main_layout');
  1901. $this->setDefaultMainviewParams($view);
  1902. $this->setNavigation($view, $ctlr);
  1903.  
  1904. $args = func_get_args();
  1905. $args = array_slice($args, 2);
  1906. $main = call_user_func_array([$ctlr, $method], $args);
  1907.  
  1908. if ($main instanceof AdminError) {
  1909. $this->error($main->getMessage(), $ctlr);
  1910. return;
  1911. }
  1912.  
  1913. if (!is_array($main)) {
  1914. $main = [
  1915. 'title' => $ctlr->getFriendlyName(),
  1916. 'content' => $main
  1917. ];
  1918. }
  1919.  
  1920. $view->browser_title = strip_tags($main['title']);
  1921. $view->main_title = $main['title'];
  1922. $view->main_content = $main['content'];
  1923. echo $view->render();
  1924. }
  1925.  
  1926.  
  1927. /**
  1928.   * Directly calls a method provided by an admin controller
  1929.   * Suports varargs - additional args are passed to the underlying function
  1930.   *
  1931.   * @param string $class The shorthand class name, e.g. 'page'
  1932.   * @param string $method The method name, e.g. 'reorder_top'
  1933.   * @return void Does whatever the called function does, e.g. echo or redirect
  1934.   */
  1935. public function call($class, $method)
  1936. {
  1937. AdminAuth::checkLogin();
  1938.  
  1939. $ctlr = $this->getController($class);
  1940. if (!$ctlr or !($ctlr instanceof ManagedAdminController)) {
  1941. throw new InvalidArgumentException('Controller "' . $class . '" does not exist');
  1942. }
  1943.  
  1944. if (!method_exists($ctlr, $method)) {
  1945. throw new InvalidArgumentException('Method "' . $method . '" does not exist');
  1946. }
  1947.  
  1948. $reflect = new ReflectionMethod($ctlr, $method);
  1949. if (!$reflect->isPublic()) {
  1950. throw new InvalidArgumentException('Method "' . $method . '" does not exist');
  1951. }
  1952.  
  1953. $args = func_get_args();
  1954. $args = array_slice($args, 2);
  1955.  
  1956. call_user_func_array([$ctlr, $method], $args);
  1957. }
  1958.  
  1959.  
  1960. /**
  1961.   * Sets the active subsite, and then redirects back to the admin area.
  1962.   * Uses the post variable "subsite".
  1963.   **/
  1964. public function setActiveSubsite()
  1965. {
  1966. AdminAuth::checkLogin();
  1967. Csrf::checkOrDie();
  1968.  
  1969. $_POST['subsite'] = (int) @$_POST['subsite'];
  1970. if ($_POST['subsite'] <= 0) {
  1971. die('Invalid POST data');
  1972. }
  1973.  
  1974. // Does the operator actually have access to edit this subsite?
  1975. if (!AdminPerms::canAccessSubsite($_POST['subsite'])) {
  1976. Notification::error('Access denied');
  1977. Url::redirect(Kohana::config('sprout.admin_intro'));
  1978. }
  1979.  
  1980. $_SESSION['admin']['active_subsite'] = $_POST['subsite'];
  1981.  
  1982. Notification::confirm('Subsite changed');
  1983. Url::redirect(Kohana::config('sprout.admin_intro'));
  1984. }
  1985.  
  1986.  
  1987. /**
  1988.   * Sets up the sidebar navigation for a view to show the navigation for a specific controller.
  1989.   *
  1990.   * @param View $view The view to set the navigation parameters for.
  1991.   * @param Controller $ctlr The controller to use for navigation (and searching if supported).
  1992.   **/
  1993. private function setNavigation(View $view, Controller $ctlr)
  1994. {
  1995. // If no navigation has been set, use the default
  1996. if (empty($view->nav)) {
  1997. $view->nav = $ctlr->_getNavigation();
  1998. }
  1999.  
  2000. $view->controller_name = $ctlr->getControllerName();
  2001. $view->controller_navigation_name = $ctlr->getNavigationName();
  2002. $view->nav_tools = $ctlr->_getTools();
  2003. }
  2004.  
  2005.  
  2006. /**
  2007.   * Sets the a bunch of parameters for a the main view.
  2008.   *
  2009.   * @param View $view The view to set the parameters for.
  2010.   **/
  2011. private function setDefaultMainviewParams($view)
  2012. {
  2013. $view->admin_authenticated = true;
  2014.  
  2015. // Browser version checks. FF3+, IE7+
  2016. $browser_ok = false;
  2017. if (Kohana::userAgent('browser') == 'Firefox') {
  2018. $browser_ok = true;
  2019. } else if (Kohana::userAgent('browser') == 'Chrome') {
  2020. $browser_ok = true;
  2021. } else if (Kohana::userAgent('browser') == 'Internet Explorer' and version_compare(Kohana::userAgent('version'), '7.0', '>=')) {
  2022. $browser_ok = true;
  2023. }
  2024.  
  2025. // Set a message if the browser is not supported.
  2026. if (! $browser_ok) {
  2027. $view->info_message = new View('sprout/admin/message_bad_browser');
  2028. }
  2029.  
  2030. // Header under the sprout logo
  2031. $view->header_subtitle = '';
  2032.  
  2033. if (!empty($_SESSION['admin'])) {
  2034. $view->live_url = Subsites::getAbsRoot($_SESSION['admin']['active_subsite']);
  2035. }
  2036. }
  2037.  
  2038. /**
  2039.   * Returns an instance of a controller class for a given type
  2040.   *
  2041.   * @deprecated This function is now just an alias for {@see Admin::getController}
  2042.   * @param string $type A class name, or shorthand identifier
  2043.   * e.g. 'Sprout\Controllers\AwesomeController' or 'awesome'
  2044.   * @return Controller
  2045.   * @throws Exception If the class is unknown
  2046.   * @todo Handle module autoloading, e.g. should be able to specify 'thingy'
  2047.   * and get SproutModules\AwesomeDeveloper\Controllers\ThingyController
  2048.   **/
  2049. private function getController($type)
  2050. {
  2051. return Admin::getController($type);
  2052. }
  2053.  
  2054.  
  2055. /**
  2056.   * Does lock checking, locking, or lock messages.
  2057.   *
  2058.   * @param string $type Admin controller slug, e.g. 'page'
  2059.   * @param int $id Record id which is being edited
  2060.   * @param View $view Main layout view to provide lock details into
  2061.   */
  2062. private function lock($type, $id, View $view)
  2063. {
  2064. if (! Admin::locksEnabled()) return;
  2065.  
  2066. $type = (string) $type;
  2067. $id = (int) $id;
  2068.  
  2069. $lock = Admin::getLock($type, $id);
  2070.  
  2071. if ($lock == null) {
  2072. // No lock; acquire it
  2073. $lock_id = Admin::lock($type, $id);
  2074. $view->currlock = [
  2075. 'id' => (int)$lock_id,
  2076. 'ctlr' => $type,
  2077. 'record_id' => $id,
  2078. 'edit_token' => Csrf::getTokenValue(),
  2079. ];
  2080.  
  2081. } else if ($lock['lock_key'] == $_SESSION['admin']['lock_key']) {
  2082. // Is locked to this session
  2083. Admin::pingLock($lock['id']);
  2084. $view->currlock = [
  2085. 'id' => (int)$lock['id'],
  2086. 'ctlr' => $type,
  2087. 'record_id' => $id,
  2088. 'edit_token' => Csrf::getTokenValue(),
  2089. ];
  2090.  
  2091. } else {
  2092. // Locked to a different session
  2093. $view->locked = $lock;
  2094. }
  2095. }
  2096.  
  2097.  
  2098. /**
  2099.   * Unlocks lock for a given record/controller, all records for a controller, or all locks
  2100.   **/
  2101. private function unlock($type = null, $id = null)
  2102. {
  2103. Admin::unlock($type, $id);
  2104. }
  2105.  
  2106.  
  2107. /**
  2108.   * Unlock a record.
  2109.   * Called via ajax in the beforeunload javascript event
  2110.   **/
  2111. public function ajaxUnlock()
  2112. {
  2113. AdminAuth::checkLogin();
  2114.  
  2115. if (!Csrf::check()) {
  2116. die('Session timeout or missing security token');
  2117. }
  2118.  
  2119. Admin::unlock($_POST['ctlr'], $_POST['record_id']);
  2120. echo '.';
  2121. }
  2122.  
  2123.  
  2124. /**
  2125.   * Restore a deleted record from log data
  2126.   * @param int $log_id ID in the history_items table
  2127.   * @return void
  2128.   */
  2129. public function restore($log_id)
  2130. {
  2131. AdminAuth::checkLogin();
  2132. Csrf::checkOrDie();
  2133. $log_id = (int) $log_id;
  2134.  
  2135. // Gather data
  2136. $q = "SELECT id, record_id, record_table, controller, data
  2137. FROM ~history_items
  2138. WHERE id = ? AND type = 'Delete'";
  2139. $records = Pdb::q($q, [$log_id], 'map-arr');
  2140. if (count($records) == 0) {
  2141. throw new Kohana_404_Exception();
  2142. }
  2143.  
  2144. $main_record = Sprout::iterableFirstValue($records);
  2145. $new = $records;
  2146. while (count($new) > 0) {
  2147. $params = [];
  2148. $q = "SELECT id, record_table, data FROM ~history_items WHERE type = 'Delete' AND ";
  2149. $q .= Pdb::buildClause([['parent_id', 'IN', array_keys($new)]], $params);
  2150. $new = Pdb::q($q, $params, 'map-arr');
  2151. foreach ($new as $id => $data) {
  2152. $records[$id] = $data;
  2153. }
  2154. }
  2155.  
  2156. // Restore data
  2157. Pdb::transact();
  2158. foreach ($records as $row) {
  2159. Pdb::validateIdentifier($row['record_table']);
  2160. $data = json_decode($row['data'], true);
  2161. $cols = $values = '';
  2162. foreach ($data as $field => $value) {
  2163. Pdb::validateIdentifier($field);
  2164. if ($cols) {
  2165. $cols .= ', ';
  2166. $values .= ', ';
  2167. }
  2168. $cols .= $field;
  2169. $values .= ':' . $field;
  2170. }
  2171. $q = "INSERT INTO ~{$row['record_table']} ({$cols}) VALUES ({$values})";
  2172. try {
  2173. Pdb::q($q, $data, 'null');
  2174. } catch (QueryException $ex) {
  2175. Pdb::rollback();
  2176. Notification::error('Database error during restore');
  2177. Url::redirect('admin/edit/action_log/' . $log_id);
  2178. }
  2179. }
  2180. $op = AdminAuth::getDetails();
  2181. $data = ['restored_date' => Pdb::now(), 'restored_operator' => @$op['name']];
  2182. Pdb::update('history_items', $data, ['id' => $log_id]);
  2183. Pdb::commit();
  2184.  
  2185. Notification::confirm('Data has been restored');
  2186. $ctlr_class = $main_record['controller'];
  2187. if (class_exists($ctlr_class)) {
  2188. $ctlr = new $ctlr_class();
  2189. Url::redirect('admin/edit/' . Enc::url($ctlr->getControllerName()) . '/' . $main_record['record_id']);
  2190. } else {
  2191. Url::redirect('admin/edit/action_log/' . $log_id);
  2192. }
  2193. }
  2194.  
  2195.  
  2196. /**
  2197.   * Browser information
  2198.   * @return void Echos HTML
  2199.   */
  2200. public function userAgent()
  2201. {
  2202. $data = UserAgent::getInfo();
  2203. $data['full_ua'] = $_SERVER['HTTP_USER_AGENT'];
  2204. $data['body_classes'] = UserAgent::getBodyClasses();
  2205.  
  2206. echo '<style>';
  2207. echo 'table { border-collapse: collapse; margin: 50px auto; }';
  2208. echo 'h1,p { font-family: sans-serif; text-align: center; margin: 50px auto; }';
  2209. echo 'td { font-family: sans-serif; padding: 10px 15px; border: 1px #eee solid; }';
  2210. echo '</style>';
  2211.  
  2212. echo '<h1>User-agent</h1>';
  2213. echo "<table>\n";
  2214. foreach ($data as $field => $val) {
  2215. echo "<tr>\n";
  2216. echo "<td><b>", Enc::html($field), "</b></td>\n";
  2217. echo "<td>", Enc::html($val), "</td>\n";
  2218. echo "</tr>\n";
  2219. }
  2220. echo "</table>\n";
  2221. echo '<p>Uses data from the <a href="https://github.com/Karmabunny/user-agents.json">user-agents.json</a> project.</p>';
  2222. }
  2223.  
  2224.  
  2225. /**
  2226.   * Activates AutoLaunch revisions
  2227.   **/
  2228. public function cronGenericActivate()
  2229. {
  2230. Cron::start('Generic autolaunch system');
  2231.  
  2232. $tbl_prefix = Pdb::prefix();
  2233.  
  2234. // Find autolaunch/autonuke tables
  2235. $q = "SHOW TABLE STATUS";
  2236. $db_tables = Pdb::query($q, [], 'pdo');
  2237.  
  2238. $tables = array();
  2239. foreach ($db_tables as $tbl) {
  2240. if (strpos($tbl['Name'], $tbl_prefix) !== 0) {
  2241. continue;
  2242. }
  2243.  
  2244. if ($tbl['Name'] === "{$tbl_prefix}page_revisions") {
  2245. continue;
  2246. }
  2247.  
  2248. $q = "SHOW COLUMNS FROM {$tbl['Name']}";
  2249. $db_cols = Pdb::query($q, [], 'pdo');
  2250.  
  2251. $tables[$tbl['Name']] = 0;
  2252. foreach ($db_cols as $col) {
  2253. if ($col['Field'] == 'date_launch') $tables[$tbl['Name']]++;
  2254. if ($col['Field'] == 'date_expire') $tables[$tbl['Name']]++;
  2255. if ($col['Field'] == 'active') $tables[$tbl['Name']]++;
  2256. }
  2257. }
  2258.  
  2259. Pdb::transact();
  2260.  
  2261. foreach ($tables as $tbl => $num_cols) {
  2262. if ($num_cols !== 3) continue;
  2263.  
  2264. $tbl_no_prefix = substr($tbl, strlen($tbl_prefix));
  2265.  
  2266. Cron::message("Processing table {$tbl}");
  2267.  
  2268. try {
  2269. // Launch
  2270. $q = "SELECT id
  2271. FROM {$tbl}
  2272. WHERE active = 0
  2273. AND date_launch != '0000-00-00'
  2274. AND date_launch IS NOT NULL
  2275. AND date_launch <= NOW()
  2276. AND (
  2277. date_expire > NOW()
  2278. OR date_expire = '0000-00-00'
  2279. OR date_expire IS NULL
  2280. )";
  2281. $res = Pdb::q($q, [], 'arr');
  2282.  
  2283. foreach ($res as $row) {
  2284. Cron::message("Activating record {$row['id']}");
  2285. Pdb::update($tbl_no_prefix, ['active' => 1], ['id' => $row['id']]);
  2286. }
  2287.  
  2288.  
  2289. // Unlaunch
  2290. $q = "SELECT id
  2291. FROM {$tbl}
  2292. WHERE active = 1
  2293. AND date_expire != '0000-00-00'
  2294. AND date_expire IS NOT NULL
  2295. AND date_expire < NOW()";
  2296. $res = Pdb::q($q, [], 'arr');
  2297.  
  2298. foreach ($res as $row) {
  2299. Cron::message("Expiring record {$row['id']}");
  2300. Pdb::update($tbl_no_prefix, ['active' => 0], ['id' => $row['id']]);
  2301. }
  2302. } catch (QueryException $ex) {
  2303. return Cron::failure('Database error');
  2304. }
  2305. }
  2306.  
  2307. Pdb::commit();
  2308.  
  2309. Cron::success();
  2310. }
  2311.  
  2312.  
  2313. public function heartbeat()
  2314. {
  2315. AdminAuth::checkLogin();
  2316.  
  2317. echo "Ah ah ah ah, stayin' alive, stayin' alive...";
  2318.  
  2319.  
  2320. // We piggyback the heartbeat to keep locks up-to-date
  2321. if (isset($_GET['lock_id'])) {
  2322. Admin::pingLock($_GET['lock_id']);
  2323. Admin::clearOldLocks();
  2324. }
  2325. }
  2326. }
  2327.