From 08f13b28cb1e1df408b13b44b5a4193e7643531b Mon Sep 17 00:00:00 2001 From: Andy Heathershaw Date: Sun, 11 Sep 2016 07:19:11 +0100 Subject: [PATCH] Added hotlink protection and restricting access to the original image to the photo's owner --- app/Helpers/ConfigHelper.php | 2 + app/Helpers/DbHelper.php | 5 + .../Controllers/Admin/DefaultController.php | 4 +- .../Controllers/Admin/PhotoController.php | 5 +- app/Http/Controllers/Controller.php | 14 +++ .../Controllers/Gallery/AlbumController.php | 12 +-- .../Controllers/Gallery/PhotoController.php | 40 +++++++- app/Photo.php | 1 + app/Providers/AuthServiceProvider.php | 14 ++- app/User.php | 9 ++ ...016_09_10_082025_add_photo_user_column.php | 37 ++++++++ public/themes/base/css/app.css | 7 ++ public/themes/bootstrap3/theme.css | 4 + resources/lang/en/admin.php | 2 + resources/lang/en/forms.php | 4 + .../views/themes/base/admin/index.blade.php | 2 +- .../themes/base/admin/settings.blade.php | 22 ++++- .../views/themes/base/gallery/photo.blade.php | 95 ++++++++++--------- 18 files changed, 216 insertions(+), 63 deletions(-) create mode 100644 database/migrations/2016_09_10_082025_add_photo_user_column.php diff --git a/app/Helpers/ConfigHelper.php b/app/Helpers/ConfigHelper.php index b036aa1..eb5521e 100644 --- a/app/Helpers/ConfigHelper.php +++ b/app/Helpers/ConfigHelper.php @@ -57,12 +57,14 @@ class ConfigHelper 'allow_self_registration' => true, 'app_name' => trans('global.app_name'), 'date_format' => $this->allowedDateFormats()[0], + 'hotlink_protection' => false, 'items_per_page' => 12, 'items_per_page_admin' => 10, 'recaptcha_enabled_registration' => false, 'recaptcha_secret_key' => '', 'recaptcha_site_key' => '', 'require_email_verification' => true, + 'restrict_original_download' => true, 'sender_address' => sprintf('hostmaster@%s', (isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : 'localhost')), 'sender_name' => (is_null($currentAppName) ? trans('global.app_name') : $currentAppName), 'smtp_server' => 'localhost', diff --git a/app/Helpers/DbHelper.php b/app/Helpers/DbHelper.php index 9ed5e71..f35b366 100644 --- a/app/Helpers/DbHelper.php +++ b/app/Helpers/DbHelper.php @@ -26,6 +26,11 @@ class DbHelper 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(); diff --git a/app/Http/Controllers/Admin/DefaultController.php b/app/Http/Controllers/Admin/DefaultController.php index 6449517..40d485c 100644 --- a/app/Http/Controllers/Admin/DefaultController.php +++ b/app/Http/Controllers/Admin/DefaultController.php @@ -49,9 +49,11 @@ class DefaultController extends Controller ]; $checkboxKeys = [ 'allow_self_registration', + 'hotlink_protection', + 'recaptcha_enabled_registration', 'require_email_verification', + 'restrict_original_download', 'smtp_encryption', - 'recaptcha_enabled_registration' ]; $updateKeys = [ 'app_name', diff --git a/app/Http/Controllers/Admin/PhotoController.php b/app/Http/Controllers/Admin/PhotoController.php index 770d1e1..27a8dd7 100644 --- a/app/Http/Controllers/Admin/PhotoController.php +++ b/app/Http/Controllers/Admin/PhotoController.php @@ -16,6 +16,7 @@ use Illuminate\Http\Request; use App\Http\Controllers\Controller; use Illuminate\Http\UploadedFile; use Illuminate\Support\Facades\App; +use Illuminate\Support\Facades\Auth; use Symfony\Component\Finder\Iterator\RecursiveDirectoryIterator; use Symfony\Component\HttpFoundation\File\File; @@ -173,6 +174,7 @@ class PhotoController extends Controller $photo = new Photo(); $photo->album_id = $album->id; + $photo->user_id = Auth::user()->id; $photo->name = pathinfo($photoFile->getClientOriginalName(), PATHINFO_FILENAME); $photo->file_name = $photoFile->getClientOriginalName(); $photo->storage_file_name = $savedFile->getFilename(); @@ -223,7 +225,7 @@ class PhotoController extends Controller { if ($fileInfo->isDir()) { - if ($fileInfo->getFilename() == '__MACOSX') + if ($fileInfo->getFilename() == '__MACOSX' || substr($fileInfo->getFilename(), 0, 1) == '.') { @rmdir($fileInfo->getPathname()); } @@ -246,6 +248,7 @@ class PhotoController extends Controller $photo = new Photo(); $photo->album_id = $album->id; + $photo->user_id = Auth::user()->id; $photo->name = pathinfo($photoFile->getFilename(), PATHINFO_FILENAME); $photo->file_name = $photoFile->getFilename(); $photo->storage_file_name = $savedFile->getFilename(); diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index 03e02a2..93f0bea 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -2,12 +2,26 @@ namespace App\Http\Controllers; +use App\User; use Illuminate\Foundation\Bus\DispatchesJobs; use Illuminate\Routing\Controller as BaseController; use Illuminate\Foundation\Validation\ValidatesRequests; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; +use Illuminate\Support\Facades\Auth; class Controller extends BaseController { use AuthorizesRequests, DispatchesJobs, ValidatesRequests; + + /** + * Gets either the authenticated user, or a user object representing the anonymous user. + * @return User + */ + protected function getUser() + { + $user = Auth::user(); + return (is_null($user) + ? User::anonymous() + : $user); + } } diff --git a/app/Http/Controllers/Gallery/AlbumController.php b/app/Http/Controllers/Gallery/AlbumController.php index efc057c..5069553 100644 --- a/app/Http/Controllers/Gallery/AlbumController.php +++ b/app/Http/Controllers/Gallery/AlbumController.php @@ -9,6 +9,7 @@ use App\Helpers\DbHelper; use App\Http\Controllers\Controller; use App\Http\Requests; use Illuminate\Http\Request; +use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\DB; class AlbumController extends Controller @@ -22,7 +23,7 @@ class AlbumController extends Controller return null; } - $this->authorize('album.view', $album); + $this->authorizeForUser($this->getUser(), 'album.view', $album); $photos = $album->photos() ->orderBy(DB::raw('COALESCE(taken_at, created_at)')) @@ -33,13 +34,4 @@ class AlbumController extends Controller 'photos' => $photos ]); } - - /** - * @param $id - * @return Album - */ - private static function loadAlbum($urlAlias) - { - - } } diff --git a/app/Http/Controllers/Gallery/PhotoController.php b/app/Http/Controllers/Gallery/PhotoController.php index 7a7a9de..e4dbb22 100644 --- a/app/Http/Controllers/Gallery/PhotoController.php +++ b/app/Http/Controllers/Gallery/PhotoController.php @@ -4,11 +4,14 @@ namespace App\Http\Controllers\Gallery; use App\Album; use App\Facade\Theme; +use App\Facade\UserConfig; use App\Helpers\DbHelper; use app\Http\Controllers\Admin\AlbumController; use App\Http\Controllers\Controller; +use App\Http\Middleware\VerifyCsrfToken; use App\Photo; use Illuminate\Support\Facades\App; +use Illuminate\Support\Facades\Gate; use Symfony\Component\HttpFoundation\Request; class PhotoController extends Controller @@ -21,14 +24,38 @@ class PhotoController extends Controller App::abort(404); return null; } - $this->authorize('album.view', $album); - $albumSource = $album->getAlbumSource(); + $this->authorizeForUser($this->getUser(), 'album.view', $album); + + if (UserConfig::get('hotlink_protection')) + { + $referrer = $request->headers->get('Referer'); + + if (!is_null($referrer)) + { + $hostname = parse_url($referrer, PHP_URL_HOST); + if (strtolower($hostname) != strtolower($request->getHttpHost())) + { + App::abort(403); + return null; + } + } + else + { + App::abort(403); + return null; + } + } - $thumbnail = $request->get('t'); $photo = PhotoController::loadPhotoByAlbumAndFilename($album, $photoFilename); - return response()->file($albumSource->getPathToPhoto($photo, $thumbnail)); + $thumbnail = $request->get('t'); + if (is_null($thumbnail)) + { + Gate::forUser($this->getUser())->authorize('photo.download_original', $photo); + } + + return response()->file($album->getAlbumSource()->getPathToPhoto($photo, $thumbnail)); } public function show($albumUrlAlias, $photoFilename) @@ -40,12 +67,15 @@ class PhotoController extends Controller return null; } - $this->authorize('album.view', $album); + $this->authorizeForUser($this->getUser(), 'album.view', $album); $photo = PhotoController::loadPhotoByAlbumAndFilename($album, $photoFilename); + $isOriginalAllowed = Gate::forUser($this->getUser())->allows('photo.download_original', $photo); + return Theme::render('gallery.photo', [ 'album' => $album, + 'is_original_allowed' => $isOriginalAllowed, 'photo' => $photo ]); } diff --git a/app/Photo.php b/app/Photo.php index ef876bf..aa36ad8 100644 --- a/app/Photo.php +++ b/app/Photo.php @@ -16,6 +16,7 @@ class Photo extends Model */ protected $fillable = [ 'album_id', + 'user_id', 'name', 'description', 'file_name', diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index 1767505..4ca04b1 100644 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -3,6 +3,8 @@ namespace App\Providers; use App\Album; +use App\Facade\UserConfig; +use App\Photo; use Illuminate\Support\Facades\Gate; use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider; @@ -30,8 +32,18 @@ class AuthServiceProvider extends ServiceProvider { return (!$album->is_private || $album->user_id == $user->id); }); - Gate::define('admin-access', function ($user) { + Gate::define('admin-access', function ($user) + { return $user->is_admin; }); + Gate::define('photo.download_original', function ($user, Photo $photo) + { + if (!UserConfig::get('restrict_original_download')) + { + return true; + } + + return ($user->id == $photo->user_id); + }); } } diff --git a/app/User.php b/app/User.php index e46b31c..6636862 100644 --- a/app/User.php +++ b/app/User.php @@ -9,6 +9,15 @@ class User extends Authenticatable { use Notifiable; + public static function anonymous() + { + $user = new User(); + $user->id = -1; + $user->name = 'Anonymous'; + + return $user; + } + /** * The attributes that are mass assignable. * diff --git a/database/migrations/2016_09_10_082025_add_photo_user_column.php b/database/migrations/2016_09_10_082025_add_photo_user_column.php new file mode 100644 index 0000000..ceaefe2 --- /dev/null +++ b/database/migrations/2016_09_10_082025_add_photo_user_column.php @@ -0,0 +1,37 @@ +unsignedInteger('user_id'); + + $table->foreign('user_id') + ->references('id')->on('users') + ->onDelete('no action'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('photos', function (Blueprint $table) { + $table->dropForeign('photos_user_id_foreign'); + $table->dropColumn('user_id'); + }); + } +} diff --git a/public/themes/base/css/app.css b/public/themes/base/css/app.css index e69de29..0660324 100644 --- a/public/themes/base/css/app.css +++ b/public/themes/base/css/app.css @@ -0,0 +1,7 @@ +.no-margin-bottom { + margin-bottom: 0; +} + +.no-padding { + padding: 0; +} \ No newline at end of file diff --git a/public/themes/bootstrap3/theme.css b/public/themes/bootstrap3/theme.css index 292c783..a0965d5 100644 --- a/public/themes/bootstrap3/theme.css +++ b/public/themes/bootstrap3/theme.css @@ -12,6 +12,10 @@ body { padding-bottom: 40px; } +.content-body { + margin-bottom: 30px; +} + div.breadcrumb { margin-top: -20px; } diff --git a/resources/lang/en/admin.php b/resources/lang/en/admin.php index 18efe05..2bb677a 100644 --- a/resources/lang/en/admin.php +++ b/resources/lang/en/admin.php @@ -25,7 +25,9 @@ return [ 'no_albums_text' => 'You have no photo albums yet. Click the button below to create one.', 'no_albums_title' => 'No Photo Albums', 'open_album' => 'Open album', + 'settings_image_protection' => 'Image Protection', 'settings_link' => 'Settings', + 'settings_recaptcha' => 'reCAPTCHA settings', 'settings_save_action' => 'Update Settings', 'settings_saved_message' => 'The settings were updated successfully.', 'settings_test_email_action' => 'Send a test e-mail', diff --git a/resources/lang/en/forms.php b/resources/lang/en/forms.php index a87f6d9..e072293 100644 --- a/resources/lang/en/forms.php +++ b/resources/lang/en/forms.php @@ -14,6 +14,10 @@ return [ 'realname_label' => 'Your name:', 'register_action' => 'Create account', 'remember_me_label' => 'Remember me', + 'settings_hotlink_protection' => 'Prevent hot-linking to images', + '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_help' => 'With this option enabled, only the photo\'s owner can download the original high-resolution images.', 'upload_action' => 'Upload', 'save_action' => 'Save Changes' ]; \ No newline at end of file diff --git a/resources/views/themes/base/admin/index.blade.php b/resources/views/themes/base/admin/index.blade.php index 30a4764..cfa4468 100644 --- a/resources/views/themes/base/admin/index.blade.php +++ b/resources/views/themes/base/admin/index.blade.php @@ -15,7 +15,7 @@ @section('content')
-
+
@include (Theme::viewName('partials.admin_sysinfo_widget'))
diff --git a/resources/views/themes/base/admin/settings.blade.php b/resources/views/themes/base/admin/settings.blade.php index 8c82892..d017d86 100644 --- a/resources/views/themes/base/admin/settings.blade.php +++ b/resources/views/themes/base/admin/settings.blade.php @@ -143,7 +143,7 @@
- reCAPTCHA settings + @lang('admin.settings_recaptcha')
{!! Form::label('recaptcha_site_key', 'Site key:', ['class' => 'control-label']) !!} @@ -155,6 +155,26 @@ {!! Form::text('recaptcha_secret_key', old('recaptcha_secret_key'), ['class' => 'form-control']) !!}
+ +
+ @lang('admin.settings_image_protection') + +
+ +
+ +
+ +
+
diff --git a/resources/views/themes/base/gallery/photo.blade.php b/resources/views/themes/base/gallery/photo.blade.php index 8160cd3..0a04cd3 100644 --- a/resources/views/themes/base/gallery/photo.blade.php +++ b/resources/views/themes/base/gallery/photo.blade.php @@ -1,5 +1,5 @@ @extends('themes.base.layout') -@section('title', 'Welcome') +@section('title', $photo->name) @section('breadcrumb')
-
- +
+ @if ($is_original_allowed) + + @endif + + @if ($is_original_allowed) + + @endif
-

Information about this photo:

+
+
Information about this photo:
+
+ + + + + + + + + + + + - - - - - - - - - - - - + @if (strlen($photo->taken_at) > 0) + + + + + @endif - @if (strlen($photo->taken_at) > 0) - - - - - @endif + @if (strlen($photo->camera_make) > 0) + + + + + @endif - @if (strlen($photo->camera_make) > 0) - - - - - @endif + @if (strlen($photo->camera_model) > 0) + + + + + @endif - @if (strlen($photo->camera_model) > 0) - - - - - @endif - - @if (strlen($photo->camera_software) > 0) - - - - - @endif - - + @if (strlen($photo->camera_software) > 0) + + Camera software: + {{ $photo->camera_software }} + + @endif + + +
+