Compare commits

..

5 Commits
main ... v2.0

509 changed files with 72653 additions and 99652 deletions

View File

@ -1,7 +1,7 @@
APP_ENV=production
APP_ENV=local
APP_KEY=
APP_DEBUG=false
APP_LOG_LEVEL=warning
APP_DEBUG=true
APP_LOG_LEVEL=debug
APP_URL=http://localhost
DB_CONNECTION=mysql

3
.gitattributes vendored
View File

@ -1,2 +1,3 @@
* text=auto
resources/assets/* linguist-vendored
*.css linguist-vendored
*.scss linguist-vendored

View File

@ -1,157 +0,0 @@
/*
This Gruntfile downloads the necessary CSS and JS includes (Bootstrap, etc.) and creates a combined file ready for
deployment with the application.
Available tasks:
- build-debug: Builds the minified CSS and JS files
*/
module.exports = function(grunt)
{
var download_url = 'https://cdn.andysh.uk/';
const sass = require('node-sass');
grunt.loadNpmTasks('grunt-contrib-clean');
grunt.loadNpmTasks('grunt-contrib-cssmin');
grunt.loadNpmTasks('grunt-contrib-concat');
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-curl');
grunt.loadNpmTasks('grunt-dart-sass');
grunt.loadNpmTasks('grunt-exec');
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
clean: {
build_css: [
// Clean the build folder - downloaded files
'build/css',
// Clean the resources folder - compiled SASS files
'resources/css'
],
build_js: [
'build/js',
],
output: [
'public/css',
'public/js'
]
},
concat: {
// Concatenate all third-party stylesheets into blue-twilight.css
bt_css: {
src: [
'build/css/*.css',
'resources/css/*.css'
],
dest: 'public/css/blue-twilight.css'
},
// Concatenate all third-party and application Javascripts into blue-twilight.js
bt_js: {
src: [
'build/js/*.js',
'resources/js/*.js',
],
dest: 'public/js/blue-twilight.js'
},
},
curl: {
/* These elements are sorted according to the order we desire them to be loaded when concatenated */
/** CSS **/
bootstrap_css: {
src: download_url + 'bootstrap/4.4.1/css/bootstrap.css',
dest: 'build/css/01-bootstrap.css'
},
/** JS **/
jquery: {
src: download_url + 'jquery/3.4.1/jquery.js',
dest: 'build/js/01-jquery.js'
},
bootstrap_js: {
src: download_url + 'bootstrap/4.4.1/js/bootstrap.bundle.js',
dest: 'build/js/02-bootstrap.js'
},
bootbox_js: {
src: download_url + 'bootbox/5.3.3/bootbox.all.js',
dest: 'build/js/03-bootbox.js'
},
font_awesome_js: {
src: download_url + 'font-awesome/5.12.0/js/all.js',
dest: 'build/js/04-fontawesome.js'
},
vuejs: {
src: download_url + 'vuejs/2.6.11/vue.js',
dest: 'build/js/05-vuejs.js'
},
},
'dart-sass': {
bt_sass: {
options: {
sourceMap: false
},
files: [{
expand: true,
cwd: 'resources/sass/',
src: ['*.scss'],
dest: 'resources/css/',
ext: '.css'
}]
},
},
cssmin: {
bt_css: {
files: {
'public/css/blue-twilight.min.css': ['public/css/blue-twilight.css']
}
}
},
uglify: {
bt_js: {
options: {
sourceMap: true
},
files: {
'public/js/blue-twilight.min.js': ['public/js/blue-twilight.js']
}
}
}
});
// Register our tasks
grunt.registerTask('build-css-debug', [
// Download third-party stylesheets
'curl:bootstrap_css',
// SASS our own CSS
'dart-sass:bt_sass',
// Create our blue-twilight.css
'concat:bt_css'
]);
grunt.registerTask('build-js-debug', [
// Download third-party Javascripts
'curl:jquery',
'curl:bootstrap_js',
'curl:bootbox_js',
'curl:font_awesome_js',
'curl:vuejs',
// Create our blue-twilight.js
'concat:bt_js'
]);
grunt.registerTask('build-css-release', [
'build-css-debug',
'cssmin:bt_css'
]);
grunt.registerTask('build-js-release', [
'build-js-debug',
'uglify:bt_js'
]);
// Shortcut tasks for the ones above
grunt.registerTask('clean-all', ['clean:build_css', 'clean:build_js', 'clean:output']);
grunt.registerTask('build-debug', ['clean-all', 'build-css-debug', 'build-js-debug']);
grunt.registerTask('build-release', ['clean-all', 'build-css-release', 'build-js-release']);
};

View File

@ -2,7 +2,6 @@
namespace App;
use App\AlbumSources\AlbumSourceBase;
use App\AlbumSources\IAlbumSource;
use App\AlbumSources\LocalFilesystemSource;
use App\Helpers\MiscHelper;
@ -22,7 +21,7 @@ class Album extends Model
* @var array
*/
protected $fillable = [
'name', 'description', 'url_alias', 'user_id', 'storage_id', 'default_view', 'parent_album_id', 'url_path', 'is_permissions_inherited'
'name', 'description', 'url_alias', 'user_id', 'storage_id', 'default_view', 'parent_album_id', 'url_path'
];
/**
@ -104,35 +103,6 @@ class Album extends Model
}
}
/**
* Try and locate the parent album ID that permissions are inherited from.
* @return integer
*/
public function effectiveAlbumIDForPermissions()
{
$current = $this;
while (!is_null($current->parent_album_id))
{
if ($current->is_permissions_inherited)
{
$current = $current->parent;
}
else
{
break;
}
}
if (is_null($current->parent_album_id) && $current->is_permissions_inherited)
{
// Use default permissions list
return 0;
}
return $current->id;
}
public function generateAlias()
{
$this->url_alias = MiscHelper::capitaliseWord(preg_replace('/[^a-z0-9\-]/', '-', strtolower($this->name)));
@ -159,7 +129,10 @@ class Album extends Model
*/
public function getAlbumSource()
{
$source = AlbumSourceBase::make($this->storage->source);
$fullClassName = sprintf('App\AlbumSources\%s', $this->storage->source);
/** @var IAlbumSource $source */
$source = new $fullClassName;
$source->setAlbum($this);
$source->setConfiguration($this->storage);
@ -213,38 +186,21 @@ class Album extends Model
public function thumbnailUrl($thumbnailName)
{
/** @var Photo $photo */
$photo = $this->photos()
->inRandomOrder()
->first();
if (!is_null($photo))
{
return $photo->thumbnailUrl($thumbnailName);
}
// See if any child albums have an image
$images = [];
/** @var Album $childAlbum */
foreach ($this->children as $childAlbum)
{
if ($childAlbum->photos()->count() > 0)
{
$images[] = $childAlbum->thumbnailUrl($thumbnailName);
}
}
if (count($images) == 0)
{
// Rotate standard images
$images = [
asset('themes/base/images/empty-album-1.jpg'),
asset('themes/base/images/empty-album-2.jpg'),
asset('themes/base/images/empty-album-3.jpg')
];
return $this->getAlbumSource()->getUrlToPhoto($photo, $thumbnailName);
}
// Rotate standard images
$images = [
asset('themes/base/images/empty-album-1.jpg'),
asset('themes/base/images/empty-album-2.jpg'),
asset('themes/base/images/empty-album-3.jpg')
];
return $images[rand(0, count($images) - 1)];
}
@ -259,11 +215,6 @@ class Album extends Model
return route('viewAlbum', $this->url_path);
}
public function user()
{
return $this->belongsTo(User::class);
}
public function userPermissions()
{
return $this->belongsToMany(Permission::class, 'album_user_permissions');

View File

@ -1,9 +0,0 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class AlbumDefaultAnonymousPermission extends Model
{
}

View File

@ -1,9 +0,0 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class AlbumDefaultGroupPermission extends Model
{
}

View File

@ -1,9 +0,0 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class AlbumDefaultUserPermission extends Model
{
}

View File

@ -17,36 +17,6 @@ abstract class AlbumSourceBase
*/
protected $configuration;
/**
* @var mixed
*/
private static $albumSourceCache = [];
/**
* Makes an album source class for the given source name (relative class name.)
* @param string $sourceName Name of the source.
* @return IAlbumSource
*/
public static function make($sourceName)
{
$fullClassName = sprintf('App\AlbumSources\%s', $sourceName);
if (!array_key_exists($fullClassName, self::$albumSourceCache))
{
/** @var IAlbumSource $source */
$source = app($fullClassName);
self::$albumSourceCache[$fullClassName] = $source;
}
return self::$albumSourceCache[$fullClassName];
}
public function getConfiguration()
{
return $this->configuration;
}
public function setAlbum(Album $album)
{
$this->album = $album;

View File

@ -2,14 +2,12 @@
namespace App\AlbumSources;
use App\Helpers\MiscHelper;
use App\Photo;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\Psr7\Stream;
use Guzzle\Http\EntityBody;
use Guzzle\Http\Exception\ClientErrorResponseException;
use Illuminate\Support\Facades\Log;
use function GuzzleHttp\Psr7\stream_for;
class AmazonS3Source extends AlbumSourceBase implements IAlbumSource, IAnalysisQueueSource
class AmazonS3Source extends AlbumSourceBase implements IAlbumSource
{
/**
* Deletes an entire album's media contents.
@ -21,30 +19,6 @@ class AmazonS3Source extends AlbumSourceBase implements IAlbumSource, IAnalysisQ
// The delete routine will have already removed all photos
}
/**
* Deletes a photo to be analysed from the storage source
* @param string $queueToken Queue token holding the photo
* @param string $fileName Filename of the photo to download
* @return void
*/
public function deleteItemFromAnalysisQueue($queueToken, $fileName)
{
$fileToDelete = $this->getPathToAnalysisQueueItem($queueToken, $fileName);
try
{
$this->getClient()->deleteObject([
'Bucket' => $this->configuration->container_name,
'Key' => $fileToDelete
]);
}
catch (GuzzleException $ex)
{
// Don't worry if the file no longer exists
Log::warning('Failed deleting image from S3.', ['error' => $ex->getMessage(), 'path' => $fileToDelete]);
}
}
/**
* Deletes a thumbnail file for a photo.
* @param Photo $photo Photo to delete the thumbnail from.
@ -62,37 +36,18 @@ class AmazonS3Source extends AlbumSourceBase implements IAlbumSource, IAnalysisQ
'Key' => $this->getPathToPhoto($photo, $thumbnail)
]);
}
catch (GuzzleException $ex)
catch (ClientErrorResponseException $ex)
{
// Don't worry if the file no longer exists
Log::warning('Failed deleting image from S3.', ['error' => $ex->getMessage(), 'path' => $photoPath]);
}
}
/**
* Downloads a photo to be analysed from the storage source to a temporary file
* @param string $queueToken Queue token holding the photo
* @param string $fileName Filename of the photo to download
* @return string Path to the photo that was downloaded
*/
public function fetchItemFromAnalysisQueue($queueToken, $fileName)
{
$tempFile = tempnam(sys_get_temp_dir(), 'BlueTwilight_');
$this->getClient()->getObject([
'Bucket' => $this->configuration->container_name,
'Key' => $this->getPathToAnalysisQueueItem($queueToken, $fileName),
'SaveAs' => $tempFile
]);
return $tempFile;
}
/**
* Fetches the contents of a thumbnail for a photo.
* @param Photo $photo Photo to fetch the thumbnail for.
* @param string $thumbnail Thumbnail to fetch (or null to fetch the original.)
* @return Stream
* @return EntityBody
*/
public function fetchPhotoContent(Photo $photo, $thumbnail = null)
{
@ -106,7 +61,7 @@ class AmazonS3Source extends AlbumSourceBase implements IAlbumSource, IAnalysisQ
'SaveAs' => $tempFile
]);
return stream_for(fopen($tempFile, 'r+'));
return EntityBody::factory(fopen($tempFile, 'r+'));
}
finally
{
@ -131,19 +86,7 @@ class AmazonS3Source extends AlbumSourceBase implements IAlbumSource, IAnalysisQ
*/
public function getUrlToPhoto(Photo $photo, $thumbnail = null)
{
$client = $this->getClient();
if ($this->configuration->s3_signed_urls)
{
$cmd = $client->getCommand('GetObject', [
'Bucket' => $this->configuration->container_name,
'Key' => $this->getPathToPhoto($photo, $thumbnail)
]);
return (string)$client->createPresignedRequest($cmd, '+5 minutes')->getUri();
}
return $client->getObjectUrl($this->configuration->container_name, $this->getPathToPhoto($photo, $thumbnail));
return $this->getClient()->getObjectUrl($this->configuration->container_name, $this->getPathToPhoto($photo, $thumbnail));
}
/**
@ -157,33 +100,7 @@ class AmazonS3Source extends AlbumSourceBase implements IAlbumSource, IAnalysisQ
{
$photoPath = $this->getPathToPhoto($photo, $thumbnail);
$uploadAcl = $this->configuration->s3_signed_urls
? 'private'
: 'public-read';
$this->getClient()->upload($this->configuration->container_name, $photoPath, fopen($tempFilename, 'r+'), $uploadAcl);
}
/**
* Uploads a new file to the analysis queue specified by queue token.
*
* @param string $sourceFilePath Path to the file to upload to the analysis queue
* @param string $queueToken Queue token to hold the photo
* @param string $overrideFilename Use a specific filename, or false to set a specific name
* @return string Path to the file
*/
public function uploadToAnalysisQueue($sourceFilePath, $queueToken, $overrideFilename = null)
{
$targetPath = sprintf(
'%s/%s',
$this->getPathToAnalysisQueue($queueToken),
is_null($overrideFilename) ? MiscHelper::randomString(20) : $overrideFilename
);
// Note: we don't use "public-read" here as it will not be publicly-accessible, and will be retrieved by an authenticated client
$this->getClient()->upload($this->configuration->container_name, $targetPath, fopen($sourceFilePath, 'r+'));
return $targetPath;
$this->getClient()->upload($this->configuration->container_name, $photoPath, fopen($tempFilename, 'r+'), 'public-read');
}
private function getClient()
@ -210,16 +127,6 @@ class AmazonS3Source extends AlbumSourceBase implements IAlbumSource, IAnalysisQ
return '_originals';
}
private function getPathToAnalysisQueue($queueToken)
{
return sprintf('analysis-queue/%s', $queueToken);
}
private function getPathToAnalysisQueueItem($queueToken, $fileName)
{
return sprintf('%s/%s', $this->getPathToAnalysisQueue($queueToken), $fileName);
}
private function getPathToPhoto(Photo $photo, $thumbnail = null)
{
return sprintf(

View File

@ -1,247 +0,0 @@
<?php
namespace App\AlbumSources;
use App\BackblazeB2FileIdCache;
use App\Photo;
use App\Services\BackblazeB2Service;
use App\Storage;
use GuzzleHttp\Psr7\Stream;
use Illuminate\Support\Facades\Log;
use function GuzzleHttp\Psr7\stream_for;
class BackblazeB2Source extends AlbumSourceBase implements IAlbumSource
{
const BUCKET_TYPE_AUTO = 0;
const BUCKET_TYPE_PRIVATE = 1;
const BUCKET_TYPE_PUBLIC = 2;
/**
* @var BackblazeB2Service
*/
private $backblaze;
/**
* Type of bucket which determines what type of URLs to generate to images.
* @var integer
*/
private $bucketType;
/**
* Token used to download files from a private bucket.
* @var string
*/
private $downloadToken;
/**
* Deletes an entire album's media contents.
* @return void
*/
public function deleteAlbumContents()
{
// No need to do anything for the album container - once the files are gone, the virtual folder is also gone
}
/**
* Deletes a thumbnail file for a photo.
* @param Photo $photo Photo to delete the thumbnail from.
* @param string $thumbnail Thumbnail to delete (or null to delete the original.)
* @return void
*/
public function deleteThumbnail(Photo $photo, $thumbnail = null)
{
$pathOnStorage = $this->getPathToPhoto($photo, $thumbnail);
// Create or update our cache record
$b2Cache = $this->getB2FileFromCache($pathOnStorage);
if (is_null($b2Cache))
{
return;
}
$this->getClient()->deleteFile($b2Cache->b2_file_id, $pathOnStorage);
$b2Cache->delete();
}
/**
* Fetches the contents of a thumbnail for a photo.
* @param Photo $photo Photo to fetch the thumbnail for.
* @param string $thumbnail Thumbnail to fetch (or null to fetch the original.)
* @return Stream
*/
public function fetchPhotoContent(Photo $photo, $thumbnail = null)
{
$pathOnStorage = $this->getPathToPhoto($photo, $thumbnail);
// First we need the file ID
$b2Cache = $this->getB2FileFromCache($pathOnStorage);
if (is_null($b2Cache))
{
return stream_for('');
}
return stream_for($this->getClient()->downloadFile($b2Cache->b2_file_id));
}
/**
* Gets the name of this album source.
* @return string
*/
public function getName()
{
return 'global.album_sources.backblaze_b2';
}
/**
* Gets the absolute URL to the given photo file.
* @param Photo $photo Photo to get the URL to.
* @param string $thumbnail Thumbnail to get the image to.
* @return string
*/
public function getUrlToPhoto(Photo $photo, $thumbnail = null)
{
$client = $this->getClient();
$pathOnStorage = $this->getPathToPhoto($photo, $thumbnail);
switch ($this->bucketType)
{
case self::BUCKET_TYPE_PRIVATE:
if (is_null($this->downloadToken))
{
$this->downloadToken = $client->getDownloadAuthToken();
}
// Once I sort out the issue with b2_download_file_by_id, this line can be removed
return sprintf('%s/file/%s/%s?Authorization=%s', $client->getDownloadUrl(), $this->configuration->container_name, $pathOnStorage, $this->downloadToken);
$b2Cache = $this->getB2FileFromCache($pathOnStorage);
if (is_null($b2Cache))
{
return '';
}
return sprintf('%s/b2api/v2/b2_download_file_by_id?fileId=%s&Authorization=%s', $client->getDownloadUrl(), urlencode($b2Cache->b2_file_id), urlencode($this->downloadToken));
case self::BUCKET_TYPE_PUBLIC:
/*
* From https://www.backblaze.com/b2/docs/b2_download_file_by_name.html:
* The base URL to use comes from the b2_authorize_account call, and looks something like
* https://f345.backblazeb2.com. The "f" in the URL stands for "file", and the number is the cluster
* number containing your account. To this base, you add "file/", your bucket name, a "/", and then the
* name of the file. The file name may itself include more "/" characters.
*/
return sprintf('%s/file/%s/%s', $client->getDownloadUrl(), $this->configuration->container_name, $pathOnStorage);
}
}
/**
* Saves a generated thumbnail to its permanent location.
* @param Photo $photo Photo the image relates to.
* @param string $tempFilename Filename containing the image.
* @param string $thumbnail Name of the thumbnail (or null for the original.)
* @return mixed
*/
public function saveThumbnail(Photo $photo, $tempFilename, $thumbnail = null)
{
$pathOnStorage = $this->getPathToPhoto($photo, $thumbnail);
$b2Cache = $this->getB2FileFromCache($pathOnStorage);
if (!is_null($b2Cache))
{
// Delete the current file version if we're replacing a file that already exists
$this->getClient()->deleteFile($b2Cache->b2_file_id, $pathOnStorage);
}
// Upload the file to B2
$b2FileID = $this->getClient()->uploadFile($tempFilename, $pathOnStorage);
// Create or update our cache record
if (is_null($b2Cache))
{
$b2Cache = new BackblazeB2FileIdCache([
'photo_id' => $photo->id,
'storage_path' => $pathOnStorage,
'b2_file_id' => $b2FileID
]);
}
else
{
$b2Cache->b2_file_id = $b2FileID;
}
$b2Cache->save();
}
public function setConfiguration(Storage $configuration)
{
parent::setConfiguration($configuration);
}
/**
* @param $pathOnStorage
* @return BackblazeB2FileIdCache|null
*/
private function getB2FileFromCache($pathOnStorage)
{
$b2Cache = BackblazeB2FileIdCache::where('storage_path', $pathOnStorage)->first();
if (is_null($b2Cache))
{
// TODO: lookup the file on B2 to get the file ID
Log::warning(sprintf('B2 file ID not found in cache: %s', $pathOnStorage));
return null;
}
return $b2Cache;
}
private function getClient()
{
if (is_null($this->backblaze))
{
$this->backblaze = new BackblazeB2Service();
$this->backblaze->setCredentials(decrypt($this->configuration->access_key), decrypt($this->configuration->secret_key));
$this->backblaze->authorizeAccount();
$this->backblaze->setBucketName($this->configuration->container_name);
if (intval($this->configuration->b2_bucket_type) == self::BUCKET_TYPE_AUTO)
{
/* Auto-detect the type of bucket in use on B2 */
switch ($this->backblaze->getBucketType())
{
case 'allPrivate':
$this->configuration->b2_bucket_type = self::BUCKET_TYPE_PRIVATE;
break;
case 'allPublic':
$this->configuration->b2_bucket_type = self::BUCKET_TYPE_PUBLIC;
break;
}
$this->configuration->save();
}
// Set the bucket type
$this->bucketType = $this->configuration->b2_bucket_type;
}
return $this->backblaze;
}
private function getOriginalsFolder()
{
return '_originals';
}
private function getPathToPhoto(Photo $photo, $thumbnail = null)
{
return sprintf(
'%s/%s/%s',
$this->album->url_alias,
is_null($thumbnail) ? $this->getOriginalsFolder() : $thumbnail,
$photo->storage_file_name
);
}
}

View File

@ -1,137 +0,0 @@
<?php
namespace App\AlbumSources;
use App\Photo;
use App\Services\DropboxService;
use GuzzleHttp\Psr7\Stream;
use function GuzzleHttp\Psr7\stream_for;
class DropboxSource extends AlbumSourceBase implements IAlbumSource
{
/**
* @var DropboxService
*/
private $dropboxClient;
/**
* Deletes an entire album's media contents.
* @return void
*/
public function deleteAlbumContents()
{
try
{
$albumPathOnStorage = sprintf('/%s', $this->album->url_alias);
$this->getClient()->deleteFile($albumPathOnStorage);
}
catch (\Exception $ex)
{
// Don't worry too much if the delete fails - the file may have been removed on Dropbox itself
}
}
/**
* Deletes a thumbnail file for a photo.
* @param Photo $photo Photo to delete the thumbnail from.
* @param string $thumbnail Thumbnail to delete (or null to delete the original.)
* @return void
*/
public function deleteThumbnail(Photo $photo, $thumbnail = null)
{
try
{
$pathOnStorage = $this->getPathToPhoto($photo, $thumbnail);
$this->getClient()->deleteFile($pathOnStorage);
}
catch (\Exception $ex)
{
// Don't worry too much if the delete fails - the file may have been removed on Dropbox itself
}
}
/**
* Fetches the contents of a thumbnail for a photo.
* @param Photo $photo Photo to fetch the thumbnail for.
* @param string $thumbnail Thumbnail to fetch (or null to fetch the original.)
* @return Stream
*/
public function fetchPhotoContent(Photo $photo, $thumbnail = null)
{
$pathOnStorage = $this->getPathToPhoto($photo, $thumbnail);
return stream_for($this->getClient()->downloadFile($pathOnStorage));
}
/**
* Gets the name of this album source.
* @return string
*/
public function getName()
{
return 'global.album_sources.dropbox';
}
/**
* Gets the absolute URL to the given photo file.
* @param Photo $photo Photo to get the URL to.
* @param string $thumbnail Thumbnail to get the image to.
* @return string
*/
public function getUrlToPhoto(Photo $photo, $thumbnail = null)
{
$photoUrl = route('downloadPhoto', [
'albumUrlAlias' => $this->album->url_path,
'photoFilename' => $photo->storage_file_name
]);
if (!is_null($thumbnail))
{
$photoUrl .= sprintf('?t=%s', urlencode($thumbnail));
}
return $photoUrl;
}
/**
* Saves a generated thumbnail to its permanent location.
* @param Photo $photo Photo the image relates to.
* @param string $tempFilename Filename containing the image.
* @param string $thumbnail Name of the thumbnail (or null for the original.)
* @return mixed
*/
public function saveThumbnail(Photo $photo, $tempFilename, $thumbnail = null)
{
$pathOnStorage = $this->getPathToPhoto($photo, $thumbnail);
$this->getClient()->uploadFile($tempFilename, $pathOnStorage);
}
private function getClient()
{
if (is_null($this->dropboxClient))
{
$this->dropboxClient = new DropboxService();
$this->dropboxClient->setAccessToken(decrypt($this->configuration->access_token));
}
return $this->dropboxClient;
}
private function getOriginalsFolder()
{
return '_originals';
}
private function getPathToPhoto(Photo $photo, $thumbnail = null)
{
return sprintf(
'/%s/%s/%s',
$this->album->url_alias,
is_null($thumbnail) ? $this->getOriginalsFolder() : $thumbnail,
$photo->storage_file_name
);
}
}

View File

@ -5,7 +5,8 @@ namespace App\AlbumSources;
use App\Album;
use App\Photo;
use App\Storage;
use GuzzleHttp\Psr7\Stream;
use Guzzle\Http\EntityBody;
use Symfony\Component\HttpFoundation\File\File;
interface IAlbumSource
{
@ -27,16 +28,10 @@ interface IAlbumSource
* Fetches the contents of a thumbnail for a photo.
* @param Photo $photo Photo to fetch the thumbnail for.
* @param string $thumbnail Thumbnail to fetch (or null to fetch the original.)
* @return Stream
* @return EntityBody
*/
function fetchPhotoContent(Photo $photo, $thumbnail = null);
/**
* Gets the configuration of the source.
* @return mixed
*/
function getConfiguration();
/**
* Gets the name of this album source.
* @return string

View File

@ -1,40 +0,0 @@
<?php
namespace App\AlbumSources;
use App\Storage;
interface IAnalysisQueueSource
{
/**
* Deletes a photo to be analysed from the storage source
* @param string $queueToken Queue token holding the photo
* @param string $fileName Filename of the photo to download
* @return void
*/
function deleteItemFromAnalysisQueue($queueToken, $fileName);
/**
* Downloads a photo to be analysed from the storage source to a temporary file
* @param string $queueToken Queue token holding the photo
* @param string $fileName Filename of the photo to download
* @return string Path to the photo that was downloaded
*/
function fetchItemFromAnalysisQueue($queueToken, $fileName);
/**
* @param Storage $configuration
* @return mixed
*/
function setConfiguration(Storage $configuration);
/**
* Uploads a new file to the analysis queue specified by queue token.
*
* @param string $sourceFilePath Path to the file to upload to the analysis queue
* @param string $queueToken Queue token to hold the photo
* @param string $overrideFilename Use a specific filename, or false to set a specific name
* @return string Path to the file
*/
function uploadToAnalysisQueue($sourceFilePath, $queueToken, $overrideFilename = null);
}

View File

@ -2,18 +2,18 @@
namespace App\AlbumSources;
use App\Helpers\FileHelper;
use App\Album;
use App\Helpers\MiscHelper;
use App\Photo;
use GuzzleHttp\Psr7\Stream;
use App\Services\PhotoService;
use Guzzle\Http\EntityBody;
use Symfony\Component\HttpFoundation\File\File;
use function GuzzleHttp\Psr7\stream_for;
/**
* Driver for managing files on the local filesystem.
* @package App\AlbumSources
*/
class LocalFilesystemSource extends AlbumSourceBase implements IAlbumSource, IAnalysisQueueSource
class LocalFilesystemSource extends AlbumSourceBase implements IAlbumSource
{
public function deleteAlbumContents()
{
@ -38,7 +38,7 @@ class LocalFilesystemSource extends AlbumSourceBase implements IAlbumSource, IAn
* Fetches the contents of a thumbnail for a photo.
* @param Photo $photo Photo to fetch the thumbnail for.
* @param string $thumbnail Thumbnail to fetch (or null to fetch the original.)
* @return Stream
* @return EntityBody
*/
public function fetchPhotoContent(Photo $photo, $thumbnail = null)
{
@ -51,7 +51,7 @@ class LocalFilesystemSource extends AlbumSourceBase implements IAlbumSource, IAn
'r+'
);
return stream_for($fh);
return EntityBody::factory($fh);
}
public function getName()
@ -120,86 +120,4 @@ class LocalFilesystemSource extends AlbumSourceBase implements IAlbumSource, IAn
rmdir($directory);
}
/**
* Deletes a photo to be analysed from the storage source
* @param string $queueToken Queue token holding the photo
* @param string $fileName Filename of the photo to download
* @return void
*/
public function deleteItemFromAnalysisQueue($queueToken, $fileName)
{
$queueFolder = $this->getQueuePath($queueToken);
$filePath = $this->getQueueItemPath($queueToken, $fileName);
@unlink($filePath);
// Delete the parent folder if empty
FileHelper::deleteIfEmpty($queueFolder);
}
/**
* Downloads a photo to be analysed from the storage source to a temporary file
* @param string $queueToken Queue token holding the photo
* @param string $fileName Filename of the photo to download
* @return string Path to the photo that was downloaded
*/
public function fetchItemFromAnalysisQueue($queueToken, $fileName)
{
// Don't actually need to download anything as it's already local
return $this->getQueueItemPath($queueToken, $fileName);
}
/**
* Uploads a new file to the analysis queue specified by queue token.
*
* @param string $sourceFilePath Path to the file to upload to the analysis queue
* @param string $queueToken Queue token to hold the photo
* @param string $overrideFilename Use a specific filename, or false to set a specific name
* @return string Path to the file
*/
public function uploadToAnalysisQueue($sourceFilePath, $queueToken, $overrideFilename = null)
{
$uploadedFile = new File($sourceFilePath);
$tempFilename = $this->getQueueItemPath(
$queueToken,
is_null($overrideFilename) ? MiscHelper::randomString(20) : basename($overrideFilename)
);
// Only add an extension if an override filename was not given, assume this is present
if (is_null($overrideFilename))
{
$extension = $uploadedFile->guessExtension();
if (!is_null($extension))
{
$tempFilename .= '.' . $extension;
}
}
copy($uploadedFile->getRealPath(), $tempFilename);
return $tempFilename;
}
private function getQueueItemPath($queueToken, $fileName)
{
return join(DIRECTORY_SEPARATOR, [$this->getQueuePath($queueToken), $fileName]);
}
private function getQueuePath($queueUid)
{
$path = join(DIRECTORY_SEPARATOR, [
dirname(dirname(__DIR__)),
'storage',
'app',
'analysis-queue',
str_replace(['.', '/', '\\'], '', $queueUid)
]);
if (!file_exists($path))
{
@mkdir($path, 0755, true);
}
return $path;
}
}

View File

@ -3,16 +3,12 @@
namespace App\AlbumSources;
use App\Photo;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Stream;
use Guzzle\Http\EntityBody;
use Guzzle\Http\Exception\ClientErrorResponseException;
use Illuminate\Support\Facades\Log;
use OpenStack\Common\Error\BadResponseError;
use OpenStack\Common\Transport\Utils as TransportUtils;
use OpenStack\Identity\v2\Service as IdentityV2Service;
use OpenStack\OpenStack;
use function GuzzleHttp\Psr7\stream_for;
use OpenCloud\ObjectStore\Exception\ObjectNotFoundException;
use OpenCloud\OpenStack;
use Symfony\Component\HttpFoundation\File\File;
/**
* Driver for managing files on an OpenStack Keystone+Swift compatible platform.
@ -42,14 +38,9 @@ class OpenStackSource extends AlbumSourceBase implements IAlbumSource
try
{
$this->getContainer()->getObject($photoPath)->delete();
$this->getContainer()->deleteObject($photoPath);
}
catch (GuzzleException $ex)
{
// Don't worry if the file no longer exists
Log::warning('Failed deleting image from OpenStack.', ['error' => $ex->getMessage(), 'path' => $photoPath]);
}
catch (BadResponseError $ex)
catch (ClientErrorResponseException $ex)
{
// Don't worry if the file no longer exists
Log::warning('Failed deleting image from OpenStack.', ['error' => $ex->getMessage(), 'path' => $photoPath]);
@ -60,13 +51,13 @@ class OpenStackSource extends AlbumSourceBase implements IAlbumSource
* Fetches the contents of a thumbnail for a photo.
* @param Photo $photo Photo to fetch the thumbnail for.
* @param string $thumbnail Thumbnail to fetch (or null to fetch the original.)
* @return Stream
* @return EntityBody
*/
public function fetchPhotoContent(Photo $photo, $thumbnail = null)
{
$object = $this->getContainer()->getObject($this->getPathToPhoto($photo, $thumbnail));
return stream_for($object->download());
return $object->getContent();
}
/**
@ -122,33 +113,17 @@ class OpenStackSource extends AlbumSourceBase implements IAlbumSource
{
$photoPath = $this->getPathToPhoto($photo, $thumbnail);
$createOptions = [
'name' => $photoPath,
'content' => file_get_contents($tempFilename)
];
$this->getContainer()->createObject($createOptions);
$container = $this->getContainer();
$container->uploadObject($photoPath, fopen($tempFilename, 'r+'));
}
protected function getClient()
{
$authURL = $this->configuration->auth_url;
$options = [
'authUrl' => $authURL,
return new OpenStack($this->configuration->auth_url, [
'username' => $this->configuration->username,
'password' => decrypt($this->configuration->password),
'tenantName' => $this->configuration->tenant_name,
'region' => $this->configuration->service_region,
'identityService' => IdentityV2Service::factory(
new Client([
'base_uri' => TransportUtils::normalizeUrl($authURL),
'handler' => HandlerStack::create(),
])
)
];
return new OpenStack($options);
]);
}
protected function getContainer()
@ -158,14 +133,11 @@ class OpenStackSource extends AlbumSourceBase implements IAlbumSource
protected function getStorageService()
{
return $this->getClient()->objectStoreV1([
'catalogName' => $this->getStorageServiceCatalogName()
]);
}
protected function getStorageServiceCatalogName()
{
return $this->configuration->service_name;
return $this->getClient()->objectStoreService(
$this->configuration->service_name,
$this->configuration->service_region,
'publicURL'
);
}
protected function getOriginalsFolder()

View File

@ -3,15 +3,26 @@
namespace App\AlbumSources;
use App\Photo;
use App\Services\Rackspace\Identity\v2\Service as RackspaceIdentityV2Service;
use App\Services\Rackspace\ObjectStoreCdn\v1\Models\Container;
use App\Services\Rackspace\Rackspace;
use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;
use OpenStack\Common\Transport\Utils as TransportUtils;
use App\Storage;
use OpenCloud\Rackspace;
class RackspaceSource extends OpenStackSource
{
protected function getClient()
{
$endpoint = Rackspace::US_IDENTITY_ENDPOINT;
if ($this->configuration->service_region == 'LON')
{
$endpoint = Rackspace::UK_IDENTITY_ENDPOINT;
}
return new Rackspace($endpoint, [
'username' => $this->configuration->username,
'apiKey' => decrypt($this->configuration->password)
]);
}
/**
* Gets the name of this album source.
* @return string
@ -30,14 +41,12 @@ class RackspaceSource extends OpenStackSource
public function getUrlToPhoto(Photo $photo, $thumbnail = null)
{
$isCdnEnabled = false;
$cdnService = $this->getCdnService();
$cdnService = $this->getStorageService()->getCdnService();
$thisCdnContainer = null;
/** @var Container $cdnContainer */
foreach ($cdnService->listContainers() as $cdnContainer)
{
if ($cdnContainer->cdn_enabled && strtolower($cdnContainer->name) == strtolower($this->configuration->container_name))
if ($cdnContainer->name == $this->configuration->container_name)
{
$isCdnEnabled = true;
$thisCdnContainer = $cdnContainer;
@ -46,47 +55,9 @@ class RackspaceSource extends OpenStackSource
if ($isCdnEnabled)
{
return sprintf('%s/%s', $thisCdnContainer->cdn_ssl_uri, $this->getPathToPhoto($photo, $thumbnail));
return sprintf('%s/%s', $thisCdnContainer->getCdnSslUri(), $this->getPathToPhoto($photo, $thumbnail));
}
return parent::getPathToPhoto($photo, $thumbnail);
}
protected function getCdnService()
{
return $this->getClient()->objectStoreCdnV1();
}
protected function getClient()
{
$authURL = config('services.rackspace.authentication_url');
// Uncomment the commented out lines below and in the $options array to get a 'storage/logs/openstack.log' file
// with passed HTTP traffic
//$logger = new Logger('MyLog');
//$logger->pushHandler(new StreamHandler(__DIR__ . '/../../storage/logs/openstack.log'), Logger::DEBUG);
$options = [
'authUrl' => $authURL,
'username' => $this->configuration->username,
'apiKey' => decrypt($this->configuration->password),
'region' => $this->configuration->service_region,
'identityService' => RackspaceIdentityV2Service::factory(
new Client([
'base_uri' => TransportUtils::normalizeUrl($authURL),
'handler' => HandlerStack::create(),
])
),
//'debugLog' => true,
//'logger' => $logger,
//'messageFormatter' => new MessageFormatter('{req_body} - {res_body}')
];
return new Rackspace($options);
}
protected function getStorageServiceCatalogName()
{
return 'cloudFiles';
}
}

View File

@ -1,19 +0,0 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class BackblazeB2FileIdCache extends Model
{
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'photo_id',
'storage_path',
'b2_file_id'
];
}

View File

@ -1,274 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Album;
use App\Facade\UserConfig;
use App\Photo;
use App\QueueItem;
use App\Services\PhotoService;
use App\Services\RabbitMQService;
use App\User;
use App\UserActivity;
use Illuminate\Console\Command;
class ProcessQueueCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'bt-queue:process';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Processes items in the processing queue.';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
if (!UserConfig::isImageProcessingQueueEnabled())
{
$this->output->error('The image processing queue is not enabled');
}
$rabbitmq = new RabbitMQService();
$this->output->writeln('Monitoring queue');
$rabbitmq->waitOnQueue([$this, 'processQueueItem']);
}
/**
* Processes a single item from the queue.
*
* @param $msg
* @return void
*/
public function processQueueItem($msg)
{
$queueItemID = intval($msg->body);
$this->output->writeln(sprintf('Processing queue item %d', $queueItemID));
/** @var QueueItem $queueItem */
$queueItem = QueueItem::where('id', $queueItemID)->first();
if (is_null($queueItem))
{
$this->output->writeln('Queue item does not exist; skipping');
$msg->delivery_info['channel']->basic_ack($msg->delivery_info['delivery_tag']);
return;
}
try
{
switch (strtolower($queueItem->action_type))
{
case 'photo.analyse':
$this->processPhotoAnalyseMessage($queueItem);
break;
case 'photo.bulk_action.change_album':
case 'photo.bulk_action.delete':
case 'photo.bulk_action.flip_both':
case 'photo.bulk_action.flip_horizontal':
case 'photo.bulk_action.flip_vertical':
case 'photo.bulk_action.refresh_thumbnails':
case 'photo.bulk_action.rotate_left':
case 'photo.bulk_action.rotate_right':
$this->processPhotoBulkActionMessage($queueItem);
break;
default:
$this->output->writeln(sprintf('Action %s is not recognised, skipping', $queueItem->action_type));
return;
}
$queueItem->completed_at = new \DateTime();
$queueItem->save();
}
catch (\Exception $ex)
{
$this->output->error($ex->getMessage());
$queueItem->error_message = $ex->getMessage();
}
finally
{
$queueItem->completed_at = new \DateTime();
$queueItem->save();
$msg->delivery_info['channel']->basic_ack($msg->delivery_info['delivery_tag']);
}
}
private function createActivityRecord(Photo $photo, $type, $userID, $activityDateTime = null)
{
if (is_null($activityDateTime))
{
$activityDateTime = new \DateTime();
}
$userActivity = new UserActivity();
$userActivity->user_id = $userID;
$userActivity->activity_at = $activityDateTime;
$userActivity->type = $type;
$userActivity->photo_id = $photo->id;
$userActivity->save();
}
private function processPhotoAnalyseMessage(QueueItem $queueItem)
{
$this->output->writeln(sprintf('Analysing photo ID %l (batch: %s)', $queueItem->photo_id, $queueItem->batch_reference));
/** @var Photo $photo */
$photo = $queueItem->photo;
if (is_null($photo))
{
$this->output->writeln('Photo does not exist; skipping');
return;
}
/* IF CHANGING THIS LOGIC, ALSO CHECK PhotoController::analyse */
$photoService = new PhotoService($photo);
$photoService->analyse($queueItem->batch_reference);
// Log an activity record for the user's feed (remove an existing one as the date may have changed)
$this->removeExistingActivityRecords($photo, 'photo.taken');
if (!is_null($photo->taken_at))
{
// Log an activity record for the user's feed
$this->createActivityRecord($photo, 'photo.taken', $queueItem->user_id, $photo->taken_at);
}
$queueItem->is_successful = true;
}
private function processPhotoBulkActionMessage(QueueItem $queueItem)
{
$action = str_replace('photo.bulk_action.', '', $queueItem->action_type);
$this->output->writeln(sprintf('Apply action \'%s\' to photo ID %l (batch: %s)', $action, $queueItem->photo_id, $queueItem->batch_reference));
/** @var Photo $photo */
$photo = $queueItem->photo;
if (is_null($photo))
{
$this->output->writeln('Photo does not exist; skipping');
return;
}
/** @var User $user */
$user = $queueItem->user;
if (is_null($user))
{
$this->output->writeln('User does not exist; skipping');
return;
}
$photoService = new PhotoService($photo);
switch (strtolower($action))
{
/* This needs a bit more work - we need to also store the new album ID in the queue_items table */
case 'change_album':
if ($user->can('change-metadata', $photo))
{
$newAlbum = Album::where('id', intval($queueItem->new_album_id))->first();
if (is_null($newAlbum) || !$user->can('upload-photos', $newAlbum))
{
$this->output->writeln('Target album does not exist or user does not have permission.');
return;
}
$this->output->writeln(sprintf('Moving photo to album \'%s\'', $newAlbum->name));
$photoService->changeAlbum($newAlbum);
}
break;
case 'delete':
if ($user->can('delete', $photo))
{
$photoService->delete();
}
break;
case 'flip_both':
if ($user->can('manipulate', $photo))
{
$photoService->flip(true, true);
}
break;
case 'flip_horizontal':
if ($user->can('manipulate', $photo))
{
$photoService->flip(true, false);
}
break;
case 'flip_vertical':
if ($user->can('manipulate', $photo))
{
$photoService->flip(false, true);
}
break;
case 'refresh_thumbnails':
if ($user->can('change-metadata', $photo))
{
$photoService->regenerateThumbnails();
}
break;
case 'rotate_left':
if ($user->can('manipulate', $photo))
{
$photoService->rotate(90);
}
break;
case 'rotate_right':
if ($user->can('manipulate', $photo))
{
$photoService->rotate(270);
}
break;
default:
$this->output->writeln(sprintf('Action \'%s\' not recognised; skipping'));
return;
}
}
private function removeExistingActivityRecords(Photo $photo, $type)
{
$existingFeedRecords = UserActivity::where([
'user_id' => $photo->user_id,
'photo_id' => $photo->id,
'type' => $type
])->get();
foreach ($existingFeedRecords as $existingFeedRecord)
{
$existingFeedRecord->delete();
}
}
}

View File

@ -1,143 +0,0 @@
<?php
namespace App\Console\Commands;
use App\EmailLog;
use App\Facade\UserConfig;
use App\Http\Middleware\GlobalConfiguration;
use Illuminate\Console\Command;
use Illuminate\Mail\Message;
class SendEmailsCommand extends Command
{
private $maxPerBatch;
private $numberOfAttempts;
private $secondsBetweenPolls;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'bt-queue:send-emails {--poll}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Sends e-mails queued in the database.';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
$this->maxPerBatch = config('mail.queue.emails_per_batch');
$this->numberOfAttempts = config('mail.queue.max_attempts');
$this->secondsBetweenPolls = config('mail.queue.seconds_between_polls');
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
if (!UserConfig::get('queue_emails'))
{
$this->output->error('E-mail queueing is not enabled. E-mails are being sent immediately.');
}
$this->output->writeln('Setting mail configuration');
GlobalConfiguration::updateMailConfig();
$this->output->writeln('E-mail queue runner started');
while (true)
{
$emailsToSend = EmailLog::where([
['sent_at', null],
['number_attempts', '<', $this->numberOfAttempts]
])->limit($this->maxPerBatch)->get();
$this->output->writeln(sprintf(
'%d e-mail%s to send',
$emailsToSend->count(),
$emailsToSend->count() == 1 ? '' : 's'
));
/** @var EmailLog $emailToSend */
foreach ($emailsToSend as $emailToSend)
{
$this->sendEmail($emailToSend);
}
if (!$this->option('poll'))
{
exit();
}
sleep($this->secondsBetweenPolls);
}
}
private function sendEmail(EmailLog $emailLog)
{
$this->output->writeln(sprintf('Sending message with subject \'%s\'', $emailLog->subject));
try
{
app('mailer')->send(
[],
[],
function (Message $message) use ($emailLog)
{
$message->setFrom($emailLog->sender_address, $emailLog->sender_name);
$message->setSubject($emailLog->subject);
$this->addAddresses($emailLog->to_addresses, $message, 'To');
$this->addAddresses($emailLog->cc_addresses, $message, 'Cc');
$this->addAddresses($emailLog->bcc_addresses, $message, 'Bcc');
$message->addPart($emailLog->body_plain, 'text/plain');
$message->setBody($emailLog->body_html, 'text/html');
}
);
$emailLog->sent_at = new \DateTime();
$this->output->writeln('Send completed');
}
catch (\Exception $ex)
{
$this->output->error(sprintf('Send failed: %s', $ex->getMessage()));
}
finally
{
$emailLog->number_attempts++;
$emailLog->save();
}
}
private function addAddresses($dbFieldData, Message $message, $property)
{
$decoded = json_decode($dbFieldData);
if (is_array($decoded))
{
foreach ($decoded as $addressInfo)
{
$this->output->writeln(sprintf('Adding %s address: \'"%s" <%s>\'', $property, $addressInfo->name, $addressInfo->address));
$message->{"set{$property}"}($addressInfo->address, $addressInfo->name);
}
}
}
}

View File

@ -6,7 +6,6 @@ use App\Console\Commands\ProcessUploadCommand;
use App\Console\Commands\RegenerateThumbnailsCommand;
use App\Upload;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
class Kernel extends ConsoleKernel
@ -37,11 +36,5 @@ class Kernel extends ConsoleKernel
protected function commands()
{
require base_path('routes/console.php');
// We can only auto-load commands for Laravel 5.5.0 or above
if (version_compare(Application::VERSION, '5.5.0') >= 0)
{
$this->load(__DIR__.'/Commands');
}
}
}

View File

@ -1,26 +0,0 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class EmailLog extends Model
{
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'sender_user_id',
'queued_at',
'sent_at',
'sender_name',
'sender_address',
'to_addresses',
'cc_addresses',
'subject',
'body_plain',
'body_html'
];
}

View File

@ -1,25 +0,0 @@
<?php
namespace App\Exceptions;
use Throwable;
class BackblazeRetryException extends \Exception
{
private $innerException;
/**
* @return mixed
*/
public function getInnerException()
{
return $this->innerException;
}
public function __construct($httpCode, \Exception $innerException)
{
parent::__construct('Backblaze requested to retry the request');
$this->innerException = $innerException;
}
}

View File

@ -1,23 +0,0 @@
<?php
namespace App\Exceptions;
class DropboxRetryException extends \Exception
{
private $innerException;
/**
* @return mixed
*/
public function getInnerException()
{
return $this->innerException;
}
public function __construct($httpCode, \Exception $innerException)
{
parent::__construct('Dropbox requested to retry the request');
$this->innerException = $innerException;
}
}

View File

@ -1,54 +0,0 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class ExternalService extends Model
{
public const DROPBOX = 'dropbox';
public const FACEBOOK = 'facebook';
public const GOOGLE = 'google';
public const TWITTER = 'twitter';
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = ['name', 'service_type', 'app_id', 'app_secret'];
/**
* Gets all possible service configurations for the given service type.
* @param $serviceType
* @return ExternalService[]
*/
public static function getForService($serviceType)
{
return ExternalService::where('service_type', $serviceType)->get();
}
public function isDropbox()
{
// This logic must be mirrored in external_services.js
return $this->service_type == self::DROPBOX;
}
public function isFacebook()
{
// This logic must be mirrored in external_services.js
return $this->service_type == self::FACEBOOK;
}
public function isGoogle()
{
// This logic must be mirrored in external_services.js
return $this->service_type == self::GOOGLE;
}
public function isTwitter()
{
// This logic must be mirrored in external_services.js
return $this->service_type == self::TWITTER;
}
}

View File

@ -1,18 +0,0 @@
<?php
namespace App\Facade;
use Illuminate\Support\Facades\Facade;
class Misc extends Facade
{
/**
* Get the registered name of the component.
*
* @return string
*/
protected static function getFacadeAccessor()
{
return 'misc';
}
}

View File

@ -1,66 +0,0 @@
<?php
namespace App\Helpers;
use App\AlbumSources\IAnalysisQueueSource;
use App\Facade\UserConfig;
use App\Storage;
class AnalysisQueueHelper
{
/**
* Gets the storage queue source in use.
* @return IAnalysisQueueSource
*/
public static function getStorageQueueSource()
{
$queueStorage = Storage::find(UserConfig::get('analysis_queue_storage_location'));
$queueSource = self::createStorageSource($queueStorage);
if (is_null($queueSource))
{
throw new \Exception(sprintf('Queue storage \'%s\' does not support the analysis queue', $queueStorage->name));
}
return $queueSource;
}
/**
* Gets a list of compatible storage sources for the analysis queue.
* @return array
*/
public static function getCompatibleStorages()
{
$storageSources = [];
foreach (Storage::where('is_active', true)->orderBy('name')->get() as $storage)
{
$queueSource = self::createStorageSource($storage);
if (is_null($queueSource))
{
continue;
}
$storageSources[$storage->id] = $storage->name;
}
return $storageSources;
}
private static function createStorageSource(Storage $queueStorage)
{
$fullClassName = sprintf('App\AlbumSources\%s', $queueStorage->source);
/** @var IAnalysisQueueSource $source */
$source = new $fullClassName;
if (!$source instanceof IAnalysisQueueSource)
{
return null;
}
$source->setConfiguration($queueStorage);
return $source;
}
}

View File

@ -2,21 +2,17 @@
namespace App\Helpers;
use App\Album;
use App\AlbumSources\AmazonS3Source;
use App\AlbumSources\BackblazeB2Source;
use App\AlbumSources\DropboxSource;
use App\AlbumSources\IAlbumSource;
use App\AlbumSources\LocalFilesystemSource;
use App\AlbumSources\OpenStackSource;
use App\AlbumSources\OpenStackV1Source;
use App\AlbumSources\RackspaceSource;
use App\Configuration;
use App\Storage;
class ConfigHelper
{
/** @var mixed Cache of configuration values */
private $cache;
public function allowedAlbumViews()
{
return ['default', 'slideshow'];
@ -27,17 +23,8 @@ class ConfigHelper
return [
'Y-m-d - H:i',
'd/m/Y - H:i',
'd-m-Y - H:i',
'd.m.Y - H:i',
'd/M/Y - H:i',
'd-M-Y - H:i',
'm/d/Y - H:i',
'm-d-Y - H:i',
'm.d.Y - H:i',
'jS F Y - H:i',
'jS M. Y - H:i',
'F jS Y - H:i',
'M. jS Y - H:i'
];
}
@ -48,8 +35,6 @@ class ConfigHelper
$classes = [
LocalFilesystemSource::class,
AmazonS3Source::class,
BackblazeB2Source::class,
DropboxSource::class,
OpenStackSource::class,
RackspaceSource::class
];
@ -102,35 +87,15 @@ class ConfigHelper
$currentAppName = $this->get('app_name', false);
return array(
'albums_menu_parents_only' => false,
'albums_menu_number_items' => 10,
'allow_photo_comments' => false,
'allow_photo_comments_anonymous' => true,
'allow_self_registration' => true,
'analysis_queue_storage_location' => Storage::where('is_default', true)->first()->id,
'analytics_code' => '',
'app_name' => trans('global.app_name'),
'date_format' => $this->allowedDateFormats()[0],
'default_album_view' => $this->allowedAlbumViews()[0],
'enable_visitor_hits' => false,
'facebook_external_service_id' => 0,
'google_external_service_id' => 0,
'hotlink_protection' => false,
'items_per_page' => 12,
'items_per_page_admin' => 10,
'moderate_anonymous_users' => true,
'moderate_known_users' => true,
'photo_comments_allowed_html' => 'p,div,span,a,b,i,u',
'photo_comments_thread_depth' => 3,
'public_statistics' => true,
'queue_emails' => false,
'rabbitmq_enabled' => false,
'rabbitmq_server' => 'localhost',
'rabbitmq_password' => encrypt('guest'),
'rabbitmq_port' => 5672,
'rabbitmq_queue' => 'blue_twilight',
'rabbitmq_username' => 'guest',
'rabbitmq_vhost' => '/',
'recaptcha_enabled_registration' => false,
'recaptcha_secret_key' => '',
'recaptcha_site_key' => '',
@ -143,24 +108,13 @@ class ConfigHelper
'smtp_password' => '',
'smtp_port' => 25,
'smtp_username' => '',
'social_facebook_login' => false,
'social_google_login' => false,
'social_twitter_login' => false,
'social_user_feeds' => false,
'social_user_profiles' => false,
'theme' => 'default',
'twitter_external_service_id' => 0
'theme' => 'default'
);
}
public function get($key, $defaultIfUnset = true)
{
if (is_null($this->cache))
{
$this->loadCache();
}
$config = isset($this->cache[$key]) ? $this->cache[$key] : null;
$config = Configuration::where('key', $key)->first();
if (is_null($config))
{
@ -180,7 +134,6 @@ class ConfigHelper
{
$results = array();
/** @var Configuration $config */
foreach (Configuration::all() as $config)
{
$results[$config->key] = $config->value;
@ -191,64 +144,15 @@ class ConfigHelper
public function getOrCreateModel($key)
{
$config = isset($this->cache[$key]) ? $this->cache[$key] : null;
$config = Configuration::where('key', $key)->first();
if (is_null($config) || $config === false)
{
$config = new Configuration();
$config->key = $key;
$config->value = '';
$config->save();
$this->loadCache();
}
return $config;
}
public function isImageProcessingQueueEnabled()
{
return $this->get('rabbitmq_enabled') &&
!empty($this->get('rabbitmq_server')) &&
!empty($this->get('rabbitmq_port')) &&
!empty($this->get('rabbitmq_username')) &&
!empty($this->get('rabbitmq_password')) &&
!empty($this->get('rabbitmq_queue')) &&
!empty($this->get('rabbitmq_vhost'));
}
public function isLoginWithFacebookEnabled()
{
return boolval($this->get('social_facebook_login')) &&
intval($this->get('facebook_external_service_id')) > 0;
}
public function isLoginWithGoogleEnabled()
{
return boolval($this->get('social_google_login')) &&
intval($this->get('google_external_service_id')) > 0;
}
public function isLoginWithTwitterEnabled()
{
return boolval($this->get('social_twitter_login')) &&
intval($this->get('twitter_external_service_id')) > 0;
}
public function isSocialMediaLoginEnabled()
{
return $this->isLoginWithFacebookEnabled() ||
$this->isLoginWithGoogleEnabled() ||
$this->isLoginWithTwitterEnabled();
}
private function loadCache()
{
$this->cache = null;
/** @var Configuration $config */
foreach (Configuration::all() as $config)
{
$this->cache[$config->key] = $config;
}
}
}

View File

@ -33,12 +33,17 @@ class DbHelper
public static function getAlbumsForCurrentUser($parentID = -1)
{
$query = self::getAlbumsForCurrentUser_NonPaged('list', $parentID);
$query = self::getAlbumsForCurrentUser_NonPaged();
if ($parentID == 0)
{
$query = $query->where('albums.parent_album_id', null);
}
return $query->paginate(UserConfig::get('items_per_page'));
}
public static function getAlbumsForCurrentUser_NonPaged($permission = 'list', $parentAlbumID = -1)
public static function getAlbumsForCurrentUser_NonPaged()
{
$albumsQuery = Album::query();
$user = Auth::user();
@ -47,23 +52,40 @@ class DbHelper
{
/* Admin users always get everything, therefore no filters are necessary */
}
else if (is_null($user))
{
/* Anonymous users need to check the album_anonymous_permissions table. If not in this table, you're not allowed! */
$albumsQuery = Album::join('album_anonymous_permissions', 'album_anonymous_permissions.album_id', '=', 'albums.id')
->join('permissions', 'permissions.id', '=', 'album_anonymous_permissions.permission_id')
->where([
['permissions.section', 'album'],
['permissions.description', 'list']
]);
}
else
{
$helper = new PermissionsHelper();
$albumIDs = $helper->getAlbumIDs($permission, $user);
//dd($albumIDs->toArray());
$albumsQuery->whereIn('albums.id', $albumIDs);
//
}
/*
Other users need to check either the album_group_permissions or album_user_permissions table. If not in either of these tables,
you're not allowed!
*/
$parentAlbumID = intval($parentAlbumID);
if ($parentAlbumID == 0)
{
$albumsQuery->where('albums.parent_album_id', null);
}
else if ($parentAlbumID > 0)
{
$albumsQuery->where('albums.parent_album_id', $parentAlbumID);
$albumsQuery = Album::leftJoin('album_group_permissions', 'album_group_permissions.album_id', '=', 'albums.id')
->leftJoin('album_user_permissions', 'album_user_permissions.album_id', '=', 'albums.id')
->leftJoin('permissions AS group_permissions', 'group_permissions.id', '=', 'album_group_permissions.permission_id')
->leftJoin('permissions AS user_permissions', 'user_permissions.id', '=', 'album_user_permissions.permission_id')
->leftJoin('user_groups', 'user_groups.group_id', '=', 'album_group_permissions.group_id')
->where('albums.user_id', $user->id)
->orWhere([
['group_permissions.section', 'album'],
['group_permissions.description', 'list'],
['user_groups.user_id', $user->id]
])
->orWhere([
['user_permissions.section', 'album'],
['user_permissions.description', 'list'],
['album_user_permissions.user_id', $user->id]
]);
}
return $albumsQuery->select('albums.*')
@ -81,14 +103,4 @@ class DbHelper
{
return Album::where('url_path', $urlPath)->first();
}
public static function getChildAlbumsCount(Album $album)
{
return self::getAlbumsForCurrentUser_NonPaged('list', $album->id)->count();
}
public static function getChildAlbums(Album $album)
{
return self::getAlbumsForCurrentUser_NonPaged('list', $album->id)->get();
}
}

View File

@ -46,7 +46,7 @@ class FileHelper
if (!file_exists($path))
{
@mkdir($path, 0755, true);
mkdir($path, 0755, true);
}
return $path;

View File

@ -52,68 +52,14 @@ class MiscHelper
return (int) $val;
}
/**
* Convert a decimal (e.g. 3.5) to a fraction (e.g. 7/2).
* Adapted from: http://jonisalonen.com/2012/converting-decimal-numbers-to-ratios/
*
* @param float $decimal the decimal number.
*
* @return array|bool a 1/2 would be [1, 2] array (this can be imploded with '/' to form a string)
*/
public static function decimalToFraction($decimal)
{
if ($decimal < 0 || !is_numeric($decimal)) {
// Negative digits need to be passed in as positive numbers
// and prefixed as negative once the response is imploded.
return false;
}
if ($decimal == 0) {
return [0, 0];
}
$tolerance = 1.e-4;
$numerator = 1;
$h2 = 0;
$denominator = 0;
$k2 = 1;
$b = 1 / $decimal;
do {
$b = 1 / $b;
$a = floor($b);
$aux = $numerator;
$numerator = $a * $numerator + $h2;
$h2 = $aux;
$aux = $denominator;
$denominator = $a * $denominator + $k2;
$k2 = $aux;
$b = $b - $a;
} while (abs($decimal - $numerator / $denominator) > $decimal * $tolerance);
return [
$numerator,
$denominator
];
}
public static function ensureHasTrailingSlash($string)
{
if (strlen($string) > 0 && substr($string, strlen($string) - 1, 1) != '/')
{
$string .= '/';
}
return $string;
}
public static function getEnvironmentFilePath()
{
return sprintf('%s/.env', dirname(dirname(__DIR__)));
}
public static function getEnvironmentSetting($settingName, $envFile = null)
public static function getEnvironmentSetting($settingName)
{
$envFile = $envFile ?? MiscHelper::getEnvironmentFilePath();
$envFile = MiscHelper::getEnvironmentFilePath();
if (!file_exists($envFile))
{
@ -134,12 +80,6 @@ class MiscHelper
return MiscHelper::getEnvironmentSetting('APP_INSTALLED');
}
public static function isExecEnabled()
{
$disabled = explode(',', ini_get('disable_functions'));
return !in_array('exec', $disabled);
}
/**
* Tests whether the provided URL belongs to the current application (i.e. both scheme and hostname match.)
* @param $url

View File

@ -1,258 +0,0 @@
<?php
namespace App\Helpers;
use App\Album;
use App\AlbumDefaultAnonymousPermission;
use App\AlbumDefaultGroupPermission;
use App\AlbumDefaultUserPermission;
use App\Permission;
use App\User;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
class PermissionsHelper
{
public function getAlbumIDs($permission = 'list', User $user = null)
{
$result = [];
// First check if the anonymous user can do what is being requested - if so, the permission would also inherit
// to logged-in users
$anonymousUsersCan = DB::table('album_permissions_cache')
->join('permissions', 'permissions.id', '=', 'album_permissions_cache.permission_id')
->where([
['album_permissions_cache.user_id', null],
['permissions.section', 'album'],
['permissions.description', $permission]
])
->select('album_permissions_cache.album_id')
->distinct()
->get();
foreach ($anonymousUsersCan as $item)
{
$result[] = $item->album_id;
}
$query = DB::table('album_permissions_cache')
->join('permissions', 'permissions.id', '=', 'album_permissions_cache.permission_id')
->where([
['album_permissions_cache.user_id', (is_null($user) || $user->isAnonymous() ? null : $user->id)],
['permissions.section', 'album'],
['permissions.description', $permission]
])
->select('album_permissions_cache.album_id')
->distinct()
->get();
foreach ($query as $item)
{
if (!in_array($item->album_id, $result))
{
$result[] = $item->album_id;
}
}
return $result;
}
public function rebuildCache()
{
$this->rebuildAlbumCache();
}
public function userCan_Album(Album $album, User $user, $permission)
{
// First check if the anonymous user can do what is being requested - if so, the permission would also inherit
// to logged-in users
$anonymousUsersCan = DB::table('album_permissions_cache')
->join('permissions', 'permissions.id', '=', 'album_permissions_cache.permission_id')
->where([
['album_permissions_cache.album_id', $album->id],
['album_permissions_cache.user_id', null],
['permissions.section', 'album'],
['permissions.description', $permission]
])
->count() > 0;
if ($anonymousUsersCan)
{
return true;
}
return DB::table('album_permissions_cache')
->join('permissions', 'permissions.id', '=', 'album_permissions_cache.permission_id')
->where([
['album_permissions_cache.album_id', $album->id],
['album_permissions_cache.user_id', (is_null($user) || $user->isAnonymous() ? null : $user->id)],
['permissions.section', 'album'],
['permissions.description', $permission]
])
->count() > 0;
}
public function usersWhoCan_Album(Album $album, $permission)
{
$users = DB::table('album_permissions_cache')
->join('permissions', 'permissions.id', '=', 'album_permissions_cache.permission_id')
->where([
['album_permissions_cache.album_id', $album->id],
['permissions.section', 'album'],
['permissions.description', $permission]
])
->get();
// Include the album's owner (who can do everything)
$users->push($album->user);
return $users;
}
private function rebuildAlbumCache()
{
// Get a list of albums
$albums = Album::all();
// Get a list of all configured permissions
$albumUserPermissions = DB::table('album_user_permissions')->get();
$albumGroupPermissions = DB::table('album_group_permissions')->get();
$albumAnonPermissions = DB::table('album_anonymous_permissions')->get();
$defaultAlbumUserPermissions = AlbumDefaultUserPermission::all();
$defaultAlbumGroupPermissions = AlbumDefaultGroupPermission::all();
$defaultAnonPermissions = AlbumDefaultAnonymousPermission::all();
// Get a list of all user->group memberships
$userGroups = DB::table('user_groups')->get();
// Build a matrix of new permissions
$permissionsCache = [];
/** @var Album $album */
foreach ($albums as $album)
{
$effectiveAlbumID = $album->effectiveAlbumIDForPermissions();
if ($effectiveAlbumID === 0)
{
/* Use the default permissions list */
foreach ($defaultAnonPermissions as $anonymousPermission)
{
$permissionsCache[] = [
'album_id' => $album->id,
'permission_id' => $anonymousPermission->permission_id,
'created_at' => new \DateTime(),
'updated_at' => new \DateTime()
];
}
foreach ($defaultAlbumUserPermissions as $userPermission)
{
$permissionsCache[] = [
'user_id' => $userPermission->user_id,
'album_id' => $album->id,
'permission_id' => $userPermission->permission_id,
'created_at' => new \DateTime(),
'updated_at' => new \DateTime()
];
}
foreach ($defaultAlbumGroupPermissions as $groupPermission)
{
// Get a list of users in this group, and add one per user
$usersInGroup = array_filter($userGroups->toArray(), function ($item) use ($groupPermission)
{
return $item->group_id = $groupPermission->group_id;
});
foreach ($usersInGroup as $userGroup)
{
$permissionsCache[] = [
'user_id' => $userGroup->user_id,
'album_id' => $album->id,
'permission_id' => $groupPermission->permission_id,
'created_at' => new \DateTime(),
'updated_at' => new \DateTime()
];
}
}
}
else
{
/* Use the specified album-specific permissions */
$anonymousPermissions = array_filter($albumAnonPermissions->toArray(), function ($item) use ($effectiveAlbumID)
{
return ($item->album_id == $effectiveAlbumID);
});
foreach ($anonymousPermissions as $anonymousPermission)
{
$permissionsCache[] = [
'album_id' => $album->id,
'permission_id' => $anonymousPermission->permission_id,
'created_at' => new \DateTime(),
'updated_at' => new \DateTime()
];
}
$userPermissions = array_filter($albumUserPermissions->toArray(), function ($item) use ($effectiveAlbumID)
{
return ($item->album_id == $effectiveAlbumID);
});
foreach ($userPermissions as $userPermission)
{
$permissionsCache[] = [
'user_id' => $userPermission->user_id,
'album_id' => $album->id,
'permission_id' => $userPermission->permission_id,
'created_at' => new \DateTime(),
'updated_at' => new \DateTime()
];
}
$groupPermissions = array_filter($albumGroupPermissions->toArray(), function ($item) use ($effectiveAlbumID)
{
return ($item->album_id == $effectiveAlbumID);
});
foreach ($groupPermissions as $groupPermission)
{
// Get a list of users in this group, and add one per user
$usersInGroup = array_filter($userGroups->toArray(), function ($item) use ($groupPermission)
{
return $item->group_id = $groupPermission->group_id;
});
foreach ($usersInGroup as $userGroup)
{
$permissionsCache[] = [
'user_id' => $userGroup->user_id,
'album_id' => $album->id,
'permission_id' => $groupPermission->permission_id,
'created_at' => new \DateTime(),
'updated_at' => new \DateTime()
];
}
}
}
}
$this->savePermissionsCache($permissionsCache);
}
private function savePermissionsCache(array $cacheToSave)
{
DB::transaction(function() use ($cacheToSave)
{
DB::table('album_permissions_cache')->truncate();
foreach ($cacheToSave as $cacheItem)
{
DB::table('album_permissions_cache')->insert($cacheItem);
}
});
}
}

View File

@ -2,37 +2,8 @@
namespace App\Helpers;
use Illuminate\Support\Facades\DB;
class ValidationHelper
{
public function albumPathUnique($attribute, $value, $parameters, $validator)
{
$data = $validator->getData();
$parentID = intval($data['parent_album_id']);
$name = $data['name'];
if ($parentID === 0)
{
$parentID = null;
}
$queryParams = [
['name', $name],
['parent_album_id', $parentID]
];
if (count($parameters) > 0)
{
$existingAlbumID = intval($parameters[0]);
$queryParams[] = ['id', '<>', $existingAlbumID];
}
$count = DB::table('albums')->where($queryParams)->count();
return ($count == 0);
}
public function directoryExists($attribute, $value, $parameters, $validator)
{
return file_exists($value) && is_dir($value);

View File

@ -3,26 +3,20 @@
namespace App\Http\Controllers\Admin;
use App\Album;
use App\AlbumDefaultAnonymousPermission;
use App\AlbumDefaultGroupPermission;
use App\AlbumDefaultUserPermission;
use App\AlbumRedirect;
use App\Facade\Theme;
use App\Facade\UserConfig;
use App\Group;
use App\Helpers\DbHelper;
use App\Helpers\MiscHelper;
use App\Helpers\PermissionsHelper;
use App\Http\Controllers\Controller;
use App\Http\Requests;
use App\Label;
use App\Permission;
use App\Photo;
use App\Services\AlbumService;
use App\Services\PhotoService;
use App\Storage;
use App\User;
use App\UserActivity;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Auth;
@ -31,30 +25,6 @@ use Illuminate\Support\Facades\View;
class AlbumController extends Controller
{
public static function doesGroupHaveDefaultPermission(Group $group, Permission $permission)
{
return AlbumDefaultGroupPermission::where([
'group_id' => $group->id,
'permission_id' => $permission->id
])->count() > 0;
}
public static function doesUserHaveDefaultPermission($user, Permission $permission)
{
// User will be null for anonymous users
if (is_null($user))
{
return AlbumDefaultAnonymousPermission::where(['permission_id' => $permission->id])->count() > 0;
}
else
{
return AlbumDefaultUserPermission::where([
'user_id' => $user->id,
'permission_id' => $permission->id
])->count() > 0;
}
}
public function __construct()
{
$this->middleware('auth');
@ -73,7 +43,7 @@ class AlbumController extends Controller
if (count($photos) == 0)
{
return redirect(route('albums.show', ['album' => $album->id]));
return redirect(route('albums.show', ['id' => $album->id]));
}
return Theme::render('admin.analyse_album', ['album' => $album, 'photos' => $photos, 'queue_token' => $queue_token]);
@ -110,41 +80,6 @@ class AlbumController extends Controller
]);
}
public function defaultPermissions()
{
$this->authorizeAccessToAdminPanel('admin:manage-albums');
$addNewGroups = [];
$existingGroups = [];
foreach (Group::orderBy('name')->get() as $group)
{
if (AlbumDefaultGroupPermission::where('group_id', $group->id)->count() == 0)
{
$addNewGroups[] = $group;
}
else
{
$existingGroups[] = $group;
}
}
$existingUsers = [];
foreach (User::orderBy('name')->get() as $user)
{
if (AlbumDefaultUserPermission::where('user_id', $user->id)->count() > 0)
{
$existingUsers[] = $user;
}
}
return Theme::render('admin.album_default_permissions', [
'add_new_groups' => $addNewGroups,
'all_permissions' => Permission::where('section', 'album')->get(),
'existing_groups' => $existingGroups,
'existing_users' => $existingUsers
]);
}
public function delete($id)
{
$this->authorizeAccessToAdminPanel('admin:manage-albums');
@ -170,7 +105,7 @@ class AlbumController extends Controller
$redirect->delete();
$request->session()->flash('success', trans('admin.delete_redirect_success_message'));
return redirect(route('albums.show', ['album' => $id, 'tab' => 'redirects']));
return redirect(route('albums.show', ['id' => $id, 'tab' => 'redirects']));
}
/**
@ -185,12 +120,6 @@ class AlbumController extends Controller
$album = $this->loadAlbum($id, 'delete');
if ($album->children()->count() > 0)
{
$request->session()->flash('error', trans('admin.delete_album_failed_children', ['album' => $album->name]));
return redirect(route('albums.index'));
}
// Delete all the photo files
/** @var Photo $photo */
foreach ($album->photos as $photo)
@ -243,10 +172,6 @@ class AlbumController extends Controller
// Only get top-level albums
$albums = DbHelper::getAlbumsForCurrentUser(0);
foreach ($albums as $album)
{
$this->loadChildAlbums($album);
}
return Theme::render('admin.list_albums', [
'albums' => $albums,
@ -254,153 +179,6 @@ class AlbumController extends Controller
]);
}
/**
* Show the form for editing the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function metadata(Request $request, $id)
{
$this->authorizeAccessToAdminPanel('admin:manage-albums');
/** @var Album $album */
$album = $this->loadAlbum($id);
$photosNeededToUpdate = $album->photos()->where('metadata_version', '<', PhotoService::METADATA_VERSION)->get();
return Theme::render('admin.album_metadata', [
'album' => $album,
'current_metadata' => PhotoService::METADATA_VERSION,
'photos' => $photosNeededToUpdate,
'queue_token' => MiscHelper::randomString()
]);
}
public function setDefaultGroupPermissions(Request $request)
{
$this->authorizeAccessToAdminPanel('admin:manage-albums');
if ($request->get('action') == 'add_group' && $request->has('group_id'))
{
/* Add a new group to the default permission list */
/** @var Group $group */
$group = Group::where('id', $request->get('group_id'))->first();
if (is_null($group))
{
App::abort(404);
}
// Link all default permissions to the group
/** @var Permission $permission */
foreach (Permission::where(['section' => 'album', 'is_default' => true])->get() as $permission)
{
$defaultPermission = new AlbumDefaultGroupPermission();
$defaultPermission->group_id = $group->id;
$defaultPermission->permission_id = $permission->id;
$defaultPermission->save();
}
}
else if ($request->get('action') == 'update_group_permissions')
{
/* Update existing group permissions for this album */
AlbumDefaultGroupPermission::truncate();
$permissions = $request->get('permissions');
if (is_array($permissions))
{
foreach ($permissions as $groupID => $permissionIDs)
{
foreach ($permissionIDs as $permissionID)
{
$defaultPermission = new AlbumDefaultGroupPermission();
$defaultPermission->group_id = $groupID;
$defaultPermission->permission_id = $permissionID;
$defaultPermission->save();
}
}
}
}
// Rebuild the permissions cache
$helper = new PermissionsHelper();
$helper->rebuildCache();
return redirect(route('albums.defaultPermissions'));
}
public function setDefaultUserPermissions(Request $request)
{
$this->authorizeAccessToAdminPanel('admin:manage-albums');
if ($request->get('action') == 'add_user' && $request->has('user_id'))
{
/* Add a new user to the permission list for this album */
/** @var User $user */
$user = User::where('id', $request->get('user_id'))->first();
if (is_null($user))
{
App::abort(404);
}
// Link all default permissions to the group
/** @var Permission $permission */
foreach (Permission::where(['section' => 'album', 'is_default' => true])->get() as $permission)
{
$defaultPermission = new AlbumDefaultUserPermission();
$defaultPermission->user_id = $user->id;
$defaultPermission->permission_id = $permission->id;
$defaultPermission->save();
}
}
else if ($request->get('action') == 'update_user_permissions')
{
/* Update existing user and anonymous permissions for this album */
AlbumDefaultAnonymousPermission::truncate();
AlbumDefaultUserPermission::truncate();
$permissions = $request->get('permissions');
if (is_array($permissions))
{
if (isset($permissions['anonymous']))
{
foreach ($permissions['anonymous'] as $permissionID)
{
$defaultPermission = new AlbumDefaultAnonymousPermission();
$defaultPermission->permission_id = $permissionID;
$defaultPermission->save();
}
}
foreach ($permissions as $key => $value)
{
$userID = intval($key);
if ($userID == 0)
{
// Skip non-numeric IDs (e.g. anonymous)
continue;
}
foreach ($value as $permissionID)
{
$defaultPermission = new AlbumDefaultUserPermission();
$defaultPermission->user_id = $userID;
$defaultPermission->permission_id = $permissionID;
$defaultPermission->save();
}
}
}
}
// Rebuild the permissions cache
$helper = new PermissionsHelper();
$helper->rebuildCache();
return redirect(route('albums.defaultPermissions'));
}
public function setGroupPermissions(Request $request, $id)
{
$this->authorizeAccessToAdminPanel('admin:manage-albums');
@ -454,10 +232,6 @@ class AlbumController extends Controller
$album->save();
// Rebuild the permissions cache
$helper = new PermissionsHelper();
$helper->rebuildCache();
return redirect(route('albums.show', [$album->id, 'tab' => 'permissions']));
}
@ -533,10 +307,6 @@ class AlbumController extends Controller
$album->save();
// Rebuild the permissions cache
$helper = new PermissionsHelper();
$helper->rebuildCache();
return redirect(route('albums.show', [$album->id, 'tab' => 'permissions']));
}
@ -624,7 +394,6 @@ class AlbumController extends Controller
'existing_users' => $existingUsers,
'file_upload_limit' => $fileUploadLimit,
'is_upload_enabled' => $isUploadEnabled,
'labels' => Label::all(),
'max_post_limit' => $postLimit,
'max_post_limit_bulk' => $fileUploadOrPostLowerLimit,
'photos' => $photos,
@ -646,7 +415,6 @@ class AlbumController extends Controller
$album = new Album();
$album->fill($request->only(['name', 'description', 'storage_id', 'parent_album_id']));
$album->is_permissions_inherited = (strtolower($request->get('is_permissions_inherited')) == 'on');
if (strlen($album->parent_album_id) == 0)
{
@ -660,52 +428,22 @@ class AlbumController extends Controller
$album->generateUrlPath();
$album->save();
// Link the default permissions (if a public album)
// Link all default permissions to anonymous users (if a public album)
$isPrivate = (strtolower($request->get('is_private')) == 'on');
if (!$album->is_permissions_inherited && !$isPrivate)
if (!$isPrivate)
{
$defaultAlbumUserPermissions = AlbumDefaultUserPermission::all();
$defaultAlbumGroupPermissions = AlbumDefaultGroupPermission::all();
$defaultAnonPermissions = AlbumDefaultAnonymousPermission::all();
/** @var AlbumDefaultAnonymousPermission $permission */
foreach ($defaultAnonPermissions as $permission)
/** @var Permission $permission */
foreach (Permission::where(['section' => 'album', 'is_default' => true])->get() as $permission)
{
$album->anonymousPermissions()->attach($permission->permission_id, [
'created_at' => new \DateTime(),
'updated_at' => new \DateTime()
]);
}
/** @var AlbumDefaultGroupPermission $permission */
foreach ($defaultAlbumGroupPermissions as $permission)
{
$album->groupPermissions()->attach($permission->permission_id, [
'group_id' => $permission->group_id,
'created_at' => new \DateTime(),
'updated_at' => new \DateTime()
]);
}
/** @var AlbumDefaultUserPermission $permission */
foreach ($defaultAlbumUserPermissions as $permission)
{
$album->userPermissions()->attach($permission->permission_id, [
'user_id' => $permission->user_id,
$album->anonymousPermissions()->attach($permission->id, [
'created_at' => new \DateTime(),
'updated_at' => new \DateTime()
]);
}
}
// Add an activity record
$this->createActivityRecord($album, 'album.created');
// Rebuild the permissions cache
$helper = new PermissionsHelper();
$helper->rebuildCache();
return redirect(route('albums.show', ['album' => $album->id]));
return redirect(route('albums.show', ['id' => $album->id]));
}
public function storeRedirect(Requests\StoreAlbumRedirectRequest $request, $id)
@ -720,7 +458,7 @@ class AlbumController extends Controller
$redirect->save();
$request->session()->flash('success', trans('admin.create_redirect_success_message'));
return redirect(route('albums.show', ['album' => $id, 'tab' => 'redirects']));
return redirect(route('albums.show', ['id' => $id, 'tab' => 'redirects']));
}
/**
@ -738,7 +476,6 @@ class AlbumController extends Controller
$currentParentID = $album->parent_album_id;
$album->fill($request->only(['name', 'description', 'parent_album_id']));
$album->is_permissions_inherited = (strtolower($request->get('is_permissions_inherited')) == 'on');
if (strlen($album->parent_album_id) == 0)
{
@ -775,29 +512,9 @@ class AlbumController extends Controller
}
$album->save();
// Rebuild the permissions cache
$helper = new PermissionsHelper();
$helper->rebuildCache();
$request->session()->flash('success', trans('admin.album_saved_successfully', ['name' => $album->name]));
return redirect(route('albums.show', ['album' => $id]));
}
private function createActivityRecord(Album $album, $type, $activityDateTime = null)
{
if (is_null($activityDateTime))
{
$activityDateTime = new \DateTime();
}
$userActivity = new UserActivity();
$userActivity->user_id = $this->getUser()->id;
$userActivity->activity_at = $activityDateTime;
$userActivity->type = $type;
$userActivity->album_id = $album->id;
$userActivity->save();
return redirect(route('albums.show', ['id' => $id]));
}
/**
@ -817,13 +534,4 @@ class AlbumController extends Controller
return $album;
}
private function loadChildAlbums(Album $album)
{
$album->child_albums = DbHelper::getChildAlbums($album);
foreach ($album->child_albums as $childAlbum)
{
$this->loadChildAlbums($childAlbum);
}
}
}

View File

@ -3,25 +3,19 @@
namespace App\Http\Controllers\Admin;
use App\Album;
use App\ExternalService;
use App\Configuration;
use App\Facade\Theme;
use App\Facade\UserConfig;
use App\Group;
use App\Helpers\AnalysisQueueHelper;
use App\Helpers\ConfigHelper;
use App\Helpers\DbHelper;
use App\Helpers\MiscHelper;
use App\Helpers\PermissionsHelper;
use App\Http\Controllers\Controller;
use App\Http\Requests\SaveSettingsRequest;
use App\Label;
use App\Mail\TestMailConfig;
use App\Photo;
use App\PhotoComment;
use App\Services\GiteaService;
use App\Services\PhotoService;
use App\Storage;
use App\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Mail;
@ -29,143 +23,29 @@ use Illuminate\Support\Facades\View;
class DefaultController extends Controller
{
private $passwordSettingKeys;
public function __construct()
{
$this->middleware('auth');
View::share('is_admin', true);
$this->passwordSettingKeys = [
'rabbitmq_password',
'smtp_password',
'facebook_app_secret',
'google_app_secret',
'twitter_app_secret'
];
}
public function about()
{
return Theme::render('admin.about', [
'current_version' => config('app.version'),
'licence_text' => file_get_contents(sprintf('%s/LICENSE', dirname(dirname(dirname(dirname(__DIR__))))))
]);
}
public function aboutLatestRelease()
{
try
{
$giteaService = new GiteaService();
$releaseInfo = $giteaService->checkForLatestRelease();
// Convert the publish date so we can re-format it with the user's settings
$publishDate = \DateTime::createFromFormat('Y-m-d\TH:i:sP', $releaseInfo->published_at);
// HTML-ify the body text
$body = nl2br($releaseInfo->body);
$body = preg_replace('/\*\*(.+)\*\*/', '<b>$1</b>', $body);
// Remove the "v" from the release name
$version = substr($releaseInfo->tag_name, 1);
// Determine if we can upgrade
$canUpgrade = version_compare($version, config('app.version')) > 0;
return response()->json([
'can_upgrade' => $canUpgrade,
'body' => $body,
'name' => $version,
'publish_date' => $publishDate->format(UserConfig::get('date_format')),
'url' => $releaseInfo->html_url
]);
}
catch (\Exception $ex)
{
return response()->json(['error' => $ex->getMessage()]);
}
}
public function metadataUpgrade()
{
$albumIDs = DbHelper::getAlbumIDsForCurrentUser();
$photoMetadata = DB::table('photos')
->whereIn('album_id', $albumIDs)
->select([
'album_id',
DB::raw('MIN(metadata_version) AS min_metadata_version')
])
->groupBy('album_id')
->get();
$resultingAlbumIDs = [];
foreach ($photoMetadata as $metadata)
{
if (isset($metadata->min_metadata_version) && $metadata->min_metadata_version > 0)
{
$resultingAlbumIDs[$metadata->album_id] = $metadata->min_metadata_version;
}
}
// Now load the full album definitions
$albumsQuery = DbHelper::getAlbumsForCurrentUser_NonPaged();
$albumsQuery->whereIn('id', array_keys($resultingAlbumIDs));
$albums = $albumsQuery->paginate(UserConfig::get('items_per_page'));
/** @var Album $album */
foreach ($resultingAlbumIDs as $albumID => $metadataMinVersion)
{
foreach ($albums as $album)
{
if ($album->id == $albumID)
{
$album->min_metadata_version = $metadataMinVersion;
}
}
}
return Theme::render('admin.metadata_upgrade', [
'albums' => $albums,
'current_metadata_version' => PhotoService::METADATA_VERSION
]);
}
public function index()
{
$this->authorizeAccessToAdminPanel();
$albumCount = count(DbHelper::getAlbumIDsForCurrentUser());
$albumCount = DbHelper::getAlbumsForCurrentUser()->count();
$photoCount = Photo::all()->count();
$groupCount = Group::all()->count();
$labelCount = Label::all()->count();
$commentCount = PhotoComment::whereNotNull('approved_at')->count();
$userCount = User::where('is_activated', true)->count();
$minMetadataVersion = Photo::min('metadata_version');
$metadataUpgradeNeeded = $minMetadataVersion > 0 && $minMetadataVersion < PhotoService::METADATA_VERSION;
// Default to a supported function call to get the OS version
$osVersion = sprintf('%s %s', php_uname('s'), php_uname('r'));
// If the exec() function is enabled, we can do a bit better
if (MiscHelper::isExecEnabled())
{
$osVersion = exec('lsb_release -ds 2>/dev/null || cat /etc/*release 2>/dev/null | head -n1 || uname -om');
}
return Theme::render('admin.index', [
'album_count' => $albumCount,
'app_version' => config('app.version'),
'comment_count' => $commentCount,
'group_count' => $groupCount,
'label_count' => $labelCount,
'memory_limit' => ini_get('memory_limit'),
'metadata_upgrade_needed' => $metadataUpgradeNeeded,
'photo_count' => $photoCount,
'php_version' => phpversion(),
'os_version' => $osVersion,
'os_version' => exec('lsb_release -ds 2>/dev/null || cat /etc/*release 2>/dev/null | head -n1 || uname -om'),
'server_name' => gethostname(),
'upload_file_size' => ini_get('upload_max_filesize'),
'upload_max_limit' => ini_get('post_max_size'),
@ -173,104 +53,26 @@ class DefaultController extends Controller
]);
}
public function quickUpload(Request $request)
{
$this->authorizeAccessToAdminPanel('admin:manage-albums');
$returnUrl = $request->headers->get('referer');
if (!MiscHelper::isSafeUrl($returnUrl))
{
$returnUrl = route('home');
}
// Pre-validate the upload before passing to the Photos controller
$files = $request->files->get('photo');
if (!is_array($files) || count($files) == 0)
{
$request->session()->flash('error', trans('admin.quick_upload.no_image_provided'));
return redirect($returnUrl);
}
$albumID = $request->get('album_id');
if (intval($albumID) == 0)
{
$albumName = trim($request->get('album_name'));
if (strlen($albumName) == 0)
{
$request->session()->flash('error', trans('admin.quick_upload.no_album_selected'));
return redirect($returnUrl);
}
$album = new Album();
$album->storage_id = Storage::where('is_default', true)->first()->id;
$album->user_id = Auth::user()->id;
$album->default_view = UserConfig::get('default_album_view');
$album->name = $albumName;
$album->description = '';
$album->is_permissions_inherited = true;
$album->save();
// Rebuild the permissions cache
$helper = new PermissionsHelper();
$helper->rebuildCache();
$request->request->set('album_id', $album->id);
}
/** @var PhotoController $photoController */
$photoController = app(PhotoController::class);
return $photoController->store($request);
}
public function rebuildPermissionsCache()
{
$helper = new PermissionsHelper();
$helper->rebuildCache();
return response()->json(true);
}
public function saveSettings(SaveSettingsRequest $request)
{
$this->authorizeAccessToAdminPanel('admin:configure');
$passwordKeys = [
'smtp_password'
];
$checkboxKeys = [
'albums_menu_parents_only',
'allow_photo_comments',
'allow_photo_comments_anonymous',
'allow_self_registration',
'enable_visitor_hits',
'hotlink_protection',
'moderate_anonymous_users',
'moderate_known_users',
'queue_emails',
'rabbitmq_enabled',
'recaptcha_enabled_registration',
'remove_copyright',
'require_email_verification',
'restrict_original_download',
'smtp_encryption',
'social_facebook_login',
'social_google_login',
'social_twitter_login',
'social_user_feeds',
'social_user_profiles'
];
$updateKeys = [
'albums_menu_number_items',
'analysis_queue_storage_location',
'app_name',
'date_format',
'facebook_external_service_id',
'google_external_service_id',
'photo_comments_allowed_html',
'photo_comments_thread_depth',
'rabbitmq_server',
'rabbitmq_port',
'rabbitmq_username',
'rabbitmq_password',
'rabbitmq_queue',
'rabbitmq_vhost',
'sender_address',
'sender_name',
'smtp_server',
@ -278,7 +80,6 @@ class DefaultController extends Controller
'smtp_username',
'smtp_password',
'theme',
'twitter_external_service_id',
'recaptcha_site_key',
'recaptcha_secret_key',
'analytics_code'
@ -293,24 +94,17 @@ class DefaultController extends Controller
// Bit of a hack when the browser returns an empty password field - meaning the user didn't change it
// - don't touch it!
if (
(
$key == 'smtp_password' &&
strlen($config->value) > 0 &&
strlen($request->request->get($key)) == 0 &&
strlen($request->request->get('smtp_username')) > 0
) || (
$key == 'rabbitmq_password' &&
strlen($config->value) > 0 &&
strlen($request->request->get($key)) == 0 &&
strlen($request->request->get('rabbitmq_username')) > 0
)
$key == 'smtp_password' &&
strlen($config->value) > 0 &&
strlen($request->request->get($key)) == 0 &&
strlen($request->request->get('smtp_username')) > 0
)
{
continue;
}
$config->value = $request->request->get($key);
if (in_array($key, $this->passwordSettingKeys) && strlen($config->value) > 0)
if (in_array($key, $passwordKeys) && strlen($config->value) > 0)
{
$config->value = encrypt($config->value);
}
@ -359,43 +153,13 @@ class DefaultController extends Controller
$dateFormatsLookup[$dateFormat] = date($dateFormat);
}
foreach ($this->passwordSettingKeys as $passwordSettingKey)
{
if (isset($config[$passwordSettingKey]) && !empty($config[$passwordSettingKey]))
{
$config[$passwordSettingKey] = decrypt($config[$passwordSettingKey]);
}
}
$themeNamesLookup = UserConfig::allowedThemeNames();
// Storage sources for the Image Processing tab
$storageSources = AnalysisQueueHelper::getCompatibleStorages();
// External services
$externalServices = ExternalService::all();
$facebookServices = $externalServices->filter(function (ExternalService $item)
{
return $item->service_type == ExternalService::FACEBOOK;
});
$googleServices = $externalServices->filter(function (ExternalService $item)
{
return $item->service_type == ExternalService::GOOGLE;
});
$twitterServices = $externalServices->filter(function (ExternalService $item)
{
return $item->service_type == ExternalService::TWITTER;
});
return Theme::render('admin.settings', [
'config' => $config,
'date_formats' => $dateFormatsLookup,
'facebookServices' => $facebookServices,
'googleServices' => $googleServices,
'storage_sources' => $storageSources,
'success' => $request->session()->get('success'),
'theme_names' => $themeNamesLookup,
'twitterServices' => $twitterServices
'theme_names' => $themeNamesLookup
]);
}

View File

@ -34,7 +34,7 @@ class GroupController extends Controller
public function delete($id)
{
$this->authorizeAccessToAdminPanel('admin:manage-groups');
$this->authorizeAccessToAdminPanel();
$group = Group::where('id', intval($id))->first();
if (is_null($group))

View File

@ -1,125 +0,0 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Facade\Theme;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreLabelRequest;
use App\Label;
use App\Photo;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\View;
use Symfony\Component\HttpFoundation\Request;
class LabelController extends Controller
{
public function __construct()
{
$this->middleware(['auth', 'max_post_size_exceeded']);
View::share('is_admin', true);
}
/**
* Applies a comma-separated string of label IDs and/or new label texts to a photo. This is called from the
* PhotoController - not directly via a route.
* @param Photo $photo Photo to apply the labels to
* @param string $labelString CSV string of label IDs and new labels to create (e.g. "1,2,Florida,nature" would
* link label IDs 1 and 2, and create 2 new labels called Florida and nature.)
*/
public function applyLabelsToPhoto(Photo $photo, $labelString)
{
foreach (explode(',', $labelString) as $labelText)
{
$labelID = intval($labelText);
if (intval($labelID) == 0)
{
// Check if the label already exists
$labelToUse = Label::where('name', $labelText)->first();
if (is_null($labelToUse))
{
// Create new label
$labelToUse = new Label();
$labelToUse->name = $labelText;
$labelToUse->save();
}
$labelID = $labelToUse->id;
}
$photo->labels()->attach(intval($labelID));
}
}
public function delete($id)
{
$this->authorizeAccessToAdminPanel();
$label = $this->loadLabel($id);
return Theme::render('admin.delete_label', ['label' => $label]);
}
/**
* Remove the specified resource from storage.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function destroy(Request $request, $id)
{
$this->authorizeAccessToAdminPanel('admin:manage-labels');
$label = $this->loadLabel($id);
$label->delete();
$request->session()->flash('success', trans('admin.delete_label_success_message', ['name' => $label->name]));
return redirect(route('labels.index'));
}
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
$labels = Label::withCount('photos')->get();
return Theme::render('admin.list_labels', [
'labels' => $labels
]);
}
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(StoreLabelRequest $request)
{
$this->authorizeAccessToAdminPanel('admin:manage-labels');
$label = new Label();
$label->fill($request->only(['name']));
$label->save();
return redirect(route('labels.index'));
}
/**
* @param $id
* @return Album
*/
private function loadLabel($id)
{
$label = Label::where('id', intval($id))->first();
if (is_null($label))
{
App::abort(404);
return null;
}
return $label;
}
}

View File

@ -1,375 +0,0 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Album;
use App\Facade\Theme;
use App\Facade\UserConfig;
use App\Http\Controllers\Controller;
use App\Notifications\PhotoCommentApproved;
use App\Notifications\PhotoCommentApprovedUser;
use App\Notifications\PhotoCommentRepliedTo;
use App\Photo;
use App\PhotoComment;
use App\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\View;
class PhotoCommentController extends Controller
{
public function __construct()
{
$this->middleware('auth');
View::share('is_admin', true);
}
public function applyBulkAction(Request $request)
{
$this->authorizeAccessToAdminPanel('admin:manage-comments');
$commentIDs = $request->get('comment_ids');
if (is_null($commentIDs) || !is_array($commentIDs) || count($commentIDs) == 0)
{
$request->session()->flash('warning', trans('admin.no_comments_selected_message'));
return redirect(route('comments.index'));
}
$comments = PhotoComment::whereIn('id', $commentIDs)->get();
$commentsActioned = 0;
if ($request->has('bulk_delete'))
{
/** @var PhotoComment $comment */
foreach ($comments as $comment)
{
$comment->delete();
$commentsActioned++;
}
$request->session()->flash('success', trans_choice('admin.bulk_comments_deleted', $commentsActioned, ['number' => $commentsActioned]));
}
else if ($request->has('bulk_approve'))
{
/** @var PhotoComment $comment */
foreach ($comments as $comment)
{
if ($comment->isApproved())
{
// Don't make changes if already approved
continue;
}
// Mark as approved
$comment->approved_at = new \DateTime();
$comment->approved_user_id = $this->getUser()->id;
// The comment may have already been rejected - remove the data if so
$comment->rejected_at = null;
$comment->rejected_user_id = null;
// Send the notification e-mail to the owner
$comment->save();
$commentsActioned++;
// Send e-mail notification
$photo = $comment->photo;
$album = $photo->album;
$this->notifyAlbumOwnerAndPoster($album, $photo, $comment);
}
$request->session()->flash('success', trans_choice('admin.bulk_comments_approved', $commentsActioned, ['number' => $commentsActioned]));
}
else if ($request->has('bulk_reject'))
{
/** @var PhotoComment $comment */
foreach ($comments as $comment)
{
if ($comment->isRejected())
{
// Don't make changes if already rejected
continue;
}
// Mark as rejected
$comment->rejected_at = new \DateTime();
$comment->rejected_user_id = $this->getUser()->id;
// The comment may have already been approved - remove the data if so
$comment->approved_at = null;
$comment->approved_user_id = null;
$comment->save();
$commentsActioned++;
}
$request->session()->flash('success', trans_choice('admin.bulk_comments_approved', $commentsActioned, ['number' => $commentsActioned]));
}
return redirect(route('comments.index'));
}
public function approve($id)
{
$this->authorizeAccessToAdminPanel('admin:manage-comments');
$comment = $this->loadCommentByID($id);
return Theme::render('admin.approve_comment', ['comment' => $comment]);
}
public function bulkAction(Request $request)
{
$this->authorizeAccessToAdminPanel('admin:manage-comments');
$commentIDs = $request->get('comment_ids');
if (is_null($commentIDs) || !is_array($commentIDs) || count($commentIDs) == 0)
{
$request->session()->flash('warning', trans('admin.no_comments_selected_message'));
return redirect(route('comments.index'));
}
if ($request->has('bulk_delete'))
{
if (count($commentIDs) == 1)
{
// Single comment selected - redirect to the single delete page
return redirect(route('comments.delete', ['comment' => $commentIDs[0]]));
}
// Show the view to confirm the delete
return Theme::render('admin.bulk_delete_comments', [
'comment_count' => count($commentIDs),
'comment_ids' => $commentIDs
]);
}
else if ($request->has('bulk_approve'))
{
if (count($commentIDs) == 1)
{
// Single comment selected - redirect to the single approve page
return redirect(route('comments.approve', ['comment' => $commentIDs[0]]));
}
// Show the view to confirm the approval
return Theme::render('admin.bulk_approve_comments', [
'comment_count' => count($commentIDs),
'comment_ids' => $commentIDs
]);
}
else if ($request->has('bulk_reject'))
{
if (count($commentIDs) == 1)
{
// Single comment selected - redirect to the single reject page
return redirect(route('comments.reject', ['comment' => $commentIDs[0]]));
}
// Show the view to confirm the rejection
return Theme::render('admin.bulk_reject_comments', [
'comment_count' => count($commentIDs),
'comment_ids' => $commentIDs
]);
}
// Unrecognised action - simply redirect back to the index page
return redirect(route('comments.index'));
}
public function confirmApprove(Request $request, $id)
{
$this->authorizeAccessToAdminPanel('admin:manage-comments');
$comment = $this->loadCommentByID($id);
if ($comment->isApproved())
{
// Comment has already been approved
return redirect(route('comments.index'));
}
// Mark as approved
$comment->approved_at = new \DateTime();
$comment->approved_user_id = $this->getUser()->id;
// The comment may have already been rejected - remove the data if so
$comment->rejected_at = null;
$comment->rejected_user_id = null;
$comment->save();
$request->session()->flash('success', trans('admin.comment_approval_successful', [
'author_name' => $comment->authorDisplayName()
]));
// Send e-mail notification
$photo = $comment->photo;
$album = $photo->album;
$this->notifyAlbumOwnerAndPoster($album, $photo, $comment);
return redirect(route('comments.index'));
}
public function confirmReject(Request $request, $id)
{
$this->authorizeAccessToAdminPanel('admin:manage-comments');
$comment = $this->loadCommentByID($id);
if ($comment->isRejected())
{
// Comment has already been rejected
return redirect(route('comments.index'));
}
// Mark as rejected
$comment->rejected_at = new \DateTime();
$comment->rejected_user_id = $this->getUser()->id;
// The comment may have already been approved - remove the data if so
$comment->approved_at = null;
$comment->approved_user_id = null;
$comment->save();
$request->session()->flash('success', trans('admin.comment_rejection_successful', [
'author_name' => $comment->authorDisplayName()
]));
return redirect(route('comments.index'));
}
public function delete($id)
{
$this->authorizeAccessToAdminPanel('admin:manage-comments');
$comment = $this->loadCommentByID($id);
return Theme::render('admin.delete_comment', ['comment' => $comment]);
}
public function destroy(Request $request, $id)
{
$this->authorizeAccessToAdminPanel('admin:manage-comments');
/** @var PhotoComment $comment */
$comment = $this->loadCommentByID($id);
$comment->delete();
$request->session()->flash('success', trans('admin.comment_deletion_successful', [
'author_name' => $comment->authorDisplayName()
]));
return redirect(route('comments.index'));
}
public function index(Request $request)
{
$this->authorizeAccessToAdminPanel('admin:manage-comments');
$validStatusList = [
'all',
'pending',
'approved',
'rejected'
];
$filterStatus = $request->get('status', 'all');
if (!in_array($filterStatus, $validStatusList))
{
$filterStatus = $validStatusList[0];
}
$comments = PhotoComment::with('photo')
->with('photo.album')
->orderBy('created_at', 'desc');
switch (strtolower($filterStatus))
{
case 'approved':
$comments->whereNotNull('approved_at')
->whereNull('rejected_at');
break;
case 'pending':
$comments->whereNull('approved_at')
->whereNull('rejected_at');
break;
case 'rejected':
$comments->whereNull('approved_at')
->whereNotNull('rejected_at');
break;
}
return Theme::render('admin.list_comments', [
'comments' => $comments->paginate(UserConfig::get('items_per_page')),
'filter_status' => $filterStatus,
'success' => $request->session()->get('success'),
'warning' => $request->session()->get('warning')
]);
}
public function reject($id)
{
$this->authorizeAccessToAdminPanel('admin:manage-comments');
$comment = $this->loadCommentByID($id);
return Theme::render('admin.reject_comment', ['comment' => $comment]);
}
/**
* Loads a given comment by its ID.
* @param $id
* @return PhotoComment
*/
private function loadCommentByID($id)
{
$comment = PhotoComment::where('id', intval($id))->first();
if (is_null($comment))
{
App::abort(404);
}
return $comment;
}
/**
* Sends an e-mail notification to an album's owned that a comment has been posted/approved.
* @param Album $album
* @param Photo $photo
* @param PhotoComment $comment
*/
private function notifyAlbumOwnerAndPoster(Album $album, Photo $photo, PhotoComment $comment)
{
/** @var User $owner */
$owner = $album->user;
$owner->notify(new PhotoCommentApproved($album, $photo, $comment));
// Also send a notification to the comment poster
$poster = new User();
$poster->name = $comment->authorDisplayName();
$poster->email = $comment->authorEmail();
$poster->notify(new PhotoCommentApprovedUser($album, $photo, $comment));
// Send notification to the parent comment owner (if this is a reply)
if (!is_null($comment->parent_comment_id))
{
$parentComment = $this->loadCommentByID($comment->parent_comment_id);
if (is_null($parentComment))
{
return;
}
$parentPoster = new User();
$parentPoster->name = $parentComment->authorDisplayName();
$parentPoster->email = $parentComment->authorEmail();
$parentPoster->notify(new PhotoCommentRepliedTo($album, $photo, $comment));
}
}
}

View File

@ -3,25 +3,25 @@
namespace App\Http\Controllers\Admin;
use App\Album;
use App\Facade\UserConfig;
use App\Helpers\AnalysisQueueHelper;
use App\AlbumSources\IAlbumSource;
use App\Facade\Image;
use App\Facade\Theme;
use App\Helpers\FileHelper;
use App\Helpers\ImageHelper;
use App\Helpers\MiscHelper;
use App\Http\Controllers\Controller;
use App\Http\Requests\UpdatePhotosBulkRequest;
use App\Photo;
use App\QueueItem;
use App\Services\PhotoService;
use App\Services\RabbitMQService;
use App\Upload;
use App\UploadPhoto;
use App\UserActivity;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\View;
use Symfony\Component\Finder\Iterator\RecursiveDirectoryIterator;
use Symfony\Component\HttpFoundation\File\File;
class PhotoController extends Controller
@ -48,76 +48,17 @@ class PhotoController extends Controller
try
{
if (UserConfig::isImageProcessingQueueEnabled())
{
// Find the last record that is analysing this photo
$photoQueueItem = QueueItem::where('photo_id', $photo->id)
->orderBy('queued_at', 'desc')
->limit(1)
->first();
$photoService = new PhotoService($photo);
$photoService->analyse($queue_token);
$timeToWait = 60;
$timeWaited = 0;
$continueToMonitor = true;
while ($continueToMonitor && $timeWaited < $timeToWait)
{
$continueToMonitor = is_null($photoQueueItem->completed_at);
if ($continueToMonitor)
{
sleep(1);
$timeWaited++;
$photoQueueItem = QueueItem::where('id', $photoQueueItem->id)->first();
$continueToMonitor = is_null($photoQueueItem->completed_at);
}
}
$didComplete = !is_null($photoQueueItem->completed_at);
if (!$didComplete)
{
$result['message'] = 'Timed out waiting for queue processing.';
}
else if (!$photoQueueItem->is_successful)
{
$result['is_successful'] = false;
$result['message'] = $photoQueueItem->error_message;
// Remove the photo from the album if it was newly-uploaded and couldn't be processed
if (intval($photo->metadata_version) === 0)
{
$photo->delete();
}
}
else
{
$result['is_successful'] = true;
}
}
else
{
/* IF CHANGING THIS LOGIC, ALSO CHECK ProcessQueueCommand::processPhotoAnalyseMessage */
$photoService = new PhotoService($photo);
$photoService->analyse($queue_token);
// Log an activity record for the user's feed (remove an existing one as the date may have changed)
$this->removeExistingActivityRecords($photo, 'photo.taken');
if (!is_null($photo->taken_at))
{
// Log an activity record for the user's feed
$this->createActivityRecord($photo, 'photo.taken', $photo->taken_at);
}
$result['is_successful'] = true;
}
$result['is_successful'] = true;
}
catch (\Exception $ex)
{
$result['is_successful'] = false;
$result['message'] = $ex->getMessage();
// Remove the photo if it cannot be analysed (only if there isn't currently a version of metadata)
// Remove the photo if it cannot be analysed
$photo->delete();
}
@ -162,7 +103,7 @@ class PhotoController extends Controller
$request->session()->flash('success', trans('admin.delete_photo_successful_message', ['name' => $photo->name]));
}
public function flip(Request $request, $photoId, $horizontal, $vertical)
public function flip($photoId, $horizontal, $vertical)
{
$this->authorizeAccessToAdminPanel();
@ -173,11 +114,6 @@ class PhotoController extends Controller
$photoService = new PhotoService($photo);
$photoService->flip($horizontal, $vertical);
// Log an activity record for the user's feed
$this->createActivityRecord($photo, 'photo.edited');
return $photo->thumbnailUrl($request->get('t', 'admin-preview'));
}
public function move(Request $request, $photoId)
@ -207,49 +143,13 @@ class PhotoController extends Controller
}
}
public function reAnalyse($id, $queue_token)
{
$this->authorizeAccessToAdminPanel();
/** @var Photo $photo */
$photo = $this->loadPhoto($id);
$result = ['is_successful' => false, 'message' => ''];
try
{
$photoService = new PhotoService($photo);
$photoService->downloadOriginalToFolder(FileHelper::getQueuePath($queue_token));
$photoService->analyse($queue_token);
// Log an activity record for the user's feed (remove an existing one as the date may have changed)
$this->removeExistingActivityRecords($photo, 'photo.taken');
if (!is_null($photo->taken_at))
{
// Log an activity record for the user's feed
$this->createActivityRecord($photo, 'photo.taken', $photo->taken_at);
}
$result['is_successful'] = true;
}
catch (\Exception $ex)
{
$result['is_successful'] = false;
$result['message'] = $ex->getMessage();
// Unlike the analyse method, we don't remove the photo if it cannot be analysed
}
return response()->json($result);
}
public function regenerateThumbnails(Request $request, $photoId)
public function regenerateThumbnails($photoId)
{
$this->authorizeAccessToAdminPanel();
$photo = $this->loadPhoto($photoId, 'change-metadata');
$result = ['is_successful' => false, 'message' => '', 'thumbnail_url' => ''];
$result = ['is_successful' => false, 'message' => ''];
try
{
@ -257,7 +157,6 @@ class PhotoController extends Controller
$photoService->regenerateThumbnails();
$result['is_successful'] = true;
$result['thumbnail_url'] = $photo->thumbnailUrl($request->get('t', 'admin-preview'));
}
catch (\Exception $ex)
{
@ -268,7 +167,7 @@ class PhotoController extends Controller
return response()->json($result);
}
public function rotate(Request $request, $photoId, $angle)
public function rotate($photoId, $angle)
{
$this->authorizeAccessToAdminPanel();
@ -276,17 +175,12 @@ class PhotoController extends Controller
if ($angle != 90 && $angle != 180 && $angle != 270)
{
App::abort(400);
App::aport(400);
return null;
}
$photoService = new PhotoService($photo);
$photoService->rotate($angle);
// Log an activity record for the user's feed
$this->createActivityRecord($photo, 'photo.edited');
return $photo->thumbnailUrl($request->get('t', 'admin-preview'));
}
/**
@ -312,7 +206,7 @@ class PhotoController extends Controller
throw new \Exception('No queue_token value was provided!');
}
$queueStorage = AnalysisQueueHelper::getStorageQueueSource();
$queueFolder = FileHelper::getQueuePath($queueUid);
foreach ($photoFiles as $photoFile)
{
@ -323,70 +217,21 @@ class PhotoController extends Controller
}
else
{
try
{
if ($request->has('photo_id'))
{
// Photo ID provided (using the Replace Photo function) - use that record
$photo = Photo::where('id', intval($request->get('photo_id')))->first();
$photo->raw_exif_data = null;
/** @var File $savedFile */
$savedFile = FileHelper::saveUploadedFile($photoFile, $queueFolder);
$queuedFileName = $queueStorage->uploadToAnalysisQueue($photoFile, $queueUid, $photo->storage_file_name);
$uploadedTempFile = new File($photoFile);
$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();
$photo->mime_type = $savedFile->getMimeType();
$photo->file_size = $savedFile->getSize();
$photo->is_analysed = false;
$photo->save();
$this->removeExistingActivityRecords($photo, 'photo.uploaded');
$this->removeExistingActivityRecords($photo, 'photo.taken');
$photo->file_name = $photoFile->getClientOriginalName();
$photo->mime_type = $uploadedTempFile->getMimeType();
$photo->file_size = $uploadedTempFile->getSize();
$photo->storage_file_name = basename($queuedFileName);
}
else
{
$queuedFileName = $queueStorage->uploadToAnalysisQueue($photoFile, $queueUid);
$uploadedTempFile = new File($photoFile);
$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->mime_type = $uploadedTempFile->getMimeType();
$photo->file_size = $uploadedTempFile->getSize();
$photo->storage_file_name = basename($queuedFileName);
}
$photo->is_analysed = false;
$photo->save();
// Log an activity record for the user's feed
$this->createActivityRecord($photo, 'photo.uploaded');
// If queueing is enabled, store the photo in the queue now
if (UserConfig::isImageProcessingQueueEnabled())
{
$queueItem = new QueueItem([
'batch_reference' => $queueUid,
'action_type' => 'photo.analyse',
'album_id' => $photo->album_id,
'photo_id' => $photo->id,
'user_id' => $this->getUser()->id,
'queued_at' => new \DateTime()
]);
$queueItem->save();
$rabbitmq = new RabbitMQService();
$rabbitmq->queueItem($queueItem);
}
$isSuccessful = true;
}
finally
{
@unlink($photoFile->getRealPath());
}
$isSuccessful = true;
}
}
@ -397,7 +242,7 @@ class PhotoController extends Controller
else
{
return redirect(route('albums.analyse', [
'album' => $album->id,
'id' => $album->id,
'queue_token' => $queueUid
]));
}
@ -410,18 +255,12 @@ class PhotoController extends Controller
// Load the linked album
$album = $this->loadAlbum($request->get('album_id'));
if (is_null($request->files->get('archive')))
{
$request->session()->flash('error', trans('admin.upload_bulk_no_file'));
return redirect(route('albums.show', ['album' => $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', ['album' => $album->id]));
return redirect(route('albums.show', ['id' => $album->id]));
}
// Create the folder to hold the analysis results if not already present
@ -431,104 +270,74 @@ class PhotoController extends Controller
throw new \Exception('No queue_token value was provided!');
}
$temporaryFolder = sprintf('%s/%s', sys_get_temp_dir(), MiscHelper::randomString());
@mkdir($temporaryFolder);
$queueFolder = FileHelper::getQueuePath($queueUid);
try
$mimeType = strtolower($archiveFile->getMimeType());
switch ($mimeType)
{
$queueStorage = AnalysisQueueHelper::getStorageQueueSource();
case 'application/zip':
$zip = new \ZipArchive();
$zip->open($archiveFile->getPathname());
$zip->extractTo($queueFolder);
$zip->close();
@unlink($archiveFile->getPathname());
break;
$mimeType = strtolower($archiveFile->getMimeType());
switch ($mimeType)
{
case 'application/zip':
$zip = new \ZipArchive();
$zip->open($archiveFile->getPathname());
$zip->extractTo($temporaryFolder);
$zip->close();
@unlink($archiveFile->getPathname());
break;
default:
$request->session()->flash('error', sprintf('The file type "%s" is not supported for bulk uploads.', $mimeType));
return redirect(route('albums.show', ['album' => $album->id]));
}
$di = new \RecursiveDirectoryIterator($temporaryFolder, \RecursiveDirectoryIterator::SKIP_DOTS);
$recursive = new \RecursiveIteratorIterator($di);
/** @var \SplFileInfo $fileInfo */
foreach ($recursive as $fileInfo)
{
if ($fileInfo->isDir())
{
if ($fileInfo->getFilename() == '__MACOSX' || substr($fileInfo->getFilename(), 0, 1) == '.')
{
@rmdir($fileInfo->getRealPath());
}
continue;
}
if (substr($fileInfo->getFilename(), 0, 1) == '.')
{
// Temporary/hidden file - skip
@unlink($fileInfo->getRealPath());
continue;
}
$result = getimagesize($fileInfo->getRealPath());
if ($result === false)
{
// Not an image file - skip
@unlink($fileInfo->getRealPath());
continue;
}
$photoFile = new File($fileInfo->getRealPath());
$queuedFileName = $queueStorage->uploadToAnalysisQueue($photoFile, $queueUid);
$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->mime_type = $photoFile->getMimeType();
$photo->file_size = $photoFile->getSize();
$photo->is_analysed = false;
$photo->storage_file_name = basename($queuedFileName);
$photo->save();
// Log an activity record for the user's feed
$this->createActivityRecord($photo, 'photo.uploaded');
// If queueing is enabled, store the photo in the queue now
if (UserConfig::isImageProcessingQueueEnabled())
{
$queueItem = new QueueItem([
'batch_reference' => $queueUid,
'action_type' => 'photo.analyse',
'album_id' => $photo->album_id,
'photo_id' => $photo->id,
'user_id' => $this->getUser()->id,
'queued_at' => new \DateTime()
]);
$queueItem->save();
$rabbitmq = new RabbitMQService();
$rabbitmq->queueItem($queueItem);
}
@unlink($fileInfo->getRealPath());
}
default:
$request->session()->flash('error', sprintf('The file type "%s" is not supported for bulk uploads.', $mimeType));
return redirect(route('albums.show', ['id' => $album->id]));
}
finally
$di = new \RecursiveDirectoryIterator($queueFolder, \RecursiveDirectoryIterator::SKIP_DOTS);
$recursive = new \RecursiveIteratorIterator($di);
/** @var \SplFileInfo $fileInfo */
foreach ($recursive as $fileInfo)
{
@rmdir($temporaryFolder);
if ($fileInfo->isDir())
{
if ($fileInfo->getFilename() == '__MACOSX' || substr($fileInfo->getFilename(), 0, 1) == '.')
{
@rmdir($fileInfo->getPathname());
}
continue;
}
if (substr($fileInfo->getFilename(), 0, 1) == '.')
{
// Temporary/hidden file - skip
@unlink($fileInfo->getPathname());
continue;
}
$result = getimagesize($fileInfo->getPathname());
if ($result === false)
{
// Not an image file - skip
@unlink($fileInfo->getPathname());
continue;
}
$photoFile = new File($fileInfo->getPathname());
/** @var File $savedFile */
$savedFile = FileHelper::saveExtractedFile($photoFile, $queueFolder);
$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();
$photo->mime_type = $savedFile->getMimeType();
$photo->file_size = $savedFile->getSize();
$photo->is_analysed = false;
$photo->save();
}
return redirect(route('albums.analyse', [
'album' => $album->id,
'id' => $album->id,
'queue_token' => $queueUid
]));
}
@ -582,16 +391,9 @@ class PhotoController extends Controller
$numberChanged = $this->updatePhotoDetails($request, $album);
}
$request->session()->flash(
'success',
trans_choice(
UserConfig::isImageProcessingQueueEnabled() ? 'admin.bulk_photos_changed_queued' : 'admin.bulk_photos_changed',
$numberChanged,
['number' => $numberChanged]
)
);
$request->session()->flash('success', trans_choice('admin.bulk_photos_changed', $numberChanged, ['number' => $numberChanged]));
return redirect(route('albums.show', array('album' => $albumId, 'page' => $request->get('page', 1))));
return redirect(route('albums.show', array('id' => $albumId, 'page' => $request->get('page', 1))));
}
private function applyBulkActions(Request $request, Album $album)
@ -631,161 +433,101 @@ class PhotoController extends Controller
$action = $request->get('bulk-action');
$numberChanged = 0;
if (UserConfig::isImageProcessingQueueEnabled())
foreach ($photosToProcess as $photo)
{
$queueUid = MiscHelper::randomString();
foreach ($photosToProcess as $photo)
$changed = false;
$photoService = new PhotoService($photo);
$doNotSave = false;
switch (strtolower($action))
{
$queueItem = new QueueItem([
'batch_reference' => $queueUid,
'action_type' => sprintf('photo.bulk_action.%s', strtolower($action)),
'album_id' => $photo->album_id,
'photo_id' => $photo->id,
'user_id' => $this->getUser()->id,
'queued_at' => new \DateTime()
]);
if (strtolower($action) == 'change_album')
{
$queueItem->new_album_id = intval($request->get('new-album-id'));
$newAlbumId = intval($request->get('new-album-id'));
if ($newAlbumId == $photo->album_id)
case 'change_album':
if (Auth::user()->can('change-metadata', $photo))
{
// Photo already belongs to this album, don't move
continue;
$newAlbumId = intval($request->get('new-album-id'));
if ($newAlbumId == $photo->album_id)
{
// Photo already belongs to this album, don't move
continue;
}
$newAlbum = $this->loadAlbum($newAlbumId, 'upload-photos');
$photoService->changeAlbum($newAlbum);
$changed = true;
}
}
break;
$queueItem->save();
case 'delete':
if (Auth::user()->can('delete', $photo))
{
$photoService->delete();
$doNotSave = true;
$changed = true;
}
break;
$rabbitmq = new RabbitMQService();
$rabbitmq->queueItem($queueItem);
case 'flip_both':
if (Auth::user()->can('manipulate', $photo))
{
$photoService->flip(true, true);
$changed = true;
}
break;
$numberChanged++;
case 'flip_horizontal':
if (Auth::user()->can('manipulate', $photo))
{
$photoService->flip(true, false);
$changed = true;
}
break;
case 'flip_vertical':
if (Auth::user()->can('manipulate', $photo))
{
$photoService->flip(false, true);
$changed = true;
}
break;
case 'refresh_thumbnails':
if (Auth::user()->can('change-metadata', $photo))
{
$photoService->regenerateThumbnails();
$changed = true;
}
break;
case 'rotate_left':
if (Auth::user()->can('manipulate', $photo))
{
$photoService->rotate(90);
$changed = true;
}
break;
case 'rotate_right':
if (Auth::user()->can('manipulate', $photo))
{
$photoService->rotate(270);
$changed = true;
}
break;
}
}
else
{
foreach ($photosToProcess as $photo)
if (!$doNotSave)
{
$changed = false;
$photoService = new PhotoService($photo);
$doNotSave = false;
$photo->save();
}
/* IF CHANGING THIS LOGIC OR ADDING EXTRA case OPTIONS, ALSO CHECK ProcessQueueCommand::processQueueItem AND ProcessQueueCommand::processPhotoBulkActionMessage */
switch (strtolower($action))
{
case 'change_album':
if (Auth::user()->can('change-metadata', $photo))
{
$newAlbumId = intval($request->get('new-album-id'));
if ($newAlbumId == $photo->album_id)
{
// Photo already belongs to this album, don't move
continue 2;
}
$newAlbum = $this->loadAlbum($newAlbumId, 'upload-photos');
$photoService->changeAlbum($newAlbum);
$changed = true;
}
break;
case 'delete':
if (Auth::user()->can('delete', $photo))
{
$photoService->delete();
$doNotSave = true;
$changed = true;
}
break;
case 'flip_both':
if (Auth::user()->can('manipulate', $photo))
{
$photoService->flip(true, true);
$changed = true;
}
break;
case 'flip_horizontal':
if (Auth::user()->can('manipulate', $photo))
{
$photoService->flip(true, false);
$changed = true;
}
break;
case 'flip_vertical':
if (Auth::user()->can('manipulate', $photo))
{
$photoService->flip(false, true);
$changed = true;
}
break;
case 'refresh_thumbnails':
if (Auth::user()->can('change-metadata', $photo))
{
$photoService->regenerateThumbnails();
$changed = true;
}
break;
case 'rotate_left':
if (Auth::user()->can('manipulate', $photo))
{
$photoService->rotate(90);
$changed = true;
}
break;
case 'rotate_right':
if (Auth::user()->can('manipulate', $photo))
{
$photoService->rotate(270);
$changed = true;
}
break;
}
if (!$doNotSave)
{
$photo->save();
}
if (!in_array(strtolower($action), ['delete', 'refresh_thumbnails', 'change_album']))
{
// Log an activity record for the user's feed
$this->createActivityRecord($photo, 'photo.edited');
}
if ($changed)
{
$numberChanged++;
}
if ($changed)
{
$numberChanged++;
}
}
return $numberChanged;
}
private function createActivityRecord(Photo $photo, $type, $activityDateTime = null)
{
if (is_null($activityDateTime))
{
$activityDateTime = new \DateTime();
}
$userActivity = new UserActivity();
$userActivity->user_id = $this->getUser()->id;
$userActivity->activity_at = $activityDateTime;
$userActivity->type = $type;
$userActivity->photo_id = $photo->id;
$userActivity->save();
}
/**
* @param $id
* @return Album
@ -826,20 +568,6 @@ class PhotoController extends Controller
return $photo;
}
private function removeExistingActivityRecords(Photo $photo, $type)
{
$existingFeedRecords = UserActivity::where([
'user_id' => $this->getUser()->id,
'photo_id' => $photo->id,
'type' => $type
])->get();
foreach ($existingFeedRecords as $existingFeedRecord)
{
$existingFeedRecord->delete();
}
}
private function updatePhotoDetails(Request $request, Album $album)
{
$numberChanged = 0;
@ -855,17 +583,6 @@ class PhotoController extends Controller
}
$photo->fill($value);
// Update the photo labels
$labelString = trim($value['labels']);
$photo->labels()->detach();
if (strlen($labelString) > 0)
{
app(LabelController::class)->applyLabelsToPhoto($photo, $labelString);
}
// Save all changes
$photo->save();
$numberChanged++;
}

View File

@ -1,355 +0,0 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Configuration;
use App\ExternalService;
use App\Facade\Theme;
use App\Facade\UserConfig;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreServiceRequest;
use App\Services\DropboxService;
use App\Storage;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\View;
class ServiceController extends Controller
{
/**
* List of fields that must be encrypted before being saved.
*
* @var string[]
*/
private $fieldsToEncrypt;
/**
* List of fields that depend on the service_type being configured.
*
* @var string[]
*/
private $serviceTypeDependentFields;
public function __construct()
{
$this->middleware('auth');
View::share('is_admin', true);
$this->serviceTypeDependentFields = ['app_id', 'app_secret'];
$this->fieldsToEncrypt = ['app_id', 'app_secret'];
}
public function authoriseDropbox(Request $request)
{
$this->authorizeAccessToAdminPanel('admin:manage-storage');
if (!$request->has('state') && !$request->has('code'))
{
// TODO flash an error
return redirect('storages.index');
}
try
{
$storageID = decrypt($request->get('state'));
$storage = Storage::where('id', intval($storageID))->first();
if (is_null($storage))
{
// TODO flash an error
return redirect('storages.index');
}
if (is_null($storage->externalService))
{
// TODO flash an error
return redirect('storages.index');
}
switch ($storage->externalService->service_type)
{
case ExternalService::DROPBOX:
$dropbox = new DropboxService();
$dropbox->handleAuthenticationResponse($request, $storage);
// TODO flash a success message
return redirect(route('storage.index'));
default:
// TODO flash an error
return redirect('storages.index');
}
}
catch (\Exception $ex)
{
// TODO flash an error
return redirect('storages.index');
}
}
/**
* Show the form for creating a new resource.
*
* @return \Illuminate\Http\Response
*/
public function create(Request $request)
{
$this->authorizeAccessToAdminPanel('admin:manage-services');
$serviceTypes = $this->serviceTypeList();
$selectedServiceType = old('service_type', $request->get('service_type'));
if (!array_key_exists($selectedServiceType, $serviceTypes))
{
$selectedServiceType = '';
}
$returnTo = old('return_to', $request->get('return_to'));
if (!array_key_exists($returnTo, $this->validReturnLocations()))
{
$returnTo = '';
}
return Theme::render('admin.create_service', [
'callbackUrls' => $this->callbackList(),
'returnTo' => $returnTo,
'selectedServiceType' => $selectedServiceType,
'service' => new ExternalService(),
'serviceTypes' => $serviceTypes
]);
}
public function delete(Request $request, $id)
{
$this->authorizeAccessToAdminPanel('admin:manage-users');
$service = ExternalService::where('id', intval($id))->first();
if (is_null($service))
{
App::abort(404);
}
if ($this->isServiceInUse($service))
{
$request->session()->flash('warning', trans('admin.cannot_delete_service_in_use'));
return redirect(route('services.index'));
}
return Theme::render('admin.delete_service', ['service' => $service]);
}
/**
* Remove the specified resource from storage.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function destroy(Request $request, $id)
{
$this->authorizeAccessToAdminPanel('admin:manage-services');
$service = ExternalService::where('id', intval($id))->first();
if (is_null($service))
{
App::abort(404);
}
if ($this->isServiceInUse($service))
{
$request->session()->flash('warning', trans('admin.cannot_delete_service_in_use'));
return redirect(route('services.index'));
}
try
{
$service->delete();
$request->session()->flash('success', trans('admin.service_deletion_successful', [
'name' => $service->name
]));
}
catch (\Exception $ex)
{
$request->session()->flash('error', trans('admin.service_deletion_failed', [
'error_message' => $ex->getMessage(),
'name' => $service->name
]));
}
return redirect(route('services.index'));
}
/**
* Show the form for editing the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function edit(Request $request, $id)
{
$this->authorizeAccessToAdminPanel('admin:manage-services');
$service = ExternalService::where('id', intval($id))->first();
if (is_null($service))
{
App::abort(404);
}
// Decrypt the fields that are stored as encrypted in the DB
foreach ($this->fieldsToEncrypt as $field)
{
if (!empty($service->$field))
{
$service->$field = decrypt($service->$field);
}
}
return Theme::render('admin.edit_service', [
'callbackUrls' => $this->callbackList(),
'service' => $service,
'serviceTypes' => $this->serviceTypeList()
]);
}
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index(Request $request)
{
$this->authorizeAccessToAdminPanel('admin:manage-services');
$services = ExternalService::orderBy('name')
->paginate(UserConfig::get('items_per_page'));
return Theme::render('admin.list_services', [
'error' => $request->session()->get('error'),
'services' => $services,
'success' => $request->session()->get('success'),
'warning' => $request->session()->get('warning')
]);
}
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(StoreServiceRequest $request)
{
$this->authorizeAccessToAdminPanel('admin:manage-services');
$service = new ExternalService($request->only(['name', 'service_type']));
foreach ($this->serviceTypeDependentFields as $field)
{
if ($request->has($field))
{
$service->$field = in_array($field, $this->fieldsToEncrypt)
? encrypt($request->get($field))
: $request->get($field);
}
}
$service->save();
$returnToLocations = $this->validReturnLocations();
$returnTo = $request->get('return_to');
if (array_key_exists($returnTo, $returnToLocations))
{
return redirect($returnToLocations[$returnTo]);
}
return redirect(route('services.index'));
}
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param int $id
* @return \Illuminate\Http\Response
*/
public function update(StoreServiceRequest $request, $id)
{
$this->authorizeAccessToAdminPanel('admin:manage-services');
$service = ExternalService::where('id', intval($id))->first();
if (is_null($service))
{
App::abort(404);
}
$service->fill($request->only(['name', 'service_type']));
foreach ($this->serviceTypeDependentFields as $field)
{
if ($request->has($field))
{
$service->$field = in_array($field, $this->fieldsToEncrypt)
? encrypt($request->get($field))
: $request->get($field);
}
}
$service->save();
return redirect(route('services.index'));
}
private function callbackList()
{
$dropboxService = new DropboxService();
return [
ExternalService::DROPBOX => $dropboxService->callbackUrl(),
ExternalService::FACEBOOK => route('login_callback.facebook'),
ExternalService::GOOGLE => route('login_callback.google'),
ExternalService::TWITTER => route('login_callback.twitter')
];
}
private function isServiceInUse(ExternalService $service)
{
switch ($service->service_type)
{
case ExternalService::FACEBOOK:
// Cannot delete Facebook service if it's set as the login provider
$facebookConfig = Configuration::where('key', 'facebook_external_service_id')->first();
return !is_null($facebookConfig) && intval($facebookConfig->value) == $service->id;
case ExternalService::GOOGLE:
// Cannot delete Google service if it's set as the login provider
$googleConfig = Configuration::where('key', 'google_external_service_id')->first();
return !is_null($googleConfig) && intval($googleConfig->value) == $service->id;
case ExternalService::DROPBOX:
return Storage::where('external_service_id', $service->id)->count() > 0;
case ExternalService::TWITTER:
// Cannot delete Twitter service if it's set as the login provider
$twitterConfig = Configuration::where('key', 'twitter_external_service_id')->first();
return !is_null($twitterConfig) && intval($twitterConfig->value) == $service->id;
}
return true;
}
private function serviceTypeList()
{
return [
ExternalService::DROPBOX => trans(sprintf('services.%s', ExternalService::DROPBOX)),
ExternalService::FACEBOOK => trans(sprintf('services.%s', ExternalService::FACEBOOK)),
ExternalService::GOOGLE => trans(sprintf('services.%s', ExternalService::GOOGLE)),
ExternalService::TWITTER => trans(sprintf('services.%s', ExternalService::TWITTER))
];
}
private function validReturnLocations()
{
return [
'settings' => route('admin.settings')
];
}
}

View File

@ -1,24 +0,0 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Configuration;
use App\Facade\UserConfig;
use App\Http\Controllers\Controller;
use Symfony\Component\HttpFoundation\Request;
class StatisticsController extends Controller
{
public function save(Request $request)
{
$isPublicStatsEnabled = strtolower($request->get('enable_public_statistics')) == 'on';
/** @var Configuration $config */
$config = UserConfig::getOrCreateModel('public_statistics');
$config->value = $isPublicStatsEnabled;
$config->save();
$request->session()->flash('success', trans('admin.statistics_prefs_saved_message'));
return redirect(route('statistics.index'));
}
}

View File

@ -2,14 +2,13 @@
namespace App\Http\Controllers\Admin;
use App\ExternalService;
use App\Facade\Theme;
use App\Facade\UserConfig;
use App\Http\Controllers\Controller;
use App\Http\Requests;
use App\Services\DropboxService;
use App\Storage;
use Illuminate\Http\Request;
use App\Http\Requests;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\View;
@ -25,39 +24,7 @@ class StorageController extends Controller
$this->middleware('auth');
View::share('is_admin', true);
$this->encryptedFields = ['password', 'access_key', 'secret_key', 'access_token'];
}
public function authoriseService(Request $request, $id)
{
$this->authorizeAccessToAdminPanel('admin:manage-storage');
$storage = Storage::where('id', intval($id))->first();
if (is_null($storage))
{
return redirect(route('storages.index'));
}
$externalServiceType = $this->getExternalServiceType($storage);
if (is_null($externalServiceType))
{
$request->session()->flash('error', trans('admin.storage_no_external_service_support'));
return redirect(route('storages.index'));
}
$serviceTypeName = trans(sprintf('services.%s', $externalServiceType));
switch ($externalServiceType)
{
case ExternalService::DROPBOX:
$dropbox = new DropboxService();
return redirect($dropbox->authoriseUrl($storage));
default:
$request->session()->flash('error', trans('admin.storage_external_service_no_authorisation', ['service_name' => $serviceTypeName]));
return redirect(route('storages.index'));
}
$this->encryptedFields = ['password', 'access_key', 'secret_key'];
}
/**
@ -89,15 +56,11 @@ class StorageController extends Controller
$this->authorizeAccessToAdminPanel('admin:manage-storage');
$filesystemDefaultLocation = sprintf('%s/storage/app/albums', dirname(dirname(dirname(dirname(__DIR__)))));
$storage = new Storage();
$storage->s3_signed_urls = true;
return Theme::render('admin.create_storage', [
'album_sources' => UserConfig::albumSources(),
'dropbox_services' => ExternalService::getForService(ExternalService::DROPBOX),
'filesystem_default_location' => $filesystemDefaultLocation,
'info' => $request->session()->get('info'),
'storage' => $storage
'info' => $request->session()->get('info')
]);
}
@ -125,14 +88,11 @@ class StorageController extends Controller
'container_name',
'cdn_url',
'access_key',
'secret_key',
'b2_bucket_type',
'external_service_id'
'secret_key'
]));
$storage->is_active = true;
$storage->is_default = (strtolower($request->get('is_default')) == 'on');
$storage->is_internal = false;
$storage->s3_signed_urls = (strtolower($request->get('s3_signed_urls')) == 'on');
if ($storage->source != 'LocalFilesystemSource' && isset($storage->location))
{
@ -154,17 +114,6 @@ class StorageController extends Controller
$this->unsetIsDefaultFromOthers($storage);
}
$externalServiceType = $this->getExternalServiceType($storage);
if (!is_null($externalServiceType))
{
switch ($externalServiceType)
{
case ExternalService::DROPBOX:
return redirect(route('storage.authoriseService', ['storage' => $storage->id]));
}
}
return redirect(route('storage.index'));
}
@ -237,10 +186,12 @@ class StorageController extends Controller
}
}
return Theme::render('admin.edit_storage', [
'dropbox_services' => ExternalService::getForService(ExternalService::DROPBOX),
'storage' => $storage
]);
if (!$request->session()->has('_old_input'))
{
$request->session()->flash('_old_input', $storage->toArray());
}
return Theme::render('admin.edit_storage', ['storage' => $storage]);
}
/**
@ -271,13 +222,10 @@ class StorageController extends Controller
'container_name',
'cdn_url',
'access_key',
'secret_key',
'b2_bucket_type',
'external_service_id'
'secret_key'
]));
$storage->is_active = (strtolower($request->get('is_active')) == 'on');
$storage->is_default = (strtolower($request->get('is_default')) == 'on');
$storage->s3_signed_urls = (strtolower($request->get('s3_signed_urls')) == 'on');
if ($storage->is_default && !$storage->is_active)
{
@ -298,10 +246,6 @@ class StorageController extends Controller
{
$this->unsetIsDefaultFromOthers($storage);
}
else
{
$this->setIsDefaultForFirstStorage();
}
return redirect(route('storage.index'));
}
@ -341,32 +285,6 @@ class StorageController extends Controller
return redirect(route('storage.index'));
}
private function getExternalServiceType(Storage $storage)
{
if (!is_null($storage->externalService))
{
return $storage->externalService->service_type;
}
return null;
}
private function setIsDefaultForFirstStorage()
{
$count = Storage::where('is_default', true)->count();
if ($count == 0)
{
$storage = Storage::where('is_active', true)->first();
if (!is_null($storage))
{
$storage->is_default = true;
$storage->save();
}
}
}
private function unsetIsDefaultFromOthers(Storage $storage)
{
// If this storage is flagged as default, remove all others

View File

@ -5,10 +5,10 @@ namespace App\Http\Controllers\Admin;
use App\Facade\Theme;
use App\Facade\UserConfig;
use App\Group;
use App\Helpers\PermissionsHelper;
use App\Http\Controllers\Controller;
use App\Http\Requests;
use App\User;
use App\Http\Requests;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Auth;
@ -88,7 +88,6 @@ class UserController extends Controller
$user->password = bcrypt($user->password);
$user->is_activated = true;
$user->is_admin = (strtolower($request->get('is_admin')) == 'on');
$user->enable_profile_page = UserConfig::get('social_user_profiles');
$user->save();
return redirect(route('users.index'));
@ -201,10 +200,6 @@ class UserController extends Controller
$user->save();
// Rebuild the permissions cache
$helper = new PermissionsHelper();
$helper->rebuildCache();
return redirect(route('users.index'));
}

View File

@ -3,7 +3,6 @@
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Traits\ActivatesUsers;
use App\User;
use Illuminate\Foundation\Auth\RedirectsUsers;
use Illuminate\Http\Request;
@ -11,7 +10,7 @@ use Illuminate\Support\Facades\App;
class ActivateController extends Controller
{
use RedirectsUsers, ActivatesUsers;
use RedirectsUsers;
/**
* Where to redirect users after activation.
@ -47,9 +46,6 @@ class ActivateController extends Controller
$request->session()->flash('info', trans('auth.account_activated_message'));
$this->logActivatedActivity($user);
$this->sendUserActivatedEmails($user);
return redirect($this->redirectPath());
}
}

View File

@ -2,20 +2,10 @@
namespace App\Http\Controllers\Auth;
use App\ExternalService;
use App\Facade\Theme;
use App\Facade\UserConfig;
use App\Helpers\MiscHelper;
use App\Http\Controllers\Controller;
use App\User;
use Illuminate\Contracts\Routing\UrlGenerator;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Request;
use Laravel\Socialite\One\TwitterProvider;
use Laravel\Socialite\Two\FacebookProvider;
use Laravel\Socialite\Two\GoogleProvider;
use League\OAuth1\Client\Server\Twitter as TwitterServer;
use Socialite;
class LoginController extends Controller
{
@ -32,61 +22,21 @@ class LoginController extends Controller
use AuthenticatesUsers;
/**
* @var UrlGenerator
*/
protected $generator;
/**
* Where to redirect users after login / registration.
*
* @var string
*/
protected $redirectTo = '/me';
protected $redirectTo = '/';
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct(UrlGenerator $generator)
public function __construct()
{
$this->middleware('guest', ['except' => 'logout']);
$this->generator = $generator;
}
public function logout(Request $request)
{
$this->guard()->logout();
$request->session()->invalidate();
return redirect()->back();
}
protected function attemptLogin(Request $request)
{
$isSuccessful = $this->guard()->attempt($this->credentials($request));
if ($isSuccessful)
{
/** @var User $user */
$user = $this->guard()->user();
// Update the social media ID if successful login and it was referred by the SSO provider
$loginData = $request->getSession()->get('ssoLoginData');
if (!is_null($loginData))
{
unset($loginData['name']);
unset($loginData['email']);
$user->fill($loginData);
$user->save();
$request->getSession()->remove('ssoLoginData');
}
}
return $isSuccessful;
}
protected function credentials(Request $request)
@ -106,285 +56,9 @@ class LoginController extends Controller
*/
public function showLoginForm(Request $request)
{
$previousUrl = MiscHelper::ensureHasTrailingSlash($this->generator->previous(false));
$homeUrl = MiscHelper::ensureHasTrailingSlash(route('home'));
if (UserConfig::get('social_user_feeds') && (empty($previousUrl) || $previousUrl == $homeUrl))
{
$previousUrl = route('userActivityFeed');
}
$request->getSession()->put('url.intended', $previousUrl);
return Theme::render('auth.v2_unified', [
'active_tab' => 'login',
'info' => $request->session()->get('info'),
'is_sso' => false
'info' => $request->session()->get('info')
]);
}
/**
* Show the application's login form (for a social media-linked account).
*
* @return \Illuminate\Http\Response
*/
public function showLoginFormSso(Request $request)
{
// Social media login info
$loginData = $request->getSession()->get('ssoLoginData');
if (is_null($loginData))
{
// No SSO data in session, use the normal login screen
return redirect(route('login'));
}
return Theme::render('auth.v2_unified', [
'active_tab' => 'login',
'info' => $request->session()->get('info'),
'is_sso' => true,
'login_data' => $loginData
]);
}
/**
* Redirect the user to the Facebook authentication page.
*
* @return \Illuminate\Http\Response
*/
public function redirectToFacebook()
{
$socialite = $this->setSocialiteConfigForFacebook();
if (is_null($socialite))
{
return redirect(route('login'));
}
return $socialite->driver('facebook')->redirect();
}
/**
* Redirect the user to the Google authentication page.
*
* @return \Illuminate\Http\Response
*/
public function redirectToGoogle()
{
$socialite = $this->setSocialiteConfigForGoogle();
if (is_null($socialite))
{
return redirect(route('login'));
}
return $socialite->driver('google')->redirect();
}
/**
* Redirect the user to the Twitter authentication page.
*
* @return \Illuminate\Http\Response
*/
public function redirectToTwitter()
{
$socialite = $this->setSocialiteConfigForTwitter();
if (is_null($socialite))
{
return redirect(route('login'));
}
return $socialite->driver('twitter')->redirect();
}
/**
* Obtain the user information from Facebook.
*
* @return \Illuminate\Http\Response
*/
public function handleFacebookCallback(Request $request)
{
$socialite = $this->setSocialiteConfigForFacebook();
if (is_null($socialite))
{
return redirect(route('login'));
}
$facebookUser = $socialite->driver('facebook')->user();
return $this->processSocialMediaLogin($request, 'facebook_id', $facebookUser);
}
/**
* Obtain the user information from Google.
*
* @return \Illuminate\Http\Response
*/
public function handleGoogleCallback(Request $request)
{
$socialite = $this->setSocialiteConfigForGoogle();
if (is_null($socialite))
{
return redirect(route('login'));
}
$googleUser = $socialite->driver('google')->user();
return $this->processSocialMediaLogin($request, 'google_id', $googleUser);
}
/**
* Obtain the user information from Twitter.
*
* @return \Illuminate\Http\Response
*/
public function handleTwitterCallback(Request $request)
{
$socialite = $this->setSocialiteConfigForTwitter();
if (is_null($socialite))
{
return redirect(route('login'));
}
$twitterUser = $socialite->driver('twitter')->user();
return $this->processSocialMediaLogin($request, 'twitter_id', $twitterUser);
}
private function getSocialMediaConfig($socialMediaEnabledField, $socialMediaExternalServiceIdField)
{
if (boolval(UserConfig::get($socialMediaEnabledField)))
{
$externalServiceID = intval(UserConfig::get($socialMediaExternalServiceIdField));
$externalService = ExternalService::where('id', $externalServiceID)->first();
return $externalService;
}
return null;
}
private function processSocialMediaLogin(Request $request, $socialMediaIdField, $socialMediaUser)
{
$userBySocialMediaId = User::where($socialMediaIdField, $socialMediaUser->getId())->first();
if (!is_null($userBySocialMediaId))
{
// We have an existing user for this Facebook ID - log them in
$this->guard()->login($userBySocialMediaId);
return redirect(route('home'));
}
// Some providers (*cough*Twitter*cough*) don't give e-mail addresses without explicit permission/additional
// verification
if (!is_null($socialMediaUser->email))
{
$userByEmailAddress = User::where('email', $socialMediaUser->getEmail())->first();
if (!is_null($userByEmailAddress))
{
// We have an existing user with the e-mail address associated with the Facebook account
// Prompt for the password for that account
$request->getSession()->put('ssoLoginData', [
'name' => $socialMediaUser->getName(),
'email' => $socialMediaUser->getEmail(),
$socialMediaIdField => $socialMediaUser->getId(),
'is_activated' => true
]);
return redirect(route('auth.login_sso'));
}
}
// We don't have an existing user - prompt for registration
$request->getSession()->put('ssoRegisterData', [
'name' => $socialMediaUser->getName(),
'email' => $socialMediaUser->getEmail(),
$socialMediaIdField => $socialMediaUser->getId(),
'is_activated' => true
]);
return redirect(route('auth.register_sso'));
}
private function setSocialiteConfigForFacebook()
{
$facebookConfig = $this->getSocialMediaConfig(
'social_facebook_login',
'facebook_external_service_id'
);
if (is_null($facebookConfig))
{
return null;
}
$socialite = app()->make(\Laravel\Socialite\Contracts\Factory::class);
$socialite->extend(
'facebook',
function ($app) use ($socialite, $facebookConfig) {
$config = [
'client_id' => trim(decrypt($facebookConfig->app_id)),
'client_secret' => trim(decrypt($facebookConfig->app_secret)),
'redirect' => route('login_callback.facebook')
];
return $socialite->buildProvider(FacebookProvider::class, $config);
}
);
return $socialite;
}
private function setSocialiteConfigForGoogle()
{
$googleConfig = $this->getSocialMediaConfig(
'social_google_login',
'google_external_service_id'
);
if (is_null($googleConfig))
{
return null;
}
$socialite = app()->make(\Laravel\Socialite\Contracts\Factory::class);
$socialite->extend(
'google',
function ($app) use ($socialite, $googleConfig) {
$config = [
'client_id' => trim(decrypt($googleConfig->app_id)),
'client_secret' => trim(decrypt($googleConfig->app_secret)),
'redirect' => route('login_callback.google')
];
return $socialite->buildProvider(GoogleProvider::class, $config);
}
);
return $socialite;
}
private function setSocialiteConfigForTwitter()
{
$twitterConfig = $this->getSocialMediaConfig(
'social_twitter_login',
'twitter_external_service_id'
);
if (is_null($twitterConfig))
{
return null;
}
$socialite = app()->make(\Laravel\Socialite\Contracts\Factory::class);
$socialite->extend(
'twitter',
function ($app) use ($socialite, $twitterConfig) {
$config = [
'identifier' => trim(decrypt($twitterConfig->app_id)),
'secret' => trim(decrypt($twitterConfig->app_secret)),
'callback_uri' => route('login_callback.twitter')
];
return new TwitterProvider($app['request'], new TwitterServer($config));
}
);
return $socialite;
}
}

View File

@ -6,12 +6,12 @@ use App\Facade\Theme;
use App\Facade\UserConfig;
use App\Helpers\MiscHelper;
use App\Helpers\RecaptchaHelper;
use App\Http\Controllers\Controller;
use App\Notifications\UserActivationRequired;
use App\Traits\ActivatesUsers;
use App\Mail\UserActivationRequired;
use App\User;
use Illuminate\Foundation\Auth\RegistersUsers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Mail;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\RegistersUsers;
use Illuminate\Support\Facades\Validator;
class RegisterController extends Controller
@ -27,7 +27,7 @@ class RegisterController extends Controller
|
*/
use RegistersUsers, ActivatesUsers;
use RegistersUsers;
/**
* Where to redirect users after login / registration.
@ -85,23 +85,25 @@ class RegisterController extends Controller
*/
protected function create(array $data)
{
if (!isset($data['is_activated']))
{
$data['is_activated'] = true;
$activationData = [
'is_activated' => true
];
if (UserConfig::get('require_email_verification'))
{
$data['is_activated'] = false;
$data['activation_token'] = MiscHelper::randomString();
}
if (UserConfig::get('require_email_verification'))
{
$activationData['is_activated'] = false;
$activationData['activation_token'] = MiscHelper::randomString();
}
$data['password'] = bcrypt($data['password']);
$data['is_admin'] = false;
$data['enable_profile_page'] = UserConfig::get('social_user_profiles');
unset($data['password_confirmation']);
return User::create($data);
return User::create(array_merge(
[
'name' => $data['name'],
'email' => $data['email'],
'password' => bcrypt($data['password']),
'is_admin' => false
],
$activationData
));
}
public function register(Request $request)
@ -113,29 +115,17 @@ class RegisterController extends Controller
$this->validator($request)->validate();
$userData = $request->all();
// Social media login info
$registerData = $request->getSession()->get('ssoRegisterData');
if (!is_null($registerData))
{
$userData = array_merge($registerData, $userData);
$request->getSession()->remove('ssoRegisterData');
}
/** @var User $user */
$user = $this->create($userData);
$user = $this->create($request->all());
if ($user->is_activated)
{
$this->logActivatedActivity($user);
$this->sendUserActivatedEmails($user);
$this->guard()->login($user);
}
else
{
// Send activation e-mail
$user->notify(new UserActivationRequired());
Mail::to($user)->send(new UserActivationRequired($user));
$request->session()->flash('info', trans('auth.activation_required_message'));
}
@ -147,7 +137,7 @@ class RegisterController extends Controller
*
* @return \Illuminate\Http\Response
*/
public function showRegistrationForm(Request $request)
public function showRegistrationForm()
{
if (!UserConfig::get('allow_self_registration'))
{
@ -155,35 +145,7 @@ class RegisterController extends Controller
}
return Theme::render('auth.v2_unified', [
'active_tab' => 'register',
'is_sso' => false
]);
}
/**
* Show the application registration form (for a social media-linked account).
*
* @return \Illuminate\Http\Response
*/
public function showRegistrationFormSso(Request $request)
{
if (!UserConfig::get('allow_self_registration'))
{
return redirect(route('home'));
}
// Social media login info
$registerData = $request->getSession()->get('ssoRegisterData');
if (is_null($registerData))
{
// No SSO data in session, use the normal registration screen
return redirect(route('register'));
}
return Theme::render('auth.v2_unified', [
'active_tab' => 'register',
'is_sso' => true,
'register_data' => $registerData
'active_tab' => 'register'
]);
}
}

View File

@ -2,11 +2,14 @@
namespace App\Http\Controllers\Gallery;
use App\Album;
use App\AlbumRedirect;
use App\Facade\Theme;
use App\Facade\UserConfig;
use App\Helpers\ConfigHelper;
use App\Helpers\DbHelper;
use App\Http\Controllers\Controller;
use App\Http\Requests;
use App\VisitorHit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
@ -31,7 +34,7 @@ class AlbumController extends Controller
}
$album = DbHelper::getAlbumById($redirect->album_id);
return redirect($album->url(), 301);
return redirect($album->url());
}
$this->authorizeForUser($this->getUser(), 'view', $album);
@ -40,7 +43,7 @@ class AlbumController extends Controller
$requestedView = strtolower($request->get('view'));
if (!in_array($requestedView, $validViews))
{
$requestedView = strtolower($album->default_view);
$requestedView = $album->default_view;
if (!in_array($requestedView, $validViews))
{
@ -68,28 +71,20 @@ class AlbumController extends Controller
else if ($requestedView != 'slideshow')
{
$photos = $album->photos()
->orderBy(DB::raw('COALESCE(taken_at, created_at), name, id'))
->orderBy(DB::raw('COALESCE(taken_at, created_at)'))
->paginate(UserConfig::get('items_per_page'));
}
else
{
// The slideshow view needs access to all photos, not paged
$photos = $album->photos()
->orderBy(DB::raw('COALESCE(taken_at, created_at), name, id'))
->orderBy(DB::raw('COALESCE(taken_at, created_at)'))
->get();
}
// Load child albums and their available children
$childAlbums = DbHelper::getChildAlbums($album);
foreach ($childAlbums as $childAlbum)
{
$childAlbum->children_count = DbHelper::getChildAlbumsCount($childAlbum);
}
return Theme::render(sprintf('gallery.album_%s', $requestedView), [
'album' => $album,
'allowed_views' => $validViews,
'child_albums' => $childAlbums,
'current_view' => $requestedView,
'photos' => $photos
]);

View File

@ -7,7 +7,6 @@ use App\Facade\Theme;
use App\Facade\UserConfig;
use App\Helpers\DbHelper;
use App\Http\Controllers\Controller;
use App\Label;
use App\Photo;
use App\VisitorHit;
use Illuminate\Http\Request;
@ -19,13 +18,6 @@ class DefaultController extends Controller
public function index(Request $request)
{
$albums = DbHelper::getAlbumsForCurrentUser(0);
/** @var Album $album */
foreach ($albums as $album)
{
$album->children_count = DbHelper::getChildAlbumsCount($album);
}
$resetStatus = $request->session()->get('status');
// Record the visit to the index (no album or photo to record a hit against though)
@ -62,18 +54,6 @@ class DefaultController extends Controller
// Albums the current user is allowed to access
$albumIDs = DbHelper::getAlbumIDsForCurrentUser();
// Add each label
$labels = Label::orderBy('name');
$labels->chunk(100, function($labelsChunk) use ($xml, $root)
{
/** @var Label $label */
foreach ($labelsChunk as $label)
{
$lastModifiedPhoto = $label->photos()->orderBy('updated_at', 'desc')->first();
$this->createSitemapNode($xml, $root, $label->url(), (is_null($lastModifiedPhoto) ? $label->updated_at : $lastModifiedPhoto->updated_at), '0.9');
}
});
// Add each album URL
$albums = Album::whereIn('id', $albumIDs)->orderBy('name');
$albums->chunk(100, function($albumsChunk) use ($xml, $root)
@ -82,7 +62,7 @@ class DefaultController extends Controller
foreach ($albumsChunk as $album)
{
$lastModifiedPhoto = Photo::where('album_id', $album->id)->orderBy('updated_at', 'desc')->first();
$this->createSitemapNode($xml, $root, $album->url(), (is_null($lastModifiedPhoto) ? $album->updated_at : $lastModifiedPhoto->updated_at), '0.8');
$this->createSitemapNode($xml, $root, $album->url(), (is_null($lastModifiedPhoto) ? $album->updated_at : $lastModifiedPhoto->updated_at), '0.9');
}
});
@ -104,7 +84,7 @@ class DefaultController extends Controller
$root,
$photo->url(),
$photo->updated_at,
'0.7',
'0.8',
$photo->thumbnailUrl('fullsize', false),
join(' - ', $photoMeta)
);

View File

@ -1,41 +0,0 @@
<?php
namespace App\Http\Controllers\Gallery;
use App\Facade\Theme;
use App\Facade\UserConfig;
use App\Http\Controllers\Controller;
use App\User;
use App\UserFollower;
class ExploreController extends Controller
{
public function users()
{
if (!UserConfig::get('social_user_profiles'))
{
return redirect(route('home'));
}
$users = User::where([
'is_activated' => true,
'enable_profile_page' => true
])
->orderBy('name')
->paginate(UserConfig::get('items_per_page'));
$usersFollowing = UserFollower::where('user_id', $this->getUser()->id)
->select('following_user_id')
->get()
->map(function($f)
{
return $f->following_user_id;
})
->toArray();
return Theme::render('gallery.explore_users', [
'users' => $users,
'users_following' => $usersFollowing
]);
}
}

View File

@ -1,79 +0,0 @@
<?php
namespace App\Http\Controllers\Gallery;
use App\Facade\Theme;
use App\Facade\UserConfig;
use App\Helpers\DbHelper;
use App\Http\Controllers\Controller;
use App\Label;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\DB;
use Symfony\Component\HttpFoundation\Request;
class LabelController extends Controller
{
public function index(Request $request)
{
$labels = Label::orderBy('name')->get();
/** @var Label $label */
foreach ($labels as $label)
{
$label->photos_count = $label->photoCount();
}
return Theme::render('gallery.labels', ['labels' => $labels]);
}
public function show(Request $request, $labelAlias)
{
$label = Label::where('url_alias', $labelAlias)->first();
if (is_null($label))
{
App::abort(404);
}
$validViews = UserConfig::allowedAlbumViews();
$requestedView = strtolower($request->get('view'));
if (!in_array($requestedView, $validViews))
{
$requestedView = $validViews[0];
}
$allowedAlbumIDs = DbHelper::getAlbumIDsForCurrentUser();
if ($label->photos()->count() == 0)
{
$requestedView = 'empty';
$photos = [];
}
else if ($requestedView != 'slideshow')
{
$photos = $label->photos()
->whereIn('album_id', $allowedAlbumIDs)
->orderBy(DB::raw('COALESCE(photos.taken_at, photos.created_at)'))
->paginate(UserConfig::get('items_per_page'));
}
else
{
// The slideshow view needs access to all photos, not paged
$photos = $label->photos()
->whereIn('album_id', $allowedAlbumIDs)
->orderBy(DB::raw('COALESCE(photos.taken_at, photos.created_at)'))
->get();
}
if (count($photos) == 0)
{
$requestedView = 'empty';
}
return Theme::render(sprintf('gallery.label_%s', $requestedView), [
'allowed_views' => $validViews,
'current_view' => $requestedView,
'label' => $label,
'photos' => $photos
]);
}
}

View File

@ -1,411 +0,0 @@
<?php
namespace App\Http\Controllers\Gallery;
use App\Album;
use App\Facade\Theme;
use App\Facade\UserConfig;
use App\Helpers\DbHelper;
use App\Helpers\PermissionsHelper;
use App\Http\Controllers\Controller;
use App\Notifications\ModeratePhotoComment;
use App\Notifications\PhotoCommentApproved;
use App\Notifications\PhotoCommentApprovedUser;
use App\Notifications\PhotoCommentRepliedTo;
use App\Photo;
use App\PhotoComment;
use App\User;
use App\UserActivity;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
use Illuminate\Validation\ValidationException;
class PhotoCommentController extends Controller
{
public function moderate(Request $request, $albumUrlAlias, $photoFilename, $commentID)
{
$album = null;
/** @var Photo $photo */
$photo = null;
/** @var PhotoComment $comment */
$comment = null;
if (!$this->loadAlbumPhotoComment($albumUrlAlias, $photoFilename, $commentID, $album, $photo, $comment))
{
return null;
}
if (!User::currentOrAnonymous()->can('moderate-comments', $photo))
{
App::abort(403);
return null;
}
if (!$comment->isModerated())
{
if ($request->has('approve'))
{
$comment->approved_at = new \DateTime();
$comment->approved_user_id = $this->getUser()->id;
$comment->save();
$this->createUserActivityRecord($comment);
$this->notifyAlbumOwnerAndPoster($album, $photo, $comment);
$request->getSession()->flash('success', trans('gallery.photo_comment_approved_successfully'));
}
else if ($request->has('reject'))
{
$comment->rejected_at = new \DateTime();
$comment->rejected_user_id = $this->getUser()->id;
$comment->save();
$request->getSession()->flash('success', trans('gallery.photo_comment_rejected_successfully'));
}
}
return redirect($photo->url());
}
public function reply(Request $request, $albumUrlAlias, $photoFilename, $commentID)
{
$album = null;
/** @var Photo $photo */
$photo = null;
/** @var PhotoComment $comment */
$comment = null;
if (!$this->loadAlbumPhotoComment($albumUrlAlias, $photoFilename, $commentID, $album, $photo, $comment))
{
return null;
}
if (!User::currentOrAnonymous()->can('post-comment', $photo))
{
App::abort(403);
return null;
}
return Theme::render('partials.photo_comments_reply_form', [
'photo' => $photo,
'reply_comment' => $comment
]);
}
public function store(Request $request, $albumUrlAlias, $photoFilename)
{
$album = null;
/** @var Photo $photo */
$photo = null;
/** @var PhotoComment $comment */
$comment = null;
if (!$this->loadAlbumPhotoComment($albumUrlAlias, $photoFilename, 0, $album, $photo, $comment))
{
return null;
}
if (!User::currentOrAnonymous()->can('post-comment', $photo))
{
App::abort(403);
return null;
}
// Validate and link the parent comment, if provided
// We do this here so if the validation fails, we still have the parent comment available in the catch block
$parentComment = null;
if ($request->has('parent_comment_id'))
{
$parentComment = $photo->comments()->where('id', intval($request->get('parent_comment_id')))->first();
if (is_null($parentComment))
{
return redirect($photo->url());
}
}
try
{
$this->validate($request, [
'name' => 'required|max:255',
'email' => 'sometimes|max:255|email',
'comment' => 'required'
]);
$commentText = $this->stripDisallowedHtmlTags($request->get('comment'));
$comment = new PhotoComment();
$comment->photo_id = $photo->id;
$comment->fill($request->only(['name', 'email']));
$comment->comment = $commentText;
if (!is_null($parentComment))
{
$comment->parent_comment_id = $parentComment->id;
}
// Set the created user ID if we're logged in
$user = $this->getUser();
if (!is_null($user) && !$user->isAnonymous())
{
$comment->created_user_id = $user->id;
}
// Auto-approve the comment if we're allowed to moderate comments
$isAutoApproved = false;
if (User::currentOrAnonymous()->can('moderate-comments', $photo))
{
$comment->approved_at = new \DateTime();
$comment->approved_user_id = $user->id;
$isAutoApproved = true;
}
// Auto-approve the comment if settings allow
if ($user->isAnonymous() && !UserConfig::get('moderate_anonymous_users'))
{
$comment->approved_at = new \DateTime();
$comment->approved_user_id = null; // we don't have a user ID to set!
$isAutoApproved = true;
}
else if (!$user->isAnonymous() && !UserConfig::get('moderate_known_users'))
{
$comment->approved_at = new \DateTime();
$comment->approved_user_id = $user->id;
$isAutoApproved = true;
}
$comment->save();
// Send notification e-mails to moderators or album owner
if (!$isAutoApproved)
{
$this->notifyAlbumModerators($album, $photo, $comment);
$request->getSession()->flash('success', trans('gallery.photo_comment_posted_successfully_pending_moderation'));
}
else
{
// Log an activity record for the user's feed
$this->createUserActivityRecord($comment);
$this->notifyAlbumOwnerAndPoster($album, $photo, $comment);
$request->getSession()->flash('success', trans('gallery.photo_comment_posted_successfully'));
}
if ($request->isXmlHttpRequest())
{
return response()->json(['redirect_url' => $photo->url()]);
}
else
{
return redirect($photo->url());
}
}
catch (ValidationException $e)
{
if (!is_null($parentComment))
{
return redirect()
->to($photo->replyToCommentFormUrl($parentComment->id))
->withErrors($e->errors())
->withInput($request->all());
}
else
{
return redirect()
->back()
->withErrors($e->errors())
->withInput($request->all());
}
}
}
private function createUserActivityRecord(PhotoComment $comment)
{
if (!is_null($comment->created_user_id))
{
$userActivity = new UserActivity();
$userActivity->user_id = $comment->created_user_id;
$userActivity->activity_at = $comment->created_at;
if (is_null($comment->parent_comment_id))
{
$userActivity->type = 'photo.commented';
}
else
{
$userActivity->type = 'photo.comment_replied';
}
$userActivity->photo_id = $comment->photo_id;
$userActivity->photo_comment_id = $comment->id;
$userActivity->save();
}
}
private function loadAlbumPhotoComment($albumUrlAlias, $photoFilename, $commentID, &$album, &$photo, &$comment)
{
$album = DbHelper::getAlbumByPath($albumUrlAlias);
if (is_null($album))
{
App::abort(404);
return false;
}
$this->authorizeForUser($this->getUser(), 'view', $album);
$photo = PhotoController::loadPhotoByAlbumAndFilename($album, $photoFilename);
if (!UserConfig::get('allow_photo_comments'))
{
// Not allowed to post comments
App::abort(404);
return false;
}
if (intval($commentID > 0))
{
$comment = $photo->comments()->where('id', $commentID)->first();
if (is_null($comment))
{
App::abort(404);
return false;
}
}
return true;
}
/**
* Loads a given comment by its ID.
* @param $id
* @return PhotoComment
*/
private function loadCommentByID($id)
{
$comment = PhotoComment::where('id', intval($id))->first();
if (is_null($comment))
{
App::abort(404);
}
return $comment;
}
/**
* Sends an e-mail notification to an album's moderators that a comment is available to moderate.
* @param Album $album
* @param Photo $photo
* @param PhotoComment $comment
*/
private function notifyAlbumModerators(Album $album, Photo $photo, PhotoComment $comment)
{
// Get all users from the cache
$helper = new PermissionsHelper();
$moderators = $helper->usersWhoCan_Album($album, 'moderate-comments');
/** @var User $moderator */
foreach ($moderators as $moderator)
{
$moderator->notify(new ModeratePhotoComment($album, $photo, $comment));
}
}
/**
* Sends an e-mail notification to an album's owned that a comment has been posted/approved.
* @param Album $album
* @param Photo $photo
* @param PhotoComment $comment
*/
private function notifyAlbumOwnerAndPoster(Album $album, Photo $photo, PhotoComment $comment)
{
/** @var User $owner */
$owner = $album->user;
$owner->notify(new PhotoCommentApproved($album, $photo, $comment));
// Also send a notification to the comment poster
$poster = new User();
$poster->name = $comment->authorDisplayName();
$poster->email = $comment->authorEmail();
$poster->notify(new PhotoCommentApprovedUser($album, $photo, $comment));
// Send notification to the parent comment owner (if this is a reply)
if (!is_null($comment->parent_comment_id))
{
$parentComment = $this->loadCommentByID($comment->parent_comment_id);
if (is_null($parentComment))
{
return;
}
$parentPoster = new User();
$parentPoster->name = $parentComment->authorDisplayName();
$parentPoster->email = $parentComment->authorEmail();
$parentPoster->notify(new PhotoCommentRepliedTo($album, $photo, $comment));
}
}
private function stripDisallowedHtmlTags($commentText)
{
$allowedHtmlTags = explode(',', UserConfig::get('photo_comments_allowed_html'));
$allowedHtmlTagsCleaned = [];
foreach ($allowedHtmlTags as $tag)
{
$allowedHtmlTagsCleaned[] = trim($tag);
}
// Match any starting HTML tags
$regexMatchString = '/<(?!\/)([a-z]+)(?:\s.*)*>/Us';
$htmlTagMatches = [];
preg_match_all($regexMatchString, $commentText, $htmlTagMatches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER);
for ($index = 0; $index < count($htmlTagMatches); $index++)
{
$htmlTagMatch = $htmlTagMatches[$index];
$htmlTag = $htmlTagMatch[1][0]; // e.g. "p" for <p>
if (in_array($htmlTag, $allowedHtmlTagsCleaned))
{
// This tag is allowed - carry on
continue;
}
/* This tag is not allowed - remove it from the string */
// Find the closing tag
$disallowedStringOffset = $htmlTagMatch[0][1];
$endingTagMatches = [];
preg_match(sprintf('/(<%1$s.*>)(.+)<\/%1$s>/Us', $htmlTag), $commentText, $endingTagMatches, 0, $disallowedStringOffset);
// Replace the matched string with the inner string
$commentText = substr_replace($commentText, $endingTagMatches[2], $disallowedStringOffset, strlen($endingTagMatches[0]));
// Adjust the offsets for strings after the one we're processing, so the offsets match up with the string correctly
for ($index2 = $index + 1; $index2 < count($htmlTagMatches); $index2++)
{
// If this string appears entirely BEFORE the next one starts, we need to subtract the entire length.
// Otherwise, we only need to substract the length of the start tag, as the next one starts within it.
$differenceAfterReplacement = strlen($endingTagMatches[1]);
if ($htmlTagMatch[0][1] + strlen($endingTagMatches[0]) < $htmlTagMatches[$index2][0][1])
{
$differenceAfterReplacement = strlen($endingTagMatches[0]) - strlen($endingTagMatches[2]);
}
$htmlTagMatches[$index2][0][1] -= $differenceAfterReplacement;
$htmlTagMatches[$index2][1][1] -= $differenceAfterReplacement;
}
}
return $commentText;
}
}

View File

@ -6,15 +6,17 @@ use App\Album;
use App\Facade\Theme;
use App\Facade\UserConfig;
use App\Helpers\DbHelper;
use App\Helpers\MiscHelper;
use app\Http\Controllers\Admin\AlbumController;
use App\Http\Controllers\Controller;
use App\Http\Middleware\VerifyCsrfToken;
use App\Photo;
use App\VisitorHit;
use GuzzleHttp\Psr7\Stream;
use Guzzle\Http\Mimetypes;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Gate;
use Symfony\Component\HttpFoundation\Request;
use function GuzzleHttp\Psr7\mimetype_from_extension;
class PhotoController extends Controller
{
@ -69,9 +71,8 @@ class PhotoController extends Controller
});
}
/** @var Stream $photoStream */
$photoStream = $album->getAlbumSource()->fetchPhotoContent($photo, $thumbnail);
$mimeType = mimetype_from_extension(pathinfo($photo->storage_file_name, PATHINFO_EXTENSION));
$mimeType = Mimetypes::getInstance()->fromFilename($photo->storage_file_name);
return response()->stream(
function() use ($photoStream)
@ -80,7 +81,7 @@ class PhotoController extends Controller
},
200,
[
'Content-Length' => strlen($photoStream->getContents()),
'Content-Length' => $photoStream->getContentLength(),
'Content-Type' => $mimeType
]
);
@ -101,42 +102,11 @@ class PhotoController extends Controller
$isOriginalAllowed = Gate::forUser($this->getUser())->allows('photo.download_original', $photo);
// Load the Next/Previous buttons
$thisPhotoDate = is_null($photo->taken_at) ? $photo->created_at : $photo->taken_at;
// I don't like the idea of using a totally raw SQL query, but it's the only sure-fire way to number the rows
// so we can get the previous/next photos accurately - and we don't have to load all data for the photo objects
$previousPhoto = null;
$nextPhoto = null;
$allAlbumPhotos = DB::select(
DB::raw(
'SELECT p.id, (@row_number:=@row_number + 1) AS row_number
FROM photos p, (SELECT @row_number:=0) AS t
WHERE p.album_id = :album_id
ORDER BY COALESCE(p.taken_at, p.created_at), p.name, p.id;'
),
[
'album_id' => $album->id
]
);
for ($i = 0; $i < count($allAlbumPhotos); $i++)
$returnAlbumUrl = $album->url();
$referer = $request->headers->get('Referer');
if (strlen($referer) > 0 && MiscHelper::isSafeUrl($referer))
{
if ($allAlbumPhotos[$i]->id === $photo->id)
{
if ($i > 0)
{
$previousPhoto = Photo::where('id', $allAlbumPhotos[$i - 1]->id)->first();
}
if ($i + 1 < count($allAlbumPhotos))
{
$nextPhoto = Photo::where('id', $allAlbumPhotos[$i + 1]->id)->first();
}
break;
}
$returnAlbumUrl = $referer;
}
// Record the visit to the photo
@ -154,33 +124,8 @@ class PhotoController extends Controller
return Theme::render('gallery.photo', [
'album' => $album,
'is_original_allowed' => $isOriginalAllowed,
'next_photo' => $nextPhoto,
'photo' => $photo,
'previous_photo' => $previousPhoto,
'success' => $request->getSession()->get('success')
]);
}
public function showExifData(Request $request, $albumUrlAlias, $photoFilename)
{
$album = DbHelper::getAlbumByPath($albumUrlAlias);
if (is_null($album))
{
App::abort(404);
return null;
}
$this->authorizeForUser($this->getUser(), 'view', $album);
$photo = PhotoController::loadPhotoByAlbumAndFilename($album, $photoFilename);
$this->authorizeForUser($this->getUser(), 'changeMetadata', $photo);
$exifData = print_r(unserialize(base64_decode($photo->raw_exif_data)), true);
return Theme::render('gallery.photo_exif', [
'album' => $album,
'exif_data' => $exifData,
'photo' => $photo
'return_album_url' => $returnAlbumUrl
]);
}

View File

@ -1,313 +0,0 @@
<?php
namespace App\Http\Controllers\Gallery;
use App\Facade\Theme;
use App\Helpers\DbHelper;
use App\Http\Controllers\Controller;
use App\Label;
use App\Photo;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class StatisticsController extends Controller
{
public function albumSizeByPhotosChart(Request $request)
{
$this->authorizeForUser($this->getUser(), 'statistics.public-access');
$stats = DB::table('photos')
->whereIn('photos.album_id', DbHelper::getAlbumIDsForCurrentUser())
->join('albums', 'albums.id', '=', 'photos.album_id')
->groupBy('albums.name')
->select('albums.name', DB::raw('count(photos.id) as photo_count'))
->orderBy('photo_count', 'desc')
->limit(10)
->get();
$labels = [];
$data = [];
foreach ($stats as $stat)
{
$labels[] = $stat->name;
$data[] = $stat->photo_count;
}
return response()->json([
'labels' => $labels,
'backgrounds' => $this->rotateColoursForData($data),
'data' => $data
]);
}
public function albumSizeByPhotoSizeChart(Request $request)
{
$this->authorizeForUser($this->getUser(), 'statistics.public-access');
$stats = DB::table('photos')
->whereIn('photos.album_id', DbHelper::getAlbumIDsForCurrentUser())
->join('albums', 'albums.id', '=', 'photos.album_id')
->groupBy('albums.name')
->select('albums.name', DB::raw('sum(photos.file_size) as photo_size'))
->orderBy('photo_size', 'desc')
->limit(10)
->get();
$labels = [];
$data = [];
foreach ($stats as $stat)
{
$labels[] = $stat->name;
$data[] = ceil($stat->photo_size / 1024 / 1024);
}
return response()->json([
'labels' => $labels,
'backgrounds' => $this->rotateColoursForData($data),
'data' => $data
]);
}
public function camerasChart(Request $request)
{
$this->authorizeForUser($this->getUser(), 'statistics.public-access');
$stats = DB::table('photos')
->where([
['camera_make', '!=', ''],
['camera_model', '!=', '']
])
->whereIn('album_id', DbHelper::getAlbumIDsForCurrentUser())
->groupBy('camera_make', 'camera_model')
->select('camera_make', 'camera_model', DB::raw('count(*) as photo_count'))
->orderBy('photo_count', 'desc')
->get();
$labels = [];
$data = [];
foreach ($stats as $stat)
{
// Remove the model from the make if it starts with it
// E.g. CANON - CANON EOS 1200D becomes just CANON EOS 1200D
if (substr($stat->camera_model, 0, strlen($stat->camera_make)) == $stat->camera_make)
{
$stat->camera_make = trim(substr($stat->camera_make, strlen($stat->camera_make)));
}
$labels[] = sprintf('%s %s', $stat->camera_make, $stat->camera_model);
$data[] = $stat->photo_count;
}
return response()->json([
'labels' => $labels,
'backgrounds' => $this->rotateColoursForData($data),
'data' => $data
]);
}
public function fileSizeChart(Request $request)
{
$this->authorizeForUser($this->getUser(), 'statistics.public-access');
$labels = [
trans('gallery.statistics.file_sizes_legend.small'),
trans('gallery.statistics.file_sizes_legend.medium'),
trans('gallery.statistics.file_sizes_legend.large'),
trans('gallery.statistics.file_sizes_legend.huge')
];
$data = [0, 0, 0, 0];
$stats = DB::table('photos')->whereIn('album_id', DbHelper::getAlbumIDsForCurrentUser())->orderBy('id');
$stats->chunk(100, function($photos) use (&$data)
{
foreach ($photos as $photo)
{
if ($photo->file_size < (1 * 1024 * 1024))
{
$data[0]++;
}
else if ($photo->file_size < (3 * 1024 * 1024))
{
$data[1]++;
}
else if ($photo->file_size < (5 * 1024 * 1024))
{
$data[2]++;
}
else if ($photo->file_size >= (5 * 1024 * 1024))
{
$data[3]++;
}
}
});
return response()->json([
'labels' => $labels,
'backgrounds' => $this->rotateColoursForData($data),
'data' => $data
]);
}
public function index(Request $request)
{
$this->authorizeForUser($this->getUser(), 'statistics.public-access');
// Numbers for at-a-glance
$albumIDs = DbHelper::getAlbumIDsForCurrentUser();
$albumCount = count($albumIDs);
$labelCount = Label::all()->count();
$photoCount = Photo::whereIn('album_id', $albumIDs)->count();
return Theme::render('gallery.statistics', [
'album_count' => $albumCount,
'label_count' => $labelCount,
'photo_count' => $photoCount
]);
}
public function photosCombined(Request $request)
{
$this->authorizeForUser($this->getUser(), 'statistics.public-access');
$labels = [];
$data = [
['label' => trans('gallery.statistics.photos_combined.taken'), 'values' => []],
['label' => trans('gallery.statistics.photos_combined.uploaded'), 'values' => []]
];
foreach ($this->lastXMonthsDates(18) as $date)
{
$fromDate = sprintf('%04d-%02d-01 00:00:00', $date[0], $date[1]);
$toDate = sprintf('%04d-%02d-%02d 23:59:59', $date[0], $date[1], cal_days_in_month(CAL_GREGORIAN, $date[1], $date[0]));
$photoCountTaken = Photo::whereBetween('taken_at', array($fromDate, $toDate))->count();
$photoCountUploaded = Photo::whereBetween('created_at', array($fromDate, $toDate))->count();
$labels[] = date('M Y', strtotime($fromDate));
$data[0]['values'][] = $photoCountTaken;
$data[1]['values'][] = $photoCountUploaded;
}
$data[0]['values'] = array_reverse($data[0]['values']);
$data[1]['values'] = array_reverse($data[1]['values']);
return response()->json([
'labels' => array_reverse($labels),
'data' => $data
]);
}
public function photosTaken12Months(Request $request)
{
$this->authorizeForUser($this->getUser(), 'statistics.public-access');
$labels = [];
$data = [];
foreach ($this->lastXMonthsDates() as $date)
{
$fromDate = sprintf('%04d-%02d-01 00:00:00', $date[0], $date[1]);
$toDate = sprintf('%04d-%02d-%02d 23:59:59', $date[0], $date[1], cal_days_in_month(CAL_GREGORIAN, $date[1], $date[0]));
$photoCount = Photo::whereBetween('taken_at', array($fromDate, $toDate))->count();
$labels[] = date('M Y', strtotime($fromDate));
$data[] = $photoCount;
}
return response()->json([
'labels' => array_reverse($labels),
'data' => array_reverse($data)
]);
}
public function photosUploaded12Months(Request $request)
{
$this->authorizeForUser($this->getUser(), 'statistics.public-access');
$labels = [];
$data = [];
foreach ($this->lastXMonthsDates() as $date)
{
$fromDate = sprintf('%04d-%02d-01 00:00:00', $date[0], $date[1]);
$toDate = sprintf('%04d-%02d-%02d 23:59:59', $date[0], $date[1], cal_days_in_month(CAL_GREGORIAN, $date[1], $date[0]));
$photoCount = Photo::whereBetween('created_at', array($fromDate, $toDate))->count();
$labels[] = date('M Y', strtotime($fromDate));
$data[] = $photoCount;
}
return response()->json([
'labels' => array_reverse($labels),
'data' => array_reverse($data)
]);
}
private function lastXMonthsDates($x = 12)
{
$year = intval(date('Y'));
$month = intval(date('m'));
$datesNeeded = [];
while (count($datesNeeded) < $x)
{
$datesNeeded[] = [$year, $month];
$month--;
if ($month == 0)
{
$month = 12;
$year--;
}
}
return $datesNeeded;
}
private function rotateColoursForData(array $data = [])
{
$colours = [
'#d54d36',
'#59c669',
'#aa5ccf',
'#85b83a',
'#5f6cd9',
'#bbb248',
'#ca49a1',
'#479341',
'#d94b70',
'#52b395',
'#7b589e',
'#da8f32',
'#6e8bd0',
'#8a722c',
'#46aed7',
'#aa5839',
'#d48cca',
'#64803f',
'#a5506d',
'#e19774'
];
$result = [];
$lastIndex = 0;
for ($i = 0; $i < count($data); $i++)
{
$result[] = $colours[$lastIndex];
$lastIndex++;
if ($lastIndex >= count($colours))
{
$lastIndex = 0;
}
}
return $result;
}
}

View File

@ -1,531 +0,0 @@
<?php
namespace App\Http\Controllers\Gallery;
use App\Facade\Theme;
use App\Facade\UserConfig;
use App\Helpers\DbHelper;
use App\Http\Controllers\Controller;
use App\Http\Requests\SaveUserSettingsRequest;
use App\Notifications\UserChangeEmailRequired;
use App\User;
use App\UserActivity;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\DB;
use Symfony\Component\HttpFoundation\Request;
class UserController extends Controller
{
public function activityFeed()
{
if (!UserConfig::get('social_user_feeds'))
{
return redirect(route('home'));
}
return Theme::render('gallery.user_activity_feed', [
'user' => $this->getUser()
]);
}
public function activityFeedJson()
{
if (!UserConfig::get('social_user_feeds'))
{
return response()->json(['message' => 'Activity feeds not enabled']);
}
$user = $this->getUser();
$result = [];
$activities = UserActivity::with('photo')
->with('photoComment')
->with('user')
->join('user_followers', 'user_followers.following_user_id', '=', 'user_activity.user_id')
->where([
'user_followers.user_id' => $user->id
])
->orderBy('activity_at', 'desc')
->limit(100) // TODO: make this configurable
->select('user_activity.*')
->get();
/** @var UserActivity $activity */
foreach ($activities as $activity)
{
$userName = $activity->user->name;
$userProfileUrl = $activity->user->profileUrl();
$userAvatar = Theme::gravatarUrl($activity->user->email, 32);
$newItem = [
'activity_at' => date(UserConfig::get('date_format'), strtotime($activity->activity_at)),
'avatar' => $userAvatar,
'description' => trans(sprintf('gallery.user_feed_type.%s', $activity->type))
];
$params = [];
$params['user_name'] = $userName;
$params['user_url'] = $userProfileUrl;
if (!is_null($activity->photo))
{
// Check the user has access
if (!$this->getUser()->can('view', $activity->photo))
{
continue;
}
$params['photo_name'] = $activity->photo->name;
$params['photo_url'] = $activity->photo->url();
}
if (!is_null($activity->album))
{
// Check the user has access
if (!$this->getUser()->can('view', $activity->album))
{
continue;
}
$params['album_name'] = $activity->album->name;
$params['album_url'] = $activity->album->url();
}
// Other activity-specific parameters
switch (strtolower($activity->type))
{
case 'user.created':
$params['app_name'] = UserConfig::get('app_name');
$params['app_url'] = route('home');
break;
}
$newItem['params'] = $params;
$result[] = $newItem;
}
return response()->json($result);
}
public function confirmEmailChangeState(Request $request)
{
$user = $this->getUser();
if (!$user->is_email_change_in_progress)
{
return redirect(route('userSettings'));
}
// Update the e-mail address
$user->email = $user->new_email_address;
// Reset the e-mail change state
$user->is_email_change_in_progress = false;
$user->new_email_address = null;
$user->save();
$request->session()->flash('success', trans('auth.change_email_success_message'));
return redirect(route('userSettings'));
}
public function followUser($idOrAlias)
{
$user = $this->loadUserProfilePage($idOrAlias);
$isFollowing = $this->getUser()->following()->where('following_user_id', $user->id)->count() > 0;
if (!$isFollowing)
{
$this->getUser()->following()->attach(
$user->id,
[
'created_at' => new \DateTime(),
'updated_at' => new \DateTime()
]
);
}
return response()->json(true);
}
public function resetEmailChangeState(Request $request)
{
$user = $this->getUser();
if (!$user->is_email_change_in_progress)
{
return redirect(route('userSettings'));
}
$data = $request->all();
if (isset($data['resend_email']))
{
$this->sendEmailChangeConfirmationEmail($user, $user->new_email_address);
$request->session()->flash('info', trans('auth.change_email_required_message'));
}
if (isset($data['cancel_change']))
{
$user->is_email_change_in_progress = false;
$user->new_email_address = null;
$user->save();
}
return redirect(route('userSettings'));
}
public function saveSettings(SaveUserSettingsRequest $request)
{
$data = $request->only(['name', 'email', 'profile_alias', 'enable_profile_page']);
$user = $this->getUser();
if (
UserConfig::get('require_email_verification') &&
isset($data['email']) &&
$data['email'] != $user->email &&
!$user->is_email_change_in_progress
)
{
// Can't update the e-mail directly until the new e-mail address has been verified.
// TODO - send e-mail and handle response, flag e-mail as being "change in-progress"
// Send activation e-mail
$this->sendEmailChangeConfirmationEmail($user, $data['email']);
$request->session()->flash('info', trans('auth.change_email_required_message'));
// Flag the user as a change e-mail in progress
$user->new_email_address = $data['email'];
$user->is_email_change_in_progress = true;
$user->save();
unset($data['email']);
$request->session()->flash('info', trans('auth.change_email_required_message'));
}
// Don't allow e-mail address to be changed if a change is in progress
if ($user->is_email_change_in_progress)
{
unset($data['email']);
}
$user->fill($data);
$user->enable_profile_page = (isset($data['enable_profile_page']) && strtolower($data['enable_profile_page']) == 'on');
$user->save();
$request->session()->flash('success', trans('gallery.user_settings.settings_saved'));
return redirect(route('userSettings'));
}
public function settings(Request $request)
{
return Theme::render('gallery.user_settings', [
'info' => $request->session()->get('info'),
'success' => $request->session()->get('success'),
'user' => $this->getUser()
]);
}
public function show(Request $request, $idOrAlias)
{
$user = $this->loadUserProfilePage($idOrAlias);
$albums = $this->getAlbumsForUser($user);
$albumIDs = $this->getAlbumIDsForUser($user);
$cameras = $this->getCamerasUsedInAlbums($albumIDs);
$activity = $this->getActivityDatesInAlbums($albumIDs);
$daysInMonth = $this->getDaysInMonths();
// Only logged-in users can follow other users (and if it's not their own page!)
$canFollow = !$this->getUser()->isAnonymous() && $this->getUser()->id != $user->id;
$isFollowing = false;
if ($canFollow)
{
// Is the current user following this user?
$isFollowing = $this->getUser()->following()->where('following_user_id', $user->id)->count() > 0;
}
return Theme::render('gallery.user_profile', [
'active_tab' => $request->get('tab'),
'activity_taken' => $this->constructActivityGrid($activity['taken']),
'activity_uploaded' => $this->constructActivityGrid($activity['uploaded']),
'albums' => $albums,
'cameras' => $cameras,
'can_follow' => $canFollow,
'is_following' => $isFollowing,
'month_days' => $daysInMonth,
'user' => $user
]);
}
public function showFeedJson(Request $request, $idOrAlias)
{
$user = $this->loadUserProfilePage($idOrAlias);
$result = [];
$activities = UserActivity::with('photo')
->with('photoComment')
->with('album')
->where([
'user_id' => $user->id
])
->orderBy('activity_at', 'desc')
->limit(100) // TODO: make this configurable
->get();
$userName = $user->name;
$userProfileUrl = $user->profileUrl();
$userAvatar = Theme::gravatarUrl($user->email, 32);
/** @var UserActivity $activity */
foreach ($activities as $activity)
{
$newItem = [
'activity_at' => date(UserConfig::get('date_format'), strtotime($activity->activity_at)),
'avatar' => $userAvatar,
'description' => trans(sprintf('gallery.user_feed_type.%s', $activity->type))
];
$params = [];
$params['user_name'] = $userName;
$params['user_url'] = $userProfileUrl;
if (!is_null($activity->photo))
{
// Check the user has access
if (!$this->getUser()->can('view', $activity->photo))
{
continue;
}
$params['photo_name'] = $activity->photo->name;
$params['photo_url'] = $activity->photo->url();
}
if (!is_null($activity->album))
{
// Check the user has access
if (!$this->getUser()->can('view', $activity->album))
{
continue;
}
$params['album_name'] = $activity->album->name;
$params['album_url'] = $activity->album->url();
}
// Other activity-specific parameters
switch (strtolower($activity->type))
{
case 'user.created':
$params['app_name'] = UserConfig::get('app_name');
$params['app_url'] = route('home');
break;
}
$newItem['params'] = $params;
$result[] = $newItem;
}
return response()->json($result);
}
public function unFollowUser($idOrAlias)
{
$user = $this->loadUserProfilePage($idOrAlias);
$isFollowing = $this->getUser()->following()->where('following_user_id', $user->id)->count() > 0;
if ($isFollowing)
{
$this->getUser()->following()->detach($user->id);
}
return response()->json(true);
}
private function constructActivityGrid(Collection $collection)
{
$results = [];
$lastYearFrom = new \DateTime();
$lastYearFrom->sub(new \DateInterval('P1Y'));
$lastYearFrom->add(new \DateInterval('P1D'));
$today = new \DateTime();
$current = clone $lastYearFrom;
while ($current < $today)
{
$year = intval($current->format('Y'));
$month = intval($current->format('m'));
$date = intval($current->format('d'));
if (!isset($results[$year]))
{
$results[$year] = [];
}
if (!isset($results[$year][$month]))
{
$results[$year][$month] = [];
}
if (!isset($results[$year][$month][$date]))
{
$results[$year][$month][$date] = 0;
}
$current->add(new \DateInterval('P1D'));
}
// Now update the totals from the collection
foreach ($collection as $photoInfo)
{
$date = \DateTime::createFromFormat('Y-m-d', $photoInfo->the_date);
$year = intval($date->format('Y'));
$month = intval($date->format('m'));
$date = intval($date->format('d'));
$results[$year][$month][$date] = $photoInfo->photos_count;
}
// Replace the month names
foreach ($results as $year => &$months)
{
foreach ($months as $month => $dates)
{
$monthDate = \DateTime::createFromFormat('m', $month);
$months[$monthDate->format('M')] = $dates;
unset($months[$month]);
}
}
return $results;
}
private function getActivityDatesInAlbums(array $albumIDs)
{
$createdAt = DB::table('photos')
->whereIn('album_id', $albumIDs)
->whereRaw(DB::raw('DATE(created_at) > DATE(DATE_SUB(NOW(), INTERVAL 1 year))'))
->select([
DB::raw('DATE(created_at) AS the_date'),
DB::raw('COUNT(photos.id) AS photos_count')
])
->groupBy(DB::raw('DATE(created_at)'))
->orderBy(DB::raw('DATE(created_at)'))
->get();
$takenAt = DB::table('photos')
->whereIn('album_id', $albumIDs)
->whereRaw(DB::raw('DATE(taken_at) > DATE(DATE_SUB(NOW(), INTERVAL 1 year))'))
->select([
DB::raw('DATE(taken_at) AS the_date'),
DB::raw('COUNT(photos.id) AS photos_count')
])
->groupBy(DB::raw('DATE(taken_at)'))
->orderBy(DB::raw('DATE(taken_at)'))
->get();
return ['uploaded' => $createdAt, 'taken' => $takenAt];
}
private function getAlbumsForUser(User $user)
{
return DbHelper::getAlbumsForCurrentUser_NonPaged()
->where('user_id', $user->id)
->paginate(UserConfig::get('items_per_page'));
}
private function getAlbumIDsForUser(User $user)
{
$results = [];
$albums = DbHelper::getAlbumsForCurrentUser_NonPaged()
->where('user_id', $user->id)
->select('albums.id')
->get();
foreach ($albums as $album)
{
$results[] = intval($album->id);
}
return $results;
}
private function getCamerasUsedInAlbums(array $albumIDs)
{
return DB::table('photos')
->whereIn('album_id', $albumIDs)
->where([
['camera_make', '!=', ''],
['camera_model', '!=', '']
])
->groupBy('camera_make', 'camera_model', 'camera_software')
->select('camera_make', 'camera_model', 'camera_software', DB::raw('count(*) as photo_count'))
->orderBy('photo_count', 'desc')
->orderBy('camera_make')
->orderBy('camera_model')
->orderBy('camera_software')
->get();
}
private function getDaysInMonths()
{
$results = [];
$lastYearFrom = new \DateTime();
$lastYearFrom->sub(new \DateInterval('P1Y'));
$lastYearFrom->sub(new \DateInterval(sprintf('P%dD', $lastYearFrom->format('d') - 1)));
$today = new \DateTime();
$current = clone $lastYearFrom;
while ($current < $today)
{
$year = intval($current->format('Y'));
$month = intval($current->format('m'));
$daysInMonth = cal_days_in_month(CAL_GREGORIAN, $month, $year);
$results[$year][$current->format('M')] = $daysInMonth;
$current->add(new \DateInterval('P1M'));
}
return $results;
}
/**
* @param $idOrAlias
* @return User
*/
private function loadUserProfilePage($idOrAlias)
{
// If a user has a profile alias set, their profile page cannot be accessed by the ID
$user = User::where(DB::raw('COALESCE(NULLIF(profile_alias, \'\'), id)'), strtolower($idOrAlias))->first();
if (is_null($user))
{
App::abort(404);
return null;
}
$this->authorizeForUser($this->getUser(), 'view', $user);
return $user;
}
private function sendEmailChangeConfirmationEmail(User $user, $newEmailAddress)
{
$oldEmailAddress = $user->email;
$user->email = $newEmailAddress;
$user->notify(new UserChangeEmailRequired());
$user->email = $oldEmailAddress;
}
}

View File

@ -2,12 +2,10 @@
namespace App\Http\Controllers;
use App\AlbumDefaultAnonymousPermission;
use App\Configuration;
use App\Facade\UserConfig;
use App\Helpers\MiscHelper;
use App\Http\Requests\StoreUserRequest;
use App\Permission;
use App\Storage;
use App\User;
use Illuminate\Http\Request;
@ -19,10 +17,10 @@ class InstallController extends Controller
public function administrator(StoreUserRequest $request)
{
// Validate we're at the required stage
$stage = 2;
$stage = 3;
if (intval($request->session()->get('install_stage')) < $stage)
{
return redirect(route('install.database'));
return redirect(route('install.check'));
}
// If we already have an admin account, this step can be skipped
@ -41,7 +39,6 @@ class InstallController extends Controller
$user->password = bcrypt($request->get('password'));
$user->is_admin = true;
$user->is_activated = true;
$user->enable_profile_page = true;
$user->save();
return $this->completeSetup();
@ -52,10 +49,71 @@ class InstallController extends Controller
]);
}
public function database(Request $request)
public function check(Request $request)
{
// This is the first installation step therefore it doesn't need to verify the stage
if ($request->getMethod() == 'POST')
{
$request->session()->set('install_stage', 2);
return redirect(route('install.database'));
}
$canContinue = true;
$runtimeMinimum = '5.6.4'; // this minimum is imposed by Laravel 5.3
$runtimeVersion = phpversion();
$phpIsValid = version_compare($runtimeVersion, $runtimeMinimum) >= 0;
if (!$phpIsValid)
{
$canContinue = false;
}
$requiredModules = [
'curl' => 'installer.php_modules.curl',
'pdo_mysql' => 'installer.php_modules.mysql',
'gd' => 'installer.php_modules.gd'
];
$availableModules = [];
foreach ($requiredModules as $key => $langString)
{
$availableModules[$key] = extension_loaded($key);
if (!$availableModules[$key])
{
$canContinue = false;
}
}
$uploadLimit = MiscHelper::convertToBytes(ini_get('upload_max_filesize'));
$postMaxSize = MiscHelper::convertToBytes(ini_get('post_max_size'));
$recommendedMinimum = 4 * 1024 * 1024;
return view('install.check', [
'available_modules' => $availableModules,
'can_continue' => $canContinue,
'php_is_valid' => $phpIsValid,
'php_version_current' => $runtimeVersion,
'php_version_required' => $runtimeMinimum,
'post_max_size' => ($postMaxSize / 1024 / 1024),
'post_max_size_warning' => $postMaxSize < $recommendedMinimum,
'recommended_minimum_upload' => ($recommendedMinimum / 1024 / 1024),
'upload_limit' => ($uploadLimit / 1024 / 1024),
'upload_limit_warning' => $uploadLimit < $recommendedMinimum,
'required_modules' => $requiredModules
]);
}
public function database(Request $request)
{
// Validate we're at the required stage
$stage = 2;
if (intval($request->session()->get('install_stage')) < $stage)
{
return redirect(route('install.check'));
}
if ($request->method() == 'POST')
{
$baseDirectory = dirname(dirname(dirname(__DIR__)));
@ -92,16 +150,12 @@ class InstallController extends Controller
Artisan::call('cache:clear');
Artisan::call('migrate', ['--force' => true]);
Artisan::call('db:seed', ['--force' => true]);
$versionNumber = UserConfig::getOrCreateModel('app_version');
$versionNumber->value = config('app.version');
$versionNumber->save();
// Default settings
$this->setConfigurationForNewSystems();
$request->session()->put('install_stage', 2);
$request->session()->set('install_stage', 3);
return redirect(route('install.administrator'));
}
catch (\Exception $ex)
@ -155,47 +209,4 @@ class InstallController extends Controller
}
}
}
private function setDefaultAnonymousPermission($section, $permission)
{
$permission = Permission::where([
['section', $section],
['description', $permission]
])->first();
if (is_null($permission))
{
return;
}
$adap = new AlbumDefaultAnonymousPermission();
$adap->permission_id = $permission->id;
$adap->save();
}
private function setConfigurationForNewSystems()
{
/** @var Configuration $socialFeeds */
$socialFeeds = UserConfig::getOrCreateModel('social_user_feeds');
$socialFeeds->value = true;
$socialFeeds->save();
/** @var Configuration $socialProfiles */
$socialProfiles = UserConfig::getOrCreateModel('social_user_profiles');
$socialProfiles->value = true;
$socialProfiles->save();
/** @var Configuration $photoComments */
$photoComments = UserConfig::getOrCreateModel('allow_photo_comments');
$photoComments->value = true;
$photoComments->save();
$defaultPermissions = ['album.list', 'album.view', 'album.post-comment'];
foreach ($defaultPermissions as $defaultPermission)
{
$permissionParts = explode('.', $defaultPermission);
$this->setDefaultAnonymousPermission($permissionParts[0], $permissionParts[1]);
}
}
}

View File

@ -5,7 +5,6 @@ namespace App\Http\Middleware;
use App\DataMigration;
use App\Facade\UserConfig;
use App\Helpers\MiscHelper;
use App\Helpers\PermissionsHelper;
use Closure;
use Illuminate\Foundation\Application;
use Illuminate\Http\Request;
@ -14,6 +13,9 @@ use Illuminate\Support\Facades\Log;
class AppInstallation
{
private $baseDirectory;
private $environmentFilePath;
/**
* The application instance.
*
@ -30,6 +32,8 @@ class AppInstallation
public function __construct(Application $app)
{
$this->app = $app;
$this->baseDirectory = dirname(dirname(dirname(__DIR__)));
$this->environmentFilePath = sprintf('%s/.env', $this->baseDirectory);
}
public function handle(Request $request, Closure $next)
@ -46,14 +50,6 @@ class AppInstallation
// See if the successful flag has been written to the .env file
$isAppInstalled = MiscHelper::getEnvironmentSetting('APP_INSTALLED');
// See if the vendors are out-of-date
if ($this->isVendorUpdateRequired())
{
return $isAppInstalled
? redirect('/update')
: redirect('/install');
}
if ($request->is('install/*'))
{
// Already in the installer
@ -69,40 +65,26 @@ class AppInstallation
if ($isAppInstalled)
{
// See if an update is necessary
if ($this->updateDatabaseIfRequired())
{
return redirect($request->fullUrl());
}
$this->updateDatabaseIfRequired();
// App is configured, continue on
return $next($request);
}
return redirect(route('install.database'));
return redirect(route('install.check'));
}
private function generateAppKey()
{
// Generate an application key and store to the .env file
if (!file_exists(MiscHelper::getEnvironmentFilePath()))
if (!file_exists($this->environmentFilePath))
{
$key = MiscHelper::randomString(32);
file_put_contents(MiscHelper::getEnvironmentFilePath(), sprintf('APP_KEY=%s', $key) . PHP_EOL);
file_put_contents($this->environmentFilePath, sprintf('APP_KEY=%s', $key) . PHP_EOL);
app('config')->set(['app' => ['key' => $key]]);
}
}
private function isVendorUpdateRequired()
{
$vendorsVersionFilename = $this->app->basePath('vendor/version.txt');
if (!file_exists($vendorsVersionFilename))
{
return true;
}
return trim(file_get_contents($vendorsVersionFilename)) != trim(config('app.version'));
}
private function updateDatabaseIfRequired()
{
$versionNumber = UserConfig::getOrCreateModel('app_version');
@ -112,9 +94,7 @@ class AppInstallation
{
Log::info('Upgrading database', ['new_version' => $appVersionNumber]);
Artisan::call('config:cache');
Artisan::call('cache:clear');
Artisan::call('view:clear');
Artisan::call('migrate', ['--force' => true]);
Artisan::call('db:seed', ['--force' => true]);
@ -151,14 +131,6 @@ class AppInstallation
// Save the new version number
$versionNumber->value = $appVersionNumber;
$versionNumber->save();
// Rebuild the permissions cache
$helper = new PermissionsHelper();
$helper->rebuildCache();
return true;
}
return false;
}
}

View File

@ -19,7 +19,6 @@ class CheckMaxPostSizeExceeded
protected $exclude = [
'/admin/photos/analyse/*',
'/admin/photos/flip/*',
'/admin/photos/reanalyse/*',
'/admin/photos/regenerate-thumbnails/*',
'/admin/photos/rotate/*'
];

View File

@ -6,7 +6,6 @@ use App\Album;
use App\Facade\Theme;
use App\Facade\UserConfig;
use App\Helpers\DbHelper;
use App\Label;
use Closure;
use Illuminate\Contracts\Encryption\DecryptException;
use Illuminate\Foundation\Application;
@ -23,10 +22,73 @@ class GlobalConfiguration
*/
protected $app;
public static function updateMailConfig()
/**
* Create a new middleware instance.
*
* @param \Illuminate\Foundation\Application $app
* @return void
*/
public function __construct(Application $app)
{
$this->app = $app;
}
public function handle(Request $request, Closure $next)
{
// We can always add the version number
$this->addVersionNumberToView();
// If running the installer, chances are our database isn't running yet
if ($request->is('install/*'))
{
return $next($request);
}
// When running migrations, CLI tasks or the installer, don't need to add things to the view
if (php_sapi_name() != 'cli')
{
$this->addThemeInfoToView();
$this->addAlbumsToView();
}
// Set the default mail configuration as per user's requirements
$this->updateMailConfig();
return $next($request);
}
private function addAlbumsToView()
{
$albums = DbHelper::getAlbumsForCurrentUser_NonPaged()->get();
View::share('albums', $albums);
}
private function addThemeInfoToView()
{
$themeInfo = Theme::info();
// Add each theme info element to the view - prefixing with theme_
// e.g. $themeInfo['name'] becomes $theme_name in the view
foreach ($themeInfo as $key => $value)
{
View::share('theme_' . $key, $value);
}
// Also add a theme_url key
View::share('theme_url', sprintf('themes/%s', Theme::current()));
}
private function addVersionNumberToView()
{
$version = config('app.version');
View::share('app_version', $version);
View::share('app_version_url', $version);
}
private function updateMailConfig()
{
/** @var Mailer $mailer */
$mailer = app('mailer');
$mailer = $this->app->mailer;
$swiftMailer = $mailer->getSwiftMailer();
/** @var \Swift_SmtpTransport $transport */
@ -60,152 +122,4 @@ class GlobalConfiguration
$mailer->alwaysFrom(UserConfig::get('sender_address'), UserConfig::get('sender_name'));
}
/**
* Create a new middleware instance.
*
* @param \Illuminate\Foundation\Application $app
* @return void
*/
public function __construct(Application $app)
{
$this->app = $app;
}
public function handle(Request $request, Closure $next)
{
// We can always add the version number
$this->addVersionNumberToView();
// If running the installer, chances are our database isn't running yet
if ($request->is('install/*'))
{
return $next($request);
}
// When running migrations, CLI tasks or the installer, don't need to add things to the view
if (php_sapi_name() != 'cli')
{
$this->addThemeInfoToView();
$this->addAlbumsToView();
$this->addLabelsToView();
$this->addFlashMessages();
}
// Set the default mail configuration as per user's requirements
$this->updateMailConfig();
return $next($request);
}
private function addAlbumsToView()
{
$albums = DbHelper::getAlbumsForCurrentUser_NonPaged()->get();
View::share('g_albums', $albums);
if (UserConfig::get('albums_menu_parents_only'))
{
// Only show top-level albums in the nav bar
$navbarAlbums = $albums->filter(function($value, $key)
{
return is_null($value->parent_album_id);
});
}
else
{
// If not just showing top-level albums, we can show all
$navbarAlbums = $albums;
}
$navbarAlbumsToDisplay = UserConfig::get('albums_menu_number_items');
View::share('g_albums_menu', $navbarAlbums->take($navbarAlbumsToDisplay));
View::share('g_more_albums', $navbarAlbums->count() - $navbarAlbumsToDisplay);
$albumsToUpload = DbHelper::getAlbumsForCurrentUser_NonPaged('upload-photos')->get();
View::share('g_albums_upload', $albumsToUpload);
}
private function addFlashMessages()
{
/** @var Request $request */
$request = app('request');
if ($request->session()->has('error'))
{
View::share('error', $request->session()->get('error'));
}
if ($request->session()->has('success'))
{
View::share('success', $request->session()->get('success'));
}
}
private function addLabelsToView()
{
$NUMBER_TO_SHOW_IN_NAVBAR = 5;
$labelCount = Label::count();
$labels = Label::all();
$labelsToAdd = [];
/** @var Label $label */
foreach ($labels as $label)
{
$label->photos_count = $label->photoCount();
$labelsToAdd[] = $label;
}
// Sort my photo count, then name
usort($labelsToAdd, function(Label $a, Label $b)
{
if ($a->photos_count == $b->photos_count)
{
if ($a->name == $b->name)
{
return 0;
}
else if ($a->name < $b->name)
{
return -1;
}
else if ($a->name > $b->name)
{
return 1;
}
}
else if ($a->photos_count < $b->photos_count)
{
return -1;
}
else if ($a->photos_count > $b->photos_count)
{
return 1;
}
});
$labelsToAdd = array_slice(array_reverse($labelsToAdd), 0, $NUMBER_TO_SHOW_IN_NAVBAR);
View::share('g_labels', $labelsToAdd);
View::share('g_more_labels', $labelCount - $NUMBER_TO_SHOW_IN_NAVBAR);
}
private function addThemeInfoToView()
{
$themeInfo = Theme::info();
// Add each theme info element to the view - prefixing with theme_
// e.g. $themeInfo['name'] becomes $theme_name in the view
foreach ($themeInfo as $key => $value)
{
View::share('theme_' . $key, $value);
}
// Also add a theme_url key
View::share('theme_url', sprintf('themes/%s', Theme::current()));
}
private function addVersionNumberToView()
{
$version = config('app.version');
View::share('app_version', $version);
View::share('app_version_url', $version);
}
}

View File

@ -24,11 +24,10 @@ class SaveSettingsRequest extends FormRequest
public function rules()
{
return [
'albums_menu_number_items' => 'required|integer|min:1',
'app_name' => 'required|max:255',
'date_format' => 'required',
'smtp_server' => 'required',
'smtp_port' => 'required|integer'
'smtp_port' => 'required:integer'
];
}
}

View File

@ -1,33 +0,0 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Auth;
class SaveUserSettingsRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'name' => 'required|max:255',
'email' => 'required|email|max:255|unique:users,email,' . Auth::user()->id,
'profile_alias' => 'sometimes|max:255|unique:users,profile_alias,' . Auth::user()->id
];
}
}

View File

@ -28,7 +28,7 @@ class StoreAlbumRequest extends FormRequest
case 'POST':
return [
'description' => '',
'name' => 'required|album_path_unique|max:255',
'name' => 'required|unique:albums|max:255',
'storage_id' => 'required|sometimes'
];
@ -38,7 +38,7 @@ class StoreAlbumRequest extends FormRequest
return [
'description' => 'sometimes',
'name' => 'required|sometimes|max:255|album_path_unique:' . $albumId,
'name' => 'required|sometimes|max:255|unique:albums,name,' . $albumId,
'storage_id' => 'required|sometimes'
];
}

View File

@ -1,28 +0,0 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreLabelRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return ['name' => 'required:max:255|unique:labels,name'];
}
}

View File

@ -1,74 +0,0 @@
<?php
namespace App\Http\Requests;
use App\ExternalService;
use Illuminate\Foundation\Http\FormRequest;
class StoreServiceRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
$result = [];
switch ($this->method())
{
case 'POST':
$result = [
'name' => 'required|unique:external_services|max:255',
'service_type' => 'required|max:255',
];
switch ($this->get('service_type'))
{
case ExternalService::DROPBOX:
case ExternalService::FACEBOOK:
case ExternalService::GOOGLE:
case ExternalService::TWITTER:
// Standard OAuth services
$result['app_id'] = 'sometimes|required';
$result['app_secret'] = 'sometimes|required';
break;
}
break;
case 'PATCH':
case 'PUT':
$serviceId = intval($this->segment(3));
$service = ExternalService::find($serviceId);
$result = [
'name' => 'required|max:255|unique:external_services,name,' . $serviceId
];
switch ($service->service_type)
{
case ExternalService::DROPBOX:
case ExternalService::FACEBOOK:
case ExternalService::GOOGLE:
case ExternalService::TWITTER:
// Standard OAuth services
$result['app_id'] = 'sometimes|required';
$result['app_secret'] = 'sometimes|required';
break;
}
break;
}
return $result;
}
}

View File

@ -65,16 +65,6 @@ class StoreStorageRequest extends FormRequest
$result['service_region'] = 'sometimes|required';
$result['container_name'] = 'sometimes|required';
break;
case 'BackblazeB2Source':
$result['access_key'] = 'sometimes|required';
$result['secret_key'] = 'sometimes|required';
$result['container_name'] = 'sometimes|required';
break;
case 'DropboxSource':
$result['external_service_id'] = 'sometimes|required';
break;
}
break;
@ -113,16 +103,6 @@ class StoreStorageRequest extends FormRequest
$result['service_region'] = 'sometimes|required';
$result['container_name'] = 'sometimes|required';
break;
case 'BackblazeB2Source':
$result['access_key'] = 'sometimes|required';
$result['secret_key'] = 'sometimes|required';
$result['container_name'] = 'sometimes|required';
break;
case 'DropboxSource':
$result['external_service_id'] = 'sometimes|required';
break;
}
break;
}

View File

@ -1,60 +0,0 @@
<?php
namespace App;
use App\Helpers\DbHelper;
use App\Helpers\MiscHelper;
use Illuminate\Database\Eloquent\Model;
class Label extends Model
{
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'name', 'url_alias'
];
public function generateAlias()
{
$this->url_alias = preg_replace('/[^a-z0-9\-]/', '-', strtolower($this->name));
}
public function photoCount()
{
return $this->photos()->whereIn('album_id', DbHelper::getAlbumIDsForCurrentUser())->count();
}
public function photos()
{
return $this->belongsToMany(Photo::class, 'photo_labels');
}
public function thumbnailUrl($thumbnailName)
{
$photo = $this->photos()
->whereIn('album_id', DbHelper::getAlbumIDsForCurrentUser())
->inRandomOrder()
->first();
if (!is_null($photo))
{
return $photo->album->getAlbumSource()->getUrlToPhoto($photo, $thumbnailName);
}
// Rotate standard images
$images = [
asset('themes/base/images/empty-album-1.jpg'),
asset('themes/base/images/empty-album-2.jpg'),
asset('themes/base/images/empty-album-3.jpg')
];
return $images[rand(0, count($images) - 1)];
}
public function url()
{
return route('viewLabel', $this->url_alias);
}
}

View File

@ -1,41 +0,0 @@
<?php
namespace App\Mail;
use App\EmailLog;
use Illuminate\Mail\Mailable;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\HtmlString;
abstract class MailableBase extends Mailable
{
public function buildEmailLog()
{
// Build the e-mail
$this->build();
// Get the current user for the ID
$currentUser = Auth::user();
// Build the body so we can use it as a string
$bodies = $this->buildView();
/** @var HtmlString $html */
$html = $bodies['html'];
/** @var HtmlString $text */
$text = $bodies['text'];
return new EmailLog([
'sender_user_id' => !is_null($currentUser) ? $currentUser->id : null,
'sender_name' => $this->from[0]['name'],
'sender_address' => $this->from[0]['address'],
'to_addresses' => json_encode($this->to),
'cc_addresses' => json_encode($this->cc),
'bcc_addresses' => json_encode($this->bcc),
'subject' => $this->subject,
'body_plain' => $text->toHtml(),
'body_html' => $html->toHtml()
]);
}
}

View File

@ -1,55 +0,0 @@
<?php
namespace App\Mail;
use App\Album;
use App\Facade\Theme;
use App\Photo;
use App\PhotoComment;
use App\User;
use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
class ModeratePhotoComment extends MailableBase
{
use Queueable, SerializesModels;
private $album;
private $comment;
private $photo;
private $user;
/**
* Create a new message instance.
*
* @return void
*/
public function __construct(User $user, Album $album, Photo $photo, PhotoComment $comment)
{
$this->user = $user;
$this->album = $album;
$this->photo = $photo;
$this->comment = $comment;
}
/**
* Build the message.
*
* @return $this
*/
public function build()
{
$subject = trans('email.moderate_photo_comment_subject', ['album_name' => $this->album->name]);
return $this
->subject($subject)
->markdown(Theme::viewName('email.moderate_photo_comment'))
->with([
'album' => $this->album,
'comment' => $this->comment,
'photo' => $this->photo,
'subject' => $subject,
'user' => $this->user
]);
}
}

View File

@ -1,59 +0,0 @@
<?php
namespace App\Mail;
use App\Album;
use App\Facade\Theme;
use App\Photo;
use App\PhotoComment;
use App\User;
use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
/**
* E-mail notification to the owner of an album that is sent when a new comment has been approved in their album.
* @package App\Mail
*/
class PhotoCommentApproved extends MailableBase
{
use Queueable, SerializesModels;
private $album;
private $comment;
private $photo;
private $user;
/**
* Create a new message instance.
*
* @return void
*/
public function __construct(User $user, Album $album, Photo $photo, PhotoComment $comment)
{
$this->user = $user;
$this->album = $album;
$this->photo = $photo;
$this->comment = $comment;
}
/**
* Build the message.
*
* @return $this
*/
public function build()
{
$subject = trans('email.photo_comment_approved_subject', ['album_name' => $this->album->name]);
return $this
->subject($subject)
->markdown(Theme::viewName('email.photo_comment_approved'))
->with([
'album' => $this->album,
'comment' => $this->comment,
'photo' => $this->photo,
'subject' => $subject,
'user' => $this->user
]);
}
}

View File

@ -1,59 +0,0 @@
<?php
namespace App\Mail;
use App\Album;
use App\Facade\Theme;
use App\Photo;
use App\PhotoComment;
use App\User;
use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
/**
* E-mail notification to the poster of a comment that is sent when it has been approved.
* @package App\Mail
*/
class PhotoCommentApprovedUser extends MailableBase
{
use Queueable, SerializesModels;
private $album;
private $comment;
private $photo;
private $user;
/**
* Create a new message instance.
*
* @return void
*/
public function __construct(User $user, Album $album, Photo $photo, PhotoComment $comment)
{
$this->user = $user;
$this->album = $album;
$this->photo = $photo;
$this->comment = $comment;
}
/**
* Build the message.
*
* @return $this
*/
public function build()
{
$subject = trans('email.photo_comment_approved_user_subject', ['album_name' => $this->album->name]);
return $this
->subject($subject)
->markdown(Theme::viewName('email.photo_comment_approved_user'))
->with([
'album' => $this->album,
'comment' => $this->comment,
'photo' => $this->photo,
'subject' => $subject,
'user' => $this->user
]);
}
}

View File

@ -1,59 +0,0 @@
<?php
namespace App\Mail;
use App\Album;
use App\Facade\Theme;
use App\Photo;
use App\PhotoComment;
use App\User;
use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
/**
* E-mail notification to the poster of a comment that is sent when it has been approved.
* @package App\Mail
*/
class PhotoCommentRepliedTo extends MailableBase
{
use Queueable, SerializesModels;
private $album;
private $comment;
private $photo;
private $user;
/**
* Create a new message instance.
*
* @return void
*/
public function __construct(User $user, Album $album, Photo $photo, PhotoComment $comment)
{
$this->user = $user;
$this->album = $album;
$this->photo = $photo;
$this->comment = $comment;
}
/**
* Build the message.
*
* @return $this
*/
public function build()
{
$subject = trans('email.photo_comment_replied_to_subject', ['album_name' => $this->album->name]);
return $this
->subject($subject)
->markdown(Theme::viewName('email.photo_comment_replied_to'))
->with([
'album' => $this->album,
'comment' => $this->comment,
'photo' => $this->photo,
'subject' => $subject,
'user' => $this->user
]);
}
}

View File

@ -1,48 +0,0 @@
<?php
namespace App\Mail;
use App\Facade\Theme;
use App\Facade\UserConfig;
use App\User;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class ResetMyPassword extends MailableBase
{
use Queueable, SerializesModels;
private $token;
private $user;
/**
* Create a new message instance.
*
* @return void
*/
public function __construct(User $user, $token)
{
$this->user = $user;
$this->token = $token;
}
/**
* Build the message.
*
* @return $this
*/
public function build()
{
$subject = trans('email.reset_my_password_subject', ['app_name' => UserConfig::get('app_name')]);
return $this
->subject($subject)
->markdown(Theme::viewName('email.reset_my_password'))
->with([
'subject' => $subject,
'token' => $this->token,
'user' => $this->user
]);
}
}

View File

@ -8,9 +8,6 @@ use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
/**
* NOTE: This does not need converting to a notification. It should always be sent immediately and not queued.
*/
class TestMailConfig extends Mailable
{
use Queueable, SerializesModels;
@ -40,7 +37,7 @@ class TestMailConfig extends Mailable
return $this
->subject($subject)
->markdown(Theme::viewName('email.test_email'))
->view(Theme::viewName('email.test_email'))
->with(['subject' => $subject]);
}
}

View File

@ -6,9 +6,11 @@ use App\Facade\Theme;
use App\Facade\UserConfig;
use App\User;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Contracts\Queue\ShouldQueue;
class UserActivationRequired extends MailableBase
class UserActivationRequired extends Mailable
{
use Queueable, SerializesModels;
@ -35,7 +37,7 @@ class UserActivationRequired extends MailableBase
return $this
->subject($subject)
->markdown(Theme::viewName('email.user_activation_required'))
->view(Theme::viewName('email.user_activation_required'))
->with([
'subject' => $subject,
'user' => $this->user

View File

@ -1,44 +0,0 @@
<?php
namespace App\Mail;
use App\Facade\Theme;
use App\Facade\UserConfig;
use App\User;
use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
class UserChangeEmailRequired extends MailableBase
{
use Queueable, SerializesModels;
private $user;
/**
* Create a new message instance.
*
* @return void
*/
public function __construct(User $user)
{
$this->user = $user;
}
/**
* Build the message.
*
* @return $this
*/
public function build()
{
$subject = trans('email.change_email_required_subject', ['app_name' => UserConfig::get('app_name')]);
return $this
->subject($subject)
->markdown(Theme::viewName('email.user_change_email_required'))
->with([
'subject' => $subject,
'user' => $this->user
]);
}
}

View File

@ -1,47 +0,0 @@
<?php
namespace App\Mail;
use App\Facade\Theme;
use App\Facade\UserConfig;
use App\User;
use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
class UserSelfActivated extends MailableBase
{
use Queueable, SerializesModels;
private $createdUser;
private $user;
/**
* Create a new message instance.
*
* @return void
*/
public function __construct(User $user, User $createdUser)
{
$this->user = $user;
$this->createdUser = $createdUser;
}
/**
* Build the message.
*
* @return $this
*/
public function build()
{
$subject = trans('email.user_self_activated_subject', ['app_name' => UserConfig::get('app_name')]);
return $this
->subject($subject)
->markdown(Theme::viewName('email.user_self_activated'))
->with([
'subject' => $subject,
'user' => $this->user,
'created_user' => $this->createdUser
]);
}
}

View File

@ -1,13 +0,0 @@
<?php
namespace App\ModelObservers;
use App\Label;
class LabelObserver
{
public function creating(Label $label)
{
$label->generateAlias();
}
}

View File

@ -1,53 +0,0 @@
<?php
namespace App\Notifications;
use App\EmailLog;
use App\Facade\UserConfig;
use Illuminate\Mail\Mailable;
/**
* Enables a notification to use a Mailable to write to the database.
*/
trait DatabaseEmailNotification
{
/**
* Get the notification's delivery channels.
*
* @param mixed $notifiable
* @return array
*/
public function via($notifiable)
{
$drivers = [];
if (UserConfig::get('queue_emails'))
{
$drivers[] = QueueEmailDatabaseChannel::class;
}
else
{
$drivers[] = 'mail';
$drivers[] = SentEmailDatabaseChannel::class;
}
return $drivers;
}
/**
* Creates the EmailLog entry to write to the database.
* @param $notifiable
* @return EmailLog
*/
public function toEmailDatabase($notifiable)
{
return $this->toMail($notifiable)->buildEmailLog();
}
protected function setPropertiesOnMailable(Mailable $mailable, $notifiable)
{
// Set to and from properties accordingly
$mailable->from(UserConfig::get('sender_address'), UserConfig::get('sender_name'));
$mailable->to($notifiable->email, $notifiable->name);
}
}

View File

@ -1,22 +0,0 @@
<?php
namespace App\Notifications;
use App\EmailLog;
class EmailDatabaseWriterChannelBase
{
protected function writeToTable(EmailLog $logEntry, $shouldQueue = false)
{
if ($shouldQueue)
{
$logEntry->queued_at = new \DateTime();
}
else
{
$logEntry->sent_at = new \DateTime();
}
$logEntry->save();
}
}

View File

@ -1,48 +0,0 @@
<?php
namespace App\Notifications;
use App\Album;
use App\Mail\MailableBase;
use App\Mail\ModeratePhotoComment as ModeratePhotoCommentMailable;
use App\Photo;
use App\PhotoComment;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
class ModeratePhotoComment extends Notification
{
use Queueable;
use DatabaseEmailNotification;
private $album;
private $comment;
private $photo;
/**
* Create a new notification instance.
*
* @return void
*/
public function __construct(Album $album, Photo $photo, PhotoComment $comment)
{
$this->album = $album;
$this->photo = $photo;
$this->comment = $comment;
}
/**
* Get the mail representation of the notification.
*
* @param mixed $notifiable
* @return \Illuminate\Notifications\Messages\MailMessage|MailableBase
*/
public function toMail($notifiable)
{
$mailable = new ModeratePhotoCommentMailable($notifiable, $this->album, $this->photo, $this->comment);
$this->setPropertiesOnMailable($mailable, $notifiable);
return $mailable;
}
}

View File

@ -1,48 +0,0 @@
<?php
namespace App\Notifications;
use App\Album;
use App\Mail\MailableBase;
use App\Mail\PhotoCommentApproved as PhotoCommentApprovedMailable;
use App\Photo;
use App\PhotoComment;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
class PhotoCommentApproved extends Notification
{
use Queueable;
use DatabaseEmailNotification;
private $album;
private $comment;
private $photo;
/**
* Create a new notification instance.
*
* @return void
*/
public function __construct(Album $album, Photo $photo, PhotoComment $comment)
{
$this->album = $album;
$this->photo = $photo;
$this->comment = $comment;
}
/**
* Get the mail representation of the notification.
*
* @param mixed $notifiable
* @return \Illuminate\Notifications\Messages\MailMessage|MailableBase
*/
public function toMail($notifiable)
{
$mailable = new PhotoCommentApprovedMailable($notifiable, $this->album, $this->photo, $this->comment);
$this->setPropertiesOnMailable($mailable, $notifiable);
return $mailable;
}
}

View File

@ -1,48 +0,0 @@
<?php
namespace App\Notifications;
use App\Album;
use App\Mail\MailableBase;
use App\Mail\PhotoCommentApprovedUser as PhotoCommentApprovedUserMailable;
use App\Photo;
use App\PhotoComment;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
class PhotoCommentApprovedUser extends Notification
{
use Queueable;
use DatabaseEmailNotification;
private $album;
private $comment;
private $photo;
/**
* Create a new notification instance.
*
* @return void
*/
public function __construct(Album $album, Photo $photo, PhotoComment $comment)
{
$this->album = $album;
$this->photo = $photo;
$this->comment = $comment;
}
/**
* Get the mail representation of the notification.
*
* @param mixed $notifiable
* @return \Illuminate\Notifications\Messages\MailMessage|MailableBase
*/
public function toMail($notifiable)
{
$mailable = new PhotoCommentApprovedUserMailable($notifiable, $this->album, $this->photo, $this->comment);
$this->setPropertiesOnMailable($mailable, $notifiable);
return $mailable;
}
}

View File

@ -1,48 +0,0 @@
<?php
namespace App\Notifications;
use App\Album;
use App\Mail\MailableBase;
use App\Mail\PhotoCommentRepliedTo as PhotoCommentRepliedToMailable;
use App\Photo;
use App\PhotoComment;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
class PhotoCommentRepliedTo extends Notification
{
use Queueable;
use DatabaseEmailNotification;
private $album;
private $comment;
private $photo;
/**
* Create a new notification instance.
*
* @return void
*/
public function __construct(Album $album, Photo $photo, PhotoComment $comment)
{
$this->album = $album;
$this->photo = $photo;
$this->comment = $comment;
}
/**
* Get the mail representation of the notification.
*
* @param mixed $notifiable
* @return \Illuminate\Notifications\Messages\MailMessage|MailableBase
*/
public function toMail($notifiable)
{
$mailable = new PhotoCommentRepliedToMailable($notifiable, $this->album, $this->photo, $this->comment);
$this->setPropertiesOnMailable($mailable, $notifiable);
return $mailable;
}
}

View File

@ -1,24 +0,0 @@
<?php
namespace App\Notifications;
use App\EmailLog;
use Illuminate\Notifications\Notification;
class QueueEmailDatabaseChannel extends EmailDatabaseWriterChannelBase
{
/**
* Send the given notification.
*
* @param mixed $notifiable
* @param \Illuminate\Notifications\Notification $notification
* @return void
*/
public function send($notifiable, Notification $notification)
{
/** @var EmailLog $logEntry */
$logEntry = $notification->toEmailDatabase($notifiable);
$this->writeToTable($logEntry, true);
}
}

View File

@ -1,47 +0,0 @@
<?php
namespace App\Notifications;
use App\Mail\MailableBase;
use App\Mail\ResetMyPassword;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
class ResetPassword extends Notification
{
use Queueable;
use DatabaseEmailNotification;
/**
* The password reset token.
*
* @var string
*/
public $token;
/**
* Create a notification instance.
*
* @param string $token
* @return void
*/
public function __construct($token)
{
$this->token = $token;
}
/**
* Get the mail representation of the notification.
*
* @param mixed $notifiable
* @return \Illuminate\Notifications\Messages\MailMessage|MailableBase
*/
public function toMail($notifiable)
{
$mailable = new ResetMyPassword($notifiable, $this->token);
$this->setPropertiesOnMailable($mailable, $notifiable);
return $mailable;
}
}

View File

@ -1,24 +0,0 @@
<?php
namespace App\Notifications;
use App\EmailLog;
use Illuminate\Notifications\Notification;
class SentEmailDatabaseChannel extends EmailDatabaseWriterChannelBase
{
/**
* Send the given notification.
*
* @param mixed $notifiable
* @param \Illuminate\Notifications\Notification $notification
* @return void
*/
public function send($notifiable, Notification $notification)
{
/** @var EmailLog $logEntry */
$logEntry = $notification->toEmailDatabase($notifiable);
$this->writeToTable($logEntry, false);
}
}

View File

@ -1,39 +0,0 @@
<?php
namespace App\Notifications;
use App\Mail\MailableBase;
use App\Mail\UserActivationRequired as UserActivationRequiredMailable;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
class UserActivationRequired extends Notification
{
use Queueable;
use DatabaseEmailNotification;
/**
* Create a new notification instance.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Get the mail representation of the notification.
*
* @param mixed $notifiable
* @return \Illuminate\Notifications\Messages\MailMessage|MailableBase
*/
public function toMail($notifiable)
{
$mailable = new UserActivationRequiredMailable($notifiable);
$this->setPropertiesOnMailable($mailable, $notifiable);
return $mailable;
}
}

View File

@ -1,39 +0,0 @@
<?php
namespace App\Notifications;
use App\Mail\MailableBase;
use App\Mail\UserChangeEmailRequired as UserChangeEmailRequiredMailable;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
class UserChangeEmailRequired extends Notification
{
use Queueable;
use DatabaseEmailNotification;
/**
* Create a new notification instance.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Get the mail representation of the notification.
*
* @param mixed $notifiable
* @return \Illuminate\Notifications\Messages\MailMessage|MailableBase
*/
public function toMail($notifiable)
{
$mailable = new UserChangeEmailRequiredMailable($notifiable);
$this->setPropertiesOnMailable($mailable, $notifiable);
return $mailable;
}
}

View File

@ -1,42 +0,0 @@
<?php
namespace App\Notifications;
use App\Mail\MailableBase;
use App\Mail\UserSelfActivated as UserSelfActivatedMailable;
use App\User;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
class UserSelfActivated extends Notification
{
use Queueable;
use DatabaseEmailNotification;
private $createdUser;
/**
* Create a new notification instance.
*
* @return void
*/
public function __construct(User $createdUser)
{
$this->createdUser = $createdUser;
}
/**
* Get the mail representation of the notification.
*
* @param mixed $notifiable
* @return \Illuminate\Notifications\Messages\MailMessage|MailableBase
*/
public function toMail($notifiable)
{
$mailable = new UserSelfActivatedMailable($notifiable, $this->createdUser);
$this->setPropertiesOnMailable($mailable, $notifiable);
return $mailable;
}
}

View File

@ -2,7 +2,6 @@
namespace App;
use App\AlbumSources\IAlbumSource;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Notifications\Notifiable;
@ -32,10 +31,6 @@ class Photo extends Model
'width',
'height',
'is_analysed',
'raw_exif_data',
'aperture_fnumber',
'iso_number',
'shutter_speed',
'created_at',
'updated_at'
];
@ -53,72 +48,11 @@ class Photo extends Model
return $this->belongsTo(Album::class);
}
public function comments()
{
return $this->hasMany(PhotoComment::class);
}
public function exifUrl()
{
return route('viewExifData', [
'albumUrlAlias' => $this->album->url_path,
'photoFilename' => $this->storage_file_name
]);
}
public function labelIDs()
{
$labelIDs = [];
foreach ($this->labels()->orderBy('name')->get() as $label)
{
$labelIDs[] = $label->id;
}
return implode(',', $labelIDs);
}
public function labels()
{
return $this->belongsToMany(Label::class, 'photo_labels');
}
public function moderateCommentUrl($commentID = -1)
{
return route('moderatePhotoComment', [
'albumUrlAlias' => $this->album->url_path,
'photoFilename' => $this->storage_file_name,
'commentID' => $commentID
]);
}
public function postCommentUrl()
{
return route('postPhotoComment', [
'albumUrlAlias' => $this->album->url_path,
'photoFilename' => $this->storage_file_name
]);
}
public function replyToCommentFormUrl($commentID = -1)
{
return route('replyPhotoComment', [
'albumUrlAlias' => $this->album->url_path,
'photoFilename' => $this->storage_file_name,
'commentID' => $commentID
]);
}
public function thumbnailUrl($thumbnailName = null, $cacheBust = true)
{
/** @var IAlbumSource $source */
$source = $this->album->getAlbumSource();
$sourceConfiguration = $source->getConfiguration();
$url = $this->album->getAlbumSource()->getUrlToPhoto($this, $thumbnailName);
$url = $source->getUrlToPhoto($this, $thumbnailName);
// Cache busting doesn't work with S3 signed URLs
if ($cacheBust && !$sourceConfiguration->s3_signed_urls)
if ($cacheBust)
{
// Append the timestamp of the last update to avoid browser caching
$theDate = is_null($this->updated_at) ? $this->created_at : $this->updated_at;

View File

@ -1,91 +0,0 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class PhotoComment extends Model
{
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'name',
'email',
'comment'
];
public function authorDisplayName()
{
return is_null($this->createdBy) ? $this->name : $this->createdBy->name;
}
public function authorEmail()
{
return is_null($this->createdBy) ? $this->email : $this->createdBy->email;
}
public function children()
{
return $this->hasMany(PhotoComment::class, 'parent_comment_id');
}
public function createdBy()
{
return $this->belongsTo(User::class, 'created_user_id');
}
public function depth()
{
$depth = 0;
$current = $this;
while (!is_null($current->parent))
{
$current = $current->parent;
$depth++;
}
return $depth;
}
public function isApproved()
{
return (!is_null($this->approved_at) && is_null($this->rejected_at));
}
public function isModerated()
{
return (!is_null($this->approved_at) || !is_null($this->rejected_at));
}
public function isRejected()
{
return (!is_null($this->rejected_at) && is_null($this->approved_at));
}
public function parent()
{
return $this->belongsTo(PhotoComment::class, 'parent_comment_id');
}
public function photo()
{
return $this->belongsTo(Photo::class);
}
public function textAsHtml()
{
$start = '<p>';
$end = '</p>';
$isHtml = (
strlen($this->comment) > (strlen($start) + strlen($end)) && // text contains both our start + end string
strtolower(substr($this->comment, 0, strlen($start))) == strtolower($start) && // text starts with our start string
strtolower(substr($this->comment, strlen($this->comment) - strlen($end))) == strtolower($end) // text ends with our end string
);
return $isHtml ? $this->comment : sprintf('<p>%s</p>', $this->comment);
}
}

View File

@ -3,9 +3,7 @@
namespace App\Policies;
use App\Album;
use App\Facade\UserConfig;
use App\Group;
use App\Helpers\PermissionsHelper;
use App\Permission;
use App\User;
use Illuminate\Auth\Access\HandlesAuthorization;
@ -47,7 +45,13 @@ class AlbumPolicy
return true;
}
return $this->userHasPermission($user, $album, 'change-photo-metadata');
// Get the edit permission
$permission = Permission::where([
'section' => 'album',
'description' => 'change-photo-metadata'
])->first();
return $this->userHasPermission($user, $album, $permission);
}
public function delete(User $user, Album $album)
@ -58,7 +62,13 @@ class AlbumPolicy
return true;
}
return $this->userHasPermission($user, $album, 'delete');
// Get the edit permission
$permission = Permission::where([
'section' => 'album',
'description' => 'delete'
])->first();
return $this->userHasPermission($user, $album, $permission);
}
public function deletePhotos(User $user, Album $album)
@ -69,7 +79,13 @@ class AlbumPolicy
return true;
}
return $this->userHasPermission($user, $album, 'delete-photos');
// Get the edit permission
$permission = Permission::where([
'section' => 'album',
'description' => 'delete-photos'
])->first();
return $this->userHasPermission($user, $album, $permission);
}
public function edit(User $user, Album $album)
@ -80,7 +96,13 @@ class AlbumPolicy
return true;
}
return $this->userHasPermission($user, $album, 'edit');
// Get the edit permission
$permission = Permission::where([
'section' => 'album',
'description' => 'edit'
])->first();
return $this->userHasPermission($user, $album, $permission);
}
public function manipulatePhotos(User $user, Album $album)
@ -91,35 +113,13 @@ class AlbumPolicy
return true;
}
return $this->userHasPermission($user, $album, 'manipulate-photos');
}
// Get the edit permission
$permission = Permission::where([
'section' => 'album',
'description' => 'manipulate-photos'
])->first();
public function moderateComments(User $user, Album $album)
{
if ($user->id == $album->user_id)
{
// The album's owner and can do everything
return true;
}
return $this->userHasPermission($user, $album, 'moderate-comments');
}
public function postComment(User $user, Album $album)
{
if ($user->id == $album->user_id)
{
// The album's owner and can do everything
return true;
}
// Don't allow comments to be posted if anonymous user, and anonymous comments disabled
if ($user->isAnonymous() && !UserConfig::get('allow_photo_comments_anonymous'))
{
return false;
}
return $this->userHasPermission($user, $album, 'post-comment');
return $this->userHasPermission($user, $album, $permission);
}
public function uploadPhotos(User $user, Album $album)
@ -130,7 +130,13 @@ class AlbumPolicy
return true;
}
return $this->userHasPermission($user, $album, 'upload-photos');
// Get the edit permission
$permission = Permission::where([
'section' => 'album',
'description' => 'upload-photos'
])->first();
return $this->userHasPermission($user, $album, $permission);
}
public function view(User $user, Album $album)
@ -141,12 +147,56 @@ class AlbumPolicy
return true;
}
return $this->userHasPermission($user, $album, 'view');
// Get the edit permission
$permission = Permission::where([
'section' => 'album',
'description' => 'view'
])->first();
return $this->userHasPermission($user, $album, $permission);
}
private function userHasPermission(User $user, Album $album, $permission)
private function userHasPermission(User $user, Album $album, Permission $permission)
{
$helper = new PermissionsHelper();
return $helper->userCan_Album($album, $user, $permission);
if ($user->isAnonymous())
{
$query = Album::query()->join('album_anonymous_permissions', 'album_anonymous_permissions.album_id', '=', 'albums.id')
->join('permissions', 'permissions.id', '=', 'album_anonymous_permissions.permission_id')
->where([
['albums.id', $album->id],
['permissions.id', $permission->id]
]);
return $query->count() > 0;
}
// If any of the user's groups are granted the permission
/** @var Group $group */
foreach ($user->groups as $group)
{
$groupPermission = $album->groupPermissions()->where([
'group_id' => $group->id,
'permission_id' => $permission->id
])->first();
if (!is_null($groupPermission))
{
return true;
}
}
// If the user is directly granted the permission
$userPermission = $album->userPermissions()->where([
'user_id' => $user->id,
'permission_id' => $permission->id
])->first();
if (!is_null($userPermission))
{
return true;
}
// Nope, no permission
return false;
}
}

View File

@ -61,37 +61,4 @@ class PhotoPolicy
return $user->can('manipulate-photos', $photo->album);
}
public function moderateComments(User $user, Photo $photo)
{
if ($user->id == $photo->user_id)
{
// The photo's owner can do everything
return true;
}
return $user->can('moderate-comments', $photo->album);
}
public function postComment(User $user, Photo $photo)
{
if ($user->id == $photo->user_id)
{
// The photo's owner can do everything
return true;
}
return $user->can('post-comment', $photo->album);
}
public function view(User $user, Photo $photo)
{
if ($user->id == $photo->user_id)
{
// The photo's owner can do everything
return true;
}
return $user->can('view', $photo->album);
}
}

View File

@ -1,36 +0,0 @@
<?php
namespace App\Policies;
use App\Facade\UserConfig;
use App\User;
use Illuminate\Auth\Access\HandlesAuthorization;
class UserPolicy
{
use HandlesAuthorization;
/**
* Create a new policy instance.
*
* @return void
*/
public function __construct()
{
//
}
public function before($user, $ability)
{
if (!UserConfig::get('social_user_profiles'))
{
// Social profiles not enabled
return false;
}
}
public function view(User $user, User $userBeingAccessed)
{
return $userBeingAccessed->enable_profile_page;
}
}

View File

@ -8,9 +8,7 @@ use App\Helpers\ImageHelper;
use App\Helpers\MiscHelper;
use App\Helpers\ThemeHelper;
use App\Helpers\ValidationHelper;
use App\Label;
use App\ModelObservers\AlbumObserver;
use App\ModelObservers\LabelObserver;
use Illuminate\Database\QueryException;
use Illuminate\Mail\Mailer;
use Illuminate\Pagination\LengthAwarePaginator;
@ -38,10 +36,6 @@ class AppServiceProvider extends ServiceProvider
{
return $themeHelper;
});
$this->app->singleton('misc', function ($app)
{
return new MiscHelper();
});
$this->app->singleton('user_config', function ($app)
{
return new ConfigHelper();
@ -50,11 +44,9 @@ class AppServiceProvider extends ServiceProvider
Validator::extend('is_dir', (ValidationHelper::class . '@directoryExists'));
Validator::extend('dir_empty', (ValidationHelper::class . '@isDirectoryEmpty'));
Validator::extend('is_writeable', (ValidationHelper::class . '@isPathWriteable'));
Validator::extend('album_path_unique', (ValidationHelper::class . '@albumPathUnique'));
// Model observers
Album::observe(AlbumObserver::class);
Label::observe(LabelObserver::class);
// Configure our default pager
if (MiscHelper::isAppInstalled())

View File

@ -9,10 +9,10 @@ use App\Permission;
use App\Photo;
use App\Policies\AlbumPolicy;
use App\Policies\PhotoPolicy;
use App\Policies\UserPolicy;
use App\User;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use function GuzzleHttp\Psr7\mimetype_from_extension;
use Illuminate\Support\Facades\Gate;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
class AuthServiceProvider extends ServiceProvider
{
@ -28,8 +28,7 @@ class AuthServiceProvider extends ServiceProvider
*/
protected $policies = [
Album::class => AlbumPolicy::class,
Photo::class => PhotoPolicy::class,
User::class => UserPolicy::class
Photo::class => PhotoPolicy::class
];
/**
@ -53,22 +52,10 @@ class AuthServiceProvider extends ServiceProvider
{
return $this->userHasAdminPermission($user, 'manage-albums');
});
Gate::define('admin:manage-comments', function ($user)
{
return $this->userHasAdminPermission($user, 'manage-comments');
});
Gate::define('admin:manage-groups', function ($user)
{
return $this->userHasAdminPermission($user, 'manage-groups');
});
Gate::define('admin:manage-labels', function ($user)
{
return $this->userHasAdminPermission($user, 'manage-labels');
});
Gate::define('admin:manage-services', function ($user)
{
return $this->userHasAdminPermission($user, 'manage-services');
});
Gate::define('admin:manage-storage', function ($user)
{
return $this->userHasAdminPermission($user, 'manage-storage');
@ -87,20 +74,6 @@ class AuthServiceProvider extends ServiceProvider
return ($user->id == $photo->user_id);
});
Gate::define('photo.quick_upload', function($user)
{
$can = true;
$can &= $this->userHasAdminPermission($user, 'access');
$can &= $this->userHasAdminPermission($user, 'manage-albums');
return $can;
});
Gate::define('statistics.public-access', function ($user)
{
return UserConfig::get('public_statistics') || !$user->isAnonymous();
});
}
private function userHasAdminPermission(User $user, $permissionDescription)

View File

@ -1,37 +0,0 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class QueueItem extends Model
{
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'batch_reference',
'action_type',
'album_id',
'photo_id',
'queued_at',
'user_id'
];
public function album()
{
return $this->belongsTo(Album::class);
}
public function photo()
{
return $this->belongsTo(Photo::class);
}
public function user()
{
return $this->belongsTo(User::class);
}
}

View File

@ -1,400 +0,0 @@
<?php
namespace App\Services;
use App\Exceptions\BackblazeRetryException;
use Illuminate\Support\Facades\Log;
class BackblazeB2Service
{
/**
* The individual URL for the account to use to access the API
* @var string
*/
private $accountApiUrl;
/**
* ID of the account in Backblaze B2.
* @var string
*/
private $accountID;
/**
* The base URL for public access to the account's files.
* @var string
*/
private $downloadUrl;
/**
* Authorisation header for authenticating to the API.
* @var string
*/
private $authHeader;
/**
* Authorisation token for accessing the API post-authentication.
* @var string
*/
private $authToken;
/**
* ID of the bucket.
* @var string
*/
private $bucketId;
/**
* Type of the bucket.
* @var integer
*/
private $bucketType;
/**
* Configuration related to the Backblaze B2 service.
* @var \Illuminate\Config\Repository|mixed
*/
private $config;
/**
* Current file upload token.
* @var string
*/
private $uploadAuthToken;
/**
* Current upload URL.
* @var string
*/
private $uploadUrl;
public function __construct()
{
$this->config = config('services.backblaze_b2');
}
public function authorizeAccount($force = false)
{
if (empty($this->authToken) || $force)
{
$result = $this->sendRequest($this->config['auth_url']);
if (!isset($result->authorizationToken))
{
throw new \Exception('Authorisation to Backblaze failed. Is the API key correct?');
}
$this->authToken = $result->authorizationToken;
$this->accountApiUrl = $result->apiUrl;
$this->accountID = $result->accountId;
$this->downloadUrl = $result->downloadUrl;
}
}
public function deleteFile($fileID, $fileName)
{
$this->sendRequest(
sprintf('%s/b2api/v2/b2_delete_file_version', $this->accountApiUrl),
'POST',
[
'fileId' => $fileID,
'fileName' => $fileName
]
);
}
public function downloadFile($fileID)
{
return $this->sendRequest(
sprintf('%s/b2api/v2/b2_download_file_by_id?fileId=%s', $this->accountApiUrl, urlencode($fileID)),
'GET',
null,
[
'http_headers' => [
sprintf('Authorization: %s', $this->authToken)
],
'response_body_is_json' => false
]
);
}
public function getBucketType()
{
return $this->bucketType;
}
public function getDownloadAuthToken()
{
$result = $this->sendRequest(
sprintf('%s/b2api/v2/b2_get_download_authorization', $this->accountApiUrl),
'POST',
[
'bucketId' => $this->bucketId,
'validDurationInSeconds' => intval($this->config['download_token_lifetime']),
'fileNamePrefix' => ''
]
);
return $result->authorizationToken;
}
public function getDownloadUrl()
{
return $this->downloadUrl;
}
public function setBucketName($bucketName)
{
$bucketDetails = $this->getBucketDetailsFromName($bucketName);
$this->bucketId = $bucketDetails->bucketId;
$this->bucketType = $bucketDetails->bucketType;
}
public function setCredentials($applicationKeyID, $applicationKey)
{
$this->authHeader = sprintf('%s:%s', $applicationKeyID, $applicationKey);
}
public function uploadFile($pathToFileToUpload, $pathToStorage)
{
// Get a URL to upload our file to
list($uploadUrl, $authorizationToken) = $this->getUploadUrl();
if (empty($uploadUrl) || empty($authorizationToken))
{
throw new \Exception('No upload URL/authorization token returned from Backblaze B2.');
}
$exponentialBackoff = 1;
$numberOfRetries = 5; // this effectively gives us 31 seconds of retries (1+2+4+8+16)
$numberOfTimesTried = 0;
while ($numberOfTimesTried < $numberOfRetries)
{
try
{
return $this->uploadFileReal($pathToFileToUpload, $pathToStorage, $uploadUrl, $authorizationToken);
}
catch (BackblazeRetryException $ex)
{
sleep($exponentialBackoff);
// Get a new upload token
$this->uploadAuthToken = null;
$this->uploadUrl = null;
list($uploadUrl, $authorizationToken) = $this->getUploadUrl();
// Keep backing off
$exponentialBackoff *= $exponentialBackoff;
$numberOfTimesTried++;
}
}
}
private function uploadFileReal($pathToFileToUpload, $pathToStorage, $uploadUrl, $authorizationToken)
{
$fileSize = filesize($pathToFileToUpload);
$handle = fopen($pathToFileToUpload, 'r');
$fileContents = fread($handle, $fileSize);
fclose($handle);
$fileContentsSha1 = sha1_file($pathToFileToUpload);
$httpHeaders = [
sprintf('Authorization: %s', $authorizationToken),
'Content-Type: b2/x-auto',
sprintf('X-Bz-Content-Sha1: %s', $fileContentsSha1),
sprintf('X-Bz-File-Name: %s', urlencode($pathToStorage))
];
$result = $this->sendRequestReal(
$uploadUrl,
'POST',
$fileContents,
[
'http_headers' => $httpHeaders,
'post_body_is_json' => false
]
);
return $result->fileId;
}
private function getBucketDetailsFromName($bucketName)
{
$result = $this->sendRequest(
sprintf('%s/b2api/v2/b2_list_buckets', $this->accountApiUrl),
'POST',
[
'accountId' => $this->accountID,
'bucketName' => $bucketName
]
);
if (isset($result->buckets) && is_array($result->buckets) && count($result->buckets) >= 1)
{
return $result->buckets[0];
}
throw new \Exception(sprintf('The bucket \'%s\' was not found or your API key does not have access.', $bucketName));
}
private function getUploadUrl($alwaysGetNewToken = false)
{
if (is_null($this->uploadAuthToken) || $alwaysGetNewToken)
{
$result = $this->sendRequest(
sprintf('%s/b2api/v2/b2_get_upload_url', $this->accountApiUrl),
'POST',
['bucketId' => $this->bucketId]
);
$this->uploadAuthToken = $result->authorizationToken;
$this->uploadUrl = $result->uploadUrl;
}
return [$this->uploadUrl, $this->uploadAuthToken];
}
private function getBasicHttpClient($url, $method = 'GET', array $httpHeaders = [])
{
$httpHeaders = array_merge(
[
'Accept: application/json'
],
$httpHeaders
);
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_HTTPHEADER, $httpHeaders);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
switch (strtoupper($method))
{
case 'GET':
curl_setopt($ch, CURLOPT_HTTPGET, true);
break;
case 'POST':
curl_setopt($ch, CURLOPT_POST, true);
break;
}
return $ch;
}
private function sendRequest($url, $method = 'GET', $postData = null, array $postOptions = [])
{
$exponentialBackoff = 1;
$numberOfRetries = 5; // this effectively gives us 31 seconds of retries (1+2+4+8+16)
$numberOfTimesTried = 0;
while ($numberOfTimesTried < $numberOfRetries)
{
try
{
return $this->sendRequestReal($url, $method, $postData, $postOptions);
}
catch (BackblazeRetryException $ex)
{
// Clear the upload token if requested
if (isset($postOptions['clear_upload_token_on_retry']) && $postOptions['clear_upload_token_on_retry'])
{
$this->uploadAuthToken = null;
$this->uploadUrl = null;
}
// Keep backing off
sleep($exponentialBackoff);
$exponentialBackoff *= $exponentialBackoff;
$numberOfTimesTried++;
}
}
}
private function sendRequestReal($url, $method = 'GET', $postData = null, array $postOptions = [])
{
$postOptions = array_merge(
[
'authorization_token' => null,
'http_headers' => [],
'post_body_is_json' => true,
'response_body_is_json' => true
],
$postOptions
);
$httpHeaders = $postOptions['http_headers'];
// Some methods may need to override the authorization token used
if (empty($postOptions['authorization_token']))
{
// No override - work out which auth token to use
if (is_null($this->authToken))
{
// No auth token yet, use username/password
$httpHeaders[] = sprintf('Authorization: Basic %s', base64_encode($this->authHeader));
}
else
{
// Use the auth token we have
$httpHeaders[] = sprintf('Authorization: %s', $this->authToken);
}
}
else
{
// Override - use the auth token specified
$httpHeaders[] = sprintf('Authorization: %s', $postOptions['authorization_token']);
}
$ch = $this->getBasicHttpClient($url, $method, $httpHeaders);
if (!is_null($postData))
{
if ($postOptions['post_body_is_json'])
{
$postData = json_encode($postData);
}
curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);
}
Log::info(sprintf('%s: %s', strtoupper($method), $url));
Log::debug('HTTP headers:', $httpHeaders);
// Only log a post body if we have one and it's in JSON format (i.e. not a file upload)
if (!is_null($postData) && $postOptions['post_body_is_json'])
{
Log::debug($postData);
}
$result = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
Log::info(sprintf('Received HTTP code %d', $httpCode));
// Only log a result if we have one and it's in JSON format (i.e. not a file download)
if (!is_null($result) && $result !== false && $postOptions['response_body_is_json'])
{
Log::debug($result);
}
// According to the Backblaze B2 Protocol, if we get a 500/503, we should retry the request
if ($httpCode == 500 || $httpCode == 503)
{
throw new BackblazeRetryException(
$httpCode,
new \Exception(sprintf('Exception from Backblaze B2: %s', $result))
);
}
else if ($httpCode != 200 && $httpCode != 304)
{
throw new \Exception(sprintf('Exception from Backblaze B2: %s', $result));
}
curl_close($ch);
return $postOptions['response_body_is_json']
? json_decode($result)
: $result;
}
}

View File

@ -1,269 +0,0 @@
<?php
namespace App\Services;
use App\Exceptions\DropboxRetryException;
use App\Storage;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
class DropboxService
{
/**
* @var string
*/
private $accessToken;
/**
* Configuration related to the Backblaze B2 service.
* @var \Illuminate\Config\Repository|mixed
*/
private $config;
public function __construct()
{
$this->config = config('services.dropbox');
}
public function authoriseUrl(Storage $storage)
{
$service = $storage->externalService;
$redirectUrl = $this->callbackUrl();
return sprintf(
'%s?client_id=%s&response_type=code&redirect_uri=%s&state=%s',
$this->config['authorise_url'],
urlencode(decrypt($service->app_id)),
urlencode($redirectUrl),
urlencode(encrypt($storage->id))
);
}
public function callbackUrl()
{
return route('services.authoriseDropbox');
}
public function deleteFile($pathOnStorage)
{
$dropboxData = ['path' => $pathOnStorage];
$deleteResult = $this->sendRequest(
$this->config['delete_url'],
'POST',
$dropboxData,
[
'http_headers' => [
'Content-Type: application/json'
],
'post_body_is_json' => true
]
);
Log::debug('DropboxService - response to deleteFile.', ['response' => $deleteResult, 'path' => $pathOnStorage]);
}
public function downloadFile($pathOnStorage)
{
$dropboxArgs = ['path' => $pathOnStorage];
return $this->sendRequest(
$this->config['download_url'],
'POST',
null,
[
'http_headers' => [
sprintf('Dropbox-API-Arg: %s', json_encode($dropboxArgs)),
'Content-Type: application/octet-stream'
],
'post_body_is_json' => false,
'response_body_is_json' => false
]
);
}
public function handleAuthenticationResponse(Request $request, Storage $storage)
{
$authorisationCode = $request->query('code');
$storage->access_token = encrypt($this->convertAuthorisationCodeToToken($authorisationCode, $storage));
$storage->save();
return true;
}
/**
* @param string $accessToken
*/
public function setAccessToken(string $accessToken)
{
$this->accessToken = $accessToken;
}
public function uploadFile($pathToFileToUpload, $pathOnStorage)
{
$dropboxArgs = [
'path' => $pathOnStorage,
'mode' => 'overwrite',
'mute' => true
];
$shouldRetry = true;
while ($shouldRetry)
{
try
{
$uploadResult = $this->sendRequest(
$this->config['upload_url'],
'POST',
file_get_contents($pathToFileToUpload),
[
'http_headers' => [
sprintf('Dropbox-API-Arg: %s', json_encode($dropboxArgs)),
'Content-Type: application/octet-stream'
],
'post_body_is_json' => false
]
);
$shouldRetry = false;
Log::debug('DropboxService - response to uploadFile.', ['response' => $uploadResult, 'path' => $pathOnStorage]);
}
catch (DropboxRetryException $dre)
{
// Retry - leave shouldRetry as true
Log::debug('DropboxService - Dropbox reported a lock/rate limit and requested to retry');
sleep(2);
}
catch (\Exception $ex)
{
$shouldRetry = false;
Log::debug('DropboxService - exception in uploadFile.', ['exception' => $ex->getMessage()]);
}
}
}
private function convertAuthorisationCodeToToken($authorisationCode, Storage $storage)
{
$service = $storage->externalService;
$credentials = sprintf('%s:%s', decrypt($service->app_id), decrypt($service->app_secret));
$redirectUrl = $this->callbackUrl();
$httpHeaders = [
'Accept: application/json',
sprintf('Authorization: Basic %s', base64_encode($credentials))
];
$ch = curl_init($this->config['token_url']);
curl_setopt($ch, CURLOPT_HTTPHEADER, $httpHeaders);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, [
'code' => $authorisationCode,
'grant_type' => 'authorization_code',
'redirect_uri' => $redirectUrl
]);
$response = json_decode(curl_exec($ch));
if (is_null($response) || $response === false)
{
throw new \Exception('Unable to read the response from Dropbox');
}
else if (isset($response->error_description))
{
throw new \Exception(sprintf('Error from Dropbox: %s', $response->error_description));
}
return $response->access_token;
}
private function getBasicHttpClient($url, $method = 'GET', array $httpHeaders = [])
{
$httpHeaders = array_merge(
[
'Accept: application/json',
sprintf('Authorization: Bearer %s', $this->accessToken)
],
$httpHeaders
);
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_HTTPHEADER, $httpHeaders);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
switch (strtoupper($method))
{
case 'GET':
curl_setopt($ch, CURLOPT_HTTPGET, true);
break;
case 'POST':
curl_setopt($ch, CURLOPT_POST, true);
break;
}
return $ch;
}
private function sendRequest($url, $method = 'GET', $postData = null, array $postOptions = [])
{
$postOptions = array_merge(
[
'http_headers' => [],
'post_body_is_json' => true,
'response_body_is_json' => true
],
$postOptions
);
$httpHeaders = $postOptions['http_headers'];
$ch = $this->getBasicHttpClient($url, $method, $httpHeaders);
Log::info(sprintf('DropboxService - %s: %s', strtoupper($method), $url));
Log::debug('DropboxService - HTTP headers:', $httpHeaders);
if (!is_null($postData))
{
if ($postOptions['post_body_is_json'])
{
// Only log a post body if we have one and it's in JSON format (i.e. not a file upload)
Log::debug('DropboxService - Body: ', $postData);
$postData = json_encode($postData);
}
curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);
}
$result = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
Log::info(sprintf('DropboxService - Received HTTP code %d', $httpCode));
// Only log a result if we have one and it's in JSON format (i.e. not a file download)
if (!is_null($result) && $result !== false && $postOptions['response_body_is_json'])
{
Log::debug($result);
}
try
{
if ($httpCode != 200 && $httpCode != 304)
{
if ($httpCode == 429)
{
throw new DropboxRetryException($httpCode, new \Exception(sprintf('Exception from Dropbox: %s', $result)));
}
throw new \Exception(sprintf('Exception from Dropbox: %s', $result));
}
return $postOptions['response_body_is_json']
? json_decode($result)
: $result;
}
finally
{
curl_close($ch);
}
}
}

View File

@ -1,164 +0,0 @@
<?php
namespace App\Services;
class GiteaService
{
private $cacheFile = null;
private $config = [];
private $currentVersionNumber;
public function __construct(array $config = null, $currentVersionNumber = null)
{
// This class is used in the Bootstrapper to fetch release information, therefore
// we need to check if the Laravel helper functions are loaded before we use them
if (is_null($config) && function_exists('config'))
{
$this->config = config('services.gitea');
}
else
{
$this->config = $config;
}
if (is_null($currentVersionNumber) && function_exists('config'))
{
$this->currentVersionNumber = config('app.version');
}
else
{
$this->currentVersionNumber = $currentVersionNumber;
}
if (function_exists('storage_path'))
{
$this->cacheFile = storage_path('app/gitea_cache.txt');
}
}
public function checkForLatestRelease()
{
$cacheData = null;
if ($this->doesCacheExist())
{
// Get the etag from the cache
$cacheData = $this->getCacheData();
}
else
{
// Lookup and store the version information
$statusCode = -1;
$result = $this->getReleasesFromGitea($statusCode);
if ($statusCode == 200)
{
$releases = json_decode($result[1]);
$latestRelease = null;
foreach ($releases as $release)
{
if (is_null($latestRelease) || version_compare($release->tag_name, $latestRelease->tag_name) > 0)
{
$latestRelease = $release;
}
}
$cacheData = $this->setCacheData($latestRelease);
}
}
// GitHub compatibility
$cacheData->html_url = sprintf($this->config['releases_url'], $this->config['repo_owner'], $this->config['repo_name']);
return $cacheData;
}
public function getSpecificRelease($versionNumber)
{
$cacheData = null;
// Lookup and store the version information
$statusCode = -1;
$result = $this->getReleasesFromGitea($statusCode);
if ($statusCode == 200)
{
$releases = json_decode($result[1]);
$foundRelease = null;
foreach ($releases as $release)
{
if (version_compare($release->tag_name, $versionNumber) === 0)
{
return $release;
}
}
}
return null;
}
private function doesCacheExist()
{
$exists = file_exists($this->cacheFile);
if ($exists)
{
// Check modified time on the file
$stat = stat($this->cacheFile);
$diff = time() - $stat['mtime'];
if ($diff > $this->config['cache_time_seconds'])
{
$exists = false;
}
}
return $exists;
}
private function getCacheData()
{
return json_decode(file_get_contents($this->cacheFile));
}
private function getReleasesFromGitea(&$statusCode)
{
$httpHeaders = [
sprintf('User-Agent: aheathershaw/blue-twilight (v%s)', $this->currentVersionNumber)
];
if (isset($this->config['api_key']) && !empty($this->config['api_key']))
{
$httpHeaders[] = sprintf('Authorization: %s', $this->config['api_key']);
}
$apiUrl = sprintf('%s/repos/%s/%s/releases', $this->config['api_url'], $this->config['repo_owner'], $this->config['repo_name']);
$ch = curl_init($apiUrl);
curl_setopt($ch, CURLOPT_HTTPHEADER, $httpHeaders);
curl_setopt($ch, CURLOPT_HEADER, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$result = curl_exec($ch);
if ($result === false)
{
throw new \Exception(sprintf('Error from Gitea: %s', curl_error($ch)));
}
$statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
return explode("\r\n\r\n", $result, 2);
}
private function setCacheData($data)
{
if (!is_null($this->cacheFile))
{
file_put_contents($this->cacheFile, json_encode(get_object_vars($data)));
}
return $data;
}
}

View File

@ -1,100 +0,0 @@
<?php
namespace App\Services;
class GithubService
{
private $cacheFile = null;
private $config = [];
public function __construct()
{
$this->config = config('services.github');
$this->cacheFile = storage_path('app/github_cache.txt');
}
public function checkForLatestRelease()
{
$releaseInfo = [];
$etag = '';
if ($this->doesCacheExist())
{
// Get the etag from the cache
$cacheData = $this->getCacheData();
$etag = $cacheData->latest_release->etag;
$releaseInfo = $cacheData->latest_release->release_info;
}
// Lookup and store the version information
$statusCode = -1;
$result = $this->getLatestReleaseFromGithub($etag, $statusCode);
if ($statusCode == 200)
{
// Store the etag (in HTTP headers) for future reference
$matches = [];
$etag = '';
if (preg_match('/^etag: "(.+)"/mi', $result[0], $matches))
{
$etag = $matches[1];
}
$releaseInfo = json_decode($result[1]);
}
if (!empty($etag))
{
$this->setCacheData([
'latest_release' => [
'etag' => $etag,
'release_info' => $releaseInfo
]
]);
}
return $releaseInfo;
}
private function doesCacheExist()
{
return file_exists($this->cacheFile);
}
private function getCacheData()
{
return json_decode(file_get_contents($this->cacheFile));
}
private function getLatestReleaseFromGithub($etag = '', &$statusCode)
{
$httpHeaders = [
sprintf('User-Agent: pandy06269/blue-twilight (v%s)', config('app.version'))
];
if (!empty($etag))
{
$httpHeaders[] = sprintf('If-None-Match: "%s"', $etag);
}
$ch = curl_init($this->config['latest_release_url']);
curl_setopt($ch, CURLOPT_HTTPHEADER, $httpHeaders);
curl_setopt($ch, CURLOPT_HEADER, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$result = curl_exec($ch);
if ($result === false)
{
throw new \Exception(sprintf('Error from Github: %s', curl_error($ch)));
}
$statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
return explode("\r\n\r\n", $result, 2);
}
private function setCacheData(array $data)
{
file_put_contents($this->cacheFile, json_encode($data));
}
}

View File

@ -4,16 +4,15 @@ namespace App\Services;
use App\Album;
use App\AlbumSources\IAlbumSource;
use App\AlbumSources\IAnalysisQueueSource;
use App\Helpers\AnalysisQueueHelper;
use App\Helpers\FileHelper;
use App\Helpers\ImageHelper;
use App\Helpers\MiscHelper;
use App\Helpers\ThemeHelper;
use App\Photo;
use Symfony\Component\HttpFoundation\File\File;
class PhotoService
{
const METADATA_VERSION = 2;
const METADATA_VERSION = 1;
/**
* @var Album
@ -50,12 +49,10 @@ class PhotoService
$this->themeHelper = new ThemeHelper();
}
public function analyse($queueToken, $isReanalyse = false)
public function analyse($queueToken)
{
/** @var IAnalysisQueueSource $analysisQueueStorage */
$analysisQueueStorage = AnalysisQueueHelper::getStorageQueueSource();
$photoFile = $analysisQueueStorage->fetchItemFromAnalysisQueue($queueToken, $this->photo->storage_file_name);
$queuePath = FileHelper::getQueuePath($queueToken);
$photoFile = join(DIRECTORY_SEPARATOR, [$queuePath, $this->photo->storage_file_name]);
try
{
@ -71,23 +68,12 @@ class PhotoService
$this->photo->mime_type = $imageInfo['mime'];
// Read the Exif data
if (empty($this->photo->raw_exif_data))
{
$exifData = @exif_read_data($photoFile);
$isExifDataFound = ($exifData !== false && is_array($exifData));
$this->photo->raw_exif_data = $isExifDataFound ? base64_encode(serialize($exifData)) : '';
}
else
{
$exifData = unserialize(base64_decode($this->photo->raw_exif_data));
$isExifDataFound = ($exifData !== false && is_array($exifData));
}
$exifData = @exif_read_data($photoFile);
$isExifDataFound = ($exifData !== false && is_array($exifData));
$angleToRotate = 0;
// If Exif data contains an Orientation, ensure we rotate the original image as such (providing we don't
// currently have a metadata version - i.e. it hasn't been read and rotated already before)
if ($isExifDataFound && isset($exifData['Orientation']) && !$isReanalyse)
// If Exif data contains an Orientation, ensure we rotate the original image as such
if ($isExifDataFound && isset($exifData['Orientation']))
{
switch ($exifData['Orientation'])
{
@ -121,14 +107,10 @@ class PhotoService
if ($isExifDataFound)
{
$this->photo->metadata_version = self::METADATA_VERSION;
$this->photo->taken_at = $this->metadataDateTime($exifData, $this->photo->taken_at);
$this->photo->camera_make = $this->metadataCameraMake($exifData, $this->photo->camera_make);
$this->photo->camera_model = $this->metadataCameraModel($exifData, $this->photo->camera_model);
$this->photo->camera_software = $this->metadataCameraSoftware($exifData, $this->photo->camera_software);
$this->photo->aperture_fnumber = $this->metadataApertureFNumber($exifData, $this->photo->aperture_fnumber);
$this->photo->iso_number = $this->metadataIsoNumber($exifData, $this->photo->iso_number);
$this->photo->focal_length = $this->metadataFocalLength($exifData, $this->photo->focal_length);
$this->photo->shutter_speed = $this->metadataExposureTime($exifData, $this->photo->shutter_speed);
$this->photo->taken_at = $this->metadataDateTime($exifData);
$this->photo->camera_make = $this->metadataCameraMake($exifData);
$this->photo->camera_model = $this->metadataCameraModel($exifData);
$this->photo->camera_software = $this->metadataCameraSoftware($exifData);
}
$this->photo->is_analysed = true;
@ -145,11 +127,10 @@ class PhotoService
}
finally
{
// Remove the temporary file
@unlink($photoFile);
// Remove from the storage
$analysisQueueStorage->deleteItemFromAnalysisQueue($queueToken, $this->photo->storage_file_name);
// If the queue directory is now empty, get rid of it
FileHelper::deleteIfEmpty($queuePath);
}
}
@ -198,24 +179,6 @@ class PhotoService
$this->photo->delete();
}
public function downloadOriginalToFolder($folderPath)
{
$photoPath = join(DIRECTORY_SEPARATOR, [$folderPath, $this->photo->storage_file_name]);
$photoHandle = fopen($photoPath, 'w');
$stream = $this->albumSource->fetchPhotoContent($this->photo);
$stream->rewind();
while (!$stream->eof())
{
fwrite($photoHandle, $stream->read(4096));
}
fflush($photoHandle);
fclose($photoHandle);
$stream->close();
return $photoPath;
}
public function flip($horizontal, $vertical)
{
// First export the original photo from the storage provider
@ -301,22 +264,6 @@ class PhotoService
@unlink($photoPath);
}
private function calculateValueFromFraction($input)
{
$split = explode('/', $input);
if (count($split) != 2)
{
return $split;
}
$numerator = intval($split[0]);
$denominator = intval($split[1]);
return $denominator == 0
? 0
: ($numerator / $denominator);
}
private function downloadToTemporaryFolder()
{
$photoPath = tempnam(sys_get_temp_dir(), 'BlueTwilight_');
@ -324,7 +271,7 @@ class PhotoService
$stream = $this->albumSource->fetchPhotoContent($this->photo);
$stream->rewind();
while (!$stream->eof())
while (!$stream->feof())
{
fwrite($photoHandle, $stream->read(4096));
}
@ -335,54 +282,37 @@ class PhotoService
return $photoPath;
}
private function metadataApertureFNumber(array $exifData, $originalValue = null)
{
if (isset($exifData['FNumber']))
{
$value = $this->calculateValueFromFraction($exifData['FNumber']);
if (intval($value) === $value)
{
return sprintf('f/%d', $value);
}
return sprintf('f/%0.1f', $value);
}
return $originalValue;
}
private function metadataCameraMake(array $exifData, $originalValue = null)
private function metadataCameraMake(array $exifData)
{
if (isset($exifData['Make']))
{
return $exifData['Make'];
}
return $originalValue;
return null;
}
private function metadataCameraModel(array $exifData, $originalValue = null)
private function metadataCameraModel(array $exifData)
{
if (isset($exifData['Model']))
{
return $exifData['Model'];
}
return $originalValue;
return null;
}
private function metadataCameraSoftware(array $exifData, $originalValue = null)
private function metadataCameraSoftware(array $exifData)
{
if (isset($exifData['Software']))
{
return $exifData['Software'];
}
return $originalValue;
return null;
}
private function metadataDateTime(array $exifData, $originalValue = null)
private function metadataDateTime(array $exifData)
{
$dateTime = null;
if (isset($exifData['DateTimeOriginal']))
@ -394,43 +324,11 @@ class PhotoService
$dateTime = $exifData['DateTime'];
}
if (is_null($dateTime))
if (!is_null($dateTime))
{
return $originalValue;
$dateTime = preg_replace('/^([\d]{4}):([\d]{2}):([\d]{2})/', '$1-$2-$3', $dateTime);
}
return preg_replace('/^([\d]{4}):([\d]{2}):([\d]{2})/', '$1-$2-$3', $dateTime);
}
private function metadataExposureTime(array $exifData, $originalValue = null)
{
if (isset($exifData['ExposureTime']))
{
$decimal = $this->calculateValueFromFraction($exifData['ExposureTime']);
$fraction = MiscHelper::decimalToFraction($decimal);
return sprintf('%d/%d', $fraction[0], $fraction[1]);
}
return $originalValue;
}
private function metadataFocalLength(array $exifData, $originalValue = null)
{
if (isset($exifData['FocalLength']))
{
return $this->calculateValueFromFraction($exifData['FocalLength']);
}
return $originalValue;
}
private function metadataIsoNumber(array $exifData, $originalValue = null)
{
if (isset($exifData['ISOSpeedRatings']))
{
return $exifData['ISOSpeedRatings'];
}
return $originalValue;
return $dateTime;
}
}

Some files were not shown because too many files have changed in this diff Show More