Compare commits

..

1 Commits

Author SHA1 Message Date
4412db1679 Add OIDC login support for external identity providers
Adds "Login with <provider>" as an alternative login method using the
jumbojett/openid-connect-php library. OIDC users must already exist in
the database (matched by email). Configurable via OIDC_PROVIDER_URL,
OIDC_CLIENT_ID, OIDC_CLIENT_SECRET, and OIDC_PROVIDER_NAME constants.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 16:10:17 +01:00
19 changed files with 88 additions and 535 deletions

View File

@@ -14,13 +14,7 @@ require_once 'vendor/autoload.php';
// Initialise the database.
Registry::set('start', microtime(true));
if (defined('DB_DRIVER') && DB_DRIVER === 'sqlite')
Registry::set('db', new Database('sqlite', ['file' => DB_FILE]));
else
Registry::set('db', new Database('mysql', [
'host' => DB_SERVER, 'user' => DB_USER,
'password' => DB_PASS, 'name' => DB_NAME,
]));
Registry::set('db', new Database(DB_SERVER, DB_USER, DB_PASS, DB_NAME));
// Handle errors our own way.
ErrorHandler::enable();

View File

@@ -40,4 +40,3 @@ const OIDC_PROVIDER_URL = ''; // e.g. 'https://kanidm.example.com/oauth2/op
const OIDC_CLIENT_ID = '';
const OIDC_CLIENT_SECRET = '';
const OIDC_PROVIDER_NAME = ''; // e.g. 'Kanidm' — used as button label
const OIDC_ADMIN_GROUP = ''; // OIDC group claim value that grants admin, e.g. 'pics_admins'

View File

@@ -206,7 +206,7 @@ class EditAlbum extends HTMLController
$data = $this->form->getData();
// Sanity check: don't let an album be its own parent
if ($id_tag && $data['id_parent'] == $id_tag)
if ($data['id_parent'] == $id_tag)
{
return $this->formview->adopt(new Alert('Invalid parent', 'An album cannot be its own parent.', 'danger'));
}

View File

@@ -109,14 +109,6 @@ class EditUser extends HTMLController
'maxlength' => 255,
'is_optional' => true,
],
'oidc_sub' => [
'header' => 'OIDC',
'type' => 'text',
'label' => 'OIDC subject identifier',
'size' => 50,
'maxlength' => 255,
'is_optional' => true,
],
'is_admin' => [
'header' => 'Privileges',
'type' => 'checkbox',
@@ -153,10 +145,6 @@ class EditUser extends HTMLController
// Quick stripping.
$data['slug'] = strtr(strtolower($data['slug']), [' ' => '-', '--' => '-', '&' => 'and', '=>' => '', "'" => "", ":"=> "", '/' => '-', '\\' => '-']);
// Normalise empty OIDC sub to null (unique constraint).
if (empty($data['oidc_sub']))
$data['oidc_sub'] = null;
// Checkboxes, fun!
$data['is_admin'] = empty($data['is_admin']) ? 0 : 1;

View File

@@ -29,7 +29,7 @@ class OIDCLogin
$oidc = new OpenIDConnectClient(OIDC_PROVIDER_URL, OIDC_CLIENT_ID, OIDC_CLIENT_SECRET);
$oidc->setRedirectURL(BASEURL . '/oidclogin/');
$oidc->addScope(['openid', 'email', 'profile', 'groups']);
$oidc->addScope(['openid', 'email']);
try
{
@@ -42,62 +42,22 @@ class OIDCLogin
exit;
}
// Get the stable subject identifier from the ID token.
$sub = $oidc->getVerifiedClaims('sub');
if (empty($sub))
{
$_SESSION['login_msg'] = ['', 'No subject identifier received from OIDC provider.', 'danger'];
header('Location: ' . BASEURL . '/login/');
exit;
}
// Step 1: Look up user by oidc_sub.
$user = Member::fromOidcSub($sub);
// Step 2: One-time email migration — link existing account by email.
if ($user === null || $user === false)
{
$email = $oidc->requestUserInfo('email');
if (!empty($email))
if (empty($email))
{
$user = Member::fromEmailAddress($email);
if ($user !== null && $user !== false)
$user->update(['oidc_sub' => $sub]);
}
}
// Step 3: Auto-enroll — create a new user from OIDC claims.
if ($user === null || $user === false)
{
$first_name = $oidc->requestUserInfo('given_name') ?: '';
$last_name = $oidc->requestUserInfo('family_name') ?: '';
$email = $oidc->requestUserInfo('email') ?: $sub . '@oidc.placeholder';
$slug = $this->generateSlug($first_name, $last_name);
$user = Member::createNew([
'first_name' => $first_name ?: 'OIDC',
'surname' => $last_name ?: 'User',
'slug' => $slug,
'emailaddress' => $email,
'oidc_sub' => $sub,
]);
if ($user === false)
{
$_SESSION['login_msg'] = ['', 'Failed to create account. Please contact an administrator.', 'danger'];
$_SESSION['login_msg'] = ['', 'No email address received from OIDC provider.', 'danger'];
header('Location: ' . BASEURL . '/login/');
exit;
}
$user = Member::fromEmailAddress($email);
if ($user === null || $user === false)
{
$_SESSION['login_msg'] = ['', 'No account found for this email address. Please contact an administrator.', 'danger'];
header('Location: ' . BASEURL . '/login/');
exit;
}
// Step 4: Sync admin status from groups claim.
$groups = $oidc->requestUserInfo('groups');
$should_be_admin = is_array($groups) && in_array(OIDC_ADMIN_GROUP, $groups);
if ($should_be_admin !== $user->isAdmin())
$user->update(['is_admin' => $should_be_admin ? 1 : 0]);
// Set session and redirect.
$_SESSION['user_id'] = $user->getUserId();
if (!empty($_SESSION['oidc_redirect_url']))
@@ -117,36 +77,4 @@ class OIDCLogin
exit;
}
private function generateSlug($first_name, $last_name)
{
$base = trim($first_name . ' ' . $last_name);
if (empty($base))
$base = 'user';
$slug = strtolower(preg_replace('/[^a-zA-Z0-9]+/', '-', $base));
$slug = trim($slug, '-');
if (empty($slug))
$slug = 'user';
// Check if slug is available.
$candidate = $slug;
for ($i = 0; $i < 10; $i++)
{
try
{
Member::fromSlug($candidate);
// Slug taken — append random suffix.
$candidate = $slug . '-' . bin2hex(random_bytes(3));
}
catch (NotFoundException $e)
{
// Slug is available.
return $candidate;
}
}
// Fallback: use sub-based slug.
return 'user-' . bin2hex(random_bytes(4));
}
}

27
flake.lock generated
View File

@@ -1,27 +0,0 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1770843696,
"narHash": "sha256-LovWTGDwXhkfCOmbgLVA10bvsi/P8eDDpRudgk68HA8=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "2343bbb58f99267223bc2aac4fc9ea301a155a16",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

View File

@@ -1,47 +0,0 @@
{
description = "HashRU Pics dev environment";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
};
outputs = { self, nixpkgs }:
let
forAllSystems = f: nixpkgs.lib.genAttrs [
"x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin"
] (system: f nixpkgs.legacyPackages.${system});
php = pkgs: pkgs.php.buildEnv {
extensions = { enabled, all }: enabled ++ (with all; [
imagick
pdo_mysql
pdo_sqlite
]);
extraConfig = ''
memory_limit = 256M
upload_max_filesize = 50M
post_max_size = 50M
'';
};
in
{
devShells = forAllSystems (pkgs: {
default = pkgs.mkShell {
buildInputs = [
(php pkgs)
(php pkgs).packages.composer
pkgs.sqlite
];
shellHook = ''
export COMPOSER_HOME="$PWD/.composer"
if [ ! -d vendor ]; then
echo "Running composer install..."
composer install
fi
'';
};
});
};
}

View File

@@ -1,2 +0,0 @@
ALTER TABLE users ADD COLUMN oidc_sub TEXT;
CREATE UNIQUE INDEX idx_users_oidc_sub ON users (oidc_sub);

View File

@@ -10,6 +10,7 @@ class AssetIterator implements Iterator
{
private $direction;
private $return_format;
private $rowCount;
private $assets_iterator;
private $meta_iterator;
@@ -20,6 +21,7 @@ class AssetIterator implements Iterator
{
$this->direction = $direction;
$this->return_format = $return_format;
$this->rowCount = $stmt_assets->rowCount();
$this->assets_iterator = new CachedPDOIterator($stmt_assets);
$this->assets_iterator->rewind();
@@ -207,7 +209,7 @@ class AssetIterator implements Iterator
public function num(): int
{
return count($this->assets_iterator);
return $this->rowCount;
}
public function rewind(): void

View File

@@ -9,36 +9,19 @@
class Database
{
private $connection;
private $driver;
private $query_count = 0;
private $logged_queries = [];
public function __construct($driver, array $options)
public function __construct($host, $user, $password, $name)
{
$this->driver = $driver;
try
{
if ($driver === 'sqlite')
{
$this->connection = new PDO("sqlite:" . $options['file'], null, null, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
$this->connection->exec('PRAGMA journal_mode=WAL');
$this->registerSQLiteFunctions();
}
else
{
$this->connection = new PDO(
"mysql:host={$options['host']};dbname={$options['name']};charset=utf8mb4",
$options['user'], $options['password'], [
$this->connection = new PDO("mysql:host=$host;dbname=$name;charset=utf8mb4", $user, $password, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]);
}
}
// Give up if we have a connection error.
catch (PDOException $e)
{
@@ -48,54 +31,6 @@ class Database
}
}
private function registerSQLiteFunctions()
{
$pdo = $this->connection;
$pdo->sqliteCreateFunction('CONCAT', function () {
return implode('', func_get_args());
}, -1);
$pdo->sqliteCreateFunction('IF', function ($cond, $t, $f) {
return $cond ? $t : $f;
}, 3);
$pdo->sqliteCreateFunction('FROM_UNIXTIME', function ($ts) {
return date('Y-m-d H:i:s', $ts);
}, 1);
$pdo->sqliteCreateFunction('UNIX_TIMESTAMP', function () {
return time();
}, 0);
$pdo->sqliteCreateFunction('CURRENT_TIMESTAMP', function () {
return date('Y-m-d H:i:s');
}, 0);
}
public function getDriver()
{
return $this->driver;
}
private function rewriteForSQLite($sql)
{
// REPLACE INTO → INSERT OR REPLACE INTO
$sql = preg_replace('/\bREPLACE\s+INTO\b/i', 'INSERT OR REPLACE INTO', $sql);
// INSERT IGNORE INTO → INSERT OR IGNORE INTO
$sql = preg_replace('/\bINSERT\s+IGNORE\s+INTO\b/i', 'INSERT OR IGNORE INTO', $sql);
// LIMIT :offset, :limit → LIMIT :limit OFFSET :offset
$sql = preg_replace(
'/\bLIMIT\s+:offset\s*,\s*:limit\b/i',
'LIMIT :limit OFFSET :offset',
$sql
);
return $sql;
}
public function getQueryCount()
{
return $this->query_count;
@@ -245,10 +180,6 @@ class Database
// Preprocessing/checks: prepare any arrays for binding
$db_string = $this->expandPlaceholders($db_string, $db_values);
// SQLite query rewriting
if ($this->driver === 'sqlite')
$db_string = $this->rewriteForSQLite($db_string);
// Prepare query for execution
$statement = $this->connection->prepare($db_string);
@@ -292,10 +223,13 @@ class Database
{
$res = $this->query($db_string, $db_values);
if (!$res || $this->rowCount($res) === 0)
return null;
$object = $this->fetchObject($res, $class);
$this->free($res);
return $object ?: null;
return $object;
}
/**
@@ -305,6 +239,9 @@ class Database
{
$res = $this->query($db_string, $db_values);
if (!$res || $this->rowCount($res) === 0)
return [];
$rows = [];
while ($object = $this->fetchObject($res, $class))
$rows[] = $object;
@@ -321,10 +258,13 @@ class Database
{
$res = $this->query($db_string, $db_values);
if ($this->rowCount($res) === 0)
return [];
$row = $this->fetchNum($res);
$this->free($res);
return $row ?: [];
return $row;
}
/**
@@ -334,6 +274,9 @@ class Database
{
$res = $this->query($db_string, $db_values);
if ($this->rowCount($res) === 0)
return [];
$rows = [];
while ($row = $this->fetchNum($res))
$rows[] = $row;
@@ -350,6 +293,9 @@ class Database
{
$res = $this->query($db_string, $db_values);
if ($this->rowCount($res) === 0)
return [];
$rows = [];
while ($row = $this->fetchNum($res))
$rows[$row[0]] = $row[1];
@@ -366,6 +312,9 @@ class Database
{
$res = $this->query($db_string, $db_values);
if (!$res || $this->rowCount($res) === 0)
return [];
$rows = [];
while ($row = $this->fetchAssoc($res))
{
@@ -385,10 +334,13 @@ class Database
{
$res = $this->query($db_string, $db_values);
if ($this->rowCount($res) === 0)
return [];
$row = $this->fetchAssoc($res);
$this->free($res);
return $row ?: [];
return $row;
}
/**
@@ -398,6 +350,9 @@ class Database
{
$res = $this->query($db_string, $db_values);
if ($this->rowCount($res) === 0)
return [];
$rows = [];
while ($row = $this->fetchAssoc($res))
$rows[] = $row;
@@ -414,13 +369,14 @@ class Database
{
$res = $this->query($db_string, $db_values);
$row = $this->fetchNum($res);
$this->free($res);
if (!$row)
// If this happens, you're doing it wrong.
if ($this->rowCount($res) === 0)
return null;
return $row[0];
list($value) = $this->fetchNum($res);
$this->free($res);
return $value;
}
/**
@@ -430,6 +386,9 @@ class Database
{
$res = $this->query($db_string, $db_values);
if ($this->rowCount($res) === 0)
return [];
$rows = [];
while ($row = $this->fetchNum($res))
$rows[] = $row[0];
@@ -453,9 +412,6 @@ class Database
$data = [$data];
// Determine the method of insertion.
if ($this->driver === 'sqlite')
$method = $method == 'replace' ? 'INSERT OR REPLACE' : ($method == 'ignore' ? 'INSERT OR IGNORE' : 'INSERT');
else
$method = $method == 'replace' ? 'REPLACE' : ($method == 'ignore' ? 'INSERT IGNORE' : 'INSERT');
// What columns are we inserting?

View File

@@ -90,7 +90,7 @@ class Dispatcher
private static function trigger404()
{
http_response_code(404);
self::errorPage('Page not found!', 'The page you requested could not be found.');
exit;
$page = new ViewErrorPage('Page not found!');
$page->showContent();
}
}

View File

@@ -123,7 +123,7 @@ class ErrorHandler
'debug_info' => $debug_info,
'file' => str_replace(BASEDIR, '', $file),
'line' => $line,
'id_user' => Registry::has('user') && Registry::get('user')->getUserId() ? Registry::get('user')->getUserId() : 0,
'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'] : '',
]))

View File

@@ -27,15 +27,6 @@ class Member extends User
['email_address' => $email_address]);
}
public static function fromOidcSub($sub)
{
return Registry::get('db')->queryObject(static::class, '
SELECT *
FROM users
WHERE oidc_sub = :oidc_sub',
['oidc_sub' => $sub]);
}
public static function fromId($id_user)
{
$row = Registry::get('db')->queryAssoc('
@@ -82,27 +73,18 @@ class Member extends User
'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,
'reset_key' => '',
];
// Password is required unless oidc_sub is provided.
if (!empty($data['password']))
$new_user['password_hash'] = Authentication::computeHash($data['password']);
elseif (!empty($data['oidc_sub']))
$new_user['password_hash'] = '';
else
$error |= true;
if (!empty($data['oidc_sub']))
$new_user['oidc_sub'] = $data['oidc_sub'];
if ($error)
return false;
$columns = [
$db = Registry::get('db');
$bool = $db->insert('insert', 'users', [
'first_name' => 'string-30',
'surname' => 'string-60',
'slug' => 'string-90',
@@ -111,14 +93,8 @@ class Member extends User
'creation_time' => 'int',
'ip_address' => 'string-45',
'is_admin' => 'int',
'reset_key' => 'string-16',
];
if (isset($new_user['oidc_sub']))
$columns['oidc_sub'] = 'string-255';
$db = Registry::get('db');
$bool = $db->insert('insert', 'users', $columns, $new_user, ['id_user']);
'reset_key' => 'string-16'
], $new_user, ['id_user']);
if (!$bool)
return false;
@@ -137,7 +113,7 @@ class Member extends User
{
foreach ($new_data as $key => $value)
{
if (in_array($key, ['first_name', 'surname', 'slug', 'emailaddress', 'oidc_sub']))
if (in_array($key, ['first_name', 'surname', 'slug', 'emailaddress']))
$this->$key = $value;
elseif ($key === 'password')
$this->password_hash = Authentication::computeHash($value);
@@ -156,8 +132,7 @@ class Member extends User
slug = :slug,
emailaddress = :emailaddress,
password_hash = :password_hash,
is_admin = :is_admin,
oidc_sub = :oidc_sub
is_admin = :is_admin
WHERE id_user = :id_user',
get_object_vars($this));
}

View File

@@ -13,7 +13,7 @@ class Router
$possibleActions = [
'accountsettings' => 'AccountSettings',
'addalbum' => 'EditAlbum',
'albums' => 'ViewPhotoAlbum',
'albums' => 'ViewPhotoAlbums',
'editalbum' => 'EditAlbum',
'editasset' => 'EditAsset',
'edittag' => 'EditTag',
@@ -55,7 +55,7 @@ class Router
return new GenerateThumbnail();
}
// Look for particular actions...
elseif (preg_match('~^/(?<action>[a-z]+)(?:/page/(?<page>\d+))?/?$~', $_SERVER['PATH_INFO'], $path) && isset($possibleActions[$path['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']]();

View File

@@ -122,12 +122,10 @@ class Tag
public static function getAlbums($id_parent = 0, $offset = 0, $limit = 24, $return_format = 'array')
{
$parent_clause = empty($id_parent) ? '(id_parent = :id_parent OR id_parent IS NULL)' : 'id_parent = :id_parent';
$rows = Registry::get('db')->queryAssocs('
SELECT *
FROM tags
WHERE ' . $parent_clause . ' AND kind = :kind
WHERE id_parent = :id_parent AND kind = :kind
ORDER BY tag ASC
LIMIT :offset, :limit',
[
@@ -165,12 +163,10 @@ class Tag
public static function getPeople($id_parent = 0, $offset = 0, $limit = 24, $return_format = 'array')
{
$parent_clause = empty($id_parent) ? '(id_parent = :id_parent OR id_parent IS NULL)' : 'id_parent = :id_parent';
$rows = Registry::get('db')->queryAssocs('
SELECT *
FROM tags
WHERE ' . $parent_clause . ' AND kind = :kind
WHERE id_parent = :id_parent AND kind = :kind
ORDER BY tag ASC
LIMIT :offset, :limit',
[
@@ -253,21 +249,7 @@ class Tag
public static function recount(array $id_tags = [])
{
$db = Registry::get('db');
if ($db->getDriver() === 'sqlite')
{
return $db->query('
UPDATE tags SET count = (
SELECT COUNT(*)
FROM `assets_tags` AS at
WHERE at.id_tag = tags.id_tag
)' . (!empty($id_tags) ? '
WHERE tags.id_tag IN(@id_tags)' : ''),
['id_tags' => $id_tags]);
}
return $db->query('
return Registry::get('db')->query('
UPDATE tags AS t SET count = (
SELECT COUNT(*)
FROM `assets_tags` AS at
@@ -290,18 +272,6 @@ class Tag
if (!isset($data['count']))
$data['count'] = 0;
if ($db->getDriver() === 'sqlite')
{
$res = $db->query('
INSERT INTO tags
(id_parent, tag, slug, kind, description, count)
VALUES
(:id_parent, :tag, :slug, :kind, :description, :count)
ON CONFLICT(slug) DO UPDATE SET count = count + 1',
$data);
}
else
{
$res = $db->query('
INSERT IGNORE INTO tags
(id_parent, tag, slug, kind, description, count)
@@ -309,7 +279,6 @@ class Tag
(:id_parent, :tag, :slug, :kind, :description, :count)
ON DUPLICATE KEY UPDATE count = count + 1',
$data);
}
if (!$res)
throw new Exception('Could not create the requested tag.');
@@ -325,8 +294,6 @@ class Tag
public function save()
{
$vars = get_object_vars($this);
return Registry::get('db')->query('
UPDATE tags
SET
@@ -339,7 +306,7 @@ class Tag
description = :description,
count = :count
WHERE id_tag = :id_tag',
$vars);
get_object_vars($this));
}
public function delete()
@@ -494,12 +461,10 @@ class Tag
$albums_by_parent = [];
while ($row = $db->fetchAssoc($res))
{
$parent = $row['id_parent'];
if (!isset($albums_by_parent[$row['id_parent']]))
$albums_by_parent[$row['id_parent']] = [];
if (!isset($albums_by_parent[$parent]))
$albums_by_parent[$parent] = [];
$albums_by_parent[$parent][] = $row + ['children' => []];
$albums_by_parent[$row['id_parent']][] = $row + ['children' => []];
}
$albums = self::getChildrenRecursively(0, 0, $albums_by_parent);

View File

@@ -24,7 +24,6 @@ abstract class User
protected $is_admin;
protected $reset_key;
protected $reset_blocked_until;
protected $oidc_sub;
protected bool $is_logged;
protected bool $is_guest;

View File

@@ -30,38 +30,21 @@ function disableKeyDownPropagation(obj) {
function enableTouchNavigation() {
var x_down = null;
var y_down = null;
var cancelled = false;
document.addEventListener('touchstart', function(event) {
if (event.touches.length > 1) {
cancelled = true;
return;
}
x_down = event.touches[0].clientX;
y_down = event.touches[0].clientY;
cancelled = false;
}, false);
document.addEventListener('touchmove', function(event) {
if (event.touches.length > 1) {
cancelled = true;
}
}, false);
document.addEventListener('touchend', function(event) {
if (cancelled || x_down === null || y_down === null) {
x_down = null;
y_down = null;
if (!x_down || !y_down) {
return;
}
var x_diff = x_down - event.changedTouches[0].clientX;
var y_diff = y_down - event.changedTouches[0].clientY;
var x_diff = x_down - event.touches[0].clientX;
var y_diff = y_down - event.touches[0].clientY;
x_down = null;
y_down = null;
if (Math.abs(x_diff) < 40 || Math.abs(y_diff) > 50) {
if (Math.abs(y_diff) > 50) {
return;
}
@@ -69,11 +52,13 @@ function enableTouchNavigation() {
if (x_diff > 0) {
var target = document.getElementById("previous_photo").href;
if (target) {
event.preventDefault();
document.location.href = target + '#photo_frame';
}
} else {
var target = document.getElementById("next_photo").href;
if (target) {
event.preventDefault();
document.location.href = target + '#photo_frame';
}
}

View File

@@ -1,104 +0,0 @@
-- SQLite schema for Kabuki CMS / pics
--
-- Usage:
-- sqlite3 data/pics.sqlite < schema.sqlite.sql
--
-- Config (add to config.php):
-- define('DB_DRIVER', 'sqlite');
-- define('DB_FILE', __DIR__ . '/data/pics.sqlite');
CREATE TABLE IF NOT EXISTS users (
id_user INTEGER PRIMARY KEY,
first_name TEXT NOT NULL,
surname TEXT NOT NULL,
slug TEXT NOT NULL UNIQUE,
emailaddress TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
creation_time INTEGER NOT NULL,
last_action_time INTEGER,
ip_address TEXT,
is_admin INTEGER NOT NULL DEFAULT 0,
reset_key TEXT,
reset_blocked_until INTEGER,
oidc_sub TEXT UNIQUE
);
CREATE TABLE IF NOT EXISTS assets (
id_asset INTEGER PRIMARY KEY,
id_user_uploaded INTEGER NOT NULL,
subdir TEXT NOT NULL,
filename TEXT NOT NULL,
title TEXT,
slug TEXT UNIQUE,
mimetype TEXT,
image_width INTEGER,
image_height INTEGER,
date_captured TEXT,
priority INTEGER DEFAULT 0
);
CREATE TABLE IF NOT EXISTS assets_meta (
id_asset INTEGER NOT NULL,
variable TEXT NOT NULL,
value TEXT,
PRIMARY KEY (id_asset, variable)
);
CREATE TABLE IF NOT EXISTS assets_thumbs (
id_asset INTEGER NOT NULL,
width INTEGER NOT NULL,
height INTEGER NOT NULL,
mode TEXT,
filename TEXT,
PRIMARY KEY (id_asset, width, height, mode)
);
CREATE TABLE IF NOT EXISTS tags (
id_tag INTEGER PRIMARY KEY,
id_parent INTEGER,
id_asset_thumb INTEGER,
id_user_owner INTEGER,
tag TEXT NOT NULL,
slug TEXT NOT NULL UNIQUE,
description TEXT,
kind TEXT NOT NULL DEFAULT 'Tag',
count INTEGER DEFAULT 0
);
CREATE TABLE IF NOT EXISTS assets_tags (
id_asset INTEGER NOT NULL,
id_tag INTEGER NOT NULL,
PRIMARY KEY (id_asset, id_tag)
);
CREATE TABLE IF NOT EXISTS posts_assets (
id_post INTEGER NOT NULL,
id_asset INTEGER NOT NULL,
PRIMARY KEY (id_post, id_asset)
);
CREATE TABLE IF NOT EXISTS posts_tags (
id_post INTEGER NOT NULL,
id_tag INTEGER NOT NULL,
PRIMARY KEY (id_post, id_tag)
);
CREATE TABLE IF NOT EXISTS settings (
id_user INTEGER NOT NULL,
variable TEXT NOT NULL,
value TEXT,
time_set TEXT DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id_user, variable)
);
CREATE TABLE IF NOT EXISTS log_errors (
id_entry INTEGER PRIMARY KEY,
id_user INTEGER,
message TEXT,
debug_info TEXT,
file TEXT,
line INTEGER,
request_uri TEXT,
time TEXT DEFAULT CURRENT_TIMESTAMP,
ip_address TEXT
);

View File

@@ -1,58 +0,0 @@
<?php
/*****************************************************************************
* seed.php
* Seeds a fresh database with an admin user and root album.
*
* Usage: php seed.php
*****************************************************************************/
require_once __DIR__ . '/config.php';
require_once __DIR__ . '/vendor/autoload.php';
if (!defined('DB_DRIVER') || DB_DRIVER !== 'sqlite')
{
echo "Error: seed.php currently only supports SQLite.\n";
echo "Set DB_DRIVER to 'sqlite' in config.php.\n";
exit(1);
}
if (!file_exists(DB_FILE))
{
echo "Error: database file not found at " . DB_FILE . "\n";
echo "Create it first: sqlite3 " . DB_FILE . " < schema.sqlite.sql\n";
exit(1);
}
$db = new Database('sqlite', ['file' => DB_FILE]);
Registry::set('db', $db);
// Create admin user.
$password = 'admin';
$hash = password_hash($password, PASSWORD_DEFAULT);
$db->insert('insert', 'users', [], [
'first_name' => 'Admin',
'surname' => 'User',
'slug' => 'admin',
'emailaddress' => 'admin@localhost',
'password_hash' => $hash,
'creation_time' => time(),
'ip_address' => '',
'is_admin' => 1,
'reset_key' => '',
]);
echo "Created admin user (admin@localhost / admin)\n";
// Create root album (id_tag = 1).
$db->insert('insert', 'tags', [], [
'id_parent' => 0,
'tag' => 'Albums',
'slug' => 'albums',
'kind' => 'Album',
'description' => '',
'count' => 0,
]);
echo "Created root album (id_tag = 1)\n";
echo "\nDone. You can now log in at the web UI.\n";