From 97a379dc4713cb9f84e07ebf57e0de61d295bea1 Mon Sep 17 00:00:00 2001 From: Nils Asmussen Date: Sat, 20 Aug 2016 15:17:11 +0200 Subject: [PATCH 1/6] Moved zip archive code into separate class. --- php/Modules/Album.php | 111 ++------------------------ php/Modules/Archive.php | 167 ++++++++++++++++++++++++++++++++++++++++ php/Modules/Photo.php | 1 - 3 files changed, 172 insertions(+), 107 deletions(-) create mode 100644 php/Modules/Archive.php diff --git a/php/Modules/Album.php b/php/Modules/Album.php index 143ac64..ca9456a 100644 --- a/php/Modules/Album.php +++ b/php/Modules/Album.php @@ -2,8 +2,6 @@ namespace Lychee\Modules; -use ZipArchive; - final class Album { private $albumIDs = null; @@ -236,41 +234,20 @@ final class Album { break; } - // Escape title - $zipTitle = $this->cleanZipName($zipTitle); - - $filename = LYCHEE_DATA . $zipTitle . '.zip'; - - // Create zip - $zip = new ZipArchive(); - if ($zip->open($filename, ZIPARCHIVE::CREATE)!==TRUE) { - Log::error(Database::get(), __METHOD__, __LINE__, 'Could not create ZipArchive'); - return false; - } + $archive = new Archive($zipTitle); - // Add photos to zip switch($this->albumIDs) { case 's': case 'f': case 'r': - $this->addPhotosToZip($zip, $zipTitle, $photos); - break; + if (!$archive->addPhotos($zipTitle, $photos)) return false; + break; default: - $this->addAlbumToZip($zip, $zipTitle, $this->albumIDs); + if (!$archive->addAlbum($zipTitle, $this->albumIDs)) return false; break; } - // Finish zip - $zip->close(); - - // Send zip - header("Content-Type: application/zip"); - header("Content-Disposition: attachment; filename=\"$zipTitle.zip\""); - header("Content-Length: " . filesize($filename)); - readfile($filename); - - // Delete zip - unlink($filename); + $archive->send(); // Call plugins Plugins::get()->activate(__METHOD__, 1, func_get_args()); @@ -279,84 +256,6 @@ final class Album { } - private function cleanZipName($name) { - - // Illicit chars - $badChars = array_merge( - array_map('chr', range(0,31)), - array("<", ">", ":", '"', "/", "\\", "|", "?", "*") - ); - - return str_replace($badChars, '', $name); - - } - - private function addAlbumToZip($zip, $path, $albumID) { - - // Fetch album title - $photos = Database::prepare(Database::get(), "SELECT title, url FROM ? WHERE album = '?'", array(LYCHEE_TABLE_PHOTOS, $albumID)); - - $this->addPhotosToZip($zip, $path, $photos); - - // Fetch subalbums - $query = Database::prepare(Database::get(), "SELECT id, title FROM ? WHERE parent = '?'", array(LYCHEE_TABLE_ALBUMS, $albumID)); - $albums = Database::execute(Database::get(), $query, __METHOD__, __LINE__); - - // Add them recursively - while($album = $albums->fetch_assoc()) { - $this->addAlbumToZip($zip, $path . '/' . $this->cleanZipName($album['title']), $album['id']); - } - - } - - private function addPhotosToZip($zip, $path, $query) { - - // Execute query - $photos = Database::execute(Database::get(), $query, __METHOD__, __LINE__); - - // Parse each path - $files = array(); - while ($photo = $photos->fetch_object()) { - - // Parse url - $photo->url = LYCHEE_UPLOADS_BIG . $photo->url; - - // Parse title - $photo->title = $this->cleanZipName($photo->title); - if (!isset($photo->title)||$photo->title==='') $photo->title = 'Untitled'; - - // Check if readable - if (!@is_readable($photo->url)) continue; - - // Get extension of image - $extension = getExtension($photo->url, false); - - // Set title for photo - $zipFileName = $path . '/' . $photo->title . $extension; - - // Check for duplicates - if (!empty($files)) { - $i = 1; - while (in_array($zipFileName, $files)) { - - // Set new title for photo - $zipFileName = $path . '/' . $photo->title . '-' . $i . $extension; - - $i++; - - } - } - - // Add to array - $files[] = $zipFileName; - - // Add photo to zip - $zip->addFile($photo->url, $zipFileName); - - } - - } - /** * @return boolean Returns true when successful. */ diff --git a/php/Modules/Archive.php b/php/Modules/Archive.php new file mode 100644 index 0000000..e29e880 --- /dev/null +++ b/php/Modules/Archive.php @@ -0,0 +1,167 @@ +title = $this->cleanZipName($title); + + $this->filename = LYCHEE_DATA . $this->title . '.zip'; + + // Create zip + $this->zip = new ZipArchive(); + if ($this->zip->open($this->filename, ZIPARCHIVE::CREATE)!==TRUE) { + Log::error(Database::get(), __METHOD__, __LINE__, 'Could not create ZipArchive'); + $this->zip = null; + } + + } + + public function __destruct() { + + if ($this->zip!==null) $this->zip->close(); + + unlink($this->filename); + + } + + /** + * Closes the zip file and sends it to the browser. + */ + public function send() { + + // Finish zip + $this->zip->close(); + $this->zip = null; + + // Send zip + header("Content-Type: application/zip"); + header("Content-Disposition: attachment; filename=\"" . $this->title . ".zip\""); + header("Content-Length: " . filesize($this->filename)); + readfile($this->filename); + + } + + /** + * Adds the given album including subalbums at given path to the zip archive. + * + * @param string $path the path in the zip archive + * @param int $albumID the id of the album + * @return true on success + */ + public function addAlbum($path, $albumID) { + + // Fetch photos + $query = Database::prepare(Database::get(), "SELECT title, url FROM ? WHERE album = '?'", array(LYCHEE_TABLE_PHOTOS, $albumID)); + + if (!$this->addPhotos($path, $query)) return false; + + // Fetch subalbums + $query = Database::prepare(Database::get(), "SELECT id, title FROM ? WHERE parent = '?'", array(LYCHEE_TABLE_ALBUMS, $albumID)); + $albums = Database::execute(Database::get(), $query, __METHOD__, __LINE__); + + if ($albums===false) return false; + + // Add them recursively + while($album = $albums->fetch_assoc()) { + if (!$this->addAlbum($path . '/' . $this->cleanZipName($album['title']), $album['id'])) return false; + } + + return true; + + } + + /** + * Adds the photos that are selected by the given query at given path to the zip archive. + * + * @param string $path the path in the zip archive + * @param mysqli_stmt $query the SQL query to execute + * @return true on success + */ + public function addPhotos($path, $query) { + + if ($this->zip===null) return false; + + $photos = Database::execute(Database::get(), $query, __METHOD__, __LINE__); + + if (!$photos) return false; + + // Parse each path + $files = array(); + while ($photo = $photos->fetch_object()) { + + // Parse url + $photo->url = LYCHEE_UPLOADS_BIG . $photo->url; + + // Parse title + $photo->title = $this->cleanZipName($photo->title); + if (!isset($photo->title)||$photo->title==='') $photo->title = 'Untitled'; + + // Check if readable + if (!@is_readable($photo->url)) continue; + + // Get extension of image + $extension = getExtension($photo->url, false); + + // Set title for photo + $zipFileName = $path . '/' . $photo->title . $extension; + + // Check for duplicates + if (!empty($files)) { + $i = 1; + while (in_array($zipFileName, $files)) { + + // Set new title for photo + $zipFileName = $path . '/' . $photo->title . '-' . $i . $extension; + + $i++; + + } + } + + // Add to array + $files[] = $zipFileName; + + // Add photo to zip + $this->zip->addFile($photo->url, $zipFileName); + + } + + return true; + + } + + private function cleanZipName($name) { + + // Illicit chars + $badChars = array_merge( + array_map('chr', range(0,31)), + array("<", ">", ":", '"', "/", "\\", "|", "?", "*") + ); + + return str_replace($badChars, '', $name); + + } + +} + +?> \ No newline at end of file diff --git a/php/Modules/Photo.php b/php/Modules/Photo.php index 59beb77..def43f0 100755 --- a/php/Modules/Photo.php +++ b/php/Modules/Photo.php @@ -2,7 +2,6 @@ namespace Lychee\Modules; -use ZipArchive; use Imagick; use ImagickPixel; From 7fd74462e23285e58e8e1f0b868669c8e6669b24 Mon Sep 17 00:00:00 2001 From: Nils Asmussen Date: Sat, 20 Aug 2016 15:59:44 +0200 Subject: [PATCH 2/6] Allow to download multiple albums. This commit adds a download item to the album and albumMulti contextMenu functions. Since the Archive class is already general enough, we can easily support to download multiple albums. This is especially nice, because we can now select individual albums. --- php/Access/Admin.php | 4 ++-- php/Access/Guest.php | 4 ++-- php/Modules/Album.php | 43 ++++++++++++++++---------------------- src/scripts/album.js | 11 ++++++---- src/scripts/contextMenu.js | 2 ++ src/scripts/header.js | 2 +- 6 files changed, 32 insertions(+), 34 deletions(-) diff --git a/php/Access/Admin.php b/php/Access/Admin.php index df622f6..fbd337c 100644 --- a/php/Access/Admin.php +++ b/php/Access/Admin.php @@ -334,9 +334,9 @@ final class Admin extends Access { private static function getAlbumArchiveAction() { - Validator::required(isset($_GET['albumID']), __METHOD__); + Validator::required(isset($_GET['albumIDs']), __METHOD__); - $album = new Album($_GET['albumID']); + $album = new Album($_GET['albumIDs']); $album->getArchive(); } diff --git a/php/Access/Guest.php b/php/Access/Guest.php index 7b988a2..44621df 100644 --- a/php/Access/Guest.php +++ b/php/Access/Guest.php @@ -140,9 +140,9 @@ final class Guest extends Access { private static function getAlbumArchiveAction() { - Validator::required(isset($_GET['albumID'], $_GET['password']), __METHOD__); + Validator::required(isset($_GET['albumIDs'], $_GET['password']), __METHOD__); - $album = new Album($_GET['albumID']); + $album = new Album($_GET['albumIDs']); if ($album->getPublic()&&$album->getDownloadable()) { diff --git a/php/Modules/Album.php b/php/Modules/Album.php index ca9456a..be0cd4d 100644 --- a/php/Modules/Album.php +++ b/php/Modules/Album.php @@ -182,7 +182,7 @@ final class Album { } /** - * Starts a download of an album. + * Starts a download of albums. * @return resource|boolean Sends a ZIP-file or returns false on failure. */ public function getArchive() { @@ -207,30 +207,12 @@ final class Album { $photos = Database::prepare(Database::get(), 'SELECT title, url FROM ? WHERE LEFT(id, 10) >= unix_timestamp(DATE_SUB(NOW(), INTERVAL 1 DAY)) GROUP BY checksum', array(LYCHEE_TABLE_PHOTOS)); $zipTitle = 'Recent'; break; - default: + case 0: + $photos = Database::prepare(Database::get(), 'SELECT title, url FROM ? WHERE album = 0', array(LYCHEE_TABLE_PHOTOS)); $zipTitle = 'Unsorted'; - - // Get title from database when album is not a SmartAlbum - if ($this->albumIDs!=0 && is_numeric($this->albumIDs)) { - - $query = Database::prepare(Database::get(), "SELECT title FROM ? WHERE id = '?' LIMIT 1", array(LYCHEE_TABLE_ALBUMS, $this->albumIDs)); - $album = Database::execute(Database::get(), $query, __METHOD__, __LINE__); - - if ($album===false) return false; - - // Get album object - $album = $album->fetch_object(); - - // Album not found? - if ($album===null) { - Log::error(Database::get(), __METHOD__, __LINE__, 'Could not find specified album'); - return false; - } - - // Set title - $zipTitle = $album->title; - - } + break; + default: + $zipTitle = 'Albums'; break; } @@ -240,10 +222,21 @@ final class Album { case 's': case 'f': case 'r': + case 0: if (!$archive->addPhotos($zipTitle, $photos)) return false; break; + default: - if (!$archive->addAlbum($zipTitle, $this->albumIDs)) return false; + // load titles from DB + $query = Database::prepare(Database::get(), "SELECT id, title FROM ? WHERE id IN (?)", array(LYCHEE_TABLE_ALBUMS, $this->albumIDs)); + $albums = Database::execute(Database::get(), $query, __METHOD__, __LINE__); + + if ($albums===false) return false; + + // add these albums to zip + while ($album = $albums->fetch_object()) { + if (!$archive->addAlbum($album->title, $album->id)) return false; + } break; } diff --git a/src/scripts/album.js b/src/scripts/album.js index e84aac9..5828c26 100644 --- a/src/scripts/album.js +++ b/src/scripts/album.js @@ -625,13 +625,16 @@ album.share = function(service) { } -album.getArchive = function(albumID) { +album.getArchive = function(albumIDs) { let link = '' - let url = `${ api.path }?function=Album::getArchive&albumID=${ albumID }` + let url = `${ api.path }?function=Album::getArchive&albumIDs=${ albumIDs.join(',') }` - if (location.href.indexOf('index.html')>0) link = location.href.replace(location.hash, '').replace('index.html', url) - else link = location.href.replace(location.hash, '') + url + var pos = location.href.indexOf('#') + link = pos!=-1 ? location.href.substring(0, pos) : location.href + + if (location.href.indexOf('index.html')>0) link = link.replace('index.html', url) + else link += url if (lychee.publicMode===true) link += `&password=${ encodeURIComponent(password.value) }` diff --git a/src/scripts/contextMenu.js b/src/scripts/contextMenu.js index 7aa67e6..85fdfda 100644 --- a/src/scripts/contextMenu.js +++ b/src/scripts/contextMenu.js @@ -115,6 +115,7 @@ contextMenu.album = function(albumID, e) { if (album.isSmartID(albumID)) return false let items = [ + { title: build.iconic('cloud-download') + 'Download', fn: () => album.getArchive([ albumID ]) }, { title: build.iconic('pencil') + 'Rename', fn: () => album.setTitle([ albumID ]) }, { title: build.iconic('collapse-left') + 'Merge', fn: () => { basicContext.close(); contextMenu.mergeAlbum(albumID, e) } }, { title: build.iconic('folder') + 'Move', fn: () => { basicContext.close(); contextMenu.moveAlbum([ albumID ], e) } }, @@ -136,6 +137,7 @@ contextMenu.albumMulti = function(albumIDs, e) { let autoMerge = (albumIDs.length>1 ? true : false) let items = [ + { title: build.iconic('cloud-download') + 'Download All', fn: () => album.getArchive(albumIDs) }, { title: build.iconic('pencil') + 'Rename All', fn: () => album.setTitle(albumIDs) }, { title: build.iconic('collapse-left') + 'Merge All', visible: autoMerge, fn: () => album.merge(albumIDs) }, { title: build.iconic('collapse-left') + 'Merge', visible: !autoMerge, fn: () => { basicContext.close(); contextMenu.mergeAlbum(albumIDs[0], e) } }, diff --git a/src/scripts/header.js b/src/scripts/header.js index 891813a..153080d 100644 --- a/src/scripts/header.js +++ b/src/scripts/header.js @@ -49,7 +49,7 @@ header.bind = function() { header.dom('.header__hostedwith') .on(eventName, function() { window.open(lychee.website) }) header.dom('#button_trash_album') .on(eventName, function() { album.delete([ album.getID() ]) }) header.dom('#button_trash') .on(eventName, function() { photo.delete([ photo.getID() ]) }) - header.dom('#button_archive') .on(eventName, function() { album.getArchive(album.getID()) }) + header.dom('#button_archive') .on(eventName, function() { album.getArchive([ album.getID() ]) }) header.dom('#button_star') .on(eventName, function() { photo.setStar([ photo.getID() ]) }) header.dom('#button_back_home') .on(eventName, function() { lychee.goto(album.getParent()) }) header.dom('#button_back') .on(eventName, function() { lychee.goto(album.getID()) }) From 9088c1ed2ca5449532a683c908bdb4099bd46245 Mon Sep 17 00:00:00 2001 From: Nils Asmussen Date: Sat, 20 Aug 2016 17:36:34 +0200 Subject: [PATCH 3/6] Renamed Photo::getArchive to Photo::getPhoto. --- docs/Plugins.md | 4 ++-- php/Access/Admin.php | 6 +++--- php/Access/Guest.php | 6 +++--- php/Modules/Photo.php | 4 ++-- src/scripts/contextMenu.js | 2 +- src/scripts/photo.js | 4 ++-- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/Plugins.md b/docs/Plugins.md index 046c2f4..03d506d 100644 --- a/docs/Plugins.md +++ b/docs/Plugins.md @@ -120,8 +120,8 @@ These hooks are called from `php/modules/Photo.php`. | Photo::get:after | | | Photo::getInfo:before | Lychee reads the metadata of an image | | Photo::getInfo:after | | -| Photo::getArchive:before | User downloads photo | -| Photo::getArchive:after | | +| Photo::getPhoto:before | User downloads photo | +| Photo::getPhoto:after | | | Photo::setTitle:before | User renames photo | | Photo::setTitle:after | | | Photo::setDescription:before | User sets description | diff --git a/php/Access/Admin.php b/php/Access/Admin.php index fbd337c..bc2f4a1 100644 --- a/php/Access/Admin.php +++ b/php/Access/Admin.php @@ -61,7 +61,7 @@ final class Admin extends Access { // $_GET functions case 'Album::getArchive': self::getAlbumArchiveAction(); break; - case 'Photo::getArchive': self::getPhotoArchiveAction(); break; + case 'Photo::getPhoto': self::getPhotoFileAction(); break; } @@ -341,12 +341,12 @@ final class Admin extends Access { } - private static function getPhotoArchiveAction() { + private static function getPhotoFileAction() { Validator::required(isset($_GET['photoID']), __METHOD__); $photo = new Photo($_GET['photoID']); - $photo->getArchive(); + $photo->getPhoto(); } diff --git a/php/Access/Guest.php b/php/Access/Guest.php index 44621df..8f0f72b 100644 --- a/php/Access/Guest.php +++ b/php/Access/Guest.php @@ -32,7 +32,7 @@ final class Guest extends Access { // $_GET functions case 'Album::getArchive': self::getAlbumArchiveAction(); break; - case 'Photo::getArchive': self::getPhotoArchiveAction(); break; + case 'Photo::getPhoto': self::getPhotoFileAction(); break; } @@ -159,7 +159,7 @@ final class Guest extends Access { } - private static function getPhotoArchiveAction() { + private static function getPhotoFileAction() { Validator::required(isset($_GET['photoID'], $_GET['password']), __METHOD__); @@ -171,7 +171,7 @@ final class Guest extends Access { if ($pgP===2) { // Photo Public - $photo->getArchive(); + $photo->getPhoto(); } else { diff --git a/php/Modules/Photo.php b/php/Modules/Photo.php index def43f0..eaa4f9a 100755 --- a/php/Modules/Photo.php +++ b/php/Modules/Photo.php @@ -889,9 +889,9 @@ final class Photo { /** * Starts a download of a photo. - * @return resource|boolean Sends a ZIP-file or returns false on failure. + * @return resource|boolean Sends the photo or returns false on failure. */ - public function getArchive() { + public function getPhoto() { // Check dependencies Validator::required(isset($this->photoIDs), __METHOD__); diff --git a/src/scripts/contextMenu.js b/src/scripts/contextMenu.js index 85fdfda..938052e 100644 --- a/src/scripts/contextMenu.js +++ b/src/scripts/contextMenu.js @@ -310,7 +310,7 @@ contextMenu.photoMore = function(photoID, e) { let items = [ { title: build.iconic('fullscreen-enter') + 'Full Photo', fn: () => window.open(photo.getDirectLink()) }, - { title: build.iconic('cloud-download') + 'Download', visible: showDownload, fn: () => photo.getArchive(photoID) } + { title: build.iconic('cloud-download') + 'Download', visible: showDownload, fn: () => photo.getPhoto(photoID) } ] basicContext.show(items, e.originalEvent) diff --git a/src/scripts/photo.js b/src/scripts/photo.js index e0b2586..7c14efa 100644 --- a/src/scripts/photo.js +++ b/src/scripts/photo.js @@ -633,10 +633,10 @@ photo.share = function(photoID, service) { } -photo.getArchive = function(photoID) { +photo.getPhoto = function(photoID) { let link - let url = `${ api.path }?function=Photo::getArchive&photoID=${ photoID }` + let url = `${ api.path }?function=Photo::getPhoto&photoID=${ photoID }` if (location.href.indexOf('index.html')>0) link = location.href.replace(location.hash, '').replace('index.html', url) else link = location.href.replace(location.hash, '') + url From 47ed1775c9fac90aaf0ab250f18209af7f294449 Mon Sep 17 00:00:00 2001 From: Nils Asmussen Date: Sun, 21 Aug 2016 14:27:51 +0200 Subject: [PATCH 4/6] Support downloading of multiple photos. This commit adds a contextmenu entry to download a single and multiple photos via selection. So far, not for guests. --- php/Access/Admin.php | 10 ++++++++++ php/Modules/Photo.php | 27 +++++++++++++++++++++++++++ src/scripts/contextMenu.js | 2 ++ src/scripts/photo.js | 14 ++++++++++++++ 4 files changed, 53 insertions(+) diff --git a/php/Access/Admin.php b/php/Access/Admin.php index bc2f4a1..30fde25 100644 --- a/php/Access/Admin.php +++ b/php/Access/Admin.php @@ -61,6 +61,7 @@ final class Admin extends Access { // $_GET functions case 'Album::getArchive': self::getAlbumArchiveAction(); break; + case 'Photo::getArchive': self::getPhotoArchiveAction(); break; case 'Photo::getPhoto': self::getPhotoFileAction(); break; } @@ -341,6 +342,15 @@ final class Admin extends Access { } + private static function getPhotoArchiveAction() { + + Validator::required(isset($_GET['photoIDs']), __METHOD__); + + $photo = new Photo($_GET['photoIDs']); + $photo->getArchive(); + + } + private static function getPhotoFileAction() { Validator::required(isset($_GET['photoID']), __METHOD__); diff --git a/php/Modules/Photo.php b/php/Modules/Photo.php index eaa4f9a..006baec 100755 --- a/php/Modules/Photo.php +++ b/php/Modules/Photo.php @@ -948,6 +948,33 @@ final class Photo { } + /** + * Starts a download of photos. + * @return resource|boolean Sends a ZIP-file or returns false on failure. + */ + public function getArchive() { + + // Check dependencies + Validator::required(isset($this->photoIDs), __METHOD__); + + // Call plugins + Plugins::get()->activate(__METHOD__, 0, func_get_args()); + + $archive = new Archive('Photos'); + + $query = Database::prepare(Database::get(), 'SELECT title, url FROM ? WHERE id IN (?)', array(LYCHEE_TABLE_PHOTOS, $this->photoIDs)); + + if (!$archive->addPhotos('', $query)) return false; + + $archive->send(); + + // Call plugins + Plugins::get()->activate(__METHOD__, 1, func_get_args()); + + return true; + + } + /** * Sets the title of a photo. * @return boolean Returns true when successful. diff --git a/src/scripts/contextMenu.js b/src/scripts/contextMenu.js index 938052e..e5af8cc 100644 --- a/src/scripts/contextMenu.js +++ b/src/scripts/contextMenu.js @@ -244,6 +244,7 @@ contextMenu.photo = function(photoID, e) { // in order to keep the selection let items = [ + { title: build.iconic('cloud-download') + 'Download', fn: () => photo.getPhoto(photoID) }, { title: build.iconic('star') + 'Star', fn: () => photo.setStar([ photoID ]) }, { title: build.iconic('tag') + 'Tags', fn: () => photo.editTags([ photoID ]) }, { }, @@ -268,6 +269,7 @@ contextMenu.photoMulti = function(photoIDs, e) { // in order to keep the selection and multiselect let items = [ + { title: build.iconic('cloud-download') + 'Download All', fn: () => photo.getArchive(photoIDs) }, { title: build.iconic('star') + 'Star All', fn: () => photo.setStar(photoIDs) }, { title: build.iconic('tag') + 'Tag All', fn: () => photo.editTags(photoIDs) }, { }, diff --git a/src/scripts/photo.js b/src/scripts/photo.js index 7c14efa..0e606c7 100644 --- a/src/scripts/photo.js +++ b/src/scripts/photo.js @@ -647,6 +647,20 @@ photo.getPhoto = function(photoID) { } +photo.getArchive = function(photoIDs) { + + let link + let url = `${ api.path }?function=Photo::getArchive&photoIDs=${ photoIDs.join(',') }` + + if (location.href.indexOf('index.html')>0) link = location.href.replace(location.hash, '').replace('index.html', url) + else link = location.href.replace(location.hash, '') + url + + if (lychee.publicMode===true) link += `&password=${ encodeURIComponent(password.value) }` + + location.href = link + +} + photo.getDirectLink = function() { let url = '' From 95c99948d72e345088048aec6a6af7f1c9e87ec6 Mon Sep 17 00:00:00 2001 From: Nils Asmussen Date: Sun, 21 Aug 2016 14:54:31 +0200 Subject: [PATCH 5/6] Allow guests to download multiple items, too. For this, the album/photo contextmenus and multiselect is now enabled for guests, but limited to the download items. Additionally, Photo::getPublic has been extended to support multiple photos and also optimized to use just one DB query. --- php/Access/Guest.php | 26 ++++++++++------- php/Modules/Photo.php | 46 +++++++++++------------------ src/scripts/contextMenu.js | 60 ++++++++++++++++++++++---------------- src/scripts/lychee.js | 2 -- src/scripts/multiselect.js | 1 - 5 files changed, 67 insertions(+), 68 deletions(-) diff --git a/php/Access/Guest.php b/php/Access/Guest.php index 8f0f72b..d5c1390 100644 --- a/php/Access/Guest.php +++ b/php/Access/Guest.php @@ -32,6 +32,7 @@ final class Guest extends Access { // $_GET functions case 'Album::getArchive': self::getAlbumArchiveAction(); break; + case 'Photo::getArchive': self::getPhotoArchiveAction(); break; case 'Photo::getPhoto': self::getPhotoFileAction(); break; } @@ -159,26 +160,29 @@ final class Guest extends Access { } - private static function getPhotoFileAction() { + private static function getPhotoArchiveAction() { - Validator::required(isset($_GET['photoID'], $_GET['password']), __METHOD__); + Validator::required(isset($_GET['photoIDs']), $_GET['password'], __METHOD__); - $photo = new Photo($_GET['photoID']); + $photo = new Photo($_GET['photoIDs']); $pgP = $photo->getPublic($_GET['password']); - // Photo Download - if ($pgP===2) { + if ($pgP===2) $photo->getArchive(); + else Response::warning('Photo private or password incorrect!'); - // Photo Public - $photo->getPhoto(); + } - } else { + private static function getPhotoFileAction() { - // Photo Private - Response::warning('Photo private or password incorrect!'); + Validator::required(isset($_GET['photoID'], $_GET['password']), __METHOD__); - } + $photo = new Photo($_GET['photoID']); + + $pgP = $photo->getPublic($_GET['password']); + + if ($pgP===2) $photo->getPhoto(); + else Response::warning('Photo private or password incorrect!'); } diff --git a/php/Modules/Photo.php b/php/Modules/Photo.php index 006baec..2f49acd 100755 --- a/php/Modules/Photo.php +++ b/php/Modules/Photo.php @@ -1067,10 +1067,10 @@ final class Photo { } /** - * Checks if photo or parent album is public. - * @return integer 0 = Photo private and parent album private - * 1 = Album public, but password incorrect - * 2 = Photo public or album public and password correct + * Checks if all photos or their parent albums are public. + * @return integer 0 = At least one photo private and parent album private + * 1 = At least one album public, but password incorrect + * 2 = All photos public or album public and password correct */ public function getPublic($password) { @@ -1081,46 +1081,34 @@ final class Photo { Plugins::get()->activate(__METHOD__, 0, func_get_args()); // Get photo - $query = Database::prepare(Database::get(), "SELECT public, album FROM ? WHERE id = '?' LIMIT 1", array(LYCHEE_TABLE_PHOTOS, $this->photoIDs)); + $query = Database::prepare(Database::get(), "SELECT p.public AS photo_public, a.public AS album_public, password FROM ? AS p JOIN ? AS a ON a.id = p.album WHERE p.id IN (?)", array(LYCHEE_TABLE_PHOTOS, LYCHEE_TABLE_ALBUMS, $this->photoIDs)); $photos = Database::execute(Database::get(), $query, __METHOD__, __LINE__); if ($photos===false) return 0; - // Get photo object - $photo = $photos->fetch_object(); - - // Photo not found? - if ($photo===null) { - Log::error(Database::get(), __METHOD__, __LINE__, 'Could not find specified photo'); - return false; + // Not all photos found? + if ($photos->num_rows != count(explode(',', $this->photoIDs))) { + Log::error(Database::get(), __METHOD__, __LINE__, 'Could not find specified photos'); + return 0; } - // Check if public - if ($photo->public==='1') { - - // Photo public - return 2; - - } else { + while ($photo = $photos->fetch_object()) { - // Check if album public - $album = new Album($photo->album); - $agP = $album->getPublic(); - $acP = $album->checkPassword($password); + // Check if public + if ($photo->photo_public==='1') continue; - // Album public and password correct - if ($agP===true&&$acP===true) return 2; + // Album private + if ($photo->album_public==='0') return 0; - // Album public, but password incorrect - if ($agP===true&&$acP===false) return 1; + // Check if password is correct + if ($photo->password!='' && $photo->password!==crypt($password, $photo->password)) return 1; } // Call plugins Plugins::get()->activate(__METHOD__, 1, func_get_args()); - // Photo private - return 0; + return 2; } diff --git a/src/scripts/contextMenu.js b/src/scripts/contextMenu.js index e5af8cc..bea0005 100644 --- a/src/scripts/contextMenu.js +++ b/src/scripts/contextMenu.js @@ -116,10 +116,10 @@ contextMenu.album = function(albumID, e) { let items = [ { title: build.iconic('cloud-download') + 'Download', fn: () => album.getArchive([ albumID ]) }, - { title: build.iconic('pencil') + 'Rename', fn: () => album.setTitle([ albumID ]) }, - { title: build.iconic('collapse-left') + 'Merge', fn: () => { basicContext.close(); contextMenu.mergeAlbum(albumID, e) } }, - { title: build.iconic('folder') + 'Move', fn: () => { basicContext.close(); contextMenu.moveAlbum([ albumID ], e) } }, - { title: build.iconic('trash') + 'Delete', fn: () => album.delete([ albumID ]) } + { title: build.iconic('pencil') + 'Rename', visible: !lychee.publicMode, fn: () => album.setTitle([ albumID ]) }, + { title: build.iconic('collapse-left') + 'Merge', visible: !lychee.publicMode, fn: () => { basicContext.close(); contextMenu.mergeAlbum(albumID, e) } }, + { title: build.iconic('folder') + 'Move', visible: !lychee.publicMode, fn: () => { basicContext.close(); contextMenu.moveAlbum([ albumID ], e) } }, + { title: build.iconic('trash') + 'Delete', visible: !lychee.publicMode, fn: () => album.delete([ albumID ]) } ] multiselect.select('.album[data-id="' + albumID + '"]') @@ -138,11 +138,11 @@ contextMenu.albumMulti = function(albumIDs, e) { let items = [ { title: build.iconic('cloud-download') + 'Download All', fn: () => album.getArchive(albumIDs) }, - { title: build.iconic('pencil') + 'Rename All', fn: () => album.setTitle(albumIDs) }, - { title: build.iconic('collapse-left') + 'Merge All', visible: autoMerge, fn: () => album.merge(albumIDs) }, - { title: build.iconic('collapse-left') + 'Merge', visible: !autoMerge, fn: () => { basicContext.close(); contextMenu.mergeAlbum(albumIDs[0], e) } }, - { title: build.iconic('folder') + 'Move All', fn: () => { basicContext.close(); contextMenu.moveAlbum(albumIDs, e) } }, - { title: build.iconic('trash') + 'Delete All', fn: () => album.delete(albumIDs) } + { title: build.iconic('pencil') + 'Rename All', visible: !lychee.publicMode, fn: () => album.setTitle(albumIDs) }, + { title: build.iconic('collapse-left') + 'Merge All', visible: !lychee.publicMode && autoMerge, fn: () => album.merge(albumIDs) }, + { title: build.iconic('collapse-left') + 'Merge', visible: !lychee.publicMode && !autoMerge, fn: () => { basicContext.close(); contextMenu.mergeAlbum(albumIDs[0], e) } }, + { title: build.iconic('folder') + 'Move All', visible: !lychee.publicMode, fn: () => { basicContext.close(); contextMenu.moveAlbum(albumIDs, e) } }, + { title: build.iconic('trash') + 'Delete All', visible: !lychee.publicMode, fn: () => album.delete(albumIDs) } ] items.push() @@ -244,16 +244,21 @@ contextMenu.photo = function(photoID, e) { // in order to keep the selection let items = [ - { title: build.iconic('cloud-download') + 'Download', fn: () => photo.getPhoto(photoID) }, - { title: build.iconic('star') + 'Star', fn: () => photo.setStar([ photoID ]) }, - { title: build.iconic('tag') + 'Tags', fn: () => photo.editTags([ photoID ]) }, - { }, - { title: build.iconic('pencil') + 'Rename', fn: () => photo.setTitle([ photoID ]) }, - { title: build.iconic('layers') + 'Duplicate', fn: () => photo.duplicate([ photoID ]) }, - { title: build.iconic('folder') + 'Move', fn: () => { basicContext.close(); contextMenu.move([ photoID ], e) } }, - { title: build.iconic('trash') + 'Delete', fn: () => photo.delete([ photoID ]) } + { title: build.iconic('cloud-download') + 'Download', fn: () => photo.getPhoto(photoID) } ] + if (!lychee.publicMode) { + items = items.concat([ + { title: build.iconic('star') + 'Star', fn: () => photo.setStar([ photoID ]) }, + { title: build.iconic('tag') + 'Tags', fn: () => photo.editTags([ photoID ]) }, + { }, + { title: build.iconic('pencil') + 'Rename', fn: () => photo.setTitle([ photoID ]) }, + { title: build.iconic('layers') + 'Duplicate', fn: () => photo.duplicate([ photoID ]) }, + { title: build.iconic('folder') + 'Move', fn: () => { basicContext.close(); contextMenu.move([ photoID ], e) } }, + { title: build.iconic('trash') + 'Delete', fn: () => photo.delete([ photoID ]) } + ]) + } + multiselect.select('.photo[data-id="' + photoID + '"]') basicContext.show(items, e.originalEvent, contextMenu.close) @@ -269,16 +274,21 @@ contextMenu.photoMulti = function(photoIDs, e) { // in order to keep the selection and multiselect let items = [ - { title: build.iconic('cloud-download') + 'Download All', fn: () => photo.getArchive(photoIDs) }, - { title: build.iconic('star') + 'Star All', fn: () => photo.setStar(photoIDs) }, - { title: build.iconic('tag') + 'Tag All', fn: () => photo.editTags(photoIDs) }, - { }, - { title: build.iconic('pencil') + 'Rename All', fn: () => photo.setTitle(photoIDs) }, - { title: build.iconic('layers') + 'Duplicate All', fn: () => photo.duplicate(photoIDs) }, - { title: build.iconic('folder') + 'Move All', fn: () => { basicContext.close(); contextMenu.move(photoIDs, e) } }, - { title: build.iconic('trash') + 'Delete All', fn: () => photo.delete(photoIDs) } + { title: build.iconic('cloud-download') + 'Download All', fn: () => photo.getArchive(photoIDs) } ] + if (!lychee.publicMode) { + items = items.concat([ + { title: build.iconic('star') + 'Star All', fn: () => photo.setStar(photoIDs) }, + { title: build.iconic('tag') + 'Tag All', fn: () => photo.editTags(photoIDs) }, + { }, + { title: build.iconic('pencil') + 'Rename All', fn: () => photo.setTitle(photoIDs) }, + { title: build.iconic('layers') + 'Duplicate All', fn: () => photo.duplicate(photoIDs) }, + { title: build.iconic('folder') + 'Move All', fn: () => { basicContext.close(); contextMenu.move(photoIDs, e) } }, + { title: build.iconic('trash') + 'Delete All', fn: () => photo.delete(photoIDs) } + ]) + } + basicContext.show(items, e.originalEvent, contextMenu.close) } diff --git a/src/scripts/lychee.js b/src/scripts/lychee.js index 5c5de87..81be85b 100644 --- a/src/scripts/lychee.js +++ b/src/scripts/lychee.js @@ -251,8 +251,6 @@ lychee.setMode = function(mode) { $(document) .off('click', '.header__title--editable') .off('touchend', '.header__title--editable') - .off('contextmenu', '.photo') - .off('contextmenu', '.album') .off('drop') Mousetrap diff --git a/src/scripts/multiselect.js b/src/scripts/multiselect.js index 1b7b2db..38a75f7 100644 --- a/src/scripts/multiselect.js +++ b/src/scripts/multiselect.js @@ -150,7 +150,6 @@ multiselect.clearSelection = function(deselect = true) { multiselect.show = function(e) { - if (lychee.publicMode) return false if (!visible.albums() && !visible.album()) return false if ($('.album:hover, .photo:hover').length!==0) return false if (visible.search()) return false From 3c4b1911fdd99e58fa585f905f7786cdabf96c91 Mon Sep 17 00:00:00 2001 From: Nils Asmussen Date: Sun, 21 Aug 2016 15:12:32 +0200 Subject: [PATCH 6/6] Move URL building into one function. --- src/scripts/album.js | 14 ++++---------- src/scripts/lychee.js | 12 ++++++++++++ src/scripts/photo.js | 27 ++++++++++----------------- 3 files changed, 26 insertions(+), 27 deletions(-) diff --git a/src/scripts/album.js b/src/scripts/album.js index 5828c26..c12110f 100644 --- a/src/scripts/album.js +++ b/src/scripts/album.js @@ -627,18 +627,12 @@ album.share = function(service) { album.getArchive = function(albumIDs) { - let link = '' - let url = `${ api.path }?function=Album::getArchive&albumIDs=${ albumIDs.join(',') }` + let path = `${ api.path }?function=Album::getArchive&albumIDs=${ albumIDs.join(',') }` + let url = lychee.getURL(path) - var pos = location.href.indexOf('#') - link = pos!=-1 ? location.href.substring(0, pos) : location.href + if (lychee.publicMode===true) url += `&password=${ encodeURIComponent(password.value) }` - if (location.href.indexOf('index.html')>0) link = link.replace('index.html', url) - else link += url - - if (lychee.publicMode===true) link += `&password=${ encodeURIComponent(password.value) }` - - location.href = link + location.href = url } diff --git a/src/scripts/lychee.js b/src/scripts/lychee.js index 81be85b..1a47c85 100644 --- a/src/scripts/lychee.js +++ b/src/scripts/lychee.js @@ -417,6 +417,18 @@ lychee.html = function(literalSections, ...substs) { } +lychee.getURL = function(path) { + + let pos = location.href.indexOf('#') + let url = pos!=-1 ? location.href.substring(0, pos) : location.href + + if (location.href.indexOf('index.html')>0) url = url.replace('index.html', path) + else url += path + + return url + +} + lychee.error = function(errorThrown, params, data) { console.error({ diff --git a/src/scripts/photo.js b/src/scripts/photo.js index 0e606c7..75d9cad 100644 --- a/src/scripts/photo.js +++ b/src/scripts/photo.js @@ -635,29 +635,23 @@ photo.share = function(photoID, service) { photo.getPhoto = function(photoID) { - let link - let url = `${ api.path }?function=Photo::getPhoto&photoID=${ photoID }` + let path = `${ api.path }?function=Photo::getPhoto&photoID=${ photoID }` + let url = lychee.getURL(path) - if (location.href.indexOf('index.html')>0) link = location.href.replace(location.hash, '').replace('index.html', url) - else link = location.href.replace(location.hash, '') + url + if (lychee.publicMode===true) url += `&password=${ encodeURIComponent(password.value) }` - if (lychee.publicMode===true) link += `&password=${ encodeURIComponent(password.value) }` - - location.href = link + location.href = url } photo.getArchive = function(photoIDs) { - let link - let url = `${ api.path }?function=Photo::getArchive&photoIDs=${ photoIDs.join(',') }` - - if (location.href.indexOf('index.html')>0) link = location.href.replace(location.hash, '').replace('index.html', url) - else link = location.href.replace(location.hash, '') + url + let path = `${ api.path }?function=Photo::getArchive&photoIDs=${ photoIDs.join(',') }` + let url = lychee.getURL(path) - if (lychee.publicMode===true) link += `&password=${ encodeURIComponent(password.value) }` + if (lychee.publicMode===true) url += `&password=${ encodeURIComponent(password.value) }` - location.href = link + location.href = url } @@ -673,9 +667,8 @@ photo.getDirectLink = function() { photo.getViewLink = function(photoID) { - let url = 'view.php?p=' + photoID + let path = 'view.php?p=' + photoID - if (location.href.indexOf('index.html')>0) return location.href.replace('index.html' + location.hash, url) - else return location.href.replace(location.hash, url) + return lychee.getURL(path) } \ No newline at end of file