diff --git a/composer.json b/composer.json index 2ad1e40..a4b52e4 100644 --- a/composer.json +++ b/composer.json @@ -19,6 +19,9 @@ "ext-mysqli": "*", "ext-imagick": "*", "ext-gd": "*", - "ext-fileinfo": "*" + "ext-imagick": "*", + "ext-mysqli": "*", + "twbs/bootstrap": "^5.3", + "twbs/bootstrap-icons": "^1.10" } } diff --git a/controllers/AccountSettings.php b/controllers/AccountSettings.php new file mode 100644 index 0000000..4f4a431 --- /dev/null +++ b/controllers/AccountSettings.php @@ -0,0 +1,135 @@ +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' => '

To change your password, please fill out the fields below.

', + '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); + } + } +} diff --git a/controllers/EditAlbum.php b/controllers/EditAlbum.php index 26e2c6a..9ac4e23 100644 --- a/controllers/EditAlbum.php +++ b/controllers/EditAlbum.php @@ -18,6 +18,9 @@ class EditAlbum extends HTMLController if (empty($id_tag) && !isset($_GET['add']) && $_GET['action'] !== 'addalbum') throw new UnexpectedValueException('Requested album not found or not requesting a new album.'); + if (!empty($id_tag)) + $album = Tag::fromId($id_tag); + // Adding an album? if (isset($_GET['add']) || $_GET['action'] === 'addalbum') { @@ -29,7 +32,6 @@ class EditAlbum extends HTMLController elseif (isset($_GET['delete'])) { // So far so good? - $album = Tag::fromId($id_tag); if (Session::validateSession('get') && $album->kind === 'Album' && $album->delete()) { header('Location: ' . BASEURL . '/managealbums/'); @@ -41,7 +43,6 @@ class EditAlbum extends HTMLController // Editing one, then, surely. else { - $album = Tag::fromId($id_tag); if ($album->kind !== 'Album') trigger_error('Cannot edit album: not an album.', E_USER_ERROR); @@ -61,41 +62,68 @@ class EditAlbum extends HTMLController elseif (!$id_tag) $after_form = ''; + // Gather possible parents for this album to be filed into + $parentChoices = [0 => '-root-']; + foreach (PhotoAlbum::getHierarchy('tag', 'up') as $parent) + { + if (!empty($id_tag) && $parent['id_tag'] == $id_tag) + continue; + + $parentChoices[$parent['id_tag']] = $parent['tag']; + } + + $fields = [ + 'id_parent' => [ + 'type' => 'select', + 'label' => 'Parent album', + 'options' => $parentChoices, + ], + 'id_asset_thumb' => [ + 'type' => 'numeric', + 'label' => 'Thumbnail asset ID', + 'is_optional' => true, + ], + 'tag' => [ + 'type' => 'text', + 'label' => 'Album title', + 'size' => 50, + 'maxlength' => 255, + ], + 'slug' => [ + 'type' => 'text', + 'label' => 'URL slug', + 'size' => 50, + 'maxlength' => 255, + ], + 'description' => [ + 'type' => 'textbox', + 'label' => 'Description', + 'size' => 50, + 'maxlength' => 255, + 'is_optional' => true, + ], + ]; + + // Fetch image assets for this album + if (!empty($id_tag)) + { + list($assets, $num_assets) = AssetIterator::getByOptions([ + 'direction' => 'desc', + 'limit' => 500, + 'id_tag' => $id_tag, + ], true); + + if ($num_assets > 0) + unset($fields['id_asset_thumb']); + } + $form = new Form([ 'request_url' => BASEURL . '/editalbum/?' . ($id_tag ? 'id=' . $id_tag : 'add'), 'content_below' => $after_form, - 'fields' => [ - 'id_parent' => [ - 'type' => 'numeric', - 'label' => 'Parent album ID', - ], - 'id_asset_thumb' => [ - 'type' => 'numeric', - 'label' => 'Thumbnail asset ID', - 'is_optional' => true, - ], - 'tag' => [ - 'type' => 'text', - 'label' => 'Album title', - 'size' => 50, - 'maxlength' => 255, - ], - 'slug' => [ - 'type' => 'text', - 'label' => 'URL slug', - 'size' => 50, - 'maxlength' => 255, - ], - 'description' => [ - 'type' => 'textbox', - 'label' => 'Description', - 'size' => 50, - 'maxlength' => 255, - 'is_optional' => true, - ], - ], + 'fields' => $fields, ]); + // Add defaults for album if none present if (empty($_POST) && isset($_GET['tag'])) { $parentTag = Tag::fromId($_GET['tag']); @@ -117,16 +145,46 @@ class EditAlbum extends HTMLController $formview = new FormView($form, $form_title ?? ''); $this->page->adopt($formview); + // If we have asset images, show the thumbnail manager + if (!empty($id_tag) && $num_assets > 0) + $this->page->adopt(new FeaturedThumbnailManager($assets, $id_tag ? $album->id_asset_thumb : 0)); + + if (isset($_POST['changeThumbnail'])) + $this->processThumbnail($album); + elseif (!empty($_POST)) + $this->processTagDetails($form, $id_tag, $album ?? null); + } + + private function processThumbnail($tag) + { + if (empty($_POST)) + return; + + $tag->id_asset_thumb = $_POST['featuredThumbnail']; + $tag->save(); + + header('Location: ' . BASEURL . '/editalbum/?id=' . $tag->id_tag); + exit; + } + + private function processTagDetails($form, $id_tag, $album) + { if (!empty($_POST)) { $form->verify($_POST); // Anything missing? if (!empty($form->getMissing())) - return $formview->adopt(new Alert('Some data missing', 'Please fill out the following fields: ' . implode(', ', $form->getMissing()), 'error')); + return $formview->adopt(new Alert('Some data missing', 'Please fill out the following fields: ' . implode(', ', $form->getMissing()), 'danger')); $data = $form->getData(); + // Sanity check: don't let an album be its own parent + if ($data['id_parent'] == $id_tag) + { + return $formview->adopt(new Alert('Invalid parent', 'An album cannot be its own parent.', 'danger')); + } + // Quick stripping. $data['tag'] = htmlentities($data['tag']); $data['description'] = htmlentities($data['description']); @@ -140,7 +198,7 @@ class EditAlbum extends HTMLController $data['kind'] = 'Album'; $newTag = Tag::createNew($data); if ($newTag === false) - return $formview->adopt(new Alert('Cannot create this album', 'Something went wrong while creating the album...', 'error')); + return $formview->adopt(new Alert('Cannot create this album', 'Something went wrong while creating the album...', 'danger')); if (isset($_POST['submit_and_new'])) { diff --git a/controllers/EditAsset.php b/controllers/EditAsset.php index 8fc7bf4..a6ca7f9 100644 --- a/controllers/EditAsset.php +++ b/controllers/EditAsset.php @@ -94,10 +94,6 @@ class EditAsset extends HTMLController $page = new EditAssetForm($asset, $thumbs); parent::__construct('Edit asset \'' . $asset->getTitle() . '\' (' . $asset->getFilename() . ') - ' . SITE_TITLE); $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) diff --git a/controllers/EditTag.php b/controllers/EditTag.php index 24126c3..364a6f4 100644 --- a/controllers/EditTag.php +++ b/controllers/EditTag.php @@ -10,14 +10,18 @@ class EditTag extends HTMLController { public function __construct() { - // Ensure it's just admins at this point. - if (!Registry::get('user')->isAdmin()) - throw new NotAllowedException(); - $id_tag = isset($_GET['id']) ? (int) $_GET['id'] : 0; if (empty($id_tag) && !isset($_GET['add'])) throw new UnexpectedValueException('Requested tag not found or not requesting a new tag.'); + if (!empty($id_tag)) + $tag = Tag::fromId($id_tag); + + // Are we allowed to edit this tag? + $user = Registry::get('user'); + if (!($user->isAdmin() || $user->getUserId() == $tag->id_user_owner)) + throw new NotAllowedException(); + // Adding an tag? if (isset($_GET['add'])) { @@ -29,7 +33,6 @@ class EditTag extends HTMLController elseif (isset($_GET['delete'])) { // So far so good? - $tag = Tag::fromId($id_tag); if (Session::validateSession('get') && $tag->kind !== 'Album' && $tag->delete()) { header('Location: ' . BASEURL . '/managetags/'); @@ -41,7 +44,6 @@ class EditTag extends HTMLController // Editing one, then, surely. else { - $tag = Tag::fromId($id_tag); if ($tag->kind === 'Album') trigger_error('Cannot edit tag: is actually an album.', E_USER_ERROR); @@ -61,47 +63,51 @@ class EditTag extends HTMLController elseif (!$id_tag) $after_form = ''; + $fields = [ + 'kind' => [ + 'type' => 'select', + 'label' => 'Kind of tag', + 'options' => [ + 'Location' => 'Location', + 'Person' => 'Person', + ], + ], + 'id_user_owner' => [ + 'type' => 'select', + 'label' => 'Owner', + 'options' => [0 => '(nobody)'] + Member::getMemberMap(), + ], + 'tag' => [ + 'type' => 'text', + 'label' => 'Tag title', + 'size' => 50, + 'maxlength' => 255, + ], + 'slug' => [ + 'type' => 'text', + 'label' => 'URL slug', + 'size' => 50, + 'maxlength' => 255, + ], + 'description' => [ + 'type' => 'textbox', + 'label' => 'Description', + 'size' => 50, + 'maxlength' => 255, + 'is_optional' => true, + ], + ]; + + if (!$user->isAdmin()) + { + unset($fields['kind']); + unset($fields['id_user_owner']); + } + $form = new Form([ 'request_url' => BASEURL . '/edittag/?' . ($id_tag ? 'id=' . $id_tag : 'add'), 'content_below' => $after_form, - 'fields' => [ - 'id_parent' => [ - 'type' => 'numeric', - 'label' => 'Parent tag ID', - ], - 'id_asset_thumb' => [ - 'type' => 'numeric', - 'label' => 'Thumbnail asset ID', - 'is_optional' => true, - ], - 'kind' => [ - 'type' => 'select', - 'label' => 'Kind of tag', - 'options' => [ - 'Location' => 'Location', - 'Person' => 'Person', - ], - ], - 'tag' => [ - 'type' => 'text', - 'label' => 'Tag title', - 'size' => 50, - 'maxlength' => 255, - ], - 'slug' => [ - 'type' => 'text', - 'label' => 'URL slug', - 'size' => 50, - 'maxlength' => 255, - ], - 'description' => [ - 'type' => 'textbox', - 'label' => 'Description', - 'size' => 50, - 'maxlength' => 255, - 'is_optional' => true, - ], - ], + 'fields' => $fields, ]); // Create the form, add in default values. @@ -109,15 +115,48 @@ class EditTag extends HTMLController $formview = new FormView($form, $form_title ?? ''); $this->page->adopt($formview); + if (!empty($id_tag)) + { + list($assets, $num_assets) = AssetIterator::getByOptions([ + 'direction' => 'desc', + 'limit' => 500, + 'id_tag' => $id_tag, + ], true); + + if ($num_assets > 0) + $this->page->adopt(new FeaturedThumbnailManager($assets, $id_tag ? $tag->id_asset_thumb : 0)); + } + + if (isset($_POST['changeThumbnail'])) + $this->processThumbnail($tag); + elseif (!empty($_POST)) + $this->processTagDetails($form, $id_tag, $tag ?? null); + } + + private function processThumbnail($tag) + { + if (empty($_POST)) + return; + + $tag->id_asset_thumb = $_POST['featuredThumbnail']; + $tag->save(); + + header('Location: ' . BASEURL . '/edittag/?id=' . $tag->id_tag); + exit; + } + + private function processTagDetails($form, $id_tag, $tag) + { if (!empty($_POST)) { $form->verify($_POST); // Anything missing? if (!empty($form->getMissing())) - return $formview->adopt(new Alert('Some data missing', 'Please fill out the following fields: ' . implode(', ', $form->getMissing()), 'error')); + return $formview->adopt(new Alert('Some data missing', 'Please fill out the following fields: ' . implode(', ', $form->getMissing()), 'danger')); $data = $form->getData(); + $data['id_parent'] = 0; // Quick stripping. $data['slug'] = strtr($data['slug'], [' ' => '-', '--' => '-', '&' => 'and', '=>' => '', "'" => "", ":"=> "", '/' => '-', '\\' => '-']); @@ -127,7 +166,7 @@ class EditTag extends HTMLController { $return = Tag::createNew($data); if ($return === false) - return $formview->adopt(new Alert('Cannot create this tag', 'Something went wrong while creating the tag...', 'error')); + return $formview->adopt(new Alert('Cannot create this tag', 'Something went wrong while creating the tag...', 'danger')); if (isset($_POST['submit_and_new'])) { @@ -144,8 +183,11 @@ class EditTag extends HTMLController $tag->save(); } - // Redirect to the tag management page. - header('Location: ' . BASEURL . '/managetags/'); + // Redirect to a clean page + if (Registry::get('user')->isAdmin()) + header('Location: ' . BASEURL . '/managetags/'); + else + header('Location: ' . BASEURL . '/edittag/?id=' . $id_tag); exit; } } diff --git a/controllers/EditUser.php b/controllers/EditUser.php index bbce589..c5f6578 100644 --- a/controllers/EditUser.php +++ b/controllers/EditUser.php @@ -129,7 +129,7 @@ class EditUser extends HTMLController // Anything missing? if (!empty($form->getMissing())) - return $formview->adopt(new Alert('Some data missing', 'Please fill out the following fields: ' . implode(', ', $form->getMissing()), 'error')); + return $formview->adopt(new Alert('Some data missing', 'Please fill out the following fields: ' . implode(', ', $form->getMissing()), 'danger')); $data = $form->getData(); @@ -150,18 +150,18 @@ class EditUser extends HTMLController // 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.', 'error')); + 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']) && 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.', 'error')); + return $formview->adopt(new Alert('Email address already in use', 'Another account is already using the e-mail address you entered.', 'danger')); // Setting passwords? We'll need two! if (!$id_user || !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 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')); + return $formview->adopt(new Alert('Password not acceptable', 'Please fill in a password that is at least six characters long and contains at least one non-alphabetic character (e.g. a number or symbol).', 'danger')); elseif ($data['password1'] !== $data['password2']) - return $formview->adopt(new Alert('Passwords do not match', 'The passwords you entered do not match. Please try again.', 'error')); + return $formview->adopt(new Alert('Passwords do not match', 'The passwords you entered do not match. Please try again.', 'danger')); else $data['password'] = $data['password1']; @@ -173,7 +173,7 @@ class EditUser extends HTMLController { $return = Member::createNew($data); if ($return === false) - return $formview->adopt(new Alert('Cannot create this user', 'Something went wrong while creating the user...', 'error')); + return $formview->adopt(new Alert('Cannot create this user', 'Something went wrong while creating the user...', 'danger')); if (isset($_POST['submit_and_new'])) { diff --git a/controllers/HTMLController.php b/controllers/HTMLController.php index d03bc50..f141638 100644 --- a/controllers/HTMLController.php +++ b/controllers/HTMLController.php @@ -12,7 +12,6 @@ abstract class HTMLController { protected $page; - protected $admin_bar; public function __construct($title) { @@ -22,8 +21,6 @@ abstract class HTMLController if (Registry::get('user')->isAdmin()) { $this->page->appendStylesheet(BASEURL . '/css/admin.css'); - $this->admin_bar = new AdminBar(); - $this->page->adopt($this->admin_bar); } } diff --git a/controllers/Login.php b/controllers/Login.php index 46e5da6..d91785d 100644 --- a/controllers/Login.php +++ b/controllers/Login.php @@ -44,7 +44,7 @@ class Login extends HTMLController parent::__construct('Log in - ' . SITE_TITLE); $form = new LogInForm('Log in'); if ($login_error) - $form->adopt(new Alert('', 'Invalid email address or password.', 'error')); + $form->adopt(new Alert('', 'Invalid email address or password.', 'danger')); // Tried anything? Be helpful, at least. if (isset($_POST['emailaddress'])) diff --git a/controllers/ManageAlbums.php b/controllers/ManageAlbums.php index f6a1b59..662714b 100644 --- a/controllers/ManageAlbums.php +++ b/controllers/ManageAlbums.php @@ -18,7 +18,7 @@ class ManageAlbums extends HTMLController 'form' => [ 'action' => BASEURL . '/editalbum/', 'method' => 'get', - 'class' => 'floatright', + 'class' => 'col-md-6 text-end', 'buttons' => [ 'add' => [ 'type' => 'submit', @@ -60,7 +60,7 @@ class ManageAlbums extends HTMLController 'title' => 'Manage albums', 'no_items_label' => 'No albums meet the requirements of the current filter.', 'items_per_page' => 9999, - 'index_class' => 'floatleft', + 'index_class' => 'col-md-6', 'base_url' => BASEURL . '/managealbums/', 'get_data' => function($offset = 0, $limit = 9999, $order = '', $direction = 'up') { if (!in_array($order, ['id_tag', 'tag', 'slug', 'count'])) @@ -68,28 +68,7 @@ class ManageAlbums extends HTMLController 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); + $rows = PhotoAlbum::getHierarchy($order, $direction); return [ 'rows' => $rows, @@ -106,42 +85,4 @@ class ManageAlbums extends HTMLController parent::__construct('Album management - Page ' . $table->getCurrentPage() .' - ' . SITE_TITLE); $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; - } } diff --git a/controllers/ManageAssets.php b/controllers/ManageAssets.php index 1afc85b..2c2bfdc 100644 --- a/controllers/ManageAssets.php +++ b/controllers/ManageAssets.php @@ -14,10 +14,37 @@ class ManageAssets extends HTMLController if (!Registry::get('user')->isAdmin()) throw new NotAllowedException(); + if (isset($_POST['deleteChecked'], $_POST['delete']) && Session::validateSession()) + $this->handleAssetDeletion(); + Session::resetSessionToken(); $options = [ + 'form' => [ + 'action' => BASEURL . '/manageassets/?' . Session::getSessionTokenKey() . '=' . Session::getSessionToken(), + 'method' => 'post', + 'class' => 'col-md-6 text-end', + 'is_embed' => true, + 'buttons' => [ + 'deleteChecked' => [ + 'type' => 'submit', + 'caption' => 'Delete checked', + 'class' => 'btn-danger', + 'onclick' => 'return confirm(\'Are you sure you want to delete these items?\')', + ], + ], + ], 'columns' => [ + 'checkbox' => [ + 'header' => '', + 'is_sortable' => false, + 'parse' => [ + 'type' => 'function', + 'data' => function($row) { + return ''; + }, + ], + ], 'id_asset' => [ 'value' => 'id_asset', 'header' => 'ID', @@ -38,13 +65,18 @@ class ManageAssets extends HTMLController 'data' => 'filename', ], ], - 'title' => [ - 'header' => 'Title', + 'id_user_uploaded' => [ + 'header' => 'User uploaded', 'is_sortable' => true, 'parse' => [ - 'type' => 'value', - 'link' => BASEURL . '/editasset/?id={ID_ASSET}', - 'data' => 'title', + 'type' => 'function', + 'data' => function($row) { + if (!empty($row['id_user'])) + return sprintf('%s', BASEURL, $row['id_user'], + $row['first_name'] . ' ' . $row['surname']); + else + return 'n/a'; + }, ], ], 'dimensions' => [ @@ -67,15 +99,18 @@ class ManageAssets extends HTMLController 'title' => 'Manage assets', 'no_items_label' => 'No assets meet the requirements of the current filter.', 'items_per_page' => 30, - 'index_class' => 'pull_left', + 'index_class' => 'col-md-6', 'base_url' => BASEURL . '/manageassets/', 'get_data' => function($offset = 0, $limit = 30, $order = '', $direction = 'down') { - if (!in_array($order, ['id_asset', 'title', 'subdir', 'filename'])) + if (!in_array($order, ['id_asset', 'id_user_uploaded', 'title', 'subdir', 'filename'])) $order = 'id_asset'; $data = Registry::get('db')->queryAssocs(' - SELECT id_asset, title, subdir, filename, image_width, image_height - FROM assets + SELECT a.id_asset, a.subdir, a.filename, + a.image_width, a.image_height, + u.id_user, u.first_name, u.surname + FROM assets AS a + LEFT JOIN users AS u ON a.id_user_uploaded = u.id_user ORDER BY {raw:order} LIMIT {int:offset}, {int:limit}', [ @@ -94,7 +129,25 @@ class ManageAssets extends HTMLController ]; $table = new GenericTable($options); - parent::__construct('Asset management - Page ' . $table->getCurrentPage() .''); - $this->page->adopt(new TabularData($table)); + parent::__construct('Asset management - Page ' . $table->getCurrentPage()); + + $wrapper = new AssetManagementWrapper(); + $this->page->adopt($wrapper); + $wrapper->adopt(new TabularData($table)); + } + + private function handleAssetDeletion() + { + if (!isset($_POST['delete']) || !is_array($_POST['delete'])) + throw new UnexpectedValueException(); + + foreach ($_POST['delete'] as $id_asset) + { + $asset = Asset::fromId($id_asset); + $asset->delete(); + } + + header('Location: ' . BASEURL . '/manageassets/'); + exit; } } diff --git a/controllers/ManageErrors.php b/controllers/ManageErrors.php index d45d37f..f510c7b 100644 --- a/controllers/ManageErrors.php +++ b/controllers/ManageErrors.php @@ -29,11 +29,12 @@ class ManageErrors extends HTMLController 'form' => [ 'action' => BASEURL . '/manageerrors/?' . Session::getSessionTokenKey() . '=' . Session::getSessionToken(), 'method' => 'post', - 'class' => 'floatright', + 'class' => 'col-md-6 text-end', 'buttons' => [ 'flush' => [ 'type' => 'submit', 'caption' => 'Delete all', + 'class' => 'btn-danger', ], ], ], @@ -99,7 +100,7 @@ class ManageErrors extends HTMLController 'sort_direction' => !empty($_GET['dir']) ? $_GET['dir'] : '', 'no_items_label' => "No errors to display -- we're all good!", 'items_per_page' => 20, - 'index_class' => 'floatleft', + 'index_class' => 'col-md-6', 'base_url' => BASEURL . '/manageerrors/', 'get_count' => 'ErrorLog::getCount', 'get_data' => function($offset = 0, $limit = 20, $order = '', $direction = 'down') { diff --git a/controllers/ManageTags.php b/controllers/ManageTags.php index b600f2d..ec12de0 100644 --- a/controllers/ManageTags.php +++ b/controllers/ManageTags.php @@ -14,11 +14,13 @@ class ManageTags extends HTMLController if (!Registry::get('user')->isAdmin()) throw new NotAllowedException(); + Session::resetSessionToken(); + $options = [ 'form' => [ 'action' => BASEURL . '/edittag/', 'method' => 'get', - 'class' => 'floatright', + 'class' => 'col-md-6 text-end', 'buttons' => [ 'add' => [ 'type' => 'submit', @@ -48,10 +50,19 @@ class ManageTags extends HTMLController 'data' => 'slug', ], ], - 'kind' => [ - 'header' => 'Kind', + 'id_user_owner' => [ + 'header' => 'Owning user', 'is_sortable' => true, - 'value' => 'kind', + 'parse' => [ + 'type' => 'function', + 'data' => function($row) { + if (!empty($row['id_user'])) + return sprintf('%s', BASEURL, $row['id_user'], + $row['first_name'] . ' ' . $row['surname']); + else + return 'n/a'; + }, + ], ], 'count' => [ 'header' => 'Cardinality', @@ -65,7 +76,7 @@ class ManageTags extends HTMLController 'title' => 'Manage tags', 'no_items_label' => 'No tags meet the requirements of the current filter.', 'items_per_page' => 30, - 'index_class' => 'floatleft', + 'index_class' => 'col-md-6', 'base_url' => BASEURL . '/managetags/', 'get_data' => function($offset = 0, $limit = 30, $order = '', $direction = 'up') { if (!in_array($order, ['id_tag', 'tag', 'slug', 'kind', 'count'])) @@ -74,8 +85,9 @@ class ManageTags extends HTMLController $direction = 'up'; $data = Registry::get('db')->queryAssocs(' - SELECT * - FROM tags + 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 != {string:album} ORDER BY {raw:order} LIMIT {int:offset}, {int:limit}', diff --git a/controllers/ManageUsers.php b/controllers/ManageUsers.php index d7fadbd..1be66a5 100644 --- a/controllers/ManageUsers.php +++ b/controllers/ManageUsers.php @@ -14,11 +14,13 @@ class ManageUsers extends HTMLController if (!Registry::get('user')->isAdmin()) throw new NotAllowedException(); + Session::resetSessionToken(); + $options = [ 'form' => [ 'action' => BASEURL . '/edituser/', 'method' => 'get', - 'class' => 'floatright', + 'class' => 'col-md-6 text-end', 'buttons' => [ 'add' => [ 'type' => 'submit', @@ -94,7 +96,7 @@ class ManageUsers extends HTMLController 'title' => 'Manage users', 'no_items_label' => 'No users meet the requirements of the current filter.', 'items_per_page' => 30, - 'index_class' => 'floatleft', + 'index_class' => 'col-md-6', 'base_url' => BASEURL . '/manageusers/', 'get_data' => function($offset = 0, $limit = 30, $order = '', $direction = 'down') { if (!in_array($order, ['id_user', 'surname', 'first_name', 'slug', 'emailaddress', 'last_action_time', 'ip_address', 'is_admin'])) diff --git a/controllers/ResetPassword.php b/controllers/ResetPassword.php index 7153498..24fa7b6 100644 --- a/controllers/ResetPassword.php +++ b/controllers/ResetPassword.php @@ -48,7 +48,7 @@ class ResetPassword extends HTMLController exit; } else - $form->adopt(new Alert('Some fields require your attention', '', 'error')); + $form->adopt(new Alert('Some fields require your attention', '', 'danger')); } } else @@ -63,7 +63,7 @@ class ResetPassword extends HTMLController $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')); + $form->adopt(new Alert('Invalid email address', 'The email address you provided could not be found in our system. Please try again.', 'danger')); return; } diff --git a/controllers/UploadMedia.php b/controllers/UploadMedia.php index 571047b..194cb9e 100644 --- a/controllers/UploadMedia.php +++ b/controllers/UploadMedia.php @@ -33,21 +33,20 @@ class UploadMedia extends HTMLController if (empty($uploaded_file)) continue; - // DIY slug club. - $slug = $tag->slug . '/' . strtr($uploaded_file['name'], [' ' => '-', '--' => '-', '&' => 'and', '=>' => '', "'" => "", ":"=> "", '\\' => '-']); - $asset = Asset::createNew([ 'filename_to_copy' => $uploaded_file['tmp_name'], 'preferred_filename' => $uploaded_file['name'], 'preferred_subdir' => $tag->slug, - 'slug' => $slug, ]); $new_ids[] = $asset->getId(); $asset->linkTags([$tag->id_tag]); - $tag->id_asset_thumb = $asset->getId(); - $tag->save(); + if (empty($tag->id_asset_thumb)) + { + $tag->id_asset_thumb = $asset->getId(); + $tag->save(); + } } if (isset($_REQUEST['format']) && $_REQUEST['format'] === 'json') diff --git a/controllers/ViewPeople.php b/controllers/ViewPeople.php index bf7d8ba..014ebcb 100644 --- a/controllers/ViewPeople.php +++ b/controllers/ViewPeople.php @@ -52,8 +52,9 @@ class ViewPeople extends HTMLController 'start' => $start, 'base_url' => BASEURL . '/people/', 'page_slug' => 'page/%PAGE%/', + 'index_class' => 'pagination-lg justify-content-center', ]); - $this->page->adopt(new Pagination($pagination)); + $this->page->adopt(new PageIndexWidget($pagination)); $this->page->setCanonicalUrl(BASEURL . '/people/' . ($page > 1 ? 'page/' . $page . '/' : '')); } diff --git a/controllers/ViewPhoto.php b/controllers/ViewPhoto.php index cff5ed6..5e29ea8 100644 --- a/controllers/ViewPhoto.php +++ b/controllers/ViewPhoto.php @@ -27,10 +27,6 @@ class ViewPhoto extends HTMLController $this->handleConfirmDelete($user, $author, $photo); else $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 handleConfirmDelete(User $user, User $author, Asset $photo) @@ -43,7 +39,7 @@ class ViewPhoto extends HTMLController $page = new ConfirmDeletePage($photo->getImage()); $this->page->adopt($page); } - else if (isset($_REQUEST['delete_confirmed'])) + elseif (isset($_REQUEST['delete_confirmed'])) { $album_url = $photo->getSubdir(); $photo->delete(); diff --git a/controllers/ViewPhotoAlbum.php b/controllers/ViewPhotoAlbum.php index aa0f10a..9f7c07e 100644 --- a/controllers/ViewPhotoAlbum.php +++ b/controllers/ViewPhotoAlbum.php @@ -60,27 +60,7 @@ class ViewPhotoAlbum extends HTMLController // Can we do fancy things here? // !!! TODO: permission system? - $buttons = []; - - if (Registry::get('user')->isLoggedIn()) - { - $buttons[] = [ - 'url' => BASEURL . '/download/?tag=' . $id_tag, - 'caption' => 'Download this album', - ]; - - $buttons[] = [ - 'url' => BASEURL . '/uploadmedia/?tag=' . $id_tag, - 'caption' => 'Upload new photos here', - ]; - } - if (Registry::get('user')->isAdmin()) - $buttons[] = [ - 'url' => BASEURL . '/addalbum/?tag=' . $id_tag, - 'caption' => 'Create new subalbum here', - ]; - - // Enough actions for a button box? + $buttons = $this->getAlbumButtons($id_tag, $tag ?? null); if (!empty($buttons)) $this->page->adopt(new AlbumButtonBox($buttons)); @@ -111,8 +91,9 @@ class ViewPhotoAlbum extends HTMLController 'start' => (isset($_GET['page']) ? $_GET['page'] - 1 : 0) * self::PER_PAGE, 'base_url' => BASEURL . '/' . (isset($_GET['tag']) ? $_GET['tag'] . '/' : ''), 'page_slug' => 'page/%PAGE%/', + 'index_class' => 'pagination-lg justify-content-center', ]); - $this->page->adopt(new Pagination($index)); + $this->page->adopt(new PageIndexWidget($index)); } // Set the canonical url. @@ -156,13 +137,67 @@ class ViewPhotoAlbum extends HTMLController 'id_tag' => $album['id_tag'], 'caption' => $album['tag'], 'link' => BASEURL . '/' . $album['slug'] . '/', - 'thumbnail' => !empty($album['id_asset_thumb']) ? $assets[$album['id_asset_thumb']]->getImage() : null, + 'thumbnail' => !empty($album['id_asset_thumb']) && isset($assets[$album['id_asset_thumb']]) + ? $assets[$album['id_asset_thumb']]->getImage() : null, ]; } return $albums; } + private function getAlbumButtons($id_tag, $tag) + { + $buttons = []; + $user = Registry::get('user'); + + if ($user->isLoggedIn()) + { + $buttons[] = [ + 'url' => BASEURL . '/download/?tag=' . $id_tag, + 'caption' => 'Download album', + ]; + } + + if (isset($tag)) + { + if ($tag->kind === 'Album') + { + $buttons[] = [ + 'url' => BASEURL . '/uploadmedia/?tag=' . $id_tag, + 'caption' => 'Upload photos here', + ]; + } + + if ($user->isAdmin()) + { + if ($tag->kind === 'Album') + { + $buttons[] = [ + 'url' => BASEURL . '/editalbum/?id=' . $id_tag, + 'caption' => 'Edit album', + ]; + } + elseif ($tag->kind === 'Person') + { + $buttons[] = [ + 'url' => BASEURL . '/edittag/?id=' . $id_tag, + 'caption' => 'Edit tag', + ]; + } + } + } + + if ($user->isAdmin() && (!isset($tag) || $tag->kind === 'Album')) + { + $buttons[] = [ + 'url' => BASEURL . '/addalbum/?tag=' . $id_tag, + 'caption' => 'Create subalbum', + ]; + } + + return $buttons; + } + public function __destruct() { if (isset($this->iterator)) diff --git a/controllers/ViewTimeline.php b/controllers/ViewTimeline.php index 3aa29b9..72dfd76 100644 --- a/controllers/ViewTimeline.php +++ b/controllers/ViewTimeline.php @@ -46,8 +46,9 @@ class ViewTimeline extends HTMLController 'start' => (isset($_GET['page']) ? $_GET['page'] - 1 : 0) * self::PER_PAGE, 'base_url' => BASEURL . '/timeline/', 'page_slug' => 'page/%PAGE%/', + 'index_class' => 'pagination-lg justify-content-center', ]); - $this->page->adopt(new Pagination($index)); + $this->page->adopt(new PageIndexWidget($index)); } // Set the canonical url. diff --git a/models/AdminMenu.php b/models/AdminMenu.php new file mode 100644 index 0000000..e0515d0 --- /dev/null +++ b/models/AdminMenu.php @@ -0,0 +1,62 @@ +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']; + } + } +} diff --git a/models/Asset.php b/models/Asset.php index 483c799..f107b4b 100644 --- a/models/Asset.php +++ b/models/Asset.php @@ -27,7 +27,10 @@ class Asset protected function __construct(array $data) { foreach ($data as $attribute => $value) - $this->$attribute = $value; + { + if (property_exists($this, $attribute)) + $this->$attribute = $value; + } if (!empty($data['date_captured']) && $data['date_captured'] !== 'NULL') $this->date_captured = new DateTime($data['date_captured']); @@ -184,9 +187,10 @@ class Asset $new_filename = $preferred_filename; $destination = ASSETSDIR . '/' . $preferred_subdir . '/' . $preferred_filename; - while (file_exists($destination)) + for ($i = 1; file_exists($destination); $i++) { - $filename = pathinfo($preferred_filename, PATHINFO_FILENAME) . '_' . mt_rand(10, 99); + $suffix = $i; + $filename = pathinfo($preferred_filename, PATHINFO_FILENAME) . ' (' . $suffix . ')'; $extension = pathinfo($preferred_filename, PATHINFO_EXTENSION); $new_filename = $filename . '.' . $extension; $destination = dirname($destination) . '/' . $new_filename; @@ -203,11 +207,14 @@ class Asset $mimetype = finfo_file($finfo, $destination); 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. - $title = isset($data['title']) ? $data['title'] : pathinfo($preferred_filename, PATHINFO_FILENAME); + $title = $data['title'] ?? $basename; // Same with the slug. - $slug = isset($data['slug']) ? $data['slug'] : $preferred_subdir . '/' . pathinfo($preferred_filename, PATHINFO_FILENAME); + $slug = $data['slug'] ?? sprintf('%s/%s', $preferred_subdir, $basename); // Detected an image? if (substr($mimetype, 0, 5) == 'image') @@ -483,9 +490,7 @@ class Asset { $db = Registry::get('db'); - if (!unlink(ASSETSDIR . '/' . $this->subdir . '/' . $this->filename)) - return false; - + // First: delete associated metadata $db->query(' DELETE FROM assets_meta WHERE id_asset = {int:id_asset}', @@ -493,6 +498,7 @@ class Asset 'id_asset' => $this->id_asset, ]); + // Second: figure out what tags to recount cardinality for $recount_tags = $db->queryValues(' SELECT id_tag FROM assets_tags @@ -510,13 +516,30 @@ class Asset Tag::recount($recount_tags); - $return = $db->query(' - DELETE FROM assets + // Third: figure out what associated thumbs to delete + $thumbs_to_delete = $db->queryValues(' + SELECT filename + FROM assets_thumbs WHERE id_asset = {int:id_asset}', [ 'id_asset' => $this->id_asset, ]); + foreach ($thumbs_to_delete as $filename) + { + $thumb_path = THUMBSDIR . '/' . $this->subdir . '/' . $filename; + if (is_file($thumb_path)) + unlink($thumb_path); + } + + $db->query(' + DELETE FROM assets_thumbs + WHERE id_asset = {int:id_asset}', + [ + 'id_asset' => $this->id_asset, + ]); + + // Reset asset ID for tags that use this asset for their thumbnail $rows = $db->query(' SELECT id_tag FROM tags @@ -534,6 +557,17 @@ class Asset } } + // Finally, delete the actual asset + if (!unlink(ASSETSDIR . '/' . $this->subdir . '/' . $this->filename)) + return false; + + $return = $db->query(' + DELETE FROM assets + WHERE id_asset = {int:id_asset}', + [ + 'id_asset' => $this->id_asset, + ]); + return $return; } diff --git a/models/Dispatcher.php b/models/Dispatcher.php index d5688ab..3a6b529 100644 --- a/models/Dispatcher.php +++ b/models/Dispatcher.php @@ -50,7 +50,7 @@ class Dispatcher public static function kickGuest($title = null, $message = null) { $form = new LogInForm('Log in'); - $form->adopt(new Alert($title ?? '', $message ?? 'You need to be logged in to view this page.', 'error')); + $form->adopt(new Alert($title ?? '', $message ?? 'You need to be logged in to view this page.', 'danger')); $form->setRedirectUrl($_SERVER['REQUEST_URI']); $page = new MainTemplate('Login required'); @@ -86,7 +86,6 @@ class Dispatcher 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!', '

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!

', 'errormsg')); diff --git a/models/ErrorHandler.php b/models/ErrorHandler.php index da650e9..3139444 100644 --- a/models/ErrorHandler.php +++ b/models/ErrorHandler.php @@ -168,7 +168,6 @@ class ErrorHandler if ($is_admin) { $page->appendStylesheet(BASEURL . '/css/admin.css'); - $page->adopt(new AdminBar()); } } elseif (!$is_sensitive) diff --git a/models/Form.php b/models/Form.php index 62952ae..3558533 100644 --- a/models/Form.php +++ b/models/Form.php @@ -3,7 +3,8 @@ * Form.php * Contains key class Form. * - * Kabuki CMS (C) 2013-2015, Aaron van Geffen + * Global Data Lab code (C) Radboud University Nijmegen + * Programming (C) Aaron van Geffen, 2015-2022 *****************************************************************************/ class Form @@ -12,9 +13,11 @@ class Form public $request_url; public $content_above; public $content_below; - private $fields; - private $data; - private $missing; + private $fields = []; + private $data = []; + private $missing = []; + private $submit_caption; + private $trim_inputs; // NOTE: this class does not verify the completeness of form options. public function __construct($options) @@ -24,9 +27,42 @@ class Form $this->fields = !empty($options['fields']) ? $options['fields'] : []; $this->content_below = !empty($options['content_below']) ? $options['content_below'] : null; $this->content_above = !empty($options['content_above']) ? $options['content_above'] : null; + $this->submit_caption = !empty($options['submit_caption']) ? $options['submit_caption'] : 'Save information'; + $this->trim_inputs = !empty($options['trim_inputs']); } - public function verify($post) + public function getFields() + { + return $this->fields; + } + + public function getData() + { + return $this->data; + } + + public function getSubmitButtonCaption() + { + return $this->submit_caption; + } + + public function getMissing() + { + return $this->missing; + } + + public function setData($data) + { + $this->verify($data, true); + $this->missing = []; + } + + public function setFieldAsMissing($field) + { + $this->missing[] = $field; + } + + public function verify($post, $initalisation = false) { $this->data = []; $this->missing = []; @@ -41,30 +77,43 @@ class Form } // No data present at all for this field? - if ((!isset($post[$field_id]) || $post[$field_id] == '') && empty($field['is_optional'])) + if ((!isset($post[$field_id]) || $post[$field_id] == '') && + $field['type'] !== 'captcha') { - $this->missing[] = $field_id; - $this->data[$field_id] = ''; + if (empty($field['is_optional'])) + $this->missing[] = $field_id; + + if ($field['type'] === 'select' && !empty($field['multiple'])) + $this->data[$field_id] = []; + else + $this->data[$field_id] = ''; + continue; } - // Verify data for all fields + // Should we trim this? + if ($this->trim_inputs && $field['type'] !== 'captcha' && empty($field['multiple'])) + $post[$field_id] = trim($post[$field_id]); + + // Using a custom validation function? + if (isset($field['validate']) && is_callable($field['validate'])) + { + // Validation functions can clean up the data if passed by reference + $this->data[$field_id] = $post[$field_id]; + + // Evaluate validation functions as boolean to see if data is missing + if (!$field['validate']($post[$field_id])) + $this->missing[] = $field_id; + + continue; + } + + // Verify data by field type switch ($field['type']) { case 'select': case 'radio': - // 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]; + $this->validateSelect($field_id, $field, $post); break; case 'checkbox': @@ -73,61 +122,22 @@ class Form break; case 'color': - // Colors are stored as a string of length 3 or 6 (hex) - if (!isset($post[$field_id]) || (strlen($post[$field_id]) != 3 && strlen($post[$field_id]) != 6)) - { - $this->missing[] = $field_id; - $this->data[$field_id] = ''; - continue 2; - } - else - $this->data[$field_id] = $post[$field_id]; + $this->validateColor($field_id, $field, $post); break; case 'file': - // Needs to be verified elsewhere! + // Asset needs to be processed out of POST! This is just a filename. + $this->data[$field_id] = isset($post[$field_id]) ? $post[$field_id] : ''; break; case 'numeric': - $data = isset($post[$field_id]) ? $post[$field_id] : ''; - // Do we need to check bounds? - if (isset($field['min_value']) && is_numeric($data)) - { - if (is_float($field['min_value']) && (float) $data < $field['min_value']) - { - $this->missing[] = $field_id; - $this->data[$field_id] = 0.0; - } - elseif (is_int($field['min_value']) && (int) $data < $field['min_value']) - { - $this->missing[] = $field_id; - $this->data[$field_id] = 0; - } - else - $this->data[$field_id] = $data; - } - elseif (isset($field['max_value']) && is_numeric($data)) - { - if (is_float($field['max_value']) && (float) $data > $field['max_value']) - { - $this->missing[] = $field_id; - $this->data[$field_id] = 0.0; - } - elseif (is_int($field['max_value']) && (int) $data > $field['max_value']) - { - $this->missing[] = $field_id; - $this->data[$field_id] = 0; - } - else - $this->data[$field_id] = $data; - } - // Does it look numeric? - elseif (is_numeric($data)) - { - $this->data[$field_id] = $data; - } - // Let's consider it missing, then. - else + $this->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; @@ -137,29 +147,200 @@ class Form case 'text': case 'textarea': default: - $this->data[$field_id] = isset($post[$field_id]) ? $post[$field_id] : ''; + $this->validateText($field_id, $field, $post); } } } - public function setData($data) + private function validateCaptcha($field_id) { - $this->verify($data); - $this->missing = []; + $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; + } } - public function getFields() + private function validateColor($field_id, array $field, array $post) { - return $this->fields; + // Colors are stored as a string of length 3 or 6 (hex) + if (!isset($post[$field_id]) || (strlen($post[$field_id]) != 3 && strlen($post[$field_id]) != 6)) + { + $this->missing[] = $field_id; + $this->data[$field_id] = ''; + } + else + $this->data[$field_id] = $post[$field_id]; } - public function getData() + private function validateNumeric($field_id, array $field, array $post) { - return $this->data; + $data = isset($post[$field_id]) ? $post[$field_id] : ''; + + // Sanity check: does this even look numeric? + if (!is_numeric($data)) + { + $this->missing[] = $field_id; + $this->data[$field_id] = 0; + return; + } + + // Do we need to a minimum bound? + if (isset($field['min_value'])) + { + if (is_float($field['min_value']) && (float) $data < $field['min_value']) + { + $this->missing[] = $field_id; + $this->data[$field_id] = 0.0; + } + elseif (is_int($field['min_value']) && (int) $data < $field['min_value']) + { + $this->missing[] = $field_id; + $this->data[$field_id] = 0; + } + } + + // What about a maximum bound? + if (isset($field['max_value'])) + { + if (is_float($field['max_value']) && (float) $data > $field['max_value']) + { + $this->missing[] = $field_id; + $this->data[$field_id] = 0.0; + } + elseif (is_int($field['max_value']) && (int) $data > $field['max_value']) + { + $this->missing[] = $field_id; + $this->data[$field_id] = 0; + } + } + + $this->data[$field_id] = $data; } - public function getMissing() + private function validateSelect($field_id, array $field, array $post) { - return $this->missing; + // Skip validation? Dangerous territory! + if (isset($field['verify_options']) && $field['verify_options'] === false) + { + $this->data[$field_id] = $post[$field_id]; + return; + } + + // Check whether selected option is valid. + if (($field['type'] !== 'select' || empty($field['multiple'])) && empty($field['has_groups'])) + { + if (isset($post[$field_id]) && !isset($field['options'][$post[$field_id]])) + { + $this->missing[] = $field_id; + $this->data[$field_id] = ''; + return; + } + else + $this->data[$field_id] = $post[$field_id]; + } + // Multiple selections involve a bit more work. + elseif (!empty($field['multiple']) && empty($field['has_groups'])) + { + $this->data[$field_id] = []; + if (!is_array($post[$field_id])) + { + if (isset($field['options'][$post[$field_id]])) + $this->data[$field_id][] = $post[$field_id]; + else + $this->missing[] = $field_id; + return; + } + + foreach ($post[$field_id] as $option) + { + if (isset($field['options'][$option])) + $this->data[$field_id][] = $option; + } + + if (empty($this->data[$field_id])) + $this->missing[] = $field_id; + } + // Any optgroups involved? + elseif (!empty($field['has_groups'])) + { + if (!isset($post[$field_id])) + { + $this->missing[] = $field_id; + $this->data[$field_id] = ''; + return; + } + + // Expensive: iterate over all groups until the value selected has been found. + foreach ($field['options'] as $label => $options) + { + if (is_array($options)) + { + // Consider each of the options as a valid a value. + foreach ($options as $value => $label) + { + if ($post[$field_id] === $value) + { + $this->data[$field_id] = $options; + return; + } + } + } + else + { + // This is an ungrouped value in disguise! Treat it as such. + if ($post[$field_id] === $options) + { + $this->data[$field_id] = $options; + return; + } + else + continue; + } + } + + // If we've reached this point, we'll consider the data invalid. + $this->missing[] = $field_id; + $this->data[$field_id] = ''; + } + else + { + throw new UnexpectedValueException('Unexpected field configuration in validateSelect!'); + } + } + + private function validateText($field_id, array $field, array $post) + { + $this->data[$field_id] = isset($post[$field_id]) ? $post[$field_id] : ''; + + // Trim leading and trailing whitespace? + if (!empty($field['trim'])) + $this->data[$field_id] = trim($this->data[$field_id]); + + // Is there a length limit to enforce? + if (isset($field['maxlength']) && strlen($post[$field_id]) > $field['maxlength']) { + $post[$field_id] = substr($post[$field_id], 0, $field['maxlength']); + } } } diff --git a/models/GenericTable.php b/models/GenericTable.php index f2147f7..d98220a 100644 --- a/models/GenericTable.php +++ b/models/GenericTable.php @@ -3,7 +3,8 @@ * GenericTable.php * Contains key class GenericTable. * - * Kabuki CMS (C) 2013-2015, Aaron van Geffen + * Global Data Lab code (C) Radboud University Nijmegen + * Programming (C) Aaron van Geffen, 2015-2021 *****************************************************************************/ class GenericTable @@ -19,7 +20,7 @@ class GenericTable public $form_above; public $form_below; - + private $table_class; private $sort_direction; private $sort_order; private $base_url; @@ -84,6 +85,8 @@ class GenericTable else $this->body = $options['no_items_label'] ?? ''; + $this->table_class = $options['table_class'] ?? ''; + // Got a title? $this->title = $options['title'] ?? ''; $this->title_class = $options['title_class'] ?? ''; @@ -105,6 +108,7 @@ class GenericTable $header = [ '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, 'href' => $isSortable ? $this->getLink($this->start, $key, $sortDirection) : null, 'label' => $column['header'], @@ -168,6 +172,11 @@ class GenericTable return $this->pageIndex; } + public function getTableClass() + { + return $this->table_class; + } + public function getTitle() { return $this->title; @@ -196,6 +205,7 @@ class GenericTable // Append the cell to the row. $newRow['cells'][] = [ + 'class' => $column['cell_class'] ?? '', 'value' => $value, ]; } diff --git a/models/MainMenu.php b/models/MainMenu.php new file mode 100644 index 0000000..e1ccb05 --- /dev/null +++ b/models/MainMenu.php @@ -0,0 +1,41 @@ +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']; + } + } +} diff --git a/models/Member.php b/models/Member.php index 9fc21f7..84912a4 100644 --- a/models/Member.php +++ b/models/Member.php @@ -110,6 +110,9 @@ class Member extends User $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(' UPDATE users SET @@ -120,7 +123,7 @@ class Member extends User password_hash = {string:password_hash}, is_admin = {int:is_admin} WHERE id_user = {int:id_user}', - get_object_vars($this)); + $params); } /** @@ -189,4 +192,15 @@ class Member extends User // We should probably phase out the use of this function, or refactor the access levels of member properties... return get_object_vars($this); } + + public static function getMemberMap() + { + return Registry::get('db')->queryPair(' + SELECT id_user, CONCAT(first_name, {string:blank}, surname) AS full_name + FROM users + ORDER BY first_name, surname', + [ + 'blank' => ' ', + ]); + } } diff --git a/models/Menu.php b/models/Menu.php new file mode 100644 index 0000000..cde4de0 --- /dev/null +++ b/models/Menu.php @@ -0,0 +1,18 @@ +items; + } +} diff --git a/models/PhotoAlbum.php b/models/PhotoAlbum.php new file mode 100644 index 0000000..efdc41f --- /dev/null +++ b/models/PhotoAlbum.php @@ -0,0 +1,76 @@ +query(' + SELECT * + FROM tags + WHERE kind = {string:album} + ORDER BY id_parent, {raw:order}', + [ + 'order' => $order . ($direction == 'up' ? ' ASC' : ' DESC'), + 'album' => 'Album', + ]); + + $albums_by_parent = []; + while ($row = $db->fetch_assoc($res)) + { + if (!isset($albums_by_parent[$row['id_parent']])) + $albums_by_parent[$row['id_parent']] = []; + + $albums_by_parent[$row['id_parent']][] = $row + ['children' => []]; + } + + $albums = self::getChildrenRecursively(0, 0, $albums_by_parent); + $rows = self::flattenChildrenRecursively($albums); + + return $rows; + } + + private static function getChildrenRecursively($id_parent, $level, &$albums_by_parent) + { + $children = []; + if (!isset($albums_by_parent[$id_parent])) + return $children; + + foreach ($albums_by_parent[$id_parent] as $child) + { + if (isset($albums_by_parent[$child['id_tag']])) + $child['children'] = self::getChildrenRecursively($child['id_tag'], $level + 1, $albums_by_parent); + + $child['tag'] = ($level ? str_repeat('—', $level * 2) . ' ' : '') . $child['tag']; + $children[] = $child; + } + + return $children; + } + + private static function flattenChildrenRecursively($albums) + { + if (empty($albums)) + return []; + + $rows = []; + foreach ($albums as $album) + { + $rows[] = array_intersect_key($album, array_flip(['id_tag', 'tag', 'slug', 'count'])); + if (!empty($album['children'])) + { + $children = self::flattenChildrenRecursively($album['children']); + foreach ($children as $child) + $rows[] = array_intersect_key($child, array_flip(['id_tag', 'tag', 'slug', 'count'])); + } + } + + return $rows; + } +} diff --git a/models/PhotoMosaic.php b/models/PhotoMosaic.php index 605bdb0..db86be7 100644 --- a/models/PhotoMosaic.php +++ b/models/PhotoMosaic.php @@ -79,7 +79,7 @@ class PhotoMosaic return -$priority_diff; // In other cases, we'll just show the newest first. - return $a->getDateCaptured() > $b->getDateCaptured() ? -1 : 1; + return $a->getDateCaptured() <=> $b->getDateCaptured(); } private static function daysApart(Image $a, Image $b) diff --git a/models/Router.php b/models/Router.php index ba54a61..da37499 100644 --- a/models/Router.php +++ b/models/Router.php @@ -11,6 +11,7 @@ class Router public static function route() { $possibleActions = [ + 'accountsettings' => 'AccountSettings', 'addalbum' => 'EditAlbum', 'albums' => 'ViewPhotoAlbums', 'editalbum' => 'EditAlbum', diff --git a/models/Tag.php b/models/Tag.php index e6b6f13..14696f3 100644 --- a/models/Tag.php +++ b/models/Tag.php @@ -11,6 +11,7 @@ class Tag public $id_tag; public $id_parent; public $id_asset_thumb; + public $id_user_owner; public $tag; public $slug; public $description; @@ -95,6 +96,25 @@ class Tag return $rows; } + public static function getAllByOwner($id_user_owner) + { + $db = Registry::get('db'); + $res = $db->query(' + SELECT * + FROM tags + WHERE id_user_owner = {int:id_user_owner} + ORDER BY tag', + [ + 'id_user_owner' => $id_user_owner, + ]); + + $objects = []; + while ($row = $db->fetch_assoc($res)) + $objects[$row['id_tag']] = new Tag($row); + + return $objects; + } + public static function getAlbums($id_parent = 0, $offset = 0, $limit = 24, $return_format = 'array') { $rows = Registry::get('db')->queryAssocs(' @@ -258,7 +278,8 @@ class Tag UPDATE tags SET id_parent = {int:id_parent}, - id_asset_thumb = {int:id_asset_thumb}, + id_asset_thumb = {int:id_asset_thumb},' . (isset($this->id_user_owner) ? ' + id_user_owner = {int:id_user_owner},' : '') . ' tag = {string:tag}, slug = {string:slug}, description = {string:description}, @@ -292,8 +313,7 @@ class Tag public function resetIdAsset() { $db = Registry::get('db'); - - $row = $db->query(' + $new_id = $db->queryValue(' SELECT MAX(id_asset) as new_id FROM assets_tags WHERE id_tag = {int:id_tag}', @@ -301,18 +321,12 @@ class Tag 'id_tag' => $this->id_tag, ]); - $new_id = 0; - if(!empty($row)) - { - $new_id = $row->fetch_assoc()['new_id']; - } - return $db->query(' UPDATE tags SET id_asset_thumb = {int:new_id} WHERE id_tag = {int:id_tag}', [ - 'new_id' => $new_id, + 'new_id' => $new_id ?? 0, 'id_tag' => $this->id_tag, ]); } diff --git a/models/UserMenu.php b/models/UserMenu.php new file mode 100644 index 0000000..0204322 --- /dev/null +++ b/models/UserMenu.php @@ -0,0 +1,60 @@ +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']; + } + } +} diff --git a/public/css/admin.css b/public/css/admin.css index f3f3b90..92c2736 100644 --- a/public/css/admin.css +++ b/public/css/admin.css @@ -1,126 +1,3 @@ -.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 { @@ -157,7 +34,7 @@ body { color: #fff; } #crop_editor input[type=number] { - width: 50px; + width: 75px; background: #555; color: #fff; } @@ -195,119 +72,3 @@ body { top: 400px; 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; -} diff --git a/public/css/default.css b/public/css/default.css index 0aadb89..93e7c58 100644 --- a/public/css/default.css +++ b/public/css/default.css @@ -4,35 +4,28 @@ * DO NOT COPY OR RE-USE WITHOUT EXPLICIT WRITTEN PERMISSION. THANK YOU. */ -@import url(//fonts.googleapis.com/css?family=Press+Start+2P); @import url(//fonts.googleapis.com/css?family=Open+Sans:400,400italic,700,700italic); +@import url('//fonts.googleapis.com/css2?family=Coda&display=swap'); @font-face { - font-family: 'Invaders'; - src: url('fonts/invaders.ttf') format('truetype'); - font-weight: normal; - font-style: normal; + font-family: 'Invaders'; + src: url('fonts/invaders.ttf') format('truetype'); + font-weight: normal; + font-style: normal; } body { - font: 13px/1.7 "Open Sans", sans-serif; - padding: 0 0 3em; - margin: 0; + font-family: "Open Sans", sans-serif; background: #aaa 0 -50% fixed; - background-image: radial-gradient(ellipse at top, #ccc 0%, #aaa 55%, #333 100%); + padding: 0 0 3rem; } -#wrapper, header { +#wrapper, header .container { width: 95%; - min-width: 900px; max-width: 1280px; margin: 0 auto; } -header { - overflow: auto; -} - a { color: #963626; text-decoration: none; @@ -41,101 +34,120 @@ a:hover { color: #262626; } -/* Logo ----------*/ -h1#logo { - color: #fff; - float: left; - font: 200 50px 'Press Start 2P', sans-serif; - margin: 40px 0 50px 10px; - padding: 0; - text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.6); +.page-link { + color: #b50707; + font-family: 'Coda', sans-serif; } -h1#logo:before { - color: #fff; - content: 'B'; - float: left; - font: 75px 'Invaders'; - margin: -4px 20px 0 0; - padding: 0; - text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.6); +.page-link:hover { + color: #a40d0d; } -a h1#logo { - text-decoration: none; +.active > .page-link, .page-link.active { + background-color: #990b0b; + border-color: #a40d0d; } -a:hover h1#logo, a:hover h1#logo:before { - text-shadow: 2px 2px 3px rgba(0, 0, 0, 0.6); + +.btn { + font-family: 'Coda', sans-serif; +} +.btn-primary { + --bs-btn-bg: #6c757d; + --bs-btn-border-color: #6c757d; + --bs-btn-hover-bg: #5c636a; + --bs-btn-hover-border-color: #565e64; + --bs-btn-focus-shadow-rgb: 130, 138, 145; + --bs-btn-active-bg: #565e64; + --bs-btn-active-border-color: #51585e; + --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + --bs-btn-disabled-bg: #6c757d; + --bs-btn-disabled-border-color: #6c757d; +} + +.dropdown-item.active, .dropdown-item:active { + background-color: #990b0b; } /* Navigation ---------------*/ -ul#nav { - margin: 55px 10px 0 0; - padding: 0; - float: right; - list-style: none; +#mainNav { + font-family: 'Coda', sans-serif; + margin-bottom: 4rem; } -ul#nav li { - float: left; +.nav-divider { + height: 2.5rem; + border-left: .1rem solid rgba(255,255,255, 0.2); + margin: 0 0.5rem; } -ul#nav li a { +.navbar-brand { + padding-left: 80px; + position: relative; +} +i.space-invader::before { color: #fff; - display: block; - float: left; - font: 200 20px 'Press Start 2P', sans-serif; - margin: 0 0 0 32px; - padding: 10px 0; - text-decoration: none; - text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.7); -} -ul#nav li a:hover { - text-shadow: 2px 2px 3px rgba(0, 0, 0, 0.6); -} - - -/* Pagination ----------------*/ -.pagination { - clear: both; - text-align: center; -} -.pagination ul { + content: 'B'; display: inline-block; - margin: 0; - padding: 0; - box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3); + font: 85px 'Invaders'; + height: 85px; + left: -25px; + position: absolute; + text-align: center; + text-shadow: 2px 2px 2px rgba(0, 0, 0, 0.6); + top: -5px; + transform: rotate(-5deg); + transition: 0.25s; + width: 110px; } -.pagination ul > li { - display: inline; +.navbar-brand:hover i.space-invader::before { + transform: rotate(5deg); } -.pagination ul > li > a, .pagination ul > li > span { - float: left; - font: 300 18px/2.2 "Open Sans", sans-serif; - padding: 6px 22px; - text-decoration: none; +i.space-invader.alt-1::before { + content: 'C'; +} +i.space-invader.alt-2::before { + content: 'D'; +} +i.space-invader.alt-3::before { + content: 'E'; +} +i.space-invader.alt-4::before { + content: 'H'; +} +i.space-invader.alt-5::before { + content: 'I'; +} +i.space-invader.alt-6::before { + content: 'N'; +} +i.space-invader.alt-7::before { + content: 'O'; +} +@media (max-width: 991px) { + .navbar-brand { + padding-left: 60px; + } + i.space-invader::before { + font-size: 50px; + left: -10px; + top: -7px; + width: 70px; + } +} + + +/* Content boxes +------------------*/ +.content-box { background-color: #fff; - border-right: 1px solid #ddd; + margin: 0 auto 2rem; + padding: 2rem; + border-radius: 0.5rem; + box-shadow: 0 0.25rem 0.5rem rgba(0,0,0,0.1); } - -.pagination ul > li > a:hover, .pagination ul > li > a:focus, -.pagination ul > .active > a, .pagination ul > .active > span { - background-color: #eee; -} - -.pagination ul > .active > a, .pagination ul > .active > span { - cursor: default; -} - -.pagination ul > .disabled > span, .pagination ul > .disabled > a, -.pagination ul > .disabled > a:hover, .pagination ul > .disabled > a:focus { - color: #999; - cursor: default; - background-color: transparent; -} - -.pagination .page-padding { - cursor: pointer; +.content-box h1, +.content-box h2, +.content-box h3 { + font-family: 'Coda', sans-serif; + margin-bottom: 0.5em; } @@ -144,11 +156,12 @@ ul#nav li a:hover { .tiled_header { background: #fff; box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3); + border-radius: 0.5rem; color: #000; clear: both; float: left; margin: 0 0 1.5% 0; - font: 400 18px/2.2 "Open Sans", sans-serif; + font: 400 18px/2.2 'Coda', sans-serif; padding: 6px 22px; text-overflow: ellipsis; white-space: nowrap; @@ -172,13 +185,15 @@ ul#nav li a:hover { width: 31%; } .tiled_grid div img { + background: url('../images/nothumb.svg') center no-repeat; border: none; + object-fit: cover; width: 100%; } .tiled_grid div h4 { color: #000; margin: 0; - font: 400 18px "Open Sans", sans-serif; + font: 400 18px 'Coda', sans-serif; padding: 15px 5px; text-overflow: ellipsis; text-align: center; @@ -285,15 +300,19 @@ ul#nav li a:hover { .album_title_box > a { background: #fff; box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3); + border-top-left-radius: 0.5rem; + border-bottom-left-radius: 0.5rem; display: inline-block; float: left; font-size: 2em; line-height: 1; - padding: 8px 10px 14px; + padding: 8px 10px; } .album_title_box > div { background: #fff; box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3); + border-top-right-radius: 0.5rem; + border-bottom-right-radius: 0.5rem; float: left; font: inherit; padding: 6px 22px; @@ -302,7 +321,7 @@ ul#nav li a:hover { } .album_title_box h2 { color: #262626; - font: 400 18px/2 "Open Sans", sans-serif !important; + font: 400 18px/2 'Coda', sans-serif !important; margin: 0; } .album_title_box p { @@ -314,29 +333,37 @@ ul#nav li a:hover { ---------------------*/ .album_button_box { float: right; - margin-bottom: 20px; + margin-bottom: 3rem; } .album_button_box > a { - background: #fff; box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3); - display: inline-block; - float: left; - font-size: 1em; padding: 8px 10px; margin-left: 12px; } -/* Generic boxed content ---------------------------*/ -.boxed_content { - background: #fff; - padding: 25px; - box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3); +/* (Tag) autosuggest +----------------------*/ +#new_tag_container { + display: block; + position: relative; } -.boxed_content h2 { - font: 300 24px "Open Sans", sans-serif; - margin: 0 0 0.2em; +.autosuggest { + background: #fff; + border: 1px solid #ccc; + position: absolute; + left: 2px; + top: 37px; + margin: 0; + padding: 0; +} +.autosuggest li { + display: block !important; + padding: 3px 8px; +} +.autosuggest li:hover, .autosuggest li.selected { + background: #CFECF7; + cursor: pointer; } @@ -354,6 +381,36 @@ ul#nav li a:hover { } +/* Featured thumbnail selection +---------------------------------*/ +#featuredThumbnail { + list-style: none; + margin: 2.5% 0 0; + padding: 0; + clear: both; + overflow: auto; +} +#featuredThumbnail li { + float: left; + width: 18%; + line-height: 0; + margin: 0 1% 2%; + min-width: 200px; + height: 149px; + position: relative; +} +#featuredThumbnail input { + position: absolute; + top: 0.5rem; + right: 0.5rem; + z-index: 100; +} +#featuredThumbnail img { + width: 100%; + box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.2); +} + + /* Footer -----------*/ footer { @@ -370,206 +427,36 @@ footer a { } -/* Input -----------*/ - -input, select, .btn { - background: #fff; - border: 1px solid #dbdbdb; - border-radius: 4px; - color: #000; - font: 13px/1.7 "Open Sans", "Helvetica", sans-serif; - padding: 3px; -} -textarea { - border: 1px solid #dbdbdb; - border-radius: 4px; - font: 14px/1.4 'Inconsolata', 'DejaVu Sans Mono', monospace; - padding: 0.75%; -} - -input[type=submit], button, .btn { - background-color: #eee; - border-color: #dbdbdb; - border-width: 1px; - border-radius: 4px; - color: #363636; - cursor: pointer; - display: inline-block; - justify-content: center; - padding-bottom: calc(0.4em - 1px); - padding-left: 0.8em; - padding-right: 0.8em; - padding-top: calc(0.4em - 1px); - text-align: center; - white-space: nowrap; -} -input:hover, select:hover, button:hover, .btn:hover { - border-color: #b5b5b5; -} -input:focus, select:focus, button:focus, .btn:focus { - border-color: #3273dc; -} -input:focus:not(:active), select:focus:not(:active), button:focus:not(:active), .btn:focus:not(:active) { - box-shadow: 0px 0px 0px 2px rgba(50, 115, 220, 0.25); -} -input:active, select:active, button:active, .btn:active { - border-color: #4a4a4a; -} - -.btn-red { - background: #eebbaa; - border-color: #cc9988; -} -.btn-red:hover, .btn-red:focus { - border-color: #bb7766; - color: #000; -} -.btn-red:focus:not(:active) { - box-shadow: 0px 0px 0px 2px rgba(241, 70, 104, 0.25); -} - - -/* Login box styles ----------------------*/ -#login { - background: #fff; - border: 1px solid #aaa; - border-radius: 10px; - box-shadow: 2px 2px 4px rgba(0,0,0,0.1); - margin: 0 auto; - overflow: auto; - padding: 15px; - width: 300px; -} -#login dl *, #login button { - font-size: 15px; - line-height: 35px; -} -#login h3 { - font: 700 24px/36px "Open Sans", sans-serif; - margin: 0; -} -#login dd { - width: 96%; - margin: 0 0 10px; -} -#login input { - background: #eee; - border: 1px solid #aaa; - border-radius: 3px; - padding: 4px 5px; - width: 100%; -} -#login div.alert { - line-height: normal; - margin: 15px 0; -} -#login div.buttonstrip { - float: right; - padding: 0 0 5px; -} -#login button { - line-height: 20px; -} - - -/* Alert boxes -- styling borrowed from Bootstrap 2 ------------------------------------------------------*/ -.alert { - padding: 8px 35px 8px 14px; - margin-bottom: 20px; - text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); - background-color: #fcf8e3; - border: 1px solid #fbeed5; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; -} -.alert, -.alert h4 { - color: #c09853; -} -.alert h4 { - margin: 0; -} -.alert .close { - position: relative; - top: -2px; - right: -21px; - line-height: 20px; -} -.alert-success { - background-color: #dff0d8; - border-color: #d6e9c6; - color: #468847; -} -.alert-success h4 { - color: #468847; -} -.alert-danger, -.alert-error { - background-color: #f2dede; - border-color: #eed3d7; - color: #b94a48; -} -.alert-danger h4, -.alert-error h4 { - color: #b94a48; -} -.alert-info { - background-color: #d9edf7; - border-color: #bce8f1; - color: #3a87ad; -} -.alert-info h4 { - color: #3a87ad; -} -.alert-block { - padding-top: 14px; - padding-bottom: 14px; -} -.alert-block > p, -.alert-block > ul { - margin-bottom: 0; -} -.alert-block p + p { - margin-top: 5px; -} - - /* Styling for the photo pages --------------------------------*/ #photo_frame { + padding-top: 1.5vh; text-align: center; } #photo_frame a { - box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3); - cursor: -moz-zoom-in; - display: inline-block; + } #photo_frame a img { border: none; - display: block; - height: auto; - width: 100%; + box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3); + cursor: -moz-zoom-in; + display: inline-block; + height: 97vh; + max-width: 100%; + object-fit: contain; + width: auto; } #previous_photo, #next_photo { background: #fff; box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3); color: #262626; - font-size: 3em; + font-size: 3rem; line-height: 0.5; - padding: 32px 8px; + padding: 2rem 0.5rem; position: fixed; text-decoration: none; - top: 45%; -} -#previous_photo em, #next_photo em { - position: absolute; - top: -1000em; - left: -1000em; + top: calc(50% - 5rem); } span#previous_photo, span#next_photo { opacity: 0.25; @@ -579,33 +466,18 @@ a#previous_photo:hover, a#next_photo:hover { color: #000; } #previous_photo { + border-top-right-radius: 0.5rem; + border-bottom-right-radius: 0.5rem; left: 0; } -#previous_photo:before { - content: '←'; -} #next_photo { + border-top-left-radius: 0.5rem; + border-bottom-left-radius: 0.5rem; right: 0; } -#next_photo:before { - content: '→'; -} #sub_photo h2, #sub_photo h3, #photo_exif_box h3, #user_actions_box h3 { - font: 600 20px/30px "Open Sans", sans-serif; - margin: 0 0 10px; -} -#sub_photo h3 { - font-size: 16px; -} - -#sub_photo { - background: #fff; - box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3); - float: left; - padding: 2%; - margin: 25px 3.5% 25px 0; - width: 68.5%; + margin-bottom: 1rem; } #sub_photo #tag_list { list-style: none; @@ -624,15 +496,6 @@ a#previous_photo:hover, a#next_photo:hover { } -#photo_exif_box { - background: #fff; - box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3); - margin: 25px 0 25px 0; - overflow: auto; - padding: 2%; - float: right; - width: 20%; -} #photo_exif_box dt { font-weight: bold; float: left; @@ -647,15 +510,6 @@ a#previous_photo:hover, a#next_photo:hover { margin: 0; } -#user_actions_box { - background: #fff; - box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3); - float: left; - margin: 25px 0 25px 0; - overflow: auto; - padding: 2%; - width: 20%; -} /* Responsive: smartphone in portrait ---------------------------------------*/ @@ -666,38 +520,6 @@ a#previous_photo:hover, a#next_photo:hover { max-width: 100% !important; } - h1#logo { - font-size: 42px; - float: none; - margin: 1em 0 0.5em; - text-align: center; - } - h1#logo:before { - float: none; - font-size: 58px; - margin-right: 8px; - } - - ul#nav { - float: none; - padding: 0; - margin: 1em 0; - text-align: center; - overflow: hidden; - } - - ul#nav li, ul#nav li a { - display: inline-block; - float: none; - } - - ul#nav li a { - float: none; - font-size: 16px; - margin-left: 6px; - padding: 12px 4px; - } - .album_title_box { margin-left: 0; } diff --git a/public/images/nothumb.png b/public/images/nothumb.png deleted file mode 100644 index ce6a2f2..0000000 Binary files a/public/images/nothumb.png and /dev/null differ diff --git a/public/images/nothumb.svg b/public/images/nothumb.svg new file mode 100644 index 0000000..339274a --- /dev/null +++ b/public/images/nothumb.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/public/js/autosuggest.js b/public/js/autosuggest.js index 76ab820..e1bcb42 100644 --- a/public/js/autosuggest.js +++ b/public/js/autosuggest.js @@ -124,7 +124,7 @@ class AutoSuggest { node.innerHTML = this.highlightMatches(query_tokens, item.label); node.jsondata = item; node.addEventListener('click', event => { - this.appendCallback(event.target.jsondata); + this.appendCallback(node.jsondata); this.closeContainer(); this.clearInput(); }); diff --git a/public/js/crop_editor.js b/public/js/crop_editor.js index 555ec83..286f1da 100644 --- a/public/js/crop_editor.js +++ b/public/js/crop_editor.js @@ -3,7 +3,7 @@ class CropEditor { this.opt = opt; this.edit_crop_button = document.createElement("span"); - this.edit_crop_button.className = "btn"; + this.edit_crop_button.className = "btn btn-light"; this.edit_crop_button.textContent = "Edit crop"; this.edit_crop_button.addEventListener('click', this.show.bind(this)); @@ -34,6 +34,7 @@ class CropEditor { this.position.appendChild(source_x_label); this.source_x = document.createElement("input"); + this.source_x.className = 'form-control d-inline'; this.source_x.type = 'number'; this.source_x.addEventListener("change", this.positionBoundary.bind(this)); this.source_x.addEventListener("keyup", this.positionBoundary.bind(this)); @@ -43,6 +44,7 @@ class CropEditor { this.position.appendChild(source_y_label); this.source_y = document.createElement("input"); + this.source_y.className = 'form-control d-inline'; this.source_y.type = 'number'; this.source_y.addEventListener("change", this.positionBoundary.bind(this)); this.source_y.addEventListener("keyup", this.positionBoundary.bind(this)); @@ -52,6 +54,7 @@ class CropEditor { this.position.appendChild(crop_width_label); this.crop_width = document.createElement("input"); + this.crop_width.className = 'form-control d-inline'; this.crop_width.type = 'number'; this.crop_width.addEventListener("change", this.positionBoundary.bind(this)); this.crop_width.addEventListener("keyup", this.positionBoundary.bind(this)); @@ -61,6 +64,7 @@ class CropEditor { this.position.appendChild(crop_height_label); this.crop_height = document.createElement("input"); + this.crop_height.className = 'form-control d-inline'; this.crop_height.type = 'number'; this.crop_height.addEventListener("change", this.positionBoundary.bind(this)); this.crop_height.addEventListener("keyup", this.positionBoundary.bind(this)); @@ -78,13 +82,13 @@ class CropEditor { this.crop_constrain_label.appendChild(this.crop_constrain_text); this.save_button = document.createElement("span"); - this.save_button.className = "btn"; + this.save_button.className = "btn btn-light"; this.save_button.textContent = "Save"; this.save_button.addEventListener('click', this.save.bind(this)); this.position.appendChild(this.save_button); this.abort_button = document.createElement("span"); - this.abort_button.className = "btn btn-red"; + this.abort_button.className = "btn btn-danger"; this.abort_button.textContent = "Abort"; this.abort_button.addEventListener('click', this.hide.bind(this)); this.position.appendChild(this.abort_button); diff --git a/public/js/photonav.js b/public/js/photonav.js index e9019f2..8d43dac 100644 --- a/public/js/photonav.js +++ b/public/js/photonav.js @@ -4,14 +4,14 @@ function enableKeyDownNavigation() { var target = document.getElementById("previous_photo").href; if (target) { event.preventDefault(); - document.location.href = target + '#photo_frame'; + document.location.href = target; } } else if (event.keyCode == 39) { var target = document.getElementById("next_photo").href; if (target) { event.preventDefault(); - document.location.href = target + '#photo_frame'; + document.location.href = target; } } }, false); diff --git a/public/js/upload_queue.js b/public/js/upload_queue.js index 75b237d..506a64b 100644 --- a/public/js/upload_queue.js +++ b/public/js/upload_queue.js @@ -1,207 +1,211 @@ -function UploadQueue(options) { - this.queue = options.queue_element; - this.preview_area = options.preview_area; - this.upload_progress = []; - this.upload_url = options.upload_url; - this.submit = options.submit_button; - this.addEvents(); -} +class UploadQueue { + constructor(options) { + this.queue = options.queue_element; + this.preview_area = options.preview_area; + this.upload_progress = []; + this.upload_url = options.upload_url; + this.submit = options.submit_button; + this.addEvents(); + } -UploadQueue.prototype.addEvents = function() { - var that = this; - that.queue.addEventListener('change', function() { - that.showSpinner(that.queue, "Generating previews (not uploading yet!)"); - that.clearPreviews(); - for (var i = 0; i < that.queue.files.length; i++) { - var callback = (i !== that.queue.files.length - 1) ? null : function() { - that.hideSpinner(); - that.submit.disabled = false; + addEvents() { + this.queue.addEventListener('change', event => { + this.showSpinner(this.queue, "Generating previews (not uploading yet!)"); + this.clearPreviews(); + for (let i = 0; i < this.queue.files.length; i++) { + const callback = (i !== this.queue.files.length - 1) ? null : () => { + this.hideSpinner(); + this.submit.disabled = false; + }; + + if (this.queue.files[0].name.toUpperCase().endsWith(".HEIC")) { + alert('Sorry, the HEIC image format is not supported.\nPlease convert your photos to JPEG before uploading.'); + this.hideSpinner(); + this.submit.disabled = false; + break; + } + + this.addPreviewBoxForQueueSlot(i); + this.addPreviewForFile(this.queue.files[i], i, callback); }; - that.addPreviewBoxForQueueSlot(i); - that.addPreviewForFile(that.queue.files[i], i, callback); - }; - }); - that.submit.addEventListener('click', function(e) { - e.preventDefault(); - that.process(); - }); - this.submit.disabled = true; -}; - -UploadQueue.prototype.clearPreviews = function() { - this.preview_area.innerHTML = ''; - this.submit.disabled = true; - this.current_upload_index = -1; -} - -UploadQueue.prototype.addPreviewBoxForQueueSlot = function(index) { - var preview_box = document.createElement('div'); - preview_box.id = 'upload_preview_' + index; - this.preview_area.appendChild(preview_box); -}; - -UploadQueue.prototype.addPreviewForFile = function(file, index, callback) { - if (!file) { - return false; - } - - var preview = document.createElement('canvas'); - preview.title = file.name; - - var preview_box = document.getElementById('upload_preview_' + index); - preview_box.appendChild(preview); - - var reader = new FileReader(); - var that = this; - reader.addEventListener('load', function() { - var original = document.createElement('img'); - original.src = reader.result; - - original.addEventListener('load', function() { - // Preparation: make canvas size proportional to the original image. - preview.height = 150; - preview.width = preview.height * (original.width / original.height); - - // First pass: resize to 50% on temp canvas. - var temp = document.createElement('canvas'), - tempCtx = temp.getContext('2d'); - - temp.width = original.width * 0.5; - temp.height = original.height * 0.5; - tempCtx.drawImage(original, 0, 0, temp.width, temp.height); - - // Second pass: resize again on temp canvas. - tempCtx.drawImage(temp, 0, 0, temp.width * 0.5, temp.height * 0.5); - - // Final pass: resize to desired size on preview canvas. - var context = preview.getContext('2d'); - context.drawImage(temp, 0, 0, temp.width * 0.5, temp.height * 0.5, - 0, 0, preview.width, preview.height); - - if (callback) { - callback(); - } }); - }, false); - reader.readAsDataURL(file); -}; - -UploadQueue.prototype.process = function() { - this.showSpinner(this.submit, "Preparing to upload files..."); - if (this.queue.files.length > 0) { + this.submit.addEventListener('click', event => { + event.preventDefault(); + this.process(); + }); this.submit.disabled = true; - this.nextFile(); } -}; -UploadQueue.prototype.nextFile = function() { - var files = this.queue.files; - var i = ++this.current_upload_index; - if (i === files.length) { - this.hideSpinner(); - } else { - this.setSpinnerLabel("Uploading file " + (i + 1) + " out of " + files.length); - this.sendFile(files[i], i, function() { + clearPreviews() { + this.preview_area.innerHTML = ''; + this.submit.disabled = true; + this.current_upload_index = -1; + } + + addPreviewBoxForQueueSlot(index) { + const preview_box = document.createElement('div'); + preview_box.id = 'upload_preview_' + index; + this.preview_area.appendChild(preview_box); + } + + addPreviewForFile(file, index, callback) { + if (!file) { + return false; + } + + const preview = document.createElement('canvas'); + preview.title = file.name; + + const preview_box = document.getElementById('upload_preview_' + index); + preview_box.appendChild(preview); + + const reader = new FileReader(); + reader.addEventListener('load', event => { + const original = document.createElement('img'); + original.src = reader.result; + + original.addEventListener('load', function() { + // Preparation: make canvas size proportional to the original image. + preview.height = 150; + preview.width = preview.height * (original.width / original.height); + + // First pass: resize to 50% on temp canvas. + const temp = document.createElement('canvas'), + tempCtx = temp.getContext('2d'); + + temp.width = original.width * 0.5; + temp.height = original.height * 0.5; + tempCtx.drawImage(original, 0, 0, temp.width, temp.height); + + // Second pass: resize again on temp canvas. + tempCtx.drawImage(temp, 0, 0, temp.width * 0.5, temp.height * 0.5); + + // Final pass: resize to desired size on preview canvas. + const context = preview.getContext('2d'); + context.drawImage(temp, 0, 0, temp.width * 0.5, temp.height * 0.5, + 0, 0, preview.width, preview.height); + + if (callback) { + callback(); + } + }); + }, false); + reader.readAsDataURL(file); + } + + process() { + this.showSpinner(this.submit, "Preparing to upload files..."); + if (this.queue.files.length > 0) { + this.submit.disabled = true; this.nextFile(); + } + } + + nextFile() { + const files = this.queue.files; + const i = ++this.current_upload_index; + if (i === files.length) { + this.hideSpinner(); + } else { + this.setSpinnerLabel("Uploading file " + (i + 1) + " out of " + files.length); + this.sendFile(files[i], i, this.nextFile); + } + } + + sendFile(file, index, callback) { + const request = new XMLHttpRequest(); + request.addEventListener('error', event => { + this.updateProgress(index, -1); }); - } -}; - -UploadQueue.prototype.sendFile = function(file, index, callback) { - // Prepare the request. - var that = this; - var request = new XMLHttpRequest(); - request.addEventListener('error', function(event) { - that.updateProgress(index, -1); - }); - request.addEventListener('progress', function(event) { - that.updateProgress(index, event.loaded / event.total); - }); - request.addEventListener('load', function(event) { - that.updateProgress(index, 1); - if (request.responseText !== null && request.status === 200) { - var obj = JSON.parse(request.responseText); - if (obj.error) { - alert(obj.error); - return; + request.addEventListener('progress', event => { + this.updateProgress(index, event.loaded / event.total); + }); + request.addEventListener('load', event => { + this.updateProgress(index, 1); + if (request.responseText !== null && request.status === 200) { + const obj = JSON.parse(request.responseText); + if (obj.error) { + alert(obj.error); + return; + } + else if (callback) { + callback.call(this, obj); + } } - else if (callback) { - callback.call(that, obj); + }); + + const data = new FormData(); + data.append('uploads', file, file.name); + + request.open('POST', this.upload_url, true); + request.send(data); + } + + addProgressBar(index) { + if (index in this.upload_progress) { + return; + } + + const progress_container = document.createElement('div'); + progress_container.className = 'progress'; + + const progress = document.createElement('div'); + progress_container.appendChild(progress); + + const preview_box = document.getElementById('upload_preview_' + index); + preview_box.appendChild(progress_container); + + this.upload_progress[index] = progress; + } + + updateProgress(index, progress) { + if (!(index in this.upload_progress)) { + this.addProgressBar(index); + } + + const bar = this.upload_progress[index]; + + if (progress >= 0) { + bar.style.width = Math.ceil(progress * 100) + '%'; + } else { + bar.style.width = ""; + if (progress === -1) { + bar.className = "error"; } } - }); - - var data = new FormData(); - data.append('uploads', file, file.name); - - request.open('POST', this.upload_url, true); - request.send(data); -}; - -UploadQueue.prototype.addProgressBar = function(index) { - if (index in this.upload_progress) { - return; } - var progress_container = document.createElement('div'); - progress_container.className = 'progress'; + showSpinner(sibling, label) { + if (this.spinner) { + return; + } - var progress = document.createElement('div'); - progress_container.appendChild(progress); + this.spinner = document.createElement('div'); + this.spinner.className = 'spinner'; + sibling.parentNode.appendChild(this.spinner); - var preview_box = document.getElementById('upload_preview_' + index); - preview_box.appendChild(progress_container); - - this.upload_progress[index] = progress; -}; - -UploadQueue.prototype.updateProgress = function(index, progress) { - if (!(index in this.upload_progress)) { - this.addProgressBar(index); - } - - var bar = this.upload_progress[index]; - - if (progress >= 0) { - bar.style.width = Math.ceil(progress * 100) + '%'; - } else { - bar.style.width = ""; - if (progress === -1) { - bar.className = "error"; + if (label) { + this.spinner_label = document.createElement('span'); + this.spinner_label.className = 'spinner_label'; + this.spinner_label.innerHTML = label; + sibling.parentNode.appendChild(this.spinner_label); } } -}; -UploadQueue.prototype.showSpinner = function(sibling, label) { - if (this.spinner) { - return; + setSpinnerLabel(label) { + if (this.spinner_label) { + this.spinner_label.innerHTML = label; + } } - this.spinner = document.createElement('div'); - this.spinner.className = 'spinner'; - sibling.parentNode.appendChild(this.spinner); - - if (label) { - this.spinner_label = document.createElement('span'); - this.spinner_label.className = 'spinner_label'; - this.spinner_label.innerHTML = label; - sibling.parentNode.appendChild(this.spinner_label); - } -}; - -UploadQueue.prototype.setSpinnerLabel = function(label) { - if (this.spinner_label) { - this.spinner_label.innerHTML = label; + hideSpinner() { + if (this.spinner) { + this.spinner.parentNode.removeChild(this.spinner); + this.spinner = null; + } + if (this.spinner_label) { + this.spinner_label.parentNode.removeChild(this.spinner_label); + this.spinner_label = null; + } } } - -UploadQueue.prototype.hideSpinner = function() { - if (this.spinner) { - this.spinner.parentNode.removeChild(this.spinner); - this.spinner = null; - } - if (this.spinner_label) { - this.spinner_label.parentNode.removeChild(this.spinner_label); - this.spinner_label = null; - } -}; diff --git a/public/vendor b/public/vendor new file mode 120000 index 0000000..42a408b --- /dev/null +++ b/public/vendor @@ -0,0 +1 @@ +../vendor/ \ No newline at end of file diff --git a/templates/AdminBar.php b/templates/AdminBar.php deleted file mode 100644 index 65662fe..0000000 --- a/templates/AdminBar.php +++ /dev/null @@ -1,38 +0,0 @@ - - - '; - } - - public function appendItem($url, $caption) - { - $this->extra_items[] = [$url, $caption]; - } -} diff --git a/templates/AlbumButtonBox.php b/templates/AlbumButtonBox.php index d18ef31..3956ffb 100644 --- a/templates/AlbumButtonBox.php +++ b/templates/AlbumButtonBox.php @@ -6,7 +6,7 @@ * Kabuki CMS (C) 2013-2016, Aaron van Geffen *****************************************************************************/ -class AlbumButtonBox extends SubTemplate +class AlbumButtonBox extends Template { private $buttons; @@ -15,14 +15,14 @@ class AlbumButtonBox extends SubTemplate $this->buttons = $buttons; } - protected function html_content() + public function html_main() { echo '
'; foreach ($this->buttons as $button) echo ' - ', $button['caption'], ''; + ', $button['caption'], ''; echo '
'; diff --git a/templates/AlbumHeaderBox.php b/templates/AlbumHeaderBox.php index 0de991d..1d82818 100644 --- a/templates/AlbumHeaderBox.php +++ b/templates/AlbumHeaderBox.php @@ -6,7 +6,7 @@ * Kabuki CMS (C) 2013-2016, Aaron van Geffen *****************************************************************************/ -class AlbumHeaderBox extends SubTemplate +class AlbumHeaderBox extends Template { private $back_link_title; private $back_link; @@ -21,11 +21,13 @@ class AlbumHeaderBox extends SubTemplate $this->back_link_title = $back_link_title; } - protected function html_content() + public function html_main() { echo '
- + + +

', $this->title, '

'; diff --git a/templates/AlbumIndex.php b/templates/AlbumIndex.php index 29a856b..ebfe9c1 100644 --- a/templates/AlbumIndex.php +++ b/templates/AlbumIndex.php @@ -6,7 +6,7 @@ * Kabuki CMS (C) 2013-2015, Aaron van Geffen *****************************************************************************/ -class AlbumIndex extends SubTemplate +class AlbumIndex extends Template { protected $albums; protected $show_edit_buttons; @@ -23,10 +23,10 @@ class AlbumIndex extends SubTemplate $this->show_labels = $show_labels; } - protected function html_content() + public function html_main() { echo ' -
'; +
'; foreach (array_chunk($this->albums, 3) as $photos) { @@ -55,11 +55,13 @@ class AlbumIndex extends SubTemplate echo ' '; + ' alt="" style="width: ', static::TILE_WIDTH, + 'px; height: ', static::TILE_HEIGHT, 'px">'; } else echo ' - '; + '; if ($this->show_labels) echo ' diff --git a/templates/Alert.php b/templates/Alert.php index d5a2091..4f24031 100644 --- a/templates/Alert.php +++ b/templates/Alert.php @@ -6,7 +6,7 @@ * Kabuki CMS (C) 2013-2015, Aaron van Geffen *****************************************************************************/ -class Alert extends SubTemplate +class Alert extends Template { private $_type; private $_message; @@ -16,20 +16,20 @@ class Alert extends SubTemplate { $this->_title = $title; $this->_message = $message; - $this->_type = in_array($type, ['alert', 'error', 'success', 'info']) ? $type : 'alert'; + $this->_type = in_array($type, ['success', 'info', 'warning', 'danger']) ? $type : 'info'; } - protected function html_content() + public function html_main() { echo ' -
', (!empty($this->_title) ? ' - ' . $this->_title . '
' : ''), '

', $this->_message, '

'; - - $this->additional_alert_content(); - - echo '
'; +
' + , !empty($this->_title) ? '' . $this->_title . '
' : '', ' + ', $this->_message, + $this->additional_alert_content(), ' +
'; } protected function additional_alert_content() - {} + { + } } diff --git a/templates/AssetManagementWrapper.php b/templates/AssetManagementWrapper.php new file mode 100644 index 0000000..545f7c4 --- /dev/null +++ b/templates/AssetManagementWrapper.php @@ -0,0 +1,36 @@ +'; + + foreach ($this->_subtemplates as $template) + $template->html_main(); + + echo ' + + '; + } +} diff --git a/templates/Button.php b/templates/Button.php index 29024e1..c978c42 100644 --- a/templates/Button.php +++ b/templates/Button.php @@ -6,7 +6,7 @@ * Kabuki CMS (C) 2013-2015, Aaron van Geffen *****************************************************************************/ -class Button extends SubTemplate +class Button extends Template { private $content = ''; private $href = ''; @@ -19,7 +19,7 @@ class Button extends SubTemplate $this->class = $class; } - protected function html_content() + public function html_main() { echo ' ', $this->content, ''; diff --git a/templates/ConfirmDeletePage.php b/templates/ConfirmDeletePage.php index bdba97c..80075c5 100644 --- a/templates/ConfirmDeletePage.php +++ b/templates/ConfirmDeletePage.php @@ -13,7 +13,7 @@ class ConfirmDeletePage extends PhotoPage parent::__construct($photo); } - protected function html_content() + public function html_main() { $this->confirm(); $this->photo(); @@ -22,7 +22,7 @@ class ConfirmDeletePage extends PhotoPage private function confirm() { $buttons = []; - $buttons[] = new Button("Delete", BASEURL . '/' . $this->photo->getSlug() . '?delete_confirmed', "btn btn-red"); + $buttons[] = new Button("Delete", BASEURL . '/' . $this->photo->getSlug() . '/?delete_confirmed', "btn btn-danger"); $buttons[] = new Button("Cancel", $this->photo->getPageUrl(), "btn"); $alert = new WarningDialog( @@ -30,6 +30,6 @@ class ConfirmDeletePage extends PhotoPage "You are about to permanently delete the following photo.", $buttons ); - $alert->html_content(); + $alert->html_main(); } } diff --git a/templates/DummyBox.php b/templates/DummyBox.php index 2632384..0ef5f24 100644 --- a/templates/DummyBox.php +++ b/templates/DummyBox.php @@ -8,28 +8,26 @@ class DummyBox extends SubTemplate { - private $_class; - private $_content; - private $_title; + protected $_content; - public function __construct($title = '', $content = '', $class = '') + public function __construct($title = '', $content = '', $class = null) { - $this->_title = $title; + parent::__construct($title); $this->_content = $content; - $this->_class = $class; + + if (isset($class)) + $this->_class .= $class; } protected function html_content() { - echo ' -
', $this->_title ? ' -

' . $this->_title . '

' : '', ' - ', $this->_content; + if ($this->_title) + echo ' +

', $this->_title, '

'; + + echo $this->_content; foreach ($this->_subtemplates as $template) $template->html_main(); - - echo ' -
'; } } diff --git a/templates/EditAssetForm.php b/templates/EditAssetForm.php index 490b0d2..d791dcd 100644 --- a/templates/EditAssetForm.php +++ b/templates/EditAssetForm.php @@ -6,7 +6,7 @@ * Kabuki CMS (C) 2013-2015, Aaron van Geffen *****************************************************************************/ -class EditAssetForm extends SubTemplate +class EditAssetForm extends Template { private $asset; private $thumbs; @@ -17,14 +17,14 @@ class EditAssetForm extends SubTemplate $this->thumbs = $thumbs; } - protected function html_content() + public function html_main() { echo '
-
-
- Delete asset - +
+
+ Delete asset +

Edit asset \'', $this->asset->getTitle(), '\' (', $this->asset->getFilename(), ')

'; @@ -32,14 +32,15 @@ class EditAssetForm extends SubTemplate $this->section_replace(); echo ' -
'; +
+
'; $this->section_key_info(); $this->section_asset_meta(); echo ' -
-
'; +
+
'; if (!empty($this->thumbs)) $this->section_thumbnails(); @@ -47,11 +48,12 @@ class EditAssetForm extends SubTemplate $this->section_linked_tags(); echo ' -
'; +
'; $this->section_crop_editor(); echo ' +
'; } @@ -59,31 +61,43 @@ class EditAssetForm extends SubTemplate { $date_captured = $this->asset->getDateCaptured(); echo ' -
+

Key info

-
-
Title
-
-
URL slug
-
- -
Date captured
-
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ - -
Display priority
-
- +
+
+
+ +
+ +
+
'; } protected function section_linked_tags() { echo ' -
+

Linked tags

-
    '; +
      '; foreach ($this->asset->getTags() as $tag) echo ' @@ -93,7 +107,7 @@ class EditAssetForm extends SubTemplate '; echo ' -
    • +
@@ -134,9 +148,9 @@ class EditAssetForm extends SubTemplate protected function section_thumbnails() { echo ' -
+

Thumbnails

- View: '; $first = INF; foreach ($this->thumbs as $i => $thumb) @@ -218,38 +232,39 @@ class EditAssetForm extends SubTemplate protected function section_asset_meta() { echo ' -
-

Asset meta data

-
    '; +
    +

    Asset meta data

    '; - $i = -1; + $i = 0; foreach ($this->asset->getMeta() as $key => $meta) { - $i++; echo ' -
  • - - -
  • '; +
    + + +
    '; + $i++; } + echo ' -
  • - - -
  • -
-

+
+ + +
+
+ +
'; } protected function section_replace() { echo ' -
+

Replace asset

- File: - Target: + Target: - +
'; } } diff --git a/templates/FeaturedThumbnailManager.php b/templates/FeaturedThumbnailManager.php new file mode 100644 index 0000000..ffbe1c0 --- /dev/null +++ b/templates/FeaturedThumbnailManager.php @@ -0,0 +1,46 @@ +assets = $assets; + $this->currentThumbnailId = $currentThumbnailId; + } + + protected function html_content() + { + echo ' +
+ +

Select thumbnail

+
    '; + + while ($asset = $this->assets->next()) + { + $image = $asset->getImage(); + echo ' +
  • + currentThumbnailId == $image->getId() ? ' checked' : '', '> + +
  • '; + } + + $this->assets->clean(); + + echo ' +
+ +
'; + } +} diff --git a/templates/ForgotPasswordForm.php b/templates/ForgotPasswordForm.php index 7a50660..447ebeb 100644 --- a/templates/ForgotPasswordForm.php +++ b/templates/ForgotPasswordForm.php @@ -11,19 +11,25 @@ class ForgotPasswordForm extends SubTemplate protected function html_content() { echo ' -
-

Password reset procedure

'; +

Password reset procedure

'; foreach ($this->_subtemplates as $template) $template->html_main(); echo ' -

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.

-
-
- - -
-
'; +

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.

+
+
+ +
+ +
+
+
+
+ +
+
+
'; } } diff --git a/templates/FormView.php b/templates/FormView.php index 389fa11..5b775ef 100644 --- a/templates/FormView.php +++ b/templates/FormView.php @@ -3,52 +3,42 @@ * FormView.php * Contains the form template. * - * Kabuki CMS (C) 2013-2015, Aaron van Geffen + * Global Data Lab code (C) Radboud University Nijmegen + * Programming (C) Aaron van Geffen, 2015-2022 *****************************************************************************/ class FormView extends SubTemplate { - private $content_below; - private $content_above; - private $data; - private $missing; - private $fields; - private $request_method; - private $request_url; + private $form; + private array $data; + private array $missing; private $title; public function __construct(Form $form, $title = '') { + $this->form = $form; $this->title = $title; - $this->request_url = $form->request_url; - $this->request_method = $form->request_method; - $this->fields = $form->getFields(); - $this->missing = $form->getMissing(); - $this->data = $form->getData(); - $this->content_above = $form->content_above; - $this->content_below = $form->content_below; } protected function html_content($exclude = [], $include = []) { if (!empty($this->title)) echo ' -
-

', htmlspecialchars($this->title), '

'; +

', $this->title, '

'; foreach ($this->_subtemplates as $template) $template->html_main(); echo ' -
'; + '; - if (isset($this->content_above)) - echo $this->content_above; + if (isset($this->form->content_above)) + echo $this->form->content_above; - echo ' -
'; + $this->missing = $this->form->getMissing(); + $this->data = $this->form->getData(); - foreach ($this->fields as $field_id => $field) + foreach ($this->form->getFields() as $field_id => $field) { // Either we have a blacklist if (!empty($exclude) && in_array($field_id, $exclude)) @@ -62,107 +52,230 @@ class FormView extends SubTemplate } echo ' -
-
- '; +
+
+ '; - if (isset($this->content_below)) + if (isset($this->form->content_below)) echo ' - ', $this->content_below; + ', $this->form->content_below; echo ' +
'; - - if (!empty($this->title)) - echo ' -
'; } - protected function renderField($field_id, $field) + protected function renderField($field_id, array $field) { if (isset($field['before_html'])) - echo ' - ', $field['before_html'], ' -
'; - - if ($field['type'] != 'checkbox' && isset($field['label'])) echo ' -
missing) ? ' style="color: red"' : '', '>', $field['label'], '
'; - elseif ($field['type'] === 'checkbox' && isset($field['header'])) - echo ' -
missing) ? ' style="color: red"' : '', '>', $field['header'], '
'; + ', $field['before_html']; echo ' -
'; +
'; if (isset($field['before'])) echo $field['before']; + if ($field['type'] !== 'checkbox') + if (isset($field['label'])) + echo ' + +
'; + else + echo ' +
'; + switch ($field['type']) { case 'select': - echo ' - '; + $this->renderSelect($field_id, $field); break; case 'radio': - foreach ($field['options'] as $value => $option) - echo ' - data[$field_id] == $value ? ' checked' : '', !empty($field['disabled']) ? ' disabled' : '', '> ', htmlentities($option); + $this->renderRadio($field_id, $field); break; case 'checkbox': - echo ' - '; + $this->renderCheckbox($field_id, $field); break; case 'textarea': - echo ' - '; + $this->renderTextArea($field_id, $field); break; case 'color': - echo ' - '; + $this->renderColor($field_id, $field); break; case 'numeric': - echo ' - '; + $this->renderNumeric($field_id, $field); break; case 'file': - if (!empty($this->data[$field_id])) - echo '
'; + $this->renderFile($field_id, $field); + break; - echo ' - '; + case 'captcha': + $this->renderCaptcha($field_id, $field); break; case 'text': case 'password': default: - echo ' - '; + $this->renderText($field_id, $field); } if (isset($field['after'])) echo ' ', $field['after']; + if ($field['type'] !== 'checkbox') + echo ' +
'; + echo ' -
'; +
'; + } + + private function renderCaptcha($field_id, array $field) + { + echo ' +
+ '; + } + + private function renderCheckbox($field_id, array $field) + { + echo ' +
+
+ data[$field_id] ? ' checked' : '', !empty($field['disabled']) ? ' disabled' : '', ' name="', $field_id, '" id="check-', $field_id, '"> + +
+
'; + } + + private function renderColor($field_id, array $field) + { + echo ' + '; + } + + private function renderFile($field_id, array $field) + { + if (!empty($this->data[$field_id])) + echo 'Currently using asset ', $this->data[$field_id], '. Upload to overwrite.
'; + + echo ' + '; + } + + private function renderNumeric($field_id, array $field) + { + echo ' + '; + } + + private function renderRadio($field_id, array $field) + { + foreach ($field['options'] as $value => $option) + echo ' +
+ data[$field_id] == $value ? ' checked' : '', !empty($field['disabled']) ? ' disabled' : '', '> + +
'; + } + + private function renderSelect($field_id, array $field) + { + echo ' + '; + } + + private function renderSelectOption($field_id, $label, $value, $multiple = false) + { + echo ' + '; + } + + private function renderSelectOptionGroup($field_id, $label, $options) + { + echo ' + '; + + foreach ($options as $value => $option) + $this->renderSelectOption($field_id, $option, $value); + + echo ' + '; + } + + private function renderText($field_id, array $field) + { + echo ' + 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 ' + '; } } diff --git a/templates/LogInForm.php b/templates/LogInForm.php index c2b8835..0e0c96a 100644 --- a/templates/LogInForm.php +++ b/templates/LogInForm.php @@ -11,45 +11,57 @@ class LogInForm extends SubTemplate private $redirect_url = ''; private $emailaddress = ''; + protected $_class = 'content-box container col-lg-6'; + public function setRedirectUrl($url) { - $_SESSION['login_url'] = $url; $this->redirect_url = $url; } public function setEmail($addr) { - $this->emailaddress = htmlentities($addr); + $this->emailaddress = htmlspecialchars($addr); } protected function html_content() { - echo ' -
-

Log in

'; + if (!empty($this->_title)) + echo ' +

Press #RU to continue

'; - foreach ($this->_subtemplates as $template) - $template->html_main(); + if (!empty($this->_subtemplates)) + { + foreach ($this->_subtemplates as $template) + $template->html_main(); + } echo ' -
-
-
- -
-
-
'; + +
+ +
+ +
+
+
+ +
+ +
+
'; // Throw in a redirect url if asked for. if (!empty($this->redirect_url)) echo ' - '; + '; echo ' - Forgotten your password? -
- -
-
'; +
+
+ + Forgotten your password? +
+
+ '; } } diff --git a/templates/MainNavBar.php b/templates/MainNavBar.php new file mode 100644 index 0000000..a83a356 --- /dev/null +++ b/templates/MainNavBar.php @@ -0,0 +1,63 @@ + 50 ? ' alt-' . ($rnd % 6 + 1) : ''; + + echo ' + '; + } +} diff --git a/templates/MainTemplate.php b/templates/MainTemplate.php index 87a1c0d..b9e903b 100644 --- a/templates/MainTemplate.php +++ b/templates/MainTemplate.php @@ -25,25 +25,30 @@ class MainTemplate extends Template echo ' - ', $this->title, '', !empty($this->canonical_url) ? ' - ' : '', ' + ', $this->title, ''; + + if (!empty($this->canonical_url)) + echo ' + '; + + echo ' + + '; + + echo ' + + - - ', !empty($this->css) ? ' - ' : '', $this->header_html, ' - + ' + , $this->header_html, ' classes) ? ' class="' . implode(' ', $this->classes) . '"' : '', '> -
- -

#pics

-
- +
'; + + $bar = new MainNavBar(); + $bar->html_main(); + + echo '
'; @@ -69,7 +74,7 @@ class MainTemplate extends Template } else echo ' - Powered by Kabuki CMS'; + Powered by Kabuki CMS'; echo ' @@ -80,15 +85,11 @@ class MainTemplate extends Template echo '
', strtr($query, "\t", " "), '
'; echo ' + '; } - public function appendCss($css) - { - $this->css .= $css; - } - public function appendHeaderHtml($html) { $this->header_html .= "\n\t\t" . $html; diff --git a/templates/MediaUploader.php b/templates/MediaUploader.php index 008cfc9..1a6aab4 100644 --- a/templates/MediaUploader.php +++ b/templates/MediaUploader.php @@ -18,14 +18,12 @@ class MediaUploader extends SubTemplate protected function html_content() { echo ' -
+

Upload new photos to "', $this->tag->tag, '"

-
-

Select files

- -
-
- +
+ +
diff --git a/templates/MyTagsView.php b/templates/MyTagsView.php new file mode 100644 index 0000000..2b06fd9 --- /dev/null +++ b/templates/MyTagsView.php @@ -0,0 +1,32 @@ +tags = $tags; + } + + protected function html_content() + { + echo ' +

Tags you can edit

+

You can currently edit the tags below. Click a tag to edit it.

+
    '; + + foreach ($this->tags as $tag) + echo ' +
  • ', $tag->tag, '
  • '; + + echo ' +
'; + } +} diff --git a/templates/NavBar.php b/templates/NavBar.php new file mode 100644 index 0000000..c625c60 --- /dev/null +++ b/templates/NavBar.php @@ -0,0 +1,62 @@ +'; + + $this->renderMenuItems($items, $navBarClasses); + + echo ' + '; + } + + public function renderMenuItems(array $items) + { + foreach ($items as $menuId => $item) + { + if (isset($item['icon'])) + $item['label'] = ' ' . $item['label']; + + if (isset($item['badge'])) + $item['label'] .= ' ' . $item['badge'] . ''; + + if (empty($item['subs'])) + { + echo ' + '; + continue; + } + + echo ' + '; + } + } +} diff --git a/templates/PageIndexWidget.php b/templates/PageIndexWidget.php new file mode 100644 index 0000000..a10070e --- /dev/null +++ b/templates/PageIndexWidget.php @@ -0,0 +1,61 @@ +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 ' + '; + } +} diff --git a/templates/Pagination.php b/templates/Pagination.php deleted file mode 100644 index fea1df4..0000000 --- a/templates/Pagination.php +++ /dev/null @@ -1,64 +0,0 @@ -index = $index; - $this->class = $index->getPageIndexClass(); - } - - protected function html_content() - { - $index = $this->index->getPageIndex(); - - echo ' -
-
    -
  • <', !empty($index['previous']) ? 'a href="' . $index['previous']['href'] . '"' : 'span', '>« previous
  • '; - - $num_wildcards = 0; - foreach ($index as $key => $page) - { - if (!is_numeric($key)) - continue; - - if (!is_array($page)) - { - $num_wildcards++; - echo ' -
  • ...
  • '; - } - else - echo ' -
  • ', $page['index'], '
  • '; - } - - echo ' -
  • <', !empty($index['next']) ? 'a href="' . $index['next']['href'] . '"' : 'span', '>next »
  • -
-
'; - - if ($num_wildcards) - { - echo ' - '; - } - } -} diff --git a/templates/PasswordResetForm.php b/templates/PasswordResetForm.php index 4bc9c5e..2393bd5 100644 --- a/templates/PasswordResetForm.php +++ b/templates/PasswordResetForm.php @@ -20,27 +20,31 @@ class PasswordResetForm extends SubTemplate protected function html_content() { echo ' -
-

Password reset procedure

'; +

Password reset procedure

'; foreach ($this->_subtemplates as $template) $template->html_main(); echo ' -

You have successfully confirmed your identify. Please use the form below to set a new password.

- -

- - -

- -

- - -

- +

You have successfully confirmed your identify. Please use the form below to set a new password.

+ +
+ +
+ +
+
+
+ +
+ +
+
+
+
- -
'; +
+
+ '; } } diff --git a/templates/PhotoPage.php b/templates/PhotoPage.php index 3ce2b55..52b6fe5 100644 --- a/templates/PhotoPage.php +++ b/templates/PhotoPage.php @@ -6,7 +6,7 @@ * Kabuki CMS (C) 2013-2016, Aaron van Geffen *****************************************************************************/ -class PhotoPage extends SubTemplate +class PhotoPage extends Template { protected $photo; private $exif; @@ -34,27 +34,33 @@ class PhotoPage extends SubTemplate $this->is_asset_owner = $flag; } - protected function html_content() + public function html_main() { $this->photoNav(); $this->photo(); echo ' -
-

', $this->photo->getTitle(), '

'; +
+
+
+

', $this->photo->getTitle(), '

'; $this->taggedPeople(); $this->linkNewTags(); echo ' -
'; +
+
+
'; $this->photoMeta(); - if($this->is_asset_owner) + if ($this->is_asset_owner) $this->addUserActions(); echo ' +
+
'; } @@ -78,23 +84,23 @@ class PhotoPage extends SubTemplate { if ($this->previous_photo_url) echo ' - Previous photo'; + '; else echo ' - Previous photo'; + '; if ($this->next_photo_url) echo ' - Next photo'; + '; else echo ' - Next photo'; + '; } private function photoMeta() { echo ' -
+

EXIF

'; @@ -171,7 +177,9 @@ class PhotoPage extends SubTemplate echo '

Link tags

-

+

+ +

@@ -240,9 +248,10 @@ class PhotoPage extends SubTemplate public function addUserActions() { echo ' -
+ '; } } diff --git a/templates/PhotosIndex.php b/templates/PhotosIndex.php index 2ec9795..29fa06a 100644 --- a/templates/PhotosIndex.php +++ b/templates/PhotosIndex.php @@ -6,7 +6,7 @@ * Kabuki CMS (C) 2013-2015, Aaron van Geffen *****************************************************************************/ -class PhotosIndex extends SubTemplate +class PhotosIndex extends Template { protected $mosaic; protected $show_edit_buttons; @@ -42,10 +42,10 @@ class PhotosIndex extends SubTemplate $this->show_labels = $show_labels; } - protected function html_content() + public function html_main() { echo ' -
'; +
'; for ($i = $this->row_limit; $i > 0 && $row = $this->mosaic->getRow(); $i--) { @@ -100,7 +100,7 @@ class PhotosIndex extends SubTemplate else echo ' srcset="', $image->getThumbnailUrl($image->width(), $image->height(), true), ' 2x"'; - echo ' alt="" title="', $image->getTitle(), '">'; + echo ' alt="" title="', $image->getTitle(), '" style="width: ', $width, 'px; height: ', $height, 'px">'; if ($this->show_labels) echo ' diff --git a/templates/SubTemplate.php b/templates/SubTemplate.php index 75030de..79f0ed0 100644 --- a/templates/SubTemplate.php +++ b/templates/SubTemplate.php @@ -8,10 +8,32 @@ 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() { - echo $this->html_content(); + echo ' +
_id) ? ' id="' . $this->_id . '"' : '', '>', + $this->html_content(), ' +
'; } abstract protected function html_content(); + + public function setClassName($className) + { + $this->_class = $className; + } + + public function setDOMId($id) + { + $this->_id = $id; + } } diff --git a/templates/TabularData.php b/templates/TabularData.php index 587d4f0..fdcfed3 100644 --- a/templates/TabularData.php +++ b/templates/TabularData.php @@ -3,56 +3,73 @@ * TabularData.php * Contains the template that displays tabular data. * - * Kabuki CMS (C) 2013-2015, Aaron van Geffen + * Global Data Lab code (C) Radboud University Nijmegen + * Programming (C) Aaron van Geffen, 2015-2022 *****************************************************************************/ class TabularData extends SubTemplate { - private Pagination $pager; private GenericTable $_t; public function __construct(GenericTable $table) { $this->_t = $table; - - $pageIndex = $table->getPageIndex(); - if ($pageIndex) - $this->pager = new Pagination($pageIndex); } protected function html_content() { - echo ' -
'; - $title = $this->_t->getTitle(); if (!empty($title)) + { + $titleclass = $this->_t->getTitleClass(); echo ' -

', $title, '

'; +
+

', htmlspecialchars($title), '

'; + } - // Showing a page index? - if (isset($this->pager)) - $this->pager->html_content(); + foreach ($this->_subtemplates as $template) + $template->html_main(); - // Maybe even a small form? - if (isset($this->_t->form_above)) - $this->showForm($this->_t->form_above); + // Showing an inline form? + $pager = $this->_t->getPageIndex(); + if (!empty($pager) || isset($this->_t->form_above)) + { + echo ' +
'; + + // Page index? + if (!empty($pager)) + PageIndexWidget::paginate($pager); + + // Form controls? + if (isset($this->_t->form_above)) + $this->showForm($this->_t->form_above); + + echo ' +
'; + } + + $tableClass = $this->_t->getTableClass(); + if ($tableClass) + echo ' +
'; // Build the table! echo ' - +
'; - // Show the table's headers. - foreach ($this->_t->getHeader() as $th) + // Show all headers in their full glory! + $header = $this->_t->getHeader(); + foreach ($header as $th) { echo ' 1 ? ' colspan="' . $th['colspan'] . '"' : ''), ' scope="', $th['scope'], '">', $th['href'] ? '' . $th['label'] . '' : $th['label']; - if ($th['sort_mode'] ) - echo ' ', $th['sort_mode'] === 'up' ? '↑' : '↓'; + if ($th['sort_mode']) + echo ' '; echo ''; } @@ -62,7 +79,7 @@ class TabularData extends SubTemplate '; - // Show the table's body. + // The body is what we came to see! $body = $this->_t->getBody(); if (is_array($body)) { @@ -72,51 +89,147 @@ class TabularData extends SubTemplate '; foreach ($tr['cells'] as $td) + { echo ' - ', $td['value'], ''; + '; + + if (!empty($td['class'])) + echo '', $td['value'], ''; + else + echo $td['value']; + + echo ''; + } echo ' '; } } + // !!! Sum colspan! else echo ' - + '; echo '
', $body, '', $body, '
'; - // Maybe another small form? - if (isset($this->_t->form_below)) - $this->showForm($this->_t->form_below); + if ($tableClass) + echo ' +
'; - // Showing a page index? - if (isset($this->pager)) - $this->pager->html_content(); + // Showing an inline form? + if (!empty($pager) || isset($this->_t->form_below)) + { + echo ' +
'; - echo ' + // Page index? + if (!empty($pager)) + PageIndexWidget::paginate($pager); + + // Form controls? + if (isset($this->_t->form_below)) + $this->showForm($this->_t->form_below); + + echo ' +
'; + } + + if (!empty($title)) + echo '
'; } protected function showForm($form) { - echo ' -
'; + if (!isset($form['is_embed'])) + echo ' + '; + else + echo ' +
'; + + if (!empty($form['is_group'])) + echo ' +
'; if (!empty($form['fields'])) + { foreach ($form['fields'] as $name => $field) - echo ' - '; + { + if ($field['type'] === 'select') + { + echo ' + '; + } + else + echo ' + '; + + if (isset($field['html_after'])) + echo $field['html_after']; + } + } + + echo ' + '; if (!empty($form['buttons'])) foreach ($form['buttons'] as $name => $button) + { echo ' - '; + '; + + if (isset($button['html_after'])) + echo $button['html_after']; + } + + if (!empty($form['is_group'])) + echo ' +
'; + + if (!isset($form['is_embed'])) + echo ' + '; + else + echo ' +
'; } } diff --git a/templates/WarningDialog.php b/templates/WarningDialog.php index 198b7e0..6a9c93e 100644 --- a/templates/WarningDialog.php +++ b/templates/WarningDialog.php @@ -24,6 +24,6 @@ class WarningDialog extends Alert private function addButtons() { foreach ($this->buttons as $button) - $button->html_content(); + $button->html_main(); } } diff --git a/upgrade.sql b/upgrade.sql new file mode 100644 index 0000000..7e07e9c --- /dev/null +++ b/upgrade.sql @@ -0,0 +1,15 @@ +/* 2023-03-11 Allow designating an owner for each tag */ +ALTER TABLE `tags` ADD `id_user_owner` INT NULL DEFAULT NULL AFTER `id_asset_thumb`; + +/* 2023-03-11 Try to assign tag owners automagically */ +UPDATE tags AS t +SET id_user_owner = ( + SELECT id_user + FROM users AS u + WHERE LOWER(u.first_name) = LOWER(t.slug) OR + LOWER(u.first_name) = LOWER(t.tag) OR + LOWER(u.slug) = LOWER(t.slug) OR + LOWER(u.slug) = LOWER(t.tag) + ) +WHERE t.kind = 'Person' AND + (t.id_user_owner = 0 OR t.id_user_owner IS NULL);