Compare commits
239 Commits
version-1.
...
0d094996df
| Author | SHA1 | Date | |
|---|---|---|---|
| 0d094996df | |||
| 238dc1d6e7 | |||
| 1fa4cb19a2 | |||
| 03ad26655c | |||
| bd03659b39 | |||
| 2bbe1881b6 | |||
| d5cddba5e9 | |||
| 33bc262f0a | |||
| 8b0459fae4 | |||
| 6930c0a06a | |||
| ed07668b2e | |||
| ef7fe60fca | |||
| 87777a6ace | |||
| 9fcde24c39 | |||
| d315f4d0c2 | |||
| be909bf54d | |||
| 68ef80fb9f | |||
| 31ea4196cf | |||
| cfb5ab9d82 | |||
| b05015e76e | |||
| a260f4ff88 | |||
| 2a528f2830 | |||
| 6c5d814a99 | |||
| 9a8a91343b | |||
| af0c8990a6 | |||
| b2bcb6a124 | |||
| d1741f2478 | |||
| d7837741cc | |||
| e496c7cc14 | |||
| 65cea8ed8a | |||
| c6dc6bbac4 | |||
| e48f065c25 | |||
| c991f05dd3 | |||
| 5c2eff09b8 | |||
| 85be093a36 | |||
| c735648468 | |||
| 41881594e9 | |||
| 29bf6af1f8 | |||
| 3f66fce262 | |||
| 244af88a9a | |||
| 3ed84eb4d5 | |||
| 229fb9e5bf | |||
| 54b69ecd11 | |||
| 544944a7f5 | |||
| 6087ebe249 | |||
| 3cf281b24d | |||
| 01822cdccf | |||
| 0325a2ec90 | |||
| 70fcd097cc | |||
| 2c24a0a7e7 | |||
| c7e4351375 | |||
| 0b8c614191 | |||
| e916489d00 | |||
| 1859a9ea2a | |||
| d83dd6ea6e | |||
| eb04e87085 | |||
| 16eda4cfe7 | |||
| 4c928af9ad | |||
| b8c53d7d4d | |||
| 1b7e745f11 | |||
| aa3a54f237 | |||
| 0b0d47acb8 | |||
| a4cc528951 | |||
| 5b8551a726 | |||
| 5cff62836e | |||
| 310fe7c3d6 | |||
| 167a50cb92 | |||
| d9fd2ae20d | |||
| a76dde927b | |||
| daa8b051c5 | |||
| 27f69b0a74 | |||
| ad816f10a3 | |||
| 59b1fa7a72 | |||
| 6d0aef4df6 | |||
| a06902335b | |||
| cf0b9ebaf9 | |||
| edc857f6fd | |||
| a9a347c638 | |||
| fa01bf8961 | |||
| 54df35073d | |||
| 4684482d67 | |||
| 4033a8813c | |||
| 4d47696dcd | |||
| 54c4294d08 | |||
| e6f7476037 | |||
| 7d19cf823d | |||
| 326c8f11ee | |||
| 556bbb2753 | |||
| febe7bb405 | |||
| 0a8da104cc | |||
| 02b43035f3 | |||
| 87df775c51 | |||
| c6902150f0 | |||
| 277611e0ac | |||
| b1378a3b59 | |||
| 5bb8c020bd | |||
| a6fd8d2764 | |||
| b9bd2bf499 | |||
| 812c7a4f20 | |||
| 021df2df93 | |||
| a9a2c64d81 | |||
| cf31f0af07 | |||
| 2d1a299fe0 | |||
| 307d34430a | |||
| 0366df9b5f | |||
| f9eefe7b41 | |||
| daf6b6b264 | |||
| 07bc784859 | |||
| 09f498695d | |||
| 6b028aac41 | |||
| 2ef1289628 | |||
| 4d05cebc40 | |||
| ce909ccfe5 | |||
| 1314cfdd30 | |||
| 7897172256 | |||
| 49390c372d | |||
| 2174e1d08b | |||
| d66f071aab | |||
| 7d82a4a924 | |||
| b7a37c85f6 | |||
| 3de87809bb | |||
| c763967463 | |||
| 6369187eb7 | |||
| b3808144ca | |||
| d8858c78bb | |||
| c0d69f7205 | |||
| b5edf09a69 | |||
| 54fb7ab410 | |||
| 086102d007 | |||
| 56b60b74bc | |||
| fc59708914 | |||
| 1c02cbea93 | |||
| 52420b8715 | |||
| 0ec0de4414 | |||
| 69417c36ed | |||
| f2d8a32e67 | |||
| 4863561129 | |||
| 8474d3b2b2 | |||
| 3bf69fd21f | |||
| 237f4005bd | |||
| 4bf4641428 | |||
| ff808ba18d | |||
| 6c662481bc | |||
| af73f00701 | |||
| 681af07985 | |||
| cba42a9129 | |||
| 96937b6952 | |||
| 5c55e45c3c | |||
| 70e6001c85 | |||
| 4402521051 | |||
| 889302cd36 | |||
| cae5c6e5cf | |||
| 162d14b35f | |||
| 555c61937b | |||
| d069ddca18 | |||
| 71b71f8a03 | |||
| 2885e24456 | |||
| c72e24c0c7 | |||
| b8191bf554 | |||
| 3594b3d021 | |||
| 936d3d20db | |||
| 5c4a075231 | |||
| 6ddf518294 | |||
| 66a411973a | |||
| a83b938f8a | |||
| 5344378333 | |||
| 8147e2b97d | |||
| d562c70667 | |||
| 5599ff8d9b | |||
| e7490e40dd | |||
| 6fcc2eb59f | |||
| b793e05980 | |||
| 340ed84272 | |||
| 93884e2e93 | |||
| 2a740d8cef | |||
| 5e0d4df2f7 | |||
| e84c4f2b43 | |||
| 893d31af52 | |||
| 5895f4faa6 | |||
| 8e7a09f3f3 | |||
| 837c92db44 | |||
| c392105814 | |||
| 9d95df81fe | |||
| d4cc72304e | |||
| 2c68b6a798 | |||
| fd84e1c9f8 | |||
| 8d02662eb3 | |||
| 31f4edc996 | |||
| a208c0482f | |||
| 909d50efa8 | |||
| bd1ca8d18c | |||
| c7d3b9c3d1 | |||
| 5a51778a6a | |||
| 2bb29d7224 | |||
| 1b7e83e11e | |||
| 354e54a0af | |||
| 17859b70e9 | |||
| 6a7defcdc9 | |||
| f193b614b7 | |||
| 12ea378b02 | |||
| 62900e7f81 | |||
| c48ba786c1 | |||
| 3694819d13 | |||
| d7b68995e8 | |||
| 5df7ea8371 | |||
| 7d3ab166c7 | |||
| ed6054e6b6 | |||
| 3fc8ccf550 | |||
| 6a7c7af7b8 | |||
| 8ec6c227d5 | |||
| 42e5c7fe37 | |||
| 05c48be785 | |||
| d3cb750874 | |||
| 20db3561cf | |||
| 768f5ee529 | |||
| 16ec547064 | |||
| e40c05c1f8 | |||
| 344db6e4c5 | |||
| fcbbc7106d | |||
| 331193019c | |||
| bcbb74a680 | |||
| c6c249787f | |||
| 068d1dad3e | |||
| f1408ad2ee | |||
| 8b73420936 | |||
| ee304dd7b9 | |||
| 1def1484cb | |||
| 981b652e25 | |||
| cda7f3115c | |||
| e439a074a6 | |||
| ee9bdd45c0 | |||
| 9fe8acc747 | |||
| 096cea078c | |||
| 2a25434862 | |||
| 943297900c | |||
| 95e289d82d | |||
| 1a15e347f2 | |||
| 31e1357b47 | |||
| 08cc6b1c77 |
11
LICENSE.md
Normal file
11
LICENSE.md
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
Copyright 2016-2021 Stichting HashRU
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
31
README.md
Normal file
31
README.md
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# HashRU Pics
|
||||||
|
|
||||||
|
This is the development repository for the HashRU photo website.
|
||||||
|
The CMS and its modules originate in [Kabuki CMS](https://aaronweb.net/projects/kabuki/), but have been extended and are maintained separately in this repository.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
The Kabuki codebase requires the following PHP extensions to be enabled for full operation:
|
||||||
|
|
||||||
|
* exif
|
||||||
|
* imagick (PECL)
|
||||||
|
* mysqli
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
Copy `config.php.dist` to `config.php` and set-up the constants contained in the file.
|
||||||
|
|
||||||
|
## Running
|
||||||
|
|
||||||
|
For development purposes, simply run the `server` script provided in the root of this repository.
|
||||||
|
This will start a PHP development server on `hashru.local:8080`.
|
||||||
|
|
||||||
|
For a production environment, please set up a proper PHP-FPM environment instead.
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Pull requests are welcome over at the [HashRU Gitea](https://gitea.hashru.nl/Public/pics/pulls).
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
The HashRU Pics repository is licensed with a BSD 3-clause license, as is Kabuki CMS.
|
||||||
10
TODO.md
10
TODO.md
@@ -1,11 +1,11 @@
|
|||||||
TODO:
|
TODO:
|
||||||
|
|
||||||
* Gebruikers *persoons*tags laten verwijderen
|
* Alleen persoonstags tonen bij foto's.
|
||||||
|
|
||||||
|
* Gebruikers persoonstags laten verwijderen vanaf fotopagina.
|
||||||
|
|
||||||
|
* 'Return to album' knop toevoegen die naar juiste pagina leidt.
|
||||||
|
|
||||||
* Bij taggen van user: thumbnail setten
|
* Bij taggen van user: thumbnail setten
|
||||||
|
|
||||||
* Grid herberekenen; captions weglaten.
|
* Grid herberekenen; captions weglaten.
|
||||||
|
|
||||||
* Sortering: alleen *albums* ASC, personen DESC!
|
|
||||||
|
|
||||||
* Album/tag management
|
|
||||||
|
|||||||
7
app.php
7
app.php
@@ -16,16 +16,15 @@ require_once 'vendor/autoload.php';
|
|||||||
Registry::set('start', microtime(true));
|
Registry::set('start', microtime(true));
|
||||||
Registry::set('db', new Database(DB_SERVER, DB_USER, DB_PASS, DB_NAME));
|
Registry::set('db', new Database(DB_SERVER, DB_USER, DB_PASS, DB_NAME));
|
||||||
|
|
||||||
|
// Handle errors our own way.
|
||||||
|
ErrorHandler::enable();
|
||||||
|
|
||||||
// Do some authentication checks.
|
// Do some authentication checks.
|
||||||
Session::start();
|
Session::start();
|
||||||
$user = Authentication::isLoggedIn() ? Member::fromId($_SESSION['user_id']) : new Guest();
|
$user = Authentication::isLoggedIn() ? Member::fromId($_SESSION['user_id']) : new Guest();
|
||||||
$user->updateAccessTime();
|
$user->updateAccessTime();
|
||||||
Registry::set('user', $user);
|
Registry::set('user', $user);
|
||||||
|
|
||||||
// Handle errors our own way.
|
|
||||||
set_error_handler('ErrorHandler::handleError');
|
|
||||||
ini_set("display_errors", DEBUG ? "On" : "Off");
|
|
||||||
|
|
||||||
// The real magic starts here!
|
// The real magic starts here!
|
||||||
ob_start();
|
ob_start();
|
||||||
Dispatcher::dispatch();
|
Dispatcher::dispatch();
|
||||||
|
|||||||
@@ -14,5 +14,14 @@
|
|||||||
"models/",
|
"models/",
|
||||||
"templates/"
|
"templates/"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-mysqli": "*",
|
||||||
|
"ext-imagick": "*",
|
||||||
|
"ext-gd": "*",
|
||||||
|
"ext-imagick": "*",
|
||||||
|
"ext-mysqli": "*",
|
||||||
|
"twbs/bootstrap": "^5.3",
|
||||||
|
"twbs/bootstrap-icons": "^1.10"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ const CACHE_KEY_PREFIX = 'hashru_';
|
|||||||
const BASEDIR = __DIR__;
|
const BASEDIR = __DIR__;
|
||||||
const BASEURL = 'https://pics.hashru.nl'; // no trailing /
|
const BASEURL = 'https://pics.hashru.nl'; // no trailing /
|
||||||
|
|
||||||
|
// Reply-To e-mail header address
|
||||||
|
const REPLY_TO_ADDRESS = 'no-reply@my.domain.tld';
|
||||||
|
|
||||||
// Assets dir and url, where assets are plentiful. (In wwwroot!)
|
// Assets dir and url, where assets are plentiful. (In wwwroot!)
|
||||||
const ASSETSDIR = BASEDIR . '/public/assets';
|
const ASSETSDIR = BASEDIR . '/public/assets';
|
||||||
const ASSETSURL = BASEURL . '/assets';
|
const ASSETSURL = BASEURL . '/assets';
|
||||||
@@ -29,5 +32,5 @@ const DB_PASS = '';
|
|||||||
const DB_NAME = 'hashru_pics';
|
const DB_NAME = 'hashru_pics';
|
||||||
const DB_LOG_QUERIES = false;
|
const DB_LOG_QUERIES = false;
|
||||||
|
|
||||||
const SITE_TITLE = 'HashRU';
|
const SITE_TITLE = 'HashRU Pics';
|
||||||
const SITE_SLOGAN = 'Nijmeegs Nerdclubje';
|
const SITE_SLOGAN = 'Nijmeegs Nerdclubje';
|
||||||
|
|||||||
134
controllers/AccountSettings.php
Normal file
134
controllers/AccountSettings.php
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
<?php
|
||||||
|
/*****************************************************************************
|
||||||
|
* AccountSettings.php
|
||||||
|
* Contains the account settings controller.
|
||||||
|
*
|
||||||
|
* Kabuki CMS (C) 2013-2023, Aaron van Geffen
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
class AccountSettings extends HTMLController
|
||||||
|
{
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
// Not logged in yet?
|
||||||
|
if (!Registry::get('user')->isLoggedIn())
|
||||||
|
throw new NotAllowedException('You need to be logged in to view this page.');
|
||||||
|
|
||||||
|
parent::__construct('Account settings');
|
||||||
|
$form_title = 'Account settings';
|
||||||
|
|
||||||
|
// Session checking!
|
||||||
|
if (empty($_POST))
|
||||||
|
Session::resetSessionToken();
|
||||||
|
else
|
||||||
|
Session::validateSession();
|
||||||
|
|
||||||
|
$fields = [
|
||||||
|
'first_name' => [
|
||||||
|
'type' => 'text',
|
||||||
|
'label' => 'First name',
|
||||||
|
'size' => 50,
|
||||||
|
'maxlength' => 255,
|
||||||
|
],
|
||||||
|
'surname' => [
|
||||||
|
'type' => 'text',
|
||||||
|
'label' => 'Family name',
|
||||||
|
'size' => 50,
|
||||||
|
'maxlength' => 255,
|
||||||
|
],
|
||||||
|
'emailaddress' => [
|
||||||
|
'type' => 'text',
|
||||||
|
'label' => 'Email address',
|
||||||
|
'size' => 50,
|
||||||
|
'maxlength' => 255,
|
||||||
|
],
|
||||||
|
'password1' => [
|
||||||
|
'before_html' => '<div class="offset-sm-2 mt-4"><p>To change your password, please fill out the fields below.</p></div>',
|
||||||
|
'type' => 'password',
|
||||||
|
'label' => 'Password',
|
||||||
|
'size' => 50,
|
||||||
|
'maxlength' => 255,
|
||||||
|
'is_optional' => true,
|
||||||
|
],
|
||||||
|
'password2' => [
|
||||||
|
'type' => 'password',
|
||||||
|
'label' => 'Password (repeat)',
|
||||||
|
'size' => 50,
|
||||||
|
'maxlength' => 255,
|
||||||
|
'is_optional' => true,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$form = new Form([
|
||||||
|
'request_url' => BASEURL . '/' . $_GET['action'] . '/',
|
||||||
|
'fields' => $fields,
|
||||||
|
'submit_caption' => 'Save details',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$user = Registry::get('user');
|
||||||
|
|
||||||
|
// Create the form, add in default values.
|
||||||
|
$form->setData(empty($_POST) ? $user->getProps() : $_POST);
|
||||||
|
$formview = new FormView($form, $form_title);
|
||||||
|
$this->page->adopt($formview);
|
||||||
|
|
||||||
|
// Fetch user tags
|
||||||
|
$tags = Tag::getAllByOwner($user->getUserId());
|
||||||
|
if (!empty($tags))
|
||||||
|
$this->page->adopt(new MyTagsView($tags));
|
||||||
|
|
||||||
|
// Left a message?
|
||||||
|
if (isset($_SESSION['account_msg']))
|
||||||
|
{
|
||||||
|
$alert = $_SESSION['account_msg'];
|
||||||
|
$formview->adopt(new Alert($alert[0], $alert[1], $alert[2]));
|
||||||
|
unset($_SESSION['account_msg']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Just updating account settings?
|
||||||
|
if (!empty($_POST))
|
||||||
|
{
|
||||||
|
$form->verify($_POST);
|
||||||
|
|
||||||
|
// Anything missing?
|
||||||
|
if (!empty($form->getMissing()))
|
||||||
|
{
|
||||||
|
$missingFields = array_intersect_key($fields, array_flip($form->getMissing()));
|
||||||
|
$missingFields = array_map(function($field) { return strtolower($field['label']); }, $missingFields);
|
||||||
|
return $formview->adopt(new Alert('Some data missing', 'Please fill out the following fields: ' . implode(', ', $missingFields), 'danger'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = $form->getData();
|
||||||
|
|
||||||
|
// Just to be on the safe side.
|
||||||
|
$data['first_name'] = htmlspecialchars(trim($data['first_name']));
|
||||||
|
$data['surname'] = htmlspecialchars(trim($data['surname']));
|
||||||
|
$data['emailaddress'] = trim($data['emailaddress']);
|
||||||
|
|
||||||
|
// If it looks like an e-mail address...
|
||||||
|
if (!empty($data['emailaddress']) && !preg_match('~^[^ ]+@[^ ]+\.[a-z]+$~', $data['emailaddress']))
|
||||||
|
return $formview->adopt(new Alert('Email addresses invalid', 'The email address you entered is not a valid email address.', 'danger'));
|
||||||
|
// Check whether email address is already linked to an account in the database -- just not to the account we happen to be editing, of course.
|
||||||
|
elseif (!empty($data['emailaddress']) && $user->getEmailAddress() !== $data['emailaddress'] && Member::exists($data['emailaddress']))
|
||||||
|
return $formview->adopt(new Alert('Email address already in use', 'Another account is already using this e-mail address.', 'danger'));
|
||||||
|
|
||||||
|
// Changing passwords?
|
||||||
|
if (!empty($data['password1']) && !empty($data['password2']))
|
||||||
|
{
|
||||||
|
if (strlen($data['password1']) < 6 || !preg_match('~[^A-z]~', $data['password1']))
|
||||||
|
return $formview->adopt(new Alert('Password not acceptable', 'Please use a password that is at least six characters long and contains at least one non-alphabetic character (e.g. a number or symbol).', 'danger'));
|
||||||
|
elseif ($data['password1'] !== $data['password2'])
|
||||||
|
return $formview->adopt(new Alert('Passwords do not match', 'The passwords you entered do not match. Please try again.', 'danger'));
|
||||||
|
|
||||||
|
// Keep just the one.
|
||||||
|
$data['password'] = $data['password1'];
|
||||||
|
unset($data['password1'], $data['password2']);
|
||||||
|
$formview->adopt(new Alert('Your password has been changed', 'Next time you log in, you can use your new password to authenticate yourself.', 'success'));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
$formview->adopt(new Alert('Your account settings have been saved', 'Thank you for keeping your information current.', 'success'));
|
||||||
|
|
||||||
|
$user->update($data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
122
controllers/Download.php
Normal file
122
controllers/Download.php
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
<?php
|
||||||
|
/*****************************************************************************
|
||||||
|
* Download.php
|
||||||
|
* Contains the code to download an album.
|
||||||
|
*
|
||||||
|
* Kabuki CMS (C) 2013-2019, Aaron van Geffen
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
class Download
|
||||||
|
{
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
// Ensure we're logged in at this point.
|
||||||
|
$user = Registry::get('user');
|
||||||
|
if (!$user->isLoggedIn())
|
||||||
|
throw new NotAllowedException();
|
||||||
|
|
||||||
|
if (!isset($_GET['tag']))
|
||||||
|
throw new UserFacingException('No album or tag has been specified for download.');
|
||||||
|
|
||||||
|
$tag = (int)$_GET['tag'];
|
||||||
|
$album = Tag::fromId($tag);
|
||||||
|
|
||||||
|
if (isset($_SESSION['current_export']))
|
||||||
|
throw new UserFacingException('You can only export one album at the same time. Please wait until the other download finishes, or try again later.');
|
||||||
|
|
||||||
|
// So far so good?
|
||||||
|
$this->exportAlbum($album);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function exportAlbum(Tag $album)
|
||||||
|
{
|
||||||
|
$files = [];
|
||||||
|
|
||||||
|
$album_ids = array_merge([$album->id_tag], $this->getChildAlbumIds($album->id_tag));
|
||||||
|
foreach ($album_ids as $album_id)
|
||||||
|
{
|
||||||
|
$iterator = AssetIterator::getByOptions(['id_tag' => $album_id]);
|
||||||
|
while ($asset = $iterator->next())
|
||||||
|
$files[] = join(DIRECTORY_SEPARATOR, [$asset->getSubdir(), $asset->getFilename()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$descriptorspec = [
|
||||||
|
0 => ['pipe', 'r'], // STDIN
|
||||||
|
1 => ['pipe', 'w'], // STDOUT
|
||||||
|
];
|
||||||
|
|
||||||
|
// Prevent simultaneous exports.
|
||||||
|
$_SESSION['current_export'] = $album->id_tag;
|
||||||
|
|
||||||
|
// Allow new exports if the connection is terminated unexpectedly (e.g. when a user aborts a download).
|
||||||
|
register_shutdown_function(function() {
|
||||||
|
if (isset($_SESSION['current_export']))
|
||||||
|
unset($_SESSION['current_export']);
|
||||||
|
});
|
||||||
|
|
||||||
|
$command = 'tar -cf - -C ' . escapeshellarg(ASSETSDIR) . ' --null -T -';
|
||||||
|
|
||||||
|
$proc = proc_open($command, $descriptorspec, $pipes, ASSETSDIR);
|
||||||
|
|
||||||
|
if(!$proc)
|
||||||
|
throw new UnexpectedValueException('Could not execute TAR command');
|
||||||
|
|
||||||
|
if(!$pipes[0])
|
||||||
|
throw new UnexpectedValueException('Could not open pipe for STDIN');
|
||||||
|
|
||||||
|
if(!$pipes[1])
|
||||||
|
throw new UnexpectedValueException('Could not open pipe for STDOUT');
|
||||||
|
|
||||||
|
// STDOUT should not block.
|
||||||
|
stream_set_blocking($pipes[1], 0);
|
||||||
|
|
||||||
|
header('Pragma: no-cache');
|
||||||
|
header('Content-Description: File Download');
|
||||||
|
header('Content-disposition: attachment; filename="' . $album->tag . '.tar"');
|
||||||
|
header('Content-Type: application/octet-stream');
|
||||||
|
header('Content-Transfer-Encoding: binary');
|
||||||
|
|
||||||
|
// Write filenames to include to STDIN, separated by null bytes.
|
||||||
|
foreach ($files as $file)
|
||||||
|
fwrite($pipes[0], $file . "\0");
|
||||||
|
|
||||||
|
// Close STDIN pipe to start archiving.
|
||||||
|
fclose($pipes[0]);
|
||||||
|
|
||||||
|
// At this point, end output buffering so we can enjoy more than ~62MB of photos.
|
||||||
|
ob_end_flush();
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
// Read STDOUT as `tar` is doing its work.
|
||||||
|
echo stream_get_contents($pipes[1], 4096);
|
||||||
|
|
||||||
|
// Are we still running?
|
||||||
|
$status = proc_get_status($proc);
|
||||||
|
}
|
||||||
|
while (!empty($status) && $status['running']);
|
||||||
|
|
||||||
|
// Close STDOUT pipe and clean up process.
|
||||||
|
fclose($pipes[1]);
|
||||||
|
|
||||||
|
proc_close($proc);
|
||||||
|
|
||||||
|
// Allow new exports from this point onward.
|
||||||
|
unset($_SESSION['current_export']);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getChildAlbumIds($parent_id)
|
||||||
|
{
|
||||||
|
$ids = [];
|
||||||
|
|
||||||
|
$albums = Tag::getAlbums($parent_id, 0, PHP_INT_MAX);
|
||||||
|
foreach ($albums as $album)
|
||||||
|
{
|
||||||
|
$ids[] = $album['id_tag'];
|
||||||
|
$ids = array_merge($ids, $this->getChildAlbumIds($album['id_tag']));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $ids;
|
||||||
|
}
|
||||||
|
}
|
||||||
223
controllers/EditAlbum.php
Normal file
223
controllers/EditAlbum.php
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
<?php
|
||||||
|
/*****************************************************************************
|
||||||
|
* EditAlbum.php
|
||||||
|
* Contains the album edit controller.
|
||||||
|
*
|
||||||
|
* Kabuki CMS (C) 2013-2017, Aaron van Geffen
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
class EditAlbum extends HTMLController
|
||||||
|
{
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
// Ensure it's just admins at this point.
|
||||||
|
if (!Registry::get('user')->isAdmin())
|
||||||
|
throw new NotAllowedException();
|
||||||
|
|
||||||
|
$id_tag = isset($_GET['id']) ? (int) $_GET['id'] : 0;
|
||||||
|
if (empty($id_tag) && !isset($_GET['add']) && $_GET['action'] !== 'addalbum')
|
||||||
|
throw new UnexpectedValueException('Requested album not found or not requesting a new album.');
|
||||||
|
|
||||||
|
if (!empty($id_tag))
|
||||||
|
$album = Tag::fromId($id_tag);
|
||||||
|
|
||||||
|
// Adding an album?
|
||||||
|
if (isset($_GET['add']) || $_GET['action'] === 'addalbum')
|
||||||
|
{
|
||||||
|
parent::__construct('Add a new album');
|
||||||
|
$form_title = 'Add a new album';
|
||||||
|
$this->page->addClass('editalbum');
|
||||||
|
}
|
||||||
|
// Deleting one?
|
||||||
|
elseif (isset($_GET['delete']))
|
||||||
|
{
|
||||||
|
// So far so good?
|
||||||
|
if (Session::validateSession('get') && $album->kind === 'Album' && $album->delete())
|
||||||
|
{
|
||||||
|
header('Location: ' . BASEURL . '/managealbums/');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
trigger_error('Cannot delete album: an error occured while processing the request.', E_USER_ERROR);
|
||||||
|
}
|
||||||
|
// Editing one, then, surely.
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if ($album->kind !== 'Album')
|
||||||
|
trigger_error('Cannot edit album: not an album.', E_USER_ERROR);
|
||||||
|
|
||||||
|
parent::__construct('Edit album \'' . $album->tag . '\'');
|
||||||
|
$form_title = 'Edit album \'' . $album->tag . '\'';
|
||||||
|
$this->page->addClass('editalbum');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Session checking!
|
||||||
|
if (empty($_POST))
|
||||||
|
Session::resetSessionToken();
|
||||||
|
else
|
||||||
|
Session::validateSession();
|
||||||
|
|
||||||
|
if ($id_tag)
|
||||||
|
$after_form = '<a href="' . BASEURL . '/editalbum/?id=' . $id_tag . '&delete&' . Session::getSessionTokenKey() . '=' . Session::getSessionToken() . '" class="btn btn-danger" onclick="return confirm(\'Are you sure you want to delete this album? You cannot undo this!\');">Delete album</a>';
|
||||||
|
elseif (!$id_tag)
|
||||||
|
$after_form = '<button name="submit_and_new" class="btn">Save and add another</button>';
|
||||||
|
|
||||||
|
// Gather possible parents for this album to be filed into
|
||||||
|
$parentChoices = [0 => '-root-'];
|
||||||
|
foreach (PhotoAlbum::getHierarchy('tag', 'up') as $parent)
|
||||||
|
{
|
||||||
|
if (!empty($id_tag) && $parent['id_tag'] == $id_tag)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
$parentChoices[$parent['id_tag']] = $parent['tag'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$fields = [
|
||||||
|
'id_parent' => [
|
||||||
|
'type' => 'select',
|
||||||
|
'label' => 'Parent album',
|
||||||
|
'options' => $parentChoices,
|
||||||
|
],
|
||||||
|
'id_asset_thumb' => [
|
||||||
|
'type' => 'numeric',
|
||||||
|
'label' => 'Thumbnail asset ID',
|
||||||
|
'is_optional' => true,
|
||||||
|
],
|
||||||
|
'tag' => [
|
||||||
|
'type' => 'text',
|
||||||
|
'label' => 'Album title',
|
||||||
|
'size' => 50,
|
||||||
|
'maxlength' => 255,
|
||||||
|
],
|
||||||
|
'slug' => [
|
||||||
|
'type' => 'text',
|
||||||
|
'label' => 'URL slug',
|
||||||
|
'size' => 50,
|
||||||
|
'maxlength' => 255,
|
||||||
|
],
|
||||||
|
'description' => [
|
||||||
|
'type' => 'textbox',
|
||||||
|
'label' => 'Description',
|
||||||
|
'size' => 50,
|
||||||
|
'maxlength' => 255,
|
||||||
|
'is_optional' => true,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
// Fetch image assets for this album
|
||||||
|
if (!empty($id_tag))
|
||||||
|
{
|
||||||
|
list($assets, $num_assets) = AssetIterator::getByOptions([
|
||||||
|
'direction' => 'desc',
|
||||||
|
'limit' => 500,
|
||||||
|
'id_tag' => $id_tag,
|
||||||
|
], true);
|
||||||
|
|
||||||
|
if ($num_assets > 0)
|
||||||
|
unset($fields['id_asset_thumb']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$form = new Form([
|
||||||
|
'request_url' => BASEURL . '/editalbum/?' . ($id_tag ? 'id=' . $id_tag : 'add'),
|
||||||
|
'content_below' => $after_form,
|
||||||
|
'fields' => $fields,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Add defaults for album if none present
|
||||||
|
if (empty($_POST) && isset($_GET['tag']))
|
||||||
|
{
|
||||||
|
$parentTag = Tag::fromId($_GET['tag']);
|
||||||
|
if ($parentTag->kind === 'Album')
|
||||||
|
{
|
||||||
|
$formDefaults = [
|
||||||
|
'id_parent' => $parentTag->id_tag,
|
||||||
|
'tag' => 'New Album Title Here',
|
||||||
|
'slug' => ($parentTag->slug ? $parentTag->slug . '/' : '') . 'NEW_ALBUM_SLUG_HERE',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($formDefaults))
|
||||||
|
$formDefaults = isset($album) ? get_object_vars($album) : $_POST;
|
||||||
|
|
||||||
|
// Create the form, add in default values.
|
||||||
|
$form->setData($formDefaults);
|
||||||
|
$formview = new FormView($form, $form_title ?? '');
|
||||||
|
$this->page->adopt($formview);
|
||||||
|
|
||||||
|
// If we have asset images, show the thumbnail manager
|
||||||
|
if (!empty($id_tag) && $num_assets > 0)
|
||||||
|
$this->page->adopt(new FeaturedThumbnailManager($assets, $id_tag ? $album->id_asset_thumb : 0));
|
||||||
|
|
||||||
|
if (isset($_POST['changeThumbnail']))
|
||||||
|
$this->processThumbnail($album);
|
||||||
|
elseif (!empty($_POST))
|
||||||
|
$this->processTagDetails($form, $id_tag, $album ?? null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function processThumbnail($tag)
|
||||||
|
{
|
||||||
|
if (empty($_POST))
|
||||||
|
return;
|
||||||
|
|
||||||
|
$tag->id_asset_thumb = $_POST['featuredThumbnail'];
|
||||||
|
$tag->save();
|
||||||
|
|
||||||
|
header('Location: ' . BASEURL . '/editalbum/?id=' . $tag->id_tag);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function processTagDetails($form, $id_tag, $album)
|
||||||
|
{
|
||||||
|
if (!empty($_POST))
|
||||||
|
{
|
||||||
|
$form->verify($_POST);
|
||||||
|
|
||||||
|
// Anything missing?
|
||||||
|
if (!empty($form->getMissing()))
|
||||||
|
return $formview->adopt(new Alert('Some data missing', 'Please fill out the following fields: ' . implode(', ', $form->getMissing()), 'danger'));
|
||||||
|
|
||||||
|
$data = $form->getData();
|
||||||
|
|
||||||
|
// Sanity check: don't let an album be its own parent
|
||||||
|
if ($data['id_parent'] == $id_tag)
|
||||||
|
{
|
||||||
|
return $formview->adopt(new Alert('Invalid parent', 'An album cannot be its own parent.', 'danger'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quick stripping.
|
||||||
|
$data['tag'] = htmlspecialchars($data['tag']);
|
||||||
|
$data['description'] = htmlspecialchars($data['description']);
|
||||||
|
$data['slug'] = strtr($data['slug'], [' ' => '-', '--' => '-', '&' => 'and', '=>' => '', "'" => "", ":"=> "", '\\' => '-']);
|
||||||
|
|
||||||
|
// TODO: when updating slug, update slug for all photos in this album.
|
||||||
|
|
||||||
|
// Creating a new album?
|
||||||
|
if (!$id_tag)
|
||||||
|
{
|
||||||
|
$data['kind'] = 'Album';
|
||||||
|
$newTag = Tag::createNew($data);
|
||||||
|
if ($newTag === false)
|
||||||
|
return $formview->adopt(new Alert('Cannot create this album', 'Something went wrong while creating the album...', 'danger'));
|
||||||
|
|
||||||
|
if (isset($_POST['submit_and_new']))
|
||||||
|
{
|
||||||
|
header('Location: ' . BASEURL . '/editalbum/?add&tag=' . $data['id_parent']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Just updating?
|
||||||
|
else
|
||||||
|
{
|
||||||
|
foreach ($data as $key => $value)
|
||||||
|
$album->$key = $value;
|
||||||
|
|
||||||
|
$album->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirect to the album management page.
|
||||||
|
header('Location: ' . BASEURL . '/managealbums/');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,10 +33,11 @@ class EditAsset extends HTMLController
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Key info
|
// Key info
|
||||||
if (isset($_POST['title'], $_POST['date_captured'], $_POST['priority']))
|
if (isset($_POST['title'], $_POST['slug'], $_POST['date_captured'], $_POST['priority']))
|
||||||
{
|
{
|
||||||
$date_captured = !empty($_POST['date_captured']) ? new DateTime($_POST['date_captured']) : null;
|
$date_captured = !empty($_POST['date_captured']) ? new DateTime($_POST['date_captured']) : null;
|
||||||
$asset->setKeyData(htmlentities($_POST['title']), $date_captured, intval($_POST['priority']));
|
$slug = strtr($_POST['slug'], [' ' => '-', '--' => '-', '&' => 'and', '=>' => '', "'" => "", ":"=> "", '\\' => '-']);
|
||||||
|
$asset->setKeyData(htmlspecialchars($_POST['title']), $slug, $date_captured, intval($_POST['priority']));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle tags
|
// Handle tags
|
||||||
@@ -76,11 +77,11 @@ class EditAsset extends HTMLController
|
|||||||
$image->removeAllThumbnails();
|
$image->removeAllThumbnails();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
elseif (preg_match('~^thumb_(\d+)x(\d+)(_c[best]?)?$~', $_POST['replacement_target']))
|
elseif (preg_match('~^thumb_(\d+x\d+(?:_c[best]?)?)$~', $_POST['replacement_target'], $match))
|
||||||
{
|
{
|
||||||
$image = $asset->getImage();
|
$image = $asset->getImage();
|
||||||
if (($replace_result = $image->replaceThumbnail($_POST['replacement_target'], $_FILES['replacement']['tmp_name'])) !== 0)
|
if (($replace_result = $image->replaceThumbnail($match[1], $_FILES['replacement']['tmp_name'])) !== 0)
|
||||||
throw new Exception('Could not replace thumbnail \'' . $_POST['replacement_target'] . '\' with the uploaded file. Error code: ' . $replace_result);
|
throw new Exception('Could not replace thumbnail \'' . $match[1] . '\' with the uploaded file. Error code: ' . $replace_result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,26 +98,39 @@ class EditAsset extends HTMLController
|
|||||||
|
|
||||||
private function getThumbs(Asset $asset)
|
private function getThumbs(Asset $asset)
|
||||||
{
|
{
|
||||||
$path = $asset->getPath();
|
if (!$asset->isImage())
|
||||||
|
return [];
|
||||||
|
|
||||||
|
$image = $asset->getImage();
|
||||||
|
$subdir = $image->getSubdir();
|
||||||
|
$metadata = $image->getMeta();
|
||||||
|
$thumb_selectors = $image->getThumbnails();
|
||||||
|
|
||||||
$thumbs = [];
|
$thumbs = [];
|
||||||
$metadata = $asset->getMeta();
|
foreach ($thumb_selectors as $selector => $filename)
|
||||||
foreach ($metadata as $key => $meta)
|
|
||||||
{
|
{
|
||||||
if (!preg_match('~^thumb_(?<width>\d+)x(?<height>\d+)(?<suffix>_c(?<method>[best]?))?$~', $key, $thumb))
|
if (!preg_match('~^(?<width>\d+)x(?<height>\d+)(?<suffix>_c(?<method>[best]?))?$~', $selector, $thumb))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
$has_crop_boundary = isset($metadata['crop_' . $thumb['width'] . 'x' . $thumb['height']]);
|
$dimensions = $thumb['width'] . 'x' . $thumb['height'];
|
||||||
$has_custom_image = isset($metadata['custom_' . $thumb['width'] . 'x' . $thumb['height']]);
|
|
||||||
|
// Does the thumbnail exist on disk? If not, use an url to generate it.
|
||||||
|
if (!$filename || !file_exists(THUMBSDIR . '/' . $subdir . '/' . $filename))
|
||||||
|
$thumb_url = BASEURL . '/thumbnail/' . $image->getId() . '/' . $dimensions . ($thumb['suffix'] ?? '') . '/';
|
||||||
|
else
|
||||||
|
$thumb_url = THUMBSURL . '/' . $subdir . '/' . $filename;
|
||||||
|
|
||||||
|
$has_crop_boundary = isset($metadata['crop_' . $dimensions]);
|
||||||
|
$has_custom_image = isset($metadata['custom_' . $dimensions]);
|
||||||
|
|
||||||
$thumbs[] = [
|
$thumbs[] = [
|
||||||
'dimensions' => [(int) $thumb['width'], (int) $thumb['height']],
|
'dimensions' => [(int) $thumb['width'], (int) $thumb['height']],
|
||||||
'cropped' => !$has_custom_image && (!empty($thumb['suffix']) || $has_crop_boundary),
|
'cropped' => !$has_custom_image && (!empty($thumb['suffix']) || $has_crop_boundary),
|
||||||
'crop_method' => !$has_custom_image && !empty($thumb['method']) ? $thumb['method'] : (!empty($thumb['suffix']) ? 'c' : null),
|
'crop_method' => !$has_custom_image && !empty($thumb['method']) ? $thumb['method'] : (!empty($thumb['suffix']) ? 'c' : null),
|
||||||
'crop_region' => $has_crop_boundary ? $metadata['crop_' . $thumb['width'] . 'x' . $thumb['height']] : null,
|
'crop_region' => $has_crop_boundary ? $metadata['crop_' . $dimensions] : null,
|
||||||
'custom_image' => $has_custom_image,
|
'custom_image' => $has_custom_image,
|
||||||
'filename' => $meta,
|
'filename' => $filename,
|
||||||
'full_path' => THUMBSDIR . '/' . $path . '/' . $meta,
|
'url' => $thumb_url,
|
||||||
'url' => THUMBSURL . '/' . $path . '/' . $meta,
|
|
||||||
'status' => file_exists(THUMBSDIR . '/' . $path . '/' . $meta),
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,18 +147,19 @@ class EditAsset extends HTMLController
|
|||||||
$crop_value = $data->crop_width . ',' . $data->crop_height . ',' . $data->source_x . ',' . $data->source_y;
|
$crop_value = $data->crop_width . ',' . $data->crop_height . ',' . $data->source_x . ',' . $data->source_y;
|
||||||
$meta[$crop_key] = $crop_value;
|
$meta[$crop_key] = $crop_value;
|
||||||
|
|
||||||
// If we uploaded a custom thumbnail, stop considering it such.
|
// If we previously uploaded a custom thumbnail, stop considering it such.
|
||||||
$custom_key = 'custom_' . $data->thumb_width . 'x' . $data->thumb_height;
|
$custom_key = 'custom_' . $data->thumb_width . 'x' . $data->thumb_height;
|
||||||
if (isset($meta[$custom_key]))
|
if (isset($meta[$custom_key]))
|
||||||
|
{
|
||||||
|
// TODO: delete from disk
|
||||||
unset($meta[$custom_key]);
|
unset($meta[$custom_key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save meta changes so far.
|
||||||
|
$image->setMetaData($meta);
|
||||||
|
|
||||||
// Force a rebuild of related thumbnails.
|
// Force a rebuild of related thumbnails.
|
||||||
$thumb_key = 'thumb_' . $data->thumb_width . 'x' . $data->thumb_height;
|
$image->removeThumbnailsOfSize($data->thumb_width, $data->thumb_height);
|
||||||
foreach ($meta as $meta_key => $meta_value)
|
|
||||||
if ($meta_key === $thumb_key || strpos($meta_key, $thumb_key . '_') !== false)
|
|
||||||
unset($meta[$meta_key]);
|
|
||||||
|
|
||||||
$image->setMetaData($meta);
|
|
||||||
|
|
||||||
$payload = [
|
$payload = [
|
||||||
'key' => $crop_key,
|
'key' => $crop_key,
|
||||||
|
|||||||
194
controllers/EditTag.php
Normal file
194
controllers/EditTag.php
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
<?php
|
||||||
|
/*****************************************************************************
|
||||||
|
* EditTag.php
|
||||||
|
* Contains the tag edit controller.
|
||||||
|
*
|
||||||
|
* Kabuki CMS (C) 2013-2017, Aaron van Geffen
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
class EditTag extends HTMLController
|
||||||
|
{
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$id_tag = isset($_GET['id']) ? (int) $_GET['id'] : 0;
|
||||||
|
if (empty($id_tag) && !isset($_GET['add']))
|
||||||
|
throw new UnexpectedValueException('Requested tag not found or not requesting a new tag.');
|
||||||
|
|
||||||
|
if (!empty($id_tag))
|
||||||
|
$tag = Tag::fromId($id_tag);
|
||||||
|
|
||||||
|
// Are we allowed to edit this tag?
|
||||||
|
$user = Registry::get('user');
|
||||||
|
if (!($user->isAdmin() || $user->getUserId() == $tag->id_user_owner))
|
||||||
|
throw new NotAllowedException();
|
||||||
|
|
||||||
|
// Adding an tag?
|
||||||
|
if (isset($_GET['add']))
|
||||||
|
{
|
||||||
|
parent::__construct('Add a new tag');
|
||||||
|
$form_title = 'Add a new tag';
|
||||||
|
$this->page->addClass('edittag');
|
||||||
|
}
|
||||||
|
// Deleting one?
|
||||||
|
elseif (isset($_GET['delete']))
|
||||||
|
{
|
||||||
|
// So far so good?
|
||||||
|
if (Session::validateSession('get') && $tag->kind !== 'Album' && $tag->delete())
|
||||||
|
{
|
||||||
|
header('Location: ' . BASEURL . '/managetags/');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
trigger_error('Cannot delete tag: an error occured while processing the request.', E_USER_ERROR);
|
||||||
|
}
|
||||||
|
// Editing one, then, surely.
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if ($tag->kind === 'Album')
|
||||||
|
trigger_error('Cannot edit tag: is actually an album.', E_USER_ERROR);
|
||||||
|
|
||||||
|
parent::__construct('Edit tag \'' . $tag->tag . '\'');
|
||||||
|
$form_title = 'Edit tag \'' . $tag->tag . '\'';
|
||||||
|
$this->page->addClass('edittag');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Session checking!
|
||||||
|
if (empty($_POST))
|
||||||
|
Session::resetSessionToken();
|
||||||
|
else
|
||||||
|
Session::validateSession();
|
||||||
|
|
||||||
|
if ($id_tag)
|
||||||
|
$after_form = '<a href="' . BASEURL . '/edittag/?id=' . $id_tag . '&delete&' . Session::getSessionTokenKey() . '=' . Session::getSessionToken() . '" class="btn btn-danger" onclick="return confirm(\'Are you sure you want to delete this tag? You cannot undo this!\');">Delete tag</a>';
|
||||||
|
elseif (!$id_tag)
|
||||||
|
$after_form = '<button name="submit_and_new" class="btn">Save and add another</button>';
|
||||||
|
|
||||||
|
$fields = [
|
||||||
|
'kind' => [
|
||||||
|
'type' => 'select',
|
||||||
|
'label' => 'Kind of tag',
|
||||||
|
'options' => [
|
||||||
|
'Location' => 'Location',
|
||||||
|
'Person' => 'Person',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'id_user_owner' => [
|
||||||
|
'type' => 'select',
|
||||||
|
'label' => 'Owner',
|
||||||
|
'options' => [0 => '(nobody)'] + Member::getMemberMap(),
|
||||||
|
],
|
||||||
|
'tag' => [
|
||||||
|
'type' => 'text',
|
||||||
|
'label' => 'Tag title',
|
||||||
|
'size' => 50,
|
||||||
|
'maxlength' => 255,
|
||||||
|
],
|
||||||
|
'slug' => [
|
||||||
|
'type' => 'text',
|
||||||
|
'label' => 'URL slug',
|
||||||
|
'size' => 50,
|
||||||
|
'maxlength' => 255,
|
||||||
|
],
|
||||||
|
'description' => [
|
||||||
|
'type' => 'textbox',
|
||||||
|
'label' => 'Description',
|
||||||
|
'size' => 50,
|
||||||
|
'maxlength' => 255,
|
||||||
|
'is_optional' => true,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!$user->isAdmin())
|
||||||
|
{
|
||||||
|
unset($fields['kind']);
|
||||||
|
unset($fields['id_user_owner']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$form = new Form([
|
||||||
|
'request_url' => BASEURL . '/edittag/?' . ($id_tag ? 'id=' . $id_tag : 'add'),
|
||||||
|
'content_below' => $after_form,
|
||||||
|
'fields' => $fields,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create the form, add in default values.
|
||||||
|
$form->setData($id_tag ? get_object_vars($tag) : $_POST);
|
||||||
|
$formview = new FormView($form, $form_title ?? '');
|
||||||
|
$this->page->adopt($formview);
|
||||||
|
|
||||||
|
if (!empty($id_tag))
|
||||||
|
{
|
||||||
|
list($assets, $num_assets) = AssetIterator::getByOptions([
|
||||||
|
'direction' => 'desc',
|
||||||
|
'limit' => 500,
|
||||||
|
'id_tag' => $id_tag,
|
||||||
|
], true);
|
||||||
|
|
||||||
|
if ($num_assets > 0)
|
||||||
|
$this->page->adopt(new FeaturedThumbnailManager($assets, $id_tag ? $tag->id_asset_thumb : 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($_POST['changeThumbnail']))
|
||||||
|
$this->processThumbnail($tag);
|
||||||
|
elseif (!empty($_POST))
|
||||||
|
$this->processTagDetails($form, $id_tag, $tag ?? null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function processThumbnail($tag)
|
||||||
|
{
|
||||||
|
if (empty($_POST))
|
||||||
|
return;
|
||||||
|
|
||||||
|
$tag->id_asset_thumb = $_POST['featuredThumbnail'];
|
||||||
|
$tag->save();
|
||||||
|
|
||||||
|
header('Location: ' . BASEURL . '/edittag/?id=' . $tag->id_tag);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function processTagDetails($form, $id_tag, $tag)
|
||||||
|
{
|
||||||
|
if (!empty($_POST))
|
||||||
|
{
|
||||||
|
$form->verify($_POST);
|
||||||
|
|
||||||
|
// Anything missing?
|
||||||
|
if (!empty($form->getMissing()))
|
||||||
|
return $formview->adopt(new Alert('Some data missing', 'Please fill out the following fields: ' . implode(', ', $form->getMissing()), 'danger'));
|
||||||
|
|
||||||
|
$data = $form->getData();
|
||||||
|
$data['id_parent'] = 0;
|
||||||
|
|
||||||
|
// Quick stripping.
|
||||||
|
$data['slug'] = strtr($data['slug'], [' ' => '-', '--' => '-', '&' => 'and', '=>' => '', "'" => "", ":"=> "", '/' => '-', '\\' => '-']);
|
||||||
|
|
||||||
|
// Creating a new tag?
|
||||||
|
if (!$id_tag)
|
||||||
|
{
|
||||||
|
$return = Tag::createNew($data);
|
||||||
|
if ($return === false)
|
||||||
|
return $formview->adopt(new Alert('Cannot create this tag', 'Something went wrong while creating the tag...', 'danger'));
|
||||||
|
|
||||||
|
if (isset($_POST['submit_and_new']))
|
||||||
|
{
|
||||||
|
header('Location: ' . BASEURL . '/edittag/?add');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Just updating?
|
||||||
|
else
|
||||||
|
{
|
||||||
|
foreach ($data as $key => $value)
|
||||||
|
$tag->$key = $value;
|
||||||
|
|
||||||
|
$tag->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirect to a clean page
|
||||||
|
if (Registry::get('user')->isAdmin())
|
||||||
|
header('Location: ' . BASEURL . '/managetags/');
|
||||||
|
else
|
||||||
|
header('Location: ' . BASEURL . '/edittag/?id=' . $id_tag);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,9 +24,8 @@ class EditUser extends HTMLController
|
|||||||
// Adding a user?
|
// Adding a user?
|
||||||
if (isset($_GET['add']))
|
if (isset($_GET['add']))
|
||||||
{
|
{
|
||||||
parent::__construct('Add a new user');
|
$form_title = 'Add a new user';
|
||||||
$view = new DummyBox('Add a new user');
|
parent::__construct($form_title);
|
||||||
$this->page->adopt($view);
|
|
||||||
$this->page->addClass('edituser');
|
$this->page->addClass('edituser');
|
||||||
}
|
}
|
||||||
// Deleting one?
|
// Deleting one?
|
||||||
@@ -50,9 +49,8 @@ class EditUser extends HTMLController
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
$user = Member::fromId($id_user);
|
$user = Member::fromId($id_user);
|
||||||
parent::__construct('Edit user \'' . $user->getFullName() . '\'');
|
$form_title = 'Edit user \'' . $user->getFullName() . '\'';
|
||||||
$view = new DummyBox('Edit user \'' . $user->getFullName() . '\'');
|
parent::__construct($form_title);
|
||||||
$this->page->adopt($view);
|
|
||||||
$this->page->addClass('edituser');
|
$this->page->addClass('edituser');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,8 +120,8 @@ class EditUser extends HTMLController
|
|||||||
|
|
||||||
// Create the form, add in default values.
|
// Create the form, add in default values.
|
||||||
$form->setData($id_user ? $user->getProps() : $_POST);
|
$form->setData($id_user ? $user->getProps() : $_POST);
|
||||||
$formview = new FormView($form);
|
$formview = new FormView($form, $form_title);
|
||||||
$view->adopt($formview);
|
$this->page->adopt($formview);
|
||||||
|
|
||||||
if (!empty($_POST))
|
if (!empty($_POST))
|
||||||
{
|
{
|
||||||
@@ -131,13 +129,13 @@ class EditUser extends HTMLController
|
|||||||
|
|
||||||
// Anything missing?
|
// Anything missing?
|
||||||
if (!empty($form->getMissing()))
|
if (!empty($form->getMissing()))
|
||||||
return $formview->adopt(new DummyBox('Some data missing', 'Please fill out the following fields: ' . implode(', ', $form->getMissing())));
|
return $formview->adopt(new Alert('Some data missing', 'Please fill out the following fields: ' . implode(', ', $form->getMissing()), 'danger'));
|
||||||
|
|
||||||
$data = $form->getData();
|
$data = $form->getData();
|
||||||
|
|
||||||
// Just to be on the safe side.
|
// Just to be on the safe side.
|
||||||
$data['first_name'] = htmlentities(trim($data['first_name']));
|
$data['first_name'] = htmlspecialchars(trim($data['first_name']));
|
||||||
$data['surname'] = htmlentities(trim($data['surname']));
|
$data['surname'] = htmlspecialchars(trim($data['surname']));
|
||||||
$data['emailaddress'] = trim($data['emailaddress']);
|
$data['emailaddress'] = trim($data['emailaddress']);
|
||||||
|
|
||||||
// Make sure there's a slug.
|
// Make sure there's a slug.
|
||||||
@@ -152,18 +150,18 @@ class EditUser extends HTMLController
|
|||||||
|
|
||||||
// If it looks like an e-mail address...
|
// If it looks like an e-mail address...
|
||||||
if (!empty($data['emailaddress']) && !preg_match('~^[^ ]+@[^ ]+\.[a-z]+$~', $data['emailaddress']))
|
if (!empty($data['emailaddress']) && !preg_match('~^[^ ]+@[^ ]+\.[a-z]+$~', $data['emailaddress']))
|
||||||
return $formview->adopt(new DummyBox('Email addresses invalid', 'The email address you entered is not a valid email address.'));
|
return $formview->adopt(new Alert('Email addresses invalid', 'The email address you entered is not a valid email address.', 'danger'));
|
||||||
// Check whether email address is already linked to an account in the database -- just not to the account we happen to be editing, of course.
|
// Check whether email address is already linked to an account in the database -- just not to the account we happen to be editing, of course.
|
||||||
elseif (!empty($data['emailaddress']) && Member::exists($data['emailaddress']) && !($id_user && $user->getEmailAddress() == $data['emailaddress']))
|
elseif (!empty($data['emailaddress']) && Member::exists($data['emailaddress']) && !($id_user && $user->getEmailAddress() == $data['emailaddress']))
|
||||||
return $formview->adopt(new DummyBox('Email address already in use', 'Another account is already using the e-mail address you entered.'));
|
return $formview->adopt(new Alert('Email address already in use', 'Another account is already using the e-mail address you entered.', 'danger'));
|
||||||
|
|
||||||
// Setting passwords? We'll need two!
|
// Setting passwords? We'll need two!
|
||||||
if (!$id_user || !empty($data['password1']) && !empty($data['password2']))
|
if (!$id_user || !empty($data['password1']) && !empty($data['password2']))
|
||||||
{
|
{
|
||||||
if (strlen($data['password1']) < 6 || !preg_match('~[^A-z]~', $data['password1']))
|
if (strlen($data['password1']) < 6 || !preg_match('~[^A-z]~', $data['password1']))
|
||||||
return $formview->adopt(new DummyBox('Password not acceptable', 'Please fill in a password that is at least six characters long and contains at least one non-alphabetic character (e.g. a number or symbol).'));
|
return $formview->adopt(new Alert('Password not acceptable', 'Please fill in a password that is at least six characters long and contains at least one non-alphabetic character (e.g. a number or symbol).', 'danger'));
|
||||||
elseif ($data['password1'] !== $data['password2'])
|
elseif ($data['password1'] !== $data['password2'])
|
||||||
return $formview->adopt(new DummyBox('Passwords do not match', 'The passwords you entered do not match. Please try again.'));
|
return $formview->adopt(new Alert('Passwords do not match', 'The passwords you entered do not match. Please try again.', 'danger'));
|
||||||
else
|
else
|
||||||
$data['password'] = $data['password1'];
|
$data['password'] = $data['password1'];
|
||||||
|
|
||||||
@@ -175,7 +173,7 @@ class EditUser extends HTMLController
|
|||||||
{
|
{
|
||||||
$return = Member::createNew($data);
|
$return = Member::createNew($data);
|
||||||
if ($return === false)
|
if ($return === false)
|
||||||
return $formview->adopt(new DummyBox('Cannot create this user', 'Something went wrong while creating the user...'));
|
return $formview->adopt(new Alert('Cannot create this user', 'Something went wrong while creating the user...', 'danger'));
|
||||||
|
|
||||||
if (isset($_POST['submit_and_new']))
|
if (isset($_POST['submit_and_new']))
|
||||||
{
|
{
|
||||||
|
|||||||
27
controllers/GenerateThumbnail.php
Normal file
27
controllers/GenerateThumbnail.php
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
/*****************************************************************************
|
||||||
|
* GenerateThumbnail.php
|
||||||
|
* Contains the asynchronous thumbnail generation controller
|
||||||
|
*
|
||||||
|
* Kabuki CMS (C) 2013-2017, Aaron van Geffen
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
class GenerateThumbnail extends HTMLController
|
||||||
|
{
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$asset = Asset::fromId($_GET['id']);
|
||||||
|
if (empty($asset) || !$asset->isImage())
|
||||||
|
throw new NotFoundException('Image not found');
|
||||||
|
|
||||||
|
$image = $asset->getImage();
|
||||||
|
$crop_mode = isset($_GET['mode']) ? $_GET['mode'] : false;
|
||||||
|
$url = $image->getThumbnailUrl($_GET['width'], $_GET['height'], $crop_mode, true, true);
|
||||||
|
|
||||||
|
if ($url)
|
||||||
|
{
|
||||||
|
header('Location: ' . $url);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,7 +12,6 @@
|
|||||||
abstract class HTMLController
|
abstract class HTMLController
|
||||||
{
|
{
|
||||||
protected $page;
|
protected $page;
|
||||||
protected $admin_bar;
|
|
||||||
|
|
||||||
public function __construct($title)
|
public function __construct($title)
|
||||||
{
|
{
|
||||||
@@ -22,8 +21,6 @@ abstract class HTMLController
|
|||||||
if (Registry::get('user')->isAdmin())
|
if (Registry::get('user')->isAdmin())
|
||||||
{
|
{
|
||||||
$this->page->appendStylesheet(BASEURL . '/css/admin.css');
|
$this->page->appendStylesheet(BASEURL . '/css/admin.css');
|
||||||
$this->admin_bar = new AdminBar();
|
|
||||||
$this->page->adopt($this->admin_bar);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,19 +3,16 @@
|
|||||||
* JSONController.php
|
* JSONController.php
|
||||||
* Contains the key JSON controller
|
* Contains the key JSON controller
|
||||||
*
|
*
|
||||||
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
* Kabuki CMS (C) 2013-2019, Aaron van Geffen
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
/**
|
|
||||||
* The abstract class that allows easy creation of json replies.
|
|
||||||
*/
|
|
||||||
class JSONController
|
class JSONController
|
||||||
{
|
{
|
||||||
protected $payload;
|
protected $payload;
|
||||||
|
|
||||||
public function showContent()
|
public function showContent()
|
||||||
{
|
{
|
||||||
header('Content-Type: text/json; charset=utf-8');
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
echo json_encode($this->payload);
|
echo json_encode($this->payload);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ class Login extends HTMLController
|
|||||||
parent::__construct('Log in - ' . SITE_TITLE);
|
parent::__construct('Log in - ' . SITE_TITLE);
|
||||||
$form = new LogInForm('Log in');
|
$form = new LogInForm('Log in');
|
||||||
if ($login_error)
|
if ($login_error)
|
||||||
$form->adopt(new Alert('', 'Invalid email address or password.', 'error'));
|
$form->adopt(new Alert('', 'Invalid email address or password.', 'danger'));
|
||||||
|
|
||||||
// Tried anything? Be helpful, at least.
|
// Tried anything? Be helpful, at least.
|
||||||
if (isset($_POST['emailaddress']))
|
if (isset($_POST['emailaddress']))
|
||||||
|
|||||||
88
controllers/ManageAlbums.php
Normal file
88
controllers/ManageAlbums.php
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
<?php
|
||||||
|
/*****************************************************************************
|
||||||
|
* ManageAlbums.php
|
||||||
|
* Contains the controller for admin album management.
|
||||||
|
*
|
||||||
|
* Kabuki CMS (C) 2013-2017, Aaron van Geffen
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
class ManageAlbums extends HTMLController
|
||||||
|
{
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
// Ensure it's just admins at this point.
|
||||||
|
if (!Registry::get('user')->isAdmin())
|
||||||
|
throw new NotAllowedException();
|
||||||
|
|
||||||
|
$options = [
|
||||||
|
'form' => [
|
||||||
|
'action' => BASEURL . '/editalbum/',
|
||||||
|
'method' => 'get',
|
||||||
|
'class' => 'col-md-6 text-end',
|
||||||
|
'buttons' => [
|
||||||
|
'add' => [
|
||||||
|
'type' => 'submit',
|
||||||
|
'caption' => 'Add new album',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'columns' => [
|
||||||
|
'id_album' => [
|
||||||
|
'value' => 'id_tag',
|
||||||
|
'header' => 'ID',
|
||||||
|
'is_sortable' => true,
|
||||||
|
],
|
||||||
|
'tag' => [
|
||||||
|
'header' => 'Album',
|
||||||
|
'is_sortable' => true,
|
||||||
|
'parse' => [
|
||||||
|
'link' => BASEURL . '/editalbum/?id={ID_TAG}',
|
||||||
|
'data' => 'tag',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'slug' => [
|
||||||
|
'header' => 'Slug',
|
||||||
|
'is_sortable' => true,
|
||||||
|
'parse' => [
|
||||||
|
'link' => BASEURL . '/editalbum/?id={ID_TAG}',
|
||||||
|
'data' => 'slug',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'count' => [
|
||||||
|
'header' => '# Photos',
|
||||||
|
'is_sortable' => true,
|
||||||
|
'value' => 'count',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'start' => !empty($_GET['start']) ? (int) $_GET['start'] : 0,
|
||||||
|
'sort_order' => !empty($_GET['order']) ? $_GET['order'] : null,
|
||||||
|
'sort_direction' => !empty($_GET['dir']) ? $_GET['dir'] : null,
|
||||||
|
'title' => 'Manage albums',
|
||||||
|
'no_items_label' => 'No albums meet the requirements of the current filter.',
|
||||||
|
'items_per_page' => 9999,
|
||||||
|
'index_class' => 'col-md-6',
|
||||||
|
'base_url' => BASEURL . '/managealbums/',
|
||||||
|
'get_data' => function($offset = 0, $limit = 9999, $order = '', $direction = 'up') {
|
||||||
|
if (!in_array($order, ['id_tag', 'tag', 'slug', 'count']))
|
||||||
|
$order = 'tag';
|
||||||
|
if (!in_array($direction, ['up', 'down']))
|
||||||
|
$direction = 'up';
|
||||||
|
|
||||||
|
$rows = PhotoAlbum::getHierarchy($order, $direction);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'rows' => $rows,
|
||||||
|
'order' => $order,
|
||||||
|
'direction' => ($direction == 'up' ? 'up' : 'down'),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
'get_count' => function() {
|
||||||
|
return 9999;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
$table = new GenericTable($options);
|
||||||
|
parent::__construct('Album management - Page ' . $table->getCurrentPage() .' - ' . SITE_TITLE);
|
||||||
|
$this->page->adopt(new TabularData($table));
|
||||||
|
}
|
||||||
|
}
|
||||||
153
controllers/ManageAssets.php
Normal file
153
controllers/ManageAssets.php
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
<?php
|
||||||
|
/*****************************************************************************
|
||||||
|
* ManageAssets.php
|
||||||
|
* Contains the asset management controller.
|
||||||
|
*
|
||||||
|
* Kabuki CMS (C) 2013-2017, Aaron van Geffen
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
class ManageAssets extends HTMLController
|
||||||
|
{
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
// Ensure it's just admins at this point.
|
||||||
|
if (!Registry::get('user')->isAdmin())
|
||||||
|
throw new NotAllowedException();
|
||||||
|
|
||||||
|
if (isset($_POST['deleteChecked'], $_POST['delete']) && Session::validateSession())
|
||||||
|
$this->handleAssetDeletion();
|
||||||
|
|
||||||
|
Session::resetSessionToken();
|
||||||
|
|
||||||
|
$options = [
|
||||||
|
'form' => [
|
||||||
|
'action' => BASEURL . '/manageassets/?' . Session::getSessionTokenKey() . '=' . Session::getSessionToken(),
|
||||||
|
'method' => 'post',
|
||||||
|
'class' => 'col-md-6 text-end',
|
||||||
|
'is_embed' => true,
|
||||||
|
'buttons' => [
|
||||||
|
'deleteChecked' => [
|
||||||
|
'type' => 'submit',
|
||||||
|
'caption' => 'Delete checked',
|
||||||
|
'class' => 'btn-danger',
|
||||||
|
'onclick' => 'return confirm(\'Are you sure you want to delete these items?\')',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'columns' => [
|
||||||
|
'checkbox' => [
|
||||||
|
'header' => '<input type="checkbox" id="selectall">',
|
||||||
|
'is_sortable' => false,
|
||||||
|
'parse' => [
|
||||||
|
'type' => 'function',
|
||||||
|
'data' => function($row) {
|
||||||
|
return '<input type="checkbox" class="asset_select" name="delete[]" value="' . $row['id_asset'] . '">';
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'id_asset' => [
|
||||||
|
'value' => 'id_asset',
|
||||||
|
'header' => 'ID',
|
||||||
|
'is_sortable' => true,
|
||||||
|
],
|
||||||
|
'subdir' => [
|
||||||
|
'value' => 'subdir',
|
||||||
|
'header' => 'Subdirectory',
|
||||||
|
'is_sortable' => true,
|
||||||
|
],
|
||||||
|
'filename' => [
|
||||||
|
'value' => 'filename',
|
||||||
|
'header' => 'Filename',
|
||||||
|
'is_sortable' => true,
|
||||||
|
'parse' => [
|
||||||
|
'type' => 'value',
|
||||||
|
'link' => BASEURL . '/editasset/?id={ID_ASSET}',
|
||||||
|
'data' => 'filename',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'id_user_uploaded' => [
|
||||||
|
'header' => 'User uploaded',
|
||||||
|
'is_sortable' => true,
|
||||||
|
'parse' => [
|
||||||
|
'type' => 'function',
|
||||||
|
'data' => function($row) {
|
||||||
|
if (!empty($row['id_user']))
|
||||||
|
return sprintf('<a href="%s/edituser/?id=%d">%s</a>', BASEURL, $row['id_user'],
|
||||||
|
$row['first_name'] . ' ' . $row['surname']);
|
||||||
|
else
|
||||||
|
return 'n/a';
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'dimensions' => [
|
||||||
|
'header' => 'Dimensions',
|
||||||
|
'is_sortable' => false,
|
||||||
|
'parse' => [
|
||||||
|
'type' => 'function',
|
||||||
|
'data' => function($row) {
|
||||||
|
if (!empty($row['image_width']))
|
||||||
|
return $row['image_width'] . ' x ' . $row['image_height'];
|
||||||
|
else
|
||||||
|
return 'n/a';
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'start' => !empty($_GET['start']) ? (int) $_GET['start'] : 0,
|
||||||
|
'sort_order' => !empty($_GET['order']) ? $_GET['order'] : '',
|
||||||
|
'sort_direction' => !empty($_GET['dir']) ? $_GET['dir'] : '',
|
||||||
|
'title' => 'Manage assets',
|
||||||
|
'no_items_label' => 'No assets meet the requirements of the current filter.',
|
||||||
|
'items_per_page' => 30,
|
||||||
|
'index_class' => 'col-md-6',
|
||||||
|
'base_url' => BASEURL . '/manageassets/',
|
||||||
|
'get_data' => function($offset = 0, $limit = 30, $order = '', $direction = 'down') {
|
||||||
|
if (!in_array($order, ['id_asset', 'id_user_uploaded', 'title', 'subdir', 'filename']))
|
||||||
|
$order = 'id_asset';
|
||||||
|
|
||||||
|
$data = Registry::get('db')->queryAssocs('
|
||||||
|
SELECT a.id_asset, a.subdir, a.filename,
|
||||||
|
a.image_width, a.image_height,
|
||||||
|
u.id_user, u.first_name, u.surname
|
||||||
|
FROM assets AS a
|
||||||
|
LEFT JOIN users AS u ON a.id_user_uploaded = u.id_user
|
||||||
|
ORDER BY {raw:order}
|
||||||
|
LIMIT {int:offset}, {int:limit}',
|
||||||
|
[
|
||||||
|
'order' => $order . ($direction == 'up' ? ' ASC' : ' DESC'),
|
||||||
|
'offset' => $offset,
|
||||||
|
'limit' => $limit,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'rows' => $data,
|
||||||
|
'order' => $order,
|
||||||
|
'direction' => $direction,
|
||||||
|
];
|
||||||
|
},
|
||||||
|
'get_count' => 'Asset::getCount',
|
||||||
|
];
|
||||||
|
|
||||||
|
$table = new GenericTable($options);
|
||||||
|
parent::__construct('Asset management - Page ' . $table->getCurrentPage());
|
||||||
|
|
||||||
|
$wrapper = new AssetManagementWrapper();
|
||||||
|
$this->page->adopt($wrapper);
|
||||||
|
$wrapper->adopt(new TabularData($table));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function handleAssetDeletion()
|
||||||
|
{
|
||||||
|
if (!isset($_POST['delete']) || !is_array($_POST['delete']))
|
||||||
|
throw new UnexpectedValueException();
|
||||||
|
|
||||||
|
foreach ($_POST['delete'] as $id_asset)
|
||||||
|
{
|
||||||
|
$asset = Asset::fromId($id_asset);
|
||||||
|
$asset->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
header('Location: ' . BASEURL . '/manageassets/');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ class ManageErrors extends HTMLController
|
|||||||
{
|
{
|
||||||
ErrorLog::flush();
|
ErrorLog::flush();
|
||||||
header('Location: ' . BASEURL . '/manageerrors/');
|
header('Location: ' . BASEURL . '/manageerrors/');
|
||||||
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
Session::resetSessionToken();
|
Session::resetSessionToken();
|
||||||
@@ -28,11 +29,12 @@ class ManageErrors extends HTMLController
|
|||||||
'form' => [
|
'form' => [
|
||||||
'action' => BASEURL . '/manageerrors/?' . Session::getSessionTokenKey() . '=' . Session::getSessionToken(),
|
'action' => BASEURL . '/manageerrors/?' . Session::getSessionTokenKey() . '=' . Session::getSessionToken(),
|
||||||
'method' => 'post',
|
'method' => 'post',
|
||||||
'class' => 'floatright',
|
'class' => 'col-md-6 text-end',
|
||||||
'buttons' => [
|
'buttons' => [
|
||||||
'flush' => [
|
'flush' => [
|
||||||
'type' => 'submit',
|
'type' => 'submit',
|
||||||
'caption' => 'Delete all',
|
'caption' => 'Delete all',
|
||||||
|
'class' => 'btn-danger',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@@ -46,9 +48,13 @@ class ManageErrors extends HTMLController
|
|||||||
'parse' => [
|
'parse' => [
|
||||||
'type' => 'function',
|
'type' => 'function',
|
||||||
'data' => function($row) {
|
'data' => function($row) {
|
||||||
return $row['message'] . '<br><div><a onclick="this.parentNode.childNodes[1].style.display=\'block\';this.style.display=\'none\';">Show debug info</a>' .
|
return $row['message'] . '<br>' .
|
||||||
'<pre style="display: none">' . $row['debug_info'] . '</pre></div>' .
|
'<div><a onclick="this.parentNode.childNodes[1].style.display=\'block\';this.style.display=\'none\';">Show debug info</a>' .
|
||||||
'<small><a href="' . BASEURL . $row['request_uri'] . '">' . $row['request_uri'] . '</a></small>';
|
'<pre style="display: none">' . htmlspecialchars($row['debug_info']) .
|
||||||
|
'</pre></div>' .
|
||||||
|
'<small><a href="' . BASEURL .
|
||||||
|
htmlspecialchars($row['request_uri']) . '">' .
|
||||||
|
htmlspecialchars($row['request_uri']) . '</a></small>';
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
'header' => 'Message / URL',
|
'header' => 'Message / URL',
|
||||||
@@ -84,7 +90,7 @@ class ManageErrors extends HTMLController
|
|||||||
'header' => 'UID',
|
'header' => 'UID',
|
||||||
'is_sortable' => true,
|
'is_sortable' => true,
|
||||||
'parse' => [
|
'parse' => [
|
||||||
'link' => BASEURL . '/member/?id={ID_USER}',
|
'link' => BASEURL . '/edituser/?id={ID_USER}',
|
||||||
'data' => 'id_user',
|
'data' => 'id_user',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@@ -94,7 +100,7 @@ class ManageErrors extends HTMLController
|
|||||||
'sort_direction' => !empty($_GET['dir']) ? $_GET['dir'] : '',
|
'sort_direction' => !empty($_GET['dir']) ? $_GET['dir'] : '',
|
||||||
'no_items_label' => "No errors to display -- we're all good!",
|
'no_items_label' => "No errors to display -- we're all good!",
|
||||||
'items_per_page' => 20,
|
'items_per_page' => 20,
|
||||||
'index_class' => 'floatleft',
|
'index_class' => 'col-md-6',
|
||||||
'base_url' => BASEURL . '/manageerrors/',
|
'base_url' => BASEURL . '/manageerrors/',
|
||||||
'get_count' => 'ErrorLog::getCount',
|
'get_count' => 'ErrorLog::getCount',
|
||||||
'get_data' => function($offset = 0, $limit = 20, $order = '', $direction = 'down') {
|
'get_data' => function($offset = 0, $limit = 20, $order = '', $direction = 'down') {
|
||||||
|
|||||||
@@ -14,9 +14,22 @@ class ManageTags extends HTMLController
|
|||||||
if (!Registry::get('user')->isAdmin())
|
if (!Registry::get('user')->isAdmin())
|
||||||
throw new NotAllowedException();
|
throw new NotAllowedException();
|
||||||
|
|
||||||
|
Session::resetSessionToken();
|
||||||
|
|
||||||
$options = [
|
$options = [
|
||||||
|
'form' => [
|
||||||
|
'action' => BASEURL . '/edittag/',
|
||||||
|
'method' => 'get',
|
||||||
|
'class' => 'col-md-6 text-end',
|
||||||
|
'buttons' => [
|
||||||
|
'add' => [
|
||||||
|
'type' => 'submit',
|
||||||
|
'caption' => 'Add new tag',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
'columns' => [
|
'columns' => [
|
||||||
'id_post' => [
|
'id_tag' => [
|
||||||
'value' => 'id_tag',
|
'value' => 'id_tag',
|
||||||
'header' => 'ID',
|
'header' => 'ID',
|
||||||
'is_sortable' => true,
|
'is_sortable' => true,
|
||||||
@@ -25,7 +38,7 @@ class ManageTags extends HTMLController
|
|||||||
'header' => 'Tag',
|
'header' => 'Tag',
|
||||||
'is_sortable' => true,
|
'is_sortable' => true,
|
||||||
'parse' => [
|
'parse' => [
|
||||||
'link' => BASEURL . '/managetag/?id={ID_TAG}',
|
'link' => BASEURL . '/edittag/?id={ID_TAG}',
|
||||||
'data' => 'tag',
|
'data' => 'tag',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@@ -33,14 +46,23 @@ class ManageTags extends HTMLController
|
|||||||
'header' => 'Slug',
|
'header' => 'Slug',
|
||||||
'is_sortable' => true,
|
'is_sortable' => true,
|
||||||
'parse' => [
|
'parse' => [
|
||||||
'link' => BASEURL . '/managetag/?id={ID_TAG}',
|
'link' => BASEURL . '/edittag/?id={ID_TAG}',
|
||||||
'data' => 'slug',
|
'data' => 'slug',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
'kind' => [
|
'id_user_owner' => [
|
||||||
'header' => 'Kind',
|
'header' => 'Owning user',
|
||||||
'is_sortable' => true,
|
'is_sortable' => true,
|
||||||
'value' => 'kind',
|
'parse' => [
|
||||||
|
'type' => 'function',
|
||||||
|
'data' => function($row) {
|
||||||
|
if (!empty($row['id_user']))
|
||||||
|
return sprintf('<a href="%s/edituser/?id=%d">%s</a>', BASEURL, $row['id_user'],
|
||||||
|
$row['first_name'] . ' ' . $row['surname']);
|
||||||
|
else
|
||||||
|
return 'n/a';
|
||||||
|
},
|
||||||
|
],
|
||||||
],
|
],
|
||||||
'count' => [
|
'count' => [
|
||||||
'header' => 'Cardinality',
|
'header' => 'Cardinality',
|
||||||
@@ -53,23 +75,27 @@ class ManageTags extends HTMLController
|
|||||||
'sort_direction' => !empty($_GET['dir']) ? $_GET['dir'] : null,
|
'sort_direction' => !empty($_GET['dir']) ? $_GET['dir'] : null,
|
||||||
'title' => 'Manage tags',
|
'title' => 'Manage tags',
|
||||||
'no_items_label' => 'No tags meet the requirements of the current filter.',
|
'no_items_label' => 'No tags meet the requirements of the current filter.',
|
||||||
'items_per_page' => 25,
|
'items_per_page' => 30,
|
||||||
|
'index_class' => 'col-md-6',
|
||||||
'base_url' => BASEURL . '/managetags/',
|
'base_url' => BASEURL . '/managetags/',
|
||||||
'get_data' => function($offset = 0, $limit = 15, $order = '', $direction = 'up') {
|
'get_data' => function($offset = 0, $limit = 30, $order = '', $direction = 'up') {
|
||||||
if (!in_array($order, ['id_post', 'tag', 'slug', 'kind', 'count']))
|
if (!in_array($order, ['id_tag', 'tag', 'slug', 'kind', 'count']))
|
||||||
$order = 'tag';
|
$order = 'tag';
|
||||||
if (!in_array($direction, ['up', 'down']))
|
if (!in_array($direction, ['up', 'down']))
|
||||||
$direction = 'up';
|
$direction = 'up';
|
||||||
|
|
||||||
$data = Registry::get('db')->queryAssocs('
|
$data = Registry::get('db')->queryAssocs('
|
||||||
SELECT *
|
SELECT t.*, u.id_user, u.first_name, u.surname
|
||||||
FROM tags
|
FROM tags AS t
|
||||||
|
LEFT JOIN users AS u ON t.id_user_owner = u.id_user
|
||||||
|
WHERE kind != {string:album}
|
||||||
ORDER BY {raw:order}
|
ORDER BY {raw:order}
|
||||||
LIMIT {int:offset}, {int:limit}',
|
LIMIT {int:offset}, {int:limit}',
|
||||||
[
|
[
|
||||||
'order' => $order . ($direction == 'up' ? ' ASC' : ' DESC'),
|
'order' => $order . ($direction == 'up' ? ' ASC' : ' DESC'),
|
||||||
'offset' => $offset,
|
'offset' => $offset,
|
||||||
'limit' => $limit,
|
'limit' => $limit,
|
||||||
|
'album' => 'Album',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@@ -81,7 +107,9 @@ class ManageTags extends HTMLController
|
|||||||
'get_count' => function() {
|
'get_count' => function() {
|
||||||
return Registry::get('db')->queryValue('
|
return Registry::get('db')->queryValue('
|
||||||
SELECT COUNT(*)
|
SELECT COUNT(*)
|
||||||
FROM tags');
|
FROM tags
|
||||||
|
WHERE kind != {string:album}',
|
||||||
|
['album' => 'Album']);
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -14,11 +14,13 @@ class ManageUsers extends HTMLController
|
|||||||
if (!Registry::get('user')->isAdmin())
|
if (!Registry::get('user')->isAdmin())
|
||||||
throw new NotAllowedException();
|
throw new NotAllowedException();
|
||||||
|
|
||||||
|
Session::resetSessionToken();
|
||||||
|
|
||||||
$options = [
|
$options = [
|
||||||
'form' => [
|
'form' => [
|
||||||
'action' => BASEURL . '/edituser/',
|
'action' => BASEURL . '/edituser/',
|
||||||
'method' => 'get',
|
'method' => 'get',
|
||||||
'class' => 'floatright',
|
'class' => 'col-md-6 text-end',
|
||||||
'buttons' => [
|
'buttons' => [
|
||||||
'add' => [
|
'add' => [
|
||||||
'type' => 'submit',
|
'type' => 'submit',
|
||||||
@@ -93,10 +95,10 @@ class ManageUsers extends HTMLController
|
|||||||
'sort_direction' => !empty($_GET['dir']) ? $_GET['dir'] : '',
|
'sort_direction' => !empty($_GET['dir']) ? $_GET['dir'] : '',
|
||||||
'title' => 'Manage users',
|
'title' => 'Manage users',
|
||||||
'no_items_label' => 'No users meet the requirements of the current filter.',
|
'no_items_label' => 'No users meet the requirements of the current filter.',
|
||||||
'items_per_page' => 15,
|
'items_per_page' => 30,
|
||||||
'index_class' => 'floatleft',
|
'index_class' => 'col-md-6',
|
||||||
'base_url' => BASEURL . '/manageusers/',
|
'base_url' => BASEURL . '/manageusers/',
|
||||||
'get_data' => function($offset = 0, $limit = 15, $order = '', $direction = 'down') {
|
'get_data' => function($offset = 0, $limit = 30, $order = '', $direction = 'down') {
|
||||||
if (!in_array($order, ['id_user', 'surname', 'first_name', 'slug', 'emailaddress', 'last_action_time', 'ip_address', 'is_admin']))
|
if (!in_array($order, ['id_user', 'surname', 'first_name', 'slug', 'emailaddress', 'last_action_time', 'ip_address', 'is_admin']))
|
||||||
$order = 'id_user';
|
$order = 'id_user';
|
||||||
|
|
||||||
|
|||||||
@@ -41,8 +41,9 @@ class ProvideAutoSuggest extends JSONController
|
|||||||
$results = Tag::matchPeople($data);
|
$results = Tag::matchPeople($data);
|
||||||
foreach ($results as $id_tag => $tag)
|
foreach ($results as $id_tag => $tag)
|
||||||
$this->payload['items'][] = [
|
$this->payload['items'][] = [
|
||||||
'label' => $tag,
|
'label' => $tag['tag'],
|
||||||
'id_tag' => $id_tag,
|
'id_tag' => $id_tag,
|
||||||
|
'url' => BASEURL . '/' . $tag['slug'] . '/',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -52,11 +53,11 @@ class ProvideAutoSuggest extends JSONController
|
|||||||
// It better not already exist!
|
// It better not already exist!
|
||||||
if (Tag::exactMatch($_REQUEST['tag']))
|
if (Tag::exactMatch($_REQUEST['tag']))
|
||||||
{
|
{
|
||||||
$this->payload = ['error' => true, 'msg' => "Tag already exists!"];
|
$this->payload = ['error' => true, 'msg' => 'Tag already exists!'];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$label = htmlentities(trim($_REQUEST['tag']));
|
$label = htmlspecialchars(trim($_REQUEST['tag']));
|
||||||
$slug = strtr($label, [' ' => '-']);
|
$slug = strtr($label, [' ' => '-']);
|
||||||
$tag = Tag::createNew([
|
$tag = Tag::createNew([
|
||||||
'tag' => $label,
|
'tag' => $label,
|
||||||
@@ -67,7 +68,7 @@ class ProvideAutoSuggest extends JSONController
|
|||||||
// Did we succeed?
|
// Did we succeed?
|
||||||
if (!$tag)
|
if (!$tag)
|
||||||
{
|
{
|
||||||
$this->payload = ['error' => true, 'msg' => "Could not create tag."];
|
$this->payload = ['error' => true, 'msg' => 'Could not create tag.'];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ class ResetPassword extends HTMLController
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
$form->adopt(new Alert('Some fields require your attention', '<ul><li>' . implode('</li><li>', $missing) . '</li></ul>', 'error'));
|
$form->adopt(new Alert('Some fields require your attention', '<ul><li>' . implode('</li><li>', $missing) . '</li></ul>', 'danger'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -63,7 +63,7 @@ class ResetPassword extends HTMLController
|
|||||||
$id_user = Authentication::getUserid(trim($_POST['emailaddress']));
|
$id_user = Authentication::getUserid(trim($_POST['emailaddress']));
|
||||||
if ($id_user === false)
|
if ($id_user === false)
|
||||||
{
|
{
|
||||||
$form->adopt(new Alert('Invalid email address', 'The email address you provided could not be found in our system. Please try again.', 'error'));
|
$form->adopt(new Alert('Invalid email address', 'The email address you provided could not be found in our system. Please try again.', 'danger'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,8 +42,11 @@ class UploadMedia extends HTMLController
|
|||||||
$new_ids[] = $asset->getId();
|
$new_ids[] = $asset->getId();
|
||||||
$asset->linkTags([$tag->id_tag]);
|
$asset->linkTags([$tag->id_tag]);
|
||||||
|
|
||||||
$tag->id_asset_thumb = $asset->getId();
|
if (empty($tag->id_asset_thumb))
|
||||||
$tag->save();
|
{
|
||||||
|
$tag->id_asset_thumb = $asset->getId();
|
||||||
|
$tag->save();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isset($_REQUEST['format']) && $_REQUEST['format'] === 'json')
|
if (isset($_REQUEST['format']) && $_REQUEST['format'] === 'json')
|
||||||
|
|||||||
@@ -52,8 +52,9 @@ class ViewPeople extends HTMLController
|
|||||||
'start' => $start,
|
'start' => $start,
|
||||||
'base_url' => BASEURL . '/people/',
|
'base_url' => BASEURL . '/people/',
|
||||||
'page_slug' => 'page/%PAGE%/',
|
'page_slug' => 'page/%PAGE%/',
|
||||||
|
'index_class' => 'pagination-lg justify-content-center',
|
||||||
]);
|
]);
|
||||||
$this->page->adopt(new Pagination($pagination));
|
$this->page->adopt(new PageIndexWidget($pagination));
|
||||||
|
|
||||||
$this->page->setCanonicalUrl(BASEURL . '/people/' . ($page > 1 ? 'page/' . $page . '/' : ''));
|
$this->page->setCanonicalUrl(BASEURL . '/people/' . ($page > 1 ? 'page/' . $page . '/' : ''));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,17 +11,49 @@ class ViewPhoto extends HTMLController
|
|||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
// Ensure we're logged in at this point.
|
// Ensure we're logged in at this point.
|
||||||
if (!Registry::get('user')->isLoggedIn())
|
$user = Registry::get('user');
|
||||||
|
if (!$user->isLoggedIn())
|
||||||
throw new NotAllowedException();
|
throw new NotAllowedException();
|
||||||
|
|
||||||
$photo = Asset::fromSlug($_GET['slug']);
|
$photo = Asset::fromSlug($_GET['slug']);
|
||||||
if (empty($photo))
|
if (empty($photo))
|
||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
|
|
||||||
|
parent::__construct($photo->getTitle() . ' - ' . SITE_TITLE);
|
||||||
|
|
||||||
|
$author = $photo->getAuthor();
|
||||||
|
|
||||||
|
if (isset($_REQUEST['confirm_delete']) || isset($_REQUEST['delete_confirmed']))
|
||||||
|
$this->handleConfirmDelete($user, $author, $photo);
|
||||||
|
else
|
||||||
|
$this->handleViewPhoto($user, $author, $photo);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function handleConfirmDelete(User $user, User $author, Asset $photo)
|
||||||
|
{
|
||||||
|
if (!($user->isAdmin() || $user->getUserId() === $author->getUserId()))
|
||||||
|
throw new NotAllowedException();
|
||||||
|
|
||||||
|
if (isset($_REQUEST['confirm_delete']))
|
||||||
|
{
|
||||||
|
$page = new ConfirmDeletePage($photo->getImage());
|
||||||
|
$this->page->adopt($page);
|
||||||
|
}
|
||||||
|
elseif (isset($_REQUEST['delete_confirmed']))
|
||||||
|
{
|
||||||
|
$album_url = $photo->getSubdir();
|
||||||
|
$photo->delete();
|
||||||
|
|
||||||
|
header('Location: ' . BASEURL . '/' . $album_url);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function handleViewPhoto(User $user, User $author, Asset $photo)
|
||||||
|
{
|
||||||
if (!empty($_POST))
|
if (!empty($_POST))
|
||||||
$this->handleTagging($photo->getImage());
|
$this->handleTagging($photo->getImage());
|
||||||
|
|
||||||
parent::__construct($photo->getTitle() . ' - ' . SITE_TITLE);
|
|
||||||
$page = new PhotoPage($photo->getImage());
|
$page = new PhotoPage($photo->getImage());
|
||||||
|
|
||||||
// Exif data?
|
// Exif data?
|
||||||
@@ -43,12 +75,11 @@ class ViewPhoto extends HTMLController
|
|||||||
if ($next_url)
|
if ($next_url)
|
||||||
$page->setNextPhotoUrl($next_url);
|
$page->setNextPhotoUrl($next_url);
|
||||||
|
|
||||||
|
if ($user->isAdmin() || $user->getUserId() === $author->getUserId())
|
||||||
|
$page->setIsAssetOwner(true);
|
||||||
|
|
||||||
$this->page->adopt($page);
|
$this->page->adopt($page);
|
||||||
$this->page->setCanonicalUrl($photo->getPageUrl());
|
$this->page->setCanonicalUrl($photo->getPageUrl());
|
||||||
|
|
||||||
// Add an edit button to the admin bar.
|
|
||||||
if (Registry::get('user')->isAdmin())
|
|
||||||
$this->admin_bar->appendItem(BASEURL . '/editasset/?id=' . $photo->getId(), 'Edit this photo');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function handleTagging(Image $photo)
|
private function handleTagging(Image $photo)
|
||||||
@@ -63,8 +94,19 @@ class ViewPhoto extends HTMLController
|
|||||||
}
|
}
|
||||||
|
|
||||||
// We are!
|
// We are!
|
||||||
$photo->linkTags([(int) $_POST['id_tag']]);
|
if (!isset($_POST['delete']))
|
||||||
echo json_encode(['success' => true]);
|
{
|
||||||
exit;
|
$photo->linkTags([(int) $_POST['id_tag']]);
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ... deleting, that is.
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$photo->unlinkTags([(int) $_POST['id_tag']]);
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,19 +60,7 @@ class ViewPhotoAlbum extends HTMLController
|
|||||||
|
|
||||||
// Can we do fancy things here?
|
// Can we do fancy things here?
|
||||||
// !!! TODO: permission system?
|
// !!! TODO: permission system?
|
||||||
$buttons = [];
|
$buttons = $this->getAlbumButtons($id_tag, $tag ?? null);
|
||||||
if (Registry::get('user')->isLoggedIn())
|
|
||||||
$buttons[] = [
|
|
||||||
'url' => BASEURL . '/uploadmedia/?tag=' . $id_tag,
|
|
||||||
'caption' => 'Upload new photos here',
|
|
||||||
];
|
|
||||||
if (Registry::get('user')->isAdmin())
|
|
||||||
$buttons[] = [
|
|
||||||
'url' => BASEURL . '/addalbum/?tag=' . $id_tag,
|
|
||||||
'caption' => 'Create new subalbum here',
|
|
||||||
];
|
|
||||||
|
|
||||||
// Enough actions for a button box?
|
|
||||||
if (!empty($buttons))
|
if (!empty($buttons))
|
||||||
$this->page->adopt(new AlbumButtonBox($buttons));
|
$this->page->adopt(new AlbumButtonBox($buttons));
|
||||||
|
|
||||||
@@ -103,8 +91,9 @@ class ViewPhotoAlbum extends HTMLController
|
|||||||
'start' => (isset($_GET['page']) ? $_GET['page'] - 1 : 0) * self::PER_PAGE,
|
'start' => (isset($_GET['page']) ? $_GET['page'] - 1 : 0) * self::PER_PAGE,
|
||||||
'base_url' => BASEURL . '/' . (isset($_GET['tag']) ? $_GET['tag'] . '/' : ''),
|
'base_url' => BASEURL . '/' . (isset($_GET['tag']) ? $_GET['tag'] . '/' : ''),
|
||||||
'page_slug' => 'page/%PAGE%/',
|
'page_slug' => 'page/%PAGE%/',
|
||||||
|
'index_class' => 'pagination-lg justify-content-center',
|
||||||
]);
|
]);
|
||||||
$this->page->adopt(new Pagination($index));
|
$this->page->adopt(new PageIndexWidget($index));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the canonical url.
|
// Set the canonical url.
|
||||||
@@ -148,13 +137,67 @@ class ViewPhotoAlbum extends HTMLController
|
|||||||
'id_tag' => $album['id_tag'],
|
'id_tag' => $album['id_tag'],
|
||||||
'caption' => $album['tag'],
|
'caption' => $album['tag'],
|
||||||
'link' => BASEURL . '/' . $album['slug'] . '/',
|
'link' => BASEURL . '/' . $album['slug'] . '/',
|
||||||
'thumbnail' => !empty($album['id_asset_thumb']) ? $assets[$album['id_asset_thumb']]->getImage() : null,
|
'thumbnail' => !empty($album['id_asset_thumb']) && isset($assets[$album['id_asset_thumb']])
|
||||||
|
? $assets[$album['id_asset_thumb']]->getImage() : null,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
return $albums;
|
return $albums;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function getAlbumButtons($id_tag, $tag)
|
||||||
|
{
|
||||||
|
$buttons = [];
|
||||||
|
$user = Registry::get('user');
|
||||||
|
|
||||||
|
if ($user->isLoggedIn())
|
||||||
|
{
|
||||||
|
$buttons[] = [
|
||||||
|
'url' => BASEURL . '/download/?tag=' . $id_tag,
|
||||||
|
'caption' => 'Download album',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($tag))
|
||||||
|
{
|
||||||
|
if ($tag->kind === 'Album')
|
||||||
|
{
|
||||||
|
$buttons[] = [
|
||||||
|
'url' => BASEURL . '/uploadmedia/?tag=' . $id_tag,
|
||||||
|
'caption' => 'Upload photos here',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($user->isAdmin())
|
||||||
|
{
|
||||||
|
if ($tag->kind === 'Album')
|
||||||
|
{
|
||||||
|
$buttons[] = [
|
||||||
|
'url' => BASEURL . '/editalbum/?id=' . $id_tag,
|
||||||
|
'caption' => 'Edit album',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
elseif ($tag->kind === 'Person')
|
||||||
|
{
|
||||||
|
$buttons[] = [
|
||||||
|
'url' => BASEURL . '/edittag/?id=' . $id_tag,
|
||||||
|
'caption' => 'Edit tag',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($user->isAdmin() && (!isset($tag) || $tag->kind === 'Album'))
|
||||||
|
{
|
||||||
|
$buttons[] = [
|
||||||
|
'url' => BASEURL . '/addalbum/?tag=' . $id_tag,
|
||||||
|
'caption' => 'Create subalbum',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $buttons;
|
||||||
|
}
|
||||||
|
|
||||||
public function __destruct()
|
public function __destruct()
|
||||||
{
|
{
|
||||||
if (isset($this->iterator))
|
if (isset($this->iterator))
|
||||||
|
|||||||
@@ -46,8 +46,9 @@ class ViewTimeline extends HTMLController
|
|||||||
'start' => (isset($_GET['page']) ? $_GET['page'] - 1 : 0) * self::PER_PAGE,
|
'start' => (isset($_GET['page']) ? $_GET['page'] - 1 : 0) * self::PER_PAGE,
|
||||||
'base_url' => BASEURL . '/timeline/',
|
'base_url' => BASEURL . '/timeline/',
|
||||||
'page_slug' => 'page/%PAGE%/',
|
'page_slug' => 'page/%PAGE%/',
|
||||||
|
'index_class' => 'pagination-lg justify-content-center',
|
||||||
]);
|
]);
|
||||||
$this->page->adopt(new Pagination($index));
|
$this->page->adopt(new PageIndexWidget($index));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the canonical url.
|
// Set the canonical url.
|
||||||
|
|||||||
@@ -1,319 +0,0 @@
|
|||||||
<?php
|
|
||||||
/*****************************************************************************
|
|
||||||
* import_albums.php
|
|
||||||
* Imports albums from a Gallery 3 database.
|
|
||||||
*
|
|
||||||
* Kabuki CMS (C) 2013-2016, Aaron van Geffen
|
|
||||||
*****************************************************************************/
|
|
||||||
|
|
||||||
// Include the project's configuration.
|
|
||||||
require_once 'config.php';
|
|
||||||
|
|
||||||
// Set up the autoloader.
|
|
||||||
require_once 'vendor/autoload.php';
|
|
||||||
|
|
||||||
// Initialise the database.
|
|
||||||
$db = new Database(DB_SERVER, DB_USER, DB_PASS, DB_NAME);
|
|
||||||
$pdb = new Database(DB_SERVER, DB_USER, DB_PASS, "hashru_gallery");
|
|
||||||
Registry::set('db', $db);
|
|
||||||
|
|
||||||
// Do some authentication checks.
|
|
||||||
Session::start();
|
|
||||||
Registry::set('user', Authentication::isLoggedIn() ? Member::fromId($_SESSION['user_id']) : new Guest());
|
|
||||||
|
|
||||||
// Enable debugging.
|
|
||||||
//set_error_handler('ErrorHandler::handleError');
|
|
||||||
ini_set("display_errors", DEBUG ? "On" : "Off");
|
|
||||||
|
|
||||||
/*******************************
|
|
||||||
* STEP 0: USERS
|
|
||||||
*******************************/
|
|
||||||
|
|
||||||
$num_users = $pdb->queryValue('
|
|
||||||
SELECT COUNT(*)
|
|
||||||
FROM users');
|
|
||||||
|
|
||||||
echo $num_users, ' users to import.', "\n";
|
|
||||||
|
|
||||||
$rs_users = $pdb->query('
|
|
||||||
SELECT id, name, full_name, password, last_login, email, admin
|
|
||||||
FROM users
|
|
||||||
WHERE id > 1
|
|
||||||
ORDER BY id ASC');
|
|
||||||
|
|
||||||
$old_user_id_to_new_user_id = [];
|
|
||||||
|
|
||||||
while ($user = $pdb->fetch_assoc($rs_users))
|
|
||||||
{
|
|
||||||
// Check whether a user already exists for this e-mail address.
|
|
||||||
if (!($id_user = Authentication::getUserId($user['email'])))
|
|
||||||
{
|
|
||||||
$bool = $db->insert('insert', 'users', [
|
|
||||||
'first_name' => 'string-30',
|
|
||||||
'surname' => 'string-60',
|
|
||||||
'slug' => 'string-90',
|
|
||||||
'emailaddress' => 'string-255',
|
|
||||||
'password_hash' => 'string-255',
|
|
||||||
'creation_time' => 'int',
|
|
||||||
'last_action_time' => 'int',
|
|
||||||
'ip_address' => 'string-15',
|
|
||||||
'is_admin' => 'int',
|
|
||||||
], [
|
|
||||||
'first_name' => substr($user['full_name'], 0, strpos($user['full_name'], ' ')),
|
|
||||||
'surname' => substr($user['full_name'], strpos($user['full_name'], ' ') + 1),
|
|
||||||
'slug' => $user['name'],
|
|
||||||
'emailaddress' => $user['email'],
|
|
||||||
'password_hash' => $user['password'],
|
|
||||||
'creation_time' => 0,
|
|
||||||
'last_action_time' => $user['last_login'],
|
|
||||||
'ip_address' => '0.0.0.0',
|
|
||||||
'is_admin' => $user['admin'],
|
|
||||||
], ['id_user']);
|
|
||||||
|
|
||||||
if ($bool)
|
|
||||||
$id_user = $db->insert_id();
|
|
||||||
else
|
|
||||||
die("User creation failed!");
|
|
||||||
}
|
|
||||||
|
|
||||||
$old_user_id_to_new_user_id[$user['id']] = $id_user;
|
|
||||||
}
|
|
||||||
|
|
||||||
$pdb->free_result($rs_users);
|
|
||||||
|
|
||||||
/*******************************
|
|
||||||
* STEP 1: ALBUMS
|
|
||||||
*******************************/
|
|
||||||
|
|
||||||
$num_albums = $pdb->queryValue('
|
|
||||||
SELECT COUNT(*)
|
|
||||||
FROM items
|
|
||||||
WHERE type = {string:album}
|
|
||||||
ORDER BY id ASC',
|
|
||||||
['album' => 'album']);
|
|
||||||
|
|
||||||
echo $num_albums, ' albums to import.', "\n";
|
|
||||||
|
|
||||||
$albums = $pdb->query('
|
|
||||||
SELECT id, album_cover_item_id, parent_id, title, description, relative_path_cache, relative_url_cache
|
|
||||||
FROM items
|
|
||||||
WHERE type = {string:album}
|
|
||||||
ORDER BY id ASC',
|
|
||||||
['album' => 'album']);
|
|
||||||
|
|
||||||
$tags = [];
|
|
||||||
$old_album_id_to_new_tag_id = [];
|
|
||||||
$dirnames_by_old_album_id = [];
|
|
||||||
$old_thumb_id_by_tag_id = [];
|
|
||||||
|
|
||||||
while ($album = $pdb->fetch_assoc($albums))
|
|
||||||
{
|
|
||||||
$tag = Tag::createNew([
|
|
||||||
'tag' => $album['title'],
|
|
||||||
'slug' => $album['relative_url_cache'],
|
|
||||||
'kind' => 'Album',
|
|
||||||
'description' => $album['description'],
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (!empty($album['parent_id']))
|
|
||||||
$parent_to_set[$tag->id_tag] = $album['parent_id'];
|
|
||||||
|
|
||||||
$tags[$tag->id_tag] = $tag;
|
|
||||||
$old_album_id_to_new_tag_id[$album['id']] = $tag->id_tag;
|
|
||||||
$dirnames_by_old_album_id[$album['id']] = str_replace('#', '', urldecode($album['relative_path_cache']));
|
|
||||||
$old_thumb_id_by_tag_id[$tag->id_tag] = $album['album_cover_item_id'];
|
|
||||||
}
|
|
||||||
|
|
||||||
$pdb->free_result($albums);
|
|
||||||
|
|
||||||
foreach ($parent_to_set as $id_tag => $old_album_id)
|
|
||||||
{
|
|
||||||
$id_parent = $old_album_id_to_new_tag_id[$old_album_id];
|
|
||||||
$db->query('
|
|
||||||
UPDATE tags
|
|
||||||
SET id_parent = ' . $id_parent . '
|
|
||||||
WHERE id_tag = ' . $id_tag);
|
|
||||||
}
|
|
||||||
|
|
||||||
unset($parent_to_set);
|
|
||||||
|
|
||||||
/*******************************
|
|
||||||
* STEP 2: PHOTOS
|
|
||||||
*******************************/
|
|
||||||
|
|
||||||
$num_photos = $pdb->queryValue('
|
|
||||||
SELECT COUNT(*)
|
|
||||||
FROM items
|
|
||||||
WHERE type = {string:photo}',
|
|
||||||
['photo' => "photo"]);
|
|
||||||
|
|
||||||
echo $num_photos, " photos to import.\n";
|
|
||||||
|
|
||||||
$old_photo_id_to_asset_id = [];
|
|
||||||
for ($i = 0; $i < $num_photos; $i += 50)
|
|
||||||
{
|
|
||||||
echo 'Offset ' . $i . "...\n";
|
|
||||||
|
|
||||||
$photos = $pdb->query('
|
|
||||||
SELECT id, owner_id, parent_id, captured, created, name, title, description, relative_url_cache, width, height, mime_type, weight
|
|
||||||
FROM items
|
|
||||||
WHERE type = {string:photo}
|
|
||||||
ORDER BY id ASC
|
|
||||||
LIMIT ' . $i . ', 50',
|
|
||||||
['photo' => 'photo']);
|
|
||||||
|
|
||||||
while ($photo = $pdb->fetch_assoc($photos))
|
|
||||||
{
|
|
||||||
$res = $db->query('
|
|
||||||
INSERT INTO assets
|
|
||||||
(id_user_uploaded, subdir, filename, title, slug, mimetype, image_width, image_height, date_captured, priority)
|
|
||||||
VALUES
|
|
||||||
({int:id_user_uploaded}, {string:subdir}, {string:filename}, {string:title}, {string:slug}, {string:mimetype},
|
|
||||||
{int:image_width}, {int:image_height},
|
|
||||||
IF({int:date_captured} > 0, FROM_UNIXTIME({int:date_captured}), NULL),
|
|
||||||
{int:priority})',
|
|
||||||
[
|
|
||||||
'id_user_uploaded' => $old_user_id_to_new_user_id[$photo['owner_id']],
|
|
||||||
'subdir' => $dirnames_by_old_album_id[$photo['parent_id']],
|
|
||||||
'filename' => str_replace('#', '', $photo['name']),
|
|
||||||
'title' => $photo['title'],
|
|
||||||
'slug' => str_replace('#', '', urldecode($photo['relative_url_cache'])),
|
|
||||||
'mimetype' => $photo['mime_type'],
|
|
||||||
'image_width' => !empty($photo['width']) ? $photo['width'] : 'NULL',
|
|
||||||
'image_height' => !empty($photo['height']) ? $photo['height'] : 'NULL',
|
|
||||||
'date_captured' => !empty($photo['captured']) ? $photo['captured'] : $photo['created'],
|
|
||||||
'priority' => !empty($photo['weight']) ? (int) $photo['weight'] : 0,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$id_asset = $db->insert_id();
|
|
||||||
$old_photo_id_to_asset_id[$photo['id']] = $id_asset;
|
|
||||||
|
|
||||||
// Link to album.
|
|
||||||
$db->query('
|
|
||||||
INSERT INTO assets_tags
|
|
||||||
(id_asset, id_tag)
|
|
||||||
VALUES
|
|
||||||
({int:id_asset}, {int:id_tag})',
|
|
||||||
[
|
|
||||||
'id_asset' => $id_asset,
|
|
||||||
'id_tag' => $old_album_id_to_new_tag_id[$photo['parent_id']],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*******************************
|
|
||||||
* STEP 3: TAGS
|
|
||||||
*******************************/
|
|
||||||
|
|
||||||
$num_tags = $pdb->queryValue('
|
|
||||||
SELECT COUNT(*)
|
|
||||||
FROM tags');
|
|
||||||
|
|
||||||
echo $num_tags, " tags to import.\n";
|
|
||||||
|
|
||||||
$rs_tags = $pdb->query('
|
|
||||||
SELECT id, name, count
|
|
||||||
FROM tags');
|
|
||||||
|
|
||||||
$old_tag_id_to_new_tag_id = [];
|
|
||||||
while ($person = $pdb->fetch_assoc($rs_tags))
|
|
||||||
{
|
|
||||||
$tag = Tag::createNew([
|
|
||||||
'tag' => $person['name'],
|
|
||||||
'slug' => $person['name'],
|
|
||||||
'kind' => 'Person',
|
|
||||||
'description' => '',
|
|
||||||
'count' => $person['count'],
|
|
||||||
]);
|
|
||||||
|
|
||||||
$tags[$tag->id_tag] = $tag;
|
|
||||||
$old_tag_id_to_new_tag_id[$person['id']] = $tag->id_tag;
|
|
||||||
}
|
|
||||||
|
|
||||||
$pdb->free_result($rs_tags);
|
|
||||||
|
|
||||||
/*******************************
|
|
||||||
* STEP 4: TAGGED PHOTOS
|
|
||||||
*******************************/
|
|
||||||
|
|
||||||
$num_tagged = $pdb->queryValue('
|
|
||||||
SELECT COUNT(*)
|
|
||||||
FROM items_tags
|
|
||||||
WHERE item_id IN(
|
|
||||||
SELECT id
|
|
||||||
FROM items
|
|
||||||
WHERE type = {string:photo}
|
|
||||||
)',
|
|
||||||
['photo' => 'photo']);
|
|
||||||
|
|
||||||
echo $num_tagged, " photo tags to import.\n";
|
|
||||||
|
|
||||||
$rs_tags = $pdb->query('
|
|
||||||
SELECT item_id, tag_id
|
|
||||||
FROM items_tags
|
|
||||||
WHERE item_id IN(
|
|
||||||
SELECT id
|
|
||||||
FROM items
|
|
||||||
WHERE type = {string:photo}
|
|
||||||
)',
|
|
||||||
['photo' => 'photo']);
|
|
||||||
|
|
||||||
while ($tag = $pdb->fetch_assoc($rs_tags))
|
|
||||||
{
|
|
||||||
if (!isset($old_tag_id_to_new_tag_id[$tag['tag_id']], $old_photo_id_to_asset_id[$tag['item_id']]))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
$id_asset = $old_photo_id_to_asset_id[$tag['item_id']];
|
|
||||||
$id_tag = $old_tag_id_to_new_tag_id[$tag['tag_id']];
|
|
||||||
|
|
||||||
// Link up.
|
|
||||||
$db->query('
|
|
||||||
INSERT IGNORE INTO assets_tags
|
|
||||||
(id_asset, id_tag)
|
|
||||||
VALUES
|
|
||||||
({int:id_asset}, {int:id_tag})',
|
|
||||||
[
|
|
||||||
'id_asset' => $id_asset,
|
|
||||||
'id_tag' => $id_tag,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$pdb->free_result($rs_tags);
|
|
||||||
|
|
||||||
/*******************************
|
|
||||||
* STEP 5: THUMBNAIL IDS
|
|
||||||
*******************************/
|
|
||||||
|
|
||||||
foreach ($old_thumb_id_by_tag_id as $id_tag => $old_thumb_id)
|
|
||||||
{
|
|
||||||
if (!isset($old_photo_id_to_asset_id[$old_thumb_id]))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
$id_asset = $old_photo_id_to_asset_id[$old_thumb_id];
|
|
||||||
$db->query('
|
|
||||||
UPDATE tags
|
|
||||||
SET id_asset_thumb = ' . $id_asset . '
|
|
||||||
WHERE id_tag = ' . $id_tag);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*******************************
|
|
||||||
* STEP 6: THUMBNAILS FOR PEOPLE
|
|
||||||
*******************************/
|
|
||||||
|
|
||||||
$db->query('
|
|
||||||
UPDATE tags AS t
|
|
||||||
SET id_asset_thumb = (
|
|
||||||
SELECT id_asset
|
|
||||||
FROM assets_tags AS a
|
|
||||||
WHERE a.id_tag = t.id_tag
|
|
||||||
ORDER BY RAND()
|
|
||||||
LIMIT 1
|
|
||||||
)
|
|
||||||
WHERE kind = {string:person}',
|
|
||||||
['person' => 'Person']);
|
|
||||||
|
|
||||||
/*******************************
|
|
||||||
* STEP 7: CLEANING UP
|
|
||||||
*******************************/
|
|
||||||
|
|
||||||
Tag::recount();
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# ALBUM UPDATE
|
|
||||||
|
|
||||||
# Hashes uit filenames.
|
|
||||||
find . -name '*#*' -exec rename -v "s/#//" {} \;
|
|
||||||
|
|
||||||
# Orientatie-tags goedzetten.
|
|
||||||
find public/assets/borrel/april-2015/ -type f -exec exiftool -n -Orientation=1 "{}" \;
|
|
||||||
find public/assets/Eetpartijtjes/ruwinterbbq/ -type f -exec exiftool -n -Orientation=1 "{}" \;
|
|
||||||
find public/assets/Eetpartijtjes/Tapasavond-oktober-2011/ -type f -exec exiftool -n -Orientation=1 "{}" \;
|
|
||||||
find public/assets/Eetpartijtjes/Verjaardag-IV-bij-Wally/ -type f -exec exiftool -n -Orientation=1 "{}" \;
|
|
||||||
find public/assets/Uitstapjes/Final-Symphony-Wuppertal-2013-05-11/ -type f -exec exiftool -n -Orientation=1 "{}" \;
|
|
||||||
find public/assets/Universiteit/Oude-sneeuwfoto\'s/ -type f -exec exiftool -n -Orientation=1 "{}" \;
|
|
||||||
find public/assets/Weekenden/Susteren-2012 -type f -exec exiftool -n -Orientation=1 "{}" \;
|
|
||||||
find public/assets/Weekenden/Susteren-2013 -type f -exec exiftool -n -Orientation=1 "{}" \;
|
|
||||||
find public/assets/Weekenden/Wijhe-2016/ -type f -exec exiftool -n -Orientation=1 "{}" \;
|
|
||||||
|
|
||||||
# Remove backup files.
|
|
||||||
find public/assets/ -type f -name '*_original' -delete
|
|
||||||
61
models/AdminMenu.php
Normal file
61
models/AdminMenu.php
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
/*****************************************************************************
|
||||||
|
* AdminMenu.php
|
||||||
|
* Contains the admin navigation logic.
|
||||||
|
*
|
||||||
|
* Kabuki CMS (C) 2013-2023, Aaron van Geffen
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
class AdminMenu extends Menu
|
||||||
|
{
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$user = Registry::has('user') ? Registry::get('user') : new Guest();
|
||||||
|
if (!$user->isAdmin())
|
||||||
|
return;
|
||||||
|
|
||||||
|
$this->items[0] = [
|
||||||
|
'label' => 'Admin',
|
||||||
|
'icon' => 'gear',
|
||||||
|
'badge' => ErrorLog::getCount(),
|
||||||
|
'subs' => [
|
||||||
|
[
|
||||||
|
'uri' => '/managealbums/',
|
||||||
|
'label' => 'Albums',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'uri' => '/manageassets/',
|
||||||
|
'label' => 'Assets',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'uri' => '/managetags/',
|
||||||
|
'label' => 'Tags',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'uri' => '/manageusers/',
|
||||||
|
'label' => 'Users',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'uri' => '/manageerrors/',
|
||||||
|
'label' => 'Errors',
|
||||||
|
'badge' => ErrorLog::getCount(),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($this->items[0]['badge'] == 0)
|
||||||
|
unset($this->items[0]['badge']);
|
||||||
|
|
||||||
|
foreach ($this->items as $i => $item)
|
||||||
|
{
|
||||||
|
if (isset($item['uri']))
|
||||||
|
$this->items[$i]['url'] = BASEURL . $item['uri'];
|
||||||
|
|
||||||
|
if (!isset($item['subs']))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
foreach ($item['subs'] as $j => $subitem)
|
||||||
|
$this->items[$i]['subs'][$j]['url'] = BASEURL . $subitem['uri'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
189
models/Asset.php
189
models/Asset.php
@@ -13,18 +13,24 @@ class Asset
|
|||||||
protected $subdir;
|
protected $subdir;
|
||||||
protected $filename;
|
protected $filename;
|
||||||
protected $title;
|
protected $title;
|
||||||
|
protected $slug;
|
||||||
protected $mimetype;
|
protected $mimetype;
|
||||||
protected $image_width;
|
protected $image_width;
|
||||||
protected $image_height;
|
protected $image_height;
|
||||||
protected $date_captured;
|
protected $date_captured;
|
||||||
protected $priority;
|
protected $priority;
|
||||||
|
|
||||||
protected $meta;
|
protected $meta;
|
||||||
protected $tags;
|
protected $tags;
|
||||||
|
protected $thumbnails;
|
||||||
|
|
||||||
protected function __construct(array $data)
|
protected function __construct(array $data)
|
||||||
{
|
{
|
||||||
foreach ($data as $attribute => $value)
|
foreach ($data as $attribute => $value)
|
||||||
$this->$attribute = $value;
|
{
|
||||||
|
if (property_exists($this, $attribute))
|
||||||
|
$this->$attribute = $value;
|
||||||
|
}
|
||||||
|
|
||||||
if (!empty($data['date_captured']) && $data['date_captured'] !== 'NULL')
|
if (!empty($data['date_captured']) && $data['date_captured'] !== 'NULL')
|
||||||
$this->date_captured = new DateTime($data['date_captured']);
|
$this->date_captured = new DateTime($data['date_captured']);
|
||||||
@@ -58,8 +64,10 @@ class Asset
|
|||||||
|
|
||||||
public static function byRow(array $row, $return_format = 'object')
|
public static function byRow(array $row, $return_format = 'object')
|
||||||
{
|
{
|
||||||
|
$db = Registry::get('db');
|
||||||
|
|
||||||
// Supplement with metadata.
|
// Supplement with metadata.
|
||||||
$row['meta'] = Registry::get('db')->queryPair('
|
$row['meta'] = $db->queryPair('
|
||||||
SELECT variable, value
|
SELECT variable, value
|
||||||
FROM assets_meta
|
FROM assets_meta
|
||||||
WHERE id_asset = {int:id_asset}',
|
WHERE id_asset = {int:id_asset}',
|
||||||
@@ -67,6 +75,24 @@ class Asset
|
|||||||
'id_asset' => $row['id_asset'],
|
'id_asset' => $row['id_asset'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// And thumbnails.
|
||||||
|
$row['thumbnails'] = $db->queryPair('
|
||||||
|
SELECT
|
||||||
|
CONCAT(
|
||||||
|
width,
|
||||||
|
{string:x},
|
||||||
|
height,
|
||||||
|
IF(mode != {string:empty}, CONCAT({string:_}, mode), {string:empty})
|
||||||
|
) AS selector, filename
|
||||||
|
FROM assets_thumbs
|
||||||
|
WHERE id_asset = {int:id_asset}',
|
||||||
|
[
|
||||||
|
'id_asset' => $row['id_asset'],
|
||||||
|
'empty' => '',
|
||||||
|
'x' => 'x',
|
||||||
|
'_' => '_',
|
||||||
|
]);
|
||||||
|
|
||||||
return $return_format == 'object' ? new Asset($row) : $row;
|
return $return_format == 'object' ? new Asset($row) : $row;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,6 +117,7 @@ class Asset
|
|||||||
{
|
{
|
||||||
$assets[$asset['id_asset']] = $asset;
|
$assets[$asset['id_asset']] = $asset;
|
||||||
$assets[$asset['id_asset']]['meta'] = [];
|
$assets[$asset['id_asset']]['meta'] = [];
|
||||||
|
$assets[$asset['id_asset']]['thumbnails'] = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
$metas = $db->queryRows('
|
$metas = $db->queryRows('
|
||||||
@@ -105,6 +132,27 @@ class Asset
|
|||||||
foreach ($metas as $meta)
|
foreach ($metas as $meta)
|
||||||
$assets[$meta[0]]['meta'][$meta[1]] = $meta[2];
|
$assets[$meta[0]]['meta'][$meta[1]] = $meta[2];
|
||||||
|
|
||||||
|
$thumbnails = $db->queryRows('
|
||||||
|
SELECT id_asset,
|
||||||
|
CONCAT(
|
||||||
|
width,
|
||||||
|
{string:x},
|
||||||
|
height,
|
||||||
|
IF(mode != {string:empty}, CONCAT({string:_}, mode), {string:empty})
|
||||||
|
) AS selector, filename
|
||||||
|
FROM assets_thumbs
|
||||||
|
WHERE id_asset IN ({array_int:id_assets})
|
||||||
|
ORDER BY id_asset',
|
||||||
|
[
|
||||||
|
'id_assets' => $id_assets,
|
||||||
|
'empty' => '',
|
||||||
|
'x' => 'x',
|
||||||
|
'_' => '_',
|
||||||
|
]);
|
||||||
|
|
||||||
|
foreach ($thumbnails as $thumb)
|
||||||
|
$assets[$thumb[0]]['thumbnails'][$thumb[1]] = $thumb[2];
|
||||||
|
|
||||||
if ($return_format == 'array')
|
if ($return_format == 'array')
|
||||||
return $assets;
|
return $assets;
|
||||||
else
|
else
|
||||||
@@ -116,13 +164,6 @@ class Asset
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function byPostId($id_post, $return_format = 'object')
|
|
||||||
{
|
|
||||||
$db = Registry::get('db');
|
|
||||||
|
|
||||||
// !!! TODO
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function createNew(array $data, $return_format = 'object')
|
public static function createNew(array $data, $return_format = 'object')
|
||||||
{
|
{
|
||||||
// Extract the data array.
|
// Extract the data array.
|
||||||
@@ -146,9 +187,10 @@ class Asset
|
|||||||
|
|
||||||
$new_filename = $preferred_filename;
|
$new_filename = $preferred_filename;
|
||||||
$destination = ASSETSDIR . '/' . $preferred_subdir . '/' . $preferred_filename;
|
$destination = ASSETSDIR . '/' . $preferred_subdir . '/' . $preferred_filename;
|
||||||
while (file_exists($destination))
|
for ($i = 1; file_exists($destination); $i++)
|
||||||
{
|
{
|
||||||
$filename = pathinfo($preferred_filename, PATHINFO_FILENAME) . '_' . mt_rand(10, 99);
|
$suffix = $i;
|
||||||
|
$filename = pathinfo($preferred_filename, PATHINFO_FILENAME) . ' (' . $suffix . ')';
|
||||||
$extension = pathinfo($preferred_filename, PATHINFO_EXTENSION);
|
$extension = pathinfo($preferred_filename, PATHINFO_EXTENSION);
|
||||||
$new_filename = $filename . '.' . $extension;
|
$new_filename = $filename . '.' . $extension;
|
||||||
$destination = dirname($destination) . '/' . $new_filename;
|
$destination = dirname($destination) . '/' . $new_filename;
|
||||||
@@ -165,11 +207,14 @@ class Asset
|
|||||||
$mimetype = finfo_file($finfo, $destination);
|
$mimetype = finfo_file($finfo, $destination);
|
||||||
finfo_close($finfo);
|
finfo_close($finfo);
|
||||||
|
|
||||||
|
// We're going to need the base name a few times...
|
||||||
|
$basename = pathinfo($new_filename, PATHINFO_FILENAME);
|
||||||
|
|
||||||
// Do we have a title yet? Otherwise, use the filename.
|
// Do we have a title yet? Otherwise, use the filename.
|
||||||
$title = isset($data['title']) ? $data['title'] : pathinfo($preferred_filename, PATHINFO_FILENAME);
|
$title = $data['title'] ?? $basename;
|
||||||
|
|
||||||
// Same with the slug.
|
// Same with the slug.
|
||||||
$slug = isset($data['slug']) ? $data['slug'] : $preferred_subdir . '/' . pathinfo($preferred_filename, PATHINFO_FILENAME);
|
$slug = $data['slug'] ?? sprintf('%s/%s', $preferred_subdir, $basename);
|
||||||
|
|
||||||
// Detected an image?
|
// Detected an image?
|
||||||
if (substr($mimetype, 0, 5) == 'image')
|
if (substr($mimetype, 0, 5) == 'image')
|
||||||
@@ -226,7 +271,7 @@ class Asset
|
|||||||
}
|
}
|
||||||
|
|
||||||
$data['id_asset'] = $db->insert_id();
|
$data['id_asset'] = $db->insert_id();
|
||||||
return $return_format == 'object' ? new self($data) : $data;
|
return $return_format === 'object' ? new self($data) : $data;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getId()
|
public function getId()
|
||||||
@@ -277,7 +322,12 @@ class Asset
|
|||||||
return ASSETSDIR . '/' . $this->subdir . '/' . $this->filename;
|
return ASSETSDIR . '/' . $this->subdir . '/' . $this->filename;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getPath()
|
public function getSlug()
|
||||||
|
{
|
||||||
|
return $this->slug;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSubdir()
|
||||||
{
|
{
|
||||||
return $this->subdir;
|
return $this->subdir;
|
||||||
}
|
}
|
||||||
@@ -322,7 +372,7 @@ class Asset
|
|||||||
|
|
||||||
public function isImage()
|
public function isImage()
|
||||||
{
|
{
|
||||||
return substr($this->mimetype, 0, 5) === 'image';
|
return isset($this->mimetype) && substr($this->mimetype, 0, 5) === 'image';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getImage()
|
public function getImage()
|
||||||
@@ -352,7 +402,7 @@ class Asset
|
|||||||
finfo_close($finfo);
|
finfo_close($finfo);
|
||||||
|
|
||||||
// Detected an image?
|
// Detected an image?
|
||||||
if (substr($this->mimetype, 0, 5) == 'image')
|
if (substr($this->mimetype, 0, 5) === 'image')
|
||||||
{
|
{
|
||||||
$image = new Imagick($destination);
|
$image = new Imagick($destination);
|
||||||
$d = $image->getImageGeometry();
|
$d = $image->getImageGeometry();
|
||||||
@@ -438,12 +488,87 @@ class Asset
|
|||||||
|
|
||||||
public function delete()
|
public function delete()
|
||||||
{
|
{
|
||||||
return Registry::get('db')->query('
|
$db = Registry::get('db');
|
||||||
|
|
||||||
|
// First: delete associated metadata
|
||||||
|
$db->query('
|
||||||
|
DELETE FROM assets_meta
|
||||||
|
WHERE id_asset = {int:id_asset}',
|
||||||
|
[
|
||||||
|
'id_asset' => $this->id_asset,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Second: figure out what tags to recount cardinality for
|
||||||
|
$recount_tags = $db->queryValues('
|
||||||
|
SELECT id_tag
|
||||||
|
FROM assets_tags
|
||||||
|
WHERE id_asset = {int:id_asset}',
|
||||||
|
[
|
||||||
|
'id_asset' => $this->id_asset,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$db->query('
|
||||||
|
DELETE FROM assets_tags
|
||||||
|
WHERE id_asset = {int:id_asset}',
|
||||||
|
[
|
||||||
|
'id_asset' => $this->id_asset,
|
||||||
|
]);
|
||||||
|
|
||||||
|
Tag::recount($recount_tags);
|
||||||
|
|
||||||
|
// Third: figure out what associated thumbs to delete
|
||||||
|
$thumbs_to_delete = $db->queryValues('
|
||||||
|
SELECT filename
|
||||||
|
FROM assets_thumbs
|
||||||
|
WHERE id_asset = {int:id_asset}',
|
||||||
|
[
|
||||||
|
'id_asset' => $this->id_asset,
|
||||||
|
]);
|
||||||
|
|
||||||
|
foreach ($thumbs_to_delete as $filename)
|
||||||
|
{
|
||||||
|
$thumb_path = THUMBSDIR . '/' . $this->subdir . '/' . $filename;
|
||||||
|
if (is_file($thumb_path))
|
||||||
|
unlink($thumb_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
$db->query('
|
||||||
|
DELETE FROM assets_thumbs
|
||||||
|
WHERE id_asset = {int:id_asset}',
|
||||||
|
[
|
||||||
|
'id_asset' => $this->id_asset,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Reset asset ID for tags that use this asset for their thumbnail
|
||||||
|
$rows = $db->query('
|
||||||
|
SELECT id_tag
|
||||||
|
FROM tags
|
||||||
|
WHERE id_asset_thumb = {int:id_asset}',
|
||||||
|
[
|
||||||
|
'id_asset' => $this->id_asset,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!empty($rows))
|
||||||
|
{
|
||||||
|
foreach ($rows as $row)
|
||||||
|
{
|
||||||
|
$tag = Tag::fromId($row['id_tag']);
|
||||||
|
$tag->resetIdAsset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally, delete the actual asset
|
||||||
|
if (!unlink(ASSETSDIR . '/' . $this->subdir . '/' . $this->filename))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
$return = $db->query('
|
||||||
DELETE FROM assets
|
DELETE FROM assets
|
||||||
WHERE id_asset = {int:id_asset}',
|
WHERE id_asset = {int:id_asset}',
|
||||||
[
|
[
|
||||||
'id_asset' => $this->id_asset,
|
'id_asset' => $this->id_asset,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
return $return;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function linkTags(array $id_tags)
|
public function linkTags(array $id_tags)
|
||||||
@@ -481,16 +606,17 @@ class Asset
|
|||||||
|
|
||||||
public static function getCount()
|
public static function getCount()
|
||||||
{
|
{
|
||||||
return $db->queryValue('
|
return Registry::get('db')->queryValue('
|
||||||
SELECT COUNT(*)
|
SELECT COUNT(*)
|
||||||
FROM assets');
|
FROM assets');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setKeyData($title, DateTime $date_captured = null, $priority)
|
public function setKeyData($title, $slug, DateTime $date_captured = null, $priority)
|
||||||
{
|
{
|
||||||
$params = [
|
$params = [
|
||||||
'id_asset' => $this->id_asset,
|
'id_asset' => $this->id_asset,
|
||||||
'title' => $title,
|
'title' => $title,
|
||||||
|
'slug' => $slug,
|
||||||
'priority' => $priority,
|
'priority' => $priority,
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -499,7 +625,8 @@ class Asset
|
|||||||
|
|
||||||
return Registry::get('db')->query('
|
return Registry::get('db')->query('
|
||||||
UPDATE assets
|
UPDATE assets
|
||||||
SET title = {string:title},' . (isset($date_captured) ? '
|
SET title = {string:title},
|
||||||
|
slug = {string:slug},' . (isset($date_captured) ? '
|
||||||
date_captured = {datetime:date_captured},' : '') . '
|
date_captured = {datetime:date_captured},' : '') . '
|
||||||
priority = {int:priority}
|
priority = {int:priority}
|
||||||
WHERE id_asset = {int:id_asset}',
|
WHERE id_asset = {int:id_asset}',
|
||||||
@@ -514,14 +641,12 @@ class Asset
|
|||||||
FROM assets_tags AS t
|
FROM assets_tags AS t
|
||||||
INNER JOIN assets AS a ON a.id_asset = t.id_asset
|
INNER JOIN assets AS a ON a.id_asset = t.id_asset
|
||||||
WHERE t.id_tag = {int:id_tag} AND
|
WHERE t.id_tag = {int:id_tag} AND
|
||||||
a.date_captured <= {datetime:date_captured} AND
|
(a.date_captured, a.id_asset) < ({datetime:date_captured}, {int:id_asset})
|
||||||
a.id_asset != {int:id_asset}
|
ORDER BY a.date_captured DESC, a.id_asset DESC'
|
||||||
ORDER BY a.date_captured DESC'
|
|
||||||
: '
|
: '
|
||||||
FROM assets AS a
|
FROM assets AS a
|
||||||
WHERE date_captured >= {datetime:date_captured} AND
|
WHERE (a.date_captured, a.id_asset) > ({datetime:date_captured}, {int:id_asset})
|
||||||
a.id_asset != {int:id_asset}
|
ORDER BY date_captured ASC, a.id_asset ASC')
|
||||||
ORDER BY date_captured ASC')
|
|
||||||
. '
|
. '
|
||||||
LIMIT 1',
|
LIMIT 1',
|
||||||
[
|
[
|
||||||
@@ -547,14 +672,12 @@ class Asset
|
|||||||
FROM assets_tags AS t
|
FROM assets_tags AS t
|
||||||
INNER JOIN assets AS a ON a.id_asset = t.id_asset
|
INNER JOIN assets AS a ON a.id_asset = t.id_asset
|
||||||
WHERE t.id_tag = {int:id_tag} AND
|
WHERE t.id_tag = {int:id_tag} AND
|
||||||
a.date_captured >= {datetime:date_captured} AND
|
(a.date_captured, a.id_asset) > ({datetime:date_captured}, {int:id_asset})
|
||||||
a.id_asset != {int:id_asset}
|
ORDER BY a.date_captured ASC, a.id_asset ASC'
|
||||||
ORDER BY a.date_captured ASC'
|
|
||||||
: '
|
: '
|
||||||
FROM assets AS a
|
FROM assets AS a
|
||||||
WHERE date_captured <= {datetime:date_captured} AND
|
WHERE (a.date_captured, a.id_asset) < ({datetime:date_captured}, {int:id_asset})
|
||||||
a.id_asset != {int:id_asset}
|
ORDER BY date_captured DESC, a.id_asset DESC')
|
||||||
ORDER BY date_captured DESC')
|
|
||||||
. '
|
. '
|
||||||
LIMIT 1',
|
LIMIT 1',
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -11,12 +11,15 @@ class AssetIterator extends Asset
|
|||||||
private $return_format;
|
private $return_format;
|
||||||
private $res_assets;
|
private $res_assets;
|
||||||
private $res_meta;
|
private $res_meta;
|
||||||
|
private $res_thumbs;
|
||||||
|
private Database $db;
|
||||||
|
|
||||||
protected function __construct($res_assets, $res_meta, $return_format)
|
protected function __construct($res_assets, $res_meta, $res_thumbs, $return_format)
|
||||||
{
|
{
|
||||||
$this->db = Registry::get('db');
|
$this->db = Registry::get('db');
|
||||||
$this->res_assets = $res_assets;
|
$this->res_assets = $res_assets;
|
||||||
$this->res_meta = $res_meta;
|
$this->res_meta = $res_meta;
|
||||||
|
$this->res_thumbs = $res_thumbs;
|
||||||
$this->return_format = $return_format;
|
$this->return_format = $return_format;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,7 +44,20 @@ class AssetIterator extends Asset
|
|||||||
// Reset internal pointer for next asset.
|
// Reset internal pointer for next asset.
|
||||||
$this->db->data_seek($this->res_meta, 0);
|
$this->db->data_seek($this->res_meta, 0);
|
||||||
|
|
||||||
if ($this->return_format == 'object')
|
// Looks up thumbnails.
|
||||||
|
$row['thumbnails'] = [];
|
||||||
|
while ($thumbs = $this->db->fetch_assoc($this->res_thumbs))
|
||||||
|
{
|
||||||
|
if ($thumbs['id_asset'] != $row['id_asset'])
|
||||||
|
continue;
|
||||||
|
|
||||||
|
$row['thumbnails'][$thumbs['selector']] = $thumbs['filename'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset internal pointer for next asset.
|
||||||
|
$this->db->data_seek($this->res_thumbs, 0);
|
||||||
|
|
||||||
|
if ($this->return_format === 'object')
|
||||||
return new Asset($row);
|
return new Asset($row);
|
||||||
else
|
else
|
||||||
return $row;
|
return $row;
|
||||||
@@ -51,6 +67,7 @@ class AssetIterator extends Asset
|
|||||||
{
|
{
|
||||||
$this->db->data_seek($this->res_assets, 0);
|
$this->db->data_seek($this->res_assets, 0);
|
||||||
$this->db->data_seek($this->res_meta, 0);
|
$this->db->data_seek($this->res_meta, 0);
|
||||||
|
$this->db->data_seek($this->res_thumbs, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function clean()
|
public function clean()
|
||||||
@@ -135,7 +152,29 @@ class AssetIterator extends Asset
|
|||||||
ORDER BY id_asset',
|
ORDER BY id_asset',
|
||||||
$params);
|
$params);
|
||||||
|
|
||||||
$iterator = new self($res_assets, $res_meta, $return_format);
|
// Get a resource object for the asset thumbs.
|
||||||
|
$res_thumbs = $db->query('
|
||||||
|
SELECT id_asset, filename,
|
||||||
|
CONCAT(
|
||||||
|
width,
|
||||||
|
{string:x},
|
||||||
|
height,
|
||||||
|
IF(mode != {string:empty}, CONCAT({string:_}, mode), {string:empty})
|
||||||
|
) AS selector
|
||||||
|
FROM assets_thumbs
|
||||||
|
WHERE id_asset IN(
|
||||||
|
SELECT id_asset
|
||||||
|
FROM assets AS a
|
||||||
|
WHERE ' . $where . '
|
||||||
|
)
|
||||||
|
ORDER BY id_asset',
|
||||||
|
$params + [
|
||||||
|
'empty' => '',
|
||||||
|
'x' => 'x',
|
||||||
|
'_' => '_',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$iterator = new self($res_assets, $res_meta, $res_thumbs, $return_format);
|
||||||
|
|
||||||
// Returning total count, too?
|
// Returning total count, too?
|
||||||
if ($return_count)
|
if ($return_count)
|
||||||
|
|||||||
@@ -1,149 +0,0 @@
|
|||||||
<?php
|
|
||||||
/*****************************************************************************
|
|
||||||
* BestColor.php
|
|
||||||
* Contains key class BestColor.
|
|
||||||
*
|
|
||||||
* !!! Licensing?
|
|
||||||
*****************************************************************************/
|
|
||||||
|
|
||||||
class BestColor
|
|
||||||
{
|
|
||||||
private $best;
|
|
||||||
|
|
||||||
public function __construct(Image $asset)
|
|
||||||
{
|
|
||||||
// Set fallback color.
|
|
||||||
$this->best = ['r' => 204, 'g' => 204, 'b' => 204]; // #cccccc
|
|
||||||
|
|
||||||
// We will be needing to read this...
|
|
||||||
if (!file_exists($asset->getPath()))
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Try the arcane stuff again.
|
|
||||||
try
|
|
||||||
{
|
|
||||||
$image = new Imagick($asset->getPath());
|
|
||||||
$width = $image->getImageWidth();
|
|
||||||
$height = $image->getImageHeight();
|
|
||||||
|
|
||||||
// Sample six points in the image: four based on the rule of thirds, as well as the horizontal and vertical centre.
|
|
||||||
$topy = round($height / 3);
|
|
||||||
$bottomy = round(($height / 3) * 2);
|
|
||||||
$leftx = round($width / 3);
|
|
||||||
$rightx = round(($width / 3) * 2);
|
|
||||||
$centery = round($height / 2);
|
|
||||||
$centerx = round($width / 2);
|
|
||||||
|
|
||||||
// Grab their colours.
|
|
||||||
$rgb = [
|
|
||||||
$image->getImagePixelColor($leftx, $topy)->getColor(),
|
|
||||||
$image->getImagePixelColor($rightx, $topy)->getColor(),
|
|
||||||
$image->getImagePixelColor($leftx, $bottomy)->getColor(),
|
|
||||||
$image->getImagePixelColor($rightx, $bottomy)->getColor(),
|
|
||||||
$image->getImagePixelColor($centerx, $centery)->getColor(),
|
|
||||||
];
|
|
||||||
|
|
||||||
// We won't be needing this anymore, so save us some memory.
|
|
||||||
$image->clear();
|
|
||||||
$image->destroy();
|
|
||||||
}
|
|
||||||
// In case something does go wrong...
|
|
||||||
catch (ImagickException $e)
|
|
||||||
{
|
|
||||||
// Fall back to default color.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process rgb values into hsv values
|
|
||||||
foreach ($rgb as $i => $color)
|
|
||||||
{
|
|
||||||
$colors[$i] = $color;
|
|
||||||
list($colors[$i]['h'], $colors[$i]['s'], $colors[$i]['v']) = self::rgb2hsv($color['r'], $color['g'], $color['b']);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Figure out which color is the best saturated.
|
|
||||||
$best_saturation = $best_brightness = 0;
|
|
||||||
$the_best_s = $the_best_v = ['v' => 0];
|
|
||||||
foreach ($colors as $color)
|
|
||||||
{
|
|
||||||
if ($color['s'] > $best_saturation)
|
|
||||||
{
|
|
||||||
$best_saturation = $color['s'];
|
|
||||||
$the_best_s = $color;
|
|
||||||
}
|
|
||||||
if ($color['v'] > $best_brightness)
|
|
||||||
{
|
|
||||||
$best_brightness = $color['v'];
|
|
||||||
$the_best_v = $color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Is brightest the same as most saturated?
|
|
||||||
$this->best = ($the_best_s['v'] >= ($the_best_v['v'] - ($the_best_v['v'] / 2))) ? $the_best_s : $the_best_v;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function hex2rgb($hex)
|
|
||||||
{
|
|
||||||
return sscanf($hex, '%2X%2X%2X');
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function rgb2hex($red, $green, $blue)
|
|
||||||
{
|
|
||||||
return sprintf('%02X%02X%02X', $red, $green, $blue);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function rgb2hsv($r, $g, $b)
|
|
||||||
{
|
|
||||||
$max = max($r, $g, $b);
|
|
||||||
$min = min($r, $g, $b);
|
|
||||||
$delta = $max - $min;
|
|
||||||
$v = round(($max / 255) * 100);
|
|
||||||
$s = ($max != 0) ? (round($delta / $max * 100)) : 0;
|
|
||||||
if ($s == 0)
|
|
||||||
{
|
|
||||||
$h = false;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if ($r == $max)
|
|
||||||
$h = ($g - $b) / $delta;
|
|
||||||
elseif ($g == $max)
|
|
||||||
$h = 2 + ($b - $r) / $delta;
|
|
||||||
elseif ($b == $max)
|
|
||||||
$h = 4 + ($r - $g) / $delta;
|
|
||||||
|
|
||||||
$h = round($h * 60);
|
|
||||||
|
|
||||||
if ($h > 360)
|
|
||||||
$h = 360;
|
|
||||||
|
|
||||||
if ($h < 0)
|
|
||||||
$h += 360;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [$h, $s, $v];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a normal (light) background color as hexadecimal value (without hash prefix).
|
|
||||||
* @return color string
|
|
||||||
*/
|
|
||||||
public function hex()
|
|
||||||
{
|
|
||||||
$c = $this->best;
|
|
||||||
return self::rgb2hex($c['r'], $c['g'], $c['b']);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a 50% darker version of the best color as string.
|
|
||||||
* @param factor, defaults to 0.5
|
|
||||||
* @param alpha, defaults to 0.7
|
|
||||||
* @return rgba(r * factor, g * factor, b * factor, alpha)
|
|
||||||
*/
|
|
||||||
public function rgba($factor = 0.5, $alpha = 0.7)
|
|
||||||
{
|
|
||||||
$c = $this->best;
|
|
||||||
return 'rgba(' . round($c['r'] * $factor) . ', ' . round($c['g'] * $factor) . ', ' . round($c['b'] * $factor) . ', ' . $alpha . ')';
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
<?php
|
|
||||||
/*****************************************************************************
|
|
||||||
* Cache.php
|
|
||||||
* Contains key class Cache.
|
|
||||||
*
|
|
||||||
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
|
||||||
*****************************************************************************/
|
|
||||||
|
|
||||||
class Cache
|
|
||||||
{
|
|
||||||
public static $hits = 0;
|
|
||||||
public static $misses = 0;
|
|
||||||
public static $puts = 0;
|
|
||||||
public static $removals = 0;
|
|
||||||
|
|
||||||
public static function put($key, $value, $ttl = 3600)
|
|
||||||
{
|
|
||||||
// If the cache is unavailable, don't bother.
|
|
||||||
if (!CACHE_ENABLED || !function_exists('apcu_store'))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Keep track of the amount of cache puts.
|
|
||||||
self::$puts++;
|
|
||||||
|
|
||||||
// Store the data in serialized form.
|
|
||||||
return apcu_store(CACHE_KEY_PREFIX . $key, serialize($value), $ttl);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get some data from the cache.
|
|
||||||
public static function get($key)
|
|
||||||
{
|
|
||||||
// If the cache is unavailable, don't bother.
|
|
||||||
if (!CACHE_ENABLED || !function_exists('apcu_fetch'))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Try to fetch it!
|
|
||||||
$value = apcu_fetch(CACHE_KEY_PREFIX . $key);
|
|
||||||
|
|
||||||
// Were we successful?
|
|
||||||
if (!empty($value))
|
|
||||||
{
|
|
||||||
self::$hits++;
|
|
||||||
return unserialize($value);
|
|
||||||
}
|
|
||||||
// Otherwise, it's a miss.
|
|
||||||
else
|
|
||||||
{
|
|
||||||
self::$misses++;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function remove($key)
|
|
||||||
{
|
|
||||||
if (!CACHE_ENABLED || !function_exists('apcu_delete'))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
self::$removals++;
|
|
||||||
return apcu_delete(CACHE_KEY_PREFIX . $key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -17,6 +17,7 @@ class Database
|
|||||||
private $connection;
|
private $connection;
|
||||||
private $query_count = 0;
|
private $query_count = 0;
|
||||||
private $logged_queries = [];
|
private $logged_queries = [];
|
||||||
|
private array $db_callback;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialises a new database connection.
|
* Initialises a new database connection.
|
||||||
@@ -37,7 +38,7 @@ class Database
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->query('SET NAMES {string:utf8}', array('utf8' => 'utf8'));
|
$this->query('SET NAMES {string:utf8mb4}', ['utf8mb4' => 'utf8mb4']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getQueryCount()
|
public function getQueryCount()
|
||||||
@@ -256,7 +257,7 @@ class Database
|
|||||||
|
|
||||||
case 'identifier':
|
case 'identifier':
|
||||||
// Backticks inside identifiers are supported as of MySQL 4.1. We don't need them here.
|
// Backticks inside identifiers are supported as of MySQL 4.1. We don't need them here.
|
||||||
return '`' . strtr($replacement, array('`' => '', '.' => '')) . '`';
|
return '`' . strtr($replacement, ['`' => '', '.' => '']) . '`';
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'raw':
|
case 'raw':
|
||||||
@@ -278,7 +279,7 @@ class Database
|
|||||||
/**
|
/**
|
||||||
* Escapes and quotes a string using values passed, and executes the query.
|
* Escapes and quotes a string using values passed, and executes the query.
|
||||||
*/
|
*/
|
||||||
public function query($db_string, $db_values = array())
|
public function query($db_string, $db_values = [])
|
||||||
{
|
{
|
||||||
// One more query....
|
// One more query....
|
||||||
$this->query_count ++;
|
$this->query_count ++;
|
||||||
@@ -293,10 +294,10 @@ class Database
|
|||||||
if (!$security_override && !empty($db_values))
|
if (!$security_override && !empty($db_values))
|
||||||
{
|
{
|
||||||
// Set some values for use in the callback function.
|
// Set some values for use in the callback function.
|
||||||
$this->db_callback = array($db_values, $this->connection);
|
$this->db_callback = [$db_values, $this->connection];
|
||||||
|
|
||||||
// Insert the values passed to this function.
|
// Insert the values passed to this function.
|
||||||
$db_string = preg_replace_callback('~{([a-z_]+)(?::([a-zA-Z0-9_-]+))?}~', array(&$this, 'replacement_callback'), $db_string);
|
$db_string = preg_replace_callback('~{([a-z_]+)(?::([a-zA-Z0-9_-]+))?}~', [&$this, 'replacement_callback'], $db_string);
|
||||||
|
|
||||||
// Save some memory.
|
// Save some memory.
|
||||||
$this->db_callback = [];
|
$this->db_callback = [];
|
||||||
@@ -320,20 +321,20 @@ class Database
|
|||||||
* Escapes and quotes a string just like db_query, but does not execute the query.
|
* Escapes and quotes a string just like db_query, but does not execute the query.
|
||||||
* Useful for debugging purposes.
|
* Useful for debugging purposes.
|
||||||
*/
|
*/
|
||||||
public function quote($db_string, $db_values = array())
|
public function quote($db_string, $db_values = [])
|
||||||
{
|
{
|
||||||
// Please, just use new style queries.
|
// Please, just use new style queries.
|
||||||
if (strpos($db_string, '\'') !== false)
|
if (strpos($db_string, '\'') !== false)
|
||||||
trigger_error('Hack attempt!', 'Illegal character (\') used in query.', E_USER_ERROR);
|
trigger_error('Hack attempt!', 'Illegal character (\') used in query.', E_USER_ERROR);
|
||||||
|
|
||||||
// Save some values for use in the callback function.
|
// Save some values for use in the callback function.
|
||||||
$this->db_callback = array($db_values, $this->connection);
|
$this->db_callback = [$db_values, $this->connection];
|
||||||
|
|
||||||
// Insert the values passed to this function.
|
// Insert the values passed to this function.
|
||||||
$db_string = preg_replace_callback('~{([a-z_]+)(?::([a-zA-Z0-9_-]+))?}~', array(&$this, 'replacement_callback'), $db_string);
|
$db_string = preg_replace_callback('~{([a-z_]+)(?::([a-zA-Z0-9_-]+))?}~', [&$this, 'replacement_callback'], $db_string);
|
||||||
|
|
||||||
// Save some memory.
|
// Save some memory.
|
||||||
$this->db_callback = array();
|
$this->db_callback = [];
|
||||||
|
|
||||||
return $db_string;
|
return $db_string;
|
||||||
}
|
}
|
||||||
@@ -341,12 +342,12 @@ class Database
|
|||||||
/**
|
/**
|
||||||
* Executes a query, returning an array of all the rows it returns.
|
* Executes a query, returning an array of all the rows it returns.
|
||||||
*/
|
*/
|
||||||
public function queryRow($db_string, $db_values = array())
|
public function queryRow($db_string, $db_values = [])
|
||||||
{
|
{
|
||||||
$res = $this->query($db_string, $db_values);
|
$res = $this->query($db_string, $db_values);
|
||||||
|
|
||||||
if (!$res || $this->num_rows($res) == 0)
|
if (!$res || $this->num_rows($res) == 0)
|
||||||
return array();
|
return [];
|
||||||
|
|
||||||
$row = $this->fetch_row($res);
|
$row = $this->fetch_row($res);
|
||||||
$this->free_result($res);
|
$this->free_result($res);
|
||||||
@@ -357,14 +358,14 @@ class Database
|
|||||||
/**
|
/**
|
||||||
* Executes a query, returning an array of all the rows it returns.
|
* Executes a query, returning an array of all the rows it returns.
|
||||||
*/
|
*/
|
||||||
public function queryRows($db_string, $db_values = array())
|
public function queryRows($db_string, $db_values = [])
|
||||||
{
|
{
|
||||||
$res = $this->query($db_string, $db_values);
|
$res = $this->query($db_string, $db_values);
|
||||||
|
|
||||||
if (!$res || $this->num_rows($res) == 0)
|
if (!$res || $this->num_rows($res) == 0)
|
||||||
return array();
|
return [];
|
||||||
|
|
||||||
$rows = array();
|
$rows = [];
|
||||||
while ($row = $this->fetch_row($res))
|
while ($row = $this->fetch_row($res))
|
||||||
$rows[] = $row;
|
$rows[] = $row;
|
||||||
|
|
||||||
@@ -376,14 +377,14 @@ class Database
|
|||||||
/**
|
/**
|
||||||
* Executes a query, returning an array of all the rows it returns.
|
* Executes a query, returning an array of all the rows it returns.
|
||||||
*/
|
*/
|
||||||
public function queryPair($db_string, $db_values = array())
|
public function queryPair($db_string, $db_values = [])
|
||||||
{
|
{
|
||||||
$res = $this->query($db_string, $db_values);
|
$res = $this->query($db_string, $db_values);
|
||||||
|
|
||||||
if (!$res || $this->num_rows($res) == 0)
|
if (!$res || $this->num_rows($res) == 0)
|
||||||
return array();
|
return [];
|
||||||
|
|
||||||
$rows = array();
|
$rows = [];
|
||||||
while ($row = $this->fetch_row($res))
|
while ($row = $this->fetch_row($res))
|
||||||
$rows[$row[0]] = $row[1];
|
$rows[$row[0]] = $row[1];
|
||||||
|
|
||||||
@@ -395,14 +396,14 @@ class Database
|
|||||||
/**
|
/**
|
||||||
* Executes a query, returning an array of all the rows it returns.
|
* Executes a query, returning an array of all the rows it returns.
|
||||||
*/
|
*/
|
||||||
public function queryPairs($db_string, $db_values = array())
|
public function queryPairs($db_string, $db_values = [])
|
||||||
{
|
{
|
||||||
$res = $this->query($db_string, $db_values);
|
$res = $this->query($db_string, $db_values);
|
||||||
|
|
||||||
if (!$res || $this->num_rows($res) == 0)
|
if (!$res || $this->num_rows($res) == 0)
|
||||||
return array();
|
return [];
|
||||||
|
|
||||||
$rows = array();
|
$rows = [];
|
||||||
while ($row = $this->fetch_assoc($res))
|
while ($row = $this->fetch_assoc($res))
|
||||||
{
|
{
|
||||||
$key_value = reset($row);
|
$key_value = reset($row);
|
||||||
@@ -417,12 +418,12 @@ class Database
|
|||||||
/**
|
/**
|
||||||
* Executes a query, returning an associative array of all the rows it returns.
|
* Executes a query, returning an associative array of all the rows it returns.
|
||||||
*/
|
*/
|
||||||
public function queryAssoc($db_string, $db_values = array())
|
public function queryAssoc($db_string, $db_values = [])
|
||||||
{
|
{
|
||||||
$res = $this->query($db_string, $db_values);
|
$res = $this->query($db_string, $db_values);
|
||||||
|
|
||||||
if (!$res || $this->num_rows($res) == 0)
|
if (!$res || $this->num_rows($res) == 0)
|
||||||
return array();
|
return [];
|
||||||
|
|
||||||
$row = $this->fetch_assoc($res);
|
$row = $this->fetch_assoc($res);
|
||||||
$this->free_result($res);
|
$this->free_result($res);
|
||||||
@@ -433,14 +434,14 @@ class Database
|
|||||||
/**
|
/**
|
||||||
* Executes a query, returning an associative array of all the rows it returns.
|
* Executes a query, returning an associative array of all the rows it returns.
|
||||||
*/
|
*/
|
||||||
public function queryAssocs($db_string, $db_values = array(), $connection = null)
|
public function queryAssocs($db_string, $db_values = [], $connection = null)
|
||||||
{
|
{
|
||||||
$res = $this->query($db_string, $db_values);
|
$res = $this->query($db_string, $db_values);
|
||||||
|
|
||||||
if (!$res || $this->num_rows($res) == 0)
|
if (!$res || $this->num_rows($res) == 0)
|
||||||
return array();
|
return [];
|
||||||
|
|
||||||
$rows = array();
|
$rows = [];
|
||||||
while ($row = $this->fetch_assoc($res))
|
while ($row = $this->fetch_assoc($res))
|
||||||
$rows[] = $row;
|
$rows[] = $row;
|
||||||
|
|
||||||
@@ -452,7 +453,7 @@ class Database
|
|||||||
/**
|
/**
|
||||||
* Executes a query, returning the first value of the first row.
|
* Executes a query, returning the first value of the first row.
|
||||||
*/
|
*/
|
||||||
public function queryValue($db_string, $db_values = array())
|
public function queryValue($db_string, $db_values = [])
|
||||||
{
|
{
|
||||||
$res = $this->query($db_string, $db_values);
|
$res = $this->query($db_string, $db_values);
|
||||||
|
|
||||||
@@ -469,14 +470,14 @@ class Database
|
|||||||
/**
|
/**
|
||||||
* Executes a query, returning an array of the first value of each row.
|
* Executes a query, returning an array of the first value of each row.
|
||||||
*/
|
*/
|
||||||
public function queryValues($db_string, $db_values = array())
|
public function queryValues($db_string, $db_values = [])
|
||||||
{
|
{
|
||||||
$res = $this->query($db_string, $db_values);
|
$res = $this->query($db_string, $db_values);
|
||||||
|
|
||||||
if (!$res || $this->num_rows($res) == 0)
|
if (!$res || $this->num_rows($res) == 0)
|
||||||
return array();
|
return [];
|
||||||
|
|
||||||
$rows = array();
|
$rows = [];
|
||||||
while ($row = $this->fetch_row($res))
|
while ($row = $this->fetch_row($res))
|
||||||
$rows[] = $row[0];
|
$rows[] = $row[0];
|
||||||
|
|
||||||
@@ -488,7 +489,7 @@ class Database
|
|||||||
/**
|
/**
|
||||||
* This function can be used to insert data into the database in a secure way.
|
* This function can be used to insert data into the database in a secure way.
|
||||||
*/
|
*/
|
||||||
public function insert($method = 'replace', $table, $columns, $data)
|
public function insert($method, $table, $columns, $data)
|
||||||
{
|
{
|
||||||
// With nothing to insert, simply return.
|
// With nothing to insert, simply return.
|
||||||
if (empty($data))
|
if (empty($data))
|
||||||
@@ -496,7 +497,7 @@ class Database
|
|||||||
|
|
||||||
// Inserting data as a single row can be done as a single array.
|
// Inserting data as a single row can be done as a single array.
|
||||||
if (!is_array($data[array_rand($data)]))
|
if (!is_array($data[array_rand($data)]))
|
||||||
$data = array($data);
|
$data = [$data];
|
||||||
|
|
||||||
// Create the mold for a single row insert.
|
// Create the mold for a single row insert.
|
||||||
$insertData = '(';
|
$insertData = '(';
|
||||||
@@ -514,12 +515,12 @@ class Database
|
|||||||
$indexed_columns = array_keys($columns);
|
$indexed_columns = array_keys($columns);
|
||||||
|
|
||||||
// Here's where the variables are injected to the query.
|
// Here's where the variables are injected to the query.
|
||||||
$insertRows = array();
|
$insertRows = [];
|
||||||
foreach ($data as $dataRow)
|
foreach ($data as $dataRow)
|
||||||
$insertRows[] = $this->quote($insertData, array_combine($indexed_columns, $dataRow));
|
$insertRows[] = $this->quote($insertData, array_combine($indexed_columns, $dataRow));
|
||||||
|
|
||||||
// Determine the method of insertion.
|
// Determine the method of insertion.
|
||||||
$queryTitle = $method == 'replace' ? 'REPLACE' : ($method == 'ignore' ? 'INSERT IGNORE' : 'INSERT');
|
$queryTitle = $method === 'replace' ? 'REPLACE' : ($method === 'ignore' ? 'INSERT IGNORE' : 'INSERT');
|
||||||
|
|
||||||
// Do the insert.
|
// Do the insert.
|
||||||
return $this->query('
|
return $this->query('
|
||||||
@@ -527,9 +528,6 @@ class Database
|
|||||||
VALUES
|
VALUES
|
||||||
' . implode(',
|
' . implode(',
|
||||||
', $insertRows),
|
', $insertRows),
|
||||||
array(
|
['security_override' => true]);
|
||||||
'security_override' => true,
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,68 +8,12 @@
|
|||||||
|
|
||||||
class Dispatcher
|
class Dispatcher
|
||||||
{
|
{
|
||||||
public static function route()
|
|
||||||
{
|
|
||||||
$possibleActions = [
|
|
||||||
'albums' => 'ViewPhotoAlbums',
|
|
||||||
'editasset' => 'EditAsset',
|
|
||||||
'edituser' => 'EditUser',
|
|
||||||
'login' => 'Login',
|
|
||||||
'logout' => 'Logout',
|
|
||||||
'managecomments' => 'ManageComments',
|
|
||||||
'manageerrors' => 'ManageErrors',
|
|
||||||
'managetags' => 'ManageTags',
|
|
||||||
'manageusers' => 'ManageUsers',
|
|
||||||
'people' => 'ViewPeople',
|
|
||||||
'resetpassword' => 'ResetPassword',
|
|
||||||
'suggest' => 'ProvideAutoSuggest',
|
|
||||||
'timeline' => 'ViewTimeline',
|
|
||||||
'uploadmedia' => 'UploadMedia',
|
|
||||||
];
|
|
||||||
|
|
||||||
// Work around PHP's FPM not always providing PATH_INFO.
|
|
||||||
if (empty($_SERVER['PATH_INFO']) && isset($_SERVER['REQUEST_URI']))
|
|
||||||
{
|
|
||||||
if (strpos($_SERVER['REQUEST_URI'], '?') === false)
|
|
||||||
$_SERVER['PATH_INFO'] = $_SERVER['REQUEST_URI'];
|
|
||||||
else
|
|
||||||
$_SERVER['PATH_INFO'] = substr($_SERVER['REQUEST_URI'], 0, strpos($_SERVER['REQUEST_URI'], '?'));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Just showing the album index?
|
|
||||||
if (empty($_SERVER['PATH_INFO']) || $_SERVER['PATH_INFO'] == '/')
|
|
||||||
{
|
|
||||||
return new ViewPhotoAlbum();
|
|
||||||
}
|
|
||||||
// Look for particular actions...
|
|
||||||
elseif (preg_match('~^/(?<action>[a-z]+)(?:/page/(?<page>\d+))?/?~', $_SERVER['PATH_INFO'], $path) && isset($possibleActions[$path['action']]))
|
|
||||||
{
|
|
||||||
$_GET = array_merge($_GET, $path);
|
|
||||||
return new $possibleActions[$path['action']]();
|
|
||||||
}
|
|
||||||
// An album, person, or any other tag?
|
|
||||||
elseif (preg_match('~^/(?<tag>.+?)(?:/page/(?<page>\d+))?/?$~', $_SERVER['PATH_INFO'], $path) && Tag::matchSlug($path['tag']))
|
|
||||||
{
|
|
||||||
$_GET = array_merge($_GET, $path);
|
|
||||||
return new ViewPhotoAlbum();
|
|
||||||
}
|
|
||||||
// A photo for sure, then, right?
|
|
||||||
elseif (preg_match('~^/(?<slug>.+?)/?$~', $_SERVER['PATH_INFO'], $path))
|
|
||||||
{
|
|
||||||
$_GET = array_merge($_GET, $path);
|
|
||||||
return new ViewPhoto();
|
|
||||||
}
|
|
||||||
// No idea, then?
|
|
||||||
else
|
|
||||||
throw new NotFoundException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function dispatch()
|
public static function dispatch()
|
||||||
{
|
{
|
||||||
// Let's try to find our bearings!
|
// Let's try to find our bearings!
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
$page = self::route();
|
$page = Router::route();
|
||||||
$page->showContent();
|
$page->showContent();
|
||||||
}
|
}
|
||||||
// Something wasn't found?
|
// Something wasn't found?
|
||||||
@@ -103,10 +47,10 @@ class Dispatcher
|
|||||||
/**
|
/**
|
||||||
* Kicks a guest to a login form, redirecting them back to this page upon login.
|
* Kicks a guest to a login form, redirecting them back to this page upon login.
|
||||||
*/
|
*/
|
||||||
public static function kickGuest()
|
public static function kickGuest($title = null, $message = null)
|
||||||
{
|
{
|
||||||
$form = new LogInForm('Log in');
|
$form = new LogInForm('Log in');
|
||||||
$form->adopt(new Alert('', 'You need to be logged in to view this page.', 'error'));
|
$form->adopt(new Alert($title ?? '', $message ?? 'You need to be logged in to view this page.', 'danger'));
|
||||||
$form->setRedirectUrl($_SERVER['REQUEST_URI']);
|
$form->setRedirectUrl($_SERVER['REQUEST_URI']);
|
||||||
|
|
||||||
$page = new MainTemplate('Login required');
|
$page = new MainTemplate('Login required');
|
||||||
@@ -142,7 +86,6 @@ class Dispatcher
|
|||||||
if (Registry::has('user') && Registry::get('user')->isAdmin())
|
if (Registry::has('user') && Registry::get('user')->isAdmin())
|
||||||
{
|
{
|
||||||
$page->appendStylesheet(BASEURL . '/css/admin.css');
|
$page->appendStylesheet(BASEURL . '/css/admin.css');
|
||||||
$page->adopt(new AdminBar());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$page->adopt(new DummyBox('Well, this is a bit embarrassing!', '<p>The page you requested could not be found. Don\'t worry, it\'s probably not your fault. You\'re welcome to browse the website, though!</p>', 'errormsg'));
|
$page->adopt(new DummyBox('Well, this is a bit embarrassing!', '<p>The page you requested could not be found. Don\'t worry, it\'s probably not your fault. You\'re welcome to browse the website, though!</p>', 'errormsg'));
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ class EXIF
|
|||||||
public $iso = 0;
|
public $iso = 0;
|
||||||
public $shutter_speed = 0;
|
public $shutter_speed = 0;
|
||||||
public $title = '';
|
public $title = '';
|
||||||
|
public $software = '';
|
||||||
|
|
||||||
private function __construct(array $meta)
|
private function __construct(array $meta)
|
||||||
{
|
{
|
||||||
@@ -35,6 +36,7 @@ class EXIF
|
|||||||
'iso' => 0,
|
'iso' => 0,
|
||||||
'shutter_speed' => 0,
|
'shutter_speed' => 0,
|
||||||
'title' => '',
|
'title' => '',
|
||||||
|
'software' => '',
|
||||||
];
|
];
|
||||||
|
|
||||||
if (!function_exists('exif_read_data'))
|
if (!function_exists('exif_read_data'))
|
||||||
@@ -96,9 +98,14 @@ class EXIF
|
|||||||
elseif (!empty($exif['Make']))
|
elseif (!empty($exif['Make']))
|
||||||
$meta['camera'] = trim($exif['Make']);
|
$meta['camera'] = trim($exif['Make']);
|
||||||
|
|
||||||
if (!empty($exif['DateTimeDigitized']))
|
if (!empty($exif['DateTimeOriginal']))
|
||||||
|
$meta['created_timestamp'] = self::toUnixTime($exif['DateTimeOriginal']);
|
||||||
|
elseif (!empty($exif['DateTimeDigitized']))
|
||||||
$meta['created_timestamp'] = self::toUnixTime($exif['DateTimeDigitized']);
|
$meta['created_timestamp'] = self::toUnixTime($exif['DateTimeDigitized']);
|
||||||
|
|
||||||
|
if (!empty($exif['Software']))
|
||||||
|
$meta['software'] = $exif['Software'];
|
||||||
|
|
||||||
return new self($meta);
|
return new self($meta);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ class Email
|
|||||||
$boundary = uniqid('sr');
|
$boundary = uniqid('sr');
|
||||||
|
|
||||||
if (empty($headers))
|
if (empty($headers))
|
||||||
$headers .= "From: HashRU Pics <no-reply@aaronweb.net>\r\n";
|
$headers .= "From: " . SITE_TITLE . " <" . REPLY_TO_ADDRESS . ">\r\n";
|
||||||
|
|
||||||
// Set up headers.
|
// Set up headers.
|
||||||
$headers .= "MIME-Version: 1.0\r\n";
|
$headers .= "MIME-Version: 1.0\r\n";
|
||||||
|
|||||||
@@ -63,11 +63,11 @@ class ErrorHandler
|
|||||||
|
|
||||||
// Include info on the contents of superglobals.
|
// Include info on the contents of superglobals.
|
||||||
if (!empty($_SESSION))
|
if (!empty($_SESSION))
|
||||||
$debug_info .= "\nSESSION: " . print_r($_SESSION, true);
|
$debug_info .= "\nSESSION: " . var_export($_SESSION, true);
|
||||||
if (!empty($_POST))
|
if (!empty($_POST))
|
||||||
$debug_info .= "\nPOST: " . print_r($_POST, true);
|
$debug_info .= "\nPOST: " . var_export($_POST, true);
|
||||||
if (!empty($_GET))
|
if (!empty($_GET))
|
||||||
$debug_info .= "\nGET: " . print_r($_GET, true);
|
$debug_info .= "\nGET: " . var_export($_GET, true);
|
||||||
|
|
||||||
return $debug_info;
|
return $debug_info;
|
||||||
}
|
}
|
||||||
@@ -96,12 +96,17 @@ class ErrorHandler
|
|||||||
$object = isset($call['class']) ? $call['class'] . $call['type'] : '';
|
$object = isset($call['class']) ? $call['class'] . $call['type'] : '';
|
||||||
|
|
||||||
$args = [];
|
$args = [];
|
||||||
foreach ($call['args'] as $j => $arg)
|
if (isset($call['args']))
|
||||||
{
|
{
|
||||||
if (is_array($arg))
|
foreach ($call['args'] as $j => $arg)
|
||||||
$args[$j] = print_r($arg, true);
|
{
|
||||||
elseif (is_object($arg))
|
// Only include the class name for objects
|
||||||
$args[$j] = var_dump($arg);
|
if (is_object($arg))
|
||||||
|
$args[$j] = get_class($arg) . '{}';
|
||||||
|
// Export everything else -- including arrays
|
||||||
|
else
|
||||||
|
$args[$j] = var_export($arg, true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$buffer .= '#' . str_pad($i, 3, ' ')
|
$buffer .= '#' . str_pad($i, 3, ' ')
|
||||||
@@ -168,7 +173,6 @@ class ErrorHandler
|
|||||||
if ($is_admin)
|
if ($is_admin)
|
||||||
{
|
{
|
||||||
$page->appendStylesheet(BASEURL . '/css/admin.css');
|
$page->appendStylesheet(BASEURL . '/css/admin.css');
|
||||||
$page->adopt(new AdminBar());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
elseif (!$is_sensitive)
|
elseif (!$is_sensitive)
|
||||||
|
|||||||
340
models/Form.php
340
models/Form.php
@@ -3,7 +3,7 @@
|
|||||||
* Form.php
|
* Form.php
|
||||||
* Contains key class Form.
|
* Contains key class Form.
|
||||||
*
|
*
|
||||||
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
* Kabuki CMS (C) 2013-2023, Aaron van Geffen
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
class Form
|
class Form
|
||||||
@@ -12,9 +12,11 @@ class Form
|
|||||||
public $request_url;
|
public $request_url;
|
||||||
public $content_above;
|
public $content_above;
|
||||||
public $content_below;
|
public $content_below;
|
||||||
private $fields;
|
private $fields = [];
|
||||||
private $data;
|
private $data = [];
|
||||||
private $missing;
|
private $missing = [];
|
||||||
|
private $submit_caption;
|
||||||
|
private $trim_inputs;
|
||||||
|
|
||||||
// NOTE: this class does not verify the completeness of form options.
|
// NOTE: this class does not verify the completeness of form options.
|
||||||
public function __construct($options)
|
public function __construct($options)
|
||||||
@@ -24,9 +26,42 @@ class Form
|
|||||||
$this->fields = !empty($options['fields']) ? $options['fields'] : [];
|
$this->fields = !empty($options['fields']) ? $options['fields'] : [];
|
||||||
$this->content_below = !empty($options['content_below']) ? $options['content_below'] : null;
|
$this->content_below = !empty($options['content_below']) ? $options['content_below'] : null;
|
||||||
$this->content_above = !empty($options['content_above']) ? $options['content_above'] : null;
|
$this->content_above = !empty($options['content_above']) ? $options['content_above'] : null;
|
||||||
|
$this->submit_caption = !empty($options['submit_caption']) ? $options['submit_caption'] : 'Save information';
|
||||||
|
$this->trim_inputs = !empty($options['trim_inputs']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function verify($post)
|
public function getFields()
|
||||||
|
{
|
||||||
|
return $this->fields;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getData()
|
||||||
|
{
|
||||||
|
return $this->data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSubmitButtonCaption()
|
||||||
|
{
|
||||||
|
return $this->submit_caption;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMissing()
|
||||||
|
{
|
||||||
|
return $this->missing;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setData($data)
|
||||||
|
{
|
||||||
|
$this->verify($data, true);
|
||||||
|
$this->missing = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setFieldAsMissing($field)
|
||||||
|
{
|
||||||
|
$this->missing[] = $field;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function verify($post, $initalisation = false)
|
||||||
{
|
{
|
||||||
$this->data = [];
|
$this->data = [];
|
||||||
$this->missing = [];
|
$this->missing = [];
|
||||||
@@ -41,30 +76,43 @@ class Form
|
|||||||
}
|
}
|
||||||
|
|
||||||
// No data present at all for this field?
|
// No data present at all for this field?
|
||||||
if ((!isset($post[$field_id]) || $post[$field_id] == '') && empty($field['is_optional']))
|
if ((!isset($post[$field_id]) || $post[$field_id] == '') &&
|
||||||
|
$field['type'] !== 'captcha')
|
||||||
{
|
{
|
||||||
$this->missing[] = $field_id;
|
if (empty($field['is_optional']))
|
||||||
$this->data[$field_id] = '';
|
$this->missing[] = $field_id;
|
||||||
|
|
||||||
|
if ($field['type'] === 'select' && !empty($field['multiple']))
|
||||||
|
$this->data[$field_id] = [];
|
||||||
|
else
|
||||||
|
$this->data[$field_id] = '';
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify data for all fields
|
// Should we trim this?
|
||||||
|
if ($this->trim_inputs && $field['type'] !== 'captcha' && empty($field['multiple']))
|
||||||
|
$post[$field_id] = trim($post[$field_id]);
|
||||||
|
|
||||||
|
// Using a custom validation function?
|
||||||
|
if (isset($field['validate']) && is_callable($field['validate']))
|
||||||
|
{
|
||||||
|
// Validation functions can clean up the data if passed by reference
|
||||||
|
$this->data[$field_id] = $post[$field_id];
|
||||||
|
|
||||||
|
// Evaluate validation functions as boolean to see if data is missing
|
||||||
|
if (!$field['validate']($post[$field_id]))
|
||||||
|
$this->missing[] = $field_id;
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify data by field type
|
||||||
switch ($field['type'])
|
switch ($field['type'])
|
||||||
{
|
{
|
||||||
case 'select':
|
case 'select':
|
||||||
case 'radio':
|
case 'radio':
|
||||||
// Skip validation? Dangerous territory!
|
$this->validateSelect($field_id, $field, $post);
|
||||||
if (isset($field['verify_options']) && $field['verify_options'] === false)
|
|
||||||
$this->data[$field_id] = $post[$field_id];
|
|
||||||
// Check whether selected option is valid.
|
|
||||||
elseif (isset($post[$field_id]) && !isset($field['options'][$post[$field_id]]))
|
|
||||||
{
|
|
||||||
$this->missing[] = $field_id;
|
|
||||||
$this->data[$field_id] = '';
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
$this->data[$field_id] = $post[$field_id];
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'checkbox':
|
case 'checkbox':
|
||||||
@@ -73,61 +121,22 @@ class Form
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'color':
|
case 'color':
|
||||||
// Colors are stored as a string of length 3 or 6 (hex)
|
$this->validateColor($field_id, $field, $post);
|
||||||
if (!isset($post[$field_id]) || (strlen($post[$field_id]) != 3 && strlen($post[$field_id]) != 6))
|
|
||||||
{
|
|
||||||
$this->missing[] = $field_id;
|
|
||||||
$this->data[$field_id] = '';
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
$this->data[$field_id] = $post[$field_id];
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'file':
|
case 'file':
|
||||||
// Needs to be verified elsewhere!
|
// Asset needs to be processed out of POST! This is just a filename.
|
||||||
|
$this->data[$field_id] = isset($post[$field_id]) ? $post[$field_id] : '';
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'numeric':
|
case 'numeric':
|
||||||
$data = isset($post[$field_id]) ? $post[$field_id] : '';
|
$this->validateNumeric($field_id, $field, $post);
|
||||||
// Do we need to check bounds?
|
break;
|
||||||
if (isset($field['min_value']) && is_numeric($data))
|
|
||||||
{
|
case 'captcha':
|
||||||
if (is_float($field['min_value']) && (float) $data < $field['min_value'])
|
if (isset($_POST['g-recaptcha-response']) && !$initalisation)
|
||||||
{
|
$this->validateCaptcha($field_id);
|
||||||
$this->missing[] = $field_id;
|
elseif (!$initalisation)
|
||||||
$this->data[$field_id] = 0.0;
|
|
||||||
}
|
|
||||||
elseif (is_int($field['min_value']) && (int) $data < $field['min_value'])
|
|
||||||
{
|
|
||||||
$this->missing[] = $field_id;
|
|
||||||
$this->data[$field_id] = 0;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
$this->data[$field_id] = $data;
|
|
||||||
}
|
|
||||||
elseif (isset($field['max_value']) && is_numeric($data))
|
|
||||||
{
|
|
||||||
if (is_float($field['max_value']) && (float) $data > $field['max_value'])
|
|
||||||
{
|
|
||||||
$this->missing[] = $field_id;
|
|
||||||
$this->data[$field_id] = 0.0;
|
|
||||||
}
|
|
||||||
elseif (is_int($field['max_value']) && (int) $data > $field['max_value'])
|
|
||||||
{
|
|
||||||
$this->missing[] = $field_id;
|
|
||||||
$this->data[$field_id] = 0;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
$this->data[$field_id] = $data;
|
|
||||||
}
|
|
||||||
// Does it look numeric?
|
|
||||||
elseif (is_numeric($data))
|
|
||||||
{
|
|
||||||
$this->data[$field_id] = $data;
|
|
||||||
}
|
|
||||||
// Let's consider it missing, then.
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
$this->missing[] = $field_id;
|
$this->missing[] = $field_id;
|
||||||
$this->data[$field_id] = 0;
|
$this->data[$field_id] = 0;
|
||||||
@@ -137,29 +146,200 @@ class Form
|
|||||||
case 'text':
|
case 'text':
|
||||||
case 'textarea':
|
case 'textarea':
|
||||||
default:
|
default:
|
||||||
$this->data[$field_id] = isset($post[$field_id]) ? $post[$field_id] : '';
|
$this->validateText($field_id, $field, $post);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setData($data)
|
private function validateCaptcha($field_id)
|
||||||
{
|
{
|
||||||
$this->verify($data);
|
$postdata = http_build_query([
|
||||||
$this->missing = [];
|
'secret' => RECAPTCHA_API_SECRET,
|
||||||
|
'response' => $_POST['g-recaptcha-response'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$opts = [
|
||||||
|
'http' => [
|
||||||
|
'method' => 'POST',
|
||||||
|
'header' => 'Content-type: application/x-www-form-urlencoded',
|
||||||
|
'content' => $postdata,
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
$context = stream_context_create($opts);
|
||||||
|
$result = file_get_contents('https://www.google.com/recaptcha/api/siteverify', false, $context);
|
||||||
|
$check = json_decode($result);
|
||||||
|
|
||||||
|
if ($check->success)
|
||||||
|
{
|
||||||
|
$this->data[$field_id] = 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$this->data[$field_id] = 0;
|
||||||
|
$this->missing[] = $field_id;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getFields()
|
private function validateColor($field_id, array $field, array $post)
|
||||||
{
|
{
|
||||||
return $this->fields;
|
// Colors are stored as a string of length 3 or 6 (hex)
|
||||||
|
if (!isset($post[$field_id]) || (strlen($post[$field_id]) != 3 && strlen($post[$field_id]) != 6))
|
||||||
|
{
|
||||||
|
$this->missing[] = $field_id;
|
||||||
|
$this->data[$field_id] = '';
|
||||||
|
}
|
||||||
|
else
|
||||||
|
$this->data[$field_id] = $post[$field_id];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getData()
|
private function validateNumeric($field_id, array $field, array $post)
|
||||||
{
|
{
|
||||||
return $this->data;
|
$data = isset($post[$field_id]) ? $post[$field_id] : '';
|
||||||
|
|
||||||
|
// Sanity check: does this even look numeric?
|
||||||
|
if (!is_numeric($data))
|
||||||
|
{
|
||||||
|
$this->missing[] = $field_id;
|
||||||
|
$this->data[$field_id] = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do we need to a minimum bound?
|
||||||
|
if (isset($field['min_value']))
|
||||||
|
{
|
||||||
|
if (is_float($field['min_value']) && (float) $data < $field['min_value'])
|
||||||
|
{
|
||||||
|
$this->missing[] = $field_id;
|
||||||
|
$this->data[$field_id] = 0.0;
|
||||||
|
}
|
||||||
|
elseif (is_int($field['min_value']) && (int) $data < $field['min_value'])
|
||||||
|
{
|
||||||
|
$this->missing[] = $field_id;
|
||||||
|
$this->data[$field_id] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// What about a maximum bound?
|
||||||
|
if (isset($field['max_value']))
|
||||||
|
{
|
||||||
|
if (is_float($field['max_value']) && (float) $data > $field['max_value'])
|
||||||
|
{
|
||||||
|
$this->missing[] = $field_id;
|
||||||
|
$this->data[$field_id] = 0.0;
|
||||||
|
}
|
||||||
|
elseif (is_int($field['max_value']) && (int) $data > $field['max_value'])
|
||||||
|
{
|
||||||
|
$this->missing[] = $field_id;
|
||||||
|
$this->data[$field_id] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->data[$field_id] = $data;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getMissing()
|
private function validateSelect($field_id, array $field, array $post)
|
||||||
{
|
{
|
||||||
return $this->missing;
|
// Skip validation? Dangerous territory!
|
||||||
|
if (isset($field['verify_options']) && $field['verify_options'] === false)
|
||||||
|
{
|
||||||
|
$this->data[$field_id] = $post[$field_id];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check whether selected option is valid.
|
||||||
|
if (($field['type'] !== 'select' || empty($field['multiple'])) && empty($field['has_groups']))
|
||||||
|
{
|
||||||
|
if (isset($post[$field_id]) && !isset($field['options'][$post[$field_id]]))
|
||||||
|
{
|
||||||
|
$this->missing[] = $field_id;
|
||||||
|
$this->data[$field_id] = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
$this->data[$field_id] = $post[$field_id];
|
||||||
|
}
|
||||||
|
// Multiple selections involve a bit more work.
|
||||||
|
elseif (!empty($field['multiple']) && empty($field['has_groups']))
|
||||||
|
{
|
||||||
|
$this->data[$field_id] = [];
|
||||||
|
if (!is_array($post[$field_id]))
|
||||||
|
{
|
||||||
|
if (isset($field['options'][$post[$field_id]]))
|
||||||
|
$this->data[$field_id][] = $post[$field_id];
|
||||||
|
else
|
||||||
|
$this->missing[] = $field_id;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($post[$field_id] as $option)
|
||||||
|
{
|
||||||
|
if (isset($field['options'][$option]))
|
||||||
|
$this->data[$field_id][] = $option;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($this->data[$field_id]))
|
||||||
|
$this->missing[] = $field_id;
|
||||||
|
}
|
||||||
|
// Any optgroups involved?
|
||||||
|
elseif (!empty($field['has_groups']))
|
||||||
|
{
|
||||||
|
if (!isset($post[$field_id]))
|
||||||
|
{
|
||||||
|
$this->missing[] = $field_id;
|
||||||
|
$this->data[$field_id] = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expensive: iterate over all groups until the value selected has been found.
|
||||||
|
foreach ($field['options'] as $label => $options)
|
||||||
|
{
|
||||||
|
if (is_array($options))
|
||||||
|
{
|
||||||
|
// Consider each of the options as a valid a value.
|
||||||
|
foreach ($options as $value => $label)
|
||||||
|
{
|
||||||
|
if ($post[$field_id] === $value)
|
||||||
|
{
|
||||||
|
$this->data[$field_id] = $options;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// This is an ungrouped value in disguise! Treat it as such.
|
||||||
|
if ($post[$field_id] === $options)
|
||||||
|
{
|
||||||
|
$this->data[$field_id] = $options;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we've reached this point, we'll consider the data invalid.
|
||||||
|
$this->missing[] = $field_id;
|
||||||
|
$this->data[$field_id] = '';
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new UnexpectedValueException('Unexpected field configuration in validateSelect!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function validateText($field_id, array $field, array $post)
|
||||||
|
{
|
||||||
|
$this->data[$field_id] = isset($post[$field_id]) ? $post[$field_id] : '';
|
||||||
|
|
||||||
|
// Trim leading and trailing whitespace?
|
||||||
|
if (!empty($field['trim']))
|
||||||
|
$this->data[$field_id] = trim($this->data[$field_id]);
|
||||||
|
|
||||||
|
// Is there a length limit to enforce?
|
||||||
|
if (isset($field['maxlength']) && strlen($post[$field_id]) > $field['maxlength']) {
|
||||||
|
$post[$field_id] = substr($post[$field_id], 0, $field['maxlength']);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,25 +3,29 @@
|
|||||||
* GenericTable.php
|
* GenericTable.php
|
||||||
* Contains key class GenericTable.
|
* Contains key class GenericTable.
|
||||||
*
|
*
|
||||||
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
* Kabuki CMS (C) 2013-2023, Aaron van Geffen
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
class GenericTable extends PageIndex
|
class GenericTable
|
||||||
{
|
{
|
||||||
protected $header = [];
|
private $header = [];
|
||||||
protected $body = [];
|
private $body = [];
|
||||||
protected $page_index = [];
|
private $pageIndex = null;
|
||||||
|
private $currentPage = 1;
|
||||||
|
|
||||||
protected $title;
|
private $title;
|
||||||
protected $title_class;
|
private $title_class;
|
||||||
protected $tableIsSortable = false;
|
private $tableIsSortable = false;
|
||||||
protected $recordCount;
|
|
||||||
protected $needsPageIndex = false;
|
|
||||||
protected $current_page;
|
|
||||||
protected $num_pages;
|
|
||||||
|
|
||||||
public $form_above;
|
public $form_above;
|
||||||
public $form_below;
|
public $form_below;
|
||||||
|
private $table_class;
|
||||||
|
private $sort_direction;
|
||||||
|
private $sort_order;
|
||||||
|
private $base_url;
|
||||||
|
private $start;
|
||||||
|
private $items_per_page;
|
||||||
|
private $recordCount;
|
||||||
|
|
||||||
public function __construct($options)
|
public function __construct($options)
|
||||||
{
|
{
|
||||||
@@ -30,40 +34,38 @@ class GenericTable extends PageIndex
|
|||||||
$options['sort_order'] = '';
|
$options['sort_order'] = '';
|
||||||
|
|
||||||
// Order in which direction?
|
// Order in which direction?
|
||||||
if (!empty($options['sort_direction']) && !in_array($options['sort_direction'], array('up', 'down')))
|
if (!empty($options['sort_direction']) && !in_array($options['sort_direction'], ['up', 'down']))
|
||||||
$options['sort_direction'] = 'up';
|
$options['sort_direction'] = 'up';
|
||||||
|
|
||||||
// Make sure we know whether we can actually sort on something.
|
// Make sure we know whether we can actually sort on something.
|
||||||
$this->tableIsSortable = !empty($options['base_url']);
|
$this->tableIsSortable = !empty($options['base_url']);
|
||||||
|
|
||||||
// How much stuff do we have?
|
// How much data do we have?
|
||||||
$this->recordCount = call_user_func_array($options['get_count'], !empty($options['get_count_params']) ? $options['get_count_params'] : array());
|
$this->recordCount = $options['get_count'](...(!empty($options['get_count_params']) ? $options['get_count_params'] : []));
|
||||||
|
|
||||||
// Should we create a page index?
|
// How much data do we need to retrieve?
|
||||||
$this->items_per_page = !empty($options['items_per_page']) ? $options['items_per_page'] : 30;
|
$this->items_per_page = !empty($options['items_per_page']) ? $options['items_per_page'] : 30;
|
||||||
$this->needsPageIndex = !empty($this->items_per_page) && $this->recordCount > $this->items_per_page;
|
|
||||||
$this->index_class = isset($options['index_class']) ? $options['index_class'] : '';
|
|
||||||
|
|
||||||
// Figure out where to start.
|
// Figure out where to start.
|
||||||
$this->start = empty($options['start']) || !is_numeric($options['start']) || $options['start'] < 0 || $options['start'] > $this->recordCount ? 0 : $options['start'];
|
$this->start = empty($options['start']) || !is_numeric($options['start']) || $options['start'] < 0 || $options['start'] > $this->recordCount ? 0 : $options['start'];
|
||||||
|
|
||||||
|
// Figure out where we are on the whole, too.
|
||||||
|
$numPages = max(1, ceil($this->recordCount / $this->items_per_page));
|
||||||
|
$this->currentPage = min(ceil($this->start / $this->items_per_page) + 1, $numPages);
|
||||||
|
|
||||||
// Let's bear a few things in mind...
|
// Let's bear a few things in mind...
|
||||||
$this->base_url = $options['base_url'];
|
$this->base_url = $options['base_url'];
|
||||||
|
|
||||||
// This should be set at all times, too.
|
|
||||||
if (empty($options['no_items_label']))
|
|
||||||
$options['no_items_label'] = '';
|
|
||||||
|
|
||||||
// Gather parameters for the data gather function first.
|
// Gather parameters for the data gather function first.
|
||||||
$parameters = array($this->start, $this->items_per_page, $options['sort_order'], $options['sort_direction']);
|
$parameters = [$this->start, $this->items_per_page, $options['sort_order'], $options['sort_direction']];
|
||||||
if (!empty($options['get_data_params']) && is_array($options['get_data_params']))
|
if (!empty($options['get_data_params']) && is_array($options['get_data_params']))
|
||||||
$parameters = array_merge($parameters, $options['get_data_params']);
|
$parameters = array_merge($parameters, $options['get_data_params']);
|
||||||
|
|
||||||
// Okay, let's fetch the data!
|
// Okay, let's fetch the data!
|
||||||
$data = call_user_func_array($options['get_data'], $parameters);
|
$data = $options['get_data'](...$parameters);
|
||||||
|
|
||||||
// Clean up a bit.
|
// Extract data into local variables.
|
||||||
$rows = $data['rows'];
|
$rawRowData = $data['rows'];
|
||||||
$this->sort_order = $data['order'];
|
$this->sort_order = $data['order'];
|
||||||
$this->sort_direction = $data['direction'];
|
$this->sort_direction = $data['direction'];
|
||||||
unset($data);
|
unset($data);
|
||||||
@@ -71,24 +73,26 @@ class GenericTable extends PageIndex
|
|||||||
// Okay, now for the column headers...
|
// Okay, now for the column headers...
|
||||||
$this->generateColumnHeaders($options);
|
$this->generateColumnHeaders($options);
|
||||||
|
|
||||||
// Generate a pagination if requested
|
// Should we create a page index?
|
||||||
if ($this->needsPageIndex)
|
$needsPageIndex = !empty($this->items_per_page) && $this->recordCount > $this->items_per_page;
|
||||||
$this->generatePageIndex();
|
if ($needsPageIndex)
|
||||||
|
$this->generatePageIndex($options);
|
||||||
|
|
||||||
// Not a single row in sight?
|
// Process the data to be shown into rows.
|
||||||
if (empty($rows))
|
if (!empty($rawRowData))
|
||||||
$this->body = $options['no_items_label'];
|
$this->processAllRows($rawRowData, $options);
|
||||||
// Otherwise, parse it all!
|
|
||||||
else
|
else
|
||||||
$this->parseAllRows($rows, $options);
|
$this->body = $options['no_items_label'] ?? '';
|
||||||
|
|
||||||
|
$this->table_class = $options['table_class'] ?? '';
|
||||||
|
|
||||||
// Got a title?
|
// Got a title?
|
||||||
$this->title = isset($options['title']) ? htmlentities($options['title']) : '';
|
$this->title = $options['title'] ?? '';
|
||||||
$this->title_class = isset($options['title_class']) ? $options['title_class'] : '';
|
$this->title_class = $options['title_class'] ?? '';
|
||||||
|
|
||||||
// Maybe even a form or two?
|
// Maybe even a form or two?
|
||||||
$this->form_above = isset($options['form_above']) ? $options['form_above'] : (isset($options['form']) ? $options['form'] : null);
|
$this->form_above = $options['form_above'] ?? $options['form'] ?? null;
|
||||||
$this->form_below = isset($options['form_below']) ? $options['form_below'] : (isset($options['form']) ? $options['form'] : null);
|
$this->form_below = $options['form_below'] ?? $options['form'] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function generateColumnHeaders($options)
|
private function generateColumnHeaders($options)
|
||||||
@@ -98,107 +102,36 @@ class GenericTable extends PageIndex
|
|||||||
if (empty($column['header']))
|
if (empty($column['header']))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
$header = array(
|
$isSortable = $this->tableIsSortable && !empty($column['is_sortable']);
|
||||||
|
$sortDirection = $key == $this->sort_order && $this->sort_direction === 'up' ? 'down' : 'up';
|
||||||
|
|
||||||
|
$header = [
|
||||||
'class' => isset($column['class']) ? $column['class'] : '',
|
'class' => isset($column['class']) ? $column['class'] : '',
|
||||||
|
'cell_class' => isset($column['cell_class']) ? $column['cell_class'] : null,
|
||||||
'colspan' => !empty($column['header_colspan']) ? $column['header_colspan'] : 1,
|
'colspan' => !empty($column['header_colspan']) ? $column['header_colspan'] : 1,
|
||||||
'href' => !$this->tableIsSortable || empty($column['is_sortable']) ? '' : $this->getLink($this->start, $key, $key == $this->sort_order && $this->sort_direction == 'up' ? 'down' : 'up'),
|
'href' => $isSortable ? $this->getLink($this->start, $key, $sortDirection) : null,
|
||||||
'label' => $column['header'],
|
'label' => $column['header'],
|
||||||
'scope' => 'col',
|
'scope' => 'col',
|
||||||
'sort_mode' => $key == $this->sort_order ? $this->sort_direction : null,
|
'sort_mode' => $key == $this->sort_order ? $this->sort_direction : null,
|
||||||
'width' => !empty($column['header_width']) && is_int($column['header_width']) ? $column['header_width'] : null,
|
'width' => !empty($column['header_width']) && is_int($column['header_width']) ? $column['header_width'] : null,
|
||||||
);
|
];
|
||||||
|
|
||||||
$this->header[] = $header;
|
$this->header[] = $header;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function parseAllRows($rows, $options)
|
private function generatePageIndex($options)
|
||||||
{
|
{
|
||||||
// Parse all rows...
|
$this->pageIndex = new PageIndex([
|
||||||
$i = 0;
|
'base_url' => $this->base_url,
|
||||||
foreach ($rows as $row)
|
'index_class' => $options['index_class'] ?? '',
|
||||||
{
|
'items_per_page' => $this->items_per_page,
|
||||||
$i ++;
|
'linkBuilder' => [$this, 'getLink'],
|
||||||
$newRow = array(
|
'recordCount' => $this->recordCount,
|
||||||
'class' => $i %2 == 1 ? 'odd' : 'even',
|
'sort_direction' => $this->sort_direction,
|
||||||
'cells' => array(),
|
'sort_order' => $this->sort_order,
|
||||||
);
|
'start' => $this->start,
|
||||||
|
]);
|
||||||
foreach ($options['columns'] as $column)
|
|
||||||
{
|
|
||||||
if (isset($column['enabled']) && $column['enabled'] == false)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
// The hard way?
|
|
||||||
if (isset($column['parse']))
|
|
||||||
{
|
|
||||||
if (!isset($column['parse']['type']))
|
|
||||||
$column['parse']['type'] = 'value';
|
|
||||||
|
|
||||||
// Parse the basic value first.
|
|
||||||
switch ($column['parse']['type'])
|
|
||||||
{
|
|
||||||
// value: easy as pie.
|
|
||||||
default:
|
|
||||||
case 'value':
|
|
||||||
$value = $row[$column['parse']['data']];
|
|
||||||
break;
|
|
||||||
|
|
||||||
// sprintf: filling the gaps!
|
|
||||||
case 'sprintf':
|
|
||||||
$parameters = array($column['parse']['data']['pattern']);
|
|
||||||
foreach ($column['parse']['data']['arguments'] as $identifier)
|
|
||||||
$parameters[] = $row[$identifier];
|
|
||||||
$value = call_user_func_array('sprintf', $parameters);
|
|
||||||
break;
|
|
||||||
|
|
||||||
// timestamps: let's make them readable!
|
|
||||||
case 'timestamp':
|
|
||||||
$pattern = !empty($column['parse']['data']['pattern']) && $column['parse']['data']['pattern'] == 'long' ? '%F %H:%M' : '%H:%M';
|
|
||||||
|
|
||||||
if (!is_numeric($row[$column['parse']['data']['timestamp']]))
|
|
||||||
$timestamp = strtotime($row[$column['parse']['data']['timestamp']]);
|
|
||||||
else
|
|
||||||
$timestamp = (int) $row[$column['parse']['data']['timestamp']];
|
|
||||||
|
|
||||||
if (isset($column['parse']['data']['if_null']) && $timestamp == 0)
|
|
||||||
$value = $column['parse']['data']['if_null'];
|
|
||||||
else
|
|
||||||
$value = strftime($pattern, $timestamp);
|
|
||||||
break;
|
|
||||||
|
|
||||||
// function: the flexible way!
|
|
||||||
case 'function':
|
|
||||||
$value = $column['parse']['data']($row);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate a link, if requested.
|
|
||||||
if (!empty($column['parse']['link']))
|
|
||||||
{
|
|
||||||
// First, generate the replacement variables.
|
|
||||||
$keys = array_keys($row);
|
|
||||||
$values = array_values($row);
|
|
||||||
foreach ($keys as $keyKey => $keyValue)
|
|
||||||
$keys[$keyKey] = '{' . strtoupper($keyValue) . '}';
|
|
||||||
|
|
||||||
$value = '<a href="' . str_replace($keys, $values, $column['parse']['link']) . '">' . $value . '</a>';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// The easy way!
|
|
||||||
else
|
|
||||||
$value = $row[$column['value']];
|
|
||||||
|
|
||||||
// Append the cell to the row.
|
|
||||||
$newRow['cells'][] = array(
|
|
||||||
'width' => !empty($column['cell_width']) && is_int($column['cell_width']) ? $column['cell_width'] : null,
|
|
||||||
'value' => $value,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Append the new row in the body.
|
|
||||||
$this->body[] = $newRow;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getLink($start = null, $order = null, $dir = null)
|
public function getLink($start = null, $order = null, $dir = null)
|
||||||
@@ -218,12 +151,6 @@ class GenericTable extends PageIndex
|
|||||||
return $this->start;
|
return $this->start;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getArray()
|
|
||||||
{
|
|
||||||
// Makes no sense to call it for a table, but inherits from PageIndex due to poor design, sorry.
|
|
||||||
throw new Exception('Function call is ambiguous.');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getHeader()
|
public function getHeader()
|
||||||
{
|
{
|
||||||
return $this->header;
|
return $this->header;
|
||||||
@@ -234,6 +161,21 @@ class GenericTable extends PageIndex
|
|||||||
return $this->body;
|
return $this->body;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getCurrentPage()
|
||||||
|
{
|
||||||
|
return $this->currentPage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPageIndex()
|
||||||
|
{
|
||||||
|
return $this->pageIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTableClass()
|
||||||
|
{
|
||||||
|
return $this->table_class;
|
||||||
|
}
|
||||||
|
|
||||||
public function getTitle()
|
public function getTitle()
|
||||||
{
|
{
|
||||||
return $this->title;
|
return $this->title;
|
||||||
@@ -243,4 +185,97 @@ class GenericTable extends PageIndex
|
|||||||
{
|
{
|
||||||
return $this->title_class;
|
return $this->title_class;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function processAllRows($rows, $options)
|
||||||
|
{
|
||||||
|
foreach ($rows as $i => $row)
|
||||||
|
{
|
||||||
|
$newRow = [
|
||||||
|
'cells' => [],
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($options['columns'] as $column)
|
||||||
|
{
|
||||||
|
// Process data for this particular cell.
|
||||||
|
if (isset($column['parse']))
|
||||||
|
$value = self::processCell($column['parse'], $row);
|
||||||
|
else
|
||||||
|
$value = $row[$column['value']];
|
||||||
|
|
||||||
|
// Append the cell to the row.
|
||||||
|
$newRow['cells'][] = [
|
||||||
|
'class' => $column['cell_class'] ?? '',
|
||||||
|
'value' => $value,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append the new row in the body.
|
||||||
|
$this->body[] = $newRow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function processCell($options, $rowData)
|
||||||
|
{
|
||||||
|
if (!isset($options['type']))
|
||||||
|
$options['type'] = 'value';
|
||||||
|
|
||||||
|
// Parse the basic value first.
|
||||||
|
switch ($options['type'])
|
||||||
|
{
|
||||||
|
// Basic option: simply take a use a particular data property.
|
||||||
|
case 'value':
|
||||||
|
$value = htmlspecialchars($rowData[$options['data']]);
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Processing via a lambda function.
|
||||||
|
case 'function':
|
||||||
|
$value = $options['data']($rowData);
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Using sprintf to fill out a particular pattern.
|
||||||
|
case 'sprintf':
|
||||||
|
$parameters = [$options['data']['pattern']];
|
||||||
|
foreach ($options['data']['arguments'] as $identifier)
|
||||||
|
$parameters[] = $rowData[$identifier];
|
||||||
|
|
||||||
|
$value = sprintf(...$parameters);
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Timestamps get custom treatment.
|
||||||
|
case 'timestamp':
|
||||||
|
if (empty($options['data']['pattern']) || $options['data']['pattern'] === 'long')
|
||||||
|
$pattern = 'Y-m-d H:i';
|
||||||
|
elseif ($options['data']['pattern'] === 'short')
|
||||||
|
$pattern = 'Y-m-d';
|
||||||
|
else
|
||||||
|
$pattern = $options['data']['pattern'];
|
||||||
|
|
||||||
|
if (!isset($rowData[$options['data']['timestamp']]))
|
||||||
|
$timestamp = 0;
|
||||||
|
elseif (!is_numeric($rowData[$options['data']['timestamp']]))
|
||||||
|
$timestamp = strtotime($rowData[$options['data']['timestamp']]);
|
||||||
|
else
|
||||||
|
$timestamp = (int) $rowData[$options['data']['timestamp']];
|
||||||
|
|
||||||
|
if (isset($options['data']['if_null']) && $timestamp == 0)
|
||||||
|
$value = $options['data']['if_null'];
|
||||||
|
else
|
||||||
|
$value = date($pattern, $timestamp);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a link, if requested.
|
||||||
|
if (!empty($options['link']))
|
||||||
|
{
|
||||||
|
// First, generate the replacement variables.
|
||||||
|
$keys = array_keys($rowData);
|
||||||
|
$values = array_values($rowData);
|
||||||
|
foreach ($keys as $keyKey => $keyValue)
|
||||||
|
$keys[$keyKey] = '{' . strtoupper($keyValue) . '}';
|
||||||
|
|
||||||
|
$value = '<a href="' . str_replace($keys, $values, $options['link']) . '">' . $value . '</a>';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ class Guest extends User
|
|||||||
$this->is_guest = true;
|
$this->is_guest = true;
|
||||||
$this->is_admin = false;
|
$this->is_admin = false;
|
||||||
$this->first_name = 'Guest';
|
$this->first_name = 'Guest';
|
||||||
$this->last_name = '';
|
$this->surname = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function updateAccessTime()
|
public function updateAccessTime()
|
||||||
|
|||||||
312
models/Image.php
312
models/Image.php
@@ -22,7 +22,7 @@ class Image extends Asset
|
|||||||
{
|
{
|
||||||
$asset = parent::fromId($id_asset, 'array');
|
$asset = parent::fromId($id_asset, 'array');
|
||||||
if ($asset)
|
if ($asset)
|
||||||
return $return_format == 'object' ? new Image($asset) : $asset;
|
return $return_format === 'object' ? new Image($asset) : $asset;
|
||||||
else
|
else
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -34,7 +34,7 @@ class Image extends Asset
|
|||||||
|
|
||||||
$assets = parent::fromIds($id_assets, 'array');
|
$assets = parent::fromIds($id_assets, 'array');
|
||||||
|
|
||||||
if ($return_format == 'array')
|
if ($return_format === 'array')
|
||||||
return $assets;
|
return $assets;
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -67,14 +67,33 @@ class Image extends Asset
|
|||||||
return EXIF::fromFile($this->getPath());
|
return EXIF::fromFile($this->getPath());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getPath()
|
public function getImageUrls($width = null, $height = null)
|
||||||
{
|
{
|
||||||
return ASSETSDIR . '/' . $this->subdir . '/' . $this->filename;
|
$image_urls = [];
|
||||||
|
if (isset($width) || isset($height))
|
||||||
|
{
|
||||||
|
$thumbnail = new Thumbnail($this);
|
||||||
|
$image_urls[1] = $this->getThumbnailUrl($width, $height, false);
|
||||||
|
|
||||||
|
// Can we afford to generate double-density thumbnails as well?
|
||||||
|
if ((!isset($width) || $this->image_width >= $width * 2) &&
|
||||||
|
(!isset($height) || $this->image_height >= $height * 2))
|
||||||
|
$image_urls[2] = $this->getThumbnailUrl($width * 2, $height * 2, false);
|
||||||
|
else
|
||||||
|
$image_urls[2] = $this->getThumbnailUrl($this->image_width, $this->image_height, true);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
$image_urls[1] = $this->getUrl();
|
||||||
|
|
||||||
|
return $image_urls;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getUrl()
|
public function getInlineImage($width = null, $height = null, $className = 'inline-image')
|
||||||
{
|
{
|
||||||
return ASSETSURL . '/' . $this->subdir . '/' . $this->filename;
|
$image_urls = $this->getImageUrls($width, $height);
|
||||||
|
|
||||||
|
return '<img class="' . $className . '" src="' . $image_urls[1] . '" alt=""' .
|
||||||
|
(isset($image_urls[2]) ? ' srcset="' . $image_urls[2] . ' 2x"' : '') . '>';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -82,221 +101,17 @@ class Image extends Asset
|
|||||||
* @param height: height of the thumbnail.
|
* @param height: height of the thumbnail.
|
||||||
* @param crop: whether and how to crop original image to fit. [false|true|'top'|'center'|'bottom']
|
* @param crop: whether and how to crop original image to fit. [false|true|'top'|'center'|'bottom']
|
||||||
* @param fit: whether to fit the image to given boundaries [true], or use them merely as an estimation [false].
|
* @param fit: whether to fit the image to given boundaries [true], or use them merely as an estimation [false].
|
||||||
|
* @param generate: whether or not to generate a thumbnail if no existing file was found.
|
||||||
*/
|
*/
|
||||||
public function getThumbnailUrl($width, $height, $crop = true, $fit = true)
|
public function getThumbnailUrl($width, $height, $crop = true, $fit = true, $generate = false)
|
||||||
{
|
{
|
||||||
// First, assert the image's dimensions are properly known in the database.
|
$thumbnail = new Thumbnail($this);
|
||||||
if (!isset($this->image_height, $this->image_width))
|
return $thumbnail->getUrl($width, $height, $crop, $fit, $generate);
|
||||||
throw new UnexpectedValueException('Image width or height is undefined -- inconsistent database?');
|
|
||||||
|
|
||||||
// Inferring width or height?
|
|
||||||
if (!$height)
|
|
||||||
$height = ceil($width / $this->image_width * $this->image_height);
|
|
||||||
elseif (!$width)
|
|
||||||
$width = ceil($height / $this->image_height * $this->image_width);
|
|
||||||
|
|
||||||
// Inferring the height from the original image's ratio?
|
|
||||||
if (!$fit)
|
|
||||||
$height = floor($width / ($this->image_width / $this->image_height));
|
|
||||||
|
|
||||||
// Assert we have both, now...
|
|
||||||
if (empty($width) || empty($height))
|
|
||||||
throw new InvalidArgumentException('Expecting at least either width or height as argument.');
|
|
||||||
|
|
||||||
// If we're cropping, verify we're in the right mode.
|
|
||||||
if ($crop)
|
|
||||||
{
|
|
||||||
// If the original image's aspect ratio is much wider, take a slice instead.
|
|
||||||
if ($this->image_width / $this->image_height > $width / $height)
|
|
||||||
$crop = 'slice';
|
|
||||||
|
|
||||||
// We won't be cropping if the thumbnail is proportional to its original.
|
|
||||||
if (abs($width / $height - $this->image_width / $this->image_height) <= 0.05)
|
|
||||||
$crop = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do we have an exact crop boundary for these dimensions?
|
|
||||||
$crop_selector = "crop_{$width}x{$height}";
|
|
||||||
if (isset($this->meta[$crop_selector]))
|
|
||||||
$crop = 'exact';
|
|
||||||
|
|
||||||
// Now, do we need to suffix the filename?
|
|
||||||
if ($crop)
|
|
||||||
$suffix = '_c' . (is_string($crop) && $crop !== 'center' ? substr($crop, 0, 1) : '');
|
|
||||||
else
|
|
||||||
$suffix = '';
|
|
||||||
|
|
||||||
// Check whether we already resized this earlier.
|
|
||||||
$thumb_selector = "thumb_{$width}x{$height}{$suffix}";
|
|
||||||
if (isset($this->meta[$thumb_selector]) && file_exists(THUMBSDIR . '/' . $this->subdir . '/' . $this->meta[$thumb_selector]))
|
|
||||||
return THUMBSURL . '/' . $this->subdir . '/' . $this->meta[$thumb_selector];
|
|
||||||
|
|
||||||
// Do we have a custom thumbnail on file?
|
|
||||||
$custom_selector = "custom_{$width}x{$height}";
|
|
||||||
if (isset($this->meta[$custom_selector]))
|
|
||||||
{
|
|
||||||
if (file_exists(ASSETSDIR . '/' . $this->subdir . '/' . $this->meta[$custom_selector]))
|
|
||||||
{
|
|
||||||
// Copy the custom thumbail to the general thumbnail directory.
|
|
||||||
copy(ASSETSDIR . '/' . $this->subdir . '/' . $this->meta[$custom_selector],
|
|
||||||
THUMBSDIR . '/' . $this->subdir . '/' . $this->meta[$custom_selector]);
|
|
||||||
|
|
||||||
// Let's remember this for future reference.
|
|
||||||
$this->meta[$thumb_selector] = $this->meta[$custom_selector];
|
|
||||||
$this->save();
|
|
||||||
|
|
||||||
return THUMBSURL . '/' . $this->subdir . '/' . $this->meta[$custom_selector];
|
|
||||||
}
|
|
||||||
else
|
|
||||||
throw new UnexpectedValueException('Custom thumbnail expected, but missing in file system!');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Let's try some arcane stuff...
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (!class_exists('Imagick'))
|
|
||||||
throw new Exception("The PHP module 'imagick' appears to be disabled. Please enable it to use image resampling functions.");
|
|
||||||
|
|
||||||
$thumb = new Imagick(ASSETSDIR . '/' . $this->subdir . '/' . $this->filename);
|
|
||||||
|
|
||||||
// The image might have some orientation set through EXIF. Let's apply this first.
|
|
||||||
self::applyRotation($thumb);
|
|
||||||
|
|
||||||
// Just resizing? Easy peasy.
|
|
||||||
if (!$crop)
|
|
||||||
$thumb->resizeImage($width, $height, Imagick::FILTER_LANCZOS, 1);
|
|
||||||
// Cropping in the center?
|
|
||||||
elseif ($crop === true || $crop === 'center')
|
|
||||||
$thumb->cropThumbnailImage($width, $height);
|
|
||||||
// Exact cropping? We can do that.
|
|
||||||
elseif ($crop === 'exact')
|
|
||||||
{
|
|
||||||
list($crop_width, $crop_height, $crop_x_pos, $crop_y_pos) = explode(',', $this->meta[$crop_selector]);
|
|
||||||
$thumb->cropImage($crop_width, $crop_height, $crop_x_pos, $crop_y_pos);
|
|
||||||
$thumb->resizeImage($width, $height, Imagick::FILTER_LANCZOS, 1);
|
|
||||||
}
|
|
||||||
// Advanced cropping? Fun!
|
|
||||||
else
|
|
||||||
{
|
|
||||||
$size = $thumb->getImageGeometry();
|
|
||||||
|
|
||||||
// Taking a horizontal slice from the top or bottom of the original image?
|
|
||||||
if ($crop === 'top' || $crop === 'bottom')
|
|
||||||
{
|
|
||||||
$crop_width = $size['width'];
|
|
||||||
$crop_height = floor($size['width'] / $width * $height);
|
|
||||||
$target_x = 0;
|
|
||||||
$target_y = $crop === 'top' ? 0 : $size['height'] - $crop_height;
|
|
||||||
}
|
|
||||||
// Otherwise, we're taking a vertical slice from the centre.
|
|
||||||
else
|
|
||||||
{
|
|
||||||
$crop_width = floor($size['height'] / $height * $width);
|
|
||||||
$crop_height = $size['height'];
|
|
||||||
$target_x = floor(($size['width'] - $crop_width) / 2);
|
|
||||||
$target_y = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
$thumb->cropImage($crop_width, $crop_height, $target_x, $target_y);
|
|
||||||
$thumb->resizeImage($width, $height, Imagick::FILTER_LANCZOS, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// What sort of image is this? Fall back to PNG if we must.
|
|
||||||
switch ($thumb->getImageFormat())
|
|
||||||
{
|
|
||||||
case 'JPEG':
|
|
||||||
$ext = 'jpg';
|
|
||||||
$thumb->setImageCompressionQuality(60);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'GIF':
|
|
||||||
$ext = 'gif';
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'PNG':
|
|
||||||
default:
|
|
||||||
$thumb->setFormat('PNG');
|
|
||||||
$ext = 'png';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// So, how do we name this?
|
|
||||||
$thumbfilename = substr($this->filename, 0, strrpos($this->filename, '.')) . "_{$width}x{$height}{$suffix}.$ext";
|
|
||||||
|
|
||||||
// Ensure the thumbnail subdirectory exists.
|
|
||||||
if (!is_dir(THUMBSDIR . '/' . $this->subdir))
|
|
||||||
mkdir(THUMBSDIR . '/' . $this->subdir, 0755, true);
|
|
||||||
|
|
||||||
// Save it in a public spot.
|
|
||||||
$thumb->writeImage(THUMBSDIR . '/' . $this->subdir . '/' . $thumbfilename);
|
|
||||||
$thumb->clear();
|
|
||||||
$thumb->destroy();
|
|
||||||
}
|
|
||||||
// Blast! Curse your sudden but inevitable betrayal!
|
|
||||||
catch (ImagickException $e)
|
|
||||||
{
|
|
||||||
throw new Exception('ImageMagick error occurred while generating thumbnail. Output: ' . $e->getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Let's remember this for future reference.
|
|
||||||
$this->meta[$thumb_selector] = $thumbfilename;
|
|
||||||
$this->save();
|
|
||||||
|
|
||||||
// Ah yes, you wanted a URL, didn't you...
|
|
||||||
return THUMBSURL . '/' . $this->subdir . '/' . $this->meta[$thumb_selector];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function applyRotation(Imagick $image)
|
public function getId()
|
||||||
{
|
{
|
||||||
switch ($image->getImageOrientation())
|
return $this->id_asset;
|
||||||
{
|
|
||||||
// Clockwise rotation
|
|
||||||
case Imagick::ORIENTATION_RIGHTTOP:
|
|
||||||
$image->rotateImage("#000", 90);
|
|
||||||
break;
|
|
||||||
|
|
||||||
// Counter-clockwise rotation
|
|
||||||
case Imagick::ORIENTATION_LEFTBOTTOM:
|
|
||||||
$image->rotateImage("#000", 270);
|
|
||||||
break;
|
|
||||||
|
|
||||||
// Upside down?
|
|
||||||
case Imagick::ORIENTATION_BOTTOMRIGHT:
|
|
||||||
$image->rotateImage("#000", 180);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Having rotated the image, make sure the EXIF data is set properly.
|
|
||||||
$image->setImageOrientation(Imagick::ORIENTATION_TOPLEFT);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function bestColor()
|
|
||||||
{
|
|
||||||
// Save some computations if we can.
|
|
||||||
if (isset($this->meta['best_color']))
|
|
||||||
return $this->meta['best_color'];
|
|
||||||
|
|
||||||
// Find out what colour is most prominent.
|
|
||||||
$color = new BestColor($this);
|
|
||||||
$this->meta['best_color'] = $color->hex();
|
|
||||||
$this->save();
|
|
||||||
|
|
||||||
// There's your colour.
|
|
||||||
return $this->meta['best_color'];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function bestLabelColor()
|
|
||||||
{
|
|
||||||
// Save some computations if we can.
|
|
||||||
if (isset($this->meta['best_color_label']))
|
|
||||||
return $this->meta['best_color_label'];
|
|
||||||
|
|
||||||
// Find out what colour is most prominent.
|
|
||||||
$color = new BestColor($this);
|
|
||||||
$this->meta['best_color_label'] = $color->rgba();
|
|
||||||
$this->save();
|
|
||||||
|
|
||||||
// There's your colour.
|
|
||||||
return $this->meta['best_color_label'];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function width()
|
public function width()
|
||||||
@@ -309,37 +124,70 @@ class Image extends Asset
|
|||||||
return $this->image_height;
|
return $this->image_height;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function ratio()
|
||||||
|
{
|
||||||
|
return $this->image_width / $this->image_height;
|
||||||
|
}
|
||||||
|
|
||||||
public function isPanorama()
|
public function isPanorama()
|
||||||
{
|
{
|
||||||
return $this->image_width / $this->image_height > 2;
|
return $this->ratio() >= 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function isPortrait()
|
public function isPortrait()
|
||||||
{
|
{
|
||||||
return $this->image_width / $this->image_height < 1;
|
return $this->ratio() < 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function isLandscape()
|
public function isLandscape()
|
||||||
{
|
{
|
||||||
$ratio = $this->image_width / $this->image_height;
|
$ratio = $this->ratio();
|
||||||
return $ratio >= 1 && $ratio <= 2;
|
return $ratio >= 1 && $ratio <= 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getThumbnails()
|
||||||
|
{
|
||||||
|
return $this->thumbnails;
|
||||||
|
}
|
||||||
|
|
||||||
public function removeAllThumbnails()
|
public function removeAllThumbnails()
|
||||||
{
|
{
|
||||||
foreach ($this->meta as $key => $value)
|
foreach ($this->thumbnails as $key => $filename)
|
||||||
{
|
{
|
||||||
if (substr($key, 0, 6) !== 'thumb_')
|
$thumb_path = THUMBSDIR . '/' . $this->subdir . '/' . $filename;
|
||||||
continue;
|
|
||||||
|
|
||||||
$thumb_path = THUMBSDIR . '/' . $this->subdir . '/' . $value;
|
|
||||||
if (is_file($thumb_path))
|
if (is_file($thumb_path))
|
||||||
unlink($thumb_path);
|
unlink($thumb_path);
|
||||||
|
|
||||||
unset($this->meta[$key]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->saveMetaData();
|
return Registry::get('db')->query('
|
||||||
|
UPDATE assets_thumbs
|
||||||
|
SET filename = NULL
|
||||||
|
WHERE id_asset = {int:id_asset}',
|
||||||
|
['id_asset' => $this->id_asset]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeThumbnailsOfSize($width, $height)
|
||||||
|
{
|
||||||
|
foreach ($this->thumbnails as $key => $filename)
|
||||||
|
{
|
||||||
|
if (strpos($key, $width . 'x' . $height) !== 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
$thumb_path = THUMBSDIR . '/' . $this->subdir . '/' . $filename;
|
||||||
|
if (is_file($thumb_path))
|
||||||
|
unlink($thumb_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Registry::get('db')->query('
|
||||||
|
DELETE FROM assets_thumbs
|
||||||
|
WHERE id_asset = {int:id_asset} AND
|
||||||
|
width = {int:width} AND
|
||||||
|
height = {int:height}',
|
||||||
|
[
|
||||||
|
'height' => $height,
|
||||||
|
'id_asset' => $this->id_asset,
|
||||||
|
'width' => $width,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function replaceThumbnail($descriptor, $tmp_file)
|
public function replaceThumbnail($descriptor, $tmp_file)
|
||||||
@@ -347,7 +195,7 @@ class Image extends Asset
|
|||||||
if (!is_file($tmp_file))
|
if (!is_file($tmp_file))
|
||||||
return -1;
|
return -1;
|
||||||
|
|
||||||
if (!isset($this->meta[$descriptor]))
|
if (!isset($this->thumbnails[$descriptor]))
|
||||||
return -2;
|
return -2;
|
||||||
|
|
||||||
$image = new Imagick($tmp_file);
|
$image = new Imagick($tmp_file);
|
||||||
@@ -355,12 +203,12 @@ class Image extends Asset
|
|||||||
unset($image);
|
unset($image);
|
||||||
|
|
||||||
// Check whether dimensions match.
|
// Check whether dimensions match.
|
||||||
$test_descriptor = 'thumb_' . $d['width'] . 'x' . $d['height'];
|
$test_descriptor = $d['width'] . 'x' . $d['height'];
|
||||||
if ($descriptor !== $test_descriptor && strpos($descriptor, $test_descriptor . '_') === false)
|
if ($descriptor !== $test_descriptor && strpos($descriptor, $test_descriptor . '_') === false)
|
||||||
return -3;
|
return -3;
|
||||||
|
|
||||||
// Save the custom thumbnail in the assets directory.
|
// Save the custom thumbnail in the assets directory.
|
||||||
$destination = ASSETSDIR . '/' . $this->subdir . '/' . $this->meta[$descriptor];
|
$destination = ASSETSDIR . '/' . $this->subdir . '/' . $this->thumbnails[$descriptor];
|
||||||
if (file_exists($destination) && !is_writable($destination))
|
if (file_exists($destination) && !is_writable($destination))
|
||||||
return -4;
|
return -4;
|
||||||
|
|
||||||
@@ -368,7 +216,7 @@ class Image extends Asset
|
|||||||
return -5;
|
return -5;
|
||||||
|
|
||||||
// Copy it to the thumbnail directory, overwriting the automatically generated one, too.
|
// Copy it to the thumbnail directory, overwriting the automatically generated one, too.
|
||||||
$destination = THUMBSDIR . '/' . $this->subdir . '/' . $this->meta[$descriptor];
|
$destination = THUMBSDIR . '/' . $this->subdir . '/' . $this->thumbnails[$descriptor];
|
||||||
if (file_exists($destination) && !is_writable($destination))
|
if (file_exists($destination) && !is_writable($destination))
|
||||||
return -6;
|
return -6;
|
||||||
|
|
||||||
@@ -376,7 +224,7 @@ class Image extends Asset
|
|||||||
return -7;
|
return -7;
|
||||||
|
|
||||||
// A little bookkeeping
|
// A little bookkeeping
|
||||||
$this->meta['custom_' . $d['width'] . 'x' . $d['height']] = $this->meta[$descriptor];
|
$this->meta['custom_' . $d['width'] . 'x' . $d['height']] = $this->thumbnails[$descriptor];
|
||||||
$this->saveMetaData();
|
$this->saveMetaData();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
40
models/MainMenu.php
Normal file
40
models/MainMenu.php
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
/*****************************************************************************
|
||||||
|
* MainMenu.php
|
||||||
|
* Contains the main navigation logic.
|
||||||
|
*
|
||||||
|
* Kabuki CMS (C) 2013-2023, Aaron van Geffen
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
class MainMenu extends Menu
|
||||||
|
{
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->items = [
|
||||||
|
[
|
||||||
|
'uri' => '/',
|
||||||
|
'label' => 'Albums',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'uri' => '/people/',
|
||||||
|
'label' => 'People',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'uri' => '/timeline/',
|
||||||
|
'label' => 'Timeline',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($this->items as $i => $item)
|
||||||
|
{
|
||||||
|
if (isset($item['uri']))
|
||||||
|
$this->items[$i]['url'] = BASEURL . $item['uri'];
|
||||||
|
|
||||||
|
if (!isset($item['subs']))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
foreach ($item['subs'] as $j => $subitem)
|
||||||
|
$this->items[$i]['subs'][$j]['url'] = BASEURL . $subitem['uri'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -110,6 +110,9 @@ class Member extends User
|
|||||||
$this->is_admin = $value == 1 ? 1 : 0;
|
$this->is_admin = $value == 1 ? 1 : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$params = get_object_vars($this);
|
||||||
|
$params['is_admin'] = $this->is_admin ? 1 : 0;
|
||||||
|
|
||||||
return Registry::get('db')->query('
|
return Registry::get('db')->query('
|
||||||
UPDATE users
|
UPDATE users
|
||||||
SET
|
SET
|
||||||
@@ -120,7 +123,7 @@ class Member extends User
|
|||||||
password_hash = {string:password_hash},
|
password_hash = {string:password_hash},
|
||||||
is_admin = {int:is_admin}
|
is_admin = {int:is_admin}
|
||||||
WHERE id_user = {int:id_user}',
|
WHERE id_user = {int:id_user}',
|
||||||
get_object_vars($this));
|
$params);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -189,4 +192,15 @@ class Member extends User
|
|||||||
// We should probably phase out the use of this function, or refactor the access levels of member properties...
|
// We should probably phase out the use of this function, or refactor the access levels of member properties...
|
||||||
return get_object_vars($this);
|
return get_object_vars($this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function getMemberMap()
|
||||||
|
{
|
||||||
|
return Registry::get('db')->queryPair('
|
||||||
|
SELECT id_user, CONCAT(first_name, {string:blank}, surname) AS full_name
|
||||||
|
FROM users
|
||||||
|
ORDER BY first_name, surname',
|
||||||
|
[
|
||||||
|
'blank' => ' ',
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
17
models/Menu.php
Normal file
17
models/Menu.php
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
/*****************************************************************************
|
||||||
|
* Menu.php
|
||||||
|
* Contains all navigational menus.
|
||||||
|
*
|
||||||
|
* Kabuki CMS (C) 2013-2023, Aaron van Geffen
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
abstract class Menu
|
||||||
|
{
|
||||||
|
protected $items = [];
|
||||||
|
|
||||||
|
public function getItems()
|
||||||
|
{
|
||||||
|
return $this->items;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,26 +8,47 @@
|
|||||||
|
|
||||||
class PageIndex
|
class PageIndex
|
||||||
{
|
{
|
||||||
protected $page_index = [];
|
private $base_url;
|
||||||
protected $current_page = 0;
|
private $current_page = 1;
|
||||||
protected $items_per_page = 0;
|
private $index_class = 'pagination';
|
||||||
protected $needsPageIndex = false;
|
private $items_per_page = 0;
|
||||||
protected $num_pages = 0;
|
private $linkBuilder;
|
||||||
protected $recordCount = 0;
|
private $needsPageIndex = false;
|
||||||
protected $start = 0;
|
private $num_pages = 1;
|
||||||
protected $sort_order = null;
|
private $page_index = [];
|
||||||
protected $sort_direction = null;
|
private $page_slug = '%AMP%page=%PAGE%';
|
||||||
protected $base_url;
|
private $recordCount = 0;
|
||||||
protected $index_class = 'pagination';
|
private $sort_direction = null;
|
||||||
protected $page_slug = '%AMP%page=%PAGE%';
|
private $sort_order = null;
|
||||||
|
private $start = 0;
|
||||||
|
|
||||||
public function __construct($options)
|
public function __construct($options)
|
||||||
{
|
{
|
||||||
foreach ($options as $key => $value)
|
static $neededKeys = ['base_url', 'items_per_page', 'recordCount'];
|
||||||
$this->$key = $value;
|
foreach ($neededKeys as $key)
|
||||||
|
{
|
||||||
|
if (!isset($options[$key]))
|
||||||
|
throw new Exception('PageIndex: argument ' . $key . ' missing in options');
|
||||||
|
|
||||||
|
$this->$key = $options[$key];
|
||||||
|
}
|
||||||
|
|
||||||
|
static $optionalKeys = ['index_class', 'linkBuilder', 'page_slug', 'sort_direction', 'sort_order', 'start'];
|
||||||
|
foreach ($optionalKeys as $key)
|
||||||
|
if (isset($options[$key]))
|
||||||
|
$this->$key = $options[$key];
|
||||||
|
|
||||||
$this->generatePageIndex();
|
$this->generatePageIndex();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function buildLink($start = null, $order = null, $dir = null)
|
||||||
|
{
|
||||||
|
if (isset($this->linkBuilder))
|
||||||
|
return call_user_func($this->linkBuilder, $start, $order, $dir);
|
||||||
|
else
|
||||||
|
return $this->getLink($start, $order, $dir);
|
||||||
|
}
|
||||||
|
|
||||||
protected function generatePageIndex()
|
protected function generatePageIndex()
|
||||||
{
|
{
|
||||||
/*
|
/*
|
||||||
@@ -42,9 +63,9 @@ class PageIndex
|
|||||||
lower current/cont. pgs. center upper
|
lower current/cont. pgs. center upper
|
||||||
*/
|
*/
|
||||||
|
|
||||||
$this->num_pages = ceil($this->recordCount / $this->items_per_page);
|
$this->num_pages = max(1, ceil($this->recordCount / $this->items_per_page));
|
||||||
$this->current_page = min(ceil($this->start / $this->items_per_page) + 1, $this->num_pages);
|
$this->current_page = min(ceil($this->start / $this->items_per_page) + 1, $this->num_pages);
|
||||||
if ($this->num_pages == 0)
|
if ($this->num_pages <= 1)
|
||||||
{
|
{
|
||||||
$this->needsPageIndex = false;
|
$this->needsPageIndex = false;
|
||||||
return;
|
return;
|
||||||
@@ -68,7 +89,7 @@ class PageIndex
|
|||||||
$this->page_index[$p] = [
|
$this->page_index[$p] = [
|
||||||
'index' => $p,
|
'index' => $p,
|
||||||
'is_selected' => $this->current_page == $p,
|
'is_selected' => $this->current_page == $p,
|
||||||
'href'=> $this->getLink(($p - 1) * $this->items_per_page, $this->sort_order, $this->sort_direction),
|
'href'=> $this->buildLink(($p - 1) * $this->items_per_page, $this->sort_order, $this->sort_direction),
|
||||||
];
|
];
|
||||||
|
|
||||||
// The center of the page index.
|
// The center of the page index.
|
||||||
@@ -81,7 +102,7 @@ class PageIndex
|
|||||||
$this->page_index[$center] = [
|
$this->page_index[$center] = [
|
||||||
'index' => $center,
|
'index' => $center,
|
||||||
'is_selected' => $this->current_page == $center,
|
'is_selected' => $this->current_page == $center,
|
||||||
'href'=> $this->getLink(($center - 1) * $this->items_per_page, $this->sort_order, $this->sort_direction),
|
'href'=> $this->buildLink(($center - 1) * $this->items_per_page, $this->sort_order, $this->sort_direction),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,7 +115,7 @@ class PageIndex
|
|||||||
$this->page_index[$p] = [
|
$this->page_index[$p] = [
|
||||||
'index' => $p,
|
'index' => $p,
|
||||||
'is_selected' => $this->current_page == $p,
|
'is_selected' => $this->current_page == $p,
|
||||||
'href'=> $this->getLink(($p - 1) * $this->items_per_page, $this->sort_order, $this->sort_direction),
|
'href'=> $this->buildLink(($p - 1) * $this->items_per_page, $this->sort_order, $this->sort_direction),
|
||||||
];
|
];
|
||||||
|
|
||||||
// The center of the page index.
|
// The center of the page index.
|
||||||
@@ -107,7 +128,7 @@ class PageIndex
|
|||||||
$this->page_index[$center] = [
|
$this->page_index[$center] = [
|
||||||
'index' => $center,
|
'index' => $center,
|
||||||
'is_selected' => $this->current_page == $center,
|
'is_selected' => $this->current_page == $center,
|
||||||
'href'=> $this->getLink(($center - 1) * $this->items_per_page, $this->sort_order, $this->sort_direction),
|
'href'=> $this->buildLink(($center - 1) * $this->items_per_page, $this->sort_order, $this->sort_direction),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,7 +141,7 @@ class PageIndex
|
|||||||
$this->page_index[$p] = [
|
$this->page_index[$p] = [
|
||||||
'index' => $p,
|
'index' => $p,
|
||||||
'is_selected' => $this->current_page == $p,
|
'is_selected' => $this->current_page == $p,
|
||||||
'href'=> $this->getLink(($p - 1) * $this->items_per_page, $this->sort_order, $this->sort_direction),
|
'href'=> $this->buildLink(($p - 1) * $this->items_per_page, $this->sort_order, $this->sort_direction),
|
||||||
];
|
];
|
||||||
|
|
||||||
// Previous page?
|
// Previous page?
|
||||||
@@ -157,11 +178,6 @@ class PageIndex
|
|||||||
return $url;
|
return $url;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getArray()
|
|
||||||
{
|
|
||||||
return $this->page_index;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getPageIndex()
|
public function getPageIndex()
|
||||||
{
|
{
|
||||||
return $this->page_index;
|
return $this->page_index;
|
||||||
|
|||||||
76
models/PhotoAlbum.php
Normal file
76
models/PhotoAlbum.php
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
<?php
|
||||||
|
/*****************************************************************************
|
||||||
|
* PhotoAlbum.php
|
||||||
|
* Contains key class PhotoAlbum.
|
||||||
|
*
|
||||||
|
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
class PhotoAlbum extends Tag
|
||||||
|
{
|
||||||
|
public static function getHierarchy($order, $direction)
|
||||||
|
{
|
||||||
|
$db = Registry::get('db');
|
||||||
|
$res = $db->query('
|
||||||
|
SELECT *
|
||||||
|
FROM tags
|
||||||
|
WHERE kind = {string:album}
|
||||||
|
ORDER BY id_parent, {raw:order}',
|
||||||
|
[
|
||||||
|
'order' => $order . ($direction == 'up' ? ' ASC' : ' DESC'),
|
||||||
|
'album' => 'Album',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$albums_by_parent = [];
|
||||||
|
while ($row = $db->fetch_assoc($res))
|
||||||
|
{
|
||||||
|
if (!isset($albums_by_parent[$row['id_parent']]))
|
||||||
|
$albums_by_parent[$row['id_parent']] = [];
|
||||||
|
|
||||||
|
$albums_by_parent[$row['id_parent']][] = $row + ['children' => []];
|
||||||
|
}
|
||||||
|
|
||||||
|
$albums = self::getChildrenRecursively(0, 0, $albums_by_parent);
|
||||||
|
$rows = self::flattenChildrenRecursively($albums);
|
||||||
|
|
||||||
|
return $rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function getChildrenRecursively($id_parent, $level, &$albums_by_parent)
|
||||||
|
{
|
||||||
|
$children = [];
|
||||||
|
if (!isset($albums_by_parent[$id_parent]))
|
||||||
|
return $children;
|
||||||
|
|
||||||
|
foreach ($albums_by_parent[$id_parent] as $child)
|
||||||
|
{
|
||||||
|
if (isset($albums_by_parent[$child['id_tag']]))
|
||||||
|
$child['children'] = self::getChildrenRecursively($child['id_tag'], $level + 1, $albums_by_parent);
|
||||||
|
|
||||||
|
$child['tag'] = ($level ? str_repeat('—', $level * 2) . ' ' : '') . $child['tag'];
|
||||||
|
$children[] = $child;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $children;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function flattenChildrenRecursively($albums)
|
||||||
|
{
|
||||||
|
if (empty($albums))
|
||||||
|
return [];
|
||||||
|
|
||||||
|
$rows = [];
|
||||||
|
foreach ($albums as $album)
|
||||||
|
{
|
||||||
|
$rows[] = array_intersect_key($album, array_flip(['id_tag', 'tag', 'slug', 'count']));
|
||||||
|
if (!empty($album['children']))
|
||||||
|
{
|
||||||
|
$children = self::flattenChildrenRecursively($album['children']);
|
||||||
|
foreach ($children as $child)
|
||||||
|
$rows[] = array_intersect_key($child, array_flip(['id_tag', 'tag', 'slug', 'count']));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $rows;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ class PhotoMosaic
|
|||||||
private $queue = [];
|
private $queue = [];
|
||||||
|
|
||||||
const NUM_DAYS_CUTOFF = 7;
|
const NUM_DAYS_CUTOFF = 7;
|
||||||
|
private AssetIterator $iterator;
|
||||||
|
|
||||||
public function __construct(AssetIterator $iterator)
|
public function __construct(AssetIterator $iterator)
|
||||||
{
|
{
|
||||||
@@ -78,7 +79,7 @@ class PhotoMosaic
|
|||||||
return -$priority_diff;
|
return -$priority_diff;
|
||||||
|
|
||||||
// In other cases, we'll just show the newest first.
|
// In other cases, we'll just show the newest first.
|
||||||
return $a->getDateCaptured() > $b->getDateCaptured() ? -1 : 1;
|
return $a->getDateCaptured() <=> $b->getDateCaptured();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function daysApart(Image $a, Image $b)
|
private static function daysApart(Image $a, Image $b)
|
||||||
@@ -113,7 +114,7 @@ class PhotoMosaic
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Sort photos by priority and date captured.
|
// Sort photos by priority and date captured.
|
||||||
usort($photos, 'self::orderPhotos');
|
usort($photos, self::orderPhotos(...));
|
||||||
|
|
||||||
// Three portraits?
|
// Three portraits?
|
||||||
if ($num_portrait === 3)
|
if ($num_portrait === 3)
|
||||||
@@ -164,7 +165,10 @@ class PhotoMosaic
|
|||||||
return [$photos, 'single'];
|
return [$photos, 'single'];
|
||||||
}
|
}
|
||||||
|
|
||||||
// A boring set it is, then.
|
// Last resort: majority vote
|
||||||
return [$photos, 'row'];
|
if ($num_portrait > $num_landscape)
|
||||||
|
return [$photos, 'portraits'];
|
||||||
|
else
|
||||||
|
return [$photos, 'landscapes'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
78
models/Router.php
Normal file
78
models/Router.php
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
<?php
|
||||||
|
/*****************************************************************************
|
||||||
|
* Router.php
|
||||||
|
* Contains key class Router.
|
||||||
|
*
|
||||||
|
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
class Router
|
||||||
|
{
|
||||||
|
public static function route()
|
||||||
|
{
|
||||||
|
$possibleActions = [
|
||||||
|
'accountsettings' => 'AccountSettings',
|
||||||
|
'addalbum' => 'EditAlbum',
|
||||||
|
'albums' => 'ViewPhotoAlbums',
|
||||||
|
'editalbum' => 'EditAlbum',
|
||||||
|
'editasset' => 'EditAsset',
|
||||||
|
'edittag' => 'EditTag',
|
||||||
|
'edituser' => 'EditUser',
|
||||||
|
'login' => 'Login',
|
||||||
|
'logout' => 'Logout',
|
||||||
|
'managealbums' => 'ManageAlbums',
|
||||||
|
'manageassets' => 'ManageAssets',
|
||||||
|
'manageerrors' => 'ManageErrors',
|
||||||
|
'managetags' => 'ManageTags',
|
||||||
|
'manageusers' => 'ManageUsers',
|
||||||
|
'people' => 'ViewPeople',
|
||||||
|
'resetpassword' => 'ResetPassword',
|
||||||
|
'suggest' => 'ProvideAutoSuggest',
|
||||||
|
'timeline' => 'ViewTimeline',
|
||||||
|
'uploadmedia' => 'UploadMedia',
|
||||||
|
'download' => 'Download',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Work around PHP's FPM not always providing PATH_INFO.
|
||||||
|
if (empty($_SERVER['PATH_INFO']) && isset($_SERVER['REQUEST_URI']))
|
||||||
|
{
|
||||||
|
if (strpos($_SERVER['REQUEST_URI'], '?') === false)
|
||||||
|
$_SERVER['PATH_INFO'] = $_SERVER['REQUEST_URI'];
|
||||||
|
else
|
||||||
|
$_SERVER['PATH_INFO'] = substr($_SERVER['REQUEST_URI'], 0, strpos($_SERVER['REQUEST_URI'], '?'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Just showing the album index?
|
||||||
|
if (empty($_SERVER['PATH_INFO']) || $_SERVER['PATH_INFO'] == '/')
|
||||||
|
{
|
||||||
|
return new ViewPhotoAlbum();
|
||||||
|
}
|
||||||
|
// Asynchronously generating thumbnails?
|
||||||
|
elseif (preg_match('~^/thumbnail/(?<id>\d+)/(?<width>\d+)x(?<height>\d+)(?:_(?<mode>c(t|b|s|)))?/?~', $_SERVER['PATH_INFO'], $path))
|
||||||
|
{
|
||||||
|
$_GET = array_merge($_GET, $path);
|
||||||
|
return new GenerateThumbnail();
|
||||||
|
}
|
||||||
|
// Look for particular actions...
|
||||||
|
elseif (preg_match('~^/(?<action>[a-z]+)(?:/page/(?<page>\d+))?/?~', $_SERVER['PATH_INFO'], $path) && isset($possibleActions[$path['action']]))
|
||||||
|
{
|
||||||
|
$_GET = array_merge($_GET, $path);
|
||||||
|
return new $possibleActions[$path['action']]();
|
||||||
|
}
|
||||||
|
// An album, person, or any other tag?
|
||||||
|
elseif (preg_match('~^/(?<tag>.+?)(?:/page/(?<page>\d+))?/?$~', $_SERVER['PATH_INFO'], $path) && Tag::matchSlug($path['tag']))
|
||||||
|
{
|
||||||
|
$_GET = array_merge($_GET, $path);
|
||||||
|
return new ViewPhotoAlbum();
|
||||||
|
}
|
||||||
|
// A photo for sure, then, right?
|
||||||
|
elseif (preg_match('~^/(?<slug>.+?)/?$~', $_SERVER['PATH_INFO'], $path))
|
||||||
|
{
|
||||||
|
$_GET = array_merge($_GET, $path);
|
||||||
|
return new ViewPhoto();
|
||||||
|
}
|
||||||
|
// No idea, then?
|
||||||
|
else
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,13 +19,13 @@ class Session
|
|||||||
if (!isset($_SERVER['HTTPS']) && isset($_SERVER['REMOTE_ADDR']) && $_SESSION['ip_address'] !== $_SERVER['REMOTE_ADDR'])
|
if (!isset($_SERVER['HTTPS']) && isset($_SERVER['REMOTE_ADDR']) && $_SESSION['ip_address'] !== $_SERVER['REMOTE_ADDR'])
|
||||||
{
|
{
|
||||||
$_SESSION = [];
|
$_SESSION = [];
|
||||||
throw new UserFacingException('Your session failed to validate: your IP address has changed. Please re-login and try again.');
|
Dispatcher::kickGuest('Your session failed to validate', 'Your IP address has changed. Please re-login and try again.');
|
||||||
}
|
}
|
||||||
// Either way, require re-login if the browser identifier has changed.
|
// Either way, require re-login if the browser identifier has changed.
|
||||||
elseif (isset($_SERVER['HTTP_USER_AGENT']) && $_SESSION['user_agent'] !== $_SERVER['HTTP_USER_AGENT'])
|
elseif (isset($_SERVER['HTTP_USER_AGENT']) && $_SESSION['user_agent'] !== $_SERVER['HTTP_USER_AGENT'])
|
||||||
{
|
{
|
||||||
$_SESSION = [];
|
$_SESSION = [];
|
||||||
throw new UserFacingException('Your session failed to validate: your browser identifier has changed. Please re-login and try again.');
|
Dispatcher::kickGuest('Your session failed to validate', 'Your browser identifier has changed. Please re-login and try again.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
elseif (!isset($_SESSION['ip_address'], $_SESSION['user_agent']))
|
elseif (!isset($_SESSION['ip_address'], $_SESSION['user_agent']))
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ class Tag
|
|||||||
public $id_tag;
|
public $id_tag;
|
||||||
public $id_parent;
|
public $id_parent;
|
||||||
public $id_asset_thumb;
|
public $id_asset_thumb;
|
||||||
|
public $id_user_owner;
|
||||||
public $tag;
|
public $tag;
|
||||||
public $slug;
|
public $slug;
|
||||||
public $description;
|
public $description;
|
||||||
@@ -39,7 +40,7 @@ class Tag
|
|||||||
if (empty($row))
|
if (empty($row))
|
||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
|
|
||||||
return $return_format == 'object' ? new Tag($row) : $row;
|
return $return_format === 'object' ? new Tag($row) : $row;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function fromSlug($slug, $return_format = 'object')
|
public static function fromSlug($slug, $return_format = 'object')
|
||||||
@@ -58,7 +59,7 @@ class Tag
|
|||||||
if (empty($row))
|
if (empty($row))
|
||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
|
|
||||||
return $return_format == 'object' ? new Tag($row) : $row;
|
return $return_format === 'object' ? new Tag($row) : $row;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function getAll($limit = 0, $return_format = 'array')
|
public static function getAll($limit = 0, $return_format = 'array')
|
||||||
@@ -84,7 +85,7 @@ class Tag
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($return_format == 'object')
|
if ($return_format === 'object')
|
||||||
{
|
{
|
||||||
$return = [];
|
$return = [];
|
||||||
foreach ($rows as $row)
|
foreach ($rows as $row)
|
||||||
@@ -95,6 +96,25 @@ class Tag
|
|||||||
return $rows;
|
return $rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function getAllByOwner($id_user_owner)
|
||||||
|
{
|
||||||
|
$db = Registry::get('db');
|
||||||
|
$res = $db->query('
|
||||||
|
SELECT *
|
||||||
|
FROM tags
|
||||||
|
WHERE id_user_owner = {int:id_user_owner}
|
||||||
|
ORDER BY tag',
|
||||||
|
[
|
||||||
|
'id_user_owner' => $id_user_owner,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$objects = [];
|
||||||
|
while ($row = $db->fetch_assoc($res))
|
||||||
|
$objects[$row['id_tag']] = new Tag($row);
|
||||||
|
|
||||||
|
return $objects;
|
||||||
|
}
|
||||||
|
|
||||||
public static function getAlbums($id_parent = 0, $offset = 0, $limit = 24, $return_format = 'array')
|
public static function getAlbums($id_parent = 0, $offset = 0, $limit = 24, $return_format = 'array')
|
||||||
{
|
{
|
||||||
$rows = Registry::get('db')->queryAssocs('
|
$rows = Registry::get('db')->queryAssocs('
|
||||||
@@ -110,7 +130,7 @@ class Tag
|
|||||||
'limit' => $limit,
|
'limit' => $limit,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if ($return_format == 'object')
|
if ($return_format === 'object')
|
||||||
{
|
{
|
||||||
$return = [];
|
$return = [];
|
||||||
foreach ($rows as $row)
|
foreach ($rows as $row)
|
||||||
@@ -136,7 +156,7 @@ class Tag
|
|||||||
'limit' => $limit,
|
'limit' => $limit,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if ($return_format == 'object')
|
if ($return_format === 'object')
|
||||||
{
|
{
|
||||||
$return = [];
|
$return = [];
|
||||||
foreach ($rows as $row)
|
foreach ($rows as $row)
|
||||||
@@ -166,7 +186,7 @@ class Tag
|
|||||||
if (empty($rows))
|
if (empty($rows))
|
||||||
return [];
|
return [];
|
||||||
|
|
||||||
if ($return_format == 'object')
|
if ($return_format === 'object')
|
||||||
{
|
{
|
||||||
$return = [];
|
$return = [];
|
||||||
foreach ($rows as $row)
|
foreach ($rows as $row)
|
||||||
@@ -196,7 +216,7 @@ class Tag
|
|||||||
if (empty($rows))
|
if (empty($rows))
|
||||||
return [];
|
return [];
|
||||||
|
|
||||||
if ($return_format == 'object')
|
if ($return_format === 'object')
|
||||||
{
|
{
|
||||||
$return = [];
|
$return = [];
|
||||||
foreach ($rows as $row)
|
foreach ($rows as $row)
|
||||||
@@ -244,7 +264,7 @@ class Tag
|
|||||||
trigger_error('Could not create the requested tag.', E_USER_ERROR);
|
trigger_error('Could not create the requested tag.', E_USER_ERROR);
|
||||||
|
|
||||||
$data['id_tag'] = $db->insert_id();
|
$data['id_tag'] = $db->insert_id();
|
||||||
return $return_format == 'object' ? new Tag($data) : $data;
|
return $return_format === 'object' ? new Tag($data) : $data;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getUrl()
|
public function getUrl()
|
||||||
@@ -258,8 +278,11 @@ class Tag
|
|||||||
UPDATE tags
|
UPDATE tags
|
||||||
SET
|
SET
|
||||||
id_parent = {int:id_parent},
|
id_parent = {int:id_parent},
|
||||||
id_asset_thumb = {int:id_asset_thumb},
|
id_asset_thumb = {int:id_asset_thumb},' . (isset($this->id_user_owner) ? '
|
||||||
|
id_user_owner = {int:id_user_owner},' : '') . '
|
||||||
tag = {string:tag},
|
tag = {string:tag},
|
||||||
|
slug = {string:slug},
|
||||||
|
description = {string:description},
|
||||||
count = {int:count}
|
count = {int:count}
|
||||||
WHERE id_tag = {int:id_tag}',
|
WHERE id_tag = {int:id_tag}',
|
||||||
get_object_vars($this));
|
get_object_vars($this));
|
||||||
@@ -270,7 +293,7 @@ class Tag
|
|||||||
$db = Registry::get('db');
|
$db = Registry::get('db');
|
||||||
|
|
||||||
$res = $db->query('
|
$res = $db->query('
|
||||||
DELETE FROM posts_tags
|
DELETE FROM assets_tags
|
||||||
WHERE id_tag = {int:id_tag}',
|
WHERE id_tag = {int:id_tag}',
|
||||||
[
|
[
|
||||||
'id_tag' => $this->id_tag,
|
'id_tag' => $this->id_tag,
|
||||||
@@ -287,6 +310,27 @@ class Tag
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function resetIdAsset()
|
||||||
|
{
|
||||||
|
$db = Registry::get('db');
|
||||||
|
$new_id = $db->queryValue('
|
||||||
|
SELECT MAX(id_asset) as new_id
|
||||||
|
FROM assets_tags
|
||||||
|
WHERE id_tag = {int:id_tag}',
|
||||||
|
[
|
||||||
|
'id_tag' => $this->id_tag,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $db->query('
|
||||||
|
UPDATE tags
|
||||||
|
SET id_asset_thumb = {int:new_id}
|
||||||
|
WHERE id_tag = {int:id_tag}',
|
||||||
|
[
|
||||||
|
'new_id' => $new_id ?? 0,
|
||||||
|
'id_tag' => $this->id_tag,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
public static function match($tokens)
|
public static function match($tokens)
|
||||||
{
|
{
|
||||||
if (!is_array($tokens))
|
if (!is_array($tokens))
|
||||||
@@ -305,8 +349,8 @@ class Tag
|
|||||||
if (!is_array($tokens))
|
if (!is_array($tokens))
|
||||||
$tokens = explode(' ', $tokens);
|
$tokens = explode(' ', $tokens);
|
||||||
|
|
||||||
return Registry::get('db')->queryPair('
|
return Registry::get('db')->queryPairs('
|
||||||
SELECT id_tag, tag
|
SELECT id_tag, tag, slug
|
||||||
FROM tags
|
FROM tags
|
||||||
WHERE LOWER(tag) LIKE {string:tokens} AND
|
WHERE LOWER(tag) LIKE {string:tokens} AND
|
||||||
kind = {string:person}
|
kind = {string:person}
|
||||||
|
|||||||
359
models/Thumbnail.php
Normal file
359
models/Thumbnail.php
Normal file
@@ -0,0 +1,359 @@
|
|||||||
|
<?php
|
||||||
|
/*****************************************************************************
|
||||||
|
* Thumbnail.php
|
||||||
|
* Contains key class Thumbnail.
|
||||||
|
*
|
||||||
|
* Kabuki CMS (C) 2013-2020, Aaron van Geffen
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
class Thumbnail
|
||||||
|
{
|
||||||
|
private $image;
|
||||||
|
private $image_meta;
|
||||||
|
private $thumbnails;
|
||||||
|
|
||||||
|
private $properly_initialised;
|
||||||
|
private $width;
|
||||||
|
private $height;
|
||||||
|
private $crop_mode;
|
||||||
|
private string $filename_suffix;
|
||||||
|
|
||||||
|
const CROP_MODE_NONE = 0;
|
||||||
|
const CROP_MODE_BOUNDARY = 1;
|
||||||
|
const CROP_MODE_CUSTOM_FILE = 2;
|
||||||
|
const CROP_MODE_SLICE_TOP = 3;
|
||||||
|
const CROP_MODE_SLICE_CENTRE = 4;
|
||||||
|
const CROP_MODE_SLICE_BOTTOM = 5;
|
||||||
|
|
||||||
|
public function __construct(Image $image)
|
||||||
|
{
|
||||||
|
$this->image = $image;
|
||||||
|
$this->image_meta = $image->getMeta();
|
||||||
|
$this->thumbnails = $image->getThumbnails();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param width: width of the thumbnail.
|
||||||
|
* @param height: height of the thumbnail.
|
||||||
|
* @param crop: whether and how to crop original image to fit. [false|true|'top'|'center'|'bottom']
|
||||||
|
* @param fit: whether to fit the image to given boundaries [true], or use them merely as an estimate [false].
|
||||||
|
* @param generate: whether or not to generate a thumbnail if no existing file was found.
|
||||||
|
*/
|
||||||
|
public function getUrl($width, $height, $crop = true, $fit = true, $generate = false)
|
||||||
|
{
|
||||||
|
$this->init($width, $height, $crop, $fit);
|
||||||
|
|
||||||
|
// Check whether we've already resized this earlier.
|
||||||
|
$thumb_selector = $this->width . 'x' . $this->height . $this->filename_suffix;
|
||||||
|
if (!empty($this->thumbnails[$thumb_selector]))
|
||||||
|
{
|
||||||
|
$thumb_filename = $this->image->getSubdir() . '/' . $this->thumbnails[$thumb_selector];
|
||||||
|
if (file_exists(THUMBSDIR . '/' . $thumb_filename))
|
||||||
|
return THUMBSURL . '/' . $thumb_filename;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do we have a custom thumbnail on file?
|
||||||
|
$custom_selector = 'custom_' . $this->width . 'x' . $this->height;
|
||||||
|
if (isset($this->image_meta[$custom_selector]))
|
||||||
|
{
|
||||||
|
$custom_filename = $this->image->getSubdir() . '/' . $this->image_meta[$custom_selector];
|
||||||
|
if (file_exists(ASSETSDIR . '/' . $custom_filename))
|
||||||
|
{
|
||||||
|
// Copy the custom thumbail to the general thumbnail directory.
|
||||||
|
copy(ASSETSDIR . '/' . $custom_filename, THUMBSDIR . '/' . $custom_filename);
|
||||||
|
|
||||||
|
// Let's remember this for future reference.
|
||||||
|
$this->markAsGenerated($this->image_meta[$custom_selector]);
|
||||||
|
|
||||||
|
return THUMBSURL . '/' . $custom_filename;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
throw new UnexpectedValueException('Custom thumbnail expected, but missing in file system!');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is this the right moment to generate a thumbnail, then?
|
||||||
|
if ($generate)
|
||||||
|
{
|
||||||
|
if (array_key_exists($thumb_selector, $this->thumbnails))
|
||||||
|
return $this->generate();
|
||||||
|
else
|
||||||
|
throw new Exception("Trying to generate a thumbnail not previously queued by the system\n" .
|
||||||
|
print_r(func_get_args(), true));
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not, queue it for generation at another time, and return a URL to generate it with.
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$this->markAsQueued();
|
||||||
|
return BASEURL . '/thumbnail/' . $this->image->getId() . '/' . $thumb_selector . '/';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param width: width of the thumbnail.
|
||||||
|
* @param height: height of the thumbnail.
|
||||||
|
* @param crop: whether and how to crop original image to fit. [false|true|'top'|'center'|'bottom']
|
||||||
|
* @param fit: whether to fit the image to given boundaries [true], or use them merely as an estimate [false].
|
||||||
|
*/
|
||||||
|
private function init($width, $height, $crop = true, $fit = true)
|
||||||
|
{
|
||||||
|
$this->properly_initialised = false;
|
||||||
|
|
||||||
|
// First, assert the image's dimensions are properly known in the database.
|
||||||
|
if ($this->image->width() === null || $this->image->height() === null)
|
||||||
|
throw new UnexpectedValueException('Image width or height is undefined -- inconsistent database?');
|
||||||
|
|
||||||
|
$this->width = $width;
|
||||||
|
$this->height = $height;
|
||||||
|
|
||||||
|
// Inferring width or height?
|
||||||
|
if (!$this->height)
|
||||||
|
$this->height = ceil($this->width / $this->image->ratio());
|
||||||
|
elseif (!$this->width)
|
||||||
|
$this->width = ceil($this->height * $this->image->ratio());
|
||||||
|
|
||||||
|
// Inferring the height from the original image's ratio?
|
||||||
|
if (!$fit)
|
||||||
|
$this->height = floor($this->width / $this->image->ratio());
|
||||||
|
|
||||||
|
// Assert we have both, now...
|
||||||
|
if (empty($this->width) || empty($this->height))
|
||||||
|
throw new InvalidArgumentException('Expecting at least either width or height as argument.');
|
||||||
|
|
||||||
|
// If we're cropping, verify we're in the right mode.
|
||||||
|
if ($crop)
|
||||||
|
{
|
||||||
|
// Do we have an exact crop boundary set for these dimensions?
|
||||||
|
$crop_selector = 'crop_' . $this->width . 'x' . $this->height;
|
||||||
|
if (isset($this->image_meta[$crop_selector]))
|
||||||
|
$this->crop_mode = self::CROP_MODE_BOUNDARY;
|
||||||
|
|
||||||
|
// We won't be cropping if the thumbnail is proportional to its original.
|
||||||
|
elseif (abs($this->ratio() - $this->image->ratio()) <= 0.025)
|
||||||
|
$this->crop_mode = self::CROP_MODE_NONE;
|
||||||
|
|
||||||
|
// If the original image's aspect ratio is much wider, take a slice instead.
|
||||||
|
elseif ($this->image->ratio() > $this->ratio())
|
||||||
|
$this->crop_mode = self::CROP_MODE_SLICE_CENTRE;
|
||||||
|
|
||||||
|
// Slice from the top?
|
||||||
|
elseif ($crop === 'top' || $crop === 'ct')
|
||||||
|
$this->crop_mode = self::CROP_MODE_SLICE_TOP;
|
||||||
|
|
||||||
|
// Slice from the bottom?
|
||||||
|
elseif ($crop === 'bottom' || $crop === 'cb')
|
||||||
|
$this->crop_mode = self::CROP_MODE_SLICE_BOTTOM;
|
||||||
|
|
||||||
|
// Slice from the centre?
|
||||||
|
elseif ($crop === 'centre' || $crop === 'center' || $crop === 'cs' || $crop === true)
|
||||||
|
$this->crop_mode = self::CROP_MODE_SLICE_CENTRE;
|
||||||
|
|
||||||
|
// Unexpected value? Assume no crop.
|
||||||
|
else
|
||||||
|
$this->crop_mode = self::CROP_MODE_NONE;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
$this->crop_mode = self::CROP_MODE_NONE;
|
||||||
|
|
||||||
|
// Now, do we need to suffix the filename?
|
||||||
|
if ($this->crop_mode !== self::CROP_MODE_NONE)
|
||||||
|
{
|
||||||
|
$this->filename_suffix = '_c';
|
||||||
|
if ($this->crop_mode === self::CROP_MODE_SLICE_TOP)
|
||||||
|
$this->filename_suffix .= 't';
|
||||||
|
elseif ($this->crop_mode === self::CROP_MODE_SLICE_CENTRE)
|
||||||
|
$this->filename_suffix .= 's';
|
||||||
|
elseif ($this->crop_mode === self::CROP_MODE_SLICE_BOTTOM)
|
||||||
|
$this->filename_suffix .= 'b';
|
||||||
|
elseif ($this->crop_mode === self::CROP_MODE_BOUNDARY)
|
||||||
|
$this->filename_suffix .= 'e';
|
||||||
|
}
|
||||||
|
else
|
||||||
|
$this->filename_suffix = '';
|
||||||
|
|
||||||
|
$this->properly_initialised = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function generate()
|
||||||
|
{
|
||||||
|
if (!$this->properly_initialised)
|
||||||
|
throw new UnexpectedValueException('The thumbnail factory was not intialised before use!');
|
||||||
|
|
||||||
|
// Let's try some arcane stuff...
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!class_exists('Imagick'))
|
||||||
|
throw new Exception("The PHP module 'imagick' appears to be disabled. Please enable it to use image resampling functions.");
|
||||||
|
|
||||||
|
$thumb = new Imagick(ASSETSDIR . '/' . $this->image->getSubdir() . '/' . $this->image->getFilename());
|
||||||
|
|
||||||
|
// The image might have some orientation set through EXIF. Let's apply this first.
|
||||||
|
self::applyRotation($thumb);
|
||||||
|
|
||||||
|
// Just resizing? Easy peasy.
|
||||||
|
if ($this->crop_mode === self::CROP_MODE_NONE)
|
||||||
|
$thumb->resizeImage($this->width, $this->height, Imagick::FILTER_LANCZOS, 1);
|
||||||
|
|
||||||
|
// // Cropping in the center?
|
||||||
|
elseif ($this->crop_mode === self::CROP_MODE_SLICE_CENTRE)
|
||||||
|
$thumb->cropThumbnailImage($this->width, $this->height);
|
||||||
|
|
||||||
|
// Exact cropping? We can do that.
|
||||||
|
elseif ($this->crop_mode === self::CROP_MODE_BOUNDARY)
|
||||||
|
{
|
||||||
|
$crop_selector = 'crop_' . $this->width . 'x' . $this->height;
|
||||||
|
list($crop_width, $crop_height, $crop_x_pos, $crop_y_pos) = explode(',', $this->image_meta[$crop_selector]);
|
||||||
|
$thumb->cropImage($crop_width, $crop_height, $crop_x_pos, $crop_y_pos);
|
||||||
|
$thumb->resizeImage($this->width, $this->height, Imagick::FILTER_LANCZOS, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Advanced cropping? Fun!
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$size = $thumb->getImageGeometry();
|
||||||
|
|
||||||
|
// Taking a horizontal slice from the top or bottom of the original image?
|
||||||
|
if ($this->crop_mode === self::CROP_MODE_SLICE_TOP || $this->crop_mode === self::CROP_MODE_SLICE_BOTTOM)
|
||||||
|
{
|
||||||
|
$crop_width = $size['width'];
|
||||||
|
$crop_height = floor($size['width'] / $this->width * $this->height);
|
||||||
|
$target_x = 0;
|
||||||
|
$target_y = $this->crop_mode === self::CROP_MODE_SLICE_TOP ? 0 : $size['height'] - $crop_height;
|
||||||
|
}
|
||||||
|
// Otherwise, we're taking a vertical slice from the centre.
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$crop_width = floor($size['height'] / $this->height * $this->width);
|
||||||
|
$crop_height = $size['height'];
|
||||||
|
$target_x = floor(($size['width'] - $crop_width) / 2);
|
||||||
|
$target_y = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$thumb->cropImage($crop_width, $crop_height, $target_x, $target_y);
|
||||||
|
$thumb->resizeImage($this->width, $this->height, Imagick::FILTER_LANCZOS, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// What sort of image is this? Fall back to PNG if we must.
|
||||||
|
switch ($thumb->getImageFormat())
|
||||||
|
{
|
||||||
|
case 'JPEG':
|
||||||
|
$ext = 'jpg';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'GIF':
|
||||||
|
$ext = 'gif';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'PNG':
|
||||||
|
default:
|
||||||
|
$thumb->setFormat('PNG');
|
||||||
|
$ext = 'png';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// So, how do we name this?
|
||||||
|
$thumb_filename = substr($this->image->getFilename(), 0, strrpos($this->image->getFilename(), '.')) .
|
||||||
|
'_' . $this->width . 'x' . $this->height . $this->filename_suffix . '.' . $ext;
|
||||||
|
|
||||||
|
// Ensure the thumbnail subdirectory exists.
|
||||||
|
$target_dir = THUMBSDIR . '/' . $this->image->getSubdir();
|
||||||
|
if (!is_dir($target_dir))
|
||||||
|
mkdir($target_dir, 0755, true);
|
||||||
|
|
||||||
|
if (!is_writable($target_dir))
|
||||||
|
throw new Exception('Thumbnail directory is not writable!');
|
||||||
|
|
||||||
|
// No need to preserve every detail.
|
||||||
|
$thumb->setImageCompressionQuality(80);
|
||||||
|
|
||||||
|
// Save it in a public spot.
|
||||||
|
$thumb->writeImage($target_dir . '/' . $thumb_filename);
|
||||||
|
|
||||||
|
// Let's remember this for future reference...
|
||||||
|
$this->markAsGenerated($thumb_filename);
|
||||||
|
|
||||||
|
$thumb->clear();
|
||||||
|
$thumb->destroy();
|
||||||
|
|
||||||
|
// Finally, return the URL for the generated thumbnail image.
|
||||||
|
return THUMBSURL . '/' . $this->image->getSubdir() . '/' . $thumb_filename;
|
||||||
|
}
|
||||||
|
catch (ImagickException $e)
|
||||||
|
{
|
||||||
|
throw new Exception('ImageMagick error occurred while generating thumbnail. Output: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function applyRotation(Imagick $image)
|
||||||
|
{
|
||||||
|
switch ($image->getImageOrientation())
|
||||||
|
{
|
||||||
|
// Clockwise rotation
|
||||||
|
case Imagick::ORIENTATION_RIGHTTOP:
|
||||||
|
$image->rotateImage("#000", 90);
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Counter-clockwise rotation
|
||||||
|
case Imagick::ORIENTATION_LEFTBOTTOM:
|
||||||
|
$image->rotateImage("#000", 270);
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Upside down?
|
||||||
|
case Imagick::ORIENTATION_BOTTOMRIGHT:
|
||||||
|
$image->rotateImage("#000", 180);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Having rotated the image, make sure the EXIF data is set properly.
|
||||||
|
$image->setImageOrientation(Imagick::ORIENTATION_TOPLEFT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function ratio()
|
||||||
|
{
|
||||||
|
return $this->width / $this->height;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function updateDb($filename)
|
||||||
|
{
|
||||||
|
if (!$this->properly_initialised)
|
||||||
|
throw new UnexpectedValueException('The thumbnail factory was not intialised before use!');
|
||||||
|
|
||||||
|
$mode = !empty($this->filename_suffix) ? substr($this->filename_suffix, 1) : '';
|
||||||
|
$success = Registry::get('db')->insert('replace', 'assets_thumbs', [
|
||||||
|
'id_asset' => 'int',
|
||||||
|
'width' => 'int',
|
||||||
|
'height' => 'int',
|
||||||
|
'mode' => 'string-3',
|
||||||
|
'filename' => 'string-255',
|
||||||
|
], [
|
||||||
|
'id_asset' => $this->image->getId(),
|
||||||
|
'width' => $this->width,
|
||||||
|
'height' => $this->height,
|
||||||
|
'mode' => $mode,
|
||||||
|
'filename' => $filename,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($success)
|
||||||
|
{
|
||||||
|
$thumb_selector = $this->width . 'x' . $this->height . $this->filename_suffix;
|
||||||
|
$this->thumbnails[$thumb_selector] = $filename !== 'NULL' ? $filename : null;
|
||||||
|
|
||||||
|
// For consistency, write new thumbnail filename to parent Image object.
|
||||||
|
// TODO: there could still be an inconsistency if multiple objects exists for the same image asset.
|
||||||
|
$this->image->getThumbnails()[$thumb_selector] = $this->thumbnails[$thumb_selector];
|
||||||
|
|
||||||
|
return $success;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
throw new UnexpectedValueException('Thumbnail queuing query failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function markAsQueued()
|
||||||
|
{
|
||||||
|
$this->updateDb('NULL');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function markAsGenerated($filename)
|
||||||
|
{
|
||||||
|
$this->updateDb($filename);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,17 +12,20 @@
|
|||||||
*/
|
*/
|
||||||
abstract class User
|
abstract class User
|
||||||
{
|
{
|
||||||
protected $id_user;
|
protected int $id_user;
|
||||||
protected $first_name;
|
protected string $first_name;
|
||||||
protected $surname;
|
protected string $surname;
|
||||||
protected $emailaddress;
|
protected string $slug;
|
||||||
|
protected string $emailaddress;
|
||||||
|
protected string $password_hash;
|
||||||
protected $creation_time;
|
protected $creation_time;
|
||||||
protected $last_action_time;
|
protected $last_action_time;
|
||||||
protected $ip_address;
|
protected $ip_address;
|
||||||
protected $is_admin;
|
protected $is_admin;
|
||||||
|
protected $reset_key;
|
||||||
|
|
||||||
protected $is_logged;
|
protected bool $is_logged;
|
||||||
protected $is_guest;
|
protected bool $is_guest;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns user id.
|
* Returns user id.
|
||||||
|
|||||||
59
models/UserMenu.php
Normal file
59
models/UserMenu.php
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
<?php
|
||||||
|
/*****************************************************************************
|
||||||
|
* UserMenu.php
|
||||||
|
* Contains the user navigation logic.
|
||||||
|
*
|
||||||
|
* Kabuki CMS (C) 2013-2023, Aaron van Geffen
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
class UserMenu extends Menu
|
||||||
|
{
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$user = Registry::has('user') ? Registry::get('user') : new Guest();
|
||||||
|
if ($user->isLoggedIn())
|
||||||
|
{
|
||||||
|
$this->items[] = [
|
||||||
|
'label' => $user->getFirstName(),
|
||||||
|
'icon' => 'person-circle',
|
||||||
|
'subs' => [
|
||||||
|
|
||||||
|
[
|
||||||
|
'label' => 'Settings',
|
||||||
|
'uri' => '/accountsettings/',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'label' => 'Log out',
|
||||||
|
'uri' => '/logout/',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$this->items[] = [
|
||||||
|
'label' => 'Log in',
|
||||||
|
'icon' => 'person-circle',
|
||||||
|
'uri' => '/login/',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->items[] = [
|
||||||
|
'label' => 'Home',
|
||||||
|
'icon' => 'house-door',
|
||||||
|
'uri' => '/',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($this->items as $i => $item)
|
||||||
|
{
|
||||||
|
if (isset($item['uri']))
|
||||||
|
$this->items[$i]['url'] = BASEURL . $item['uri'];
|
||||||
|
|
||||||
|
if (!isset($item['subs']))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
foreach ($item['subs'] as $j => $subitem)
|
||||||
|
$this->items[$i]['subs'][$j]['url'] = BASEURL . $subitem['uri'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,304 +1,83 @@
|
|||||||
.admin_box {
|
|
||||||
margin: 0;
|
|
||||||
padding: 20px;
|
|
||||||
background: #fff;
|
|
||||||
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin_box h2 {
|
|
||||||
font: 700 24px "Open Sans", sans-serif;
|
|
||||||
margin: 0 0 0.2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.floatleft {
|
|
||||||
float: left;
|
|
||||||
}
|
|
||||||
.floatright {
|
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Admin bar styles
|
|
||||||
---------------------*/
|
|
||||||
body {
|
|
||||||
padding-top: 30px;
|
|
||||||
}
|
|
||||||
#admin_bar {
|
|
||||||
background: #333;
|
|
||||||
color: #ccc;
|
|
||||||
left: 0;
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
width: 100%;
|
|
||||||
z-index: 100;
|
|
||||||
}
|
|
||||||
#admin_bar ul {
|
|
||||||
list-style: none;
|
|
||||||
margin: 0 auto;
|
|
||||||
max-width: 1280px;
|
|
||||||
min-width: 900px;
|
|
||||||
padding: 2px;
|
|
||||||
width: 95%;
|
|
||||||
}
|
|
||||||
#admin_bar ul > li {
|
|
||||||
display: inline;
|
|
||||||
border-right: 1px solid #aaa;
|
|
||||||
}
|
|
||||||
#admin_bar ul > li:last-child {
|
|
||||||
border-right: none;
|
|
||||||
}
|
|
||||||
#admin_bar li > a {
|
|
||||||
color: inherit;
|
|
||||||
display: inline-block;
|
|
||||||
padding: 4px 6px;
|
|
||||||
}
|
|
||||||
#admin_bar li a:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* (Tag) autosuggest
|
|
||||||
----------------------*/
|
|
||||||
#new_tag_container {
|
|
||||||
display: block;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
.autosuggest {
|
|
||||||
background: #fff;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
position: absolute;
|
|
||||||
top: 29px;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
.autosuggest li {
|
|
||||||
display: block !important;
|
|
||||||
padding: 3px;
|
|
||||||
}
|
|
||||||
.autosuggest li:hover, .autosuggest li.selected {
|
|
||||||
background: #CFECF7;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* Edit user screen
|
|
||||||
---------------------*/
|
|
||||||
.edituser dt {
|
|
||||||
clear: left;
|
|
||||||
float: left;
|
|
||||||
width: 150px;
|
|
||||||
}
|
|
||||||
.edituser dd {
|
|
||||||
float: left;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
.edituser form div:last-child {
|
|
||||||
padding: 1em 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* Admin widgets
|
|
||||||
------------------*/
|
|
||||||
.widget {
|
|
||||||
background: #fff;
|
|
||||||
padding: 25px;
|
|
||||||
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
|
|
||||||
}
|
|
||||||
.widget h3 {
|
|
||||||
margin: 0 0 1em;
|
|
||||||
font: 400 18px "Raleway", sans-serif;
|
|
||||||
}
|
|
||||||
.widget p, .errormsg p {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
.widget ul {
|
|
||||||
margin: 0;
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
.widget li {
|
|
||||||
line-height: 1.7em;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* Edit icon on tiled grids
|
/* Edit icon on tiled grids
|
||||||
-----------------------------*/
|
-----------------------------*/
|
||||||
.tiled_grid div.landscape, .tiled_grid div.portrait, .tiled_grid div.panorama {
|
.polaroid {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
.tiled_grid div > a.edit {
|
.polaroid a.edit {
|
||||||
background: #fff;
|
background: var(--bs-body-bg);
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
box-shadow: 1px 1px 2px rgba(0,0,0,0.3);
|
box-shadow: 1px 1px 2px rgba(0,0,0,0.3);
|
||||||
display: none;
|
color: var(--bs-body-color);
|
||||||
|
opacity: 0;
|
||||||
left: 20px;
|
left: 20px;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
padding: 5px 10px;
|
padding: 5px 10px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
transition: 0.25s;
|
||||||
top: 20px;
|
top: 20px;
|
||||||
}
|
}
|
||||||
.tiled_grid div:hover > a.edit {
|
.polaroid:hover > a.edit {
|
||||||
display: block;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Crop editor
|
/* Crop editor
|
||||||
----------------*/
|
----------------*/
|
||||||
#crop_editor {
|
#crop_editor {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background: #000;
|
background: rgba(0, 0, 0, 0.8);
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
#crop_editor input {
|
#crop_editor .input-group-text {
|
||||||
width: 50px;
|
background-color: rgba(233, 236, 239, 0.5);
|
||||||
background: #555;
|
border-color: rgba(233, 236, 239, 0.5);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
.crop_image_container {
|
#crop_editor input[type=number] {
|
||||||
position: relative;
|
background: #555;
|
||||||
|
border-color: rgba(233, 236, 239, 0.5);
|
||||||
|
color: #fff;
|
||||||
|
width: 85px;
|
||||||
|
}
|
||||||
|
#crop_editor input[type=checkbox] {
|
||||||
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
.crop_position {
|
.crop_position {
|
||||||
|
background: rgba(0, 0, 0, 1.0);
|
||||||
|
border: none;
|
||||||
|
display: flex;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
.crop_position input, .crop_position .btn {
|
.crop_position input, .crop_position .btn {
|
||||||
margin: 0 5px;
|
margin: 0 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.crop_image_container {
|
||||||
|
position: relative;
|
||||||
|
flex-grow: 1;
|
||||||
|
max-height: calc(100% - 34px);
|
||||||
|
}
|
||||||
.crop_image_container img {
|
.crop_image_container img {
|
||||||
height: auto;
|
border: 1px solid #000;
|
||||||
width: auto;
|
max-height: 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
max-height: 700px;
|
|
||||||
}
|
}
|
||||||
#crop_boundary {
|
#crop_boundary {
|
||||||
border: 1px solid rgba(255, 255, 255, 0.75);
|
border: 1px dashed rgb(255, 255, 255);
|
||||||
background: rgba(255, 255, 255, 0.75);
|
background: rgba(255, 255, 255, 0.4);
|
||||||
|
cursor: move;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 200;
|
z-index: 200;
|
||||||
width: 500px;
|
width: 500px;
|
||||||
height: 300px;
|
height: 300px;
|
||||||
top: 400px;
|
top: 400px;
|
||||||
left: 300px;
|
left: 300px;
|
||||||
filter: invert(100%); /* temp */
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* The pagination styles below are based on Bootstrap 2.3.2
|
|
||||||
-------------------------------------------------------------*/
|
|
||||||
|
|
||||||
.table_pagination, .table_form {
|
|
||||||
margin: 20px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table_pagination ul {
|
|
||||||
display: inline-block;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.table_pagination ul > li {
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table_pagination ul > li > a,
|
|
||||||
.table_pagination ul > li > span {
|
|
||||||
float: left;
|
|
||||||
padding: 4px 12px;
|
|
||||||
line-height: 20px;
|
|
||||||
text-decoration: none;
|
|
||||||
background-color: #ffffff;
|
|
||||||
border: 1px solid #dddddd;
|
|
||||||
border-left-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table_pagination ul > li > a:hover,
|
|
||||||
.table_pagination ul > li > a:focus,
|
|
||||||
.table_pagination ul > .active > a,
|
|
||||||
.table_pagination ul > .active > span {
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table_pagination ul > .active > a,
|
|
||||||
.table_pagination ul > .active > span {
|
|
||||||
color: #999999;
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table_pagination ul > .disabled > span,
|
|
||||||
.table_pagination ul > .disabled > a,
|
|
||||||
.table_pagination ul > .disabled > a:hover,
|
|
||||||
.table_pagination ul > .disabled > a:focus {
|
|
||||||
color: #999999;
|
|
||||||
cursor: default;
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table_pagination ul > li:first-child > a,
|
|
||||||
.table_pagination ul > li:first-child > span {
|
|
||||||
border-left-width: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* The table styles below were taken from Bootstrap 2.3.2
|
|
||||||
-----------------------------------------------------------*/
|
|
||||||
table {
|
|
||||||
max-width: 100%;
|
|
||||||
background-color: transparent;
|
|
||||||
border-collapse: collapse;
|
|
||||||
border-spacing: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table {
|
|
||||||
width: 100%;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table th,
|
|
||||||
.table td {
|
|
||||||
border-top: 1px solid #dddddd;
|
|
||||||
line-height: 20px;
|
|
||||||
padding: 8px;
|
|
||||||
text-align: left;
|
|
||||||
vertical-align: top;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table th {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table thead th {
|
|
||||||
vertical-align: bottom;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table caption + thead tr:first-child th,
|
|
||||||
.table caption + thead tr:first-child td,
|
|
||||||
.table colgroup + thead tr:first-child th,
|
|
||||||
.table colgroup + thead tr:first-child td,
|
|
||||||
.table thead:first-child tr:first-child th,
|
|
||||||
.table thead:first-child tr:first-child td {
|
|
||||||
border-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table tbody + tbody {
|
|
||||||
border-top: 2px solid #dddddd;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table .table {
|
|
||||||
background-color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-striped tbody > tr:nth-child(odd) > td,
|
|
||||||
.table-striped tbody > tr:nth-child(odd) > th {
|
|
||||||
background-color: #f9f9f9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-hover tbody tr:hover > td,
|
|
||||||
.table-hover tbody tr:hover > th {
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,309 +4,274 @@
|
|||||||
* DO NOT COPY OR RE-USE WITHOUT EXPLICIT WRITTEN PERMISSION. THANK YOU.
|
* DO NOT COPY OR RE-USE WITHOUT EXPLICIT WRITTEN PERMISSION. THANK YOU.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@import url(//fonts.googleapis.com/css?family=Press+Start+2P);
|
|
||||||
@import url(//fonts.googleapis.com/css?family=Open+Sans:400,400italic,700,700italic);
|
@import url(//fonts.googleapis.com/css?family=Open+Sans:400,400italic,700,700italic);
|
||||||
|
@import url('//fonts.googleapis.com/css2?family=Coda&display=swap');
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Invaders';
|
font-family: 'Invaders';
|
||||||
src: url('fonts/invaders.ttf') format('truetype');
|
src: url('fonts/invaders.ttf') format('truetype');
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font: 13px/1.7 "Open Sans", sans-serif;
|
font-family: "Open Sans", sans-serif;
|
||||||
padding: 0 0 3em;
|
background: #aaa 0 -50% fixed;
|
||||||
margin: 0;
|
padding: 0 0 3rem;
|
||||||
background: #99BFCE 0 -50% fixed;
|
transition: 0.5s;
|
||||||
background-image: radial-gradient(ellipse at top, #c3dee5 0%,#92b9ca 55%,#365e77 100%); /* W3C */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#wrapper, header {
|
[data-bs-theme=dark] body {
|
||||||
|
background-color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
#wrapper, header .container {
|
||||||
width: 95%;
|
width: 95%;
|
||||||
min-width: 900px;
|
|
||||||
max-width: 1280px;
|
max-width: 1280px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
header {
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: #487C96;
|
color: #963626;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
a:hover {
|
a:hover {
|
||||||
color: #222;
|
color: #262626;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Logo
|
[data-bs-theme=dark] .content-box a:not(.btn) {
|
||||||
---------*/
|
color: #b94b4b;
|
||||||
h1#logo {
|
|
||||||
color: #fff;
|
|
||||||
float: left;
|
|
||||||
font: 200 50px 'Press Start 2P', sans-serif;
|
|
||||||
margin: 40px 0 50px 10px;
|
|
||||||
padding: 0;
|
|
||||||
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.6);
|
|
||||||
}
|
}
|
||||||
h1#logo:before {
|
[data-bs-theme=dark] .content-box a:not(.btn):hover {
|
||||||
color: #fff;
|
color: #963626;
|
||||||
content: 'B';
|
|
||||||
float: left;
|
|
||||||
font: 75px 'Invaders';
|
|
||||||
margin: -4px 20px 0 0;
|
|
||||||
padding: 0;
|
|
||||||
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.6);
|
|
||||||
}
|
}
|
||||||
a h1#logo {
|
|
||||||
text-decoration: none;
|
|
||||||
|
.page-link {
|
||||||
|
color: #b50707;
|
||||||
|
font-family: 'Coda', sans-serif;
|
||||||
}
|
}
|
||||||
a:hover h1#logo, a:hover h1#logo:before {
|
.page-link:hover {
|
||||||
text-shadow: 2px 2px 3px rgba(0, 0, 0, 0.6);
|
color: #a40d0d;
|
||||||
|
}
|
||||||
|
.active > .page-link, .page-link.active {
|
||||||
|
background-color: #990b0b;
|
||||||
|
border-color: #a40d0d;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.pagination {
|
||||||
|
box-shadow: none;
|
||||||
|
height: 52px;
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.pagination .page-number, .pagination .page-padding {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.pagination .page-link {
|
||||||
|
border-radius: var(--bs-pagination-border-radius) !important;
|
||||||
|
}
|
||||||
|
.pagination > :first-child, .pagination > :last-child {
|
||||||
|
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
|
||||||
|
display: inline-block;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
.pagination > :first-child {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
.pagination > :last-child {
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
font-family: 'Coda', sans-serif;
|
||||||
|
}
|
||||||
|
.btn-primary {
|
||||||
|
--bs-btn-bg: #6c757d;
|
||||||
|
--bs-btn-border-color: #6c757d;
|
||||||
|
--bs-btn-hover-bg: #5c636a;
|
||||||
|
--bs-btn-hover-border-color: #565e64;
|
||||||
|
--bs-btn-focus-shadow-rgb: 130, 138, 145;
|
||||||
|
--bs-btn-active-bg: #565e64;
|
||||||
|
--bs-btn-active-border-color: #51585e;
|
||||||
|
--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
|
||||||
|
--bs-btn-disabled-bg: #6c757d;
|
||||||
|
--bs-btn-disabled-border-color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item.active, .dropdown-item:active {
|
||||||
|
background-color: #990b0b;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Navigation
|
/* Navigation
|
||||||
---------------*/
|
---------------*/
|
||||||
ul#nav {
|
#mainNav {
|
||||||
margin: 55px 10px 0 0;
|
font-family: 'Coda', sans-serif;
|
||||||
padding: 0;
|
margin-bottom: 4rem;
|
||||||
float: right;
|
|
||||||
list-style: none;
|
|
||||||
}
|
}
|
||||||
ul#nav li {
|
.nav-divider {
|
||||||
float: left;
|
height: 2.5rem;
|
||||||
|
border-left: .1rem solid rgba(255,255,255, 0.2);
|
||||||
|
margin: 0 0.5rem;
|
||||||
}
|
}
|
||||||
ul#nav li a {
|
.navbar-brand {
|
||||||
color: #fff;
|
padding-left: 80px;
|
||||||
display: block;
|
|
||||||
float: left;
|
|
||||||
font: 200 20px 'Press Start 2P', sans-serif;
|
|
||||||
margin: 0 0 0 32px;
|
|
||||||
padding: 10px 0;
|
|
||||||
text-decoration: none;
|
|
||||||
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.7);
|
|
||||||
}
|
|
||||||
ul#nav li a:hover {
|
|
||||||
text-shadow: 2px 2px 3px rgba(0, 0, 0, 0.6);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* Pagination
|
|
||||||
---------------*/
|
|
||||||
.pagination {
|
|
||||||
clear: both;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.pagination ul {
|
|
||||||
display: inline-block;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
|
|
||||||
}
|
|
||||||
.pagination ul > li {
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
.pagination ul > li > a, .pagination ul > li > span {
|
|
||||||
float: left;
|
|
||||||
font: 300 18px/2.2 "Open Sans", sans-serif;
|
|
||||||
padding: 6px 22px;
|
|
||||||
text-decoration: none;
|
|
||||||
background-color: #fff;
|
|
||||||
border-right: 1px solid #ddd;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pagination ul > li > a:hover, .pagination ul > li > a:focus,
|
|
||||||
.pagination ul > .active > a, .pagination ul > .active > span {
|
|
||||||
background-color: #eee;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pagination ul > .active > a, .pagination ul > .active > span {
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pagination ul > .disabled > span, .pagination ul > .disabled > a,
|
|
||||||
.pagination ul > .disabled > a:hover, .pagination ul > .disabled > a:focus {
|
|
||||||
color: #999;
|
|
||||||
cursor: default;
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pagination .page-padding {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* Tiled grid
|
|
||||||
---------------*/
|
|
||||||
.tiled_header {
|
|
||||||
background: #fff;
|
|
||||||
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
|
|
||||||
color: #000;
|
|
||||||
clear: both;
|
|
||||||
float: left;
|
|
||||||
margin: 0 0 1.5% 0;
|
|
||||||
font: 400 18px/2.2 "Open Sans", sans-serif;
|
|
||||||
padding: 6px 22px;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tiled_grid {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
.tiled_grid div.landscape, .tiled_grid div.portrait, .tiled_grid div.panorama,
|
|
||||||
.tiled_grid div.duo, .tiled_grid div.single {
|
|
||||||
background: #fff;
|
|
||||||
border-bottom-style: none !important;
|
|
||||||
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
|
|
||||||
float: left;
|
|
||||||
line-height: 0;
|
|
||||||
margin: 0 3.5% 3.5% 0;
|
|
||||||
overflow: none;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
padding-bottom: 5px;
|
|
||||||
width: 31%;
|
|
||||||
}
|
}
|
||||||
.tiled_grid div img {
|
i.space-invader::before {
|
||||||
|
color: #fff;
|
||||||
|
content: 'B';
|
||||||
|
display: inline-block;
|
||||||
|
font: 85px 'Invaders';
|
||||||
|
height: 85px;
|
||||||
|
left: -25px;
|
||||||
|
position: absolute;
|
||||||
|
text-align: center;
|
||||||
|
text-shadow: 2px 2px 2px rgba(0, 0, 0, 0.6);
|
||||||
|
top: -5px;
|
||||||
|
transform: rotate(-5deg);
|
||||||
|
transition: 0.25s;
|
||||||
|
width: 110px;
|
||||||
|
}
|
||||||
|
.navbar-brand:hover i.space-invader::before {
|
||||||
|
transform: rotate(5deg);
|
||||||
|
}
|
||||||
|
i.space-invader.alt-1::before {
|
||||||
|
content: 'C';
|
||||||
|
}
|
||||||
|
i.space-invader.alt-2::before {
|
||||||
|
content: 'D';
|
||||||
|
}
|
||||||
|
i.space-invader.alt-3::before {
|
||||||
|
content: 'E';
|
||||||
|
}
|
||||||
|
i.space-invader.alt-4::before {
|
||||||
|
content: 'H';
|
||||||
|
}
|
||||||
|
i.space-invader.alt-5::before {
|
||||||
|
content: 'I';
|
||||||
|
}
|
||||||
|
i.space-invader.alt-6::before {
|
||||||
|
content: 'N';
|
||||||
|
}
|
||||||
|
i.space-invader.alt-7::before {
|
||||||
|
content: 'O';
|
||||||
|
}
|
||||||
|
@media (max-width: 991px) {
|
||||||
|
.navbar-brand {
|
||||||
|
padding-left: 60px;
|
||||||
|
}
|
||||||
|
i.space-invader::before {
|
||||||
|
font-size: 50px;
|
||||||
|
left: -10px;
|
||||||
|
top: -7px;
|
||||||
|
width: 70px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Content boxes
|
||||||
|
------------------*/
|
||||||
|
.content-box {
|
||||||
|
background-color: var(--bs-body-bg);
|
||||||
|
margin: 0 auto 2rem;
|
||||||
|
padding: 2rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
box-shadow: 0 0.25rem 0.5rem rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
.content-box h1,
|
||||||
|
.content-box h2,
|
||||||
|
.content-box h3 {
|
||||||
|
font-family: 'Coda', sans-serif;
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Album and photo index pages
|
||||||
|
--------------------------------*/
|
||||||
|
.album-index, .photo-index {
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tiled-header > a {
|
||||||
|
background: var(--bs-body-bg);
|
||||||
|
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
color: var(--bs-body-color);
|
||||||
|
display: inline-block;
|
||||||
|
font: 400 18px/2.2 'Coda', sans-serif;
|
||||||
|
margin: 0 0 1.5% 0;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 6px 22px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.polaroid {
|
||||||
|
background: var(--bs-body-bg);
|
||||||
|
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
|
||||||
|
line-height: 0;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
div.polaroid img {
|
||||||
|
background: url('../images/nothumb.svg') center no-repeat;
|
||||||
border: none;
|
border: none;
|
||||||
|
object-fit: cover;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
.tiled_grid div h4 {
|
div.polaroid h4 {
|
||||||
color: #000;
|
color: var(--bs-body-color);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font: 400 18px "Open Sans", sans-serif;
|
font: 400 18px 'Coda', sans-serif;
|
||||||
padding: 20px 5px 15px;
|
padding: 15px 5px;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.tiled_grid div a {
|
div.polaroid a {
|
||||||
|
display: block;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
.tiled_grid div.landscape:hover, .tiled_grid div.portrait:hover, .tiled_grid div.panorama:hover,
|
div.polaroid:hover {
|
||||||
.tiled_grid div.duo:hover, .tiled_grid div.single:hover {
|
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
|
||||||
padding-bottom: 0;
|
|
||||||
border-bottom-width: 5px;
|
|
||||||
border-bottom-style: solid !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Panoramas */
|
|
||||||
.tiled_grid div.panorama, .tiled_grid .tiled_row {
|
|
||||||
clear: both;
|
|
||||||
float: left;
|
|
||||||
width: 100%;
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Tiling: one landscape, two tiles */
|
|
||||||
.tiled_row .column_landscape {
|
|
||||||
float: left;
|
|
||||||
width: 65.5%;
|
|
||||||
margin: 0 3.5% 3.5% 0;
|
|
||||||
}
|
|
||||||
.tiled_row .column_landscape div {
|
|
||||||
width: auto;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tiled_row .column_tiles_two {
|
|
||||||
float: left;
|
|
||||||
width: 31%;
|
|
||||||
}
|
|
||||||
.tiled_row .column_tiles_two div {
|
|
||||||
float: left;
|
|
||||||
width: 100%;
|
|
||||||
margin: 0 0 10% 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Tiling: big portrait, four tiles */
|
|
||||||
.tiled_row .column_portrait {
|
|
||||||
float: left;
|
|
||||||
width: 31%;
|
|
||||||
margin: 0 3.5% 3.5% 0;
|
|
||||||
}
|
|
||||||
.tiled_row .column_portrait div {
|
|
||||||
width: auto;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tiled_row .column_tiles_four {
|
|
||||||
float: left;
|
|
||||||
width: 65.5%;
|
|
||||||
}
|
|
||||||
.tiled_row .column_tiles_four div {
|
|
||||||
float: left;
|
|
||||||
width: 47.45%;
|
|
||||||
margin: 0 5% 5% 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Tiling: two tiles */
|
|
||||||
.tiled_row .duo {
|
|
||||||
width: 48.25% !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Tiling: one tile */
|
|
||||||
.tiled_row .single {
|
|
||||||
width: 48.25% !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Tiling: remove horizontal margin at end of row. */
|
|
||||||
.tiled_row > div:nth-child(3n),
|
|
||||||
.tiled_row > .duo:nth-child(2) {
|
|
||||||
margin-right: 0 !important;
|
|
||||||
}
|
|
||||||
.tiled_row .column_tiles_four > div:nth-child(2n) {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Tiling: switch places for odd rows */
|
|
||||||
.tiled_row:nth-child(odd) .column_landscape,
|
|
||||||
.tiled_row:nth-child(odd) .column_portrait {
|
|
||||||
float: right;
|
|
||||||
margin: 0 0 3.5% 3.5%;
|
|
||||||
}
|
|
||||||
.tiled_row:nth-child(odd) .column_tiles_four {
|
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
.tiled_row:nth-child(odd) .column_tiles_two {
|
|
||||||
float: right;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Album title boxes
|
/* Album title boxes
|
||||||
----------------------*/
|
----------------------*/
|
||||||
.album_title_box {
|
.album_title_box {
|
||||||
margin-left: -46px;
|
margin-left: -2.85rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
.album_title_box > a {
|
.album_title_box > a {
|
||||||
background: #fff;
|
background: var(--bs-body-bg);
|
||||||
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
|
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
|
||||||
|
border-top-left-radius: 0.5rem;
|
||||||
|
border-bottom-left-radius: 0.5rem;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
float: left;
|
float: left;
|
||||||
font-size: 2em;
|
font-size: 2rem;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
padding: 8px 10px 14px;
|
padding: 8px 10px;
|
||||||
}
|
}
|
||||||
.album_title_box > div {
|
.album_title_box > div {
|
||||||
background: #fff;
|
background: var(--bs-body-bg);
|
||||||
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
|
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
|
||||||
|
border-top-right-radius: 0.5rem;
|
||||||
|
border-bottom-right-radius: 0.5rem;
|
||||||
float: left;
|
float: left;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
padding: 6px 22px;
|
padding: 0.4rem 1.3rem;
|
||||||
max-width: 90%;
|
max-width: 90%;
|
||||||
margin: 0 0 1.5% 0;
|
margin: 0 0 1.5% 0;
|
||||||
}
|
}
|
||||||
.album_title_box h2 {
|
.album_title_box h2 {
|
||||||
color: #487C96;
|
color: var(--bs-body-color);
|
||||||
font: 400 18px/2 "Open Sans", sans-serif !important;
|
font: 400 18px/2 'Coda', sans-serif !important;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
.album_title_box p {
|
.album_title_box p {
|
||||||
@@ -318,29 +283,46 @@ ul#nav li a:hover {
|
|||||||
---------------------*/
|
---------------------*/
|
||||||
.album_button_box {
|
.album_button_box {
|
||||||
float: right;
|
float: right;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 3rem;
|
||||||
}
|
}
|
||||||
.album_button_box > a {
|
.album_button_box > a {
|
||||||
background: #fff;
|
background: var(--bs-body-bg);
|
||||||
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
|
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
|
||||||
display: inline-block;
|
border-color: var( --bs-secondary-bg);
|
||||||
float: left;
|
color: var(--bs-body-color);
|
||||||
font-size: 1em;
|
|
||||||
padding: 8px 10px;
|
padding: 8px 10px;
|
||||||
margin-left: 12px;
|
margin-left: 12px;
|
||||||
}
|
}
|
||||||
|
.album_button_box > a:hover {
|
||||||
|
background: var(--bs-secondary-bg);
|
||||||
/* Generic boxed content
|
border-color: var(--bs-tertiary-bg);
|
||||||
--------------------------*/
|
color: var(--bs-secondary-color);
|
||||||
.boxed_content {
|
|
||||||
background: #fff;
|
|
||||||
padding: 25px;
|
|
||||||
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
|
|
||||||
}
|
}
|
||||||
.boxed_content h2 {
|
|
||||||
font: 300 24px "Open Sans", sans-serif;
|
|
||||||
margin: 0 0 0.2em;
|
/* (Tag) autosuggest
|
||||||
|
----------------------*/
|
||||||
|
#new_tag_container {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.autosuggest {
|
||||||
|
background: var(--bs-body-bg);
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
color: var(--bs-body-color);
|
||||||
|
position: absolute;
|
||||||
|
left: 2px;
|
||||||
|
top: 37px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.autosuggest li {
|
||||||
|
display: block !important;
|
||||||
|
padding: 3px 8px;
|
||||||
|
}
|
||||||
|
.autosuggest li:hover, .autosuggest li.selected {
|
||||||
|
background: #CFECF7;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -358,6 +340,36 @@ ul#nav li a:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Featured thumbnail selection
|
||||||
|
---------------------------------*/
|
||||||
|
#featuredThumbnail {
|
||||||
|
list-style: none;
|
||||||
|
margin: 2.5% 0 0;
|
||||||
|
padding: 0;
|
||||||
|
clear: both;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
#featuredThumbnail li {
|
||||||
|
float: left;
|
||||||
|
width: 18%;
|
||||||
|
line-height: 0;
|
||||||
|
margin: 0 1% 2%;
|
||||||
|
min-width: 200px;
|
||||||
|
height: 149px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
#featuredThumbnail input {
|
||||||
|
position: absolute;
|
||||||
|
top: 0.5rem;
|
||||||
|
right: 0.5rem;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
#featuredThumbnail img {
|
||||||
|
width: 100%;
|
||||||
|
box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Footer
|
/* Footer
|
||||||
-----------*/
|
-----------*/
|
||||||
footer {
|
footer {
|
||||||
@@ -374,219 +386,57 @@ footer a {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Input
|
|
||||||
----------*/
|
|
||||||
|
|
||||||
input, select, .btn {
|
|
||||||
background: #fff;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
color: #000;
|
|
||||||
font: 13px/1.7 "Open Sans", "Helvetica", sans-serif;
|
|
||||||
padding: 3px;
|
|
||||||
}
|
|
||||||
input[type=submit], button, .btn {
|
|
||||||
background: #C5E2EA;
|
|
||||||
border-radius: 3px;
|
|
||||||
border: 1px solid #8BBFCE;
|
|
||||||
display: inline-block;
|
|
||||||
font: inherit;
|
|
||||||
padding: 4px 5px;
|
|
||||||
}
|
|
||||||
input[type=submit]:hover, button:hover, .btn:hover {
|
|
||||||
background-color: #bddce5;
|
|
||||||
border-color: #88b7c6;
|
|
||||||
}
|
|
||||||
textarea {
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
font: 12px/1.4 'Monaco', 'Inconsolata', 'DejaVu Sans Mono', monospace;
|
|
||||||
padding: 0.75%;
|
|
||||||
width: 98.5%;
|
|
||||||
}
|
|
||||||
.btn-red {
|
|
||||||
background: #F3B076;
|
|
||||||
border-color: #C98245;
|
|
||||||
color: #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* Login box styles
|
|
||||||
---------------------*/
|
|
||||||
#login {
|
|
||||||
background: #fff;
|
|
||||||
border: 1px solid #aaa;
|
|
||||||
border-radius: 10px;
|
|
||||||
box-shadow: 2px 2px 4px rgba(0,0,0,0.1);
|
|
||||||
margin: 0 auto;
|
|
||||||
overflow: auto;
|
|
||||||
padding: 15px;
|
|
||||||
width: 300px;
|
|
||||||
}
|
|
||||||
#login dl *, #login button {
|
|
||||||
font-size: 15px;
|
|
||||||
line-height: 35px;
|
|
||||||
}
|
|
||||||
#login h3 {
|
|
||||||
font: 700 24px/36px "Open Sans", sans-serif;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
#login dd {
|
|
||||||
width: 96%;
|
|
||||||
margin: 0 0 10px;
|
|
||||||
}
|
|
||||||
#login input {
|
|
||||||
background: #eee;
|
|
||||||
border: 1px solid #aaa;
|
|
||||||
border-radius: 3px;
|
|
||||||
padding: 4px 5px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
#login div.alert {
|
|
||||||
margin: 15px 0;
|
|
||||||
}
|
|
||||||
#login div.buttonstrip {
|
|
||||||
float: right;
|
|
||||||
padding: 0 0 5px;
|
|
||||||
}
|
|
||||||
#login button {
|
|
||||||
line-height: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* Alert boxes -- styling borrowed from Bootstrap 2
|
|
||||||
-----------------------------------------------------*/
|
|
||||||
.alert {
|
|
||||||
padding: 8px 35px 8px 14px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
|
|
||||||
background-color: #fcf8e3;
|
|
||||||
border: 1px solid #fbeed5;
|
|
||||||
-webkit-border-radius: 4px;
|
|
||||||
-moz-border-radius: 4px;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
.alert,
|
|
||||||
.alert h4 {
|
|
||||||
color: #c09853;
|
|
||||||
}
|
|
||||||
.alert h4 {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
.alert .close {
|
|
||||||
position: relative;
|
|
||||||
top: -2px;
|
|
||||||
right: -21px;
|
|
||||||
line-height: 20px;
|
|
||||||
}
|
|
||||||
.alert-success {
|
|
||||||
background-color: #dff0d8;
|
|
||||||
border-color: #d6e9c6;
|
|
||||||
color: #468847;
|
|
||||||
}
|
|
||||||
.alert-success h4 {
|
|
||||||
color: #468847;
|
|
||||||
}
|
|
||||||
.alert-danger,
|
|
||||||
.alert-error {
|
|
||||||
background-color: #f2dede;
|
|
||||||
border-color: #eed3d7;
|
|
||||||
color: #b94a48;
|
|
||||||
}
|
|
||||||
.alert-danger h4,
|
|
||||||
.alert-error h4 {
|
|
||||||
color: #b94a48;
|
|
||||||
}
|
|
||||||
.alert-info {
|
|
||||||
background-color: #d9edf7;
|
|
||||||
border-color: #bce8f1;
|
|
||||||
color: #3a87ad;
|
|
||||||
}
|
|
||||||
.alert-info h4 {
|
|
||||||
color: #3a87ad;
|
|
||||||
}
|
|
||||||
.alert-block {
|
|
||||||
padding-top: 14px;
|
|
||||||
padding-bottom: 14px;
|
|
||||||
}
|
|
||||||
.alert-block > p,
|
|
||||||
.alert-block > ul {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
.alert-block p + p {
|
|
||||||
margin-top: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* Styling for the photo pages
|
/* Styling for the photo pages
|
||||||
--------------------------------*/
|
--------------------------------*/
|
||||||
#photo_frame {
|
#photo_frame {
|
||||||
overflow: hidden;
|
padding-top: 1.5vh;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
#photo_frame a {
|
#photo_frame a {
|
||||||
background: #fff;
|
|
||||||
border: 0.9em solid #fff;
|
|
||||||
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
|
|
||||||
cursor: -moz-zoom-in;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
}
|
||||||
#photo_frame a img {
|
#photo_frame a img {
|
||||||
border: none;
|
border: none;
|
||||||
display: block;
|
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
|
||||||
height: auto;
|
cursor: -moz-zoom-in;
|
||||||
width: 100%;
|
display: inline-block;
|
||||||
|
height: 97vh;
|
||||||
|
max-width: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
width: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
#previous_photo, #next_photo {
|
#previous_photo, #next_photo {
|
||||||
background: #fff;
|
background: var(--bs-body-bg);
|
||||||
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
|
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
|
||||||
color: #678FA4;
|
color: var(--bs-body-color);
|
||||||
font-size: 3em;
|
font-size: 3rem;
|
||||||
line-height: 0.5;
|
line-height: 0.5;
|
||||||
padding: 32px 8px;
|
padding: 2rem 0.5rem;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
top: 45%;
|
top: calc(50% - 5rem);
|
||||||
}
|
|
||||||
#previous_photo em, #next_photo em {
|
|
||||||
position: absolute;
|
|
||||||
top: -1000em;
|
|
||||||
left: -1000em;
|
|
||||||
}
|
}
|
||||||
span#previous_photo, span#next_photo {
|
span#previous_photo, span#next_photo {
|
||||||
opacity: 0.25;
|
opacity: 0.25;
|
||||||
}
|
}
|
||||||
a#previous_photo:hover, a#next_photo:hover {
|
a#previous_photo:hover, a#next_photo:hover {
|
||||||
background: #eee;
|
background: var(--bs-secondary-bg);
|
||||||
color: #000;
|
color: var(--bs-secondary-color);
|
||||||
}
|
}
|
||||||
#previous_photo {
|
#previous_photo {
|
||||||
|
border-top-right-radius: 0.5rem;
|
||||||
|
border-bottom-right-radius: 0.5rem;
|
||||||
left: 0;
|
left: 0;
|
||||||
}
|
}
|
||||||
#previous_photo:before {
|
|
||||||
content: '←';
|
|
||||||
}
|
|
||||||
#next_photo {
|
#next_photo {
|
||||||
|
border-top-left-radius: 0.5rem;
|
||||||
|
border-bottom-left-radius: 0.5rem;
|
||||||
right: 0;
|
right: 0;
|
||||||
}
|
}
|
||||||
#next_photo:before {
|
|
||||||
content: '→';
|
|
||||||
}
|
|
||||||
|
|
||||||
#sub_photo h2, #sub_photo h3, #photo_exif_box h3 {
|
#sub_photo h2, #sub_photo h3, #photo_exif_box h3, #user_actions_box h3 {
|
||||||
font: 600 20px/30px "Open Sans", sans-serif;
|
margin-bottom: 1rem;
|
||||||
margin: 0 0 10px;
|
|
||||||
}
|
|
||||||
#sub_photo h3 {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#sub_photo {
|
|
||||||
background: #fff;
|
|
||||||
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
|
|
||||||
float: left;
|
|
||||||
padding: 2%;
|
|
||||||
margin: 25px 3.5% 25px 0;
|
|
||||||
width: 68.5%;
|
|
||||||
}
|
}
|
||||||
#sub_photo #tag_list {
|
#sub_photo #tag_list {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
@@ -595,23 +445,16 @@ a#previous_photo:hover, a#next_photo:hover {
|
|||||||
}
|
}
|
||||||
#sub_photo #tag_list li {
|
#sub_photo #tag_list li {
|
||||||
display: inline;
|
display: inline;
|
||||||
|
padding-right: 0.75em;
|
||||||
}
|
}
|
||||||
#sub_photo #tag_list li:after {
|
#tag_list .delete-tag {
|
||||||
content: ', ';
|
opacity: 0.25;
|
||||||
}
|
}
|
||||||
#sub_photo #tag_list li:last-child:after {
|
#tag_list .delete-tag:hover {
|
||||||
content: '';
|
opacity: 1.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#photo_exif_box {
|
|
||||||
background: #fff;
|
|
||||||
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
|
|
||||||
margin: 25px 0 25px 0;
|
|
||||||
overflow: auto;
|
|
||||||
padding: 2%;
|
|
||||||
float: right;
|
|
||||||
width: 20%;
|
|
||||||
}
|
|
||||||
#photo_exif_box dt {
|
#photo_exif_box dt {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
float: left;
|
float: left;
|
||||||
@@ -636,57 +479,17 @@ a#previous_photo:hover, a#next_photo:hover {
|
|||||||
max-width: 100% !important;
|
max-width: 100% !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1#logo {
|
.album_title_box {
|
||||||
font-size: 42px;
|
margin-left: 0;
|
||||||
float: none;
|
|
||||||
margin: 1em 0 0.5em;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
h1#logo:before {
|
|
||||||
float: none;
|
|
||||||
font-size: 58px;
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ul#nav {
|
.tiled-header {
|
||||||
float: none;
|
|
||||||
padding: 0;
|
|
||||||
margin: 1em 0;
|
|
||||||
text-align: center;
|
|
||||||
overflow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul#nav li, ul#nav li a {
|
|
||||||
display: inline-block;
|
|
||||||
float: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul#nav li a {
|
|
||||||
float: none;
|
|
||||||
font-size: 16px;
|
|
||||||
margin-left: 6px;
|
|
||||||
padding: 12px 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tiled_header {
|
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
margin: 0 0 3.5% 0;
|
margin: 0 0 3.5% 0;
|
||||||
}
|
}
|
||||||
.tiled_grid div h4 {
|
.panorama h4 {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
padding: 15px 5px 10px;
|
padding: 15px 5px;
|
||||||
}
|
|
||||||
|
|
||||||
.tiled_row > div, .tiled_row .single, .tiled_row .duo {
|
|
||||||
float: none !important;
|
|
||||||
width: 100% !important;
|
|
||||||
margin: 0 0 5% !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tiled_row > div > div {
|
|
||||||
float: none !important;
|
|
||||||
width: 100% !important;
|
|
||||||
margin: 0 0 5% !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#previous_photo, #next_photo {
|
#previous_photo, #next_photo {
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.6 KiB |
10
public/images/nothumb.svg
Normal file
10
public/images/nothumb.svg
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 480 150">
|
||||||
|
<defs><style>.cls-2{fill:#cc9d9d;}</style></defs>
|
||||||
|
<g>
|
||||||
|
<path class="cls-2" d="m221.34,135.39c-13.69,0-27.38-.09-41.07.06-3.43.04-4.94-.61-4.91-4.56.17-27.21.14-54.42.02-81.63-.02-3.41.9-4.57,4.45-4.56,27.69.13,55.38.12,83.07,0,3.36-.01,4.19,1.18,4.18,4.34-.1,27.37-.12,54.73.02,82.1.02,3.73-1.44,4.34-4.69,4.3-13.69-.14-27.38-.06-41.07-.06Zm-.11-27.1c11.37,0,22.74-.1,34.1.06,3.26.05,4.3-.97,4.28-4.25-.14-16.19-.14-32.38,0-48.56.03-3.28-1.01-4.27-4.27-4.25-22.74.12-45.47.12-68.21,0-3.26-.02-4.3.97-4.27,4.25.14,16.19.14,32.38,0,48.56-.03,3.28,1.01,4.3,4.27,4.26,11.37-.16,22.74-.06,34.1-.06Z"/>
|
||||||
|
<path class="cls-2" d="m271.69,111.12c.4-3.72-.27-8.33-.9-12.95-.4-2.96.59-3.73,3.62-3.01,6.71,1.61,6.75,1.45,8.74-5.81,3.66-13.3,7.37-26.59,10.95-39.91,1.64-6.09,1.55-6.23-4.53-7.87-20.8-5.63-41.65-11.12-62.43-16.82-3.48-.95-5.32-.26-6.11,3.33-.73,3.33-1.85,6.57-2.55,9.9-.71,3.39-3,4.22-5.87,3.73-3.34-.57-2.27-2.94-1.71-5.06,1.7-6.44,3.31-12.91,5.03-19.34.47-1.74.7-3.35,3.66-2.54,27.36,7.52,54.77,14.85,82.2,22.1,2.71.72,3.31,1.43,2.52,4.29-7.26,26.45-14.3,52.97-21.49,79.44-.5,1.84-.24,5.23-3.51,4.25-3.05-.92-8.22.3-7.68-5.77.21-2.32.03-4.67.03-7.96Z"/>
|
||||||
|
<path class="cls-2" d="m237.89,68.65c3.58,9.04,7.13,18.07,10.74,27.08.87,2.17.4,3.25-2.07,3.25-16.63-.01-33.25,0-49.88-.01-2.63,0-2.8-1.35-1.8-3.33.7-1.39,1.37-2.79,2.07-4.17,2.84-5.69,2.92-5.78,8.04-1.6,1.77,1.44,2.44,1.1,3.45-.67,1.69-2.95,3.7-5.72,5.45-8.64,1.39-2.31,2.67-2.5,4.73-.62,2.11,1.93,3.79,5.97,6.49,5.2,2.2-.63,3.51-4.41,5.19-6.81,2.13-3.04,4.23-6.1,6.37-9.13.15-.21.54-.25,1.23-.54Z"/>
|
||||||
|
<path class="cls-2" d="m201.38,75.62c-3.33.17-5.32-1.1-5.41-4.73-.09-3.64,1.37-6.17,5.12-6.38,3.38-.19,5.57,1.83,6,5.22.4,3.09-2.39,5.81-5.72,5.89Z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -1,14 +1,14 @@
|
|||||||
function enableKeyDownNavigation() {
|
function enableKeyDownNavigation() {
|
||||||
document.addEventListener("keydown", function (event) {
|
document.addEventListener("keydown", function (event) {
|
||||||
if (event.keyCode == 37) {
|
if (event.keyCode == 37) {
|
||||||
var target = document.querySelector(".pagination ul > :first-child a");
|
var target = document.querySelector("ul.pagination > :first-child a");
|
||||||
if (target && target.href) {
|
if (target && target.href) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
document.location.href = target.href;
|
document.location.href = target.href;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (event.keyCode == 39) {
|
else if (event.keyCode == 39) {
|
||||||
var target = document.querySelector(".pagination ul > :last-child a");
|
var target = document.querySelector("ul.pagination > :last-child a");
|
||||||
if (target && target.href) {
|
if (target && target.href) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
document.location.href = target.href;
|
document.location.href = target.href;
|
||||||
|
|||||||
@@ -13,166 +13,165 @@ provided that the following conditions are met:
|
|||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
function AutoSuggest(opt) {
|
class AutoSuggest {
|
||||||
if (typeof opt.inputElement === "undefined" || typeof opt.listElement === "undefined" || typeof opt.baseUrl === "undefined" || typeof opt.appendCallback === "undefined") {
|
constructor(opt) {
|
||||||
return;
|
if (typeof opt.inputElement === "undefined" || typeof opt.listElement === "undefined" ||
|
||||||
|
typeof opt.baseUrl === "undefined" || typeof opt.appendCallback === "undefined") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.input = document.getElementById(opt.inputElement);
|
||||||
|
this.input.autocomplete = "off";
|
||||||
|
this.list = document.getElementById(opt.listElement);
|
||||||
|
this.appendCallback = opt.appendCallback;
|
||||||
|
this.baseurl = opt.baseUrl;
|
||||||
|
|
||||||
|
this.input.addEventListener('keydown', event => this.doSelection(event), false);
|
||||||
|
this.input.addEventListener('keyup', event => this.onType(event), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.input = document.getElementById(opt.inputElement);
|
doSelection(event) {
|
||||||
this.input.autocomplete = "off";
|
if (typeof this.container === "undefined" || this.container.children.length === 0) {
|
||||||
this.list = document.getElementById(opt.listElement);
|
return;
|
||||||
this.appendCallback = opt.appendCallback;
|
}
|
||||||
this.baseurl = opt.baseUrl;
|
|
||||||
|
|
||||||
var self = this;
|
switch (event.key) {
|
||||||
this.input.addEventListener('keydown', function(event) {
|
case 'Enter':
|
||||||
self.doSelection(event);
|
event.preventDefault();
|
||||||
}, false);
|
this.container.children[this.selectedIndex].click();
|
||||||
this.input.addEventListener('keyup', function(event) {
|
break;
|
||||||
self.onType(this, event);
|
|
||||||
}, false);
|
case 'ArrowUp':
|
||||||
|
case 'ArrowDown':
|
||||||
|
event.preventDefault();
|
||||||
|
this.findSelectedElement().className = '';
|
||||||
|
this.selectedIndex += event.key === 'ArrowUp' ? -1 : 1;
|
||||||
|
if (this.selectedIndex < 0) {
|
||||||
|
this.selectedIndex = this.container.children.length - 1;
|
||||||
|
} else if (this.selectedIndex === this.container.children.length) {
|
||||||
|
this.selectedIndex = 0;
|
||||||
|
}
|
||||||
|
let new_el = this.findSelectedElement().className = 'selected';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
findSelectedElement() {
|
||||||
|
return this.container.children[this.selectedIndex];
|
||||||
|
};
|
||||||
|
|
||||||
|
onType(event) {
|
||||||
|
if (['Enter', 'ArrowDown', 'ArrowUp'].indexOf(event.key) !== -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let tokens = event.target.value.split(/\s+/).filter(token => token.length >= 2);
|
||||||
|
|
||||||
|
if (tokens.length === 0) {
|
||||||
|
if (typeof this.container !== "undefined") {
|
||||||
|
this.clearContainer();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let request_uri = this.baseurl + '/suggest/?type=tags&data=' + window.encodeURIComponent(tokens.join(" "));
|
||||||
|
let request = new HttpRequest('get', request_uri, {}, this.onReceive, this);
|
||||||
|
};
|
||||||
|
|
||||||
|
onReceive(response, self) {
|
||||||
|
self.openContainer();
|
||||||
|
self.clearContainer();
|
||||||
|
self.fillContainer(response);
|
||||||
|
};
|
||||||
|
|
||||||
|
openContainer() {
|
||||||
|
if (this.container) {
|
||||||
|
if (!this.container.parentNode) {
|
||||||
|
this.input.parentNode.appendChild(this.container);
|
||||||
|
}
|
||||||
|
return this.container;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.container = document.createElement('ul');
|
||||||
|
this.container.className = 'autosuggest';
|
||||||
|
this.input.parentNode.appendChild(this.container);
|
||||||
|
return this.container;
|
||||||
|
};
|
||||||
|
|
||||||
|
clearContainer() {
|
||||||
|
while (this.container.children.length > 0) {
|
||||||
|
this.container.removeChild(this.container.children[0]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
clearInput() {
|
||||||
|
this.input.value = "";
|
||||||
|
this.input.focus();
|
||||||
|
};
|
||||||
|
|
||||||
|
closeContainer() {
|
||||||
|
this.container.parentNode.removeChild(this.container);
|
||||||
|
};
|
||||||
|
|
||||||
|
fillContainer(response) {
|
||||||
|
this.selectedIndex = 0;
|
||||||
|
|
||||||
|
let query = this.input.value.trim().replace(/[\-\[\]{}()*+?.,\\\/^\$|#]/g, ' ');
|
||||||
|
let query_tokens = query.split(/ +/).sort((a,b) => a.length - b.length);
|
||||||
|
|
||||||
|
response.items.forEach((item, i) => {
|
||||||
|
let node = document.createElement('li');
|
||||||
|
node.innerHTML = this.highlightMatches(query_tokens, item.label);
|
||||||
|
node.jsondata = item;
|
||||||
|
node.addEventListener('click', event => {
|
||||||
|
this.appendCallback(node.jsondata);
|
||||||
|
this.closeContainer();
|
||||||
|
this.clearInput();
|
||||||
|
});
|
||||||
|
this.container.appendChild(node);
|
||||||
|
if (this.container.children.length === 1) {
|
||||||
|
node.className = 'selected';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
highlightMatches(query_tokens, item) {
|
||||||
|
let itemTokens = item.split(/ +/);
|
||||||
|
let queryTokens = new RegExp('(' + query_tokens.join('\|') + ')', 'i');
|
||||||
|
itemTokens.forEach((token, index) => {
|
||||||
|
item = item.replace(token, token.replace(queryTokens, ($1, match) => '<strong>' + match + '</strong>'));
|
||||||
|
});
|
||||||
|
return item;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
AutoSuggest.prototype.doSelection = function(event) {
|
class TagAutoSuggest extends AutoSuggest {
|
||||||
if (typeof this.container === "undefined" || this.container.children.length === 0) {
|
constructor(opt) {
|
||||||
return;
|
super(opt);
|
||||||
|
this.type = "tags";
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (event.keyCode) {
|
fillContainer(response) {
|
||||||
case 13: // Enter
|
if (response.items.length > 0) {
|
||||||
event.preventDefault();
|
super.fillContainer.call(this, response);
|
||||||
this.container.children[this.selectedIndex].click();
|
} else {
|
||||||
break;
|
let node = document.createElement('li')
|
||||||
|
node.innerHTML = "<em>Tag does not exist yet. Create it?</em>";
|
||||||
|
|
||||||
case 38: // Arrow up
|
node.addEventListener('click', event => {
|
||||||
case 40: // Arrow down
|
this.createNewTag(response => this.appendCallback(response));
|
||||||
event.preventDefault();
|
this.closeContainer();
|
||||||
this.findSelectedElement().className = '';
|
this.clearInput();
|
||||||
this.selectedIndex += event.keyCode === 38 ? -1 : 1;
|
});
|
||||||
if (this.selectedIndex < 0) {
|
|
||||||
this.selectedIndex = this.container.children.length - 1;
|
|
||||||
} else if (this.selectedIndex === this.container.children.length) {
|
|
||||||
this.selectedIndex = 0;
|
|
||||||
}
|
|
||||||
var new_el = this.findSelectedElement().className = 'selected';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
AutoSuggest.prototype.findSelectedElement = function() {
|
this.container.appendChild(node);
|
||||||
return this.container.children[this.selectedIndex];
|
this.selectedIndex = 0;
|
||||||
};
|
|
||||||
|
|
||||||
AutoSuggest.prototype.onType = function(input, event) {
|
|
||||||
if (event.keyCode === 13 || event.keyCode === 38 || event.keyCode === 40) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var tokens = input.value.split(/\s+/).filter(function(token) {
|
|
||||||
return token.length >= 2;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (tokens.length === 0) {
|
|
||||||
if (typeof this.container !== "undefined") {
|
|
||||||
this.clearContainer();
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var request_uri = this.baseurl + '/suggest/?type=tags&data=' + window.encodeURIComponent(tokens.join(" "));
|
|
||||||
var request = new HttpRequest('get', request_uri, {}, this.onReceive, this);
|
|
||||||
};
|
|
||||||
|
|
||||||
AutoSuggest.prototype.onReceive = function(response, self) {
|
|
||||||
self.openContainer();
|
|
||||||
self.clearContainer();
|
|
||||||
self.fillContainer(response);
|
|
||||||
};
|
|
||||||
|
|
||||||
AutoSuggest.prototype.openContainer = function() {
|
|
||||||
if (this.container) {
|
|
||||||
if (!this.container.parentNode) {
|
|
||||||
this.input.parentNode.appendChild(this.container);
|
|
||||||
}
|
|
||||||
return this.container;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.container = document.createElement('ul');
|
|
||||||
this.container.className = 'autosuggest';
|
|
||||||
this.input.parentNode.appendChild(this.container);
|
|
||||||
return this.container;
|
|
||||||
};
|
|
||||||
|
|
||||||
AutoSuggest.prototype.clearContainer = function() {
|
|
||||||
while (this.container.children.length > 0) {
|
|
||||||
this.container.removeChild(this.container.children[0]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
AutoSuggest.prototype.clearInput = function() {
|
|
||||||
this.input.value = "";
|
|
||||||
this.input.focus();
|
|
||||||
};
|
|
||||||
|
|
||||||
AutoSuggest.prototype.closeContainer = function() {
|
|
||||||
this.container.parentNode.removeChild(this.container);
|
|
||||||
};
|
|
||||||
|
|
||||||
AutoSuggest.prototype.fillContainer = function(response) {
|
|
||||||
var self = this;
|
|
||||||
this.selectedIndex = 0;
|
|
||||||
response.items.forEach(function(item, i) {
|
|
||||||
var node = document.createElement('li');
|
|
||||||
var text = document.createTextNode(item.label);
|
|
||||||
node.jsondata = item;
|
|
||||||
node.addEventListener('click', function(event) {
|
|
||||||
self.appendCallback(this.jsondata);
|
|
||||||
self.closeContainer();
|
|
||||||
self.clearInput();
|
|
||||||
});
|
|
||||||
node.appendChild(text);
|
|
||||||
self.container.appendChild(node);
|
|
||||||
if (self.container.children.length === 1) {
|
|
||||||
node.className = 'selected';
|
node.className = 'selected';
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
};
|
|
||||||
|
|
||||||
|
createNewTag(callback) {
|
||||||
function TagAutoSuggest(opt) {
|
let request_uri = this.baseurl + '/suggest/?type=createtag';
|
||||||
AutoSuggest.prototype.constructor.call(this, opt);
|
let request = new HttpRequest('post', request_uri, 'tag=' + encodeURIComponent(this.input.value), callback, this);
|
||||||
this.type = "tags";
|
|
||||||
}
|
|
||||||
|
|
||||||
TagAutoSuggest.prototype = Object.create(AutoSuggest.prototype);
|
|
||||||
|
|
||||||
TagAutoSuggest.prototype.constructor = TagAutoSuggest;
|
|
||||||
|
|
||||||
TagAutoSuggest.prototype.fillContainer = function(response) {
|
|
||||||
if (response.items.length > 0) {
|
|
||||||
AutoSuggest.prototype.fillContainer.call(this, response);
|
|
||||||
} else {
|
|
||||||
var node = document.createElement('li')
|
|
||||||
node.innerHTML = "<em>Tag does not exist yet. Create it?</em>";
|
|
||||||
|
|
||||||
var self = this;
|
|
||||||
node.addEventListener('click', function(event) {
|
|
||||||
self.createNewTag(function(response) {
|
|
||||||
console.log('Nieuwe tag!!');
|
|
||||||
console.log(response);
|
|
||||||
self.appendCallback(response);
|
|
||||||
});
|
|
||||||
self.closeContainer();
|
|
||||||
self.clearInput();
|
|
||||||
});
|
|
||||||
|
|
||||||
self.container.appendChild(node);
|
|
||||||
this.selectedIndex = 0;
|
|
||||||
node.className = 'selected';
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
TagAutoSuggest.prototype.createNewTag = function(callback) {
|
|
||||||
var request_uri = this.baseurl + '/suggest/?type=createtag';
|
|
||||||
var request = new HttpRequest('post', request_uri, 'tag=' + encodeURIComponent(this.input.value), callback, this);
|
|
||||||
}
|
}
|
||||||
|
|||||||
77
public/js/color-modes.js
Normal file
77
public/js/color-modes.js
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
/*!
|
||||||
|
* Color mode toggler for Bootstrap's docs (https://getbootstrap.com/)
|
||||||
|
* Copyright 2011-2023 The Bootstrap Authors
|
||||||
|
* Licensed under the Creative Commons Attribution 3.0 Unported License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
(() => {
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
const getStoredTheme = () => localStorage.getItem('theme');
|
||||||
|
const setStoredTheme = theme => localStorage.setItem('theme', theme);
|
||||||
|
|
||||||
|
const getPreferredTheme = () => {
|
||||||
|
const storedTheme = getStoredTheme();
|
||||||
|
if (storedTheme) {
|
||||||
|
return storedTheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||||
|
}
|
||||||
|
|
||||||
|
const setTheme = theme => {
|
||||||
|
if (theme === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||||
|
document.documentElement.setAttribute('data-bs-theme', 'dark');
|
||||||
|
} else {
|
||||||
|
document.documentElement.setAttribute('data-bs-theme', theme);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setTheme(getPreferredTheme());
|
||||||
|
|
||||||
|
const showActiveTheme = (theme, focus = false) => {
|
||||||
|
const themeSwitcher = document.querySelector('#bd-theme');
|
||||||
|
|
||||||
|
if (!themeSwitcher) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const themeSwitcherText = document.querySelector('#bd-theme-text');
|
||||||
|
const activeThemeIcon = document.querySelector('#theme-icon-active');
|
||||||
|
const btnToActive = document.querySelector(`[data-bs-theme-value="${theme}"]`);
|
||||||
|
const activeButtonIcon = btnToActive.querySelector('i.bi').className;
|
||||||
|
|
||||||
|
document.querySelectorAll('[data-bs-theme-value]').forEach(element => {
|
||||||
|
element.classList.remove('active');
|
||||||
|
});
|
||||||
|
|
||||||
|
btnToActive.classList.add('active');
|
||||||
|
activeThemeIcon.className = activeButtonIcon;
|
||||||
|
const themeSwitcherLabel = `${themeSwitcherText.textContent} (${btnToActive.dataset.bsThemeValue})`
|
||||||
|
|
||||||
|
if (focus) {
|
||||||
|
themeSwitcher.focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
|
||||||
|
const storedTheme = getStoredTheme()
|
||||||
|
if (storedTheme !== 'light' && storedTheme !== 'dark') {
|
||||||
|
setTheme(getPreferredTheme())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
window.addEventListener('DOMContentLoaded', () => {
|
||||||
|
showActiveTheme(getPreferredTheme())
|
||||||
|
|
||||||
|
document.querySelectorAll('[data-bs-theme-value]')
|
||||||
|
.forEach(toggle => {
|
||||||
|
toggle.addEventListener('click', () => {
|
||||||
|
const theme = toggle.getAttribute('data-bs-theme-value')
|
||||||
|
setStoredTheme(theme)
|
||||||
|
setTheme(theme)
|
||||||
|
showActiveTheme(theme, true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})()
|
||||||
@@ -1,218 +1,378 @@
|
|||||||
function CropEditor(opt) {
|
class CropEditor {
|
||||||
this.opt = opt;
|
constructor(opt) {
|
||||||
|
this.opt = opt;
|
||||||
|
|
||||||
this.edit_crop_button = document.createElement("span");
|
this.edit_crop_button = document.createElement("span");
|
||||||
this.edit_crop_button.className = "btn";
|
this.edit_crop_button.className = "btn btn-light";
|
||||||
this.edit_crop_button.innerHTML = "Edit crop";
|
this.edit_crop_button.textContent = "Edit crop";
|
||||||
this.edit_crop_button.addEventListener('click', this.show.bind(this));
|
this.edit_crop_button.addEventListener('click', this.show.bind(this));
|
||||||
|
|
||||||
this.thumbnail_select = document.getElementById(opt.thumbnail_select_id);
|
this.thumbnail_select = document.getElementById(opt.thumbnail_select_id);
|
||||||
this.thumbnail_select.addEventListener('change', this.toggleCropButton.bind(this));
|
this.thumbnail_select.addEventListener('change', this.toggleCropButton.bind(this));
|
||||||
this.thumbnail_select.parentNode.insertBefore(this.edit_crop_button, this.thumbnail_select.nextSibling);
|
this.thumbnail_select.parentNode.insertBefore(this.edit_crop_button, this.thumbnail_select.nextSibling);
|
||||||
|
|
||||||
this.toggleCropButton();
|
this.toggleCropButton();
|
||||||
}
|
}
|
||||||
|
|
||||||
CropEditor.prototype.buildContainer = function() {
|
initDOM() {
|
||||||
this.container = document.createElement("div");
|
this.container = document.createElement("div");
|
||||||
this.container.id = "crop_editor";
|
this.container.className = 'container-fluid';
|
||||||
|
this.container.id = "crop_editor";
|
||||||
|
|
||||||
this.position = document.createElement("div");
|
this.initPositionForm();
|
||||||
this.position.className = "crop_position";
|
this.initImageContainer();
|
||||||
this.container.appendChild(this.position);
|
|
||||||
|
|
||||||
var source_x_label = document.createTextNode("Source X:");
|
this.parent = document.getElementById(this.opt.editor_container_parent_id);
|
||||||
this.position.appendChild(source_x_label);
|
this.parent.appendChild(this.container);
|
||||||
|
}
|
||||||
|
|
||||||
this.source_x = document.createElement("input");
|
initPositionForm() {
|
||||||
this.source_x.addEventListener("keyup", this.positionBoundary.bind(this));
|
this.position = document.createElement("fieldset");
|
||||||
this.position.appendChild(this.source_x);
|
this.position.className = "crop_position flex-row justify-content-center";
|
||||||
|
this.container.appendChild(this.position);
|
||||||
|
|
||||||
var source_y_label = document.createTextNode("Source Y:");
|
const addNumericControl = (label, changeEvent) => {
|
||||||
this.position.appendChild(source_y_label);
|
const column = document.createElement('div');
|
||||||
|
column.className = 'col-auto';
|
||||||
|
this.position.appendChild(column);
|
||||||
|
|
||||||
this.source_y = document.createElement("input");
|
const group = document.createElement('div');
|
||||||
this.source_y.addEventListener("keyup", this.positionBoundary.bind(this));
|
group.className = 'input-group';
|
||||||
this.position.appendChild(this.source_y);
|
column.appendChild(group);
|
||||||
|
|
||||||
var crop_width_label = document.createTextNode("Crop width:");
|
const labelEl = document.createElement("span");
|
||||||
this.position.appendChild(crop_width_label);
|
labelEl.className = 'input-group-text';
|
||||||
|
labelEl.textContent = label;
|
||||||
|
group.appendChild(labelEl);
|
||||||
|
|
||||||
this.crop_width = document.createElement("input");
|
const control = document.createElement("input");
|
||||||
this.crop_width.addEventListener("keyup", this.positionBoundary.bind(this));
|
control.className = 'form-control';
|
||||||
this.position.appendChild(this.crop_width);
|
control.type = 'number';
|
||||||
|
control.addEventListener("change", changeEvent);
|
||||||
|
control.addEventListener("keyup", changeEvent);
|
||||||
|
group.appendChild(control);
|
||||||
|
|
||||||
var crop_height_label = document.createTextNode("Crop height:");
|
return control;
|
||||||
this.position.appendChild(crop_height_label);
|
};
|
||||||
|
|
||||||
this.crop_height = document.createElement("input");
|
this.source_x = addNumericControl("Source X:", this.positionBoundary);
|
||||||
this.crop_height.addEventListener("keyup", this.positionBoundary.bind(this));
|
this.source_y = addNumericControl("Source Y:", this.positionBoundary);
|
||||||
this.position.appendChild(this.crop_height);
|
this.crop_width = addNumericControl("Crop width:", this.positionBoundary);
|
||||||
|
this.crop_height = addNumericControl("Crop height:", this.positionBoundary);
|
||||||
|
|
||||||
this.save_button = document.createElement("span");
|
const otherColumn = document.createElement('div');
|
||||||
this.save_button.className = "btn";
|
otherColumn.className = 'col-auto text-nowrap';
|
||||||
this.save_button.innerHTML = "Save";
|
this.position.appendChild(otherColumn);
|
||||||
this.save_button.addEventListener('click', this.save.bind(this));
|
|
||||||
this.position.appendChild(this.save_button);
|
|
||||||
|
|
||||||
this.abort_button = document.createElement("span");
|
const constrainContainer = document.createElement("div");
|
||||||
this.abort_button.className = "btn btn-red";
|
constrainContainer.className = 'form-checkbox d-inline';
|
||||||
this.abort_button.innerHTML = "Abort";
|
otherColumn.appendChild(constrainContainer);
|
||||||
this.abort_button.addEventListener('click', this.hide.bind(this));
|
|
||||||
this.position.appendChild(this.abort_button);
|
|
||||||
|
|
||||||
this.image_container = document.createElement("div");
|
this.crop_constrain = document.createElement("input");
|
||||||
this.image_container.className = "crop_image_container";
|
this.crop_constrain.checked = true;
|
||||||
this.container.appendChild(this.image_container);
|
this.crop_constrain.className = 'form-check-input';
|
||||||
|
this.crop_constrain.id = 'check_constrain';
|
||||||
|
this.crop_constrain.type = 'checkbox';
|
||||||
|
constrainContainer.appendChild(this.crop_constrain);
|
||||||
|
|
||||||
this.crop_boundary = document.createElement("div");
|
this.crop_constrain_label = document.createElement("label");
|
||||||
this.crop_boundary.id = "crop_boundary";
|
this.crop_constrain_label.className = 'form-check-label';
|
||||||
this.image_container.appendChild(this.crop_boundary);
|
this.crop_constrain_label.htmlFor = 'check_constrain';
|
||||||
|
this.crop_constrain_label.textContent = 'Constrain proportions';
|
||||||
|
constrainContainer.appendChild(this.crop_constrain_label);
|
||||||
|
|
||||||
this.original_image = document.createElement("img");
|
this.save_button = document.createElement("span");
|
||||||
this.original_image.id = "original_image";
|
this.save_button.className = "btn btn-light";
|
||||||
this.original_image.src = this.opt.original_image_src;
|
this.save_button.textContent = "Save";
|
||||||
this.image_container.appendChild(this.original_image);
|
this.save_button.addEventListener('click', this.save.bind(this));
|
||||||
|
otherColumn.appendChild(this.save_button);
|
||||||
|
|
||||||
this.parent = document.getElementById(this.opt.editor_container_parent_id);
|
this.abort_button = document.createElement("span");
|
||||||
this.parent.appendChild(this.container);
|
this.abort_button.className = "btn btn-danger";
|
||||||
};
|
this.abort_button.textContent = "Abort";
|
||||||
|
this.abort_button.addEventListener('click', this.hide.bind(this));
|
||||||
|
otherColumn.appendChild(this.abort_button);
|
||||||
|
}
|
||||||
|
|
||||||
CropEditor.prototype.setInputValues = function() {
|
initImageContainer() {
|
||||||
var current = this.thumbnail_select.options[this.thumbnail_select.selectedIndex].dataset;
|
this.image_container = document.createElement("div");
|
||||||
|
this.image_container.className = "crop_image_container";
|
||||||
|
this.container.appendChild(this.image_container);
|
||||||
|
|
||||||
if (typeof current.crop_region === "undefined") {
|
this.crop_boundary = document.createElement("div");
|
||||||
var source_ratio = this.original_image.naturalWidth / this.original_image.naturalHeight,
|
this.crop_boundary.id = "crop_boundary";
|
||||||
crop_ratio = current.crop_width / current.crop_height,
|
this.image_container.appendChild(this.crop_boundary);
|
||||||
min_dim = Math.min(this.original_image.naturalWidth, this.original_image.naturalHeight);
|
|
||||||
|
this.original_image = document.createElement("img");
|
||||||
|
this.original_image.draggable = false;
|
||||||
|
this.original_image.id = "original_image";
|
||||||
|
this.original_image.src = this.opt.original_image_src;
|
||||||
|
this.image_container.appendChild(this.original_image);
|
||||||
|
}
|
||||||
|
|
||||||
|
setDefaultCrop(cropAspectRatio, cropMethod) {
|
||||||
|
let source = this.original_image;
|
||||||
|
let sourceAspectRatio = source.naturalWidth / source.naturalHeight;
|
||||||
|
|
||||||
// Cropping from the centre?
|
// Cropping from the centre?
|
||||||
if (current.crop_method === "c") {
|
if (cropMethod === "c" || cropMethod === "s") {
|
||||||
// Crop vertically from the centre, using the entire width.
|
// Crop vertically from the centre, using the entire width.
|
||||||
if (source_ratio < crop_ratio) {
|
if (sourceAspectRatio <= cropAspectRatio) {
|
||||||
this.crop_width.value = this.original_image.naturalWidth;
|
this.crop_width.value = source.naturalWidth;
|
||||||
this.crop_height.value = Math.ceil(this.original_image.naturalWidth / crop_ratio);
|
this.crop_height.value = Math.ceil(source.naturalWidth / cropAspectRatio);
|
||||||
this.source_x.value = 0;
|
this.source_x.value = 0;
|
||||||
this.source_y.value = Math.ceil((this.original_image.naturalHeight - this.crop_height.value) / 2);
|
this.source_y.value = Math.ceil((source.naturalHeight - this.crop_height.value) / 2);
|
||||||
}
|
}
|
||||||
// Crop horizontally from the centre, using the entire height.
|
// Crop horizontally from the centre, using the entire height.
|
||||||
else {
|
else {
|
||||||
this.crop_width.value = Math.ceil(current.crop_width * this.original_image.naturalHeight / current.crop_height);
|
this.crop_width.value = Math.ceil(cropAspectRatio * source.naturalHeight);
|
||||||
this.crop_height.value = this.original_image.naturalHeight;
|
this.crop_height.value = source.naturalHeight;
|
||||||
this.source_x.value = Math.ceil((this.original_image.naturalWidth - this.crop_width.value) / 2);
|
this.source_x.value = Math.ceil((source.naturalWidth - this.crop_width.value) / 2);
|
||||||
this.source_y.value = 0;
|
this.source_y.value = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Cropping a top or bottom slice?
|
// Cropping a top or bottom slice?
|
||||||
else {
|
else {
|
||||||
// Can we actually take a top or bottom slice from the original image?
|
// Can we actually take a top or bottom slice from the original image?
|
||||||
if (source_ratio < crop_ratio) {
|
if (sourceAspectRatio <= cropAspectRatio) {
|
||||||
this.crop_width.value = this.original_image.naturalWidth;
|
this.crop_width.value = source.naturalWidth;
|
||||||
this.crop_height.value = Math.floor(this.original_image.naturalHeight / crop_ratio);
|
this.crop_height.value = Math.floor(source.naturalWidth / cropAspectRatio);
|
||||||
this.source_x.value = "0";
|
this.source_x.value = "0";
|
||||||
this.source_y.value = current.crop_method.indexOf("t") !== -1 ? "0" : this.original_image.naturalHeight - this.crop_height.value;
|
this.source_y.value = cropMethod.indexOf("t") !== -1 ? "0" : source.naturalHeight - this.crop_height.value;
|
||||||
}
|
}
|
||||||
// Otherwise, take a vertical slice from the centre.
|
// Otherwise, take a vertical slice from the centre.
|
||||||
else {
|
else {
|
||||||
this.crop_width.value = Math.floor(this.original_image.naturalHeight * crop_ratio);
|
this.crop_width.value = Math.floor(source.naturalHeight * cropAspectRatio);
|
||||||
this.crop_height.value = this.original_image.naturalHeight;
|
this.crop_height.value = source.naturalHeight;
|
||||||
this.source_x.value = Math.floor((this.original_image.naturalWidth - this.crop_width.value) / 2);
|
this.source_x.value = Math.floor((source.naturalWidth - this.crop_width.value) / 2);
|
||||||
this.source_y.value = "0";
|
this.source_y.value = "0";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
var region = current.crop_region.split(',');
|
|
||||||
this.crop_width.value = region[0];
|
|
||||||
this.crop_height.value = region[1];
|
|
||||||
this.source_x.value = region[2];
|
|
||||||
this.source_y.value = region[3];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
CropEditor.prototype.showContainer = function() {
|
|
||||||
this.container.style.display = "block";
|
|
||||||
this.setInputValues();
|
|
||||||
this.positionBoundary();
|
|
||||||
}
|
|
||||||
|
|
||||||
CropEditor.prototype.save = function() {
|
|
||||||
var current = this.thumbnail_select.options[this.thumbnail_select.selectedIndex].dataset;
|
|
||||||
var payload = {
|
|
||||||
thumb_width: current.crop_width,
|
|
||||||
thumb_height: current.crop_height,
|
|
||||||
crop_method: current.crop_method,
|
|
||||||
crop_width: this.crop_width.value,
|
|
||||||
crop_height: this.crop_height.value,
|
|
||||||
source_x: this.source_x.value,
|
|
||||||
source_y: this.source_y.value
|
|
||||||
};
|
|
||||||
var req = HttpRequest("post", this.parent.action + "?id=" + this.opt.asset_id + "&updatethumb",
|
|
||||||
"data=" + encodeURIComponent(JSON.stringify(payload)), function(response) {
|
|
||||||
this.opt.after_save(response);
|
|
||||||
this.hide();
|
|
||||||
}.bind(this));
|
|
||||||
};
|
|
||||||
|
|
||||||
CropEditor.prototype.show = function() {
|
|
||||||
if (typeof this.container === "undefined") {
|
|
||||||
this.buildContainer();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Defer showing and positioning until image is loaded.
|
setPositionFormValues() {
|
||||||
// !!! TODO: add a spinner in the mean time?
|
let current = this.thumbnail_select.options[this.thumbnail_select.selectedIndex].dataset;
|
||||||
if (this.original_image.naturalWidth > 0) {
|
|
||||||
this.showContainer();
|
if (typeof current.crop_region === "undefined") {
|
||||||
} else {
|
let aspectRatio = current.crop_width / current.crop_height;
|
||||||
this.original_image.addEventListener("load", function() {
|
this.setDefaultCrop(aspectRatio, current.crop_method);
|
||||||
|
} else {
|
||||||
|
let region = current.crop_region.split(',');
|
||||||
|
this.crop_width.value = region[0];
|
||||||
|
this.crop_height.value = region[1];
|
||||||
|
this.source_x.value = region[2];
|
||||||
|
this.source_y.value = region[3];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.crop_width.min = 1;
|
||||||
|
this.crop_height.min = 1;
|
||||||
|
this.source_x.min = 0;
|
||||||
|
this.source_y.min = 0;
|
||||||
|
|
||||||
|
let source = this.original_image;
|
||||||
|
this.crop_width.max = source.naturalWidth;
|
||||||
|
this.crop_height.max = source.naturalHeight;
|
||||||
|
this.source_x.max = source.naturalWidth - 1;
|
||||||
|
this.source_y.max = source.naturalHeight - 1;
|
||||||
|
|
||||||
|
this.crop_constrain_label.textContent = `Constrain proportions (${current.crop_width} × ${current.crop_height})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
showContainer() {
|
||||||
|
this.container.style.display = '';
|
||||||
|
this.setPositionFormValues();
|
||||||
|
this.positionBoundary();
|
||||||
|
this.addEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
save() {
|
||||||
|
let current = this.thumbnail_select.options[this.thumbnail_select.selectedIndex].dataset;
|
||||||
|
let payload = {
|
||||||
|
thumb_width: current.crop_width,
|
||||||
|
thumb_height: current.crop_height,
|
||||||
|
crop_method: current.crop_method,
|
||||||
|
crop_width: this.crop_width.value,
|
||||||
|
crop_height: this.crop_height.value,
|
||||||
|
source_x: this.source_x.value,
|
||||||
|
source_y: this.source_y.value
|
||||||
|
};
|
||||||
|
let req = HttpRequest("post", this.opt.submitUrl + "?id=" + this.opt.asset_id + "&updatethumb",
|
||||||
|
"data=" + encodeURIComponent(JSON.stringify(payload)), function(response) {
|
||||||
|
this.opt.after_save(response);
|
||||||
|
this.hide();
|
||||||
|
}.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
show() {
|
||||||
|
if (typeof this.container === "undefined") {
|
||||||
|
this.initDOM();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defer showing and positioning until image is loaded.
|
||||||
|
// !!! TODO: add a spinner in the mean time?
|
||||||
|
if (this.original_image.naturalWidth > 0) {
|
||||||
this.showContainer();
|
this.showContainer();
|
||||||
}.bind(this));
|
} else {
|
||||||
|
this.original_image.addEventListener("load", event => this.showContainer());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
CropEditor.prototype.hide = function() {
|
hide() {
|
||||||
this.container.style.display = "none";
|
this.container.style.display = "none";
|
||||||
};
|
}
|
||||||
|
|
||||||
CropEditor.prototype.addEvents = function(event) {
|
addEvents(event) {
|
||||||
var drag_target = document.getElementById(opt.drag_target);
|
let cropTarget = this.image_container;
|
||||||
drag_target.addEventListener('dragstart', this.dragStart);
|
cropTarget.addEventListener('mousedown', this.cropSelectionStart.bind(this));
|
||||||
drag_target.addEventListener('drag', this.drag);
|
cropTarget.addEventListener('mousemove', this.cropSelection.bind(this));
|
||||||
drag_target.addEventListener('dragend', this.dragEnd);
|
cropTarget.addEventListener('mouseup', this.cropSelectionEnd.bind(this));
|
||||||
};
|
// cropTarget.addEventListener('mouseout', this.cropSelectionEnd.bind(this));
|
||||||
|
|
||||||
CropEditor.prototype.dragStart = function(event) {
|
this.original_image.addEventListener('mousedown', event => {return false});
|
||||||
console.log(event);
|
this.original_image.addEventListener('dragstart', event => {return false});
|
||||||
event.preventDefault();
|
|
||||||
};
|
|
||||||
|
|
||||||
CropEditor.prototype.dragEnd = function(event) {
|
let moveTarget = this.crop_boundary;
|
||||||
console.log(event);
|
moveTarget.addEventListener('mousedown', this.moveSelectionStart.bind(this));
|
||||||
};
|
moveTarget.addEventListener('mousemove', this.moveSelection.bind(this));
|
||||||
|
moveTarget.addEventListener('mouseup', this.moveSelectionEnd.bind(this));
|
||||||
|
|
||||||
CropEditor.prototype.drag = function(event) {
|
window.addEventListener('resize', this.positionBoundary.bind(this));
|
||||||
console.log(event);
|
}
|
||||||
};
|
|
||||||
|
|
||||||
CropEditor.prototype.toggleCropButton = function() {
|
cropSelectionStart(event) {
|
||||||
var current = this.thumbnail_select.options[this.thumbnail_select.selectedIndex].dataset;
|
if (this.isMoving) {
|
||||||
this.edit_crop_button.style.display = typeof current.crop_method === "undefined" ? "none" : "";
|
return false;
|
||||||
};
|
}
|
||||||
|
|
||||||
CropEditor.prototype.positionBoundary = function(event) {
|
let dragStartX = event.x - this.image_container.offsetLeft;
|
||||||
var source_x = parseInt(this.source_x.value),
|
let dragStartY = event.y - this.image_container.offsetTop;
|
||||||
source_y = parseInt(this.source_y.value),
|
|
||||||
crop_width = parseInt(this.crop_width.value),
|
|
||||||
crop_height = parseInt(this.crop_height.value),
|
|
||||||
real_width = this.original_image.naturalWidth,
|
|
||||||
real_height = this.original_image.naturalHeight,
|
|
||||||
scaled_width = this.original_image.clientWidth,
|
|
||||||
scaled_height = this.original_image.clientHeight;
|
|
||||||
|
|
||||||
var width_scale = scaled_width / real_width,
|
if (dragStartX > this.original_image.clientWidth ||
|
||||||
height_scale = scaled_height / real_height;
|
dragStartY > this.original_image.clientHeight) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
crop_boundary.style.left = (this.source_x.value) * width_scale + "px";
|
this.isDragging = true;
|
||||||
crop_boundary.style.top = (this.source_y.value) * height_scale + "px";
|
this.dragStartX = dragStartX;
|
||||||
crop_boundary.style.width = (this.crop_width.value) * width_scale + "px";
|
this.dragStartY = dragStartY;
|
||||||
crop_boundary.style.height = (this.crop_height.value) * height_scale + "px";
|
}
|
||||||
};
|
|
||||||
|
cropSelectionEnd(event) {
|
||||||
|
this.isDragging = false;
|
||||||
|
this.handleCropSelectionEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
cropSelection(event) {
|
||||||
|
this.handleCropSelectionEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
getScaleFactor() {
|
||||||
|
return this.original_image.naturalWidth / this.original_image.clientWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCropSelectionEvent(event) {
|
||||||
|
if (!this.isDragging) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dragEndX = event.x - this.image_container.offsetLeft;
|
||||||
|
this.dragEndY = event.y - this.image_container.offsetTop;
|
||||||
|
|
||||||
|
let scaleFactor = this.getScaleFactor();
|
||||||
|
|
||||||
|
this.source_x.value = Math.ceil(Math.min(this.dragStartX, this.dragEndX) * scaleFactor);
|
||||||
|
this.source_y.value = Math.ceil(Math.min(this.dragStartY, this.dragEndY) * scaleFactor);
|
||||||
|
|
||||||
|
let width = Math.ceil(Math.abs(this.dragEndX - this.dragStartX) * scaleFactor);
|
||||||
|
this.crop_width.value = Math.min(width, this.original_image.naturalWidth - this.source_x.value);
|
||||||
|
|
||||||
|
let height = Math.ceil(Math.abs(this.dragEndY - this.dragStartY) * scaleFactor);
|
||||||
|
this.crop_height.value = Math.min(height, this.original_image.naturalHeight - this.source_y.value);
|
||||||
|
|
||||||
|
if (this.crop_constrain.checked) {
|
||||||
|
let current = this.thumbnail_select.options[this.thumbnail_select.selectedIndex].dataset;
|
||||||
|
|
||||||
|
let currentAspectRatio = parseInt(this.crop_width.value) / parseInt(this.crop_height.value);
|
||||||
|
let targetAspectRatio = current.crop_width / current.crop_height;
|
||||||
|
|
||||||
|
if (Math.abs(currentAspectRatio - targetAspectRatio) > 0.001) {
|
||||||
|
// Landscape?
|
||||||
|
if (targetAspectRatio > 1.0) {
|
||||||
|
let height = Math.ceil(this.crop_width.value / targetAspectRatio);
|
||||||
|
if (parseInt(this.source_y.value) + height > this.original_image.naturalHeight) {
|
||||||
|
height = this.original_image.naturalHeight - this.source_y.value;
|
||||||
|
}
|
||||||
|
this.crop_width.value = height * targetAspectRatio;
|
||||||
|
this.crop_height.value = height;
|
||||||
|
}
|
||||||
|
// Portrait?
|
||||||
|
else {
|
||||||
|
let width = Math.ceil(this.crop_height.value * targetAspectRatio);
|
||||||
|
if (parseInt(this.source_x.value) + width > this.original_image.naturalWidth) {
|
||||||
|
width = this.original_image.naturalWidth - this.source_x.value;
|
||||||
|
}
|
||||||
|
this.crop_width.value = width;
|
||||||
|
this.crop_height.value = width / targetAspectRatio;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.positionBoundary();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCropMoveEvent(event) {
|
||||||
|
if (!this.isMoving) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dragEndX = event.x - this.crop_boundary.offsetLeft;
|
||||||
|
this.dragEndY = event.y - this.crop_boundary.offsetTop;
|
||||||
|
|
||||||
|
let scaleFactor = this.getScaleFactor();
|
||||||
|
|
||||||
|
let x = parseInt(this.source_x.value) + Math.ceil((this.dragEndX - this.dragStartX) * scaleFactor);
|
||||||
|
if (x + parseInt(this.crop_width.value) > this.original_image.naturalWidth) {
|
||||||
|
x += this.original_image.naturalWidth - (x + parseInt(this.crop_width.value));
|
||||||
|
}
|
||||||
|
this.source_x.value = Math.max(x, 0);
|
||||||
|
|
||||||
|
let y = parseInt(this.source_y.value) + Math.ceil((this.dragEndY - this.dragStartY) * scaleFactor);
|
||||||
|
if (y + parseInt(this.crop_height.value) > this.original_image.naturalHeight) {
|
||||||
|
y += this.original_image.naturalHeight - (y + parseInt(this.crop_height.value));
|
||||||
|
}
|
||||||
|
this.source_y.value = Math.max(y, 0);
|
||||||
|
|
||||||
|
this.positionBoundary();
|
||||||
|
}
|
||||||
|
|
||||||
|
moveSelectionStart(event) {
|
||||||
|
if (this.isDragging) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this.isMoving = true;
|
||||||
|
this.dragStartX = event.x - this.crop_boundary.offsetLeft;
|
||||||
|
this.dragStartY = event.y - this.crop_boundary.offsetTop;
|
||||||
|
}
|
||||||
|
|
||||||
|
moveSelectionEnd(event) {
|
||||||
|
this.isMoving = false;
|
||||||
|
this.handleCropMoveEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
moveSelection(event) {
|
||||||
|
this.handleCropMoveEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleCropButton() {
|
||||||
|
let current = this.thumbnail_select.options[this.thumbnail_select.selectedIndex].dataset;
|
||||||
|
this.edit_crop_button.style.display = typeof current.crop_method === "undefined" ? "none" : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
positionBoundary(event) {
|
||||||
|
let scaleFactor = this.getScaleFactor();
|
||||||
|
crop_boundary.style.left = parseInt(this.source_x.value) / scaleFactor + "px";
|
||||||
|
crop_boundary.style.top = parseInt(this.source_y.value) / scaleFactor + "px";
|
||||||
|
crop_boundary.style.width = parseInt(this.crop_width.value) / scaleFactor + "px";
|
||||||
|
crop_boundary.style.height = parseInt(this.crop_height.value) / scaleFactor + "px";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,14 +4,14 @@ function enableKeyDownNavigation() {
|
|||||||
var target = document.getElementById("previous_photo").href;
|
var target = document.getElementById("previous_photo").href;
|
||||||
if (target) {
|
if (target) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
document.location.href = target + '#photo_frame';
|
document.location.href = target;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (event.keyCode == 39) {
|
else if (event.keyCode == 39) {
|
||||||
var target = document.getElementById("next_photo").href;
|
var target = document.getElementById("next_photo").href;
|
||||||
if (target) {
|
if (target) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
document.location.href = target + '#photo_frame';
|
document.location.href = target;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, false);
|
}, false);
|
||||||
|
|||||||
@@ -1,186 +1,211 @@
|
|||||||
function UploadQueue(options) {
|
class UploadQueue {
|
||||||
this.queue = options.queue_element;
|
constructor(options) {
|
||||||
this.preview_area = options.preview_area;
|
this.queue = options.queue_element;
|
||||||
this.upload_progress = [];
|
this.preview_area = options.preview_area;
|
||||||
this.upload_url = options.upload_url;
|
this.upload_progress = [];
|
||||||
this.submit = options.submit_button;
|
this.upload_url = options.upload_url;
|
||||||
this.addEvents();
|
this.submit = options.submit_button;
|
||||||
}
|
this.addEvents();
|
||||||
|
}
|
||||||
|
|
||||||
UploadQueue.prototype.addEvents = function() {
|
addEvents() {
|
||||||
var that = this;
|
this.queue.addEventListener('change', event => {
|
||||||
that.queue.addEventListener('change', function() {
|
this.showSpinner(this.queue, "Generating previews (not uploading yet!)");
|
||||||
that.showSpinner(that.queue, "Generating previews (not uploading yet!)");
|
this.clearPreviews();
|
||||||
that.clearPreviews();
|
for (let i = 0; i < this.queue.files.length; i++) {
|
||||||
for (var i = 0; i < that.queue.files.length; i++) {
|
const callback = (i !== this.queue.files.length - 1) ? null : () => {
|
||||||
var callback = (i !== that.queue.files.length - 1) ? null : function() {
|
this.hideSpinner();
|
||||||
that.hideSpinner();
|
this.submit.disabled = false;
|
||||||
that.submit.disabled = false;
|
};
|
||||||
|
|
||||||
|
if (this.queue.files[0].name.toUpperCase().endsWith(".HEIC")) {
|
||||||
|
alert('Sorry, the HEIC image format is not supported.\nPlease convert your photos to JPEG before uploading.');
|
||||||
|
this.hideSpinner();
|
||||||
|
this.submit.disabled = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.addPreviewBoxForQueueSlot(i);
|
||||||
|
this.addPreviewForFile(this.queue.files[i], i, callback);
|
||||||
};
|
};
|
||||||
that.addPreviewBoxForQueueSlot(i);
|
|
||||||
that.addPreviewForFile(that.queue.files[i], i, callback);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
that.submit.addEventListener('click', function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
that.process();
|
|
||||||
});
|
|
||||||
this.submit.disabled = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
UploadQueue.prototype.clearPreviews = function() {
|
|
||||||
this.preview_area.innerHTML = '';
|
|
||||||
this.submit.disabled = true;
|
|
||||||
this.current_upload_index = -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
UploadQueue.prototype.addPreviewBoxForQueueSlot = function(index) {
|
|
||||||
var preview_box = document.createElement('div');
|
|
||||||
preview_box.id = 'upload_preview_' + index;
|
|
||||||
this.preview_area.appendChild(preview_box);
|
|
||||||
};
|
|
||||||
|
|
||||||
UploadQueue.prototype.addPreviewForFile = function(file, index, callback) {
|
|
||||||
if (!file) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var preview = document.createElement('img');
|
|
||||||
preview.title = file.name;
|
|
||||||
preview.style.maxHeight = '150px';
|
|
||||||
|
|
||||||
var preview_box = document.getElementById('upload_preview_' + index);
|
|
||||||
preview_box.appendChild(preview);
|
|
||||||
|
|
||||||
var reader = new FileReader();
|
|
||||||
var that = this;
|
|
||||||
reader.addEventListener('load', function() {
|
|
||||||
preview.src = reader.result;
|
|
||||||
if (callback) {
|
|
||||||
preview.addEventListener('load', function() {
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, false);
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
};
|
|
||||||
|
|
||||||
UploadQueue.prototype.process = function() {
|
|
||||||
this.showSpinner(this.submit, "Preparing to upload files...");
|
|
||||||
if (this.queue.files.length > 0) {
|
|
||||||
this.submit.disabled = true;
|
|
||||||
this.nextFile();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
UploadQueue.prototype.nextFile = function() {
|
|
||||||
var files = this.queue.files;
|
|
||||||
var i = ++this.current_upload_index;
|
|
||||||
if (i === files.length) {
|
|
||||||
this.hideSpinner();
|
|
||||||
} else {
|
|
||||||
this.setSpinnerLabel("Uploading file " + (i + 1) + " out of " + files.length);
|
|
||||||
this.sendFile(files[i], i, function() {
|
|
||||||
this.nextFile();
|
|
||||||
});
|
});
|
||||||
|
this.submit.addEventListener('click', event => {
|
||||||
|
event.preventDefault();
|
||||||
|
this.process();
|
||||||
|
});
|
||||||
|
this.submit.disabled = true;
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
UploadQueue.prototype.sendFile = function(file, index, callback) {
|
clearPreviews() {
|
||||||
// Prepare the request.
|
this.preview_area.innerHTML = '';
|
||||||
var that = this;
|
this.submit.disabled = true;
|
||||||
var request = new XMLHttpRequest();
|
this.current_upload_index = -1;
|
||||||
request.addEventListener('error', function(event) {
|
}
|
||||||
that.updateProgress(index, -1);
|
|
||||||
});
|
addPreviewBoxForQueueSlot(index) {
|
||||||
request.addEventListener('progress', function(event) {
|
const preview_box = document.createElement('div');
|
||||||
that.updateProgress(index, event.loaded / event.total);
|
preview_box.id = 'upload_preview_' + index;
|
||||||
});
|
this.preview_area.appendChild(preview_box);
|
||||||
request.addEventListener('load', function(event) {
|
}
|
||||||
that.updateProgress(index, 1);
|
|
||||||
if (request.responseText !== null && request.status === 200) {
|
addPreviewForFile(file, index, callback) {
|
||||||
var obj = JSON.parse(request.responseText);
|
if (!file) {
|
||||||
if (obj.error) {
|
return false;
|
||||||
alert(obj.error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
else if (callback) {
|
|
||||||
callback.call(that, obj);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
var data = new FormData();
|
const preview = document.createElement('canvas');
|
||||||
data.append('uploads', file, file.name);
|
preview.title = file.name;
|
||||||
|
|
||||||
request.open('POST', this.upload_url, true);
|
const preview_box = document.getElementById('upload_preview_' + index);
|
||||||
request.send(data);
|
preview_box.appendChild(preview);
|
||||||
};
|
|
||||||
|
|
||||||
UploadQueue.prototype.addProgressBar = function(index) {
|
const reader = new FileReader();
|
||||||
if (index in this.upload_progress) {
|
reader.addEventListener('load', event => {
|
||||||
return;
|
const original = document.createElement('img');
|
||||||
|
original.src = reader.result;
|
||||||
|
|
||||||
|
original.addEventListener('load', function() {
|
||||||
|
// Preparation: make canvas size proportional to the original image.
|
||||||
|
preview.height = 150;
|
||||||
|
preview.width = preview.height * (original.width / original.height);
|
||||||
|
|
||||||
|
// First pass: resize to 50% on temp canvas.
|
||||||
|
const temp = document.createElement('canvas'),
|
||||||
|
tempCtx = temp.getContext('2d');
|
||||||
|
|
||||||
|
temp.width = original.width * 0.5;
|
||||||
|
temp.height = original.height * 0.5;
|
||||||
|
tempCtx.drawImage(original, 0, 0, temp.width, temp.height);
|
||||||
|
|
||||||
|
// Second pass: resize again on temp canvas.
|
||||||
|
tempCtx.drawImage(temp, 0, 0, temp.width * 0.5, temp.height * 0.5);
|
||||||
|
|
||||||
|
// Final pass: resize to desired size on preview canvas.
|
||||||
|
const context = preview.getContext('2d');
|
||||||
|
context.drawImage(temp, 0, 0, temp.width * 0.5, temp.height * 0.5,
|
||||||
|
0, 0, preview.width, preview.height);
|
||||||
|
|
||||||
|
if (callback) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, false);
|
||||||
|
reader.readAsDataURL(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
var progress_container = document.createElement('div');
|
process() {
|
||||||
progress_container.className = 'progress';
|
this.showSpinner(this.submit, "Preparing to upload files...");
|
||||||
|
if (this.queue.files.length > 0) {
|
||||||
var progress = document.createElement('div');
|
this.submit.disabled = true;
|
||||||
progress_container.appendChild(progress);
|
this.nextFile();
|
||||||
|
|
||||||
var preview_box = document.getElementById('upload_preview_' + index);
|
|
||||||
preview_box.appendChild(progress_container);
|
|
||||||
|
|
||||||
this.upload_progress[index] = progress;
|
|
||||||
};
|
|
||||||
|
|
||||||
UploadQueue.prototype.updateProgress = function(index, progress) {
|
|
||||||
if (!(index in this.upload_progress)) {
|
|
||||||
this.addProgressBar(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
var bar = this.upload_progress[index];
|
|
||||||
|
|
||||||
if (progress >= 0) {
|
|
||||||
bar.style.width = Math.ceil(progress * 100) + '%';
|
|
||||||
} else {
|
|
||||||
bar.style.width = "";
|
|
||||||
if (progress === -1) {
|
|
||||||
bar.className = "error";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
UploadQueue.prototype.showSpinner = function(sibling, label) {
|
nextFile() {
|
||||||
if (this.spinner) {
|
const files = this.queue.files;
|
||||||
return;
|
const i = ++this.current_upload_index;
|
||||||
|
if (i === files.length) {
|
||||||
|
this.hideSpinner();
|
||||||
|
} else {
|
||||||
|
this.setSpinnerLabel("Uploading file " + (i + 1) + " out of " + files.length);
|
||||||
|
this.sendFile(files[i], i, this.nextFile);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.spinner = document.createElement('div');
|
sendFile(file, index, callback) {
|
||||||
this.spinner.className = 'spinner';
|
const request = new XMLHttpRequest();
|
||||||
sibling.parentNode.appendChild(this.spinner);
|
request.addEventListener('error', event => {
|
||||||
|
this.updateProgress(index, -1);
|
||||||
|
});
|
||||||
|
request.addEventListener('progress', event => {
|
||||||
|
this.updateProgress(index, event.loaded / event.total);
|
||||||
|
});
|
||||||
|
request.addEventListener('load', event => {
|
||||||
|
this.updateProgress(index, 1);
|
||||||
|
if (request.responseText !== null && request.status === 200) {
|
||||||
|
const obj = JSON.parse(request.responseText);
|
||||||
|
if (obj.error) {
|
||||||
|
alert(obj.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else if (callback) {
|
||||||
|
callback.call(this, obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (label) {
|
const data = new FormData();
|
||||||
this.spinner_label = document.createElement('span');
|
data.append('uploads', file, file.name);
|
||||||
this.spinner_label.className = 'spinner_label';
|
|
||||||
this.spinner_label.innerHTML = label;
|
request.open('POST', this.upload_url, true);
|
||||||
sibling.parentNode.appendChild(this.spinner_label);
|
request.send(data);
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
UploadQueue.prototype.setSpinnerLabel = function(label) {
|
addProgressBar(index) {
|
||||||
if (this.spinner_label) {
|
if (index in this.upload_progress) {
|
||||||
this.spinner_label.innerHTML = label;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const progress_container = document.createElement('div');
|
||||||
|
progress_container.className = 'progress';
|
||||||
|
|
||||||
|
const progress = document.createElement('div');
|
||||||
|
progress_container.appendChild(progress);
|
||||||
|
|
||||||
|
const preview_box = document.getElementById('upload_preview_' + index);
|
||||||
|
preview_box.appendChild(progress_container);
|
||||||
|
|
||||||
|
this.upload_progress[index] = progress;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateProgress(index, progress) {
|
||||||
|
if (!(index in this.upload_progress)) {
|
||||||
|
this.addProgressBar(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
const bar = this.upload_progress[index];
|
||||||
|
|
||||||
|
if (progress >= 0) {
|
||||||
|
bar.style.width = Math.ceil(progress * 100) + '%';
|
||||||
|
} else {
|
||||||
|
bar.style.width = "";
|
||||||
|
if (progress === -1) {
|
||||||
|
bar.className = "error";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showSpinner(sibling, label) {
|
||||||
|
if (this.spinner) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.spinner = document.createElement('div');
|
||||||
|
this.spinner.className = 'spinner';
|
||||||
|
sibling.parentNode.appendChild(this.spinner);
|
||||||
|
|
||||||
|
if (label) {
|
||||||
|
this.spinner_label = document.createElement('span');
|
||||||
|
this.spinner_label.className = 'spinner_label';
|
||||||
|
this.spinner_label.innerHTML = label;
|
||||||
|
sibling.parentNode.appendChild(this.spinner_label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setSpinnerLabel(label) {
|
||||||
|
if (this.spinner_label) {
|
||||||
|
this.spinner_label.innerHTML = label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hideSpinner() {
|
||||||
|
if (this.spinner) {
|
||||||
|
this.spinner.parentNode.removeChild(this.spinner);
|
||||||
|
this.spinner = null;
|
||||||
|
}
|
||||||
|
if (this.spinner_label) {
|
||||||
|
this.spinner_label.parentNode.removeChild(this.spinner_label);
|
||||||
|
this.spinner_label = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
UploadQueue.prototype.hideSpinner = function() {
|
|
||||||
if (this.spinner) {
|
|
||||||
this.spinner.parentNode.removeChild(this.spinner);
|
|
||||||
this.spinner = null;
|
|
||||||
}
|
|
||||||
if (this.spinner_label) {
|
|
||||||
this.spinner_label.parentNode.removeChild(this.spinner_label);
|
|
||||||
this.spinner_label = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|||||||
1
public/vendor
Symbolic link
1
public/vendor
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../vendor/
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
<?php
|
|
||||||
/*****************************************************************************
|
|
||||||
* AdminBar.php
|
|
||||||
* Defines the AdminBar class.
|
|
||||||
*
|
|
||||||
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
|
||||||
*****************************************************************************/
|
|
||||||
|
|
||||||
class AdminBar extends SubTemplate
|
|
||||||
{
|
|
||||||
private $extra_items = [];
|
|
||||||
|
|
||||||
protected function html_content()
|
|
||||||
{
|
|
||||||
echo '
|
|
||||||
<div id="admin_bar">
|
|
||||||
<ul>
|
|
||||||
<li><a href="', BASEURL, '/managetags/">Tags</a></li>
|
|
||||||
<li><a href="', BASEURL, '/manageusers/">Users</a></li>
|
|
||||||
<li><a href="', BASEURL, '/manageerrors/">Errors [', ErrorLog::getCount(), ']</a></li>';
|
|
||||||
|
|
||||||
foreach ($this->extra_items as $item)
|
|
||||||
echo '
|
|
||||||
<li><a href="', $item[0], '">', $item[1], '</a></li>';
|
|
||||||
|
|
||||||
echo '
|
|
||||||
<li><a href="', BASEURL, '/logout/">Log out [', Registry::get('user')->getFullName(), ']</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>';
|
|
||||||
}
|
|
||||||
|
|
||||||
public function appendItem($url, $caption)
|
|
||||||
{
|
|
||||||
$this->extra_items[] = [$url, $caption];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,21 +6,23 @@
|
|||||||
* Kabuki CMS (C) 2013-2016, Aaron van Geffen
|
* Kabuki CMS (C) 2013-2016, Aaron van Geffen
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
class AlbumButtonBox extends SubTemplate
|
class AlbumButtonBox extends Template
|
||||||
{
|
{
|
||||||
|
private $buttons;
|
||||||
|
|
||||||
public function __construct($buttons)
|
public function __construct($buttons)
|
||||||
{
|
{
|
||||||
$this->buttons = $buttons;
|
$this->buttons = $buttons;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function html_content()
|
public function html_main()
|
||||||
{
|
{
|
||||||
echo '
|
echo '
|
||||||
<div class="album_button_box">';
|
<div class="album_button_box">';
|
||||||
|
|
||||||
foreach ($this->buttons as $button)
|
foreach ($this->buttons as $button)
|
||||||
echo '
|
echo '
|
||||||
<a href="', $button['url'], '">', $button['caption'], '</a>';
|
<a class="btn btn-light" href="', $button['url'], '">', $button['caption'], '</a>';
|
||||||
|
|
||||||
echo '
|
echo '
|
||||||
</div>';
|
</div>';
|
||||||
|
|||||||
@@ -6,8 +6,13 @@
|
|||||||
* Kabuki CMS (C) 2013-2016, Aaron van Geffen
|
* Kabuki CMS (C) 2013-2016, Aaron van Geffen
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
class AlbumHeaderBox extends SubTemplate
|
class AlbumHeaderBox extends Template
|
||||||
{
|
{
|
||||||
|
private $back_link_title;
|
||||||
|
private $back_link;
|
||||||
|
private $description;
|
||||||
|
private $title;
|
||||||
|
|
||||||
public function __construct($title, $description, $back_link, $back_link_title)
|
public function __construct($title, $description, $back_link, $back_link_title)
|
||||||
{
|
{
|
||||||
$this->title = $title;
|
$this->title = $title;
|
||||||
@@ -16,11 +21,13 @@ class AlbumHeaderBox extends SubTemplate
|
|||||||
$this->back_link_title = $back_link_title;
|
$this->back_link_title = $back_link_title;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function html_content()
|
public function html_main()
|
||||||
{
|
{
|
||||||
echo '
|
echo '
|
||||||
<div class="album_title_box">
|
<div class="album_title_box">
|
||||||
<a class="back_button" href="', $this->back_link, '" title="', $this->back_link_title, '">←</a>
|
<a class="back_button" href="', $this->back_link, '" title="', $this->back_link_title, '">
|
||||||
|
<i class="bi bi-arrow-left"></i>
|
||||||
|
</a>
|
||||||
<div>
|
<div>
|
||||||
<h2>', $this->title, '</h2>';
|
<h2>', $this->title, '</h2>';
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
class AlbumIndex extends SubTemplate
|
class AlbumIndex extends Template
|
||||||
{
|
{
|
||||||
protected $albums;
|
protected $albums;
|
||||||
protected $show_edit_buttons;
|
protected $show_edit_buttons;
|
||||||
@@ -14,7 +14,8 @@ class AlbumIndex extends SubTemplate
|
|||||||
protected $row_limit = 1000;
|
protected $row_limit = 1000;
|
||||||
|
|
||||||
const TILE_WIDTH = 400;
|
const TILE_WIDTH = 400;
|
||||||
const TILE_HEIGHT = 267;
|
const TILE_HEIGHT = 300;
|
||||||
|
const TILE_RATIO = self::TILE_WIDTH / self::TILE_HEIGHT;
|
||||||
|
|
||||||
public function __construct(array $albums, $show_edit_buttons = false, $show_labels = true)
|
public function __construct(array $albums, $show_edit_buttons = false, $show_labels = true)
|
||||||
{
|
{
|
||||||
@@ -23,53 +24,59 @@ class AlbumIndex extends SubTemplate
|
|||||||
$this->show_labels = $show_labels;
|
$this->show_labels = $show_labels;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function html_content()
|
public function html_main()
|
||||||
{
|
{
|
||||||
echo '
|
echo '
|
||||||
<div class="tiled_grid">';
|
<div class="container album-index">
|
||||||
|
<div class="row g-5">';
|
||||||
|
|
||||||
foreach (array_chunk($this->albums, 3) as $photos)
|
foreach ($this->albums as $album)
|
||||||
{
|
$this->renderAlbum($album);
|
||||||
echo '
|
|
||||||
<div class="tiled_row">';
|
|
||||||
|
|
||||||
foreach ($photos as $album)
|
|
||||||
{
|
|
||||||
$color = isset($album['thumbnail']) ? $album['thumbnail']->bestColor() : 'ccc';
|
|
||||||
if ($color == 'FFFFFF')
|
|
||||||
$color = 'ccc';
|
|
||||||
|
|
||||||
echo '
|
|
||||||
<div class="landscape" style="border-color: #', $color, '">';
|
|
||||||
|
|
||||||
if ($this->show_edit_buttons)
|
|
||||||
echo '
|
|
||||||
<a class="edit" href="#">Edit</a>';
|
|
||||||
|
|
||||||
echo '
|
|
||||||
<a href="', $album['link'], '">';
|
|
||||||
|
|
||||||
if (isset($album['thumbnail']))
|
|
||||||
echo '
|
|
||||||
<img src="', $album['thumbnail']->getThumbnailUrl(static::TILE_WIDTH, static::TILE_HEIGHT, true, true), '" alt="">';
|
|
||||||
else
|
|
||||||
echo '
|
|
||||||
<img src="', BASEURL, '/images/nothumb.png" alt="">';
|
|
||||||
|
|
||||||
if ($this->show_labels)
|
|
||||||
echo '
|
|
||||||
<h4>', $album['caption'], '</h4>';
|
|
||||||
|
|
||||||
echo '
|
|
||||||
</a>
|
|
||||||
</div>';
|
|
||||||
}
|
|
||||||
|
|
||||||
echo '
|
|
||||||
</div>';
|
|
||||||
}
|
|
||||||
|
|
||||||
echo '
|
echo '
|
||||||
|
</div>
|
||||||
</div>';
|
</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function renderAlbum(array $album)
|
||||||
|
{
|
||||||
|
echo '
|
||||||
|
<div class="col-md-6 col-xl-4">
|
||||||
|
<div class="polaroid landscape">';
|
||||||
|
|
||||||
|
if ($this->show_edit_buttons)
|
||||||
|
echo '
|
||||||
|
<a class="edit" href="#">Edit</a>';
|
||||||
|
|
||||||
|
echo '
|
||||||
|
<a href="', $album['link'], '">';
|
||||||
|
|
||||||
|
if (isset($album['thumbnail']))
|
||||||
|
{
|
||||||
|
$thumbs = [];
|
||||||
|
foreach ([1, 2] as $factor)
|
||||||
|
$thumbs[$factor] = $album['thumbnail']->getThumbnailUrl(
|
||||||
|
static::TILE_WIDTH * $factor, static::TILE_HEIGHT * $factor, true, true);
|
||||||
|
|
||||||
|
echo '
|
||||||
|
<img alt="" src="', $thumbs[1], '"' . (isset($thumbs[2]) ?
|
||||||
|
' srcset="' . $thumbs[2] . ' 2x"' : '') .
|
||||||
|
' alt="" style="aspect-ratio: ', self::TILE_RATIO, '">';
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
echo '
|
||||||
|
<img alt="" src="', BASEURL, '/images/nothumb.svg"',
|
||||||
|
' style="aspect-ratio: ', self::TILE_RATIO, '; object-fit: unset">';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->show_labels)
|
||||||
|
echo '
|
||||||
|
<h4>', $album['caption'], '</h4>';
|
||||||
|
|
||||||
|
echo '
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,19 +6,30 @@
|
|||||||
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
class Alert extends SubTemplate
|
class Alert extends Template
|
||||||
{
|
{
|
||||||
|
private $_type;
|
||||||
|
private $_message;
|
||||||
|
private $_title;
|
||||||
|
|
||||||
public function __construct($title = '', $message = '', $type = 'alert')
|
public function __construct($title = '', $message = '', $type = 'alert')
|
||||||
{
|
{
|
||||||
$this->_title = $title;
|
$this->_title = $title;
|
||||||
$this->_message = $message;
|
$this->_message = $message;
|
||||||
$this->_type = in_array($type, ['alert', 'error', 'success', 'info']) ? $type : 'alert';
|
$this->_type = in_array($type, ['success', 'info', 'warning', 'danger']) ? $type : 'info';
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function html_content()
|
public function html_main()
|
||||||
{
|
{
|
||||||
echo '
|
echo '
|
||||||
<div class="alert', $this->_type != 'alert' ? ' alert-' . $this->_type : '', '">', (!empty($this->_title) ? '
|
<div class="alert', $this->_type !== 'alert' ? ' alert-' . $this->_type : '', '">'
|
||||||
<strong>' . $this->_title . '</strong><br>' : ''), $this->_message, '</div>';
|
, !empty($this->_title) ? '<strong>' . $this->_title . '</strong><br>' : '', '
|
||||||
|
', $this->_message,
|
||||||
|
$this->additional_alert_content(), '
|
||||||
|
</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function additional_alert_content()
|
||||||
|
{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
36
templates/AssetManagementWrapper.php
Normal file
36
templates/AssetManagementWrapper.php
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
/*****************************************************************************
|
||||||
|
* AssetManagementWrapper.php
|
||||||
|
* Defines asset management wrapper template.
|
||||||
|
*
|
||||||
|
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
class AssetManagementWrapper extends Template
|
||||||
|
{
|
||||||
|
public function html_main()
|
||||||
|
{
|
||||||
|
echo '
|
||||||
|
<form action="" method="post">';
|
||||||
|
|
||||||
|
foreach ($this->_subtemplates as $template)
|
||||||
|
$template->html_main();
|
||||||
|
|
||||||
|
echo '
|
||||||
|
</form>
|
||||||
|
<script type="text/javascript" defer="defer">
|
||||||
|
const allAreSelected = () => {
|
||||||
|
return document.querySelectorAll(".asset_select").length ===
|
||||||
|
document.querySelectorAll(".asset_select:checked").length;
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectAll = document.getElementById("selectall");
|
||||||
|
selectAll.addEventListener("change", event => {
|
||||||
|
const newSelectedState = !allAreSelected();
|
||||||
|
document.querySelectorAll(".asset_select").forEach(el => {
|
||||||
|
el.checked = newSelectedState;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>';
|
||||||
|
}
|
||||||
|
}
|
||||||
27
templates/Button.php
Normal file
27
templates/Button.php
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
/*****************************************************************************
|
||||||
|
* Button.php
|
||||||
|
* Defines the Button template.
|
||||||
|
*
|
||||||
|
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
class Button extends Template
|
||||||
|
{
|
||||||
|
private $content = '';
|
||||||
|
private $href = '';
|
||||||
|
private $class = '';
|
||||||
|
|
||||||
|
public function __construct($content = '', $href = '', $class = '')
|
||||||
|
{
|
||||||
|
$this->content = $content;
|
||||||
|
$this->href = $href;
|
||||||
|
$this->class = $class;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function html_main()
|
||||||
|
{
|
||||||
|
echo '
|
||||||
|
<a class="', $this->class, '" href="', $this->href, '">', $this->content, '</a>';
|
||||||
|
}
|
||||||
|
}
|
||||||
35
templates/ConfirmDeletePage.php
Normal file
35
templates/ConfirmDeletePage.php
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
/*****************************************************************************
|
||||||
|
* ConfirmDeletePage.php
|
||||||
|
* Contains the confirm delete page template.
|
||||||
|
*
|
||||||
|
* Kabuki CMS (C) 2013-2016, Aaron van Geffen
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
class ConfirmDeletePage extends PhotoPage
|
||||||
|
{
|
||||||
|
public function __construct(Image $photo)
|
||||||
|
{
|
||||||
|
parent::__construct($photo);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function html_main()
|
||||||
|
{
|
||||||
|
$this->confirm();
|
||||||
|
$this->photo();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function confirm()
|
||||||
|
{
|
||||||
|
$buttons = [];
|
||||||
|
$buttons[] = new Button("Delete", BASEURL . '/' . $this->photo->getSlug() . '/?delete_confirmed', "btn btn-danger");
|
||||||
|
$buttons[] = new Button("Cancel", $this->photo->getPageUrl(), "btn");
|
||||||
|
|
||||||
|
$alert = new WarningDialog(
|
||||||
|
"Confirm deletion.",
|
||||||
|
"You are about to permanently delete the following photo.",
|
||||||
|
$buttons
|
||||||
|
);
|
||||||
|
$alert->html_main();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,24 +8,26 @@
|
|||||||
|
|
||||||
class DummyBox extends SubTemplate
|
class DummyBox extends SubTemplate
|
||||||
{
|
{
|
||||||
public function __construct($title = '', $content = '', $class = '')
|
protected $_content;
|
||||||
|
|
||||||
|
public function __construct($title = '', $content = '', $class = null)
|
||||||
{
|
{
|
||||||
$this->_title = $title;
|
parent::__construct($title);
|
||||||
$this->_content = $content;
|
$this->_content = $content;
|
||||||
$this->_class = $class;
|
|
||||||
|
if (isset($class))
|
||||||
|
$this->_class .= $class;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function html_content()
|
protected function html_content()
|
||||||
{
|
{
|
||||||
echo '
|
if ($this->_title)
|
||||||
<div class="boxed_content', $this->_class ? ' ' . $this->_class : '', '">', $this->_title ? '
|
echo '
|
||||||
<h2>' . $this->_title . '</h2>' : '', '
|
<h2>', $this->_title, '</h2>';
|
||||||
', $this->_content;
|
|
||||||
|
echo $this->_content;
|
||||||
|
|
||||||
foreach ($this->_subtemplates as $template)
|
foreach ($this->_subtemplates as $template)
|
||||||
$template->html_main();
|
$template->html_main();
|
||||||
|
|
||||||
echo '
|
|
||||||
</div>';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
class EditAssetForm extends SubTemplate
|
class EditAssetForm extends Template
|
||||||
{
|
{
|
||||||
private $asset;
|
private $asset;
|
||||||
private $thumbs;
|
private $thumbs;
|
||||||
@@ -17,14 +17,14 @@ class EditAssetForm extends SubTemplate
|
|||||||
$this->thumbs = $thumbs;
|
$this->thumbs = $thumbs;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function html_content()
|
public function html_main()
|
||||||
{
|
{
|
||||||
echo '
|
echo '
|
||||||
<form id="asset_form" action="" method="post" enctype="multipart/form-data">
|
<form id="asset_form" action="" method="post" enctype="multipart/form-data">
|
||||||
<div class="boxed_content" style="margin-bottom: 2%">
|
<div class="content-box">
|
||||||
<div style="float: right">
|
<div class="float-end">
|
||||||
<a class="btn btn-red" href="', BASEURL, '/editasset/?id=', $this->asset->getId(), '&delete">Delete asset</a>
|
<a class="btn btn-danger" href="', BASEURL, '/', $this->asset->getSlug(), '?delete_confirmed">Delete asset</a>
|
||||||
<input type="submit" value="Save asset data">
|
<button class="btn btn-primary" type="submit">Save asset data</button>
|
||||||
</div>
|
</div>
|
||||||
<h2>Edit asset \'', $this->asset->getTitle(), '\' (', $this->asset->getFilename(), ')</h2>
|
<h2>Edit asset \'', $this->asset->getTitle(), '\' (', $this->asset->getFilename(), ')</h2>
|
||||||
</div>';
|
</div>';
|
||||||
@@ -32,14 +32,15 @@ class EditAssetForm extends SubTemplate
|
|||||||
$this->section_replace();
|
$this->section_replace();
|
||||||
|
|
||||||
echo '
|
echo '
|
||||||
<div style="float: left; width: 60%; margin-right: 2%">';
|
<div class="row">
|
||||||
|
<div class="col-md-8">';
|
||||||
|
|
||||||
$this->section_key_info();
|
$this->section_key_info();
|
||||||
$this->section_asset_meta();
|
$this->section_asset_meta();
|
||||||
|
|
||||||
echo '
|
echo '
|
||||||
</div>
|
</div>
|
||||||
<div style="float: left; width: 38%;">';
|
<div class="col-md-4">';
|
||||||
|
|
||||||
if (!empty($this->thumbs))
|
if (!empty($this->thumbs))
|
||||||
$this->section_thumbnails();
|
$this->section_thumbnails();
|
||||||
@@ -47,11 +48,12 @@ class EditAssetForm extends SubTemplate
|
|||||||
$this->section_linked_tags();
|
$this->section_linked_tags();
|
||||||
|
|
||||||
echo '
|
echo '
|
||||||
</div>';
|
</div>';
|
||||||
|
|
||||||
$this->section_crop_editor();
|
$this->section_crop_editor();
|
||||||
|
|
||||||
echo '
|
echo '
|
||||||
|
</div>
|
||||||
</form>';
|
</form>';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,28 +61,43 @@ class EditAssetForm extends SubTemplate
|
|||||||
{
|
{
|
||||||
$date_captured = $this->asset->getDateCaptured();
|
$date_captured = $this->asset->getDateCaptured();
|
||||||
echo '
|
echo '
|
||||||
<div class="widget key_info">
|
<div class="content-box key_info">
|
||||||
<h3>Key info</h3>
|
<h3>Key info</h3>
|
||||||
<dl>
|
|
||||||
<dt>Title</dt>
|
|
||||||
<dd><input type="text" name="title" maxlength="255" size="70" value="', $this->asset->getTitle(), '">
|
|
||||||
|
|
||||||
<dt>Date captured</dt>
|
<div class="row mb-2">
|
||||||
<dd><input type="text" name="date_captured" size="30" value="',
|
<label class="col-form-label col-sm-3">Title (internal):</label>
|
||||||
|
<div class="col-sm">
|
||||||
|
<input class="form-control" type="text" name="title" maxlength="255" size="70" value="', $this->asset->getTitle(), '">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-2">
|
||||||
|
<label class="col-form-label col-sm-3">URL slug:</label>
|
||||||
|
<div class="col-sm">
|
||||||
|
<input class="form-control" type="text" name="slug" maxlength="255" size="70" value="', $this->asset->getSlug(), '">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-2">
|
||||||
|
<label class="col-form-label col-sm-3">Date captured:</label>
|
||||||
|
<div class="col-sm">
|
||||||
|
<input class="form-control" name="date_captured" size="30" value="',
|
||||||
$date_captured ? $date_captured->format('Y-m-d H:i:s') : '', '" placeholder="Y-m-d H:i:s">
|
$date_captured ? $date_captured->format('Y-m-d H:i:s') : '', '" placeholder="Y-m-d H:i:s">
|
||||||
|
</div>
|
||||||
<dt>Display priority</dt>
|
</div>
|
||||||
<dd><input type="number" name="priority" min="0" max="100" step="1" value="', $this->asset->getPriority(), '">
|
<div class="row mb-2">
|
||||||
</dl>
|
<label class="col-form-label col-sm-3">Display priority:</label>
|
||||||
|
<div class="col-sm-3">
|
||||||
|
<input class="form-control" type="number" name="priority" min="0" max="100" step="1" value="', $this->asset->getPriority(), '">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>';
|
</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function section_linked_tags()
|
protected function section_linked_tags()
|
||||||
{
|
{
|
||||||
echo '
|
echo '
|
||||||
<div class="widget linked_tags" style="margin-top: 2%">
|
<div class="content-box linked_tags">
|
||||||
<h3>Linked tags</h3>
|
<h3>Linked tags</h3>
|
||||||
<ul id="tag_list">';
|
<ul class="list-unstyled" id="tag_list">';
|
||||||
|
|
||||||
foreach ($this->asset->getTags() as $tag)
|
foreach ($this->asset->getTags() as $tag)
|
||||||
echo '
|
echo '
|
||||||
@@ -90,7 +107,7 @@ class EditAssetForm extends SubTemplate
|
|||||||
</li>';
|
</li>';
|
||||||
|
|
||||||
echo '
|
echo '
|
||||||
<li id="new_tag_container"><input type="text" id="new_tag" placeholder="Type to link a new tag"></li>
|
<li id="new_tag_container"><input class="form-control" type="text" id="new_tag" placeholder="Type to link a new tag"></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<script type="text/javascript" src="', BASEURL, '/js/ajax.js"></script>
|
<script type="text/javascript" src="', BASEURL, '/js/ajax.js"></script>
|
||||||
@@ -131,14 +148,14 @@ class EditAssetForm extends SubTemplate
|
|||||||
protected function section_thumbnails()
|
protected function section_thumbnails()
|
||||||
{
|
{
|
||||||
echo '
|
echo '
|
||||||
<div class="widget linked_thumbs">
|
<div class="content-box linked_thumbs">
|
||||||
<h3>Thumbnails</h3>
|
<h3>Thumbnails</h3>
|
||||||
View: <select id="thumbnail_src">';
|
View: <select class="form-select w-auto d-inline" id="thumbnail_src">';
|
||||||
|
|
||||||
foreach ($this->thumbs as $thumb)
|
$first = INF;
|
||||||
|
foreach ($this->thumbs as $i => $thumb)
|
||||||
{
|
{
|
||||||
if (!$thumb['status'])
|
$first = min($i, $first);
|
||||||
continue;
|
|
||||||
|
|
||||||
echo '
|
echo '
|
||||||
<option data-url="', $thumb['url'], '" data-crop_width="', $thumb['dimensions'][0], '" data-crop_height="', $thumb['dimensions'][1], '"',
|
<option data-url="', $thumb['url'], '" data-crop_width="', $thumb['dimensions'][0], '" data-crop_height="', $thumb['dimensions'][1], '"',
|
||||||
@@ -168,18 +185,16 @@ class EditAssetForm extends SubTemplate
|
|||||||
|
|
||||||
echo '
|
echo '
|
||||||
</select>
|
</select>
|
||||||
<a id="thumbnail_link" href="', $this->thumbs[0]['url'], '" target="_blank">
|
<a id="thumbnail_link" href="', $this->thumbs[$first]['url'], '" target="_blank">
|
||||||
<img id="thumbnail" src="', $this->thumbs[0]['url'], '" alt="Thumbnail" style="width: 100%; height: auto;">
|
<img id="thumbnail" src="', $this->thumbs[$first]['url'], '" alt="Thumbnail" style="width: 100%; height: auto;">
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<script type="text/javascript">
|
<script type="text/javascript" defer="defer">
|
||||||
setTimeout(function() {
|
document.getElementById("thumbnail_src").addEventListener("change", event => {
|
||||||
document.getElementById("thumbnail_src").addEventListener("change", function(event) {
|
let selection = event.target.options[event.target.selectedIndex];
|
||||||
var selection = event.target.options[event.target.selectedIndex];
|
document.getElementById("thumbnail_link").href = selection.dataset.url;
|
||||||
document.getElementById("thumbnail_link").href = selection.dataset.url;
|
document.getElementById("thumbnail").src = selection.dataset.url;
|
||||||
document.getElementById("thumbnail").src = selection.dataset.url;
|
});
|
||||||
});
|
|
||||||
}, 100);
|
|
||||||
</script>';
|
</script>';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,71 +205,70 @@ class EditAssetForm extends SubTemplate
|
|||||||
|
|
||||||
echo '
|
echo '
|
||||||
<script type="text/javascript" src="', BASEURL, '/js/crop_editor.js"></script>
|
<script type="text/javascript" src="', BASEURL, '/js/crop_editor.js"></script>
|
||||||
<script type="text/javascript">
|
<script type="text/javascript" defer="defer">
|
||||||
setTimeout(function() {
|
let editor = new CropEditor({
|
||||||
var editor = new CropEditor({
|
submit_url: "', BASEURL, '/editasset/",
|
||||||
original_image_src: "', $this->asset->getUrl(), '",
|
original_image_src: "', $this->asset->getUrl(), '",
|
||||||
editor_container_parent_id: "asset_form",
|
editor_container_parent_id: "asset_form",
|
||||||
thumbnail_select_id: "thumbnail_src",
|
thumbnail_select_id: "thumbnail_src",
|
||||||
drag_target: "drag_target",
|
drag_target: ".crop_image_container",
|
||||||
asset_id: ', $this->asset->getId(), ',
|
asset_id: ', $this->asset->getId(), ',
|
||||||
after_save: function(data) {
|
after_save: function(data) {
|
||||||
// Update thumbnail
|
// Update thumbnail
|
||||||
document.getElementById("thumbnail").src = data.url + "?" + (new Date()).getTime();
|
document.getElementById("thumbnail").src = data.url + "?" + (new Date()).getTime();
|
||||||
|
|
||||||
// Update select
|
// Update select
|
||||||
var src = document.getElementById("thumbnail_src");
|
let src = document.getElementById("thumbnail_src");
|
||||||
src.options[src.selectedIndex].dataset.crop_region = data.value;
|
let option = src.options[src.selectedIndex];
|
||||||
|
option.dataset.crop_region = data.value;
|
||||||
|
option.textContent = option.textContent.replace(/top|bottom|centre|slice/, "exact");
|
||||||
|
|
||||||
// TODO: update meta
|
// TODO: update meta
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, 100);
|
|
||||||
</script>';
|
</script>';
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function section_asset_meta()
|
protected function section_asset_meta()
|
||||||
{
|
{
|
||||||
echo '
|
echo '
|
||||||
<div class="widget asset_meta" style="margin-top: 2%">
|
<div class="content-box asset_meta mt-2">
|
||||||
<h3>Asset meta data</h3>
|
<h3>Asset meta data</h3>';
|
||||||
<ul>';
|
|
||||||
|
|
||||||
$i = -1;
|
$i = 0;
|
||||||
foreach ($this->asset->getMeta() as $key => $meta)
|
foreach ($this->asset->getMeta() as $key => $meta)
|
||||||
{
|
{
|
||||||
$i++;
|
|
||||||
echo '
|
echo '
|
||||||
<li>
|
<div class="input-group">
|
||||||
<input type="text" name="meta_key[', $i, ']" value="', htmlentities($key), '">
|
<input type="text" class="form-control" name="meta_key[', $i, ']" value="', htmlspecialchars($key), '" placeholder="key">
|
||||||
<input type="text" name="meta_value[', $i, ']" value="', htmlentities($meta), '">
|
<input type="text" class="form-control" name="meta_value[', $i, ']" value="', htmlspecialchars($meta), '" placeholder="value">
|
||||||
</li>';
|
</div>';
|
||||||
|
$i++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
echo '
|
echo '
|
||||||
<li>
|
<div class="input-group">
|
||||||
<input type="text" name="meta_key[', $i + 1, ']" value="">
|
<input type="text" class="form-control" name="meta_key[', $i + 1, ']" value="" placeholder="key">
|
||||||
<input type="text" name="meta_value[', $i + 1, ']" value="">
|
<input type="text" class="form-control" name="meta_value[', $i + 1, ']" value="" placeholder="value">
|
||||||
</li>
|
</div>
|
||||||
</ul>
|
<div class="text-end mt-3">
|
||||||
<p><input type="submit" value="Save metadata"></p>
|
<button class="btn btn-primary" type="submit">Save metadata</button>
|
||||||
|
</div>
|
||||||
</div>';
|
</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function section_replace()
|
protected function section_replace()
|
||||||
{
|
{
|
||||||
echo '
|
echo '
|
||||||
<div class="widget replace_asset" style="margin-bottom: 2%; display: block">
|
<div class="content-box replace_asset mt-2">
|
||||||
<h3>Replace asset</h3>
|
<h3>Replace asset</h3>
|
||||||
File: <input type="file" name="replacement">
|
File: <input class="form-control d-inline w-auto" type="file" name="replacement">
|
||||||
Target: <select name="replacement_target">
|
Target: <select class="form-select d-inline w-auto" name="replacement_target">
|
||||||
<option value="full">master file</option>';
|
<option value="full">master file</option>';
|
||||||
|
|
||||||
foreach ($this->thumbs as $thumb)
|
foreach ($this->thumbs as $thumb)
|
||||||
{
|
{
|
||||||
if (!$thumb['status'])
|
|
||||||
continue;
|
|
||||||
|
|
||||||
echo '
|
echo '
|
||||||
<option value="thumb_', implode('x', $thumb['dimensions']);
|
<option value="thumb_', implode('x', $thumb['dimensions']);
|
||||||
|
|
||||||
@@ -278,7 +292,7 @@ class EditAssetForm extends SubTemplate
|
|||||||
echo ' crop';
|
echo ' crop';
|
||||||
}
|
}
|
||||||
elseif ($thumb['custom_image'])
|
elseif ($thumb['custom_image'])
|
||||||
echo ' (custom)';
|
echo ', custom';
|
||||||
|
|
||||||
echo ')
|
echo ')
|
||||||
</option>';
|
</option>';
|
||||||
@@ -286,7 +300,7 @@ class EditAssetForm extends SubTemplate
|
|||||||
|
|
||||||
echo '
|
echo '
|
||||||
</select>
|
</select>
|
||||||
<input type="submit" value="Save asset">
|
<button class="btn btn-primary" type="submit">Save asset</button>
|
||||||
</div>';
|
</div>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
46
templates/FeaturedThumbnailManager.php
Normal file
46
templates/FeaturedThumbnailManager.php
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
/*****************************************************************************
|
||||||
|
* FeaturedThumbnailManager.php
|
||||||
|
* Contains the featured thumbnail manager template.
|
||||||
|
*
|
||||||
|
* Kabuki CMS (C) 2013-2021, Aaron van Geffen
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
class FeaturedThumbnailManager extends SubTemplate
|
||||||
|
{
|
||||||
|
private $assets;
|
||||||
|
private $currentThumbnailId;
|
||||||
|
|
||||||
|
public function __construct(AssetIterator $assets, $currentThumbnailId)
|
||||||
|
{
|
||||||
|
$this->assets = $assets;
|
||||||
|
$this->currentThumbnailId = $currentThumbnailId;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function html_content()
|
||||||
|
{
|
||||||
|
echo '
|
||||||
|
<form action="" method="post">
|
||||||
|
<button class="btn btn-primary float-end" type="submit" name="changeThumbnail">Save thumbnail selection</button>
|
||||||
|
<h2>Select thumbnail</h2>
|
||||||
|
<ul id="featuredThumbnail">';
|
||||||
|
|
||||||
|
while ($asset = $this->assets->next())
|
||||||
|
{
|
||||||
|
$image = $asset->getImage();
|
||||||
|
echo '
|
||||||
|
<li>
|
||||||
|
<input class="form-check-input" type="radio" name="featuredThumbnail" value="', $image->getId(), '"',
|
||||||
|
$this->currentThumbnailId == $image->getId() ? ' checked' : '', '>
|
||||||
|
<img src="', $image->getThumbnailUrl(150, 100, 'top'), '" alt="" title="', $image->getTitle(), '" onclick="this.parentNode.children[0].checked = true">
|
||||||
|
</li>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->assets->clean();
|
||||||
|
|
||||||
|
echo '
|
||||||
|
</ul>
|
||||||
|
<input type="hidden" name="', Session::getSessionTokenKey(), '" value="', Session::getSessionToken(), '">
|
||||||
|
</form>';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,19 +11,25 @@ class ForgotPasswordForm extends SubTemplate
|
|||||||
protected function html_content()
|
protected function html_content()
|
||||||
{
|
{
|
||||||
echo '
|
echo '
|
||||||
<div class="boxed_content">
|
<h1>Password reset procedure</h1>';
|
||||||
<h2>Password reset procedure</h2>';
|
|
||||||
|
|
||||||
foreach ($this->_subtemplates as $template)
|
foreach ($this->_subtemplates as $template)
|
||||||
$template->html_main();
|
$template->html_main();
|
||||||
|
|
||||||
echo '
|
echo '
|
||||||
<p>Please fill in the email address you used to sign up in the form below. You will be sent a reset link to your email address.</p>
|
<p class="mt-3">Please fill in the email address you used to sign up in the form below. We will send a reset link to your email address.</p>
|
||||||
<form class="form-horizontal" action="', BASEURL, '/resetpassword/?step=1" method="post">
|
<form action="', BASEURL, '/resetpassword/?step=1" method="post">
|
||||||
<label class="control-label" for="field_emailaddress">E-mail address:</label><br>
|
<div class="row">
|
||||||
<input type="text" id="field_emailaddress" name="emailaddress">
|
<label class="col-sm-2 col-form-label" for="field_emailaddress">E-mail address:</label>
|
||||||
<button type="submit" class="btn btn-primary">Send mail</button>
|
<div class="col-sm-4">
|
||||||
</form>
|
<input type="text" class="form-control" id="field_emailaddress" name="emailaddress">
|
||||||
</div>';
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mt-3">
|
||||||
|
<div class="offset-sm-2 col-sm-2">
|
||||||
|
<button type="submit" class="btn btn-primary">Send mail</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,45 +3,41 @@
|
|||||||
* FormView.php
|
* FormView.php
|
||||||
* Contains the form template.
|
* Contains the form template.
|
||||||
*
|
*
|
||||||
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
* Kabuki CMS (C) 2013-2023, Aaron van Geffen
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
class FormView extends SubTemplate
|
class FormView extends SubTemplate
|
||||||
{
|
{
|
||||||
|
private $form;
|
||||||
|
private array $data;
|
||||||
|
private array $missing;
|
||||||
|
private $title;
|
||||||
|
|
||||||
public function __construct(Form $form, $title = '')
|
public function __construct(Form $form, $title = '')
|
||||||
{
|
{
|
||||||
|
$this->form = $form;
|
||||||
$this->title = $title;
|
$this->title = $title;
|
||||||
$this->request_url = $form->request_url;
|
|
||||||
$this->request_method = $form->request_method;
|
|
||||||
$this->fields = $form->getFields();
|
|
||||||
$this->missing = $form->getMissing();
|
|
||||||
$this->data = $form->getData();
|
|
||||||
$this->content_above = $form->content_above;
|
|
||||||
$this->content_below = $form->content_below;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function html_content($exclude = [], $include = [])
|
protected function html_content($exclude = [], $include = [])
|
||||||
{
|
{
|
||||||
if (!empty($this->title))
|
if (!empty($this->title))
|
||||||
echo '
|
echo '
|
||||||
<div id="journal_title">
|
<h1>', $this->title, '</h1>';
|
||||||
<h3>', $this->title, '</h3>
|
|
||||||
</div>
|
|
||||||
<div id="inner">';
|
|
||||||
|
|
||||||
foreach ($this->_subtemplates as $template)
|
foreach ($this->_subtemplates as $template)
|
||||||
$template->html_main();
|
$template->html_main();
|
||||||
|
|
||||||
echo '
|
echo '
|
||||||
<form action="', $this->request_url, '" method="', $this->request_method, '" enctype="multipart/form-data">';
|
<form action="', $this->form->request_url, '" method="', $this->form->request_method, '" enctype="multipart/form-data">';
|
||||||
|
|
||||||
if (isset($this->content_above))
|
if (isset($this->form->content_above))
|
||||||
echo $this->content_above;
|
echo $this->form->content_above;
|
||||||
|
|
||||||
echo '
|
$this->missing = $this->form->getMissing();
|
||||||
<dl>';
|
$this->data = $this->form->getData();
|
||||||
|
|
||||||
foreach ($this->fields as $field_id => $field)
|
foreach ($this->form->getFields() as $field_id => $field)
|
||||||
{
|
{
|
||||||
// Either we have a blacklist
|
// Either we have a blacklist
|
||||||
if (!empty($exclude) && in_array($field_id, $exclude))
|
if (!empty($exclude) && in_array($field_id, $exclude))
|
||||||
@@ -55,107 +51,230 @@ class FormView extends SubTemplate
|
|||||||
}
|
}
|
||||||
|
|
||||||
echo '
|
echo '
|
||||||
</dl>
|
|
||||||
<input type="hidden" name="', Session::getSessionTokenKey(), '" value="', Session::getSessionToken(), '">
|
<input type="hidden" name="', Session::getSessionTokenKey(), '" value="', Session::getSessionToken(), '">
|
||||||
<div style="clear: both">
|
<div class="form-group">
|
||||||
<button type="submit" class="btn btn-primary">Save information</button>';
|
<div class="offset-sm-2 col-sm-10">
|
||||||
|
<button type="submit" name="submit" class="btn btn-primary">', $this->form->getSubmitButtonCaption(), '</button>';
|
||||||
|
|
||||||
if (isset($this->content_below))
|
if (isset($this->form->content_below))
|
||||||
echo '
|
echo '
|
||||||
', $this->content_below;
|
', $this->form->content_below;
|
||||||
|
|
||||||
echo '
|
echo '
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>';
|
</form>';
|
||||||
|
|
||||||
if (!empty($this->title))
|
|
||||||
echo '
|
|
||||||
</div>';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function renderField($field_id, $field)
|
protected function renderField($field_id, array $field)
|
||||||
{
|
{
|
||||||
if (isset($field['before_html']))
|
if (isset($field['before_html']))
|
||||||
echo '</dl>
|
|
||||||
', $field['before_html'], '
|
|
||||||
<dl>';
|
|
||||||
|
|
||||||
if ($field['type'] != 'checkbox' && isset($field['label']))
|
|
||||||
echo '
|
echo '
|
||||||
<dt class="cont_', $field_id, isset($field['tab_class']) ? ' target target-' . $field['tab_class'] : '', '"', in_array($field_id, $this->missing) ? ' style="color: red"' : '', '>', $field['label'], '</dt>';
|
', $field['before_html'];
|
||||||
elseif ($field['type'] == 'checkbox' && isset($field['header']))
|
|
||||||
echo '
|
|
||||||
<dt class="cont_', $field_id, isset($field['tab_class']) ? ' target target-' . $field['tab_class'] : '', '"', in_array($field_id, $this->missing) ? ' style="color: red"' : '', '>', $field['header'], '</dt>';
|
|
||||||
|
|
||||||
echo '
|
echo '
|
||||||
<dd class="cont_', $field_id, isset($field['dd_class']) ? ' ' . $field['dd_class'] : '', isset($field['tab_class']) ? ' target target-' . $field['tab_class'] : '', '">';
|
<div class="row mb-2">';
|
||||||
|
|
||||||
if (isset($field['before']))
|
if (isset($field['before']))
|
||||||
echo $field['before'];
|
echo $field['before'];
|
||||||
|
|
||||||
|
if ($field['type'] !== 'checkbox')
|
||||||
|
if (isset($field['label']))
|
||||||
|
echo '
|
||||||
|
<label class="col-sm-2 col-form-label" for="', $field_id, '"', in_array($field_id, $this->missing) ? ' style="color: red"' : '', '>', $field['label'], ':</label>
|
||||||
|
<div class="', isset($field['class']) ? $field['class'] : 'col-sm-6', '">';
|
||||||
|
else
|
||||||
|
echo '
|
||||||
|
<div class="offset-sm-2 ', isset($field['class']) ? $field['class'] : 'col-sm-6', '">';
|
||||||
|
|
||||||
switch ($field['type'])
|
switch ($field['type'])
|
||||||
{
|
{
|
||||||
case 'select':
|
case 'select':
|
||||||
echo '
|
$this->renderSelect($field_id, $field);
|
||||||
<select name="', $field_id, '" id="', $field_id, '"', !empty($field['disabled']) ? ' disabled' : '', '>';
|
|
||||||
|
|
||||||
if (isset($field['placeholder']))
|
|
||||||
echo '
|
|
||||||
<option value="">', $field['placeholder'], '</option>';
|
|
||||||
|
|
||||||
foreach ($field['options'] as $value => $option)
|
|
||||||
echo '
|
|
||||||
<option value="', $value, '"', $this->data[$field_id] == $value ? ' selected' : '', '>', htmlentities($option), '</option>';
|
|
||||||
|
|
||||||
echo '
|
|
||||||
</select>';
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'radio':
|
case 'radio':
|
||||||
foreach ($field['options'] as $value => $option)
|
$this->renderRadio($field_id, $field);
|
||||||
echo '
|
|
||||||
<input type="radio" name="', $field_id, '" value="', $value, '"', $this->data[$field_id] == $value ? ' checked' : '', !empty($field['disabled']) ? ' disabled' : '', '> ', htmlentities($option);
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'checkbox':
|
case 'checkbox':
|
||||||
echo '
|
$this->renderCheckbox($field_id, $field);
|
||||||
<label><input type="checkbox"', $this->data[$field_id] ? ' checked' : '', !empty($field['disabled']) ? ' disabled' : '', ' name="', $field_id, '"> ', htmlentities($field['label']), '</label>';
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'textarea':
|
case 'textarea':
|
||||||
echo '
|
$this->renderTextArea($field_id, $field);
|
||||||
<textarea name="', $field_id, '" id="', $field_id, '" cols="', isset($field['columns']) ? $field['columns'] : 40, '" rows="', isset($field['rows']) ? $field['rows'] : 4, '"', !empty($field['disabled']) ? ' disabled' : '', '>', $this->data[$field_id], '</textarea>';
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'color':
|
case 'color':
|
||||||
echo '
|
$this->renderColor($field_id, $field);
|
||||||
<input type="color" name="', $field_id, '" id="', $field_id, '" value="', htmlentities($this->data[$field_id]), '"', !empty($field['disabled']) ? ' disabled' : '', '>';
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'numeric':
|
case 'numeric':
|
||||||
echo '
|
$this->renderNumeric($field_id, $field);
|
||||||
<input type="number"', isset($field['step']) ? ' step="' . $field['step'] . '"' : '', '" min="', isset($field['min_value']) ? $field['min_value'] : '0', '" max="', isset($field['max_value']) ? $field['max_value'] : '9999', '" name="', $field_id, '" id="', $field_id, '"', isset($field['size']) ? ' size="' . $field['size'] . '"' : '', isset($field['maxlength']) ? ' maxlength="' . $field['maxlength'] . '"' : '', ' value="', htmlentities($this->data[$field_id]), '"', !empty($field['disabled']) ? ' disabled' : '', '>';
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'file':
|
case 'file':
|
||||||
if (!empty($this->data[$field_id]))
|
$this->renderFile($field_id, $field);
|
||||||
echo '<img src="', $this->data[$field_id], '" alt=""><br>';
|
break;
|
||||||
|
|
||||||
echo '
|
case 'captcha':
|
||||||
<input type="file" name="', $field_id, '" id="', $field_id, '"', !empty($field['disabled']) ? ' disabled' : '', '>';
|
$this->renderCaptcha($field_id, $field);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'text':
|
case 'text':
|
||||||
case 'password':
|
case 'password':
|
||||||
default:
|
default:
|
||||||
echo '
|
$this->renderText($field_id, $field);
|
||||||
<input type="', $field['type'], '" name="', $field_id, '" id="', $field_id, '"', isset($field['size']) ? ' size="' . $field['size'] . '"' : '', isset($field['maxlength']) ? ' maxlength="' . $field['maxlength'] . '"' : '', ' value="', htmlentities($this->data[$field_id]), '"', !empty($field['disabled']) ? ' disabled' : '', isset($field['trigger']) ? ' class="trigger-' . $field['trigger'] . '"' : '', '>';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isset($field['after']))
|
if (isset($field['after']))
|
||||||
echo ' ', $field['after'];
|
echo ' ', $field['after'];
|
||||||
|
|
||||||
|
if ($field['type'] !== 'checkbox')
|
||||||
|
echo '
|
||||||
|
</div>';
|
||||||
|
|
||||||
echo '
|
echo '
|
||||||
</dd>';
|
</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function renderCaptcha($field_id, array $field)
|
||||||
|
{
|
||||||
|
echo '
|
||||||
|
<div class="g-recaptcha" data-sitekey="', RECAPTCHA_API_KEY, '"></div>
|
||||||
|
<script src="https://www.google.com/recaptcha/api.js"></script>';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function renderCheckbox($field_id, array $field)
|
||||||
|
{
|
||||||
|
echo '
|
||||||
|
<div class="offset-sm-2 col-sm-10">
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox"', $this->data[$field_id] ? ' checked' : '', !empty($field['disabled']) ? ' disabled' : '', ' name="', $field_id, '" id="check-', $field_id, '">
|
||||||
|
<label class="form-check-label" for="check-', $field_id, '">
|
||||||
|
', $field['label'], '
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function renderColor($field_id, array $field)
|
||||||
|
{
|
||||||
|
echo '
|
||||||
|
<input class="form-control" type="color" name="', $field_id, '" id="', $field_id, '" value="', htmlspecialchars($this->data[$field_id]), '"', !empty($field['disabled']) ? ' disabled' : '', '>';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function renderFile($field_id, array $field)
|
||||||
|
{
|
||||||
|
if (!empty($this->data[$field_id]))
|
||||||
|
echo 'Currently using asset <tt>', $this->data[$field_id], '</tt>. Upload to overwrite.<br>';
|
||||||
|
|
||||||
|
echo '
|
||||||
|
<input class="form-control" type="file" name="', $field_id, '" id="', $field_id, '"', !empty($field['disabled']) ? ' disabled' : '', '>';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function renderNumeric($field_id, array $field)
|
||||||
|
{
|
||||||
|
echo '
|
||||||
|
<input class="form-control" type="number"',
|
||||||
|
isset($field['step']) ? ' step="' . $field['step'] . '"' : '',
|
||||||
|
' min="', isset($field['min_value']) ? $field['min_value'] : '0', '"',
|
||||||
|
' max="', isset($field['max_value']) ? $field['max_value'] : '9999', '"',
|
||||||
|
' name="', $field_id, '" id="', $field_id, '"',
|
||||||
|
isset($field['size']) ? ' size="' . $field['size'] . '"' : '',
|
||||||
|
isset($field['maxlength']) ? ' maxlength="' . $field['maxlength'] . '"' : '',
|
||||||
|
' value="', htmlspecialchars($this->data[$field_id]), '"',
|
||||||
|
!empty($field['disabled']) ? ' disabled' : '', '>';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function renderRadio($field_id, array $field)
|
||||||
|
{
|
||||||
|
foreach ($field['options'] as $value => $option)
|
||||||
|
echo '
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="radio" name="', $field_id, '" id="radio-', $field_id, '-', $value, '" value="', $value, '"', $this->data[$field_id] == $value ? ' checked' : '', !empty($field['disabled']) ? ' disabled' : '', '>
|
||||||
|
<label class="form-check-label" for="radio-', $field_id, '-', $value, '">
|
||||||
|
', htmlspecialchars($option), '
|
||||||
|
</label>
|
||||||
|
</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function renderSelect($field_id, array $field)
|
||||||
|
{
|
||||||
|
echo '
|
||||||
|
<select class="form-select" name="', $field_id, !empty($field['multiple']) ? '[]' : '',
|
||||||
|
'" id="', $field_id, '"',
|
||||||
|
!empty($field['disabled']) ? ' disabled' : '',
|
||||||
|
!empty($field['multiple']) ? ' multiple' : '',
|
||||||
|
!empty($field['size']) ? ' size="' . $field['size'] . '"' : '',
|
||||||
|
'>';
|
||||||
|
|
||||||
|
if (isset($field['placeholder']))
|
||||||
|
echo '
|
||||||
|
<option value="">', $field['placeholder'], '</option>';
|
||||||
|
|
||||||
|
foreach ($field['options'] as $key => $value)
|
||||||
|
{
|
||||||
|
if (is_array($value))
|
||||||
|
{
|
||||||
|
assert(empty($field['multiple']));
|
||||||
|
$this->renderSelectOptionGroup($field_id, $key, $value);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
$this->renderSelectOption($field_id, $value, $key, !empty($field['multiple']));
|
||||||
|
}
|
||||||
|
|
||||||
|
echo '
|
||||||
|
</select>';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function renderSelectOption($field_id, $label, $value, $multiple = false)
|
||||||
|
{
|
||||||
|
echo '
|
||||||
|
<option value="', $value, '"',
|
||||||
|
!$multiple && $this->data[$field_id] == $value ? ' selected' : '',
|
||||||
|
$multiple && in_array($value, $this->data[$field_id]) ? ' selected' : '',
|
||||||
|
'>', htmlspecialchars($label), '</option>';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function renderSelectOptionGroup($field_id, $label, $options)
|
||||||
|
{
|
||||||
|
echo '
|
||||||
|
<optgroup label="', $label, '">';
|
||||||
|
|
||||||
|
foreach ($options as $value => $option)
|
||||||
|
$this->renderSelectOption($field_id, $option, $value);
|
||||||
|
|
||||||
|
echo '
|
||||||
|
</optgroup>';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function renderText($field_id, array $field)
|
||||||
|
{
|
||||||
|
echo '
|
||||||
|
<input class="form-control" ',
|
||||||
|
'type="', $field['type'], '" ',
|
||||||
|
'name="', $field_id, '" ',
|
||||||
|
'id="', $field_id, '"',
|
||||||
|
isset($field['size']) ? ' size="' . $field['size'] . '"' : '',
|
||||||
|
isset($field['maxlength']) ? ' maxlength="' . $field['maxlength'] . '"' : '',
|
||||||
|
isset($this->data[$field_id]) ? ' value="' . htmlspecialchars($this->data[$field_id]) . '"' : '',
|
||||||
|
isset($field['placeholder']) ? ' placeholder="' . $field['placeholder'] . '"' : '',
|
||||||
|
!empty($field['disabled']) ? ' disabled' : '',
|
||||||
|
isset($field['trigger']) ? ' class="trigger-' . $field['trigger'] . '"' : '',
|
||||||
|
'>';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function renderTextArea($field_id, array $field)
|
||||||
|
{
|
||||||
|
echo '
|
||||||
|
<textarea class="form-control' .
|
||||||
|
'" name="', $field_id,
|
||||||
|
'" id="', $field_id,
|
||||||
|
'" cols="', isset($field['columns']) ? $field['columns'] : 40,
|
||||||
|
'" rows="', isset($field['rows']) ? $field['rows'] : 4, '"',
|
||||||
|
isset($field['placeholder']) ? ' placeholder="' . $field['placeholder'] . '"' : '',
|
||||||
|
'"', !empty($field['disabled']) ? ' disabled' : '',
|
||||||
|
'>', $this->data[$field_id], '</textarea>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,45 +11,57 @@ class LogInForm extends SubTemplate
|
|||||||
private $redirect_url = '';
|
private $redirect_url = '';
|
||||||
private $emailaddress = '';
|
private $emailaddress = '';
|
||||||
|
|
||||||
|
protected $_class = 'content-box container col-lg-6';
|
||||||
|
|
||||||
public function setRedirectUrl($url)
|
public function setRedirectUrl($url)
|
||||||
{
|
{
|
||||||
$_SESSION['login_url'] = $url;
|
|
||||||
$this->redirect_url = $url;
|
$this->redirect_url = $url;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setEmail($addr)
|
public function setEmail($addr)
|
||||||
{
|
{
|
||||||
$this->emailaddress = htmlentities($addr);
|
$this->emailaddress = htmlspecialchars($addr);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function html_content()
|
protected function html_content()
|
||||||
{
|
{
|
||||||
echo '
|
if (!empty($this->_title))
|
||||||
<form action="', BASEURL, '/login/" method="post" id="login">
|
echo '
|
||||||
<h3>Log in</h3>';
|
<h1 class="mb-4">Press #RU to continue</h1>';
|
||||||
|
|
||||||
foreach ($this->_subtemplates as $template)
|
if (!empty($this->_subtemplates))
|
||||||
$template->html_main();
|
{
|
||||||
|
foreach ($this->_subtemplates as $template)
|
||||||
|
$template->html_main();
|
||||||
|
}
|
||||||
|
|
||||||
echo '
|
echo '
|
||||||
<dl>
|
<form class="mt-4" action="', BASEURL, '/login/" method="post">
|
||||||
<dt><label for="field_emailaddress">E-mail address:</label></dt>
|
<div class="row">
|
||||||
<dd><input type="text" id="field_emailaddress" name="emailaddress" tabindex="1" value="', $this->emailaddress, '" autofocus></dd>
|
<label class="col-sm-3 col-form-label" for="field_emailaddress">E-mail address:</label>
|
||||||
|
<div class="col-sm">
|
||||||
<dt><label for="field_password">Password:</label></dt>
|
<input type="text" class="form-control" id="field_emailaddress" name="emailaddress" value="', $this->emailaddress, '">
|
||||||
<dd><input type="password" id="field_password" name="password" tabindex="2"></dd>
|
</div>
|
||||||
</dl>';
|
</div>
|
||||||
|
<div class="row mt-3">
|
||||||
|
<label class="col-sm-3 col-form-label" for="field_password">Password:</label>
|
||||||
|
<div class="col-sm">
|
||||||
|
<input type="password" class="form-control" id="field_password" name="password">
|
||||||
|
</div>
|
||||||
|
</div>';
|
||||||
|
|
||||||
// Throw in a redirect url if asked for.
|
// Throw in a redirect url if asked for.
|
||||||
if (!empty($this->redirect_url))
|
if (!empty($this->redirect_url))
|
||||||
echo '
|
echo '
|
||||||
<input type="hidden" name="redirect_url" value="', base64_encode($this->redirect_url), '">';
|
<input type="hidden" name="redirect_url" value="', base64_encode($this->redirect_url), '">';
|
||||||
|
|
||||||
echo '
|
echo '
|
||||||
<a href="', BASEURL, '/resetpassword/">Forgotten your password?</a>
|
<div class="mt-4">
|
||||||
<div class="buttonstrip">
|
<div class="offset-sm-3 col-sm-9">
|
||||||
<button type="submit" class="btn btn-primary" id="field_login" name="login" tabindex="3">Log in</button>
|
<button type="submit" class="btn btn-primary">Sign in</button>
|
||||||
</div>
|
<a class="btn btn-light" href="', BASEURL, '/resetpassword/" style="margin-left: 1em">Forgotten your password?</a>
|
||||||
</form>';
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
96
templates/MainNavBar.php
Normal file
96
templates/MainNavBar.php
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
<?php
|
||||||
|
/*****************************************************************************
|
||||||
|
* MainNavBar.php
|
||||||
|
* Contains the primary navigational menu template.
|
||||||
|
*
|
||||||
|
* Kabuki CMS (C) 2013-2023, Aaron van Geffen
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
class MainNavBar extends NavBar
|
||||||
|
{
|
||||||
|
protected $outerMenuId = 'mainNav';
|
||||||
|
protected $innerMenuId = 'mainNavigation';
|
||||||
|
protected $ariaLabel = 'Main navigation';
|
||||||
|
protected $navBarClasses = 'navbar-dark bg-dark sticky-top';
|
||||||
|
protected $primaryBadgeClasses = 'bg-light text-dark';
|
||||||
|
protected $secondaryBadgeClasses = 'bg-dark text-light';
|
||||||
|
|
||||||
|
public function html_main()
|
||||||
|
{
|
||||||
|
// Select a random space invader, with a bias towards the mascot
|
||||||
|
$rnd = rand(0, 100);
|
||||||
|
$alt = $rnd > 50 ? ' alt-' . ($rnd % 6 + 1) : '';
|
||||||
|
|
||||||
|
echo '
|
||||||
|
<nav id="', $this->outerMenuId, '" class="navbar navbar-expand-lg ', $this->navBarClasses, '" aria-label="', $this->ariaLabel, '">
|
||||||
|
<div class="container">
|
||||||
|
<a class="navbar-brand flex-grow-1" href="', BASEURL, '/">
|
||||||
|
<i class="space-invader', $alt, '"></i>
|
||||||
|
HashRU Pics
|
||||||
|
</a>
|
||||||
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#', $this->innerMenuId, '" aria-controls="', $this->innerMenuId, '" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>';
|
||||||
|
|
||||||
|
if (Registry::get('user')->isLoggedIn())
|
||||||
|
{
|
||||||
|
echo '
|
||||||
|
<div class="collapse navbar-collapse justify-content-end" id="', $this->innerMenuId, '">
|
||||||
|
<ul class="navbar-nav mb-2 mb-lg-0">';
|
||||||
|
|
||||||
|
$mainMenu = new MainMenu();
|
||||||
|
$this->renderMenuItems($mainMenu->getItems());
|
||||||
|
|
||||||
|
echo '
|
||||||
|
<li class="nav-divider d-none d-lg-inline"></li>';
|
||||||
|
|
||||||
|
$adminMenu = new AdminMenu();
|
||||||
|
$this->renderMenuItems($adminMenu->getItems());
|
||||||
|
|
||||||
|
$userMenu = new UserMenu();
|
||||||
|
$this->renderMenuItems($userMenu->getItems());
|
||||||
|
|
||||||
|
$this->darkModeToggle();
|
||||||
|
|
||||||
|
echo '
|
||||||
|
</ul>
|
||||||
|
</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
echo '
|
||||||
|
</div>
|
||||||
|
</nav>';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function darkModeToggle()
|
||||||
|
{
|
||||||
|
echo '
|
||||||
|
<li class="nav-item dropdown">
|
||||||
|
<button class="btn btn-link nav-link py-2 px-0 px-lg-2 dropdown-toggle d-flex align-items-center"
|
||||||
|
id="bd-theme" type="button" data-bs-toggle="dropdown" data-bs-display="static">
|
||||||
|
<i id="theme-icon-active" class="bi bi-light"></i>
|
||||||
|
<span class="d-lg-none ms-2" id="bd-theme-text">Toggle theme</span>
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu dropdown-menu-end">
|
||||||
|
<li>
|
||||||
|
<button type="button" class="dropdown-item d-flex align-items-center" data-bs-theme-value="light">
|
||||||
|
<i class="bi bi-sun-fill"></i>
|
||||||
|
Light
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button type="button" class="dropdown-item d-flex align-items-center" data-bs-theme-value="dark">
|
||||||
|
<i class="bi bi-moon-stars-fill"></i>
|
||||||
|
Dark
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button type="button" class="dropdown-item d-flex align-items-center active" data-bs-theme-value="auto">
|
||||||
|
<i class="bi bi-circle-half"></i>
|
||||||
|
Auto
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,25 +25,31 @@ class MainTemplate extends Template
|
|||||||
echo '<!DOCTYPE html>
|
echo '<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title>', $this->title, '</title>', !empty($this->canonical_url) ? '
|
<title>', $this->title, '</title>';
|
||||||
<link rel="canonical" href="' . $this->canonical_url . '">' : '', '
|
|
||||||
|
if (!empty($this->canonical_url))
|
||||||
|
echo '
|
||||||
|
<link rel="canonical" href="', $this->canonical_url, '">';
|
||||||
|
|
||||||
|
echo '
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">';
|
||||||
|
|
||||||
|
echo '
|
||||||
|
<link rel="stylesheet" href="', BASEURL, '/vendor/twbs/bootstrap/dist/css/bootstrap.min.css">
|
||||||
|
<link rel="stylesheet" href="', BASEURL, '/vendor/twbs/bootstrap-icons/font/bootstrap-icons.css">
|
||||||
<link type="text/css" rel="stylesheet" href="', BASEURL, '/css/default.css">
|
<link type="text/css" rel="stylesheet" href="', BASEURL, '/css/default.css">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">', !empty($this->css) ? '
|
|
||||||
<style type="text/css">' . $this->css . '
|
|
||||||
</style>' : '', $this->header_html, '
|
|
||||||
<script type="text/javascript" src="', BASEURL, '/js/main.js"></script>
|
<script type="text/javascript" src="', BASEURL, '/js/main.js"></script>
|
||||||
|
<script type="text/javascript" src="', BASEURL, '/js/color-modes.js"></script>'
|
||||||
|
, $this->header_html, '
|
||||||
</head>
|
</head>
|
||||||
<body', !empty($this->classes) ? ' class="' . implode(' ', $this->classes) . '"' : '', '>
|
<body', !empty($this->classes) ? ' class="' . implode(' ', $this->classes) . '"' : '', '>
|
||||||
<header>
|
<header>';
|
||||||
<a href="', BASEURL, '/">
|
|
||||||
<h1 id="logo">#pics</h1>
|
$bar = new MainNavBar();
|
||||||
</a>
|
$bar->html_main();
|
||||||
<ul id="nav">
|
|
||||||
<li><a href="', BASEURL, '/">albums</a></li>
|
echo '
|
||||||
<li><a href="', BASEURL, '/people/">people</a></li>
|
|
||||||
<li><a href="', BASEURL, '/timeline/">timeline</a></li>
|
|
||||||
</ul>
|
|
||||||
</header>
|
</header>
|
||||||
<div id="wrapper">';
|
<div id="wrapper">';
|
||||||
|
|
||||||
@@ -55,12 +61,8 @@ class MainTemplate extends Template
|
|||||||
|
|
||||||
if (Registry::has('user') && Registry::get('user')->isAdmin())
|
if (Registry::has('user') && Registry::get('user')->isAdmin())
|
||||||
{
|
{
|
||||||
if (class_exists('Cache'))
|
|
||||||
echo '
|
|
||||||
<span class="cache-info">Cache info: ', Cache::$hits, ' hits, ', Cache::$misses, ' misses, ', Cache::$puts, ' puts, ', Cache::$removals, ' removals</span>';
|
|
||||||
|
|
||||||
if (Registry::has('start'))
|
if (Registry::has('start'))
|
||||||
echo '<br>
|
echo '
|
||||||
<span class="creation-time">Page creation time: ', sprintf('%1.4f', microtime(true) - Registry::get('start')), ' seconds</span>';
|
<span class="creation-time">Page creation time: ', sprintf('%1.4f', microtime(true) - Registry::get('start')), ' seconds</span>';
|
||||||
|
|
||||||
if (Registry::has('db'))
|
if (Registry::has('db'))
|
||||||
@@ -69,7 +71,7 @@ class MainTemplate extends Template
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
echo '
|
echo '
|
||||||
<span class="vanity">Powered by <a href="https://aaronweb.net/projects/kabuki/">Kabuki CMS</a></span>';
|
<span class="vanity">Powered by <a href="https://aaronweb.net/projects/kabuki/" target="_blank">Kabuki CMS</a></span>';
|
||||||
|
|
||||||
echo '
|
echo '
|
||||||
</footer>
|
</footer>
|
||||||
@@ -80,15 +82,11 @@ class MainTemplate extends Template
|
|||||||
echo '<pre>', strtr($query, "\t", " "), '</pre>';
|
echo '<pre>', strtr($query, "\t", " "), '</pre>';
|
||||||
|
|
||||||
echo '
|
echo '
|
||||||
|
<script type="text/javascript" src="', BASEURL, '/vendor/twbs/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>';
|
</html>';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function appendCss($css)
|
|
||||||
{
|
|
||||||
$this->css .= $css;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function appendHeaderHtml($html)
|
public function appendHeaderHtml($html)
|
||||||
{
|
{
|
||||||
$this->header_html .= "\n\t\t" . $html;
|
$this->header_html .= "\n\t\t" . $html;
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
|
|
||||||
class MediaUploader extends SubTemplate
|
class MediaUploader extends SubTemplate
|
||||||
{
|
{
|
||||||
|
private Tag $tag;
|
||||||
|
|
||||||
public function __construct(Tag $tag)
|
public function __construct(Tag $tag)
|
||||||
{
|
{
|
||||||
$this->tag = $tag;
|
$this->tag = $tag;
|
||||||
@@ -16,14 +18,12 @@ class MediaUploader extends SubTemplate
|
|||||||
protected function html_content()
|
protected function html_content()
|
||||||
{
|
{
|
||||||
echo '
|
echo '
|
||||||
<form action="', BASEURL, '/uploadmedia/?tag=', $this->tag->id_tag, '" class="boxed_content" method="post" enctype="multipart/form-data">
|
<form action="', BASEURL, '/uploadmedia/?tag=', $this->tag->id_tag, '" method="post" enctype="multipart/form-data">
|
||||||
<h2>Upload new photos to "', $this->tag->tag, '"</h2>
|
<h2>Upload new photos to "', $this->tag->tag, '"</h2>
|
||||||
<div>
|
<div class="input-group">
|
||||||
<h3>Select files</h3>
|
<input class="form-control d-inline" type="file" id="upload_queue" name="uploads[]"
|
||||||
<input type="file" id="upload_queue" name="uploads[]" multiple>
|
accept="image/jpeg" multiple>
|
||||||
</div>
|
<button class="btn btn-primary" name="save" id="photo_submit" type="submit">Upload the lot</button>
|
||||||
<div>
|
|
||||||
<input name="save" id="photo_submit" type="submit" value="Upload the lot">
|
|
||||||
</div>
|
</div>
|
||||||
<div id="upload_preview_area">
|
<div id="upload_preview_area">
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
32
templates/MyTagsView.php
Normal file
32
templates/MyTagsView.php
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
/*****************************************************************************
|
||||||
|
* MyTagsView.php
|
||||||
|
* Contains the user tag list.
|
||||||
|
*
|
||||||
|
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
class MyTagsView extends SubTemplate
|
||||||
|
{
|
||||||
|
private $tags;
|
||||||
|
|
||||||
|
public function __construct(array $tags)
|
||||||
|
{
|
||||||
|
$this->tags = $tags;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function html_content()
|
||||||
|
{
|
||||||
|
echo '
|
||||||
|
<h2>Tags you can edit</h2>
|
||||||
|
<p>You can currently edit the tags below. Click a tag to edit it.</p>
|
||||||
|
<ul>';
|
||||||
|
|
||||||
|
foreach ($this->tags as $tag)
|
||||||
|
echo '
|
||||||
|
<li><a href="', BASEURL, '/edittag/?id=', $tag->id_tag, '">', $tag->tag, '</a></li>';
|
||||||
|
|
||||||
|
echo '
|
||||||
|
</ul>';
|
||||||
|
}
|
||||||
|
}
|
||||||
61
templates/NavBar.php
Normal file
61
templates/NavBar.php
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
/*****************************************************************************
|
||||||
|
* NavBar.php
|
||||||
|
* Contains the navigational menu template.
|
||||||
|
*
|
||||||
|
* Kabuki CMS (C) 2013-2023, Aaron van Geffen
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
abstract class NavBar extends Template
|
||||||
|
{
|
||||||
|
protected $primaryBadgeClasses = 'bg-dark text-light';
|
||||||
|
protected $secondaryBadgeClasses = 'bg-light text-dark';
|
||||||
|
|
||||||
|
public function renderMenu(array $items, $navBarClasses = '')
|
||||||
|
{
|
||||||
|
echo '
|
||||||
|
<ul class="navbar-nav ', $navBarClasses, '">';
|
||||||
|
|
||||||
|
$this->renderMenuItems($items, $navBarClasses);
|
||||||
|
|
||||||
|
echo '
|
||||||
|
</ul>';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function renderMenuItems(array $items)
|
||||||
|
{
|
||||||
|
foreach ($items as $menuId => $item)
|
||||||
|
{
|
||||||
|
if (isset($item['icon']))
|
||||||
|
$item['label'] = '<i class="bi bi-' . $item['icon'] . '"></i> ' . $item['label'];
|
||||||
|
|
||||||
|
if (isset($item['badge']))
|
||||||
|
$item['label'] .= ' <span class="badge ' . $this->primaryBadgeClasses . '">' . $item['badge'] . '</span>';
|
||||||
|
|
||||||
|
if (empty($item['subs']))
|
||||||
|
{
|
||||||
|
echo '
|
||||||
|
<li class="nav-item"><a class="nav-link" href="', $item['url'], '">', $item['label'], '</a></li>';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo '
|
||||||
|
<li class="nav-item dropdown">
|
||||||
|
<a class="nav-link dropdown-toggle" href="#" id="menu', $menuId, '" data-bs-toggle="dropdown" aria-expanded="false">', $item['label'], '</a>
|
||||||
|
<ul class="dropdown-menu" aria-labelledby="menu', $menuId, '">';
|
||||||
|
|
||||||
|
foreach ($item['subs'] as $subitem)
|
||||||
|
{
|
||||||
|
if (isset($subitem['badge']))
|
||||||
|
$subitem['label'] .= ' <span class="badge ' . $this->secondaryBadgeClasses . '">' . $subitem['badge'] . '</span>';
|
||||||
|
|
||||||
|
echo '
|
||||||
|
<li><a class="dropdown-item" href="', $subitem['url'], '">', $subitem['label'], '</a></li>';
|
||||||
|
}
|
||||||
|
|
||||||
|
echo '
|
||||||
|
</ul>
|
||||||
|
</li>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
60
templates/PageIndexWidget.php
Normal file
60
templates/PageIndexWidget.php
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
<?php
|
||||||
|
/*****************************************************************************
|
||||||
|
* PageIndexWidget.php
|
||||||
|
* Contains the template that displays a page index.
|
||||||
|
*
|
||||||
|
* Kabuki CMS (C) 2013-2023, Aaron van Geffen
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
class PageIndexWidget extends Template
|
||||||
|
{
|
||||||
|
private $index;
|
||||||
|
private string $class;
|
||||||
|
|
||||||
|
public function __construct(PageIndex $index)
|
||||||
|
{
|
||||||
|
$this->index = $index;
|
||||||
|
$this->class = $index->getPageIndexClass();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function html_main()
|
||||||
|
{
|
||||||
|
self::paginate($this->index, $this->class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function paginate(PageIndex $index, $class = null)
|
||||||
|
{
|
||||||
|
$page_index = $index->getPageIndex();
|
||||||
|
if (empty($page_index) || count($page_index) == 1)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!isset($class))
|
||||||
|
$class = $index->getPageIndexClass();
|
||||||
|
|
||||||
|
echo '
|
||||||
|
<ul class="pagination', $class ? ' ' . $class : '', '">
|
||||||
|
<li class="page-item', empty($page_index['previous']) ? ' disabled' : '', '">',
|
||||||
|
'<a class="page-link"', !empty($page_index['previous']) ? ' href="' . $page_index['previous']['href'] . '"' : '', '>',
|
||||||
|
'« previous</a></li>';
|
||||||
|
|
||||||
|
foreach ($page_index as $key => $page)
|
||||||
|
{
|
||||||
|
if (!is_numeric($key))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (!is_array($page))
|
||||||
|
echo '
|
||||||
|
<li class="page-item page-padding disabled"><a class="page-link">...</a></li>';
|
||||||
|
else
|
||||||
|
echo '
|
||||||
|
<li class="page-item page-number', $page['is_selected'] ? ' active" aria-current="page' : '', '">',
|
||||||
|
'<a class="page-link" href="', $page['href'], '">', $page['index'], '</a></li>';
|
||||||
|
}
|
||||||
|
|
||||||
|
echo '
|
||||||
|
<li class="page-item', empty($page_index['next']) ? ' disabled' : '', '">',
|
||||||
|
'<a class="page-link"', !empty($page_index['next']) ? ' href="' . $page_index['next']['href'] . '"' : '', '>',
|
||||||
|
'next »</a></li>
|
||||||
|
</ul>';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
<?php
|
|
||||||
/*****************************************************************************
|
|
||||||
* Pagination.php
|
|
||||||
* Contains the pagination template.
|
|
||||||
*
|
|
||||||
* Kabuki CMS (C) 2013-2016, Aaron van Geffen
|
|
||||||
*****************************************************************************/
|
|
||||||
|
|
||||||
class Pagination extends SubTemplate
|
|
||||||
{
|
|
||||||
private $index;
|
|
||||||
private static $unique_index_count = 0;
|
|
||||||
|
|
||||||
public function __construct(PageIndex $index)
|
|
||||||
{
|
|
||||||
$this->index = $index;
|
|
||||||
$this->class = $index->getPageIndexClass();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function html_content()
|
|
||||||
{
|
|
||||||
$index = $this->index->getPageIndex();
|
|
||||||
|
|
||||||
echo '
|
|
||||||
<div class="table_pagination', !empty($this->class) ? ' ' . $this->class : '', '">
|
|
||||||
<ul>
|
|
||||||
<li class="first"><', !empty($index['previous']) ? 'a href="' . $index['previous']['href'] . '"' : 'span', '>« previous</', !empty($index['previous']) ? 'a' : 'span', '></li>';
|
|
||||||
|
|
||||||
$num_wildcards = 0;
|
|
||||||
foreach ($index as $key => $page)
|
|
||||||
{
|
|
||||||
if (!is_numeric($key))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (!is_array($page))
|
|
||||||
{
|
|
||||||
$num_wildcards++;
|
|
||||||
echo '
|
|
||||||
<li class="page-padding" onclick="javascript:promptGoToPage(', self::$unique_index_count, ')"><span>...</span></li>';
|
|
||||||
}
|
|
||||||
else
|
|
||||||
echo '
|
|
||||||
<li class="page-number', $page['is_selected'] ? ' active' : '', '"><a href="', $page['href'], '">', $page['index'], '</a></li>';
|
|
||||||
}
|
|
||||||
|
|
||||||
echo '
|
|
||||||
<li class="last"><', !empty($index['next']) ? 'a href="' . $index['next']['href'] . '"' : 'span', '>next »</', !empty($index['next']) ? 'a' : 'span', '></li>
|
|
||||||
</ul>
|
|
||||||
</div>';
|
|
||||||
|
|
||||||
if ($num_wildcards)
|
|
||||||
{
|
|
||||||
echo '
|
|
||||||
<script type="text/javascript">
|
|
||||||
var page_index_', self::$unique_index_count++, ' = {
|
|
||||||
wildcard_url: "', $this->index->getLink("%d"), '",
|
|
||||||
num_pages: ', $this->index->getNumberOfPages(), ',
|
|
||||||
per_page: ', $this->index->getItemsPerPage(), '
|
|
||||||
};
|
|
||||||
</script>';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -20,27 +20,31 @@ class PasswordResetForm extends SubTemplate
|
|||||||
protected function html_content()
|
protected function html_content()
|
||||||
{
|
{
|
||||||
echo '
|
echo '
|
||||||
<div class="boxed_content">
|
<h1 class="mb-4">Password reset procedure</h1>';
|
||||||
<h2>Password reset procedure</h2>';
|
|
||||||
|
|
||||||
foreach ($this->_subtemplates as $template)
|
foreach ($this->_subtemplates as $template)
|
||||||
$template->html_main();
|
$template->html_main();
|
||||||
|
|
||||||
echo '
|
echo '
|
||||||
<p>You have successfully confirmed your identify. Please use the form below to set a new password.</p>
|
<p>You have successfully confirmed your identify. Please use the form below to set a new password.</p>
|
||||||
<form class="form-horizontal" action="', BASEURL, '/resetpassword/?step=2&email=', rawurlencode($this->email), '&key=', $this->key, '" method="post">
|
<form action="', BASEURL, '/resetpassword/?step=2&email=', rawurlencode($this->email), '&key=', $this->key, '" method="post">
|
||||||
<p>
|
<div class="row mt-3">
|
||||||
<label class="control-label" for="field_password1">New password:</label>
|
<label class="col-sm-2 col-form-label" for="field_password1">New password:</label>
|
||||||
<input type="password" id="field_password1" name="password1">
|
<div class="col-sm-3">
|
||||||
</p>
|
<input type="password" class="form-control" id="field_password1" name="password1">
|
||||||
|
</div>
|
||||||
<p>
|
</div>
|
||||||
<label class="control-label" for="field_password2">Repeat new password:</label>
|
<div class="row mt-3">
|
||||||
<input type="password" id="field_password2" name="password2">
|
<label class="col-sm-2 col-form-label" for="field_password2">Repeat new password:</label>
|
||||||
</p>
|
<div class="col-sm-3">
|
||||||
|
<input type="password" class="form-control" id="field_password2" name="password2">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mt-3">
|
||||||
|
<div class="offset-sm-2 col-sm-2">
|
||||||
<button type="submit" class="btn btn-primary">Reset password</button>
|
<button type="submit" class="btn btn-primary">Reset password</button>
|
||||||
</form>
|
</div>
|
||||||
</div>';
|
</div>
|
||||||
|
</form>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,12 +6,13 @@
|
|||||||
* Kabuki CMS (C) 2013-2016, Aaron van Geffen
|
* Kabuki CMS (C) 2013-2016, Aaron van Geffen
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
class PhotoPage extends SubTemplate
|
class PhotoPage extends Template
|
||||||
{
|
{
|
||||||
private $photo;
|
protected $photo;
|
||||||
private $exif;
|
private $exif;
|
||||||
private $previous_photo_url = '';
|
private $previous_photo_url = '';
|
||||||
private $next_photo_url = '';
|
private $next_photo_url = '';
|
||||||
|
private $is_asset_owner = false;
|
||||||
|
|
||||||
public function __construct(Image $photo)
|
public function __construct(Image $photo)
|
||||||
{
|
{
|
||||||
@@ -28,39 +29,51 @@ class PhotoPage extends SubTemplate
|
|||||||
$this->next_photo_url = $url;
|
$this->next_photo_url = $url;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function html_content()
|
public function setIsAssetOwner($flag)
|
||||||
|
{
|
||||||
|
$this->is_asset_owner = $flag;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function html_main()
|
||||||
{
|
{
|
||||||
$this->photoNav();
|
$this->photoNav();
|
||||||
$this->photo();
|
$this->photo();
|
||||||
|
|
||||||
echo '
|
echo '
|
||||||
<div id="sub_photo">
|
<div class="row mt-5">
|
||||||
<h2 class="entry-title">', $this->photo->getTitle(), '</h2>';
|
<div class="col-lg-8">
|
||||||
|
<div id="sub_photo" class="content-box">
|
||||||
|
<h2 class="entry-title">', $this->photo->getTitle(), '</h2>';
|
||||||
|
|
||||||
$this->taggedPeople();
|
$this->taggedPeople();
|
||||||
$this->linkNewTags();
|
$this->linkNewTags();
|
||||||
|
|
||||||
echo '
|
echo '
|
||||||
</div>';
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-4">';
|
||||||
|
|
||||||
$this->photoMeta();
|
$this->photoMeta();
|
||||||
|
|
||||||
|
if ($this->is_asset_owner)
|
||||||
|
$this->addUserActions();
|
||||||
|
|
||||||
echo '
|
echo '
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<script type="text/javascript" src="', BASEURL, '/js/photonav.js"></script>';
|
<script type="text/javascript" src="', BASEURL, '/js/photonav.js"></script>';
|
||||||
}
|
}
|
||||||
|
|
||||||
private function photo()
|
protected function photo()
|
||||||
{
|
{
|
||||||
echo '
|
echo '
|
||||||
<div id="photo_frame">
|
<div id="photo_frame">
|
||||||
<a href="', $this->photo->getUrl(), '">';
|
<a href="', $this->photo->getUrl(), '">';
|
||||||
|
|
||||||
if ($this->photo->isPortrait())
|
if ($this->photo->isPortrait())
|
||||||
echo '
|
echo $this->photo->getInlineImage(null, 960);
|
||||||
<img src="', $this->photo->getThumbnailUrl(null, 960), '" alt="">';
|
|
||||||
else
|
else
|
||||||
echo '
|
echo $this->photo->getInlineImage(1280, null);
|
||||||
<img src="', $this->photo->getThumbnailUrl(1280, null), '" alt="">';
|
|
||||||
|
|
||||||
echo '
|
echo '
|
||||||
</a>
|
</a>
|
||||||
@@ -71,23 +84,23 @@ class PhotoPage extends SubTemplate
|
|||||||
{
|
{
|
||||||
if ($this->previous_photo_url)
|
if ($this->previous_photo_url)
|
||||||
echo '
|
echo '
|
||||||
<a href="', $this->previous_photo_url, '" id="previous_photo"><em>Previous photo</em></a>';
|
<a href="', $this->previous_photo_url, '#photo_frame" id="previous_photo"><i class="bi bi-arrow-left"></i></a>';
|
||||||
else
|
else
|
||||||
echo '
|
echo '
|
||||||
<span id="previous_photo"><em>Previous photo</em></span>';
|
<span id="previous_photo"><i class="bi bi-arrow-left"></i></span>';
|
||||||
|
|
||||||
if ($this->next_photo_url)
|
if ($this->next_photo_url)
|
||||||
echo '
|
echo '
|
||||||
<a href="', $this->next_photo_url, '" id="next_photo"><em>Next photo</em></a>';
|
<a href="', $this->next_photo_url, '#photo_frame" id="next_photo"><i class="bi bi-arrow-right"></i></a>';
|
||||||
else
|
else
|
||||||
echo '
|
echo '
|
||||||
<span id="next_photo"><em>Next photo</em></span>';
|
<span id="next_photo"><i class="bi bi-arrow-right"></i></span>';
|
||||||
}
|
}
|
||||||
|
|
||||||
private function photoMeta()
|
private function photoMeta()
|
||||||
{
|
{
|
||||||
echo '
|
echo '
|
||||||
<div id="photo_exif_box">
|
<div id="photo_exif_box" class="content-box clearfix">
|
||||||
<h3>EXIF</h3>
|
<h3>EXIF</h3>
|
||||||
<dl class="photo_meta">';
|
<dl class="photo_meta">';
|
||||||
|
|
||||||
@@ -125,6 +138,11 @@ class PhotoPage extends SubTemplate
|
|||||||
<dt>ISO Speed</dt>
|
<dt>ISO Speed</dt>
|
||||||
<dd>', $this->exif->iso, '</dd>';
|
<dd>', $this->exif->iso, '</dd>';
|
||||||
|
|
||||||
|
if (!empty($this->exif->software))
|
||||||
|
echo '
|
||||||
|
<dt>Software</dt>
|
||||||
|
<dd>', $this->exif->software, '</dd>';
|
||||||
|
|
||||||
echo '
|
echo '
|
||||||
</dl>
|
</dl>
|
||||||
</div>';
|
</div>';
|
||||||
@@ -139,8 +157,14 @@ class PhotoPage extends SubTemplate
|
|||||||
foreach ($this->photo->getTags() as $tag)
|
foreach ($this->photo->getTags() as $tag)
|
||||||
{
|
{
|
||||||
echo '
|
echo '
|
||||||
<li>
|
<li id="tag-', $tag->id_tag, '">
|
||||||
<a rel="tag" title="View all posts tagged ', $tag->tag, '" href="', $tag->getUrl(), '" class="entry-tag">', $tag->tag, '</a>
|
<a rel="tag" title="View all posts tagged ', $tag->tag, '" href="', $tag->getUrl(), '" class="entry-tag">', $tag->tag, '</a>';
|
||||||
|
|
||||||
|
if ($tag->kind === 'Person')
|
||||||
|
echo '
|
||||||
|
<a class="delete-tag" title="Unlink this tag from this photo" href="#" data-id="', $tag->id_tag, '">❌</a>';
|
||||||
|
|
||||||
|
echo '
|
||||||
</li>';
|
</li>';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,12 +177,33 @@ class PhotoPage extends SubTemplate
|
|||||||
echo '
|
echo '
|
||||||
<div>
|
<div>
|
||||||
<h3>Link tags</h3>
|
<h3>Link tags</h3>
|
||||||
<p style="position: relative"><input type="text" id="new_tag" placeholder="Type to link a new tag"></p>
|
<p style="position: relative">
|
||||||
|
<input class="form-control w-auto" type="text" id="new_tag" placeholder="Type to link a new tag">
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<script type="text/javascript" src="', BASEURL, '/js/ajax.js"></script>
|
<script type="text/javascript" src="', BASEURL, '/js/ajax.js"></script>
|
||||||
<script type="text/javascript" src="', BASEURL, '/js/autosuggest.js"></script>
|
<script type="text/javascript" src="', BASEURL, '/js/autosuggest.js"></script>
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
setTimeout(function() {
|
setTimeout(function() {
|
||||||
|
var removeTag = function(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
var that = this;
|
||||||
|
var request = new HttpRequest("post", "', $this->photo->getPageUrl(), '",
|
||||||
|
"id_tag=" + this.dataset["id"] + "&delete", function(response) {
|
||||||
|
if (!response.success) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var tagNode = document.getElementById("tag-" + that.dataset["id"]);
|
||||||
|
tagNode.parentNode.removeChild(tagNode);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var tagRemovalTargets = document.getElementsByClassName("delete-tag");
|
||||||
|
for (var i = 0; i < tagRemovalTargets.length; i++) {
|
||||||
|
tagRemovalTargets[i].addEventListener("click", removeTag);
|
||||||
|
}
|
||||||
|
|
||||||
var tag_autosuggest = new TagAutoSuggest({
|
var tag_autosuggest = new TagAutoSuggest({
|
||||||
inputElement: "new_tag",
|
inputElement: "new_tag",
|
||||||
listElement: "tag_list",
|
listElement: "tag_list",
|
||||||
@@ -166,9 +211,25 @@ class PhotoPage extends SubTemplate
|
|||||||
appendCallback: function(item) {
|
appendCallback: function(item) {
|
||||||
var request = new HttpRequest("post", "', $this->photo->getPageUrl(), '",
|
var request = new HttpRequest("post", "', $this->photo->getPageUrl(), '",
|
||||||
"id_tag=" + item.id_tag, function(response) {
|
"id_tag=" + item.id_tag, function(response) {
|
||||||
var newNode = document.createElement("li");
|
var newLink = document.createElement("a");
|
||||||
|
newLink.href = item.url;
|
||||||
|
|
||||||
var newLabel = document.createTextNode(item.label);
|
var newLabel = document.createTextNode(item.label);
|
||||||
newNode.appendChild(newLabel);
|
newLink.appendChild(newLabel);
|
||||||
|
|
||||||
|
var removeLink = document.createElement("a");
|
||||||
|
removeLink.className = "delete-tag";
|
||||||
|
removeLink.dataset["id"] = item.id_tag;
|
||||||
|
removeLink.href = "#";
|
||||||
|
removeLink.addEventListener("click", removeTag);
|
||||||
|
|
||||||
|
var crossmark = document.createTextNode("❌");
|
||||||
|
removeLink.appendChild(crossmark);
|
||||||
|
|
||||||
|
var newNode = document.createElement("li");
|
||||||
|
newNode.id = "tag-" + item.id_tag;
|
||||||
|
newNode.appendChild(newLink);
|
||||||
|
newNode.appendChild(removeLink);
|
||||||
|
|
||||||
var list = document.getElementById("tag_list");
|
var list = document.getElementById("tag_list");
|
||||||
list.appendChild(newNode);
|
list.appendChild(newNode);
|
||||||
@@ -183,4 +244,14 @@ class PhotoPage extends SubTemplate
|
|||||||
{
|
{
|
||||||
$this->exif = $exif;
|
$this->exif = $exif;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function addUserActions()
|
||||||
|
{
|
||||||
|
echo '
|
||||||
|
<div id="user_actions_box" class="content-box">
|
||||||
|
<h3>Actions</h3>
|
||||||
|
<a class="btn btn-primary" href="', BASEURL, '/editasset/?id=', $this->photo->getId(), '">Edit photo</a>
|
||||||
|
<a class="btn btn-danger" href="', BASEURL, '/', $this->photo->getSlug(), '/?confirm_delete">Delete photo</a>
|
||||||
|
</div>';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,11 @@
|
|||||||
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
class PhotosIndex extends SubTemplate
|
class PhotosIndex extends Template
|
||||||
{
|
{
|
||||||
protected $mosaic;
|
protected $mosaic;
|
||||||
protected $show_edit_buttons;
|
protected $show_edit_buttons;
|
||||||
|
protected $show_headers;
|
||||||
protected $show_labels;
|
protected $show_labels;
|
||||||
protected $row_limit = 1000;
|
protected $row_limit = 1000;
|
||||||
protected $previous_header = '';
|
protected $previous_header = '';
|
||||||
@@ -19,7 +20,7 @@ class PhotosIndex extends SubTemplate
|
|||||||
const PANORAMA_HEIGHT = null;
|
const PANORAMA_HEIGHT = null;
|
||||||
|
|
||||||
const PORTRAIT_WIDTH = 400;
|
const PORTRAIT_WIDTH = 400;
|
||||||
const PORTRAIT_HEIGHT = 640;
|
const PORTRAIT_HEIGHT = 645;
|
||||||
|
|
||||||
const LANDSCAPE_WIDTH = 850;
|
const LANDSCAPE_WIDTH = 850;
|
||||||
const LANDSCAPE_HEIGHT = 640;
|
const LANDSCAPE_HEIGHT = 640;
|
||||||
@@ -31,9 +32,9 @@ class PhotosIndex extends SubTemplate
|
|||||||
const SINGLE_HEIGHT = 412;
|
const SINGLE_HEIGHT = 412;
|
||||||
|
|
||||||
const TILE_WIDTH = 400;
|
const TILE_WIDTH = 400;
|
||||||
const TILE_HEIGHT = 267;
|
const TILE_HEIGHT = 300;
|
||||||
|
|
||||||
public function __construct(PhotoMosaic $mosaic, $show_edit_buttons = false, $show_labels = true, $show_headers = true)
|
public function __construct(PhotoMosaic $mosaic, $show_edit_buttons = false, $show_labels = false, $show_headers = true)
|
||||||
{
|
{
|
||||||
$this->mosaic = $mosaic;
|
$this->mosaic = $mosaic;
|
||||||
$this->show_edit_buttons = $show_edit_buttons;
|
$this->show_edit_buttons = $show_edit_buttons;
|
||||||
@@ -41,16 +42,16 @@ class PhotosIndex extends SubTemplate
|
|||||||
$this->show_labels = $show_labels;
|
$this->show_labels = $show_labels;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function html_content()
|
public function html_main()
|
||||||
{
|
{
|
||||||
echo '
|
echo '
|
||||||
<div class="tiled_grid">';
|
<div class="container photo-index">';
|
||||||
|
|
||||||
for ($i = $this->row_limit; $i > 0 && $row = $this->mosaic->getRow(); $i--)
|
for ($i = $this->row_limit; $i > 0 && $row = $this->mosaic->getRow(); $i--)
|
||||||
{
|
{
|
||||||
list($photos, $what) = $row;
|
list($photos, $what) = $row;
|
||||||
$this->header($photos);
|
$this->header($photos);
|
||||||
$this->$what($photos);
|
$this->$what($photos, $i % 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
echo '
|
echo '
|
||||||
@@ -73,129 +74,140 @@ class PhotosIndex extends SubTemplate
|
|||||||
|
|
||||||
$name = str_replace(' ', '', strtolower($header));
|
$name = str_replace(' ', '', strtolower($header));
|
||||||
echo '
|
echo '
|
||||||
<div class="tiled_header" id="', $name, '">
|
<h4 class="tiled-header" id="', $name, '">
|
||||||
<a href="#', $name, '">', $header, '</a>
|
<a href="#', $name, '">', $header, '</a>
|
||||||
</div>';
|
</h4>';
|
||||||
|
|
||||||
$this->previous_header = $header;
|
$this->previous_header = $header;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function color(Image $image)
|
protected function photo(Image $image, $className, $width, $height, $crop = true, $fit = true)
|
||||||
{
|
{
|
||||||
$color = $image->bestColor();
|
echo '
|
||||||
if ($color == 'FFFFFF')
|
<div class="polaroid ', $className, '">';
|
||||||
$color = 'ccc';
|
|
||||||
|
|
||||||
return $color;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function photo(Image $image, $width, $height, $crop = true, $fit = true)
|
|
||||||
{
|
|
||||||
if ($this->show_edit_buttons)
|
if ($this->show_edit_buttons)
|
||||||
echo '
|
echo '
|
||||||
<a class="edit" href="', BASEURL, '/editasset/?id=', $image->getId(), '">Edit</a>';
|
<a class="edit" href="', BASEURL, '/editasset/?id=', $image->getId(), '">Edit</a>';
|
||||||
|
|
||||||
echo '
|
echo '
|
||||||
<a href="', $image->getPageUrl(), $this->url_suffix, '">
|
<a href="', $image->getPageUrl(), $this->url_suffix, '#photo_frame">
|
||||||
<img src="', $image->getThumbnailUrl($width, $height, $crop, $fit), '" alt="" title="', $image->getTitle(), '">';
|
<img src="', $image->getThumbnailUrl($width, $height, $crop, $fit), '"';
|
||||||
|
|
||||||
|
// Can we offer double-density thumbs?
|
||||||
|
if ($image->width() >= $width * 2 && $image->height() >= $height * 2)
|
||||||
|
echo ' srcset="', $image->getThumbnailUrl($width * 2, $height * 2, $crop, $fit), ' 2x"';
|
||||||
|
else
|
||||||
|
echo ' srcset="', $image->getThumbnailUrl($image->width(), $image->height(), true), ' 2x"';
|
||||||
|
|
||||||
|
// Prefer thumbnail aspect ratio if available, otherwise use image aspect ratio.
|
||||||
|
$aspectRatio = isset($width, $height) ? $width / $height : $image->ratio();
|
||||||
|
|
||||||
|
echo ' alt="" title="', $image->getTitle(), '" style="aspect-ratio: ', $aspectRatio, '">';
|
||||||
|
|
||||||
if ($this->show_labels)
|
if ($this->show_labels)
|
||||||
echo '
|
echo '
|
||||||
<h4>', $image->getTitle(), '</h4>';
|
<h4>', $image->getTitle(), '</h4>';
|
||||||
|
|
||||||
echo '
|
echo '
|
||||||
</a>';
|
|
||||||
|
</a>
|
||||||
|
</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function panorama(array $photos)
|
protected function panorama(array $photos, $altLayout)
|
||||||
{
|
{
|
||||||
foreach ($photos as $image)
|
foreach ($photos as $image)
|
||||||
{
|
{
|
||||||
echo '
|
echo '
|
||||||
<div style="border-color: #', $this->color($image), '" class="panorama">';
|
<div class="row mb-5 tile-panorama">
|
||||||
|
<div class="col">';
|
||||||
|
|
||||||
$this->photo($image, static::PANORAMA_WIDTH, static::PANORAMA_HEIGHT, false, false);
|
$this->photo($image, 'panorama', static::PANORAMA_WIDTH, static::PANORAMA_HEIGHT, false, false);
|
||||||
|
|
||||||
echo '
|
echo '
|
||||||
|
</div>
|
||||||
</div>';
|
</div>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function portrait(array $photos)
|
protected function portrait(array $photos, $altLayout)
|
||||||
{
|
{
|
||||||
$image = array_shift($photos);
|
$image = array_shift($photos);
|
||||||
|
|
||||||
echo '
|
echo '
|
||||||
<div class="tiled_row">
|
<div class="row g-5 mb-5 tile-feat-portrait',
|
||||||
<div class="column_portrait">
|
$altLayout ? ' flex-row-reverse' : '', '">
|
||||||
<div style="border-color: #', $this->color($image), '" class="portrait">';
|
<div class="col-md-4">';
|
||||||
|
|
||||||
$this->photo($image, static::PORTRAIT_WIDTH, static::PORTRAIT_HEIGHT, 'top');
|
$this->photo($image, 'portrait', static::PORTRAIT_WIDTH, static::PORTRAIT_HEIGHT, 'centre');
|
||||||
|
|
||||||
echo '
|
echo '
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="column_tiles_four">';
|
<div class="col-md-8">
|
||||||
|
<div class="row g-5">';
|
||||||
|
|
||||||
foreach ($photos as $image)
|
foreach ($photos as $image)
|
||||||
{
|
{
|
||||||
echo '
|
echo '
|
||||||
<div style="border-color: #', $this->color($image), '" class="landscape">';
|
<div class="col-md-6">';
|
||||||
|
|
||||||
$this->photo($image, static::TILE_WIDTH, static::TILE_HEIGHT, 'top');
|
$this->photo($image, 'landscape', static::TILE_WIDTH, static::TILE_HEIGHT, 'top');
|
||||||
|
|
||||||
echo '
|
echo '
|
||||||
</div>';
|
</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
echo '
|
echo '
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>';
|
</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function landscape(array $photos)
|
protected function landscape(array $photos, $altLayout)
|
||||||
{
|
{
|
||||||
$image = array_shift($photos);
|
$image = array_shift($photos);
|
||||||
|
|
||||||
echo '
|
echo '
|
||||||
<div class="tiled_row">
|
<div class="row g-5 mb-5 tile-feat-landscape',
|
||||||
<div class="column_landscape">
|
$altLayout ? ' flex-row-reverse' : '', '">
|
||||||
<div style="border-color: #', $this->color($image), '" class="landscape">';
|
<div class="col-md-8">';
|
||||||
|
|
||||||
$this->photo($image, static::LANDSCAPE_WIDTH, static::LANDSCAPE_HEIGHT, 'top');
|
$this->photo($image, 'landscape', static::LANDSCAPE_WIDTH, static::LANDSCAPE_HEIGHT, 'top');
|
||||||
|
|
||||||
|
echo '
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="row g-5">';
|
||||||
|
|
||||||
|
foreach ($photos as $image)
|
||||||
|
{
|
||||||
|
echo '
|
||||||
|
<div>';
|
||||||
|
|
||||||
|
$this->photo($image, 'landscape', static::TILE_WIDTH, static::TILE_HEIGHT, 'top');
|
||||||
|
|
||||||
|
echo '
|
||||||
|
</div>';
|
||||||
|
}
|
||||||
|
|
||||||
echo '
|
echo '
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="column_tiles_two">';
|
|
||||||
|
|
||||||
foreach ($photos as $image)
|
|
||||||
{
|
|
||||||
echo '
|
|
||||||
<div style="border-color: #', $this->color($image), '" class="landscape">';
|
|
||||||
|
|
||||||
$this->photo($image, static::TILE_WIDTH, static::TILE_HEIGHT, 'top');
|
|
||||||
|
|
||||||
echo '
|
|
||||||
</div>';
|
|
||||||
}
|
|
||||||
|
|
||||||
echo '
|
|
||||||
</div>
|
|
||||||
</div>';
|
</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function duo(array $photos)
|
protected function duo(array $photos, $altLayout)
|
||||||
{
|
{
|
||||||
echo '
|
echo '
|
||||||
<div class="tiled_row">';
|
<div class="row g-5 mb-5 tile-duo">';
|
||||||
|
|
||||||
foreach ($photos as $image)
|
foreach ($photos as $image)
|
||||||
{
|
{
|
||||||
echo '
|
echo '
|
||||||
<div style="border-color: #', $this->color($image), '" class="duo">';
|
<div class="col-md-6">';
|
||||||
|
|
||||||
$this->photo($image, static::DUO_WIDTH, static::DUO_HEIGHT, true);
|
$this->photo($image, 'duo', static::DUO_WIDTH, static::DUO_HEIGHT, true);
|
||||||
|
|
||||||
echo '
|
echo '
|
||||||
</div>';
|
</div>';
|
||||||
@@ -205,32 +217,31 @@ class PhotosIndex extends SubTemplate
|
|||||||
</div>';
|
</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function single(array $photos)
|
protected function single(array $photos, $altLayout)
|
||||||
{
|
{
|
||||||
$image = array_shift($photos);
|
|
||||||
|
|
||||||
echo '
|
echo '
|
||||||
<div class="tiled_row">
|
<div class="row g-5 mb-5 tile-single">
|
||||||
<div style="border-color: #', $this->color($image), '" class="single">';
|
<div class="col-md-6">';
|
||||||
|
|
||||||
$this->photo($image, static::SINGLE_WIDTH, static::SINGLE_HEIGHT, 'top');
|
$image = array_shift($photos);
|
||||||
|
$this->photo($image, 'single', static::SINGLE_WIDTH, static::SINGLE_HEIGHT, 'top');
|
||||||
|
|
||||||
echo '
|
echo '
|
||||||
</div>
|
</div>
|
||||||
</div>';
|
</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function row(array $photos)
|
protected function landscapes(array $photos, $altLayout)
|
||||||
{
|
{
|
||||||
echo '
|
echo '
|
||||||
<div class="tiled_row">';
|
<div class="row g-5 mb-5 tile-row-landscapes">';
|
||||||
|
|
||||||
foreach ($photos as $image)
|
foreach ($photos as $image)
|
||||||
{
|
{
|
||||||
echo '
|
echo '
|
||||||
<div style="border-color: #', $this->color($image), '" class="landscape">';
|
<div class="col-md-4">';
|
||||||
|
|
||||||
$this->photo($image, static::TILE_WIDTH, static::TILE_HEIGHT, true);
|
$this->photo($image, 'landscape', static::TILE_WIDTH, static::TILE_HEIGHT, true);
|
||||||
|
|
||||||
echo '
|
echo '
|
||||||
</div>';
|
</div>';
|
||||||
@@ -240,17 +251,17 @@ class PhotosIndex extends SubTemplate
|
|||||||
</div>';
|
</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function portraits(array $photos)
|
protected function portraits(array $photos, $altLayout)
|
||||||
{
|
{
|
||||||
echo '
|
echo '
|
||||||
<div class="tiled_row">';
|
<div class="row g-5 mb-5 tile-row-portraits">';
|
||||||
|
|
||||||
foreach ($photos as $image)
|
foreach ($photos as $image)
|
||||||
{
|
{
|
||||||
echo '
|
echo '
|
||||||
<div style="border-color: #', $this->color($image), '" class="portrait">';
|
<div class="col-md-4">';
|
||||||
|
|
||||||
$this->photo($image, static::PORTRAIT_WIDTH, static::PORTRAIT_HEIGHT, true);
|
$this->photo($image, 'portrait', static::PORTRAIT_WIDTH, static::PORTRAIT_HEIGHT, true);
|
||||||
|
|
||||||
echo '
|
echo '
|
||||||
</div>';
|
</div>';
|
||||||
|
|||||||
@@ -8,10 +8,32 @@
|
|||||||
|
|
||||||
abstract class SubTemplate extends Template
|
abstract class SubTemplate extends Template
|
||||||
{
|
{
|
||||||
|
protected $_class = 'content-box container';
|
||||||
|
protected $_id;
|
||||||
|
protected $_title;
|
||||||
|
|
||||||
|
public function __construct($title = '')
|
||||||
|
{
|
||||||
|
$this->_title = $title;
|
||||||
|
}
|
||||||
|
|
||||||
public function html_main()
|
public function html_main()
|
||||||
{
|
{
|
||||||
echo $this->html_content();
|
echo '
|
||||||
|
<div class="', $this->_class, '"', isset($this->_id) ? ' id="' . $this->_id . '"' : '', '>',
|
||||||
|
$this->html_content(), '
|
||||||
|
</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract protected function html_content();
|
abstract protected function html_content();
|
||||||
|
|
||||||
|
public function setClassName($className)
|
||||||
|
{
|
||||||
|
$this->_class = $className;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setDOMId($id)
|
||||||
|
{
|
||||||
|
$this->_id = $id;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,49 +3,72 @@
|
|||||||
* TabularData.php
|
* TabularData.php
|
||||||
* Contains the template that displays tabular data.
|
* Contains the template that displays tabular data.
|
||||||
*
|
*
|
||||||
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
* Kabuki CMS (C) 2013-2023, Aaron van Geffen
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
class TabularData extends Pagination
|
class TabularData extends SubTemplate
|
||||||
{
|
{
|
||||||
|
private GenericTable $_t;
|
||||||
|
|
||||||
public function __construct(GenericTable $table)
|
public function __construct(GenericTable $table)
|
||||||
{
|
{
|
||||||
$this->_t = $table;
|
$this->_t = $table;
|
||||||
parent::__construct($table);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function html_content()
|
protected function html_content()
|
||||||
{
|
{
|
||||||
echo '
|
|
||||||
<div class="admin_box">';
|
|
||||||
|
|
||||||
$title = $this->_t->getTitle();
|
$title = $this->_t->getTitle();
|
||||||
if (!empty($title))
|
if (!empty($title))
|
||||||
|
{
|
||||||
|
$titleclass = $this->_t->getTitleClass();
|
||||||
echo '
|
echo '
|
||||||
<h2>', $title, '</h2>';
|
<div class="generic-table', !empty($titleclass) ? ' ' . $titleclass : '', '">
|
||||||
|
<h1>', htmlspecialchars($title), '</h1>';
|
||||||
|
}
|
||||||
|
|
||||||
// Showing a page index?
|
foreach ($this->_subtemplates as $template)
|
||||||
parent::html_content();
|
$template->html_main();
|
||||||
|
|
||||||
// Maybe even a small form?
|
// Showing an inline form?
|
||||||
if (isset($this->_t->form_above))
|
$pager = $this->_t->getPageIndex();
|
||||||
$this->showForm($this->_t->form_above);
|
if (!empty($pager) || isset($this->_t->form_above))
|
||||||
|
{
|
||||||
|
echo '
|
||||||
|
<div class="row clearfix justify-content-end">';
|
||||||
|
|
||||||
|
// Page index?
|
||||||
|
if (!empty($pager))
|
||||||
|
PageIndexWidget::paginate($pager);
|
||||||
|
|
||||||
|
// Form controls?
|
||||||
|
if (isset($this->_t->form_above))
|
||||||
|
$this->showForm($this->_t->form_above);
|
||||||
|
|
||||||
|
echo '
|
||||||
|
</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$tableClass = $this->_t->getTableClass();
|
||||||
|
if ($tableClass)
|
||||||
|
echo '
|
||||||
|
<div class="', $tableClass, '">';
|
||||||
|
|
||||||
// Build the table!
|
// Build the table!
|
||||||
echo '
|
echo '
|
||||||
<table class="table table-striped">
|
<table class="table table-striped table-condensed">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>';
|
<tr>';
|
||||||
|
|
||||||
// Show the table's headers.
|
// Show all headers in their full glory!
|
||||||
foreach ($this->_t->getHeader() as $th)
|
$header = $this->_t->getHeader();
|
||||||
|
foreach ($header as $th)
|
||||||
{
|
{
|
||||||
echo '
|
echo '
|
||||||
<th', (!empty($th['width']) ? ' width="' . $th['width'] . '"' : ''), (!empty($th['class']) ? ' class="' . $th['class'] . '"' : ''), ($th['colspan'] > 1 ? ' colspan="' . $th['colspan'] . '"' : ''), ' scope="', $th['scope'], '">',
|
<th', (!empty($th['width']) ? ' width="' . $th['width'] . '"' : ''), (!empty($th['class']) ? ' class="' . $th['class'] . '"' : ''), ($th['colspan'] > 1 ? ' colspan="' . $th['colspan'] . '"' : ''), ' scope="', $th['scope'], '">',
|
||||||
$th['href'] ? '<a href="' . $th['href'] . '">' . $th['label'] . '</a>' : $th['label'];
|
$th['href'] ? '<a href="' . $th['href'] . '">' . $th['label'] . '</a>' : $th['label'];
|
||||||
|
|
||||||
if ($th['sort_mode'] )
|
if ($th['sort_mode'])
|
||||||
echo ' ', $th['sort_mode'] == 'up' ? '↑' : '↓';
|
echo ' <i class="bi bi-caret-' . ($th['sort_mode'] === 'down' ? 'down' : 'up') . '-fill"></i>';
|
||||||
|
|
||||||
echo '</th>';
|
echo '</th>';
|
||||||
}
|
}
|
||||||
@@ -55,7 +78,7 @@ class TabularData extends Pagination
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>';
|
<tbody>';
|
||||||
|
|
||||||
// Show the table's body.
|
// The body is what we came to see!
|
||||||
$body = $this->_t->getBody();
|
$body = $this->_t->getBody();
|
||||||
if (is_array($body))
|
if (is_array($body))
|
||||||
{
|
{
|
||||||
@@ -65,50 +88,147 @@ class TabularData extends Pagination
|
|||||||
<tr', (!empty($tr['class']) ? ' class="' . $tr['class'] . '"' : ''), '>';
|
<tr', (!empty($tr['class']) ? ' class="' . $tr['class'] . '"' : ''), '>';
|
||||||
|
|
||||||
foreach ($tr['cells'] as $td)
|
foreach ($tr['cells'] as $td)
|
||||||
|
{
|
||||||
echo '
|
echo '
|
||||||
<td', (!empty($td['width']) ? ' width="' . $td['width'] . '"' : ''), '>', $td['value'], '</td>';
|
<td', (!empty($td['width']) ? ' width="' . $td['width'] . '"' : ''), '>';
|
||||||
|
|
||||||
|
if (!empty($td['class']))
|
||||||
|
echo '<span class="', $td['class'], '">', $td['value'], '</span>';
|
||||||
|
else
|
||||||
|
echo $td['value'];
|
||||||
|
|
||||||
|
echo '</td>';
|
||||||
|
}
|
||||||
|
|
||||||
echo '
|
echo '
|
||||||
</tr>';
|
</tr>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// !!! Sum colspan!
|
||||||
else
|
else
|
||||||
echo '
|
echo '
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="', count($this->_t->getHeader()), '">', $body, '</td>
|
<td colspan="', count($header), '" class="fullwidth">', $body, '</td>
|
||||||
</tr>';
|
</tr>';
|
||||||
|
|
||||||
echo '
|
echo '
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>';
|
</table>';
|
||||||
|
|
||||||
// Maybe another small form?
|
if ($tableClass)
|
||||||
if (isset($this->_t->form_below))
|
echo '
|
||||||
$this->showForm($this->_t->form_below);
|
</div>';
|
||||||
|
|
||||||
// Showing a page index?
|
// Showing an inline form?
|
||||||
parent::html_content();
|
if (!empty($pager) || isset($this->_t->form_below))
|
||||||
|
{
|
||||||
|
echo '
|
||||||
|
<div class="row clearfix justify-content-end">';
|
||||||
|
|
||||||
echo '
|
// Page index?
|
||||||
|
if (!empty($pager))
|
||||||
|
PageIndexWidget::paginate($pager);
|
||||||
|
|
||||||
|
// Form controls?
|
||||||
|
if (isset($this->_t->form_below))
|
||||||
|
$this->showForm($this->_t->form_below);
|
||||||
|
|
||||||
|
echo '
|
||||||
|
</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($title))
|
||||||
|
echo '
|
||||||
</div>';
|
</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function showForm($form)
|
protected function showForm($form)
|
||||||
{
|
{
|
||||||
echo '
|
if (!isset($form['is_embed']))
|
||||||
<form action="', $form['action'], '" method="', $form['method'], '" class="table_form ', $form['class'], '">';
|
echo '
|
||||||
|
<form action="', $form['action'], '" method="', $form['method'], '" class="', $form['class'], '">';
|
||||||
|
else
|
||||||
|
echo '
|
||||||
|
<div class="', $form['class'], '">';
|
||||||
|
|
||||||
|
if (!empty($form['is_group']))
|
||||||
|
echo '
|
||||||
|
<div class="input-group">';
|
||||||
|
|
||||||
if (!empty($form['fields']))
|
if (!empty($form['fields']))
|
||||||
|
{
|
||||||
foreach ($form['fields'] as $name => $field)
|
foreach ($form['fields'] as $name => $field)
|
||||||
echo '
|
{
|
||||||
<input name="', $name, '" type="', $field['type'], '" placeholder="', $field['placeholder'], '"', isset($field['class']) ? ' class="' . $field['class'] . '"' : '', isset($field['value']) ? ' value="' . $field['value'] . '"' : '', '>';
|
if ($field['type'] === 'select')
|
||||||
|
{
|
||||||
|
echo '
|
||||||
|
<select class="form-select" name="', $name, '"', (isset($field['onchange']) ? ' onchange="' . $field['onchange'] . '"' : ''), '>';
|
||||||
|
|
||||||
|
foreach ($field['values'] as $value => $caption)
|
||||||
|
{
|
||||||
|
if (!is_array($caption))
|
||||||
|
{
|
||||||
|
echo '
|
||||||
|
<option value="', $value, '"', $value === $field['selected'] ? ' selected' : '', '>', $caption, '</option>';
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$label = $value;
|
||||||
|
$options = $caption;
|
||||||
|
|
||||||
|
echo '
|
||||||
|
<optgroup label="', $label, '">';
|
||||||
|
|
||||||
|
foreach ($options as $value => $caption)
|
||||||
|
{
|
||||||
|
echo '
|
||||||
|
<option value="', $value, '"', $value === $field['selected'] ? ' selected' : '', '>', $caption, '</option>';
|
||||||
|
}
|
||||||
|
|
||||||
|
echo '
|
||||||
|
</optgroup>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo '
|
||||||
|
</select>';
|
||||||
|
}
|
||||||
|
else
|
||||||
|
echo '
|
||||||
|
<input name="', $name, '" id="field_', $name, '" type="', $field['type'], '" placeholder="', $field['placeholder'], '" class="form-control', isset($field['class']) ? ' ' . $field['class'] : '', '"', isset($field['value']) ? ' value="' . htmlspecialchars($field['value']) . '"' : '', '>';
|
||||||
|
|
||||||
|
if (isset($field['html_after']))
|
||||||
|
echo $field['html_after'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo '
|
||||||
|
<input type="hidden" name="', Session::getSessionTokenKey(), '" value="', Session::getSessionToken(), '">';
|
||||||
|
|
||||||
if (!empty($form['buttons']))
|
if (!empty($form['buttons']))
|
||||||
foreach ($form['buttons'] as $name => $button)
|
foreach ($form['buttons'] as $name => $button)
|
||||||
|
{
|
||||||
echo '
|
echo '
|
||||||
<input name="', $name, '" type="', $button['type'], '" value="', $button['caption'], '" class="btn', isset($button['class']) ? ' ' . $button['class'] . '' : '', '">';
|
<button class="btn ', isset($button['class']) ? $button['class'] : 'btn-primary', '" type="', $button['type'], '" name="', $name, '"';
|
||||||
|
|
||||||
echo '
|
if (isset($button['onclick']))
|
||||||
</form>';
|
echo ' onclick="', $button['onclick'], '"';
|
||||||
|
|
||||||
|
echo '>', $button['caption'], '</button>';
|
||||||
|
|
||||||
|
if (isset($button['html_after']))
|
||||||
|
echo $button['html_after'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($form['is_group']))
|
||||||
|
echo '
|
||||||
|
</div>';
|
||||||
|
|
||||||
|
if (!isset($form['is_embed']))
|
||||||
|
echo '
|
||||||
|
</form>';
|
||||||
|
else
|
||||||
|
echo '
|
||||||
|
</div>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,14 +8,14 @@
|
|||||||
|
|
||||||
abstract class Template
|
abstract class Template
|
||||||
{
|
{
|
||||||
protected $_subtemplates = array();
|
protected $_subtemplates = [];
|
||||||
|
|
||||||
abstract public function html_main();
|
abstract public function html_main();
|
||||||
|
|
||||||
public function adopt(Template $template, $position = 'end')
|
public function adopt(Template $template, $position = 'end')
|
||||||
{
|
{
|
||||||
// By default, we append it.
|
// By default, we append it.
|
||||||
if ($position == 'end')
|
if ($position === 'end')
|
||||||
$this->_subtemplates[] = $template;
|
$this->_subtemplates[] = $template;
|
||||||
// We can also add it to the beginning of the list, though.
|
// We can also add it to the beginning of the list, though.
|
||||||
else
|
else
|
||||||
|
|||||||
29
templates/WarningDialog.php
Normal file
29
templates/WarningDialog.php
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
/*****************************************************************************
|
||||||
|
* WarningDialog.php
|
||||||
|
* Defines the WarningDialog template.
|
||||||
|
*
|
||||||
|
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
class WarningDialog extends Alert
|
||||||
|
{
|
||||||
|
protected $buttons;
|
||||||
|
|
||||||
|
public function __construct($title = '', $message = '', $buttons = [])
|
||||||
|
{
|
||||||
|
parent::__construct($title, $message);
|
||||||
|
$this->buttons = $buttons;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function additional_alert_content()
|
||||||
|
{
|
||||||
|
$this->addButtons();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function addButtons()
|
||||||
|
{
|
||||||
|
foreach ($this->buttons as $button)
|
||||||
|
$button->html_main();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user