12 Commits

Author SHA1 Message Date
Joost Rijneveld
a211e3ae4a Elaborate on how to set up dev env 2022-11-27 14:03:14 +01:00
7d82a4a924 Merge pull request 'Complete date-ordered orderings' (#29) from electricdusk/pics:assets-complete-ordering into master
Reviewed-on: Public/pics#29
2022-11-22 21:09:10 +01:00
b7a37c85f6 Complete date-ordered orderings
Bug as reported by Yorick: When two Assets have the same capture
date, a bug occurs in the interface where the user gets stuck in
a loop when moving to the next image.

This patch uses the primary key as a fallback when ordering the
images by capture date.  This way, the asset ordering is complete
and it should resolve the bug.
2022-11-22 12:00:53 +01:00
0ec0de4414 Replace deprecated strftime calls 2022-05-07 13:25:19 +02:00
69417c36ed Merge pull request 'EXIF: prefer DateTimeOriginal over DateTimeDigitized' (#27) from exif-date-time-original into master
Reviewed-on: Public/pics#27
2022-02-16 21:56:04 +01:00
f2d8a32e67 EXIF: prefer DateTimeOriginal over DateTimeDigitized 2022-02-16 21:43:55 +01:00
4863561129 Merge pull request 'Refactor generic tables and page index classes' (#26) from refactor-tables into master
Reviewed-on: Public/pics#26
2021-05-17 20:19:18 +02:00
8474d3b2b2 Merge pull request 'Modernise autosuggest code' (#25) from autosuggest into master
Reviewed-on: Public/pics#25
2021-05-17 20:19:05 +02:00
3bf69fd21f Prevent XSS in error log viewer. 2021-03-10 17:40:06 +01:00
70e6001c85 Replace event.keyCode with event.key equivalents. 2021-02-16 15:26:57 +01:00
4402521051 Highlight matching string in autosuggest entries. 2021-02-15 12:14:24 +01:00
889302cd36 Modernise AutoSuggest and TagAutoSuggest classes. 2021-02-15 12:14:23 +01:00
10 changed files with 232 additions and 175 deletions

7
.gitignore vendored
View File

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

View File

@@ -13,12 +13,22 @@ The Kabuki codebase requires the following PHP extensions to be enabled for full
## Setup ## Setup
Copy `config.php.dist` to `config.php` and set-up the constants contained in the file. Copy `config.php.dist` to `config.php` and set-up the constants contained in the file. For development, consider starting from `config-dev.php.dist`.
Ensure you have a MySQL database running with credentials matching your `config.php`. For development, consider the /dev/docker-compose.yml file.
Run `composer install`. If you do not have composer installed globally, run it from the project directory as follows:
```
wget -O composer-setup.php https://getcomposer.org/installer
php composer-setup.php --install-dir=.
php ./composer.phar install
```
## Running ## Running
For development purposes, simply run the `server` script provided in the root of this repository. For development purposes, simply run the `server` script provided in the root of this repository.
This will start a PHP development server on `hashru.local:8080`. This will start a PHP development server on `127.0.0.1:8080`.
For a production environment, please set up a proper PHP-FPM environment instead. For a production environment, please set up a proper PHP-FPM environment instead.

36
config-dev.php.dist Normal file
View File

@@ -0,0 +1,36 @@
<?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 = 'http://127.0.0.1:8080'; // no trailing /
// Reply-To e-mail header address
const REPLY_TO_ADDRESS = 'no-reply@my.domain.tld';
// 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 = 'hashru';
const DB_NAME = 'hashru_pics';
const DB_LOG_QUERIES = false;
const SITE_TITLE = 'HashRU Pics';
const SITE_SLOGAN = 'Nijmeegs Nerdclubje';

View File

@@ -47,9 +47,13 @@ class ManageErrors extends HTMLController
'parse' => [ 'parse' => [
'type' => 'function', 'type' => 'function',
'data' => function($row) { '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>' . return $row['message'] . '<br>' .
'<pre style="display: none">' . $row['debug_info'] . '</pre></div>' . '<div><a onclick="this.parentNode.childNodes[1].style.display=\'block\';this.style.display=\'none\';">Show debug info</a>' .
'<small><a href="' . BASEURL . $row['request_uri'] . '">' . $row['request_uri'] . '</a></small>'; '<pre style="display: none">' . htmlspecialchars($row['debug_info']) .
'</pre></div>' .
'<small><a href="' . BASEURL .
htmlspecialchars($row['request_uri']) . '">' .
htmlspecialchars($row['request_uri']) . '</a></small>';
} }
], ],
'header' => 'Message / URL', 'header' => 'Message / URL',
@@ -85,7 +89,7 @@ class ManageErrors extends HTMLController
'header' => 'UID', 'header' => 'UID',
'is_sortable' => true, 'is_sortable' => true,
'parse' => [ 'parse' => [
'link' => BASEURL . '/member/?id={ID_USER}', 'link' => BASEURL . '/edituser/?id={ID_USER}',
'data' => 'id_user', 'data' => 'id_user',
], ],
], ],

11
dev/docker-compose.yml Normal file
View File

@@ -0,0 +1,11 @@
version: '3'
services:
mysql:
image: mysql:latest
ports:
- 3306:3306
environment:
MYSQL_USER: 'hashru'
MYSQL_PASSWORD: 'hashru'
MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
MYSQL_DATABASE: 'hashru_pics'

View File

@@ -606,14 +606,12 @@ class Asset
FROM assets_tags AS t FROM assets_tags AS t
INNER JOIN assets AS a ON a.id_asset = t.id_asset INNER JOIN assets AS a ON a.id_asset = t.id_asset
WHERE t.id_tag = {int:id_tag} AND WHERE t.id_tag = {int:id_tag} AND
a.date_captured <= {datetime:date_captured} AND (a.date_captured, a.id_asset) < ({datetime:date_captured}, {int:id_asset})
a.id_asset != {int:id_asset} ORDER BY a.date_captured DESC, a.id_asset DESC'
ORDER BY a.date_captured DESC'
: ' : '
FROM assets AS a FROM assets AS a
WHERE date_captured >= {datetime:date_captured} AND WHERE (a.date_captured, a.id_asset) > ({datetime:date_captured}, {int:id_asset})
a.id_asset != {int:id_asset} ORDER BY date_captured ASC, a.id_asset ASC')
ORDER BY date_captured ASC')
. ' . '
LIMIT 1', LIMIT 1',
[ [
@@ -639,14 +637,12 @@ class Asset
FROM assets_tags AS t FROM assets_tags AS t
INNER JOIN assets AS a ON a.id_asset = t.id_asset INNER JOIN assets AS a ON a.id_asset = t.id_asset
WHERE t.id_tag = {int:id_tag} AND WHERE t.id_tag = {int:id_tag} AND
a.date_captured >= {datetime:date_captured} AND (a.date_captured, a.id_asset) > ({datetime:date_captured}, {int:id_asset})
a.id_asset != {int:id_asset} ORDER BY a.date_captured ASC, a.id_asset ASC'
ORDER BY a.date_captured ASC'
: ' : '
FROM assets AS a FROM assets AS a
WHERE date_captured <= {datetime:date_captured} AND WHERE (a.date_captured, a.id_asset) < ({datetime:date_captured}, {int:id_asset})
a.id_asset != {int:id_asset} ORDER BY date_captured DESC, a.id_asset DESC')
ORDER BY date_captured DESC')
. ' . '
LIMIT 1', LIMIT 1',
[ [

View File

@@ -96,7 +96,9 @@ class EXIF
elseif (!empty($exif['Make'])) elseif (!empty($exif['Make']))
$meta['camera'] = trim($exif['Make']); $meta['camera'] = trim($exif['Make']);
if (!empty($exif['DateTimeDigitized'])) if (!empty($exif['DateTimeOriginal']))
$meta['created_timestamp'] = self::toUnixTime($exif['DateTimeOriginal']);
elseif (!empty($exif['DateTimeDigitized']))
$meta['created_timestamp'] = self::toUnixTime($exif['DateTimeDigitized']); $meta['created_timestamp'] = self::toUnixTime($exif['DateTimeDigitized']);
return new self($meta); return new self($meta);

View File

@@ -228,9 +228,9 @@ class GenericTable
// Timestamps get custom treatment. // Timestamps get custom treatment.
case 'timestamp': case 'timestamp':
if (empty($options['data']['pattern']) || $options['data']['pattern'] === 'long') if (empty($options['data']['pattern']) || $options['data']['pattern'] === 'long')
$pattern = '%F %H:%M'; $pattern = 'Y-m-d H:i';
elseif ($options['data']['pattern'] === 'short') elseif ($options['data']['pattern'] === 'short')
$pattern = '%F'; $pattern = 'Y-m-d';
else else
$pattern = $options['data']['pattern']; $pattern = $options['data']['pattern'];
@@ -242,7 +242,7 @@ class GenericTable
if (isset($options['data']['if_null']) && $timestamp == 0) if (isset($options['data']['if_null']) && $timestamp == 0)
$value = $options['data']['if_null']; $value = $options['data']['if_null'];
else else
$value = strftime($pattern, $timestamp); $value = date($pattern, $timestamp);
break; break;
} }

View File

@@ -13,164 +13,165 @@ provided that the following conditions are met:
'use strict'; 'use strict';
function AutoSuggest(opt) { class AutoSuggest {
if (typeof opt.inputElement === "undefined" || typeof opt.listElement === "undefined" || typeof opt.baseUrl === "undefined" || typeof opt.appendCallback === "undefined") { constructor(opt) {
return; 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;
this.input.addEventListener('keydown', event => this.doSelection(event), false);
this.input.addEventListener('keyup', event => this.onType(event), false);
} }
this.input = document.getElementById(opt.inputElement); doSelection(event) {
this.input.autocomplete = "off"; if (typeof this.container === "undefined" || this.container.children.length === 0) {
this.list = document.getElementById(opt.listElement); return;
this.appendCallback = opt.appendCallback; }
this.baseurl = opt.baseUrl;
var self = this; switch (event.key) {
this.input.addEventListener('keydown', function(event) { case 'Enter':
self.doSelection(event); event.preventDefault();
}, false); this.container.children[this.selectedIndex].click();
this.input.addEventListener('keyup', function(event) { break;
self.onType(this, event);
}, false); case 'ArrowUp':
case 'ArrowDown':
event.preventDefault();
this.findSelectedElement().className = '';
this.selectedIndex += event.key === 'ArrowUp' ? -1 : 1;
if (this.selectedIndex < 0) {
this.selectedIndex = this.container.children.length - 1;
} else if (this.selectedIndex === this.container.children.length) {
this.selectedIndex = 0;
}
let new_el = this.findSelectedElement().className = 'selected';
break;
}
};
findSelectedElement() {
return this.container.children[this.selectedIndex];
};
onType(event) {
if (['Enter', 'ArrowDown', 'ArrowUp'].indexOf(event.key) !== -1) {
return;
}
let tokens = event.target.value.split(/\s+/).filter(token => token.length >= 2);
if (tokens.length === 0) {
if (typeof this.container !== "undefined") {
this.clearContainer();
}
return false;
}
let request_uri = this.baseurl + '/suggest/?type=tags&data=' + window.encodeURIComponent(tokens.join(" "));
let request = new HttpRequest('get', request_uri, {}, this.onReceive, this);
};
onReceive(response, self) {
self.openContainer();
self.clearContainer();
self.fillContainer(response);
};
openContainer() {
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;
};
clearContainer() {
while (this.container.children.length > 0) {
this.container.removeChild(this.container.children[0]);
}
};
clearInput() {
this.input.value = "";
this.input.focus();
};
closeContainer() {
this.container.parentNode.removeChild(this.container);
};
fillContainer(response) {
this.selectedIndex = 0;
let query = this.input.value.trim().replace(/[\-\[\]{}()*+?.,\\\/^\$|#]/g, ' ');
let query_tokens = query.split(/ +/).sort((a,b) => a.length - b.length);
response.items.forEach((item, i) => {
let node = document.createElement('li');
node.innerHTML = this.highlightMatches(query_tokens, item.label);
node.jsondata = item;
node.addEventListener('click', event => {
this.appendCallback(event.target.jsondata);
this.closeContainer();
this.clearInput();
});
this.container.appendChild(node);
if (this.container.children.length === 1) {
node.className = 'selected';
}
});
};
highlightMatches(query_tokens, item) {
let itemTokens = item.split(/ +/);
let queryTokens = new RegExp('(' + query_tokens.join('\|') + ')', 'i');
itemTokens.forEach((token, index) => {
item = item.replace(token, token.replace(queryTokens, ($1, match) => '<strong>' + match + '</strong>'));
});
return item;
};
} }
AutoSuggest.prototype.doSelection = function(event) { class TagAutoSuggest extends AutoSuggest {
if (typeof this.container === "undefined" || this.container.children.length === 0) { constructor(opt) {
return; super(opt);
this.type = "tags";
} }
switch (event.keyCode) { fillContainer(response) {
case 13: // Enter if (response.items.length > 0) {
event.preventDefault(); super.fillContainer.call(this, response);
this.container.children[this.selectedIndex].click(); } else {
break; let node = document.createElement('li')
node.innerHTML = "<em>Tag does not exist yet. Create it?</em>";
case 38: // Arrow up node.addEventListener('click', event => {
case 40: // Arrow down this.createNewTag(response => this.appendCallback(response));
event.preventDefault(); this.closeContainer();
this.findSelectedElement().className = ''; this.clearInput();
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() { this.container.appendChild(node);
return this.container.children[this.selectedIndex]; this.selectedIndex = 0;
};
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 >= 2;
});
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'; node.className = 'selected';
} }
}); };
};
createNewTag(callback) {
function TagAutoSuggest(opt) { let request_uri = this.baseurl + '/suggest/?type=createtag';
AutoSuggest.prototype.constructor.call(this, opt); let request = new HttpRequest('post', request_uri, 'tag=' + encodeURIComponent(this.input.value), callback, this);
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) {
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 + '/suggest/?type=createtag';
var request = new HttpRequest('post', request_uri, 'tag=' + encodeURIComponent(this.input.value), callback, this);
} }

2
server
View File

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