diff --git a/composer.json b/composer.json index 3c0eae4..b96ebd7 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "require": { - "chillerlan/php-qrcode": "^5.0" + "chillerlan/php-qrcode": "^6.0.1" }, "autoload": { "psr-4": { diff --git a/composer.lock b/composer.lock index 20d242d..5a13262 100644 --- a/composer.lock +++ b/composer.lock @@ -4,40 +4,44 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "b4f5229f78cd0eed0c7166614bf05110", + "content-hash": "c898c79e7e1cf9625a3a3f757c17ee0d", "packages": [ { "name": "chillerlan/php-qrcode", - "version": "5.0.3", + "version": "6.0.1", "source": { "type": "git", "url": "https://github.com/chillerlan/php-qrcode.git", - "reference": "42e215640e9ebdd857570c9e4e52245d1ee51de2" + "reference": "49006e34bd5328f163e80329e7312f34dceea59b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/chillerlan/php-qrcode/zipball/42e215640e9ebdd857570c9e4e52245d1ee51de2", - "reference": "42e215640e9ebdd857570c9e4e52245d1ee51de2", + "url": "https://api.github.com/repos/chillerlan/php-qrcode/zipball/49006e34bd5328f163e80329e7312f34dceea59b", + "reference": "49006e34bd5328f163e80329e7312f34dceea59b", "shasum": "" }, "require": { - "chillerlan/php-settings-container": "^2.1.6 || ^3.2.1", + "chillerlan/php-settings-container": "^3.2.1", "ext-mbstring": "*", - "php": "^7.4 || ^8.0" + "php": "^8.2" }, "require-dev": { - "chillerlan/php-authenticator": "^4.3.1 || ^5.2.1", + "chillerlan/php-authenticator": "^5.3", "ext-fileinfo": "*", - "phan/phan": "^5.4.5", - "phpcompatibility/php-compatibility": "10.x-dev", + "intervention/image": "^3.11", + "phan/phan": "^6.0.1", + "phpbench/phpbench": "^1.4", "phpmd/phpmd": "^2.15", - "phpunit/phpunit": "^9.6", - "setasign/fpdf": "^1.8.2", - "slevomat/coding-standard": "^8.15", - "squizlabs/php_codesniffer": "^3.11" + "phpstan/phpstan": "^2.1.40", + "phpstan/phpstan-deprecation-rules": "^2.0.4", + "phpunit/phpunit": "^11.5", + "setasign/fpdf": "^1.8.6", + "slevomat/coding-standard": "^8.28", + "squizlabs/php_codesniffer": "^4.0" }, "suggest": { "chillerlan/php-authenticator": "Yet another Google authenticator! Also creates URIs for mobile apps.", + "intervention/image": "More advanced GD and ImageMagick output.", "setasign/fpdf": "Required to use the QR FPDF output.", "simple-icons/simple-icons": "SVG icons that you can use to embed as logos in the QR Code" }, @@ -75,7 +79,7 @@ "homepage": "https://github.com/chillerlan/php-qrcode/graphs/contributors" } ], - "description": "A QR Code generator and reader with a user-friendly API. PHP 7.4+", + "description": "A QR Code generator and reader with a user-friendly API. PHP 8.2+", "homepage": "https://github.com/chillerlan/php-qrcode", "keywords": [ "phpqrcode", @@ -97,20 +101,20 @@ "type": "Ko-Fi" } ], - "time": "2024-11-21T16:12:34+00:00" + "time": "2026-03-18T21:21:07+00:00" }, { "name": "chillerlan/php-settings-container", - "version": "3.2.1", + "version": "3.3.0", "source": { "type": "git", "url": "https://github.com/chillerlan/php-settings-container.git", - "reference": "95ed3e9676a1d47cab2e3174d19b43f5dbf52681" + "reference": "a0a487cbf5344f721eb504bf0f59bada40c381b7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/chillerlan/php-settings-container/zipball/95ed3e9676a1d47cab2e3174d19b43f5dbf52681", - "reference": "95ed3e9676a1d47cab2e3174d19b43f5dbf52681", + "url": "https://api.github.com/repos/chillerlan/php-settings-container/zipball/a0a487cbf5344f721eb504bf0f59bada40c381b7", + "reference": "a0a487cbf5344f721eb504bf0f59bada40c381b7", "shasum": "" }, "require": { @@ -118,11 +122,13 @@ "php": "^8.1" }, "require-dev": { + "phan/phan": "^5.5.2", "phpmd/phpmd": "^2.15", - "phpstan/phpstan": "^1.11", - "phpstan/phpstan-deprecation-rules": "^1.2", + "phpstan/phpstan": "^2.1.31", + "phpstan/phpstan-deprecation-rules": "^2.0.3", "phpunit/phpunit": "^10.5", - "squizlabs/php_codesniffer": "^3.10" + "slevomat/coding-standard": "^8.22", + "squizlabs/php_codesniffer": "^4.0" }, "type": "library", "autoload": { @@ -147,7 +153,8 @@ "Settings", "configuration", "container", - "helper" + "helper", + "property hook" ], "support": { "issues": "https://github.com/chillerlan/php-settings-container/issues", @@ -163,7 +170,7 @@ "type": "ko_fi" } ], - "time": "2024-07-16T11:13:48+00:00" + "time": "2026-03-20T21:10:52+00:00" } ], "packages-dev": [ @@ -692,16 +699,16 @@ }, { "name": "amphp/process", - "version": "v2.0.3", + "version": "v2.1.0", "source": { "type": "git", "url": "https://github.com/amphp/process.git", - "reference": "52e08c09dec7511d5fbc1fb00d3e4e79fc77d58d" + "reference": "583959df17d00304ad7b0b32285373f985935643" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/process/zipball/52e08c09dec7511d5fbc1fb00d3e4e79fc77d58d", - "reference": "52e08c09dec7511d5fbc1fb00d3e4e79fc77d58d", + "url": "https://api.github.com/repos/amphp/process/zipball/583959df17d00304ad7b0b32285373f985935643", + "reference": "583959df17d00304ad7b0b32285373f985935643", "shasum": "" }, "require": { @@ -715,7 +722,7 @@ "amphp/php-cs-fixer-config": "^2", "amphp/phpunit-util": "^3", "phpunit/phpunit": "^9", - "psalm/phar": "^5.4" + "psalm/phar": "6.16.1" }, "type": "library", "autoload": { @@ -748,7 +755,7 @@ "homepage": "https://amphp.org/process", "support": { "issues": "https://github.com/amphp/process/issues", - "source": "https://github.com/amphp/process/tree/v2.0.3" + "source": "https://github.com/amphp/process/tree/v2.1.0" }, "funding": [ { @@ -756,7 +763,7 @@ "type": "github" } ], - "time": "2024-04-19T03:13:44+00:00" + "time": "2026-05-31T15:11:55+00:00" }, { "name": "amphp/serialization", @@ -986,28 +993,29 @@ }, { "name": "composer/pcre", - "version": "3.3.2", + "version": "3.4.0", "source": { "type": "git", "url": "https://github.com/composer/pcre.git", - "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e" + "reference": "d5a341b3fb61f3001970940afb1d332968a183ed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e", - "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "url": "https://api.github.com/repos/composer/pcre/zipball/d5a341b3fb61f3001970940afb1d332968a183ed", + "reference": "d5a341b3fb61f3001970940afb1d332968a183ed", "shasum": "" }, "require": { "php": "^7.4 || ^8.0" }, "conflict": { - "phpstan/phpstan": "<1.11.10" + "phpstan/phpstan": "<2.2.2" }, "require-dev": { - "phpstan/phpstan": "^1.12 || ^2", - "phpstan/phpstan-strict-rules": "^1 || ^2", - "phpunit/phpunit": "^8 || ^9" + "phpstan/phpstan": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^9" }, "type": "library", "extra": { @@ -1045,7 +1053,7 @@ ], "support": { "issues": "https://github.com/composer/pcre/issues", - "source": "https://github.com/composer/pcre/tree/3.3.2" + "source": "https://github.com/composer/pcre/tree/3.4.0" }, "funding": [ { @@ -1055,13 +1063,9 @@ { "url": "https://github.com/composer", "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" } ], - "time": "2024-11-12T16:29:46+00:00" + "time": "2026-06-07T11:47:49+00:00" }, { "name": "composer/semver", @@ -1567,16 +1571,16 @@ }, { "name": "johnpbloch/wordpress-core", - "version": "6.9.0", + "version": "6.9.4", "source": { "type": "git", "url": "https://github.com/johnpbloch/wordpress-core.git", - "reference": "4626d4e896c36ab77a69ce58627bc76243b5dd07" + "reference": "13e02e0047ca5c8ec8dc837c2de8a5bd3583b879" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/johnpbloch/wordpress-core/zipball/4626d4e896c36ab77a69ce58627bc76243b5dd07", - "reference": "4626d4e896c36ab77a69ce58627bc76243b5dd07", + "url": "https://api.github.com/repos/johnpbloch/wordpress-core/zipball/13e02e0047ca5c8ec8dc837c2de8a5bd3583b879", + "reference": "13e02e0047ca5c8ec8dc837c2de8a5bd3583b879", "shasum": "" }, "require": { @@ -1584,7 +1588,7 @@ "php": ">=7.2.24" }, "provide": { - "wordpress/core-implementation": "6.9.0" + "wordpress/core-implementation": "6.9.4" }, "type": "wordpress-core", "notification-url": "https://packagist.org/downloads/", @@ -1611,7 +1615,7 @@ "source": "https://core.trac.wordpress.org/browser", "wiki": "https://codex.wordpress.org/" }, - "time": "2025-12-02T19:10:58+00:00" + "time": "2026-03-11T15:27:36+00:00" }, { "name": "kelunik/certificate", @@ -3246,16 +3250,16 @@ }, { "name": "symfony/polyfill-mbstring", - "version": "v1.38.1", + "version": "v1.38.2", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "14c5439eec4ccff081ac14eca2dc57feb2a66d92" + "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/14c5439eec4ccff081ac14eca2dc57feb2a66d92", - "reference": "14c5439eec4ccff081ac14eca2dc57feb2a66d92", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", + "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", "shasum": "" }, "require": { @@ -3307,7 +3311,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.38.1" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.38.2" }, "funding": [ { @@ -3327,7 +3331,7 @@ "type": "tidelift" } ], - "time": "2026-05-26T12:51:13+00:00" + "time": "2026-05-27T06:59:30+00:00" }, { "name": "symfony/polyfill-php84", @@ -3786,16 +3790,16 @@ }, { "name": "webmozart/assert", - "version": "2.4.0", + "version": "2.4.1", "source": { "type": "git", "url": "https://github.com/webmozarts/assert.git", - "reference": "9007ea6f45ecf352a9422b36644e4bfc039b9155" + "reference": "2ccb7c2e821038c03a3e6e1700c570c158c55f70" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozarts/assert/zipball/9007ea6f45ecf352a9422b36644e4bfc039b9155", - "reference": "9007ea6f45ecf352a9422b36644e4bfc039b9155", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/2ccb7c2e821038c03a3e6e1700c570c158c55f70", + "reference": "2ccb7c2e821038c03a3e6e1700c570c158c55f70", "shasum": "" }, "require": { @@ -3846,9 +3850,9 @@ ], "support": { "issues": "https://github.com/webmozarts/assert/issues", - "source": "https://github.com/webmozarts/assert/tree/2.4.0" + "source": "https://github.com/webmozarts/assert/tree/2.4.1" }, - "time": "2026-05-20T13:07:01+00:00" + "time": "2026-06-15T15:31:57+00:00" }, { "name": "wp-hooks/wordpress-core", diff --git a/includes/Listeners/RsvEmailListener.php b/includes/Listeners/RsvEmailListener.php index 7ec7d2c..958fb34 100644 --- a/includes/Listeners/RsvEmailListener.php +++ b/includes/Listeners/RsvEmailListener.php @@ -180,11 +180,18 @@ class RsvEmailListener { global $rsv_template_registry; $engine = new RsvTemplateEngine(registry: $rsv_template_registry); + $form_definition = new RsvFormDefinition((string) $form_submit['form_id'], $definition['definition']); + $form_data = new RsvFormData($form_values); + + // Calculated values (e.g. price) win over submitted fields so they can't be shadowed. + $symbols = array_merge($form_values, (new RsvFormCalculatedValues())->for($form_definition, $form_data)); + // Subject is plain text: render without HTML-escaping, then strip tags/newlines. - $subject = sanitize_text_field($engine->render_plain($subject_tpl, $form_values)); + $subject = sanitize_text_field($engine->render_plain($subject_tpl, $symbols)); // Body is HTML: the engine HTML-escapes all interpolated values. - $body = $engine->render($body_tpl, $form_values); + $body = $engine->render($body_tpl, $symbols); + error_log("Prc"); (new RsvEmailSender())->send($user_email, $subject, $body); } catch (\Throwable $e) { diff --git a/includes/Listeners/RsvGoogleCalendarListener.php b/includes/Listeners/RsvGoogleCalendarListener.php index d5bd5ef..5067c17 100644 --- a/includes/Listeners/RsvGoogleCalendarListener.php +++ b/includes/Listeners/RsvGoogleCalendarListener.php @@ -15,7 +15,7 @@ class RsvGoogleCalendarListener { return; } - $calendar_id = self::resolve_calendar_id($gcal, $event->timetable_id); + $calendar_id = self::resolve_calendar_id($gcal, $event->reservation->timetable_id); if (!$calendar_id) { return; } @@ -24,8 +24,8 @@ class RsvGoogleCalendarListener { $gcal->add_event( $calendar_id, "Reservation #{$event->reservation->id}", - $event->reservation->start, - $event->reservation->end, + $event->reservation->start_utc, + $event->reservation->end_utc, $event->reservation->user_email, $event->reservation->id, $status, diff --git a/includes/Repository/RsvTimetableRepository.php b/includes/Repository/RsvTimetableRepository.php index 9f72ea7..054745f 100644 --- a/includes/Repository/RsvTimetableRepository.php +++ b/includes/Repository/RsvTimetableRepository.php @@ -50,6 +50,7 @@ class RsvTimetableRepository { 'name' => $timetable->name, 'block_size' => $timetable->block_size, 'maintainer_email' => $timetable->maintainer_email, + 'google_calendar_id' => $timetable->google_calendar_id, ], ['id' => $id] ); diff --git a/includes/Services/Forms/Pricing/RsvFormPriceCalculator.php b/includes/Services/Forms/Pricing/RsvFormPriceCalculator.php new file mode 100644 index 0000000..f406a6b --- /dev/null +++ b/includes/Services/Forms/Pricing/RsvFormPriceCalculator.php @@ -0,0 +1,21 @@ +getElements() as $element) { + $calculator = $rsv_form_price_registry->get($element->getType()); + if ($calculator === null) { + continue; // Unpriced element type contributes nothing. + } + + $total += (float) $calculator($element, $data->getValue($element->getName())); + } + + return $total; + } +} diff --git a/includes/Services/Forms/Pricing/RsvFormPriceCalculatorRegistry.php b/includes/Services/Forms/Pricing/RsvFormPriceCalculatorRegistry.php new file mode 100644 index 0000000..549319d --- /dev/null +++ b/includes/Services/Forms/Pricing/RsvFormPriceCalculatorRegistry.php @@ -0,0 +1,21 @@ + */ + private array $calculators = []; + + public function register(string $type, callable $calculator): void { + $this->calculators[$type] = $calculator; + } + + public function get(string $type): ?callable { + return $this->calculators[$type] ?? null; + } + + /** Builds the registry and lets other modules contribute calculators. */ + public static function boot(): self { + $registry = new self(); + do_action('rsv-register-price-calculator', $registry); + return $registry; + } +} diff --git a/includes/Services/Forms/RsvFormCalculatedValues.php b/includes/Services/Forms/RsvFormCalculatedValues.php new file mode 100644 index 0000000..6c2d6ab --- /dev/null +++ b/includes/Services/Forms/RsvFormCalculatedValues.php @@ -0,0 +1,19 @@ + */ + public function for(RsvFormDefinition $definition, RsvFormData $data): array { + return [ + 'price' => (new RsvFormPriceCalculator())->calculate($definition, $data), + ]; + } + + /** + * The names these values expose, so template validation accepts {{ price }}. + * @return list + */ + public static function names(): array { + return ['price']; + } +} diff --git a/includes/Services/Forms/RsvFormDefinitionValidator.php b/includes/Services/Forms/RsvFormDefinitionValidator.php index 61ce50b..1e4262a 100644 --- a/includes/Services/Forms/RsvFormDefinitionValidator.php +++ b/includes/Services/Forms/RsvFormDefinitionValidator.php @@ -44,7 +44,7 @@ final class RsvFormDefinitionValidator { * @return list */ private function symbols(array $elements): array { - $names = []; + $names = RsvFormCalculatedValues::names(); // calculated values are referencable too foreach ($elements as $el) { $name = is_array($el) ? ($el['name'] ?? '') : ''; if (is_string($name) && $name !== '') { diff --git a/modules/Templating/Elements/RsvQrPaymentElement.php b/modules/Templating/Elements/RsvQrPaymentElement.php new file mode 100644 index 0000000..ae3be6c --- /dev/null +++ b/modules/Templating/Elements/RsvQrPaymentElement.php @@ -0,0 +1,90 @@ +get('account', '')); + if ($account === '') { + return ''; // No payee account configured — nothing to render. + } + + $payload = $this->spayd( + $account, + (float) $symbols->get('price', 0), + (string) $symbols->get('currency', 'CZK'), + (string) $symbols->get('message', ''), + (string) $symbols->get('variable_symbol', '') + ); + + $src = $this->image_url($payload); + if ($src === '') { + return ''; // Image could not be written. + } + + return '' . esc_attr__('QR platba', 'reservair')
+            . ''; + } + + public function symbols(): array { + return ['account', 'currency', 'message', 'variable_symbol']; + } + + /** Builds a SPAYD string (Czech QR payment). '*' is the field delimiter. */ + private function spayd(string $account, float $price, string $currency, string $message, string $vs): string { + $parts = [ + 'SPD*1.0', + 'ACC:' . $account, + 'AM:' . number_format($price, 2, '.', ''), + 'CC:' . $currency, + ]; + if ($message !== '') { + $parts[] = 'MSG:' . str_replace('*', ' ', $message); + } + if ($vs !== '') { + $parts[] = 'X-VS:' . preg_replace('/\D/', '', $vs); + } + return implode('*', $parts); + } + + /** + * Writes the QR PNG under uploads (deduped by payload hash) and returns its + * public URL, or '' if it could not be written. + */ + private function image_url(string $payload): string { + $uploads = wp_upload_dir(); + $dir = $uploads['basedir'] . '/reservair-qr'; + $name = hash('sha256', $payload) . '.png'; + $path = $dir . '/' . $name; + + if (!file_exists($path)) { + wp_mkdir_p($dir); + if (file_put_contents($path, $this->png($payload)) === false) { + return ''; + } + } + + return $uploads['baseurl'] . '/reservair-qr/' . $name; + } + + private function png(string $payload): string { + $options = new QROptions(); + $options->version = 7; + $options->outputInterface = QRGdImagePNG::class; + $options->scale = 4; + $options->outputBase64 = false; + $options->bgColor = [200, 150, 200]; + + return (string) (new QRCode($options))->render($payload); + } +} diff --git a/reservair.php b/reservair.php index 4a3ed55..b954339 100644 --- a/reservair.php +++ b/reservair.php @@ -3,6 +3,7 @@ use Reservair\Templating\Elements\RsvReservationSummaryElement; use Reservair\Templating\Elements\RsvReservationActionsElement; use Reservair\Templating\Elements\RsvResetFormButtonElement; +use Reservair\Templating\Elements\RsvQrPaymentElement; /** * Plugin Name: Reservair * Description: A reservation and booking system for WordPress. Site visitors browse available time slots and submit reservation requests via a Gutenberg block; administrators manage timetables, services, forms, and reservations from the WordPress admin panel. @@ -33,7 +34,7 @@ register_activation_hook( __FILE__, [ 'RsvInstaller', 'install' ] ); * plugins we might interact with) is fully loaded. */ function rsv_bootstrap(): void { - global $rsv_form_registry, $rsv_template_registry; + global $rsv_form_registry, $rsv_template_registry, $rsv_form_price_registry; // Re-grant the custom capability after a plugin *update* (the activation hook // only runs on activate). No-op once the stored version matches. @@ -53,11 +54,26 @@ function rsv_bootstrap(): void { $rsv_form_registry->register( 'output-reservation-summary', new RsvReservationSummaryElementHandler() ); $rsv_form_registry->register( 'output-text', new RsvOutputTextElementHandler() ); + // Price calculators — extensions add per-element calculators via the action. + add_action( 'rsv-register-price-calculator', function ( RsvFormPriceCalculatorRegistry $reg ): void { + $reg->register( 'reservation', function ( RsvFormElementDefinition $def, $value ): float { + if ( ! is_array( $value ) || ! is_array( $value['timetable_reservations'] ?? null ) ) { + return 0.0; + } + + $price_per_block = (float) $def->getAttr( 'price_per_block', 0 ); + + return $price_per_block * count( $value['timetable_reservations'] ); + } ); + } ); + $rsv_form_price_registry = RsvFormPriceCalculatorRegistry::boot(); + // Template custom-element registry. Extensions register via the action. add_action( 'rsv-template-register-custom-elements', function ( \Reservair\Templating\RsvTemplateRegistry $reg ): void { $reg->register( 'reservation-summary', new RsvReservationSummaryElement() ); $reg->register( 'reservation-actions', new RsvReservationActionsElement() ); $reg->register( 'reset-form-button', new RsvResetFormButtonElement() ); + $reg->register( 'qr-payment', new RsvQrPaymentElement() ); } ); $rsv_template_registry = new \Reservair\Templating\RsvTemplateRegistry(); do_action( 'rsv-template-register-custom-elements', $rsv_template_registry );