Compare commits
	
		
			No commits in common. "master" and "exif-date-time-original" have entirely different histories.
		
	
	
		
			master
			...
			exif-date-
		
	
		
@ -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"
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -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);
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -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"');
 | 
				
			||||||
 | 
				
			|||||||
@ -6,14 +6,8 @@
 | 
				
			|||||||
 * Kabuki CMS (C) 2013-2017, Aaron van Geffen
 | 
					 * Kabuki CMS (C) 2013-2017, Aaron van Geffen
 | 
				
			||||||
 *****************************************************************************/
 | 
					 *****************************************************************************/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// TODO: extend EditTag?
 | 
					 | 
				
			||||||
class EditAlbum extends HTMLController
 | 
					class EditAlbum extends HTMLController
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
	private $form;
 | 
					 | 
				
			||||||
	private $formview;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	const THUMBS_PER_PAGE = 20;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	public function __construct()
 | 
						public function __construct()
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		// Ensure it's just admins at this point.
 | 
							// Ensure it's just admins at this point.
 | 
				
			||||||
@ -24,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')
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
@ -38,19 +29,21 @@ 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/');
 | 
				
			||||||
				exit;
 | 
									exit;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			else
 | 
								else
 | 
				
			||||||
				throw new Exception('Cannot delete album: an error occured while processing the request.');
 | 
									trigger_error('Cannot delete album: an error occured while processing the request.', E_USER_ERROR);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		// Editing one, then, surely.
 | 
							// Editing one, then, surely.
 | 
				
			||||||
		else
 | 
							else
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
 | 
								$album = Tag::fromId($id_tag);
 | 
				
			||||||
			if ($album->kind !== 'Album')
 | 
								if ($album->kind !== 'Album')
 | 
				
			||||||
				throw new Exception('Cannot edit album: not an album.');
 | 
									trigger_error('Cannot edit album: not an album.', E_USER_ERROR);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			parent::__construct('Edit album \'' . $album->tag . '\'');
 | 
								parent::__construct('Edit album \'' . $album->tag . '\'');
 | 
				
			||||||
			$form_title = 'Edit album \'' . $album->tag . '\'';
 | 
								$form_title = 'Edit album \'' . $album->tag . '\'';
 | 
				
			||||||
@ -68,21 +61,18 @@ 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-'];
 | 
								'request_url' => BASEURL . '/editalbum/?' . ($id_tag ? 'id=' . $id_tag : 'add'),
 | 
				
			||||||
		foreach (Tag::getOffset(0, 9999, 'tag', 'up', true) as $parent)
 | 
								'content_below' => $after_form,
 | 
				
			||||||
		{
 | 
								'fields' => [
 | 
				
			||||||
			if (!empty($id_tag) && $parent['id_tag'] == $id_tag)
 | 
					 | 
				
			||||||
				continue;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			$parentChoices[$parent['id_tag']] = $parent['tag'];
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		$fields = [
 | 
					 | 
				
			||||||
				'id_parent' => [
 | 
									'id_parent' => [
 | 
				
			||||||
				'type' => 'select',
 | 
										'type' => 'numeric',
 | 
				
			||||||
				'label' => 'Parent album',
 | 
										'label' => 'Parent album ID',
 | 
				
			||||||
				'options' => $parentChoices,
 | 
									],
 | 
				
			||||||
 | 
									'id_asset_thumb' => [
 | 
				
			||||||
 | 
										'type' => 'numeric',
 | 
				
			||||||
 | 
										'label' => 'Thumbnail asset ID',
 | 
				
			||||||
 | 
										'is_optional' => true,
 | 
				
			||||||
				],
 | 
									],
 | 
				
			||||||
				'tag' => [
 | 
									'tag' => [
 | 
				
			||||||
					'type' => 'text',
 | 
										'type' => 'text',
 | 
				
			||||||
@ -103,15 +93,9 @@ class EditAlbum extends HTMLController
 | 
				
			|||||||
					'maxlength' => 255,
 | 
										'maxlength' => 255,
 | 
				
			||||||
					'is_optional' => true,
 | 
										'is_optional' => true,
 | 
				
			||||||
				],
 | 
									],
 | 
				
			||||||
		];
 | 
								],
 | 
				
			||||||
 | 
					 | 
				
			||||||
		$this->form = new Form([
 | 
					 | 
				
			||||||
			'request_url' => BASEURL . '/editalbum/?' . ($id_tag ? 'id=' . $id_tag : 'add'),
 | 
					 | 
				
			||||||
			'buttons_extra' => $after_form,
 | 
					 | 
				
			||||||
			'fields' => $fields,
 | 
					 | 
				
			||||||
		]);
 | 
							]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// 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']);
 | 
				
			||||||
@ -124,96 +108,28 @@ class EditAlbum extends HTMLController
 | 
				
			|||||||
				];
 | 
									];
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		elseif (empty($_POST) && isset($album))
 | 
					
 | 
				
			||||||
		{
 | 
							if (!isset($formDefaults))
 | 
				
			||||||
			$formDefaults = get_object_vars($album);
 | 
								$formDefaults = isset($album) ? get_object_vars($album) : $_POST;
 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		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)];
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		else
 | 
					 | 
				
			||||||
			$formDefaults = $_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 (!empty($id_tag))
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			$current_page = isset($_GET['page']) ? (int) $_GET['page'] : 1;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			list($assets, $num_assets) = AssetIterator::getByOptions([
 | 
					 | 
				
			||||||
				'direction' => 'desc',
 | 
					 | 
				
			||||||
				'limit' => self::THUMBS_PER_PAGE,
 | 
					 | 
				
			||||||
				'page' => $current_page,
 | 
					 | 
				
			||||||
				'id_tag' => $id_tag,
 | 
					 | 
				
			||||||
			], true);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			// If we have asset images, show the thumbnail manager
 | 
					 | 
				
			||||||
			if ($num_assets > 0)
 | 
					 | 
				
			||||||
			{
 | 
					 | 
				
			||||||
				$manager = new FeaturedThumbnailManager($assets, $id_tag ? $album->id_asset_thumb : 0);
 | 
					 | 
				
			||||||
				$this->page->adopt($manager);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				// Make a page index as needed, while we're at it.
 | 
					 | 
				
			||||||
				if ($num_assets > self::THUMBS_PER_PAGE)
 | 
					 | 
				
			||||||
				{
 | 
					 | 
				
			||||||
					$index = new PageIndex([
 | 
					 | 
				
			||||||
						'recordCount' => $num_assets,
 | 
					 | 
				
			||||||
						'items_per_page' => self::THUMBS_PER_PAGE,
 | 
					 | 
				
			||||||
						'start' => ($current_page - 1) * self::THUMBS_PER_PAGE,
 | 
					 | 
				
			||||||
						'base_url' => BASEURL . '/editalbum/?id=' . $id_tag,
 | 
					 | 
				
			||||||
						'page_slug' => '&page=%PAGE%',
 | 
					 | 
				
			||||||
					]);
 | 
					 | 
				
			||||||
					$manager->adopt(new PageIndexWidget($index));
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		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.
 | 
				
			||||||
@ -224,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']))
 | 
				
			||||||
				{
 | 
									{
 | 
				
			||||||
 | 
				
			|||||||
@ -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 (Tag::getOffset(0, 9999, 'tag', 'up', true) 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)
 | 
				
			||||||
 | 
				
			|||||||
@ -8,22 +8,16 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
class EditTag extends HTMLController
 | 
					class EditTag extends HTMLController
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
	const THUMBS_PER_PAGE = 20;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	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']))
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
@ -35,19 +29,21 @@ 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/');
 | 
				
			||||||
				exit;
 | 
									exit;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			else
 | 
								else
 | 
				
			||||||
				throw new Exception('Cannot delete tag: an error occured while processing the request.');
 | 
									trigger_error('Cannot delete tag: an error occured while processing the request.', E_USER_ERROR);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		// Editing one, then, surely.
 | 
							// Editing one, then, surely.
 | 
				
			||||||
		else
 | 
							else
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
 | 
								$tag = Tag::fromId($id_tag);
 | 
				
			||||||
			if ($tag->kind === 'Album')
 | 
								if ($tag->kind === 'Album')
 | 
				
			||||||
				throw new Exception('Cannot edit tag: is actually an album.');
 | 
									trigger_error('Cannot edit tag: is actually an album.', E_USER_ERROR);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			parent::__construct('Edit tag \'' . $tag->tag . '\'');
 | 
								parent::__construct('Edit tag \'' . $tag->tag . '\'');
 | 
				
			||||||
			$form_title = 'Edit tag \'' . $tag->tag . '\'';
 | 
								$form_title = 'Edit tag \'' . $tag->tag . '\'';
 | 
				
			||||||
@ -65,7 +61,19 @@ 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 = [
 | 
							$form = new Form([
 | 
				
			||||||
 | 
								'request_url' => BASEURL . '/edittag/?' . ($id_tag ? 'id=' . $id_tag : 'add'),
 | 
				
			||||||
 | 
								'content_below' => $after_form,
 | 
				
			||||||
 | 
								'fields' => [
 | 
				
			||||||
 | 
									'id_parent' => [
 | 
				
			||||||
 | 
										'type' => 'numeric',
 | 
				
			||||||
 | 
										'label' => 'Parent tag ID',
 | 
				
			||||||
 | 
									],
 | 
				
			||||||
 | 
									'id_asset_thumb' => [
 | 
				
			||||||
 | 
										'type' => 'numeric',
 | 
				
			||||||
 | 
										'label' => 'Thumbnail asset ID',
 | 
				
			||||||
 | 
										'is_optional' => true,
 | 
				
			||||||
 | 
									],
 | 
				
			||||||
				'kind' => [
 | 
									'kind' => [
 | 
				
			||||||
					'type' => 'select',
 | 
										'type' => 'select',
 | 
				
			||||||
					'label' => 'Kind of tag',
 | 
										'label' => 'Kind of tag',
 | 
				
			||||||
@ -74,11 +82,6 @@ class EditTag extends HTMLController
 | 
				
			|||||||
						'Person' => 'Person',
 | 
											'Person' => 'Person',
 | 
				
			||||||
					],
 | 
										],
 | 
				
			||||||
				],
 | 
									],
 | 
				
			||||||
			'id_user_owner' => [
 | 
					 | 
				
			||||||
				'type' => 'select',
 | 
					 | 
				
			||||||
				'label' => 'Owner',
 | 
					 | 
				
			||||||
				'options' => [0 => '(nobody)'] + Member::getMemberMap(),
 | 
					 | 
				
			||||||
			],
 | 
					 | 
				
			||||||
				'tag' => [
 | 
									'tag' => [
 | 
				
			||||||
					'type' => 'text',
 | 
										'type' => 'text',
 | 
				
			||||||
					'label' => 'Tag title',
 | 
										'label' => 'Tag title',
 | 
				
			||||||
@ -98,18 +101,7 @@ class EditTag extends HTMLController
 | 
				
			|||||||
					'maxlength' => 255,
 | 
										'maxlength' => 255,
 | 
				
			||||||
					'is_optional' => true,
 | 
										'is_optional' => true,
 | 
				
			||||||
				],
 | 
									],
 | 
				
			||||||
		];
 | 
								],
 | 
				
			||||||
 | 
					 | 
				
			||||||
		if (!$user->isAdmin())
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			unset($fields['kind']);
 | 
					 | 
				
			||||||
			unset($fields['id_user_owner']);
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		$form = new Form([
 | 
					 | 
				
			||||||
			'request_url' => BASEURL . '/edittag/?' . ($id_tag ? 'id=' . $id_tag : 'add'),
 | 
					 | 
				
			||||||
			'buttons_extra' => $after_form,
 | 
					 | 
				
			||||||
			'fields' => $fields,
 | 
					 | 
				
			||||||
		]);
 | 
							]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Create the form, add in default values.
 | 
							// Create the form, add in default values.
 | 
				
			||||||
@ -117,68 +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))
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			$current_page = isset($_GET['page']) ? (int) $_GET['page'] : 1;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			list($assets, $num_assets) = AssetIterator::getByOptions([
 | 
					 | 
				
			||||||
				'direction' => 'desc',
 | 
					 | 
				
			||||||
				'limit' => self::THUMBS_PER_PAGE,
 | 
					 | 
				
			||||||
				'page' => $current_page,
 | 
					 | 
				
			||||||
				'id_tag' => $id_tag,
 | 
					 | 
				
			||||||
			], true);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			// If we have asset images, show the thumbnail manager
 | 
					 | 
				
			||||||
			if ($num_assets > 0)
 | 
					 | 
				
			||||||
			{
 | 
					 | 
				
			||||||
				$manager = new FeaturedThumbnailManager($assets, $id_tag ? $tag->id_asset_thumb : 0);
 | 
					 | 
				
			||||||
				$this->page->adopt($manager);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				// Make a page index as needed, while we're at it.
 | 
					 | 
				
			||||||
				if ($num_assets > self::THUMBS_PER_PAGE)
 | 
					 | 
				
			||||||
				{
 | 
					 | 
				
			||||||
					$index = new PageIndex([
 | 
					 | 
				
			||||||
						'recordCount' => $num_assets,
 | 
					 | 
				
			||||||
						'items_per_page' => self::THUMBS_PER_PAGE,
 | 
					 | 
				
			||||||
						'start' => ($current_page - 1) * self::THUMBS_PER_PAGE,
 | 
					 | 
				
			||||||
						'base_url' => BASEURL . '/edittag/?id=' . $id_tag,
 | 
					 | 
				
			||||||
						'page_slug' => '&page=%PAGE%',
 | 
					 | 
				
			||||||
					]);
 | 
					 | 
				
			||||||
					$manager->adopt(new PageIndexWidget($index));
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		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', '=>' => '', "'" => "", ":"=> "", '/' => '-', '\\' => '-']);
 | 
				
			||||||
@ -188,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']))
 | 
				
			||||||
				{
 | 
									{
 | 
				
			||||||
@ -205,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;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
				
			|||||||
@ -33,7 +33,7 @@ class EditUser extends HTMLController
 | 
				
			|||||||
		{
 | 
							{
 | 
				
			||||||
			// Don't be stupid.
 | 
								// Don't be stupid.
 | 
				
			||||||
			if ($current_user->getUserId() == $id_user)
 | 
								if ($current_user->getUserId() == $id_user)
 | 
				
			||||||
				throw new Exception('Sorry, I cannot allow you to delete yourself.');
 | 
									trigger_error('Sorry, I cannot allow you to delete yourself.', E_USER_ERROR);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			// So far so good?
 | 
								// So far so good?
 | 
				
			||||||
			$user = Member::fromId($id_user);
 | 
								$user = Member::fromId($id_user);
 | 
				
			||||||
@ -43,7 +43,7 @@ class EditUser extends HTMLController
 | 
				
			|||||||
				exit;
 | 
									exit;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			else
 | 
								else
 | 
				
			||||||
				throw new Exception('Cannot delete user: an error occured while processing the request.');
 | 
									trigger_error('Cannot delete user: an error occured while processing the request.', E_USER_ERROR);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		// Editing one, then, surely.
 | 
							// Editing one, then, surely.
 | 
				
			||||||
		else
 | 
							else
 | 
				
			||||||
@ -69,7 +69,7 @@ class EditUser extends HTMLController
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
		$form = new Form([
 | 
							$form = new Form([
 | 
				
			||||||
			'request_url' => BASEURL . '/edituser/?' . ($id_user ? 'id=' . $id_user : 'add'),
 | 
								'request_url' => BASEURL . '/edituser/?' . ($id_user ? 'id=' . $id_user : 'add'),
 | 
				
			||||||
			'buttons_extra' => $after_form,
 | 
								'content_below' => $after_form,
 | 
				
			||||||
			'fields' => [
 | 
								'fields' => [
 | 
				
			||||||
				'first_name' => [
 | 
									'first_name' => [
 | 
				
			||||||
					'type' => 'text',
 | 
										'type' => 'text',
 | 
				
			||||||
@ -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']))
 | 
				
			||||||
				{
 | 
									{
 | 
				
			||||||
 | 
				
			|||||||
@ -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);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -24,9 +24,7 @@ class Login extends HTMLController
 | 
				
			|||||||
			if (Authentication::checkPassword($_POST['emailaddress'], $_POST['password']))
 | 
								if (Authentication::checkPassword($_POST['emailaddress'], $_POST['password']))
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				parent::__construct('Login');
 | 
									parent::__construct('Login');
 | 
				
			||||||
 | 
									$_SESSION['user_id'] = Authentication::getUserId($_POST['emailaddress']);
 | 
				
			||||||
				$user = Member::fromEmailAddress($_POST['emailaddress']);
 | 
					 | 
				
			||||||
				$_SESSION['user_id'] = $user->getUserId();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
				if (isset($_POST['redirect_url']))
 | 
									if (isset($_POST['redirect_url']))
 | 
				
			||||||
					header('Location: ' . base64_decode($_POST['redirect_url']));
 | 
										header('Location: ' . base64_decode($_POST['redirect_url']));
 | 
				
			||||||
@ -46,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']))
 | 
				
			||||||
 | 
				
			|||||||
@ -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);
 | 
				
			||||||
 | 
				
			|||||||
@ -18,7 +18,8 @@ class ManageAlbums extends HTMLController
 | 
				
			|||||||
			'form' => [
 | 
								'form' => [
 | 
				
			||||||
				'action' => BASEURL . '/editalbum/',
 | 
									'action' => BASEURL . '/editalbum/',
 | 
				
			||||||
				'method' => 'get',
 | 
									'method' => 'get',
 | 
				
			||||||
				'controls' => [
 | 
									'class' => 'floatright',
 | 
				
			||||||
 | 
									'buttons' => [
 | 
				
			||||||
					'add' => [
 | 
										'add' => [
 | 
				
			||||||
						'type' => 'submit',
 | 
											'type' => 'submit',
 | 
				
			||||||
						'caption' => 'Add new album',
 | 
											'caption' => 'Add new album',
 | 
				
			||||||
@ -34,14 +35,18 @@ class ManageAlbums extends HTMLController
 | 
				
			|||||||
				'tag' => [
 | 
									'tag' => [
 | 
				
			||||||
					'header' => 'Album',
 | 
										'header' => 'Album',
 | 
				
			||||||
					'is_sortable' => true,
 | 
										'is_sortable' => true,
 | 
				
			||||||
 | 
										'parse' => [
 | 
				
			||||||
						'link' => BASEURL . '/editalbum/?id={ID_TAG}',
 | 
											'link' => BASEURL . '/editalbum/?id={ID_TAG}',
 | 
				
			||||||
					'value' => 'tag',
 | 
											'data' => 'tag',
 | 
				
			||||||
 | 
										],
 | 
				
			||||||
				],
 | 
									],
 | 
				
			||||||
				'slug' => [
 | 
									'slug' => [
 | 
				
			||||||
					'header' => 'Slug',
 | 
										'header' => 'Slug',
 | 
				
			||||||
					'is_sortable' => true,
 | 
										'is_sortable' => true,
 | 
				
			||||||
 | 
										'parse' => [
 | 
				
			||||||
						'link' => BASEURL . '/editalbum/?id={ID_TAG}',
 | 
											'link' => BASEURL . '/editalbum/?id={ID_TAG}',
 | 
				
			||||||
					'value' => 'slug',
 | 
											'data' => 'slug',
 | 
				
			||||||
 | 
										],
 | 
				
			||||||
				],
 | 
									],
 | 
				
			||||||
				'count' => [
 | 
									'count' => [
 | 
				
			||||||
					'header' => '# Photos',
 | 
										'header' => '# Photos',
 | 
				
			||||||
@ -49,20 +54,51 @@ class ManageAlbums extends HTMLController
 | 
				
			|||||||
					'value' => 'count',
 | 
										'value' => 'count',
 | 
				
			||||||
				],
 | 
									],
 | 
				
			||||||
			],
 | 
								],
 | 
				
			||||||
			'default_sort_order' => 'tag',
 | 
								'start' => !empty($_GET['start']) ? (int) $_GET['start'] : 0,
 | 
				
			||||||
			'default_sort_direction' => 'up',
 | 
								'sort_order' => !empty($_GET['order']) ? $_GET['order'] : null,
 | 
				
			||||||
			'start' => $_GET['start'] ?? 0,
 | 
								'sort_direction' => !empty($_GET['dir']) ? $_GET['dir'] : null,
 | 
				
			||||||
			'sort_order' => $_GET['order'] ?? '',
 | 
					 | 
				
			||||||
			'sort_direction' => $_GET['dir'] ?? '',
 | 
					 | 
				
			||||||
			'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' => 'floatleft',
 | 
				
			||||||
			'base_url' => BASEURL . '/managealbums/',
 | 
								'base_url' => BASEURL . '/managealbums/',
 | 
				
			||||||
			'get_data' => function($offset, $limit, $order, $direction) {
 | 
								'get_data' => function($offset = 0, $limit = 9999, $order = '', $direction = 'up') {
 | 
				
			||||||
				return Tag::getOffset($offset, $limit, $order, $direction, true);
 | 
									if (!in_array($order, ['id_tag', 'tag', 'slug', 'count']))
 | 
				
			||||||
 | 
										$order = 'tag';
 | 
				
			||||||
 | 
									if (!in_array($direction, ['up', 'down']))
 | 
				
			||||||
 | 
										$direction = 'up';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									$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' => $rows,
 | 
				
			||||||
 | 
										'order' => $order,
 | 
				
			||||||
 | 
										'direction' => ($direction == 'up' ? 'up' : 'down'),
 | 
				
			||||||
 | 
									];
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
			'get_count' => function() {
 | 
								'get_count' => function() {
 | 
				
			||||||
				return Tag::getCount(false, 'Album', true);
 | 
									return 9999;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		];
 | 
							];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -70,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;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -14,57 +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',
 | 
					 | 
				
			||||||
				'is_embed' => true,
 | 
					 | 
				
			||||||
				'controls' => [
 | 
					 | 
				
			||||||
					'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,
 | 
					 | 
				
			||||||
					'format' => fn($row) =>
 | 
					 | 
				
			||||||
						'<input type="checkbox" class="asset_select" name="delete[]" value="' . $row['id_asset'] . '">',
 | 
					 | 
				
			||||||
				],
 | 
					 | 
				
			||||||
				'thumbnail' => [
 | 
					 | 
				
			||||||
					'header' => ' ',
 | 
					 | 
				
			||||||
					'is_sortable' => false,
 | 
					 | 
				
			||||||
					'cell_class' => 'text-center',
 | 
					 | 
				
			||||||
					'format' => function($row) {
 | 
					 | 
				
			||||||
						$asset = Image::byRow($row);
 | 
					 | 
				
			||||||
						$width = $height = 65;
 | 
					 | 
				
			||||||
						if ($asset->isImage())
 | 
					 | 
				
			||||||
						{
 | 
					 | 
				
			||||||
							if ($asset->isPortrait())
 | 
					 | 
				
			||||||
								$width = null;
 | 
					 | 
				
			||||||
							else
 | 
					 | 
				
			||||||
								$height = null;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
							$thumb = $asset->getThumbnailUrl($width, $height);
 | 
					 | 
				
			||||||
						}
 | 
					 | 
				
			||||||
						else
 | 
					 | 
				
			||||||
							$thumb = BASEURL . '/images/nothumb.svg';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
						$width = isset($width) ? $width . 'px' : 'auto';
 | 
					 | 
				
			||||||
						$height = isset($height) ? $height . 'px' : 'auto';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
						return sprintf('<img src="%s" style="width: %s; height: %s;">', $thumb, $width, $height);
 | 
					 | 
				
			||||||
					},
 | 
					 | 
				
			||||||
				],
 | 
					 | 
				
			||||||
				'id_asset' => [
 | 
									'id_asset' => [
 | 
				
			||||||
					'value' => 'id_asset',
 | 
										'value' => 'id_asset',
 | 
				
			||||||
					'header' => 'ID',
 | 
										'header' => 'ID',
 | 
				
			||||||
@ -79,24 +32,27 @@ class ManageAssets extends HTMLController
 | 
				
			|||||||
					'value' => 'filename',
 | 
										'value' => 'filename',
 | 
				
			||||||
					'header' => 'Filename',
 | 
										'header' => 'Filename',
 | 
				
			||||||
					'is_sortable' => true,
 | 
										'is_sortable' => true,
 | 
				
			||||||
 | 
										'parse' => [
 | 
				
			||||||
 | 
											'type' => 'value',
 | 
				
			||||||
						'link' => BASEURL . '/editasset/?id={ID_ASSET}',
 | 
											'link' => BASEURL . '/editasset/?id={ID_ASSET}',
 | 
				
			||||||
					'value' => 'filename',
 | 
											'data' => 'filename',
 | 
				
			||||||
					],
 | 
										],
 | 
				
			||||||
				'id_user_uploaded' => [
 | 
									],
 | 
				
			||||||
					'header' => 'User uploaded',
 | 
									'title' => [
 | 
				
			||||||
 | 
										'header' => 'Title',
 | 
				
			||||||
					'is_sortable' => true,
 | 
										'is_sortable' => true,
 | 
				
			||||||
					'format' => function($row) {
 | 
										'parse' => [
 | 
				
			||||||
						if (!empty($row['id_user']))
 | 
											'type' => 'value',
 | 
				
			||||||
							return sprintf('<a href="%s/edituser/?id=%d">%s</a>', BASEURL, $row['id_user'],
 | 
											'link' => BASEURL . '/editasset/?id={ID_ASSET}',
 | 
				
			||||||
								$row['first_name'] . ' ' . $row['surname']);
 | 
											'data' => 'title',
 | 
				
			||||||
						else
 | 
										],
 | 
				
			||||||
							return 'n/a';
 | 
					 | 
				
			||||||
					},
 | 
					 | 
				
			||||||
				],
 | 
									],
 | 
				
			||||||
				'dimensions' => [
 | 
									'dimensions' => [
 | 
				
			||||||
					'header' => 'Dimensions',
 | 
										'header' => 'Dimensions',
 | 
				
			||||||
					'is_sortable' => false,
 | 
										'is_sortable' => false,
 | 
				
			||||||
					'format' => function($row) {
 | 
										'parse' => [
 | 
				
			||||||
 | 
											'type' => 'function',
 | 
				
			||||||
 | 
											'data' => function($row) {
 | 
				
			||||||
							if (!empty($row['image_width']))
 | 
												if (!empty($row['image_width']))
 | 
				
			||||||
								return $row['image_width'] . ' x ' . $row['image_height'];
 | 
													return $row['image_width'] . ' x ' . $row['image_height'];
 | 
				
			||||||
							else
 | 
												else
 | 
				
			||||||
@ -104,39 +60,41 @@ class ManageAssets extends HTMLController
 | 
				
			|||||||
						},
 | 
											},
 | 
				
			||||||
					],
 | 
										],
 | 
				
			||||||
				],
 | 
									],
 | 
				
			||||||
			'default_sort_order' => 'id_asset',
 | 
								],
 | 
				
			||||||
			'default_sort_direction' => 'down',
 | 
								'start' => !empty($_GET['start']) ? (int) $_GET['start'] : 0,
 | 
				
			||||||
			'start' => $_GET['start'] ?? 0,
 | 
								'sort_order' => !empty($_GET['order']) ? $_GET['order'] : '',
 | 
				
			||||||
			'sort_order' => $_GET['order'] ?? '',
 | 
								'sort_direction' => !empty($_GET['dir']) ? $_GET['dir'] : '',
 | 
				
			||||||
			'sort_direction' => $_GET['dir'] ?? '',
 | 
					 | 
				
			||||||
			'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' => 'pull_left',
 | 
				
			||||||
			'base_url' => BASEURL . '/manageassets/',
 | 
								'base_url' => BASEURL . '/manageassets/',
 | 
				
			||||||
			'get_data' => 'Asset::getOffset',
 | 
								'get_data' => function($offset = 0, $limit = 30, $order = '', $direction = 'down') {
 | 
				
			||||||
 | 
									if (!in_array($order, ['id_asset', 'title', 'subdir', 'filename']))
 | 
				
			||||||
 | 
										$order = 'id_asset';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									$data = Registry::get('db')->queryAssocs('
 | 
				
			||||||
 | 
										SELECT id_asset, title, subdir, filename, image_width, image_height
 | 
				
			||||||
 | 
										FROM assets
 | 
				
			||||||
 | 
										ORDER BY {raw:order}
 | 
				
			||||||
 | 
										LIMIT {int:offset}, {int:limit}',
 | 
				
			||||||
 | 
										[
 | 
				
			||||||
 | 
											'order' => $order . ($direction == 'up' ? ' ASC' : ' DESC'),
 | 
				
			||||||
 | 
											'offset' => $offset,
 | 
				
			||||||
 | 
											'limit' => $limit,
 | 
				
			||||||
 | 
										]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									return [
 | 
				
			||||||
 | 
										'rows' => $data,
 | 
				
			||||||
 | 
										'order' => $order,
 | 
				
			||||||
 | 
										'direction' => $direction,
 | 
				
			||||||
 | 
									];
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
			'get_count' => 'Asset::getCount',
 | 
								'get_count' => 'Asset::getCount',
 | 
				
			||||||
		];
 | 
							];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		$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;
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -14,8 +14,8 @@ class ManageErrors extends HTMLController
 | 
				
			|||||||
		if (!Registry::get('user')->isAdmin())
 | 
							if (!Registry::get('user')->isAdmin())
 | 
				
			||||||
			throw new NotAllowedException();
 | 
								throw new NotAllowedException();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Clearing, are we?
 | 
							// Flushing, are we?
 | 
				
			||||||
		if (isset($_POST['clear']) && Session::validateSession('get'))
 | 
							if (isset($_POST['flush']) && Session::validateSession('get'))
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			ErrorLog::flush();
 | 
								ErrorLog::flush();
 | 
				
			||||||
			header('Location: ' . BASEURL . '/manageerrors/');
 | 
								header('Location: ' . BASEURL . '/manageerrors/');
 | 
				
			||||||
@ -29,24 +29,24 @@ class ManageErrors extends HTMLController
 | 
				
			|||||||
			'form' => [
 | 
								'form' => [
 | 
				
			||||||
				'action' => BASEURL . '/manageerrors/?' . Session::getSessionTokenKey() . '=' . Session::getSessionToken(),
 | 
									'action' => BASEURL . '/manageerrors/?' . Session::getSessionTokenKey() . '=' . Session::getSessionToken(),
 | 
				
			||||||
				'method' => 'post',
 | 
									'method' => 'post',
 | 
				
			||||||
				'controls' => [
 | 
									'class' => 'floatright',
 | 
				
			||||||
					'clear' => [
 | 
									'buttons' => [
 | 
				
			||||||
 | 
										'flush' => [
 | 
				
			||||||
						'type' => 'submit',
 | 
											'type' => 'submit',
 | 
				
			||||||
						'caption' => 'Delete all',
 | 
											'caption' => 'Delete all',
 | 
				
			||||||
						'class' => 'btn-danger',
 | 
					 | 
				
			||||||
					],
 | 
										],
 | 
				
			||||||
				],
 | 
									],
 | 
				
			||||||
			],
 | 
								],
 | 
				
			||||||
			'columns' => [
 | 
								'columns' => [
 | 
				
			||||||
				'id_entry' => [
 | 
									'id' => [
 | 
				
			||||||
					'value' => 'id_entry',
 | 
										'value' => 'id_entry',
 | 
				
			||||||
					'header' => '#',
 | 
										'header' => '#',
 | 
				
			||||||
					'is_sortable' => true,
 | 
										'is_sortable' => true,
 | 
				
			||||||
				],
 | 
									],
 | 
				
			||||||
				'message' => [
 | 
									'message' => [
 | 
				
			||||||
					'header' => 'Message / URL',
 | 
										'parse' => [
 | 
				
			||||||
					'is_sortable' => false,
 | 
											'type' => 'function',
 | 
				
			||||||
					'format' => function($row) {
 | 
											'data' => function($row) {
 | 
				
			||||||
							return $row['message'] . '<br>' .
 | 
												return $row['message'] . '<br>' .
 | 
				
			||||||
								'<div><a onclick="this.parentNode.childNodes[1].style.display=\'block\';this.style.display=\'none\';">Show debug info</a>' .
 | 
													'<div><a onclick="this.parentNode.childNodes[1].style.display=\'block\';this.style.display=\'none\';">Show debug info</a>' .
 | 
				
			||||||
								'<pre style="display: none">' . htmlspecialchars($row['debug_info']) .
 | 
													'<pre style="display: none">' . htmlspecialchars($row['debug_info']) .
 | 
				
			||||||
@ -54,7 +54,10 @@ class ManageErrors extends HTMLController
 | 
				
			|||||||
								'<small><a href="' . BASEURL .
 | 
													'<small><a href="' . BASEURL .
 | 
				
			||||||
									htmlspecialchars($row['request_uri']) . '">' .
 | 
														htmlspecialchars($row['request_uri']) . '">' .
 | 
				
			||||||
									htmlspecialchars($row['request_uri']) . '</a></small>';
 | 
														htmlspecialchars($row['request_uri']) . '</a></small>';
 | 
				
			||||||
					},
 | 
											}
 | 
				
			||||||
 | 
										],
 | 
				
			||||||
 | 
										'header' => 'Message / URL',
 | 
				
			||||||
 | 
										'is_sortable' => false,
 | 
				
			||||||
				],
 | 
									],
 | 
				
			||||||
				'file' => [
 | 
									'file' => [
 | 
				
			||||||
					'value' => 'file',
 | 
										'value' => 'file',
 | 
				
			||||||
@ -67,10 +70,12 @@ class ManageErrors extends HTMLController
 | 
				
			|||||||
					'is_sortable' => true,
 | 
										'is_sortable' => true,
 | 
				
			||||||
				],
 | 
									],
 | 
				
			||||||
				'time' => [
 | 
									'time' => [
 | 
				
			||||||
					'format' => [
 | 
										'parse' => [
 | 
				
			||||||
						'type' => 'timestamp',
 | 
											'type' => 'timestamp',
 | 
				
			||||||
 | 
											'data' => [
 | 
				
			||||||
 | 
												'timestamp' => 'time',
 | 
				
			||||||
							'pattern' => 'long',
 | 
												'pattern' => 'long',
 | 
				
			||||||
						'value' => 'time',
 | 
											],
 | 
				
			||||||
					],
 | 
										],
 | 
				
			||||||
					'header' => 'Time',
 | 
										'header' => 'Time',
 | 
				
			||||||
					'is_sortable' => true,
 | 
										'is_sortable' => true,
 | 
				
			||||||
@ -83,20 +88,41 @@ class ManageErrors extends HTMLController
 | 
				
			|||||||
				'uid' => [
 | 
									'uid' => [
 | 
				
			||||||
					'header' => 'UID',
 | 
										'header' => 'UID',
 | 
				
			||||||
					'is_sortable' => true,
 | 
										'is_sortable' => true,
 | 
				
			||||||
					'link' => BASEURL . '/edituser/?id={ID_USER}',
 | 
										'parse' => [
 | 
				
			||||||
					'value' => 'id_user',
 | 
											'link' => BASEURL . '/member/?id={ID_USER}',
 | 
				
			||||||
 | 
											'data' => 'id_user',
 | 
				
			||||||
					],
 | 
										],
 | 
				
			||||||
				],
 | 
									],
 | 
				
			||||||
			'default_sort_order' => 'id_entry',
 | 
								],
 | 
				
			||||||
			'default_sort_direction' => 'down',
 | 
								'start' => !empty($_GET['start']) ? (int) $_GET['start'] : 0,
 | 
				
			||||||
			'start' => $_GET['start'] ?? 0,
 | 
								'sort_order' => !empty($_GET['order']) ? $_GET['order'] : '',
 | 
				
			||||||
			'sort_order' => $_GET['order'] ?? '',
 | 
								'sort_direction' => !empty($_GET['dir']) ? $_GET['dir'] : '',
 | 
				
			||||||
			'sort_direction' => $_GET['dir'] ?? '',
 | 
					 | 
				
			||||||
			'no_items_label' => "No errors to display -- we're all good!",
 | 
								'no_items_label' => "No errors to display -- we're all good!",
 | 
				
			||||||
			'items_per_page' => 20,
 | 
								'items_per_page' => 20,
 | 
				
			||||||
 | 
								'index_class' => 'floatleft',
 | 
				
			||||||
			'base_url' => BASEURL . '/manageerrors/',
 | 
								'base_url' => BASEURL . '/manageerrors/',
 | 
				
			||||||
			'get_count' => 'ErrorLog::getCount',
 | 
								'get_count' => 'ErrorLog::getCount',
 | 
				
			||||||
			'get_data' => 'ErrorLog::getOffset',
 | 
								'get_data' => function($offset = 0, $limit = 20, $order = '', $direction = 'down') {
 | 
				
			||||||
 | 
									if (!in_array($order, ['id_entry', 'file', 'line', 'time', 'ipaddress', 'id_user']))
 | 
				
			||||||
 | 
										$order = 'id_entry';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									$data = Registry::get('db')->queryAssocs('
 | 
				
			||||||
 | 
										SELECT *
 | 
				
			||||||
 | 
										FROM log_errors
 | 
				
			||||||
 | 
										ORDER BY {raw:order}
 | 
				
			||||||
 | 
										LIMIT {int:offset}, {int:limit}',
 | 
				
			||||||
 | 
										[
 | 
				
			||||||
 | 
											'order' => $order . ($direction === 'up' ? ' ASC' : ' DESC'),
 | 
				
			||||||
 | 
											'offset' => $offset,
 | 
				
			||||||
 | 
											'limit' => $limit,
 | 
				
			||||||
 | 
										]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									return [
 | 
				
			||||||
 | 
										'rows' => $data,
 | 
				
			||||||
 | 
										'order' => $order,
 | 
				
			||||||
 | 
										'direction' => $direction,
 | 
				
			||||||
 | 
									];
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
		];
 | 
							];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		$error_log = new GenericTable($options);
 | 
							$error_log = new GenericTable($options);
 | 
				
			||||||
 | 
				
			|||||||
@ -14,13 +14,12 @@ 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',
 | 
				
			||||||
				'controls' => [
 | 
									'class' => 'floatright',
 | 
				
			||||||
 | 
									'buttons' => [
 | 
				
			||||||
					'add' => [
 | 
										'add' => [
 | 
				
			||||||
						'type' => 'submit',
 | 
											'type' => 'submit',
 | 
				
			||||||
						'caption' => 'Add new tag',
 | 
											'caption' => 'Add new tag',
 | 
				
			||||||
@ -36,25 +35,23 @@ class ManageTags extends HTMLController
 | 
				
			|||||||
				'tag' => [
 | 
									'tag' => [
 | 
				
			||||||
					'header' => 'Tag',
 | 
										'header' => 'Tag',
 | 
				
			||||||
					'is_sortable' => true,
 | 
										'is_sortable' => true,
 | 
				
			||||||
 | 
										'parse' => [
 | 
				
			||||||
						'link' => BASEURL . '/edittag/?id={ID_TAG}',
 | 
											'link' => BASEURL . '/edittag/?id={ID_TAG}',
 | 
				
			||||||
					'value' => 'tag',
 | 
											'data' => 'tag',
 | 
				
			||||||
 | 
										],
 | 
				
			||||||
				],
 | 
									],
 | 
				
			||||||
				'slug' => [
 | 
									'slug' => [
 | 
				
			||||||
					'header' => 'Slug',
 | 
										'header' => 'Slug',
 | 
				
			||||||
					'is_sortable' => true,
 | 
										'is_sortable' => true,
 | 
				
			||||||
 | 
										'parse' => [
 | 
				
			||||||
						'link' => BASEURL . '/edittag/?id={ID_TAG}',
 | 
											'link' => BASEURL . '/edittag/?id={ID_TAG}',
 | 
				
			||||||
					'value' => 'slug',
 | 
											'data' => 'slug',
 | 
				
			||||||
					],
 | 
										],
 | 
				
			||||||
				'id_user_owner' => [
 | 
									],
 | 
				
			||||||
					'header' => 'Owning user',
 | 
									'kind' => [
 | 
				
			||||||
 | 
										'header' => 'Kind',
 | 
				
			||||||
					'is_sortable' => true,
 | 
										'is_sortable' => true,
 | 
				
			||||||
					'format' => function($row) {
 | 
										'value' => 'kind',
 | 
				
			||||||
						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',
 | 
				
			||||||
@ -62,20 +59,45 @@ class ManageTags extends HTMLController
 | 
				
			|||||||
					'value' => 'count',
 | 
										'value' => 'count',
 | 
				
			||||||
				],
 | 
									],
 | 
				
			||||||
			],
 | 
								],
 | 
				
			||||||
			'default_sort_order' => 'tag',
 | 
								'start' => !empty($_GET['start']) ? (int) $_GET['start'] : 0,
 | 
				
			||||||
			'default_sort_direction' => 'up',
 | 
								'sort_order' => !empty($_GET['order']) ? $_GET['order'] : null,
 | 
				
			||||||
			'start' => $_GET['start'] ?? 0,
 | 
								'sort_direction' => !empty($_GET['dir']) ? $_GET['dir'] : null,
 | 
				
			||||||
			'sort_order' => $_GET['order'] ?? '',
 | 
					 | 
				
			||||||
			'sort_direction' => $_GET['dir'] ?? '',
 | 
					 | 
				
			||||||
			'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' => 9999,
 | 
								'items_per_page' => 30,
 | 
				
			||||||
 | 
								'index_class' => 'floatleft',
 | 
				
			||||||
			'base_url' => BASEURL . '/managetags/',
 | 
								'base_url' => BASEURL . '/managetags/',
 | 
				
			||||||
			'get_data' => function($offset, $limit, $order, $direction) {
 | 
								'get_data' => function($offset = 0, $limit = 30, $order = '', $direction = 'up') {
 | 
				
			||||||
				return Tag::getOffset($offset, $limit, $order, $direction, false);
 | 
									if (!in_array($order, ['id_tag', 'tag', 'slug', 'kind', 'count']))
 | 
				
			||||||
 | 
										$order = 'tag';
 | 
				
			||||||
 | 
									if (!in_array($direction, ['up', 'down']))
 | 
				
			||||||
 | 
										$direction = 'up';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									$data = Registry::get('db')->queryAssocs('
 | 
				
			||||||
 | 
										SELECT *
 | 
				
			||||||
 | 
										FROM tags
 | 
				
			||||||
 | 
										WHERE kind != {string:album}
 | 
				
			||||||
 | 
										ORDER BY {raw:order}
 | 
				
			||||||
 | 
										LIMIT {int:offset}, {int:limit}',
 | 
				
			||||||
 | 
										[
 | 
				
			||||||
 | 
											'order' => $order . ($direction == 'up' ? ' ASC' : ' DESC'),
 | 
				
			||||||
 | 
											'offset' => $offset,
 | 
				
			||||||
 | 
											'limit' => $limit,
 | 
				
			||||||
 | 
											'album' => 'Album',
 | 
				
			||||||
 | 
										]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									return [
 | 
				
			||||||
 | 
										'rows' => $data,
 | 
				
			||||||
 | 
										'order' => $order,
 | 
				
			||||||
 | 
										'direction' => ($direction == 'up' ? 'up' : 'down'),
 | 
				
			||||||
 | 
									];
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
			'get_count' => function() {
 | 
								'get_count' => function() {
 | 
				
			||||||
				return Tag::getCount(false, null, false);
 | 
									return Registry::get('db')->queryValue('
 | 
				
			||||||
 | 
										SELECT COUNT(*)
 | 
				
			||||||
 | 
										FROM tags
 | 
				
			||||||
 | 
										WHERE kind != {string:album}',
 | 
				
			||||||
 | 
										['album' => 'Album']);
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		];
 | 
							];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -14,13 +14,12 @@ 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',
 | 
				
			||||||
				'controls' => [
 | 
									'class' => 'floatright',
 | 
				
			||||||
 | 
									'buttons' => [
 | 
				
			||||||
					'add' => [
 | 
										'add' => [
 | 
				
			||||||
						'type' => 'submit',
 | 
											'type' => 'submit',
 | 
				
			||||||
						'caption' => 'Add new user',
 | 
											'caption' => 'Add new user',
 | 
				
			||||||
@ -36,20 +35,26 @@ class ManageUsers extends HTMLController
 | 
				
			|||||||
				'surname' => [
 | 
									'surname' => [
 | 
				
			||||||
					'header' => 'Last name',
 | 
										'header' => 'Last name',
 | 
				
			||||||
					'is_sortable' => true,
 | 
										'is_sortable' => true,
 | 
				
			||||||
 | 
										'parse' => [
 | 
				
			||||||
						'link' => BASEURL . '/edituser/?id={ID_USER}',
 | 
											'link' => BASEURL . '/edituser/?id={ID_USER}',
 | 
				
			||||||
					'value' => 'surname',
 | 
											'data' => 'surname',
 | 
				
			||||||
 | 
										],
 | 
				
			||||||
				],
 | 
									],
 | 
				
			||||||
				'first_name' => [
 | 
									'first_name' => [
 | 
				
			||||||
					'header' => 'First name',
 | 
										'header' => 'First name',
 | 
				
			||||||
					'is_sortable' => true,
 | 
										'is_sortable' => true,
 | 
				
			||||||
 | 
										'parse' => [
 | 
				
			||||||
						'link' => BASEURL . '/edituser/?id={ID_USER}',
 | 
											'link' => BASEURL . '/edituser/?id={ID_USER}',
 | 
				
			||||||
					'value' => 'first_name',
 | 
											'data' => 'first_name',
 | 
				
			||||||
 | 
										],
 | 
				
			||||||
				],
 | 
									],
 | 
				
			||||||
				'slug' => [
 | 
									'slug' => [
 | 
				
			||||||
					'header' => 'Slug',
 | 
										'header' => 'Slug',
 | 
				
			||||||
					'is_sortable' => true,
 | 
										'is_sortable' => true,
 | 
				
			||||||
 | 
										'parse' => [
 | 
				
			||||||
						'link' => BASEURL . '/edituser/?id={ID_USER}',
 | 
											'link' => BASEURL . '/edituser/?id={ID_USER}',
 | 
				
			||||||
					'value' => 'slug',
 | 
											'data' => 'slug',
 | 
				
			||||||
 | 
										],
 | 
				
			||||||
				],
 | 
									],
 | 
				
			||||||
				'emailaddress' => [
 | 
									'emailaddress' => [
 | 
				
			||||||
					'value' => 'emailaddress',
 | 
										'value' => 'emailaddress',
 | 
				
			||||||
@ -57,11 +62,12 @@ class ManageUsers extends HTMLController
 | 
				
			|||||||
					'is_sortable' => true,
 | 
										'is_sortable' => true,
 | 
				
			||||||
				],
 | 
									],
 | 
				
			||||||
				'last_action_time' => [
 | 
									'last_action_time' => [
 | 
				
			||||||
					'format' => [
 | 
										'parse' => [
 | 
				
			||||||
						'type' => 'timestamp',
 | 
											'type' => 'timestamp',
 | 
				
			||||||
 | 
											'data' => [
 | 
				
			||||||
 | 
												'timestamp' => 'last_action_time',
 | 
				
			||||||
							'pattern' => 'long',
 | 
												'pattern' => 'long',
 | 
				
			||||||
						'value' => 'last_action_time',
 | 
											],
 | 
				
			||||||
						'if_null' => 'n/a',
 | 
					 | 
				
			||||||
					],
 | 
										],
 | 
				
			||||||
					'header' => 'Last activity',
 | 
										'header' => 'Last activity',
 | 
				
			||||||
					'is_sortable' => true,
 | 
										'is_sortable' => true,
 | 
				
			||||||
@ -74,20 +80,48 @@ class ManageUsers extends HTMLController
 | 
				
			|||||||
				'is_admin' => [
 | 
									'is_admin' => [
 | 
				
			||||||
					'is_sortable' => true,
 | 
										'is_sortable' => true,
 | 
				
			||||||
					'header' => 'Admin?',
 | 
										'header' => 'Admin?',
 | 
				
			||||||
					'format' => fn($row) => $row['is_admin'] ? 'yes' : 'no',
 | 
										'parse' => [
 | 
				
			||||||
 | 
											'type' => 'function',
 | 
				
			||||||
 | 
											'data' => function($row) {
 | 
				
			||||||
 | 
												return $row['is_admin'] ? 'yes' : 'no';
 | 
				
			||||||
 | 
											}
 | 
				
			||||||
					],
 | 
										],
 | 
				
			||||||
				],
 | 
									],
 | 
				
			||||||
			'default_sort_order' => 'id_user',
 | 
								],
 | 
				
			||||||
			'default_sort_direction' => 'down',
 | 
								'start' => !empty($_GET['start']) ? (int) $_GET['start'] : 0,
 | 
				
			||||||
			'start' => $_GET['start'] ?? 0,
 | 
								'sort_order' => !empty($_GET['order']) ? $_GET['order'] : '',
 | 
				
			||||||
			'sort_order' => $_GET['order'] ?? '',
 | 
								'sort_direction' => !empty($_GET['dir']) ? $_GET['dir'] : '',
 | 
				
			||||||
			'sort_direction' => $_GET['dir'] ?? '',
 | 
					 | 
				
			||||||
			'title' => 'Manage users',
 | 
								'title' => 'Manage users',
 | 
				
			||||||
			'no_items_label' => 'No users meet the requirements of the current filter.',
 | 
								'no_items_label' => 'No users meet the requirements of the current filter.',
 | 
				
			||||||
			'items_per_page' => 30,
 | 
								'items_per_page' => 30,
 | 
				
			||||||
 | 
								'index_class' => 'floatleft',
 | 
				
			||||||
			'base_url' => BASEURL . '/manageusers/',
 | 
								'base_url' => BASEURL . '/manageusers/',
 | 
				
			||||||
			'get_data' => 'Member::getOffset',
 | 
								'get_data' => function($offset = 0, $limit = 30, $order = '', $direction = 'down') {
 | 
				
			||||||
			'get_count' => 'Member::getCount',
 | 
									if (!in_array($order, ['id_user', 'surname', 'first_name', 'slug', 'emailaddress', 'last_action_time', 'ip_address', 'is_admin']))
 | 
				
			||||||
 | 
										$order = 'id_user';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									$data = Registry::get('db')->queryAssocs('
 | 
				
			||||||
 | 
										SELECT *
 | 
				
			||||||
 | 
										FROM users
 | 
				
			||||||
 | 
										ORDER BY {raw:order}
 | 
				
			||||||
 | 
										LIMIT {int:offset}, {int:limit}',
 | 
				
			||||||
 | 
										[
 | 
				
			||||||
 | 
											'order' => $order . ($direction == 'up' ? ' ASC' : ' DESC'),
 | 
				
			||||||
 | 
											'offset' => $offset,
 | 
				
			||||||
 | 
											'limit' => $limit,
 | 
				
			||||||
 | 
										]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									return [
 | 
				
			||||||
 | 
										'rows' => $data,
 | 
				
			||||||
 | 
										'order' => $order,
 | 
				
			||||||
 | 
										'direction' => $direction,
 | 
				
			||||||
 | 
									];
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								'get_count' => function() {
 | 
				
			||||||
 | 
									return Registry::get('db')->queryValue('
 | 
				
			||||||
 | 
										SELECT COUNT(*)
 | 
				
			||||||
 | 
										FROM users');
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
		];
 | 
							];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		$table = new GenericTable($options);
 | 
							$table = new GenericTable($options);
 | 
				
			||||||
 | 
				
			|||||||
@ -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,
 | 
				
			||||||
 | 
				
			|||||||
@ -16,65 +16,14 @@ class ResetPassword extends HTMLController
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
		// Verifying an existing reset key?
 | 
							// Verifying an existing reset key?
 | 
				
			||||||
		if (isset($_GET['step'], $_GET['email'], $_GET['key']) && $_GET['step'] == 2)
 | 
							if (isset($_GET['step'], $_GET['email'], $_GET['key']) && $_GET['step'] == 2)
 | 
				
			||||||
			$this->verifyResetKey();
 | 
					 | 
				
			||||||
		else
 | 
					 | 
				
			||||||
			$this->requestResetKey();
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	private function requestResetKey()
 | 
					 | 
				
			||||||
	{
 | 
					 | 
				
			||||||
		parent::__construct('Reset password - ' . SITE_TITLE);
 | 
					 | 
				
			||||||
		$form = new ForgotPasswordForm();
 | 
					 | 
				
			||||||
		$this->page->adopt($form);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Have they submitted an email address yet?
 | 
					 | 
				
			||||||
		if (isset($_POST['emailaddress']) && preg_match('~^.+@.+\.[a-z]+$~', trim($_POST['emailaddress'])))
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			$user = Member::fromEmailAddress($_POST['emailaddress']);
 | 
					 | 
				
			||||||
			if (!$user)
 | 
					 | 
				
			||||||
			{
 | 
					 | 
				
			||||||
				$form->adopt(new Alert('Invalid email address', 'The email address you provided could not be found in our system. Please try again.', 'danger'));
 | 
					 | 
				
			||||||
				return;
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			if (Authentication::getResetTimeOut($user->getUserId()) > 0)
 | 
					 | 
				
			||||||
			{
 | 
					 | 
				
			||||||
				// Update the reset time-out to prevent hammering
 | 
					 | 
				
			||||||
				$resetTimeOut = Authentication::updateResetTimeOut($user->getUserId());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				// Present it to the user in a readable way
 | 
					 | 
				
			||||||
				if ($resetTimeOut > 3600)
 | 
					 | 
				
			||||||
					$timeOut = sprintf('%d hours', ceil($resetTimeOut / 3600));
 | 
					 | 
				
			||||||
				elseif ($resetTimeOut > 60)
 | 
					 | 
				
			||||||
					$timeOut = sprintf('%d minutes', ceil($resetTimeOut / 60));
 | 
					 | 
				
			||||||
				else
 | 
					 | 
				
			||||||
					$timeOut = sprintf('%d seconds', $resetTimeOut);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				$form->adopt(new Alert('Password reset token already sent', 'We already sent a password reset token to this email address recently. ' .
 | 
					 | 
				
			||||||
					'If no email was received, please wait ' . $timeOut . ' to try again.', 'error'));
 | 
					 | 
				
			||||||
				return;
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			Authentication::setResetKey($user->getUserId());
 | 
					 | 
				
			||||||
			Email::resetMail($user->getUserId());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			// Show the success message
 | 
					 | 
				
			||||||
			$this->page->clear();
 | 
					 | 
				
			||||||
			$box = new DummyBox('An email has been sent');
 | 
					 | 
				
			||||||
			$box->adopt(new Alert('', 'We have sent an email to ' . $_POST['emailaddress'] . ' containing details on how to reset your password.', 'success'));
 | 
					 | 
				
			||||||
			$this->page->adopt($box);
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	private function verifyResetKey()
 | 
					 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			$email = rawurldecode($_GET['email']);
 | 
								$email = rawurldecode($_GET['email']);
 | 
				
			||||||
		$user = Member::fromEmailAddress($email);
 | 
								$id_user = Authentication::getUserid($email);
 | 
				
			||||||
		if (!$user)
 | 
								if ($id_user === false)
 | 
				
			||||||
				throw new UserFacingException('Invalid email address. Please make sure you copied the full link in the email you received.');
 | 
									throw new UserFacingException('Invalid email address. Please make sure you copied the full link in the email you received.');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			$key = $_GET['key'];
 | 
								$key = $_GET['key'];
 | 
				
			||||||
		if (!Authentication::checkResetKey($user->getUserId(), $key))
 | 
								if (!Authentication::checkResetKey($id_user, $key))
 | 
				
			||||||
				throw new UserFacingException('Invalid reset token. Please make sure you copied the full link in the email you received. Note: you cannot use the same token twice.');
 | 
									throw new UserFacingException('Invalid reset token. Please make sure you copied the full link in the email you received. Note: you cannot use the same token twice.');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			parent::__construct('Reset password - ' . SITE_TITLE);
 | 
								parent::__construct('Reset password - ' . SITE_TITLE);
 | 
				
			||||||
@ -93,17 +42,40 @@ class ResetPassword extends HTMLController
 | 
				
			|||||||
				// So, are we good to go?
 | 
									// So, are we good to go?
 | 
				
			||||||
				if (empty($missing))
 | 
									if (empty($missing))
 | 
				
			||||||
				{
 | 
									{
 | 
				
			||||||
				Authentication::updatePassword($user->getUserId(), Authentication::computeHash($_POST['password1']));
 | 
										Authentication::updatePassword($id_user, Authentication::computeHash($_POST['password1']));
 | 
				
			||||||
 | 
					 | 
				
			||||||
				// Consume token, ensuring it isn't used again
 | 
					 | 
				
			||||||
				Authentication::consumeResetKey($user->getUserId());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
					$_SESSION['login_msg'] = ['Your password has been reset', 'You can now use the form below to log in to your account.', 'success'];
 | 
										$_SESSION['login_msg'] = ['Your password has been reset', 'You can now use the form below to log in to your account.', 'success'];
 | 
				
			||||||
					header('Location: ' . BASEURL . '/login/');
 | 
										header('Location: ' . BASEURL . '/login/');
 | 
				
			||||||
					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
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								parent::__construct('Reset password - ' . SITE_TITLE);
 | 
				
			||||||
 | 
								$form = new ForgotPasswordForm();
 | 
				
			||||||
 | 
								$this->page->adopt($form);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// Have they submitted an email address yet?
 | 
				
			||||||
 | 
								if (isset($_POST['emailaddress']) && preg_match('~^.+@.+\.[a-z]+$~', trim($_POST['emailaddress'])))
 | 
				
			||||||
 | 
								{
 | 
				
			||||||
 | 
									$id_user = Authentication::getUserid(trim($_POST['emailaddress']));
 | 
				
			||||||
 | 
									if ($id_user === false)
 | 
				
			||||||
 | 
									{
 | 
				
			||||||
 | 
										$form->adopt(new Alert('Invalid email address', 'The email address you provided could not be found in our system. Please try again.', 'error'));
 | 
				
			||||||
 | 
										return;
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									Authentication::setResetKey($id_user);
 | 
				
			||||||
 | 
									Email::resetMail($id_user);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									// Show the success message
 | 
				
			||||||
 | 
									$this->page->clear();
 | 
				
			||||||
 | 
									$box = new DummyBox('An email has been sent');
 | 
				
			||||||
 | 
									$box->adopt(new Alert('', 'We have sent an email to ' . $_POST['emailaddress'] . ' containing details on how to reset your password.', 'success'));
 | 
				
			||||||
 | 
									$this->page->adopt($box);
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -33,21 +33,22 @@ 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->id_asset_thumb = $asset->getId();
 | 
				
			||||||
				$tag->save();
 | 
									$tag->save();
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if (isset($_REQUEST['format']) && $_REQUEST['format'] === 'json')
 | 
								if (isset($_REQUEST['format']) && $_REQUEST['format'] === 'json')
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
 | 
				
			|||||||
@ -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 . '/' : ''));
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
				
			|||||||
@ -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;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -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 "' . $ptag->tag . '"';
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								elseif ($tag->kind === 'Person')
 | 
				
			||||||
 | 
								{
 | 
				
			||||||
 | 
									$back_link = BASEURL . '/people/';
 | 
				
			||||||
 | 
									$back_link_title = 'Back to "People"';
 | 
				
			||||||
 | 
									$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,124 +156,16 @@ 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)
 | 
						public function __destruct()
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		$buttons = [];
 | 
							if (isset($this->iterator))
 | 
				
			||||||
		$user = Registry::get('user');
 | 
								$this->iterator->clean();
 | 
				
			||||||
 | 
					 | 
				
			||||||
		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 "' . $ptag->tag . '"';
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		elseif ($tag->kind === 'Person')
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			$back_link = BASEURL . '/people/';
 | 
					 | 
				
			||||||
			$back_link_title = 'Back to "People"';
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		$description = !empty($tag->description) ? $tag->description : '';
 | 
					 | 
				
			||||||
		return new AlbumHeaderBox($tag->tag, $description, $back_link, $back_link_title);
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -46,12 +46,17 @@ 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.
 | 
				
			||||||
		$this->page->setCanonicalUrl(BASEURL . '/timeline/');
 | 
							$this->page->setCanonicalUrl(BASEURL . '/timeline/');
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public function __destruct()
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							if (isset($this->iterator))
 | 
				
			||||||
 | 
								$this->iterator->clean();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										319
									
								
								import_albums.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										319
									
								
								import_albums.php
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										20
									
								
								import_postprocess.sh
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										53
									
								
								migrate_thumbs.php
									
									
									
									
									
										Normal 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";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -1,2 +0,0 @@
 | 
				
			|||||||
/* Add time-out to password reset keys, and prevent repeated mails */
 | 
					 | 
				
			||||||
ALTER TABLE `users` ADD `reset_blocked_until` INT  UNSIGNED  NULL  AFTER `reset_key`;
 | 
					 | 
				
			||||||
@ -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'];
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										411
									
								
								models/Asset.php
									
									
									
									
									
								
							
							
						
						
									
										411
									
								
								models/Asset.php
									
									
									
									
									
								
							@ -8,55 +8,36 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
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;
 | 
				
			||||||
	protected $thumbnails;
 | 
						protected $thumbnails;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public function __construct(array $data)
 | 
						protected function __construct(array $data)
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		foreach ($data as $attribute => $value)
 | 
							foreach ($data as $attribute => $value)
 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			if (property_exists($this, $attribute))
 | 
					 | 
				
			||||||
			$this->$attribute = $value;
 | 
								$this->$attribute = $value;
 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (isset($data['date_captured']) && $data['date_captured'] !== null && !is_object($data['date_captured']))
 | 
							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('
 | 
				
			||||||
			SELECT *
 | 
								SELECT *
 | 
				
			||||||
			FROM assets
 | 
								FROM assets
 | 
				
			||||||
			WHERE id_asset = :id_asset',
 | 
								WHERE id_asset = {int:id_asset}',
 | 
				
			||||||
			[
 | 
								[
 | 
				
			||||||
				'id_asset' => $id_asset,
 | 
									'id_asset' => $id_asset,
 | 
				
			||||||
			]);
 | 
								]);
 | 
				
			||||||
@ -69,7 +50,7 @@ class Asset
 | 
				
			|||||||
		$row = Registry::get('db')->queryAssoc('
 | 
							$row = Registry::get('db')->queryAssoc('
 | 
				
			||||||
			SELECT *
 | 
								SELECT *
 | 
				
			||||||
			FROM assets
 | 
								FROM assets
 | 
				
			||||||
			WHERE slug = :slug',
 | 
								WHERE slug = {string:slug}',
 | 
				
			||||||
			[
 | 
								[
 | 
				
			||||||
				'slug' => $slug,
 | 
									'slug' => $slug,
 | 
				
			||||||
			]);
 | 
								]);
 | 
				
			||||||
@ -85,7 +66,7 @@ class Asset
 | 
				
			|||||||
		$row['meta'] = $db->queryPair('
 | 
							$row['meta'] = $db->queryPair('
 | 
				
			||||||
			SELECT variable, value
 | 
								SELECT variable, value
 | 
				
			||||||
			FROM assets_meta
 | 
								FROM assets_meta
 | 
				
			||||||
			WHERE id_asset = :id_asset',
 | 
								WHERE id_asset = {int:id_asset}',
 | 
				
			||||||
			[
 | 
								[
 | 
				
			||||||
				'id_asset' => $row['id_asset'],
 | 
									'id_asset' => $row['id_asset'],
 | 
				
			||||||
			]);
 | 
								]);
 | 
				
			||||||
@ -94,20 +75,21 @@ class Asset
 | 
				
			|||||||
		$row['thumbnails'] = $db->queryPair('
 | 
							$row['thumbnails'] = $db->queryPair('
 | 
				
			||||||
			SELECT
 | 
								SELECT
 | 
				
			||||||
				CONCAT(
 | 
									CONCAT(
 | 
				
			||||||
					width, :x, height,
 | 
										width,
 | 
				
			||||||
					IF(mode != :empty1, CONCAT(:_, mode), :empty2)
 | 
										{string:x},
 | 
				
			||||||
 | 
										height,
 | 
				
			||||||
 | 
										IF(mode != {string:empty}, CONCAT({string:_}, mode), {string:empty})
 | 
				
			||||||
				) AS selector, filename
 | 
									) AS selector, filename
 | 
				
			||||||
			FROM assets_thumbs
 | 
								FROM assets_thumbs
 | 
				
			||||||
			WHERE id_asset = :id_asset',
 | 
								WHERE id_asset = {int:id_asset}',
 | 
				
			||||||
			[
 | 
								[
 | 
				
			||||||
				'id_asset' => $row['id_asset'],
 | 
									'id_asset' => $row['id_asset'],
 | 
				
			||||||
				'empty1' => '',
 | 
									'empty' => '',
 | 
				
			||||||
				'empty2' => '',
 | 
					 | 
				
			||||||
				'x' => 'x',
 | 
									'x' => 'x',
 | 
				
			||||||
				'_' => '_',
 | 
									'_' => '_',
 | 
				
			||||||
			]);
 | 
								]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		return $return_format === 'object' ? new static($row) : $row;
 | 
							return $return_format == 'object' ? new Asset($row) : $row;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public static function fromIds(array $id_assets, $return_format = 'array')
 | 
						public static function fromIds(array $id_assets, $return_format = 'array')
 | 
				
			||||||
@ -120,14 +102,14 @@ class Asset
 | 
				
			|||||||
		$res = $db->query('
 | 
							$res = $db->query('
 | 
				
			||||||
			SELECT *
 | 
								SELECT *
 | 
				
			||||||
			FROM assets
 | 
								FROM assets
 | 
				
			||||||
			WHERE id_asset IN (@id_assets)
 | 
								WHERE id_asset IN ({array_int:id_assets})
 | 
				
			||||||
			ORDER BY id_asset',
 | 
								ORDER BY id_asset',
 | 
				
			||||||
			[
 | 
								[
 | 
				
			||||||
				'id_assets' => $id_assets,
 | 
									'id_assets' => $id_assets,
 | 
				
			||||||
			]);
 | 
								]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		$assets = [];
 | 
							$assets = [];
 | 
				
			||||||
		while ($asset = $db->fetchAssoc($res))
 | 
							while ($asset = $db->fetch_assoc($res))
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			$assets[$asset['id_asset']] = $asset;
 | 
								$assets[$asset['id_asset']] = $asset;
 | 
				
			||||||
			$assets[$asset['id_asset']]['meta'] = [];
 | 
								$assets[$asset['id_asset']]['meta'] = [];
 | 
				
			||||||
@ -137,7 +119,7 @@ class Asset
 | 
				
			|||||||
		$metas = $db->queryRows('
 | 
							$metas = $db->queryRows('
 | 
				
			||||||
			SELECT id_asset, variable, value
 | 
								SELECT id_asset, variable, value
 | 
				
			||||||
			FROM assets_meta
 | 
								FROM assets_meta
 | 
				
			||||||
			WHERE id_asset IN (@id_assets)
 | 
								WHERE id_asset IN ({array_int:id_assets})
 | 
				
			||||||
			ORDER BY id_asset',
 | 
								ORDER BY id_asset',
 | 
				
			||||||
			[
 | 
								[
 | 
				
			||||||
				'id_assets' => $id_assets,
 | 
									'id_assets' => $id_assets,
 | 
				
			||||||
@ -149,16 +131,17 @@ class Asset
 | 
				
			|||||||
		$thumbnails = $db->queryRows('
 | 
							$thumbnails = $db->queryRows('
 | 
				
			||||||
			SELECT id_asset,
 | 
								SELECT id_asset,
 | 
				
			||||||
				CONCAT(
 | 
									CONCAT(
 | 
				
			||||||
					width, :x, height,
 | 
										width,
 | 
				
			||||||
					IF(mode != :empty1, CONCAT(:_, mode), :empty2)
 | 
										{string:x},
 | 
				
			||||||
 | 
										height,
 | 
				
			||||||
 | 
										IF(mode != {string:empty}, CONCAT({string:_}, mode), {string:empty})
 | 
				
			||||||
				) AS selector, filename
 | 
									) AS selector, filename
 | 
				
			||||||
			FROM assets_thumbs
 | 
								FROM assets_thumbs
 | 
				
			||||||
			WHERE id_asset IN (@id_assets)
 | 
								WHERE id_asset IN ({array_int:id_assets})
 | 
				
			||||||
			ORDER BY id_asset',
 | 
								ORDER BY id_asset',
 | 
				
			||||||
			[
 | 
								[
 | 
				
			||||||
				'id_assets' => $id_assets,
 | 
									'id_assets' => $id_assets,
 | 
				
			||||||
				'empty1' => '',
 | 
									'empty' => '',
 | 
				
			||||||
				'empty2' => '',
 | 
					 | 
				
			||||||
				'x' => 'x',
 | 
									'x' => 'x',
 | 
				
			||||||
				'_' => '_',
 | 
									'_' => '_',
 | 
				
			||||||
			]);
 | 
								]);
 | 
				
			||||||
@ -166,10 +149,8 @@ class Asset
 | 
				
			|||||||
		foreach ($thumbnails as $thumb)
 | 
							foreach ($thumbnails as $thumb)
 | 
				
			||||||
			$assets[$thumb[0]]['thumbnails'][$thumb[1]] = $thumb[2];
 | 
								$assets[$thumb[0]]['thumbnails'][$thumb[1]] = $thumb[2];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if ($return_format === 'array')
 | 
							if ($return_format == 'array')
 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			return $assets;
 | 
								return $assets;
 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		else
 | 
							else
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			$objects = [];
 | 
								$objects = [];
 | 
				
			||||||
@ -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')
 | 
				
			||||||
@ -262,10 +239,10 @@ class Asset
 | 
				
			|||||||
			INSERT INTO assets
 | 
								INSERT INTO assets
 | 
				
			||||||
			(id_user_uploaded, subdir, filename, title, slug, mimetype, image_width, image_height, date_captured, priority)
 | 
								(id_user_uploaded, subdir, filename, title, slug, mimetype, image_width, image_height, date_captured, priority)
 | 
				
			||||||
			VALUES
 | 
								VALUES
 | 
				
			||||||
			(:id_user_uploaded, :subdir, :filename, :title, :slug, :mimetype,
 | 
								({int:id_user_uploaded}, {string:subdir}, {string:filename}, {string:title}, {string:slug}, {string:mimetype},
 | 
				
			||||||
			 :image_width, :image_height,
 | 
								 {int:image_width}, {int:image_height},
 | 
				
			||||||
			 ' . (!empty($date_captured) ? 'FROM_UNIXTIME(:date_captured)' : 'NULL') . ',
 | 
								 IF({int:date_captured} > 0, FROM_UNIXTIME({int:date_captured}), NULL),
 | 
				
			||||||
			 :priority)',
 | 
								 {int:priority})',
 | 
				
			||||||
			[
 | 
								[
 | 
				
			||||||
				'id_user_uploaded' => isset($id_user) ? $id_user : Registry::get('user')->getUserId(),
 | 
									'id_user_uploaded' => isset($id_user) ? $id_user : Registry::get('user')->getUserId(),
 | 
				
			||||||
				'subdir' => $preferred_subdir,
 | 
									'subdir' => $preferred_subdir,
 | 
				
			||||||
@ -273,9 +250,9 @@ class Asset
 | 
				
			|||||||
				'title' => $title,
 | 
									'title' => $title,
 | 
				
			||||||
				'slug' => $slug,
 | 
									'slug' => $slug,
 | 
				
			||||||
				'mimetype' => $mimetype,
 | 
									'mimetype' => $mimetype,
 | 
				
			||||||
				'image_width' => isset($image_width) ? $image_width : null,
 | 
									'image_width' => isset($image_width) ? $image_width : 'NULL',
 | 
				
			||||||
				'image_height' => isset($image_height) ? $image_height : null,
 | 
									'image_height' => isset($image_height) ? $image_height : 'NULL',
 | 
				
			||||||
				'date_captured' => isset($date_captured) ? $date_captured : null,
 | 
									'date_captured' => isset($date_captured) ? $date_captured : 'NULL',
 | 
				
			||||||
				'priority' => isset($priority) ? (int) $priority : 0,
 | 
									'priority' => isset($priority) ? (int) $priority : 0,
 | 
				
			||||||
			]);
 | 
								]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -285,8 +262,8 @@ class Asset
 | 
				
			|||||||
			return false;
 | 
								return false;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		$data['id_asset'] = $db->insertId();
 | 
							$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;
 | 
				
			||||||
@ -324,7 +291,7 @@ class Asset
 | 
				
			|||||||
		$posts = Registry::get('db')->queryValues('
 | 
							$posts = Registry::get('db')->queryValues('
 | 
				
			||||||
			SELECT id_post
 | 
								SELECT id_post
 | 
				
			||||||
			FROM posts_assets
 | 
								FROM posts_assets
 | 
				
			||||||
			WHERE id_asset = :id_asset',
 | 
								WHERE id_asset = {int:id_asset}',
 | 
				
			||||||
			['id_asset' => $this->id_asset]);
 | 
								['id_asset' => $this->id_asset]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// TODO: fix empty post iterator.
 | 
							// TODO: fix empty post iterator.
 | 
				
			||||||
@ -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 = :subdir,
 | 
					 | 
				
			||||||
					slug = :slug
 | 
					 | 
				
			||||||
				WHERE id_asset = :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();
 | 
				
			||||||
@ -495,18 +418,18 @@ class Asset
 | 
				
			|||||||
		return Registry::get('db')->query('
 | 
							return Registry::get('db')->query('
 | 
				
			||||||
			UPDATE assets
 | 
								UPDATE assets
 | 
				
			||||||
			SET
 | 
								SET
 | 
				
			||||||
				mimetype = :mimetype,
 | 
									mimetype = {string:mimetype},
 | 
				
			||||||
				image_width = :image_width,
 | 
									image_width = {int:image_width},
 | 
				
			||||||
				image_height = :image_height,
 | 
									image_height = {int:image_height},
 | 
				
			||||||
				date_captured = :date_captured,
 | 
									date_captured = {datetime:date_captured},
 | 
				
			||||||
				priority = :priority
 | 
									priority = {int:priority}
 | 
				
			||||||
			WHERE id_asset = :id_asset',
 | 
								WHERE id_asset = {int:id_asset}',
 | 
				
			||||||
			[
 | 
								[
 | 
				
			||||||
				'id_asset' => $this->id_asset,
 | 
									'id_asset' => $this->id_asset,
 | 
				
			||||||
				'mimetype' => $this->mimetype,
 | 
									'mimetype' => $this->mimetype,
 | 
				
			||||||
				'image_width' => isset($this->image_width) ? $this->image_width : null,
 | 
									'image_width' => isset($this->image_width) ? $this->image_width : 'NULL',
 | 
				
			||||||
				'image_height' => isset($this->image_height) ? $this->image_height : null,
 | 
									'image_height' => isset($this->image_height) ? $this->image_height : 'NULL',
 | 
				
			||||||
				'date_captured' => isset($this->date_captured) ? $this->date_captured : null,
 | 
									'date_captured' => isset($this->date_captured) ? $this->date_captured : 'NULL',
 | 
				
			||||||
				'priority' => $this->priority,
 | 
									'priority' => $this->priority,
 | 
				
			||||||
			]);
 | 
								]);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@ -527,8 +450,8 @@ class Asset
 | 
				
			|||||||
			if (!empty($to_remove))
 | 
								if (!empty($to_remove))
 | 
				
			||||||
				$db->query('
 | 
									$db->query('
 | 
				
			||||||
					DELETE FROM assets_meta
 | 
										DELETE FROM assets_meta
 | 
				
			||||||
					WHERE id_asset = :id_asset AND
 | 
										WHERE id_asset = {int:id_asset} AND
 | 
				
			||||||
						variable IN(@variables)',
 | 
											variable IN({array_string:variables})',
 | 
				
			||||||
					[
 | 
										[
 | 
				
			||||||
						'id_asset' => $this->id_asset,
 | 
											'id_asset' => $this->id_asset,
 | 
				
			||||||
						'variables' => array_keys($to_remove),
 | 
											'variables' => array_keys($to_remove),
 | 
				
			||||||
@ -559,40 +482,47 @@ class Asset
 | 
				
			|||||||
	{
 | 
						{
 | 
				
			||||||
		$db = Registry::get('db');
 | 
							$db = Registry::get('db');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Delete any and all thumbnails, if this is an image.
 | 
							if (!unlink(ASSETSDIR . '/' . $this->subdir . '/' . $this->filename))
 | 
				
			||||||
		if ($this->isImage())
 | 
								return false;
 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			$image = $this->getImage();
 | 
					 | 
				
			||||||
			$image->removeAllThumbnails();
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Delete all meta info for this asset.
 | 
					 | 
				
			||||||
		$db->query('
 | 
							$db->query('
 | 
				
			||||||
			DELETE FROM assets_meta
 | 
								DELETE FROM assets_meta
 | 
				
			||||||
			WHERE id_asset = :id_asset',
 | 
								WHERE id_asset = {int:id_asset}',
 | 
				
			||||||
			['id_asset' => $this->id_asset]);
 | 
								[
 | 
				
			||||||
 | 
									'id_asset' => $this->id_asset,
 | 
				
			||||||
 | 
								]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// 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
 | 
				
			||||||
			WHERE id_asset = :id_asset',
 | 
								WHERE id_asset = {int:id_asset}',
 | 
				
			||||||
			['id_asset' => $this->id_asset]);
 | 
								[
 | 
				
			||||||
 | 
									'id_asset' => $this->id_asset,
 | 
				
			||||||
 | 
								]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Delete asset association for these tags
 | 
					 | 
				
			||||||
		$db->query('
 | 
							$db->query('
 | 
				
			||||||
			DELETE FROM assets_tags
 | 
								DELETE FROM assets_tags
 | 
				
			||||||
			WHERE id_asset = :id_asset',
 | 
								WHERE id_asset = {int:id_asset}',
 | 
				
			||||||
			['id_asset' => $this->id_asset]);
 | 
								[
 | 
				
			||||||
 | 
									'id_asset' => $this->id_asset,
 | 
				
			||||||
 | 
								]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		Tag::recount($recount_tags);
 | 
							Tag::recount($recount_tags);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Reset asset ID for tags that use this asset for their thumbnail
 | 
							$return = $db->query('
 | 
				
			||||||
		$rows = $db->queryValues('
 | 
								DELETE FROM assets
 | 
				
			||||||
 | 
								WHERE id_asset = {int:id_asset}',
 | 
				
			||||||
 | 
								[
 | 
				
			||||||
 | 
									'id_asset' => $this->id_asset,
 | 
				
			||||||
 | 
								]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							$rows = $db->query('
 | 
				
			||||||
			SELECT id_tag
 | 
								SELECT id_tag
 | 
				
			||||||
			FROM tags
 | 
								FROM tags
 | 
				
			||||||
			WHERE id_asset_thumb = :id_asset',
 | 
								WHERE id_asset_thumb = {int:id_asset}',
 | 
				
			||||||
			['id_asset' => $this->id_asset]);
 | 
								[
 | 
				
			||||||
 | 
									'id_asset' => $this->id_asset,
 | 
				
			||||||
 | 
								]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (!empty($rows))
 | 
							if (!empty($rows))
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
@ -603,15 +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 = :id_asset',
 | 
					 | 
				
			||||||
			['id_asset' => $this->id_asset]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		return $return;
 | 
							return $return;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -639,7 +560,7 @@ class Asset
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
		Registry::get('db')->query('
 | 
							Registry::get('db')->query('
 | 
				
			||||||
			DELETE FROM assets_tags
 | 
								DELETE FROM assets_tags
 | 
				
			||||||
			WHERE id_asset = :id_asset AND id_tag IN (@id_tags)',
 | 
								WHERE id_asset = {int:id_asset} AND id_tag IN ({array_int:id_tags})',
 | 
				
			||||||
			[
 | 
								[
 | 
				
			||||||
				'id_asset' => $this->id_asset,
 | 
									'id_asset' => $this->id_asset,
 | 
				
			||||||
				'id_tags' => $id_tags,
 | 
									'id_tags' => $id_tags,
 | 
				
			||||||
@ -655,117 +576,91 @@ class Asset
 | 
				
			|||||||
			FROM assets');
 | 
								FROM assets');
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public static function getOffset($offset, $limit, $order, $direction)
 | 
						public function setKeyData($title, $slug, DateTime $date_captured = null, $priority)
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		$order = $order . ($direction == 'up' ? ' ASC' : ' DESC');
 | 
							$params = [
 | 
				
			||||||
 | 
								'id_asset' => $this->id_asset,
 | 
				
			||||||
 | 
								'title' => $title,
 | 
				
			||||||
 | 
								'slug' => $slug,
 | 
				
			||||||
 | 
								'priority' => $priority,
 | 
				
			||||||
 | 
							];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		return Registry::get('db')->queryAssocs('
 | 
							if (isset($date_captured))
 | 
				
			||||||
			SELECT a.id_asset, a.subdir, a.filename,
 | 
								$params['date_captured'] = $date_captured->format('Y-m-d H:i:s');
 | 
				
			||||||
				a.image_width, a.image_height, a.mimetype,
 | 
					 | 
				
			||||||
				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 ' . $order . '
 | 
					 | 
				
			||||||
			LIMIT :offset, :limit',
 | 
					 | 
				
			||||||
			[
 | 
					 | 
				
			||||||
				'offset' => $offset,
 | 
					 | 
				
			||||||
				'limit' => $limit,
 | 
					 | 
				
			||||||
			]);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	public function save()
 | 
					 | 
				
			||||||
	{
 | 
					 | 
				
			||||||
		if (empty($this->id_asset))
 | 
					 | 
				
			||||||
			throw new UnexpectedValueException();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
		return Registry::get('db')->query('
 | 
							return Registry::get('db')->query('
 | 
				
			||||||
			UPDATE assets
 | 
								UPDATE assets
 | 
				
			||||||
			SET subdir = :subdir,
 | 
								SET title = {string:title},
 | 
				
			||||||
				filename = :filename,
 | 
									slug = {string:slug},' . (isset($date_captured) ? '
 | 
				
			||||||
				title = :title,
 | 
									date_captured = {datetime:date_captured},' : '') . '
 | 
				
			||||||
				slug = :slug,
 | 
									priority = {int:priority}
 | 
				
			||||||
				mimetype = :mimetype,
 | 
								WHERE id_asset = {int:id_asset}',
 | 
				
			||||||
				image_width = :image_width,
 | 
								$params);
 | 
				
			||||||
				image_height = :image_height,
 | 
					 | 
				
			||||||
				date_captured = :date_captured,
 | 
					 | 
				
			||||||
				priority = :priority
 | 
					 | 
				
			||||||
			WHERE id_asset = :id_asset',
 | 
					 | 
				
			||||||
			get_object_vars($this));
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	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 = :id_tag';
 | 
					 | 
				
			||||||
			$params['id_tag'] = $tag->id_tag;
 | 
					 | 
				
			||||||
			$where_op = $previous ? '<' : '>';
 | 
					 | 
				
			||||||
			$order_dir = $previous ? 'DESC' : 'ASC';
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		else
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			$where_op = $previous ? '>' : '<';
 | 
					 | 
				
			||||||
			$order_dir = $previous ? 'ASC' : 'DESC';
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Take active filter into account as well
 | 
					 | 
				
			||||||
		if (!empty($activeFilter) && ($user = Member::fromSlug($activeFilter)) !== false)
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			$where[] = 'id_user_uploaded = :id_user_uploaded';
 | 
					 | 
				
			||||||
			$params['id_user_uploaded'] = $user->getUserId();
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Use complete ordering when sorting the set
 | 
					 | 
				
			||||||
		$where[] = '(a.date_captured, a.id_asset) ' . $where_op .
 | 
					 | 
				
			||||||
			' (:date_captured, :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 ' . $order_dir . ', a.id_asset ' . $order_dir . '
 | 
								 . '
 | 
				
			||||||
			LIMIT 1',
 | 
								LIMIT 1',
 | 
				
			||||||
			$params);
 | 
								[
 | 
				
			||||||
 | 
									'id_asset' => $this->id_asset,
 | 
				
			||||||
		if (!$row)
 | 
									'id_tag' => $id_tag,
 | 
				
			||||||
			return false;
 | 
									'date_captured' => $this->date_captured,
 | 
				
			||||||
 | 
								]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if ($row)
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
			$obj = self::byRow($row, 'object');
 | 
								$obj = self::byRow($row, 'object');
 | 
				
			||||||
 | 
								return $obj->getPageUrl() . ($id_tag ? '?in=' . $id_tag : '');
 | 
				
			||||||
		$urlParams = [];
 | 
							}
 | 
				
			||||||
		if (isset($tag))
 | 
							else
 | 
				
			||||||
			$urlParams['in'] = $tag->id_tag;
 | 
								return false;
 | 
				
			||||||
		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;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,50 +1,38 @@
 | 
				
			|||||||
<?php
 | 
					<?php
 | 
				
			||||||
/*****************************************************************************
 | 
					/*****************************************************************************
 | 
				
			||||||
 * AssetIterator.php
 | 
					 * AssetIterator.php
 | 
				
			||||||
 * Contains model class AssetIterator.
 | 
					 * Contains key class AssetIterator.
 | 
				
			||||||
 *
 | 
					 *
 | 
				
			||||||
 * Kabuki CMS (C) 2013-2025, Aaron van Geffen
 | 
					 * Kabuki CMS (C) 2013-2015, Aaron van Geffen
 | 
				
			||||||
 *****************************************************************************/
 | 
					 *****************************************************************************/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AssetIterator implements Iterator
 | 
					class AssetIterator extends Asset
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
	private $direction;
 | 
					 | 
				
			||||||
	private $return_format;
 | 
						private $return_format;
 | 
				
			||||||
	private $rowCount;
 | 
						private $res_assets;
 | 
				
			||||||
 | 
						private $res_meta;
 | 
				
			||||||
 | 
						private $res_thumbs;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private $assets_iterator;
 | 
						protected function __construct($res_assets, $res_meta, $res_thumbs, $return_format)
 | 
				
			||||||
	private $meta_iterator;
 | 
					 | 
				
			||||||
	private $thumbs_iterator;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	protected function __construct(PDOStatement $stmt_assets, PDOStatement $stmt_meta, PDOStatement $stmt_thumbs,
 | 
					 | 
				
			||||||
		$return_format, $direction)
 | 
					 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		$this->direction = $direction;
 | 
							$this->db = Registry::get('db');
 | 
				
			||||||
 | 
							$this->res_assets = $res_assets;
 | 
				
			||||||
 | 
							$this->res_meta = $res_meta;
 | 
				
			||||||
 | 
							$this->res_thumbs = $res_thumbs;
 | 
				
			||||||
		$this->return_format = $return_format;
 | 
							$this->return_format = $return_format;
 | 
				
			||||||
		$this->rowCount = $stmt_assets->rowCount();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		$this->assets_iterator = new CachedPDOIterator($stmt_assets);
 | 
					 | 
				
			||||||
		$this->assets_iterator->rewind();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		$this->meta_iterator = new CachedPDOIterator($stmt_meta);
 | 
					 | 
				
			||||||
		$this->thumbs_iterator = new CachedPDOIterator($stmt_thumbs);
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public static function all()
 | 
						public function next()
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		return self::getByOptions();
 | 
							$row = $this->db->fetch_assoc($this->res_assets);
 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public function current(): mixed
 | 
							// No more rows?
 | 
				
			||||||
	{
 | 
					 | 
				
			||||||
		$row = $this->assets_iterator->current();
 | 
					 | 
				
			||||||
		if (!$row)
 | 
							if (!$row)
 | 
				
			||||||
			return $row;
 | 
								return false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Collect metadata
 | 
							// Looks up metadata.
 | 
				
			||||||
		$row['meta'] = [];
 | 
							$row['meta'] = [];
 | 
				
			||||||
		$this->meta_iterator->rewind();
 | 
							while ($meta = $this->db->fetch_assoc($this->res_meta))
 | 
				
			||||||
		foreach ($this->meta_iterator as $meta)
 | 
					 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			if ($meta['id_asset'] != $row['id_asset'])
 | 
								if ($meta['id_asset'] != $row['id_asset'])
 | 
				
			||||||
				continue;
 | 
									continue;
 | 
				
			||||||
@ -52,23 +40,54 @@ class AssetIterator implements Iterator
 | 
				
			|||||||
			$row['meta'][$meta['variable']] = $meta['value'];
 | 
								$row['meta'][$meta['variable']] = $meta['value'];
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Collect thumbnails
 | 
							// Reset internal pointer for next asset.
 | 
				
			||||||
 | 
							$this->db->data_seek($this->res_meta, 0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Looks up thumbnails.
 | 
				
			||||||
		$row['thumbnails'] = [];
 | 
							$row['thumbnails'] = [];
 | 
				
			||||||
		$this->thumbs_iterator->rewind();
 | 
							while ($thumbs = $this->db->fetch_assoc($this->res_thumbs))
 | 
				
			||||||
		foreach ($this->thumbs_iterator as $thumb)
 | 
					 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			if ($thumb['id_asset'] != $row['id_asset'])
 | 
								if ($thumbs['id_asset'] != $row['id_asset'])
 | 
				
			||||||
				continue;
 | 
									continue;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			$row['thumbnails'][$thumb['selector']] = $thumb['filename'];
 | 
								$row['thumbnails'][$thumbs['selector']] = $thumbs['filename'];
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if ($this->return_format === 'object')
 | 
							// Reset internal pointer for next asset.
 | 
				
			||||||
 | 
							$this->db->data_seek($this->res_thumbs, 0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if ($this->return_format == 'object')
 | 
				
			||||||
			return new Asset($row);
 | 
								return new Asset($row);
 | 
				
			||||||
		else
 | 
							else
 | 
				
			||||||
			return $row;
 | 
								return $row;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public function reset()
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							$this->db->data_seek($this->res_assets, 0);
 | 
				
			||||||
 | 
							$this->db->data_seek($this->res_meta, 0);
 | 
				
			||||||
 | 
							$this->db->data_seek($this->res_thumbs, 0);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public function clean()
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							if (!$this->res_assets)
 | 
				
			||||||
 | 
								return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							$this->db->free_result($this->res_assets);
 | 
				
			||||||
 | 
							$this->res_assets = null;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public function num()
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							return $this->db->num_rows($this->res_assets);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public static function all()
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							return self::getByOptions();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public static function getByOptions(array $options = [], $return_count = false, $return_format = 'object')
 | 
						public static function getByOptions(array $options = [], $return_count = false, $return_format = 'object')
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		$params = [
 | 
							$params = [
 | 
				
			||||||
@ -91,14 +110,9 @@ class AssetIterator implements Iterator
 | 
				
			|||||||
		{
 | 
							{
 | 
				
			||||||
			$params['mime_type'] = $options['mime_type'];
 | 
								$params['mime_type'] = $options['mime_type'];
 | 
				
			||||||
			if (is_array($options['mime_type']))
 | 
								if (is_array($options['mime_type']))
 | 
				
			||||||
				$where[] = 'a.mimetype IN(@mime_type)';
 | 
									$where[] = 'a.mimetype IN({array_string:mime_type})';
 | 
				
			||||||
			else
 | 
								else
 | 
				
			||||||
				$where[] = 'a.mimetype = :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 = :id_user_uploaded';
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		if (isset($options['id_tag']))
 | 
							if (isset($options['id_tag']))
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
@ -106,17 +120,7 @@ class AssetIterator implements Iterator
 | 
				
			|||||||
			$where[] = 'id_asset IN(
 | 
								$where[] = 'id_asset IN(
 | 
				
			||||||
				SELECT l.id_asset
 | 
									SELECT l.id_asset
 | 
				
			||||||
				FROM assets_tags AS l
 | 
									FROM assets_tags AS l
 | 
				
			||||||
				WHERE l.id_tag = :id_tag)';
 | 
									WHERE l.id_tag = {int:id_tag})';
 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		elseif (isset($options['tag']))
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			$params['tag'] = $options['tag'];
 | 
					 | 
				
			||||||
			$where[] = 'id_asset IN(
 | 
					 | 
				
			||||||
				SELECT l.id_asset
 | 
					 | 
				
			||||||
				FROM assets_tags AS l
 | 
					 | 
				
			||||||
				INNER JOIN tags AS t
 | 
					 | 
				
			||||||
				ON l.id_tag = t.id_tag
 | 
					 | 
				
			||||||
				WHERE t.slug = :tag)';
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Make it valid SQL.
 | 
							// Make it valid SQL.
 | 
				
			||||||
@ -132,7 +136,7 @@ class AssetIterator implements Iterator
 | 
				
			|||||||
			FROM assets AS a
 | 
								FROM assets AS a
 | 
				
			||||||
			WHERE ' . $where . '
 | 
								WHERE ' . $where . '
 | 
				
			||||||
			ORDER BY ' . $order . (!empty($params['limit']) ? '
 | 
								ORDER BY ' . $order . (!empty($params['limit']) ? '
 | 
				
			||||||
			LIMIT :offset, :limit' : ''),
 | 
								LIMIT {int:offset}, {int:limit}' : ''),
 | 
				
			||||||
			$params);
 | 
								$params);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Get a resource object for the asset meta.
 | 
							// Get a resource object for the asset meta.
 | 
				
			||||||
@ -152,9 +156,9 @@ class AssetIterator implements Iterator
 | 
				
			|||||||
			SELECT id_asset, filename,
 | 
								SELECT id_asset, filename,
 | 
				
			||||||
				CONCAT(
 | 
									CONCAT(
 | 
				
			||||||
					width,
 | 
										width,
 | 
				
			||||||
					:x,
 | 
										{string:x},
 | 
				
			||||||
					height,
 | 
										height,
 | 
				
			||||||
					IF(mode != :empty1, CONCAT(:_, mode), :empty2)
 | 
										IF(mode != {string:empty}, CONCAT({string:_}, mode), {string:empty})
 | 
				
			||||||
				) AS selector
 | 
									) AS selector
 | 
				
			||||||
			FROM assets_thumbs
 | 
								FROM assets_thumbs
 | 
				
			||||||
			WHERE id_asset IN(
 | 
								WHERE id_asset IN(
 | 
				
			||||||
@ -164,13 +168,12 @@ class AssetIterator implements Iterator
 | 
				
			|||||||
			)
 | 
								)
 | 
				
			||||||
			ORDER BY id_asset',
 | 
								ORDER BY id_asset',
 | 
				
			||||||
			$params + [
 | 
								$params + [
 | 
				
			||||||
				'empty1' => '',
 | 
									'empty' => '',
 | 
				
			||||||
				'empty2' => '',
 | 
					 | 
				
			||||||
				'x' => 'x',
 | 
									'x' => 'x',
 | 
				
			||||||
				'_' => '_',
 | 
									'_' => '_',
 | 
				
			||||||
			]);
 | 
								]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		$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)
 | 
				
			||||||
@ -186,39 +189,4 @@ class AssetIterator implements Iterator
 | 
				
			|||||||
		else
 | 
							else
 | 
				
			||||||
			return $iterator;
 | 
								return $iterator;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					 | 
				
			||||||
	public function key(): mixed
 | 
					 | 
				
			||||||
	{
 | 
					 | 
				
			||||||
		return $this->assets_iterator->key();
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	public function isAscending(): bool
 | 
					 | 
				
			||||||
	{
 | 
					 | 
				
			||||||
		return $this->direction === 'asc';
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	public function isDescending(): bool
 | 
					 | 
				
			||||||
	{
 | 
					 | 
				
			||||||
		return $this->direction === 'desc';
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	public function next(): void
 | 
					 | 
				
			||||||
	{
 | 
					 | 
				
			||||||
		$this->assets_iterator->next();
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	public function num(): int
 | 
					 | 
				
			||||||
	{
 | 
					 | 
				
			||||||
		return $this->rowCount;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	public function rewind(): void
 | 
					 | 
				
			||||||
	{
 | 
					 | 
				
			||||||
		$this->assets_iterator->rewind();
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	public function valid(): bool
 | 
					 | 
				
			||||||
	{
 | 
					 | 
				
			||||||
		return $this->assets_iterator->valid();
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -12,27 +12,48 @@
 | 
				
			|||||||
 */
 | 
					 */
 | 
				
			||||||
class Authentication
 | 
					class Authentication
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
	const DEFAULT_RESET_TIMEOUT = 30;
 | 
						/**
 | 
				
			||||||
 | 
						 * Checks whether a user still exists in the database.
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
						public static function checkExists($id_user)
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							$res = Registry::get('db')->queryValue('
 | 
				
			||||||
 | 
								SELECT id_user
 | 
				
			||||||
 | 
								FROM users
 | 
				
			||||||
 | 
								WHERE id_user = {int:id}',
 | 
				
			||||||
 | 
								[
 | 
				
			||||||
 | 
									'id' => $id_user,
 | 
				
			||||||
 | 
								]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return $res !== null;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/**
 | 
						/**
 | 
				
			||||||
	 * Checks a password for a given username against the database.
 | 
						 * Finds the user id belonging to a certain emailaddress.
 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
	public static function checkPassword($emailaddress, $password)
 | 
						public static function getUserId($emailaddress)
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		// Retrieve password hash for user matching the provided emailaddress.
 | 
							$res = Registry::get('db')->queryValue('
 | 
				
			||||||
		$password_hash = Registry::get('db')->queryValue('
 | 
								SELECT id_user
 | 
				
			||||||
			SELECT password_hash
 | 
					 | 
				
			||||||
			FROM users
 | 
								FROM users
 | 
				
			||||||
			WHERE emailaddress = :emailaddress',
 | 
								WHERE emailaddress = {string:emailaddress}',
 | 
				
			||||||
			[
 | 
								[
 | 
				
			||||||
				'emailaddress' => $emailaddress,
 | 
									'emailaddress' => $emailaddress,
 | 
				
			||||||
			]);
 | 
								]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// If there's no hash, the user likely does not exist.
 | 
							return empty($res) ? false : $res;
 | 
				
			||||||
		if (!$password_hash)
 | 
						}
 | 
				
			||||||
			return false;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
		return password_verify($password, $password_hash);
 | 
						public static function setResetKey($id_user)
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							return Registry::get('db')->query('
 | 
				
			||||||
 | 
								UPDATE users
 | 
				
			||||||
 | 
								SET reset_key = {string:key}
 | 
				
			||||||
 | 
								WHERE id_user = {int:id}',
 | 
				
			||||||
 | 
								[
 | 
				
			||||||
 | 
									'id' => $id_user,
 | 
				
			||||||
 | 
									'key' => self::newActivationKey(),
 | 
				
			||||||
 | 
								]);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public static function checkResetKey($id_user, $reset_key)
 | 
						public static function checkResetKey($id_user, $reset_key)
 | 
				
			||||||
@ -40,7 +61,7 @@ class Authentication
 | 
				
			|||||||
		$key = Registry::get('db')->queryValue('
 | 
							$key = Registry::get('db')->queryValue('
 | 
				
			||||||
			SELECT reset_key
 | 
								SELECT reset_key
 | 
				
			||||||
			FROM users
 | 
								FROM users
 | 
				
			||||||
			WHERE id_user = :id',
 | 
								WHERE id_user = {int:id}',
 | 
				
			||||||
			[
 | 
								[
 | 
				
			||||||
				'id' => $id_user,
 | 
									'id' => $id_user,
 | 
				
			||||||
			]);
 | 
								]);
 | 
				
			||||||
@ -48,55 +69,22 @@ class Authentication
 | 
				
			|||||||
		return $key == $reset_key;
 | 
							return $key == $reset_key;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/**
 | 
					 | 
				
			||||||
	 * Computes a password hash.
 | 
					 | 
				
			||||||
	 */
 | 
					 | 
				
			||||||
	public static function computeHash($password)
 | 
					 | 
				
			||||||
	{
 | 
					 | 
				
			||||||
		$hash = password_hash($password, PASSWORD_DEFAULT);
 | 
					 | 
				
			||||||
		if (!$hash)
 | 
					 | 
				
			||||||
			throw new Exception('Hash creation failed!');
 | 
					 | 
				
			||||||
		return $hash;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	public static function consumeResetKey($id_user)
 | 
					 | 
				
			||||||
	{
 | 
					 | 
				
			||||||
		return Registry::get('db')->query('
 | 
					 | 
				
			||||||
			UPDATE users
 | 
					 | 
				
			||||||
			SET reset_key = NULL,
 | 
					 | 
				
			||||||
				reset_blocked_until = NULL
 | 
					 | 
				
			||||||
			WHERE id_user = :id_user',
 | 
					 | 
				
			||||||
			['id_user' => $id_user]);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	public static function getResetTimeOut($id_user)
 | 
					 | 
				
			||||||
	{
 | 
					 | 
				
			||||||
		$resetTime = Registry::get('db')->queryValue('
 | 
					 | 
				
			||||||
			SELECT reset_blocked_until
 | 
					 | 
				
			||||||
			FROM users
 | 
					 | 
				
			||||||
			WHERE id_user = :id_user',
 | 
					 | 
				
			||||||
			['id_user' => $id_user]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		return max(0, $resetTime - time());
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	/**
 | 
						/**
 | 
				
			||||||
	 * Verifies whether the user is currently logged in.
 | 
						 * Verifies whether the user is currently logged in.
 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
	public static function isLoggedIn()
 | 
						public static function isLoggedIn()
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		if (!isset($_SESSION['user_id']))
 | 
							// Check whether the active session matches the current user's environment.
 | 
				
			||||||
 | 
							if (isset($_SESSION['ip_address'], $_SESSION['user_agent']) && (
 | 
				
			||||||
 | 
								 (isset($_SERVER['REMOTE_ADDR']) && $_SESSION['ip_address'] != $_SERVER['REMOTE_ADDR']) ||
 | 
				
			||||||
 | 
								 (isset($_SERVER['HTTP_USER_AGENT']) && $_SESSION['user_agent'] != $_SERVER['HTTP_USER_AGENT'])))
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								session_destroy();
 | 
				
			||||||
			return false;
 | 
								return false;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		try
 | 
							// A user is logged in if a user id exists in the session and this id is (still) in the database.
 | 
				
			||||||
		{
 | 
							return isset($_SESSION['user_id']) && self::checkExists($_SESSION['user_id']);
 | 
				
			||||||
			$exists = Member::fromId($_SESSION['user_id']);
 | 
					 | 
				
			||||||
			return true;
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		catch (NotFoundException $e)
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			return false;
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/**
 | 
						/**
 | 
				
			||||||
@ -111,17 +99,36 @@ class Authentication
 | 
				
			|||||||
		return $string;
 | 
							return $string;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public static function setResetKey($id_user)
 | 
						/**
 | 
				
			||||||
 | 
						 * Checks a password for a given username against the database.
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
						public static function checkPassword($emailaddress, $password)
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		return Registry::get('db')->query('
 | 
							// Retrieve password hash for user matching the provided emailaddress.
 | 
				
			||||||
			UPDATE users
 | 
							$password_hash = Registry::get('db')->queryValue('
 | 
				
			||||||
			SET reset_key = :key,
 | 
								SELECT password_hash
 | 
				
			||||||
				reset_blocked_until = UNIX_TIMESTAMP() + ' . static::DEFAULT_RESET_TIMEOUT . '
 | 
								FROM users
 | 
				
			||||||
			WHERE id_user = :id',
 | 
								WHERE emailaddress = {string:emailaddress}',
 | 
				
			||||||
			[
 | 
								[
 | 
				
			||||||
				'id' => $id_user,
 | 
									'emailaddress' => $emailaddress,
 | 
				
			||||||
				'key' => self::newActivationKey(),
 | 
					 | 
				
			||||||
			]);
 | 
								]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// If there's no hash, the user likely does not exist.
 | 
				
			||||||
 | 
							if (!$password_hash)
 | 
				
			||||||
 | 
								return false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return password_verify($password, $password_hash);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * Computes a password hash.
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
						public static function computeHash($password)
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							$hash = password_hash($password, PASSWORD_DEFAULT);
 | 
				
			||||||
 | 
							if (!$hash)
 | 
				
			||||||
 | 
								throw new Exception('Hash creation failed!');
 | 
				
			||||||
 | 
							return $hash;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/**
 | 
						/**
 | 
				
			||||||
@ -132,35 +139,13 @@ class Authentication
 | 
				
			|||||||
		return Registry::get('db')->query('
 | 
							return Registry::get('db')->query('
 | 
				
			||||||
			UPDATE users
 | 
								UPDATE users
 | 
				
			||||||
			SET
 | 
								SET
 | 
				
			||||||
				password_hash = :hash,
 | 
									password_hash = {string:hash},
 | 
				
			||||||
				reset_key = :blank
 | 
									reset_key = {string:blank}
 | 
				
			||||||
			WHERE id_user = :id_user',
 | 
								WHERE id_user = {int:id_user}',
 | 
				
			||||||
			[
 | 
								[
 | 
				
			||||||
				'id_user' => $id_user,
 | 
									'id_user' => $id_user,
 | 
				
			||||||
				'hash' => $hash,
 | 
									'hash' => $hash,
 | 
				
			||||||
				'blank' => '',
 | 
									'blank' => '',
 | 
				
			||||||
			]);
 | 
								]);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					 | 
				
			||||||
	public static function updateResetTimeOut($id_user)
 | 
					 | 
				
			||||||
	{
 | 
					 | 
				
			||||||
		$currentResetTimeOut = static::getResetTimeOut($id_user);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// New timeout: between 30 seconds, double the current timeout, and a full day
 | 
					 | 
				
			||||||
		$newResetTimeOut = min(max(static::DEFAULT_RESET_TIMEOUT, $currentResetTimeOut * 2), 60 * 60 * 24);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		$success = Registry::get('db')->query('
 | 
					 | 
				
			||||||
			UPDATE users
 | 
					 | 
				
			||||||
			SET reset_blocked_until = :new_time_out
 | 
					 | 
				
			||||||
			WHERE id_user = :id_user',
 | 
					 | 
				
			||||||
			[
 | 
					 | 
				
			||||||
				'id_user' => $id_user,
 | 
					 | 
				
			||||||
				'new_time_out' => time() + $newResetTimeOut,
 | 
					 | 
				
			||||||
			]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if (!$success)
 | 
					 | 
				
			||||||
			throw new UnexpectedValueException('Could not set password reset timeout!');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		return $newResetTimeOut;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										61
									
								
								models/Cache.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								models/Cache.php
									
									
									
									
									
										Normal 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);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,56 +0,0 @@
 | 
				
			|||||||
<?php
 | 
					 | 
				
			||||||
/*****************************************************************************
 | 
					 | 
				
			||||||
 * CachedPDOIterator.php
 | 
					 | 
				
			||||||
 * Contains model class CachedPDOIterator.
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * Based on https://gist.github.com/hakre/5152090
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * Kabuki CMS (C) 2013-2021, Aaron van Geffen
 | 
					 | 
				
			||||||
 *****************************************************************************/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class CachedPDOIterator extends CachingIterator
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
    private $index;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public function __construct(PDOStatement $statement)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        parent::__construct(new IteratorIterator($statement), self::FULL_CACHE);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public function rewind(): void
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        if ($this->index === null)
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            parent::rewind();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        $this->index = 0;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public function current(): mixed
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        if ($this->offsetExists($this->index))
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            return $this->offsetGet($this->index);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return parent::current();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public function key(): mixed
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        return $this->index;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public function next(): void
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        $this->index++;
 | 
					 | 
				
			||||||
        if (!$this->offsetExists($this->index))
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            parent::next();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public function valid(): bool
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        return $this->offsetExists($this->index) || parent::valid();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,34 +1,43 @@
 | 
				
			|||||||
<?php
 | 
					<?php
 | 
				
			||||||
/*****************************************************************************
 | 
					/*****************************************************************************
 | 
				
			||||||
 * Database.php
 | 
					 * Database.php
 | 
				
			||||||
 * Contains model class Database.
 | 
					 * Contains key class Database.
 | 
				
			||||||
 *
 | 
					 *
 | 
				
			||||||
 * Kabuki CMS (C) 2013-2025, Aaron van Geffen
 | 
					 * Adapted from SMF 2.0's DBA (C) 2011 Simple Machines
 | 
				
			||||||
 | 
					 * Used under BSD 3-clause license.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * Kabuki CMS (C) 2013-2015, Aaron van Geffen
 | 
				
			||||||
 *****************************************************************************/
 | 
					 *****************************************************************************/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * The database model used to communicate with the MySQL server.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
class Database
 | 
					class Database
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
	private $connection;
 | 
						private $connection;
 | 
				
			||||||
	private $query_count = 0;
 | 
						private $query_count = 0;
 | 
				
			||||||
	private $logged_queries = [];
 | 
						private $logged_queries = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public function __construct($host, $user, $password, $name)
 | 
						/**
 | 
				
			||||||
 | 
						 * Initialises a new database connection.
 | 
				
			||||||
 | 
						 * @param server: server to connect to.
 | 
				
			||||||
 | 
						 * @param user: username to use for authentication.
 | 
				
			||||||
 | 
						 * @param password: password to use for authentication.
 | 
				
			||||||
 | 
						 * @param name: database to select.
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
						public function __construct($server, $user, $password, $name)
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		try
 | 
							$this->connection = @mysqli_connect($server, $user, $password, $name);
 | 
				
			||||||
		{
 | 
					
 | 
				
			||||||
			$this->connection = new PDO("mysql:host=$host;dbname=$name;charset=utf8mb4", $user, $password, [
 | 
					 | 
				
			||||||
				PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
 | 
					 | 
				
			||||||
				PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
 | 
					 | 
				
			||||||
				PDO::ATTR_EMULATE_PREPARES => false,
 | 
					 | 
				
			||||||
			]);
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		// Give up if we have a connection error.
 | 
							// Give up if we have a connection error.
 | 
				
			||||||
		catch (PDOException $e)
 | 
							if (mysqli_connect_error())
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			http_response_code(503);
 | 
								header('HTTP/1.1 503 Service Temporarily Unavailable');
 | 
				
			||||||
			echo '<h2>Database Connection Problems</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>Database Connection Problems</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;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							$this->query('SET NAMES {string:utf8}', ['utf8' => 'utf8']);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public function getQueryCount()
 | 
						public function getQueryCount()
 | 
				
			||||||
@ -42,227 +51,305 @@ class Database
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/**
 | 
						/**
 | 
				
			||||||
	 * Fetches a row from a given statement/recordset, using field names as keys.
 | 
						 * Fetches a row from a given recordset, using field names as keys.
 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
	public function fetchAssoc($stmt)
 | 
						public function fetch_assoc($resource)
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		return $stmt->fetch(PDO::FETCH_ASSOC);
 | 
							return mysqli_fetch_assoc($resource);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/**
 | 
						/**
 | 
				
			||||||
	 * Fetches a row from a given statement/recordset, encapsulating into an object.
 | 
						 * Fetches a row from a given recordset, using numeric keys.
 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
	public function fetchObject($stmt, $class)
 | 
						public function fetch_row($resource)
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		return $stmt->fetchObject($class);
 | 
							return mysqli_fetch_row($resource);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/**
 | 
						/**
 | 
				
			||||||
	 * Fetches a row from a given statement/recordset, using numeric keys.
 | 
						 * Destroys a given recordset.
 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
	public function fetchNum($stmt)
 | 
						public function free_result($resource)
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		return $stmt->fetch(PDO::FETCH_NUM);
 | 
							return mysqli_free_result($resource);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public function data_seek($result, $row_num)
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							return mysqli_data_seek($result, $row_num);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/**
 | 
						/**
 | 
				
			||||||
	 * Destroys a given statement/recordset.
 | 
						 * Returns the amount of rows in a given recordset.
 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
	public function free($stmt)
 | 
						public function num_rows($resource)
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		return $stmt->closeCursor();
 | 
							return mysqli_num_rows($resource);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/**
 | 
						/**
 | 
				
			||||||
	 * Returns the amount of rows in a given statement/recordset.
 | 
						 * Returns the amount of fields in a given recordset.
 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
	public function rowCount($stmt)
 | 
						public function num_fields($resource)
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		return $stmt->rowCount();
 | 
							return mysqli_num_fields($resource);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/**
 | 
						/**
 | 
				
			||||||
	 * Returns the amount of fields in a given statement/recordset.
 | 
						 * Escapes a string.
 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
	public function columnCount($stmt)
 | 
						public function escape_string($string)
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		return $stmt->columnCount();
 | 
							return mysqli_real_escape_string($this->connection, $string);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * Unescapes a string.
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
						public function unescape_string($string)
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							return stripslashes($string);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * Returns the last MySQL error.
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
						public function error()
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							return mysqli_error($this->connection);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public function server_info()
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							return mysqli_get_server_info($this->connection);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * Selects a database on a given connection.
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
						public function select_db($database)
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							return mysqli_select_db($database, $this->connection);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * Returns the amount of rows affected by the previous query.
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
						public function affected_rows()
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							return mysqli_affected_rows($this->connection);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/**
 | 
						/**
 | 
				
			||||||
	 * Returns the id of the row created by a previous query.
 | 
						 * Returns the id of the row created by a previous query.
 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
	public function insertId($name = null)
 | 
						public function insert_id()
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		return $this->connection->lastInsertId($name);
 | 
							return mysqli_insert_id($this->connection);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/**
 | 
						/**
 | 
				
			||||||
	 * Start a transaction.
 | 
						 * Do a MySQL transaction.
 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
	public function beginTransaction()
 | 
						public function transaction($operation = 'commit')
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		return $this->connection->beginTransaction();
 | 
							switch ($operation)
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
									case 'begin':
 | 
				
			||||||
 | 
									case 'rollback':
 | 
				
			||||||
 | 
									case 'commit':
 | 
				
			||||||
 | 
										return @mysqli_query($this->connection, strtoupper($operation));
 | 
				
			||||||
 | 
								default:
 | 
				
			||||||
 | 
									return false;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/**
 | 
						/**
 | 
				
			||||||
	 * Rollback changes in a transaction.
 | 
						 * Function used as a callback for the preg_match function that parses variables into database queries.
 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
	public function rollback()
 | 
						private function replacement_callback($matches)
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		return $this->connection->rollBack();
 | 
							list ($values, $connection) = $this->db_callback;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (!isset($matches[2]))
 | 
				
			||||||
 | 
								trigger_error('Invalid value inserted or no type specified.', E_USER_ERROR);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (!isset($values[$matches[2]]))
 | 
				
			||||||
 | 
								trigger_error('The database value you\'re trying to insert does not exist: ' . htmlspecialchars($matches[2]), E_USER_ERROR);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							$replacement = $values[$matches[2]];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							switch ($matches[1])
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								case 'int':
 | 
				
			||||||
 | 
									if ((!is_numeric($replacement) || (string) $replacement !== (string) (int) $replacement) && $replacement !== 'NULL')
 | 
				
			||||||
 | 
										trigger_error('Wrong value type sent to the database for field: ' . $matches[2] . '. Integer expected.', E_USER_ERROR);
 | 
				
			||||||
 | 
									return $replacement !== 'NULL' ? (string) (int) $replacement : 'NULL';
 | 
				
			||||||
 | 
								break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								case 'string':
 | 
				
			||||||
 | 
								case 'text':
 | 
				
			||||||
 | 
									return $replacement !== 'NULL' ? sprintf('\'%1$s\'', mysqli_real_escape_string($connection, $replacement)) : 'NULL';
 | 
				
			||||||
 | 
								break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								case 'array_int':
 | 
				
			||||||
 | 
									if (is_array($replacement))
 | 
				
			||||||
 | 
									{
 | 
				
			||||||
 | 
										if (empty($replacement))
 | 
				
			||||||
 | 
											trigger_error('Database error, given array of integer values is empty.', E_USER_ERROR);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										foreach ($replacement as $key => $value)
 | 
				
			||||||
 | 
										{
 | 
				
			||||||
 | 
											if (!is_numeric($value) || (string) $value !== (string) (int) $value)
 | 
				
			||||||
 | 
												trigger_error('Wrong value type sent to the database for field: ' . $matches[2] . '. Array of integers expected.', E_USER_ERROR);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
											$replacement[$key] = (string) (int) $value;
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/**
 | 
										return implode(', ', $replacement);
 | 
				
			||||||
	 * Commit changes in a transaction.
 | 
					 | 
				
			||||||
	 */
 | 
					 | 
				
			||||||
	public function commit()
 | 
					 | 
				
			||||||
	{
 | 
					 | 
				
			||||||
		return $this->connection->commit();
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	private function expandPlaceholders($db_string, array &$db_values)
 | 
					 | 
				
			||||||
	{
 | 
					 | 
				
			||||||
		foreach ($db_values as $key => &$value)
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			if (str_contains($db_string, ':' . $key))
 | 
					 | 
				
			||||||
			{
 | 
					 | 
				
			||||||
				if (is_array($value))
 | 
					 | 
				
			||||||
				{
 | 
					 | 
				
			||||||
					throw new UnexpectedValueException('Array ' . $key .
 | 
					 | 
				
			||||||
						' is used as a scalar placeholder. Did you mean to use \'@\' instead?');
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				// Prepare date/time values
 | 
					 | 
				
			||||||
				if (is_a($value, 'DateTime'))
 | 
					 | 
				
			||||||
				{
 | 
					 | 
				
			||||||
					$value = $value->format('Y-m-d H:i:s');
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			elseif (str_contains($db_string, '@' . $key))
 | 
					 | 
				
			||||||
			{
 | 
					 | 
				
			||||||
				if (!is_array($value))
 | 
					 | 
				
			||||||
				{
 | 
					 | 
				
			||||||
					throw new UnexpectedValueException('Scalar value ' . $key .
 | 
					 | 
				
			||||||
						' is used as an array placeholder. Did you mean to use \':\' instead?');
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				// Create placeholders for all array elements
 | 
					 | 
				
			||||||
				$placeholders = array_map(fn($num) => ':' . $key . $num, range(0, count($value) - 1));
 | 
					 | 
				
			||||||
				$db_string = str_replace('@' . $key, implode(', ', $placeholders), $db_string);
 | 
					 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
				else
 | 
									else
 | 
				
			||||||
			{
 | 
										trigger_error('Wrong value type sent to the database for field: ' . $matches[2] . '. Array of integers expected.', E_USER_ERROR);
 | 
				
			||||||
				// throw new Exception('Warning: unused key in query: ' . $key);
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
		return $db_string;
 | 
								break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								case 'array_string':
 | 
				
			||||||
 | 
									if (is_array($replacement))
 | 
				
			||||||
 | 
									{
 | 
				
			||||||
 | 
										if (empty($replacement))
 | 
				
			||||||
 | 
											trigger_error('Database error, given array of string values is empty.', E_USER_ERROR);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										foreach ($replacement as $key => $value)
 | 
				
			||||||
 | 
											$replacement[$key] = sprintf('\'%1$s\'', mysqli_real_escape_string($connection, $value));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										return implode(', ', $replacement);
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									else
 | 
				
			||||||
 | 
										trigger_error('Wrong value type sent to the database for field: ' . $matches[2] . '. Array of strings expected.', E_USER_ERROR);
 | 
				
			||||||
 | 
								break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								case 'date':
 | 
				
			||||||
 | 
									if (preg_match('~^(\d{4})-([0-1]?\d)-([0-3]?\d)$~', $replacement, $date_matches) === 1)
 | 
				
			||||||
 | 
										return sprintf('\'%04d-%02d-%02d\'', $date_matches[1], $date_matches[2], $date_matches[3]);
 | 
				
			||||||
 | 
									elseif ($replacement === 'NULL')
 | 
				
			||||||
 | 
										return 'NULL';
 | 
				
			||||||
 | 
									else
 | 
				
			||||||
 | 
										trigger_error('Wrong value type sent to the database for field: ' . $matches[2] . '. Date expected.', E_USER_ERROR);
 | 
				
			||||||
 | 
								break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								case 'datetime':
 | 
				
			||||||
 | 
									if (is_a($replacement, 'DateTime'))
 | 
				
			||||||
 | 
										return $replacement->format('\'Y-m-d H:i:s\'');
 | 
				
			||||||
 | 
									elseif (preg_match('~^(\d{4})-([0-1]?\d)-([0-3]?\d) (\d{2}):(\d{2}):(\d{2})$~', $replacement, $date_matches) === 1)
 | 
				
			||||||
 | 
										return sprintf('\'%04d-%02d-%02d %02d:%02d:%02d\'', $date_matches[1], $date_matches[2], $date_matches[3], $date_matches[4], $date_matches[5], $date_matches[6]);
 | 
				
			||||||
 | 
									elseif ($replacement === 'NULL')
 | 
				
			||||||
 | 
										return 'NULL';
 | 
				
			||||||
 | 
									else
 | 
				
			||||||
 | 
										trigger_error('Wrong value type sent to the database for field: ' . $matches[2] . '. DateTime expected.', E_USER_ERROR);
 | 
				
			||||||
 | 
								break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								case 'float':
 | 
				
			||||||
 | 
									if (!is_numeric($replacement) && $replacement !== 'NULL')
 | 
				
			||||||
 | 
										trigger_error('Wrong value type sent to the database for field: ' . $matches[2] . '. Floating point number expected.', E_USER_ERROR);
 | 
				
			||||||
 | 
									return $replacement !== 'NULL' ? (string) (float) $replacement : 'NULL';
 | 
				
			||||||
 | 
								break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								case 'identifier':
 | 
				
			||||||
 | 
									// Backticks inside identifiers are supported as of MySQL 4.1. We don't need them here.
 | 
				
			||||||
 | 
									return '`' . strtr($replacement, ['`' => '', '.' => '']) . '`';
 | 
				
			||||||
 | 
								break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								case 'raw':
 | 
				
			||||||
 | 
									return $replacement;
 | 
				
			||||||
 | 
								break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								case 'bool':
 | 
				
			||||||
 | 
								case 'boolean':
 | 
				
			||||||
 | 
									// In mysql this is a synonym for tinyint(1)
 | 
				
			||||||
 | 
									return (bool)$replacement ? 1 : 0;
 | 
				
			||||||
 | 
								break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								default:
 | 
				
			||||||
 | 
									trigger_error('Undefined type <b>' . $matches[1] . '</b> used in the database query', E_USER_ERROR);
 | 
				
			||||||
 | 
								break;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/**
 | 
						/**
 | 
				
			||||||
	 * Escapes and quotes a string using values passed, and executes the query.
 | 
						 * Escapes and quotes a string using values passed, and executes the query.
 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
	public function query($db_string, array $db_values = []): PDOStatement
 | 
						public function query($db_string, $db_values = [])
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		// One more query...
 | 
							// One more query....
 | 
				
			||||||
		$this->query_count ++;
 | 
							$this->query_count ++;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Error out if hardcoded strings are detected
 | 
							// Overriding security? This is evil!
 | 
				
			||||||
		if (strpos($db_string, '\'') !== false)
 | 
							$security_override = $db_values === 'security_override' || !empty($db_values['security_override']);
 | 
				
			||||||
			throw new UnexpectedValueException('Hack attempt: illegal character (\') used in query.');
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (defined('DB_LOG_QUERIES') && DB_LOG_QUERIES)
 | 
							// Please, just use new style queries.
 | 
				
			||||||
 | 
							if (strpos($db_string, '\'') !== false && !$security_override)
 | 
				
			||||||
 | 
								trigger_error('Hack attempt!', 'Illegal character (\') used in query.', E_USER_ERROR);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (!$security_override && !empty($db_values))
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								// Set some values for use in the callback function.
 | 
				
			||||||
 | 
								$this->db_callback = [$db_values, $this->connection];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// Insert the values passed to this function.
 | 
				
			||||||
 | 
								$db_string = preg_replace_callback('~{([a-z_]+)(?::([a-zA-Z0-9_-]+))?}~', [&$this, 'replacement_callback'], $db_string);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// Save some memory.
 | 
				
			||||||
 | 
								$this->db_callback = [];
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (defined("DB_LOG_QUERIES") && DB_LOG_QUERIES)
 | 
				
			||||||
			$this->logged_queries[] = $db_string;
 | 
								$this->logged_queries[] = $db_string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		try
 | 
							$return = @mysqli_query($this->connection, $db_string, empty($this->unbuffered) ? MYSQLI_STORE_RESULT : MYSQLI_USE_RESULT);
 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			// Preprocessing/checks: prepare any arrays for binding
 | 
					 | 
				
			||||||
			$db_string = $this->expandPlaceholders($db_string, $db_values);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
			// Prepare query for execution
 | 
							if (!$return)
 | 
				
			||||||
			$statement = $this->connection->prepare($db_string);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			// Bind parameters... the hard way, due to a limit/offset hack.
 | 
					 | 
				
			||||||
			// NB: bindParam binds by reference, hence &$value here.
 | 
					 | 
				
			||||||
			foreach ($db_values as $key => &$value)
 | 
					 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
				// Assumption: both scalar and array values are preprocessed to use named ':' placeholders
 | 
								$clean_sql = implode("\n", array_map('trim', explode("\n", $db_string)));
 | 
				
			||||||
				if (!str_contains($db_string, ':' . $key))
 | 
								trigger_error($this->error() . '<br>' . $clean_sql, E_USER_ERROR);
 | 
				
			||||||
					continue;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				if (!is_array($value))
 | 
					 | 
				
			||||||
				{
 | 
					 | 
				
			||||||
					$statement->bindParam(':' . $key, $value);
 | 
					 | 
				
			||||||
					continue;
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				foreach (array_values($value) as $num => &$element)
 | 
							return $return;
 | 
				
			||||||
				{
 | 
					 | 
				
			||||||
					$statement->bindParam(':' . $key . $num, $element);
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			$statement->execute();
 | 
					 | 
				
			||||||
			return $statement;
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		catch (PDOException $e)
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			ob_start();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			$debug = ob_get_clean();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			throw new Exception($e->getMessage() . "\n" . var_export($e->errorInfo, true) . "\n" . var_export($db_values, true));
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/**
 | 
						/**
 | 
				
			||||||
	 * Executes a query, returning an object of the row it returns.
 | 
						 * Escapes and quotes a string just like db_query, but does not execute the query.
 | 
				
			||||||
 | 
						 * Useful for debugging purposes.
 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
	public function queryObject($class, $db_string, $db_values = [])
 | 
						public function quote($db_string, $db_values = [])
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		$res = $this->query($db_string, $db_values);
 | 
							// Please, just use new style queries.
 | 
				
			||||||
 | 
							if (strpos($db_string, '\'') !== false)
 | 
				
			||||||
 | 
								trigger_error('Hack attempt!', 'Illegal character (\') used in query.', E_USER_ERROR);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (!$res || $this->rowCount($res) === 0)
 | 
							// Save some values for use in the callback function.
 | 
				
			||||||
			return null;
 | 
							$this->db_callback = [$db_values, $this->connection];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		$object = $this->fetchObject($res, $class);
 | 
							// Insert the values passed to this function.
 | 
				
			||||||
		$this->free($res);
 | 
							$db_string = preg_replace_callback('~{([a-z_]+)(?::([a-zA-Z0-9_-]+))?}~', [&$this, 'replacement_callback'], $db_string);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		return $object;
 | 
							// Save some memory.
 | 
				
			||||||
	}
 | 
							$this->db_callback = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/**
 | 
							return $db_string;
 | 
				
			||||||
	 * Executes a query, returning an array of objects of all the rows returns.
 | 
					 | 
				
			||||||
	 */
 | 
					 | 
				
			||||||
	public function queryObjects($class, $db_string, $db_values = [])
 | 
					 | 
				
			||||||
	{
 | 
					 | 
				
			||||||
		$res = $this->query($db_string, $db_values);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if (!$res || $this->rowCount($res) === 0)
 | 
					 | 
				
			||||||
			return [];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		$rows = [];
 | 
					 | 
				
			||||||
		while ($object = $this->fetchObject($res, $class))
 | 
					 | 
				
			||||||
			$rows[] = $object;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		$this->free($res);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		return $rows;
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/**
 | 
						/**
 | 
				
			||||||
	 * Executes a query, returning an array of all the rows it returns.
 | 
						 * Executes a query, returning an array of all the rows it returns.
 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
	public function queryRow($db_string, array $db_values = [])
 | 
						public function queryRow($db_string, $db_values = [])
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		$res = $this->query($db_string, $db_values);
 | 
							$res = $this->query($db_string, $db_values);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if ($this->rowCount($res) === 0)
 | 
							if (!$res || $this->num_rows($res) == 0)
 | 
				
			||||||
			return [];
 | 
								return [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		$row = $this->fetchNum($res);
 | 
							$row = $this->fetch_row($res);
 | 
				
			||||||
		$this->free($res);
 | 
							$this->free_result($res);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		return $row;
 | 
							return $row;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@ -270,18 +357,18 @@ class Database
 | 
				
			|||||||
	/**
 | 
						/**
 | 
				
			||||||
	 * Executes a query, returning an array of all the rows it returns.
 | 
						 * Executes a query, returning an array of all the rows it returns.
 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
	public function queryRows($db_string, array $db_values = [])
 | 
						public function queryRows($db_string, $db_values = [])
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		$res = $this->query($db_string, $db_values);
 | 
							$res = $this->query($db_string, $db_values);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if ($this->rowCount($res) === 0)
 | 
							if (!$res || $this->num_rows($res) == 0)
 | 
				
			||||||
			return [];
 | 
								return [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		$rows = [];
 | 
							$rows = [];
 | 
				
			||||||
		while ($row = $this->fetchNum($res))
 | 
							while ($row = $this->fetch_row($res))
 | 
				
			||||||
			$rows[] = $row;
 | 
								$rows[] = $row;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		$this->free($res);
 | 
							$this->free_result($res);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		return $rows;
 | 
							return $rows;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@ -289,18 +376,18 @@ class Database
 | 
				
			|||||||
	/**
 | 
						/**
 | 
				
			||||||
	 * Executes a query, returning an array of all the rows it returns.
 | 
						 * Executes a query, returning an array of all the rows it returns.
 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
	public function queryPair($db_string, array $db_values = [])
 | 
						public function queryPair($db_string, $db_values = [])
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		$res = $this->query($db_string, $db_values);
 | 
							$res = $this->query($db_string, $db_values);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if ($this->rowCount($res) === 0)
 | 
							if (!$res || $this->num_rows($res) == 0)
 | 
				
			||||||
			return [];
 | 
								return [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		$rows = [];
 | 
							$rows = [];
 | 
				
			||||||
		while ($row = $this->fetchNum($res))
 | 
							while ($row = $this->fetch_row($res))
 | 
				
			||||||
			$rows[$row[0]] = $row[1];
 | 
								$rows[$row[0]] = $row[1];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		$this->free($res);
 | 
							$this->free_result($res);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		return $rows;
 | 
							return $rows;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@ -308,21 +395,21 @@ class Database
 | 
				
			|||||||
	/**
 | 
						/**
 | 
				
			||||||
	 * Executes a query, returning an array of all the rows it returns.
 | 
						 * Executes a query, returning an array of all the rows it returns.
 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
	public function queryPairs($db_string, $db_values = array())
 | 
						public function queryPairs($db_string, $db_values = [])
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		$res = $this->query($db_string, $db_values);
 | 
							$res = $this->query($db_string, $db_values);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (!$res || $this->rowCount($res) === 0)
 | 
							if (!$res || $this->num_rows($res) == 0)
 | 
				
			||||||
			return [];
 | 
								return [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		$rows = [];
 | 
							$rows = [];
 | 
				
			||||||
		while ($row = $this->fetchAssoc($res))
 | 
							while ($row = $this->fetch_assoc($res))
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			$key_value = reset($row);
 | 
								$key_value = reset($row);
 | 
				
			||||||
			$rows[$key_value] = $row;
 | 
								$rows[$key_value] = $row;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		$this->free($res);
 | 
							$this->free_result($res);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		return $rows;
 | 
							return $rows;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@ -330,15 +417,15 @@ class Database
 | 
				
			|||||||
	/**
 | 
						/**
 | 
				
			||||||
	 * Executes a query, returning an associative array of all the rows it returns.
 | 
						 * Executes a query, returning an associative array of all the rows it returns.
 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
	public function queryAssoc($db_string, array $db_values = [])
 | 
						public function queryAssoc($db_string, $db_values = [])
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		$res = $this->query($db_string, $db_values);
 | 
							$res = $this->query($db_string, $db_values);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if ($this->rowCount($res) === 0)
 | 
							if (!$res || $this->num_rows($res) == 0)
 | 
				
			||||||
			return [];
 | 
								return [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		$row = $this->fetchAssoc($res);
 | 
							$row = $this->fetch_assoc($res);
 | 
				
			||||||
		$this->free($res);
 | 
							$this->free_result($res);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		return $row;
 | 
							return $row;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@ -346,18 +433,18 @@ class Database
 | 
				
			|||||||
	/**
 | 
						/**
 | 
				
			||||||
	 * Executes a query, returning an associative array of all the rows it returns.
 | 
						 * Executes a query, returning an associative array of all the rows it returns.
 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
	public function queryAssocs($db_string, array $db_values = [])
 | 
						public function queryAssocs($db_string, $db_values = [], $connection = null)
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		$res = $this->query($db_string, $db_values);
 | 
							$res = $this->query($db_string, $db_values);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if ($this->rowCount($res) === 0)
 | 
							if (!$res || $this->num_rows($res) == 0)
 | 
				
			||||||
			return [];
 | 
								return [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		$rows = [];
 | 
							$rows = [];
 | 
				
			||||||
		while ($row = $this->fetchAssoc($res))
 | 
							while ($row = $this->fetch_assoc($res))
 | 
				
			||||||
			$rows[] = $row;
 | 
								$rows[] = $row;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		$this->free($res);
 | 
							$this->free_result($res);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		return $rows;
 | 
							return $rows;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@ -365,16 +452,16 @@ class Database
 | 
				
			|||||||
	/**
 | 
						/**
 | 
				
			||||||
	 * Executes a query, returning the first value of the first row.
 | 
						 * Executes a query, returning the first value of the first row.
 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
	public function queryValue($db_string, array $db_values = [])
 | 
						public function queryValue($db_string, $db_values = [])
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		$res = $this->query($db_string, $db_values);
 | 
							$res = $this->query($db_string, $db_values);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// If this happens, you're doing it wrong.
 | 
							// If this happens, you're doing it wrong.
 | 
				
			||||||
		if ($this->rowCount($res) === 0)
 | 
							if (!$res || $this->num_rows($res) == 0)
 | 
				
			||||||
			return null;
 | 
								return null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		list($value) = $this->fetchNum($res);
 | 
							list($value) = $this->fetch_row($res);
 | 
				
			||||||
		$this->free($res);
 | 
							$this->free_result($res);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		return $value;
 | 
							return $value;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@ -382,18 +469,18 @@ class Database
 | 
				
			|||||||
	/**
 | 
						/**
 | 
				
			||||||
	 * Executes a query, returning an array of the first value of each row.
 | 
						 * Executes a query, returning an array of the first value of each row.
 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
	public function queryValues($db_string, array $db_values = [])
 | 
						public function queryValues($db_string, $db_values = [])
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		$res = $this->query($db_string, $db_values);
 | 
							$res = $this->query($db_string, $db_values);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if ($this->rowCount($res) === 0)
 | 
							if (!$res || $this->num_rows($res) == 0)
 | 
				
			||||||
			return [];
 | 
								return [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		$rows = [];
 | 
							$rows = [];
 | 
				
			||||||
		while ($row = $this->fetchNum($res))
 | 
							while ($row = $this->fetch_row($res))
 | 
				
			||||||
			$rows[] = $row[0];
 | 
								$rows[] = $row[0];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		$this->free($res);
 | 
							$this->free_result($res);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		return $rows;
 | 
							return $rows;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@ -401,7 +488,7 @@ class Database
 | 
				
			|||||||
	/**
 | 
						/**
 | 
				
			||||||
	 * This function can be used to insert data into the database in a secure way.
 | 
						 * This function can be used to insert data into the database in a secure way.
 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
	public function insert($method, $table, $columns, $data)
 | 
						public function insert($method = 'replace', $table, $columns, $data)
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		// With nothing to insert, simply return.
 | 
							// With nothing to insert, simply return.
 | 
				
			||||||
		if (empty($data))
 | 
							if (empty($data))
 | 
				
			||||||
@ -411,45 +498,35 @@ class Database
 | 
				
			|||||||
		if (!is_array($data[array_rand($data)]))
 | 
							if (!is_array($data[array_rand($data)]))
 | 
				
			||||||
			$data = [$data];
 | 
								$data = [$data];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Determine the method of insertion.
 | 
					 | 
				
			||||||
		$method = $method == 'replace' ? 'REPLACE' : ($method == 'ignore' ? 'INSERT IGNORE' : 'INSERT');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// What columns are we inserting?
 | 
					 | 
				
			||||||
		$columns = array_keys($data[0]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Start building the query.
 | 
					 | 
				
			||||||
		$db_string = $method . ' INTO ' . $table . ' (' . implode(',', $columns) . ') VALUES ';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Create the mold for a single row insert.
 | 
							// Create the mold for a single row insert.
 | 
				
			||||||
		$placeholders = '(' . substr(str_repeat('?, ', count($columns)), 0, -2) . '), ';
 | 
							$insertData = '(';
 | 
				
			||||||
 | 
							foreach ($columns as $columnName => $type)
 | 
				
			||||||
		// Append it for every row we're to insert.
 | 
					 | 
				
			||||||
		$values = [];
 | 
					 | 
				
			||||||
		foreach ($data as $row)
 | 
					 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			$values = array_merge($values, array_values($row));
 | 
								// Are we restricting the length?
 | 
				
			||||||
			$db_string .= $placeholders;
 | 
								if (strpos($type, 'string-') !== false)
 | 
				
			||||||
 | 
									$insertData .= sprintf('SUBSTRING({string:%1$s}, 1, ' . substr($type, 7) . '), ', $columnName);
 | 
				
			||||||
 | 
								else
 | 
				
			||||||
 | 
									$insertData .= sprintf('{%1$s:%2$s}, ', $type, $columnName);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
							$insertData = substr($insertData, 0, -2) . ')';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Get rid of the tailing comma.
 | 
							// Create an array consisting of only the columns.
 | 
				
			||||||
		$db_string = substr($db_string, 0, -2);
 | 
							$indexed_columns = array_keys($columns);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Prepare for your impending demise!
 | 
							// Here's where the variables are injected to the query.
 | 
				
			||||||
		$statement = $this->connection->prepare($db_string);
 | 
							$insertRows = [];
 | 
				
			||||||
 | 
							foreach ($data as $dataRow)
 | 
				
			||||||
 | 
								$insertRows[] = $this->quote($insertData, array_combine($indexed_columns, $dataRow));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Bind parameters... the hard way, due to a limit/offset hack.
 | 
							// Determine the method of insertion.
 | 
				
			||||||
		foreach ($values as $key => $value)
 | 
							$queryTitle = $method == 'replace' ? 'REPLACE' : ($method == 'ignore' ? 'INSERT IGNORE' : 'INSERT');
 | 
				
			||||||
			$statement->bindValue($key + 1, $values[$key]);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Handle errors.
 | 
							// Do the insert.
 | 
				
			||||||
		try
 | 
							return $this->query('
 | 
				
			||||||
		{
 | 
								' . $queryTitle . ' INTO ' . $table . ' (`' . implode('`, `', $indexed_columns) . '`)
 | 
				
			||||||
			$statement->execute();
 | 
								VALUES
 | 
				
			||||||
			return $statement;
 | 
									' . implode(',
 | 
				
			||||||
		}
 | 
									', $insertRows),
 | 
				
			||||||
		catch (PDOException $e)
 | 
								['security_override' => true]);
 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			throw new Exception($e->getMessage() . '<br><br>' . $db_string . '<br><br>' . print_r($values, true));
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -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?
 | 
				
			||||||
@ -44,26 +111,13 @@ class Dispatcher
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public static function errorPage($title, $body)
 | 
					 | 
				
			||||||
	{
 | 
					 | 
				
			||||||
		$page = new MainTemplate($title);
 | 
					 | 
				
			||||||
		$page->adopt(new ErrorPage($title, $body));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if (Registry::get('user')->isAdmin())
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			$page->appendStylesheet(BASEURL . '/css/admin.css');
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		$page->html_main();
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	/**
 | 
						/**
 | 
				
			||||||
	 * Kicks a guest to a login form, redirecting them back to this page upon login.
 | 
						 * Kicks a guest to a login form, redirecting them back to this page upon login.
 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
	public static function kickGuest($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');
 | 
				
			||||||
@ -73,24 +127,38 @@ class Dispatcher
 | 
				
			|||||||
		exit;
 | 
							exit;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private static function trigger400()
 | 
						public static function trigger400()
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		http_response_code(400);
 | 
							header('HTTP/1.1 400 Bad Request');
 | 
				
			||||||
		self::errorPage('Bad request', 'The server does not understand your request.');
 | 
							$page = new MainTemplate('Bad request');
 | 
				
			||||||
 | 
							$page->adopt(new DummyBox('Bad request', '<p>The server does not understand your request.</p>'));
 | 
				
			||||||
 | 
							$page->html_main();
 | 
				
			||||||
		exit;
 | 
							exit;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private static function trigger403()
 | 
						public static function trigger403()
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		http_response_code(403);
 | 
							header('HTTP/1.1 403 Forbidden');
 | 
				
			||||||
		self::errorPage('Forbidden', 'You do not have access to this page.');
 | 
							$page = new MainTemplate('Access denied');
 | 
				
			||||||
 | 
							$page->adopt(new DummyBox('Forbidden', '<p>You do not have access to the page you requested.</p>'));
 | 
				
			||||||
 | 
							$page->html_main();
 | 
				
			||||||
		exit;
 | 
							exit;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private static function trigger404()
 | 
						public static function trigger404()
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		http_response_code(404);
 | 
							header('HTTP/1.1 404 Not Found');
 | 
				
			||||||
		$page = new ViewErrorPage('Page not found!');
 | 
							$page = new MainTemplate('Page not found');
 | 
				
			||||||
		$page->showContent();
 | 
					
 | 
				
			||||||
 | 
							if (Registry::has('user') && Registry::get('user')->isAdmin())
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								$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->addClass('errorpage');
 | 
				
			||||||
 | 
							$page->html_main();
 | 
				
			||||||
 | 
							exit;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -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);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -69,7 +69,7 @@ class Email
 | 
				
			|||||||
		$row = Registry::get('db')->queryAssoc('
 | 
							$row = Registry::get('db')->queryAssoc('
 | 
				
			||||||
			SELECT first_name, surname, emailaddress, reset_key
 | 
								SELECT first_name, surname, emailaddress, reset_key
 | 
				
			||||||
			FROM users
 | 
								FROM users
 | 
				
			||||||
			WHERE id_user = :id_user',
 | 
								WHERE id_user = {int:id_user}',
 | 
				
			||||||
			[
 | 
								[
 | 
				
			||||||
				'id_user' => $id_user,
 | 
									'id_user' => $id_user,
 | 
				
			||||||
			]);
 | 
								]);
 | 
				
			||||||
 | 
				
			|||||||
@ -3,7 +3,7 @@
 | 
				
			|||||||
 * ErrorHandler.php
 | 
					 * ErrorHandler.php
 | 
				
			||||||
 * Contains key class ErrorHandler.
 | 
					 * Contains key class ErrorHandler.
 | 
				
			||||||
 *
 | 
					 *
 | 
				
			||||||
 * Kabuki CMS (C) 2013-2025, Aaron van Geffen
 | 
					 * Kabuki CMS (C) 2013-2016, Aaron van Geffen
 | 
				
			||||||
 *****************************************************************************/
 | 
					 *****************************************************************************/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ErrorHandler
 | 
					class ErrorHandler
 | 
				
			||||||
@ -47,8 +47,10 @@ class ErrorHandler
 | 
				
			|||||||
		// Log the error in the database.
 | 
							// Log the error in the database.
 | 
				
			||||||
		self::logError($error_message, $debug_info, $file, $line);
 | 
							self::logError($error_message, $debug_info, $file, $line);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Display error and exit.
 | 
							// Are we considering this fatal? Then display and exit.
 | 
				
			||||||
		self::display($error_message, $file, $line, $debug_info);
 | 
							// !!! TODO: should we consider warnings fatal?
 | 
				
			||||||
 | 
							if (true) // DEBUG || (!DEBUG && $error_level === E_WARNING || $error_level === E_USER_WARNING))
 | 
				
			||||||
 | 
								self::display($file . ' (' . $line . ')<br>' . $error_message, $debug_info);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// If it wasn't a fatal error, well...
 | 
							// If it wasn't a fatal error, well...
 | 
				
			||||||
		self::$handling_error = false;
 | 
							self::$handling_error = false;
 | 
				
			||||||
@ -61,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;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@ -94,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)
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
					// Only include the class name for objects
 | 
									if (is_array($arg))
 | 
				
			||||||
					if (is_object($arg))
 | 
										$args[$j] = print_r($arg, true);
 | 
				
			||||||
						$args[$j] = get_class($arg) . '{}';
 | 
									elseif (is_object($arg))
 | 
				
			||||||
					// Export everything else -- including arrays
 | 
										$args[$j] = var_dump($arg);
 | 
				
			||||||
					else
 | 
					 | 
				
			||||||
						$args[$j] = var_export($arg, true);
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			$buffer .= '#' . str_pad($i, 3, ' ')
 | 
								$buffer .= '#' . str_pad($i, 3, ' ')
 | 
				
			||||||
@ -116,7 +113,7 @@ class ErrorHandler
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Logs an error into the database.
 | 
						// Logs an error into the database.
 | 
				
			||||||
	public static function logError($error_message = '', $debug_info = '', $file = '', $line = 0)
 | 
						private static function logError($error_message = '', $debug_info = '', $file = '', $line = 0)
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		if (!ErrorLog::log([
 | 
							if (!ErrorLog::log([
 | 
				
			||||||
			'message' => $error_message,
 | 
								'message' => $error_message,
 | 
				
			||||||
@ -128,15 +125,15 @@ class ErrorHandler
 | 
				
			|||||||
			'request_uri' => isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '',
 | 
								'request_uri' => isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '',
 | 
				
			||||||
			]))
 | 
								]))
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			http_response_code(503);
 | 
								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;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		return $error_message;
 | 
							return $error_message;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public static function display($message, $file, $line, $debug_info, $is_sensitive = true)
 | 
						public static function display($message, $debug_info, $is_sensitive = true)
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		$is_admin = Registry::has('user') && Registry::get('user')->isAdmin();
 | 
							$is_admin = Registry::has('user') && Registry::get('user')->isAdmin();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -154,30 +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)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			$debug_info = sprintf("Trigger point:\n%s (L%d)\n\n%s", $file, $line, $debug_info);
 | 
								$page->adopt(new DummyBox('An error occured!', '<p>' . $message . '</p><pre>' . $debug_info . '</pre>'));
 | 
				
			||||||
			$page->adopt(new ErrorPage('An error occurred!', $message, $debug_info));
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
			// 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 ErrorPage('An error occurred!', '<p>' . $message . '</p>'));
 | 
								$page->adopt(new DummyBox('An error occured!', '<p>' . $message . '</p>'));
 | 
				
			||||||
		else
 | 
							else
 | 
				
			||||||
			$page->adopt(new ErrorPage('An error occurred!', 'Our apologies, an error occurred while we were processing your request. Please try again later, or contact us if the problem persists.'));
 | 
								$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();
 | 
				
			||||||
 | 
				
			|||||||
@ -17,14 +17,14 @@ class ErrorLog
 | 
				
			|||||||
			INSERT INTO log_errors
 | 
								INSERT INTO log_errors
 | 
				
			||||||
			(id_user, message, debug_info, file, line, request_uri, time, ip_address)
 | 
								(id_user, message, debug_info, file, line, request_uri, time, ip_address)
 | 
				
			||||||
			VALUES
 | 
								VALUES
 | 
				
			||||||
			(:id_user, :message, :debug_info, :file, :line,
 | 
								({int:id_user}, {string:message}, {string:debug_info}, {string:file}, {int:line},
 | 
				
			||||||
			 :request_uri, CURRENT_TIMESTAMP, :ip_address)',
 | 
								 {string:request_uri}, CURRENT_TIMESTAMP, {string:ip_address})',
 | 
				
			||||||
			$data);
 | 
								$data);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public static function flush()
 | 
						public static function flush()
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		return Registry::get('db')->query('DELETE FROM log_errors');
 | 
							return Registry::get('db')->query('TRUNCATE log_errors');
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public static function getCount()
 | 
						public static function getCount()
 | 
				
			||||||
@ -33,20 +33,4 @@ class ErrorLog
 | 
				
			|||||||
			SELECT COUNT(*)
 | 
								SELECT COUNT(*)
 | 
				
			||||||
			FROM log_errors');
 | 
								FROM log_errors');
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					 | 
				
			||||||
	public static function getOffset($offset, $limit, $order, $direction)
 | 
					 | 
				
			||||||
	{
 | 
					 | 
				
			||||||
		assert(in_array($order, ['id_entry', 'file', 'line', 'time', 'ipaddress', 'id_user']));
 | 
					 | 
				
			||||||
		$order = $order . ($direction === 'up' ? ' ASC' : ' DESC');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		return Registry::get('db')->queryAssocs('
 | 
					 | 
				
			||||||
			SELECT *
 | 
					 | 
				
			||||||
			FROM log_errors
 | 
					 | 
				
			||||||
			ORDER BY ' . $order . '
 | 
					 | 
				
			||||||
			LIMIT :offset, :limit',
 | 
					 | 
				
			||||||
			[
 | 
					 | 
				
			||||||
				'offset' => $offset,
 | 
					 | 
				
			||||||
				'limit' => $limit,
 | 
					 | 
				
			||||||
			]);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										330
									
								
								models/Form.php
									
									
									
									
									
								
							
							
						
						
									
										330
									
								
								models/Form.php
									
									
									
									
									
								
							@ -3,77 +3,30 @@
 | 
				
			|||||||
 * 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
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
	public $request_method;
 | 
						public $request_method;
 | 
				
			||||||
	public $request_url;
 | 
						public $request_url;
 | 
				
			||||||
 | 
						public $content_above;
 | 
				
			||||||
	private $fields = [];
 | 
						public $content_below;
 | 
				
			||||||
	public $before_fields;
 | 
						private $fields;
 | 
				
			||||||
	public $after_fields;
 | 
						private $data;
 | 
				
			||||||
 | 
						private $missing;
 | 
				
			||||||
	private $submit_caption;
 | 
					 | 
				
			||||||
	public $buttons_extra;
 | 
					 | 
				
			||||||
	private $trim_inputs;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	private $data = [];
 | 
					 | 
				
			||||||
	private $missing = [];
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 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)
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		static $optionKeys = [
 | 
							$this->request_method = !empty($options['request_method']) ? $options['request_method'] : 'POST';
 | 
				
			||||||
			'request_method' => 'POST',
 | 
							$this->request_url = !empty($options['request_url']) ? $options['request_url'] : BASEURL;
 | 
				
			||||||
			'request_url' => BASEURL,
 | 
							$this->fields = !empty($options['fields']) ? $options['fields'] : [];
 | 
				
			||||||
 | 
							$this->content_below = !empty($options['content_below']) ? $options['content_below'] : null;
 | 
				
			||||||
			'fields' => [],
 | 
							$this->content_above = !empty($options['content_above']) ? $options['content_above'] : null;
 | 
				
			||||||
			'before_fields' => null,
 | 
					 | 
				
			||||||
			'after_fields' => null,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			'submit_caption' => 'Save information',
 | 
					 | 
				
			||||||
			'buttons_extra' => null,
 | 
					 | 
				
			||||||
			'trim_inputs' => true,
 | 
					 | 
				
			||||||
		];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		foreach ($optionKeys as $optionKey => $default)
 | 
					 | 
				
			||||||
			$this->$optionKey = !empty($options[$optionKey]) ? $options[$optionKey] : $default;
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	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 = [];
 | 
				
			||||||
@ -88,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;
 | 
				
			||||||
 | 
					 | 
				
			||||||
				if ($field['type'] === 'select' && !empty($field['multiple']))
 | 
					 | 
				
			||||||
					$this->data[$field_id] = [];
 | 
					 | 
				
			||||||
				else
 | 
					 | 
				
			||||||
				$this->data[$field_id] = '';
 | 
									$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':
 | 
				
			||||||
@ -133,92 +73,25 @@ class Form
 | 
				
			|||||||
					break;
 | 
										break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				case 'color':
 | 
									case 'color':
 | 
				
			||||||
					$this->validateColor($field_id, $field, $post);
 | 
					 | 
				
			||||||
					break;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				case 'file':
 | 
					 | 
				
			||||||
					// Asset needs to be processed out of POST! This is just a filename.
 | 
					 | 
				
			||||||
					$this->data[$field_id] = isset($post[$field_id]) ? $post[$field_id] : '';
 | 
					 | 
				
			||||||
					break;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				case 'numeric':
 | 
					 | 
				
			||||||
					$this->validateNumeric($field_id, $field, $post);
 | 
					 | 
				
			||||||
					break;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				case 'captcha':
 | 
					 | 
				
			||||||
					if (isset($_POST['g-recaptcha-response']) && !$initalisation)
 | 
					 | 
				
			||||||
						$this->validateCaptcha($field_id);
 | 
					 | 
				
			||||||
					elseif (!$initalisation)
 | 
					 | 
				
			||||||
					{
 | 
					 | 
				
			||||||
						$this->missing[] = $field_id;
 | 
					 | 
				
			||||||
						$this->data[$field_id] = 0;
 | 
					 | 
				
			||||||
					}
 | 
					 | 
				
			||||||
					break;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				case 'text':
 | 
					 | 
				
			||||||
				case 'textarea':
 | 
					 | 
				
			||||||
				default:
 | 
					 | 
				
			||||||
					$this->validateText($field_id, $field, $post);
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	private function validateCaptcha($field_id)
 | 
					 | 
				
			||||||
	{
 | 
					 | 
				
			||||||
		$postdata = http_build_query([
 | 
					 | 
				
			||||||
			'secret' => RECAPTCHA_API_SECRET,
 | 
					 | 
				
			||||||
			'response' => $_POST['g-recaptcha-response'],
 | 
					 | 
				
			||||||
		]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		$opts = [
 | 
					 | 
				
			||||||
			'http' => [
 | 
					 | 
				
			||||||
				'method'  => 'POST',
 | 
					 | 
				
			||||||
				'header'  => 'Content-type: application/x-www-form-urlencoded',
 | 
					 | 
				
			||||||
				'content' => $postdata,
 | 
					 | 
				
			||||||
			]
 | 
					 | 
				
			||||||
		];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		$context  = stream_context_create($opts);
 | 
					 | 
				
			||||||
		$result = file_get_contents('https://www.google.com/recaptcha/api/siteverify', false, $context);
 | 
					 | 
				
			||||||
		$check = json_decode($result);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if ($check->success)
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			$this->data[$field_id] = 1;
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		else
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			$this->data[$field_id] = 0;
 | 
					 | 
				
			||||||
			$this->missing[] = $field_id;
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	private function validateColor($field_id, array $field, array $post)
 | 
					 | 
				
			||||||
	{
 | 
					 | 
				
			||||||
					// Colors are stored as a string of length 3 or 6 (hex)
 | 
										// 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))
 | 
										if (!isset($post[$field_id]) || (strlen($post[$field_id]) != 3 && strlen($post[$field_id]) != 6))
 | 
				
			||||||
					{
 | 
										{
 | 
				
			||||||
						$this->missing[] = $field_id;
 | 
											$this->missing[] = $field_id;
 | 
				
			||||||
						$this->data[$field_id] = '';
 | 
											$this->data[$field_id] = '';
 | 
				
			||||||
 | 
											continue 2;
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
					else
 | 
										else
 | 
				
			||||||
						$this->data[$field_id] = $post[$field_id];
 | 
											$this->data[$field_id] = $post[$field_id];
 | 
				
			||||||
	}
 | 
										break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private function validateNumeric($field_id, array $field, array $post)
 | 
									case 'file':
 | 
				
			||||||
	{
 | 
										// Needs to be verified elsewhere!
 | 
				
			||||||
 | 
										break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									case 'numeric':
 | 
				
			||||||
					$data = isset($post[$field_id]) ? $post[$field_id] : '';
 | 
										$data = isset($post[$field_id]) ? $post[$field_id] : '';
 | 
				
			||||||
 | 
										// Do we need to check bounds?
 | 
				
			||||||
		// Sanity check: does this even look numeric?
 | 
										if (isset($field['min_value']) && is_numeric($data))
 | 
				
			||||||
		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'])
 | 
											if (is_float($field['min_value']) && (float) $data < $field['min_value'])
 | 
				
			||||||
						{
 | 
											{
 | 
				
			||||||
@ -230,10 +103,10 @@ class Form
 | 
				
			|||||||
							$this->missing[] = $field_id;
 | 
												$this->missing[] = $field_id;
 | 
				
			||||||
							$this->data[$field_id] = 0;
 | 
												$this->data[$field_id] = 0;
 | 
				
			||||||
						}
 | 
											}
 | 
				
			||||||
 | 
											else
 | 
				
			||||||
 | 
												$this->data[$field_id] = $data;
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
 | 
										elseif (isset($field['max_value']) && is_numeric($data))
 | 
				
			||||||
		// What about a maximum bound?
 | 
					 | 
				
			||||||
		if (isset($field['max_value']))
 | 
					 | 
				
			||||||
					{
 | 
										{
 | 
				
			||||||
						if (is_float($field['max_value']) && (float) $data > $field['max_value'])
 | 
											if (is_float($field['max_value']) && (float) $data > $field['max_value'])
 | 
				
			||||||
						{
 | 
											{
 | 
				
			||||||
@ -245,113 +118,48 @@ class Form
 | 
				
			|||||||
							$this->missing[] = $field_id;
 | 
												$this->missing[] = $field_id;
 | 
				
			||||||
							$this->data[$field_id] = 0;
 | 
												$this->data[$field_id] = 0;
 | 
				
			||||||
						}
 | 
											}
 | 
				
			||||||
		}
 | 
											else
 | 
				
			||||||
 | 
					 | 
				
			||||||
							$this->data[$field_id] = $data;
 | 
												$this->data[$field_id] = $data;
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
 | 
										// Does it look numeric?
 | 
				
			||||||
	private function validateSelect($field_id, array $field, array $post)
 | 
										elseif (is_numeric($data))
 | 
				
			||||||
					{
 | 
										{
 | 
				
			||||||
		// Skip validation? Dangerous territory!
 | 
											$this->data[$field_id] = $data;
 | 
				
			||||||
		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;
 | 
					 | 
				
			||||||
						}
 | 
					 | 
				
			||||||
					}
 | 
					 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
 | 
										// Let's consider it missing, then.
 | 
				
			||||||
					else
 | 
										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->missing[] = $field_id;
 | 
				
			||||||
			$this->data[$field_id] = '';
 | 
											$this->data[$field_id] = 0;
 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		else
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			throw new UnexpectedValueException('Unexpected field configuration in validateSelect!');
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
 | 
										break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private function validateText($field_id, array $field, array $post)
 | 
									case 'text':
 | 
				
			||||||
	{
 | 
									case 'textarea':
 | 
				
			||||||
 | 
									default:
 | 
				
			||||||
					$this->data[$field_id] = isset($post[$field_id]) ? $post[$field_id] : '';
 | 
										$this->data[$field_id] = isset($post[$field_id]) ? $post[$field_id] : '';
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Trim leading and trailing whitespace?
 | 
						public function setData($data)
 | 
				
			||||||
		if (!empty($field['trim']))
 | 
						{
 | 
				
			||||||
			$this->data[$field_id] = trim($this->data[$field_id]);
 | 
							$this->verify($data);
 | 
				
			||||||
 | 
							$this->missing = [];
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Is there a length limit to enforce?
 | 
						public function getFields()
 | 
				
			||||||
		if (isset($field['maxlength']) && strlen($post[$field_id]) > $field['maxlength']) {
 | 
						{
 | 
				
			||||||
			$post[$field_id] = substr($post[$field_id], 0, $field['maxlength']);
 | 
							return $this->fields;
 | 
				
			||||||
		}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public function getData()
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							return $this->data;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public function getMissing()
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							return $this->missing;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -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
 | 
				
			||||||
@ -15,40 +15,68 @@ class GenericTable
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	private $title;
 | 
						private $title;
 | 
				
			||||||
	private $title_class;
 | 
						private $title_class;
 | 
				
			||||||
 | 
						private $tableIsSortable = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	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)
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		$this->initOrder($options);
 | 
							// Make sure we're actually sorting on something sortable.
 | 
				
			||||||
		$this->initPagination($options);
 | 
							if (!isset($options['sort_order']) || (!empty($options['sort_order']) && empty($options['columns'][$options['sort_order']]['is_sortable'])))
 | 
				
			||||||
 | 
								$options['sort_order'] = '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		$data = $options['get_data']($this->start, $this->items_per_page,
 | 
							// Order in which direction?
 | 
				
			||||||
			$this->sort_order, $this->sort_direction);
 | 
							if (!empty($options['sort_direction']) && !in_array($options['sort_direction'], ['up', 'down']))
 | 
				
			||||||
 | 
								$options['sort_direction'] = 'up';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Make sure we know whether we can actually sort on something.
 | 
				
			||||||
 | 
							$this->tableIsSortable = !empty($options['base_url']);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// How much data do we have?
 | 
				
			||||||
 | 
							$this->recordCount = $options['get_count'](...(!empty($options['get_count_params']) ? $options['get_count_params'] : []));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// How much data do we need to retrieve?
 | 
				
			||||||
 | 
							$this->items_per_page = !empty($options['items_per_page']) ? $options['items_per_page'] : 30;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Figure out where to start.
 | 
				
			||||||
 | 
							$this->start = empty($options['start']) || !is_numeric($options['start']) || $options['start'] < 0 || $options['start'] > $this->recordCount ? 0 : $options['start'];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Figure out where we are on the whole, too.
 | 
				
			||||||
 | 
							$numPages = ceil($this->recordCount / $this->items_per_page);
 | 
				
			||||||
 | 
							$this->currentPage = min(ceil($this->start / $this->items_per_page) + 1, $numPages);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Let's bear a few things in mind...
 | 
				
			||||||
 | 
							$this->base_url = $options['base_url'];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Gather parameters for the data gather function first.
 | 
				
			||||||
 | 
							$parameters = [$this->start, $this->items_per_page, $options['sort_order'], $options['sort_direction']];
 | 
				
			||||||
 | 
							if (!empty($options['get_data_params']) && is_array($options['get_data_params']))
 | 
				
			||||||
 | 
								$parameters = array_merge($parameters, $options['get_data_params']);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Okay, let's fetch the data!
 | 
				
			||||||
 | 
							$data = $options['get_data'](...$parameters);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Extract data into local variables.
 | 
				
			||||||
 | 
							$rawRowData = $data['rows'];
 | 
				
			||||||
 | 
							$this->sort_order = $data['order'];
 | 
				
			||||||
 | 
							$this->sort_direction = $data['direction'];
 | 
				
			||||||
 | 
							unset($data);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Okay, now for the column headers...
 | 
							// Okay, now for the column headers...
 | 
				
			||||||
		$this->generateColumnHeaders($options);
 | 
							$this->generateColumnHeaders($options);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Should we create a page index?
 | 
							// Should we create a page index?
 | 
				
			||||||
		if ($this->recordCount > $this->items_per_page)
 | 
							$needsPageIndex = !empty($this->items_per_page) && $this->recordCount > $this->items_per_page;
 | 
				
			||||||
 | 
							if ($needsPageIndex)
 | 
				
			||||||
			$this->generatePageIndex($options);
 | 
								$this->generatePageIndex($options);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Process the data to be shown into rows.
 | 
							// Process the data to be shown into rows.
 | 
				
			||||||
		if (!empty($data))
 | 
							if (!empty($rawRowData))
 | 
				
			||||||
			$this->processAllRows($data, $options);
 | 
								$this->processAllRows($rawRowData, $options);
 | 
				
			||||||
		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'] ?? '';
 | 
				
			||||||
@ -58,38 +86,6 @@ class GenericTable
 | 
				
			|||||||
		$this->form_below = $options['form_below'] ?? $options['form'] ?? null;
 | 
							$this->form_below = $options['form_below'] ?? $options['form'] ?? null;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private function initOrder($options)
 | 
					 | 
				
			||||||
	{
 | 
					 | 
				
			||||||
		assert(isset($options['default_sort_order']));
 | 
					 | 
				
			||||||
		assert(isset($options['default_sort_direction']));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Validate sort order (column)
 | 
					 | 
				
			||||||
		$this->sort_order = $options['sort_order'];
 | 
					 | 
				
			||||||
		if (empty($this->sort_order) || empty($options['columns'][$this->sort_order]['is_sortable']))
 | 
					 | 
				
			||||||
			$this->sort_order = $options['default_sort_order'];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Validate sort direction
 | 
					 | 
				
			||||||
		$this->sort_direction = $options['sort_direction'];
 | 
					 | 
				
			||||||
		if (empty($this->sort_direction) || !in_array($this->sort_direction, ['up', 'down']))
 | 
					 | 
				
			||||||
			$this->sort_direction = $options['default_sort_direction'];
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	private function initPagination(array $options)
 | 
					 | 
				
			||||||
	{
 | 
					 | 
				
			||||||
		assert(isset($options['base_url']));
 | 
					 | 
				
			||||||
		assert(isset($options['items_per_page']));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		$this->base_url = $options['base_url'];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		$this->recordCount = $options['get_count']();
 | 
					 | 
				
			||||||
		$this->items_per_page = !empty($options['items_per_page']) ? $options['items_per_page'] : 30;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		$this->start = empty($options['start']) || !is_numeric($options['start']) || $options['start'] < 0 || $options['start'] > $this->recordCount ? 0 : $options['start'];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		$numPages = max(1, ceil($this->recordCount / $this->items_per_page));
 | 
					 | 
				
			||||||
		$this->currentPage = min(ceil($this->start / $this->items_per_page) + 1, $numPages);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	private function generateColumnHeaders($options)
 | 
						private function generateColumnHeaders($options)
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		foreach ($options['columns'] as $key => $column)
 | 
							foreach ($options['columns'] as $key => $column)
 | 
				
			||||||
@ -97,14 +93,13 @@ class GenericTable
 | 
				
			|||||||
			if (empty($column['header']))
 | 
								if (empty($column['header']))
 | 
				
			||||||
				continue;
 | 
									continue;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			$isSortable = !empty($column['is_sortable']);
 | 
								$isSortable = $this->tableIsSortable && !empty($column['is_sortable']);
 | 
				
			||||||
			$sortDirection = $key == $this->sort_order && $this->sort_direction === 'up' ? 'down' : 'up';
 | 
								$sortDirection = $key == $this->sort_order && $this->sort_direction === 'up' ? 'down' : 'up';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			$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->getHeaderLink($this->start, $key, $sortDirection) : null,
 | 
									'href' => $isSortable ? $this->getLink($this->start, $key, $sortDirection) : null,
 | 
				
			||||||
				'label' => $column['header'],
 | 
									'label' => $column['header'],
 | 
				
			||||||
				'scope' => 'col',
 | 
									'scope' => 'col',
 | 
				
			||||||
				'sort_mode' => $key == $this->sort_order ? $this->sort_direction : null,
 | 
									'sort_mode' => $key == $this->sort_order ? $this->sort_direction : null,
 | 
				
			||||||
@ -121,7 +116,7 @@ class GenericTable
 | 
				
			|||||||
			'base_url' => $this->base_url,
 | 
								'base_url' => $this->base_url,
 | 
				
			||||||
			'index_class' => $options['index_class'] ?? '',
 | 
								'index_class' => $options['index_class'] ?? '',
 | 
				
			||||||
			'items_per_page' => $this->items_per_page,
 | 
								'items_per_page' => $this->items_per_page,
 | 
				
			||||||
			'linkBuilder' => [$this, 'getHeaderLink'],
 | 
								'linkBuilder' => [$this, 'getLink'],
 | 
				
			||||||
			'recordCount' => $this->recordCount,
 | 
								'recordCount' => $this->recordCount,
 | 
				
			||||||
			'sort_direction' => $this->sort_direction,
 | 
								'sort_direction' => $this->sort_direction,
 | 
				
			||||||
			'sort_order' => $this->sort_order,
 | 
								'sort_order' => $this->sort_order,
 | 
				
			||||||
@ -129,7 +124,7 @@ class GenericTable
 | 
				
			|||||||
		]);
 | 
							]);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public function getHeaderLink($start = null, $order = null, $dir = null)
 | 
						public function getLink($start = null, $order = null, $dir = null)
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		if ($start === null)
 | 
							if ($start === null)
 | 
				
			||||||
			$start = $this->start;
 | 
								$start = $this->start;
 | 
				
			||||||
@ -166,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;
 | 
				
			||||||
@ -191,21 +181,14 @@ class GenericTable
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
			foreach ($options['columns'] as $column)
 | 
								foreach ($options['columns'] as $column)
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				// Process formatting
 | 
									// Process data for this particular cell.
 | 
				
			||||||
				if (isset($column['format']) && is_callable($column['format']))
 | 
									if (isset($column['parse']))
 | 
				
			||||||
					$value = $column['format']($row);
 | 
										$value = self::processCell($column['parse'], $row);
 | 
				
			||||||
				elseif (isset($column['format']))
 | 
					 | 
				
			||||||
					$value = self::processFormatting($column['format'], $row);
 | 
					 | 
				
			||||||
				else
 | 
									else
 | 
				
			||||||
					$value = $row[$column['value']];
 | 
										$value = $row[$column['value']];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				// Turn value into a link?
 | 
					 | 
				
			||||||
				if (!empty($column['link']))
 | 
					 | 
				
			||||||
					$value = $this->processLink($column['link'], $value, $row);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				// Append the cell to the row.
 | 
									// Append the cell to the row.
 | 
				
			||||||
				$newRow['cells'][] = [
 | 
									$newRow['cells'][] = [
 | 
				
			||||||
					'class' => $column['cell_class'] ?? '',
 | 
					 | 
				
			||||||
					'value' => $value,
 | 
										'value' => $value,
 | 
				
			||||||
				];
 | 
									];
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
@ -215,47 +198,66 @@ class GenericTable
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private function processFormatting($options, $rowData)
 | 
						private function processCell($options, $rowData)
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		if ($options['type'] === 'timestamp')
 | 
							if (!isset($options['type']))
 | 
				
			||||||
 | 
								$options['type'] = 'value';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Parse the basic value first.
 | 
				
			||||||
 | 
							switch ($options['type'])
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			if (empty($options['pattern']) || $options['pattern'] === 'long')
 | 
								// Basic option: simply take a use a particular data property.
 | 
				
			||||||
				$pattern = 'Y-m-d H:i';
 | 
								case 'value':
 | 
				
			||||||
			elseif ($options['pattern'] === 'short')
 | 
									$value = htmlspecialchars($rowData[$options['data']]);
 | 
				
			||||||
				$pattern = 'Y-m-d';
 | 
									break;
 | 
				
			||||||
			else
 | 
					 | 
				
			||||||
				$pattern = $options['pattern'];
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
			assert(array_key_exists($options['value'], $rowData));
 | 
								// Processing via a lambda function.
 | 
				
			||||||
			if (isset($rowData[$options['value']]) && !is_numeric($rowData[$options['value']]))
 | 
								case 'function':
 | 
				
			||||||
				$timestamp = strtotime($rowData[$options['value']]);
 | 
									$value = $options['data']($rowData);
 | 
				
			||||||
			else
 | 
									break;
 | 
				
			||||||
				$timestamp = (int) $rowData[$options['value']];
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if (isset($options['if_null']) && $timestamp == 0)
 | 
								// Using sprintf to fill out a particular pattern.
 | 
				
			||||||
				$value = $options['if_null'];
 | 
								case 'sprintf':
 | 
				
			||||||
			else
 | 
									$parameters = [$options['data']['pattern']];
 | 
				
			||||||
				$value = date($pattern, $timestamp);
 | 
									foreach ($options['data']['arguments'] as $identifier)
 | 
				
			||||||
 | 
										$parameters[] = $rowData[$identifier];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			return $value;
 | 
									$value = sprintf(...$parameters);
 | 
				
			||||||
		}
 | 
									break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// Timestamps get custom treatment.
 | 
				
			||||||
 | 
								case 'timestamp':
 | 
				
			||||||
 | 
									if (empty($options['data']['pattern']) || $options['data']['pattern'] === 'long')
 | 
				
			||||||
 | 
										$pattern = '%F %H:%M';
 | 
				
			||||||
 | 
									elseif ($options['data']['pattern'] === 'short')
 | 
				
			||||||
 | 
										$pattern = '%F';
 | 
				
			||||||
				else
 | 
									else
 | 
				
			||||||
			throw ValueError('Unexpected formatter type: ' . $options['type']);
 | 
										$pattern = $options['data']['pattern'];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									if (!is_numeric($rowData[$options['data']['timestamp']]))
 | 
				
			||||||
 | 
										$timestamp = strtotime($rowData[$options['data']['timestamp']]);
 | 
				
			||||||
 | 
									else
 | 
				
			||||||
 | 
										$timestamp = (int) $rowData[$options['data']['timestamp']];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									if (isset($options['data']['if_null']) && $timestamp == 0)
 | 
				
			||||||
 | 
										$value = $options['data']['if_null'];
 | 
				
			||||||
 | 
									else
 | 
				
			||||||
 | 
										$value = strftime($pattern, $timestamp);
 | 
				
			||||||
 | 
									break;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private function processLink($template, $value, array $rowData)
 | 
							// Generate a link, if requested.
 | 
				
			||||||
	{
 | 
							if (!empty($options['link']))
 | 
				
			||||||
		$href = $this->rowReplacements($template, $rowData);
 | 
					 | 
				
			||||||
		return '<a href="' . $href . '">' . $value . '</a>';
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	private function rowReplacements($template, array $rowData)
 | 
					 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
 | 
								// First, generate the replacement variables.
 | 
				
			||||||
			$keys = array_keys($rowData);
 | 
								$keys = array_keys($rowData);
 | 
				
			||||||
			$values = array_values($rowData);
 | 
								$values = array_values($rowData);
 | 
				
			||||||
			foreach ($keys as $keyKey => $keyValue)
 | 
								foreach ($keys as $keyKey => $keyValue)
 | 
				
			||||||
				$keys[$keyKey] = '{' . strtoupper($keyValue) . '}';
 | 
									$keys[$keyKey] = '{' . strtoupper($keyValue) . '}';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		return str_replace($keys, $values, $template);
 | 
								$value = '<a href="' . str_replace($keys, $values, $options['link']) . '">' . $value . '</a>';
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return $value;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -21,7 +21,7 @@ class Guest extends User
 | 
				
			|||||||
		$this->is_guest = true;
 | 
							$this->is_guest = true;
 | 
				
			||||||
		$this->is_admin = false;
 | 
							$this->is_admin = false;
 | 
				
			||||||
		$this->first_name = 'Guest';
 | 
							$this->first_name = 'Guest';
 | 
				
			||||||
		$this->surname = '';
 | 
							$this->last_name = '';
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public function updateAccessTime()
 | 
						public function updateAccessTime()
 | 
				
			||||||
 | 
				
			|||||||
@ -12,11 +12,17 @@ class Image extends Asset
 | 
				
			|||||||
	const TYPE_LANDSCAPE = 2;
 | 
						const TYPE_LANDSCAPE = 2;
 | 
				
			||||||
	const TYPE_PORTRAIT = 4;
 | 
						const TYPE_PORTRAIT = 4;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						protected function __construct(array $data)
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							foreach ($data as $attribute => $value)
 | 
				
			||||||
 | 
								$this->$attribute = $value;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public static function fromId($id_asset, $return_format = 'object')
 | 
						public static function fromId($id_asset, $return_format = 'object')
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		$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;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@ -28,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
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
@ -61,33 +67,14 @@ class Image extends Asset
 | 
				
			|||||||
		return EXIF::fromFile($this->getPath());
 | 
							return EXIF::fromFile($this->getPath());
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public function getImageUrls($width = null, $height = null)
 | 
						public function getPath()
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		$image_urls = [];
 | 
							return ASSETSDIR . '/' . $this->subdir . '/' . $this->filename;
 | 
				
			||||||
		if (isset($width) || isset($height))
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			$thumbnail = new Thumbnail($this);
 | 
					 | 
				
			||||||
			$image_urls[1] = $this->getThumbnailUrl($width, $height, false);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			// Can we afford to generate double-density thumbnails as well?
 | 
					 | 
				
			||||||
			if ((!isset($width) || $this->image_width >= $width * 2) &&
 | 
					 | 
				
			||||||
				(!isset($height) || $this->image_height >= $height * 2))
 | 
					 | 
				
			||||||
				$image_urls[2] = $this->getThumbnailUrl($width * 2, $height * 2, false);
 | 
					 | 
				
			||||||
			else
 | 
					 | 
				
			||||||
				$image_urls[2] = $this->getThumbnailUrl($this->image_width, $this->image_height, true);
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		else
 | 
					 | 
				
			||||||
			$image_urls[1] = $this->getUrl();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		return $image_urls;
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public function getInlineImage($width = null, $height = null, $className = 'inline-image')
 | 
						public function getUrl()
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		$image_urls = $this->getImageUrls($width, $height);
 | 
							return ASSETSURL . '/' . $this->subdir . '/' . $this->filename;
 | 
				
			||||||
 | 
					 | 
				
			||||||
		return '<img class="' . $className . '" src="' . $image_urls[1] . '" alt=""' .
 | 
					 | 
				
			||||||
			(isset($image_urls[2]) ? ' srcset="' . $image_urls[2] . ' 2x"' : '') . '>';
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/**
 | 
						/**
 | 
				
			||||||
@ -139,16 +126,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;
 | 
				
			||||||
@ -165,7 +142,7 @@ class Image extends Asset
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
		return Registry::get('db')->query('
 | 
							return Registry::get('db')->query('
 | 
				
			||||||
			DELETE FROM assets_thumbs
 | 
								DELETE FROM assets_thumbs
 | 
				
			||||||
			WHERE id_asset = :id_asset',
 | 
								WHERE id_asset = {int:id_asset}',
 | 
				
			||||||
			['id_asset' => $this->id_asset]);
 | 
								['id_asset' => $this->id_asset]);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -183,9 +160,9 @@ class Image extends Asset
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
		return Registry::get('db')->query('
 | 
							return Registry::get('db')->query('
 | 
				
			||||||
			DELETE FROM assets_thumbs
 | 
								DELETE FROM assets_thumbs
 | 
				
			||||||
			WHERE id_asset = :id_asset AND
 | 
								WHERE id_asset = {int:id_asset} AND
 | 
				
			||||||
				width = :width AND
 | 
									width = {int:width} AND
 | 
				
			||||||
				height = :height',
 | 
									height = {int:height}',
 | 
				
			||||||
			[
 | 
								[
 | 
				
			||||||
				'height' => $height,
 | 
									'height' => $height,
 | 
				
			||||||
				'id_asset' => $this->id_asset,
 | 
									'id_asset' => $this->id_asset,
 | 
				
			||||||
 | 
				
			|||||||
@ -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'];
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -8,7 +8,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
class Member extends User
 | 
					class Member extends User
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
	private function __construct($data = [])
 | 
						private function __construct($data)
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		foreach ($data as $key => $value)
 | 
							foreach ($data as $key => $value)
 | 
				
			||||||
			$this->$key = $value;
 | 
								$this->$key = $value;
 | 
				
			||||||
@ -18,21 +18,12 @@ class Member extends User
 | 
				
			|||||||
		$this->is_admin = $this->is_admin == 1;
 | 
							$this->is_admin = $this->is_admin == 1;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public static function fromEmailAddress($email_address)
 | 
					 | 
				
			||||||
	{
 | 
					 | 
				
			||||||
		return Registry::get('db')->queryObject(static::class, '
 | 
					 | 
				
			||||||
			SELECT *
 | 
					 | 
				
			||||||
			FROM users
 | 
					 | 
				
			||||||
			WHERE emailaddress = :email_address',
 | 
					 | 
				
			||||||
			['email_address' => $email_address]);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	public static function fromId($id_user)
 | 
						public static function fromId($id_user)
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		$row = Registry::get('db')->queryAssoc('
 | 
							$row = Registry::get('db')->queryAssoc('
 | 
				
			||||||
			SELECT *
 | 
								SELECT *
 | 
				
			||||||
			FROM users
 | 
								FROM users
 | 
				
			||||||
			WHERE id_user = :id_user',
 | 
								WHERE id_user = {int:id_user}',
 | 
				
			||||||
			[
 | 
								[
 | 
				
			||||||
				'id_user' => $id_user,
 | 
									'id_user' => $id_user,
 | 
				
			||||||
			]);
 | 
								]);
 | 
				
			||||||
@ -49,7 +40,7 @@ class Member extends User
 | 
				
			|||||||
		$row = Registry::get('db')->queryAssoc('
 | 
							$row = Registry::get('db')->queryAssoc('
 | 
				
			||||||
			SELECT *
 | 
								SELECT *
 | 
				
			||||||
			FROM users
 | 
								FROM users
 | 
				
			||||||
			WHERE slug = :slug',
 | 
								WHERE slug = {string:slug}',
 | 
				
			||||||
			[
 | 
								[
 | 
				
			||||||
				'slug' => $slug,
 | 
									'slug' => $slug,
 | 
				
			||||||
			]);
 | 
								]);
 | 
				
			||||||
@ -77,7 +68,6 @@ class Member extends User
 | 
				
			|||||||
			'creation_time' => time(),
 | 
								'creation_time' => time(),
 | 
				
			||||||
			'ip_address' => isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '',
 | 
								'ip_address' => isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '',
 | 
				
			||||||
			'is_admin' => empty($data['is_admin']) ? 0 : 1,
 | 
								'is_admin' => empty($data['is_admin']) ? 0 : 1,
 | 
				
			||||||
			'reset_key' => '',
 | 
					 | 
				
			||||||
		];
 | 
							];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if ($error)
 | 
							if ($error)
 | 
				
			||||||
@ -93,13 +83,12 @@ class Member extends User
 | 
				
			|||||||
			'creation_time' => 'int',
 | 
								'creation_time' => 'int',
 | 
				
			||||||
			'ip_address' => 'string-45',
 | 
								'ip_address' => 'string-45',
 | 
				
			||||||
			'is_admin' => 'int',
 | 
								'is_admin' => 'int',
 | 
				
			||||||
			'reset_key' => 'string-16'
 | 
					 | 
				
			||||||
		], $new_user, ['id_user']);
 | 
							], $new_user, ['id_user']);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (!$bool)
 | 
							if (!$bool)
 | 
				
			||||||
			return false;
 | 
								return false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		$new_user['id_user'] = $db->insertId();
 | 
							$new_user['id_user'] = $db->insert_id();
 | 
				
			||||||
		$member = new Member($new_user);
 | 
							$member = new Member($new_user);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		return $member;
 | 
							return $member;
 | 
				
			||||||
@ -121,19 +110,16 @@ 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
 | 
				
			||||||
				first_name = :first_name,
 | 
									first_name = {string:first_name},
 | 
				
			||||||
				surname = :surname,
 | 
									surname = {string:surname},
 | 
				
			||||||
				slug = :slug,
 | 
									slug = {string:slug},
 | 
				
			||||||
				emailaddress = :emailaddress,
 | 
									emailaddress = {string:emailaddress},
 | 
				
			||||||
				password_hash = :password_hash,
 | 
									password_hash = {string:password_hash},
 | 
				
			||||||
				is_admin = :is_admin
 | 
									is_admin = {int:is_admin}
 | 
				
			||||||
			WHERE id_user = :id_user',
 | 
								WHERE id_user = {int:id_user}',
 | 
				
			||||||
			get_object_vars($this));
 | 
								get_object_vars($this));
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -145,7 +131,7 @@ class Member extends User
 | 
				
			|||||||
	{
 | 
						{
 | 
				
			||||||
		return Registry::get('db')->query('
 | 
							return Registry::get('db')->query('
 | 
				
			||||||
			DELETE FROM users
 | 
								DELETE FROM users
 | 
				
			||||||
			WHERE id_user = :id_user',
 | 
								WHERE id_user = {int:id_user}',
 | 
				
			||||||
			['id_user' => $this->id_user]);
 | 
								['id_user' => $this->id_user]);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -160,7 +146,7 @@ class Member extends User
 | 
				
			|||||||
		$res = Registry::get('db')->queryValue('
 | 
							$res = Registry::get('db')->queryValue('
 | 
				
			||||||
			SELECT id_user
 | 
								SELECT id_user
 | 
				
			||||||
			FROM users
 | 
								FROM users
 | 
				
			||||||
			WHERE emailaddress = :emailaddress',
 | 
								WHERE emailaddress = {string:emailaddress}',
 | 
				
			||||||
			[
 | 
								[
 | 
				
			||||||
				'emailaddress' => $emailaddress,
 | 
									'emailaddress' => $emailaddress,
 | 
				
			||||||
			]);
 | 
								]);
 | 
				
			||||||
@ -176,9 +162,9 @@ class Member extends User
 | 
				
			|||||||
		return Registry::get('db')->query('
 | 
							return Registry::get('db')->query('
 | 
				
			||||||
			UPDATE users
 | 
								UPDATE users
 | 
				
			||||||
			SET
 | 
								SET
 | 
				
			||||||
				last_action_time = :now,
 | 
									last_action_time = {int:now},
 | 
				
			||||||
				ip_address = :ip
 | 
									ip_address = {string:ip}
 | 
				
			||||||
			WHERE id_user = :id',
 | 
								WHERE id_user = {int:id}',
 | 
				
			||||||
			[
 | 
								[
 | 
				
			||||||
				'now' => time(),
 | 
									'now' => time(),
 | 
				
			||||||
				'id' => $this->id_user,
 | 
									'id' => $this->id_user,
 | 
				
			||||||
@ -198,36 +184,9 @@ class Member extends User
 | 
				
			|||||||
			FROM users');
 | 
								FROM users');
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public static function getOffset($offset, $limit, $order, $direction)
 | 
					 | 
				
			||||||
	{
 | 
					 | 
				
			||||||
		assert(in_array($order, ['id_user', 'surname', 'first_name', 'slug', 'emailaddress', 'last_action_time', 'ip_address', 'is_admin']));
 | 
					 | 
				
			||||||
		$order = $order . ($direction === 'up' ? ' ASC' : ' DESC');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		return Registry::get('db')->queryAssocs('
 | 
					 | 
				
			||||||
			SELECT *
 | 
					 | 
				
			||||||
			FROM users
 | 
					 | 
				
			||||||
			ORDER BY ' . $order . '
 | 
					 | 
				
			||||||
			LIMIT :offset, :limit',
 | 
					 | 
				
			||||||
			[
 | 
					 | 
				
			||||||
				'offset' => $offset,
 | 
					 | 
				
			||||||
				'limit' => $limit,
 | 
					 | 
				
			||||||
			]);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	public function getProps()
 | 
						public function getProps()
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		// 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, :blank, surname) AS full_name
 | 
					 | 
				
			||||||
			FROM users
 | 
					 | 
				
			||||||
			ORDER BY first_name, surname',
 | 
					 | 
				
			||||||
			[
 | 
					 | 
				
			||||||
				'blank' => ' ',
 | 
					 | 
				
			||||||
			]);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -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;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -63,9 +63,9 @@ class PageIndex
 | 
				
			|||||||
			   lower     current/cont. pgs.       center            upper
 | 
								   lower     current/cont. pgs.       center            upper
 | 
				
			||||||
		*/
 | 
							*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		$this->num_pages = max(1, ceil($this->recordCount / $this->items_per_page));
 | 
							$this->num_pages = ceil($this->recordCount / $this->items_per_page);
 | 
				
			||||||
		$this->current_page = min(ceil($this->start / $this->items_per_page) + 1, $this->num_pages);
 | 
							$this->current_page = min(ceil($this->start / $this->items_per_page) + 1, $this->num_pages);
 | 
				
			||||||
		if ($this->num_pages <= 1)
 | 
							if ($this->num_pages == 0)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			$this->needsPageIndex = false;
 | 
								$this->needsPageIndex = false;
 | 
				
			||||||
			return;
 | 
								return;
 | 
				
			||||||
@ -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;
 | 
				
			||||||
 | 
				
			|||||||
@ -8,255 +8,163 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
class PhotoMosaic
 | 
					class PhotoMosaic
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
	private bool $descending;
 | 
						private $queue = [];
 | 
				
			||||||
	private AssetIterator $iterator;
 | 
					 | 
				
			||||||
	private array $layouts;
 | 
					 | 
				
			||||||
	private int $processedImages = 0;
 | 
					 | 
				
			||||||
	private array $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();
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private function availableLayouts()
 | 
						public function __destruct()
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		static $layouts = [
 | 
							$this->iterator->clean();
 | 
				
			||||||
			// Single panorama
 | 
					 | 
				
			||||||
			'panorama' => [Image::TYPE_PANORAMA],
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			// A whopping six landscapes?
 | 
					 | 
				
			||||||
			'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)
 | 
						public static function getRecentPhotos()
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		return $a->diff($b)->days;
 | 
							return new self(AssetIterator::getByOptions([
 | 
				
			||||||
 | 
								'tag' => 'photo',
 | 
				
			||||||
 | 
								'order' => 'date_captured',
 | 
				
			||||||
 | 
								'direction' => 'desc',
 | 
				
			||||||
 | 
								'limit' => 15, // worst case: 3 rows * (portrait + 4 thumbs)
 | 
				
			||||||
 | 
							]));
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private function fetchImage($desired_type = self::IMAGE_MASK_ALL, ?DateTime $refDate = null)
 | 
						private static function matchTypeMask(Image $image, $type_mask)
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							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 = 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;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Check whatever's up next!
 | 
							// Check whatever's next up!
 | 
				
			||||||
		// NB: not is not a `foreach` so as to not reset the iterator implicitly
 | 
							while (($asset = $this->iterator->next()) && ($image = $asset->getImage()))
 | 
				
			||||||
		while ($this->iterator->valid())
 | 
					 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			$asset = $this->iterator->current();
 | 
					 | 
				
			||||||
			$image = $asset->getImage();
 | 
					 | 
				
			||||||
			$this->iterator->next();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			// 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)
 | 
					 | 
				
			||||||
	{
 | 
					 | 
				
			||||||
		$refDate = null;
 | 
					 | 
				
			||||||
		$prevImage = true;
 | 
					 | 
				
			||||||
		$images = [];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		for ($i = 0; $i < $num || !$prevImage; $i++)
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			$image = $this->fetchImage($spec, $refDate);
 | 
					 | 
				
			||||||
			if ($image !== false)
 | 
					 | 
				
			||||||
			{
 | 
					 | 
				
			||||||
				$images[] = $image;
 | 
					 | 
				
			||||||
				$refDate = $image->getDateCaptured();
 | 
					 | 
				
			||||||
				$prevImage = $image;
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		return $images;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	public function getRow()
 | 
					 | 
				
			||||||
	{
 | 
					 | 
				
			||||||
		$requiredImages = array_map('count', $this->layouts);
 | 
					 | 
				
			||||||
		$currentImages = $this->fetchImages(self::NUM_BATCH_PHOTOS);
 | 
					 | 
				
			||||||
		$selectedLayout = null;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if (empty($currentImages))
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			// Ensure we have no images left in the iterator before giving up
 | 
					 | 
				
			||||||
			assert($this->processedImages === $this->iterator->num());
 | 
					 | 
				
			||||||
			return false;
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Assign fitness score for each layout
 | 
					 | 
				
			||||||
		$fitnessScores = $this->getScoresByLayout($currentImages);
 | 
					 | 
				
			||||||
		$scoresByLayout = array_map(fn($el) => $el[0], $fitnessScores);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Select the best-fitting layout
 | 
					 | 
				
			||||||
		$bestLayouts = array_keys($scoresByLayout, max($scoresByLayout));
 | 
					 | 
				
			||||||
		$bestLayout = $bestLayouts[0];
 | 
					 | 
				
			||||||
		$layoutImages = $fitnessScores[$bestLayout][1];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Push any unused back into the queue
 | 
					 | 
				
			||||||
		if (count($layoutImages) < count($currentImages))
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			$diff = array_udiff($currentImages, $layoutImages, function($a, $b) {
 | 
					 | 
				
			||||||
				return $a->getId() <=> $b->getId();
 | 
					 | 
				
			||||||
			});
 | 
					 | 
				
			||||||
			array_map([$this, 'pushToQueue'], $diff);
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Finally, allow tweaking image order through display priority
 | 
					 | 
				
			||||||
		usort($layoutImages, [$this, 'orderPhotosByPriority']);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// 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]))
 | 
					 | 
				
			||||||
				$score += 1;
 | 
					 | 
				
			||||||
			else
 | 
					 | 
				
			||||||
				$score -= 10;
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		return $score;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	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)
 | 
						private function pushToQueue(Image $image)
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		$this->queue[] = $image;
 | 
							$this->queue[] = $image;
 | 
				
			||||||
		$this->orderQueueByDate();
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						private static function orderPhotos(Image $a, Image $b)
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							// Show images of highest priority first.
 | 
				
			||||||
 | 
							$priority_diff = $a->getPriority() - $b->getPriority();
 | 
				
			||||||
 | 
							if ($priority_diff !== 0)
 | 
				
			||||||
 | 
								return -$priority_diff;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 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()
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							// Fetch the first image...
 | 
				
			||||||
 | 
							$image = $this->fetchImage();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// No image at all?
 | 
				
			||||||
 | 
							if (!$image)
 | 
				
			||||||
 | 
								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;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Sort photos by priority and date captured.
 | 
				
			||||||
 | 
							usort($photos, 'self::orderPhotos');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Three portraits?
 | 
				
			||||||
 | 
							if ($num_portrait === 3)
 | 
				
			||||||
 | 
								return [$photos, 'portraits'];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// At least one portrait?
 | 
				
			||||||
 | 
							if ($num_portrait >= 1)
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								// Grab two more landscapes, so we can put a total of four tiles on the side.
 | 
				
			||||||
 | 
								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);
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// 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'];
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// One landscape at least, hopefully?
 | 
				
			||||||
 | 
							if ($num_landscape >= 1)
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								if (count($photos) === 3)
 | 
				
			||||||
 | 
								{
 | 
				
			||||||
 | 
									// 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
 | 
				
			||||||
 | 
									return [$photos, 'single'];
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// A boring set it is, then.
 | 
				
			||||||
 | 
							return [$photos, 'row'];
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -24,7 +24,7 @@ class Registry
 | 
				
			|||||||
	public static function get($key)
 | 
						public static function get($key)
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		if (!isset(self::$storage[$key]))
 | 
							if (!isset(self::$storage[$key]))
 | 
				
			||||||
			throw new Exception('Key does not exist in Registry: ' . $key);
 | 
								trigger_error('Key does not exist in Registry: ' . $key, E_USER_ERROR);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		return self::$storage[$key];
 | 
							return self::$storage[$key];
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@ -32,7 +32,7 @@ class Registry
 | 
				
			|||||||
	public static function remove($key)
 | 
						public static function remove($key)
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		if (!isset(self::$storage[$key]))
 | 
							if (!isset(self::$storage[$key]))
 | 
				
			||||||
			throw new Exception('Key does not exist in Registry: ' . $key);
 | 
								trigger_error('Key does not exist in Registry: ' . $key, E_USER_ERROR);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		unset(self::$storage[$key]);
 | 
							unset(self::$storage[$key]);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
				
			|||||||
@ -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();
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -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']))
 | 
					 | 
				
			||||||
			throw new Exception('Call to getSessionToken without a session token being set!');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		return $_SESSION['session_token'];
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	public static function getSessionTokenKey()
 | 
					 | 
				
			||||||
	{
 | 
					 | 
				
			||||||
		if (empty($_SESSION['session_token_key']))
 | 
					 | 
				
			||||||
			throw new Exception('Call to getSessionTokenKey without a session token key being set!');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		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'];
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -21,7 +21,7 @@ class Setting
 | 
				
			|||||||
			REPLACE INTO settings
 | 
								REPLACE INTO settings
 | 
				
			||||||
			(id_user, variable, value, time_set)
 | 
								(id_user, variable, value, time_set)
 | 
				
			||||||
			VALUES
 | 
								VALUES
 | 
				
			||||||
			(:id_user, :key, :value, CURRENT_TIMESTAMP())',
 | 
								({int:id_user}, {string:key}, {string:value}, CURRENT_TIMESTAMP())',
 | 
				
			||||||
			[
 | 
								[
 | 
				
			||||||
				'id_user' => $id_user,
 | 
									'id_user' => $id_user,
 | 
				
			||||||
				'key' => $key,
 | 
									'key' => $key,
 | 
				
			||||||
@ -45,7 +45,7 @@ class Setting
 | 
				
			|||||||
		$value = Registry::get('db')->queryValue('
 | 
							$value = Registry::get('db')->queryValue('
 | 
				
			||||||
			SELECT value
 | 
								SELECT value
 | 
				
			||||||
			FROM settings
 | 
								FROM settings
 | 
				
			||||||
			WHERE id_user = :id_user AND variable = :key',
 | 
								WHERE id_user = {int:id_user} AND variable = {string:key}',
 | 
				
			||||||
			[
 | 
								[
 | 
				
			||||||
				'id_user' => $id_user,
 | 
									'id_user' => $id_user,
 | 
				
			||||||
				'key' => $key,
 | 
									'key' => $key,
 | 
				
			||||||
@ -63,30 +63,11 @@ class Setting
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	public static function remove($key, $id_user = null)
 | 
						public static function remove($key, $id_user = null)
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		// User setting or global setting?
 | 
					 | 
				
			||||||
		if ($id_user === null)
 | 
					 | 
				
			||||||
			$id_user = Registry::get('user')->getUserId();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		$pairs = Registry::get('db')->queryPair('
 | 
					 | 
				
			||||||
			SELECT variable, value
 | 
					 | 
				
			||||||
			FROM settings
 | 
					 | 
				
			||||||
			WHERE id_user = :id_user',
 | 
					 | 
				
			||||||
			[
 | 
					 | 
				
			||||||
				'id_user' => $id_user,
 | 
					 | 
				
			||||||
			]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		return $pairs;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	public static function remove($key, $id_user = 0)
 | 
					 | 
				
			||||||
	{
 | 
					 | 
				
			||||||
		// User setting or global setting?
 | 
					 | 
				
			||||||
		if ($id_user === null)
 | 
					 | 
				
			||||||
		$id_user = Registry::get('user')->getUserId();
 | 
							$id_user = Registry::get('user')->getUserId();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (Registry::get('db')->query('
 | 
							if (Registry::get('db')->query('
 | 
				
			||||||
			DELETE FROM settings
 | 
								DELETE FROM settings
 | 
				
			||||||
			WHERE id_user = :id_user AND variable = :key',
 | 
								WHERE id_user = {int:id_user} AND variable = {string:key}',
 | 
				
			||||||
			[
 | 
								[
 | 
				
			||||||
				'id_user' => $id_user,
 | 
									'id_user' => $id_user,
 | 
				
			||||||
				'key' => $key,
 | 
									'key' => $key,
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										229
									
								
								models/Tag.php
									
									
									
									
									
								
							
							
						
						
									
										229
									
								
								models/Tag.php
									
									
									
									
									
								
							@ -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;
 | 
				
			||||||
@ -24,11 +23,6 @@ class Tag
 | 
				
			|||||||
			$this->$attribute = $value;
 | 
								$this->$attribute = $value;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public function __toString()
 | 
					 | 
				
			||||||
	{
 | 
					 | 
				
			||||||
		return $this->tag;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	public static function fromId($id_tag, $return_format = 'object')
 | 
						public static function fromId($id_tag, $return_format = 'object')
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		$db = Registry::get('db');
 | 
							$db = Registry::get('db');
 | 
				
			||||||
@ -36,7 +30,7 @@ class Tag
 | 
				
			|||||||
		$row = $db->queryAssoc('
 | 
							$row = $db->queryAssoc('
 | 
				
			||||||
			SELECT *
 | 
								SELECT *
 | 
				
			||||||
			FROM tags
 | 
								FROM tags
 | 
				
			||||||
			WHERE id_tag = :id_tag',
 | 
								WHERE id_tag = {int:id_tag}',
 | 
				
			||||||
			[
 | 
								[
 | 
				
			||||||
				'id_tag' => $id_tag,
 | 
									'id_tag' => $id_tag,
 | 
				
			||||||
			]);
 | 
								]);
 | 
				
			||||||
@ -45,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')
 | 
				
			||||||
@ -55,7 +49,7 @@ class Tag
 | 
				
			|||||||
		$row = $db->queryAssoc('
 | 
							$row = $db->queryAssoc('
 | 
				
			||||||
			SELECT *
 | 
								SELECT *
 | 
				
			||||||
			FROM tags
 | 
								FROM tags
 | 
				
			||||||
			WHERE slug = :slug',
 | 
								WHERE slug = {string:slug}',
 | 
				
			||||||
			[
 | 
								[
 | 
				
			||||||
				'slug' => $slug,
 | 
									'slug' => $slug,
 | 
				
			||||||
			]);
 | 
								]);
 | 
				
			||||||
@ -64,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')
 | 
				
			||||||
@ -73,7 +67,7 @@ class Tag
 | 
				
			|||||||
			SELECT *
 | 
								SELECT *
 | 
				
			||||||
			FROM tags
 | 
								FROM tags
 | 
				
			||||||
			ORDER BY ' . ($limit > 0 ? 'count
 | 
								ORDER BY ' . ($limit > 0 ? 'count
 | 
				
			||||||
			LIMIT :limit' : 'tag'),
 | 
								LIMIT {int:limit}' : 'tag'),
 | 
				
			||||||
			[
 | 
								[
 | 
				
			||||||
				'limit' => $limit,
 | 
									'limit' => $limit,
 | 
				
			||||||
			]);
 | 
								]);
 | 
				
			||||||
@ -90,7 +84,7 @@ class Tag
 | 
				
			|||||||
			});
 | 
								});
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if ($return_format === 'object')
 | 
							if ($return_format == 'object')
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			$return = [];
 | 
								$return = [];
 | 
				
			||||||
			foreach ($rows as $row)
 | 
								foreach ($rows as $row)
 | 
				
			||||||
@ -101,33 +95,14 @@ 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 = :id_user_owner
 | 
					 | 
				
			||||||
			ORDER BY tag',
 | 
					 | 
				
			||||||
			[
 | 
					 | 
				
			||||||
				'id_user_owner' => $id_user_owner,
 | 
					 | 
				
			||||||
			]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		$objects = [];
 | 
					 | 
				
			||||||
		while ($row = $db->fetchAssoc($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('
 | 
				
			||||||
			SELECT *
 | 
								SELECT *
 | 
				
			||||||
			FROM tags
 | 
								FROM tags
 | 
				
			||||||
			WHERE id_parent = :id_parent AND kind = :kind
 | 
								WHERE id_parent = {int:id_parent} AND kind = {string:kind}
 | 
				
			||||||
			ORDER BY tag ASC
 | 
								ORDER BY tag ASC
 | 
				
			||||||
			LIMIT :offset, :limit',
 | 
								LIMIT {int:offset}, {int:limit}',
 | 
				
			||||||
			[
 | 
								[
 | 
				
			||||||
				'id_parent' => $id_parent,
 | 
									'id_parent' => $id_parent,
 | 
				
			||||||
				'kind' => 'Album',
 | 
									'kind' => 'Album',
 | 
				
			||||||
@ -135,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)
 | 
				
			||||||
@ -146,29 +121,14 @@ 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 = :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('
 | 
				
			||||||
			SELECT *
 | 
								SELECT *
 | 
				
			||||||
			FROM tags
 | 
								FROM tags
 | 
				
			||||||
			WHERE id_parent = :id_parent AND kind = :kind
 | 
								WHERE id_parent = {int:id_parent} AND kind = {string:kind}
 | 
				
			||||||
			ORDER BY tag ASC
 | 
								ORDER BY tag ASC
 | 
				
			||||||
			LIMIT :offset, :limit',
 | 
								LIMIT {int:offset}, {int:limit}',
 | 
				
			||||||
			[
 | 
								[
 | 
				
			||||||
				'id_parent' => $id_parent,
 | 
									'id_parent' => $id_parent,
 | 
				
			||||||
				'kind' => 'Person',
 | 
									'kind' => 'Person',
 | 
				
			||||||
@ -176,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)
 | 
				
			||||||
@ -195,7 +155,7 @@ class Tag
 | 
				
			|||||||
			WHERE id_tag IN(
 | 
								WHERE id_tag IN(
 | 
				
			||||||
				SELECT id_tag
 | 
									SELECT id_tag
 | 
				
			||||||
				FROM assets_tags
 | 
									FROM assets_tags
 | 
				
			||||||
				WHERE id_asset = :id_asset
 | 
									WHERE id_asset = {int:id_asset}
 | 
				
			||||||
			)
 | 
								)
 | 
				
			||||||
			ORDER BY count DESC',
 | 
								ORDER BY count DESC',
 | 
				
			||||||
			[
 | 
								[
 | 
				
			||||||
@ -206,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)
 | 
				
			||||||
@ -225,7 +185,7 @@ class Tag
 | 
				
			|||||||
			WHERE id_tag IN(
 | 
								WHERE id_tag IN(
 | 
				
			||||||
				SELECT id_tag
 | 
									SELECT id_tag
 | 
				
			||||||
				FROM posts_tags
 | 
									FROM posts_tags
 | 
				
			||||||
				WHERE id_post = :id_post
 | 
									WHERE id_post = {int:id_post}
 | 
				
			||||||
			)
 | 
								)
 | 
				
			||||||
			ORDER BY count DESC',
 | 
								ORDER BY count DESC',
 | 
				
			||||||
			[
 | 
								[
 | 
				
			||||||
@ -236,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)
 | 
				
			||||||
@ -255,7 +215,7 @@ class Tag
 | 
				
			|||||||
				FROM `assets_tags` AS at
 | 
									FROM `assets_tags` AS at
 | 
				
			||||||
				WHERE at.id_tag = t.id_tag
 | 
									WHERE at.id_tag = t.id_tag
 | 
				
			||||||
			)' . (!empty($id_tags) ? '
 | 
								)' . (!empty($id_tags) ? '
 | 
				
			||||||
			WHERE t.id_tag IN(@id_tags)' : ''),
 | 
								WHERE t.id_tag IN({array_int:id_tags})' : ''),
 | 
				
			||||||
			['id_tags' => $id_tags]);
 | 
								['id_tags' => $id_tags]);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -276,15 +236,15 @@ class Tag
 | 
				
			|||||||
			INSERT IGNORE INTO tags
 | 
								INSERT IGNORE INTO tags
 | 
				
			||||||
			(id_parent, tag, slug, kind, description, count)
 | 
								(id_parent, tag, slug, kind, description, count)
 | 
				
			||||||
			VALUES
 | 
								VALUES
 | 
				
			||||||
			(:id_parent, :tag, :slug, :kind, :description, :count)
 | 
								({int:id_parent}, {string:tag}, {string:slug}, {string:kind}, {string:description}, {int:count})
 | 
				
			||||||
			ON DUPLICATE KEY UPDATE count = count + 1',
 | 
								ON DUPLICATE KEY UPDATE count = count + 1',
 | 
				
			||||||
			$data);
 | 
								$data);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (!$res)
 | 
							if (!$res)
 | 
				
			||||||
			throw new Exception('Could not create the requested tag.');
 | 
								trigger_error('Could not create the requested tag.', E_USER_ERROR);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		$data['id_tag'] = $db->insertId();
 | 
							$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()
 | 
				
			||||||
@ -297,15 +257,13 @@ class Tag
 | 
				
			|||||||
		return Registry::get('db')->query('
 | 
							return Registry::get('db')->query('
 | 
				
			||||||
			UPDATE tags
 | 
								UPDATE tags
 | 
				
			||||||
			SET
 | 
								SET
 | 
				
			||||||
				id_parent = :id_parent,
 | 
									id_parent = {int:id_parent},
 | 
				
			||||||
				id_asset_thumb = :id_asset_thumb,' . (isset($this->id_user_owner) ? '
 | 
									id_asset_thumb = {int:id_asset_thumb},
 | 
				
			||||||
				id_user_owner = :id_user_owner,' : '') . '
 | 
									tag = {string:tag},
 | 
				
			||||||
				tag = :tag,
 | 
									slug = {string:slug},
 | 
				
			||||||
				slug = :slug,
 | 
									description = {string:description},
 | 
				
			||||||
				kind = :kind,
 | 
									count = {int:count}
 | 
				
			||||||
				description = :description,
 | 
								WHERE id_tag = {int:id_tag}',
 | 
				
			||||||
				count = :count
 | 
					 | 
				
			||||||
			WHERE id_tag = :id_tag',
 | 
					 | 
				
			||||||
			get_object_vars($this));
 | 
								get_object_vars($this));
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -313,10 +271,9 @@ class Tag
 | 
				
			|||||||
	{
 | 
						{
 | 
				
			||||||
		$db = Registry::get('db');
 | 
							$db = Registry::get('db');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Unlink any tagged assets
 | 
					 | 
				
			||||||
		$res = $db->query('
 | 
							$res = $db->query('
 | 
				
			||||||
			DELETE FROM assets_tags
 | 
								DELETE FROM assets_tags
 | 
				
			||||||
			WHERE id_tag = :id_tag',
 | 
								WHERE id_tag = {int:id_tag}',
 | 
				
			||||||
			[
 | 
								[
 | 
				
			||||||
				'id_tag' => $this->id_tag,
 | 
									'id_tag' => $this->id_tag,
 | 
				
			||||||
			]);
 | 
								]);
 | 
				
			||||||
@ -324,10 +281,9 @@ class Tag
 | 
				
			|||||||
		if (!$res)
 | 
							if (!$res)
 | 
				
			||||||
			return false;
 | 
								return false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Delete the actual tag
 | 
					 | 
				
			||||||
		return $db->query('
 | 
							return $db->query('
 | 
				
			||||||
			DELETE FROM tags
 | 
								DELETE FROM tags
 | 
				
			||||||
			WHERE id_tag = :id_tag',
 | 
								WHERE id_tag = {int:id_tag}',
 | 
				
			||||||
			[
 | 
								[
 | 
				
			||||||
				'id_tag' => $this->id_tag,
 | 
									'id_tag' => $this->id_tag,
 | 
				
			||||||
			]);
 | 
								]);
 | 
				
			||||||
@ -336,20 +292,27 @@ 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 = :id_tag',
 | 
								WHERE id_tag = {int:id_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 = :new_id
 | 
								SET id_asset_thumb = {int:new_id}
 | 
				
			||||||
			WHERE id_tag = :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,
 | 
				
			||||||
			]);
 | 
								]);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@ -362,7 +325,7 @@ class Tag
 | 
				
			|||||||
		return Registry::get('db')->queryPair('
 | 
							return Registry::get('db')->queryPair('
 | 
				
			||||||
			SELECT id_tag, tag
 | 
								SELECT id_tag, tag
 | 
				
			||||||
			FROM tags
 | 
								FROM tags
 | 
				
			||||||
			WHERE LOWER(tag) LIKE :tokens
 | 
								WHERE LOWER(tag) LIKE {string:tokens}
 | 
				
			||||||
			ORDER BY tag ASC',
 | 
								ORDER BY tag ASC',
 | 
				
			||||||
			['tokens' => '%' . strtolower(implode('%', $tokens)) . '%']);
 | 
								['tokens' => '%' . strtolower(implode('%', $tokens)) . '%']);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@ -375,8 +338,8 @@ class Tag
 | 
				
			|||||||
		return Registry::get('db')->queryPairs('
 | 
							return Registry::get('db')->queryPairs('
 | 
				
			||||||
			SELECT id_tag, tag, slug
 | 
								SELECT id_tag, tag, slug
 | 
				
			||||||
			FROM tags
 | 
								FROM tags
 | 
				
			||||||
			WHERE LOWER(tag) LIKE :tokens AND
 | 
								WHERE LOWER(tag) LIKE {string:tokens} AND
 | 
				
			||||||
				kind = :person
 | 
									kind = {string:person}
 | 
				
			||||||
			ORDER BY tag ASC',
 | 
								ORDER BY tag ASC',
 | 
				
			||||||
			[
 | 
								[
 | 
				
			||||||
				'tokens' => '%' . strtolower(implode('%', $tokens)) . '%',
 | 
									'tokens' => '%' . strtolower(implode('%', $tokens)) . '%',
 | 
				
			||||||
@ -392,7 +355,7 @@ class Tag
 | 
				
			|||||||
		return Registry::get('db')->queryPair('
 | 
							return Registry::get('db')->queryPair('
 | 
				
			||||||
			SELECT id_tag, tag
 | 
								SELECT id_tag, tag
 | 
				
			||||||
			FROM tags
 | 
								FROM tags
 | 
				
			||||||
			WHERE tag = :tag',
 | 
								WHERE tag = {string:tag}',
 | 
				
			||||||
			['tag' => $tag]);
 | 
								['tag' => $tag]);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -404,7 +367,7 @@ class Tag
 | 
				
			|||||||
		return Registry::get('db')->queryValue('
 | 
							return Registry::get('db')->queryValue('
 | 
				
			||||||
			SELECT id_tag
 | 
								SELECT id_tag
 | 
				
			||||||
			FROM tags
 | 
								FROM tags
 | 
				
			||||||
			WHERE slug = :slug',
 | 
								WHERE slug = {string:slug}',
 | 
				
			||||||
			['slug' => $slug]);
 | 
								['slug' => $slug]);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -413,103 +376,31 @@ class Tag
 | 
				
			|||||||
		return Registry::get('db')->queryPair('
 | 
							return Registry::get('db')->queryPair('
 | 
				
			||||||
			SELECT tag, id_tag
 | 
								SELECT tag, id_tag
 | 
				
			||||||
			FROM tags
 | 
								FROM tags
 | 
				
			||||||
			WHERE tag IN (:tags)',
 | 
								WHERE tag IN ({array_string:tags})',
 | 
				
			||||||
			['tags' => $tags]);
 | 
								['tags' => $tags]);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public static function getCount($only_used = true, $kind = '', $isAlbum = false)
 | 
						public static function getCount($only_active = 1, $kind = '')
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		$where = [];
 | 
							$where = [];
 | 
				
			||||||
		if ($only_used)
 | 
							if ($only_active)
 | 
				
			||||||
			$where[] = 'count > 0';
 | 
								$where[] = 'count > 0';
 | 
				
			||||||
		if (empty($kind))
 | 
							if (!empty($kind))
 | 
				
			||||||
			$kind = 'Album';
 | 
								$where[] = 'kind = {string:kind}';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		$operator = $isAlbum ? '=' : '!=';
 | 
							if (!empty($where))
 | 
				
			||||||
		$where[] = 'kind ' . $operator . ' :kind';
 | 
								$where = 'WHERE ' . implode(' AND ', $where);
 | 
				
			||||||
		$where = implode(' AND ', $where);
 | 
							else
 | 
				
			||||||
 | 
								$where = '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		return Registry::get('db')->queryValue('
 | 
							return Registry::get('db')->queryValue('
 | 
				
			||||||
			SELECT COUNT(*)
 | 
								SELECT COUNT(*)
 | 
				
			||||||
			FROM tags
 | 
								FROM tags ' . $where,
 | 
				
			||||||
			WHERE ' . $where,
 | 
								['kind' => $kind]);
 | 
				
			||||||
			[
 | 
					 | 
				
			||||||
				'kind' => $kind,
 | 
					 | 
				
			||||||
			]);
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public static function getOffset($offset, $limit, $order, $direction, $isAlbum = false)
 | 
						public function __toString()
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		assert(in_array($order, ['id_tag', 'tag', 'slug', 'count']));
 | 
							return $this->tag;
 | 
				
			||||||
		$order = $order . ($direction === 'up' ? ' ASC' : ' DESC');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		$operator = $isAlbum ? '=' : '!=';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		$db = Registry::get('db');
 | 
					 | 
				
			||||||
		$res = $db->query('
 | 
					 | 
				
			||||||
			SELECT t.*, u.id_user, u.first_name, u.surname
 | 
					 | 
				
			||||||
			FROM tags AS t
 | 
					 | 
				
			||||||
			LEFT JOIN users AS u ON t.id_user_owner = u.id_user
 | 
					 | 
				
			||||||
			WHERE kind ' . $operator . ' :album
 | 
					 | 
				
			||||||
			ORDER BY id_parent, ' . $order . '
 | 
					 | 
				
			||||||
			LIMIT :offset, :limit',
 | 
					 | 
				
			||||||
			[
 | 
					 | 
				
			||||||
				'offset' => $offset,
 | 
					 | 
				
			||||||
				'limit' => $limit,
 | 
					 | 
				
			||||||
				'album' => 'Album',
 | 
					 | 
				
			||||||
			]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		$albums_by_parent = [];
 | 
					 | 
				
			||||||
		while ($row = $db->fetchAssoc($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)
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			static $headers_to_keep = ['id_tag', 'tag', 'slug', 'count', 'id_user', 'first_name', 'surname'];
 | 
					 | 
				
			||||||
			$rows[] = array_intersect_key($album, array_flip($headers_to_keep));
 | 
					 | 
				
			||||||
			if (!empty($album['children']))
 | 
					 | 
				
			||||||
			{
 | 
					 | 
				
			||||||
				$children = self::flattenChildrenRecursively($album['children']);
 | 
					 | 
				
			||||||
				foreach ($children as $child)
 | 
					 | 
				
			||||||
					$rows[] = array_intersect_key($child, array_flip($headers_to_keep));
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		return $rows;
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -9,14 +9,12 @@
 | 
				
			|||||||
class Thumbnail
 | 
					class Thumbnail
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
	private $image;
 | 
						private $image;
 | 
				
			||||||
	private $image_meta;
 | 
					 | 
				
			||||||
	private $thumbnails;
 | 
						private $thumbnails;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private $properly_initialised;
 | 
						private $properly_initialised;
 | 
				
			||||||
	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;
 | 
				
			||||||
@ -25,7 +23,7 @@ class Thumbnail
 | 
				
			|||||||
	const CROP_MODE_SLICE_CENTRE = 4;
 | 
						const CROP_MODE_SLICE_CENTRE = 4;
 | 
				
			||||||
	const CROP_MODE_SLICE_BOTTOM = 5;
 | 
						const CROP_MODE_SLICE_BOTTOM = 5;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public function __construct(Image $image)
 | 
						public function __construct($image)
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		$this->image      = $image;
 | 
							$this->image      = $image;
 | 
				
			||||||
		$this->image_meta = $image->getMeta();
 | 
							$this->image_meta = $image->getMeta();
 | 
				
			||||||
@ -47,45 +45,51 @@ class Thumbnail
 | 
				
			|||||||
		$thumb_selector = $this->width . 'x' . $this->height . $this->filename_suffix;
 | 
							$thumb_selector = $this->width . 'x' . $this->height . $this->filename_suffix;
 | 
				
			||||||
		if (!empty($this->thumbnails[$thumb_selector]))
 | 
							if (!empty($this->thumbnails[$thumb_selector]))
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			$thumb_filename = $this->image->getSubdir() . '/' . $this->thumbnails[$thumb_selector];
 | 
								$thumb_path = '/' . $this->image->getSubdir() . '/' . $this->thumbnails[$thumb_selector];
 | 
				
			||||||
			if (file_exists(THUMBSDIR . '/' . $thumb_filename))
 | 
								if (file_exists(THUMBSDIR . $thumb_path))
 | 
				
			||||||
				return THUMBSURL . '/' . $thumb_filename;
 | 
									return THUMBSURL . $thumb_path;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Do we have a custom thumbnail on file?
 | 
							// Do we have a custom thumbnail on file?
 | 
				
			||||||
		$custom_selector = 'custom_' . $this->width . 'x' . $this->height;
 | 
							$custom_selector = 'custom_' . $this->width . 'x' . $this->height;
 | 
				
			||||||
		if (isset($this->image_meta[$custom_selector]))
 | 
							if (isset($this->image_meta[$custom_selector]))
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			$custom_filename = $this->image->getSubdir() . '/' . $this->image_meta[$custom_selector];
 | 
								$custom_thumb_path = '/' . $this->image->getSubdir() . '/' . $this->image_meta[$custom_selector];
 | 
				
			||||||
			if (file_exists(ASSETSDIR . '/' . $custom_filename))
 | 
								if (file_exists(ASSETSDIR . $custom_thumb_path))
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
 | 
									// Ensure destination thumbnail directory exists.
 | 
				
			||||||
 | 
									if (!file_exists($this->image->getSubdir()))
 | 
				
			||||||
 | 
										@mkdir(THUMBSDIR . '/' . $this->image->getSubdir(), 0755, true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				// Copy the custom thumbail to the general thumbnail directory.
 | 
									// Copy the custom thumbail to the general thumbnail directory.
 | 
				
			||||||
				copy(ASSETSDIR . '/' . $custom_filename, THUMBSDIR . '/' . $custom_filename);
 | 
									copy(ASSETSDIR . $custom_thumb_path, THUMBSDIR . $custom_thumb_path);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				// Let's remember this for future reference.
 | 
									// Let's remember this for future reference.
 | 
				
			||||||
				$this->markAsGenerated($this->image_meta[$custom_selector]);
 | 
									$this->markAsGenerated($this->image_meta[$custom_selector]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				return THUMBSURL . '/' . $custom_filename;
 | 
									return THUMBSURL . $custom_thumb_path;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			else
 | 
								else
 | 
				
			||||||
				throw new UnexpectedValueException('Custom thumbnail expected, but missing in file system!');
 | 
									throw new UnexpectedValueException('Custom thumbnail expected, but missing in file system!');
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Is this the right moment to generate a thumbnail, then?
 | 
							// Is this the right moment to generate a thumbnail, then?
 | 
				
			||||||
		if ($generate)
 | 
							if ($generate && array_key_exists($thumb_selector, $this->thumbnails))
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			if (array_key_exists($thumb_selector, $this->thumbnails))
 | 
					 | 
				
			||||||
			return $this->generate();
 | 
								return $this->generate();
 | 
				
			||||||
			else
 | 
					 | 
				
			||||||
				throw new Exception("Trying to generate a thumbnail not previously queued by the system\n" .
 | 
					 | 
				
			||||||
					print_r(func_get_args(), true));
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// If not, queue it for generation at another time, and return a URL to generate it with.
 | 
							// If not, queue it for generation at another time, and return a URL to generate it with.
 | 
				
			||||||
		else
 | 
							elseif (!$generate)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			$this->markAsQueued();
 | 
								$this->markAsQueued();
 | 
				
			||||||
			return BASEURL . '/thumbnail/' . $this->image->getId() . '/' . $thumb_selector . '/';
 | 
								return BASEURL . '/thumbnail/' . $this->image->getId() . '/' . $this->width . 'x' . $this->height . $this->filename_suffix . '/';
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Still here..? What are you up to? ..Sneaking?
 | 
				
			||||||
 | 
							else
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								throw new Exception("Trying to generate a thumbnail for selector " . $thumb_selector . ", which does not appear to have been requested by the system.\n" . print_r(func_get_args(), true));
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -256,18 +260,14 @@ class Thumbnail
 | 
				
			|||||||
				'_' . $this->width . 'x' . $this->height . $this->filename_suffix . '.' . $ext;
 | 
									'_' . $this->width . 'x' . $this->height . $this->filename_suffix . '.' . $ext;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			// Ensure the thumbnail subdirectory exists.
 | 
								// Ensure the thumbnail subdirectory exists.
 | 
				
			||||||
			$target_dir = THUMBSDIR . '/' . $this->image->getSubdir();
 | 
								if (!is_dir(THUMBSDIR . '/' . $this->image->getSubdir()))
 | 
				
			||||||
			if (!is_dir($target_dir))
 | 
									mkdir(THUMBSDIR . '/' . $this->image->getSubdir(), 0755, true);
 | 
				
			||||||
				mkdir($target_dir, 0755, true);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			if (!is_writable($target_dir))
 | 
					 | 
				
			||||||
				throw new Exception('Thumbnail directory is not writable!');
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
			// No need to preserve every detail.
 | 
								// No need to preserve every detail.
 | 
				
			||||||
			$thumb->setImageCompressionQuality(80);
 | 
								$thumb->setImageCompressionQuality(80);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			// Save it in a public spot.
 | 
								// Save it in a public spot.
 | 
				
			||||||
			$thumb->writeImage($target_dir . '/' . $thumb_filename);
 | 
								$thumb->writeImage(THUMBSDIR . '/' . $this->image->getSubdir() . '/' . $thumb_filename);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			// Let's remember this for future reference...
 | 
								// Let's remember this for future reference...
 | 
				
			||||||
			$this->markAsGenerated($thumb_filename);
 | 
								$this->markAsGenerated($thumb_filename);
 | 
				
			||||||
@ -278,6 +278,7 @@ class Thumbnail
 | 
				
			|||||||
			// Finally, return the URL for the generated thumbnail image.
 | 
								// Finally, return the URL for the generated thumbnail image.
 | 
				
			||||||
			return THUMBSURL . '/' . $this->image->getSubdir() . '/' . $thumb_filename;
 | 
								return THUMBSURL . '/' . $this->image->getSubdir() . '/' . $thumb_filename;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
							// Blast! Curse your sudden but inevitable betrayal!
 | 
				
			||||||
		catch (ImagickException $e)
 | 
							catch (ImagickException $e)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			throw new Exception('ImageMagick error occurred while generating thumbnail. Output: ' . $e->getMessage());
 | 
								throw new Exception('ImageMagick error occurred while generating thumbnail. Output: ' . $e->getMessage());
 | 
				
			||||||
@ -335,21 +336,15 @@ class Thumbnail
 | 
				
			|||||||
		if ($success)
 | 
							if ($success)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			$thumb_selector = $this->width . 'x' . $this->height . $this->filename_suffix;
 | 
								$thumb_selector = $this->width . 'x' . $this->height . $this->filename_suffix;
 | 
				
			||||||
			$this->thumbnails[$thumb_selector] = $filename ?? null;
 | 
								$this->thumbnails[$thumb_selector] = $filename !== 'NULL' ? $filename : '';
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
			// For consistency, write new thumbnail filename to parent Image object.
 | 
					 | 
				
			||||||
			// TODO: there could still be an inconsistency if multiple objects exists for the same image asset.
 | 
					 | 
				
			||||||
			$this->image->getThumbnails()[$thumb_selector] = $this->thumbnails[$thumb_selector];
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
		return $success;
 | 
							return $success;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
		else
 | 
					 | 
				
			||||||
			throw new UnexpectedValueException('Thumbnail queuing query failed');
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private function markAsQueued()
 | 
						private function markAsQueued()
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		$this->updateDb(null);
 | 
							$this->updateDb('NULL');
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private function markAsGenerated($filename)
 | 
						private function markAsGenerated($filename)
 | 
				
			||||||
 | 
				
			|||||||
@ -12,21 +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 $reset_blocked_until;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	protected bool $is_logged;
 | 
						protected $is_logged;
 | 
				
			||||||
	protected bool $is_guest;
 | 
						protected $is_guest;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/**
 | 
						/**
 | 
				
			||||||
	 * Returns user id.
 | 
						 * Returns user id.
 | 
				
			||||||
@ -76,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.
 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
 | 
				
			|||||||
@ -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'];
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -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
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/images/nothumb.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 3.6 KiB  | 
@ -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  | 
@ -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;
 | 
				
			||||||
 | 
				
			|||||||
@ -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();
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
 | 
				
			|||||||
@ -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)
 | 
					 | 
				
			||||||
				})
 | 
					 | 
				
			||||||
			})
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
})()
 | 
					 | 
				
			||||||
@ -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() {
 | 
				
			||||||
 | 
				
			|||||||
@ -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);
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,4 @@
 | 
				
			|||||||
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 = [];
 | 
				
			||||||
@ -8,60 +7,54 @@ class UploadQueue {
 | 
				
			|||||||
	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;
 | 
				
			||||||
			};
 | 
								};
 | 
				
			||||||
 | 
								that.addPreviewBoxForQueueSlot(i);
 | 
				
			||||||
				if (this.queue.files[0].name.toUpperCase().endsWith(".HEIC")) {
 | 
								that.addPreviewForFile(that.queue.files[i], i, callback);
 | 
				
			||||||
					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);
 | 
					 | 
				
			||||||
		};
 | 
							};
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
		this.submit.addEventListener('click', event => {
 | 
						that.submit.addEventListener('click', function(e) {
 | 
				
			||||||
			event.preventDefault();
 | 
							e.preventDefault();
 | 
				
			||||||
			this.process();
 | 
							that.process();
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
	this.submit.disabled = true;
 | 
						this.submit.disabled = true;
 | 
				
			||||||
	}
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	clearPreviews() {
 | 
					UploadQueue.prototype.clearPreviews = function() {
 | 
				
			||||||
	this.preview_area.innerHTML = '';
 | 
						this.preview_area.innerHTML = '';
 | 
				
			||||||
	this.submit.disabled = true;
 | 
						this.submit.disabled = true;
 | 
				
			||||||
	this.current_upload_index = -1;
 | 
						this.current_upload_index = -1;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	addPreviewBoxForQueueSlot(index) {
 | 
					UploadQueue.prototype.addPreviewBoxForQueueSlot = function(index) {
 | 
				
			||||||
		const preview_box = document.createElement('div');
 | 
						var preview_box = document.createElement('div');
 | 
				
			||||||
	preview_box.id = 'upload_preview_' + index;
 | 
						preview_box.id = 'upload_preview_' + index;
 | 
				
			||||||
	this.preview_area.appendChild(preview_box);
 | 
						this.preview_area.appendChild(preview_box);
 | 
				
			||||||
	}
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	addPreviewForFile(file, index, callback) {
 | 
					UploadQueue.prototype.addPreviewForFile = function(file, index, callback) {
 | 
				
			||||||
	if (!file) {
 | 
						if (!file) {
 | 
				
			||||||
		return false;
 | 
							return false;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		const preview = document.createElement('canvas');
 | 
						var preview = document.createElement('canvas');
 | 
				
			||||||
	preview.title = file.name;
 | 
						preview.title = file.name;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		const preview_box = document.getElementById('upload_preview_' + index);
 | 
						var preview_box = document.getElementById('upload_preview_' + index);
 | 
				
			||||||
	preview_box.appendChild(preview);
 | 
						preview_box.appendChild(preview);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		const reader = new FileReader();
 | 
						var reader = new FileReader();
 | 
				
			||||||
		reader.addEventListener('load', event => {
 | 
						var that = this;
 | 
				
			||||||
			const original = document.createElement('img');
 | 
						reader.addEventListener('load', function() {
 | 
				
			||||||
 | 
							var original = document.createElement('img');
 | 
				
			||||||
		original.src = reader.result;
 | 
							original.src = reader.result;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		original.addEventListener('load', function() {
 | 
							original.addEventListener('load', function() {
 | 
				
			||||||
@ -70,7 +63,7 @@ class UploadQueue {
 | 
				
			|||||||
			preview.width = preview.height * (original.width / original.height);
 | 
								preview.width = preview.height * (original.width / original.height);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			// First pass: resize to 50% on temp canvas.
 | 
								// First pass: resize to 50% on temp canvas.
 | 
				
			||||||
				const temp = document.createElement('canvas'),
 | 
								var temp = document.createElement('canvas'),
 | 
				
			||||||
				tempCtx = temp.getContext('2d');
 | 
									tempCtx = temp.getContext('2d');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			temp.width = original.width * 0.5;
 | 
								temp.width = original.width * 0.5;
 | 
				
			||||||
@ -81,7 +74,7 @@ class UploadQueue {
 | 
				
			|||||||
			tempCtx.drawImage(temp, 0, 0, temp.width * 0.5, temp.height * 0.5);
 | 
								tempCtx.drawImage(temp, 0, 0, temp.width * 0.5, temp.height * 0.5);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			// Final pass: resize to desired size on preview canvas.
 | 
								// Final pass: resize to desired size on preview canvas.
 | 
				
			||||||
				const context = preview.getContext('2d');
 | 
								var context = preview.getContext('2d');
 | 
				
			||||||
			context.drawImage(temp, 0, 0, temp.width * 0.5, temp.height * 0.5,
 | 
								context.drawImage(temp, 0, 0, temp.width * 0.5, temp.height * 0.5,
 | 
				
			||||||
				0, 0, preview.width, preview.height);
 | 
									0, 0, preview.width, preview.height);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -91,79 +84,83 @@ class UploadQueue {
 | 
				
			|||||||
		});
 | 
							});
 | 
				
			||||||
	}, false);
 | 
						}, false);
 | 
				
			||||||
	reader.readAsDataURL(file);
 | 
						reader.readAsDataURL(file);
 | 
				
			||||||
	}
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	process() {
 | 
					UploadQueue.prototype.process = function() {
 | 
				
			||||||
	this.showSpinner(this.submit, "Preparing to upload files...");
 | 
						this.showSpinner(this.submit, "Preparing to upload files...");
 | 
				
			||||||
	if (this.queue.files.length > 0) {
 | 
						if (this.queue.files.length > 0) {
 | 
				
			||||||
		this.submit.disabled = true;
 | 
							this.submit.disabled = true;
 | 
				
			||||||
		this.nextFile();
 | 
							this.nextFile();
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	}
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	nextFile() {
 | 
					UploadQueue.prototype.nextFile = function() {
 | 
				
			||||||
		const files = this.queue.files;
 | 
						var files = this.queue.files;
 | 
				
			||||||
		const i = ++this.current_upload_index;
 | 
						var i = ++this.current_upload_index;
 | 
				
			||||||
	if (i === files.length) {
 | 
						if (i === files.length) {
 | 
				
			||||||
		this.hideSpinner();
 | 
							this.hideSpinner();
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
		this.setSpinnerLabel("Uploading file " + (i + 1) + " out of " + files.length);
 | 
							this.setSpinnerLabel("Uploading file " + (i + 1) + " out of " + files.length);
 | 
				
			||||||
			this.sendFile(files[i], i, this.nextFile);
 | 
							this.sendFile(files[i], i, function() {
 | 
				
			||||||
		}
 | 
								this.nextFile();
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	sendFile(file, index, callback) {
 | 
					UploadQueue.prototype.sendFile = function(file, index, callback) {
 | 
				
			||||||
		const request = new XMLHttpRequest();
 | 
						// Prepare the request.
 | 
				
			||||||
		request.addEventListener('error', event => {
 | 
						var that = this;
 | 
				
			||||||
			this.updateProgress(index, -1);
 | 
						var request = new XMLHttpRequest();
 | 
				
			||||||
 | 
						request.addEventListener('error', function(event) {
 | 
				
			||||||
 | 
							that.updateProgress(index, -1);
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
		request.addEventListener('progress', event => {
 | 
						request.addEventListener('progress', function(event) {
 | 
				
			||||||
			this.updateProgress(index, event.loaded / event.total);
 | 
							that.updateProgress(index, event.loaded / event.total);
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
		request.addEventListener('load', event => {
 | 
						request.addEventListener('load', function(event) {
 | 
				
			||||||
			this.updateProgress(index, 1);
 | 
							that.updateProgress(index, 1);
 | 
				
			||||||
		if (request.responseText !== null && request.status === 200) {
 | 
							if (request.responseText !== null && request.status === 200) {
 | 
				
			||||||
				const obj = JSON.parse(request.responseText);
 | 
								var obj = JSON.parse(request.responseText);
 | 
				
			||||||
			if (obj.error) {
 | 
								if (obj.error) {
 | 
				
			||||||
				alert(obj.error);
 | 
									alert(obj.error);
 | 
				
			||||||
				return;
 | 
									return;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			else if (callback) {
 | 
								else if (callback) {
 | 
				
			||||||
					callback.call(this, obj);
 | 
									callback.call(that, obj);
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		const data = new FormData();
 | 
						var data = new FormData();
 | 
				
			||||||
	data.append('uploads', file, file.name);
 | 
						data.append('uploads', file, file.name);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	request.open('POST', this.upload_url, true);
 | 
						request.open('POST', this.upload_url, true);
 | 
				
			||||||
	request.send(data);
 | 
						request.send(data);
 | 
				
			||||||
	}
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	addProgressBar(index) {
 | 
					UploadQueue.prototype.addProgressBar = function(index) {
 | 
				
			||||||
	if (index in this.upload_progress) {
 | 
						if (index in this.upload_progress) {
 | 
				
			||||||
		return;
 | 
							return;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		const progress_container = document.createElement('div');
 | 
						var progress_container = document.createElement('div');
 | 
				
			||||||
	progress_container.className = 'progress';
 | 
						progress_container.className = 'progress';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		const progress = document.createElement('div');
 | 
						var progress = document.createElement('div');
 | 
				
			||||||
	progress_container.appendChild(progress);
 | 
						progress_container.appendChild(progress);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		const preview_box = document.getElementById('upload_preview_' + index);
 | 
						var preview_box = document.getElementById('upload_preview_' + index);
 | 
				
			||||||
	preview_box.appendChild(progress_container);
 | 
						preview_box.appendChild(progress_container);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	this.upload_progress[index]	= progress;
 | 
						this.upload_progress[index]	= progress;
 | 
				
			||||||
	}
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	updateProgress(index, progress) {
 | 
					UploadQueue.prototype.updateProgress = function(index, progress) {
 | 
				
			||||||
	if (!(index in this.upload_progress)) {
 | 
						if (!(index in this.upload_progress)) {
 | 
				
			||||||
		this.addProgressBar(index);
 | 
							this.addProgressBar(index);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		const bar = this.upload_progress[index];
 | 
						var bar = this.upload_progress[index];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (progress >= 0) {
 | 
						if (progress >= 0) {
 | 
				
			||||||
		bar.style.width = Math.ceil(progress * 100) + '%';
 | 
							bar.style.width = Math.ceil(progress * 100) + '%';
 | 
				
			||||||
@ -173,9 +170,9 @@ class UploadQueue {
 | 
				
			|||||||
			bar.className = "error";
 | 
								bar.className = "error";
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	}
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	showSpinner(sibling, label) {
 | 
					UploadQueue.prototype.showSpinner = function(sibling, label) {
 | 
				
			||||||
	if (this.spinner) {
 | 
						if (this.spinner) {
 | 
				
			||||||
		return;
 | 
							return;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@ -190,15 +187,15 @@ class UploadQueue {
 | 
				
			|||||||
		this.spinner_label.innerHTML = label;
 | 
							this.spinner_label.innerHTML = label;
 | 
				
			||||||
		sibling.parentNode.appendChild(this.spinner_label);
 | 
							sibling.parentNode.appendChild(this.spinner_label);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	}
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	setSpinnerLabel(label) {
 | 
					UploadQueue.prototype.setSpinnerLabel = function(label) {
 | 
				
			||||||
	if (this.spinner_label) {
 | 
						if (this.spinner_label) {
 | 
				
			||||||
		this.spinner_label.innerHTML = label;
 | 
							this.spinner_label.innerHTML = label;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	hideSpinner() {
 | 
					UploadQueue.prototype.hideSpinner = function() {
 | 
				
			||||||
	if (this.spinner) {
 | 
						if (this.spinner) {
 | 
				
			||||||
		this.spinner.parentNode.removeChild(this.spinner);
 | 
							this.spinner.parentNode.removeChild(this.spinner);
 | 
				
			||||||
		this.spinner = null;
 | 
							this.spinner = null;
 | 
				
			||||||
@ -207,5 +204,4 @@ class UploadQueue {
 | 
				
			|||||||
		this.spinner_label.parentNode.removeChild(this.spinner_label);
 | 
							this.spinner_label.parentNode.removeChild(this.spinner_label);
 | 
				
			||||||
		this.spinner_label = null;
 | 
							this.spinner_label = null;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	}
 | 
					};
 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
				
			|||||||
@ -1 +0,0 @@
 | 
				
			|||||||
../vendor/
 | 
					 | 
				
			||||||
							
								
								
									
										38
									
								
								templates/AdminBar.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								templates/AdminBar.php
									
									
									
									
									
										Normal 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];
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -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="container 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>';
 | 
				
			||||||
 | 
				
			|||||||
@ -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, '">←</a>
 | 
				
			||||||
					<i class="bi bi-arrow-left"></i>
 | 
					 | 
				
			||||||
				</a>
 | 
					 | 
				
			||||||
				<div>
 | 
									<div>
 | 
				
			||||||
					<h2>', $this->title, '</h2>';
 | 
										<h2>', $this->title, '</h2>';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -6,7 +6,7 @@
 | 
				
			|||||||
 * Kabuki CMS (C) 2013-2015, Aaron van Geffen
 | 
					 * Kabuki CMS (C) 2013-2015, Aaron van Geffen
 | 
				
			||||||
 *****************************************************************************/
 | 
					 *****************************************************************************/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AlbumIndex extends 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,25 +23,20 @@ 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 '
 | 
								echo '
 | 
				
			||||||
					<div class="col-md-6 col-xl-4">
 | 
										<div class="tiled_row">';
 | 
				
			||||||
						<div class="polaroid landscape" style="aspect-ratio: 1.12">';
 | 
					
 | 
				
			||||||
 | 
								foreach ($photos as $album)
 | 
				
			||||||
 | 
								{
 | 
				
			||||||
 | 
									echo '
 | 
				
			||||||
 | 
											<div class="landscape">';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				if ($this->show_edit_buttons)
 | 
									if ($this->show_edit_buttons)
 | 
				
			||||||
					echo '
 | 
										echo '
 | 
				
			||||||
@ -52,28 +46,11 @@ class AlbumIndex extends Template
 | 
				
			|||||||
							<a href="', $album['link'], '">';
 | 
												<a href="', $album['link'], '">';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				if (isset($album['thumbnail']))
 | 
									if (isset($album['thumbnail']))
 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			$thumbs = [];
 | 
					 | 
				
			||||||
			foreach ([1, 2] as $factor)
 | 
					 | 
				
			||||||
				$thumbs[$factor] = $album['thumbnail']->getThumbnailUrl(
 | 
					 | 
				
			||||||
					static::TILE_WIDTH * $factor, static::TILE_HEIGHT * $factor, true, true);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			foreach (['normal-photo', 'blur-photo'] as $className)
 | 
					 | 
				
			||||||
			{
 | 
					 | 
				
			||||||
					echo '
 | 
										echo '
 | 
				
			||||||
								<img alt="" src="', $thumbs[1], '"' . (isset($thumbs[2]) ?
 | 
													<img src="', $album['thumbnail']->getThumbnailUrl(static::TILE_WIDTH, static::TILE_HEIGHT, true, true), '" alt="">';
 | 
				
			||||||
									' srcset="' . $thumbs[2] . ' 2x"' : '') .
 | 
					 | 
				
			||||||
									' class="', $className, '"' .
 | 
					 | 
				
			||||||
									' alt="" style="aspect-ratio: ', self::TILE_RATIO, '">';
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
				else
 | 
									else
 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
					echo '
 | 
										echo '
 | 
				
			||||||
								<img alt="" src="', BASEURL, '/images/nothumb.svg"',
 | 
													<img src="', BASEURL, '/images/nothumb.png" alt="">';
 | 
				
			||||||
									' class="placeholder-image"',
 | 
					 | 
				
			||||||
									' style="aspect-ratio: ', self::TILE_RATIO, '; object-fit: unset">';
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
				if ($this->show_labels)
 | 
									if ($this->show_labels)
 | 
				
			||||||
					echo '
 | 
										echo '
 | 
				
			||||||
@ -81,7 +58,14 @@ class AlbumIndex extends Template
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
				echo '
 | 
									echo '
 | 
				
			||||||
							</a>
 | 
												</a>
 | 
				
			||||||
						</div>
 | 
											</div>';
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								echo '
 | 
				
			||||||
 | 
										</div>';
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							echo '
 | 
				
			||||||
			</div>';
 | 
								</div>';
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -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()
 | 
				
			||||||
	{
 | 
						{}
 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -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
									
								
							
							
						
						
									
										27
									
								
								templates/Button.php
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,27 @@
 | 
				
			|||||||
 | 
					<?php
 | 
				
			||||||
 | 
					/*****************************************************************************
 | 
				
			||||||
 | 
					 * Button.php
 | 
				
			||||||
 | 
					 * Defines the Button template.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * Kabuki CMS (C) 2013-2015, Aaron van Geffen
 | 
				
			||||||
 | 
					 *****************************************************************************/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Button extends 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>';
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										35
									
								
								templates/ConfirmDeletePage.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								templates/ConfirmDeletePage.php
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,35 @@
 | 
				
			|||||||
 | 
					<?php
 | 
				
			||||||
 | 
					/*****************************************************************************
 | 
				
			||||||
 | 
					 * ConfirmDeletePage.php
 | 
				
			||||||
 | 
					 * Contains the confirm delete page template.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * Kabuki CMS (C) 2013-2016, Aaron van Geffen
 | 
				
			||||||
 | 
					 *****************************************************************************/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ConfirmDeletePage extends PhotoPage
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						public function __construct(Image $photo)
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							parent::__construct($photo);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						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();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -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 '
 | 
				
			||||||
				<h2>', $this->_title, '</h2>';
 | 
								<div class="boxed_content', $this->_class ? ' ' . $this->_class : '', '">', $this->_title ? '
 | 
				
			||||||
 | 
									<h2>' . $this->_title . '</h2>' : '', '
 | 
				
			||||||
		echo $this->_content;
 | 
									', $this->_content;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		foreach ($this->_subtemplates as $template)
 | 
							foreach ($this->_subtemplates as $template)
 | 
				
			||||||
			$template->html_main();
 | 
								$template->html_main();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							echo '
 | 
				
			||||||
 | 
								</div>';
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -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();
 | 
				
			||||||
@ -61,7 +52,6 @@ class EditAssetForm extends Template
 | 
				
			|||||||
		$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)
 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			if ($tag->kind === 'Album')
 | 
					 | 
				
			||||||
				continue;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				echo '
 | 
									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>';
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,41 +0,0 @@
 | 
				
			|||||||
<?php
 | 
					 | 
				
			||||||
/*****************************************************************************
 | 
					 | 
				
			||||||
 * ErrorPage.php
 | 
					 | 
				
			||||||
 * Defines the template class ErrorPage.
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * Kabuki CMS (C) 2013-2025, Aaron van Geffen
 | 
					 | 
				
			||||||
 *****************************************************************************/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class ErrorPage extends Template
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	private $debug_info;
 | 
					 | 
				
			||||||
	private $message;
 | 
					 | 
				
			||||||
	private $title;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	public function __construct($title, $message, $debug_info = null)
 | 
					 | 
				
			||||||
	{
 | 
					 | 
				
			||||||
		$this->title = $title;
 | 
					 | 
				
			||||||
		$this->message = $message;
 | 
					 | 
				
			||||||
		$this->debug_info = $debug_info;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	public function html_main()
 | 
					 | 
				
			||||||
	{
 | 
					 | 
				
			||||||
		echo '
 | 
					 | 
				
			||||||
				<div class="content-box container">
 | 
					 | 
				
			||||||
					<h2>', $this->title, '</h2>
 | 
					 | 
				
			||||||
					<p>', nl2br(htmlspecialchars($this->message)), '</p>';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if (isset($this->debug_info))
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			echo '
 | 
					 | 
				
			||||||
				</div>
 | 
					 | 
				
			||||||
				<div class="content-box container">
 | 
					 | 
				
			||||||
					<h4>Debug Info</h4>
 | 
					 | 
				
			||||||
					<pre>', htmlspecialchars($this->debug_info), '</pre>';
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		echo '
 | 
					 | 
				
			||||||
				</div>';
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,57 +0,0 @@
 | 
				
			|||||||
<?php
 | 
					 | 
				
			||||||
/*****************************************************************************
 | 
					 | 
				
			||||||
 * FeaturedThumbnailManager.php
 | 
					 | 
				
			||||||
 * Contains the featured thumbnail manager template.
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * Kabuki CMS (C) 2013-2021, Aaron van Geffen
 | 
					 | 
				
			||||||
 *****************************************************************************/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class FeaturedThumbnailManager extends SubTemplate
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	private $iterator;
 | 
					 | 
				
			||||||
	private $currentThumbnailId;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	public function __construct(AssetIterator $iterator, $currentThumbnailId)
 | 
					 | 
				
			||||||
	{
 | 
					 | 
				
			||||||
		$this->iterator = $iterator;
 | 
					 | 
				
			||||||
		$this->currentThumbnailId = $currentThumbnailId;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	protected function html_content()
 | 
					 | 
				
			||||||
	{
 | 
					 | 
				
			||||||
		echo '
 | 
					 | 
				
			||||||
		<form action="" method="post">
 | 
					 | 
				
			||||||
			<div class="row">
 | 
					 | 
				
			||||||
				<div class="col-lg">
 | 
					 | 
				
			||||||
					<h2>Select thumbnail</h2>
 | 
					 | 
				
			||||||
				</div>
 | 
					 | 
				
			||||||
				<div class="col-lg">';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		foreach ($this->_subtemplates as $template)
 | 
					 | 
				
			||||||
			$template->html_main();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		echo '
 | 
					 | 
				
			||||||
				</div>
 | 
					 | 
				
			||||||
				<div class="col-lg-auto">
 | 
					 | 
				
			||||||
					<button class="btn btn-primary" type="submit" name="changeThumbnail">Save thumbnail selection</button>
 | 
					 | 
				
			||||||
				</div>
 | 
					 | 
				
			||||||
			</div>
 | 
					 | 
				
			||||||
			<ul id="featuredThumbnail">';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		foreach ($this->iterator as $asset)
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			$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>';
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		echo '
 | 
					 | 
				
			||||||
				</ul>
 | 
					 | 
				
			||||||
				<input type="hidden" name="', Session::getSessionTokenKey(), '" value="', Session::getSessionToken(), '">
 | 
					 | 
				
			||||||
			</form>';
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -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">
 | 
					 | 
				
			||||||
							<input type="text" class="form-control" id="field_emailaddress" name="emailaddress">
 | 
					 | 
				
			||||||
						</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>
 | 
													<button type="submit" class="btn btn-primary">Send mail</button>
 | 
				
			||||||
						</div>
 | 
												</form>
 | 
				
			||||||
					</div>
 | 
											</div>';
 | 
				
			||||||
				</form>';
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -3,273 +3,157 @@
 | 
				
			|||||||
 * 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()
 | 
						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->before_fields))
 | 
							if (isset($this->content_above))
 | 
				
			||||||
			echo $this->form->before_fields;
 | 
								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
 | 
				
			||||||
 | 
								if (!empty($exclude) && in_array($field_id, $exclude))
 | 
				
			||||||
 | 
									continue;
 | 
				
			||||||
 | 
								// ... or a whitelist
 | 
				
			||||||
 | 
								elseif (!empty($include) && !in_array($field_id, $include))
 | 
				
			||||||
 | 
									continue;
 | 
				
			||||||
 | 
								// ... or neither (ha)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			$this->renderField($field_id, $field);
 | 
								$this->renderField($field_id, $field);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (isset($this->form->after_fields))
 | 
					 | 
				
			||||||
			echo $this->form->after_fields;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		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->buttons_extra))
 | 
							if (isset($this->content_below))
 | 
				
			||||||
			echo '
 | 
								echo '
 | 
				
			||||||
						', $this->form->buttons_extra;
 | 
										', $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 ($field['type'] !== 'checkbox')
 | 
							if (isset($field['before']))
 | 
				
			||||||
		{
 | 
								echo $field['before'];
 | 
				
			||||||
			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);
 | 
					 | 
				
			||||||
				break;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			case 'radio':
 | 
					 | 
				
			||||||
				$this->renderRadio($field_id, $field);
 | 
					 | 
				
			||||||
				break;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			case 'checkbox':
 | 
					 | 
				
			||||||
				$this->renderCheckbox($field_id, $field);
 | 
					 | 
				
			||||||
				break;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			case 'textarea':
 | 
					 | 
				
			||||||
				$this->renderTextArea($field_id, $field);
 | 
					 | 
				
			||||||
				break;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			case 'color':
 | 
					 | 
				
			||||||
				$this->renderColor($field_id, $field);
 | 
					 | 
				
			||||||
				break;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			case 'numeric':
 | 
					 | 
				
			||||||
				$this->renderNumeric($field_id, $field);
 | 
					 | 
				
			||||||
				break;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			case 'file':
 | 
					 | 
				
			||||||
				$this->renderFile($field_id, $field);
 | 
					 | 
				
			||||||
				break;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			case 'captcha':
 | 
					 | 
				
			||||||
				$this->renderCaptcha($field_id, $field);
 | 
					 | 
				
			||||||
				break;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			case 'text':
 | 
					 | 
				
			||||||
			case 'password':
 | 
					 | 
				
			||||||
			default:
 | 
					 | 
				
			||||||
				$this->renderText($field_id, $field);
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if ($field['type'] !== 'checkbox')
 | 
					 | 
				
			||||||
				echo '
 | 
									echo '
 | 
				
			||||||
					</div>';
 | 
											<select name="', $field_id, '" id="', $field_id, '"', !empty($field['disabled']) ? ' disabled' : '', '>';
 | 
				
			||||||
 | 
					 | 
				
			||||||
		echo '
 | 
					 | 
				
			||||||
				</div>';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if (isset($field['after_html']))
 | 
					 | 
				
			||||||
			echo '
 | 
					 | 
				
			||||||
				', $field['after_html'];
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	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']))
 | 
									if (isset($field['placeholder']))
 | 
				
			||||||
					echo '
 | 
										echo '
 | 
				
			||||||
							<option value="">', $field['placeholder'], '</option>';
 | 
												<option value="">', $field['placeholder'], '</option>';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		foreach ($field['options'] as $key => $value)
 | 
									foreach ($field['options'] as $value => $option)
 | 
				
			||||||
		{
 | 
										echo '
 | 
				
			||||||
			if (is_array($value))
 | 
												<option value="', $value, '"', $this->data[$field_id] == $value ? ' selected' : '', '>', htmlentities($option), '</option>';
 | 
				
			||||||
			{
 | 
					 | 
				
			||||||
				assert(empty($field['multiple']));
 | 
					 | 
				
			||||||
				$this->renderSelectOptionGroup($field_id, $key, $value);
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			else
 | 
					 | 
				
			||||||
				$this->renderSelectOption($field_id, $value, $key, !empty($field['multiple']));
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
				echo '
 | 
									echo '
 | 
				
			||||||
						</select>';
 | 
											</select>';
 | 
				
			||||||
	}
 | 
									break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private function renderSelectOption($field_id, $label, $value, $multiple = false)
 | 
								case 'radio':
 | 
				
			||||||
	{
 | 
									foreach ($field['options'] as $value => $option)
 | 
				
			||||||
					echo '
 | 
										echo '
 | 
				
			||||||
							<option value="', $value, '"',
 | 
											<input type="radio" name="', $field_id, '" value="', $value, '"', $this->data[$field_id] == $value ? ' checked' : '', !empty($field['disabled']) ? ' disabled' : '', '> ', htmlentities($option);
 | 
				
			||||||
							!$multiple && $this->data[$field_id] == $value ? ' selected' : '',
 | 
									break;
 | 
				
			||||||
							$multiple && in_array($value, $this->data[$field_id]) ? ' selected' : '',
 | 
					 | 
				
			||||||
							'>', htmlspecialchars($label), '</option>';
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private function renderSelectOptionGroup($field_id, $label, $options)
 | 
								case 'checkbox':
 | 
				
			||||||
	{
 | 
					 | 
				
			||||||
				echo '
 | 
									echo '
 | 
				
			||||||
							<optgroup label="', $label, '">';
 | 
											<label><input type="checkbox"', $this->data[$field_id] ? ' checked' : '', !empty($field['disabled']) ? ' disabled' : '', ' name="', $field_id, '"> ', htmlentities($field['label']), '</label>';
 | 
				
			||||||
 | 
									break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		foreach ($options as $value => $option)
 | 
								case 'textarea':
 | 
				
			||||||
			$this->renderSelectOption($field_id, $option, $value);
 | 
									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;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								case 'color':
 | 
				
			||||||
 | 
									echo '
 | 
				
			||||||
 | 
											<input type="color" name="', $field_id, '" id="', $field_id, '" value="', htmlentities($this->data[$field_id]), '"', !empty($field['disabled']) ? ' disabled' : '', '>';
 | 
				
			||||||
 | 
									break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								case 'numeric':
 | 
				
			||||||
 | 
									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;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								case 'file':
 | 
				
			||||||
 | 
									if (!empty($this->data[$field_id]))
 | 
				
			||||||
 | 
										echo '<img src="', $this->data[$field_id], '" alt=""><br>';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				echo '
 | 
									echo '
 | 
				
			||||||
							</optgroup>';
 | 
											<input type="file" name="', $field_id, '" id="', $field_id, '"', !empty($field['disabled']) ? ' disabled' : '', '>';
 | 
				
			||||||
 | 
									break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								case 'text':
 | 
				
			||||||
 | 
								case 'password':
 | 
				
			||||||
 | 
								default:
 | 
				
			||||||
 | 
									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'] . '"' : '', '>';
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private function renderText($field_id, array $field)
 | 
							if (isset($field['after']))
 | 
				
			||||||
	{
 | 
								echo ' ', $field['after'];
 | 
				
			||||||
		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 '
 | 
							echo '
 | 
				
			||||||
						<textarea class="form-control' .
 | 
										</dd>';
 | 
				
			||||||
							'" 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>';
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,105 +0,0 @@
 | 
				
			|||||||
<?php
 | 
					 | 
				
			||||||
/*****************************************************************************
 | 
					 | 
				
			||||||
 * InlineFormView.php
 | 
					 | 
				
			||||||
 * Contains the template that renders inline forms.
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * Kabuki CMS (C) 2013-2025, Aaron van Geffen
 | 
					 | 
				
			||||||
 *****************************************************************************/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class InlineFormView
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	public static function renderInlineForm($form)
 | 
					 | 
				
			||||||
	{
 | 
					 | 
				
			||||||
		if (!isset($form['is_embed']))
 | 
					 | 
				
			||||||
			echo '
 | 
					 | 
				
			||||||
			<form action="', $form['action'], '" method="', $form['method'], '" class="', $form['class'] ?? '', '">';
 | 
					 | 
				
			||||||
		else
 | 
					 | 
				
			||||||
			echo '
 | 
					 | 
				
			||||||
			<div class="', $form['class'] ?? '', '">';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if (!empty($form['is_group']))
 | 
					 | 
				
			||||||
			echo '
 | 
					 | 
				
			||||||
				<div class="input-group">';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		foreach ($form['controls'] as $name => $control)
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			if ($control['type'] === 'select')
 | 
					 | 
				
			||||||
				self::renderSelectBox($control, $name);
 | 
					 | 
				
			||||||
			elseif ($control['type'] === 'submit')
 | 
					 | 
				
			||||||
				self::renderSubmitButton($control, $name);
 | 
					 | 
				
			||||||
			else
 | 
					 | 
				
			||||||
				self::renderInputBox($control, $name);
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		echo '
 | 
					 | 
				
			||||||
					<input type="hidden" name="', Session::getSessionTokenKey(), '" value="', Session::getSessionToken(), '">';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if (!empty($form['is_group']))
 | 
					 | 
				
			||||||
			echo '
 | 
					 | 
				
			||||||
				</div>';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if (!isset($form['is_embed']))
 | 
					 | 
				
			||||||
			echo '
 | 
					 | 
				
			||||||
			</form>';
 | 
					 | 
				
			||||||
		else
 | 
					 | 
				
			||||||
			echo '
 | 
					 | 
				
			||||||
			</div>';
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	private static function renderInputBox(array $field, $name)
 | 
					 | 
				
			||||||
	{
 | 
					 | 
				
			||||||
		echo '
 | 
					 | 
				
			||||||
					<input name="', $name, '" id="field_', $name, '" type="', $field['type'], '" ',
 | 
					 | 
				
			||||||
						'class="form-control', isset($field['class']) ? ' ' . $field['class'] : '', '"',
 | 
					 | 
				
			||||||
						isset($field['placeholder']) ? ' placeholder="' . $field['placeholder'] . '"' : '',
 | 
					 | 
				
			||||||
						isset($field['value']) ? ' value="' . htmlspecialchars($field['value']) . '"' : '', '>';
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	private static function renderSelectBox(array $field, $name)
 | 
					 | 
				
			||||||
	{
 | 
					 | 
				
			||||||
		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>';
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	private static function renderSubmitButton(array $button, $name)
 | 
					 | 
				
			||||||
	{
 | 
					 | 
				
			||||||
		echo '
 | 
					 | 
				
			||||||
					<button class="btn ', isset($button['class']) ? $button['class'] : 'btn-primary', '" ',
 | 
					 | 
				
			||||||
						'type="', $button['type'], '" name="', $name, '"';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if (isset($button['onclick']))
 | 
					 | 
				
			||||||
			echo ' onclick="', $button['onclick'], '"';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		echo '>', $button['caption'], '</button>';
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -11,44 +11,34 @@ 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 '
 | 
				
			||||||
						<h1 class="mb-4">Press #RU to continue</h1>';
 | 
								<form action="', BASEURL, '/login/" method="post" id="login">
 | 
				
			||||||
 | 
									<h3>Log in</h3>';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (!empty($this->_subtemplates))
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
		foreach ($this->_subtemplates as $template)
 | 
							foreach ($this->_subtemplates as $template)
 | 
				
			||||||
			$template->html_main();
 | 
								$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))
 | 
				
			||||||
@ -56,11 +46,9 @@ class LogInForm extends SubTemplate
 | 
				
			|||||||
				<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>
 | 
									</div>
 | 
				
			||||||
			</form>';
 | 
								</form>';
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
				
			|||||||
@ -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::has('user') && Registry::get('user')->isLoggedIn())
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			echo '
 | 
					 | 
				
			||||||
				<div class="collapse navbar-collapse justify-content-end" id="', $this->innerMenuId, '">
 | 
					 | 
				
			||||||
					<ul class="navbar-nav mb-2 mb-lg-0">';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			$mainMenu = new MainMenu();
 | 
					 | 
				
			||||||
			$this->renderMenuItems($mainMenu->getItems());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			echo '
 | 
					 | 
				
			||||||
						<li class="nav-divider d-none d-lg-inline"></li>';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			$adminMenu = new AdminMenu();
 | 
					 | 
				
			||||||
			$this->renderMenuItems($adminMenu->getItems());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			$userMenu = new UserMenu();
 | 
					 | 
				
			||||||
			$this->renderMenuItems($userMenu->getItems());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			$this->darkModeToggle();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			echo '
 | 
					 | 
				
			||||||
					</ul>
 | 
					 | 
				
			||||||
				</div>';
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		echo '
 | 
					 | 
				
			||||||
			</div>
 | 
					 | 
				
			||||||
		</nav>';
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	private function darkModeToggle()
 | 
					 | 
				
			||||||
	{
 | 
					 | 
				
			||||||
		echo '
 | 
					 | 
				
			||||||
						<li class="nav-item dropdown">
 | 
					 | 
				
			||||||
							<button class="btn btn-link nav-link py-2 px-0 px-lg-2 dropdown-toggle d-flex align-items-center"
 | 
					 | 
				
			||||||
								id="bd-theme" type="button" data-bs-toggle="dropdown" data-bs-display="static">
 | 
					 | 
				
			||||||
								<i id="theme-icon-active" class="bi bi-light"></i>
 | 
					 | 
				
			||||||
								<span class="d-lg-none ms-2" id="bd-theme-text">Toggle theme</span>
 | 
					 | 
				
			||||||
							</button>
 | 
					 | 
				
			||||||
							<ul class="dropdown-menu dropdown-menu-end">
 | 
					 | 
				
			||||||
								<li>
 | 
					 | 
				
			||||||
									<button type="button" class="dropdown-item d-flex align-items-center" data-bs-theme-value="light">
 | 
					 | 
				
			||||||
										<i class="bi bi-sun-fill"></i>
 | 
					 | 
				
			||||||
										Light
 | 
					 | 
				
			||||||
									</button>
 | 
					 | 
				
			||||||
								</li>
 | 
					 | 
				
			||||||
								<li>
 | 
					 | 
				
			||||||
									<button type="button" class="dropdown-item d-flex align-items-center" data-bs-theme-value="dark">
 | 
					 | 
				
			||||||
										<i class="bi bi-moon-stars-fill"></i>
 | 
					 | 
				
			||||||
										Dark
 | 
					 | 
				
			||||||
									</button>
 | 
					 | 
				
			||||||
								</li>
 | 
					 | 
				
			||||||
								<li>
 | 
					 | 
				
			||||||
									<button type="button" class="dropdown-item d-flex align-items-center active" data-bs-theme-value="auto">
 | 
					 | 
				
			||||||
										<i class="bi bi-circle-half"></i>
 | 
					 | 
				
			||||||
										Auto
 | 
					 | 
				
			||||||
									</button>
 | 
					 | 
				
			||||||
								</li>
 | 
					 | 
				
			||||||
							</ul>
 | 
					 | 
				
			||||||
						</li>';
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -25,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;
 | 
				
			||||||
 | 
				
			|||||||
@ -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 "', $this->tag->tag, '"</h2>
 | 
									<h2>Upload new photos to "', $this->tag->tag, '"</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>
 | 
				
			||||||
 | 
				
			|||||||
@ -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>';
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -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>';
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -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'] . '"' : '', '>',
 | 
					 | 
				
			||||||
						'« 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 »</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
									
								
							
							
						
						
									
										63
									
								
								templates/Pagination.php
									
									
									
									
									
										Normal 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', '>« previous</', !empty($index['previous']) ? 'a' : 'span', '></li>';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							$num_wildcards = 0;
 | 
				
			||||||
 | 
							foreach ($index as $key => $page)
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								if (!is_numeric($key))
 | 
				
			||||||
 | 
									continue;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if (!is_array($page))
 | 
				
			||||||
 | 
								{
 | 
				
			||||||
 | 
									$num_wildcards++;
 | 
				
			||||||
 | 
									echo '
 | 
				
			||||||
 | 
											<li class="page-padding" onclick="javascript:promptGoToPage(', self::$unique_index_count, ')"><span>...</span></li>';
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								else
 | 
				
			||||||
 | 
									echo '
 | 
				
			||||||
 | 
											<li class="page-number', $page['is_selected'] ? ' active' : '', '"><a href="', $page['href'], '">', $page['index'], '</a></li>';
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							echo '
 | 
				
			||||||
 | 
											<li class="last"><', !empty($index['next']) ? 'a href="' . $index['next']['href'] . '"' : 'span', '>next »</', !empty($index['next']) ? 'a' : 'span', '></li>
 | 
				
			||||||
 | 
										</ul>
 | 
				
			||||||
 | 
									</div>';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if ($num_wildcards)
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								echo '
 | 
				
			||||||
 | 
								<script type="text/javascript">
 | 
				
			||||||
 | 
									var page_index_', self::$unique_index_count++, ' = {
 | 
				
			||||||
 | 
										wildcard_url: "', $this->index->getLink("%d"), '",
 | 
				
			||||||
 | 
										num_pages: ', $this->index->getNumberOfPages(), ',
 | 
				
			||||||
 | 
										per_page: ', $this->index->getItemsPerPage(), '
 | 
				
			||||||
 | 
									};
 | 
				
			||||||
 | 
								</script>';
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -20,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&email=', rawurlencode($this->email), '&key=', $this->key, '" method="post">
 | 
												<form class="form-horizontal" action="', BASEURL, '/resetpassword/?step=2&email=', rawurlencode($this->email), '&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>';
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -6,234 +6,222 @@
 | 
				
			|||||||
 * 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 '
 | 
								echo '
 | 
				
			||||||
						<figure id="photo-figure" class="portrait-figure">',
 | 
											<img src="', $this->photo->getThumbnailUrl(null, 960), '" alt="">';
 | 
				
			||||||
				$this->photo->getInlineImage(null, 960, 'normal-photo'),
 | 
					 | 
				
			||||||
				$this->photo->getInlineImage(null, 960, 'blur-photo'), '
 | 
					 | 
				
			||||||
						</figure>';
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		else
 | 
							else
 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			$className = $this->photo->isPanorama() ? 'panorama-figure' : 'landscape-figure';
 | 
					 | 
				
			||||||
			echo '
 | 
								echo '
 | 
				
			||||||
						<figure id="photo-figure" class="', $className, '">',
 | 
											<img src="', $this->photo->getThumbnailUrl(1280, null), '" alt="">';
 | 
				
			||||||
				$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 '
 | 
				
			||||||
 | 
											<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>';
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		echo '
 | 
						private function taggedPeople()
 | 
				
			||||||
				</ul>';
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	private function printTags($header, $tagKind, $allowLinkingNewTags)
 | 
					 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		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>';
 | 
				
			||||||
 | 
					 | 
				
			||||||
		if ($allowLinkingNewTags)
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			$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);
 | 
				
			||||||
							}
 | 
												}
 | 
				
			||||||
						});
 | 
											});
 | 
				
			||||||
@ -241,24 +229,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>';
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -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,25 @@ 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.
 | 
							echo '
 | 
				
			||||||
		$aspectRatio = isset($width, $height) ? $width / $height : $image->ratio();
 | 
									<div class="', $className, '">';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if ($this->show_edit_buttons)
 | 
				
			||||||
 | 
								echo '
 | 
				
			||||||
 | 
										<a class="edit" href="', BASEURL, '/editasset/?id=', $image->getId(), '">Edit</a>';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		echo '
 | 
							echo '
 | 
				
			||||||
				<div class="polaroid ', $className, '" style="aspect-ratio: ', $aspectRatio, '">';
 | 
										<a href="', $image->getPageUrl(), $this->url_suffix, '">
 | 
				
			||||||
 | 
											<img src="', $image->getThumbnailUrl($width, $height, $crop, $fit), '" alt="" title="', $image->getTitle(), '">';
 | 
				
			||||||
		if ($this->show_edit_buttons && $image->canBeEditedBy(Registry::get('user')))
 | 
					 | 
				
			||||||
			$this->editMenu($image);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		echo '
 | 
					 | 
				
			||||||
					<a href="', $image->getPageUrl(), $this->photo_url_suffix, '#photo_frame">';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		foreach (['normal-photo', 'blur-photo'] as $className)
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			echo '
 | 
					 | 
				
			||||||
						<img src="', $image->getThumbnailUrl($width, $height, $crop, $fit), '"';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			// Can we offer double-density thumbs?
 | 
					 | 
				
			||||||
			if ($image->width() >= $width * 2 && $image->height() >= $height * 2)
 | 
					 | 
				
			||||||
				echo ' srcset="', $image->getThumbnailUrl($width * 2, $height * 2, $crop, $fit), ' 2x"';
 | 
					 | 
				
			||||||
			else
 | 
					 | 
				
			||||||
				echo ' srcset="', $image->getThumbnailUrl($image->width(), $image->height(), true), ' 2x"';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			echo ' alt="" title="', $image->getTitle(), '" class="', $className, '" style="aspect-ratio: ', $aspectRatio, '">';
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if ($this->show_labels)
 | 
							if ($this->show_labels)
 | 
				
			||||||
			echo '
 | 
								echo '
 | 
				
			||||||
@ -146,211 +103,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 '
 | 
							echo '
 | 
				
			||||||
							</div>';
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		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 '
 | 
							echo '
 | 
				
			||||||
							</div>';
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		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 '
 | 
							echo '
 | 
				
			||||||
				</div>';
 | 
									</div>';
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		echo '
 | 
						protected function single(array $photos)
 | 
				
			||||||
				</div>';
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	protected function dualMixed(array $photos, $altLayout)
 | 
					 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		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;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -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;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -3,132 +3,64 @@
 | 
				
			|||||||
 * TabularData.php
 | 
					 * TabularData.php
 | 
				
			||||||
 * Contains the template that displays tabular data.
 | 
					 * Contains the template that displays tabular data.
 | 
				
			||||||
 *
 | 
					 *
 | 
				
			||||||
 * Kabuki CMS (C) 2013-2025, Aaron van Geffen
 | 
					 * Kabuki CMS (C) 2013-2015, Aaron van Geffen
 | 
				
			||||||
 *****************************************************************************/
 | 
					 *****************************************************************************/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class TabularData extends SubTemplate
 | 
					class TabularData extends SubTemplate
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
	protected 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()
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		$this->renderTitle();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		foreach ($this->_subtemplates as $template)
 | 
					 | 
				
			||||||
			$template->html_main();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Showing an inline form?
 | 
					 | 
				
			||||||
		$pager = $this->_t->getPageIndex();
 | 
					 | 
				
			||||||
		if (!empty($pager) || isset($this->_t->form_above))
 | 
					 | 
				
			||||||
			$this->renderPaginationForm($pager, $this->_t->form_above);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		$tableClass = $this->_t->getTableClass();
 | 
					 | 
				
			||||||
		if ($tableClass)
 | 
					 | 
				
			||||||
		echo '
 | 
							echo '
 | 
				
			||||||
			<div class="', $tableClass, '">';
 | 
								<div class="admin_box">';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							$title = $this->_t->getTitle();
 | 
				
			||||||
 | 
							if (!empty($title))
 | 
				
			||||||
 | 
								echo '
 | 
				
			||||||
 | 
									<h2>', $title, '</h2>';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Showing a page index?
 | 
				
			||||||
 | 
							if (isset($this->pager))
 | 
				
			||||||
 | 
								$this->pager->html_content();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Maybe even a small form?
 | 
				
			||||||
 | 
							if (isset($this->_t->form_above))
 | 
				
			||||||
 | 
								$this->showForm($this->_t->form_above);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Build the table!
 | 
							// Build the table!
 | 
				
			||||||
		echo '
 | 
							echo '
 | 
				
			||||||
				<table class="table table-striped table-condensed">';
 | 
									<table class="table table-striped">
 | 
				
			||||||
 | 
					 | 
				
			||||||
		$this->renderTableHead($this->_t->getHeader());
 | 
					 | 
				
			||||||
		$this->renderTableBody($this->_t->getBody());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		echo '
 | 
					 | 
				
			||||||
				</table>';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if ($tableClass)
 | 
					 | 
				
			||||||
			echo '
 | 
					 | 
				
			||||||
			</div>';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Showing an inline form?
 | 
					 | 
				
			||||||
		if (!empty($pager) || isset($this->_t->form_below))
 | 
					 | 
				
			||||||
			$this->renderPaginationForm($pager, $this->_t->form_below);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		$title = $this->_t->getTitle();
 | 
					 | 
				
			||||||
		if (!empty($title))
 | 
					 | 
				
			||||||
			echo '
 | 
					 | 
				
			||||||
			</div>';
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	protected function renderTitle()
 | 
					 | 
				
			||||||
	{
 | 
					 | 
				
			||||||
		$title = $this->_t->getTitle();
 | 
					 | 
				
			||||||
		if (!empty($title))
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			$titleclass = $this->_t->getTitleClass();
 | 
					 | 
				
			||||||
			echo '
 | 
					 | 
				
			||||||
			<div class="generic-table', !empty($titleclass) ? ' ' . $titleclass : '', '">
 | 
					 | 
				
			||||||
				<h1>', htmlspecialchars($title), '</h1>';
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	protected function renderPaginationForm($pager, $form)
 | 
					 | 
				
			||||||
	{
 | 
					 | 
				
			||||||
		echo '
 | 
					 | 
				
			||||||
			<div class="row clearfix justify-content-end">';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Page index?
 | 
					 | 
				
			||||||
		if (!empty($pager))
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			echo '
 | 
					 | 
				
			||||||
				<div class="col-md">';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			PageIndexWidget::paginate($pager);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			echo '
 | 
					 | 
				
			||||||
				</div>';
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Form controls?
 | 
					 | 
				
			||||||
		if (isset($form))
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			echo '
 | 
					 | 
				
			||||||
				<div class="col-md-auto">';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			InlineFormView::renderInlineForm($form);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			echo '
 | 
					 | 
				
			||||||
				</div>';
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		echo '
 | 
					 | 
				
			||||||
			</div>';
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	protected function renderTableHead(array $headers)
 | 
					 | 
				
			||||||
	{
 | 
					 | 
				
			||||||
		echo '
 | 
					 | 
				
			||||||
					<thead>
 | 
										<thead>
 | 
				
			||||||
						<tr>';
 | 
											<tr>';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		foreach ($headers as $th)
 | 
							// Show the table's headers.
 | 
				
			||||||
 | 
							foreach ($this->_t->getHeader() 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' ? '↑' : '↓';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			echo '</th>';
 | 
								echo '</th>';
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		echo '
 | 
							echo '
 | 
				
			||||||
						</tr>
 | 
											</tr>
 | 
				
			||||||
					</thead>';
 | 
										</thead>
 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	protected function renderTableBody($body)
 | 
					 | 
				
			||||||
	{
 | 
					 | 
				
			||||||
		echo '
 | 
					 | 
				
			||||||
					<tbody>';
 | 
										<tbody>';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Show the table's body.
 | 
				
			||||||
 | 
							$body = $this->_t->getBody();
 | 
				
			||||||
		if (is_array($body))
 | 
							if (is_array($body))
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			foreach ($body as $tr)
 | 
								foreach ($body as $tr)
 | 
				
			||||||
@ -137,29 +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',
 | 
												<td', (!empty($td['width']) ? ' width="' . $td['width'] . '"' : ''), '>', $td['value'], '</td>';
 | 
				
			||||||
								(!empty($td['class']) ? ' class="' . $td['class'] . '"' : ''),
 | 
					 | 
				
			||||||
								(!empty($td['width']) ? ' width="' . $td['width'] . '"' : ''), '>',
 | 
					 | 
				
			||||||
								$td['value'],
 | 
					 | 
				
			||||||
							'</td>';
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
				echo '
 | 
									echo '
 | 
				
			||||||
						</tr>';
 | 
											</tr>';
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		else
 | 
							else
 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			$header = $this->_t->getHeader();
 | 
					 | 
				
			||||||
			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>';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Maybe another small form?
 | 
				
			||||||
 | 
							if (isset($this->_t->form_below))
 | 
				
			||||||
 | 
								$this->showForm($this->_t->form_below);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Showing a page index?
 | 
				
			||||||
 | 
							if (isset($this->pager))
 | 
				
			||||||
 | 
								$this->pager->html_content();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							echo '
 | 
				
			||||||
 | 
								</div>';
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						protected function showForm($form)
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							echo '
 | 
				
			||||||
 | 
									<form action="', $form['action'], '" method="', $form['method'], '" class="table_form ', $form['class'], '">';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (!empty($form['fields']))
 | 
				
			||||||
 | 
								foreach ($form['fields'] as $name => $field)
 | 
				
			||||||
 | 
									echo '
 | 
				
			||||||
 | 
										<input name="', $name, '" type="', $field['type'], '" placeholder="', $field['placeholder'], '"', isset($field['class']) ? ' class="' . $field['class'] . '"' : '', isset($field['value']) ? ' value="' . $field['value'] . '"' : '', '>';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (!empty($form['buttons']))
 | 
				
			||||||
 | 
								foreach ($form['buttons'] as $name => $button)
 | 
				
			||||||
 | 
									echo '
 | 
				
			||||||
 | 
										<input name="', $name, '" type="', $button['type'], '" value="', $button['caption'], '" class="btn', isset($button['class']) ? ' ' . $button['class'] . '' : '', '">';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							echo '
 | 
				
			||||||
 | 
									</form>';
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -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
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										29
									
								
								templates/WarningDialog.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								templates/WarningDialog.php
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,29 @@
 | 
				
			|||||||
 | 
					<?php
 | 
				
			||||||
 | 
					/*****************************************************************************
 | 
				
			||||||
 | 
					 * WarningDialog.php
 | 
				
			||||||
 | 
					 * Defines the WarningDialog template.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * Kabuki CMS (C) 2013-2015, Aaron van Geffen
 | 
				
			||||||
 | 
					 *****************************************************************************/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class WarningDialog extends Alert
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						protected $buttons;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public function __construct($title = '', $message = '', $buttons = [])
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							parent::__construct($title, $message);
 | 
				
			||||||
 | 
							$this->buttons = $buttons;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						protected function additional_alert_content()
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							$this->addButtons();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						private function addButtons()
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							foreach ($this->buttons as $button)
 | 
				
			||||||
 | 
								$button->html_content();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user