<?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\Controllers;
use Exception;
use InvalidArgumentException;
use ReflectionException;
use ReflectionMethod;
use Kohana;
use Kohana_404_Exception;
use Sprout\Controllers\Admin\CategoryAdminController;
use Sprout\Controllers\Admin\ManagedAdminController;
use Sprout\Controllers\Admin\PageAdminController;
use karmabunny\pdb\Exceptions\ConstraintQueryException;
use karmabunny\pdb\Exceptions\QueryException;
use karmabunny\pdb\Exceptions\RowMissingException;
use Sprout\Helpers\Admin;
use Sprout\Helpers\AdminAuth;
use Sprout\Helpers\AdminDashboard;
use Sprout\Helpers\AdminError;
use Sprout\Helpers\AdminPerms;
use Sprout\Helpers\AdminSeo;
use Sprout\Helpers\Category;
use Sprout\Helpers\Constants;
use Sprout\Helpers\Cron;
use Sprout\Helpers\Csrf;
use Sprout\Helpers\Enc;
use Sprout\Helpers\FileIndexing;
use Sprout\Helpers\Form;
use Sprout\Helpers\Inflector;
use Sprout\Helpers\Navigation;
use Sprout\Helpers\Notification;
use Sprout\Helpers\Pdb;
use Sprout\Helpers\PerRecordPerms;
use Sprout\Helpers\Register;
use Sprout\Helpers\Replication;
use Sprout\Helpers\Request;
use Sprout\Helpers\Router;
use Sprout\Helpers\Session;
use Sprout\Helpers\Sprout;
use Sprout\Helpers\Subsites;
use Sprout\Helpers\Tags;
use Sprout\Helpers\Text;
use Sprout\Helpers\TwoFactor\GoogleAuthenticator;
use Sprout\Helpers\Upload;
use Sprout\Helpers\Url;
use Sprout\Helpers\UserAgent;
use Sprout\Helpers\View;
/**
* Main class to handle admin processing.
* This delegates processing to controllers registered with {@see Register::adminControllers}
*/
class AdminController extends Controller
{
/**
* Does some general admin loading
**/
public function __construct()
{
parent::__construct();
Session::instance();
// Check the IP whitelist
if (PHP_SAPI != 'cli') {
$whitelist = Kohana::config('sprout.admin_ips');
if ($whitelist and
count($whitelist) > 0) { if (! Sprout::ipaddressInArray(Request::userIp(), $whitelist)) {
throw new Kohana_404_Exception();
}
}
}
// If it's the wrong server, switch to the right one.
$admin_url = Replication::adminUrl();
if ($admin_url) {
Url::redirect($admin_url);
}
AdminPerms::loadAccessFlags();
// A little domain-name check for multi-site installs
$domain = Kohana::config('sprout.admin_domain');
if ($domain and $domain != $_SERVER['HTTP_HOST']) {
Url
::redirect('http://' . $domain . Kohana
::config('config.site_domain') . Url
::current()); }
Register::docImport('csv', 'Sprout\\Helpers\\DocImport\\DocImportCSV', 'CSV');
Register::docImport('txt', 'Sprout\\Helpers\\DocImport\\DocImportPlaintext', 'Plain text');
Register::docImport('docx', 'Sprout\\Helpers\\DocImport\\DocImportDOCX', 'Microsoft Word 2007 and later');
Register::coreContentControllers();
// Most methods require auth, but a few do not
$methods_no_auth = ['login', 'loginAction', 'loginTwoFactor', 'loginTwoFactorAction', 'logout', 'userAgent'];
// Also, some initalisation doesn't work properly when not authenticated
if (!in_array(Router
::$method, $methods_no_auth) and PHP_SAPI
!== 'cli') { AdminAuth::checkLogin();
// Load page tree
Navigation::loadPageTree(@$_SESSION['admin']['active_subsite'], true);
// Execute some code for each module
// This usually just loads some menu items
$module_paths = Register::getModuleDirs();
foreach ($module_paths as $path) {
$path .= '/admin_load.php';
}
}
// Default config
if (! Kohana::config('sprout.admin_intro')) {
Kohana::configSet('sprout.admin_intro', 'admin/dashboard');
}
}
/**
* Home page of admin area
**/
public function index()
{
AdminAuth::checkLogin();
Url::redirect(Kohana::config('sprout.admin_intro'));
}
/**
* Shows a login form
**/
public function login()
{
if (AdminAuth::isLoggedIn()) {
Url::redirect(Kohana::config('sprout.admin_intro'));
}
$view = new View('sprout/admin/login_layout');
$this->setDefaultMainviewParams($view);
$view->nav = null;
$view->admin_authenticated = false;
$view->browser_title = 'Login';
$view->main_title = 'Login';
$msg = Sprout::extraPage(Constants::EXTRAPAGES_ADMIN_LOGIN);
if ($msg and
empty($_GET['nomsg'])) { $view->main_content = new View('sprout/admin/login_message');
$view->main_content->msg = $msg;
} else {
$view->main_content = new View('sprout/admin/login_form');
}
if (!empty($_GET['username'])) { $view->main_content->username = trim($_GET['username']); }
echo $view->render();
}
/**
* Processes a user login
**/
public function loginAction()
{
Csrf::checkOrDie();
Session::instance();
Session::regenerate();
$_POST['Username'] = trim($_POST['Username']); $_POST['Password'] = trim($_POST['Password']); $_POST['redirect'] = trim($_POST['redirect']);
if ($_POST['Username'] == '' or $_POST['Password'] == '') {
Notification::error("Username or password not specified.");
Url::redirect('admin/login?username=' . Enc::url($_POST['Username']) . '&redirect=' . Enc::url($_POST['redirect']) . '&nomsg=1');
}
$result = AdminAuth::checkRateLimit($_POST['Username'], Request::userIp());
if ($result !== true) {
list($aspect, $limit) = $result; Notification::error('Login rate limit exceeded.');
Notification::error("Limit: {$aspect}, {$limit}");
Url::redirect('admin/login&redirect=' . Enc::url($_POST['redirect']) . '&nomsg=1');
}
$result = AdminAuth::processLogin($_POST['Username'], $_POST['Password']);
if (! $result) {
$result = AdminAuth::processRemote($_POST['Username'], $_POST['Password']);
}
if (! $result) {
$result = AdminAuth::processLocal($_POST['Username'], $_POST['Password']);
}
AdminAuth::saveLoginAttempt($_POST['Username'], Request::userIp(), $result === true ? 1 : 0);
if (! $result) {
Notification::error('Incorrect username or password specified');
Url::redirect('admin/login?username=' . Enc::url($_POST['Username']) . '&redirect=' . Enc::url($_POST['redirect']) . '&nomsg=1');
}
// Login requires two-factor auth
if (isset($_SESSION['admin']['tfa_id'])) { Url::redirect('admin/login-two-factor?redirect=' . $_POST['redirect']);
}
$this->loginComplete();
}
/**
* Show the two-factor-auth ui for a half-logged-in operator
*/
public function loginTwoFactor()
{
if (!isset($_SESSION['admin']['tfa_id'])) { Url::redirect('admin/login');
}
try {
$q = "SELECT tfa_method FROM ~operators WHERE id = ?";
$tfa_method = Pdb::query($q, [$_SESSION['admin']['tfa_id']], 'val');
} catch (RowMissingException $ex) {
Url::redirect('admin/login');
}
switch ($tfa_method) {
case 'none':
$this->loginComplete();
break;
case 'totp':
$view = new View('sprout/tfa/totp_login');
$view->action_url = 'admin/login-two-factor-action';
break;
default:
throw new Exception('Unknown TFA method');
}
$skin = new View('sprout/admin/login_layout');
$skin->browser_title = 'Login';
$skin->main_title = 'Login';
$skin->main_content = $view->render();
echo $skin->render();
}
/**
* Process the result of a two-factor-auth for a half-logged-in operator
*/
public function loginTwoFactorAction()
{
if (!isset($_SESSION['admin']['tfa_id'])) { Url::redirect('admin/login');
}
$_POST['redirect'] = trim(@$_POST['redirect']);
$q = "SELECT tfa_method, tfa_secret FROM ~operators WHERE id = ?";
$operator = Pdb::query($q, [$_SESSION['admin']['tfa_id']], 'row');
switch ($operator['tfa_method']) {
case 'totp':
$goog = new GoogleAuthenticator();
$success = $goog->checkCode($operator['tfa_secret'], $_POST['code']);
break;
default:
throw new Exception('Unknown TFA method');
}
if (!$success) {
Notification::error('Two-factor authentication failed - please try again');
Url::redirect('admin/login-two-factor?redirect=' . $_POST['redirect']);
}
$_SESSION['admin']['login_id'] = $_SESSION['admin']['tfa_id'];
unset($_SESSION['admin']['tfa_id']); $this->loginComplete();
}
/**
* Set up various login params and redirect into admin
*
* Called after a successful login (either one-factor or two-factor)
*/
private function loginComplete()
{
if (empty($_POST['Username'])) $_POST['Username'] = ''; if (empty($_POST['redirect'])) $_POST['redirect'] = '';
$subsite = Subsites::getFirstAccessable();
if (! $subsite) {
Notification::error('No subsites are accessible by your user account');
Url::redirect('admin/login?username=' . Enc::url($_POST['Username']) . '&redirect=' . Enc::url($_POST['redirect']) . '&nomsg=1');
}
// Permissions system requires users to be in a category
if (!AdminAuth::isSuper()) {
$cats = Category::categoryList('operators', AdminAuth::getId());
Notification::error('Your user account isn\'t in any categories.');
Url::redirect('admin/login?username=' . Enc::url($_POST['Username']) . '&redirect=' . Enc::url($_POST['redirect']) . '&nomsg=1');
}
}
$_SESSION['admin']['active_subsite'] = $subsite;
Notification::confirm('You are now logged in to the admin control panel');
if (!empty($_POST['redirect']) and Url
::checkRedirect($_POST['redirect'], true)) { Url::redirect($_POST['redirect']);
}
Url::redirect(Kohana::config('sprout.admin_intro'));
}
/**
* Processes a user logout
**/
public function logout()
{
try {
Admin::unlock();
} catch (QueryException $ex) {
// Assume DB has no tables
}
AdminAuth::logout();
Session::instance();
Session::regenerate();
Notification::confirm('You are now logged out');
Url::redirect('admin/login');
}
/**
* View the various styles available in the admin area
**/
public function styleGuide($section)
{
AdminAuth::checkLogin();
$buttons = new View('sprout/admin/style_guide/index');
if ($section != 'index') {
$inner_view = new View('sprout/admin/style_guide/' . $section);
} else {
$inner_view = '';
}
$view = new View('sprout/admin/main_layout');
$ctlr = $this->getController('Sprout\Controllers\Admin\PageAdminController');
$this->setDefaultMainviewParams($view);
$this->setNavigation($view, $ctlr);
$view->controller_name = '_style_guide';
$view->browser_title = 'Style guide';
$view->main_title = 'SproutCMS Style Guide';
$view->main_content = $buttons . $inner_view;
echo $view->render();
}
/**
* Dashboard shown when a user first logs in to the admin
*/
public function dashboard()
{
AdminAuth::checkLogin();
$first = AdminPerms::getFirstAccessable();
if ($first === null) {
Url::redirect('admin/intro/my_settings');
} else if ($first != 'page') {
Url::redirect('admin/intro/' . $first);
}
$ctlr = $this->getController('Sprout\Controllers\Admin\PageAdminController');
if (! $ctlr) return;
$dash_html = AdminDashboard::render();
$view = new View('sprout/admin/main_layout');
$this->setDefaultMainviewParams($view);
$this->setNavigation($view, $ctlr);
$view->controller_name = '_dashboard';
$view->browser_title = 'Dashboard';
$view->main_title = 'SproutCMS Administration';
$view->main_content = $dash_html;
echo $view->render();
}
/**
* Closes the 'first run' box, which is shown on the admin dashboard
*
* @return void Redirects to the admin dashboard
*/
public function closeFirstrun()
{
AdminAuth::checkLogin();
Pdb::update(
'operators',
['firstrun' => 0],
['id' => AdminAuth::getId()]
);
Url::redirect('admin/dashboard');
}
/**
* Shows an introduction for a specified type.
*
* @param string $type The type to show an intro for.
**/
public function intro($type)
{
AdminAuth::checkLogin();
$ctlr = $this->getController($type);
if (! $ctlr) return;
$view = new View('sprout/admin/main_layout');
$this->setDefaultMainviewParams($view);
$this->setNavigation($view, $ctlr);
$main = $ctlr->_intro();
$main = array('title' => $ctlr->getFriendlyName(), 'content' => $main); }
$view->browser_title = $ctlr->getFriendlyName();
$view->main_title = $ctlr->getFriendlyName();
$view->main_title = $main['title'];
$view->main_content = $main['content'];
echo $view->render();
}
/**
* Shows a search form for the specified item
*
* @param string $type The type of item to show the search form for
**/
public function search($type)
{
AdminAuth::checkLogin();
$this->unlock($type);
$view = new View('sprout/admin/main_layout');
$this->setDefaultMainviewParams($view);
$ctlr = $this->getController($type);
if (! $ctlr) return;
if (! $this->checkAccess($ctlr, 'contents', false)) return;
$this->setNavigation($view, $ctlr);
$main = $ctlr->_getSearchForm();
if ($main instanceof AdminError) {
$this->error($main->getMessage(), $ctlr);
return;
}
if (! is_array($main)) $main = array('title' => $ctlr->getFriendlyName(), 'content' => $main);
$view->browser_title = strip_tags($main['title']); $view->main_title = $main['title'];
$view->main_content = $main['content'];
echo $view->render();
}
/**
* Shows an edit form for the specified item
*
* @param string $type The type of item to show the edit form of
* @param int $id The id of the record to edit
**/
public function contents($type)
{
AdminAuth::checkLogin();
$this->unlock($type);
$view = new View('sprout/admin/main_layout');
$this->setDefaultMainviewParams($view);
$ctlr = $this->getController($type);
if (! $ctlr) return;
if (! $this->checkAccess($ctlr, 'contents', false)) return;
$this->setNavigation($view, $ctlr);
$main = $ctlr->_getContents();
if ($main instanceof AdminError) {
$this->error($main->getMessage(), $ctlr);
return;
}
if (! is_array($main)) $main = array('title' => $ctlr->getFriendlyName(), 'content' => $main);
$view->browser_title = strip_tags($main['title']); $view->main_title = $main['title'];
$view->main_content = $main['content'];
echo $view->render();
}
/**
* Shows an edit form for the specified item
*
* @param string $type The type of item to show the edit form of
* @param int $id The id of the record to edit
**/
public function export($type)
{
AdminAuth::checkLogin();
$view = new View('sprout/admin/main_layout');
$this->setDefaultMainviewParams($view);
$ctlr = $this->getController($type);
if (! $ctlr) return;
if (! $this->checkAccess($ctlr, 'export', false)) return;
$this->setNavigation($view, $ctlr);
$main = $ctlr->_getExport();
if ($main instanceof AdminError) {
$this->error($main->getMessage(), $ctlr);
return;
}
if (! is_array($main)) $main = array('title' => $ctlr->getFriendlyName(), 'content' => $main);
$view->browser_title = strip_tags($main['title']); $view->main_title = $main['title'];
$view->main_content = $main['content'];
echo $view->render();
}
/**
* Executes the save action for a specific item
*
* @param string $type The type of item to save
* @param int $id The id of the record to save
**/
public function exportAction($type)
{
AdminAuth::checkLogin();
Csrf::checkOrDie();
$ctlr = $this->getController($type);
if (! $ctlr) return;
if (! $this->checkAccess($ctlr, 'export', true)) return;
$result = $ctlr->_exportData();
if ($result == false) {
Notification::error('There was an error performing the export');
Url::redirect("admin/export/{$type}");
}
$length = strlen($result['data']); header("Content-type: {$result['type']}"); header("Content-disposition: attachment; filename={$result['filename']}"); header("Content-length: {$length}");
// MSIE needs "public" when under SSL - http://support.microsoft.com/kb/316431
header('Cache-Control: public, max-age=1');
echo $result['data'];
}
/**
* File upload box for importing, options are the next step
*
* @param string $type The type of item to show the import form of
**/
public function importUpload($type)
{
AdminAuth::checkLogin();
$view = new View('sprout/admin/main_layout');
$this->setDefaultMainviewParams($view);
$ctlr = $this->getController($type);
if (! $ctlr) return;
if (! $this->checkAccess($ctlr, 'import', false)) return;
$this->setNavigation($view, $ctlr);
if ($type == 'page') {
$title = 'Document import';
$main = $ctlr->_importUploadForm();
} else {
$title = 'Import ' . strtolower($ctlr->getFriendlyName()); $main = new View('sprout/admin/import_upload');
$main->type = $type;
$main->xls = FileIndexing::isExtSupported('xls');
}
$view->main_title = $title;
$view->main_content = $main;
echo $view->render();
}
/**
* Copies the file to a temporary directory
**/
public function importUploadAction($type)
{
AdminAuth::checkLogin();
Csrf::checkOrDie();
if (FileIndexing::isExtSupported('xls')) {
$formats[] = 'xls';
}
// validate upload
$error = false;
if (! Upload::required($_FILES['import'])) {
$error = 'No file provided';
} else if (! Upload::valid($_FILES['import'])) {
$error = 'File upload error';
} else if (! Upload::type($_FILES['import'], $formats)) {
$error = 'Incorrect file type, accepted types are: ' . implode(', ', $formats); }
if (! $error) {
$tempname = APPPATH . "temp/import_{$timestamp}.csv";
if (preg_match('/\.xls$/', $_FILES['import']['name'])) { // Load XLS from fileindexing tool
$plaintext = FileIndexing::getPlaintext($_FILES['import']['tmp_name'], 'xls');
if (! $plaintext) {
$error = 'Unable to copy file to temporary directory (read)';
}
if ($res === false) {
$error = 'Unable to copy file to temporary directory (write)';
}
} else if (preg_match('/\.csv$/', $_FILES['import']['name'])) { // Copy the CSV directly
$res = @copy($_FILES['import']['tmp_name'], $tempname); if (! $res) {
$error = 'Unable to copy file to temporary directory';
}
} else {
$error = 'Unknown file type';
}
}
if ($error) {
Notification::error($error);
Url::redirect("admin/import_upload/{$type}");
}
Url::redirect("admin/import_options/{$type}?timestamp={$timestamp}");
}
/**
* Shows the import form for the specified item
*
* @param string $type The type of item to show the import form of
**/
public function importOptions($type)
{
AdminAuth::checkLogin();
$_GET['timestamp'] = (int)@$_GET['timestamp'];
$_GET['ext'] = trim(@$_GET['ext']); if (! $_GET['ext']) $_GET['ext'] = 'csv';
$filename = APPPATH . "temp/import_{$_GET['timestamp']}.{$_GET['ext']}";
$this->error("Uploaded import file not found on server");
return;
}
$view = new View('sprout/admin/main_layout');
$this->setDefaultMainviewParams($view);
$ctlr = $this->getController($type);
if (! $ctlr) return;
if (! $this->checkAccess($ctlr, 'import', false)) return;
$this->setNavigation($view, $ctlr);
$main = $ctlr->_getImport($filename);
if ($main instanceof AdminError) {
$this->error($main->getMessage(), $ctlr);
return;
}
if (! is_array($main)) $main = array('title' => $ctlr->getFriendlyName(), 'content' => $main);
$view->browser_title = strip_tags($main['title']); $view->main_title = $main['title'];
$view->main_content = $main['content'];
echo $view->render();
}
/**
* Executes the import action
*
* @param string $type The type of item to import
**/
public function importAction($type)
{
AdminAuth::checkLogin();
Csrf::checkOrDie();
$_POST['timestamp'] = (int) @$_POST['timestamp'];
$_POST['ext'] = trim(@$_POST['ext']); if (! $_POST['ext']) $_POST['ext'] = 'csv';
$filename = APPPATH . "temp/import_{$_POST['timestamp']}.{$_POST['ext']}";
$this->error("Uploaded import file not found on server");
return;
}
$ctlr = $this->getController($type);
if (! $ctlr) return;
if (! $this->checkAccess($ctlr, 'import', true)) return;
$result = $ctlr->_importData($filename);
if ($result == false) {
Notification::error('There was an error performing the import');
Url::redirect("admin/import_options/{$type}?timestamp={$_POST['timestamp']}&ext={$_POST['ext']}");
}
$ctlr->_invalidateCaches('import');
Notification::confirm('Import has been completed successfully');
Url::redirect("admin/contents/{$type}");
}
/**
* Shows an error message in the admin skin
*
* @param string $message The message to show. Should be plain-text.
* @param ManagedAdminController $ctlr A controller to show the navigation of.
**/
private function error($message, ManagedAdminController $ctlr = null)
{
AdminAuth::checkLogin();
$content = new View('sprout/admin/error');
$content->message = $message;
$view = new View('sprout/admin/main_layout');
$this->setDefaultMainviewParams($view);
if ($ctlr) {
$this->setNavigation($view, $ctlr);
}
$view->browser_title = 'Error';
$view->main_title = 'Error';
$view->main_content = $content;
echo $view->render();
}
/**
* Check access for a given controller/access flag combo
* If this returns false, it will echo out an error message; the calling code should just return.
* @param ManagedAdminController $ctlr A controller to check
* @param string $access_flag The access flag to check, e.g. 'add', 'edit', etc
* @param bool $action True if it's an action method, false if it's a form method.
* @return True if auth is okay, false if it is not.
**/
private function checkAccess(ManagedAdminController $ctlr, $access_flag, $action)
{
AdminAuth::checkLogin();
if ($ctlr instanceof CategoryAdminController) {
$ctlr = $ctlr->getParentInst();
$access_flag = 'categories';
}
if (AdminPerms::controllerAccess($ctlr->getControllerName(), $access_flag)) {
return true;
}
if ($action) {
Notification::error('Access Denied; Section: ' . $ctlr->getFriendlyName() . '; Action: ' . $access_flag);
Url::redirect('admin');
} else {
$content = new View('sprout/admin/access_denied');
$content->friendly_name = $ctlr->getFriendlyName();
$content->access_flag = $access_flag;
$view = new View('sprout/admin/main_layout');
$this->setDefaultMainviewParams($view);
if ($ctlr) {
$this->setNavigation($view, $ctlr);
}
$view->browser_title = 'Access denied';
$view->main_title = 'Access denied';
$view->main_content = $content;
echo $view->render();
}
return false;
}
/**
* Checks that an admin user has access to an individual record
*
* @param ManagedAdminController $ctlr The controller which manages the table containing the record
* @param int $item_id The id of the row to be edited/deleted
* @return bool True if access allowed
*/
function checkRecordAccess(ManagedAdminController $ctlr, $item_id)
{
$restrict = PerRecordPerms::controllerRestricted($ctlr);
if (!$restrict) return true;
$params = [];
$cat_clause = PerRecordPerms::getCategoryClause();
$params[] = $ctlr->getControllerName();
$params[] = $item_id;
$q = "SELECT {$cat_clause}
FROM ~per_record_permissions
WHERE controller = ? AND item_id = ?";
$res = Pdb::q($q, $params, 'col');
if (count($res) == 0) return true;
return (bool) Sprout::iterableFirstValue($res);
}
/**
* Ensure 'active' flag and 'tags' POST fields have values set, if they are nonexistant
*
* @param ManagedAdminController $ctlr
*/
private function cleanupCommonPostData(ManagedAdminController $ctlr)
{
if (!isset($_POST['tags'])) { $_POST['tags'] = '';
}
$visibility = $ctlr->_getVisibilityFields();
foreach ($visibility as $name => $label) {
if (empty($_POST[$name])) { $_POST[$name] = 0;
} elseif ($_POST[$name] != '0' and $_POST[$name] != '1') {
$_POST[$name] = 0;
}
}
// Ensure that the session always has data, so that the initial lookup is treated
// differently from a form submission with no categories selected
if (!isset($_POST['_prm_categories'])) { $_POST['_prm_categories'] = [];
}
}
/**
* Render a list of sub-actions into HTML as links
*
* Each sub-action should be an array, with the following keys:
* url Link URL
* name Link text
* class Optional class
* new_tab Optional bool to show in new window/tab
*
* Use a special entry with a key of "_preview" and a value of the
* preview URL to set up a preview button
*
* @param array $list Sub-actions to render
* @return HTML
*/
private function renderSubActions
(array $list) {
$out = '<ul class="list-style-1">';
foreach ($list as $key => $item) {
if ($key === '_preview') continue;
$class = 'sub-action';
if (isset($item['class'])) $class .= ' ' . $item['class'];
$out .= '<li>';
$out .= '<a href="' . Enc::html($item['url']) . '" class="' . Enc::html($class) . '"';
if (isset($item['new_tab']) and
$item['new_tab'] === true) { $out .= ' target="_blank"';
}
$out .= '>';
$out .= Enc::html($item['name']);
$out .= '</a>';
$out .= '</li>';
}
$out .= '</ul>';
return $out;
}
/**
* Generates HTML for fields relating to per-record permissions in the 'save changes' box
*
* @param string ManagedAdminController $ctlr The controller to check permissions for
* @param int $item_id The ID of the record being edited (0 when adding a new record)
* @return string HTML
*/
protected function perRecordPermissionsFields(ManagedAdminController $ctlr, $item_id)
{
if (!PerRecordPerms::controllerRestricted($ctlr)) {
return '';
}
// Preload operator categories for per-user permissions
if ($item_id > 0) {
$q = "SELECT operator_categories
FROM ~per_record_permissions
WHERE controller = ? AND item_id = ?";
$access = Pdb::q($q, [$ctlr->getControllerName(), $item_id], 'arr');
if (count($access) > 0) { $access = Sprout::iterableFirstValue($access);
if (Form::getData('_prm_categories') === null) {
Form::setFieldValue('_prm_categories', $cat_ids);
}
}
}
$out = '';
if (AdminPerms::canAccess('access_operators')) {
$cat_list = AdminAuth::getAllCategories();
} else {
$cat_list = [];
$cat_ids = AdminAuth::getOperatorCategories();
if (count($cat_ids) > 0) { $params = [];
$conds = [
['id', 'IN', $cat_ids],
];
$where = Pdb::buildClause($conds, $params);
$q = "SELECT id, name
FROM ~operators_cat_list
WHERE {$where}
ORDER BY name";
$cat_list = Pdb::q($q, $params, 'map');
}
}
// Don't display primary administrators category; they always get access
$primary_cat_id = AdminAuth::getPrimaryCategoryId();
unset($cat_list[$primary_cat_id]);
$checked_cats = Form::getData('_prm_categories');
// Pre-tick all categories if on add form
// N.B. primary admins don't have any categories ticked because they belong to ALL categories,
// and it defeats the purpose of per-record controls if everyone has access by default.
if ($item_id == 0 and
count($checked_cats) == 0) { if (!AdminAuth::inCategory($primary_cat_id)) {
Form::setFieldValue('_prm_categories', $checked_cats);
}
}
$allow_cats = '';
if ($item_id == 0 or AdminAuth::inCategory($primary_cat_id)) {
Form::nextFieldDetails('Allow changes by', false);
$allow_cats = Form::checkboxSet('_prm_categories', [], $cat_list);
// Hack in 'all operators' option for primary admins
if (AdminAuth::inCategory($primary_cat_id)) {
$all = '<div class="field-element__input-set">';
$all .= '<div class="fieldset-input"><input type="checkbox" value="1" name="_prm_all_cats" id="_prm_all"';
if ($checked_cats == ['*'] or ($item_id == 0 and Form::getData('_prm_all_cats'))) {
$all .= ' checked';
}
$all .= '><label for="_prm_all">All operators</label></div>';
$allow_cats = str_replace('<div class="field-element__input-set">', $all, $allow_cats); }
}
$out .= $allow_cats;
return $out;
}
/**
* Shows an add form for the specified item
*
* @param string $type The type of item to show the add form of
**/
public function add($type)
{
AdminAuth::checkLogin();
$ctlr = $this->getController($type);
if (! $ctlr) return;
if (! $this->checkAccess($ctlr, 'add', false)) return;
$view = new View('sprout/admin/main_layout');
$this->setDefaultMainviewParams($view);
$this->setNavigation($view, $ctlr);
$main = $ctlr->_getAddForm();
if ($main instanceof AdminError) {
$this->error($main->getMessage(), $ctlr);
return;
}
throw new InvalidArgumentException('Return value from _getAddForm must be an array');
}
if ($ctlr->_isAddSaved() and Text::containsFormTag($main['content'])) {
throw new Exception("Add view must not include the form tag");
}
if (Request::isAjax()) {
$class = 'admin-ajax action-add type-' . Enc::id($type);
echo '<h2 class="popup-title">', $main['title'], '</h2>';
echo '<div class="', $class, '">';
echo '<form action="admin/add_save/' . Enc::html($ctlr->getControllerName()) . '" method="post">';
echo Csrf::token();
echo $main['content'];
echo '<div class="action-bar"><button type="submit" class="button button-regular button-green icon-after icon-save">Save changes</button></div>';
echo '</form>';
echo '</div>';
return;
}
// Create tags area, and inject it into content after the <form> tag
$tags = new View('sprout/admin/main_tags');
$tags->type = $type;
$tags->suggestions = Tags::suggestTags($ctlr->getTableName());
$tags->table = $ctlr->getTableName();
$tags->current_tags = @$_SESSION['admin']['tags'];
unset ($_SESSION['admin']['tags']);
if ($ctlr->_isAddSaved()) {
$single = Inflector::singular($ctlr->getFriendlyName());
$content = '<form action="admin/add_save/' . Enc::html($ctlr->getControllerName()) . '" method="post" id="edit-form" class="-clearfix">';
$content .= Csrf::token();
$content .= '<div class="mainbar-with-right-sidebar">';
$content .= $tags->render();
$content .= $main['content'];
$content .= '</div>';
$content .= '<div class="right-sidebar">';
$content .= '<div class="right-sidebar-inner">';
$content .= '<div class="save-changes-box">';
$html = $ctlr->_getCustomAddSaveHTML();
if ($html) {
$content .= $html;
} else {
$visibility = $ctlr->_getVisibilityFields();
$sub_actions = $ctlr->_getAddSubActions();
$content .= '<h2 class="icon-before icon-add">Add ' . Enc::html($single) . '</h2>';
if (!empty($visibility)) { Form::nextFieldDetails('Visibility', false);
$content .= Form::checkboxBoolList(null, [], $visibility);
}
$content .= $this->perRecordPermissionsFields($ctlr, 0);
if ($ctlr->isPerSubsite()) {
$subsites = Pdb::lookup('subsites');
Form::nextFieldDetails('Subsite', false);
Form::setFieldValue('subsite_id', $_SESSION['admin']['active_subsite']);
$content .= Form::dropdown('subsite_id', ['-dropdown-top' => 'Show on all sites'], $subsites);
}
$content .= $this->renderSubActions($sub_actions);
$content .= '<div class="save-changes-box-bottom -clearfix">';
if (!empty($sub_actions['_preview'])) { $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>';
}
$content .= '<button type="submit" class="save-changes-save-button button button-regular button-green icon-after icon-add">Save changes</button>';
$content .= '</div>';
}
$content .= '</div>';
$content .= '</div>';
$content .= '</div>';
$content .= '</form>';
} else {
$content = $main['content'];
}
$view->browser_title = strip_tags($main['title']); $view->main_title = $main['title'];
$view->main_content = $content;
$view->has_tags = true;
$view->main_class = 'do-action-box';
echo $view->render();
}
/**
* Executes the save action for a specific item
*
* @param string $type The type of item to add
**/
public function addSave($type)
{
AdminAuth::checkLogin();
Csrf::checkOrDie();
$ctlr = $this->getController($type);
if (! $ctlr) return;
if (! $this->checkAccess($ctlr, 'add', true)) return;
$this->cleanupCommonPostData($ctlr);
$_SESSION['admin']['tags'] = $_POST['tags'];
$id = 0;
$result = $ctlr->_addSave($id);
// Set per-record permissions
if ($result) {
PerRecordPerms::save($ctlr, $id);
}
if (Request::isAjax()) {
$result = (int) $result;
}
if ($result == false) {
Notification::error('There was an error saving your changes');
if (!empty($_POST['current_url'])) { Url::redirect($_POST['current_url']);
}
Url::redirect("admin/add/{$type}");
}
$new_tags = Tags::splitupTags($_POST['tags']);
$tag_result = Tags::update($ctlr->getTableName(), $id, $new_tags);
unset ($_SESSION['admin']['tags']);
if ($tag_result == false) {
Notification::error('There was an error updating the tags for this item');
}
$ctlr->_invalidateCaches('add', $id);
unset ($_SESSION['admin']['field_values']);
$single = strtolower(Inflector
::singular($ctlr->getFriendlyName())); $message = "Your {$single} has been added";
if (!Notification::has(Notification::TYPE_CONFIRM)) {
Notification::confirm($message, []);
}
Url::redirect($result);
} else {
Url::redirect("admin/edit/{$type}/{$id}");
}
}
/**
* Shows an edit form for the specified item
*
* @param string $type The type of item to show the edit form of
* @param int $id The id of the record to edit
**/
public function edit($type, $id)
{
AdminAuth::checkLogin();
$id = (int) $id;
$view = new View('sprout/admin/main_layout');
$this->setDefaultMainviewParams($view);
$view->has_tags = true;
$ctlr = $this->getController($type);
if (! $ctlr) return;
if (! $this->checkAccess($ctlr, 'edit', false)) return;
if (! $this->checkRecordAccess($ctlr, $id)) return;
$this->setNavigation($view, $ctlr);
$main = $ctlr->_getEditForm($id);
if ($main instanceof AdminError) {
$this->error($main->getMessage(), $ctlr);
return;
}
throw new InvalidArgumentException('Return value from _getEditForm must be an array');
}
// Disallow view if it contains a <FORM> tag or output will contain nested-forms and that doesn't work
if ($ctlr->_isEditSaved($id) and Text::containsFormTag($main['content'])) {
throw new Exception("Edit view must not include the form tag");
}
// Create tags area, and inject it into content after the <form> tag
$tags = new View('sprout/admin/main_tags');
$tags->suggestions = Tags::suggestTags($ctlr->getTableName());
$tags->table = $ctlr->getTableName();
$tags->current_tags = @$_SESSION['admin']['tags'];
if (empty($_SESSION['admin']['tags'])) { $tags->current_tags = implode(', ', Tags
::byRecord($ctlr->getTableName(), $id)); }
unset ($_SESSION['admin']['tags']);
// Check for SEO enabled content
$view->enable_seo = !empty(AdminSeo
::$content)?
true : false;
if ($ctlr->_isEditSaved($id)) {
$content = '<form action="admin/edit_save/' . Enc::html($ctlr->getControllerName()) . '/' . $id;
$content .= '" method="post" id="edit-form" class="-clearfix" enctype="multipart/form-data">';
$content .= Csrf::token();
$content .= '<div class="mainbar-with-right-sidebar">';
$content .= $tags->render();
$content .= AdminSeo::getAnalysis();
$content .= $main['content'];
$content .= '</div>';
$content .= '<div class="right-sidebar">';
$content .= '<div class="right-sidebar-inner">';
$content .= '<div class="save-changes-box">';
$html = $ctlr->_getCustomEditSaveHTML($id);
if ($html) {
$content .= $html;
} else {
$visibility = $ctlr->_getVisibilityFields();
$sub_actions = $ctlr->_getEditSubActions($id);
$content .= '<h2 class="icon-before icon-save">Save changes</h2>';
if (!empty($visibility)) { Form::nextFieldDetails('Visibility', false);
$content .= Form::checkboxBoolList(null, [], $visibility);
}
$content .= $this->perRecordPermissionsFields($ctlr, $id);
if ($ctlr->isPerSubsite()) {
$subsites = Pdb::lookup('subsites');
Form::nextFieldDetails('Subsite', false);
$content .= Form::dropdown('subsite_id', ['-dropdown-top' => 'Show on all sites'], $subsites);
}
$content .= $this->renderSubActions($sub_actions);
$content .= '<div class="save-changes-box-bottom -clearfix">';
if (!empty($sub_actions['_preview'])) { $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>';
}
$content .= '<button type="submit" class="save-changes-save-button button button-regular button-green icon-after icon-save">Save changes</button>';
$content .= '</div>';
}
$content .= '</div>';
$content .= '</div>';
$content .= '</div>';
$content .= '</form>';
} else {
$content = $main['content'];
}
$this->lock($type, $id, $view);
// Render the main view
$view->browser_title = Text
::limitChars(strip_tags($main['title']), 50, '...'); $view->main_title = $main['title'];
$view->main_content = $content;
$view->main_class = 'do-action-box';
$url = $ctlr->_getEditLiveUrl($id);
if ($url) {
$view->live_url = Admin::ensureUrlAbsolute($url);
}
echo $view->render();
}
/**
* Executes the save action for a specific item
*
* @param string $type The type of item to save
* @param int $id The id of the record to save
**/
public function editSave($type, $id)
{
AdminAuth::checkLogin();
Csrf::checkOrDie();
$id = (int) $id;
$ctlr = $this->getController($type);
if (! $ctlr) return;
if (! $this->checkAccess($ctlr, 'edit', true)) return;
if (! $this->checkRecordAccess($ctlr, $id)) return;
$this->unlock($type, $id);
$this->cleanupCommonPostData($ctlr);
$_SESSION['admin']['tags'] = $_POST['tags'];
$result = $ctlr->_editSave($id);
if (Request::isAjax()) {
$result = (int) $result;
return;
}
if ($result == false) {
Notification::error('There was an error saving your changes');
Url::redirect("admin/edit/{$type}/{$id}");
}
// Update per-record permissions
if ($result and AdminPerms::canAccess('access_operators')) {
PerRecordPerms::save($ctlr, $id);
}
$new_tags = Tags::splitupTags($_POST['tags']);
$tag_result = Tags::update($ctlr->getTableName(), $id, $new_tags);
unset ($_SESSION['admin']['tags']);
if ($tag_result == false) {
Notification::error('There was an error updating the tags for this item');
}
$ctlr->_invalidateCaches('edit', $id);
unset ($_SESSION['admin']['field_values']); if (!Notification::has(Notification::TYPE_CONFIRM)) {
Notification::confirm('Your changes have been saved');
}
Url::redirect($result);
} else {
Url::redirect("admin/edit/{$type}/{$id}");
}
}
/**
* Shows a delete form for the specified item
* @param string $type Shorthand controller name; see {@see Register::adminControllers}
* @param int $id The id of the record to show
*/
public function delete($type, $id)
{
AdminAuth::checkLogin();
$ctlr = $this->getController($type);
if (!$ctlr) return;
if (!$this->checkAccess($ctlr, 'delete', false)) return;
if (!$this->checkRecordAccess($ctlr, $id)) return;
if (!$ctlr->_isDeleteSaved($id)) return;
$main = $ctlr->_getDeleteForm($id);
if ($main instanceof AdminError) {
$this->error($main->getMessage(), $ctlr);
return;
}
throw new InvalidArgumentException('Return value from _getDeleteForm must be an array');
}
if ($ctlr->_isDeleteSaved($id) and Text::containsFormTag($main['content'])) {
throw new Exception("Delete view must not include the form tag");
}
$view = new View('sprout/admin/main_layout');
$this->setDefaultMainviewParams($view);
$this->setNavigation($view, $ctlr);
$single = Inflector::singular($ctlr->getFriendlyName());
$content = '<form action="admin/delete_save/' . Enc::html($ctlr->getControllerName()) . '/' . $id . '" method="post" id="edit-form">';
$content .= Csrf::token();
$content .= '<div class="mainbar-with-right-sidebar">';
$content .= $main['content'];
$content .= '</div>';
$content .= '<div class="right-sidebar">';
$content .= '<div class="right-sidebar-inner">';
$content .= '<div class="save-changes-box">';
$content .= '<h2 class="icon-before icon-delete">Delete ' . Enc::html($single) . '</h2>';
$content .= $this->renderSubActions($ctlr->_getDeleteSubActions($id));
$content .= '<div class="save-changes-box-bottom -clearfix">';
$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>';
$content .= '</div>';
$content .= '</div>';
$content .= '</div>';
$content .= '</div>';
$content .= '</form>';
$view->browser_title = Text
::limitChars(strip_tags($main['title']), 50, '...'); $view->main_title = $main['title'];
$view->main_content = $content;
$view->main_class = 'delete';
echo $view->render();
}
/**
* Executes the delete action for a specific item
*
* @param string $type The Type of the item to delete
* @param int $id The id of the record to delete
**/
public function deleteSave($type, $id)
{
AdminAuth::checkLogin();
Csrf::checkOrDie();
$ctlr = $this->getController($type);
if (! $ctlr) return;
if (! $this->checkAccess($ctlr, 'delete', true)) return;
if (! $this->checkRecordAccess($ctlr, $id)) return;
try {
$ctlr->_deletePreSave($id);
} catch (Exception $ex) {
Notification::error($ex->getMessage());
Url::redirect("admin/delete/{$type}/{$id}");
}
$result = false;
try {
$result = $ctlr->_deleteSave($id);
} catch (ConstraintQueryException $ex) {
$item_name = Inflector::singular($ctlr->getFriendlyName());
Notification::error("This {$item_name} is in use and can't be deleted");
Url::redirect("admin/edit/{$type}/{$id}");
}
if ($result) $ctlr->_deletePostSave($id);
$tag_result = Tags
::update($ctlr->getTableName(), $id, array()); if (! $tag_result) $result = false;
if (Request::isAjax()) {
$result = (int) $result;
}
if ($result == false) {
Notification::error('There was a database error deleting the specified item');
Url::redirect("admin/delete/{$type}/{$id}");
}
$ctlr->_invalidateCaches('delete', $id);
Notification::confirm('Deletion was successful');
Url::redirect($result);
} else {
Url::redirect("admin/contents/{$type}");
}
}
/**
* Shows a duplication form for the specified item
* This uses the edit form with some string replacements
*
* @param string $type The type of item to show the duplication form of
* @param int $id The id of the record to duplicate
**/
public function duplicate($type, $id)
{
AdminAuth::checkLogin();
$id = (int) $id;
$view = new View('sprout/admin/main_layout');
$this->setDefaultMainviewParams($view);
$view->has_tags = true;
$ctlr = $this->getController($type);
if (! $ctlr) return;
if (! $this->checkAccess($ctlr, 'edit', false)) return;
if (! $this->checkRecordAccess($ctlr, $id)) return;
if (! $ctlr->getDuplicateEnabled()) {
$this->error("Duplication is not enabled for this controller");
return;
}
$this->setNavigation($view, $ctlr);
$main = $ctlr->_getDuplicateForm($id);
if ($main instanceof AdminError) {
$this->error($main->getMessage(), $ctlr);
return;
}
if (! is_array($main)) $main = array('title' => $ctlr->getFriendlyName(), 'content' => $main);
if (Text::containsFormTag($main['content'])) {
throw new Exception("Duplicate view must not include the form tag");
}
// Create tags area, and inject it into content after the <form> tag
$tags = new View('sprout/admin/main_tags');
$tags->suggestions = Tags::suggestTags($ctlr->getTableName());
$tags->table = $ctlr->getTableName();
// Inject tags UI
$tags->current_tags = @$_SESSION['admin']['tags'];
if (empty($_SESSION['admin']['tags'])) { $tags->current_tags = implode(', ', Tags
::byRecord($ctlr->getTableName(), $id)); }
unset ($_SESSION['admin']['tags']);
// Rejig the edit form to be about duplication instead of editing
$name_find = array ('edit_save', 'editing', 'Editing', 'Save changes'); $name_replace = array ('duplicate_save', 'duplicating', 'Duplicating', 'Duplicate'); $main['content'] = str_replace($name_find, $name_replace, $main['content']); $main['title'] = str_replace($name_find, $name_replace, $main['title']);
if ($ctlr->_isEditSaved($id)) {
$single = Inflector::singular($ctlr->getFriendlyName());
$content = '<form action="admin/duplicate_save/' . Enc::html($ctlr->getControllerName()) . '/' . $id . '" method="post" id="edit-form">';
$content .= Csrf::token();
$content .= '<div class="mainbar-with-right-sidebar">';
$content .= $tags->render();
$content .= $main['content'];
$content .= '</div>';
$content .= '<div class="right-sidebar">';
$content .= '<div class="right-sidebar-inner">';
$content .= '<div class="save-changes-box">';
$html = $ctlr->_getCustomDuplicateSaveHTML($id);
if ($html) {
$content .= $html;
} else {
$visibility = $ctlr->_getVisibilityFields();
$sub_actions = $ctlr->_getDuplicateSubActions($id);
$content .= '<h2 class="icon-before icon-save">Duplicate ' . Enc::html($single) . '</h2>';
if (!empty($visibility)) { Form::nextFieldDetails('Visibility', false);
$content .= Form::checkboxBoolList(null, [], $visibility);
}
$content .= '<div class="save-changes-box-bottom -clearfix">';
if (!empty($sub_actions['_preview'])) { $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>';
}
$content .= '<button type="submit" class="save-changes-save-button button button-regular button-green icon-after icon-save">Save changes</button>';
$content .= '</div>';
}
$content .= '</div>';
$content .= '</div>';
$content .= '</div>';
$content .= '</form>';
} else {
$content = $main['content'];
}
$this->lock($type, $id, $view);
// Render the main view
$view->browser_title = Text
::limitChars(strip_tags($main['title']), 50, '...'); $view->main_title = $main['title'];
$view->main_content = $content;
$view->main_class = 'do-action-box';
echo $view->render();
}
/**
* Executes the save action for a record duplication
*
* @param string $type The type of item to save
* @param int $id The id of the record to save
**/
public function duplicateSave($type, $orig_id)
{
AdminAuth::checkLogin();
Csrf::checkOrDie();
$orig_id = (int) $orig_id;
$ctlr = $this->getController($type);
if (! $ctlr) return;
if (! $this->checkAccess($ctlr, 'edit', true)) return;
if (! $this->checkRecordAccess($ctlr, $orig_id)) return;
$this->cleanupCommonPostData($ctlr);
$_SESSION['admin']['tags'] = $_POST['tags'];
// Start transaction
Pdb::transact();
// Create new id
// Nasty hack to prevent errors with null values in fields with foreign key constraints
// This shouldn't be an issue as the actual data from the POST submission should comply with the constraints
Pdb::q('SET foreign_key_checks = 0', [], 'count');
$id = Pdb
::insert($ctlr->getTableName(), array('date_added' => Pdb
::now()));
// Set "id" columns from multiedit records to zero for force an insert
foreach ($_POST as $key => &$val) {
foreach ($val as &$multiedit_row) {
$multiedit_row['id'] = 0;
}
}
}
$result = $ctlr->_duplicateSave($id);
// Re-enable foreign key constraints now that the real data has been saved
Pdb::q('SET foreign_key_checks = 1', [], 'count');
// Commit
if ($result == true) {
// Copy across per-record permissions, or create new ones if none exist for the original record
try {
$perms = PerRecordPerms::fetchDetails($ctlr, $orig_id);
$_POST['_prm_categories'] = $perms['categories'];
PerRecordPerms::save($ctlr, $id);
} catch (RowMissingException $ex) {
$_POST['_prm_categories'] = '*';
PerRecordPerms::save($ctlr, $id);
}
Pdb::commit();
}
if (Request::isAjax()) {
$result = (int) $result;
return;
}
if ($result == false) {
Notification::error('There was an error saving your changes');
Url::redirect("admin/duplicate/{$type}/{$orig_id}");
}
$new_tags = Tags::splitupTags($_POST['tags']);
$tag_result = Tags::update($ctlr->getTableName(), $id, $new_tags);
unset ($_SESSION['admin']['tags']);
if ($tag_result == false) {
Notification::error('There was an error updating the tags for this item');
}
$ctlr->_invalidateCaches('duplicate', $id);
unset ($_SESSION['admin']['field_values']); Notification::confirm('Your changes have been saved');
Url::redirect("admin/edit/{$type}/{$id}");
}
/**
* Moderation
**/
public function moderate()
{
AdminAuth::checkLogin();
$view = new View('sprout/admin/main_layout');
$this->setDefaultMainviewParams($view);
$this->setNavigation($view, new PageAdminController());
$moderators = Register::getModerators();
if (count($moderators) == 0) { $this->error("No moderation classes are registered.");
return;
}
$out = '<form action="SITE/admin/moderate_action" method="post">';
$out .= Csrf::token();
$idx = 0;
foreach ($moderators as $class) {
$inst = Sprout::instance($class, 'Sprout\Helpers\Moderate');
$out .= '<h3>' . Enc::html($inst->getFriendlyName()) . '</h3>';
$list = $inst->getList();
if ($list === null) {
$out .= '<p><i>Error: Unable to load record list for moderation.</i></p>';
continue;
}
$out .= '<p><i>Nothing needs moderation.</i></p>';
continue;
}
$out .= '<table class="main-list main-list-no-js moderation">';
$out .= '<thead>';
$out .= '<tr><th>Item details</th><th class="mod">Approve</th><th class="mod">Delete</th><th class="mod">Do nothing</th></tr>';
$out .= '</thead><tbody>';
foreach ($list as $id => $html) {
$idx++;
$out .= '<tr>';
$out .= '<td>' . $html . '</td>';
$out .= "<td class=\"mod\"><input type=\"radio\" name=\"moderate[{$class}][{$id}]\" value=\"app\" checked></td>";
$out .= "<td class=\"mod\"><input type=\"radio\" name=\"moderate[{$class}][{$id}]\" value=\"del\"></td>";
$out .= "<td class=\"mod\"><input type=\"radio\" name=\"moderate[{$class}][{$id}]\" value=\"\"></td>";
$out .= '</tr>';
}
$out .= '</tbody></table>';
}
$out .= '<div class="action-bar">';
$out .= '<button type="submit" class="button button-regular button-green icon-after icon-save">Save changes</button>';
$out .= '</div>';
$out .= '</form>';
$view->browser_title = 'Content Moderation';
$view->main_title = 'Content Moderation';
$view->main_content = $out;
echo $view->render();
}
/**
* Processes the moderation form
**/
public function moderateAction()
{
AdminAuth::checkLogin();
Csrf::checkOrDie();
if (@!is_array($_POST['moderate'])) $_POST['moderate'] = array();
Pdb::transact();
$approve = 0;
$delete = 0;
foreach ($_POST['moderate'] as $class => $records) {
$inst = Sprout::instance($class, 'Sprout\Helpers\Moderate');
foreach ($records as $id => $do) {
$id = (int) $id;
if ($do == 'app') {
$inst->approve($id);
$approve++;
} else if ($do == 'del') {
$inst->delete($id);
$delete++;
}
}
}
Pdb::commit();
if ($approve) Notification::confirm('Approved ' . $approve . ' record' . ($approve != 1 ? 's.' : '.'));
if ($delete) Notification::confirm('Deleted ' . $delete . ' record' . ($delete != 1 ? 's.' : '.'));
Url::redirect('admin/moderate');
}
/**
* Calls any 'extra' commands that might be provided by the controller
*
* Method names will be prefixed with '_extra' and must be public
* Names which are lower_cased will be converted to camelCase
*
* Supports varargs - additional args are passed to the underlying function
* Called method should return an array, with two keys:
* title string Main title
* content string HTML for the main content area
*
* @example
* class BookingAdminController extends ManagedAdminController {
* // Call with url admin/extra/booking/send_email/:record_id
* public function _extraSendEmail($record_id) {
* return ['title' => '...', 'content' => '...'];
* }
* }
*
* @param string $type The class name of the method to call (must extend ManagedAdminController)
* @param string $method The method name to call
* @return void Outputs HTML
**/
public function extra($class, $method)
{
AdminAuth::checkLogin();
throw new InvalidArgumentException('Invalid method specified');
}
$method = '_extra' . ucfirst(Text
::lc2camelCase($method));
$ctlr = $this->getController($class);
try {
$reflect = new ReflectionMethod($ctlr, $method);
} catch (ReflectionException $ex) {
throw new InvalidArgumentException('Method "' . $method . '" does not exist');
}
if (!$reflect->isPublic()) {
throw new InvalidArgumentException('Method "' . $method . '" does not exist');
}
$view = new View('sprout/admin/main_layout');
$this->setDefaultMainviewParams($view);
$this->setNavigation($view, $ctlr);
if ($main instanceof AdminError) {
$this->error($main->getMessage(), $ctlr);
return;
}
$main = [
'title' => $ctlr->getFriendlyName(),
'content' => $main
];
}
$view->browser_title = strip_tags($main['title']); $view->main_title = $main['title'];
$view->main_content = $main['content'];
echo $view->render();
}
/**
* Directly calls a method provided by an admin controller
* Suports varargs - additional args are passed to the underlying function
*
* @param string $class The shorthand class name, e.g. 'page'
* @param string $method The method name, e.g. 'reorder_top'
* @return void Does whatever the called function does, e.g. echo or redirect
*/
public function call($class, $method)
{
AdminAuth::checkLogin();
$ctlr = $this->getController($class);
if (!$ctlr or !($ctlr instanceof ManagedAdminController)) {
throw new InvalidArgumentException('Controller "' . $class . '" does not exist');
}
throw new InvalidArgumentException('Method "' . $method . '" does not exist');
}
$reflect = new ReflectionMethod($ctlr, $method);
if (!$reflect->isPublic()) {
throw new InvalidArgumentException('Method "' . $method . '" does not exist');
}
}
/**
* Sets the active subsite, and then redirects back to the admin area.
* Uses the post variable "subsite".
**/
public function setActiveSubsite()
{
AdminAuth::checkLogin();
Csrf::checkOrDie();
$_POST['subsite'] = (int) @$_POST['subsite'];
if ($_POST['subsite'] <= 0) {
die('Invalid POST data'); }
// Does the operator actually have access to edit this subsite?
if (!AdminPerms::canAccessSubsite($_POST['subsite'])) {
Notification::error('Access denied');
Url::redirect(Kohana::config('sprout.admin_intro'));
}
$_SESSION['admin']['active_subsite'] = $_POST['subsite'];
Notification::confirm('Subsite changed');
Url::redirect(Kohana::config('sprout.admin_intro'));
}
/**
* Sets up the sidebar navigation for a view to show the navigation for a specific controller.
*
* @param View $view The view to set the navigation parameters for.
* @param Controller $ctlr The controller to use for navigation (and searching if supported).
**/
private function setNavigation(View $view, Controller $ctlr)
{
// If no navigation has been set, use the default
$view->nav = $ctlr->_getNavigation();
}
$view->controller_name = $ctlr->getControllerName();
$view->controller_navigation_name = $ctlr->getNavigationName();
$view->nav_tools = $ctlr->_getTools();
}
/**
* Sets the a bunch of parameters for a the main view.
*
* @param View $view The view to set the parameters for.
**/
private function setDefaultMainviewParams($view)
{
$view->admin_authenticated = true;
// Browser version checks. FF3+, IE7+
$browser_ok = false;
if (Kohana::userAgent('browser') == 'Firefox') {
$browser_ok = true;
} else if (Kohana::userAgent('browser') == 'Chrome') {
$browser_ok = true;
} else if (Kohana
::userAgent('browser') == 'Internet Explorer' and
version_compare(Kohana
::userAgent('version'), '7.0', '>=')) { $browser_ok = true;
}
// Set a message if the browser is not supported.
if (! $browser_ok) {
$view->info_message = new View('sprout/admin/message_bad_browser');
}
// Header under the sprout logo
$view->header_subtitle = '';
if (!empty($_SESSION['admin'])) { $view->live_url = Subsites::getAbsRoot($_SESSION['admin']['active_subsite']);
}
}
/**
* Returns an instance of a controller class for a given type
*
* @deprecated This function is now just an alias for {@see Admin::getController}
* @param string $type A class name, or shorthand identifier
* e.g. 'Sprout\Controllers\AwesomeController' or 'awesome'
* @return Controller
* @throws Exception If the class is unknown
* @todo Handle module autoloading, e.g. should be able to specify 'thingy'
* and get SproutModules\AwesomeDeveloper\Controllers\ThingyController
**/
private function getController($type)
{
return Admin::getController($type);
}
/**
* Does lock checking, locking, or lock messages.
*
* @param string $type Admin controller slug, e.g. 'page'
* @param int $id Record id which is being edited
* @param View $view Main layout view to provide lock details into
*/
private function lock($type, $id, View $view)
{
if (! Admin::locksEnabled()) return;
$type = (string) $type;
$id = (int) $id;
$lock = Admin::getLock($type, $id);
if ($lock == null) {
// No lock; acquire it
$lock_id = Admin::lock($type, $id);
$view->currlock = [
'id' => (int)$lock_id,
'ctlr' => $type,
'record_id' => $id,
'edit_token' => Csrf::getTokenValue(),
];
} else if ($lock['lock_key'] == $_SESSION['admin']['lock_key']) {
// Is locked to this session
Admin::pingLock($lock['id']);
$view->currlock = [
'id' => (int)$lock['id'],
'ctlr' => $type,
'record_id' => $id,
'edit_token' => Csrf::getTokenValue(),
];
} else {
// Locked to a different session
$view->locked = $lock;
}
}
/**
* Unlocks lock for a given record/controller, all records for a controller, or all locks
**/
private function unlock($type = null, $id = null)
{
Admin::unlock($type, $id);
}
/**
* Unlock a record.
* Called via ajax in the beforeunload javascript event
**/
public function ajaxUnlock()
{
AdminAuth::checkLogin();
if (!Csrf::check()) {
die('Session timeout or missing security token'); }
Admin::unlock($_POST['ctlr'], $_POST['record_id']);
echo '.';
}
/**
* Restore a deleted record from log data
* @param int $log_id ID in the history_items table
* @return void
*/
public function restore($log_id)
{
AdminAuth::checkLogin();
Csrf::checkOrDie();
$log_id = (int) $log_id;
// Gather data
$q = "SELECT id, record_id, record_table, controller, data
FROM ~history_items
WHERE id = ? AND type = 'Delete'";
$records = Pdb::q($q, [$log_id], 'map-arr');
if (count($records) == 0) { throw new Kohana_404_Exception();
}
$main_record = Sprout::iterableFirstValue($records);
$new = $records;
while (count($new) > 0) { $params = [];
$q = "SELECT id, record_table, data FROM ~history_items WHERE type = 'Delete' AND ";
$q .= Pdb
::buildClause([['parent_id', 'IN', array_keys($new)]], $params); $new = Pdb::q($q, $params, 'map-arr');
foreach ($new as $id => $data) {
$records[$id] = $data;
}
}
// Restore data
Pdb::transact();
foreach ($records as $row) {
Pdb::validateIdentifier($row['record_table']);
$cols = $values = '';
foreach ($data as $field => $value) {
Pdb::validateIdentifier($field);
if ($cols) {
$cols .= ', ';
$values .= ', ';
}
$cols .= $field;
$values .= ':' . $field;
}
$q = "INSERT INTO ~{$row['record_table']} ({$cols}) VALUES ({$values})";
try {
Pdb::q($q, $data, 'null');
} catch (QueryException $ex) {
Pdb::rollback();
Notification::error('Database error during restore');
Url::redirect('admin/edit/action_log/' . $log_id);
}
}
$op = AdminAuth::getDetails();
$data = ['restored_date' => Pdb::now(), 'restored_operator' => @$op['name']];
Pdb::update('history_items', $data, ['id' => $log_id]);
Pdb::commit();
Notification::confirm('Data has been restored');
$ctlr_class = $main_record['controller'];
$ctlr = new $ctlr_class();
Url::redirect('admin/edit/' . Enc::url($ctlr->getControllerName()) . '/' . $main_record['record_id']);
} else {
Url::redirect('admin/edit/action_log/' . $log_id);
}
}
/**
* Browser information
* @return void Echos HTML
*/
public function userAgent()
{
$data = UserAgent::getInfo();
$data['full_ua'] = $_SERVER['HTTP_USER_AGENT'];
$data['body_classes'] = UserAgent::getBodyClasses();
echo '<style>';
echo 'table { border-collapse: collapse; margin: 50px auto; }';
echo 'h1,p { font-family: sans-serif; text-align: center; margin: 50px auto; }';
echo 'td { font-family: sans-serif; padding: 10px 15px; border: 1px #eee solid; }';
echo '</style>';
echo '<h1>User-agent</h1>';
echo "<table>\n";
foreach ($data as $field => $val) {
echo "<tr>\n";
echo "<td><b>", Enc::html($field), "</b></td>\n";
echo "<td>", Enc::html($val), "</td>\n";
echo "</tr>\n";
}
echo "</table>\n";
echo '<p>Uses data from the <a href="https://github.com/Karmabunny/user-agents.json">user-agents.json</a> project.</p>';
}
/**
* Activates AutoLaunch revisions
**/
public function cronGenericActivate()
{
Cron::start('Generic autolaunch system');
$tbl_prefix = Pdb::prefix();
// Find autolaunch/autonuke tables
$q = "SHOW TABLE STATUS";
$db_tables = Pdb::query($q, [], 'pdo');
foreach ($db_tables as $tbl) {
if (strpos($tbl['Name'], $tbl_prefix) !== 0) { continue;
}
if ($tbl['Name'] === "{$tbl_prefix}page_revisions") {
continue;
}
$q = "SHOW COLUMNS FROM {$tbl['Name']}";
$db_cols = Pdb::query($q, [], 'pdo');
$tables[$tbl['Name']] = 0;
foreach ($db_cols as $col) {
if ($col['Field'] == 'date_launch') $tables[$tbl['Name']]++;
if ($col['Field'] == 'date_expire') $tables[$tbl['Name']]++;
if ($col['Field'] == 'active') $tables[$tbl['Name']]++;
}
}
Pdb::transact();
foreach ($tables as $tbl => $num_cols) {
if ($num_cols !== 3) continue;
Cron::message("Processing table {$tbl}");
try {
// Launch
$q = "SELECT id
FROM {$tbl}
WHERE active = 0
AND date_launch != '0000-00-00'
AND date_launch IS NOT NULL
AND date_launch <= NOW()
AND (
date_expire > NOW()
OR date_expire = '0000-00-00'
OR date_expire IS NULL
)";
$res = Pdb::q($q, [], 'arr');
foreach ($res as $row) {
Cron::message("Activating record {$row['id']}");
Pdb::update($tbl_no_prefix, ['active' => 1], ['id' => $row['id']]);
}
// Unlaunch
$q = "SELECT id
FROM {$tbl}
WHERE active = 1
AND date_expire != '0000-00-00'
AND date_expire IS NOT NULL
AND date_expire < NOW()";
$res = Pdb::q($q, [], 'arr');
foreach ($res as $row) {
Cron::message("Expiring record {$row['id']}");
Pdb::update($tbl_no_prefix, ['active' => 0], ['id' => $row['id']]);
}
} catch (QueryException $ex) {
return Cron::failure('Database error');
}
}
Pdb::commit();
Cron::success();
}
public function heartbeat()
{
AdminAuth::checkLogin();
echo "Ah ah ah ah, stayin' alive, stayin' alive...";
// We piggyback the heartbeat to keep locks up-to-date
if (isset($_GET['lock_id'])) { Admin::pingLock($_GET['lock_id']);
Admin::clearOldLocks();
}
}
}