Compare commits

..

13 Commits

Author SHA1 Message Date
187a7cd02f GenericTable: prevent passing NULL to strtotime 2022-07-14 16:45:32 +02:00
8414843bbf Prevent current page from being 0 if no items are present 2022-07-14 16:45:17 +02:00
474c387786 Add double-density thumbnails to albums and photo pages 2022-07-08 23:53:28 +02:00
12407d797d Address deprecation notices for certain function signatures 2022-07-08 23:52:03 +02:00
64d7433a56 Thumbnails: crop from original size if 2x is unavailable 2022-07-08 23:49:29 +02:00
58b7204fbf Do not delete thumbnail queue when replacing an asset
Thumbnails are normally created on demand, e.g. when processing the format codes in a post's body text.
Normally, the temporary URL is only used once to generate thumbnails ad-hoc. However, when cache is
enabled, a reference to the asset may be used in a cached version of a formatted body text, skipping
the normal thumbnail generation routine.

When an asset is replaced, currently, all thumbnails are removed and references to them are removed
from the database. In case the asset is still referenced in a cached formatted body text, this could lead
to an error when requesting the thumbnail, as the thumbnail request is no longer present in the system.

As we do not know what posts use particular assets at this point in the code, it is best to work around this
issue by unsetting the thumbnail filenames rather than deleting the entries outright. This effectively
generates them again on the next request.

In the future, we should aim to keep track of what posts make use of assets, so cache may be invalidated
in a more targeted way.
2022-07-08 23:49:20 +02:00
36a2779381 Don't try to generate double-density thumbs for small images 2022-07-08 23:49:13 +02:00
44bb501d13 Write new thumbnail filenames to parent Image object as well 2022-07-08 23:48:53 +02:00
9010123d18 Thumbnail class: minor refactor of generate method 2022-07-08 23:48:45 +02:00
e3b67c4022 Thumbnail class: refactor getUrl method 2022-07-08 23:48:38 +02:00
2bcdc5fe6e Split Image::getImageUrls from Image::getInlineImage 2022-07-08 23:48:30 +02:00
edfad992cc Rewrite Image::getInlineImage to support double density displays 2022-07-08 23:48:19 +02:00
357d95f6ff Add Image::getInlineImage method 2022-07-08 23:47:55 +02:00
89 changed files with 2917 additions and 4148 deletions

View File

@ -14,14 +14,5 @@
"models/", "models/",
"templates/" "templates/"
] ]
},
"require": {
"ext-mysqli": "*",
"ext-imagick": "*",
"ext-gd": "*",
"ext-imagick": "*",
"ext-mysqli": "*",
"twbs/bootstrap": "^5.3",
"twbs/bootstrap-icons": "^1.10"
} }
} }

View File

@ -1,134 +0,0 @@
<?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);
}
}
}

View File

@ -21,30 +21,22 @@ class Download
$tag = (int)$_GET['tag']; $tag = (int)$_GET['tag'];
$album = Tag::fromId($tag); $album = Tag::fromId($tag);
if (isset($_GET['by']) && ($user = Member::fromSlug($_GET['by'])) !== false)
$id_user_uploaded = $user->getUserId();
else
$id_user_uploaded = null;
if (isset($_SESSION['current_export'])) 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.'); 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? // So far so good?
$this->exportAlbum($album, $id_user_uploaded); $this->exportAlbum($album);
exit; exit;
} }
private function exportAlbum(Tag $album, $id_user_uploaded) private function exportAlbum(Tag $album)
{ {
$files = []; $files = [];
$album_ids = array_merge([$album->id_tag], $this->getChildAlbumIds($album->id_tag)); $album_ids = array_merge([$album->id_tag], $this->getChildAlbumIds($album->id_tag));
foreach ($album_ids as $album_id) foreach ($album_ids as $album_id)
{ {
$iterator = AssetIterator::getByOptions([ $iterator = AssetIterator::getByOptions(['id_tag' => $album_id]);
'id_tag' => $album_id,
'id_user_uploaded' => $id_user_uploaded,
]);
while ($asset = $iterator->next()) while ($asset = $iterator->next())
$files[] = join(DIRECTORY_SEPARATOR, [$asset->getSubdir(), $asset->getFilename()]); $files[] = join(DIRECTORY_SEPARATOR, [$asset->getSubdir(), $asset->getFilename()]);
} }
@ -79,9 +71,6 @@ class Download
// STDOUT should not block. // STDOUT should not block.
stream_set_blocking($pipes[1], 0); stream_set_blocking($pipes[1], 0);
// Allow this the download to take its time...
set_time_limit(0);
header('Pragma: no-cache'); header('Pragma: no-cache');
header('Content-Description: File Download'); header('Content-Description: File Download');
header('Content-disposition: attachment; filename="' . $album->tag . '.tar"'); header('Content-disposition: attachment; filename="' . $album->tag . '.tar"');

View File

@ -8,9 +8,6 @@
class EditAlbum extends HTMLController class EditAlbum extends HTMLController
{ {
private $form;
private $formview;
public function __construct() public function __construct()
{ {
// Ensure it's just admins at this point. // Ensure it's just admins at this point.
@ -21,9 +18,6 @@ class EditAlbum extends HTMLController
if (empty($id_tag) && !isset($_GET['add']) && $_GET['action'] !== 'addalbum') if (empty($id_tag) && !isset($_GET['add']) && $_GET['action'] !== 'addalbum')
throw new UnexpectedValueException('Requested album not found or not requesting a new album.'); 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? // Adding an album?
if (isset($_GET['add']) || $_GET['action'] === 'addalbum') if (isset($_GET['add']) || $_GET['action'] === 'addalbum')
{ {
@ -35,6 +29,7 @@ class EditAlbum extends HTMLController
elseif (isset($_GET['delete'])) elseif (isset($_GET['delete']))
{ {
// So far so good? // So far so good?
$album = Tag::fromId($id_tag);
if (Session::validateSession('get') && $album->kind === 'Album' && $album->delete()) if (Session::validateSession('get') && $album->kind === 'Album' && $album->delete())
{ {
header('Location: ' . BASEURL . '/managealbums/'); header('Location: ' . BASEURL . '/managealbums/');
@ -46,6 +41,7 @@ class EditAlbum extends HTMLController
// Editing one, then, surely. // Editing one, then, surely.
else else
{ {
$album = Tag::fromId($id_tag);
if ($album->kind !== 'Album') if ($album->kind !== 'Album')
trigger_error('Cannot edit album: not an album.', E_USER_ERROR); trigger_error('Cannot edit album: not an album.', E_USER_ERROR);
@ -65,68 +61,41 @@ class EditAlbum extends HTMLController
elseif (!$id_tag) elseif (!$id_tag)
$after_form = '<button name="submit_and_new" class="btn">Save and add another</button>'; $after_form = '<button name="submit_and_new" class="btn">Save and add another</button>';
// Gather possible parents for this album to be filed into $form = new Form([
$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']);
}
$this->form = new Form([
'request_url' => BASEURL . '/editalbum/?' . ($id_tag ? 'id=' . $id_tag : 'add'), 'request_url' => BASEURL . '/editalbum/?' . ($id_tag ? 'id=' . $id_tag : 'add'),
'content_below' => $after_form, 'content_below' => $after_form,
'fields' => $fields, 'fields' => [
'id_parent' => [
'type' => 'numeric',
'label' => 'Parent album ID',
],
'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,
],
],
]); ]);
// Add defaults for album if none present
if (empty($_POST) && isset($_GET['tag'])) if (empty($_POST) && isset($_GET['tag']))
{ {
$parentTag = Tag::fromId($_GET['tag']); $parentTag = Tag::fromId($_GET['tag']);
@ -139,65 +108,28 @@ class EditAlbum extends HTMLController
]; ];
} }
} }
elseif (empty($_POST) && count($parentChoices) > 1)
{
// Choose the first non-root album as the default parent
reset($parentChoices);
next($parentChoices);
$formDefaults = ['id_parent' => key($parentChoices)];
}
if (!isset($formDefaults)) if (!isset($formDefaults))
$formDefaults = isset($album) ? get_object_vars($album) : $_POST; $formDefaults = isset($album) ? get_object_vars($album) : $_POST;
// Create the form, add in default values. // Create the form, add in default values.
$this->form->setData($formDefaults); $form->setData($formDefaults);
$this->formview = new FormView($this->form, $form_title ?? ''); $formview = new FormView($form, $form_title ?? '');
$this->page->adopt($this->formview); $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($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($id_tag, $album)
{
if (!empty($_POST)) if (!empty($_POST))
{ {
$this->form->verify($_POST); $form->verify($_POST);
// Anything missing? // Anything missing?
if (!empty($this->form->getMissing())) if (!empty($form->getMissing()))
return $this->formview->adopt(new Alert('Some data missing', 'Please fill out the following fields: ' . implode(', ', $this->form->getMissing()), 'danger')); return $formview->adopt(new Alert('Some data missing', 'Please fill out the following fields: ' . implode(', ', $form->getMissing()), 'error'));
$data = $this->form->getData(); $data = $form->getData();
// Sanity check: don't let an album be its own parent
if ($data['id_parent'] == $id_tag)
{
return $this->formview->adopt(new Alert('Invalid parent', 'An album cannot be its own parent.', 'danger'));
}
// Quick stripping. // Quick stripping.
$data['tag'] = htmlspecialchars($data['tag']); $data['tag'] = htmlentities($data['tag']);
$data['description'] = htmlspecialchars($data['description']); $data['description'] = htmlentities($data['description']);
$data['slug'] = strtr($data['slug'], [' ' => '-', '--' => '-', '&' => 'and', '=>' => '', "'" => "", ":"=> "", '\\' => '-']); $data['slug'] = strtr($data['slug'], [' ' => '-', '--' => '-', '&' => 'and', '=>' => '', "'" => "", ":"=> "", '\\' => '-']);
// TODO: when updating slug, update slug for all photos in this album. // TODO: when updating slug, update slug for all photos in this album.
@ -208,7 +140,7 @@ class EditAlbum extends HTMLController
$data['kind'] = 'Album'; $data['kind'] = 'Album';
$newTag = Tag::createNew($data); $newTag = Tag::createNew($data);
if ($newTag === false) if ($newTag === false)
return $this->formview->adopt(new Alert('Cannot create this album', 'Something went wrong while creating the album...', 'danger')); return $formview->adopt(new Alert('Cannot create this album', 'Something went wrong while creating the album...', 'error'));
if (isset($_POST['submit_and_new'])) if (isset($_POST['submit_and_new']))
{ {

View File

@ -10,6 +10,10 @@ class EditAsset extends HTMLController
{ {
public function __construct() public function __construct()
{ {
// Ensure it's just admins at this point.
if (!Registry::get('user')->isAdmin())
throw new NotAllowedException();
if (empty($_GET['id'])) if (empty($_GET['id']))
throw new Exception('Invalid request.'); throw new Exception('Invalid request.');
@ -17,72 +21,8 @@ class EditAsset extends HTMLController
if (empty($asset)) if (empty($asset))
throw new NotFoundException('Asset not found'); throw new NotFoundException('Asset not found');
// Can we edit this asset? if (isset($_REQUEST['delete']))
$user = Registry::get('user'); throw new Exception('Not implemented.');
if (!($user->isAdmin() || $asset->isOwnedBy($user)))
throw new NotAllowedException();
if (isset($_REQUEST['delete']) && Session::validateSession('get'))
{
$redirectUrl = BASEURL . '/' . $asset->getSubdir();
$asset->delete();
header('Location: ' . $redirectUrl);
exit;
}
else
{
$isPrioChange = isset($_REQUEST['inc_prio']) || isset($_REQUEST['dec_prio']);
$isCoverChange = isset($_REQUEST['album_cover'], $_REQUEST['in']);
$madeChanges = false;
if ($user->isAdmin() && $isPrioChange && Session::validateSession('get'))
{
if (isset($_REQUEST['inc_prio']))
$priority = $asset->priority + 1;
else
$priority = $asset->priority - 1;
$asset->priority = max(0, min(100, $priority));
$asset->save();
$madeChanges = true;
}
elseif ($user->isAdmin() && $isCoverChange && Session::validateSession('get'))
{
$tag = Tag::fromId($_REQUEST['in']);
$tag->id_asset_thumb = $asset->getId();
$tag->save();
$madeChanges = true;
}
if ($madeChanges)
{
if (isset($_SERVER['HTTP_REFERER']))
header('Location: ' . $_SERVER['HTTP_REFERER']);
else
header('Location: ' . BASEURL . '/' . $asset->getSubdir());
exit;
}
}
// Get a list of available photo albums
$allAlbums = [];
foreach (PhotoAlbum::getHierarchy('tag', 'up') as $album)
$allAlbums[$album['id_tag']] = $album['tag'];
// Figure out the current album id
$currentAlbumId = 0;
$currentAlbumSlug = '';
$currentTags = $asset->getTags();
foreach ($currentTags as $tag)
{
if ($tag->kind === 'Album')
{
$currentAlbumId = $tag->id_tag;
$currentAlbumSlug = $tag->slug;
break;
}
}
if (!empty($_POST)) if (!empty($_POST))
{ {
@ -95,46 +35,17 @@ class EditAsset extends HTMLController
// Key info // Key info
if (isset($_POST['title'], $_POST['slug'], $_POST['date_captured'], $_POST['priority'])) if (isset($_POST['title'], $_POST['slug'], $_POST['date_captured'], $_POST['priority']))
{ {
$asset->date_captured = !empty($_POST['date_captured']) ? $date_captured = !empty($_POST['date_captured']) ? new DateTime($_POST['date_captured']) : null;
new DateTime(str_replace('T', ' ', $_POST['date_captured'])) : null; $slug = strtr($_POST['slug'], [' ' => '-', '--' => '-', '&' => 'and', '=>' => '', "'" => "", ":"=> "", '\\' => '-']);
$asset->slug = Asset::cleanSlug($_POST['slug']); $asset->setKeyData(htmlentities($_POST['title']), $slug, $date_captured, intval($_POST['priority']));
$asset->title = htmlspecialchars($_POST['title']);
$asset->priority = intval($_POST['priority']);
$asset->save();
}
// Changing parent album?
if ($_POST['id_album'] != $currentAlbumId)
{
$targetAlbum = Tag::fromId($_POST['id_album']);
// First move the asset, then sort out the album tag
if (($retCode = $asset->moveToSubDir($targetAlbum->slug)) === true)
{
if (!isset($_POST['tag']))
$_POST['tag'] = [];
// Unset tag for current parent album
if (isset($_POST['tag'][$currentAlbumId]))
unset($_POST['tag'][$currentAlbumId]);
// Set tag for new parent album
$_POST['tag'][$_POST['id_album']] = true;
}
}
else
{
$_POST['tag'][$currentAlbumId] = true;
} }
// Handle tags // Handle tags
$new_tags = []; $new_tags = [];
if (isset($_POST['tag']) && is_array($_POST['tag'])) if (isset($_POST['tag']) && is_array($_POST['tag']))
{
foreach ($_POST['tag'] as $id_tag => $bool) foreach ($_POST['tag'] as $id_tag => $bool)
if (is_numeric($id_tag)) if (is_numeric($id_tag))
$new_tags[] = $id_tag; $new_tags[] = $id_tag;
}
$current_tags = array_keys($asset->getTags()); $current_tags = array_keys($asset->getTags());
@ -177,15 +88,16 @@ class EditAsset extends HTMLController
header('Location: ' . BASEURL . '/editasset/?id=' . $asset->getId()); header('Location: ' . BASEURL . '/editasset/?id=' . $asset->getId());
} }
$page = new EditAssetForm([ // Get list of thumbnails
'asset' => $asset, $thumbs = $this->getThumbs($asset);
'thumbs' => $this->getThumbs($asset),
'allAlbums' => $allAlbums,
'currentAlbumId' => $currentAlbumId,
]);
$page = new EditAssetForm($asset, $thumbs);
parent::__construct('Edit asset \'' . $asset->getTitle() . '\' (' . $asset->getFilename() . ') - ' . SITE_TITLE); parent::__construct('Edit asset \'' . $asset->getTitle() . '\' (' . $asset->getFilename() . ') - ' . SITE_TITLE);
$this->page->adopt($page); $this->page->adopt($page);
// Add a view button to the admin bar for photos.
if ($asset->isImage())
$this->admin_bar->appendItem($asset->getImage()->getPageUrl(), 'View this photo');
} }
private function getThumbs(Asset $asset) private function getThumbs(Asset $asset)

View File

@ -10,18 +10,14 @@ class EditTag extends HTMLController
{ {
public function __construct() 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; $id_tag = isset($_GET['id']) ? (int) $_GET['id'] : 0;
if (empty($id_tag) && !isset($_GET['add'])) if (empty($id_tag) && !isset($_GET['add']))
throw new UnexpectedValueException('Requested tag not found or not requesting a new tag.'); 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? // Adding an tag?
if (isset($_GET['add'])) if (isset($_GET['add']))
{ {
@ -33,6 +29,7 @@ class EditTag extends HTMLController
elseif (isset($_GET['delete'])) elseif (isset($_GET['delete']))
{ {
// So far so good? // So far so good?
$tag = Tag::fromId($id_tag);
if (Session::validateSession('get') && $tag->kind !== 'Album' && $tag->delete()) if (Session::validateSession('get') && $tag->kind !== 'Album' && $tag->delete())
{ {
header('Location: ' . BASEURL . '/managetags/'); header('Location: ' . BASEURL . '/managetags/');
@ -44,6 +41,7 @@ class EditTag extends HTMLController
// Editing one, then, surely. // Editing one, then, surely.
else else
{ {
$tag = Tag::fromId($id_tag);
if ($tag->kind === 'Album') if ($tag->kind === 'Album')
trigger_error('Cannot edit tag: is actually an album.', E_USER_ERROR); trigger_error('Cannot edit tag: is actually an album.', E_USER_ERROR);
@ -63,51 +61,47 @@ class EditTag extends HTMLController
elseif (!$id_tag) elseif (!$id_tag)
$after_form = '<button name="submit_and_new" class="btn">Save and add another</button>'; $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([ $form = new Form([
'request_url' => BASEURL . '/edittag/?' . ($id_tag ? 'id=' . $id_tag : 'add'), 'request_url' => BASEURL . '/edittag/?' . ($id_tag ? 'id=' . $id_tag : 'add'),
'content_below' => $after_form, 'content_below' => $after_form,
'fields' => $fields, 'fields' => [
'id_parent' => [
'type' => 'numeric',
'label' => 'Parent tag ID',
],
'id_asset_thumb' => [
'type' => 'numeric',
'label' => 'Thumbnail asset ID',
'is_optional' => true,
],
'kind' => [
'type' => 'select',
'label' => 'Kind of tag',
'options' => [
'Location' => 'Location',
'Person' => 'Person',
],
],
'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,
],
],
]); ]);
// Create the form, add in default values. // Create the form, add in default values.
@ -115,48 +109,15 @@ class EditTag extends HTMLController
$formview = new FormView($form, $form_title ?? ''); $formview = new FormView($form, $form_title ?? '');
$this->page->adopt($formview); $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)) if (!empty($_POST))
{ {
$form->verify($_POST); $form->verify($_POST);
// Anything missing? // Anything missing?
if (!empty($form->getMissing())) if (!empty($form->getMissing()))
return $formview->adopt(new Alert('Some data missing', 'Please fill out the following fields: ' . implode(', ', $form->getMissing()), 'danger')); return $formview->adopt(new Alert('Some data missing', 'Please fill out the following fields: ' . implode(', ', $form->getMissing()), 'error'));
$data = $form->getData(); $data = $form->getData();
$data['id_parent'] = 0;
// Quick stripping. // Quick stripping.
$data['slug'] = strtr($data['slug'], [' ' => '-', '--' => '-', '&' => 'and', '=>' => '', "'" => "", ":"=> "", '/' => '-', '\\' => '-']); $data['slug'] = strtr($data['slug'], [' ' => '-', '--' => '-', '&' => 'and', '=>' => '', "'" => "", ":"=> "", '/' => '-', '\\' => '-']);
@ -166,7 +127,7 @@ class EditTag extends HTMLController
{ {
$return = Tag::createNew($data); $return = Tag::createNew($data);
if ($return === false) if ($return === false)
return $formview->adopt(new Alert('Cannot create this tag', 'Something went wrong while creating the tag...', 'danger')); return $formview->adopt(new Alert('Cannot create this tag', 'Something went wrong while creating the tag...', 'error'));
if (isset($_POST['submit_and_new'])) if (isset($_POST['submit_and_new']))
{ {
@ -183,11 +144,8 @@ class EditTag extends HTMLController
$tag->save(); $tag->save();
} }
// Redirect to a clean page // Redirect to the tag management page.
if (Registry::get('user')->isAdmin()) header('Location: ' . BASEURL . '/managetags/');
header('Location: ' . BASEURL . '/managetags/');
else
header('Location: ' . BASEURL . '/edittag/?id=' . $id_tag);
exit; exit;
} }
} }

View File

@ -129,13 +129,13 @@ class EditUser extends HTMLController
// Anything missing? // Anything missing?
if (!empty($form->getMissing())) if (!empty($form->getMissing()))
return $formview->adopt(new Alert('Some data missing', 'Please fill out the following fields: ' . implode(', ', $form->getMissing()), 'danger')); return $formview->adopt(new Alert('Some data missing', 'Please fill out the following fields: ' . implode(', ', $form->getMissing()), 'error'));
$data = $form->getData(); $data = $form->getData();
// Just to be on the safe side. // Just to be on the safe side.
$data['first_name'] = htmlspecialchars(trim($data['first_name'])); $data['first_name'] = htmlentities(trim($data['first_name']));
$data['surname'] = htmlspecialchars(trim($data['surname'])); $data['surname'] = htmlentities(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.
@ -150,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 Alert('Email addresses invalid', 'The email address you entered is not a valid email address.', 'danger')); return $formview->adopt(new Alert('Email addresses invalid', 'The email address you entered is not a valid email address.', 'error'));
// 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 Alert('Email address already in use', 'Another account is already using the e-mail address you entered.', 'danger')); return $formview->adopt(new Alert('Email address already in use', 'Another account is already using the e-mail address you entered.', 'error'));
// 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 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')); 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).', 'error'));
elseif ($data['password1'] !== $data['password2']) 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')); return $formview->adopt(new Alert('Passwords do not match', 'The passwords you entered do not match. Please try again.', 'error'));
else else
$data['password'] = $data['password1']; $data['password'] = $data['password1'];
@ -173,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 Alert('Cannot create this user', 'Something went wrong while creating the user...', 'danger')); return $formview->adopt(new Alert('Cannot create this user', 'Something went wrong while creating the user...', 'error'));
if (isset($_POST['submit_and_new'])) if (isset($_POST['submit_and_new']))
{ {

View File

@ -12,6 +12,7 @@
abstract class HTMLController abstract class HTMLController
{ {
protected $page; protected $page;
protected $admin_bar;
public function __construct($title) public function __construct($title)
{ {
@ -21,6 +22,8 @@ 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);
} }
} }

View File

@ -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.', 'danger')); $form->adopt(new Alert('', 'Invalid email address or password.', 'error'));
// Tried anything? Be helpful, at least. // Tried anything? Be helpful, at least.
if (isset($_POST['emailaddress'])) if (isset($_POST['emailaddress']))

View File

@ -11,7 +11,7 @@ class Logout extends HTMLController
public function __construct() public function __construct()
{ {
// Clear the entire sesssion. // Clear the entire sesssion.
Session::clear(); $_SESSION = [];
// Back to the frontpage you go. // Back to the frontpage you go.
header('Location: ' . BASEURL); header('Location: ' . BASEURL);

View File

@ -18,7 +18,7 @@ class ManageAlbums extends HTMLController
'form' => [ 'form' => [
'action' => BASEURL . '/editalbum/', 'action' => BASEURL . '/editalbum/',
'method' => 'get', 'method' => 'get',
'class' => 'col-md-6 text-end', 'class' => 'floatright',
'buttons' => [ 'buttons' => [
'add' => [ 'add' => [
'type' => 'submit', 'type' => 'submit',
@ -60,7 +60,7 @@ class ManageAlbums extends HTMLController
'title' => 'Manage albums', 'title' => 'Manage albums',
'no_items_label' => 'No albums meet the requirements of the current filter.', 'no_items_label' => 'No albums meet the requirements of the current filter.',
'items_per_page' => 9999, 'items_per_page' => 9999,
'index_class' => 'col-md-6', 'index_class' => 'floatleft',
'base_url' => BASEURL . '/managealbums/', 'base_url' => BASEURL . '/managealbums/',
'get_data' => function($offset = 0, $limit = 9999, $order = '', $direction = 'up') { 'get_data' => function($offset = 0, $limit = 9999, $order = '', $direction = 'up') {
if (!in_array($order, ['id_tag', 'tag', 'slug', 'count'])) if (!in_array($order, ['id_tag', 'tag', 'slug', 'count']))
@ -68,7 +68,28 @@ class ManageAlbums extends HTMLController
if (!in_array($direction, ['up', 'down'])) if (!in_array($direction, ['up', 'down']))
$direction = 'up'; $direction = 'up';
$rows = PhotoAlbum::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 [ return [
'rows' => $rows, 'rows' => $rows,
@ -85,4 +106,42 @@ class ManageAlbums extends HTMLController
parent::__construct('Album management - Page ' . $table->getCurrentPage() .' - ' . SITE_TITLE); parent::__construct('Album management - Page ' . $table->getCurrentPage() .' - ' . SITE_TITLE);
$this->page->adopt(new TabularData($table)); $this->page->adopt(new TabularData($table));
} }
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;
}
} }

View File

@ -14,37 +14,10 @@ class ManageAssets extends HTMLController
if (!Registry::get('user')->isAdmin()) if (!Registry::get('user')->isAdmin())
throw new NotAllowedException(); throw new NotAllowedException();
if (isset($_POST['deleteChecked'], $_POST['delete']) && Session::validateSession())
$this->handleAssetDeletion();
Session::resetSessionToken(); Session::resetSessionToken();
$options = [ $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' => [ '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' => [ 'id_asset' => [
'value' => 'id_asset', 'value' => 'id_asset',
'header' => 'ID', 'header' => 'ID',
@ -65,18 +38,13 @@ class ManageAssets extends HTMLController
'data' => 'filename', 'data' => 'filename',
], ],
], ],
'id_user_uploaded' => [ 'title' => [
'header' => 'User uploaded', 'header' => 'Title',
'is_sortable' => true, 'is_sortable' => true,
'parse' => [ 'parse' => [
'type' => 'function', 'type' => 'value',
'data' => function($row) { 'link' => BASEURL . '/editasset/?id={ID_ASSET}',
if (!empty($row['id_user'])) 'data' => 'title',
return sprintf('<a href="%s/edituser/?id=%d">%s</a>', BASEURL, $row['id_user'],
$row['first_name'] . ' ' . $row['surname']);
else
return 'n/a';
},
], ],
], ],
'dimensions' => [ 'dimensions' => [
@ -99,18 +67,15 @@ class ManageAssets extends HTMLController
'title' => 'Manage assets', 'title' => 'Manage assets',
'no_items_label' => 'No assets meet the requirements of the current filter.', 'no_items_label' => 'No assets meet the requirements of the current filter.',
'items_per_page' => 30, 'items_per_page' => 30,
'index_class' => 'col-md-6', 'index_class' => 'pull_left',
'base_url' => BASEURL . '/manageassets/', 'base_url' => BASEURL . '/manageassets/',
'get_data' => function($offset = 0, $limit = 30, $order = '', $direction = 'down') { 'get_data' => function($offset = 0, $limit = 30, $order = '', $direction = 'down') {
if (!in_array($order, ['id_asset', 'id_user_uploaded', 'title', 'subdir', 'filename'])) if (!in_array($order, ['id_asset', 'title', 'subdir', 'filename']))
$order = 'id_asset'; $order = 'id_asset';
$data = Registry::get('db')->queryAssocs(' $data = Registry::get('db')->queryAssocs('
SELECT a.id_asset, a.subdir, a.filename, SELECT id_asset, title, subdir, filename, image_width, image_height
a.image_width, a.image_height, FROM assets
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} ORDER BY {raw:order}
LIMIT {int:offset}, {int:limit}', LIMIT {int:offset}, {int:limit}',
[ [
@ -129,25 +94,7 @@ class ManageAssets extends HTMLController
]; ];
$table = new GenericTable($options); $table = new GenericTable($options);
parent::__construct('Asset management - Page ' . $table->getCurrentPage()); parent::__construct('Asset management - Page ' . $table->getCurrentPage() .'');
$this->page->adopt(new TabularData($table));
$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;
} }
} }

View File

@ -29,12 +29,11 @@ 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' => 'col-md-6 text-end', 'class' => 'floatright',
'buttons' => [ 'buttons' => [
'flush' => [ 'flush' => [
'type' => 'submit', 'type' => 'submit',
'caption' => 'Delete all', 'caption' => 'Delete all',
'class' => 'btn-danger',
], ],
], ],
], ],
@ -100,7 +99,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' => 'col-md-6', 'index_class' => 'floatleft',
'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') {

View File

@ -14,13 +14,11 @@ 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' => [ 'form' => [
'action' => BASEURL . '/edittag/', 'action' => BASEURL . '/edittag/',
'method' => 'get', 'method' => 'get',
'class' => 'col-md-6 text-end', 'class' => 'floatright',
'buttons' => [ 'buttons' => [
'add' => [ 'add' => [
'type' => 'submit', 'type' => 'submit',
@ -50,19 +48,10 @@ class ManageTags extends HTMLController
'data' => 'slug', 'data' => 'slug',
], ],
], ],
'id_user_owner' => [ 'kind' => [
'header' => 'Owning user', 'header' => 'Kind',
'is_sortable' => true, 'is_sortable' => true,
'parse' => [ 'value' => 'kind',
'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',
@ -76,7 +65,7 @@ class ManageTags extends HTMLController
'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' => 30, 'items_per_page' => 30,
'index_class' => 'col-md-6', 'index_class' => 'floatleft',
'base_url' => BASEURL . '/managetags/', 'base_url' => BASEURL . '/managetags/',
'get_data' => function($offset = 0, $limit = 30, $order = '', $direction = 'up') { 'get_data' => function($offset = 0, $limit = 30, $order = '', $direction = 'up') {
if (!in_array($order, ['id_tag', 'tag', 'slug', 'kind', 'count'])) if (!in_array($order, ['id_tag', 'tag', 'slug', 'kind', 'count']))
@ -85,9 +74,8 @@ class ManageTags extends HTMLController
$direction = 'up'; $direction = 'up';
$data = Registry::get('db')->queryAssocs(' $data = Registry::get('db')->queryAssocs('
SELECT t.*, u.id_user, u.first_name, u.surname SELECT *
FROM tags AS t FROM tags
LEFT JOIN users AS u ON t.id_user_owner = u.id_user
WHERE kind != {string:album} WHERE kind != {string:album}
ORDER BY {raw:order} ORDER BY {raw:order}
LIMIT {int:offset}, {int:limit}', LIMIT {int:offset}, {int:limit}',

View File

@ -14,13 +14,11 @@ 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' => 'col-md-6 text-end', 'class' => 'floatright',
'buttons' => [ 'buttons' => [
'add' => [ 'add' => [
'type' => 'submit', 'type' => 'submit',
@ -96,7 +94,7 @@ class ManageUsers extends HTMLController
'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' => 30, 'items_per_page' => 30,
'index_class' => 'col-md-6', 'index_class' => 'floatleft',
'base_url' => BASEURL . '/manageusers/', 'base_url' => BASEURL . '/manageusers/',
'get_data' => function($offset = 0, $limit = 30, $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']))

View File

@ -57,7 +57,7 @@ class ProvideAutoSuggest extends JSONController
return; return;
} }
$label = htmlspecialchars(trim($_REQUEST['tag'])); $label = htmlentities(trim($_REQUEST['tag']));
$slug = strtr($label, [' ' => '-']); $slug = strtr($label, [' ' => '-']);
$tag = Tag::createNew([ $tag = Tag::createNew([
'tag' => $label, 'tag' => $label,

View File

@ -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>', 'danger')); $form->adopt(new Alert('Some fields require your attention', '<ul><li>' . implode('</li><li>', $missing) . '</li></ul>', 'error'));
} }
} }
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.', 'danger')); $form->adopt(new Alert('Invalid email address', 'The email address you provided could not be found in our system. Please try again.', 'error'));
return; return;
} }

View File

@ -33,20 +33,21 @@ class UploadMedia extends HTMLController
if (empty($uploaded_file)) if (empty($uploaded_file))
continue; continue;
// DIY slug club.
$slug = $tag->slug . '/' . strtr($uploaded_file['name'], [' ' => '-', '--' => '-', '&' => 'and', '=>' => '', "'" => "", ":"=> "", '\\' => '-']);
$asset = Asset::createNew([ $asset = Asset::createNew([
'filename_to_copy' => $uploaded_file['tmp_name'], 'filename_to_copy' => $uploaded_file['tmp_name'],
'preferred_filename' => $uploaded_file['name'], 'preferred_filename' => $uploaded_file['name'],
'preferred_subdir' => $tag->slug, 'preferred_subdir' => $tag->slug,
'slug' => $slug,
]); ]);
$new_ids[] = $asset->getId(); $new_ids[] = $asset->getId();
$asset->linkTags([$tag->id_tag]); $asset->linkTags([$tag->id_tag]);
if (empty($tag->id_asset_thumb)) $tag->id_asset_thumb = $asset->getId();
{ $tag->save();
$tag->id_asset_thumb = $asset->getId();
$tag->save();
}
} }
if (isset($_REQUEST['format']) && $_REQUEST['format'] === 'json') if (isset($_REQUEST['format']) && $_REQUEST['format'] === 'json')

View File

@ -52,9 +52,8 @@ 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 mt-5 justify-content-around justify-content-lg-center',
]); ]);
$this->page->adopt(new PageIndexWidget($pagination)); $this->page->adopt(new Pagination($pagination));
$this->page->setCanonicalUrl(BASEURL . '/people/' . ($page > 1 ? 'page/' . $page . '/' : '')); $this->page->setCanonicalUrl(BASEURL . '/people/' . ($page > 1 ? 'page/' . $page . '/' : ''));
} }

View File

@ -8,8 +8,6 @@
class ViewPhoto extends HTMLController class ViewPhoto extends HTMLController
{ {
private Image $photo;
public function __construct() public function __construct()
{ {
// Ensure we're logged in at this point. // Ensure we're logged in at this point.
@ -21,48 +19,74 @@ class ViewPhoto extends HTMLController
if (empty($photo)) if (empty($photo))
throw new NotFoundException(); throw new NotFoundException();
$this->photo = $photo->getImage(); parent::__construct($photo->getTitle() . ' - ' . SITE_TITLE);
Session::resetSessionToken(); $author = $photo->getAuthor();
parent::__construct($this->photo->getTitle() . ' - ' . SITE_TITLE); if (isset($_REQUEST['confirm_delete']) || isset($_REQUEST['delete_confirmed']))
$this->handleConfirmDelete($user, $author, $photo);
if (!empty($_POST))
$this->handleTagging();
else else
$this->handleViewPhoto(); $this->handleViewPhoto($user, $author, $photo);
// Add an edit button to the admin bar.
if ($user->isAdmin())
$this->admin_bar->appendItem(BASEURL . '/editasset/?id=' . $photo->getId(), 'Edit this photo');
} }
private function handleViewPhoto() private function handleConfirmDelete(User $user, User $author, Asset $photo)
{ {
$page = new PhotoPage($this->photo); if (!($user->isAdmin() || $user->getUserId() === $author->getUserId()))
throw new NotAllowedException();
// Any (EXIF) meta data? if (isset($_REQUEST['confirm_delete']))
$metaData = $this->prepareMetaData(); {
$page->setMetaData($metaData); $page = new ConfirmDeletePage($photo->getImage());
$this->page->adopt($page);
}
else if (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))
$this->handleTagging($photo->getImage());
$page = new PhotoPage($photo->getImage());
// Exif data?
$exif = EXIF::fromFile($photo->getFullPath());
if ($exif)
$page->setExif($exif);
// What tag are we browsing? // What tag are we browsing?
$tag = isset($_GET['in']) ? Tag::fromId($_GET['in']) : null; $tag = isset($_GET['in']) ? Tag::fromId($_GET['in']) : null;
if (isset($tag)) $id_tag = isset($tag) ? $tag->id_tag : null;
$page->setTag($tag);
// Keeping tabs on a filter? // Find previous photo in set.
if (isset($_GET['by'])) $previous_url = $photo->getUrlForPreviousInSet($id_tag);
{ if ($previous_url)
// Let's first verify that the filter is valid $page->setPreviousPhotoUrl($previous_url);
$user = Member::fromSlug($_GET['by']);
if (!$user)
throw new UnexpectedValueException('Invalid filter for this album or tag.');
// Alright, let's run with it then // ... and the next photo, too.
$page->setActiveFilter($user->getSlug()); $next_url = $photo->getUrlForNextInSet($id_tag);
} if ($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($this->photo->getPageUrl()); $this->page->setCanonicalUrl($photo->getPageUrl());
} }
private function handleTagging() private function handleTagging(Image $photo)
{ {
header('Content-Type: text/json; charset=utf-8'); header('Content-Type: text/json; charset=utf-8');
@ -76,7 +100,7 @@ class ViewPhoto extends HTMLController
// We are! // We are!
if (!isset($_POST['delete'])) if (!isset($_POST['delete']))
{ {
$this->photo->linkTags([(int) $_POST['id_tag']]); $photo->linkTags([(int) $_POST['id_tag']]);
echo json_encode(['success' => true]); echo json_encode(['success' => true]);
exit; exit;
} }
@ -84,43 +108,9 @@ class ViewPhoto extends HTMLController
// ... deleting, that is. // ... deleting, that is.
else else
{ {
$this->photo->unlinkTags([(int) $_POST['id_tag']]); $photo->unlinkTags([(int) $_POST['id_tag']]);
echo json_encode(['success' => true]); echo json_encode(['success' => true]);
exit; exit;
} }
} }
private function prepareMetaData()
{
if (!($exif = EXIF::fromFile($this->photo->getFullPath())))
throw new UnexpectedValueException('Photo file not found!');
$metaData = [];
if (!empty($exif->created_timestamp))
$metaData['Date Taken'] = date("j M Y, H:i:s", $exif->created_timestamp);
if ($author = $this->photo->getAuthor())
$metaData['Uploaded by'] = $author->getfullName();
if (!empty($exif->camera))
$metaData['Camera Model'] = $exif->camera;
if (!empty($exif->shutter_speed))
$metaData['Shutter Speed'] = $exif->shutterSpeedFraction();
if (!empty($exif->aperture))
$metaData['Aperture'] = 'f/' . number_format($exif->aperture, 1);
if (!empty($exif->focal_length))
$metaData['Focal Length'] = $exif->focal_length . ' mm';
if (!empty($exif->iso))
$metaData['ISO Speed'] = $exif->iso;
if (!empty($exif->software))
$metaData['Software'] = $exif->software;
return $metaData;
}
} }

View File

@ -26,92 +26,80 @@ class ViewPhotoAlbum extends HTMLController
$tag = Tag::fromSlug($_GET['tag']); $tag = Tag::fromSlug($_GET['tag']);
$id_tag = $tag->id_tag; $id_tag = $tag->id_tag;
$title = $tag->tag; $title = $tag->tag;
$header_box = $this->getHeaderBox($tag); $description = !empty($tag->description) ? $tag->description : '';
// Can we go up a level?
if ($tag->id_parent != 0)
{
$ptag = Tag::fromId($tag->id_parent);
$back_link = BASEURL . '/' . (!empty($ptag->slug) ? $ptag->slug . '/' : '');
$back_link_title = 'Back to &quot;' . $ptag->tag . '&quot;';
}
elseif ($tag->kind === 'Person')
{
$back_link = BASEURL . '/people/';
$back_link_title = 'Back to &quot;People&quot;';
$is_person = true;
}
$header_box = new AlbumHeaderBox($title, $description, $back_link, $back_link_title);
} }
// View the album root. // View the album root.
else else
{ {
$id_tag = 1; $id_tag = 1;
$tag = Tag::fromId($id_tag);
$title = 'Albums'; $title = 'Albums';
} }
// What page are we at? // What page are we at?
$current_page = isset($_GET['page']) ? (int) $_GET['page'] : 1; $page = isset($_GET['page']) ? (int) $_GET['page'] : 1;
parent::__construct($title . ' - Page ' . $current_page . ' - ' . SITE_TITLE); parent::__construct($title . ' - Page ' . $page . ' - ' . SITE_TITLE);
if (isset($header_box)) if (isset($header_box))
$this->page->adopt($header_box); $this->page->adopt($header_box);
// Who contributed to this album? // Can we do fancy things here?
$contributors = $tag->getContributorList(); // !!! TODO: permission system?
$buttons = [];
// Enumerate possible filters if (Registry::get('user')->isLoggedIn())
$filters = [];
if (!empty($contributors))
{ {
$filters[''] = ['id_user' => null, 'label' => '', 'caption' => 'All photos', $buttons[] = [
'link' => $tag->getUrl()]; 'url' => BASEURL . '/download/?tag=' . $id_tag,
'caption' => 'Download this album',
];
foreach ($contributors as $contributor) $buttons[] = [
{ 'url' => BASEURL . '/uploadmedia/?tag=' . $id_tag,
$filters[$contributor['slug']] = [ 'caption' => 'Upload new photos here',
'id_user' => $contributor['id_user'], ];
'label' => $contributor['first_name'],
'caption' => sprintf('By %s (%s photos)',
$contributor['first_name'], $contributor['num_assets']),
'link' => $tag->getUrl() . '?by=' . $contributor['slug'],
];
}
} }
if (Registry::get('user')->isAdmin())
$buttons[] = [
'url' => BASEURL . '/addalbum/?tag=' . $id_tag,
'caption' => 'Create new subalbum here',
];
// Limit to a particular uploader? // Enough actions for a button box?
$active_filter = ''; if (!empty($buttons))
$id_user_uploaded = null; $this->page->adopt(new AlbumButtonBox($buttons));
if (!empty($_GET['by']))
{
if (!isset($filters[$_GET['by']]))
throw new UnexpectedValueException('Invalid filter for this album or tag.');
$active_filter = $_GET['by'];
$id_user_uploaded = $filters[$active_filter]['id_user'];
$filters[$active_filter]['is_active'] = true;
}
// Add an interface to query and modify the album/tag
$buttons = $this->getAlbumButtons($tag, $active_filter);
$button_strip = new AlbumButtonBox($buttons, $filters, $active_filter);
$this->page->adopt($button_strip);
// Fetch subalbums, but only if we're on the first page. // Fetch subalbums, but only if we're on the first page.
if ($current_page === 1) if ($page === 1)
{ {
$albums = $this->getAlbums($id_tag); $albums = $this->getAlbums($id_tag);
$index = new AlbumIndex($albums); $index = new AlbumIndex($albums);
$this->page->adopt($index); $this->page->adopt($index);
} }
// Are we viewing a person tag?
$is_person = $tag->kind === 'Person';
// Load a photo mosaic for the current tag. // Load a photo mosaic for the current tag.
list($mosaic, $total_count) = $this->getPhotoMosaic($id_tag, $id_user_uploaded, $current_page, !$is_person); list($mosaic, $total_count) = $this->getPhotoMosaic($id_tag, $page, !isset($is_person));
if (isset($mosaic)) if (isset($mosaic))
{ {
$index = new PhotosIndex($mosaic, Registry::get('user')->isAdmin()); $index = new PhotosIndex($mosaic, Registry::get('user')->isAdmin());
$this->page->adopt($index); $this->page->adopt($index);
if ($id_tag > 1)
$url_params = []; $index->setUrlSuffix('?in=' . $id_tag);
if (isset($tag))
$url_params['in'] = $tag->id_tag;
if (!empty($active_filter))
$url_params['by'] = $active_filter;
$url_suffix = http_build_query($url_params);
$index->setUrlSuffix('?' . $url_suffix);
$menu_items = $this->getEditMenuItems('&' . $url_suffix);
$index->setEditMenuItems($menu_items);
} }
// Make a page index as needed, while we're at it. // Make a page index as needed, while we're at it.
@ -120,24 +108,23 @@ class ViewPhotoAlbum extends HTMLController
$index = new PageIndex([ $index = new PageIndex([
'recordCount' => $total_count, 'recordCount' => $total_count,
'items_per_page' => self::PER_PAGE, 'items_per_page' => self::PER_PAGE,
'start' => ($current_page - 1) * self::PER_PAGE, 'start' => (isset($_GET['page']) ? $_GET['page'] - 1 : 0) * self::PER_PAGE,
'base_url' => $tag->getUrl(), 'base_url' => BASEURL . '/' . (isset($_GET['tag']) ? $_GET['tag'] . '/' : ''),
'page_slug' => 'page/%PAGE%/' . (!empty($active_filter) ? '?by=' . $active_filter : ''), 'page_slug' => 'page/%PAGE%/',
'index_class' => 'pagination-lg justify-content-around justify-content-lg-center',
]); ]);
$this->page->adopt(new PageIndexWidget($index)); $this->page->adopt(new Pagination($index));
} }
// Set the canonical url. // Set the canonical url.
$this->page->setCanonicalUrl($tag->getUrl() . ($current_page > 1 ? 'page/' . $current_page . '/' : '')); $this->page->setCanonicalUrl(BASEURL . '/' . (isset($_GET['tag']) ? $_GET['tag'] . '/' : '') .
($page > 1 ? 'page/' . $page . '/' : ''));
} }
public function getPhotoMosaic($id_tag, $id_user_uploaded, $page, $sort_linear) public function getPhotoMosaic($id_tag, $page, $sort_linear)
{ {
// Create an iterator. // Create an iterator.
list($this->iterator, $total_count) = AssetIterator::getByOptions([ list($this->iterator, $total_count) = AssetIterator::getByOptions([
'id_tag' => $id_tag, 'id_tag' => $id_tag,
'id_user_uploaded' => $id_user_uploaded,
'order' => 'date_captured', 'order' => 'date_captured',
'direction' => $sort_linear ? 'asc' : 'desc', 'direction' => $sort_linear ? 'asc' : 'desc',
'limit' => self::PER_PAGE, 'limit' => self::PER_PAGE,
@ -169,127 +156,13 @@ 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']) && isset($assets[$album['id_asset_thumb']]) 'thumbnail' => !empty($album['id_asset_thumb']) ? $assets[$album['id_asset_thumb']]->getImage() : null,
? $assets[$album['id_asset_thumb']]->getImage() : null,
]; ];
} }
return $albums; return $albums;
} }
private function getAlbumButtons(Tag $tag, $active_filter)
{
$buttons = [];
$user = Registry::get('user');
if ($user->isLoggedIn())
{
$suffix = !empty($active_filter) ? '&by=' . $active_filter : '';
$buttons[] = [
'url' => BASEURL . '/download/?tag=' . $tag->id_tag . $suffix,
'caption' => 'Download album',
];
}
if ($tag->id_parent != 0)
{
if ($tag->kind === 'Album')
{
$buttons[] = [
'url' => BASEURL . '/uploadmedia/?tag=' . $tag->id_tag,
'caption' => 'Upload photos here',
];
}
if ($user->isAdmin())
{
if ($tag->kind === 'Album')
{
$buttons[] = [
'url' => BASEURL . '/editalbum/?id=' . $tag->id_tag,
'caption' => 'Edit album',
];
}
elseif ($tag->kind === 'Person')
{
$buttons[] = [
'url' => BASEURL . '/edittag/?id=' . $tag->id_tag,
'caption' => 'Edit tag',
];
}
}
}
if ($user->isAdmin() && (!isset($tag) || $tag->kind === 'Album'))
{
$buttons[] = [
'url' => BASEURL . '/addalbum/?tag=' . $tag->id_tag,
'caption' => 'Create subalbum',
];
}
return $buttons;
}
private function getEditMenuItems($url_suffix)
{
$items = [];
$sess = '&' . Session::getSessionTokenKey() . '=' . Session::getSessionToken();
if (Registry::get('user')->isLoggedIn())
{
$items[] = [
'label' => 'Edit image',
'uri' => fn($image) => $image->getEditUrl() . $url_suffix,
];
$items[] = [
'label' => 'Delete image',
'uri' => fn($image) => $image->getDeleteUrl() . $url_suffix . $sess,
'onclick' => 'return confirm(\'Are you sure you want to delete this image?\');',
];
}
if (Registry::get('user')->isAdmin())
{
$items[] = [
'label' => 'Make album cover',
'uri' => fn($image) => $image->getEditUrl() . $url_suffix . '&album_cover' . $sess,
];
$items[] = [
'label' => 'Increase priority',
'uri' => fn($image) => $image->getEditUrl() . $url_suffix . '&inc_prio' . $sess,
];
$items[] = [
'label' => 'Decrease priority',
'uri' => fn($image) => $image->getEditUrl() . $url_suffix . '&dec_prio' . $sess,
];
}
return $items;
}
private function getHeaderBox(Tag $tag)
{
// Can we go up a level?
if ($tag->id_parent != 0)
{
$ptag = Tag::fromId($tag->id_parent);
$back_link = BASEURL . '/' . (!empty($ptag->slug) ? $ptag->slug . '/' : '');
$back_link_title = 'Back to &quot;' . $ptag->tag . '&quot;';
}
elseif ($tag->kind === 'Person')
{
$back_link = BASEURL . '/people/';
$back_link_title = 'Back to &quot;People&quot;';
}
$description = !empty($tag->description) ? $tag->description : '';
return new AlbumHeaderBox($tag->tag, $description, $back_link, $back_link_title);
}
public function __destruct() public function __destruct()
{ {
if (isset($this->iterator)) if (isset($this->iterator))

View File

@ -46,9 +46,8 @@ 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-around justify-content-lg-center',
]); ]);
$this->page->adopt(new PageIndexWidget($index)); $this->page->adopt(new Pagination($index));
} }
// Set the canonical url. // Set the canonical url.

319
import_albums.php Normal file
View File

@ -0,0 +1,319 @@
<?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();

20
import_postprocess.sh Normal file
View File

@ -0,0 +1,20 @@
#!/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

53
migrate_thumbs.php Normal file
View File

@ -0,0 +1,53 @@
<?php
/*****************************************************************************
* migrate_thumbs.php
* Migrates old-style thumbnails (meta) to new table.
*
* Kabuki CMS (C) 2013-2015, 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);
Registry::set('db', $db);
// Do some authentication checks.
Session::start();
Registry::set('user', Authentication::isLoggedIn() ? Member::fromId($_SESSION['user_id']) : new Guest());
$res = $db->query('
SELECT id_asset, variable, value
FROM assets_meta
WHERE variable LIKE {string:thumbs}',
['thumbs' => 'thumb_%']);
while ($row = $db->fetch_assoc($res))
{
if (!preg_match('~^thumb_(?<width>\d+)x(?<height>\d+)(?:_(?<mode>c[best]?))?$~', $row['variable'], $match))
continue;
echo 'Migrating ... ', $row['value'], '(#', $row['id_asset'], ")\r";
$db->insert('replace', 'assets_thumbs', [
'id_asset' => 'int',
'width' => 'int',
'height' => 'int',
'mode' => 'string-3',
'filename' => 'string-255',
], [
'id_asset' => $row['id_asset'],
'width' => $match['width'],
'height' => $match['height'],
'mode' => $match['mode'] ?? '',
'filename' => $row['value'],
]);
}
echo "\nDone\n";

View File

@ -1,61 +0,0 @@
<?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'];
}
}
}

View File

@ -8,17 +8,16 @@
class Asset class Asset
{ {
public $id_asset; protected $id_asset;
public $id_user_uploaded; protected $id_user_uploaded;
public $subdir; protected $subdir;
public $filename; protected $filename;
public $title; protected $title;
public $slug; protected $mimetype;
public $mimetype; protected $image_width;
public $image_width; protected $image_height;
public $image_height; protected $date_captured;
public $date_captured; protected $priority;
public $priority;
protected $meta; protected $meta;
protected $tags; protected $tags;
@ -27,30 +26,12 @@ class Asset
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']);
} }
public function canBeEditedBy(User $user)
{
return $this->isOwnedBy($user) || $user->isAdmin();
}
public static function cleanSlug($slug)
{
// Only alphanumerical chars, underscores and forward slashes are allowed
if (!preg_match_all('~([A-z0-9\/_]+)~', $slug, $allowedTokens, PREG_PATTERN_ORDER))
throw new UnexpectedValueException('Slug does not make sense.');
// Join valid substrings together with hyphens
return implode('-', $allowedTokens[1]);
}
public static function fromId($id_asset, $return_format = 'object') public static function fromId($id_asset, $return_format = 'object')
{ {
$row = Registry::get('db')->queryAssoc(' $row = Registry::get('db')->queryAssoc('
@ -202,10 +183,9 @@ class Asset
$new_filename = $preferred_filename; $new_filename = $preferred_filename;
$destination = ASSETSDIR . '/' . $preferred_subdir . '/' . $preferred_filename; $destination = ASSETSDIR . '/' . $preferred_subdir . '/' . $preferred_filename;
for ($i = 1; file_exists($destination); $i++) while (file_exists($destination))
{ {
$suffix = $i; $filename = pathinfo($preferred_filename, PATHINFO_FILENAME) . '_' . mt_rand(10, 99);
$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;
@ -222,14 +202,11 @@ 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 = $data['title'] ?? $basename; $title = isset($data['title']) ? $data['title'] : pathinfo($preferred_filename, PATHINFO_FILENAME);
// Same with the slug. // Same with the slug.
$slug = $data['slug'] ?? self::cleanSlug(sprintf('%s/%s', $preferred_subdir, $basename)); $slug = isset($data['slug']) ? $data['slug'] : $preferred_subdir . '/' . pathinfo($preferred_filename, PATHINFO_FILENAME);
// Detected an image? // Detected an image?
if (substr($mimetype, 0, 5) == 'image') if (substr($mimetype, 0, 5) == 'image')
@ -286,7 +263,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()
@ -304,16 +281,6 @@ class Asset
return $this->date_captured; return $this->date_captured;
} }
public function getDeleteUrl()
{
return BASEURL . '/editasset/?id=' . $this->id_asset . '&delete';
}
public function getEditUrl()
{
return BASEURL . '/editasset/?id=' . $this->id_asset;
}
public function getFilename() public function getFilename()
{ {
return $this->filename; return $this->filename;
@ -397,7 +364,7 @@ class Asset
public function isImage() public function isImage()
{ {
return isset($this->mimetype) && substr($this->mimetype, 0, 5) === 'image'; return substr($this->mimetype, 0, 5) === 'image';
} }
public function getImage() public function getImage()
@ -408,50 +375,6 @@ class Asset
return new Image(get_object_vars($this)); return new Image(get_object_vars($this));
} }
public function isOwnedBy(User $user)
{
return $this->id_user_uploaded == $user->getUserId();
}
public function moveToSubDir($destSubDir)
{
// Verify the original exists
$source = ASSETSDIR . '/' . $this->subdir . '/' . $this->filename;
if (!file_exists($source))
return -1;
// Ensure the intended target file doesn't exist yet
$destDir = ASSETSDIR . '/' . $destSubDir;
$destFile = $destDir . '/' . $this->filename;
if (file_exists($destFile))
return -2;
// Can we write to the target directory?
if (!is_writable($destDir))
return -3;
// Perform move
if (rename($source, $destFile))
{
$this->subdir = $destSubDir;
$this->slug = $this->subdir . '/' . $this->title;
Registry::get('db')->query('
UPDATE assets
SET subdir = {string:subdir},
slug = {string:slug}
WHERE id_asset = {int:id_asset}',
[
'id_asset' => $this->id_asset,
'subdir' => $this->subdir,
'slug' => $this->slug,
]);
return true;
}
return -4;
}
public function replaceFile($filename) public function replaceFile($filename)
{ {
// No filename? Abort! // No filename? Abort!
@ -471,7 +394,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();
@ -559,7 +482,9 @@ class Asset
{ {
$db = Registry::get('db'); $db = Registry::get('db');
// First: delete associated metadata if (!unlink(ASSETSDIR . '/' . $this->subdir . '/' . $this->filename))
return false;
$db->query(' $db->query('
DELETE FROM assets_meta DELETE FROM assets_meta
WHERE id_asset = {int:id_asset}', WHERE id_asset = {int:id_asset}',
@ -567,7 +492,6 @@ class Asset
'id_asset' => $this->id_asset, 'id_asset' => $this->id_asset,
]); ]);
// Second: figure out what tags to recount cardinality for
$recount_tags = $db->queryValues(' $recount_tags = $db->queryValues('
SELECT id_tag SELECT id_tag
FROM assets_tags FROM assets_tags
@ -585,30 +509,13 @@ class Asset
Tag::recount($recount_tags); Tag::recount($recount_tags);
// Third: figure out what associated thumbs to delete $return = $db->query('
$thumbs_to_delete = $db->queryValues(' DELETE FROM assets
SELECT filename
FROM assets_thumbs
WHERE id_asset = {int:id_asset}', WHERE id_asset = {int:id_asset}',
[ [
'id_asset' => $this->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(' $rows = $db->query('
SELECT id_tag SELECT id_tag
FROM tags FROM tags
@ -626,17 +533,6 @@ class Asset
} }
} }
// Finally, delete the actual asset
if (!unlink(ASSETSDIR . '/' . $this->subdir . '/' . $this->filename))
return false;
$return = $db->query('
DELETE FROM assets
WHERE id_asset = {int:id_asset}',
[
'id_asset' => $this->id_asset,
]);
return $return; return $return;
} }
@ -680,101 +576,91 @@ class Asset
FROM assets'); FROM assets');
} }
public function save() public function setKeyData($title, $slug, DateTime $date_captured = null, $priority)
{ {
if (empty($this->id_asset)) $params = [
throw new UnexpectedValueException(); 'id_asset' => $this->id_asset,
'title' => $title,
'slug' => $slug,
'priority' => $priority,
];
if (isset($date_captured))
$params['date_captured'] = $date_captured->format('Y-m-d H:i:s');
return Registry::get('db')->query(' return Registry::get('db')->query('
UPDATE assets UPDATE assets
SET id_asset = {int:id_asset}, SET title = {string:title},
id_user_uploaded = {int:id_user_uploaded}, slug = {string:slug},' . (isset($date_captured) ? '
subdir = {string:subdir}, date_captured = {datetime:date_captured},' : '') . '
filename = {string:filename},
title = {string:title},
slug = {string:slug},
mimetype = {string:mimetype},
image_width = {int:image_width},
image_height = {int:image_height},
date_captured = {datetime:date_captured},
priority = {int:priority} priority = {int:priority}
WHERE id_asset = {int:id_asset}', WHERE id_asset = {int:id_asset}',
get_object_vars($this)); $params);
} }
protected function getUrlForAdjacentInSet($prevNext, ?Tag $tag, $activeFilter) public function getUrlForPreviousInSet($id_tag = null)
{ {
$next = $prevNext === 'next';
$previous = !$next;
$where = [];
$params = [
'id_asset' => $this->id_asset,
'date_captured' => $this->date_captured,
];
// Direction depends on whether we're browsing a tag or timeline
if (isset($tag))
{
$where[] = 't.id_tag = {int:id_tag}';
$params['id_tag'] = $tag->id_tag;
$params['where_op'] = $previous ? '<' : '>';
$params['order_dir'] = $previous ? 'DESC' : 'ASC';
}
else
{
$params['where_op'] = $previous ? '>' : '<';
$params['order_dir'] = $previous ? 'ASC' : 'DESC';
}
// Take active filter into account as well
if (!empty($activeFilter) && ($user = Member::fromSlug($activeFilter)) !== false)
{
$where[] = 'id_user_uploaded = {int:id_user_uploaded}';
$params['id_user_uploaded'] = $user->getUserId();
}
// Use complete ordering when sorting the set
$where[] = '(a.date_captured, a.id_asset) {raw:where_op} ' .
'({datetime:date_captured}, {int:id_asset})';
// Stringify conditions together
$where = '(' . implode(') AND (', $where) . ')';
// Run query, leaving out tags table if not required
$row = Registry::get('db')->queryAssoc(' $row = Registry::get('db')->queryAssoc('
SELECT a.* SELECT a.*
' . (isset($id_tag) ? '
FROM assets_tags AS t
INNER JOIN assets AS a ON a.id_asset = t.id_asset
WHERE t.id_tag = {int:id_tag} AND
a.date_captured <= {datetime:date_captured} AND
a.id_asset != {int:id_asset}
ORDER BY a.date_captured DESC'
: '
FROM assets AS a FROM assets AS a
' . (isset($tag) ? ' WHERE date_captured >= {datetime:date_captured} AND
INNER JOIN assets_tags AS t ON a.id_asset = t.id_asset' : '') . ' a.id_asset != {int:id_asset}
WHERE ' . $where . ' ORDER BY date_captured ASC')
ORDER BY a.date_captured {raw:order_dir}, a.id_asset {raw:order_dir} . '
LIMIT 1', LIMIT 1',
$params); [
'id_asset' => $this->id_asset,
'id_tag' => $id_tag,
'date_captured' => $this->date_captured,
]);
if (!$row) if ($row)
{
$obj = self::byRow($row, 'object');
return $obj->getPageUrl() . ($id_tag ? '?in=' . $id_tag : '');
}
else
return false; return false;
$obj = self::byRow($row, 'object');
$urlParams = [];
if (isset($tag))
$urlParams['in'] = $tag->id_tag;
if (!empty($activeFilter))
$urlParams['by'] = $activeFilter;
$queryString = !empty($urlParams) ? '?' . http_build_query($urlParams) : '';
return $obj->getPageUrl() . $queryString;
} }
public function getUrlForPreviousInSet(?Tag $tag, $activeFilter) public function getUrlForNextInSet($id_tag = null)
{ {
return $this->getUrlForAdjacentInSet('previous', $tag, $activeFilter); $row = Registry::get('db')->queryAssoc('
} SELECT a.*
' . (isset($id_tag) ? '
FROM assets_tags AS t
INNER JOIN assets AS a ON a.id_asset = t.id_asset
WHERE t.id_tag = {int:id_tag} AND
a.date_captured >= {datetime:date_captured} AND
a.id_asset != {int:id_asset}
ORDER BY a.date_captured ASC'
: '
FROM assets AS a
WHERE date_captured <= {datetime:date_captured} AND
a.id_asset != {int:id_asset}
ORDER BY date_captured DESC')
. '
LIMIT 1',
[
'id_asset' => $this->id_asset,
'id_tag' => $id_tag,
'date_captured' => $this->date_captured,
]);
public function getUrlForNextInSet(?Tag $tag, $activeFilter) if ($row)
{ {
return $this->getUrlForAdjacentInSet('next', $tag, $activeFilter); $obj = self::byRow($row, 'object');
return $obj->getPageUrl() . ($id_tag ? '?in=' . $id_tag : '');
}
else
return false;
} }
} }

View File

@ -8,18 +8,14 @@
class AssetIterator extends Asset class AssetIterator extends Asset
{ {
private Database $db;
private $direction;
private $return_format; private $return_format;
private $res_assets; private $res_assets;
private $res_meta; private $res_meta;
private $res_thumbs; private $res_thumbs;
protected function __construct($res_assets, $res_meta, $res_thumbs, $return_format, $direction) protected function __construct($res_assets, $res_meta, $res_thumbs, $return_format)
{ {
$this->db = Registry::get('db'); $this->db = Registry::get('db');
$this->direction = $direction;
$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->res_thumbs = $res_thumbs;
@ -60,7 +56,7 @@ class AssetIterator extends Asset
// Reset internal pointer for next asset. // Reset internal pointer for next asset.
$this->db->data_seek($this->res_thumbs, 0); $this->db->data_seek($this->res_thumbs, 0);
if ($this->return_format === 'object') if ($this->return_format == 'object')
return new Asset($row); return new Asset($row);
else else
return $row; return $row;
@ -118,11 +114,6 @@ class AssetIterator extends Asset
else else
$where[] = 'a.mimetype = {string:mime_type}'; $where[] = 'a.mimetype = {string:mime_type}';
} }
if (isset($options['id_user_uploaded']))
{
$params['id_user_uploaded'] = $options['id_user_uploaded'];
$where[] = 'id_user_uploaded = {int:id_user_uploaded}';
}
if (isset($options['id_tag'])) if (isset($options['id_tag']))
{ {
$params['id_tag'] = $options['id_tag']; $params['id_tag'] = $options['id_tag'];
@ -182,7 +173,7 @@ class AssetIterator extends Asset
'_' => '_', '_' => '_',
]); ]);
$iterator = new self($res_assets, $res_meta, $res_thumbs, $return_format, $params['direction']); $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)
@ -198,14 +189,4 @@ class AssetIterator extends Asset
else else
return $iterator; return $iterator;
} }
public function isAscending()
{
return $this->direction === 'asc';
}
public function isDescending()
{
return $this->direction === 'desc';
}
} }

61
models/Cache.php Normal file
View File

@ -0,0 +1,61 @@
<?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);
}
}

View File

@ -17,7 +17,6 @@ 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.
@ -38,7 +37,7 @@ class Database
exit; exit;
} }
$this->query('SET NAMES {string:utf8mb4}', ['utf8mb4' => 'utf8mb4']); $this->query('SET NAMES {string:utf8}', ['utf8' => 'utf8']);
} }
public function getQueryCount() public function getQueryCount()
@ -520,7 +519,7 @@ class Database
$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('

View File

@ -8,12 +8,79 @@
class Dispatcher class Dispatcher
{ {
public static function route()
{
$possibleActions = [
'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();
}
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 = Router::route(); $page = self::route();
$page->showContent(); $page->showContent();
} }
// Something wasn't found? // Something wasn't found?
@ -50,7 +117,7 @@ class Dispatcher
public static function kickGuest($title = null, $message = null) public static function kickGuest($title = null, $message = null)
{ {
$form = new LogInForm('Log in'); $form = new LogInForm('Log in');
$form->adopt(new Alert($title ?? '', $message ?? 'You need to be logged in to view this page.', 'danger')); $form->adopt(new Alert($title ?? '', $message ?? 'You need to be logged in to view this page.', 'error'));
$form->setRedirectUrl($_SERVER['REQUEST_URI']); $form->setRedirectUrl($_SERVER['REQUEST_URI']);
$page = new MainTemplate('Login required'); $page = new MainTemplate('Login required');
@ -86,6 +153,7 @@ 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'));

View File

@ -12,7 +12,6 @@ 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)
{ {
@ -36,7 +35,6 @@ 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'))
@ -90,9 +88,7 @@ class EXIF
if (!empty($exif['Model'])) if (!empty($exif['Model']))
{ {
if (strpos($exif['Model'], 'PENTAX') !== false) if (!empty($exif['Make']) && strpos($exif['Model'], $exif['Make']) === false)
$meta['camera'] = trim($exif['Model']);
elseif (!empty($exif['Make']) && strpos($exif['Model'], $exif['Make']) === false)
$meta['camera'] = trim($exif['Make']) . ' ' . trim($exif['Model']); $meta['camera'] = trim($exif['Make']) . ' ' . trim($exif['Model']);
else else
$meta['camera'] = trim($exif['Model']); $meta['camera'] = trim($exif['Model']);
@ -105,9 +101,6 @@ class EXIF
elseif (!empty($exif['DateTimeDigitized'])) 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);
} }

View File

@ -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: " . var_export($_SESSION, true); $debug_info .= "\nSESSION: " . print_r($_SESSION, true);
if (!empty($_POST)) if (!empty($_POST))
$debug_info .= "\nPOST: " . var_export($_POST, true); $debug_info .= "\nPOST: " . print_r($_POST, true);
if (!empty($_GET)) if (!empty($_GET))
$debug_info .= "\nGET: " . var_export($_GET, true); $debug_info .= "\nGET: " . print_r($_GET, true);
return $debug_info; return $debug_info;
} }
@ -96,17 +96,12 @@ class ErrorHandler
$object = isset($call['class']) ? $call['class'] . $call['type'] : ''; $object = isset($call['class']) ? $call['class'] . $call['type'] : '';
$args = []; $args = [];
if (isset($call['args'])) foreach ($call['args'] as $j => $arg)
{ {
foreach ($call['args'] as $j => $arg) if (is_array($arg))
{ $args[$j] = print_r($arg, true);
// Only include the class name for objects elseif (is_object($arg))
if (is_object($arg)) $args[$j] = var_dump($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, ' ')
@ -131,7 +126,7 @@ class ErrorHandler
])) ]))
{ {
header('HTTP/1.1 503 Service Temporarily Unavailable'); header('HTTP/1.1 503 Service Temporarily Unavailable');
echo '<h2>An Error Occurred</h2><p>Our software could not connect to the database. We apologise for any inconvenience and ask you to check back later.</p>'; echo '<h2>An Error Occured</h2><p>Our software could not connect to the database. We apologise for any inconvenience and ask you to check back later.</p>';
exit; exit;
} }
@ -156,29 +151,30 @@ class ErrorHandler
elseif (!$is_sensitive) elseif (!$is_sensitive)
echo json_encode(['error' => $message]); echo json_encode(['error' => $message]);
else else
echo json_encode(['error' => 'Our apologies, an error occurred while we were processing your request. Please try again later, or contact us if the problem persists.']); echo json_encode(['error' => 'Our apologies, an error occured while we were processing your request. Please try again later, or contact us if the problem persists.']);
exit; exit;
} }
// Initialise the main template to present a nice message to the user. // Initialise the main template to present a nice message to the user.
$page = new MainTemplate('An error occurred!'); $page = new MainTemplate('An error occured!');
// Show the error. // Show the error.
$is_admin = Registry::has('user') && Registry::get('user')->isAdmin(); $is_admin = Registry::has('user') && Registry::get('user')->isAdmin();
if (DEBUG || $is_admin) if (DEBUG || $is_admin)
{ {
$page->adopt(new DummyBox('An error occurred!', '<p>' . $message . '</p><pre>' . $debug_info . '</pre>')); $page->adopt(new DummyBox('An error occured!', '<p>' . $message . '</p><pre>' . $debug_info . '</pre>'));
// Let's provide the admin navigation despite it all! // Let's provide the admin navigation despite it all!
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)
$page->adopt(new DummyBox('An error occurred!', '<p>' . $message . '</p>')); $page->adopt(new DummyBox('An error occured!', '<p>' . $message . '</p>'));
else else
$page->adopt(new DummyBox('An error occurred!', '<p>Our apologies, an error occurred while we were processing your request. Please try again later, or contact us if the problem persists.</p>')); $page->adopt(new DummyBox('An error occured!', '<p>Our apologies, an error occured while we were processing your request. Please try again later, or contact us if the problem persists.</p>'));
// If we got this far, make sure we're not showing stuff twice. // If we got this far, make sure we're not showing stuff twice.
ob_end_clean(); ob_end_clean();

View File

@ -3,7 +3,7 @@
* Form.php * Form.php
* Contains key class Form. * Contains key class Form.
* *
* Kabuki CMS (C) 2013-2023, Aaron van Geffen * Kabuki CMS (C) 2013-2015, Aaron van Geffen
*****************************************************************************/ *****************************************************************************/
class Form class Form
@ -12,11 +12,9 @@ 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)
@ -26,42 +24,9 @@ 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 getFields() public function verify($post)
{
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 = [];
@ -76,43 +41,30 @@ 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] == '') && if ((!isset($post[$field_id]) || $post[$field_id] == '') && empty($field['is_optional']))
$field['type'] !== 'captcha')
{ {
if (empty($field['is_optional'])) $this->missing[] = $field_id;
$this->missing[] = $field_id; $this->data[$field_id] = '';
if ($field['type'] === 'select' && !empty($field['multiple']))
$this->data[$field_id] = [];
else
$this->data[$field_id] = '';
continue; continue;
} }
// Should we trim this? // Verify data for all fields
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':
$this->validateSelect($field_id, $field, $post); // Skip validation? Dangerous territory!
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 2;
}
else
$this->data[$field_id] = $post[$field_id];
break; break;
case 'checkbox': case 'checkbox':
@ -121,22 +73,61 @@ class Form
break; break;
case 'color': case 'color':
$this->validateColor($field_id, $field, $post); // 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] = '';
continue 2;
}
else
$this->data[$field_id] = $post[$field_id];
break; break;
case 'file': case 'file':
// Asset needs to be processed out of POST! This is just a filename. // Needs to be verified elsewhere!
$this->data[$field_id] = isset($post[$field_id]) ? $post[$field_id] : '';
break; break;
case 'numeric': case 'numeric':
$this->validateNumeric($field_id, $field, $post); $data = isset($post[$field_id]) ? $post[$field_id] : '';
break; // Do we need to check bounds?
if (isset($field['min_value']) && is_numeric($data))
case 'captcha': {
if (isset($_POST['g-recaptcha-response']) && !$initalisation) if (is_float($field['min_value']) && (float) $data < $field['min_value'])
$this->validateCaptcha($field_id); {
elseif (!$initalisation) $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;
}
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;
@ -146,200 +137,29 @@ class Form
case 'text': case 'text':
case 'textarea': case 'textarea':
default: default:
$this->validateText($field_id, $field, $post); $this->data[$field_id] = isset($post[$field_id]) ? $post[$field_id] : '';
} }
} }
} }
private function validateCaptcha($field_id) public function setData($data)
{ {
$postdata = http_build_query([ $this->verify($data);
'secret' => RECAPTCHA_API_SECRET, $this->missing = [];
'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;
}
} }
private function validateColor($field_id, array $field, array $post) public function getFields()
{ {
// Colors are stored as a string of length 3 or 6 (hex) return $this->fields;
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];
} }
private function validateNumeric($field_id, array $field, array $post) public function getData()
{ {
$data = isset($post[$field_id]) ? $post[$field_id] : ''; return $this->data;
// 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;
} }
private function validateSelect($field_id, array $field, array $post) public function getMissing()
{ {
// Skip validation? Dangerous territory! return $this->missing;
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']);
}
} }
} }

View File

@ -3,7 +3,7 @@
* GenericTable.php * GenericTable.php
* Contains key class GenericTable. * Contains key class GenericTable.
* *
* Kabuki CMS (C) 2013-2023, Aaron van Geffen * Kabuki CMS (C) 2013-2015, Aaron van Geffen
*****************************************************************************/ *****************************************************************************/
class GenericTable class GenericTable
@ -19,13 +19,6 @@ class GenericTable
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)
{ {
@ -84,8 +77,6 @@ class GenericTable
else else
$this->body = $options['no_items_label'] ?? ''; $this->body = $options['no_items_label'] ?? '';
$this->table_class = $options['table_class'] ?? '';
// Got a title? // Got a title?
$this->title = $options['title'] ?? ''; $this->title = $options['title'] ?? '';
$this->title_class = $options['title_class'] ?? ''; $this->title_class = $options['title_class'] ?? '';
@ -107,7 +98,6 @@ class GenericTable
$header = [ $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' => $isSortable ? $this->getLink($this->start, $key, $sortDirection) : null, 'href' => $isSortable ? $this->getLink($this->start, $key, $sortDirection) : null,
'label' => $column['header'], 'label' => $column['header'],
@ -171,11 +161,6 @@ class GenericTable
return $this->pageIndex; return $this->pageIndex;
} }
public function getTableClass()
{
return $this->table_class;
}
public function getTitle() public function getTitle()
{ {
return $this->title; return $this->title;
@ -204,7 +189,6 @@ class GenericTable
// Append the cell to the row. // Append the cell to the row.
$newRow['cells'][] = [ $newRow['cells'][] = [
'class' => $column['cell_class'] ?? '',
'value' => $value, 'value' => $value,
]; ];
} }

View File

@ -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->surname = ''; $this->last_name = '';
} }
public function updateAccessTime() public function updateAccessTime()

View File

@ -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
{ {
@ -145,16 +145,6 @@ class Image extends Asset
return $ratio >= 1 && $ratio <= 2; return $ratio >= 1 && $ratio <= 2;
} }
public function getType()
{
if ($this->isPortrait())
return self::TYPE_PORTRAIT;
elseif ($this->isPanorama())
return self::TYPE_PANORAMA;
else
return self::TYPE_LANDSCAPE;
}
public function getThumbnails() public function getThumbnails()
{ {
return $this->thumbnails; return $this->thumbnails;
@ -170,7 +160,8 @@ class Image extends Asset
} }
return Registry::get('db')->query(' return Registry::get('db')->query('
DELETE FROM assets_thumbs UPDATE assets_thumbs
SET filename = NULL
WHERE id_asset = {int:id_asset}', WHERE id_asset = {int:id_asset}',
['id_asset' => $this->id_asset]); ['id_asset' => $this->id_asset]);
} }

View File

@ -1,40 +0,0 @@
<?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'];
}
}
}

View File

@ -110,9 +110,6 @@ 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
@ -123,7 +120,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}',
$params); get_object_vars($this));
} }
/** /**
@ -192,15 +189,4 @@ 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' => ' ',
]);
}
} }

View File

@ -1,17 +0,0 @@
<?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;
}
}

View File

@ -155,20 +155,24 @@ class PageIndex
public function getLink($start = null, $order = null, $dir = null) public function getLink($start = null, $order = null, $dir = null)
{ {
$page = !is_string($start) ? ($start / $this->items_per_page) + 1 : $start; $url = $this->base_url;
$url = $this->base_url . str_replace('%PAGE%', $page, $this->page_slug); $amp = strpos($this->base_url, '?') ? '&' : '?';
$urlParams = []; if (!empty($start))
if (!empty($order))
$urlParams['order'] = $order;
if (!empty($dir))
$urlParams['dir'] = $dir;
if (!empty($urlParams))
{ {
$queryString = (strpos($uri, '?') !== false ? '&' : '?'); $page = $start !== '%d' ? ($start / $this->items_per_page) + 1 : $start;
$queryString .= http_build_query($urlParams); $url .= strtr($this->page_slug, ['%PAGE%' => $page, '%AMP%' => $amp]);
$url .= $queryString; $amp = '&';
}
if (!empty($order))
{
$url .= $amp . 'order=' . $order;
$amp = '&';
}
if (!empty($dir))
{
$url .= $amp . 'dir=' . $dir;
$amp = '&';
} }
return $url; return $url;

View File

@ -1,76 +0,0 @@
<?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;
}
}

View File

@ -8,21 +8,13 @@
class PhotoMosaic class PhotoMosaic
{ {
private $descending;
private AssetIterator $iterator;
private $layouts;
private $processedImages = 0;
private $queue = []; private $queue = [];
const IMAGE_MASK_ALL = Image::TYPE_PORTRAIT | Image::TYPE_LANDSCAPE | Image::TYPE_PANORAMA;
const NUM_DAYS_CUTOFF = 7; const NUM_DAYS_CUTOFF = 7;
const NUM_BATCH_PHOTOS = 6;
public function __construct(AssetIterator $iterator) public function __construct(AssetIterator $iterator)
{ {
$this->iterator = $iterator; $this->iterator = $iterator;
$this->layouts = $this->availableLayouts();
$this->descending = $iterator->isDescending();
} }
public function __destruct() public function __destruct()
@ -30,56 +22,30 @@ class PhotoMosaic
$this->iterator->clean(); $this->iterator->clean();
} }
private function availableLayouts() public static function getRecentPhotos()
{ {
static $layouts = [ return new self(AssetIterator::getByOptions([
// Single panorama 'tag' => 'photo',
'panorama' => [Image::TYPE_PANORAMA], 'order' => 'date_captured',
'direction' => 'desc',
// A whopping six landscapes? 'limit' => 15, // worst case: 3 rows * (portrait + 4 thumbs)
'sixLandscapes' => [Image::TYPE_LANDSCAPE, Image::TYPE_LANDSCAPE, Image::TYPE_LANDSCAPE, ]));
Image::TYPE_LANDSCAPE, Image::TYPE_LANDSCAPE, Image::TYPE_LANDSCAPE],
// Big-small juxtapositions
'sidePortrait' => [Image::TYPE_PORTRAIT, Image::TYPE_LANDSCAPE, Image::TYPE_LANDSCAPE,
Image::TYPE_LANDSCAPE, Image::TYPE_LANDSCAPE],
'sideLandscape' => [Image::TYPE_LANDSCAPE, Image::TYPE_LANDSCAPE, Image::TYPE_LANDSCAPE],
// Single row of three
'threeLandscapes' => [Image::TYPE_LANDSCAPE, Image::TYPE_LANDSCAPE, Image::TYPE_LANDSCAPE],
'threePortraits' => [Image::TYPE_PORTRAIT, Image::TYPE_PORTRAIT, Image::TYPE_PORTRAIT],
// Dual layouts
'dualLandscapes' => [Image::TYPE_LANDSCAPE, Image::TYPE_LANDSCAPE],
'dualPortraits' => [Image::TYPE_PORTRAIT, Image::TYPE_PORTRAIT],
'dualMixed' => [Image::TYPE_LANDSCAPE, Image::TYPE_PORTRAIT],
// Fallback layouts
'singleLandscape' => [Image::TYPE_LANDSCAPE],
'singlePortrait' => [Image::TYPE_PORTRAIT],
];
return $layouts;
} }
private static function daysApart(DateTime $a, DateTime $b) private static function matchTypeMask(Image $image, $type_mask)
{ {
return $a->diff($b)->days; return ($type_mask & Image::TYPE_PANORAMA) && $image->isPanorama() ||
($type_mask & Image::TYPE_LANDSCAPE) && $image->isLandscape() ||
($type_mask & Image::TYPE_PORTRAIT) && $image->isPortrait();
} }
private function fetchImage($desired_type = self::IMAGE_MASK_ALL, ?DateTime $refDate = null) private function fetchImage($desired_type = Image::TYPE_PORTRAIT | Image::TYPE_LANDSCAPE | Image::TYPE_PANORAMA, Image $refDateImage = null)
{ {
// First, check if we have what we're looking for in the queue. // First, check if we have what we're looking for in the queue.
foreach ($this->queue as $i => $image) foreach ($this->queue as $i => $image)
{ {
// Give up on the queue once the dates are too far apart
if (isset($refDate) && abs(self::daysApart($image->getDateCaptured(), $refDate)) > self::NUM_DAYS_CUTOFF)
{
break;
}
// Image has to match the desired type and be taken within a week of the reference image. // Image has to match the desired type and be taken within a week of the reference image.
if (self::matchTypeMask($image, $desired_type)) if (self::matchTypeMask($image, $desired_type) && !(isset($refDateImage) && abs(self::daysApart($image, $refDateImage)) > self::NUM_DAYS_CUTOFF))
{ {
unset($this->queue[$i]); unset($this->queue[$i]);
return $image; return $image;
@ -89,174 +55,116 @@ class PhotoMosaic
// Check whatever's next up! // Check whatever's next up!
while (($asset = $this->iterator->next()) && ($image = $asset->getImage())) while (($asset = $this->iterator->next()) && ($image = $asset->getImage()))
{ {
// Give up on the recordset once dates are too far apart
if (isset($refDate) && abs(self::daysApart($image->getDateCaptured(), $refDate)) > self::NUM_DAYS_CUTOFF)
{
$this->pushToQueue($image);
break;
}
// Image has to match the desired type and be taken within a week of the reference image. // Image has to match the desired type and be taken within a week of the reference image.
if (self::matchTypeMask($image, $desired_type)) if (self::matchTypeMask($image, $desired_type) && !(isset($refDateImage) && abs(self::daysApart($image, $refDateImage)) > self::NUM_DAYS_CUTOFF))
{
return $image; return $image;
}
else else
{
$this->pushToQueue($image); $this->pushToQueue($image);
}
} }
return false; return false;
} }
public function fetchImages($num, $refDate = null, $spec = self::IMAGE_MASK_ALL) private function pushToQueue(Image $image)
{ {
$refDate = null; $this->queue[] = $image;
$prevImage = true; }
$images = [];
for ($i = 0; $i < $num || !$prevImage; $i++) private static function orderPhotos(Image $a, Image $b)
{ {
$image = $this->fetchImage($spec, $refDate); // Show images of highest priority first.
if ($image !== false) $priority_diff = $a->getPriority() - $b->getPriority();
{ if ($priority_diff !== 0)
$images[] = $image; return -$priority_diff;
$refDate = $image->getDateCaptured();
$prevImage = $image;
}
}
return $images; // In other cases, we'll just show the newest first.
return $a->getDateCaptured() > $b->getDateCaptured() ? -1 : 1;
}
private static function daysApart(Image $a, Image $b)
{
return $a->getDateCaptured()->diff($b->getDateCaptured())->days;
} }
public function getRow() public function getRow()
{ {
$requiredImages = array_map('count', $this->layouts); // Fetch the first image...
$currentImages = $this->fetchImages(self::NUM_BATCH_PHOTOS); $image = $this->fetchImage();
$selectedLayout = null;
if (empty($currentImages)) // No image at all?
{ if (!$image)
// Ensure we have no images left in the iterator before giving up
assert($this->processedImages === $this->iterator->num());
return false; return false;
// Is it a panorama? Then we've got our row!
elseif ($image->isPanorama())
return [[$image], 'panorama'];
// Alright, let's initalise a proper row, then.
$photos = [$image];
$num_portrait = $image->isPortrait() ? 1 : 0;
$num_landscape = $image->isLandscape() ? 1 : 0;
// Get an initial batch of non-panorama images to work with.
for ($i = 1; $i < 3 && ($image = $this->fetchImage(Image::TYPE_LANDSCAPE | Image::TYPE_PORTRAIT, $image)); $i++)
{
$num_portrait += $image->isPortrait() ? 1 : 0;
$num_landscape += $image->isLandscape() ? 1 : 0;
$photos[] = $image;
} }
// Assign fitness score for each layout // Sort photos by priority and date captured.
$fitnessScores = $this->getScoresByLayout($currentImages); usort($photos, 'self::orderPhotos');
$scoresByLayout = array_map(fn($el) => $el[0], $fitnessScores);
// Select the best-fitting layout // Three portraits?
$bestLayouts = array_keys($scoresByLayout, max($scoresByLayout)); if ($num_portrait === 3)
$bestLayout = $bestLayouts[0]; return [$photos, 'portraits'];
$layoutImages = $fitnessScores[$bestLayout][1];
// Push any unused back into the queue // At least one portrait?
if (count($layoutImages) < count($currentImages)) if ($num_portrait >= 1)
{ {
$diff = array_udiff($currentImages, $layoutImages, function($a, $b) { // Grab two more landscapes, so we can put a total of four tiles on the side.
return $a->getId() <=> $b->getId(); for ($i = 0; $image && $i < 2 && ($image = $this->fetchImage(Image::TYPE_LANDSCAPE | Image::TYPE_PORTRAIT, $image)); $i++)
$photos[] = $image;
// We prefer to have the portrait on the side, so prepare to process that first.
usort($photos, function($a, $b) {
if ($a->isPortrait() && !$b->isPortrait())
return -1;
elseif ($b->isPortrait() && !$a->isPortrait())
return 1;
else
return self::orderPhotos($a, $b);
}); });
array_map([$this, 'pushToQueue'], $diff);
// We might not have a full set of photos, but only bother if we have at least three.
if (count($photos) > 3)
return [$photos, 'portrait'];
} }
// Finally, allow tweaking image order through display priority // One landscape at least, hopefully?
usort($layoutImages, [$this, 'orderPhotosByPriority']); if ($num_landscape >= 1)
// Done! Return the result
$this->processedImages += count($layoutImages);
return [$layoutImages, $bestLayout];
}
public function getScoreForRow(array $images, array $specs)
{
assert(count($images) === count($specs));
$score = 0;
foreach ($images as $i => $image)
{ {
if (self::matchTypeMask($image, $specs[$i])) if (count($photos) === 3)
$score += 1; {
// We prefer to have the landscape on the side, so prepare to process that first.
usort($photos, function($a, $b) {
if ($a->isLandscape() && !$b->isLandscape())
return -1;
elseif ($b->isLandscape() && !$a->isLandscape())
return 1;
else
return self::orderPhotos($a, $b);
});
return [$photos, 'landscape'];
}
elseif (count($photos) === 2)
return [$photos, 'duo'];
else else
$score -= 10; return [$photos, 'single'];
} }
return $score; // A boring set it is, then.
} return [$photos, 'row'];
public function getScoresByLayout(array $candidateImages)
{
$fitnessScores = [];
foreach ($this->layouts as $layout => $requiredImageTypes)
{
// If we don't have enough candidate images for this layout, skip it
if (count($candidateImages) < count($requiredImageTypes))
continue;
$imageSelection = [];
$remainingImages = $candidateImages;
// Try to satisfy the layout spec using the images available
foreach ($requiredImageTypes as $spec)
{
foreach ($remainingImages as $i => $candidate)
{
// Satisfied spec from selection?
if (self::matchTypeMask($candidate, $spec))
{
$imageSelection[] = $candidate;
unset($remainingImages[$i]);
continue 2;
}
}
// Unable to satisfy spec from selection
break;
}
// Have we satisfied the spec? Great, assign a score
if (count($imageSelection) === count($requiredImageTypes))
{
$score = $this->getScoreForRow($imageSelection, $requiredImageTypes);
$fitnessScores[$layout] = [$score, $imageSelection];
// Perfect score? Bail out early
if ($score === count($requiredImageTypes))
break;
}
}
return $fitnessScores;
}
private static function matchTypeMask(Image $image, $type_mask)
{
return $image->getType() & $type_mask;
}
private static function orderPhotosByPriority(Image $a, Image $b)
{
// Leave images of different types as-is
if ($a->isLandscape() !== $b->isLandscape())
return 0;
// Otherwise, show images of highest priority first
$priority_diff = $a->getPriority() - $b->getPriority();
return -$priority_diff;
}
private function orderQueueByDate()
{
usort($this->queue, function($a, $b) {
$score = $a->getDateCaptured() <=> $b->getDateCaptured();
return $score * ($this->descending ? -1 : 1);
});
}
private function pushToQueue(Image $image)
{
$this->queue[] = $image;
$this->orderQueueByDate();
} }
} }

View File

@ -1,78 +0,0 @@
<?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();
}
}

View File

@ -3,52 +3,44 @@
* Session.php * Session.php
* Contains the key class Session. * Contains the key class Session.
* *
* Kabuki CMS (C) 2013-2023, Aaron van Geffen * Kabuki CMS (C) 2013-2015, Aaron van Geffen
*****************************************************************************/ *****************************************************************************/
class Session class Session
{ {
public static function clear()
{
$_SESSION = [];
}
public static function start() public static function start()
{ {
session_start(); session_start();
if (!isset($_SESSION['session_token_key'], $_SESSION['session_token'])) // Resuming an existing session? Check what we know!
self::generateSessionToken(); if (isset($_SESSION['user_id'], $_SESSION['ip_address'], $_SESSION['user_agent']))
{
// If we're not browsing over HTTPS, protect against session hijacking.
if (!isset($_SERVER['HTTPS']) && isset($_SERVER['REMOTE_ADDR']) && $_SESSION['ip_address'] !== $_SERVER['REMOTE_ADDR'])
{
$_SESSION = [];
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.
elseif (isset($_SERVER['HTTP_USER_AGENT']) && $_SESSION['user_agent'] !== $_SERVER['HTTP_USER_AGENT'])
{
$_SESSION = [];
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']))
$_SESSION = [
'ip_address' => isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '',
'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '',
];
return true; return true;
} }
public static function generateSessionToken()
{
$_SESSION['session_token'] = sha1(session_id() . mt_rand());
$_SESSION['session_token_key'] = substr(preg_replace('~^\d+~', '', sha1(mt_rand() . session_id() . mt_rand())), 0, rand(7, 12));
return true;
}
public static function getSessionToken()
{
if (empty($_SESSION['session_token']))
trigger_error('Call to getSessionToken without a session token being set!', E_USER_ERROR);
return $_SESSION['session_token'];
}
public static function getSessionTokenKey()
{
if (empty($_SESSION['session_token_key']))
trigger_error('Call to getSessionTokenKey without a session token key being set!', E_USER_ERROR);
return $_SESSION['session_token_key'];
}
public static function resetSessionToken() public static function resetSessionToken()
{ {
// Old interface; now always true. $_SESSION['session_token'] = sha1(session_id() . mt_rand());
$_SESSION['session_token_key'] = substr(preg_replace('~^\d+~', '', sha1(mt_rand() . session_id() . mt_rand())), 0, rand(7, 12));
return true; return true;
} }
@ -75,7 +67,23 @@ class Session
throw new UserFacingException('Invalid referring URL. Please reload the page and try again.'); throw new UserFacingException('Invalid referring URL. Please reload the page and try again.');
} }
// All looks good from here! // All looks good from here! But you can only use this token once, so...
return true; return self::resetSessionToken();
}
public static function getSessionToken()
{
if (empty($_SESSION['session_token']))
trigger_error('Call to getSessionToken without a session token being set!', E_USER_ERROR);
return $_SESSION['session_token'];
}
public static function getSessionTokenKey()
{
if (empty($_SESSION['session_token_key']))
trigger_error('Call to getSessionTokenKey without a session token key being set!', E_USER_ERROR);
return $_SESSION['session_token_key'];
} }
} }

View File

@ -11,7 +11,6 @@ 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;
@ -40,7 +39,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')
@ -59,7 +58,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')
@ -85,7 +84,7 @@ class Tag
}); });
} }
if ($return_format === 'object') if ($return_format == 'object')
{ {
$return = []; $return = [];
foreach ($rows as $row) foreach ($rows as $row)
@ -96,25 +95,6 @@ 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('
@ -130,7 +110,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)
@ -141,21 +121,6 @@ class Tag
return $rows; return $rows;
} }
public function getContributorList()
{
return Registry::get('db')->queryPairs('
SELECT u.id_user, u.first_name, u.surname, u.slug, COUNT(*) AS num_assets
FROM assets_tags AS at
LEFT JOIN assets AS a ON at.id_asset = a.id_asset
LEFT JOIN users AS u ON a.id_user_uploaded = u.id_user
WHERE at.id_tag = {int:id_tag}
GROUP BY a.id_user_uploaded
ORDER BY u.first_name, u.surname',
[
'id_tag' => $this->id_tag,
]);
}
public static function getPeople($id_parent = 0, $offset = 0, $limit = 24, $return_format = 'array') public static function getPeople($id_parent = 0, $offset = 0, $limit = 24, $return_format = 'array')
{ {
$rows = Registry::get('db')->queryAssocs(' $rows = Registry::get('db')->queryAssocs('
@ -171,7 +136,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)
@ -201,7 +166,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)
@ -231,7 +196,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)
@ -279,7 +244,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()
@ -293,8 +258,7 @@ 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},' . (isset($this->id_user_owner) ? ' id_asset_thumb = {int:id_asset_thumb},
id_user_owner = {int:id_user_owner},' : '') . '
tag = {string:tag}, tag = {string:tag},
slug = {string:slug}, slug = {string:slug},
description = {string:description}, description = {string:description},
@ -328,7 +292,8 @@ class Tag
public function resetIdAsset() public function resetIdAsset()
{ {
$db = Registry::get('db'); $db = Registry::get('db');
$new_id = $db->queryValue('
$row = $db->query('
SELECT MAX(id_asset) as new_id SELECT MAX(id_asset) as new_id
FROM assets_tags FROM assets_tags
WHERE id_tag = {int:id_tag}', WHERE id_tag = {int:id_tag}',
@ -336,12 +301,18 @@ class Tag
'id_tag' => $this->id_tag, 'id_tag' => $this->id_tag,
]); ]);
$new_id = 0;
if(!empty($row))
{
$new_id = $row->fetch_assoc()['new_id'];
}
return $db->query(' return $db->query('
UPDATE tags UPDATE tags
SET id_asset_thumb = {int:new_id} SET id_asset_thumb = {int:new_id}
WHERE id_tag = {int:id_tag}', WHERE id_tag = {int:id_tag}',
[ [
'new_id' => $new_id ?? 0, 'new_id' => $new_id,
'id_tag' => $this->id_tag, 'id_tag' => $this->id_tag,
]); ]);
} }

View File

@ -16,7 +16,6 @@ class Thumbnail
private $width; private $width;
private $height; private $height;
private $crop_mode; private $crop_mode;
private string $filename_suffix;
const CROP_MODE_NONE = 0; const CROP_MODE_NONE = 0;
const CROP_MODE_BOUNDARY = 1; const CROP_MODE_BOUNDARY = 1;

View File

@ -12,20 +12,17 @@
*/ */
abstract class User abstract class User
{ {
protected int $id_user; protected $id_user;
protected string $first_name; protected $first_name;
protected string $surname; protected $surname;
protected string $slug; protected $emailaddress;
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 bool $is_logged; protected $is_logged;
protected bool $is_guest; protected $is_guest;
/** /**
* Returns user id. * Returns user id.
@ -75,11 +72,6 @@ abstract class User
return $this->ip_address; return $this->ip_address;
} }
public function getSlug()
{
return $this->slug;
}
/** /**
* Returns whether user is logged in. * Returns whether user is logged in.
*/ */

View File

@ -1,59 +0,0 @@
<?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'];
}
}
}

View File

@ -1,3 +1,147 @@
.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
-----------------------------*/
.tiled_grid div.landscape, .tiled_grid div.portrait, .tiled_grid div.panorama {
position: relative;
}
.tiled_grid div > a.edit {
background: #fff;
border-radius: 3px;
box-shadow: 1px 1px 2px rgba(0,0,0,0.3);
display: none;
left: 20px;
line-height: 1.5;
padding: 5px 10px;
position: absolute;
top: 20px;
}
.tiled_grid div:hover > a.edit {
display: block;
}
/* Crop editor /* Crop editor
----------------*/ ----------------*/
#crop_editor { #crop_editor {
@ -12,16 +156,10 @@
z-index: 100; z-index: 100;
color: #fff; color: #fff;
} }
#crop_editor .input-group-text {
background-color: rgba(233, 236, 239, 0.5);
border-color: rgba(233, 236, 239, 0.5);
color: #fff;
}
#crop_editor input[type=number] { #crop_editor input[type=number] {
width: 50px;
background: #555; background: #555;
border-color: rgba(233, 236, 239, 0.5);
color: #fff; color: #fff;
width: 85px;
} }
#crop_editor input[type=checkbox] { #crop_editor input[type=checkbox] {
vertical-align: middle; vertical-align: middle;
@ -29,7 +167,6 @@
.crop_position { .crop_position {
background: rgba(0, 0, 0, 1.0); background: rgba(0, 0, 0, 1.0);
border: none; border: none;
display: flex;
padding: 5px; padding: 5px;
text-align: center; text-align: center;
} }
@ -58,3 +195,119 @@
top: 400px; top: 400px;
left: 300px; left: 300px;
} }
/* 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;
}

File diff suppressed because it is too large Load Diff

BIN
public/images/nothumb.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@ -1,10 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

View File

@ -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("ul.pagination > :first-child a"); var target = document.querySelector(".pagination ul > :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("ul.pagination > :last-child a"); var target = document.querySelector(".pagination ul > :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;

View File

@ -124,7 +124,7 @@ class AutoSuggest {
node.innerHTML = this.highlightMatches(query_tokens, item.label); node.innerHTML = this.highlightMatches(query_tokens, item.label);
node.jsondata = item; node.jsondata = item;
node.addEventListener('click', event => { node.addEventListener('click', event => {
this.appendCallback(node.jsondata); this.appendCallback(event.target.jsondata);
this.closeContainer(); this.closeContainer();
this.clearInput(); this.clearInput();
}); });

View File

@ -1,77 +0,0 @@
/*!
* 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') {
document.documentElement.setAttribute('data-bs-theme', (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'))
} 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)
})
})
})
})()

View File

@ -3,7 +3,7 @@ class CropEditor {
this.opt = opt; this.opt = opt;
this.edit_crop_button = document.createElement("span"); this.edit_crop_button = document.createElement("span");
this.edit_crop_button.className = "btn btn-light"; this.edit_crop_button.className = "btn";
this.edit_crop_button.textContent = "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));
@ -16,7 +16,6 @@ class CropEditor {
initDOM() { initDOM() {
this.container = document.createElement("div"); this.container = document.createElement("div");
this.container.className = 'container-fluid';
this.container.id = "crop_editor"; this.container.id = "crop_editor";
this.initPositionForm(); this.initPositionForm();
@ -28,70 +27,67 @@ class CropEditor {
initPositionForm() { initPositionForm() {
this.position = document.createElement("fieldset"); this.position = document.createElement("fieldset");
this.position.className = "crop_position flex-row justify-content-center"; this.position.className = "crop_position";
this.container.appendChild(this.position); this.container.appendChild(this.position);
const addNumericControl = (label, changeEvent) => { let source_x_label = document.createTextNode("Source X:");
const column = document.createElement('div'); this.position.appendChild(source_x_label);
column.className = 'col-auto';
this.position.appendChild(column);
const group = document.createElement('div'); this.source_x = document.createElement("input");
group.className = 'input-group'; this.source_x.type = 'number';
column.appendChild(group); this.source_x.addEventListener("change", this.positionBoundary.bind(this));
this.source_x.addEventListener("keyup", this.positionBoundary.bind(this));
this.position.appendChild(this.source_x);
const labelEl = document.createElement("span"); let source_y_label = document.createTextNode("Source Y:");
labelEl.className = 'input-group-text'; this.position.appendChild(source_y_label);
labelEl.textContent = label;
group.appendChild(labelEl);
const control = document.createElement("input"); this.source_y = document.createElement("input");
control.className = 'form-control'; this.source_y.type = 'number';
control.type = 'number'; this.source_y.addEventListener("change", this.positionBoundary.bind(this));
control.addEventListener("change", changeEvent); this.source_y.addEventListener("keyup", this.positionBoundary.bind(this));
control.addEventListener("keyup", changeEvent); this.position.appendChild(this.source_y);
group.appendChild(control);
return control; let crop_width_label = document.createTextNode("Crop width:");
}; this.position.appendChild(crop_width_label);
this.source_x = addNumericControl("Source X:", this.positionBoundary); this.crop_width = document.createElement("input");
this.source_y = addNumericControl("Source Y:", this.positionBoundary); this.crop_width.type = 'number';
this.crop_width = addNumericControl("Crop width:", this.positionBoundary); this.crop_width.addEventListener("change", this.positionBoundary.bind(this));
this.crop_height = addNumericControl("Crop height:", this.positionBoundary); this.crop_width.addEventListener("keyup", this.positionBoundary.bind(this));
this.position.appendChild(this.crop_width);
const otherColumn = document.createElement('div'); let crop_height_label = document.createTextNode("Crop height:");
otherColumn.className = 'col-auto text-nowrap'; this.position.appendChild(crop_height_label);
this.position.appendChild(otherColumn);
const constrainContainer = document.createElement("div"); this.crop_height = document.createElement("input");
constrainContainer.className = 'form-checkbox d-inline'; this.crop_height.type = 'number';
otherColumn.appendChild(constrainContainer); this.crop_height.addEventListener("change", this.positionBoundary.bind(this));
this.crop_height.addEventListener("keyup", this.positionBoundary.bind(this));
this.position.appendChild(this.crop_height);
this.crop_constrain_label = document.createElement("label");
this.position.appendChild(this.crop_constrain_label);
this.crop_constrain = document.createElement("input"); this.crop_constrain = document.createElement("input");
this.crop_constrain.checked = true; this.crop_constrain.checked = true;
this.crop_constrain.className = 'form-check-input';
this.crop_constrain.id = 'check_constrain';
this.crop_constrain.type = 'checkbox'; this.crop_constrain.type = 'checkbox';
constrainContainer.appendChild(this.crop_constrain); this.crop_constrain_label.appendChild(this.crop_constrain);
this.crop_constrain_label = document.createElement("label"); this.crop_constrain_text = document.createTextNode('Constrain proportions');
this.crop_constrain_label.className = 'form-check-label'; this.crop_constrain_label.appendChild(this.crop_constrain_text);
this.crop_constrain_label.htmlFor = 'check_constrain';
this.crop_constrain_label.textContent = 'Constrain proportions';
constrainContainer.appendChild(this.crop_constrain_label);
this.save_button = document.createElement("span"); this.save_button = document.createElement("span");
this.save_button.className = "btn btn-light"; this.save_button.className = "btn";
this.save_button.textContent = "Save"; this.save_button.textContent = "Save";
this.save_button.addEventListener('click', this.save.bind(this)); this.save_button.addEventListener('click', this.save.bind(this));
otherColumn.appendChild(this.save_button); this.position.appendChild(this.save_button);
this.abort_button = document.createElement("span"); this.abort_button = document.createElement("span");
this.abort_button.className = "btn btn-danger"; this.abort_button.className = "btn btn-red";
this.abort_button.textContent = "Abort"; this.abort_button.textContent = "Abort";
this.abort_button.addEventListener('click', this.hide.bind(this)); this.abort_button.addEventListener('click', this.hide.bind(this));
otherColumn.appendChild(this.abort_button); this.position.appendChild(this.abort_button);
} }
initImageContainer() { initImageContainer() {
@ -175,7 +171,7 @@ class CropEditor {
this.source_x.max = source.naturalWidth - 1; this.source_x.max = source.naturalWidth - 1;
this.source_y.max = source.naturalHeight - 1; this.source_y.max = source.naturalHeight - 1;
this.crop_constrain_label.textContent = `Constrain proportions (${current.crop_width} × ${current.crop_height})`; this.crop_constrain_text.textContent = `Constrain proportions (${current.crop_width} × ${current.crop_height})`;
} }
showContainer() { showContainer() {

View File

@ -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; document.location.href = target + '#photo_frame';
} }
} }
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; document.location.href = target + '#photo_frame';
} }
} }
}, false); }, false);

View File

@ -1,211 +1,207 @@
class UploadQueue { function UploadQueue(options) {
constructor(options) { this.queue = options.queue_element;
this.queue = options.queue_element; this.preview_area = options.preview_area;
this.preview_area = options.preview_area; this.upload_progress = [];
this.upload_progress = []; this.upload_url = options.upload_url;
this.upload_url = options.upload_url; this.submit = options.submit_button;
this.submit = options.submit_button; this.addEvents();
this.addEvents(); }
}
addEvents() { UploadQueue.prototype.addEvents = function() {
this.queue.addEventListener('change', event => { var that = this;
this.showSpinner(this.queue, "Generating previews (not uploading yet!)"); that.queue.addEventListener('change', function() {
this.clearPreviews(); that.showSpinner(that.queue, "Generating previews (not uploading yet!)");
for (let i = 0; i < this.queue.files.length; i++) { that.clearPreviews();
const callback = (i !== this.queue.files.length - 1) ? null : () => { for (var i = 0; i < that.queue.files.length; i++) {
this.hideSpinner(); var callback = (i !== that.queue.files.length - 1) ? null : function() {
this.submit.disabled = false; that.hideSpinner();
}; 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('canvas');
preview.title = file.name;
var preview_box = document.getElementById('upload_preview_' + index);
preview_box.appendChild(preview);
var reader = new FileReader();
var that = this;
reader.addEventListener('load', function() {
var 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.
var 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.
var 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();
}
}); });
this.submit.addEventListener('click', event => { }, false);
event.preventDefault(); reader.readAsDataURL(file);
this.process(); };
});
UploadQueue.prototype.process = function() {
this.showSpinner(this.submit, "Preparing to upload files...");
if (this.queue.files.length > 0) {
this.submit.disabled = true; this.submit.disabled = true;
this.nextFile();
} }
};
clearPreviews() { UploadQueue.prototype.nextFile = function() {
this.preview_area.innerHTML = ''; var files = this.queue.files;
this.submit.disabled = true; var i = ++this.current_upload_index;
this.current_upload_index = -1; if (i === files.length) {
} this.hideSpinner();
} else {
addPreviewBoxForQueueSlot(index) { this.setSpinnerLabel("Uploading file " + (i + 1) + " out of " + files.length);
const preview_box = document.createElement('div'); this.sendFile(files[i], i, function() {
preview_box.id = 'upload_preview_' + index;
this.preview_area.appendChild(preview_box);
}
addPreviewForFile(file, index, callback) {
if (!file) {
return false;
}
const preview = document.createElement('canvas');
preview.title = file.name;
const preview_box = document.getElementById('upload_preview_' + index);
preview_box.appendChild(preview);
const reader = new FileReader();
reader.addEventListener('load', event => {
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);
}
process() {
this.showSpinner(this.submit, "Preparing to upload files...");
if (this.queue.files.length > 0) {
this.submit.disabled = true;
this.nextFile(); this.nextFile();
}
}
nextFile() {
const files = this.queue.files;
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);
}
}
sendFile(file, index, callback) {
const request = new XMLHttpRequest();
request.addEventListener('error', event => {
this.updateProgress(index, -1);
}); });
request.addEventListener('progress', event => { }
this.updateProgress(index, event.loaded / event.total); };
});
request.addEventListener('load', event => { UploadQueue.prototype.sendFile = function(file, index, callback) {
this.updateProgress(index, 1); // Prepare the request.
if (request.responseText !== null && request.status === 200) { var that = this;
const obj = JSON.parse(request.responseText); var request = new XMLHttpRequest();
if (obj.error) { request.addEventListener('error', function(event) {
alert(obj.error); that.updateProgress(index, -1);
return; });
} request.addEventListener('progress', function(event) {
else if (callback) { that.updateProgress(index, event.loaded / event.total);
callback.call(this, obj); });
} request.addEventListener('load', function(event) {
that.updateProgress(index, 1);
if (request.responseText !== null && request.status === 200) {
var obj = JSON.parse(request.responseText);
if (obj.error) {
alert(obj.error);
return;
} }
}); else if (callback) {
callback.call(that, obj);
const data = new FormData();
data.append('uploads', file, file.name);
request.open('POST', this.upload_url, true);
request.send(data);
}
addProgressBar(index) {
if (index in this.upload_progress) {
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";
} }
} }
});
var data = new FormData();
data.append('uploads', file, file.name);
request.open('POST', this.upload_url, true);
request.send(data);
};
UploadQueue.prototype.addProgressBar = function(index) {
if (index in this.upload_progress) {
return;
} }
showSpinner(sibling, label) { var progress_container = document.createElement('div');
if (this.spinner) { progress_container.className = 'progress';
return;
}
this.spinner = document.createElement('div'); var progress = document.createElement('div');
this.spinner.className = 'spinner'; progress_container.appendChild(progress);
sibling.parentNode.appendChild(this.spinner);
if (label) { var preview_box = document.getElementById('upload_preview_' + index);
this.spinner_label = document.createElement('span'); preview_box.appendChild(progress_container);
this.spinner_label.className = 'spinner_label';
this.spinner_label.innerHTML = label; this.upload_progress[index] = progress;
sibling.parentNode.appendChild(this.spinner_label); };
}
UploadQueue.prototype.updateProgress = function(index, progress) {
if (!(index in this.upload_progress)) {
this.addProgressBar(index);
} }
setSpinnerLabel(label) { var bar = this.upload_progress[index];
if (this.spinner_label) {
this.spinner_label.innerHTML = label; if (progress >= 0) {
bar.style.width = Math.ceil(progress * 100) + '%';
} else {
bar.style.width = "";
if (progress === -1) {
bar.className = "error";
} }
} }
};
hideSpinner() { UploadQueue.prototype.showSpinner = function(sibling, label) {
if (this.spinner) { if (this.spinner) {
this.spinner.parentNode.removeChild(this.spinner); return;
this.spinner = null; }
}
if (this.spinner_label) { this.spinner = document.createElement('div');
this.spinner_label.parentNode.removeChild(this.spinner_label); this.spinner.className = 'spinner';
this.spinner_label = null; 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);
}
};
UploadQueue.prototype.setSpinnerLabel = function(label) {
if (this.spinner_label) {
this.spinner_label.innerHTML = label;
} }
} }
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;
}
};

View File

@ -1 +0,0 @@
../vendor/

38
templates/AdminBar.php Normal file
View File

@ -0,0 +1,38 @@
<?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, '/managealbums/">Albums</a></li>
<li><a href="', BASEURL, '/manageassets/">Assets</a></li>
<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];
}
}

View File

@ -6,60 +6,21 @@
* Kabuki CMS (C) 2013-2016, Aaron van Geffen * Kabuki CMS (C) 2013-2016, Aaron van Geffen
*****************************************************************************/ *****************************************************************************/
class AlbumButtonBox extends Template class AlbumButtonBox extends SubTemplate
{ {
private $active_filter; public function __construct($buttons)
private $buttons;
private $filters;
public function __construct(array $buttons, array $filters, $active_filter)
{ {
$this->active_filter = $active_filter;
$this->buttons = $buttons; $this->buttons = $buttons;
$this->filters = $filters;
} }
public function html_main() protected function html_content()
{ {
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 class="btn btn-light" href="', $button['url'], '">', $button['caption'], '</a>'; <a href="', $button['url'], '">', $button['caption'], '</a>';
if (!empty($this->filters))
{
echo '
<div class="dropdown">
<button class="btn btn-light dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-filter"></i>';
if ($this->active_filter)
{
echo '
<span class="badge text-bg-danger">',
$this->filters[$this->active_filter]['label'], '</span>';
}
echo '
</button>
<ul class="dropdown-menu">';
foreach ($this->filters as $key => $filter)
{
$is_active = $key === $this->active_filter;
echo '
<li><a class="dropdown-item', $is_active ? ' active' : '',
'" href="', $filter['link'], '">',
$filter['caption'],
'</a></li>';
}
echo '
</ul>
</div>';
}
echo ' echo '
</div>'; </div>';

View File

@ -6,13 +6,8 @@
* Kabuki CMS (C) 2013-2016, Aaron van Geffen * Kabuki CMS (C) 2013-2016, Aaron van Geffen
*****************************************************************************/ *****************************************************************************/
class AlbumHeaderBox extends Template class AlbumHeaderBox extends SubTemplate
{ {
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;
@ -21,13 +16,11 @@ class AlbumHeaderBox extends Template
$this->back_link_title = $back_link_title; $this->back_link_title = $back_link_title;
} }
public function html_main() protected function html_content()
{ {
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 class="back_button" href="', $this->back_link, '" title="', $this->back_link_title, '">&larr;</a>
<i class="bi bi-arrow-left"></i>
</a>
<div> <div>
<h2>', $this->title, '</h2>'; <h2>', $this->title, '</h2>';

View File

@ -6,7 +6,7 @@
* Kabuki CMS (C) 2013-2015, Aaron van Geffen * Kabuki CMS (C) 2013-2015, Aaron van Geffen
*****************************************************************************/ *****************************************************************************/
class AlbumIndex extends Template class AlbumIndex extends SubTemplate
{ {
protected $albums; protected $albums;
protected $show_edit_buttons; protected $show_edit_buttons;
@ -15,7 +15,6 @@ class AlbumIndex extends Template
const TILE_WIDTH = 400; const TILE_WIDTH = 400;
const TILE_HEIGHT = 300; 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)
{ {
@ -24,64 +23,49 @@ class AlbumIndex extends Template
$this->show_labels = $show_labels; $this->show_labels = $show_labels;
} }
public function html_main() protected function html_content()
{ {
echo ' echo '
<div class="container album-index"> <div class="tiled_grid">';
<div class="row g-5">';
foreach ($this->albums as $album) foreach (array_chunk($this->albums, 3) as $photos)
$this->renderAlbum($album);
echo '
</div>
</div>';
}
private function renderAlbum(array $album)
{
echo '
<div class="col-md-6 col-xl-4">
<div class="polaroid landscape" style="aspect-ratio: 1.12">';
if ($this->show_edit_buttons)
echo '
<a class="edit" href="#">Edit</a>';
echo '
<a href="', $album['link'], '">';
if (isset($album['thumbnail']))
{ {
$thumbs = []; echo '
foreach ([1, 2] as $factor) <div class="tiled_row">';
$thumbs[$factor] = $album['thumbnail']->getThumbnailUrl(
static::TILE_WIDTH * $factor, static::TILE_HEIGHT * $factor, true, true);
foreach (['normal-photo', 'blur-photo'] as $className) foreach ($photos as $album)
{ {
echo ' echo '
<img alt="" src="', $thumbs[1], '"' . (isset($thumbs[2]) ? <div class="landscape">';
' srcset="' . $thumbs[2] . ' 2x"' : '') .
' class="', $className, '"' .
' alt="" style="aspect-ratio: ', self::TILE_RATIO, '">';
}
}
else
{
echo '
<img alt="" src="', BASEURL, '/images/nothumb.svg"',
' class="placeholder-image"',
' style="aspect-ratio: ', self::TILE_RATIO, '; object-fit: unset">';
}
if ($this->show_labels) if ($this->show_edit_buttons)
echo ' 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>'; <h4>', $album['caption'], '</h4>';
echo ' echo '
</a> </a>
</div> </div>';
}
echo '
</div>'; </div>';
}
echo '
</div>';
} }
} }

View File

@ -6,30 +6,26 @@
* Kabuki CMS (C) 2013-2015, Aaron van Geffen * Kabuki CMS (C) 2013-2015, Aaron van Geffen
*****************************************************************************/ *****************************************************************************/
class Alert extends Template class Alert extends SubTemplate
{ {
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, ['success', 'info', 'warning', 'danger']) ? $type : 'info'; $this->_type = in_array($type, ['alert', 'error', 'success', 'info']) ? $type : 'alert';
} }
public function html_main() protected function html_content()
{ {
echo ' echo '
<div class="alert', $this->_type !== 'alert' ? ' alert-' . $this->_type : '', '">' <div class="alert', $this->_type != 'alert' ? ' alert-' . $this->_type : '', '">', (!empty($this->_title) ? '
, !empty($this->_title) ? '<strong>' . $this->_title . '</strong><br>' : '', ' <strong>' . $this->_title . '</strong><br>' : ''), '<p>', $this->_message, '</p>';
', $this->_message,
$this->additional_alert_content(), ' $this->additional_alert_content();
</div>';
echo '</div>';
} }
protected function additional_alert_content() protected function additional_alert_content()
{ {}
}
} }

View File

@ -1,36 +0,0 @@
<?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
View File

@ -0,0 +1,27 @@
<?php
/*****************************************************************************
* Button.php
* Defines the Button template.
*
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
*****************************************************************************/
class Button extends SubTemplate
{
private $content = '';
private $href = '';
private $class = '';
public function __construct($content = '', $href = '', $class = '')
{
$this->content = $content;
$this->href = $href;
$this->class = $class;
}
protected function html_content()
{
echo '
<a class="', $this->class, '" href="', $this->href, '">', $this->content, '</a>';
}
}

View 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);
}
protected function html_content()
{
$this->confirm();
$this->photo();
}
private function confirm()
{
$buttons = [];
$buttons[] = new Button("Delete", BASEURL . '/' . $this->photo->getSlug() . '?delete_confirmed', "btn btn-red");
$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_content();
}
}

View File

@ -8,26 +8,24 @@
class DummyBox extends SubTemplate class DummyBox extends SubTemplate
{ {
protected $_content; public function __construct($title = '', $content = '', $class = '')
public function __construct($title = '', $content = '', $class = null)
{ {
parent::__construct($title); $this->_title = $title;
$this->_content = $content; $this->_content = $content;
$this->_class = $class;
if (isset($class))
$this->_class .= $class;
} }
protected function html_content() protected function html_content()
{ {
if ($this->_title) echo '
echo ' <div class="boxed_content', $this->_class ? ' ' . $this->_class : '', '">', $this->_title ? '
<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>';
} }
} }

View File

@ -6,49 +6,40 @@
* Kabuki CMS (C) 2013-2015, Aaron van Geffen * Kabuki CMS (C) 2013-2015, Aaron van Geffen
*****************************************************************************/ *****************************************************************************/
class EditAssetForm extends Template class EditAssetForm extends SubTemplate
{ {
private $allAlbums;
private $asset; private $asset;
private $currentAlbumId;
private $thumbs; private $thumbs;
public function __construct(array $options) public function __construct(Asset $asset, array $thumbs = [])
{ {
$this->allAlbums = $options['allAlbums']; $this->asset = $asset;
$this->asset = $options['asset']; $this->thumbs = $thumbs;
$this->currentAlbumId = $options['currentAlbumId'];
$this->thumbs = $options['thumbs'];
} }
public function html_main() protected function html_content()
{ {
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="content-box"> <div class="boxed_content" style="margin-bottom: 2%">
<div class="float-end"> <div style="float: right">
<a class="btn btn-danger" href="', $this->asset->getDeleteUrl(), '&', <a class="btn btn-red" href="', BASEURL, '/', $this->asset->getSlug(), '?delete_confirmed">Delete asset</a>
Session::getSessionTokenKey(), '=', Session::getSessionToken(), <input type="submit" value="Save asset data">
'" onclick="return confirm(\'Are you sure you want to delete this asset?\');">',
'Delete asset</a>
<a class="btn btn-light" href="', $this->asset->getPageUrl(), '#photo_frame">View asset</a>
<button class="btn btn-primary" type="submit">Save asset data</button>
</div> </div>
<h2 class="mb-0">Edit asset \'', $this->asset->getTitle(), '\'</h2> <h2>Edit asset \'', $this->asset->getTitle(), '\' (', $this->asset->getFilename(), ')</h2>
</div>'; </div>';
$this->section_replace(); $this->section_replace();
echo ' echo '
<div class="row"> <div style="float: left; width: 60%; margin-right: 2%">';
<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 class="col-md-4">'; <div style="float: left; width: 38%;">';
if (!empty($this->thumbs)) if (!empty($this->thumbs))
$this->section_thumbnails(); $this->section_thumbnails();
@ -56,12 +47,11 @@ class EditAssetForm extends Template
$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>';
} }
@ -69,74 +59,41 @@ class EditAssetForm extends Template
{ {
$date_captured = $this->asset->getDateCaptured(); $date_captured = $this->asset->getDateCaptured();
echo ' echo '
<div class="content-box key_info"> <div class="widget 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(), '">
<div class="row mb-2"> <dt>URL slug</dt>
<label class="col-form-label col-sm-3">Album:</label> <dd><input type="text" name="slug" maxlength="255" size="70" value="', $this->asset->getSlug(), '">
<div class="col-sm">
<select class="form-select" name="id_album">';
foreach ($this->allAlbums as $id_album => $album) <dt>Date captured</dt>
echo ' <dd><input type="text" name="date_captured" size="30" value="',
<option value="', $id_album, '"', $date_captured ? $date_captured->format('Y-m-d H:i:s') : '', '" placeholder="Y-m-d H:i:s">
$this->currentAlbumId == $id_album ? ' selected' : '',
'>', htmlspecialchars($album), '</option>';
echo ' <dt>Display priority</dt>
</select> <dd><input type="number" name="priority" min="0" max="100" step="1" value="', $this->asset->getPriority(), '">
</div> </dl>
</div>
<div class="row mb-2">
<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" type="datetime-local" step="1"
name="date_captured" size="30" placeholder="Y-m-d H:i:s" value="',
$date_captured ? $date_captured->format('Y-m-d H:i:s') : '', '">
</div>
</div>
<div class="row mb-2">
<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="content-box linked_tags"> <div class="widget linked_tags" style="margin-top: 2%">
<h3>Linked tags</h3> <h3>Linked tags</h3>
<ul class="list-unstyled" id="tag_list">'; <ul id="tag_list">';
foreach ($this->asset->getTags() as $tag) foreach ($this->asset->getTags() as $tag)
{ echo '
if ($tag->kind === 'Album')
continue;
echo '
<li> <li>
<input class="tag_check" type="checkbox" name="tag[', $tag->id_tag, ']" id="linked_tag_', $tag->id_tag, '" title="Uncheck to delete" checked> <input class="tag_check" type="checkbox" name="tag[', $tag->id_tag, ']" id="linked_tag_', $tag->id_tag, '" title="Uncheck to delete" checked>
', $tag->tag, ' ', $tag->tag, '
</li>'; </li>';
}
echo ' echo '
<li id="new_tag_container"><input class="form-control" type="text" id="new_tag" placeholder="Type to link a new tag"></li> <li id="new_tag_container"><input 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>
@ -177,9 +134,9 @@ class EditAssetForm extends Template
protected function section_thumbnails() protected function section_thumbnails()
{ {
echo ' echo '
<div class="content-box linked_thumbs"> <div class="widget linked_thumbs">
<h3>Thumbnails</h3> <h3>Thumbnails</h3>
View: <select class="form-select w-auto d-inline" id="thumbnail_src">'; View: <select id="thumbnail_src">';
$first = INF; $first = INF;
foreach ($this->thumbs as $i => $thumb) foreach ($this->thumbs as $i => $thumb)
@ -261,39 +218,38 @@ class EditAssetForm extends Template
protected function section_asset_meta() protected function section_asset_meta()
{ {
echo ' echo '
<div class="content-box asset_meta mt-2"> <div class="widget asset_meta" style="margin-top: 2%">
<h3>Asset meta data</h3>'; <h3>Asset meta data</h3>
<ul>';
$i = 0; $i = -1;
foreach ($this->asset->getMeta() as $key => $meta) foreach ($this->asset->getMeta() as $key => $meta)
{ {
echo '
<div class="input-group">
<input type="text" class="form-control" name="meta_key[', $i, ']" value="', htmlspecialchars($key), '" placeholder="key">
<input type="text" class="form-control" name="meta_value[', $i, ']" value="', htmlspecialchars($meta), '" placeholder="value">
</div>';
$i++; $i++;
echo '
<li>
<input type="text" name="meta_key[', $i, ']" value="', htmlentities($key), '">
<input type="text" name="meta_value[', $i, ']" value="', htmlentities($meta), '">
</li>';
} }
echo ' echo '
<div class="input-group"> <li>
<input type="text" class="form-control" name="meta_key[', $i + 1, ']" value="" placeholder="key"> <input type="text" name="meta_key[', $i + 1, ']" value="">
<input type="text" class="form-control" name="meta_value[', $i + 1, ']" value="" placeholder="value"> <input type="text" name="meta_value[', $i + 1, ']" value="">
</div> </li>
<div class="text-end mt-3"> </ul>
<button class="btn btn-primary" type="submit">Save metadata</button> <p><input type="submit" value="Save metadata"></p>
</div>
</div>'; </div>';
} }
protected function section_replace() protected function section_replace()
{ {
echo ' echo '
<div class="content-box replace_asset mt-2"> <div class="widget replace_asset" style="margin-bottom: 2%; display: block">
<h3>Replace asset</h3> <h3>Replace asset</h3>
File: <input class="form-control d-inline w-auto" type="file" name="replacement"> File: <input type="file" name="replacement">
Target: <select class="form-select d-inline w-auto" name="replacement_target"> Target: <select 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)
@ -329,7 +285,7 @@ class EditAssetForm extends Template
echo ' echo '
</select> </select>
<button class="btn btn-primary" type="submit">Save asset</button> <input type="submit" value="Save asset">
</div>'; </div>';
} }
} }

View File

@ -1,46 +0,0 @@
<?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>';
}
}

View File

@ -11,25 +11,19 @@ class ForgotPasswordForm extends SubTemplate
protected function html_content() protected function html_content()
{ {
echo ' echo '
<h1>Password reset procedure</h1>'; <div class="boxed_content">
<h2>Password reset procedure</h2>';
foreach ($this->_subtemplates as $template) foreach ($this->_subtemplates as $template)
$template->html_main(); $template->html_main();
echo ' echo '
<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> <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>
<form action="', BASEURL, '/resetpassword/?step=1" method="post"> <form class="form-horizontal" action="', BASEURL, '/resetpassword/?step=1" method="post">
<div class="row"> <label class="control-label" for="field_emailaddress">E-mail address:</label><br>
<label class="col-sm-2 col-form-label" for="field_emailaddress">E-mail address:</label> <input type="text" id="field_emailaddress" name="emailaddress">
<div class="col-sm-4"> <button type="submit" class="btn btn-primary">Send mail</button>
<input type="text" class="form-control" id="field_emailaddress" name="emailaddress"> </form>
</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>';
} }
} }

View File

@ -3,41 +3,43 @@
* FormView.php * FormView.php
* Contains the form template. * Contains the form template.
* *
* Kabuki CMS (C) 2013-2023, Aaron van Geffen * Kabuki CMS (C) 2013-2015, 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 '
<h1>', $this->title, '</h1>'; <div class="admin_box">
<h2>', htmlspecialchars($this->title), '</h2>';
foreach ($this->_subtemplates as $template) foreach ($this->_subtemplates as $template)
$template->html_main(); $template->html_main();
echo ' echo '
<form action="', $this->form->request_url, '" method="', $this->form->request_method, '" enctype="multipart/form-data">'; <form action="', $this->request_url, '" method="', $this->request_method, '" enctype="multipart/form-data">';
if (isset($this->form->content_above)) if (isset($this->content_above))
echo $this->form->content_above; echo $this->content_above;
$this->missing = $this->form->getMissing(); echo '
$this->data = $this->form->getData(); <dl>';
foreach ($this->form->getFields() as $field_id => $field) foreach ($this->fields 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))
@ -51,230 +53,107 @@ 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 class="form-group"> <div style="clear: both">
<div class="offset-sm-2 col-sm-10"> <button type="submit" class="btn btn-primary">Save information</button>';
<button type="submit" name="submit" class="btn btn-primary">', $this->form->getSubmitButtonCaption(), '</button>';
if (isset($this->form->content_below)) if (isset($this->content_below))
echo ' echo '
', $this->form->content_below; ', $this->content_below;
echo ' echo '
</div>
</div> </div>
</form>'; </form>';
if (!empty($this->title))
echo '
</div>';
} }
protected function renderField($field_id, array $field) protected function renderField($field_id, $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 '
', $field['before_html']; <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>';
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 '
<div class="row mb-2">'; <dd class="cont_', $field_id, isset($field['dd_class']) ? ' ' . $field['dd_class'] : '', isset($field['tab_class']) ? ' target target-' . $field['tab_class'] : '', '">';
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':
$this->renderSelect($field_id, $field); echo '
<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':
$this->renderRadio($field_id, $field); foreach ($field['options'] as $value => $option)
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':
$this->renderCheckbox($field_id, $field); echo '
<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':
$this->renderTextArea($field_id, $field); echo '
<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':
$this->renderColor($field_id, $field); echo '
<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':
$this->renderNumeric($field_id, $field); echo '
<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':
$this->renderFile($field_id, $field); if (!empty($this->data[$field_id]))
break; echo '<img src="', $this->data[$field_id], '" alt=""><br>';
case 'captcha': echo '
$this->renderCaptcha($field_id, $field); <input type="file" name="', $field_id, '" id="', $field_id, '"', !empty($field['disabled']) ? ' disabled' : '', '>';
break; break;
case 'text': case 'text':
case 'password': case 'password':
default: default:
$this->renderText($field_id, $field); echo '
<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 '
</div>'; </dd>';
}
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>';
} }
} }

View File

@ -11,57 +11,45 @@ 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 = htmlspecialchars($addr); $this->emailaddress = htmlentities($addr);
} }
protected function html_content() protected function html_content()
{ {
if (!empty($this->_title)) echo '
echo ' <form action="', BASEURL, '/login/" method="post" id="login">
<h1 class="mb-4">Press #RU to continue</h1>'; <h3>Log in</h3>';
if (!empty($this->_subtemplates)) foreach ($this->_subtemplates as $template)
{ $template->html_main();
foreach ($this->_subtemplates as $template)
$template->html_main();
}
echo ' echo '
<form class="mt-4" action="', BASEURL, '/login/" method="post"> <dl>
<div class="row"> <dt><label for="field_emailaddress">E-mail address:</label></dt>
<label class="col-sm-3 col-form-label" for="field_emailaddress">E-mail address:</label> <dd><input type="text" id="field_emailaddress" name="emailaddress" tabindex="1" value="', $this->emailaddress, '" autofocus></dd>
<div class="col-sm">
<input type="text" class="form-control" id="field_emailaddress" name="emailaddress" value="', $this->emailaddress, '"> <dt><label for="field_password">Password:</label></dt>
</div> <dd><input type="password" id="field_password" name="password" tabindex="2"></dd>
</div> </dl>';
<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 '
<div class="mt-4"> <a href="', BASEURL, '/resetpassword/">Forgotten your password?</a>
<div class="offset-sm-3 col-sm-9"> <div class="buttonstrip">
<button type="submit" class="btn btn-primary">Sign in</button> <button type="submit" class="btn btn-primary" id="field_login" name="login" tabindex="3">Log in</button>
<a class="btn btn-light" href="', BASEURL, '/resetpassword/" style="margin-left: 1em">Forgotten your password?</a> </div>
</div> </form>';
</div>
</form>';
} }
} }

View File

@ -1,97 +0,0 @@
<?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) : '';
$className = $rnd > 5 ? 'space-invader' . $alt : 'nyan-cat';
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="', $className, '"></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>';
}
}

View File

@ -25,31 +25,25 @@ class MainTemplate extends Template
echo '<!DOCTYPE html> echo '<!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<title>', $this->title, '</title>'; <title>', $this->title, '</title>', !empty($this->canonical_url) ? '
<link rel="canonical" href="' . $this->canonical_url . '">' : '', '
if (!empty($this->canonical_url)) <link type="text/css" rel="stylesheet" href="', BASEURL, '/css/default.css">
echo ' <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="canonical" href="', $this->canonical_url, '">'; <meta http-equiv="Content-Type" content="text/html; charset=utf-8">', !empty($this->css) ? '
<style type="text/css">' . $this->css . '
echo ' </style>' : '', $this->header_html, '
<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?v2">
<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, '/">
$bar = new MainNavBar(); <h1 id="logo">#pics</h1>
$bar->html_main(); </a>
<ul id="nav">
echo ' <li><a href="', BASEURL, '/">albums</a></li>
<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">';
@ -61,8 +55,12 @@ class MainTemplate extends Template
if (Registry::has('user') && Registry::get('user')->isAdmin()) if (Registry::has('user') && Registry::get('user')->isAdmin())
{ {
if (Registry::has('start')) if (class_exists('Cache'))
echo ' echo '
<span class="cache-info">Cache info: ', Cache::$hits, ' hits, ', Cache::$misses, ' misses, ', Cache::$puts, ' puts, ', Cache::$removals, ' removals</span>';
if (Registry::has('start'))
echo '<br>
<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'))
@ -71,7 +69,7 @@ class MainTemplate extends Template
} }
else else
echo ' echo '
<span class="vanity">Powered by <a href="https://aaronweb.net/projects/kabuki/" target="_blank">Kabuki CMS</a></span>'; <span class="vanity">Powered by <a href="https://aaronweb.net/projects/kabuki/">Kabuki CMS</a></span>';
echo ' echo '
</footer> </footer>
@ -82,11 +80,15 @@ 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;

View File

@ -8,8 +8,6 @@
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;
@ -18,12 +16,14 @@ class MediaUploader extends SubTemplate
protected function html_content() protected function html_content()
{ {
echo ' echo '
<form action="', BASEURL, '/uploadmedia/?tag=', $this->tag->id_tag, '" method="post" enctype="multipart/form-data"> <form action="', BASEURL, '/uploadmedia/?tag=', $this->tag->id_tag, '" class="boxed_content" method="post" enctype="multipart/form-data">
<h2>Upload new photos to &quot;', $this->tag->tag, '&quot;</h2> <h2>Upload new photos to &quot;', $this->tag->tag, '&quot;</h2>
<div class="input-group"> <div>
<input class="form-control d-inline" type="file" id="upload_queue" name="uploads[]" <h3>Select files</h3>
accept="image/jpeg" multiple> <input type="file" id="upload_queue" name="uploads[]" multiple>
<button class="btn btn-primary" name="save" id="photo_submit" type="submit">Upload the lot</button> </div>
<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>

View File

@ -1,32 +0,0 @@
<?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>';
}
}

View File

@ -1,61 +0,0 @@
<?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>';
}
}
}

View File

@ -1,82 +0,0 @@
<?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;
private static $unique_index_count = 0;
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'] . '"' : '', '>',
'&laquo; previous</a></li>';
$num_wildcards = 0;
foreach ($page_index as $key => $page)
{
if (!is_numeric($key))
continue;
if (!is_array($page))
{
$first_wildcard = $num_wildcards === 0;
$num_wildcards++;
echo '
<li class="page-item page-padding wildcard',
$first_wildcard ? ' first-wildcard' : '',
'" onclick="javascript:promptGoToPage(',
self::$unique_index_count, ')"><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 &raquo;</a></li>
</ul>';
if ($num_wildcards)
{
echo '
<script type="text/javascript">
var page_index_', self::$unique_index_count++, ' = {
wildcard_url: "', $index->getLink("%d"), '",
num_pages: ', $index->getNumberOfPages(), ',
per_page: ', $index->getItemsPerPage(), '
};
</script>';
}
}
}

63
templates/Pagination.php Normal file
View File

@ -0,0 +1,63 @@
<?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', '>&laquo; 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 &raquo;</', !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>';
}
}
}

View File

@ -20,31 +20,27 @@ class PasswordResetForm extends SubTemplate
protected function html_content() protected function html_content()
{ {
echo ' echo '
<h1 class="mb-4">Password reset procedure</h1>'; <div class="boxed_content">
<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 action="', BASEURL, '/resetpassword/?step=2&amp;email=', rawurlencode($this->email), '&amp;key=', $this->key, '" method="post"> <form class="form-horizontal" action="', BASEURL, '/resetpassword/?step=2&amp;email=', rawurlencode($this->email), '&amp;key=', $this->key, '" method="post">
<div class="row mt-3"> <p>
<label class="col-sm-2 col-form-label" for="field_password1">New password:</label> <label class="control-label" for="field_password1">New password:</label>
<div class="col-sm-3"> <input type="password" id="field_password1" name="password1">
<input type="password" class="form-control" id="field_password1" name="password1"> </p>
</div>
</div> <p>
<div class="row mt-3"> <label class="control-label" for="field_password2">Repeat new password:</label>
<label class="col-sm-2 col-form-label" for="field_password2">Repeat new password:</label> <input type="password" id="field_password2" name="password2">
<div class="col-sm-3"> </p>
<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>
</div> </form>
</div> </div>';
</form>';
} }
} }

View File

@ -6,231 +6,220 @@
* Kabuki CMS (C) 2013-2016, Aaron van Geffen * Kabuki CMS (C) 2013-2016, Aaron van Geffen
*****************************************************************************/ *****************************************************************************/
class PhotoPage extends Template class PhotoPage extends SubTemplate
{ {
private $activeFilter; protected $photo;
private $photo; private $exif;
private $metaData; private $previous_photo_url = '';
private $tag; private $next_photo_url = '';
private $is_asset_owner = false;
public function __construct(Image $photo) public function __construct(Image $photo)
{ {
$this->photo = $photo; $this->photo = $photo;
} }
public function html_main() public function setPreviousPhotoUrl($url)
{
$this->previous_photo_url = $url;
}
public function setNextPhotoUrl($url)
{
$this->next_photo_url = $url;
}
public function setIsAssetOwner($flag)
{
$this->is_asset_owner = $flag;
}
protected function html_content()
{ {
$this->photoNav(); $this->photoNav();
$this->photo(); $this->photo();
echo ' echo '
<div class="row mt-5"> <div id="sub_photo">
<div class="col-lg">'; <h2 class="entry-title">', $this->photo->getTitle(), '</h2>';
$this->taggedPeople();
$this->linkNewTags();
echo '
</div>';
$this->photoMeta(); $this->photoMeta();
echo ' if($this->is_asset_owner)
</div> $this->addUserActions();
</div>
<div class="row mt-5">
<div class="col-lg">
<div id="sub_photo" class="content-box">';
$this->userActions();
echo ' echo '
<h2 class="entry-title">', $this->photo->getTitle(), '</h2>';
$this->printTags('Album', 'Album', false);
$this->printTags('Tagged People', 'Person', true);
echo '
</div>
</div>
</div>
<script type="text/javascript" src="', BASEURL, '/js/photonav.js"></script>'; <script type="text/javascript" src="', BASEURL, '/js/photonav.js"></script>';
} }
protected function photo() protected function photo()
{ {
echo ' echo '
<a href="', $this->photo->getUrl(), '"> <div id="photo_frame">
<div id="photo_frame">'; <a href="', $this->photo->getUrl(), '">';
if ($this->photo->isPortrait()) if ($this->photo->isPortrait())
{ echo $this->photo->getInlineImage(null, 960);
echo '
<figure id="photo-figure" class="portrait-figure">',
$this->photo->getInlineImage(null, 960, 'normal-photo'),
$this->photo->getInlineImage(null, 960, 'blur-photo'), '
</figure>';
}
else else
{ echo $this->photo->getInlineImage(1280, null);
$className = $this->photo->isPanorama() ? 'panorama-figure' : 'landscape-figure';
echo '
<figure id="photo-figure" class="', $className, '">',
$this->photo->getInlineImage(1280, null, 'normal-photo'),
$this->photo->getInlineImage(1280, null, 'blur-photo'), '
</figure>';
}
echo ' echo '
</figure> </a>
</div> </div>';
</a>';
}
public function setActiveFilter($filter)
{
$this->activeFilter = $filter;
}
public function setTag(Tag $tag)
{
$this->tag = $tag;
} }
private function photoNav() private function photoNav()
{ {
if ($previousUrl = $this->photo->getUrlForPreviousInSet($this->tag, $this->activeFilter)) if ($this->previous_photo_url)
echo ' echo '
<a href="', $previousUrl, '#photo_frame" id="previous_photo"><i class="bi bi-arrow-left"></i></a>'; <a href="', $this->previous_photo_url, '" id="previous_photo"><em>Previous photo</em></a>';
else else
echo ' echo '
<span id="previous_photo"><i class="bi bi-arrow-left"></i></span>'; <span id="previous_photo"><em>Previous photo</em></span>';
if ($nextUrl = $this->photo->getUrlForNextInSet($this->tag, $this->activeFilter)) if ($this->next_photo_url)
echo ' echo '
<a href="', $nextUrl, '#photo_frame" id="next_photo"><i class="bi bi-arrow-right"></i></a>'; <a href="', $this->next_photo_url, '" id="next_photo"><em>Next photo</em></a>';
else else
echo ' echo '
<span id="next_photo"><i class="bi bi-arrow-right"></i></span>'; <span id="next_photo"><em>Next photo</em></span>';
} }
private function photoMeta() private function photoMeta()
{ {
echo ' echo '
<ul class="list-group list-group-horizontal photo_meta">'; <div id="photo_exif_box">
<h3>EXIF</h3>
<dl class="photo_meta">';
foreach ($this->metaData as $header => $body) if (!empty($this->exif->created_timestamp))
{
echo ' echo '
<li class="list-group-item flex-fill"> <dt>Date Taken</dt>
<h4>', $header, '</h4> <dd>', date("j M Y, H:i:s", $this->exif->created_timestamp), '</dd>';
', $body, '
</li>';
}
echo ' echo '
</ul>'; <dt>Uploaded by</dt>
<dd>', $this->photo->getAuthor()->getfullName(), '</dd>';
if (!empty($this->exif->camera))
echo '
<dt>Camera Model</dt>
<dd>', $this->exif->camera, '</dd>';
if (!empty($this->exif->shutter_speed))
echo '
<dt>Shutter Speed</dt>
<dd>', $this->exif->shutterSpeedFraction(), '</dd>';
if (!empty($this->exif->aperture))
echo '
<dt>Aperture</dt>
<dd>f/', number_format($this->exif->aperture, 1), '</dd>';
if (!empty($this->exif->focal_length))
echo '
<dt>Focal Length</dt>
<dd>', $this->exif->focal_length, ' mm</dd>';
if (!empty($this->exif->iso))
echo '
<dt>ISO Speed</dt>
<dd>', $this->exif->iso, '</dd>';
echo '
</dl>
</div>';
} }
private function printTags($header, $tagKind, $allowLinkingNewTags) private function taggedPeople()
{ {
static $nextTagListId = 1;
$tagListId = 'tagList' . ($nextTagListId++);
echo ' echo '
<h3>', $header, '</h3> <h3>Tags</h3>
<ul id="', $tagListId, '" class="tag-list">'; <ul id="tag_list">';
foreach ($this->photo->getTags() as $tag) foreach ($this->photo->getTags() as $tag)
{ {
if ($tag->kind !== $tagKind)
continue;
echo ' echo '
<li id="tag-', $tag->id_tag, '"> <li id="tag-', $tag->id_tag, '">
<div class="input-group"> <a rel="tag" title="View all posts tagged ', $tag->tag, '" href="', $tag->getUrl(), '" class="entry-tag">', $tag->tag, '</a>';
<a class="input-group-text" href="', $tag->getUrl(), '" title="View all posts tagged ', $tag->tag, '">
', $tag->tag, '
</a>';
if ($tag->kind === 'Person') if ($tag->kind === 'Person')
{
echo ' echo '
<a class="delete-tag btn btn-danger px-1" title="Unlink this tag from this photo" href="#" data-id="', $tag->id_tag, '"> <a class="delete-tag" title="Unlink this tag from this photo" href="#" data-id="', $tag->id_tag, '"></a>';
<i class="bi bi-x"></i>
</a>';
}
echo ' echo '
</div>
</li>';
}
static $nextNewTagId = 1;
$newTagId = 'newTag' . ($nextNewTagId++);
if ($allowLinkingNewTags)
{
echo '
<li style="position: relative">
<input class="form-control w-auto" type="text" id="', $newTagId, '" placeholder="Type to link a new tag">
</li>'; </li>';
} }
echo ' echo '
</ul>'; </ul>';
$this->printNewTagScript($tagKind, $tagListId, $newTagId);
} }
private function printNewTagScript($tagKind, $tagListId, $newTagId) private function linkNewTags()
{ {
echo ' echo '
<div>
<h3>Link tags</h3>
<p style="position: relative"><input type="text" id="new_tag" placeholder="Type to link a new tag"></p>
</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() {
const removeTag = function(event) { var removeTag = function(event) {
event.preventDefault(); event.preventDefault();
const request = new HttpRequest("post", "', $this->photo->getPageUrl(), '", var that = this;
"id_tag=" + this.dataset["id"] + "&delete", (response) => { var request = new HttpRequest("post", "', $this->photo->getPageUrl(), '",
"id_tag=" + this.dataset["id"] + "&delete", function(response) {
if (!response.success) { if (!response.success) {
return; return;
} }
const tagNode = document.getElementById("tag-" + this.dataset["id"]); var tagNode = document.getElementById("tag-" + that.dataset["id"]);
tagNode.parentNode.removeChild(tagNode); tagNode.parentNode.removeChild(tagNode);
}); });
}; };
let tagRemovalTargets = document.querySelectorAll(".delete-tag"); var tagRemovalTargets = document.getElementsByClassName("delete-tag");
tagRemovalTargets.forEach(el => el.addEventListener("click", removeTag)); for (var i = 0; i < tagRemovalTargets.length; i++) {
tagRemovalTargets[i].addEventListener("click", removeTag);
}
let tag_autosuggest = new TagAutoSuggest({ var tag_autosuggest = new TagAutoSuggest({
inputElement: "', $newTagId, '", inputElement: "new_tag",
listElement: "', $tagListId, '", listElement: "tag_list",
baseUrl: "', BASEURL, '", baseUrl: "', BASEURL, '",
appendCallback: (item) => { appendCallback: function(item) {
const request = new HttpRequest("post", "', $this->photo->getPageUrl(), '", var request = new HttpRequest("post", "', $this->photo->getPageUrl(), '",
"id_tag=" + item.id_tag, (response) => { "id_tag=" + item.id_tag, function(response) {
const newListItem = document.createElement("li"); var newLink = document.createElement("a");
newListItem.id = "tag-" + item.id_tag;
const newInputGroup = document.createElement("div");
newInputGroup.className = "input-group";
newListItem.appendChild(newInputGroup);
const newLink = document.createElement("a");
newLink.className = "input-group-text";
newLink.href = item.url; newLink.href = item.url;
newLink.title = "View all posts tagged " + item.label;
newLink.textContent = item.label;
newInputGroup.appendChild(newLink);
const removeLink = document.createElement("a"); var newLabel = document.createTextNode(item.label);
removeLink.className = "delete-tag btn btn-danger px-1"; newLink.appendChild(newLabel);
var removeLink = document.createElement("a");
removeLink.className = "delete-tag";
removeLink.dataset["id"] = item.id_tag; removeLink.dataset["id"] = item.id_tag;
removeLink.href = "#"; removeLink.href = "#";
removeLink.innerHTML = \'<i class="bi bi-x"></i>\';
removeLink.addEventListener("click", removeTag); removeLink.addEventListener("click", removeTag);
newInputGroup.appendChild(removeLink);
const list = document.getElementById("', $tagListId, '"); var crossmark = document.createTextNode("");
list.insertBefore(newListItem, list.querySelector("li:last-child")); 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");
list.appendChild(newNode);
}, this); }, this);
} }
}); });
@ -238,24 +227,17 @@ class PhotoPage extends Template
</script>'; </script>';
} }
public function setMetaData(array $metaData) public function setExif(EXIF $exif)
{ {
$this->metaData = $metaData; $this->exif = $exif;
} }
public function userActions() public function addUserActions()
{ {
if (!$this->photo->isOwnedBy(Registry::get('user')))
return;
echo ' echo '
<div class="float-end"> <div id=user_actions_box>
<a class="btn btn-primary" href="', $this->photo->getEditUrl(), '"> <h3>Actions</h3>
<i class="bi bi-pencil"></i> Edit</a> <a class="btn btn-red" href="', BASEURL, '/', $this->photo->getSlug(), '?confirm_delete">Delete</a>
<a class="btn btn-danger" href="', $this->photo->getDeleteUrl(), '&',
Session::getSessionTokenKey(), '=', Session::getSessionToken(),
'" onclick="return confirm(\'Are you sure you want to delete this photo?\');"',
'"><i class="bi bi-pencil"></i> Delete</a></a>
</div>'; </div>';
} }
} }

View File

@ -6,34 +6,32 @@
* Kabuki CMS (C) 2013-2015, Aaron van Geffen * Kabuki CMS (C) 2013-2015, Aaron van Geffen
*****************************************************************************/ *****************************************************************************/
class PhotosIndex extends Template class PhotosIndex extends SubTemplate
{ {
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 $previous_header = ''; protected $previous_header = '';
protected $url_suffix;
protected $edit_menu_items = []; const PANORAMA_WIDTH = 1280;
protected $photo_url_suffix;
const PANORAMA_WIDTH = 1256;
const PANORAMA_HEIGHT = null; const PANORAMA_HEIGHT = null;
const PORTRAIT_WIDTH = 387; const PORTRAIT_WIDTH = 400;
const PORTRAIT_HEIGHT = 628; const PORTRAIT_HEIGHT = 645;
const LANDSCAPE_WIDTH = 822; const LANDSCAPE_WIDTH = 850;
const LANDSCAPE_HEIGHT = 628; const LANDSCAPE_HEIGHT = 640;
const DUO_WIDTH = 604; const DUO_WIDTH = 618;
const DUO_HEIGHT = 403; const DUO_HEIGHT = 412;
const SINGLE_WIDTH = 618; const SINGLE_WIDTH = 618;
const SINGLE_HEIGHT = 412; const SINGLE_HEIGHT = 412;
const TILE_WIDTH = 387; const TILE_WIDTH = 400;
const TILE_HEIGHT = 290; const TILE_HEIGHT = 300;
public function __construct(PhotoMosaic $mosaic, $show_edit_buttons = false, $show_labels = false, $show_headers = true) public function __construct(PhotoMosaic $mosaic, $show_edit_buttons = false, $show_labels = false, $show_headers = true)
{ {
@ -43,17 +41,16 @@ class PhotosIndex extends Template
$this->show_labels = $show_labels; $this->show_labels = $show_labels;
} }
public function html_main() protected function html_content()
{ {
echo ' echo '
<div class="container photo-index">'; <div class="tiled_grid">';
$i = 0; for ($i = $this->row_limit; $i > 0 && $row = $this->mosaic->getRow(); $i--)
while ($row = $this->mosaic->getRow())
{ {
[$photos, $what] = $row; list($photos, $what) = $row;
$this->header($photos); $this->header($photos);
$this->$what($photos, ($i++) % 2); $this->$what($photos);
} }
echo ' echo '
@ -76,65 +73,31 @@ class PhotosIndex extends Template
$name = str_replace(' ', '', strtolower($header)); $name = str_replace(' ', '', strtolower($header));
echo ' echo '
<h4 class="tiled-header" id="', $name, '"> <h4 class="tiled_header" id="', $name, '">
<a href="#', $name, '">', $header, '</a> <a href="#', $name, '">', $header, '</a>
</h4>'; </h4>';
$this->previous_header = $header; $this->previous_header = $header;
} }
protected function editMenu(Image $image)
{
if (empty($this->edit_menu_items))
return;
echo '
<div class="edit dropdown">
<button class="btn btn-primary btn-sm dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
</button>
<ul class="dropdown-menu">';
foreach ($this->edit_menu_items as $item)
{
echo '
<li><a class="dropdown-item" href="', $item['uri']($image), '"',
isset($item['onclick']) ? ' onclick="' . $item['onclick'] . '"' : '',
'>', $item['label'], '</a></li>';
}
echo '
</ul>
</div>';
}
protected function photo(Image $image, $className, $width, $height, $crop = true, $fit = true) protected function photo(Image $image, $className, $width, $height, $crop = true, $fit = true)
{ {
// Prefer thumbnail aspect ratio if available, otherwise use image aspect ratio.
$aspectRatio = isset($width, $height) ? $width / $height : $image->ratio();
echo ' echo '
<div class="polaroid ', $className, '" style="aspect-ratio: ', $aspectRatio, '">'; <div class="', $className, '">';
if ($this->show_edit_buttons && $image->canBeEditedBy(Registry::get('user'))) if ($this->show_edit_buttons)
$this->editMenu($image);
echo '
<a href="', $image->getPageUrl(), $this->photo_url_suffix, '#photo_frame">';
foreach (['normal-photo', 'blur-photo'] as $className)
{
echo ' echo '
<a class="edit" href="', BASEURL, '/editasset/?id=', $image->getId(), '">Edit</a>';
echo '
<a href="', $image->getPageUrl(), $this->url_suffix, '#photo_frame">
<img src="', $image->getThumbnailUrl($width, $height, $crop, $fit), '"'; <img src="', $image->getThumbnailUrl($width, $height, $crop, $fit), '"';
// Can we offer double-density thumbs? // Can we offer double-density thumbs?
if ($image->width() >= $width * 2 && $image->height() >= $height * 2) if ($image->width() >= $width * 2 && $image->height() >= $height * 2)
echo ' srcset="', $image->getThumbnailUrl($width * 2, $height * 2, $crop, $fit), ' 2x"'; echo ' srcset="', $image->getThumbnailUrl($width * 2, $height * 2, $crop, $fit), ' 2x"';
else
echo ' srcset="', $image->getThumbnailUrl($image->width(), $image->height(), true), ' 2x"';
echo ' alt="" title="', $image->getTitle(), '" class="', $className, '" style="aspect-ratio: ', $aspectRatio, '">'; echo ' alt="" title="', $image->getTitle(), '">';
}
if ($this->show_labels) if ($this->show_labels)
echo ' echo '
@ -146,211 +109,106 @@ class PhotosIndex extends Template
</div>'; </div>';
} }
protected function panorama(array $photos, $altLayout) protected function panorama(array $photos)
{ {
foreach ($photos as $image) foreach ($photos as $image)
{
echo '
<div class="row mb-5 tile-panorama">
<div class="col">';
$this->photo($image, 'panorama', static::PANORAMA_WIDTH, static::PANORAMA_HEIGHT, false, false); $this->photo($image, 'panorama', static::PANORAMA_WIDTH, static::PANORAMA_HEIGHT, false, false);
echo '
</div>
</div>';
}
} }
protected function sixLandscapes(array $photos, $altLayout) protected function portrait(array $photos)
{
$chunks = array_chunk($photos, 3);
$this->sideLandscape($chunks[0], $altLayout);
$this->threeLandscapes($chunks[1], $altLayout);
}
protected function sidePortrait(array $photos, $altLayout)
{ {
$image = array_shift($photos); $image = array_shift($photos);
echo ' echo '
<div class="row g-5 mb-5 tile-feat-portrait', <div class="tiled_row">
$altLayout ? ' flex-row-reverse' : '', '"> <div class="column_portrait">';
<div class="col-md-4">';
$this->photo($image, 'portrait', static::PORTRAIT_WIDTH, static::PORTRAIT_HEIGHT, 'centre'); $this->photo($image, 'portrait', static::PORTRAIT_WIDTH, static::PORTRAIT_HEIGHT, 'centre');
echo ' echo '
</div> </div>
<div class="col-md-8"> <div class="column_tiles_four">';
<div class="row g-5">';
foreach ($photos as $image) foreach ($photos as $image)
{ $this->photo($image, 'landscape', static::TILE_WIDTH, static::TILE_HEIGHT, 'centre');
echo '
<div class="col-md-6">';
$this->photo($image, 'landscape', static::TILE_WIDTH, static::TILE_HEIGHT, 'top');
echo '
</div>';
}
echo ' echo '
</div>
</div> </div>
</div>'; </div>';
} }
protected function sideLandscape(array $photos, $altLayout) protected function landscape(array $photos)
{ {
$image = array_shift($photos); $image = array_shift($photos);
echo ' echo '
<div class="row g-5 mb-5 tile-feat-landscape', <div class="tiled_row">
$altLayout ? ' flex-row-reverse' : '', '"> <div class="column_landscape">';
<div class="col-md-8">';
$this->photo($image, 'landscape', static::LANDSCAPE_WIDTH, static::LANDSCAPE_HEIGHT, 'top'); $this->photo($image, 'landscape', static::LANDSCAPE_WIDTH, static::LANDSCAPE_HEIGHT, 'top');
echo ' echo '
</div> </div>
<div class="col-md-4"> <div class="column_tiles_two">';
<div class="row g-5">';
foreach ($photos as $image) foreach ($photos as $image)
{
echo '
<div>';
$this->photo($image, 'landscape', static::TILE_WIDTH, static::TILE_HEIGHT, 'top'); $this->photo($image, 'landscape', static::TILE_WIDTH, static::TILE_HEIGHT, 'top');
echo '
</div>';
}
echo ' echo '
</div>
</div> </div>
</div>'; </div>';
} }
protected function threeLandscapes(array $photos, $altLayout) protected function duo(array $photos)
{ {
echo ' echo '
<div class="row g-5 mb-5 tile-row-landscapes">'; <div class="tiled_row">';
foreach ($photos as $image) foreach ($photos as $image)
{
echo '
<div class="col-md-4">';
$this->photo($image, 'landscape', static::TILE_WIDTH, static::TILE_HEIGHT, true);
echo '
</div>';
}
echo '
</div>';
}
protected function threePortraits(array $photos, $altLayout)
{
echo '
<div class="row g-5 mb-5 tile-row-portraits">';
foreach ($photos as $image)
{
echo '
<div class="col-md-4">';
$this->photo($image, 'portrait', static::PORTRAIT_WIDTH, static::PORTRAIT_HEIGHT, true);
echo '
</div>';
}
echo '
</div>';
}
protected function dualLandscapes(array $photos, $altLayout)
{
echo '
<div class="row g-5 mb-5 tile-duo">';
foreach ($photos as $image)
{
echo '
<div class="col-md-6">';
$this->photo($image, 'duo', static::DUO_WIDTH, static::DUO_HEIGHT, true); $this->photo($image, 'duo', static::DUO_WIDTH, static::DUO_HEIGHT, true);
echo '
</div>';
}
echo ' echo '
</div>'; </div>';
} }
protected function dualMixed(array $photos, $altLayout) protected function single(array $photos)
{ {
echo ' echo '
<div class="row g-5 mb-5 tile-feat-landscape', <div class="tiled_row">';
$altLayout ? ' flex-row-reverse' : '', '">
<div class="col-md-8">';
$image = array_shift($photos);
$this->photo($image, 'landscape', static::LANDSCAPE_WIDTH, static::LANDSCAPE_HEIGHT, 'top');
echo '
</div>
<div class="col-md-4">';
$image = array_shift($photos);
$this->photo($image, 'portrait', static::PORTRAIT_WIDTH, static::PORTRAIT_HEIGHT, true);
echo '
</div>
</div>
</div>';
}
protected function dualPortraits(array $photos, $altLayout)
{
// Recycle the row layout so portraits don't appear too large
$this->threePortraits($photos, $altLayout);
}
protected function singleLandscape(array $photos, $altLayout)
{
echo '
<div class="row g-5 mb-5 tile-single">
<div class="col-md-6">';
$image = array_shift($photos); $image = array_shift($photos);
$this->photo($image, 'single', static::SINGLE_WIDTH, static::SINGLE_HEIGHT, 'top'); $this->photo($image, 'single', static::SINGLE_WIDTH, static::SINGLE_HEIGHT, 'top');
echo ' echo '
</div>
</div>'; </div>';
} }
protected function singlePortrait(array $photos, $altLayout) protected function row(array $photos)
{ {
// Recycle the row layout so portraits don't appear too large echo '
$this->threePortraits($photos, $altLayout); <div class="tiled_row">';
foreach ($photos as $image)
$this->photo($image, 'landscape', static::TILE_WIDTH, static::TILE_HEIGHT, true);
echo '
</div>';
} }
public function setEditMenuItems(array $items) protected function portraits(array $photos)
{ {
$this->edit_menu_items = $items; echo '
<div class="tiled_row">';
foreach ($photos as $image)
$this->photo($image, 'portrait', static::PORTRAIT_WIDTH, static::PORTRAIT_HEIGHT, true);
echo '
</div>';
} }
public function setUrlSuffix($suffix) public function setUrlSuffix($suffix)
{ {
$this->photo_url_suffix = $suffix; $this->url_suffix = $suffix;
} }
} }

View File

@ -8,32 +8,10 @@
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 ' echo $this->html_content();
<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;
}
} }

View File

@ -3,72 +3,53 @@
* TabularData.php * TabularData.php
* Contains the template that displays tabular data. * Contains the template that displays tabular data.
* *
* Kabuki CMS (C) 2013-2023, Aaron van Geffen * Kabuki CMS (C) 2013-2015, Aaron van Geffen
*****************************************************************************/ *****************************************************************************/
class TabularData extends SubTemplate class TabularData extends SubTemplate
{ {
private GenericTable $_t;
public function __construct(GenericTable $table) public function __construct(GenericTable $table)
{ {
$this->_t = $table; $this->_t = $table;
$pageIndex = $table->getPageIndex();
if ($pageIndex)
$this->pager = new Pagination($pageIndex);
} }
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 '
<div class="generic-table', !empty($titleclass) ? ' ' . $titleclass : '', '"> <h2>', $title, '</h2>';
<h1>', htmlspecialchars($title), '</h1>';
}
foreach ($this->_subtemplates as $template) // Showing a page index?
$template->html_main(); if (isset($this->pager))
$this->pager->html_content();
// Showing an inline form? // Maybe even a small form?
$pager = $this->_t->getPageIndex(); if (isset($this->_t->form_above))
if (!empty($pager) || isset($this->_t->form_above)) $this->showForm($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-condensed"> <table class="table table-striped">
<thead> <thead>
<tr>'; <tr>';
// Show all headers in their full glory! // Show the table's headers.
$header = $this->_t->getHeader(); foreach ($this->_t->getHeader() as $th)
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 ' <i class="bi bi-caret-' . ($th['sort_mode'] === 'down' ? 'down' : 'up') . '-fill"></i>'; echo ' ', $th['sort_mode'] == 'up' ? '&uarr;' : '&darr;';
echo '</th>'; echo '</th>';
} }
@ -78,7 +59,7 @@ class TabularData extends SubTemplate
</thead> </thead>
<tbody>'; <tbody>';
// The body is what we came to see! // Show the table's body.
$body = $this->_t->getBody(); $body = $this->_t->getBody();
if (is_array($body)) if (is_array($body))
{ {
@ -88,147 +69,51 @@ class TabularData extends SubTemplate
<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', (!empty($td['width']) ? ' width="' . $td['width'] . '"' : ''), '>', $td['value'], '</td>';
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($header), '" class="fullwidth">', $body, '</td> <td colspan="', count($this->_t->getHeader()), '">', $body, '</td>
</tr>'; </tr>';
echo ' echo '
</tbody> </tbody>
</table>'; </table>';
if ($tableClass) // Maybe another small form?
echo ' if (isset($this->_t->form_below))
</div>'; $this->showForm($this->_t->form_below);
// Showing an inline form? // Showing a page index?
if (!empty($pager) || isset($this->_t->form_below)) if (isset($this->pager))
{ $this->pager->html_content();
echo '
<div class="row clearfix justify-content-end">';
// Page index? echo '
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)
{ {
if (!isset($form['is_embed'])) echo '
echo ' <form action="', $form['action'], '" method="', $form['method'], '" class="table_form ', $form['class'], '">';
<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 '
if ($field['type'] === 'select') <input name="', $name, '" type="', $field['type'], '" placeholder="', $field['placeholder'], '"', isset($field['class']) ? ' class="' . $field['class'] . '"' : '', isset($field['value']) ? ' value="' . $field['value'] . '"' : '', '>';
{
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 '
<button class="btn ', isset($button['class']) ? $button['class'] : 'btn-primary', '" type="', $button['type'], '" name="', $name, '"'; <input name="', $name, '" type="', $button['type'], '" value="', $button['caption'], '" class="btn', isset($button['class']) ? ' ' . $button['class'] . '' : '', '">';
if (isset($button['onclick'])) echo '
echo ' onclick="', $button['onclick'], '"'; </form>';
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>';
} }
} }

View File

@ -15,7 +15,7 @@ abstract class Template
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

View 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_content();
}
}