diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..49c7ed05 --- /dev/null +++ b/.env.example @@ -0,0 +1,16 @@ +### .env.example +### The following environment variables are useful for Unit Test websocket server +### In a local development environment, copy this file to .env.development to ensure that Unit Test websocket server will run locally +### and will use the local API instance to run tests. +### Set the same value for WS_PORT in the Unit Test frontend project folder. +### For more information, see https://github.com/Liturgical-Calendar/LiturgicalCalendarAPI/blob/development/LitCalTestServer.php + +WS_PORT=8080 +API_PROTOCOL=http +API_HOST=localhost +API_PORT=8000 # will not determine on which port the API will launch, only on which port the Unit Test server will look for the API +# When production, the API will add `/api/{version}` to the path +# where {version} is the version of the API that is automatically detected +# based on the current folder name (dev, v3, v4...). +# When development, no path will be added. +APP_ENV=development # development | production diff --git a/.gitignore b/.gitignore index 49b97714..ee52a0b1 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,6 @@ ValidateTestsAgainstSchema.php .envrc debuginfo.php testYaml.php +.env +.env.* +!.env.example diff --git a/.vscode/tasks.json b/.vscode/tasks.json index b147c72b..9b716c76 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -44,6 +44,15 @@ "dependsOn": [ "launch-browser" ] + }, + { + "label": "litcal-tests-websockets", + "type": "shell", + "command": "php LitCalTestServer.php", + "group": { + "kind": "build", + "isDefault": true + } } ] } diff --git a/LitCalTestServer.php b/LitCalTestServer.php index 26e948b5..ff98013a 100644 --- a/LitCalTestServer.php +++ b/LitCalTestServer.php @@ -10,9 +10,25 @@ use Ratchet\Http\HttpServer; use Ratchet\WebSocket\WsServer; use LiturgicalCalendar\Api\Health; +use Dotenv\Dotenv; -$apiVersion = basename(__DIR__); -define('API_BASE_PATH', "https://litcal.johnromanodorazio.com/api/{$apiVersion}"); +$dotenv = Dotenv::createImmutable(__DIR__, ['.env', '.env.local', '.env.development', '.env.production'], false); +$dotenv->ifPresent(['API_PROTOCOL', 'API_HOST'])->notEmpty(); +$dotenv->ifPresent(['API_PORT'])->isInteger(); +$dotenv->ifPresent(['APP_ENV'])->notEmpty()->allowedValues(['development', 'production']); +$dotenv->ifPresent(['WS_PROTOCOL', 'WS_HOST'])->notEmpty(); +$dotenv->ifPresent(['WS_PORT'])->isInteger(); +$dotenv->safeLoad(); +$API_PROTOCOL = $_ENV['API_PROTOCOL'] ?? 'https'; +$API_HOST = $_ENV['API_HOST'] ?? 'litcal.johnromanodorazio.com'; +$API_PORT = $_ENV['API_PORT'] ?? 443; + +if (isset($_ENV['APP_ENV']) && $_ENV['APP_ENV'] === 'development') { + define('API_BASE_PATH', "{$API_PROTOCOL}://{$API_HOST}:{$API_PORT}"); +} else { + $apiVersion = basename(__DIR__); + define('API_BASE_PATH', "{$API_PROTOCOL}://{$API_HOST}/api/{$apiVersion}"); +} $server = IoServer::factory( new HttpServer( @@ -20,7 +36,7 @@ new Health() ) ), - 8080 + $_ENV['WS_PORT'] ?? 8080 ); $server->run(); diff --git a/README.md b/README.md index fb834cfc..20e0131f 100644 --- a/README.md +++ b/README.md @@ -42,8 +42,8 @@ Some characteristics of this API: There are a few proof of concept example applications for usage of the API at https://litcal.johnromanodorazio.com/usage.php, which demonstrate generating an HTML representation of the Liturgical Calendar. * The [first example](https://litcal.johnromanodorazio.com/examples/php/) uses cURL in PHP to make a request to the endpoint and handle the results. -* The [second example](https://litcal.johnromanodorazio.com/examples/javascript/) uses AJAX in Javascript to make the request to the endpoint and handle the results. -* The [third example](https://litcal.johnromanodorazio.com/examples/fullcalendar/examples/month-view.html) makes use of the [FullCalendar javascript framework](https://github.com/fullcalendar/fullcalendar) to display the results from the AJAX request in a nicely formatted calendar view. +* The [second example](https://litcal.johnromanodorazio.com/examples/javascript/) uses `fetch` in Javascript to make the request to the endpoint and handle the results. +* The [third example](https://litcal.johnromanodorazio.com/examples/fullcalendar/examples/month-view.html) makes use of the [FullCalendar javascript framework](https://github.com/fullcalendar/fullcalendar) to display the results from the `fetch` request in a nicely formatted calendar view. * The [fourth example](https://litcal.johnromanodorazio.com/examples/fullcalendar/examples/messages.html) is the same as the third except that it outputs the Messages first and the [FullCalendar](https://github.com/fullcalendar/fullcalendar) calendar view after. All of these examples request `JSON` as the data exchange format generated by the endpoint. Any application could use the endpoint in a similar manner: an Android App, a plugin for a Desktop Publishing App... @@ -59,6 +59,13 @@ _(See [usage.php#calSubscription](https://litcal.johnromanodorazio.com/usage.php # Testing locally +System requirements: +* PHP >= 8.1 +* PHP modules installed and enabled: `intl` * `zip` * `gettext` * `calendar` * `yaml` +* System language packs for all the supported languages + +## Using PHP's builtin server + To test the API locally, you can use PHP's builtin server. However, you will need to spawn at least a couple of workers, since some routes will make a request internally to another route. For example, a request to the `/calendar` route will make a request internally to the `/calendars` route. Spawn at least two workers: @@ -66,8 +73,20 @@ Spawn at least two workers: PHP_CLI_SERVER_WORKERS=2 php -S localhost:8000 ``` +## Using a docker container + For convenience when using VSCode, a `tasks.json` has been defined so that you can simply type CTRL+SHIFT+B (CMD+SHIFT+B on MacOS) to start the PHP builtin server and open the browser. +To further simplify your setup, without having to worry about getting all the system requirements in place, you can also launch the API in a docker container using the repo `Dockerfile`: + +```bash +# If you haven't cloned the repo locally, you can build directly from the remote repo (replace `{branch}` with the branch or tag from which you want to build): +docker build -t liturgy-api:{branch} https://github.com/Liturgical-Calendar/LiturgicalCalendarAPI.git#{branch} +# If instead you have cloned the repo locally, you can build from the local repo (replace `{branch}` with the branch or tag that you have checked out locally): +docker build -t liturgy-api:{branch} . +docker run -p 8000:8000 -d liturgy-api:{branch} +``` + # Translations diff --git a/composer.json b/composer.json index b2a6b7e3..3d84a5e3 100644 --- a/composer.json +++ b/composer.json @@ -8,7 +8,8 @@ "php": ">=8.1", "swaggest/json-schema": "~0.12", "cboden/ratchet": "~0.4", - "sabre/vobject": "^4.5.1" + "sabre/vobject": "^4.5.1", + "vlucas/phpdotenv": "^5.6" }, "require-dev": { "squizlabs/php_codesniffer": "*" diff --git a/composer.lock b/composer.lock index 00fbfe6e..5c4ef388 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "69e62a87a18e3801a1cc4e8746f9e117", + "content-hash": "0c3aa268dce27d9cedc97a8d8fa860c2", "packages": [ { "name": "cboden/ratchet", @@ -116,6 +116,68 @@ }, "time": "2023-08-08T05:53:35+00:00" }, + { + "name": "graham-campbell/result-type", + "version": "v1.1.3", + "source": { + "type": "git", + "url": "https://github.com/GrahamCampbell/Result-Type.git", + "reference": "3ba905c11371512af9d9bdd27d99b782216b6945" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/3ba905c11371512af9d9bdd27d99b782216b6945", + "reference": "3ba905c11371512af9d9bdd27d99b782216b6945", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.3" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.39 || ^9.6.20 || ^10.5.28" + }, + "type": "library", + "autoload": { + "psr-4": { + "GrahamCampbell\\ResultType\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "An Implementation Of The Result Type", + "keywords": [ + "Graham Campbell", + "GrahamCampbell", + "Result Type", + "Result-Type", + "result" + ], + "support": { + "issues": "https://github.com/GrahamCampbell/Result-Type/issues", + "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.3" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/graham-campbell/result-type", + "type": "tidelift" + } + ], + "time": "2024-07-20T21:45:45+00:00" + }, { "name": "guzzlehttp/psr7", "version": "2.7.0", @@ -280,6 +342,81 @@ }, "time": "2016-09-17T00:15:18+00:00" }, + { + "name": "phpoption/phpoption", + "version": "1.9.3", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/php-option.git", + "reference": "e3fac8b24f56113f7cb96af14958c0dd16330f54" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/e3fac8b24f56113f7cb96af14958c0dd16330f54", + "reference": "e3fac8b24f56113f7cb96af14958c0dd16330f54", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.39 || ^9.6.20 || ^10.5.28" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, + "branch-alias": { + "dev-master": "1.9-dev" + } + }, + "autoload": { + "psr-4": { + "PhpOption\\": "src/PhpOption/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com", + "homepage": "https://github.com/schmittjoh" + }, + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "Option Type for PHP", + "keywords": [ + "language", + "option", + "php", + "type" + ], + "support": { + "issues": "https://github.com/schmittjoh/php-option/issues", + "source": "https://github.com/schmittjoh/php-option/tree/1.9.3" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption", + "type": "tidelift" + } + ], + "time": "2024-07-20T21:41:07+00:00" + }, { "name": "psr/http-factory", "version": "1.1.0", @@ -1286,12 +1423,12 @@ }, "type": "library", "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, "branch-alias": { "dev-main": "3.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" } }, "autoload": { @@ -1411,6 +1548,85 @@ ], "time": "2024-11-13T18:58:10+00:00" }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.31.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.31.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.31.0", @@ -1491,6 +1707,86 @@ ], "time": "2024-09-09T11:45:10+00:00" }, + { + "name": "symfony/polyfill-php80", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", + "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.31.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-php83", "version": "v1.31.0", @@ -1511,8 +1807,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -1649,6 +1945,90 @@ } ], "time": "2024-11-13T15:31:34+00:00" + }, + { + "name": "vlucas/phpdotenv", + "version": "v5.6.1", + "source": { + "type": "git", + "url": "https://github.com/vlucas/phpdotenv.git", + "reference": "a59a13791077fe3d44f90e7133eb68e7d22eaff2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/a59a13791077fe3d44f90e7133eb68e7d22eaff2", + "reference": "a59a13791077fe3d44f90e7133eb68e7d22eaff2", + "shasum": "" + }, + "require": { + "ext-pcre": "*", + "graham-campbell/result-type": "^1.1.3", + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.3", + "symfony/polyfill-ctype": "^1.24", + "symfony/polyfill-mbstring": "^1.24", + "symfony/polyfill-php80": "^1.24" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-filter": "*", + "phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2" + }, + "suggest": { + "ext-filter": "Required to use the boolean validator." + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, + "branch-alias": { + "dev-master": "5.6-dev" + } + }, + "autoload": { + "psr-4": { + "Dotenv\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Vance Lucas", + "email": "vance@vancelucas.com", + "homepage": "https://github.com/vlucas" + } + ], + "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.", + "keywords": [ + "dotenv", + "env", + "environment" + ], + "support": { + "issues": "https://github.com/vlucas/phpdotenv/issues", + "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.1" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/vlucas/phpdotenv", + "type": "tidelift" + } + ], + "time": "2024-07-20T21:52:34+00:00" } ], "packages-dev": [ @@ -1735,12 +2115,12 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, "platform": { "php": ">=8.1" }, - "platform-dev": [], + "platform-dev": {}, "plugin-api-version": "2.6.0" } diff --git a/jsondata/schemas/DiocesanCalendar.json b/jsondata/schemas/DiocesanCalendar.json index 2aa296c7..3aa431e8 100644 --- a/jsondata/schemas/DiocesanCalendar.json +++ b/jsondata/schemas/DiocesanCalendar.json @@ -35,7 +35,8 @@ "type": "string" }, "group": { - "type": "string"} + "type": "string" + } }, "required": [ "diocese_id", diff --git a/jsondata/sourcedata/calendars/dioceses/CA/calgar_ca/Calgary.json b/jsondata/sourcedata/calendars/dioceses/CA/calgar_ca/Calgary.json new file mode 100644 index 00000000..b8501304 --- /dev/null +++ b/jsondata/sourcedata/calendars/dioceses/CA/calgar_ca/Calgary.json @@ -0,0 +1,32 @@ +{ + "litcal": [ + { + "festivity": { + "event_key": "DedicationofSaintMarysCathedral", + "color": [ + "white" + ], + "grade": 6, + "common": [ + "Dedication of a Church" + ], + "day": 11, + "month": 12 + }, + "metadata": { + "since_year": 1970, + "form_rownum": 1 + } + } + ], + "metadata": { + "locales": [ + "fr_CA", + "en_CA" + ], + "nation": "CA", + "diocese_id": "calgar_ca", + "diocese_name": "Calgary", + "timezone": "America\/Edmonton" + } +} diff --git a/jsondata/sourcedata/calendars/dioceses/CA/calgar_ca/i18n/en_CA.json b/jsondata/sourcedata/calendars/dioceses/CA/calgar_ca/i18n/en_CA.json new file mode 100644 index 00000000..86163d19 --- /dev/null +++ b/jsondata/sourcedata/calendars/dioceses/CA/calgar_ca/i18n/en_CA.json @@ -0,0 +1,3 @@ +{ + "DedicationofSaintMarysCathedral": "Dedication of Saint Mary's Cathedral" +} diff --git a/jsondata/sourcedata/calendars/dioceses/CA/calgar_ca/i18n/fr_CA.json b/jsondata/sourcedata/calendars/dioceses/CA/calgar_ca/i18n/fr_CA.json new file mode 100644 index 00000000..d82c00b3 --- /dev/null +++ b/jsondata/sourcedata/calendars/dioceses/CA/calgar_ca/i18n/fr_CA.json @@ -0,0 +1,3 @@ +{ + "DedicationofSaintMarysCathedral": "" +} diff --git a/src/Enum/JsonData.php b/src/Enum/JsonData.php index 49d38e14..529d89df 100644 --- a/src/Enum/JsonData.php +++ b/src/Enum/JsonData.php @@ -158,4 +158,10 @@ class JsonData * Evaluates to 'jsondata/sourcedata/calendars/dioceses/{nation}/{diocese}/i18n/{locale}.json'. */ public const DIOCESAN_CALENDARS_I18N_FILE = JsonData::DIOCESAN_CALENDARS_I18N_FOLDER . '/{locale}.json'; + + /** + * The file containing the data for the world dioceses of the Latin Rite. + * Evaluates to 'jsondata/world_dioceses.json'. + */ + public const WORLD_DIOCESES_LATIN_RITE = JsonData::FOLDER . '/world_dioceses.json'; } diff --git a/src/Params/RegionalDataParams.php b/src/Params/RegionalDataParams.php index 6ef85335..a6d159ff 100644 --- a/src/Params/RegionalDataParams.php +++ b/src/Params/RegionalDataParams.php @@ -39,6 +39,7 @@ class RegionalDataParams public ?string $key = null; public ?string $locale = null; public ?object $payload = null; + public ?string $i18nRequest = null; public function __construct() { @@ -127,7 +128,11 @@ private function checkNationalCalendarConditions(object $data): string $validLangs = $currentNation[0]->locales; if (property_exists($data, 'locale')) { $data->locale = \Locale::canonicalize($data->locale); - if (in_array($data->locale, $validLangs)) { + if ( + RegionalData::$Core->getRequestMethod() === RequestMethod::PUT // short circuit for PUT requests that don't need to validate against existing locales + || null !== $this->i18nRequest // short circuit for i18n requests + || in_array($data->locale, $validLangs, true) + ) { $this->locale = $data->locale; } else { $message = "Invalid value {$data->locale} for param `locale`, valid values for nation {$data->key} are: " @@ -136,7 +141,11 @@ private function checkNationalCalendarConditions(object $data): string } } elseif (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { $value = \Locale::acceptFromHttp($_SERVER['HTTP_ACCEPT_LANGUAGE']); - if (in_array($value, $validLangs)) { + if ( + RegionalData::$Core->getRequestMethod() === RequestMethod::PUT // short circuit for PUT requests that don't need to validate against existing locales + || null !== $this->i18nRequest // short circuit for i18n requests + || in_array($value, $validLangs, true) + ) { $this->locale = $value; } else { $message = "Invalid value {$value} for Accept-Language header, valid values for nation {$data->key} are: " @@ -182,11 +191,17 @@ private function checkDiocesanCalendarConditions(object $data): string ); } - $currentDiocese = array_values(array_filter($this->calendars->diocesan_calendars, fn ($el) => $el->calendar_id === $data->key))[0]; - $validLangs = $currentDiocese->locales; + if (RegionalData::$Core->getRequestMethod() !== RequestMethod::PUT) { + $currentDiocese = array_values(array_filter($this->calendars->diocesan_calendars, fn ($el) => $el->calendar_id === $data->key))[0]; + $validLangs = $currentDiocese->locales; + } if (property_exists($data, 'locale')) { $data->locale = \Locale::canonicalize($data->locale); - if (in_array($data->locale, $validLangs)) { + if ( + RegionalData::$Core->getRequestMethod() === RequestMethod::PUT // short circuit for PUT requests that don't need to validate against existing locales + || null !== $this->i18nRequest // short circuit for i18n requests + || in_array($data->locale, $validLangs, true) + ) { $this->locale = $data->locale; } else { $message = "Invalid value {$data->locale} for param `locale`, valid values for {$currentDiocese->diocese} are: " @@ -195,7 +210,11 @@ private function checkDiocesanCalendarConditions(object $data): string } } elseif (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { $value = \Locale::acceptFromHttp($_SERVER['HTTP_ACCEPT_LANGUAGE']); - if (in_array($value, $validLangs)) { + if ( + RegionalData::$Core->getRequestMethod() === RequestMethod::PUT // short circuit for PUT requests which don't require a check against valid langs + || null !== $this->i18nRequest // short circuit for i18n requests + || in_array($value, $validLangs, true) // otherwise check against valid langs + ) { $this->locale = $value; } else { $message = "Invalid value {$value} for Accept-Language header, valid values for {$currentDiocese->diocese} are: " @@ -203,8 +222,7 @@ private function checkDiocesanCalendarConditions(object $data): string RegionalData::produceErrorResponse(StatusCode::BAD_REQUEST, $message); } } else { - // if no locale was requested, just use the first valid locale - $this->locale = $validLangs[0]; + RegionalData::produceErrorResponse(StatusCode::BAD_REQUEST, "`locale` param or `Accept-Language` header required for Diocesan calendar data when request method is PUT"); } return $data->key; @@ -230,7 +248,7 @@ private function checkDiocesanCalendarConditions(object $data): string private function checkWiderRegionCalendarConditions(object $data) { if ( - false === in_array($data->key, $this->calendars->wider_regions_keys) + false === in_array($data->key, $this->calendars->wider_regions_keys, true) && RegionalData::$Core->getRequestMethod() !== RequestMethod::PUT ) { $validVals = implode(', ', $this->calendars->wider_regions_keys); @@ -243,7 +261,11 @@ private function checkWiderRegionCalendarConditions(object $data) $validLangs = $currentWiderRegion->locales; if (property_exists($data, 'locale')) { $data->locale = \Locale::canonicalize($data->locale); - if (in_array($data->locale, $validLangs)) { + if ( + RegionalData::$Core->getRequestMethod() === RequestMethod::PUT // short circuit for PUT requests that don't need to validate against existing locales + || null !== $this->i18nRequest // short circuit for i18n requests + || in_array($data->locale, $validLangs, true) + ) { $this->locale = $data->locale; } else { $message = "Invalid value {$data->locale} for param `locale`, valid values for wider region {$currentWiderRegion->name} are: " @@ -252,7 +274,11 @@ private function checkWiderRegionCalendarConditions(object $data) } } elseif (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { $value = \Locale::acceptFromHttp($_SERVER['HTTP_ACCEPT_LANGUAGE']); - if (in_array($value, $validLangs)) { + if ( + RegionalData::$Core->getRequestMethod() === RequestMethod::PUT // short circuit for PUT requests that don't need to validate against existing locales + || null !== $this->i18nRequest // short circuit for i18n requests + || in_array($value, $validLangs, true) + ) { $this->locale = $value; } else { $message = "Invalid value {$value} for Accept-Language header, valid values for wider region {$currentWiderRegion->name} are: " @@ -296,11 +322,10 @@ private function validatePayload(object $payload): bool case 'PUT': if ( false === property_exists($payload, 'litcal') - || false === property_exists($payload, 'diocese') - || false === property_exists($payload, 'nation') + || false === property_exists($payload, 'metadata') || false === property_exists($payload->metadata, 'locales') ) { - $message = "Cannot create Diocesan calendar data when the payload does not have required properties `litcal`, `diocese` or `nation`. Payload was:\n" . json_encode($payload, JSON_PRETTY_PRINT); + $message = "Cannot create Diocesan calendar data when the payload does not have required properties `litcal`, `metadata`, or `metadata.locales`. Payload was:\n" . json_encode($payload, JSON_PRETTY_PRINT); RegionalData::produceErrorResponse(StatusCode::BAD_REQUEST, $message); } break; @@ -342,10 +367,16 @@ public function setData(object $data): bool if (false === in_array($data->category, self::EXPECTED_CATEGORIES)) { RegionalData::produceErrorResponse( StatusCode::BAD_REQUEST, - "Unexpected value '{$data->category}' for param `category`, acceptable values are: " . implode(', ', array_values(self::EXPECTED_CATEGORIES)) + "Unexpected value '{$data->category}' for param `category`, acceptable values are: " . implode(', ', array_keys(self::EXPECTED_CATEGORIES)) ); } + if (in_array(RegionalData::$Core->getRequestMethod(), [RequestMethod::GET, RequestMethod::POST], true)) { + if (property_exists($data, 'i18n')) { + $this->i18nRequest = $data->i18n; + } + } + $this->category = $data->category; switch ($data->category) { case 'NATIONALCALENDAR': @@ -361,7 +392,7 @@ public function setData(object $data): bool $this->key = null; } - if (in_array(RegionalData::$Core->getRequestMethod(), [RequestMethod::PUT,RequestMethod::PATCH], true)) { + if (in_array(RegionalData::$Core->getRequestMethod(), [RequestMethod::PUT, RequestMethod::PATCH], true)) { if (false === property_exists($data, 'payload') || false === $data->payload instanceof \stdClass) { RegionalData::produceErrorResponse(StatusCode::BAD_REQUEST, "Cannot create or update Calendar data without a payload"); } diff --git a/src/Paths/RegionalData.php b/src/Paths/RegionalData.php index 9bf8c2e6..9f752faf 100644 --- a/src/Paths/RegionalData.php +++ b/src/Paths/RegionalData.php @@ -13,6 +13,7 @@ use LiturgicalCalendar\Api\Enum\LitSchema; use LiturgicalCalendar\Api\Enum\RequestContentType; use LiturgicalCalendar\Api\Params\RegionalDataParams; +use PHP_CodeSniffer\Tokenizers\JS; /** * RegionalData @@ -94,6 +95,8 @@ private function handleRequestMethod() */ private function getRegionalCalendar(): void { + $i18nDataFile = null; + $calendarDataFile = null; switch ($this->params->category) { case "DIOCESANCALENDAR": $dioceseEntry = array_values(array_filter($this->CalendarsMetadata->diocesan_calendars, function ($el) { @@ -102,21 +105,44 @@ private function getRegionalCalendar(): void if (empty($dioceseEntry)) { self::produceErrorResponse(StatusCode::NOT_FOUND, "The requested resource {$this->params->key} was not found in the index"); } - $calendarDataFile = strtr(JsonData::DIOCESAN_CALENDARS_FILE, [ - '{nation}' => $dioceseEntry[0]->nation, - '{diocese}' => $this->params->key, - '{diocese_name}' => $dioceseEntry[0]->diocese - ]); + + if (property_exists($this->params, 'i18nRequest') && null !== $this->params->i18nRequest) { + $i18nDataFile = strtr(JsonData::DIOCESAN_CALENDARS_I18N_FILE, [ + '{nation}' => $dioceseEntry[0]->nation, + '{diocese}' => $this->params->key, + '{locale}' => $this->params->i18nRequest + ]); + } else { + $calendarDataFile = strtr(JsonData::DIOCESAN_CALENDARS_FILE, [ + '{nation}' => $dioceseEntry[0]->nation, + '{diocese}' => $this->params->key, + '{diocese_name}' => $dioceseEntry[0]->diocese + ]); + } break; case "WIDERREGIONCALENDAR": - $calendarDataFile = strtr(JsonData::WIDER_REGIONS_FILE, [ - '{wider_region}' => $this->params->key - ]); + if (property_exists($this->params, 'i18nRequest') && null !== $this->params->i18nRequest) { + $i18nDataFile = strtr(JsonData::WIDER_REGIONS_I18N_FILE, [ + '{wider_region}' => $this->params->key, + '{locale}' => $this->params->i18nRequest + ]); + } else { + $calendarDataFile = strtr(JsonData::WIDER_REGIONS_FILE, [ + '{wider_region}' => $this->params->key + ]); + } break; case "NATIONALCALENDAR": - $calendarDataFile = strtr(JsonData::NATIONAL_CALENDARS_FILE, [ - '{nation}' => $this->params->key - ]); + if (property_exists($this->params, 'i18nRequest') && null !== $this->params->i18nRequest) { + $i18nDataFile = strtr(JsonData::NATIONAL_CALENDARS_I18N_FILE, [ + '{nation}' => $this->params->key, + '{locale}' => $this->params->i18nRequest + ]); + } else { + $calendarDataFile = strtr(JsonData::NATIONAL_CALENDARS_FILE, [ + '{nation}' => $this->params->key + ]); + } break; default: self::produceErrorResponse( @@ -126,11 +152,21 @@ private function getRegionalCalendar(): void ); } - if (file_exists($calendarDataFile)) { + // If a simple i18n data request was made, we only return the i18n data + if (null !== $i18nDataFile) { + if (file_exists($i18nDataFile)) { + self::produceResponse(file_get_contents($i18nDataFile)); + } else { + self::produceErrorResponse(StatusCode::NOT_FOUND, "RegionalData::getRegionalCalendar: file $i18nDataFile does not exist"); + } + } + + // Else if a calendar data request was made, we return the calendar data with the requested locale + if (null !== $calendarDataFile && file_exists($calendarDataFile)) { $CalendarData = json_decode(file_get_contents($calendarDataFile)); if (null === $this->params->locale) { $this->params->locale = $CalendarData->metadata->locales[0]; - } elseif (false === in_array($this->params->locale, $CalendarData->metadata->locales)) { + } elseif (false === in_array($this->params->locale, $CalendarData->metadata->locales, true)) { self::produceErrorResponse( StatusCode::BAD_REQUEST, "Invalid value `{$this->params->locale}` for param `locale`. Valid values for current requested Wider region calendar data `{$this->params->key}` are: " @@ -167,13 +203,29 @@ private function getRegionalCalendar(): void } } } else { - self::produceErrorResponse(StatusCode::NOT_FOUND, "file $CalendarDataI18nFile does not exist"); + self::produceErrorResponse(StatusCode::NOT_FOUND, "RegionalData::getRegionalCalendar: file $CalendarDataI18nFile does not exist"); } self::produceResponse(json_encode($CalendarData)); } else { - self::produceErrorResponse(StatusCode::NOT_FOUND, "file $calendarDataFile does not exist"); + self::produceErrorResponse(StatusCode::NOT_FOUND, "RegionalData::getRegionalCalendar: file $calendarDataFile does not exist"); + } + } + + /** + private static function getCountryIsoByDioceseId($data, $targetDioceseId) { + foreach ($data as $country) { + foreach ($country['dioceses'] as $diocese) { + if ($diocese['diocese_id'] === $targetDioceseId) { + return [ + 'country_iso' => $country['country_iso'], + 'diocese_name' => $diocese['diocese_name'] + ]; + } + } } + return null; // Return null if no match is found } + */ /** * Handle PUT requests to create or update a regional calendar data resource. @@ -190,56 +242,94 @@ private function getRegionalCalendar(): void private function createRegionalCalendar(): void { $response = new \stdClass(); - $updateData = new \stdClass(); - switch ($this->params->category) { - case 'DIOCESANCALENDAR': - $nationType = gettype($this->params->payload->nation); - $dioceseType = gettype($this->params->payload->diocese); - if ($nationType !== 'string' || $dioceseType !== 'string') { - self::produceErrorResponse(StatusCode::BAD_REQUEST, "Params `nation` and `key` in payload are expected to be of type string, instead `nation` was of type `{$nationType}` and `key` was of type `{$dioceseType}`"); - } - $updateData->nation = strip_tags($this->params->payload->nation); - $updateData->diocese = strip_tags($this->params->payload->diocese); - if (false === $this->params->payload instanceof \stdClass) { - $calType = gettype($this->params->payload); - self::produceErrorResponse(StatusCode::BAD_REQUEST, "`caldata` param in payload expected to be serialized object, instead it was of type `{$calType}` after unserialization"); - } - if (property_exists($this->params->payload, 'group')) { - $groupType = gettype($this->params->payload->group); - if ($groupType !== 'string') { - self::produceErrorResponse(StatusCode::BAD_REQUEST, "Param `group` in payload is expected to be of type `string`, instead it was of type `{$groupType}`"); - } - $updateData->group = strip_tags($this->params->payload->group); - } + if (false === $this->params->payload instanceof \stdClass) { + $calType = gettype($this->params->payload); + self::produceErrorResponse(StatusCode::BAD_REQUEST, "`payload` param expected to be serialized object, instead it was of type `{$calType}` after unserialization"); + } - // make sure we have all the necessary folders in place - if (!file_exists(JsonData::DIOCESAN_CALENDARS_FOLDER . $this->params->payload->nation)) { - mkdir(JsonData::DIOCESAN_CALENDARS_FOLDER . $this->params->payload->nation, 0755, true); - } - if (!file_exists(JsonData::DIOCESAN_CALENDARS_FOLDER . $this->params->payload->nation . '/' . $this->params->payload->diocese)) { - mkdir(JsonData::DIOCESAN_CALENDARS_FOLDER . $this->params->payload->nation . '/' . $this->params->payload->diocese, 0755, true); - } - $diocesanCalendarI18nFolder = strtr(JsonData::DIOCESAN_CALENDARS_I18N_FOLDER, [ - '{nation}' => $this->params->payload->nation, - '{diocese}' => $this->params->payload->diocese - ]); - if (!file_exists($diocesanCalendarI18nFolder)) { - mkdir($diocesanCalendarI18nFolder, 0755, true); - } + $test = $this->validateDataAgainstSchema($this->params->payload, LitSchema::DIOCESAN); + if ($test === true) { - $test = $this->validateDataAgainstSchema($this->params->payload, LitSchema::DIOCESAN); - if ($test === true) { - $calendarData = json_encode($this->params->payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); - file_put_contents( - $updateData->path . "/{$updateData->diocese}.json", - $calendarData . PHP_EOL - ); - $response->success = "Calendar data created or updated for Diocese \"{$updateData->diocese}\" (Nation: \"$updateData->nation\")"; - self::produceResponse(json_encode($response)); - } else { - self::produceErrorResponse(StatusCode::UNPROCESSABLE_CONTENT, $test); - } - break; + // make sure we have all the necessary folders in place + if (!file_exists(JsonData::DIOCESAN_CALENDARS_FOLDER . $this->params->payload->metadata->nation)) { + mkdir(JsonData::DIOCESAN_CALENDARS_FOLDER . $this->params->payload->metadata->nation, 0755, true); + } + if (!file_exists(JsonData::DIOCESAN_CALENDARS_FOLDER . $this->params->payload->metadata->nation . '/' . $this->params->payload->metadata->diocese_id)) { + mkdir(JsonData::DIOCESAN_CALENDARS_FOLDER . $this->params->payload->metadata->nation . '/' . $this->params->payload->metadata->diocese_id, 0755, true); + } + $diocesanCalendarI18nFolder = strtr(JsonData::DIOCESAN_CALENDARS_I18N_FOLDER, [ + '{nation}' => $this->params->payload->metadata->nation, + '{diocese}' => $this->params->payload->metadata->diocese_id + ]); + if (!file_exists($diocesanCalendarI18nFolder)) { + mkdir($diocesanCalendarI18nFolder, 0755, true); + } + + $diocesanCalendarFile = strtr( + JsonData::DIOCESAN_CALENDARS_FILE, + [ + '{nation}' => $this->params->payload->metadata->nation, + '{diocese}' => $this->params->payload->metadata->diocese_id, + '{diocese_name}' => $this->params->payload->metadata->diocese_name + ] + ); + + $diocesanCalendarI18nFile = strtr( + JsonData::DIOCESAN_CALENDARS_I18N_FILE, + [ + '{nation}' => $this->params->payload->metadata->nation, + '{diocese}' => $this->params->payload->metadata->diocese_id, + '{locale}' => $this->params->locale + ] + ); + + $litCalEventsI18n = array_reduce($this->params->payload->litcal, function ($carry, $item) { + $carry[$item->festivity->event_key] = $item->festivity->name; + unset($item->festivity->name); + return $carry; + }, []); + + $litCalEventsI18nOtherLocales = array_reduce(array_keys($litCalEventsI18n), function ($carry, $key) { + $carry[$key] = ''; + return $carry; + }, []); + + $otherLocales = array_values(array_filter($this->params->payload->metadata->locales, function ($el) { + return $el !== $this->params->locale; + })); + + $calendarData = json_encode($this->params->payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); + file_put_contents( + $diocesanCalendarFile, + $calendarData . PHP_EOL + ); + + $calendarI18nData = json_encode($litCalEventsI18n, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); + file_put_contents( + $diocesanCalendarI18nFile, + $calendarI18nData . PHP_EOL + ); + + $calendarI18nDataOtherLocales = json_encode($litCalEventsI18nOtherLocales, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); + foreach($otherLocales as $locale) { + $diocesanCalendarI18nFileOtherLocales = strtr( + JsonData::DIOCESAN_CALENDARS_I18N_FILE, + [ + '{nation}' => $this->params->payload->metadata->nation, + '{diocese}' => $this->params->payload->metadata->diocese_id, + '{locale}' => $locale + ] + ); + file_put_contents( + $diocesanCalendarI18nFileOtherLocales, + $calendarI18nDataOtherLocales . PHP_EOL + ); + } + + $response->success = "Calendar data created or updated for Diocese \"{$this->params->payload->metadata->diocese_name}\" (Nation: \"{$this->params->payload->metadata->nation}\")"; + self::produceResponse(json_encode($response)); + } else { + self::produceErrorResponse(StatusCode::UNPROCESSABLE_CONTENT, $test); } } @@ -637,7 +727,7 @@ private static function retrievePayloadFromPostPutPatchRequest(object $data): ?o $payload = (object)$_POST; break; default: - if (in_array(self::$Core->getRequestMethod(), [RequestMethod::PUT, RequestMethod::PATCH])) { + if (in_array(self::$Core->getRequestMethod(), [RequestMethod::PUT, RequestMethod::PATCH], true)) { // the payload MUST be in the body of the request, either JSON encoded or YAML encoded self::produceErrorResponse( StatusCode::BAD_REQUEST, @@ -668,13 +758,19 @@ private static function setDataFromPath(array $requestPathParts): object $data->category = RegionalDataParams::EXPECTED_CATEGORIES[$requestPathParts[0]]; $data->key = $requestPathParts[1]; - if (in_array(self::$Core->getRequestMethod(), [RequestMethod::POST, RequestMethod::PUT, RequestMethod::PATCH])) { + if (in_array(self::$Core->getRequestMethod(), [RequestMethod::GET, RequestMethod::POST], true)) { + if (isset($requestPathParts[2])) { + $data->i18n = $requestPathParts[2]; + } + } + + if (in_array(self::$Core->getRequestMethod(), [RequestMethod::POST, RequestMethod::PUT, RequestMethod::PATCH], true)) { $data = RegionalData::retrievePayloadFromPostPutPatchRequest($data); - } elseif ( - self::$Core->getRequestMethod() === RequestMethod::GET - && isset($_GET['locale']) - ) { - $data->locale = \Locale::canonicalize($_GET['locale']); + } + elseif (self::$Core->getRequestMethod() === RequestMethod::GET) { + if (isset($_GET['locale'])) { + $data->locale = \Locale::canonicalize($_GET['locale']); + } } return $data; } @@ -688,10 +784,18 @@ private static function setDataFromPath(array $requestPathParts): object */ private static function validateRequestPath(array $requestPathParts): void { - if (count($requestPathParts) !== 2) { + if (in_array(self::$Core->getRequestMethod(), [RequestMethod::GET, RequestMethod::POST], true)) { + if (count($requestPathParts) < 2 || count($requestPathParts) > 3) { + self::produceErrorResponse( + StatusCode::BAD_REQUEST, + "Expected at least two and at most three path params for GET and POST requests, received " . count($requestPathParts) + ); + } + } + else if (count($requestPathParts) !== 2) { self::produceErrorResponse( StatusCode::BAD_REQUEST, - "Expected two and exactly two path params, received " . count($requestPathParts) + "Expected two and exactly two path params for PATCH and DELETE requests, received " . count($requestPathParts) ); } @@ -733,8 +837,14 @@ private function handleRequestParams(array $requestPathParts = []): void default: $data = (object)$_REQUEST; } - if (null === $data || !property_exists($data, 'payload')) { - self::produceErrorResponse(StatusCode::BAD_REQUEST, "No payload received. Must receive payload in body of request, in JSON or YAML format, with properties `key` and `caldata`"); + if ( + null === $data + || !property_exists($data, 'payload') + || !property_exists($data->payload, 'litcal') + || !property_exists($data->payload, 'metadata') + || !property_exists($data->payload->metadata, 'diocese_id') + ) { + self::produceErrorResponse(StatusCode::BAD_REQUEST, "Invalid payload in request. Must receive payload in body of request, in JSON or YAML format, with properties `payload`, `payload.litcal`, `payload.metadata`, and `payload.metadata.diocese_id`"); } if (false === count($requestPathParts)) { self::produceErrorResponse(StatusCode::BAD_REQUEST, "No request path received. Must receive request path with path param `category`"); @@ -745,6 +855,7 @@ private function handleRequestParams(array $requestPathParts = []): void break; case 'diocese': $data->category = 'DIOCESANCALENDAR'; + $data->key = $data->payload->metadata->diocese_id; break; case 'widerregion': $data->category = 'WIDERREGIONCALENDAR'; @@ -838,7 +949,7 @@ public static function produceErrorResponse(int $statusCode, string $description */ private static function produceResponse(string $jsonEncodedResponse): void { - if (in_array(self::$Core->getRequestMethod(), ['PUT','PATCH'])) { + if (in_array(self::$Core->getRequestMethod(), [RequestMethod::PUT, RequestMethod::PATCH], true)) { header($_SERVER[ "SERVER_PROTOCOL" ] . " 201 Created", true, 201); } switch (self::$Core->getResponseContentType()) { @@ -869,13 +980,13 @@ private static function produceResponse(string $jsonEncodedResponse): void public function init(array $requestPathParts = []) { self::$Core->init(); - if (self::$Core->getRequestMethod() === RequestMethod::GET || self::$Core->getRequestMethod() === RequestMethod::OPTIONS) { + if (in_array(self::$Core->getRequestMethod(), [RequestMethod::GET, RequestMethod::OPTIONS], true)) { self::$Core->validateAcceptHeader(true); } else { self::$Core->validateAcceptHeader(false); } if (self::$Core->getRequestMethod() === RequestMethod::OPTIONS) { - return; + die(); } self::$Core->setResponseContentTypeHeader(); $this->handleRequestParams($requestPathParts); diff --git a/src/Router.php b/src/Router.php index 045ddbee..b0d90541 100644 --- a/src/Router.php +++ b/src/Router.php @@ -2,6 +2,7 @@ namespace LiturgicalCalendar\Api; +use GuzzleHttp\Psr7\Request; use LiturgicalCalendar\Api\Enum\RequestMethod; use LiturgicalCalendar\Api\Enum\RequestContentType; use LiturgicalCalendar\Api\Enum\AcceptHeader; @@ -120,9 +121,12 @@ public static function route(): void case 'calendar': $LitCalEngine = new Calendar(); // Calendar::$Core will not exist until the Calendar class is instantiated! - //Calendar::$Core->setAllowedOrigins(self::$allowedOrigins); - Calendar::$Core->setAllowedRequestMethods([ RequestMethod::GET, RequestMethod::POST, RequestMethod::OPTIONS ]); - Calendar::$Core->setAllowedRequestContentTypes([ RequestContentType::JSON, RequestContentType::FORMDATA ]); + Calendar::$Core->setAllowedRequestMethods([ + RequestMethod::GET, + RequestMethod::POST, + RequestMethod::OPTIONS + ]); + Calendar::$Core->setAllowedRequestContentTypes([ RequestContentType::JSON, RequestContentType::YAML, RequestContentType::FORMDATA ]); Calendar::$Core->setAllowedAcceptHeaders([ AcceptHeader::JSON, AcceptHeader::XML, AcceptHeader::ICS, AcceptHeader::YAML ]); $LitCalEngine->setAllowedReturnTypes([ ReturnType::JSON, ReturnType::XML, ReturnType::ICS, ReturnType::YAML ]); $LitCalEngine->setCacheDuration(CacheDuration::MONTH); @@ -148,13 +152,17 @@ public static function route(): void ) { Tests::$Core->setAllowedOrigins(self::$allowedOrigins); } - Tests::$Core->setAllowedRequestContentTypes([ RequestContentType::JSON ]); + Tests::$Core->setAllowedRequestContentTypes([ RequestContentType::JSON, RequestContentType::YAML ]); Tests::$Core->setAllowedAcceptHeaders([ AcceptHeader::JSON, AcceptHeader::YAML ]); Tests::handleRequest(); break; case 'events': $Events = new Events(); - Events::$Core->setAllowedRequestMethods([ RequestMethod::GET, RequestMethod::POST, RequestMethod::OPTIONS ]); + Events::$Core->setAllowedRequestMethods([ + RequestMethod::GET, + RequestMethod::POST, + RequestMethod::OPTIONS + ]); if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD'])) { if ( in_array($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD'], [ RequestMethod::PUT, RequestMethod::PATCH, RequestMethod::DELETE ], true) diff --git a/src/Utilities.php b/src/Utilities.php index 8897b596..58ab917e 100644 --- a/src/Utilities.php +++ b/src/Utilities.php @@ -27,15 +27,22 @@ class Utilities 'feasts_keys', 'memorials', 'memorials_keys', + 'suppressed_events', + 'suppressed_events_keys', + 'reinstated_events', + 'reinstated_events_keys', 'request_headers', 'color', 'color_lcl', 'common' ]; private static string $LAST_ARRAY_KEY = ''; + /** + * All snake_case keys are automatically transformed to their PascalCase equivalent. + * If any key needs a specific case transformation other than the automatic snake_case to PascalCase, add it to this array. + */ private const TRANSFORM_KEYS = [ - "litcal" => "LitCal", - "has_vesper_ii" => "HasVesperII" + "litcal" => "LitCal" ]; public static string $HASH_REQUEST = ''; @@ -75,36 +82,41 @@ public static function convertArray2XML(array $data, ?\SimpleXMLElement &$xml): //self::debugWrite( "proceeding to convert array value of <$key> to xml sequence..." ); self::convertArray2XML($value, $new_object); } else { - // XML elements cannot have numerical names, they must have text + // XML elements cannot have numerical names, they must have text if (is_numeric($key)) { - //self::debugWrite( "key <$key> is numerical, have to deal with this..." ); if (self::$LAST_ARRAY_KEY === 'messages') { - //self::debugWrite( "key <$key> seems to belong to the Messages array: will create a corresponding element with attribute 'idx'" ); $el = $xml->addChild('Message', htmlspecialchars($value)); $el->addAttribute("idx", $key); - } elseif (in_array(self::$LAST_ARRAY_KEY, ['solemnities_keys','feasts_keys','memorials_keys'])) { + } elseif (in_array(self::$LAST_ARRAY_KEY, ['solemnities_keys','feasts_keys','memorials_keys','suppressed_events_keys','reinstated_events_keys'])) { $el = $xml->addChild('Key', $value); $el->addAttribute("idx", $key); } else { - //self::debugWrite( "key <$key> does not seem to belong to the Messages array: will create a corresponding