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,
'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',

View File

@ -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();

View File

@ -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',

View File

@ -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();

View File

@ -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);
}
}

View File

@ -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)
{
}
}

View File

@ -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
]);
}

View File

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

View File

@ -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);
});
}
}

View File

@ -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.
*

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;
}
.content-body {
margin-bottom: 30px;
}
div.breadcrumb {
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_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',

View File

@ -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'
];

View File

@ -15,7 +15,7 @@
@section('content')
<div class="container">
<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'))
</div>

View File

@ -143,7 +143,7 @@
</div>
<fieldset style="margin-top: 30px;">
<legend>reCAPTCHA settings</legend>
<legend>@lang('admin.settings_recaptcha')</legend>
<div class="form-group">
{!! 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']) !!}
</div>
</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>

View File

@ -1,5 +1,5 @@
@extends('themes.base.layout')
@section('title', 'Welcome')
@section('title', $photo->name)
@section('breadcrumb')
<div class="breadcrumb">
@ -25,55 +25,64 @@
</div>
<div class="row">
<div class="col-xs-12 col-sm-8">
<a href="{{ $photo->thumbnailUrl() }}"><img src="{{ $photo->thumbnailUrl('fullsize') }}" alt="" class="img-thumbnail"/></a>
<div class="col-xs-12 col-sm-8 content-body">
@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 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">
<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>
@if (strlen($photo->taken_at) > 0)
<tr>
<td class="metadata_name">Date taken:</td>
<td class="metadata_value">{{ date(UserConfig::get('date_format'), strtotime($photo->taken_at)) }}</td>
</tr>
@endif
@if (strlen($photo->taken_at) > 0)
<tr>
<td class="metadata_name">Date taken:</td>
<td class="metadata_value">{{ date(UserConfig::get('date_format'), strtotime($photo->taken_at)) }}</td>
</tr>
@endif
@if (strlen($photo->camera_make) > 0)
<tr>
<td class="metadata_name">Camera make:</td>
<td class="metadata_value">{{ $photo->camera_make }}</td>
</tr>
@endif
@if (strlen($photo->camera_make) > 0)
<tr>
<td class="metadata_name">Camera make:</td>
<td class="metadata_value">{{ $photo->camera_make }}</td>
</tr>
@endif
@if (strlen($photo->camera_model) > 0)
<tr>
<td class="metadata_name">Camera model:</td>
<td class="metadata_value">{{ $photo->camera_model }}</td>
</tr>
@endif
@if (strlen($photo->camera_model) > 0)
<tr>
<td class="metadata_name">Camera model:</td>
<td class="metadata_value">{{ $photo->camera_model }}</td>
</tr>
@endif
@if (strlen($photo->camera_software) > 0)
<tr>
<td class="metadata_name">Camera software:</td>
<td class="metadata_value">{{ $photo->camera_software }}</td>
</tr>
@endif
</tbody>
</table>
@if (strlen($photo->camera_software) > 0)
<tr>
<td class="metadata_name">Camera software:</td>
<td class="metadata_value">{{ $photo->camera_software }}</td>
</tr>
@endif
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>