<?php
/*
* Copyright (C) 2017 Karmabunny Pty Ltd.
*
* This file is a part of SproutCMS.
*
* SproutCMS is free software: you can redistribute it and/or modify it under the terms
* of the GNU General Public License as published by the Free Software Foundation, either
* version 2 of the License, or (at your option) any later version.
*
* For more information, visit <http://getsproutcms.com>.
*/
namespace Sprout\Helpers;
use Exception;
use Kohana;
use karmabunny\pdb\Exceptions\QueryException;
/**
* Sorter for widgets
**/
function _widgetSort($a, $b) {
$a = Widgets::instantiate($a);
$b = Widgets::instantiate($b);
return strcmp($a->getFriendlyName(), $b->getFriendlyName()); }
/**
* Useful functions for the admin
**/
class Admin
{
static
private $error_msgs = array( 'required' => 'This field is required',
'length\[0,([0-9]+)\]' => 'Too long, max length is $1 characters',
'length\[([0-9]+),([0-9]+)\]' => 'Incorrect length, must be between $1 and $2 characters',
'length\[([0-9]+)\]' => 'Incorrect length, must be exactly $1 characters',
'alphaDash' => 'Field can only contain letters, numbers, underscores (_) and dashes (-)',
'emailUnique' => 'Email address is not unique',
'email' => 'Email address is not valid',
'matches\[password.*' => 'Password fields do not match',
'numMin\[([-0-9]+)\]' => 'Number must be at least $1',
'numMax\[([-0-9]+)\]' => 'Number must be at most $1',
'numBetween\[([-0-9]+),([-0-9]+)\]' => 'Number must be between $1 and $2 inclusive',
'check_redirect' => 'URL must begin with http:// (external url) or / (internal url)',
'exclusive\[(.*)\]' => 'These fields cannot be used together, only one of them may be used',
'check_controller_entrance\[.*\]' => 'Only one page may reference any given module',
'raw\[(.+)\]' => '$1',
'.+' => 'Incorrect value provided',
);
private static $cat_tablename;
private static $cat_singlecat = false;
/**
* Finds an appropriate error message for the specified error code
**/
public static function lookupErrMsg($err)
{
$message = null;
if ($err instanceof AdminError) {
return $err->getMessage();
}
foreach (self::$error_msgs as $search => $replace) {
$count = 0;
$message = preg_replace('/^'.$search.'$/', $replace, $err, 1, $count); if ($count > 0) break;
}
if (! $message) $message = 'Invalid value provided.';
return $message;
}
/**
* Shows a per-field error message
*
* @param string $field_name The name of the field to show the error message for.
**/
public static function fieldError($field_name, $scope = 'admin')
{
if (empty($_SESSION[$scope]['field_errors'][$field_name])) return;
$error = self::lookupErrMsg($_SESSION[$scope]['field_errors'][$field_name]);
$class = Enc::id('field-error-' . $field_name);
$out = "<span class=\"field-error {$class}\">";
$out .= '</span>';
return $out;
}
/**
* Clears all pre-field error messages
**/
public static function clearFieldErrors($scope = 'admin')
{
unset ($_SESSION[$scope]['field_errors']); }
/**
* Shows a UI for the list of widgets, for editing
*
* @param string $field_name Name of the field to store the final value when the form is submitted
* @param WidgetArea $area The area that is being edited
* @param array $curr_widgets A list of the widgets currently being used, in order, as db rows with keys:
* type string Class name, e.g. 'RichText'
* settings string Opaque JSON string
* conditions string Opaque JSON string
* active int 1 for active, 0 for inactive
* heading string HTML H2 rendered front-end within widget
* @param boolean $enable_all Toggle whether all the widgets are enabled by default (defaults to true)
**/
public static function widgetList($field_name, WidgetArea $area, $curr_widgets, $enable_all = true)
{
if ($curr_widgets == null) {
$curr_widgets = [];
}
$widget_list_id = 'wl-' . Enc::id($field_name);
echo '<script type="text/javascript">';
echo "$(document).ready(function() {\n";
echo " var list = new widget_list('", Enc::js($field_name), "');\n";
foreach ($curr_widgets as $widget) {
$inst = Widgets::instantiate($widget['type']);
$eng_name = Enc::js($inst->getFriendlyName());
if (!$enable_all) $widget['active'] = 0;
$add_opts = [
'type' => $widget['type'],
'label' => $eng_name,
'settings' => $widget['settings'],
'conditions' => $widget['conditions'],
'active' => (bool)$widget['active'],
'heading' => @$widget['heading'],
'template' => @$widget['template'],
];
echo " list.add_widget(", json_encode($add_opts), ");\n"; }
echo "\n";
echo " $('#{$widget_list_id}').bind('add-widget', function(e, widget_name, english_name) {\n";
echo " list.add_widget({ type: widget_name, label: english_name, settings: '', active: true });\n";
echo " return false;\n";
echo " });\n";
echo "});\n";
echo '</script>';
if ($enable_all) {
echo "<div id=\"{$widget_list_id}\" class=\"widget-list\">\n";
} else {
echo "<div id=\"{$widget_list_id}\" class=\"widget-list all-collapsed\">\n";
}
echo '<div class="widgets-sel"></div>';
echo '<div class="widgets-empty">This area does not have any content blocks. Click the button below to add a content block:</div>';
echo '<div class="content-block-button-wrap">';
echo "<input type=\"checkbox\" id=\"{$widget_list_id}--add-btn\" class=\"giant-popup-checkbox -vis-hidden\">";
if ($enable_all) {
$add_tooltip = 'Add a block of content.';
if ($field_name == "embedded") {
$add_tooltip = 'Add a block of content to the page.';
} else if ($field_name == "sidebar") {
$add_tooltip = 'Add a block of content to the sidebar.';
} else if ($field_name == "email") {
$add_tooltip = 'Add a block of content to the email.';
}
echo "<label for=\"{$widget_list_id}--add-btn\" class=\"button button-green button-regular button-block add-content-block-button\">Add content block";
echo '<span class="tooltip-wrapper">';
echo '<span class="tooltip-trigger">';
echo '<span class="tooltip-trigger-icon icon-before icon-live_help"></span>';
echo '</span>';
echo '<span class="tooltip-content">';
echo Enc::html($add_tooltip);
echo '</span>';
echo '</span>';
echo "</label>";
}
echo '<div class="giant-popup-outer">';
echo '<div class="giant-popup-wrapper">';
echo '<div class="giant-popup-inner">';
echo "<label for=\"{$widget_list_id}--add-btn\" class=\"giant-popup-close-button icon-before icon-close\">Close</label>";
echo '<h2 class="giant-popup-title">Content blocks</h2>';
echo '<ul class="giant-popup-content columns -clearfix">';
$tiles = Register::getWidgetTiles($area->getName());
// For the "embedded" area, sort the 'Content' tile to the start and the 'Advanced' tile to the end
if ($area->getName() === 'embedded') {
$content = $tiles['Text blocks'];
$collections = $tiles['Collections'];
$advanced = $tiles['Advanced'];
unset($tiles['Text blocks'], $tiles['Collections'], $tiles['Advanced']); }
$index = 0;
foreach ($tiles as $tile) {
if ((++$index) % 4 == 0) {
echo '<li class="giant-popup-item column column-3 column-last">';
} else {
echo '<li class="giant-popup-item column column-3">';
}
echo '<div class="giant-popup-item-inner">';
echo '<div class="giant-popup-item-title-wrapper">';
echo '<div class="giant-popup-item-title-icon icon-before icon-', Enc::html($tile['icon']), '"></div>';
echo '<h3 class="giant-popup-item-title">', Enc::html($tile['name']), '</h3>';
echo '</div>';
echo '<div class="giant-popup-item-content -clearfix">';
echo '<div class="giant-popup-item-links">';
echo '<ul class="list-style-1">';
foreach ($tile['widgets'] as $widg => $name) {
$inst = Widgets::instantiate($widg);
$desc = $inst->getFriendlyDesc();
echo '<li><a class="widget-list-new-widget" href="javascript:;" data-name="', Enc::html($widg), '" title="' . Enc::html($desc) . '">', Enc::html($name), '</a></li>';
}
echo '</ul>';
echo '</div>';
echo '</div>';
echo '</div>';
echo '</li>';
}
echo '</ul>';
echo '</div>';
echo '</div>';
echo '</div>';
echo "</div>\n";
echo "</div>\n";
}
/**
* Deprecated wrapper around {@see Fb::pageDropdown}
*
* @param string $field_name The name of the field
* @param int $selected The id of the page to select
* @param int $exclude The id of the page to exclude from the list
* @param int $subsite_id The subsite of pages to show. Defaults to the current admin subsite
* @param string $top_text Text for the top (id 0) item
**/
public static function pageDropdown($field_name, $selected = null, $exclude = null, $subsite_id = null, $top_text = 'None (top level page)')
{
echo Fb::pageDropdown($field_name, [], [
'exclude' => [$exclude],
]);
}
/**
* Render a tree of nodes for the navigation area of the admin
* Used by the pages module
*
* @param Treenode $node The node to render
* @param array $actions Additional links to show in the cog icon menu; keys: url, class, name
* @param int $depth How deep in the tree the rendering is, starts at 1 for the top-level
* @return void Outputs HTML directly
**/
public static
function navigationTreeNode
($node, array $actions, $depth = 1) {
$admin_perms = AdminPerms::checkPermissionsTree('pages', $node['id']);
$name = Enc::html(Text::limitChars($node['name'], 35, '...'));
$class = "node depth{$depth}";
$class .= ($admin_perms ? ' allow-access' : ' no-access');
if (count($node->children) > 0) { $class .= ' has-children collapsed';
}
if (self::getControllerSlug() === 'page') {
if (self::getRecordId() == $node['id']) {
$class .= ' active-node';
} else if ($node->findNodeValue('id', self::getRecordId())) {
$class .= ' active-parent-node';
}
}
// Render node
echo "<li class=\"{$class}\" data-id=\"{$node['id']}\">";
echo "<div>";
$action = reset($actions); echo "<a class=\"node-link\" href=\"{$url}\">{$name}</a>";
if (count($node->children) > 0) { echo "<button class=\"tree-list-expand-button icon-before icon-keyboard_arrow_right\" type=\"button\">Expand</button>";
}
echo "<button class=\"tree-list-settings-button icon-before icon-settings\" type=\"button\">Settings</button>";
echo "<div class=\"tree-list-settings-dropdown dropdown-box\">";
echo "<ul class=\"tree-list-settings-dropdown-list list-style-2\">";
foreach ($actions as $action) {
$class = trim('tree-list-settings-dropdown-list-item ' . @$action['class']); echo "<li class=\"{$class}\"><a href=\"", Enc::html($url), "\">", Enc::html($action['name']), "</a></li>";
}
echo "</ul>";
echo "</div>";
echo "</div>";
// Render children
if (count($node->children) > 0 and
$admin_perms) { echo "<ul class=\"node-children-list\">";
$depth++;
foreach ($node->children as $child) {
self::navigationTreeNode($child, $actions, $depth);
}
echo "</ul>";
}
echo "</li>";
}
/**
* For a set of multi-edit fields, loads the data into a much more usable array
*
* Input:
* [phone] = ('1234 1234', '4321 4321')
* [type] = ('Mobile', 'Home')
*
* Output:
* [1] = ('phone' => '1234 1234', 'type' => 'Mobile')
* [2] = ('phone' => '4321 4321', 'type' => 'Home')
*
* @param array $dataset The original data to use
* @param array $field_names An array of the names of the fields to use
**/
public static function multieditBuild($dataset, $field_names)
{
foreach ($dataset[$primary] as $idx => $value) {
$row[$primary] = $value;
foreach ($field_names as $name) {
$row[$name] = $dataset[$name][$idx];
}
$has_data = false;
foreach ($row as $val) {
if ($val != '') {
$has_data = true;
break;
}
}
if ($has_data) $records[] = $row;
}
array_pop($records); // the last one is a dummy
return $records;
}
/**
* Outputs a list of checkboxes.
*
* If the $field parameter is provided, multiple checkboxes with the same field name will be rendered.
* $data should be a key-value pair, with the keys being the value of the checkbox, and the value being the label.
* $selected should be an array of selected checkbox ids.
*
* If the $field parameter is omitted (null), multiple checkboxes widh different field names will be rendered.
* The checkboxes will be binary checkboxes with a value of 1.
* $data should be a key-value pair, with the keys being the field name, and the value being the label.
* $selected should be a key-value pair, with the keys being the field name, and the value being 1 or 0.
**/
public static function checkboxList($field, $data, $selected)
{
echo "<div class=\"checkbox-list-wrapper\">\n";
echo "<div class=\"checkbox-list\">\n";
$common_field = false;
if ($field != null) {
$common_field = true;
if (! preg_match('/\[\]$/', $field)) $field .= '[]'; }
echo "<div class=\"field-element field-element--white field-element--checkbox'\">";
echo "<fieldset class=\"fieldset--checkboxboollist\">";
echo "<legend class=\"fieldset__legend\">Categories</legend>";
echo "<div class=\"field-element__input-set\">";
$val = '';
foreach ($data as $id => $name) {
if ($common_field) {
$val = $id;
$html_id = Enc::id($field . $val);
} else {
$field = $id;
$html_id = Enc::id($field . $val);
$val = 1;
$checked = (bool) @$selected[$id];
}
echo "<div class=\"fieldset-input\">";
if ($checked) {
echo '<input type="checkbox" name="', Enc::html($field), '" value="', Enc::html($val), '" id="', $html_id, '" checked>';
} else {
echo '<input type="checkbox" name="', Enc::html($field), '" value="', Enc::html($val), '" id="', $html_id, '">';
}
echo "<label for=\"{$html_id}\">";
echo Enc::html($name);
echo "</label>";
echo "</div>";
}
echo "</div>";
echo "</fieldset>";
echo "</div>";
echo "</div>\n";
echo "</div>\n";
}
public static function setCategoryTablename($name)
{
self::$cat_tablename = $name;
}
public static function setCategorySinglecat($value)
{
self::$cat_singlecat = $value;
}
/**
* Outputs an interface for selecting multiple categories
*
* @param $field The field name
* @param $data An array of key-value pairs, with the keys being the id of the category, and the value being the name.
* @param $selected An array of selected category ids.
**/
public static function categorySelection($field, $data, $selected)
{
if (! self::$cat_tablename) {
echo '<p>ERROR: <code>Admin::setCategoryTablename()</code> has not been called.</p>';
return;
}
if (! preg_match('/\[\]$/', $field)) $field .= '[]';
if (self::$cat_singlecat and
@count($selected) > 1) { }
$type = (self::$cat_singlecat ? 'radio' : 'checkbox');
echo "<div class=\"onthefly-catadd-wrapper\">\n";
echo "<div class=\"onthefly-catadd-table-wrapper\">\n";
echo "<div class=\"checkbox-list-wrapper\">\n";
echo "<div class=\"checkbox-list category-selection\">\n";
echo "<div class=\"field-element field-element--{$type}'\">";
echo "<fieldset class=\"fieldset--{$type}boollist\">";
echo "<legend class=\"fieldset__legend\">Categories</legend>";
echo "<div class=\"field-element__input-set\">";
foreach ($data as $id => $name) {
$html_id = Enc::id($field . $id);
echo "<div class=\"fieldset-input\">";
if ($checked) {
echo '<input type="', $type, '" name="', Enc::html($field), '" value="', Enc::html($id), '" id="', $html_id, '" checked>';
} else {
echo '<input type="', $type, '" name="', Enc::html($field), '" value="', Enc::html($id), '" id="', $html_id, '">';
}
echo "<label for=\"{$html_id}\">";
echo Enc::html($name);
echo "</label>";
echo "</div>";
}
echo "</div>";
echo "</fieldset>";
echo "</div>";
echo "</div>\n";
echo "</div>\n";
}
echo "</div>\n";
// Show category quickadd, if allowed
$controller = Inflector::singular(Category::tableCat2main(self::$cat_tablename));
if (AdminPerms::controllerAccess($controller, 'categories')) {
echo '<div class="onthefly-catadd -clearfix" data-tablename="' . self::$cat_tablename . '" data-singlecat="' . ((int)self::$cat_singlecat) . '" data-field="' . $field . '">';
echo Csrf::token();
echo '<div class="field-element field-element--white field-element--text field-element--small">';
echo '<div class="field-label"><label for="onthefly-catadd">Add a new category</label></div>';
echo '<div class="field-input"><input type="text" id="onthefly-catadd" spellcheck="true" class="textbox" placeholder="Category name"></div>';
echo "</div>\n";
echo '<div class="field-element field-element--white field-element--button field-element--small">';
echo '<button id="onthefly-catadd-button" type="button" class="button button-green button-small onthefly-catadd icon-after icon-add">Add</button>';
echo "</div>\n";
echo "</div>\n";
}
echo "</div>\n";
}
/**
* Outputs a list of radiobuttons, which will all use the same field name
*
* @param string $field The field name.
* @param array $data The data. Key is the radiobutton value, Value is used in the label for the radiobutton.
* @param array $selected The selected item
**/
public static function radioList($field, $data, $selected)
{
$field_id = Enc::id($field);
$error = self::fieldError($field);
echo "<div class=\"checkbox-list-wrapper\">\n";
echo "<div class=\"checkbox-list\">\n";
echo "<div class=\"field-element field-element--white field-element--radio'\">";
echo "<fieldset class=\"fieldset--checkboxboollist\">";
echo "<legend class=\"fieldset__legend\">Categories</legend>";
echo "<div class=\"field-element__input-set\">";
foreach ($data as $id => $name) {
echo "<div class=\"fieldset-input\">";
if ($id == $selected) {
echo '<input type="radio" name="', Enc::html($field), '" value="', Enc::html($id), '" id="', $field_id, $id, '" checked>';
} else {
echo '<input type="radio" name="', Enc::html($field), '" value="', Enc::html($id), '" id="', $field_id, $id, '">';
}
echo "<label for=\"{$field_id}{$id}\">";
echo Enc::html($name);
echo "</label>";
echo "</div>";
}
echo "</div>";
echo "</fieldset>";
echo "</div>";
echo "</div>\n";
echo "</div>\n";
if ($error) echo "{$error}";
}
/**
* Renders an interface for editing attributes
* Uses a multiedit
**/
public static function attrEditor($current_attrs)
{
$attrs = Register::getPageattrs();
// Load any required needs for all registered attrs
foreach ($attrs as $val) {
$classes[$val[1]] = $val[1];
}
foreach ($classes as $class_name) {
$inst = new $class_name;
if ($inst instanceof AttrEditor) $inst->needs();
}
?>
<div id="multiedit-attrs">
<div class="field-element field-element--dropdown field-element--white">
<div class="field-input">
<select name="m_name" id="custom-attribute" class="dropdown">
<option value="">Select a custom attribute</option>
<?php
foreach ($attrs as $key => $val) {
$val[0] = Enc::html($val[0]);
echo "<option value=\"{$key}\">{$val[0]}</option>";
}
?>
</select>
</div>
</div>
<div class="value">
<input type="hidden" name="m_value" value="">
</div>
</div>
<script>
function attribute_editor($div, data, idx) {
$div.find('select#custom-attribute').change(function() {
if ($(this).val() === '') {
$div.find('.value').html('<input type="hidden" name="multiedit_attrs[' + idx + '][value]" value="">');
return;
}
var val = $div.find('.value [name^="multiedit_attrs"]').val();
$.post(SITE + 'admin_ajax/attr_editor', {val:val, attr_name:$(this).val()}, function(data) {
var $outer = $div.find('.value');
$outer.html(data.html);
$outer.find('[name=value]').attr('name', 'multiedit_attrs[' + idx + '][value]');
if (data.js !== '') {
(function($outer,script){ eval(script); })($outer, data.js);
}
}, 'json');
});
if (typeof(data) !== 'undefined') {
$div.find('.dropdown').change();
}
}
</script>
<?php
MultiEdit::setPostAddJavaScriptFunc('attribute_editor');
MultiEdit::itemName('Attribute');
MultiEdit::display('attrs', $current_attrs);
}
/**
* Return HTML for the top nav tabs
*
* @param string $selected_controller
* @return string HTML
*/
public static function topNav($selected_controller)
{
if (!AdminAuth::isLoggedIn()) return;
echo '<ul class="-clearfix">';
if (AdminPerms::controllerAccess('page', 'contents')) {
$dashboard_url = Enc::html(Kohana::config('sprout.admin_intro'));
if ($selected_controller == '_dashboard') {
echo '<li class="home depth-1 on"><a href="', $dashboard_url, '">Home</a></li>';
} else {
echo '<li class="home depth-1"><a href="', $dashboard_url, '">Home</a></li>';
}
if ($selected_controller == 'page') {
echo '<li class="depth-1 on"><a href="admin/intro/page">Pages</a></li>';
} else {
echo '<li class="depth-1"><a href="admin/intro/page">Pages</a></li>';
}
}
if (AdminPerms::controllerAccess('file', 'contents')) {
if ($selected_controller == 'file') {
echo '<li class="depth-1 on"><a href="admin/intro/file">Media</a></li>';
} else {
echo '<li class="depth-1"><a href="admin/intro/file">Media</a></li>';
}
}
if (Register::hasFeature('users') and AdminPerms::controllerAccess('user', 'contents')) {
if ($selected_controller == 'user') {
echo '<li class="depth-1 on"><a href="admin/intro/user">Users</a></li>';
} else {
echo '<li class="depth-1"><a href="admin/intro/user">Users</a></li>';
}
}
$tiles = Register::getAdminTiles();
$tiles = AdminPerms::filterAdminTiles($tiles);
$on = false;
foreach ($tiles as $tile) {
foreach ($tile['controllers'] as $ctlr => $name) {
if ($ctlr == $selected_controller) {
$on = true;
break;
}
}
}
if ($on) {
echo '<li class="depth-1 has-sub-nav on">';
} else {
echo '<li class="depth-1 has-sub-nav">';
}
echo '<input type="checkbox" id="open-sub-nav-1" class="giant-popup-checkbox -vis-hidden">';
echo '<label for="open-sub-nav-1" class="giant-popup-link">Modules</label>';
echo '<div class="giant-popup-outer">';
echo '<div class="giant-popup-wrapper">';
echo '<div class="giant-popup-inner">';
echo '<label for="open-sub-nav-1" class="giant-popup-close-button icon-before icon-close">Close</label>';
echo '<h2 class="giant-popup-title">Modules</h2>';
echo '<ul class="sub-nav giant-popup-content columns -clearfix">';
$index = 0;
ksort($tiles, SORT_NATURAL
); foreach ($tiles as $tile) {
if ((++$index) % 4 == 0) {
echo '<li class="giant-popup-item column column-3 column-last">';
} else {
echo '<li class="giant-popup-item column column-3">';
}
echo '<div class="giant-popup-item-inner">';
echo '<div class="giant-popup-item-title-wrapper">';
echo '<div class="giant-popup-item-title-icon icon-before icon-', Enc::html($tile['icon']), '"></div>';
echo '<h3 class="giant-popup-item-title">', Enc::html($tile['name']), '</h3>';
echo '</div>';
echo '<div class="giant-popup-item-content -clearfix">';
echo '<div class="giant-popup-item-links">';
echo '<ul class="list-style-1">';
foreach ($tile['controllers'] as $ctlr => $name) {
echo '<li><a href="admin/intro/', Enc::html($ctlr), '">', Enc::html($name), '</a></li>';
}
echo '</ul>';
echo '</div>';
echo '</div>';
echo '</div>';
echo '</li>';
}
echo '</ul>';
echo '</div>';
echo '</div>';
echo '</div>';
echo '</li>';
}
echo '</ul>';
}
/**
* When in the admin, return the slug of the controller being used, e.g. 'page' or 'blog_post'
*
* @return string
*/
public static function getControllerSlug()
{
if (isset(Router
::$arguments[0])) { return Router::$arguments[0];
} else {
return null;
}
}
/**
* When in the admin, return the record id being added or edited.
* If it's not an add or an edit, returns null.
* If used outside of the admin, behaviour is undefined
**/
public static function getRecordId()
{
if (Router::$method === 'edit' or Router::$method === 'delete') {
return Router::$arguments[1];
}
return null;
}
/**
* Has a given JavaScript tour been completed?
*
* @param string $tour_name Internal name for the tour, e.g. "page_edit"
* @return bool True if it's been completed, false if it hasn't
*/
public static function isTourCompleted($tour_name)
{
$op_id = AdminAuth::getLocalId();
if ($op_id === 0) return true;
$q = "SELECT completed_tours FROM ~operators WHERE id = ?";
$op = Pdb::query($q, [$op_id], 'row');
$completed_tours = explode(",", $op['completed_tours']);
return in_array($tour_name, $completed_tours); }
/**
* Set a given JavaScript tour as being "completed", preventing it from being shown again.
*
* @param string $tour_name Internal name for the tour, e.g. "page_edit"
*/
public static function setTourCompleted($tour_name)
{
$op_id = AdminAuth::getLocalId();
if ($op_id === 0) return;
$q = "UPDATE ~operators SET completed_tours = TRIM(',' FROM CONCAT(completed_tours, ',', ?)) WHERE id = ?";
Pdb::query($q, [$tour_name, $op_id], 'null');
}
/**
* Is admin locks enabled?
*
* @return boolean True if they are, false if they aren't
**/
public static function locksEnabled()
{
$conf = Kohana::config('sprout.admin_locks');
if ($conf === null) return true;
return (bool) $conf;
}
/**
* Gets the lock details for a given record
* @param string $ctlr Controller name
* @param int $record_id DB record ID
* @return array If locked; has keys 'id', 'operator_name', 'lock_key', 'date_modified'
* @return null If not locked
**/
public static function getLock($ctlr, $record_id)
{
$record_id = (int) $record_id;
$q = "SELECT id, operator_name, lock_key, date_modified
FROM ~admin_locks
WHERE ctlr = ? AND record_id = ?
LIMIT 1";
try {
$row = Pdb::q($q, [$ctlr, $record_id], 'row');
} catch (QueryException $ex) {
return null;
}
// If it's too old (10 mins), force it to unlock
if (strtotime($row['date_modified']) + Constants
::LOCK_AGE < time()) { Admin::forceUnlock($row['id']);
return null;
}
return $row;
}
/**
* Locks the given record for the current user
* @param string $ctlr The controller responsible for the record
* @param int $record_id The record ID
* @throws Exception If the lock fails to acquire
* @return int Lock id
*/
public static function lock($ctlr, $record_id)
{
$op = AdminAuth::getDetails();
if (! $_SESSION['admin']['lock_key']) {
$_SESSION['admin']['lock_key'] = Admin::createLockKey();
}
$update_data = [];
$update_data['ctlr'] = $ctlr;
$update_data['record_id'] = (int) $record_id;
$update_data['operator_name'] = $op['name'];
$update_data['ip_address'] = bin2hex(inet_pton
(trim(Request
::userIp()))); $update_data['user_agent'] = (string) $_SERVER['HTTP_USER_AGENT'];
$update_data['lock_key'] = $_SESSION['admin']['lock_key'];
$update_data['date_added'] = Pdb::now();
$update_data['date_modified'] = Pdb::now();
try {
$lock_id = Pdb::insert('admin_locks', $update_data);
} catch (Exception $ex) {
throw new Exception('Failed to acquire edit lock.');
}
return $lock_id;
}
/**
* Updates the timestamp for a given lock record to prevent it from timing out
* @param int $lock_id
* @return void
*/
public static function pingLock($lock_id)
{
$lock_id = (int) $lock_id;
$update_data = ['date_modified' => Pdb::now()];
Pdb::update('admin_locks', $update_data, ['id' => $lock_id]);
}
/**
* Nuke a lock
**/
public static function forceUnlock($lock_id)
{
Pdb::delete('admin_locks', ['id' => (int) $lock_id]);
}
/**
* Removes locks for the current user
*
* @param string $ctlr Will refine lock removal by controller
* @param int $record_id Will refine lock removal by record id
**/
public static function unlock($ctlr = null, $record_id = null)
{
if (empty($_SESSION['admin']['lock_key'])) { return;
}
$where = ['lock_key' => $_SESSION['admin']['lock_key']];
if ($ctlr) {
$where['ctlr'] = $ctlr;
if ($record_id) $where['record_id'] = $record_id;
}
Pdb::delete('admin_locks', $where);
}
/**
* Return a unique key for identifying a session.
* This will be constant even if the session id changes, although it's nuked if you log out.
**/
public static function createLockKey()
{
return sha1(Request
::userIp() . $_SERVER['HTTP_USER_AGENT'] . time()); }
/**
* Remove all locks which are older than the allowed lock time.
**/
public static function clearOldLocks()
{
$q = "SELECT id, date_modified FROM ~admin_locks";
$res = Pdb::query($q, [], 'pdo');
foreach ($res as $row) {
if (strtotime($row['date_modified']) + Constants
::LOCK_AGE < time()) { Admin::forceUnlock($row['id']);
}
}
$res->closeCursor();
}
/**
* Gets an instance of a managed admin controller
*
* @param string $class_name A class name, or shorthand identifier
* e.g. 'Sprout\Controllers\Admin\AwesomeAdminController' or 'awesome'
* @return ManagedAdminController
* @throws Exception If the class is unknown
*/
public static function getController($class_name)
{
if (strpos($class_name, '\\') !== false) { $full_name = $class_name;
} else {
// Use registered shorthand names for classes in modules
try {
$full_name = Register::getAdminController($class_name);
// Auto-determine names of Sprout internal controllers
} catch (Exception $ex) {
$full_name = ucfirst(Inflector
::camelize($class_name)); if (substr($full_name, -15) != 'AdminController') { $full_name .= 'AdminController';
}
$full_name = 'Sprout\\Controllers\\Admin\\' . $full_name;
}
}
$inst = Sprout::instance(
$full_name,
'Sprout\\Controllers\\Admin\\ManagedAdminController'
);
return $inst;
}
/**
* For a given URL, ensure it's absolute.
* If it's not absolute, the current admin abs-root is prepended
*
* @param string $url Either relative or absolute
* @return string Absolute URL
*/
public static function ensureUrlAbsolute($url)
{
return $url;
} else {
return Subsites
::getAbsRoot($_SESSION['admin']['active_subsite']) . ltrim($url, '/'); }
}
}