Merge permissions cache and inherited permissions #110

Merged
aheathershaw merged 10 commits from feature/71-permissions-cache into master 2018-09-16 22:19:38 +01:00
23 changed files with 736 additions and 248 deletions

View File

@ -21,7 +21,7 @@ class Album extends Model
* @var array
*/
protected $fillable = [
'name', 'description', 'url_alias', 'user_id', 'storage_id', 'default_view', 'parent_album_id', 'url_path'
'name', 'description', 'url_alias', 'user_id', 'storage_id', 'default_view', 'parent_album_id', 'url_path', 'is_permissions_inherited'
];
/**
@ -103,6 +103,29 @@ class Album extends Model
}
}
/**
* Try and locate the parent album ID that permissions are inherited from.
* @return integer
*/
public function effectiveAlbumIDForPermissions()
{
$current = $this;
while (!is_null($current->parent_album_id))
{
if ($current->is_permissions_inherited)
{
$current = $current->parent;
}
else
{
break;
}
}
return $current->id;
}
public function generateAlias()
{
$this->url_alias = MiscHelper::capitaliseWord(preg_replace('/[^a-z0-9\-]/', '-', strtolower($this->name)));

View File

@ -47,43 +47,13 @@ class DbHelper
{
/* Admin users always get everything, therefore no filters are necessary */
}
else if (is_null($user))
{
/* Anonymous users need to check the album_anonymous_permissions table. If not in this table, you're not allowed! */
$albumsQuery = Album::join('album_anonymous_permissions', 'album_anonymous_permissions.album_id', '=', 'albums.id')
->join('permissions', 'permissions.id', '=', 'album_anonymous_permissions.permission_id')
->where([
['permissions.section', 'album'],
['permissions.description', $permission]
]);
}
else
{
/*
Other users need to check either the album_group_permissions or album_user_permissions table. If not in either of these tables,
you're not allowed!
*/
$albumsQuery = Album::leftJoin('album_group_permissions', 'album_group_permissions.album_id', '=', 'albums.id')
->leftJoin('album_user_permissions', 'album_user_permissions.album_id', '=', 'albums.id')
->leftJoin('permissions AS group_permissions', 'group_permissions.id', '=', 'album_group_permissions.permission_id')
->leftJoin('permissions AS user_permissions', 'user_permissions.id', '=', 'album_user_permissions.permission_id')
->leftJoin('user_groups', 'user_groups.group_id', '=', 'album_group_permissions.group_id')
->where(function($query) use ($user, $permission)
{
$query->where('albums.user_id', $user->id)
->orWhere([
['group_permissions.section', 'album'],
['group_permissions.description', $permission],
['user_groups.user_id', $user->id]
])
->orWhere([
['user_permissions.section', 'album'],
['user_permissions.description', $permission],
['album_user_permissions.user_id', $user->id]
]);
});
$helper = new PermissionsHelper();
$albumIDs = $helper->getAlbumIDs($permission, $user);
//dd($albumIDs->toArray());
$albumsQuery->whereIn('albums.id', $albumIDs);
//
}
$parentAlbumID = intval($parentAlbumID);

View File

@ -0,0 +1,146 @@
<?php
namespace App\Helpers;
use App\Album;
use App\Permission;
use App\User;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
class PermissionsHelper
{
public function getAlbumIDs($permission = 'list', User $user = null)
{
$result = [];
$query = DB::table('album_permissions_cache')
->join('permissions', 'permissions.id', '=', 'album_permissions_cache.permission_id')
->where([
['album_permissions_cache.user_id', (is_null($user) || $user->isAnonymous() ? null : $user->id)],
['permissions.section', 'album'],
['permissions.description', $permission]
])
->select('album_permissions_cache.album_id')
->distinct()
->get();
foreach ($query as $item)
{
$result[] = $item->album_id;
}
return $result;
}
public function rebuildCache()
{
$this->rebuildAlbumCache();
}
public function userCan_Album(Album $album, User $user, $permission)
{
return DB::table('album_permissions_cache')
->join('permissions', 'permissions.id', '=', 'album_permissions_cache.permission_id')
->where([
['album_permissions_cache.album_id', $album->id],
['album_permissions_cache.user_id', (is_null($user) || $user->isAnonymous() ? null : $user->id)],
['permissions.section', 'album'],
['permissions.description', $permission]
])
->count() > 0;
}
private function rebuildAlbumCache()
{
// Get a list of albums
$albums = Album::all();
// Get a list of all configured permissions
$albumUserPermissions = DB::table('album_user_permissions')->get();
$albumGroupPermissions = DB::table('album_group_permissions')->get();
$albumAnonPermissions = DB::table('album_anonymous_permissions')->get();
// Get a list of all user->group memberships
$userGroups = DB::table('user_groups')->get();
// Build a matrix of new permissions
$permissionsCache = [];
/** @var Album $album */
foreach ($albums as $album)
{
$effectiveAlbumID = $album->effectiveAlbumIDForPermissions();
$anonymousPermissions = array_filter($albumAnonPermissions->toArray(), function($item) use ($effectiveAlbumID)
{
return ($item->album_id == $effectiveAlbumID);
});
foreach ($anonymousPermissions as $anonymousPermission)
{
$permissionsCache[] = [
'album_id' => $album->id,
'permission_id' => $anonymousPermission->permission_id,
'created_at' => new \DateTime(),
'updated_at' => new \DateTime()
];
}
$userPermissions = array_filter($albumUserPermissions->toArray(), function($item) use ($effectiveAlbumID)
{
return ($item->album_id == $effectiveAlbumID);
});
foreach ($userPermissions as $userPermission)
{
$permissionsCache[] = [
'user_id' => $userPermission->user_id,
'album_id' => $album->id,
'permission_id' => $userPermission->permission_id,
'created_at' => new \DateTime(),
'updated_at' => new \DateTime()
];
}
$groupPermissions = array_filter($albumGroupPermissions->toArray(), function($item) use ($effectiveAlbumID)
{
return ($item->album_id == $effectiveAlbumID);
});
foreach ($groupPermissions as $groupPermission)
{
// Get a list of users in this group, and add one per user
$usersInGroup = array_filter($userGroups->toArray(), function($item) use ($groupPermission)
{
return $item->group_id = $groupPermission->group_id;
});
foreach ($usersInGroup as $userGroup)
{
$permissionsCache[] = [
'user_id' => $userGroup->user_id,
'album_id' => $album->id,
'permission_id' => $groupPermission->permission_id,
'created_at' => new \DateTime(),
'updated_at' => new \DateTime()
];
}
}
}
$this->savePermissionsCache($permissionsCache);
}
private function savePermissionsCache(array $cacheToSave)
{
DB::transaction(function() use ($cacheToSave)
{
DB::table('album_permissions_cache')->truncate();
foreach ($cacheToSave as $cacheItem)
{
DB::table('album_permissions_cache')->insert($cacheItem);
}
});
}
}

View File

@ -10,6 +10,7 @@ use App\Group;
use App\Helpers\DbHelper;
use App\Helpers\FileHelper;
use App\Helpers\MiscHelper;
use App\Helpers\PermissionsHelper;
use App\Http\Controllers\Controller;
use App\Http\Requests;
use App\Label;
@ -267,6 +268,10 @@ class AlbumController extends Controller
$album->save();
// Rebuild the permissions cache
$helper = new PermissionsHelper();
$helper->rebuildCache();
return redirect(route('albums.show', [$album->id, 'tab' => 'permissions']));
}
@ -342,6 +347,10 @@ class AlbumController extends Controller
$album->save();
// Rebuild the permissions cache
$helper = new PermissionsHelper();
$helper->rebuildCache();
return redirect(route('albums.show', [$album->id, 'tab' => 'permissions']));
}
@ -451,6 +460,7 @@ class AlbumController extends Controller
$album = new Album();
$album->fill($request->only(['name', 'description', 'storage_id', 'parent_album_id']));
$album->is_permissions_inherited = (strtolower($request->get('is_permissions_inherited')) == 'on');
if (strlen($album->parent_album_id) == 0)
{
@ -465,20 +475,27 @@ class AlbumController extends Controller
$album->save();
// Link all default permissions to anonymous users (if a public album)
$isPrivate = (strtolower($request->get('is_private')) == 'on');
if (!$isPrivate)
if (!$album->is_permissions_inherited)
{
/** @var Permission $permission */
foreach (Permission::where(['section' => 'album', 'is_default' => true])->get() as $permission)
$isPrivate = (strtolower($request->get('is_private')) == 'on');
if (!$isPrivate)
{
$album->anonymousPermissions()->attach($permission->id, [
'created_at' => new \DateTime(),
'updated_at' => new \DateTime()
]);
/** @var Permission $permission */
foreach (Permission::where(['section' => 'album', 'is_default' => true])->get() as $permission)
{
$album->anonymousPermissions()->attach($permission->id, [
'created_at' => new \DateTime(),
'updated_at' => new \DateTime()
]);
}
}
}
// Rebuild the permissions cache
$helper = new PermissionsHelper();
$helper->rebuildCache();
return redirect(route('albums.show', ['id' => $album->id]));
}
@ -512,6 +529,7 @@ class AlbumController extends Controller
$currentParentID = $album->parent_album_id;
$album->fill($request->only(['name', 'description', 'parent_album_id']));
$album->is_permissions_inherited = (strtolower($request->get('is_permissions_inherited')) == 'on');
if (strlen($album->parent_album_id) == 0)
{
@ -548,6 +566,11 @@ class AlbumController extends Controller
}
$album->save();
// Rebuild the permissions cache
$helper = new PermissionsHelper();
$helper->rebuildCache();
$request->session()->flash('success', trans('admin.album_saved_successfully', ['name' => $album->name]));
return redirect(route('albums.show', ['id' => $id]));

View File

@ -10,6 +10,7 @@ use App\Group;
use App\Helpers\ConfigHelper;
use App\Helpers\DbHelper;
use App\Helpers\MiscHelper;
use App\Helpers\PermissionsHelper;
use App\Http\Controllers\Controller;
use App\Http\Requests\SaveSettingsRequest;
use App\Label;
@ -216,6 +217,14 @@ class DefaultController extends Controller
return $photoController->store($request);
}
public function rebuildPermissionsCache()
{
$helper = new PermissionsHelper();
$helper->rebuildCache();
return response()->json(true);
}
public function saveSettings(SaveSettingsRequest $request)
{
$this->authorizeAccessToAdminPanel('admin:configure');

View File

@ -5,6 +5,7 @@ namespace App\Http\Controllers\Admin;
use App\Facade\Theme;
use App\Facade\UserConfig;
use App\Group;
use App\Helpers\PermissionsHelper;
use App\User;
use App\Http\Requests;
@ -200,6 +201,10 @@ class UserController extends Controller
$user->save();
// Rebuild the permissions cache
$helper = new PermissionsHelper();
$helper->rebuildCache();
return redirect(route('users.index'));
}

View File

@ -4,6 +4,7 @@ namespace App\Policies;
use App\Album;
use App\Group;
use App\Helpers\PermissionsHelper;
use App\Permission;
use App\User;
use Illuminate\Auth\Access\HandlesAuthorization;
@ -45,13 +46,7 @@ class AlbumPolicy
return true;
}
// Get the edit permission
$permission = Permission::where([
'section' => 'album',
'description' => 'change-photo-metadata'
])->first();
return $this->userHasPermission($user, $album, $permission);
return $this->userHasPermission($user, $album, 'change-photo-metadata');
}
public function delete(User $user, Album $album)
@ -62,13 +57,7 @@ class AlbumPolicy
return true;
}
// Get the edit permission
$permission = Permission::where([
'section' => 'album',
'description' => 'delete'
])->first();
return $this->userHasPermission($user, $album, $permission);
return $this->userHasPermission($user, $album, 'delete');
}
public function deletePhotos(User $user, Album $album)
@ -79,13 +68,7 @@ class AlbumPolicy
return true;
}
// Get the edit permission
$permission = Permission::where([
'section' => 'album',
'description' => 'delete-photos'
])->first();
return $this->userHasPermission($user, $album, $permission);
return $this->userHasPermission($user, $album, 'delete-photos');
}
public function edit(User $user, Album $album)
@ -96,13 +79,7 @@ class AlbumPolicy
return true;
}
// Get the edit permission
$permission = Permission::where([
'section' => 'album',
'description' => 'edit'
])->first();
return $this->userHasPermission($user, $album, $permission);
return $this->userHasPermission($user, $album, 'edit');
}
public function manipulatePhotos(User $user, Album $album)
@ -113,13 +90,7 @@ class AlbumPolicy
return true;
}
// Get the edit permission
$permission = Permission::where([
'section' => 'album',
'description' => 'manipulate-photos'
])->first();
return $this->userHasPermission($user, $album, $permission);
return $this->userHasPermission($user, $album, 'manipulate-photos');
}
public function uploadPhotos(User $user, Album $album)
@ -130,13 +101,7 @@ class AlbumPolicy
return true;
}
// Get the edit permission
$permission = Permission::where([
'section' => 'album',
'description' => 'upload-photos'
])->first();
return $this->userHasPermission($user, $album, $permission);
return $this->userHasPermission($user, $album, 'upload-photos');
}
public function view(User $user, Album $album)
@ -147,56 +112,12 @@ class AlbumPolicy
return true;
}
// Get the edit permission
$permission = Permission::where([
'section' => 'album',
'description' => 'view'
])->first();
return $this->userHasPermission($user, $album, $permission);
return $this->userHasPermission($user, $album, 'view');
}
private function userHasPermission(User $user, Album $album, Permission $permission)
private function userHasPermission(User $user, Album $album, $permission)
{
if ($user->isAnonymous())
{
$query = Album::query()->join('album_anonymous_permissions', 'album_anonymous_permissions.album_id', '=', 'albums.id')
->join('permissions', 'permissions.id', '=', 'album_anonymous_permissions.permission_id')
->where([
['albums.id', $album->id],
['permissions.id', $permission->id]
]);
return $query->count() > 0;
}
// If any of the user's groups are granted the permission
/** @var Group $group */
foreach ($user->groups as $group)
{
$groupPermission = $album->groupPermissions()->where([
'group_id' => $group->id,
'permission_id' => $permission->id
])->first();
if (!is_null($groupPermission))
{
return true;
}
}
// If the user is directly granted the permission
$userPermission = $album->userPermissions()->where([
'user_id' => $user->id,
'permission_id' => $permission->id
])->first();
if (!is_null($userPermission))
{
return true;
}
// Nope, no permission
return false;
$helper = new PermissionsHelper();
return $helper->userCan_Album($album, $user, $permission);
}
}

View File

@ -0,0 +1,43 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateAlbumPermissionsCacheTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('album_permissions_cache', function (Blueprint $table) {
$table->unsignedInteger('album_id');
$table->unsignedInteger('user_id')->nullable(true);
$table->unsignedInteger('permission_id');
$table->timestamps();
$table->foreign('user_id')
->references('id')->on('users')
->onDelete('cascade');
$table->foreign('album_id')
->references('id')->on('albums')
->onDelete('cascade');
$table->foreign('permission_id')
->references('id')->on('permissions')
->onDelete('no action');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('album_permissions_cache');
}
}

View File

@ -0,0 +1,34 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateAlbumInheritPermissionsColumn extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('albums', function (Blueprint $table)
{
$table->boolean('is_permissions_inherited')->default(false);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('albums', function (Blueprint $table)
{
$table->dropColumn('is_permissions_inherited');
});
}
}

View File

@ -36,6 +36,53 @@
[v-cloak] {
display: none;
}
.activity-grid {
font-size: smaller;
}
.activity-grid th,
.activity-grid td {
padding: 5px !important;
text-align: center;
}
.activity-grid td {
color: #fff;
height: 20px;
}
.activity-grid .has-activity {
background-color: #1e90ff;
}
.activity-grid .invalid-date {
background-color: #e5e5e5;
}
.activity-grid .no-activity {
background-color: #fff;
}
.activity-grid th:first-child,
.activity-grid td:first-child {
border-left: 1px solid #dee2e6;
}
.activity-grid th,
.activity-grid td {
border-right: 1px solid #dee2e6;
}
.activity-grid tr:last-child td {
border-bottom: 1px solid #dee2e6;
}
.activity-grid .border-spacer-element {
border-right-width: 0;
padding: 0 !important;
width: 1px;
}
.album-slideshow-container #image-preview {
height: 600px;
max-width: 100%;

View File

@ -1,4 +1,4 @@
.admin-sidebar-card{margin-bottom:15px}.album-expand-handle{cursor:pointer;margin-top:5px}.meta-label,.meta-value{vertical-align:middle !important}.photo .loading{background-color:#fff;display:none;height:100%;left:0;opacity:.8;position:absolute;text-align:center;top:0;width:100%;z-index:1000}.photo .loading img{margin-top:40px}.text-red{color:red}[v-cloak]{display:none}.album-slideshow-container #image-preview{height:600px;max-width:100%;width:800px}.album-slideshow-container #image-preview img{max-width:100%}.album-slideshow-container .thumbnails{overflow-x:scroll;overflow-y:hidden;white-space:nowrap;width:auto}.stats-table .icon-col{font-size:1.4em;width:20%;vertical-align:middle}.stats-table .stat-col{font-size:1.8em;font-weight:bold;width:40%}.stats-table .text-col{font-size:1.2em;vertical-align:middle;width:40%}html{font-size:14px !important}button,input,optgroup,select,textarea{cursor:pointer;font-family:inherit !important}.album-photo-cards .card{margin-bottom:15px}.container,.container-fluid{margin-top:20px}.hidden{display:none}.tab-content{border:solid 1px #ddd;border-top:0;padding:20px}.tether-element,.tether-element:after,.tether-element:before,.tether-element *,.tether-element *:after,.tether-element *:before{box-sizing:border-box}.tether-element{position:absolute;display:none}.tether-element.tether-open{display:block}.tether-element.tether-theme-basic{max-width:100%;max-height:100%}.tether-element.tether-theme-basic .tether-content{border-radius:5px;box-shadow:0 2px 8px rgba(0,0,0,0.2);font-family:inherit;background:#fff;color:inherit;padding:1em;font-size:1.1em;line-height:1.5em}.tether-element,.tether-element:after,.tether-element:before,.tether-element *,.tether-element *:after,.tether-element *:before{box-sizing:border-box}.tether-element{position:absolute;display:none}.tether-element.tether-open{display:block}.tt-query{-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.tt-hint{color:#999}.tt-menu{width:422px;margin-top:4px;padding:4px 0;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.2);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);-moz-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2)}.tt-suggestion{padding:3px 20px;line-height:24px}.tt-suggestion.tt-cursor,.tt-suggestion:hover{color:#fff;background-color:#0097cf}.tt-suggestion p{margin:0}/*!
.admin-sidebar-card{margin-bottom:15px}.album-expand-handle{cursor:pointer;margin-top:5px}.meta-label,.meta-value{vertical-align:middle !important}.photo .loading{background-color:#fff;display:none;height:100%;left:0;opacity:.8;position:absolute;text-align:center;top:0;width:100%;z-index:1000}.photo .loading img{margin-top:40px}.text-red{color:red}[v-cloak]{display:none}.activity-grid{font-size:smaller}.activity-grid th,.activity-grid td{padding:5px !important;text-align:center}.activity-grid td{color:#fff;height:20px}.activity-grid .has-activity{background-color:#1e90ff}.activity-grid .invalid-date{background-color:#e5e5e5}.activity-grid .no-activity{background-color:#fff}.activity-grid th:first-child,.activity-grid td:first-child{border-left:1px solid #dee2e6}.activity-grid th,.activity-grid td{border-right:1px solid #dee2e6}.activity-grid tr:last-child td{border-bottom:1px solid #dee2e6}.activity-grid .border-spacer-element{border-right-width:0;padding:0 !important;width:1px}.album-slideshow-container #image-preview{height:600px;max-width:100%;width:800px}.album-slideshow-container #image-preview img{max-width:100%}.album-slideshow-container .thumbnails{overflow-x:scroll;overflow-y:hidden;white-space:nowrap;width:auto}.stats-table .icon-col{font-size:1.4em;width:20%;vertical-align:middle}.stats-table .stat-col{font-size:1.8em;font-weight:bold;width:40%}.stats-table .text-col{font-size:1.2em;vertical-align:middle;width:40%}html{font-size:14px !important}button,input,optgroup,select,textarea{cursor:pointer;font-family:inherit !important}.album-photo-cards .card{margin-bottom:15px}.container,.container-fluid{margin-top:20px}.hidden{display:none}.tab-content{border:solid 1px #ddd;border-top:0;padding:20px}.tether-element,.tether-element:after,.tether-element:before,.tether-element *,.tether-element *:after,.tether-element *:before{box-sizing:border-box}.tether-element{position:absolute;display:none}.tether-element.tether-open{display:block}.tether-element.tether-theme-basic{max-width:100%;max-height:100%}.tether-element.tether-theme-basic .tether-content{border-radius:5px;box-shadow:0 2px 8px rgba(0,0,0,0.2);font-family:inherit;background:#fff;color:inherit;padding:1em;font-size:1.1em;line-height:1.5em}.tether-element,.tether-element:after,.tether-element:before,.tether-element *,.tether-element *:after,.tether-element *:before{box-sizing:border-box}.tether-element{position:absolute;display:none}.tether-element.tether-open{display:block}.tt-query{-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.tt-hint{color:#999}.tt-menu{width:422px;margin-top:4px;padding:4px 0;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.2);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);-moz-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2)}.tt-suggestion{padding:3px 20px;line-height:24px}.tt-suggestion.tt-cursor,.tt-suggestion:hover{color:#fff;background-color:#0097cf}.tt-suggestion p{margin:0}/*!
* Bootstrap v4.1.2 (https://getbootstrap.com/)
* Copyright 2011-2018 The Bootstrap Authors
* Copyright 2011-2018 Twitter, Inc.

View File

@ -22245,6 +22245,85 @@ function AboutViewModel(urls) {
}
};
}
/**
* This model is used by admin/create_album.blade.php.
* @constructor
*/
function CreateAlbumViewModel() {
this.el = '#create-album-app';
this.data = {
is_inherit_permissions: true,
is_private: false,
parent_id: ''
};
this.computed = {
isParentAlbum: function() {
return this.parent_id == '';
},
isPrivateDisabled: function() {
return !this.isParentAlbum && this.is_inherit_permissions;
}
}
}
/**
* This model is used by admin/edit_album.blade.php.
* @constructor
*/
function EditAlbumViewModel() {
this.el = '#edit-album-app';
this.data = {
parent_id: ''
};
this.computed = {
isParentAlbum: function() {
return this.parent_id == '';
}
}
}
/**
* This model is used by admin/settings.blade.php.
* @constructor
*/
function SettingsViewModel(urls, lang) {
this.el = '#settings-app';
this.data = {
is_rebuilding_permissions_cache: false
};
this.methods = {
rebuildPermissionsCache: function (e) {
var self = this;
$.ajax(
urls.rebuild_permissions_cache,
{
complete: function() {
self.is_rebuilding_permissions_cache = false;
},
dataType: 'json',
error: function (xhr, textStatus, errorThrown) {
alert(lang.permissions_cache_rebuild_failed);
},
method: 'POST',
success: function (data) {
alert(lang.permissions_cache_rebuild_succeeded);
}
}
);
e.preventDefault();
return false;
}
};
}
/**
* This model is used by admin/analyse_album.blade.php, to analyse all images.
* @constructor

File diff suppressed because one or more lines are too long

View File

@ -44,4 +44,83 @@ function AboutViewModel(urls) {
);
}
};
}
/**
* This model is used by admin/create_album.blade.php.
* @constructor
*/
function CreateAlbumViewModel() {
this.el = '#create-album-app';
this.data = {
is_inherit_permissions: true,
is_private: false,
parent_id: ''
};
this.computed = {
isParentAlbum: function() {
return this.parent_id == '';
},
isPrivateDisabled: function() {
return !this.isParentAlbum && this.is_inherit_permissions;
}
}
}
/**
* This model is used by admin/edit_album.blade.php.
* @constructor
*/
function EditAlbumViewModel() {
this.el = '#edit-album-app';
this.data = {
parent_id: ''
};
this.computed = {
isParentAlbum: function() {
return this.parent_id == '';
}
}
}
/**
* This model is used by admin/settings.blade.php.
* @constructor
*/
function SettingsViewModel(urls, lang) {
this.el = '#settings-app';
this.data = {
is_rebuilding_permissions_cache: false
};
this.methods = {
rebuildPermissionsCache: function (e) {
var self = this;
$.ajax(
urls.rebuild_permissions_cache,
{
complete: function() {
self.is_rebuilding_permissions_cache = false;
},
dataType: 'json',
error: function (xhr, textStatus, errorThrown) {
alert(lang.permissions_cache_rebuild_failed);
},
method: 'POST',
success: function (data) {
alert(lang.permissions_cache_rebuild_succeeded);
}
}
);
e.preventDefault();
return false;
}
};
}

View File

@ -32,6 +32,10 @@ return [
'album_cameras_heading' => 'Cameras used in this album',
'album_cameras_tab' => 'Cameras',
'album_cameras_text' => 'Blue Twilight analyses the Exif data in your photos to determine which cameras have been used. The cameras that were found are displayed below.',
'album_change_more_details' => 'You can change more details about this album by editing it. Click the button below to go to the album\'s Edit page.',
'album_inheriting_permissions_p1' => 'Inherited permissions are in effect',
'album_inheriting_permissions_p2' => 'This album is inheriting permissions from a parent album and therefore permissions cannot be applied directly to it.',
'album_inheriting_permissions_p3' => 'You can change the permissions applied to this album (and other albums under the same parent) from the :l_parent_start parent album\'s permissions tab:l_parent_end, or stop permissions from being inherited by :l_edit_start editing this album:l_edit_end.',
'album_no_cameras_found_p1' => 'No cameras were found',
'album_no_cameras_found_p2' => 'Upload more photos to this album or ensure the cameras you use support Exif image tagging.',
'album_no_photos_p1' => 'No photos in this album',
@ -209,6 +213,11 @@ return [
'analytics_enable_visitor_hits' => 'Enable built-in visitor hit tracking',
'analytics_enable_visitor_hits_description' => 'Visitor hits to the public gallery will be recorded in the Blue Twilight database, allowing for analysis such as the most popular album/photo.',
'analytics_tab' => 'Analytics',
'permissions_cache' => 'Permissions Cache',
'permissions_cache_intro' => 'Blue Twilight maintains the permissions each user has to albums in the database. If you feel these aren\'t correct based on what\'s configured, you can rebuild the cache by clicking the button below.',
'rebuild_permissions_cache' => 'Rebuild Permissions Cache',
'rebuild_permissions_cache_failed' => 'The permissions cache rebuild failed to complete. Please check the server logs.',
'rebuild_permissions_cache_succeeded' => 'The permissions cache rebuild completed successfully.',
'security_allow_self_registration' => 'Allow self-registration',
'security_allow_self_registration_description' => 'With this option enabled, users can sign up for their own accounts. You can grant permissions to accounts to allow users to upload their own photos or manage yours.',
'social_facebook' => 'Facebook',

View File

@ -24,8 +24,10 @@ return [
'description_label' => 'Description:',
'download_action' => 'Download',
'edit_action' => 'Edit',
'edit_album_action' => 'Edit this album',
'email_label' => 'E-mail address:',
'enable_profile_page_label' => 'Allow others to see my profile page',
'inherit_album_permissions' => 'Inherit permissions from parent album',
'labels_label' => 'Labels:',
'login_action' => 'Login',
'name_label' => 'Name:',

View File

@ -9,7 +9,7 @@
@endsection
@section('content')
<div class="container">
<div class="container" id="create-album-app">
<div class="row">
<div class="col">
<h1>@lang('admin.create_album')</h1>
@ -37,10 +37,10 @@
<div class="form-group">
<label class="form-control-label" for="parent-album">@lang('forms.parent_album_label')</label>
<select class="form-control" name="parent_album_id" id="parent-album">
<select class="form-control" name="parent_album_id" id="parent-album" v-model="parent_id">
<option value="">@lang('forms.parent_album_placeholder')</option>
@foreach ($parent_albums as $key => $value)
<option value="{{ $key }}"{{ $key == old('parent_album_id') ? ' selected="selected"' : '' }}>{{ $value->display_name }}</option>
<option value="{{ $key }}">{{ $value->display_name }}</option>
@endforeach
</select>
</div>
@ -54,9 +54,18 @@
</select>
</div>
<div class="form-group" v-if="!isParentAlbum">
<div class="mt-3 form-check">
<input type="checkbox" class="form-check-input" id="inherit-permissions" name="is_permissions_inherited" v-model="is_inherit_permissions" />
<label class="form-check-label" for="inherit-permissions">
@lang('forms.inherit_album_permissions')
</label>
</div>
</div>
<div class="form-check">
<label class="form-check-label">
<input class="form-check-inline" type="checkbox" name="is_private">
<input class="form-check-input" type="checkbox" name="is_private" id="is-private" v-bind:disabled="isPrivateDisabled" v-model="is_private">
<label class="form-check-label" for="is-private">
<i class="fa fa-fw fa-lock"></i> @lang('forms.private_album_label')
</label>
</div>
@ -69,4 +78,35 @@
</div>
</div>
</div>
@endsection
@endsection
@push('scripts')
<script type="text/javascript">
var createAlbumViewModel = new CreateAlbumViewModel();
createAlbumViewModel.data.parent_id = '{{ old('parent_album_id') }}';
var app = null;
$(document).ready(function()
{
app = new Vue(createAlbumViewModel);
app.$watch('is_private', function()
{
// If user chooses to make the album private, it cannot then inherit permissions (secure by default)
if (app.is_private)
{
app.is_inherit_permissions = false;
}
});
app.$watch('is_inherit_permissions', function()
{
// If user chooses to inherit permissions, it cannot also be private
if (app.is_inherit_permissions)
{
app.is_private = false;
}
});
});
</script>
@endpush

View File

@ -10,7 +10,7 @@
@endsection
@section('content')
<div class="container">
<div class="container" id="edit-album-app">
<div class="row">
<div class="col">
<h1>@lang('admin.edit_album', ['album_name' => $album->name])</h1>
@ -46,14 +46,23 @@
<div class="form-group">
<label class="form-control-label" for="parent-album">@lang('forms.parent_album_label')</label>
<select class="form-control" name="parent_album_id" id="parent-album">
<select class="form-control" name="parent_album_id" id="parent-album" v-model="parent_id">
<option value="">@lang('forms.parent_album_placeholder')</option>
@foreach ($parent_albums as $key => $value)
<option value="{{ $key }}"{{ $key == $album->id || $value->isChildOf($album) ? ' disabled="disabled"' : '' }}{{ $key == old('parent_album_id') ? ' selected="selected"' : '' }}>{{ $value->display_name }}{{ $key == old('parent_album_id') ? ' ' . trans('forms.select_current_text') : '' }}</option>
<option value="{{ $key }}"{{ $key == $album->id || $value->isChildOf($album) ? ' disabled="disabled"' : '' }}>{{ $value->display_name }}{{ $key == old('parent_album_id') ? ' ' . trans('forms.select_current_text') : '' }}</option>
@endforeach
</select>
</div>
<div class="form-group" v-if="!isParentAlbum">
<div class="mt-3 form-check">
<input type="checkbox" class="form-check-input" id="inherit-permissions" name="is_permissions_inherited"{{ $album->is_permissions_inherited ? ' checked="checked"' : '' }}>
<label class="form-check-label" for="inherit-permissions">
<strong>@lang('forms.inherit_album_permissions')</strong>
</label>
</div>
</div>
<div id="change-parent-warning" class="alert alert-warning" style="display: none;">
@lang('admin.edit_album_change_parent_warning')
@ -77,7 +86,12 @@
@push('scripts')
<script type="text/javascript">
var editAlbumViewModel = new EditAlbumViewModel();
editAlbumViewModel.data.parent_id = '{{ old('parent_album_id', $album->parent_album_id) }}';
$(document).ready(function() {
var app = new Vue(editAlbumViewModel);
// Show the change parent warning
var current_parent_id = '{{ $album->parent_album_id }}';
$('#parent-album').change(function() {

View File

@ -10,7 +10,7 @@
@section('content')
<div class="container">
<div class="row">
<div class="col">
<div class="col" id="settings-app">
<h1>@yield('title')</h1>
<p style="margin-bottom: 30px;">@lang('admin.settings_intro')</p>
@ -239,7 +239,9 @@
</label>
</div>
<fieldset style="margin-top: 30px;">
<hr/>
<fieldset>
<legend>@lang('admin.settings_recaptcha')</legend>
<div class="form-group">
@ -265,7 +267,9 @@
</div>
</fieldset>
<fieldset style="margin-top: 20px;">
<hr/>
<fieldset>
<legend>@lang('admin.settings_image_protection')</legend>
<div class="form-check">
@ -284,6 +288,14 @@
</label>
</div>
</fieldset>
<hr/>
<fieldset>
<legend>@lang('admin.settings.permissions_cache')</legend>
<p>@lang('admin.settings.permissions_cache_intro')</p>
<button class="btn btn-primary" v-on:click="rebuildPermissionsCache">@lang('admin.settings.rebuild_permissions_cache')</button>
</fieldset>
</div>
{{-- Analytics --}}
@ -479,7 +491,19 @@
@push('scripts')
<script type="text/javascript">
var viewModel = new SettingsViewModel(
{
'rebuild_permissions_cache': '{{ route('admin.rebuildPermissionsCache') }}'
},
{
'permissions_cache_rebuild_failed': '@lang('admin.settings.rebuild_permissions_cache_failed')',
'permissions_cache_rebuild_succeeded': '@lang('admin.settings.rebuild_permissions_cache_succeeded')'
}
);
$(document).ready(function() {
var app = new Vue(viewModel);
$('#test-email-button').click(function() {
var data = $('form').serialize();
$('#test-email-status').show();

View File

@ -1,89 +1,101 @@
<div role="tabpanel" class="tab-pane{{ $active_tab == 'permissions' ? ' active' : '' }}" id="permissions-tab">
<h4>@lang('admin.security_heading')</h4>
<p>@lang('admin.security_text')</p>
<hr/>
@if ($album->is_permissions_inherited)
<div class="text-center mt-3">
<h4 class="text-info"><b>@lang('admin.album_inheriting_permissions_p1')</b></h4>
<p>@lang('admin.album_inheriting_permissions_p2')</p>
<p>@lang('admin.album_inheriting_permissions_p3', [
'l_parent_start' => sprintf('<a href="%s">', route('albums.show', [$album->effectiveAlbumIDForPermissions(), 'tab' => 'permissions'])),
'l_parent_end' => '</a>',
'l_edit_start' => sprintf('<a href="%s">', route('albums.edit', [$album->id])),
'l_edit_end' => '</a>'
])</p>
</div>
@else
<h4>@lang('admin.security_heading')</h4>
<p>@lang('admin.security_text')</p>
<hr/>
<h5 style="font-weight: bold;">@lang('admin.security_groups_heading')</h5>
<h5 style="font-weight: bold;">@lang('admin.security_groups_heading')</h5>
<form action="{{ route('albums.set_group_permissions', ['id' => $album->id]) }}" method="post">
{{ csrf_field() }}
<form action="{{ route('albums.set_group_permissions', ['id' => $album->id]) }}" method="post">
{{ csrf_field() }}
@if (count($existing_groups) > 0)
<div id="groups-accordion" role="tablist" aria-multiselectable="true">
@foreach ($existing_groups as $group)
@include(Theme::viewName('partials.album_permissions'), [
'key_id' => 'group_' . $group->id,
'object_id' => $group->id,
'title' => $group->name,
'callback' => [$album, 'doesGroupHavePermission'],
'callback_object' => $group,
'parent_id' => 'groups-accordion'
])
@endforeach
</div>
@endif
@if (count($existing_groups) > 0)
<div id="groups-accordion" role="tablist" aria-multiselectable="true">
@foreach ($existing_groups as $group)
<div class="row mt-3">
<div class="col-md-4">
<select class="form-control" name="group_id" style="margin-bottom: 2px;"@if (count($add_new_groups) == 0) disabled="disabled"@endif>
@foreach ($add_new_groups as $group)
<option value="{{ $group->id }}">{{ $group->name }}</option>
@endforeach
</select>
</div>
<div class="col-md-2">
<button type="submit" name="action" value="add_group" class="btn btn-primary">Assign Permissions</button>
</div>
<div class="col-md-6 text-right">
<button type="submit" name="action" value="update_group_permissions" class="btn btn-success">
<i class="fa fa-fw fa-check"></i> @lang('forms.save_action')
</button>
</div>
</div>
</form>
<hr/>
<h5 style="font-weight: bold;">@lang('admin.security_users_heading')</h5>
<form action="{{ route('albums.set_user_permissions', ['id' => $album->id]) }}" method="post">
{{ csrf_field() }}
<div id="users-accordion" role="tablist" aria-multiselectable="true">
{{-- Anonymous users --}}
@include(Theme::viewName('partials.album_permissions'), [
'key_id' => 'anonymous',
'object_id' => 'anonymous',
'title' => trans('admin.anonymous_users'),
'callback' => [$album, 'doesUserHavePermission'],
'callback_object' => null,
'parent_id' => 'users-accordion'
])
@foreach ($existing_users as $user)
@include(Theme::viewName('partials.album_permissions'), [
'key_id' => 'group_' . $group->id,
'object_id' => $group->id,
'title' => $group->name,
'callback' => [$album, 'doesGroupHavePermission'],
'callback_object' => $group,
'parent_id' => 'groups-accordion'
'key_id' => 'user_' . $user->id,
'object_id' => $user->id,
'title' => $user->name,
'callback' => [$album, 'doesUserHavePermission'],
'callback_object' => $user,
'parent_id' => 'users-accordion'
])
@endforeach
</div>
@endif
<div class="row mt-3">
<div class="col-md-4">
<select class="form-control" name="group_id" style="margin-bottom: 2px;"@if (count($add_new_groups) == 0) disabled="disabled"@endif>
@foreach ($add_new_groups as $group)
<option value="{{ $group->id }}">{{ $group->name }}</option>
@endforeach
</select>
<div class="row mt-3">
<div class="col-md-4">
<input class="form-control" name="user_name" id="user-search-textbox" size="20" style="margin-bottom: 2px;" />
<input type="hidden" name="user_id" id="user-id-field" />
</div>
<div class="col-md-2">
<button type="submit" name="action" value="add_user" class="btn btn-primary">Assign Permissions</button>
</div>
<div class="col-md-6 text-right">
<button type="submit" name="action" value="update_user_permissions" class="btn btn-success">
<i class="fa fa-fw fa-check"></i> @lang('forms.save_action')
</button>
</div>
</div>
<div class="col-md-2">
<button type="submit" name="action" value="add_group" class="btn btn-primary">Assign Permissions</button>
</div>
<div class="col-md-6 text-right">
<button type="submit" name="action" value="update_group_permissions" class="btn btn-success">
<i class="fa fa-fw fa-check"></i> @lang('forms.save_action')
</button>
</div>
</div>
</form>
<hr/>
<h5 style="font-weight: bold;">@lang('admin.security_users_heading')</h5>
<form action="{{ route('albums.set_user_permissions', ['id' => $album->id]) }}" method="post">
{{ csrf_field() }}
<div id="users-accordion" role="tablist" aria-multiselectable="true">
{{-- Anonymous users --}}
@include(Theme::viewName('partials.album_permissions'), [
'key_id' => 'anonymous',
'object_id' => 'anonymous',
'title' => trans('admin.anonymous_users'),
'callback' => [$album, 'doesUserHavePermission'],
'callback_object' => null,
'parent_id' => 'users-accordion'
])
@foreach ($existing_users as $user)
@include(Theme::viewName('partials.album_permissions'), [
'key_id' => 'user_' . $user->id,
'object_id' => $user->id,
'title' => $user->name,
'callback' => [$album, 'doesUserHavePermission'],
'callback_object' => $user,
'parent_id' => 'users-accordion'
])
@endforeach
</div>
<div class="row mt-3">
<div class="col-md-4">
<input class="form-control" name="user_name" id="user-search-textbox" size="20" style="margin-bottom: 2px;" />
<input type="hidden" name="user_id" id="user-id-field" />
</div>
<div class="col-md-2">
<button type="submit" name="action" value="add_user" class="btn btn-primary">Assign Permissions</button>
</div>
<div class="col-md-6 text-right">
<button type="submit" name="action" value="update_user_permissions" class="btn btn-success">
<i class="fa fa-fw fa-check"></i> @lang('forms.save_action')
</button>
</div>
</div>
</form>
</form>
@endif
</div>

View File

@ -4,29 +4,31 @@
@if ($album->redirects()->count() > 0)
<p>@lang('admin.existing_album_redirects')</p>
<table class="table table-striped table-responsive">
<thead>
<tr>
<th>@lang('admin.redirects_source_url_heading')</th>
<th style="width: 100px;">@lang('admin.redirects_actions_heading')</th>
</tr>
</thead>
<tbody>
@foreach ($album->redirects as $redirect)
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<td><a href="{{ route('home') }}/a{{ $redirect->source_url }}">{{ route('home') }}/a{{ $redirect->source_url }}</a></td>
<td>
<form method="post" action="{{ route('albums.delete_redirect', [$album->id, $redirect->id]) }}">
{{ csrf_field() }}
{{ method_field('DELETE') }}
<input type="hidden" name="redirect_id" value="{{ $redirect->id }}" />
<button type="submit" class="btn btn-sm btn-danger delete-redirect">@lang('forms.delete_action')</button>
</form>
</td>
<th>@lang('admin.redirects_source_url_heading')</th>
<th style="width: 100px;">@lang('admin.redirects_actions_heading')</th>
</tr>
@endforeach
</tbody>
</table>
</thead>
<tbody>
@foreach ($album->redirects as $redirect)
<tr>
<td><a href="{{ route('home') }}/a{{ $redirect->source_url }}">{{ route('home') }}/a{{ $redirect->source_url }}</a></td>
<td>
<form method="post" action="{{ route('albums.delete_redirect', [$album->id, $redirect->id]) }}">
{{ csrf_field() }}
{{ method_field('DELETE') }}
<input type="hidden" name="redirect_id" value="{{ $redirect->id }}" />
<button type="submit" class="btn btn-sm btn-danger delete-redirect">@lang('forms.delete_action')</button>
</form>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
<br/>
@endif

View File

@ -22,6 +22,11 @@
<textarea class="form-control" id="album-description" name="description" rows="5">{{ old('description') }}</textarea>
</div>
<div class="form-group">
<p>@lang('admin.album_change_more_details')</p>
<a href="{{ route('albums.edit', [$album->id]) }}" class="btn btn-outline-primary">@lang('forms.edit_album_action')</a>
</div>
<hr/>
<h4><i class="fa fa-fw fa-paint-brush"></i> @lang('admin.album_appearance_heading')</h4>

View File

@ -21,6 +21,7 @@ Route::group(['prefix' => 'admin'], function () {
Route::get('/photo-metadata', 'Admin\DefaultController@metadataUpgrade')->name('admin.metadataUpgrade');
Route::post('quick-upload', 'Admin\DefaultController@quickUpload')->name('admin.quickUpload');
Route::post('settings/save', 'Admin\DefaultController@saveSettings')->name('admin.saveSettings');
Route::post('settings/rebuild-permissions-cache', 'Admin\DefaultController@rebuildPermissionsCache')->name('admin.rebuildPermissionsCache');
Route::post('settings/test-email', 'Admin\DefaultController@testMailSettings')->name('admin.testMailSettings');
Route::get('settings', 'Admin\DefaultController@settings')->name('admin.settings');
Route::post('statistics/save', 'Admin\StatisticsController@save')->name('admin.statistics.save');