#3: User permissions can now be specified for an album. Added a new config to the User class that allows users to login and manage albums without needing full admin access

This commit is contained in:
Andy Heathershaw 2017-03-21 21:48:55 +00:00
parent 6be31c9b7e
commit fd19c9db55
31 changed files with 409 additions and 518 deletions

View File

@ -35,11 +35,6 @@ class Album extends Model
return $this->belongsToMany(Permission::class, 'album_anonymous_permissions');
}
public function doesAnonymousHavePermission(Permission $permission)
{
return $this->anonymousPermissions()->where(['permission_id' => $permission->id])->count() > 0;
}
public function doesGroupHavePermission(Group $group, Permission $permission)
{
return $this->groupPermissions()->where([
@ -48,6 +43,22 @@ class Album extends Model
])->count() > 0;
}
public function doesUserHavePermission($user, Permission $permission)
{
// User will be null for anonymous users
if (is_null($user))
{
return $this->anonymousPermissions()->where(['permission_id' => $permission->id])->count() > 0;
}
else
{
return $this->userPermissions()->where([
'user_id' => $user->id,
'permission_id' => $permission->id
])->count() > 0;
}
}
public function generateAlias()
{
$this->url_alias = ucfirst(preg_replace('/[^a-z0-9\-]/', '-', strtolower($this->name)));
@ -107,4 +118,9 @@ class Album extends Model
{
return route('viewAlbum', $this->url_alias);
}
public function userPermissions()
{
return $this->belongsToMany(Permission::class, 'album_user_permissions');
}
}

View File

@ -4,35 +4,60 @@ namespace App\Helpers;
use App\Album;
use App\Facade\UserConfig;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Facades\Auth;
class DbHelper
{
public static function getAlbumsForCurrentUser()
{
$albumsQuery = Album::query();
$user = Auth::user();
$userId = is_null($user) ? 0 : $user->id;
$albums = Album::where('is_private', false)
->orWhere(function ($query) use ($userId)
{
$query->where('is_private', true)
->where('user_id', $userId);
})
if (!is_null($user) && $user->is_admin)
{
/* 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', 'list']
]);
}
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('albums.user_id', $user->id)
->orWhere([
['group_permissions.section', 'album'],
['group_permissions.description', 'list'],
['user_groups.user_id', $user->id]
])
->orWhere([
['user_permissions.section', 'album'],
['user_permissions.description', 'list'],
['album_user_permissions.user_id', $user->id]
]);
}
return $albumsQuery->select('albums.*')
->distinct()
->orderBy('name')
->withCount('photos')
->paginate(UserConfig::get('items_per_page'));
return $albums;
}
/**
* Fetches an album using its URL alias.
* @param string $urlAlias URL alias of the album to fetch.
* @return Album|null
*/
public static function loadAlbumByUrlAlias($urlAlias)
{
return Album::where('url_alias', $urlAlias)->first();
}
}

View File

@ -7,6 +7,7 @@ use App\AlbumGroupPermission;
use App\Facade\Theme;
use App\Facade\UserConfig;
use App\Group;
use App\Helpers\DbHelper;
use App\Helpers\FileHelper;
use App\Helpers\MiscHelper;
use App\Http\Controllers\Controller;
@ -16,6 +17,7 @@ use App\Photo;
use App\Services\PhotoService;
use App\Storage;
use App\Upload;
use App\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Auth;
@ -32,7 +34,7 @@ class AlbumController extends Controller
public function analyse($id, $queue_token)
{
$this->authorize('admin-access');
$this->authorizeAccessToAdminPanel();
$album = $this->loadAlbum($id);
$photos = $album->photos()
@ -55,7 +57,7 @@ class AlbumController extends Controller
*/
public function create(Request $request)
{
$this->authorize('admin-access');
$this->authorizeAccessToAdminPanel();
$albumSources = [];
foreach (Storage::where('is_active', true)->orderBy('name')->get() as $storage)
@ -79,7 +81,7 @@ class AlbumController extends Controller
public function delete($id)
{
$this->authorize('admin-access');
$this->authorizeAccessToAdminPanel();
$album = $this->loadAlbum($id);
@ -94,7 +96,7 @@ class AlbumController extends Controller
*/
public function destroy($id)
{
$this->authorize('admin-access');
$this->authorizeAccessToAdminPanel();
$album = $this->loadAlbum($id);
@ -120,7 +122,7 @@ class AlbumController extends Controller
*/
public function edit($id)
{
$this->authorize('admin-access');
$this->authorizeAccessToAdminPanel();
$album = $this->loadAlbum($id);
@ -134,11 +136,9 @@ class AlbumController extends Controller
*/
public function index()
{
$this->authorize('admin-access');
$this->authorizeAccessToAdminPanel();
$albums = Album::orderBy('name')
->withCount('photos')
->paginate(UserConfig::get('items_per_page'));
$albums = DbHelper::getAlbumsForCurrentUser();
return Theme::render('admin.list_albums', [
'albums' => $albums
@ -147,7 +147,7 @@ class AlbumController extends Controller
public function setGroupPermissions(Request $request, $id)
{
$this->authorize('admin-access');
$this->authorizeAccessToAdminPanel();
/** @var Album $album */
$album = $this->loadAlbum($id);
@ -167,10 +167,12 @@ class AlbumController extends Controller
/** @var Permission $permission */
foreach (Permission::where(['section' => 'album', 'is_default' => true])->get() as $permission)
{
$album->groupPermissions()->attach($permission->id, ['group_id' => $group->id]);
$album->groupPermissions()->attach($permission->id, [
'group_id' => $group->id,
'created_at' => new \DateTime(),
'updated_at' => new \DateTime()
]);
}
$album->save();
}
else if ($request->get('action') == 'update_group_permissions')
{
@ -193,10 +195,46 @@ class AlbumController extends Controller
}
}
}
$album->save();
return redirect(route('albums.show', [$album->id, 'tab' => 'permissions']));
}
public function setUserPermissions(Request $request, $id)
{
$this->authorizeAccessToAdminPanel();
/** @var Album $album */
$album = $this->loadAlbum($id);
if ($request->get('action') == 'add_user' && $request->has('user_id'))
{
/* Add a new user to the permission list for this album */
/** @var User $user */
$user = User::where('id', $request->get('user_id'))->first();
if (is_null($user))
{
App::abort(404);
}
// Link all default permissions to the group
/** @var Permission $permission */
foreach (Permission::where(['section' => 'album', 'is_default' => true])->get() as $permission)
{
$album->userPermissions()->attach($permission->id, [
'user_id' => $user->id,
'created_at' => new \DateTime(),
'updated_at' => new \DateTime()
]);
}
}
else if ($request->get('action') == 'update_user_permissions')
{
/* Update existing user and anonymous permissions for this album */
$album->anonymousPermissions()->detach();
$album->userPermissions()->detach();
$permissions = $request->get('permissions');
if (is_array($permissions))
@ -211,9 +249,30 @@ class AlbumController extends Controller
]);
}
}
foreach ($permissions as $key => $value)
{
$userID = intval($key);
if ($userID == 0)
{
// Skip non-numeric IDs (e.g. anonymous)
continue;
}
foreach ($value as $permissionID)
{
$album->userPermissions()->attach($permissionID, [
'user_id' => $userID,
'created_at' => new \DateTime(),
'updated_at' => new \DateTime()
]);
}
}
}
}
$album->save();
return redirect(route('albums.show', [$album->id, 'tab' => 'permissions']));
}
@ -225,7 +284,7 @@ class AlbumController extends Controller
*/
public function show(Request $request, $id)
{
$this->authorize('admin-access');
$this->authorizeAccessToAdminPanel();
$album = $this->loadAlbum($id);
$photos = $album->photos()
@ -258,6 +317,15 @@ class AlbumController extends Controller
}
}
$existingUsers = [];
foreach (User::orderBy('name')->get() as $user)
{
if ($album->userPermissions()->where('user_id', $user->id)->count() > 0)
{
$existingUsers[] = $user;
}
}
$activeTab = $request->get('tab');
return Theme::render('admin.show_album', [
@ -280,6 +348,7 @@ class AlbumController extends Controller
],
'error' => $request->session()->get('error'),
'existing_groups' => $existingGroups,
'existing_users' => $existingUsers,
'file_upload_limit' => $fileUploadLimit,
'is_upload_enabled' => $isUploadEnabled,
'max_post_limit' => $postLimit,
@ -299,7 +368,7 @@ class AlbumController extends Controller
*/
public function store(Requests\StoreAlbumRequest $request)
{
$this->authorize('admin-access');
$this->authorizeAccessToAdminPanel();
$album = new Album();
$album->fill($request->only(['name', 'description', 'storage_id']));
@ -323,7 +392,7 @@ class AlbumController extends Controller
*/
public function update(Requests\StoreAlbumRequest $request, $id)
{
$this->authorize('admin-access');
$this->authorizeAccessToAdminPanel();
$album = $this->loadAlbum($id);
$album->fill($request->only(['name', 'description']));

View File

@ -8,6 +8,7 @@ use App\Facade\Theme;
use App\Facade\UserConfig;
use App\Group;
use App\Helpers\ConfigHelper;
use App\Helpers\DbHelper;
use App\Http\Controllers\Controller;
use App\Http\Requests\SaveSettingsRequest;
use App\Mail\TestMailConfig;
@ -30,9 +31,9 @@ class DefaultController extends Controller
public function index()
{
$this->authorize('admin-access');
$this->authorizeAccessToAdminPanel();
$albumCount = Album::all()->count();
$albumCount = DbHelper::getAlbumsForCurrentUser()->count();
$photoCount = Photo::all()->count();
$groupCount = Group::all()->count();
$userCount = User::where('is_activated', true)->count();

View File

@ -88,6 +88,7 @@ class UserController extends Controller
$user->password = bcrypt($user->password);
$user->is_activated = true;
$user->is_admin = (strtolower($request->get('is_admin')) == 'on');
$user->can_create_albums = (strtolower($request->get('can_create_albums')) == 'on');
$user->save();
return redirect(route('users.index'));
@ -168,6 +169,8 @@ class UserController extends Controller
$user->is_admin = (strtolower($request->get('is_admin')) == 'on');
}
$user->can_create_albums = (strtolower($request->get('can_create_albums')) == 'on');
// Manually activate account if requested
if (strtolower($request->get('is_activated')) == 'on')
{
@ -238,4 +241,33 @@ class UserController extends Controller
return redirect(route('users.index'));
}
/**
* Returns a list of users in JSON format - either all users or users matching the "q" query string parameter
*
* @param string $q Search term
* @return \Illuminate\Http\Response
*/
public function searchJson(Request $request)
{
$this->authorize('admin-access');
$limit = intval($request->get('n'));
if ($limit == 0)
{
$limit = 100;
}
$q = $request->get('q');
if (strlen($q) == 0)
{
return User::limit($limit)->get()->toJson();
}
return User::where('name', 'like', '%' . $q . '%')
->limit($limit)
->orderBy('name')
->get()
->toJson();
}
}

View File

@ -16,6 +16,16 @@ class Controller extends BaseController
{
use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
protected function authorizeAccessToAdminPanel()
{
// A user can access the admin panel if they are either an administrator, or are allowed to create albums
// Further checks within the admin panel determine what a user can do within the panel
if (!Auth::user()->can('admin-access') && !Auth::user()->can('admin-create-albums'))
{
App::abort(403);
}
}
/**
* Gets either the authenticated user, or a user object representing the anonymous user.
* @return User

View File

@ -17,7 +17,7 @@ class AlbumController extends Controller
{
public function index(Request $request, $albumUrlAlias)
{
$album = DbHelper::loadAlbumByUrlAlias($albumUrlAlias);
$album = DbHelper::getAlbumByAliasForCurrentUser($albumUrlAlias);
if (is_null($album))
{
App::abort(404);

View File

@ -36,6 +36,10 @@ class AuthServiceProvider extends ServiceProvider
{
return $user->is_admin;
});
Gate::define('admin-create-albums', function ($user)
{
return $user->can_create_albums;
});
Gate::define('photo.download_original', function ($user, Photo $photo)
{
if (!UserConfig::get('restrict_original_download'))

View File

@ -15,7 +15,7 @@ class User extends Authenticatable
* @var array
*/
protected $fillable = [
'name', 'email', 'password', 'is_admin', 'is_activated', 'activation_token'
'name', 'email', 'password', 'is_admin', 'is_activated', 'activation_token', 'can_create_albums'
];
/**

View File

@ -18,6 +18,7 @@ class CreatePermissionsTable extends Migration
$table->string('section');
$table->string('description');
$table->boolean('is_default');
$table->integer('sort_order');
$table->timestamps();
});

View File

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

View File

@ -15,14 +15,24 @@ class PermissionsSeeder extends Seeder
DatabaseSeeder::createOrUpdate('permissions', [
'section' => 'album',
'description' => 'list-gallery',
'is_default' => true
'is_default' => true,
'sort_order' => 0
]);
// album:view = controls if the album can be viewed
DatabaseSeeder::createOrUpdate('permissions', [
'section' => 'album',
'description' => 'view',
'is_default' => true
'is_default' => true,
'sort_order' => 20
]);
// album:edit = controls if the album is visible and can be edited in the admin panel
DatabaseSeeder::createOrUpdate('permissions', [
'section' => 'album',
'description' => 'edit',
'is_default' => true,
'sort_order' => 10
]);
}
}

View File

@ -1,98 +0,0 @@
<?php
/* START EXISTING LICENSE CHECK */
/* For security reasons, don't allow use of this page if a license file already exists */
$licenseFile = sprintf('%s/blue-twilight.lic', dirname(__DIR__));
if (file_exists($licenseFile))
{
header('Location: index.php');
exit();
}
/* END EXISTING LICENSE CHECK */
/* START LANGUAGE */
$lang = 'en';
if (isset($_GET['lang']))
{
$lang = trim(strtolower(stripslashes($_GET['lang'])));
}
$langFile = sprintf('%s/raw/lang.%s.php', __DIR__, $lang);
if (!file_exists($langFile))
{
$langFile = sprintf('%s/raw/lang.en.php', __DIR__);
}
$lang = include $langFile;
/* END LANGUAGE */
/* START UPLOAD PROCESSING */
$uploadError = null;
if (strtolower($_SERVER['REQUEST_METHOD']) == 'post' && isset($_FILES['upload-license-file']))
{
if ($_FILES['upload-license-file']['error'] != 0)
{
$uploadError = $lang['upload_errors'][$_FILES['upload-license-file']['error']];
}
elseif (!move_uploaded_file($_FILES['upload-license-file']['tmp_name'], $licenseFile))
{
$uploadError = $lang['upload_errors'][99];
}
else
{
header('Location: index.php');
exit();
}
}
/* END UPLOAD PROCESSING */
/* START LICENSE ERROR */
$licenseError = null;
if (isset($_GET['licerror']))
{
$licenseErrorNumber = intval($_GET['licerror']);
if (isset($lang['license_errors'][$licenseErrorNumber]))
{
$licenseError = $lang['license_errors'][$licenseErrorNumber];
}
else
{
$licenseError = $lang['license_errors'][99];
}
}
/* END LICENSE ERROR */
ob_start();
?>
<h1><?php echo $lang['license_required_title']; ?></h1>
<p><?php echo $lang['license_required_p1']; ?></p>
<p><?php echo str_replace(':host_name', sprintf('<b>%s</b>', $_SERVER['SERVER_NAME']), $lang['license_required_p2']); ?></p>
<hr/>
<?php if (!is_null($uploadError)): ?>
<div class="alert alert-danger">
<p><?php echo $uploadError; ?></p>
</div>
<?php endif; ?>
<?php if (!is_null($licenseError)): ?>
<div class="alert alert-danger">
<p><?php echo $licenseError; ?></p>
</div>
<?php endif; ?>
<form action="license-required.php" method="post" enctype="multipart/form-data">
<div class="form-group">
<label class="control-label"><?php echo $lang['upload_license_label']; ?></label>
<input type="file" name="upload-license-file"/>
</div>
<div class="form-group">
<button class="btn btn-success" type="submit"><?php echo $lang['upload_action']; ?></button>
</div>
</form>
<?php
$content = ob_get_clean();
require sprintf('%s/raw/layout.php', __DIR__);
?>

View File

@ -1,49 +0,0 @@
<?php
/* START LOADER CHECK */
/* For security reasons, don't allow use of this page if a loader is already installed */
if (function_exists('sg_get_const'))
{
header('Location: index.php');
exit();
}
/* END LOADER CHECK */
/* START PHPINFO REQUEST */
if (isset($_GET['phpinfo']))
{
phpinfo();
exit();
}
/* END PHPINFO REQUEST */
/* START LANGUAGE */
$lang = 'en';
if (isset($_GET['lang']))
{
$lang = trim(strtolower(stripslashes($_GET['lang'])));
}
$langFile = sprintf('%s/raw/lang.%s.php', __DIR__, $lang);
if (!file_exists($langFile))
{
$langFile = sprintf('%s/raw/lang.en.php', __DIR__);
}
$lang = include $langFile;
/* END LANGUAGE */
ob_start();
?>
<h1><?php echo $lang['loader_required_title']; ?></h1>
<p><?php echo $lang['loader_required_p1']; ?></p>
<p><?php echo $lang['loader_required_p2']; ?></p>
<?php $url = ((isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? "https" : "http") . "://$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]?phpinfo=1"; ?>
<p><b><a href="<?php echo $url; ?>" target="_blank"><?php echo $url ?></a></b></p>
<p style="margin-top: 30px;"><a class="btn btn-primary" href="javascript:window.location.reload();"><?php echo $lang['loader_required_retry']; ?></a></p>
<?php
$content = ob_get_clean();
require sprintf('%s/raw/layout.php', __DIR__);
?>

View File

@ -1,36 +0,0 @@
<?php
return [
'app_name' => 'Blue Twilight - Install',
'copyright' => sprintf('© %s <a href="http://www.andyheathershaw.uk/" target="_blank">Andy Heathershaw</a>.', (date('Y') == 2016 ? 2016 : '2016-' . date('Y'))),
'license_errors' => [
1 => 'The application is not licensed to run on this machine or domain.',
2 => 'The application is not licensed to run on this machine or domain.',
3 => 'The application is not licensed to run on this machine or domain.',
6 => 'The license file provided is invalid.',
7 => 'This version of PHP is not supported. Please upgrade to a supported version of PHP or contact support.',
9 => 'The application/license has expired.',
13 => 'No license is currently available.',
20 => 'The application requires an Internet connection that was not available.',
99 => 'An unexpected error occurred, please contact support.'
],
'license_required_p1' => 'Blue Twilight requires a license to run correctly. You can generate and download a license file from the <a href="http://shop.andyheathershaw.uk/user/orders" target="_blank">My Orders</a> page.',
'license_required_p2' => 'Your license file must match the hostname: :host_name.',
'license_required_title' => 'License Required',
'loader_required_p1' => 'Blue Twilight uses the Source Guardian source code protection system which requires a small &quot;loader&quot; to be installed on your system.',
'loader_required_p2' => 'Please see <a href="https://www.sourceguardian.com/loaders.html" target="_blank">this web page</a> to download the loader for your system. You can use the &quot;loader assistant&quot; and provide the following URL for the PHP information:',
'loader_required_retry' => 'Click here to retry',
'loader_required_title' => 'Source Guardian Loader Required',
'powered_by' => 'Powered by <a href="http://www.andyheathershaw.uk/blue-twilight" target="_blank">Blue Twilight</a> - the self-hosted photo gallery software.',
'upload_action' => 'Upload',
'upload_errors' => [
1 => 'The uploaded file exceeds the upload_max_filesize directive in php.ini.',
2 => 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form.',
3 => 'The uploaded file was only partially uploaded.',
4 => 'No file was uploaded.',
6 => 'Missing a temporary folder.',
7 => 'Failed to write file to disk.',
8 => 'A PHP extension blocked the file upload - please contact your system administrator.',
99 => 'Failed to write the license file - please check file permissions in Blue Twilight\'s root directory.'
],
'upload_license_label' => 'Please locate your license file to upload:'
];

View File

@ -1,60 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="generator" content="{{ config('app.name') }} v{{ config('app.version') }} (framework v{{ App::VERSION() }})">
<title><?php echo $lang['app_name']; ?></title>
<link href="themes/base/bootstrap/css/bootstrap.min.css" rel="stylesheet">
<link href="themes/base/font-awesome/css/font-awesome.min.css" rel="stylesheet">
<link href="themes/base/css/app.css" rel="stylesheet">
<link href="themes/bootstrap3/theme.css" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-inverse navbar-static-top">
<div class="container">
<!-- Brand and toggle get grouped for better mobile display -->
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="index.php"><i class="fa fa-fw fa-photo"></i> <?php echo $lang['app_name']; ?></a>
</div>
<!-- Collect the nav links, forms, and other content for toggling -->
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav">
</ul>
</div><!-- /.navbar-collapse -->
</div><!-- /.container-fluid -->
</nav>
<div class="container-fluid">
<div class="container">
<?php echo $content; ?>
</div>
<div class="container" style="margin-top: 40px;">
<div class="row">
<div class="col-xs-12">
<hr/>
<p style="font-size: smaller;">
<b><?php echo $lang['powered_by']; ?></b><br/>
<?php echo $lang['copyright']; ?>
</p>
</div>
</div>
</div>
</div>
<script src="themes/base/js/jquery.min.js"></script>
<script src="themes/base/bootstrap/js/bootstrap.min.js"></script>
<script src="themes/base/js/app.js"></script>
</body>
</html>

View File

@ -1,37 +0,0 @@
<?php
if (!function_exists('btw_license_error'))
{
function btw_license_error($code, $message)
{
// Remove an invalid license file - relevant to index.php as that's the main entry script
$licenseFile = sprintf('%s/blue-twilight.lic', dirname(__DIR__));
if (file_exists($licenseFile))
{
$number = 0;
do
{
$backupFilename = sprintf('%s_invalid.%d', $licenseFile, $number++);
} while (file_exists($backupFilename));
// Rename or remove the invalid file
if (!rename($licenseFile, $backupFilename))
{
@unlink($licenseFile);
}
}
header(sprintf('Location: license-required.php?licerror=%d', $code));
exit();
}
}
if (!function_exists('btw_loader_error'))
{
function btw_loader_error()
{
header('Location: loader-required.php');
exit();
}
}
?>

View File

@ -17,4 +17,8 @@
.no-padding {
padding: 0;
}
span.twitter-typeahead {
width: 100%;
}

30
public/themes/base/css/typeahead.css vendored Normal file
View File

@ -0,0 +1,30 @@
.tt-hint {
color: #999;
}
.tt-menu {
width: 422px;
margin-top: 12px;
padding: 8px 0;
background-color: #fff;
border: 1px solid #ccc;
border: 1px solid rgba(0, 0, 0, 0.2);
border-radius: 8px;
box-shadow: 0 5px 10px rgba(0,0,0,.2);
}
.tt-suggestion {
padding: 3px 20px;
font-size: 18px;
line-height: 24px;
}
.tt-suggestion.tt-cursor {
color: #fff;
background-color: #0097cf;
}
.tt-suggestion p {
margin: 0;
}

View File

@ -1,120 +0,0 @@
<?php
define('PROJECT_ID', 'blue-twilight');
define('PROJECT_KEY', 's7Ebes37ChACHeC8A8AzAXUswestuZ8v');
define('LICENSE_FILE', 'blue-twilight.lic');
// Regex patterns of files we don't want to ship
$ignoredFiles = [
'.env*',
'.git*',
'.idea/*',
'artisan',
'build.php',
'composer.phar',
'gulpfile.js',
'package.json',
'phpunit.xml',
'readme.md',
'server.php',
'storage/app/*',
'storage/framework/*',
'storage/logs/*',
'tests/*'
];
echo 'Blue Twilight Packaging Script' . PHP_EOL;
echo '(c) Andy Heathershaw 2016-2017' . PHP_EOL;
echo '------------------------------' . PHP_EOL . PHP_EOL;
if ($argc != 2)
{
echo sprintf('Usage: %s [version_number]', $argv[0]) . PHP_EOL . PHP_EOL;
exit(1);
}
echo 'Checking current folder...' . PHP_EOL . PHP_EOL;
$appRoot = dirname(dirname(__DIR__));
if (getcwd() != $appRoot)
{
echo sprintf('The build script must be run in the application root - %s', $appRoot) . PHP_EOL;
exit();
}
echo 'Updating app.version config value...' . PHP_EOL . PHP_EOL;
$appConfigFile = sprintf('%s/config/app.php', $appRoot);
file_put_contents($appConfigFile, str_replace('**DEV**', $argv[1], file_get_contents($appConfigFile)));
/*echo 'Downloading Composer...' . PHP_EOL . PHP_EOL;
copy('https://getcomposer.org/installer', 'composer-setup.php');
if (hash_file('SHA384', 'composer-setup.php') === trim(file_get_contents('https://composer.github.io/installer.sig'))) { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); exit; } echo PHP_EOL;
system('php composer-setup.php');
unlink('composer-setup.php');
echo 'Installing dependencies using Composer...' . PHP_EOL . PHP_EOL;
system('./composer.phar install');
echo PHP_EOL;
echo 'Licensing and encoding the application using Source Guardian...' . PHP_EOL . PHP_EOL;
$sgCommand = sprintf(
'/usr/local/sourceguardian/bin/sourceguardian --phpversion "5.6" --phpversion "7.0" --external "%s" --projid "%s" --projkey "%s" ' .
'--stop-on-error --strict-errors --deprec-errors -x "build.php" -x "*.blade.php" -x "vendor/*.php" -x "public/raw/*.php" ' .
'-x public/license-required.php -x public/loader-required.php -x "storage/*" -r -b- ' .
'-p @./public/raw/sg-license-error.php -j "<?php btw_loader_error(); ?>" --catch ERR_ALL="btw_license_error" "*.php"',
LICENSE_FILE,
PROJECT_ID,
PROJECT_KEY
);
system($sgCommand);*/
echo 'Creating the release archive...' . PHP_EOL . PHP_EOL;
// Initialize archive object
$zip = new ZipArchive();
$zip->open(sprintf('%s/blue-twilight_%s.zip', dirname($appRoot), $argv[1]), ZipArchive::CREATE | ZipArchive::OVERWRITE);
/** @var SplFileInfo[] $files */
$files = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($appRoot),
RecursiveIteratorIterator::LEAVES_ONLY
);
foreach ($files as $name => $file)
{
// Skip directories (they will be added automatically) and unnecessary files
if (!$file->isDir())
{
// Get real and relative path for current file
$filePath = $file->getRealPath();
$relativePath = substr($filePath, strlen($appRoot) + 1);
// See if the file matches any of ignore patterns
$includeFile = true;
if (
strlen($relativePath) < strlen('vendor') ||
substr($relativePath, 0, strlen('vendor')) != 'vendor'
)
{
array_walk($ignoredFiles, function ($value) use ($relativePath, &$includeFile)
{
$includeFile &= !(preg_match('/^' . preg_quote($value, '/') . '$/', $relativePath));
});
}
// Add to the archive
if ($includeFile)
{
$zip->addFile($filePath, sprintf('blue-twilight_%s/%s', $argv[1], $relativePath));
}
}
}
$zip->close();
echo PHP_EOL . PHP_EOL;
echo 'All done!';
echo PHP_EOL . PHP_EOL;
exit();
?>

View File

@ -7,6 +7,7 @@ return [
'apply_action' => 'Apply',
'bulk_edit_photos_label' => 'Bulk edit selected photos:',
'bulk_edit_photos_placeholder' => 'Select an action',
'can_create_albums_label' => 'User can create new albums',
'cancel_action' => 'Cancel',
'continue_action' => 'Continue',
'create_action' => 'Create',

View File

@ -86,6 +86,13 @@
</label>
</div>
<div class="checkbox">
<label>
<input type="checkbox" name="can_create_albums">
<strong>@lang('forms.can_create_albums_label')</strong>
</label>
</div>
<div class="form-actions">
<a href="{{ route('users.index') }}" class="btn btn-default">@lang('forms.cancel_action')</a>
<button type="submit" class="btn btn-success"><i class="fa fa-fw fa-check"></i> @lang('forms.create_action')</button>

View File

@ -97,6 +97,13 @@
</label>
</div>
<div class="checkbox">
<label>
<input type="checkbox" name="can_create_albums"@if ($user->can_create_albums) checked="checked"@endif>
<strong>@lang('forms.can_create_albums_label')</strong>
</label>
</div>
@if (!$user->is_activated)
<div class="checkbox">
<label>

View File

@ -180,42 +180,34 @@
@if (count($existing_groups) > 0)
<div class="panel-group" id="groups-accordion" role="tablist" aria-multiselectable="true">
@foreach ($existing_groups as $group)
<div class="panel panel-default">
<div class="panel-heading" role="tab" id="heading-{{ $group->id }}">
<h4 class="panel-title">
<a class="collapsed" role="button" data-toggle="collapse" data-parent="#groups-accordion" href="#collapse-{{ $group->id }}" aria-expanded="true" aria-controls="collapse-{{ $group->id }}">
{{ $group->name }}
</a>
</h4>
</div>
<div id="collapse-{{ $group->id }}" class="panel-collapse collapse" role="tabpanel" aria-labelledby="heading-{{ $group->id }}">
<div class="panel-body">
<p style="margin-bottom: 20px;"><a class="select-all" href="#">Select All</a> &middot; <a class="select-none" href="">Select None</a></p>
@foreach ($all_permissions as $permission)
<div class="checkbox">
<label for="permission|{{ $group->id }}|{{ $permission->id }}">
<input id="permission|{{ $group->id }}|{{ $permission->id }}" name="permissions[{{ $group->id }}][]" value="{{ $permission->id }}" type="checkbox"{{ $album->doesGroupHavePermission($group, $permission) ? ' checked="checked"' : '' }} /> {{ trans(sprintf('permissions.%s.%s', $permission->section, $permission->description)) }}
</label>
</div>
@endforeach
</div>
</div>
</div>
@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
<div class="form-group">
<select class="form-control" name="group_id" style="width: auto; display: inline;"@if (count($add_new_groups) == 0) disabled="disabled"@endif>
@foreach ($add_new_groups as $group)
<option value="{{ $group->id }}">{{ $group->name }}</option>
@endforeach
</select>
<button type="submit" name="action" value="add_group" class="btn btn-primary"@if (count($add_new_groups) == 0) disabled="disabled"@endif>Assign Permissions</button>
<button type="submit" name="action" value="update_group_permissions" class="btn btn-success pull-right">
<i class="fa fa-fw fa-check"></i> @lang('forms.save_action')
</button>
<div class="row">
<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/>
@ -227,39 +219,41 @@
<div class="panel-group" id="users-accordion" role="tablist" aria-multiselectable="true">
{{-- Anonymous users --}}
<div class="panel panel-default">
<div class="panel-heading" role="tab" id="heading-anonymous">
<h4 class="panel-title">
<a class="collapsed" role="button" data-toggle="collapse" data-parent="#users-accordion" href="#collapse-anonymous" aria-expanded="true" aria-controls="collapse-anonymous">
@lang('admin.anonymous_users')
</a>
</h4>
</div>
<div id="collapse-anonymous" class="panel-collapse collapse" role="tabpanel" aria-labelledby="heading-anonymous">
<div class="panel-body">
<p style="margin-bottom: 20px;"><a class="select-all" href="#">Select All</a> &middot; <a class="select-none" href="">Select None</a></p>
@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 ($all_permissions as $permission)
<div class="checkbox">
<label for="permission|anonymous|{{ $permission->id }}">
<input id="permission|anonymous|{{ $permission->id }}" name="permissions[anonymous][]" value="{{ $permission->id }}" type="checkbox"{{ $album->doesAnonymousHavePermission($permission) ? ' checked="checked"' : '' }} /> {{ trans(sprintf('permissions.%s.%s', $permission->section, $permission->description)) }}
</label>
</div>
@endforeach
</div>
</div>
@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">
<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="form-group">
<input class="form-control" name="user_id" size="20" style="width: auto; display: inline;" />
<button type="submit" name="action" value="add_user" class="btn btn-primary">Assign Permissions</button>
<button type="submit" name="action" value="update_user_permissions" class="btn btn-success pull-right">
<i class="fa fa-fw fa-check"></i> @lang('forms.save_action')
</button>
</div>
<div class="clearfix"><!-- --></div>
</form>
</div>
@ -455,6 +449,26 @@
return false;
});
{{-- Type-ahead support for users textbox on the permissions tab --}}
var userDataSource = new Bloodhound({
datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'),
queryTokenizer: Bloodhound.tokenizers.whitespace,
//prefetch: '../data/films/post_1960.json',
remote: {
url: '{{ route('users.searchJson') }}?q=%QUERY',
wildcard: '%QUERY'
}
});
$('#user-search-textbox').typeahead(null, {
name: 'user-search',
display: 'name',
source: userDataSource
});
$('#user-search-textbox').bind('typeahead:select', function(ev, suggestion) {
$('#user-id-field').val(suggestion.id);
});
// Bind the view models to the relevant tab
ko.applyBindings(editViewModel, document.getElementById('photos-tab'));
ko.applyBindings(viewModel, document.getElementById('upload-tab'));

View File

@ -15,6 +15,7 @@
{{-- As these files are shipped with core (not a theme) use the main app.version instead of the current theme's version --}}
<link href="themes/base/bootstrap/css/bootstrap.min.css?v={{ urlencode(config('app.version')) }}" rel="stylesheet">
<link href="themes/base/font-awesome/css/font-awesome.min.css?v={{ urlencode(config('app.version')) }}" rel="stylesheet">
<link href="themes/base/css/typeahead.css?v={{ urlencode(config('app.version')) }}" rel="stylesheet">
<link href="themes/base/css/app.css?v={{ urlencode(config('app.version')) }}" rel="stylesheet">
@if (\App\Facade\Theme::hasStylesheet())
@ -76,6 +77,7 @@
<script src="themes/base/bootstrap/js/bootstrap.min.js?v={{ urlencode(config('app.version')) }}"></script>
<script src="themes/base/js/bootbox.min.js?v={{ urlencode(config('app.version')) }}"></script>
<script src="themes/base/js/knockout.min.js?v={{ urlencode(config('app.version')) }}"></script>
<script src="themes/base/js/typeahead.bundle.js?v={{ urlencode(config('app.version')) }}"></script>
<script src="themes/base/js/app.js?v={{ urlencode(config('app.version')) }}"></script>
<script type="text/javascript">
function _bt_showLoadingModal() {

View File

@ -3,10 +3,13 @@
<div class="panel-body">
<ul class="nav nav-pills">
<li role="presentation"><a href="{{ route('albums.index') }}"><i class="fa fa-fw fa-picture-o"></i> @lang('navigation.breadcrumb.albums')</a></li>
<li role="presentation"><a href="{{ route('users.index') }}"><i class="fa fa-fw fa-user"></i> @lang('navigation.breadcrumb.users')</a></li>
<li role="presentation"><a href="{{ route('groups.index') }}"><i class="fa fa-fw fa-users"></i> @lang('navigation.breadcrumb.groups')</a></li>
<li role="presentation"><a href="{{ route('storage.index') }}"><i class="fa fa-fw fa-folder"></i> @lang('navigation.breadcrumb.storage')</a></li>
<li role="presentation"><a href="{{ route('admin.settings') }}"><i class="fa fa-fw fa-cog"></i> @lang('navigation.breadcrumb.settings')</a></li>
@can('admin-access')
<li role="presentation"><a href="{{ route('users.index') }}"><i class="fa fa-fw fa-user"></i> @lang('navigation.breadcrumb.users')</a></li>
<li role="presentation"><a href="{{ route('groups.index') }}"><i class="fa fa-fw fa-users"></i> @lang('navigation.breadcrumb.groups')</a></li>
<li role="presentation"><a href="{{ route('storage.index') }}"><i class="fa fa-fw fa-folder"></i> @lang('navigation.breadcrumb.storage')</a></li>
<li role="presentation"><a href="{{ route('admin.settings') }}"><i class="fa fa-fw fa-cog"></i> @lang('navigation.breadcrumb.settings')</a></li>
@endcan
</ul>
</div>
</div>

View File

@ -3,8 +3,11 @@
<div class="panel-body">
<p>
<b>{{ $album_count }}</b> {{ trans_choice('admin.stats_widget.albums', $album_count) }}<br/>
<b>{{ $photo_count }}</b> {{ trans_choice('admin.stats_widget.photos', $photo_count) }}<br/>
<b>{{ $user_count }}</b> {{ trans_choice('admin.stats_widget.users', $user_count) }} / <b>{{ $group_count }}</b> {{ trans_choice('admin.stats_widget.groups', $group_count) }}
<b>{{ $photo_count }}</b> {{ trans_choice('admin.stats_widget.photos', $photo_count) }}
@can('admin-access')
<br/>
<b>{{ $user_count }}</b> {{ trans_choice('admin.stats_widget.users', $user_count) }} / <b>{{ $group_count }}</b> {{ trans_choice('admin.stats_widget.groups', $group_count) }}
@endcan
</p>
</div>
</div>

View File

@ -0,0 +1,20 @@
<div class="panel panel-default">
<div class="panel-heading" role="tab" id="heading-{{ $key_id }}">
<h4 class="panel-title">
<a class="collapsed" role="button" data-toggle="collapse" data-parent="#{{ $parent_id }}" href="#collapse-{{ $key_id }}" aria-expanded="true" aria-controls="collapse-{{ $key_id }}">{{ $title }}</a>
</h4>
</div>
<div id="collapse-{{ $key_id }}" class="panel-collapse collapse" role="tabpanel" aria-labelledby="heading-{{ $key_id }}">
<div class="panel-body">
<p style="margin-bottom: 20px;"><a class="select-all" href="#">Select All</a> &middot; <a class="select-none" href="">Select None</a></p>
@foreach ($all_permissions as $permission)
<div class="checkbox">
<label for="permission|{{ $key_id }}|{{ $permission->id }}">
<input id="permission|{{ $key_id }}|{{ $permission->id }}" name="permissions[{{ $object_id }}][]" value="{{ $permission->id }}" type="checkbox"{{ call_user_func($callback, $callback_object, $permission) ? ' checked="checked"' : '' }} /> {{ trans(sprintf('permissions.%s.%s', $permission->section, $permission->description)) }}
</label>
</div>
@endforeach
</div>
</div>
</div>

View File

@ -16,7 +16,6 @@
])
</p>
<p style="font-size: smaller;">
@lang('global.licensed_to', ['name' => $license_name, 'number' => $license_no])<br/>
@lang('global.version_number', ['version' => config('app.version')])
</p>
</div>

View File

@ -25,9 +25,9 @@
</li>
@endif
@can('admin-access')
@if (!Auth::guest() && (Auth::user()->can('admin-access') || Auth::user()->can('admin-create-albums')))
<li><a href="{{ route('admin') }}"><i class="fa fa-fw fa-cog"></i> @lang('navigation.navbar.admin')</a></li>
@endcan
@endif
</ul>
<ul class="nav navbar-nav navbar-right">

View File

@ -42,7 +42,7 @@ Route::group(['prefix' => 'admin'], function () {
Route::get('albums/{id}/analyse/{queue_token}', 'Admin\AlbumController@analyse')->name('albums.analyse');
Route::get('albums/{id}/delete', 'Admin\AlbumController@delete')->name('albums.delete');
Route::post('albums/{id}/set-group-permissions', 'Admin\AlbumController@setGroupPermissions')->name('albums.set_group_permissions');
Route::post('albums/{id}/set-user-permissions', 'Admin\AlbumController@setGroupPermissions')->name('albums.set_user_permissions');
Route::post('albums/{id}/set-user-permissions', 'Admin\AlbumController@setUserPermissions')->name('albums.set_user_permissions');
Route::resource('albums', 'Admin\AlbumController');
// Photo management
@ -61,6 +61,7 @@ Route::group(['prefix' => 'admin'], function () {
// User management
Route::get('users/{id}/delete', 'Admin\UserController@delete')->name('users.delete');
Route::get('users.json', 'Admin\UserController@searchJson')->name('users.searchJson');
Route::resource('users', 'Admin\UserController');
// Group management