diff --git a/composer.json b/composer.json index e2945f1..ddbb30b 100644 --- a/composer.json +++ b/composer.json @@ -1,55 +1,52 @@ { - "name": "a8cteam51/simple-events", - "type": "wordpress-plugin", + "name": "a8cteam51/simple-events", + "type": "wordpress-plugin", + "description": "A simple Gutenberg-first event management plugin that integrates with WooCommerce Box Office.", + "homepage": "https://github.com/a8cteam51/simple-events", + "license": "GPL-2.0-or-later", + "authors": [{ + "name": "WordPress.com Special Projects Team", + "homepage": "https://wpspecialprojects.wordpress.com" + }, + { + "name": "Contributors", + "homepage": "https://github.com/a8cteam51/simple-events/graphs/contributors" + } + ], - "description": "A simple Gutenberg-first event management plugin that integrates with WooCommerce Box Office.", - "homepage": "https://github.com/a8cteam51/simple-events", - "license": "GPL-2.0-or-later", - "authors": [ - { - "name": "WordPress.com Special Projects Team", - "homepage": "https://wpspecialprojects.wordpress.com" + "repositories": [{ + "type": "vcs", + "url": "https://github.com/a8cteam51/team51-configs" + }], + "require": { + "eluceo/ical": "^2.14.0" }, - { - "name": "Contributors", - "homepage": "https://github.com/a8cteam51/simple-events/graphs/contributors" - } - ], - - "repositories": [ - { - "type": "vcs", - "url": "https://github.com/a8cteam51/team51-configs" - } - ], - "require": { - "eluceo/ical": "^0.16.0" - }, - "require-dev": { - "a8cteam51/team51-configs": "dev-trunk", + "require-dev": { + "a8cteam51/team51-configs": "dev-trunk", - "wp-coding-standards/wpcs": "^3.0", - "phpcompatibility/phpcompatibility-wp": "*", + "wp-coding-standards/wpcs": "^3.0", + "phpcompatibility/phpcompatibility-wp": "*", - "roave/security-advisories": "dev-latest" - }, + "roave/security-advisories": "dev-latest" + }, - "scripts": { - "generate-autoloader": "@composer dump-autoload -o", + "scripts": { + "generate-autoloader": "@composer dump-autoload -o", - "format:php": "phpcbf --basepath=. . -v", - "lint:php": "phpcs --basepath=. . -v", + "format:php": "phpcbf --basepath=. . -v", + "lint:php": "phpcs --basepath=. . -v", - "packages-install": "@composer install --ignore-platform-reqs --no-interaction", - "packages-update": [ - "@composer clear-cache", - "@composer update --prefer-stable --no-interaction" - ] - }, - "config": { - "allow-plugins": { - "composer/*": true, - "dealerdirect/phpcodesniffer-composer-installer": true + "packages-install": "@composer install --ignore-platform-reqs --no-interaction", + "packages-update": [ + "@composer clear-cache", + "@composer update --prefer-stable --no-interaction" + ] + }, + "config": { + "allow-plugins": { + "composer/*": true, + "dealerdirect/phpcodesniffer-composer-installer": true, + "phpstan/extension-installer": true + } } - } -} +} \ No newline at end of file diff --git a/composer.lock b/composer.lock index 3e68f59..e840cf1 100644 --- a/composer.lock +++ b/composer.lock @@ -4,30 +4,37 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "dc9939c4ef3864569f02d4d374da0639", + "content-hash": "c58913b7b5a16b9d665efe3497cf7b30", "packages": [ { "name": "eluceo/ical", - "version": "0.16.1", + "version": "2.14.0", "source": { "type": "git", "url": "https://github.com/markuspoerschke/iCal.git", - "reference": "7043337feaeacbc016844e7e52ef41bba504ad8f" + "reference": "3123533f7ff0af015da1d788476204f936d18135" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/markuspoerschke/iCal/zipball/7043337feaeacbc016844e7e52ef41bba504ad8f", - "reference": "7043337feaeacbc016844e7e52ef41bba504ad8f", + "url": "https://api.github.com/repos/markuspoerschke/iCal/zipball/3123533f7ff0af015da1d788476204f936d18135", + "reference": "3123533f7ff0af015da1d788476204f936d18135", "shasum": "" }, "require": { - "php": ">=7.1 || ~8.0.0" + "ext-mbstring": "*", + "php": ">=7.4 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0", + "symfony/deprecation-contracts": "^2.1 || ^3.0" }, - "require-dev": { - "phpunit/phpunit": "^7.0" + "conflict": { + "php": "7.4.6" }, - "suggest": { - "ext-mbstring": "Massive performance enhancement of line folding" + "require-dev": { + "ergebnis/composer-normalize": "^2.23.1", + "friendsofphp/php-cs-fixer": "^3.4", + "infection/infection": "^0.23 || ^0.26 || ^0.27", + "phpmd/phpmd": "^2.13", + "phpunit/phpunit": "^9.5", + "vimeo/psalm": "^4.8 || ^5.0" }, "type": "library", "autoload": { @@ -42,11 +49,11 @@ "authors": [ { "name": "Markus Poerschke", - "email": "markus@eluceo.de", + "email": "markus@poerschke.nrw", "role": "Developer" } ], - "description": "The eluceo/iCal package offers a abstraction layer for creating iCalendars. You can easily create iCal files by using PHP object instead of typing your *.ics file by hand. The output will follow RFC 5545 as best as possible.", + "description": "The eluceo/iCal package offers an abstraction layer for creating iCalendars. You can easily create iCal files by using PHP objects instead of typing your *.ics file by hand. The output will follow RFC 5545 as best as possible.", "homepage": "https://github.com/markuspoerschke/iCal", "keywords": [ "calendar", @@ -56,10 +63,79 @@ "php calendar" ], "support": { + "docs": "https://ical.poerschke.nrw", + "forum": "https://github.com/markuspoerschke/iCal/discussions", "issues": "https://github.com/markuspoerschke/iCal/issues", "source": "https://github.com/markuspoerschke/iCal" }, - "time": "2020-10-04T17:41:11+00:00" + "time": "2024-07-11T22:33:13+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" } ], "packages-dev": [ @@ -69,23 +145,31 @@ "source": { "type": "git", "url": "https://github.com/a8cteam51/team51-configs.git", - "reference": "e4737f2cba9a57d72a32cbf46bca54b69555e3a5" + "reference": "1f025c367e0287886d1f7490618950131fa8d491" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/a8cteam51/team51-configs/zipball/e4737f2cba9a57d72a32cbf46bca54b69555e3a5", - "reference": "e4737f2cba9a57d72a32cbf46bca54b69555e3a5", + "url": "https://api.github.com/repos/a8cteam51/team51-configs/zipball/1f025c367e0287886d1f7490618950131fa8d491", + "reference": "1f025c367e0287886d1f7490618950131fa8d491", "shasum": "" }, "require": { "ext-json": "*", - "php": ">=7.4" - }, - "require-dev": { - "composer/composer": "^2.6", + "johnbillion/wp-compat": "^1", + "php": ">=8.3", "phpcompatibility/phpcompatibility-wp": "*", + "phpmd/phpmd": "^2", + "phpstan/extension-installer": "^1", + "phpstan/phpstan": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-strict-rules": "^2", + "swissspidy/phpstan-no-private": "^1", + "szepeviktor/phpstan-wordpress": "^2", "wp-coding-standards/wpcs": "^3" }, + "require-dev": { + "composer/composer": "^2" + }, "default-branch": true, "type": "library", "autoload": { @@ -94,12 +178,15 @@ ] }, "scripts": { - "composer:install": [ + "generate-autoloader": [ + "@composer dump-autoload --ignore-platform-reqs -o" + ], + "packages-install": [ "@composer install --ignore-platform-reqs --no-interaction" ], - "composer:update": [ + "packages-update": [ "@composer clear-cache", - "@composer update --prefer-stable --no-interaction" + "@composer update --prefer-stable --ignore-platform-reqs --no-interaction" ] }, "license": [ @@ -116,32 +203,177 @@ "source": "https://github.com/a8cteam51/team51-configs/tree/trunk", "issues": "https://github.com/a8cteam51/team51-configs/issues" }, - "time": "2023-10-10T15:57:04+00:00" + "time": "2025-05-25T15:59:20+00:00" + }, + { + "name": "composer/pcre", + "version": "3.3.2", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<1.11.10" + }, + "require-dev": { + "phpstan/phpstan": "^1.12 || ^2", + "phpstan/phpstan-strict-rules": "^1 || ^2", + "phpunit/phpunit": "^8 || ^9" + }, + "type": "library", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Pcre\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", + "regex", + "regular expression" + ], + "support": { + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.3.2" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "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" + }, + { + "name": "composer/xdebug-handler", + "version": "3.0.5", + "source": { + "type": "git", + "url": "https://github.com/composer/xdebug-handler.git", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6c1925561632e83d60a44492e0b344cf48ab85ef", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef", + "shasum": "" + }, + "require": { + "composer/pcre": "^1 || ^2 || ^3", + "php": "^7.2.5 || ^8.0", + "psr/log": "^1 || ^2 || ^3" + }, + "require-dev": { + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-strict-rules": "^1.1", + "phpunit/phpunit": "^8.5 || ^9.6 || ^10.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Composer\\XdebugHandler\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "John Stevenson", + "email": "john-stevenson@blueyonder.co.uk" + } + ], + "description": "Restarts a process without Xdebug.", + "keywords": [ + "Xdebug", + "performance" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/xdebug-handler/issues", + "source": "https://github.com/composer/xdebug-handler/tree/3.0.5" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-05-06T16:37:16+00:00" }, { "name": "dealerdirect/phpcodesniffer-composer-installer", - "version": "v1.0.0", + "version": "v1.1.1", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/composer-installer.git", - "reference": "4be43904336affa5c2f70744a348312336afd0da" + "reference": "6e0fa428497bf560152ee73ffbb8af5c6a56b0dd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/4be43904336affa5c2f70744a348312336afd0da", - "reference": "4be43904336affa5c2f70744a348312336afd0da", + "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/6e0fa428497bf560152ee73ffbb8af5c6a56b0dd", + "reference": "6e0fa428497bf560152ee73ffbb8af5c6a56b0dd", "shasum": "" }, "require": { - "composer-plugin-api": "^1.0 || ^2.0", + "composer-plugin-api": "^2.2", "php": ">=5.4", "squizlabs/php_codesniffer": "^2.0 || ^3.1.0 || ^4.0" }, "require-dev": { - "composer/composer": "*", + "composer/composer": "^2.2", "ext-json": "*", "ext-zip": "*", - "php-parallel-lint/php-parallel-lint": "^1.3.1", + "php-parallel-lint/php-parallel-lint": "^1.4.0", "phpcompatibility/php-compatibility": "^9.0", "yoast/phpunit-polyfills": "^1.0" }, @@ -161,9 +393,9 @@ "authors": [ { "name": "Franck Nijhof", - "email": "franck.nijhof@dealerdirect.com", - "homepage": "http://www.frenck.nl", - "role": "Developer / IT Manager" + "email": "opensource@frenck.dev", + "homepage": "https://frenck.dev", + "role": "Open source developer" }, { "name": "Contributors", @@ -171,7 +403,6 @@ } ], "description": "PHP_CodeSniffer Standards Composer Installer Plugin", - "homepage": "http://www.dealerdirect.com", "keywords": [ "PHPCodeSniffer", "PHP_CodeSniffer", @@ -192,152 +423,379 @@ ], "support": { "issues": "https://github.com/PHPCSStandards/composer-installer/issues", + "security": "https://github.com/PHPCSStandards/composer-installer/security/policy", "source": "https://github.com/PHPCSStandards/composer-installer" }, - "time": "2023-01-05T11:28:13+00:00" + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" + } + ], + "time": "2025-06-27T17:24:01+00:00" }, { - "name": "phpcompatibility/php-compatibility", - "version": "9.3.5", + "name": "johnbillion/wp-compat", + "version": "1.3.0", "source": { "type": "git", - "url": "https://github.com/PHPCompatibility/PHPCompatibility.git", - "reference": "9fb324479acf6f39452e0655d2429cc0d3914243" + "url": "https://github.com/johnbillion/wp-compat.git", + "reference": "ea44e487191b39b2beea0e8e4ee4e1f28376e0eb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibility/zipball/9fb324479acf6f39452e0655d2429cc0d3914243", - "reference": "9fb324479acf6f39452e0655d2429cc0d3914243", + "url": "https://api.github.com/repos/johnbillion/wp-compat/zipball/ea44e487191b39b2beea0e8e4ee4e1f28376e0eb", + "reference": "ea44e487191b39b2beea0e8e4ee4e1f28376e0eb", "shasum": "" }, "require": { - "php": ">=5.3", - "squizlabs/php_codesniffer": "^2.3 || ^3.0.2" - }, - "conflict": { - "squizlabs/php_codesniffer": "2.6.2" + "php": ">= 7.4", + "phpstan/phpstan": "^2.0", + "wp-hooks/wordpress-core": "^1.10" }, "require-dev": { - "phpunit/phpunit": "~4.5 || ^5.0 || ^6.0 || ^7.0" + "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", + "johnbillion/plugin-infrastructure": "dev-trunk", + "nikic/php-parser": "^5.1", + "php-stubs/wordpress-stubs": "^6.6", + "phpstan/phpstan-deprecation-rules": "2.0.0", + "phpstan/phpstan-phpunit": "2.0.1", + "phpstan/phpstan-strict-rules": "2.0.0", + "phpunit/phpunit": "^9.0", + "roots/wordpress-core-installer": "1.100.0", + "roots/wordpress-full": "*", + "wp-coding-standards/wpcs": "3.1.0" }, "suggest": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.5 || This Composer plugin will sort out the PHPCS 'installed_paths' automatically.", - "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues." + "phpstan/phpstan-deprecation-rules": "PHPStan rules for detecting usage of deprecated symbols", + "swissspidy/phpstan-no-private": "PHPStan rules for detecting usage of pseudo-private functions, classes, and methods", + "szepeviktor/phpstan-wordpress": "WordPress extensions for PHPStan" + }, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "wordpress-install-dir": "vendor/wordpress/wordpress" + }, + "autoload": { + "psr-4": { + "WPCompat\\PHPStan\\": "src/" + } }, - "type": "phpcodesniffer-standard", "notification-url": "https://packagist.org/downloads/", "license": [ - "LGPL-3.0-or-later" + "MIT" ], "authors": [ { - "name": "Wim Godden", - "homepage": "https://github.com/wimg", - "role": "lead" - }, - { - "name": "Juliette Reinders Folmer", - "homepage": "https://github.com/jrfnl", - "role": "lead" - }, - { - "name": "Contributors", - "homepage": "https://github.com/PHPCompatibility/PHPCompatibility/graphs/contributors" + "name": "John Blackbourn", + "homepage": "https://johnblackbourn.com/" } ], - "description": "A set of sniffs for PHP_CodeSniffer that checks for PHP cross-version compatibility.", - "homepage": "http://techblog.wimgodden.be/tag/codesniffer/", + "description": "PHPStan extension to help verify that your PHP code is compatible with a given version of WordPress", "keywords": [ - "compatibility", - "phpcs", - "standards" + "PHPStan", + "wordpress" ], "support": { - "issues": "https://github.com/PHPCompatibility/PHPCompatibility/issues", - "source": "https://github.com/PHPCompatibility/PHPCompatibility" + "issues": "https://github.com/johnbillion/wp-compat/issues", + "source": "https://github.com/johnbillion/wp-compat" }, - "time": "2019-12-27T09:44:58+00:00" + "funding": [ + { + "url": "https://github.com/sponsors/johnbillion", + "type": "github" + } + ], + "time": "2025-07-03T15:08:02+00:00" }, { - "name": "phpcompatibility/phpcompatibility-paragonie", - "version": "1.3.2", + "name": "pdepend/pdepend", + "version": "2.16.2", "source": { "type": "git", - "url": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie.git", - "reference": "bba5a9dfec7fcfbd679cfaf611d86b4d3759da26" + "url": "https://github.com/pdepend/pdepend.git", + "reference": "f942b208dc2a0868454d01b29f0c75bbcfc6ed58" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityParagonie/zipball/bba5a9dfec7fcfbd679cfaf611d86b4d3759da26", - "reference": "bba5a9dfec7fcfbd679cfaf611d86b4d3759da26", + "url": "https://api.github.com/repos/pdepend/pdepend/zipball/f942b208dc2a0868454d01b29f0c75bbcfc6ed58", + "reference": "f942b208dc2a0868454d01b29f0c75bbcfc6ed58", "shasum": "" }, "require": { - "phpcompatibility/php-compatibility": "^9.0" + "php": ">=5.3.7", + "symfony/config": "^2.3.0|^3|^4|^5|^6.0|^7.0", + "symfony/dependency-injection": "^2.3.0|^3|^4|^5|^6.0|^7.0", + "symfony/filesystem": "^2.3.0|^3|^4|^5|^6.0|^7.0", + "symfony/polyfill-mbstring": "^1.19" }, "require-dev": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.7", - "paragonie/random_compat": "dev-master", - "paragonie/sodium_compat": "dev-master" + "easy-doc/easy-doc": "0.0.0|^1.2.3", + "gregwar/rst": "^1.0", + "squizlabs/php_codesniffer": "^2.0.0" }, - "suggest": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.7 || This Composer plugin will sort out the PHP_CodeSniffer 'installed_paths' automatically.", - "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues." + "bin": [ + "src/bin/pdepend" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "PDepend\\": "src/main/php/PDepend" + } }, - "type": "phpcodesniffer-standard", "notification-url": "https://packagist.org/downloads/", "license": [ - "LGPL-3.0-or-later" - ], - "authors": [ - { - "name": "Wim Godden", - "role": "lead" - }, - { - "name": "Juliette Reinders Folmer", - "role": "lead" - } + "BSD-3-Clause" ], - "description": "A set of rulesets for PHP_CodeSniffer to check for PHP cross-version compatibility issues in projects, while accounting for polyfills provided by the Paragonie polyfill libraries.", - "homepage": "http://phpcompatibility.com/", + "description": "Official version of pdepend to be handled with Composer", "keywords": [ - "compatibility", - "paragonie", - "phpcs", - "polyfill", - "standards", - "static analysis" + "PHP Depend", + "PHP_Depend", + "dev", + "pdepend" ], "support": { - "issues": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie/issues", - "source": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie" + "issues": "https://github.com/pdepend/pdepend/issues", + "source": "https://github.com/pdepend/pdepend/tree/2.16.2" }, - "time": "2022-10-25T01:46:02+00:00" + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/pdepend/pdepend", + "type": "tidelift" + } + ], + "time": "2023-12-17T18:09:59+00:00" }, { - "name": "phpcompatibility/phpcompatibility-wp", - "version": "2.1.4", + "name": "php-stubs/wordpress-stubs", + "version": "v6.8.1", "source": { "type": "git", - "url": "https://github.com/PHPCompatibility/PHPCompatibilityWP.git", - "reference": "b6c1e3ee1c35de6c41a511d5eb9bd03e447480a5" + "url": "https://github.com/php-stubs/wordpress-stubs.git", + "reference": "92e444847d94f7c30f88c60004648f507688acd5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityWP/zipball/b6c1e3ee1c35de6c41a511d5eb9bd03e447480a5", - "reference": "b6c1e3ee1c35de6c41a511d5eb9bd03e447480a5", + "url": "https://api.github.com/repos/php-stubs/wordpress-stubs/zipball/92e444847d94f7c30f88c60004648f507688acd5", + "reference": "92e444847d94f7c30f88c60004648f507688acd5", "shasum": "" }, - "require": { + "conflict": { + "phpdocumentor/reflection-docblock": "5.6.1" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "nikic/php-parser": "^5.4", + "php": "^7.4 || ^8.0", + "php-stubs/generator": "^0.8.3", + "phpdocumentor/reflection-docblock": "^5.4.1", + "phpstan/phpstan": "^2.1", + "phpunit/phpunit": "^9.5", + "szepeviktor/phpcs-psr-12-neutron-hybrid-ruleset": "^1.1.1", + "wp-coding-standards/wpcs": "3.1.0 as 2.3.0" + }, + "suggest": { + "paragonie/sodium_compat": "Pure PHP implementation of libsodium", + "symfony/polyfill-php80": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "szepeviktor/phpstan-wordpress": "WordPress extensions for PHPStan" + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "WordPress function and class declaration stubs for static analysis.", + "homepage": "https://github.com/php-stubs/wordpress-stubs", + "keywords": [ + "PHPStan", + "static analysis", + "wordpress" + ], + "support": { + "issues": "https://github.com/php-stubs/wordpress-stubs/issues", + "source": "https://github.com/php-stubs/wordpress-stubs/tree/v6.8.1" + }, + "time": "2025-05-02T12:33:34+00:00" + }, + { + "name": "phpcompatibility/php-compatibility", + "version": "9.3.5", + "source": { + "type": "git", + "url": "https://github.com/PHPCompatibility/PHPCompatibility.git", + "reference": "9fb324479acf6f39452e0655d2429cc0d3914243" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibility/zipball/9fb324479acf6f39452e0655d2429cc0d3914243", + "reference": "9fb324479acf6f39452e0655d2429cc0d3914243", + "shasum": "" + }, + "require": { + "php": ">=5.3", + "squizlabs/php_codesniffer": "^2.3 || ^3.0.2" + }, + "conflict": { + "squizlabs/php_codesniffer": "2.6.2" + }, + "require-dev": { + "phpunit/phpunit": "~4.5 || ^5.0 || ^6.0 || ^7.0" + }, + "suggest": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.5 || This Composer plugin will sort out the PHPCS 'installed_paths' automatically.", + "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues." + }, + "type": "phpcodesniffer-standard", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Wim Godden", + "homepage": "https://github.com/wimg", + "role": "lead" + }, + { + "name": "Juliette Reinders Folmer", + "homepage": "https://github.com/jrfnl", + "role": "lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCompatibility/PHPCompatibility/graphs/contributors" + } + ], + "description": "A set of sniffs for PHP_CodeSniffer that checks for PHP cross-version compatibility.", + "homepage": "http://techblog.wimgodden.be/tag/codesniffer/", + "keywords": [ + "compatibility", + "phpcs", + "standards" + ], + "support": { + "issues": "https://github.com/PHPCompatibility/PHPCompatibility/issues", + "source": "https://github.com/PHPCompatibility/PHPCompatibility" + }, + "time": "2019-12-27T09:44:58+00:00" + }, + { + "name": "phpcompatibility/phpcompatibility-paragonie", + "version": "1.3.3", + "source": { + "type": "git", + "url": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie.git", + "reference": "293975b465e0e709b571cbf0c957c6c0a7b9a2ac" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityParagonie/zipball/293975b465e0e709b571cbf0c957c6c0a7b9a2ac", + "reference": "293975b465e0e709b571cbf0c957c6c0a7b9a2ac", + "shasum": "" + }, + "require": { + "phpcompatibility/php-compatibility": "^9.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "paragonie/random_compat": "dev-master", + "paragonie/sodium_compat": "dev-master" + }, + "suggest": { + "dealerdirect/phpcodesniffer-composer-installer": "^1.0 || This Composer plugin will sort out the PHP_CodeSniffer 'installed_paths' automatically.", + "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues." + }, + "type": "phpcodesniffer-standard", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Wim Godden", + "role": "lead" + }, + { + "name": "Juliette Reinders Folmer", + "role": "lead" + } + ], + "description": "A set of rulesets for PHP_CodeSniffer to check for PHP cross-version compatibility issues in projects, while accounting for polyfills provided by the Paragonie polyfill libraries.", + "homepage": "http://phpcompatibility.com/", + "keywords": [ + "compatibility", + "paragonie", + "phpcs", + "polyfill", + "standards", + "static analysis" + ], + "support": { + "issues": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie/issues", + "security": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie/security/policy", + "source": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie" + }, + "funding": [ + { + "url": "https://github.com/PHPCompatibility", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + } + ], + "time": "2024-04-24T21:30:46+00:00" + }, + { + "name": "phpcompatibility/phpcompatibility-wp", + "version": "2.1.7", + "source": { + "type": "git", + "url": "https://github.com/PHPCompatibility/PHPCompatibilityWP.git", + "reference": "5bfbbfbabb3df2b9a83e601de9153e4a7111962c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityWP/zipball/5bfbbfbabb3df2b9a83e601de9153e4a7111962c", + "reference": "5bfbbfbabb3df2b9a83e601de9153e4a7111962c", + "shasum": "" + }, + "require": { "phpcompatibility/php-compatibility": "^9.0", - "phpcompatibility/phpcompatibility-paragonie": "^1.0" + "phpcompatibility/phpcompatibility-paragonie": "^1.0", + "squizlabs/php_codesniffer": "^3.3" }, "require-dev": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.7" + "dealerdirect/phpcodesniffer-composer-installer": "^1.0" }, "suggest": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.7 || This Composer plugin will sort out the PHP_CodeSniffer 'installed_paths' automatically.", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0 || This Composer plugin will sort out the PHP_CodeSniffer 'installed_paths' automatically.", "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues." }, "type": "phpcodesniffer-standard", @@ -366,35 +824,54 @@ ], "support": { "issues": "https://github.com/PHPCompatibility/PHPCompatibilityWP/issues", + "security": "https://github.com/PHPCompatibility/PHPCompatibilityWP/security/policy", "source": "https://github.com/PHPCompatibility/PHPCompatibilityWP" }, - "time": "2022-10-24T09:00:36+00:00" + "funding": [ + { + "url": "https://github.com/PHPCompatibility", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcompatibility", + "type": "thanks_dev" + } + ], + "time": "2025-05-12T16:38:37+00:00" }, { "name": "phpcsstandards/phpcsextra", - "version": "1.2.1", + "version": "1.4.0", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHPCSExtra.git", - "reference": "11d387c6642b6e4acaf0bd9bf5203b8cca1ec489" + "reference": "fa4b8d051e278072928e32d817456a7fdb57b6ca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHPCSExtra/zipball/11d387c6642b6e4acaf0bd9bf5203b8cca1ec489", - "reference": "11d387c6642b6e4acaf0bd9bf5203b8cca1ec489", + "url": "https://api.github.com/repos/PHPCSStandards/PHPCSExtra/zipball/fa4b8d051e278072928e32d817456a7fdb57b6ca", + "reference": "fa4b8d051e278072928e32d817456a7fdb57b6ca", "shasum": "" }, "require": { "php": ">=5.4", - "phpcsstandards/phpcsutils": "^1.0.9", - "squizlabs/php_codesniffer": "^3.8.0" + "phpcsstandards/phpcsutils": "^1.1.0", + "squizlabs/php_codesniffer": "^3.13.0 || ^4.0" }, "require-dev": { "php-parallel-lint/php-console-highlighter": "^1.0", - "php-parallel-lint/php-parallel-lint": "^1.3.2", + "php-parallel-lint/php-parallel-lint": "^1.4.0", "phpcsstandards/phpcsdevcs": "^1.1.6", "phpcsstandards/phpcsdevtools": "^1.2.1", - "phpunit/phpunit": "^4.5 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.0" + "phpunit/phpunit": "^4.5 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4" }, "type": "phpcodesniffer-standard", "extra": { @@ -444,35 +921,39 @@ { "url": "https://opencollective.com/php_codesniffer", "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" } ], - "time": "2023-12-08T16:49:07+00:00" + "time": "2025-06-14T07:40:39+00:00" }, { "name": "phpcsstandards/phpcsutils", - "version": "1.0.9", + "version": "1.1.0", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHPCSUtils.git", - "reference": "908247bc65010c7b7541a9551e002db12e9dae70" + "reference": "65355670ac17c34cd235cf9d3ceae1b9252c4dad" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/908247bc65010c7b7541a9551e002db12e9dae70", - "reference": "908247bc65010c7b7541a9551e002db12e9dae70", + "url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/65355670ac17c34cd235cf9d3ceae1b9252c4dad", + "reference": "65355670ac17c34cd235cf9d3ceae1b9252c4dad", "shasum": "" }, "require": { "dealerdirect/phpcodesniffer-composer-installer": "^0.4.1 || ^0.5 || ^0.6.2 || ^0.7 || ^1.0", "php": ">=5.4", - "squizlabs/php_codesniffer": "^3.8.0 || 4.0.x-dev@dev" + "squizlabs/php_codesniffer": "^3.13.0 || ^4.0" }, "require-dev": { "ext-filter": "*", "php-parallel-lint/php-console-highlighter": "^1.0", - "php-parallel-lint/php-parallel-lint": "^1.3.2", + "php-parallel-lint/php-parallel-lint": "^1.4.0", "phpcsstandards/phpcsdevcs": "^1.1.6", - "yoast/phpunit-polyfills": "^1.1.0 || ^2.0.0" + "yoast/phpunit-polyfills": "^1.1.0 || ^2.0.0 || ^3.0.0" }, "type": "phpcodesniffer-standard", "extra": { @@ -509,6 +990,7 @@ "phpcodesniffer-standard", "phpcs", "phpcs3", + "phpcs4", "standards", "static analysis", "tokens", @@ -532,45 +1014,445 @@ { "url": "https://opencollective.com/php_codesniffer", "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" } ], - "time": "2023-12-08T14:50:00+00:00" + "time": "2025-06-12T04:32:33+00:00" }, { - "name": "roave/security-advisories", - "version": "dev-latest", + "name": "phpmd/phpmd", + "version": "2.15.0", "source": { "type": "git", - "url": "https://github.com/Roave/SecurityAdvisories.git", - "reference": "e3b3cce1f2454ee4575500e084211b11efdaf64b" + "url": "https://github.com/phpmd/phpmd.git", + "reference": "74a1f56e33afad4128b886e334093e98e1b5e7c0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpmd/phpmd/zipball/74a1f56e33afad4128b886e334093e98e1b5e7c0", + "reference": "74a1f56e33afad4128b886e334093e98e1b5e7c0", + "shasum": "" + }, + "require": { + "composer/xdebug-handler": "^1.0 || ^2.0 || ^3.0", + "ext-xml": "*", + "pdepend/pdepend": "^2.16.1", + "php": ">=5.3.9" + }, + "require-dev": { + "easy-doc/easy-doc": "0.0.0 || ^1.3.2", + "ext-json": "*", + "ext-simplexml": "*", + "gregwar/rst": "^1.0", + "mikey179/vfsstream": "^1.6.8", + "squizlabs/php_codesniffer": "^2.9.2 || ^3.7.2" + }, + "bin": [ + "src/bin/phpmd" + ], + "type": "library", + "autoload": { + "psr-0": { + "PHPMD\\": "src/main/php" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Manuel Pichler", + "email": "github@manuel-pichler.de", + "homepage": "https://github.com/manuelpichler", + "role": "Project Founder" + }, + { + "name": "Marc Würth", + "email": "ravage@bluewin.ch", + "homepage": "https://github.com/ravage84", + "role": "Project Maintainer" + }, + { + "name": "Other contributors", + "homepage": "https://github.com/phpmd/phpmd/graphs/contributors", + "role": "Contributors" + } + ], + "description": "PHPMD is a spin-off project of PHP Depend and aims to be a PHP equivalent of the well known Java tool PMD.", + "homepage": "https://phpmd.org/", + "keywords": [ + "dev", + "mess detection", + "mess detector", + "pdepend", + "phpmd", + "pmd" + ], + "support": { + "irc": "irc://irc.freenode.org/phpmd", + "issues": "https://github.com/phpmd/phpmd/issues", + "source": "https://github.com/phpmd/phpmd/tree/2.15.0" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/phpmd/phpmd", + "type": "tidelift" + } + ], + "time": "2023-12-11T08:22:20+00:00" + }, + { + "name": "phpstan/extension-installer", + "version": "1.4.3", + "source": { + "type": "git", + "url": "https://github.com/phpstan/extension-installer.git", + "reference": "85e90b3942d06b2326fba0403ec24fe912372936" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/extension-installer/zipball/85e90b3942d06b2326fba0403ec24fe912372936", + "reference": "85e90b3942d06b2326fba0403ec24fe912372936", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^2.0", + "php": "^7.2 || ^8.0", + "phpstan/phpstan": "^1.9.0 || ^2.0" + }, + "require-dev": { + "composer/composer": "^2.0", + "php-parallel-lint/php-parallel-lint": "^1.2.0", + "phpstan/phpstan-strict-rules": "^0.11 || ^0.12 || ^1.0" + }, + "type": "composer-plugin", + "extra": { + "class": "PHPStan\\ExtensionInstaller\\Plugin" + }, + "autoload": { + "psr-4": { + "PHPStan\\ExtensionInstaller\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Composer plugin for automatic installation of PHPStan extensions", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "issues": "https://github.com/phpstan/extension-installer/issues", + "source": "https://github.com/phpstan/extension-installer/tree/1.4.3" + }, + "time": "2024-09-04T20:21:43+00:00" + }, + { + "name": "phpstan/phpstan", + "version": "2.1.17", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan.git", + "reference": "89b5ef665716fa2a52ecd2633f21007a6a349053" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/e3b3cce1f2454ee4575500e084211b11efdaf64b", - "reference": "e3b3cce1f2454ee4575500e084211b11efdaf64b", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/89b5ef665716fa2a52ecd2633f21007a6a349053", + "reference": "89b5ef665716fa2a52ecd2633f21007a6a349053", "shasum": "" }, + "require": { + "php": "^7.4|^8.0" + }, "conflict": { - "3f/pygmentize": "<1.2", - "admidio/admidio": "<4.2.13", - "adodb/adodb-php": "<=5.20.20|>=5.21,<=5.21.3", - "aheinze/cockpit": "<2.2", - "aimeos/aimeos-typo3": "<19.10.12|>=20,<20.10.5", - "airesvsg/acf-to-rest-api": "<=3.1", - "akaunting/akaunting": "<2.1.13", + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + } + ], + "time": "2025-05-21T20:55:28+00:00" + }, + { + "name": "phpstan/phpstan-deprecation-rules", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan-deprecation-rules.git", + "reference": "468e02c9176891cc901143da118f09dc9505fc2f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan-deprecation-rules/zipball/468e02c9176891cc901143da118f09dc9505fc2f", + "reference": "468e02c9176891cc901143da118f09dc9505fc2f", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^2.1.15" + }, + "require-dev": { + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^9.6" + }, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "rules.neon" + ] + } + }, + "autoload": { + "psr-4": { + "PHPStan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan rules for detecting usage of deprecated classes, methods, properties, constants and traits.", + "support": { + "issues": "https://github.com/phpstan/phpstan-deprecation-rules/issues", + "source": "https://github.com/phpstan/phpstan-deprecation-rules/tree/2.0.3" + }, + "time": "2025-05-14T10:56:57+00:00" + }, + { + "name": "phpstan/phpstan-strict-rules", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan-strict-rules.git", + "reference": "3e139cbe67fafa3588e1dbe27ca50f31fdb6236a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/3e139cbe67fafa3588e1dbe27ca50f31fdb6236a", + "reference": "3e139cbe67fafa3588e1dbe27ca50f31fdb6236a", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^2.0.4" + }, + "require-dev": { + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpstan-deprecation-rules": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^9.6" + }, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "rules.neon" + ] + } + }, + "autoload": { + "psr-4": { + "PHPStan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Extra strict and opinionated rules for PHPStan", + "support": { + "issues": "https://github.com/phpstan/phpstan-strict-rules/issues", + "source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.0.4" + }, + "time": "2025-03-18T11:42:40+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "roave/security-advisories", + "version": "dev-latest", + "source": { + "type": "git", + "url": "https://github.com/Roave/SecurityAdvisories.git", + "reference": "a76f62e135c8b583602bd99df737b5c20f4d7200" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/a76f62e135c8b583602bd99df737b5c20f4d7200", + "reference": "a76f62e135c8b583602bd99df737b5c20f4d7200", + "shasum": "" + }, + "conflict": { + "3f/pygmentize": "<1.2", + "adaptcms/adaptcms": "<=1.3", + "admidio/admidio": "<4.3.12", + "adodb/adodb-php": "<=5.22.8", + "aheinze/cockpit": "<2.2", + "aimeos/ai-admin-graphql": ">=2022.04.1,<2022.10.10|>=2023.04.1,<2023.10.6|>=2024.04.1,<2024.07.2", + "aimeos/ai-admin-jsonadm": "<2020.10.13|>=2021.04.1,<2021.10.6|>=2022.04.1,<2022.10.3|>=2023.04.1,<2023.10.4|==2024.04.1", + "aimeos/ai-client-html": ">=2020.04.1,<2020.10.27|>=2021.04.1,<2021.10.22|>=2022.04.1,<2022.10.13|>=2023.04.1,<2023.10.15|>=2024.04.1,<2024.04.7", + "aimeos/ai-controller-frontend": "<2020.10.15|>=2021.04.1,<2021.10.8|>=2022.04.1,<2022.10.8|>=2023.04.1,<2023.10.9|==2024.04.1", + "aimeos/aimeos-core": ">=2022.04.1,<2022.10.17|>=2023.04.1,<2023.10.17|>=2024.04.1,<2024.04.7", + "aimeos/aimeos-typo3": "<19.10.12|>=20,<20.10.5", + "airesvsg/acf-to-rest-api": "<=3.1", + "akaunting/akaunting": "<2.1.13", "akeneo/pim-community-dev": "<5.0.119|>=6,<6.0.53", - "alextselegidis/easyappointments": "<1.5", + "alextselegidis/easyappointments": "<=1.5.1", "alterphp/easyadmin-extension-bundle": ">=1.2,<1.2.11|>=1.3,<1.3.1", "amazing/media2click": ">=1,<1.3.3", + "ameos/ameos_tarteaucitron": "<1.2.23", "amphp/artax": "<1.0.6|>=2,<2.0.6", - "amphp/http": "<1.0.1", + "amphp/http": "<=1.7.2|>=2,<=2.1", "amphp/http-client": ">=4,<4.4", "anchorcms/anchor-cms": "<=0.12.7", "andreapollastri/cipi": "<=3.1.15", "andrewhaine/silverstripe-form-capture": ">=0.2,<=0.2.3|>=1,<1.0.2|>=2,<2.2.5", + "aoe/restler": "<1.7.1", "apache-solr-for-typo3/solr": "<2.8.3", "apereo/phpcas": "<1.6", - "api-platform/core": ">=2.2,<2.2.10|>=2.3,<2.3.6|>=2.6,<2.7.10|>=3,<3.0.12|>=3.1,<3.1.3", + "api-platform/core": "<3.4.17|>=4.0.0.0-alpha1,<4.0.22", + "api-platform/graphql": "<3.4.17|>=4.0.0.0-alpha1,<4.0.22", "appwrite/server-ce": "<=1.2.1", "arc/web": "<3", "area17/twill": "<1.2.5|>=2,<2.5.3", @@ -578,35 +1460,52 @@ "asymmetricrypt/asymmetricrypt": "<9.9.99", "athlon1600/php-proxy": "<=5.1", "athlon1600/php-proxy-app": "<=3", + "athlon1600/youtube-downloader": "<=4", "austintoddj/canvas": "<=3.4.2", - "automad/automad": "<=1.10.9", + "auth0/auth0-php": ">=8.0.0.0-beta1,<8.14", + "auth0/login": "<7.17", + "auth0/symfony": "<5.4", + "auth0/wordpress": "<5.3", + "automad/automad": "<2.0.0.0-alpha5", + "automattic/jetpack": "<9.8", "awesome-support/awesome-support": "<=6.0.7", "aws/aws-sdk-php": "<3.288.1", "azuracast/azuracast": "<0.18.3", - "backdrop/backdrop": "<1.24.2", + "b13/seo_basics": "<0.8.2", + "backdrop/backdrop": "<1.27.3|>=1.28,<1.28.2", "backpack/crud": "<3.4.9", + "backpack/filemanager": "<2.0.2|>=3,<3.0.9", "bacula-web/bacula-web": "<8.0.0.0-RC2-dev", "badaso/core": "<2.7", - "bagisto/bagisto": "<1.3.2", + "bagisto/bagisto": "<2.1", "barrelstrength/sprout-base-email": "<1.2.7", "barrelstrength/sprout-forms": "<3.9", - "barryvdh/laravel-translation-manager": "<0.6.2", + "barryvdh/laravel-translation-manager": "<0.6.8", "barzahlen/barzahlen-php": "<2.0.1", - "baserproject/basercms": "<4.8", + "baserproject/basercms": "<=5.1.1", "bassjobsen/bootstrap-3-typeahead": ">4.0.2", + "bbpress/bbpress": "<2.6.5", + "bcit-ci/codeigniter": "<3.1.3", + "bcosca/fatfree": "<3.7.2", + "bedita/bedita": "<4", + "bednee/cooluri": "<1.0.30", "bigfork/silverstripe-form-capture": ">=3,<3.1.1", - "billz/raspap-webgui": "<2.9.5", + "billz/raspap-webgui": "<3.3.6", "bk2k/bootstrap-package": ">=7.1,<7.1.2|>=8,<8.0.8|>=9,<9.0.4|>=9.1,<9.1.3|>=10,<10.0.10|>=11,<11.0.3", + "blueimp/jquery-file-upload": "==6.4.4", "bmarshall511/wordpress_zero_spam": "<5.2.13", "bolt/bolt": "<3.7.2", "bolt/core": "<=4.2", + "born05/craft-twofactorauthentication": "<3.3.4", "bottelet/flarepoint": "<2.2.1", + "bref/bref": "<2.1.17", "brightlocal/phpwhois": "<=4.2.5", "brotkrueml/codehighlight": "<2.7", "brotkrueml/schema": "<1.13.1|>=2,<2.5.1", "brotkrueml/typo3-matomo-integration": "<1.3.2", "buddypress/buddypress": "<7.2.1", - "bugsnag/bugsnag-laravel": "<2.0.2", + "bugsnag/bugsnag-laravel": ">=2,<2.0.2", + "bvbmedia/multishop": "<2.0.39", "bytefury/crater": "<6.0.2", "cachethq/cachet": "<2.5.1", "cakephp/cakephp": "<3.10.3|>=4,<4.0.10|>=4.1,<4.1.4|>=4.2,<4.2.12|>=4.3,<4.3.11|>=4.4,<4.4.10", @@ -614,69 +1513,113 @@ "cardgate/magento2": "<2.0.33", "cardgate/woocommerce": "<=3.1.15", "cart2quote/module-quotation": ">=4.1.6,<=4.4.5|>=5,<5.4.4", + "cart2quote/module-quotation-encoded": ">=4.1.6,<=4.4.5|>=5,<5.4.4", "cartalyst/sentry": "<=2.1.6", "catfan/medoo": "<1.7.5", + "causal/oidc": "<4", "cecil/cecil": "<7.47.1", - "centreon/centreon": "<22.10.0.0-beta1", + "centreon/centreon": "<22.10.15", "cesnet/simplesamlphp-module-proxystatistics": "<3.1", "chriskacerguis/codeigniter-restserver": "<=2.7.1", + "chrome-php/chrome": "<1.14", "civicrm/civicrm-core": ">=4.2,<4.2.9|>=4.3,<4.3.3", - "ckeditor/ckeditor": "<4.17", - "cockpit-hq/cockpit": "<=2.6.3", + "ckeditor/ckeditor": "<4.25", + "clickstorm/cs-seo": ">=6,<6.8|>=7,<7.5|>=8,<8.4|>=9,<9.3", + "co-stack/fal_sftp": "<0.2.6", + "cockpit-hq/cockpit": "<2.11.4", "codeception/codeception": "<3.1.3|>=4,<4.1.22", - "codeigniter/framework": "<3.1.9", - "codeigniter4/framework": "<=4.4.2", + "codeigniter/framework": "<3.1.10", + "codeigniter4/framework": "<4.5.8", "codeigniter4/shield": "<1.0.0.0-beta8", "codiad/codiad": "<=2.8.4", - "composer/composer": "<1.10.27|>=2,<2.2.22|>=2.3,<2.6.4", - "concrete5/concrete5": "<9.2.3", + "codingms/additional-tca": ">=1.7,<1.15.17|>=1.16,<1.16.9", + "commerceteam/commerce": ">=0.9.6,<0.9.9", + "components/jquery": ">=1.0.3,<3.5", + "composer/composer": "<1.10.27|>=2,<2.2.24|>=2.3,<2.7.7", + "concrete5/concrete5": "<9.4.0.0-RC2-dev", "concrete5/core": "<8.5.8|>=9,<9.1", "contao-components/mediaelement": ">=2.14.2,<2.21.1", - "contao/contao": ">=4,<4.4.56|>=4.5,<4.9.40|>=4.10,<4.11.7|>=4.13,<4.13.21|>=5.1,<5.1.4", - "contao/core": ">=2,<3.5.39", - "contao/core-bundle": "<4.9.42|>=4.10,<4.13.28|>=5,<5.1.10", - "contao/listing-bundle": ">=4,<4.4.8", + "contao/comments-bundle": ">=2,<4.13.40|>=5.0.0.0-RC1-dev,<5.3.4", + "contao/contao": ">=3,<3.5.37|>=4,<4.4.56|>=4.5,<4.9.40|>=4.10,<4.11.7|>=4.13,<4.13.21|>=5.1,<5.1.4", + "contao/core": "<3.5.39", + "contao/core-bundle": "<4.13.54|>=5,<5.3.30|>=5.4,<5.5.6", + "contao/listing-bundle": ">=3,<=3.5.30|>=4,<4.4.8", "contao/managed-edition": "<=1.5", "corveda/phpsandbox": "<1.3.5", "cosenary/instagram": "<=2.3", - "craftcms/cms": "<4.6.2", + "couleurcitron/tarteaucitron-wp": "<0.3", + "craftcms/cms": "<4.15.3|>=5,<5.7.5", "croogo/croogo": "<4", "cuyz/valinor": "<0.12", + "czim/file-handling": "<1.5|>=2,<2.3", "czproject/git-php": "<4.0.3", + "damienharper/auditor-bundle": "<5.2.6", + "dapphp/securimage": "<3.6.6", "darylldoyle/safe-svg": "<1.9.10", "datadog/dd-trace": ">=0.30,<0.30.2", "datatables/datatables": "<1.10.10", "david-garcia/phpwhois": "<=4.3.1", "dbrisinajumi/d2files": "<1", - "dcat/laravel-admin": "<=2.1.3.0-beta", + "dcat/laravel-admin": "<=2.1.3|==2.2.0.0-beta|==2.2.2.0-beta", "derhansen/fe_change_pwd": "<2.0.5|>=3,<3.0.3", - "derhansen/sf_event_mgt": "<4.3.1|>=5,<5.1.1", + "derhansen/sf_event_mgt": "<4.3.1|>=5,<5.1.1|>=7,<7.4", "desperado/xml-bundle": "<=0.1.7", + "dev-lancer/minecraft-motd-parser": "<=1.0.5", + "devgroup/dotplant": "<2020.09.14-dev", + "digimix/wp-svg-upload": "<=1", "directmailteam/direct-mail": "<6.0.3|>=7,<7.0.3|>=8,<9.5.2", + "dl/yag": "<3.0.1", + "dmk/webkitpdf": "<1.1.4", + "dnadesign/silverstripe-elemental": "<5.3.12", "doctrine/annotations": "<1.2.7", - "doctrine/cache": "<1.3.2|>=1.4,<1.4.2", + "doctrine/cache": ">=1,<1.3.2|>=1.4,<1.4.2", "doctrine/common": "<2.4.3|>=2.5,<2.5.1", "doctrine/dbal": ">=2,<2.0.8|>=2.1,<2.1.2|>=3,<3.1.4", "doctrine/doctrine-bundle": "<1.5.2", - "doctrine/doctrine-module": "<=0.7.1", + "doctrine/doctrine-module": "<0.7.2", "doctrine/mongodb-odm": "<1.0.2", "doctrine/mongodb-odm-bundle": "<3.0.1", - "doctrine/orm": ">=2,<2.4.8|>=2.5,<2.5.1|>=2.8.3,<2.8.4", - "dolibarr/dolibarr": "<18.0.2", + "doctrine/orm": ">=1,<1.2.4|>=2,<2.4.8|>=2.5,<2.5.1|>=2.8.3,<2.8.4", + "dolibarr/dolibarr": "<19.0.2|==21.0.0.0-beta", "dompdf/dompdf": "<2.0.4", "doublethreedigital/guest-entries": "<3.1.2", - "drupal/core": "<9.5.11|>=10,<10.0.11|>=10.1,<10.1.4", - "drupal/drupal": ">=6,<6.38|>=7,<7.80|>=8,<8.9.16|>=9,<9.1.12|>=9.2,<9.2.4", + "drupal/admin_audit_trail": "<1.0.5", + "drupal/ai": "<1.0.5", + "drupal/alogin": "<2.0.6", + "drupal/cache_utility": "<1.2.1", + "drupal/commerce_alphabank_redirect": "<1.0.3", + "drupal/commerce_eurobank_redirect": "<2.1.1", + "drupal/config_split": "<1.10|>=2,<2.0.2", + "drupal/core": ">=6,<6.38|>=7,<7.102|>=8,<10.3.14|>=10.4,<10.4.5|>=11,<11.0.13|>=11.1,<11.1.5", + "drupal/core-recommended": ">=7,<7.102|>=8,<10.2.11|>=10.3,<10.3.9|>=11,<11.0.8", + "drupal/drupal": ">=5,<5.11|>=6,<6.38|>=7,<7.102|>=8,<10.2.11|>=10.3,<10.3.9|>=11,<11.0.8", + "drupal/formatter_suite": "<2.1", + "drupal/gdpr": "<3.0.1|>=3.1,<3.1.2", + "drupal/google_tag": "<1.8|>=2,<2.0.8", + "drupal/ignition": "<1.0.4", + "drupal/lightgallery": "<1.6", + "drupal/link_field_display_mode_formatter": "<1.6", + "drupal/matomo": "<1.24", + "drupal/oauth2_client": "<4.1.3", + "drupal/oauth2_server": "<2.1", + "drupal/obfuscate": "<2.0.1", + "drupal/quick_node_block": "<2", + "drupal/rapidoc_elements_field_formatter": "<1.0.1", + "drupal/spamspan": "<3.2.1", + "drupal/tfa": "<1.10", "duncanmcclean/guest-entries": "<3.1.2", "dweeves/magmi": "<=0.7.24", - "ec-cube/ec-cube": "<2.4.4", + "ec-cube/ec-cube": "<2.4.4|>=2.11,<=2.17.1|>=3,<=3.0.18.0-patch4|>=4,<=4.1.2", "ecodev/newsletter": "<=4", "ectouch/ectouch": "<=2.7.2", + "egroupware/egroupware": "<23.1.20240624", "elefant/cms": "<2.0.7", "elgg/elgg": "<3.3.24|>=4,<4.0.5", "elijaa/phpmemcacheadmin": "<=1.3", + "elmsln/haxcms": "<11", "encore/laravel-admin": "<=1.8.19", "endroid/qr-code-bundle": "<3.4.2", + "enhavo/enhavo-app": "<=0.13.1", "enshrined/svg-sanitize": "<0.15", "erusev/parsedown": "<1.7.2", "ether/logs": "<3.0.4", @@ -688,30 +1631,39 @@ "ezsystems/ezdemo-ls-extension": ">=5.4,<5.4.2.1-dev", "ezsystems/ezfind-ls": ">=5.3,<5.3.6.1-dev|>=5.4,<5.4.11.1-dev|>=2017.12,<2017.12.0.1-dev", "ezsystems/ezplatform": "<=1.13.6|>=2,<=2.5.24", - "ezsystems/ezplatform-admin-ui": ">=1.3,<1.3.5|>=1.4,<1.4.6|>=1.5,<1.5.29|>=2.3,<2.3.26", - "ezsystems/ezplatform-admin-ui-assets": ">=4,<4.2.1|>=5,<5.0.1|>=5.1,<5.1.1", + "ezsystems/ezplatform-admin-ui": ">=1.3,<1.3.5|>=1.4,<1.4.6|>=1.5,<1.5.29|>=2.3,<2.3.38|>=3.3,<3.3.39", + "ezsystems/ezplatform-admin-ui-assets": ">=4,<4.2.1|>=5,<5.0.1|>=5.1,<5.1.1|>=5.3.0.0-beta1,<5.3.5", "ezsystems/ezplatform-graphql": ">=1.0.0.0-RC1-dev,<1.0.13|>=2.0.0.0-beta1,<2.3.12", - "ezsystems/ezplatform-kernel": "<1.2.5.1-dev|>=1.3,<1.3.34", + "ezsystems/ezplatform-http-cache": "<2.3.16", + "ezsystems/ezplatform-kernel": "<1.2.5.1-dev|>=1.3,<1.3.35", "ezsystems/ezplatform-rest": ">=1.2,<=1.2.2|>=1.3,<1.3.8", - "ezsystems/ezplatform-richtext": ">=2.3,<2.3.7.1-dev", + "ezsystems/ezplatform-richtext": ">=2.3,<2.3.26|>=3.3,<3.3.40", "ezsystems/ezplatform-solr-search-engine": ">=1.7,<1.7.12|>=2,<2.0.2|>=3.3,<3.3.15", "ezsystems/ezplatform-user": ">=1,<1.0.1", "ezsystems/ezpublish-kernel": "<6.13.8.2-dev|>=7,<7.5.31", "ezsystems/ezpublish-legacy": "<=2017.12.7.3|>=2018.6,<=2019.03.5.1", "ezsystems/platform-ui-assets-bundle": ">=4.2,<4.2.3", "ezsystems/repository-forms": ">=2.3,<2.3.2.1-dev|>=2.5,<2.5.15", - "ezyang/htmlpurifier": "<4.1.1", + "ezyang/htmlpurifier": "<=4.2", "facade/ignition": "<1.16.15|>=2,<2.4.2|>=2.5,<2.5.2", "facturascripts/facturascripts": "<=2022.08", + "fastly/magento2": "<1.2.26", "feehi/cms": "<=2.1.1", "feehi/feehicms": "<=2.1.1", "fenom/fenom": "<=2.12.1", + "filament/actions": ">=3.2,<3.2.123", + "filament/infolists": ">=3,<3.2.115", + "filament/tables": ">=3,<3.2.115", "filegator/filegator": "<7.8", + "filp/whoops": "<2.1.13", + "fineuploader/php-traditional-server": "<=1.2.2", "firebase/php-jwt": "<6", + "fisharebest/webtrees": "<=2.1.18", "fixpunkt/fp-masterquiz": "<2.2.1|>=3,<3.5.2", - "fixpunkt/fp-newsletter": "<1.1.1|>=2,<2.1.2|>=2.2,<3.2.6", - "flarum/core": "<1.8.5", - "flarum/framework": "<1.8.5", + "fixpunkt/fp-newsletter": "<1.1.1|>=1.2,<2.1.2|>=2.2,<3.2.6", + "flarum/core": "<1.8.10", + "flarum/flarum": "<0.1.0.0-beta8", + "flarum/framework": "<1.8.10", "flarum/mentions": "<1.6.3", "flarum/sticky": ">=0.1.0.0-beta14,<=0.1.0.0-beta15", "flarum/tags": "<=0.1.0.0-beta13", @@ -723,37 +1675,46 @@ "fooman/tcpdf": "<6.2.22", "forkcms/forkcms": "<5.11.1", "fossar/tcpdf-parser": "<6.2.22", - "francoisjacquet/rosariosis": "<11", + "francoisjacquet/rosariosis": "<=11.5.1", "frappant/frp-form-answers": "<3.1.2|>=4,<4.0.2", "friendsofsymfony/oauth2-php": "<1.3", "friendsofsymfony/rest-bundle": ">=1.2,<1.2.2", - "friendsofsymfony/user-bundle": ">=1.2,<1.3.5", + "friendsofsymfony/user-bundle": ">=1,<1.3.5", + "friendsofsymfony1/swiftmailer": ">=4,<5.4.13|>=6,<6.2.5", + "friendsofsymfony1/symfony1": ">=1.1,<1.5.19", "friendsoftypo3/mediace": ">=7.6.2,<7.6.5", "friendsoftypo3/openid": ">=4.5,<4.5.31|>=4.7,<4.7.16|>=6,<6.0.11|>=6.1,<6.1.6", - "froala/wysiwyg-editor": "<3.2.7|>=4.0.1,<=4.1.1", - "froxlor/froxlor": "<=2.1.1", + "froala/wysiwyg-editor": "<=4.3", + "froxlor/froxlor": "<=2.2.5", + "frozennode/administrator": "<=5.0.12", "fuel/core": "<1.8.1", - "funadmin/funadmin": "<=3.2|>=3.3.2,<=3.3.3", + "funadmin/funadmin": "<=5.0.2", "gaoming13/wechat-php-sdk": "<=1.10.2", "genix/cms": "<=1.1.11", - "getgrav/grav": "<=1.7.42.1", - "getkirby/cms": "<3.5.8.3-dev|>=3.6,<3.6.6.3-dev|>=3.7,<3.7.5.2-dev|>=3.8,<3.8.4.1-dev|>=3.9,<3.9.6", - "getkirby/kirby": "<=2.5.12", + "georgringer/news": "<1.3.3", + "geshi/geshi": "<=1.0.9.1", + "getformwork/formwork": "<1.13.1|>=2.0.0.0-beta1,<2.0.0.0-beta4", + "getgrav/grav": "<1.7.46", + "getkirby/cms": "<3.9.8.3-dev|>=3.10,<3.10.1.2-dev|>=4,<4.7.1", + "getkirby/kirby": "<3.9.8.3-dev|>=3.10,<3.10.1.2-dev|>=4,<4.7.1", "getkirby/panel": "<2.5.14", "getkirby/starterkit": "<=3.7.0.2", "gilacms/gila": "<=1.15.4", - "gleez/cms": "<=1.2|==2", + "gleez/cms": "<=1.3|==2", "globalpayments/php-sdk": "<2", + "goalgorilla/open_social": "<12.3.11|>=12.4,<12.4.10|>=13.0.0.0-alpha1,<13.0.0.0-alpha11", "gogentooss/samlbase": "<1.2.7", "google/protobuf": "<3.15", "gos/web-socket-bundle": "<1.10.4|>=2,<2.6.1|>=3,<3.3", "gree/jose": "<2.2.1", "gregwar/rst": "<1.0.3", - "grumpydictator/firefly-iii": "<6.1.7", + "grumpydictator/firefly-iii": "<6.1.17", "gugoan/economizzer": "<=0.9.0.0-beta1", "guzzlehttp/guzzle": "<6.5.8|>=7,<7.4.5", + "guzzlehttp/oauth-subscriber": "<0.8.1", "guzzlehttp/psr7": "<1.9.1|>=2,<2.4.5", "haffner/jh_captcha": "<=2.1.3|>=3,<=3.0.2", + "handcraftedinthealps/goodby-csv": "<1.4.3", "harvesthq/chosen": "<1.8.7", "helloxz/imgurl": "<=2.31", "hhxsv5/laravel-s": "<3.7.36", @@ -763,200 +1724,269 @@ "hov/jobfair": "<1.0.13|>=2,<2.0.2", "httpsoft/http-message": "<1.0.12", "hyn/multi-tenant": ">=5.6,<5.7.2", - "ibexa/admin-ui": ">=4.2,<4.2.3", - "ibexa/core": ">=4,<4.0.7|>=4.1,<4.1.4|>=4.2,<4.2.3|>=4.5,<4.5.4", + "ibexa/admin-ui": ">=4.2,<4.2.3|>=4.6,<4.6.21", + "ibexa/admin-ui-assets": ">=4.6.0.0-alpha1,<4.6.21", + "ibexa/core": ">=4,<4.0.7|>=4.1,<4.1.4|>=4.2,<4.2.3|>=4.5,<4.5.6|>=4.6,<4.6.2", + "ibexa/fieldtype-richtext": ">=4.6,<4.6.21", "ibexa/graphql": ">=2.5,<2.5.31|>=3.3,<3.3.28|>=4.2,<4.2.3", - "ibexa/post-install": "<=1.0.4", + "ibexa/http-cache": ">=4.6,<4.6.14", + "ibexa/post-install": "<1.0.16|>=4.6,<4.6.14", "ibexa/solr": ">=4.5,<4.5.4", "ibexa/user": ">=4,<4.4.3", "icecoder/icecoder": "<=8.1", "idno/known": "<=1.3.1", - "illuminate/auth": ">=4,<4.0.99|>=4.1,<=4.1.31|>=4.2,<=4.2.22|>=5,<=5.0.35|>=5.1,<=5.1.46|>=5.2,<=5.2.45|>=5.3,<=5.3.31|>=5.4,<=5.4.36|>=5.5,<5.5.10", - "illuminate/cookie": ">=4,<=4.0.11|>=4.1,<=4.1.99999|>=4.2,<=4.2.99999|>=5,<=5.0.99999|>=5.1,<=5.1.99999|>=5.2,<=5.2.99999|>=5.3,<=5.3.99999|>=5.4,<=5.4.99999|>=5.5,<=5.5.49|>=5.6,<=5.6.99999|>=5.7,<=5.7.99999|>=5.8,<=5.8.99999|>=6,<6.18.31|>=7,<7.22.4", + "ilicmiljan/secure-props": ">=1.2,<1.2.2", + "illuminate/auth": "<5.5.10", + "illuminate/cookie": ">=4,<=4.0.11|>=4.1,<6.18.31|>=7,<7.22.4", "illuminate/database": "<6.20.26|>=7,<7.30.5|>=8,<8.40", "illuminate/encryption": ">=4,<=4.0.11|>=4.1,<=4.1.31|>=4.2,<=4.2.22|>=5,<=5.0.35|>=5.1,<=5.1.46|>=5.2,<=5.2.45|>=5.3,<=5.3.31|>=5.4,<=5.4.36|>=5.5,<5.5.40|>=5.6,<5.6.15", "illuminate/view": "<6.20.42|>=7,<7.30.6|>=8,<8.75", + "imdbphp/imdbphp": "<=5.1.1", "impresscms/impresscms": "<=1.4.5", - "impresspages/impresspages": "<=1.0.12", - "in2code/femanager": "<5.5.3|>=6,<6.3.4|>=7,<7.2.3", + "impresspages/impresspages": "<1.0.13", + "in2code/femanager": "<5.5.5|>=6,<6.4.1|>=7,<7.4.2|>=8,<8.2.2", "in2code/ipandlanguageredirect": "<5.1.2", "in2code/lux": "<17.6.1|>=18,<24.0.2", + "in2code/powermail": "<7.5.1|>=8,<8.5.1|>=9,<10.9.1|>=11,<12.4.1", "innologi/typo3-appointments": "<2.0.6", "intelliants/subrion": "<4.2.2", + "inter-mediator/inter-mediator": "==5.5", + "ipl/web": "<0.10.1", + "islandora/crayfish": "<4.1", "islandora/islandora": ">=2,<2.4.1", "ivankristianto/phpwhois": "<=4.3", "jackalope/jackalope-doctrine-dbal": "<1.7.4", + "jambagecom/div2007": "<0.10.2", "james-heinrich/getid3": "<1.9.21", "james-heinrich/phpthumb": "<1.7.12", "jasig/phpcas": "<1.3.3", + "jbartels/wec-map": "<3.0.3", "jcbrand/converse.js": "<3.3.3", + "joelbutcher/socialstream": "<5.6|>=6,<6.2", + "johnbillion/wp-crontrol": "<1.16.2", "joomla/application": "<1.0.13", "joomla/archive": "<1.1.12|>=2,<2.0.1", + "joomla/database": ">=1,<2.2|>=3,<3.4", "joomla/filesystem": "<1.6.2|>=2,<2.0.1", "joomla/filter": "<1.4.4|>=2,<2.0.1", - "joomla/framework": ">=2.5.4,<=3.8.12", + "joomla/framework": "<1.5.7|>=2.5.4,<=3.8.12", "joomla/input": ">=2,<2.0.2", - "joomla/joomla-cms": ">=2.5,<3.9.12", + "joomla/joomla-cms": "<3.9.12|>=4,<4.4.13|>=5,<5.2.6", + "joomla/joomla-platform": "<1.5.4", "joomla/session": "<1.3.1", "joyqi/hyper-down": "<=2.4.27", "jsdecena/laracom": "<2.0.9", "jsmitty12/phpwhois": "<5.1", - "juzaweb/cms": "<=3.4", + "juzaweb/cms": "<=3.4.2", + "jweiland/events2": "<8.3.8|>=9,<9.0.6", + "jweiland/kk-downloader": "<1.2.2", "kazist/phpwhois": "<=4.2.6", "kelvinmo/simplexrd": "<3.1.1", "kevinpapst/kimai2": "<1.16.7", "khodakhah/nodcms": "<=3", - "kimai/kimai": "<2.1", + "kimai/kimai": "<=2.20.1", "kitodo/presentation": "<3.2.3|>=3.3,<3.3.4", "klaviyo/magento2-extension": ">=1,<3", "knplabs/knp-snappy": "<=1.4.2", "kohana/core": "<3.3.3", - "krayin/laravel-crm": "<1.2.2", + "koillection/koillection": "<1.6.12", + "krayin/laravel-crm": "<=1.3", "kreait/firebase-php": ">=3.2,<3.8.1", + "kumbiaphp/kumbiapp": "<=1.1.1", "la-haute-societe/tcpdf": "<6.2.22", "laminas/laminas-diactoros": "<2.18.1|==2.19|==2.20|==2.21|==2.22|==2.23|>=2.24,<2.24.2|>=2.25,<2.25.2", "laminas/laminas-form": "<2.17.1|>=3,<3.0.2|>=3.1,<3.1.1", "laminas/laminas-http": "<2.14.2", + "lara-zeus/artemis": ">=1,<=1.0.6", + "lara-zeus/dynamic-dashboard": ">=3,<=3.0.1", "laravel/fortify": "<1.11.1", - "laravel/framework": "<6.20.44|>=7,<7.30.6|>=8,<8.75", - "laravel/socialite": ">=1,<1.0.99|>=2,<2.0.10", + "laravel/framework": "<10.48.29|>=11,<11.44.1|>=12,<12.1.1", + "laravel/laravel": ">=5.4,<5.4.22", + "laravel/pulse": "<1.3.1", + "laravel/reverb": "<1.4", + "laravel/socialite": ">=1,<2.0.10", "latte/latte": "<2.10.8", - "lavalite/cms": "<=9", + "lavalite/cms": "<=9|==10.1", "lcobucci/jwt": ">=3.4,<3.4.6|>=4,<4.0.4|>=4.1,<4.1.5", - "league/commonmark": "<0.18.3", + "league/commonmark": "<2.7", "league/flysystem": "<1.1.4|>=2,<2.1.1", "league/oauth2-server": ">=8.3.2,<8.4.2|>=8.5,<8.5.3", + "leantime/leantime": "<3.3", "lexik/jwt-authentication-bundle": "<2.10.7|>=2.11,<2.11.3", + "libreform/libreform": ">=2,<=2.0.8", "librenms/librenms": "<2017.08.18", "liftkit/database": "<2.13.2", - "limesurvey/limesurvey": "<3.27.19", + "lightsaml/lightsaml": "<1.3.5", + "limesurvey/limesurvey": "<6.5.12", "livehelperchat/livehelperchat": "<=3.91", - "livewire/livewire": ">2.2.4,<2.2.6", + "livewire/livewire": "<2.12.7|>=3.0.0.0-beta1,<3.5.2", + "livewire/volt": "<1.7", "lms/routes": "<2.1.1", "localizationteam/l10nmgr": "<7.4|>=8,<8.7|>=9,<9.2", + "lomkit/laravel-rest-api": "<2.13", + "luracast/restler": "<3.1", "luyadev/yii-helpers": "<1.2.1", - "magento/community-edition": "<2.4.3.0-patch3|>=2.4.4,<2.4.5", + "macropay-solutions/laravel-crud-wizard-free": "<3.4.17", + "maestroerror/php-heic-to-jpg": "<1.0.5", + "magento/community-edition": "<2.4.5.0-patch13|==2.4.6|>=2.4.6.0-patch1,<2.4.6.0-patch11|>=2.4.7.0-beta1,<2.4.7.0-patch6|>=2.4.8.0-beta1,<2.4.8.0-patch1", "magento/core": "<=1.9.4.5", "magento/magento1ce": "<1.9.4.3-dev", "magento/magento1ee": ">=1,<1.14.4.3-dev", - "magento/product-community-edition": ">=2,<2.2.10|>=2.3,<2.3.2.0-patch2", + "magento/product-community-edition": "<2.4.4.0-patch9|>=2.4.5,<2.4.5.0-patch8|>=2.4.6,<2.4.6.0-patch6|>=2.4.7,<2.4.7.0-patch1", + "magento/project-community-edition": "<=2.0.2", "magneto/core": "<1.9.4.4-dev", "maikuolan/phpmussel": ">=1,<1.6", "mainwp/mainwp": "<=4.4.3.3", - "mantisbt/mantisbt": "<=2.25.7", + "mantisbt/mantisbt": "<=2.26.3", "marcwillmann/turn": "<0.3.3", + "matomo/matomo": "<1.11", "matyhtf/framework": "<3.0.6", - "mautic/core": "<4.3", - "mediawiki/core": ">=1.27,<1.27.6|>=1.29,<1.29.3|>=1.30,<1.30.2|>=1.31,<1.31.9|>=1.32,<1.32.6|>=1.32.99,<1.33.3|>=1.33.99,<1.34.3|>=1.34.99,<1.35", + "mautic/core": "<5.2.6|>=6.0.0.0-alpha,<6.0.2", + "mautic/core-lib": ">=1.0.0.0-beta,<4.4.13|>=5.0.0.0-alpha,<5.1.1", + "maximebf/debugbar": "<1.19", + "mdanter/ecc": "<2", + "mediawiki/abuse-filter": "<1.39.9|>=1.40,<1.41.3|>=1.42,<1.42.2", + "mediawiki/cargo": "<3.6.1", + "mediawiki/core": "<1.39.5|==1.40", + "mediawiki/data-transfer": ">=1.39,<1.39.11|>=1.41,<1.41.3|>=1.42,<1.42.2", "mediawiki/matomo": "<2.4.3", "mediawiki/semantic-media-wiki": "<4.0.2", + "mehrwert/phpmyadmin": "<3.2", "melisplatform/melis-asset-manager": "<5.0.1", "melisplatform/melis-cms": "<5.0.1", "melisplatform/melis-front": "<5.0.1", "mezzio/mezzio-swoole": "<3.7|>=4,<4.3", "mgallegos/laravel-jqgrid": "<=1.3", - "microsoft/microsoft-graph": ">=1.16,<1.109.1|>=2.0.0.0-RC1-dev,<2.0.1", + "microsoft/microsoft-graph": ">=1.16,<1.109.1|>=2,<2.0.1", "microsoft/microsoft-graph-beta": "<2.0.1", "microsoft/microsoft-graph-core": "<2.0.2", - "microweber/microweber": "<=2.0.4", + "microweber/microweber": "<=2.0.16", + "mikehaertl/php-shellcommand": "<1.6.1", "miniorange/miniorange-saml": "<1.4.3", "mittwald/typo3_forum": "<1.2.1", "mobiledetect/mobiledetectlib": "<2.8.32", - "modx/revolution": "<=2.8.3.0-patch", + "modx/revolution": "<=3.1", "mojo42/jirafeau": "<4.4", "mongodb/mongodb": ">=1,<1.9.2", "monolog/monolog": ">=1.8,<1.12", - "moodle/moodle": "<4.3.0.0-RC2-dev", + "moodle/moodle": "<4.3.12|>=4.4,<4.4.8|>=4.5.0.0-beta,<4.5.4", "mos/cimage": "<0.7.19", "movim/moxl": ">=0.8,<=0.10", + "movingbytes/social-network": "<=1.2.1", "mpdf/mpdf": "<=7.1.7", "munkireport/comment": "<4.1", "munkireport/managedinstalls": "<2.6", + "munkireport/munki_facts": "<1.5", "munkireport/munkireport": ">=2.5.3,<5.6.3", + "munkireport/reportdata": "<3.5", + "munkireport/softwareupdate": "<1.6", "mustache/mustache": ">=2,<2.14.1", + "mwdelaney/wp-enable-svg": "<=0.2", "namshi/jose": "<2.2", + "nasirkhan/laravel-starter": "<11.11", + "nategood/httpful": "<1", "neoan3-apps/template": "<1.1.1", "neorazorx/facturascripts": "<2022.04", "neos/flow": ">=1,<1.0.4|>=1.1,<1.1.1|>=2,<2.0.1|>=2.3,<2.3.16|>=3,<3.0.12|>=3.1,<3.1.10|>=3.2,<3.2.13|>=3.3,<3.3.13|>=4,<4.0.6", "neos/form": ">=1.2,<4.3.3|>=5,<5.0.9|>=5.1,<5.1.3", "neos/media-browser": "<7.3.19|>=8,<8.0.16|>=8.1,<8.1.11|>=8.2,<8.2.11|>=8.3,<8.3.9", - "neos/neos": ">=1.1,<1.1.3|>=1.2,<1.2.13|>=2,<2.0.4|>=2.3,<2.9.99|>=3,<3.0.20|>=3.1,<3.1.18|>=3.2,<3.2.14|>=3.3,<5.3.10|>=7,<7.0.9|>=7.1,<7.1.7|>=7.2,<7.2.6|>=7.3,<7.3.4|>=8,<8.0.2", - "neos/swiftmailer": ">=4.1,<4.1.99|>=5.4,<5.4.5", + "neos/neos": ">=1.1,<1.1.3|>=1.2,<1.2.13|>=2,<2.0.4|>=2.3,<3.0.20|>=3.1,<3.1.18|>=3.2,<3.2.14|>=3.3,<5.3.10|>=7,<7.0.9|>=7.1,<7.1.7|>=7.2,<7.2.6|>=7.3,<7.3.4|>=8,<8.0.2", + "neos/swiftmailer": "<5.4.5", + "nesbot/carbon": "<2.72.6|>=3,<3.8.4", + "netcarver/textile": "<=4.1.2", "netgen/tagsbundle": ">=3.4,<3.4.11|>=4,<4.0.15", "nette/application": ">=2,<2.0.19|>=2.1,<2.1.13|>=2.2,<2.2.10|>=2.3,<2.3.14|>=2.4,<2.4.16|>=3,<3.0.6", "nette/nette": ">=2,<2.0.19|>=2.1,<2.1.13", - "nilsteampassnet/teampass": "<3.0.10", + "nilsteampassnet/teampass": "<3.1.3.1-dev", + "nitsan/ns-backup": "<13.0.1", "nonfiction/nterchange": "<4.1.1", "notrinos/notrinos-erp": "<=0.7", "noumo/easyii": "<=0.9", + "novaksolutions/infusionsoft-php-sdk": "<1", "nukeviet/nukeviet": "<4.5.02", "nyholm/psr7": "<1.6.1", "nystudio107/craft-seomatic": "<3.4.12", + "nzedb/nzedb": "<0.8", "nzo/url-encryptor-bundle": ">=4,<4.3.2|>=5,<5.0.1", "october/backend": "<1.1.2", "october/cms": "<1.0.469|==1.0.469|==1.0.471|==1.1.1", - "october/october": "<=3.4.4", + "october/october": "<3.7.5", "october/rain": "<1.0.472|>=1.1,<1.1.2", - "october/system": "<1.0.476|>=1.1,<1.1.12|>=2,<2.2.34|>=3,<3.5.2", + "october/system": "<3.7.5", + "oliverklee/phpunit": "<3.5.15", "omeka/omeka-s": "<4.0.3", "onelogin/php-saml": "<2.10.4", - "oneup/uploader-bundle": "<1.9.3|>=2,<2.1.5", + "oneup/uploader-bundle": ">=1,<1.9.3|>=2,<2.1.5", "open-web-analytics/open-web-analytics": "<1.7.4", - "opencart/opencart": "<=3.0.3.7|>=4,<4.0.2.3-dev", + "opencart/opencart": ">=0", "openid/php-openid": "<2.3", - "openmage/magento-lts": "<20.2", - "opensource-workshop/connect-cms": "<1.7.2|>=2,<2.3.2", - "orchid/platform": ">=9,<9.4.4|>=14.0.0.0-alpha4,<14.5", + "openmage/magento-lts": "<20.12.3", + "opensolutions/vimbadmin": "<=3.0.15", + "opensource-workshop/connect-cms": "<1.8.7|>=2,<2.4.7", + "orchid/platform": ">=8,<14.43", "oro/calendar-bundle": ">=4.2,<=4.2.6|>=5,<=5.0.6|>=5.1,<5.1.1", "oro/commerce": ">=4.1,<5.0.11|>=5.1,<5.1.1", "oro/crm": ">=1.7,<1.7.4|>=3.1,<4.1.17|>=4.2,<4.2.7", "oro/crm-call-bundle": ">=4.2,<=4.2.5|>=5,<5.0.4|>=5.1,<5.1.1", - "oro/customer-portal": ">=4.2,<=4.2.8|>=5,<5.0.11|>=5.1,<5.1.1", - "oro/platform": ">=1.7,<1.7.4|>=3.1,<3.1.29|>=4.1,<4.1.17|>=4.2,<=4.2.10|>=5,<5.0.8", - "oxid-esales/oxideshop-ce": "<4.5", + "oro/customer-portal": ">=4.1,<=4.1.13|>=4.2,<=4.2.10|>=5,<=5.0.11|>=5.1,<=5.1.3", + "oro/platform": ">=1.7,<1.7.4|>=3.1,<3.1.29|>=4.1,<4.1.17|>=4.2,<=4.2.10|>=5,<=5.0.12|>=5.1,<=5.1.3", + "oveleon/contao-cookiebar": "<1.16.3|>=2,<2.1.3", + "oxid-esales/oxideshop-ce": "<=7.0.5", + "oxid-esales/paymorrow-module": ">=1,<1.0.2|>=2,<2.0.1", "packbackbooks/lti-1-3-php-library": "<5", "padraic/humbug_get_contents": "<1.1.2", "pagarme/pagarme-php": "<3", "pagekit/pagekit": "<=1.0.18", + "paragonie/ecc": "<2.0.1", "paragonie/random_compat": "<2", - "passbolt/passbolt_api": "<2.11", + "passbolt/passbolt_api": "<4.6.2", + "paypal/adaptivepayments-sdk-php": "<=3.9.2", + "paypal/invoice-sdk-php": "<=3.9", "paypal/merchant-sdk-php": "<3.12", + "paypal/permissions-sdk-php": "<=3.9.1", "pear/archive_tar": "<1.4.14", + "pear/auth": "<1.2.4", "pear/crypt_gpg": "<1.6.7", + "pear/http_request2": "<2.7", "pear/pear": "<=1.10.1", "pegasus/google-for-jobs": "<1.5.1|>=2,<2.1.1", "personnummer/personnummer": "<3.0.2", "phanan/koel": "<5.1.4", - "phenx/php-svg-lib": "<0.5.1", + "phenx/php-svg-lib": "<0.5.2", + "php-censor/php-censor": "<2.0.13|>=2.1,<2.1.5", "php-mod/curl": "<2.3.2", - "phpbb/phpbb": "<3.2.10|>=3.3,<3.3.1", + "phpbb/phpbb": "<3.3.11", "phpems/phpems": ">=6,<=6.1.3", "phpfastcache/phpfastcache": "<6.1.5|>=7,<7.1.2|>=8,<8.0.7", "phpmailer/phpmailer": "<6.5", "phpmussel/phpmussel": ">=1,<1.6", - "phpmyadmin/phpmyadmin": "<5.2.1", - "phpmyfaq/phpmyfaq": "<=3.1.7", - "phpoffice/phpexcel": "<1.8", - "phpoffice/phpspreadsheet": "<1.16", - "phpseclib/phpseclib": "<2.0.31|>=3,<3.0.34", + "phpmyadmin/phpmyadmin": "<5.2.2", + "phpmyfaq/phpmyfaq": "<3.2.5|==3.2.5|>=3.2.10,<=4.0.1", + "phpoffice/common": "<0.2.9", + "phpoffice/math": "<=0.2", + "phpoffice/phpexcel": "<=1.8.2", + "phpoffice/phpspreadsheet": "<1.29.9|>=2,<2.1.8|>=2.2,<2.3.7|>=3,<3.9", + "phpseclib/phpseclib": "<2.0.47|>=3,<3.0.36", "phpservermon/phpservermon": "<3.6", "phpsysinfo/phpsysinfo": "<3.4.3", - "phpunit/phpunit": ">=4.8.19,<4.8.28|>=5,<5.6.3", + "phpunit/phpunit": ">=4.8.19,<4.8.28|>=5.0.10,<5.6.3", "phpwhois/phpwhois": "<=4.2.5", "phpxmlrpc/extras": "<0.6.1", "phpxmlrpc/phpxmlrpc": "<4.9.2", "pi/pi": "<=2.5", - "pimcore/admin-ui-classic-bundle": "<1.3.2", - "pimcore/customer-management-framework-bundle": "<4.0.6", + "pimcore/admin-ui-classic-bundle": "<1.7.6", + "pimcore/customer-management-framework-bundle": "<4.2.1", "pimcore/data-hub": "<1.2.4", + "pimcore/data-importer": "<1.8.9|>=1.9,<1.9.3", "pimcore/demo": "<10.3", "pimcore/ecommerce-framework-bundle": "<1.0.10", "pimcore/perspective-editor": "<1.5.1", - "pimcore/pimcore": "<11.1.1", - "pixelfed/pixelfed": "<=0.11.4", + "pimcore/pimcore": "<11.5.4", + "piwik/piwik": "<1.11", + "pixelfed/pixelfed": "<0.12.5", "plotly/plotly.js": "<2.25.2", "pocketmine/bedrock-protocol": "<8.0.2", - "pocketmine/pocketmine-mp": "<=4.23|>=5,<5.3.1", + "pocketmine/pocketmine-mp": "<5.25.2", "pocketmine/raklib": ">=0.14,<0.14.6|>=0.15,<0.15.1", "pressbooks/pressbooks": "<5.18", "prestashop/autoupgrade": ">=4,<4.10.1", @@ -964,22 +1994,27 @@ "prestashop/blockwishlist": ">=2,<2.1.1", "prestashop/contactform": ">=1.0.1,<4.3", "prestashop/gamification": "<2.3.2", - "prestashop/prestashop": "<8.1.3", + "prestashop/prestashop": "<8.1.6", "prestashop/productcomments": "<5.0.2", + "prestashop/ps_contactinfo": "<=3.3.2", "prestashop/ps_emailsubscription": "<2.6.1", "prestashop/ps_facetedsearch": "<3.4.1", "prestashop/ps_linklist": "<3.1", - "privatebin/privatebin": "<1.4", - "processwire/processwire": "<=3.0.210", + "privatebin/privatebin": "<1.4|>=1.5,<1.7.4", + "processwire/processwire": "<=3.0.229", "propel/propel": ">=2.0.0.0-alpha1,<=2.0.0.0-alpha7", "propel/propel1": ">=1,<=1.7.1", - "pterodactyl/panel": "<1.7", + "pterodactyl/panel": "<=1.11.10", "ptheofan/yii2-statemachine": ">=2.0.0.0-RC1-dev,<=2", "ptrofimov/beanstalk_console": "<1.7.14", "pubnub/pubnub": "<6.1", + "punktde/pt_extbase": "<1.5.1", "pusher/pusher-php-server": "<2.2.1", "pwweb/laravel-core": "<=0.3.6.0-beta", + "pxlrbt/filament-excel": "<1.1.14|>=2.0.0.0-alpha,<2.3.3", "pyrocms/pyrocms": "<=3.9.1", + "qcubed/qcubed": "<=3.1.1", + "quickapps/cms": "<=2.0.0.0-beta2", "rainlab/blog-plugin": "<1.4.1", "rainlab/debugbar-plugin": "<3.1", "rainlab/user-plugin": "<=1.4.5", @@ -987,87 +2022,106 @@ "rap2hpoutre/laravel-log-viewer": "<0.13", "react/http": ">=0.7,<1.9", "really-simple-plugins/complianz-gdpr": "<6.4.2", - "remdex/livehelperchat": "<3.99", - "reportico-web/reportico": "<=7.1.21", + "redaxo/source": "<5.18.3", + "remdex/livehelperchat": "<4.29", + "renolit/reint-downloadmanager": "<4.0.2|>=5,<5.0.1", + "reportico-web/reportico": "<=8.1", "rhukster/dom-sanitizer": "<1.0.7", "rmccue/requests": ">=1.6,<1.8", - "robrichards/xmlseclibs": "<3.0.4", + "robrichards/xmlseclibs": ">=1,<3.0.4", "roots/soil": "<4.1", + "roundcube/roundcubemail": "<1.5.10|>=1.6,<1.6.11", "rudloff/alltube": "<3.0.3", + "rudloff/rtmpdump-bin": "<=2.3.1", "s-cart/core": "<6.9", "s-cart/s-cart": "<6.9", "sabberworm/php-css-parser": ">=1,<1.0.1|>=2,<2.0.1|>=3,<3.0.1|>=4,<4.0.1|>=5,<5.0.9|>=5.1,<5.1.3|>=5.2,<5.2.1|>=6,<6.0.2|>=7,<7.0.4|>=8,<8.0.1|>=8.1,<8.1.1|>=8.2,<8.2.1|>=8.3,<8.3.1", - "sabre/dav": "<1.7.11|>=1.8,<1.8.9", + "sabre/dav": ">=1.6,<1.7.11|>=1.8,<1.8.9", + "samwilson/unlinked-wikibase": "<1.42", "scheb/two-factor-bundle": "<3.26|>=4,<4.11", "sensiolabs/connect": "<4.2.3", "serluck/phpwhois": "<=4.2.6", "sfroemken/url_redirect": "<=1.2.1", - "sheng/yiicms": "<=1.2", - "shopware/core": "<=6.5.7.3", - "shopware/platform": "<=6.5.7.3", + "sheng/yiicms": "<1.2.1", + "shopware/core": "<6.5.8.18-dev|>=6.6,<6.6.10.3-dev|>=6.7.0.0-RC1-dev,<6.7.0.0-RC2-dev", + "shopware/platform": "<6.5.8.18-dev|>=6.6,<6.6.10.3-dev|>=6.7.0.0-RC1-dev,<6.7.0.0-RC2-dev", "shopware/production": "<=6.3.5.2", "shopware/shopware": "<=5.7.17", - "shopware/storefront": "<=6.4.8.1", - "shopxo/shopxo": "<2.2.6", + "shopware/storefront": "<=6.4.8.1|>=6.5.8,<6.5.8.7-dev", + "shopxo/shopxo": "<=6.4", "showdoc/showdoc": "<2.10.4", + "shuchkin/simplexlsx": ">=1.0.12,<1.1.13", "silverstripe-australia/advancedreports": ">=1,<=2", "silverstripe/admin": "<1.13.19|>=2,<2.1.8", "silverstripe/assets": ">=1,<1.11.1", "silverstripe/cms": "<4.11.3", - "silverstripe/comments": ">=1.3,<1.9.99|>=2,<2.9.99|>=3,<3.1.1", + "silverstripe/comments": ">=1.3,<3.1.1", "silverstripe/forum": "<=0.6.1|>=0.7,<=0.7.3", - "silverstripe/framework": "<4.13.39|>=5,<5.1.11", - "silverstripe/graphql": "<3.8.2|>=4,<4.3.7|>=5,<5.1.3", + "silverstripe/framework": "<5.3.23", + "silverstripe/graphql": ">=2,<2.0.5|>=3,<3.8.2|>=4,<4.3.7|>=5,<5.1.3", "silverstripe/hybridsessions": ">=1,<2.4.1|>=2.5,<2.5.1", "silverstripe/recipe-cms": ">=4.5,<4.5.3", "silverstripe/registry": ">=2.1,<2.1.2|>=2.2,<2.2.1", - "silverstripe/restfulserver": ">=1,<1.0.9|>=2,<2.0.4", + "silverstripe/reports": "<5.2.3", + "silverstripe/restfulserver": ">=1,<1.0.9|>=2,<2.0.4|>=2.1,<2.1.2", "silverstripe/silverstripe-omnipay": "<2.5.2|>=3,<3.0.2|>=3.1,<3.1.4|>=3.2,<3.2.1", "silverstripe/subsites": ">=2,<2.6.1", "silverstripe/taxonomy": ">=1.3,<1.3.1|>=2,<2.0.1", - "silverstripe/userforms": "<3", + "silverstripe/userforms": "<3|>=5,<5.4.2", "silverstripe/versioned-admin": ">=1,<1.11.1", "simple-updates/phpwhois": "<=1", - "simplesamlphp/saml2": "<1.15.4|>=2,<2.3.8|>=3,<3.1.4|==5.0.0.0-alpha12", + "simplesamlphp/saml2": "<=4.16.15|>=5.0.0.0-alpha1,<=5.0.0.0-alpha19", + "simplesamlphp/saml2-legacy": "<=4.16.15", "simplesamlphp/simplesamlphp": "<1.18.6", "simplesamlphp/simplesamlphp-module-infocard": "<1.0.1", "simplesamlphp/simplesamlphp-module-openid": "<1", "simplesamlphp/simplesamlphp-module-openidprovider": "<0.9", + "simplesamlphp/xml-common": "<1.20", "simplesamlphp/xml-security": "==1.6.11", "simplito/elliptic-php": "<1.0.6", "sitegeist/fluid-components": "<3.5", + "sjbr/sr-feuser-register": "<2.6.2|>=5.1,<12.5", "sjbr/sr-freecap": "<2.4.6|>=2.5,<2.5.3", + "sjbr/static-info-tables": "<2.3.1", "slim/psr7": "<1.4.1|>=1.5,<1.5.1|>=1.6,<1.6.1", "slim/slim": "<2.6", "slub/slub-events": "<3.0.3", - "smarty/smarty": "<3.1.48|>=4,<4.3.1", - "snipe/snipe-it": "<=6.2.2", + "smarty/smarty": "<4.5.3|>=5,<5.1.1", + "snipe/snipe-it": "<8.1", "socalnick/scn-social-auth": "<1.15.2", "socialiteproviders/steam": "<1.1", - "spatie/browsershot": "<3.57.4", + "spatie/browsershot": "<5.0.5", + "spatie/image-optimizer": "<1.7.3", + "spencer14420/sp-php-email-handler": "<1", "spipu/html2pdf": "<5.2.8", "spoon/library": "<1.4.1", "spoonity/tcpdf": "<6.2.22", "squizlabs/php_codesniffer": ">=1,<2.8.1|>=3,<3.0.1", - "ssddanbrown/bookstack": "<22.02.3", - "statamic/cms": "<4.36", + "ssddanbrown/bookstack": "<24.05.1", + "starcitizentools/citizen-skin": ">=1.9.4,<3.4", + "starcitizentools/short-description": ">=4,<4.0.1", + "starcitizentools/tabber-neue": ">=1.9.1,<2.7.2|>=3,<3.1.1", + "statamic/cms": "<=5.16", "stormpath/sdk": "<9.9.99", - "studio-42/elfinder": "<2.1.62", + "studio-42/elfinder": "<=2.1.64", + "studiomitte/friendlycaptcha": "<0.1.4", "subhh/libconnect": "<7.0.8|>=8,<8.1", "sukohi/surpass": "<1", - "sulu/sulu": "<1.6.44|>=2,<2.2.18|>=2.3,<2.3.8|==2.4.0.0-RC1|>=2.5,<2.5.10", + "sulu/form-bundle": ">=2,<2.5.3", + "sulu/sulu": "<1.6.44|>=2,<2.5.25|>=2.6,<2.6.9|>=3.0.0.0-alpha1,<3.0.0.0-alpha3", "sumocoders/framework-user-bundle": "<1.4", "superbig/craft-audit": "<3.0.2", + "svewap/a21glossary": "<=0.4.10", "swag/paypal": "<5.4.4", - "swiftmailer/swiftmailer": ">=4,<5.4.5", + "swiftmailer/swiftmailer": "<6.2.5", "swiftyedit/swiftyedit": "<1.2", "sylius/admin-bundle": ">=1,<1.0.17|>=1.1,<1.1.9|>=1.2,<1.2.2", "sylius/grid": ">=1,<1.1.19|>=1.2,<1.2.18|>=1.3,<1.3.13|>=1.4,<1.4.5|>=1.5,<1.5.1", "sylius/grid-bundle": "<1.10.1", - "sylius/paypal-plugin": ">=1,<1.2.4|>=1.3,<1.3.1", - "sylius/resource-bundle": "<1.3.14|>=1.4,<1.4.7|>=1.5,<1.5.2|>=1.6,<1.6.4", - "sylius/sylius": "<1.9.10|>=1.10,<1.10.11|>=1.11,<1.11.2", - "symbiote/silverstripe-multivaluefield": ">=3,<3.0.99", + "sylius/paypal-plugin": "<1.6.2|>=1.7,<1.7.2|>=2,<2.0.2", + "sylius/resource-bundle": ">=1,<1.3.14|>=1.4,<1.4.7|>=1.5,<1.5.2|>=1.6,<1.6.4", + "sylius/sylius": "<1.12.19|>=1.13.0.0-alpha1,<1.13.4", + "symbiote/silverstripe-multivaluefield": ">=3,<3.1", "symbiote/silverstripe-queuedjobs": ">=3,<3.0.2|>=3.1,<3.1.4|>=4,<4.0.7|>=4.1,<4.1.2|>=4.2,<4.2.4|>=4.3,<4.3.3|>=4.4,<4.4.3|>=4.5,<4.5.1|>=4.6,<4.6.4", "symbiote/silverstripe-seed": "<6.0.3", "symbiote/silverstripe-versionedfiles": "<=2.0.3", @@ -1076,8 +2130,9 @@ "symfony/dependency-injection": ">=2,<2.0.17|>=2.7,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7", "symfony/error-handler": ">=4.4,<4.4.4|>=5,<5.0.4", "symfony/form": ">=2.3,<2.3.35|>=2.4,<2.6.12|>=2.7,<2.7.50|>=2.8,<2.8.49|>=3,<3.4.20|>=4,<4.0.15|>=4.1,<4.1.9|>=4.2,<4.2.1", - "symfony/framework-bundle": ">=2,<2.3.18|>=2.4,<2.4.8|>=2.5,<2.5.2|>=2.7,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7|>=5.3.14,<=5.3.14|>=5.4.3,<=5.4.3|>=6.0.3,<=6.0.3", - "symfony/http-foundation": ">=2,<2.8.52|>=3,<3.4.35|>=4,<4.2.12|>=4.3,<4.3.8|>=4.4,<4.4.7|>=5,<5.0.7", + "symfony/framework-bundle": ">=2,<2.3.18|>=2.4,<2.4.8|>=2.5,<2.5.2|>=2.7,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7|>=5.3.14,<5.3.15|>=5.4.3,<5.4.4|>=6.0.3,<6.0.4", + "symfony/http-client": ">=4.3,<5.4.47|>=6,<6.4.15|>=7,<7.1.8", + "symfony/http-foundation": "<5.4.46|>=6,<6.4.14|>=7,<7.1.7", "symfony/http-kernel": ">=2,<4.4.50|>=5,<5.4.20|>=6,<6.0.20|>=6.1,<6.1.12|>=6.2,<6.2.6", "symfony/intl": ">=2.7,<2.7.38|>=2.8,<2.8.31|>=3,<3.2.14|>=3.3,<3.3.13", "symfony/maker-bundle": ">=1.27,<1.29.2|>=1.30,<1.31.1", @@ -1085,57 +2140,78 @@ "symfony/phpunit-bridge": ">=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7", "symfony/polyfill": ">=1,<1.10", "symfony/polyfill-php55": ">=1,<1.10", + "symfony/process": "<5.4.46|>=6,<6.4.14|>=7,<7.1.7", "symfony/proxy-manager-bridge": ">=2.7,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7", "symfony/routing": ">=2,<2.0.19", + "symfony/runtime": ">=5.3,<5.4.46|>=6,<6.4.14|>=7,<7.1.7", "symfony/security": ">=2,<2.7.51|>=2.8,<3.4.49|>=4,<4.4.24|>=5,<5.2.8", - "symfony/security-bundle": ">=2,<4.4.50|>=5,<5.4.20|>=6,<6.0.20|>=6.1,<6.1.12|>=6.2,<6.2.6", + "symfony/security-bundle": ">=2,<4.4.50|>=5,<5.4.20|>=6,<6.0.20|>=6.1,<6.1.12|>=6.2,<6.4.10|>=7,<7.0.10|>=7.1,<7.1.3", "symfony/security-core": ">=2.4,<2.6.13|>=2.7,<2.7.9|>=2.7.30,<2.7.32|>=2.8,<3.4.49|>=4,<4.4.24|>=5,<5.2.9", "symfony/security-csrf": ">=2.4,<2.7.48|>=2.8,<2.8.41|>=3,<3.3.17|>=3.4,<3.4.11|>=4,<4.0.11", "symfony/security-guard": ">=2.8,<3.4.48|>=4,<4.4.23|>=5,<5.2.8", - "symfony/security-http": ">=2.3,<2.3.41|>=2.4,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.2.12|>=4.3,<4.3.8|>=4.4,<4.4.7|>=5,<5.0.7|>=5.1,<5.2.8|>=5.3,<5.3.2|>=5.4,<5.4.31|>=6,<6.3.8", + "symfony/security-http": ">=2.3,<2.3.41|>=2.4,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.2.12|>=4.3,<4.3.8|>=4.4,<4.4.7|>=5,<5.0.7|>=5.1,<5.2.8|>=5.3,<5.4.47|>=6,<6.4.15|>=7,<7.1.8", "symfony/serializer": ">=2,<2.0.11|>=4.1,<4.4.35|>=5,<5.3.12", - "symfony/symfony": "<4.4.51|>=5,<5.4.31|>=6,<6.3.8", + "symfony/symfony": "<5.4.47|>=6,<6.4.15|>=7,<7.1.8", "symfony/translation": ">=2,<2.0.17", "symfony/twig-bridge": ">=2,<4.4.51|>=5,<5.4.31|>=6,<6.3.8", "symfony/ux-autocomplete": "<2.11.2", - "symfony/validator": ">=2,<2.0.24|>=2.1,<2.1.12|>=2.2,<2.2.5|>=2.3,<2.3.3", + "symfony/ux-live-component": "<2.25.1", + "symfony/ux-twig-component": "<2.25.1", + "symfony/validator": "<5.4.43|>=6,<6.4.11|>=7,<7.1.4", "symfony/var-exporter": ">=4.2,<4.2.12|>=4.3,<4.3.8", "symfony/web-profiler-bundle": ">=2,<2.3.19|>=2.4,<2.4.9|>=2.5,<2.5.4", "symfony/webhook": ">=6.3,<6.3.8", - "symfony/yaml": ">=2,<2.0.22|>=2.1,<2.1.7", + "symfony/yaml": ">=2,<2.0.22|>=2.1,<2.1.7|>=2.2.0.0-beta1,<2.2.0.0-beta2", "symphonycms/symphony-2": "<2.6.4", "t3/dce": "<0.11.5|>=2.2,<2.6.2", "t3g/svg-sanitizer": "<1.0.3", "t3s/content-consent": "<1.0.3|>=2,<2.0.2", - "tastyigniter/tastyigniter": "<3.3", - "tcg/voyager": "<=1.4", - "tecnickcom/tcpdf": "<6.2.22", + "tastyigniter/tastyigniter": "<4", + "tcg/voyager": "<=1.8", + "tecnickcom/tc-lib-pdf-font": "<2.6.4", + "tecnickcom/tcpdf": "<6.8", "terminal42/contao-tablelookupwizard": "<3.3.5", "thelia/backoffice-default-template": ">=2.1,<2.1.2", "thelia/thelia": ">=2.1,<2.1.3", "theonedemon/phpwhois": "<=4.2.5", - "thinkcmf/thinkcmf": "<=5.1.7", - "thorsten/phpmyfaq": "<3.2.2", + "thinkcmf/thinkcmf": "<6.0.8", + "thorsten/phpmyfaq": "<=4.0.1", "tikiwiki/tiki-manager": "<=17.1", - "tinymce/tinymce": "<5.10.9|>=6,<6.7.3", + "timber/timber": ">=0.16.6,<1.23.1|>=1.24,<1.24.1|>=2,<2.1", + "tinymce/tinymce": "<7.2", "tinymighty/wiki-seo": "<1.2.2", "titon/framework": "<9.9.99", + "tltneon/lgsl": "<7", "tobiasbg/tablepress": "<=2.0.0.0-RC1", - "topthink/framework": "<6.0.14", + "topthink/framework": "<6.0.17|>=6.1,<=8.0.4", "topthink/think": "<=6.1.1", - "topthink/thinkphp": "<=3.2.3", + "topthink/thinkphp": "<=3.2.3|>=6.1.3,<=8.0.4", + "torrentpier/torrentpier": "<=2.4.3", "tpwd/ke_search": "<4.0.3|>=4.1,<4.6.6|>=5,<5.0.2", - "tribalsystems/zenario": "<=9.4.59197", + "tribalsystems/zenario": "<=9.7.61188", "truckersmp/phpwhois": "<=4.3.1", "ttskch/pagination-service-provider": "<1", - "twig/twig": "<1.44.7|>=2,<2.15.3|>=3,<3.4.3", + "twbs/bootstrap": "<=3.4.1|>=4,<=4.6.2", + "twig/twig": "<3.11.2|>=3.12,<3.14.1|>=3.16,<3.19", "typo3/cms": "<9.5.29|>=10,<10.4.35|>=11,<11.5.23|>=12,<12.2", - "typo3/cms-backend": ">=7,<=7.6.50|>=8,<=8.7.39|>=9,<=9.5.24|>=10,<=10.4.13|>=11,<=11.1", - "typo3/cms-core": "<8.7.55|>=9,<9.5.44|>=10,<10.4.41|>=11,<11.5.33|>=12,<12.4.8", + "typo3/cms-backend": "<4.1.14|>=4.2,<4.2.15|>=4.3,<4.3.7|>=4.4,<4.4.4|>=7,<=7.6.50|>=8,<=8.7.39|>=9,<=9.5.24|>=10,<10.4.46|>=11,<11.5.40|>=12,<=12.4.30|>=13,<=13.4.11", + "typo3/cms-belog": ">=10,<=10.4.47|>=11,<=11.5.41|>=12,<=12.4.24|>=13,<=13.4.2", + "typo3/cms-beuser": ">=10,<=10.4.47|>=11,<=11.5.41|>=12,<=12.4.24|>=13,<=13.4.2", + "typo3/cms-core": "<=8.7.56|>=9,<=9.5.50|>=10,<=10.4.49|>=11,<=11.5.43|>=12,<=12.4.30|>=13,<=13.4.11", + "typo3/cms-dashboard": ">=10,<=10.4.47|>=11,<=11.5.41|>=12,<=12.4.24|>=13,<=13.4.2", "typo3/cms-extbase": "<6.2.24|>=7,<7.6.8|==8.1.1", - "typo3/cms-form": ">=8,<=8.7.39|>=9,<=9.5.24|>=10,<=10.4.13|>=11,<=11.1", - "typo3/cms-install": ">=12.2,<12.4.8", + "typo3/cms-extensionmanager": ">=10,<=10.4.47|>=11,<=11.5.41|>=12,<=12.4.24|>=13,<=13.4.2", + "typo3/cms-felogin": ">=4.2,<4.2.3", + "typo3/cms-fluid": "<4.3.4|>=4.4,<4.4.1", + "typo3/cms-form": ">=8,<=8.7.39|>=9,<=9.5.24|>=10,<=10.4.47|>=11,<=11.5.41|>=12,<=12.4.24|>=13,<=13.4.2", + "typo3/cms-frontend": "<4.3.9|>=4.4,<4.4.5", + "typo3/cms-indexed-search": ">=10,<=10.4.47|>=11,<=11.5.41|>=12,<=12.4.24|>=13,<=13.4.2", + "typo3/cms-install": "<4.1.14|>=4.2,<4.2.16|>=4.3,<4.3.9|>=4.4,<4.4.5|>=12.2,<12.4.8|==13.4.2", + "typo3/cms-lowlevel": ">=11,<=11.5.41", "typo3/cms-rte-ckeditor": ">=9.5,<9.5.42|>=10,<10.4.39|>=11,<11.5.30", + "typo3/cms-scheduler": ">=11,<=11.5.41", + "typo3/cms-setup": ">=9,<=9.5.50|>=10,<=10.4.49|>=11,<=11.5.43|>=12,<=12.4.30|>=13,<=13.4.11", + "typo3/cms-webhooks": ">=12,<=12.4.30|>=13,<=13.4.11", "typo3/flow": ">=1,<1.0.4|>=1.1,<1.1.1|>=2,<2.0.1|>=2.3,<2.3.16|>=3,<3.0.12|>=3.1,<3.1.10|>=3.2,<3.2.13|>=3.3,<3.3.13|>=4,<4.0.6", "typo3/html-sanitizer": ">=1,<=1.5.2|>=2,<=2.1.3", "typo3/neos": ">=1.1,<1.1.3|>=1.2,<1.2.13|>=2,<2.0.4|>=2.3,<2.3.99|>=3,<3.0.20|>=3.1,<3.1.18|>=3.2,<3.2.14|>=3.3,<3.3.23|>=4,<4.0.17|>=4.1,<4.1.16|>=4.2,<4.2.12|>=4.3,<4.3.3", @@ -1144,19 +2220,32 @@ "typo3fluid/fluid": ">=2,<2.0.8|>=2.1,<2.1.7|>=2.2,<2.2.4|>=2.3,<2.3.7|>=2.4,<2.4.4|>=2.5,<2.5.11|>=2.6,<2.6.10", "ua-parser/uap-php": "<3.8", "uasoft-indonesia/badaso": "<=2.9.7", - "unisharp/laravel-filemanager": "<2.6.4", + "unisharp/laravel-filemanager": "<2.9.1", + "unopim/unopim": "<0.1.5", "userfrosting/userfrosting": ">=0.3.1,<4.6.3", "usmanhalalit/pixie": "<1.0.3|>=2,<2.0.2", "uvdesk/community-skeleton": "<=1.1.1", + "uvdesk/core-framework": "<=1.1.1", "vanilla/safecurl": "<0.9.2", + "verbb/comments": "<1.5.5", + "verbb/formie": "<=2.1.43", + "verbb/image-resizer": "<2.0.9", + "verbb/knock-knock": "<1.2.8", "verot/class.upload.php": "<=2.1.6", + "vertexvaar/falsftp": "<0.2.6", + "villagedefrance/opencart-overclocked": "<=1.11.1", "vova07/yii2-fileapi-widget": "<0.1.9", "vrana/adminer": "<4.8.1", + "vufind/vufind": ">=2,<9.1.1", "waldhacker/hcaptcha": "<2.1.2", "wallabag/tcpdf": "<6.2.22", - "wallabag/wallabag": "<2.6.7", + "wallabag/wallabag": "<2.6.11", "wanglelecc/laracms": "<=1.0.3", - "web-auth/webauthn-framework": ">=3.3,<3.3.4", + "wapplersystems/a21glossary": "<=0.4.10", + "web-auth/webauthn-framework": ">=3.3,<3.3.4|>=4.5,<4.9", + "web-auth/webauthn-lib": ">=4.5,<4.9", + "web-feet/coastercms": "==5.5", + "web-tp3/wec_map": "<3.0.3", "webbuilders-group/silverstripe-kapost-bridge": "<0.4", "webcoast/deferred-image-processing": "<1.0.2", "webklex/laravel-imap": "<5.3", @@ -1166,38 +2255,45 @@ "wikimedia/parsoid": "<0.12.2", "willdurand/js-translation-bundle": "<2.1.1", "winter/wn-backend-module": "<1.2.4", + "winter/wn-cms-module": "<1.0.476|>=1.1,<1.1.11|>=1.2,<1.2.7", + "winter/wn-dusk-plugin": "<2.1", "winter/wn-system-module": "<1.2.4", - "wintercms/winter": "<1.2.3", - "woocommerce/woocommerce": "<6.6", - "wp-cli/wp-cli": "<2.5", + "wintercms/winter": "<=1.2.3", + "wireui/wireui": "<1.19.3|>=2,<2.1.3", + "woocommerce/woocommerce": "<6.6|>=8.8,<8.8.5|>=8.9,<8.9.3", + "wp-cli/wp-cli": ">=0.12,<2.5", "wp-graphql/wp-graphql": "<=1.14.5", + "wp-premium/gravityforms": "<2.4.21", "wpanel/wpanel4-cms": "<=4.3.1", "wpcloud/wp-stateless": "<3.2", - "wwbn/avideo": "<=12.4", + "wpglobus/wpglobus": "<=1.9.6", + "wwbn/avideo": "<14.3", "xataface/xataface": "<3", "xpressengine/xpressengine": "<3.0.15", - "yeswiki/yeswiki": "<4.1", - "yetiforce/yetiforce-crm": "<=6.4", + "yab/quarx": "<2.4.5", + "yeswiki/yeswiki": "<4.5.4", + "yetiforce/yetiforce-crm": "<6.5", "yidashi/yii2cmf": "<=2", "yii2mod/yii2-cms": "<1.9.2", - "yiisoft/yii": "<1.1.29", - "yiisoft/yii2": "<2.0.38", + "yiisoft/yii": "<1.1.31", + "yiisoft/yii2": "<2.0.52", "yiisoft/yii2-authclient": "<2.2.15", "yiisoft/yii2-bootstrap": "<2.0.4", - "yiisoft/yii2-dev": "<2.0.43", + "yiisoft/yii2-dev": "<=2.0.45", "yiisoft/yii2-elasticsearch": "<2.0.5", "yiisoft/yii2-gii": "<=2.2.4", "yiisoft/yii2-jui": "<2.0.4", - "yiisoft/yii2-redis": "<2.0.8", + "yiisoft/yii2-redis": "<2.0.20", "yikesinc/yikes-inc-easy-mailchimp-extender": "<6.8.6", "yoast-seo-for-typo3/yoast_seo": "<7.2.3", "yourls/yourls": "<=1.8.2", + "yuan1994/tpadmin": "<=1.3.12", "zencart/zencart": "<=1.5.7.0-beta", "zendesk/zendesk_api_client_php": "<2.2.11", - "zendframework/zend-cache": "<2.4.8|>=2.5,<2.5.3", + "zendframework/zend-cache": ">=2.4,<2.4.8|>=2.5,<2.5.3", "zendframework/zend-captcha": ">=2,<2.4.9|>=2.5,<2.5.2", "zendframework/zend-crypt": ">=2,<2.4.9|>=2.5,<2.5.2", - "zendframework/zend-db": ">=2,<2.0.99|>=2.1,<2.1.99|>=2.2,<2.2.10|>=2.3,<2.3.5", + "zendframework/zend-db": "<2.2.10|>=2.3,<2.3.5", "zendframework/zend-developer-tools": ">=1.2.2,<1.2.3", "zendframework/zend-diactoros": "<1.8.4", "zendframework/zend-feed": "<2.10.3", @@ -1205,9 +2301,9 @@ "zendframework/zend-http": "<2.8.1", "zendframework/zend-json": ">=2.1,<2.1.6|>=2.2,<2.2.6", "zendframework/zend-ldap": ">=2,<2.0.99|>=2.1,<2.1.99|>=2.2,<2.2.8|>=2.3,<2.3.3", - "zendframework/zend-mail": ">=2,<2.4.11|>=2.5,<2.7.2", + "zendframework/zend-mail": "<2.4.11|>=2.5,<2.7.2", "zendframework/zend-navigation": ">=2,<2.2.7|>=2.3,<2.3.1", - "zendframework/zend-session": ">=2,<2.0.99|>=2.1,<2.1.99|>=2.2,<2.2.9|>=2.3,<2.3.4", + "zendframework/zend-session": ">=2,<2.2.9|>=2.3,<2.3.4", "zendframework/zend-validator": ">=2.3,<2.3.6", "zendframework/zend-view": ">=2,<2.2.7|>=2.3,<2.3.1", "zendframework/zend-xmlrpc": ">=2.1,<2.1.6|>=2.2,<2.2.6", @@ -1222,11 +2318,11 @@ "zendframework/zendservice-slideshare": "<2.0.2", "zendframework/zendservice-technorati": "<2.0.2", "zendframework/zendservice-windowsazure": "<2.0.2", - "zendframework/zendxml": "<1.0.1", + "zendframework/zendxml": ">=1,<1.0.1", "zenstruck/collection": "<0.2.1", "zetacomponents/mail": "<1.8.2", "zf-commons/zfc-user": "<1.2.2", - "zfcampus/zf-apigility-doctrine": "<1.0.3", + "zfcampus/zf-apigility-doctrine": ">=1,<1.0.3", "zfr/zfr-oauth2-server-module": "<0.1.2", "zoujingli/thinkadmin": "<=6.1.53" }, @@ -1266,20 +2362,20 @@ "type": "tidelift" } ], - "time": "2024-01-31T19:04:19+00:00" + "time": "2025-07-04T13:13:44+00:00" }, { "name": "squizlabs/php_codesniffer", - "version": "3.8.1", + "version": "3.13.2", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", - "reference": "14f5fff1e64118595db5408e946f3a22c75807f7" + "reference": "5b5e3821314f947dd040c70f7992a64eac89025c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/14f5fff1e64118595db5408e946f3a22c75807f7", - "reference": "14f5fff1e64118595db5408e946f3a22c75807f7", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/5b5e3821314f947dd040c70f7992a64eac89025c", + "reference": "5b5e3821314f947dd040c70f7992a64eac89025c", "shasum": "" }, "require": { @@ -1344,75 +2440,816 @@ { "url": "https://opencollective.com/php_codesniffer", "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" } ], - "time": "2024-01-11T20:47:48+00:00" + "time": "2025-06-17T22:17:01+00:00" }, { - "name": "wp-coding-standards/wpcs", - "version": "3.0.1", + "name": "swissspidy/phpstan-no-private", + "version": "v1.0.0", "source": { "type": "git", - "url": "https://github.com/WordPress/WordPress-Coding-Standards.git", - "reference": "b4caf9689f1a0e4a4c632679a44e638c1c67aff1" + "url": "https://github.com/swissspidy/phpstan-no-private.git", + "reference": "559cb0e8d092df7314ed4254db83db0427440af2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/WordPress/WordPress-Coding-Standards/zipball/b4caf9689f1a0e4a4c632679a44e638c1c67aff1", - "reference": "b4caf9689f1a0e4a4c632679a44e638c1c67aff1", + "url": "https://api.github.com/repos/swissspidy/phpstan-no-private/zipball/559cb0e8d092df7314ed4254db83db0427440af2", + "reference": "559cb0e8d092df7314ed4254db83db0427440af2", "shasum": "" }, "require": { - "ext-filter": "*", - "ext-libxml": "*", - "ext-tokenizer": "*", - "ext-xmlreader": "*", - "php": ">=5.4", - "phpcsstandards/phpcsextra": "^1.1.0", - "phpcsstandards/phpcsutils": "^1.0.8", - "squizlabs/php_codesniffer": "^3.7.2" + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^2.0" }, "require-dev": { - "php-parallel-lint/php-console-highlighter": "^1.0.0", - "php-parallel-lint/php-parallel-lint": "^1.3.2", - "phpcompatibility/php-compatibility": "^9.0", - "phpcsstandards/phpcsdevtools": "^1.2.0", - "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" + "dealerdirect/phpcodesniffer-composer-installer": "^1.0.0", + "nikic/php-parser": "^v5.3.1", + "php-parallel-lint/php-parallel-lint": "^1.4", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.5", + "slevomat/coding-standard": "^8.8.0", + "squizlabs/php_codesniffer": "^3.5.3" + }, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "rules.neon" + ] + } }, - "suggest": { - "ext-iconv": "For improved results", - "ext-mbstring": "For improved results" + "autoload": { + "psr-4": { + "Swissspidy\\PHPStan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan rules for detecting usage of pseudo-private functions, classes, and methods.", + "support": { + "issues": "https://github.com/swissspidy/phpstan-no-private/issues", + "source": "https://github.com/swissspidy/phpstan-no-private/tree/v1.0.0" + }, + "time": "2024-11-11T11:04:45+00:00" + }, + { + "name": "symfony/config", + "version": "v7.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/config.git", + "reference": "ba62ae565f1327c2f6366726312ed828c85853bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/config/zipball/ba62ae565f1327c2f6366726312ed828c85853bc", + "reference": "ba62ae565f1327c2f6366726312ed828c85853bc", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/filesystem": "^7.1", + "symfony/polyfill-ctype": "~1.8" + }, + "conflict": { + "symfony/finder": "<6.4", + "symfony/service-contracts": "<2.5" + }, + "require-dev": { + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/finder": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/yaml": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Config\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] }, - "type": "phpcodesniffer-standard", "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], "authors": [ { - "name": "Contributors", - "homepage": "https://github.com/WordPress/WordPress-Coding-Standards/graphs/contributors" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "PHP_CodeSniffer rules (sniffs) to enforce WordPress coding conventions", - "keywords": [ - "phpcs", - "standards", - "static analysis", - "wordpress" - ], + "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", + "homepage": "https://symfony.com", "support": { - "issues": "https://github.com/WordPress/WordPress-Coding-Standards/issues", - "source": "https://github.com/WordPress/WordPress-Coding-Standards", - "wiki": "https://github.com/WordPress/WordPress-Coding-Standards/wiki" + "source": "https://github.com/symfony/config/tree/v7.3.0" }, "funding": [ { - "url": "https://opencollective.com/thewpcc/contribute/wp-php-63406", + "url": "https://symfony.com/sponsor", "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-05-15T09:04:05+00:00" + }, + { + "name": "symfony/dependency-injection", + "version": "v7.3.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/dependency-injection.git", + "reference": "8656c4848b48784c4bb8c4ae50d2b43f832cead8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/8656c4848b48784c4bb8c4ae50d2b43f832cead8", + "reference": "8656c4848b48784c4bb8c4ae50d2b43f832cead8", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/service-contracts": "^3.5", + "symfony/var-exporter": "^6.4.20|^7.2.5" + }, + "conflict": { + "ext-psr": "<1.1|>=2", + "symfony/config": "<6.4", + "symfony/finder": "<6.4", + "symfony/yaml": "<6.4" + }, + "provide": { + "psr/container-implementation": "1.1|2.0", + "symfony/service-implementation": "1.1|2.0|3.0" + }, + "require-dev": { + "symfony/config": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/yaml": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\DependencyInjection\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows you to standardize and centralize the way objects are constructed in your application", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/dependency-injection/tree/v7.3.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-24T04:04:43+00:00" + }, + { + "name": "symfony/filesystem", + "version": "v7.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "b8dce482de9d7c9fe2891155035a7248ab5c7fdb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/b8dce482de9d7c9fe2891155035a7248ab5c7fdb", + "reference": "b8dce482de9d7c9fe2891155035a7248ab5c7fdb", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" + }, + "require-dev": { + "symfony/process": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides basic utilities for the filesystem", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v7.3.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-10-25T15:15:23+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-23T08:48:59+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f021b05a130d35510bd6b25fe9053c2a8a15d5d4", + "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-04-25T09:37:31+00:00" + }, + { + "name": "symfony/var-exporter", + "version": "v7.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-exporter.git", + "reference": "c9a1168891b5aaadfd6332ef44393330b3498c4c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/c9a1168891b5aaadfd6332ef44393330b3498c4c", + "reference": "c9a1168891b5aaadfd6332ef44393330b3498c4c", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "require-dev": { + "symfony/property-access": "^6.4|^7.0", + "symfony/serializer": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\VarExporter\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows exporting any serializable PHP data structure to plain PHP code", + "homepage": "https://symfony.com", + "keywords": [ + "clone", + "construct", + "export", + "hydrate", + "instantiate", + "lazy-loading", + "proxy", + "serialize" + ], + "support": { + "source": "https://github.com/symfony/var-exporter/tree/v7.3.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-05-15T09:04:05+00:00" + }, + { + "name": "szepeviktor/phpstan-wordpress", + "version": "v2.0.2", + "source": { + "type": "git", + "url": "https://github.com/szepeviktor/phpstan-wordpress.git", + "reference": "963887b04c21fe7ac78e61c1351f8b00fff9f8f8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/szepeviktor/phpstan-wordpress/zipball/963887b04c21fe7ac78e61c1351f8b00fff9f8f8", + "reference": "963887b04c21fe7ac78e61c1351f8b00fff9f8f8", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "php-stubs/wordpress-stubs": "^6.6.2", + "phpstan/phpstan": "^2.0" + }, + "require-dev": { + "composer/composer": "^2.1.14", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.1", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.0", + "szepeviktor/phpcs-psr-12-neutron-hybrid-ruleset": "^1.0", + "wp-coding-standards/wpcs": "3.1.0 as 2.3.0" + }, + "suggest": { + "swissspidy/phpstan-no-private": "Detect usage of internal core functions, classes and methods" + }, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + } + }, + "autoload": { + "psr-4": { + "SzepeViktor\\PHPStan\\WordPress\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "WordPress extensions for PHPStan", + "keywords": [ + "PHPStan", + "code analyse", + "code analysis", + "static analysis", + "wordpress" + ], + "support": { + "issues": "https://github.com/szepeviktor/phpstan-wordpress/issues", + "source": "https://github.com/szepeviktor/phpstan-wordpress/tree/v2.0.2" + }, + "time": "2025-02-12T18:43:37+00:00" + }, + { + "name": "wp-coding-standards/wpcs", + "version": "3.1.0", + "source": { + "type": "git", + "url": "https://github.com/WordPress/WordPress-Coding-Standards.git", + "reference": "9333efcbff231f10dfd9c56bb7b65818b4733ca7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/WordPress/WordPress-Coding-Standards/zipball/9333efcbff231f10dfd9c56bb7b65818b4733ca7", + "reference": "9333efcbff231f10dfd9c56bb7b65818b4733ca7", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "ext-libxml": "*", + "ext-tokenizer": "*", + "ext-xmlreader": "*", + "php": ">=5.4", + "phpcsstandards/phpcsextra": "^1.2.1", + "phpcsstandards/phpcsutils": "^1.0.10", + "squizlabs/php_codesniffer": "^3.9.0" + }, + "require-dev": { + "php-parallel-lint/php-console-highlighter": "^1.0.0", + "php-parallel-lint/php-parallel-lint": "^1.3.2", + "phpcompatibility/php-compatibility": "^9.0", + "phpcsstandards/phpcsdevtools": "^1.2.0", + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.0" + }, + "suggest": { + "ext-iconv": "For improved results", + "ext-mbstring": "For improved results" + }, + "type": "phpcodesniffer-standard", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Contributors", + "homepage": "https://github.com/WordPress/WordPress-Coding-Standards/graphs/contributors" + } + ], + "description": "PHP_CodeSniffer rules (sniffs) to enforce WordPress coding conventions", + "keywords": [ + "phpcs", + "standards", + "static analysis", + "wordpress" + ], + "support": { + "issues": "https://github.com/WordPress/WordPress-Coding-Standards/issues", + "source": "https://github.com/WordPress/WordPress-Coding-Standards", + "wiki": "https://github.com/WordPress/WordPress-Coding-Standards/wiki" + }, + "funding": [ + { + "url": "https://opencollective.com/php_codesniffer", + "type": "custom" + } + ], + "time": "2024-03-25T16:39:00+00:00" + }, + { + "name": "wp-hooks/wordpress-core", + "version": "1.10.0", + "source": { + "type": "git", + "url": "https://github.com/wp-hooks/wordpress-core-hooks.git", + "reference": "127af21a918a52bcead7ce9b743b17b5d64eb148" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/wp-hooks/wordpress-core-hooks/zipball/127af21a918a52bcead7ce9b743b17b5d64eb148", + "reference": "127af21a918a52bcead7ce9b743b17b5d64eb148", + "shasum": "" + }, + "replace": { + "johnbillion/wp-hooks": "*" + }, + "require-dev": { + "erusev/parsedown": "1.8.0-beta-7", + "oomphinc/composer-installers-extender": "^2", + "roots/wordpress-core-installer": "^1.0.0", + "roots/wordpress-full": "6.8", + "wp-hooks/generator": "1.0.0" + }, + "type": "library", + "extra": { + "wp-hooks": { + "ignore-files": [ + "wp-admin/includes/deprecated.php", + "wp-admin/includes/ms-deprecated.php", + "wp-content/", + "wp-includes/deprecated.php", + "wp-includes/ID3/", + "wp-includes/ms-deprecated.php", + "wp-includes/pomo/", + "wp-includes/random_compat/", + "wp-includes/Requests/", + "wp-includes/SimplePie/", + "wp-includes/sodium_compat/", + "wp-includes/Text/" + ], + "ignore-hooks": [ + "load-categories.php", + "load-edit-link-categories.php", + "load-edit-tags.php", + "load-page-new.php", + "load-page.php", + "option_enable_xmlrpc", + "edit_post_{$field}", + "pre_post_{$field}", + "post_{$field}", + "pre_option_enable_xmlrpc", + "$page_hook", + "$hook", + "$hook_name" + ] + }, + "wordpress-install-dir": "vendor/wordpress/wordpress" + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-3.0-or-later" + ], + "authors": [ + { + "name": "John Blackbourn", + "homepage": "https://johnblackbourn.com/" + } + ], + "description": "All the actions and filters from WordPress core in machine-readable JSON format.", + "support": { + "issues": "https://github.com/wp-hooks/wordpress-core-hooks/issues", + "source": "https://github.com/wp-hooks/wordpress-core-hooks/tree/1.10.0" + }, + "funding": [ + { + "url": "https://github.com/sponsors/johnbillion", + "type": "github" } ], - "time": "2023-09-14T07:06:09+00:00" + "time": "2025-04-16T22:20:41+00:00" } ], "aliases": [], @@ -1421,9 +3258,9 @@ "a8cteam51/team51-configs": 20, "roave/security-advisories": 20 }, - "prefer-stable": true, + "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [], + "platform": {}, + "platform-dev": {}, "plugin-api-version": "2.6.0" } diff --git a/package.json b/package.json index 91d68ad..049a2f7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wpcomsp-simple-events", - "version": "1.0.55", + "version": "2.0.0", "description": "A simple Gutenberg-first event management plugin that integrates with WooCommerce Box Office.", "author": { "name": "WordPress.com Special Projects Team", diff --git a/plugin.php b/plugin.php index 7fdb5ee..81df0b0 100644 --- a/plugin.php +++ b/plugin.php @@ -3,7 +3,7 @@ * Simple Events Plugin bootstrap file. * * @since 1.0.0 - * @version 1.0.55 + * @version 2.0.0-RC1 * @author WordPress.com Special Projects * @license GPL-3.0-or-later * @@ -13,7 +13,7 @@ * Description: Event management frontend for WooCommerce Box Office. * Requires at least: 6.2 * Tested up to: 6.4 - * Version: 1.0.55 + * Version: 2.0.0-RC1 * Requires PHP: 8.0 * Author: WordPress.com Special Projects * Author URI: https://wpspecialprojects.wordpress.com @@ -31,13 +31,17 @@ function_exists( 'get_plugin_data' ) || require_once ABSPATH . 'wp-admin/includes/plugin.php'; define( 'SE_METADATA', get_plugin_data( __FILE__, false, false ) ); -define( 'SE_VERSION', '1.0.55' ); +define( 'SE_VERSION', '2.0.0' ); define( 'SE_BASENAME', plugin_basename( __FILE__ ) ); define( 'SE_PLUGIN_DIR', untrailingslashit( plugin_dir_path( __FILE__ ) ) ); define( 'SE_PLUGIN_URL', untrailingslashit( plugin_dir_url( __FILE__ ) ) ); define( 'SE_SRC_PATH', untrailingslashit( SE_PLUGIN_DIR . '/src' ) ); define( 'SE_TEMPLATE_PATH', untrailingslashit( SE_SRC_PATH . '/templates' ) ); +// This should only be updated if there are changes to the way we handle dates and there are migration method to handle. +// This is used to determine if we need to run migrations. +define( 'SE_MIGRATION_VERSION', '2.0.0' ); + // Load the autoloader. if ( ! is_file( SE_PLUGIN_DIR . '/vendor/autoload.php' ) ) { add_action( @@ -52,20 +56,6 @@ static function () { } require_once SE_PLUGIN_DIR . '/vendor/autoload.php'; -// Initialize the plugin if system requirements check out. -$se_requirements = validate_plugin_requirements( SE_BASENAME ); -define( 'SE_REQUIREMENTS', $se_requirements ); - -if ( $se_requirements instanceof WP_Error ) { - add_action( - 'admin_notices', - static function () use ( $se_requirements ) { - $html_message = wp_sprintf( '
%s
', $se_requirements->get_error_message() ); - echo wp_kses_post( $html_message ); - } - ); - return; -} require_once SE_SRC_PATH . '/classes/class-se-event-post-type.php'; require_once SE_SRC_PATH . '/classes/class-se-blocks.php'; @@ -76,6 +66,9 @@ static function () use ( $se_requirements ) { require_once SE_SRC_PATH . '/classes/class-se-calendar-export.php'; require_once SE_SRC_PATH . '/classes/class-se-calendar.php'; require_once SE_SRC_PATH . '/classes/class-se-event-query-dates.php'; +require_once SE_SRC_PATH . '/classes/class-se-event-dates.php'; +require_once SE_SRC_PATH . '/classes/class-date-display-formatter.php'; +require_once SE_SRC_PATH . '/classes/class-se-migrate-events.php'; require_once SE_SRC_PATH . '/calendar-functions.php'; require_once SE_SRC_PATH . '/event-functions.php'; @@ -85,6 +78,7 @@ static function () use ( $se_requirements ) { require_once SE_SRC_PATH . '/rest-api.php'; require_once SE_SRC_PATH . '/back-compat.php'; + /** * Add a flag to leverage for flushing rewrite rules. * diff --git a/simple-events.zip b/simple-events.zip new file mode 100644 index 0000000..a5153f8 Binary files /dev/null and b/simple-events.zip differ diff --git a/src/assets/js/admin.js b/src/assets/js/admin.js index 1aa6c57..581a809 100644 --- a/src/assets/js/admin.js +++ b/src/assets/js/admin.js @@ -4,55 +4,269 @@ * @package simple-events */ -jQuery( document ).ready( function( $ ) { - +jQuery(document).ready(function ($) { + // Handle Migrate Events. + $('#se_migrate_events_btn').on('click', function () { + startMigrationProcess(); + }); + + /** + * Start the migration process and continue until all events are processed + */ + function startMigrationProcess() { + // Show warning notice and disable button + showMigrationNotice(); + $('#se_migrate_events_btn').prop('disabled', true); + + // Start processing batches + processMigrationBatch(); + } + + /** + * Show the migration warning notice + */ + function showMigrationNotice() { + // Remove existing notice if any + $('#se_migration_notice').remove(); + + // Create and insert the notice + const notice = ` +
+
+ ⚠️ + MIGRATION IN PROGRESS - DO NOT CLOSE YOUR BROWSER +
+

+ Please keep this page open while events are being migrated. Closing the browser will interrupt the process. +

+
+ `; + + $('#se_migrate_events_wrapper').before(notice); + } + + /** + * Hide the migration warning notice + */ + function hideMigrationNotice() { + $('#se_migration_notice').fadeOut(300, function () { + $(this).remove(); + }); + // Keep the button disabled - no retry functionality + $('#se_migrate_events_btn').prop('disabled', true); + } + + /** + * Hide the migration warning notice but keep button disabled (for completion) + */ + function hideMigrationNoticeCompleted() { + $('#se_migration_notice').fadeOut(300, function () { + $(this).remove(); + }); + // Keep the button disabled when migration is completed + $('#se_migrate_events_btn').prop('disabled', true); + } + + /** + * Process a single batch of migration events + */ + function processMigrationBatch() { + // Track events processed. + const perBatch = 24; + + // Get the next events that are still pending + const nextEvents = $('#se_migrate_events_wrapper .se_migrate_event').filter('[data-status="pending"]').slice(0, perBatch); + + // If no more events to process, we're done + if (nextEvents.length === 0) { + hideMigrationNoticeCompleted(); + console.log('Migration completed - all events processed!'); + return; + } + + // Get all the event ids. + const eventIds = nextEvents.map(function () { + return $(this).data('event-id'); + }); + + // Set events to processing status before making the request + nextEvents.each(function () { + const eventElement = $(this); + eventElement.attr('data-status', 'processing'); + const statusElement = eventElement.find('.se_migrate_event_status'); + statusElement.css({ + 'background': '#007cba', + 'color': '#fff' + }).text('Processing...'); + }); + + // Make REST API call to migrate events + $.ajax({ + url: window.location.origin + '/wp-json/simple-events/migrate-events', + type: 'POST', + data: { + events: JSON.stringify(eventIds.get()) + }, + beforeSend: function (xhr) { + xhr.setRequestHeader('X-WP-Nonce', seAdmin.nonce); + }, + xhrFields: { + withCredentials: true // SEND cookies! + }, + success: function (response) { + // Update the status of processed events + if (response.data) { + Object.keys(response.data).forEach(function (eventId) { + const resultData = response.data[eventId]; + const eventElement = $(`[data-event-id="${eventId}"]`); + const statusElement = eventElement.find('.se_migrate_event_status'); + const versionElement = eventElement.find('span[style*="monospace"]'); + + // Handle the new response format + if (typeof resultData === 'object' && resultData !== null) { + const success = resultData.success; + const version = resultData.version; + + // Update status + eventElement.attr('data-status', success ? 'completed' : 'error'); + + if (success) { + statusElement.css({ + 'background': '#00a32a', + 'color': '#fff' + }).text('Completed'); + } else { + statusElement.css({ + 'background': '#d63638', + 'color': '#fff' + }).text('Error'); + } + + // Update version if provided + if (version && versionElement.length) { + versionElement.text('v' + version); + } + } else { + // Fallback for old response format (boolean) + const success = Boolean(resultData); + eventElement.attr('data-status', success ? 'completed' : 'error'); + + if (success) { + statusElement.css({ + 'background': '#00a32a', + 'color': '#fff' + }).text('Completed'); + } else { + statusElement.css({ + 'background': '#d63638', + 'color': '#fff' + }).text('Error'); + } + } + }); + } + + // Process the next batch after a short delay + setTimeout(function () { + processMigrationBatch(); + }, 500); + }, + error: function (xhr, status, error) { + console.error('Migration failed:', error); + // Reset processing events back to pending on error + nextEvents.each(function () { + const eventElement = $(this); + if (eventElement.attr('data-status') === 'processing') { + eventElement.attr('data-status', 'pending'); + const statusElement = eventElement.find('.se_migrate_event_status'); + statusElement.css({ + 'background': '#ffc107', + 'color': '#856404' + }).text('Pending'); + } + }); + + // Hide notice and re-enable button on error + hideMigrationNotice(); + } + }); + } + // Handle Ticket Only Order Completion.. - $( '#se_ajax_btn' ).on( 'click', function() { + $('#se_ajax_btn').on('click', function () { // Disable button and extract action. - $( this ).prop( 'disabled', true ); - const action = $( this ).data( 'action' ); - + $(this).prop('disabled', true); + const action = $(this).data('action'); + // Perform AJAX request. - $.ajax( { + $.ajax({ url: ajaxurl, type: 'POST', data: { action: action, }, - success: function( response ) { - $( '#se_ajax_response' ).html( `

${response?.data}

` ); + success: function (response) { + $('#se_ajax_response').html(`

${response?.data}

`); }, - error: function() { - $( '#se_ajax_response' ).html( '

Something went wrong!

' ); + error: function () { + $('#se_ajax_response').html('

Something went wrong!

'); }, - complete: function() { - $( '#se_ajax_btn' ).prop( 'disabled', false ); - setTimeout( () => { - $( '#se_ajax_response' ).html( '' ); - }, 2000 ); + complete: function () { + $('#se_ajax_btn').prop('disabled', false); + setTimeout(() => { + $('#se_ajax_response').html(''); + }, 2000); }, - } ); - } ); + }); + }); + + // Handle Clear Orphaned Events button + $('#se_clear_orphaned_btn').on('click', function () { + // Disable button and extract action. + $(this).prop('disabled', true); + const action = $(this).data('action'); + + // Perform AJAX request. + $.ajax({ + url: ajaxurl, + type: 'POST', + data: { + action: action, + }, + success: function (response) { + $('#se_clear_orphaned_response').html(`

${response?.data}

`); + }, + error: function () { + $('#se_clear_orphaned_response').html('

Something went wrong!

'); + }, + complete: function () { + $('#se_clear_orphaned_btn').prop('disabled', false); + setTimeout(() => { + $('#se_clear_orphaned_response').html(''); + }, 2000); + }, + }); + }); // Handle Skip Cart and Empty Cart options. - const skipCart = $( 'input[name="se_options[skip_cart]"]' ); - const emptyCartBeforeAddingTickets = $( 'input[name="se_options[empty_cart_before_adding_tickets]"]' ); + const skipCart = $('input[name="se_options[skip_cart]"]'); + const emptyCartBeforeAddingTickets = $('input[name="se_options[empty_cart_before_adding_tickets]"]'); // If Skip Cart is not enabled, disable Empty Cart Before Adding Tickets. - $( window ).on( 'load', function () { - if( skipCart && ! skipCart.is( ':checked' ) ) { - emptyCartBeforeAddingTickets.prop( 'checked', false ); - emptyCartBeforeAddingTickets.closest( 'tr' ).hide(); + $(window).on('load', function () { + if (skipCart && !skipCart.is(':checked')) { + emptyCartBeforeAddingTickets.prop('checked', false); + emptyCartBeforeAddingTickets.closest('tr').hide(); } - } ); - + }); + // Handle Skip Cart option change. - skipCart.on( 'input', function() { - if( $( this ).is( ':checked' ) ) { - emptyCartBeforeAddingTickets.closest( 'tr' ).show(); + skipCart.on('input', function () { + if ($(this).is(':checked')) { + emptyCartBeforeAddingTickets.closest('tr').show(); } else { - emptyCartBeforeAddingTickets.prop( 'checked', false ); - emptyCartBeforeAddingTickets.closest( 'tr' ).hide(); + emptyCartBeforeAddingTickets.prop('checked', false); + emptyCartBeforeAddingTickets.closest('tr').hide(); } - } ); -} ); + }); +}); diff --git a/src/back-compat.php b/src/back-compat.php index 6f50ea2..9b5e06e 100644 --- a/src/back-compat.php +++ b/src/back-compat.php @@ -26,3 +26,38 @@ function se_pre_gutenberg_14_3_0_compat( $classes ) { } add_filter( 'admin_body_class', 'se_pre_gutenberg_14_3_0_compat' ); + +/** + * Adds an admin notice to say we have events that need to be migrated. + * + * @return void + */ +function se_admin_notice_events_to_migrate() { + if ( ! current_user_can( 'manage_options' ) ) { + return; + } + + // Check if we have events that need to be migrated. + if ( SE_Migrate_Events::has_events_to_migrate() ) { + // Lets make the error large so it cant be ingnored with a link to the settings page. + + ?> +
+

+ ⚠️ +

+

+ +

+

+ + + +

+
+ { + const [tempEventDate, setTempEventDate] = useState(null); + const [tempEventTime, setTempEventTime] = useState(null); + // Add local state to track the current eventDateTime + const [currentEventDateTime, setCurrentEventDateTime] = useState(eventDateTime); + // Add state to track if this date has been removed + const [isRemoved, setIsRemoved] = useState(false); + + // Update local state when eventDateTime prop changes (e.g., timezone update) + useEffect(() => { + setCurrentEventDateTime(eventDateTime); + }, [eventDateTime]); + + + const eventStart = getMoment( + currentEventDateTime.start_date, + true, + currentTimezone + ); + const eventEnd = getMoment(currentEventDateTime.end_date, true, currentTimezone); + const timeFormat = DATE_SETTINGS.formats.datetime; + + /** + * Handle the set date and time button click. + * + * @param {boolean} isStartChange Whether this is start or end date change. + * + * @return {void} + */ + const setDateTimeHandler = (isStartChange, onClose) => { + // Ensure we have either new date or time in state. + if (!tempEventDate && !tempEventTime) { + return; + } + + const newDate = + tempEventDate || + (isStartChange ? eventStart : eventEnd); + const newTime = + tempEventTime || + (isStartChange ? eventStart : eventEnd); + + // Combine the new date and time and convert to a timestamp. + const newDateTime = getTimestamp( + combineDateAndTime(newDate, newTime), + currentTimezone + ); + + const newEventDateTime = clone(currentEventDateTime); + + if (isStartChange) { + newEventDateTime.start_date = newDateTime; + + // Check if the new start time is after the current end time. + if ( + parseInt(newEventDateTime.start_date) >= + parseInt(newEventDateTime.end_date) + ) { + // Set the new end time to be 1 hour after the start dateTime. + newEventDateTime.end_date = String( + parseInt(newEventDateTime.start_date) + + 3600 + ); + } + } else { + newEventDateTime.end_date = newDateTime; + + // Check if the new end time is before the current start time. + if ( + parseInt(newEventDateTime.start_date) >= + parseInt(newEventDateTime.end_date) + ) { + // Set the new start time to be 1 hour before the end dateTime. + newEventDateTime.start_date = String( + parseInt(newEventDateTime.end_date) - 3600 + ); + } + } + + // Reset the temp date and time. + setTempEventDate(null); + setTempEventTime(null); + + // Use dateManagerInstance to save the changes if available + if (dateManagerInstance && dateManagerInstance.upsertDate) { + dateManagerInstance.upsertDate(newEventDateTime); + } + + // Update the current eventDateTime state + setCurrentEventDateTime(newEventDateTime); + + // Close the appropriate dropdown + onClose(); + }; + + /** + * Handles DateTimePicker changes. + * + * @param {string} currentDateTime The current dateTime. + * @param {string} newDateTime The new selected dateTime. + * + * @return {void} + */ + const datePickerHandler = (currentDateTime, newDateTime) => { + // Compare the date without time to see if the time or date was changed. + const isDateChange = + moment(currentDateTime).format('YYYY-MM-DD') === + moment(newDateTime).format('YYYY-MM-DD'); + + if (isDateChange) { + setTempEventTime(newDateTime); + } else { + setTempEventDate(newDateTime); + } + }; + + return ( +
+ {hasMultipleDates && ( +
+
+ )} +
+ + ( + + )} + renderContent={({ onClose }) => ( + + + datePickerHandler( + eventStart, + newDateTime + ) + } + __nextRemoveHelpButton + __nextRemoveResetButton + /> + + + )} + /> + + + ( + + )} + renderContent={({ onClose }) => ( + + + datePickerHandler( + eventEnd, + newDateTime + ) + } + __nextRemoveHelpButton + __nextRemoveResetButton + /> +
+
+ ); +}; + +export default DateTimeGroupNew; diff --git a/src/blocks/event-info/date-utils.js b/src/blocks/event-info/date-utils.js new file mode 100644 index 0000000..6ec691e --- /dev/null +++ b/src/blocks/event-info/date-utils.js @@ -0,0 +1,314 @@ +import moment from 'moment'; +import { getSettings } from '@wordpress/date'; +import { __ } from '@wordpress/i18n'; +import { head, last } from 'lodash'; + +/** + * Date and Time Utilities + * + * Utility functions for handling date/time operations, timezone conversions, + * and date formatting within the Simple Events plugin. Provides consistent + * date handling across the event management system. + * + * @package SimpleEvents + * @since 1.0.0 + */ + +/** + * Date/Time Constants + */ +export const DEFAULT_START_HOUR = 9; +export const DEFAULT_END_HOUR = 10; +export const FORMAT = 'YYYY-MM-DD HH:mm'; + +const DATE_SETTINGS = getSettings(); +export const OFFSET = Number(DATE_SETTINGS.timezone.offset); +export const TIMEZONE = DATE_SETTINGS.timezone.string; + +export let TIMEZONE_NAME = TIMEZONE; +if ('' === TIMEZONE) { + TIMEZONE_NAME = 'UTC' + (OFFSET >= 0 ? '+' : '') + OFFSET; +} + +export const TIMEZONES = moment.tz + .names() + .map((tz) => ({ label: tz, value: tz })); + +// Add an option to use the site settings +TIMEZONES.unshift({ + label: __('Same as site', 'simple-events'), + value: '', +}); + +/** + * Gets the DST offset for a given timestamp and timezone. + * + * Calculates the daylight saving time offset for a specific timestamp + * within a given timezone. Handles timezone conversions and DST transitions. + * + * @since 1.0.0 + * + * @param {number} timestamp The timestamp to check. + * @param {string|null} timezone The timezone to check (defaults to current timezone). + * @param {string} currentTimezone The current event timezone. + * @return {number} The offset in minutes. + */ +export const getDstOffset = (timestamp, timezone = null, currentTimezone = TIMEZONE) => { + // Return no offset if the event timezone is the same as the site. + if (null === timezone) { + timezone = currentTimezone; + } + + if ('' === timezone) { + return OFFSET; + } + + // Get the timezone details. + const timezoneDetails = moment.tz.zone(timezone); + + // Get the index of the current timezone offset i.e DST or non-DST. -1 at the end to account for search algorithm. + const untilIndex = timezoneDetails.untils.findIndex(function (number) { + return number / 1000 > timestamp; + }); + + return timezoneDetails.offsets[untilIndex] * -1; +}; + +/** + * Creates a moment object in the site timezone from a unix timestamp. + * + * Converts a unix timestamp to a moment object using the appropriate + * timezone offset. Can optionally return a formatted string instead. + * + * @since 1.0.0 + * + * @param {string} timestamp Timestamp to convert to a moment. + * @param {boolean} formatted Whether to return a human-readable formatted string. + * @param {string} currentTimezone The current timezone for the event. + * @return {moment.Moment|string} Human readable formatted string if `formatted` is true, moment object otherwise. + */ +export const getMoment = (timestamp, formatted = false, currentTimezone = TIMEZONE) => { + const dateTime = moment + .unix(timestamp) + .utcOffset(getDstOffset(timestamp, null, currentTimezone)); + + if (!formatted) { + return dateTime; + } + + return dateTime.format(FORMAT); +}; + +/** + * Creates a timestamp from a date string. + * + * Converts a date string to a unix timestamp, applying the appropriate + * timezone offset for accurate time representation. + * + * @since 1.0.0 + * + * @param {string} dateTime Date string to convert to a timestamp. + * @param {string} currentTimezone The current timezone for the event. + * @return {string} The timestamp, cast as a string. + */ +export const getTimestamp = (dateTime, currentTimezone = TIMEZONE) => { + return String( + moment(dateTime) + .utcOffset( + getDstOffset(moment(dateTime).unix(), null, currentTimezone), + true + ) + .utc() + .unix() + ); +}; + +/** + * Gets the start and end date from a collection of dates. + * + * Analyzes a collection of event dates to determine the overall start and end + * times, filtering out dates that have already passed. Returns the earliest + * start date and latest end date from valid future dates. + * + * @since 1.0.0 + * + * @param {Array} dates Array of date objects with all_day, start_date, and end_date properties. + * @return {Object} Object with start_date and end_date properties (strings or null). + */ +export const getStartAndEndDate = (dates) => { + // iterate over and remove any that has passed. + const now = moment().utcOffset(OFFSET); + const filteredDates = dates.filter((date) => { + const endDate = moment.unix(date.end_date).utcOffset(OFFSET); + return endDate.isAfter(now); + }); + + /** + * Gets the first and last date from the collection. + * + * Helper closure that extracts the earliest start date and + * latest end date from the full date collection. + * + * @since 1.0.0 + * + * @return {Object} Object with start_date and end_date properties. + */ + const getFirstAndLastDate = () => { + return { + start_date: moment.unix(head(dates).start_date).utcOffset(OFFSET).unix().toString(), + end_date: moment.unix(last(dates).end_date).utcOffset(OFFSET).unix().toString(), + }; + }; + + let startDate = null; + let endDate = null; + + // Do not trust the order of the dates. + if (filteredDates.length === 0) { + // Return the earliest start date and the latest end date. + return getFirstAndLastDate(); + } + + // Loop over the dates and set the start date as the earliest and the end as the latest. + filteredDates.forEach((date) => { + const startDateMoment = moment.unix(date.start_date).utcOffset(OFFSET); + const endDateMoment = moment.unix(date.end_date).utcOffset(OFFSET); + + // If the end date has passed, skip it. + if (endDateMoment.isBefore(now)) { + return; + } + + /** + * Sets the start or end date based on comparison logic. + * + * Helper closure for determining and setting the earliest start date + * and latest end date from the filtered date collection. + * + * @since 1.0.0 + * + * @param {moment.Moment} startDateMoment The start date moment to evaluate. + * @param {moment.Moment} endDateMoment The end date moment to evaluate. + */ + const setDate = (startDateMoment, endDateMoment) => { + // If the start date is before the current start date, set it. + if (!startDate || startDateMoment.isBefore(startDate) || (startDate.isAfter(startDateMoment) && startDate.isBefore(now))) { + startDate = startDateMoment; + } + + // If the end date is after the current end date, set it. + if (!endDate || endDateMoment.isAfter(endDate)) { + endDate = endDateMoment; + } + }; + + // If the start date if after now + if (startDateMoment.isAfter(now) && endDateMoment.isAfter(now)) { + setDate(startDateMoment, endDateMoment); + } else if (startDateMoment.isBefore(now) && endDateMoment.isAfter(now)) { + setDate(startDateMoment, endDateMoment); + } + }); + + // If we have no startDate or endDate, just get the first from dates. + if (!startDate) { + startDate = moment.unix(head(filteredDates).start_date).utcOffset(OFFSET); + } + if (!endDate) { + endDate = moment.unix(last(filteredDates).end_date).utcOffset(OFFSET); + } + return { + start_date: startDate.unix().toString(), + end_date: endDate.unix().toString(), + }; +}; + +/** + * Creates a default date object for new events. + * + * Generates a new event date object with sensible defaults based on + * existing dates and timezone. Sets the new date to be one day after + * the last existing date, or uses current time with default hours. + * + * @since 2.0.0 + * + * @param {Array} existingDates Array of existing date objects. + * @param {string} currentTimezone The current timezone identifier. + * @return {Object} New date object with start_date, end_date, and flag properties. + */ +export const createDefaultDate = (existingDates = [], currentTimezone = TIMEZONE) => { + // Set default date and time. + let eventStart = moment().utcOffset(OFFSET); + eventStart.hour(DEFAULT_START_HOUR); + eventStart.minute(0); + eventStart.second(0); + + let eventEnd = eventStart.clone(); + eventEnd.hour(DEFAULT_END_HOUR); + + // Override with existing date if there is one. + if (existingDates.length) { + eventStart = getMoment(last(existingDates).start_date, false, currentTimezone); + eventEnd = getMoment(last(existingDates).end_date, false, currentTimezone); + } + + // Set default date to be +1 day from the last date. + eventStart.add(1, 'days'); + eventEnd.add(1, 'days'); + + return { + start_date: wp.date.date('U', eventStart), + end_date: wp.date.date('U', eventEnd), + all_day: false, + hide_from_calendar: false, + hide_from_feed: false, + }; +}; + +/** + * Combines a date and time into a moment object. + * + * Takes separate date and time strings and combines them into a single + * moment object, preserving the date from the first parameter and the + * time from the second parameter. + * + * @since 1.0.0 + * + * @param {string} date The date string to use. + * @param {string} time The time string to use. + * @return {moment.Moment} The combined date and time as a moment object. + */ +export const combineDateAndTime = (date, time) => { + const timeMoment = moment(time); + const dateMoment = moment(date); + + // Set the timeMoment's time to the dateMoment. + return dateMoment.set({ + hour: timeMoment.get('hour'), + minute: timeMoment.get('minute'), + }); +}; + +/** + * Checks if the current time format is 12-hour. + * + * Analyzes the WordPress date format settings to determine if the + * site is configured to use 12-hour time format (with AM/PM indicators). + * + * @since 2.0.0 + * + * @return {boolean} True if 12-hour format is used, false otherwise. + */ +export const is12HourTime = () => { + const timeFormat = DATE_SETTINGS.formats.datetime; + // To know if the current timezone is a 12 hour time with look for an "a" in the time format. + // We also make sure this a is not escaped by a "/". + return /a(?!\\)/i.test( + timeFormat + .toLowerCase() // Test only the lower case a + .replace(/\\\\/g, '') // Replace "//" with empty strings + .split('') + .reverse() + .join('') // Reverse the string and test for "a" not followed by a slash + ); +}; diff --git a/src/blocks/event-info/editor.scss b/src/blocks/event-info/editor.scss index 222e043..ce11bbe 100644 --- a/src/blocks/event-info/editor.scss +++ b/src/blocks/event-info/editor.scss @@ -6,197 +6,187 @@ */ .wp-block-simple-events-event-info { - .components-placeholder__fieldset { - flex-direction: column; - } - - .components-base-control { - margin: 0 0 8px; - text-align: left; - - &:last-of-type { - margin-bottom: 0; - - .components-base-control__field { - white-space: nowrap; - } - - .components-checkbox-control__input-container { - align-content: center; - align-items: center; - display: inline-flex; - height: 33px; - position: relative; - - .components-checkbox-control__input { - height: 20px; - min-width: 20px; - } - } - } - } - - .components-base-control__label { - display: block; - } + .components-placeholder__fieldset { + flex-direction: column; + } + .components-base-control { + margin: 0 0 8px; + text-align: left; + &:last-of-type { + margin-bottom: 0; + .components-base-control__field { + white-space: nowrap; + } + .components-checkbox-control__input-container { + align-content: center; + align-items: center; + display: inline-flex; + height: 33px; + position: relative; + .components-checkbox-control__input { + height: 20px; + min-width: 20px; + } + } + } + } + .components-base-control__label { + display: block; + } } .se__button-done { - align-self: flex-start; - margin-top: 10px; + align-self: flex-start; + margin-top: 10px; } .se-datetime-popover { - .components-popover__content { - justify-content: center; - width: max-content; - padding: 1em; - position: relative; - - .components-datetime__date { - margin-top: 50px; - } - } - - .components-datetime__time { - padding-bottom: 0; - - fieldset { - margin-bottom: 0; - - legend { - display: none; - } - } - - .components-datetime__time-wrapper .components-datetime__time-field-time { - align-items: center; - display: flex; - } - - .components-datetime__time-field-am-pm { - white-space: nowrap; - } - } + .components-popover__content { + justify-content: center; + width: max-content; + padding: 1em; + position: relative; + .components-datetime__date { + margin-top: 50px; + } + } + .components-datetime__time { + padding-bottom: 0; + fieldset { + margin-bottom: 0; + legend { + display: none; + } + } + .components-datetime__time-wrapper .components-datetime__time-field-time { + align-items: center; + display: flex; + } + .components-datetime__time-field-am-pm { + white-space: nowrap; + } + } } + /* Date: remove time */ -.se-datetime-popover__date { - fieldset:first-child { - display: none; - } - .components-datetime__time-field-month-select { - height: 100%; - } +.se-datetime-popover__date { + fieldset:first-child { + display: none; + } + .components-datetime__time-field-month-select { + height: 100%; + } } + /* Time: remove date */ + .se-datetime-popover__time { - .components-datetime__time { - fieldset + fieldset { - margin-top: 10px; - } - } - - .components-datetime__timezone { - display: none; - } + .components-datetime__time { + fieldset+fieldset { + margin-top: 10px; + } + } + .components-datetime__timezone { + display: none; + } } + /* Set date/time button */ + .se-datetime-popover__set-datetime { - margin-top: 15px; - position: absolute; - top: 100px; + margin-top: 15px; + position: absolute; + top: 100px; } .se-datetimegroup-controls-label { - display: flex; - font-weight: 700; - - + div { - margin-top: -4px; - } + display: flex; + font-weight: 700; + +div { + margin-top: -4px; + } } .se-datetimegroup-container { - border-bottom: 1px solid #e2e4e7; + border-bottom: 1px solid #e2e4e7; + position: relative; + padding-top: 8px; } .se-datetimegroup-controls { - display: grid; - grid-template-columns: auto auto 70px; - grid-column-gap: 10px; - margin-top: 8px; - max-width: 450px; - - .components-dropdown { - width: 100%; - } - - .components-base-control:nth-of-type(3) { - flex: none; - align-self: flex-end; - margin-bottom: 10px; - margin-left: auto; - - .components-checkbox-control__input-container { - margin-right: 8px; - } - - label { - font-weight: normal; - font-size: 14px; - } - } - - .se-datetime-control__delete { - grid-column-start: 4; - margin-top: -68px; - margin-right: -50px; - margin-left: auto; - align-self: center; - } - - .se-datetime-popover__button { - width: 100%; - justify-content: center; - margin-bottom: 0; - margin-top: 0; - } - - .is-button.is-default:not(:disabled) { - border-color: #7e8993; - background-color: #fff; - color: #32373c; - } + display: grid; + grid-template-columns: auto auto 70px; + grid-column-gap: 10px; + margin-top: 8px; + max-width: 450px; + .components-dropdown { + width: 100%; + } + .components-base-control:nth-of-type(3) { + flex: none; + align-self: flex-end; + margin-bottom: 10px; + margin-left: auto; + .components-checkbox-control__input-container { + margin-right: 8px; + } + label { + font-weight: normal; + font-size: 14px; + } + } + .se-datetime-control__delete { + position: absolute; + top: 0; + right: 0; + z-index: 1; + .components-button { + min-width: 24px; + width: 24px; + height: 24px; + padding: 0; + } + } + .se-datetime-popover__button { + width: 100%; + justify-content: center; + margin-bottom: 0; + margin-top: 0; + } + .is-button.is-default:not(:disabled) { + border-color: #7e8993; + background-color: #fff; + color: #32373c; + } } .se-datetime-addmore { - display: flex; - margin-top: 10px; - margin-bottom: 20px; + display: flex; + margin-top: 10px; + margin-bottom: 20px; } .se-location-label { - max-width: 450px; - - label { - font-weight: 700; - } + max-width: 450px; + label { + font-weight: 700; + } } .se-site-timezone-label { - flex-direction: column; - font-size: 12px; - color: rgb(117, 117, 117); + flex-direction: column; + font-size: 12px; + color: rgb(117, 117, 117); } .se-event-calendar-export { - margin-top: 20px; + margin-top: 20px; } .se-all-day-checkbox .components-flex { - align-items: center; + align-items: center; } \ No newline at end of file diff --git a/src/blocks/event-info/event-manager.js b/src/blocks/event-info/event-manager.js new file mode 100644 index 0000000..31f8d1e --- /dev/null +++ b/src/blocks/event-info/event-manager.js @@ -0,0 +1,339 @@ +import { sortBy, isEqual, clone } from 'lodash'; +import { getStartAndEndDate, createDefaultDate, getDstOffset, TIMEZONE, OFFSET } from './date-utils'; +import moment from 'moment'; + +/** + * Date Manager Service + * + * Creates a date manager service for handling event dates with change tracking, + * timezone management, and state synchronization. Provides a centralized way to + * manage event dates with automatic dirty state tracking and meta synchronization. + * + * @package SimpleEvents + * @since 2.0.0 + */ + +/** + * Creates a hash for a date object based on its start and end times. + * + * Generates a unique identifier for a date object using start time, end time, + * and current timestamp to ensure uniqueness across date operations. + * + * @since 2.0.0 + * + * @param {string} start The start time of the date. + * @param {string} end The end time of the date. + * @return {string} A unique hash for the date. + */ +const createDateHash = (start, end) => { + // Get the current timestamp. + const timestamp = Date.now(); + // Create a hash using the start and end times along with the timestamp. + const hash = `${start}-${end}-${timestamp}`; + return hash; +} + +/** + * Creates a date manager instance for handling event dates. + * + * Provides a comprehensive date management system with change tracking, + * timezone conversion, and state synchronization. Manages both original + * and current date states with automatic dirty flag tracking. + * + * @since 2.0.0 + * + * @param {Array} initialDates Array of initial date objects with dates property. + * @param {string} timezone Current timezone for the event. + * @param {Object} metaSync Optional meta sync object with meta and setMeta properties. + * @return {Object} Date management service with public interface. + */ +export const dateManager = (initialDates = [], timezone = '', metaSync = null) => { + + // lOOP through dates and add a hash to each date + initialDates.dates.forEach(date => { + date.hash = createDateHash(date.start_date, date.end_date); + }); + + // Internal state + let originalDates = clone(initialDates.dates || []); + let currentDates = clone(initialDates.dates || []); + let originalTimezone = timezone || TIMEZONE; + let currentTimezone = timezone || TIMEZONE; + let isDirty = false; + + // Meta sync helpers + const { meta, setMeta } = metaSync || {}; + + /** + * Refreshes the date manager with new dates. + * + * Updates both original and current date states with new data, + * adds hashes to dates if missing, and resets the dirty flag. + * + * @since 2.0.0 + * + * @param {Array} newDates Array of new date objects to set. + * @return {Object} Updated date management service state. + */ + const refreshWithNewDates = (newDates) => { + // Add hash to each date if not present + newDates.forEach(date => { + if (!date.hash) { + date.hash = createDateHash(date.start_date, date.end_date); + } + }); + + // Update internal state + originalDates = clone(newDates); + currentDates = clone(newDates); + isDirty = false; + + // If orginal timezone is not the same as current timezone, update current timezone + if (originalTimezone !== currentTimezone && '' !== currentTimezone) { + originalTimezone = currentTimezone; + } + + + return getCurrentDates(); + }; + + /** + * Gets the current dates and timezone information. + * + * Returns the current state including dates, timezone, and dirty flag. + * Considers timezone changes when determining dirty state. + * + * @since 2.0.0 + * + * @return {Object} Current dates object with dates, timezone, and isDirty properties. + */ + const getCurrentDates = () => { + const timezoneChanged = currentTimezone !== originalTimezone; + return { + dates: currentDates, + timezone: currentTimezone, + isDirty: isDirty || timezoneChanged, + }; + } + + /** + * Finds a date by its hash identifier. + * + * Searches through current dates to find a date object + * matching the provided hash. + * + * @since 2.0.0 + * + * @param {string} hash The hash identifier of the date to find. + * @return {Object|undefined} The date object if found, undefined otherwise. + */ + const findDateByHash = (hash) => { + return currentDates.find(d => d.hash === hash); + } + + /** + * Updates the timezone and converts all dates accordingly. + * + * Changes the event timezone and adjusts all date timestamps to + * maintain the same local time in the new timezone. Updates meta + * if sync is available. + * + * @since 2.0.0 + * + * @param {string} newTimezone The new timezone identifier to set. + * @return {Object} Updated date management service state. + */ + const updateTimezone = (newTimezone) => { + const updatedDates = clone(currentDates); + + // Ensure that the value is a string. + newTimezone = !Boolean(newTimezone) ? '' : newTimezone; + let targetTimezone = newTimezone; + + if ('' === newTimezone) { + targetTimezone = TIMEZONE; + } + + updatedDates.forEach((eventDateTime) => { + [ + 'start_date', + 'end_date', + ].forEach((key) => { + // Get the current DST offset + const currentOffset = getDstOffset( + eventDateTime[key], + currentTimezone, + currentTimezone + ); + + // Get the target DST offset + const targetOffset = '' !== targetTimezone + ? getDstOffset( + eventDateTime[key], + targetTimezone, + targetTimezone + ) + : OFFSET; + + // Apply target timezone offset and keep same local time + eventDateTime[key] = String( + moment + .unix(eventDateTime[key]) + .utcOffset(currentOffset) + .utcOffset(targetOffset, true) + .utc() + .unix() + ); + }); + }); + + // Update internal state + currentDates = updatedDates; + currentTimezone = newTimezone; + isDirty = true; // Mark as dirty since timezone changed + + // Sync to meta if available + if (setMeta && meta) { + setMeta({ + ...meta, + se_event_timezone: newTimezone + }); + } + + return getCurrentDates(); + }; + + /** + * Upserts a date to the event dates collection. + * + * Updates an existing date if the hash matches, otherwise adds a new date. + * Automatically generates hash if missing and maintains sorted order by start date. + * + * @since 2.0.0 + * + * @param {Object} date Date object to add or update with properties: + * - id: null|int + * - hash: string + * - start_date: string + * - end_date: string + * - all_day: boolean + * - hide_from_feed: boolean + * - hide_from_calendar: boolean + * @return {Object} Updated date management service state. + */ + const upsertDate = (date) => { + // If the date doesnt contain a hash, generate one + if (!date.hash) { + date.hash = createDateHash(date.start_date, date.end_date); + } + + // Check if the hash exists in the current dates + const existingIndex = currentDates.findIndex(d => d.hash === date.hash); + if (existingIndex !== -1) { + // If it exists, update the date + currentDates[existingIndex] = date; + } else { + // If it doesn't exist, add the new date + currentDates.push(date); + } + // Mark as dirty + isDirty = true; + + // Sort the dates by start date + currentDates = sortBy(currentDates, 'start_date'); + + return getCurrentDates(); + } + + /** + * Removes a date from the event dates collection. + * + * Finds and removes a date object by its hash identifier, + * then maintains sorted order and marks state as dirty. + * + * @since 2.0.0 + * + * @param {Object} date Date object to remove (must contain hash property). + * @return {Object} Updated date management service state. + */ + const removeDate = (date) => { + // Find the index of the date + const index = currentDates.findIndex(d => d.hash === date.hash); + if (index !== -1) { + // Remove the date + currentDates.splice(index, 1); + // Mark as dirty + isDirty = true; + // Sort the dates by start date + currentDates = sortBy(currentDates, 'start_date'); + } + return getCurrentDates(); + } + + /** + * Adds a new default date to the event dates collection. + * + * Creates a new date object with default values based on existing dates + * and current timezone, then adds it to the collection. + * + * @since 2.0.0 + * + * @return {Object} Updated date management service state. + */ + const addDate = () => { + const newDate = createDefaultDate(currentDates, currentTimezone); + upsertDate(newDate); + return getCurrentDates(); + } + + /** + * Reverts dates and timezone to their original state. + * + * Restores both dates and timezone to their initial values, + * clears dirty flag, and syncs timezone back to meta if available. + * + * @since 2.0.0 + * + * @return {Object} Reverted date management service state. + */ + const revertDates = () => { + currentDates = clone(originalDates); + currentTimezone = originalTimezone; + isDirty = false; + + // Sync timezone revert to meta if available + if (setMeta && meta) { + setMeta({ + ...meta, + se_event_timezone: originalTimezone + }); + } + + return getCurrentDates(); + } + + // Return the public interface + return { + getCurrentDates, + updateTimezone, + upsertDate, + removeDate, + addDate, + revertDates, + refreshWithNewDates, + // Expose internal state getters for external access + get originalDates() { return originalDates; }, + get currentDates() { return currentDates; }, + get originalTimezone() { return originalTimezone; }, + get currentTimezone() { return currentTimezone; }, + get isDirty() { return isDirty; }, + // Expose internal state setters for external access + set originalDates(value) { originalDates = clone(value); }, + set currentDates(value) { currentDates = clone(value); }, + set originalTimezone(value) { originalTimezone = value; }, + set currentTimezone(value) { currentTimezone = value; }, + set isDirty(value) { isDirty = value; } + }; + + +}; diff --git a/src/blocks/event-info/index.js b/src/blocks/event-info/index.js index 391407f..2c7d083 100644 --- a/src/blocks/event-info/index.js +++ b/src/blocks/event-info/index.js @@ -1,8 +1,13 @@ /* global lodash, ajaxurl */ /** - * BLOCK: Events Info + * Event Info Block * - * Event date and location management. + * Gutenberg block for managing event date, time, location, and venue information. + * Provides an interface for adding multiple event dates with timezone support, + * venue and location details, external links, and calendar integration options. + * + * @package SimpleEvents + * @since 1.0.0 */ import './editor.scss'; @@ -11,19 +16,17 @@ import moment from 'moment'; import { clone, isEqual, sortBy, head, last, pull } from 'lodash'; import { __ } from '@wordpress/i18n'; import { registerBlockType } from '@wordpress/blocks'; -import { Fragment } from '@wordpress/element'; +import { Fragment, useState, useEffect } from '@wordpress/element'; import { PanelRow, Placeholder, - BaseControl, - Dropdown, + Button, TextControl, Toolbar, Disabled, CheckboxControl, ComboboxControl, - DateTimePicker, PanelBody, ToggleControl, } from '@wordpress/components'; @@ -34,145 +37,265 @@ import { InspectorControls, useBlockProps, } from '@wordpress/block-editor'; -import { withState } from '@wordpress/compose'; import { useEntityProp } from '@wordpress/core-data'; +import { useSelect, useDispatch } from '@wordpress/data'; -/** - * Constants - */ -const DEFAULT_START_HOUR = 9; -const DEFAULT_END_HOUR = 10; -const DATE_SETTINGS = getSettings(); // eslint-disable-line no-restricted-syntax - -const OFFSET = Number(DATE_SETTINGS.timezone.offset); -const TIMEZONE = DATE_SETTINGS.timezone.string; -let TIMEZONE_NAME = TIMEZONE; -if ('' === TIMEZONE) { - TIMEZONE_NAME = 'UTC' + (OFFSET >= 0 ? '+' : '') + OFFSET; -} -const FORMAT = 'YYYY-MM-DD HH:mm'; -const TIMEZONES = moment.tz - .names() - .map((tz) => ({ label: tz, value: tz })); - -// Add an option to use the site settings. -// (This label is as helpful as we can be since manual offsets have no string.) -TIMEZONES.unshift({ - label: __('Same as site', 'simple-events'), - value: '', -}); +// Import date utilities +import { + + TIMEZONE, + TIMEZONE_NAME, + TIMEZONES, + +} from './date-utils'; + +import apiFetch from '@wordpress/api-fetch'; + +import { dateManager } from './event-manager'; + +// Import the new DateTimeGroup component as DateTimeGroupNew +import DateTimeGroupNew from './components/DateTimeGroup'; + +const DATE_SETTINGS = getSettings(); // Still needed for timeFormat /** - * Get the start and end date from a collection of dates. - * Will remove any event that has passed. + * Fetches event dates from the custom REST API endpoint. + * + * Retrieves all event dates associated with the current post via the + * Simple Events REST API. Returns an empty array if no post ID is found. * - * @param {{all_day: boolean, datetime_start: string, datetime_end: string}[]} dates The dates to check. + * @since 2.0.0 * - * returns {{ datetime_start: string, datetime_end: string }} + * @return {Promise} Promise that resolves to an array of event date objects. */ -const getStartAndEndDate = (dates) => { - // iterate over and remove any that has passed. - const now = moment().utcOffset(OFFSET); - const filteredDates = dates.filter((date) => { +export const getEventDatePosts = () => { + // Get the current post id. + const postId = window?.wp?.data?.select('core/editor')?.getCurrentPostId(); + if (!postId) { + // Return an empty array if no post id is found. + return Promise.resolve([]); + } - const endDate = moment.unix(date.datetime_end).utcOffset(OFFSET); - return endDate.isAfter(now); + // simple-events/event-dates/{event} + return apiFetch({ path: '/simple-events/event-dates/' + postId }).then((posts) => posts + ).catch((error) => { + console.error('Error fetching event dates:', error); + return []; }); - // If we have no filtered dates, but we had dates, before. - if (filteredDates.length === 0 && dates.length > 0) { - // Extract all start dates with the offset. - const allStartDates = dates.map((date) => - moment.unix(date.datetime_start).utcOffset(OFFSET) - ); - const allEndDates = dates.map((date) => - moment.unix(date.datetime_end).utcOffset(OFFSET) - ); +}; - // Return the latest start date and earliest end date. - return { - datetime_start: moment.max(allStartDates).unix().toString(), - datetime_end: moment.max(allEndDates).unix().toString(), - } +/** + * Saves event dates to the custom REST API endpoint. + * + * Sends event dates to the server for persistence via the Simple Events + * REST API sync endpoint. Displays success/error notifications and optionally + * refreshes the date manager instance with updated data. + * + * @since 2.0.0 + * + * @param {Array} dates Array of event date objects to save. + * @param {Object|null} dateManagerInstance Optional date manager instance to refresh after save. + * @return {Promise} Promise that resolves to the API response object. + */ +export const saveEventDates = (dates, dateManagerInstance = null) => { + // Get the current post id. + const postId = window?.wp?.data?.select('core/editor')?.getCurrentPostId(); + if (!postId) { + return Promise.reject(new Error('No post ID found')); } - let startDate = null; - let endDate = null; - - // Loop over the dates and set the start date as the earliest and the end as the latest. - filteredDates.forEach((date) => { - const startDateMoment = moment.unix(date.datetime_start).utcOffset(OFFSET); - const endDateMoment = moment.unix(date.datetime_end).utcOffset(OFFSET); + // simple-events/event-dates/{event} + return apiFetch({ + path: '/simple-events/event-dates/' + postId + '/sync', + method: 'POST', + data: { + dates: dates, + event_id: postId, + nonce: seSettings.syncDatesNonce + } + }).then((response) => { + // Show notification message if available + if (response.message) { + // Show success notification in bottom left + window.wp.data.dispatch('core/notices').createSuccessNotice( + response.message, + { + type: 'snackbar', + isDismissible: true, + id: 'event-dates-saved' + } + ); + } - // If the end date has passed, skip it. - if (endDateMoment.isBefore(now)) { - return; + // Refresh dateManager with new dates if available and dateManager instance is provided + if (response.dates && dateManagerInstance && dateManagerInstance.refreshWithNewDates) { + dateManagerInstance.refreshWithNewDates(response.dates); } - /** - * Closure for setting the start or end date. - * @param {moment.Moment} startDateMoment - * @param {moment.Moment} endDateMoment - */ - const setDate = (startDateMoment, endDateMoment) => { - // If the start date is before the current start date, set it. - if (!startDate || startDateMoment.isBefore(startDate) || (startDate.isAfter(startDateMoment) && startDate.isBefore(now))) { - startDate = startDateMoment; - } - // If the end date is after the current end date, set it. - if (!endDate || endDateMoment.isAfter(endDate)) { - endDate = endDateMoment; + + return response; + }).catch((error) => { + console.error('Error saving event dates:', error); + + // Show error notification + window.wp.data.dispatch('core/notices').createErrorNotice( + __('Failed to save event dates. Please try again.', 'simple-events'), + { + type: 'snackbar', + isDismissible: true, + id: 'event-dates-error' } - }; + ); - // If the start date if after now - if (startDateMoment.isAfter(now) && endDateMoment.isAfter(now)) { - setDate(startDateMoment, endDateMoment); - } else if (startDateMoment.isBefore(now) && endDateMoment.isAfter(now)) { - setDate(startDateMoment, endDateMoment); - } + throw error; }); +}; - // If we have no startDate or endDate, just get the first from dates. - if (!startDate) { - startDate = moment.unix(head(filteredDates).datetime_start).utcOffset(OFFSET); +/** + * Auto-saves event dates when they change. + * + * Wrapper function for saveEventDates that handles automatic saving + * of event dates during user interactions. + * + * @since 2.0.0 + * + * @param {Array} dates Array of event date objects to auto-save. + * @param {Object|null} dateManagerInstance Optional date manager instance to refresh. + * @return {Promise} Promise that resolves to the saved event dates response. + */ +export const autoSaveEventDates = async (dates, dateManagerInstance = null) => { + try { + // Save to REST API + const savedDates = await saveEventDates(dates, dateManagerInstance); + + return savedDates; + } catch (error) { + console.error('Error auto-saving event dates:', error); + throw error; } - if (!endDate) { - endDate = moment.unix(last(filteredDates).datetime_end).utcOffset(OFFSET); +}; + +/** + * Saves event dates when the post is being saved. + * + * Handles saving event dates during WordPress post save operations. + * Also updates block attributes to ensure the saved dates with IDs + * are properly persisted in the block editor. + * + * @since 2.0.0 + * + * @param {Array} dates Array of event date objects to save. + * @param {Object|null} dateManagerInstance Optional date manager instance to refresh. + * @return {Promise} Promise that resolves to the saved event dates response. + */ +export const saveEventDatesOnPostSave = async (dates, dateManagerInstance = null) => { + try { + // Save to REST API + const savedDates = await saveEventDates(dates, dateManagerInstance); + + // Update the post meta to ensure the updated dates (with IDs) are persisted + const postId = window?.wp?.data?.select('core/editor')?.getCurrentPostId(); + if (postId && savedDates) { + + // Update the block attributes to ensure the updated dates (with IDs) are persisted + const blocks = window.wp.data.select('core/block-editor').getBlocks(); + const eventInfoBlock = blocks.find(block => block.name === 'simple-events/event-info'); + + if (eventInfoBlock) { + window.wp.data.dispatch('core/block-editor').updateBlockAttributes( + eventInfoBlock.clientId, + { + eventDates: savedDates.dates || savedDates, + } + ); + } + } + + return savedDates; + } catch (error) { + console.error('Error saving event dates on post save:', error); + throw error; + } +}; + + +// Initialize date manager instance outside the component +let dateManagerInstance = null; +let gettingDates = false; + +/** + * Initializes the date manager with resolved event date posts. + * + * Creates and configures a date manager instance with event dates fetched + * from the REST API. Handles post meta synchronization and timezone settings. + * Uses singleton pattern to avoid multiple initializations. + * + * @since 2.0.0 + * + * @return {Promise} Promise that resolves to the date manager instance or null on error. + */ +const initializeDateManager = async () => { + if (gettingDates || dateManagerInstance) { + return dateManagerInstance; } - return { - datetime_start: startDate.unix().toString(), - datetime_end: endDate.unix().toString(), - }; -} + gettingDates = true; + try { + const eventDatePosts = await getEventDatePosts(); + + // Get current post meta to pass to dateManager for sync + const currentPostId = window?.wp?.data?.select('core/editor')?.getCurrentPostId(); + const currentMeta = currentPostId ? window?.wp?.data?.select('core/editor')?.getEditedPostAttribute('meta') : {}; + const currentTimezone = currentMeta?.se_event_timezone || ''; + + // Create meta sync object + const metaSync = { + meta: currentMeta, + setMeta: (updates) => { + window.wp.data.dispatch('core/editor').editPost({ + meta: updates + }); + } + }; + dateManagerInstance = dateManager(eventDatePosts, currentTimezone, metaSync); + return dateManagerInstance; + } catch (error) { + console.error('Error initializing date manager:', error); + return null; + } finally { + gettingDates = false; + } +}; /** - * Register: a Gutenberg Block. + * Registers the Event Info Gutenberg block. + * + * Creates a custom block for event information management including dates, + * times, locations, venues, and related settings. The block provides both + * edit and preview modes with extensive customization options. * - * Registers a new block provided a unique name and an object defining its - * behavior. Once registered, the block is made editor as an option to any - * editor interface where blocks are implemented. + * @since 1.0.0 * - * @link https://wordpress.org/gutenberg/handbook/block-api/ - * @param {string} name Block name. - * @param {Object} settings Block settings. - * @return {?WPBlock} The block, if it has been successfully - * registered; otherwise `undefined`. + * @see https://wordpress.org/gutenberg/handbook/block-api/ */ registerBlockType('simple-events/event-info', { /** - * The edit function describes the structure of your block in the context of the editor. - * This represents what the editor will render when the block is used. + * Block edit function. * - * The "edit" property must be a valid function. + * Defines the structure and behavior of the Event Info block in the editor. + * Manages event dates through a date manager instance, handles post meta + * synchronization, and provides interfaces for editing event information + * including dates, times, locations, venues, and display options. * - * @link https://wordpress.org/gutenberg/handbook/block-api/block-edit-save/ + * @since 1.0.0 * - * @param {Object} props Props. - * @return {JSX.Element} JSX Component. + * @param {Object} props Block properties. + * @param {Object} props.attributes Block attributes including editMode and showOnFrontEnd. + * @param {Function} props.setAttributes Function to update block attributes. + * @return {JSX.Element} The block editor interface. */ edit: (props) => { const { attributes, setAttributes } = props; @@ -184,79 +307,109 @@ registerBlockType('simple-events/event-info', { 'meta' ); + // Add state for loading indication + const [isGettingDates, setIsGettingDates] = useState(false); + const [dateManagerReady, setDateManagerReady] = useState(false); + const [dateManagerState, setDateManagerState] = useState(null); + // Add refresh counter to force re-renders when dateManager state changes + const [refreshCounter, setRefreshCounter] = useState(0); + + // Watch for post save events + const { isSavingPost, isAutosavingPost } = useSelect((select) => { + const { isSavingPost, isAutosavingPost } = select('core/editor'); + return { + isSavingPost: isSavingPost(), + isAutosavingPost: isAutosavingPost(), + }; + }, []); + + // Trigger date save when post is being saved + useEffect(() => { + const saveDatesOnPostSave = async () => { + if (isSavingPost && !isAutosavingPost && dateManagerState?.getCurrentDates()?.dates) { + try { + await saveEventDatesOnPostSave(dateManagerState.getCurrentDates().dates, dateManagerState); + } catch (error) { + console.error('Failed to save event dates on post save:', error); + } + } + }; - // Sets the default timezone for calculations. - - let currentTimezone = meta?.se_event_timezone; - if ('' === currentTimezone) { - currentTimezone = TIMEZONE; - } + saveDatesOnPostSave(); + }, [isSavingPost, isAutosavingPost, dateManagerState]); - const getDstOffset = (timestamp, timezone = null) => { - // Return no offset if the event timezone is the same as the site. - if (null === timezone) { - timezone = currentTimezone; + // Sync dateManagerState dates to block attributes + useEffect(() => { + if (dateManagerState?.getCurrentDates()?.dates) { + setAttributes({ + eventDates: dateManagerState.getCurrentDates().dates + }); } + }, [dateManagerState, refreshCounter, setAttributes]); - if ('' === timezone) { - return OFFSET; - } + // Check if we should be in edit mode based on missing data + useEffect(() => { + // Only check after dateManager is ready to avoid premature decisions + if (dateManagerReady && dateManagerState) { + const hasDates = dateManagerState?.getCurrentDates()?.dates && + dateManagerState.getCurrentDates().dates.length > 0; - // Get the timezone details. - const timezoneDetails = moment.tz.zone(timezone); + // Enter edit mode if we don't have either location/venue AND dates + const shouldBeInEditMode = !hasDates; - // Get the index of the current timezone offset i.e DST or non-DST. -1 at the end to account for search algorithm. - const untilIndex = timezoneDetails.untils.findIndex(function ( - number - ) { - return number / 1000 > timestamp; - }); + if (shouldBeInEditMode && !editMode) { + setAttributes({ editMode: true }); + } + } + }, [dateManagerReady, dateManagerState, meta?.se_event_location, meta?.se_event_venue, editMode, setAttributes]); + + // Initialize date manager on component mount + useEffect(() => { + const initManager = async () => { + setIsGettingDates(true); + try { + const manager = await initializeDateManager(); + setDateManagerReady(true); + setDateManagerState(manager); + } catch (error) { + console.error('Failed to initialize date manager:', error); + } finally { + setIsGettingDates(false); + } + }; - return timezoneDetails.offsets[untilIndex] * -1; - }; + if (!dateManagerReady && !isGettingDates) { + initManager(); + } + }, [dateManagerReady, isGettingDates]); /** - * Creates a moment in the site timezone from the provided unix timestamp. + * Handles the Done button click event. + * + * Exits edit mode by setting the editMode attribute to false. * - * @param {string} timestamp Timestamp to convert to a moment. - * @param {boolean} formatted Whether to return a human-readable formatted string. - * @return {Mixed} Human readable formatted string if `formatted` is true, - * moment object otherwise. + * @since 1.0.0 */ - const getMoment = (timestamp, formatted = false) => { - const dateTime = moment - .unix(timestamp) - .utcOffset(getDstOffset(timestamp)); + const onDone = () => { + setAttributes({ editMode: false }); - if (!formatted) { - return dateTime; + // Update the date attriutes from dateManagerState + if (dateManagerState?.getCurrentDates()?.dates) { + setAttributes({ + eventDates: dateManagerState.getCurrentDates().dates, + }); } - - return dateTime.format(FORMAT); }; /** - * Creates a timestamp from the provided date string. + * Handles event location input changes. + * + * Updates the se_event_location meta field when the location input changes. + * + * @since 1.0.0 * - * @param {string} dateTime Date string to convert to a timestamp. - * @return {string} The timestamp, cast as a string. + * @param {string} value The new location value. */ - const getTimestamp = (dateTime) => { - return String( - moment(dateTime) - .utcOffset( - getDstOffset(moment(dateTime).unix()), - true - ) - .utc() - .unix() - ); - }; - - const onDone = () => { - setAttributes({ editMode: false }); - }; - const onChangeEventLocation = (value) => { setMeta({ ...meta, @@ -264,24 +417,16 @@ registerBlockType('simple-events/event-info', { }); }; - const maybeUpdateEventDateTime = (oldDate, newDate) => { - if (!isEqual(oldDate, newDate)) { - const updatedDates = sortBy( - meta?.se_event_dates.map((item) => - item === oldDate ? newDate : item - ), - 'datetime_start' - ); - - setMeta({ - ...meta, - se_event_dates: updatedDates, - se_event_date_start: getStartAndEndDate(updatedDates).datetime_start, - se_event_date_end: getStartAndEndDate(updatedDates).datetime_end, - }); - } - }; - + /** + * Renders the block toolbar controls. + * + * Creates the edit and visibility toggle buttons that appear in the + * block toolbar when the block is selected. + * + * @since 1.0.0 + * + * @return {JSX.Element} The BlockControls component with toolbar buttons. + */ const getBlockControls = () => ( ); - const DateTimeGroup = withState({ - tempEventDate: null, - tempEventTime: null, - })( - ({ - eventDateTime, - removeDate, - multiDay, - tempEventDate, - tempEventTime, - setState, - }) => { - const eventStart = getMoment( - eventDateTime.datetime_start, - true - ); - const eventEnd = getMoment(eventDateTime.datetime_end, true); - const timeFormat = DATE_SETTINGS.formats.datetime; - - // To know if the current timezone is a 12 hour time with look for an "a" in the time format. - // We also make sure this a is not escaped by a "/". - const is12HourTime = /a(?!\\)/i.test( - timeFormat - .toLowerCase() // Test only the lower case a - .replace(/\\\\/g, '') // Replace "//" with empty strings - .split('') - .reverse() - .join('') // Reverse the string and test for "a" not followed by a slash - ); - - /** - * Combines a given date and time into a moment object. - * - * @param {string} date The date to combine. - * @param {string} time The time to combine. - * - * @return {moment} The combined date and time. - */ - const combineDateAndTime = (date, time) => { - const timeMoment = moment(time); - const dateMoment = moment(date); - - // Set the timeMoment's time to the dateMoment. - return dateMoment.set({ - hour: timeMoment.get('hour'), - minute: timeMoment.get('minute'), - }); - }; - - /** - * Handle the set date and time button click. - * - * @param {boolean} isStartChange Whether this is start or end date change. - * - * @return {void} - */ - const setDateTimeHandler = (isStartChange) => { - // Ensure we have either new date or time in state. - if (!tempEventDate && !tempEventTime) { - return; - } - - const newDate = - tempEventDate || - (isStartChange ? eventStart : eventEnd); - const newTime = - tempEventTime || - (isStartChange ? eventStart : eventEnd); - - // Combine the new date and time and convert to a timestamp. - const newDateTime = getTimestamp( - combineDateAndTime(newDate, newTime) - ); - - const newEventDateTime = clone(eventDateTime); - - if (isStartChange) { - newEventDateTime.datetime_start = newDateTime; - - // Check if the new start time is after the cuurent end time. - if ( - parseInt(newEventDateTime.datetime_start) >= - parseInt(newEventDateTime.datetime_end) - ) { - // Set the new end time to be 1 hour after the start dateTime. - newEventDateTime.datetime_end = String( - parseInt(newEventDateTime.datetime_start) + - 3600 - ); - } - } else { - newEventDateTime.datetime_end = newDateTime; - - // Check if the new end time is before the current start time. - if ( - parseInt(newEventDateTime.datetime_start) >= - parseInt(newEventDateTime.datetime_end) - ) { - // Set the new start time to be 1 hour before the end dateTime. - newEventDateTime.datetime_start = String( - parseInt(newEventDateTime.datetime_end) - 3600 - ); - } - } - - // Reset the temp date and time. - setState({ - tempEventDate: null, - tempEventTime: null, - }); - - - maybeUpdateEventDateTime(eventDateTime, newEventDateTime); - }; - - /** - * Handles DateTimePicker changes. - * - * @param {string} currentDateTime The current dateTime. - * @param {string} newDateTime The new selected dateTime. - * - * @return {void} - */ - const datePickerHandler = (currentDateTime, newDateTime) => { - // Compare the date without time to see if the time or date was changed. - const isDateChange = - moment(currentDateTime).format('YYYY-MM-DD') === - moment(newDateTime).format('YYYY-MM-DD'); - const stateUpdate = isDateChange - ? { tempEventTime: newDateTime } - : { tempEventDate: newDateTime }; - setState(stateUpdate); - }; - - return ( -
-
- - ( - - )} - renderContent={() => ( - - - datePickerHandler( - eventStart, - newDateTime - ) - } - __nextRemoveHelpButton - __nextRemoveResetButton - /> - - - )} - /> - - - ( - - )} - renderContent={() => ( - - - datePickerHandler( - eventEnd, - newDateTime - ) - } - __nextRemoveHelpButton - __nextRemoveResetButton - /> -
- )} -
- - ); + // Wrapper functions to trigger re-renders when dateManager state changes + /** + * Handles adding a new date to the event. + * + * Calls the date manager's addDate method and triggers a re-render + * by incrementing the refresh counter. + * + * @since 2.0.0 + */ + const handleAddDate = () => { + if (dateManagerState?.addDate) { + dateManagerState.addDate(); + setRefreshCounter(prev => prev + 1); } - ); - - const EventDateTime = ({ dates }) => { - const addNewDate = () => { - const existingDates = - !dates || 0 === dates.length ? [] : dates; - - // Set default date and time. - let eventStart = moment().utcOffset(OFFSET); - - eventStart.hour(DEFAULT_START_HOUR); - eventStart.minute(0); - eventStart.second(0); - - let eventEnd = eventStart.clone(); - - eventEnd.hour(DEFAULT_END_HOUR); - - // Override with existing date if there is one. - if (existingDates.length) { - eventStart = getMoment( - last(existingDates).datetime_start - ); - eventEnd = getMoment(last(existingDates).datetime_end); - } - - // Set default date to be +1 day from the last date. - eventStart.add(1, 'days'); - eventEnd.add(1, 'days'); - - const updatedDates = sortBy( - [ - ...existingDates, - { - datetime_start: wp.date.date('U', eventStart), - datetime_end: wp.date.date('U', eventEnd), - all_day: false, - }, - ], - 'datetime_start' - ); + }; - setMeta({ - ...meta, - se_event_dates: updatedDates, - se_event_date_start: getStartAndEndDate(updatedDates).datetime_start, - se_event_date_end: getStartAndEndDate(updatedDates).datetime_end, - }); - }; + /** + * Handles reverting date changes. + * + * Calls the date manager's revertDates method and triggers a re-render + * by incrementing the refresh counter. + * + * @since 2.0.0 + */ + const handleRevertDates = () => { + if (dateManagerState?.revertDates) { + dateManagerState.revertDates(); + setRefreshCounter(prev => prev + 1); + } + }; - const removeDate = (date) => { - if (!dates.length) { - return; + // Create enhanced dateManagerInstance that triggers re-renders + const enhancedDateManagerInstance = dateManagerState ? { + ...dateManagerState, + upsertDate: (date) => { + const result = dateManagerState.upsertDate(date); + setRefreshCounter(prev => prev + 1); + return result; + }, + removeDate: (date) => { + const result = dateManagerState.removeDate(date); + setRefreshCounter(prev => prev + 1); + return result; + }, + addDate: () => { + const result = dateManagerState.addDate(); + setRefreshCounter(prev => prev + 1); + return result; + }, + revertDates: () => { + const result = dateManagerState.revertDates(); + setRefreshCounter(prev => prev + 1); + return result; + }, + refreshWithNewDates: (newDates) => { + if (dateManagerState.refreshWithNewDates) { + dateManagerState.refreshWithNewDates(newDates); + setRefreshCounter(prev => prev + 1); } + }, + updateTimezone: (newTimezone) => { + const result = dateManagerState.updateTimezone(newTimezone); + setRefreshCounter(prev => prev + 1); + return result; + } + } : null; - const updatedDates = pull(dates, date); + /** + * Renders the unsaved changes warning component. + * + * Displays a warning message when the date manager has unsaved changes, + * informing the user that they need to save the post to persist changes. + * + * @since 2.0.0 + * + * @return {JSX.Element|null} Warning component or null if no unsaved changes. + */ + const UnsavedChangesWarning = () => { + if (!dateManagerState?.getCurrentDates()?.isDirty) { + return null; + } - setMeta({ - ...meta, - se_event_dates: updatedDates, - se_event_date_start: getStartAndEndDate(updatedDates).datetime_start, - se_event_date_end: getStartAndEndDate(updatedDates).datetime_end, - }); - }; + return ( +
+ +
+ {__('Unsaved Changes', 'simple-events')} +
+ + {__('You have unsaved date and timezone changes. Save the post to persist these changes.', 'simple-events')} + +
+
+ ); + }; - // If no dates, add a date. - if (!dates || 0 === dates.length) { - addNewDate(); - } + /** + * Renders the event date/time component. + * + * Creates a list of DateTimeGroupNew components for each event date, + * sorted by start date. Each component allows editing of individual + * date and time information. + * + * @since 2.0.0 + * + * @param {Object} props Component properties. + * @param {Array} props.dates Array of event date objects to render. + * @param {number} props.refreshCounter Counter to force component re-renders. + * @return {JSX.Element} Fragment containing the event dates interface. + */ + const EventDateTime = ({ dates, refreshCounter }) => { const datesOutput = []; - sortBy(dates, 'datetime_start').forEach((date, index) => { + sortBy(dates, 'start_date').forEach((date, index) => { datesOutput.push( - 1} + removeDate={null} + hasMultipleDates={dates.length > 1} + currentTimezone={dateManagerState?.getCurrentDates()?.timezone ?? meta?.se_event_timezone ?? TIMEZONE} + dateManagerInstance={enhancedDateManagerInstance} /> ); }); @@ -730,46 +600,49 @@ registerBlockType('simple-events/event-info', { return ( - {__('Date & Time', 'simple-events')} + {__('Event Dates (New)', 'simple-events')} {datesOutput} -
- -
+
); }; + /** + * Renders the block preview mode. + * + * Displays the front-end representation of the block using ServerSideRender + * with the current event data. Includes block controls and unsaved changes warning. + * + * @since 1.0.0 + * + * @return {JSX.Element} The preview component with ServerSideRender. + */ const renderPreview = () => (
{getBlockControls()} +
); - // Show editMode if no location or date set. - if ( - meta?.se_event_location.length === 0 && - (!meta?.se_event_dates || !meta?.se_event_dates?.length) - ) { - setAttributes({ editMode: true }); - } - if (!editMode) { return renderPreview(); } @@ -783,7 +656,35 @@ registerBlockType('simple-events/event-info', { isColumnLayout className={props.className} > - + + + + {/* Button container with 50/50 layout */} +
+
{ - const updatedDates = clone( - meta?.se_event_dates ?? [] - ); - - // Ensure that the value is a string. - value = !Boolean(value) ? '' : value; - currentTimezone = value; - - if ('' === value) { - currentTimezone = TIMEZONE; + if (enhancedDateManagerInstance?.updateTimezone) { + enhancedDateManagerInstance.updateTimezone(value); + setRefreshCounter(prev => prev + 1); } - - updatedDates.forEach((eventDateTime) => { - [ - 'datetime_start', - 'datetime_end', - ].forEach((key) => { - const dateTime = moment - .unix(eventDateTime[key]) - .utcOffset( - getDstOffset( - eventDateTime[key], - meta?.se_event_timezone - ) - ); - - const newOffset = - '' !== currentTimezone - ? getDstOffset( - eventDateTime[key], - currentTimezone - ) - : OFFSET; - - eventDateTime[key] = String( - dateTime - .utcOffset(newOffset, true) - .utc() - .unix() - ); - }); - }); - - setMeta({ - ...meta, - se_event_dates: updatedDates, - se_event_date_start: getStartAndEndDate(updatedDates).datetime_start, - se_event_date_end: getStartAndEndDate(updatedDates).datetime_end, - se_event_timezone: value, - }); }} /> { return null; diff --git a/src/blocks/event-tickets/style.scss b/src/blocks/event-tickets/style.scss index c84ff66..bc480f9 100644 --- a/src/blocks/event-tickets/style.scss +++ b/src/blocks/event-tickets/style.scss @@ -1,42 +1,52 @@ .wp-block-se-event-tickets { - &__heading { - margin: 0 0 32px; - } + &__heading { + margin: 0 0 32px; + } + &__ticket-row { + display: flex; + flex-direction: row; + flex-wrap: wrap; + align-items: center; + width: 100%; + margin: 0 0 16px; + } + &__ticket-column { + display: flex; + flex-direction: column; + flex-basis: 100%; + &--title { + flex: 3; + } + &--price { + flex: 2; + } + &--buy { + flex: 1; + } + } + &__ticket-stock { + display: block; + opacity: 0.6; + font-size: .75em; + } + &__button { + text-align: center; + } +} - &__ticket-row { - display: flex; - flex-direction: row; - flex-wrap: wrap; - align-items: center; - width: 100%; - margin: 0 0 16px; - } - &__ticket-column { - display: flex; - flex-direction: column; - flex-basis: 100%; +/* Grouped date list styles */ - &--title { - flex: 3; - } +.se-event-date-list-item__grouped { + display: inline-flex; + gap: 16px; + /* 16px spacing between date and time */ +} - &--price { - flex: 2; - } +.se-event-date-list-item__grouped-date { + /* Date styling can be added here if needed */ +} - &--buy { - flex: 1; - } - } - - &__ticket-stock { - display: block; - opacity: 0.6; - font-size: .75em; - } - - &__button { - text-align: center; - } +.se-event-date-list-item__grouped-time { + /* Time styling can be added here if needed */ } \ No newline at end of file diff --git a/src/blocks/loop-event-info/block.json b/src/blocks/loop-event-info/block.json index 7a6d35f..cf16fce 100644 --- a/src/blocks/loop-event-info/block.json +++ b/src/blocks/loop-event-info/block.json @@ -1,61 +1,69 @@ { - "$schema": "https://schemas.wp.org/trunk/block.json", - "apiVersion": 2, - "name": "simple-events/loop-event-info", - "title": "Event Metadata", - "description": "Display event meta in a custom query loop.", - "icon": "tag", - "category": "simple-events", - "keywords": ["event info", "date", "location", "Simple Events", "meta"], - "usesContext": ["postId"], - "attributes": { - "textAlign": { - "type": "string", - "default": "left" - }, - "thePostId": { - "type": "integer", - "default": 0 - }, - "metaName": { - "enum": ["location", "venue", "dates", "date", "time"], - "type": "string", - "default": "dates" - }, - "metaPrefix": { - "type": "string", - "default": "" - }, - "addCalendarLinks": { - "type": "boolean" - } - }, - "supports": { - "html": false, - "align": true, - "typography": { - "fontSize": true, - "lineHeight": true, - "__experimentalFontFamily": true, - "__experimentalFontWeight": true, - "__experimentalFontStyle": true, - "__experimentalTextTransform": true, - "__experimentalTextDecoration": true, - "__experimentalLetterSpacing": true, - "__experimentalDefaultControls": { - "fontSize": true - } - }, - "spacing": { - "margin": true, - "padding": true, - "blockGap": true - }, - "color": { - "text": true - } - }, - "editorScript": "file:./index.js", - "editorStyle": "file:./editor.css", - "style": "file:./style.css" -} + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 2, + "name": "simple-events/loop-event-info", + "title": "Event Metadata", + "description": "Display event meta in a custom query loop.", + "icon": "tag", + "category": "simple-events", + "keywords": ["event info", "date", "location", "Simple Events", "meta"], + "usesContext": ["postId"], + "attributes": { + "textAlign": { + "type": "string", + "default": "left" + }, + "thePostId": { + "type": "integer", + "default": 0 + }, + "metaName": { + "enum": ["location", "venue", "dates", "date", "time"], + "type": "string", + "default": "dates" + }, + "metaPrefix": { + "type": "string", + "default": "" + }, + "addCalendarLinks": { + "type": "boolean" + }, + "feedType": { + "type": "string", + "default": "default" + }, + "order": { + "type": "string", + "default": "asc" + } + }, + "supports": { + "html": false, + "align": true, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalFontFamily": true, + "__experimentalFontWeight": true, + "__experimentalFontStyle": true, + "__experimentalTextTransform": true, + "__experimentalTextDecoration": true, + "__experimentalLetterSpacing": true, + "__experimentalDefaultControls": { + "fontSize": true + } + }, + "spacing": { + "margin": true, + "padding": true, + "blockGap": true + }, + "color": { + "text": true + } + }, + "editorScript": "file:./index.js", + "editorStyle": "file:./editor.css", + "style": "file:./style.css" +} \ No newline at end of file diff --git a/src/blocks/loop-event-info/index.js b/src/blocks/loop-event-info/index.js index e0bdfe6..1722c44 100644 --- a/src/blocks/loop-event-info/index.js +++ b/src/blocks/loop-event-info/index.js @@ -17,9 +17,40 @@ import { InspectorControls, BlockControls, } from '@wordpress/block-editor'; +import { useSelect } from '@wordpress/data'; +import { useEffect } from '@wordpress/element'; registerBlockType(metadata, { - edit: ({ attributes: { metaName, metaPrefix, thePostId, textAlign, addCalendarLinks }, setAttributes, context: { postId } }) => { + edit: ({ attributes: { metaName, metaPrefix, thePostId, textAlign, addCalendarLinks, feedType, order }, setAttributes, context: { postId }, clientId }) => { + + // Get query loop data from our custom store + const queryData = useSelect((select) => { + const blockEditor = select('core/block-editor'); + const parents = blockEditor.getBlockParents(clientId); + + // Find the query block parent + for (const parentId of parents) { + const parentBlock = blockEditor.getBlock(parentId); + if (parentBlock && parentBlock.name === 'core/query') { + const storeData = select('se-events/query-data').getQueryData(parentId); + return storeData || {}; + } + } + return {}; + }, [clientId]); + + const { feedType: contextFeedType = feedType, order: contextOrder = order } = queryData; + + // Update block attributes when context values change + useEffect(() => { + if (contextFeedType !== feedType || contextOrder !== order) { + setAttributes({ + feedType: contextFeedType, + order: contextOrder, + }); + } + }, [contextFeedType, contextOrder, feedType, order, setAttributes]); + return ( <> @@ -75,6 +106,8 @@ registerBlockType(metadata, { textAlign, thePostId: postId, // Passes the current post ID to the render callback, even if in a query loop. addCalendarLinks, + feedType, // Use block attribute values + order, // Use block attribute values }} /> diff --git a/src/classes/class-date-display-formatter.php b/src/classes/class-date-display-formatter.php new file mode 100644 index 0000000..5d87ee1 --- /dev/null +++ b/src/classes/class-date-display-formatter.php @@ -0,0 +1,671 @@ + false, + 'allow_grouping_dates_different_time' => false, + ) + ); + $this->event_id = $event_id; + $this->treat_each_date_as_own_event = isset( $options['treat_each_date_as_own_event'] ) && 'on' === $options['treat_each_date_as_own_event']; + $this->allow_grouping_dates_different_time = isset( $options['allow_grouping_dates_different_time'] ) && 'on' === $options['allow_grouping_dates_different_time']; + $this->group_dates = filter_var( get_post_meta( $event_id, 'se_event_display_grouped', true ), FILTER_VALIDATE_BOOLEAN ); + $this->is_single_view = is_single(); + $this->event_timezone = get_post_meta( $event_id, 'se_event_timezone', true ); + $this->event_date_id = se_template_get_event_date_id(); + $this->display_timezone = filter_var( get_post_meta( $event_id, 'se_event_display_timezone', true ), FILTER_VALIDATE_BOOLEAN ); + $this->hide_end_time = filter_var( get_post_meta( $event_id, 'se_event_hide_end_time', true ), FILTER_VALIDATE_BOOLEAN ); + $this->hide_start_time = filter_var( get_post_meta( $event_id, 'se_event_hide_start_time', true ), FILTER_VALIDATE_BOOLEAN ); + $this->show_add_to_calendar = filter_var( get_post_meta( $event_id, 'se_event_add_calendar_links', true ), FILTER_VALIDATE_BOOLEAN ); + $this->open_in_new_tab = filter_var( get_post_meta( $event_id, 'se_event_open_in_new_window', true ), FILTER_VALIDATE_BOOLEAN ); + } + + /** + * Set the date only. + * + * @param boolean $date_only The date only. + * + * @return void + */ + public function set_date_only( bool $date_only = true ) { + $this->date_only = $date_only; + } + + /** + * Set the time only. + * + * @param boolean $time_only The time only. + * + * @return void + */ + public function set_time_only( bool $time_only = true ) { + $this->time_only = $time_only; + } + + /** + * Modify Timezone. + * + * @param string $timezone The timezone. + * + * @return void + */ + public function modify_timezone( $timezone ): void { + if ( ! empty( $timezone ) ) { + $this->event_timezone = $timezone; + } + } + + /** + * Has event date id. + * + * @return boolean + */ + public function has_event_date_in_url() { + return $this->event_date_id > 0; + } + + /** + * Treat each date as own event for navigation. + * + * @return boolean + */ + public function is_treating_each_date_as_own_event() { + return $this->treat_each_date_as_own_event; + } + + + /** + * Get the date range for the event. + * + * @param array $event_dates Event dates. + * + * @return array{start_date: string, end_date: string} + */ + public function get_date_range( array $event_dates ) { + $start = null; + $end = null; + + // Loop over each date. + foreach ( $event_dates as $date ) { + if ( $start === null || $date['start_date'] < $start ) { // phpcs:ignore + $start = $date['start_date']; + } + + if ( $end === null || $date['end_date'] > $end ) { // phpcs:ignore + $end = $date['end_date']; + } + + // If all day and start is after the latest end date, set the end date to the start date. + if ( $date['all_day'] && $date['start_date'] > $end ) { // phpcs:ignore + $end = $date['start_date']; + } + } + + return array( + 'start_date' => $start, + 'end_date' => $end, + ); + } + + /** + * Gets the header date for the event. + * + * @param array $event_dates Event dates. + * + * @return string + */ + public function get_header_date( array $event_dates ) { + // If we are treating each date as it own. + if ( $this->treat_each_date_as_own_event && $this->event_date_id ) { + $found_date = array_filter( + $event_dates, + function ( $date ) { + return $date['id'] === $this->event_date_id; + } + ); + + if ( $found_date ) { + return $this->render_single_date( $found_date[0] ); + } + } + // If we are grouping dates, return the first date. + $date_range = $this->get_date_range( $event_dates ); + $cloned = $event_dates[0]; + $cloned['start_date'] = $date_range['start_date']; + $cloned['end_date'] = $date_range['end_date']; + $cloned['id'] = $this->event_id; + return $this->render_single_date( $cloned ); + } + + /** + * Render active date. + * + * @param array $event_dates Event dates. + * + * @return string|null + */ + public function render_active_date( array $event_dates ) { + // If we dont have an event date id, return the first date. + if ( ! $this->event_date_id ) { + return null; + } + + // If we are not treating each date as it own, return null. + if ( ! $this->treat_each_date_as_own_event ) { + return null; + } + + // Find the date in the event dates. + $found_date = array_filter( + $event_dates, + function ( $date ) { + return isset( $date['id'] ) && $date['id'] === $this->event_date_id; + } + ); + + // If we found the date, return it. + if ( $found_date ) { + return $this->render_single_date( array_values( $found_date )[0] ); + } + + return null; + } + + /** + * Renders a date list. + * + * @param array $event_dates Event dates. + * @param boolean $exclude_current_date Exclude the current date. + * @param boolean $exclude_past_dates Exclude dates that are in the past. + * + * @return string + */ + public function render_date_list( array $event_dates, bool $exclude_current_date = false, bool $exclude_past_dates = false ) { + // Filter the event dates. + $event_dates = array_filter( + $event_dates, + function ( $date ) use ( $exclude_current_date, $exclude_past_dates ) { + // If the date is the current date, exclude it. + if ( $exclude_current_date && $this->event_date_id && $date['id'] === $this->event_date_id ) { + return false; + } + + // If the date is in the past, exclude it. + if ( $exclude_past_dates && $date['start_date'] < SE_Calendar::get_instance()->create_date_time( 'now' )->format( 'U' ) ) { + return false; + } + + return true; + } + ); + + // Sort by the start date. + usort( + $event_dates, + function ( $a, $b ) { + return $a['start_date'] - $b['start_date']; + } + ); + + // Get the date count. + $dates_count = count( $event_dates ); + + // If there is only one date, return the single date. + if ( 1 === $dates_count ) { + return sprintf( '
  • %s
', $this->render_single_date( $event_dates[0] ) ); + } + + // Start building the output. + $wrapper_class = array( 'se-event-date-list', $this->group_dates ? 'se-event-date-list__grouped' : '', $this->event_date_id ? 'se-event-date-list__active' : '' ); + $output = sprintf( '
    ', implode( ' ', $wrapper_class ) ); + // Base if we are grouped, or not. + if ( $this->can_group_dates( $event_dates ) ) { + $output .= $this->render_date_list_grouped( $event_dates ); + } else { + $output .= $this->render_date_list_ungrouped( $event_dates ); + } + + $output .= '
'; + + return $output; + } + + /** + * Checks if the dates can be grouped. + * + * @param array $event_dates Event dates. + * + * @return boolean + */ + private function can_group_dates( array $event_dates ) { + // If we are not grouping dates, return false. + if ( ! $this->group_dates ) { + return false; + } + + $times = array(); + + foreach ( $event_dates as $date ) { + $index = $date['all_day'] ? 'all_day' : $this->format_time( $date['start_date'] ) . ' - ' . $this->format_time( $date['end_date'] ); + + // If this index is not in the array, add it. + if ( ! in_array( $index, $times, true ) ) { + $times[] = $index; + } + + // If have more than one time and do not allow_grouping_dates_different_time, return false. + if ( count( $times ) > 1 && ! $this->allow_grouping_dates_different_time ) { + return false; + } + } + + return true; + } + + /** + * Renders the list of date as single items (not grouped view) + * + * @param array $event_dates Event dates. + * @param string $existing_output Existing output. + * + * @return string + */ + private function render_date_list_ungrouped( array $event_dates, string $existing_output = '' ) { + foreach ( $event_dates as $date ) { + // If we dont have an id on date, set as null. + if ( ! isset( $date['id'] ) ) { + $date['id'] = null; + } + $item_class = array( 'se-event-date-list-item', $date['id'] === $this->event_date_id ? 'se-event-date-list-item__active' : '' ); + $existing_output .= sprintf( '
  • %s
  • ', $date['id'], implode( ' ', $item_class ), $this->render_single_date( $date ) ); + } + + return $existing_output; + } + + /** + * Renders the list of date as grouped items (grouped view) + * + * @param array $event_dates Event dates. + * @param string $existing_output Existing output. + * + * @return string + */ + private function render_date_list_grouped( array $event_dates, string $existing_output = '' ) { + $groups = array(); + + // iterate over the dates and group them by the start and end times. + foreach ( $event_dates as $date ) { + // If the dates all_day is a string, convert 'true' to true. + if ( is_string( $date['all_day'] ) ) { + $date['all_day'] = 'true' === $date['all_day'] ? true : false; + } + + // If this event is all day. + if ( true === (bool) $date['all_day'] ) { + $groups['all_day'][] = $date; + continue; + } + // Convert the start and end times,/ + $start = $this->format_time( $date['start_date'] ); + $end = $this->format_time( $date['end_date'] ); + + // Add the date to the group. + $groups[ $start . ' - ' . $end ][] = $date; + } + // Iterate over each group, and break them down to the starting month. + foreach ( $groups as $group ) { + // Create the time label. + $time_label = $group[0]['all_day'] ? SE_Settings::get_all_day_message() : null; + if ( ! $time_label ) { + $time_start = ( $this->hide_start_time ) ? '' : $this->format_time( $group[0]['start_date'] ); + $time_end = ( $this->hide_end_time ) ? '' : $this->format_time( $group[0]['end_date'] ); + // Join using &ndash if we have a start and end time. + if ( ! empty( $time_start ) && ! empty( $time_end ) ) { + $time_label = $time_start . ' – ' . $time_end; + } elseif ( ! empty( $time_start ) ) { + $time_label = $time_start; + } elseif ( ! empty( $time_end ) ) { + $time_label = $time_end; + } else { + $time_label = ''; + } + } + + $dates = array(); + foreach ( $group as $date ) { + // Get 2020-12 for the start date. + $month_year = wp_date( 'Y-m', $date['start_date'], $this->get_timezone_instance() ); + $same_day = wp_date( 'Y-m-d', $date['start_date'], $this->get_timezone_instance() ) === wp_date( 'Y-m-d', $date['end_date'], $this->get_timezone_instance() ); + $dates[ $month_year ][] = array( + 'date' => $date, + 'same_day' => $same_day, + 'display_date' => $same_day ? $this->format_date( $date['start_date'] ) : $this->format_date( $date['start_date'] ) . ' – ' . $this->format_date( $date['end_date'] ), + ); + } + + foreach ( $dates as $month_dates ) { + $dates_string = $this->join_string( array_column( $month_dates, 'display_date' ), ', ', ' and ' ); + + // Lets start compiling the output. + $output = ''; + // If the date is on the same day, we can just render the date. + $output .= '
  • '; + // Add the date. + $output .= '
    '; + $output .= $this->time_only ? '' : $dates_string; + $output .= '
    '; + + // Add the time. + $output .= '
    '; + $output .= $this->date_only ? '' : $time_label; + $output .= '
    '; + + $output .= '
    '; + $output .= '
  • '; + $existing_output .= $output; + } + } + + return $existing_output; + } + + /** + * Join a string with differing separators a the then + * + * Example: join_string(['a','b','c'], ',', ' and ') => 'a, b and c' + * + * @param string[] $items The items to join. + * @param string $separator The separator to use. + * @param string $separator_end The final separator to use. + * + * @return string + */ + private function join_string( array $items, string $separator, string $separator_end ) { + // If arrray only contains one item, return it. + if ( count( $items ) === 1 ) { + return $items[0]; + } + + // If array contains two items, return them joined by the separator. + if ( count( $items ) === 2 ) { + return implode( $separator_end, $items ); + } + + // Remove the last item. + $last_item = array_pop( $items ); + + // Join the items with the separator. + $output = implode( $separator, $items ); + + // Add the separator_end to the last item. + return $output . $separator_end . $last_item; + } + + /** + * Formats the dates for the event. + * + * @param array $event_dates Event dates. + * + * @return string + */ + public function format_dates( array $event_dates ) { + // Reset indexes + $event_dates = array_values( $event_dates ); + + // Sort all dates by start date. + usort( + $event_dates, + function ( $a, $b ) { + return $a['start_date'] - $b['start_date']; + } + ); + + // Get the date count. + $dates_count = count( $event_dates ); + + // If there is only one date, return the single date. + if ( 1 === $dates_count ) { + return sprintf( '
    • %s
    ', $this->render_single_date( $event_dates[0] ) ); + } + + // Start building the output. + $wrapper_class = array( 'se-event-date-list', $this->group_dates ? 'se-event-date-list__grouped' : '', $this->event_date_id ? 'se-event-date-list__active' : '' ); + $output = sprintf( '
      ', implode( ' ', $wrapper_class ) ); + + // Loop over each date. + foreach ( $event_dates as $date ) { + $item_class = array( 'se-event-date-list-item', $date['id'] === $this->event_date_id ? 'se-event-date-list-item__active' : '' ); + $output .= sprintf( '
    • %s
    • ', $date['id'], implode( ' ', $item_class ), $this->render_single_date( $date ) ); + } + + $output .= '
    '; + + return $output; + } + + /** + * Get the posts timezone instance. + * + * @return DateTimeZone + */ + private function get_timezone_instance() { + return '' !== $this->event_timezone ? new DateTimeZone( $this->event_timezone ) : wp_timezone(); + } + + /** + * Get the timezone abbreviation. + * + * @return string + */ + private function get_timezone_abbreviation() { + $timezone_date = new DateTime( '', $this->get_timezone_instance() ); + return $timezone_date->format( 'T' ); + } + + /** + * Formats a date to the sites tiemzone and date format. + * + * @param integer $date_timestamp The date timestamp. + * + * @return string + */ + public function format_date( $date_timestamp ) { + return wp_date( get_option( 'date_format' ), $date_timestamp, $this->get_timezone_instance() ); + } + + /** + * Formats a time to the sites tiemzone and time format. + * + * @param integer $time_timestamp The time timestamp. + * + * @return string + */ + public function format_time( $time_timestamp ) { + return wp_date( get_option( 'time_format' ), $time_timestamp, $this->get_timezone_instance() ); + } + + /** + * Renders a single date. + * + * @param array $event_date The event date. + * + * @return string + */ + public function render_single_date( $event_date ) { + // Check if the event starts and ends on the same day. + $same_day = wp_date( 'Y-m-d', $event_date['start_date'], $this->get_timezone_instance() ) === wp_date( 'Y-m-d', $event_date['end_date'], $this->get_timezone_instance() ); + + // Get start and end times. + $time_start = ( $this->hide_start_time || $this->date_only ) ? '' : $this->format_time( $event_date['start_date'] ); + $time_end = ( $this->hide_end_time || $this->date_only ) ? '' : $this->format_time( $event_date['end_date'] ); + + // Get the start and end date. + $start_date = $this->time_only ? '' : $this->format_date( $event_date['start_date'] ); + $end_date = $this->time_only ? '' : $this->format_date( $event_date['end_date'] ); + + // Check if it's an all day event. + $is_all_day = array_key_exists( 'all_day', $event_date ) ? filter_var( $event_date['all_day'], FILTER_VALIDATE_BOOLEAN ) : false; + + // Start building the output. + $output = $start_date; + + // Handle different cases based on whether it's same day, all day, etc. + if ( $is_all_day ) { + + // For all day events, just show the date (or date range if different days). + if ( ! $same_day ) { + $output .= ' – ' . $end_date . ' ' . SE_Settings::get_all_day_message(); + } else { + $output .= ' ' . SE_Settings::get_all_day_message(); + } + } elseif ( $same_day ) { + // Same day event with times. + $time_parts = array(); + if ( ! $this->hide_start_time && ! empty( $time_start ) ) { + $time_parts[] = $time_start; + } + if ( ! $this->hide_end_time && ! empty( $time_end ) && $time_start !== $time_end ) { + $time_parts[] = $time_end; + } + + if ( ! empty( $time_parts ) ) { + $output .= ' ' . implode( ' – ', $time_parts ); + } + } else { + // Multi-day event with times. + if ( ! $this->hide_start_time && ! empty( $time_start ) ) { + $output .= ' ' . $time_start; + } + $output .= ' – ' . $end_date; + if ( ! $this->hide_end_time && ! empty( $time_end ) ) { + $output .= ' ' . $time_end; + } + } + + // Add timezone if the option is set. + if ( $this->display_timezone ) { + $output .= ' (' . $this->get_timezone_abbreviation() . ')'; + } + + return $output; + } +} diff --git a/src/classes/class-se-admin.php b/src/classes/class-se-admin.php index ad438f5..4449f7c 100644 --- a/src/classes/class-se-admin.php +++ b/src/classes/class-se-admin.php @@ -32,7 +32,7 @@ public static function init() { * @return void */ public static function enqueue_admin_scripts() { - wp_enqueue_script( + wp_register_script( 'se-admin', SE_PLUGIN_URL . '/build/js/admin.js', array( 'jquery' ), @@ -42,6 +42,16 @@ public static function enqueue_admin_scripts() { 'strategy' => 'async', ) ); + + wp_localize_script( + 'se-admin', + 'seAdmin', + array( + 'ajaxUrl' => admin_url( 'admin-ajax.php' ), + 'nonce' => wp_create_nonce( 'wp_rest' ), + ) + ); + wp_enqueue_script( 'se-admin' ); } /** diff --git a/src/classes/class-se-block-variations.php b/src/classes/class-se-block-variations.php index 3a4ed18..d279fb7 100644 --- a/src/classes/class-se-block-variations.php +++ b/src/classes/class-se-block-variations.php @@ -98,9 +98,56 @@ public function build_query( $query ) { } } + // Change the post type. + $query['post_type'] = SE_Event_Post_Type::$event_date_post_type; + return $this->set_event_query_args( $query, $feed_type, $feed_order ); } + /** + * Modify event posts results. + * + * @param array $posts The array of post objects. + * @param WP_Query $query The WP_Query instance. + * + * @return array + */ + public function modify_event_posts( $posts, $query ) { + // Check if this is our events query + if ( ! isset( $query->query_vars['sub-type'] ) || self::QUERY_LOOP_EVENTS !== $query->query_vars['sub-type'] ) { + return $posts; + } + + // Return back the + return array_map( + function ( $post ) { + $parent = get_post( $post->post_parent ); + + // Get the start date from the event. + $start_date_ts = get_post_meta( $post->ID, 'se_event_date_start', true ); + + // Get the event timezone. + $timezone = get_post_meta( $parent->ID, 'se_event_timezone', true ); + // use the timezone or default to the site timezone. + $timezone = $timezone ? $timezone : wp_timezone_string(); + + // Get the date im this format 2025-07-01 13:14:09 + $start_date = wp_date( 'Y-m-d H:i:s', $start_date_ts, new \DateTimeZone( $timezone ) ); + $start_date_gmt = wp_date( 'Y-m-d H:i:s', $start_date_ts, new \DateTimeZone( 'UTC' ) ); + + // update the parent posts post date + $parent->post_date = $start_date; + $parent->post_date_gmt = $start_date_gmt; + $parent->post_modified = $start_date; + $parent->post_modified_gmt = $start_date_gmt; + $parent->event_date_id = $post->ID; + + return $parent; + }, + $posts + ); + } + /** * Set the query args for the event loop query admin. * @@ -112,7 +159,7 @@ public function build_query( $query ) { public function set_admin_query( $args, $request ) { $feed_type = $request->get_param( 'feedType' ); - $feed_order = $request->get_param( 'order' ); + $feed_order = $request->get_param( 'order' );# return $this->set_event_query_args( $args, $feed_type, $feed_order ); } @@ -128,6 +175,13 @@ public function set_admin_query( $args, $request ) { */ private function set_event_query_args( $args, $feed_type, $feed_order = 'ASC' ) { + // If we are ordering by desc. we need to sort by end date, else start. + $args['meta_key'] = 'desc' === strtolower( $feed_order ) ? 'se_event_date_end' : 'se_event_date_start'; + $args['orderby'] = 'meta_value'; + $args['order'] = $feed_order; + + $args['sub-type'] = self::QUERY_LOOP_EVENTS; + if ( 'upcoming' === $feed_type ) { $args['meta_query'] = array( array( @@ -156,6 +210,19 @@ private function set_event_query_args( $args, $feed_type, $feed_order = 'ASC' ) $args['order'] = $feed_order; } + // add the arg to denote unique parents. + $args['unique_parents'] = true; + $args['feed_order'] = $feed_order; // Store feed order for use in the WHERE filter + + // Ensure we only get the correct event date for each parent. + add_filter( 'posts_where', array( $this, 'filter_unique_parents_where' ), 10, 2 ); + + // Add a filter to modify the posts results. + add_filter( 'the_posts', array( $this, 'modify_event_posts' ), 10, 2 ); + + // Add a custom order by. + add_filter( 'posts_orderby', array( $this, 'fix_editor_sort_order' ), 10, 2 ); + /** * A filter to customize the args of the event query loop. * @@ -165,6 +232,101 @@ private function set_event_query_args( $args, $feed_type, $feed_order = 'ASC' ) */ return apply_filters( 'se_pre_set_event_query_loop_args', $args, $feed_type, $feed_order ); } + + /** + * Ensure the sort order is correctly set for the unique parents query. + * + * This fixes a weird bug where the admin/editor order is always ASC. + * + * @param string $orderby The current orderby clause. + * @param WP_Query $query The WP_Query instance. + * + * @return string + */ + public function fix_editor_sort_order( $orderby, $query ) { + if ( isset( $query->query_vars['unique_parents'] ) && $query->query_vars['unique_parents'] ) { + if ( str_ends_with( $orderby, '+0 ASC' ) ) { + $feed_order = isset( $query->query_vars['feed_order'] ) ? $query->query_vars['feed_order'] : 'ASC'; + $new_order = sprintf( ' %s', strtoupper( $feed_order ) ); + $orderby = str_replace( '+0 ASC', $new_order, $orderby ); + } + } + + return $orderby; + } + + + /** + * Filter posts to only include the correct event date for each parent. + * + * @param string $where The WHERE clause of the query. + * @param WP_Query $query The WP_Query instance. + * + * @return string + */ + public function filter_unique_parents_where( $where, $query ) { + // Check if this is our events query and unique parents is enabled + if ( ! isset( $query->query_vars['unique_parents'] ) || ! isset( $query->query_vars['feed_order'] ) ) { + return $where; + } + + // Skip if treating each date as own event + if ( se_event_treat_each_date_as_own_event() ) { + return $where; + } + + global $wpdb; + + $feed_order = $query->query_vars['feed_order']; + $meta_key = 'desc' === $feed_order ? 'se_event_date_end' : 'se_event_date_start'; + // Get the current time filtering from the main query's meta_query + $time_filter = ''; + $meta_query = $query->get( 'meta_query' ); + if ( ! empty( $meta_query ) && is_array( $meta_query ) ) { + foreach ( $meta_query as $meta_condition ) { + if ( isset( $meta_condition['key'] ) && 'se_event_date_end' === $meta_condition['key'] ) { + $compare = $meta_condition['compare']; + $value = $meta_condition['value']; + + // Add the same time filtering to the subquery + if ( '>=' === $compare ) { + // For upcoming events + $time_filter = "AND pm3.meta_value >= {$value}"; + } elseif ( '<' === $compare ) { + // For past events + $time_filter = "AND pm3.meta_value < {$value}"; + } + break; + } + } + } + + // Subquery to get the correct post ID for each parent based on sort order + $subquery = " + AND {$wpdb->posts}.ID IN ( + SELECT p1.ID + FROM {$wpdb->posts} p1 + INNER JOIN {$wpdb->postmeta} pm1 ON p1.ID = pm1.post_id AND pm1.meta_key = '{$meta_key}' + WHERE p1.post_type = '" . SE_Event_Post_Type::$event_date_post_type . "' + AND p1.post_status = 'publish' + AND pm1.meta_value = ( + SELECT " . ( 'desc' === $feed_order ? 'MAX' : 'MIN' ) . "(pm2.meta_value) + FROM {$wpdb->posts} p2 + INNER JOIN {$wpdb->postmeta} pm2 ON p2.ID = pm2.post_id AND pm2.meta_key = '{$meta_key}' + " . ( $time_filter ? "INNER JOIN {$wpdb->postmeta} pm3 ON p2.ID = pm3.post_id AND pm3.meta_key = 'se_event_date_end'" : '' ) . " + WHERE p2.post_parent = p1.post_parent + AND p2.post_type = '" . SE_Event_Post_Type::$event_date_post_type . "' + AND p2.post_status = 'publish' + {$time_filter} + ) + GROUP BY p1.post_parent + ) + "; + + $where .= $subquery; + + return $where; + } } ( new SE_Block_Variations() )->init(); diff --git a/src/classes/class-se-blocks.php b/src/classes/class-se-blocks.php index bdf2f6f..e9d0543 100644 --- a/src/classes/class-se-blocks.php +++ b/src/classes/class-se-blocks.php @@ -121,6 +121,11 @@ public static function block_assets() { $block_settings['pastEventsNotice'] = $value; $block_settings['postType'] = get_post_type(); + // Pass through new event data information. + $block_settings['eventVersion'] = SE_Event_Post_Type::$current_event_version; + $block_settings['eventDatePostType'] = SE_Event_Post_Type::$event_date_post_type; + $block_settings['syncDatesNonce'] = wp_create_nonce( 'se_event_nonce' ); + wp_localize_script( 'wp-blocks', 'seSettings', @@ -236,10 +241,12 @@ public static function event_info_render( $attributes, $content, $block ) { $post_ID = isset( $block->context['postId'] ) ? $block->context['postId'] : get_the_ID(); + $date_display_formatter = new SE_Date_Display_Formatter( $post_ID ); + $output = ''; // Event time / date. - $event_dates = get_post_meta( $post_ID, 'se_event_dates', true ); + $event_dates = se_event_get_event_dates( $post_ID ); // Previewing? if ( ! empty( $attributes['eventDates'] ) ) { @@ -252,13 +259,42 @@ public static function event_info_render( $attributes, $content, $block ) { // Previewing? if ( isset( $attributes['eventTimezone'] ) ) { $event_timezone = $attributes['eventTimezone']; + $date_display_formatter->modify_timezone( $event_timezone ); + } + + // If we are hiding past dates. + $options = get_option( 'se_options' ); + $event_options = array( 'hide_events_on_both', 'hide_events_on_feed', 'on' ); + if ( isset( $options['hide_past_events'] ) && in_array( $options['hide_past_events'], $event_options, true ) ) { + try { + $ts = '' !== $event_timezone ? new \DateTimeZone( $event_timezone ) : null; + } catch ( \Throwable $th ) { + $ts = null; + } + + $now = se_create_date_time_from_timestamp( time(), $ts )->getTimestamp(); + // Remove any dates that have passed. + $event_dates = array_filter( + $event_dates, + fn( array $date ): bool => ! isset( $date['end_date'] ) || $date['end_date'] >= $now + ); } $dates_output = ''; if ( ! empty( $event_dates ) ) { - $dates_count = count( $event_dates ); - $date_heading = '

    ' . _n( 'Date', 'Dates', $dates_count, 'simple-events' ) . '

    '; + $has_header_date = false; + $dates_count = count( $event_dates ); + $active_date = $date_display_formatter->render_active_date( $event_dates ); + if ( $active_date ) { + $dates_output .= '
    ' . $active_date . '
    '; + $has_header_date = true; + if ( $dates_count > 1 ) { + $date_heading = '

    ' . _n( 'Additional Date', 'Additional Dates', $dates_count - 1, 'simple-events' ) . '

    '; + } + } else { + $date_heading = '

    ' . _n( 'Date', 'Dates', $dates_count, 'simple-events' ) . '

    '; + } /** * Filter the markup used for the date heading. @@ -267,7 +303,10 @@ public static function event_info_render( $attributes, $content, $block ) { * @param int $dates_count The number of event dates. */ $dates_output .= apply_filters( 'se_event_info_date_heading', $date_heading, $dates_count ); - $dates_output .= se_event_get_formatted_dates( $post_ID, false, false, $event_dates ); + // If we have a header date and 2 or more dates, we need to exclude the current date from the list. + if ( ( $has_header_date && $dates_count > 1 ) || ! $has_header_date ) { + $dates_output .= $date_display_formatter->render_date_list( $event_dates, ( $has_header_date && $date_display_formatter->is_treating_each_date_as_own_event() ) ); + } } // Event location. @@ -455,21 +494,22 @@ private static function render_ticket( $product ) { /** * Render upcoming events block. * - * @param array $attributes Block attributes. - * @param string $content Block content. + * @param array $attributes The attributes passed to the block renderer. + * @param string $content The content of the event. * * @return HTML Upcoming events render. + * + * @todo: This is a mess. We need to refactor this. */ public static function upcoming_events_render( $attributes = array(), $content = '' ) { $events_query_args = array(); $events_query = null; $output = ''; - if ( ! empty( $attributes['count'] ) ) { // By default shows the "mixed" feed type (no meta_query). $events_query_args = array( - 'post_type' => SE_Event_Post_Type::$post_type, + 'post_type' => SE_Event_Post_Type::$event_date_post_type, 'post_status' => 'publish', 'posts_per_page' => absint( $attributes['count'] ), ); @@ -521,10 +561,32 @@ public static function upcoming_events_render( $attributes = array(), $content = $events_query_args['se_event_order'] = $attributes['feedOrder']; } + // Set up sorting and unique parent filtering + $feed_order = ! empty( $attributes['feedOrder'] ) ? $attributes['feedOrder'] : 'ASC'; + + // Set meta key and order based on feed order + $events_query_args['meta_key'] = 'desc' === strtolower( $feed_order ) ? 'se_event_date_end' : 'se_event_date_start'; + $events_query_args['orderby'] = 'meta_value'; + $events_query_args['order'] = $feed_order; + + // Add unique parents filtering if not treating each date as own event + if ( ! se_event_treat_each_date_as_own_event() ) { + $events_query_args['unique_parents'] = true; + $events_query_args['feed_order'] = $feed_order; + + // Add filter for unique parents WHERE clause + add_filter( 'posts_where', array( __CLASS__, 'filter_unique_parents_where' ), 10, 2 ); + } + $show_year_dividers = ! empty( $attributes['showYearDividers'] ); $events_query = new \WP_Query( $events_query_args ); + // Add filter to modify posts for event_date_id if not treating each date as own event + if ( ! se_event_treat_each_date_as_own_event() ) { + add_filter( 'the_posts', array( __CLASS__, 'modify_event_posts_for_blocks' ), 10, 2 ); + } + if ( $events_query->have_posts() ) { $container_class[] = 'wp-block-se-upcoming-events'; $container_class[] = 'wp-block-se-upcoming-events-view-' . $attributes['layout']; @@ -554,6 +616,12 @@ public static function upcoming_events_render( $attributes = array(), $content = $output = $content; } + // Clean up filters + if ( ! se_event_treat_each_date_as_own_event() ) { + remove_filter( 'posts_where', array( __CLASS__, 'filter_unique_parents_where' ), 10 ); + remove_filter( 'the_posts', array( __CLASS__, 'modify_event_posts_for_blocks' ), 10 ); + } + wp_reset_postdata(); } @@ -731,14 +799,29 @@ public static function calendar_render( $attributes = array() ) { * @return string Returns the filtered post date for the current post wrapped inside "time" tags. */ public static function loop_event_info_render( $attributes, $content, $block ): string { - - $output = ''; - $prefix = ''; - $post_ID = ( isset( $attributes['thePostId'] ) && $attributes['thePostId'] > 0 ) ? $attributes['thePostId'] : $block->context['postId']; + global $post; + $output = ''; + $prefix = ''; + $post_ID = ( isset( $attributes['thePostId'] ) && $attributes['thePostId'] > 0 ) ? $attributes['thePostId'] : $block->context['postId']; + $event_date_id = $post instanceof \WP_Post && property_exists( $post, 'event_date_id' ) && is_numeric( $post->event_date_id ) && se_event_treat_each_date_as_own_event() + ? absint( $post->event_date_id ) + : null; if ( isset( $attributes['metaPrefix'] ) ) { $prefix = '' . esc_html( $attributes['metaPrefix'] ) . ''; } + // Based on the feed type, set the get_date_function + switch ( $attributes['feedType'] ?? 'default' ) { + case 'upcoming': + $get_date_function = 'se_event_get_future_dates'; + break; + case 'past': + $get_date_function = 'se_event_get_past_dates'; + break; + default: + $get_date_function = 'se_event_get_formatted_dates'; + break; + } // Generate output based on meta name. if ( ! empty( $post_ID ) ) { @@ -750,13 +833,13 @@ public static function loop_event_info_render( $attributes, $content, $block ): $output = se_event_get_venue( $post_ID ); break; case 'dates': - $output = se_event_get_future_dates( $post_ID ); + $output = $get_date_function( $post_ID, $event_date_id ); break; case 'date': - $output = se_event_get_future_dates( $post_ID, true, false ); + $output = $get_date_function( $post_ID, $event_date_id, true, false ); break; case 'time': - $output = se_event_get_future_dates( $post_ID, false, true ); + $output = $get_date_function( $post_ID, $event_date_id, false, true ); break; } } @@ -893,6 +976,123 @@ public static function add_event_query_vars( $vars ) { return $vars; } + + /** + * Filter posts to only include the correct event date for each parent. + * + * @param string $where The WHERE clause of the query. + * @param WP_Query $query The WP_Query instance. + * + * @return string + */ + public static function filter_unique_parents_where( $where, $query ) { + // Check if this is our events query and unique parents is enabled + if ( ! isset( $query->query_vars['unique_parents'] ) || ! isset( $query->query_vars['feed_order'] ) ) { + return $where; + } + + // Skip if treating each date as own event + if ( se_event_treat_each_date_as_own_event() ) { + return $where; + } + + global $wpdb; + + $feed_order = $query->query_vars['feed_order']; + $meta_key = 'desc' === strtolower( $feed_order ) ? 'se_event_date_end' : 'se_event_date_start'; + + // Get the current time filtering from the main query's meta_query + $time_filter = ''; + $meta_query = $query->get( 'meta_query' ); + if ( ! empty( $meta_query ) && is_array( $meta_query ) ) { + foreach ( $meta_query as $meta_condition ) { + if ( isset( $meta_condition['key'] ) && 'se_event_date_end' === $meta_condition['key'] ) { + $compare = $meta_condition['compare']; + $value = $meta_condition['value']; + + // Add the same time filtering to the subquery + if ( '>=' === $compare ) { + // For upcoming events + $time_filter = "AND pm3.meta_value >= {$value}"; + } elseif ( '<' === $compare ) { + // For past events + $time_filter = "AND pm3.meta_value < {$value}"; + } + break; + } + } + } + + // Subquery to get the correct post ID for each parent based on sort order + $subquery = " + AND {$wpdb->posts}.ID IN ( + SELECT p1.ID + FROM {$wpdb->posts} p1 + INNER JOIN {$wpdb->postmeta} pm1 ON p1.ID = pm1.post_id AND pm1.meta_key = '{$meta_key}' + WHERE p1.post_type = '" . SE_Event_Post_Type::$event_date_post_type . "' + AND p1.post_status = 'publish' + AND pm1.meta_value = ( + SELECT " . ( 'desc' === strtolower( $feed_order ) ? 'MAX' : 'MIN' ) . "(pm2.meta_value) + FROM {$wpdb->posts} p2 + INNER JOIN {$wpdb->postmeta} pm2 ON p2.ID = pm2.post_id AND pm2.meta_key = '{$meta_key}' + " . ( $time_filter ? "INNER JOIN {$wpdb->postmeta} pm3 ON p2.ID = pm3.post_id AND pm3.meta_key = 'se_event_date_end'" : '' ) . " + WHERE p2.post_parent = p1.post_parent + AND p2.post_type = '" . SE_Event_Post_Type::$event_date_post_type . "' + AND p2.post_status = 'publish' + {$time_filter} + ) + GROUP BY p1.post_parent + ) + "; + + $where .= $subquery; + + return $where; + } + + /** + * Modify event posts results for blocks. + * + * @param array $posts The array of post objects. + * @param WP_Query $query The WP_Query instance. + * + * @return array + */ + public static function modify_event_posts_for_blocks( $posts, $query ) { + // Check if this is our events query + if ( ! isset( $query->query_vars['unique_parents'] ) || ! $query->query_vars['unique_parents'] ) { + return $posts; + } + + // Return back the modified posts with parent info and event_date_id + return array_map( + function ( $post ) { + $parent = get_post( $post->post_parent ); + + // Get the start date from the event. + $start_date_ts = get_post_meta( $post->ID, 'se_event_date_start', true ); + + // Get the event timezone. + $timezone = get_post_meta( $parent->ID, 'se_event_timezone', true ); + // use the timezone or default to the site timezone. + $timezone = $timezone ? $timezone : get_option( 'timezone_string' ); + + // Get the date in this format 2025-07-01 13:14:09 + $start_date = wp_date( 'Y-m-d H:i:s', $start_date_ts, new \DateTimeZone( $timezone ) ); + $start_date_gmt = wp_date( 'Y-m-d H:i:s', $start_date_ts, new \DateTimeZone( 'UTC' ) ); + + // update the parent posts post date + $parent->post_date = $start_date; + $parent->post_date_gmt = $start_date_gmt; + $parent->post_modified = $start_date; + $parent->post_modified_gmt = $start_date_gmt; + $parent->event_date_id = $post->ID; + + return $parent; + }, + $posts + ); + } } SE_Blocks::init(); diff --git a/src/classes/class-se-calendar-export.php b/src/classes/class-se-calendar-export.php index 6fc8db7..341eef9 100644 --- a/src/classes/class-se-calendar-export.php +++ b/src/classes/class-se-calendar-export.php @@ -72,20 +72,25 @@ public static function icalendar() { // Get dates. if ( ! empty( $events ) ) { foreach ( $events as $event_id ) { - $event_dates = se_event_get_dates( $event_id ); + $event_dates = se_event_get_event_dates( $event_id ); foreach ( $event_dates as $event_date ) { $v_event = new \Eluceo\iCal\Component\Event(); - if ( empty( $event_date['datetime_start'] ) || empty( $event_date['datetime_end'] ) ) { + // If the date is hidden from the calendar, skip it. + if ( true === (bool) $event_date['hide_from_calendar'] ) { + continue; + } + + if ( empty( $event_date['start_date'] ) || empty( $event_date['end_date'] ) ) { continue; } $date_start = new \DateTime(); - $date_start->setTimestamp( $event_date['datetime_start'] ); + $date_start->setTimestamp( $event_date['start_date'] ); $date_end = new \DateTime(); - $date_end->setTimestamp( $event_date['datetime_end'] ); + $date_end->setTimestamp( $event_date['end_date'] ); $v_event ->setDtStart( $date_start ) diff --git a/src/classes/class-se-calendar.php b/src/classes/class-se-calendar.php index b13db20..a408109 100644 --- a/src/classes/class-se-calendar.php +++ b/src/classes/class-se-calendar.php @@ -124,34 +124,6 @@ public function create_date_time( $date_time, $timezone = null ): DateTime { } - /** - * Create a DateTime object from a timestamp, with an optional timezone. - * - * @param mixed $timestamp The Unix timestamp to create the DateTime object from. - * @param string|null $timezone The timezone to be used, or null to use the site timezone. - * - * @return DateTime The created DateTime object. - */ - public function create_date_time_from_timestamp( $timestamp, $timezone = null ): DateTime { - /** - * If no timezone is passed, use the site timezone - */ - if ( null === $timezone ) { - $timezone = wp_timezone_string(); - } - - try { - $date_time_object = new DateTime( 'now', new DateTimeZone( $timezone ) ); - $date_time_object->setTimestamp( $timestamp ); - } catch ( Exception $e ) { - $date_time_object = new DateTime(); - // todo handle exception - } - - return $date_time_object->setTimestamp( $timestamp ); - } - - /** * Retrieves the days of the month and related event information. * @@ -272,19 +244,44 @@ public function get_previous_month_with_events( $current_date ) { $previous_date_time->modify( 'first day of this month' ); $previous_date_time->settime( 0, 0, 0 ); - $sql_query = $wpdb->prepare( - "SELECT start_meta.meta_value from {$wpdb->prefix}postmeta as start_meta WHERE start_meta.meta_key = 'se_event_date_start' AND start_meta.meta_value < %s ORDER BY start_meta.meta_value DESC LIMIT 1;", - $previous_date_time->getTimestamp() + $args = array( + 'post_type' => SE_Event_Post_Type::$event_date_post_type, + 'meta_query' => array( + 'relation' => 'AND', + array( + 'key' => 'se_event_date_start', + 'value' => $previous_date_time->getTimestamp(), + 'compare' => '<', + ), + array( + 'relation' => 'OR', + array( + 'key' => 'se_event_hide_from_calendar', + 'compare' => 'NOT EXISTS', + ), + array( + 'key' => 'se_event_hide_from_calendar', + 'value' => '1', + 'compare' => '!=', + ), + ), + ), + 'orderby' => 'meta_value', + 'order' => 'DESC', + 'limit' => 1, ); - // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared - $previous_event = $wpdb->get_row( $sql_query ); // the query is prepared above + $query = new WP_Query( $args ); + + if ( $query->have_posts() ) { + $previous_event = $query->posts[0]; - if ( empty( $previous_event ) ) { + // Get the start date of the previous event. + $previous_event_start_date = get_post_meta( $previous_event->ID, 'se_event_date_start', true ); + return se_create_date_time_from_timestamp( $previous_event_start_date ); + } else { return null; } - - return $this->create_date_time_from_timestamp( $previous_event->meta_value ); } /** @@ -295,36 +292,68 @@ public function get_previous_month_with_events( $current_date ) { * @return DateTime|null */ public function get_next_month_with_events( $current_date ) { - global $wpdb; - $next_date_time = clone $current_date; - $next_date_time->modify( 'last day of this month' ); - $next_date_time->settime( 23, 23, 59 ); - - $sql_query = $wpdb->prepare( - "SELECT start_meta.meta_value from {$wpdb->prefix}postmeta as start_meta WHERE start_meta.meta_key = 'se_event_date_start' AND start_meta.meta_value > %s ORDER BY start_meta.meta_value ASC LIMIT 1;", - $next_date_time->getTimestamp() - ); - - // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared - $next_event = $wpdb->get_row( $sql_query ); // the query is prepared above - - // If we dont have a next date, check if we have any end dates. - if ( empty( $next_event ) ) { - $sql_query = $wpdb->prepare( - "SELECT end_meta.meta_value from {$wpdb->prefix}postmeta as end_meta WHERE end_meta.meta_key = 'se_event_date_end' AND end_meta.meta_value > %s ORDER BY end_meta.meta_value ASC LIMIT 1;", - $next_date_time->getTimestamp() + /** + * Compile a shared set of query args. + * + * @var string $meta_key The meta key to use for the query. + * @var DateTime $current_date The current date. + * + * @return array The query arguments. + */ + $query_args = function ( string $meta_key ) use ( $current_date ) { + $next_date_time = clone $current_date; + $next_date_time->modify( 'last day of this month' ); + $next_date_time->settime( 23, 59, 59 ); + + return array( + 'post_type' => SE_Event_Post_Type::$event_date_post_type, + 'meta_query' => array( + 'relation' => 'AND', + array( + 'key' => $meta_key, + 'value' => $next_date_time->getTimestamp(), + 'compare' => '>', + ), + array( + 'relation' => 'OR', + array( + 'key' => 'se_event_hide_from_calendar', + 'compare' => 'NOT EXISTS', + ), + array( + 'key' => 'se_event_hide_from_calendar', + 'value' => '1', + 'compare' => '!=', + ), + ), + ), + 'orderby' => 'meta_value', + 'order' => 'ASC', + 'limit' => 1, ); - - // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared - $next_event = $wpdb->get_row( $sql_query ); // the query is prepared above + }; + + $next_event = null; + + // At first try to get the next event with the start date. + $query = new WP_Query( $query_args( 'se_event_date_start' ) ); + if ( $query->have_posts() ) { + $next_event = $query->posts[0]; + } else { + // If we don't have a next event with the start date, try with the end date. + $query = new WP_Query( $query_args( 'se_event_date_end' ) ); + if ( $query->have_posts() ) { + $next_event = $query->posts[0]; + } } if ( empty( $next_event ) ) { return null; } - - return $this->create_date_time_from_timestamp( $next_event->meta_value ); + // Get the start date of the next event. + $next_event_start_date = get_post_meta( $next_event->ID, 'se_event_date_start', true ); + return se_create_date_time_from_timestamp( $next_event_start_date ); } @@ -340,57 +369,35 @@ private function get_events_by_date( $date ): array { $day_events = array(); - $start_timestamp = $date->setTime( 0, 0, 0 )->getTimeStamp(); - $end_timestamp = $date->setTime( 23, 59, 59 )->getTimestamp(); - - $sql_query = $wpdb->prepare( - " -SELECT * from {$wpdb->prefix}posts -INNER JOIN {$wpdb->prefix}postmeta AS start_meta ON {$wpdb->prefix}posts.ID = start_meta.post_id AND start_meta.meta_key = 'se_event_date_start' -INNER JOIN {$wpdb->prefix}postmeta AS end_meta ON {$wpdb->prefix}posts.ID = end_meta.post_id AND end_meta.meta_key = 'se_event_date_end' -WHERE wp_posts.post_type = %s AND (wp_posts.post_status = 'publish') AND -((start_meta.meta_value >= %s AND start_meta.meta_value < %s) -OR -(start_meta.meta_value < %s AND end_meta.meta_value > %s) -OR -(end_meta.meta_value <= %s AND end_meta.meta_value > %s)) -GROUP BY {$wpdb->prefix}posts.ID -ORDER BY start_meta.meta_value ASC;", - 'se-event', - $start_timestamp, - $end_timestamp, - $start_timestamp, - $end_timestamp, - $end_timestamp, - $start_timestamp - ); - - // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared - $all_events = $wpdb->get_results( $sql_query ); // the query is prepared above - if ( $all_events ) { - foreach ( $all_events as $event ) { - $event_dates = se_event_get_dates( $event->ID ); + $new_all = SE_Event_Dates::get_event_dates_for_date( $date->format( 'Y-m-d' ), true, false ); + // If we new dates. + if ( ! empty( $new_all ) ) { + foreach ( $new_all as $event ) { - if ( ! $event_dates ) { + // Get the parent post. + $parent_post = get_post( $event['event_id'] ); + if ( ! $parent_post ) { continue; } - foreach ( $event_dates as $event_date ) { - $event->event_start_date = $this->create_date_time_from_timestamp( $event_date['datetime_start'] ); - $event->event_end_date = $this->create_date_time_from_timestamp( $event_date['datetime_end'] ); - $event->hide_start_time = '1' === get_post_meta( $event->ID, 'se_event_hide_start_time', true ); - $event->hide_end_time = '1' === get_post_meta( $event->ID, 'se_event_hide_end_time', true ); - if ( $event_date['datetime_start'] >= $start_timestamp && $event_date['datetime_start'] <= $end_timestamp ) { - $new_event = clone $event; - $new_event->event_start_date = $this->create_date_time_from_timestamp( $event_date['datetime_start'] ); - $new_event->event_end_date = $this->create_date_time_from_timestamp( $event_date['datetime_end'] ); - $new_event->open_in_new_window = (bool) get_post_meta( $event->ID, 'se_event_open_in_new_window', true ); - - $day_events[] = $new_event; - } - } + $event['ID'] = $parent_post->ID; + $event['post_title'] = $parent_post->post_title; + $event['post_content'] = $parent_post->post_content; + $event['post_excerpt'] = $parent_post->post_excerpt; + $event['post_date'] = $parent_post->post_date; + $event['post_date_gmt'] = $parent_post->post_date_gmt; + $event['post_modified'] = $parent_post->post_modified; + // Add the meta. + $event['event_start_date'] = se_create_date_time_from_timestamp( $event['event_start_date'] ); + $event['event_end_date'] = se_create_date_time_from_timestamp( $event['event_end_date'] ); + $event['hide_start_time'] = '1' === get_post_meta( $parent_post->ID, 'se_event_hide_start_time', true ); + $event['hide_end_time'] = '1' === get_post_meta( $parent_post->ID, 'se_event_hide_end_time', true ); + $event['open_in_new_window'] = (bool) get_post_meta( $parent_post->ID, 'se_event_open_in_new_window', true ); + + $day_events[] = (object) $event; } } + return $day_events; } diff --git a/src/classes/class-se-event-dates.php b/src/classes/class-se-event-dates.php new file mode 100644 index 0000000..3de1f63 --- /dev/null +++ b/src/classes/class-se-event-dates.php @@ -0,0 +1,436 @@ +namespace, + '/' . $this->rest_base_dates . '/(?P[\d]+)', + array( + 'methods' => 'GET', + 'callback' => array( $this, 'get_event_dates' ), + 'permission_callback' => '__return_true', + ) + ); + + register_rest_route( + $this->namespace, + '/' . $this->rest_base_dates . '/(?P[\d]+)/sync', + array( + 'methods' => 'POST', + 'callback' => array( $this, 'sync_event_dates' ), + 'permission_callback' => function () { + return current_user_can( 'edit_posts' ); + }, + 'args' => array( + 'dates' => array( + 'required' => true, + 'type' => 'array', + 'description' => 'Array of date objects from dateManager', + ), + ), + ) + ); + } + + /** + * Get event dates. + * + * @param WP_REST_Request $request The request object. + * + * @return WP_REST_Response + */ + public function get_event_dates( WP_REST_Request $request ): WP_REST_Response { + // If we dont have an event ID, return an error. + $event_id = $request->get_param( 'event_id' ); + if ( empty( $event_id ) || ! is_numeric( $event_id ) ) { + return new WP_REST_Response( + array( + 'code' => 'invalid_event_id', + 'message' => __( 'Invalid event ID provided.', 'simple-events' ), + ), + 400 + ); + } + + // Check if we have a valid event. + $event = get_post( $event_id ); + if ( ! $event || 'se-event' !== $event->post_type ) { + return new WP_REST_Response( + array( + 'code' => 'invalid_event', + 'message' => __( 'Invalid event provided.', 'simple-events' ), + ), + 404 + ); + } + + try { + $dates = se_event_get_event_dates( $event_id ); + } catch ( \Throwable $th ) { + return new WP_REST_Response( + array( + 'code' => 'server_error', + 'message' => __( 'An error occurred while fetching event dates.', 'simple-events' ), + ), + 500 + ); + } + + // Create the return. + $data = array( + 'event_id' => $event_id, + 'dates' => $dates, + 'timezone' => get_post_meta( $event_id, 'se_event_timezone', true ) ?: wp_timezone_string(), // phpcs:ignore + ); + + // Return the response. + return new WP_REST_Response( + $data, + 200 + ); + } + + /** + * Sync event dates. + * + * @param WP_REST_Request $request The request object. + * + * @return WP_REST_Response + */ + public function sync_event_dates( WP_REST_Request $request ): WP_REST_Response { + $event_id = $request->get_param( 'event_id' ); + $dates = $request->get_param( 'dates' ); + $nonce = $request->get_param( 'nonce' ); + + // Check if the nonce is valid. + if ( ! wp_verify_nonce( $nonce, 'se_event_nonce' ) ) { + return new WP_REST_Response( + array( + 'code' => 'invalid_nonce', + 'message' => __( 'Invalid nonce provided.', 'simple-events' ), + ), + 403 + ); + } + + // Get the existing dates. + $existing_date_ids = array_map( + function ( $date ) { + return $date['id']; + }, + se_event_get_event_dates( $event_id ) + ); + + // Iterate over the existing dates and delete any that are not in the new dates. + foreach ( $existing_date_ids as $existing_date_id ) { + if ( ! in_array( $existing_date_id, array_column( $dates, 'id' ), true ) ) { + wp_delete_post( $existing_date_id, true ); + } + } + // Iterate over the dates and update the event dates. + foreach ( $dates as $date ) { + // If we dont have a date ID, create a new date. + if ( ! isset( $date['id'] ) ) { + $event_date = se_event_create_event_date( $event_id, $date ); + // If we dont have a WP_Post object, return an error. + if ( ! $event_date ) { + return new WP_REST_Response( + array( + 'code' => 'server_error', + 'message' => __( 'An error occurred while creating the event date.', 'simple-events' ), + ), + 500 + ); + } + $date['id'] = $event_date->ID; + } + + // Update the even dates meta. + $event_date_id = absint( $date['id'] ); + update_post_meta( $event_date_id, 'se_event_date_start', esc_attr( $date['start_date'] ) ); + update_post_meta( $event_date_id, 'se_event_date_end', esc_attr( $date['end_date'] ) ); + update_post_meta( $event_date_id, 'se_event_all_day', boolval( $date['all_day'] ) ); + update_post_meta( $event_date_id, 'se_event_hide_from_calendar', boolval( $date['hide_from_calendar'] ) ); + update_post_meta( $event_date_id, 'se_event_hide_from_feed', boolval( $date['hide_from_feed'] ) ); + } + + // Update the event version. + update_post_meta( $event_id, 'se_event_version', SE_MIGRATION_VERSION ); + + // Re fetch the event dates. + try { + $dates = se_event_get_event_dates( $event_id ); + } catch ( \Throwable $th ) { + return new WP_REST_Response( + array( + 'code' => 'server_error', + 'message' => __( 'An error occurred while fetching event dates.', 'simple-events' ), + ), + 500 + ); + } + + // Update all legacy meta values. + self::update_legacy_meta_values( $event_id, $dates ); + + // Return the response. + return new WP_REST_Response( + array( + 'code' => 'success', + 'message' => __( 'Event dates synced successfully.', 'simple-events' ), + 'dates' => $dates, + ), + 200 + ); + } + + /** + * Update all legacy meta values. + * + * @param integer $event_id The event ID. + * @param array $dates The dates. + * + * @return void + */ + public static function update_legacy_meta_values( $event_id, $dates ): void { + // Create the legacy date array. + $legacy_dates = array_map( + function ( $date ) { + return array( + 'datetime_start' => $date['start_date'], + 'datetime_end' => $date['end_date'], + 'all_day' => $date['all_day'], + ); + }, + $dates + ); + + // Update the legacy meta values. + update_post_meta( $event_id, 'se_event_dates', $legacy_dates ); + + se_event_update_event_query_dates( $event_id ); + } + + /** + * Fiind event dates. + * + * @param string $start_date The start date as a timestamp. + * @param string $end_date The end date as a timestamp. + * @param boolean $hide_from_calendar Whether the event is hidden from the calendar. + * @param boolean $hide_from_feed Whether the event is hidden from the feed. + * + * @return array + */ + public static function find_event_dates( $start_date, $end_date, $hide_from_calendar, $hide_from_feed ): array { + // Create the timestamp for the start and end of the $start_date. + $start_date_time = se_create_date_time_from_timestamp( $start_date )->setTimezone( wp_timezone() ); + + $start_date_range = array( + $start_date_time->setTime( 0, 0, 0 )->getTimestamp(), + $start_date_time->setTime( 23, 59, 59 )->getTimestamp(), + ); + + // Query the event dates. + $query = new WP_Query( + array( + 'post_type' => SE_Event_Post_Type::$event_date_post_type, + 'meta_query' => array( + 'relation' => 'OR', + // Exact match for all conditions + array( + 'relation' => 'AND', + array( + 'key' => 'se_event_date_start', + 'value' => $start_date, + 'compare' => '>=', + ), + array( + 'key' => 'se_event_date_end', + 'value' => $end_date, + 'compare' => '<=', + ), + ), + // All day events with matching start date + array( + 'relation' => 'AND', + array( + 'key' => 'se_event_date_start', + 'value' => $start_date_range, + 'compare' => 'BETWEEN', + ), + array( + 'key' => 'se_event_all_day', + 'value' => '1', + 'compare' => '=', + ), + ), + ), + 'posts_per_page' => -1, + 'orderby' => 'meta_value', + 'meta_key' => 'se_event_date_start', + 'order' => 'ASC', + 'post_status' => 'publish', + ) + ); + + $mapped = self::map_events_dates_to_event_dates( $query->posts ); + + // Remove the event dates that are hidden from the calendar or feed. + return array_filter( + $mapped, + function ( $event_date ) use ( $hide_from_calendar, $hide_from_feed ) { + return ! $event_date['event_hide_from_calendar'] && ! $event_date['event_hide_from_feed']; + } + ); + } + + /** + * Get the events dates for a given date. + * + * @param string $date The date to get the events for. + * @param boolean $hide_from_calendar Whether the event is hidden from the calendar. + * @param boolean $hide_from_feed Whether the event is hidden from the feed. + * + * @return array + */ + public static function get_event_dates_for_date( $date, $hide_from_calendar = false, $hide_from_feed = false ): array { + // Create date explicitly in site timezone + $date_time = DateTime::createFromFormat( 'Y-m-d H:i:s', $date . ' 00:00:00', wp_timezone() ); + + // Set the time to the start of the day. + $date_time->setTime( 0, 0, 0 ); + // set as a timestamp. + $start_date = $date_time->getTimestamp(); + + // Get the end of the day for the date. + $date_time->setTime( 23, 59, 59 ); + $end_date = $date_time->getTimestamp(); + + // Get the events dates. + $events_dates = self::find_event_dates( $start_date, $end_date, $hide_from_calendar, $hide_from_feed ); + + // Return the events dates. + return $events_dates; + } + + /** + * Map the events dates to the event dates. + * + * @param array $events_dates The events dates. + * + * @return array{event_id: int, event_date_id: int, event_start_date: string, event_end_date: string, event_all_day: bool, event_hide_from_calendar: bool, event_hide_from_feed: bool} + */ + public static function map_events_dates_to_event_dates( $events_dates ): array { + $compiled_events = array(); + foreach ( $events_dates as $event_date ) { + // Get the parent event. + $event = get_post( $event_date->post_parent ); + if ( ! $event ) { + continue; + } + + // Get the event date. + $start_date = get_post_meta( $event_date->ID, 'se_event_date_start', true ); + $end_date = get_post_meta( $event_date->ID, 'se_event_date_end', true ); + $all_day = get_post_meta( $event_date->ID, 'se_event_all_day', true ); + $hide_from_calendar = get_post_meta( $event_date->ID, 'se_event_hide_from_calendar', true ); + $hide_from_feed = get_post_meta( $event_date->ID, 'se_event_hide_from_feed', true ); + + // Add the event date to the compiled events. + $compiled_events[] = array( + 'event_id' => absint( $event->ID ), + 'event_date_id' => absint( $event_date->ID ), + 'event_start_date' => esc_attr( $start_date ), + 'event_end_date' => esc_attr( $end_date ), + 'event_all_day' => boolval( $all_day ), + 'event_hide_from_calendar' => boolval( $hide_from_calendar ), + 'event_hide_from_feed' => boolval( $hide_from_feed ), + ); + } + + // Return the compiled events. + return $compiled_events; + } + + /** + * Delete all event dates for a given event. + * + * @param integer $event_id The event ID. + * + * @return void + */ + public static function delete_all_event_dates( $event_id ): void { + // Get all the event dates. + try { + $event_dates = se_event_get_event_dates( $event_id ); + } catch ( \Exception $e ) { + // If we can't get the dates, there's nothing to delete + return; + } + + // Iterate over the event dates and delete them. + foreach ( $event_dates as $event_date ) { + wp_delete_post( $event_date['id'], true ); + } + } + + /** + * Delete a single event date. + * + * @param integer $event_date_id The event date ID. + * + * @return void + */ + public static function delete_event_date( $event_date_id ): void { + wp_delete_post( $event_date_id, true ); + } +} +SE_Event_Dates::init(); diff --git a/src/classes/class-se-event-post-type.php b/src/classes/class-se-event-post-type.php index c7b91b3..a2eb072 100644 --- a/src/classes/class-se-event-post-type.php +++ b/src/classes/class-se-event-post-type.php @@ -12,6 +12,15 @@ * Post types Class. */ class SE_Event_Post_Type { + + /** + * The current event version. + * + * @var string + */ + public static $current_event_version = '2.0.0'; + + /** * This is the name of this post type. * @@ -19,6 +28,14 @@ class SE_Event_Post_Type { */ public static $post_type = 'se-event'; + + /** + * The event date post type. + * + * @var string + */ + public static $event_date_post_type = 'se-event-date'; + /** * This is the slug of this post type. * @@ -123,6 +140,64 @@ public static function register_post_type() { ), ) ); + + // Register the event-date post type. This is a child of the above event post type. + register_post_type( + 'se-event-date', + array( + 'labels' => array( + 'name' => __( 'Event Dates', 'simple-events' ), + 'singular_name' => __( 'Event Date', 'simple-events' ), + 'all_items' => __( 'All Event Dates', 'simple-events' ), + 'archives' => __( 'Event Date Archives', 'simple-events' ), + 'attributes' => __( 'Event Date Attributes', 'simple-events' ), + 'insert_into_item' => __( 'Insert into Event Date', 'simple-events' ), + 'uploaded_to_this_item' => __( 'Uploaded to this Event Date', 'simple-events' ), + 'featured_image' => _x( 'Featured Image', 'se-event-date', 'simple-events' ), + 'set_featured_image' => _x( 'Set featured image', 'se-event-date', 'simple-events' ), + 'remove_featured_image' => _x( 'Remove featured image', 'se-event-date', 'simple-events' ), + 'use_featured_image' => _x( 'Use as featured image', 'se-event-date', 'simple-events' ), + 'filter_items_list' => __( 'Filter Event Dates list', 'simple-events' ), + 'items_list_navigation' => __( 'Event Dates list navigation', 'simple-events' ), + 'items_list' => __( 'Event Dates list', 'simple-events' ), + 'new_item' => __( 'New Event Date', 'simple-events' ), + 'add_new' => __( 'Add New', 'simple-events' ), + 'add_new_item' => __( 'Add New Event Date', 'simple-events' ), + 'edit_item' => __( 'Edit Event Date', 'simple-events' ), + 'view_item' => __( 'View Event Date', 'simple-events' ), + 'view_items' => __( 'View Event Dates', 'simple-events' ), + 'search_items' => __( 'Search Event Dates', 'simple-events' ), + 'not_found' => __( 'No Event Dates found', 'simple-events' ), + 'not_found_in_trash' => __( 'No Event Dates found in trash', 'simple-events' ), + 'parent_item_colon' => __( 'Parent Event Date:', 'simple-events' ), + 'menu_name' => __( 'Event Dates', 'simple-events' ), + ), + 'public' => false, + 'hierarchical' => false, + 'show_ui' => false, + 'show_in_nav_menus' => false, + 'supports' => array( + 'title', + 'editor', + 'thumbnail', + 'custom-fields', + ), + 'rewrite' => array( + 'slug' => 'event-date', + 'with_front' => false, + ), + 'has_archive' => false, + 'query_var' => false, + 'menu_position' => null, + 'menu_icon' => 'dashicons-calendar-alt', + 'show_in_rest' => true, + 'rest_base' => 'se-event-date', + 'rest_controller_class' => 'WP_REST_Posts_Controller', + 'capabilities' => array( + 'create_posts' => 'do_not_allow', // Disable creation of new event dates. + ), + ) + ); } /** @@ -200,26 +275,15 @@ public static function register_meta() { 'post', 'se_event_dates', array( - 'single' => true, - 'type' => 'array', - 'show_in_rest' => array( - 'schema' => array( - 'items' => array( - 'type' => 'object', - 'properties' => array( - 'datetime_start' => array( - 'type' => 'string', - ), - 'datetime_end' => array( - 'type' => 'string', - ), - 'all_day' => array( - 'type' => 'boolean', - ), - ), - ), - ), - ), + 'single' => true, + 'type' => 'array', + 'default' => array(), + 'sanitize_callback' => function ( $value ) { + if ( is_null( $value ) || ! is_array( $value ) ) { + return array(); + } + return $value; + }, ) ); @@ -414,6 +478,44 @@ public static function register_meta() { 'default' => true, ) ); + + // is all day (bool) + register_meta( + 'post', + 'se_event_all_day', + array( + 'show_in_rest' => true, + 'single' => true, + 'type' => 'boolean', + 'object_subtype' => self::$event_date_post_type, + ) + ); + + // hide from calendar (bool) + register_meta( + 'post', + 'se_event_hide_from_calendar', + array( + 'show_in_rest' => true, + 'single' => true, + 'type' => 'boolean', + 'object_subtype' => self::$event_date_post_type, + 'default' => false, + ) + ); + + // hide from feed (bool) + register_meta( + 'post', + 'se_event_hide_from_feed', + array( + 'show_in_rest' => true, + 'single' => true, + 'type' => 'boolean', + 'object_subtype' => self::$event_date_post_type, + 'default' => false, + ) + ); } /** @@ -452,9 +554,60 @@ public static function pre_get_posts( $query ) { if ( ( $query->is_main_query() && ( is_post_type_archive( self::$post_type ) || is_tax( self::$post_type . '-category' ) ) ) - || ( ! $query->is_main_query() && self::$post_type === $query->get( 'post_type' ) && ! $query->get( 'se_countdown' ) && $query->get( 'sub-type' ) !== SE_Block_Variations::QUERY_LOOP_EVENTS ) + || ( ! $query->is_main_query() && self::$post_type === $query->get( 'post_type' ) && ! $query->get( 'se_countdown' ) && $query->get( 'sub-type' ) === SE_Block_Variations::QUERY_LOOP_EVENTS ) ) { - $query->set( 'orderby', 'meta_value' ); + // Handle taxonomy filtering by getting parent event IDs first + $parent_event_ids = null; + $tax_query = $query->get( 'tax_query' ); + + // Check if we have taxonomy queries for event categories + if ( ! empty( $tax_query ) || is_tax( self::$post_type . '-category' ) ) { + // Create a separate query to get parent events that match taxonomy criteria + $parent_query_args = array( + 'post_type' => self::$post_type, + 'posts_per_page' => -1, + 'fields' => 'ids', + 'post_status' => 'publish', + ); + + // Add taxonomy query from original query + if ( ! empty( $tax_query ) ) { + $parent_query_args['tax_query'] = $tax_query; + } + + // Handle category archive pages + if ( is_tax( self::$post_type . '-category' ) ) { + $term = get_queried_object(); + $parent_query_args['tax_query'] = array( + array( + 'taxonomy' => self::$post_type . '-category', + 'field' => 'term_id', + 'terms' => $term->term_id, + ), + ); + } + + $parent_events = new WP_Query( $parent_query_args ); + $parent_event_ids = $parent_events->posts; + + // If no parent events match, set to empty array to return no results + if ( empty( $parent_event_ids ) ) { + $parent_event_ids = array( 0 ); + } + } + + // Change query to target event dates instead of events + $query->set( 'post_type', self::$event_date_post_type ); + + // If we have taxonomy filtering, limit to dates of matching parent events + if ( null !== $parent_event_ids ) { + $query->set( 'post_parent__in', $parent_event_ids ); + // Remove tax_query since we're now querying date posts + $query->set( 'tax_query', array() ); + } + + // Order by event date start timestamp + $query->set( 'orderby', 'meta_value_num' ); $query->set( 'meta_key', 'se_event_date_start' ); $query->set( 'order', apply_filters( 'se_pre_get_posts_order', $sort_order, $query ) ); @@ -462,16 +615,19 @@ public static function pre_get_posts( $query ) { $event_options = array( 'hide_events_on_both', 'hide_events_on_feed', 'on' ); if ( isset( $options['hide_past_events'] ) && in_array( $options['hide_past_events'], $event_options, true ) ) { - $query->set( - 'meta_query', - array( - array( - 'key' => 'se_event_date_end', - 'value' => wp_date( 'U' ), - 'compare' => '>=', - ), - ) + $existing_meta_query = $query->get( 'meta_query' ); + if ( ! is_array( $existing_meta_query ) ) { + $existing_meta_query = array(); + } + + $existing_meta_query[] = array( + 'key' => 'se_event_date_end', + 'value' => time(), + 'compare' => '>=', // what is this? + 'type' => 'NUMERIC', ); + + $query->set( 'meta_query', $existing_meta_query ); } } } @@ -553,7 +709,8 @@ public static function delete_event_dates_if_no_event_info_block( $event_id ) { } if ( ! $is_event_info_block_present ) { - delete_post_meta( $event_id, 'se_event_dates' ); + // Delete all the event dates. + SE_Event_Dates::delete_all_event_dates( $event_id ); } } } diff --git a/src/classes/class-se-event-query-dates.php b/src/classes/class-se-event-query-dates.php index f87c16f..f285d1d 100644 --- a/src/classes/class-se-event-query-dates.php +++ b/src/classes/class-se-event-query-dates.php @@ -11,7 +11,7 @@ /** * Event Dates */ -class SE_Event_Dates { +class SE_Event_Query_Dates { public const UPDATE_QUERY_DATES_HOOK = 'se_event_update_query_dates_cron'; /** @@ -154,4 +154,4 @@ public static function handle_cron() { } // Self initialization. -SE_Event_Dates::init(); +SE_Event_Query_Dates::init(); diff --git a/src/classes/class-se-migrate-events.php b/src/classes/class-se-migrate-events.php new file mode 100644 index 0000000..109e836 --- /dev/null +++ b/src/classes/class-se-migrate-events.php @@ -0,0 +1,359 @@ + array( 'migrate_1_0_0_to_2_0_0' ), + ); + + /** + * Initialize the class. + * + * @return void + */ + public static function init() { + + add_action( 'rest_api_init', array( __CLASS__, 'register_rest_route' ) ); + add_action( 'se_migrate_events', array( __CLASS__, 'migrate_events' ) ); + } + + /** + * Registers the rest route. + * + * @return void + */ + public static function register_rest_route() { + // The Namespace + $namespace = 'simple-events'; + + // Route to pass a list of events to update. + register_rest_route( + $namespace, + '/migrate-events', + array( + 'methods' => 'POST', + 'callback' => array( __CLASS__, 'migrate_events_rest' ), + 'permission_callback' => function () { + return current_user_can( 'manage_options' ); + }, + ) + ); + + // Register the route to migrate all events. + register_rest_route( + $namespace, + '/migrate-all-events', + array( + 'methods' => array( 'POST', 'GET' ), + 'callback' => array( __CLASS__, 'migrate_events_rest_all' ), + 'permission_callback' => function () { + return true; + }, + ) + ); + } + + /** + * Get all events that need to be migrated. + * + * @return array + */ + public static function get_events_to_migrate() { + + $versions = array_keys( self::VERSION_UPGRADES ); + + // Get all events. + return get_posts( + array( + 'post_type' => SE_Event_Post_Type::$post_type, + // only results that have a version lower that the max version. + 'meta_query' => array( + 'relation' => 'OR', + array( + 'key' => 'se_event_version', + 'value' => max( $versions ), + 'compare' => '<', + ), + // or does not exist. + array( + 'key' => 'se_event_version', + 'compare' => 'NOT EXISTS', + ), + // or is empty. + array( + 'key' => 'se_event_version', + 'value' => '', + 'compare' => '=', + ), + ), + 'posts_per_page' => -1, + 'post_status' => 'any', + ) + ); + } + + /** + * Checks if we have any events to migrate. + * + * @return boolean + */ + public static function has_events_to_migrate() { + return count( self::get_events_to_migrate() ) > 0; + } + + /** + * Migrate all events. + * + * @param WP_REST_Request $request The request object. + * + * @return WP_REST_Response + */ + public static function migrate_events_rest_all( $request ) { // phpcs:ignore + // Check if we have events to migrate. + if ( ! self::has_events_to_migrate() ) { + return new WP_REST_Response( + array( + 'message' => esc_html__( 'No events to migrate.', 'simple-events' ), + ), + 200 + ); + } + + try { + // Get all events to migrate. + $events = self::get_events_to_migrate(); + // Migrate the events. + $response = self::migrate_events_by_ids( wp_list_pluck( $events, 'ID' ) ); + } catch ( \Throwable $th ) { + return new WP_REST_Response( + array( + 'message' => esc_html__( 'An error occurred while migrating events.', 'simple-events' ), + 'error' => esc_html( $th->getMessage() ), + ), + 500 + ); + } + // Return the response. + return new WP_REST_Response( + array( + 'message' => esc_html__( 'Events migrated successfully.', 'simple-events' ), + 'data' => $response, + ), + 200 + ); + } + + /** + * Migrate events via REST. + * + * @param WP_REST_Request $request The request object. + * + * @return WP_REST_Response + */ + public static function migrate_events_rest( $request ) { + // Check if we have events in the body (form-data). + $event_ids = $request->get_param( 'events' ); + // If no events are provided, return an error. + if ( empty( $event_ids ) ) { + return new WP_REST_Response( + array( + 'message' => esc_html__( 'No events provided for migration.', 'simple-events' ), + ), + 400 + ); + } + + // Convert the event IDs from a string to an array. + $event_ids = json_decode( $event_ids, true ); + + // if there was an error decoding the JSON, return an error. + if ( json_last_error() !== JSON_ERROR_NONE ) { + return new WP_REST_Response( + array( + 'message' => esc_html__( 'Invalid event IDs provided : ', 'simple-events' ) . esc_html( json_last_error_msg() ), + ), + 400 + ); + } + + // Cast to an array of integers from JSON + $event_ids = array_map( 'intval', $event_ids ); + // Validate the event IDs. + + try { + $response = self::migrate_events_by_ids( $event_ids ); + } catch ( \Throwable $th ) { + return new WP_REST_Response( + array( + 'message' => esc_html__( 'An error occurred while migrating events.', 'simple-events' ), + 'error' => esc_html( $th->getMessage() ), + ), + 500 + ); + } + + // Return the response. + return new WP_REST_Response( + array( + 'message' => esc_html__( 'Events migrated successfully.', 'simple-events' ), + 'data' => $response, + ), + 200 + ); + } + + /** + * Migrate events. + * + * @param array $event_ids The event IDs to migrate. + * + * @return array + */ + private static function migrate_events_by_ids( $event_ids ) { + // Iterate over the event IDs and update them. + $results = array(); + + foreach ( $event_ids as $event_id ) { + // Check if the event exists. + if ( ! get_post( $event_id ) ) { + $results[ $event_id ] = false; + continue; + } + + // Migrate the event. + $success = self::migrate_event( $event_id ); + + $new_version = get_post_meta( $event_id, 'se_event_version', true ); //phpcs:ignore + + $results[ $event_id ] = array( + 'success' => $success, + 'version' => ! $new_version || '' === $new_version ? '1.0.0' : $new_version, // Default to 1.0.0 if no version is set. + ); + } + + return $results; + } + + /** + * Migrate a single event. + * + * @param integer $event_id The event ID to migrate. + * + * @return boolean + */ + private static function migrate_event( $event_id ) { + // Get the event post. + $event_post = get_post( $event_id ); + + if ( ! $event_post || 'se-event' !== $event_post->post_type ) { + return false; // Not a valid event post. + } + + try { + // Get the version of the event. + $migration_methods = self::get_migration_methods( get_post_meta( $event_id, 'se_event_version', true ) ?: '1.0.0' ); // phpcs:ignore + foreach ( $migration_methods as $version => $methods ) { + // Iterate over the methods. + foreach ( $methods as $method ) { + // Check if the method exists in the class. + if ( ! method_exists( __CLASS__, $method ) ) { + continue; // Skip if the method does not exist. + } + + // Call the migration method. + call_user_func( array( __CLASS__, $method ), $event_id ); + } + + // Update the posts meta to say updated to. + update_post_meta( $event_id, 'se_event_version', $version ); + } + } catch ( \Throwable $th ) { + return false; // If any error occurs, return false. + } + return true; // If everything goes well, return true. + } + + /** + * Get the list of methods based on the version. + * + * @param string $version The version to check. + * + * @return array + */ + private static function get_migration_methods( $version ) { + // If the version is not set, return all methods. + if ( empty( $version ) ) { + return self::VERSION_UPGRADES; + } + + // Filter the methods based on the version. + return array_filter( + self::VERSION_UPGRADES, + function ( $methods, $min_version ) use ( $version ) { + return version_compare( $version, $min_version, '<' ); + }, + ARRAY_FILTER_USE_BOTH + ); + } + + + + ################## + # Migration Methods + ################## + + /** + * Migrate from version 1.0.0 to 2.0.0. + * + * This method migrates the event dates from the old format to the new format. + * + * @param integer $event_id The event ID to migrate. + * + * @return void + */ + public static function migrate_1_0_0_to_2_0_0( int $event_id ): void { + // Get all the events from its meta. + $dates = get_post_meta( $event_id, 'se_event_dates', true ); + // Iterate over the dates. + if ( ! is_array( $dates ) || empty( $dates ) ) { + return; // No dates to migrate. + } + foreach ( $dates as $key => $date ) { + // Unpack + $start = $date['datetime_start'] ?? ''; + $end = $date['datetime_end'] ?? ''; + $all_day = $date['all_day'] ?? false; + + se_event_create_event_date( + $event_id, + array( + 'all_day' => $all_day, + 'start_date' => $start, + 'end_date' => $end, + ) + ); + } + } +} + +SE_Migrate_Events::init(); diff --git a/src/classes/class-se-settings.php b/src/classes/class-se-settings.php index f142c7d..1fd65e7 100644 --- a/src/classes/class-se-settings.php +++ b/src/classes/class-se-settings.php @@ -26,6 +26,7 @@ public static function init() { // Ajax actions. add_action( 'wp_ajax_se_mark_existing_orders_as_completed', array( __CLASS__, 'mark_existing_orders_as_completed' ), 10 ); + add_action( 'wp_ajax_se_clear_orphaned_events', array( __CLASS__, 'clear_orphaned_events' ), 10 ); } /** @@ -91,6 +92,25 @@ public static function settings_init() { 'se_section_archives', array( 'label_for' => 'past_event_notice', + 'required' => true, + ) + ); + + // Add a settings to define the all day message + add_settings_field( + 'all_day_message', + sprintf( + // translators: %s is a HTML break tag. + __( 'All Day Message%s', 'simple-events' ), + wp_kses_post( '
    This message will be displayed for all day events.' ), + ), + array( __CLASS__, 'text_cb' ), + 'simple_events', + 'se_section_archives', + array( + 'label_for' => 'all_day_message', + 'value' => '', + 'required' => false, ) ); @@ -134,6 +154,38 @@ public static function settings_init() { ) ); + // Treat each date as own event for navigation. + add_settings_field( + 'treat_each_date_as_own_event', + sprintf( + // translators: %s is a HTML break tag. + __( 'Treat each date as own event%s', 'simple-events' ), + wp_kses_post( '
    When this is selected, next and previous events will treat consecutive dates as unique.' ), + ), + array( __CLASS__, 'field_cb' ), + 'simple_events', + 'se_section_archives', + array( + 'label_for' => 'treat_each_date_as_own_event', + ) + ); + + // Allow grouping of dates with different time. + add_settings_field( + 'allow_grouping_dates_different_time', + sprintf( + // translators: %s is a HTML break tag. + __( 'Allow grouping of dates with different times.%s', 'simple-events' ), + wp_kses_post( '
    When enabled, events with different time ranges (e.g., 9AM-5PM vs 10AM-6PM) will be grouped separately. When disabled, only events with identical times will be grouped together.' ), + ), + array( __CLASS__, 'field_cb' ), + 'simple_events', + 'se_section_archives', + array( + 'label_for' => 'allow_grouping_dates_different_time', + ) + ); + // Select link for the caledar page. add_settings_field( 'calendar_page', @@ -255,6 +307,36 @@ public static function settings_init() { 'label_for' => 'disable_download_calendar', ) ); + + add_settings_field( + 'clear_orphaned_events', + sprintf( + // translators: %s is a HTML break tag. + __( 'Clear orphaned events.%s', 'simple-events' ), + wp_kses_post( '
    Removes events with missing or corrupted data.' ), + ), + array( __CLASS__, 'clear_orphaned_events_cb' ), + 'simple_events', + 'se_section_calendar', + array( + 'action' => 'se_clear_orphaned_events', + 'btn_text' => __( 'Clear orphaned events', 'simple-events' ), + ) + ); + + // Add the migrate events button, if we have events to migrate. + if ( SE_Migrate_Events::has_events_to_migrate() ) { + add_settings_field( + 'migrate_events', + esc_html__( 'Migrate Events', 'simple-events' ), + array( __CLASS__, 'migrate_events_cb' ), + 'simple_events', + 'se_section_calendar', + array( + 'label_for' => 'migrate_events', + ) + ); + } } /** @@ -357,7 +439,6 @@ public static function field_cb( $args ) { + +
    + +
    +
    +

    + +

    + + +
    +
    +
    + #ID ); ?> - post_title ); ?> +
    +
    + + vID, 'se_event_version', true ) ); ?> + + + + +
    +
    +
    + + +
    + +
    + + > get_results( + $wpdb->prepare( + "SELECT child.ID, child.post_title, child.post_parent FROM {$wpdb->prefix}posts AS child + LEFT JOIN {$wpdb->prefix}posts AS parent ON child.post_parent = parent.ID + WHERE child.post_type = %s AND (child.post_parent = 0 OR parent.ID IS NULL)", + SE_Event_Post_Type::$event_date_post_type + ) + ); + + $deleted_events = 0; + + // Delete orphaned events + if ( ! empty( $orphaned_events ) ) { + foreach ( $orphaned_events as $event ) { + wp_delete_post( $event->ID, true ); // Force delete permanently + ++$deleted_events; + } + } + + // translators: %d is the number of orphaned events deleted. + wp_send_json_success( sprintf( __( '%d orphaned events deleted successfully', 'simple-events' ), $deleted_events ) ); + } + + /** + * Gets the all day message. + * + * @return string + */ + public static function get_all_day_message() { + $options = get_option( 'se_options' ); + if ( isset( $options['all_day_message'] ) && ! empty( $options['all_day_message'] ) ) { + return esc_html( $options['all_day_message'] ); + } + + return ''; + } } SE_Settings::init(); diff --git a/src/event-functions.php b/src/event-functions.php index a089f12..50f6813 100644 --- a/src/event-functions.php +++ b/src/event-functions.php @@ -120,9 +120,13 @@ function se_event_get_tickets_stock( $event_id ) { * @param integer $event_id Event id. * @param array $event_dates Event dates. * + * @deprecated 2.0.0 Please use the new se_event_get_event_dates() instead. + * * @return mixed */ function se_event_get_dates( $event_id, $event_dates = null ) { + __doing_it_wrong( __FUNCTION__, 'Please use the new se_event_get_event_dates() instead.', '2.0.0' ); + if ( is_null( $event_dates ) ) { $event_dates = get_post_meta( $event_id, 'se_event_dates', true ); } @@ -146,94 +150,177 @@ function se_event_get_dates( $event_id, $event_dates = null ) { /** * Gets only the future event dates in a formatted string. * - * @param integer $event_id Event id. - * @param boolean $date_only Whether to return only the date. - * @param boolean $time_only Whether to return only the time. - * @param array $event_dates Event dates. + * Please note in the original function we never actually used the time_only and date_only parameters. + * The inner call chain was as both to null. + * + * @param integer $event_id Event id. + * @param integer|null $event_date_id Event date id. + * @param boolean $date_only Whether to return only the date. + * @param boolean $time_only Whether to return only the time. + * @param array $event_dates Event dates. * * @return string */ -function se_event_get_future_dates( $event_id, $date_only = false, $time_only = false, $event_dates = null ) { - // Get required post meta. - $event_dates = se_event_get_dates( $event_id, $event_dates ); - $event_timezone = get_post_meta( $event_id, 'se_event_timezone', true ); - $hide_end_time = get_post_meta( $event_id, 'se_event_hide_end_time', true ); - $hide_start_time = get_post_meta( $event_id, 'se_event_hide_start_time', true ); - $display_timezone = (bool) get_post_meta( $event_id, 'se_event_display_timezone', true ); - $now = SE_Calendar::get_instance()->create_date_time( 'now' )->format( 'U' ); +function se_event_get_future_dates( $event_id, $event_date_id = null, $date_only = false, $time_only = false, $event_dates = null ) { + + $date_display_formatter = new SE_Date_Display_Formatter( $event_id ); + $now = SE_Calendar::get_instance()->create_date_time( 'now' )->format( 'U' ); + + // If dateonly is true, we need to return the date only. + if ( $date_only ) { + $date_display_formatter->set_date_only( true ); + } elseif ( $time_only ) { + $date_display_formatter->set_time_only( true ); + } + // If we dont have any dates. if ( ! $event_dates ) { - return ''; + $event_dates = se_event_get_event_dates( $event_id ); + } + // Filter out only the current event date, if set. + if ( se_event_treat_each_date_as_own_event() && $event_date_id ) { + $event_dates = array_filter( + $event_dates, + function ( $date ) use ( $event_date_id ) { + return $date['id'] === $event_date_id; + } + ); } - // Iterate over all the events and remove any where the start and end has passed. - foreach ( $event_dates as $key => $date ) { - if ( $date['datetime_start'] < $now && $date['datetime_end'] < $now ) { - unset( $event_dates[ $key ] ); + // Filter out any dates that are in the past. (start and end) + $event_dates = array_filter( + $event_dates, + function ( $date ) use ( $now ) { + return $date['start_date'] > $now && $date['end_date'] > $now; } - } + ); + + // If we have no dates, return an empty string. if ( empty( $event_dates ) ) { return ''; } - return se_event_format_dates( + return $date_display_formatter->format_dates( $event_dates ); +} + +/** + * Gets only the past event dates in a formatted string. + * + * @param integer $event_id Event id. + * @param integer|null $event_date_id Event date id. + * @param boolean $date_only Whether to return only the date. + * @param boolean $time_only Whether to return only the time. + * @param array $event_dates Event dates. + * + * @return string + */ +function se_event_get_past_dates( $event_id, $event_date_id = null, $date_only = false, $time_only = false, $event_dates = null ) { + + // Match the se_event_get_future_dates but for past dates + $date_display_formatter = new SE_Date_Display_Formatter( $event_id ); + $now = SE_Calendar::get_instance()->create_date_time( 'now' )->format( 'U' ); + + if ( $date_only ) { + $date_display_formatter->set_date_only( true ); + } elseif ( $time_only ) { + $date_display_formatter->set_time_only( true ); + } + + // If we dont have any dates. + if ( ! $event_dates ) { + $event_dates = se_event_get_event_dates( $event_id ); + } + + // Filter out only the current event date, if set. + if ( se_event_treat_each_date_as_own_event() && $event_date_id ) { + $event_dates = array_filter( + $event_dates, + function ( $date ) use ( $event_date_id ) { + return $date['id'] === $event_date_id; + } + ); + } + + // Filter out any dates that are in the past. (start and end) + $event_dates = array_filter( $event_dates, - $event_timezone, - $hide_end_time, - $hide_start_time, - $display_timezone, - false, - false + function ( $date ) use ( $now ) { + return $date['start_date'] < $now && $date['end_date'] < $now; + } ); + + // If we have no dates, return an empty string. + if ( empty( $event_dates ) ) { + return ''; + } + + return $date_display_formatter->format_dates( $event_dates ); } + /** * Get the event dates in a formatted string. * - * @param integer $event_id Event id. - * @param boolean $date_only Whether to return only the date. - * @param boolean $time_only Whether to return only the time. - * @param array $event_dates Event dates. + * @param integer $event_id Event id. + * @param integer|null $event_date_id Event date id. + * @param boolean $date_only Whether to return only the date. + * @param boolean $time_only Whether to return only the time. + * @param array $event_dates Event dates. * * @return string */ -function se_event_get_formatted_dates( $event_id, $date_only = false, $time_only = false, $event_dates = null ) { +function se_event_get_formatted_dates( $event_id, $event_date_id = null, $date_only = false, $time_only = false, $event_dates = null ) { - // Get required post meta. - $event_dates = se_event_get_dates( $event_id, $event_dates ); - $event_timezone = get_post_meta( $event_id, 'se_event_timezone', true ); - $hide_end_time = get_post_meta( $event_id, 'se_event_hide_end_time', true ); - $hide_start_time = get_post_meta( $event_id, 'se_event_hide_start_time', true ); - $display_timezone = (bool) get_post_meta( $event_id, 'se_event_display_timezone', true ); + $date_display_formatter = new SE_Date_Display_Formatter( $event_id ); + + // if we dont have any dates. + if ( ! $event_dates ) { + $event_dates = se_event_get_event_dates( $event_id ); + } + // Filter out only the current event date, if set. + if ( se_event_treat_each_date_as_own_event() && $event_date_id ) { + $event_dates = array_filter( + $event_dates, + function ( $date ) use ( $event_date_id ) { + return $date['id'] === $event_date_id; + } + ); + } if ( ! $event_dates ) { return ''; } - return se_event_format_dates( - $event_dates, - $event_timezone, - $hide_end_time, - $hide_start_time, - $display_timezone, - $date_only, - $time_only - ); + if ( $date_only ) { + $date_display_formatter->set_date_only( true ); + } elseif ( $time_only ) { + $date_display_formatter->set_time_only( true ); + } + + return $date_display_formatter->format_dates( $event_dates ); } /** * Formats the dates for the event. * - * @param array $event_dates Event dates. - * @param string $timezone Timezone. - * @param mixed $hide_end_time If we should hide the end time. - * @param mixed $hide_start_time If we should hide the start time. - * @param mixed $display_timezone If we should display the timezone. - * @param mixed $date_only If we should only show the date. - * @param mixed $time_only If we should only show the time. + * @param array $event_dates Event dates. + * @param string $timezone Timezone. + * @param mixed $hide_end_time If we should hide the end time. + * @param mixed $hide_start_time If we should hide the start time. + * @param mixed $display_timezone If we should display the timezone. + * @param mixed $date_only If we should only show the date. + * @param mixed $time_only If we should only show the time. * * @return string + * + * @deprecated 2.0.0 */ function se_event_format_dates( $event_dates, $timezone, $hide_end_time, $hide_start_time, $display_timezone, $date_only, $time_only ) { + // Add doing it wrong, suggest they use the new dateFormatter class instead. + __doing_it_wrong( __FUNCTION__, 'Please use the new dateFormatter class instead.', '2.0.0' ); + + // Attempt to get the event date id from url. + $event_date_id = se_template_get_event_date_id(); + $dates_count = is_array( $event_dates ) ? count( $event_dates ) : 1; if ( ! empty( $event_timezone ) ) { @@ -250,19 +337,20 @@ function se_event_format_dates( $event_dates, $timezone, $hide_end_time, $hide_s // Get the start and end times from the first date. // Assume all start and end times are the same until proven otherwise. - $event_time_start = wp_date( get_option( 'time_format' ), $event_dates[0]['datetime_start'], $timezone ); - $event_time_end = wp_date( get_option( 'time_format' ), $event_dates[0]['datetime_end'], $timezone ); + $event_time_start = wp_date( get_option( 'time_format' ), $event_dates[0]['start_date'], $timezone ); + $event_time_end = wp_date( get_option( 'time_format' ), $event_dates[0]['end_date'], $timezone ); $same_times = ( 1 < $dates_count ) ? true : false; // Loop over each available event date. foreach ( $event_dates as $date ) { + $opening_li = $date['id'] === $event_date_id ? '
  • ' : '
  • '; // Check if start and end times are on the same day. - $same_day = wp_date( 'Y-m-d', $date['datetime_start'], $timezone ) === wp_date( 'Y-m-d', $date['datetime_end'], $timezone ); + $same_day = wp_date( 'Y-m-d', $date['start_date'], $timezone ) === wp_date( 'Y-m-d', $date['end_date'], $timezone ); // Get start and end times. - $time_start = ( $hide_start_time ) ? '' : wp_date( get_option( 'time_format' ), $date['datetime_start'], $timezone ); - $time_end = ( $hide_end_time ) ? '' : wp_date( get_option( 'time_format' ), $date['datetime_end'], $timezone ); + $time_start = ( $hide_start_time ) ? '' : wp_date( get_option( 'time_format' ), $date['start_date'], $timezone ); + $time_end = ( $hide_end_time ) ? '' : wp_date( get_option( 'time_format' ), $date['end_date'], $timezone ); $time_separator = ( 1 === (int) $hide_start_time ) ? '' : '–'; @@ -275,20 +363,20 @@ function se_event_format_dates( $event_dates, $timezone, $hide_end_time, $hide_s $date['all_day'] = array_key_exists( 'all_day', $date ) ? filter_var( $date['all_day'], FILTER_VALIDATE_BOOLEAN ) : false; // Start the output string. - $single_date_output = wp_date( get_option( 'date_format' ), $date['datetime_start'], $timezone ); + $single_date_output = wp_date( get_option( 'date_format' ), $date['start_date'], $timezone ); // Return early if we only want the date. if ( $date_only ) { - $end_date = wp_date( get_option( 'date_format' ), $date['datetime_end'], $timezone ); + $end_date = wp_date( get_option( 'date_format' ), $date['end_date'], $timezone ); $date_only_output = ( $same_day ) ? $single_date_output : $single_date_output . ' – ' . $end_date; - $dates_output .= ( $dates_count > 1 ) ? '
  • ' . $date_only_output . '
  • ' : $date_only_output; + $dates_output .= ( $dates_count > 1 ) ? $opening_li . $date_only_output . '' : $date_only_output; continue; } // Return early if we only want the time. if ( $time_only ) { $time = sprintf( '%s %s %s', $time_start, $time_separator, $time_end ); - $dates_output .= ( $dates_count > 1 ) ? '
  • ' . $time . '
  • ' : $time; + $dates_output .= ( $dates_count > 1 ) ? $opening_li . $time . '' : $time; continue; } @@ -298,7 +386,7 @@ function se_event_format_dates( $event_dates, $timezone, $hide_end_time, $hide_s $single_date_output .= sprintf( ' %s – %s %s', $time_start, - wp_date( get_option( 'date_format' ), $date['datetime_end'], $timezone ), + wp_date( get_option( 'date_format' ), $date['end_date'], $timezone ), $time_end ); } elseif ( false === $date['all_day'] && $time_start !== $time_end ) { @@ -313,7 +401,7 @@ function se_event_format_dates( $event_dates, $timezone, $hide_end_time, $hide_s } // Return output for this date. - $dates_output .= ( $dates_count > 1 ) ? '
  • ' . $single_date_output . '
  • ' : $single_date_output; + $dates_output .= ( $dates_count > 1 ) ? $opening_li . $single_date_output . '' : $single_date_output; } // Overwrite output if all start and end times are the same and "Group event dates with matching times" otpion is selected. @@ -399,30 +487,63 @@ function se_event_get_venue( $event_id ) { * @return boolean */ function se_event_is_expired( $event_id ) { - $event_end_date = get_post_meta( $event_id, 'se_event_date_end', true ); + $event_dates = se_event_get_event_dates( $event_id ); + + $latest_date = null; + foreach ( $event_dates as $date ) { + // Get the end date. + $end_date = $date['end_date']; + + // If the event is all day, get the start date. + if ( $date['all_day'] ) { + $temp = se_create_date_time_from_timestamp( $date['start_date'] ); + $end_date = $temp->setTime( 23, 59, 59 )->getTimestamp(); + } - if ( ! empty( $event_end_date ) && $event_end_date < wp_date( 'U' ) ) { - return true; + // If the end date is greater than the latest date, set it. + if ( $end_date > $latest_date ) { + $latest_date = $end_date; + } } - return false; + if ( null === $latest_date ) { + return false; + } + + return $latest_date < SE_Calendar::get_instance()->create_date_time( 'now' )->format( 'U' ); } /** * Gets the calendar event link. * - * @param integer $event_id Event id. + * @param integer $event_id Event id. + * @param integer|null $event_date_id Event date id. * * @return string */ -function se_event_get_calendar_link( $event_id ) { +function se_event_get_calendar_link( $event_id, $event_date_id = null ) { // Set the link. $external_link = esc_url( get_post_meta( $event_id, 'se_event_external_link', true ) ); $open_external_link = (bool) get_post_meta( $event_id, 'se_open_external_link', true ); + if ( $external_link && $open_external_link ) { + return $external_link; + } + + $permalink = get_the_permalink( $event_id ); + + // Either add ?se-date=7424 or append &se-date=7424 if permalink has ? + if ( $event_date_id ) { + $permalink .= sprintf( + '%sse-date=%s', + ( strpos( $permalink, '?' ) !== false ) ? '&' : '?', + esc_attr( $event_date_id ) + ); + } + return ( $external_link && $open_external_link ) ? $external_link - : get_the_permalink( $event_id ); + : get_the_permalink( $event_id ) . ( $event_date_id ? '?se-date=' . $event_date_id : '' ); } /** @@ -479,6 +600,8 @@ function se_event_show_links_above_content(): bool { * Updates an event start and end meta dates. * This will ensure that the start and end dates will not be in the past. * + * This is for legacy reasons only. + * * @param integer $event_id Event id. * * @return void @@ -528,3 +651,158 @@ function se_event_update_event_query_dates( $event_id ) { update_post_meta( $event_id, 'se_event_date_end', esc_attr( $end_date ) ); } } + + /** + * Create event date. + * + * @param integer $event_id Event id. + * @param array{ start_date: integer, end_date: integer, all_day: boolean, hide_from_calendar: boolean, hide_from_feed: boolean, } $event_dates Event dates. + * + * @return \WP_Post|null + */ +function se_event_create_event_date( $event_id, $event_dates ) { + $default_args = array( + 'start_date' => 0, + 'end_date' => 0, + 'all_day' => false, + 'hide_from_calendar' => false, + 'hide_from_feed' => false, + ); + // Merge the default args with the provided event dates. + $event_dates = wp_parse_args( $event_dates, $default_args ); + // Validate the event dates, start date should be a timestamp and end date should be a timestamp. + if ( ! is_numeric( $event_dates['start_date'] ) ) { + return null; + } + + // Validate parent event exists + if ( ! get_post( $event_id ) ) { + return null; + } + + // Create the event date post. + $event_date_post = array( + 'post_title' => sprintf( + // translators: %s is the event title. + __( 'Event Date for %s', 'simple-events' ), + get_the_title( $event_id ) + ), + 'post_content' => '', + 'post_status' => 'publish', + 'post_type' => SE_Event_Post_Type::$event_date_post_type, + 'post_parent' => $event_id, + ); + + // Insert the post into the database. + $event_date_id = wp_insert_post( $event_date_post ); + + if ( is_wp_error( $event_date_id ) || ! $event_date_id ) { + return null; // Failed to create the event date. + } + + // Update the post meta for the event date. + update_post_meta( $event_date_id, 'se_event_date_start', esc_attr( $event_dates['start_date'] ) ); + update_post_meta( $event_date_id, 'se_event_date_end', esc_attr( $event_dates['end_date'] ) ); + update_post_meta( $event_date_id, 'se_event_all_day', boolval( $event_dates['all_day'] ) ); + update_post_meta( $event_date_id, 'se_event_hide_from_calendar', boolval( $event_dates['hide_from_calendar'] ) ); + update_post_meta( $event_date_id, 'se_event_hide_from_feed', boolval( $event_dates['hide_from_feed'] ) ); + + return get_post( $event_date_id ); +} + +/** + * Get the dates for an event. + * + * @param integer $event_id Event id. + * + * @return array{ + * start_date: integer, + * end_date: integer, + * all_day: boolean, + * hide_from_calendar: boolean, + * hide_from_feed: boolean, + * }[] + * + * @throws \Exception If the event ID is invalid or if the event dates cannot be retrieved. + * + * @since 2.0.0 + */ +function se_event_get_event_dates( $event_id ): array { + if ( ! is_numeric( $event_id ) || $event_id <= 0 ) { + throw new \Exception( esc_html( __( 'Invalid event ID provided.', 'simple-events' ) ) ); + } + + $event_dates = get_posts( + array( + 'post_type' => SE_Event_Post_Type::$event_date_post_type, + 'post_status' => 'publish', + 'posts_per_page' => -1, + 'post_parent' => $event_id, + 'fields' => 'ids', + ) + ); + + // Map with meta. + $dates = array_map( + function ( $date_id ) { + $start_date = get_post_meta( $date_id, 'se_event_date_start', true ); + $end_date = get_post_meta( $date_id, 'se_event_date_end', true ); + $all_day = get_post_meta( $date_id, 'se_event_all_day', true ); + $hide_from_calendar = get_post_meta( $date_id, 'se_event_hide_from_calendar', true ); + $hide_from_feed = get_post_meta( $date_id, 'se_event_hide_from_feed', true ); + + return array( + 'id' => $date_id, + 'start_date' => esc_attr( $start_date ), + 'end_date' => esc_attr( $end_date ), + 'all_day' => boolval( $all_day ), + 'hide_from_calendar' => '' === $hide_from_calendar ? false : boolval( $hide_from_calendar ), + 'hide_from_feed' => '' === $hide_from_feed ? false : boolval( $hide_from_feed ), + ); + }, + $event_dates + ); + + // Legacy filter. + $dates = apply_filters( 'se_event_get_dates', $dates, $event_id ); + + return apply_filters( 'se_event_get_event_dates', $dates, $event_id ); +} + +/** + * Create a DateTime object from a timestamp, with an optional timezone. + * + * @param mixed $timestamp The Unix timestamp to create the DateTime object from. + * @param string|null $timezone The timezone to be used, or null to use the site timezone. + * + * @return DateTime The created DateTime object. + */ +function se_create_date_time_from_timestamp( $timestamp, $timezone = null ): DateTime { + /** + * If no timezone is passed, use the site timezone + */ + if ( null === $timezone ) { + $timezone = wp_timezone_string(); + } + + try { + $date_time_object = new DateTime( 'now', new DateTimeZone( $timezone ) ); + } catch ( Exception $e ) { + $date_time_object = new DateTime(); + // todo handle exception + } + + return $date_time_object->setTimestamp( $timestamp ); +} + +/** + * Checks if the settings are defined to treat each date as an own event. + * + * @return boolean + */ +function se_event_treat_each_date_as_own_event(): bool { + $settings = get_option( 'se_options' ); + return is_array( $settings ) + && array_key_exists( 'treat_each_date_as_own_event', $settings ) + && 'on' === $settings['treat_each_date_as_own_event']; +} diff --git a/src/template-functions.php b/src/template-functions.php index 03a9cdc..d291064 100644 --- a/src/template-functions.php +++ b/src/template-functions.php @@ -10,6 +10,16 @@ exit; } +/** + * Checks and get the event date id from url if set, + * + * @return integer|null The event date id or null if not set. + */ +function se_template_get_event_date_id() { + $event_date_id = array_key_exists( 'se-date', $_GET ) ? sanitize_text_field( $_GET['se-date'] ) : null; // phpcs:ignore + return is_numeric( $event_date_id ) ? absint( $event_date_id ) : null; +} + if ( ! function_exists( 'se_template_content_wrapper_start' ) ) { /** @@ -54,6 +64,7 @@ function se_template_event_archive_title() { * @return void */ function se_template_event_single_title() { + the_title( '

    ', '

    ' ); } } @@ -84,9 +95,13 @@ function se_template_event_thumbnail() { /** * Output the event date and time. * + * @deprecated 2.0.0 This has been replaced by the new date formatter class. + * * @return void */ function se_template_event_date() { + __doing_it_wrong( __FUNCTION__, 'Please use the new date formatter class instead.', '2.0.0' ); + $event_dates = se_event_get_dates( get_the_ID() ); if ( ! empty( $event_dates ) ) { @@ -194,8 +209,14 @@ function se_template_event_ticket_stock() { * @return void */ function se_template_event_more_info() { + global $post; + if ( se_event_treat_each_date_as_own_event() && isset( $post->event_date_id ) ) { + $permalink = get_permalink( $post->post_parent ) . '?se-date=' . $post->event_date_id; + } else { + $permalink = get_permalink(); + } ?> - + %2$s', - esc_url( get_permalink( $previous_event->ID ) ), - apply_filters( 'se_event_previous_link_text', esc_html( '<< ' . get_the_title( $previous_event->ID ) ), $previous_event ) + esc_url( get_permalink( $previous_event->post_parent ) . '?se-date=' . $previous_event->ID ), + apply_filters( 'se_event_previous_link_text', esc_html( '<< ' . get_the_title( $previous_event->post_parent ) ), $previous_event ) ); - $next_event = se_event_get_next_event( $event_start_date ); + $next_event = se_event_get_next_event( get_the_ID(), se_template_get_event_date_id() ); $next_link = null === $next_event ? '' : sprintf( // translators: %1$s is the link to the next event, %2$s is the title of the next event. '%s', - esc_url( get_permalink( $next_event->ID ) ), - apply_filters( 'se_event_next_link_text', esc_html( get_the_title( $next_event->ID ) . ' >>' ), $next_event ) + esc_url( get_permalink( $next_event->post_parent ) . '?se-date=' . $next_event->ID ), + apply_filters( 'se_event_next_link_text', esc_html( get_the_title( $next_event->post_parent ) . ' >>' ), $next_event ) ); $calendar_link = null !== $calendar_page @@ -368,84 +381,135 @@ function se_template_event_next_previous(): void { /** * Gets the next event based on a time stamp. * - * @param integer $timestamp The timestamp to get the next event from. + * @param integer $event_id The event ID to get the next event from. + * @param integer|null $event_date_id The event date ID to get the next event from, if available. * * @return WP_Post|null The next event or null if none found. */ -function se_event_get_next_event( int $timestamp ): ?WP_Post { - $query = new WP_Query( - array( - 'previous_events' => false, - 'post_type' => 'se-event', - 'posts_per_page' => -1, - 'meta_query' => array( - array( - 'key' => 'se_event_date_start', - 'value' => absint( $timestamp ), - 'compare' => '>', - 'type' => 'NUMERIC', - ), +function se_event_get_next_event( int $event_id, ?int $event_date_id = null ): ?WP_Post { + $options = get_option( 'se_options' ); + $allow_grouping = isset( $options['treat_each_date_as_own_event'] ) ? 'on' === $options['treat_each_date_as_own_event'] : false; + + // If we dont have an event date id, we need to get the event dates. + if ( ! $event_date_id ) { + $event_dates = se_event_get_event_dates( $event_id ); + if ( empty( $event_dates ) ) { + return null; + } + $event_date_id = $event_dates[0]['id']; + } + + // Define the query to get next events. + $args = array( + 'post_type' => SE_Event_Post_Type::$event_date_post_type, + 'posts_per_page' => 1, + 'orderby' => 'meta_value_num', + 'meta_key' => 'se_event_date_start', + 'order' => 'ASC', + 'post_status' => 'publish', + 'meta_query' => array( + array( + 'key' => 'se_event_date_start', + 'value' => get_post_meta( $event_date_id, 'se_event_date_start', true ), + 'compare' => '>', + 'type' => 'NUMERIC', + ), + array( + 'key' => 'se_event_hide_from_feed', + 'value' => 1, + 'compare' => '!=', ), - ) + ), ); + // If we dont allow grouping, add the event id to parent not in. + if ( ! $allow_grouping ) { + $args['post__not_in'] = array_map( + function ( $post ) { + return $post['id']; + }, + se_event_get_event_dates( $event_id ) + ); + } + + $query = new WP_Query( $args ); + // If we have no posts, return null. if ( ! $query->have_posts() ) { return null; } - $event = $query->posts[0]; - - // Reset the post data. + // Get the first next event. + $next_event = $query->posts[0]; wp_reset_postdata(); - return $event; + return $next_event; } /** * Gets the previous event based on a time stamp. * - * @param integer $timestamp The timestamp to get the previous event from. + * @param integer $event_id The event ID to get the previous event from. + * @param integer|null $event_date_id The event date ID to get the previous event from, if available. * * @return WP_Post|null The previous event or null if none found. */ -function se_event_get_previous_event( int $timestamp ): ?WP_Post { - - // Ensure previous events are sorted by Descending order. - add_action( - 'pre_get_posts', - function ( $wp_query ) { - if ( array_key_exists( 'previous_events', $wp_query->query ) - && $wp_query->query['previous_events'] - ) { - $wp_query->set( 'order', 'DESC' ); - } - } - ); +function se_event_get_previous_event( int $event_id, ?int $event_date_id = null ): ?WP_Post { + $options = get_option( 'se_options' ); + $allow_grouping = isset( $options['treat_each_date_as_own_event'] ) ? 'on' === $options['treat_each_date_as_own_event'] : false; + + // If we dont have an event date id, we need to get the event dates. + if ( ! $event_date_id ) { + $event_dates = se_event_get_event_dates( $event_id ); + if ( empty( $event_dates ) ) { + return null; + } + $event_date_id = $event_dates[0]['id']; + } - // Get the first previous event. - $previous_events = new WP_Query( + // Define the query to get previous events. + $args = array( + 'post_type' => SE_Event_Post_Type::$event_date_post_type, + 'posts_per_page' => 1, + 'orderby' => 'meta_value_num', + 'meta_key' => 'se_event_date_start', + 'order' => 'DESC', + 'post_status' => 'publish', + 'meta_query' => array( array( - 'previous_events' => true, - 'post_type' => 'se-event', - 'posts_per_page' => 1, - 'meta_query' => array( - array( - 'key' => 'se_event_date_start', - 'value' => absint( $timestamp ), - 'compare' => '<', - 'type' => 'NUMERIC', - ), - ), - ) + 'key' => 'se_event_date_start', + 'value' => get_post_meta( $event_date_id, 'se_event_date_start', true ), + 'compare' => '<', + 'type' => 'NUMERIC', + ), + array( + 'key' => 'se_event_hide_from_feed', + 'value' => 1, + 'compare' => '!=', + ), + ), + ); + // If we dont allow grouping, add the event id to parent not in. + if ( ! $allow_grouping ) { + $args['post__not_in'] = array_map( + function ( $post ) { + return $post['id']; + }, + se_event_get_event_dates( $event_id ) ); - if ( ! $previous_events->have_posts() ) { + } + + $query = new WP_Query( $args ); + + // If we have no posts, return null. + if ( ! $query->have_posts() ) { return null; } - $event = $previous_events->posts[0]; + // Get the first previous event. + $previous_event = $query->posts[0]; wp_reset_postdata(); - return $event; + return $previous_event; } if ( ! function_exists( 'se_expired_event_notice' ) ) { @@ -472,14 +536,30 @@ function se_expired_event_notice() { * @return void */ function se_template_event_content() { + global $post; $show_on_frontend = get_post_meta( get_the_ID(), 'se_event_show_on_frontend', true ); - if ( empty( $show_on_frontend ) ) { return; } + $date_display_formatter = new SE_Date_Display_Formatter( get_the_ID() ); + $dates = se_event_get_event_dates( get_the_ID() ); + + // If we have an event date and we treating each date as own event, we need to get the event date id. + if ( se_event_treat_each_date_as_own_event() && isset( $post->event_date_id ) ) { + $dates = array_filter( + $dates, + function ( $date ) use ( $post ) { + return $date['id'] === $post->event_date_id; + } + ); + + $dates = array_values( $dates ); + } else { + $date_display_formatter->set_date_only( true ); + } // Output the content for archive template. - se_template_event_date(); + echo wp_kses_post( $date_display_formatter->get_header_date( $dates ) ); se_template_event_location(); se_template_event_price(); se_template_event_ticket_stock(); diff --git a/src/templates/calendar/day/event.php b/src/templates/calendar/day/event.php index bc369a0..a2b7749 100644 --- a/src/templates/calendar/day/event.php +++ b/src/templates/calendar/day/event.php @@ -27,7 +27,6 @@ if ( $se_hide_end_time ) { $se_hide_css .= ' se-event-hide-end-time'; } - ?>
    @@ -51,7 +50,7 @@

    post_parent ) { + $se_parent_post = get_post( $se_post_event_date->post_parent ); + if ( $parent_post ) { + $post = $se_parent_post; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited + $post->event_date_id = $se_post_event_date->ID; + } + } +} ?>
  • > diff --git a/src/variations/query-loop-events/block.js b/src/variations/query-loop-events/block.js index 2ecf120..cf750c9 100644 --- a/src/variations/query-loop-events/block.js +++ b/src/variations/query-loop-events/block.js @@ -5,13 +5,42 @@ */ import { InspectorControls } from '@wordpress/block-editor'; -import { registerBlockVariation } from '@wordpress/blocks'; +import { registerBlockType, registerBlockVariation } from '@wordpress/blocks'; import { addFilter } from '@wordpress/hooks'; -import { PanelBody, SelectControl } from '@wordpress/components'; +import { PanelBody, SelectControl, RangeControl } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; +import { useEffect, useState } from '@wordpress/element'; +import { createReduxStore, register, dispatch, select } from '@wordpress/data'; const EVENTS_VARIATION = 'se-events/query-loop-events'; +// Create a simple store for query loop data +const eventsQueryStore = createReduxStore('se-events/query-data', { + reducer: (state = {}, action) => { + switch (action.type) { + case 'SET_QUERY_DATA': + return { + ...state, + [action.blockId]: action.data, + }; + default: + return state; + } + }, + actions: { + setQueryData: (blockId, data) => ({ + type: 'SET_QUERY_DATA', + blockId, + data, + }), + }, + selectors: { + getQueryData: (state, blockId) => state[blockId] || {}, + }, +}); + +register(eventsQueryStore); + registerBlockVariation('core/query', { name: EVENTS_VARIATION, title: 'Query Loop Events', @@ -37,13 +66,15 @@ registerBlockVariation('core/query', { inherit: false, inheritTaxQuery: true, feedType: 'default', + _cacheBuster: Date.now(), }, + eventsPerPage: 6, }, innerBlocks: [ [ 'core/post-template', {}, - [['core/post-title'], ['core/post-date']], + [['core/post-title'], ['simple-events/loop-event-info']], ], ['core/query-pagination'], ['core/query-no-results'], @@ -53,10 +84,51 @@ registerBlockVariation('core/query', { allowedControls: ['taxQuery', 'search', 'feedType'], }); -const FeedTypeControl = ({ attributes, setAttributes }) => { + +const FeedTypeControl = ({ attributes, setAttributes, clientId }) => { const { query } = attributes; const feedType = query.feedType || 'default'; const feedOrder = query.order || 'asc'; + const [eventsPerPage, setEventsPerPage] = useState( + query.perPage || 6 + ); + + // Store the query data so child blocks can access it + useEffect(() => { + dispatch('se-events/query-data').setQueryData(clientId, { + feedType, + order: feedOrder, + }); + }, [feedType, feedOrder, clientId]); + + +/** + * Gets options for feed order based on the type. + * @param {string} type The current feed type. + * @returns options for feed order based on the type. + */ +const getFeedOrderOptions = (type) => { + switch (type) { + case 'upcoming': + return [ + { label: 'Soonest First', value: 'asc' }, + { label: 'Furthest in Future First', value: 'desc' }, + ]; + case 'past': + return [ + { label: 'Oldest First', value: 'asc' }, + { label: 'Most Recent First', value: 'desc' }, + ]; + case 'default': + default: + return [ + { label: 'Oldest to Newest', value: 'asc' }, + { label: 'Newest to Oldest', value: 'desc' }, + ]; + } +}; + +let feedOrderOptions = getFeedOrderOptions(feedType); return ( <> @@ -73,6 +145,7 @@ const FeedTypeControl = ({ attributes, setAttributes }) => { query: { ...query, feedType: value, + _cacheBuster: Date.now() }, }); }} @@ -81,20 +154,42 @@ const FeedTypeControl = ({ attributes, setAttributes }) => { { setAttributes({ query: { ...query, order: value, + _cacheBuster: Date.now() + }, + }); + }} + __nextHasNoMarginBottom + /> + { + setEventsPerPage(value); + setAttributes({ + query: { + ...query, + perPage: value, + _cacheBuster: Date.now() }, }); }} + min={1} + max={100} + step={1} __nextHasNoMarginBottom /> +

    + {__( + 'Select the type of events to display and their order.', + 'simple-events' + )} +

    ); };