BLUE-1: A default local storage location is created on install that cannot be deleted. Storage locations can be made inactive and no new albums can be created against them.

BLUE-3: Validation is now performed on the file path selected.

Tweaks to the storage locations form to display validation errors against the correct fields.
This commit is contained in:
Andy Heathershaw 2016-10-27 11:36:37 +01:00
parent 79111ed6ca
commit e7fbdaaa66
16 changed files with 235 additions and 65 deletions

View File

@ -0,0 +1,39 @@
<?php
namespace App\Helpers;
class ValidationHelper
{
public function directoryExists($attribute, $value, $parameters, $validator)
{
return file_exists($value) && is_dir($value);
}
public function isDirectoryEmpty($attribute, $value, $parameters, $validator)
{
if (!$this->directoryExists($attribute, $value, $parameters, $validator))
{
return false;
}
$iterator = new \DirectoryIterator($value);
$count = 0;
foreach ($iterator as $item)
{
if ($item->isDot())
{
continue;
}
$count++;
}
return ($count == 0);
}
public function isPathWriteable($attribute, $value, $parameters, $validator)
{
return $this->directoryExists($attribute, $value, $parameters, $validator) && is_writeable($value);
}
}

View File

@ -49,7 +49,7 @@ class AlbumController extends Controller
$this->authorize('admin-access'); $this->authorize('admin-access');
$albumSources = []; $albumSources = [];
foreach (Storage::all()->sortBy('name') as $storage) foreach (Storage::where('is_active', true)->orderBy('name')->get() as $storage)
{ {
$albumSources[$storage->id] = $storage->name; $albumSources[$storage->id] = $storage->name;
} }
@ -60,11 +60,11 @@ class AlbumController extends Controller
return redirect(route('storage.create')); return redirect(route('storage.create'));
} }
$defaultSourceId = Storage::where('is_default', true)->limit(1)->first(); $defaultSource = Storage::where('is_default', true)->limit(1)->first();
return Theme::render('admin.create_album', [ return Theme::render('admin.create_album', [
'album_sources' => $albumSources, 'album_sources' => $albumSources,
'default_storage_id' => (!is_null($defaultSourceId) ? $defaultSourceId->id : 0) 'default_storage_id' => (!is_null($defaultSource) ? $defaultSource->id : 0)
]); ]);
} }

View File

@ -34,7 +34,8 @@ class StorageController extends Controller
return Theme::render('admin.list_storage', [ return Theme::render('admin.list_storage', [
'error' => $request->session()->get('error'), 'error' => $request->session()->get('error'),
'storageLocations' => $storageLocations 'storageLocations' => $storageLocations,
'warning' => $request->session()->get('warning'),
]); ]);
} }
@ -68,7 +69,9 @@ class StorageController extends Controller
$storage = new Storage(); $storage = new Storage();
$storage->fill($request->only(['name', 'source', 'location'])); $storage->fill($request->only(['name', 'source', 'location']));
$storage->is_active = true;
$storage->is_default = (strtolower($request->get('is_default')) == 'on'); $storage->is_default = (strtolower($request->get('is_default')) == 'on');
$storage->is_internal = false;
$storage->save(); $storage->save();
if ($storage->is_default) if ($storage->is_default)
@ -106,10 +109,17 @@ class StorageController extends Controller
App::abort(404); App::abort(404);
} }
if ($storage->is_internal)
{
// Can't delete the default storage location
$request->session()->flash('warning', trans('admin.delete_storage_internal'));
return redirect(route('storage.index'));
}
if ($storage->albums()->count() > 0) if ($storage->albums()->count() > 0)
{ {
// Can't delete storage location while albums exist // Can't delete storage location while albums exist
$request->session()->set('error', trans('admin.delete_storage_existing_albums')); $request->session()->flash('error', trans('admin.delete_storage_existing_albums'));
return redirect(route('storage.index')); return redirect(route('storage.index'));
} }
@ -153,7 +163,14 @@ class StorageController extends Controller
} }
$storage->fill($request->only(['name'])); $storage->fill($request->only(['name']));
$storage->is_active = (strtolower($request->get('is_active')) == 'on');
$storage->is_default = (strtolower($request->get('is_default')) == 'on'); $storage->is_default = (strtolower($request->get('is_default')) == 'on');
if ($storage->is_default && !$storage->is_active)
{
$storage->is_default = false;
}
$storage->save(); $storage->save();
if ($storage->is_default) if ($storage->is_default)
@ -180,6 +197,13 @@ class StorageController extends Controller
App::abort(404); App::abort(404);
} }
if ($storage->is_internal)
{
// Can't delete the default storage location
$request->session()->flash('warning', trans('admin.delete_storage_internal'));
return redirect(route('storage.index'));
}
if ($storage->albums()->count() > 0) if ($storage->albums()->count() > 0)
{ {
// Can't delete storage location while albums exist // Can't delete storage location while albums exist

View File

@ -6,6 +6,7 @@ use App\Configuration;
use App\Facade\UserConfig; use App\Facade\UserConfig;
use App\Helpers\MiscHelper; use App\Helpers\MiscHelper;
use App\Http\Requests\StoreUserRequest; use App\Http\Requests\StoreUserRequest;
use App\Storage;
use App\User; use App\User;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\Artisan;
@ -27,9 +28,7 @@ class InstallController extends Controller
if ($canSkip && $request->has('skip')) if ($canSkip && $request->has('skip'))
{ {
MiscHelper::setEnvironmentSetting('APP_INSTALLED', true); return $this->completeSetup();
return redirect(route('home'));
} }
if ($request->method() == 'POST') if ($request->method() == 'POST')
@ -42,11 +41,7 @@ class InstallController extends Controller
$user->is_activated = true; $user->is_activated = true;
$user->save(); $user->save();
MiscHelper::setEnvironmentSetting('APP_INSTALLED', true); return $this->completeSetup();
$request->session()->flash('success', trans('installer.install_completed_message'));
return redirect(route('home'));
} }
return view('install.administrator', [ return view('install.administrator', [
@ -160,9 +155,6 @@ class InstallController extends Controller
$versionNumber->value = config('app.version'); $versionNumber->value = config('app.version');
$versionNumber->save(); $versionNumber->save();
// Now the database is up-to-date, we can enable database sessions
MiscHelper::setEnvironmentSetting('SESSION_DRIVER', 'database');
$request->session()->set('install_stage', 3); $request->session()->set('install_stage', 3);
return redirect(route('install.administrator')); return redirect(route('install.administrator'));
} }
@ -178,4 +170,43 @@ class InstallController extends Controller
'database_error' => $request->session()->get('database_error') 'database_error' => $request->session()->get('database_error')
]); ]);
} }
private function completeSetup()
{
// Flag as installed
MiscHelper::setEnvironmentSetting('APP_INSTALLED', true);
// Switch to database sessions (more reliable)
MiscHelper::setEnvironmentSetting('SESSION_DRIVER', 'database');
// Add an internal storage if it doesn't already exist
$this->createInternalStorageLocationIfNotExist();
// Skip forward. If you go past Go, collect £200!
return redirect(route('home', ['install_completed' => true]));
}
private function createInternalStorageLocationIfNotExist()
{
$storage = Storage::where('is_internal', true)->first();
if (is_null($storage))
{
$location = sprintf('%s/storage/app/albums', dirname(dirname(dirname(__DIR__))));
$storage = new Storage();
$storage->name = trans('installer.default_storage_name');
$storage->is_active = true;
$storage->is_default = true;
$storage->is_internal = true;
$storage->location = $location;
$storage->source = 'LocalFilesystemSource';
$storage->save();
// Try and create the physical location
if (!file_exists($location))
{
@mkdir($location);
}
}
}
} }

View File

@ -27,11 +27,18 @@ class StoreStorageRequest extends FormRequest
switch ($this->method()) switch ($this->method())
{ {
case 'POST': case 'POST':
return [ $result = [
'name' => 'required|unique:storages|max:255', 'name' => 'required|unique:storages|max:255',
'source' => 'required|max:255', 'source' => 'required|max:255',
]; ];
if ($this->get('source') == 'LocalFilesystemSource')
{
$result['location'] = 'sometimes|required|is_dir|dir_empty|is_writeable';
}
return $result;
case 'PATCH': case 'PATCH':
case 'PUT': case 'PUT':
$storageId = intval($this->segment(3)); $storageId = intval($this->segment(3));

View File

@ -2,17 +2,14 @@
namespace App\Providers; namespace App\Providers;
use App\Album;
use App\Configuration;
use App\Facade\Theme;
use App\Facade\UserConfig;
use App\Helpers\ConfigHelper; use App\Helpers\ConfigHelper;
use App\Helpers\ImageHelper; use App\Helpers\ImageHelper;
use App\Helpers\MiscHelper;
use App\Helpers\ThemeHelper; use App\Helpers\ThemeHelper;
use App\Helpers\ValidationHelper;
use Illuminate\Database\QueryException; use Illuminate\Database\QueryException;
use Illuminate\Mail\Mailer; use Illuminate\Mail\Mailer;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Facades\View; use Illuminate\Support\Facades\View;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
@ -37,6 +34,10 @@ class AppServiceProvider extends ServiceProvider
{ {
return new ConfigHelper(); return new ConfigHelper();
}); });
Validator::extend('is_dir', (ValidationHelper::class . '@directoryExists'));
Validator::extend('dir_empty', (ValidationHelper::class . '@isDirectoryEmpty'));
Validator::extend('is_writeable', (ValidationHelper::class . '@isPathWriteable'));
} }
/** /**

View File

@ -15,7 +15,7 @@ class Storage extends Model
* @var array * @var array
*/ */
protected $fillable = [ protected $fillable = [
'name', 'source', 'is_default', 'location' 'name', 'source', 'is_default', 'location', 'is_internal', 'is_active'
]; ];
public function albums() public function albums()

View File

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

View File

@ -34,6 +34,7 @@ return [
'create_user_title' => 'Create a user account', 'create_user_title' => 'Create a user account',
'danger_zone_heading' => 'Danger zone', 'danger_zone_heading' => 'Danger zone',
'danger_zone_intro' => 'The options below WILL cause data loss - please be careful!', 'danger_zone_intro' => 'The options below WILL cause data loss - please be careful!',
'default_storage_legend' => 'Default storage location for new albums.',
'delete_album' => 'Delete album :name', 'delete_album' => 'Delete album :name',
'delete_album_confirm' => 'Are you sure you want to permanently delete this album and all its contents?', 'delete_album_confirm' => 'Are you sure you want to permanently delete this album and all its contents?',
'delete_album_warning' => 'This is a permanent action that cannot be undone!', 'delete_album_warning' => 'This is a permanent action that cannot be undone!',
@ -45,6 +46,7 @@ return [
'delete_storage' => 'Delete storage location: :name', 'delete_storage' => 'Delete storage location: :name',
'delete_storage_confirm' => 'Are you sure you want to permanently remove this storage location?', 'delete_storage_confirm' => 'Are you sure you want to permanently remove this storage location?',
'delete_storage_existing_albums' => 'At least one album is still using the storage location. Please delete all albums before removing the storage location.', 'delete_storage_existing_albums' => 'At least one album is still using the storage location. Please delete all albums before removing the storage location.',
'delete_storage_internal' => 'You cannot delete the local, internal storage location. You can de-activate it using the Edit link below.',
'delete_storage_warning' => 'This is a permanent action that cannot be reversed!', 'delete_storage_warning' => 'This is a permanent action that cannot be reversed!',
'delete_user' => 'Delete user account: :name', 'delete_user' => 'Delete user account: :name',
'delete_user_confirm' => 'Are you sure you want to permanently remove :name\'s user account? They will be immediately logged out.', 'delete_user_confirm' => 'Are you sure you want to permanently remove :name\'s user account? They will be immediately logged out.',
@ -57,7 +59,9 @@ return [
'edit_storage_intro' => 'Use the form below to update the details of the :storage_name storage location.', 'edit_storage_intro' => 'Use the form below to update the details of the :storage_name storage location.',
'edit_user_intro' => 'You can use the form below to edit the above user account. Changes take effect immediately.', 'edit_user_intro' => 'You can use the form below to edit the above user account. Changes take effect immediately.',
'edit_user_title' => 'Edit user account: :name', 'edit_user_title' => 'Edit user account: :name',
'inactive_storage_legend' => 'Inactive storage location that cannot be used for new albums.',
'is_uploading' => 'Uploading in progress...', 'is_uploading' => 'Uploading in progress...',
'legend' => 'Legend/Key',
'manage_widget' => [ 'manage_widget' => [
'panel_header' => 'Manage' 'panel_header' => 'Manage'
], ],

View File

@ -29,6 +29,7 @@ return [
'settings_hotlink_protection_help' => 'With this option enabled, direct linking to images is not allowed. Photos can only be viewed through Blue Twilight.', 'settings_hotlink_protection_help' => 'With this option enabled, direct linking to images is not allowed. Photos can only be viewed through Blue Twilight.',
'settings_restrict_originals_download' => 'Restrict access to original images', 'settings_restrict_originals_download' => 'Restrict access to original images',
'settings_restrict_originals_download_help' => 'With this option enabled, only the photo\'s owner can download the original high-resolution images.', 'settings_restrict_originals_download_help' => 'With this option enabled, only the photo\'s owner can download the original high-resolution images.',
'storage_active_label' => 'Location is active. Uncheck to prevent creating new albums in this location.',
'storage_driver_label' => 'Storage driver:', 'storage_driver_label' => 'Storage driver:',
'storage_location_label' => 'Physical location:', 'storage_location_label' => 'Physical location:',
'upload_action' => 'Upload', 'upload_action' => 'Upload',

View File

@ -9,6 +9,7 @@ return [
], ],
'database_intro' => 'Please provide the connection details for an empty MySQL or MariaDB database.', 'database_intro' => 'Please provide the connection details for an empty MySQL or MariaDB database.',
'database_title' => 'Connect to a Database', 'database_title' => 'Connect to a Database',
'default_storage_name' => 'Local',
'install_completed_message' => 'Congratulations, Blue Twilight has been installed successfully. You can now login with an administrator account using the "Login" link above.', 'install_completed_message' => 'Congratulations, Blue Twilight has been installed successfully. You can now login with an administrator account using the "Login" link above.',
'php_config' => [ 'php_config' => [
'heading' => 'PHP configuration:', 'heading' => 'PHP configuration:',

View File

@ -112,4 +112,8 @@ return [
'attributes' => [], 'attributes' => [],
// Added by Andy H. for custom validators
'dir_empty' => 'The path must be an empty folder.',
'is_dir' => 'The folder must be a valid directory.',
'is_writeable' => 'Unable to write to this folder - please check permissions.'
]; ];

View File

@ -22,32 +22,34 @@
<p>@lang('admin.create_storage_intro')</p> <p>@lang('admin.create_storage_intro')</p>
<hr/> <hr/>
@if (count($errors) > 0)
<div class="alert alert-danger">
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
{!! Form::open(['route' => 'storage.store', 'method' => 'POST']) !!} {!! Form::open(['route' => 'storage.store', 'method' => 'POST']) !!}
<div class="form-group"> <div class="form-group{{ $errors->has('name') ? ' has-error' : '' }}">
{!! Form::label('name', trans('forms.name_label'), ['class' => 'control-label']) !!} {!! Form::label('name', trans('forms.name_label'), ['class' => 'control-label']) !!}
{!! Form::text('name', old('name'), ['class' => 'form-control']) !!} {!! Form::text('name', old('name'), ['class' => 'form-control']) !!}
@if ($errors->has('name'))
<span class="help-block">
<strong>{{ $errors->first('name') }}</strong>
</span>
@endif
</div> </div>
<div class="form-group"> <div class="form-group">
{!! Form::label('source', trans('forms.album_source_label'), ['class' => 'control-label']) !!} {!! Form::label('source', trans('forms.storage_driver_label'), ['class' => 'control-label']) !!}
{!! Form::select('source', $album_sources, old('source'), ['class' => 'form-control', 'data-bind' => 'value: selectedLocation']) !!} {!! Form::select('source', $album_sources, old('source'), ['class' => 'form-control', 'data-bind' => 'value: selectedLocation']) !!}
</div> </div>
<div id="storage-options"> <div id="storage-options">
<div id="local-filesystem" data-bind="visible: selectedLocation() == 'LocalFilesystemSource'"> <div id="local-filesystem" data-bind="visible: selectedLocation() == 'LocalFilesystemSource'">
<div class="form-group"> <div class="form-group{{ $errors->has('location') ? ' has-error' : '' }}">
{!! Form::label('location', trans('forms.storage_driver_label'), ['class' => 'control-label']) !!} {!! Form::label('location', trans('forms.storage_location_label'), ['class' => 'control-label']) !!}
{!! Form::text('location', old('location', $filesystem_default_location), ['class' => 'form-control']) !!} {!! Form::text('location', old('location', $filesystem_default_location), ['class' => 'form-control']) !!}
@if ($errors->has('location'))
<span class="help-block">
<strong>{{ $errors->first('location') }}</strong>
</span>
@endif
</div> </div>
</div> </div>
</div> </div>
@ -60,7 +62,7 @@
</div> </div>
<div class="form-actions"> <div class="form-actions">
<a href="{{ route('albums.index') }}" class="btn btn-default">@lang('forms.cancel_action')</a> <a href="{{ route('storage.index') }}" class="btn btn-default">@lang('forms.cancel_action')</a>
{!! Form::submit(trans('forms.create_action'), ['class' => 'btn btn-success']) !!} {!! Form::submit(trans('forms.create_action'), ['class' => 'btn btn-success']) !!}
</div> </div>
{!! Form::close() !!} {!! Form::close() !!}

View File

@ -22,20 +22,16 @@
<p>@lang('admin.edit_storage_intro', ['storage_name' => $storage->name])</p> <p>@lang('admin.edit_storage_intro', ['storage_name' => $storage->name])</p>
<hr/> <hr/>
@if (count($errors) > 0)
<div class="alert alert-danger">
<ul>
@foreach ($errors->all() as $formError)
<li>{{ $formError }}</li>
@endforeach
</ul>
</div>
@endif
{!! Form::model($storage, ['route' => ['storage.update', $storage->id], 'method' => 'PUT']) !!} {!! Form::model($storage, ['route' => ['storage.update', $storage->id], 'method' => 'PUT']) !!}
<div class="form-group"> <div class="form-group{{ $errors->has('name') ? ' has-error' : '' }}">
{!! Form::label('name', trans('forms.name_label'), ['class' => 'control-label']) !!} {!! Form::label('name', trans('forms.name_label'), ['class' => 'control-label']) !!}
{!! Form::text('name', old('name'), ['class' => 'form-control']) !!} {!! Form::text('name', old('name'), ['class' => 'form-control']) !!}
@if ($errors->has('name'))
<span class="help-block">
<strong>{{ $errors->first('name') }}</strong>
</span>
@endif
</div> </div>
<div class="checkbox"> <div class="checkbox">
@ -45,6 +41,13 @@
</label> </label>
</div> </div>
<div class="checkbox">
<label>
<input type="checkbox" name="is_active"@if ($storage->is_active) checked="checked"@endif>
<strong>@lang('forms.storage_active_label')</strong>
</label>
</div>
<div class="form-actions"> <div class="form-actions">
<a href="{{ route('storage.index') }}" class="btn btn-default">@lang('forms.cancel_action')</a> <a href="{{ route('storage.index') }}" class="btn btn-default">@lang('forms.cancel_action')</a>
{!! Form::submit(trans('forms.save_action'), ['class' => 'btn btn-success']) !!} {!! Form::submit(trans('forms.save_action'), ['class' => 'btn btn-success']) !!}

View File

@ -32,12 +32,17 @@
@foreach ($storageLocations as $storage) @foreach ($storageLocations as $storage)
<tr> <tr>
<td> <td>
<span style="font-size: 1.3em;">{{ $storage->name }}@if ($storage->is_default) <i class="fa fa-fw fa-check text-success"></i>@endif</span><br/> <span style="font-size: 1.3em;">
{{ $storage->location }} {{ $storage->name }}
@if ($storage->is_default) <i class="fa fa-fw fa-check text-success"></i>@endif
@if (!$storage->is_active) <i class="fa fa-fw fa-minus-circle text-danger"></i>@endif
</span><br/>{{ $storage->location }}
</td> </td>
<td class="text-right"> <td class="text-right">
<a href="{{ route('storage.edit', ['id' => $storage->id]) }}" class="btn btn-default">@lang('forms.edit_action')</a> <a href="{{ route('storage.edit', ['id' => $storage->id]) }}" class="btn btn-default">@lang('forms.edit_action')</a>
@if (!$storage->is_internal)
<a href="{{ route('storage.delete', ['id' => $storage->id]) }}" class="btn btn-danger">@lang('forms.delete_action')</a> <a href="{{ route('storage.delete', ['id' => $storage->id]) }}" class="btn btn-danger">@lang('forms.delete_action')</a>
@endif
</td> </td>
</tr> </tr>
@endforeach @endforeach
@ -51,6 +56,20 @@
<div class="pull-right" style="margin-top: 10px;"> <div class="pull-right" style="margin-top: 10px;">
<a href="{{ route('storage.create') }}" class="btn btn-success"><i class="fa fa-fw fa-plus"></i> @lang('admin.create_storage')</a> <a href="{{ route('storage.create') }}" class="btn btn-success"><i class="fa fa-fw fa-plus"></i> @lang('admin.create_storage')</a>
</div> </div>
<div class="clearfix"><!-- --></div>
<div class="row" style="margin-top: 15px;">
<div class="col-sm-6">
<div class="panel panel-info">
<div class="panel-heading">@lang('admin.legend')</div>
<div class="panel-body">
<i class="fa fa-fw fa-check text-success" style="font-size: 1.3em;"></i> @lang('admin.default_storage_legend')<br/>
<i class="fa fa-fw fa-minus-circle text-danger" style="font-size: 1.3em;"></i> @lang('admin.inactive_storage_legend')
</div>
</div>
</div>
</div>
@endif @endif
</div> </div>
</div> </div>

View File

@ -194,19 +194,7 @@
<hr/> <hr/>
<div class="row"> <div class="row">
<div class="col-sm-6"> <div class="col-sm-6 col-sm-push-6">
<div class="panel panel-danger">
<div class="panel-heading">@lang('admin.danger_zone_heading')</div>
<div class="panel-body">
<p class="text-danger">@lang('admin.danger_zone_intro')</p>
<p>
<a href="{{ route('albums.delete', ['id' => $album->id]) }}" class="btn btn-danger">@lang('forms.delete_action')</a>
</p>
</div>
</div>
</div>
<div class="col-sm-6">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading">@lang('admin.save_changes_heading')</div> <div class="panel-heading">@lang('admin.save_changes_heading')</div>
<div class="panel-body"> <div class="panel-body">
@ -217,6 +205,18 @@
</div> </div>
</div> </div>
</div> </div>
<div class="col-sm-6 col-sm-pull-6">
<div class="panel panel-danger">
<div class="panel-heading">@lang('admin.danger_zone_heading')</div>
<div class="panel-body">
<p class="text-danger">@lang('admin.danger_zone_intro')</p>
<p>
<a href="{{ route('albums.delete', ['id' => $album->id]) }}" class="btn btn-danger">@lang('forms.delete_action')</a>
</p>
</div>
</div>
</div>
</div> </div>
{!! Form::close() !!} {!! Form::close() !!}
</div> </div>