forked from Public/pics
Initial commit.
This is to be the new HashRU website based on the Aaronweb.net/Kabuki CMS.
This commit is contained in:
commit
ab0e4efbcb
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
||||
text eol=lf
|
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
.DS_Store
|
||||
composer.lock
|
||||
config.php
|
||||
hashru.sublime-project
|
||||
hashru.sublime-workspace
|
1
README
Normal file
1
README
Normal file
@ -0,0 +1 @@
|
||||
This marks the development repository for the HashRU website.
|
10
TODO.md
Normal file
10
TODO.md
Normal file
@ -0,0 +1,10 @@
|
||||
TODO:
|
||||
|
||||
* Draaiing van foto's bij importeren goedzetten
|
||||
* Import asset ownership
|
||||
* Import users
|
||||
* Pagina om één foto te bekijken
|
||||
* Taggen door gebruikers
|
||||
* Uploaden door gebruikers
|
||||
* Album management
|
||||
* Password reset via e-mail
|
32
app.php
Normal file
32
app.php
Normal file
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
/*****************************************************************************
|
||||
* app.php
|
||||
* Initiates key classes and determines which controller to use.
|
||||
*
|
||||
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
||||
*****************************************************************************/
|
||||
|
||||
// Include the project's configuration.
|
||||
require_once 'config.php';
|
||||
|
||||
// Set up the autoloader.
|
||||
require_once 'vendor/autoload.php';
|
||||
|
||||
// Initialise the database.
|
||||
Registry::set('start', microtime(true));
|
||||
Registry::set('db', new Database(DB_SERVER, DB_USER, DB_PASS, DB_NAME));
|
||||
|
||||
// Do some authentication checks.
|
||||
Session::start();
|
||||
$user = Authentication::isLoggedIn() ? Member::fromId($_SESSION['user_id']) : new Guest();
|
||||
$user->updateAccessTime();
|
||||
Registry::set('user', $user);
|
||||
|
||||
// Handle errors our own way.
|
||||
//set_error_handler('ErrorHandler::handleError');
|
||||
ini_set("display_errors", DEBUG ? "On" : "Off");
|
||||
|
||||
// The real magic starts here!
|
||||
ob_start();
|
||||
Dispatcher::dispatch();
|
||||
ob_end_flush();
|
18
composer.json
Normal file
18
composer.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "aaron/hashru",
|
||||
"description": "HashRU",
|
||||
"license": "Proprietary",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Aaron van Geffen",
|
||||
"email": "aaron@aaronweb.net"
|
||||
}
|
||||
],
|
||||
"autoload": {
|
||||
"classmap": [
|
||||
"controllers/",
|
||||
"models/",
|
||||
"templates/"
|
||||
]
|
||||
}
|
||||
}
|
33
config.php.dist
Normal file
33
config.php.dist
Normal file
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
/*****************************************************************************
|
||||
* config.php
|
||||
* Contains general settings for the project.
|
||||
*
|
||||
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
||||
*****************************************************************************/
|
||||
|
||||
const DEBUG = true;
|
||||
const CACHE_ENABLED = true;
|
||||
const CACHE_KEY_PREFIX = 'hashru_';
|
||||
|
||||
// Basedir and base URL of the project.
|
||||
const BASEDIR = __DIR__;
|
||||
const BASEURL = 'https://pics.hashru.nl'; // no trailing /
|
||||
|
||||
// Assets dir and url, where assets are plentiful. (In wwwroot!)
|
||||
const ASSETSDIR = BASEDIR . '/public/assets';
|
||||
const ASSETSURL = BASEURL . '/assets';
|
||||
|
||||
// Thumbs dir and url, where thumbnails for assets reside.
|
||||
const THUMBSDIR = BASEDIR . '/public/thumbs';
|
||||
const THUMBSURL = BASEURL . '/thumbs';
|
||||
|
||||
// Database server, username, password, name
|
||||
const DB_SERVER = '127.0.0.1';
|
||||
const DB_USER = 'hashru';
|
||||
const DB_PASS = '';
|
||||
const DB_NAME = 'hashru_pics';
|
||||
const DB_LOG_QUERIES = false;
|
||||
|
||||
const SITE_TITLE = 'HashRU';
|
||||
const SITE_SLOGAN = 'Nijmeegs Nerdclubje';
|
159
controllers/EditAsset.php
Normal file
159
controllers/EditAsset.php
Normal file
@ -0,0 +1,159 @@
|
||||
<?php
|
||||
/*****************************************************************************
|
||||
* EditAsset.php
|
||||
* Contains the asset management controller
|
||||
*
|
||||
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
||||
*****************************************************************************/
|
||||
|
||||
class EditAsset extends HTMLController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
// Ensure it's just admins at this point.
|
||||
if (!Registry::get('user')->isAdmin())
|
||||
throw new NotAllowedException();
|
||||
|
||||
if (empty($_GET['id']))
|
||||
throw new Exception('Invalid request.');
|
||||
|
||||
$asset = Asset::fromId($_GET['id']);
|
||||
if (empty($asset))
|
||||
throw new NotFoundException('Asset not found');
|
||||
|
||||
if (isset($_REQUEST['delete']))
|
||||
throw new Exception('Not implemented.');
|
||||
|
||||
if (!empty($_POST))
|
||||
{
|
||||
if (isset($_GET['updatethumb']))
|
||||
{
|
||||
$image = $asset->getImage();
|
||||
return $this->updateThumb($image);
|
||||
}
|
||||
|
||||
// Key info
|
||||
if (isset($_POST['title'], $_POST['date_captured'], $_POST['priority']))
|
||||
{
|
||||
$date_captured = !empty($_POST['date_captured']) ? new DateTime($_POST['date_captured']) : null;
|
||||
$asset->setKeyData(htmlentities($_POST['title']), $date_captured, intval($_POST['priority']));
|
||||
}
|
||||
|
||||
// Handle tags
|
||||
$new_tags = [];
|
||||
if (isset($_POST['tag']) && is_array($_POST['tag']))
|
||||
foreach ($_POST['tag'] as $id_tag => $bool)
|
||||
if (is_numeric($id_tag))
|
||||
$new_tags[] = $id_tag;
|
||||
|
||||
$current_tags = array_keys($asset->getTags());
|
||||
|
||||
$tags_to_unlink = array_diff($current_tags, $new_tags);
|
||||
$asset->unlinkTags($tags_to_unlink);
|
||||
|
||||
$tags_to_link = array_diff($new_tags, $current_tags);
|
||||
$asset->linkTags($tags_to_link);
|
||||
|
||||
// Meta data
|
||||
if (isset($_POST['meta_key'], $_POST['meta_value']))
|
||||
{
|
||||
$new_meta = array_filter(array_combine($_POST['meta_key'], $_POST['meta_value']), function($e) {
|
||||
return !empty($e);
|
||||
});
|
||||
|
||||
$asset->setMetaData($new_meta);
|
||||
}
|
||||
|
||||
// A replacement file?
|
||||
if (isset($_FILES['replacement'], $_POST['replacement_target']) && !empty($_FILES['replacement']['tmp_name']))
|
||||
{
|
||||
if ($_POST['replacement_target'] === 'full')
|
||||
{
|
||||
$asset->replaceFile($_FILES['replacement']['tmp_name']);
|
||||
if ($asset->isImage())
|
||||
{
|
||||
$image = $asset->getImage();
|
||||
$image->removeAllThumbnails();
|
||||
}
|
||||
}
|
||||
elseif (preg_match('~^thumb_(\d+)x(\d+)(_c[best]?)?$~', $_POST['replacement_target']))
|
||||
{
|
||||
$image = $asset->getImage();
|
||||
if (($replace_result = $image->replaceThumbnail($_POST['replacement_target'], $_FILES['replacement']['tmp_name'])) !== 0)
|
||||
throw new Exception('Could not replace thumbnail \'' . $_POST['replacement_target'] . '\' with the uploaded file. Error code: ' . $replace_result);
|
||||
}
|
||||
}
|
||||
|
||||
header('Location: ' . BASEURL . '/editasset/?id=' . $asset->getId());
|
||||
}
|
||||
|
||||
// Get list of thumbnails
|
||||
$thumbs = $this->getThumbs($asset);
|
||||
|
||||
$page = new EditAssetForm($asset, $thumbs);
|
||||
parent::__construct('Edit asset \'' . $asset->getTitle() . '\' (' . $asset->getFilename() . ') - ' . SITE_TITLE);
|
||||
$this->page->adopt($page);
|
||||
}
|
||||
|
||||
private function getThumbs(Asset $asset)
|
||||
{
|
||||
$path = $asset->getPath();
|
||||
$thumbs = [];
|
||||
$metadata = $asset->getMeta();
|
||||
foreach ($metadata as $key => $meta)
|
||||
{
|
||||
if (!preg_match('~^thumb_(?<width>\d+)x(?<height>\d+)(?<suffix>_c(?<method>[best]?))?$~', $key, $thumb))
|
||||
continue;
|
||||
|
||||
$has_crop_boundary = isset($metadata['crop_' . $thumb['width'] . 'x' . $thumb['height']]);
|
||||
$has_custom_image = isset($metadata['custom_' . $thumb['width'] . 'x' . $thumb['height']]);
|
||||
$thumbs[] = [
|
||||
'dimensions' => [(int) $thumb['width'], (int) $thumb['height']],
|
||||
'cropped' => !$has_custom_image && (!empty($thumb['suffix']) || $has_crop_boundary),
|
||||
'crop_method' => !$has_custom_image && !empty($thumb['method']) ? $thumb['method'] : (!empty($thumb['suffix']) ? 'c' : null),
|
||||
'crop_region' => $has_crop_boundary ? $metadata['crop_' . $thumb['width'] . 'x' . $thumb['height']] : null,
|
||||
'custom_image' => $has_custom_image,
|
||||
'filename' => $meta,
|
||||
'full_path' => THUMBSDIR . '/' . $path . '/' . $meta,
|
||||
'url' => THUMBSURL . '/' . $path . '/' . $meta,
|
||||
'status' => file_exists(THUMBSDIR . '/' . $path . '/' . $meta),
|
||||
];
|
||||
}
|
||||
|
||||
return $thumbs;
|
||||
}
|
||||
|
||||
private function updateThumb(Image $image)
|
||||
{
|
||||
$data = json_decode($_POST['data']);
|
||||
$meta = $image->getMeta();
|
||||
|
||||
// Set new crop boundary.
|
||||
$crop_key = 'crop_' . $data->thumb_width . 'x' . $data->thumb_height;
|
||||
$crop_value = $data->crop_width . ',' . $data->crop_height . ',' . $data->source_x . ',' . $data->source_y;
|
||||
$meta[$crop_key] = $crop_value;
|
||||
|
||||
// If we uploaded a custom thumbnail, stop considering it such.
|
||||
$custom_key = 'custom_' . $data->thumb_width . 'x' . $data->thumb_height;
|
||||
if (isset($meta[$custom_key]))
|
||||
unset($meta[$custom_key]);
|
||||
|
||||
// Force a rebuild of related thumbnails.
|
||||
$thumb_key = 'thumb_' . $data->thumb_width . 'x' . $data->thumb_height;
|
||||
foreach ($meta as $meta_key => $meta_value)
|
||||
if ($meta_key === $thumb_key || strpos($meta_key, $thumb_key . '_') !== false)
|
||||
unset($meta[$meta_key]);
|
||||
|
||||
$image->setMetaData($meta);
|
||||
|
||||
$payload = [
|
||||
'key' => $crop_key,
|
||||
'value' => $crop_value,
|
||||
'url' => $image->getThumbnailUrl($data->thumb_width, $data->thumb_height, 'exact'),
|
||||
];
|
||||
|
||||
header('Content-Type: text/json; charset=utf-8');
|
||||
echo json_encode($payload);
|
||||
exit;
|
||||
}
|
||||
}
|
195
controllers/EditUser.php
Normal file
195
controllers/EditUser.php
Normal file
@ -0,0 +1,195 @@
|
||||
<?php
|
||||
/*****************************************************************************
|
||||
* EditUser.php
|
||||
* Contains the edit user controller.
|
||||
*
|
||||
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
||||
*****************************************************************************/
|
||||
|
||||
class EditUser extends HTMLController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
// Ensure it's just admins at this point.
|
||||
if (!Registry::get('user')->isAdmin())
|
||||
throw new NotAllowedException();
|
||||
|
||||
// Who are we, again?
|
||||
$current_user = Registry::get('user');
|
||||
|
||||
$id_user = isset($_GET['id']) ? (int) $_GET['id'] : 0;
|
||||
if (empty($id_user) && !isset($_GET['add']))
|
||||
throw new UnexpectedValueException('Requested user not found or not requesting a new user.');
|
||||
|
||||
// Adding a user?
|
||||
if (isset($_GET['add']))
|
||||
{
|
||||
parent::__construct('Add a new user');
|
||||
$view = new DummyBox('Add a new user');
|
||||
$this->page->adopt($view);
|
||||
$this->page->addClass('edituser');
|
||||
}
|
||||
// Deleting one?
|
||||
elseif (isset($_GET['delete']))
|
||||
{
|
||||
// Don't be stupid.
|
||||
if ($current_user->getUserId() == $id_user)
|
||||
trigger_error('Sorry, I cannot allow you to delete yourself.', E_USER_ERROR);
|
||||
|
||||
// So far so good?
|
||||
$user = Member::fromId($id_user);
|
||||
if (Session::validateSession('get') && $user->delete())
|
||||
{
|
||||
header('Location: ' . BASEURL . '/manageusers/');
|
||||
exit;
|
||||
}
|
||||
else
|
||||
trigger_error('Cannot delete user: an error occured while processing the request.', E_USER_ERROR);
|
||||
}
|
||||
// Editing one, then, surely.
|
||||
else
|
||||
{
|
||||
$user = Member::fromId($id_user);
|
||||
parent::__construct('Edit user \'' . $user->getFullName() . '\'');
|
||||
$view = new DummyBox('Edit user \'' . $user->getFullName() . '\'');
|
||||
$this->page->adopt($view);
|
||||
$this->page->addClass('edituser');
|
||||
}
|
||||
|
||||
// Session checking!
|
||||
if (empty($_POST))
|
||||
Session::resetSessionToken();
|
||||
else
|
||||
Session::validateSession();
|
||||
|
||||
if ($id_user && !($current_user->isAdmin() && $current_user->getUserId() == $id_user))
|
||||
$after_form = '<a href="' . BASEURL . '/edituser/?id=' . $id_user . '&delete&' . Session::getSessionTokenKey() . '=' . Session::getSessionToken() . '" class="btn btn-danger" onclick="return confirm(\'Are you sure you want to delete this user? You cannot undo this!\');">Delete user</a>';
|
||||
elseif (!$id_user)
|
||||
$after_form = '<button name="submit_and_new" class="btn">Save and add another</button>';
|
||||
else
|
||||
$after_form = '';
|
||||
|
||||
$form = new Form([
|
||||
'request_url' => BASEURL . '/edituser/?' . ($id_user ? 'id=' . $id_user : 'add'),
|
||||
'content_below' => $after_form,
|
||||
'fields' => [
|
||||
'first_name' => [
|
||||
'type' => 'text',
|
||||
'label' => 'First name',
|
||||
'size' => 50,
|
||||
'maxlength' => 255,
|
||||
],
|
||||
'surname' => [
|
||||
'type' => 'text',
|
||||
'label' => 'Surname',
|
||||
'size' => 50,
|
||||
'maxlength' => 255,
|
||||
],
|
||||
'slug' => [
|
||||
'type' => 'text',
|
||||
'label' => 'URL slug',
|
||||
'size' => 50,
|
||||
'maxlength' => 255,
|
||||
],
|
||||
'emailaddress' => [
|
||||
'type' => 'text',
|
||||
'label' => 'Email address',
|
||||
'size' => 50,
|
||||
'maxlength' => 255,
|
||||
],
|
||||
'password1' => [
|
||||
'type' => 'password',
|
||||
'label' => 'Password',
|
||||
'size' => 50,
|
||||
'maxlength' => 255,
|
||||
'is_optional' => true,
|
||||
],
|
||||
'password2' => [
|
||||
'type' => 'password',
|
||||
'label' => 'Password (repeat)',
|
||||
'size' => 50,
|
||||
'maxlength' => 255,
|
||||
'is_optional' => true,
|
||||
],
|
||||
'is_admin' => [
|
||||
'header' => 'Privileges',
|
||||
'type' => 'checkbox',
|
||||
'label' => 'This user ' . ($id_user ? 'has' : 'should have') . ' administrative privileges.',
|
||||
'is_optional' => true,
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
// Create the form, add in default values.
|
||||
$form->setData($id_user ? $user->getProps() : $_POST);
|
||||
$formview = new FormView($form);
|
||||
$view->adopt($formview);
|
||||
|
||||
if (!empty($_POST))
|
||||
{
|
||||
$form->verify($_POST);
|
||||
|
||||
// Anything missing?
|
||||
if (!empty($form->getMissing()))
|
||||
return $formview->adopt(new DummyBox('Some data missing', 'Please fill out the following fields: ' . implode(', ', $form->getMissing())));
|
||||
|
||||
$data = $form->getData();
|
||||
|
||||
// Just to be on the safe side.
|
||||
$data['first_name'] = htmlentities(trim($data['first_name']));
|
||||
$data['surname'] = htmlentities(trim($data['surname']));
|
||||
$data['emailaddress'] = trim($data['emailaddress']);
|
||||
|
||||
// Make sure there's a slug.
|
||||
if (empty($data['slug']))
|
||||
$data['slug'] = $data['first_name'];
|
||||
|
||||
// Quick stripping.
|
||||
$data['slug'] = strtr(strtolower($data['slug']), [' ' => '-', '--' => '-', '&' => 'and', '=>' => '', "'" => "", ":"=> "", '/' => '-', '\\' => '-']);
|
||||
|
||||
// Checkboxes, fun!
|
||||
$data['is_admin'] = empty($data['is_admin']) ? 0 : 1;
|
||||
|
||||
// If it looks like an e-mail address...
|
||||
if (!empty($data['emailaddress']) && !preg_match('~^[^ ]+@[^ ]+\.[a-z]+$~', $data['emailaddress']))
|
||||
return $formview->adopt(new DummyBox('Email addresses invalid', 'The email address you entered is not a valid email address.'));
|
||||
// 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 DummyBox('Email address already in use', 'Another account is already using the e-mail address you entered.'));
|
||||
|
||||
// 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 DummyBox('Password not acceptable', 'Please fill in a password that is at least six characters long and contains at least one non-alphabetic character (e.g. a number or symbol).'));
|
||||
elseif ($data['password1'] !== $data['password2'])
|
||||
return $formview->adopt(new DummyBox('Passwords do not match', 'The passwords you entered do not match. Please try again.'));
|
||||
else
|
||||
$data['password'] = $data['password1'];
|
||||
|
||||
unset($data['password1'], $data['password2']);
|
||||
}
|
||||
|
||||
// Creating a new user?
|
||||
if (!$id_user)
|
||||
{
|
||||
$return = Member::createNew($data);
|
||||
if ($return === false)
|
||||
return $formview->adopt(new DummyBox('Cannot create this user', 'Something went wrong while creating the user...'));
|
||||
|
||||
if (isset($_POST['submit_and_new']))
|
||||
{
|
||||
header('Location: ' . BASEURL . '/edituser/?add');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
// Just updating?
|
||||
else
|
||||
$user->update($data);
|
||||
|
||||
// Redirect to the user management page.
|
||||
header('Location: ' . BASEURL . '/manageusers/');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
}
|
34
controllers/HTMLController.php
Normal file
34
controllers/HTMLController.php
Normal file
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
/*****************************************************************************
|
||||
* HTMLController.php
|
||||
* Contains the key HTML controller
|
||||
*
|
||||
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
||||
*****************************************************************************/
|
||||
|
||||
/**
|
||||
* The abstract class that allows easy creation of html pages.
|
||||
*/
|
||||
abstract class HTMLController
|
||||
{
|
||||
protected $page;
|
||||
protected $admin_bar;
|
||||
|
||||
public function __construct($title)
|
||||
{
|
||||
header('Content-Type: text/html; charset=utf-8');
|
||||
$this->page = new MainTemplate($title);
|
||||
|
||||
if (Registry::get('user')->isAdmin())
|
||||
{
|
||||
$this->page->appendStylesheet(BASEURL . '/css/admin.css');
|
||||
$this->admin_bar = new AdminBar();
|
||||
$this->page->adopt($this->admin_bar);
|
||||
}
|
||||
}
|
||||
|
||||
public function showContent()
|
||||
{
|
||||
$this->page->html_main();
|
||||
}
|
||||
}
|
21
controllers/JSONController.php
Normal file
21
controllers/JSONController.php
Normal file
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
/*****************************************************************************
|
||||
* JSONController.php
|
||||
* Contains the key JSON controller
|
||||
*
|
||||
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
||||
*****************************************************************************/
|
||||
|
||||
/**
|
||||
* The abstract class that allows easy creation of json replies.
|
||||
*/
|
||||
class JSONController
|
||||
{
|
||||
protected $payload;
|
||||
|
||||
public function showContent()
|
||||
{
|
||||
header('Content-Type: text/json; charset=utf-8');
|
||||
echo json_encode($this->payload);
|
||||
}
|
||||
}
|
56
controllers/Login.php
Normal file
56
controllers/Login.php
Normal file
@ -0,0 +1,56 @@
|
||||
<?php
|
||||
/*****************************************************************************
|
||||
* Login.php
|
||||
* Contains the controller for logging the user in.
|
||||
*
|
||||
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
||||
*****************************************************************************/
|
||||
|
||||
class Login extends HTMLController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
// No need to log in twice, dear heart!
|
||||
if (Registry::get('user')->isLoggedIn())
|
||||
{
|
||||
if (Registry::get('user')->isAdmin())
|
||||
header('Location: ' . BASEURL . '/admin/');
|
||||
else
|
||||
header('Location: ' . BASEURL . '/');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Sanity check
|
||||
$login_error = false;
|
||||
if (isset($_POST['emailaddress'], $_POST['password']))
|
||||
{
|
||||
if (Authentication::checkPassword($_POST['emailaddress'], $_POST['password']))
|
||||
{
|
||||
parent::__construct('Login');
|
||||
$_SESSION['user_id'] = Authentication::getUserId($_POST['emailaddress']);
|
||||
|
||||
if (isset($_POST['redirect_url']))
|
||||
header('Location: ' . base64_decode($_POST['redirect_url']));
|
||||
elseif (isset($_SESSION['login_url']))
|
||||
header('Location: ' . $_SESSION['redirect_url']);
|
||||
else
|
||||
header('Location: ' . BASEURL . '/admin/');
|
||||
exit;
|
||||
}
|
||||
else
|
||||
$login_error = true;
|
||||
}
|
||||
|
||||
parent::__construct('Log in');
|
||||
$this->page->appendStylesheet(BASEURL . '/css/admin.css');
|
||||
$form = new LogInForm('Log in');
|
||||
if ($login_error)
|
||||
$form->setErrorMessage('Invalid email address or password.');
|
||||
|
||||
// Tried anything? Be helpful, at least.
|
||||
if (isset($_POST['emailaddress']))
|
||||
$form->setEmail($_POST['emailaddress']);
|
||||
|
||||
$this->page->adopt($form);
|
||||
}
|
||||
}
|
20
controllers/Logout.php
Normal file
20
controllers/Logout.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
/*****************************************************************************
|
||||
* Logout.php
|
||||
* Contains the controller for logging the user out.
|
||||
*
|
||||
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
||||
*****************************************************************************/
|
||||
|
||||
class Logout extends HTMLController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
// Clear the entire sesssion.
|
||||
$_SESSION = [];
|
||||
|
||||
// Back to the frontpage you go.
|
||||
header('Location: ' . BASEURL);
|
||||
exit;
|
||||
}
|
||||
}
|
122
controllers/ManageErrors.php
Normal file
122
controllers/ManageErrors.php
Normal file
@ -0,0 +1,122 @@
|
||||
<?php
|
||||
/*****************************************************************************
|
||||
* ManageErrors.php
|
||||
* Contains the controller for managing errors.
|
||||
*
|
||||
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
||||
*****************************************************************************/
|
||||
|
||||
class ManageErrors extends HTMLController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
// Ensure it's just admins at this point.
|
||||
if (!Registry::get('user')->isAdmin())
|
||||
throw new NotAllowedException();
|
||||
|
||||
// Flushing, are we?
|
||||
if (isset($_POST['flush']))
|
||||
ErrorLog::flush();
|
||||
|
||||
$options = [
|
||||
'title' => 'Error log',
|
||||
'form' => [
|
||||
'action' => BASEURL . '/manageerrors/',
|
||||
'method' => 'post',
|
||||
'class' => 'floatright',
|
||||
'buttons' => [
|
||||
'flush' => [
|
||||
'type' => 'submit',
|
||||
'caption' => 'Delete all',
|
||||
],
|
||||
],
|
||||
],
|
||||
'columns' => [
|
||||
'id' => [
|
||||
'value' => 'id_entry',
|
||||
'header' => '#',
|
||||
'is_sortable' => true,
|
||||
],
|
||||
'message' => [
|
||||
'parse' => [
|
||||
'type' => 'function',
|
||||
'data' => function($row) {
|
||||
return $row['message'] . '<br><div><a onclick="this.parentNode.childNodes[1].style.display=\'block\';this.style.display=\'none\';">Show debug info</a>' .
|
||||
'<pre style="display: none">' . $row['debug_info'] . '</pre></div>' .
|
||||
'<small><a href="' . BASEURL . $row['request_uri'] . '">' . $row['request_uri'] . '</a></small>';
|
||||
}
|
||||
],
|
||||
'header' => 'Message / URL',
|
||||
'is_sortable' => false,
|
||||
],
|
||||
'file' => [
|
||||
'value' => 'file',
|
||||
'header' => 'File',
|
||||
'is_sortable' => true,
|
||||
],
|
||||
'line' => [
|
||||
'value' => 'line',
|
||||
'header' => 'Line',
|
||||
'is_sortable' => true,
|
||||
],
|
||||
'time' => [
|
||||
'parse' => [
|
||||
'type' => 'timestamp',
|
||||
'data' => [
|
||||
'timestamp' => 'time',
|
||||
'pattern' => 'long',
|
||||
],
|
||||
],
|
||||
'header' => 'Time',
|
||||
'is_sortable' => true,
|
||||
],
|
||||
'ip' => [
|
||||
'value' => 'ip_address',
|
||||
'header' => 'IP',
|
||||
'is_sortable' => true,
|
||||
],
|
||||
'uid' => [
|
||||
'header' => 'UID',
|
||||
'is_sortable' => true,
|
||||
'parse' => [
|
||||
'link' => BASEURL . '/member/?id={ID_USER}',
|
||||
'data' => 'id_user',
|
||||
],
|
||||
],
|
||||
],
|
||||
'start' => !empty($_GET['start']) ? (int) $_GET['start'] : 0,
|
||||
'sort_order' => !empty($_GET['order']) ? $_GET['order'] : '',
|
||||
'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',
|
||||
'base_url' => BASEURL . '/manageerrors/',
|
||||
'get_count' => 'ErrorLog::getCount',
|
||||
'get_data' => function($offset = 0, $limit = 20, $order = '', $direction = 'down') {
|
||||
if (!in_array($order, ['id_entry', 'file', 'line', 'time', 'ipaddress', 'id_user']))
|
||||
$order = 'id_entry';
|
||||
|
||||
$data = Registry::get('db')->queryAssocs('
|
||||
SELECT *
|
||||
FROM log_errors
|
||||
ORDER BY {raw:order}
|
||||
LIMIT {int:offset}, {int:limit}',
|
||||
[
|
||||
'order' => $order . ($direction === 'up' ? ' ASC' : ' DESC'),
|
||||
'offset' => $offset,
|
||||
'limit' => $limit,
|
||||
]);
|
||||
|
||||
return [
|
||||
'rows' => $data,
|
||||
'order' => $order,
|
||||
'direction' => $direction,
|
||||
];
|
||||
},
|
||||
];
|
||||
|
||||
$error_log = new GenericTable($options);
|
||||
parent::__construct('Error log - Page ' . $error_log->getCurrentPage() .' - ' . SITE_TITLE);
|
||||
$this->page->adopt(new TabularData($error_log));
|
||||
}
|
||||
}
|
122
controllers/ManageTags.php
Normal file
122
controllers/ManageTags.php
Normal file
@ -0,0 +1,122 @@
|
||||
<?php
|
||||
/*****************************************************************************
|
||||
* ManageTags.php
|
||||
* Contains the controller with the admin's list of tags.
|
||||
*
|
||||
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
||||
*****************************************************************************/
|
||||
|
||||
class ManageTags extends HTMLController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
// Ensure it's just admins at this point.
|
||||
if (!Registry::get('user')->isAdmin())
|
||||
throw new NotAllowedException();
|
||||
|
||||
if (isset($_REQUEST['create']) && isset($_POST['tag']))
|
||||
$this->handleTagCreation();
|
||||
|
||||
$options = [
|
||||
'columns' => [
|
||||
'id_post' => [
|
||||
'value' => 'id_tag',
|
||||
'header' => 'ID',
|
||||
'is_sortable' => true,
|
||||
],
|
||||
'tag' => [
|
||||
'header' => 'Tag',
|
||||
'is_sortable' => true,
|
||||
'parse' => [
|
||||
'link' => BASEURL . '/managetag/?id={ID_TAG}',
|
||||
'data' => 'tag',
|
||||
],
|
||||
],
|
||||
'slug' => [
|
||||
'header' => 'Slug',
|
||||
'is_sortable' => true,
|
||||
'parse' => [
|
||||
'link' => BASEURL . '/managetag/?id={ID_TAG}',
|
||||
'data' => 'slug',
|
||||
],
|
||||
],
|
||||
'count' => [
|
||||
'header' => 'Cardinality',
|
||||
'is_sortable' => true,
|
||||
'value' => 'count',
|
||||
],
|
||||
],
|
||||
'start' => !empty($_GET['start']) ? (int) $_GET['start'] : 0,
|
||||
'sort_order' => !empty($_GET['order']) ? $_GET['order'] : null,
|
||||
'sort_direction' => !empty($_GET['dir']) ? $_GET['dir'] : null,
|
||||
'title' => 'Manage tags',
|
||||
'no_items_label' => 'No tags meet the requirements of the current filter.',
|
||||
'items_per_page' => 25,
|
||||
'base_url' => BASEURL . '/managetags/',
|
||||
'get_data' => function($offset = 0, $limit = 15, $order = '', $direction = 'up') {
|
||||
if (!in_array($order, ['id_post', 'tag', 'slug', 'count']))
|
||||
$order = 'tag';
|
||||
if (!in_array($direction, ['up', 'down']))
|
||||
$direction = 'up';
|
||||
|
||||
$data = Registry::get('db')->queryAssocs('
|
||||
SELECT *
|
||||
FROM tags
|
||||
ORDER BY {raw:order}
|
||||
LIMIT {int:offset}, {int:limit}',
|
||||
[
|
||||
'order' => $order . ($direction == 'up' ? ' ASC' : ' DESC'),
|
||||
'offset' => $offset,
|
||||
'limit' => $limit,
|
||||
]);
|
||||
|
||||
return [
|
||||
'rows' => $data,
|
||||
'order' => $order,
|
||||
'direction' => ($direction == 'up' ? 'up' : 'down'),
|
||||
];
|
||||
},
|
||||
'get_count' => function() {
|
||||
return Registry::get('db')->queryValue('
|
||||
SELECT COUNT(*)
|
||||
FROM tags');
|
||||
}
|
||||
];
|
||||
|
||||
$table = new GenericTable($options);
|
||||
parent::__construct('Tag management - Page ' . $table->getCurrentPage() .' - ' . SITE_TITLE);
|
||||
$this->page->adopt(new TabularData($table));
|
||||
}
|
||||
|
||||
private function handleTagCreation()
|
||||
{
|
||||
header('Content-Type: text/json; charset=utf-8');
|
||||
|
||||
// It better not already exist!
|
||||
if (Tag::exactMatch($_POST['tag']))
|
||||
{
|
||||
echo '{"error":"Tag already exists!"}';
|
||||
exit;
|
||||
}
|
||||
|
||||
$label = htmlentities(trim($_POST['tag']));
|
||||
$slug = strtr(strtolower($label), [' ' => '-']);
|
||||
$tag = Tag::createNew([
|
||||
'tag' => $label,
|
||||
'slug' => $slug,
|
||||
]);
|
||||
|
||||
// Did we succeed?
|
||||
if (!$tag)
|
||||
{
|
||||
echo '{"error":"Could not create tag."}';
|
||||
exit;
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'label' => $tag->tag,
|
||||
'id_tag' => $tag->id_tag,
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
}
|
131
controllers/ManageUsers.php
Normal file
131
controllers/ManageUsers.php
Normal file
@ -0,0 +1,131 @@
|
||||
<?php
|
||||
/*****************************************************************************
|
||||
* ManageUsers.php
|
||||
* Contains the controller with the list of users.
|
||||
*
|
||||
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
||||
*****************************************************************************/
|
||||
|
||||
class ManageUsers extends HTMLController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
// Ensure it's just admins at this point.
|
||||
if (!Registry::get('user')->isAdmin())
|
||||
throw new NotAllowedException();
|
||||
|
||||
$options = [
|
||||
'form' => [
|
||||
'action' => BASEURL . '/edituser/',
|
||||
'method' => 'get',
|
||||
'class' => 'floatright',
|
||||
'buttons' => [
|
||||
'add' => [
|
||||
'type' => 'submit',
|
||||
'caption' => 'Add new user',
|
||||
],
|
||||
],
|
||||
],
|
||||
'columns' => [
|
||||
'id_user' => [
|
||||
'value' => 'id_user',
|
||||
'header' => 'ID',
|
||||
'is_sortable' => true,
|
||||
],
|
||||
'surname' => [
|
||||
'header' => 'Last name',
|
||||
'is_sortable' => true,
|
||||
'parse' => [
|
||||
'link' => BASEURL . '/edituser/?id={ID_USER}',
|
||||
'data' => 'surname',
|
||||
],
|
||||
],
|
||||
'first_name' => [
|
||||
'header' => 'First name',
|
||||
'is_sortable' => true,
|
||||
'parse' => [
|
||||
'link' => BASEURL . '/edituser/?id={ID_USER}',
|
||||
'data' => 'first_name',
|
||||
],
|
||||
],
|
||||
'slug' => [
|
||||
'header' => 'Slug',
|
||||
'is_sortable' => true,
|
||||
'parse' => [
|
||||
'link' => BASEURL . '/edituser/?id={ID_USER}',
|
||||
'data' => 'slug',
|
||||
],
|
||||
],
|
||||
'emailaddress' => [
|
||||
'value' => 'emailaddress',
|
||||
'header' => 'Email address',
|
||||
'is_sortable' => true,
|
||||
],
|
||||
'last_action_time' => [
|
||||
'parse' => [
|
||||
'type' => 'timestamp',
|
||||
'data' => [
|
||||
'timestamp' => 'last_action_time',
|
||||
'pattern' => 'long',
|
||||
],
|
||||
],
|
||||
'header' => 'Last activity',
|
||||
'is_sortable' => true,
|
||||
],
|
||||
'ip_address' => [
|
||||
'is_sortable' => true,
|
||||
'value' => 'ip_address',
|
||||
'header' => 'IP address',
|
||||
],
|
||||
'is_admin' => [
|
||||
'is_sortable' => true,
|
||||
'header' => 'Admin?',
|
||||
'parse' => [
|
||||
'type' => 'function',
|
||||
'data' => function($row) {
|
||||
return $row['is_admin'] ? 'yes' : 'no';
|
||||
}
|
||||
],
|
||||
],
|
||||
],
|
||||
'start' => !empty($_GET['start']) ? (int) $_GET['start'] : 0,
|
||||
'sort_order' => !empty($_GET['order']) ? $_GET['order'] : '',
|
||||
'sort_direction' => !empty($_GET['dir']) ? $_GET['dir'] : '',
|
||||
'title' => 'Manage users',
|
||||
'no_items_label' => 'No users meet the requirements of the current filter.',
|
||||
'items_per_page' => 15,
|
||||
'index_class' => 'floatleft',
|
||||
'base_url' => BASEURL . '/manageusers/',
|
||||
'get_data' => function($offset = 0, $limit = 15, $order = '', $direction = 'down') {
|
||||
if (!in_array($order, ['id_user', 'surname', 'first_name', 'slug', 'emailaddress', 'last_action_time', 'ip_address', 'is_admin']))
|
||||
$order = 'id_user';
|
||||
|
||||
$data = Registry::get('db')->queryAssocs('
|
||||
SELECT *
|
||||
FROM users
|
||||
ORDER BY {raw:order}
|
||||
LIMIT {int:offset}, {int:limit}',
|
||||
[
|
||||
'order' => $order . ($direction == 'up' ? ' ASC' : ' DESC'),
|
||||
'offset' => $offset,
|
||||
'limit' => $limit,
|
||||
]);
|
||||
|
||||
return [
|
||||
'rows' => $data,
|
||||
'order' => $order,
|
||||
'direction' => $direction,
|
||||
];
|
||||
},
|
||||
'get_count' => function() {
|
||||
return Registry::get('db')->queryValue('
|
||||
SELECT COUNT(*)
|
||||
FROM users');
|
||||
}
|
||||
];
|
||||
|
||||
$table = new GenericTable($options);
|
||||
parent::__construct('User management - Page ' . $table->getCurrentPage() .' - ' . SITE_TITLE);
|
||||
$this->page->adopt(new TabularData($table));
|
||||
}
|
||||
}
|
41
controllers/ProvideAutoSuggest.php
Normal file
41
controllers/ProvideAutoSuggest.php
Normal file
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
/*****************************************************************************
|
||||
* ProvideAutoSuggest.php
|
||||
* Contains the autosuggest provider.
|
||||
*
|
||||
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
||||
*****************************************************************************/
|
||||
|
||||
class ProvideAutoSuggest extends JSONController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
// Ensure it's just admins at this point.
|
||||
if (!Registry::get('user')->isAdmin())
|
||||
throw new NotAllowedException();
|
||||
|
||||
if (!isset($_GET['type']))
|
||||
throw new UnexpectedValueException('Unsupported autosuggest request.');
|
||||
|
||||
if ($_GET['type'] === 'tags' && isset($_GET['data']))
|
||||
{
|
||||
$data = array_unique(explode(' ', urldecode($_GET['data'])));
|
||||
$data = array_filter($data, function($item) {
|
||||
return strlen($item) >= 3;
|
||||
});
|
||||
|
||||
$this->payload = ['items' => []];
|
||||
|
||||
// Nothing to look for?
|
||||
if (count($data) === 0)
|
||||
return;
|
||||
|
||||
$results = Tag::match($data);
|
||||
foreach ($results as $id_tag => $tag)
|
||||
$this->payload['items'][] = [
|
||||
'label' => $tag,
|
||||
'id_tag' => $id_tag,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
58
controllers/UploadMedia.php
Normal file
58
controllers/UploadMedia.php
Normal file
@ -0,0 +1,58 @@
|
||||
<?php
|
||||
/*****************************************************************************
|
||||
* UploadMedia.php
|
||||
* Contains the media uploading controller
|
||||
*
|
||||
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
||||
*****************************************************************************/
|
||||
|
||||
class UploadMedia extends HTMLController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
// Ensure it's just admins at this point.
|
||||
if (!Registry::get('user')->isAdmin())
|
||||
throw new NotAllowedException();
|
||||
|
||||
$page = new MediaUploader();
|
||||
parent::__construct('Upload new media - ' . SITE_TITLE);
|
||||
$this->page->adopt($page);
|
||||
|
||||
// Are we saving something?
|
||||
if (isset($_POST['save']))
|
||||
{
|
||||
if (empty($_FILES) || empty($_FILES['new_asset']))
|
||||
return;
|
||||
|
||||
// Any tags?
|
||||
$new_tags = [];
|
||||
if (isset($_POST['tag']) && is_array($_POST['tag']))
|
||||
{
|
||||
foreach ($_POST['tag'] as $id_tag => $bool)
|
||||
if (is_numeric($id_tag))
|
||||
$new_tags[] = $id_tag;
|
||||
}
|
||||
|
||||
var_dump($_FILES);
|
||||
var_dump($_POST);
|
||||
|
||||
foreach ($_FILES['new_asset']['tmp_name'] as $num => $uploaded_file)
|
||||
{
|
||||
if (empty($uploaded_file))
|
||||
continue;
|
||||
|
||||
$asset = Asset::createNew([
|
||||
'filename_to_copy' => $uploaded_file,
|
||||
'preferred_filename' => $_FILES['new_asset']['name'][$num],
|
||||
'title' => !empty($_POST['title'][$num]) ? $_POST['title'][$num] : null,
|
||||
]);
|
||||
|
||||
$asset->linkTags($new_tags);
|
||||
}
|
||||
|
||||
// Prevent uploading twice.
|
||||
header('Location: ' . BASEURL . '/uploadmedia/');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
}
|
45
controllers/ViewPeople.php
Normal file
45
controllers/ViewPeople.php
Normal file
@ -0,0 +1,45 @@
|
||||
<?php
|
||||
/*****************************************************************************
|
||||
* ViewPeople.php
|
||||
* Contains the people index controller
|
||||
*
|
||||
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
||||
*****************************************************************************/
|
||||
|
||||
class ViewPeople extends HTMLController
|
||||
{
|
||||
const PER_PAGE = 24;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
// Fetch subalbums.
|
||||
// !!! TODO: pagination.
|
||||
$subalbums = Tag::getPeople();
|
||||
|
||||
// What assets are we using?
|
||||
$id_assets = array_map(function($album) {
|
||||
return (int) $album['id_asset_thumb'];
|
||||
}, $subalbums);
|
||||
|
||||
// Fetch assets for thumbnails.
|
||||
$assets = Asset::fromIds($id_assets, 'object');
|
||||
|
||||
// Build album list.
|
||||
$albums = [];
|
||||
foreach ($subalbums as $album)
|
||||
{
|
||||
$albums[$album['id_tag']] = [
|
||||
'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,
|
||||
];
|
||||
}
|
||||
|
||||
$index = new AlbumIndex($albums);
|
||||
|
||||
parent::__construct('People - ' . SITE_TITLE);
|
||||
$this->page->adopt($index);
|
||||
$this->page->setCanonicalUrl(BASEURL . '/people/');
|
||||
}
|
||||
}
|
130
controllers/ViewPhotoAlbum.php
Normal file
130
controllers/ViewPhotoAlbum.php
Normal file
@ -0,0 +1,130 @@
|
||||
<?php
|
||||
/*****************************************************************************
|
||||
* ViewPhotoAlbum.php
|
||||
* Contains the photo album index controller
|
||||
*
|
||||
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
||||
*****************************************************************************/
|
||||
|
||||
class ViewPhotoAlbum extends HTMLController
|
||||
{
|
||||
protected $iterator;
|
||||
protected $total_count;
|
||||
protected $base_url;
|
||||
|
||||
const PER_PAGE = 24;
|
||||
|
||||
public function __construct($title = 'Photos - ' . SITE_TITLE)
|
||||
{
|
||||
// Viewing an album?
|
||||
if (isset($_GET['tag']))
|
||||
{
|
||||
$tag = Tag::fromSlug($_GET['tag']);
|
||||
$id_tag = $tag->id_tag;
|
||||
$title = $tag->tag;
|
||||
$description = !empty($tag->description) ? '<p>' . $tag->description . '</p>' : '';
|
||||
|
||||
// Can we go up a level?
|
||||
if ($tag->id_parent != 0)
|
||||
{
|
||||
$ptag = Tag::fromId($tag->id_parent);
|
||||
$description .= '<p><a href="' . BASEURL . '/' . (!empty($ptag->slug) ? $ptag->slug . '/' : '') . '">« Go back to "' . $ptag->tag . '"</a></p>';
|
||||
}
|
||||
elseif ($tag->kind === 'Person')
|
||||
$description .= '<p><a href="' . BASEURL . '/people/">« Go back to "People"</a></p>';
|
||||
}
|
||||
// View the album root.
|
||||
else
|
||||
{
|
||||
$id_tag = 1;
|
||||
$title = 'Albums';
|
||||
$description = '';
|
||||
}
|
||||
|
||||
// What page are we at?
|
||||
$page = isset($_GET['page']) ? (int) $_GET['page'] : 1;
|
||||
|
||||
parent::__construct($title . ' - Page ' . $page . ' - ' . SITE_TITLE);
|
||||
|
||||
if ($id_tag !== 1)
|
||||
$this->page->adopt(new DummyBox($title, $description, 'page_title_box'));
|
||||
|
||||
// Fetch subalbums, but only if we're on the first page.
|
||||
if ($page === 1)
|
||||
{
|
||||
$albums = $this->getAlbums($id_tag);
|
||||
$index = new AlbumIndex($albums);
|
||||
$this->page->adopt($index);
|
||||
}
|
||||
|
||||
// Load a photo mosaic for the current tag.
|
||||
list($mosaic, $total_count) = $this->getPhotoMosaic($id_tag, $page);
|
||||
if (isset($mosaic))
|
||||
$this->page->adopt(new PhotosIndex($mosaic, Registry::get('user')->isAdmin()));
|
||||
|
||||
// Make a page index as needed, while we're at it.
|
||||
if ($total_count > self::PER_PAGE)
|
||||
{
|
||||
$index = new PageIndex([
|
||||
'recordCount' => $total_count,
|
||||
'items_per_page' => self::PER_PAGE,
|
||||
'start' => (isset($_GET['page']) ? $_GET['page'] - 1 : 0) * self::PER_PAGE,
|
||||
'base_url' => BASEURL . '/' . (isset($_GET['tag']) ? $_GET['tag'] . '/' : ''),
|
||||
'page_slug' => 'page/%PAGE%/',
|
||||
]);
|
||||
$this->page->adopt(new Pagination($index));
|
||||
}
|
||||
|
||||
// Set the canonical url.
|
||||
$this->page->setCanonicalUrl(BASEURL . '/' . (isset($_GET['tag']) ? $_GET['tag'] . '/' : ''));
|
||||
}
|
||||
|
||||
public function getPhotoMosaic($id_tag, $page)
|
||||
{
|
||||
// Create an iterator.
|
||||
list($this->iterator, $total_count) = AssetIterator::getByOptions([
|
||||
'id_tag' => $id_tag,
|
||||
'order' => 'date_captured',
|
||||
'direction' => 'desc',
|
||||
'limit' => self::PER_PAGE,
|
||||
'page' => $page,
|
||||
], true);
|
||||
|
||||
$mosaic = $total_count > 0 ? new PhotoMosaic($this->iterator) : null;
|
||||
return [$mosaic, $total_count];
|
||||
}
|
||||
|
||||
private function getAlbums($id_tag)
|
||||
{
|
||||
// Fetch subalbums.
|
||||
$subalbums = Tag::getAlbums($id_tag);
|
||||
|
||||
// What assets are we using?
|
||||
$id_assets = array_map(function($album) {
|
||||
return (int) $album['id_asset_thumb'];
|
||||
}, $subalbums);
|
||||
|
||||
// Fetch assets for thumbnails.
|
||||
$assets = Asset::fromIds($id_assets, 'object');
|
||||
|
||||
// Build album list.
|
||||
$albums = [];
|
||||
foreach ($subalbums as $album)
|
||||
{
|
||||
$albums[$album['id_tag']] = [
|
||||
'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,
|
||||
];
|
||||
}
|
||||
|
||||
return $albums;
|
||||
}
|
||||
|
||||
public function __destruct()
|
||||
{
|
||||
if (isset($this->iterator))
|
||||
$this->iterator->clean();
|
||||
}
|
||||
}
|
58
controllers/ViewTimeline.php
Normal file
58
controllers/ViewTimeline.php
Normal file
@ -0,0 +1,58 @@
|
||||
<?php
|
||||
/*****************************************************************************
|
||||
* ViewTimeline.php
|
||||
* Contains the photo timeline index controller
|
||||
*
|
||||
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
||||
*****************************************************************************/
|
||||
|
||||
class ViewTimeline extends HTMLController
|
||||
{
|
||||
protected $iterator;
|
||||
protected $total_count;
|
||||
protected $base_url;
|
||||
|
||||
const PER_PAGE = 24;
|
||||
|
||||
public function __construct($title = 'Photos - ' . SITE_TITLE)
|
||||
{
|
||||
// What page are we at?
|
||||
$page = isset($_GET['page']) ? (int) $_GET['page'] : 1;
|
||||
|
||||
parent::__construct('Timeline - Page ' . $page . ' - ' . SITE_TITLE);
|
||||
|
||||
// Load a photo mosaic.
|
||||
list($this->iterator, $total_count) = AssetIterator::getByOptions([
|
||||
'order' => 'date_captured',
|
||||
'direction' => 'desc',
|
||||
'limit' => self::PER_PAGE,
|
||||
'page' => $page,
|
||||
], true);
|
||||
|
||||
$mosaic = $total_count > 0 ? new PhotoMosaic($this->iterator) : null;
|
||||
if (isset($mosaic))
|
||||
$this->page->adopt(new PhotosIndex($mosaic, Registry::get('user')->isAdmin()));
|
||||
|
||||
// Make a page index as needed, while we're at it.
|
||||
if ($total_count > self::PER_PAGE)
|
||||
{
|
||||
$index = new PageIndex([
|
||||
'recordCount' => $total_count,
|
||||
'items_per_page' => self::PER_PAGE,
|
||||
'start' => (isset($_GET['page']) ? $_GET['page'] - 1 : 0) * self::PER_PAGE,
|
||||
'base_url' => BASEURL . '/timeline/',
|
||||
'page_slug' => 'page/%PAGE%/',
|
||||
]);
|
||||
$this->page->adopt(new Pagination($index));
|
||||
}
|
||||
|
||||
// Set the canonical url.
|
||||
$this->page->setCanonicalUrl(BASEURL . '/timeline/');
|
||||
}
|
||||
|
||||
public function __destruct()
|
||||
{
|
||||
if (isset($this->iterator))
|
||||
$this->iterator->clean();
|
||||
}
|
||||
}
|
256
import_albums.php
Normal file
256
import_albums.php
Normal file
@ -0,0 +1,256 @@
|
||||
<?php
|
||||
/*****************************************************************************
|
||||
* import_albums.php
|
||||
* Imports albums from a Gallery 3 database.
|
||||
*
|
||||
* Kabuki CMS (C) 2013-2016, Aaron van Geffen
|
||||
*****************************************************************************/
|
||||
|
||||
// Include the project's configuration.
|
||||
require_once 'config.php';
|
||||
|
||||
// Set up the autoloader.
|
||||
require_once 'vendor/autoload.php';
|
||||
|
||||
// Initialise the database.
|
||||
$db = new Database(DB_SERVER, DB_USER, DB_PASS, DB_NAME);
|
||||
$pdb = new Database(DB_SERVER, DB_USER, DB_PASS, "hashru_gallery");
|
||||
Registry::set('db', $db);
|
||||
|
||||
// Do some authentication checks.
|
||||
Session::start();
|
||||
Registry::set('user', Authentication::isLoggedIn() ? Member::fromId($_SESSION['user_id']) : new Guest());
|
||||
|
||||
// Enable debugging.
|
||||
//set_error_handler('ErrorHandler::handleError');
|
||||
ini_set("display_errors", DEBUG ? "On" : "Off");
|
||||
|
||||
/*******************************
|
||||
* STEP 1: ALBUMS
|
||||
*******************************/
|
||||
|
||||
$num_albums = $pdb->queryValue('
|
||||
SELECT COUNT(*)
|
||||
FROM items
|
||||
WHERE type = {string:album}
|
||||
ORDER BY id ASC',
|
||||
['album' => 'album']);
|
||||
|
||||
echo $num_albums, ' albums to import.', "\n";
|
||||
|
||||
$albums = $pdb->query('
|
||||
SELECT id, album_cover_item_id, parent_id, title, description, relative_path_cache, relative_url_cache
|
||||
FROM items
|
||||
WHERE type = {string:album}
|
||||
ORDER BY id ASC',
|
||||
['album' => 'album']);
|
||||
|
||||
$tags = [];
|
||||
$old_album_id_to_new_tag_id = [];
|
||||
$dirnames_by_old_album_id = [];
|
||||
$old_thumb_id_by_tag_id = [];
|
||||
|
||||
while ($album = $pdb->fetch_assoc($albums))
|
||||
{
|
||||
$tag = Tag::createNew([
|
||||
'tag' => $album['title'],
|
||||
'slug' => $album['relative_url_cache'],
|
||||
'kind' => 'Album',
|
||||
'description' => $album['description'],
|
||||
]);
|
||||
|
||||
if (!empty($album['parent_id']))
|
||||
$parent_to_set[$tag->id_tag] = $album['parent_id'];
|
||||
|
||||
$tags[$tag->id_tag] = $tag;
|
||||
$old_album_id_to_new_tag_id[$album['id']] = $tag->id_tag;
|
||||
$dirnames_by_old_album_id[$album['id']] = str_replace('#', '', urldecode($album['relative_path_cache']));
|
||||
$old_thumb_id_by_tag_id[$tag->id_tag] = $album['album_cover_item_id'];
|
||||
}
|
||||
|
||||
$pdb->free_result($albums);
|
||||
|
||||
foreach ($parent_to_set as $id_tag => $old_album_id)
|
||||
{
|
||||
$id_parent = $old_album_id_to_new_tag_id[$old_album_id];
|
||||
$db->query('
|
||||
UPDATE tags
|
||||
SET id_parent = ' . $id_parent . '
|
||||
WHERE id_tag = ' . $id_tag);
|
||||
}
|
||||
|
||||
unset($parent_to_set);
|
||||
|
||||
/*******************************
|
||||
* STEP 2: PHOTOS
|
||||
*******************************/
|
||||
|
||||
$num_photos = $pdb->queryValue('
|
||||
SELECT COUNT(*)
|
||||
FROM items
|
||||
WHERE type = {string:photo}',
|
||||
['photo' => "photo"]);
|
||||
|
||||
echo $num_photos, " photos to import.\n";
|
||||
|
||||
$old_photo_id_to_asset_id = [];
|
||||
for ($i = 0; $i < $num_photos; $i += 50)
|
||||
{
|
||||
echo 'Offset ' . $i . "...\n";
|
||||
|
||||
$photos = $pdb->query('
|
||||
SELECT id, parent_id, captured, created, name, title, description, relative_url_cache, width, height, mime_type, weight
|
||||
FROM items
|
||||
WHERE type = {string:photo}
|
||||
ORDER BY id ASC
|
||||
LIMIT ' . $i . ', 50',
|
||||
['photo' => 'photo']);
|
||||
|
||||
while ($photo = $pdb->fetch_assoc($photos))
|
||||
{
|
||||
$res = $db->query('
|
||||
INSERT INTO assets
|
||||
(subdir, filename, title, slug, mimetype, image_width, image_height, date_captured, priority)
|
||||
VALUES
|
||||
({string:subdir}, {string:filename}, {string:title}, {string:slug}, {string:mimetype},
|
||||
{int:image_width}, {int:image_height},
|
||||
IF({int:date_captured} > 0, FROM_UNIXTIME({int:date_captured}), NULL),
|
||||
{int:priority})',
|
||||
[
|
||||
'subdir' => $dirnames_by_old_album_id[$photo['parent_id']],
|
||||
'filename' => $photo['name'],
|
||||
'title' => $photo['title'],
|
||||
'slug' => $photo['relative_url_cache'],
|
||||
'mimetype' => $photo['mime_type'],
|
||||
'image_width' => !empty($photo['width']) ? $photo['width'] : 'NULL',
|
||||
'image_height' => !empty($photo['height']) ? $photo['height'] : 'NULL',
|
||||
'date_captured' => !empty($photo['captured']) ? $photo['captured'] : $photo['created'],
|
||||
'priority' => !empty($photo['weight']) ? (int) $photo['weight'] : 0,
|
||||
]);
|
||||
|
||||
$id_asset = $db->insert_id();
|
||||
$old_photo_id_to_asset_id[$photo['id']] = $id_asset;
|
||||
|
||||
// Link to album.
|
||||
$db->query('
|
||||
INSERT INTO assets_tags
|
||||
(id_asset, id_tag)
|
||||
VALUES
|
||||
({int:id_asset}, {int:id_tag})',
|
||||
[
|
||||
'id_asset' => $id_asset,
|
||||
'id_tag' => $old_album_id_to_new_tag_id[$photo['parent_id']],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************
|
||||
* STEP 3: TAGS
|
||||
*******************************/
|
||||
|
||||
$num_tags = $pdb->queryValue('
|
||||
SELECT COUNT(*)
|
||||
FROM tags');
|
||||
|
||||
echo $num_tags, " tags to import.\n";
|
||||
|
||||
$rs_tags = $pdb->query('
|
||||
SELECT id, name, count
|
||||
FROM tags');
|
||||
|
||||
$old_tag_id_to_new_tag_id = [];
|
||||
while ($person = $pdb->fetch_assoc($rs_tags))
|
||||
{
|
||||
$tag = Tag::createNew([
|
||||
'tag' => $person['name'],
|
||||
'slug' => $person['name'],
|
||||
'kind' => 'Person',
|
||||
'description' => '',
|
||||
'count' => $person['count'],
|
||||
]);
|
||||
|
||||
$tags[$tag->id_tag] = $tag;
|
||||
$old_tag_id_to_new_tag_id[$person['id']] = $tag->id_tag;
|
||||
}
|
||||
|
||||
$pdb->free_result($rs_tags);
|
||||
|
||||
/*******************************
|
||||
* STEP 4: TAGGED PHOTOS
|
||||
*******************************/
|
||||
|
||||
$num_tagged = $pdb->queryValue('
|
||||
SELECT COUNT(*)
|
||||
FROM items_tags
|
||||
WHERE item_id IN(
|
||||
SELECT id
|
||||
FROM items
|
||||
WHERE type = {string:photo}
|
||||
)',
|
||||
['photo' => 'photo']);
|
||||
|
||||
echo $num_tagged, " photo tags to import.\n";
|
||||
|
||||
$rs_tags = $pdb->query('
|
||||
SELECT item_id, tag_id
|
||||
FROM items_tags
|
||||
WHERE item_id IN(
|
||||
SELECT id
|
||||
FROM items
|
||||
WHERE type = {string:photo}
|
||||
)',
|
||||
['photo' => 'photo']);
|
||||
|
||||
while ($tag = $pdb->fetch_assoc($rs_tags))
|
||||
{
|
||||
if (!isset($old_tag_id_to_new_tag_id[$tag['tag_id']], $old_photo_id_to_asset_id[$tag['item_id']]))
|
||||
continue;
|
||||
|
||||
$id_asset = $old_photo_id_to_asset_id[$tag['item_id']];
|
||||
$id_tag = $old_tag_id_to_new_tag_id[$tag['tag_id']];
|
||||
|
||||
// Link up.
|
||||
$db->query('
|
||||
INSERT IGNORE INTO assets_tags
|
||||
(id_asset, id_tag)
|
||||
VALUES
|
||||
({int:id_asset}, {int:id_tag})',
|
||||
[
|
||||
'id_asset' => $id_asset,
|
||||
'id_tag' => $id_tag,
|
||||
]);
|
||||
}
|
||||
|
||||
$pdb->free_result($rs_tags);
|
||||
|
||||
/*******************************
|
||||
* STEP 5: THUMBNAIL IDS
|
||||
*******************************/
|
||||
|
||||
foreach ($old_thumb_id_by_tag_id as $id_tag => $old_thumb_id)
|
||||
{
|
||||
if (!isset($old_photo_id_to_asset_id[$old_thumb_id]))
|
||||
continue;
|
||||
|
||||
$id_asset = $old_photo_id_to_asset_id[$old_thumb_id];
|
||||
$db->query('
|
||||
UPDATE tags
|
||||
SET id_asset_thumb = ' . $id_asset . '
|
||||
WHERE id_tag = ' . $id_tag);
|
||||
}
|
||||
|
||||
/*******************************
|
||||
* STEP 6: THUMBNAILS FOR PEOPLE
|
||||
*******************************/
|
||||
|
||||
$db->query('
|
||||
UPDATE tags AS t
|
||||
SET id_asset_thumb = (
|
||||
SELECT id_asset
|
||||
FROM assets_tags AS a
|
||||
WHERE a.id_tag = t.id_tag
|
||||
ORDER BY RAND()
|
||||
LIMIT 1
|
||||
)
|
||||
WHERE kind = {string:person}',
|
||||
['person' => 'Person']);
|
478
models/Asset.php
Normal file
478
models/Asset.php
Normal file
@ -0,0 +1,478 @@
|
||||
<?php
|
||||
/*****************************************************************************
|
||||
* Asset.php
|
||||
* Contains key class Asset.
|
||||
*
|
||||
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
||||
*****************************************************************************/
|
||||
|
||||
class Asset
|
||||
{
|
||||
protected $id_asset;
|
||||
protected $subdir;
|
||||
protected $filename;
|
||||
protected $title;
|
||||
protected $mimetype;
|
||||
protected $image_width;
|
||||
protected $image_height;
|
||||
protected $date_captured;
|
||||
protected $priority;
|
||||
protected $meta;
|
||||
protected $tags;
|
||||
|
||||
protected function __construct(array $data)
|
||||
{
|
||||
foreach ($data as $attribute => $value)
|
||||
$this->$attribute = $value;
|
||||
|
||||
if (!empty($data['date_captured']) && $data['date_captured'] !== 'NULL')
|
||||
$this->date_captured = new DateTime($data['date_captured']);
|
||||
}
|
||||
|
||||
public static function fromId($id_asset, $return_format = 'object')
|
||||
{
|
||||
$db = Registry::get('db');
|
||||
|
||||
$row = $db->queryAssoc('
|
||||
SELECT *
|
||||
FROM assets
|
||||
WHERE id_asset = {int:id_asset}',
|
||||
[
|
||||
'id_asset' => $id_asset,
|
||||
]);
|
||||
|
||||
// Asset not found?
|
||||
if (empty($row))
|
||||
return false;
|
||||
|
||||
$row['meta'] = $db->queryPair('
|
||||
SELECT variable, value
|
||||
FROM assets_meta
|
||||
WHERE id_asset = {int:id_asset}',
|
||||
[
|
||||
'id_asset' => $id_asset,
|
||||
]);
|
||||
|
||||
return $return_format == 'object' ? new Asset($row) : $row;
|
||||
}
|
||||
|
||||
public static function fromIds(array $id_assets, $return_format = 'array')
|
||||
{
|
||||
if (empty($id_assets))
|
||||
return [];
|
||||
|
||||
$db = Registry::get('db');
|
||||
|
||||
$res = $db->query('
|
||||
SELECT *
|
||||
FROM assets
|
||||
WHERE id_asset IN ({array_int:id_assets})
|
||||
ORDER BY id_asset',
|
||||
[
|
||||
'id_assets' => $id_assets,
|
||||
]);
|
||||
|
||||
$assets = [];
|
||||
while ($asset = $db->fetch_assoc($res))
|
||||
{
|
||||
$assets[$asset['id_asset']] = $asset;
|
||||
$assets[$asset['id_asset']]['meta'] = [];
|
||||
}
|
||||
|
||||
$metas = $db->queryRows('
|
||||
SELECT id_asset, variable, value
|
||||
FROM assets_meta
|
||||
WHERE id_asset IN ({array_int:id_assets})
|
||||
ORDER BY id_asset',
|
||||
[
|
||||
'id_assets' => $id_assets,
|
||||
]);
|
||||
|
||||
foreach ($metas as $meta)
|
||||
$assets[$meta[0]]['meta'][$meta[1]] = $meta[2];
|
||||
|
||||
if ($return_format == 'array')
|
||||
return $assets;
|
||||
else
|
||||
{
|
||||
$objects = [];
|
||||
foreach ($assets as $id => $asset)
|
||||
$objects[$id] = new Asset($asset);
|
||||
return $objects;
|
||||
}
|
||||
}
|
||||
|
||||
public static function byPostId($id_post, $return_format = 'object')
|
||||
{
|
||||
$db = Registry::get('db');
|
||||
|
||||
// !!! TODO
|
||||
}
|
||||
|
||||
public static function createNew(array $data, $return_format = 'object')
|
||||
{
|
||||
// Extract the data array.
|
||||
extract($data);
|
||||
|
||||
// No filename? Abort!
|
||||
if (!isset($filename_to_copy) || !is_file($filename_to_copy))
|
||||
return false;
|
||||
|
||||
// No subdir? Use YYYY/MM
|
||||
if (!isset($preferred_subdir))
|
||||
$preferred_subdir = date('Y') . '/' . date('m');
|
||||
|
||||
// Does this dir exist yet? If not, create it.
|
||||
if (!is_dir(ASSETSDIR . '/' . $preferred_subdir))
|
||||
mkdir(ASSETSDIR . '/' . $preferred_subdir, 0755, true);
|
||||
|
||||
// Construct the destination filename. Make sure we don't accidentally overwrite anything.
|
||||
if (!isset($preferred_filename))
|
||||
$preferred_filename = basename($filename_to_copy);
|
||||
|
||||
$new_filename = $preferred_filename;
|
||||
$destination = ASSETSDIR . '/' . $preferred_subdir . '/' . $preferred_filename;
|
||||
while (file_exists($destination))
|
||||
{
|
||||
$filename = pathinfo($preferred_filename, PATHINFO_FILENAME) . '_' . mt_rand(10, 99);
|
||||
$extension = pathinfo($preferred_filename, PATHINFO_EXTENSION);
|
||||
$new_filename = $filename . '.' . $extension;
|
||||
$destination = dirname($destination) . '/' . $new_filename;
|
||||
}
|
||||
|
||||
// Can we write to the target directory? Then copy the file.
|
||||
if (is_writable(ASSETSDIR . '/' . $preferred_subdir))
|
||||
copy($filename_to_copy, $destination);
|
||||
else
|
||||
return false;
|
||||
|
||||
// Figure out the mime type for the file.
|
||||
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
||||
$mimetype = finfo_file($finfo, $destination);
|
||||
finfo_close($finfo);
|
||||
|
||||
// Do we have a title yet? Otherwise, use the filename.
|
||||
$title = isset($data['title']) ? $data['title'] : pathinfo($preferred_filename, PATHINFO_FILENAME);
|
||||
|
||||
// Same with the slug.
|
||||
$slug = isset($data['slug']) ? $data['slug'] : $preferred_subdir . '/' . pathinfo($preferred_filename, PATHINFO_FILENAME);
|
||||
|
||||
// Detected an image?
|
||||
if (substr($mimetype, 0, 5) == 'image')
|
||||
{
|
||||
$image = new Imagick($destination);
|
||||
$d = $image->getImageGeometry();
|
||||
|
||||
// Get image dimensions, bearing orientation in mind.
|
||||
switch ($image->getImageOrientation())
|
||||
{
|
||||
case Imagick::ORIENTATION_LEFTBOTTOM:
|
||||
case Imagick::ORIENTATION_RIGHTTOP:
|
||||
$image_width = $d['height'];
|
||||
$image_height = $d['width'];
|
||||
break;
|
||||
|
||||
default:
|
||||
$image_width = $d['width'];
|
||||
$image_height = $d['height'];
|
||||
}
|
||||
|
||||
unset($image);
|
||||
|
||||
$exif = EXIF::fromFile($destination);
|
||||
$date_captured = intval($exif->created_timestamp);
|
||||
}
|
||||
|
||||
$db = Registry::get('db');
|
||||
$res = $db->query('
|
||||
INSERT INTO assets
|
||||
(subdir, filename, title, slug, mimetype, image_width, image_height, date_captured, priority)
|
||||
VALUES
|
||||
({string:subdir}, {string:filename}, {string:title}, {string:slug}, {string:mimetype},
|
||||
{int:image_width}, {int:image_height},
|
||||
IF({int:date_captured} > 0, FROM_UNIXTIME({int:date_captured}), NULL),
|
||||
{int:priority})',
|
||||
[
|
||||
'subdir' => $preferred_subdir,
|
||||
'filename' => $new_filename,
|
||||
'title' => $title,
|
||||
'slug' => $slug,
|
||||
'mimetype' => $mimetype,
|
||||
'image_width' => isset($image_width) ? $image_width : 'NULL',
|
||||
'image_height' => isset($image_height) ? $image_height : 'NULL',
|
||||
'date_captured' => isset($date_captured) ? $date_captured : 'NULL',
|
||||
'priority' => isset($priority) ? (int) $priority : 0,
|
||||
]);
|
||||
|
||||
if (!$res)
|
||||
{
|
||||
unlink($destination);
|
||||
return false;
|
||||
}
|
||||
|
||||
$data['id_asset'] = $db->insert_id();
|
||||
return $return_format == 'object' ? new self($data) : $data;
|
||||
}
|
||||
|
||||
public function getId()
|
||||
{
|
||||
return $this->id_asset;
|
||||
}
|
||||
|
||||
public function getDateCaptured()
|
||||
{
|
||||
return $this->date_captured;
|
||||
}
|
||||
|
||||
public function getFilename()
|
||||
{
|
||||
return $this->filename;
|
||||
}
|
||||
|
||||
public function getLinkedPosts()
|
||||
{
|
||||
$posts = Registry::get('db')->queryValues('
|
||||
SELECT id_post
|
||||
FROM posts_assets
|
||||
WHERE id_asset = {int:id_asset}',
|
||||
['id_asset' => $this->id_asset]);
|
||||
|
||||
// TODO: fix empty post iterator.
|
||||
if (empty($posts))
|
||||
return [];
|
||||
|
||||
return PostIterator::getByOptions([
|
||||
'ids' => $posts,
|
||||
'type' => '',
|
||||
]);
|
||||
}
|
||||
|
||||
public function getMeta()
|
||||
{
|
||||
return $this->meta;
|
||||
}
|
||||
|
||||
public function getPath()
|
||||
{
|
||||
return $this->subdir;
|
||||
}
|
||||
|
||||
public function getPriority()
|
||||
{
|
||||
return $this->priority;
|
||||
}
|
||||
|
||||
public function getTags()
|
||||
{
|
||||
if (!isset($this->tags))
|
||||
$this->tags = Tag::byAssetId($this->id_asset);
|
||||
|
||||
return $this->tags;
|
||||
}
|
||||
|
||||
public function getTitle()
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
public function getUrl()
|
||||
{
|
||||
return BASEURL . '/assets/' . $this->subdir . '/' . $this->filename;
|
||||
}
|
||||
|
||||
public function getType()
|
||||
{
|
||||
return substr($this->mimetype, 0, strpos($this->mimetype, '/'));
|
||||
}
|
||||
|
||||
public function getDimensions($as_type = 'string')
|
||||
{
|
||||
return $as_type === 'string' ? $this->image_width . 'x' . $this->image_height : [$this->image_width, $this->image_height];
|
||||
}
|
||||
|
||||
public function isImage()
|
||||
{
|
||||
return substr($this->mimetype, 0, 5) === 'image';
|
||||
}
|
||||
|
||||
public function getImage()
|
||||
{
|
||||
if (!$this->isImage())
|
||||
throw new Exception('Trying to upgrade an Asset to an Image while the Asset is not an image!');
|
||||
|
||||
return new Image(get_object_vars($this));
|
||||
}
|
||||
|
||||
public function replaceFile($filename)
|
||||
{
|
||||
// No filename? Abort!
|
||||
if (!isset($filename) || !is_readable($filename))
|
||||
return false;
|
||||
|
||||
// Can we write to the target file?
|
||||
$destination = ASSETSDIR . '/' . $this->subdir . '/' . $this->filename;
|
||||
if (!is_writable($destination))
|
||||
return false;
|
||||
|
||||
copy($filename, $destination);
|
||||
|
||||
// Figure out the mime type for the file.
|
||||
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
||||
$this->mimetype = finfo_file($finfo, $destination);
|
||||
finfo_close($finfo);
|
||||
|
||||
// Detected an image?
|
||||
if (substr($this->mimetype, 0, 5) == 'image')
|
||||
{
|
||||
$image = new Imagick($destination);
|
||||
$d = $image->getImageGeometry();
|
||||
$this->image_width = $d['width'];
|
||||
$this->image_height = $d['height'];
|
||||
unset($image);
|
||||
|
||||
$exif = EXIF::fromFile($destination);
|
||||
if (!empty($exif->created_timestamp))
|
||||
$this->date_captured = new DateTime(date('r', $exif->created_timestamp));
|
||||
else
|
||||
$this->date_captured = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->image_width = null;
|
||||
$this->image_height = null;
|
||||
$this->date_captured = null;
|
||||
}
|
||||
|
||||
return Registry::get('db')->query('
|
||||
UPDATE assets
|
||||
SET
|
||||
mimetype = {string:mimetype},
|
||||
image_width = {int:image_width},
|
||||
image_height = {int:image_height},
|
||||
date_captured = {datetime:date_captured},
|
||||
priority = {int:priority}
|
||||
WHERE id_asset = {int:id_asset}',
|
||||
[
|
||||
'id_asset' => $this->id_asset,
|
||||
'mimetype' => $this->mimetype,
|
||||
'image_width' => isset($this->image_width) ? $this->image_width : 'NULL',
|
||||
'image_height' => isset($this->image_height) ? $this->image_height : 'NULL',
|
||||
'date_captured' => isset($this->date_captured) ? $this->date_captured : 'NULL',
|
||||
'priority' => $this->priority,
|
||||
]);
|
||||
}
|
||||
|
||||
protected function saveMetaData()
|
||||
{
|
||||
$this->setMetaData($this->meta);
|
||||
}
|
||||
|
||||
public function setMetaData(array $new_meta, $mode = 'replace')
|
||||
{
|
||||
$db = Registry::get('db');
|
||||
|
||||
// If we're replacing, delete current data first.
|
||||
if ($mode === 'replace')
|
||||
{
|
||||
$to_remove = array_diff_key($this->meta, $new_meta);
|
||||
if (!empty($to_remove))
|
||||
$db->query('
|
||||
DELETE FROM assets_meta
|
||||
WHERE id_asset = {int:id_asset} AND
|
||||
variable IN({array_string:variables})',
|
||||
[
|
||||
'id_asset' => $this->id_asset,
|
||||
'variables' => array_keys($to_remove),
|
||||
]);
|
||||
}
|
||||
|
||||
// Build rows
|
||||
$to_insert = [];
|
||||
foreach ($new_meta as $key => $value)
|
||||
$to_insert[] = [
|
||||
'id_asset' => $this->id_asset,
|
||||
'variable' => $key,
|
||||
'value' => $value,
|
||||
];
|
||||
|
||||
// Do the insertion
|
||||
$res = Registry::get('db')->insert('replace', 'assets_meta', [
|
||||
'id_asset' => 'int',
|
||||
'variable' => 'string',
|
||||
'value' => 'string',
|
||||
], $to_insert, ['id_asset', 'variable']);
|
||||
|
||||
if ($res)
|
||||
$this->meta = $new_meta;
|
||||
}
|
||||
|
||||
public function delete()
|
||||
{
|
||||
return Registry::get('db')->query('
|
||||
DELETE FROM assets
|
||||
WHERE id_asset = {int:id_asset}',
|
||||
[
|
||||
'id_asset' => $this->id_asset,
|
||||
]);
|
||||
}
|
||||
|
||||
public function linkTags(array $id_tags)
|
||||
{
|
||||
if (empty($id_tags))
|
||||
return true;
|
||||
|
||||
$pairs = [];
|
||||
foreach ($id_tags as $id_tag)
|
||||
$pairs[] = ['id_asset' => $this->id_asset, 'id_tag' => $id_tag];
|
||||
|
||||
Registry::get('db')->insert('ignore', 'assets_tags', [
|
||||
'id_asset' => 'int',
|
||||
'id_tag' => 'int',
|
||||
], $pairs, ['id_asset', 'id_tag']);
|
||||
|
||||
Tag::recount($id_tags);
|
||||
}
|
||||
|
||||
public function unlinkTags(array $id_tags)
|
||||
{
|
||||
if (empty($id_tags))
|
||||
return true;
|
||||
|
||||
Registry::get('db')->query('
|
||||
DELETE FROM assets_tags
|
||||
WHERE id_asset = {int:id_asset} AND id_tag IN ({array_int:id_tags})',
|
||||
[
|
||||
'id_asset' => $this->id_asset,
|
||||
'id_tags' => $id_tags,
|
||||
]);
|
||||
|
||||
Tag::recount($id_tags);
|
||||
}
|
||||
|
||||
public static function getCount()
|
||||
{
|
||||
return $db->queryValue('
|
||||
SELECT COUNT(*)
|
||||
FROM assets');
|
||||
}
|
||||
|
||||
public function setKeyData($title, DateTime $date_captured = null, $priority)
|
||||
{
|
||||
$params = [
|
||||
'id_asset' => $this->id_asset,
|
||||
'title' => $title,
|
||||
'priority' => $priority,
|
||||
];
|
||||
|
||||
if (isset($date_captured))
|
||||
$params['date_captured'] = $date_captured->format('Y-m-d H:i:s');
|
||||
|
||||
return Registry::get('db')->query('
|
||||
UPDATE assets
|
||||
SET title = {string:title},' . (isset($date_captured) ? '
|
||||
date_captured = {datetime:date_captured},' : '') . '
|
||||
priority = {int:priority}
|
||||
WHERE id_asset = {int:id_asset}',
|
||||
$params);
|
||||
}
|
||||
}
|
154
models/AssetIterator.php
Normal file
154
models/AssetIterator.php
Normal file
@ -0,0 +1,154 @@
|
||||
<?php
|
||||
/*****************************************************************************
|
||||
* AssetIterator.php
|
||||
* Contains key class AssetIterator.
|
||||
*
|
||||
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
||||
*****************************************************************************/
|
||||
|
||||
class AssetIterator extends Asset
|
||||
{
|
||||
private $return_format;
|
||||
private $res_assets;
|
||||
private $res_meta;
|
||||
|
||||
protected function __construct($res_assets, $res_meta, $return_format)
|
||||
{
|
||||
$this->db = Registry::get('db');
|
||||
$this->res_assets = $res_assets;
|
||||
$this->res_meta = $res_meta;
|
||||
$this->return_format = $return_format;
|
||||
}
|
||||
|
||||
public function next()
|
||||
{
|
||||
$row = $this->db->fetch_assoc($this->res_assets);
|
||||
|
||||
// No more rows?
|
||||
if (!$row)
|
||||
return false;
|
||||
|
||||
// Looks up metadata.
|
||||
$row['meta'] = [];
|
||||
while ($meta = $this->db->fetch_assoc($this->res_meta))
|
||||
{
|
||||
if ($meta['id_asset'] != $row['id_asset'])
|
||||
continue;
|
||||
|
||||
$row['meta'][$meta['variable']] = $meta['value'];
|
||||
}
|
||||
|
||||
// Reset internal pointer for next asset.
|
||||
$this->db->data_seek($this->res_meta, 0);
|
||||
|
||||
if ($this->return_format == 'object')
|
||||
return new Asset($row);
|
||||
else
|
||||
return $row;
|
||||
}
|
||||
|
||||
public function reset()
|
||||
{
|
||||
$this->db->data_seek($this->res_assets, 0);
|
||||
$this->db->data_seek($this->res_meta, 0);
|
||||
}
|
||||
|
||||
public function clean()
|
||||
{
|
||||
if (!$this->res_assets)
|
||||
return;
|
||||
|
||||
$this->db->free_result($this->res_assets);
|
||||
$this->res_assets = null;
|
||||
}
|
||||
|
||||
public function num()
|
||||
{
|
||||
return $this->db->num_rows($this->res_assets);
|
||||
}
|
||||
|
||||
public static function all()
|
||||
{
|
||||
return self::getByOptions();
|
||||
}
|
||||
|
||||
public static function getByOptions(array $options = [], $return_count = false, $return_format = 'object')
|
||||
{
|
||||
$params = [
|
||||
'limit' => isset($options['limit']) ? $options['limit'] : 0,
|
||||
'order' => isset($options['order']) && in_array($options['order'], ['id_asset', 'subdir', 'filename', 'title',
|
||||
'mime_type', 'image_width', 'image_height', 'date_captured']) ? $options['order'] : 'id_asset',
|
||||
'direction' => isset($options['direction']) ? $options['direction'] : 'asc',
|
||||
];
|
||||
|
||||
if (isset($options['offset']))
|
||||
$params['offset'] = $options['offset'];
|
||||
elseif (isset($options['page']) && $options['page'] >= 1)
|
||||
$params['offset'] = $params['limit'] * ($options['page'] - 1);
|
||||
else
|
||||
$params['offset'] = 0;
|
||||
|
||||
$where = [];
|
||||
|
||||
if (isset($options['mime_type']))
|
||||
{
|
||||
$params['mime_type'] = $options['mime_type'];
|
||||
if (is_array($options['mime_type']))
|
||||
$where[] = 'a.mimetype IN({array_string:mime_type})';
|
||||
else
|
||||
$where[] = 'a.mimetype = {string:mime_type}';
|
||||
}
|
||||
if (isset($options['id_tag']))
|
||||
{
|
||||
$params['id_tag'] = $options['id_tag'];
|
||||
$where[] = 'id_asset IN(
|
||||
SELECT l.id_asset
|
||||
FROM assets_tags AS l
|
||||
WHERE l.id_tag = {int:id_tag})';
|
||||
}
|
||||
|
||||
// Make it valid SQL.
|
||||
$order = 'a.' . $params['order'] . ' ' . ($params['direction'] == 'desc' ? 'DESC' : 'ASC');
|
||||
$where = empty($where) ? '1' : implode(' AND ', $where);
|
||||
|
||||
// And ... go!
|
||||
$db = Registry::get('db');
|
||||
|
||||
// Get a resource object for the assets.
|
||||
$res_assets = $db->query('
|
||||
SELECT a.*
|
||||
FROM assets AS a
|
||||
WHERE ' . $where . '
|
||||
ORDER BY ' . $order . (!empty($params['limit']) ? '
|
||||
LIMIT {int:offset}, {int:limit}' : ''),
|
||||
$params);
|
||||
|
||||
// Get a resource object for the asset meta.
|
||||
$res_meta = $db->query('
|
||||
SELECT id_asset, variable, value
|
||||
FROM assets_meta
|
||||
WHERE id_asset IN(
|
||||
SELECT id_asset
|
||||
FROM assets AS a
|
||||
WHERE ' . $where . '
|
||||
)
|
||||
ORDER BY id_asset',
|
||||
$params);
|
||||
|
||||
$iterator = new self($res_assets, $res_meta, $return_format);
|
||||
|
||||
// Returning total count, too?
|
||||
if ($return_count)
|
||||
{
|
||||
$count = $db->queryValue('
|
||||
SELECT COUNT(*)
|
||||
FROM assets AS a
|
||||
WHERE ' . $where,
|
||||
$params);
|
||||
|
||||
return [$iterator, $count];
|
||||
}
|
||||
else
|
||||
return $iterator;
|
||||
}
|
||||
}
|
114
models/Authentication.php
Normal file
114
models/Authentication.php
Normal file
@ -0,0 +1,114 @@
|
||||
<?php
|
||||
/*****************************************************************************
|
||||
* Authentication.php
|
||||
* Contains key class Authentication.
|
||||
*
|
||||
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
||||
*****************************************************************************/
|
||||
|
||||
/**
|
||||
* Authentication class, containing various static functions used for account verification
|
||||
* and session management.
|
||||
*/
|
||||
class Authentication
|
||||
{
|
||||
/**
|
||||
* Checks whether a user still exists in the database.
|
||||
*/
|
||||
public static function checkExists($id_user)
|
||||
{
|
||||
$res = Registry::get('db')->queryValue('
|
||||
SELECT id_user
|
||||
FROM users
|
||||
WHERE id_user = {int:id}',
|
||||
[
|
||||
'id' => $id_user,
|
||||
]);
|
||||
|
||||
return $res !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the user id belonging to a certain emailaddress.
|
||||
*/
|
||||
public static function getUserId($emailaddress)
|
||||
{
|
||||
$res = Registry::get('db')->queryValue('
|
||||
SELECT id_user
|
||||
FROM users
|
||||
WHERE emailaddress = {string:emailaddress}',
|
||||
[
|
||||
'emailaddress' => $emailaddress,
|
||||
]);
|
||||
|
||||
return empty($res) ? false : $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies whether the user is currently logged in.
|
||||
*/
|
||||
public static function isLoggedIn()
|
||||
{
|
||||
// Check whether the active session matches the current user's environment.
|
||||
if (isset($_SESSION['ip_address'], $_SESSION['user_agent']) && (
|
||||
(isset($_SERVER['REMOTE_ADDR']) && $_SESSION['ip_address'] != $_SERVER['REMOTE_ADDR']) ||
|
||||
(isset($_SERVER['HTTP_USER_AGENT']) && $_SESSION['user_agent'] != $_SERVER['HTTP_USER_AGENT'])))
|
||||
{
|
||||
session_destroy();
|
||||
return false;
|
||||
}
|
||||
|
||||
// A user is logged in if a user id exists in the session and this id is (still) in the database.
|
||||
return isset($_SESSION['user_id']) && self::checkExists($_SESSION['user_id']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks a password for a given username against the database.
|
||||
*/
|
||||
public static function checkPassword($emailaddress, $password)
|
||||
{
|
||||
// Retrieve password hash for user matching the provided emailaddress.
|
||||
$password_hash = Registry::get('db')->queryValue('
|
||||
SELECT password_hash
|
||||
FROM users
|
||||
WHERE emailaddress = {string:emailaddress}',
|
||||
[
|
||||
'emailaddress' => $emailaddress,
|
||||
]);
|
||||
|
||||
// If there's no hash, the user likely does not exist.
|
||||
if (!$password_hash)
|
||||
return false;
|
||||
|
||||
return password_verify($password, $password_hash);
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes a password hash.
|
||||
*/
|
||||
public static function computeHash($password)
|
||||
{
|
||||
$hash = password_hash($password, PASSWORD_DEFAULT);
|
||||
if (!$hash)
|
||||
throw new Exception('Hash creation failed!');
|
||||
return $hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets a password for a certain user.
|
||||
*/
|
||||
public static function updatePassword($id_user, $hash)
|
||||
{
|
||||
return Registry::get('db')->query('
|
||||
UPDATE users
|
||||
SET
|
||||
password_hash = {string:hash},
|
||||
reset_key = {string:blank}
|
||||
WHERE id_user = {int:id_user}',
|
||||
[
|
||||
'id_user' => $id_user,
|
||||
'hash' => $hash,
|
||||
'blank' => '',
|
||||
]);
|
||||
}
|
||||
}
|
149
models/BestColor.php
Normal file
149
models/BestColor.php
Normal file
@ -0,0 +1,149 @@
|
||||
<?php
|
||||
/*****************************************************************************
|
||||
* BestColor.php
|
||||
* Contains key class BestColor.
|
||||
*
|
||||
* !!! Licensing?
|
||||
*****************************************************************************/
|
||||
|
||||
class BestColor
|
||||
{
|
||||
private $best;
|
||||
|
||||
public function __construct(Image $asset)
|
||||
{
|
||||
// Set fallback color.
|
||||
$this->best = ['r' => 204, 'g' => 204, 'b' => 204]; // #cccccc
|
||||
|
||||
// We will be needing to read this...
|
||||
if (!file_exists($asset->getPath()))
|
||||
return;
|
||||
|
||||
// Try the arcane stuff again.
|
||||
try
|
||||
{
|
||||
$image = new Imagick($asset->getPath());
|
||||
$width = $image->getImageWidth();
|
||||
$height = $image->getImageHeight();
|
||||
|
||||
// Sample six points in the image: four based on the rule of thirds, as well as the horizontal and vertical centre.
|
||||
$topy = round($height / 3);
|
||||
$bottomy = round(($height / 3) * 2);
|
||||
$leftx = round($width / 3);
|
||||
$rightx = round(($width / 3) * 2);
|
||||
$centery = round($height / 2);
|
||||
$centerx = round($width / 2);
|
||||
|
||||
// Grab their colours.
|
||||
$rgb = [
|
||||
$image->getImagePixelColor($leftx, $topy)->getColor(),
|
||||
$image->getImagePixelColor($rightx, $topy)->getColor(),
|
||||
$image->getImagePixelColor($leftx, $bottomy)->getColor(),
|
||||
$image->getImagePixelColor($rightx, $bottomy)->getColor(),
|
||||
$image->getImagePixelColor($centerx, $centery)->getColor(),
|
||||
];
|
||||
|
||||
// We won't be needing this anymore, so save us some memory.
|
||||
$image->clear();
|
||||
$image->destroy();
|
||||
}
|
||||
// In case something does go wrong...
|
||||
catch (ImagickException $e)
|
||||
{
|
||||
// Fall back to default color.
|
||||
return;
|
||||
}
|
||||
|
||||
// Process rgb values into hsv values
|
||||
foreach ($rgb as $i => $color)
|
||||
{
|
||||
$colors[$i] = $color;
|
||||
list($colors[$i]['h'], $colors[$i]['s'], $colors[$i]['v']) = self::rgb2hsv($color['r'], $color['g'], $color['b']);
|
||||
}
|
||||
|
||||
// Figure out which color is the best saturated.
|
||||
$best_saturation = $best_brightness = 0;
|
||||
$the_best_s = $the_best_v = ['v' => 0];
|
||||
foreach ($colors as $color)
|
||||
{
|
||||
if ($color['s'] > $best_saturation)
|
||||
{
|
||||
$best_saturation = $color['s'];
|
||||
$the_best_s = $color;
|
||||
}
|
||||
if ($color['v'] > $best_brightness)
|
||||
{
|
||||
$best_brightness = $color['v'];
|
||||
$the_best_v = $color;
|
||||
}
|
||||
}
|
||||
|
||||
// Is brightest the same as most saturated?
|
||||
$this->best = ($the_best_s['v'] >= ($the_best_v['v'] - ($the_best_v['v'] / 2))) ? $the_best_s : $the_best_v;
|
||||
}
|
||||
|
||||
public static function hex2rgb($hex)
|
||||
{
|
||||
return sscanf($hex, '%2X%2X%2X');
|
||||
}
|
||||
|
||||
public static function rgb2hex($red, $green, $blue)
|
||||
{
|
||||
return sprintf('%02X%02X%02X', $red, $green, $blue);
|
||||
}
|
||||
|
||||
public static function rgb2hsv($r, $g, $b)
|
||||
{
|
||||
$max = max($r, $g, $b);
|
||||
$min = min($r, $g, $b);
|
||||
$delta = $max - $min;
|
||||
$v = round(($max / 255) * 100);
|
||||
$s = ($max != 0) ? (round($delta / $max * 100)) : 0;
|
||||
if ($s == 0)
|
||||
{
|
||||
$h = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
if ($r == $max)
|
||||
$h = ($g - $b) / $delta;
|
||||
elseif ($g == $max)
|
||||
$h = 2 + ($b - $r) / $delta;
|
||||
elseif ($b == $max)
|
||||
$h = 4 + ($r - $g) / $delta;
|
||||
|
||||
$h = round($h * 60);
|
||||
|
||||
if ($h > 360)
|
||||
$h = 360;
|
||||
|
||||
if ($h < 0)
|
||||
$h += 360;
|
||||
}
|
||||
|
||||
return [$h, $s, $v];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a normal (light) background color as hexadecimal value (without hash prefix).
|
||||
* @return color string
|
||||
*/
|
||||
public function hex()
|
||||
{
|
||||
$c = $this->best;
|
||||
return self::rgb2hex($c['r'], $c['g'], $c['b']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a 50% darker version of the best color as string.
|
||||
* @param factor, defaults to 0.5
|
||||
* @param alpha, defaults to 0.7
|
||||
* @return rgba(r * factor, g * factor, b * factor, alpha)
|
||||
*/
|
||||
public function rgba($factor = 0.5, $alpha = 0.7)
|
||||
{
|
||||
$c = $this->best;
|
||||
return 'rgba(' . round($c['r'] * $factor) . ', ' . round($c['g'] * $factor) . ', ' . round($c['b'] * $factor) . ', ' . $alpha . ')';
|
||||
}
|
||||
|
||||
}
|
61
models/Cache.php
Normal file
61
models/Cache.php
Normal file
@ -0,0 +1,61 @@
|
||||
<?php
|
||||
/*****************************************************************************
|
||||
* Cache.php
|
||||
* Contains key class Cache.
|
||||
*
|
||||
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
||||
*****************************************************************************/
|
||||
|
||||
class Cache
|
||||
{
|
||||
public static $hits = 0;
|
||||
public static $misses = 0;
|
||||
public static $puts = 0;
|
||||
public static $removals = 0;
|
||||
|
||||
public static function put($key, $value, $ttl = 3600)
|
||||
{
|
||||
// If the cache is unavailable, don't bother.
|
||||
if (!CACHE_ENABLED || !function_exists('apcu_store'))
|
||||
return false;
|
||||
|
||||
// Keep track of the amount of cache puts.
|
||||
self::$puts++;
|
||||
|
||||
// Store the data in serialized form.
|
||||
return apcu_store(CACHE_KEY_PREFIX . $key, serialize($value), $ttl);
|
||||
}
|
||||
|
||||
// Get some data from the cache.
|
||||
public static function get($key)
|
||||
{
|
||||
// If the cache is unavailable, don't bother.
|
||||
if (!CACHE_ENABLED || !function_exists('apcu_fetch'))
|
||||
return false;
|
||||
|
||||
// Try to fetch it!
|
||||
$value = apcu_fetch(CACHE_KEY_PREFIX . $key);
|
||||
|
||||
// Were we successful?
|
||||
if (!empty($value))
|
||||
{
|
||||
self::$hits++;
|
||||
return unserialize($value);
|
||||
}
|
||||
// Otherwise, it's a miss.
|
||||
else
|
||||
{
|
||||
self::$misses++;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static function remove($key)
|
||||
{
|
||||
if (!CACHE_ENABLED || !function_exists('apcu_delete'))
|
||||
return false;
|
||||
|
||||
self::$removals++;
|
||||
return apcu_delete(CACHE_KEY_PREFIX . $key);
|
||||
}
|
||||
}
|
535
models/Database.php
Normal file
535
models/Database.php
Normal file
@ -0,0 +1,535 @@
|
||||
<?php
|
||||
/*****************************************************************************
|
||||
* Database.php
|
||||
* Contains key class Database.
|
||||
*
|
||||
* Adapted from SMF 2.0's DBA (C) 2011 Simple Machines
|
||||
* Used under BSD 3-clause license.
|
||||
*
|
||||
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
||||
*****************************************************************************/
|
||||
|
||||
/**
|
||||
* The database model used to communicate with the MySQL server.
|
||||
*/
|
||||
class Database
|
||||
{
|
||||
private $connection;
|
||||
private $query_count = 0;
|
||||
private $logged_queries = [];
|
||||
|
||||
/**
|
||||
* Initialises a new database connection.
|
||||
* @param server: server to connect to.
|
||||
* @param user: username to use for authentication.
|
||||
* @param password: password to use for authentication.
|
||||
* @param name: database to select.
|
||||
*/
|
||||
public function __construct($server, $user, $password, $name)
|
||||
{
|
||||
$this->connection = @mysqli_connect($server, $user, $password, $name);
|
||||
|
||||
// Give up if we have a connection error.
|
||||
if (mysqli_connect_error())
|
||||
{
|
||||
header('HTTP/1.1 503 Service Temporarily Unavailable');
|
||||
echo '<h2>Database Connection Problems</h2><p>Our software could not connect to the database. We apologise for any inconvenience and ask you to check back later.</p>';
|
||||
exit;
|
||||
}
|
||||
|
||||
$this->query('SET NAMES {string:utf8}', array('utf8' => 'utf8'));
|
||||
}
|
||||
|
||||
public function getQueryCount()
|
||||
{
|
||||
return $this->query_count;
|
||||
}
|
||||
|
||||
public function getLoggedQueries()
|
||||
{
|
||||
return $this->logged_queries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a row from a given recordset, using field names as keys.
|
||||
*/
|
||||
public function fetch_assoc($resource)
|
||||
{
|
||||
return mysqli_fetch_assoc($resource);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a row from a given recordset, using numeric keys.
|
||||
*/
|
||||
public function fetch_row($resource)
|
||||
{
|
||||
return mysqli_fetch_row($resource);
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroys a given recordset.
|
||||
*/
|
||||
public function free_result($resource)
|
||||
{
|
||||
return mysqli_free_result($resource);
|
||||
}
|
||||
|
||||
public function data_seek($result, $row_num)
|
||||
{
|
||||
return mysqli_data_seek($result, $row_num);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the amount of rows in a given recordset.
|
||||
*/
|
||||
public function num_rows($resource)
|
||||
{
|
||||
return mysqli_num_rows($resource);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the amount of fields in a given recordset.
|
||||
*/
|
||||
public function num_fields($resource)
|
||||
{
|
||||
return mysqli_num_fields($resource);
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes a string.
|
||||
*/
|
||||
public function escape_string($string)
|
||||
{
|
||||
return mysqli_real_escape_string($this->connection, $string);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unescapes a string.
|
||||
*/
|
||||
public function unescape_string($string)
|
||||
{
|
||||
return stripslashes($string);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last MySQL error.
|
||||
*/
|
||||
public function error()
|
||||
{
|
||||
return mysqli_error($this->connection);
|
||||
}
|
||||
|
||||
public function server_info()
|
||||
{
|
||||
return mysqli_get_server_info($this->connection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects a database on a given connection.
|
||||
*/
|
||||
public function select_db($database)
|
||||
{
|
||||
return mysqli_select_db($database, $this->connection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the amount of rows affected by the previous query.
|
||||
*/
|
||||
public function affected_rows()
|
||||
{
|
||||
return mysqli_affected_rows($this->connection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the id of the row created by a previous query.
|
||||
*/
|
||||
public function insert_id()
|
||||
{
|
||||
return mysqli_insert_id($this->connection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Do a MySQL transaction.
|
||||
*/
|
||||
public function transaction($operation = 'commit')
|
||||
{
|
||||
switch ($operation)
|
||||
{
|
||||
case 'begin':
|
||||
case 'rollback':
|
||||
case 'commit':
|
||||
return @mysqli_query($this->connection, strtoupper($operation));
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Function used as a callback for the preg_match function that parses variables into database queries.
|
||||
*/
|
||||
private function replacement_callback($matches)
|
||||
{
|
||||
list ($values, $connection) = $this->db_callback;
|
||||
|
||||
if (!isset($matches[2]))
|
||||
trigger_error('Invalid value inserted or no type specified.', E_USER_ERROR);
|
||||
|
||||
if (!isset($values[$matches[2]]))
|
||||
trigger_error('The database value you\'re trying to insert does not exist: ' . htmlspecialchars($matches[2]), E_USER_ERROR);
|
||||
|
||||
$replacement = $values[$matches[2]];
|
||||
|
||||
switch ($matches[1])
|
||||
{
|
||||
case 'int':
|
||||
if ((!is_numeric($replacement) || (string) $replacement !== (string) (int) $replacement) && $replacement !== 'NULL')
|
||||
trigger_error('Wrong value type sent to the database. Integer expected.', E_USER_ERROR);
|
||||
return $replacement !== 'NULL' ? (string) (int) $replacement : 'NULL';
|
||||
break;
|
||||
|
||||
case 'string':
|
||||
case 'text':
|
||||
return $replacement !== 'NULL' ? sprintf('\'%1$s\'', mysqli_real_escape_string($connection, $replacement)) : 'NULL';
|
||||
break;
|
||||
|
||||
case 'array_int':
|
||||
if (is_array($replacement))
|
||||
{
|
||||
if (empty($replacement))
|
||||
trigger_error('Database error, given array of integer values is empty.', E_USER_ERROR);
|
||||
|
||||
foreach ($replacement as $key => $value)
|
||||
{
|
||||
if (!is_numeric($value) || (string) $value !== (string) (int) $value)
|
||||
trigger_error('Wrong value type sent to the database. Array of integers expected.', E_USER_ERROR);
|
||||
|
||||
$replacement[$key] = (string) (int) $value;
|
||||
}
|
||||
|
||||
return implode(', ', $replacement);
|
||||
}
|
||||
else
|
||||
trigger_error('Wrong value type sent to the database. Array of integers expected.', E_USER_ERROR);
|
||||
|
||||
break;
|
||||
|
||||
case 'array_string':
|
||||
if (is_array($replacement))
|
||||
{
|
||||
if (empty($replacement))
|
||||
trigger_error('Database error, given array of string values is empty.', E_USER_ERROR);
|
||||
|
||||
foreach ($replacement as $key => $value)
|
||||
$replacement[$key] = sprintf('\'%1$s\'', mysqli_real_escape_string($connection, $value));
|
||||
|
||||
return implode(', ', $replacement);
|
||||
}
|
||||
else
|
||||
trigger_error('Wrong value type sent to the database. Array of strings expected.', E_USER_ERROR);
|
||||
break;
|
||||
|
||||
case 'date':
|
||||
if (preg_match('~^(\d{4})-([0-1]?\d)-([0-3]?\d)$~', $replacement, $date_matches) === 1)
|
||||
return sprintf('\'%04d-%02d-%02d\'', $date_matches[1], $date_matches[2], $date_matches[3]);
|
||||
elseif ($replacement === 'NULL')
|
||||
return 'NULL';
|
||||
else
|
||||
trigger_error('Wrong value type sent to the database. Date expected.', E_USER_ERROR);
|
||||
break;
|
||||
|
||||
case 'datetime':
|
||||
if (is_a($replacement, 'DateTime'))
|
||||
return $replacement->format('\'Y-m-d H:i:s\'');
|
||||
elseif (preg_match('~^(\d{4})-([0-1]?\d)-([0-3]?\d) (\d{2}):(\d{2}):(\d{2})$~', $replacement, $date_matches) === 1)
|
||||
return sprintf('\'%04d-%02d-%02d %02d:%02d:%02d\'', $date_matches[1], $date_matches[2], $date_matches[3], $date_matches[4], $date_matches[5], $date_matches[6]);
|
||||
elseif ($replacement === 'NULL')
|
||||
return 'NULL';
|
||||
else
|
||||
trigger_error('Wrong value type sent to the database. DateTime expected.', E_USER_ERROR);
|
||||
break;
|
||||
|
||||
case 'float':
|
||||
if (!is_numeric($replacement) && $replacement !== 'NULL')
|
||||
trigger_error('Wrong value type sent to the database. Floating point number expected.', E_USER_ERROR);
|
||||
return $replacement !== 'NULL' ? (string) (float) $replacement : 'NULL';
|
||||
break;
|
||||
|
||||
case 'identifier':
|
||||
// Backticks inside identifiers are supported as of MySQL 4.1. We don't need them here.
|
||||
return '`' . strtr($replacement, array('`' => '', '.' => '')) . '`';
|
||||
break;
|
||||
|
||||
case 'raw':
|
||||
return $replacement;
|
||||
break;
|
||||
|
||||
case 'bool':
|
||||
case 'boolean':
|
||||
// In mysql this is a synonym for tinyint(1)
|
||||
return (bool)$replacement ? 1 : 0;
|
||||
break;
|
||||
|
||||
default:
|
||||
trigger_error('Undefined type <b>' . $matches[1] . '</b> used in the database query', E_USER_ERROR);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes and quotes a string using values passed, and executes the query.
|
||||
*/
|
||||
public function query($db_string, $db_values = array())
|
||||
{
|
||||
// One more query....
|
||||
$this->query_count ++;
|
||||
|
||||
// Overriding security? This is evil!
|
||||
$security_override = $db_values === 'security_override' || !empty($db_values['security_override']);
|
||||
|
||||
// Please, just use new style queries.
|
||||
if (strpos($db_string, '\'') !== false && !$security_override)
|
||||
trigger_error('Hack attempt!', 'Illegal character (\') used in query.', E_USER_ERROR);
|
||||
|
||||
if (!$security_override && !empty($db_values))
|
||||
{
|
||||
// Set some values for use in the callback function.
|
||||
$this->db_callback = array($db_values, $this->connection);
|
||||
|
||||
// Insert the values passed to this function.
|
||||
$db_string = preg_replace_callback('~{([a-z_]+)(?::([a-zA-Z0-9_-]+))?}~', array(&$this, 'replacement_callback'), $db_string);
|
||||
|
||||
// Save some memory.
|
||||
$this->db_callback = [];
|
||||
}
|
||||
|
||||
if (defined("DB_LOG_QUERIES") && DB_LOG_QUERIES)
|
||||
$this->logged_queries[] = $db_string;
|
||||
|
||||
$return = @mysqli_query($this->connection, $db_string, empty($this->unbuffered) ? MYSQLI_STORE_RESULT : MYSQLI_USE_RESULT);
|
||||
|
||||
if (!$return)
|
||||
{
|
||||
$clean_sql = implode("\n", array_map('trim', explode("\n", $db_string)));
|
||||
trigger_error($this->error() . '<br>' . $clean_sql, E_USER_ERROR);
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes and quotes a string just like db_query, but does not execute the query.
|
||||
* Useful for debugging purposes.
|
||||
*/
|
||||
public function quote($db_string, $db_values = array())
|
||||
{
|
||||
// Please, just use new style queries.
|
||||
if (strpos($db_string, '\'') !== false)
|
||||
trigger_error('Hack attempt!', 'Illegal character (\') used in query.', E_USER_ERROR);
|
||||
|
||||
// Save some values for use in the callback function.
|
||||
$this->db_callback = array($db_values, $this->connection);
|
||||
|
||||
// Insert the values passed to this function.
|
||||
$db_string = preg_replace_callback('~{([a-z_]+)(?::([a-zA-Z0-9_-]+))?}~', array(&$this, 'replacement_callback'), $db_string);
|
||||
|
||||
// Save some memory.
|
||||
$this->db_callback = array();
|
||||
|
||||
return $db_string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a query, returning an array of all the rows it returns.
|
||||
*/
|
||||
public function queryRow($db_string, $db_values = array())
|
||||
{
|
||||
$res = $this->query($db_string, $db_values);
|
||||
|
||||
if (!$res || $this->num_rows($res) == 0)
|
||||
return array();
|
||||
|
||||
$row = $this->fetch_row($res);
|
||||
$this->free_result($res);
|
||||
|
||||
return $row;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a query, returning an array of all the rows it returns.
|
||||
*/
|
||||
public function queryRows($db_string, $db_values = array())
|
||||
{
|
||||
$res = $this->query($db_string, $db_values);
|
||||
|
||||
if (!$res || $this->num_rows($res) == 0)
|
||||
return array();
|
||||
|
||||
$rows = array();
|
||||
while ($row = $this->fetch_row($res))
|
||||
$rows[] = $row;
|
||||
|
||||
$this->free_result($res);
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a query, returning an array of all the rows it returns.
|
||||
*/
|
||||
public function queryPair($db_string, $db_values = array())
|
||||
{
|
||||
$res = $this->query($db_string, $db_values);
|
||||
|
||||
if (!$res || $this->num_rows($res) == 0)
|
||||
return array();
|
||||
|
||||
$rows = array();
|
||||
while ($row = $this->fetch_row($res))
|
||||
$rows[$row[0]] = $row[1];
|
||||
|
||||
$this->free_result($res);
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a query, returning an array of all the rows it returns.
|
||||
*/
|
||||
public function queryPairs($db_string, $db_values = array())
|
||||
{
|
||||
$res = $this->query($db_string, $db_values);
|
||||
|
||||
if (!$res || $this->num_rows($res) == 0)
|
||||
return array();
|
||||
|
||||
$rows = array();
|
||||
while ($row = $this->fetch_assoc($res))
|
||||
{
|
||||
$key_value = reset($row);
|
||||
$rows[$key_value] = $row;
|
||||
}
|
||||
|
||||
$this->free_result($res);
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a query, returning an associative array of all the rows it returns.
|
||||
*/
|
||||
public function queryAssoc($db_string, $db_values = array())
|
||||
{
|
||||
$res = $this->query($db_string, $db_values);
|
||||
|
||||
if (!$res || $this->num_rows($res) == 0)
|
||||
return array();
|
||||
|
||||
$row = $this->fetch_assoc($res);
|
||||
$this->free_result($res);
|
||||
|
||||
return $row;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a query, returning an associative array of all the rows it returns.
|
||||
*/
|
||||
public function queryAssocs($db_string, $db_values = array(), $connection = null)
|
||||
{
|
||||
$res = $this->query($db_string, $db_values);
|
||||
|
||||
if (!$res || $this->num_rows($res) == 0)
|
||||
return array();
|
||||
|
||||
$rows = array();
|
||||
while ($row = $this->fetch_assoc($res))
|
||||
$rows[] = $row;
|
||||
|
||||
$this->free_result($res);
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a query, returning the first value of the first row.
|
||||
*/
|
||||
public function queryValue($db_string, $db_values = array())
|
||||
{
|
||||
$res = $this->query($db_string, $db_values);
|
||||
|
||||
// If this happens, you're doing it wrong.
|
||||
if (!$res || $this->num_rows($res) == 0)
|
||||
return null;
|
||||
|
||||
list($value) = $this->fetch_row($res);
|
||||
$this->free_result($res);
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a query, returning an array of the first value of each row.
|
||||
*/
|
||||
public function queryValues($db_string, $db_values = array())
|
||||
{
|
||||
$res = $this->query($db_string, $db_values);
|
||||
|
||||
if (!$res || $this->num_rows($res) == 0)
|
||||
return array();
|
||||
|
||||
$rows = array();
|
||||
while ($row = $this->fetch_row($res))
|
||||
$rows[] = $row[0];
|
||||
|
||||
$this->free_result($res);
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function can be used to insert data into the database in a secure way.
|
||||
*/
|
||||
public function insert($method = 'replace', $table, $columns, $data)
|
||||
{
|
||||
// With nothing to insert, simply return.
|
||||
if (empty($data))
|
||||
return;
|
||||
|
||||
// Inserting data as a single row can be done as a single array.
|
||||
if (!is_array($data[array_rand($data)]))
|
||||
$data = array($data);
|
||||
|
||||
// Create the mold for a single row insert.
|
||||
$insertData = '(';
|
||||
foreach ($columns as $columnName => $type)
|
||||
{
|
||||
// Are we restricting the length?
|
||||
if (strpos($type, 'string-') !== false)
|
||||
$insertData .= sprintf('SUBSTRING({string:%1$s}, 1, ' . substr($type, 7) . '), ', $columnName);
|
||||
else
|
||||
$insertData .= sprintf('{%1$s:%2$s}, ', $type, $columnName);
|
||||
}
|
||||
$insertData = substr($insertData, 0, -2) . ')';
|
||||
|
||||
// Create an array consisting of only the columns.
|
||||
$indexed_columns = array_keys($columns);
|
||||
|
||||
// Here's where the variables are injected to the query.
|
||||
$insertRows = array();
|
||||
foreach ($data as $dataRow)
|
||||
$insertRows[] = $this->quote($insertData, array_combine($indexed_columns, $dataRow));
|
||||
|
||||
// Determine the method of insertion.
|
||||
$queryTitle = $method == 'replace' ? 'REPLACE' : ($method == 'ignore' ? 'INSERT IGNORE' : 'INSERT');
|
||||
|
||||
// Do the insert.
|
||||
return $this->query('
|
||||
' . $queryTitle . ' INTO ' . $table . ' (`' . implode('`, `', $indexed_columns) . '`)
|
||||
VALUES
|
||||
' . implode(',
|
||||
', $insertRows),
|
||||
array(
|
||||
'security_override' => true,
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
140
models/Dispatcher.php
Normal file
140
models/Dispatcher.php
Normal file
@ -0,0 +1,140 @@
|
||||
<?php
|
||||
/*****************************************************************************
|
||||
* Dispatcher.php
|
||||
* Contains key class Dispatcher.
|
||||
*
|
||||
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
||||
*****************************************************************************/
|
||||
|
||||
class Dispatcher
|
||||
{
|
||||
public static function route()
|
||||
{
|
||||
$possibleActions = [
|
||||
'albums' => 'ViewPhotoAlbums',
|
||||
'editasset' => 'EditAsset',
|
||||
'edituser' => 'EditUser',
|
||||
'login' => 'Login',
|
||||
'logout' => 'Logout',
|
||||
'managecomments' => 'ManageComments',
|
||||
'manageerrors' => 'ManageErrors',
|
||||
'managetags' => 'ManageTags',
|
||||
'manageusers' => 'ManageUsers',
|
||||
'people' => 'ViewPeople',
|
||||
'suggest' => 'ProvideAutoSuggest',
|
||||
'timeline' => 'ViewTimeline',
|
||||
'uploadmedia' => 'UploadMedia',
|
||||
];
|
||||
|
||||
// Work around PHP's FPM not always providing PATH_INFO.
|
||||
if (empty($_SERVER['PATH_INFO']) && isset($_SERVER['REQUEST_URI']))
|
||||
{
|
||||
if (strpos($_SERVER['REQUEST_URI'], '?') === false)
|
||||
$_SERVER['PATH_INFO'] = $_SERVER['REQUEST_URI'];
|
||||
else
|
||||
$_SERVER['PATH_INFO'] = substr($_SERVER['REQUEST_URI'], 0, strpos($_SERVER['REQUEST_URI'], '?'));
|
||||
}
|
||||
|
||||
// Nothing in particular? Just show the root of the album list.
|
||||
if (empty($_SERVER['PATH_INFO']) || $_SERVER['PATH_INFO'] == '/')
|
||||
{
|
||||
return new ViewPhotoAlbum();
|
||||
}
|
||||
// An album, person, or any other tag?
|
||||
elseif (preg_match('~^/(?<tag>.+?)(?:/page/(?<page>\d+))?/?$~', $_SERVER['PATH_INFO'], $path) && Tag::matchSlug($path['tag']))
|
||||
{
|
||||
$_GET = array_merge($_GET, $path);
|
||||
return new ViewPhotoAlbum();
|
||||
}
|
||||
// Look for an action...
|
||||
elseif (preg_match('~^/(?<action>[a-z]+)(?:/page/(?<page>\d+))?/?~', $_SERVER['PATH_INFO'], $path) && isset($possibleActions[$path['action']]))
|
||||
{
|
||||
$_GET = array_merge($_GET, $path);
|
||||
return new $possibleActions[$path['action']]();
|
||||
}
|
||||
else
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
public static function dispatch()
|
||||
{
|
||||
// Let's try to find our bearings!
|
||||
try
|
||||
{
|
||||
$page = self::route();
|
||||
$page->showContent();
|
||||
}
|
||||
// Something wasn't found?
|
||||
catch (NotFoundException $e)
|
||||
{
|
||||
self::trigger404();
|
||||
}
|
||||
// Or are they just sneaking into areas they don't belong?
|
||||
catch (NotAllowedException $e)
|
||||
{
|
||||
if (Registry::get('user')->isGuest())
|
||||
self::kickGuest();
|
||||
else
|
||||
self::trigger403();
|
||||
}
|
||||
catch (Exception $e)
|
||||
{
|
||||
ErrorHandler::handleError(E_USER_ERROR, 'Unspecified exception: ' . $e->getMessage(), $e->getFile(), $e->getLine());
|
||||
}
|
||||
catch (Error $e)
|
||||
{
|
||||
ErrorHandler::handleError(E_USER_ERROR, 'Fatal error: ' . $e->getMessage(), $e->getFile(), $e->getLine());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Kicks a guest to a login form, redirecting them back to this page upon login.
|
||||
*/
|
||||
public static function kickGuest()
|
||||
{
|
||||
$form = new LogInForm('Log in');
|
||||
$form->setErrorMessage('Admin access required. Please log in.');
|
||||
$form->setRedirectUrl($_SERVER['REQUEST_URI']);
|
||||
|
||||
$page = new MainTemplate('Login required');
|
||||
$page->appendStylesheet(BASEURL . '/css/admin.css');
|
||||
$page->adopt($form);
|
||||
$page->html_main();
|
||||
exit;
|
||||
}
|
||||
|
||||
public static function trigger400()
|
||||
{
|
||||
header('HTTP/1.1 400 Bad Request');
|
||||
$page = new MainTemplate('Bad request');
|
||||
$page->adopt(new DummyBox('Bad request', '<p>The server does not understand your request.</p>'));
|
||||
$page->html_main();
|
||||
exit;
|
||||
}
|
||||
|
||||
public static function trigger403()
|
||||
{
|
||||
header('HTTP/1.1 403 Forbidden');
|
||||
$page = new MainTemplate('Access denied');
|
||||
$page->adopt(new DummyBox('Forbidden', '<p>You do not have access to the page you requested.</p>'));
|
||||
$page->html_main();
|
||||
exit;
|
||||
}
|
||||
|
||||
public static function trigger404()
|
||||
{
|
||||
header('HTTP/1.1 404 Not Found');
|
||||
$page = new MainTemplate('Page not found');
|
||||
|
||||
if (Registry::has('user') && Registry::get('user')->isAdmin())
|
||||
{
|
||||
$page->appendStylesheet(BASEURL . '/css/admin.css');
|
||||
$page->adopt(new AdminBar());
|
||||
}
|
||||
|
||||
$page->adopt(new DummyBox('Well, this is a bit embarrassing!', '<p>The page you requested could not be found. Don\'t worry, it\'s probably not your fault. You\'re welcome to browse the website, though!</p>', 'errormsg'));
|
||||
$page->addClass('errorpage');
|
||||
$page->html_main();
|
||||
exit;
|
||||
}
|
||||
}
|
135
models/EXIF.php
Normal file
135
models/EXIF.php
Normal file
@ -0,0 +1,135 @@
|
||||
<?php
|
||||
|
||||
class EXIF
|
||||
{
|
||||
public $aperture = 0;
|
||||
public $credit = '';
|
||||
public $camera = '';
|
||||
public $caption = '';
|
||||
public $created_timestamp = 0;
|
||||
public $copyright = '';
|
||||
public $focal_length = 0;
|
||||
public $iso = 0;
|
||||
public $shutter_speed = 0;
|
||||
public $title = '';
|
||||
|
||||
private function __construct(array $meta)
|
||||
{
|
||||
foreach ($meta as $key => $value)
|
||||
$this->$key = $value;
|
||||
}
|
||||
|
||||
public static function fromFile($file, $as_array = false, $override_cache = false)
|
||||
{
|
||||
if (!file_exists($file))
|
||||
return false;
|
||||
|
||||
$meta = [
|
||||
'aperture' => 0,
|
||||
'credit' => '',
|
||||
'camera' => '',
|
||||
'caption' => '',
|
||||
'created_timestamp' => 0,
|
||||
'copyright' => '',
|
||||
'focal_length' => 0,
|
||||
'iso' => 0,
|
||||
'shutter_speed' => 0,
|
||||
'title' => '',
|
||||
];
|
||||
|
||||
if (!function_exists('exif_read_data'))
|
||||
throw new Exception("The PHP module 'exif' appears to be disabled. Please enable it to use EXIF functions.");
|
||||
|
||||
list(, , $image_type) = getimagesize($file);
|
||||
if (!in_array($image_type, [IMAGETYPE_JPEG, IMAGETYPE_TIFF_II, IMAGETYPE_TIFF_MM]))
|
||||
return new self($meta);
|
||||
|
||||
$exif = @exif_read_data($file);
|
||||
|
||||
if (!empty($exif['Title']))
|
||||
$meta['title'] = trim($exif['Title']);
|
||||
|
||||
if (!empty($exif['ImageDescription']))
|
||||
{
|
||||
if (empty($meta['title']) && strlen($exif['ImageDescription']) < 80)
|
||||
{
|
||||
$meta['title'] = trim($exif['ImageDescription']);
|
||||
if (!empty($exif['COMPUTED']['UserComment']) && trim($exif['COMPUTED']['UserComment']) != $meta['title'])
|
||||
$meta['caption'] = trim($exif['COMPUTED']['UserComment']);
|
||||
}
|
||||
elseif (trim($exif['ImageDescription']) != $meta['title'])
|
||||
$meta['caption'] = trim($exif['ImageDescription']);
|
||||
}
|
||||
elseif (!empty($exif['Comments']) && trim($exif['Comments']) != $meta['title'])
|
||||
$meta['caption'] = trim($exif['Comments']);
|
||||
|
||||
if (!empty($exif['Artist']))
|
||||
$meta['credit'] = trim($exif['Artist']);
|
||||
elseif (!empty($exif['Author']))
|
||||
$meta['credit'] = trim($exif['Author']);
|
||||
|
||||
if (!empty($exif['Copyright']))
|
||||
$meta['copyright'] = trim($exif['Copyright']);
|
||||
|
||||
if (!empty($exif['ExposureTime']))
|
||||
$meta['shutter_speed'] = (string) self::frac2dec($exif['ExposureTime']);
|
||||
|
||||
if (!empty($exif['FNumber']))
|
||||
$meta['aperture'] = round(self::frac2dec($exif['FNumber']), 2);
|
||||
|
||||
if (!empty($exif['FocalLength']))
|
||||
$meta['focal_length'] = (string) self::frac2dec($exif['FocalLength']);
|
||||
|
||||
if (!empty($exif['ISOSpeedRatings']))
|
||||
{
|
||||
$meta['iso'] = is_array($exif['ISOSpeedRatings']) ? reset($exif['ISOSpeedRatings']) : $exif['ISOSpeedRatings'];
|
||||
$meta['iso'] = trim($meta['iso']);
|
||||
}
|
||||
|
||||
if (!empty($exif['Model']))
|
||||
{
|
||||
if (!empty($exif['Make']) && strpos($exif['Model'], $exif['Make']) === false)
|
||||
$meta['camera'] = trim($exif['Make']) . ' ' . trim($exif['Model']);
|
||||
else
|
||||
$meta['camera'] = trim($exif['Model']);
|
||||
}
|
||||
elseif (!empty($exif['Make']))
|
||||
$meta['camera'] = trim($exif['Make']);
|
||||
|
||||
if (!empty($exif['DateTimeDigitized']))
|
||||
$meta['created_timestamp'] = self::toUnixTime($exif['DateTimeDigitized']);
|
||||
|
||||
return new self($meta);
|
||||
}
|
||||
|
||||
public function shutterSpeedFraction()
|
||||
{
|
||||
$speed = $this->shutter_speed;
|
||||
if (empty($this->shutter_speed))
|
||||
return '';
|
||||
|
||||
if (1 / $this->shutter_speed <= 1)
|
||||
return $this->shutter_speed . ' seconds';
|
||||
|
||||
$speed = (float) 1 / $this->shutter_speed;
|
||||
if (in_array($speed, [1.3, 1.5, 1.6, 2.5]))
|
||||
return "1/" . number_format($speed, 1, '.', '') . ' second';
|
||||
else
|
||||
return "1/" . number_format($speed, 0, '.', '') . ' second';
|
||||
}
|
||||
|
||||
// Convert a fraction string to a decimal.
|
||||
private static function frac2dec($str)
|
||||
{
|
||||
@list($n, $d) = explode('/', $str);
|
||||
return !empty($d) ? $n / $d : $str;
|
||||
}
|
||||
|
||||
// Convert an EXIF formatted date to a UNIX timestamp.
|
||||
private static function toUnixTime($str)
|
||||
{
|
||||
@list($date, $time) = explode(' ', trim($str));
|
||||
@list($y, $m, $d) = explode(':', $date);
|
||||
return strtotime("{$y}-{$m}-{$d} {$time}");
|
||||
}
|
||||
}
|
159
models/ErrorHandler.php
Normal file
159
models/ErrorHandler.php
Normal file
@ -0,0 +1,159 @@
|
||||
<?php
|
||||
/*****************************************************************************
|
||||
* ErrorHandler.php
|
||||
* Contains key class ErrorHandler.
|
||||
*
|
||||
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
||||
*****************************************************************************/
|
||||
|
||||
class ErrorHandler
|
||||
{
|
||||
private static $error_count = 0;
|
||||
private static $handling_error;
|
||||
|
||||
// Handler for standard PHP error messages.
|
||||
public static function handleError($error_level, $error_message, $file, $line, $context = null)
|
||||
{
|
||||
// Don't handle suppressed errors (e.g. through @ operator)
|
||||
if (!(error_reporting() & $error_level))
|
||||
return;
|
||||
|
||||
// Prevent recursing if we've messed up in this code path.
|
||||
if (self::$handling_error)
|
||||
return;
|
||||
|
||||
self::$error_count++;
|
||||
self::$handling_error = true;
|
||||
|
||||
// Basically, htmlspecialchars it, minus '&' for HTML entities.
|
||||
$error_message = strtr($error_message, ['<' => '<', '>' => '>', '"' => '"', "\t" => ' ']);
|
||||
$error_message = strtr($error_message, ['<br>' => "<br>", '<br />' => "<br>", '<b>' => '<strong>', '</b>' => '</strong>', '<pre>' => '<pre>', '</pre>' => '</pre>']);
|
||||
|
||||
// Generate a bunch of useful information to ease debugging later.
|
||||
$debug_info = self::getDebugInfo(debug_backtrace());
|
||||
|
||||
// Log the error in the database.
|
||||
self::logError($error_message, $debug_info, $file, $line);
|
||||
|
||||
// Are we considering this fatal? Then display and exit.
|
||||
// !!! TODO: should we consider warnings fatal?
|
||||
if (true) // DEBUG || (!DEBUG && $error_level === E_WARNING || $error_level === E_USER_WARNING))
|
||||
self::display($file . ' (' . $line . ')<br>' . $error_message, $debug_info);
|
||||
|
||||
// If it wasn't a fatal error, well...
|
||||
self::$handling_error = false;
|
||||
}
|
||||
|
||||
public static function getDebugInfo(array $trace)
|
||||
{
|
||||
$debug_info = "Backtrace:\n";
|
||||
$debug_info .= self::formatBacktrace($trace);
|
||||
|
||||
// Include info on the contents of superglobals.
|
||||
if (!empty($_SESSION))
|
||||
$debug_info .= "\nSESSION: " . print_r($_SESSION, true);
|
||||
if (!empty($_POST))
|
||||
$debug_info .= "\nPOST: " . print_r($_POST, true);
|
||||
if (!empty($_GET))
|
||||
$debug_info .= "\nGET: " . print_r($_GET, true);
|
||||
|
||||
return $debug_info;
|
||||
}
|
||||
|
||||
private static function formatBacktrace(array $trace)
|
||||
{
|
||||
$buffer = '';
|
||||
$skipping = true;
|
||||
|
||||
foreach ($trace as $i => $call)
|
||||
{
|
||||
if (isset($call['class']) && ($call['class'] === 'ErrorHandler' || $call['class'] === 'Database') ||
|
||||
isset($call['function']) && $call['function'] === 'preg_replace_callback')
|
||||
{
|
||||
if (!$skipping)
|
||||
{
|
||||
$buffer .= "[...]\n";
|
||||
$skipping = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
else
|
||||
$skipping = false;
|
||||
|
||||
$file = isset($call['file']) ? str_replace(BASEDIR, '', $call['file']) : 'Unknown';
|
||||
$object = isset($call['class']) ? $call['class'] . $call['type'] : '';
|
||||
|
||||
$args = [];
|
||||
foreach ($call['args'] as $j => $arg)
|
||||
{
|
||||
if (is_array($arg))
|
||||
$args[$j] = print_r($arg, true);
|
||||
elseif (is_object($arg))
|
||||
$args[$j] = var_dump($arg);
|
||||
}
|
||||
|
||||
$buffer .= '#' . str_pad($i, 3, ' ')
|
||||
. $object . $call['function'] . '(' . implode(', ', $args) . ')'
|
||||
. ' called at [' . $file . ':' . $call['line'] . "]\n";
|
||||
}
|
||||
|
||||
return $buffer;
|
||||
}
|
||||
|
||||
// Logs an error into the database.
|
||||
private static function logError($error_message = '', $debug_info = '', $file = '', $line = 0)
|
||||
{
|
||||
if (!ErrorLog::log([
|
||||
'message' => $error_message,
|
||||
'debug_info' => $debug_info,
|
||||
'file' => str_replace(BASEDIR, '', $file),
|
||||
'line' => $line,
|
||||
'id_user' => Registry::has('user') ? Registry::get('user')->getUserId() : 0,
|
||||
'ip_address' => isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '',
|
||||
'request_uri' => isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '',
|
||||
]))
|
||||
{
|
||||
header('HTTP/1.1 503 Service Temporarily Unavailable');
|
||||
echo '<h2>An Error Occured</h2><p>Our software could not connect to the database. We apologise for any inconvenience and ask you to check back later.</p>';
|
||||
exit;
|
||||
}
|
||||
|
||||
return $error_message;
|
||||
}
|
||||
|
||||
public static function display($message, $debug_info)
|
||||
{
|
||||
// Just show the message if we're running in a console.
|
||||
if (empty($_SERVER['HTTP_HOST']))
|
||||
{
|
||||
echo $message;
|
||||
exit;
|
||||
}
|
||||
|
||||
// Initialise the main template to present a nice message to the user.
|
||||
$page = new MainTemplate('An error occured!');
|
||||
|
||||
// Show the error.
|
||||
$is_admin = Registry::has('user') && Registry::get('user')->isAdmin();
|
||||
if (DEBUG || $is_admin)
|
||||
{
|
||||
$page->adopt(new DummyBox('An error occured!', '<p>' . $message . '</p><pre>' . $debug_info . '</pre>'));
|
||||
|
||||
// Let's provide the admin navigation despite it all!
|
||||
if ($is_admin)
|
||||
{
|
||||
$page->appendStylesheet(BASEURL . '/css/admin.css');
|
||||
$page->adopt(new AdminBar());
|
||||
}
|
||||
}
|
||||
else
|
||||
$page->adopt(new DummyBox('An error occured!', '<p>Our apologies, an error occured while we were processing your request. Please try again later, or contact us if the problem persists.</p>'));
|
||||
|
||||
// If we got this far, make sure we're not showing stuff twice.
|
||||
ob_end_clean();
|
||||
|
||||
// Render the page.
|
||||
$page->html_main();
|
||||
exit;
|
||||
}
|
||||
}
|
36
models/ErrorLog.php
Normal file
36
models/ErrorLog.php
Normal file
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
/*****************************************************************************
|
||||
* ErrorLog.php
|
||||
* Contains key class ErrorLog.
|
||||
*
|
||||
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
||||
*****************************************************************************/
|
||||
|
||||
class ErrorLog
|
||||
{
|
||||
public static function log(array $data)
|
||||
{
|
||||
if (!Registry::has('db'))
|
||||
return false;
|
||||
|
||||
return Registry::get('db')->query('
|
||||
INSERT INTO log_errors
|
||||
(id_user, message, debug_info, file, line, request_uri, time, ip_address)
|
||||
VALUES
|
||||
({int:id_user}, {string:message}, {string:debug_info}, {string:file}, {int:line},
|
||||
{string:request_uri}, CURRENT_TIMESTAMP, {string:ip_address})',
|
||||
$data);
|
||||
}
|
||||
|
||||
public static function flush()
|
||||
{
|
||||
return Registry::get('db')->query('TRUNCATE log_errors');
|
||||
}
|
||||
|
||||
public static function getCount()
|
||||
{
|
||||
return Registry::get('db')->queryValue('
|
||||
SELECT COUNT(*)
|
||||
FROM log_errors');
|
||||
}
|
||||
}
|
165
models/Form.php
Normal file
165
models/Form.php
Normal file
@ -0,0 +1,165 @@
|
||||
<?php
|
||||
/*****************************************************************************
|
||||
* Form.php
|
||||
* Contains key class Form.
|
||||
*
|
||||
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
||||
*****************************************************************************/
|
||||
|
||||
class Form
|
||||
{
|
||||
public $request_method;
|
||||
public $request_url;
|
||||
public $content_above;
|
||||
public $content_below;
|
||||
private $fields;
|
||||
private $data;
|
||||
private $missing;
|
||||
|
||||
// NOTE: this class does not verify the completeness of form options.
|
||||
public function __construct($options)
|
||||
{
|
||||
$this->request_method = !empty($options['request_method']) ? $options['request_method'] : 'POST';
|
||||
$this->request_url = !empty($options['request_url']) ? $options['request_url'] : BASEURL;
|
||||
$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;
|
||||
}
|
||||
|
||||
public function verify($post)
|
||||
{
|
||||
$this->data = [];
|
||||
$this->missing = [];
|
||||
|
||||
foreach ($this->fields as $field_id => $field)
|
||||
{
|
||||
// Field disabled?
|
||||
if (!empty($field['disabled']))
|
||||
{
|
||||
$this->data[$field_id] = '';
|
||||
continue;
|
||||
}
|
||||
|
||||
// No data present at all for this field?
|
||||
if ((!isset($post[$field_id]) || $post[$field_id] == '') && empty($field['is_optional']))
|
||||
{
|
||||
$this->missing[] = $field_id;
|
||||
$this->data[$field_id] = '';
|
||||
continue;
|
||||
}
|
||||
|
||||
// Verify data for all fields
|
||||
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;
|
||||
}
|
||||
else
|
||||
$this->data[$field_id] = $post[$field_id];
|
||||
break;
|
||||
|
||||
case 'checkbox':
|
||||
// Just give us a 'boolean' int for this one
|
||||
$this->data[$field_id] = empty($post[$field_id]) ? 0 : 1;
|
||||
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;
|
||||
}
|
||||
else
|
||||
$this->data[$field_id] = $post[$field_id];
|
||||
break;
|
||||
|
||||
case 'file':
|
||||
// Needs to be verified elsewhere!
|
||||
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->missing[] = $field_id;
|
||||
$this->data[$field_id] = 0;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'text':
|
||||
case 'textarea':
|
||||
default:
|
||||
$this->data[$field_id] = isset($post[$field_id]) ? $post[$field_id] : '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function setData($data)
|
||||
{
|
||||
$this->verify($data);
|
||||
$this->missing = [];
|
||||
}
|
||||
|
||||
public function getFields()
|
||||
{
|
||||
return $this->fields;
|
||||
}
|
||||
|
||||
public function getData()
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
public function getMissing()
|
||||
{
|
||||
return $this->missing;
|
||||
}
|
||||
}
|
246
models/GenericTable.php
Normal file
246
models/GenericTable.php
Normal file
@ -0,0 +1,246 @@
|
||||
<?php
|
||||
/*****************************************************************************
|
||||
* GenericTable.php
|
||||
* Contains key class GenericTable.
|
||||
*
|
||||
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
||||
*****************************************************************************/
|
||||
|
||||
class GenericTable extends PageIndex
|
||||
{
|
||||
protected $header = [];
|
||||
protected $body = [];
|
||||
protected $page_index = [];
|
||||
|
||||
protected $title;
|
||||
protected $title_class;
|
||||
protected $tableIsSortable = false;
|
||||
protected $recordCount;
|
||||
protected $needsPageIndex = false;
|
||||
protected $current_page;
|
||||
protected $num_pages;
|
||||
|
||||
public $form_above;
|
||||
public $form_below;
|
||||
|
||||
public function __construct($options)
|
||||
{
|
||||
// Make sure we're actually sorting on something sortable.
|
||||
if (!isset($options['sort_order']) || (!empty($options['sort_order']) && empty($options['columns'][$options['sort_order']]['is_sortable'])))
|
||||
$options['sort_order'] = '';
|
||||
|
||||
// Order in which direction?
|
||||
if (!empty($options['sort_direction']) && !in_array($options['sort_direction'], array('up', 'down')))
|
||||
$options['sort_direction'] = 'up';
|
||||
|
||||
// Make sure we know whether we can actually sort on something.
|
||||
$this->tableIsSortable = !empty($options['base_url']);
|
||||
|
||||
// How much stuff do we have?
|
||||
$this->recordCount = call_user_func_array($options['get_count'], !empty($options['get_count_params']) ? $options['get_count_params'] : array());
|
||||
|
||||
// Should we create a page index?
|
||||
$this->items_per_page = !empty($options['items_per_page']) ? $options['items_per_page'] : 30;
|
||||
$this->needsPageIndex = !empty($this->items_per_page) && $this->recordCount > $this->items_per_page;
|
||||
$this->index_class = isset($options['index_class']) ? $options['index_class'] : '';
|
||||
|
||||
// Figure out where to start.
|
||||
$this->start = empty($options['start']) || !is_numeric($options['start']) || $options['start'] < 0 || $options['start'] > $this->recordCount ? 0 : $options['start'];
|
||||
|
||||
// Let's bear a few things in mind...
|
||||
$this->base_url = $options['base_url'];
|
||||
|
||||
// This should be set at all times, too.
|
||||
if (empty($options['no_items_label']))
|
||||
$options['no_items_label'] = '';
|
||||
|
||||
// Gather parameters for the data gather function first.
|
||||
$parameters = array($this->start, $this->items_per_page, $options['sort_order'], $options['sort_direction']);
|
||||
if (!empty($options['get_data_params']) && is_array($options['get_data_params']))
|
||||
$parameters = array_merge($parameters, $options['get_data_params']);
|
||||
|
||||
// Okay, let's fetch the data!
|
||||
$data = call_user_func_array($options['get_data'], $parameters);
|
||||
|
||||
// Clean up a bit.
|
||||
$rows = $data['rows'];
|
||||
$this->sort_order = $data['order'];
|
||||
$this->sort_direction = $data['direction'];
|
||||
unset($data);
|
||||
|
||||
// Okay, now for the column headers...
|
||||
$this->generateColumnHeaders($options);
|
||||
|
||||
// Generate a pagination if requested
|
||||
if ($this->needsPageIndex)
|
||||
$this->generatePageIndex();
|
||||
|
||||
// Not a single row in sight?
|
||||
if (empty($rows))
|
||||
$this->body = $options['no_items_label'];
|
||||
// Otherwise, parse it all!
|
||||
else
|
||||
$this->parseAllRows($rows, $options);
|
||||
|
||||
// Got a title?
|
||||
$this->title = isset($options['title']) ? htmlentities($options['title']) : '';
|
||||
$this->title_class = isset($options['title_class']) ? $options['title_class'] : '';
|
||||
|
||||
// Maybe even a form or two?
|
||||
$this->form_above = isset($options['form_above']) ? $options['form_above'] : (isset($options['form']) ? $options['form'] : null);
|
||||
$this->form_below = isset($options['form_below']) ? $options['form_below'] : (isset($options['form']) ? $options['form'] : null);
|
||||
}
|
||||
|
||||
private function generateColumnHeaders($options)
|
||||
{
|
||||
foreach ($options['columns'] as $key => $column)
|
||||
{
|
||||
if (empty($column['header']))
|
||||
continue;
|
||||
|
||||
$header = array(
|
||||
'class' => isset($column['class']) ? $column['class'] : '',
|
||||
'colspan' => !empty($column['header_colspan']) ? $column['header_colspan'] : 1,
|
||||
'href' => !$this->tableIsSortable || empty($column['is_sortable']) ? '' : $this->getLink($this->start, $key, $key == $this->sort_order && $this->sort_direction == 'up' ? 'down' : 'up'),
|
||||
'label' => $column['header'],
|
||||
'scope' => 'col',
|
||||
'sort_mode' => $key == $this->sort_order ? $this->sort_direction : null,
|
||||
'width' => !empty($column['header_width']) && is_int($column['header_width']) ? $column['header_width'] : null,
|
||||
);
|
||||
|
||||
$this->header[] = $header;
|
||||
}
|
||||
}
|
||||
|
||||
private function parseAllRows($rows, $options)
|
||||
{
|
||||
// Parse all rows...
|
||||
$i = 0;
|
||||
foreach ($rows as $row)
|
||||
{
|
||||
$i ++;
|
||||
$newRow = array(
|
||||
'class' => $i %2 == 1 ? 'odd' : 'even',
|
||||
'cells' => array(),
|
||||
);
|
||||
|
||||
foreach ($options['columns'] as $column)
|
||||
{
|
||||
if (isset($column['enabled']) && $column['enabled'] == false)
|
||||
continue;
|
||||
|
||||
// The hard way?
|
||||
if (isset($column['parse']))
|
||||
{
|
||||
if (!isset($column['parse']['type']))
|
||||
$column['parse']['type'] = 'value';
|
||||
|
||||
// Parse the basic value first.
|
||||
switch ($column['parse']['type'])
|
||||
{
|
||||
// value: easy as pie.
|
||||
default:
|
||||
case 'value':
|
||||
$value = $row[$column['parse']['data']];
|
||||
break;
|
||||
|
||||
// sprintf: filling the gaps!
|
||||
case 'sprintf':
|
||||
$parameters = array($column['parse']['data']['pattern']);
|
||||
foreach ($column['parse']['data']['arguments'] as $identifier)
|
||||
$parameters[] = $row[$identifier];
|
||||
$value = call_user_func_array('sprintf', $parameters);
|
||||
break;
|
||||
|
||||
// timestamps: let's make them readable!
|
||||
case 'timestamp':
|
||||
$pattern = !empty($column['parse']['data']['pattern']) && $column['parse']['data']['pattern'] == 'long' ? '%F %H:%M' : '%H:%M';
|
||||
|
||||
if (!is_numeric($row[$column['parse']['data']['timestamp']]))
|
||||
$timestamp = strtotime($row[$column['parse']['data']['timestamp']]);
|
||||
else
|
||||
$timestamp = (int) $row[$column['parse']['data']['timestamp']];
|
||||
|
||||
if (isset($column['parse']['data']['if_null']) && $timestamp == 0)
|
||||
$value = $column['parse']['data']['if_null'];
|
||||
else
|
||||
$value = strftime($pattern, $timestamp);
|
||||
break;
|
||||
|
||||
// function: the flexible way!
|
||||
case 'function':
|
||||
$value = $column['parse']['data']($row);
|
||||
break;
|
||||
}
|
||||
|
||||
// Generate a link, if requested.
|
||||
if (!empty($column['parse']['link']))
|
||||
{
|
||||
// First, generate the replacement variables.
|
||||
$keys = array_keys($row);
|
||||
$values = array_values($row);
|
||||
foreach ($keys as $keyKey => $keyValue)
|
||||
$keys[$keyKey] = '{' . strtoupper($keyValue) . '}';
|
||||
|
||||
$value = '<a href="' . str_replace($keys, $values, $column['parse']['link']) . '">' . $value . '</a>';
|
||||
}
|
||||
}
|
||||
// The easy way!
|
||||
else
|
||||
$value = $row[$column['value']];
|
||||
|
||||
// Append the cell to the row.
|
||||
$newRow['cells'][] = array(
|
||||
'width' => !empty($column['cell_width']) && is_int($column['cell_width']) ? $column['cell_width'] : null,
|
||||
'value' => $value,
|
||||
);
|
||||
}
|
||||
|
||||
// Append the new row in the body.
|
||||
$this->body[] = $newRow;
|
||||
}
|
||||
}
|
||||
|
||||
protected function getLink($start = null, $order = null, $dir = null)
|
||||
{
|
||||
if ($start === null)
|
||||
$start = $this->start;
|
||||
if ($order === null)
|
||||
$order = $this->sort_order;
|
||||
if ($dir === null)
|
||||
$dir = $this->sort_direction;
|
||||
|
||||
return $this->base_url . (strpos($this->base_url, '?') ? '&' : '?') . 'start=' . $start . '&order=' . $order. '&dir=' . $dir;
|
||||
}
|
||||
|
||||
public function getOffset()
|
||||
{
|
||||
return $this->start;
|
||||
}
|
||||
|
||||
public function getArray()
|
||||
{
|
||||
// Makes no sense to call it for a table, but inherits from PageIndex due to poor design, sorry.
|
||||
throw new Exception('Function call is ambiguous.');
|
||||
}
|
||||
|
||||
public function getHeader()
|
||||
{
|
||||
return $this->header;
|
||||
}
|
||||
|
||||
public function getBody()
|
||||
{
|
||||
return $this->body;
|
||||
}
|
||||
|
||||
public function getTitle()
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
public function getTitleClass()
|
||||
{
|
||||
return $this->title_class;
|
||||
}
|
||||
}
|
31
models/Guest.php
Normal file
31
models/Guest.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
/*****************************************************************************
|
||||
* Guest.php
|
||||
* Contains key class Guest, derived from User.
|
||||
*
|
||||
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
||||
*****************************************************************************/
|
||||
|
||||
/**
|
||||
* Guest model; sets typical guest settings to the common attributes.
|
||||
*/
|
||||
class Guest extends User
|
||||
{
|
||||
/**
|
||||
* Constructor for the Guest class. Sets common attributes.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->id_user = 0;
|
||||
$this->is_logged = false;
|
||||
$this->is_guest = true;
|
||||
$this->is_admin = false;
|
||||
$this->first_name = 'Guest';
|
||||
$this->last_name = '';
|
||||
}
|
||||
|
||||
public function updateAccessTime()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
382
models/Image.php
Normal file
382
models/Image.php
Normal file
@ -0,0 +1,382 @@
|
||||
<?php
|
||||
/*****************************************************************************
|
||||
* Image.php
|
||||
* Contains key class Image.
|
||||
*
|
||||
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
||||
*****************************************************************************/
|
||||
|
||||
class Image extends Asset
|
||||
{
|
||||
const TYPE_PANORAMA = 1;
|
||||
const TYPE_LANDSCAPE = 2;
|
||||
const TYPE_PORTRAIT = 4;
|
||||
|
||||
protected function __construct(array $data)
|
||||
{
|
||||
foreach ($data as $attribute => $value)
|
||||
$this->$attribute = $value;
|
||||
}
|
||||
|
||||
public static function fromId($id_asset, $return_format = 'object')
|
||||
{
|
||||
$asset = parent::fromId($id_asset, 'array');
|
||||
if ($asset)
|
||||
return $return_format == 'object' ? new Image($asset) : $asset;
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function fromIds(array $id_assets, $return_format = 'object')
|
||||
{
|
||||
if (empty($id_assets))
|
||||
return [];
|
||||
|
||||
$assets = parent::fromIds($id_assets, 'array');
|
||||
|
||||
if ($return_format == 'array')
|
||||
return $assets;
|
||||
else
|
||||
{
|
||||
$objects = [];
|
||||
foreach ($assets as $id => $asset)
|
||||
$objects[$id] = new Image($asset);
|
||||
return $objects;
|
||||
}
|
||||
}
|
||||
|
||||
public function save()
|
||||
{
|
||||
$data = [];
|
||||
foreach ($this->meta as $key => $value)
|
||||
$data[] = [
|
||||
'id_asset' => $this->id_asset,
|
||||
'variable' => $key,
|
||||
'value' => $value,
|
||||
];
|
||||
|
||||
return Registry::get('db')->insert('replace', 'assets_meta', [
|
||||
'id_asset' => 'int',
|
||||
'variable' => 'string',
|
||||
'value' => 'string',
|
||||
], $data);
|
||||
}
|
||||
|
||||
public function getExif()
|
||||
{
|
||||
return EXIF::fromFile($this->getPath());
|
||||
}
|
||||
|
||||
public function getPath()
|
||||
{
|
||||
return ASSETSDIR . '/' . $this->subdir . '/' . $this->filename;
|
||||
}
|
||||
|
||||
public function getUrl()
|
||||
{
|
||||
return ASSETSURL . '/' . $this->subdir . '/' . $this->filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param width: width of the thumbnail.
|
||||
* @param height: height of the thumbnail.
|
||||
* @param crop: whether and how to crop original image to fit. [false|true|'top'|'center'|'bottom']
|
||||
* @param fit: whether to fit the image to given boundaries [true], or use them merely as an estimation [false].
|
||||
*/
|
||||
public function getThumbnailUrl($width, $height, $crop = true, $fit = true)
|
||||
{
|
||||
// First, assert the image's dimensions are properly known in the database.
|
||||
if (!isset($this->image_height, $this->image_width))
|
||||
throw new UnexpectedValueException('Image width or height is undefined -- inconsistent database?');
|
||||
|
||||
// Inferring width or height?
|
||||
if (!$height)
|
||||
$height = ceil($width / $this->image_width * $this->image_height);
|
||||
elseif (!$width)
|
||||
$width = ceil($height / $this->image_height * $this->image_width);
|
||||
|
||||
// Inferring the height from the original image's ratio?
|
||||
if (!$fit)
|
||||
$height = floor($width / ($this->image_width / $this->image_height));
|
||||
|
||||
// Assert we have both, now...
|
||||
if (empty($width) || empty($height))
|
||||
throw new InvalidArgumentException('Expecting at least either width or height as argument.');
|
||||
|
||||
// If we're cropping, verify we're in the right mode.
|
||||
if ($crop)
|
||||
{
|
||||
// If the original image's aspect ratio is much wider, take a slice instead.
|
||||
if ($this->image_width / $this->image_height > $width / $height)
|
||||
$crop = 'slice';
|
||||
|
||||
// We won't be cropping if the thumbnail is proportional to its original.
|
||||
if (abs($width / $height - $this->image_width / $this->image_height) <= 0.05)
|
||||
$crop = false;
|
||||
}
|
||||
|
||||
// Do we have an exact crop boundary for these dimensions?
|
||||
$crop_selector = "crop_{$width}x{$height}";
|
||||
if (isset($this->meta[$crop_selector]))
|
||||
$crop = 'exact';
|
||||
|
||||
// Now, do we need to suffix the filename?
|
||||
if ($crop)
|
||||
$suffix = '_c' . (is_string($crop) && $crop !== 'center' ? substr($crop, 0, 1) : '');
|
||||
else
|
||||
$suffix = '';
|
||||
|
||||
// Check whether we already resized this earlier.
|
||||
$thumb_selector = "thumb_{$width}x{$height}{$suffix}";
|
||||
if (isset($this->meta[$thumb_selector]) && file_exists(THUMBSDIR . '/' . $this->subdir . '/' . $this->meta[$thumb_selector]))
|
||||
return THUMBSURL . '/' . $this->subdir . '/' . $this->meta[$thumb_selector];
|
||||
|
||||
// Do we have a custom thumbnail on file?
|
||||
$custom_selector = "custom_{$width}x{$height}";
|
||||
if (isset($this->meta[$custom_selector]))
|
||||
{
|
||||
if (file_exists(ASSETSDIR . '/' . $this->subdir . '/' . $this->meta[$custom_selector]))
|
||||
{
|
||||
// Copy the custom thumbail to the general thumbnail directory.
|
||||
copy(ASSETSDIR . '/' . $this->subdir . '/' . $this->meta[$custom_selector],
|
||||
THUMBSDIR . '/' . $this->subdir . '/' . $this->meta[$custom_selector]);
|
||||
|
||||
// Let's remember this for future reference.
|
||||
$this->meta[$thumb_selector] = $this->meta[$custom_selector];
|
||||
$this->save();
|
||||
|
||||
return THUMBSURL . '/' . $this->subdir . '/' . $this->meta[$custom_selector];
|
||||
}
|
||||
else
|
||||
throw new UnexpectedValueException('Custom thumbnail expected, but missing in file system!');
|
||||
}
|
||||
|
||||
// Let's try some arcane stuff...
|
||||
try
|
||||
{
|
||||
if (!class_exists('Imagick'))
|
||||
throw new Exception("The PHP module 'imagick' appears to be disabled. Please enable it to use image resampling functions.");
|
||||
|
||||
$thumb = new Imagick(ASSETSDIR . '/' . $this->subdir . '/' . $this->filename);
|
||||
|
||||
// The image might have some orientation set through EXIF. Let's apply this first.
|
||||
self::applyRotation($thumb);
|
||||
|
||||
// Just resizing? Easy peasy.
|
||||
if (!$crop)
|
||||
$thumb->resizeImage($width, $height, Imagick::FILTER_LANCZOS, 1);
|
||||
// Cropping in the center?
|
||||
elseif ($crop === true || $crop === 'center')
|
||||
$thumb->cropThumbnailImage($width, $height);
|
||||
// Exact cropping? We can do that.
|
||||
elseif ($crop === 'exact')
|
||||
{
|
||||
list($crop_width, $crop_height, $crop_x_pos, $crop_y_pos) = explode(',', $this->meta[$crop_selector]);
|
||||
$thumb->cropImage($crop_width, $crop_height, $crop_x_pos, $crop_y_pos);
|
||||
$thumb->resizeImage($width, $height, Imagick::FILTER_LANCZOS, 1);
|
||||
}
|
||||
// Advanced cropping? Fun!
|
||||
else
|
||||
{
|
||||
$size = $thumb->getImageGeometry();
|
||||
|
||||
// Taking a horizontal slice from the top or bottom of the original image?
|
||||
if ($crop === 'top' || $crop === 'bottom')
|
||||
{
|
||||
$crop_width = $size['width'];
|
||||
$crop_height = floor($size['width'] / $width * $height);
|
||||
$target_x = 0;
|
||||
$target_y = $crop === 'top' ? 0 : $size['height'] - $crop_height;
|
||||
}
|
||||
// Otherwise, we're taking a vertical slice from the centre.
|
||||
else
|
||||
{
|
||||
$crop_width = floor($size['height'] / $height * $width);
|
||||
$crop_height = $size['height'];
|
||||
$target_x = floor(($size['width'] - $crop_width) / 2);
|
||||
$target_y = 0;
|
||||
}
|
||||
|
||||
$thumb->cropImage($crop_width, $crop_height, $target_x, $target_y);
|
||||
$thumb->resizeImage($width, $height, Imagick::FILTER_LANCZOS, 1);
|
||||
}
|
||||
|
||||
// What sort of image is this? Fall back to PNG if we must.
|
||||
switch ($thumb->getImageFormat())
|
||||
{
|
||||
case 'JPEG':
|
||||
$ext = 'jpg';
|
||||
break;
|
||||
|
||||
case 'GIF':
|
||||
$ext = 'gif';
|
||||
break;
|
||||
|
||||
case 'PNG':
|
||||
default:
|
||||
$thumb->setFormat('PNG');
|
||||
$ext = 'png';
|
||||
break;
|
||||
}
|
||||
|
||||
// So, how do we name this?
|
||||
$thumbfilename = substr($this->filename, 0, strrpos($this->filename, '.')) . "_{$width}x{$height}{$suffix}.$ext";
|
||||
|
||||
// Ensure the thumbnail subdirectory exists.
|
||||
if (!is_dir(THUMBSDIR . '/' . $this->subdir))
|
||||
mkdir(THUMBSDIR . '/' . $this->subdir, 0755, true);
|
||||
|
||||
// Save it in a public spot.
|
||||
$thumb->writeImage(THUMBSDIR . '/' . $this->subdir . '/' . $thumbfilename);
|
||||
$thumb->clear();
|
||||
$thumb->destroy();
|
||||
}
|
||||
// Blast! Curse your sudden but inevitable betrayal!
|
||||
catch (ImagickException $e)
|
||||
{
|
||||
throw new Exception('ImageMagick error occurred while generating thumbnail. Output: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
// Let's remember this for future reference.
|
||||
$this->meta[$thumb_selector] = $thumbfilename;
|
||||
$this->save();
|
||||
|
||||
// Ah yes, you wanted a URL, didn't you...
|
||||
return THUMBSURL . '/' . $this->subdir . '/' . $this->meta[$thumb_selector];
|
||||
}
|
||||
|
||||
private static function applyRotation(Imagick $image)
|
||||
{
|
||||
switch ($image->getImageOrientation())
|
||||
{
|
||||
// Clockwise rotation
|
||||
case Imagick::ORIENTATION_RIGHTTOP:
|
||||
$image->rotateImage("#000", 90);
|
||||
break;
|
||||
|
||||
// Counter-clockwise rotation
|
||||
case Imagick::ORIENTATION_LEFTBOTTOM:
|
||||
$image->rotateImage("#000", 270);
|
||||
break;
|
||||
|
||||
// Upside down?
|
||||
case Imagick::ORIENTATION_BOTTOMRIGHT:
|
||||
$image->rotateImage("#000", 180);
|
||||
}
|
||||
|
||||
// Having rotated the image, make sure the EXIF data is set properly.
|
||||
$image->setImageOrientation(Imagick::ORIENTATION_TOPLEFT);
|
||||
}
|
||||
|
||||
public function bestColor()
|
||||
{
|
||||
// Save some computations if we can.
|
||||
if (isset($this->meta['best_color']))
|
||||
return $this->meta['best_color'];
|
||||
|
||||
// Find out what colour is most prominent.
|
||||
$color = new BestColor($this);
|
||||
$this->meta['best_color'] = $color->hex();
|
||||
$this->save();
|
||||
|
||||
// There's your colour.
|
||||
return $this->meta['best_color'];
|
||||
}
|
||||
|
||||
public function bestLabelColor()
|
||||
{
|
||||
// Save some computations if we can.
|
||||
if (isset($this->meta['best_color_label']))
|
||||
return $this->meta['best_color_label'];
|
||||
|
||||
// Find out what colour is most prominent.
|
||||
$color = new BestColor($this);
|
||||
$this->meta['best_color_label'] = $color->rgba();
|
||||
$this->save();
|
||||
|
||||
// There's your colour.
|
||||
return $this->meta['best_color_label'];
|
||||
}
|
||||
|
||||
public function width()
|
||||
{
|
||||
return $this->image_width;
|
||||
}
|
||||
|
||||
public function height()
|
||||
{
|
||||
return $this->image_height;
|
||||
}
|
||||
|
||||
public function isPanorama()
|
||||
{
|
||||
return $this->image_width / $this->image_height > 2;
|
||||
}
|
||||
|
||||
public function isPortrait()
|
||||
{
|
||||
return $this->image_width / $this->image_height < 1;
|
||||
}
|
||||
|
||||
public function isLandscape()
|
||||
{
|
||||
$ratio = $this->image_width / $this->image_height;
|
||||
return $ratio >= 1 && $ratio <= 2;
|
||||
}
|
||||
|
||||
public function removeAllThumbnails()
|
||||
{
|
||||
foreach ($this->meta as $key => $value)
|
||||
{
|
||||
if (substr($key, 0, 6) !== 'thumb_')
|
||||
continue;
|
||||
|
||||
$thumb_path = THUMBSDIR . '/' . $this->subdir . '/' . $value;
|
||||
if (is_file($thumb_path))
|
||||
unlink($thumb_path);
|
||||
|
||||
unset($this->meta[$key]);
|
||||
}
|
||||
|
||||
$this->saveMetaData();
|
||||
}
|
||||
|
||||
public function replaceThumbnail($descriptor, $tmp_file)
|
||||
{
|
||||
if (!is_file($tmp_file))
|
||||
return -1;
|
||||
|
||||
if (!isset($this->meta[$descriptor]))
|
||||
return -2;
|
||||
|
||||
$image = new Imagick($tmp_file);
|
||||
$d = $image->getImageGeometry();
|
||||
unset($image);
|
||||
|
||||
// Check whether dimensions match.
|
||||
$test_descriptor = 'thumb_' . $d['width'] . 'x' . $d['height'];
|
||||
if ($descriptor !== $test_descriptor && strpos($descriptor, $test_descriptor . '_') === false)
|
||||
return -3;
|
||||
|
||||
// Save the custom thumbnail in the assets directory.
|
||||
$destination = ASSETSDIR . '/' . $this->subdir . '/' . $this->meta[$descriptor];
|
||||
if (file_exists($destination) && !is_writable($destination))
|
||||
return -4;
|
||||
|
||||
if (!copy($tmp_file, $destination))
|
||||
return -5;
|
||||
|
||||
// Copy it to the thumbnail directory, overwriting the automatically generated one, too.
|
||||
$destination = THUMBSDIR . '/' . $this->subdir . '/' . $this->meta[$descriptor];
|
||||
if (file_exists($destination) && !is_writable($destination))
|
||||
return -6;
|
||||
|
||||
if (!copy($tmp_file, $destination))
|
||||
return -7;
|
||||
|
||||
// A little bookkeeping
|
||||
$this->meta['custom_' . $d['width'] . 'x' . $d['height']] = $this->meta[$descriptor];
|
||||
$this->saveMetaData();
|
||||
return 0;
|
||||
}
|
||||
}
|
192
models/Member.php
Normal file
192
models/Member.php
Normal file
@ -0,0 +1,192 @@
|
||||
<?php
|
||||
/*****************************************************************************
|
||||
* Member.php
|
||||
* Contains key class Member, derived from User.
|
||||
*
|
||||
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
||||
*****************************************************************************/
|
||||
|
||||
class Member extends User
|
||||
{
|
||||
private function __construct($data)
|
||||
{
|
||||
foreach ($data as $key => $value)
|
||||
$this->$key = $value;
|
||||
|
||||
$this->is_logged = true;
|
||||
$this->is_guest = false;
|
||||
$this->is_admin = $this->is_admin == 1;
|
||||
}
|
||||
|
||||
public static function fromId($id_user)
|
||||
{
|
||||
$row = Registry::get('db')->queryAssoc('
|
||||
SELECT *
|
||||
FROM users
|
||||
WHERE id_user = {int:id_user}',
|
||||
[
|
||||
'id_user' => $id_user,
|
||||
]);
|
||||
|
||||
// This should never happen.
|
||||
if (empty($row))
|
||||
throw new NotFoundException('Cannot create Member object; user not found in db!');
|
||||
|
||||
return new Member($row);
|
||||
}
|
||||
|
||||
public static function fromSlug($slug)
|
||||
{
|
||||
$row = Registry::get('db')->queryAssoc('
|
||||
SELECT *
|
||||
FROM users
|
||||
WHERE slug = {string:slug}',
|
||||
[
|
||||
'slug' => $slug,
|
||||
]);
|
||||
|
||||
// This shouldn't happen.
|
||||
if (empty($row))
|
||||
throw new NotFoundException('Cannot create Member object; user not found in db!');
|
||||
|
||||
return new Member($row);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new member from the data provided.
|
||||
* @param data
|
||||
*/
|
||||
public static function createNew(array $data)
|
||||
{
|
||||
$error = false;
|
||||
$new_user = [
|
||||
'first_name' => !empty($data['first_name']) ? $data['first_name'] : $error |= true,
|
||||
'surname' => !empty($data['surname']) ? $data['surname'] : $error |= true,
|
||||
'slug' => !empty($data['slug']) ? $data['slug'] : $error |= true,
|
||||
'emailaddress' => !empty($data['emailaddress']) ? $data['emailaddress'] : $error |= true,
|
||||
'password_hash' => !empty($data['password']) ? Authentication::computeHash($data['password']) : $error |= true,
|
||||
'creation_time' => time(),
|
||||
'ip_address' => isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '',
|
||||
'is_admin' => empty($data['is_admin']) ? 0 : 1,
|
||||
];
|
||||
|
||||
if ($error)
|
||||
return false;
|
||||
|
||||
$db = Registry::get('db');
|
||||
$bool = $db->insert('insert', 'users', [
|
||||
'first_name' => 'string-30',
|
||||
'surname' => 'string-60',
|
||||
'slug' => 'string-90',
|
||||
'emailaddress' => 'string-255',
|
||||
'password_hash' => 'string-255',
|
||||
'creation_time' => 'int',
|
||||
'ip_address' => 'string-15',
|
||||
'is_admin' => 'int',
|
||||
], $new_user, ['id_user']);
|
||||
|
||||
if (!$bool)
|
||||
return false;
|
||||
|
||||
$new_user['id_user'] = $db->insert_id();
|
||||
$member = new Member($new_user);
|
||||
|
||||
return $member;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the member using the data provided.
|
||||
* @param data
|
||||
*/
|
||||
public function update(array $new_data)
|
||||
{
|
||||
foreach ($new_data as $key => $value)
|
||||
{
|
||||
if (in_array($key, ['first_name', 'surname', 'slug', 'emailaddress']))
|
||||
$this->$key = $value;
|
||||
elseif ($key === 'password')
|
||||
$this->password_hash = Authentication::computeHash($value);
|
||||
elseif ($key === 'is_admin')
|
||||
$this->is_admin = $value == 1 ? 1 : 0;
|
||||
}
|
||||
|
||||
return Registry::get('db')->query('
|
||||
UPDATE users
|
||||
SET
|
||||
first_name = {string:first_name},
|
||||
surname = {string:surname},
|
||||
slug = {string:slug},
|
||||
emailaddress = {string:emailaddress},
|
||||
password_hash = {string:password_hash},
|
||||
is_admin = {int:is_admin}
|
||||
WHERE id_user = {int:id_user}',
|
||||
get_object_vars($this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the member.
|
||||
* @param data
|
||||
*/
|
||||
public function delete()
|
||||
{
|
||||
return Registry::get('db')->query('
|
||||
DELETE FROM users
|
||||
WHERE id_user = {int:id_user}',
|
||||
['id_user' => $this->id_user]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether an email address is already linked to an account.
|
||||
* @param emailaddress to check
|
||||
* @return false if account does not exist
|
||||
* @return user id if user does exist
|
||||
*/
|
||||
public static function exists($emailaddress)
|
||||
{
|
||||
$res = Registry::get('db')->queryValue('
|
||||
SELECT id_user
|
||||
FROM users
|
||||
WHERE emailaddress = {string:emailaddress}',
|
||||
[
|
||||
'emailaddress' => $emailaddress,
|
||||
]);
|
||||
|
||||
if (empty($res))
|
||||
return false;
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
public function updateAccessTime()
|
||||
{
|
||||
return Registry::get('db')->query('
|
||||
UPDATE users
|
||||
SET
|
||||
last_action_time = {int:now},
|
||||
ip_address = {string:ip}
|
||||
WHERE id_user = {int:id}',
|
||||
[
|
||||
'now' => time(),
|
||||
'id' => $this->id_user,
|
||||
'ip' => isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '',
|
||||
]);
|
||||
}
|
||||
|
||||
public function getUrl()
|
||||
{
|
||||
return BASEURL . '/author/' . $this->slug . '/';
|
||||
}
|
||||
|
||||
public static function getCount()
|
||||
{
|
||||
return Registry::get('db')->queryValue('
|
||||
SELECT COUNT(*)
|
||||
FROM users');
|
||||
}
|
||||
|
||||
public function getProps()
|
||||
{
|
||||
// We should probably phase out the use of this function, or refactor the access levels of member properties...
|
||||
return get_object_vars($this);
|
||||
}
|
||||
}
|
12
models/NotAllowedException.php
Normal file
12
models/NotAllowedException.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
/*****************************************************************************
|
||||
* NotAllowedException.php
|
||||
* Contains exception class NotAllowedException.
|
||||
*
|
||||
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
||||
*****************************************************************************/
|
||||
|
||||
class NotAllowedException extends Exception
|
||||
{
|
||||
|
||||
}
|
12
models/NotFoundException.php
Normal file
12
models/NotFoundException.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
/*****************************************************************************
|
||||
* NotFoundException.php
|
||||
* Contains exception class NotFoundException.
|
||||
*
|
||||
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
||||
*****************************************************************************/
|
||||
|
||||
class NotFoundException extends Exception
|
||||
{
|
||||
|
||||
}
|
189
models/PageIndex.php
Normal file
189
models/PageIndex.php
Normal file
@ -0,0 +1,189 @@
|
||||
<?php
|
||||
/*****************************************************************************
|
||||
* PageIndex.php
|
||||
* Contains key class PageIndex.
|
||||
*
|
||||
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
||||
*****************************************************************************/
|
||||
|
||||
class PageIndex
|
||||
{
|
||||
protected $page_index = [];
|
||||
protected $current_page = 0;
|
||||
protected $items_per_page = 0;
|
||||
protected $needsPageIndex = false;
|
||||
protected $num_pages = 0;
|
||||
protected $recordCount = 0;
|
||||
protected $start = 0;
|
||||
protected $sort_order = null;
|
||||
protected $sort_direction = null;
|
||||
protected $base_url;
|
||||
protected $index_class = 'pagination';
|
||||
protected $page_slug = '%AMP%page=%PAGE%';
|
||||
|
||||
public function __construct($options)
|
||||
{
|
||||
foreach ($options as $key => $value)
|
||||
$this->$key = $value;
|
||||
$this->generatePageIndex();
|
||||
}
|
||||
|
||||
protected function generatePageIndex()
|
||||
{
|
||||
/*
|
||||
Example 1:
|
||||
[1] [2] [3] [...] [c-2] [c-1] [c] [c+1] [c+2] [...] [n-2] [n-1] [n]
|
||||
\---------/ \-------------------------/ \-------------/
|
||||
lower current/contiguous pages upper
|
||||
|
||||
Example 2:
|
||||
[1] [2] [3] [4] [5] [c] [6] [7] [...] [n/2] [...] [n-2] [n-1] [n]
|
||||
\---------/ \-----------------/ \---/ \-------------/
|
||||
lower current/cont. pgs. center upper
|
||||
*/
|
||||
|
||||
$this->num_pages = ceil($this->recordCount / $this->items_per_page);
|
||||
$this->current_page = min(ceil($this->start / $this->items_per_page) + 1, $this->num_pages);
|
||||
if ($this->num_pages == 0)
|
||||
{
|
||||
$this->needsPageIndex = false;
|
||||
return;
|
||||
}
|
||||
|
||||
$lowerLower = 1;
|
||||
$lowerUpper = min($this->num_pages, 3);
|
||||
|
||||
$contigLower = max($lowerUpper + 1, max($this->current_page - 2, 1));
|
||||
$contigUpper = min($this->current_page + 2, $this->num_pages);
|
||||
|
||||
$center = floor($this->num_pages / 2);
|
||||
|
||||
$upperLower = max($contigUpper + 1, max(0, $this->num_pages - 2));
|
||||
$upperUpper = $this->num_pages;
|
||||
|
||||
$this->page_index = [];
|
||||
|
||||
// Lower pages
|
||||
for ($p = $lowerLower; $p <= $lowerUpper; $p++)
|
||||
$this->page_index[$p] = [
|
||||
'index' => $p,
|
||||
'is_selected' => $this->current_page == $p,
|
||||
'href'=> $this->getLink(($p - 1) * $this->items_per_page, $this->sort_order, $this->sort_direction),
|
||||
];
|
||||
|
||||
// The center of the page index.
|
||||
if ($center > $lowerUpper && $center < $contigLower)
|
||||
{
|
||||
// Gap?
|
||||
if ($lowerUpper != $center)
|
||||
$this->page_index[] = '...';
|
||||
|
||||
$this->page_index[$center] = [
|
||||
'index' => $center,
|
||||
'is_selected' => $this->current_page == $center,
|
||||
'href'=> $this->getLink(($center - 1) * $this->items_per_page, $this->sort_order, $this->sort_direction),
|
||||
];
|
||||
}
|
||||
|
||||
// Gap?
|
||||
if ($contigLower != $p)
|
||||
$this->page_index[] = '...';
|
||||
|
||||
// contig pages
|
||||
for ($p = $contigLower; $p <= $contigUpper; $p++)
|
||||
$this->page_index[$p] = [
|
||||
'index' => $p,
|
||||
'is_selected' => $this->current_page == $p,
|
||||
'href'=> $this->getLink(($p - 1) * $this->items_per_page, $this->sort_order, $this->sort_direction),
|
||||
];
|
||||
|
||||
// The center of the page index.
|
||||
if ($center > $contigUpper && $center < $upperLower)
|
||||
{
|
||||
// Gap?
|
||||
if ($contigUpper != $center)
|
||||
$this->page_index[] = '...';
|
||||
|
||||
$this->page_index[$center] = [
|
||||
'index' => $center,
|
||||
'is_selected' => $this->current_page == $center,
|
||||
'href'=> $this->getLink(($center - 1) * $this->items_per_page, $this->sort_order, $this->sort_direction),
|
||||
];
|
||||
}
|
||||
|
||||
// Gap?
|
||||
if ($upperLower != $p)
|
||||
$this->page_index[] = '...';
|
||||
|
||||
// Upper pages
|
||||
for ($p = $upperLower; $p <= $upperUpper; $p++)
|
||||
$this->page_index[$p] = [
|
||||
'index' => $p,
|
||||
'is_selected' => $this->current_page == $p,
|
||||
'href'=> $this->getLink(($p - 1) * $this->items_per_page, $this->sort_order, $this->sort_direction),
|
||||
];
|
||||
|
||||
// Previous page?
|
||||
if ($this->current_page > 1)
|
||||
$this->page_index['previous'] = $this->page_index[$this->current_page - 1];
|
||||
|
||||
// Next page?
|
||||
if ($this->current_page < $this->num_pages)
|
||||
$this->page_index['next'] = $this->page_index[$this->current_page + 1];
|
||||
}
|
||||
|
||||
protected function getLink($start = null, $order = null, $dir = null)
|
||||
{
|
||||
$url = $this->base_url;
|
||||
$amp = strpos($this->base_url, '?') ? '&' : '?';
|
||||
|
||||
if (!empty($start))
|
||||
{
|
||||
$page = ($start / $this->items_per_page) + 1;
|
||||
$url .= strtr($this->page_slug, ['%PAGE%' => $page, '%AMP%' => $amp]);
|
||||
$amp = '&';
|
||||
}
|
||||
if (!empty($order))
|
||||
{
|
||||
$url .= $amp . 'order=' . $order;
|
||||
$amp = '&';
|
||||
}
|
||||
if (!empty($dir))
|
||||
{
|
||||
$url .= $amp . 'dir=' . $dir;
|
||||
$amp = '&';
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
public function getArray()
|
||||
{
|
||||
return $this->page_index;
|
||||
}
|
||||
|
||||
public function getPageIndex()
|
||||
{
|
||||
return $this->page_index;
|
||||
}
|
||||
|
||||
public function getPageIndexClass()
|
||||
{
|
||||
return $this->index_class;
|
||||
}
|
||||
|
||||
public function getCurrentPage()
|
||||
{
|
||||
return $this->current_page;
|
||||
}
|
||||
|
||||
public function getNumberOfPages()
|
||||
{
|
||||
return $this->num_pages;
|
||||
}
|
||||
|
||||
public function getRecordCount()
|
||||
{
|
||||
return $this->recordCount;
|
||||
}
|
||||
}
|
166
models/PhotoMosaic.php
Normal file
166
models/PhotoMosaic.php
Normal file
@ -0,0 +1,166 @@
|
||||
<?php
|
||||
/*****************************************************************************
|
||||
* PhotoMosaic.php
|
||||
* Contains the photo mosaic model, an iterator to create tiled photo galleries.
|
||||
*
|
||||
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
||||
*****************************************************************************/
|
||||
|
||||
class PhotoMosaic
|
||||
{
|
||||
private $queue = [];
|
||||
|
||||
const NUM_DAYS_CUTOFF = 7;
|
||||
|
||||
public function __construct(AssetIterator $iterator)
|
||||
{
|
||||
$this->iterator = $iterator;
|
||||
}
|
||||
|
||||
public function __destruct()
|
||||
{
|
||||
$this->iterator->clean();
|
||||
}
|
||||
|
||||
public static function getRecentPhotos()
|
||||
{
|
||||
return new self(AssetIterator::getByOptions([
|
||||
'tag' => 'photo',
|
||||
'order' => 'date_captured',
|
||||
'direction' => 'desc',
|
||||
'limit' => 15, // worst case: 3 rows * (portrait + 4 thumbs)
|
||||
]));
|
||||
}
|
||||
|
||||
private static function matchTypeMask(Image $image, $type_mask)
|
||||
{
|
||||
return ($type_mask & Image::TYPE_PANORAMA) && $image->isPanorama() ||
|
||||
($type_mask & Image::TYPE_LANDSCAPE) && $image->isLandscape() ||
|
||||
($type_mask & Image::TYPE_PORTRAIT) && $image->isPortrait();
|
||||
}
|
||||
|
||||
private function fetchImage($desired_type = Image::TYPE_PORTRAIT | Image::TYPE_LANDSCAPE | Image::TYPE_PANORAMA, Image $refDateImage = null)
|
||||
{
|
||||
// First, check if we have what we're looking for in the queue.
|
||||
foreach ($this->queue as $i => $image)
|
||||
{
|
||||
// Image has to match the desired type and be taken within a week of the reference image.
|
||||
if (self::matchTypeMask($image, $desired_type) && !(isset($refDateImage) && abs(self::daysApart($image, $refDateImage)) > self::NUM_DAYS_CUTOFF))
|
||||
{
|
||||
unset($this->queue[$i]);
|
||||
return $image;
|
||||
}
|
||||
}
|
||||
|
||||
// Check whatever's next up!
|
||||
while (($asset = $this->iterator->next()) && ($image = $asset->getImage()))
|
||||
{
|
||||
// Image has to match the desired type and be taken within a week of the reference image.
|
||||
if (self::matchTypeMask($image, $desired_type) && !(isset($refDateImage) && abs(self::daysApart($image, $refDateImage)) > self::NUM_DAYS_CUTOFF))
|
||||
return $image;
|
||||
else
|
||||
$this->pushToQueue($image);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function pushToQueue(Image $image)
|
||||
{
|
||||
$this->queue[] = $image;
|
||||
}
|
||||
|
||||
private static function orderPhotos(Image $a, Image $b)
|
||||
{
|
||||
// Show images of highest priority first.
|
||||
$priority_diff = $a->getPriority() - $b->getPriority();
|
||||
if ($priority_diff !== 0)
|
||||
return -$priority_diff;
|
||||
|
||||
// In other cases, we'll just show the newest first.
|
||||
return $a->getDateCaptured() > $b->getDateCaptured() ? -1 : 1;
|
||||
}
|
||||
|
||||
private static function daysApart(Image $a, Image $b)
|
||||
{
|
||||
return $a->getDateCaptured()->diff($b->getDateCaptured())->days;
|
||||
}
|
||||
|
||||
public function getRow()
|
||||
{
|
||||
// Fetch the first image...
|
||||
$image = $this->fetchImage();
|
||||
|
||||
// No image at all?
|
||||
if (!$image)
|
||||
return false;
|
||||
|
||||
// Is it a panorama? Then we've got our row!
|
||||
elseif ($image->isPanorama())
|
||||
return [[$image], 'panorama'];
|
||||
|
||||
// Alright, let's initalise a proper row, then.
|
||||
$photos = [$image];
|
||||
$num_portrait = $image->isPortrait() ? 1 : 0;
|
||||
$num_landscape = $image->isLandscape() ? 1 : 0;
|
||||
|
||||
// Get an initial batch of non-panorama images to work with.
|
||||
for ($i = 1; $i < 3 && ($image = $this->fetchImage(Image::TYPE_LANDSCAPE | Image::TYPE_PORTRAIT, $image)); $i++)
|
||||
{
|
||||
$num_portrait += $image->isPortrait() ? 1 : 0;
|
||||
$num_landscape += $image->isLandscape() ? 1 : 0;
|
||||
$photos[] = $image;
|
||||
}
|
||||
|
||||
// Sort photos by priority and date captured.
|
||||
usort($photos, 'self::orderPhotos');
|
||||
|
||||
// At least one portrait?
|
||||
if ($num_portrait >= 1)
|
||||
{
|
||||
// Grab two more landscapes, so we can put a total of four tiles on the side.
|
||||
for ($i = 0; $image && $i < 2 && ($image = $this->fetchImage(Image::TYPE_LANDSCAPE, $image)); $i++)
|
||||
$photos[] = $image;
|
||||
|
||||
// We prefer to have the portrait on the side, so prepare to process that first.
|
||||
usort($photos, function($a, $b) {
|
||||
if ($a->isPortrait() && !$b->isPortrait())
|
||||
return -1;
|
||||
elseif ($b->isPortrait() && !$a->isPortrait())
|
||||
return 1;
|
||||
else
|
||||
return self::orderPhotos($a, $b);
|
||||
});
|
||||
|
||||
// We might not have a full set of photos, but only bother if we have at least three.
|
||||
if (count($photos) > 3)
|
||||
return [$photos, 'portrait'];
|
||||
}
|
||||
|
||||
// One landscape at least, hopefully?
|
||||
if ($num_landscape >= 1)
|
||||
{
|
||||
if (count($photos) === 3)
|
||||
{
|
||||
// We prefer to have the landscape on the side, so prepare to process that first.
|
||||
usort($photos, function($a, $b) {
|
||||
if ($a->isLandscape() && !$b->isLandscape())
|
||||
return -1;
|
||||
elseif ($b->isLandscape() && !$a->isLandscape())
|
||||
return 1;
|
||||
else
|
||||
return self::orderPhotos($a, $b);
|
||||
});
|
||||
|
||||
return [$photos, 'landscape'];
|
||||
}
|
||||
elseif (count($photos) === 2)
|
||||
return [$photos, 'duo'];
|
||||
else
|
||||
return [$photos, 'single'];
|
||||
}
|
||||
|
||||
// A boring set it is, then.
|
||||
return [$photos, 'row'];
|
||||
}
|
||||
}
|
39
models/Registry.php
Normal file
39
models/Registry.php
Normal file
@ -0,0 +1,39 @@
|
||||
<?php
|
||||
/*****************************************************************************
|
||||
* Registry.php
|
||||
* Allows sharing static variables between classes.
|
||||
*
|
||||
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
||||
*****************************************************************************/
|
||||
|
||||
class Registry
|
||||
{
|
||||
public static $storage = [];
|
||||
|
||||
public static function set($key, $value)
|
||||
{
|
||||
self::$storage[$key] = $value;
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function has($key)
|
||||
{
|
||||
return isset(self::$storage[$key]);
|
||||
}
|
||||
|
||||
public static function get($key)
|
||||
{
|
||||
if (!isset(self::$storage[$key]))
|
||||
trigger_error('Key does not exist in Registry: ' . $key, E_USER_ERROR);
|
||||
|
||||
return self::$storage[$key];
|
||||
}
|
||||
|
||||
public static function remove($key)
|
||||
{
|
||||
if (!isset(self::$storage[$key]))
|
||||
trigger_error('Key does not exist in Registry: ' . $key, E_USER_ERROR);
|
||||
|
||||
unset(self::$storage[$key]);
|
||||
}
|
||||
}
|
87
models/Session.php
Normal file
87
models/Session.php
Normal file
@ -0,0 +1,87 @@
|
||||
<?php
|
||||
/*****************************************************************************
|
||||
* Session.php
|
||||
* Contains the key class Session.
|
||||
*
|
||||
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
||||
*****************************************************************************/
|
||||
|
||||
class Session
|
||||
{
|
||||
public static function start()
|
||||
{
|
||||
session_start();
|
||||
|
||||
// Resuming an existing session? Check what we know!
|
||||
if (isset($_SESSION['user_id'], $_SESSION['ip_address'], $_SESSION['user_agent']))
|
||||
{
|
||||
if (isset($_SERVER['REMOTE_ADDR']) && $_SESSION['ip_address'] !== $_SERVER['REMOTE_ADDR'])
|
||||
{
|
||||
$_SESSION = [];
|
||||
throw new NotAllowedException('Your session failed to validate: your IP address has changed. Please re-login and try again.');
|
||||
}
|
||||
elseif (isset($_SERVER['HTTP_USER_AGENT']) && $_SESSION['user_agent'] !== $_SERVER['HTTP_USER_AGENT'])
|
||||
{
|
||||
$_SESSION = [];
|
||||
throw new NotAllowedException('Your session failed to validate: your browser identifier has changed. Please re-login and try again.');
|
||||
}
|
||||
}
|
||||
elseif (!isset($_SESSION['ip_address'], $_SESSION['user_agent']))
|
||||
$_SESSION = [
|
||||
'ip_address' => isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '',
|
||||
'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '',
|
||||
];
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function resetSessionToken()
|
||||
{
|
||||
$_SESSION['session_token'] = sha1(session_id() . mt_rand());
|
||||
$_SESSION['session_token_key'] = substr(preg_replace('~^\d+~', '', sha1(mt_rand() . session_id() . mt_rand())), 0, rand(7, 12));
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function validateSession($method = 'post')
|
||||
{
|
||||
// First, check whether the submitted token and key match the ones in storage.
|
||||
if (($method === 'post' && (!isset($_POST[$_SESSION['session_token_key']]) || $_POST[$_SESSION['session_token_key']] !== $_SESSION['session_token'])) ||
|
||||
($method === 'get' && (!isset($_GET[$_SESSION['session_token_key']]) || $_GET[$_SESSION['session_token_key']] !== $_SESSION['session_token'])))
|
||||
trigger_error('Session failed to verify (' . $method . '). Please reload the page and try again.', E_USER_ERROR);
|
||||
|
||||
// Check the referring site, too -- should be the same site!
|
||||
$referring_host = isset($_SERVER['HTTP_REFERER']) ? parse_url($_SERVER['HTTP_REFERER'], PHP_URL_HOST) : '';
|
||||
if (!empty($referring_host))
|
||||
{
|
||||
if (strpos($_SERVER['HTTP_HOST'], ':') !== false)
|
||||
$current_host = substr($_SERVER['HTTP_HOST'], 0, strpos($_SERVER['HTTP_HOST'], ':'));
|
||||
else
|
||||
$current_host = $_SERVER['HTTP_HOST'];
|
||||
|
||||
$base_url_host = parse_url(BASEURL, PHP_URL_HOST);
|
||||
|
||||
// The referring_host must match either the base_url_host or the current_host.
|
||||
if (strtolower($referring_host) !== strtolower($base_url_host) && strtolower($referring_host) !== strtolower($current_host))
|
||||
trigger_error('Invalid referring URL. Please reload the page and try again.', E_USER_ERROR);
|
||||
}
|
||||
|
||||
// All looks good from here! But you can only use this token once, so...
|
||||
return self::resetSessionToken();
|
||||
}
|
||||
|
||||
public static function getSessionToken()
|
||||
{
|
||||
if (empty($_SESSION['session_token']))
|
||||
trigger_error('Call to getSessionToken without a session token being set!', E_USER_ERROR);
|
||||
|
||||
return $_SESSION['session_token'];
|
||||
}
|
||||
|
||||
public static function getSessionTokenKey()
|
||||
{
|
||||
if (empty($_SESSION['session_token_key']))
|
||||
trigger_error('Call to getSessionTokenKey without a session token key being set!', E_USER_ERROR);
|
||||
|
||||
return $_SESSION['session_token_key'];
|
||||
}
|
||||
}
|
80
models/Setting.php
Normal file
80
models/Setting.php
Normal file
@ -0,0 +1,80 @@
|
||||
<?php
|
||||
/*****************************************************************************
|
||||
* Setting.php
|
||||
* Contains key class Setting.
|
||||
*
|
||||
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
||||
*****************************************************************************/
|
||||
|
||||
class Setting
|
||||
{
|
||||
public static $cache = [];
|
||||
|
||||
public static function set($key, $value, $id_user = null)
|
||||
{
|
||||
$id_user = Registry::get('user')->getUserId();
|
||||
|
||||
if (isset(self::$cache[$id_user], self::$cache[$id_user][$key]))
|
||||
unset(self::$cache[$id_user][$key]);
|
||||
|
||||
if (Registry::get('db')->query('
|
||||
REPLACE INTO settings
|
||||
(id_user, variable, value, time_set)
|
||||
VALUES
|
||||
({int:id_user}, {string:key}, {string:value}, CURRENT_TIMESTAMP())',
|
||||
[
|
||||
'id_user' => $id_user,
|
||||
'key' => $key,
|
||||
'value' => $value,
|
||||
]))
|
||||
{
|
||||
if (!isset(self::$cache[$id_user]))
|
||||
self::$cache[$id_user] = [];
|
||||
|
||||
self::$cache[$id_user][$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
public static function get($key, $id_user = null)
|
||||
{
|
||||
$id_user = Registry::get('user')->getUserId();
|
||||
|
||||
if (isset(self::$cache[$id_user], self::$cache[$id_user][$key]))
|
||||
return self::$cache[$id_user][$key];
|
||||
|
||||
$value = Registry::get('db')->queryValue('
|
||||
SELECT value
|
||||
FROM settings
|
||||
WHERE id_user = {int:id_user} AND variable = {string:key}',
|
||||
[
|
||||
'id_user' => $id_user,
|
||||
'key' => $key,
|
||||
]);
|
||||
|
||||
if (!$value)
|
||||
return false;
|
||||
|
||||
if (!isset(self::$cache[$id_user]))
|
||||
self::$cache[$id_user] = [];
|
||||
|
||||
self::$cache[$id_user][$key] = $value;
|
||||
return $value;
|
||||
}
|
||||
|
||||
public static function remove($key, $id_user = null)
|
||||
{
|
||||
$id_user = Registry::get('user')->getUserId();
|
||||
|
||||
if (Registry::get('db')->query('
|
||||
DELETE FROM settings
|
||||
WHERE id_user = {int:id_user} AND variable = {string:key}',
|
||||
[
|
||||
'id_user' => $id_user,
|
||||
'key' => $key,
|
||||
]))
|
||||
{
|
||||
if (isset(self::$cache[$id_user], self::$cache[$id_user][$key]))
|
||||
unset(self::$cache[$id_user][$key]);
|
||||
}
|
||||
}
|
||||
}
|
337
models/Tag.php
Normal file
337
models/Tag.php
Normal file
@ -0,0 +1,337 @@
|
||||
<?php
|
||||
/*****************************************************************************
|
||||
* Tag.php
|
||||
* Contains key class Tag.
|
||||
*
|
||||
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
||||
*****************************************************************************/
|
||||
|
||||
class Tag
|
||||
{
|
||||
public $id_tag;
|
||||
public $id_parent;
|
||||
public $id_asset_id;
|
||||
public $tag;
|
||||
public $slug;
|
||||
public $description;
|
||||
public $kind;
|
||||
public $count;
|
||||
|
||||
protected function __construct(array $data)
|
||||
{
|
||||
foreach ($data as $attribute => $value)
|
||||
$this->$attribute = $value;
|
||||
}
|
||||
|
||||
public static function fromId($id_tag, $return_format = 'object')
|
||||
{
|
||||
$db = Registry::get('db');
|
||||
|
||||
$row = $db->queryAssoc('
|
||||
SELECT *
|
||||
FROM tags
|
||||
WHERE id_tag = {int:id_tag}',
|
||||
[
|
||||
'id_tag' => $id_tag,
|
||||
]);
|
||||
|
||||
// Tag not found?
|
||||
if (empty($row))
|
||||
throw new NotFoundException();
|
||||
|
||||
return $return_format == 'object' ? new Tag($row) : $row;
|
||||
}
|
||||
|
||||
public static function fromSlug($slug, $return_format = 'object')
|
||||
{
|
||||
$db = Registry::get('db');
|
||||
|
||||
$row = $db->queryAssoc('
|
||||
SELECT *
|
||||
FROM tags
|
||||
WHERE slug = {string:slug}',
|
||||
[
|
||||
'slug' => $slug,
|
||||
]);
|
||||
|
||||
// Tag not found?
|
||||
if (empty($row))
|
||||
throw new NotFoundException();
|
||||
|
||||
return $return_format == 'object' ? new Tag($row) : $row;
|
||||
}
|
||||
|
||||
public static function getAll($limit = 0, $return_format = 'array')
|
||||
{
|
||||
$rows = Registry::get('db')->queryAssocs('
|
||||
SELECT *
|
||||
FROM tags
|
||||
ORDER BY ' . ($limit > 0 ? 'count
|
||||
LIMIT {int:limit}' : 'tag'),
|
||||
[
|
||||
'limit' => $limit,
|
||||
]);
|
||||
|
||||
// No tags found?
|
||||
if (empty($rows))
|
||||
return [];
|
||||
|
||||
// Limited? Make sure the lot is sorted alphabetically.
|
||||
if (!empty($limit))
|
||||
{
|
||||
usort($rows, function($a, $b) {
|
||||
return strcmp($a['tag'], $b['tag']);
|
||||
});
|
||||
}
|
||||
|
||||
if ($return_format == 'object')
|
||||
{
|
||||
$return = [];
|
||||
foreach ($rows as $row)
|
||||
$return[$row['id_tag']] = new Tag($row);
|
||||
return $return;
|
||||
}
|
||||
else
|
||||
return $rows;
|
||||
}
|
||||
|
||||
public static function getAlbums($id_parent = 0, $return_format = 'array')
|
||||
{
|
||||
$rows = Registry::get('db')->queryAssocs('
|
||||
SELECT *
|
||||
FROM tags
|
||||
WHERE id_parent = {int:id_parent} AND kind = {string:kind}
|
||||
ORDER BY tag ASC',
|
||||
[
|
||||
'id_parent' => $id_parent,
|
||||
'kind' => 'Album',
|
||||
]);
|
||||
|
||||
if ($return_format == 'object')
|
||||
{
|
||||
$return = [];
|
||||
foreach ($rows as $row)
|
||||
$return[$row['id_tag']] = new Tag($row);
|
||||
return $return;
|
||||
}
|
||||
else
|
||||
return $rows;
|
||||
}
|
||||
|
||||
public static function getPeople($id_parent = 0, $return_format = 'array')
|
||||
{
|
||||
$rows = Registry::get('db')->queryAssocs('
|
||||
SELECT *
|
||||
FROM tags
|
||||
WHERE id_parent = {int:id_parent} AND kind = {string:kind}
|
||||
ORDER BY tag ASC',
|
||||
[
|
||||
'id_parent' => $id_parent,
|
||||
'kind' => 'Person',
|
||||
]);
|
||||
|
||||
if ($return_format == 'object')
|
||||
{
|
||||
$return = [];
|
||||
foreach ($rows as $row)
|
||||
$return[$row['id_tag']] = new Tag($row);
|
||||
return $return;
|
||||
}
|
||||
else
|
||||
return $rows;
|
||||
}
|
||||
|
||||
public static function byAssetId($id_asset, $return_format = 'object')
|
||||
{
|
||||
$rows = Registry::get('db')->queryAssocs('
|
||||
SELECT *
|
||||
FROM tags
|
||||
WHERE id_tag IN(
|
||||
SELECT id_tag
|
||||
FROM assets_tags
|
||||
WHERE id_asset = {int:id_asset}
|
||||
)
|
||||
ORDER BY count DESC',
|
||||
[
|
||||
'id_asset' => $id_asset,
|
||||
]);
|
||||
|
||||
// No tags found?
|
||||
if (empty($rows))
|
||||
return [];
|
||||
|
||||
if ($return_format == 'object')
|
||||
{
|
||||
$return = [];
|
||||
foreach ($rows as $row)
|
||||
$return[$row['id_tag']] = new Tag($row);
|
||||
return $return;
|
||||
}
|
||||
else
|
||||
return $rows;
|
||||
}
|
||||
|
||||
public static function byPostId($id_post, $return_format = 'object')
|
||||
{
|
||||
$rows = Registry::get('db')->queryAssocs('
|
||||
SELECT *
|
||||
FROM tags
|
||||
WHERE id_tag IN(
|
||||
SELECT id_tag
|
||||
FROM posts_tags
|
||||
WHERE id_post = {int:id_post}
|
||||
)
|
||||
ORDER BY count DESC',
|
||||
[
|
||||
'id_post' => $id_post,
|
||||
]);
|
||||
|
||||
// No tags found?
|
||||
if (empty($rows))
|
||||
return [];
|
||||
|
||||
if ($return_format == 'object')
|
||||
{
|
||||
$return = [];
|
||||
foreach ($rows as $row)
|
||||
$return[$row['id_tag']] = new Tag($row);
|
||||
return $return;
|
||||
}
|
||||
else
|
||||
return $rows;
|
||||
}
|
||||
|
||||
public static function recount(array $id_tags = [])
|
||||
{
|
||||
return Registry::get('db')->query('
|
||||
UPDATE tags AS t SET count = (
|
||||
SELECT COUNT(*)
|
||||
FROM `assets_tags` AS at
|
||||
WHERE at.id_tag = t.id_tag
|
||||
)' . (!empty($id_tags) ? '
|
||||
WHERE t.id_tag IN({array_int:id_tags})' : ''),
|
||||
['id_tags' => $id_tags]);
|
||||
}
|
||||
|
||||
public static function createNew(array $data, $return_format = 'object')
|
||||
{
|
||||
$db = Registry::get('db');
|
||||
|
||||
if (!isset($data['id_parent']))
|
||||
$data['id_parent'] = 0;
|
||||
|
||||
if (!isset($data['count']))
|
||||
$data['count'] = 0;
|
||||
|
||||
$res = $db->query('
|
||||
INSERT IGNORE INTO tags
|
||||
(id_parent, tag, slug, kind, description, count)
|
||||
VALUES
|
||||
({int:id_parent}, {string:tag}, {string:slug}, {string:kind}, {string:description}, {int:count})
|
||||
ON DUPLICATE KEY UPDATE count = count + 1',
|
||||
$data);
|
||||
|
||||
if (!$res)
|
||||
trigger_error('Could not create the requested tag.', E_USER_ERROR);
|
||||
|
||||
$data['id_tag'] = $db->insert_id();
|
||||
return $return_format == 'object' ? new Tag($data) : $data;
|
||||
}
|
||||
|
||||
public function getUrl()
|
||||
{
|
||||
return BASEURL . '/tag/' . $this->slug . '/';
|
||||
}
|
||||
|
||||
public function save()
|
||||
{
|
||||
return Registry::get('db')->query('
|
||||
UPDATE tags
|
||||
SET
|
||||
tag = {string:tag},
|
||||
count = {int:count}
|
||||
WHERE id_tag = {int:id_tag}',
|
||||
get_object_vars($this));
|
||||
}
|
||||
|
||||
public function delete()
|
||||
{
|
||||
$db = Registry::get('db');
|
||||
|
||||
$res = $db->query('
|
||||
DELETE FROM posts_tags
|
||||
WHERE id_tag = {int:id_tag}',
|
||||
[
|
||||
'id_tag' => $this->id_tag,
|
||||
]);
|
||||
|
||||
if (!$res)
|
||||
return false;
|
||||
|
||||
return $db->query('
|
||||
DELETE FROM tags
|
||||
WHERE id_tag = {int:id_tag}',
|
||||
[
|
||||
'id_tag' => $this->id_tag,
|
||||
]);
|
||||
}
|
||||
|
||||
public static function match($tokens)
|
||||
{
|
||||
if (!is_array($tokens))
|
||||
$tokens = explode(' ', $tokens);
|
||||
|
||||
return Registry::get('db')->queryPair('
|
||||
SELECT id_tag, tag
|
||||
FROM tags
|
||||
WHERE LOWER(tag) LIKE {string:tokens}
|
||||
ORDER BY tag ASC',
|
||||
['tokens' => '%' . strtolower(implode('%', $tokens)) . '%']);
|
||||
}
|
||||
|
||||
public static function exactMatch($tag)
|
||||
{
|
||||
if (!is_string($tag))
|
||||
throw new InvalidArgumentException('Expecting a string!');
|
||||
|
||||
return Registry::get('db')->queryPair('
|
||||
SELECT id_tag, tag
|
||||
FROM tags
|
||||
WHERE tag = {string:tag}',
|
||||
['tag' => $tag]);
|
||||
}
|
||||
|
||||
public static function matchSlug($slug)
|
||||
{
|
||||
if (!is_string($slug))
|
||||
throw new InvalidArgumentException('Expecting a string!');
|
||||
|
||||
return Registry::get('db')->queryValue('
|
||||
SELECT id_tag
|
||||
FROM tags
|
||||
WHERE slug = {string:slug}',
|
||||
['slug' => $slug]);
|
||||
}
|
||||
|
||||
public static function matchAll(array $tags)
|
||||
{
|
||||
return Registry::get('db')->queryPair('
|
||||
SELECT tag, id_tag
|
||||
FROM tags
|
||||
WHERE tag IN ({array_string:tags})',
|
||||
['tags' => $tags]);
|
||||
}
|
||||
|
||||
public static function getCount($only_active = 1)
|
||||
{
|
||||
return $db->queryValue('
|
||||
SELECT COUNT(*)
|
||||
FROM tags' . ($only_active ? '
|
||||
WHERE count > 0' : ''));
|
||||
}
|
||||
|
||||
public function __toString()
|
||||
{
|
||||
return $this->tag;
|
||||
}
|
||||
}
|
98
models/User.php
Normal file
98
models/User.php
Normal file
@ -0,0 +1,98 @@
|
||||
<?php
|
||||
/*****************************************************************************
|
||||
* User.php
|
||||
* Contains key class User, as well as the Guest and Member class derived
|
||||
* from it.
|
||||
*
|
||||
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
||||
*****************************************************************************/
|
||||
|
||||
/**
|
||||
* User model; contains attributes and methods shared by both members and guests.
|
||||
*/
|
||||
abstract class User
|
||||
{
|
||||
protected $id_user;
|
||||
protected $first_name;
|
||||
protected $surname;
|
||||
protected $emailaddress;
|
||||
protected $creation_time;
|
||||
protected $last_action_time;
|
||||
protected $ip_address;
|
||||
protected $is_admin;
|
||||
|
||||
protected $is_logged;
|
||||
protected $is_guest;
|
||||
|
||||
/**
|
||||
* Returns user id.
|
||||
*/
|
||||
public function getUserId()
|
||||
{
|
||||
return $this->id_user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns first name.
|
||||
*/
|
||||
public function getFirstName()
|
||||
{
|
||||
return $this->first_name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns surname.
|
||||
*/
|
||||
public function getSurname()
|
||||
{
|
||||
return $this->surname;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns full name.
|
||||
*/
|
||||
public function getFullName()
|
||||
{
|
||||
return trim($this->first_name . ' ' . $this->surname);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns email address.
|
||||
*/
|
||||
public function getEmailAddress()
|
||||
{
|
||||
return $this->emailaddress;
|
||||
}
|
||||
|
||||
/**
|
||||
* Have a guess!
|
||||
*/
|
||||
public function getIPAddress()
|
||||
{
|
||||
return $this->ip_address;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether user is logged in.
|
||||
*/
|
||||
public function isLoggedIn()
|
||||
{
|
||||
return $this->is_logged;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether user is a guest.
|
||||
*/
|
||||
public function isGuest()
|
||||
{
|
||||
return $this->is_guest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether user is an administrator.
|
||||
*/
|
||||
public function isAdmin()
|
||||
{
|
||||
return $this->is_admin;
|
||||
}
|
||||
}
|
342
public/css/admin.css
Normal file
342
public/css/admin.css
Normal file
@ -0,0 +1,342 @@
|
||||
.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;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
padding: 15px;
|
||||
width: 275px;
|
||||
}
|
||||
#login * {
|
||||
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 {
|
||||
text-align: right;
|
||||
}
|
||||
#login button {
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
|
||||
/* (Tag) autosuggest
|
||||
----------------------*/
|
||||
#new_tag_container {
|
||||
display: block;
|
||||
position: relative;
|
||||
}
|
||||
.autosuggest {
|
||||
background: #fff;
|
||||
border: 1px solid #ccc;
|
||||
position: absolute;
|
||||
top: 29px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.autosuggest li {
|
||||
display: block !important;
|
||||
padding: 3px;
|
||||
}
|
||||
.autosuggest li:hover, .autosuggest li.selected {
|
||||
background: #CFECF7;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
/* Edit user screen
|
||||
---------------------*/
|
||||
.edituser dt {
|
||||
clear: left;
|
||||
float: left;
|
||||
width: 150px;
|
||||
}
|
||||
.edituser dd {
|
||||
float: left;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.edituser form div:last-child {
|
||||
padding: 1em 0 0;
|
||||
}
|
||||
|
||||
|
||||
/* Admin widgets
|
||||
------------------*/
|
||||
.widget {
|
||||
background: #fff;
|
||||
padding: 25px;
|
||||
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
.widget h3 {
|
||||
margin: 0 0 1em;
|
||||
font: 400 18px "Raleway", sans-serif;
|
||||
}
|
||||
.widget p, .errormsg p {
|
||||
margin: 0;
|
||||
}
|
||||
.widget ul {
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
.widget li {
|
||||
line-height: 1.7em;
|
||||
}
|
||||
|
||||
|
||||
/* Edit icon on tiled grids
|
||||
-----------------------------*/
|
||||
.tiled_grid div.landscape, .tiled_grid div.portrait, .tiled_grid div.panorama {
|
||||
position: relative;
|
||||
}
|
||||
.tiled_grid div > a.edit {
|
||||
background: #fff;
|
||||
border-radius: 3px;
|
||||
box-shadow: 1px 1px 2px rgba(0,0,0,0.3);
|
||||
display: none;
|
||||
left: 20px;
|
||||
line-height: 1.5;
|
||||
padding: 5px 10px;
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
}
|
||||
.tiled_grid div:hover > a.edit {
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
/* Crop editor
|
||||
----------------*/
|
||||
#crop_editor {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: #000;
|
||||
z-index: 100;
|
||||
color: #fff;
|
||||
}
|
||||
#crop_editor input {
|
||||
width: 50px;
|
||||
background: #555;
|
||||
color: #fff;
|
||||
}
|
||||
.crop_image_container {
|
||||
position: relative;
|
||||
}
|
||||
.crop_position {
|
||||
padding: 5px;
|
||||
text-align: center;
|
||||
}
|
||||
.crop_position input, .crop_position .btn {
|
||||
margin: 0 5px;
|
||||
}
|
||||
.crop_image_container img {
|
||||
height: auto;
|
||||
width: auto;
|
||||
max-width: 100%;
|
||||
max-height: 700px;
|
||||
}
|
||||
#crop_boundary {
|
||||
border: 1px solid rgba(255, 255, 255, 0.75);
|
||||
background: rgba(255, 255, 255, 0.75);
|
||||
position: absolute;
|
||||
z-index: 200;
|
||||
width: 500px;
|
||||
height: 300px;
|
||||
top: 400px;
|
||||
left: 300px;
|
||||
filter: invert(100%); /* temp */
|
||||
}
|
||||
|
||||
|
||||
/* The pagination styles below are based on Bootstrap 2.3.2
|
||||
-------------------------------------------------------------*/
|
||||
|
||||
.table_pagination, .table_form {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.table_pagination ul {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.table_pagination ul > li {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.table_pagination ul > li > a,
|
||||
.table_pagination ul > li > span {
|
||||
float: left;
|
||||
padding: 4px 12px;
|
||||
line-height: 20px;
|
||||
text-decoration: none;
|
||||
background-color: #ffffff;
|
||||
border: 1px solid #dddddd;
|
||||
border-left-width: 0;
|
||||
}
|
||||
|
||||
.table_pagination ul > li > a:hover,
|
||||
.table_pagination ul > li > a:focus,
|
||||
.table_pagination ul > .active > a,
|
||||
.table_pagination ul > .active > span {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.table_pagination ul > .active > a,
|
||||
.table_pagination ul > .active > span {
|
||||
color: #999999;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.table_pagination ul > .disabled > span,
|
||||
.table_pagination ul > .disabled > a,
|
||||
.table_pagination ul > .disabled > a:hover,
|
||||
.table_pagination ul > .disabled > a:focus {
|
||||
color: #999999;
|
||||
cursor: default;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.table_pagination ul > li:first-child > a,
|
||||
.table_pagination ul > li:first-child > span {
|
||||
border-left-width: 1px;
|
||||
}
|
||||
|
||||
|
||||
/* The table styles below were taken from Bootstrap 2.3.2
|
||||
-----------------------------------------------------------*/
|
||||
table {
|
||||
max-width: 100%;
|
||||
background-color: transparent;
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
.table {
|
||||
width: 100%;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.table th,
|
||||
.table td {
|
||||
border-top: 1px solid #dddddd;
|
||||
line-height: 20px;
|
||||
padding: 8px;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.table th {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.table thead th {
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
.table caption + thead tr:first-child th,
|
||||
.table caption + thead tr:first-child td,
|
||||
.table colgroup + thead tr:first-child th,
|
||||
.table colgroup + thead tr:first-child td,
|
||||
.table thead:first-child tr:first-child th,
|
||||
.table thead:first-child tr:first-child td {
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
.table tbody + tbody {
|
||||
border-top: 2px solid #dddddd;
|
||||
}
|
||||
|
||||
.table .table {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.table-striped tbody > tr:nth-child(odd) > td,
|
||||
.table-striped tbody > tr:nth-child(odd) > th {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.table-hover tbody tr:hover > td,
|
||||
.table-hover tbody tr:hover > th {
|
||||
background-color: #f5f5f5;
|
||||
}
|
422
public/css/default.css
Normal file
422
public/css/default.css
Normal file
@ -0,0 +1,422 @@
|
||||
/**
|
||||
* Styles for the fifth version of Aaronweb.net.
|
||||
* (C) Aaron van Geffen 2013, 2014
|
||||
* 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);
|
||||
|
||||
body {
|
||||
font: 13px/1.7 "Open Sans", "Helvetica", sans-serif;
|
||||
padding: 0 0 3em;
|
||||
margin: 0;
|
||||
background: #99BFCE 0 -50% fixed;
|
||||
background-image: radial-gradient(ellipse at top, #c3dee5 0%,#92b9ca 55%,#365e77 100%); /* W3C */
|
||||
}
|
||||
|
||||
#wrapper, header {
|
||||
width: 95%;
|
||||
min-width: 900px;
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
header {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #487C96;
|
||||
text-decoration: none;
|
||||
}
|
||||
a:hover {
|
||||
color: #222;
|
||||
}
|
||||
|
||||
/* Logo
|
||||
---------*/
|
||||
h1#logo {
|
||||
color: #fff;
|
||||
float: left;
|
||||
font: 200 50px 'Press Start 2P', "Helvetica Neue", sans-serif;
|
||||
margin: 40px 0 50px 10px;
|
||||
padding: 0;
|
||||
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
h1#logo a {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
h1#logo span.name {
|
||||
padding-right: 7px;
|
||||
}
|
||||
h1#logo span.area:before {
|
||||
content: '|';
|
||||
font-weight: 400;
|
||||
letter-spacing: 7px;
|
||||
}
|
||||
|
||||
/* Navigation
|
||||
---------------*/
|
||||
ul#nav {
|
||||
margin: 55px 10px 0 0;
|
||||
padding: 0;
|
||||
float: right;
|
||||
list-style: none;
|
||||
}
|
||||
ul#nav li {
|
||||
float: left;
|
||||
}
|
||||
ul#nav li a {
|
||||
color: #fff;
|
||||
display: block;
|
||||
float: left;
|
||||
font: 200 20px 'Press Start 2P', "Helvetica Neue", 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 {
|
||||
border-bottom: 3px solid rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
|
||||
/* Pagination
|
||||
---------------*/
|
||||
.pagination {
|
||||
clear: both;
|
||||
text-align: center;
|
||||
}
|
||||
.pagination ul {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
.pagination ul > li {
|
||||
display: inline;
|
||||
}
|
||||
.pagination ul > li > a, .pagination ul > li > span {
|
||||
float: left;
|
||||
font: 300 18px/2.2 "Open Sans", "Helvetica", sans-serif;
|
||||
padding: 6px 22px;
|
||||
text-decoration: none;
|
||||
background-color: #fff;
|
||||
border-right: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.pagination ul > li > a:hover, .pagination ul > li > a:focus,
|
||||
.pagination ul > .active > a, .pagination ul > .active > span {
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
.pagination ul > .active > a, .pagination ul > .active > span {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.pagination ul > .disabled > span, .pagination ul > .disabled > a,
|
||||
.pagination ul > .disabled > a:hover, .pagination ul > .disabled > a:focus {
|
||||
color: #999;
|
||||
cursor: default;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
|
||||
/* Tiled grid
|
||||
---------------*/
|
||||
.tiled_header, .page_title_box {
|
||||
background: #fff;
|
||||
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
|
||||
color: #000;
|
||||
float: left;
|
||||
margin: 0 0 1.5% 0;
|
||||
font: 400 18px/2.2 "Open Sans", "Helvetica Neue", sans-serif;
|
||||
padding: 6px 22px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
.page_title_box {
|
||||
padding: 6px 22px !important;
|
||||
clear: both;
|
||||
display: inline-block;
|
||||
float: none;
|
||||
font: inherit;
|
||||
}
|
||||
.page_title_box h2 {
|
||||
font: 400 18px/2.2 "Open Sans", "Helvetica Neue", sans-serif !important;
|
||||
}
|
||||
.page_title_box p {
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
|
||||
.tiled_grid {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
.tiled_grid div.landscape, .tiled_grid div.portrait, .tiled_grid div.panorama,
|
||||
.tiled_grid div.duo, .tiled_grid div.single {
|
||||
background: #fff;
|
||||
border-bottom-style: none !important;
|
||||
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
|
||||
float: left;
|
||||
line-height: 0;
|
||||
margin: 0 3.5% 3.5% 0;
|
||||
overflow: none;
|
||||
position: relative;
|
||||
padding-bottom: 5px;
|
||||
width: 31%;
|
||||
}
|
||||
.tiled_grid div img {
|
||||
border: none;
|
||||
width: 100%;
|
||||
}
|
||||
.tiled_grid div h4 {
|
||||
color: #000;
|
||||
margin: 0;
|
||||
font: 400 18px "Open Sans", "Helvetica Neue", sans-serif;
|
||||
padding: 20px 5px 15px;
|
||||
text-overflow: ellipsis;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
.tiled_grid div a {
|
||||
text-decoration: none;
|
||||
}
|
||||
.tiled_grid div.landscape:hover, .tiled_grid div.portrait:hover, .tiled_grid div.panorama:hover,
|
||||
.tiled_grid div.duo:hover, .tiled_grid div.single:hover {
|
||||
padding-bottom: 0;
|
||||
border-bottom-width: 5px;
|
||||
border-bottom-style: solid !important;
|
||||
}
|
||||
|
||||
/* Panoramas */
|
||||
.tiled_grid div.panorama, .tiled_grid .tiled_row {
|
||||
clear: both;
|
||||
float: left;
|
||||
width: 100%;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
/* Tiling: one landscape, two tiles */
|
||||
.tiled_row .column_landscape {
|
||||
float: left;
|
||||
width: 65.5%;
|
||||
margin: 0 3.5% 3.5% 0;
|
||||
}
|
||||
.tiled_row .column_landscape div {
|
||||
width: auto;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.tiled_row .column_tiles_two {
|
||||
float: left;
|
||||
width: 31%;
|
||||
}
|
||||
.tiled_row .column_tiles_two div {
|
||||
float: left;
|
||||
width: 100%;
|
||||
margin: 0 0 10% 0;
|
||||
}
|
||||
|
||||
/* Tiling: big portrait, four tiles */
|
||||
.tiled_row .column_portrait {
|
||||
float: left;
|
||||
width: 31%;
|
||||
margin: 0 3.5% 3.5% 0;
|
||||
}
|
||||
.tiled_row .column_portrait div {
|
||||
width: auto;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.tiled_row .column_tiles_four {
|
||||
float: left;
|
||||
width: 65.5%;
|
||||
}
|
||||
.tiled_row .column_tiles_four div {
|
||||
float: left;
|
||||
width: 47.45%;
|
||||
margin: 0 5% 5% 0;
|
||||
}
|
||||
|
||||
/* Tiling: two tiles */
|
||||
.tiled_row .duo {
|
||||
width: 48.25% !important;
|
||||
}
|
||||
|
||||
/* Tiling: one tile */
|
||||
.tiled_row .single {
|
||||
width: 48.25% !important;
|
||||
}
|
||||
|
||||
/* Tiling: remove horizontal margin at end of row. */
|
||||
.tiled_row > div:nth-child(3n),
|
||||
.tiled_row > .duo:nth-child(2) {
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
.tiled_row .column_tiles_four > div:nth-child(2n) {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
/* Tiling: switch places for odd rows */
|
||||
.tiled_row:nth-child(odd) .column_landscape,
|
||||
.tiled_row:nth-child(odd) .column_portrait {
|
||||
float: right;
|
||||
margin: 0 0 3.5% 3.5%;
|
||||
}
|
||||
.tiled_row:nth-child(odd) .column_tiles_four {
|
||||
float: right;
|
||||
}
|
||||
.tiled_row:nth-child(odd) .column_tiles_two {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.boxed_content {
|
||||
background: #fff;
|
||||
padding: 25px;
|
||||
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
.boxed_content h2 {
|
||||
font: 300 24px "Open Sans", "Helvetica Neue", sans-serif;
|
||||
margin: 0 0 0.2em;
|
||||
}
|
||||
|
||||
|
||||
/* Error pages
|
||||
----------------*/
|
||||
.errormsg p {
|
||||
font-size: 1.1em;
|
||||
margin: 1em 0 0;
|
||||
}
|
||||
.errorpage .widget {
|
||||
margin-top: 3%;
|
||||
}
|
||||
.errorpage .widget_recentposts {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
|
||||
/* Footer
|
||||
-----------*/
|
||||
footer {
|
||||
clear: both;
|
||||
color: #fff;
|
||||
font: 400 12px/1.7 "Open Sans", "Helvetica Neue", sans-serif;
|
||||
padding: 40px 0 0;
|
||||
text-align: center;
|
||||
overflow: auto;
|
||||
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
footer a {
|
||||
color: #eee;
|
||||
}
|
||||
|
||||
|
||||
/* Input
|
||||
----------*/
|
||||
|
||||
input, select, .btn {
|
||||
background: #fff;
|
||||
border: 1px solid #ccc;
|
||||
color: #000;
|
||||
font: 13px/1.7 "Open Sans", "Helvetica", sans-serif;
|
||||
padding: 3px;
|
||||
}
|
||||
input[type=submit], button, .btn {
|
||||
background: #C5E2EA;
|
||||
border-radius: 3px;
|
||||
border: 1px solid #8BBFCE;
|
||||
display: inline-block;
|
||||
font: inherit;
|
||||
padding: 4px 5px;
|
||||
}
|
||||
input[type=submit]:hover, button:hover, .btn:hover {
|
||||
background-color: #bddce5;
|
||||
border-color: #88b7c6;
|
||||
}
|
||||
textarea {
|
||||
border: 1px solid #ccc;
|
||||
font: 12px/1.4 'Monaco', 'Inconsolata', 'DejaVu Sans Mono', monospace;
|
||||
padding: 0.75%;
|
||||
width: 98.5%;
|
||||
}
|
||||
.btn-red {
|
||||
background: #F3B076;
|
||||
border-color: #C98245;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
|
||||
/* Responsive: smartphone in portrait
|
||||
---------------------------------------*/
|
||||
@media only screen and (max-width: 895px) {
|
||||
#wrapper, header {
|
||||
width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
h1#logo {
|
||||
font-size: 42px;
|
||||
float: none;
|
||||
margin: 1em 0 0.5em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
ul#nav {
|
||||
float: none;
|
||||
padding: 0;
|
||||
margin: 2em 0 2.5em;
|
||||
text-align: center;
|
||||
overflow: none;
|
||||
}
|
||||
|
||||
ul#nav li, ul#nav li a {
|
||||
display: inline;
|
||||
float: none;
|
||||
}
|
||||
|
||||
ul#nav li a {
|
||||
float: none;
|
||||
font-size: 16px;
|
||||
margin-left: 6px;
|
||||
padding: 15px 4px;
|
||||
}
|
||||
|
||||
.grid li {
|
||||
margin: 0 0 5%;
|
||||
width: 47.5%;
|
||||
}
|
||||
.grid li:nth-child(2n) {
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
.grid li:nth-child(2n+1) {
|
||||
margin-right: 5% !important;
|
||||
}
|
||||
|
||||
.tiled_header {
|
||||
font-size: 14px;
|
||||
margin: 0 0 3.5% 0;
|
||||
}
|
||||
.tiled_grid div h4 {
|
||||
font-size: 14px;
|
||||
padding: 15px 5px 10px;
|
||||
}
|
||||
|
||||
.tiled_row > div, .tiled_row .single, .tiled_row .duo {
|
||||
float: none !important;
|
||||
width: 100% !important;
|
||||
margin: 0 0 5% !important;
|
||||
}
|
||||
|
||||
.tiled_row > div > div {
|
||||
float: none !important;
|
||||
width: 100% !important;
|
||||
margin: 0 0 5% !important;
|
||||
}
|
||||
}
|
BIN
public/images/nothumb.png
Normal file
BIN
public/images/nothumb.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.6 KiB |
10
public/index.php
Normal file
10
public/index.php
Normal file
@ -0,0 +1,10 @@
|
||||
<?php
|
||||
/*****************************************************************************
|
||||
* index.php
|
||||
* Bootstraps the framework.
|
||||
*
|
||||
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
||||
*****************************************************************************/
|
||||
|
||||
// Bootstrap the framework.
|
||||
require_once dirname(__DIR__) . '/app.php';
|
38
public/js/ajax.js
Normal file
38
public/js/ajax.js
Normal file
@ -0,0 +1,38 @@
|
||||
function HttpRequest(method, url, payload, callback, context) {
|
||||
if (!window.XMLHttpRequest) {
|
||||
return;
|
||||
}
|
||||
|
||||
var request = new XMLHttpRequest();
|
||||
var async = typeof callback !== 'undefined';
|
||||
|
||||
if (async) {
|
||||
request.onreadystatechange = function() {
|
||||
if (request.readyState !== 4) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (request.responseText !== null && request.status === 200) {
|
||||
var obj = JSON.parse(request.responseText);
|
||||
if (obj.error) {
|
||||
alert(obj.error);
|
||||
return;
|
||||
}
|
||||
else {
|
||||
callback(obj, context);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (method === 'get') {
|
||||
request.open('GET', url, async);
|
||||
request.send(null);
|
||||
} else {
|
||||
request.open('POST', url, async);
|
||||
request.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
|
||||
request.send(payload);
|
||||
}
|
||||
|
||||
return request;
|
||||
};
|
178
public/js/autosuggest.js
Normal file
178
public/js/autosuggest.js
Normal file
@ -0,0 +1,178 @@
|
||||
/*
|
||||
Copyright (c) 2015, Aaron van Geffen
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification, are permitted
|
||||
provided that the following conditions are met:
|
||||
|
||||
- Redistributions of source code must retain the above copyright notice, this list of conditions
|
||||
and the following disclaimer.
|
||||
- Redistributions in binary form must reproduce the above copyright notice, this list of conditions
|
||||
and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
function AutoSuggest(opt) {
|
||||
if (typeof opt.inputElement === "undefined" || typeof opt.listElement === "undefined" || typeof opt.baseUrl === "undefined" || typeof opt.appendCallback === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
this.input = document.getElementById(opt.inputElement);
|
||||
this.input.autocomplete = "off";
|
||||
this.list = document.getElementById(opt.listElement);
|
||||
this.appendCallback = opt.appendCallback;
|
||||
this.baseurl = opt.baseUrl;
|
||||
|
||||
var self = this;
|
||||
this.input.addEventListener('keydown', function(event) {
|
||||
self.doSelection(event);
|
||||
}, false);
|
||||
this.input.addEventListener('keyup', function(event) {
|
||||
self.onType(this, event);
|
||||
}, false);
|
||||
}
|
||||
|
||||
AutoSuggest.prototype.doSelection = function(event) {
|
||||
if (typeof this.container === "undefined" || this.container.children.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (event.keyCode) {
|
||||
case 13: // Enter
|
||||
event.preventDefault();
|
||||
this.container.children[this.selectedIndex].click();
|
||||
break;
|
||||
|
||||
case 38: // Arrow up
|
||||
case 40: // Arrow down
|
||||
event.preventDefault();
|
||||
this.findSelectedElement().className = '';
|
||||
this.selectedIndex += event.keyCode === 38 ? -1 : 1;
|
||||
if (this.selectedIndex < 0) {
|
||||
this.selectedIndex = this.container.children.length - 1;
|
||||
} else if (this.selectedIndex === this.container.children.length) {
|
||||
this.selectedIndex = 0;
|
||||
}
|
||||
var new_el = this.findSelectedElement().className = 'selected';
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
AutoSuggest.prototype.findSelectedElement = function() {
|
||||
return this.container.children[this.selectedIndex];
|
||||
};
|
||||
|
||||
AutoSuggest.prototype.onType = function(input, event) {
|
||||
if (event.keyCode === 13 || event.keyCode === 38 || event.keyCode === 40) {
|
||||
return;
|
||||
}
|
||||
|
||||
var tokens = input.value.split(/\s+/).filter(function(token) {
|
||||
return token.length >= 3;
|
||||
});
|
||||
|
||||
if (tokens.length === 0) {
|
||||
if (typeof this.container !== "undefined") {
|
||||
this.clearContainer();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
var request_uri = this.baseurl + '/suggest/?type=tags&data=' + window.encodeURIComponent(tokens.join(" "));
|
||||
var request = new HttpRequest('get', request_uri, {}, this.onReceive, this);
|
||||
};
|
||||
|
||||
AutoSuggest.prototype.onReceive = function(response, self) {
|
||||
self.openContainer();
|
||||
self.clearContainer();
|
||||
self.fillContainer(response);
|
||||
};
|
||||
|
||||
AutoSuggest.prototype.openContainer = function() {
|
||||
if (this.container) {
|
||||
if (!this.container.parentNode) {
|
||||
this.input.parentNode.appendChild(this.container);
|
||||
}
|
||||
return this.container;
|
||||
}
|
||||
|
||||
this.container = document.createElement('ul');
|
||||
this.container.className = 'autosuggest';
|
||||
this.input.parentNode.appendChild(this.container);
|
||||
return this.container;
|
||||
};
|
||||
|
||||
AutoSuggest.prototype.clearContainer = function() {
|
||||
while (this.container.children.length > 0) {
|
||||
this.container.removeChild(this.container.children[0]);
|
||||
}
|
||||
};
|
||||
|
||||
AutoSuggest.prototype.clearInput = function() {
|
||||
this.input.value = "";
|
||||
this.input.focus();
|
||||
};
|
||||
|
||||
AutoSuggest.prototype.closeContainer = function() {
|
||||
this.container.parentNode.removeChild(this.container);
|
||||
};
|
||||
|
||||
AutoSuggest.prototype.fillContainer = function(response) {
|
||||
var self = this;
|
||||
this.selectedIndex = 0;
|
||||
response.items.forEach(function(item, i) {
|
||||
var node = document.createElement('li');
|
||||
var text = document.createTextNode(item.label);
|
||||
node.jsondata = item;
|
||||
node.addEventListener('click', function(event) {
|
||||
self.appendCallback(this.jsondata);
|
||||
self.closeContainer();
|
||||
self.clearInput();
|
||||
});
|
||||
node.appendChild(text);
|
||||
self.container.appendChild(node);
|
||||
if (self.container.children.length === 1) {
|
||||
node.className = 'selected';
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
function TagAutoSuggest(opt) {
|
||||
AutoSuggest.prototype.constructor.call(this, opt);
|
||||
this.type = "tags";
|
||||
}
|
||||
|
||||
TagAutoSuggest.prototype = Object.create(AutoSuggest.prototype);
|
||||
|
||||
TagAutoSuggest.prototype.constructor = TagAutoSuggest;
|
||||
|
||||
TagAutoSuggest.prototype.fillContainer = function(response) {
|
||||
if (response.items.length > 0) {
|
||||
AutoSuggest.prototype.fillContainer.call(this, response);
|
||||
} else {
|
||||
var node = document.createElement('li')
|
||||
node.innerHTML = "<em>Tag does not exist yet. Create it?</em>";
|
||||
|
||||
var self = this;
|
||||
node.addEventListener('click', function(event) {
|
||||
self.createNewTag(function(response) {
|
||||
console.log('Nieuwe tag!!');
|
||||
console.log(response);
|
||||
self.appendCallback(response);
|
||||
});
|
||||
self.closeContainer();
|
||||
self.clearInput();
|
||||
});
|
||||
|
||||
self.container.appendChild(node);
|
||||
this.selectedIndex = 0;
|
||||
node.className = 'selected';
|
||||
}
|
||||
};
|
||||
|
||||
TagAutoSuggest.prototype.createNewTag = function(callback) {
|
||||
var request_uri = this.baseurl + '/managetags/?create';
|
||||
var request = new HttpRequest('post', request_uri, 'tag=' + encodeURIComponent(this.input.value), callback, this);
|
||||
}
|
218
public/js/crop_editor.js
Normal file
218
public/js/crop_editor.js
Normal file
@ -0,0 +1,218 @@
|
||||
function CropEditor(opt) {
|
||||
this.opt = opt;
|
||||
|
||||
this.edit_crop_button = document.createElement("span");
|
||||
this.edit_crop_button.className = "btn";
|
||||
this.edit_crop_button.innerHTML = "Edit crop";
|
||||
this.edit_crop_button.addEventListener('click', this.show.bind(this));
|
||||
|
||||
this.thumbnail_select = document.getElementById(opt.thumbnail_select_id);
|
||||
this.thumbnail_select.addEventListener('change', this.toggleCropButton.bind(this));
|
||||
this.thumbnail_select.parentNode.insertBefore(this.edit_crop_button, this.thumbnail_select.nextSibling);
|
||||
|
||||
this.toggleCropButton();
|
||||
}
|
||||
|
||||
CropEditor.prototype.buildContainer = function() {
|
||||
this.container = document.createElement("div");
|
||||
this.container.id = "crop_editor";
|
||||
|
||||
this.position = document.createElement("div");
|
||||
this.position.className = "crop_position";
|
||||
this.container.appendChild(this.position);
|
||||
|
||||
var source_x_label = document.createTextNode("Source X:");
|
||||
this.position.appendChild(source_x_label);
|
||||
|
||||
this.source_x = document.createElement("input");
|
||||
this.source_x.addEventListener("keyup", this.positionBoundary.bind(this));
|
||||
this.position.appendChild(this.source_x);
|
||||
|
||||
var source_y_label = document.createTextNode("Source Y:");
|
||||
this.position.appendChild(source_y_label);
|
||||
|
||||
this.source_y = document.createElement("input");
|
||||
this.source_y.addEventListener("keyup", this.positionBoundary.bind(this));
|
||||
this.position.appendChild(this.source_y);
|
||||
|
||||
var crop_width_label = document.createTextNode("Crop width:");
|
||||
this.position.appendChild(crop_width_label);
|
||||
|
||||
this.crop_width = document.createElement("input");
|
||||
this.crop_width.addEventListener("keyup", this.positionBoundary.bind(this));
|
||||
this.position.appendChild(this.crop_width);
|
||||
|
||||
var crop_height_label = document.createTextNode("Crop height:");
|
||||
this.position.appendChild(crop_height_label);
|
||||
|
||||
this.crop_height = document.createElement("input");
|
||||
this.crop_height.addEventListener("keyup", this.positionBoundary.bind(this));
|
||||
this.position.appendChild(this.crop_height);
|
||||
|
||||
this.save_button = document.createElement("span");
|
||||
this.save_button.className = "btn";
|
||||
this.save_button.innerHTML = "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.innerHTML = "Abort";
|
||||
this.abort_button.addEventListener('click', this.hide.bind(this));
|
||||
this.position.appendChild(this.abort_button);
|
||||
|
||||
this.image_container = document.createElement("div");
|
||||
this.image_container.className = "crop_image_container";
|
||||
this.container.appendChild(this.image_container);
|
||||
|
||||
this.crop_boundary = document.createElement("div");
|
||||
this.crop_boundary.id = "crop_boundary";
|
||||
this.image_container.appendChild(this.crop_boundary);
|
||||
|
||||
this.original_image = document.createElement("img");
|
||||
this.original_image.id = "original_image";
|
||||
this.original_image.src = this.opt.original_image_src;
|
||||
this.image_container.appendChild(this.original_image);
|
||||
|
||||
this.parent = document.getElementById(this.opt.editor_container_parent_id);
|
||||
this.parent.appendChild(this.container);
|
||||
};
|
||||
|
||||
CropEditor.prototype.setInputValues = function() {
|
||||
var current = this.thumbnail_select.options[this.thumbnail_select.selectedIndex].dataset;
|
||||
|
||||
if (typeof current.crop_region === "undefined") {
|
||||
var source_ratio = this.original_image.naturalWidth / this.original_image.naturalHeight,
|
||||
crop_ratio = current.crop_width / current.crop_height,
|
||||
min_dim = Math.min(this.original_image.naturalWidth, this.original_image.naturalHeight);
|
||||
|
||||
// Cropping from the centre?
|
||||
if (current.crop_method === "c") {
|
||||
// Crop vertically from the centre, using the entire width.
|
||||
if (source_ratio < crop_ratio) {
|
||||
this.crop_width.value = this.original_image.naturalWidth;
|
||||
this.crop_height.value = Math.ceil(this.original_image.naturalWidth / crop_ratio);
|
||||
this.source_x.value = 0;
|
||||
this.source_y.value = Math.ceil((this.original_image.naturalHeight - this.crop_height.value) / 2);
|
||||
}
|
||||
// Crop horizontally from the centre, using the entire height.
|
||||
else {
|
||||
this.crop_width.value = Math.ceil(current.crop_width * this.original_image.naturalHeight / current.crop_height);
|
||||
this.crop_height.value = this.original_image.naturalHeight;
|
||||
this.source_x.value = Math.ceil((this.original_image.naturalWidth - this.crop_width.value) / 2);
|
||||
this.source_y.value = 0;
|
||||
}
|
||||
}
|
||||
// Cropping a top or bottom slice?
|
||||
else {
|
||||
// Can we actually take a top or bottom slice from the original image?
|
||||
if (source_ratio < crop_ratio) {
|
||||
this.crop_width.value = this.original_image.naturalWidth;
|
||||
this.crop_height.value = Math.floor(this.original_image.naturalHeight / crop_ratio);
|
||||
this.source_x.value = "0";
|
||||
this.source_y.value = current.crop_method.indexOf("t") !== -1 ? "0" : this.original_image.naturalHeight - this.crop_height.value;
|
||||
}
|
||||
// Otherwise, take a vertical slice from the centre.
|
||||
else {
|
||||
this.crop_width.value = Math.floor(this.original_image.naturalHeight * crop_ratio);
|
||||
this.crop_height.value = this.original_image.naturalHeight;
|
||||
this.source_x.value = Math.floor((this.original_image.naturalWidth - this.crop_width.value) / 2);
|
||||
this.source_y.value = "0";
|
||||
}
|
||||
}
|
||||
} else {
|
||||
var region = current.crop_region.split(',');
|
||||
this.crop_width.value = region[0];
|
||||
this.crop_height.value = region[1];
|
||||
this.source_x.value = region[2];
|
||||
this.source_y.value = region[3];
|
||||
}
|
||||
};
|
||||
|
||||
CropEditor.prototype.showContainer = function() {
|
||||
this.container.style.display = "block";
|
||||
this.setInputValues();
|
||||
this.positionBoundary();
|
||||
}
|
||||
|
||||
CropEditor.prototype.save = function() {
|
||||
var current = this.thumbnail_select.options[this.thumbnail_select.selectedIndex].dataset;
|
||||
var payload = {
|
||||
thumb_width: current.crop_width,
|
||||
thumb_height: current.crop_height,
|
||||
crop_method: current.crop_method,
|
||||
crop_width: this.crop_width.value,
|
||||
crop_height: this.crop_height.value,
|
||||
source_x: this.source_x.value,
|
||||
source_y: this.source_y.value
|
||||
};
|
||||
var req = HttpRequest("post", this.parent.action + "?id=" + this.opt.asset_id + "&updatethumb",
|
||||
"data=" + encodeURIComponent(JSON.stringify(payload)), function(response) {
|
||||
this.opt.after_save(response);
|
||||
this.hide();
|
||||
}.bind(this));
|
||||
};
|
||||
|
||||
CropEditor.prototype.show = function() {
|
||||
if (typeof this.container === "undefined") {
|
||||
this.buildContainer();
|
||||
}
|
||||
|
||||
// Defer showing and positioning until image is loaded.
|
||||
// !!! TODO: add a spinner in the mean time?
|
||||
if (this.original_image.naturalWidth > 0) {
|
||||
this.showContainer();
|
||||
} else {
|
||||
this.original_image.addEventListener("load", function() {
|
||||
this.showContainer();
|
||||
}.bind(this));
|
||||
}
|
||||
};
|
||||
|
||||
CropEditor.prototype.hide = function() {
|
||||
this.container.style.display = "none";
|
||||
};
|
||||
|
||||
CropEditor.prototype.addEvents = function(event) {
|
||||
var drag_target = document.getElementById(opt.drag_target);
|
||||
drag_target.addEventListener('dragstart', this.dragStart);
|
||||
drag_target.addEventListener('drag', this.drag);
|
||||
drag_target.addEventListener('dragend', this.dragEnd);
|
||||
};
|
||||
|
||||
CropEditor.prototype.dragStart = function(event) {
|
||||
console.log(event);
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
CropEditor.prototype.dragEnd = function(event) {
|
||||
console.log(event);
|
||||
};
|
||||
|
||||
CropEditor.prototype.drag = function(event) {
|
||||
console.log(event);
|
||||
};
|
||||
|
||||
CropEditor.prototype.toggleCropButton = function() {
|
||||
var current = this.thumbnail_select.options[this.thumbnail_select.selectedIndex].dataset;
|
||||
this.edit_crop_button.style.display = typeof current.crop_method === "undefined" ? "none" : "";
|
||||
};
|
||||
|
||||
CropEditor.prototype.positionBoundary = function(event) {
|
||||
var source_x = parseInt(this.source_x.value),
|
||||
source_y = parseInt(this.source_y.value),
|
||||
crop_width = parseInt(this.crop_width.value),
|
||||
crop_height = parseInt(this.crop_height.value),
|
||||
real_width = this.original_image.naturalWidth,
|
||||
real_height = this.original_image.naturalHeight,
|
||||
scaled_width = this.original_image.clientWidth,
|
||||
scaled_height = this.original_image.clientHeight;
|
||||
|
||||
var width_scale = scaled_width / real_width,
|
||||
height_scale = scaled_height / real_height;
|
||||
|
||||
crop_boundary.style.left = (this.source_x.value) * width_scale + "px";
|
||||
crop_boundary.style.top = (this.source_y.value) * height_scale + "px";
|
||||
crop_boundary.style.width = (this.crop_width.value) * width_scale + "px";
|
||||
crop_boundary.style.height = (this.crop_height.value) * height_scale + "px";
|
||||
};
|
3
public/robots.txt
Normal file
3
public/robots.txt
Normal file
@ -0,0 +1,3 @@
|
||||
User-agent: *
|
||||
Disallow: /login
|
||||
|
36
templates/AdminBar.php
Normal file
36
templates/AdminBar.php
Normal file
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
/*****************************************************************************
|
||||
* AdminBar.php
|
||||
* Defines the AdminBar class.
|
||||
*
|
||||
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
||||
*****************************************************************************/
|
||||
|
||||
class AdminBar extends SubTemplate
|
||||
{
|
||||
private $extra_items = [];
|
||||
|
||||
protected function html_content()
|
||||
{
|
||||
echo '
|
||||
<div id="admin_bar">
|
||||
<ul>
|
||||
<li><a href="', BASEURL, '/managetags/">Tags</a></li>
|
||||
<li><a href="', BASEURL, '/manageusers/">Users</a></li>
|
||||
<li><a href="', BASEURL, '/manageerrors/">Errors [', ErrorLog::getCount(), ']</a></li>';
|
||||
|
||||
foreach ($this->extra_items as $item)
|
||||
echo '
|
||||
<li><a href="', $item[0], '">', $item[1], '</a></li>';
|
||||
|
||||
echo '
|
||||
<li><a href="', BASEURL, '/logout/">Log out [', Registry::get('user')->getFullName(), ']</a></li>
|
||||
</ul>
|
||||
</div>';
|
||||
}
|
||||
|
||||
public function appendItem($url, $caption)
|
||||
{
|
||||
$this->extra_items[] = [$url, $caption];
|
||||
}
|
||||
}
|
71
templates/AlbumIndex.php
Normal file
71
templates/AlbumIndex.php
Normal file
@ -0,0 +1,71 @@
|
||||
<?php
|
||||
/*****************************************************************************
|
||||
* AlbumIndex.php
|
||||
* Contains the album index template.
|
||||
*
|
||||
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
||||
*****************************************************************************/
|
||||
|
||||
class AlbumIndex extends SubTemplate
|
||||
{
|
||||
protected $albums;
|
||||
protected $show_edit_buttons;
|
||||
protected $show_labels;
|
||||
protected $row_limit = 1000;
|
||||
|
||||
const TILE_WIDTH = 400;
|
||||
const TILE_HEIGHT = 267;
|
||||
|
||||
public function __construct(array $albums, $show_edit_buttons = false, $show_labels = true)
|
||||
{
|
||||
$this->albums = $albums;
|
||||
$this->show_edit_buttons = $show_edit_buttons;
|
||||
$this->show_labels = $show_labels;
|
||||
}
|
||||
|
||||
protected function html_content()
|
||||
{
|
||||
echo '
|
||||
<div class="tiled_grid">';
|
||||
|
||||
foreach (array_chunk($this->albums, 3) as $photos)
|
||||
{
|
||||
echo '
|
||||
<div class="tiled_row">';
|
||||
|
||||
foreach ($photos as $album)
|
||||
{
|
||||
echo '
|
||||
<div class="landscape">';
|
||||
|
||||
if ($this->show_edit_buttons)
|
||||
echo '
|
||||
<a class="edit" href="#">Edit</a>';
|
||||
|
||||
echo '
|
||||
<a href="', $album['link'], '">';
|
||||
|
||||
if (isset($album['thumbnail']))
|
||||
echo '
|
||||
<img src="', $album['thumbnail']->getThumbnailUrl(static::TILE_WIDTH, static::TILE_HEIGHT, true, true), '" alt="">';
|
||||
else
|
||||
echo '
|
||||
<img src="', BASEURL, '/images/nothumb.png" alt="">';
|
||||
|
||||
if ($this->show_labels)
|
||||
echo '
|
||||
<h4>', $album['caption'], '</h4>';
|
||||
|
||||
echo '
|
||||
</a>
|
||||
</div>';
|
||||
}
|
||||
|
||||
echo '
|
||||
</div>';
|
||||
}
|
||||
|
||||
echo '
|
||||
</div>';
|
||||
}
|
||||
}
|
31
templates/DummyBox.php
Normal file
31
templates/DummyBox.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
/*****************************************************************************
|
||||
* DummyBox.php
|
||||
* Defines the key template DummyBox.
|
||||
*
|
||||
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
||||
*****************************************************************************/
|
||||
|
||||
class DummyBox extends SubTemplate
|
||||
{
|
||||
public function __construct($title = '', $content = '', $class = '')
|
||||
{
|
||||
$this->_title = $title;
|
||||
$this->_content = $content;
|
||||
$this->_class = $class;
|
||||
}
|
||||
|
||||
protected function html_content()
|
||||
{
|
||||
echo '
|
||||
<div class="boxed_content', $this->_class ? ' ' . $this->_class : '', '">', $this->_title ? '
|
||||
<h2>' . $this->_title . '</h2>' : '', '
|
||||
', $this->_content;
|
||||
|
||||
foreach ($this->_subtemplates as $template)
|
||||
$template->html_main();
|
||||
|
||||
echo '
|
||||
</div>';
|
||||
}
|
||||
}
|
292
templates/EditAssetForm.php
Normal file
292
templates/EditAssetForm.php
Normal file
@ -0,0 +1,292 @@
|
||||
<?php
|
||||
/*****************************************************************************
|
||||
* EditAssetForm.php
|
||||
* Contains the edit asset template.
|
||||
*
|
||||
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
||||
*****************************************************************************/
|
||||
|
||||
class EditAssetForm extends SubTemplate
|
||||
{
|
||||
private $asset;
|
||||
private $thumbs;
|
||||
|
||||
public function __construct(Asset $asset, array $thumbs = [])
|
||||
{
|
||||
$this->asset = $asset;
|
||||
$this->thumbs = $thumbs;
|
||||
}
|
||||
|
||||
protected function html_content()
|
||||
{
|
||||
echo '
|
||||
<form id="asset_form" action="" method="post" enctype="multipart/form-data">
|
||||
<div class="boxed_content" style="margin-bottom: 2%">
|
||||
<div style="float: right">
|
||||
<a class="btn btn-red" href="', BASEURL, '/editasset/?id=', $this->asset->getId(), '&delete">Delete asset</a>
|
||||
<input type="submit" value="Save asset data">
|
||||
</div>
|
||||
<h2>Edit asset \'', $this->asset->getTitle(), '\' (', $this->asset->getFilename(), ')</h2>
|
||||
</div>';
|
||||
|
||||
$this->section_replace();
|
||||
|
||||
echo '
|
||||
<div style="float: left; width: 60%; margin-right: 2%">';
|
||||
|
||||
$this->section_key_info();
|
||||
$this->section_asset_meta();
|
||||
|
||||
echo '
|
||||
</div>
|
||||
<div style="float: left; width: 38%;">';
|
||||
|
||||
if (!empty($this->thumbs))
|
||||
$this->section_thumbnails();
|
||||
|
||||
$this->section_linked_tags();
|
||||
|
||||
echo '
|
||||
</div>';
|
||||
|
||||
$this->section_crop_editor();
|
||||
|
||||
echo '
|
||||
</form>';
|
||||
}
|
||||
|
||||
protected function section_key_info()
|
||||
{
|
||||
$date_captured = $this->asset->getDateCaptured();
|
||||
echo '
|
||||
<div class="widget key_info">
|
||||
<h3>Key info</h3>
|
||||
<dl>
|
||||
<dt>Title</dt>
|
||||
<dd><input type="text" name="title" maxlength="255" size="70" value="', $this->asset->getTitle(), '">
|
||||
|
||||
<dt>Date captured</dt>
|
||||
<dd><input type="text" name="date_captured" size="30" value="',
|
||||
$date_captured ? $date_captured->format('Y-m-d H:i:s') : '', '" placeholder="Y-m-d H:i:s">
|
||||
|
||||
<dt>Display priority</dt>
|
||||
<dd><input type="number" name="priority" min="0" max="100" step="1" value="', $this->asset->getPriority(), '">
|
||||
</dl>
|
||||
</div>';
|
||||
}
|
||||
|
||||
protected function section_linked_tags()
|
||||
{
|
||||
echo '
|
||||
<div class="widget linked_tags" style="margin-top: 2%">
|
||||
<h3>Linked tags</h3>
|
||||
<ul id="tag_list">';
|
||||
|
||||
foreach ($this->asset->getTags() as $tag)
|
||||
echo '
|
||||
<li>
|
||||
<input class="tag_check" type="checkbox" name="tag[', $tag->id_tag, ']" id="linked_tag_', $tag->id_tag, '" title="Uncheck to delete" checked>
|
||||
', $tag->tag, '
|
||||
</li>';
|
||||
|
||||
echo '
|
||||
<li id="new_tag_container"><input type="text" id="new_tag" placeholder="Type to link a new tag"></li>
|
||||
</ul>
|
||||
</div>
|
||||
<script type="text/javascript" src="', BASEURL, '/js/ajax.js"></script>
|
||||
<script type="text/javascript" src="', BASEURL, '/js/autosuggest.js"></script>
|
||||
<script type="text/javascript">
|
||||
setTimeout(function() {
|
||||
var tag_autosuggest = new TagAutoSuggest({
|
||||
inputElement: "new_tag",
|
||||
listElement: "tag_list",
|
||||
baseUrl: "', BASEURL, '",
|
||||
appendCallback: function(item) {
|
||||
if (document.getElementById("linked_tag_" + item.id_tag)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var newCheck = document.createElement("input");
|
||||
newCheck.type = "checkbox";
|
||||
newCheck.name = "tag[" + item.id_tag + "]";
|
||||
newCheck.id = "linked_tag_" + item.id_tag;
|
||||
newCheck.title = "Uncheck to delete";
|
||||
newCheck.checked = "checked";
|
||||
|
||||
var newNode = document.createElement("li");
|
||||
newNode.appendChild(newCheck);
|
||||
|
||||
var newLabel = document.createTextNode(item.label);
|
||||
newNode.appendChild(newLabel);
|
||||
|
||||
var list = document.getElementById("tag_list");
|
||||
var input = document.getElementById("new_tag_container");
|
||||
list.insertBefore(newNode, input);
|
||||
}
|
||||
});
|
||||
}, 100);
|
||||
</script>';
|
||||
}
|
||||
|
||||
protected function section_thumbnails()
|
||||
{
|
||||
echo '
|
||||
<div class="widget linked_thumbs">
|
||||
<h3>Thumbnails</h3>
|
||||
View: <select id="thumbnail_src">';
|
||||
|
||||
foreach ($this->thumbs as $thumb)
|
||||
{
|
||||
if (!$thumb['status'])
|
||||
continue;
|
||||
|
||||
echo '
|
||||
<option data-url="', $thumb['url'], '" data-crop_width="', $thumb['dimensions'][0], '" data-crop_height="', $thumb['dimensions'][1], '"',
|
||||
isset($thumb['crop_method']) ? ' data-crop_method="' . $thumb['crop_method'] . '"' : '',
|
||||
isset($thumb['crop_region']) ? ' data-crop_region="' . $thumb['crop_region'] . '"' : '', '>
|
||||
', implode('x', $thumb['dimensions']);
|
||||
|
||||
if ($thumb['cropped'])
|
||||
{
|
||||
echo ' (';
|
||||
switch ($thumb['crop_method'])
|
||||
{
|
||||
case 'b': echo 'bottom'; break;
|
||||
case 'e': echo 'exact'; break;
|
||||
case 's': echo 'slice'; break;
|
||||
case 't': echo 'top'; break;
|
||||
default: echo 'centre'; break;
|
||||
}
|
||||
echo ' crop)';
|
||||
}
|
||||
elseif ($thumb['custom_image'])
|
||||
echo ' (custom)';
|
||||
|
||||
echo '
|
||||
</option>';
|
||||
}
|
||||
|
||||
echo '
|
||||
</select>
|
||||
<a id="thumbnail_link" href="', $this->thumbs[0]['url'], '" target="_blank">
|
||||
<img id="thumbnail" src="', $this->thumbs[0]['url'], '" alt="Thumbnail" style="width: 100%; height: auto;">
|
||||
</a>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
setTimeout(function() {
|
||||
document.getElementById("thumbnail_src").addEventListener("change", function(event) {
|
||||
var selection = event.target.options[event.target.selectedIndex];
|
||||
document.getElementById("thumbnail_link").href = selection.dataset.url;
|
||||
document.getElementById("thumbnail").src = selection.dataset.url;
|
||||
});
|
||||
}, 100);
|
||||
</script>';
|
||||
}
|
||||
|
||||
protected function section_crop_editor()
|
||||
{
|
||||
if (!$this->asset->isImage())
|
||||
return;
|
||||
|
||||
echo '
|
||||
<script type="text/javascript" src="', BASEURL, '/js/crop_editor.js"></script>
|
||||
<script type="text/javascript">
|
||||
setTimeout(function() {
|
||||
var editor = new CropEditor({
|
||||
original_image_src: "', $this->asset->getUrl(), '",
|
||||
editor_container_parent_id: "asset_form",
|
||||
thumbnail_select_id: "thumbnail_src",
|
||||
drag_target: "drag_target",
|
||||
asset_id: ', $this->asset->getId(), ',
|
||||
after_save: function(data) {
|
||||
// Update thumbnail
|
||||
document.getElementById("thumbnail").src = data.url + "?" + (new Date()).getTime();
|
||||
|
||||
// Update select
|
||||
var src = document.getElementById("thumbnail_src");
|
||||
src.options[src.selectedIndex].dataset.crop_region = data.value;
|
||||
|
||||
// TODO: update meta
|
||||
}
|
||||
});
|
||||
}, 100);
|
||||
</script>';
|
||||
}
|
||||
|
||||
protected function section_asset_meta()
|
||||
{
|
||||
echo '
|
||||
<div class="widget asset_meta" style="margin-top: 2%">
|
||||
<h3>Asset meta data</h3>
|
||||
<ul>';
|
||||
|
||||
$i = -1;
|
||||
foreach ($this->asset->getMeta() as $key => $meta)
|
||||
{
|
||||
$i++;
|
||||
echo '
|
||||
<li>
|
||||
<input type="text" name="meta_key[', $i, ']" value="', htmlentities($key), '">
|
||||
<input type="text" name="meta_value[', $i, ']" value="', htmlentities($meta), '">
|
||||
</li>';
|
||||
}
|
||||
|
||||
echo '
|
||||
<li>
|
||||
<input type="text" name="meta_key[', $i + 1, ']" value="">
|
||||
<input type="text" name="meta_value[', $i + 1, ']" value="">
|
||||
</li>
|
||||
</ul>
|
||||
<p><input type="submit" value="Save metadata"></p>
|
||||
</div>';
|
||||
}
|
||||
|
||||
protected function section_replace()
|
||||
{
|
||||
echo '
|
||||
<div class="widget replace_asset" style="margin-bottom: 2%; display: block">
|
||||
<h3>Replace asset</h3>
|
||||
File: <input type="file" name="replacement">
|
||||
Target: <select name="replacement_target">
|
||||
<option value="full">master file</option>';
|
||||
|
||||
foreach ($this->thumbs as $thumb)
|
||||
{
|
||||
if (!$thumb['status'])
|
||||
continue;
|
||||
|
||||
echo '
|
||||
<option value="thumb_', implode('x', $thumb['dimensions']);
|
||||
|
||||
if ($thumb['cropped'])
|
||||
echo $thumb['crop_method'] === 'c' ? '_c' : '_c' . $thumb['crop_method'];
|
||||
|
||||
echo '">
|
||||
thumbnail (', implode('x', $thumb['dimensions']);
|
||||
|
||||
if ($thumb['cropped'])
|
||||
{
|
||||
echo ', ';
|
||||
switch ($thumb['crop_method'])
|
||||
{
|
||||
case 'b': echo 'bottom'; break;
|
||||
case 'e': echo 'exact'; break;
|
||||
case 's': echo 'slice'; break;
|
||||
case 't': echo 'top'; break;
|
||||
default: echo 'centre'; break;
|
||||
}
|
||||
echo ' crop';
|
||||
}
|
||||
elseif ($thumb['custom_image'])
|
||||
echo ' (custom)';
|
||||
|
||||
echo ')
|
||||
</option>';
|
||||
}
|
||||
|
||||
echo '
|
||||
</select>
|
||||
<input type="submit" value="Save asset">
|
||||
</div>';
|
||||
}
|
||||
}
|
161
templates/FormView.php
Normal file
161
templates/FormView.php
Normal file
@ -0,0 +1,161 @@
|
||||
<?php
|
||||
/*****************************************************************************
|
||||
* FormView.php
|
||||
* Contains the form template.
|
||||
*
|
||||
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
||||
*****************************************************************************/
|
||||
|
||||
class FormView extends SubTemplate
|
||||
{
|
||||
public function __construct(Form $form, $title = '')
|
||||
{
|
||||
$this->title = $title;
|
||||
$this->request_url = $form->request_url;
|
||||
$this->request_method = $form->request_method;
|
||||
$this->fields = $form->getFields();
|
||||
$this->missing = $form->getMissing();
|
||||
$this->data = $form->getData();
|
||||
$this->content_above = $form->content_above;
|
||||
$this->content_below = $form->content_below;
|
||||
}
|
||||
|
||||
protected function html_content($exclude = [], $include = [])
|
||||
{
|
||||
if (!empty($this->title))
|
||||
echo '
|
||||
<div id="journal_title">
|
||||
<h3>', $this->title, '</h3>
|
||||
</div>
|
||||
<div id="inner">';
|
||||
|
||||
foreach ($this->_subtemplates as $template)
|
||||
$template->html_main();
|
||||
|
||||
echo '
|
||||
<form action="', $this->request_url, '" method="', $this->request_method, '" enctype="multipart/form-data">';
|
||||
|
||||
if (isset($this->content_above))
|
||||
echo $this->content_above;
|
||||
|
||||
echo '
|
||||
<dl>';
|
||||
|
||||
foreach ($this->fields as $field_id => $field)
|
||||
{
|
||||
// Either we have a blacklist
|
||||
if (!empty($exclude) && in_array($field_id, $exclude))
|
||||
continue;
|
||||
// ... or a whitelist
|
||||
elseif (!empty($include) && !in_array($field_id, $include))
|
||||
continue;
|
||||
// ... or neither (ha)
|
||||
|
||||
$this->renderField($field_id, $field);
|
||||
}
|
||||
|
||||
echo '
|
||||
</dl>
|
||||
<input type="hidden" name="', Session::getSessionTokenKey(), '" value="', Session::getSessionToken(), '">
|
||||
<div style="clear: both">
|
||||
<button type="submit" class="btn btn-primary">Save information</button>';
|
||||
|
||||
if (isset($this->content_below))
|
||||
echo '
|
||||
', $this->content_below;
|
||||
|
||||
echo '
|
||||
</div>
|
||||
</form>';
|
||||
|
||||
if (!empty($this->title))
|
||||
echo '
|
||||
</div>';
|
||||
}
|
||||
|
||||
protected function renderField($field_id, $field)
|
||||
{
|
||||
if (isset($field['before_html']))
|
||||
echo '</dl>
|
||||
', $field['before_html'], '
|
||||
<dl>';
|
||||
|
||||
if ($field['type'] != 'checkbox' && isset($field['label']))
|
||||
echo '
|
||||
<dt class="cont_', $field_id, isset($field['tab_class']) ? ' target target-' . $field['tab_class'] : '', '"', in_array($field_id, $this->missing) ? ' style="color: red"' : '', '>', $field['label'], '</dt>';
|
||||
elseif ($field['type'] == 'checkbox' && isset($field['header']))
|
||||
echo '
|
||||
<dt class="cont_', $field_id, isset($field['tab_class']) ? ' target target-' . $field['tab_class'] : '', '"', in_array($field_id, $this->missing) ? ' style="color: red"' : '', '>', $field['header'], '</dt>';
|
||||
|
||||
echo '
|
||||
<dd class="cont_', $field_id, isset($field['dd_class']) ? ' ' . $field['dd_class'] : '', isset($field['tab_class']) ? ' target target-' . $field['tab_class'] : '', '">';
|
||||
|
||||
if (isset($field['before']))
|
||||
echo $field['before'];
|
||||
|
||||
switch ($field['type'])
|
||||
{
|
||||
case 'select':
|
||||
echo '
|
||||
<select name="', $field_id, '" id="', $field_id, '"', !empty($field['disabled']) ? ' disabled' : '', '>';
|
||||
|
||||
if (isset($field['placeholder']))
|
||||
echo '
|
||||
<option value="">', $field['placeholder'], '</option>';
|
||||
|
||||
foreach ($field['options'] as $value => $option)
|
||||
echo '
|
||||
<option value="', $value, '"', $this->data[$field_id] == $value ? ' selected' : '', '>', htmlentities($option), '</option>';
|
||||
|
||||
echo '
|
||||
</select>';
|
||||
break;
|
||||
|
||||
case 'radio':
|
||||
foreach ($field['options'] as $value => $option)
|
||||
echo '
|
||||
<input type="radio" name="', $field_id, '" value="', $value, '"', $this->data[$field_id] == $value ? ' checked' : '', !empty($field['disabled']) ? ' disabled' : '', '> ', htmlentities($option);
|
||||
break;
|
||||
|
||||
case 'checkbox':
|
||||
echo '
|
||||
<label><input type="checkbox"', $this->data[$field_id] ? ' checked' : '', !empty($field['disabled']) ? ' disabled' : '', ' name="', $field_id, '"> ', htmlentities($field['label']), '</label>';
|
||||
break;
|
||||
|
||||
case 'textarea':
|
||||
echo '
|
||||
<textarea name="', $field_id, '" id="', $field_id, '" cols="', isset($field['columns']) ? $field['columns'] : 40, '" rows="', isset($field['rows']) ? $field['rows'] : 4, '"', !empty($field['disabled']) ? ' disabled' : '', '>', $this->data[$field_id], '</textarea>';
|
||||
break;
|
||||
|
||||
case 'color':
|
||||
echo '
|
||||
<input type="color" name="', $field_id, '" id="', $field_id, '" value="', htmlentities($this->data[$field_id]), '"', !empty($field['disabled']) ? ' disabled' : '', '>';
|
||||
break;
|
||||
|
||||
case 'numeric':
|
||||
echo '
|
||||
<input type="number"', isset($field['step']) ? ' step="' . $field['step'] . '"' : '', '" min="', isset($field['min_value']) ? $field['min_value'] : '0', '" max="', isset($field['max_value']) ? $field['max_value'] : '9999', '" name="', $field_id, '" id="', $field_id, '"', isset($field['size']) ? ' size="' . $field['size'] . '"' : '', isset($field['maxlength']) ? ' maxlength="' . $field['maxlength'] . '"' : '', ' value="', htmlentities($this->data[$field_id]), '"', !empty($field['disabled']) ? ' disabled' : '', '>';
|
||||
break;
|
||||
|
||||
case 'file':
|
||||
if (!empty($this->data[$field_id]))
|
||||
echo '<img src="', $this->data[$field_id], '" alt=""><br>';
|
||||
|
||||
echo '
|
||||
<input type="file" name="', $field_id, '" id="', $field_id, '"', !empty($field['disabled']) ? ' disabled' : '', '>';
|
||||
break;
|
||||
|
||||
case 'text':
|
||||
case 'password':
|
||||
default:
|
||||
echo '
|
||||
<input type="', $field['type'], '" name="', $field_id, '" id="', $field_id, '"', isset($field['size']) ? ' size="' . $field['size'] . '"' : '', isset($field['maxlength']) ? ' maxlength="' . $field['maxlength'] . '"' : '', ' value="', htmlentities($this->data[$field_id]), '"', !empty($field['disabled']) ? ' disabled' : '', isset($field['trigger']) ? ' class="trigger-' . $field['trigger'] . '"' : '', '>';
|
||||
}
|
||||
|
||||
if (isset($field['after']))
|
||||
echo ' ', $field['after'];
|
||||
|
||||
echo '
|
||||
</dd>';
|
||||
}
|
||||
}
|
60
templates/LogInForm.php
Normal file
60
templates/LogInForm.php
Normal file
@ -0,0 +1,60 @@
|
||||
<?php
|
||||
/*****************************************************************************
|
||||
* LogInForm.php
|
||||
* Contains the login form template.
|
||||
*
|
||||
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
||||
*****************************************************************************/
|
||||
|
||||
class LogInForm extends SubTemplate
|
||||
{
|
||||
private $error_message = '';
|
||||
private $redirect_url = '';
|
||||
private $emailaddress = '';
|
||||
|
||||
public function setErrorMessage($message)
|
||||
{
|
||||
$this->error_message = $message;
|
||||
}
|
||||
|
||||
public function setRedirectUrl($url)
|
||||
{
|
||||
$_SESSION['login_url'] = $url;
|
||||
$this->redirect_url = $url;
|
||||
}
|
||||
|
||||
public function setEmail($addr)
|
||||
{
|
||||
$this->emailaddress = htmlentities($addr);
|
||||
}
|
||||
|
||||
protected function html_content()
|
||||
{
|
||||
echo '
|
||||
<form action="', BASEURL, '/login/" method="post" id="login">
|
||||
<h3>Admin login</h3>';
|
||||
|
||||
// Invalid login? Show a message.
|
||||
if (!empty($this->error_message))
|
||||
echo '
|
||||
<p style="color: red">', $this->error_message, '</p>';
|
||||
|
||||
echo '
|
||||
<dl>
|
||||
<dt><label for="field_emailaddress">E-mail address:</label></dt>
|
||||
<dd><input type="text" id="field_emailaddress" name="emailaddress" tabindex="1" value="', $this->emailaddress, '" autofocus></dd>
|
||||
|
||||
<dt><label for="field_password">Password:</label></dt>
|
||||
<dd><input type="password" id="field_password" name="password" tabindex="2"></dd>
|
||||
</dl>';
|
||||
|
||||
// Throw in a redirect url if asked for.
|
||||
if (!empty($this->redirect_url))
|
||||
echo '
|
||||
<input type="hidden" name="redirect_url" value="', base64_encode($this->redirect_url), '">';
|
||||
|
||||
echo '
|
||||
<div><button type="submit" class="btn btn-primary" id="field_login" name="login" tabindex="3">Log in</button></div>
|
||||
</form>';
|
||||
}
|
||||
}
|
115
templates/MainTemplate.php
Normal file
115
templates/MainTemplate.php
Normal file
@ -0,0 +1,115 @@
|
||||
<?php
|
||||
/*****************************************************************************
|
||||
* MainTemplate.php
|
||||
* Defines the key template MainTemplate.
|
||||
*
|
||||
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
||||
*****************************************************************************/
|
||||
|
||||
class MainTemplate extends Template
|
||||
{
|
||||
private $title = '';
|
||||
private $classes = [];
|
||||
private $area;
|
||||
private $css = '';
|
||||
private $header_html = '';
|
||||
private $canonical_url = '';
|
||||
|
||||
public function __construct($title = '')
|
||||
{
|
||||
$this->title = $title;
|
||||
}
|
||||
|
||||
public function html_main()
|
||||
{
|
||||
echo '<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>', $this->title, '</title>', !empty($this->canonical_url) ? '
|
||||
<link rel="canonical" href="' . $this->canonical_url . '">' : '', '
|
||||
<link type="text/css" rel="stylesheet" href="', BASEURL, '/css/default.css">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">', !empty($this->css) ? '
|
||||
<style type="text/css">' . $this->css . '
|
||||
</style>' : '', $this->header_html, '
|
||||
</head>
|
||||
<body', !empty($this->classes) ? ' class="' . implode(' ', $this->classes) . '"' : '', '>
|
||||
<header>
|
||||
<h1 id="logo"><a href="', BASEURL, '/">#pics</a></h1>
|
||||
<ul id="nav">
|
||||
<li><a href="', BASEURL, '/">albums</a></li>
|
||||
<li><a href="', BASEURL, '/people/">people</a></li>
|
||||
<li><a href="', BASEURL, '/timeline/">timeline</a></li>
|
||||
</ul>
|
||||
</header>
|
||||
<div id="wrapper">';
|
||||
|
||||
foreach ($this->_subtemplates as $template)
|
||||
$template->html_main();
|
||||
|
||||
echo '
|
||||
<footer>';
|
||||
|
||||
if (Registry::has('user') && Registry::get('user')->isAdmin())
|
||||
{
|
||||
if (class_exists('Cache'))
|
||||
echo '
|
||||
<span class="cache-info">Cache info: ', Cache::$hits, ' hits, ', Cache::$misses, ' misses, ', Cache::$puts, ' puts, ', Cache::$removals, ' removals</span>';
|
||||
|
||||
if (Registry::has('start'))
|
||||
echo '<br>
|
||||
<span class="creation-time">Page creation time: ', sprintf('%1.4f', microtime(true) - Registry::get('start')), ' seconds</span>';
|
||||
|
||||
if (Registry::has('db'))
|
||||
echo '<br>
|
||||
<span class="query-count">Database interaction: ', Registry::get('db')->getQueryCount(), ' queries</span>';
|
||||
}
|
||||
else
|
||||
echo '
|
||||
<span class="vanity">Powered by <a href="https://aaronweb.net/projects/kabuki/">Kabuki CMS</a> | <a href="', BASEURL, '/login/">Admin</a></span>';
|
||||
|
||||
echo '
|
||||
</footer>
|
||||
</div>';
|
||||
|
||||
if (Registry::has('db') && defined("DB_LOG_QUERIES") && DB_LOG_QUERIES)
|
||||
foreach (Registry::get('db')->getLoggedQueries() as $query)
|
||||
echo '<pre>', strtr($query, "\t", " "), '</pre>';
|
||||
|
||||
echo '
|
||||
</body>
|
||||
</html>';
|
||||
}
|
||||
|
||||
public function appendCss($css)
|
||||
{
|
||||
$this->css .= $css;
|
||||
}
|
||||
|
||||
public function appendHeaderHtml($html)
|
||||
{
|
||||
$this->header_html .= "\n\t\t" . $html;
|
||||
}
|
||||
|
||||
public function appendStylesheet($uri)
|
||||
{
|
||||
$this->appendHeaderHtml('<link text="text/css" rel="stylesheet" href="'. $uri . '">');
|
||||
}
|
||||
|
||||
public function setCanonicalUrl($url)
|
||||
{
|
||||
$this->canonical_url = $url;
|
||||
}
|
||||
|
||||
public function addClass($class)
|
||||
{
|
||||
if (!in_array($class, $this->classes))
|
||||
$this->classes[] = $class;
|
||||
}
|
||||
|
||||
public function removeClass($class)
|
||||
{
|
||||
if ($key = array_search($class, $this->classes) !== false)
|
||||
unset($this->classes[$key]);
|
||||
}
|
||||
}
|
77
templates/MediaUploader.php
Normal file
77
templates/MediaUploader.php
Normal file
@ -0,0 +1,77 @@
|
||||
<?php
|
||||
/*****************************************************************************
|
||||
* MediaUploader.php
|
||||
* Contains the media uploading template.
|
||||
*
|
||||
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
||||
*****************************************************************************/
|
||||
|
||||
class MediaUploader extends SubTemplate
|
||||
{
|
||||
protected function html_content()
|
||||
{
|
||||
echo '
|
||||
<form action="" class="admin_box" method="post" enctype="multipart/form-data">
|
||||
<h2>Upload new media</h2>
|
||||
<div>
|
||||
<h3>Select files</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<input type="file" name="new_asset[0]"><br>
|
||||
<input type="text" name="title[0]" placeholder="Custom title (optional)" size="50">
|
||||
</li>
|
||||
<li>
|
||||
<input type="file" name="new_asset[1]"><br>
|
||||
<input type="text" name="title[1]" placeholder="Custom title (optional)" size="50">
|
||||
</li>
|
||||
<li>
|
||||
<input type="file" name="new_asset[2]"><br>
|
||||
<input type="text" name="title[2]" placeholder="Custom title (optional)" size="50">
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Link tags</h3>
|
||||
<ul id="tag_list">
|
||||
<li id="new_tag_container"><input type="text" id="new_tag" placeholder="Type to link a new tag"></li>
|
||||
</ul>
|
||||
</div>
|
||||
<script type="text/javascript" src="', BASEURL, '/js/ajax.js"></script>
|
||||
<script type="text/javascript" src="', BASEURL, '/js/autosuggest.js"></script>
|
||||
<script type="text/javascript">
|
||||
setTimeout(function() {
|
||||
var tag_autosuggest = new TagAutoSuggest({
|
||||
inputElement: "new_tag",
|
||||
listElement: "tag_list",
|
||||
baseUrl: "', BASEURL, '",
|
||||
appendCallback: function(item) {
|
||||
if (document.getElementById("linked_tag_" + item.id_tag)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var newCheck = document.createElement("input");
|
||||
newCheck.type = "checkbox";
|
||||
newCheck.name = "tag[" + item.id_tag + "]";
|
||||
newCheck.id = "linked_tag_" + item.id_tag;
|
||||
newCheck.title = "Uncheck to delete";
|
||||
newCheck.checked = "checked";
|
||||
|
||||
var newNode = document.createElement("li");
|
||||
newNode.appendChild(newCheck);
|
||||
|
||||
var newLabel = document.createTextNode(item.label);
|
||||
newNode.appendChild(newLabel);
|
||||
|
||||
var list = document.getElementById("tag_list");
|
||||
var input = document.getElementById("new_tag_container");
|
||||
list.insertBefore(newNode, input);
|
||||
}
|
||||
});
|
||||
}, 100);
|
||||
</script>
|
||||
<div>
|
||||
<input name="save" type="submit" value="Upload the lot">
|
||||
</div>
|
||||
</form>';
|
||||
}
|
||||
}
|
44
templates/Pagination.php
Normal file
44
templates/Pagination.php
Normal file
@ -0,0 +1,44 @@
|
||||
<?php
|
||||
/*****************************************************************************
|
||||
* Pagination.php
|
||||
* Contains the pagination template.
|
||||
*
|
||||
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
||||
*****************************************************************************/
|
||||
|
||||
class Pagination extends SubTemplate
|
||||
{
|
||||
private $index;
|
||||
|
||||
public function __construct(PageIndex $index)
|
||||
{
|
||||
$this->index = $index->getPageIndex();
|
||||
$this->class = $index->getPageIndexClass();
|
||||
}
|
||||
|
||||
protected function html_content()
|
||||
{
|
||||
echo '
|
||||
<div class="table_pagination', !empty($this->class) ? ' ' . $this->class : '', '">
|
||||
<ul>
|
||||
<li class="first"><', !empty($this->index['previous']) ? 'a href="' . $this->index['previous']['href'] . '"' : 'span', '>« previous</', !empty($this->index['previous']) ? 'a' : 'span', '></li>';
|
||||
|
||||
foreach ($this->index as $key => $page)
|
||||
{
|
||||
if (!is_numeric($key))
|
||||
continue;
|
||||
|
||||
if (!is_array($page))
|
||||
echo '
|
||||
<li class="page-padding"><span>...</span></li>';
|
||||
else
|
||||
echo '
|
||||
<li class="page-number', $page['is_selected'] ? ' active' : '', '"><a href="', $page['href'], '">', $page['index'], '</a></li>';
|
||||
}
|
||||
|
||||
echo '
|
||||
<li class="last"><', !empty($this->index['next']) ? 'a href="' . $this->index['next']['href'] . '"' : 'span', '>next »</', !empty($this->index['next']) ? 'a' : 'span', '></li>
|
||||
</ul>
|
||||
</div>';
|
||||
}
|
||||
}
|
244
templates/PhotosIndex.php
Normal file
244
templates/PhotosIndex.php
Normal file
@ -0,0 +1,244 @@
|
||||
<?php
|
||||
/*****************************************************************************
|
||||
* PhotosIndex.php
|
||||
* Contains the project index template.
|
||||
*
|
||||
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
||||
*****************************************************************************/
|
||||
|
||||
class PhotosIndex extends SubTemplate
|
||||
{
|
||||
protected $mosaic;
|
||||
protected $show_edit_buttons;
|
||||
protected $show_labels;
|
||||
protected $row_limit = 1000;
|
||||
protected $previous_header = '';
|
||||
|
||||
const PANORAMA_WIDTH = 1280;
|
||||
const PANORAMA_HEIGHT = null;
|
||||
|
||||
const PORTRAIT_WIDTH = 400;
|
||||
const PORTRAIT_HEIGHT = 640;
|
||||
|
||||
const LANDSCAPE_WIDTH = 850;
|
||||
const LANDSCAPE_HEIGHT = 640;
|
||||
|
||||
const DUO_WIDTH = 618;
|
||||
const DUO_HEIGHT = 412;
|
||||
|
||||
const SINGLE_WIDTH = 618;
|
||||
const SINGLE_HEIGHT = 412;
|
||||
|
||||
const TILE_WIDTH = 400;
|
||||
const TILE_HEIGHT = 267;
|
||||
|
||||
public function __construct(PhotoMosaic $mosaic, $show_edit_buttons = false, $show_labels = true, $show_headers = true)
|
||||
{
|
||||
$this->mosaic = $mosaic;
|
||||
$this->show_edit_buttons = $show_edit_buttons;
|
||||
$this->show_headers = $show_headers;
|
||||
$this->show_labels = $show_labels;
|
||||
}
|
||||
|
||||
protected function html_content()
|
||||
{
|
||||
echo '
|
||||
<div class="tiled_grid">';
|
||||
|
||||
for ($i = $this->row_limit; $i > 0 && $row = $this->mosaic->getRow(); $i--)
|
||||
{
|
||||
list($photos, $what) = $row;
|
||||
$this->header($photos);
|
||||
$this->$what($photos);
|
||||
}
|
||||
|
||||
echo '
|
||||
</div>';
|
||||
}
|
||||
|
||||
protected function header($photos)
|
||||
{
|
||||
if (!$this->show_headers)
|
||||
return;
|
||||
|
||||
$date = $photos[0]->getDateCaptured();
|
||||
if (!$date)
|
||||
return;
|
||||
|
||||
$header = $date->format('F Y');
|
||||
if ($header === $this->previous_header)
|
||||
return;
|
||||
|
||||
$name = str_replace(' ', '', strtolower($header));
|
||||
echo '
|
||||
<div class="tiled_header" id="', $name, '">
|
||||
<a href="#', $name, '">', $header, '</a>
|
||||
</div>';
|
||||
|
||||
$this->previous_header = $header;
|
||||
}
|
||||
|
||||
protected function color(Image $image)
|
||||
{
|
||||
$color = $image->bestColor();
|
||||
if ($color == 'FFFFFF')
|
||||
$color = 'ccc';
|
||||
|
||||
return $color;
|
||||
}
|
||||
|
||||
protected function photo(Image $image, $width, $height, $crop = true, $fit = true)
|
||||
{
|
||||
if ($this->show_edit_buttons)
|
||||
echo '
|
||||
<a class="edit" href="', BASEURL, '/editasset/?id=', $image->getId(), '">Edit</a>';
|
||||
|
||||
echo '
|
||||
<a href="', $image->getUrl(), '">
|
||||
<img src="', $image->getThumbnailUrl($width, $height, $crop, $fit), '" alt="" title="', $image->getTitle(), '">';
|
||||
|
||||
if ($this->show_labels)
|
||||
echo '
|
||||
<h4>', $image->getTitle(), '</h4>';
|
||||
|
||||
echo '
|
||||
</a>';
|
||||
}
|
||||
|
||||
protected function panorama(array $photos)
|
||||
{
|
||||
foreach ($photos as $image)
|
||||
{
|
||||
echo '
|
||||
<div style="border-color: #', $this->color($image), '" class="panorama">';
|
||||
|
||||
$this->photo($image, static::PANORAMA_WIDTH, static::PANORAMA_HEIGHT, false, false);
|
||||
|
||||
echo '
|
||||
</div>';
|
||||
}
|
||||
}
|
||||
|
||||
protected function portrait(array $photos)
|
||||
{
|
||||
$image = array_shift($photos);
|
||||
|
||||
echo '
|
||||
<div class="tiled_row">
|
||||
<div class="column_portrait">
|
||||
<div style="border-color: #', $this->color($image), '" class="portrait">';
|
||||
|
||||
$this->photo($image, static::PORTRAIT_WIDTH, static::PORTRAIT_HEIGHT, 'top');
|
||||
|
||||
echo '
|
||||
</div>
|
||||
</div>
|
||||
<div class="column_tiles_four">';
|
||||
|
||||
foreach ($photos as $image)
|
||||
{
|
||||
$color = $image->bestColor();
|
||||
if ($color == 'FFFFFF')
|
||||
$color = 'ccc';
|
||||
|
||||
echo '
|
||||
<div style="border-color: #', $color, '" class="landscape">';
|
||||
|
||||
$this->photo($image, static::TILE_WIDTH, static::TILE_HEIGHT, 'top');
|
||||
|
||||
echo '
|
||||
</div>';
|
||||
}
|
||||
|
||||
echo '
|
||||
</div>
|
||||
</div>';
|
||||
}
|
||||
|
||||
protected function landscape(array $photos)
|
||||
{
|
||||
$image = array_shift($photos);
|
||||
|
||||
echo '
|
||||
<div class="tiled_row">
|
||||
<div class="column_landscape">
|
||||
<div style="border-color: #', $this->color($image), '" class="landscape">';
|
||||
|
||||
$this->photo($image, static::LANDSCAPE_WIDTH, static::LANDSCAPE_HEIGHT, 'top');
|
||||
|
||||
echo '
|
||||
</div>
|
||||
</div>
|
||||
<div class="column_tiles_two">';
|
||||
|
||||
foreach ($photos as $image)
|
||||
{
|
||||
echo '
|
||||
<div style="border-color: #', $this->color($image), '" class="landscape">';
|
||||
|
||||
$this->photo($image, static::TILE_WIDTH, static::TILE_HEIGHT, 'top');
|
||||
|
||||
echo '
|
||||
</div>';
|
||||
}
|
||||
|
||||
echo '
|
||||
</div>
|
||||
</div>';
|
||||
}
|
||||
|
||||
protected function duo(array $photos)
|
||||
{
|
||||
echo '
|
||||
<div class="tiled_row">';
|
||||
|
||||
foreach ($photos as $image)
|
||||
{
|
||||
echo '
|
||||
<div style="border-color: #', $this->color($image), '" class="duo">';
|
||||
|
||||
$this->photo($image, static::DUO_WIDTH, static::DUO_HEIGHT, true);
|
||||
|
||||
echo '
|
||||
</div>';
|
||||
}
|
||||
|
||||
echo '
|
||||
</div>';
|
||||
}
|
||||
|
||||
protected function single(array $photos)
|
||||
{
|
||||
$image = array_shift($photos);
|
||||
|
||||
echo '
|
||||
<div class="tiled_row">
|
||||
<div style="border-color: #', $this->color($image), '" class="single">';
|
||||
|
||||
$this->photo($image, static::SINGLE_WIDTH, static::SINGLE_HEIGHT, 'top');
|
||||
|
||||
echo '
|
||||
</div>
|
||||
</div>';
|
||||
}
|
||||
|
||||
protected function row(array $photos)
|
||||
{
|
||||
echo '
|
||||
<div class="tiled_row">';
|
||||
|
||||
foreach ($photos as $image)
|
||||
{
|
||||
echo '
|
||||
<div style="border-color: #', $this->color($image), '" class="landscape">';
|
||||
|
||||
$this->photo($image, static::TILE_WIDTH, static::TILE_HEIGHT, true);
|
||||
|
||||
echo '
|
||||
</div>';
|
||||
}
|
||||
|
||||
echo '
|
||||
</div>';
|
||||
}
|
||||
}
|
17
templates/SubTemplate.php
Normal file
17
templates/SubTemplate.php
Normal file
@ -0,0 +1,17 @@
|
||||
<?php
|
||||
/*****************************************************************************
|
||||
* SubTemplate.php
|
||||
* Defines the key template SubTemplate.
|
||||
*
|
||||
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
||||
*****************************************************************************/
|
||||
|
||||
abstract class SubTemplate extends Template
|
||||
{
|
||||
public function html_main()
|
||||
{
|
||||
echo $this->html_content();
|
||||
}
|
||||
|
||||
abstract protected function html_content();
|
||||
}
|
114
templates/TabularData.php
Normal file
114
templates/TabularData.php
Normal file
@ -0,0 +1,114 @@
|
||||
<?php
|
||||
/*****************************************************************************
|
||||
* TabularData.php
|
||||
* Contains the template that displays tabular data.
|
||||
*
|
||||
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
||||
*****************************************************************************/
|
||||
|
||||
class TabularData extends Pagination
|
||||
{
|
||||
public function __construct(GenericTable $table)
|
||||
{
|
||||
$this->_t = $table;
|
||||
parent::__construct($table);
|
||||
}
|
||||
|
||||
protected function html_content()
|
||||
{
|
||||
echo '
|
||||
<div class="admin_box">';
|
||||
|
||||
$title = $this->_t->getTitle();
|
||||
if (!empty($title))
|
||||
echo '
|
||||
<h2>', $title, '</h2>';
|
||||
|
||||
// Showing a page index?
|
||||
parent::html_content();
|
||||
|
||||
// Maybe even a small form?
|
||||
if (isset($this->_t->form_above))
|
||||
$this->showForm($this->_t->form_above);
|
||||
|
||||
// Build the table!
|
||||
echo '
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>';
|
||||
|
||||
// Show the table's headers.
|
||||
foreach ($this->_t->getHeader() as $th)
|
||||
{
|
||||
echo '
|
||||
<th', (!empty($th['width']) ? ' width="' . $th['width'] . '"' : ''), (!empty($th['class']) ? ' class="' . $th['class'] . '"' : ''), ($th['colspan'] > 1 ? ' colspan="' . $th['colspan'] . '"' : ''), ' scope="', $th['scope'], '">',
|
||||
$th['href'] ? '<a href="' . $th['href'] . '">' . $th['label'] . '</a>' : $th['label'];
|
||||
|
||||
if ($th['sort_mode'] )
|
||||
echo ' ', $th['sort_mode'] == 'up' ? '↑' : '↓';
|
||||
|
||||
echo '</th>';
|
||||
}
|
||||
|
||||
echo '
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>';
|
||||
|
||||
// Show the table's body.
|
||||
$body = $this->_t->getBody();
|
||||
if (is_array($body))
|
||||
{
|
||||
foreach ($body as $tr)
|
||||
{
|
||||
echo '
|
||||
<tr', (!empty($tr['class']) ? ' class="' . $tr['class'] . '"' : ''), '>';
|
||||
|
||||
foreach ($tr['cells'] as $td)
|
||||
echo '
|
||||
<td', (!empty($td['width']) ? ' width="' . $td['width'] . '"' : ''), '>', $td['value'], '</td>';
|
||||
|
||||
echo '
|
||||
</tr>';
|
||||
}
|
||||
}
|
||||
else
|
||||
echo '
|
||||
<tr>
|
||||
<td colspan="', count($this->_t->getHeader()), '">', $body, '</td>
|
||||
</tr>';
|
||||
|
||||
echo '
|
||||
</tbody>
|
||||
</table>';
|
||||
|
||||
// Maybe another small form?
|
||||
if (isset($this->_t->form_below))
|
||||
$this->showForm($this->_t->form_below);
|
||||
|
||||
// Showing a page index?
|
||||
parent::html_content();
|
||||
|
||||
echo '
|
||||
</div>';
|
||||
}
|
||||
|
||||
protected function showForm($form)
|
||||
{
|
||||
echo '
|
||||
<form action="', $form['action'], '" method="', $form['method'], '" class="table_form ', $form['class'], '">';
|
||||
|
||||
if (!empty($form['fields']))
|
||||
foreach ($form['fields'] as $name => $field)
|
||||
echo '
|
||||
<input name="', $name, '" type="', $field['type'], '" placeholder="', $field['placeholder'], '"', isset($field['class']) ? ' class="' . $field['class'] . '"' : '', isset($field['value']) ? ' value="' . $field['value'] . '"' : '', '>';
|
||||
|
||||
if (!empty($form['buttons']))
|
||||
foreach ($form['buttons'] as $name => $button)
|
||||
echo '
|
||||
<input name="', $name, '" type="', $button['type'], '" value="', $button['caption'], '" class="btn', isset($button['class']) ? ' ' . $button['class'] . '' : '', '">';
|
||||
|
||||
echo '
|
||||
</form>';
|
||||
}
|
||||
}
|
34
templates/Template.php
Normal file
34
templates/Template.php
Normal file
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
/*****************************************************************************
|
||||
* Template.php
|
||||
* Contains key Template interface.
|
||||
*
|
||||
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
||||
*****************************************************************************/
|
||||
|
||||
abstract class Template
|
||||
{
|
||||
protected $_subtemplates = array();
|
||||
|
||||
abstract public function html_main();
|
||||
|
||||
public function adopt(Template $template, $position = 'end')
|
||||
{
|
||||
// By default, we append it.
|
||||
if ($position == 'end')
|
||||
$this->_subtemplates[] = $template;
|
||||
// We can also add it to the beginning of the list, though.
|
||||
else
|
||||
array_unshift($this->_subtemplates, $template);
|
||||
}
|
||||
|
||||
public function clear()
|
||||
{
|
||||
$this->_subtemplates = [];
|
||||
}
|
||||
|
||||
public function pass($id, $data)
|
||||
{
|
||||
$this->{$id} = $data;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user