Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions admin/components/_getSettings.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@
case 'icon':
echo AdminInput::renderIcon($setting, $i18ntag);
break;
case 'lucide-icon':
echo AdminInput::renderLucideIcon($setting, $i18ntag);
break;
case 'input':
case 'number':
echo AdminInput::renderInput($setting, $i18ntag);
Expand Down
3 changes: 3 additions & 0 deletions admin/components/footer.scripts.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,7 @@
$assetService = AssetService::getInstance();

echo '<script src="' . $assetService->getUrl('api/settings.php') . '"></script>';
echo '<script src="' . $assetService->getUrl('node_modules/lucide/dist/umd/lucide.min.js') . '"></script>';

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This introduces runtime dependencies on node_modules/lucide/..., node_modules/iconify-icon/..., and also assets/js/admin/iconSelect.js, but the release archive builder does not ship those new paths. scripts/pack-build.js only archives a fixed allowlist of node_modules packages and resources/; it does not include lucide, iconify-icon, or the assets/ tree. In a zip-based install these requests become 404s, so the picker/start-page rendering will work in dev checkouts but break in packaged releases.

echo '<script src="' . $assetService->getUrl('node_modules/iconify-icon/dist/iconify-icon.min.js') . '"></script>';
echo '<script src="' . $assetService->getUrl('resources/js/main.admin.js') . '"></script>';
echo '<script src="' . $assetService->getUrl('assets/js/admin/iconSelect.js') . '"></script>';
197 changes: 196 additions & 1 deletion api/admin.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,15 @@
use Photobooth\Service\ConfigurationService;
use Photobooth\Service\DatabaseManagerService;
use Photobooth\Service\ImageMetadataCacheService;
use Photobooth\Service\LanguageService;
use Photobooth\Service\LoggerService;
use Photobooth\Service\MailService;
use Photobooth\Service\PrintManagerService;
use Photobooth\Service\ProcessService;
use Photobooth\Utility\ArrayUtility;
use Photobooth\Utility\AdminKeypad;
use Photobooth\Utility\EventIconCatalogUtility;
use Photobooth\Utility\EventSymbolUtility;
use Photobooth\Utility\PathUtility;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Finder\Finder;
Expand All @@ -25,6 +28,7 @@
$loggerService = LoggerService::getInstance();
$logger = $loggerService->getLogger('main');
$logger->debug(basename($_SERVER['PHP_SELF']));
$languageService = LanguageService::getInstance();

checkCsrfOrFail($_POST);

Expand All @@ -35,7 +39,190 @@
$action = $data['type'] ?? null;

// Reset
if ($action === 'reset') {
if ($action === 'event_symbol_upload') {
if (!isset($_FILES['event_symbol_image']) || !is_array($_FILES['event_symbol_image'])) {
echo json_encode([
'status' => 'error',
'message' => $languageService->translate('event_symbol:api_no_file_uploaded'),
]);
exit();
}

$upload = $_FILES['event_symbol_image'];
$uploadError = isset($upload['error']) ? (int) $upload['error'] : UPLOAD_ERR_NO_FILE;
if ($uploadError !== UPLOAD_ERR_OK) {
echo json_encode([
'status' => 'error',
'message' => str_replace(
'%d',
(string) $uploadError,
$languageService->translate('event_symbol:api_upload_failed_code'),
),
]);
exit();
}

$tmpName = (string) ($upload['tmp_name'] ?? '');
if ($tmpName === '' || !is_uploaded_file($tmpName)) {
echo json_encode([
'status' => 'error',
'message' => $languageService->translate('event_symbol:api_invalid_upload_file'),
]);
exit();
}

$originalName = (string) ($upload['name'] ?? '');
$extension = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
if (!EventIconCatalogUtility::isAllowedCustomImageExtension($extension)) {
echo json_encode([
'status' => 'error',
'message' => $languageService->translate('event_symbol:api_file_type_not_allowed'),
]);
exit();
}

$maxBytes = 8 * 1024 * 1024;
$sizeBytes = isset($upload['size']) ? (int) $upload['size'] : 0;
if ($sizeBytes <= 0 || $sizeBytes > $maxBytes) {
echo json_encode([
'status' => 'error',
'message' => $languageService->translate('event_symbol:api_file_empty_or_too_large'),
]);
exit();
}

$allowedMimes = [
'svg' => ['image/svg+xml', 'text/xml', 'application/xml', 'text/plain'],
'png' => ['image/png'],
'jpg' => ['image/jpeg'],
'jpeg' => ['image/jpeg'],
'webp' => ['image/webp'],
'gif' => ['image/gif'],
'avif' => ['image/avif'],
];
$mime = '';
if (function_exists('finfo_open')) {
$finfo = finfo_open(FILEINFO_MIME_TYPE);
if ($finfo !== false) {
$mime = (string) finfo_file($finfo, $tmpName);
finfo_close($finfo);
}
}
if ($mime === '' || !isset($allowedMimes[$extension]) || !in_array($mime, $allowedMimes[$extension], true)) {
echo json_encode([
'status' => 'error',
'message' => $languageService->translate('event_symbol:api_invalid_mime_type'),
]);
exit();
}

$targetDirRelative = EventIconCatalogUtility::getCustomImageDirectoryRelative();
$targetDirAbsolute = EventIconCatalogUtility::getCustomImageDirectoryAbsolute();
if (!is_dir($targetDirAbsolute) && !@mkdir($targetDirAbsolute, 0775, true) && !is_dir($targetDirAbsolute)) {
echo json_encode([
'status' => 'error',
'message' => $languageService->translate('event_symbol:api_upload_dir_create_failed'),
]);
exit();
}
if (!is_writable($targetDirAbsolute)) {
echo json_encode([
'status' => 'error',
'message' => $languageService->translate('event_symbol:api_upload_dir_not_writable'),
]);
exit();
}

$basename = strtolower(pathinfo($originalName, PATHINFO_FILENAME));
$basename = preg_replace('/[^a-z0-9._-]+/', '-', $basename);
$basename = trim((string) $basename, '-._');
if ($basename === '') {
$basename = 'symbol';
}

$fileName = $basename . '.' . $extension;
$counter = 1;
while (is_file($targetDirAbsolute . DIRECTORY_SEPARATOR . $fileName) && $counter < 1000) {
$fileName = $basename . '-' . $counter . '.' . $extension;
$counter++;
}

$targetAbsolutePath = $targetDirAbsolute . DIRECTORY_SEPARATOR . $fileName;
if (!move_uploaded_file($tmpName, $targetAbsolutePath)) {
echo json_encode([
'status' => 'error',
'message' => $languageService->translate('event_symbol:api_file_save_failed'),
]);
exit();
}
@chmod($targetAbsolutePath, 0644);

$relativePath = $targetDirRelative . '/' . $fileName;
$iconEntry = EventIconCatalogUtility::buildCustomImageEntry($relativePath);
if ($iconEntry === null) {
@unlink($targetAbsolutePath);
echo json_encode([
'status' => 'error',
'message' => $languageService->translate('event_symbol:api_custom_icon_create_failed'),
]);
exit();
}

EventIconCatalogUtility::invalidateCache();

echo json_encode([
'status' => 'success',
'message' => $languageService->translate('event_symbol:api_upload_success'),
'icon' => $iconEntry,
]);
exit();
} elseif ($action === 'event_symbol_delete') {
$value = (string) ($data['value'] ?? '');
$normalized = EventSymbolUtility::normalize($value);
if (!EventSymbolUtility::isCustomImageSymbol($normalized)) {
echo json_encode([
'status' => 'error',
'message' => $languageService->translate('event_symbol:api_invalid_custom_image_selection'),
]);
exit();
}

$relativePath = EventSymbolUtility::getCustomImagePath($normalized);
$relativePath = ltrim(PathUtility::fixFilePath($relativePath), '/');
$allowedPrefix = rtrim(EventIconCatalogUtility::getCustomImageDirectoryRelative(), '/') . '/';
if (!str_starts_with($relativePath, $allowedPrefix) || str_contains($relativePath, '..')) {
echo json_encode([
'status' => 'error',
'message' => $languageService->translate('event_symbol:api_invalid_file_path'),
]);
exit();
}

$absolutePath = PathUtility::getAbsolutePath($relativePath);
if (!is_file($absolutePath)) {
echo json_encode([
'status' => 'error',
'message' => $languageService->translate('event_symbol:api_file_not_found'),
]);
exit();
}

if (!@unlink($absolutePath)) {
echo json_encode([
'status' => 'error',
'message' => $languageService->translate('event_symbol:api_file_delete_failed'),
]);
exit();
}

EventIconCatalogUtility::invalidateCache();

echo json_encode([
'status' => 'success',
'message' => $languageService->translate('event_symbol:api_delete_success'),
]);
exit();
} elseif ($action === 'reset') {
// First step in resetting the photobooth is always resetting the logs
// This ensures we are able to write logmessages afterwards.
$loggerService->addLogger('main');
Expand Down Expand Up @@ -124,6 +311,14 @@
} elseif ($action === 'config') {
$logger->debug('Saving Photobooth configuration...');
$newConfig = ArrayUtility::mergeRecursive($defaultConfig, $data);
if (isset($newConfig['event']['symbol'])) {
$newConfig['event']['symbol'] = EventSymbolUtility::normalize($newConfig['event']['symbol']);
}
$collageInput = $data['collage'] ?? null;
if (!is_array($collageInput) || !array_key_exists('background', $collageInput)) {
// Keep legacy single background value if the field is not part of the current admin form.
$newConfig['collage']['background'] = $config['collage']['background'] ?? '';
}

$rootPath = PathUtility::getRootPath();

Expand Down
5 changes: 5 additions & 0 deletions api/settings.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<?php

use Photobooth\Environment;
use Photobooth\Utility\EventSymbolUtility;

require_once '../lib/boot.php';

Expand Down Expand Up @@ -65,6 +66,10 @@
$config['screensaver']['text_position'] = 'center';
}

if (!empty($config['event']['symbol'])) {
$config['event']['symbol'] = EventSymbolUtility::normalize($config['event']['symbol']);
}

echo 'const config = ' . json_encode($config) . ';';
echo 'const environment = ' . json_encode(new Environment()) . ';';
echo 'const csrf = { key: "csrf", token: "' . $_SESSION['csrf'] . '" };';
Loading
Loading