mirror of
https://github.com/Terranom674/Piwigo_Bratonien_Tools.git
synced 2026-09-19 17:34:31 +00:00
Compare commits
95 Commits
ceea3b5483
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
157c6724ea | ||
|
|
bfc1a679db | ||
|
|
a6a2f65f72 | ||
|
|
940a5e3124 | ||
|
|
1a119aeb70 | ||
|
|
a99aee3bcd | ||
|
|
de193ca491 | ||
|
|
51e864060b | ||
|
|
c10c188587 | ||
|
|
ac0be6dcf7 | ||
|
|
c4813161cd | ||
|
|
d97c828066 | ||
|
|
1cff325e3c | ||
|
|
776c5842ca | ||
|
|
a5de1209d9 | ||
|
|
6cb6cd29e2 | ||
|
|
d50cdaaccd | ||
|
|
65cfc0ae97 | ||
|
|
6a985219e9 | ||
|
|
8149793149 | ||
|
|
36bb76f97e | ||
|
|
675815e7c0 | ||
|
|
e5fb8ad90a | ||
|
|
38bff89484 | ||
|
|
3102a3a3f5 | ||
|
|
6e1cf27ef1 | ||
|
|
3af01bc812 | ||
|
|
e58ebc5538 | ||
|
|
eb002b007f | ||
|
|
6983a98875 | ||
|
|
9f8a2569b2 | ||
|
|
5a9980de24 | ||
|
|
50f4343029 | ||
|
|
bd51c27f5b | ||
|
|
7dc8348fd0 | ||
|
|
0fa552285e | ||
|
|
5d995dcca1 | ||
|
|
c06b9fc476 | ||
|
|
c96046b0ca | ||
|
|
d039416dc5 | ||
|
|
f9aa1f1a68 | ||
|
|
15059f5b68 | ||
|
|
136972b0eb | ||
|
|
30967247c0 | ||
|
|
6826963712 | ||
|
|
dc01294bdb | ||
|
|
30b5207c2b | ||
|
|
48ed46b1f0 | ||
|
|
4dfc1c871c | ||
|
|
6ccc61eb40 | ||
|
|
2f976aa9ed | ||
|
|
a0fa4dfd1a | ||
|
|
839683a260 | ||
|
|
92724aa9a2 | ||
|
|
e7b0ecd85d | ||
|
|
11d2b85948 | ||
|
|
05a5013b8a | ||
|
|
acbf316f58 | ||
|
|
930399c847 | ||
|
|
a441ad2991 | ||
|
|
d0f13a6d1f | ||
|
|
efff4360f7 | ||
|
|
6cfcfd624e | ||
|
|
7fb3969647 | ||
|
|
d006217660 | ||
|
|
3662bdefc2 | ||
|
|
3d66bd1137 | ||
|
|
21cbe59b55 | ||
|
|
420ec47a51 | ||
|
|
0c8065a01c | ||
|
|
7015d822cd | ||
|
|
1c1d29af07 | ||
|
|
074a74af1f | ||
|
|
fa86adc1d2 | ||
|
|
289eea396e | ||
|
|
a386c71c3c | ||
|
|
9a4d39af1a | ||
|
|
958c9242ff | ||
|
|
8d5e99389c | ||
|
|
5a35244029 | ||
|
|
d87c395be4 | ||
|
|
e8cf6cc6c8 | ||
|
|
3d654d30cf | ||
|
|
b9140103ec | ||
|
|
86a2a0fbec | ||
|
|
c058e48fb0 | ||
|
|
d20be6a39a | ||
|
|
3d7114d782 | ||
|
|
c20795d108 | ||
|
|
2863a51acf | ||
|
|
ec8036d226 | ||
|
|
0094942f68 | ||
|
|
12834041be | ||
|
|
1f2651b8f0 | ||
|
|
791d67929b |
82
.github/workflows/lint.yml
vendored
82
.github/workflows/lint.yml
vendored
@@ -1,82 +0,0 @@
|
||||
name: Syntax Check
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
php-syntax:
|
||||
name: PHP ${{ matrix.php }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
php: ['8.2', '8.3', '8.4', '8.5']
|
||||
steps:
|
||||
- name: Repository auschecken
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: PHP installieren
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
coverage: none
|
||||
|
||||
- name: PHP Syntax pruefen
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
while IFS= read -r -d '' file; do
|
||||
php -l "$file"
|
||||
done < <(find . -type f -name '*.php' -print0)
|
||||
|
||||
- name: Doppelte Bratonien-Funktionen pruefen
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
python3 - <<'PY'
|
||||
import pathlib
|
||||
import re
|
||||
import sys
|
||||
|
||||
found = {}
|
||||
for path in pathlib.Path('.').rglob('*.php'):
|
||||
text = path.read_text(encoding='utf-8', errors='ignore')
|
||||
for name in re.findall(r'\bfunction\s+(bratonien_tools_[A-Za-z0-9_]+)\s*\(', text):
|
||||
found.setdefault(name, []).append(str(path))
|
||||
|
||||
duplicates = {name: paths for name, paths in found.items() if len(paths) > 1}
|
||||
if duplicates:
|
||||
for name, paths in sorted(duplicates.items()):
|
||||
print(f'{name}: {", ".join(paths)}', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
PY
|
||||
|
||||
script-syntax:
|
||||
name: JavaScript, Python, Shell
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Repository auschecken
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: JavaScript Syntax pruefen
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
while IFS= read -r -d '' file; do
|
||||
node --check "$file"
|
||||
done < <(find . -type f -name '*.js' -print0)
|
||||
|
||||
- name: Python Syntax pruefen
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
python3 -m compileall -q runtime
|
||||
|
||||
- name: Shell Syntax pruefen
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
while IFS= read -r -d '' file; do
|
||||
bash -n "$file"
|
||||
done < <(find . -type f -name '*.sh' -print0)
|
||||
44
admin.php
44
admin.php
@@ -110,6 +110,47 @@ $engine = bratonien_tools_get_watermark_engine_config();
|
||||
$cache_workers = bratonien_tools_get_cache_worker_settings();
|
||||
$public_selection = bratonien_tools_get_public_selection_settings();
|
||||
$public_selection_groups = bratonien_tools_get_piwigo_groups();
|
||||
$customer_qr = bratonien_tools_customer_qr_admin_data();
|
||||
$customer_qr['friendship_codes'] = bratonien_tools_friendship_code_admin_data();
|
||||
|
||||
$qr_present = array();
|
||||
foreach ($customer_qr['uploads'] as $qr_upload)
|
||||
{
|
||||
$qr_year = (int)$qr_upload['year'];
|
||||
$qr_number = (int)$qr_upload['number'];
|
||||
if ($qr_year < 1 || $qr_number < 1)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (!isset($qr_present[$qr_year]))
|
||||
{
|
||||
$qr_present[$qr_year] = array();
|
||||
}
|
||||
$qr_present[$qr_year][$qr_number] = $qr_upload;
|
||||
}
|
||||
|
||||
$qr_batch_base = get_absolute_root_url(true).'plugins/'.BRATONIEN_TOOLS_ID.'/customer-qr-admin-batch.php';
|
||||
$customer_qr['batch_download_available'] = class_exists('ZipArchive');
|
||||
$customer_qr['batch_download_url'] = $qr_batch_base;
|
||||
foreach ($customer_qr['years'] as &$qr_year)
|
||||
{
|
||||
$year = (int)$qr_year['year'];
|
||||
$capacity = (int)$qr_year['capacity'];
|
||||
$qr_year['batch_download_url'] = $qr_batch_base.'?year='.$year;
|
||||
$qr_year['slots'] = array();
|
||||
|
||||
for ($number = 1; $number <= $capacity; $number++)
|
||||
{
|
||||
$present = isset($qr_present[$year][$number]);
|
||||
$qr_year['slots'][] = array(
|
||||
'number' => $number,
|
||||
'present' => $present,
|
||||
'preview_url' => $present ? (string)$qr_present[$year][$number]['preview_url'] : '',
|
||||
);
|
||||
}
|
||||
}
|
||||
unset($qr_year);
|
||||
|
||||
$assets = bratonien_tools_get_assets();
|
||||
$asset_environment = bratonien_tools_get_asset_environment();
|
||||
$album_shares = bratonien_tools_get_album_shares();
|
||||
@@ -244,6 +285,7 @@ $template->assign(array(
|
||||
'CACHE_WORKERS' => $cache_workers,
|
||||
'PUBLIC_SELECTION' => $public_selection,
|
||||
'PUBLIC_SELECTION_GROUPS' => $public_selection_groups,
|
||||
'CUSTOMER_QR' => $customer_qr,
|
||||
'BRATONIEN_ASSETS' => $assets,
|
||||
'ASSET_ENV' => $asset_environment,
|
||||
'BRATONIEN_ALBUM_SHARES' => $album_shares,
|
||||
@@ -261,6 +303,7 @@ $template->set_filename('admin_tabs_content', BRATONIEN_TOOLS_PATH . 'template/a
|
||||
$template->set_filename('plugin_admin_content', BRATONIEN_TOOLS_PATH . 'template/admin.tpl');
|
||||
$template->set_filename('webdav_warmup_admin_content', BRATONIEN_TOOLS_PATH . 'template/webdav_warmup_admin.tpl');
|
||||
$template->set_filename('public_selection_admin_content', BRATONIEN_TOOLS_PATH . 'template/public_selection_admin.tpl');
|
||||
$template->set_filename('customer_qr_admin_content', BRATONIEN_TOOLS_PATH . 'template/customer_qr_admin.tpl');
|
||||
$template->set_filename('asset_manager_admin_content', BRATONIEN_TOOLS_PATH . 'template/asset_manager_admin.tpl');
|
||||
$template->set_filename('album_shares_admin_content', BRATONIEN_TOOLS_PATH . 'template/album_shares_admin.tpl');
|
||||
$template->set_filename('nc_connector_admin_content', BRATONIEN_TOOLS_PATH . 'template/nc_connector_admin.tpl');
|
||||
@@ -270,6 +313,7 @@ $admin_content = $template->parse('admin_tabs_content', true);
|
||||
$admin_content .= $template->parse('plugin_admin_content', true);
|
||||
$admin_content .= $template->parse('webdav_warmup_admin_content', true);
|
||||
$admin_content .= $template->parse('public_selection_admin_content', true);
|
||||
$admin_content .= $template->parse('customer_qr_admin_content', true);
|
||||
$admin_content .= $template->parse('asset_manager_admin_content', true);
|
||||
$admin_content .= $template->parse('album_shares_admin_content', true);
|
||||
$admin_content .= $template->parse('nc_connector_admin_content', true);
|
||||
|
||||
122
customer-qr-admin-batch.php
Normal file
122
customer-qr-admin-batch.php
Normal file
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
$piwigo_root = realpath(dirname(__DIR__, 2));
|
||||
if ($piwigo_root === false)
|
||||
{
|
||||
http_response_code(500);
|
||||
exit;
|
||||
}
|
||||
|
||||
define('PHPWG_ROOT_PATH', rtrim($piwigo_root, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR);
|
||||
include_once(PHPWG_ROOT_PATH.'include/common.inc.php');
|
||||
|
||||
if (!defined('BRATONIEN_TOOLS_PATH'))
|
||||
{
|
||||
http_response_code(404);
|
||||
exit;
|
||||
}
|
||||
|
||||
check_status(ACCESS_ADMINISTRATOR);
|
||||
require_once(BRATONIEN_TOOLS_PATH.'include/customer_qr_upload.inc.php');
|
||||
|
||||
if (!class_exists('ZipArchive'))
|
||||
{
|
||||
http_response_code(500);
|
||||
header('Content-Type: text/plain; charset=utf-8');
|
||||
exit('ZIP-Unterstützung ist auf diesem Server nicht verfügbar.');
|
||||
}
|
||||
|
||||
bratonien_tools_customer_qr_ensure_storage();
|
||||
$limits = bratonien_tools_customer_qr_year_limits();
|
||||
$table = bratonien_tools_customer_qr_table();
|
||||
$requested_year = isset($_GET['year']) && $_GET['year'] !== '' ? bratonien_tools_customer_qr_year($_GET['year']) : 0;
|
||||
|
||||
$query = 'SELECT upload_year, code_number, stored_name FROM `'.$table.'`';
|
||||
if ($requested_year > 0)
|
||||
{
|
||||
$query .= ' WHERE upload_year='.(int)$requested_year;
|
||||
}
|
||||
$query .= ' ORDER BY upload_year ASC, CAST(code_number AS UNSIGNED) ASC, id ASC';
|
||||
|
||||
$result = pwg_query($query);
|
||||
$files = array();
|
||||
$root = bratonien_tools_customer_qr_storage_root();
|
||||
while ($row = pwg_db_fetch_assoc($result))
|
||||
{
|
||||
$year = (int)$row['upload_year'];
|
||||
if (!isset($limits[$year]))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$number = bratonien_tools_customer_qr_number_for_year($year, $row['code_number']);
|
||||
$stored_name = (string)$row['stored_name'];
|
||||
if ($stored_name === '' || basename($stored_name) !== $stored_name)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$path = $root.DIRECTORY_SEPARATOR.$year.DIRECTORY_SEPARATOR.$stored_name;
|
||||
if (!is_file($path) || !is_readable($path))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$extension = strtolower((string)pathinfo($stored_name, PATHINFO_EXTENSION));
|
||||
if (!in_array($extension, array('png', 'jpg', 'jpeg', 'webp', 'gif'), true))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$width = max(2, strlen((string)$limits[$year]));
|
||||
$archive_name = 'qr-'.str_pad($number, $width, '0', STR_PAD_LEFT).'.'.$extension;
|
||||
if ($requested_year === 0)
|
||||
{
|
||||
$archive_name = $year.'/'.$archive_name;
|
||||
}
|
||||
|
||||
$files[] = array(
|
||||
'path' => $path,
|
||||
'archive_name' => $archive_name,
|
||||
);
|
||||
}
|
||||
|
||||
if (empty($files))
|
||||
{
|
||||
http_response_code(404);
|
||||
header('Content-Type: text/plain; charset=utf-8');
|
||||
exit('Keine QR-Codes für den Batch-Download vorhanden.');
|
||||
}
|
||||
|
||||
$temp = tempnam(sys_get_temp_dir(), 'bratonien-qr-');
|
||||
if ($temp === false)
|
||||
{
|
||||
http_response_code(500);
|
||||
exit;
|
||||
}
|
||||
|
||||
$zip = new ZipArchive();
|
||||
if ($zip->open($temp, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true)
|
||||
{
|
||||
@unlink($temp);
|
||||
http_response_code(500);
|
||||
exit;
|
||||
}
|
||||
|
||||
foreach ($files as $file)
|
||||
{
|
||||
$zip->addFile($file['path'], $file['archive_name']);
|
||||
}
|
||||
$zip->close();
|
||||
|
||||
$download_name = $requested_year > 0
|
||||
? 'qr-codes-'.$requested_year.'.zip'
|
||||
: 'qr-codes-alle-jahre.zip';
|
||||
|
||||
header('Content-Type: application/zip');
|
||||
header('Content-Length: '.filesize($temp));
|
||||
header('Content-Disposition: attachment; filename="'.$download_name.'"');
|
||||
header('Cache-Control: private, no-store, max-age=0');
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
readfile($temp);
|
||||
@unlink($temp);
|
||||
exit;
|
||||
70
customer-qr-admin-file.php
Normal file
70
customer-qr-admin-file.php
Normal file
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
$piwigo_root = realpath(dirname(__DIR__, 2));
|
||||
if ($piwigo_root === false)
|
||||
{
|
||||
http_response_code(500);
|
||||
exit;
|
||||
}
|
||||
|
||||
define('PHPWG_ROOT_PATH', rtrim($piwigo_root, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR);
|
||||
include_once(PHPWG_ROOT_PATH.'include/common.inc.php');
|
||||
|
||||
if (!defined('BRATONIEN_TOOLS_PATH'))
|
||||
{
|
||||
http_response_code(404);
|
||||
exit;
|
||||
}
|
||||
|
||||
check_status(ACCESS_ADMINISTRATOR);
|
||||
require_once(BRATONIEN_TOOLS_PATH.'include/customer_qr_upload.inc.php');
|
||||
|
||||
$id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
|
||||
if ($id < 1)
|
||||
{
|
||||
http_response_code(404);
|
||||
exit;
|
||||
}
|
||||
|
||||
$table = bratonien_tools_customer_qr_table();
|
||||
$result = pwg_query('SELECT id, upload_year, code_number, stored_name, mime_type FROM `'.$table.'` WHERE id='.$id.' LIMIT 1');
|
||||
$row = pwg_db_fetch_assoc($result);
|
||||
if (!$row)
|
||||
{
|
||||
http_response_code(404);
|
||||
exit;
|
||||
}
|
||||
|
||||
$year = (int)$row['upload_year'];
|
||||
$number = (string)$row['code_number'];
|
||||
$stored_name = (string)$row['stored_name'];
|
||||
$mime = strtolower((string)$row['mime_type']);
|
||||
$allowed_mimes = array('image/png', 'image/jpeg', 'image/webp', 'image/gif');
|
||||
|
||||
if (
|
||||
$stored_name === ''
|
||||
|| basename($stored_name) !== $stored_name
|
||||
|| !in_array($mime, $allowed_mimes, true)
|
||||
)
|
||||
{
|
||||
http_response_code(404);
|
||||
exit;
|
||||
}
|
||||
|
||||
$path = bratonien_tools_customer_qr_storage_root().DIRECTORY_SEPARATOR.$year.DIRECTORY_SEPARATOR.$stored_name;
|
||||
if (!is_file($path) || !is_readable($path))
|
||||
{
|
||||
http_response_code(404);
|
||||
exit;
|
||||
}
|
||||
|
||||
$extension = strtolower((string)pathinfo($stored_name, PATHINFO_EXTENSION));
|
||||
$download = isset($_GET['download']) && (string)$_GET['download'] === '1';
|
||||
$filename = 'qr-'.$year.'-'.$number.'.'.$extension;
|
||||
|
||||
header('Content-Type: '.$mime);
|
||||
header('Content-Length: '.filesize($path));
|
||||
header('Cache-Control: private, no-store, max-age=0');
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
header('Content-Disposition: '.($download ? 'attachment' : 'inline').'; filename="'.$filename.'"');
|
||||
readfile($path);
|
||||
exit;
|
||||
584
customer-qr-upload.php
Normal file
584
customer-qr-upload.php
Normal file
@@ -0,0 +1,584 @@
|
||||
<?php
|
||||
$piwigo_root = realpath(dirname(__DIR__, 2));
|
||||
if ($piwigo_root === false)
|
||||
{
|
||||
http_response_code(500);
|
||||
exit;
|
||||
}
|
||||
define('PHPWG_ROOT_PATH', rtrim($piwigo_root, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR);
|
||||
include_once(PHPWG_ROOT_PATH.'include/common.inc.php');
|
||||
|
||||
if (!defined('BRATONIEN_TOOLS_PATH'))
|
||||
{
|
||||
http_response_code(404);
|
||||
exit('Bratonien Tools ist nicht aktiv.');
|
||||
}
|
||||
|
||||
require_once(BRATONIEN_TOOLS_PATH.'include/customer_qr_upload.inc.php');
|
||||
require_once(BRATONIEN_TOOLS_PATH.'include/friendship_code_upload.inc.php');
|
||||
|
||||
$settings = bratonien_tools_customer_qr_settings();
|
||||
if (empty($settings['enabled']))
|
||||
{
|
||||
http_response_code(404);
|
||||
header('Content-Type: text/html; charset=utf-8');
|
||||
echo '<!doctype html><html lang="de"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>QR-Code Upload</title></head><body><main style="max-width:720px;margin:48px auto;padding:24px;font-family:system-ui,sans-serif"><h1>QR-Code Upload</h1><p>Dieses Formular ist derzeit nicht aktiviert.</p></main></body></html>';
|
||||
exit;
|
||||
}
|
||||
|
||||
bratonien_tools_customer_qr_ensure_storage();
|
||||
bratonien_tools_friendship_code_ensure_storage();
|
||||
$year_limits = bratonien_tools_customer_qr_year_limits();
|
||||
$action = (string)($_GET['action'] ?? '');
|
||||
|
||||
function bratonien_tools_customer_qr_public_inventory($year)
|
||||
{
|
||||
$year = bratonien_tools_customer_qr_year($year);
|
||||
$limits = bratonien_tools_customer_qr_year_limits();
|
||||
$limit = (int)$limits[$year];
|
||||
$present = array();
|
||||
$table = bratonien_tools_customer_qr_table();
|
||||
|
||||
$result = pwg_query(
|
||||
'SELECT code_number FROM `'.$table.'` '
|
||||
.'WHERE upload_year='.(int)$year.' '
|
||||
.'ORDER BY CAST(code_number AS UNSIGNED) ASC'
|
||||
);
|
||||
|
||||
while ($row = pwg_db_fetch_assoc($result))
|
||||
{
|
||||
$number = (int)$row['code_number'];
|
||||
if ($number >= 1 && $number <= $limit)
|
||||
{
|
||||
$present[$number] = true;
|
||||
}
|
||||
}
|
||||
|
||||
$slots = array();
|
||||
for ($number = 1; $number <= $limit; $number++)
|
||||
{
|
||||
$slots[] = array(
|
||||
'number' => $number,
|
||||
'present' => isset($present[$number]),
|
||||
);
|
||||
}
|
||||
|
||||
$present_count = count($present);
|
||||
return array(
|
||||
'year' => $year,
|
||||
'limit' => $limit,
|
||||
'present_count' => $present_count,
|
||||
'missing_count' => max(0, $limit - $present_count),
|
||||
'slots' => $slots,
|
||||
);
|
||||
}
|
||||
|
||||
if ($action === 'inventory')
|
||||
{
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('Cache-Control: no-store, max-age=0');
|
||||
|
||||
try
|
||||
{
|
||||
$inventory = bratonien_tools_customer_qr_public_inventory($_GET['year'] ?? '');
|
||||
echo json_encode(array(
|
||||
'ok' => true,
|
||||
'inventory' => $inventory,
|
||||
), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
catch (Throwable $e)
|
||||
{
|
||||
http_response_code(400);
|
||||
echo json_encode(array('ok' => false, 'message' => $e->getMessage()), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action === 'check')
|
||||
{
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('Cache-Control: no-store, max-age=0');
|
||||
|
||||
try
|
||||
{
|
||||
$year = bratonien_tools_customer_qr_year($_GET['year'] ?? '');
|
||||
$number = bratonien_tools_customer_qr_number_for_year($year, $_GET['number'] ?? '');
|
||||
$exists = bratonien_tools_customer_qr_exists($year, $number);
|
||||
echo json_encode(array(
|
||||
'ok' => true,
|
||||
'available' => !$exists,
|
||||
'year' => $year,
|
||||
'number' => $number,
|
||||
'limit' => (int)$year_limits[$year],
|
||||
'message' => $exists
|
||||
? 'Diese QR-Code-Nummer ist für '.$year.' bereits vorhanden.'
|
||||
: 'Nummer ist für '.$year.' frei.',
|
||||
), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
catch (Throwable $e)
|
||||
{
|
||||
http_response_code(400);
|
||||
echo json_encode(array('ok' => false, 'message' => $e->getMessage()), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action === 'check_friendship')
|
||||
{
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('Cache-Control: no-store, max-age=0');
|
||||
|
||||
try
|
||||
{
|
||||
$name = bratonien_tools_friendship_code_name($_GET['name'] ?? '');
|
||||
$exists = bratonien_tools_friendship_code_exists_for_name($name);
|
||||
echo json_encode(array(
|
||||
'ok' => true,
|
||||
'available' => !$exists,
|
||||
'name' => $name,
|
||||
'message' => $exists
|
||||
? 'Für diesen Namen ist bereits ein Freundschaftscode hinterlegt.'
|
||||
: 'Für diesen Namen kann ein Freundschaftscode hinterlegt werden.',
|
||||
), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
catch (Throwable $e)
|
||||
{
|
||||
http_response_code(400);
|
||||
echo json_encode(array('ok' => false, 'message' => $e->getMessage()), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST')
|
||||
{
|
||||
$results = array();
|
||||
$year = bratonien_tools_customer_qr_default_year();
|
||||
$upload_type = (string)($_POST['upload_type'] ?? 'qr');
|
||||
|
||||
try
|
||||
{
|
||||
if (
|
||||
empty($_POST)
|
||||
&& empty($_FILES)
|
||||
&& !empty($_SERVER['CONTENT_LENGTH'])
|
||||
)
|
||||
{
|
||||
throw new RuntimeException('Der Upload überschreitet das PHP-Limit post_max_size ('.ini_get('post_max_size').').');
|
||||
}
|
||||
|
||||
if (function_exists('check_pwg_token'))
|
||||
{
|
||||
check_pwg_token();
|
||||
}
|
||||
|
||||
if ($upload_type === 'friendship')
|
||||
{
|
||||
$name = bratonien_tools_friendship_code_name($_POST['friend_name'] ?? '');
|
||||
if (empty($_FILES['friend_file']))
|
||||
{
|
||||
throw new RuntimeException('Es wurde keine Freundschaftscode-Datei ausgewählt.');
|
||||
}
|
||||
|
||||
$results[] = bratonien_tools_friendship_code_process_upload($name, $_FILES['friend_file']);
|
||||
}
|
||||
else
|
||||
{
|
||||
$upload_type = 'qr';
|
||||
$year = bratonien_tools_customer_qr_year($_POST['upload_year'] ?? '');
|
||||
|
||||
if (empty($_FILES['qr_files']))
|
||||
{
|
||||
throw new RuntimeException('Es wurden keine QR-Code-Dateien ausgewählt.');
|
||||
}
|
||||
|
||||
$numbers = isset($_POST['qr_numbers']) && is_array($_POST['qr_numbers'])
|
||||
? array_values($_POST['qr_numbers'])
|
||||
: array();
|
||||
|
||||
$results = bratonien_tools_customer_qr_process_uploads($year, $_FILES['qr_files'], $numbers);
|
||||
}
|
||||
}
|
||||
catch (Throwable $e)
|
||||
{
|
||||
$results[] = array(
|
||||
'status' => 'error',
|
||||
'kind' => $upload_type === 'friendship' ? 'friendship' : 'qr',
|
||||
'file' => '',
|
||||
'name' => $upload_type === 'friendship' ? trim((string)($_POST['friend_name'] ?? '')) : '',
|
||||
'year' => $year,
|
||||
'number' => '',
|
||||
'message' => $e->getMessage(),
|
||||
);
|
||||
}
|
||||
|
||||
$_SESSION['bratonien_customer_qr_flash'] = array(
|
||||
'type' => $upload_type,
|
||||
'year' => $year,
|
||||
'results' => $results,
|
||||
);
|
||||
|
||||
$target = strtok($_SERVER['REQUEST_URI'], '?');
|
||||
header('Location: '.$target, true, 303);
|
||||
exit;
|
||||
}
|
||||
|
||||
$flash = array();
|
||||
if (!empty($_SESSION['bratonien_customer_qr_flash']) && is_array($_SESSION['bratonien_customer_qr_flash']))
|
||||
{
|
||||
$flash = $_SESSION['bratonien_customer_qr_flash'];
|
||||
unset($_SESSION['bratonien_customer_qr_flash']);
|
||||
}
|
||||
|
||||
$selected_year = isset($flash['year']) ? (int)$flash['year'] : bratonien_tools_customer_qr_default_year();
|
||||
if (!isset($year_limits[$selected_year]))
|
||||
{
|
||||
$selected_year = bratonien_tools_customer_qr_default_year();
|
||||
}
|
||||
$selected_limit = (int)$year_limits[$selected_year];
|
||||
$inventory = bratonien_tools_customer_qr_public_inventory($selected_year);
|
||||
$results = isset($flash['results']) && is_array($flash['results']) ? $flash['results'] : array();
|
||||
$token = function_exists('get_pwg_token') ? get_pwg_token() : '';
|
||||
$max_files = max(1, (int)ini_get('max_file_uploads'));
|
||||
$endpoint = strtok($_SERVER['REQUEST_URI'], '?');
|
||||
|
||||
header('Content-Type: text/html; charset=utf-8');
|
||||
header('Cache-Control: no-store, max-age=0');
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>QR-Code Upload</title>
|
||||
<style>
|
||||
:root{color-scheme:light dark;--bg:#111214;--card:#1a1c20;--text:#f2f2f2;--muted:#a9adb5;--border:#383c44;--accent:#d99a43;--ok:#67b779;--bad:#d46b6b;--wait:#c7a65a}
|
||||
*{box-sizing:border-box}
|
||||
body{margin:0;background:var(--bg);color:var(--text);font:16px/1.5 system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}
|
||||
main{max-width:860px;margin:0 auto;padding:34px 18px 60px}
|
||||
h1{font-size:clamp(1.8rem,5vw,2.6rem);margin:0 0 8px}
|
||||
h2{font-size:1.12rem;margin:0 0 12px}
|
||||
p{margin:0 0 16px}.muted{color:var(--muted)}
|
||||
.card{background:var(--card);border:1px solid var(--border);border-radius:14px;padding:20px;margin-top:18px}
|
||||
.grid{display:grid;grid-template-columns:minmax(0,180px) minmax(0,1fr);gap:14px 18px;align-items:center}
|
||||
label{font-weight:650}.control{min-width:0}
|
||||
select,input[type="text"],input[type="file"]{width:100%;font:inherit;color:inherit;background:#121418;border:1px solid var(--border);border-radius:9px;padding:11px 12px}
|
||||
input[type="file"]{padding:9px}
|
||||
input:focus,select:focus,button:focus{outline:2px solid var(--accent);outline-offset:2px}
|
||||
.file-list{display:grid;gap:10px;margin-top:16px}
|
||||
.file-row{display:grid;grid-template-columns:minmax(0,1fr) minmax(120px,180px) minmax(170px,220px);gap:10px;align-items:center;padding:12px;border:1px solid var(--border);border-radius:10px;background:#15171b}
|
||||
.file-name{overflow-wrap:anywhere}.status{font-size:.92rem;color:var(--muted)}.status.ok{color:var(--ok)}.status.bad{color:var(--bad)}.status.wait{color:var(--wait)}
|
||||
.actions{display:flex;gap:12px;align-items:center;flex-wrap:wrap;margin-top:18px}
|
||||
button{border:0;border-radius:9px;padding:12px 18px;background:var(--accent);color:#1a140d;font:inherit;font-weight:750;cursor:pointer}
|
||||
button:disabled{opacity:.45;cursor:not-allowed}
|
||||
.result{padding:12px 14px;border-radius:9px;margin-top:8px;border:1px solid var(--border)}
|
||||
.result.ok{border-color:color-mix(in srgb,var(--ok),transparent 45%)}.result.duplicate,.result.error{border-color:color-mix(in srgb,var(--bad),transparent 45%)}
|
||||
.result strong{display:block}.meta{color:var(--muted);font-size:.92rem}
|
||||
.inventory{margin-top:20px;padding-top:18px;border-top:1px solid var(--border)}
|
||||
.inventory-head{display:flex;align-items:baseline;justify-content:space-between;gap:12px;flex-wrap:wrap;margin-bottom:10px}
|
||||
.inventory-head h2{margin:0}
|
||||
.inventory-legend{display:flex;gap:14px;align-items:center;flex-wrap:wrap;margin-bottom:12px;font-size:.92rem;color:var(--muted)}
|
||||
.inventory-legend span{display:inline-flex;align-items:center;gap:6px}
|
||||
.inventory-dot{width:14px;height:14px;border-radius:3px;border:1px solid}
|
||||
.inventory-dot.present{background:rgba(62,170,94,.30);border-color:#58c878}
|
||||
.inventory-dot.missing{background:rgba(205,72,72,.28);border-color:#db6d6d}
|
||||
.inventory-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(42px,1fr));gap:6px}
|
||||
.inventory-slot{min-height:38px;display:flex;align-items:center;justify-content:center;border:1px solid;border-radius:6px;font-weight:750}
|
||||
.inventory-slot.present{background:rgba(62,170,94,.30);border-color:#58c878;color:#dff7e6}
|
||||
.inventory-slot.missing{background:rgba(205,72,72,.28);border-color:#db6d6d;color:#ffdede}
|
||||
@media(max-width:680px){.grid{grid-template-columns:1fr}.file-row{grid-template-columns:1fr}.card{padding:16px}.inventory-grid{grid-template-columns:repeat(auto-fill,minmax(38px,1fr))}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<header>
|
||||
<h1>QR-Code Upload</h1>
|
||||
<p class="muted">QR-Codes und persönliche Freundschaftscodes hochladen.</p>
|
||||
</header>
|
||||
|
||||
<?php if (!empty($results)): ?>
|
||||
<section class="card" aria-labelledby="upload-result-title">
|
||||
<h2 id="upload-result-title">Ergebnis</h2>
|
||||
<?php foreach ($results as $result): ?>
|
||||
<?php
|
||||
$status = in_array(($result['status'] ?? ''), array('ok','duplicate','error'), true) ? $result['status'] : 'error';
|
||||
$kind = (string)($result['kind'] ?? 'qr');
|
||||
if ($status === 'ok') $headline = 'Gespeichert';
|
||||
elseif ($status === 'duplicate') $headline = $kind === 'friendship' ? 'Nicht gespeichert: bereits vorhanden' : 'Nicht gespeichert: Nummer bereits vergeben';
|
||||
else $headline = 'Nicht gespeichert';
|
||||
?>
|
||||
<div class="result <?php echo htmlspecialchars($status, ENT_QUOTES, 'UTF-8'); ?>">
|
||||
<strong><?php echo htmlspecialchars($headline, ENT_QUOTES, 'UTF-8'); ?></strong>
|
||||
<?php if (!empty($result['file'])): ?><div><?php echo htmlspecialchars($result['file'], ENT_QUOTES, 'UTF-8'); ?></div><?php endif; ?>
|
||||
<?php if ($kind === 'friendship' && !empty($result['name'])): ?><div class="meta">Name: <?php echo htmlspecialchars($result['name'], ENT_QUOTES, 'UTF-8'); ?></div><?php endif; ?>
|
||||
<?php if ($kind !== 'friendship' && !empty($result['number'])): ?><div class="meta">Jahr <?php echo (int)$result['year']; ?> · QR-Code #<?php echo htmlspecialchars($result['number'], ENT_QUOTES, 'UTF-8'); ?></div><?php endif; ?>
|
||||
<div class="meta"><?php echo htmlspecialchars((string)($result['message'] ?? ''), ENT_QUOTES, 'UTF-8'); ?></div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
|
||||
<form class="card" method="post" enctype="multipart/form-data" id="qr-upload-form" novalidate>
|
||||
<input type="hidden" name="pwg_token" value="<?php echo htmlspecialchars($token, ENT_QUOTES, 'UTF-8'); ?>">
|
||||
<input type="hidden" name="upload_type" value="qr">
|
||||
<h2>QR-Code</h2>
|
||||
|
||||
<div class="grid">
|
||||
<label for="upload-year">Jahr</label>
|
||||
<div class="control">
|
||||
<select id="upload-year" name="upload_year" required>
|
||||
<?php foreach ($year_limits as $year => $limit): ?>
|
||||
<option value="<?php echo (int)$year; ?>"<?php echo (int)$year === $selected_year ? ' selected' : ''; ?>><?php echo (int)$year; ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<div id="year-limit-note" class="muted" style="margin-top:6px">Für <?php echo $selected_year; ?> sind QR-Code-Nummern 1 bis <?php echo $selected_limit; ?> vorgesehen.</div>
|
||||
</div>
|
||||
|
||||
<label for="qr-files">QR-Code-Datei(en)</label>
|
||||
<div class="control">
|
||||
<input id="qr-files" name="qr_files[]" type="file" accept="image/png,image/jpeg,image/webp,image/gif" multiple required>
|
||||
<div class="muted" style="margin-top:6px">PNG, JPG, WEBP oder GIF. Batch-Upload bis zum Serverlimit von <?php echo $max_files; ?> Dateien pro Anfrage.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="inventory" aria-labelledby="inventory-title">
|
||||
<div class="inventory-head">
|
||||
<h2 id="inventory-title">QR-Code-Bestand <span id="inventory-year"><?php echo (int)$inventory['year']; ?></span></h2>
|
||||
<span id="inventory-count" class="muted"><?php echo (int)$inventory['present_count']; ?> vorhanden · <?php echo (int)$inventory['missing_count']; ?> fehlen</span>
|
||||
</div>
|
||||
<div class="inventory-legend" aria-hidden="true">
|
||||
<span><i class="inventory-dot present"></i>Vorhanden</span>
|
||||
<span><i class="inventory-dot missing"></i>Fehlt</span>
|
||||
</div>
|
||||
<div id="inventory-grid" class="inventory-grid" aria-live="polite">
|
||||
<?php foreach ($inventory['slots'] as $slot): ?>
|
||||
<span class="inventory-slot <?php echo !empty($slot['present']) ? 'present' : 'missing'; ?>" title="QR-Code #<?php echo (int)$slot['number']; ?> <?php echo !empty($slot['present']) ? 'vorhanden' : 'fehlt'; ?>"><?php echo (int)$slot['number']; ?></span>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div id="file-list" class="file-list" aria-live="polite"></div>
|
||||
|
||||
<div class="actions">
|
||||
<button id="submit-button" type="submit" disabled>QR-Code(s) hochladen</button>
|
||||
<span class="muted">Eine Nummer kann innerhalb eines Jahres nur einmal verwendet werden.</span>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form class="card" method="post" enctype="multipart/form-data" id="friendship-upload-form" novalidate>
|
||||
<input type="hidden" name="pwg_token" value="<?php echo htmlspecialchars($token, ENT_QUOTES, 'UTF-8'); ?>">
|
||||
<input type="hidden" name="upload_type" value="friendship">
|
||||
<h2>Persönlicher Freundschaftscode</h2>
|
||||
|
||||
<div class="grid">
|
||||
<label for="friend-name">Name</label>
|
||||
<div class="control">
|
||||
<input id="friend-name" name="friend_name" type="text" maxlength="120" autocomplete="name" required>
|
||||
<div id="friend-name-status" class="status" style="margin-top:6px">Bitte Namen eingeben.</div>
|
||||
</div>
|
||||
|
||||
<label for="friend-file">Freundschaftscode</label>
|
||||
<div class="control">
|
||||
<input id="friend-file" name="friend_file" type="file" accept="image/png,image/jpeg,image/webp,image/gif" required>
|
||||
<div class="muted" style="margin-top:6px">Ein Bild pro Person. PNG, JPG, WEBP oder GIF.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button id="friend-submit-button" type="submit" disabled>Freundschaftscode hochladen</button>
|
||||
<span class="muted">Pro Name wird genau ein persönlicher Freundschaftscode gespeichert.</span>
|
||||
</div>
|
||||
</form>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
(function(){
|
||||
'use strict';
|
||||
var form=document.getElementById('qr-upload-form');
|
||||
var fileInput=document.getElementById('qr-files');
|
||||
var yearInput=document.getElementById('upload-year');
|
||||
var yearNote=document.getElementById('year-limit-note');
|
||||
var list=document.getElementById('file-list');
|
||||
var submit=document.getElementById('submit-button');
|
||||
var inventoryYear=document.getElementById('inventory-year');
|
||||
var inventoryCount=document.getElementById('inventory-count');
|
||||
var inventoryGrid=document.getElementById('inventory-grid');
|
||||
var endpoint=<?php echo json_encode($endpoint, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); ?>;
|
||||
var limits=<?php echo json_encode($year_limits, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); ?>;
|
||||
var timers=new WeakMap();
|
||||
|
||||
function yearLimit(){return Number(limits[String(yearInput.value)]||limits[yearInput.value]||0);}
|
||||
|
||||
function canonical(value){
|
||||
value=String(value||'').trim();
|
||||
if(!/^\d+$/.test(value))return '';
|
||||
value=value.replace(/^0+(?=\d)/,'');
|
||||
if(value==='0')return '';
|
||||
return value;
|
||||
}
|
||||
|
||||
function rows(){return Array.prototype.slice.call(list.querySelectorAll('.file-row'));}
|
||||
|
||||
function setStatus(row,text,kind){
|
||||
var node=row.querySelector('.status');
|
||||
node.textContent=text;
|
||||
node.className='status'+(kind?' '+kind:'');
|
||||
row.dataset.valid=kind==='ok'?'1':'0';
|
||||
updateSubmit();
|
||||
}
|
||||
|
||||
function updateSubmit(){
|
||||
var all=rows();
|
||||
submit.disabled=!all.length||all.some(function(row){return row.dataset.valid!=='1';});
|
||||
}
|
||||
|
||||
function renderInventory(inventory){
|
||||
inventoryYear.textContent=String(inventory.year);
|
||||
inventoryCount.textContent=String(inventory.present_count)+' vorhanden · '+String(inventory.missing_count)+' fehlen';
|
||||
inventoryGrid.textContent='';
|
||||
(inventory.slots||[]).forEach(function(slot){
|
||||
var node=document.createElement('span');
|
||||
node.className='inventory-slot '+(slot.present?'present':'missing');
|
||||
node.textContent=String(slot.number);
|
||||
node.title='QR-Code #'+slot.number+' '+(slot.present?'vorhanden':'fehlt');
|
||||
inventoryGrid.appendChild(node);
|
||||
});
|
||||
}
|
||||
|
||||
function loadInventory(){
|
||||
inventoryCount.textContent='Bestand wird geladen …';
|
||||
var requestedYear=yearInput.value;
|
||||
var url=endpoint+'?action=inventory&year='+encodeURIComponent(requestedYear)+'&_='+Date.now();
|
||||
fetch(url,{credentials:'same-origin',cache:'no-store',headers:{'Accept':'application/json'}})
|
||||
.then(function(response){return response.json().then(function(data){return {response:response,data:data};});})
|
||||
.then(function(result){
|
||||
if(!result.response.ok||!result.data.ok||!result.data.inventory)throw new Error(result.data.message||'Bestand konnte nicht geladen werden.');
|
||||
if(String(yearInput.value)!==String(requestedYear))return;
|
||||
renderInventory(result.data.inventory);
|
||||
})
|
||||
.catch(function(error){inventoryCount.textContent=error.message||'Bestand konnte nicht geladen werden.';});
|
||||
}
|
||||
|
||||
function markBatchDuplicates(){
|
||||
var grouped={};
|
||||
rows().forEach(function(row){
|
||||
var input=row.querySelector('input[name="qr_numbers[]"]');
|
||||
var value=canonical(input.value);
|
||||
if(!value)return;
|
||||
if(!grouped[value])grouped[value]=[];
|
||||
grouped[value].push(row);
|
||||
});
|
||||
Object.keys(grouped).forEach(function(key){
|
||||
if(grouped[key].length>1){
|
||||
grouped[key].forEach(function(row){setStatus(row,'Nummer ist in diesem Batch doppelt.','bad');});
|
||||
}
|
||||
});
|
||||
return grouped;
|
||||
}
|
||||
|
||||
function checkRow(row){
|
||||
var input=row.querySelector('input[name="qr_numbers[]"]');
|
||||
var value=canonical(input.value);
|
||||
var limit=yearLimit();
|
||||
if(!value){setStatus(row,'Bitte eine QR-Code-Nummer ab 1 eingeben.','bad');return;}
|
||||
if(!limit||Number(value)>limit){setStatus(row,'Für '+yearInput.value+' sind nur die Nummern 1 bis '+limit+' vorgesehen.','bad');return;}
|
||||
|
||||
var grouped=markBatchDuplicates();
|
||||
if(grouped[value]&&grouped[value].length>1)return;
|
||||
|
||||
setStatus(row,'Nummer wird geprüft …','wait');
|
||||
var url=endpoint+'?action=check&year='+encodeURIComponent(yearInput.value)+'&number='+encodeURIComponent(value)+'&_='+Date.now();
|
||||
fetch(url,{credentials:'same-origin',cache:'no-store',headers:{'Accept':'application/json'}})
|
||||
.then(function(response){return response.json().then(function(data){return {response:response,data:data};});})
|
||||
.then(function(result){
|
||||
if(!result.response.ok||!result.data.ok)throw new Error(result.data.message||'Prüfung fehlgeschlagen.');
|
||||
if(result.data.available)setStatus(row,result.data.message,'ok');
|
||||
else setStatus(row,result.data.message,'bad');
|
||||
})
|
||||
.catch(function(error){setStatus(row,error.message||'Prüfung fehlgeschlagen.','bad');});
|
||||
}
|
||||
|
||||
function scheduleCheck(row){
|
||||
var old=timers.get(row);if(old)window.clearTimeout(old);
|
||||
timers.set(row,window.setTimeout(function(){checkRow(row);},250));
|
||||
}
|
||||
|
||||
function scheduleAll(){rows().forEach(function(row){scheduleCheck(row);});}
|
||||
|
||||
function updateYearNote(){
|
||||
yearNote.textContent='Für '+yearInput.value+' sind QR-Code-Nummern 1 bis '+yearLimit()+' vorgesehen.';
|
||||
}
|
||||
|
||||
function rebuild(){
|
||||
list.textContent='';
|
||||
Array.prototype.slice.call(fileInput.files||[]).forEach(function(file,index){
|
||||
var row=document.createElement('div');row.className='file-row';row.dataset.valid='0';
|
||||
var name=document.createElement('div');name.className='file-name';name.textContent=file.name;
|
||||
var input=document.createElement('input');input.type='text';input.name='qr_numbers[]';input.inputMode='numeric';input.autocomplete='off';input.placeholder='QR-Nummer';input.setAttribute('aria-label','QR-Code-Nummer für '+file.name);input.maxLength=3;input.required=true;
|
||||
var status=document.createElement('div');status.className='status';status.textContent='Nummer fehlt.';
|
||||
row.appendChild(name);row.appendChild(input);row.appendChild(status);list.appendChild(row);
|
||||
input.addEventListener('input',scheduleAll);
|
||||
if(index===0)window.setTimeout(function(){input.focus();},0);
|
||||
});
|
||||
updateSubmit();
|
||||
}
|
||||
|
||||
fileInput.addEventListener('change',rebuild);
|
||||
yearInput.addEventListener('change',function(){updateYearNote();scheduleAll();loadInventory();});
|
||||
form.addEventListener('submit',function(event){
|
||||
var invalid=rows().some(function(row){return row.dataset.valid!=='1';});
|
||||
if(invalid){event.preventDefault();scheduleAll();}
|
||||
else{submit.disabled=true;submit.textContent='Upload läuft …';}
|
||||
});
|
||||
})();
|
||||
|
||||
(function(){
|
||||
'use strict';
|
||||
var form=document.getElementById('friendship-upload-form');
|
||||
var nameInput=document.getElementById('friend-name');
|
||||
var fileInput=document.getElementById('friend-file');
|
||||
var status=document.getElementById('friend-name-status');
|
||||
var submit=document.getElementById('friend-submit-button');
|
||||
var endpoint=<?php echo json_encode($endpoint, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); ?>;
|
||||
var timer=null;
|
||||
var nameValid=false;
|
||||
|
||||
function updateSubmit(){
|
||||
submit.disabled=!nameValid||!fileInput.files||fileInput.files.length!==1;
|
||||
}
|
||||
|
||||
function setStatus(text,kind){
|
||||
status.textContent=text;
|
||||
status.className='status'+(kind?' '+kind:'');
|
||||
nameValid=kind==='ok';
|
||||
updateSubmit();
|
||||
}
|
||||
|
||||
function checkName(){
|
||||
var name=String(nameInput.value||'').trim().replace(/\s+/g,' ');
|
||||
if(!name){setStatus('Bitte Namen eingeben.','bad');return;}
|
||||
setStatus('Name wird geprüft …','wait');
|
||||
var url=endpoint+'?action=check_friendship&name='+encodeURIComponent(name)+'&_='+Date.now();
|
||||
fetch(url,{credentials:'same-origin',cache:'no-store',headers:{'Accept':'application/json'}})
|
||||
.then(function(response){return response.json().then(function(data){return {response:response,data:data};});})
|
||||
.then(function(result){
|
||||
if(!result.response.ok||!result.data.ok)throw new Error(result.data.message||'Prüfung fehlgeschlagen.');
|
||||
if(result.data.available)setStatus(result.data.message,'ok');
|
||||
else setStatus(result.data.message,'bad');
|
||||
})
|
||||
.catch(function(error){setStatus(error.message||'Prüfung fehlgeschlagen.','bad');});
|
||||
}
|
||||
|
||||
nameInput.addEventListener('input',function(){
|
||||
nameValid=false;updateSubmit();
|
||||
if(timer)window.clearTimeout(timer);
|
||||
timer=window.setTimeout(checkName,250);
|
||||
});
|
||||
fileInput.addEventListener('change',updateSubmit);
|
||||
form.addEventListener('submit',function(event){
|
||||
if(!nameValid||!fileInput.files||fileInput.files.length!==1){event.preventDefault();checkName();return;}
|
||||
submit.disabled=true;submit.textContent='Upload läuft …';
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
74
friendship-code-admin-file.php
Normal file
74
friendship-code-admin-file.php
Normal file
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
$piwigo_root = realpath(dirname(__DIR__, 2));
|
||||
if ($piwigo_root === false)
|
||||
{
|
||||
http_response_code(500);
|
||||
exit;
|
||||
}
|
||||
|
||||
define('PHPWG_ROOT_PATH', rtrim($piwigo_root, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR);
|
||||
include_once(PHPWG_ROOT_PATH.'include/common.inc.php');
|
||||
|
||||
if (!defined('BRATONIEN_TOOLS_PATH'))
|
||||
{
|
||||
http_response_code(404);
|
||||
exit;
|
||||
}
|
||||
|
||||
check_status(ACCESS_ADMINISTRATOR);
|
||||
require_once(BRATONIEN_TOOLS_PATH.'include/friendship_code_upload.inc.php');
|
||||
|
||||
$id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
|
||||
if ($id < 1)
|
||||
{
|
||||
http_response_code(404);
|
||||
exit;
|
||||
}
|
||||
|
||||
$table = bratonien_tools_friendship_code_table();
|
||||
$result = pwg_query('SELECT id, person_name, stored_name, mime_type FROM `'.$table.'` WHERE id='.$id.' LIMIT 1');
|
||||
$row = pwg_db_fetch_assoc($result);
|
||||
if (!$row)
|
||||
{
|
||||
http_response_code(404);
|
||||
exit;
|
||||
}
|
||||
|
||||
$stored_name = (string)$row['stored_name'];
|
||||
$mime = strtolower((string)$row['mime_type']);
|
||||
$allowed_mimes = array('image/png', 'image/jpeg', 'image/webp', 'image/gif');
|
||||
if ($stored_name === '' || basename($stored_name) !== $stored_name || !in_array($mime, $allowed_mimes, true))
|
||||
{
|
||||
http_response_code(404);
|
||||
exit;
|
||||
}
|
||||
|
||||
$path = bratonien_tools_friendship_code_storage_root().DIRECTORY_SEPARATOR.$stored_name;
|
||||
if (!is_file($path) || !is_readable($path))
|
||||
{
|
||||
http_response_code(404);
|
||||
exit;
|
||||
}
|
||||
|
||||
$extension = strtolower((string)pathinfo($stored_name, PATHINFO_EXTENSION));
|
||||
if ($extension === '')
|
||||
{
|
||||
$extension = 'png';
|
||||
}
|
||||
|
||||
header('Content-Type: '.$mime);
|
||||
header('Content-Length: '.filesize($path));
|
||||
header('Cache-Control: private, no-store, max-age=0');
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
|
||||
if (!empty($_GET['download']))
|
||||
{
|
||||
header('Content-Disposition: attachment; filename="freundschaftscode-'.$id.'.'.$extension.'"');
|
||||
}
|
||||
else
|
||||
{
|
||||
header('Content-Disposition: inline; filename="freundschaftscode-'.$id.'.'.$extension.'"');
|
||||
}
|
||||
|
||||
readfile($path);
|
||||
exit;
|
||||
@@ -60,6 +60,114 @@ function bratonien_tools_atomic_cache_remove_tree($root, array &$failed)
|
||||
if (!@rmdir($root) && is_dir($root)) $failed[] = $root;
|
||||
}
|
||||
|
||||
function bratonien_tools_atomic_cache_acquire_lock($path, $label, $timeout_seconds=10.0)
|
||||
{
|
||||
$directory = dirname($path);
|
||||
if (!is_dir($directory) && !@mkdir($directory, 0775, true) && !is_dir($directory))
|
||||
{
|
||||
throw new RuntimeException($label.'-Lockverzeichnis konnte nicht angelegt werden.');
|
||||
}
|
||||
|
||||
$handle = @fopen($path, 'c');
|
||||
if (!$handle)
|
||||
{
|
||||
throw new RuntimeException($label.'-Lock konnte nicht geöffnet werden: '.$path);
|
||||
}
|
||||
|
||||
$deadline = microtime(true) + max(0.1, (float)$timeout_seconds);
|
||||
do
|
||||
{
|
||||
if (@flock($handle, LOCK_EX | LOCK_NB))
|
||||
{
|
||||
return $handle;
|
||||
}
|
||||
usleep(100000);
|
||||
}
|
||||
while (microtime(true) < $deadline);
|
||||
|
||||
fclose($handle);
|
||||
throw new RuntimeException($label.' ist noch aktiv. Der Cache wurde nicht angefasst; bitte den laufenden Vorgang kurz abschließen lassen und erneut leeren.');
|
||||
}
|
||||
|
||||
function bratonien_tools_atomic_cache_release_locks(array &$locks)
|
||||
{
|
||||
foreach (array_reverse($locks) as $handle)
|
||||
{
|
||||
if (!is_resource($handle)) continue;
|
||||
@flock($handle, LOCK_UN);
|
||||
fclose($handle);
|
||||
}
|
||||
$locks = array();
|
||||
}
|
||||
|
||||
function bratonien_tools_atomic_cache_prepare_webdav_guards(array &$locks, array &$cancel_files)
|
||||
{
|
||||
if (!function_exists('bratonien_tools_webdav_warmup_connections')) return 0;
|
||||
|
||||
$connections = bratonien_tools_webdav_warmup_connections();
|
||||
if (!$connections) return 0;
|
||||
ksort($connections, SORT_NUMERIC);
|
||||
|
||||
// Jeder aktive WebDAV-Worker bekommt zuerst sein reguläres Abbruchsignal.
|
||||
// Ein bereits gestarteter 10er-Batch darf sauber fertig werden; ein neuer
|
||||
// Batch darf danach nicht mehr beginnen.
|
||||
if (function_exists('bratonien_tools_request_webdav_cache_cancel'))
|
||||
{
|
||||
bratonien_tools_request_webdav_cache_cancel();
|
||||
}
|
||||
|
||||
// Das Signal wird für alle Verbindungen gesetzt, auch wenn deren Statusdatei
|
||||
// veraltet sein sollte. Sobald wir anschließend den Prozess-Lock besitzen,
|
||||
// kann garantiert kein WebDAV-Worker während des Cache-Leerens schreiben.
|
||||
foreach ($connections as $connection_id=>$runtime)
|
||||
{
|
||||
$cancel = function_exists('bratonien_tools_webdav_warmup_cancel_file_for_connection')
|
||||
? bratonien_tools_webdav_warmup_cancel_file_for_connection($connection_id)
|
||||
: PHPWG_ROOT_PATH.PWG_LOCAL_DIR.'bratonien-webdav-warmup.cancel-'.(int)$connection_id;
|
||||
|
||||
if (@file_put_contents($cancel, (string)time()."\n", LOCK_EX) === false)
|
||||
{
|
||||
throw new RuntimeException('WebDAV-Abbruchsignal für Verbindung #'.(int)$connection_id.' konnte nicht geschrieben werden.');
|
||||
}
|
||||
@chmod($cancel, 0664);
|
||||
$cancel_files[] = $cancel;
|
||||
}
|
||||
|
||||
foreach ($connections as $connection_id=>$runtime)
|
||||
{
|
||||
$state_dir = rtrim((string)($runtime['state_dir'] ?? ''), '/');
|
||||
if ($state_dir === '')
|
||||
{
|
||||
throw new RuntimeException('WebDAV-State-Verzeichnis für Verbindung #'.(int)$connection_id.' fehlt.');
|
||||
}
|
||||
$locks[] = bratonien_tools_atomic_cache_acquire_lock(
|
||||
$state_dir.'/webdav-cache-warmup.lock',
|
||||
'WebDAV-Cache-Worker #'.(int)$connection_id,
|
||||
10.0
|
||||
);
|
||||
}
|
||||
|
||||
return count($connections);
|
||||
}
|
||||
|
||||
function bratonien_tools_atomic_cache_prepare_presentation_guard(array &$locks)
|
||||
{
|
||||
if (!function_exists('bratonien_tools_presentation_refresh_queue_dir')) return false;
|
||||
|
||||
$queue_dir = bratonien_tools_presentation_refresh_queue_dir();
|
||||
if (!is_dir($queue_dir)) return false;
|
||||
|
||||
// Der Presentation-Worker erzeugt Bratonien-Wasserzeichen-Vorschauen im
|
||||
// selben Derivatbaum. Deshalb muss auch er während des atomaren Leerens
|
||||
// vollständig draußen bleiben.
|
||||
$locks[] = bratonien_tools_atomic_cache_acquire_lock(
|
||||
rtrim($queue_dir, '/').'/.worker.lock',
|
||||
'Vorschau-Aktualisierung',
|
||||
10.0
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
function bratonien_tools_clear_image_cache_atomic()
|
||||
{
|
||||
global $conf;
|
||||
@@ -69,125 +177,166 @@ function bratonien_tools_clear_image_cache_atomic()
|
||||
throw new RuntimeException('PWG_DERIVATIVE_DIR ist nicht definiert.');
|
||||
}
|
||||
|
||||
if (bratonien_tools_main_cache_process_active() || bratonien_tools_main_cache_is_running())
|
||||
$held_locks = array();
|
||||
$webdav_cancel_files = array();
|
||||
$webdav_guards_ready = false;
|
||||
|
||||
try
|
||||
{
|
||||
bratonien_tools_request_main_cache_cancel();
|
||||
if (!bratonien_tools_wait_main_cache_stopped(10.0))
|
||||
// 1. Lokalen Cache-Builder sauber anhalten und seinen Prozess-Lock selbst
|
||||
// halten. Damit kann zwischen Prüfung und atomarem Rename kein neuer
|
||||
// lokaler Builder starten.
|
||||
if (bratonien_tools_main_cache_process_active() || bratonien_tools_main_cache_is_running())
|
||||
{
|
||||
throw new RuntimeException('Der laufende Cache-Aufbau konnte noch nicht beendet werden. Bitte den Abbruch kurz abschließen lassen und erneut leeren.');
|
||||
bratonien_tools_request_main_cache_cancel();
|
||||
if (!bratonien_tools_wait_main_cache_stopped(10.0))
|
||||
{
|
||||
throw new RuntimeException('Der laufende Cache-Aufbau konnte noch nicht beendet werden. Bitte den Abbruch kurz abschließen lassen und erneut leeren.');
|
||||
}
|
||||
}
|
||||
}
|
||||
@unlink(bratonien_tools_main_cache_cancel_file());
|
||||
|
||||
$piwigo_root = realpath(PHPWG_ROOT_PATH);
|
||||
if ($piwigo_root === false)
|
||||
{
|
||||
throw new RuntimeException('Piwigo-Root konnte für die Cache-Sicherheitsprüfung nicht aufgelöst werden.');
|
||||
}
|
||||
|
||||
$cache_root = rtrim(PHPWG_ROOT_PATH, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR.trim(PWG_DERIVATIVE_DIR, '/\\');
|
||||
$real_cache_root = realpath($cache_root);
|
||||
if ($real_cache_root === false || !is_dir($real_cache_root))
|
||||
{
|
||||
throw new RuntimeException('Bildcache-Verzeichnis wurde nicht gefunden: '.$cache_root);
|
||||
}
|
||||
|
||||
$root_prefix = rtrim($piwigo_root, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR;
|
||||
if (strpos(rtrim($real_cache_root, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR, $root_prefix) !== 0)
|
||||
{
|
||||
throw new RuntimeException('Bildcache liegt außerhalb der Piwigo-Installation. Abbruch.');
|
||||
}
|
||||
if (rtrim($real_cache_root, DIRECTORY_SEPARATOR) === rtrim($piwigo_root, DIRECTORY_SEPARATOR))
|
||||
{
|
||||
throw new RuntimeException('Bildcache-Pfad entspricht dem Piwigo-Root. Sicherheitsabbruch.');
|
||||
}
|
||||
|
||||
$before = bratonien_tools_scan_image_cache($real_cache_root);
|
||||
$parent = dirname($real_cache_root);
|
||||
if (!is_dir($parent) || !is_writable($parent))
|
||||
{
|
||||
throw new RuntimeException('Übergeordnetes Bildcache-Verzeichnis ist nicht beschreibbar: '.$parent);
|
||||
}
|
||||
|
||||
$detached = $parent.'/.'.basename($real_cache_root).'.bratonien-clear-'.date('YmdHis').'-'.bin2hex(random_bytes(4));
|
||||
if (file_exists($detached) || is_link($detached))
|
||||
{
|
||||
throw new RuntimeException('Temporäres Cache-Auslagerungsverzeichnis existiert bereits. Abbruch.');
|
||||
}
|
||||
|
||||
if (!@rename($real_cache_root, $detached))
|
||||
{
|
||||
throw new RuntimeException('Bildcache konnte nicht atomar aus dem aktiven Pfad ausgelagert werden.');
|
||||
}
|
||||
|
||||
$mode = isset($conf['chmod_value']) ? (int)$conf['chmod_value'] : 0755;
|
||||
$umask = umask(0);
|
||||
$created = @mkdir($real_cache_root, $mode, true);
|
||||
umask($umask);
|
||||
if (!$created && !is_dir($real_cache_root))
|
||||
{
|
||||
if (!file_exists($real_cache_root) && @rename($detached, $real_cache_root))
|
||||
{
|
||||
throw new RuntimeException('Neuer Bildcache konnte nicht angelegt werden; der bisherige Cache wurde vollständig wiederhergestellt.');
|
||||
}
|
||||
throw new RuntimeException('Neuer Bildcache konnte nicht angelegt und der bisherige Cache nicht automatisch wiederhergestellt werden. Manueller Eingriff erforderlich.');
|
||||
}
|
||||
|
||||
@chmod($real_cache_root, $mode);
|
||||
@file_put_contents($real_cache_root.'/index.htm', 'Not allowed!');
|
||||
|
||||
// Der Quellenindex selbst bleibt bestehen, weil sich die Quellen durch ein
|
||||
// Cache-Leeren nicht ändern. Seine Fertigmarkierungen sind danach aber
|
||||
// zwingend ungültig. Der Worker darf sonst einen leeren Piwigo-Cache als
|
||||
// bereits verarbeitet ansehen, ohne den Cache selbst anzuschauen.
|
||||
$webdav_invalidated = 0;
|
||||
if (function_exists('bratonien_tools_invalidate_webdav_cache_completion'))
|
||||
{
|
||||
$webdav_invalidated = bratonien_tools_invalidate_webdav_cache_completion('Piwigo-Bildcache wurde atomar geleert.');
|
||||
}
|
||||
|
||||
$failed = array();
|
||||
bratonien_tools_atomic_cache_remove_tree($detached, $failed);
|
||||
|
||||
$active = bratonien_tools_scan_image_cache($real_cache_root);
|
||||
|
||||
bratonien_tools_write_main_cache_status(array(
|
||||
'state'=>'idle',
|
||||
'message'=>'Bildcache wurde atomar geleert. Kein manueller Cache-Aufbau aktiv.',
|
||||
));
|
||||
|
||||
if ($failed)
|
||||
{
|
||||
throw new RuntimeException(sprintf(
|
||||
'Der aktive Bildcache wurde erfolgreich geleert und neu angelegt, aber %d Datei(en)/Verzeichnis(se) des ausgelagerten Altbestands konnten nicht entfernt werden. Erste problematische Stelle: %s',
|
||||
count($failed),
|
||||
$failed[0]
|
||||
));
|
||||
}
|
||||
|
||||
$message = sprintf(
|
||||
'Bildcache atomar geleert: %d alte Datei(en) (%s) entfernt, davon %d Custom-Derivate.',
|
||||
$before['files'],
|
||||
bratonien_tools_format_bytes($before['bytes']),
|
||||
$before['custom']
|
||||
);
|
||||
|
||||
if ($webdav_invalidated > 0)
|
||||
{
|
||||
$message .= sprintf(' Die Cache-Fertigmarkierungen von %d WebDAV-Worker-Index(en) wurden verworfen; die Quellenindizes selbst bleiben erhalten.', $webdav_invalidated);
|
||||
}
|
||||
|
||||
if ($active['files'] > 0)
|
||||
{
|
||||
$message .= sprintf(
|
||||
' Während bzw. unmittelbar nach dem Umschalten wurden bereits %d neue Derivatdatei(en) durch laufende Anfragen erzeugt; diese gehören zum neuen Cache und sind kein Rest des gelöschten Bestands.',
|
||||
$active['files']
|
||||
@unlink(bratonien_tools_main_cache_cancel_file());
|
||||
$held_locks[] = bratonien_tools_atomic_cache_acquire_lock(
|
||||
bratonien_tools_main_cache_lock_file(),
|
||||
'Lokaler Piwigo-Cache-Worker',
|
||||
10.0
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
$message .= ' Der neu aktive Cache ist zum Abschluss der Prüfung leer.';
|
||||
}
|
||||
|
||||
return array('message'=>$message);
|
||||
// 2. WebDAV-Warmup regulär abbrechen und danach alle Verbindungs-Locks
|
||||
// exklusiv halten. Der laufende Batch wird nicht hart beendet.
|
||||
bratonien_tools_atomic_cache_prepare_webdav_guards($held_locks, $webdav_cancel_files);
|
||||
$webdav_guards_ready = true;
|
||||
|
||||
// 3. Seit der Presentation-Refresh-Einführung kann auch dieser Worker
|
||||
// Derivate erzeugen. Ohne seinen Lock konnte ein gerade geleerter Cache
|
||||
// sofort wieder gefüllt werden und wie ein fehlgeschlagenes Löschen wirken.
|
||||
bratonien_tools_atomic_cache_prepare_presentation_guard($held_locks);
|
||||
|
||||
$piwigo_root = realpath(PHPWG_ROOT_PATH);
|
||||
if ($piwigo_root === false)
|
||||
{
|
||||
throw new RuntimeException('Piwigo-Root konnte für die Cache-Sicherheitsprüfung nicht aufgelöst werden.');
|
||||
}
|
||||
|
||||
$cache_root = rtrim(PHPWG_ROOT_PATH, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR.trim(PWG_DERIVATIVE_DIR, '/\\');
|
||||
$real_cache_root = realpath($cache_root);
|
||||
if ($real_cache_root === false || !is_dir($real_cache_root))
|
||||
{
|
||||
throw new RuntimeException('Bildcache-Verzeichnis wurde nicht gefunden: '.$cache_root);
|
||||
}
|
||||
|
||||
$root_prefix = rtrim($piwigo_root, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR;
|
||||
if (strpos(rtrim($real_cache_root, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR, $root_prefix) !== 0)
|
||||
{
|
||||
throw new RuntimeException('Bildcache liegt außerhalb der Piwigo-Installation. Abbruch.');
|
||||
}
|
||||
if (rtrim($real_cache_root, DIRECTORY_SEPARATOR) === rtrim($piwigo_root, DIRECTORY_SEPARATOR))
|
||||
{
|
||||
throw new RuntimeException('Bildcache-Pfad entspricht dem Piwigo-Root. Sicherheitsabbruch.');
|
||||
}
|
||||
|
||||
$before = bratonien_tools_scan_image_cache($real_cache_root);
|
||||
$parent = dirname($real_cache_root);
|
||||
if (!is_dir($parent) || !is_writable($parent))
|
||||
{
|
||||
throw new RuntimeException('Übergeordnetes Bildcache-Verzeichnis ist nicht beschreibbar: '.$parent);
|
||||
}
|
||||
|
||||
$detached = $parent.'/.'.basename($real_cache_root).'.bratonien-clear-'.date('YmdHis').'-'.bin2hex(random_bytes(4));
|
||||
if (file_exists($detached) || is_link($detached))
|
||||
{
|
||||
throw new RuntimeException('Temporäres Cache-Auslagerungsverzeichnis existiert bereits. Abbruch.');
|
||||
}
|
||||
|
||||
if (!@rename($real_cache_root, $detached))
|
||||
{
|
||||
throw new RuntimeException('Bildcache konnte nicht atomar aus dem aktiven Pfad ausgelagert werden.');
|
||||
}
|
||||
|
||||
$mode = isset($conf['chmod_value']) ? (int)$conf['chmod_value'] : 0755;
|
||||
$umask = umask(0);
|
||||
$created = @mkdir($real_cache_root, $mode, true);
|
||||
umask($umask);
|
||||
if (!$created && !is_dir($real_cache_root))
|
||||
{
|
||||
if (!file_exists($real_cache_root) && @rename($detached, $real_cache_root))
|
||||
{
|
||||
throw new RuntimeException('Neuer Bildcache konnte nicht angelegt werden; der bisherige Cache wurde vollständig wiederhergestellt.');
|
||||
}
|
||||
throw new RuntimeException('Neuer Bildcache konnte nicht angelegt und der bisherige Cache nicht automatisch wiederhergestellt werden. Manueller Eingriff erforderlich.');
|
||||
}
|
||||
|
||||
@chmod($real_cache_root, $mode);
|
||||
@file_put_contents($real_cache_root.'/index.htm', 'Not allowed!');
|
||||
|
||||
// Der Quellenindex selbst bleibt bestehen, weil sich die Quellen durch ein
|
||||
// Cache-Leeren nicht ändern. Seine Fertigmarkierungen sind danach aber
|
||||
// zwingend ungültig. Der Worker darf sonst einen leeren Piwigo-Cache als
|
||||
// bereits verarbeitet ansehen, ohne den Cache selbst anzuschauen.
|
||||
$webdav_invalidated = 0;
|
||||
if (function_exists('bratonien_tools_invalidate_webdav_cache_completion'))
|
||||
{
|
||||
$webdav_invalidated = bratonien_tools_invalidate_webdav_cache_completion('Piwigo-Bildcache wurde atomar geleert.');
|
||||
}
|
||||
|
||||
$failed = array();
|
||||
bratonien_tools_atomic_cache_remove_tree($detached, $failed);
|
||||
|
||||
$active = bratonien_tools_scan_image_cache($real_cache_root);
|
||||
|
||||
bratonien_tools_write_main_cache_status(array(
|
||||
'state'=>'idle',
|
||||
'message'=>'Bildcache wurde atomar geleert. Kein Cache-Worker schreibt während des Löschvorgangs in den Derivatbaum.',
|
||||
));
|
||||
|
||||
if ($failed)
|
||||
{
|
||||
throw new RuntimeException(sprintf(
|
||||
'Der aktive Bildcache wurde erfolgreich geleert und neu angelegt, aber %d Datei(en)/Verzeichnis(se) des ausgelagerten Altbestands konnten nicht entfernt werden. Erste problematische Stelle: %s',
|
||||
count($failed),
|
||||
$failed[0]
|
||||
));
|
||||
}
|
||||
|
||||
$message = sprintf(
|
||||
'Bildcache atomar geleert: %d alte Datei(en) (%s) entfernt, davon %d Custom-Derivate.',
|
||||
$before['files'],
|
||||
bratonien_tools_format_bytes($before['bytes']),
|
||||
$before['custom']
|
||||
);
|
||||
|
||||
if ($webdav_invalidated > 0)
|
||||
{
|
||||
$message .= sprintf(' Die Cache-Fertigmarkierungen von %d WebDAV-Worker-Index(en) wurden verworfen; die Quellenindizes selbst bleiben erhalten.', $webdav_invalidated);
|
||||
}
|
||||
|
||||
if ($active['files'] > 0)
|
||||
{
|
||||
$message .= sprintf(
|
||||
' Unmittelbar nach dem atomaren Umschalten wurden bereits %d neue Derivatdatei(en) durch normale Piwigo-Anfragen erzeugt; die eigenen Cache-Worker waren während des Löschens gesperrt und der alte Cachebaum wurde vollständig entfernt.',
|
||||
$active['files']
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
$message .= ' Der neu aktive Cache ist zum Abschluss der Prüfung leer.';
|
||||
}
|
||||
|
||||
return array('message'=>$message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Nur wenn wirklich alle WebDAV-Prozess-Locks gesichert wurden, sind die
|
||||
// Cancel-Dateien nicht mehr nötig. Bei einem Timeout bleiben sie bestehen,
|
||||
// damit der noch laufende Worker nach seinem aktuellen Batch tatsächlich
|
||||
// stoppt und das fehlgeschlagene Cache-Leeren nicht durch einen Folgebatch
|
||||
// verschärft wird.
|
||||
if ($webdav_guards_ready)
|
||||
{
|
||||
foreach ($webdav_cancel_files as $cancel)
|
||||
{
|
||||
@unlink($cancel);
|
||||
}
|
||||
}
|
||||
bratonien_tools_atomic_cache_release_locks($held_locks);
|
||||
}
|
||||
}
|
||||
|
||||
105
include/customer_qr_frontend_menu.inc.php
Normal file
105
include/customer_qr_frontend_menu.inc.php
Normal file
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
if (!defined('PHPWG_ROOT_PATH'))
|
||||
{
|
||||
die('Hacking attempt!');
|
||||
}
|
||||
|
||||
function bratonien_tools_customer_qr_register_frontend_menu($menu_ref_array)
|
||||
{
|
||||
if (defined('IN_ADMIN'))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
$settings = bratonien_tools_customer_qr_settings();
|
||||
if (empty($settings['enabled']))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!is_array($menu_ref_array) || empty($menu_ref_array[0]) || !is_object($menu_ref_array[0]))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
$menu = $menu_ref_array[0];
|
||||
if (!method_exists($menu, 'get_id') || $menu->get_id() !== 'menubar' || !method_exists($menu, 'register_block'))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!class_exists('RegisteredBlock'))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
$menu->register_block(new RegisteredBlock(
|
||||
'mbBratonienCustomerQr',
|
||||
'QR-Code hochladen',
|
||||
BRATONIEN_TOOLS_ID
|
||||
));
|
||||
}
|
||||
|
||||
function bratonien_tools_customer_qr_prepare_frontend_menu($menu_ref_array)
|
||||
{
|
||||
if (defined('IN_ADMIN'))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
$settings = bratonien_tools_customer_qr_settings();
|
||||
if (empty($settings['enabled']))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!is_array($menu_ref_array) || empty($menu_ref_array[0]) || !is_object($menu_ref_array[0]))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
$menu = $menu_ref_array[0];
|
||||
if (!method_exists($menu, 'get_id') || $menu->get_id() !== 'menubar')
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
$block = $menu->get_block('mbBratonienCustomerQr');
|
||||
if ($block === null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
$url = htmlspecialchars(
|
||||
get_root_url().'plugins/'.BRATONIEN_TOOLS_ID.'/customer-qr-upload.php',
|
||||
ENT_QUOTES,
|
||||
'UTF-8'
|
||||
);
|
||||
|
||||
// Bootstrap Darkroom renders raw-content blocks directly in the root navbar.
|
||||
// Keeping this as a separate Piwigo menu block avoids hiding the customer
|
||||
// action inside mbMenu/"Entdecken" and still follows the normal menubar
|
||||
// lifecycle instead of modifying the theme.
|
||||
$block->raw_content = '<li class="nav-item bratonien-customer-qr-nav">'
|
||||
.'<a class="nav-link" href="'.$url.'" title="QR-Code hochladen">'
|
||||
.'<i class="fas fa-qrcode fa-fw" aria-hidden="true"></i> QR-Code hochladen'
|
||||
.'</a></li>';
|
||||
|
||||
if (method_exists($menu, 'set_block_position'))
|
||||
{
|
||||
// Core defaults: Specials=200, Menu=250. Darkroom combines those two as
|
||||
// "Entdecken"; 225 therefore places QR upload directly after it.
|
||||
$menu->set_block_position('mbBratonienCustomerQr', 225);
|
||||
}
|
||||
}
|
||||
|
||||
add_event_handler(
|
||||
'blockmanager_register_blocks',
|
||||
'bratonien_tools_customer_qr_register_frontend_menu',
|
||||
EVENT_HANDLER_PRIORITY_NEUTRAL + 10
|
||||
);
|
||||
add_event_handler(
|
||||
'blockmanager_prepare_display',
|
||||
'bratonien_tools_customer_qr_prepare_frontend_menu',
|
||||
EVENT_HANDLER_PRIORITY_NEUTRAL + 10
|
||||
);
|
||||
503
include/customer_qr_upload.inc.php
Normal file
503
include/customer_qr_upload.inc.php
Normal file
@@ -0,0 +1,503 @@
|
||||
<?php
|
||||
if (!defined('PHPWG_ROOT_PATH'))
|
||||
{
|
||||
die('Hacking attempt!');
|
||||
}
|
||||
|
||||
function bratonien_tools_customer_qr_table()
|
||||
{
|
||||
return $GLOBALS['prefixeTable'].'bratonien_tools_customer_qr_uploads';
|
||||
}
|
||||
|
||||
function bratonien_tools_customer_qr_year_limits()
|
||||
{
|
||||
return array(
|
||||
2023 => 100,
|
||||
2024 => 50,
|
||||
2025 => 30,
|
||||
2026 => 30,
|
||||
);
|
||||
}
|
||||
|
||||
function bratonien_tools_customer_qr_settings()
|
||||
{
|
||||
$default = array(
|
||||
'enabled' => false,
|
||||
);
|
||||
|
||||
if (!function_exists('conf_get_param'))
|
||||
{
|
||||
return $default;
|
||||
}
|
||||
|
||||
$raw = conf_get_param('bratonien_customer_qr_upload', null);
|
||||
if ($raw === null || $raw === '')
|
||||
{
|
||||
return $default;
|
||||
}
|
||||
|
||||
$decoded = json_decode($raw, true);
|
||||
return is_array($decoded) ? array_merge($default, $decoded) : $default;
|
||||
}
|
||||
|
||||
function bratonien_tools_customer_qr_save_settings()
|
||||
{
|
||||
if (!function_exists('conf_update_param'))
|
||||
{
|
||||
throw new RuntimeException('Piwigo-Konfiguration ist nicht verfügbar.');
|
||||
}
|
||||
|
||||
$settings = array(
|
||||
'enabled' => !empty($_POST['customer_qr_enabled']),
|
||||
);
|
||||
conf_update_param('bratonien_customer_qr_upload', json_encode($settings));
|
||||
|
||||
return array(
|
||||
'message' => $settings['enabled']
|
||||
? 'QR-Upload aktiviert.'
|
||||
: 'QR-Upload deaktiviert.',
|
||||
);
|
||||
}
|
||||
|
||||
function bratonien_tools_customer_qr_storage_root()
|
||||
{
|
||||
return PHPWG_ROOT_PATH.PWG_LOCAL_DIR.'bratonien-customer-qr';
|
||||
}
|
||||
|
||||
function bratonien_tools_customer_qr_ensure_storage()
|
||||
{
|
||||
$table = bratonien_tools_customer_qr_table();
|
||||
pwg_query("CREATE TABLE IF NOT EXISTS `$table` (
|
||||
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
upload_year smallint(5) unsigned NOT NULL,
|
||||
code_number varchar(32) NOT NULL,
|
||||
original_name varchar(255) NOT NULL,
|
||||
stored_name varchar(255) NOT NULL,
|
||||
mime_type varchar(100) NOT NULL,
|
||||
file_size bigint(20) unsigned NOT NULL DEFAULT 0,
|
||||
created datetime NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY year_code (upload_year, code_number),
|
||||
KEY upload_year (upload_year)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;");
|
||||
|
||||
$root = bratonien_tools_customer_qr_storage_root();
|
||||
if (!is_dir($root) && !@mkdir($root, 0770, true) && !is_dir($root))
|
||||
{
|
||||
throw new RuntimeException('QR-Upload-Verzeichnis konnte nicht angelegt werden.');
|
||||
}
|
||||
|
||||
return $root;
|
||||
}
|
||||
|
||||
function bratonien_tools_customer_qr_year($value)
|
||||
{
|
||||
$year = (int)$value;
|
||||
$limits = bratonien_tools_customer_qr_year_limits();
|
||||
if (!isset($limits[$year]))
|
||||
{
|
||||
throw new InvalidArgumentException('Erlaubt sind nur die Jahre 2023, 2024, 2025 und 2026.');
|
||||
}
|
||||
return $year;
|
||||
}
|
||||
|
||||
function bratonien_tools_customer_qr_default_year()
|
||||
{
|
||||
$limits = bratonien_tools_customer_qr_year_limits();
|
||||
$current = (int)date('Y');
|
||||
if (isset($limits[$current]))
|
||||
{
|
||||
return $current;
|
||||
}
|
||||
|
||||
$years = array_keys($limits);
|
||||
sort($years, SORT_NUMERIC);
|
||||
return (int)end($years);
|
||||
}
|
||||
|
||||
function bratonien_tools_customer_qr_number($value)
|
||||
{
|
||||
$value = trim((string)$value);
|
||||
if ($value === '' || !preg_match('/^[0-9]{1,32}$/', $value))
|
||||
{
|
||||
throw new InvalidArgumentException('Die QR-Code-Nummer darf nur aus Ziffern bestehen.');
|
||||
}
|
||||
|
||||
$value = ltrim($value, '0');
|
||||
if ($value === '' || $value === '0')
|
||||
{
|
||||
throw new InvalidArgumentException('Die QR-Code-Nummer muss mindestens 1 sein.');
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
function bratonien_tools_customer_qr_number_for_year($year, $value)
|
||||
{
|
||||
$year = bratonien_tools_customer_qr_year($year);
|
||||
$number = bratonien_tools_customer_qr_number($value);
|
||||
$limits = bratonien_tools_customer_qr_year_limits();
|
||||
$limit = (int)$limits[$year];
|
||||
|
||||
if (strlen($number) > strlen((string)$limit) || (int)$number > $limit)
|
||||
{
|
||||
throw new InvalidArgumentException('Für '.$year.' sind nur QR-Code-Nummern 1 bis '.$limit.' vorgesehen.');
|
||||
}
|
||||
|
||||
return $number;
|
||||
}
|
||||
|
||||
function bratonien_tools_customer_qr_exists($year, $number)
|
||||
{
|
||||
bratonien_tools_customer_qr_ensure_storage();
|
||||
$year = bratonien_tools_customer_qr_year($year);
|
||||
$number = bratonien_tools_customer_qr_number_for_year($year, $number);
|
||||
$table = bratonien_tools_customer_qr_table();
|
||||
|
||||
$query = 'SELECT id FROM `'.$table.'` WHERE upload_year='.(int)$year
|
||||
." AND code_number='".pwg_db_real_escape_string($number)."' LIMIT 1";
|
||||
return pwg_db_num_rows(pwg_query($query)) > 0;
|
||||
}
|
||||
|
||||
function bratonien_tools_customer_qr_files_array(array $files)
|
||||
{
|
||||
if (!isset($files['name']))
|
||||
{
|
||||
return array();
|
||||
}
|
||||
|
||||
if (!is_array($files['name']))
|
||||
{
|
||||
return array($files);
|
||||
}
|
||||
|
||||
$normalized = array();
|
||||
$count = count($files['name']);
|
||||
for ($i = 0; $i < $count; $i++)
|
||||
{
|
||||
$normalized[] = array(
|
||||
'name' => isset($files['name'][$i]) ? $files['name'][$i] : '',
|
||||
'type' => isset($files['type'][$i]) ? $files['type'][$i] : '',
|
||||
'tmp_name' => isset($files['tmp_name'][$i]) ? $files['tmp_name'][$i] : '',
|
||||
'error' => isset($files['error'][$i]) ? $files['error'][$i] : UPLOAD_ERR_NO_FILE,
|
||||
'size' => isset($files['size'][$i]) ? $files['size'][$i] : 0,
|
||||
);
|
||||
}
|
||||
return $normalized;
|
||||
}
|
||||
|
||||
function bratonien_tools_customer_qr_validate_image(array $file)
|
||||
{
|
||||
if ((int)$file['error'] !== UPLOAD_ERR_OK)
|
||||
{
|
||||
throw new RuntimeException('Datei-Upload fehlgeschlagen (Code '.(int)$file['error'].').');
|
||||
}
|
||||
if (empty($file['tmp_name']) || !is_uploaded_file($file['tmp_name']))
|
||||
{
|
||||
throw new RuntimeException('Die hochgeladene Datei konnte nicht verifiziert werden.');
|
||||
}
|
||||
|
||||
$image = @getimagesize($file['tmp_name']);
|
||||
$mime = is_array($image) && !empty($image['mime']) ? strtolower((string)$image['mime']) : '';
|
||||
$extensions = array(
|
||||
'image/png' => 'png',
|
||||
'image/jpeg' => 'jpg',
|
||||
'image/webp' => 'webp',
|
||||
'image/gif' => 'gif',
|
||||
);
|
||||
|
||||
if (!isset($extensions[$mime]))
|
||||
{
|
||||
throw new RuntimeException('Erlaubt sind PNG, JPG, WEBP und GIF.');
|
||||
}
|
||||
|
||||
return array(
|
||||
'mime' => $mime,
|
||||
'extension' => $extensions[$mime],
|
||||
);
|
||||
}
|
||||
|
||||
function bratonien_tools_customer_qr_unique_name($number, $extension)
|
||||
{
|
||||
try
|
||||
{
|
||||
$suffix = bin2hex(random_bytes(6));
|
||||
}
|
||||
catch (Throwable $e)
|
||||
{
|
||||
$suffix = str_replace('.', '', uniqid('', true));
|
||||
}
|
||||
|
||||
return 'qr-'.$number.'-'.$suffix.'.'.$extension;
|
||||
}
|
||||
|
||||
function bratonien_tools_customer_qr_process_uploads($year, array $files, array $numbers)
|
||||
{
|
||||
$year = bratonien_tools_customer_qr_year($year);
|
||||
$root = bratonien_tools_customer_qr_ensure_storage();
|
||||
$year_dir = $root.DIRECTORY_SEPARATOR.$year;
|
||||
if (!is_dir($year_dir) && !@mkdir($year_dir, 0770, true) && !is_dir($year_dir))
|
||||
{
|
||||
throw new RuntimeException('Jahresverzeichnis für den QR-Upload konnte nicht angelegt werden.');
|
||||
}
|
||||
|
||||
$lock_dir = $root.DIRECTORY_SEPARATOR.'.locks';
|
||||
if (!is_dir($lock_dir) && !@mkdir($lock_dir, 0770, true) && !is_dir($lock_dir))
|
||||
{
|
||||
throw new RuntimeException('QR-Lockverzeichnis konnte nicht angelegt werden.');
|
||||
}
|
||||
|
||||
$items = bratonien_tools_customer_qr_files_array($files);
|
||||
$results = array();
|
||||
$seen = array();
|
||||
|
||||
foreach ($items as $index => $file)
|
||||
{
|
||||
$display_name = trim((string)($file['name'] ?? ''));
|
||||
$number = '';
|
||||
if ($display_name === '')
|
||||
{
|
||||
$display_name = 'Datei '.($index + 1);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$number = bratonien_tools_customer_qr_number_for_year($year, isset($numbers[$index]) ? $numbers[$index] : '');
|
||||
|
||||
if (isset($seen[$number]))
|
||||
{
|
||||
$results[] = array(
|
||||
'status' => 'duplicate',
|
||||
'file' => $display_name,
|
||||
'year' => $year,
|
||||
'number' => $number,
|
||||
'message' => 'Nummer ist innerhalb dieses Batches doppelt.',
|
||||
);
|
||||
continue;
|
||||
}
|
||||
$seen[$number] = true;
|
||||
|
||||
$lock_path = $lock_dir.DIRECTORY_SEPARATOR.$year.'-'.$number.'.lock';
|
||||
$lock = @fopen($lock_path, 'c+');
|
||||
if ($lock === false || !@flock($lock, LOCK_EX))
|
||||
{
|
||||
if (is_resource($lock)) fclose($lock);
|
||||
throw new RuntimeException('Die Nummer konnte nicht sicher reserviert werden.');
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (bratonien_tools_customer_qr_exists($year, $number))
|
||||
{
|
||||
$results[] = array(
|
||||
'status' => 'duplicate',
|
||||
'file' => $display_name,
|
||||
'year' => $year,
|
||||
'number' => $number,
|
||||
'message' => 'Diese QR-Code-Nummer ist für das gewählte Jahr bereits vorhanden.',
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
$image = bratonien_tools_customer_qr_validate_image($file);
|
||||
$stored_name = bratonien_tools_customer_qr_unique_name($number, $image['extension']);
|
||||
$target = $year_dir.DIRECTORY_SEPARATOR.$stored_name;
|
||||
|
||||
if (!@move_uploaded_file($file['tmp_name'], $target))
|
||||
{
|
||||
throw new RuntimeException('Die Datei konnte nicht dauerhaft gespeichert werden.');
|
||||
}
|
||||
@chmod($target, 0660);
|
||||
|
||||
$table = bratonien_tools_customer_qr_table();
|
||||
$query = "INSERT INTO `$table` (upload_year, code_number, original_name, stored_name, mime_type, file_size, created) VALUES ("
|
||||
.(int)$year.", '".pwg_db_real_escape_string($number)."', '"
|
||||
.pwg_db_real_escape_string($display_name)."', '"
|
||||
.pwg_db_real_escape_string($stored_name)."', '"
|
||||
.pwg_db_real_escape_string($image['mime'])."', "
|
||||
.max(0, (int)($file['size'] ?? 0)).", NOW())";
|
||||
|
||||
if (pwg_query($query) === false)
|
||||
{
|
||||
@unlink($target);
|
||||
throw new RuntimeException('Der Upload konnte nicht in der QR-Code-Liste gespeichert werden.');
|
||||
}
|
||||
|
||||
$results[] = array(
|
||||
'status' => 'ok',
|
||||
'file' => $display_name,
|
||||
'year' => $year,
|
||||
'number' => $number,
|
||||
'message' => 'QR-Code erfolgreich gespeichert.',
|
||||
);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@flock($lock, LOCK_UN);
|
||||
fclose($lock);
|
||||
}
|
||||
}
|
||||
catch (Throwable $e)
|
||||
{
|
||||
$results[] = array(
|
||||
'status' => 'error',
|
||||
'file' => $display_name,
|
||||
'year' => $year,
|
||||
'number' => $number,
|
||||
'message' => $e->getMessage(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
function bratonien_tools_customer_qr_delete_upload()
|
||||
{
|
||||
$id = isset($_POST['customer_qr_id']) ? (int)$_POST['customer_qr_id'] : 0;
|
||||
if ($id < 1)
|
||||
{
|
||||
throw new InvalidArgumentException('Ungültiger QR-Upload.');
|
||||
}
|
||||
|
||||
$root = bratonien_tools_customer_qr_ensure_storage();
|
||||
$table = bratonien_tools_customer_qr_table();
|
||||
$result = pwg_query('SELECT id, upload_year, code_number, stored_name FROM `'.$table.'` WHERE id='.$id.' LIMIT 1');
|
||||
$row = pwg_db_fetch_assoc($result);
|
||||
if (!$row)
|
||||
{
|
||||
throw new RuntimeException('Der QR-Upload wurde nicht gefunden.');
|
||||
}
|
||||
|
||||
$year = bratonien_tools_customer_qr_year($row['upload_year']);
|
||||
$number = bratonien_tools_customer_qr_number_for_year($year, $row['code_number']);
|
||||
$stored_name = (string)$row['stored_name'];
|
||||
if ($stored_name === '' || basename($stored_name) !== $stored_name)
|
||||
{
|
||||
throw new RuntimeException('Der gespeicherte Dateiname ist ungültig.');
|
||||
}
|
||||
|
||||
$lock_dir = $root.DIRECTORY_SEPARATOR.'.locks';
|
||||
if (!is_dir($lock_dir) && !@mkdir($lock_dir, 0770, true) && !is_dir($lock_dir))
|
||||
{
|
||||
throw new RuntimeException('QR-Lockverzeichnis konnte nicht angelegt werden.');
|
||||
}
|
||||
|
||||
$lock_path = $lock_dir.DIRECTORY_SEPARATOR.$year.'-'.$number.'.lock';
|
||||
$lock = @fopen($lock_path, 'c+');
|
||||
if ($lock === false || !@flock($lock, LOCK_EX))
|
||||
{
|
||||
if (is_resource($lock)) fclose($lock);
|
||||
throw new RuntimeException('Der QR-Upload konnte nicht sicher gesperrt werden.');
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$file = $root.DIRECTORY_SEPARATOR.$year.DIRECTORY_SEPARATOR.$stored_name;
|
||||
if (is_file($file) && !@unlink($file))
|
||||
{
|
||||
throw new RuntimeException('Die QR-Datei konnte nicht gelöscht werden. Der Datenbankeintrag bleibt erhalten.');
|
||||
}
|
||||
|
||||
if (pwg_query('DELETE FROM `'.$table.'` WHERE id='.$id.' LIMIT 1') === false)
|
||||
{
|
||||
throw new RuntimeException('Der QR-Datenbankeintrag konnte nicht gelöscht werden.');
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
@flock($lock, LOCK_UN);
|
||||
fclose($lock);
|
||||
}
|
||||
|
||||
return array(
|
||||
'message' => 'QR-Code '.$number.' aus '.$year.' gelöscht. Die Nummer ist wieder frei.',
|
||||
);
|
||||
}
|
||||
|
||||
function bratonien_tools_customer_qr_size_label($bytes)
|
||||
{
|
||||
$bytes = max(0, (int)$bytes);
|
||||
if ($bytes >= 1048576)
|
||||
{
|
||||
return number_format($bytes / 1048576, 1, ',', '.').' MB';
|
||||
}
|
||||
if ($bytes >= 1024)
|
||||
{
|
||||
return number_format($bytes / 1024, 0, ',', '.').' KB';
|
||||
}
|
||||
return $bytes.' B';
|
||||
}
|
||||
|
||||
function bratonien_tools_customer_qr_admin_data()
|
||||
{
|
||||
bratonien_tools_customer_qr_ensure_storage();
|
||||
$settings = bratonien_tools_customer_qr_settings();
|
||||
$table = bratonien_tools_customer_qr_table();
|
||||
$limits = bratonien_tools_customer_qr_year_limits();
|
||||
$counts = array_fill_keys(array_keys($limits), 0);
|
||||
$total = 0;
|
||||
$current_year = bratonien_tools_customer_qr_default_year();
|
||||
|
||||
$result = pwg_query('SELECT upload_year, COUNT(*) AS cnt FROM `'.$table.'` GROUP BY upload_year');
|
||||
while ($row = pwg_db_fetch_assoc($result))
|
||||
{
|
||||
$year = (int)$row['upload_year'];
|
||||
$count = (int)$row['cnt'];
|
||||
$total += $count;
|
||||
if (isset($counts[$year]))
|
||||
{
|
||||
$counts[$year] = $count;
|
||||
}
|
||||
}
|
||||
|
||||
$years = array();
|
||||
foreach ($limits as $year => $capacity)
|
||||
{
|
||||
$used = (int)$counts[$year];
|
||||
$years[] = array(
|
||||
'year' => (int)$year,
|
||||
'capacity' => (int)$capacity,
|
||||
'used' => $used,
|
||||
'remaining' => max(0, (int)$capacity - $used),
|
||||
);
|
||||
}
|
||||
|
||||
$uploads = array();
|
||||
$upload_result = pwg_query(
|
||||
'SELECT id, upload_year, code_number, original_name, file_size, created '
|
||||
.'FROM `'.$table.'` '
|
||||
.'ORDER BY upload_year DESC, CAST(code_number AS UNSIGNED) ASC, id ASC'
|
||||
);
|
||||
while ($row = pwg_db_fetch_assoc($upload_result))
|
||||
{
|
||||
$id = (int)$row['id'];
|
||||
$uploads[] = array(
|
||||
'id' => $id,
|
||||
'year' => (int)$row['upload_year'],
|
||||
'number' => (string)$row['code_number'],
|
||||
'original_name' => (string)$row['original_name'],
|
||||
'file_size' => (int)$row['file_size'],
|
||||
'file_size_label' => bratonien_tools_customer_qr_size_label($row['file_size']),
|
||||
'created' => (string)$row['created'],
|
||||
'preview_url' => get_absolute_root_url(true).'plugins/'.BRATONIEN_TOOLS_ID.'/customer-qr-admin-file.php?id='.$id,
|
||||
);
|
||||
}
|
||||
|
||||
$year_keys = array_keys($limits);
|
||||
sort($year_keys, SORT_NUMERIC);
|
||||
|
||||
return array(
|
||||
'enabled' => !empty($settings['enabled']),
|
||||
'url' => get_absolute_root_url(true).'plugins/'.BRATONIEN_TOOLS_ID.'/customer-qr-upload.php',
|
||||
'year_min' => (int)reset($year_keys),
|
||||
'year_max' => (int)end($year_keys),
|
||||
'default_year' => $current_year,
|
||||
'years' => $years,
|
||||
'uploads' => $uploads,
|
||||
'total' => $total,
|
||||
'current_year_total' => (int)$counts[$current_year],
|
||||
'current_year_capacity' => (int)$limits[$current_year],
|
||||
'max_files' => max(1, (int)ini_get('max_file_uploads')),
|
||||
);
|
||||
}
|
||||
@@ -20,6 +20,7 @@ function bratonien_tools_create_tables()
|
||||
$profiles = bratonien_tools_table('watermark_profiles');
|
||||
$rules = bratonien_tools_table('watermark_rules');
|
||||
$nc_connections = bratonien_tools_table('nc_connections');
|
||||
$customer_qr = bratonien_tools_table('customer_qr_uploads');
|
||||
|
||||
pwg_query("CREATE TABLE IF NOT EXISTS `$profiles` (
|
||||
id int(11) NOT NULL AUTO_INCREMENT,
|
||||
@@ -81,6 +82,20 @@ function bratonien_tools_create_tables()
|
||||
UNIQUE KEY connection_key (connection_key)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;");
|
||||
|
||||
pwg_query("CREATE TABLE IF NOT EXISTS `$customer_qr` (
|
||||
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
upload_year smallint(5) unsigned NOT NULL,
|
||||
code_number varchar(32) NOT NULL,
|
||||
original_name varchar(255) NOT NULL,
|
||||
stored_name varchar(255) NOT NULL,
|
||||
mime_type varchar(100) NOT NULL,
|
||||
file_size bigint(20) unsigned NOT NULL DEFAULT 0,
|
||||
created datetime NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY year_code (upload_year, code_number),
|
||||
KEY upload_year (upload_year)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;");
|
||||
|
||||
if (function_exists('bratonien_tools_create_album_shares_table'))
|
||||
{
|
||||
bratonien_tools_create_album_shares_table();
|
||||
@@ -92,6 +107,7 @@ function bratonien_tools_drop_tables()
|
||||
pwg_query('DROP TABLE IF EXISTS `'.bratonien_tools_table('watermark_profiles').'`');
|
||||
pwg_query('DROP TABLE IF EXISTS `'.bratonien_tools_table('watermark_rules').'`');
|
||||
pwg_query('DROP TABLE IF EXISTS `'.bratonien_tools_table('nc_connections').'`');
|
||||
pwg_query('DROP TABLE IF EXISTS `'.bratonien_tools_table('customer_qr_uploads').'`');
|
||||
|
||||
if (function_exists('bratonien_tools_drop_album_shares_table'))
|
||||
{
|
||||
|
||||
320
include/friendship_code_upload.inc.php
Normal file
320
include/friendship_code_upload.inc.php
Normal file
@@ -0,0 +1,320 @@
|
||||
<?php
|
||||
if (!defined('PHPWG_ROOT_PATH'))
|
||||
{
|
||||
die('Hacking attempt!');
|
||||
}
|
||||
|
||||
function bratonien_tools_friendship_code_table()
|
||||
{
|
||||
return $GLOBALS['prefixeTable'].'bratonien_tools_friendship_codes';
|
||||
}
|
||||
|
||||
function bratonien_tools_friendship_code_storage_root()
|
||||
{
|
||||
return PHPWG_ROOT_PATH.PWG_LOCAL_DIR.'bratonien-friendship-codes';
|
||||
}
|
||||
|
||||
function bratonien_tools_friendship_code_ensure_storage()
|
||||
{
|
||||
$table = bratonien_tools_friendship_code_table();
|
||||
pwg_query("CREATE TABLE IF NOT EXISTS `$table` (
|
||||
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
person_name varchar(190) NOT NULL,
|
||||
normalized_name varchar(190) NOT NULL,
|
||||
original_name varchar(255) NOT NULL,
|
||||
stored_name varchar(255) NOT NULL,
|
||||
mime_type varchar(100) NOT NULL,
|
||||
file_size bigint(20) unsigned NOT NULL DEFAULT 0,
|
||||
file_sha256 char(64) NOT NULL,
|
||||
created datetime NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY normalized_name (normalized_name),
|
||||
UNIQUE KEY file_sha256 (file_sha256)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;");
|
||||
|
||||
$root = bratonien_tools_friendship_code_storage_root();
|
||||
if (!is_dir($root) && !@mkdir($root, 0770, true) && !is_dir($root))
|
||||
{
|
||||
throw new RuntimeException('Verzeichnis für Freundschaftscodes konnte nicht angelegt werden.');
|
||||
}
|
||||
|
||||
return $root;
|
||||
}
|
||||
|
||||
function bratonien_tools_friendship_code_name($value)
|
||||
{
|
||||
$name = trim((string)$value);
|
||||
$name = preg_replace('/\s+/u', ' ', $name);
|
||||
if ($name === '')
|
||||
{
|
||||
throw new InvalidArgumentException('Bitte einen Namen eingeben.');
|
||||
}
|
||||
|
||||
$length = function_exists('mb_strlen') ? mb_strlen($name, 'UTF-8') : strlen($name);
|
||||
if ($length > 120)
|
||||
{
|
||||
throw new InvalidArgumentException('Der Name darf höchstens 120 Zeichen lang sein.');
|
||||
}
|
||||
|
||||
if (preg_match('/[\x00-\x1F\x7F]/u', $name))
|
||||
{
|
||||
throw new InvalidArgumentException('Der Name enthält ungültige Steuerzeichen.');
|
||||
}
|
||||
|
||||
return $name;
|
||||
}
|
||||
|
||||
function bratonien_tools_friendship_code_normalized_name($value)
|
||||
{
|
||||
$name = bratonien_tools_friendship_code_name($value);
|
||||
return function_exists('mb_strtolower')
|
||||
? mb_strtolower($name, 'UTF-8')
|
||||
: strtolower($name);
|
||||
}
|
||||
|
||||
function bratonien_tools_friendship_code_exists_for_name($name)
|
||||
{
|
||||
bratonien_tools_friendship_code_ensure_storage();
|
||||
$normalized = bratonien_tools_friendship_code_normalized_name($name);
|
||||
$table = bratonien_tools_friendship_code_table();
|
||||
$query = "SELECT id FROM `$table` WHERE normalized_name='"
|
||||
.pwg_db_real_escape_string($normalized)."' LIMIT 1";
|
||||
return pwg_db_num_rows(pwg_query($query)) > 0;
|
||||
}
|
||||
|
||||
function bratonien_tools_friendship_code_validate_image(array $file)
|
||||
{
|
||||
if ((int)($file['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK)
|
||||
{
|
||||
throw new RuntimeException('Datei-Upload fehlgeschlagen (Code '.(int)($file['error'] ?? UPLOAD_ERR_NO_FILE).').');
|
||||
}
|
||||
if (empty($file['tmp_name']) || !is_uploaded_file($file['tmp_name']))
|
||||
{
|
||||
throw new RuntimeException('Die hochgeladene Datei konnte nicht verifiziert werden.');
|
||||
}
|
||||
|
||||
$image = @getimagesize($file['tmp_name']);
|
||||
$mime = is_array($image) && !empty($image['mime']) ? strtolower((string)$image['mime']) : '';
|
||||
$extensions = array(
|
||||
'image/png' => 'png',
|
||||
'image/jpeg' => 'jpg',
|
||||
'image/webp' => 'webp',
|
||||
'image/gif' => 'gif',
|
||||
);
|
||||
if (!isset($extensions[$mime]))
|
||||
{
|
||||
throw new RuntimeException('Erlaubt sind PNG, JPG, WEBP und GIF.');
|
||||
}
|
||||
|
||||
return array(
|
||||
'mime' => $mime,
|
||||
'extension' => $extensions[$mime],
|
||||
);
|
||||
}
|
||||
|
||||
function bratonien_tools_friendship_code_unique_name($extension)
|
||||
{
|
||||
try
|
||||
{
|
||||
$suffix = bin2hex(random_bytes(8));
|
||||
}
|
||||
catch (Throwable $e)
|
||||
{
|
||||
$suffix = str_replace('.', '', uniqid('', true));
|
||||
}
|
||||
return 'friendship-'.$suffix.'.'.$extension;
|
||||
}
|
||||
|
||||
function bratonien_tools_friendship_code_process_upload($name, array $file)
|
||||
{
|
||||
$name = bratonien_tools_friendship_code_name($name);
|
||||
$normalized = bratonien_tools_friendship_code_normalized_name($name);
|
||||
$root = bratonien_tools_friendship_code_ensure_storage();
|
||||
$table = bratonien_tools_friendship_code_table();
|
||||
|
||||
$lock_dir = $root.DIRECTORY_SEPARATOR.'.locks';
|
||||
if (!is_dir($lock_dir) && !@mkdir($lock_dir, 0770, true) && !is_dir($lock_dir))
|
||||
{
|
||||
throw new RuntimeException('Lockverzeichnis für Freundschaftscodes konnte nicht angelegt werden.');
|
||||
}
|
||||
|
||||
$lock_path = $lock_dir.DIRECTORY_SEPARATOR.sha1($normalized).'.lock';
|
||||
$lock = @fopen($lock_path, 'c+');
|
||||
if ($lock === false || !@flock($lock, LOCK_EX))
|
||||
{
|
||||
if (is_resource($lock)) fclose($lock);
|
||||
throw new RuntimeException('Der Freundschaftscode konnte nicht sicher reserviert werden.');
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (bratonien_tools_friendship_code_exists_for_name($name))
|
||||
{
|
||||
return array(
|
||||
'status' => 'duplicate',
|
||||
'kind' => 'friendship',
|
||||
'name' => $name,
|
||||
'file' => trim((string)($file['name'] ?? '')),
|
||||
'message' => 'Für diesen Namen ist bereits ein Freundschaftscode hinterlegt.',
|
||||
);
|
||||
}
|
||||
|
||||
$image = bratonien_tools_friendship_code_validate_image($file);
|
||||
$sha256 = @hash_file('sha256', $file['tmp_name']);
|
||||
if (!is_string($sha256) || !preg_match('/^[a-f0-9]{64}$/', $sha256))
|
||||
{
|
||||
throw new RuntimeException('Der Freundschaftscode konnte nicht eindeutig geprüft werden.');
|
||||
}
|
||||
|
||||
$hash_query = "SELECT person_name FROM `$table` WHERE file_sha256='"
|
||||
.pwg_db_real_escape_string($sha256)."' LIMIT 1";
|
||||
$hash_result = pwg_query($hash_query);
|
||||
if (pwg_db_num_rows($hash_result) > 0)
|
||||
{
|
||||
$existing = pwg_db_fetch_assoc($hash_result);
|
||||
return array(
|
||||
'status' => 'duplicate',
|
||||
'kind' => 'friendship',
|
||||
'name' => $name,
|
||||
'file' => trim((string)($file['name'] ?? '')),
|
||||
'message' => 'Dieser Freundschaftscode wurde bereits hinterlegt'.(!empty($existing['person_name']) ? ' ('.(string)$existing['person_name'].')' : '').'.',
|
||||
);
|
||||
}
|
||||
|
||||
$stored_name = bratonien_tools_friendship_code_unique_name($image['extension']);
|
||||
$target = $root.DIRECTORY_SEPARATOR.$stored_name;
|
||||
if (!@move_uploaded_file($file['tmp_name'], $target))
|
||||
{
|
||||
throw new RuntimeException('Die Datei konnte nicht dauerhaft gespeichert werden.');
|
||||
}
|
||||
@chmod($target, 0660);
|
||||
|
||||
$original_name = trim((string)($file['name'] ?? ''));
|
||||
if ($original_name === '')
|
||||
{
|
||||
$original_name = 'Freundschaftscode.'.$image['extension'];
|
||||
}
|
||||
|
||||
$query = "INSERT INTO `$table` (person_name, normalized_name, original_name, stored_name, mime_type, file_size, file_sha256, created) VALUES ('"
|
||||
.pwg_db_real_escape_string($name)."', '"
|
||||
.pwg_db_real_escape_string($normalized)."', '"
|
||||
.pwg_db_real_escape_string($original_name)."', '"
|
||||
.pwg_db_real_escape_string($stored_name)."', '"
|
||||
.pwg_db_real_escape_string($image['mime'])."', "
|
||||
.max(0, (int)($file['size'] ?? 0)).", '"
|
||||
.pwg_db_real_escape_string($sha256)."', NOW())";
|
||||
|
||||
if (pwg_query($query) === false)
|
||||
{
|
||||
@unlink($target);
|
||||
throw new RuntimeException('Der Freundschaftscode konnte nicht in der Liste gespeichert werden.');
|
||||
}
|
||||
|
||||
return array(
|
||||
'status' => 'ok',
|
||||
'kind' => 'friendship',
|
||||
'name' => $name,
|
||||
'file' => $original_name,
|
||||
'message' => 'Freundschaftscode erfolgreich gespeichert.',
|
||||
);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@flock($lock, LOCK_UN);
|
||||
fclose($lock);
|
||||
}
|
||||
}
|
||||
|
||||
function bratonien_tools_friendship_code_delete_upload()
|
||||
{
|
||||
$id = isset($_POST['friendship_code_id']) ? (int)$_POST['friendship_code_id'] : 0;
|
||||
if ($id < 1)
|
||||
{
|
||||
throw new InvalidArgumentException('Ungültiger Freundschaftscode.');
|
||||
}
|
||||
|
||||
$root = bratonien_tools_friendship_code_ensure_storage();
|
||||
$table = bratonien_tools_friendship_code_table();
|
||||
$result = pwg_query('SELECT id, person_name, normalized_name, stored_name FROM `'.$table.'` WHERE id='.$id.' LIMIT 1');
|
||||
$row = pwg_db_fetch_assoc($result);
|
||||
if (!$row)
|
||||
{
|
||||
throw new RuntimeException('Der Freundschaftscode wurde nicht gefunden.');
|
||||
}
|
||||
|
||||
$stored_name = (string)$row['stored_name'];
|
||||
if ($stored_name === '' || basename($stored_name) !== $stored_name)
|
||||
{
|
||||
throw new RuntimeException('Der gespeicherte Dateiname ist ungültig.');
|
||||
}
|
||||
|
||||
$normalized = (string)$row['normalized_name'];
|
||||
$lock_dir = $root.DIRECTORY_SEPARATOR.'.locks';
|
||||
if (!is_dir($lock_dir) && !@mkdir($lock_dir, 0770, true) && !is_dir($lock_dir))
|
||||
{
|
||||
throw new RuntimeException('Lockverzeichnis für Freundschaftscodes konnte nicht angelegt werden.');
|
||||
}
|
||||
|
||||
$lock = @fopen($lock_dir.DIRECTORY_SEPARATOR.sha1($normalized).'.lock', 'c+');
|
||||
if ($lock === false || !@flock($lock, LOCK_EX))
|
||||
{
|
||||
if (is_resource($lock)) fclose($lock);
|
||||
throw new RuntimeException('Der Freundschaftscode konnte nicht sicher gesperrt werden.');
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$file = $root.DIRECTORY_SEPARATOR.$stored_name;
|
||||
if (is_file($file) && !@unlink($file))
|
||||
{
|
||||
throw new RuntimeException('Die Datei konnte nicht gelöscht werden. Der Datenbankeintrag bleibt erhalten.');
|
||||
}
|
||||
|
||||
if (pwg_query('DELETE FROM `'.$table.'` WHERE id='.$id.' LIMIT 1') === false)
|
||||
{
|
||||
throw new RuntimeException('Der Datenbankeintrag konnte nicht gelöscht werden.');
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
@flock($lock, LOCK_UN);
|
||||
fclose($lock);
|
||||
}
|
||||
|
||||
return array(
|
||||
'message' => 'Freundschaftscode von '.(string)$row['person_name'].' gelöscht.',
|
||||
);
|
||||
}
|
||||
|
||||
function bratonien_tools_friendship_code_admin_data()
|
||||
{
|
||||
bratonien_tools_friendship_code_ensure_storage();
|
||||
$table = bratonien_tools_friendship_code_table();
|
||||
$uploads = array();
|
||||
|
||||
$result = pwg_query(
|
||||
'SELECT id, person_name, original_name, file_size, created FROM `'.$table.'` ORDER BY person_name ASC, id ASC'
|
||||
);
|
||||
while ($row = pwg_db_fetch_assoc($result))
|
||||
{
|
||||
$id = (int)$row['id'];
|
||||
$base_url = get_absolute_root_url(true).'plugins/'.BRATONIEN_TOOLS_ID.'/friendship-code-admin-file.php?id='.$id;
|
||||
$uploads[] = array(
|
||||
'id' => $id,
|
||||
'name' => (string)$row['person_name'],
|
||||
'original_name' => (string)$row['original_name'],
|
||||
'file_size' => (int)$row['file_size'],
|
||||
'file_size_label' => function_exists('bratonien_tools_customer_qr_size_label')
|
||||
? bratonien_tools_customer_qr_size_label($row['file_size'])
|
||||
: (string)((int)$row['file_size']).' B',
|
||||
'created' => (string)$row['created'],
|
||||
'preview_url' => $base_url,
|
||||
'download_url' => $base_url.'&download=1',
|
||||
);
|
||||
}
|
||||
|
||||
return array(
|
||||
'total' => count($uploads),
|
||||
'uploads' => $uploads,
|
||||
);
|
||||
}
|
||||
@@ -4,7 +4,7 @@ if (!defined('PHPWG_ROOT_PATH'))
|
||||
die('Hacking attempt!');
|
||||
}
|
||||
|
||||
function bratonien_tools_nc_connector_systemctl_run(array $args)
|
||||
function bratonien_tools_nc_connector_command_run(array $command)
|
||||
{
|
||||
$result = array('exit'=>-1, 'stdout'=>'', 'stderr'=>'');
|
||||
if (!function_exists('proc_open'))
|
||||
@@ -13,7 +13,6 @@ function bratonien_tools_nc_connector_systemctl_run(array $args)
|
||||
return $result;
|
||||
}
|
||||
|
||||
$command = array_merge(array('/usr/bin/systemctl'), $args);
|
||||
$spec = array(
|
||||
0=>array('file','/dev/null','r'),
|
||||
1=>array('pipe','w'),
|
||||
@@ -23,7 +22,7 @@ function bratonien_tools_nc_connector_systemctl_run(array $args)
|
||||
$process = @proc_open($command, $spec, $pipes, null, $environment);
|
||||
if (!is_resource($process))
|
||||
{
|
||||
$result['stderr'] = 'systemctl konnte nicht gestartet werden.';
|
||||
$result['stderr'] = 'Prozess konnte nicht gestartet werden.';
|
||||
return $result;
|
||||
}
|
||||
|
||||
@@ -35,6 +34,11 @@ function bratonien_tools_nc_connector_systemctl_run(array $args)
|
||||
return $result;
|
||||
}
|
||||
|
||||
function bratonien_tools_nc_connector_systemctl_run(array $args)
|
||||
{
|
||||
return bratonien_tools_nc_connector_command_run(array_merge(array('/usr/bin/systemctl'), $args));
|
||||
}
|
||||
|
||||
function bratonien_tools_nc_connector_systemctl_value(array $args)
|
||||
{
|
||||
$result = bratonien_tools_nc_connector_systemctl_run($args);
|
||||
@@ -43,49 +47,51 @@ function bratonien_tools_nc_connector_systemctl_value(array $args)
|
||||
|
||||
function bratonien_tools_nc_connector_run_now()
|
||||
{
|
||||
if (!function_exists('proc_open'))
|
||||
$service = 'bratonien-nc-connector.service';
|
||||
$load_state = bratonien_tools_nc_connector_systemctl_value(array('show',$service,'--property=LoadState','--value'));
|
||||
if ($load_state !== 'loaded')
|
||||
{
|
||||
throw new RuntimeException('WebDAV-Abgleich konnte nicht gestartet werden: proc_open ist in PHP nicht verfügbar.');
|
||||
throw new RuntimeException('WebDAV-Abgleich konnte nicht gestartet werden: der privilegierte Connector-Dienst '.$service.' ist nicht geladen.');
|
||||
}
|
||||
|
||||
$runtime_dir = realpath(dirname(__DIR__).'/runtime');
|
||||
if ($runtime_dir === false || !is_dir($runtime_dir))
|
||||
// Der Connector verwaltet bewusst geschützte Runtime-Daten unter /etc und
|
||||
// /var/lib. Er darf deshalb niemals direkt als Webserver-Benutzer laufen.
|
||||
// Der manuelle Admin-Button startet denselben privilegierten systemd-Dienst,
|
||||
// den auch der Timer verwendet.
|
||||
$result = bratonien_tools_nc_connector_systemctl_run(array('start',$service));
|
||||
|
||||
// Auf Installationen ohne passende polkit-Regel darf ausschließlich die
|
||||
// eng begrenzte, vom Betreiber eingerichtete sudo-Freigabe für genau diesen
|
||||
// systemd-Dienst verwendet werden. Es gibt absichtlich keinen Fallback auf
|
||||
// runtime/run-all.sh als www-data.
|
||||
if ($result['exit'] !== 0 && is_file('/usr/bin/sudo') && is_executable('/usr/bin/sudo'))
|
||||
{
|
||||
throw new RuntimeException('WebDAV-Abgleich konnte nicht gestartet werden: Runtime-Verzeichnis wurde nicht gefunden.');
|
||||
$result = bratonien_tools_nc_connector_command_run(array(
|
||||
'/usr/bin/sudo',
|
||||
'-n',
|
||||
'/usr/bin/systemctl',
|
||||
'start',
|
||||
$service,
|
||||
));
|
||||
}
|
||||
|
||||
$script = $runtime_dir.'/run-all.sh';
|
||||
if (!is_file($script) || !is_readable($script))
|
||||
if ($result['exit'] !== 0)
|
||||
{
|
||||
throw new RuntimeException('WebDAV-Abgleich konnte nicht gestartet werden: runtime/run-all.sh ist nicht lesbar.');
|
||||
$detail = $result['stderr'] !== '' ? $result['stderr'] : $result['stdout'];
|
||||
if ($detail === '') $detail = 'Exit-Code '.$result['exit'];
|
||||
throw new RuntimeException(
|
||||
'WebDAV-Abgleich konnte den privilegierten Connector-Dienst nicht starten. '
|
||||
.'Der Connector wird aus Sicherheitsgründen nicht als Webserver-Benutzer direkt ausgeführt. '.$detail
|
||||
);
|
||||
}
|
||||
|
||||
$spec = array(
|
||||
0=>array('file','/dev/null','r'),
|
||||
1=>array('pipe','w'),
|
||||
2=>array('pipe','w'),
|
||||
);
|
||||
$environment = array_merge($_ENV, array('LC_ALL'=>'C','LANG'=>'C'));
|
||||
$process = @proc_open(array('/usr/bin/env','bash',$script), $spec, $pipes, $runtime_dir, $environment);
|
||||
if (!is_resource($process))
|
||||
$service_result = bratonien_tools_nc_connector_systemctl_value(array('show',$service,'--property=Result','--value'));
|
||||
if ($service_result !== '' && $service_result !== 'success')
|
||||
{
|
||||
throw new RuntimeException('WebDAV-Abgleich konnte nicht gestartet werden: Connector-Prozess konnte nicht geöffnet werden.');
|
||||
throw new RuntimeException('WebDAV-Abgleich wurde gestartet, der Connector-Dienst endete aber mit Ergebnis '.$service_result.'.');
|
||||
}
|
||||
|
||||
$stdout = trim((string)stream_get_contents($pipes[1]));
|
||||
$stderr = trim((string)stream_get_contents($pipes[2]));
|
||||
fclose($pipes[1]);
|
||||
fclose($pipes[2]);
|
||||
$exit = (int)proc_close($process);
|
||||
|
||||
if ($exit !== 0)
|
||||
{
|
||||
$detail = $stderr !== '' ? $stderr : $stdout;
|
||||
if ($detail === '') $detail = 'Exit-Code '.$exit;
|
||||
throw new RuntimeException('WebDAV-Abgleich fehlgeschlagen: '.$detail);
|
||||
}
|
||||
|
||||
return array('message'=>'WebDAV-Abgleich wurde ausgeführt.');
|
||||
return array('message'=>'WebDAV-Abgleich wurde über den privilegierten Connector-Dienst ausgeführt.');
|
||||
}
|
||||
|
||||
function bratonien_tools_nc_connector_parse_systemd_time($value)
|
||||
|
||||
161
include/presentation_refresh.inc.php
Normal file
161
include/presentation_refresh.inc.php
Normal file
@@ -0,0 +1,161 @@
|
||||
<?php
|
||||
if (!defined('PHPWG_ROOT_PATH'))
|
||||
{
|
||||
die('Hacking attempt!');
|
||||
}
|
||||
|
||||
function bratonien_tools_presentation_refresh_queue_dir()
|
||||
{
|
||||
return PHPWG_ROOT_PATH.PWG_LOCAL_DIR.'bratonien-presentation-refresh';
|
||||
}
|
||||
|
||||
function bratonien_tools_presentation_refresh_normalize_categories(array $category_ids)
|
||||
{
|
||||
$result = array();
|
||||
foreach ($category_ids as $category_id)
|
||||
{
|
||||
$category_id = (int)$category_id;
|
||||
if ($category_id > 0) $result[$category_id] = $category_id;
|
||||
}
|
||||
ksort($result, SORT_NUMERIC);
|
||||
return array_values($result);
|
||||
}
|
||||
|
||||
function bratonien_tools_presentation_refresh_start()
|
||||
{
|
||||
if (!function_exists('exec'))
|
||||
{
|
||||
error_log('Bratonien presentation refresh: PHP exec() ist deaktiviert; Auftrag bleibt in der Queue.');
|
||||
return false;
|
||||
}
|
||||
|
||||
$php = function_exists('bratonien_tools_webdav_warmup_php_cli')
|
||||
? bratonien_tools_webdav_warmup_php_cli()
|
||||
: null;
|
||||
if (!$php)
|
||||
{
|
||||
foreach (array('/usr/bin/php', '/usr/bin/php8.4', '/usr/bin/php8.3', '/usr/local/bin/php') as $candidate)
|
||||
{
|
||||
if (is_file($candidate) && is_executable($candidate))
|
||||
{
|
||||
$php = $candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!$php)
|
||||
{
|
||||
error_log('Bratonien presentation refresh: PHP-CLI wurde nicht gefunden; Auftrag bleibt in der Queue.');
|
||||
return false;
|
||||
}
|
||||
|
||||
$worker = realpath(BRATONIEN_TOOLS_PATH.'runtime/piwigo-presentation-refresh.php');
|
||||
if (!$worker || !is_file($worker))
|
||||
{
|
||||
error_log('Bratonien presentation refresh: Worker fehlt; Auftrag bleibt in der Queue.');
|
||||
return false;
|
||||
}
|
||||
|
||||
$log = PHPWG_ROOT_PATH.PWG_LOCAL_DIR.'bratonien-presentation-refresh.log';
|
||||
$command = 'nohup '.escapeshellarg($php).' '.escapeshellarg($worker)
|
||||
.' >> '.escapeshellarg($log).' 2>&1 < /dev/null & echo $!';
|
||||
$output = array();
|
||||
$exit = 1;
|
||||
@exec($command, $output, $exit);
|
||||
return $exit === 0 && !empty($output[0]) && (int)$output[0] > 0;
|
||||
}
|
||||
|
||||
function bratonien_tools_presentation_refresh_enqueue(array $category_ids, $reason='presentation-changed', $all=false)
|
||||
{
|
||||
$category_ids = bratonien_tools_presentation_refresh_normalize_categories($category_ids);
|
||||
$all = (bool)$all;
|
||||
if (!$all && !$category_ids) return array('queued'=>false, 'started'=>false);
|
||||
|
||||
$dir = bratonien_tools_presentation_refresh_queue_dir();
|
||||
if (!is_dir($dir) && !@mkdir($dir, 0775, true) && !is_dir($dir))
|
||||
{
|
||||
throw new RuntimeException('Queue für die Vorschau-Aktualisierung konnte nicht angelegt werden.');
|
||||
}
|
||||
|
||||
$payload = array(
|
||||
'all'=>$all,
|
||||
'categories'=>$category_ids,
|
||||
'reason'=>(string)$reason,
|
||||
'requested_at'=>time(),
|
||||
);
|
||||
$json = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
|
||||
if ($json === false) throw new RuntimeException('Vorschau-Auftrag konnte nicht serialisiert werden.');
|
||||
|
||||
$name = sprintf('%010d-%s.json', time(), bin2hex(random_bytes(6)));
|
||||
$target = $dir.'/'.$name;
|
||||
$tmp = $target.'.tmp';
|
||||
if (@file_put_contents($tmp, $json."\n", LOCK_EX) === false)
|
||||
{
|
||||
throw new RuntimeException('Vorschau-Auftrag konnte nicht geschrieben werden.');
|
||||
}
|
||||
@chmod($tmp, 0664);
|
||||
if (!@rename($tmp, $target))
|
||||
{
|
||||
@unlink($tmp);
|
||||
throw new RuntimeException('Vorschau-Auftrag konnte nicht atomar bereitgestellt werden.');
|
||||
}
|
||||
|
||||
return array('queued'=>true, 'started'=>bratonien_tools_presentation_refresh_start());
|
||||
}
|
||||
|
||||
function bratonien_tools_presentation_refresh_enqueue_all($reason='presentation-changed')
|
||||
{
|
||||
return bratonien_tools_presentation_refresh_enqueue(array(), $reason, true);
|
||||
}
|
||||
|
||||
function bratonien_tools_presentation_refresh_category_snapshot()
|
||||
{
|
||||
$snapshot = array();
|
||||
$result = pwg_query('SELECT id, status, visible FROM '.CATEGORIES_TABLE.' ORDER BY id');
|
||||
while ($row = pwg_db_fetch_assoc($result))
|
||||
{
|
||||
$snapshot[(int)$row['id']] = array(
|
||||
'status'=>(string)$row['status'],
|
||||
'visible'=>(string)$row['visible'],
|
||||
);
|
||||
}
|
||||
return $snapshot;
|
||||
}
|
||||
|
||||
function bratonien_tools_presentation_refresh_watch_admin_categories()
|
||||
{
|
||||
if (!defined('IN_ADMIN') || ($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') return;
|
||||
|
||||
// Connector-Sync hat einen eigenen, priorisierten Bildcache-Pfad. Neue
|
||||
// Kategorien werden dort bereits korrekt an den Worker übergeben.
|
||||
if ((string)($_GET['page'] ?? '') === 'site_update' && (string)($_POST['bratonien_connector'] ?? '') === '1') return;
|
||||
|
||||
$before = bratonien_tools_presentation_refresh_category_snapshot();
|
||||
register_shutdown_function(function () use ($before) {
|
||||
try
|
||||
{
|
||||
$after = bratonien_tools_presentation_refresh_category_snapshot();
|
||||
$changed = array();
|
||||
foreach ($after as $category_id=>$state)
|
||||
{
|
||||
if (!isset($before[$category_id])) continue;
|
||||
if (
|
||||
(string)$before[$category_id]['status'] !== (string)$state['status']
|
||||
|| (string)$before[$category_id]['visible'] !== (string)$state['visible']
|
||||
)
|
||||
{
|
||||
$changed[] = (int)$category_id;
|
||||
}
|
||||
}
|
||||
|
||||
if ($changed)
|
||||
{
|
||||
bratonien_tools_presentation_refresh_enqueue($changed, 'album-status-or-visibility-changed');
|
||||
}
|
||||
}
|
||||
catch (Throwable $e)
|
||||
{
|
||||
error_log('Bratonien presentation refresh watcher: '.$e->getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -12,10 +12,13 @@ require_once(BRATONIEN_TOOLS_PATH . 'tools/watermark_settings.inc.php');
|
||||
require_once(BRATONIEN_TOOLS_PATH . 'tools/album_rules.inc.php');
|
||||
require_once(BRATONIEN_TOOLS_PATH . 'tools/asset_manager.inc.php');
|
||||
require_once(BRATONIEN_TOOLS_PATH . 'include/watermark_engine.inc.php');
|
||||
require_once(BRATONIEN_TOOLS_PATH . 'include/presentation_refresh.inc.php');
|
||||
require_once(BRATONIEN_TOOLS_PATH . 'include/public_selection.inc.php');
|
||||
require_once(BRATONIEN_TOOLS_PATH . 'include/self_update.inc.php');
|
||||
require_once(BRATONIEN_TOOLS_PATH . 'include/album_shares.inc.php');
|
||||
require_once(BRATONIEN_TOOLS_PATH . 'include/album_lock.inc.php');
|
||||
require_once(BRATONIEN_TOOLS_PATH . 'include/customer_qr_upload.inc.php');
|
||||
require_once(BRATONIEN_TOOLS_PATH . 'include/friendship_code_upload.inc.php');
|
||||
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector.inc.php');
|
||||
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_piwigo_api.inc.php');
|
||||
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_wizard.inc.php');
|
||||
@@ -32,7 +35,7 @@ function bratonien_tools_get_tools()
|
||||
return array(
|
||||
'image_cache_clear' => array('handler' => 'bratonien_tools_clear_image_cache_atomic'),
|
||||
'image_cache_build' => array('handler' => 'bratonien_tools_start_combined_image_cache_build'),
|
||||
'image_cache_cancel' => array('handler' => 'bratonien_tools_cancel_main_cache_build'),
|
||||
'image_cache_cancel' => array('handler' => 'bratonien_tools_cancel_combined_image_cache_build'),
|
||||
'image_cache_worker_settings' => array('handler' => 'bratonien_tools_save_cache_worker_settings'),
|
||||
'image_cache_webdav_warmup_settings' => array('handler' => 'bratonien_tools_save_webdav_warmup_settings'),
|
||||
'image_cache_webdav_warmup_manual' => array('handler' => 'bratonien_tools_start_webdav_warmup_manual'),
|
||||
@@ -47,6 +50,9 @@ function bratonien_tools_get_tools()
|
||||
'watermark_defaults' => array('handler' => 'bratonien_tools_save_watermark_defaults'),
|
||||
'watermark_rule' => array('handler' => 'bratonien_tools_save_album_rule'),
|
||||
'public_selection_settings' => array('handler' => 'bratonien_tools_save_public_selection_settings'),
|
||||
'customer_qr_settings' => array('handler' => 'bratonien_tools_customer_qr_save_settings'),
|
||||
'customer_qr_delete' => array('handler' => 'bratonien_tools_customer_qr_delete_upload'),
|
||||
'friendship_code_delete' => array('handler' => 'bratonien_tools_friendship_code_delete_upload'),
|
||||
'asset_upload' => array('handler' => 'bratonien_tools_upload_asset'),
|
||||
'asset_delete' => array('handler' => 'bratonien_tools_delete_asset'),
|
||||
'asset_upload_limits' => array('handler' => 'bratonien_tools_save_upload_limits'),
|
||||
@@ -81,9 +87,14 @@ function bratonien_tools_handle_watermark_engine()
|
||||
$enabled = !empty($_POST['engine_enabled']);
|
||||
bratonien_tools_set_watermark_engine($enabled);
|
||||
|
||||
if (function_exists('bratonien_tools_presentation_refresh_enqueue_all'))
|
||||
{
|
||||
bratonien_tools_presentation_refresh_enqueue_all('watermark-engine-changed');
|
||||
}
|
||||
|
||||
return array(
|
||||
'message' => $enabled
|
||||
? 'Bratonien-Wasserzeichenverwaltung aktiviert. Piwigos bisherige Wasserzeichen-Nutzung wurde gesichert und unterdrueckt.'
|
||||
: 'Bratonien-Wasserzeichenverwaltung deaktiviert. Die zuvor gesicherte Piwigo-Wasserzeichen-Nutzung wurde wiederhergestellt.',
|
||||
? 'Bratonien-Wasserzeichenverwaltung aktiviert. Piwigos bisherige Wasserzeichen-Nutzung wurde gesichert und die Vorschauen werden aktualisiert.'
|
||||
: 'Bratonien-Wasserzeichenverwaltung deaktiviert. Die zuvor gesicherte Piwigo-Wasserzeichen-Nutzung wurde wiederhergestellt und die Vorschauen werden aktualisiert.',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -82,16 +82,37 @@ function bratonien_tools_runtime_b64url_encode($value)
|
||||
return rtrim(strtr(base64_encode($value), '+/', '-_'), '=');
|
||||
}
|
||||
|
||||
function bratonien_tools_profile_watermark_relative(array $profile)
|
||||
{
|
||||
$relative = ltrim(trim((string)($profile['watermark_file'] ?? '')), '/');
|
||||
if ($relative !== '')
|
||||
{
|
||||
return $relative;
|
||||
}
|
||||
|
||||
// Ein Profil ohne eigene Datei bedeutet bewusst: Einstellungen dieses
|
||||
// Profils verwenden, aber die zentrale Basis-Wasserzeichendatei erben.
|
||||
// Dadurch kann das Standardprofil "Oeffentlich" ohne doppelte Dateipflege
|
||||
// fuer jedes öffentliche Album als Schutzregel dienen.
|
||||
if (function_exists('bratonien_tools_get_base_watermark_config'))
|
||||
{
|
||||
$base = bratonien_tools_get_base_watermark_config();
|
||||
$relative = ltrim(trim((string)($base['file'] ?? '')), '/');
|
||||
}
|
||||
|
||||
return $relative;
|
||||
}
|
||||
|
||||
function bratonien_tools_profile_watermark_path(array $profile)
|
||||
{
|
||||
static $paths = array();
|
||||
|
||||
if (empty($profile['watermark_file']))
|
||||
$relative = bratonien_tools_profile_watermark_relative($profile);
|
||||
if ($relative === '')
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
$relative = ltrim((string)$profile['watermark_file'], '/');
|
||||
if (array_key_exists($relative, $paths))
|
||||
{
|
||||
return $paths[$relative];
|
||||
@@ -119,9 +140,10 @@ function bratonien_tools_profile_watermark_path(array $profile)
|
||||
function bratonien_tools_runtime_profile_version(array $profile)
|
||||
{
|
||||
$watermark_path = bratonien_tools_profile_watermark_path($profile);
|
||||
$effective_file = bratonien_tools_profile_watermark_relative($profile);
|
||||
$parts = array(
|
||||
$profile['id'] ?? 0,
|
||||
$profile['watermark_file'] ?? '',
|
||||
$effective_file,
|
||||
$profile['scale_percent'] ?? 100,
|
||||
$profile['xpos'] ?? 90,
|
||||
$profile['ypos'] ?? 90,
|
||||
|
||||
@@ -4,6 +4,11 @@ if (!defined('PHPWG_ROOT_PATH'))
|
||||
die('Hacking attempt!');
|
||||
}
|
||||
|
||||
if (!defined('BRATONIEN_WEBDAV_GENERATION_VERSION'))
|
||||
{
|
||||
define('BRATONIEN_WEBDAV_GENERATION_VERSION', 2);
|
||||
}
|
||||
|
||||
function bratonien_tools_webdav_source_index_file($state_dir, $connection_id=0)
|
||||
{
|
||||
// Der Quellenindex gehoert dem Piwigo-Worker und muss deshalb in einem
|
||||
@@ -87,6 +92,7 @@ function bratonien_tools_webdav_source_index_key(array $source)
|
||||
function bratonien_tools_webdav_source_index_signature(array $source)
|
||||
{
|
||||
return sha1(implode('|', array(
|
||||
'generation-v'.(int)BRATONIEN_WEBDAV_GENERATION_VERSION,
|
||||
(int)($source['connection_id'] ?? 0),
|
||||
(int)($source['root_fileid'] ?? 0),
|
||||
(int)($source['fileid'] ?? 0),
|
||||
@@ -114,6 +120,7 @@ function bratonien_tools_webdav_source_index_metadata(array $source, $shadow_rel
|
||||
'height'=>(int)($source['height'] ?? 0),
|
||||
'shadow_relative'=>ltrim((string)$shadow_relative, '/'),
|
||||
'source_signature'=>bratonien_tools_webdav_source_index_signature($source),
|
||||
'generation_version'=>(int)BRATONIEN_WEBDAV_GENERATION_VERSION,
|
||||
'last_seen_at'=>time(),
|
||||
);
|
||||
}
|
||||
@@ -122,6 +129,7 @@ function bratonien_tools_webdav_source_index_is_current(array $entry, $signature
|
||||
{
|
||||
$signature = (string)$signature;
|
||||
return isset($entry['source_signature'], $entry['stage1_signature'], $entry['stage2_signature'])
|
||||
&& (int)($entry['generation_version'] ?? 0) === (int)BRATONIEN_WEBDAV_GENERATION_VERSION
|
||||
&& hash_equals((string)$entry['source_signature'], $signature)
|
||||
&& hash_equals((string)$entry['stage1_signature'], $signature)
|
||||
&& hash_equals((string)$entry['stage2_signature'], $signature);
|
||||
|
||||
@@ -57,6 +57,11 @@ function bratonien_tools_webdav_warmup_status_file_for_connection($connection_id
|
||||
return PHPWG_ROOT_PATH.PWG_LOCAL_DIR.'bratonien-webdav-warmup.status-'.(int)$connection_id.'.json';
|
||||
}
|
||||
|
||||
function bratonien_tools_webdav_warmup_cancel_file_for_connection($connection_id)
|
||||
{
|
||||
return PHPWG_ROOT_PATH.PWG_LOCAL_DIR.'bratonien-webdav-warmup.cancel-'.(int)$connection_id;
|
||||
}
|
||||
|
||||
function bratonien_tools_webdav_warmup_write_status($connection_id, $state, $message, array $extra=array())
|
||||
{
|
||||
$file = bratonien_tools_webdav_warmup_status_file_for_connection($connection_id);
|
||||
@@ -180,6 +185,10 @@ function bratonien_tools_start_webdav_warmup_mode($mode, $message)
|
||||
$run_id = date('YmdHis').'-'.bin2hex(random_bytes(4));
|
||||
foreach ($connections as $connection_id=>$runtime)
|
||||
{
|
||||
// Ein altes Abbruchsignal darf niemals den nächsten bewusst gestarteten
|
||||
// Lauf stoppen. Es wird unmittelbar vor dem neuen Start entfernt.
|
||||
@unlink(bratonien_tools_webdav_warmup_cancel_file_for_connection($connection_id));
|
||||
|
||||
bratonien_tools_webdav_warmup_write_status(
|
||||
$connection_id,
|
||||
'queued',
|
||||
@@ -224,6 +233,72 @@ function bratonien_tools_start_webdav_cache_rebuild()
|
||||
);
|
||||
}
|
||||
|
||||
function bratonien_tools_request_webdav_cache_cancel()
|
||||
{
|
||||
$requested = 0;
|
||||
foreach (bratonien_tools_webdav_warmup_connections() as $connection_id=>$runtime)
|
||||
{
|
||||
$status_file = bratonien_tools_webdav_warmup_status_file_for_connection($connection_id);
|
||||
$status = null;
|
||||
if (is_file($status_file) && is_readable($status_file))
|
||||
{
|
||||
$raw = @file_get_contents($status_file);
|
||||
$status = $raw !== false ? json_decode($raw, true) : null;
|
||||
}
|
||||
if (!is_array($status)) $status = array();
|
||||
$state = (string)($status['state'] ?? 'idle');
|
||||
if (!in_array($state, array('queued','scan','running','cancelling'), true)) continue;
|
||||
|
||||
$cancel = bratonien_tools_webdav_warmup_cancel_file_for_connection($connection_id);
|
||||
if (@file_put_contents($cancel, (string)time()."\n", LOCK_EX) === false)
|
||||
{
|
||||
throw new RuntimeException('WebDAV-Abbruchsignal für Verbindung #'.(int)$connection_id.' konnte nicht geschrieben werden.');
|
||||
}
|
||||
@chmod($cancel, 0664);
|
||||
|
||||
$extra = $status;
|
||||
unset($extra['state'], $extra['message'], $extra['connection_id'], $extra['updated_at']);
|
||||
$extra['cancel_requested'] = true;
|
||||
bratonien_tools_webdav_warmup_write_status(
|
||||
$connection_id,
|
||||
'cancelling',
|
||||
'Abbruch angefordert. Der aktuell laufende Batch wird noch vollständig beendet; danach werden keine weiteren Nextcloud-Originale geladen.',
|
||||
$extra
|
||||
);
|
||||
$requested++;
|
||||
}
|
||||
return $requested;
|
||||
}
|
||||
|
||||
function bratonien_tools_cancel_combined_image_cache_build()
|
||||
{
|
||||
$webdav = bratonien_tools_request_webdav_cache_cancel();
|
||||
$local = false;
|
||||
if (function_exists('bratonien_tools_main_cache_process_active') && function_exists('bratonien_tools_main_cache_is_running'))
|
||||
{
|
||||
$local = bratonien_tools_main_cache_process_active() || bratonien_tools_main_cache_is_running();
|
||||
}
|
||||
if ($local && function_exists('bratonien_tools_request_main_cache_cancel'))
|
||||
{
|
||||
bratonien_tools_request_main_cache_cancel();
|
||||
}
|
||||
|
||||
if ($webdav > 0 && $local)
|
||||
{
|
||||
return array('message'=>'Cache-Abbruch angefordert. Der aktuelle WebDAV-Batch wird noch beendet; lokale Cache-Worker erhalten ebenfalls sofort das Abbruchsignal.');
|
||||
}
|
||||
if ($webdav > 0)
|
||||
{
|
||||
return array('message'=>'WebDAV-Cache-Abbruch angefordert. Der aktuell laufende Batch wird noch vollständig beendet; danach ist Schluss.');
|
||||
}
|
||||
if ($local)
|
||||
{
|
||||
return array('message'=>'Lokaler Piwigo-Cache-Abbruch wurde angefordert.');
|
||||
}
|
||||
|
||||
return array('message'=>'Es läuft aktuell kein Cache-Aufbau.');
|
||||
}
|
||||
|
||||
function bratonien_tools_has_local_cache_sources()
|
||||
{
|
||||
if (!function_exists('bratonien_tools_webdav_materialize_source_info')) return true;
|
||||
|
||||
@@ -40,7 +40,7 @@ function bratonien_tools_status_webdav_label(array $status)
|
||||
$state = (string)($status['state'] ?? 'idle');
|
||||
$prefix = $connection > 0 ? 'WebDAV #'.$connection.': ' : 'WebDAV: ';
|
||||
|
||||
if ($state === 'queued') return $prefix.((string)($status['message'] ?? '') !== '' ? (string)$status['message'] : 'Worker wartet auf den Start.');
|
||||
if ($state === 'queued' || $state === 'waiting') return $prefix.((string)($status['message'] ?? '') !== '' ? (string)$status['message'] : 'Worker wartet auf den nächsten sicheren Startpunkt.');
|
||||
if ($state === 'scan')
|
||||
{
|
||||
$sources = (int)($status['shadow_sources'] ?? 0);
|
||||
@@ -64,6 +64,9 @@ function bratonien_tools_status_webdav_label(array $status)
|
||||
if ($total > 0) $parts[] = $completed.' / '.$total.' Quellen dieser Stufe abgeschlossen';
|
||||
return implode(' · ', $parts).'.';
|
||||
}
|
||||
if ($state === 'cancelling') return $prefix.((string)($status['message'] ?? '') !== '' ? (string)$status['message'] : 'Abbruch angefordert; aktueller Batch wird noch beendet.');
|
||||
if ($state === 'cancelled') return $prefix.((string)($status['message'] ?? '') !== '' ? (string)$status['message'] : 'Cache-Aufbau wurde abgebrochen.');
|
||||
if ($state === 'partial') return $prefix.((string)($status['message'] ?? '') !== '' ? (string)$status['message'] : 'Einzelne Quellen sind noch offen und werden beim nächsten Lauf erneut versucht.');
|
||||
if ($state === 'preempted') return $prefix.'Stufe 2 wurde nach einem vollständigen Batch unterbrochen, damit neue Connector-Inhalte zuerst verarbeitet werden können.';
|
||||
if ($state === 'complete') return $prefix.((string)($status['message'] ?? '') !== '' ? (string)$status['message'] : 'Verarbeitung abgeschlossen.');
|
||||
if ($state === 'error' || $state === 'fatal') return $prefix.'FEHLER: '.((string)($status['message'] ?? '') !== '' ? (string)$status['message'] : 'WebDAV-Verarbeitung fehlgeschlagen.');
|
||||
@@ -93,6 +96,10 @@ usort($webdav, function($a, $b) {
|
||||
});
|
||||
|
||||
$webdav_active = false;
|
||||
$webdav_waiting = false;
|
||||
$webdav_partial = false;
|
||||
$webdav_cancelling = false;
|
||||
$webdav_cancelled = false;
|
||||
$webdav_error = false;
|
||||
$webdav_pending = false;
|
||||
$webdav_latest = 0;
|
||||
@@ -114,22 +121,34 @@ foreach ($webdav as &$status)
|
||||
$status['message'] = 'Worker wurde angefordert, hat aber innerhalb von 45 Sekunden keinen Scan gestartet.';
|
||||
}
|
||||
|
||||
// Ältere Dispatcher-Versionen konnten den Zustand "waiting" schreiben und
|
||||
// anschließend ohne Fortsetzungsauftrag enden. Dieser reine Statuswert ist
|
||||
// kein Beweis für einen laufenden Connector und darf die Cache-Seite nicht
|
||||
// dauerhaft blockieren. Neue Wartezustände tragen resume_scheduled=true und
|
||||
// besitzen einen echten Hintergrund-Warter.
|
||||
if ($state === 'waiting' && empty($status['resume_scheduled']) && $updated > 0 && (time() - $updated) > 45)
|
||||
{
|
||||
$state = 'idle';
|
||||
$status['state'] = 'idle';
|
||||
$status['message'] = 'Veralteter Connector-Wartezustand wurde verworfen; aktuell ist kein fortsetzbarer Cache-Auftrag registriert.';
|
||||
}
|
||||
|
||||
if ($state === 'scan')
|
||||
{
|
||||
$selected = max(0, (int)($status['selected'] ?? 0));
|
||||
$webdav_source_total += $selected;
|
||||
$webdav_progress_total += $selected * 2;
|
||||
}
|
||||
elseif ($state === 'running')
|
||||
elseif ($state === 'running' || $state === 'cancelling')
|
||||
{
|
||||
$selected = max(0, (int)($status['selected_total'] ?? 0));
|
||||
$selected = max(0, (int)($status['selected_total'] ?? $status['selected'] ?? 0));
|
||||
$stage = max(1, (int)($status['stage'] ?? 1));
|
||||
$stage_completed = max(0, min($selected, (int)($status['stage_completed'] ?? 0)));
|
||||
$webdav_source_total += $selected;
|
||||
$webdav_progress_total += $selected * 2;
|
||||
$webdav_progress_completed += $stage >= 2 ? $selected + $stage_completed : $stage_completed;
|
||||
}
|
||||
elseif ($state === 'complete' || $state === 'error' || $state === 'fatal' || $state === 'preempted')
|
||||
elseif (in_array($state, array('complete','error','fatal','preempted','cancelled','waiting','partial'), true))
|
||||
{
|
||||
$selected = max(0, (int)($status['selected'] ?? 0));
|
||||
if ($selected > 0)
|
||||
@@ -138,16 +157,22 @@ foreach ($webdav as &$status)
|
||||
$stage2_completed = max(0, (int)($status['stage2_completed'] ?? 0));
|
||||
$stage1_failed = max(0, (int)($status['stage1_failed'] ?? 0));
|
||||
$stage2_failed = max(0, (int)($status['stage2_failed'] ?? 0));
|
||||
$stage1_deferred = max(0, (int)($status['stage1_deferred'] ?? 0));
|
||||
$stage2_deferred = max(0, (int)($status['stage2_deferred'] ?? 0));
|
||||
$webdav_source_total += $selected;
|
||||
$webdav_progress_total += $selected * 2;
|
||||
$webdav_progress_completed += min($selected * 2, $stage1_completed + $stage2_completed + $stage1_failed + $stage2_failed);
|
||||
$webdav_progress_completed += min($selected * 2, $stage1_completed + $stage2_completed + $stage1_failed + $stage2_failed + $stage1_deferred + $stage2_deferred);
|
||||
}
|
||||
}
|
||||
|
||||
if (in_array($state, array('queued','scan','running'), true)) $webdav_active = true;
|
||||
if ($state === 'waiting') $webdav_waiting = true;
|
||||
if ($state === 'partial') $webdav_partial = true;
|
||||
if ($state === 'cancelling') $webdav_cancelling = true;
|
||||
if ($state === 'cancelled') $webdav_cancelled = true;
|
||||
if ($state === 'preempted') $webdav_pending = true;
|
||||
if (in_array($state, array('error','fatal'), true)) $webdav_error = true;
|
||||
if ($state !== 'complete') $webdav_all_complete = false;
|
||||
if ($state !== 'complete' && $state !== 'idle') $webdav_all_complete = false;
|
||||
$webdav_lines[] = bratonien_tools_status_webdav_label($status);
|
||||
}
|
||||
unset($status);
|
||||
@@ -155,7 +180,7 @@ unset($status);
|
||||
$local_state = (string)($local['state'] ?? 'idle');
|
||||
$local_updated = (int)($local['updated_at'] ?? 0);
|
||||
$local_stale = in_array($local_state, array('running','queued'), true) && $local_updated > 0 && (time() - $local_updated) > 45;
|
||||
if ($local_stale && !$webdav_active)
|
||||
if ($local_stale && !$webdav_active && !$webdav_cancelling && !$webdav_waiting)
|
||||
{
|
||||
$local['state'] = 'error';
|
||||
$local['message'] = 'Der lokale Piwigo-Teil liefert seit mehr als 45 Sekunden keinen Fortschritt.';
|
||||
@@ -180,7 +205,15 @@ $local_completed = max(0, min($local_total, (int)($local['completed'] ?? 0)));
|
||||
$combined_total = $local_total + $webdav_progress_total;
|
||||
$combined_completed = $local_completed + min($webdav_progress_total, $webdav_progress_completed);
|
||||
|
||||
if ($webdav_active)
|
||||
if ($webdav_cancelling || $local_state === 'cancelling')
|
||||
{
|
||||
$overall['state'] = 'cancelling';
|
||||
$overall['total'] = $combined_total;
|
||||
$overall['completed'] = $combined_completed;
|
||||
$overall['message'] = 'Cache-Aufbau wird nach dem aktuell laufenden Batch beendet'.($webdav_source_total > 0 ? ' · '.$webdav_source_total.' WebDAV-Quellen' : '').'.';
|
||||
$overall['current'] = implode(' ', $webdav_lines);
|
||||
}
|
||||
elseif ($webdav_active)
|
||||
{
|
||||
$overall['state'] = 'running';
|
||||
$overall['total'] = $combined_total;
|
||||
@@ -191,6 +224,23 @@ if ($webdav_active)
|
||||
$overall['message'] = 'Gesamtaufbau läuft'.($webdav_source_total > 0 ? ' · '.$webdav_source_total.' WebDAV-Quellen' : '').'. '.$local_label;
|
||||
$overall['current'] = implode(' ', $webdav_lines);
|
||||
}
|
||||
elseif ($webdav_waiting)
|
||||
{
|
||||
$overall['state'] = 'queued';
|
||||
$overall['total'] = $combined_total;
|
||||
$overall['completed'] = $combined_completed;
|
||||
$overall['message'] = 'Cache-Aufbau pausiert für laufende Connector-Synchronisierung'.($webdav_source_total > 0 ? ' · '.$webdav_source_total.' WebDAV-Quellen' : '').'. Der Fortsetzungsauftrag bleibt registriert.';
|
||||
$overall['current'] = implode(' ', $webdav_lines);
|
||||
}
|
||||
elseif ($webdav_partial)
|
||||
{
|
||||
$overall['state'] = 'queued';
|
||||
$overall['total'] = $combined_total;
|
||||
$overall['completed'] = $combined_completed;
|
||||
$overall['errors'] = 0;
|
||||
$overall['message'] = 'Cache-Aufbau bleibt fortsetzbar: einzelne Quellen sind noch offen; der restliche Bestand wurde weiterverarbeitet.';
|
||||
$overall['current'] = implode(' ', $webdav_lines);
|
||||
}
|
||||
elseif ($webdav_error || $local_state === 'error')
|
||||
{
|
||||
$overall['state'] = 'error';
|
||||
@@ -211,6 +261,14 @@ elseif ($webdav_pending)
|
||||
$overall['message'] = 'Cache-Aufbau wartet auf priorisierte Connector-Inhalte'.($webdav_source_total > 0 ? ' · '.$webdav_source_total.' WebDAV-Quellen' : '').'. '.$local_label;
|
||||
$overall['current'] = implode(' ', $webdav_lines);
|
||||
}
|
||||
elseif ($webdav_cancelled || $local_state === 'cancelled')
|
||||
{
|
||||
$overall['state'] = 'cancelled';
|
||||
$overall['total'] = $combined_total;
|
||||
$overall['completed'] = $combined_completed;
|
||||
$overall['message'] = 'Cache-Aufbau wurde nach dem letzten vollständig abgeschlossenen Batch beendet.';
|
||||
$overall['current'] = implode(' ', $webdav_lines);
|
||||
}
|
||||
elseif ($webdav_all_complete && $local_state === 'complete')
|
||||
{
|
||||
$overall['state'] = 'complete';
|
||||
@@ -224,9 +282,6 @@ elseif ($webdav_all_complete && $local_state === 'complete')
|
||||
}
|
||||
else
|
||||
{
|
||||
// Ein alter complete-Status darf niemals einen neuen Lauf als fertig markieren.
|
||||
// Idle/unklare WebDAV-Zustände bleiben sichtbar, bis ein echter Lauf einen
|
||||
// aktuellen queued/scan/running/complete/error-Zustand geschrieben hat.
|
||||
$overall['state'] = $local_state === 'complete' ? 'idle' : $local_state;
|
||||
$overall['message'] = $local_label;
|
||||
if ($webdav_lines) $overall['current'] = implode(' ', $webdav_lines);
|
||||
|
||||
23
main.inc.php
23
main.inc.php
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
/*
|
||||
Plugin Name: Bratonien Tools
|
||||
Version: 0.9.7.1.27
|
||||
Version: 0.9.7.1.51
|
||||
Description: Erweiterbare Administrationswerkzeuge fuer die Bratonien-Piwigo-Installation.
|
||||
Plugin URI: https://github.com/Terranom674/Piwigo_Bratonien_Tools
|
||||
Author: Bratonien
|
||||
@@ -19,12 +19,15 @@ require_once(BRATONIEN_TOOLS_PATH . 'include/watermark_runtime.inc.php');
|
||||
require_once(BRATONIEN_TOOLS_PATH . 'include/webdav_image_runtime.inc.php');
|
||||
require_once(BRATONIEN_TOOLS_PATH . 'include/webdav_cache_validation.inc.php');
|
||||
require_once(BRATONIEN_TOOLS_PATH . 'include/webdav_materialize_runtime.inc.php');
|
||||
require_once(BRATONIEN_TOOLS_PATH . 'include/presentation_refresh.inc.php');
|
||||
require_once(BRATONIEN_TOOLS_PATH . 'include/public_selection.inc.php');
|
||||
require_once(BRATONIEN_TOOLS_PATH . 'include/picture_navigation.inc.php');
|
||||
require_once(BRATONIEN_TOOLS_PATH . 'include/batch_titles.inc.php');
|
||||
require_once(BRATONIEN_TOOLS_PATH . 'include/album_shares.inc.php');
|
||||
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_orphan_ws.inc.php');
|
||||
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_productive_ws.inc.php');
|
||||
require_once(BRATONIEN_TOOLS_PATH . 'include/customer_qr_upload.inc.php');
|
||||
require_once(BRATONIEN_TOOLS_PATH . 'include/customer_qr_frontend_menu.inc.php');
|
||||
|
||||
add_event_handler('get_admin_plugin_menu_links', 'bratonien_tools_admin_menu');
|
||||
add_event_handler('get_derivative_url', 'bratonien_tools_filter_derivative_url', EVENT_HANDLER_PRIORITY_NEUTRAL, 4);
|
||||
@@ -36,8 +39,10 @@ add_event_handler('init', 'bratonien_tools_prepare_connector_private_import', EV
|
||||
add_event_handler('init', 'bratonien_tools_prepare_private_album_permissions', EVENT_HANDLER_PRIORITY_NEUTRAL - 20);
|
||||
add_event_handler('init', 'bratonien_tools_preserve_private_album_access', EVENT_HANDLER_PRIORITY_NEUTRAL - 10);
|
||||
add_event_handler('init', 'bratonien_tools_preserve_connector_top_level_access', EVENT_HANDLER_PRIORITY_NEUTRAL - 9);
|
||||
add_event_handler('init', 'bratonien_tools_presentation_refresh_watch_admin_categories', EVENT_HANDLER_PRIORITY_NEUTRAL - 8);
|
||||
add_event_handler('init', 'bratonien_tools_album_shares_init');
|
||||
add_event_handler('loc_end_intro', 'bratonien_tools_fix_admin_album_stat_tile');
|
||||
add_event_handler('loc_end_page_tail', 'bratonien_tools_add_legal_footer_links');
|
||||
add_event_handler('delete_categories', 'bratonien_tools_album_shares_on_delete_categories');
|
||||
add_event_handler('ws_add_methods', 'bratonien_tools_register_nc_orphan_ws_methods');
|
||||
add_event_handler('ws_add_methods', 'bratonien_tools_register_nc_productive_ws_methods');
|
||||
@@ -181,6 +186,22 @@ function bratonien_tools_prefilter_admin_album_stat_tile($source)
|
||||
return str_replace('{if $NB_ALBUMS > 1}', '{if $NB_ALBUMS > 0}', $source);
|
||||
}
|
||||
|
||||
function bratonien_tools_add_legal_footer_links()
|
||||
{
|
||||
global $template;
|
||||
|
||||
if (defined('IN_ADMIN'))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
$template->append(
|
||||
'footer_elements',
|
||||
' - <a href="https://links.bratonien.de/@bratonien_cosplay#bratonien-popup-856170002" rel="noopener">Impressum</a>'
|
||||
.' - <a href="https://links.bratonien.de/@bratonien_cosplay#bratonien-popup-907192090" rel="noopener">Datenschutzerklärung</a>'
|
||||
);
|
||||
}
|
||||
|
||||
function bratonien_tools_admin_menu($menu)
|
||||
{
|
||||
$menu[] = array(
|
||||
|
||||
46
runtime/lib/bratonien-watermark-call.php
Normal file
46
runtime/lib/bratonien-watermark-call.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
if (PHP_SAPI !== 'cli')
|
||||
{
|
||||
http_response_code(404);
|
||||
exit;
|
||||
}
|
||||
|
||||
$query = '';
|
||||
foreach ($argv as $arg)
|
||||
{
|
||||
if (strpos($arg, '--query=') === 0)
|
||||
{
|
||||
$query = substr($arg, strlen('--query='));
|
||||
}
|
||||
}
|
||||
if ($query === '')
|
||||
{
|
||||
fwrite(STDERR, "Parameter --query fehlt.\n");
|
||||
exit(2);
|
||||
}
|
||||
|
||||
$piwigo_root = realpath(dirname(__DIR__, 4));
|
||||
$plugin_root = realpath(dirname(__DIR__, 2));
|
||||
if ($piwigo_root === false || $plugin_root === false || !is_file($plugin_root.'/watermark.php'))
|
||||
{
|
||||
fwrite(STDERR, "Piwigo-Root oder Bratonien-Wasserzeichenmodul wurde nicht gefunden.\n");
|
||||
exit(3);
|
||||
}
|
||||
|
||||
$plugin_id = basename($plugin_root);
|
||||
parse_str($query, $_GET);
|
||||
$_SERVER['REMOTE_ADDR'] = '127.0.0.1';
|
||||
$_SERVER['SERVER_ADDR'] = '127.0.0.1';
|
||||
$_SERVER['SERVER_NAME'] = 'localhost';
|
||||
$_SERVER['HTTP_HOST'] = 'localhost';
|
||||
$_SERVER['SERVER_PORT'] = '80';
|
||||
$_SERVER['REQUEST_METHOD'] = 'GET';
|
||||
$_SERVER['REQUEST_URI'] = '/plugins/'.$plugin_id.'/watermark.php?'.$query;
|
||||
$_SERVER['SCRIPT_NAME'] = '/plugins/'.$plugin_id.'/watermark.php';
|
||||
$_SERVER['PHP_SELF'] = $_SERVER['SCRIPT_NAME'];
|
||||
$_SERVER['QUERY_STRING'] = $query;
|
||||
$_SERVER['HTTP_USER_AGENT'] = 'Bratonien-Presentation-Refresh/0.9.7.1.41';
|
||||
$_SERVER['HTTPS'] = 'off';
|
||||
|
||||
chdir($piwigo_root);
|
||||
include $plugin_root.'/watermark.php';
|
||||
@@ -32,6 +32,17 @@ SUPPORTED_IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".gif", ".webp"}
|
||||
PLACEHOLDER = base64.b64decode("R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==")
|
||||
|
||||
|
||||
class WebDavAuthenticationError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
class WebDavPathUnavailable(RuntimeError):
|
||||
def __init__(self, relative: str, status: int) -> None:
|
||||
self.relative = validate_relative(relative)
|
||||
self.status = int(status)
|
||||
super().__init__(f"Nextcloud path unavailable with HTTP {self.status}: {self.relative or '/'}")
|
||||
|
||||
|
||||
def fail(message: str) -> None:
|
||||
raise RuntimeError(message)
|
||||
|
||||
@@ -136,8 +147,10 @@ class WebDavClient:
|
||||
status = response.status
|
||||
payload = response.read()
|
||||
except urllib.error.HTTPError as error:
|
||||
if error.code in {401, 403}:
|
||||
fail("Nextcloud rejected the WebDAV credentials or directory access")
|
||||
if error.code == 401:
|
||||
raise WebDavAuthenticationError("Nextcloud rejected the WebDAV credentials") from error
|
||||
if error.code in {403, 404}:
|
||||
raise WebDavPathUnavailable(relative, error.code) from error
|
||||
fail(f"Nextcloud PROPFIND failed with HTTP {error.code}")
|
||||
except urllib.error.URLError as error:
|
||||
fail(f"Nextcloud WebDAV is unreachable: {error.reason}")
|
||||
@@ -189,6 +202,25 @@ class WebDavClient:
|
||||
return current, children
|
||||
|
||||
|
||||
def confirmed_removed_root(client: WebDavClient, remote_root: str, error: WebDavPathUnavailable) -> bool:
|
||||
remote_root = validate_relative(remote_root)
|
||||
if remote_root == "":
|
||||
return False
|
||||
if error.status == 404:
|
||||
return True
|
||||
if error.status != 403:
|
||||
return False
|
||||
|
||||
parent_path = PurePosixPath(remote_root).parent
|
||||
parent = "" if str(parent_path) == "." else str(parent_path).strip("/")
|
||||
_, siblings = client.list_collection(parent)
|
||||
wanted = PurePosixPath(remote_root).name.casefold()
|
||||
for sibling in siblings:
|
||||
if str(sibling.get("display_name", "")).casefold() == wanted:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def link_placeholder(seed: Path, target: Path) -> None:
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
try:
|
||||
@@ -301,14 +333,34 @@ def main() -> int:
|
||||
staging.mkdir(parents=True)
|
||||
|
||||
client = WebDavClient(args.base_url, args.user, password, max(1, args.timeout))
|
||||
|
||||
# A successful request against the user's WebDAV root proves that the
|
||||
# credentials and the Nextcloud endpoint are healthy. Only after this
|
||||
# guard may an individual configured root be interpreted as removed.
|
||||
webdav_home, _ = client.list_collection("")
|
||||
|
||||
mapping: dict[str, dict[str, object]] = {}
|
||||
manifest: list[str] = []
|
||||
total_files = total_folders = total_skipped = 0
|
||||
used_names: set[str] = set()
|
||||
removed_roots: list[dict[str, object]] = []
|
||||
|
||||
for remote_root_raw in args.root:
|
||||
remote_root = validate_relative(remote_root_raw)
|
||||
current, _ = client.list_collection(remote_root)
|
||||
try:
|
||||
current, _ = client.list_collection(remote_root)
|
||||
except WebDavPathUnavailable as error:
|
||||
if not confirmed_removed_root(client, remote_root, error):
|
||||
raise RuntimeError(
|
||||
f"Configured WebDAV root cannot be read safely; existing mirror is preserved: {remote_root or '/'} (HTTP {error.status})"
|
||||
) from error
|
||||
removed_roots.append({"path": remote_root, "status": error.status})
|
||||
print(
|
||||
f"webdav-placeholder: configured root no longer available and will be removed from the mirror: {remote_root or '/'} (HTTP {error.status})",
|
||||
file=sys.stderr,
|
||||
)
|
||||
continue
|
||||
|
||||
fileid = int(current["fileid"])
|
||||
display = str(current.get("display_name", "")).strip() or (PurePosixPath(remote_root).name if remote_root else args.user)
|
||||
local_name = f"root-{fileid}"
|
||||
@@ -357,6 +409,19 @@ def main() -> int:
|
||||
final_path = source_text + path[len(staging_text):] if path.startswith(staging_text) else path
|
||||
final_mapping[final_path] = data
|
||||
|
||||
# Der Worker erwartet ein syntaktisch nicht-leeres Connector-Mapping,
|
||||
# filtert aber selbst ausschließlich kind=file. Wenn alle ausgewählten
|
||||
# Roots entzogen wurden, hält dieser echte WebDAV-Home-Verzeichniseintrag
|
||||
# das Mapping gültig, während der Shadowtree bewusst null Bildquellen hat.
|
||||
if not final_mapping:
|
||||
final_mapping[source_text] = {
|
||||
"kind": "folder",
|
||||
"fileid": int(webdav_home["fileid"]),
|
||||
"webdav_path": "",
|
||||
"display_name": str(webdav_home.get("display_name", "")) or args.user,
|
||||
"control_entry": True,
|
||||
}
|
||||
|
||||
atomic_text(args.manifest, "\n".join(manifest) + "\n")
|
||||
atomic_json(args.mapping, {
|
||||
"version": 1,
|
||||
@@ -366,6 +431,9 @@ def main() -> int:
|
||||
})
|
||||
print(json.dumps({
|
||||
"roots": len(args.root),
|
||||
"roots_available": len(args.root) - len(removed_roots),
|
||||
"roots_removed": len(removed_roots),
|
||||
"removed_root_details": removed_roots,
|
||||
"files": total_files,
|
||||
"folders": total_folders,
|
||||
"skipped": total_skipped,
|
||||
|
||||
@@ -23,11 +23,12 @@ $_SERVER['REQUEST_URI'] = '/';
|
||||
$_SERVER['SCRIPT_NAME'] = '/plugins/bratonien_tools/runtime/lib/piwigo-cache-request-by-shadow.php';
|
||||
$_SERVER['PHP_SELF'] = $_SERVER['SCRIPT_NAME'];
|
||||
$_SERVER['QUERY_STRING'] = '';
|
||||
$_SERVER['HTTP_USER_AGENT'] = 'Bratonien-Piwigo-Shadow-Cache-Bridge/0.9.7.1.17';
|
||||
$_SERVER['HTTP_USER_AGENT'] = 'Bratonien-Piwigo-Shadow-Cache-Bridge/0.9.7.1.41';
|
||||
$_SERVER['HTTPS'] = 'off';
|
||||
|
||||
require_once(PHPWG_ROOT_PATH.'include/common.inc.php');
|
||||
require_once(PHPWG_ROOT_PATH.'include/derivative.inc.php');
|
||||
require_once(PHPWG_ROOT_PATH.'admin/include/image.class.php');
|
||||
if (!defined('BRATONIEN_TOOLS_PATH'))
|
||||
{
|
||||
fwrite(STDERR, "Bratonien Tools ist nicht aktiv.\n");
|
||||
@@ -72,6 +73,43 @@ function bratonien_tools_shadow_bridge_call_i($request, &$detail=null)
|
||||
return true;
|
||||
}
|
||||
|
||||
function bratonien_tools_shadow_bridge_sync_actual_metadata(array &$row, $source_path)
|
||||
{
|
||||
$size = @getimagesize($source_path);
|
||||
if (!is_array($size) || (int)($size[0] ?? 0) < 1 || (int)($size[1] ?? 0) < 1)
|
||||
{
|
||||
bratonien_tools_shadow_bridge_fail('Das temporär eingesetzte Original besitzt keine lesbaren Bildabmessungen.');
|
||||
}
|
||||
|
||||
$width = (int)$size[0];
|
||||
$height = (int)$size[1];
|
||||
$rotation_angle = pwg_image::get_rotation_angle($source_path);
|
||||
$rotation_angle = $rotation_angle === null ? 0 : (int)$rotation_angle;
|
||||
$rotation = pwg_image::get_rotation_code_from_angle($rotation_angle);
|
||||
if ($rotation === null) $rotation = 0;
|
||||
$rotation = (int)$rotation;
|
||||
|
||||
if (
|
||||
(int)($row['width'] ?? 0) === $width
|
||||
&& (int)($row['height'] ?? 0) === $height
|
||||
&& isset($row['rotation'])
|
||||
&& (int)$row['rotation'] === $rotation
|
||||
)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
pwg_query(
|
||||
'UPDATE '.IMAGES_TABLE
|
||||
.' SET width='.$width.', height='.$height.', rotation='.$rotation
|
||||
.' WHERE id='.(int)$row['id'].' LIMIT 1'
|
||||
);
|
||||
$row['width'] = $width;
|
||||
$row['height'] = $height;
|
||||
$row['rotation'] = $rotation;
|
||||
return true;
|
||||
}
|
||||
|
||||
function bratonien_tools_shadow_bridge_variants(array $row, $stage)
|
||||
{
|
||||
$src = new SrcImage($row);
|
||||
@@ -151,10 +189,12 @@ if (count($rows) !== 1)
|
||||
bratonien_tools_shadow_bridge_fail('Piwigo kann den Shadow-Pfad nicht eindeutig einem Bild zuordnen: '.$db_path);
|
||||
}
|
||||
$row = $rows[0];
|
||||
$metadata_repaired = bratonien_tools_shadow_bridge_sync_actual_metadata($row, $shadow_real);
|
||||
|
||||
$variants = bratonien_tools_shadow_bridge_variants($row, $stage);
|
||||
$generated = 0;
|
||||
$already = 0;
|
||||
$invalid_removed = 0;
|
||||
foreach ($variants as $variant)
|
||||
{
|
||||
$reason = '';
|
||||
@@ -164,6 +204,18 @@ foreach ($variants as $variant)
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ein vorhandenes, aber geometrisch falsches Derivat darf nicht an i.php
|
||||
// vorbeikommen. Piwigo entscheidet sonst allein anhand von mtimes und kann
|
||||
// eine alte Datei trotz korrigierter Breite/Höhe/EXIF-Rotation behalten.
|
||||
if (is_file($variant['target']))
|
||||
{
|
||||
if (!@unlink($variant['target']))
|
||||
{
|
||||
bratonien_tools_shadow_bridge_fail($variant['name'].': ungültiges Alt-Derivat konnte nicht entfernt werden ('.$reason.').');
|
||||
}
|
||||
$invalid_removed++;
|
||||
}
|
||||
|
||||
$request = bratonien_tools_shadow_bridge_i_request($variant['target']);
|
||||
if ($request === null) bratonien_tools_shadow_bridge_fail($variant['name'].': ungültiger Piwigo-Derivatpfad.');
|
||||
|
||||
@@ -187,8 +239,10 @@ echo json_encode(array(
|
||||
'shadow_relative'=>$shadow_relative,
|
||||
'stage'=>$stage,
|
||||
'piwigo_image_id'=>(int)$row['id'],
|
||||
'metadata_repaired'=>$metadata_repaired,
|
||||
'variants'=>count($variants),
|
||||
'generated'=>$generated,
|
||||
'already_valid'=>$already,
|
||||
'invalid_removed'=>$invalid_removed,
|
||||
), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)."\n";
|
||||
exit(0);
|
||||
|
||||
26
runtime/lib/run-webdav-after-sync.sh
Normal file
26
runtime/lib/run-webdav-after-sync.sh
Normal file
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
STATE_DIR="${1:-}"
|
||||
shift || true
|
||||
|
||||
[[ -n "$STATE_DIR" ]] || { echo "Connector-State-Verzeichnis fehlt." >&2; exit 2; }
|
||||
[[ $# -gt 0 ]] || { echo "Warmup-Kommando fehlt." >&2; exit 2; }
|
||||
|
||||
SYNC_LOCK="$STATE_DIR/webdav-sync.lock"
|
||||
WAITER_LOCK="$STATE_DIR/webdav-cache-warmup-after-sync.lock"
|
||||
|
||||
# Pro Verbindung darf genau ein wartender Cache-Start existieren. Dadurch
|
||||
# erzeugen wiederholte Klicks im Adminbereich keine Warteschlange aus Workern.
|
||||
exec 8>"$WAITER_LOCK"
|
||||
flock -n 8 || exit 0
|
||||
|
||||
# Der Connector hält diesen Lock exklusiv. Wir warten blockierend, ohne dabei
|
||||
# Nextcloud-Dateien oder den Worker-Index anzufassen. Sobald der Connector den
|
||||
# Lock freigibt, geben wir unseren kurzen Leselock sofort wieder frei und
|
||||
# starten den normalen Worker. Dessen eigene Guards bleiben damit maßgeblich.
|
||||
exec 9<>"$SYNC_LOCK"
|
||||
flock -s 9
|
||||
flock -u 9
|
||||
|
||||
exec "$@"
|
||||
@@ -25,19 +25,33 @@ if (!is_array($mapping) || !isset($mapping['files']) || !is_array($mapping['file
|
||||
}
|
||||
|
||||
$dimensions = array();
|
||||
$mapped_files = 0;
|
||||
foreach ($mapping['files'] as $path => $entry)
|
||||
{
|
||||
if (!is_array($entry) || (string)($entry['kind'] ?? '') !== 'file') continue;
|
||||
$mapped_files++;
|
||||
$width = (int)($entry['width'] ?? 0);
|
||||
$height = (int)($entry['height'] ?? 0);
|
||||
if ($width < 1 || $height < 1) continue;
|
||||
$dimensions[str_replace('\\', '/', (string)$path)] = array($width, $height);
|
||||
}
|
||||
|
||||
if ($mapped_files === 0)
|
||||
{
|
||||
echo "WebDAV-Metadaten: keine aktuellen Bildquellen vorhanden.\n";
|
||||
exit(0);
|
||||
}
|
||||
|
||||
// Nextcloud liefert die Bildabmessungen ueber WebDAV nicht fuer jede Datei.
|
||||
// Das ist kein Fehler des Connector-Syncs: die Dateien und ihr Shadow Tree
|
||||
// sind trotzdem gueltig und Piwigo kann die Bildquelle spaeter bei Bedarf
|
||||
// materialisieren. Vorhandene Abmessungen werden uebernommen, fehlende Werte
|
||||
// bleiben unangetastet. Nur ein syntaktisch/technisch ungueltiges Mapping ist
|
||||
// oben weiterhin fatal.
|
||||
if (!$dimensions)
|
||||
{
|
||||
fwrite(STDERR, "WebDAV-Mapping enthaelt keine Originalabmessungen.\n");
|
||||
exit(1);
|
||||
echo 'WebDAV-Metadaten: bilder='.$mapped_files.' aktualisiert=0 ohne_masse='.$mapped_files." (Nextcloud lieferte keine Bildabmessungen; Synchronisierung bleibt gueltig)\n";
|
||||
exit(0);
|
||||
}
|
||||
|
||||
define('PHPWG_ROOT_PATH', $piwigo_root.'/');
|
||||
@@ -51,7 +65,7 @@ $_SERVER['REQUEST_URI'] = '/';
|
||||
$_SERVER['SCRIPT_NAME'] = '/plugins/bratonien_tools/runtime/lib/sync-webdav-metadata.php';
|
||||
$_SERVER['PHP_SELF'] = $_SERVER['SCRIPT_NAME'];
|
||||
$_SERVER['QUERY_STRING'] = '';
|
||||
$_SERVER['HTTP_USER_AGENT'] = 'Bratonien-WebDAV-Metadata/0.9.6.8.20';
|
||||
$_SERVER['HTTP_USER_AGENT'] = 'Bratonien-WebDAV-Metadata/0.9.7.1.47';
|
||||
$_SERVER['HTTPS'] = 'off';
|
||||
|
||||
require_once(PHPWG_ROOT_PATH.'include/common.inc.php');
|
||||
@@ -102,5 +116,10 @@ if ($updated > 0)
|
||||
if (function_exists('invalidate_user_cache')) invalidate_user_cache(true);
|
||||
}
|
||||
|
||||
echo 'WebDAV-Metadaten: bilder='.$checked.' aktualisiert='.$updated.' ohne_masse='.$missing."\n";
|
||||
exit($missing > 0 ? 1 : 0);
|
||||
echo 'WebDAV-Metadaten: bilder='.$checked.' aktualisiert='.$updated.' ohne_masse='.$missing;
|
||||
if ($missing > 0)
|
||||
{
|
||||
echo ' (fehlende WebDAV-Abmessungen sind nicht fatal)';
|
||||
}
|
||||
echo "\n";
|
||||
exit(0);
|
||||
|
||||
@@ -23,7 +23,7 @@ $_SERVER['REQUEST_URI'] = '/';
|
||||
$_SERVER['SCRIPT_NAME'] = '/plugins/bratonien_tools/runtime/lib/webdav-cache-warmup.php';
|
||||
$_SERVER['PHP_SELF'] = $_SERVER['SCRIPT_NAME'];
|
||||
$_SERVER['QUERY_STRING'] = '';
|
||||
$_SERVER['HTTP_USER_AGENT'] = 'Bratonien-WebDAV-Cache-Worker/0.9.7.1.17';
|
||||
$_SERVER['HTTP_USER_AGENT'] = 'Bratonien-WebDAV-Cache-Worker/0.9.7.1.30';
|
||||
$_SERVER['HTTPS'] = 'off';
|
||||
|
||||
require_once(PHPWG_ROOT_PATH.'include/common.inc.php');
|
||||
@@ -54,6 +54,16 @@ function bratonien_tools_cache_warmup_status_file($connection_id)
|
||||
return PHPWG_ROOT_PATH.PWG_LOCAL_DIR.'bratonien-webdav-warmup.status-'.(int)$connection_id.'.json';
|
||||
}
|
||||
|
||||
function bratonien_tools_cache_warmup_cancel_file($connection_id)
|
||||
{
|
||||
return PHPWG_ROOT_PATH.PWG_LOCAL_DIR.'bratonien-webdav-warmup.cancel-'.(int)$connection_id;
|
||||
}
|
||||
|
||||
function bratonien_tools_cache_warmup_cancel_pending($connection_id)
|
||||
{
|
||||
return is_file(bratonien_tools_cache_warmup_cancel_file($connection_id));
|
||||
}
|
||||
|
||||
function bratonien_tools_cache_warmup_priority_file($state_dir)
|
||||
{
|
||||
return rtrim((string)$state_dir, '/').'/webdav-cache-warmup-priority-sync';
|
||||
@@ -247,7 +257,7 @@ function bratonien_tools_cache_warmup_download(array $source, array $credentials
|
||||
CURLOPT_RETURNTRANSFER=>false,
|
||||
CURLOPT_FAILONERROR=>false,
|
||||
CURLOPT_FILE=>$fp,
|
||||
CURLOPT_USERAGENT=>'Bratonien-WebDAV-Cache-Worker/0.9.7.1.17',
|
||||
CURLOPT_USERAGENT=>'Bratonien-WebDAV-Cache-Worker/0.9.7.1.30',
|
||||
));
|
||||
$ok = curl_exec($ch);
|
||||
$errno = curl_errno($ch);
|
||||
@@ -484,26 +494,70 @@ function bratonien_tools_cache_warmup_stage_pending(array $selected, array $inde
|
||||
return $pending;
|
||||
}
|
||||
|
||||
function bratonien_tools_cache_warmup_stage_completed_count(array $selected, array $index, $stage)
|
||||
{
|
||||
return count($selected) - count(bratonien_tools_cache_warmup_stage_pending($selected, $index, $stage));
|
||||
}
|
||||
|
||||
function bratonien_tools_cache_warmup_run_stage($connection_id, $stage, array $selected, array &$index, array $credentials, $batch_size, $priority_file='')
|
||||
{
|
||||
$pending = bratonien_tools_cache_warmup_stage_pending($selected, $index, $stage);
|
||||
if (!$pending) return array('failed'=>0, 'preempted'=>false, 'completed'=>0);
|
||||
if (!$pending) return array('failed'=>0, 'deferred'=>0, 'waiting'=>false, 'preempted'=>false, 'cancelled'=>false, 'completed'=>0);
|
||||
if (bratonien_tools_cache_warmup_cancel_pending($connection_id))
|
||||
{
|
||||
return array('failed'=>0, 'deferred'=>0, 'waiting'=>false, 'preempted'=>false, 'cancelled'=>true, 'completed'=>0);
|
||||
}
|
||||
if ($stage === 2 && bratonien_tools_cache_warmup_priority_pending($priority_file))
|
||||
{
|
||||
return array('failed'=>0, 'preempted'=>true, 'completed'=>0);
|
||||
return array('failed'=>0, 'deferred'=>0, 'waiting'=>false, 'preempted'=>true, 'cancelled'=>false, 'completed'=>0);
|
||||
}
|
||||
|
||||
$temp_root = PHPWG_ROOT_PATH.'upload/bratonien-webdav-warmup';
|
||||
if (!is_dir($temp_root) && !@mkdir($temp_root, 0775, true) && !is_dir($temp_root)) throw new RuntimeException('Worker-Temp-Verzeichnis konnte nicht angelegt werden.');
|
||||
$failed = 0;
|
||||
$deferred = 0;
|
||||
$completed = 0;
|
||||
$batch_number = 0;
|
||||
|
||||
foreach (array_chunk(array_keys($pending), max(1, (int)$batch_size)) as $keys)
|
||||
{
|
||||
// Harte Batch-Grenze: Sobald ein Abbruchsignal existiert, darf dieser
|
||||
// Batch gar nicht mehr begonnen und damit auch kein weiteres Original
|
||||
// aus Nextcloud geladen werden.
|
||||
if (bratonien_tools_cache_warmup_cancel_pending($connection_id))
|
||||
{
|
||||
return array('failed'=>$failed, 'deferred'=>$deferred, 'waiting'=>false, 'preempted'=>false, 'cancelled'=>true, 'completed'=>$completed);
|
||||
}
|
||||
|
||||
if ($stage === 2 && bratonien_tools_cache_warmup_priority_pending($priority_file))
|
||||
{
|
||||
return array('failed'=>$failed, 'deferred'=>$deferred, 'waiting'=>false, 'preempted'=>true, 'cancelled'=>false, 'completed'=>$completed);
|
||||
}
|
||||
|
||||
// Der Connector-Lock wird VOR jedem Download geprueft und fuer den
|
||||
// kompletten Batch gemeinsam gehalten. Ist der Connector aktiv, werden
|
||||
// exakt null Bilder dieses Batches geladen.
|
||||
$batch_sync_lock = null;
|
||||
$batch_sync_detail = '';
|
||||
if (!bratonien_tools_cache_warmup_sync_guard($credentials, $batch_sync_lock, $batch_sync_detail))
|
||||
{
|
||||
bratonien_tools_cache_warmup_status($connection_id, 'waiting', 'Connector-Synchronisierung läuft. Der nächste Batch startet erst danach; es wurden für diesen Batch keine Nextcloud-Originale geladen.', array(
|
||||
'stage'=>$stage,
|
||||
'batch'=>$batch_number + 1,
|
||||
'selected_total'=>count($selected),
|
||||
'stage_completed'=>$completed,
|
||||
'deferred_by_connector'=>true,
|
||||
));
|
||||
return array('failed'=>$failed, 'deferred'=>$deferred, 'waiting'=>true, 'preempted'=>false, 'cancelled'=>false, 'completed'=>$completed);
|
||||
}
|
||||
|
||||
$batch_number++;
|
||||
$dir = $temp_root.'/connection-'.(int)$connection_id.'-stage'.$stage.'-'.date('YmdHis').'-'.bin2hex(random_bytes(4));
|
||||
if (!@mkdir($dir, 0775, true) && !is_dir($dir)) throw new RuntimeException('Worker-Batch-Verzeichnis konnte nicht angelegt werden.');
|
||||
if (!@mkdir($dir, 0775, true) && !is_dir($dir))
|
||||
{
|
||||
bratonien_tools_cache_warmup_unlock($batch_sync_lock);
|
||||
throw new RuntimeException('Worker-Batch-Verzeichnis konnte nicht angelegt werden.');
|
||||
}
|
||||
|
||||
$downloads = array();
|
||||
foreach ($keys as $key)
|
||||
@@ -518,28 +572,42 @@ function bratonien_tools_cache_warmup_run_stage($connection_id, $stage, array $s
|
||||
}
|
||||
}
|
||||
|
||||
bratonien_tools_cache_warmup_status($connection_id, 'running', 'Piwigo verarbeitet WebDAV-Quellen aus dem Worker-Index.', array(
|
||||
'stage'=>$stage,
|
||||
'batch'=>$batch_number,
|
||||
'batch_requested'=>count($keys),
|
||||
'batch_downloaded'=>count($downloads),
|
||||
'selected_total'=>count($selected),
|
||||
'stage_completed'=>$completed,
|
||||
));
|
||||
$cancel_requested = bratonien_tools_cache_warmup_cancel_pending($connection_id);
|
||||
bratonien_tools_cache_warmup_status(
|
||||
$connection_id,
|
||||
$cancel_requested ? 'cancelling' : 'running',
|
||||
$cancel_requested
|
||||
? 'Abbruch angefordert. Der bereits geladene aktuelle Batch wird vollständig beendet; danach ist Schluss.'
|
||||
: 'Piwigo verarbeitet WebDAV-Quellen aus dem Worker-Index.',
|
||||
array(
|
||||
'stage'=>$stage,
|
||||
'batch'=>$batch_number,
|
||||
'batch_requested'=>count($keys),
|
||||
'batch_downloaded'=>count($downloads),
|
||||
'selected_total'=>count($selected),
|
||||
'stage_completed'=>$completed,
|
||||
'cancel_requested'=>$cancel_requested,
|
||||
)
|
||||
);
|
||||
|
||||
foreach ($downloads as $key=>$file)
|
||||
{
|
||||
$result = bratonien_tools_cache_warmup_process($pending[$key], $file, $credentials, $stage);
|
||||
bratonien_tools_cache_warmup_log('source', array('stage'=>$stage, 'source'=>$key, 'ok'=>!empty($result['ok']), 'message'=>(string)($result['message'] ?? '')));
|
||||
bratonien_tools_cache_warmup_log('source', array('stage'=>$stage, 'source'=>$key, 'ok'=>!empty($result['ok']), 'defer'=>!empty($result['defer']), 'message'=>(string)($result['message'] ?? '')));
|
||||
if (!empty($result['fatal']))
|
||||
{
|
||||
foreach ($downloads as $cleanup) @unlink($cleanup);
|
||||
@rmdir($dir);
|
||||
bratonien_tools_cache_warmup_unlock($batch_sync_lock);
|
||||
throw new RuntimeException('Sicherheitsabbruch bei '.$key.': '.(string)$result['message']);
|
||||
}
|
||||
if (empty($result['ok']))
|
||||
{
|
||||
$failed++;
|
||||
// Temporäre Sperren oder parallel belegte Quellen sind kein Fehler.
|
||||
// Der Index bleibt offen und der nächste Lauf versucht nur diese
|
||||
// Quelle erneut.
|
||||
if (!empty($result['defer'])) $deferred++;
|
||||
else $failed++;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -554,14 +622,20 @@ function bratonien_tools_cache_warmup_run_stage($connection_id, $stage, array $s
|
||||
|
||||
foreach ($downloads as $file) @unlink($file);
|
||||
@rmdir($dir);
|
||||
bratonien_tools_cache_warmup_unlock($batch_sync_lock);
|
||||
|
||||
if (bratonien_tools_cache_warmup_cancel_pending($connection_id))
|
||||
{
|
||||
return array('failed'=>$failed, 'deferred'=>$deferred, 'waiting'=>false, 'preempted'=>false, 'cancelled'=>true, 'completed'=>$completed);
|
||||
}
|
||||
|
||||
if ($stage === 2 && bratonien_tools_cache_warmup_priority_pending($priority_file))
|
||||
{
|
||||
return array('failed'=>$failed, 'preempted'=>true, 'completed'=>$completed);
|
||||
return array('failed'=>$failed, 'deferred'=>$deferred, 'waiting'=>false, 'preempted'=>true, 'cancelled'=>false, 'completed'=>$completed);
|
||||
}
|
||||
}
|
||||
|
||||
return array('failed'=>$failed, 'preempted'=>false, 'completed'=>$completed);
|
||||
return array('failed'=>$failed, 'deferred'=>$deferred, 'waiting'=>false, 'preempted'=>false, 'cancelled'=>false, 'completed'=>$completed);
|
||||
}
|
||||
|
||||
function bratonien_tools_cache_warmup_run($connection_id, $mode)
|
||||
@@ -618,6 +692,7 @@ function bratonien_tools_cache_warmup_run($connection_id, $mode)
|
||||
{
|
||||
if ($mode === 'periodic' || $mode === 'manual') $index['last_periodic_at'] = time();
|
||||
bratonien_tools_webdav_source_index_save($state_dir, $connection_id, $index);
|
||||
@unlink(bratonien_tools_cache_warmup_cancel_file($connection_id));
|
||||
bratonien_tools_cache_warmup_status($connection_id, 'complete', 'Keine neuen oder geänderten Shadow-Quellen gefunden.', array('mode'=>$mode, 'shadow_sources'=>count($scan['sources'])));
|
||||
return 0;
|
||||
}
|
||||
@@ -632,6 +707,29 @@ function bratonien_tools_cache_warmup_run($connection_id, $mode)
|
||||
}
|
||||
|
||||
$stage1 = bratonien_tools_cache_warmup_run_stage($connection_id, 1, $selected, $index, $credentials, $settings['batch_size'], $priority_file);
|
||||
if (!empty($stage1['cancelled']))
|
||||
{
|
||||
bratonien_tools_webdav_source_index_save($state_dir, $connection_id, $index);
|
||||
@unlink(bratonien_tools_cache_warmup_cancel_file($connection_id));
|
||||
bratonien_tools_cache_warmup_status($connection_id, 'cancelled', 'Cache-Aufbau nach dem vollständig abgeschlossenen aktuellen Batch beendet.', array(
|
||||
'mode'=>$mode,
|
||||
'shadow_sources'=>count($scan['sources']),
|
||||
'selected'=>count($selected),
|
||||
'stage1_completed'=>bratonien_tools_cache_warmup_stage_completed_count($selected, $index, 1),
|
||||
'stage2_completed'=>bratonien_tools_cache_warmup_stage_completed_count($selected, $index, 2),
|
||||
'stage1_failed'=>(int)$stage1['failed'],
|
||||
'stage1_deferred'=>(int)$stage1['deferred'],
|
||||
'stage2_failed'=>0,
|
||||
'stage2_deferred'=>0,
|
||||
));
|
||||
return 0;
|
||||
}
|
||||
if (!empty($stage1['waiting']))
|
||||
{
|
||||
bratonien_tools_webdav_source_index_save($state_dir, $connection_id, $index);
|
||||
return 0;
|
||||
}
|
||||
|
||||
$stage2_candidates = array();
|
||||
foreach ($selected as $key=>$item)
|
||||
{
|
||||
@@ -640,30 +738,79 @@ function bratonien_tools_cache_warmup_run($connection_id, $mode)
|
||||
}
|
||||
$stage2 = bratonien_tools_cache_warmup_run_stage($connection_id, 2, $stage2_candidates, $index, $credentials, $settings['batch_size'], $priority_file);
|
||||
|
||||
if (!empty($stage2['cancelled']))
|
||||
{
|
||||
bratonien_tools_webdav_source_index_save($state_dir, $connection_id, $index);
|
||||
@unlink(bratonien_tools_cache_warmup_cancel_file($connection_id));
|
||||
bratonien_tools_cache_warmup_status($connection_id, 'cancelled', 'Cache-Aufbau nach dem vollständig abgeschlossenen aktuellen Batch beendet.', array(
|
||||
'mode'=>$mode,
|
||||
'shadow_sources'=>count($scan['sources']),
|
||||
'selected'=>count($selected),
|
||||
'stage1_completed'=>bratonien_tools_cache_warmup_stage_completed_count($selected, $index, 1),
|
||||
'stage2_completed'=>bratonien_tools_cache_warmup_stage_completed_count($selected, $index, 2),
|
||||
'stage1_failed'=>(int)$stage1['failed'],
|
||||
'stage1_deferred'=>(int)$stage1['deferred'],
|
||||
'stage2_failed'=>(int)$stage2['failed'],
|
||||
'stage2_deferred'=>(int)$stage2['deferred'],
|
||||
));
|
||||
return 0;
|
||||
}
|
||||
if (!empty($stage2['waiting']))
|
||||
{
|
||||
bratonien_tools_webdav_source_index_save($state_dir, $connection_id, $index);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!empty($stage2['preempted']))
|
||||
{
|
||||
bratonien_tools_webdav_source_index_save($state_dir, $connection_id, $index);
|
||||
bratonien_tools_cache_warmup_status($connection_id, 'preempted', 'Stufe 2 wurde nach einem vollständigen Batch für neue Connector-Inhalte freigegeben.', array(
|
||||
'mode'=>$mode,
|
||||
'selected'=>count($selected),
|
||||
'stage1_completed'=>bratonien_tools_cache_warmup_stage_completed_count($selected, $index, 1),
|
||||
'stage2_completed'=>bratonien_tools_cache_warmup_stage_completed_count($selected, $index, 2),
|
||||
'stage1_failed'=>(int)$stage1['failed'],
|
||||
'stage1_deferred'=>(int)$stage1['deferred'],
|
||||
'stage2_failed'=>(int)$stage2['failed'],
|
||||
'stage2_deferred'=>(int)$stage2['deferred'],
|
||||
));
|
||||
return ((int)$stage1['failed'] + (int)$stage2['failed']) > 0 ? 2 : 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ($mode === 'periodic' || $mode === 'manual') $index['last_periodic_at'] = time();
|
||||
bratonien_tools_webdav_source_index_save($state_dir, $connection_id, $index);
|
||||
@unlink(bratonien_tools_cache_warmup_cancel_file($connection_id));
|
||||
|
||||
$failed = (int)$stage1['failed'] + (int)$stage2['failed'];
|
||||
bratonien_tools_cache_warmup_status($connection_id, $failed ? 'error' : 'complete', $failed ? 'Worker-Lauf mit einzelnen Fehlern beendet.' : ($mode === 'rebuild' ? 'Piwigo hat den vollständigen Shadow-Bestand für den Cache-Rebuild verarbeitet.' : 'Worker-Lauf vollständig beendet.'), array(
|
||||
$deferred = (int)$stage1['deferred'] + (int)$stage2['deferred'];
|
||||
if ($failed > 0 || $deferred > 0)
|
||||
{
|
||||
bratonien_tools_cache_warmup_status($connection_id, 'partial', 'Worker-Lauf beendet; einzelne Quellen bleiben offen und werden beim nächsten Lauf erneut versucht. Der übrige Bestand wurde weiterverarbeitet.', array(
|
||||
'mode'=>$mode,
|
||||
'shadow_sources'=>count($scan['sources']),
|
||||
'selected'=>count($selected),
|
||||
'stage1_completed'=>bratonien_tools_cache_warmup_stage_completed_count($selected, $index, 1),
|
||||
'stage2_completed'=>bratonien_tools_cache_warmup_stage_completed_count($selected, $index, 2),
|
||||
'stage1_failed'=>(int)$stage1['failed'],
|
||||
'stage1_deferred'=>(int)$stage1['deferred'],
|
||||
'stage2_failed'=>(int)$stage2['failed'],
|
||||
'stage2_deferred'=>(int)$stage2['deferred'],
|
||||
));
|
||||
return 0;
|
||||
}
|
||||
|
||||
bratonien_tools_cache_warmup_status($connection_id, 'complete', $mode === 'rebuild' ? 'Piwigo hat den vollständigen Shadow-Bestand für den Cache-Rebuild verarbeitet.' : 'Worker-Lauf vollständig beendet.', array(
|
||||
'mode'=>$mode,
|
||||
'shadow_sources'=>count($scan['sources']),
|
||||
'selected'=>count($selected),
|
||||
'stage1_completed'=>(int)$stage1['completed'],
|
||||
'stage2_completed'=>(int)$stage2['completed'],
|
||||
'stage1_failed'=>(int)$stage1['failed'],
|
||||
'stage2_failed'=>(int)$stage2['failed'],
|
||||
'stage1_completed'=>bratonien_tools_cache_warmup_stage_completed_count($selected, $index, 1),
|
||||
'stage2_completed'=>bratonien_tools_cache_warmup_stage_completed_count($selected, $index, 2),
|
||||
'stage1_failed'=>0,
|
||||
'stage1_deferred'=>0,
|
||||
'stage2_failed'=>0,
|
||||
'stage2_deferred'=>0,
|
||||
));
|
||||
return $failed ? 2 : 0;
|
||||
return 0;
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
324
runtime/piwigo-presentation-refresh.php
Normal file
324
runtime/piwigo-presentation-refresh.php
Normal file
@@ -0,0 +1,324 @@
|
||||
<?php
|
||||
if (PHP_SAPI !== 'cli')
|
||||
{
|
||||
http_response_code(404);
|
||||
exit;
|
||||
}
|
||||
|
||||
$piwigo_root = realpath(dirname(__DIR__, 3));
|
||||
if ($piwigo_root === false)
|
||||
{
|
||||
fwrite(STDERR, "Piwigo-Root wurde nicht gefunden.\n");
|
||||
exit(1);
|
||||
}
|
||||
define('PHPWG_ROOT_PATH', rtrim($piwigo_root, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR);
|
||||
|
||||
$_SERVER['REMOTE_ADDR'] = '127.0.0.1';
|
||||
$_SERVER['SERVER_ADDR'] = '127.0.0.1';
|
||||
$_SERVER['SERVER_NAME'] = 'localhost';
|
||||
$_SERVER['HTTP_HOST'] = 'localhost';
|
||||
$_SERVER['SERVER_PORT'] = '80';
|
||||
$_SERVER['REQUEST_METHOD'] = 'GET';
|
||||
$_SERVER['REQUEST_URI'] = '/';
|
||||
$_SERVER['SCRIPT_NAME'] = '/plugins/bratonien_tools/runtime/piwigo-presentation-refresh.php';
|
||||
$_SERVER['PHP_SELF'] = $_SERVER['SCRIPT_NAME'];
|
||||
$_SERVER['QUERY_STRING'] = '';
|
||||
$_SERVER['HTTP_USER_AGENT'] = 'Bratonien-Presentation-Refresh/0.9.7.1.41';
|
||||
$_SERVER['HTTPS'] = 'off';
|
||||
|
||||
require_once(PHPWG_ROOT_PATH.'include/common.inc.php');
|
||||
require_once(PHPWG_ROOT_PATH.'include/derivative.inc.php');
|
||||
if (!defined('BRATONIEN_TOOLS_PATH'))
|
||||
{
|
||||
fwrite(STDERR, "Bratonien Tools ist nicht aktiv.\n");
|
||||
exit(1);
|
||||
}
|
||||
require_once(BRATONIEN_TOOLS_PATH.'include/presentation_refresh.inc.php');
|
||||
require_once(BRATONIEN_TOOLS_PATH.'include/watermark_runtime.inc.php');
|
||||
require_once(BRATONIEN_TOOLS_PATH.'include/webdav_materialize_runtime.inc.php');
|
||||
|
||||
function bratonien_tools_presentation_worker_log($event, array $data=array())
|
||||
{
|
||||
$parts = array('[BRAT-PRESENTATION]', $event);
|
||||
foreach ($data as $key=>$value)
|
||||
{
|
||||
if (is_array($value) || is_object($value)) $value = json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
$parts[] = $key.'='.str_replace(array("\r", "\n"), array('\\r', '\\n'), (string)$value);
|
||||
}
|
||||
fwrite(STDOUT, implode(' ', $parts)."\n");
|
||||
}
|
||||
|
||||
function bratonien_tools_presentation_worker_categories(array $requested, $all)
|
||||
{
|
||||
$requested = array_fill_keys(array_map('intval', $requested), true);
|
||||
$categories = array();
|
||||
$result = pwg_query('SELECT id, uppercats FROM '.CATEGORIES_TABLE.' ORDER BY id');
|
||||
while ($row = pwg_db_fetch_assoc($result))
|
||||
{
|
||||
$id = (int)$row['id'];
|
||||
if ($id < 1) continue;
|
||||
if ($all)
|
||||
{
|
||||
$categories[$id] = $id;
|
||||
continue;
|
||||
}
|
||||
|
||||
$uppercats = array_filter(array_map('intval', explode(',', (string)$row['uppercats'])));
|
||||
foreach ($uppercats as $ancestor)
|
||||
{
|
||||
if (isset($requested[$ancestor]))
|
||||
{
|
||||
$categories[$id] = $id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
ksort($categories, SORT_NUMERIC);
|
||||
return array_values($categories);
|
||||
}
|
||||
|
||||
function bratonien_tools_presentation_worker_variants(SrcImage $src)
|
||||
{
|
||||
$variants = array();
|
||||
foreach (ImageStdParams::get_defined_type_map() as $type=>$params)
|
||||
{
|
||||
$variants['standard:'.$type] = new DerivativeImage($params, $src);
|
||||
}
|
||||
foreach (ImageStdParams::$custom as $key=>$last_used)
|
||||
{
|
||||
$params = bratonien_tools_webdav_materialize_custom_params($key);
|
||||
if ($params) $variants['custom:'.$key] = new DerivativeImage($params, $src);
|
||||
}
|
||||
return $variants;
|
||||
}
|
||||
|
||||
function bratonien_tools_presentation_worker_call_watermark($url, &$detail=null)
|
||||
{
|
||||
$detail = '';
|
||||
$url = html_entity_decode((string)$url, ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||
$path = (string)parse_url($url, PHP_URL_PATH);
|
||||
$query = (string)parse_url($url, PHP_URL_QUERY);
|
||||
$endpoint = '/plugins/'.trim(BRATONIEN_TOOLS_ID, '/').'/watermark.php';
|
||||
|
||||
if ($path === '' || substr($path, -strlen($endpoint)) !== $endpoint)
|
||||
{
|
||||
if (strpos($path, '/bratonien-watermark/') !== false)
|
||||
{
|
||||
$detail = 'Bereits vorhanden.';
|
||||
return true;
|
||||
}
|
||||
$detail = 'Kein Bratonien-Wasserzeichenaufruf erforderlich.';
|
||||
return true;
|
||||
}
|
||||
if ($query === '')
|
||||
{
|
||||
$detail = 'Wasserzeichen-URL enthält keine Parameter.';
|
||||
return false;
|
||||
}
|
||||
if (!function_exists('exec'))
|
||||
{
|
||||
$detail = 'PHP exec() ist deaktiviert.';
|
||||
return false;
|
||||
}
|
||||
|
||||
$runner = BRATONIEN_TOOLS_PATH.'runtime/lib/bratonien-watermark-call.php';
|
||||
if (!is_file($runner))
|
||||
{
|
||||
$detail = 'Lokaler Wasserzeichen-Aufrufer fehlt.';
|
||||
return false;
|
||||
}
|
||||
|
||||
$command = escapeshellarg(PHP_BINARY).' '.escapeshellarg($runner)
|
||||
.' --query='.escapeshellarg($query).' > /dev/null 2>&1';
|
||||
$output = array();
|
||||
$exit = 1;
|
||||
@exec($command, $output, $exit);
|
||||
if ($exit !== 0)
|
||||
{
|
||||
$detail = 'Wasserzeichen-Aufrufer Exit '.$exit.'.';
|
||||
return false;
|
||||
}
|
||||
|
||||
$detail = 'Wasserzeichen-Vorschau bereit.';
|
||||
return true;
|
||||
}
|
||||
|
||||
function bratonien_tools_presentation_worker_process(array $category_ids)
|
||||
{
|
||||
global $page;
|
||||
|
||||
if (!$category_ids) return array('images'=>0, 'profiles'=>0, 'variants'=>0, 'generated'=>0, 'missing_base'=>0, 'errors'=>0);
|
||||
$category_lookup = array_fill_keys($category_ids, true);
|
||||
$ids_sql = implode(',', array_map('intval', $category_ids));
|
||||
|
||||
$image_categories = array();
|
||||
$result = pwg_query('SELECT image_id, category_id FROM '.IMAGE_CATEGORY_TABLE.' WHERE category_id IN ('.$ids_sql.') ORDER BY image_id, category_id');
|
||||
while ($row = pwg_db_fetch_assoc($result))
|
||||
{
|
||||
$image_id = (int)$row['image_id'];
|
||||
$category_id = (int)$row['category_id'];
|
||||
if ($image_id > 0 && isset($category_lookup[$category_id])) $image_categories[$image_id][] = $category_id;
|
||||
}
|
||||
if (!$image_categories) return array('images'=>0, 'profiles'=>0, 'variants'=>0, 'generated'=>0, 'missing_base'=>0, 'errors'=>0);
|
||||
|
||||
$stats = array('images'=>0, 'profiles'=>0, 'variants'=>0, 'generated'=>0, 'missing_base'=>0, 'errors'=>0);
|
||||
$image_ids = array_keys($image_categories);
|
||||
foreach (array_chunk($image_ids, 500) as $chunk)
|
||||
{
|
||||
$rows = pwg_query('SELECT * FROM '.IMAGES_TABLE.' WHERE id IN ('.implode(',', array_map('intval', $chunk)).') ORDER BY id');
|
||||
while ($row = pwg_db_fetch_assoc($rows))
|
||||
{
|
||||
$image_id = (int)$row['id'];
|
||||
$stats['images']++;
|
||||
$profile_contexts = array();
|
||||
|
||||
foreach ($image_categories[$image_id] ?? array() as $category_id)
|
||||
{
|
||||
$rule = bratonien_tools_runtime_effective_rule($category_id);
|
||||
if ((string)($rule['mode'] ?? '') !== 'profile' || empty($rule['profile_id'])) continue;
|
||||
$profile_id = (int)$rule['profile_id'];
|
||||
if ($profile_id < 1 || isset($profile_contexts[$profile_id])) continue;
|
||||
$profile = bratonien_tools_runtime_get_profile($profile_id);
|
||||
if (!$profile || empty($profile['active']) || !bratonien_tools_profile_watermark_path($profile)) continue;
|
||||
$profile_contexts[$profile_id] = $category_id;
|
||||
}
|
||||
|
||||
if (!$profile_contexts) continue;
|
||||
|
||||
try
|
||||
{
|
||||
$src = new SrcImage($row);
|
||||
$variants = bratonien_tools_presentation_worker_variants($src);
|
||||
}
|
||||
catch (Throwable $e)
|
||||
{
|
||||
$stats['errors']++;
|
||||
bratonien_tools_presentation_worker_log('image_error', array('image_id'=>$image_id, 'message'=>$e->getMessage()));
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($profile_contexts as $profile_id=>$category_id)
|
||||
{
|
||||
$stats['profiles']++;
|
||||
$page['category'] = array('id'=>(int)$category_id);
|
||||
|
||||
foreach ($variants as $variant_name=>$derivative)
|
||||
{
|
||||
try
|
||||
{
|
||||
if ($derivative->same_as_source()) continue;
|
||||
$stats['variants']++;
|
||||
$target = $derivative->get_path();
|
||||
if ($target === '' || !is_file($target) || !is_readable($target))
|
||||
{
|
||||
$stats['missing_base']++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$detail = '';
|
||||
if (!bratonien_tools_presentation_worker_call_watermark($derivative->get_url(), $detail))
|
||||
{
|
||||
$stats['errors']++;
|
||||
bratonien_tools_presentation_worker_log('variant_error', array(
|
||||
'image_id'=>$image_id,
|
||||
'category_id'=>$category_id,
|
||||
'profile_id'=>$profile_id,
|
||||
'variant'=>$variant_name,
|
||||
'message'=>$detail,
|
||||
));
|
||||
continue;
|
||||
}
|
||||
$stats['generated']++;
|
||||
}
|
||||
catch (Throwable $e)
|
||||
{
|
||||
$stats['errors']++;
|
||||
bratonien_tools_presentation_worker_log('variant_error', array(
|
||||
'image_id'=>$image_id,
|
||||
'category_id'=>$category_id,
|
||||
'profile_id'=>$profile_id,
|
||||
'variant'=>$variant_name,
|
||||
'message'=>$e->getMessage(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unset($page['category']);
|
||||
return $stats;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$queue_dir = bratonien_tools_presentation_refresh_queue_dir();
|
||||
if (!is_dir($queue_dir)) exit(0);
|
||||
|
||||
$lock_path = $queue_dir.'/.worker.lock';
|
||||
$lock = @fopen($lock_path, 'c');
|
||||
if (!$lock) throw new RuntimeException('Presentation-Worker-Lock konnte nicht geöffnet werden.');
|
||||
if (!@flock($lock, LOCK_EX)) throw new RuntimeException('Presentation-Worker-Lock konnte nicht gesetzt werden.');
|
||||
|
||||
try
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
$request_files = glob($queue_dir.'/*.json') ?: array();
|
||||
sort($request_files, SORT_STRING);
|
||||
if (!$request_files) break;
|
||||
|
||||
$all = false;
|
||||
$requested = array();
|
||||
$valid_files = array();
|
||||
$reasons = array();
|
||||
foreach ($request_files as $file)
|
||||
{
|
||||
$data = json_decode((string)@file_get_contents($file), true);
|
||||
if (!is_array($data))
|
||||
{
|
||||
bratonien_tools_presentation_worker_log('invalid_request', array('file'=>basename($file)));
|
||||
@unlink($file);
|
||||
continue;
|
||||
}
|
||||
$valid_files[] = $file;
|
||||
if (!empty($data['all'])) $all = true;
|
||||
foreach ((array)($data['categories'] ?? array()) as $category_id)
|
||||
{
|
||||
$category_id = (int)$category_id;
|
||||
if ($category_id > 0) $requested[$category_id] = $category_id;
|
||||
}
|
||||
if (!empty($data['reason'])) $reasons[] = (string)$data['reason'];
|
||||
}
|
||||
|
||||
if (!$valid_files) continue;
|
||||
$categories = bratonien_tools_presentation_worker_categories(array_values($requested), $all);
|
||||
$stats = bratonien_tools_presentation_worker_process($categories);
|
||||
bratonien_tools_presentation_worker_log('complete', array(
|
||||
'all'=>$all,
|
||||
'requested'=>count($requested),
|
||||
'categories'=>count($categories),
|
||||
'reasons'=>array_values(array_unique($reasons)),
|
||||
'images'=>$stats['images'],
|
||||
'profiles'=>$stats['profiles'],
|
||||
'variants'=>$stats['variants'],
|
||||
'prepared'=>$stats['generated'],
|
||||
'missing_base'=>$stats['missing_base'],
|
||||
'errors'=>$stats['errors'],
|
||||
));
|
||||
|
||||
foreach ($valid_files as $file) @unlink($file);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
@flock($lock, LOCK_UN);
|
||||
fclose($lock);
|
||||
}
|
||||
exit(0);
|
||||
}
|
||||
catch (Throwable $e)
|
||||
{
|
||||
fwrite(STDERR, '[BRAT-PRESENTATION] fatal '.$e->getMessage()."\n");
|
||||
exit(1);
|
||||
}
|
||||
@@ -25,22 +25,8 @@ $_SERVER['HTTPS'] = 'off';
|
||||
require_once(PHPWG_ROOT_PATH.'include/common.inc.php');
|
||||
require_once(PHPWG_ROOT_PATH.'admin/include/functions.php');
|
||||
|
||||
$stateRoot = '/var/lib/bratonien-tools/nc-connector';
|
||||
$marker = $stateRoot.'/.webdav-orphan-repair-0963.done';
|
||||
|
||||
try
|
||||
{
|
||||
if (is_file($marker))
|
||||
{
|
||||
echo "NC WebDAV Altlasten-Reparatur: bereits abgeschlossen.\n";
|
||||
exit(0);
|
||||
}
|
||||
|
||||
if (!is_dir($stateRoot) && !mkdir($stateRoot, 0750, true))
|
||||
{
|
||||
throw new RuntimeException('State-Verzeichnis konnte nicht angelegt werden: '.$stateRoot);
|
||||
}
|
||||
|
||||
$relativePrefix = './_data/bratonien-tools/nc-webdav-gallery/connection-';
|
||||
$absolutePrefix = rtrim(PHPWG_ROOT_PATH, '/').'/_data/bratonien-tools/nc-webdav-gallery/connection-';
|
||||
$relativeLength = strlen($relativePrefix);
|
||||
@@ -51,6 +37,7 @@ try
|
||||
" OR LEFT(path,".$absoluteLength.")='".pwg_db_real_escape_string($absolutePrefix)."'";
|
||||
$result = pwg_query($query);
|
||||
|
||||
$checked = 0;
|
||||
$staleIds = array();
|
||||
while ($row = pwg_db_fetch_assoc($result))
|
||||
{
|
||||
@@ -71,6 +58,7 @@ try
|
||||
continue;
|
||||
}
|
||||
|
||||
$checked++;
|
||||
if (!is_file($filesystemPath))
|
||||
{
|
||||
$staleIds[] = $id;
|
||||
@@ -80,22 +68,19 @@ try
|
||||
$staleIds = array_values(array_unique(array_map('intval', $staleIds)));
|
||||
if ($staleIds)
|
||||
{
|
||||
// Die Shadow-Quelle existiert nicht mehr. Der Bilddatensatz ist damit
|
||||
// unabhängig von eventuellen virtuellen Album-Verknüpfungen ungültig.
|
||||
// delete_elements(..., false) entfernt Piwigos Datensatz und Derivate,
|
||||
// fasst aber keine bereits verschwundene Quelldatei an.
|
||||
delete_elements($staleIds, false);
|
||||
invalidate_user_cache(true);
|
||||
}
|
||||
|
||||
$payload = date('c').' removed='.count($staleIds)."\n";
|
||||
if (file_put_contents($marker, $payload, LOCK_EX) === false)
|
||||
{
|
||||
throw new RuntimeException('Abschlussmarker konnte nicht geschrieben werden: '.$marker);
|
||||
}
|
||||
@chmod($marker, 0640);
|
||||
|
||||
echo 'NC WebDAV Altlasten-Reparatur: entfernte verwaiste Bilder='.count($staleIds)."\n";
|
||||
echo 'NC WebDAV laufende Bereinigung: geprueft='.$checked.' entfernte_verwaiste_bilder='.count($staleIds)."\n";
|
||||
exit(0);
|
||||
}
|
||||
catch (Throwable $e)
|
||||
{
|
||||
fwrite(STDERR, 'NC WebDAV Altlasten-Reparatur: '.$e->getMessage()."\n");
|
||||
fwrite(STDERR, 'NC WebDAV laufende Bereinigung: '.$e->getMessage()."\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
@@ -101,14 +101,34 @@ if [[ -r "$WEBDAV_MAPPING_FILE" ]]; then
|
||||
OLD_MAPPING_HASH="$(sha256sum -- "$WEBDAV_MAPPING_FILE" | awk '{print $1}')"
|
||||
fi
|
||||
|
||||
python3 "$SCRIPT_DIR/lib/build_webdav_placeholder_source.py" \
|
||||
# Der Scanner liefert die eigentliche Fehlerursache auf stderr. Diese Ausgabe
|
||||
# darf nicht vom allgemeinen ERR-Trap verschluckt werden, weil im Admin sonst
|
||||
# nur die aufgerufene Python-Befehlszeile statt der Ursache sichtbar ist.
|
||||
PLACEHOLDER_OUTPUT=""
|
||||
PLACEHOLDER_EXIT=0
|
||||
trap - ERR
|
||||
if PLACEHOLDER_OUTPUT="$(python3 "$SCRIPT_DIR/lib/build_webdav_placeholder_source.py" \
|
||||
--base-url "$WEBDAV_BASE_URL" \
|
||||
--user "$WEBDAV_USER" \
|
||||
--password-file "$WEBDAV_PASSWORD_FILE" \
|
||||
"${ROOT_ARGS[@]}" \
|
||||
--source-dir "$WEBDAV_SOURCE_DIR" \
|
||||
--manifest "$MANIFEST" \
|
||||
--mapping "$WEBDAV_MAPPING_FILE"
|
||||
--mapping "$WEBDAV_MAPPING_FILE" 2>&1)"; then
|
||||
PLACEHOLDER_EXIT=0
|
||||
else
|
||||
PLACEHOLDER_EXIT=$?
|
||||
fi
|
||||
trap 'failure $? "$BASH_COMMAND" "$LINENO"' ERR
|
||||
[[ -z "$PLACEHOLDER_OUTPUT" ]] || printf '%s\n' "$PLACEHOLDER_OUTPUT"
|
||||
if [[ "$PLACEHOLDER_EXIT" -ne 0 ]]; then
|
||||
DETAIL="Exit-Code: $PLACEHOLDER_EXIT"
|
||||
if [[ -n "$PLACEHOLDER_OUTPUT" ]]; then
|
||||
DETAIL+="; Ausgabe: $(printf '%s\n' "$PLACEHOLDER_OUTPUT" | compact_output)"
|
||||
fi
|
||||
write_status error "Nextcloud-WebDAV konnte nicht eingelesen werden" "$DETAIL"
|
||||
exit "$PLACEHOLDER_EXIT"
|
||||
fi
|
||||
|
||||
find "$WEBDAV_SOURCE_DIR" -type d -exec chgrp www-data -- {} +
|
||||
find "$WEBDAV_SOURCE_DIR" -type d -exec chmod 0775 -- {} +
|
||||
@@ -180,6 +200,23 @@ if [[ "${PIWIGO_SYNC_ENABLED:-0}" == "1" ]]; then
|
||||
exit "$PIWIGO_EXIT"
|
||||
fi
|
||||
|
||||
CLEANUP_OUTPUT=""
|
||||
CLEANUP_EXIT=0
|
||||
if CLEANUP_OUTPUT="$(php "$SCRIPT_DIR/repair-webdav-orphans.php" 2>&1)"; then
|
||||
CLEANUP_EXIT=0
|
||||
else
|
||||
CLEANUP_EXIT=$?
|
||||
fi
|
||||
[[ -z "$CLEANUP_OUTPUT" ]] || printf '%s\n' "$CLEANUP_OUTPUT"
|
||||
if [[ "$CLEANUP_EXIT" -ne 0 ]]; then
|
||||
DETAIL="Exit-Code: $CLEANUP_EXIT"
|
||||
if [[ -n "$CLEANUP_OUTPUT" ]]; then
|
||||
DETAIL+="; Ausgabe: $(printf '%s\n' "$CLEANUP_OUTPUT" | compact_output)"
|
||||
fi
|
||||
write_status error "Verwaiste Piwigo-Bilder entfernter WebDAV-Quellen konnten nicht bereinigt werden" "$DETAIL"
|
||||
exit "$CLEANUP_EXIT"
|
||||
fi
|
||||
|
||||
METADATA_OUTPUT=""
|
||||
METADATA_EXIT=0
|
||||
if METADATA_OUTPUT="$(php "$SCRIPT_DIR/lib/sync-webdav-metadata.php" \
|
||||
|
||||
@@ -23,7 +23,7 @@ $_SERVER['REQUEST_URI'] = '/';
|
||||
$_SERVER['SCRIPT_NAME'] = '/plugins/bratonien_tools/runtime/webdav-warmup-dispatch.php';
|
||||
$_SERVER['PHP_SELF'] = $_SERVER['SCRIPT_NAME'];
|
||||
$_SERVER['QUERY_STRING'] = '';
|
||||
$_SERVER['HTTP_USER_AGENT'] = 'Bratonien-WebDAV-Worker-Dispatcher/0.9.7.1.23';
|
||||
$_SERVER['HTTP_USER_AGENT'] = 'Bratonien-WebDAV-Worker-Dispatcher/0.9.7.1.50';
|
||||
$_SERVER['HTTPS'] = 'off';
|
||||
|
||||
require_once(PHPWG_ROOT_PATH.'include/common.inc.php');
|
||||
@@ -66,6 +66,113 @@ function bratonien_tools_webdav_dispatch_failure_lines(array $output)
|
||||
return $failures;
|
||||
}
|
||||
|
||||
function bratonien_tools_webdav_dispatch_only_connector_deferrals(array $output)
|
||||
{
|
||||
$seen = false;
|
||||
foreach ($output as $line)
|
||||
{
|
||||
$line = trim((string)$line);
|
||||
if ($line === '') continue;
|
||||
if (strpos($line, '[BRAT-WORKER] fatal ') !== false || strpos($line, 'download_failed') !== false || stripos($line, 'RESTORE FEHLGESCHLAGEN') !== false)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (strpos($line, ' ok=0 ') !== false)
|
||||
{
|
||||
$seen = true;
|
||||
if (strpos($line, 'message=Connector-Synchronisierung läuft.') === false && strpos($line, 'message=Connector-Mapping hat sich seit dem Batch-Download geändert.') === false && strpos($line, 'message=Shadow-Tree hat sich seit dem Batch-Download geändert.') === false)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $seen;
|
||||
}
|
||||
|
||||
function bratonien_tools_webdav_dispatch_connector_service_active()
|
||||
{
|
||||
if (!function_exists('exec') || !is_executable('/usr/bin/systemctl')) return false;
|
||||
$output = array();
|
||||
$exit = 1;
|
||||
@exec('/usr/bin/systemctl is-active --quiet bratonien-nc-connector.service 2>/dev/null', $output, $exit);
|
||||
return $exit === 0;
|
||||
}
|
||||
|
||||
function bratonien_tools_webdav_dispatch_reset_orphan_sync_lock($state_dir)
|
||||
{
|
||||
$path = rtrim((string)$state_dir, '/').'/webdav-sync.lock';
|
||||
if (!is_file($path)) return true;
|
||||
|
||||
$lock = @fopen($path, 'c');
|
||||
if (!$lock) return false;
|
||||
if (@flock($lock, LOCK_SH | LOCK_NB))
|
||||
{
|
||||
@flock($lock, LOCK_UN);
|
||||
fclose($lock);
|
||||
return true;
|
||||
}
|
||||
fclose($lock);
|
||||
|
||||
// Der Produktions-Connector darf ausschließlich über den systemd-Dienst
|
||||
// laufen. Ist dieser nicht aktiv, ist ein weiterhin exklusiv belegter Lock
|
||||
// kein gültiger Connector-Zustand. Den alten Inode hängen wir aus und legen
|
||||
// eine frische Lockdatei an, damit ein verwaister Altprozess den Cache nicht
|
||||
// dauerhaft blockiert. Ein späterer echter Connector benutzt automatisch die
|
||||
// neue Datei.
|
||||
if (bratonien_tools_webdav_dispatch_connector_service_active()) return false;
|
||||
|
||||
$orphan = $path.'.orphan-'.date('YmdHis').'-'.getmypid();
|
||||
if (!@rename($path, $orphan)) return false;
|
||||
$fresh = @fopen($path, 'c');
|
||||
if (!$fresh)
|
||||
{
|
||||
@rename($orphan, $path);
|
||||
return false;
|
||||
}
|
||||
@chmod($path, 0660);
|
||||
fclose($fresh);
|
||||
@file_put_contents(PHPWG_ROOT_PATH.PWG_LOCAL_DIR.'bratonien-webdav-warmup.log', '[BRAT-WORKER] orphan_sync_lock_reset old='.basename($orphan)."\n", FILE_APPEND | LOCK_EX);
|
||||
return true;
|
||||
}
|
||||
|
||||
function bratonien_tools_webdav_dispatch_connector_busy($state_dir)
|
||||
{
|
||||
$path = rtrim((string)$state_dir, '/').'/webdav-sync.lock';
|
||||
$lock = @fopen($path, 'c');
|
||||
if (!$lock) return false;
|
||||
if (@flock($lock, LOCK_SH | LOCK_NB))
|
||||
{
|
||||
@flock($lock, LOCK_UN);
|
||||
fclose($lock);
|
||||
return false;
|
||||
}
|
||||
fclose($lock);
|
||||
|
||||
if (bratonien_tools_webdav_dispatch_connector_service_active()) return true;
|
||||
|
||||
// Genau der hier beobachtete Fehlerfall: Lock belegt, aber kein Connector-
|
||||
// Dienst aktiv. Nicht als "Connector läuft" darstellen, sondern den
|
||||
// verwaisten Lockzustand reparieren und den Cache normal starten lassen.
|
||||
if (bratonien_tools_webdav_dispatch_reset_orphan_sync_lock($state_dir)) return false;
|
||||
|
||||
// Konnte der ungültige Lockzustand nicht sicher repariert werden, wird nicht
|
||||
// behauptet, dass ein Connector läuft. Der Worker erhält stattdessen einen
|
||||
// echten Fehler und kann den Zustand sichtbar machen.
|
||||
throw new RuntimeException('WebDAV-Sync-Lock ist belegt, obwohl der Connector-Dienst nicht aktiv ist, und konnte nicht sicher zurückgesetzt werden.');
|
||||
}
|
||||
|
||||
function bratonien_tools_webdav_dispatch_schedule_after_sync($waiter, $state_dir, $worker_command, $log, &$pid=null)
|
||||
{
|
||||
$pid = 0;
|
||||
$deferred = escapeshellarg('/bin/bash').' '.escapeshellarg($waiter).' '.escapeshellarg($state_dir).' '.$worker_command;
|
||||
$command = 'nohup '.$deferred.' >> '.escapeshellarg($log).' 2>&1 < /dev/null & echo $!';
|
||||
$output = array();
|
||||
$exit = 1;
|
||||
@exec($command, $output, $exit);
|
||||
$pid = isset($output[0]) ? (int)$output[0] : 0;
|
||||
return $exit === 0 && $pid > 0;
|
||||
}
|
||||
|
||||
$mode = 'periodic';
|
||||
$wait = false;
|
||||
$connection_filter = 0;
|
||||
@@ -90,6 +197,7 @@ if (!function_exists('exec'))
|
||||
|
||||
$worker = realpath(BRATONIEN_TOOLS_PATH.'runtime/lib/webdav-cache-warmup.php');
|
||||
$priority_waiter = realpath(BRATONIEN_TOOLS_PATH.'runtime/lib/run-webdav-priority-wait.sh');
|
||||
$after_sync_waiter = realpath(BRATONIEN_TOOLS_PATH.'runtime/lib/run-webdav-after-sync.sh');
|
||||
if (!$worker || !is_file($worker))
|
||||
{
|
||||
fwrite(STDERR, "WebDAV-Worker wurde nicht gefunden.\n");
|
||||
@@ -100,6 +208,11 @@ if (!$priority_waiter || !is_file($priority_waiter))
|
||||
fwrite(STDERR, "WebDAV-Prioritäts-Warter wurde nicht gefunden.\n");
|
||||
exit(1);
|
||||
}
|
||||
if (!$after_sync_waiter || !is_file($after_sync_waiter))
|
||||
{
|
||||
fwrite(STDERR, "WebDAV-Connector-Warter wurde nicht gefunden.\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$log = PHPWG_ROOT_PATH.PWG_LOCAL_DIR.'bratonien-webdav-warmup.log';
|
||||
$result = 0;
|
||||
@@ -125,6 +238,25 @@ foreach (bratonien_tools_nc_connector_connections() as $connection)
|
||||
continue;
|
||||
}
|
||||
|
||||
$worker_command = escapeshellarg(PHP_BINARY).' '.escapeshellarg($worker)
|
||||
.' --connection-id='.$connection_id
|
||||
.' --mode='.escapeshellarg($mode);
|
||||
|
||||
try
|
||||
{
|
||||
$connector_busy = $mode !== 'sync' && bratonien_tools_webdav_dispatch_connector_busy($state_dir);
|
||||
}
|
||||
catch (Throwable $e)
|
||||
{
|
||||
if (function_exists('bratonien_tools_webdav_warmup_write_status'))
|
||||
{
|
||||
bratonien_tools_webdav_warmup_write_status($connection_id, 'error', $e->getMessage(), array('mode'=>$mode, 'deferred_by_connector'=>false));
|
||||
}
|
||||
fwrite(STDERR, $e->getMessage()."\n");
|
||||
$result = 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($mode === 'sync')
|
||||
{
|
||||
if (!is_dir($state_dir) && !@mkdir($state_dir, 0750, true) && !is_dir($state_dir))
|
||||
@@ -147,10 +279,32 @@ foreach (bratonien_tools_nc_connector_connections() as $connection)
|
||||
}
|
||||
@chmod($priority_file, 0664);
|
||||
}
|
||||
elseif ($connector_busy)
|
||||
{
|
||||
$resume_pid = 0;
|
||||
$scheduled = bratonien_tools_webdav_dispatch_schedule_after_sync($after_sync_waiter, $state_dir, $worker_command, $log, $resume_pid);
|
||||
if (function_exists('bratonien_tools_webdav_warmup_write_status'))
|
||||
{
|
||||
bratonien_tools_webdav_warmup_write_status(
|
||||
$connection_id,
|
||||
$scheduled ? 'waiting' : 'error',
|
||||
$scheduled
|
||||
? 'Connector-Synchronisierung läuft. Der Cache-Worker ist vorgemerkt und startet automatisch unmittelbar danach; es werden vorher keine Nextcloud-Originale geladen.'
|
||||
: 'Connector-Synchronisierung läuft, aber der automatische Cache-Fortsetzungsauftrag konnte nicht gestartet werden.',
|
||||
array('mode'=>$mode, 'deferred_by_connector'=>true, 'resume_scheduled'=>$scheduled, 'resume_pid'=>$resume_pid)
|
||||
);
|
||||
}
|
||||
if (!$scheduled)
|
||||
{
|
||||
fwrite(STDERR, "Wartender WebDAV-Worker für Verbindung #{$connection_id} konnte nicht gestartet werden.\n");
|
||||
$result = 1;
|
||||
continue;
|
||||
}
|
||||
$started++;
|
||||
fwrite(STDOUT, "WebDAV-Worker für Verbindung #{$connection_id} wartet auf den Connector-Sync und startet danach automatisch (PID {$resume_pid}).\n");
|
||||
continue;
|
||||
}
|
||||
|
||||
$worker_command = escapeshellarg(PHP_BINARY).' '.escapeshellarg($worker)
|
||||
.' --connection-id='.$connection_id
|
||||
.' --mode='.escapeshellarg($mode);
|
||||
$base = $mode === 'sync'
|
||||
? escapeshellarg('/bin/bash').' '.escapeshellarg($priority_waiter).' '.escapeshellarg($state_dir).' '.$worker_command
|
||||
: $worker_command;
|
||||
@@ -167,12 +321,40 @@ foreach (bratonien_tools_nc_connector_connections() as $connection)
|
||||
}
|
||||
if ($exit !== 0)
|
||||
{
|
||||
$deferred = bratonien_tools_webdav_dispatch_only_connector_deferrals($output);
|
||||
if ($deferred)
|
||||
{
|
||||
$resume_pid = 0;
|
||||
$scheduled = bratonien_tools_webdav_dispatch_schedule_after_sync($after_sync_waiter, $state_dir, $worker_command, $log, $resume_pid);
|
||||
if (function_exists('bratonien_tools_webdav_warmup_write_status'))
|
||||
{
|
||||
$existing = bratonien_tools_webdav_dispatch_existing_status($connection_id);
|
||||
$extra = is_array($existing) ? $existing : array();
|
||||
unset($extra['state'], $extra['message'], $extra['connection_id'], $extra['updated_at']);
|
||||
$extra['worker_exit_code'] = (int)$exit;
|
||||
$extra['deferred_by_connector'] = true;
|
||||
$extra['resume_scheduled'] = $scheduled;
|
||||
$extra['resume_pid'] = $resume_pid;
|
||||
bratonien_tools_webdav_warmup_write_status(
|
||||
$connection_id,
|
||||
$scheduled ? 'waiting' : 'error',
|
||||
$scheduled
|
||||
? 'Connector-Synchronisierung hat während des Batches Vorrang erhalten. Der bisherige Indexstand bleibt erhalten; der Cache-Worker ist zur automatischen Fortsetzung vorgemerkt.'
|
||||
: 'Connector-Synchronisierung hat während des Batches Vorrang erhalten, aber die automatische Fortsetzung konnte nicht vorgemerkt werden.',
|
||||
$extra
|
||||
);
|
||||
}
|
||||
if (!$scheduled) $result = 1;
|
||||
else $started++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$result = $exit;
|
||||
if (function_exists('bratonien_tools_webdav_warmup_write_status'))
|
||||
{
|
||||
$existing = bratonien_tools_webdav_dispatch_existing_status($connection_id);
|
||||
$failures = bratonien_tools_webdav_dispatch_failure_lines($output);
|
||||
$terminal = is_array($existing) && in_array((string)($existing['state'] ?? ''), array('error','fatal','preempted'), true);
|
||||
$terminal = is_array($existing) && in_array((string)($existing['state'] ?? ''), array('error','fatal','preempted','cancelled'), true);
|
||||
|
||||
if ($terminal)
|
||||
{
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
{ id:'wasserzeichen', label:'Wasserzeichen', sections:['uebersicht','wasserzeichen','regeln'] },
|
||||
{ id:'auswahl-download', label:'Fotoauswahl & Downloads' },
|
||||
{ id:'freigaben', label:'Geschützte Freigaben' },
|
||||
{ id:'qr-upload', label:'QR-Upload' },
|
||||
{ id:'nc-connector', label:'NC Connector' },
|
||||
{ id:'bilddateien', label:'Bilddateien & Pfade' },
|
||||
{ id:'wartung', label:'Wartung / Cache' },
|
||||
|
||||
198
template/customer_qr_admin.tpl
Normal file
198
template/customer_qr_admin.tpl
Normal file
@@ -0,0 +1,198 @@
|
||||
{literal}
|
||||
<style>
|
||||
.bratonien-qr-stock-head{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;margin-bottom:14px}
|
||||
.bratonien-qr-stock-head h4{margin:0}
|
||||
.bratonien-qr-legend{display:flex;gap:14px;align-items:center;flex-wrap:wrap;margin:0 0 16px}
|
||||
.bratonien-qr-legend-item{display:inline-flex;gap:7px;align-items:center}
|
||||
.bratonien-qr-legend-box{width:18px;height:18px;border-radius:4px;border:1px solid}
|
||||
.bratonien-qr-legend-box.is-present{background:rgba(62,170,94,.28);border-color:#58c878}
|
||||
.bratonien-qr-legend-box.is-missing{background:rgba(205,72,72,.25);border-color:#db6d6d}
|
||||
.bratonien-qr-year{padding:14px 0;border-top:1px solid rgba(255,255,255,.10)}
|
||||
.bratonien-qr-year:first-of-type{border-top:0;padding-top:0}
|
||||
.bratonien-qr-year-head{display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin-bottom:10px}
|
||||
.bratonien-qr-year-head strong{font-size:1.05rem}
|
||||
.bratonien-qr-year-count{opacity:.78}
|
||||
.bratonien-qr-year-download{margin-left:auto}
|
||||
.bratonien-qr-slots{display:grid;grid-template-columns:repeat(auto-fill,minmax(42px,1fr));gap:6px}
|
||||
.bratonien-qr-slot{min-height:38px;display:flex;align-items:center;justify-content:center;border:1px solid;border-radius:5px;font-weight:700;text-decoration:none!important}
|
||||
.bratonien-qr-slot.is-present{background:rgba(62,170,94,.28);border-color:#58c878;color:#dff7e6}
|
||||
.bratonien-qr-slot.is-present:hover{background:rgba(62,170,94,.42);color:#fff}
|
||||
.bratonien-qr-slot.is-missing{background:rgba(205,72,72,.25);border-color:#db6d6d;color:#ffdede}
|
||||
@media(max-width:760px){.bratonien-qr-year-download{margin-left:0}.bratonien-qr-slots{grid-template-columns:repeat(auto-fill,minmax(38px,1fr))}}
|
||||
</style>
|
||||
{/literal}
|
||||
|
||||
<section class="bratonien-section" id="qr-upload">
|
||||
<h3>QR-Upload</h3>
|
||||
<p class="bratonien-section__intro">Aktivierbares Uploadformular für nummerierte QR-Codes und persönliche Freundschaftscodes.</p>
|
||||
|
||||
<div class="bratonien-grid">
|
||||
<div class="bratonien-card">
|
||||
<h4>Formular</h4>
|
||||
<form method="post">
|
||||
<input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}">
|
||||
<input type="hidden" name="bratonien_tool" value="customer_qr_settings">
|
||||
|
||||
<label style="display:flex;gap:10px;align-items:center;margin:0 0 14px">
|
||||
<input type="checkbox" name="customer_qr_enabled" value="1" {if $CUSTOMER_QR.enabled}checked{/if}>
|
||||
<strong>QR-Upload aktivieren</strong>
|
||||
</label>
|
||||
|
||||
<button class="buttonLike" type="submit">Einstellung speichern</button>
|
||||
</form>
|
||||
|
||||
<p class="bratonien-base-note" style="margin-top:14px">Ist das Formular deaktiviert, verschwindet der Menüpunkt und die Uploadseite nimmt keine Dateien an.</p>
|
||||
</div>
|
||||
|
||||
<div class="bratonien-card">
|
||||
<h4>Formular-Adresse</h4>
|
||||
<div class="bratonien-form-grid">
|
||||
<span class="bratonien-label">Status</span><strong>{if $CUSTOMER_QR.enabled}Aktiv{else}Deaktiviert{/if}</strong>
|
||||
<span class="bratonien-label">Jahre</span><strong>{$CUSTOMER_QR.year_min}–{$CUSTOMER_QR.year_max}</strong>
|
||||
<span class="bratonien-label">Standardjahr</span><strong>{$CUSTOMER_QR.default_year}</strong>
|
||||
<span class="bratonien-label">Server-Batchlimit</span><strong>{$CUSTOMER_QR.max_files} Dateien</strong>
|
||||
<span class="bratonien-label">Freundschaftscodes</span><strong>{$CUSTOMER_QR.friendship_codes.total}</strong>
|
||||
</div>
|
||||
<p style="margin-top:14px;overflow-wrap:anywhere"><a href="{$CUSTOMER_QR.url|escape:html}" target="_blank" rel="noopener">{$CUSTOMER_QR.url|escape:html}</a></p>
|
||||
</div>
|
||||
|
||||
<div class="bratonien-card">
|
||||
<h4>Vorgesehene QR-Codes</h4>
|
||||
<div class="bratonien-form-grid">
|
||||
{foreach from=$CUSTOMER_QR.years item=qr_year}
|
||||
<span class="bratonien-label">{$qr_year.year}</span>
|
||||
<strong>{$qr_year.used} / {$qr_year.capacity} belegt · {$qr_year.remaining} frei</strong>
|
||||
{/foreach}
|
||||
</div>
|
||||
<p class="bratonien-base-note">2023: Nummern 1–100 · 2024: 1–50 · 2025 und 2026: jeweils 1–30. Weitere Jahre werden erst mit dem jeweiligen Jahresupdate ergänzt.</p>
|
||||
</div>
|
||||
|
||||
<div class="bratonien-card">
|
||||
<h4>Belegte QR-Code-Nummern</h4>
|
||||
<div class="bratonien-form-grid">
|
||||
<span class="bratonien-label">Gesamt</span><strong>{$CUSTOMER_QR.total}</strong>
|
||||
<span class="bratonien-label">Im Standardjahr {$CUSTOMER_QR.default_year}</span><strong>{$CUSTOMER_QR.current_year_total} / {$CUSTOMER_QR.current_year_capacity}</strong>
|
||||
</div>
|
||||
<p class="bratonien-base-note">Eine QR-Code-Nummer ist pro Jahr eindeutig. Wird ein Upload gelöscht, ist die Nummer für dieses Jahr wieder frei.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bratonien-card" style="margin-top:18px">
|
||||
<div class="bratonien-qr-stock-head">
|
||||
<h4>QR-Code-Bestand</h4>
|
||||
{if $CUSTOMER_QR.batch_download_available and $CUSTOMER_QR.total > 0}
|
||||
<a class="buttonLike" href="{$CUSTOMER_QR.batch_download_url|escape:html}">Alle QR-Codes als ZIP herunterladen</a>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="bratonien-qr-legend">
|
||||
<span class="bratonien-qr-legend-item"><span class="bratonien-qr-legend-box is-present"></span>Vorhanden</span>
|
||||
<span class="bratonien-qr-legend-item"><span class="bratonien-qr-legend-box is-missing"></span>Fehlt</span>
|
||||
</div>
|
||||
|
||||
{foreach from=$CUSTOMER_QR.years item=qr_year}
|
||||
<div class="bratonien-qr-year">
|
||||
<div class="bratonien-qr-year-head">
|
||||
<strong>{$qr_year.year}</strong>
|
||||
<span class="bratonien-qr-year-count">{$qr_year.used} / {$qr_year.capacity} vorhanden · {$qr_year.remaining} fehlen</span>
|
||||
{if $CUSTOMER_QR.batch_download_available and $qr_year.used > 0}
|
||||
<a class="buttonLike bratonien-qr-year-download" href="{$qr_year.batch_download_url|escape:html}">{$qr_year.year} als ZIP</a>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="bratonien-qr-slots" aria-label="QR-Code-Bestand {$qr_year.year}">
|
||||
{foreach from=$qr_year.slots item=qr_slot}
|
||||
{if $qr_slot.present}
|
||||
<a class="bratonien-qr-slot is-present" href="{$qr_slot.preview_url|escape:html}" target="_blank" rel="noopener" title="QR-Code #{$qr_slot.number} vorhanden – Vorschau öffnen">{$qr_slot.number}</a>
|
||||
{else}
|
||||
<span class="bratonien-qr-slot is-missing" title="QR-Code #{$qr_slot.number} fehlt">{$qr_slot.number}</span>
|
||||
{/if}
|
||||
{/foreach}
|
||||
</div>
|
||||
</div>
|
||||
{/foreach}
|
||||
|
||||
{if not $CUSTOMER_QR.batch_download_available}
|
||||
<p class="bratonien-base-note" style="margin-top:14px">Batch-Download ist erst verfügbar, wenn die PHP-Erweiterung ZipArchive auf dem Server aktiv ist.</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="bratonien-card" style="margin-top:18px;overflow-x:auto">
|
||||
<h4>QR-Codes verwalten</h4>
|
||||
{if $CUSTOMER_QR.total > 0}
|
||||
<table style="width:100%;border-collapse:collapse;min-width:820px">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="text-align:left;padding:9px 8px;border-bottom:1px solid rgba(255,255,255,.14)">Jahr</th>
|
||||
<th style="text-align:left;padding:9px 8px;border-bottom:1px solid rgba(255,255,255,.14)">QR-Nr.</th>
|
||||
<th style="text-align:left;padding:9px 8px;border-bottom:1px solid rgba(255,255,255,.14)">Datei</th>
|
||||
<th style="text-align:left;padding:9px 8px;border-bottom:1px solid rgba(255,255,255,.14)">Größe</th>
|
||||
<th style="text-align:left;padding:9px 8px;border-bottom:1px solid rgba(255,255,255,.14)">Upload</th>
|
||||
<th style="text-align:right;padding:9px 8px;border-bottom:1px solid rgba(255,255,255,.14)">Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{foreach from=$CUSTOMER_QR.uploads item=qr_upload}
|
||||
<tr>
|
||||
<td style="padding:9px 8px;border-bottom:1px solid rgba(255,255,255,.08)"><strong>{$qr_upload.year}</strong></td>
|
||||
<td style="padding:9px 8px;border-bottom:1px solid rgba(255,255,255,.08)"><strong>#{$qr_upload.number|escape:html}</strong></td>
|
||||
<td style="padding:9px 8px;border-bottom:1px solid rgba(255,255,255,.08);max-width:300px;overflow-wrap:anywhere">{$qr_upload.original_name|escape:html}</td>
|
||||
<td style="padding:9px 8px;border-bottom:1px solid rgba(255,255,255,.08)">{$qr_upload.file_size_label|escape:html}</td>
|
||||
<td style="padding:9px 8px;border-bottom:1px solid rgba(255,255,255,.08)">{$qr_upload.created|escape:html}</td>
|
||||
<td style="padding:9px 8px;border-bottom:1px solid rgba(255,255,255,.08);text-align:right;white-space:nowrap">
|
||||
<a class="buttonLike" href="{$qr_upload.preview_url|escape:html}" target="_blank" rel="noopener" style="display:inline-block;margin-right:6px">Vorschau</a>
|
||||
<a class="buttonLike" href="{$qr_upload.preview_url|escape:html}&download=1" style="display:inline-block;margin-right:6px">Herunterladen</a>
|
||||
<form method="post" style="display:inline" onsubmit="return confirm('Diesen QR-Upload wirklich löschen? Die QR-Nummer wird danach wieder frei.');">
|
||||
<input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}">
|
||||
<input type="hidden" name="bratonien_tool" value="customer_qr_delete">
|
||||
<input type="hidden" name="customer_qr_id" value="{$qr_upload.id}">
|
||||
<button class="buttonLike" type="submit">Löschen</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{/foreach}
|
||||
</tbody>
|
||||
</table>
|
||||
{else}
|
||||
<p class="bratonien-base-note">Noch keine QR-Codes hochgeladen.</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="bratonien-card" style="margin-top:18px;overflow-x:auto">
|
||||
<h4>Persönliche Freundschaftscodes</h4>
|
||||
{if $CUSTOMER_QR.friendship_codes.total > 0}
|
||||
<table style="width:100%;border-collapse:collapse;min-width:760px">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="text-align:left;padding:9px 8px;border-bottom:1px solid rgba(255,255,255,.14)">Name</th>
|
||||
<th style="text-align:left;padding:9px 8px;border-bottom:1px solid rgba(255,255,255,.14)">Datei</th>
|
||||
<th style="text-align:left;padding:9px 8px;border-bottom:1px solid rgba(255,255,255,.14)">Größe</th>
|
||||
<th style="text-align:left;padding:9px 8px;border-bottom:1px solid rgba(255,255,255,.14)">Upload</th>
|
||||
<th style="text-align:right;padding:9px 8px;border-bottom:1px solid rgba(255,255,255,.14)">Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{foreach from=$CUSTOMER_QR.friendship_codes.uploads item=friendship}
|
||||
<tr>
|
||||
<td style="padding:9px 8px;border-bottom:1px solid rgba(255,255,255,.08)"><strong>{$friendship.name|escape:html}</strong></td>
|
||||
<td style="padding:9px 8px;border-bottom:1px solid rgba(255,255,255,.08);max-width:300px;overflow-wrap:anywhere">{$friendship.original_name|escape:html}</td>
|
||||
<td style="padding:9px 8px;border-bottom:1px solid rgba(255,255,255,.08)">{$friendship.file_size_label|escape:html}</td>
|
||||
<td style="padding:9px 8px;border-bottom:1px solid rgba(255,255,255,.08)">{$friendship.created|escape:html}</td>
|
||||
<td style="padding:9px 8px;border-bottom:1px solid rgba(255,255,255,.08);text-align:right;white-space:nowrap">
|
||||
<a class="buttonLike" href="{$friendship.preview_url|escape:html}" target="_blank" rel="noopener" style="display:inline-block;margin-right:6px">Vorschau</a>
|
||||
<a class="buttonLike" href="{$friendship.download_url|escape:html}" style="display:inline-block;margin-right:6px">Herunterladen</a>
|
||||
<form method="post" style="display:inline" onsubmit="return confirm('Diesen Freundschaftscode wirklich löschen?');">
|
||||
<input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}">
|
||||
<input type="hidden" name="bratonien_tool" value="friendship_code_delete">
|
||||
<input type="hidden" name="friendship_code_id" value="{$friendship.id}">
|
||||
<button class="buttonLike" type="submit">Löschen</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{/foreach}
|
||||
</tbody>
|
||||
</table>
|
||||
{else}
|
||||
<p class="bratonien-base-note">Noch keine persönlichen Freundschaftscodes hochgeladen.</p>
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
@@ -61,7 +61,12 @@ function bratonien_tools_save_album_rule()
|
||||
)));
|
||||
}
|
||||
|
||||
return array('message'=>'Albumregel gespeichert.');
|
||||
if (function_exists('bratonien_tools_presentation_refresh_enqueue'))
|
||||
{
|
||||
bratonien_tools_presentation_refresh_enqueue(array($category), 'album-watermark-rule-changed');
|
||||
}
|
||||
|
||||
return array('message'=>'Albumregel gespeichert. Betroffene Vorschauen werden im Hintergrund aktualisiert.');
|
||||
}
|
||||
|
||||
function bratonien_tools_get_category_tree()
|
||||
|
||||
@@ -108,9 +108,10 @@ function bratonien_tools_save_watermark_profile()
|
||||
throw new RuntimeException('Profilname darf nicht leer sein.');
|
||||
}
|
||||
|
||||
$existing_profile = $id > 0 ? bratonien_tools_get_watermark_profile($id) : null;
|
||||
if ($id > 0)
|
||||
{
|
||||
if (!bratonien_tools_get_watermark_profile($id))
|
||||
if (!$existing_profile)
|
||||
{
|
||||
throw new RuntimeException('Wasserzeichenprofil nicht gefunden.');
|
||||
}
|
||||
@@ -128,7 +129,14 @@ function bratonien_tools_save_watermark_profile()
|
||||
mass_inserts($table, array_keys($data), array($data));
|
||||
}
|
||||
|
||||
return array('message'=>'Wasserzeichenprofil gespeichert.');
|
||||
if ($id > 0 && function_exists('bratonien_tools_presentation_refresh_enqueue_all'))
|
||||
{
|
||||
bratonien_tools_presentation_refresh_enqueue_all('watermark-profile-changed');
|
||||
}
|
||||
|
||||
return array('message'=>$id > 0
|
||||
? 'Wasserzeichenprofil gespeichert. Betroffene Vorschauen werden im Hintergrund aktualisiert.'
|
||||
: 'Wasserzeichenprofil gespeichert.');
|
||||
}
|
||||
|
||||
function bratonien_tools_profile_is_referenced($id)
|
||||
|
||||
@@ -4,15 +4,47 @@ if (!defined('PHPWG_ROOT_PATH'))
|
||||
die('Hacking attempt!');
|
||||
}
|
||||
|
||||
function bratonien_tools_default_public_watermark_profile_id()
|
||||
{
|
||||
if (!function_exists('bratonien_tools_create_default_watermark_profiles'))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
bratonien_tools_create_default_watermark_profiles();
|
||||
$table = bratonien_tools_table('watermark_profiles');
|
||||
$row = pwg_db_fetch_assoc(pwg_query(
|
||||
"SELECT id FROM ".$table.
|
||||
" WHERE active=1 AND name='Oeffentlich' ORDER BY id ASC LIMIT 1"
|
||||
));
|
||||
return $row ? (int)$row['id'] : null;
|
||||
}
|
||||
|
||||
function bratonien_tools_get_watermark_defaults()
|
||||
{
|
||||
$defaults = conf_get_param('bratonien_watermark_defaults', null);
|
||||
$defaults = $defaults ? json_decode($defaults, true) : array();
|
||||
|
||||
return array_merge(array(
|
||||
$defaults = array_merge(array(
|
||||
'public_profile' => null,
|
||||
'private_profile' => null,
|
||||
), is_array($defaults) ? $defaults : array());
|
||||
|
||||
// Oeffentliche Alben sind standardmaessig geschuetzt. Nur eine explizite
|
||||
// Albumregel (anderes Profil oder deaktiviert) darf dieses Verhalten
|
||||
// ueberschreiben. Bestehende Installationen ohne gesetztes globales
|
||||
// Oeffentlich-Profil werden automatisch auf das mitgelieferte Profil
|
||||
// "Oeffentlich" migriert.
|
||||
if (empty($defaults['public_profile']))
|
||||
{
|
||||
$public_profile = bratonien_tools_default_public_watermark_profile_id();
|
||||
if ($public_profile)
|
||||
{
|
||||
$defaults['public_profile'] = $public_profile;
|
||||
conf_update_param('bratonien_watermark_defaults', json_encode($defaults));
|
||||
}
|
||||
}
|
||||
|
||||
return $defaults;
|
||||
}
|
||||
|
||||
function bratonien_tools_validate_default_profile($value)
|
||||
@@ -34,12 +66,23 @@ function bratonien_tools_validate_default_profile($value)
|
||||
|
||||
function bratonien_tools_save_watermark_defaults()
|
||||
{
|
||||
$public_profile = bratonien_tools_validate_default_profile($_POST['public_profile'] ?? null);
|
||||
if ($public_profile === null)
|
||||
{
|
||||
$public_profile = bratonien_tools_default_public_watermark_profile_id();
|
||||
}
|
||||
|
||||
$config = array(
|
||||
'public_profile' => bratonien_tools_validate_default_profile($_POST['public_profile'] ?? null),
|
||||
'public_profile' => $public_profile,
|
||||
'private_profile' => bratonien_tools_validate_default_profile($_POST['private_profile'] ?? null),
|
||||
);
|
||||
|
||||
conf_update_param('bratonien_watermark_defaults', json_encode($config));
|
||||
|
||||
return array('message'=>'Globale Wasserzeichenregeln gespeichert.');
|
||||
if (function_exists('bratonien_tools_presentation_refresh_enqueue_all'))
|
||||
{
|
||||
bratonien_tools_presentation_refresh_enqueue_all('global-watermark-rules-changed');
|
||||
}
|
||||
|
||||
return array('message'=>'Globale Wasserzeichenregeln gespeichert. Oeffentliche Alben verwenden ohne abweichende Albumregel immer das Standard-Wasserzeichenprofil. Vorschauen werden im Hintergrund an die neuen Regeln angepasst.');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user