Added hotlink protection and restricting access to the original image to the photo's owner

This commit is contained in:
Andy Heathershaw 2016-09-11 07:19:11 +01:00
parent 068ed2018a
commit 08f13b28cb
18 changed files with 216 additions and 63 deletions

View File

@ -57,12 +57,14 @@ class ConfigHelper
'allow_self_registration' => true, 'allow_self_registration' => true,
'app_name' => trans('global.app_name'), 'app_name' => trans('global.app_name'),
'date_format' => $this->allowedDateFormats()[0], 'date_format' => $this->allowedDateFormats()[0],
'hotlink_protection' => false,
'items_per_page' => 12, 'items_per_page' => 12,
'items_per_page_admin' => 10, 'items_per_page_admin' => 10,
'recaptcha_enabled_registration' => false, 'recaptcha_enabled_registration' => false,
'recaptcha_secret_key' => '', 'recaptcha_secret_key' => '',
'recaptcha_site_key' => '', 'recaptcha_site_key' => '',
'require_email_verification' => true, 'require_email_verification' => true,
'restrict_original_download' => true,
'sender_address' => sprintf('hostmaster@%s', (isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : 'localhost')), 'sender_address' => sprintf('hostmaster@%s', (isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : 'localhost')),
'sender_name' => (is_null($currentAppName) ? trans('global.app_name') : $currentAppName), 'sender_name' => (is_null($currentAppName) ? trans('global.app_name') : $currentAppName),
'smtp_server' => 'localhost', 'smtp_server' => 'localhost',

View File

@ -26,6 +26,11 @@ class DbHelper
return $albums; 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) public static function loadAlbumByUrlAlias($urlAlias)
{ {
return Album::where('url_alias', $urlAlias)->first(); return Album::where('url_alias', $urlAlias)->first();

View File

@ -49,9 +49,11 @@ class DefaultController extends Controller
]; ];
$checkboxKeys = [ $checkboxKeys = [
'allow_self_registration', 'allow_self_registration',
'hotlink_protection',
'recaptcha_enabled_registration',
'require_email_verification', 'require_email_verification',
'restrict_original_download',
'smtp_encryption', 'smtp_encryption',
'recaptcha_enabled_registration'
]; ];
$updateKeys = [ $updateKeys = [
'app_name', 'app_name',

View File

@ -16,6 +16,7 @@ use Illuminate\Http\Request;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use Illuminate\Http\UploadedFile; use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\Finder\Iterator\RecursiveDirectoryIterator; use Symfony\Component\Finder\Iterator\RecursiveDirectoryIterator;
use Symfony\Component\HttpFoundation\File\File; use Symfony\Component\HttpFoundation\File\File;
@ -173,6 +174,7 @@ class PhotoController extends Controller
$photo = new Photo(); $photo = new Photo();
$photo->album_id = $album->id; $photo->album_id = $album->id;
$photo->user_id = Auth::user()->id;
$photo->name = pathinfo($photoFile->getClientOriginalName(), PATHINFO_FILENAME); $photo->name = pathinfo($photoFile->getClientOriginalName(), PATHINFO_FILENAME);
$photo->file_name = $photoFile->getClientOriginalName(); $photo->file_name = $photoFile->getClientOriginalName();
$photo->storage_file_name = $savedFile->getFilename(); $photo->storage_file_name = $savedFile->getFilename();
@ -223,7 +225,7 @@ class PhotoController extends Controller
{ {
if ($fileInfo->isDir()) if ($fileInfo->isDir())
{ {
if ($fileInfo->getFilename() == '__MACOSX') if ($fileInfo->getFilename() == '__MACOSX' || substr($fileInfo->getFilename(), 0, 1) == '.')
{ {
@rmdir($fileInfo->getPathname()); @rmdir($fileInfo->getPathname());
} }
@ -246,6 +248,7 @@ class PhotoController extends Controller
$photo = new Photo(); $photo = new Photo();
$photo->album_id = $album->id; $photo->album_id = $album->id;
$photo->user_id = Auth::user()->id;
$photo->name = pathinfo($photoFile->getFilename(), PATHINFO_FILENAME); $photo->name = pathinfo($photoFile->getFilename(), PATHINFO_FILENAME);
$photo->file_name = $photoFile->getFilename(); $photo->file_name = $photoFile->getFilename();
$photo->storage_file_name = $savedFile->getFilename(); $photo->storage_file_name = $savedFile->getFilename();

View File

@ -2,12 +2,26 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\User;
use Illuminate\Foundation\Bus\DispatchesJobs; use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Routing\Controller as BaseController; use Illuminate\Routing\Controller as BaseController;
use Illuminate\Foundation\Validation\ValidatesRequests; use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Facades\Auth;
class Controller extends BaseController class Controller extends BaseController
{ {
use AuthorizesRequests, DispatchesJobs, ValidatesRequests; 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);
}
} }

View File

@ -9,6 +9,7 @@ use App\Helpers\DbHelper;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Requests; use App\Http\Requests;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
class AlbumController extends Controller class AlbumController extends Controller
@ -22,7 +23,7 @@ class AlbumController extends Controller
return null; return null;
} }
$this->authorize('album.view', $album); $this->authorizeForUser($this->getUser(), 'album.view', $album);
$photos = $album->photos() $photos = $album->photos()
->orderBy(DB::raw('COALESCE(taken_at, created_at)')) ->orderBy(DB::raw('COALESCE(taken_at, created_at)'))
@ -33,13 +34,4 @@ class AlbumController extends Controller
'photos' => $photos 'photos' => $photos
]); ]);
} }
/**
* @param $id
* @return Album
*/
private static function loadAlbum($urlAlias)
{
}
} }

View File

@ -4,11 +4,14 @@ namespace App\Http\Controllers\Gallery;
use App\Album; use App\Album;
use App\Facade\Theme; use App\Facade\Theme;
use App\Facade\UserConfig;
use App\Helpers\DbHelper; use App\Helpers\DbHelper;
use app\Http\Controllers\Admin\AlbumController; use app\Http\Controllers\Admin\AlbumController;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Middleware\VerifyCsrfToken;
use App\Photo; use App\Photo;
use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Gate;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
class PhotoController extends Controller class PhotoController extends Controller
@ -21,14 +24,38 @@ class PhotoController extends Controller
App::abort(404); App::abort(404);
return null; 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); $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) public function show($albumUrlAlias, $photoFilename)
@ -40,12 +67,15 @@ class PhotoController extends Controller
return null; return null;
} }
$this->authorize('album.view', $album); $this->authorizeForUser($this->getUser(), 'album.view', $album);
$photo = PhotoController::loadPhotoByAlbumAndFilename($album, $photoFilename); $photo = PhotoController::loadPhotoByAlbumAndFilename($album, $photoFilename);
$isOriginalAllowed = Gate::forUser($this->getUser())->allows('photo.download_original', $photo);
return Theme::render('gallery.photo', [ return Theme::render('gallery.photo', [
'album' => $album, 'album' => $album,
'is_original_allowed' => $isOriginalAllowed,
'photo' => $photo 'photo' => $photo
]); ]);
} }

View File

@ -16,6 +16,7 @@ class Photo extends Model
*/ */
protected $fillable = [ protected $fillable = [
'album_id', 'album_id',
'user_id',
'name', 'name',
'description', 'description',
'file_name', 'file_name',

View File

@ -3,6 +3,8 @@
namespace App\Providers; namespace App\Providers;
use App\Album; use App\Album;
use App\Facade\UserConfig;
use App\Photo;
use Illuminate\Support\Facades\Gate; use Illuminate\Support\Facades\Gate;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider; 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); 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; 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);
});
} }
} }

View File

@ -9,6 +9,15 @@ class User extends Authenticatable
{ {
use Notifiable; use Notifiable;
public static function anonymous()
{
$user = new User();
$user->id = -1;
$user->name = 'Anonymous';
return $user;
}
/** /**
* The attributes that are mass assignable. * The attributes that are mass assignable.
* *

View File

@ -0,0 +1,37 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddPhotoUserColumn extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('photos', function (Blueprint $table) {
$table->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');
});
}
}

View File

@ -0,0 +1,7 @@
.no-margin-bottom {
margin-bottom: 0;
}
.no-padding {
padding: 0;
}

View File

@ -12,6 +12,10 @@ body {
padding-bottom: 40px; padding-bottom: 40px;
} }
.content-body {
margin-bottom: 30px;
}
div.breadcrumb { div.breadcrumb {
margin-top: -20px; margin-top: -20px;
} }

View File

@ -25,7 +25,9 @@ return [
'no_albums_text' => 'You have no photo albums yet. Click the button below to create one.', 'no_albums_text' => 'You have no photo albums yet. Click the button below to create one.',
'no_albums_title' => 'No Photo Albums', 'no_albums_title' => 'No Photo Albums',
'open_album' => 'Open album', 'open_album' => 'Open album',
'settings_image_protection' => 'Image Protection',
'settings_link' => 'Settings', 'settings_link' => 'Settings',
'settings_recaptcha' => 'reCAPTCHA settings',
'settings_save_action' => 'Update Settings', 'settings_save_action' => 'Update Settings',
'settings_saved_message' => 'The settings were updated successfully.', 'settings_saved_message' => 'The settings were updated successfully.',
'settings_test_email_action' => 'Send a test e-mail', 'settings_test_email_action' => 'Send a test e-mail',

View File

@ -14,6 +14,10 @@ return [
'realname_label' => 'Your name:', 'realname_label' => 'Your name:',
'register_action' => 'Create account', 'register_action' => 'Create account',
'remember_me_label' => 'Remember me', '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', 'upload_action' => 'Upload',
'save_action' => 'Save Changes' 'save_action' => 'Save Changes'
]; ];

View File

@ -15,7 +15,7 @@
@section('content') @section('content')
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col-xs-12 col-sm-8"> <div class="col-xs-12 col-sm-8 content-body">
@include (Theme::viewName('partials.admin_sysinfo_widget')) @include (Theme::viewName('partials.admin_sysinfo_widget'))
</div> </div>

View File

@ -143,7 +143,7 @@
</div> </div>
<fieldset style="margin-top: 30px;"> <fieldset style="margin-top: 30px;">
<legend>reCAPTCHA settings</legend> <legend>@lang('admin.settings_recaptcha')</legend>
<div class="form-group"> <div class="form-group">
{!! Form::label('recaptcha_site_key', 'Site key:', ['class' => 'control-label']) !!} {!! 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']) !!} {!! Form::text('recaptcha_secret_key', old('recaptcha_secret_key'), ['class' => 'form-control']) !!}
</div> </div>
</fieldset> </fieldset>
<fieldset style="margin-top: 20px;">
<legend>@lang('admin.settings_image_protection')</legend>
<div class="checkbox">
<label>
<input type="checkbox" name="restrict_original_download" @if (UserConfig::get('restrict_original_download'))checked="checked"@endif>
<strong>@lang('forms.settings_restrict_originals_download')</strong><br/>
@lang('forms.settings_restrict_originals_download_help')
</label>
</div>
<div class="checkbox" style="margin-top: 20px;">
<label>
<input type="checkbox" name="hotlink_protection" @if (UserConfig::get('hotlink_protection'))checked="checked"@endif>
<strong>@lang('forms.settings_hotlink_protection')</strong><br/>
@lang('forms.settings_hotlink_protection_help')
</label>
</div>
</fieldset>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,5 +1,5 @@
@extends('themes.base.layout') @extends('themes.base.layout')
@section('title', 'Welcome') @section('title', $photo->name)
@section('breadcrumb') @section('breadcrumb')
<div class="breadcrumb"> <div class="breadcrumb">
@ -25,55 +25,64 @@
</div> </div>
<div class="row"> <div class="row">
<div class="col-xs-12 col-sm-8"> <div class="col-xs-12 col-sm-8 content-body">
<a href="{{ $photo->thumbnailUrl() }}"><img src="{{ $photo->thumbnailUrl('fullsize') }}" alt="" class="img-thumbnail"/></a> @if ($is_original_allowed)
<a href="{{ $photo->thumbnailUrl() }}">
@endif
<img src="{{ $photo->thumbnailUrl('fullsize') }}" alt="" class="img-thumbnail"/>
@if ($is_original_allowed)
</a>
@endif
</div> </div>
<div class="col-xs-12 col-sm-4"> <div class="col-xs-12 col-sm-4">
<p>Information about this photo:</p> <div class="panel panel-default">
<div class="panel-heading">Information about this photo:</div>
<div class="panel-body no-padding">
<table class="table table-striped photo-metadata no-margin-bottom">
<thead>
<tr>
<th>Name</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td class="metadata_name">File name:</td>
<td class="metadata_value">{{ $photo->file_name }}</td>
</tr>
<table class="table table-striped photo-metadata"> @if (strlen($photo->taken_at) > 0)
<thead> <tr>
<tr> <td class="metadata_name">Date taken:</td>
<th>Name</th> <td class="metadata_value">{{ date(UserConfig::get('date_format'), strtotime($photo->taken_at)) }}</td>
<th>Value</th> </tr>
</tr> @endif
</thead>
<tbody>
<tr>
<td class="metadata_name">File name:</td>
<td class="metadata_value">{{ $photo->file_name }}</td>
</tr>
@if (strlen($photo->taken_at) > 0) @if (strlen($photo->camera_make) > 0)
<tr> <tr>
<td class="metadata_name">Date taken:</td> <td class="metadata_name">Camera make:</td>
<td class="metadata_value">{{ date(UserConfig::get('date_format'), strtotime($photo->taken_at)) }}</td> <td class="metadata_value">{{ $photo->camera_make }}</td>
</tr> </tr>
@endif @endif
@if (strlen($photo->camera_make) > 0) @if (strlen($photo->camera_model) > 0)
<tr> <tr>
<td class="metadata_name">Camera make:</td> <td class="metadata_name">Camera model:</td>
<td class="metadata_value">{{ $photo->camera_make }}</td> <td class="metadata_value">{{ $photo->camera_model }}</td>
</tr> </tr>
@endif @endif
@if (strlen($photo->camera_model) > 0) @if (strlen($photo->camera_software) > 0)
<tr> <tr>
<td class="metadata_name">Camera model:</td> <td class="metadata_name">Camera software:</td>
<td class="metadata_value">{{ $photo->camera_model }}</td> <td class="metadata_value">{{ $photo->camera_software }}</td>
</tr> </tr>
@endif @endif
</tbody>
@if (strlen($photo->camera_software) > 0) </table>
<tr> </div>
<td class="metadata_name">Camera software:</td> </div>
<td class="metadata_value">{{ $photo->camera_software }}</td>
</tr>
@endif
</tbody>
</table>
</div> </div>
</div> </div>
</div> </div>