From fde988e35905a64bdbe0652bf028b072ebc846fa Mon Sep 17 00:00:00 2001 From: Andy Heathershaw Date: Sat, 24 Sep 2016 08:17:51 +0100 Subject: [PATCH] Implemented a Javascript viewmodel for analysing an album, added checks for if uploads cannot be completed. Implemented handling if POST request is over the max size configured in php.ini --- app/Helpers/MiscHelper.php | 33 +++++ .../Controllers/Admin/AlbumController.php | 13 +- .../Controllers/Admin/PhotoController.php | 12 +- app/Http/Kernel.php | 2 + .../Middleware/CheckMaxPostSizeExceeded.php | 51 +++++++ app/Http/Middleware/VerifyCsrfToken.php | 3 +- public/themes/base/js/app.js | 101 ++++++++++++++ resources/lang/en/admin.php | 9 ++ resources/lang/en/global.php | 6 +- ...ress.blade.php => analyse_album.blade.php} | 68 +++++----- .../themes/base/admin/show_album.blade.php | 127 +++++++++++------- 11 files changed, 333 insertions(+), 92 deletions(-) create mode 100644 app/Http/Middleware/CheckMaxPostSizeExceeded.php rename resources/views/themes/base/admin/{album_analyse_progress.blade.php => analyse_album.blade.php} (64%) diff --git a/app/Helpers/MiscHelper.php b/app/Helpers/MiscHelper.php index 37a4032..aa04da7 100644 --- a/app/Helpers/MiscHelper.php +++ b/app/Helpers/MiscHelper.php @@ -4,6 +4,39 @@ namespace App\Helpers; class MiscHelper { + public static function convertToBytes($val) + { + if(empty($val))return 0; + + $val = trim($val); + + preg_match('#([0-9]+)[\s]*([a-z]+)#i', $val, $matches); + + $last = ''; + if(isset($matches[2])){ + $last = $matches[2]; + } + + if(isset($matches[1])){ + $val = (int) $matches[1]; + } + + switch (strtolower($last)) + { + case 'g': + case 'gb': + $val *= 1024; + case 'm': + case 'mb': + $val *= 1024; + case 'k': + case 'kb': + $val *= 1024; + } + + return (int) $val; + } + public static function gravatarUrl($emailAddress, $size = 48, $default = 'identicon') { $hash = md5(strtolower(trim($emailAddress))); diff --git a/app/Http/Controllers/Admin/AlbumController.php b/app/Http/Controllers/Admin/AlbumController.php index 2378992..1c82cc8 100644 --- a/app/Http/Controllers/Admin/AlbumController.php +++ b/app/Http/Controllers/Admin/AlbumController.php @@ -5,6 +5,7 @@ namespace app\Http\Controllers\Admin; use App\Album; use App\Facade\Theme; use App\Facade\UserConfig; +use App\Helpers\MiscHelper; use App\Http\Controllers\Controller; use App\Http\Requests; use App\Photo; @@ -32,7 +33,7 @@ class AlbumController extends Controller ->orderBy('created_at') ->get(); - return Theme::render('admin.album_analyse_progress', ['album' => $album, 'photos' => $photos]); + return Theme::render('admin.analyse_album', ['album' => $album, 'photos' => $photos]); } /** @@ -130,6 +131,12 @@ class AlbumController extends Controller ->orderBy(DB::raw('COALESCE(taken_at, created_at)')) ->paginate(UserConfig::get('items_per_page_admin')); + // See if we can upload (need the GD extension) + $isUploadEnabled = extension_loaded('gd'); + $fileUploadLimit = MiscHelper::convertToBytes(ini_get('upload_max_filesize')) / (1024*1024); + $postLimit = MiscHelper::convertToBytes(ini_get('post_max_size')) / (1024*1024); + $fileUploadOrPostLowerLimit = ($postLimit < $fileUploadLimit) ? $postLimit : $fileUploadLimit; + return Theme::render('admin.show_album', [ 'album' => $album, 'bulk_actions' => [ @@ -140,6 +147,10 @@ class AlbumController extends Controller 'delete' => trans('admin.photo_actions.delete') ], 'error' => $request->session()->get('error'), + 'file_upload_limit' => $fileUploadLimit, + 'is_upload_enabled' => $isUploadEnabled, + 'max_post_limit' => $postLimit, + 'max_post_limit_bulk' => $fileUploadOrPostLowerLimit, 'photos' => $photos ]); } diff --git a/app/Http/Controllers/Admin/PhotoController.php b/app/Http/Controllers/Admin/PhotoController.php index cfca75d..4820c3a 100644 --- a/app/Http/Controllers/Admin/PhotoController.php +++ b/app/Http/Controllers/Admin/PhotoController.php @@ -25,7 +25,7 @@ class PhotoController extends Controller { public function __construct() { - $this->middleware('auth'); + $this->middleware(['auth', 'max_post_size_exceeded']); } public function analyse($photoId) @@ -215,11 +215,17 @@ class PhotoController extends Controller { $this->authorize('admin-access'); - $archiveFile = UploadedFile::createFromBase($request->files->get('archive')); - // Load the linked album $album = $this->loadAlbum($request->get('album_id')); + $archiveFile = UploadedFile::createFromBase($request->files->get('archive')); + if ($archiveFile->getError() != UPLOAD_ERR_OK) + { + Log::error('Bulk image upload failed.', ['error' => $archiveFile->getError(), 'reason' => $archiveFile->getErrorMessage()]); + $request->session()->flash('error', $archiveFile->getErrorMessage()); + return redirect(route('albums.show', ['id' => $album->id])); + } + // Create a temporary folder to hold the extracted files $tempFolder = sprintf('%s/btw_upload_%s', env('TEMP_FOLDER', '/tmp'), MiscHelper::randomString()); mkdir($tempFolder); diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index bcabec4..5449361 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -2,6 +2,7 @@ namespace App\Http; +use App\Http\Middleware\CheckMaxPostSizeExceeded; use Illuminate\Foundation\Http\Kernel as HttpKernel; class Kernel extends HttpKernel @@ -51,6 +52,7 @@ class Kernel extends HttpKernel 'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class, 'can' => \Illuminate\Auth\Middleware\Authorize::class, 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, + 'max_post_size_exceeded' => CheckMaxPostSizeExceeded::class, 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, ]; } diff --git a/app/Http/Middleware/CheckMaxPostSizeExceeded.php b/app/Http/Middleware/CheckMaxPostSizeExceeded.php new file mode 100644 index 0000000..8241b6a --- /dev/null +++ b/app/Http/Middleware/CheckMaxPostSizeExceeded.php @@ -0,0 +1,51 @@ +method() == 'POST' && !$this->shouldExclude($request)) + { + // Check post limit and see if it may have been exceeded + $postLimit = MiscHelper::convertToBytes(ini_get('post_max_size')); + + if ( + (isset($_SERVER['CONTENT_LENGTH']) && $_SERVER['CONTENT_LENGTH'] > $postLimit) || + (empty($_POST) && empty($_REQUEST)) + ) + { + $request->session()->flash('error', trans('global.post_max_exceeded')); + return back(); + } + } + + return $next($request); + } + + protected function shouldExclude(Request $request) + { + foreach ($this->exclude as $exclude) + { + if ($exclude !== '/') { + $exclude = trim($exclude, '/'); + } + + if ($request->is($exclude)) { + return true; + } + } + + return false; + } +} \ No newline at end of file diff --git a/app/Http/Middleware/VerifyCsrfToken.php b/app/Http/Middleware/VerifyCsrfToken.php index a2c3541..b25b7c4 100644 --- a/app/Http/Middleware/VerifyCsrfToken.php +++ b/app/Http/Middleware/VerifyCsrfToken.php @@ -12,6 +12,7 @@ class VerifyCsrfToken extends BaseVerifier * @var array */ protected $except = [ - // + '/admin/photos', + '/admin/photos/store-bulk' ]; } diff --git a/public/themes/base/js/app.js b/public/themes/base/js/app.js index ee969e4..8a958ca 100644 --- a/public/themes/base/js/app.js +++ b/public/themes/base/js/app.js @@ -1,3 +1,103 @@ +function AnalyseAlbumViewModel() +{ + var self = this; + + self.imagesFailed = ko.observableArray(); + self.imagesToAnalyse = ko.observableArray(); + self.numberSuccessful = ko.observable(0); + self.numberFailed = ko.observable(0); + + self.numberTotal = ko.computed(function() { + return self.imagesToAnalyse().length; + }); + + self.failedPercentage = ko.computed(function() + { + return ((self.numberFailed() / self.numberTotal()) * 100).toFixed(2) + '%'; + }); + self.successfulPercentage = ko.computed(function() + { + return ((self.numberSuccessful() / self.numberTotal()) * 100).toFixed(2) + '%'; + }); + + self.isCompleted = ko.computed(function() + { + return self.numberTotal() > 0 && (self.numberSuccessful() + self.numberFailed() >= self.numberTotal()); + }); + + // When an image is added to the array, automatically issue it for analysis + self.imagesToAnalyse.subscribe(function(changes) + { + // changes[0].value is an instance of AnalyseImageViewModel + var item = changes[0].value; + $.ajax( + item.url(), + { + dataType: 'json', + error: function (xhr, textStatus, errorThrown) + { + self.numberFailed(self.numberFailed() + 1); + self.imagesFailed.push({ + 'name': item.name(), + 'reason': textStatus + }); + item.isSuccessful(false); + item.isPending(false); + }, + method: 'POST', + success: function (data) + { + if (data.is_successful) + { + self.numberSuccessful(self.numberSuccessful() + 1); + item.isSuccessful(true); + item.isPending(false); + } + else + { + self.numberFailed(self.numberFailed() + 1); + self.imagesFailed.push({ + 'name': item.name(), + 'reason': data.message + }); + item.isSuccessful(false); + item.isPending(false); + } + } + } + ); + }, null, 'arrayChange'); +} + +function AnalyseImageViewModel(image_info) +{ + var self = this; + + self.isPending = ko.observable(true); + self.isSuccessful = ko.observable(false); + self.name = ko.observable(image_info.name); + self.photoID = ko.observable(image_info.photo_id); + self.url = ko.observable(image_info.url); + + self.iconClass = ko.computed(function() { + var string = 'fa fa-fw '; + + if (!self.isPending()) + { + string += (self.isSuccessful() ? 'check text-success' : 'times text-danger') + } + + return string; + }); +} + +/** + * This file is used by admin/show_album.blade.php to handle photo uploads. + * @param album_id ID of the album the photos are being uploaded to + * @param language Array containing language strings + * @param urls Array containing URLs + * @constructor + */ function UploadPhotosViewModel(album_id, language, urls) { var self = this; @@ -6,6 +106,7 @@ function UploadPhotosViewModel(album_id, language, urls) self.imagesFailed = ko.observable(0); self.imagesUploaded = ko.observable(0); self.imagesTotal = ko.observable(0); + self.isBulkUploadInProgress = ko.observable(false); self.isUploadInProgress = ko.observable(false); self.statusMessages = ko.observableArray(); diff --git a/resources/lang/en/admin.php b/resources/lang/en/admin.php index 2afb5eb..497c6a5 100644 --- a/resources/lang/en/admin.php +++ b/resources/lang/en/admin.php @@ -10,6 +10,7 @@ return [ 'album_photos_tab' => 'Photos', 'album_settings_tab' => 'Settings', 'album_upload_tab' => 'Upload', + 'analyse_photos_failed' => 'The following items could not be analysed and were removed:', 'create_album' => 'Create a photo album', 'create_album_intro' => 'Photo albums contain individual photographs together in the same way as a physical photo album or memory book.', 'create_album_intro2' => 'Complete the form below to create a photo album.', @@ -57,10 +58,18 @@ return [ 'upload_max_limit' => 'Upload limit (maximum size):' ], 'title' => 'Gallery Admin', + 'upload_bulk_heading' => 'Upload a zip archive', + 'upload_bulk_text' => 'You can use the form below to upload a zip archive that contains your photographs. Any valid image files in the zip archive will be imported. Common hidden folders used by operating systems will be ignored.', + 'upload_bulk_text2' => 'Your web server is configured to allow files up to :max_upload_size to be uploaded.', + 'upload_disabled_heading' => 'Uploading is not supported on this system.', + 'upload_disabled_text' => 'This system does not have the GD extension installed, so images cannot be processed. Please ensure this extension is installed and refresh this page to try again.', 'upload_file_failed_continue' => 'Click the "Continue" button to analyse the file(s) that were uploaded.', 'upload_file_not_image_messages' => 'The file ":file_name" is not recognised as an image and won\'t be uploaded.', 'upload_file_number_failed' => 'file(s) failed to upload.', 'upload_file_status_failed' => ':file_name failed to upload', 'upload_file_status_progress' => ':current of :total files completed', 'upload_file_status_success' => ':file_name uploaded successfully', + 'upload_single_file_heading' => 'Upload photos individually', + 'upload_single_file_text' => 'You can use the form below to upload individual files. To upload multiple files at once, hold down CTRL in the file browser.', + 'upload_single_file_text2' => 'Your web server is configured to allow files up to :file_size. If you browser does not support HTML 5 (most modern browsers do), the combined size of all selected files must be less than :max_upload_size.' ]; \ No newline at end of file diff --git a/resources/lang/en/global.php b/resources/lang/en/global.php index 741d2a2..0f3d33d 100644 --- a/resources/lang/en/global.php +++ b/resources/lang/en/global.php @@ -1,4 +1,8 @@ 'Blue Twilight' + 'app_name' => 'Blue Twilight', + 'post_max_exceeded' => 'Your upload exceeded the maximum size the web server is configured to allow. Please check the value of the "post_max_size" parameter in php.ini.', + 'units' => [ + 'megabytes' => 'MB' + ] ]; \ No newline at end of file diff --git a/resources/views/themes/base/admin/album_analyse_progress.blade.php b/resources/views/themes/base/admin/analyse_album.blade.php similarity index 64% rename from resources/views/themes/base/admin/album_analyse_progress.blade.php rename to resources/views/themes/base/admin/analyse_album.blade.php index 92ebfec..d9b2069 100644 --- a/resources/views/themes/base/admin/album_analyse_progress.blade.php +++ b/resources/views/themes/base/admin/analyse_album.blade.php @@ -5,26 +5,37 @@
-
+
Analysing...

Your uploaded photos are now being analysed.

-
-
+ +
+
+ +
+
+ +
-
- @foreach ($photos as $photo) -

{{ $photo->name }} ...

- @endforeach +
+

...

-