SproutCMS

This is the code documentation for the SproutCMS project

source of /sprout/Helpers/Admin.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\Helpers;
  15.  
  16. use Exception;
  17.  
  18. use Kohana;
  19.  
  20. use karmabunny\pdb\Exceptions\QueryException;
  21.  
  22.  
  23. /**
  24. * Sorter for widgets
  25. **/
  26. function _widgetSort($a, $b) {
  27. $a = Widgets::instantiate($a);
  28. $b = Widgets::instantiate($b);
  29. return strcmp($a->getFriendlyName(), $b->getFriendlyName());
  30. }
  31.  
  32. /**
  33. * Useful functions for the admin
  34. **/
  35. class Admin
  36. {
  37. static private $error_msgs = array(
  38. 'required' => 'This field is required',
  39. 'length\[0,([0-9]+)\]' => 'Too long, max length is $1 characters',
  40. 'length\[([0-9]+),([0-9]+)\]' => 'Incorrect length, must be between $1 and $2 characters',
  41. 'length\[([0-9]+)\]' => 'Incorrect length, must be exactly $1 characters',
  42. 'alphaDash' => 'Field can only contain letters, numbers, underscores (_) and dashes (-)',
  43. 'emailUnique' => 'Email address is not unique',
  44. 'email' => 'Email address is not valid',
  45. 'matches\[password.*' => 'Password fields do not match',
  46. 'numMin\[([-0-9]+)\]' => 'Number must be at least $1',
  47. 'numMax\[([-0-9]+)\]' => 'Number must be at most $1',
  48. 'numBetween\[([-0-9]+),([-0-9]+)\]' => 'Number must be between $1 and $2 inclusive',
  49. 'check_redirect' => 'URL must begin with http:// (external url) or / (internal url)',
  50. 'exclusive\[(.*)\]' => 'These fields cannot be used together, only one of them may be used',
  51. 'check_controller_entrance\[.*\]' => 'Only one page may reference any given module',
  52.  
  53. 'raw\[(.+)\]' => '$1',
  54. '.+' => 'Incorrect value provided',
  55. );
  56. private static $cat_tablename;
  57. private static $cat_singlecat = false;
  58.  
  59. /**
  60.   * Finds an appropriate error message for the specified error code
  61.   **/
  62. public static function lookupErrMsg($err)
  63. {
  64. $message = null;
  65.  
  66. if ($err instanceof AdminError) {
  67. return $err->getMessage();
  68. }
  69.  
  70. foreach (self::$error_msgs as $search => $replace) {
  71. $count = 0;
  72. $message = preg_replace('/^'.$search.'$/', $replace, $err, 1, $count);
  73. if ($count > 0) break;
  74. }
  75.  
  76. if (! $message) $message = 'Invalid value provided.';
  77.  
  78. return $message;
  79. }
  80.  
  81.  
  82. /**
  83.   * Shows a per-field error message
  84.   *
  85.   * @param string $field_name The name of the field to show the error message for.
  86.   **/
  87. public static function fieldError($field_name, $scope = 'admin')
  88. {
  89. if (empty($_SESSION[$scope]['field_errors'][$field_name])) return;
  90.  
  91. $error = self::lookupErrMsg($_SESSION[$scope]['field_errors'][$field_name]);
  92.  
  93. $class = Enc::id('field-error-' . $field_name);
  94.  
  95. $out = "<span class=\"field-error {$class}\">";
  96. $out .= str_replace(' ', '&nbsp;', Enc::html($error));
  97. $out .= '</span>';
  98.  
  99. return $out;
  100. }
  101.  
  102.  
  103. /**
  104.   * Clears all pre-field error messages
  105.   **/
  106. public static function clearFieldErrors($scope = 'admin')
  107. {
  108. unset ($_SESSION[$scope]['field_errors']);
  109. }
  110.  
  111.  
  112. /**
  113.   * Shows a UI for the list of widgets, for editing
  114.   *
  115.   * @param string $field_name Name of the field to store the final value when the form is submitted
  116.   * @param WidgetArea $area The area that is being edited
  117.   * @param array $curr_widgets A list of the widgets currently being used, in order, as db rows with keys:
  118.   * type string Class name, e.g. 'RichText'
  119.   * settings string Opaque JSON string
  120.   * conditions string Opaque JSON string
  121.   * active int 1 for active, 0 for inactive
  122.   * heading string HTML H2 rendered front-end within widget
  123.   * @param boolean $enable_all Toggle whether all the widgets are enabled by default (defaults to true)
  124.   **/
  125. public static function widgetList($field_name, WidgetArea $area, $curr_widgets, $enable_all = true)
  126. {
  127. Needs::fileGroup('widget_list');
  128.  
  129. if ($curr_widgets == null) {
  130. $curr_widgets = [];
  131. }
  132.  
  133. $widget_list_id = 'wl-' . Enc::id($field_name);
  134.  
  135. echo '<script type="text/javascript">';
  136. echo "$(document).ready(function() {\n";
  137. echo " var list = new widget_list('", Enc::js($field_name), "');\n";
  138. foreach ($curr_widgets as $widget) {
  139. $inst = Widgets::instantiate($widget['type']);
  140. $eng_name = Enc::js($inst->getFriendlyName());
  141.  
  142. if (!$enable_all) $widget['active'] = 0;
  143.  
  144. $add_opts = [
  145. 'type' => $widget['type'],
  146. 'label' => $eng_name,
  147. 'settings' => $widget['settings'],
  148. 'conditions' => $widget['conditions'],
  149. 'active' => (bool)$widget['active'],
  150. 'heading' => @$widget['heading'],
  151. 'template' => @$widget['template'],
  152. ];
  153.  
  154. echo " list.add_widget(", json_encode($add_opts), ");\n";
  155. }
  156. echo "\n";
  157. echo " $('#{$widget_list_id}').bind('add-widget', function(e, widget_name, english_name) {\n";
  158. echo " list.add_widget({ type: widget_name, label: english_name, settings: '', active: true });\n";
  159. echo " return false;\n";
  160. echo " });\n";
  161. echo "});\n";
  162. echo '</script>';
  163.  
  164. if ($enable_all) {
  165. echo "<div id=\"{$widget_list_id}\" class=\"widget-list\">\n";
  166. } else {
  167. echo "<div id=\"{$widget_list_id}\" class=\"widget-list all-collapsed\">\n";
  168. }
  169. echo '<div class="widgets-sel"></div>';
  170. echo '<div class="widgets-empty">This area does not have any content blocks. Click the button below to add a content block:</div>';
  171. echo '<div class="content-block-button-wrap">';
  172. echo "<input type=\"checkbox\" id=\"{$widget_list_id}--add-btn\" class=\"giant-popup-checkbox -vis-hidden\">";
  173.  
  174. if ($enable_all) {
  175. $add_tooltip = 'Add a block of content.';
  176. if ($field_name == "embedded") {
  177. $add_tooltip = 'Add a block of content to the page.';
  178. } else if ($field_name == "sidebar") {
  179. $add_tooltip = 'Add a block of content to the sidebar.';
  180. } else if ($field_name == "email") {
  181. $add_tooltip = 'Add a block of content to the email.';
  182. }
  183. echo "<label for=\"{$widget_list_id}--add-btn\" class=\"button button-green button-regular button-block add-content-block-button\">Add content block";
  184. echo '<span class="tooltip-wrapper">';
  185. echo '<span class="tooltip-trigger">';
  186. echo '<span class="tooltip-trigger-icon icon-before icon-live_help"></span>';
  187. echo '</span>';
  188. echo '<span class="tooltip-content">';
  189. echo Enc::html($add_tooltip);
  190. echo '</span>';
  191. echo '</span>';
  192. echo "</label>";
  193. }
  194.  
  195.  
  196. echo '<div class="giant-popup-outer">';
  197. echo '<div class="giant-popup-wrapper">';
  198. echo '<div class="giant-popup-inner">';
  199.  
  200. echo "<label for=\"{$widget_list_id}--add-btn\" class=\"giant-popup-close-button icon-before icon-close\">Close</label>";
  201. echo '<h2 class="giant-popup-title">Content blocks</h2>';
  202. echo '<ul class="giant-popup-content columns -clearfix">';
  203.  
  204. $tiles = Register::getWidgetTiles($area->getName());
  205. ksort($tiles);
  206.  
  207. // For the "embedded" area, sort the 'Content' tile to the start and the 'Advanced' tile to the end
  208. if ($area->getName() === 'embedded') {
  209. $content = $tiles['Text blocks'];
  210. $collections = $tiles['Collections'];
  211. $advanced = $tiles['Advanced'];
  212. unset($tiles['Text blocks'], $tiles['Collections'], $tiles['Advanced']);
  213. array_unshift($tiles, $collections);
  214. array_unshift($tiles, $content);
  215. array_push($tiles, $advanced);
  216. }
  217.  
  218. $index = 0;
  219. foreach ($tiles as $tile) {
  220.  
  221. if ((++$index) % 4 == 0) {
  222. echo '<li class="giant-popup-item column column-3 column-last">';
  223. } else {
  224. echo '<li class="giant-popup-item column column-3">';
  225. }
  226. echo '<div class="giant-popup-item-inner">';
  227.  
  228. echo '<div class="giant-popup-item-title-wrapper">';
  229. echo '<div class="giant-popup-item-title-icon icon-before icon-', Enc::html($tile['icon']), '"></div>';
  230. echo '<h3 class="giant-popup-item-title">', Enc::html($tile['name']), '</h3>';
  231. echo '</div>';
  232.  
  233. echo '<div class="giant-popup-item-content -clearfix">';
  234. echo '<div class="giant-popup-item-links">';
  235. echo '<ul class="list-style-1">';
  236. foreach ($tile['widgets'] as $widg => $name) {
  237.  
  238. $inst = Widgets::instantiate($widg);
  239. $desc = $inst->getFriendlyDesc();
  240.  
  241. echo '<li><a class="widget-list-new-widget" href="javascript:;" data-name="', Enc::html($widg), '" title="' . Enc::html($desc) . '">', Enc::html($name), '</a></li>';
  242. }
  243. echo '</ul>';
  244. echo '</div>';
  245. echo '</div>';
  246.  
  247. echo '</div>';
  248. echo '</li>';
  249. }
  250.  
  251. echo '</ul>';
  252.  
  253. echo '</div>';
  254. echo '</div>';
  255. echo '</div>';
  256.  
  257. echo "</div>\n";
  258. echo "</div>\n";
  259. }
  260.  
  261.  
  262. /**
  263.   * Deprecated wrapper around {@see Fb::pageDropdown}
  264.   *
  265.   * @param string $field_name The name of the field
  266.   * @param int $selected The id of the page to select
  267.   * @param int $exclude The id of the page to exclude from the list
  268.   * @param int $subsite_id The subsite of pages to show. Defaults to the current admin subsite
  269.   * @param string $top_text Text for the top (id 0) item
  270.   **/
  271. public static function pageDropdown($field_name, $selected = null, $exclude = null, $subsite_id = null, $top_text = 'None (top level page)')
  272. {
  273. echo Fb::pageDropdown($field_name, [], [
  274. 'exclude' => [$exclude],
  275. ]);
  276. }
  277.  
  278.  
  279. /**
  280.   * Render a tree of nodes for the navigation area of the admin
  281.   * Used by the pages module
  282.   *
  283.   * @param Treenode $node The node to render
  284.   * @param array $actions Additional links to show in the cog icon menu; keys: url, class, name
  285.   * @param int $depth How deep in the tree the rendering is, starts at 1 for the top-level
  286.   * @return void Outputs HTML directly
  287.   **/
  288. public static function navigationTreeNode($node, array $actions, $depth = 1)
  289. {
  290. $admin_perms = AdminPerms::checkPermissionsTree('pages', $node['id']);
  291.  
  292. $name = Enc::html(Text::limitChars($node['name'], 35, '...'));
  293.  
  294. $class = "node depth{$depth}";
  295. $class .= ($admin_perms ? ' allow-access' : ' no-access');
  296.  
  297. if (count($node->children) > 0) {
  298. $class .= ' has-children collapsed';
  299. }
  300.  
  301. if (self::getControllerSlug() === 'page') {
  302. if (self::getRecordId() == $node['id']) {
  303. $class .= ' active-node';
  304. } else if ($node->findNodeValue('id', self::getRecordId())) {
  305. $class .= ' active-parent-node';
  306. $class = str_replace(' collapsed', '', $class);
  307. }
  308. }
  309.  
  310. // Render node
  311. echo "<li class=\"{$class}\" data-id=\"{$node['id']}\">";
  312. echo "<div>";
  313.  
  314. $action = reset($actions);
  315. $url = str_replace('%%', $node['id'], $action['url']);
  316. echo "<a class=\"node-link\" href=\"{$url}\">{$name}</a>";
  317.  
  318. if (count($node->children) > 0) {
  319. echo "<button class=\"tree-list-expand-button icon-before icon-keyboard_arrow_right\" type=\"button\">Expand</button>";
  320. }
  321. echo "<button class=\"tree-list-settings-button icon-before icon-settings\" type=\"button\">Settings</button>";
  322.  
  323. echo "<div class=\"tree-list-settings-dropdown dropdown-box\">";
  324. echo "<ul class=\"tree-list-settings-dropdown-list list-style-2\">";
  325. foreach ($actions as $action) {
  326. $url = str_replace('%%', $node['id'], $action['url']);
  327. $class = trim('tree-list-settings-dropdown-list-item ' . @$action['class']);
  328. echo "<li class=\"{$class}\"><a href=\"", Enc::html($url), "\">", Enc::html($action['name']), "</a></li>";
  329. }
  330. echo "</ul>";
  331. echo "</div>";
  332. echo "</div>";
  333.  
  334.  
  335. // Render children
  336. if (count($node->children) > 0 and $admin_perms) {
  337. echo "<ul class=\"node-children-list\">";
  338.  
  339. $depth++;
  340.  
  341. foreach ($node->children as $child) {
  342. self::navigationTreeNode($child, $actions, $depth);
  343. }
  344.  
  345. echo "</ul>";
  346.  
  347. }
  348.  
  349. echo "</li>";
  350. }
  351.  
  352.  
  353. /**
  354.   * For a set of multi-edit fields, loads the data into a much more usable array
  355.   *
  356.   * Input:
  357.   * [phone] = ('1234 1234', '4321 4321')
  358.   * [type] = ('Mobile', 'Home')
  359.   *
  360.   * Output:
  361.   * [1] = ('phone' => '1234 1234', 'type' => 'Mobile')
  362.   * [2] = ('phone' => '4321 4321', 'type' => 'Home')
  363.   *
  364.   * @param array $dataset The original data to use
  365.   * @param array $field_names An array of the names of the fields to use
  366.   **/
  367. public static function multieditBuild($dataset, $field_names)
  368. {
  369. $records = array();
  370.  
  371. $primary = array_shift($field_names);
  372.  
  373. foreach ($dataset[$primary] as $idx => $value) {
  374. $row[$primary] = $value;
  375. foreach ($field_names as $name) {
  376. $row[$name] = $dataset[$name][$idx];
  377. }
  378.  
  379. $has_data = false;
  380. foreach ($row as $val) {
  381. if ($val != '') {
  382. $has_data = true;
  383. break;
  384. }
  385. }
  386.  
  387. if ($has_data) $records[] = $row;
  388. }
  389.  
  390. array_pop($records); // the last one is a dummy
  391.  
  392. return $records;
  393. }
  394.  
  395.  
  396. /**
  397.   * Outputs a list of checkboxes.
  398.   *
  399.   * If the $field parameter is provided, multiple checkboxes with the same field name will be rendered.
  400.   * $data should be a key-value pair, with the keys being the value of the checkbox, and the value being the label.
  401.   * $selected should be an array of selected checkbox ids.
  402.   *
  403.   * If the $field parameter is omitted (null), multiple checkboxes widh different field names will be rendered.
  404.   * The checkboxes will be binary checkboxes with a value of 1.
  405.   * $data should be a key-value pair, with the keys being the field name, and the value being the label.
  406.   * $selected should be a key-value pair, with the keys being the field name, and the value being 1 or 0.
  407.   **/
  408. public static function checkboxList($field, $data, $selected)
  409. {
  410. echo "<div class=\"checkbox-list-wrapper\">\n";
  411. echo "<div class=\"checkbox-list\">\n";
  412.  
  413. $common_field = false;
  414. if ($field != null) {
  415. $common_field = true;
  416. if (! preg_match('/\[\]$/', $field)) $field .= '[]';
  417. }
  418.  
  419. echo "<div class=\"field-element field-element--white field-element--checkbox'\">";
  420. echo "<fieldset class=\"fieldset--checkboxboollist\">";
  421. echo "<legend class=\"fieldset__legend\">Categories</legend>";
  422. echo "<div class=\"field-element__input-set\">";
  423.  
  424. $val = '';
  425. foreach ($data as $id => $name) {
  426. if ($common_field) {
  427. $val = $id;
  428. $html_id = Enc::id($field . $val);
  429. $checked = @in_array($id, $selected);
  430.  
  431. } else {
  432. $field = $id;
  433. $html_id = Enc::id($field . $val);
  434. $val = 1;
  435. $checked = (bool) @$selected[$id];
  436. }
  437.  
  438. echo "<div class=\"fieldset-input\">";
  439. if ($checked) {
  440. echo '<input type="checkbox" name="', Enc::html($field), '" value="', Enc::html($val), '" id="', $html_id, '" checked>';
  441. } else {
  442. echo '<input type="checkbox" name="', Enc::html($field), '" value="', Enc::html($val), '" id="', $html_id, '">';
  443. }
  444. echo "<label for=\"{$html_id}\">";
  445. echo Enc::html($name);
  446. echo "</label>";
  447. echo "</div>";
  448. }
  449.  
  450. echo "</div>";
  451. echo "</fieldset>";
  452. echo "</div>";
  453.  
  454. echo "</div>\n";
  455. echo "</div>\n";
  456. }
  457.  
  458.  
  459. public static function setCategoryTablename($name)
  460. {
  461. self::$cat_tablename = $name;
  462. }
  463.  
  464. public static function setCategorySinglecat($value)
  465. {
  466. self::$cat_singlecat = $value;
  467. }
  468.  
  469. /**
  470.   * Outputs an interface for selecting multiple categories
  471.   *
  472.   * @param $field The field name
  473.   * @param $data An array of key-value pairs, with the keys being the id of the category, and the value being the name.
  474.   * @param $selected An array of selected category ids.
  475.   **/
  476. public static function categorySelection($field, $data, $selected)
  477. {
  478. if (! self::$cat_tablename) {
  479. echo '<p>ERROR: <code>Admin::setCategoryTablename()</code> has not been called.</p>';
  480. return;
  481. }
  482.  
  483. if (! preg_match('/\[\]$/', $field)) $field .= '[]';
  484.  
  485. if (self::$cat_singlecat and @count($selected) > 1) {
  486. $selected = array_slice($selected, 0, 1, true);
  487. }
  488.  
  489. $type = (self::$cat_singlecat ? 'radio' : 'checkbox');
  490.  
  491. echo "<div class=\"onthefly-catadd-wrapper\">\n";
  492.  
  493. echo "<div class=\"onthefly-catadd-table-wrapper\">\n";
  494. if(!empty($data)){
  495. echo "<div class=\"checkbox-list-wrapper\">\n";
  496. echo "<div class=\"checkbox-list category-selection\">\n";
  497.  
  498. echo "<div class=\"field-element field-element--{$type}'\">";
  499. echo "<fieldset class=\"fieldset--{$type}boollist\">";
  500. echo "<legend class=\"fieldset__legend\">Categories</legend>";
  501. echo "<div class=\"field-element__input-set\">";
  502.  
  503. foreach ($data as $id => $name) {
  504. $html_id = Enc::id($field . $id);
  505. $checked = @in_array($id, $selected);
  506.  
  507. echo "<div class=\"fieldset-input\">";
  508. if ($checked) {
  509. echo '<input type="', $type, '" name="', Enc::html($field), '" value="', Enc::html($id), '" id="', $html_id, '" checked>';
  510. } else {
  511. echo '<input type="', $type, '" name="', Enc::html($field), '" value="', Enc::html($id), '" id="', $html_id, '">';
  512. }
  513. echo "<label for=\"{$html_id}\">";
  514. echo Enc::html($name);
  515. echo "</label>";
  516. echo "</div>";
  517. }
  518.  
  519. echo "</div>";
  520. echo "</fieldset>";
  521. echo "</div>";
  522.  
  523. echo "</div>\n";
  524. echo "</div>\n";
  525. }
  526. echo "</div>\n";
  527.  
  528. // Show category quickadd, if allowed
  529. $controller = Inflector::singular(Category::tableCat2main(self::$cat_tablename));
  530. if (AdminPerms::controllerAccess($controller, 'categories')) {
  531. echo '<div class="onthefly-catadd -clearfix" data-tablename="' . self::$cat_tablename . '" data-singlecat="' . ((int)self::$cat_singlecat) . '" data-field="' . $field . '">';
  532. echo Csrf::token();
  533. echo '<div class="field-element field-element--white field-element--text field-element--small">';
  534. echo '<div class="field-label"><label for="onthefly-catadd">Add a new category</label></div>';
  535. echo '<div class="field-input"><input type="text" id="onthefly-catadd" spellcheck="true" class="textbox" placeholder="Category name"></div>';
  536. echo "</div>\n";
  537. echo '<div class="field-element field-element--white field-element--button field-element--small">';
  538. echo '<button id="onthefly-catadd-button" type="button" class="button button-green button-small onthefly-catadd icon-after icon-add">Add</button>';
  539. echo "</div>\n";
  540. echo "</div>\n";
  541. }
  542.  
  543. echo "</div>\n";
  544.  
  545. }
  546.  
  547. /**
  548.   * Outputs a list of radiobuttons, which will all use the same field name
  549.   *
  550.   * @param string $field The field name.
  551.   * @param array $data The data. Key is the radiobutton value, Value is used in the label for the radiobutton.
  552.   * @param array $selected The selected item
  553.   **/
  554. public static function radioList($field, $data, $selected)
  555. {
  556. $field_id = Enc::id($field);
  557. $error = self::fieldError($field);
  558.  
  559. echo "<div class=\"checkbox-list-wrapper\">\n";
  560. echo "<div class=\"checkbox-list\">\n";
  561.  
  562. echo "<div class=\"field-element field-element--white field-element--radio'\">";
  563. echo "<fieldset class=\"fieldset--checkboxboollist\">";
  564. echo "<legend class=\"fieldset__legend\">Categories</legend>";
  565. echo "<div class=\"field-element__input-set\">";
  566.  
  567. foreach ($data as $id => $name) {
  568. echo "<div class=\"fieldset-input\">";
  569. if ($id == $selected) {
  570. echo '<input type="radio" name="', Enc::html($field), '" value="', Enc::html($id), '" id="', $field_id, $id, '" checked>';
  571. } else {
  572. echo '<input type="radio" name="', Enc::html($field), '" value="', Enc::html($id), '" id="', $field_id, $id, '">';
  573. }
  574. echo "<label for=\"{$field_id}{$id}\">";
  575. echo Enc::html($name);
  576. echo "</label>";
  577. echo "</div>";
  578. }
  579.  
  580. echo "</div>";
  581. echo "</fieldset>";
  582. echo "</div>";
  583.  
  584. echo "</div>\n";
  585. echo "</div>\n";
  586.  
  587. if ($error) echo "{$error}";
  588.  
  589. }
  590.  
  591.  
  592. /**
  593.   * Renders an interface for editing attributes
  594.   * Uses a multiedit
  595.   **/
  596. public static function attrEditor($current_attrs)
  597. {
  598. $attrs = Register::getPageattrs();
  599. asort($attrs);
  600.  
  601. // Load any required needs for all registered attrs
  602. $classes = array();
  603. foreach ($attrs as $val) {
  604. $classes[$val[1]] = $val[1];
  605. }
  606. foreach ($classes as $class_name) {
  607. $inst = new $class_name;
  608. if ($inst instanceof AttrEditor) $inst->needs();
  609. }
  610. ?>
  611.  
  612. <div id="multiedit-attrs">
  613. <div class="field-element field-element--dropdown field-element--white">
  614. <div class="field-input">
  615. <select name="m_name" id="custom-attribute" class="dropdown">
  616. <option value="">Select a custom attribute</option>
  617. <?php
  618. foreach ($attrs as $key => $val) {
  619. $val[0] = Enc::html($val[0]);
  620. echo "<option value=\"{$key}\">{$val[0]}</option>";
  621. }
  622. ?>
  623. </select>
  624. </div>
  625. </div>
  626.  
  627. <div class="value">
  628. <input type="hidden" name="m_value" value="">
  629. </div>
  630.  
  631. </div>
  632.  
  633. <script>
  634. function attribute_editor($div, data, idx) {
  635. $div.find('select#custom-attribute').change(function() {
  636. if ($(this).val() === '') {
  637. $div.find('.value').html('<input type="hidden" name="multiedit_attrs[' + idx + '][value]" value="">');
  638. return;
  639. }
  640. var val = $div.find('.value [name^="multiedit_attrs"]').val();
  641. $.post(SITE + 'admin_ajax/attr_editor', {val:val, attr_name:$(this).val()}, function(data) {
  642. var $outer = $div.find('.value');
  643.  
  644. $outer.html(data.html);
  645. $outer.find('[name=value]').attr('name', 'multiedit_attrs[' + idx + '][value]');
  646. if (data.js !== '') {
  647. (function($outer,script){ eval(script); })($outer, data.js);
  648. }
  649. }, 'json');
  650. });
  651. if (typeof(data) !== 'undefined') {
  652. $div.find('.dropdown').change();
  653. }
  654. }
  655. </script>
  656.  
  657. <?php
  658. MultiEdit::setPostAddJavaScriptFunc('attribute_editor');
  659. MultiEdit::itemName('Attribute');
  660. MultiEdit::display('attrs', $current_attrs);
  661. }
  662.  
  663.  
  664. /**
  665.   * Return HTML for the top nav tabs
  666.   *
  667.   * @param string $selected_controller
  668.   * @return string HTML
  669.   */
  670. public static function topNav($selected_controller)
  671. {
  672. if (!AdminAuth::isLoggedIn()) return;
  673.  
  674. echo '<ul class="-clearfix">';
  675.  
  676. if (AdminPerms::controllerAccess('page', 'contents')) {
  677. $dashboard_url = Enc::html(Kohana::config('sprout.admin_intro'));
  678. if ($selected_controller == '_dashboard') {
  679. echo '<li class="home depth-1 on"><a href="', $dashboard_url, '">Home</a></li>';
  680. } else {
  681. echo '<li class="home depth-1"><a href="', $dashboard_url, '">Home</a></li>';
  682. }
  683.  
  684. if ($selected_controller == 'page') {
  685. echo '<li class="depth-1 on"><a href="admin/intro/page">Pages</a></li>';
  686. } else {
  687. echo '<li class="depth-1"><a href="admin/intro/page">Pages</a></li>';
  688. }
  689. }
  690.  
  691. if (AdminPerms::controllerAccess('file', 'contents')) {
  692. if ($selected_controller == 'file') {
  693. echo '<li class="depth-1 on"><a href="admin/intro/file">Media</a></li>';
  694. } else {
  695. echo '<li class="depth-1"><a href="admin/intro/file">Media</a></li>';
  696. }
  697. }
  698.  
  699. if (Register::hasFeature('users') and AdminPerms::controllerAccess('user', 'contents')) {
  700. if ($selected_controller == 'user') {
  701. echo '<li class="depth-1 on"><a href="admin/intro/user">Users</a></li>';
  702. } else {
  703. echo '<li class="depth-1"><a href="admin/intro/user">Users</a></li>';
  704. }
  705. }
  706.  
  707. $tiles = Register::getAdminTiles();
  708. $tiles = AdminPerms::filterAdminTiles($tiles);
  709.  
  710. if (count($tiles)) {
  711. $on = false;
  712. foreach ($tiles as $tile) {
  713. foreach ($tile['controllers'] as $ctlr => $name) {
  714. if ($ctlr == $selected_controller) {
  715. $on = true;
  716. break;
  717. }
  718. }
  719. }
  720.  
  721. if ($on) {
  722. echo '<li class="depth-1 has-sub-nav on">';
  723. } else {
  724. echo '<li class="depth-1 has-sub-nav">';
  725. }
  726. echo '<input type="checkbox" id="open-sub-nav-1" class="giant-popup-checkbox -vis-hidden">';
  727. echo '<label for="open-sub-nav-1" class="giant-popup-link">Modules</label>';
  728. echo '<div class="giant-popup-outer">';
  729. echo '<div class="giant-popup-wrapper">';
  730. echo '<div class="giant-popup-inner">';
  731. echo '<label for="open-sub-nav-1" class="giant-popup-close-button icon-before icon-close">Close</label>';
  732. echo '<h2 class="giant-popup-title">Modules</h2>';
  733. echo '<ul class="sub-nav giant-popup-content columns -clearfix">';
  734.  
  735. $index = 0;
  736. ksort($tiles, SORT_NATURAL);
  737. foreach ($tiles as $tile) {
  738. if ((++$index) % 4 == 0) {
  739. echo '<li class="giant-popup-item column column-3 column-last">';
  740. } else {
  741. echo '<li class="giant-popup-item column column-3">';
  742. }
  743. echo '<div class="giant-popup-item-inner">';
  744. echo '<div class="giant-popup-item-title-wrapper">';
  745. echo '<div class="giant-popup-item-title-icon icon-before icon-', Enc::html($tile['icon']), '"></div>';
  746. echo '<h3 class="giant-popup-item-title">', Enc::html($tile['name']), '</h3>';
  747. echo '</div>';
  748. echo '<div class="giant-popup-item-content -clearfix">';
  749. echo '<div class="giant-popup-item-links">';
  750. echo '<ul class="list-style-1">';
  751. foreach ($tile['controllers'] as $ctlr => $name) {
  752. echo '<li><a href="admin/intro/', Enc::html($ctlr), '">', Enc::html($name), '</a></li>';
  753. }
  754. echo '</ul>';
  755. echo '</div>';
  756. echo '</div>';
  757. echo '</div>';
  758. echo '</li>';
  759. }
  760.  
  761. echo '</ul>';
  762. echo '</div>';
  763. echo '</div>';
  764. echo '</div>';
  765. echo '</li>';
  766. }
  767.  
  768. echo '</ul>';
  769. }
  770.  
  771.  
  772. /**
  773.   * When in the admin, return the slug of the controller being used, e.g. 'page' or 'blog_post'
  774.   *
  775.   * @return string
  776.   */
  777. public static function getControllerSlug()
  778. {
  779. if (isset(Router::$arguments[0])) {
  780. return Router::$arguments[0];
  781. } else {
  782. return null;
  783. }
  784. }
  785.  
  786.  
  787. /**
  788.   * When in the admin, return the record id being added or edited.
  789.   * If it's not an add or an edit, returns null.
  790.   * If used outside of the admin, behaviour is undefined
  791.   **/
  792. public static function getRecordId()
  793. {
  794. if (Router::$method === 'edit' or Router::$method === 'delete') {
  795. return Router::$arguments[1];
  796. }
  797. return null;
  798. }
  799.  
  800.  
  801. /**
  802.   * Has a given JavaScript tour been completed?
  803.   *
  804.   * @param string $tour_name Internal name for the tour, e.g. "page_edit"
  805.   * @return bool True if it's been completed, false if it hasn't
  806.   */
  807. public static function isTourCompleted($tour_name)
  808. {
  809. $op_id = AdminAuth::getLocalId();
  810. if ($op_id === 0) return true;
  811.  
  812. $q = "SELECT completed_tours FROM ~operators WHERE id = ?";
  813. $op = Pdb::query($q, [$op_id], 'row');
  814. $completed_tours = explode(",", $op['completed_tours']);
  815.  
  816. return in_array($tour_name, $completed_tours);
  817. }
  818.  
  819.  
  820. /**
  821.   * Set a given JavaScript tour as being "completed", preventing it from being shown again.
  822.   *
  823.   * @param string $tour_name Internal name for the tour, e.g. "page_edit"
  824.   */
  825. public static function setTourCompleted($tour_name)
  826. {
  827. $op_id = AdminAuth::getLocalId();
  828. if ($op_id === 0) return;
  829.  
  830. $q = "UPDATE ~operators SET completed_tours = TRIM(',' FROM CONCAT(completed_tours, ',', ?)) WHERE id = ?";
  831. Pdb::query($q, [$tour_name, $op_id], 'null');
  832. }
  833.  
  834.  
  835. /**
  836.   * Is admin locks enabled?
  837.   *
  838.   * @return boolean True if they are, false if they aren't
  839.   **/
  840. public static function locksEnabled()
  841. {
  842. $conf = Kohana::config('sprout.admin_locks');
  843. if ($conf === null) return true;
  844. return (bool) $conf;
  845. }
  846.  
  847.  
  848. /**
  849.   * Gets the lock details for a given record
  850.   * @param string $ctlr Controller name
  851.   * @param int $record_id DB record ID
  852.   * @return array If locked; has keys 'id', 'operator_name', 'lock_key', 'date_modified'
  853.   * @return null If not locked
  854.   **/
  855. public static function getLock($ctlr, $record_id)
  856. {
  857. $record_id = (int) $record_id;
  858.  
  859. $q = "SELECT id, operator_name, lock_key, date_modified
  860. FROM ~admin_locks
  861. WHERE ctlr = ? AND record_id = ?
  862. LIMIT 1";
  863. try {
  864. $row = Pdb::q($q, [$ctlr, $record_id], 'row');
  865. } catch (QueryException $ex) {
  866. return null;
  867. }
  868.  
  869. // If it's too old (10 mins), force it to unlock
  870. if (strtotime($row['date_modified']) + Constants::LOCK_AGE < time()) {
  871. Admin::forceUnlock($row['id']);
  872. return null;
  873. }
  874.  
  875. return $row;
  876. }
  877.  
  878.  
  879. /**
  880.   * Locks the given record for the current user
  881.   * @param string $ctlr The controller responsible for the record
  882.   * @param int $record_id The record ID
  883.   * @throws Exception If the lock fails to acquire
  884.   * @return int Lock id
  885.   */
  886. public static function lock($ctlr, $record_id)
  887. {
  888. $op = AdminAuth::getDetails();
  889.  
  890. if (! $_SESSION['admin']['lock_key']) {
  891. $_SESSION['admin']['lock_key'] = Admin::createLockKey();
  892. }
  893.  
  894. $update_data = [];
  895. $update_data['ctlr'] = $ctlr;
  896. $update_data['record_id'] = (int) $record_id;
  897. $update_data['operator_name'] = $op['name'];
  898. $update_data['ip_address'] = bin2hex(inet_pton(trim(Request::userIp())));
  899. $update_data['user_agent'] = (string) $_SERVER['HTTP_USER_AGENT'];
  900. $update_data['lock_key'] = $_SESSION['admin']['lock_key'];
  901. $update_data['date_added'] = Pdb::now();
  902. $update_data['date_modified'] = Pdb::now();
  903.  
  904. try {
  905. $lock_id = Pdb::insert('admin_locks', $update_data);
  906. } catch (Exception $ex) {
  907. throw new Exception('Failed to acquire edit lock.');
  908. }
  909.  
  910. return $lock_id;
  911. }
  912.  
  913.  
  914. /**
  915.   * Updates the timestamp for a given lock record to prevent it from timing out
  916.   * @param int $lock_id
  917.   * @return void
  918.   */
  919. public static function pingLock($lock_id)
  920. {
  921. $lock_id = (int) $lock_id;
  922.  
  923. $update_data = ['date_modified' => Pdb::now()];
  924. Pdb::update('admin_locks', $update_data, ['id' => $lock_id]);
  925. }
  926.  
  927.  
  928. /**
  929.   * Nuke a lock
  930.   **/
  931. public static function forceUnlock($lock_id)
  932. {
  933. Pdb::delete('admin_locks', ['id' => (int) $lock_id]);
  934. }
  935.  
  936.  
  937. /**
  938.   * Removes locks for the current user
  939.   *
  940.   * @param string $ctlr Will refine lock removal by controller
  941.   * @param int $record_id Will refine lock removal by record id
  942.   **/
  943. public static function unlock($ctlr = null, $record_id = null)
  944. {
  945. if (empty($_SESSION['admin']['lock_key'])) {
  946. return;
  947. }
  948.  
  949. $where = ['lock_key' => $_SESSION['admin']['lock_key']];
  950. if ($ctlr) {
  951. $where['ctlr'] = $ctlr;
  952. if ($record_id) $where['record_id'] = $record_id;
  953. }
  954.  
  955. Pdb::delete('admin_locks', $where);
  956. }
  957.  
  958.  
  959. /**
  960.   * Return a unique key for identifying a session.
  961.   * This will be constant even if the session id changes, although it's nuked if you log out.
  962.   **/
  963. public static function createLockKey()
  964. {
  965. return sha1(Request::userIp() . $_SERVER['HTTP_USER_AGENT'] . time());
  966. }
  967.  
  968.  
  969. /**
  970.   * Remove all locks which are older than the allowed lock time.
  971.   **/
  972. public static function clearOldLocks()
  973. {
  974. $q = "SELECT id, date_modified FROM ~admin_locks";
  975. $res = Pdb::query($q, [], 'pdo');
  976.  
  977. foreach ($res as $row) {
  978. if (strtotime($row['date_modified']) + Constants::LOCK_AGE < time()) {
  979. Admin::forceUnlock($row['id']);
  980. }
  981. }
  982.  
  983. $res->closeCursor();
  984. }
  985.  
  986.  
  987. /**
  988.   * Gets an instance of a managed admin controller
  989.   *
  990.   * @param string $class_name A class name, or shorthand identifier
  991.   * e.g. 'Sprout\Controllers\Admin\AwesomeAdminController' or 'awesome'
  992.   * @return ManagedAdminController
  993.   * @throws Exception If the class is unknown
  994.   */
  995. public static function getController($class_name)
  996. {
  997. if (strpos($class_name, '\\') !== false) {
  998. $full_name = $class_name;
  999. } else {
  1000. // Use registered shorthand names for classes in modules
  1001. try {
  1002. $full_name = Register::getAdminController($class_name);
  1003.  
  1004. // Auto-determine names of Sprout internal controllers
  1005. } catch (Exception $ex) {
  1006. $full_name = ucfirst(Inflector::camelize($class_name));
  1007. if (substr($full_name, -15) != 'AdminController') {
  1008. $full_name .= 'AdminController';
  1009. }
  1010. $full_name = 'Sprout\\Controllers\\Admin\\' . $full_name;
  1011. }
  1012. }
  1013.  
  1014. $inst = Sprout::instance(
  1015. $full_name,
  1016. 'Sprout\\Controllers\\Admin\\ManagedAdminController'
  1017. );
  1018.  
  1019. return $inst;
  1020. }
  1021.  
  1022.  
  1023. /**
  1024.   * For a given URL, ensure it's absolute.
  1025.   * If it's not absolute, the current admin abs-root is prepended
  1026.   *
  1027.   * @param string $url Either relative or absolute
  1028.   * @return string Absolute URL
  1029.   */
  1030. public static function ensureUrlAbsolute($url)
  1031. {
  1032. if (preg_match('!^https?://!', $url)) {
  1033. return $url;
  1034. } else {
  1035. return Subsites::getAbsRoot($_SESSION['admin']['active_subsite']) . ltrim($url, '/');
  1036. }
  1037. }
  1038.  
  1039. }
  1040.