implement task authorization with AJAX support [fix #47]

This commit is contained in:
Gert
2015-05-11 16:37:43 +02:00
parent d92cb462ea
commit 52acf6304e
10 changed files with 268 additions and 74 deletions

View File

@@ -260,13 +260,13 @@ class AdminPlugin extends Plugin
]; ];
echo json_encode([ echo json_encode([
"success" => true, "status" => "success",
"payload" => ["resources" => $resources_updates, "grav" => $grav_updates, "installed" => $gpm->countInstalled()] "payload" => ["resources" => $resources_updates, "grav" => $grav_updates, "installed" => $gpm->countInstalled()]
]); ]);
break; break;
} }
} catch (\Exception $e) { } catch (\Exception $e) {
echo json_encode(["success" => false, "message" => $e->getMessage()]); echo json_encode(["status" => "error", "message" => $e->getMessage()]);
} }
exit; exit;

View File

@@ -259,11 +259,15 @@ class AdminController
protected function taskClearCache() protected function taskClearCache()
{ {
if (!$this->authoriseTask('clear cache', ['admin.cache', 'admin.super'])) {
return;
}
$results = Cache::clearCache('standard'); $results = Cache::clearCache('standard');
if (count($results) > 0) { if (count($results) > 0) {
$this->admin->json_response = ['success', 'Cache cleared']; $this->admin->json_response = ['status' => 'success', 'message' => 'Cache cleared'];
} else { } else {
$this->admin->json_response = ['error', 'Error clearing cache']; $this->admin->json_response = ['status' => 'error', 'message' => 'Error clearing cache'];
} }
return true; return true;
@@ -271,6 +275,10 @@ class AdminController
protected function taskBackup() protected function taskBackup()
{ {
if (!$this->authoriseTask('backup', ['admin.maintenance', 'admin.super'])) {
return;
}
$download = $this->grav['uri']->param('download'); $download = $this->grav['uri']->param('download');
if ($download) { if ($download) {
@@ -313,6 +321,10 @@ class AdminController
protected function taskFilterPages() protected function taskFilterPages()
{ {
if (!$this->authoriseTask('filter pages', ['admin.pages', 'admin.super'])) {
return;
}
$data = $this->post; $data = $this->post;
$flags = !empty($data['flags']) ? array_map('strtolower', explode(',', $data['flags'])) : []; $flags = !empty($data['flags']) ? array_map('strtolower', explode(',', $data['flags'])) : [];
@@ -344,16 +356,29 @@ class AdminController
} }
} }
$this->admin->json_response = ['success', 'Pages filtered']; $results = [];
foreach ($collection as $path => $page) {
$results[] = $page->route();
}
$this->admin->json_response = [
'status' => 'success',
'message' => 'Pages filtered',
'results' => $results
];
$this->admin->collection = $collection; $this->admin->collection = $collection;
} }
protected function taskListmedia() protected function taskListmedia()
{ {
if (!$this->authoriseTask('list media', ['admin.pages', 'admin.super'])) {
return;
}
$page = $this->admin->page(true); $page = $this->admin->page(true);
if (!$page) { if (!$page) {
$this->admin->json_response = ['error', 'No Page found']; $this->admin->json_response = ['status' => 'error', 'message' => 'No Page found'];
return false; return false;
} }
@@ -361,20 +386,24 @@ class AdminController
foreach ($page->media()->all() as $name => $media) { foreach ($page->media()->all() as $name => $media) {
$media_list[$name] = ['url' => $media->cropZoom(150, 100)->url(),'size' => $media->get('size')]; $media_list[$name] = ['url' => $media->cropZoom(150, 100)->url(),'size' => $media->get('size')];
} }
$this->admin->media = $media_list; $this->admin->json_response = ['status' => 'ok', 'results' => $media_list];
return true; return true;
} }
protected function taskAddmedia() protected function taskAddmedia()
{ {
if (!$this->authoriseTask('add media', ['admin.pages', 'admin.super'])) {
return;
}
$page = $this->admin->page(true); $page = $this->admin->page(true);
/** @var Config $config */ /** @var Config $config */
$config = $this->grav['config']; $config = $this->grav['config'];
if (!isset($_FILES['file']['error']) || is_array($_FILES['file']['error'])) { if (!isset($_FILES['file']['error']) || is_array($_FILES['file']['error'])) {
$this->admin->json_response = ['error', 'Invalid Parameters']; $this->admin->json_response = ['status' => 'error', 'message' => 'Invalid Parameters'];
return; return;
} }
@@ -383,21 +412,21 @@ class AdminController
case UPLOAD_ERR_OK: case UPLOAD_ERR_OK:
break; break;
case UPLOAD_ERR_NO_FILE: case UPLOAD_ERR_NO_FILE:
$this->admin->json_response = ['error', 'No files sent']; $this->admin->json_response = ['status' => 'error', 'message' => 'No files sent'];
return; return;
case UPLOAD_ERR_INI_SIZE: case UPLOAD_ERR_INI_SIZE:
case UPLOAD_ERR_FORM_SIZE: case UPLOAD_ERR_FORM_SIZE:
$this->admin->json_response = ['error', 'Exceeded filesize limit.']; $this->admin->json_response = ['status' => 'error', 'message' => 'Exceeded filesize limit.'];
return; return;
default: default:
$this->admin->json_response = ['error', 'Unkown errors']; $this->admin->json_response = ['status' => 'error', 'message' => 'Unkown errors'];
return; return;
} }
$grav_limit = $config->get('system.media.upload_limit', 0); $grav_limit = $config->get('system.media.upload_limit', 0);
// You should also check filesize here. // You should also check filesize here.
if ($grav_limit > 0 && $_FILES['file']['size'] > grav_limit) { if ($grav_limit > 0 && $_FILES['file']['size'] > grav_limit) {
$this->admin->json_response = ['error', 'Exceeded Grav filesize limit.']; $this->admin->json_response = ['status' => 'error', 'message' => 'Exceeded Grav filesize limit.'];
return; return;
} }
@@ -408,28 +437,32 @@ class AdminController
// If not a supported type, return // If not a supported type, return
if (!$config->get("media.{$fileExt}")) { if (!$config->get("media.{$fileExt}")) {
$this->admin->json_response = ['error', 'Unsupported file type: '.$fileExt]; $this->admin->json_response = ['status' => 'error', 'message' => 'Unsupported file type: '.$fileExt];
return; return;
} }
// Upload it // Upload it
if (!move_uploaded_file($_FILES['file']['tmp_name'], sprintf('%s/%s', $page->path(), $_FILES['file']['name']))) { if (!move_uploaded_file($_FILES['file']['tmp_name'], sprintf('%s/%s', $page->path(), $_FILES['file']['name']))) {
$this->admin->json_response = ['error', 'Failed to move uploaded file.']; $this->admin->json_response = ['status' => 'error', 'message' => 'Failed to move uploaded file.'];
return; return;
} }
$this->admin->json_response = ['success', 'File uploaded successfully']; $this->admin->json_response = ['status' => 'success', 'message' => 'File uploaded successfully'];
return; return;
} }
protected function taskDelmedia() protected function taskDelmedia()
{ {
if (!$this->authoriseTask('delete media', ['admin.pages', 'admin.super'])) {
return;
}
$page = $this->admin->page(true); $page = $this->admin->page(true);
if (!$page) { if (!$page) {
$this->admin->json_response = ['error', 'No Page found']; $this->admin->json_response = ['status' => 'error', 'message' => 'No Page found'];
return false; return false;
} }
@@ -439,15 +472,15 @@ class AdminController
if (file_exists($targetPath)) { if (file_exists($targetPath)) {
if (unlink($targetPath)) { if (unlink($targetPath)) {
$this->admin->json_response = ['success', 'File deleted: '.$filename]; $this->admin->json_response = ['status' => 'success', 'message' => 'File deleted: '.$filename];
} else { } else {
$this->admin->json_response = ['error', 'File could not be deleted: '.$filename]; $this->admin->json_response = ['status' => 'error', 'message' => 'File could not be deleted: '.$filename];
} }
} else { } else {
$this->admin->json_response = ['error', 'File not found: '.$filename]; $this->admin->json_response = ['status' => 'error', 'message' => 'File not found: '.$filename];
} }
} else { } else {
$this->admin->json_response = ['error', 'No file found']; $this->admin->json_response = ['status' => 'error', 'message' => 'No file found'];
} }
return true; return true;
} }
@@ -459,6 +492,10 @@ class AdminController
*/ */
public function taskEnable() public function taskEnable()
{ {
if (!$this->authoriseTask('enable plugin', ['admin.plugins', 'admin.super'])) {
return;
}
if ($this->view != 'plugins') { if ($this->view != 'plugins') {
return false; return false;
} }
@@ -479,6 +516,10 @@ class AdminController
*/ */
public function taskDisable() public function taskDisable()
{ {
if (!$this->authoriseTask('disable plugin', ['admin.plugins', 'admin.super'])) {
return;
}
if ($this->view != 'plugins') { if ($this->view != 'plugins') {
return false; return false;
} }
@@ -499,6 +540,10 @@ class AdminController
*/ */
public function taskActivate() public function taskActivate()
{ {
if (!$this->authoriseTask('activate theme', ['admin.themes', 'admin.super'])) {
return;
}
if ($this->view != 'themes') { if ($this->view != 'themes') {
return false; return false;
} }
@@ -534,6 +579,11 @@ class AdminController
*/ */
public function taskInstall() public function taskInstall()
{ {
$type = $this->view === 'plugins' ? 'plugins' : 'themes';
if (!$this->authoriseTask('install ' . $type, ['admin.' . $type, 'admin.super'])) {
return;
}
require_once __DIR__ . '/gpm.php'; require_once __DIR__ . '/gpm.php';
$package = $this->route; $package = $this->route;
@@ -561,6 +611,7 @@ class AdminController
require_once __DIR__ . '/gpm.php'; require_once __DIR__ . '/gpm.php';
$package = $this->route; $package = $this->route;
$permissions = [];
// Update multi mode // Update multi mode
if (!$package) { if (!$package) {
@@ -568,10 +619,18 @@ class AdminController
if ($this->view === 'plugins' || $this->view === 'update') { if ($this->view === 'plugins' || $this->view === 'update') {
$package = $this->admin->gpm()->getUpdatablePlugins(); $package = $this->admin->gpm()->getUpdatablePlugins();
$permissions['plugins'] = ['admin.super', 'admin.plugins'];
} }
if ($this->view === 'themes' || $this->view === 'update') { if ($this->view === 'themes' || $this->view === 'update') {
$package = array_merge($package, $this->admin->gpm()->getUpdatableThemes()); $package = array_merge($package, $this->admin->gpm()->getUpdatableThemes());
$permissions['themes'] = ['admin.super', 'admin.themes'];
}
}
foreach ($permissions as $type => $p) {
if (!$this->authoriseTask('update ' . $type , $p)) {
return;
} }
} }
@@ -580,9 +639,9 @@ class AdminController
if ($this->view === 'update') { if ($this->view === 'update') {
if ($result) { if ($result) {
$this->admin->json_response = ['success', 'Everything updated']; $this->admin->json_response = ['status' => 'success', 'message' => 'Everything updated'];
} else { } else {
$this->admin->json_response = ['error', 'Updates failed']; $this->admin->json_response = ['status' => 'error', 'message' => 'Updates failed'];
} }
} else { } else {
@@ -605,6 +664,11 @@ class AdminController
*/ */
public function taskUninstall() public function taskUninstall()
{ {
$type = $this->view === 'plugins' ? 'plugins' : 'themes';
if (!$this->authoriseTask('uninstall ' . $type, ['admin.' . $type, 'admin.super'])) {
return;
}
require_once __DIR__ . '/gpm.php'; require_once __DIR__ . '/gpm.php';
$package = $this->route; $package = $this->route;
@@ -629,6 +693,10 @@ class AdminController
*/ */
public function taskSave() public function taskSave()
{ {
if (!$this->authoriseTask('save', $this->dataPermissions())) {
return;
}
$data = $this->post; $data = $this->post;
// Special handler for pages data. // Special handler for pages data.
@@ -718,6 +786,10 @@ class AdminController
*/ */
protected function taskCopy() protected function taskCopy()
{ {
if (!$this->authoriseTask('copy page', ['admin.pages', 'admin.super'])) {
return;
}
// Only applies to pages. // Only applies to pages.
if ($this->view != 'pages') { if ($this->view != 'pages') {
return false; return false;
@@ -774,6 +846,10 @@ class AdminController
*/ */
protected function taskReorder() protected function taskReorder()
{ {
if (!$this->authoriseTask('reorder pages', ['admin.pages', 'admin.super'])) {
return;
}
// Only applies to pages. // Only applies to pages.
if ($this->view != 'pages') { if ($this->view != 'pages') {
return false; return false;
@@ -791,6 +867,10 @@ class AdminController
*/ */
protected function taskDelete() protected function taskDelete()
{ {
if (!$this->authoriseTask('delete page', ['admin.pages', 'admin.super'])) {
return;
}
// Only applies to pages. // Only applies to pages.
if ($this->view != 'pages') { if ($this->view != 'pages') {
return false; return false;
@@ -869,6 +949,34 @@ class AdminController
return $data; return $data;
} }
protected function dataPermissions()
{
$type = $this->view;
$permissions = ['admin.super'];
switch ($type) {
case 'configuration':
case 'system':
$permissions = ['admin.configuration'];
break;
case 'settings':
case 'site':
$permissions = ['admin.settings'];
break;
case 'plugins':
$permissions = ['admin.plugins'];
break;
case 'themes':
$permissions = ['admin.themes'];
break;
case 'users':
$permissions = ['admin.users'];
break;
}
return $permissions;
}
protected function preparePage(\Grav\Common\Page\Page $page) protected function preparePage(\Grav\Common\Page\Page $page)
{ {
$input = $this->post; $input = $this->post;
@@ -899,4 +1007,19 @@ class AdminController
$page->content((string) $input['content']); $page->content((string) $input['content']);
} }
} }
protected function authoriseTask($task = '', $permissions = [])
{
if (!$this->admin->authorise($permissions)) {
if ($this->grav['uri']->extension() === 'json')
$this->admin->json_response = ['status' => 'unauthorized', 'message' => 'You have insufficient permissions for task ' . $task . '.'];
else
$this->admin->setMessage('You have insufficient permissions for task ' . $task . '.');
return false;
}
return true;
}
} }

View File

@@ -58,14 +58,11 @@ $(function () {
GravAjax({ GravAjax({
dataType: "json", dataType: "json",
url: url, url: url,
toastErrors: true,
success: function(result, status) { success: function(result, status) {
if (result.status == 'success') {
toastr.success(result.message); toastr.success(result.message);
} else {
toastr.error(result.message);
} }
} }).always(function() {
}).complete(function() {
$('[data-clear-cache]').removeAttr('disabled').find('> .fa').removeClass('fa-refresh fa-spin').addClass('fa-trash'); $('[data-clear-cache]').removeAttr('disabled').find('> .fa').removeClass('fa-refresh fa-spin').addClass('fa-trash');
}); });
}); });
@@ -79,14 +76,11 @@ $(function () {
GravAjax({ GravAjax({
dataType: "json", dataType: "json",
url: url, url: url,
toastErrors: true,
success: function(result, status) { success: function(result, status) {
if (result.status == 'success') {
toastr.success(result.message); toastr.success(result.message);
} else {
toastr.error(result.message);
} }
} }).always(function() {
}).complete(function() {
GPMRefresh(); GPMRefresh();
$('[data-maintenance-update]').removeAttr('disabled').find('> .fa').removeClass('fa-refresh fa-spin').addClass('fa-cloud-download'); $('[data-maintenance-update]').removeAttr('disabled').find('> .fa').removeClass('fa-refresh fa-spin').addClass('fa-cloud-download');
}); });
@@ -120,6 +114,7 @@ $(function () {
GravAjax({ GravAjax({
dataType: "json", dataType: "json",
url: url, url: url,
toastErrors: true,
success: function(result, status) { success: function(result, status) {
var toastrBackup = {}; var toastrBackup = {};
@@ -131,18 +126,14 @@ $(function () {
} }
} }
if (result.status == 'success') {
toastr.success(result.message || 'Task completed.'); toastr.success(result.message || 'Task completed.');
} else {
toastr.error(result.message || 'Something went terribly wrong.');
}
for (var setting in toastrBackup) { if (toastrBackup.hasOwnProperty(setting)) { for (var setting in toastrBackup) { if (toastrBackup.hasOwnProperty(setting)) {
toastr.options[setting] = toastrBackup[setting]; toastr.options[setting] = toastrBackup[setting];
} }
} }
} }
}).complete(function() { }).always(function() {
// Restore button // Restore button
button.removeAttr('disabled'); button.removeAttr('disabled');
icon.removeClass('fa-refresh fa-spin').addClass(iconClasses.join(' ')); icon.removeClass('fa-refresh fa-spin').addClass(iconClasses.join(' '));
@@ -158,11 +149,8 @@ $(function () {
task: 'GPM', task: 'GPM',
action: 'getUpdates' action: 'getUpdates'
}, },
toastErrors: true,
success: function (response) { success: function (response) {
if (!response.success) {
throw new Error(response.message);
}
var grav = response.payload.grav, var grav = response.payload.grav,
installed = response.payload.installed, installed = response.payload.installed,
resources = response.payload.resources; resources = response.payload.resources;

View File

@@ -5,16 +5,91 @@ $(function(){
settings = typeof settings === 'undefined' ? typeof url === 'string' ? {} : url : settings; settings = typeof settings === 'undefined' ? typeof url === 'string' ? {} : url : settings;
settings.url = typeof settings.url === 'undefined' && typeof url === 'string' ? url : settings.url; settings.url = typeof settings.url === 'undefined' && typeof url === 'string' ? url : settings.url;
var successHandler = typeof settings.success !== 'undefined' ? typeof settings.success === 'function' ? [ settings.success ] : settings.success : []; var callbacks = {
successHandler.unshift(root.GravAjax.logoutHandler); success: typeof settings.success !== 'undefined' ? typeof settings.success === 'function' ? [ settings.success ] : settings.success : [],
settings.success = successHandler; error: typeof settings.error !== 'undefined' ? typeof settings.error === 'function' ? [ settings.error ] : settings.error : []
return $.ajax(settings);
}; };
root.GravAjax.logoutHandler = function (response, status, xhr) {
if (response.status && (response.status === "unauthorized" || response.status === "forbidden")) { if (settings.toastErrors) {
callbacks.error.push(root.GravAjax.toastErrorHandler);
delete settings.toastErrors;
}
delete settings.success;
delete settings.error;
var deferred = $.Deferred(),
jqxhr = $.ajax(settings);
jqxhr.done(function (response, status, xhr) {
var responseObject = {
response: response,
status: status,
xhr: xhr
};
switch (response.status) {
case "unauthenticated":
document.location.href = GravAdmin.config.base_url_relative; document.location.href = GravAdmin.config.base_url_relative;
throw "Logged out"; throw "Logged out";
break;
case "unauthorized":
responseObject.response.message = responseObject.response.message || "Unauthorized.";
root.GravAjax.errorHandler(deferred, callbacks, responseObject);
break;
case "error":
responseObject.response.message = responseObject.response.message || "Unknown error.";
root.GravAjax.errorHandler(deferred, callbacks, responseObject);
break;
case "success":
root.GravAjax.successHandler(deferred, callbacks, responseObject);
break;
default:
responseObject.response.message = responseObject.response.message || "Invalid AJAX response.";
root.GravAjax.errorHandler(deferred, callbacks, responseObject);
break;
} }
});
jqxhr.fail(function (xhr, status, error) {
var response = {
status: 'error',
message: error
};
root.GravAjax.errorHandler(deferred, callbacks, { xhr: xhr, status: status, response: response});
});
return deferred;
};
root.GravAjax.successHandler = function (promise, callbacks, response) {
callbacks = callbacks.success;
for (var i = 0; i < callbacks.length; i++) {
if (typeof callbacks[i] === 'function') {
callbacks[i](response.response, response.status, response.xhr);
}
}
promise.resolve(response.response, response.status, response.xhr);
};
root.GravAjax.errorHandler = function (promise, callbacks, response) {
callbacks = callbacks.error;
for (var i = 0; i < callbacks.length; i++) {
if (typeof callbacks[i] === 'function') {
callbacks[i](response.xhr, response.status, response.response.message);
}
}
promise.reject(response.xhr, response.status, response.response.message);
};
root.GravAjax.toastErrorHandler = function (xhr, status, error) {
toastr.error(error);
}; };
}); });

View File

@@ -43,6 +43,7 @@ $(function(){
flags: flags, flags: flags,
query: query query: query
}, },
toastErrors: true,
success: function (result, status) { success: function (result, status) {
finishFilterPages(result.results); finishFilterPages(result.results);
} }

View File

@@ -1,4 +1,5 @@
{% if admin.json_response %} {% if admin.json_response %}
{% set output = {'status':admin.json_response[0], 'message':admin.json_response[1] } %} {{ admin.json_response|json_encode }}
{{ output|json_encode }} {% else %}
{}
{% endif %} {% endif %}

View File

@@ -9,7 +9,7 @@
$(function(){ $(function(){
var URI = $('[data-media-url]').data('media-url'), thisDropzone, var URI = $('[data-media-url]').data('media-url'), thisDropzone,
modalError = function(args){ modalError = function(args){
if (args.data.status == 'error'){ if (args.data.status == 'error' || args.data.status == 'unauthorized'){
if (args.mode == 'addBack'){ if (args.mode == 'addBack'){
// let's add back the file // let's add back the file
@@ -60,12 +60,20 @@
init: function() { init: function() {
thisDropzone = this; thisDropzone = this;
$.get(URI + '/task:listmedia', function(data) { $.get(URI + '/task:listmedia', function(data) {
$.each(data, function(filename, data){
$.proxy(modalError, this, {
data: data,
msg: '<p>An error occurred while trying to list files</p><pre>'+data.message+'</pre>'
})();
if (data.results) {
$.each(data.results, function(filename, data){
var mockFile = { name: filename, size: data.size, accepted: true, extras: data }; var mockFile = { name: filename, size: data.size, accepted: true, extras: data };
thisDropzone.files.push(mockFile); thisDropzone.files.push(mockFile);
thisDropzone.options.addedfile.call(thisDropzone, mockFile); thisDropzone.options.addedfile.call(thisDropzone, mockFile);
thisDropzone.options.thumbnail.call(thisDropzone, mockFile, data.url); thisDropzone.options.thumbnail.call(thisDropzone, mockFile, data.url);
}); });
}
$('.dz-preview').prop('draggable', 'true'); $('.dz-preview').prop('draggable', 'true');
}); });

View File

@@ -1,8 +1,5 @@
{% if admin.json_response %} {% if admin.json_response %}
{% set output = {'status':admin.json_response[0], 'message':admin.json_response[1] } %} {{ admin.json_response|json_encode }}
{{ output|json_encode }}
{% else %} {% else %}
{% if admin.task == 'listmedia' %} {}
{{ admin.media|json_encode }}
{% endif %}
{% endif %} {% endif %}

View File

@@ -1,5 +1,5 @@
{"status":{{admin.json_response[0]|json_encode}}, "results":[ {% if admin.json_response %}
{%- for search_result in admin.collection -%} {{ admin.json_response|json_encode }}
{{- search_result.route|json_encode -}}{{ not loop.last ? ',' }} {% else %}
{%- endfor -%} {}
]} {% endif %}

View File

@@ -1,4 +1,5 @@
{% if admin.json_response %} {% if admin.json_response %}
{% set output = {'status':admin.json_response[0], 'message':admin.json_response[1] } %} {{ admin.json_response|json_encode }}
{{ output|json_encode }} {% else %}
{}
{% endif %} {% endif %}