Initial commit.

This is to be the new HashRU website based on the Aaronweb.net/Kabuki CMS.
This commit is contained in:
Aaron van Geffen 2016-09-01 23:13:23 +02:00
commit ab0e4efbcb
68 changed files with 8054 additions and 0 deletions

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
text eol=lf

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
.DS_Store
composer.lock
config.php
hashru.sublime-project
hashru.sublime-workspace

1
README Normal file
View File

@ -0,0 +1 @@
This marks the development repository for the HashRU website.

10
TODO.md Normal file
View 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
View 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
View 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
View 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
View 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
View 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;
}
}
}

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

View 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
View 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
View 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;
}
}

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

View 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,
];
}
}
}

View 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;
}
}
}

View 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/');
}
}

View 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 . '/' : '') . '">&laquo; Go back to &quot;' . $ptag->tag . '&quot;</a></p>';
}
elseif ($tag->kind === 'Person')
$description .= '<p><a href="' . BASEURL . '/people/">&laquo; Go back to &quot;People&quot;</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();
}
}

View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,61 @@
<?php
/*****************************************************************************
* Cache.php
* Contains key class Cache.
*
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
*****************************************************************************/
class Cache
{
public static $hits = 0;
public static $misses = 0;
public static $puts = 0;
public static $removals = 0;
public static function put($key, $value, $ttl = 3600)
{
// If the cache is unavailable, don't bother.
if (!CACHE_ENABLED || !function_exists('apcu_store'))
return false;
// Keep track of the amount of cache puts.
self::$puts++;
// Store the data in serialized form.
return apcu_store(CACHE_KEY_PREFIX . $key, serialize($value), $ttl);
}
// Get some data from the cache.
public static function get($key)
{
// If the cache is unavailable, don't bother.
if (!CACHE_ENABLED || !function_exists('apcu_fetch'))
return false;
// Try to fetch it!
$value = apcu_fetch(CACHE_KEY_PREFIX . $key);
// Were we successful?
if (!empty($value))
{
self::$hits++;
return unserialize($value);
}
// Otherwise, it's a miss.
else
{
self::$misses++;
return null;
}
}
public static function remove($key)
{
if (!CACHE_ENABLED || !function_exists('apcu_delete'))
return false;
self::$removals++;
return apcu_delete(CACHE_KEY_PREFIX . $key);
}
}

535
models/Database.php Normal file
View 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
View 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
View 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
View 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, ['<' => '&lt;', '>' => '&gt;', '"' => '&quot;', "\t" => ' ']);
$error_message = strtr($error_message, ['&lt;br&gt;' => "<br>", '&lt;br /&gt;' => "<br>", '&lt;b&gt;' => '<strong>', '&lt;/b&gt;' => '</strong>', '&lt;pre&gt;' => '<pre>', '&lt;/pre&gt;' => '</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
View 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
View 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
View 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
View 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
View 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
View 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);
}
}

View File

@ -0,0 +1,12 @@
<?php
/*****************************************************************************
* NotAllowedException.php
* Contains exception class NotAllowedException.
*
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
*****************************************************************************/
class NotAllowedException extends Exception
{
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

10
public/index.php Normal file
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,3 @@
User-agent: *
Disallow: /login

2
server Normal file
View File

@ -0,0 +1,2 @@
#!/bin/bash
php -S hashru.local:8080 -t public

36
templates/AdminBar.php Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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]);
}
}

View 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
View 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', '>&laquo; 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 &raquo;</', !empty($this->index['next']) ? 'a' : 'span', '></li>
</ul>
</div>';
}
}

244
templates/PhotosIndex.php Normal file
View 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
View 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
View 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' ? '&uarr;' : '&darr;';
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
View 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;
}
}