diff --git a/.github/actions/build-plugin/action.yml b/.github/actions/build-plugin/action.yml index c5d65e200..4284965e4 100644 --- a/.github/actions/build-plugin/action.yml +++ b/.github/actions/build-plugin/action.yml @@ -20,25 +20,18 @@ runs: - name: Cache Composer packages id: composer-cache - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: vendor key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} restore-keys: | ${{ runner.os }}-php- - - name: Install dependencies - run: | - composer install --no-dev --prefer-dist --no-progress - shell: bash - - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: node-version-file: '.nvmrc' cache: 'npm' - - run: npm ci --legacy-peer-deps --include=dev - shell: bash - + - name: Run build script (skip zip generation) if: ${{ inputs.generate_zip == 'false' }} run: bin/build-zip.sh --skip-zip diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 485e2b752..1f1f09134 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -17,7 +17,7 @@ jobs: steps: - name: Checkout PR branch - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: ref: ${{ github.head_ref }} @@ -46,7 +46,7 @@ jobs: php vendor/bin/phpbench run tests/benchmark --report=aggregate > /tmp/pr-benchmark.txt - name: Checkout master branch - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: ref: master diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index ffe19a700..c06819ef9 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -26,7 +26,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: fetch-depth: 0 @@ -44,7 +44,7 @@ jobs: - name: Upload documentation if: github.event_name == 'push' && github.ref == 'refs/heads/master' - uses: SamKirkland/FTP-Deploy-Action@v4.3.6 + uses: SamKirkland/FTP-Deploy-Action@v4.4.0 with: server: ${{ secrets.CBPAGE_SERVER }} username: ${{ secrets.CBPAGE_USER }} diff --git a/.github/workflows/build-rc.yml b/.github/workflows/build-rc.yml index 0088935d6..fede46c33 100644 --- a/.github/workflows/build-rc.yml +++ b/.github/workflows/build-rc.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: ./.github/actions/build-plugin diff --git a/.github/workflows/compile.yml b/.github/workflows/compile.yml index 53f52dfb7..4935a41ed 100644 --- a/.github/workflows/compile.yml +++ b/.github/workflows/compile.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-24.04 name: Check i18n coverage steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: shivammathur/setup-php@v2 with: @@ -47,7 +47,7 @@ jobs: needs: i18n-coverage steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: ref: ${{ github.ref }} # checkout the latest commit so that we can push changes diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index 853bcfffb..2f478fe7e 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -27,7 +27,7 @@ jobs: # If you do not check out your code, Copilot will do this for you. steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Set up MySQL run: | diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 48398af57..d20a3abc9 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -36,7 +36,7 @@ jobs: - {name: 'trunk', version: 'WordPress/WordPress#master'} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: ./.github/actions/build-plugin @@ -44,12 +44,16 @@ jobs: shell: bash run: npm -g i @wordpress/env - - name: Overwrite PHP versions & core for wp-env - if: matrix.core.version != 'null' - run: jq '. + {"phpVersion":"${{ matrix.php }}","core":"${{ matrix.core.version }}"}' tests/cypress/wordpress-files/e2e-override.json > .wp-env.override.json + - name: Overwrite PHP version & core for wp-env + run: > + jq + --arg php "${{ matrix.php }}" + --arg core "${{ matrix.core.version }}" + '. + {"phpVersion": $php} + (if $core == "null" then {} else {"core": $core} end)' + tests/cypress/wordpress-files/e2e-override.json > .wp-env.test.override.json - name: setup wp env - run: npm run env:start + run: npm run env:test:start - name: install test data for e2e test run: npm run cypress:setup @@ -69,15 +73,23 @@ jobs: - name: Enable API run: | - wp-env run tests-cli wp option patch insert commonsbooking_options_api api-activated on - wp-env run tests-cli wp option patch insert commonsbooking_options_api apikey_not_required on + wp-env --config=.wp-env.test.json run cli wp option patch insert commonsbooking_options_api api-activated on + wp-env --config=.wp-env.test.json run cli wp option patch insert commonsbooking_options_api apikey_not_required on - name: Checkout GBFS validator - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: repository: MobilityData/gbfs-validator path: gbfs-validator + - name: Cache GBFS validator dependencies + uses: actions/cache@v6 + with: + path: gbfs-validator/node_modules + key: ${{ runner.os }}-gbfs-validator-${{ hashFiles('gbfs-validator/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-gbfs-validator- + - name: Install GBFS validator dependencies working-directory: gbfs-validator run: yarn diff --git a/.github/workflows/phpcbf-check.yml b/.github/workflows/phpcbf-check.yml index eab686670..0406c7514 100644 --- a/.github/workflows/phpcbf-check.yml +++ b/.github/workflows/phpcbf-check.yml @@ -16,12 +16,12 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Set up PHP uses: shivammathur/setup-php@v2 with: - php-version: '8.1' # Adjust this to your PHP version + php-version: '8.1' extensions: uopz diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml index e79a98618..82e25427e 100644 --- a/.github/workflows/phpstan.yml +++ b/.github/workflows/phpstan.yml @@ -25,7 +25,7 @@ jobs: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 # TODO Use the composer action below - uses: shivammathur/setup-php@v2 @@ -44,7 +44,7 @@ jobs: run: | composer dump-autoload -o - #- uses: actions/checkout@v6 + #- uses: actions/checkout@v7 #- uses: php-actions/composer@v6 - name: PHPStan Static Analysis diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index c26fa59a5..442861e46 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -38,7 +38,7 @@ jobs: - { name: 'trunk', version: 'trunk' } steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Set up MySQL run: | @@ -53,13 +53,12 @@ jobs: - name: Install SVN run: sudo apt install -y subversion - - name: Validate composer.json and composer.lock run: composer validate - name: Cache Composer packages id: composer-cache - uses: actions/cache@v5 + uses: actions/cache@v6 with: path: vendor key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} @@ -84,7 +83,7 @@ jobs: php vendor/bin/phpunit - name: Upload coverage reports to Codecov - uses: codecov/codecov-action@v6 + uses: codecov/codecov-action@v7 with: token: ${{ secrets.CODECOV_TOKEN }} files: ./build/logs/clover.xml diff --git a/.github/workflows/prettier.yml b/.github/workflows/prettier.yml index 8c1b2cf0c..9e5dbe541 100644 --- a/.github/workflows/prettier.yml +++ b/.github/workflows/prettier.yml @@ -7,8 +7,10 @@ jobs: prettier: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: actions/setup-node@v6 + with: + node-version-file: '.nvmrc' - run: | yarn yarn prettier . --check diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8da28da87..d2903dd3f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,9 +8,11 @@ jobs: publish: name: New Release runs-on: ubuntu-24.04 + permissions: + contents: write steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: ./.github/actions/build-plugin @@ -34,11 +36,6 @@ jobs: SLUG: commonsbooking # optional, remove if GitHub repo name matches SVN slug, including capitalization - name: Upload release asset - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: softprops/action-gh-release@v3 with: - upload_url: ${{ github.event.release.upload_url }} - asset_path: ${{ steps.deploy.outputs.zip-path }} - asset_name: ${{ github.event.repository.name }}.zip - asset_content_type: application/zip + files: ${{ steps.deploy.outputs.zip-path }} \ No newline at end of file diff --git a/.github/workflows/wpcs.yml b/.github/workflows/wpcs.yml index 755b720b4..d31045292 100644 --- a/.github/workflows/wpcs.yml +++ b/.github/workflows/wpcs.yml @@ -16,7 +16,7 @@ jobs: name: WPCS runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Install dependencies run: composer install --prefer-dist --no-progress --ignore-platform-reqs - name: WPCS check diff --git a/.github/workflows/zip-pr.yml b/.github/workflows/zip-pr.yml index 65bde2e21..40cde4f9a 100644 --- a/.github/workflows/zip-pr.yml +++ b/.github/workflows/zip-pr.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: fetch-depth: 0 # Important for getting full commit history @@ -29,7 +29,7 @@ jobs: - name: Upload zip if: ${{ !startsWith(github.head_ref, 'dependabot') }} - uses: SamKirkland/FTP-Deploy-Action@v4.3.6 + uses: SamKirkland/FTP-Deploy-Action@v4.4.0 with: exclude: | ** diff --git a/.nvmrc b/.nvmrc index 953490f51..eb6ead3cf 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v20.20.0 +v24.16.0 diff --git a/.wp-env.json b/.wp-env.json index 472d450d2..4e738e380 100644 --- a/.wp-env.json +++ b/.wp-env.json @@ -1,6 +1,6 @@ { "phpVersion" : "8.1", - "core" : "Wordpress/Wordpress#6.9", + "core" : "Wordpress/Wordpress#7.0", "plugins" : [ ".", "https://downloads.wordpress.org/plugin/wp-crontrol.zip", @@ -11,7 +11,7 @@ "https://downloads.wordpress.org/plugin/disable-welcome-messages-and-tips.zip" ], "port" : 1000, - "testsPort" : 1001, + "testsEnvironment" : false, "config" : { "WP_DEBUG" : true, "WP_DEBUG_LOG" : true, @@ -19,17 +19,5 @@ }, "themes" : [ "flegfleg/kasimir-theme" - ], - "env" : { - "tests" : { - "mappings" : { - "wp-content/plugins/commonsbooking" : "." - }, - "config" : { - "WP_DEBUG" : true, - "WP_DEBUG_LOG" : true, - "WP_DEBUG_DISPLAY" : false - } - } - } + ] } diff --git a/.wp-env.test.json b/.wp-env.test.json new file mode 100644 index 000000000..463c34170 --- /dev/null +++ b/.wp-env.test.json @@ -0,0 +1,23 @@ +{ + "phpVersion" : "8.1", + "core" : "Wordpress/Wordpress#6.9", + "plugins" : [ + ".", + "https://downloads.wordpress.org/plugin/wp-crontrol.zip", + "https://downloads.wordpress.org/plugin/wordpress-importer.zip", + "https://downloads.wordpress.org/plugin/query-monitor.zip", + "https://downloads.wordpress.org/plugin/wp-mail-logging.zip", + "https://downloads.wordpress.org/plugin/jsm-show-post-meta.zip", + "https://downloads.wordpress.org/plugin/disable-welcome-messages-and-tips.zip" + ], + "port" : 1001, + "testsEnvironment" : false, + "config" : { + "WP_DEBUG" : true, + "WP_DEBUG_LOG" : true, + "WP_DEBUG_DISPLAY" : false + }, + "themes" : [ + "flegfleg/kasimir-theme" + ] +} diff --git a/EXTENSIBILITY_PLAN.md b/EXTENSIBILITY_PLAN.md new file mode 100644 index 000000000..0eba76997 --- /dev/null +++ b/EXTENSIBILITY_PLAN.md @@ -0,0 +1,302 @@ +# CommonsBooking Extensibility Plan + +> **Goal:** Set CommonsBooking core up for success as the *booking engine* of an +> ecosystem — solid at its core purpose (managing and booking shared common +> goods) and cleanly extensible so third parties can adapt it to their specific +> booking use case (smart locks, vehicle sharing, PDF confirmations, custom +> pricing, external calendars, …) **without forking core or overriding +> templates**. + +Status: proposal / design document. Author basis: audit of the current +`master` (v2.10.x → 2.11 line) merged with `wielebenwir/master`. + +--- + +## 1. What CommonsBooking is + +CommonsBooking is a WordPress plugin for the **management and booking of common +goods** — associations, collectives and individuals share items (cargo bikes, +tools, rooms, equipment) with a community. It is built around a small, stable +domain model expressed as WordPress custom post types (CPTs): + +| Concept | CPT | Role | +|---|---|---| +| **Item** | `cb_item` | The shared good that is lent out | +| **Location** | `cb_location` | The physical place an item is lent from | +| **Timeframe** | `cb_timeframe` | Groups all events: availability, bookable periods, holidays, **and bookings** | +| **Booking** | `cb_timeframe` (`type = BOOKING_ID`) | A concrete slot: item @ location, for a user, in a time range | +| **Restriction** | `cb_restriction` | Usage notes / breakdowns that overlay a bookable timeframe | +| **Map** | — | Frontend discovery of items/locations | + +Supporting subsystems: + +- **Availability engine** — `Model\Calendar`, `Model\Day`, `Model\Week`, + `Model\Timeframe`, `Repository\*` compute what can be booked when. +- **Booking lifecycle** — `Wordpress\CustomPostType\Booking` (request handling, + save, status transitions) + `Model\Booking` (`cancel()`, validation, codes). +- **Booking rules** — `Service\BookingRule` / `BookingRuleApplied` enforce + policy (max bookings, chains, …). +- **Messages** — `Messages\Message` and subclasses (confirmation, cancellation, + reminder, booking-code emails, iCal). +- **Booking codes** — `Service\BookingCodes` generate offline verification codes. +- **APIs** — a REST **CommonsAPI** (`API\*`) and a **GBFS** feed family + (`API\GBFS\*`) for interoperability with mobility/sharing platforms. +- **Templates & template tags** — `includes/Template*`, `CB\CB`, `templates/*`. + +### The ecosystem around it + +CommonsBooking is deliberately a **platform**, not a closed app. Real +downstream consumers already exist and drive the extensibility requirements: + +- **`cb-map`** — map/discovery front-end that consumes CB data. +- **`cb-vehicles`** — adds vehicle-specific GBFS feeds on top of core + (see upstream issue #2143 → PR #2154: a `commonsbooking_gbfs_feeds` filter was + added precisely so this plugin can register new feeds). +- **flotte-berlin** — a fork/deployment contributing bookings fixes upstream + (e.g. PR #2218). +- **Smart-lock integrations** — the concrete motivation behind the open request + for a `commonsbooking_booking_confirmed` action hook (issue #2052): "people + that are currently extending CB to work with automatic locking systems." +- **Custom deployments** (this fork, `datengraben/commonsbooking`, and many + library-of-things installations) that need per-site behavior. + +The pattern is clear: **extensions want to react to booking events, augment +data feeds, add metadata, and adjust availability/pricing — and today they +mostly can't without touching core.** Hooks have historically been added +reactively, one issue at a time (#2130 custom meta, #1938 tag object context, +#2143 GBFS feeds, #2156 API response, #2052 booking action — still open). This +plan turns that reactive trickle into a deliberate, documented extension +surface. + +--- + +## 2. How WordPress plugins expose extensibility (the model we follow) + +WordPress has one idiomatic mechanism, and mature booking/e‑commerce plugins +(WooCommerce, Easy Digital Downloads, The Events Calendar) all apply it the same +way. We adopt their conventions: + +- **Actions** (`do_action`) — "something happened; react to it." Side effects: + notify a lock system, write a log, sync an external calendar. No return value. +- **Filters** (`apply_filters`) — "here is a value; reshape it before I use it." + Must return the (possibly modified) value. Used for data, markup, config, + query args, permissions. +- **Pluggable data via CPT/meta** — third parties add their own meta through a + registration filter rather than editing forms. +- **Overridable presentation** — template resolution runs through a filter so a + theme/plugin can substitute a template part. + +**Best-practice conventions (WooCommerce-grade) we commit to:** + +1. **Namespaced prefix**: every hook starts with `commonsbooking_`. +2. **Consistent verb/tense**: `..._before_x` / `..._after_x` for render points; + past-tense `..._x_confirmed` / `..._x_cancelled` for lifecycle events; + `..._x_args` / `..._x_query` for query shaping. +3. **Rich, stable payloads**: pass the model object *and* its ID, not just a + scalar — so callbacks don't have to re-query. (Core already started doing + this in 2.10.8 for template hooks; we make it the rule.) +4. **Fire once, at the right layer**: lifecycle hooks belong in the **model / + service layer** (so they fire regardless of whether the change came from the + frontend calendar, the admin, the REST API, or WP‑CLI) — not only in the + admin `save_post` path. +5. **Filters must always return a value**; document the default and the type. +6. **Backward compatibility is a contract**: once shipped, a hook name and its + signature are stable. Deprecate via `apply_filters_deprecated` / + `do_action_deprecated`, never silently. +7. **Every public hook is documented and, where it guards behavior, unit-tested.** + +--- + +## 3. Current extension surface (audit) + +CommonsBooking already exposes ~26 filters and ~6 actions. They cluster well in +some areas and are absent in the most important one. + +**Reasonably covered today:** + +- *Presentation*: `commonsbooking_before/after_*` template actions (item, + location, booking, calendar headers, timeframe) — now with object context; + `commonsbooking_get_template_part`, `commonsbooking_template_tag`, + `commonsbooking_tag_{key}_{property}`, `commonsbooking_mobile_calendar_month_count`. +- *Email*: `commonsbooking_mail_to/subject/body/attachment`, + `commonsbooking_mail_sent`, reminder-specific filters, iCal title/description. +- *Metadata*: `commonsbooking_custom_metadata` (add CMB2 fields to any CPT). +- *Permissions*: `commonsbooking_manager_roles`, `commonsbooking_admin_roles`, + `commonsbooking_privileged_roles`, `commonsbooking_isUserAdmin`, + `commonsbooking_isCurrentUserSubscriber`. +- *Policy/config*: `commonsbooking_booking-rules`, `commonsbooking_disableCache`. +- *APIs*: `commonsbooking_gbfs_feeds`, `commonsbooking_booking_filter` (list view). + +**The critical gap — the booking lifecycle has no hooks at all.** + +The core purpose of the plugin — a booking being created, confirmed, changed, +cancelled — emits **nothing** an extension can subscribe to: + +- `Wordpress\CustomPostType\Booking::savePost()` fires the confirmation mail at + line ~162 but no action. (`src/Wordpress/CustomPostType/Booking.php`) +- `Booking::handleBookingRequest()` inserts/updates the booking post (lines + ~231–400) with no pre/post hooks and no way to veto or annotate a request. +- `Booking::postUpdated()` handles status transitions (lines ~504–528) silently. +- `Model\Booking::cancel()` updates the DB directly and mails the user + (`src/Model/Booking.php:96`) with no `..._cancelled` action. + +That single gap is why smart-lock, external-calendar and audit integrations have +to override templates or fork. It is the highest-value work in this plan. + +Secondary gaps: availability/query results are not filterable, booking-code +generation isn't pluggable, item/location repository queries can't be adjusted, +and the CommonsAPI response (unlike GBFS) still lacks a filter (#2156). + +--- + +## 4. Proposed extension-point catalog + +Concrete hooks to add, grouped by subsystem. Names follow §2. Each lifecycle +action passes the model object plus IDs; each filter documents its default. + +### 4.1 Booking lifecycle — **Priority 1** (closes #2052) + +Fire from the **model/service layer** so they trigger for frontend, admin, REST +and CLI alike. Introduce a small internal `Booking::transitionStatus()` / +`fireLifecycleHook()` helper invoked by both `savePost`, `postUpdated`, and +`Model\Booking::cancel()` to avoid double-firing. + +| Hook | Type | Signature | When | +|---|---|---|---| +| `commonsbooking_booking_before_save` | filter | `array $postarr, ?Booking $existing` | Before insert/update in `handleBookingRequest` — lets extensions annotate/adjust the booking (e.g. inject meta). | +| `commonsbooking_booking_created` | action | `int $booking_id, \CommonsBooking\Model\Booking $booking` | A new booking post is created (any status). | +| `commonsbooking_booking_confirmed` | action | `int $booking_id, \CommonsBooking\Model\Booking $booking` | Status → `confirmed` (the exact hook #2052 asks for; smart-lock trigger). | +| `commonsbooking_booking_cancelled` | action | `int $booking_id, \CommonsBooking\Model\Booking $booking` | `Model\Booking::cancel()` completes. | +| `commonsbooking_booking_status_changed` | action | `int $booking_id, string $old, string $new, \CommonsBooking\Model\Booking $booking` | Any status transition (superset; audit/logging). | +| `commonsbooking_booking_request_denied` | action | `\Exception $reason, array $requestData` | A request was rejected (double-booking, rule violation). | +| `commonsbooking_can_cancel_booking` | filter | `bool $canCancel, \CommonsBooking\Model\Booking $booking, \WP_User $user` | Override cancellation policy. | + +### 4.2 Availability & query engine — **Priority 2** + +Let extensions shape *what is bookable* (custom pricing plugins, blackout +integrations, capacity rules) without reimplementing the calendar. + +| Hook | Type | Signature | Where | +|---|---|---|---| +| `commonsbooking_calendar_data` | filter | `array $calendarData, \CommonsBooking\Model\Calendar $calendar` | `Model\Calendar` / `View\Calendar` before render. | +| `commonsbooking_day_availability` | filter | `array $slots, \CommonsBooking\Model\Day $day, int $itemId, int $locationId` | `Model\Day` slot computation. | +| `commonsbooking_bookable_timeframes` | filter | `array $timeframes, int $itemId, int $locationId` | `Repository\Timeframe`/`BookablePost` result. | +| `commonsbooking_is_timeframe_bookable` | filter | `bool $bookable, \CommonsBooking\Model\Timeframe $tf, \WP_User $user` | `Timeframe::isBookable()`. | +| `commonsbooking_booking_query_args` | filter | `array $args, string $context` | Central point in `Repository\Booking` query builders. | + +### 4.3 Booking codes — **Priority 3** + +Make offline-verification codes pluggable (some deployments want deterministic +codes tied to external lock systems, per #2052's lock-code use case). + +| Hook | Type | Signature | +|---|---|---| +| `commonsbooking_generate_booking_code` | filter | `?string $code, \CommonsBooking\Model\Timeframe $tf, int $date` (return non-null to supply your own) | +| `commonsbooking_booking_code_pool` | filter | `array $wordPool` (customize the code alphabet/word list) | + +### 4.4 Data model / repositories — **Priority 3** + +| Hook | Type | Signature | Where | +|---|---|---|---| +| `commonsbooking_item_query_args` / `commonsbooking_location_query_args` | filter | `array $args` | `Repository\Item` / `Repository\Location`. | +| `commonsbooking_post_types` | filter | `array $cpts` | `Plugin::getCustomPostTypes()` — register related CPTs / relations. | +| `commonsbooking_map_item_data` | filter | `array $data, \WP_Post $item` | `Map\*` before serialization. | + +### 4.5 APIs — **Priority 2** (parity with GBFS; closes #2156) + +GBFS already has `commonsbooking_gbfs_feeds` and per-feed response filters. Give +the CommonsAPI the same treatment. + +| Hook | Type | Signature | +|---|---|---| +| `commonsbooking_api_item_response` | filter | `array $data, \WP_Post $item` | +| `commonsbooking_api_availability_response` | filter | `array $data, array $query` | +| `commonsbooking_gbfs_{feed}_response` | filter | `array $feed` (generalize the pattern already used for feed registration to each feed body) | + +### 4.6 Presentation — **already strong; small additions** + +- `commonsbooking_booking_form_fields` (filter) — add fields to the frontend + booking form so extensions capture extra data at booking time. +- `commonsbooking_before/after_booking_form` (actions) — render points around + the form. + +--- + +## 5. Design principles for this work + +1. **Model layer is the source of truth for events.** Lifecycle actions fire in + `Model\Booking` / a dedicated `Service\BookingLifecycle`, never only in the + admin `save_post` callback. This is what makes REST/CLI/admin/frontend all + emit the same events — the property extensions depend on. +2. **Idempotent firing.** Guard against WordPress's double-save behavior (the + codebase already uses `hasRunBefore()` and `remove_action` self-unhooking — + reuse those patterns so a hook fires exactly once per real transition). +3. **Pass objects, not just scalars.** Every model hook gets the hydrated + `Model\*` instance; filters document default value + type. +4. **Filters are safe by default.** A no-op callback returning the input must + leave behavior unchanged. Never require a filter to be present. +5. **No breaking changes.** Existing hook names/signatures are frozen. New hooks + are additive. Any future rename goes through `*_deprecated` wrappers. +6. **Document + test every public hook.** A hook without a docs entry and (for + behavior-guarding hooks) a test does not ship. + +--- + +## 6. Phased roadmap + +**Phase 0 — Foundation (docs + convention).** +Adopt this document. Add a "Hook reference" appendix generated/maintained +alongside `docs/*/advanced-functionality/hooks-and-filters.md`. Define the +naming convention in `TECHNICAL.md`. Establish the `Service\BookingLifecycle` +helper and a test pattern (`WP_UnitTestCase` asserting `did_action()` and filter +application). + +**Phase 1 — Booking lifecycle actions (closes #2052).** +Implement §4.1. This is the single highest-impact deliverable and unblocks the +smart-lock ecosystem. Ship with docs + tests + a worked example +(`commonsbooking_booking_confirmed` → call an external lock API). + +**Phase 2 — Availability & API filters.** +Implement §4.2 and §4.5. Enables custom pricing/capacity extensions and gives +CommonsAPI response parity with GBFS. + +**Phase 3 — Booking codes, repositories, form fields.** +Implement §4.3, §4.4, §4.6. Rounds out the surface for deployment-specific +customization. + +**Phase 4 — Consolidate & document.** +Publish a complete, versioned hook reference; add an "Extending CommonsBooking" +developer guide with end-to-end examples (smart lock, extra booking metadata, +custom feed). Consider a lightweight `commonsbooking-extension-boilerplate`. + +Each phase is independently shippable and backward compatible. + +--- + +## 7. Testing, docs & compatibility strategy + +- **Tests:** for every behavior-guarding hook, a PHPUnit test that (a) asserts + the action fires with the documented arguments (`did_action`, spy callbacks) + and (b) asserts a filter actually alters the outcome. Lifecycle tests cover + all four entry paths (frontend request, admin save, REST, `Model::cancel`). +- **Docs:** extend `hooks-and-filters.md` (EN + DE) with each new hook, its + signature, default value, "since" version, and a copy-paste example. Keep the + existing "Hooks in the context of an object" tone. +- **Versioning:** introduce hooks in minor releases; note them in the changelog + under a dedicated "For developers" heading (the readme already has this). +- **Backward compatibility:** never change a shipped signature; use + `apply_filters_deprecated`/`do_action_deprecated` if a hook must evolve. + +--- + +## 8. Summary + +CommonsBooking is already a well-factored domain platform with a good +*presentation* and *email* extension surface, but its **core purpose — the +booking lifecycle — is currently a black box** to extensions. The community is +asking for exactly the hooks this gap implies (smart locks #2052, vehicle feeds +#2143, API responses #2156, custom meta #2130). By (1) adding lifecycle actions +in the model layer, (2) making availability/queries and API responses +filterable, and (3) making booking codes and forms pluggable — all additively, +documented and tested — CommonsBooking becomes a booking *engine* that people +can adapt to their specific use case without forking core. diff --git a/Readme.md b/Readme.md index 3aace4a9e..e6fbc7f0e 100644 --- a/Readme.md +++ b/Readme.md @@ -6,12 +6,15 @@ # CommonsBooking -Contributors: wielebenwirteam, m0rb, flegfleg, chriwen -Donate link: https://www.wielebenwir.de/verein/unterstutzen -License: GPLv2 or later -License URI: http://www.gnu.org/licenses/gpl-2.0.html +**Contributors**: wielebenwirteam, m0rb, flegfleg, chriwen, hansmorb, datengraben -CommonsBooking is a plugin for the management and booking of common goods. This plugin provides associations, groups, and individuals with the ability to share items (such as cargo bikes and tools) among users. It is based on the concept of Commons, where resources are shared for the benefit of the community. +**Donate link**: https://www.wielebenwir.de/verein/unterstutzen + +**License**: GPLv2 or later + +**License URI**: http://www.gnu.org/licenses/gpl-2.0.html + +CommonsBooking is a plugin for the management and booking of common goods. This plugin gives associations, groups and individuals the ability to share items (e.g. cargo bikes, tools) with users. It is based on the idea of the commons and sharing resources for the benefit of the community. ## Links @@ -86,6 +89,7 @@ First, we have to install the necessary dependencies and packages: We can do thi npm run start ``` `npm run start` runs `composer install`, `npm install` and then builds assets via `grunt dist`. +This might take a while, as dependencies are downloaded and namespaces are prefixed using Strauss. The most easy way to start hacking WordPress plugins in general (if you have no other development environment set up) is using [wp-env](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-env/). Install it and it's dependencies (mainly Docker) and run this to start the enviroment: ``` @@ -105,14 +109,14 @@ To test the code you first run the [preparation scripts](https://github.com/wp-c bash bin/install-wp-tests.sh wordpress root password 127.0.0.1:49153 latest ``` -Testing the plugin code via `phpunit`. At the moment it works only with a manually downloaded phar. We are using PHPUnit 9 and PHP7.4 for the automated tests. The tests might fail if you are using a different version. +Testing the plugin code via `phpunit`. We are using PHPUnit 9, because it is the latest version currently compatible with the WordPress testing suite. ``` -php ~/phpunit.phar --bootstrap tests/php/bootstrap.php +php vendor/bin/phpunit --bootstrap tests/php/bootstrap.php ``` E2E (end to end) tests are written in [cypress](https://www.cypress.io/). To run them you need to install cypress and start the wordpress environment: ```bash -npm run env:start +npm run env:test:start ``` Now, install the test data needed for the tests: ```bash diff --git a/assets/admin/js/src/timeframe.js b/assets/admin/js/src/timeframe.js index 632aa565d..88561efa0 100644 --- a/assets/admin/js/src/timeframe.js +++ b/assets/admin/js/src/timeframe.js @@ -170,7 +170,7 @@ * Currently only for holidays, holidays used to only have one assignable single selection. */ const migrateSingleSelection = () => { - if (typeInput.val() != HOLIDAYS_ID) { + if (typeInput.val() != HOLIDAYS_ID || typeInput.val() != REPAIR_ID) { return; } // get single selection @@ -222,7 +222,7 @@ } //we migrate the single selection to the multiselect (new holiday timeframes do not have a single selection anymore) - if (selectedType == HOLIDAYS_ID) { + if (selectedType == HOLIDAYS_ID || selectedType == REPAIR_ID) { itemSelectionInput.show(); locationSelectionInput.show(); migrateSingleSelection(); @@ -244,7 +244,7 @@ const handleLocationSelection = function () { const selectedType = $('option:selected', typeInput).val(); //disable the mass selection for all timeframes except holidays - if (selectedType == HOLIDAYS_ID) { + if (selectedType == HOLIDAYS_ID || selectedType == REPAIR_ID) { singleLocationSelection.hide(); //handle different selection types const selectedOption = $('option:selected', locationSelectionInput).val(); @@ -275,7 +275,7 @@ const handleItemSelection = function () { const selectedType = $('option:selected', typeInput).val(); //disable the mass selection for all timeframes except holidays (for now) - if (selectedType == HOLIDAYS_ID) { + if (selectedType == HOLIDAYS_ID || selectedType == REPAIR_ID) { singleItemSelection.hide(); //handle different selection types const selectedOption = $('option:selected', itemSelectionInput).val(); diff --git a/assets/public/js/src/lib/litepicker.js b/assets/public/js/src/lib/litepicker.js index 64f0e754a..3d0a3a41c 100644 --- a/assets/public/js/src/lib/litepicker.js +++ b/assets/public/js/src/lib/litepicker.js @@ -247,7 +247,7 @@ ) t.styleSheet.cssText = o; else { - for (; t.firstChild; ) t.removeChild(t.firstChild); + for (; t.firstChild;) t.removeChild(t.firstChild); t.appendChild(document.createTextNode(o)); } } @@ -826,7 +826,7 @@ this.datePicked[0].getTime() < k.getTime() && u.push(k); } - for (; h > 0; ) { + for (; h > 0;) { ((h -= 1), (c = c.add(1, 'day'))); for (var D = 0, v = u; D < v.length; D++) v[D].getTime() === c.getTime() && @@ -1113,7 +1113,7 @@ if (/^-?\d{10,}$/.test(e)) return t.getDateZeroTime(new Date(Number(e))); if ('string' == typeof e) { - for (var n = [], s = null; null != (s = t.regex.exec(i)); ) + for (var n = [], s = null; null != (s = t.regex.exec(i));) '\\' !== s[1] && n.push(s); if (n.length) { var a = { @@ -1510,7 +1510,7 @@ }), (t.prototype.format = function (e, i) { void 0 === i && (i = 'en-US'); - for (var o = '', n = [], s = null; null != (s = t.regex.exec(e)); ) + for (var o = '', n = [], s = null; null != (s = t.regex.exec(e));) '\\' !== s[1] && n.push(s); if (n.length) { n[0].index > 0 && (o += e.substring(0, n[0].index)); diff --git a/bin/setup-cypress-env.sh b/bin/setup-cypress-env.sh index 0ca73a05c..24a72e5d3 100755 --- a/bin/setup-cypress-env.sh +++ b/bin/setup-cypress-env.sh @@ -1,8 +1,8 @@ #!/bin/bash -wp-env run tests-cli wp plugin install --activate wordpress-importer +wp-env --config=.wp-env.test.json run cli wp plugin install --activate wordpress-importer # Install our example posts from a WP export file -wp-env run tests-cli wp import /var/www/html/wp-content/plugins/commonsbooking/tests/cypress/wordpress-files/content-example.xml --authors=create +wp-env --config=.wp-env.test.json run cli wp import /var/www/html/wp-content/plugins/commonsbooking/tests/cypress/wordpress-files/content-example.xml --authors=create # Switch to Kasimir theme -wp-env run tests-cli wp theme activate kasimir-theme +wp-env --config=.wp-env.test.json run cli wp theme activate kasimir-theme # Create subscriber with username "subscriber" and password "password" -wp-env run tests-cli wp user create subscriber sub@sub.de --role=subscriber --user_pass=password +wp-env --config=.wp-env.test.json run cli wp user create subscriber sub@sub.de --role=subscriber --user_pass=password diff --git a/commonsbooking.php b/commonsbooking.php index 6d9c4884e..dfa5ab981 100644 --- a/commonsbooking.php +++ b/commonsbooking.php @@ -43,3 +43,6 @@ require __DIR__ . '/vendor-prefixed/mustardbees/cmb-field-select2/cmb-field-select2.php'; require __DIR__ . '/vendor-prefixed/ed-itsolutions/cmb2-field-ajax-search/cmb2-field-ajax-search.php'; require_once __DIR__ . '/includes/Plugin.php'; + +// Optional third-party integrations (loaded conditionally, no hard dependency). +require_once __DIR__ . '/integrations/uncanny-automator/bootstrap.php'; diff --git a/composer.json b/composer.json index 4416b5a2e..ef863aa4d 100644 --- a/composer.json +++ b/composer.json @@ -78,7 +78,7 @@ "@prefix-namespaces" ], "prefix-namespaces": [ - "sh -c 'test -f ./bin/strauss.phar || curl -o bin/strauss.phar -L -C - https://github.com/BrianHenryIE/strauss/releases/download/0.26.5/strauss.phar'", + "sh -c 'test -f ./bin/strauss.phar || curl -o bin/strauss.phar -L -C - https://github.com/BrianHenryIE/strauss/releases/download/0.27.3/strauss.phar'", "@php bin/strauss.phar --info", "@composer dump-autoload" ] diff --git a/composer.lock b/composer.lock index c25b1dba2..b12caf2cd 100644 --- a/composer.lock +++ b/composer.lock @@ -74,26 +74,23 @@ }, { "name": "cmb2/cmb2", - "version": "v2.11.0", + "version": "v2.12.0", "source": { "type": "git", "url": "https://github.com/CMB2/CMB2.git", - "reference": "2847828b5cce1b48d09427ee13e6f7c752704468" + "reference": "9a3686d42539ee3c1113bd09623f4ffbbee94397" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/CMB2/CMB2/zipball/2847828b5cce1b48d09427ee13e6f7c752704468", - "reference": "2847828b5cce1b48d09427ee13e6f7c752704468", + "url": "https://api.github.com/repos/CMB2/CMB2/zipball/9a3686d42539ee3c1113bd09623f4ffbbee94397", + "reference": "9a3686d42539ee3c1113bd09623f4ffbbee94397", "shasum": "" }, "require": { "php": ">7.4" }, "require-dev": { - "apigen/apigen": "4.1.2", - "awesomemotive/am-cli-tools": ">=1.3.7", - "nette/utils": "2.5.3", - "phpunit/phpunit": "^6.5", + "phpunit/phpunit": "^9.6", "yoast/phpunit-polyfills": "^1.1" }, "suggest": { @@ -129,7 +126,7 @@ "issues": "https://github.com/CMB2/CMB2/issues", "source": "http://wordpress.org/support/plugin/cmb2" }, - "time": "2024-04-02T19:30:07+00:00" + "time": "2026-05-31T02:21:57+00:00" }, { "name": "ed-itsolutions/cmb2-field-ajax-search", @@ -978,16 +975,16 @@ }, { "name": "phpmailer/phpmailer", - "version": "v7.0.2", + "version": "v7.1.1", "source": { "type": "git", "url": "https://github.com/PHPMailer/PHPMailer.git", - "reference": "ebf1655bd5b99b3f97e1a3ec0a69e5f4cd7ea088" + "reference": "1bc1716a507a65e039d4ac9d9adebbbd0d346e15" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/ebf1655bd5b99b3f97e1a3ec0a69e5f4cd7ea088", - "reference": "ebf1655bd5b99b3f97e1a3ec0a69e5f4cd7ea088", + "url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/1bc1716a507a65e039d4ac9d9adebbbd0d346e15", + "reference": "1bc1716a507a65e039d4ac9d9adebbbd0d346e15", "shasum": "" }, "require": { @@ -1048,7 +1045,7 @@ "description": "PHPMailer is a full-featured email creation and transfer class for PHP", "support": { "issues": "https://github.com/PHPMailer/PHPMailer/issues", - "source": "https://github.com/PHPMailer/PHPMailer/tree/v7.0.2" + "source": "https://github.com/PHPMailer/PHPMailer/tree/v7.1.1" }, "funding": [ { @@ -1056,7 +1053,7 @@ "type": "github" } ], - "time": "2026-01-09T18:02:33+00:00" + "time": "2026-05-18T08:06:14+00:00" }, { "name": "psr/cache", @@ -1452,16 +1449,16 @@ }, { "name": "symfony/cache", - "version": "v5.4.40", + "version": "v5.4.52", "source": { "type": "git", "url": "https://github.com/symfony/cache.git", - "reference": "89005bc368ca02ed0433c592e4d27670d0844a66" + "reference": "03b191dda148c490b5b3929eaac827ee64f1d421" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache/zipball/89005bc368ca02ed0433c592e4d27670d0844a66", - "reference": "89005bc368ca02ed0433c592e4d27670d0844a66", + "url": "https://api.github.com/repos/symfony/cache/zipball/03b191dda148c490b5b3929eaac827ee64f1d421", + "reference": "03b191dda148c490b5b3929eaac827ee64f1d421", "shasum": "" }, "require": { @@ -1529,7 +1526,7 @@ "psr6" ], "support": { - "source": "https://github.com/symfony/cache/tree/v5.4.40" + "source": "https://github.com/symfony/cache/tree/v5.4.52" }, "funding": [ { @@ -1540,12 +1537,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-05-31T14:33:22+00:00" + "time": "2026-05-15T06:40:15+00:00" }, { "name": "symfony/cache-contracts", @@ -1628,16 +1629,16 @@ }, { "name": "symfony/deprecation-contracts", - "version": "v3.6.0", + "version": "v3.7.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", - "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/50f59d1f3ca46d41ac911f97a78626b6756af35b", + "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b", "shasum": "" }, "require": { @@ -1650,7 +1651,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.6-dev" + "dev-main": "3.7-dev" } }, "autoload": { @@ -1675,7 +1676,7 @@ "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" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.0" }, "funding": [ { @@ -1686,12 +1687,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-25T14:21:43+00:00" + "time": "2026-04-13T15:52:40+00:00" }, { "name": "symfony/http-client", @@ -1942,7 +1947,7 @@ }, { "name": "symfony/polyfill-php73", - "version": "v1.33.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php73.git", @@ -1998,7 +2003,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php73/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-php73/tree/v1.37.0" }, "funding": [ { @@ -2022,16 +2027,16 @@ }, { "name": "symfony/polyfill-php80", - "version": "v1.33.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608" + "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608", - "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dfb55726c3a76ea3b6459fcfda1ec2d80a682411", + "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411", "shasum": "" }, "require": { @@ -2082,7 +2087,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.37.0" }, "funding": [ { @@ -2102,7 +2107,7 @@ "type": "tidelift" } ], - "time": "2025-01-02T08:10:11+00:00" + "time": "2026-04-10T16:19:22+00:00" }, { "name": "symfony/polyfill-php83", @@ -2186,16 +2191,16 @@ }, { "name": "symfony/service-contracts", - "version": "v3.6.1", + "version": "v3.7.0", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43" + "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43", - "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/d25d82433a80eba6aa0e6c24b61d7370d99e444a", + "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a", "shasum": "" }, "require": { @@ -2213,7 +2218,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.6-dev" + "dev-main": "3.7-dev" } }, "autoload": { @@ -2249,7 +2254,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.6.1" + "source": "https://github.com/symfony/service-contracts/tree/v3.7.0" }, "funding": [ { @@ -2269,28 +2274,30 @@ "type": "tidelift" } ], - "time": "2025-07-15T11:30:57+00:00" + "time": "2026-03-28T09:44:51+00:00" }, { "name": "symfony/var-exporter", - "version": "v5.4.40", + "version": "v6.4.37", "source": { "type": "git", "url": "https://github.com/symfony/var-exporter.git", - "reference": "6a13d37336d512927986e09f19a4bed24178baa6" + "reference": "34f6957deffacabd1b1c579a312daa481e3e99ca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-exporter/zipball/6a13d37336d512927986e09f19a4bed24178baa6", - "reference": "6a13d37336d512927986e09f19a4bed24178baa6", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/34f6957deffacabd1b1c579a312daa481e3e99ca", + "reference": "34f6957deffacabd1b1c579a312daa481e3e99ca", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/polyfill-php80": "^1.16" + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3" }, "require-dev": { - "symfony/var-dumper": "^4.4.9|^5.0.9|^6.0" + "symfony/property-access": "^6.4|^7.0", + "symfony/serializer": "^6.4|^7.0", + "symfony/var-dumper": "^5.4|^6.0|^7.0" }, "type": "library", "autoload": { @@ -2323,10 +2330,12 @@ "export", "hydrate", "instantiate", + "lazy-loading", + "proxy", "serialize" ], "support": { - "source": "https://github.com/symfony/var-exporter/tree/v5.4.40" + "source": "https://github.com/symfony/var-exporter/tree/v6.4.37" }, "funding": [ { @@ -2337,12 +2346,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-05-31T14:33:22+00:00" + "time": "2026-04-14T12:12:40+00:00" }, { "name": "willdurand/geocoder", @@ -3736,11 +3749,11 @@ }, { "name": "phpstan/phpstan", - "version": "2.1.44", + "version": "2.2.1", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/4a88c083c668b2c364a425c9b3171b2d9ea5d218", - "reference": "4a88c083c668b2c364a425c9b3171b2d9ea5d218", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/dea9c8f2d25cc849391042b71e429c1a4bf82660", + "reference": "dea9c8f2d25cc849391042b71e429c1a4bf82660", "shasum": "" }, "require": { @@ -3763,6 +3776,17 @@ "license": [ "MIT" ], + "authors": [ + { + "name": "Ondřej Mirtes" + }, + { + "name": "Markus Staab" + }, + { + "name": "Vincent Langlet" + } + ], "description": "PHPStan - PHP Static Analysis Tool", "keywords": [ "dev", @@ -3785,7 +3809,7 @@ "type": "github" } ], - "time": "2026-03-25T17:34:21+00:00" + "time": "2026-05-28T14:44:12+00:00" }, { "name": "phpunit/php-code-coverage", diff --git a/docs/.vitepress/components/AnnouncementBanner.vue b/docs/.vitepress/components/AnnouncementBanner.vue new file mode 100644 index 000000000..13e0cf076 --- /dev/null +++ b/docs/.vitepress/components/AnnouncementBanner.vue @@ -0,0 +1,75 @@ + + + + + diff --git a/docs/.vitepress/components/ExpandAll.vue b/docs/.vitepress/components/ExpandAll.vue new file mode 100644 index 000000000..85c12a95b --- /dev/null +++ b/docs/.vitepress/components/ExpandAll.vue @@ -0,0 +1,53 @@ + + + + + diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index cbf4fdbc8..a1ae2fa7d 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -273,19 +273,7 @@ export function sidebarDocs_de(): DefaultTheme.SidebarItem[] { { text: 'Häufige Fragen (Version 0.9.x)', link: 'faq' } ] }, - { - text: 'Häufige Fragen (FAQ)', base: '/de/documentation/faq/', - link: '/', - collapsed: true, - items: [ - { text: 'Die Seite ist sehr langsam', link: 'site-slow' }, - { text: 'Probleme und Antworten', link: 'problems-and-answers' }, - { text: 'Wie bekomme ich den Buchungskommentar ...', link: 'booking-comment-emails' }, - { text: 'Kann ich Zahlenschloss-Codes in E-Mails einfügen?', link: 'lock-codes-email' }, - { text: 'Artikeldetailseite übersichtlicher gestalten', link: 'organise-article-page' }, - { text: 'Wie verhindere ich Spam Registrierungen', link: 'avoid-spam-registrations'} - ] - }, + { text: 'Häufige Fragen (FAQ)', link: '/de/documentation/faq/', items: [] }, ] } @@ -395,19 +383,7 @@ export function sidebarDocs_en(): DefaultTheme.SidebarItem[] { { text: 'Overview of releases and planned further development', link: 'release-overview' } ] }, - { - text: 'Frequently Asked Questions (FAQ)', base: '/en/documentation/faq/', - link: '/', - collapsed: true, - items: [ - { text: 'The site is very slow', link: 'site-slow' }, - { text: 'Problems and answers', link: 'problems-and-answers' }, - { text: 'How do I show the booking comment on the page and in the email?', link: 'booking-comment-emails' }, - { text: 'How do I show lock codes in emails?', link: 'lock-codes-email' }, - { text: 'How can I make the item detail page clearer?', link: 'organise-article-page' }, - { text: 'How do I prevent spam registrations?', link: 'avoid-spam-registrations'} - ] - }, + { text: 'Frequently Asked Questions (FAQ)', link: '/en/documentation/faq/', items: [] }, ] } diff --git a/docs/.vitepress/theme/index.js b/docs/.vitepress/theme/index.js index fd81d720f..293df5935 100644 --- a/docs/.vitepress/theme/index.js +++ b/docs/.vitepress/theme/index.js @@ -1,25 +1,28 @@ // .vitepress/theme/index.js +import { h, onMounted } from 'vue' import DefaultTheme from 'vitepress/theme' import './custom.css' -import { onMounted } from 'vue'; -import mediumZoom from 'medium-zoom'; -import { useData } from 'vitepress'; +import mediumZoom from 'medium-zoom' +import { useData } from 'vitepress' +import AnnouncementBanner from '../components/AnnouncementBanner.vue' export default { - ...DefaultTheme, - setup() { - const { page } = useData(); - - onMounted(() => { - const LOCALE_STORAGE_KEY = 'cb-doc-locale'; - const path = window.location.pathname; - const locale = path.startsWith('/en/') ? '/en/' : path.startsWith('/de/') ? '/de/' : '/de/'; - - // Persist the currently viewed locale so root can reuse the choice later - localStorage.setItem(LOCALE_STORAGE_KEY, locale); - - // Keep zoom working across locales - mediumZoom('[data-zoomable]', { background: 'var(--vp-c-bg)' }); - }); - }, + ...DefaultTheme, + Layout() { + return h(DefaultTheme.Layout, null, { + 'home-hero-before': () => h(AnnouncementBanner), + }) + }, + setup() { + const { page } = useData(); + onMounted(() => { + const LOCALE_STORAGE_KEY = 'cb-doc-locale'; + const path = window.location.pathname; + const locale = path.startsWith('/en/') ? '/en/' : path.startsWith('/de/') ? '/de/' : '/de/'; + // Persist the currently viewed locale so root can reuse the choice later + localStorage.setItem(LOCALE_STORAGE_KEY, locale); + // Keep zoom working across locales + mediumZoom('[data-zoomable]', { background: 'var(--vp-c-bg)' }); + }); + }, }; diff --git a/docs/de/documentation/advanced-functionality/cache.md b/docs/de/documentation/advanced-functionality/cache.md index 8e280ba5b..b1f37169d 100644 --- a/docs/de/documentation/advanced-functionality/cache.md +++ b/docs/de/documentation/advanced-functionality/cache.md @@ -36,7 +36,7 @@ Ein falsch konfigurierter Cache kann deine Seite verlangsamen. Probiere dann einen anderen Pfad für den Cache oder einen anderen Cache Adapter. * Falls deine Seite sehr langsam ist, kann das auch auf Problem mit dem Cache hindeuten. - Mehr dazu: [Die Seite ist sehr langsam](../faq/site-slow). + Mehr dazu: [Die Seite ist sehr langsam](../faq/index#my-site-is-very-slow-what-can-i-do). * Siehe auch die Seite [Häufige Fragen](../faq/) für bekannte Inkompatibilitäten. * **Regelmäßiges Aufwärmen des Caches durch Cronjob**: diff --git a/docs/de/documentation/advanced-functionality/hooks-and-filters.md b/docs/de/documentation/advanced-functionality/hooks-and-filters.md index 01f7587be..1b48d8452 100644 --- a/docs/de/documentation/advanced-functionality/hooks-and-filters.md +++ b/docs/de/documentation/advanced-functionality/hooks-and-filters.md @@ -50,6 +50,10 @@ add_action( 'commonsbooking_before_item-single', 'itemsingle_callback' ); * commonsbooking_before_item-single * commonsbooking_after_item-single * commonsbooking_mail_sent + * [commonsbooking_booking_created](#buchungs-lifecycle-hooks-seit-2-11-0) + * [commonsbooking_booking_confirmed](#buchungs-lifecycle-hooks-seit-2-11-0) + * [commonsbooking_booking_cancelled](#buchungs-lifecycle-hooks-seit-2-11-0) + * [commonsbooking_booking_status_changed](#buchungs-lifecycle-hooks-seit-2-11-0) ### Hooks im Objektkontext (seit 2.10.8) @@ -75,6 +79,43 @@ function my_cb_before_booking_single( $booking_id, $booking ) { add_action( 'commonsbooking_before_booking-single', 'my_cb_before_booking_single', 10, 2 ); ``` +### Buchungs-Lifecycle-Hooks (seit 2.11.0) + +Mit diesen Action Hooks kannst du auf den Lebenszyklus einer Buchung reagieren – +zum Beispiel um ein elektronisches Schloss anzusteuern, einen externen Kalender +zu synchronisieren oder ein Protokoll zu schreiben. Sie werden unabhängig davon +ausgelöst, wo die Änderung ausgelöst wurde (Frontend-Kalender, Admin-Backend, +REST-API oder WP-CLI), da sie am Statuswechsel des WordPress-Posts und nicht an +einem einzelnen Formular-Handler hängen. + +Jeder Hook erhält die Post-ID der Buchung und die zugehörige Instanz aus +`\CommonsBooking\Model\Booking` (Ausnahme: `..._status_changed`, hier werden +zusätzlich der alte und der neue Status übergeben). + + * `commonsbooking_booking_created` + * Wird **einmalig** ausgelöst, sobald eine Buchung zum ersten Mal einen echten Status erhält. + * Parameter: `int $booking_id`, `\CommonsBooking\Model\Booking $booking` + * `commonsbooking_booking_confirmed` + * Wird ausgelöst, sobald eine Buchung den Status `confirmed` erhält. Das ist der empfohlene Hook für Integrationen, die auf eine aktiv werdende Buchung reagieren müssen. + * Parameter: `int $booking_id`, `\CommonsBooking\Model\Booking $booking` + * `commonsbooking_booking_cancelled` + * Wird ausgelöst, wenn eine Buchung storniert wird. + * Parameter: `int $booking_id`, `\CommonsBooking\Model\Booking $booking` + * `commonsbooking_booking_status_changed` + * Wird bei jedem Statuswechsel einer Buchung ausgelöst (Obermenge der Hooks oben). Hinweis: Da eine Stornierung den Status direkt in die Datenbank schreibt, sollten Stornierungen besser über `commonsbooking_booking_cancelled` als über diesen Hook beobachtet werden. + * Parameter: `int $booking_id`, `string $old_status`, `string $new_status`, `\CommonsBooking\Model\Booking $booking` + +#### Beispiel: Schloss ansteuern, wenn eine Buchung bestätigt wird + +```php +add_action( 'commonsbooking_booking_confirmed', function ( $booking_id, $booking ) { + $item = $booking->getItem(); + $lockCode = $booking->formattedBookingCode(); + // Code / Buchungszeitraum hier an das Schließsystem übergeben … + my_locking_system_program( $item->ID, $lockCode, $booking->getStartDate(), $booking->getEndDate() ); +}, 10, 2 ); +``` + ## Filter Hooks Filter Hooks (https://developer.wordpress.org/plugins/hooks/filters) funktionieren @@ -85,7 +126,7 @@ zurückgibt. ### Alle Filter Hooks im Überblick: * commonsbooking_custom_metadata - * [commonsbooking_isCurrentUserAdmin](../basics/permission-management#filterhook-isCurrentUserAdmin) + * [commonsbooking_isUserAdmin](../basics/permission-management#filterhook-isUserAdmin) * commonsbooking_isCurrentUserSubscriber * commonsbooking_get_template_part * commonsbooking_template_tag @@ -96,6 +137,7 @@ zurückgibt. * commonsbooking_mail_body * commonsbooking_mail_attachment * commonsbooking_disableCache + * commonsbooking_gbfs_feeds Es gibt auch Filter Hooks, mit denen du zusätzliche Benutzerrollen, die zusätzlich zum CB Manager Artikel und Standorte administrieren können, diff --git a/docs/de/documentation/api/commonsbooking-api.md b/docs/de/documentation/api/commonsbooking-api.md index e42d0fe44..976fc7865 100644 --- a/docs/de/documentation/api/commonsbooking-api.md +++ b/docs/de/documentation/api/commonsbooking-api.md @@ -29,6 +29,10 @@ Die API erreichst du über _Einstellungen_ -> _CommonsBooking_ -> Tab: _API / Ex * Push-URL: Hier könnt ihr die URL des empfangenden Systems eingeben. CommonsBooking wird diese URL bei jeder Änderung der Daten aufrufen, sodass das entfernte System dann über eine Änderung der Daten informiert und diese über einen separaten API-Call abrufen kann. So kann ein Datenaustausch in Echtzeit ermöglicht werden. * API-Schlüssel: Hier könnt ihr einen selbstgewählten API-Schlüssel eingeben. Wenn der Schlüssel gesetzt ist, muss das abfragende System in jeder Abfrage den Parameter apikey= _[Schlüssel]_ mitliefern. +## Artikel von der API ausschliessen + +Wenn Artikel nicht in der API erscheinen sollen, müssen diese explizit ausgeschlossen werden. Diese Einstellungen kann auf Artikelebene vorgenommen werden, wenn dort der entsprechende Haken gesetzt ist, tauchen diese Artikel nicht in den API Routen auf. + ## Spezifikation der API-Routen **Verfügbarkeit** diff --git a/docs/de/documentation/api/gbfs.md b/docs/de/documentation/api/gbfs.md index aca58d32b..c8ea24219 100644 --- a/docs/de/documentation/api/gbfs.md +++ b/docs/de/documentation/api/gbfs.md @@ -1,17 +1,22 @@ # GBFS -::: danger Fehler in der API -Mit [Issue 1388](https://github.com/wielebenwir/commonsbooking/issues/1388) ist ein Fehler in der API dokumentiert. Die zeitliche Verfügbarkeit ist u.U. also nicht korrekt. -::: - -Seit 2.5 - Diese Schnittstelle stellt Daten der [Standorte](../first-steps/create-location), [Artikel](../first-steps/create-item) und deren Verfügbarkeit über die [Zeitrahmen](../first-steps/booking-timeframes-manage) in einem stadardisierten Schema bereit. -Die folgenden Endpunkte der _General Bikeshare Feed Specification_ ([GBFS](https://www.gbfs.org/documentation/)) Schema werden unterstützt: +Aktuell wird die Version 3.1-RC3 der _General Bikeshare Feed Specification_ ([GBFS](https://www.gbfs.org/documentation/)) unterstützt und die folgenden Endpunkte werden vom Plugin exponiert: + +* station_status.json +* station_information.json +* system_information.json +* vehicle_status.json +* vehicle_availability.json +* vehicle_types.json +* gbfs.json (Discovery) -* station_status -* station_information -* system_information -* gbfs.json (Discovery) \ No newline at end of file +## Zu vehicle_types.json + +Da über CommonsBooking hauptsächlich Lastenräder verliehen werden, gibt die API standardmäßig den `form_factor` als `cargo_bicycle` zurück. Da `propulsion_type` ein Pflichtfeld ist, wird hier standardmäßig `human` gesetzt. Wenn das nicht der Fall wäre, müssten noch viel mehr zusätzliche Informationen zum Antrieb zur Verfügung gestellt werden. + +::: info Behobenes Problem +Seit Juni 2026 (Version 2.11) ist der [Fehler zur Bereitstellung der zeitlichen Verfügbarkeit](https://github.com/wielebenwir/commonsbooking/issues/1388) behoben. +::: diff --git a/docs/de/documentation/basics/permission-management.md b/docs/de/documentation/basics/permission-management.md index af22ab9f6..7415c1fc2 100644 --- a/docs/de/documentation/basics/permission-management.md +++ b/docs/de/documentation/basics/permission-management.md @@ -60,14 +60,14 @@ Folgende Dinge darf der CommonsBooking Manager zum Beispiel nicht: * Das Design der Seite ändern * usw. -## CommonsBooking Manager zum Manager für alle Artikel / Standorte machen {#filterhook-isCurrentUserAdmin} +## CommonsBooking Manager zum Manager für alle Artikel / Standorte machen {#filterhook-isUserAdmin} Mithilfe eines [Filter Hooks](../advanced-functionality/hooks-and-filters) kannst du eine bestimmte Rolle so einstellen, dass sie automatisch Manager für alle Artikel / Standorte wird. Das unten genannte Beispiel macht das mit der Rolle 'cb_manager', also konfiguriert den CB-Manager so, dass er automatisch allen Artikeln und Standorten der Instanz zugerechnet wird. Falls das mit einer anderen Rolle passieren soll, muss diese Rolle auch den Manager Rollen mit einem [weiteren Codeschnipsel hinzugefügt werden](#filterhook-manager-roles). ```php add_filter( - 'commonsbooking_isCurrentUserAdmin', + 'commonsbooking_isUserAdmin', function ( bool $isAdmin, WP_User $user ) { return in_array( 'cb_manager', $user->roles, true ) ? true : $isAdmin; }, diff --git a/docs/de/documentation/faq/avoid-spam-registrations.md b/docs/de/documentation/faq/avoid-spam-registrations.md deleted file mode 100644 index 475a5f90f..000000000 --- a/docs/de/documentation/faq/avoid-spam-registrations.md +++ /dev/null @@ -1,9 +0,0 @@ -# Wie verhindere ich Spam Registrierungen - -Dafür gibt es verschiedene Möglichkeiten (Vorschläge aus der Community): - -* Ein HoneyPot lenkt Bots ab, ohne Menschen zu nerven: https://wordpress.org/plugins/honeypot/ - -* "Ich hatte mal für **UltimateMember ein winziges Plugin geschrieben mit dem man einfach einen Text eingeben muss um sich zu registrieren**. Barrierearm und hält alle Bots ab: [Download von Github](https://github.com/hansmorb/um-captchaquiz/raw/refs/heads/master/um-captchaquiz.zip). Dazu einfach eine Textbox erstellen und den Metaschlüssel in den Plugin Einstellungen eintragen." - -* "Wir nutzen Hcaptcha für wordpress. Nach der installation in den Einstellungen des Plugins die von euch genutzte Registrierung (z.B. UltraMember auswählen, standardmäßig sollte die WordPress-eigene Registrierung gewählt werden). Für die Nutzung muss ein Hcaptcha Account erstellt werden. Sie werben mit Privacy-First und das keine Nutzerdaten verkauft werden. Ich selber habe das nicht geprüft." — [Download aus dem Plugin-Verzeichnis](https://wordpress.org/plugins/hcaptcha-for-forms-and-more) diff --git a/docs/de/documentation/faq/booking-comment-emails.md b/docs/de/documentation/faq/booking-comment-emails.md deleted file mode 100644 index 6a80d0592..000000000 --- a/docs/de/documentation/faq/booking-comment-emails.md +++ /dev/null @@ -1,5 +0,0 @@ -# Wie bekomme ich den Buchungskommentar auf die Seite und in die Email? - -In den Einstellungen kannst du die Buchungskommentare aktivieren. In den email -Vorlagen musst du dann folgenden Code einfügen: `{{booking:returnComment}}` - diff --git a/docs/de/documentation/faq/index.md b/docs/de/documentation/faq/index.md index 973d42c8c..01cc4b9c5 100644 --- a/docs/de/documentation/faq/index.md +++ b/docs/de/documentation/faq/index.md @@ -1,105 +1,172 @@ -# Häufige Fragen (FAQ) +# Häufige Fragen (FAQ) {#faq} + -### Artikel +Diese Seite ist in zwei Bereiche unterteilt: [allgemeine **FAQ**](#faq) mit häufigen Fragen zur Nutzung, und [**Plugin- und Theme-Inkompatibilitäten**](#plugin-incompatibilities) mit bekannten Problemen durch Drittanbieter-Software. - * [ Wie bekomme ich den Buchungskommentar auf die Seite und in die Email? ](booking-comment-emails) - * [ Wie kann ich die Artikeldetailseite übersichtlicher gestalten? ](organise-article-page) - * [ Die Seite ist sehr langsam ](site-slow) - * [ Wie füge ich Zahlenschloss-Codes in E-Mails ein? ](lock-codes-email) + +## Wie bekomme ich den Buchungskommentar auf die Seite und in die Email? -## Ich möchte einzelne Nutzer vorübergehend sperren +::: details Antwort anzeigen +In den Einstellungen kannst du die Buchungskommentare aktivieren. In den E-Mail-Vorlagen musst du dann folgenden Code einfügen: `{{booking:returnComment}}` +::: + +## Wie kann ich die Artikeldetailseite übersichtlicher gestalten? + +::: details Antwort anzeigen +Lange Artikelseiten bedeuten, dass Menschen lange bis zum Buchungskalender scrollen müssen. + +Hier empfiehlt sich ein Plugin wie [Show-Hide/Collapse-Expand](https://de.wordpress.org/plugins/show-hidecollapse-expand) zu nutzen, mit dem Informationen eingeklappt werden können. + +![](/img/item-collapse.png) +::: + +## Meine Seite ist sehr langsam – was kann ich tun? {#my-site-is-very-slow-what-can-i-do} + +:::: details Antwort anzeigen +Wenn deine CommonsBooking Seite sehr langsam ist, kann das verschiedene Gründe haben. +Wir nutzen eine Technologie namens Caching, mit der wir häufig gestellte Anfragen in einem Zwischenspeicher +zurückhalten, um Serverkapazitäten einzusparen. + +Das Caching kann unter Umständen nicht funktionieren, wenn: + + * [WP_DEBUG](https://developer.wordpress.org/advanced-administration/debug/debug-wordpress/) aktiviert ist, dafür musst du deine wp-config.php bearbeiten + * Der Ordner /tmp/ auf deinem Server nicht beschreibbar ist. Falls dies der Fall sein sollte, dann kontaktiere bitte deinen Webhoster und bitte ihn darum den Ordner beschreibbar zu machen. + * Falls das nicht möglich sein sollte, kannst du in den CommonsBooking Einstellungen unter "Erweiterte Optionen" den Pfad für den Dateisystem-Cache einstellen. Bitte frage bei deinem Webhoster nach, welche Ordner auf dem Server für temporäre Dateien verfügbar sind. + * Falls das auch nicht möglich sein sollte: Gehe zu deiner [Webseiten-Info](https://wordpress.org/documentation/article/site-health-screen/) unter (http://DEINE-URL/wp-admin/site-health.php?tab=debug). + Dort findest du unter **Verzeichnisse** den Pfad deines WordPress-Verzeichnisses. Wähle alternativ einen Ordner im Format `DEIN_VERZEICHNIS/symfony` als Cache-Ziel. + :::danger Achtung + Dies kann dazu führen, dass dein WordPress-Verzeichnis sehr groß wird. + ::: + +Alternativ kannst du auch [REDIS](https://redis.io) auf deinem Server installieren und den Cache durch REDIS verwalten lassen. +Da REDIS den Cache im RAM speichert, statt im Dateisystem, ist das meistens etwas schneller. +:::: -Ihr möchtet Nutzende vorübergehend für einen bestimmten Zeitraum sperren, weil diese z.B. die Ausleihe übermäßig nutzen oder gegen Ausleihbedingungen verstoßen? +## Wie kann ich Konfigurationsprobleme debuggen? -Ich würde euch dazu raten mit einem entsprechenden Plugin zum Blockieren von Nutzenden für einen bestimmten Zeitraum einfach den gesamten Login zu blockieren. Da gibt es auch schon einige Plugins dafür. Wenn eure Nutzenden sowieso nichts anderes auf der Seite machen können als Räder zu buchen erscheint mir das auch sinnvoll es auf diese Art und Weise zu machen. +:::: details Antwort anzeigen +:::warning Technische Expertise nötig! +::: + +Mit dem Plugin `query-monitor` kannst du Anfragen deiner Seite live überwachen. Damit ist es z.B. möglich einen +falsch konfigurierten Cache schnell zu identifizieren. +:::: + +## Wie füge ich Zahlenschloss-Codes in E-Mails ein? -Ich habe nur kurz bei mir mal das Plugin https://wordpress.org/plugins/user-blocker/ ausprobiert und das hat problemlos funktioniert, auch mit einer Zeitschaltfunktion. Das ist aber nicht das einzige Plugin, was diese Funktion hat. +::: details Antwort anzeigen +Eine häufige Frage ist, ob Codes für Zahlenschlösser an Artikeln oder Standorten hinzugefügt werden können, um sie in den versendeten E-Mails anzuzeigen. -Das Blockieren bestimmter Benutzergruppen ist kein Feature in CommonsBooking und wird wahrscheinlich auch so bald nicht als Feature kommen, da es schon einige Plugins gibt die genau das erledigen. +Das funktioniert über sog. Meta-Felder, welche Artikel und Standort zugewiesen werden. Diese Felder können dann auch in den E-Mail-Templates verwendet werden. -## Überbuchbare Tage erlauben / Buchung übers Wochenende +Der Artikel [Template-Tags](../administration/template-tags#eigene-metafelder-fur-standorte-und-artikel-verwenden) der Dokumentation enthält eine ausführliche Anleitung. +::: -Wenn ihr euren Nutzenden ermöglichen wollt, den Artikel z.B. über das Wochenende zu buchen, könnt ihr diese Einstellung in den Standort-Einstellungen vornehmen. +## Wie sperre ich einzelne Nutzer vorübergehend? -Infos dazu unter: https://commonsbooking.org/documentation/first-steps/create-location/ +::: details Antwort anzeigen +Möchtest du Nutzende vorübergehend für einen bestimmten Zeitraum sperren, weil diese z.B. die Ausleihe üdermäßig nutzen oder gegen Ausleihbedingungen verstoßen? -## Anzahl der Artikel in der cb_items Liste erhöhen +Wir empfehlen, ein dediziertes WordPress-Plugin zum Blockieren von Nutzenden zu verwenden. Wenn deine Nutzenden auf der Seite sowieso nichts anderes tun können als buchen, ist das auch die einfachste Lösung. -Die Anzahl der Artikel pro Seite wird über die globalen Einstellungen von Wordpress übernommen. +Das Plugin [User Blocker](https://wordpress.org/plugins/user-blocker/) wurde getestet und funktioniert problemlos, auch mit einer Zeitschaltfunktion. Es gibt weitere Plugins mit dieser Funktionalität. -Diese globale Einstellungen können hier geändert werden: +Das Blockieren bestimmter Benutzergruppen ist kein eingebautes Feature von CommonsBooking und wird wahrscheinlich auch in naher Zukunft nicht kommen, da es bereits WordPress-Plugins gibt, die genau das abdecken. +::: -Als Wordpress Administrator einlogge: +## Wie erlaube ich Buchungen über geschlossene Tage hinweg? -Einstellungen -> Lesen -> Blogseiten zeigen maximal +::: details Antwort anzeigen +Wenn du deinen Nutzenden ermöglichen möchtest, einen Artikel über geschlossene Tage hinweg zu buchen (z.B. über das Wochenende, wenn die Station geschlossen ist), kannst du das in den Standort-Einstellungen konfigurieren. -## Fehlerhafte Anzeige des Kalender-Widget im Admin-Bereich +Ausführliche Informationen findest du unter [Standorte anlegen](../first-steps/create-location). +::: + +## Wie erhöhe ich die Anzahl der Artikel in der cb_items-Liste? + +::: details Antwort anzeigen +Die Anzahl der Artikel pro Seite wird über die globalen WordPress-Leseeinstellungen übernommen. + +So änderst du sie: + +1. Als WordPress-Administrator einloggen +2. **Einstellungen -> Lesen** aufrufen +3. Den Wert bei **Blogseiten zeigen maximal** anpassen +::: -Treten Probleme bei der Anzeige des Kalenders im Admin-Bereich der Buchungen -auf (sog. Admin-Backend), siehe das folgende Bild rechts unten, kann eine -mögliche Lösung sein, das [ Plugin "Lightstart" (wp-maintenance-mode) ](https://wordpress.org/plugins/wp-maintenance-mode) zu deaktivieren oder zu -entfernen und neu zu installieren. Das Problem ist eine Inkompatibilität von -Lightstart mit CommonsBooking und kein Fehler im Code von CommonsBooking. Das -Problem tritt nicht mehr auf, wenn eine Neuinstallation von Lightstart -vorgenommen wurde. Mehr dazu auf [ Github im CommonsBooking Quellcode-Repository ](https://github.com/wielebenwir/commonsbooking/issues/1646). +## Wie verhindere ich Spam-Registrierungen? + +::: details Antwort anzeigen +Dafür gibt es verschiedene Möglichkeiten (Vorschläge aus der Community): + +* Ein HoneyPot lenkt Bots ab, ohne Menschen zu nerven: [Honeypot Plugin](https://wordpress.org/plugins/honeypot/) + +* "Ich hatte mal für **UltimateMember ein winziges Plugin geschrieben mit dem man einfach einen Text eingeben muss um sich zu registrieren**. Barrierearm und hält alle Bots ab: [Download von Github](https://github.com/hansmorb/um-captchaquiz/raw/refs/heads/master/um-captchaquiz.zip). Dazu einfach eine Textbox erstellen und den Metaschlüssel in den Plugin Einstellungen eintragen." + +* "Wir nutzen Hcaptcha für wordpress. Nach der Installation in den Einstellungen des Plugins die von euch genutzte Registrierung (z.B. UltimateMember) auswählen. Für die Nutzung muss ein Hcaptcha Account erstellt werden. Sie werben mit Privacy-First und das keine Nutzerdaten verkauft werden. Ich selber habe das nicht geprüft." - [Download aus dem Plugin-Verzeichnis](https://wordpress.org/plugins/hcaptcha-for-forms-and-more) +::: + +# Plugin- und Theme-Inkompatibilitäten {#plugin-incompatibilities} + +## Plugin: Lightstart + +::: details Antwort anzeigen +Treten Probleme bei der Anzeige des Kalenders im Admin-Bereich der Buchungen auf (sog. Admin-Backend), kann eine mögliche Lösung sein, das [Plugin "Lightstart" (wp-maintenance-mode)](https://wordpress.org/plugins/wp-maintenance-mode) zu deaktivieren oder zu entfernen und neu zu installieren. Das Problem ist eine Inkompatibilität von Lightstart mit CommonsBooking und kein Fehler im Code von CommonsBooking. Das Problem tritt nicht mehr auf, wenn eine Neuinstallation von Lightstart vorgenommen wurde. Mehr dazu auf [Github im CommonsBooking Quellcode-Repository](https://github.com/wielebenwir/commonsbooking/issues/1646). ![](/img/backend-booking-list-bug.png) +::: -## Inkompatibles Plugin All-in-one-Event Plugins +## Theme: GridBulletin + +::: details Antwort anzeigen +In der letzten Version von [GridBulletin](https://wordpress.org/themes/gridbulletin) kommt es zu einer Inkompatibilität mit CommonsBooking. Probleme tauchen auf, wenn der Footer aktiviert ist. Konkrete Probleme sind z.B. das Fehlen des Buchungs-Kalenders auf der Artikelseite. Aus technischer Sicht liegt es daran, dass die nötigen Javascript-Quellen von CommonsBooking nicht ausgeliefert werden. Der Grund innerhalb des GridBulletin Themes oder eine Lösung konnte bisher nicht gefunden werden. +::: +## Plugin: All-in-one-Event + +:::: details Antwort anzeigen :::info Behoben seit 2.7.2 (06.2022) Für Experten siehe [Issue 675](https://github.com/wielebenwir/commonsbooking/issues/675) ::: -Leider kommt es bei der gleichzeitigen Nutzung des Plugins "All-in-one-Event" zu Fehlern, sodass Seiten, die von CommonsBooking erzeugt werden nicht angezeigt werden. +Leider kommt es bei der gleichzeitigen Nutzung des Plugins "All-in-one-Event" zu Fehlern, sodass Seiten, die von CommonsBooking erzeugt werden, nicht angezeigt werden. Die Ursache ist leider auf eine schlechte Programmierung des All-in-one-Event-Plugins zurückzuführen, welches sich nicht an die Wordpress-Standards hält und so tief in Wordpress eingreift, dass es die Programmlogik von CommonsBooking quasi überschreibt. Wir haben einiges versucht, um eine parallele Nutzung zu ermöglichen, leider aber bisher keine Lösung gefunden. Wenn ihr das Problem auch habt, schreibt gerne direkt an den Support des Plugins, vielleicht passen sie ihr Plugin doch noch irgendwann an. +:::: -## Inkompatibles Plugin REDIS Object Cache - -Im Zusammenhang mit dem [Cache](../advanced-functionality/) gab -es in der Vergangenheit bereits Probleme mit anderen Wordpress-Plugins wie -z.B. 'REDIS Object Cache'. Aus diesem Grund raten wir von der Nutzung solcher -Plugins ab. +## Plugin: REDIS Object Cache -Deshalb sollten die von CommonsBooking generierten Seiten von der Optimierung durch Dritt-Plugins ausgenommen werden. -CommonsBooking verwendet ein eigenes Caching. +::: details Antwort anzeigen +Im Zusammenhang mit dem [Cache](../advanced-functionality/) gab es in der Vergangenheit bereits Probleme mit anderen Wordpress-Plugins wie z.B. 'REDIS Object Cache'. Aus diesem Grund raten wir von der Nutzung solcher Plugins ab. +Die von CommonsBooking generierten Seiten sollten von der Optimierung durch Dritt-Plugins ausgenommen werden. CommonsBooking verwendet ein eigenes Caching. +::: -## Inkompatibles Plugin Ultimate Member +## Plugin: Ultimate Member -Wenn ihr das Plugin Ultimate Member benutzt und die Benutzerrolle "CommonsBooking Manager" nutzen möchtet, müsst ihr in Ultimate Member für die Rolle cb_manager noch ein Häkchen setzen, um diese für den AP-Admin-Zugang zu aktivieren. +::: details Antwort anzeigen +Wenn ihr das Plugin Ultimate Member benutzt und die Benutzerrolle "CommonsBooking Manager" nutzen möchtet, müsst ihr in Ultimate Member für die Rolle `cb_manager` noch ein Häkchen setzen, um diese für den Admin-Zugang zu aktivieren. +::: -## Inkompatible Plugins: Autoptimize / Caching +## Plugin: Autoptimize / Caching-Plugins -Optimierungs-Plugins oder weitere Caching-Plugins können dazu führen, dass CommonsBooking nicht alle Seiten anzeigen kann. +:::: details Antwort anzeigen +Optimierungs-Plugins oder weitere Caching-Plugins können dazu führen, dass CommonsBooking nicht alle Seiten korrekt anzeigen kann. -Nicht vollständige Liste: +Betroffene Plugins (nicht vollständig): * Autoptimize -Deshalb sollten die von CommonsBooking generierten Seiten von der Optimierung durch Dritt-Plugins ausgenommen werden. -CommonsBooking verwendet ein eigenes Caching. +Die von CommonsBooking generierten Seiten sollten von der Optimierung durch Dritt-Plugins ausgenommen werden. CommonsBooking verwendet ein eigenes Caching. -::: info Hast du ein Problem festgestellt? +:::info Hast du ein Problem festgestellt? Dann trage die Inkompatibilität hier ein! ::: - - -## Inkompatibles Theme Gridbulletin - -In der letzten Version von [ GridBulletin ](https://wordpress.org/themes/gridbulletin) kommt es zu einer -Inkompatibilität mit CommonsBooking. Probleme tauchen auf, wenn der Footer -aktiviert ist. Konkrete Probleme sind z.B. das Fehlen des Buchungs-Kalenders -auf der Artikelseite. Aus technischer Sicht liegt es daran, dass die nötigen -Javascript-Quellen von CommonsBooking nicht ausgeliefert werden. Der Grund -innerhalb des GridBulletin Themes oder eine Lösung konnte bisher nicht -gefunden werden. - - - +:::: diff --git a/docs/de/documentation/faq/lock-codes-email.md b/docs/de/documentation/faq/lock-codes-email.md deleted file mode 100644 index 1d000040d..000000000 --- a/docs/de/documentation/faq/lock-codes-email.md +++ /dev/null @@ -1,11 +0,0 @@ -# Wie füge ich Zahlenschloss-Codes in E-Mails ein? - -Eine häufige Frage ist, ob Codes für Zahlenschlösser an Artikeln oder -Standorten hinzugefügt werden können um Sie in den versendeten E-Mails -anzuzeigen. - -Das funktioniert über sog. Meta-Felder, welche Artikel und Standort zugewiesen -werden. Diese Felder können dann auch in den E-Mail Templates verwendet -werden. - -Der Aritkel [Template-Tags](../administration/template-tags#eigene-metafelder-fur-standorte-und-artikel-verwenden) der Dokumentation enthält eine Ausführliche Anleitung. diff --git a/docs/de/documentation/faq/organise-article-page.md b/docs/de/documentation/faq/organise-article-page.md deleted file mode 100644 index cbce05f12..000000000 --- a/docs/de/documentation/faq/organise-article-page.md +++ /dev/null @@ -1,13 +0,0 @@ -# Wie kann ich die Artikeldetailseite übersichtlicher gestalten? - -## Informationen erst auf Klick anzeigen - -Lange Artikelseiten bedeuten, dass Menschen lange bis zum Buchungskalender -scrollen müssen. - -Hier empfiehlt sich ein Plugin wie [ Show-Hide/Collapse-Expand -](https://de.wordpress.org/plugins/show-hidecollapse-expand) zu nutzen, mit -dem Informationen eingeklappt werden können. - -![](/img/item-collapse.png) - diff --git a/docs/de/documentation/faq/problems-and-answers.md b/docs/de/documentation/faq/problems-and-answers.md deleted file mode 100644 index aebab7705..000000000 --- a/docs/de/documentation/faq/problems-and-answers.md +++ /dev/null @@ -1,27 +0,0 @@ -# Probleme und Antworten - -### Anzeige Kalender-Widget im Admin-Bereich - -Treten Probleme bei der Anzeige des Kalenders im Admin-Bereich der Buchungen -auf (sog. Admin-Backend), siehe das folgende Bild rechts unten, kann eine -mögliche Lösung sein, das [ Plugin "Lightstart" (wp-maintenance-mode) -](https://wordpress.org/plugins/wp-maintenance-mode) zu deaktivieren oder zu -entfernen und neu zu installieren. Das Problem ist eine Inkompatibilität von -Lightstart mit CommonsBooking und kein Fehler im Code von CommonsBooking. Das -Problem tritt nicht mehr auf, wenn eine Neuinstallation von Lightstart -vorgenommen wurde. Mehr dazu auf [ Github im CommonsBooking Quellcode- -Repository ](https://github.com/wielebenwir/commonsbooking/issues/1646) . - -![](/img/backend-booking-list-bug.png) - -### Inkompatibles Theme Gridbulletin - -In der letzten Version von [ GridBulletin -](https://wordpress.org/themes/gridbulletin) kommt es zu einer -Inkompatibilität mit CommonsBooking. Probleme tauchen auf, wenn der Footer -aktiviert ist. Konkrete Probleme sind z.B. das Fehlen des Buchungs-Kalenders -auf der Artikelseite. Aus technischer Sicht liegt es daran, dass die nötigen -Javascript-Quellen von CommonsBooking nicht ausgeliefert werden. Der Grund -innerhalb des GridBulletin Themes oder eine Lösung konnte bisher nicht -gefunden werden. - diff --git a/docs/de/documentation/faq/site-slow.md b/docs/de/documentation/faq/site-slow.md deleted file mode 100644 index 582b2f284..000000000 --- a/docs/de/documentation/faq/site-slow.md +++ /dev/null @@ -1,28 +0,0 @@ -# Die Seite ist sehr langsam - -Wenn deine CommonsBooking Seite sehr langsam ist, kann das verschiedene Gründe haben. -Wir nutzen eine Technologie namens Caching, mit der wir häufig gestellte Anfragen in einem Zwischenspeicher -zurückhalten, um Serverkapazitäten einzusparen. - -Das Caching kann unter Umständen nicht funktionieren, wenn: - - * [WP_DEBUG](https://developer.wordpress.org/advanced-administration/debug/debug-wordpress/) aktiviert ist, dafür musst du deine wp-config.php bearbeiten - * Der Ordner /tmp/ auf deinem Server nicht beschreibbar ist. Falls dies der Fall sein sollte, dann kontaktiere bitte deinen Webhoster und bitte ihn darum den Ordner beschreibbar zu machen. - * Falls das nicht möglich sein sollte, kannst du in den CommonsBooking Einstellungen unter “Erweiterte Optionen” den Pfad für den Dateisystem-Cache einstellen. Bitte frage bei deinem Webhoster nach, welche Ordner auf dem Server für temporäre Dateien verfügbar sind. - * Falls das auch nicht möglich sein sollte: Gehe zu deiner [Webseiten-Info](https://wordpress.org/documentation/article/site-health-screen/) unter (http://DEINE-URL/wp-admin/site-health.php?tab=debug). - Dort findest du unter **Verzeichnisse** den Pfad deines WordPress-Verzeichnisses. Wähle alternativ einen Ordner im Format `DEIN_VERZEICHNIS/symfony` als Cache-Ziel. - ::: danger Achtung - Dies kann dazu führen, dass dein WordPress-Verzeichnis sehr groß wird. - ::: - -Alternativ kannst du auch [REDIS](https://redis.io) auf deinem Server installieren und den Cache durch REDIS verwalten lassen. -Da REDIS den Cache im RAM speichert, statt im Dateisystem, ist das meistens etwas schneller. - -## Troubleshooting - -::: warning Technische Expertise nötig! -::: - -Mit dem Plugin `query-monitor` kannst du Anfragen deiner Seite live überwachen. Damit es es z.B. möglich ein -falsch konfigurierten Cache schnell zu identifizieren. - diff --git a/docs/de/documentation/setup/migration-from-cb1.md b/docs/de/documentation/setup/migration-from-cb1.md index b35fc55b9..6610466cc 100644 --- a/docs/de/documentation/setup/migration-from-cb1.md +++ b/docs/de/documentation/setup/migration-from-cb1.md @@ -1,5 +1,9 @@ # Migration von Version 0.9.x +:::danger ACHTUNG +Mit CommonsBooking 2.12 (Veröffentlichung Anfang 2027) wird die Migration von CommonsBooking 0.9.X auf die neuste Version eingestellt. +Alle Nutzenden von CommonsBooking 0.9.X sind dazu angehalten **zeitnah zu migrieren**, bei Fragen unterstützen wir gerne und helfen euch auch gerne bei der Migration. +::: Die Migration von Version CB 0.9.x zu CB 2.x.x könnt ihr per Knopfdruck erledigen. Die Migration importiert folgende Daten: diff --git a/docs/en/documentation/advanced-functionality/cache.md b/docs/en/documentation/advanced-functionality/cache.md index 03fa863d0..a6dc8563d 100644 --- a/docs/en/documentation/advanced-functionality/cache.md +++ b/docs/en/documentation/advanced-functionality/cache.md @@ -36,7 +36,7 @@ A misconfigured cache can slow down your site! working properly. In that case, try a different path for the cache or a different cache adapter. * If your site is very slow, this may also indicate a problem with the cache. - More about this: [The site is very slow](../faq/site-slow). + More about this: [The site is very slow](../faq/index#my-site-is-very-slow-what-can-i-do). * **Periodical cache warmup through cron job**: :::warning diff --git a/docs/en/documentation/advanced-functionality/hooks-and-filters.md b/docs/en/documentation/advanced-functionality/hooks-and-filters.md index fa83c1623..3a44386da 100644 --- a/docs/en/documentation/advanced-functionality/hooks-and-filters.md +++ b/docs/en/documentation/advanced-functionality/hooks-and-filters.md @@ -43,6 +43,10 @@ add_action( 'commonsbooking_before_item-single', 'itemsingle_callback' ); * commonsbooking_before_item-single * commonsbooking_after_item-single * commonsbooking_mail_sent + * [commonsbooking_booking_created](#booking-lifecycle-hooks-since-2-11-0) + * [commonsbooking_booking_confirmed](#booking-lifecycle-hooks-since-2-11-0) + * [commonsbooking_booking_cancelled](#booking-lifecycle-hooks-since-2-11-0) + * [commonsbooking_booking_status_changed](#booking-lifecycle-hooks-since-2-11-0) ### Hooks in the context of an object (since 2.10.8) @@ -68,6 +72,75 @@ function my_cb_before_booking_single( $booking_id, $booking ) { add_action( 'commonsbooking_before_booking-single', 'my_cb_before_booking_single', 10, 2 ); ``` +### Booking lifecycle hooks (since 2.11.0) + +These action hooks let you react to the lifecycle of a booking — for example to +program a smart lock, sync an external calendar, or write an audit log. They fire +regardless of where the change originated (frontend calendar, admin backend, REST +API or WP-CLI), because they are dispatched from WordPress' post status +transition rather than from a single form handler. + +Every hook receives the booking post ID and the corresponding +`\CommonsBooking\Model\Booking` instance (except `..._status_changed`, which also +passes the old and new status). + + * `commonsbooking_booking_created` + * Fires **once** when a booking first enters a real status. + * Parameters: `int $booking_id`, `\CommonsBooking\Model\Booking $booking` + * `commonsbooking_booking_confirmed` + * Fires whenever a booking becomes `confirmed`. This is the recommended hook for integrations that need to react to a booking becoming active. + * Parameters: `int $booking_id`, `\CommonsBooking\Model\Booking $booking` + * `commonsbooking_booking_cancelled` + * Fires when a booking is cancelled. + * Parameters: `int $booking_id`, `\CommonsBooking\Model\Booking $booking` + * `commonsbooking_booking_status_changed` + * Fires on every booking status transition (audit superset of the hooks above). Note: because cancellation writes the status directly to the database, cancellations are best observed via `commonsbooking_booking_cancelled` rather than this hook. + * Parameters: `int $booking_id`, `string $old_status`, `string $new_status`, `\CommonsBooking\Model\Booking $booking` + +#### Example: trigger a smart lock when a booking is confirmed + +```php +add_action( 'commonsbooking_booking_confirmed', function ( $booking_id, $booking ) { + $item = $booking->getItem(); + $lockCode = $booking->formattedBookingCode(); + // hand the code / booking window over to your locking system here … + my_locking_system_program( $item->ID, $lockCode, $booking->getStartDate(), $booking->getEndDate() ); +}, 10, 2 ); +``` + +#### Example: release the lock again when a booking is cancelled + +```php +add_action( 'commonsbooking_booking_cancelled', function ( $booking_id, $booking ) { + my_locking_system_revoke( $booking->getItem()->ID, $booking_id ); +}, 10, 2 ); +``` + +#### No-code automations via Uncanny Automator + +If the [Uncanny Automator](https://wordpress.org/plugins/uncanny-automator/) plugin +is active, CommonsBooking registers an integration with two triggers: + + * **A booking is confirmed** (built on `commonsbooking_booking_confirmed`) + * **A booking is cancelled** (built on `commonsbooking_booking_cancelled`) + +Both expose the same tokens — booking ID, item, location, start/end, booking +code, user email, **user phone** and booking URL — so you can route booking +events to any gateway Automator supports (SMS via Twilio, Slack, Telegram, +Google Calendar, Mailchimp, a webhook, …) without writing code. The integration +loads only when Automator is active and adds no overhead otherwise. + +**Example — SMS an access code when a booking is confirmed:** +*Trigger:* CommonsBooking → *A booking is confirmed*. +*Action:* Twilio → Send SMS, with **To** = `{{CB_BOOKING_USER_PHONE}}` and +**Body** = "Your booking of {{CB_ITEM_NAME}} at {{CB_LOCATION_NAME}} is confirmed. +Access code: {{CB_BOOKING_CODE}}". + +**Example — alert the team on Slack/SMS when a booking is cancelled:** +*Trigger:* CommonsBooking → *A booking is cancelled*. +*Action:* Slack (or Twilio SMS to the team number) — "{{CB_ITEM_NAME}} at +{{CB_LOCATION_NAME}} just freed up for {{CB_BOOKING_START}}–{{CB_BOOKING_END}}". + ## Filter hooks Filter hooks (https://developer.wordpress.org/plugins/hooks/filters) work @@ -77,7 +150,7 @@ receives a value, modifies it, and then returns it. ### Overview of all filter hooks * commonsbooking_custom_metadata - * [commonsbooking_isCurrentUserAdmin](../basics/permission-management#filterhook-isCurrentUserAdmin) + * [commonsbooking_isUserAdmin](../basics/permission-management#filterhook-isUserAdmin) * commonsbooking_isCurrentUserSubscriber * commonsbooking_get_template_part * commonsbooking_template_tag @@ -88,6 +161,15 @@ receives a value, modifies it, and then returns it. * commonsbooking_mail_body * commonsbooking_mail_attachment * commonsbooking_disableCache + * commonsbooking_gbfs_feeds + * commonsbooking_can_cancel_booking + * commonsbooking_booking_before_save + * commonsbooking_is_timeframe_bookable + * commonsbooking_day_availability + * commonsbooking_bookable_timeframes + * commonsbooking_calendar_data + * commonsbooking_api_item_response + * commonsbooking_api_availability_response There are also filter hooks that allow you to add additional user roles akin to the CB Manager that can manage items and locations. @@ -156,6 +238,133 @@ add_filter('commonsbooking_tag_cb_item_yourFunction', function( $value, $obj) { }, 10, 2); ``` +### Filter `commonsbooking_api_availability_response` + +::: tip Since version 2.11.0 +::: + +Adjusts the availability slots exposed through the CommonsAPI, e.g. to reflect an +external booking source. Receives the slots array and the item ID (or `null` for +all items). + +```php +add_filter('commonsbooking_api_availability_response', function (array $slots, $id): array { + // adjust $slots as needed + return $slots; +}, 10, 2); +``` + +### Filter `commonsbooking_api_item_response` + +::: tip Since version 2.11.0 +::: + +Adds or adjusts fields exposed for an item in the CommonsAPI. Receives the +prepared item object and the source `WP_Post`. Added fields must conform to the +CommonsAPI JSON schema, otherwise response validation will reject them. + +```php +add_filter('commonsbooking_api_item_response', function ($preparedItem, $item) { + $preparedItem->myField = get_post_meta($item->ID, 'my_field', true); + return $preparedItem; +}, 10, 2); +``` + +### Filter `commonsbooking_calendar_data` + +::: tip Since version 2.11.0 +::: + +Adjusts the data array that drives the frontend booking calendar (and its AJAX +endpoint) before it is rendered or returned as JSON. Receives the calendar data +and the item and location it is for. + +```php +add_filter('commonsbooking_calendar_data', function (array $calendarData, $item, $location): array { + // adjust $calendarData as needed + return $calendarData; +}, 10, 3); +``` + +### Filter `commonsbooking_bookable_timeframes` + +::: tip Since version 2.11.0 +::: + +Restricts or extends which timeframes are offered for booking. Receives the +timeframes and the location/item IDs the query was scoped to. The element type +follows the repository's `$returnAsModel` argument (post IDs, `WP_Post` or +`\CommonsBooking\Model\Timeframe`). + +```php +add_filter('commonsbooking_bookable_timeframes', function (array $timeframes, array $locations, array $items): array { + // filter $timeframes as needed + return $timeframes; +}, 10, 3); +``` + +### Filter `commonsbooking_day_availability` + +::: tip Since version 2.11.0 +::: + +Adjusts the bookable slots computed for a single day, e.g. to reflect an external +availability source. Receives the slots array and the `\CommonsBooking\Model\Day`. + +```php +add_filter('commonsbooking_day_availability', function (array $slots, $day): array { + // inspect $day->getDate() and filter $slots as needed + return $slots; +}, 10, 2); +``` + +### Filter `commonsbooking_is_timeframe_bookable` + +::: tip Since version 2.11.0 +::: + +Adds custom booking-window rules on top of the default advance-booking-days check. +Receives the default decision (`bool`) and the `\CommonsBooking\Model\Timeframe`. + +```php +// Block bookings on the timeframe's item while it is flagged for maintenance. +add_filter('commonsbooking_is_timeframe_bookable', function (bool $bookable, $timeframe): bool { + return $bookable && ! get_post_meta($timeframe->getItem()->ID, 'in_maintenance', true); +}, 10, 2); +``` + +### Filter `commonsbooking_booking_before_save` + +::: tip Since version 2.11.0 +::: + +Adjusts the post array right before a booking is inserted or updated, e.g. to add +meta data. Receives the `wp_insert_post()`/`wp_update_post()` array and the +existing `\CommonsBooking\Model\Booking` (or `null` for a new booking); return the +modified array. + +```php +add_filter('commonsbooking_booking_before_save', function (array $postarr, $booking): array { + $postarr['meta_input']['my_external_ref'] = 'ext-123'; + return $postarr; +}, 10, 2); +``` + +### Filter `commonsbooking_can_cancel_booking` + +::: tip Since version 2.11.0 +::: + +Overrides whether the current user may cancel a booking. Receives the default +decision (`bool`) and the `\CommonsBooking\Model\Booking` instance. + +```php +// Forbid cancelling a booking that starts within the next 24 hours. +add_filter('commonsbooking_can_cancel_booking', function (bool $canCancel, $booking): bool { + return $canCancel && $booking->getStartDate() > time() + DAY_IN_SECONDS; +}, 10, 2); +``` + ### Filter `commonsbooking_mobile_calendar_month_count` ::: tip Since version 2.10.5 diff --git a/docs/en/documentation/api/commonsbooking-api.md b/docs/en/documentation/api/commonsbooking-api.md index 5eb756105..51420d144 100644 --- a/docs/en/documentation/api/commonsbooking-api.md +++ b/docs/en/documentation/api/commonsbooking-api.md @@ -26,6 +26,10 @@ You can access the API via _Settings_ -> _CommonsBooking_ -> Tab: _API / Export_ * Push URL: Here you can enter the URL of the receiving system. CommonsBooking will call this URL whenever the data changes, so that the remote system is informed about a data change and can retrieve it via a separate API call. This enables real-time data exchange. * API Key: Here you can enter a custom API key. If the key is set, the requesting system must provide the parameter apikey= _[key]_ in every query. +## Excluding items from the API + +If you do not want certain items to appear in the API, they must be explicitly excluded. These settings can be configured at item level; if the relevant checkbox is ticked, these items will not appear in the API routes. + ## API route specification **Availability** diff --git a/docs/en/documentation/api/gbfs.md b/docs/en/documentation/api/gbfs.md index e34ae5aea..22ce62af2 100644 --- a/docs/en/documentation/api/gbfs.md +++ b/docs/en/documentation/api/gbfs.md @@ -1,17 +1,21 @@ # GBFS -::: danger API bug -A bug in the API is documented in [Issue 1388](https://github.com/wielebenwir/commonsbooking/issues/1388). The temporal availability may therefore not be correct. -::: - -Since 2.5 - This interface provides data of [locations](../first-steps/create-location), [items](../first-steps/create-item) and their availability via [timeframes](../first-steps/booking-timeframes-manage) in a standardized schema. -The following endpoints of the _General Bikeshare Feed Specification_ ([GBFS](https://www.gbfs.org/documentation/)) schema are supported: +Currently, version 3.1-RC3 of the _General Bikeshare Feed Specification_ ([GBFS](https://www.gbfs.org/documentation/)) is supported with the following routes: -* station_status -* station_information -* system_information +* station_status.json +* station_information.json +* system_information.json +* vehicle_status.json +* vehicle_availability.json +* vehicle_types.json * gbfs.json (Discovery) + +## Regarding vehicle_types.json + +Since CommonsBooking is primarily used to rent out cargo bikes, the API returns `cargo_bicycle` as the default value for `form_factor`. As `propulsion_type` is a required field, the default value here is set to `human`. If this were not the case, much more additional information about the propulsion method would need to be provided. + +::: info Fixed Issue +Since June 2026 (version 2.11) the [issue regarding a wrong display of availability](https://github.com/wielebenwir/commonsbooking/issues/1388) is fixed. diff --git a/docs/en/documentation/basics/permission-management.md b/docs/en/documentation/basics/permission-management.md index b45b586d8..888a95ad5 100644 --- a/docs/en/documentation/basics/permission-management.md +++ b/docs/en/documentation/basics/permission-management.md @@ -47,7 +47,7 @@ In addition, CommonsBooking Managers have no special permissions regarding other * Change the site design * etc. -## Make a CommonsBooking Manager a manager for all items / locations {#filterhook-isCurrentUserAdmin} +## Make a CommonsBooking Manager a manager for all items / locations {#filterhook-isUserAdmin} With a [filter hook](../advanced-functionality/hooks-and-filters) you can set a specific role so that it automatically becomes a manager for all items / locations. The example below does this for the role 'cb_manager', i.e. it configures the CB Manager to be automatically assigned to all items and locations in the instance. @@ -55,7 +55,7 @@ If this should happen with a different role, that role must also be added to the ```php add_filter( - 'commonsbooking_isCurrentUserAdmin', + 'commonsbooking_isUserAdmin', function ( bool $isAdmin, WP_User $user ) { return in_array( 'cb_manager', $user->roles, true ) ? true : $isAdmin; }, diff --git a/docs/en/documentation/faq/avoid-spam-registrations.md b/docs/en/documentation/faq/avoid-spam-registrations.md deleted file mode 100644 index 8c0efe268..000000000 --- a/docs/en/documentation/faq/avoid-spam-registrations.md +++ /dev/null @@ -1,9 +0,0 @@ -# How do I prevent spam registrations - -There are several ways to do this (suggestions from the community): - -* A honeypot diverts bots without bothering people: https://wordpress.org/plugins/honeypot/ - -* "I once wrote a tiny plugin for **UltimateMember where you simply have to enter a text to register**. Accessible and it keeps all bots out: [Download from GitHub](https://github.com/hansmorb/um-captchaquiz/raw/refs/heads/master/um-captchaquiz.zip). Just create a text box and enter the meta key in the plugin settings." - -* "We use hCaptcha for WordPress. After installation, select the registration you use in the plugin settings (e.g., UltimateMember; by default the built-in WordPress registration should be selected). To use it, you need to create an hCaptcha account. They advertise Privacy-First and that no user data is sold. I have not checked this myself." — [Download from the plugin directory](https://wordpress.org/plugins/hcaptcha-for-forms-and-more) diff --git a/docs/en/documentation/faq/booking-comment-emails.md b/docs/en/documentation/faq/booking-comment-emails.md deleted file mode 100644 index 4598c49f1..000000000 --- a/docs/en/documentation/faq/booking-comment-emails.md +++ /dev/null @@ -1,3 +0,0 @@ -# How do I show the booking comment on the page and in the email? - -In the settings you can enable booking comments. In the email templates you then need to insert the following code:
`{{booking:returnComment}}`
diff --git a/docs/en/documentation/faq/index.md b/docs/en/documentation/faq/index.md index 958700318..04d4f5557 100644 --- a/docs/en/documentation/faq/index.md +++ b/docs/en/documentation/faq/index.md @@ -1,46 +1,121 @@ -# Frequently Asked Questions (FAQ) +# Frequently Asked Questions (FAQ) {#faq} -### Pages + - * [How do I show the booking comment on the page and in the email?](booking-comment-emails) - * [How can I make the item detail page clearer?](organise-article-page) - * [The site is very slow](site-slow) - * [How do I show lock codes in emails?](lock-codes-email) +This page is split into two sections: [general **FAQ**](#faq) with common how-to questions, and [**Plugin & Theme Incompatibilities**](#plugin-incompatibilities) listing known issues with third-party software. -## I want to temporarily block individual users + +## How do I show the booking comment on the page and in the email? + +::: details Expand for answer +In the settings you can enable booking comments. In the email templates you then need to insert the following code:
`{{booking:returnComment}}`
+::: + +## How can I make the item detail page clearer? + +::: details Expand for answer +Long item pages mean users have to scroll a long way to reach the booking calendar. + +A plugin such as [Show-Hide/Collapse-Expand](https://de.wordpress.org/plugins/show-hidecollapse-expand) can be used to collapse information. + +![](/img/item-collapse.png) +::: + +## My site is very slow, what can I do? + +::: details Expand for answer +If your CommonsBooking site is very slow, there may be several underlying causes. +We use a technology called caching, which keeps frequently requested data +in temporary storage to save server resources. +Caching may not work under certain conditions, for example when: + + * [WP_DEBUG](https://wordpress.org/documentation/article/debugging-in-wordpress) is enabled; in that case you need to edit your wp-config.php + * The /tmp/ folder on your server is not writable. If that is the case, contact your web host and ask them to make the folder writable. + * If that is not possible, you can set the path for the filesystem cache in the CommonsBooking settings under "Advanced Options". Ask your web host which folders on the server are available for temporary files. + * If that is also not possible: Go to your Site Health screen at (http://YOUR-URL/wp-admin/site-health.php?tab=debug). There you will find the path to your WordPress directory under **Directories**. Alternatively, choose a folder in the format `YOUR_DIRECTORY/symfony` as the cache target. **Warning:** This can cause your WordPress directory to grow very large. + +Alternatively, you can install [Redis](https://redis.io) on your server and let Redis manage the cache. Since Redis stores the cache in RAM instead of the filesystem, it is usually a bit faster. +::: + +## How do I show lock codes in emails? + +::: details Expand for answer +A frequent question is whether lock codes for combination locks can be added to items or locations so that they are displayed in the sent emails. + +This is possible via so-called meta fields, which are assigned to items and locations. These fields can then also be used in email templates. + +[This page of the documentation contains a detailed guide.](../administration/template-tags) +::: + +## How do I temporarily block individual users? + +::: details Expand for answer Do you want to temporarily block users for a certain period of time, e.g. because they use the rental service excessively or violate the rental conditions? -I would advise you to simply block the entire login with an appropriate plugin for blocking users for a certain period of time. There are already some plugins for this. If your users can't do anything else on the site anyway other than booking bikes, it seems to make sense to do it this way. +The recommended approach is to use a dedicated WordPress plugin for blocking users. If your users cannot do anything on the site other than making bookings, this is also the simplest solution. -I just briefly tried the plugin https://wordpress.org/plugins/user-blocker/ and it worked without any problems, even with a timer function. But this is not the only plugin that has this function. +The plugin [User Blocker](https://wordpress.org/plugins/user-blocker/) has been tested and works without issues, including a timer function. There are other plugins available with the same functionality. -Blocking specific user groups is not a feature in CommonsBooking and probably won't be added as a feature anytime soon, as there are already some plugins that do exactly that. +Blocking specific user groups is not a built-in feature of CommonsBooking and is unlikely to be added soon, as existing WordPress plugins already cover this use case well. +::: -## Allow overbookable days / booking over the weekend +## How do I allow booking across closed days? -If you want to allow your users to book the item over the weekend, for example, you can make this setting in the location settings. +::: details Expand for answer +If you want to allow your users to book an item across closed days (e.g. over a weekend when the station is closed), you can configure this in the location settings. -More information at: https://commonsbooking.org/documentation/first-steps/create-location/ +For detailed instructions, see [Create Locations](../first-steps/create-location). +::: -## Increase the number of items in the cb_items list +## How do I increase the number of items shown in the cb_items list? -The number of items per page is taken from the global WordPress settings. +::: details Expand for answer +The number of items shown per page is taken from the global WordPress reading settings. -These global settings can be changed here: +To change it: -Log in as WordPress administrator: +1. Log in as a WordPress administrator +2. Go to **Settings -> Reading** +3. Change the value for **Blog pages show at most** +::: -Settings -> Reading -> Blog pages show at most +## How do I prevent spam registrations? -## Incorrect display of the calendar widget in the admin area +::: details Expand for answer +There are several ways to do this (suggestions from the community): + +* A honeypot diverts bots without bothering people: [Honeypot plugin](https://wordpress.org/plugins/honeypot/) + +* "I once wrote a tiny plugin for **UltimateMember where you simply have to enter a text to register**. Accessible and it keeps all bots out: [Download from GitHub](https://github.com/hansmorb/um-captchaquiz/raw/refs/heads/master/um-captchaquiz.zip). Just create a text box and enter the meta key in the plugin settings." + +* "We use hCaptcha for WordPress. After installation, select the registration you use in the plugin settings (e.g., UltimateMember; by default the built-in WordPress registration should be selected). To use it, you need to create an hCaptcha account. They advertise Privacy-First and that no user data is sold. I have not checked this myself." - [Download from the plugin directory](https://wordpress.org/plugins/hcaptcha-for-forms-and-more) +::: -If there are problems displaying the calendar in the admin area of the bookings (so-called admin backend), see the following picture on the bottom right, a possible solution can be to deactivate or remove and reinstall the [ "Lightstart" (wp-maintenance-mode) plugin ](https://wordpress.org/plugins/wp-maintenance-mode). The problem is an incompatibility of Lightstart with CommonsBooking and not an error in the CommonsBooking code. The problem no longer occurs if Lightstart has been reinstalled. Read more about this on [ Github in the CommonsBooking source code repository ](https://github.com/wielebenwir/commonsbooking/issues/1646). +# Plugin & Theme Incompatibilities {#plugin-incompatibilities} + +## Plugin: Lightstart + +::: details Expand for answer +If there are problems displaying the calendar in the booking admin area (the admin backend), see the image below on the right, one possible solution is to disable or remove and reinstall the ["Lightstart" (wp-maintenance-mode) plugin](https://wordpress.org/plugins/wp-maintenance-mode). +The issue is an incompatibility between Lightstart and CommonsBooking and not a bug in CommonsBooking's code. +The problem does not occur after reinstalling Lightstart. More details on [GitHub in the CommonsBooking source repository](https://github.com/wielebenwir/commonsbooking/issues/1646). ![](/img/backend-booking-list-bug.png) +::: + +## Theme: GridBulletin -## Incompatible plugin All-in-one-Event Plugins +::: details Expand for answer +The latest version of [GridBulletin](https://wordpress.org/themes/gridbulletin) is incompatible with CommonsBooking. +Problems occur when the footer is enabled. One concrete issue is the missing booking calendar on the item page. From a technical perspective, the required JavaScript sources from CommonsBooking are not being loaded. The root cause within the GridBulletin theme or a solution has not yet been found. +::: + +## Plugin: All-in-one-Event +:::: details Expand for answer :::info Fixed since 2.7.2 (06.2022) For experts see [Issue 675](https://github.com/wielebenwir/commonsbooking/issues/675) ::: @@ -52,28 +127,35 @@ The cause is unfortunately due to poor programming of the All-in-one-Event plugi We have tried a few things to enable parallel use, but unfortunately have not found a solution yet. If you also have the problem, please write directly to the plugin's support, maybe they will adapt their plugin at some point. +:::: -## Incompatible plugin REDIS Object Cache +## Plugin: REDIS Object Cache +::: details Expand for answer In connection with the [Cache](../advanced-functionality/), there have been problems with other WordPress plugins such as 'REDIS Object Cache' in the past. For this reason, we advise against using such plugins. -Therefore, the pages generated by CommonsBooking should be excluded from optimization by third-party plugins. +The pages generated by CommonsBooking should be excluded from optimization by third-party plugins. CommonsBooking uses its own caching. +::: -## Incompatible plugin Ultimate Member +## Plugin: Ultimate Member -If you use the Ultimate Member plugin and want to use the "CommonsBooking Manager" user role, you have to check a box in Ultimate Member for the cb_manager role to activate it for AP admin access. +::: details Expand for answer +If you use the Ultimate Member plugin and want to use the "CommonsBooking Manager" user role, you have to check a box in Ultimate Member for the `cb_manager` role to activate it for admin access. +::: -## Incompatible plugins: Autoptimize / Caching +## Plugin: Autoptimize / Caching plugins -Optimization plugins or other caching plugins can cause CommonsBooking to not be able to display all pages. +:::: details Expand for answer +Optimization plugins or other caching plugins can cause CommonsBooking to not display all pages correctly. -Incomplete list: +Affected plugins include (incomplete list): * Autoptimize -Therefore, the pages generated by CommonsBooking should be excluded from optimization by third-party plugins. +The pages generated by CommonsBooking should be excluded from optimization by third-party plugins. CommonsBooking uses its own caching. -::: info Have you noticed a problem? -Then enter the incompatibility here! +:::info Have you noticed a problem? +Add incompatible plugins or themes here! ::: +:::: diff --git a/docs/en/documentation/faq/lock-codes-email.md b/docs/en/documentation/faq/lock-codes-email.md deleted file mode 100644 index c3ddad0b3..000000000 --- a/docs/en/documentation/faq/lock-codes-email.md +++ /dev/null @@ -1,8 +0,0 @@ -# How do I show lock codes in emails? - -A frequent question is whether lock codes for combination locks can be added to items or locations so that they are displayed in the sent emails. - -This is possible via so-called meta fields, which are assigned to items and locations. These fields can then also be used in email templates. -werden. - -[This page of the documentation contains a detailed guide.](../administration/template-tags) diff --git a/docs/en/documentation/faq/organise-article-page.md b/docs/en/documentation/faq/organise-article-page.md deleted file mode 100644 index a5794ecca..000000000 --- a/docs/en/documentation/faq/organise-article-page.md +++ /dev/null @@ -1,9 +0,0 @@ -# How can I make the item detail page clearer? - -## Information only shown on click - -Long item pages mean users have to scroll a long way to reach the booking calendar. - -A plugin such as [Show-Hide/Collapse-Expand](https://de.wordpress.org/plugins/show-hidecollapse-expand) can be used to collapse information. - -![](/img/item-collapse.png) diff --git a/docs/en/documentation/faq/problems-and-answers.md b/docs/en/documentation/faq/problems-and-answers.md deleted file mode 100644 index 3a8582ec6..000000000 --- a/docs/en/documentation/faq/problems-and-answers.md +++ /dev/null @@ -1,14 +0,0 @@ -# Problems and answers - -### Calendar widget display in the admin area - -If there are problems displaying the calendar in the booking admin area (the admin backend), see the image below on the right, one possible solution is to disable or remove and reinstall the ["Lightstart" (wp-maintenance-mode) plugin](https://wordpress.org/plugins/wp-maintenance-mode). -The issue is an incompatibility between Lightstart and CommonsBooking and not a bug in CommonsBooking's code. -The problem does not occur after reinstalling Lightstart. More details on [GitHub in the CommonsBooking source repository](https://github.com/wielebenwir/commonsbooking/issues/1646). - -![](/img/backend-booking-list-bug.png) - -### Incompatible theme: GridBulletin - -The latest version of [GridBulletin](https://wordpress.org/themes/gridbulletin) is incompatible with CommonsBooking. -Problems occur when the footer is enabled. One concrete issue is the missing booking calendar on the item page. From a technical perspective, the required JavaScript sources from CommonsBooking are not being loaded. The root cause within the GridBulletin theme or a solution has not yet been found. diff --git a/docs/en/documentation/faq/site-slow.md b/docs/en/documentation/faq/site-slow.md deleted file mode 100644 index a2c19002b..000000000 --- a/docs/en/documentation/faq/site-slow.md +++ /dev/null @@ -1,13 +0,0 @@ -# The site is very slow - -If your CommonsBooking site is very slow, there may be several underlying causes. -We use a technology called caching, which keeps frequently requested data -in temporary storage to save server resources. -Caching may not work under certain conditions, for example when: - - * [WP_DEBUG](https://wordpress.org/documentation/article/debugging-in-wordpress) is enabled; in that case you need to edit your wp-config.php - * The /tmp/ folder on your server is not writable. If that is the case, contact your web host and ask them to make the folder writable. - * If that is not possible, you can set the path for the filesystem cache in the CommonsBooking settings under "Advanced Options". Ask your web host which folders on the server are available for temporary files. - * If that is also not possible: Go to your Site Health screen at (http://YOUR-URL/wp-admin/site-health.php?tab=debug). There you will find the path to your WordPress directory under **Directories**. Alternatively, choose a folder in the format `YOUR_DIRECTORY/symfony` as the cache target. **Warning:** This can cause your WordPress directory to grow very large. - -Alternatively, you can install [Redis](https://redis.io) on your server and let Redis manage the cache. Since Redis stores the cache in RAM instead of the filesystem, it is usually a bit faster. diff --git a/docs/en/documentation/setup/migration-from-cb1.md b/docs/en/documentation/setup/migration-from-cb1.md index 89a92dba3..04c390412 100644 --- a/docs/en/documentation/setup/migration-from-cb1.md +++ b/docs/en/documentation/setup/migration-from-cb1.md @@ -1,5 +1,10 @@ # Migration from version 0.9.x +:::danger danger +With CommonsBooking 2.12 (releasing early 2027), migration support from CommonsBooking 0.9.X to the latest version will end. +All users of CommonsBooking 0.9.X are urged to **migrate as soon as possible**. We are happy to help with any questions and will support you in migration. +::: + You can migrate from CB 0.9.x to CB 2.x.x with a single click. The migration imports the following data: * Items diff --git a/includes/Users.php b/includes/Users.php index c3220544b..9b9f5272a 100644 --- a/includes/Users.php +++ b/includes/Users.php @@ -171,17 +171,7 @@ function commonsbooking_isCurrentUserAdmin() { return false; } $user = wp_get_current_user(); - $isAdmin = commonsbooking_isUserAdmin( $user ); - /** - * Default value if current user is admin. - * - * @since 2.10.0 add $user param - * @since 2.4.3 - * - * @param bool $isAdmin true or false, if current user is admin - * @param null|WP_User $user current user - */ - return apply_filters( 'commonsbooking_isCurrentUserAdmin', $isAdmin, $user ); + return commonsbooking_isUserAdmin( $user ); } /** @@ -195,12 +185,24 @@ function commonsbooking_isCurrentUserAdmin() { * @return bool */ function commonsbooking_isUserAdmin( \WP_User $user ) { + $isAdmin = false; foreach ( \CommonsBooking\Repository\UserRepository::getAdminRoles() as $adminRole ) { if ( in_array( $adminRole, $user->roles ) ) { - return true; + $isAdmin = true; + break; } } - return false; + /** + * Default value if user is admin. + * + * @since 2.11 changed from commonsbooking_isCurrentUserAdmin to commonsbooking_isUserAdmin + * @since 2.10.0 add $user param + * @since 2.4.3 + * + * @param bool $isAdmin true or false, if current user is admin + * @param null|WP_User $user current user + */ + return apply_filters( 'commonsbooking_isUserAdmin', $isAdmin, $user ); } /** diff --git a/includes/gbfs-json-schema/gbfs.json b/includes/gbfs-json-schema/gbfs.json index 2898226e2..0bd292143 100644 --- a/includes/gbfs-json-schema/gbfs.json +++ b/includes/gbfs-json-schema/gbfs.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/draft-07/schema", - "$id": "https://github.com/MobilityData/gbfs-json-schema/blob/master/v3.1-RC2/gbfs.json", + "$id": "https://github.com/MobilityData/gbfs-json-schema/blob/master/v3.1-RC3/gbfs.json", "description": "Auto-discovery file that links to all of the other files published by the system.", "type": "object", "properties": { @@ -17,7 +17,7 @@ "version": { "description": "GBFS version number to which the feed conforms, according to the versioning framework (added in v1.1).", "type": "string", - "const": "3.1-RC2" + "const": "3.1-RC3" }, "data": { "type": "object", diff --git a/includes/gbfs-json-schema/station_information.json b/includes/gbfs-json-schema/station_information.json index d801ed3e6..8b9ca5700 100644 --- a/includes/gbfs-json-schema/station_information.json +++ b/includes/gbfs-json-schema/station_information.json @@ -1,7 +1,7 @@ { "$schema": "http://json-schema.org/draft-07/schema", "$id": - "https://github.com/MobilityData/gbfs-json-schema/blob/master/v3.1-RC2/station_information.json", + "https://github.com/MobilityData/gbfs-json-schema/blob/master/v3.1-RC3/station_information.json", "description": "List of all stations, their capacities and locations. REQUIRED of systems utilizing docks.", "type": "object", @@ -22,7 +22,7 @@ "description": "GBFS version number to which the feed conforms, according to the versioning framework (added in v1.1).", "type": "string", - "const": "3.1-RC2" + "const": "3.1-RC3" }, "data": { "description": @@ -107,7 +107,7 @@ "type": "string" }, "city": { - "description": "City where station is located. (added in v3.1-RC2)", + "description": "City where station is located. (added in v3.1-RC3)", "type": "string" }, "station_opening_hours": { diff --git a/includes/gbfs-json-schema/station_status.json b/includes/gbfs-json-schema/station_status.json index 7249ab6b8..19cf5e119 100644 --- a/includes/gbfs-json-schema/station_status.json +++ b/includes/gbfs-json-schema/station_status.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/draft-07/schema", - "$id": "https://github.com/MobilityData/gbfs-json-schema/blob/master/v3.1-RC2/station_status.json", + "$id": "https://github.com/MobilityData/gbfs-json-schema/blob/master/v3.1-RC3/station_status.json", "description": "Describes the capacity and rental availability of the station", "type": "object", @@ -21,7 +21,7 @@ "description": "GBFS version number to which the feed conforms, according to the versioning framework (added in v1.1).", "type": "string", - "const": "3.1-RC2" + "const": "3.1-RC3" }, "data": { "description": diff --git a/includes/gbfs-json-schema/system_information.json b/includes/gbfs-json-schema/system_information.json index 1f29028f6..8c29b2d28 100644 --- a/includes/gbfs-json-schema/system_information.json +++ b/includes/gbfs-json-schema/system_information.json @@ -1,7 +1,7 @@ { "$schema": "http://json-schema.org/draft-07/schema", "$id": - "https://github.com/MobilityData/gbfs-json-schema/blob/master/v3.1-RC2/system_information.json", + "https://github.com/MobilityData/gbfs-json-schema/blob/master/v3.1-RC3/system_information.json", "description": "Details including system operator, system location, year implemented, URL, contact info, time zone.", "type": "object", @@ -22,7 +22,7 @@ "description": "GBFS version number to which the feed conforms, according to the versioning framework (added in v1.1).", "type": "string", - "const": "3.1-RC2" + "const": "3.1-RC3" }, "data": { "description": "Response data in the form of name:value pairs.", diff --git a/includes/gbfs-json-schema/vehicle_availability.json b/includes/gbfs-json-schema/vehicle_availability.json new file mode 100644 index 000000000..56ac9c593 --- /dev/null +++ b/includes/gbfs-json-schema/vehicle_availability.json @@ -0,0 +1,91 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://github.com/MobilityData/gbfs-json-schema/blob/master/v3.1-RC3/vehicle_availability.json", + "description": "Describes the vehicle availabilities of the system.", + "type": "object", + "properties": { + "last_updated": { + "description": "Last time the data in the feed was updated in RFC3339 format.", + "type": "string", + "format": "date-time" + }, + "ttl": { + "description": "Number of seconds before the data in the feed will be updated again (0 if the data should always be refreshed).", + "type": "integer", + "minimum": 0 + }, + "version": { + "description": "GBFS version number to which the feed conforms, according to the versioning framework (added in v1.1).", + "type": "string", + "const": "3.1-RC3" + }, + "data": { + "type": "object", + "properties": { + "vehicles": { + "type": "array", + "description": "Contains one object per vehicle.", + "items": { + "type": "object", + "properties": { + "vehicle_id": { + "type": "string", + "description": "Identifier of a vehicle" + }, + "vehicle_type_id": { + "type": "string", + "description": "Unique identifier of a vehicle type as defined in vehicle_types.json" + }, + "station_id": { + "type": "string", + "description": " The id of the station where this vehicle is located when available" + }, + "pricing_plan_id": { + "type": "string", + "description": "The plan_id of the pricing plan this vehicle is eligible for" + }, + "vehicle_equipment": { + "type": "array", + "description": "List of vehicle equipment provided by the operator", + "items": { + "type": "string" + } + }, + "availabilities": { + "type": "array", + "description": "Array of time slots during which the specified vehicle is available.", + "items": { + "type": "object", + "properties": { + "from": { + "type": "string", + "description": "Start date and time of available time slot.", + "format": "date-time" + }, + "until": { + "type": "string", + "description": "End date and time of available time slot.", + "format": "date-time" + } + }, + "required": ["from"], + "additionalProperties": false + } + } + }, + "required": [ + "vehicle_id", + "station_id", + "availabilities" + ], + "additionalProperties": false + } + } + }, + "required": ["vehicles"], + "additionalProperties": false + } + }, + "required": ["last_updated", "ttl", "version", "data"], + "additionalProperties": false +} diff --git a/includes/gbfs-json-schema/vehicle_status.json b/includes/gbfs-json-schema/vehicle_status.json new file mode 100644 index 000000000..87e280408 --- /dev/null +++ b/includes/gbfs-json-schema/vehicle_status.json @@ -0,0 +1,153 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": + "https://github.com/MobilityData/gbfs-json-schema/blob/master/v3.1-RC3/vehicle_status.json", + "description": + "Describes the vehicles that are available for rent (as of v3.0, formerly free_bike_status).", + "type": "object", + "properties": { + "last_updated": { + "description": + "Last time the data in the feed was updated in RFC3339 format.", + "type": "string", + "format": "date-time" + }, + "ttl": { + "description": + "Number of seconds before the data in the feed will be updated again (0 if the data should always be refreshed).", + "type": "integer", + "minimum": 0 + }, + "version": { + "description": + "GBFS version number to which the feed conforms, according to the versioning framework (added in v1.1).", + "type": "string", + "const": "3.1-RC3" + }, + "data": { + "description": + "Array that contains one object per vehicle as defined below.", + "type": "object", + "properties": { + "vehicles": { + "type": "array", + "items": { + "type": "object", + "properties": { + "vehicle_id": { + "description": "Rotating (as of v2.0) identifier of a vehicle.", + "type": "string" + }, + "lat": { + "description": "The latitude of the vehicle.", + "type": "number", + "minimum": -90, + "maximum": 90 + }, + "lon": { + "description": "The longitude of the vehicle.", + "type": "number", + "minimum": -180, + "maximum": 180 + }, + "is_reserved": { + "description": "Is the vehicle currently reserved?", + "type": "boolean" + }, + "is_disabled": { + "description": "Is the vehicle currently disabled (broken)?", + "type": "boolean" + }, + "rental_uris": { + "description": "Contains rental uris for Android, iOS, and web in the android, ios, and web fields (added in v1.1).", + "type": "object", + "properties": { + "android": { + "description": "URI that can be passed to an Android app with an intent (added in v1.1).", + "type": "string", + "format": "uri" + }, + "ios": { + "description": "URI that can be used on iOS to launch the rental app for this vehicle (added in v1.1).", + "type": "string", + "format": "uri" + }, + "web": { + "description": "URL that can be used by a web browser to show more information about renting this vehicle (added in v1.1).", + "type": "string", + "format": "uri" + } + } + }, + "vehicle_type_id": { + "description": "The vehicle_type_id of this vehicle (added in v2.1-RC).", + "type": "string" + }, + "last_reported": { + "description": "The last time this vehicle reported its status to the operator's backend in RFC3339 format (added in v2.1-RC).", + "type": "string", + "format": "date-time" + }, + "current_range_meters": { + "description": "The furthest distance in meters that the vehicle can travel without recharging or refueling with the vehicle's current charge or fuel (added in v2.1-RC).", + "type": "number", + "minimum": 0 + }, + "current_fuel_percent": { + "description": "This value represents the current percentage, expressed from 0 to 1, of fuel or battery power remaining in the vehicle. Added in v2.3-RC.", + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "station_id": { + "description": "Identifier referencing the station_id if the vehicle is currently at a station (added in v2.1-RC2).", + "type": "string" + }, + "home_station_id": { + "description": "The station_id of the station this vehicle must be returned to (added in v2.3-RC).", + "type": "string" + }, + "pricing_plan_id": { + "description": "The plan_id of the pricing plan this vehicle is eligible for (added in v2.2).", + "type": "string" + }, + "vehicle_equipment": { + "description": "List of vehicle equipment provided by the operator in addition to the accessories already provided in the vehicle. Added in v2.3.", + "type": "array", + "items": { + "enum": ["child_seat_a", "child_seat_b", "child_seat_c", "winter_tires", "snow_chains"] + } + }, + "available_until": { + "description": "The date and time when any rental of the vehicle must be completed. Added in v2.3.", + "type": "string", + "pattern": "^([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})(([+-]([0-9]{2}):([0-9]{2}))|Z)$" + } + }, + "anyOf": [ + { + "required": ["lat", "lon"], + "errorMessage": "Both 'lat' and 'lon' are required." + }, + { + "required": ["station_id"], + "properties": { + "lat": { + "not": {} + }, + "lon": { + "not": {} + } + }, + "errorMessage": "'station_id' is required if 'lat' and 'lon' are not present." + } + ], + "required": ["vehicle_id", "is_reserved", "is_disabled"] + } + } + }, + "required": ["vehicles"] + } + }, + "required": ["last_updated", "ttl", "version", "data"] +} diff --git a/includes/gbfs-json-schema/vehicle_types.json b/includes/gbfs-json-schema/vehicle_types.json new file mode 100644 index 000000000..02eb9644c --- /dev/null +++ b/includes/gbfs-json-schema/vehicle_types.json @@ -0,0 +1,273 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": + "https://github.com/MobilityData/gbfs-json-schema/blob/master/v3.1-RC3/vehicle_types.json", + "description": + "Describes the types of vehicles that System operator has available for rent (added in v2.1-RC).", + "type": "object", + "properties": { + "last_updated": { + "description": + "Last time the data in the feed was updated in RFC3339 format.", + "type": "string", + "format": "date-time" + }, + "ttl": { + "description": + "Number of seconds before the data in the feed will be updated again (0 if the data should always be refreshed).", + "type": "integer", + "minimum": 0 + }, + "version": { + "description": + "GBFS version number to which the feed conforms, according to the versioning framework.", + "type": "string", + "const": "3.1-RC3" + }, + "data": { + "description": "Response data in the form of name:value pairs.", + "type": "object", + "properties": { + "vehicle_types": { + "description": + "Array that contains one object per vehicle type in the system as defined below.", + "type": "array", + "items": { + "type": "object", + "properties": { + "vehicle_type_id": { + "description": "Unique identifier of a vehicle type.", + "type": "string" + }, + "form_factor": { + "description": "The vehicle's general form factor.", + "type": "string", + "enum": ["bicycle", "cargo_bicycle" ,"car", "moped", "scooter_standing", "scooter_seated", "other"] + }, + "rider_capacity": { + "description": "The number of riders (driver included) the vehicle can legally accommodate", + "type": "integer", + "minimum": 0 + }, + "cargo_volume_capacity": { + "description": "Cargo volume available in the vehicle, expressed in liters.", + "type": "integer", + "minimum": 0 + }, + "cargo_load_capacity": { + "description": "The capacity of the vehicle cargo space (excluding passengers), expressed in kilograms.", + "type": "integer", + "minimum": 0 + }, + + "propulsion_type": { + "description": "The primary propulsion type of the vehicle. Updated in v2.3 to represent car-sharing", + "type": "string", + "enum": ["human", "electric_assist", "electric", "combustion", "combustion_diesel", "hybrid", "plug_in_hybrid", "hydrogen_fuel_cell"] + }, + "eco_labels": { + "description": "Vehicle air quality certificate. added in v2.3.", + "type": "array", + "items": { + "type": "object", + "properties": { + "country_code": { + "description": " Country code following the ISO 3166-1 alpha-2 notation. Added in v2.3.", + "type": "string", + "pattern": "^[A-Z]{2}" + }, + "eco_sticker": { + "description": " Name of the eco label. Added in v2.3.", + "type": "string" + } + }, + "required": ["country_code", "eco_sticker"] + } + }, + "max_range_meters": { + "description": + "The furthest distance in meters that the vehicle can travel without recharging or refueling when it has the maximum amount of energy potential.", + "type": "number", + "minimum": 0 + }, + "name": { + "description": "The public name of this vehicle type. An array with one object per supported language with the following keys:", + "type": "array", + "items": { + "type": "object", + "properties": { + "text": { + "description": "The translated text.", + "type": "string" + }, + "language": { + "description": "IETF BCP 47 language code.", + "type": "string", + "pattern": "^[a-z]{2,3}(-[A-Z]{2})?$" + } + }, + "required": ["text", "language"] + } + }, + "vehicle_accessories": { + "description": "Description of accessories available in the vehicle.", + "type": "array", + "items": { + "enum": ["air_conditioning", "automatic", "manual", "convertible", "cruise_control", "doors_2", "doors_3", "doors_4", "doors_5", "navigation"] + } + }, + "g_CO2_km": { + "description": "Maximum quantity of CO2, in grams, emitted per kilometer, according to the WLTP. Added in v2.3", + "type": "integer", + "minimum": 0 + }, + "vehicle_image": { + "description": "URL to an image that would assist the user in identifying the vehicle. JPEG or PNG. Added in v2.3", + "type": "string", + "format": "uri" + }, + "make": { + "description": "The name of the vehicle manufacturer. An array with one object per supported language with the following keys:", + "type": "array", + "items": { + "type": "object", + "properties": { + "text": { + "description": "The translated text.", + "type": "string" + }, + "language": { + "description": "IETF BCP 47 language code.", + "type": "string", + "pattern": "^[a-z]{2,3}(-[A-Z]{2})?$" + } + }, + "required": ["text", "language"] + } + }, + "model": { + "description": "The name of the vehicle model. An array with one object per supported language with the following keys:", + "type": "array", + "items": { + "type": "object", + "properties": { + "text": { + "description": "The translated text.", + "type": "string" + }, + "language": { + "description": "IETF BCP 47 language code.", + "type": "string", + "pattern": "^[a-z]{2,3}(-[A-Z]{2})?$" + } + }, + "required": ["text", "language"] + } + }, + "color": { + "description": "The color of the vehicle. Added in v2.3", + "type": "string" + }, + "description": { + "description": "Customer-readable description of the vehicle type outlining special features or how-tos. An array with one object per supported language with the following keys:", + "type": "array", + "items": { + "type": "object", + "properties": { + "text": { + "description": "The translated text.", + "type": "string" + }, + "language": { + "description": "IETF BCP 47 language code.", + "type": "string", + "pattern": "^[a-z]{2,3}(-[A-Z]{2})?$" + } + }, + "required": ["text", "language"] + } + }, + "wheel_count": { + "description": "Number of wheels this vehicle type has. Added in v2.3", + "type": "integer", + "minimum": 0 + }, + "max_permitted_speed": { + "description": "The maximum speed in kilometers per hour this vehicle is permitted to reach in accordance with local permit and regulations. Added in v2.3", + "type": "integer", + "minimum": 0 + }, + "rated_power": { + "description": "The rated power of the motor for this vehicle type in watts. Added in v2.3", + "type": "integer", + "minimum": 0 + }, + "default_reserve_time": { + "description": "Maximum time in minutes that a vehicle can be reserved before a rental begins added in v2.3-RC.", + "type": "integer", + "minimum": 0 + }, + "return_constraint": { + "description": "The conditions for returning the vehicle at the end of the trip. Added in v2.3-RC as return_type, and updated to return_constraint in v2.3.", + "type": "string", + "enum": ["free_floating", "roundtrip_station", "any_station", "hybrid"] + }, + "min_age": { + "description": "Minimum age required to use this vehicle. Added in v3.1-RC3.", + "type": "integer", + "minimum": 0 + }, + "vehicle_assets": { + "description": "An object where each key defines one of the items listed below added in v2.3-RC.", + "type": "object", + "properties": { + "icon_url": { + "description": "A fully qualified URL pointing to the location of a graphic icon file that MAY be used to represent this vehicle type on maps and in other applications added in v2.3-RC.", + "type": "string", + "format": "uri" + }, + "icon_url_dark": { + "description": "A fully qualified URL pointing to the location of a graphic icon file to be used to represent this vehicle type when in dark mode added in v2.3-RC.", + "type": "string", + "format": "uri" + }, + "icon_last_modified": { + "description": "Date that indicates the last time any included vehicle icon images were modified or updated added in v2.3-RC.", + "type": "string", + "format": "date" + } + }, + "required": ["icon_url", "icon_last_modified"] + }, + "default_pricing_plan_id": { + "description": "A plan_id as defined in system_pricing_plans.json added in v2.3-RC.", + "type": "string" + }, + "pricing_plan_ids": { + "description": "Array of all pricing plan IDs as defined in system_pricing_plans.json added in v2.3-RC.", + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": ["vehicle_type_id", "form_factor", "propulsion_type"], + "if": { + "properties": { + "propulsion_type": { + "enum": ["electric", "electric_assist", "combustion", "combustion_diesel", "hybrid", "plug_in_hybrid", "hydrogen_fuel_cell"] + } + } + }, + "then": { + "required": ["max_range_meters"] + } + } + } + }, + "required": ["vehicle_types"] + } + }, + "required": ["last_updated", "ttl", "version", "data"] +} + diff --git a/integrations/uncanny-automator/bootstrap.php b/integrations/uncanny-automator/bootstrap.php new file mode 100644 index 000000000..37cfaac40 --- /dev/null +++ b/integrations/uncanny-automator/bootstrap.php @@ -0,0 +1,41 @@ +set_trigger_type( 'anonymous' ); + + $this->set_integration( 'COMMONSBOOKING' ); + $this->set_trigger_code( 'CB_BOOKING_CANCELLED' ); + $this->set_trigger_meta( 'CBBOOKINGCANCELLED' ); + $this->set_sentence( esc_attr__( 'A booking is cancelled', 'commonsbooking' ) ); + $this->set_readable_sentence( esc_attr__( 'A booking is cancelled', 'commonsbooking' ) ); + + // The CommonsBooking lifecycle action passes ( int $booking_id, Booking $booking ). + $this->add_action( 'commonsbooking_booking_cancelled', 10, 2 ); + } + + /** + * This trigger has no configurable options; it fires for every cancelled booking. + * + * @return array + */ + public function options() { + return array(); + } + + /** + * The trigger fires for every cancelled booking. + * + * @param array $trigger The trigger configuration. + * @param array $hook_args The arguments passed by the WordPress hook. + * + * @return bool + */ + public function validate( $trigger, $hook_args ) { + return true; + } + + // define_tokens() and hydrate_tokens() are provided by the Booking_Tokens trait. +} diff --git a/integrations/uncanny-automator/class-booking-confirmed-trigger.php b/integrations/uncanny-automator/class-booking-confirmed-trigger.php new file mode 100644 index 000000000..d7757216d --- /dev/null +++ b/integrations/uncanny-automator/class-booking-confirmed-trigger.php @@ -0,0 +1,65 @@ +set_trigger_type( 'anonymous' ); + + $this->set_integration( 'COMMONSBOOKING' ); + $this->set_trigger_code( 'CB_BOOKING_CONFIRMED' ); + $this->set_trigger_meta( 'CBBOOKINGCONFIRMED' ); + $this->set_sentence( esc_attr__( 'A booking is confirmed', 'commonsbooking' ) ); + $this->set_readable_sentence( esc_attr__( 'A booking is confirmed', 'commonsbooking' ) ); + + // The CommonsBooking lifecycle action passes ( int $booking_id, Booking $booking ). + $this->add_action( 'commonsbooking_booking_confirmed', 10, 2 ); + } + + /** + * This trigger has no configurable options; it fires for every confirmed booking. + * + * @return array + */ + public function options() { + return array(); + } + + /** + * The trigger fires for every confirmed booking. + * + * @param array $trigger The trigger configuration. + * @param array $hook_args The arguments passed by the WordPress hook. + * + * @return bool + */ + public function validate( $trigger, $hook_args ) { + return true; + } + + // define_tokens() and hydrate_tokens() are provided by the Booking_Tokens trait. +} diff --git a/integrations/uncanny-automator/class-cb-integration.php b/integrations/uncanny-automator/class-cb-integration.php new file mode 100644 index 000000000..9aea5a6f5 --- /dev/null +++ b/integrations/uncanny-automator/class-cb-integration.php @@ -0,0 +1,27 @@ +set_integration( 'COMMONSBOOKING' ); + $this->set_name( 'CommonsBooking' ); + $this->set_icon_url( plugin_dir_url( __FILE__ ) . 'icon.svg' ); + } +} diff --git a/integrations/uncanny-automator/icon.svg b/integrations/uncanny-automator/icon.svg new file mode 100644 index 000000000..de536375a --- /dev/null +++ b/integrations/uncanny-automator/icon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/integrations/uncanny-automator/trait-booking-tokens.php b/integrations/uncanny-automator/trait-booking-tokens.php new file mode 100644 index 000000000..60e259b43 --- /dev/null +++ b/integrations/uncanny-automator/trait-booking-tokens.php @@ -0,0 +1,124 @@ + 'CB_BOOKING_ID', + 'tokenName' => __( 'Booking ID', 'commonsbooking' ), + 'tokenType' => 'int', + ); + $tokens[] = array( + 'tokenId' => 'CB_ITEM_NAME', + 'tokenName' => __( 'Item name', 'commonsbooking' ), + 'tokenType' => 'text', + ); + $tokens[] = array( + 'tokenId' => 'CB_LOCATION_NAME', + 'tokenName' => __( 'Location name', 'commonsbooking' ), + 'tokenType' => 'text', + ); + $tokens[] = array( + 'tokenId' => 'CB_BOOKING_START', + 'tokenName' => __( 'Booking start', 'commonsbooking' ), + 'tokenType' => 'text', + ); + $tokens[] = array( + 'tokenId' => 'CB_BOOKING_END', + 'tokenName' => __( 'Booking end', 'commonsbooking' ), + 'tokenType' => 'text', + ); + $tokens[] = array( + 'tokenId' => 'CB_BOOKING_CODE', + 'tokenName' => __( 'Booking code', 'commonsbooking' ), + 'tokenType' => 'text', + ); + $tokens[] = array( + 'tokenId' => 'CB_BOOKING_USER_EMAIL', + 'tokenName' => __( 'Booking user email', 'commonsbooking' ), + 'tokenType' => 'email', + ); + $tokens[] = array( + 'tokenId' => 'CB_BOOKING_USER_PHONE', + 'tokenName' => __( 'Booking user phone', 'commonsbooking' ), + 'tokenType' => 'text', + ); + $tokens[] = array( + 'tokenId' => 'CB_BOOKING_URL', + 'tokenName' => __( 'Booking URL', 'commonsbooking' ), + 'tokenType' => 'url', + ); + + return $tokens; + } + + /** + * Populates the token values from the booking passed by the lifecycle hook. + * + * @param array $trigger The trigger configuration. + * @param array $hook_args The arguments passed by the lifecycle action ( int $booking_id, Booking $booking ). + * + * @return array + */ + public function hydrate_tokens( $trigger, $hook_args ) { + $booking_id = isset( $hook_args[0] ) ? (int) $hook_args[0] : 0; + $booking = isset( $hook_args[1] ) ? $hook_args[1] : null; + + if ( ! $booking instanceof \CommonsBooking\Model\Booking ) { + $booking = $booking_id ? new \CommonsBooking\Model\Booking( $booking_id ) : null; + } + + if ( ! $booking ) { + return array( 'CB_BOOKING_ID' => $booking_id ); + } + + $item = $booking->getItem(); + $location = $booking->getLocation(); + $author_id = (int) $booking->post_author; + $user = get_userdata( $author_id ); + + // The booking code is optional and may throw if unavailable. + $code = ''; + try { + $code = $booking->formattedBookingCode(); + } catch ( \Exception $e ) { + $code = ''; + } + + return array( + 'CB_BOOKING_ID' => $booking_id, + 'CB_ITEM_NAME' => $item ? $item->post_title : '', + 'CB_LOCATION_NAME' => $location ? $location->post_title : '', + 'CB_BOOKING_START' => $booking->getFormattedStartDate(), + 'CB_BOOKING_END' => $booking->getFormattedEndDate(), + 'CB_BOOKING_CODE' => $code, + 'CB_BOOKING_USER_EMAIL' => $user ? $user->user_email : '', + 'CB_BOOKING_USER_PHONE' => $author_id ? (string) get_user_meta( $author_id, 'phone', true ) : '', + 'CB_BOOKING_URL' => $booking->bookingLinkUrl(), + ); + } +} diff --git a/languages/commonsbooking-de_DE.mo b/languages/commonsbooking-de_DE.mo index c79a46cd8..fae1c549b 100644 Binary files a/languages/commonsbooking-de_DE.mo and b/languages/commonsbooking-de_DE.mo differ diff --git a/languages/commonsbooking-de_DE.po b/languages/commonsbooking-de_DE.po index 0be42d0b2..1aa95cf0f 100644 --- a/languages/commonsbooking-de_DE.po +++ b/languages/commonsbooking-de_DE.po @@ -8,10 +8,10 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "POT-Creation-Date: 2023-09-15T04:04:14+02:00\n" -"PO-Revision-Date: 2026-02-19 18:07+0100\n" +"PO-Revision-Date: 2026-06-17 15:48+0200\n" "Language: de_DE\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -"X-Generator: Poedit 3.8\n" +"X-Generator: Poedit 3.9\n" #. Plugin Name of the plugin #: commonsbooking.php @@ -1006,71 +1006,71 @@ msgstr "Erlaubt die Anpassung der Optionen für das Caching-System" msgid "Current connection status" msgstr "Aktueller Verbindungsstatus" -#: src/CB/CB1UserFields.php:69 +#: src/CB/CB1UserFields.php:72 msgid "First Name" msgstr "Vorname" -#: src/CB/CB1UserFields.php:72 +#: src/CB/CB1UserFields.php:75 msgid "Please enter your first name" msgstr "Bitte gib deinen Vornamen ein" -#: src/CB/CB1UserFields.php:76 +#: src/CB/CB1UserFields.php:79 msgid "Last Name" msgstr "Nachname" -#: src/CB/CB1UserFields.php:79 +#: src/CB/CB1UserFields.php:82 msgid "Please enter your last name" msgstr "Bitte gib Deinen Nachnamen ein" -#: src/CB/CB1UserFields.php:83 +#: src/CB/CB1UserFields.php:86 msgid "Phone Number" msgstr "Telefonnummer" -#: src/CB/CB1UserFields.php:86 +#: src/CB/CB1UserFields.php:89 msgid "Please enter your phone number" msgstr "Bitte gib Deine Telefonnummer ein" -#: src/CB/CB1UserFields.php:90 -#: src/CB/CB1UserFields.php:279 +#: src/CB/CB1UserFields.php:93 +#: src/CB/CB1UserFields.php:283 #: src/Wordpress/CustomPostType/Location.php:237 #: templates/booking-single.php:77 msgid "Address" msgstr "Adresse" -#: src/CB/CB1UserFields.php:93 +#: src/CB/CB1UserFields.php:96 msgid "Please enter your address" msgstr "Bitte gib deine Adresse ein" -#: src/CB/CB1UserFields.php:96 +#: src/CB/CB1UserFields.php:99 msgid "Terms and Conditions" msgstr "Nutzungsbedingungen" -#: src/CB/CB1UserFields.php:99 +#: src/CB/CB1UserFields.php:102 msgid "I accept the terms & conditions" msgstr "Ich akzeptiere die Nutzungsbedingungen" -#: src/CB/CB1UserFields.php:100 +#: src/CB/CB1UserFields.php:103 msgid "Please accept the terms & conditions" msgstr "Bitte akzeptiere die Nutzungsbedingungen" -#: src/CB/CB1UserFields.php:188 +#: src/CB/CB1UserFields.php:191 #, php-format msgid "Read the terms and services" msgstr "Lies die Nutzungsbedingungen" -#: src/CB/CB1UserFields.php:267 +#: src/CB/CB1UserFields.php:271 msgid "Extra Fields" msgstr "Zusatzfelder" -#: src/CB/CB1UserFields.php:271 +#: src/CB/CB1UserFields.php:275 msgid "Phone number" msgstr "Telefonnummer" -#: src/CB/CB1UserFields.php:287 +#: src/CB/CB1UserFields.php:291 msgid "Terms and conditions" msgstr "Nutzungsbedingungen" -#: src/CB/CB1UserFields.php:297 +#: src/CB/CB1UserFields.php:301 msgid "Accepted Terms & Conditions" msgstr "Nutzungsbedingungen akzeptiert" @@ -1179,56 +1179,56 @@ msgstr "Link zu deiner Buchung" msgid "Please contact the contact persons at the location directly if you have any questions regarding collection or return:" msgstr "Bitte kontaktiere die Kontaktpersonen vor Ort direkt, wenn du Fragen zur Abholung oder Rückgabe hast:" -#: src/Model/Restriction.php:244 -#: src/Model/Restriction.php:268 +#: src/Model/Restriction.php:248 +#: src/Model/Restriction.php:275 msgid "Not set" msgstr "Nicht gesetzt" -#: src/Model/Timeframe.php:255 +#: src/Model/Timeframe.php:331 msgid "Available here" msgstr "Hier verfügbar" #. translators: %s = date in WordPress defined format #: src/Model/Booking.php:437 -#: src/Model/Timeframe.php:260 +#: src/Model/Timeframe.php:336 #, php-format msgid "on %s" msgstr "am %s" #. translators: %s = date in WordPress defined format -#: src/Model/Timeframe.php:265 +#: src/Model/Timeframe.php:341 #, php-format msgid "from %s" msgstr "von %s" -#: src/Model/Timeframe.php:269 +#: src/Model/Timeframe.php:345 msgid "permanently" msgstr "dauerhaft" #. translators: %1 = startdate, %2 = enddate in WordPress defined format #. translators: %1$s = startdate, second %2$s = enddate in WordPress defined format #: src/Model/Booking.php:440 -#: src/Model/Timeframe.php:275 +#: src/Model/Timeframe.php:351 #, php-format msgid "from %1$s until %2$s" msgstr "von %1$s bis %2$s" #. translators: %s = enddate in WordPress defined format -#: src/Model/Timeframe.php:282 +#: src/Model/Timeframe.php:358 #, php-format msgid "until %s" msgstr "bis %s" -#: src/Model/Timeframe.php:610 +#: src/Model/Timeframe.php:686 msgid "A pickup time but no return time has been set. Please set the return time." msgstr "Eine Abholzeit, aber keine Rückgabezeit wurde festgelegt. Lege bitte eine Rückgabezeit fest." -#: src/Model/Timeframe.php:620 +#: src/Model/Timeframe.php:696 msgid "End date is before start date. Please set a valid end date." msgstr "Enddatum liegt vor dem Startdatum. Bitte setze ein gültiges Enddatum." #. translators: %1$s = timeframe-ID, %2$s is timeframe post_title -#: src/Model/Timeframe.php:655 +#: src/Model/Timeframe.php:731 #, php-format msgid "Item is already bookable at another location within the same date range. See other timeframe ID: %1$s: %2$s" msgstr "Artikel ist bereits an einem anderen Ort innerhalb desselben Datumsbereichs buchbar. Siehe andere Zeitrahmen ID: %1$s: %2$s" @@ -1243,8 +1243,8 @@ msgstr "Artikel-Kategorien" msgid "Location Categories" msgstr "Standort-Kategorien" -#: src/Wordpress/CustomPostType/Item.php:324 -#: src/Wordpress/CustomPostType/Item.php:344 +#: src/Wordpress/CustomPostType/Item.php:334 +#: src/Wordpress/CustomPostType/Item.php:354 msgid "Item Category" msgstr "Artikel-Kategorie" @@ -1275,14 +1275,14 @@ msgid "Directory %s could not be written to." msgstr "Der Ordner %s ist nicht beschreibbar." #: src/View/Admin/Filter.php:58 -#: src/Wordpress/CustomPostType/Booking.php:779 +#: src/Wordpress/CustomPostType/Booking.php:792 #: src/Wordpress/CustomPostType/Restriction.php:451 #: src/Wordpress/CustomPostType/Timeframe.php:678 msgid "Start date" msgstr "Startdatum" #: src/View/Admin/Filter.php:62 -#: src/Wordpress/CustomPostType/Booking.php:796 +#: src/Wordpress/CustomPostType/Booking.php:809 #: src/Wordpress/CustomPostType/Restriction.php:461 #: src/Wordpress/CustomPostType/Timeframe.php:702 msgid "End date" @@ -1307,7 +1307,7 @@ msgstr "Nicht verfügbar" #: src/View/Booking.php:196 #: src/View/MassOperations.php:38 -#: src/Wordpress/CustomPostType/Booking.php:446 +#: src/Wordpress/CustomPostType/Booking.php:459 #: src/Wordpress/CustomPostType/Timeframe.php:97 #: templates/shortcode-bookings.php:74 msgid "User" @@ -1315,7 +1315,7 @@ msgstr "Nutzende*r" #: src/View/Booking.php:200 #: src/View/MassOperations.php:42 -#: src/Wordpress/CustomPostType/Booking.php:973 +#: src/Wordpress/CustomPostType/Booking.php:986 #: src/Wordpress/CustomPostType/Restriction.php:477 msgid "Status" msgstr "Status" @@ -1332,10 +1332,10 @@ msgstr "Die Datei wird als .txt Datei durch Tabstopp getrennt exportiert, sodass msgid "Booking codes list" msgstr "Liste der Buchungscodes" -#: src/View/Calendar.php:233 -#: src/Wordpress/CustomPostType/Booking.php:447 -#: src/Wordpress/CustomPostType/Booking.php:761 -#: src/Wordpress/CustomPostType/Booking.php:989 +#: src/View/Calendar.php:237 +#: src/Wordpress/CustomPostType/Booking.php:460 +#: src/Wordpress/CustomPostType/Booking.php:774 +#: src/Wordpress/CustomPostType/Booking.php:1002 #: src/Wordpress/CustomPostType/Item.php:117 #: src/Wordpress/CustomPostType/Restriction.php:48 #: src/Wordpress/CustomPostType/Restriction.php:438 @@ -1344,14 +1344,14 @@ msgstr "Liste der Buchungscodes" msgid "Item" msgstr "Artikel" -#: src/View/Calendar.php:177 +#: src/View/Calendar.php:181 msgid "No items found." msgstr "Keine Artikel gefunden." -#: src/View/Calendar.php:234 -#: src/Wordpress/CustomPostType/Booking.php:448 -#: src/Wordpress/CustomPostType/Booking.php:767 -#: src/Wordpress/CustomPostType/Booking.php:985 +#: src/View/Calendar.php:238 +#: src/Wordpress/CustomPostType/Booking.php:461 +#: src/Wordpress/CustomPostType/Booking.php:780 +#: src/Wordpress/CustomPostType/Booking.php:998 #: src/Wordpress/CustomPostType/Location.php:139 #: src/Wordpress/CustomPostType/Restriction.php:49 #: src/Wordpress/CustomPostType/Restriction.php:431 @@ -1503,189 +1503,189 @@ msgstr "Standort existiert nicht. (%s)" msgid "Start- and/or end-date is missing." msgstr "Das Start- und/oder Enddatum fehlt." -#: src/Wordpress/CustomPostType/Booking.php:307 +#: src/Wordpress/CustomPostType/Booking.php:320 msgid "Booking canceled." msgstr "Buchung storniert." #: src/Wordpress/CustomPostType/Booking.php:291 -#: src/Wordpress/CustomPostType/Booking.php:323 +#: src/Wordpress/CustomPostType/Booking.php:336 msgid "There is already a booking in this time-range. This notice may also appear if there is an unconfirmed booking in the requested period. Unconfirmed bookings are deleted after about 10 minutes. Please try again in a few minutes." msgstr "Es gibt bereits eine Buchung in diesem Zeitraum. Dieser Hinweis kann auch erscheinen, wenn es eine unbestätigte Buchung in dem gewünschten Zeitraum gibt. Unbestätigte Buchungen werden nach etwa 10 Minuten gelöscht. Bitte versuche es in ein paar Minuten erneut." -#: src/Wordpress/CustomPostType/Booking.php:331 -#: src/Wordpress/CustomPostType/Booking.php:524 -#: src/Wordpress/CustomPostType/Booking.php:712 +#: src/Wordpress/CustomPostType/Booking.php:344 +#: src/Wordpress/CustomPostType/Booking.php:537 +#: src/Wordpress/CustomPostType/Booking.php:725 #: src/Wordpress/CustomPostType/Timeframe.php:132 msgid "Booking" msgstr "Buchung" -#: src/Wordpress/CustomPostType/Booking.php:375 +#: src/Wordpress/CustomPostType/Booking.php:388 msgid "There was an error while saving the booking. Please try again. Thrown error:" msgstr "Es gab einen Fehler während der Buchung. Geworfener Fehler:" -#: src/Wordpress/CustomPostType/Booking.php:392 +#: src/Wordpress/CustomPostType/Booking.php:405 msgid "There was an error while saving the booking. Please try again. Resulting WP_ERROR: " msgstr "Es gab einen Fehler während der Buchung. WP_ERROR: " -#: src/Wordpress/CustomPostType/Booking.php:449 +#: src/Wordpress/CustomPostType/Booking.php:462 msgid "Bookingdate" msgstr "Buchungsdatum" -#: src/Wordpress/CustomPostType/Booking.php:450 +#: src/Wordpress/CustomPostType/Booking.php:463 #: src/Wordpress/CustomPostType/Restriction.php:50 #: src/Wordpress/CustomPostType/Timeframe.php:101 #: src/Wordpress/CustomPostType/Timeframe.php:758 msgid "Start Date" msgstr "Startdatum" -#: src/Wordpress/CustomPostType/Booking.php:451 +#: src/Wordpress/CustomPostType/Booking.php:464 #: src/Wordpress/CustomPostType/Restriction.php:51 #: src/Wordpress/CustomPostType/Timeframe.php:102 msgid "End Date" msgstr "Enddatum" -#: src/Wordpress/CustomPostType/Booking.php:452 +#: src/Wordpress/CustomPostType/Booking.php:465 msgid "Booking Status" msgstr "Buchungsstatus" -#: src/Wordpress/CustomPostType/Booking.php:453 -#: src/Wordpress/CustomPostType/Booking.php:981 +#: src/Wordpress/CustomPostType/Booking.php:466 +#: src/Wordpress/CustomPostType/Booking.php:994 #: src/Wordpress/CustomPostType/Timeframe.php:448 msgid "Comment" msgstr "Kommentar" -#: src/Wordpress/CustomPostType/Booking.php:523 +#: src/Wordpress/CustomPostType/Booking.php:536 #: templates/dashboard-index.php:45 msgid "Bookings" msgstr "Buchungen" -#: src/Wordpress/CustomPostType/Booking.php:525 +#: src/Wordpress/CustomPostType/Booking.php:538 #: src/Wordpress/CustomPostType/Item.php:118 #: src/Wordpress/CustomPostType/Location.php:140 #: src/Wordpress/CustomPostType/Restriction.php:307 -#: src/Wordpress/CustomPostType/Timeframe.php:1152 +#: src/Wordpress/CustomPostType/Timeframe.php:1153 msgid "Add new" msgstr "Neu hinzufügen" -#: src/Wordpress/CustomPostType/Booking.php:526 -#: src/Wordpress/CustomPostType/Booking.php:528 +#: src/Wordpress/CustomPostType/Booking.php:539 +#: src/Wordpress/CustomPostType/Booking.php:541 msgid "Add new booking" msgstr "Neue Buchung hinzufügen" -#: src/Wordpress/CustomPostType/Booking.php:527 -#: src/Wordpress/CustomPostType/Booking.php:743 +#: src/Wordpress/CustomPostType/Booking.php:540 +#: src/Wordpress/CustomPostType/Booking.php:756 msgid "Edit booking" msgstr "Buchung bearbeiten" -#: src/Wordpress/CustomPostType/Booking.php:529 +#: src/Wordpress/CustomPostType/Booking.php:542 msgid "Show booking" msgstr "Buchung anzeigen" -#: src/Wordpress/CustomPostType/Booking.php:530 +#: src/Wordpress/CustomPostType/Booking.php:543 msgid "Show bookings" msgstr "Buchungen anzeigen" -#: src/Wordpress/CustomPostType/Booking.php:531 +#: src/Wordpress/CustomPostType/Booking.php:544 msgid "Search bookings" msgstr "Buchungen suchen" -#: src/Wordpress/CustomPostType/Timeframe.php:1159 +#: src/Wordpress/CustomPostType/Timeframe.php:1160 msgid "Timeframes not found" msgstr "Zeitrahmen wurden nicht gefunden" -#: src/Wordpress/CustomPostType/Booking.php:533 +#: src/Wordpress/CustomPostType/Booking.php:546 msgid "No bookings found in trash" msgstr "Keine Buchungen im Papierkorb gefunden" -#: src/Wordpress/CustomPostType/Booking.php:534 +#: src/Wordpress/CustomPostType/Booking.php:547 msgid "Parent bookings:" msgstr "Übergeordnete Buchungen:" -#: src/Wordpress/CustomPostType/Booking.php:535 +#: src/Wordpress/CustomPostType/Booking.php:548 msgid "All bookings" msgstr "Alle Buchungen" -#: src/Wordpress/CustomPostType/Booking.php:536 -#: src/Wordpress/CustomPostType/Timeframe.php:1163 +#: src/Wordpress/CustomPostType/Booking.php:549 +#: src/Wordpress/CustomPostType/Timeframe.php:1164 msgid "Timeframe archive" msgstr "Zeitrahmenarchiv" -#: src/Wordpress/CustomPostType/Booking.php:537 -#: src/Wordpress/CustomPostType/Timeframe.php:1164 +#: src/Wordpress/CustomPostType/Booking.php:550 +#: src/Wordpress/CustomPostType/Timeframe.php:1165 msgid "Timeframe attributes" msgstr "Zeitrahmenattribute" -#: src/Wordpress/CustomPostType/Booking.php:538 +#: src/Wordpress/CustomPostType/Booking.php:551 msgid "Add to booking" msgstr "Der Buchung hinzufügen" -#: src/Wordpress/CustomPostType/Booking.php:539 +#: src/Wordpress/CustomPostType/Booking.php:552 msgid "Added to booking" msgstr "Zur Buchung hinzugefügt" -#: src/Wordpress/CustomPostType/Booking.php:540 -#: src/Wordpress/CustomPostType/Timeframe.php:1167 +#: src/Wordpress/CustomPostType/Booking.php:553 +#: src/Wordpress/CustomPostType/Timeframe.php:1168 msgid "Timeframe image" msgstr "Zeitrahmenbild" -#: src/Wordpress/CustomPostType/Booking.php:541 +#: src/Wordpress/CustomPostType/Booking.php:554 msgid "set booking image" msgstr "Buchungsbild festlegen" -#: src/Wordpress/CustomPostType/Booking.php:542 +#: src/Wordpress/CustomPostType/Booking.php:555 msgid "remove booking image" msgstr "Buchungsbild entfernen" -#: src/Wordpress/CustomPostType/Booking.php:543 +#: src/Wordpress/CustomPostType/Booking.php:556 msgid "use as booking image" msgstr "als Buchungsbild verwenden" -#: src/Wordpress/CustomPostType/Booking.php:544 -#: src/Wordpress/CustomPostType/Timeframe.php:1150 -#: src/Wordpress/CustomPostType/Timeframe.php:1171 +#: src/Wordpress/CustomPostType/Booking.php:557 +#: src/Wordpress/CustomPostType/Timeframe.php:1151 +#: src/Wordpress/CustomPostType/Timeframe.php:1172 #: templates/dashboard-index.php:37 msgid "Timeframes" msgstr "Zeitrahmen" -#: src/Wordpress/CustomPostType/Booking.php:852 +#: src/Wordpress/CustomPostType/Booking.php:865 msgid "External comment" msgstr "Öffentlicher Kommentar" -#: src/Wordpress/CustomPostType/Booking.php:853 +#: src/Wordpress/CustomPostType/Booking.php:866 msgid "This comment can be seen by users in booking details. It can be set by users during the booking confirmation process if comments are enabled in settings." msgstr "Dieser Kommentar ist intern für Zeitrahmen wie buchbar, Reparatur, Urlaub. Wenn es sich um eine Buchung handelt, kann dieser Kommentar von den Benutzenden während des Buchungsbestätigungsprozesses eingegeben werden." -#: src/Wordpress/CustomPostType/Booking.php:858 +#: src/Wordpress/CustomPostType/Booking.php:871 #: templates/booking-single.php:131 msgid "Internal comment" msgstr "Interner Kommentar" -#: src/Wordpress/CustomPostType/Booking.php:859 +#: src/Wordpress/CustomPostType/Booking.php:872 msgid "This internal comment can only be seen in the backend by privileged users like admins or cb-managers" msgstr "Dieser interne Kommentar kann im Backend nur von autorisierten Nutzenden wie Admins oder CB-Managern gesehen werden" -#: src/Wordpress/CustomPostType/Booking.php:780 +#: src/Wordpress/CustomPostType/Booking.php:793 msgid "Set the start date. You must set the time to 00:00 if you want to book the full day " msgstr "Lege das Startdatum fest. Setze die Uhrzeit auf 00:00, wenn du den ganzen Tag buchen willst " -#: src/Wordpress/CustomPostType/Booking.php:797 +#: src/Wordpress/CustomPostType/Booking.php:810 msgid "Set the end date. You must set time to 23:59 if you want to book the full day" msgstr "Lege das Enddatum fest. Setze die Zeit auf 23:59 Uhr, wenn du den ganzen Tag buchen willst" -#: src/Wordpress/CustomPostType/Booking.php:813 +#: src/Wordpress/CustomPostType/Booking.php:826 #: templates/booking-single.php:59 msgid "Booking Code" msgstr "Buchungscode" -#: src/Wordpress/CustomPostType/Booking.php:819 +#: src/Wordpress/CustomPostType/Booking.php:832 msgid "Booking User" msgstr "Buchende Person" -#: src/Wordpress/CustomPostType/Booking.php:834 +#: src/Wordpress/CustomPostType/Booking.php:847 msgid "Admin Booking User" msgstr "Admin Booking Konto" -#: src/Wordpress/CustomPostType/Booking.php:845 +#: src/Wordpress/CustomPostType/Booking.php:858 msgid "This is the admin user who created or modified this booking." msgstr "Dies ist der/die Admin-Nutzende, der diese Buchung erstellt oder geändert hat." @@ -1792,7 +1792,7 @@ msgstr "E-Mail-Adresse des Artikelbetreuers" msgid "Email addresses to which notifications about a change of item status (restriction, breakdown) shall be sent. You can enter multiple addresses separated by commas." msgstr "E-Mail-Adressen die benachrichtigt werden soll, wenn eine Nutzungseinschränkung für den Artikel eingerichtet wurde (Hinweis oder Totalausfall). Es können mehrere Adressen durch Komma getrennt eingeben werden." -#: src/Wordpress/CustomPostType/Item.php:293 +#: src/Wordpress/CustomPostType/Item.php:303 msgid "Item Meta-Data" msgstr "Artikel Metadaten" @@ -2185,7 +2185,7 @@ msgid "Blocked (not overbookable)" msgstr "Geblockt (nicht überbuchbar)" #: src/Wordpress/CustomPostType/Timeframe.php:420 -#: src/Wordpress/CustomPostType/Timeframe.php:1151 +#: src/Wordpress/CustomPostType/Timeframe.php:1152 msgid "Timeframe" msgstr "Zeitrahmen" @@ -2364,56 +2364,56 @@ msgstr "Monatlich" msgid "Yearly" msgstr "Jährlich" -#: src/Wordpress/CustomPostType/Timeframe.php:1153 -#: src/Wordpress/CustomPostType/Timeframe.php:1155 +#: src/Wordpress/CustomPostType/Timeframe.php:1154 +#: src/Wordpress/CustomPostType/Timeframe.php:1156 msgid "Add new timeframe" msgstr "Hinzufügen eines neuen Zeitrahmens" -#: src/Wordpress/CustomPostType/Timeframe.php:1154 +#: src/Wordpress/CustomPostType/Timeframe.php:1155 msgid "Edit timeframe" msgstr "Zeitrahmen bearbeiten" -#: src/Wordpress/CustomPostType/Timeframe.php:1156 +#: src/Wordpress/CustomPostType/Timeframe.php:1157 msgid "Show timeframe" msgstr "Zeitrahmen anzeigen" -#: src/Wordpress/CustomPostType/Timeframe.php:1157 +#: src/Wordpress/CustomPostType/Timeframe.php:1158 msgid "Show timeframes" msgstr "Zeitrahmen anzeigen" -#: src/Wordpress/CustomPostType/Timeframe.php:1158 +#: src/Wordpress/CustomPostType/Timeframe.php:1159 msgid "Search timeframes" msgstr "Zeitrahmen suchen" -#: src/Wordpress/CustomPostType/Timeframe.php:1160 +#: src/Wordpress/CustomPostType/Timeframe.php:1161 msgid "No timeframes found in trash" msgstr "Keine Zeitrahmen im Papierkorb gefunden" -#: src/Wordpress/CustomPostType/Timeframe.php:1161 +#: src/Wordpress/CustomPostType/Timeframe.php:1162 msgid "Parent timeframes:" msgstr "Übergeordnete Zeitrahmen:" -#: src/Wordpress/CustomPostType/Timeframe.php:1162 +#: src/Wordpress/CustomPostType/Timeframe.php:1163 msgid "All timeframes" msgstr "Alle Zeitrahmen" -#: src/Wordpress/CustomPostType/Timeframe.php:1165 +#: src/Wordpress/CustomPostType/Timeframe.php:1166 msgid "Add to timeframe" msgstr "Zum Zeitrahmen hinzufügen" -#: src/Wordpress/CustomPostType/Timeframe.php:1166 +#: src/Wordpress/CustomPostType/Timeframe.php:1167 msgid "Added to timeframe" msgstr "Zum Zeitrahmen hinzugefügt" -#: src/Wordpress/CustomPostType/Timeframe.php:1168 +#: src/Wordpress/CustomPostType/Timeframe.php:1169 msgid "set timeframe image" msgstr "Festlegen des Zeitrahmenbilds" -#: src/Wordpress/CustomPostType/Timeframe.php:1169 +#: src/Wordpress/CustomPostType/Timeframe.php:1170 msgid "remove timeframe image" msgstr "Zeitrahmenbild entfernen" -#: src/Wordpress/CustomPostType/Timeframe.php:1170 +#: src/Wordpress/CustomPostType/Timeframe.php:1171 msgid "use as timeframe image" msgstr "Verwendung als Zeitrahmenbild" @@ -3025,41 +3025,41 @@ msgstr "Es ist nicht möglich, diesen Zeitrahmen im Frontend abzurufen. Bitte be msgid "Cancelled" msgstr "Storniert" -#: src/Model/Timeframe.php:630 +#: src/Model/Timeframe.php:706 msgid "The start- and end-time of the timeframe can not be the same. Please check the full-day checkbox if you want users to be able to book the full day." msgstr "Die Start- und Endzeit des Zeitrahmens dürfen nicht gleich sein. Wenn der gesamte Tag buchbar sein soll muss die Option \"Ganzer Tag\" angewählt sein." #: src/Plugin.php:720 #: src/Plugin.php:735 -#: src/Wordpress/CustomPostType/Booking.php:936 +#: src/Wordpress/CustomPostType/Booking.php:949 msgid "CommonsBooking Bookings" msgstr "CommonsBooking Buchungen" -#: src/Wordpress/CustomPostType/Booking.php:532 +#: src/Wordpress/CustomPostType/Booking.php:545 msgid "Bookings not found" msgstr "Keine Buchungen gefunden" -#: src/Wordpress/CustomPostType/Booking.php:961 +#: src/Wordpress/CustomPostType/Booking.php:974 msgid "Booking start" msgstr "Buchungsbeginn" -#: src/Wordpress/CustomPostType/Booking.php:965 +#: src/Wordpress/CustomPostType/Booking.php:978 msgid "Booking end" msgstr "Buchungsende" -#: src/Wordpress/CustomPostType/Booking.php:969 +#: src/Wordpress/CustomPostType/Booking.php:982 msgid "Time of booking" msgstr "Buchungszeit" -#: src/Wordpress/CustomPostType/Booking.php:977 +#: src/Wordpress/CustomPostType/Booking.php:990 msgid "Booking code" msgstr "Buchungscode" -#: src/Wordpress/CustomPostType/Booking.php:993 +#: src/Wordpress/CustomPostType/Booking.php:1006 msgid "Time of cancellation" msgstr "Storniert am" -#: src/Wordpress/CustomPostType/Booking.php:997 +#: src/Wordpress/CustomPostType/Booking.php:1010 msgid "Admin booking by" msgstr "Adminbuchung von" @@ -3110,25 +3110,25 @@ msgid "If selected, days that are overbooked will be counted towards the maximum msgstr "Ist diese Option aktiviert, werden überbuchte Tage auf die maximale Anzahl buchbarer Tage angerechnet. Wenn diese Option deaktiviert ist, dann werden überbuchte Tage nicht zusätzlich gezählt und ermöglichen Buchungszeiträume die länger sind als die im Zeitrahmen konfigurierte maximale Anzahl an gebuchten Tagen." #. translators: first %s = timeframe-ID, second %s is timeframe post_title -#: src/Model/Timeframe.php:691 +#: src/Model/Timeframe.php:767 #, php-format msgid "See overlapping timeframe ID: %1$s %2$s" msgstr "Sich überschneidende Zeitrahmen ID: %1$s %2$s" -#: src/Model/Timeframe.php:790 +#: src/Model/Timeframe.php:866 msgid "Overlapping bookable timeframes are only allowed to have the same grid." msgstr "Sich überschneidende buchbare Zeiträume müssen das gleiche Raster haben." -#: src/Model/Timeframe.php:820 +#: src/Model/Timeframe.php:896 msgid "Overlapping bookable timeframes are not allowed to have the same weekdays." msgstr "Mehrere buchbare Zeiträume überschneiden sich in den definierten Wochentagen." -#: src/Model/Timeframe.php:832 +#: src/Model/Timeframe.php:908 msgid "Overlapping bookable timeframes are not allowed to have the same dates." msgstr "Mehrere buchbare Zeiträume überschneiden sich in dem definierten Datumsbereich." -#: src/Model/Timeframe.php:839 -#: src/Model/Timeframe.php:846 +#: src/Model/Timeframe.php:915 +#: src/Model/Timeframe.php:922 msgid "The other timeframe is overlapping with your weekly configuration." msgstr "Der andere Zeitrahmen überschneidet sich mit einer wöchentlichen Konfiguration." @@ -3168,7 +3168,7 @@ msgstr "Feiertage importieren" msgid "Select the year and state to import holidays for (as of now only German holidays are supported)" msgstr "Wählen Sie das Jahr und das Bundesland, für das Sie Feiertage importieren möchten (derzeit werden Feiertage in Deutschland unterstützt)" -#: src/Model/Timeframe.php:806 +#: src/Model/Timeframe.php:882 msgid "Daily repeated time periods are not allowed to overlap." msgstr "Täglich wiederholende Zeiträume dürfen sich nicht überschneiden." @@ -3318,11 +3318,11 @@ msgstr "Fehlende ID des Zeitrahmens" msgid "Error sending booking codes" msgstr "Fehler beim Senden von Buchungscodes" -#: src/View/BookingCodes.php:556 +#: src/View/BookingCodes.php:555 msgid "An unknown error occured" msgstr "Ein unbekannter Fehler ist aufgetreten" -#: src/View/BookingCodes.php:557 +#: src/View/BookingCodes.php:556 msgid "Email booking codes" msgstr "Buchungscodes per E-Mail versenden" @@ -3867,64 +3867,44 @@ msgstr "Bitte passe das Start- oder Enddatum an." msgid "Affected Bookings: %s" msgstr "Betroffene Buchungen: %s" -#: src/Model/Timeframe.php:555 +#: src/Model/Timeframe.php:631 msgid "Could not get item or location. Please set a valid item and location." msgstr "Artikel oder Standort konnte nicht gefunden werden. Bitte wähle einen gültigen Artikel oder Standort." -#: src/Model/Timeframe.php:564 +#: src/Model/Timeframe.php:640 msgid "Item or location is missing. Please set item and location." msgstr "Artikel oder Standort fehlen. Bitte Artikel und Standort festlegen." -#: src/Model/Timeframe.php:577 +#: src/Model/Timeframe.php:653 msgid "No dates selected. Please select at least one date." msgstr "Kein Datum ausgewählt. Bitte mindestens ein Datum auswählen." -#: src/Model/Timeframe.php:587 +#: src/Model/Timeframe.php:663 msgid "The same date was selected multiple times. Please select each date only once." msgstr "Das gleiche Datum wurde mehrere Male ausgewählt. Bitte ein Datum nur einmal auswählen." -#: src/Model/Timeframe.php:596 +#: src/Model/Timeframe.php:672 msgid "Startdate is missing. Please enter a start date to publish this timeframe." msgstr "Das Startdatum fehlt. Ein Startdatum muss eingegeben werden, damit der Zeitrahmen veröffentlicht werden kann." -#: src/View/Booking.php:581 -#: src/Wordpress/CustomPostType/Booking.php:864 +#: src/View/Booking.php:578 +#: src/Wordpress/CustomPostType/Booking.php:877 msgid "Submit booking" msgstr "Buchung abschicken" -#: src/Wordpress/CustomPostType/Booking.php:745 -msgid "" -"

Notice

In this view, you as an admin can create or modify existing bookings. Please use it with caution.
\n" -"\t\t\t\t

\n" -"\t\t\t\tPlease note: Only a few basic checks against existing bookings are performed. Please be wary of overlapping bookings.\n" -"

\n" -"\t\t\t\t" -msgstr "" -"

Achtung

In dieser Ansicht kannst du als Administrator*in Buchungen erstellen oder ändern. Bitte nutze die Möglichkeit mit Vorsicht.
\n" -"\t\t\t\t

\n" -"\t\t\t\tBitte beachte: Es werden nur grundlegende Überprüfungen gegen bestehende Buchungen durchgeführt. Bitte prüfe, ob es keine Buchungskonflikte mit anderen Buchungen gibt. \n" -"

\n" -"\t\t\t\t" - -#: src/Wordpress/CustomPostType/Booking.php:773 +#: src/Wordpress/CustomPostType/Booking.php:786 msgid "Book full day" msgstr "Ganztägig buchen" -#: src/Wordpress/CustomPostType/Booking.php:776 +#: src/Wordpress/CustomPostType/Booking.php:789 msgid "The booking should apply to the entire day(s)" msgstr "Die Buchung soll auf den gesamten Tag / die gesamten Tage angewendet werden" -#: src/Wordpress/CustomPostType/Booking.php:816 +#: src/Wordpress/CustomPostType/Booking.php:829 msgid "Valid booking code will be automatically retrieved for bookings that apply to the full day." msgstr "Ein gültiger Buchungscode wird automatisch für Buchungen, die sich auf den ganzen Tag beziehen, generiert." -#: src/Wordpress/CustomPostType/Booking.php:825 +#: src/Wordpress/CustomPostType/Booking.php:838 msgid "" "Here you must select the user for whom the booking is made.
\n" " If the booking was made by a user via frontend booking process, the user will be shown in this field.\n" @@ -3934,7 +3914,7 @@ msgstr "" " Wenn die Buchung von einem/einer Nutzenden über den Frontend-Buchungsprozess vorgenommen wurde, wird der/die Nutzende in diesem Feld angezeigt.\n" "
Hinweis: Der/die Nutzende erhält eine Buchungsbestätigung, sobald die Buchung abgeschickt wurde." -#: src/Wordpress/CustomPostType/Booking.php:865 +#: src/Wordpress/CustomPostType/Booking.php:878 msgid "This will create the specified booking and send out the booking confirmation email." msgstr "Dadurch wird die angegebene Buchung erstellt und die Buchungsbestätigung per E-Mail verschickt." @@ -4006,11 +3986,11 @@ msgstr "Buchungen bei Totalausfall nicht stornieren" msgid "If checked, bookings will not be cancelled if the item has broken down. The user will be notified and once the item becomes available again, the old bookings are still valid." msgstr "Wenn ausgewählt, werden Buchungen nicht storniert, wenn ein Totalausfall für den Artikel gesetzt ist. Der / die Benutzer*in wird benachrichtigt und sobald der Artikel wieder verfügbar ist, sind die alten Buchungen weiterhin gültig." -#: src/Wordpress/CustomPostType/Item.php:353 +#: src/Wordpress/CustomPostType/Item.php:363 msgid "Add custom title for filter" msgstr "Benutzerdefinierten Titel für Filtergruppe hinzufügen" -#: src/Wordpress/CustomPostType/Item.php:356 +#: src/Wordpress/CustomPostType/Item.php:366 msgid "Define name that should be used for the category if it is displayed in the map as a filter group. You can also use this to add custom HTML to the category name. When left empty, the defined name of the category will be used." msgstr "Legt den Namen fest, der für die Kategorie verwendet werden soll, wenn sie in der Karte als Teil der Filtergruppe angezeigt wird. Hier kann auch benutzerdefinierter HTML-Code zum Kategorienamen hinzugefügt werden. Bleibt der Eintrag leer, wird der Standard Name der Kategorie verwendet." @@ -4408,23 +4388,6 @@ msgstr "Wenn diese Option ausgewählt ist, ist die API ohne API-Schlüssel zugä msgid "You can define on or more API shares. Read the documentation for more information about API shares and configuration %1$sAPI documentation%2$s" msgstr "Du kannst ein oder mehrere API-Freigaben definieren. Weitere Informationen zu API-Freigaben und zur Konfiguration findest du in der %1$sAPI-Dokumentation%2$s" -#: src/Wordpress/CustomPostType/Booking.php:888 -#, php-format -msgid "" -"Bookings should be created via frontend booking calendar.
\n" -"\t\tAs an admin you can create bookings via this admin interface. Please be aware that admin bookings are not validated\n" -"\t\tand checked. Use this function with care.
\n" -"\t\tClick on preview to show booking details in frontend
\n" -"\t\tTo search and filter bookings please integrate the frontend booking list via shortcode. \n" -"\t\tSee here %1$sHow to display the booking list%2$s" -msgstr "" -"Buchungen sollten über den Frontend-Buchungskalender erstellt werden.
\r\n" -"\t\tAls Admin kannst du über diese Admin-Oberfläche Buchungen anlegen. Bitte beachte, dass Admin-Buchungen nicht validiert\r\n" -"\t\tund überprüft werden. Verwende diese Funktion mit Vorsicht.
\r\n" -"\t\tKlicke auf Vorschau, um die Buchungsdetails im Frontend anzuzeigen.
\r\n" -"\t\tZur Suche und Filterung von Buchungen, binde bitte die Frontend-Buchungsliste per Shortcode ein. \r\n" -"\t\tMehr dazu %1$sBinde die Buchungsliste im Frontend ein.%2$s" - #: src/Wordpress/CustomPostType/Location.php:540 #, php-format msgid "If selected, all not selected days in any bookable timeframe that is connected to this location can be overbooked. Read the documentation %1$sCreate Locations%2$s for more information." @@ -4450,25 +4413,17 @@ msgstr "" "\t\t\t\t%1$sWeitere Informationen in der Dokumentation%2$s\r\n" "\t\t\t\t" -#: src/Model/Booking.php:1024 -#~ msgid "Unconfirmed" -#~ msgstr "Unbestätigt" - -#: src/Model/Booking.php:1022 -#~ msgid "Confirmed" -#~ msgstr "Bestätigt" - #: includes/OptionsArray.php:1553 msgid "Enables users to copy a url for a dynamic iCalendar feed into their own digital calendars." -msgstr "" +msgstr "Erlaubt Nutzenden eine URL für einen dynamischen iCalendar in ihre eigenen digitalen Kalender zu kopieren." #: includes/OptionsArray.php:1567 msgid "This is the event title that will be given to bookings that do not belong to the current user. This is what the CB-Manager will see when they subscribe to the station calendar. You can use template tags here as well" -msgstr "" +msgstr "Dies ist der Titel für Kalendereinträge von Buchungen, die nicht dem aktuell angemeldeten Nutzenden zugeordnet sind. Solche Einträge sind für Personen mit der Rolle „CB-Manager“ sichtbar, wenn sie den Kalender einer Station abonnieren. Hier können auch Template-Tags genutzt werden." #: includes/OptionsArray.php:1574 msgid "This is the event description that will be given to bookings that do not belong to the current user. This is what the CB-Manager will see when they subscribe to the station calendar. You can use template tags here." -msgstr "" +msgstr "Dies ist die Beschreibung für Kalendereinträge von Buchungen, die nicht dem aktuell angemeldeten Nutzenden zugeordnet sind. Solche Einträge sind für Personen mit der Rolle „CB-Manager“ sichtbar, wenn sie den Kalender einer Station abonnieren. Hier können auch Template-Tags genutzt werden." #: includes/OptionsArray.php:1576 msgid "" @@ -4478,3 +4433,65 @@ msgid "" "Booking code: {{booking:formattedBookingCode}}\n" "User Email: {{user:user_email}}" msgstr "" +"\n" +"Abholung: {{booking:pickupDatetime}}\r\n" +"Rückgabe: {{booking:returnDatetime}}\r\n" +"Buchungscode: {{booking:formattedBookingCode}}\r\n" +"E-Mail des Nutzenden: {{user:user_email}}" + +#: src/Wordpress/CustomPostType/Item.php:287 +msgid "Exclude from API" +msgstr "In API verstecken" + +#: src/Wordpress/CustomPostType/Item.php:288 +msgid "When this box is checked, the item will not appear in any of the API shares." +msgstr "Wenn diese Box angewählt ist taucht der Artikel in keiner der API Freigaben auf." + +#: src/Wordpress/CustomPostType/Booking.php:758 +msgid "" +"

Notice

In this view, you as an admin can create or modify existing bookings. Please use it with caution.
\n" +"\t\t\t\t

\n" +"\t\t\t\tPlease note: Only a few basic checks against existing bookings are performed. Please be wary of overlapping bookings.\n" +"

\n" +"\t\t\t\t" +msgstr "" +"

Achtung

In dieser Ansicht kannst du als Administrator*in Buchungen erstellen oder ändern. Bitte nutze die Möglichkeit mit Vorsicht.
\r\n" +"\t\t\t\t

\r\n" +"\t\t\t\tBitte beachte:: Es werden nur grundlegende Überprüfungen gegen bestehende Buchungen durchgeführt. Bitte prüfe, ob es keine Buchungskonflikte mit anderen Buchungen gibt.\r\n" +"

\r\n" +"\t\t\t\t" + +#: src/Wordpress/CustomPostType/Booking.php:901 +#, php-format +msgid "" +"Bookings should be created via frontend booking calendar.
\n" +"\t\tAs an admin you can create bookings via this admin interface. Please be aware that admin bookings are not validated\n" +"\t\tand checked. Use this function with care.
\n" +"\t\tClick on preview to show booking details in frontend
\n" +"\t\tTo search and filter bookings please integrate the frontend booking list via shortcode.\n" +"\t\tSee here %1$sHow to display the booking list%2$s" +msgstr "" +"Buchungen sollten über den Buchungskalender im Frontend erstellt werden.
\r\n" +"\t\tAls Admin kannst du Buchungen über das Backend erstellen. Bitte beachte, dass die Buchungen dort nicht validiert werden.\r\n" +"\t\tNutze diese Funktion mit Vorsicht.
\r\n" +"\t\tKlicke auf den Vorschau Knopf, um die Details der Buchung im Frontend anzuzeigen.
\r\n" +"\t\tUm Buchungen zu filtern und durchsuchen, kann der Buchungsshortcode auf einer Frontend Seite eingefügt werden.\r\n" +"\t\tIn der Dokumentation %1$sBuchungsliste anzeigen%2$s" + +#: src/Wordpress/CustomPostType/Booking.php:304 +msgid "Invalid booking request. Please try again." +msgstr "Ungültige Buchungsanfrage. Bitte erneut versuchen." + +#: src/Model/Booking.php:1024 +#~ msgid "Unconfirmed" +#~ msgstr "Unbestätigt" + +#: src/Model/Booking.php:1022 +#~ msgid "Confirmed" +#~ msgstr "Bestätigt" diff --git a/languages/commonsbooking.pot b/languages/commonsbooking.pot index 1feb6de32..ccc305c2b 100644 --- a/languages/commonsbooking.pot +++ b/languages/commonsbooking.pot @@ -9,7 +9,7 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"POT-Creation-Date: 2026-04-09T19:23:59+00:00\n" +"POT-Creation-Date: 2026-06-17T13:53:30+00:00\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "X-Generator: WP-CLI 2.12.0\n" "X-Domain: commonsbooking\n" @@ -1299,71 +1299,71 @@ msgstr "" msgid "Every day" msgstr "" -#: src/CB/CB1UserFields.php:69 +#: src/CB/CB1UserFields.php:72 msgid "First Name" msgstr "" -#: src/CB/CB1UserFields.php:72 +#: src/CB/CB1UserFields.php:75 msgid "Please enter your first name" msgstr "" -#: src/CB/CB1UserFields.php:76 +#: src/CB/CB1UserFields.php:79 msgid "Last Name" msgstr "" -#: src/CB/CB1UserFields.php:79 +#: src/CB/CB1UserFields.php:82 msgid "Please enter your last name" msgstr "" -#: src/CB/CB1UserFields.php:83 +#: src/CB/CB1UserFields.php:86 msgid "Phone Number" msgstr "" -#: src/CB/CB1UserFields.php:86 +#: src/CB/CB1UserFields.php:89 msgid "Please enter your phone number" msgstr "" -#: src/CB/CB1UserFields.php:90 -#: src/CB/CB1UserFields.php:279 +#: src/CB/CB1UserFields.php:93 +#: src/CB/CB1UserFields.php:283 #: src/Wordpress/CustomPostType/Location.php:237 #: templates/booking-single.php:77 msgid "Address" msgstr "" -#: src/CB/CB1UserFields.php:93 +#: src/CB/CB1UserFields.php:96 msgid "Please enter your address" msgstr "" -#: src/CB/CB1UserFields.php:96 +#: src/CB/CB1UserFields.php:99 msgid "Terms and Conditions" msgstr "" -#: src/CB/CB1UserFields.php:99 +#: src/CB/CB1UserFields.php:102 msgid "I accept the terms & conditions" msgstr "" -#: src/CB/CB1UserFields.php:100 +#: src/CB/CB1UserFields.php:103 msgid "Please accept the terms & conditions" msgstr "" -#: src/CB/CB1UserFields.php:188 +#: src/CB/CB1UserFields.php:191 #, php-format msgid "Read the terms and services" msgstr "" -#: src/CB/CB1UserFields.php:267 +#: src/CB/CB1UserFields.php:271 msgid "Extra Fields" msgstr "" -#: src/CB/CB1UserFields.php:271 +#: src/CB/CB1UserFields.php:275 msgid "Phone number" msgstr "" -#: src/CB/CB1UserFields.php:287 +#: src/CB/CB1UserFields.php:291 msgid "Terms and conditions" msgstr "" -#: src/CB/CB1UserFields.php:297 +#: src/CB/CB1UserFields.php:301 msgid "Accepted Terms & Conditions" msgstr "" @@ -1476,7 +1476,7 @@ msgstr "" #. translators: %s = date in WordPress defined format #: src/Model/Booking.php:437 -#: src/Model/Timeframe.php:260 +#: src/Model/Timeframe.php:336 #, php-format msgid "on %s" msgstr "" @@ -1484,7 +1484,7 @@ msgstr "" #. translators: %1 = startdate, %2 = enddate in WordPress defined format #. translators: %1$s = startdate, second %2$s = enddate in WordPress defined format #: src/Model/Booking.php:440 -#: src/Model/Timeframe.php:275 +#: src/Model/Timeframe.php:351 #, php-format msgid "from %1$s until %2$s" msgstr "" @@ -1559,93 +1559,93 @@ msgstr "" msgid "Please contact the contact persons at the location directly if you have any questions regarding collection or return:" msgstr "" -#: src/Model/Restriction.php:244 -#: src/Model/Restriction.php:268 +#: src/Model/Restriction.php:248 +#: src/Model/Restriction.php:275 msgid "Not set" msgstr "" -#: src/Model/Timeframe.php:255 +#: src/Model/Timeframe.php:331 msgid "Available here" msgstr "" #. translators: %s = date in WordPress defined format -#: src/Model/Timeframe.php:265 +#: src/Model/Timeframe.php:341 #, php-format msgid "from %s" msgstr "" -#: src/Model/Timeframe.php:269 +#: src/Model/Timeframe.php:345 msgid "permanently" msgstr "" #. translators: %s = enddate in WordPress defined format -#: src/Model/Timeframe.php:282 +#: src/Model/Timeframe.php:358 #, php-format msgid "until %s" msgstr "" -#: src/Model/Timeframe.php:555 +#: src/Model/Timeframe.php:631 msgid "Could not get item or location. Please set a valid item and location." msgstr "" -#: src/Model/Timeframe.php:564 +#: src/Model/Timeframe.php:640 msgid "Item or location is missing. Please set item and location." msgstr "" -#: src/Model/Timeframe.php:577 +#: src/Model/Timeframe.php:653 msgid "No dates selected. Please select at least one date." msgstr "" -#: src/Model/Timeframe.php:587 +#: src/Model/Timeframe.php:663 msgid "The same date was selected multiple times. Please select each date only once." msgstr "" -#: src/Model/Timeframe.php:596 +#: src/Model/Timeframe.php:672 msgid "Startdate is missing. Please enter a start date to publish this timeframe." msgstr "" -#: src/Model/Timeframe.php:610 +#: src/Model/Timeframe.php:686 msgid "A pickup time but no return time has been set. Please set the return time." msgstr "" -#: src/Model/Timeframe.php:620 +#: src/Model/Timeframe.php:696 msgid "End date is before start date. Please set a valid end date." msgstr "" -#: src/Model/Timeframe.php:630 +#: src/Model/Timeframe.php:706 msgid "The start- and end-time of the timeframe can not be the same. Please check the full-day checkbox if you want users to be able to book the full day." msgstr "" #. translators: %1$s = timeframe-ID, %2$s is timeframe post_title -#: src/Model/Timeframe.php:655 +#: src/Model/Timeframe.php:731 #, php-format msgid "Item is already bookable at another location within the same date range. See other timeframe ID: %1$s: %2$s" msgstr "" #. translators: first %s = timeframe-ID, second %s is timeframe post_title -#: src/Model/Timeframe.php:691 +#: src/Model/Timeframe.php:767 #, php-format msgid "See overlapping timeframe ID: %1$s %2$s" msgstr "" -#: src/Model/Timeframe.php:790 +#: src/Model/Timeframe.php:866 msgid "Overlapping bookable timeframes are only allowed to have the same grid." msgstr "" -#: src/Model/Timeframe.php:806 +#: src/Model/Timeframe.php:882 msgid "Daily repeated time periods are not allowed to overlap." msgstr "" -#: src/Model/Timeframe.php:820 +#: src/Model/Timeframe.php:896 msgid "Overlapping bookable timeframes are not allowed to have the same weekdays." msgstr "" -#: src/Model/Timeframe.php:832 +#: src/Model/Timeframe.php:908 msgid "Overlapping bookable timeframes are not allowed to have the same dates." msgstr "" -#: src/Model/Timeframe.php:839 -#: src/Model/Timeframe.php:846 +#: src/Model/Timeframe.php:915 +#: src/Model/Timeframe.php:922 msgid "The other timeframe is overlapping with your weekly configuration." msgstr "" @@ -1666,7 +1666,7 @@ msgstr "" #: src/Plugin.php:720 #: src/Plugin.php:735 -#: src/Wordpress/CustomPostType/Booking.php:936 +#: src/Wordpress/CustomPostType/Booking.php:949 msgid "CommonsBooking Bookings" msgstr "" @@ -1992,14 +1992,14 @@ msgid "" msgstr "" #: src/View/Admin/Filter.php:58 -#: src/Wordpress/CustomPostType/Booking.php:779 +#: src/Wordpress/CustomPostType/Booking.php:792 #: src/Wordpress/CustomPostType/Restriction.php:451 #: src/Wordpress/CustomPostType/Timeframe.php:678 msgid "Start date" msgstr "" #: src/View/Admin/Filter.php:62 -#: src/Wordpress/CustomPostType/Booking.php:796 +#: src/Wordpress/CustomPostType/Booking.php:809 #: src/Wordpress/CustomPostType/Restriction.php:461 #: src/Wordpress/CustomPostType/Timeframe.php:702 msgid "End date" @@ -2024,7 +2024,7 @@ msgstr "" #: src/View/Booking.php:196 #: src/View/MassOperations.php:38 -#: src/Wordpress/CustomPostType/Booking.php:446 +#: src/Wordpress/CustomPostType/Booking.php:459 #: src/Wordpress/CustomPostType/Timeframe.php:97 #: templates/shortcode-bookings.php:74 msgid "User" @@ -2032,7 +2032,7 @@ msgstr "" #: src/View/Booking.php:200 #: src/View/MassOperations.php:42 -#: src/Wordpress/CustomPostType/Booking.php:973 +#: src/Wordpress/CustomPostType/Booking.php:986 #: src/Wordpress/CustomPostType/Restriction.php:477 msgid "Status" msgstr "" @@ -2041,8 +2041,8 @@ msgstr "" msgid "Code" msgstr "" -#: src/View/Booking.php:581 -#: src/Wordpress/CustomPostType/Booking.php:864 +#: src/View/Booking.php:578 +#: src/Wordpress/CustomPostType/Booking.php:877 msgid "Submit booking" msgstr "" @@ -2135,22 +2135,22 @@ msgstr "" msgid "Error sending booking codes" msgstr "" -#: src/View/BookingCodes.php:556 +#: src/View/BookingCodes.php:555 msgid "An unknown error occured" msgstr "" -#: src/View/BookingCodes.php:557 +#: src/View/BookingCodes.php:556 msgid "Email booking codes" msgstr "" -#: src/View/Calendar.php:177 +#: src/View/Calendar.php:181 msgid "No items found." msgstr "" -#: src/View/Calendar.php:233 -#: src/Wordpress/CustomPostType/Booking.php:447 -#: src/Wordpress/CustomPostType/Booking.php:761 -#: src/Wordpress/CustomPostType/Booking.php:989 +#: src/View/Calendar.php:237 +#: src/Wordpress/CustomPostType/Booking.php:460 +#: src/Wordpress/CustomPostType/Booking.php:774 +#: src/Wordpress/CustomPostType/Booking.php:1002 #: src/Wordpress/CustomPostType/Item.php:117 #: src/Wordpress/CustomPostType/Restriction.php:48 #: src/Wordpress/CustomPostType/Restriction.php:438 @@ -2159,10 +2159,10 @@ msgstr "" msgid "Item" msgstr "" -#: src/View/Calendar.php:234 -#: src/Wordpress/CustomPostType/Booking.php:448 -#: src/Wordpress/CustomPostType/Booking.php:767 -#: src/Wordpress/CustomPostType/Booking.php:985 +#: src/View/Calendar.php:238 +#: src/Wordpress/CustomPostType/Booking.php:461 +#: src/Wordpress/CustomPostType/Booking.php:780 +#: src/Wordpress/CustomPostType/Booking.php:998 #: src/Wordpress/CustomPostType/Location.php:139 #: src/Wordpress/CustomPostType/Restriction.php:49 #: src/Wordpress/CustomPostType/Restriction.php:431 @@ -2362,150 +2362,154 @@ msgid "Your reservation has expired, please try to book again" msgstr "" #: src/Wordpress/CustomPostType/Booking.php:291 -#: src/Wordpress/CustomPostType/Booking.php:323 +#: src/Wordpress/CustomPostType/Booking.php:336 msgid "There is already a booking in this time-range. This notice may also appear if there is an unconfirmed booking in the requested period. Unconfirmed bookings are deleted after about 10 minutes. Please try again in a few minutes." msgstr "" -#: src/Wordpress/CustomPostType/Booking.php:307 +#: src/Wordpress/CustomPostType/Booking.php:304 +msgid "Invalid booking request. Please try again." +msgstr "" + +#: src/Wordpress/CustomPostType/Booking.php:320 msgid "Booking canceled." msgstr "" -#: src/Wordpress/CustomPostType/Booking.php:331 -#: src/Wordpress/CustomPostType/Booking.php:524 -#: src/Wordpress/CustomPostType/Booking.php:712 +#: src/Wordpress/CustomPostType/Booking.php:344 +#: src/Wordpress/CustomPostType/Booking.php:537 +#: src/Wordpress/CustomPostType/Booking.php:725 #: src/Wordpress/CustomPostType/Timeframe.php:132 msgid "Booking" msgstr "" -#: src/Wordpress/CustomPostType/Booking.php:375 +#: src/Wordpress/CustomPostType/Booking.php:388 msgid "There was an error while saving the booking. Please try again. Thrown error:" msgstr "" -#: src/Wordpress/CustomPostType/Booking.php:392 +#: src/Wordpress/CustomPostType/Booking.php:405 msgid "There was an error while saving the booking. Please try again. Resulting WP_ERROR: " msgstr "" -#: src/Wordpress/CustomPostType/Booking.php:449 +#: src/Wordpress/CustomPostType/Booking.php:462 msgid "Bookingdate" msgstr "" -#: src/Wordpress/CustomPostType/Booking.php:450 +#: src/Wordpress/CustomPostType/Booking.php:463 #: src/Wordpress/CustomPostType/Restriction.php:50 #: src/Wordpress/CustomPostType/Timeframe.php:101 #: src/Wordpress/CustomPostType/Timeframe.php:758 msgid "Start Date" msgstr "" -#: src/Wordpress/CustomPostType/Booking.php:451 +#: src/Wordpress/CustomPostType/Booking.php:464 #: src/Wordpress/CustomPostType/Restriction.php:51 #: src/Wordpress/CustomPostType/Timeframe.php:102 msgid "End Date" msgstr "" -#: src/Wordpress/CustomPostType/Booking.php:452 +#: src/Wordpress/CustomPostType/Booking.php:465 msgid "Booking Status" msgstr "" -#: src/Wordpress/CustomPostType/Booking.php:453 -#: src/Wordpress/CustomPostType/Booking.php:981 +#: src/Wordpress/CustomPostType/Booking.php:466 +#: src/Wordpress/CustomPostType/Booking.php:994 #: src/Wordpress/CustomPostType/Timeframe.php:448 msgid "Comment" msgstr "" -#: src/Wordpress/CustomPostType/Booking.php:523 +#: src/Wordpress/CustomPostType/Booking.php:536 #: templates/dashboard-index.php:45 msgid "Bookings" msgstr "" -#: src/Wordpress/CustomPostType/Booking.php:525 +#: src/Wordpress/CustomPostType/Booking.php:538 #: src/Wordpress/CustomPostType/Item.php:118 #: src/Wordpress/CustomPostType/Location.php:140 #: src/Wordpress/CustomPostType/Restriction.php:307 -#: src/Wordpress/CustomPostType/Timeframe.php:1152 +#: src/Wordpress/CustomPostType/Timeframe.php:1153 msgid "Add new" msgstr "" -#: src/Wordpress/CustomPostType/Booking.php:526 -#: src/Wordpress/CustomPostType/Booking.php:528 +#: src/Wordpress/CustomPostType/Booking.php:539 +#: src/Wordpress/CustomPostType/Booking.php:541 msgid "Add new booking" msgstr "" -#: src/Wordpress/CustomPostType/Booking.php:527 -#: src/Wordpress/CustomPostType/Booking.php:743 +#: src/Wordpress/CustomPostType/Booking.php:540 +#: src/Wordpress/CustomPostType/Booking.php:756 msgid "Edit booking" msgstr "" -#: src/Wordpress/CustomPostType/Booking.php:529 +#: src/Wordpress/CustomPostType/Booking.php:542 msgid "Show booking" msgstr "" -#: src/Wordpress/CustomPostType/Booking.php:530 +#: src/Wordpress/CustomPostType/Booking.php:543 msgid "Show bookings" msgstr "" -#: src/Wordpress/CustomPostType/Booking.php:531 +#: src/Wordpress/CustomPostType/Booking.php:544 msgid "Search bookings" msgstr "" -#: src/Wordpress/CustomPostType/Booking.php:532 +#: src/Wordpress/CustomPostType/Booking.php:545 msgid "Bookings not found" msgstr "" -#: src/Wordpress/CustomPostType/Booking.php:533 +#: src/Wordpress/CustomPostType/Booking.php:546 msgid "No bookings found in trash" msgstr "" -#: src/Wordpress/CustomPostType/Booking.php:534 +#: src/Wordpress/CustomPostType/Booking.php:547 msgid "Parent bookings:" msgstr "" -#: src/Wordpress/CustomPostType/Booking.php:535 +#: src/Wordpress/CustomPostType/Booking.php:548 msgid "All bookings" msgstr "" -#: src/Wordpress/CustomPostType/Booking.php:536 -#: src/Wordpress/CustomPostType/Timeframe.php:1163 +#: src/Wordpress/CustomPostType/Booking.php:549 +#: src/Wordpress/CustomPostType/Timeframe.php:1164 msgid "Timeframe archive" msgstr "" -#: src/Wordpress/CustomPostType/Booking.php:537 -#: src/Wordpress/CustomPostType/Timeframe.php:1164 +#: src/Wordpress/CustomPostType/Booking.php:550 +#: src/Wordpress/CustomPostType/Timeframe.php:1165 msgid "Timeframe attributes" msgstr "" -#: src/Wordpress/CustomPostType/Booking.php:538 +#: src/Wordpress/CustomPostType/Booking.php:551 msgid "Add to booking" msgstr "" -#: src/Wordpress/CustomPostType/Booking.php:539 +#: src/Wordpress/CustomPostType/Booking.php:552 msgid "Added to booking" msgstr "" -#: src/Wordpress/CustomPostType/Booking.php:540 -#: src/Wordpress/CustomPostType/Timeframe.php:1167 +#: src/Wordpress/CustomPostType/Booking.php:553 +#: src/Wordpress/CustomPostType/Timeframe.php:1168 msgid "Timeframe image" msgstr "" -#: src/Wordpress/CustomPostType/Booking.php:541 +#: src/Wordpress/CustomPostType/Booking.php:554 msgid "set booking image" msgstr "" -#: src/Wordpress/CustomPostType/Booking.php:542 +#: src/Wordpress/CustomPostType/Booking.php:555 msgid "remove booking image" msgstr "" -#: src/Wordpress/CustomPostType/Booking.php:543 +#: src/Wordpress/CustomPostType/Booking.php:556 msgid "use as booking image" msgstr "" -#: src/Wordpress/CustomPostType/Booking.php:544 -#: src/Wordpress/CustomPostType/Timeframe.php:1150 -#: src/Wordpress/CustomPostType/Timeframe.php:1171 +#: src/Wordpress/CustomPostType/Booking.php:557 +#: src/Wordpress/CustomPostType/Timeframe.php:1151 +#: src/Wordpress/CustomPostType/Timeframe.php:1172 #: templates/dashboard-index.php:37 msgid "Timeframes" msgstr "" -#: src/Wordpress/CustomPostType/Booking.php:745 +#: src/Wordpress/CustomPostType/Booking.php:758 msgid "" "

Notice

In this view, you as an admin can create or modify existing bookings. Please use it with caution.
\n" "\t\t\t\t

\n" "\t\t\t\tPlease note: Only a few basic checks against existing bookings are performed. Please be wary of overlapping bookings.\n" -"

\n" +"

\n" "\t\t\t\t" msgstr "" -#: src/Wordpress/CustomPostType/Booking.php:773 +#: src/Wordpress/CustomPostType/Booking.php:786 msgid "Book full day" msgstr "" -#: src/Wordpress/CustomPostType/Booking.php:776 +#: src/Wordpress/CustomPostType/Booking.php:789 msgid "The booking should apply to the entire day(s)" msgstr "" -#: src/Wordpress/CustomPostType/Booking.php:780 +#: src/Wordpress/CustomPostType/Booking.php:793 msgid "Set the start date. You must set the time to 00:00 if you want to book the full day " msgstr "" -#: src/Wordpress/CustomPostType/Booking.php:797 +#: src/Wordpress/CustomPostType/Booking.php:810 msgid "Set the end date. You must set time to 23:59 if you want to book the full day" msgstr "" -#: src/Wordpress/CustomPostType/Booking.php:813 +#: src/Wordpress/CustomPostType/Booking.php:826 #: templates/booking-single.php:59 msgid "Booking Code" msgstr "" -#: src/Wordpress/CustomPostType/Booking.php:816 +#: src/Wordpress/CustomPostType/Booking.php:829 msgid "Valid booking code will be automatically retrieved for bookings that apply to the full day." msgstr "" -#: src/Wordpress/CustomPostType/Booking.php:819 +#: src/Wordpress/CustomPostType/Booking.php:832 msgid "Booking User" msgstr "" -#: src/Wordpress/CustomPostType/Booking.php:825 +#: src/Wordpress/CustomPostType/Booking.php:838 msgid "" "Here you must select the user for whom the booking is made.
\n" " If the booking was made by a user via frontend booking process, the user will be shown in this field.\n" "
Notice:The user will receive a booking confirmation as soon as the booking is submitted." msgstr "" -#: src/Wordpress/CustomPostType/Booking.php:834 +#: src/Wordpress/CustomPostType/Booking.php:847 msgid "Admin Booking User" msgstr "" -#: src/Wordpress/CustomPostType/Booking.php:845 +#: src/Wordpress/CustomPostType/Booking.php:858 msgid "This is the admin user who created or modified this booking." msgstr "" -#: src/Wordpress/CustomPostType/Booking.php:852 +#: src/Wordpress/CustomPostType/Booking.php:865 msgid "External comment" msgstr "" -#: src/Wordpress/CustomPostType/Booking.php:853 +#: src/Wordpress/CustomPostType/Booking.php:866 msgid "This comment can be seen by users in booking details. It can be set by users during the booking confirmation process if comments are enabled in settings." msgstr "" -#: src/Wordpress/CustomPostType/Booking.php:858 +#: src/Wordpress/CustomPostType/Booking.php:871 #: templates/booking-single.php:131 msgid "Internal comment" msgstr "" -#: src/Wordpress/CustomPostType/Booking.php:859 +#: src/Wordpress/CustomPostType/Booking.php:872 msgid "This internal comment can only be seen in the backend by privileged users like admins or cb-managers" msgstr "" -#: src/Wordpress/CustomPostType/Booking.php:865 +#: src/Wordpress/CustomPostType/Booking.php:878 msgid "This will create the specified booking and send out the booking confirmation email." msgstr "" -#: src/Wordpress/CustomPostType/Booking.php:888 +#: src/Wordpress/CustomPostType/Booking.php:901 #, php-format msgid "" "Bookings should be created via frontend booking calendar.
\n" "\t\tAs an admin you can create bookings via this admin interface. Please be aware that admin bookings are not validated\n" "\t\tand checked. Use this function with care.
\n" "\t\tClick on preview to show booking details in frontend
\n" -"\t\tTo search and filter bookings please integrate the frontend booking list via shortcode. \n" +"\t\tTo search and filter bookings please integrate the frontend booking list via shortcode.\n" "\t\tSee here %1$sHow to display the booking list%2$s" msgstr "" -#: src/Wordpress/CustomPostType/Booking.php:961 +#: src/Wordpress/CustomPostType/Booking.php:974 msgid "Booking start" msgstr "" -#: src/Wordpress/CustomPostType/Booking.php:965 +#: src/Wordpress/CustomPostType/Booking.php:978 msgid "Booking end" msgstr "" -#: src/Wordpress/CustomPostType/Booking.php:969 +#: src/Wordpress/CustomPostType/Booking.php:982 msgid "Time of booking" msgstr "" -#: src/Wordpress/CustomPostType/Booking.php:977 +#: src/Wordpress/CustomPostType/Booking.php:990 msgid "Booking code" msgstr "" -#: src/Wordpress/CustomPostType/Booking.php:993 +#: src/Wordpress/CustomPostType/Booking.php:1006 msgid "Time of cancellation" msgstr "" -#: src/Wordpress/CustomPostType/Booking.php:997 +#: src/Wordpress/CustomPostType/Booking.php:1010 msgid "Admin booking by" msgstr "" @@ -2720,20 +2724,28 @@ msgstr "" msgid "Email addresses to which notifications about a change of item status (restriction, breakdown) shall be sent. You can enter multiple addresses separated by commas." msgstr "" -#: src/Wordpress/CustomPostType/Item.php:293 +#: src/Wordpress/CustomPostType/Item.php:287 +msgid "Exclude from API" +msgstr "" + +#: src/Wordpress/CustomPostType/Item.php:288 +msgid "When this box is checked, the item will not appear in any of the API shares." +msgstr "" + +#: src/Wordpress/CustomPostType/Item.php:303 msgid "Item Meta-Data" msgstr "" -#: src/Wordpress/CustomPostType/Item.php:324 -#: src/Wordpress/CustomPostType/Item.php:344 +#: src/Wordpress/CustomPostType/Item.php:334 +#: src/Wordpress/CustomPostType/Item.php:354 msgid "Item Category" msgstr "" -#: src/Wordpress/CustomPostType/Item.php:353 +#: src/Wordpress/CustomPostType/Item.php:363 msgid "Add custom title for filter" msgstr "" -#: src/Wordpress/CustomPostType/Item.php:356 +#: src/Wordpress/CustomPostType/Item.php:366 msgid "Define name that should be used for the category if it is displayed in the map as a filter group. You can also use this to add custom HTML to the category name. When left empty, the defined name of the category will be used." msgstr "" @@ -3643,7 +3655,7 @@ msgid "Select Dates:" msgstr "" #: src/Wordpress/CustomPostType/Timeframe.php:420 -#: src/Wordpress/CustomPostType/Timeframe.php:1151 +#: src/Wordpress/CustomPostType/Timeframe.php:1152 msgid "Timeframe" msgstr "" @@ -3928,60 +3940,60 @@ msgstr "" msgid "Orphaned bookings found, can migrate. Click here to migrate " msgstr "" -#: src/Wordpress/CustomPostType/Timeframe.php:1153 -#: src/Wordpress/CustomPostType/Timeframe.php:1155 +#: src/Wordpress/CustomPostType/Timeframe.php:1154 +#: src/Wordpress/CustomPostType/Timeframe.php:1156 msgid "Add new timeframe" msgstr "" -#: src/Wordpress/CustomPostType/Timeframe.php:1154 +#: src/Wordpress/CustomPostType/Timeframe.php:1155 msgid "Edit timeframe" msgstr "" -#: src/Wordpress/CustomPostType/Timeframe.php:1156 +#: src/Wordpress/CustomPostType/Timeframe.php:1157 msgid "Show timeframe" msgstr "" -#: src/Wordpress/CustomPostType/Timeframe.php:1157 +#: src/Wordpress/CustomPostType/Timeframe.php:1158 msgid "Show timeframes" msgstr "" -#: src/Wordpress/CustomPostType/Timeframe.php:1158 +#: src/Wordpress/CustomPostType/Timeframe.php:1159 msgid "Search timeframes" msgstr "" -#: src/Wordpress/CustomPostType/Timeframe.php:1159 +#: src/Wordpress/CustomPostType/Timeframe.php:1160 msgid "Timeframes not found" msgstr "" -#: src/Wordpress/CustomPostType/Timeframe.php:1160 +#: src/Wordpress/CustomPostType/Timeframe.php:1161 msgid "No timeframes found in trash" msgstr "" -#: src/Wordpress/CustomPostType/Timeframe.php:1161 +#: src/Wordpress/CustomPostType/Timeframe.php:1162 msgid "Parent timeframes:" msgstr "" -#: src/Wordpress/CustomPostType/Timeframe.php:1162 +#: src/Wordpress/CustomPostType/Timeframe.php:1163 msgid "All timeframes" msgstr "" -#: src/Wordpress/CustomPostType/Timeframe.php:1165 +#: src/Wordpress/CustomPostType/Timeframe.php:1166 msgid "Add to timeframe" msgstr "" -#: src/Wordpress/CustomPostType/Timeframe.php:1166 +#: src/Wordpress/CustomPostType/Timeframe.php:1167 msgid "Added to timeframe" msgstr "" -#: src/Wordpress/CustomPostType/Timeframe.php:1168 +#: src/Wordpress/CustomPostType/Timeframe.php:1169 msgid "set timeframe image" msgstr "" -#: src/Wordpress/CustomPostType/Timeframe.php:1169 +#: src/Wordpress/CustomPostType/Timeframe.php:1170 msgid "remove timeframe image" msgstr "" -#: src/Wordpress/CustomPostType/Timeframe.php:1170 +#: src/Wordpress/CustomPostType/Timeframe.php:1171 msgid "use as timeframe image" msgstr "" diff --git a/package-lock.json b/package-lock.json index 3433b16f0..14d30577f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,18 +17,18 @@ "leaflet.markercluster": "^1.5.0", "moment": "^2.19.4", "search-insights": "^2.17.3", - "select2": "^4.0.13", + "select2": "^4.1.0", "shufflejs": "^6.1.1", "spin.js": "^2.3.2", - "vue": "^3.5.30" + "vue": "^3.5.38" }, "devDependencies": { - "@babel/preset-env": "^7.29.2", - "@wordpress/env": "^11.0.0", + "@babel/preset-env": "^7.29.7", + "@wordpress/env": "^11.10.0", "commons-api": "git+https://github.com/wielebenwir/commons-api.git", - "cypress": "^15.13.1", + "cypress": "^15.18.0", "editorconfig": "^3.0.2", - "grunt": "^1.6.1", + "grunt": "^1.6.2", "grunt-babel": "^8.0.0", "grunt-cli": "^1.5.0", "grunt-contrib-clean": "^2.0.1", @@ -41,8 +41,8 @@ "grunt-dart-sass": "^2.0.1", "matchdep": "^2.0.0", "medium-zoom": "^1.1.0", - "prettier": "^3.3.3", - "sass": "^1.97.3", + "prettier": "^3.8.4", + "sass": "^1.101.0", "vitepress": "^1.6.4" } }, @@ -285,13 +285,13 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", + "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -300,9 +300,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", - "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", "dev": true, "license": "MIT", "engines": { @@ -346,14 +346,14 @@ "dev": true }, "node_modules/@babel/generator": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.0.tgz", - "integrity": "sha512-vSH118/wwM/pLR38g/Sgk05sNtro6TlTJKuiMXDaZqPUfjTFcudpCOt00IhOfj+1BFAX+UFAlzCU+6WXr3GLFQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.0", - "@babel/types": "^7.29.0", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -363,27 +363,27 @@ } }, "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", - "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.29.7.tgz", + "integrity": "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.27.3" + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", - "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.28.6", - "@babel/helper-validator-option": "^7.27.1", + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" @@ -393,18 +393,18 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.6.tgz", - "integrity": "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.7.tgz", + "integrity": "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-member-expression-to-functions": "^7.28.5", - "@babel/helper-optimise-call-expression": "^7.27.1", - "@babel/helper-replace-supers": "^7.28.6", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/traverse": "^7.28.6", + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/traverse": "^7.29.7", "semver": "^6.3.1" }, "engines": { @@ -415,13 +415,13 @@ } }, "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.28.5.tgz", - "integrity": "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.29.7.tgz", + "integrity": "sha512-907Uymvqgg1dwUA+7IGwFAOSYzQOuzPXKNJ1yxzwPffzkYFg2q2eHi1fIOs6sXkG9NbIUMunnUlkYsfRFNvomg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-annotate-as-pure": "^7.29.7", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, @@ -450,9 +450,9 @@ } }, "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", "dev": true, "license": "MIT", "engines": { @@ -460,43 +460,43 @@ } }, "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", - "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.29.7.tgz", + "integrity": "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.28.5", - "@babel/types": "^7.28.5" + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-imports": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", - "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", - "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.28.6" + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -506,22 +506,22 @@ } }, "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", - "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.29.7.tgz", + "integrity": "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.27.1" + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", - "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", "dev": true, "license": "MIT", "engines": { @@ -529,15 +529,15 @@ } }, "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz", - "integrity": "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.29.7.tgz", + "integrity": "sha512-16AMiW26DbXWBbr3B8wNozKM0ydMLB892vaOaJW/fPJdnT8vJk5sdkQcU/isqUxyCE0cEoa8wZOcbgDuC4b6Og==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", - "@babel/helper-wrap-function": "^7.27.1", - "@babel/traverse": "^7.27.1" + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-wrap-function": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -547,15 +547,15 @@ } }, "node_modules/@babel/helper-replace-supers": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz", - "integrity": "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.29.7.tgz", + "integrity": "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-member-expression-to-functions": "^7.28.5", - "@babel/helper-optimise-call-expression": "^7.27.1", - "@babel/traverse": "^7.28.6" + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -565,41 +565,41 @@ } }, "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", - "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.29.7.tgz", + "integrity": "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", "dev": true, "license": "MIT", "engines": { @@ -607,15 +607,15 @@ } }, "node_modules/@babel/helper-wrap-function": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.28.6.tgz", - "integrity": "sha512-z+PwLziMNBeSQJonizz2AGnndLsP2DeGHIxDAn+wdHOGuo4Fo1x1HBPPXeE9TAOPHNNWQKCSlA2VZyYyyibDnQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.29.7.tgz", + "integrity": "sha512-iES0Skag9ERIF68aXadpO6dbXa03mNWK3sEqJaMnLNs/eC3l0lkImdfoy6Y09/SfkpawdAB4RjQ7PVA7TcVGdw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -636,12 +636,12 @@ } }, "node_modules/@babel/parser": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", - "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", "license": "MIT", "dependencies": { - "@babel/types": "^7.29.0" + "@babel/types": "^7.29.7" }, "bin": { "parser": "bin/babel-parser.js" @@ -651,14 +651,14 @@ } }, "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.28.5.tgz", - "integrity": "sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.29.7.tgz", + "integrity": "sha512-j8SrR0zLZrRsC09DlszEx8FpMiwukKffYXMK0d5LmOglO7vGG6sz/BR/20yHqWH+Lnn31JTt2PE3hIWNgM2J6w==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.28.5" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -668,13 +668,13 @@ } }, "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz", - "integrity": "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.29.7.tgz", + "integrity": "sha512-r8j8escF+U2FUHo0KOhPUdMzUO+jp9fInva6+ACVAF3Y97Ev+5iNZwiqTghmzNeWwDkOPlYuTcfb1vDaoZKmAQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -684,13 +684,30 @@ } }, "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz", - "integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.29.7.tgz", + "integrity": "sha512-GE1TFSiuFeGsCxmYXZl8HwoPrVlwe4rHPFE8weieGKZqnDORK+Ar3vgWMgW+AOxQ6/2TgLSKx9p6W7O4rC6qgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array/-/plugin-bugfix-safari-rest-destructuring-rhs-array-7.29.7.tgz", + "integrity": "sha512-oBNVCvnO5tND+xSopWvV8WNGfpTfgP4Zr/YXXSj8zfmcPktp5Ku/aZlsIowgSD4fjmgHn6sGmB9APVsU5zOdhA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -700,15 +717,15 @@ } }, "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz", - "integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.29.7.tgz", + "integrity": "sha512-QQt9qKHZ2sg/kivaLr7lnQr8HVrQDdBNSfCsTjiDxRuX/K5ORyKq+Bu8Xr0cDE3Dfkv0cw28Ve0EKyKMvulkOw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/plugin-transform-optional-chaining": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/plugin-transform-optional-chaining": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -718,14 +735,14 @@ } }, "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.6.tgz", - "integrity": "sha512-a0aBScVTlNaiUe35UtfxAN7A/tehvvG4/ByO6+46VPKTRSlfnAFsgKy0FUh+qAkQrDTmhDkT+IBOKlOoMUxQ0g==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.29.7.tgz", + "integrity": "sha512-pn6QacGLgvCcwc+syUhKE/qSjV2D1IHDB84RNxWYSt1mW3K/SCtjinZ2p0cETJxAWBjPy3K/1lHwG5BjjPxNlw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/traverse": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -747,13 +764,13 @@ } }, "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.28.6.tgz", - "integrity": "sha512-pSJUpFHdx9z5nqTSirOCMtYVP2wFgoWhP0p3g8ONK/4IHhLIBd0B9NYqAvIUAhq+OkhO4VM1tENCt0cjlsNShw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.29.7.tgz", + "integrity": "sha512-/An1OCBN93thpBAGyfsK2pcf0jvju1SAtKkL2Ny++B5Sy6sqgzXDQH1cZxWbF96Wuk+bn41MDA9bLd4VVAw6rw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -763,13 +780,13 @@ } }, "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", - "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.29.7.tgz", + "integrity": "sha512-zGYcYfq/WmZ4V+kBIXQon9dSSc8ircGZqw9ZaNhhGj9nZkeBu1jHLBDQqYYi5WA9uawvA2sIMbry2nCFhf5Djg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -795,13 +812,13 @@ } }, "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", - "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.29.7.tgz", + "integrity": "sha512-N7zArUXWzAMzm+/N0uPBeVB3Fam5lMxtUwMmDK5f/IBBS7a7p1qeUoxd/6CckXoxUdgsntq1Dh8xNW06maZbDQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -811,15 +828,15 @@ } }, "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.0.tgz", - "integrity": "sha512-va0VdWro4zlBr2JsXC+ofCPB2iG12wPtVGTWFx2WLDOM3nYQZZIGP82qku2eW/JR83sD+k2k+CsNtyEbUqhU6w==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.7.tgz", + "integrity": "sha512-d98gXZkgswvkyohMBABkhm3GeXhYj8psWfwQ2C7gtfrKGTykQa/iOIi+JJhwMjPlZ6Vm2XN+DCf3Es1EoG4ZLA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-remap-async-to-generator": "^7.27.1", - "@babel/traverse": "^7.29.0" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-remap-async-to-generator": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -829,15 +846,15 @@ } }, "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.28.6.tgz", - "integrity": "sha512-ilTRcmbuXjsMmcZ3HASTe4caH5Tpo93PkTxF9oG2VZsSWsahydmcEHhix9Ik122RcTnZnUzPbmux4wh1swfv7g==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.29.7.tgz", + "integrity": "sha512-pcUb2SS+RMo9TWVBwKGI5ShtoG7R+zBsFmCKDa6fe8c+hPr3XJlZgoE5j6i8W7gDjhyvy+85vmYexanvXh3d1w==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-remap-async-to-generator": "^7.27.1" + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-remap-async-to-generator": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -847,13 +864,13 @@ } }, "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz", - "integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.29.7.tgz", + "integrity": "sha512-cUSmjh72N+rN4PrkFlN1dJwNCwjVp5d38/CQrEsFggkD10UiFlBFgdH3tv5dNsLuHY+3S8db2xCHjhZcv5WgvA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -863,13 +880,13 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.6.tgz", - "integrity": "sha512-tt/7wOtBmwHPNMPu7ax4pdPz6shjFrmHDghvNC+FG9Qvj7D6mJcoRQIF5dy4njmxR941l6rgtvfSB2zX3VlUIw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.29.7.tgz", + "integrity": "sha512-ONyr4+AZhKh8yKWInVxU9AXA9EbsyeLcL6V0dJy6M2/62vuvpGm29zzuymbTpdc451GEpDIdAyPLP3r+P61yKQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -879,14 +896,14 @@ } }, "node_modules/@babel/plugin-transform-class-properties": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.28.6.tgz", - "integrity": "sha512-dY2wS3I2G7D697VHndN91TJr8/AAfXQNt5ynCTI/MpxMsSzHp+52uNivYT5wCPax3whc47DR8Ba7cmlQMg24bw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.29.7.tgz", + "integrity": "sha512-GtcpjFvanPfzNQi3eTitsCqtRRmmqzpy/A+yhTR1HaZo1Ly3EA8ZXxlPyHdR8/IuRMYc3E4wdGBewB2QKQjAaA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -896,14 +913,14 @@ } }, "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.6.tgz", - "integrity": "sha512-rfQ++ghVwTWTqQ7w8qyDxL1XGihjBss4CmTgGRCTAC9RIbhVpyp4fOeZtta0Lbf+dTNIVJer6ych2ibHwkZqsQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.29.7.tgz", + "integrity": "sha512-kibJgmEdX2iMwsHY2tSZNDgj8PwIlCQz7FK9KuGKO8zsuoUwSEhoNnNVp/emKWrbY4HeO6kkXfdMqRKKKXBm2A==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -913,18 +930,18 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.6.tgz", - "integrity": "sha512-EF5KONAqC5zAqT783iMGuM2ZtmEBy+mJMOKl2BCvPZ2lVrwvXnB6o+OBWCS+CoeCCpVRF2sA2RBKUxvT8tQT5Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.29.7.tgz", + "integrity": "sha512-qV0OGGBVacduzQHE649JyCneOFI/maT+YKsO+K4Yi3xv2wTPNjM/W2o2gdzMwEAZz7fXNTHAe0NcSg30bIN69g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-globals": "^7.28.0", - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-replace-supers": "^7.28.6", - "@babel/traverse": "^7.28.6" + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -934,14 +951,14 @@ } }, "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.28.6.tgz", - "integrity": "sha512-bcc3k0ijhHbc2lEfpFHgx7eYw9KNXqOerKWfzbxEHUGKnS3sz9C4CNL9OiFN1297bDNfUiSO7DaLzbvHQQQ1BQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.29.7.tgz", + "integrity": "sha512-RK7/IyU5phpuCdBAuig5VkzG/EnbDaui5SQGdU9BFrHdV+mV4cUjLMQ9lJDjLNtWHsqtiefpGZUXQP2BiTYMsA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/template": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/template": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -951,14 +968,14 @@ } }, "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.5.tgz", - "integrity": "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.29.7.tgz", + "integrity": "sha512-iPX8aD6H9zV5s7ZsqTdNocPN/MGQ5sSMnElKrktxjJRMnB2jN/1p2+R7GkfD6CAYoVFqy5A4XnSIUeGgJzIWpg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.28.5" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -968,14 +985,14 @@ } }, "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.28.6.tgz", - "integrity": "sha512-SljjowuNKB7q5Oayv4FoPzeB74g3QgLt8IVJw9ADvWy3QnUb/01aw8I4AVv8wYnPvQz2GDDZ/g3GhcNyDBI4Bg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.29.7.tgz", + "integrity": "sha512-3qc18hsD2RdZiyJNDNc7HQpv6xbncwh8FYtxNFFzclSyh/trPD9KkVR9BDECUjDLvb7yJVF15GfYUuC+LMkkiQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.28.5", - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -985,13 +1002,13 @@ } }, "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz", - "integrity": "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.29.7.tgz", + "integrity": "sha512-6IvRRriEMqnBwD6chtxdLpMYCHWEzN+oL5cyQtjykya19UgzbmKhxmhZgKC/LHxS2nYr9Q/qYPZ5Lr6jOL9+yQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1001,14 +1018,14 @@ } }, "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.29.0.tgz", - "integrity": "sha512-zBPcW2lFGxdiD8PUnPwJjag2J9otbcLQzvbiOzDxpYXyCuYX9agOwMPGn1prVH0a4qzhCKu24rlH4c1f7yA8rw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.29.7.tgz", + "integrity": "sha512-2wiIyo2BjtgU7HufSeDnL9L2O7zr8jmhFKuSr65VpRkUiRKRNpb0mdlk56+XPPKoIrfHqzbMuglDvZun0RISsA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.28.5", - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1018,13 +1035,13 @@ } }, "node_modules/@babel/plugin-transform-dynamic-import": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz", - "integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.29.7.tgz", + "integrity": "sha512-giOlEm/EFjfjr+te9NsdjkUo2v4f8rS/SXPumRVHAtbNcyNlvtREkU1dZzaIDclNpnaVhlCqRdFKhJBjBikzLg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1034,14 +1051,14 @@ } }, "node_modules/@babel/plugin-transform-explicit-resource-management": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.6.tgz", - "integrity": "sha512-Iao5Konzx2b6g7EPqTy40UZbcdXE126tTxVFr/nAIj+WItNxjKSYTEw3RC+A2/ZetmdJsgueL1KhaMCQHkLPIg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.29.7.tgz", + "integrity": "sha512-Rstj7coNz8sE+7Ju7ihpHLI564lsK5pUpNNlvptCIC/16E/S5hbl6n3kESPKdNRmqEWlpn5xpS5Q2dvXBsySLw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/plugin-transform-destructuring": "^7.28.5" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-transform-destructuring": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1051,13 +1068,13 @@ } }, "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.28.6.tgz", - "integrity": "sha512-WitabqiGjV/vJ0aPOLSFfNY1u9U3R7W36B03r5I2KoNix+a3sOhJ3pKFB3R5It9/UiK78NiO0KE9P21cMhlPkw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.29.7.tgz", + "integrity": "sha512-zFpMOTLZBdW5LfObqcSbL6kefg4R4eLdmvS0wbN9M6D5Mym/sKm9toOoWyVOa+xDjvCnuWcHls2YonXwHvH3CQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1067,13 +1084,13 @@ } }, "node_modules/@babel/plugin-transform-export-namespace-from": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz", - "integrity": "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.29.7.tgz", + "integrity": "sha512-24B2nOy2TeJSMheqwPD4DDQOV/elLSIlKxjZt4i05H5AgdPdWR3n18HnNrcJ+j76WJd9gbwb9jPjNYUy6RautA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1083,14 +1100,14 @@ } }, "node_modules/@babel/plugin-transform-for-of": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz", - "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.29.7.tgz", + "integrity": "sha512-zeSIHh0+E1Um1WJRXCFlHQYu2ieJNdivLLjlBEp+dIBu3S51n+SZZmIXjxnItw6pz56Cn+KvK68BIBVsxq2JiQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1100,15 +1117,15 @@ } }, "node_modules/@babel/plugin-transform-function-name": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz", - "integrity": "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.29.7.tgz", + "integrity": "sha512-otRWaHXE6fbAGkePvaj/kvs3HsqXfPhlnzwSOlnFgbqCPMd975dW+4wZ00WFBt+/YlBGcJwNrARQTOJOb4ZrIg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-compilation-targets": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.27.1" + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1118,13 +1135,13 @@ } }, "node_modules/@babel/plugin-transform-json-strings": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.28.6.tgz", - "integrity": "sha512-Nr+hEN+0geQkzhbdgQVPoqr47lZbm+5fCUmO70722xJZd0Mvb59+33QLImGj6F+DkK3xgDi1YVysP8whD6FQAw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.29.7.tgz", + "integrity": "sha512-RRnE2+eon1rJAq8MnoF1b5kTpY1vU88twHcvcKMrsqP/jxIRqDVs9iJB5fqPuqyeFAW0wJo4MlUIPpQCq/aRsg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1134,13 +1151,13 @@ } }, "node_modules/@babel/plugin-transform-literals": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz", - "integrity": "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.29.7.tgz", + "integrity": "sha512-DZ/oLP21ZuWx1vKqnoNv6/tvEK48AQOBRai40CX9dTjGluvT/YZCyY3rryDtyUqCEoyNroy5KKPwX2iQCiRvyw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1150,13 +1167,13 @@ } }, "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.28.6.tgz", - "integrity": "sha512-+anKKair6gpi8VsM/95kmomGNMD0eLz1NQ8+Pfw5sAwWH9fGYXT50E55ZpV0pHUHWf6IUTWPM+f/7AAff+wr9A==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.29.7.tgz", + "integrity": "sha512-A0H91hh6W8MFRkp5TqJmMr39jzGD1A1E1Ysiv2O06Sfbhkapm+XyIzxWCEh5kqwOZ1/8QZ0dY3SeQ7XBqfJd5Q==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1166,13 +1183,13 @@ } }, "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz", - "integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.29.7.tgz", + "integrity": "sha512-hl1kwFZCCiDyfH25Xmco9jTrkPgnS9pmOzSG7W5I4SaGbLeqKv417hcU2RKmaxoPEgsoJh7ZPOrnPGq99bHoUg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1182,14 +1199,14 @@ } }, "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz", - "integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.29.7.tgz", + "integrity": "sha512-fxtQoH3m5ywUSIfaH0FGCzWu4McsYon5bD3K4XnskC7f+OyQMj7rsOMi4NvvmJ83WwBAg4UCe+ov4VZlqEvyew==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1199,14 +1216,14 @@ } }, "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.28.6.tgz", - "integrity": "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.29.7.tgz", + "integrity": "sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1216,16 +1233,16 @@ } }, "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.0.tgz", - "integrity": "sha512-PrujnVFbOdUpw4UHiVwKvKRLMMic8+eC0CuNlxjsyZUiBjhFdPsewdXCkveh2KqBA9/waD0W1b4hXSOBQJezpQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.7.tgz", + "integrity": "sha512-TM2ZcQLoG2/y4HODiStCo10DibYhWhGWAwVv+EQKmG/7GFl0N+AAmUiXOMKM+aiJ9XBJ9AHVZBvTzMnJ2sM3cQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.29.0" + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1235,14 +1252,14 @@ } }, "node_modules/@babel/plugin-transform-modules-umd": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz", - "integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.29.7.tgz", + "integrity": "sha512-B4UkaTK3QpgCwJnrxKfMPKdo92CN7OKXAlpAAnM3UPu0Q0lCCk57ylA9AJbRy2v8dDKOPAAWcoR6CMyeoHwRCA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1252,14 +1269,14 @@ } }, "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.29.0.tgz", - "integrity": "sha512-1CZQA5KNAD6ZYQLPw7oi5ewtDNxH/2vuCh+6SmvgDfhumForvs8a1o9n0UrEoBD8HU4djO2yWngTQlXl1NDVEQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.29.7.tgz", + "integrity": "sha512-vuFoLwr4qnv2xbZ16SQd6uPcH5FNrLHhk/Jzo++0XJFcaDsr4gjJVg6j398oMHiC+83k/GiBzviwF5KBJkPUtQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.28.5", - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1269,13 +1286,13 @@ } }, "node_modules/@babel/plugin-transform-new-target": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz", - "integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.29.7.tgz", + "integrity": "sha512-fEo41GmsOUhOBlw8ioo6zvjX5Xc2Lqkzlyfqbpsk3eB6TReV18uhxZ0esfEokVbY2+PVJAQHNKxER6lGrzNd3A==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1285,13 +1302,13 @@ } }, "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.28.6.tgz", - "integrity": "sha512-3wKbRgmzYbw24mDJXT7N+ADXw8BC/imU9yo9c9X9NKaLF1fW+e5H1U5QjMUBe4Qo4Ox/o++IyUkl1sVCLgevKg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.29.7.tgz", + "integrity": "sha512-idmp1dFaekP9GbcMvG24Kvw2BfhFZjHnNJCkV4WuIY4PskJzwI3f1N5OdgYke38T7rftO6ERulFRn2cFeZwRkg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1301,13 +1318,13 @@ } }, "node_modules/@babel/plugin-transform-numeric-separator": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.28.6.tgz", - "integrity": "sha512-SJR8hPynj8outz+SlStQSwvziMN4+Bq99it4tMIf5/Caq+3iOc0JtKyse8puvyXkk3eFRIA5ID/XfunGgO5i6w==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.29.7.tgz", + "integrity": "sha512-zR7fv/z14OjgHl4AgRtkDBvBMhIzCxqV/qN/2BCRC7LjFwvuzjYe7gDWxC4Wl/SNsLM6SE1IWvRPYMgSJaUvNw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1317,17 +1334,17 @@ } }, "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.6.tgz", - "integrity": "sha512-5rh+JR4JBC4pGkXLAcYdLHZjXudVxWMXbB6u6+E9lRL5TrGVbHt1TjxGbZ8CkmYw9zjkB7jutzOROArsqtncEA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.29.7.tgz", + "integrity": "sha512-Ld98jn4c0smUywL57m7SgsHq3OpThOa6LqZJif3G6jYOovPleoFhVrBJ1WegRApSFB2wu4+RelAj9AC9G08Z4A==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/plugin-transform-destructuring": "^7.28.5", - "@babel/plugin-transform-parameters": "^7.27.7", - "@babel/traverse": "^7.28.6" + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-transform-destructuring": "^7.29.7", + "@babel/plugin-transform-parameters": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1337,14 +1354,14 @@ } }, "node_modules/@babel/plugin-transform-object-super": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz", - "integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.29.7.tgz", + "integrity": "sha512-Ea/diGcw0twB5IlZPO5sgET6fJsLJqPABqTuFWIR+iMPGPZJkATEIWx0wa+aEQ5UY1CBQyP/gkAiLEqn1vBiQA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-replace-supers": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1354,13 +1371,13 @@ } }, "node_modules/@babel/plugin-transform-optional-catch-binding": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.28.6.tgz", - "integrity": "sha512-R8ja/Pyrv0OGAvAXQhSTmWyPJPml+0TMqXlO5w+AsMEiwb2fg3WkOvob7UxFSL3OIttFSGSRFKQsOhJ/X6HQdQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.29.7.tgz", + "integrity": "sha512-sLsyndxK2VwX6yNUOakMb7Sh553ZTe/vVM1XJ+9Z5aW1ytsc8xOIwmyk05NNjN60vkc5/KqoTH6hB4V41LJhng==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1370,14 +1387,14 @@ } }, "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.6.tgz", - "integrity": "sha512-A4zobikRGJTsX9uqVFdafzGkqD30t26ck2LmOzAuLL8b2x6k3TIqRiT2xVvA9fNmFeTX484VpsdgmKNA0bS23w==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.29.7.tgz", + "integrity": "sha512-6GM1dhvK3gNODkXcEcMCOLEDCLSoZ/sBbro2Ax8HURyasQ4NshagQixkRFdh5niI6E4gmA/jYI/4aT7rRos3ZQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1387,13 +1404,13 @@ } }, "node_modules/@babel/plugin-transform-parameters": { - "version": "7.27.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz", - "integrity": "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.29.7.tgz", + "integrity": "sha512-ZDOBqV/qLYJI0YElr8DcENEyARsFQeESqWXH6gZlghYXuPPjvweuDhP4VyEi4BlUBlLRFZVjxoZDMjxhLW766g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1403,14 +1420,14 @@ } }, "node_modules/@babel/plugin-transform-private-methods": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.28.6.tgz", - "integrity": "sha512-piiuapX9CRv7+0st8lmuUlRSmX6mBcVeNQ1b4AYzJxfCMuBfB0vBXDiGSmm03pKJw1v6cZ8KSeM+oUnM6yAExg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.29.7.tgz", + "integrity": "sha512-/6Rz4DK1ETDEM/bWHsPHcaEe7ZaT1EqSXjtSP/L0DijOYuaUhiRiOKcwpZ8P7zR4xXEHc2ITdiCgBm9Tpyv9ug==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1420,15 +1437,15 @@ } }, "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.28.6.tgz", - "integrity": "sha512-b97jvNSOb5+ehyQmBpmhOCiUC5oVK4PMnpRvO7+ymFBoqYjeDHIU9jnrNUuwHOiL9RpGDoKBpSViarV+BU+eVA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.29.7.tgz", + "integrity": "sha512-+BNo06dnrzdNNqCm1X6YUaVv0DKk8Q+JYcoZfOkLhYWNCXzlwTSRq8zGWayT1csjcpNXV9CQTBRRbmTLZac5cA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-create-class-features-plugin": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1438,13 +1455,13 @@ } }, "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz", - "integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.29.7.tgz", + "integrity": "sha512-bOMRLQuI0A5ZqHq3OWJ89/rXpJ/NJrbVhXiP4zwPGMs6kpcVsuTUNjwoE30K0Qm3mf48a/TnRYYD6vPNqcg6jA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1454,13 +1471,13 @@ } }, "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.0.tgz", - "integrity": "sha512-FijqlqMA7DmRdg/aINBSs04y8XNTYw/lr1gJ2WsmBnnaNw1iS43EPkJW+zK7z65auG3AWRFXWj+NcTQwYptUog==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.7.tgz", + "integrity": "sha512-rNNFV0DBAJp988xW2DOntfDoYn1eR8GGF5AT5vYc+rjyfaQkM242c9tZUHHPe7KYaiJizXPWhQTzzdbXySyhBw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1470,14 +1487,14 @@ } }, "node_modules/@babel/plugin-transform-regexp-modifiers": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.28.6.tgz", - "integrity": "sha512-QGWAepm9qxpaIs7UM9FvUSnCGlb8Ua1RhyM4/veAxLwt3gMat/LSGrZixyuj4I6+Kn9iwvqCyPTtbdxanYoWYg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.29.7.tgz", + "integrity": "sha512-mB5Fs0VWrJ42ZCmc8114v60qetdaUVNkj9PmSZRmanCZM3S9hm0CFRLjRmYIsuXav14l2jvZ+4T8iiCGnhj3nQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.28.5", - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1487,13 +1504,13 @@ } }, "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz", - "integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.29.7.tgz", + "integrity": "sha512-5+YhdpVgmfSmwZyLMftfaiffLRMHjzIRHFHHLdibcSyJm2pasMrKHrO3Ptrt2DRshjvpgjEJJ1zVW14WPq/6QA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1503,13 +1520,13 @@ } }, "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz", - "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.29.7.tgz", + "integrity": "sha512-I+WYbGBAiCn7nA6xBrlgPH+MB7HWb4u8pv5S0Pv7OtwNvIFvCCb24YlttKEeUFVurfBCEaOTnuhlqsb7f0Z5Dg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1519,14 +1536,14 @@ } }, "node_modules/@babel/plugin-transform-spread": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.28.6.tgz", - "integrity": "sha512-9U4QObUC0FtJl05AsUcodau/RWDytrU6uKgkxu09mLR9HLDAtUMoPuuskm5huQsoktmsYpI+bGmq+iapDcriKA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.29.7.tgz", + "integrity": "sha512-/u5K1QWada7tbYNqTjMh96718g9NTwh9tfPJMsSmVsQwGT447FskV+KcfeXkXq2GWki4EM/MuTdmBec+hOuVTQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1536,13 +1553,13 @@ } }, "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz", - "integrity": "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.29.7.tgz", + "integrity": "sha512-BCHzNYJGe9l7EpwwDBN/ztlL2NYFFq8hp9ddjtUEM9f2O7S7kKV/lL6Fwo7IF7NSkYhPK2vO+86nIGltA90MsA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1552,13 +1569,13 @@ } }, "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz", - "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.29.7.tgz", + "integrity": "sha512-NCSEJ4sLFU2gqAub45HYh4fus2yQ36rr6ei6vpU7NdoJqCpxvEG8E6eJpscGyXP3VHD2Ny+fSXr04k1hoUrFqA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1568,13 +1585,13 @@ } }, "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz", - "integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.29.7.tgz", + "integrity": "sha512-223mNGoTkBiTEWFoK+Q6Go3tueMRclO8vxxxxquNCYuNI4jWOofFKJRRDu6SDrB8Sgo1UEGW9T4GAQ8ZyRso1A==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1584,13 +1601,13 @@ } }, "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz", - "integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.29.7.tgz", + "integrity": "sha512-jCfXxSjf94lf4E0hKE0AByxF6F3/pVFqRdUUNkDJhsY0m1ZKjnN6ZYyMeHNpzflxb/0q5b7t3p+BE+SLF1WOtA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1600,14 +1617,14 @@ } }, "node_modules/@babel/plugin-transform-unicode-property-regex": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.28.6.tgz", - "integrity": "sha512-4Wlbdl/sIZjzi/8St0evF0gEZrgOswVO6aOzqxh1kDZOl9WmLrHq2HtGhnOJZmHZYKP8WZ1MDLCt5DAWwRo57A==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.29.7.tgz", + "integrity": "sha512-OgZ+zoAJgZLUCunsTRQ5LAjOywDv5zzZ2/hQ5aMw1pGXyY2rtE8/chXYUmu3AlVHKpm10KEdG9aMwbI/K76ZGw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.28.5", - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1617,14 +1634,14 @@ } }, "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz", - "integrity": "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.29.7.tgz", + "integrity": "sha512-7D/x/23/d/3VqZ0QA+LGbZMlGwZjztBygSWWWsfTPoQ1oQ6Q1P6Mr3d0kk42XabyUVw+fha3LqdRsFqeKqvCyA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1634,14 +1651,14 @@ } }, "node_modules/@babel/plugin-transform-unicode-sets-regex": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.28.6.tgz", - "integrity": "sha512-/wHc/paTUmsDYN7SZkpWxogTOBNnlx7nBQYfy6JJlCT7G3mVhltk3e++N7zV0XfgGsrqBxd4rJQt9H16I21Y1Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.29.7.tgz", + "integrity": "sha512-BLOhLht9DOJwIxlmp91wHvkXv1lguuHS3/FwUO8HL1H0u8s4hR1gASVFyilu9iGtcTRYqjTZmlsFFeQletntEg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.28.5", - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1651,76 +1668,77 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.29.2.tgz", - "integrity": "sha512-DYD23veRYGvBFhcTY1iUvJnDNpuqNd/BzBwCvzOTKUnJjKg5kpUBh3/u9585Agdkgj+QuygG7jLfOPWMa2KVNw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.29.0", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-validator-option": "^7.27.1", - "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.28.5", - "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.6", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.29.7.tgz", + "integrity": "sha512-GYzX36n1nsciIb0uyH0GHwxwtNwPQIcpxSeiVLDtG/B7jB5xXgchnmL1f/jCX5o+pwnaDBtO60ONSJhEBJfxYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.29.7", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.29.7", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.29.7", + "@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": "^7.29.7", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.29.7", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.29.7", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", - "@babel/plugin-syntax-import-assertions": "^7.28.6", - "@babel/plugin-syntax-import-attributes": "^7.28.6", + "@babel/plugin-syntax-import-assertions": "^7.29.7", + "@babel/plugin-syntax-import-attributes": "^7.29.7", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", - "@babel/plugin-transform-arrow-functions": "^7.27.1", - "@babel/plugin-transform-async-generator-functions": "^7.29.0", - "@babel/plugin-transform-async-to-generator": "^7.28.6", - "@babel/plugin-transform-block-scoped-functions": "^7.27.1", - "@babel/plugin-transform-block-scoping": "^7.28.6", - "@babel/plugin-transform-class-properties": "^7.28.6", - "@babel/plugin-transform-class-static-block": "^7.28.6", - "@babel/plugin-transform-classes": "^7.28.6", - "@babel/plugin-transform-computed-properties": "^7.28.6", - "@babel/plugin-transform-destructuring": "^7.28.5", - "@babel/plugin-transform-dotall-regex": "^7.28.6", - "@babel/plugin-transform-duplicate-keys": "^7.27.1", - "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.29.0", - "@babel/plugin-transform-dynamic-import": "^7.27.1", - "@babel/plugin-transform-explicit-resource-management": "^7.28.6", - "@babel/plugin-transform-exponentiation-operator": "^7.28.6", - "@babel/plugin-transform-export-namespace-from": "^7.27.1", - "@babel/plugin-transform-for-of": "^7.27.1", - "@babel/plugin-transform-function-name": "^7.27.1", - "@babel/plugin-transform-json-strings": "^7.28.6", - "@babel/plugin-transform-literals": "^7.27.1", - "@babel/plugin-transform-logical-assignment-operators": "^7.28.6", - "@babel/plugin-transform-member-expression-literals": "^7.27.1", - "@babel/plugin-transform-modules-amd": "^7.27.1", - "@babel/plugin-transform-modules-commonjs": "^7.28.6", - "@babel/plugin-transform-modules-systemjs": "^7.29.0", - "@babel/plugin-transform-modules-umd": "^7.27.1", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.29.0", - "@babel/plugin-transform-new-target": "^7.27.1", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.28.6", - "@babel/plugin-transform-numeric-separator": "^7.28.6", - "@babel/plugin-transform-object-rest-spread": "^7.28.6", - "@babel/plugin-transform-object-super": "^7.27.1", - "@babel/plugin-transform-optional-catch-binding": "^7.28.6", - "@babel/plugin-transform-optional-chaining": "^7.28.6", - "@babel/plugin-transform-parameters": "^7.27.7", - "@babel/plugin-transform-private-methods": "^7.28.6", - "@babel/plugin-transform-private-property-in-object": "^7.28.6", - "@babel/plugin-transform-property-literals": "^7.27.1", - "@babel/plugin-transform-regenerator": "^7.29.0", - "@babel/plugin-transform-regexp-modifiers": "^7.28.6", - "@babel/plugin-transform-reserved-words": "^7.27.1", - "@babel/plugin-transform-shorthand-properties": "^7.27.1", - "@babel/plugin-transform-spread": "^7.28.6", - "@babel/plugin-transform-sticky-regex": "^7.27.1", - "@babel/plugin-transform-template-literals": "^7.27.1", - "@babel/plugin-transform-typeof-symbol": "^7.27.1", - "@babel/plugin-transform-unicode-escapes": "^7.27.1", - "@babel/plugin-transform-unicode-property-regex": "^7.28.6", - "@babel/plugin-transform-unicode-regex": "^7.27.1", - "@babel/plugin-transform-unicode-sets-regex": "^7.28.6", + "@babel/plugin-transform-arrow-functions": "^7.29.7", + "@babel/plugin-transform-async-generator-functions": "^7.29.7", + "@babel/plugin-transform-async-to-generator": "^7.29.7", + "@babel/plugin-transform-block-scoped-functions": "^7.29.7", + "@babel/plugin-transform-block-scoping": "^7.29.7", + "@babel/plugin-transform-class-properties": "^7.29.7", + "@babel/plugin-transform-class-static-block": "^7.29.7", + "@babel/plugin-transform-classes": "^7.29.7", + "@babel/plugin-transform-computed-properties": "^7.29.7", + "@babel/plugin-transform-destructuring": "^7.29.7", + "@babel/plugin-transform-dotall-regex": "^7.29.7", + "@babel/plugin-transform-duplicate-keys": "^7.29.7", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.29.7", + "@babel/plugin-transform-dynamic-import": "^7.29.7", + "@babel/plugin-transform-explicit-resource-management": "^7.29.7", + "@babel/plugin-transform-exponentiation-operator": "^7.29.7", + "@babel/plugin-transform-export-namespace-from": "^7.29.7", + "@babel/plugin-transform-for-of": "^7.29.7", + "@babel/plugin-transform-function-name": "^7.29.7", + "@babel/plugin-transform-json-strings": "^7.29.7", + "@babel/plugin-transform-literals": "^7.29.7", + "@babel/plugin-transform-logical-assignment-operators": "^7.29.7", + "@babel/plugin-transform-member-expression-literals": "^7.29.7", + "@babel/plugin-transform-modules-amd": "^7.29.7", + "@babel/plugin-transform-modules-commonjs": "^7.29.7", + "@babel/plugin-transform-modules-systemjs": "^7.29.7", + "@babel/plugin-transform-modules-umd": "^7.29.7", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.29.7", + "@babel/plugin-transform-new-target": "^7.29.7", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.29.7", + "@babel/plugin-transform-numeric-separator": "^7.29.7", + "@babel/plugin-transform-object-rest-spread": "^7.29.7", + "@babel/plugin-transform-object-super": "^7.29.7", + "@babel/plugin-transform-optional-catch-binding": "^7.29.7", + "@babel/plugin-transform-optional-chaining": "^7.29.7", + "@babel/plugin-transform-parameters": "^7.29.7", + "@babel/plugin-transform-private-methods": "^7.29.7", + "@babel/plugin-transform-private-property-in-object": "^7.29.7", + "@babel/plugin-transform-property-literals": "^7.29.7", + "@babel/plugin-transform-regenerator": "^7.29.7", + "@babel/plugin-transform-regexp-modifiers": "^7.29.7", + "@babel/plugin-transform-reserved-words": "^7.29.7", + "@babel/plugin-transform-shorthand-properties": "^7.29.7", + "@babel/plugin-transform-spread": "^7.29.7", + "@babel/plugin-transform-sticky-regex": "^7.29.7", + "@babel/plugin-transform-template-literals": "^7.29.7", + "@babel/plugin-transform-typeof-symbol": "^7.29.7", + "@babel/plugin-transform-unicode-escapes": "^7.29.7", + "@babel/plugin-transform-unicode-property-regex": "^7.29.7", + "@babel/plugin-transform-unicode-regex": "^7.29.7", + "@babel/plugin-transform-unicode-sets-regex": "^7.29.7", "@babel/preset-modules": "0.1.6-no-external-plugins", "babel-plugin-polyfill-corejs2": "^0.4.15", "babel-plugin-polyfill-corejs3": "^0.14.0", @@ -1762,33 +1780,33 @@ } }, "node_modules/@babel/template": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", - "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", - "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", "debug": "^4.3.1" }, "engines": { @@ -1796,13 +1814,13 @@ } }, "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1838,9 +1856,9 @@ } }, "node_modules/@cypress/request": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.10.tgz", - "integrity": "sha512-hauBrOdvu08vOsagkZ/Aju5XuiZx6ldsLfByg1htFeldhex+PeMrYauANzFsMJeAA0+dyPLbDoX2OYuvVoLDkQ==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@cypress/request/-/request-4.0.1.tgz", + "integrity": "sha512-y20e+e6dFYkOUUJLVUZTsJRuTiXZaUQ32WD+R/ux/HBybbTx4ge7cNINcua0pU8+SNkKuRbOF12mBmzuzM8n5w==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1857,14 +1875,29 @@ "json-stringify-safe": "~5.0.1", "mime-types": "~2.1.19", "performance-now": "^2.1.0", - "qs": "~6.14.1", + "qs": "^6.15.2", "safe-buffer": "^5.1.2", "tough-cookie": "^5.0.0", - "tunnel-agent": "^0.6.0", - "uuid": "^8.3.2" + "tunnel-agent": "^0.6.0" }, "engines": { - "node": ">= 6" + "node": ">= 14.17.0" + } + }, + "node_modules/@cypress/request/node_modules/qs": { + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/@cypress/xvfb": { @@ -1934,175 +1967,549 @@ } } }, - "node_modules/@esbuild/linux-x64": { + "node_modules/@esbuild/aix-ppc64": { "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", "cpu": [ - "x64" + "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ - "linux" + "aix" ], "engines": { "node": ">=12" } }, - "node_modules/@headlessui/vue": { - "version": "1.7.16", - "resolved": "https://registry.npmjs.org/@headlessui/vue/-/vue-1.7.16.tgz", - "integrity": "sha512-nKT+nf/q6x198SsyK54mSszaQl/z+QxtASmgMEJtpxSX2Q0OPJX0upS/9daDyiECpeAsvjkoOrm2O/6PyBQ+Qg==", + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=10" - }, - "peerDependencies": { - "vue": "^3.2.0" + "node": ">=12" } }, - "node_modules/@iconify-json/simple-icons": { - "version": "1.2.35", - "resolved": "https://registry.npmjs.org/@iconify-json/simple-icons/-/simple-icons-1.2.35.tgz", - "integrity": "sha512-PAHZZn6P5ToHMhmEeeh/O96E/Ep4PctN44N64dWYbDasEvbVoN6x62m+Doz8au0SVS4/zYEMAsDO6TdO9ep84Q==", + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], "dev": true, - "dependencies": { - "@iconify/types": "*" + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" } }, - "node_modules/@iconify/types": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", - "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", - "dev": true - }, - "node_modules/@inquirer/checkbox": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.0.4.tgz", - "integrity": "sha512-fYAKCAcGNMdfjL6hZTRUwkIByQ8EIZCXKrIQZH7XjADnN/xvRUhj8UdBbpC4zoUzvChhkSC/zRKaP/tDs3dZpg==", + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "@inquirer/core": "^10.1.2", - "@inquirer/figures": "^1.0.9", - "@inquirer/type": "^3.0.2", - "ansi-escapes": "^4.3.2", - "yoctocolors-cjs": "^2.1.2" - }, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" + "node": ">=12" } }, - "node_modules/@inquirer/confirm": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.1.tgz", - "integrity": "sha512-vVLSbGci+IKQvDOtzpPTCOiEJCNidHcAq9JYVoWTW0svb5FiwSLotkM+JXNXejfjnzVYV9n0DTBythl9+XgTxg==", + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], "dev": true, - "dependencies": { - "@inquirer/core": "^10.1.2", - "@inquirer/type": "^3.0.2" - }, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" + "node": ">=12" } }, - "node_modules/@inquirer/core": { - "version": "10.1.15", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.15.tgz", - "integrity": "sha512-8xrp836RZvKkpNbVvgWUlxjT4CraKk2q+I3Ksy+seI2zkcE+y6wNs1BVhgcv8VyImFecUhdQrYLdW32pAjwBdA==", + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@inquirer/figures": "^1.0.13", - "@inquirer/type": "^3.0.8", - "ansi-escapes": "^4.3.2", - "cli-width": "^4.1.0", - "mute-stream": "^2.0.0", - "signal-exit": "^4.1.0", - "wrap-ansi": "^6.2.0", - "yoctocolors-cjs": "^2.1.2" - }, + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } + "node": ">=12" } }, - "node_modules/@inquirer/core/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": ">=12" } }, - "node_modules/@inquirer/core/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">=7.0.0" + "node": ">=12" } }, - "node_modules/@inquirer/core/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/@inquirer/core/node_modules/mute-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", - "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": ">=12" } }, - "node_modules/@inquirer/core/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=12" } }, - "node_modules/@inquirer/core/node_modules/wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], "dev": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@headlessui/vue": { + "version": "1.7.16", + "resolved": "https://registry.npmjs.org/@headlessui/vue/-/vue-1.7.16.tgz", + "integrity": "sha512-nKT+nf/q6x198SsyK54mSszaQl/z+QxtASmgMEJtpxSX2Q0OPJX0upS/9daDyiECpeAsvjkoOrm2O/6PyBQ+Qg==", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/@iconify-json/simple-icons": { + "version": "1.2.35", + "resolved": "https://registry.npmjs.org/@iconify-json/simple-icons/-/simple-icons-1.2.35.tgz", + "integrity": "sha512-PAHZZn6P5ToHMhmEeeh/O96E/Ep4PctN44N64dWYbDasEvbVoN6x62m+Doz8au0SVS4/zYEMAsDO6TdO9ep84Q==", + "dev": true, + "dependencies": { + "@iconify/types": "*" + } + }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "dev": true + }, + "node_modules/@inquirer/checkbox": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.0.4.tgz", + "integrity": "sha512-fYAKCAcGNMdfjL6hZTRUwkIByQ8EIZCXKrIQZH7XjADnN/xvRUhj8UdBbpC4zoUzvChhkSC/zRKaP/tDs3dZpg==", + "dev": true, + "dependencies": { + "@inquirer/core": "^10.1.2", + "@inquirer/figures": "^1.0.9", + "@inquirer/type": "^3.0.2", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + } + }, + "node_modules/@inquirer/confirm": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.1.tgz", + "integrity": "sha512-vVLSbGci+IKQvDOtzpPTCOiEJCNidHcAq9JYVoWTW0svb5FiwSLotkM+JXNXejfjnzVYV9n0DTBythl9+XgTxg==", + "dev": true, + "dependencies": { + "@inquirer/core": "^10.1.2", + "@inquirer/type": "^3.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + } + }, + "node_modules/@inquirer/core": { + "version": "10.1.15", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.15.tgz", + "integrity": "sha512-8xrp836RZvKkpNbVvgWUlxjT4CraKk2q+I3Ksy+seI2zkcE+y6wNs1BVhgcv8VyImFecUhdQrYLdW32pAjwBdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/figures": "^1.0.13", + "@inquirer/type": "^3.0.8", + "ansi-escapes": "^4.3.2", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@inquirer/core/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@inquirer/core/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/@inquirer/core/node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "dev": true, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@inquirer/core/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@inquirer/core/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, "engines": { "node": ">=8" } @@ -3083,40 +3490,208 @@ "dev": true, "license": "MIT" }, - "node_modules/@parcel/watcher": { + "node_modules/@parcel/watcher": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.0.tgz", + "integrity": "sha512-i0GV1yJnm2n3Yq1qw6QrUrd/LI9bE8WEBOTtOkpCXHHdyN3TAGgqAK/DAT05z4fq2x04cARXt2pDmjWjL92iTQ==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "dependencies": { + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.0", + "@parcel/watcher-darwin-arm64": "2.5.0", + "@parcel/watcher-darwin-x64": "2.5.0", + "@parcel/watcher-freebsd-x64": "2.5.0", + "@parcel/watcher-linux-arm-glibc": "2.5.0", + "@parcel/watcher-linux-arm-musl": "2.5.0", + "@parcel/watcher-linux-arm64-glibc": "2.5.0", + "@parcel/watcher-linux-arm64-musl": "2.5.0", + "@parcel/watcher-linux-x64-glibc": "2.5.0", + "@parcel/watcher-linux-x64-musl": "2.5.0", + "@parcel/watcher-win32-arm64": "2.5.0", + "@parcel/watcher-win32-ia32": "2.5.0", + "@parcel/watcher-win32-x64": "2.5.0" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.0.tgz", + "integrity": "sha512-qlX4eS28bUcQCdribHkg/herLe+0A9RyYC+mm2PXpncit8z5b3nSqGVzMNR3CmtAOgRutiZ02eIJJgP/b1iEFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.0.tgz", + "integrity": "sha512-hyZ3TANnzGfLpRA2s/4U1kbw2ZI4qGxaRJbBH2DCSREFfubMswheh8TeiC1sGZ3z2jUf3s37P0BBlrD3sjVTUw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.0.tgz", + "integrity": "sha512-9rhlwd78saKf18fT869/poydQK8YqlU26TMiNg7AIu7eBp9adqbJZqmdFOsbZ5cnLp5XvRo9wcFmNHgHdWaGYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.0.tgz", + "integrity": "sha512-syvfhZzyM8kErg3VF0xpV8dixJ+RzbUaaGaeb7uDuz0D3FK97/mZ5AJQ3XNnDsXX7KkFNtyQyFrXZzQIcN49Tw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.0.tgz", + "integrity": "sha512-0VQY1K35DQET3dVYWpOaPFecqOT9dbuCfzjxoQyif1Wc574t3kOSkKevULddcR9znz1TcklCE7Ht6NIxjvTqLA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.0.tgz", + "integrity": "sha512-6uHywSIzz8+vi2lAzFeltnYbdHsDm3iIB57d4g5oaB9vKwjb6N6dRIgZMujw4nm5r6v9/BQH0noq6DzHrqr2pA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.0.tgz", - "integrity": "sha512-i0GV1yJnm2n3Yq1qw6QrUrd/LI9bE8WEBOTtOkpCXHHdyN3TAGgqAK/DAT05z4fq2x04cARXt2pDmjWjL92iTQ==", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.0.tgz", + "integrity": "sha512-BfNjXwZKxBy4WibDb/LDCriWSKLz+jJRL3cM/DllnHH5QUyoiUNEp3GmL80ZqxeumoADfCCP19+qiYiC8gUBjA==", + "cpu": [ + "arm64" + ], "dev": true, - "hasInstallScript": true, + "license": "MIT", "optional": true, - "dependencies": { - "detect-libc": "^1.0.3", - "is-glob": "^4.0.3", - "micromatch": "^4.0.5", - "node-addon-api": "^7.0.0" - }, + "os": [ + "linux" + ], "engines": { "node": ">= 10.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.0.tgz", + "integrity": "sha512-S1qARKOphxfiBEkwLUbHjCY9BWPdWnW9j7f7Hb2jPplu8UZ3nes7zpPOW9bkLbHRvWM0WDTsjdOTUgW0xLBN1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" }, - "optionalDependencies": { - "@parcel/watcher-android-arm64": "2.5.0", - "@parcel/watcher-darwin-arm64": "2.5.0", - "@parcel/watcher-darwin-x64": "2.5.0", - "@parcel/watcher-freebsd-x64": "2.5.0", - "@parcel/watcher-linux-arm-glibc": "2.5.0", - "@parcel/watcher-linux-arm-musl": "2.5.0", - "@parcel/watcher-linux-arm64-glibc": "2.5.0", - "@parcel/watcher-linux-arm64-musl": "2.5.0", - "@parcel/watcher-linux-x64-glibc": "2.5.0", - "@parcel/watcher-linux-x64-musl": "2.5.0", - "@parcel/watcher-win32-arm64": "2.5.0", - "@parcel/watcher-win32-ia32": "2.5.0", - "@parcel/watcher-win32-x64": "2.5.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, "node_modules/@parcel/watcher-linux-x64-glibc": { @@ -3159,6 +3734,69 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.0.tgz", + "integrity": "sha512-twtft1d+JRNkM5YbmexfcH/N4znDtjgysFaV9zvZmmJezQsKpkfLYJ+JFV3uygugK6AtIM2oADPkB2AdhBrNig==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.0.tgz", + "integrity": "sha512-+rgpsNRKwo8A53elqbbHXdOMtY/tAtTzManTWShB5Kk54N8Q9mzNWV7tV+IbGueCbcj826MfWGU3mprWtuf1TA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.0.tgz", + "integrity": "sha512-lPrxve92zEHdgeff3aiu4gDOIt4u7sJYha6wbdEZDCDUhtjTsOMiaJzG5lMY4GkWH8p0fMmO2Ppq5G5XXG+DQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/@peculiar/asn1-cms": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.6.1.tgz", @@ -4013,76 +4651,286 @@ "resolved": "https://registry.npmjs.org/@php-wasm/xdebug-bridge/-/xdebug-bridge-3.0.52.tgz", "integrity": "sha512-lqVwXpjMuD4niKU3cmAiqTnTg6tk6gRqI7PLbMlmDDXJ7g59HebYkBd5NCXRCphGSj47ge44Fy/itoa0EOiluw==", "dev": true, - "license": "GPL-2.0-or-later", - "dependencies": { - "@php-wasm/logger": "3.0.52", - "@php-wasm/node": "3.0.52", - "@php-wasm/universal": "3.0.52", - "@wp-playground/common": "3.0.52", - "express": "4.22.0", - "ini": "4.1.2", - "wasm-feature-detect": "1.8.0", - "ws": "8.18.3", - "xml2js": "0.6.2", - "yargs": "17.7.2" - }, - "bin": { - "xdebug-bridge": "xdebug-bridge.js" - }, - "engines": { - "node": ">=20.18.3", - "npm": ">=10.1.0" - }, - "optionalDependencies": { - "fs-ext": "2.1.1" - } + "license": "GPL-2.0-or-later", + "dependencies": { + "@php-wasm/logger": "3.0.52", + "@php-wasm/node": "3.0.52", + "@php-wasm/universal": "3.0.52", + "@wp-playground/common": "3.0.52", + "express": "4.22.0", + "ini": "4.1.2", + "wasm-feature-detect": "1.8.0", + "ws": "8.18.3", + "xml2js": "0.6.2", + "yargs": "17.7.2" + }, + "bin": { + "xdebug-bridge": "xdebug-bridge.js" + }, + "engines": { + "node": ">=20.18.3", + "npm": ">=10.1.0" + }, + "optionalDependencies": { + "fs-ext": "2.1.1" + } + }, + "node_modules/@php-wasm/xdebug-bridge/node_modules/ini": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.2.tgz", + "integrity": "sha512-AMB1mvwR1pyBFY/nSevUX6y8nJWS63/SzUKD3JyQn97s4xgIdgQPT75IRouIiBAN4yLQBUShNYVW0+UG25daCw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@rokoli/vue-tiny-i18n": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@rokoli/vue-tiny-i18n/-/vue-tiny-i18n-0.3.0.tgz", + "integrity": "sha512-UedVdP670KrV/lzDzDyhpKHBBIqeHLydf06xuGdLWTqJcq8hu6ZKjnnlNaoxOs+Fwu8En6fUCWo1wAcQIIVyaQ==", + "dependencies": { + "js-yaml": "^4.1.0" + }, + "peerDependencies": { + "vue": "^3.2" + } + }, + "node_modules/@rokoli/vue-tiny-i18n/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "node_modules/@rokoli/vue-tiny-i18n/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.41.1.tgz", + "integrity": "sha512-NELNvyEWZ6R9QMkiytB4/L4zSEaBC03KIXEghptLGLZWJ6VPrL63ooZQCOnlx36aQPGhzuOMwDerC1Eb2VmrLw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.41.1.tgz", + "integrity": "sha512-DXdQe1BJ6TK47ukAoZLehRHhfKnKg9BjnQYUu9gzhI8Mwa1d2fzxA1aw2JixHVl403bwp1+/o/NhhHtxWJBgEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.41.1.tgz", + "integrity": "sha512-5afxvwszzdulsU2w8JKWwY8/sJOLPzf0e1bFuvcW5h9zsEg+RQAojdW0ux2zyYAz7R8HvvzKCjLNJhVq965U7w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.41.1.tgz", + "integrity": "sha512-egpJACny8QOdHNNMZKf8xY0Is6gIMz+tuqXlusxquWu3F833DcMwmGM7WlvCO9sB3OsPjdC4U0wHw5FabzCGZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.41.1.tgz", + "integrity": "sha512-DBVMZH5vbjgRk3r0OzgjS38z+atlupJ7xfKIDJdZZL6sM6wjfDNo64aowcLPKIx7LMQi8vybB56uh1Ftck/Atg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.41.1.tgz", + "integrity": "sha512-3FkydeohozEskBxNWEIbPfOE0aqQgB6ttTkJ159uWOFn42VLyfAiyD9UK5mhu+ItWzft60DycIN1Xdgiy8o/SA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.41.1.tgz", + "integrity": "sha512-wC53ZNDgt0pqx5xCAgNunkTzFE8GTgdZ9EwYGVcg+jEjJdZGtq9xPjDnFgfFozQI/Xm1mh+D9YlYtl+ueswNEg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.41.1.tgz", + "integrity": "sha512-jwKCca1gbZkZLhLRtsrka5N8sFAaxrGz/7wRJ8Wwvq3jug7toO21vWlViihG85ei7uJTpzbXZRcORotE+xyrLA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.41.1.tgz", + "integrity": "sha512-g0UBcNknsmmNQ8V2d/zD2P7WWfJKU0F1nu0k5pW4rvdb+BIqMm8ToluW/eeRmxCared5dD76lS04uL4UaNgpNA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.41.1.tgz", + "integrity": "sha512-XZpeGB5TKEZWzIrj7sXr+BEaSgo/ma/kCgrZgL0oo5qdB1JlTzIYQKel/RmhT6vMAvOdM2teYlAaOGJpJ9lahg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@php-wasm/xdebug-bridge/node_modules/ini": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.2.tgz", - "integrity": "sha512-AMB1mvwR1pyBFY/nSevUX6y8nJWS63/SzUKD3JyQn97s4xgIdgQPT75IRouIiBAN4yLQBUShNYVW0+UG25daCw==", + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.41.1.tgz", + "integrity": "sha512-bkCfDJ4qzWfFRCNt5RVV4DOw6KEgFTUZi2r2RuYhGWC8WhCA8lCAJhDeAmrM/fdiAH54m0mA0Vk2FGRPyzI+tw==", + "cpu": [ + "loong64" + ], "dev": true, - "license": "ISC", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.41.1.tgz", + "integrity": "sha512-3mr3Xm+gvMX+/8EKogIZSIEF0WUu0HL9di+YWlJpO8CQBnoLAEL/roTCxuLncEdgcfJcvA4UMOf+2dnjl4Ut1A==", + "cpu": [ + "ppc64" + ], "dev": true, + "license": "MIT", "optional": true, - "engines": { - "node": ">=14" - } + "os": [ + "linux" + ] }, - "node_modules/@rokoli/vue-tiny-i18n": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@rokoli/vue-tiny-i18n/-/vue-tiny-i18n-0.3.0.tgz", - "integrity": "sha512-UedVdP670KrV/lzDzDyhpKHBBIqeHLydf06xuGdLWTqJcq8hu6ZKjnnlNaoxOs+Fwu8En6fUCWo1wAcQIIVyaQ==", - "dependencies": { - "js-yaml": "^4.1.0" - }, - "peerDependencies": { - "vue": "^3.2" - } + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.41.1.tgz", + "integrity": "sha512-3rwCIh6MQ1LGrvKJitQjZFuQnT2wxfU+ivhNBzmxXTXPllewOF7JR1s2vMX/tWtUYFgphygxjqMl76q4aMotGw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@rokoli/vue-tiny-i18n/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.41.1.tgz", + "integrity": "sha512-LdIUOb3gvfmpkgFZuccNa2uYiqtgZAz3PTzjuM5bH3nvuy9ty6RGc/Q0+HDFrHrizJGVpjnTZ1yS5TNNjFlklw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@rokoli/vue-tiny-i18n/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.41.1.tgz", + "integrity": "sha512-oIE6M8WC9ma6xYqjvPhzZYk6NbobIURvP/lEbh7FWplcMO6gn7MM2yHKA1eC/GvYwzNKK/1LYgqzdkZ8YFxR8g==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { "version": "4.41.1", @@ -4110,6 +4958,48 @@ "linux" ] }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.41.1.tgz", + "integrity": "sha512-lZkCxIrjlJlMt1dLO/FbpZbzt6J/A8p4DnqzSa4PWqPEUUUnzXLeki/iyPLfV0BmHItlYgHUqJe+3KiyydmiNQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.41.1.tgz", + "integrity": "sha512-+psFT9+pIh2iuGsxFYYa/LhS5MFKmuivRsx9iPJWNSGbh2XVEjk90fmpUEjCnILPEPJnikAU6SFDiEUyOv90Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.41.1.tgz", + "integrity": "sha512-Wq2zpapRYLfi4aKxf2Xff0tN+7slj2d4R87WEzqw7ZLsVvO5zwYCIuEGSZYiK41+GlwUo1HiR+GdkLEJnCKTCw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@shikijs/core": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-2.5.0.tgz", @@ -4366,16 +5256,6 @@ "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz", "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==" }, - "node_modules/@types/yauzl": { - "version": "2.10.1", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.1.tgz", - "integrity": "sha512-CHzgNU3qYBnp/O4S3yv2tXPlvMTq0YWSTVg2/JYLqWZGHwwgJGAwd00poay/11asPq8wLFwHzubyInqHIFmmiw==", - "dev": true, - "optional": true, - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@ungap/structured-clone": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", @@ -4396,13 +5276,13 @@ } }, "node_modules/@vue/compiler-core": { - "version": "3.5.30", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.30.tgz", - "integrity": "sha512-s3DfdZkcu/qExZ+td75015ljzHc6vE+30cFMGRPROYjqkroYI5NV2X1yAMX9UeyBNWB9MxCfPcsjpLS11nzkkw==", + "version": "3.5.38", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.38.tgz", + "integrity": "sha512-s99aGxWYig9ErHbct27KXEGhrBYlRI6c4MwAgXErOAbX9xiW37/uMa+XUDO69zLz83dng8UUZ70CTOJrLrYrEQ==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.0", - "@vue/shared": "3.5.30", + "@babel/parser": "^7.29.7", + "@vue/shared": "3.5.38", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" @@ -4421,40 +5301,40 @@ } }, "node_modules/@vue/compiler-dom": { - "version": "3.5.30", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.30.tgz", - "integrity": "sha512-eCFYESUEVYHhiMuK4SQTldO3RYxyMR/UQL4KdGD1Yrkfdx4m/HYuZ9jSfPdA+nWJY34VWndiYdW/wZXyiPEB9g==", + "version": "3.5.38", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.38.tgz", + "integrity": "sha512-JTqp25l8aFfJYF7/KmsXZjAxJz7T+SjmTJLoXVjHtc2BrSgSiW2n9Aem/cWq1OPe68A8JL06B3eVdhlP0H4TVw==", "license": "MIT", "dependencies": { - "@vue/compiler-core": "3.5.30", - "@vue/shared": "3.5.30" + "@vue/compiler-core": "3.5.38", + "@vue/shared": "3.5.38" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.5.30", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.30.tgz", - "integrity": "sha512-LqmFPDn89dtU9vI3wHJnwaV6GfTRD87AjWpTWpyrdVOObVtjIuSeZr181z5C4PmVx/V3j2p+0f7edFKGRMpQ5A==", + "version": "3.5.38", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.38.tgz", + "integrity": "sha512-DuA2GiZawSEW442iw/9+Fkol8hTgb4Ke5KkhmSry65QA7YuyMbIdy8p0XZRMvNwJdgRz307W8g1CSzdvS4nuNg==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.0", - "@vue/compiler-core": "3.5.30", - "@vue/compiler-dom": "3.5.30", - "@vue/compiler-ssr": "3.5.30", - "@vue/shared": "3.5.30", + "@babel/parser": "^7.29.7", + "@vue/compiler-core": "3.5.38", + "@vue/compiler-dom": "3.5.38", + "@vue/compiler-ssr": "3.5.38", + "@vue/shared": "3.5.38", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", - "postcss": "^8.5.8", + "postcss": "^8.5.15", "source-map-js": "^1.2.1" } }, "node_modules/@vue/compiler-ssr": { - "version": "3.5.30", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.30.tgz", - "integrity": "sha512-NsYK6OMTnx109PSL2IAyf62JP6EUdk4Dmj6AkWcJGBvN0dQoMYtVekAmdqgTtWQgEJo+Okstbf/1p7qZr5H+bA==", + "version": "3.5.38", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.38.tgz", + "integrity": "sha512-7s+W5Gc42FGxZMcuwl8H5B29T8BJPMdBT7KHFE+BbAuZ/iTEdTtv7z2XiMjiaUUw4w3ZcCEdHs36RuYJ2VA7bA==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.30", - "@vue/shared": "3.5.30" + "@vue/compiler-dom": "3.5.38", + "@vue/shared": "3.5.38" } }, "node_modules/@vue/devtools-api": { @@ -4491,53 +5371,53 @@ } }, "node_modules/@vue/reactivity": { - "version": "3.5.30", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.30.tgz", - "integrity": "sha512-179YNgKATuwj9gB+66snskRDOitDiuOZqkYia7mHKJaidOMo/WJxHKF8DuGc4V4XbYTJANlfEKb0yxTQotnx4Q==", + "version": "3.5.38", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.38.tgz", + "integrity": "sha512-pG6LV/NDNRbKizcUjFFLAfjaL8mcv4DmR9avNcUw2gDHBzZneuS2TWCmp633ynzxz9YYKNeEPK2I8Wraqy2HUQ==", "license": "MIT", "dependencies": { - "@vue/shared": "3.5.30" + "@vue/shared": "3.5.38" } }, "node_modules/@vue/runtime-core": { - "version": "3.5.30", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.30.tgz", - "integrity": "sha512-e0Z+8PQsUTdwV8TtEsLzUM7SzC7lQwYKePydb7K2ZnmS6jjND+WJXkmmfh/swYzRyfP1EY3fpdesyYoymCzYfg==", + "version": "3.5.38", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.38.tgz", + "integrity": "sha512-iyW8WVfF1CpCXxncZY5Ei6rSd6oZr5DgEom//fUjRBRl56AXPD+s9ATvukRt77ZFTuYlnVA1bxY+dJB94tWVYw==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.30", - "@vue/shared": "3.5.30" + "@vue/reactivity": "3.5.38", + "@vue/shared": "3.5.38" } }, "node_modules/@vue/runtime-dom": { - "version": "3.5.30", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.30.tgz", - "integrity": "sha512-2UIGakjU4WSQ0T4iwDEW0W7vQj6n7AFn7taqZ9Cvm0Q/RA2FFOziLESrDL4GmtI1wV3jXg5nMoJSYO66egDUBw==", + "version": "3.5.38", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.38.tgz", + "integrity": "sha512-apX2wt9sdfDshS+a2xueFZLVpt0GkRJZSoPmrW/SA4yzXTznhfcMVW59gr7h4YQeY0vJhdJkk2rsIDwgfFgC5A==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.30", - "@vue/runtime-core": "3.5.30", - "@vue/shared": "3.5.30", + "@vue/reactivity": "3.5.38", + "@vue/runtime-core": "3.5.38", + "@vue/shared": "3.5.38", "csstype": "^3.2.3" } }, "node_modules/@vue/server-renderer": { - "version": "3.5.30", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.30.tgz", - "integrity": "sha512-v+R34icapydRwbZRD0sXwtHqrQJv38JuMB4JxbOxd8NEpGLny7cncMp53W9UH/zo4j8eDHjQ1dEJXwzFQknjtQ==", + "version": "3.5.38", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.38.tgz", + "integrity": "sha512-vue8vbf2QlV4quHqzwmJy6dWfmRhP1J8l4wtZg60CL6VoKqcPY2oe7may3+1d9qfpedjK5PRLFqd5k3Isj9mUw==", "license": "MIT", "dependencies": { - "@vue/compiler-ssr": "3.5.30", - "@vue/shared": "3.5.30" + "@vue/compiler-ssr": "3.5.38", + "@vue/shared": "3.5.38" }, "peerDependencies": { - "vue": "3.5.30" + "vue": "3.5.38" } }, "node_modules/@vue/shared": { - "version": "3.5.30", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.30.tgz", - "integrity": "sha512-YXgQ7JjaO18NeK2K9VTbDHaFy62WrObMa6XERNfNOkAhD1F1oDSf3ZJ7K6GqabZ0BvSDHajp8qfS5Sa2I9n8uQ==", + "version": "3.5.38", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.38.tgz", + "integrity": "sha512-FTW0AFZNaK5/mOqvGBwVfUlNLU38TiQn4+DQgIFUnrBBJQ1crMJ82yeGQLV5jyKFsO8yRukpbuP7x+nRbH6aug==", "license": "MIT" }, "node_modules/@vueuse/core": { @@ -4732,24 +5612,24 @@ } }, "node_modules/@wordpress/env": { - "version": "11.3.0", - "resolved": "https://registry.npmjs.org/@wordpress/env/-/env-11.3.0.tgz", - "integrity": "sha512-LxJGteosQmrOV8JEqEIeh3N8/poplFexJjW2M0b+GpKfd7Njt6zoEZhONo01YIR01gk2d21HPBApBw5goa84Xg==", + "version": "11.10.0", + "resolved": "https://registry.npmjs.org/@wordpress/env/-/env-11.10.0.tgz", + "integrity": "sha512-y0q2RLJ3PsWHrRPt7bQbJauF/23JvCkfjBHRefJlGDZ49x1ma+UYQCrhT07G/05CYYJH2Sw6qC3Q4XYBKe7ASA==", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { "@inquirer/prompts": "^7.2.0", "@wp-playground/cli": "^3.0.48", - "chalk": "^4.0.0", + "adm-zip": "^0.5.9", + "chalk": "^4.1.1", "copy-dir": "^1.3.0", "cross-spawn": "^7.0.6", "docker-compose": "^0.24.3", - "extract-zip": "^1.6.7", "got": "^11.8.5", "js-yaml": "^3.13.1", "ora": "^4.0.2", "rimraf": "^5.0.10", - "simple-git": "^3.5.0", + "simple-git": "^3.24.0", "yargs": "^17.3.0" }, "bin": { @@ -5342,6 +6222,16 @@ "node": ">= 0.6" } }, + "node_modules/adm-zip": { + "version": "0.5.17", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.17.tgz", + "integrity": "sha512-+Ut8d9LLqwEvHHJl1+PIHqoyDxFgVN847JTVM3Izi3xHDWPE4UtzzXysMZQs64DMcrJfBeS/uoEP4AD3HQHnQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0" + } + }, "node_modules/aggregate-error": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", @@ -5396,15 +6286,6 @@ "node": ">= 14.0.0" } }, - "node_modules/ansi-colors": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", - "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -5611,15 +6492,6 @@ "node": ">=0.10.0" } }, - "node_modules/astral-regex": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", - "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/async": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", @@ -6092,15 +6964,6 @@ "ieee754": "^1.1.13" } }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true, - "engines": { - "node": "*" - } - }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", @@ -6490,21 +7353,68 @@ } }, "node_modules/cli-truncate": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", - "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.2.0.tgz", + "integrity": "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==", "dev": true, + "license": "MIT", "dependencies": { - "slice-ansi": "^3.0.0", - "string-width": "^4.2.0" + "slice-ansi": "^8.0.0", + "string-width": "^8.2.0" }, "engines": { - "node": ">=8" + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/cli-truncate/node_modules/string-width": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.1.tgz", + "integrity": "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cli-truncate/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/cli-width": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", @@ -6590,7 +7500,8 @@ "version": "2.0.20", "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/colors": { "version": "1.1.2", @@ -6667,21 +7578,6 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, - "node_modules/concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "dev": true, - "engines": [ - "node >= 0.8" - ], - "dependencies": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - } - }, "node_modules/console-browserify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", @@ -6849,14 +7745,14 @@ "license": "MIT" }, "node_modules/cypress": { - "version": "15.13.1", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-15.13.1.tgz", - "integrity": "sha512-jLkgo75zlwo7PhXp0XJot+zIfFSDzN1SvTml6Xf3ETM1XHRWnH3Q4LAR3orCo/BsnxPnhjG3m5HYSvn9DAtwBg==", + "version": "15.18.0", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-15.18.0.tgz", + "integrity": "sha512-aLfOYSLlVt1b6QSoVUjbCY27taZlYAT8ST47xQbwd9pvQrY/g5gXi12yItZTB+kxkkj+ZcvUYmRLUC95SlCJsw==", "dev": true, "hasInstallScript": true, "license": "MIT", "dependencies": { - "@cypress/request": "^3.0.10", + "@cypress/request": "^4.0.0", "@cypress/xvfb": "^1.2.4", "@types/sinonjs__fake-timers": "8.1.1", "@types/sizzle": "^2.3.2", @@ -6865,25 +7761,21 @@ "blob-util": "^2.0.2", "bluebird": "^3.7.2", "buffer": "^5.7.1", - "cachedir": "^2.3.0", + "cachedir": "^2.4.0", "chalk": "^4.1.0", "ci-info": "^4.1.0", - "cli-cursor": "^3.1.0", "cli-table3": "0.6.1", "commander": "^6.2.1", "common-tags": "^1.8.0", "dayjs": "^1.10.4", "debug": "^4.3.4", - "enquirer": "^2.3.6", "eventemitter2": "6.4.7", "execa": "4.1.0", "executable": "^4.1.1", - "extract-zip": "2.0.1", - "figures": "^3.2.0", "fs-extra": "^9.1.0", "hasha": "5.2.2", "is-installed-globally": "~0.4.0", - "listr2": "^3.8.3", + "listr2": "^9.0.5", "lodash": "^4.17.23", "log-symbols": "^4.0.0", "minimist": "^1.2.8", @@ -6898,7 +7790,7 @@ "tree-kill": "1.2.2", "tslib": "1.14.1", "untildify": "^4.0.0", - "yauzl": "^2.10.0" + "yauzl": "^3.3.1" }, "bin": { "cypress": "bin/cypress" @@ -6968,26 +7860,6 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "node_modules/cypress/node_modules/extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "dev": true, - "dependencies": { - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - }, - "engines": { - "node": ">= 10.17.0" - }, - "optionalDependencies": { - "@types/yauzl": "^2.9.1" - } - }, "node_modules/cypress/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -7035,6 +7907,19 @@ "dev": true, "license": "0BSD" }, + "node_modules/cypress/node_modules/yauzl": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.4.0.tgz", + "integrity": "sha512-jIH9yLR9wqr0wOS0TpBvo/g/2UgZH5qePVbjgRliiF0BYvOZyaBknKsF+x9Iht0O6sqgnB93rCICdOZFecJuDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", @@ -7614,21 +8499,8 @@ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", "dev": true, - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/enquirer": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", - "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", - "dev": true, - "dependencies": { - "ansi-colors": "^4.1.1", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8.6" + "dependencies": { + "once": "^1.4.0" } }, "node_modules/entities": { @@ -7637,6 +8509,19 @@ "integrity": "sha512-LbLqfXgJMmy81t+7c14mnulFHJ170cM6E+0vMXR9k/ZiZwgX8i5pNgjTCX3SO4VeUsFLV+8InixoretwU+MjBQ==", "dev": true }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/error": { "version": "7.2.1", "resolved": "https://registry.npmjs.org/error/-/error-7.2.1.tgz", @@ -7808,6 +8693,13 @@ "integrity": "sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==", "dev": true }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "dev": true, + "license": "MIT" + }, "node_modules/events-to-array": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/events-to-array/-/events-to-array-1.1.2.tgz", @@ -8124,36 +9016,6 @@ "node": ">=0.10.0" } }, - "node_modules/extract-zip": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.7.0.tgz", - "integrity": "sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA==", - "dev": true, - "dependencies": { - "concat-stream": "^1.6.2", - "debug": "^2.6.9", - "mkdirp": "^0.5.4", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - } - }, - "node_modules/extract-zip/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/extract-zip/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - }, "node_modules/extsprintf": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", @@ -8202,15 +9064,6 @@ "node": ">=0.4.0" } }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "dev": true, - "dependencies": { - "pend": "~1.2.0" - } - }, "node_modules/feiertagejs": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/feiertagejs/-/feiertagejs-1.5.1.tgz", @@ -8574,6 +9427,21 @@ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -8620,6 +9488,19 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-east-asian-width": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", + "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -8897,24 +9778,25 @@ "dev": true }, "node_modules/grunt": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/grunt/-/grunt-1.6.1.tgz", - "integrity": "sha512-/ABUy3gYWu5iBmrUSRBP97JLpQUm0GgVveDCp6t3yRNIoltIYw7rEj3g5y1o2PGPR2vfTRGa7WC/LZHLTXnEzA==", + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/grunt/-/grunt-1.6.2.tgz", + "integrity": "sha512-bUzh5nA/P5L66ihXTDP6J5BGnMB/8lXJXejYWSbH4Y4TvWM9t2S39sggQDYYQlx06cYcCsmu63HMYHGCIzUVfg==", "dev": true, + "license": "MIT", "dependencies": { "dateformat": "~4.6.2", "eventemitter2": "~0.4.13", "exit": "~0.1.2", "findup-sync": "~5.0.0", "glob": "~7.1.6", - "grunt-cli": "~1.4.3", + "grunt-cli": "^1.4.3", "grunt-known-options": "~2.0.0", "grunt-legacy-log": "~3.0.0", "grunt-legacy-util": "~2.0.1", "iconv-lite": "~0.6.3", "js-yaml": "~3.14.0", - "minimatch": "~3.0.4", - "nopt": "~3.0.6" + "minimatch": "^3.1.5", + "nopt": "^5.0.0" }, "bin": { "grunt": "bin/grunt" @@ -8955,21 +9837,6 @@ "node": ">=10" } }, - "node_modules/grunt-cli/node_modules/nopt": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", - "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", - "dev": true, - "dependencies": { - "abbrev": "1" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/grunt-contrib-clean": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/grunt-contrib-clean/-/grunt-contrib-clean-2.0.1.tgz", @@ -9525,62 +10392,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/grunt/node_modules/grunt-cli": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/grunt-cli/-/grunt-cli-1.4.3.tgz", - "integrity": "sha512-9Dtx/AhVeB4LYzsViCjUQkd0Kw0McN2gYpdmGYKtE2a5Yt7v1Q+HYZVWhqXc/kGnxlMtqKDxSwotiGeFmkrCoQ==", - "dev": true, - "dependencies": { - "grunt-known-options": "~2.0.0", - "interpret": "~1.1.0", - "liftup": "~3.0.1", - "nopt": "~4.0.1", - "v8flags": "~3.2.0" - }, - "bin": { - "grunt": "bin/grunt" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/grunt/node_modules/grunt-cli/node_modules/nopt": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", - "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", - "dev": true, - "dependencies": { - "abbrev": "1", - "osenv": "^0.1.4" - }, - "bin": { - "nopt": "bin/nopt.js" - } - }, - "node_modules/grunt/node_modules/minimatch": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz", - "integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/grunt/node_modules/v8flags": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.2.0.tgz", - "integrity": "sha512-mH8etigqMfiGWdeXpaaqGfs6BndypxusHHcv2qSHyZkGEznCd/qAXCWWRzeowtL54147cktFOC4P5y+kl8d8Jg==", - "dev": true, - "dependencies": { - "homedir-polyfill": "^1.0.1" - }, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/gzip-size": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-5.1.1.tgz", @@ -10011,10 +10822,11 @@ } }, "node_modules/immutable": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.0.3.tgz", - "integrity": "sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw==", - "dev": true + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.5.tgz", + "integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==", + "dev": true, + "license": "MIT" }, "node_modules/imurmurhash": { "version": "0.1.4", @@ -11037,39 +11849,106 @@ } }, "node_modules/listr2": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-3.14.0.tgz", - "integrity": "sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz", + "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==", "dev": true, + "license": "MIT", "dependencies": { - "cli-truncate": "^2.1.0", - "colorette": "^2.0.16", - "log-update": "^4.0.0", - "p-map": "^4.0.0", - "rfdc": "^1.3.0", - "rxjs": "^7.5.1", - "through": "^2.3.8", - "wrap-ansi": "^7.0.0" + "cli-truncate": "^5.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" }, "engines": { - "node": ">=10.0.0" + "node": ">=20.0.0" + } + }, + "node_modules/listr2/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" }, - "peerDependencies": { - "enquirer": ">= 2.3.0 < 3" + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/listr2/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" }, - "peerDependenciesMeta": { - "enquirer": { - "optional": true - } + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/listr2/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/listr2/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/listr2/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/listr2/node_modules/rxjs": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", - "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "node_modules/listr2/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", "dev": true, + "license": "MIT", "dependencies": { - "tslib": "^2.1.0" + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, "node_modules/livereload-js": { @@ -11175,133 +12054,267 @@ "dev": true, "license": "MIT" }, - "node_modules/lodash.isstring": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", - "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.keys": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", + "integrity": "sha512-CuBsapFjcubOGMn3VD+24HOAPxM79tH+V6ivJL3CHYjtrawauDJHUk//Yew9Hvc6e9rbCrURGk8z6PC+8WJBfQ==", + "dev": true, + "dependencies": { + "lodash._getnative": "^3.0.0", + "lodash.isarguments": "^3.0.0", + "lodash.isarray": "^3.0.0" + } + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "dev": true + }, + "node_modules/lodash.toarray": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/lodash.toarray/-/lodash.toarray-3.0.2.tgz", + "integrity": "sha512-ptkjUqvuHjTuMJJxiktJpZhxM5l60bEkfntJx+NFzdQd1bZVxfpTF1bhFYFqBrT4F0wZ1qx9KbVmHJV3Rfc7Tw==", + "dev": true, + "dependencies": { + "lodash._arraycopy": "^3.0.0", + "lodash._basevalues": "^3.0.0", + "lodash.keys": "^3.0.0" + } + }, + "node_modules/log-symbols": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz", + "integrity": "sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==", + "dev": true, + "dependencies": { + "chalk": "^2.4.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } }, - "node_modules/lodash.keys": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", - "integrity": "sha512-CuBsapFjcubOGMn3VD+24HOAPxM79tH+V6ivJL3CHYjtrawauDJHUk//Yew9Hvc6e9rbCrURGk8z6PC+8WJBfQ==", + "node_modules/log-update/node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", "dev": true, + "license": "MIT", "dependencies": { - "lodash._getnative": "^3.0.0", - "lodash.isarguments": "^3.0.0", - "lodash.isarray": "^3.0.0" + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lodash.once": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", - "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", - "dev": true + "node_modules/log-update/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" }, - "node_modules/lodash.toarray": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/lodash.toarray/-/lodash.toarray-3.0.2.tgz", - "integrity": "sha512-ptkjUqvuHjTuMJJxiktJpZhxM5l60bEkfntJx+NFzdQd1bZVxfpTF1bhFYFqBrT4F0wZ1qx9KbVmHJV3Rfc7Tw==", + "node_modules/log-update/node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", "dev": true, + "license": "MIT", "dependencies": { - "lodash._arraycopy": "^3.0.0", - "lodash._basevalues": "^3.0.0", - "lodash.keys": "^3.0.0" + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/log-symbols": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz", - "integrity": "sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==", + "node_modules/log-update/node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", "dev": true, + "license": "MIT", "dependencies": { - "chalk": "^2.4.2" + "mimic-function": "^5.0.0" }, "engines": { - "node": ">=8" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/log-update": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz", - "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==", + "node_modules/log-update/node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-escapes": "^4.3.0", - "cli-cursor": "^3.1.0", - "slice-ansi": "^4.0.0", - "wrap-ansi": "^6.2.0" + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/log-update/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/log-update/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", "dev": true, + "license": "MIT", "dependencies": { - "color-convert": "^2.0.1" + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" }, "engines": { - "node": ">=8" + "node": ">=18" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, - "node_modules/log-update/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/log-update/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, + "license": "MIT", "dependencies": { - "color-name": "~1.1.4" + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=7.0.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/log-update/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/log-update/node_modules/slice-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", - "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "node_modules/log-update/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" + "ansi-regex": "^6.2.2" }, "engines": { - "node": ">=10" + "node": ">=12" }, "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, "node_modules/log-update/node_modules/wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=8" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, "node_modules/lowercase-keys": { @@ -11857,6 +12870,19 @@ "node": ">=6" } }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mimic-response": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", @@ -11867,10 +12893,11 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -11952,18 +12979,6 @@ "node": ">=0.10.0" } }, - "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dev": true, - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, "node_modules/moment": { "version": "2.30.1", "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", @@ -11995,9 +13010,9 @@ "optional": true }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", "funding": [ { "type": "github", @@ -12084,15 +13099,19 @@ } }, "node_modules/nopt": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", - "integrity": "sha512-4GUt3kSEYmk4ITxzB/b9vaIDfUVWN/Ml1Fwl11IlnIG2iaJ9O6WXZ9SrYM9NLI8OCBieN2Y8SWC2oJV0RQ7qYg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", "dev": true, + "license": "ISC", "dependencies": { "abbrev": "1" }, "bin": { "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" } }, "node_modules/normalize-path": { @@ -12560,35 +13579,6 @@ "node": ">=8" } }, - "node_modules/os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/osenv": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", - "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", - "deprecated": "This package is no longer supported.", - "dev": true, - "dependencies": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, "node_modules/ospath": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/ospath/-/ospath-1.2.2.tgz", @@ -12646,21 +13636,6 @@ "node": ">=8" } }, - "node_modules/p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", - "dev": true, - "dependencies": { - "aggregate-error": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -12937,9 +13912,9 @@ } }, "node_modules/postcss": { - "version": "8.5.8", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", - "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", "funding": [ { "type": "opencollective", @@ -12956,7 +13931,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -12976,9 +13951,9 @@ } }, "node_modules/prettier": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", - "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "version": "3.8.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.4.tgz", + "integrity": "sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q==", "dev": true, "license": "MIT", "bin": { @@ -13012,12 +13987,6 @@ "node": ">= 0.6.0" } }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true - }, "node_modules/process-on-spawn": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.0.0.tgz", @@ -13163,21 +14132,6 @@ "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", "dev": true }, - "node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -13551,48 +14505,50 @@ "dev": true }, "node_modules/sass": { - "version": "1.97.3", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.97.3.tgz", - "integrity": "sha512-fDz1zJpd5GycprAbu4Q2PV/RprsRtKC/0z82z0JLgdytmcq0+ujJbJ/09bPGDxCLkKY3Np5cRAOcWiVkLXJURg==", + "version": "1.101.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.101.0.tgz", + "integrity": "sha512-OL3GoQyoUdDt843DpVmDO6y2k1sc5IhUDSpu8XucEI+35neq5QivZ1iuegnpraEVTJXlQGK1gl27zKcTLEPbQw==", "dev": true, "license": "MIT", "dependencies": { - "chokidar": "^4.0.0", - "immutable": "^5.0.2", + "chokidar": "^5.0.0", + "immutable": "^5.1.5", "source-map-js": ">=0.6.2 <2.0.0" }, "bin": { "sass": "sass.js" }, "engines": { - "node": ">=14.0.0" + "node": ">=20.19.0" }, "optionalDependencies": { "@parcel/watcher": "^2.4.1" } }, "node_modules/sass/node_modules/chokidar": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", - "integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", "dev": true, + "license": "MIT", "dependencies": { - "readdirp": "^4.0.1" + "readdirp": "^5.0.0" }, "engines": { - "node": ">= 14.16.0" + "node": ">= 20.19.0" }, "funding": { "url": "https://paulmillr.com/funding/" } }, "node_modules/sass/node_modules/readdirp": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", - "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", "dev": true, + "license": "MIT", "engines": { - "node": ">= 14.16.0" + "node": ">= 20.19.0" }, "funding": { "type": "individual", @@ -13616,10 +14572,13 @@ "license": "MIT" }, "node_modules/select2": { - "version": "4.0.13", - "resolved": "https://registry.npmjs.org/select2/-/select2-4.0.13.tgz", - "integrity": "sha512-1JeB87s6oN/TDxQQYCvS5EFoQyvV6eYMZZ0AeA4tdFDYWN3BAGZ8npr17UBFddU0lgAt3H0yjX3X6/ekOj1yjw==", - "license": "MIT" + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/select2/-/select2-4.1.0.tgz", + "integrity": "sha512-i9KalWOP4/LRRGc8+rj2krNm0ZqP14cV+j1TRCEBSsOhCPkKH8rYZ2MCRXcgvqIqN+llqGci0hj9aVkCMvL0+g==", + "license": "MIT", + "engines": { + "node": ">=24" + } }, "node_modules/selfsigned": { "version": "5.5.0", @@ -13994,52 +14953,51 @@ } }, "node_modules/slice-ansi": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", - "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-8.0.0.tgz", + "integrity": "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" + "ansi-styles": "^6.2.3", + "is-fullwidth-code-point": "^5.1.0" }, "engines": { - "node": ">=8" + "node": ">=20" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, "node_modules/slice-ansi/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" }, "funding": { "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/slice-ansi/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", "dev": true, + "license": "MIT", "dependencies": { - "color-name": "~1.1.4" + "get-east-asian-width": "^1.3.1" }, "engines": { - "node": ">=7.0.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/slice-ansi/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, "node_modules/snapdragon": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", @@ -16667,12 +17625,6 @@ "integrity": "sha512-rkTVqu6IjfQ/6+uNuuc3sZek4CEYxTJom3IktzgdSxcZqdARuebbA/f4QmAxMQIxqq9ZLEUkSYqvuk1I6VKq4g==", "dev": true }, - "node_modules/through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", - "dev": true - }, "node_modules/tiny-emitter": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", @@ -16974,12 +17926,6 @@ "node": ">= 0.4" } }, - "node_modules/typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", - "dev": true - }, "node_modules/typedarray-to-buffer": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", @@ -17586,16 +18532,16 @@ } }, "node_modules/vue": { - "version": "3.5.30", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.30.tgz", - "integrity": "sha512-hTHLc6VNZyzzEH/l7PFGjpcTvUgiaPK5mdLkbjrTeWSRcEfxFrv56g/XckIYlE9ckuobsdwqd5mk2g1sBkMewg==", + "version": "3.5.38", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.38.tgz", + "integrity": "sha512-vAMKHfImQlYSy0C+PBue4s3ERZ2xGKfgZg5GXAsLInq1dyh2H78ILVP5sK0KPFPVW4kv+OGCIvBEondcjpZp7A==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.30", - "@vue/compiler-sfc": "3.5.30", - "@vue/runtime-dom": "3.5.30", - "@vue/server-renderer": "3.5.30", - "@vue/shared": "3.5.30" + "@vue/compiler-dom": "3.5.38", + "@vue/compiler-sfc": "3.5.38", + "@vue/runtime-dom": "3.5.38", + "@vue/server-renderer": "3.5.38", + "@vue/shared": "3.5.38" }, "peerDependencies": { "typescript": "*" @@ -17904,16 +18850,6 @@ "node": ">=12" } }, - "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "dev": true, - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" - } - }, "node_modules/yoctocolors-cjs": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz", diff --git a/package.json b/package.json index 64700cc75..4974ed749 100644 --- a/package.json +++ b/package.json @@ -9,12 +9,12 @@ "url": "https://github.com/wielebenwir/cb" }, "devDependencies": { - "@babel/preset-env": "^7.29.2", - "@wordpress/env": "^11.0.0", + "@babel/preset-env": "^7.29.7", + "@wordpress/env": "^11.10.0", "commons-api": "git+https://github.com/wielebenwir/commons-api.git", - "cypress": "^15.13.1", + "cypress": "^15.18.0", "editorconfig": "^3.0.2", - "grunt": "^1.6.1", + "grunt": "^1.6.2", "grunt-babel": "^8.0.0", "grunt-cli": "^1.5.0", "grunt-contrib-clean": "^2.0.1", @@ -27,15 +27,17 @@ "grunt-dart-sass": "^2.0.1", "matchdep": "^2.0.0", "medium-zoom": "^1.1.0", - "sass": "^1.97.3", + "sass": "^1.101.0", "vitepress": "^1.6.4", - "prettier": "^3.3.3" + "prettier": "^3.8.4" }, "scripts": { "start": "composer install --ignore-platform-reqs && npm install && npm run dist", "env": "wp-env", "env:start": "wp-env start", "env:stop": "wp-env stop", + "env:test:start": "wp-env start --config=.wp-env.test.json", + "env:test:stop": "wp-env stop --config=.wp-env.test.json", "cypress:setup": "./bin/setup-cypress-env.sh", "cypress:open": "cypress open --config-file tests/cypress/cypress.config.js", "cypress:run": "cypress run --config-file tests/cypress/cypress.config.js", @@ -53,9 +55,9 @@ "leaflet.markercluster": "^1.5.0", "moment": "^2.19.4", "search-insights": "^2.17.3", - "select2": "^4.0.13", + "select2": "^4.1.0", "shufflejs": "^6.1.1", "spin.js": "^2.3.2", - "vue": "^3.5.30" + "vue": "^3.5.38" } } diff --git a/readme.txt b/readme.txt index fcbcb10a3..e5ff1b205 100644 --- a/readme.txt +++ b/readme.txt @@ -3,7 +3,7 @@ Contributors: wielebenwirteam, m0rb, flegfleg, chriwen, hansmorb, datengraben Donate link: https://www.wielebenwir.de/verein/unterstutzen Tags: booking, calendar, sharing, commoning, open-source Requires at least: 5.9 -Tested up to: 6.9 +Tested up to: 7.0 Stable Tag: 2.10.10 Requires PHP: 8.1 License: GPLv2 or later diff --git a/src/API/AvailabilityRoute.php b/src/API/AvailabilityRoute.php index 026256be6..cf76a1fbc 100644 --- a/src/API/AvailabilityRoute.php +++ b/src/API/AvailabilityRoute.php @@ -38,12 +38,12 @@ class AvailabilityRoute extends BaseRoute { /** * This retrieves bookable timeframes and the different items assigned, with their respective availability. * - * @param bool $id The id of a {@see \CommonsBooking\Wordpress\CustomPostType\Item::post_type} post to search for + * @param ?int $id The id of a {@see \CommonsBooking\Wordpress\CustomPostType\Item::post_type} post to search for * * @return array * @throws Exception */ - public function getItemData( $id = false ): array { + public static function getItemData( $id = null ): array { $calendar = new Calendar( new Day( date( 'Y-m-d', time() ) ), new Day( date( 'Y-m-d', strtotime( '+2 weeks' ) ) ), // TODO why two weeks? seems like a configurable option @@ -51,7 +51,18 @@ public function getItemData( $id = false ): array { $id ? [ $id ] : [] ); - return $calendar->getAvailabilitySlots(); + /** + * Filters the CommonsAPI availability slots for an item. + * + * Lets integrations adjust the availability exposed through the API, e.g. + * to reflect an external booking source. + * + * @since 2.11.0 + * + * @param array $slots The availability slots. + * @param int|null $id The item ID the availability was requested for, or null for all items. + */ + return apply_filters( 'commonsbooking_api_availability_response', $calendar->getAvailabilitySlots(), $id ); } /** diff --git a/src/API/BaseRoute.php b/src/API/BaseRoute.php index 5e13be79d..f2ab20f75 100644 --- a/src/API/BaseRoute.php +++ b/src/API/BaseRoute.php @@ -187,7 +187,16 @@ public static function hasPermission(): bool { $isApiActive = Settings::getOption( 'commonsbooking_options_api', 'api-activated' ); $anonymousAccessAllowed = Settings::getOption( 'commonsbooking_options_api', 'apikey_not_required' ); $apiKey = array_key_exists( self::API_KEY_PARAM, $_REQUEST ) ? sanitize_text_field( $_REQUEST[ self::API_KEY_PARAM ] ) : false; - $apiShare = ApiShares::getByKey( $apiKey ); + if ( ! $apiKey ) { + // get apikey from headers (#2251) + if ( function_exists( 'getallheaders' ) ) { + $allHeaders = array_change_key_case( getallheaders(), CASE_LOWER ); + $apiKey = array_key_exists( self::API_KEY_PARAM, $allHeaders ) ? sanitize_text_field( $allHeaders[ self::API_KEY_PARAM ] ) : false; + } else { + $apiKey = array_key_exists( 'HTTP_' . strtoupper( self::API_KEY_PARAM ), $_SERVER ) ? sanitize_text_field( $_SERVER[ 'HTTP_' . strtoupper( self::API_KEY_PARAM ) ] ) : false; + } + } + $apiShare = ApiShares::getByKey( $apiKey ); // Only if api is active we return something if ( $isApiActive ) { diff --git a/src/API/GBFS/BaseRoute.php b/src/API/GBFS/BaseRoute.php index f44c3ecfc..a708363f4 100644 --- a/src/API/GBFS/BaseRoute.php +++ b/src/API/GBFS/BaseRoute.php @@ -4,6 +4,7 @@ namespace CommonsBooking\API\GBFS; use CommonsBooking\Repository\Location; +use CommonsBooking\Repository\PostRepository; use Exception; use stdClass; use WP_REST_Request; @@ -11,10 +12,15 @@ /** * Base class which implements retrieval of basic data attributes of - * GBFS spec. + * GBFS spec. Derive from this class, when you want to have a route that iterates over CustomPosts. * - * Note: When deriving from this class, implement \WP_REST_Controller::prepare_item_for_response, - * which is called in \BaseRoute::getItemData + * Note: When deriving from this class + * - implement \WP_REST_Controller::prepare_item_for_response, + * which is called in \BaseRoute::getItemData + * - implement $rest_base + * - implement $schemaUrl + * - (if necessary) overwrite getRepository + * - (if necessary) overwrite getListName */ class BaseRoute extends \CommonsBooking\API\BaseRoute { @@ -26,18 +32,14 @@ class BaseRoute extends \CommonsBooking\API\BaseRoute { * @return WP_REST_Response */ public function get_items( $request ): WP_REST_Response { - $response = new stdClass(); - $response->data = new stdClass(); - $response->data->stations = $this->getItemData( $request ); - $response->last_updated = date( 'c' ); // ISO-8601 timestamp - $response->ttl = 60; - $response->version = '3.1-RC2'; + $response = new stdClass(); + $response->data = new stdClass(); + $response->data->{static::getListName()} = $this->getItemData( $request ); + $response->last_updated = date( 'c' ); // ISO-8601 timestamp + $response->ttl = 60; + $response->version = '3.1-RC3'; - if ( WP_DEBUG ) { - $this->validateData( $response ); - } - - return new WP_REST_Response( $response, 200 ); + return $this->respond_with_validation( $response ); } /** @@ -48,12 +50,15 @@ public function get_items( $request ): WP_REST_Response { * @return array */ public function getItemData( $request ): array { - $data = []; - $locations = Location::get(); + $data = []; + $items = static::getRepository()::get(); - foreach ( $locations as $location ) { + foreach ( $items as $item ) { + if ( $item->getMeta( COMMONSBOOKING_METABOX_PREFIX . 'api_exclude' ) == 'on' ) { + continue; + } try { - $itemdata = $this->prepare_item_for_response( $location, $request ); + $itemdata = $this->prepare_item_for_response( $item, $request ); $data[] = $itemdata->data; } catch ( Exception $exception ) { if ( WP_DEBUG ) { @@ -64,4 +69,23 @@ public function getItemData( $request ): array { return $data; } + + /** + * Overwrite this, if you don't iterate over stations but need the resulting items in a list with a different name + * + * @return string + */ + protected static function getListName(): string { + return 'stations'; + } + + /** + * The post type that the route will iterate over. + * By default, these are all the locations. + * + * @return PostRepository + */ + protected static function getRepository(): PostRepository { + return new Location(); + } } diff --git a/src/API/GBFS/Discovery.php b/src/API/GBFS/Discovery.php index 0a31b16e9..0f923c88f 100644 --- a/src/API/GBFS/Discovery.php +++ b/src/API/GBFS/Discovery.php @@ -34,23 +34,36 @@ class Discovery extends \CommonsBooking\API\BaseRoute { */ public function get_items( $request ): WP_REST_Response { - $feeds = array(); - $feeds[] = $this->get_feed( 'system_information' ); - $feeds[] = $this->get_feed( 'station_information' ); - $feeds[] = $this->get_feed( 'station_status' ); + /** + * The names of the feeds that are available in the GBFS route. + * They will be announced through the gbfs.json. + * The routes still need to be registered using register_rest_route. + * + * @since 2.11 + * + * @param String[] $raw_feeds the names of the feeds without a .json suffix + */ + $raw_feeds = apply_filters( + 'commonsbooking_gbfs_feeds', + [ + 'system_information', + 'station_information', + 'station_status', + 'vehicle_availability', + 'vehicle_status', + 'vehicle_types', + ] + ); + $feeds = array_map( fn( $feed ) => $this->get_feed( $feed ), $raw_feeds ); $response = new stdClass(); $response->data = new stdClass(); $response->data->feeds = $feeds; $response->last_updated = date( 'c' ); // ISO-8601 timestamp $response->ttl = 86400; - $response->version = '3.1-RC2'; + $response->version = '3.1-RC3'; - if ( WP_DEBUG ) { - $this->validateData( $response ); - } - - return new WP_REST_Response( $response, 200 ); + return $this->respond_with_validation( $response ); } private function get_feed( $name ): stdClass { diff --git a/src/API/GBFS/StationInformation.php b/src/API/GBFS/StationInformation.php index 807cffaee..e3ea84ccc 100644 --- a/src/API/GBFS/StationInformation.php +++ b/src/API/GBFS/StationInformation.php @@ -35,7 +35,7 @@ class StationInformation extends BaseRoute { */ public function prepare_item_for_response( $item, $request ): WP_REST_Response { $preparedItem = new stdClass(); - $preparedItem->station_id = $item->ID . ''; + $preparedItem->station_id = strval( $item->ID ); $preparedItem->name = [ (object) [ 'text' => $item->post_title, diff --git a/src/API/GBFS/StationStatus.php b/src/API/GBFS/StationStatus.php index 85221c454..6c0f8156e 100644 --- a/src/API/GBFS/StationStatus.php +++ b/src/API/GBFS/StationStatus.php @@ -3,9 +3,6 @@ namespace CommonsBooking\API\GBFS; -use CommonsBooking\Helper\Wordpress; -use CommonsBooking\Model\Calendar; -use CommonsBooking\Model\Day; use CommonsBooking\Model\Location; use CommonsBooking\Repository\Item; use stdClass; @@ -28,61 +25,34 @@ class StationStatus extends BaseRoute { protected $schemaUrl = COMMONSBOOKING_PLUGIN_DIR . 'includes/gbfs-json-schema/station_status.json'; /** - * @param Location $item + * @param Location $location * @param $request * * @return WP_REST_Response * @throws \Exception */ - public function prepare_item_for_response( $item, $request ): WP_REST_Response { - $preparedItem = new stdClass(); - $preparedItem->station_id = $item->ID . ''; - $preparedItem->num_vehicles_available = $this->getItemCountAtLocation( $item->ID ); - $preparedItem->is_installed = true; - $preparedItem->is_renting = true; - $preparedItem->is_returning = true; - $preparedItem->last_reported = date( 'c' ); // ISO-8601 timestamp + public function prepare_item_for_response( $location, $request ): WP_REST_Response { + $availableItems = count( + array_filter( + Item::getByLocation( $location->ID, true ), + fn( $item ) => $item->isCurrentlyFreeAtLocation( $location->ID, true ) && $item->getMeta( COMMONSBOOKING_METABOX_PREFIX . 'api_exclude' ) != 'on' + ) + ); + + $preparedItem = new stdClass(); + $preparedItem->station_id = strval( $location->ID ); + $preparedItem->num_vehicles_available = $availableItems; + $preparedItem->vehicle_types_available = [ + (object) [ + 'vehicle_type_id' => VehicleTypes::DEFAULT_NAME, + 'count' => $availableItems, + ], + ]; + $preparedItem->is_installed = true; + $preparedItem->is_renting = true; + $preparedItem->is_returning = true; + $preparedItem->last_reported = date( 'c' ); // ISO-8601 timestamp return new WP_REST_Response( $preparedItem ); } - - /** - * Will get the amount of items that are currently in the location - * and marked as "green" (bookable right now by the user) for the current time. - * This purposefully excludes items that have a Holiday Timeframe, are bookable in advance - * or can only be booked through overbooking. - * This is because the GBFS spec only accounts for items available in that instant. - * - * @param int $locationId - * - * @return int - * @throws \Exception - */ - private function getItemCountAtLocation( $locationId ): int { - $items = Item::getByLocation( $locationId, true ); - $nowDT = Wordpress::getUTCDateTimeByTimestamp( current_time( 'timestamp' ) ); - $availableCounter = 0; - foreach ( $items as $item ) { - // we have to make our calendar span at least one day, otherwise we get no results - $itemCalendar = new Calendar( - new Day( date( 'Y-m-d', strtotime( '-1 day' ) ) ), - new Day( date( 'Y-m-d', strtotime( '+1 day' ) ) ), - [ $locationId ], - [ $item->ID ] - ); - $availabilitySlots = $itemCalendar->getAvailabilitySlots(); - // we have to iterate over multiple slots because the calendar will give us more than we asked for - foreach ( $availabilitySlots as $availabilitySlot ) { - // match our exact current time to the slot - $startDT = new \DateTime( $availabilitySlot->start ); - $endDT = new \DateTime( $availabilitySlot->end ); - if ( $nowDT >= $startDT && $nowDT <= $endDT ) { - ++$availableCounter; - // break out of the loop, we only need one match of availability per item - break; - } - } - } - return $availableCounter; - } } diff --git a/src/API/GBFS/SystemInformation.php b/src/API/GBFS/SystemInformation.php index 7eb4e4ad1..95fd9c512 100644 --- a/src/API/GBFS/SystemInformation.php +++ b/src/API/GBFS/SystemInformation.php @@ -3,6 +3,8 @@ namespace CommonsBooking\API\GBFS; +use CommonsBooking\CB\CB; +use CommonsBooking\Repository\Timeframe; use stdClass; use WP_REST_Response; @@ -36,19 +38,24 @@ public function get_items( $request ): WP_REST_Response { 'language' => get_bloginfo( 'language' ), ], ]; - $response->data->opening_hours = '24/7'; // TODO: Close, when no items are available - $response->data->system_id = sha1( site_url() ); + $response->data->opening_hours = $this->isOpen() ? '24/7' : '24/7 closed'; + $response->data->system_id = COMMONSBOOKING_PLUGIN_SLUG . '_' . strtolower( preg_replace( '/\s+/', '_', get_bloginfo( 'name' ) ) ); $response->data->feed_contact_email = get_bloginfo( 'admin_email' ); $response->data->languages = [ get_bloginfo( 'language' ) ]; $response->data->timezone = $tz; $response->last_updated = date( 'c' ); // ISO-8601 timestamp; $response->ttl = 86400; - $response->version = '3.1-RC2'; + $response->version = '3.1-RC3'; - if ( WP_DEBUG ) { - $this->validateData( $response ); - } + return $this->respond_with_validation( $response ); + } - return new WP_REST_Response( $response, 200 ); + private function isOpen(): bool { + $timeframes = Timeframe::getBookable( + [], + [], + date( CB::getInternalDateFormat(), current_time( 'timestamp' ) ), + ); + return count( $timeframes ) > 0; } } diff --git a/src/API/GBFS/VehicleAvailability.php b/src/API/GBFS/VehicleAvailability.php new file mode 100644 index 000000000..f02b522f3 --- /dev/null +++ b/src/API/GBFS/VehicleAvailability.php @@ -0,0 +1,89 @@ +getLocation(); + if ( ! $location ) { + throw new \Exception( 'No location for item. (ID: ' . $item->ID . ')' ); + } + + $preparedItem = new stdClass(); + $preparedItem->vehicle_id = strval( $item->getCloakedId() ); + $preparedItem->vehicle_type_id = VehicleTypes::DEFAULT_NAME; + $preparedItem->station_id = strval( $location->ID ); // This is what you could consider the home location. Regardless if the item is there atm or not. + $preparedItem->availabilities = self::getAvailabilities( $item ); + + return new WP_REST_Response( $preparedItem ); + } + + private static function getAvailabilities( $item ): array { + $availabilities = AvailabilityRoute::getItemData( $item->ID ); + + if ( empty( $availabilities ) ) { + return []; + } + + $firstAvailability = array_shift( $availabilities ); + $slots = [ + (object) [ + 'from' => $firstAvailability->start, + 'until' => $firstAvailability->end, + ], + ]; + + foreach ( $availabilities as $availability ) { + $gapSeconds = strtotime( $availability->start ) - strtotime( end( $slots )->until ); + + if ( $gapSeconds < 59 ) { + end( $slots )->until = $availability->end; + } else { + $slots[] = (object) [ + 'from' => $availability->start, + 'until' => $availability->end, + ]; + } + } + + return $slots; + } + + protected static function getListName(): string { + return 'vehicles'; + } + + + protected static function getRepository(): PostRepository { + // we iterate over posts with cb_item post type + return new Item(); + } +} diff --git a/src/API/GBFS/VehicleStatus.php b/src/API/GBFS/VehicleStatus.php new file mode 100644 index 000000000..180357a75 --- /dev/null +++ b/src/API/GBFS/VehicleStatus.php @@ -0,0 +1,134 @@ +getLocation(); + + if ( ! $location ) { + throw new \Exception( 'No location for item. (ID: ' . $item->ID . ')' ); + } + + // Vehicles that are part of an active rental MUST NOT appear in this feed + if ( ! $item->isCurrentlyFreeAtLocation( $location->ID, true, true ) ) { + throw new \Exception( 'Item currently not available (skipped in VehicleStatus) ' . '(ID: ' . $item->ID . ')' ); + } + + $preparedItem = new stdClass(); + $preparedItem->vehicle_id = strval( $item->getCloakedId() ); + $preparedItem->vehicle_type_id = VehicleTypes::DEFAULT_NAME; + $preparedItem->station_id = strval( $location->ID ); + $preparedItem->is_reserved = false; // this never happens, we do not know the difference between the start of a booking period and if it has actually been picked up + $preparedItem->is_disabled = $this->isDisabled( $item, $location ); + $preparedItem->rental_uris = (object) [ + 'web' => $item->getCloakedURL(), + ]; + $preparedItem->available_until = $this->getAvailableUntil( $item ); + + return new WP_REST_Response( $preparedItem ); + } + + private function isDisabled( \CommonsBooking\Model\Item $item, Location $location ): bool { + $today = new Day( date( 'Y-m-d', time() ), [ $location->ID ], [ $item->ID ], [ Timeframe::BOOKABLE_ID ] ); + $restrictions = $today->getRestrictions(); + $restrictions = array_filter( + $restrictions, + fn( $restriction ) => $restriction->isActive() && $restriction->getType() == Restriction::TYPE_REPAIR + ); + + if ( empty( $restrictions ) ) { + return false; + } else { + return true; + } + } + + /** + * The date and time when any rental of the vehicle must be completed. + * The vehicle must be returned and made available for the next user by this time. + * If this field is empty, it indicates that the vehicle is available indefinitely. + * This field SHOULD be published by carsharing or other mobility systems where vehicles can be booked in advance for future travel. + * + * @param \CommonsBooking\Model\Item $item + * @return string in ISO 8601 notation + */ + private function getAvailableUntil( \CommonsBooking\Model\Item $item ): string { + $timeframe = $item->getClosestBookableTimeframe(); + + // Counting from the first bookable day (inclusive), maxDays of 3 means + // the item is bookable today, tomorrow, and the day after. Using + // maxDays as-is for the offset would incorrectly point to the fourth, + // unavailable day, so we subtract 1. + $maxDaysOffset = $timeframe->getMaxDays() - 1; + $firstBookableDay = $timeframe->getFirstBookableDay(); + $latestAllowedEnd = new \DateTime( $firstBookableDay . ' +' . $maxDaysOffset . ' day 23:59:59' ); + + $itemCalendar = new Calendar( + new Day( date( 'Y-m-d', strtotime( '-1 day' ) ) ), + new Day( date( 'Y-m-d', strtotime( '+' . $maxDaysOffset . ' day', strtotime( $firstBookableDay ) ) ) ), + [ $timeframe->getLocation()->ID ], + [ $item->ID ] + ); + $itemCalendar->setIgnoreStartDayOffset( true ); + + $slotsInRange = array_filter( + $itemCalendar->getAvailabilitySlots(), + fn( $slot ) => new \DateTime( $slot->end ) <= $latestAllowedEnd + ); + + usort( + $slotsInRange, + fn( $a, $b ) => new \DateTime( $a->end ) <=> new \DateTime( $b->end ) + ); + + $lastSlot = array_pop( $slotsInRange ); + + return ( new \DateTime( $lastSlot->end ) )->format( 'c' ); + } + + protected static function getListName(): string { + return 'vehicles'; + } + + + protected static function getRepository(): PostRepository { + // we iterate over posts with cb_item post type + return new Item(); + } +} diff --git a/src/API/GBFS/VehicleTypes.php b/src/API/GBFS/VehicleTypes.php new file mode 100644 index 000000000..5533d5650 --- /dev/null +++ b/src/API/GBFS/VehicleTypes.php @@ -0,0 +1,52 @@ +data = new stdClass(); + $response->data->vehicle_types = [ + (object) [ + 'vehicle_type_id' => self::DEFAULT_NAME, + 'form_factor' => 'cargo_bicycle', + 'propulsion_type' => 'human', + ], + ]; + $response->last_updated = date( 'c' ); // ISO-8601 timestamp + $response->ttl = 86400; + $response->version = '3.1-RC3'; + + return $this->respond_with_validation( $response ); + } +} diff --git a/src/API/ItemsRoute.php b/src/API/ItemsRoute.php index d8aa3b8c0..33b2d8f6f 100644 --- a/src/API/ItemsRoute.php +++ b/src/API/ItemsRoute.php @@ -51,7 +51,10 @@ public function getItemData( $request ): stdClass { $items = Item::get( $args ); foreach ( $items as $item ) { - $itemdata = $this->prepare_item_for_response( $item, $request ); + $itemdata = $this->prepare_item_for_response( $item, $request ); + if ( $item->getMeta( COMMONSBOOKING_METABOX_PREFIX . 'api_exclude' ) == 'on' ) { + continue; + } $data->items[] = $itemdata->get_data(); } @@ -140,6 +143,20 @@ public function prepare_item_for_response( $item, $request ): WP_REST_Response { ]; } + /** + * Filters the CommonsAPI item response object before it is returned. + * + * Lets integrations add or adjust fields exposed for an item. Note that + * added fields must conform to the CommonsAPI JSON schema or validation + * will reject the response. + * + * @since 2.11.0 + * + * @param stdClass $preparedItem The prepared item data. + * @param \WP_Post $item The source item post. + */ + $preparedItem = apply_filters( 'commonsbooking_api_item_response', $preparedItem, $item ); + return new WP_REST_Response( $preparedItem ); } } diff --git a/src/CB/CB1UserFields.php b/src/CB/CB1UserFields.php index ab13df649..995488cbc 100644 --- a/src/CB/CB1UserFields.php +++ b/src/CB/CB1UserFields.php @@ -8,6 +8,9 @@ use CommonsBooking\Settings\Settings; +/** + * @deprecated since 2.11, to be removed in 2.12 + */ class CB1UserFields { /** @@ -33,7 +36,7 @@ class CB1UserFields { /** * @var array|mixed */ - private $user_vars; // @phpstan-ignore property.onlyWritten + private $user_vars; public function __construct() { @@ -226,7 +229,7 @@ public function registration_add_meta( $user_id ) { /** * Sets a flat array of user field/value pairs * - * @since 2.10 deprecated (cb_object_to_array is unspecified) + * @since 2.10 deprecated (cb_object_to_array is unspecified), removal in 2.12 * @since 0.6 */ public function set_basic_user_vars( $user_id ) { @@ -248,6 +251,7 @@ public function set_basic_user_vars( $user_id ) { /** * Add addiotinal key/value pairs to the user_vars array * + * @deprecated since 2.11, removal in 2.12 * @since 0.5.3 */ public function add_user_vars( $key, $value ) { diff --git a/src/Helper/Wordpress.php b/src/Helper/Wordpress.php index 43256ccde..22d44f19e 100644 --- a/src/Helper/Wordpress.php +++ b/src/Helper/Wordpress.php @@ -320,6 +320,14 @@ public static function getUTCDateTime( $datetime = 'now' ): DateTime { return $dto; } + /** + * UNUSED + * + * @deprecated since 2.11, to be removed in 2.12 + * @param $timestamp + * @return DateTime + * @throws \DateInvalidTimeZoneException + */ public static function getLocalDateTime( $timestamp ): DateTime { $dto = new DateTime(); $dto->setTimestamp( diff --git a/src/Map/MapFilter.php b/src/Map/MapFilter.php index 7c3f5e5a4..cb2a839e2 100644 --- a/src/Map/MapFilter.php +++ b/src/Map/MapFilter.php @@ -2,8 +2,20 @@ namespace CommonsBooking\Map; +/** + * Class MapFilter + * + * @deprecated since 2.11, to be removed in 2.12 + */ class MapFilter { + /** + * @deprecated since 2.11, to be removed in 2.12 + * + * @param $item_terms + * @param $category_groups + * @return bool + */ protected static function check_item_terms_against_categories( $item_terms, $category_groups ): bool { $valid_groups_count = 0; diff --git a/src/Migration/Migration.php b/src/Migration/Migration.php index fef012036..613d9e27d 100644 --- a/src/Migration/Migration.php +++ b/src/Migration/Migration.php @@ -16,6 +16,9 @@ use WP_Query; /** + * + * @deprecated since 2.11, will be removed in 2.12 + * * The logic for handling the migration from CB1 to CB2. * The CB1 fields are fetched from the @see \CommonsBooking\Repository\CB1 repository and migrated using the * respective migration functions in this class. diff --git a/src/Model/Booking.php b/src/Model/Booking.php index 8a5a4411c..bcdfc5349 100755 --- a/src/Model/Booking.php +++ b/src/Model/Booking.php @@ -76,17 +76,24 @@ public function getBookingCode() { * @return bool */ public function canCancel(): bool { - if ( $this->isPast() ) { - return false; + $canCancel = false; + if ( ! $this->isPast() ) { + $canCancel = intval( $this->post_author ) === get_current_user_id() + || commonsbooking_isCurrentUserAllowedToEdit( $this->post ); } - if ( intval( $this->post_author ) === get_current_user_id() ) { - return true; - } elseif ( commonsbooking_isCurrentUserAllowedToEdit( $this->post ) ) { - return true; - } else { - return false; - } + /** + * Filters whether the current user may cancel this booking. + * + * Lets integrations relax or tighten the default cancellation policy + * (e.g. forbid cancelling within a notice period, or allow a custom role). + * + * @since 2.11.0 + * + * @param bool $canCancel Whether cancellation is allowed by default. + * @param Booking $booking The booking model instance. + */ + return (bool) apply_filters( 'commonsbooking_can_cancel_booking', $canCancel, $this ); } /** @@ -112,6 +119,22 @@ public function cancel(): void { update_post_meta( $this->post->ID, 'cancellation_time', current_time( 'timestamp' ) ); $this->sendCancellationMail(); + + /** + * Fires after a booking has been cancelled. + * + * Because cancellation writes the status directly to the database (to + * preserve meta data) it does not run through WordPress' post status + * transition, so this is the authoritative hook for reacting to a + * cancelled booking, e.g. releasing a smart lock or an external + * calendar slot. + * + * @since 2.11.0 + * + * @param int $booking_id The booking post ID. + * @param Booking $booking The booking model instance. + */ + do_action( 'commonsbooking_booking_cancelled', $this->post->ID, $this ); } /** diff --git a/src/Model/Calendar.php b/src/Model/Calendar.php index 806b52feb..ed33cf093 100644 --- a/src/Model/Calendar.php +++ b/src/Model/Calendar.php @@ -45,6 +45,27 @@ class Calendar { */ protected array $timeframes; + /** + * When this is enabled, @see Timeframe::META_BOOKING_START_DAY_OFFSET is ignored. + * This is used for the API routes, where we want to show the actual availability of the items, regardless of the booking start day offset. + * + * @var bool + */ + protected bool $ignoreStartDayOffset = false; + + /** + * When this is enabled, restrictions are ignored when creating availabilities. + * This is useful when you want to differentiate between an item that is booked / not available + * and an item that would be bookable but is in repair. + * + * Just passed to the \CommonsBooking\Model\Day model, because restriction calculations are made there + * + * Used in @see \CommonsBooking\API\GBFS\VehicleStatus to differentiate between booked items and disabled items + * + * @var bool + */ + protected bool $ignoreRestrictions = false; + /** * Calendar constructor. * @@ -60,6 +81,9 @@ public function __construct( Day $startDate, Day $endDate, array $locations = [] throw new \InvalidArgumentException( 'Calendar must span at least two days' ); } + $startDate->setIgnoreRestrictions( $this->ignoreRestrictions ); + $endDate->setIgnoreRestrictions( $this->ignoreRestrictions ); + $this->startDate = $startDate; $this->endDate = $endDate; $this->items = $items; @@ -139,6 +163,7 @@ public function getAvailabilitySlots(): array { foreach ( $this->getWeeks() as $week ) { /** @var Day $day */ foreach ( $week->getDays() as $day ) { + $day->setIgnoreRestrictions( $this->ignoreRestrictions ); foreach ( $day->getGrid() as $slot ) { $timeframe = new Timeframe( $slot['timeframe'] ); $timeFrameType = get_post_meta( $slot['timeframe']->ID, 'type', true ); @@ -149,7 +174,7 @@ public function getAvailabilitySlots(): array { } // Skip timeframes that are not bookable today - if ( $timeframe->getFirstBookableDay() > $day->getDate() ) { + if ( ! $this->ignoreStartDayOffset && $timeframe->getFirstBookableDay() > $day->getDate() ) { continue; } @@ -185,4 +210,12 @@ public function getAvailabilitySlots(): array { } return $slots; } + + public function setIgnoreStartDayOffset( bool $ignoreStartDayOffset ): void { + $this->ignoreStartDayOffset = $ignoreStartDayOffset; + } + + public function setIgnoreRestrictions( bool $ignoreRestrictions ): void { + $this->ignoreRestrictions = $ignoreRestrictions; + } } diff --git a/src/Model/CustomPost.php b/src/Model/CustomPost.php index ec47fd928..9b47bd342 100644 --- a/src/Model/CustomPost.php +++ b/src/Model/CustomPost.php @@ -29,6 +29,13 @@ class CustomPost { */ protected $post; + /** + * The post ID of the WordPress post + * + * @var int + */ + protected int $ID; + /** * @var string */ @@ -49,6 +56,10 @@ public function __construct( $post ) { } else { throw new Exception( 'Invalid post param. Needed WP_Post or ID (int)' ); } + if ( ! $this->post instanceof WP_Post ) { + throw new Exception( 'Invalid post param. No post found for ID ' . $post ); + } + $this->ID = $this->post->ID; } /** diff --git a/src/Model/Day.php b/src/Model/Day.php index 72d0ec13f..0e0d596db 100644 --- a/src/Model/Day.php +++ b/src/Model/Day.php @@ -42,6 +42,17 @@ class Day { */ protected ?array $timeframes = null; + /** + * When this is enabled, restrictions are ignored when creating availabilities. + * This is useful when you want to differentiate between an item that is booked / not available + * and an item that would be bookable but is in repair. + * + * Used in @see \CommonsBooking\API\GBFS\VehicleStatus to differentiate between booked items and disabled items + * + * @var bool + */ + protected bool $ignoreRestrictions = false; + /** * Day constructor. * @@ -151,7 +162,7 @@ public function getTimeframes(): array { /** * Returns array with restrictions. * - * @return array + * @return Restriction[] * @throws Exception */ public function getRestrictions(): array { @@ -173,7 +184,18 @@ public function getRestrictions(): array { * @throws Exception */ public function getGrid(): array { - return $this->getTimeframeSlots(); + /** + * Filters the availability slots computed for this day. + * + * Lets integrations add, remove or adjust the bookable slots of a day + * (e.g. to reflect an external availability source). + * + * @since 2.11.0 + * + * @param array $slots The computed slots for this day. + * @param Day $day The day model instance. + */ + return apply_filters( 'commonsbooking_day_availability', $this->getTimeframeSlots(), $this ); } /** @@ -545,7 +567,7 @@ protected function removeEmptySlots( &$slots ) { * @throws Exception */ protected function getTimeframeSlots(): array { - $customCacheKey = $this->getDate() . serialize( $this->items ) . serialize( $this->locations ); + $customCacheKey = $this->getDate() . serialize( $this->items ) . serialize( $this->locations ) . serialize( $this->ignoreRestrictions ); $customCacheKey = md5( $customCacheKey ); $cacheItem = Plugin::getCacheItem( $customCacheKey ); if ( $cacheItem ) { @@ -566,7 +588,9 @@ protected function getTimeframeSlots(): array { } $this->mapTimeFrames( $slots ); - $this->mapRestrictions( $slots ); + if ( ! $this->ignoreRestrictions ) { + $this->mapRestrictions( $slots ); + } $this->sanitizeSlots( $slots ); Plugin::setCacheItem( @@ -602,4 +626,8 @@ protected function getSlotTimestampStart( $slotsPerDay, $slotNr ) { protected function getSlotTimestampEnd( $slotsPerDay, $slotNr ) { return strtotime( $this->getDate() ) + ( ( $slotNr + 1 ) * ( ( 24 / $slotsPerDay ) * 3600 ) ) - 1; } + + public function setIgnoreRestrictions( bool $ignoreRestrictions ): void { + $this->ignoreRestrictions = $ignoreRestrictions; + } } diff --git a/src/Model/Item.php b/src/Model/Item.php index 25aeae158..084be6bd1 100644 --- a/src/Model/Item.php +++ b/src/Model/Item.php @@ -4,6 +4,7 @@ namespace CommonsBooking\Model; use CommonsBooking\Helper\Helper; +use CommonsBooking\Helper\Wordpress; use CommonsBooking\Repository\Timeframe; use Exception; @@ -70,6 +71,7 @@ public function getAdmins() { * * This function is not used anywhere yet. * + * @deprecated since 2.11, removal in 2.12. * @return array * @throws Exception */ @@ -81,4 +83,159 @@ public function getRestrictions(): array { true ); } + + /** + * Will get a rotating ID by which the vehicle can be identified. + * The ID rotates after every trip and can be used to publish a vehicle ID to the GBFS API. + * + * The GBFS spec demands, that a vehicle ID must be rotated after each trip so that the users cannot be profiled. + * This needs to be deterministic and reversible. + * + * The ID should be reversible, so that we can get a deep link to the item that you found in the feed. + * + * Inspired by: https://tier.engineering/How-we-anonymize-user-trips-on-public-APIs + * Difference: We don't care about anonymity as much, because we don't offer A -> B trips. + * + * This function uses wp_hash to get a hashed identifier, this uses md5 hashing internally. + * + * @return string - a rotating ID for a vehicle + */ + public function getCloakedId(): string { + $closestBooking = $this->getClosestBooking(); + $secondHash = $closestBooking->post_name ?? $this->post_name; // If there is a booking, hash that. Otherwise, get the post name of the item as second hash item + + return wp_hash( $this->ID . $secondHash ); + } + + /** + * Gets the permalink for an item as a cloaked URL to be published in the API. + * + * @return string + */ + public function getCloakedURL(): string { + return add_query_arg( + array( + \CommonsBooking\Repository\Item::QUERY_VEHICLE_ID => $this->getCloakedId(), + \CommonsBooking\Repository\Item::URL_SLUG => true, + ), + get_site_url() . '/' + ); + } + + /** + * Gets the closest Booking for a given Item. + * + * @return ?Booking the booking that is past and closest to the current time, null if no booking is present + */ + public function getClosestBooking(): ?Booking { + + $location = $this->getLocation(); + if ( $location === null ) { + return null; + } + + $allBookings = \CommonsBooking\Repository\Booking::get( + [ $location->ID ], + [ $this->ID ], + null, + true, + null, + [ 'confirmed' ] + ); + + $allBookings = array_filter( $allBookings, fn( $b ) => $b->isPast() ); + + if ( empty( $allBookings ) ) { + return null; + } + + usort( + $allBookings, + function ( $a, $b ) { + $aStartDate = $a->getStartDate(); + $bStartDate = $b->getStartDate(); + + if ( $aStartDate == $bStartDate ) { + $aStartTimeDT = $a->getStartTimeDateTime(); + $bStartTimeDT = $b->getStartTimeDateTime(); + + return $aStartTimeDT <=> $bStartTimeDT; + } + + return $aStartDate <=> $bStartDate; + } + ); + + return array_pop( $allBookings ); + } + + /** + * Will get the location that the item is currently stationed at and bookable. + * Will take into account the current time, the item can have timeframes + * at multiple locations but can only be at one location at a time. + * + * @return ?Location will return null when no Location was found + */ + public function getLocation(): ?Location { + return $this->getClosestBookableTimeframe()?->getLocation();// why I used a deprecated method here: https://github.com/wielebenwir/commonsbooking/issues/507#issuecomment-4235848408 + } + + /** + * Will get the timeframe that is currently applicable for this item. + * When there are multiple timeframes, it wil select the closest. + * + * @return \CommonsBooking\Model\Timeframe|null + */ + public function getClosestBookableTimeframe(): ?\CommonsBooking\Model\Timeframe { + $locations = \CommonsBooking\Repository\Location::getByItem( $this->ID, true ); + + if ( empty( $locations ) ) { + return null; + } + + $timeframes = []; + foreach ( $locations as $location ) { + $timeframes = array_merge( + $timeframes, + $location->getBookableTimeframesByItem( $this->ID, true ) + ); + } + return \CommonsBooking\View\Calendar::getClosestBookableTimeFrameForToday( $timeframes ); + } + + /** + * Determines whether this item is currently free at a given location. + * Free is the opposite of rented. Checks against live availability slots, so it excludes items with Holiday + * Timeframes or overbooking-only availability, although they are technically not rented during these periods. + * + * This method will include items that may only be booked in advance (\CommonsBooking\Model\Timeframe::META_TIMEFRAME_ADVANCE_BOOKING_DAYS), + * because they are technically available at this location. Just not for pickup right now. + * + * @param int $locationId + * @param bool $ignoreStartDayOffset + * @param bool $ignoreRestrictions + * @return bool true if the item is free right now, false otherwise + * @throws Exception + */ + public function isCurrentlyFreeAtLocation( int $locationId, bool $ignoreStartDayOffset = false, bool $ignoreRestrictions = false ): bool { + $nowDT = Wordpress::getUTCDateTimeByTimestamp( current_time( 'timestamp' ) ); + $itemCalendar = new Calendar( + new Day( date( 'Y-m-d', strtotime( '-1 day' ) ) ), + new Day( date( 'Y-m-d', strtotime( '+1 day' ) ) ), + [ $locationId ], + [ $this->ID ] + ); + $itemCalendar->setIgnoreStartDayOffset( $ignoreStartDayOffset ); + $itemCalendar->setIgnoreRestrictions( $ignoreRestrictions ); + + foreach ( $itemCalendar->getAvailabilitySlots() as $availabilitySlot ) { + $startDT = new \DateTime( $availabilitySlot->start ); + $endDT = new \DateTime( $availabilitySlot->end ); + if ( $nowDT >= $startDT && $nowDT <= $endDT ) { + return true; + } + } + + return false; + } } diff --git a/src/Model/Location.php b/src/Model/Location.php index bc178aaf3..7a1139c93 100644 --- a/src/Model/Location.php +++ b/src/Model/Location.php @@ -258,6 +258,7 @@ public function getAdmins() { /** * Will get the currently applicable restrictions for the location. * + * @deprecated since 2.11, removal in 2.12 * @return Restriction[] * @throws \Exception */ diff --git a/src/Model/Restriction.php b/src/Model/Restriction.php index 80bd7cfbd..e6b8560a6 100644 --- a/src/Model/Restriction.php +++ b/src/Model/Restriction.php @@ -171,8 +171,9 @@ public function isActive(): bool { /** * Returns true if restriction ist active. - * TODO this function seems unused in restriction context. Check if it can be removed @markus-mw + * Unused in restriction context * + * @deprecated since 2.11, removal in 2.12 * @return bool */ public function isLocked(): bool { @@ -238,6 +239,9 @@ public function getEndDateDateTime(): DateTime { /** * Returns item name for the item that is restricted. * + * Unused: accessed through {{item:post_title}} template tag over Item Model, not this method + * + * @deprecated since 2.11, removal in 2.12 * @return string */ public function getItemName(): string { @@ -262,6 +266,9 @@ public function getItemId() { /** * Returns location name for the location that the restricted item is in. * + * Unused, can be accessed through {{location:post_title}} template tag, not needed here + * + * @deprecated since 2.11, removal in 2.12. * @return string */ public function getLocationName(): string { diff --git a/src/Model/Timeframe.php b/src/Model/Timeframe.php index 6b135ce72..b297bd6f8 100644 --- a/src/Model/Timeframe.php +++ b/src/Model/Timeframe.php @@ -28,46 +28,119 @@ class Timeframe extends CustomPost { */ public const ERROR_TYPE = 'timeframeValidationFailed'; + /** + * The error type that occurs, when a timeframe has bookings that have been orphaned. through items changing locations. + * The user is notified about this, so that they can move the bookings using Service::MassOperations. + */ public const ORPHANED_TYPE = 'timeframehasOrphanedBookings'; + /** + * The metafield that stores the timestamp (int) for when the timeframe starts. + */ public const REPETITION_START = 'repetition-start'; + /** + * The metafield that stores the timestamp (int) for when the timeframe ends. + */ public const REPETITION_END = 'repetition-end'; + /** + * The meta field for what kind of selection type is used to select the item. + * Value is either one of self::SELECTION_MANUAL_ID , self::SELECTION_CATEGORY_ID or self::SELECTION_ALL_ID + */ public const META_ITEM_SELECTION_TYPE = 'item-select'; + /** + * The metafield for the item post id associated with the timeframe. + */ public const META_ITEM_ID = 'item-id'; + /** + * The metafield where a serialized array of post IDs (multiple) of items associated with the timeframe is stored + */ public const META_ITEM_ID_LIST = 'item-id-list'; + /** + * The metafield where a serialized array of term_ids that make up the categories from which the items associated with the timeframe are stored. + */ public const META_ITEM_CATEGORY_IDS = 'item-category-ids'; + /** + * The meta field for what kind of selection type is used to select the location. + * Value is either one of self::SELECTION_MANUAL_ID , self::SELECTION_CATEGORY_ID or self::SELECTION_ALL_ID + */ public const META_LOCATION_SELECTION_TYPE = 'location-select'; + /** + * The metafield for the location post id associated with the timeframe. + */ public const META_LOCATION_ID = 'location-id'; + /** + * The metafield where a serialized array of post IDs (multiple) of locations associated with the timeframe is stored + */ public const META_LOCATION_ID_LIST = 'location-id-list'; + /** + * The metafield where a serialized array of term_ids that make up the categories from which the locaations associated with the timeframe are stored. + */ public const META_LOCATION_CATEGORY_IDS = 'location-category-ids'; + /** + * Metafield for what type of repetition the timeframe uses. + * For possible values @see Timeframe::getRepetition() + */ public const META_REPETITION = 'timeframe-repetition'; + /** + * Metafield for how many days in advance the item on that timeframe can be booked. + * When this is limited to for example 30 days, only the next 30 days can be booked. + */ public const META_TIMEFRAME_ADVANCE_BOOKING_DAYS = 'timeframe-advance-booking-days'; + /** + * Metafield for the maximum length in days a booking on this timeframe may have. + */ public const META_MAX_DAYS = 'timeframe-max-days'; + /** + * A possible meta_value for META_ITEM_SELECTION_TYPE and META_LOCATION_SELECTION_TYPE + * This value implies, that posts are individually selected from a list of posts. + */ public const SELECTION_MANUAL_ID = 0; + /** + * A possible meta_value for META_ITEM_SELECTION_TYPE and META_LOCATION_SELECTION_TYPE + * This value implies, that the user selects a category of posts that the timeframe applies to. + */ public const SELECTION_CATEGORY_ID = 1; + /** + * A possible meta_value for META_ITEM_SELECTION_TYPE and META_LOCATION_SELECTION_TYPE + * This value implies, that the timeframe applies to all items / locations of the instance. + */ public const SELECTION_ALL_ID = 2; + /** + * A metafield with an on/off value determining if booking codes shall be created for the timeframe (only bookable timeframes). + */ public const META_CREATE_BOOKING_CODES = 'create-booking-codes'; + /** + * A metafield that defines how many days in advance the user HAS to book an item. This means, that the location often needs some kind of + * booking ahead, and that an item cannot be booked at the same day. Integer value in days. + */ public const META_BOOKING_START_DAY_OFFSET = 'booking-startday-offset'; + /** + * A metafield with an on/off value determining if the booking codes generated in the timeframe shall also be shown to the user. + */ public const META_SHOW_BOOKING_CODES = 'show-booking-codes'; + /** + * A metafield that stores a serialized array of strings with slugs of user roles that are allowed to book this item. When this is empty, + * all user roles may book this item. + */ public const META_ALLOWED_USER_ROLES = 'allowed_user_roles'; /** @@ -75,7 +148,10 @@ class Timeframe extends CustomPost { * Example: 2020-01-01,2020-01-02,2020-01-03 */ public const META_MANUAL_SELECTION = 'timeframe_manual_date'; - const MAX_DAYS_DEFAULT = 3; + /** + * The default value for self::META_MAX_DAYS. + */ + const MAX_DAYS_DEFAULT = 3; /** * null means the data is not fetched yet @@ -318,7 +394,20 @@ public function isBookable(): bool { $startDateTimestamp = $this->getStartDate(); $latestPossibleBookingDateTimestamp = $this->getLatestPossibleBookingDateTimestamp(); - return $startDateTimestamp <= $latestPossibleBookingDateTimestamp; + $bookable = $startDateTimestamp <= $latestPossibleBookingDateTimestamp; + + /** + * Filters whether this timeframe is currently bookable. + * + * Lets integrations add their own booking-window rules on top of the + * default advance-booking-days check. + * + * @since 2.11.0 + * + * @param bool $bookable Whether the timeframe is bookable by default. + * @param Timeframe $timeframe The timeframe model instance. + */ + return (bool) apply_filters( 'commonsbooking_is_timeframe_bookable', $bookable, $this ); } /** @@ -1023,15 +1112,6 @@ public function usesBookingCodes(): bool { return $this->getMeta( self::META_CREATE_BOOKING_CODES ) == 'on'; } - /** - * Returns true if booking codes were enabled for this timeframe - * - * @return bool - */ - public function hasBookingCodes(): bool { - return $this->getMeta( 'create-booking-codes' ) == 'on'; - } - /** * Returns repetition-start \DateTime. * This function contains a weird hotfix for full day timeframes. diff --git a/src/Plugin.php b/src/Plugin.php index b7e143506..febaab871 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -10,6 +10,7 @@ use CommonsBooking\Map\SearchShortcode; use CommonsBooking\Model\Booking; use CommonsBooking\Model\BookingCode; +use CommonsBooking\Service\BookingLifecycle; use CommonsBooking\Service\BookingRuleApplied; use CommonsBooking\Service\Cache; use CommonsBooking\Service\Scheduler; @@ -763,6 +764,9 @@ public function init() { // loads the Scheduler add_action( 'init', array( Scheduler::class, 'initHooks' ), 40 ); + // dispatch booking lifecycle action hooks (created / confirmed / status changed) + BookingLifecycle::initHooks(); + // handle the booking forms, needs to happen after taxonomy registration so that we can access the taxonomy add_action( 'init', array( self::class, 'handleBookingForms' ), 50 ); @@ -831,6 +835,9 @@ function () { // iCal rewrite iCalendar::initRewrite(); + + // permalink resolution rewrite + \CommonsBooking\Repository\Item::initRewrite(); } /** @@ -918,6 +925,9 @@ function () { new \CommonsBooking\API\GBFS\Discovery(), new \CommonsBooking\API\GBFS\StationInformation(), new \CommonsBooking\API\GBFS\StationStatus(), + new \CommonsBooking\API\GBFS\VehicleAvailability(), + new \CommonsBooking\API\GBFS\VehicleStatus(), + new \CommonsBooking\API\GBFS\VehicleTypes(), new \CommonsBooking\API\GBFS\SystemInformation(), ]; diff --git a/src/Repository/BookablePost.php b/src/Repository/BookablePost.php index 924eed4a6..a0203edce 100644 --- a/src/Repository/BookablePost.php +++ b/src/Repository/BookablePost.php @@ -217,9 +217,9 @@ abstract protected static function getModelClass(); * @param array $args WP Post args * @param bool $bookable * - * @return array + * @return \CommonsBooking\Model\Location[]|\CommonsBooking\Model\Item[] */ - public static function get( array $args = array(), bool $bookable = false ) { + public static function get( array $args = array(), bool $bookable = false ): array { $posts = []; $args['post_type'] = static::getPostType(); $args['nopaging'] = true; @@ -273,7 +273,7 @@ public static function get( array $args = array(), bool $bookable = false ) { * @param $relatedType * @param bool $bookable * - * @return int[] Array of post ids + * @return \CommonsBooking\Model\Location[] | \CommonsBooking\Model\Item[] * @throws Exception */ protected static function getByRelatedPost( $postId, $originType, $relatedType, bool $bookable = false ): array { diff --git a/src/Repository/Booking.php b/src/Repository/Booking.php index 2fb725d54..a85394379 100644 --- a/src/Repository/Booking.php +++ b/src/Repository/Booking.php @@ -351,8 +351,8 @@ public static function getForCurrentUser( * Returns bookings. This uses the CommonsBooking\Repository\Timeframe::get() method which * is not based on the WP_Query class but will perform its own SQL query. * - * @param array $locations - * @param array $items + * @param array $locations an array of post IDs + * @param array $items an array of post IDs * @param string|null $date Date-String in format YYYY-mm-dd * @param bool $returnAsModel if true, returns booking model, if false return int array (defaults to false) * @param int|null $minTimestamp diff --git a/src/Repository/CB1.php b/src/Repository/CB1.php index 0b519ada2..024e4afe7 100644 --- a/src/Repository/CB1.php +++ b/src/Repository/CB1.php @@ -9,6 +9,8 @@ * This class contains methods to query the database for old CB1 data. * CB1 is CommonsBooking up to version 0.9.4.18 (https://wordpress.org/plugins/commons-booking/) * This class is mainly used to migrate over the old data to CB2. + * + * @deprecated since 2.11 , removal in 2.12 */ class CB1 { @@ -234,10 +236,10 @@ public static function getCB2PostIdByCB1Id( $id ): ?int { $table_postmeta = $wpdb->prefix . 'postmeta'; $sql = $wpdb->prepare( - "SELECT meta_value as cb1_id, post_id as cb2_id + "SELECT meta_value as cb1_id, post_id as cb2_id FROM $table_postmeta WHERE - meta_key = '_cb_cb1_post_post_ID' AND + meta_key = '_cb_cb1_post_post_ID' AND meta_value = %s; ", $id @@ -266,10 +268,10 @@ public static function getCB1Taxonomies() { return $wpdb->get_results( " SELECT - tr.*, + tr.*, tt.taxonomy, t.slug as term - FROM $table_term_relationships tr + FROM $table_term_relationships tr LEFT JOIN $table_term_taxonomy tt ON tr.term_taxonomy_id = tt.term_id LEFT JOIN $table_terms t ON diff --git a/src/Repository/Item.php b/src/Repository/Item.php index ada54a228..9629ac961 100644 --- a/src/Repository/Item.php +++ b/src/Repository/Item.php @@ -5,6 +5,8 @@ use Exception; class Item extends BookablePost { + public const URL_SLUG = COMMONSBOOKING_PLUGIN_SLUG . '_get_vehicle'; + public const QUERY_VEHICLE_ID = COMMONSBOOKING_PLUGIN_SLUG . '_vehicle_cloaked_id'; /** * Returns array with items at location based on bookable timeframes. @@ -20,6 +22,50 @@ public static function getByLocation( $locationId, bool $bookable = false ): arr return self::getByRelatedPost( $locationId, 'location', 'item', $bookable ); } + /** + * Allows resolving cloaked vehicle IDs published by the GBFS API back to the corresponding items + * + * @return void + */ + public static function initRewrite() { + add_action( + 'wp_loaded', + function () { + add_rewrite_rule( self::URL_SLUG, 'index.php?' . self::URL_SLUG . '=1', 'top' ); + } + ); + + add_filter( + 'query_vars', + function ( $query_vars ) { + $query_vars[] = self::URL_SLUG; + return $query_vars; + } + ); + + add_action( + 'parse_request', + function ( &$wp ) { + + if ( + ! array_key_exists( self::URL_SLUG, $wp->query_vars ) || + ! isset( $_GET[ self::QUERY_VEHICLE_ID ] ) || + empty( $_GET[ self::QUERY_VEHICLE_ID ] ) + ) { + return; + } + $cloakedId = sanitize_text_field( wp_unslash( $_GET[ self::QUERY_VEHICLE_ID ] ) ); + $item = Item::getByCloakedId( $cloakedId ); + if ( $item === null ) { + die( 'invalid vehicle id' ); + } + $url = get_permalink( $item->ID ); + wp_redirect( $url ); + exit; + } + ); + } + /** * @return string */ @@ -27,6 +73,22 @@ protected static function getPostType(): string { return \CommonsBooking\Wordpress\CustomPostType\Item::getPostType(); } + /** + * Gets an individual item from a cloaked ID. The GBFS API uses this to provide + * deep-links with rotating vehicle IDs. + * + * @param string $cloakedId + * @return \CommonsBooking\Model\Item|null + */ + public static function getByCloakedId( string $cloakedId ): ?\CommonsBooking\Model\Item { + foreach ( self::get() as $item ) { + if ( $item->getCloakedId() == $cloakedId ) { + return $item; + } + } + return null; + } + /** * This is the model class that belongs to the post type. * With the model class, you are able to perform additional functions on the post type. diff --git a/src/Repository/Location.php b/src/Repository/Location.php index ae3b881c1..3f448c461 100644 --- a/src/Repository/Location.php +++ b/src/Repository/Location.php @@ -13,7 +13,7 @@ class Location extends BookablePost { * * @param bool $bookable * - * @return array + * @return \CommonsBooking\Model\Location[] * @throws Exception */ public static function getByItem( $itemId, bool $bookable = false ): array { diff --git a/src/Repository/PostRepository.php b/src/Repository/PostRepository.php index 16135751a..e8a73b2f7 100644 --- a/src/Repository/PostRepository.php +++ b/src/Repository/PostRepository.php @@ -57,4 +57,6 @@ public static function getPostById( $postId ) { return $post; } } + + abstract public static function get( array $args = array() ): array; } diff --git a/src/Repository/Timeframe.php b/src/Repository/Timeframe.php index 3130c26c5..d2f98e826 100644 --- a/src/Repository/Timeframe.php +++ b/src/Repository/Timeframe.php @@ -37,7 +37,7 @@ public static function getBookable( $minTimestamp = null, array $postStatus = [ 'confirmed', 'unconfirmed', 'publish', 'inherit' ] ): array { - return self::get( + $timeframes = self::get( $locations, $items, [ \CommonsBooking\Wordpress\CustomPostType\Timeframe::BOOKABLE_ID ], @@ -46,6 +46,21 @@ public static function getBookable( $minTimestamp, $postStatus ); + + /** + * Filters the list of bookable timeframes for the given locations and items. + * + * Lets integrations restrict or extend which timeframes are offered for + * booking. The element type follows the $returnAsModel argument (post IDs, + * WP_Post or Model\Timeframe). + * + * @since 2.11.0 + * + * @param array $timeframes The bookable timeframes. + * @param array $locations The location IDs the query was scoped to. + * @param array $items The item IDs the query was scoped to. + */ + return apply_filters( 'commonsbooking_bookable_timeframes', $timeframes, $locations, $items ); } /** diff --git a/src/Service/BookingLifecycle.php b/src/Service/BookingLifecycle.php new file mode 100644 index 000000000..cf7a455b3 --- /dev/null +++ b/src/Service/BookingLifecycle.php @@ -0,0 +1,135 @@ +post_type !== BookingPostType::getPostType() ) { + return; + } + + // No real transition, nothing to announce. + if ( $new_status === $old_status ) { + return; + } + + // Ignore transitions into transient placeholder states (e.g. auto-draft). + if ( in_array( $new_status, self::PLACEHOLDER_STATUSES, true ) ) { + return; + } + + // Only announce events for posts that are actual bookings. The booking + // type meta is written as part of the insert/update, so it is available + // by the time this action fires. + if ( get_post_meta( $post->ID, 'type', true ) != Timeframe::BOOKING_ID ) { + return; + } + + $booking = new Booking( $post->ID ); + + /** + * Fires once when a booking first enters a real status. + * + * @since 2.11.0 + * + * @param int $booking_id The booking post ID. + * @param Booking $booking The booking model instance. + */ + if ( ! get_post_meta( $post->ID, self::CREATED_FIRED_META, true ) ) { + update_post_meta( $post->ID, self::CREATED_FIRED_META, 1 ); + do_action( 'commonsbooking_booking_created', $post->ID, $booking ); + } + + /** + * Fires on every booking status transition. + * + * @since 2.11.0 + * + * @param int $booking_id The booking post ID. + * @param string $old_status The previous post status. + * @param string $new_status The new post status. + * @param Booking $booking The booking model instance. + */ + do_action( 'commonsbooking_booking_status_changed', $post->ID, $old_status, $new_status, $booking ); + + /** + * Fires when a booking is confirmed. + * + * This is the recommended hook for integrations that need to react to a + * booking becoming active, e.g. programming a smart lock or syncing an + * external calendar. + * + * @since 2.11.0 + * + * @param int $booking_id The booking post ID. + * @param Booking $booking The booking model instance. + */ + if ( $new_status === 'confirmed' ) { + do_action( 'commonsbooking_booking_confirmed', $post->ID, $booking ); + } + } +} diff --git a/src/Service/BookingRule.php b/src/Service/BookingRule.php index bbe26f427..47299519c 100644 --- a/src/Service/BookingRule.php +++ b/src/Service/BookingRule.php @@ -443,7 +443,7 @@ public static function checkMaxBookingDays( Booking $booking, array $args, $appl $allowedBookedDays = $args[0]; $periodDays = $args[1]; // split the period in half, when it is an uneven number, the left side will be one day longer - $daysHalf = $periodDays / 2; + $daysHalf = floor( $periodDays / 2 ); if ( $periodDays % 2 ) { $daysLeft = $daysHalf + 1; $daysRight = $daysHalf - 1; diff --git a/src/Service/BookingRuleApplied.php b/src/Service/BookingRuleApplied.php index a9db1e2b6..5335cfd00 100644 --- a/src/Service/BookingRuleApplied.php +++ b/src/Service/BookingRuleApplied.php @@ -42,8 +42,8 @@ public function __construct( BookingRule $rule ) { $rule->description, $rule->errorMessage, $rule->validationFunction, - $rule->params ?? [], - $rule->selectParam ?? [], + empty( $rule->params ) ? [] : $rule->params, + empty( $rule->selectParam ) ? [] : $rule->selectParam, $rule->errorFromArgs ?? null ); } diff --git a/src/View/Booking.php b/src/View/Booking.php index 0c3ded1dc..2bdfa4124 100755 --- a/src/View/Booking.php +++ b/src/View/Booking.php @@ -344,9 +344,15 @@ public static function getLocationForItem_AJAX() { try { $itemModel = new \CommonsBooking\Model\Item( $itemID ); - $location = \CommonsBooking\Repository\Location::getByItem( $itemID, true ); - // pick the first location, no matter what - $location = reset( $location ); + $location = $itemModel->getLocation(); + if ( ! $location ) { + // This won't be displayed anywhere + wp_send_json_error( + array( + 'message' => 'No location found for this item.', + ) + ); + } $timeframe = Timeframe::getBookable( [ $location->ID ], [ $itemID ], @@ -363,22 +369,13 @@ public static function getLocationForItem_AJAX() { ) ); } - if ( $location ) { - wp_send_json( - array( - 'success' => true, - 'locationID' => $location->ID, - 'fullDay' => $timeframe->isFullDay(), - ) - ); - } else { - // This won't be displayed anywhere - wp_send_json_error( - array( - 'message' => 'No location found for this item.', - ) - ); - } + wp_send_json( + array( + 'success' => true, + 'locationID' => $location->ID, + 'fullDay' => $timeframe->isFullDay(), + ) + ); } /** diff --git a/src/View/BookingCodes.php b/src/View/BookingCodes.php index 02a1b917d..96ce87764 100644 --- a/src/View/BookingCodes.php +++ b/src/View/BookingCodes.php @@ -181,7 +181,7 @@ public static function renderCronEmailFields( $field, $escaped_value, $object_id __( 'Unable to send Emails. No location email(s) configured, check location', 'commonsbooking' ) . sprintf( ' %s', esc_url( get_edit_post_link( $location->ID ) ), commonsbooking_sanitizeHTML( $location->post_title ) ) ); - } elseif ( ! $timeframe->hasBookingCodes() ) { + } elseif ( ! $timeframe->usesBookingCodes() ) { $errMsg = commonsbooking_sanitizeHTML( __( 'This timeframe has no booking codes. To generate booking codes you need to save the timeframe.', 'commonsbooking' ) ); } @@ -290,7 +290,7 @@ public static function renderDirectEmailRow( $field_args, $field ) { __( 'Unable to send Emails. No location email(s) configured, check location', 'commonsbooking' ) . sprintf( ' %s', esc_url( get_edit_post_link( $location->ID ) ), commonsbooking_sanitizeHTML( $location->post_title ) ) ); - } elseif ( ! $timeframe->hasBookingCodes() ) { + } elseif ( ! $timeframe->usesBookingCodes() ) { echo commonsbooking_sanitizeHTML( __( 'This timeframe has no booking codes. To generate booking codes you need to save the timeframe.', 'commonsbooking' ) ); } else { echo ' @@ -386,7 +386,7 @@ public static function renderTable( $timeframeId ) {
-
' . ( $timeframe->hasBookingCodes() ? ' +
' . ( $timeframe->usesBookingCodes() ? ' Download booking codes

' . @@ -410,7 +410,7 @@ public static function renderTable( $timeframeId ) {

'; - if ( $timeframe->hasBookingCodes() ) { + if ( $timeframe->usesBookingCodes() ) { echo self::renderTableFor( 'timeframe_form', $bookingCodes ); echo '
'; @@ -547,7 +547,6 @@ function ( $action, $result ) use ( $timeframeId ) { 2 ); - // @phpstan-ignore deadCode.unreachable (false positive, covered by unit tests) $booking_msg = new BookingCodesMessage( $timeframeId, 'codes', $tsFrom, $tsTo ); $booking_msg->sendMessage(); diff --git a/src/View/Calendar.php b/src/View/Calendar.php index dba371928..b04233f26 100644 --- a/src/View/Calendar.php +++ b/src/View/Calendar.php @@ -147,7 +147,11 @@ public static function renderTable( $atts ): string { $locations = []; foreach ( $timeframes as $timeframe ) { // TODO #507 - $locations[ $timeframe->getLocationID() ] = $timeframe->getLocation()->post_title; + $location = $timeframe->getLocation(); + if ( ! $location ) { + continue; + } + $locations[ $location->ID ] = $location->post_title; } // loop through location @@ -407,7 +411,21 @@ public static function getCalendarDataArray( $item, $location, string $startDate } } - return self::prepareJsonResponse( $startDate, $endDate, [ $location ], [ $item ], $advanceBookingDays, $lastBookableDate, $firstBookableDay ); + $calendarData = self::prepareJsonResponse( $startDate, $endDate, [ $location ], [ $item ], $advanceBookingDays, $lastBookableDate, $firstBookableDay ); + + /** + * Filters the calendar data array before it is rendered or returned as JSON. + * + * This is the payload that drives the frontend booking calendar (and its + * AJAX endpoint), so integrations can adjust the data shown to users. + * + * @since 2.11.0 + * + * @param array $calendarData The prepared calendar data. + * @param mixed $item The item (post ID or WP_Post) the calendar is for. + * @param mixed $location The location (post ID or WP_Post) the calendar is for. + */ + return apply_filters( 'commonsbooking_calendar_data', $calendarData, $item, $location ); } /** diff --git a/src/Wordpress/CustomPostType/Booking.php b/src/Wordpress/CustomPostType/Booking.php index 2d92d7e77..9cd5e15c0 100644 --- a/src/Wordpress/CustomPostType/Booking.php +++ b/src/Wordpress/CustomPostType/Booking.php @@ -291,6 +291,19 @@ public static function handleBookingRequest( throw new BookingDeniedException( __( 'There is already a booking in this time-range. This notice may also appear if there is an unconfirmed booking in the requested period. Unconfirmed bookings are deleted after about 10 minutes. Please try again in a few minutes.', 'commonsbooking' ) ); } + // The initial calendar flow always submits post_status=unconfirmed without a post_ID. + // If an exact-slot booking already exists for the current user, redirect to that booking + // instead of downgrading it back to unconfirmed via the update path below. + if ( $booking && $post_status === 'unconfirmed' && $post_ID === null ) { + return $booking->ID; + } + + // Frontend requests should never reopen an existing booking as unconfirmed by ID. + // Treat this as an invalid request instead of mutating the current booking state. + if ( $post_status === 'unconfirmed' && $post_ID !== null ) { + throw new BookingDeniedException( __( 'Invalid booking request. Please try again.', 'commonsbooking' ) ); + } + $existingBookings = \CommonsBooking\Repository\Booking::getExistingBookings( $itemId, @@ -316,7 +329,7 @@ public static function handleBookingRequest( array_values( $existingBookings )[0]->getPost()->post_name === $requestedPostName && intval( array_values( $existingBookings )[0]->getPost()->post_author ) === get_current_user_id(); - if ( ( ! $isEdit || count( $existingBookings ) > 1 ) && $post_status !== 'canceled' ) { + if ( ! $isEdit && $post_status !== 'canceled' ) { if ( $booking ) { $post_status = 'unconfirmed'; } else { @@ -342,6 +355,7 @@ public static function handleBookingRequest( 'type' => Timeframe::BOOKING_ID, ); + $postarr = self::filterBookingBeforeSave( $postarr, null ); $postId = wp_insert_post( $postarr, true ); $needsValidation = true; @@ -351,7 +365,8 @@ public static function handleBookingRequest( if ( $postarr['post_status'] === 'canceled' ) { $postarr['meta_input']['cancellation_time'] = current_time( 'timestamp' ); } - $postId = wp_update_post( $postarr ); + $postarr = self::filterBookingBeforeSave( $postarr, $booking ); + $postId = wp_update_post( $postarr ); // we check if this is an already denied booking and demand validation again if ( $postarr['post_status'] == 'unconfirmed' ) { @@ -397,6 +412,25 @@ public static function handleBookingRequest( return $postId; } + /** + * Applies the `commonsbooking_booking_before_save` filter to the post array + * that is about to be inserted or updated for a booking. + * + * Lets integrations adjust or add meta data before a booking is persisted + * (e.g. attach an external reference or default values). Return the modified + * `$postarr` (a wp_insert_post()/wp_update_post() array). + * + * @since 2.11.0 + * + * @param array $postarr The post array to be saved. + * @param \CommonsBooking\Model\Booking|null $booking The existing booking being updated, or null for a new booking. + * + * @return array + */ + private static function filterBookingBeforeSave( array $postarr, ?\CommonsBooking\Model\Booking $booking ): array { + return apply_filters( 'commonsbooking_booking_before_save', $postarr, $booking ); + } + /** * Multi grid size * We need to save the grid size for timeframes with full slot grid. @@ -749,7 +783,7 @@ protected function getCustomFields() {
  • Click on the Submit booking button at the end of the page to submit a new booking.
  • Please note: Only a few basic checks against existing bookings are performed. Please be wary of overlapping bookings. -

    +

    ', 'commonsbooking' ) . '

    ' @@ -890,7 +924,7 @@ public function displayBookingsAdminListNotice() { As an admin you can create bookings via this admin interface. Please be aware that admin bookings are not validated and checked. Use this function with care.
    Click on preview to show booking details in frontend
    - To search and filter bookings please integrate the frontend booking list via shortcode. + To search and filter bookings please integrate the frontend booking list via shortcode. See here %1$sHow to display the booking list%2$s', 'commonsbooking' ), diff --git a/src/Wordpress/CustomPostType/Item.php b/src/Wordpress/CustomPostType/Item.php index d29cb0eae..cea8b3d12 100644 --- a/src/Wordpress/CustomPostType/Item.php +++ b/src/Wordpress/CustomPostType/Item.php @@ -280,6 +280,16 @@ public function registerMetabox() { 'show_on_cb' => 'cmb2_hide_if_no_cats', // function should return a bool value ) ); + + // checkbox if item should be excluded from API shares + $cmb->add_field( + array( + 'name' => esc_html__( 'Exclude from API', 'commonsbooking' ), + 'desc' => esc_html__( 'When this box is checked, the item will not appear in any of the API shares.', 'commonsbooking' ), + 'id' => COMMONSBOOKING_METABOX_PREFIX . 'api_exclude', + 'type' => 'checkbox', + ) + ); } // Check if custom meta fields are set in CB Options and generate MetaData-Box and fields diff --git a/src/Wordpress/CustomPostType/Timeframe.php b/src/Wordpress/CustomPostType/Timeframe.php index d20f8571f..b6bc88ac4 100644 --- a/src/Wordpress/CustomPostType/Timeframe.php +++ b/src/Wordpress/CustomPostType/Timeframe.php @@ -1115,7 +1115,7 @@ public static function removeIrrelevantPostmeta( \CommonsBooking\Model\Timeframe \CommonsBooking\Model\Timeframe::META_SHOW_BOOKING_CODES, ]; // remove multi-select postmeta if not relevant (#507) - $onlyRelevantForHolidays = [ + $onlyRelevantForHolidaysOrRepair = [ \CommonsBooking\Model\Timeframe::META_ITEM_ID_LIST, \CommonsBooking\Model\Timeframe::META_LOCATION_ID_LIST, \CommonsBooking\Model\Timeframe::META_ITEM_CATEGORY_IDS, @@ -1124,14 +1124,15 @@ public static function removeIrrelevantPostmeta( \CommonsBooking\Model\Timeframe \CommonsBooking\Model\Timeframe::META_LOCATION_SELECTION_TYPE, ]; - if ( $timeframe->getType() != self::BOOKABLE_ID ) { + $type = $timeframe->getType(); + if ( $type !== self::BOOKABLE_ID ) { foreach ( $onlyRelevantForBookable as $metaKey ) { delete_post_meta( $timeframe->ID, $metaKey ); } } - if ( $timeframe->getType() != self::HOLIDAYS_ID ) { - foreach ( $onlyRelevantForHolidays as $metaKey ) { + if ( ! ( $type === self::HOLIDAYS_ID || $type === self::REPAIR_ID ) ) { + foreach ( $onlyRelevantForHolidaysOrRepair as $metaKey ) { delete_post_meta( $timeframe->ID, $metaKey ); } // reset to manual selection diff --git a/tests/cypress/e2e/02-litepicker-overbooking.cy.js b/tests/cypress/e2e/02-litepicker-overbooking.cy.js index e1d975b6e..ff19df689 100644 --- a/tests/cypress/e2e/02-litepicker-overbooking.cy.js +++ b/tests/cypress/e2e/02-litepicker-overbooking.cy.js @@ -18,7 +18,7 @@ describe('test overbooking process', () => { function updatePostMetaAndReload(postID, metaKey, metaValue) { cy.exec( - 'npx wp-env run tests-cli wp post meta update ' + + 'npx wp-env --config=.wp-env.test.json run cli wp post meta update ' + postID + ' ' + metaKey + diff --git a/tests/cypress/wordpress-files/e2e-override.json b/tests/cypress/wordpress-files/e2e-override.json index 8127d5ca2..7b08cdb57 100644 --- a/tests/cypress/wordpress-files/e2e-override.json +++ b/tests/cypress/wordpress-files/e2e-override.json @@ -8,12 +8,8 @@ "themes": [ "flegfleg/kasimir-theme" ], - "env": { - "tests": { - "mappings": { - "wp-content/plugins/commonsbooking": "./build/commonsbooking", - "wp-content/plugins/commonsbooking/tests/cypress/wordpress-files": "./tests/cypress/wordpress-files" - } - } + "mappings": { + "wp-content/plugins/commonsbooking": "./build/commonsbooking", + "wp-content/plugins/commonsbooking/tests/cypress/wordpress-files": "./tests/cypress/wordpress-files" } } diff --git a/tests/php/API/AvailabilityRouteTest.php b/tests/php/API/AvailabilityRouteTest.php index 475ee313f..5f71839e1 100644 --- a/tests/php/API/AvailabilityRouteTest.php +++ b/tests/php/API/AvailabilityRouteTest.php @@ -43,11 +43,37 @@ public function testsAvailabilitySuccess() { $this->assertSame( 200, $response->get_status() ); $this->assertSame( 2, count( $response->get_data()->availability ) ); + $availabilityStart = new \DateTime( self::CURRENT_DATE ); + $availabilityEnd = new \DateTime( self::CURRENT_DATE ); + $availabilityEnd->modify( '23:59:59' ); + // Checks availability for the first day $this->assertEquals( $this->locationId, $response->get_data()->availability[0]->locationId ); $this->assertEquals( $this->itemId, $response->get_data()->availability[0]->itemId ); - $this->assertEquals( self::CURRENT_DATE . 'T00:00:00+00:00', $response->get_data()->availability[0]->start ); - $this->assertEquals( self::CURRENT_DATE . 'T23:59:59+00:00', $response->get_data()->availability[0]->end ); + $this->assertEquals( $availabilityStart->format( 'c' ), $response->get_data()->availability[0]->start ); + $this->assertEquals( $availabilityEnd->format( 'c' ), $response->get_data()->availability[0]->end ); + + ClockMock::reset(); + } + + public function testApiAvailabilityResponseFilter() { + ClockMock::freeze( new \DateTime( self::CURRENT_DATE ) ); + + // Default: the item has availability slots. + $this->assertNotEmpty( \CommonsBooking\API\AvailabilityRoute::getItemData( $this->itemId ) ); + + $received = 'unset'; + $filter = function ( array $slots, $id ) use ( &$received ) { + $received = $id; + return array(); // clear availability + }; + + add_filter( 'commonsbooking_api_availability_response', $filter, 10, 2 ); + $slots = \CommonsBooking\API\AvailabilityRoute::getItemData( $this->itemId ); + remove_filter( 'commonsbooking_api_availability_response', $filter, 10 ); + + $this->assertSame( array(), $slots ); + $this->assertEquals( $this->itemId, $received ); ClockMock::reset(); } diff --git a/tests/php/API/CB_REST_Route_UnitTestCase.php b/tests/php/API/CB_REST_Route_UnitTestCase.php index 8292e463b..02ab5b103 100644 --- a/tests/php/API/CB_REST_Route_UnitTestCase.php +++ b/tests/php/API/CB_REST_Route_UnitTestCase.php @@ -3,6 +3,7 @@ namespace CommonsBooking\Tests\API; use CommonsBooking\Tests\CPTCreationTrait; +use CommonsBooking\Tests\Wordpress\CustomPostTypeTest; /** * Abstract test case which implicitly tests REST Routes @@ -12,7 +13,7 @@ class CB_REST_Route_UnitTestCase extends CB_REST_UnitTestCase { use CPTCreationTrait; - const CURRENT_DATE = '2021-05-21'; + const CURRENT_DATE = CustomPostTypeTest::CURRENT_DATE; protected $ENDPOINT; diff --git a/tests/php/API/GBFS/DiscoveryRouteTest.php b/tests/php/API/GBFS/DiscoveryRouteTest.php index dea527f80..28a9cb075 100644 --- a/tests/php/API/GBFS/DiscoveryRouteTest.php +++ b/tests/php/API/GBFS/DiscoveryRouteTest.php @@ -4,10 +4,25 @@ use CommonsBooking\Tests\API\CB_REST_Route_UnitTestCase; -/** - * TODO: add result unit test - */ class DiscoveryRouteTest extends CB_REST_Route_UnitTestCase { protected $ENDPOINT = '/commonsbooking/v1/gbfs.json'; + + public function testRoute() { + $request = new \WP_REST_Request( 'GET', $this->ENDPOINT ); + $response = rest_do_request( $request ); + + $this->assertSame( 200, $response->get_status() ); + $data = $response->get_data()->data; + $this->assertNotEmpty( $data->feeds ); + $this->assertIsArray( $data->feeds ); + + $header = $response->get_data(); // the top level info that is always present + + $this->assertNotFalse( \DateTime::createFromFormat( \DateTime::ATOM, $header->last_updated ) ); + $this->assertIsInt( $header->ttl ); + $this->assertIsString( $header->version ); + + parent::testRoute(); + } } diff --git a/tests/php/API/GBFS/StationInformationRouteTest.php b/tests/php/API/GBFS/StationInformationRouteTest.php index fd9bd65d8..465af31fd 100644 --- a/tests/php/API/GBFS/StationInformationRouteTest.php +++ b/tests/php/API/GBFS/StationInformationRouteTest.php @@ -3,11 +3,61 @@ namespace CommonsBooking\Tests\API\GBFS; use CommonsBooking\Tests\API\CB_REST_Route_UnitTestCase; +use CommonsBooking\Tests\Helper\GeoHelperTest; +use WP_REST_Request; -/** - * TODO: add result unit test - */ class StationInformationRouteTest extends CB_REST_Route_UnitTestCase { protected $ENDPOINT = '/commonsbooking/v1/station_information.json'; + + public function testBasicStationInformation_withLatLonMeta() { + update_post_meta( $this->locationId, 'geo_latitude', '50.123' ); + update_post_meta( $this->locationId, 'geo_longitude', '8.123' ); + + $request = new WP_REST_Request( 'GET', $this->ENDPOINT ); + $response = rest_do_request( $request ); + + $this->assertSame( 200, $response->get_status() ); + + $data = $response->get_data()->data; + $this->assertNotEmpty( $data->stations ); + $this->assertCount( 1, $data->stations ); + + $station = $data->stations[0]; + + $this->assertEquals( (string) $this->locationId, $station->station_id ); + $this->assertNotEmpty( $station->name ); + $this->assertNotEmpty( $station->rental_uris->web ); + + $this->assertEquals( 50.123, $station->lat ); + $this->assertEquals( 8.123, $station->lon ); + } + + // no gps data and no address defined, skip location + public function testStationInformation_geocodingNonExistent() { + delete_post_meta( $this->locationId, 'geo_latitude' ); + delete_post_meta( $this->locationId, 'geo_longitude' ); + + $mockedLocationCoordinates = GeoHelperTest::mockedLocation()->getCoordinates(); + + $request = new WP_REST_Request( 'GET', $this->ENDPOINT ); + $response = rest_do_request( $request ); + + $data = $response->get_data()->data; + $this->assertEmpty( $data->stations ); + } + + public function setUp(): void { + parent::setUp(); + + $this->locationId = $this->createLocation( 'Test Location', 'publish' ); + $this->itemId = $this->createItem( 'TestItem', 'publish' ); + + $this->createTimeframe( + $this->locationId, + $this->itemId, + strtotime( '-1 day', strtotime( self::CURRENT_DATE ) ), + strtotime( '+10 days', strtotime( self::CURRENT_DATE ) ) + ); + } } diff --git a/tests/php/API/GBFS/StationStatusRouteTest.php b/tests/php/API/GBFS/StationStatusRouteTest.php index 12fb49807..6d59a174e 100644 --- a/tests/php/API/GBFS/StationStatusRouteTest.php +++ b/tests/php/API/GBFS/StationStatusRouteTest.php @@ -2,12 +2,161 @@ namespace CommonsBooking\Tests\API\GBFS; +use CommonsBooking\Model\Restriction; use CommonsBooking\Tests\API\CB_REST_Route_UnitTestCase; +use CommonsBooking\Tests\Wordpress\CustomPostTypeTest; +use SlopeIt\ClockMock\ClockMock; -/** - * TODO: add result unit test - */ class StationStatusRouteTest extends CB_REST_Route_UnitTestCase { protected $ENDPOINT = '/commonsbooking/v1/station_status.json'; + public function testBasicStationStatus() { + $request = new \WP_REST_Request( 'GET', $this->ENDPOINT ); + $response = rest_do_request( $request ); + + $this->assertSame( 200, $response->get_status() ); + + $data = $response->get_data()->data; + + $this->assertNotEmpty( $data->stations ); + $this->assertCount( 1, $data->stations ); + + $station = $data->stations[0]; + + $this->assertEquals( (string) $this->locationId, $station->station_id ); + $this->assertEquals( 1, $station->num_vehicles_available ); + $this->assertTrue( $station->is_installed ); + $this->assertTrue( $station->is_renting ); + $this->assertTrue( $station->is_returning ); + + $reported = new \DateTime( $station->last_reported ); + $now = new \DateTime( self::CURRENT_DATE ); + + $this->assertEqualsWithDelta( $now->getTimestamp(), $reported->getTimestamp(), 1.0 ); + } + + public function testStationStatus_whenBooked_isEmpty() { + $booking = $this->createConfirmedBookingStartingToday(); + + $request = new \WP_REST_Request( 'GET', $this->ENDPOINT ); + $response = rest_do_request( $request ); + + $data = $response->get_data()->data; + $station = $data->stations[0]; + + $this->assertEquals( 0, $station->num_vehicles_available ); + } + + public function testStationStatus_afterTimeframeEnd_isEmpty() { + $future = new \DateTime( self::CURRENT_DATE ); + $future->modify( '+11 days' ); + ClockMock::freeze( $future ); + + $request = new \WP_REST_Request( 'GET', $this->ENDPOINT ); + $response = rest_do_request( $request ); + + $data = $response->get_data()->data; + $station = $data->stations[0]; + + $this->assertEquals( 0, $station->num_vehicles_available ); + } + + public function testStationStatus_withBookingOffset() { + $otherLocationId = $this->createLocation( 'Other Location', ); + $otherItemId = $this->createItem( 'Other Item', ); + + $timeframeID = $this->createTimeframe( + $otherLocationId, + $otherItemId, + strtotime( '-1 day', strtotime( self::CURRENT_DATE ) ), + strtotime( '+10 days', strtotime( self::CURRENT_DATE ) ), + \CommonsBooking\Wordpress\CustomPostType\Timeframe::BOOKABLE_ID, + 'on', + 'd', + 0, + '8:00 AM', + '12:00 PM', + 'publish', + [], + '', + CustomPostTypeTest::USER_ID, + 3, + 30, + 2 + ); + + // with offset → still counted as available because it is not booked + $request = new \WP_REST_Request( 'GET', $this->ENDPOINT ); + $response = rest_do_request( $request ); + $data = $response->get_data()->data; + + $relevantStation = current( + array_filter( + $data->stations, + function ( $station ) use ( $otherLocationId ) { + return $station->station_id === (string) $otherLocationId; + } + ) + ); + $this->assertEquals( 1, $relevantStation->num_vehicles_available ); + + // remove offset → still available + update_post_meta( $timeframeID, \CommonsBooking\Model\Timeframe::META_BOOKING_START_DAY_OFFSET, 0 ); + + $response = rest_do_request( $request ); + $data = $response->get_data()->data; + $relevantStation = current( + array_filter( + $data->stations, + function ( $station ) use ( $otherLocationId ) { + return $station->station_id === (string) $otherLocationId; + } + ) + ); + $this->assertEquals( 1, $relevantStation->num_vehicles_available ); + } + public function testStationStatusWithRestriction() { + $this->createRestriction( + Restriction::TYPE_REPAIR, + $this->locationId, + $this->itemId, + strtotime( '-1 day', strtotime( self::CURRENT_DATE ) ), + strtotime( '+1 day', strtotime( self::CURRENT_DATE ) ), + ); + $request = new \WP_REST_Request( 'GET', $this->ENDPOINT ); + $response = rest_do_request( $request ); + $data = $response->get_data()->data; + $station = $data->stations[0]; + $this->assertEquals( 0, $station->num_vehicles_available ); + } + + public function testNotCountedWhenItemExcluded() { + update_post_meta( $this->itemId, COMMONSBOOKING_METABOX_PREFIX . 'api_exclude', 'on' ); + $request = new \WP_REST_Request( 'GET', $this->ENDPOINT ); + $response = rest_do_request( $request ); + $data = $response->get_data()->data; + $station = $data->stations[0]; + $this->assertEquals( 0, $station->num_vehicles_available ); + } + + public function setUp(): void { + parent::setUp(); + + ClockMock::freeze( new \DateTime( self::CURRENT_DATE ) ); + + $this->locationId = $this->createLocation( 'Testlocation', 'publish' ); + $this->itemId = $this->createItem( 'TestItem', 'publish' ); + + $this->createTimeframe( + $this->locationId, + $this->itemId, + strtotime( '-1 day', strtotime( self::CURRENT_DATE ) ), + strtotime( '+10 days', strtotime( self::CURRENT_DATE ) ) + ); + } + + public function tearDown(): void { + ClockMock::reset(); + parent::tearDown(); + } } diff --git a/tests/php/API/GBFS/StationStatusTest.php b/tests/php/API/GBFS/StationStatusTest.php deleted file mode 100644 index 9bac7d5f3..000000000 --- a/tests/php/API/GBFS/StationStatusTest.php +++ /dev/null @@ -1,124 +0,0 @@ -locationId ); - ClockMock::freeze( $currDate ); - $routeObject = new StationStatus(); - $spanningTimeframe = $this->createTimeframe( - $this->locationId, - $this->itemId, - strtotime( '-1 day', strtotime( self::CURRENT_DATE ) ), - strtotime( '+10 days', strtotime( self::CURRENT_DATE ) ) - ); - $stationStatus = $routeObject->prepare_item_for_response( $locationObject, null )->get_data(); - $this->assertEquals( $this->locationId, $stationStatus->station_id ); - $this->assertEquals( 1, $stationStatus->num_vehicles_available ); - $this->assertTrue( $stationStatus->is_installed ); - $this->assertTrue( $stationStatus->is_renting ); - $this->assertTrue( $stationStatus->is_returning ); - $this->assertEquals( date( 'c' ), $stationStatus->last_reported ); - - // now let's book the current day and check, that the station is empty - $this->createConfirmedBookingStartingToday(); - $this->assertEquals( 0, $routeObject->prepare_item_for_response( $locationObject, null )->get_data()->num_vehicles_available ); - - // the timeframe has ended now, so the station should be empty - $currDate->modify( '+11 days' ); - ClockMock::freeze( $currDate ); - $this->assertEquals( 0, $routeObject->prepare_item_for_response( $locationObject, null )->get_data()->num_vehicles_available ); - - // very important for GBFS: when bookings are only allowed with a certain offset (time difference between booking and start of booking), the station should be empty - ClockMock::freeze( new \DateTime( self::CURRENT_DATE ) ); - $otherLocationId = $this->createLocation( 'Other Location', 'publish' ); - $otherItemId = $this->createItem( 'Other Item', 'publish' ); - $timeframeID = $this->createTimeframe( - $otherLocationId, - $otherItemId, - strtotime( '-1 day', strtotime( self::CURRENT_DATE ) ), - strtotime( '+10 days', strtotime( self::CURRENT_DATE ) ), - \CommonsBooking\Wordpress\CustomPostType\Timeframe::BOOKABLE_ID, - 'on', - 'd', - 0, - '8:00 AM', - '12:00 PM', - 'publish', - [], - '', - self::USER_ID, - 3, - 30, - 2 - ); - $this->assertEquals( 0, $routeObject->prepare_item_for_response( new Location( $otherLocationId ), null )->get_data()->num_vehicles_available ); - // remove the offset and the station should have the item - update_post_meta( $timeframeID, 'booking-startday-offset', 0 ); - $this->assertEquals( 1, $routeObject->prepare_item_for_response( new Location( $otherLocationId ), null )->get_data()->num_vehicles_available ); - } - - public function testPrepare_item_for_response_hourly() { - $currDate = new \DateTime( self::CURRENT_DATE ); - $currDate->setTime( 8, 0, 0 ); - $locationObject = new Location( $this->locationId ); - ClockMock::freeze( $currDate ); - $routeObject = new StationStatus(); - $hourlyTimeframe = $this->createTimeframe( - $this->locationId, - $this->itemId, - strtotime( '-1 day', strtotime( self::CURRENT_DATE ) ), - strtotime( '+2 days', strtotime( self::CURRENT_DATE ) ), - \CommonsBooking\Wordpress\CustomPostType\Timeframe::BOOKABLE_ID, - 'off', - 'd', - 1, - '8:00 AM', - '11:59 PM' - ); - $this->assertEquals( 1, $routeObject->prepare_item_for_response( $locationObject, null )->get_data()->num_vehicles_available ); - - // before 08:00AM the bike is not available - $currDate->setTime( 6, 0, 0 ); - ClockMock::freeze( $currDate ); - $this->assertEquals( 0, $routeObject->prepare_item_for_response( $locationObject, null )->get_data()->num_vehicles_available ); - - // now let's book two hours out of the timeframe and check that the station is empty for those two hours - $startBooking = new \DateTime( self::CURRENT_DATE ); - $startBooking->setTime( 10, 0, 0 ); - $endBooking = clone $startBooking; - $endBooking->setTime( 13, 0, 0 ); - $this->createBooking( - $this->locationId, - $this->itemId, - strtotime( '10:00 AM', strtotime( self::CURRENT_DATE ) ), - strtotime( '01:00 PM', strtotime( self::CURRENT_DATE ) ), - '10:00 AM', - '01:00 PM' - ); - ClockMock::freeze( $startBooking ); - $this->assertEquals( 0, $routeObject->prepare_item_for_response( $locationObject, null )->get_data()->num_vehicles_available ); - $startBooking->modify( '+1 hour' ); - ClockMock::freeze( $startBooking ); - $this->assertEquals( 0, $routeObject->prepare_item_for_response( $locationObject, null )->get_data()->num_vehicles_available ); - ClockMock::freeze( $endBooking ); - $this->assertEquals( 1, $routeObject->prepare_item_for_response( $locationObject, null )->get_data()->num_vehicles_available ); - } - - protected function setUp(): void { - parent::setUp(); - } - - protected function tearDown(): void { - parent::tearDown(); - } -} diff --git a/tests/php/API/GBFS/SystemInformationRouteTest.php b/tests/php/API/GBFS/SystemInformationRouteTest.php index 0dc4b8b26..bd8c4fb12 100644 --- a/tests/php/API/GBFS/SystemInformationRouteTest.php +++ b/tests/php/API/GBFS/SystemInformationRouteTest.php @@ -3,11 +3,54 @@ namespace CommonsBooking\Tests\API\GBFS; use CommonsBooking\Tests\API\CB_REST_Route_UnitTestCase; +use SlopeIt\ClockMock\ClockMock; -/** - * TODO: add result unit test - */ class SystemInformationRouteTest extends CB_REST_Route_UnitTestCase { protected $ENDPOINT = '/commonsbooking/v1/system_information.json'; + + public function testRoute() { + $request = new \WP_REST_Request( 'GET', $this->ENDPOINT ); + $response = rest_do_request( $request ); + + $data = $response->get_data()->data; + $this->assertNotEmpty( $data->name ); + $this->assertNotEmpty( $data->system_id ); + + $header = $response->get_data(); // the top level info that is always present + + $this->assertNotFalse( \DateTime::createFromFormat( \DateTime::ATOM, $header->last_updated ) ); + $this->assertIsInt( $header->ttl ); + $this->assertIsString( $header->version ); + parent::testRoute(); + } + + public function testOpeningHours() { + // no timeframe setup, meaning no items to book -> closed + ClockMock::freeze( new \DateTime( self::CURRENT_DATE ) ); + $request = new \WP_REST_Request( 'GET', $this->ENDPOINT ); + $response = rest_do_request( $request ); + $data = $response->get_data()->data; + $this->assertEquals( '24/7 closed', $data->opening_hours ); + + // with timeframe, is open + $this->createBookableTimeFrameIncludingCurrentDay(); + $request = new \WP_REST_Request( 'GET', $this->ENDPOINT ); + $response = rest_do_request( $request ); + $data = $response->get_data()->data; + $this->assertEquals( '24/7', $data->opening_hours ); + + // outside of the timeframe, closed again + $inAWeek = new \DateTime( self::CURRENT_DATE ); + $inAWeek->modify( '+1 week' ); + ClockMock::freeze( $inAWeek ); + $request = new \WP_REST_Request( 'GET', $this->ENDPOINT ); + $response = rest_do_request( $request ); + $data = $response->get_data()->data; + $this->assertEquals( '24/7 closed', $data->opening_hours ); + } + + public function setUp(): void { + parent::setUp(); + } } diff --git a/tests/php/API/GBFS/VehicleAvailabilityRouteTest.php b/tests/php/API/GBFS/VehicleAvailabilityRouteTest.php new file mode 100644 index 000000000..7f8bd7a05 --- /dev/null +++ b/tests/php/API/GBFS/VehicleAvailabilityRouteTest.php @@ -0,0 +1,173 @@ +ENDPOINT ); + $response = rest_do_request( $request ); + + $this->assertSame( 200, $response->get_status() ); + + $data = $response->get_data()->data; + $this->assertNotEmpty( $data->vehicles ); + $this->assertCount( 1, $data->vehicles ); + $availabilities = $data->vehicles[0]->availabilities; + $this->assertCount( 1, $availabilities ); + + $startDT = new \DateTime( $availabilities[0]->from ); + $today = new \DateTime( self::CURRENT_DATE ); + + $this->assertEqualsWithDelta( $today->getTimestamp(), $startDT->getTimestamp(), 1.0 ); + } + + public function testAvailabilityWithOffset() { + ClockMock::freeze( new \DateTime( self::CURRENT_DATE ) ); + + // delete other timeframe so it doesn't mess with our tests + wp_delete_post( $this->timeframe, true ); + $otherLocationId = $this->createLocation( 'Other Location', ); + $otherItemId = $this->createItem( 'Other Item', ); + + $timeframeID = $this->createTimeframe( + $otherLocationId, + $otherItemId, + strtotime( '-1 day', strtotime( self::CURRENT_DATE ) ), + strtotime( '+10 days', strtotime( self::CURRENT_DATE ) ), + \CommonsBooking\Wordpress\CustomPostType\Timeframe::BOOKABLE_ID, + 'on', + 'd', + 0, + '8:00 AM', + '12:00 PM', + 'publish', + [], + '', + CustomPostTypeTest::USER_ID, + 3, + 30, + 2 + ); + + // with offset → not available right now + $request = new \WP_REST_Request( 'GET', $this->ENDPOINT ); + $response = rest_do_request( $request ); + $data = $response->get_data()->data; + + $availabilities = $data->vehicles[0]->availabilities; + + $startDT = new \DateTime( $availabilities[0]->from ); + $today = new \DateTime( self::CURRENT_DATE ); + + $this->assertNotEqualsWithDelta( $today->getTimestamp(), $startDT->getTimestamp(), 1.0 ); + + // remove offset → now available today + update_post_meta( $timeframeID, \CommonsBooking\Model\Timeframe::META_BOOKING_START_DAY_OFFSET, 0 ); + + $response = rest_do_request( $request ); + $data = $response->get_data()->data; + $availabilities = $data->vehicles[0]->availabilities; + $this->assertCount( 1, $availabilities ); + + $startDT = new \DateTime( $availabilities[0]->from ); + $today = new \DateTime( self::CURRENT_DATE ); + + $this->assertEqualsWithDelta( $today->getTimestamp(), $startDT->getTimestamp(), 1.0 ); + } + + public function testHourlyAvailability() { + delete_post_meta( $this->timeframe, 'full-day', 'on' ); + update_post_meta( $this->timeframe, 'grid', 1 ); // hourly grid + update_post_meta( $this->timeframe, 'start-time', '08:00 AM' ); + update_post_meta( $this->timeframe, 'end-time', '01:00 PM' ); + + $startDT = new \DateTime(); + $startDT->modify( '08:00 AM' ); + $endDT = new \DateTime(); + $endDT->modify( '01:00 PM' ); + $endDT->modify( '-1 second' ); // timeframes always have one second cut + + $request = new \WP_REST_Request( 'GET', $this->ENDPOINT ); + $response = rest_do_request( $request ); + $this->assertSame( 200, $response->get_status() ); + + $data = $response->get_data()->data; + $this->assertNotEmpty( $data->vehicles ); + $this->assertCount( 1, $data->vehicles ); + $availabilities = $data->vehicles[0]->availabilities; + $this->assertCount( 2, $availabilities ); // today and tomorrow + $this->assertEquals( $startDT->format( 'c' ), $availabilities[0]->from ); + $this->assertEquals( $endDT->format( 'c' ), $availabilities[0]->until ); + } + + public function testVehicleIDChanges() { + // test, that the vehicle ID rotates after each trip + $request = new \WP_REST_Request( 'GET', $this->ENDPOINT ); + $response = rest_do_request( $request ); + $this->assertSame( 200, $response->get_status() ); + + $data = $response->get_data()->data; + $id = $data->vehicles[0]->vehicle_id; + + // add a trip in the past + $this->createBooking( + $this->locationId, + $this->itemId, + strtotime( '-2 days', strtotime( self::CURRENT_DATE ) ), + strtotime( '-1 day', strtotime( self::CURRENT_DATE ) ) + ); + + // after the trip, the vehicle ID should have changed + $request = new \WP_REST_Request( 'GET', $this->ENDPOINT ); + $response = rest_do_request( $request ); + $this->assertSame( 200, $response->get_status() ); + $data = $response->get_data()->data; + + $this->assertNotEquals( $id, $data->vehicles[0]->vehicle_id ); + } + + public function testExcludedIfBoxChecked() { + update_post_meta( $this->itemId, COMMONSBOOKING_METABOX_PREFIX . 'api_exclude', 'on' ); + $request = new \WP_REST_Request( 'GET', $this->ENDPOINT ); + $response = rest_do_request( $request ); + $this->assertSame( 200, $response->get_status() ); + $data = $response->get_data()->data; + $this->assertEmpty( $data->vehicles ); + } + + public function setUp(): void { + parent::setUp(); + + ClockMock::freeze( new \DateTime( self::CURRENT_DATE ) ); + + $this->locationId = $this->createLocation( 'Testlocation', 'publish', [] ); + $this->itemId = $this->createItem( 'TestItem', 'publish' ); + + $mocked = new \DateTimeImmutable( self::CURRENT_DATE ); + $this->start = $mocked->modify( '-1 days' ); + $this->end = $mocked->modify( '+1 days' ); + + $this->timeframe = $this->createTimeframe( + $this->locationId, + $this->itemId, + $this->start->getTimestamp(), + $this->end->getTimestamp() + ); + } + + public function tearDown(): void { + ClockMock::reset(); + parent::tearDown(); + } +} diff --git a/tests/php/API/GBFS/VehicleStatusRouteTest.php b/tests/php/API/GBFS/VehicleStatusRouteTest.php new file mode 100644 index 000000000..7918b06a5 --- /dev/null +++ b/tests/php/API/GBFS/VehicleStatusRouteTest.php @@ -0,0 +1,326 @@ +ENDPOINT ); + $response = rest_do_request( $request ); + + $this->assertSame( 200, $response->get_status() ); + + $data = $response->get_data()->data; + + $this->assertCount( 1, $data->vehicles ); + + $this->createConfirmedBookingStartingToday(); + $request = new \WP_REST_Request( 'GET', $this->ENDPOINT ); + $response = rest_do_request( $request ); + $this->assertSame( 200, $response->get_status() ); + $data = $response->get_data()->data; + $this->assertEmpty( $data->vehicles ); + } + + /** + * Item should not appear, when it is currently not available for rent. + * + * @return void + */ + public function testNoTimeframe() { + // base case, not disabled + $request = new \WP_REST_Request( 'GET', $this->ENDPOINT ); + $response = rest_do_request( $request ); + $this->assertSame( 200, $response->get_status() ); + $data = $response->get_data()->data; + $this->assertFalse( $data->vehicles[0]->is_disabled ); + + // timeframe expired: item vanishes from feed + $future = $this->end->modify( '+1 day' ); + ClockMock::freeze( $future ); + $request = new \WP_REST_Request( 'GET', $this->ENDPOINT ); + $response = rest_do_request( $request ); + $this->assertSame( 200, $response->get_status() ); + $data = $response->get_data()->data; + $this->assertEmpty( $data->vehicles ); + } + + /** + * This field is used to indicate vehicles that are in the field but not available for rental due to a mechanical issue or low battery etc. + * Publishing this data may prevent users from attempting to rent vehicles that are disabled and not available for rental. + * This field SHOULD NOT be set to true when the system is closed for vehicles that would otherwise be rentable. + * + * In CB, this is handled through restrictions. When a total breakdown is present, the item is disabled. + * @return void + */ + public function testIsDisabled() { + // base case, not disabled + $request = new \WP_REST_Request( 'GET', $this->ENDPOINT ); + $response = rest_do_request( $request ); + $this->assertSame( 200, $response->get_status() ); + $data = $response->get_data()->data; + $this->assertFalse( $data->vehicles[0]->is_disabled ); + + // restriction present: should not disappear from feed, but is considered disabled + $this->createRestriction( + \CommonsBooking\Model\Restriction::TYPE_REPAIR, + $this->locationId, + $this->itemId, + strtotime( self::CURRENT_DATE ), + strtotime( '+1 day', strtotime( self::CURRENT_DATE ) ) + ); + $request = new \WP_REST_Request( 'GET', $this->ENDPOINT ); + $response = rest_do_request( $request ); + $this->assertSame( 200, $response->get_status() ); + $data = $response->get_data()->data; + $this->assertCount( 1, $data->vehicles ); + $this->assertTrue( $data->vehicles[0]->is_disabled ); + } + + public function testIsDisabled_hint() { + // hints should not affect availability + $this->createRestriction( + \CommonsBooking\Model\Restriction::TYPE_HINT, + $this->locationId, + $this->itemId, + strtotime( self::CURRENT_DATE ), + strtotime( '+2 days', strtotime( self::CURRENT_DATE ) ) + ); + + $request = new \WP_REST_Request( 'GET', $this->ENDPOINT ); + $response = rest_do_request( $request ); + $this->assertSame( 200, $response->get_status() ); + $data = $response->get_data()->data; + $this->assertFalse( $data->vehicles[0]->is_disabled ); + } + + public function testIsDisabled_inFuture() { + // restrictions in the future should not affect current state + $this->createRestriction( + \CommonsBooking\Model\Restriction::TYPE_REPAIR, + $this->locationId, + $this->itemId, + strtotime( '+1 day', strtotime( self::CURRENT_DATE ) ), + strtotime( '+2 days', strtotime( self::CURRENT_DATE ) ) + ); + + $request = new \WP_REST_Request( 'GET', $this->ENDPOINT ); + $response = rest_do_request( $request ); + $this->assertSame( 200, $response->get_status() ); + $data = $response->get_data()->data; + $this->assertFalse( $data->vehicles[0]->is_disabled ); + } + + public function testIsDisabled_inactiveRestriction() { + // inactive restrictions should also not trigger + $this->createRestriction( + \CommonsBooking\Model\Restriction::TYPE_REPAIR, + $this->locationId, + $this->itemId, + strtotime( self::CURRENT_DATE ), + null, + \CommonsBooking\Model\Restriction::STATE_SOLVED + ); + + $request = new \WP_REST_Request( 'GET', $this->ENDPOINT ); + $response = rest_do_request( $request ); + $this->assertSame( 200, $response->get_status() ); + $data = $response->get_data()->data; + $this->assertFalse( $data->vehicles[0]->is_disabled ); + } + + public function testExclusion() { + ClockMock::freeze( new \DateTime( self::CURRENT_DATE ) ); + update_post_meta( $this->itemId, COMMONSBOOKING_METABOX_PREFIX . 'api_exclude', 'on' ); + $request = new \WP_REST_Request( 'GET', $this->ENDPOINT ); + $response = rest_do_request( $request ); + $this->assertSame( 200, $response->get_status() ); + $data = $response->get_data()->data; + $this->assertEmpty( $data->vehicles ); + } + + public function testAvailableUntil_BaseCase() { + $rightNow = new \DateTime( self::CURRENT_DATE ); + ClockMock::freeze( $rightNow ); + $request = new \WP_REST_Request( 'GET', $this->ENDPOINT ); + $response = rest_do_request( $request ); + $this->assertSame( 200, $response->get_status() ); + $data = $response->get_data()->data; + $inThreeDays = clone $rightNow; + $inThreeDays->modify( '+3 days' ); + $inThreeDays->modify( '-1 second' ); // because the item is only available exactly three days, not on the fourth + + $this->assertCount( 1, $data->vehicles ); + $this->assertSame( $inThreeDays->format( 'c' ), $data->vehicles[0]->available_until ); + } + + public function testAvailableUntil_hourlyBooking() { + // delete our original timeframe so we don't have multiple vehicles + wp_delete_post( $this->timeframe, true ); + + // create a timeframe with hourly availability and one day max booking + $this->createTimeframe( + $this->locationId, + $this->itemId, + strtotime( self::CURRENT_DATE ), + null, + Timeframe::BOOKABLE_ID, + 'off', + 'd', + 1, + '8:00 AM', + '5:00 PM', + 'publish', + [ '1', '2', '3', '4', '5', '6', '7' ], + '', + CustomPostTypeTest::USER_ID, + 1 + ); + + $rightNow = new \DateTime( self::CURRENT_DATE . ' 10:00 AM' ); + ClockMock::freeze( $rightNow ); + $request = new \WP_REST_Request( 'GET', $this->ENDPOINT ); + $response = rest_do_request( $request ); + $this->assertSame( 200, $response->get_status() ); + $data = $response->get_data()->data; + $sameDayFivePM = clone $rightNow; + $sameDayFivePM->modify( '5:00 PM' ); + $sameDayFivePM->modify( '-1 second' ); + + $this->assertCount( 1, $data->vehicles ); + $this->assertSame( $sameDayFivePM->format( 'c' ), $data->vehicles[0]->available_until ); + } + + public function testAvailableUntil_slotBased() { + // delete our original timeframe so we don't have multiple vehicles + wp_delete_post( $this->timeframe, true ); + + // create a timeframe a slot based timeframe and one day max booking + $this->createTimeframe( + $this->locationId, + $this->itemId, + strtotime( self::CURRENT_DATE ), + null, + Timeframe::BOOKABLE_ID, + 'off', + 'd', + 0, + '8:00 AM', + '10:00 AM', + 'publish', + [ '1', '2', '3', '4', '5', '6', '7' ], + '', + CustomPostTypeTest::USER_ID, + 1 + ); + + // create another slot for the afternoon + $this->createTimeframe( + $this->locationId, + $this->itemId, + strtotime( self::CURRENT_DATE ), + null, + Timeframe::BOOKABLE_ID, + 'off', + 'd', + 0, + '1:00 PM', + '5:00 PM', + 'publish', + [ '1', '2', '3', '4', '5', '6', '7' ], + '', + CustomPostTypeTest::USER_ID, + 1 + ); + + $rightNow = new \DateTime( self::CURRENT_DATE . ' 9:00 AM' ); + ClockMock::freeze( $rightNow ); + $request = new \WP_REST_Request( 'GET', $this->ENDPOINT ); + $response = rest_do_request( $request ); + $this->assertSame( 200, $response->get_status() ); + $data = $response->get_data()->data; + $sameDayFivePM = clone $rightNow; + $sameDayFivePM->modify( '5:00 PM' ); + $sameDayFivePM->modify( '-1 second' ); + + $this->assertCount( 1, $data->vehicles ); + $this->assertSame( $sameDayFivePM->format( 'c' ), $data->vehicles[0]->available_until ); + } + + public function testAvailableUntil_offset() { + // delete our original timeframe so we don't have multiple vehicles + wp_delete_post( $this->timeframe, true ); + + $rightNow = new \DateTime( self::CURRENT_DATE ); + ClockMock::freeze( $rightNow ); + $otherLocationId = $this->createLocation( 'Other Location', ); + $otherItemId = $this->createItem( 'Other Item', ); + + $timeframeID = $this->createTimeframe( + $otherLocationId, + $otherItemId, + strtotime( '-1 day', strtotime( self::CURRENT_DATE ) ), + strtotime( '+10 days', strtotime( self::CURRENT_DATE ) ), + \CommonsBooking\Wordpress\CustomPostType\Timeframe::BOOKABLE_ID, + 'on', + 'd', + 0, + '8:00 AM', + '12:00 PM', + 'publish', + [], + '', + CustomPostTypeTest::USER_ID, + 3, + 30, + 2 + ); + + // with offset - available until is extended to account for the offset + $availableUntil = clone $rightNow; + $availableUntil->modify( '+5 days' ); + $availableUntil->modify( '-1 second' ); + + $request = new \WP_REST_Request( 'GET', $this->ENDPOINT ); + $response = rest_do_request( $request ); + $data = $response->get_data()->data; + + $this->assertCount( 1, $data->vehicles ); + $this->assertSame( $availableUntil->format( 'c' ), $data->vehicles[0]->available_until ); + } + + public function setUp(): void { + parent::setUp(); + + ClockMock::freeze( new \DateTime( self::CURRENT_DATE ) ); + + $this->locationId = $this->createLocation( 'Testlocation', 'publish', [] ); + $this->itemId = $this->createItem( 'TestItem', 'publish' ); + + $mocked = new \DateTimeImmutable( self::CURRENT_DATE ); + $this->start = $mocked->modify( '-1 days' ); + $this->end = $mocked->modify( '+5 days' ); + + $this->timeframe = $this->createTimeframe( + $this->locationId, + $this->itemId, + $this->start->getTimestamp(), + $this->end->getTimestamp() + ); + } + + public function tearDown(): void { + ClockMock::reset(); + parent::tearDown(); + } +} diff --git a/tests/php/API/GBFS/VehicleTypesRouteTest.php b/tests/php/API/GBFS/VehicleTypesRouteTest.php new file mode 100644 index 000000000..dfbb6e0ab --- /dev/null +++ b/tests/php/API/GBFS/VehicleTypesRouteTest.php @@ -0,0 +1,22 @@ +ENDPOINT ); + $response = rest_do_request( $request ); + + $this->assertSame( 200, $response->get_status() ); + + $data = $response->get_data()->data; + + $this->assertCount( 1, $data->vehicle_types ); // just one vehicle type with the default settings + + parent::testRoute(); + } +} diff --git a/tests/php/API/ItemsRouteTest.php b/tests/php/API/ItemsRouteTest.php index 52f9f7b3e..86ed45052 100644 --- a/tests/php/API/ItemsRouteTest.php +++ b/tests/php/API/ItemsRouteTest.php @@ -46,6 +46,26 @@ public function testItemsSuccess() { $this->assertNotEmpty( $item->url ); } + public function testApiItemResponseFilter() { + // Modify an existing (schema-conforming) field so validation still passes. + $received = null; + $filter = function ( $preparedItem, $item ) use ( &$received ) { + $received = $item; + $preparedItem->name = 'Filtered Name'; + return $preparedItem; + }; + + add_filter( 'commonsbooking_api_item_response', $filter, 10, 2 ); + $request = new \WP_REST_Request( 'GET', $this->ENDPOINT ); + $response = rest_do_request( $request ); + remove_filter( 'commonsbooking_api_item_response', $filter, 10 ); + + $data = $response->get_data(); + $this->assertEquals( 'Filtered Name', $data->items[0]->name ); + $this->assertInstanceOf( \WP_Post::class, $received ); + $this->assertEquals( $this->itemId, $received->ID ); + } + public function testItemsIncludesLocations() { $request = new \WP_REST_Request( 'GET', $this->ENDPOINT ); $response = rest_do_request( $request ); @@ -81,4 +101,16 @@ public function testSingleItem() { $this->assertCount( 1, $data->items ); $this->assertEquals( (string) $this->itemId, $data->items[0]->id ); } + + public function testExcludedIfBoxChecked() { + update_post_meta( $this->itemId, COMMONSBOOKING_METABOX_PREFIX . 'api_exclude', 'on' ); + $request = new \WP_REST_Request( 'GET', $this->ENDPOINT . '/' . $this->itemId ); + $response = rest_do_request( $request ); + + $this->assertSame( 200, $response->get_status() ); + + $data = $response->get_data(); + + $this->assertEmpty( $data->items ); + } } diff --git a/tests/php/Model/BookingTest.php b/tests/php/Model/BookingTest.php index 2b3c72f63..65491b736 100644 --- a/tests/php/Model/BookingTest.php +++ b/tests/php/Model/BookingTest.php @@ -352,6 +352,31 @@ public function testCanCancelBaseCase() { $this->assertTrue( commonsbooking_isCurrentUserAdmin() ); } + public function testCanCancelFilterOverridesDecision() { + ClockMock::freeze( new \DateTime( self::CURRENT_DATE ) ); + wp_set_current_user( $this->subscriberId ); + + // Default: the subscriber may cancel their own future booking. + $this->assertTrue( $this->subscriberBookingInFuture->canCancel() ); + + // The filter can forbid it, and receives the booking instance. + $received = null; + $deny = function ( $canCancel, $booking ) use ( &$received ) { + $received = $booking; + return false; + }; + add_filter( 'commonsbooking_can_cancel_booking', $deny, 10, 2 ); + $this->assertFalse( $this->subscriberBookingInFuture->canCancel() ); + $this->assertInstanceOf( Booking::class, $received ); + remove_filter( 'commonsbooking_can_cancel_booking', $deny, 10 ); + + // The filter can also allow cancelling a booking that is denied by default (a past booking). + $allow = fn() => true; + add_filter( 'commonsbooking_can_cancel_booking', $allow, 10, 2 ); + $this->assertTrue( $this->testBookingPast->canCancel() ); + remove_filter( 'commonsbooking_can_cancel_booking', $allow, 10 ); + } + public function testHasTotalBreakdown() { $testBooking = new Booking( $this->createConfirmedBookingStartingToday() ); $this->assertFalse( $this->testBookingTomorrow->hasTotalBreakdown() ); diff --git a/tests/php/Model/CalendarTest.php b/tests/php/Model/CalendarTest.php index 65481071b..ae414af6a 100644 --- a/tests/php/Model/CalendarTest.php +++ b/tests/php/Model/CalendarTest.php @@ -92,6 +92,22 @@ public function testGetAvailabilitySlots() { ], ]; $this->assertEquals( $expectedSlotObject, $availabilitySlots ); + + // book the other day too, assert an empty + $this->createBooking( + $this->locationId, + $this->itemId, + strtotime( $tomorrow ), + $tomorrowEnd + ); + // recreate the calendar object to get the updated availability + $this->calendar = new Calendar( + new Day( $today, [ $this->locationId ], [ $this->itemId ] ), + new Day( $tomorrow, [ $this->locationId ], [ $this->itemId ] ), + [ $this->locationId ], + [ $this->itemId ] + ); + $this->assertEmpty( $this->calendar->getAvailabilitySlots() ); } public function testGetAvailabilitySlotsWithHourlyTimeframe() { diff --git a/tests/php/Model/DayTest.php b/tests/php/Model/DayTest.php index 34155f021..aeb1be8e6 100644 --- a/tests/php/Model/DayTest.php +++ b/tests/php/Model/DayTest.php @@ -217,6 +217,22 @@ public function testGetEndTimestamp() { $this->assertEquals( $end, $this->instance->getEndTimestamp() ); } + public function testDayAvailabilityFilterOverridesSlots() { + // Default: one slot spanning the whole day. + $this->assertCount( 1, $this->instance->getGrid() ); + + // The filter can adjust the slots and receives the day instance. + $received = null; + $filter = function ( array $slots, $day ) use ( &$received ) { + $received = $day; + return array(); // remove all availability + }; + add_filter( 'commonsbooking_day_availability', $filter, 10, 2 ); + $this->assertCount( 0, $this->instance->getGrid() ); + $this->assertInstanceOf( Day::class, $received ); + remove_filter( 'commonsbooking_day_availability', $filter, 10 ); + } + public function testGetGrid() { // one grid entry spanning whole day $grid = $this->instance->getGrid(); diff --git a/tests/php/Model/ItemTest.php b/tests/php/Model/ItemTest.php index abfa1c997..e564cd5bd 100644 --- a/tests/php/Model/ItemTest.php +++ b/tests/php/Model/ItemTest.php @@ -6,6 +6,7 @@ use CommonsBooking\Model\Restriction; use CommonsBooking\Model\Timeframe; use CommonsBooking\Tests\Wordpress\CustomPostTypeTest; +use SlopeIt\ClockMock\ClockMock; class ItemTest extends CustomPostTypeTest { @@ -55,6 +56,280 @@ public function testGetRestrictions() { } */ + public function testGetLocation() { + $dt = new \DateTime( self::CURRENT_DATE ); + ClockMock::freeze( $dt ); + + // just one item that is currently bookable + $this->assertEquals( $this->locationId, $this->itemModel->getLocation()->ID ); + + // in two weeks, the timeframe is expired, so the item will not be at the location anymore + $dt->modify( '+2 weeks' ); + ClockMock::freeze( $dt ); + $this->assertNull( $this->itemModel->getLocation() ); + + // item that is in location A monday-thursday and in location B friday-sunday. Position should change depending on the day of the week + $locationA = $this->createLocation( 'location a' ); + $locationB = $this->createLocation( 'location b' ); + $movingItem = $this->createItem( 'location changing item' ); + $movingItemModel = new Item( $movingItem ); + $tf1 = new Timeframe( + $this->createTimeframe( + $locationA, + $movingItem, + strtotime( '-1 day', strtotime( \CommonsBooking\Tests\Wordpress\CustomPostTypeTest::CURRENT_DATE ) ), + strtotime( '+14 days', strtotime( \CommonsBooking\Tests\Wordpress\CustomPostTypeTest::CURRENT_DATE ) ), + \CommonsBooking\Wordpress\CustomPostType\Timeframe::BOOKABLE_ID, + '', + 'w', + 0, + '8:00 AM', + '12:00 PM', + 'publish', + [ '1','2','3','4' ] + ) + ); + $tf2 = new Timeframe( + $this->createTimeframe( + $locationB, + $movingItem, + strtotime( '-1 day', strtotime( \CommonsBooking\Tests\Wordpress\CustomPostTypeTest::CURRENT_DATE ) ), + strtotime( '+14 days', strtotime( \CommonsBooking\Tests\Wordpress\CustomPostTypeTest::CURRENT_DATE ) ), + \CommonsBooking\Wordpress\CustomPostType\Timeframe::BOOKABLE_ID, + '', + 'w', + 0, + '8:00 AM', + '12:00 PM', + 'publish', + [ '5','6','7' ] + ) + ); + $this->assertFalse( $tf1->overlaps( $tf2 ) ); + $dt = new \DateTime( self::CURRENT_DATE ); + $dt->modify( 'monday' ); + ClockMock::freeze( $dt ); + $this->assertEquals( $locationA, $movingItemModel->getLocation()->ID ); + $dt->modify( 'friday' ); + ClockMock::freeze( $dt ); + $this->assertEquals( $locationB, $movingItemModel->getLocation()->ID ); + } + + public function testGetCloakedID() { + ClockMock::freeze( new \DateTime( self::CURRENT_DATE ) ); + $cloakedID = $this->itemModel->getCloakedId(); + $this->assertIsString( $cloakedID ); + // create a booking, assert that it has changed + $this->createBooking( + $this->locationId, + $this->itemId, + strtotime( '-2 days', strtotime( self::CURRENT_DATE ) ), + strtotime( '-1 day', strtotime( self::CURRENT_DATE ) ) + ); + $cloakedIDWithBooking = $this->itemModel->getCloakedId(); + $this->assertNotEquals( $cloakedID, $cloakedIDWithBooking ); + + // this booking is not in the past, trip has not ended. Therefore it should not affect the ID + $booking = $this->createConfirmedBookingStartingToday(); + $cloakedIDwithTwoBookings = $this->itemModel->getCloakedId(); + $this->assertEquals( $cloakedIDWithBooking, $cloakedIDwithTwoBookings ); + } + + public function testGetClosestBooking() { + ClockMock::freeze( new \DateTime( self::CURRENT_DATE ) ); + // no booking present + $this->assertNull( $this->itemModel->getClosestBooking() ); + + // one past booking + $bookingInPast = $this->createBooking( + $this->locationId, + $this->itemId, + strtotime( '-4 days', strtotime( self::CURRENT_DATE ) ), + strtotime( '-3 days', strtotime( self::CURRENT_DATE ) ) + ); + $closestBooking = $this->itemModel->getClosestBooking(); + $this->assertEquals( $bookingInPast, $closestBooking->ID ); + + // newer booking in front of that + $newerBooking = $this->createBooking( + $this->locationId, + $this->itemId, + strtotime( '-2 days', strtotime( self::CURRENT_DATE ) ), + strtotime( '-1 day', strtotime( self::CURRENT_DATE ) ) + ); + $closestBooking = $this->itemModel->getClosestBooking(); + $this->assertEquals( $newerBooking, $closestBooking->ID ); + + // booking in future (should not affect) + $this->createBooking( + $this->locationId, + $this->itemId, + strtotime( '+3 days' ), + strtotime( '+4 days' ) + ); + $this->assertEquals( $newerBooking, $this->itemModel->getClosestBooking()->ID ); + } + + // Test case where last booking is way in the past but other booking is in the nearer future. Should still return booking in the past + public function testGetClosestBooking_notIncludingFutureBookings() { + ClockMock::freeze( new \DateTime( self::CURRENT_DATE ) ); + // one past booking + $pastBooking = $this->createBooking( + $this->locationId, + $this->itemId, + strtotime( '-7 days' ), + strtotime( '-5 days' ) + ); + // one future booking + $bookingStartingTomorrow = $this->createBooking( + $this->locationId, + $this->itemId, + strtotime( '+1 day' ), + strtotime( '+2 days' ) + ); + $closestBooking = $this->itemModel->getClosestBooking(); + $this->assertEquals( $pastBooking, $closestBooking->ID ); + } + + public function testGetClosestBooking_picksMostRecentPastBooking() { + ClockMock::freeze( new \DateTime( self::CURRENT_DATE ) ); + $this->createBooking( + $this->locationId, + $this->itemId, + strtotime( '-14 days' ), + strtotime( '-12 days' ) + ); + $recentPastBooking = $this->createBooking( + $this->locationId, + $this->itemId, + strtotime( '-3 days' ), + strtotime( '-1 day' ) + ); + $this->createBooking( + $this->locationId, + $this->itemId, + strtotime( '+1 day' ), + strtotime( '+2 days' ) + ); + + $closestBooking = $this->itemModel->getClosestBooking(); + $this->assertEquals( $recentPastBooking, $closestBooking->ID ); + } + + public function testIsCurrentlyFreeAtLocation() { + ClockMock::freeze( new \DateTime( self::CURRENT_DATE ) ); + + // Item has a bookable timeframe including today, should be free + $this->assertTrue( $this->itemModel->isCurrentlyFreeAtLocation( $this->locationId ) ); + + // A confirmed booking starting today means the item is now rented + $this->createConfirmedBookingStartingToday(); + $this->assertFalse( $this->itemModel->isCurrentlyFreeAtLocation( $this->locationId ) ); + + // In two weeks, the timeframe is expired, should no longer be free + $dt = new \DateTime( self::CURRENT_DATE ); + $dt->modify( '+2 weeks' ); + ClockMock::freeze( $dt ); + $this->assertFalse( $this->itemModel->isCurrentlyFreeAtLocation( $this->locationId ) ); + + // Wrong location should not be free + ClockMock::freeze( new \DateTime( self::CURRENT_DATE ) ); + $otherLocationId = $this->createLocation( 'other location' ); + $this->assertFalse( $this->itemModel->isCurrentlyFreeAtLocation( $otherLocationId ) ); + } + + public function testIsCurrentlyFreeAtLocation_withBookingOffset() { + $otherLocationId = $this->createLocation( 'Other Location' ); + $otherItemId = $this->createItem( 'Other Item' ); + $otherItemModel = new Item( $otherItemId ); + + $timeframeId = $this->createTimeframe( + $otherLocationId, + $otherItemId, + strtotime( '-1 day', strtotime( self::CURRENT_DATE ) ), + strtotime( '+10 days', strtotime( self::CURRENT_DATE ) ), + \CommonsBooking\Wordpress\CustomPostType\Timeframe::BOOKABLE_ID, + 'on', + 'd', + 0, + '8:00 AM', + '12:00 PM', + 'publish', + [], + '', + CustomPostTypeTest::USER_ID, + 3, // booking start day offset + 30, + 2 + ); + + ClockMock::freeze( new \DateTime( self::CURRENT_DATE ) ); + + // With offset → item is still free, it's just not possible to pick it up right now + $this->assertTrue( $otherItemModel->isCurrentlyFreeAtLocation( $otherLocationId, true ) ); + + // Remove offset → still free, no change in rental status + update_post_meta( $timeframeId, \CommonsBooking\Model\Timeframe::META_BOOKING_START_DAY_OFFSET, 0 ); + $this->assertTrue( $otherItemModel->isCurrentlyFreeAtLocation( $otherLocationId, true ) ); + } + + public function testGetClosestBookableTimeframe() { + $dt = new \DateTime( self::CURRENT_DATE ); + ClockMock::freeze( $dt ); + $this->assertEquals( $this->timeframeModel->ID, $this->itemModel->getClosestBookableTimeframe()->ID ); + // in two weeks, the timeframe is expired, so the item will not be at the location anymore + $dt->modify( '+2 weeks' ); + ClockMock::freeze( $dt ); + $this->assertNull( $this->itemModel->getClosestBookableTimeframe() ); + } + + public function testGetClosestBookableTimeframe_withBookingOffset() { + $otherLocationId = $this->createLocation( 'Other Location' ); + $otherItemId = $this->createItem( 'Other Item' ); + $otherItemModel = new Item( $otherItemId ); + + $timeframeId = $this->createTimeframe( + $otherLocationId, + $otherItemId, + strtotime( '-1 day', strtotime( self::CURRENT_DATE ) ), + strtotime( '+10 days', strtotime( self::CURRENT_DATE ) ), + \CommonsBooking\Wordpress\CustomPostType\Timeframe::BOOKABLE_ID, + 'on', + 'd', + 0, + '8:00 AM', + '12:00 PM', + 'publish', + [], + '', + CustomPostTypeTest::USER_ID, + 3, // booking start day offset + 30, + 2 + ); + + ClockMock::freeze( new \DateTime( self::CURRENT_DATE ) ); + + // should still show the timeframe as the closest + $this->assertEquals( $timeframeId, $otherItemModel->getClosestBookableTimeframe()->ID ); + } + + public function testGetClosestBookableTimeframe_withSlots() { + $otherLocationId = $this->createLocation( 'Other Location' ); + $otherItemId = $this->createItem( 'Other Item' ); + $timeframes = $this->createTwoBookableTimeframeSlotsIncludingCurrentDay( $otherLocationId, $otherItemId ); + $otherItemModel = new Item( $otherItemId ); + $dt = new \DateTime( self::CURRENT_DATE ); + ClockMock::freeze( $dt ); + + // even though it is not open yet (bc we have midnight), the first slot should be the closest + $this->assertEquals( $timeframes[0], $otherItemModel->getClosestBookableTimeframe()->ID ); + + // After 3PM, the other one should be picked + $dt->modify( '15:01' ); + ClockMock::freeze( $dt ); + $this->assertEquals( $timeframes[1], $otherItemModel->getClosestBookableTimeframe()->ID ); + } protected function setUp(): void { parent::setUp(); $this->restrictionIds[] = $this->createRestriction( diff --git a/tests/php/Model/TimeframeTest.php b/tests/php/Model/TimeframeTest.php index b6fb99ce6..590fd2131 100644 --- a/tests/php/Model/TimeframeTest.php +++ b/tests/php/Model/TimeframeTest.php @@ -731,6 +731,22 @@ public function testIsBookable() { // This test does not work, function maybe broken? } + public function testIsBookableFilterOverridesDecision() { + // Default: the timeframe is bookable. + $this->assertTrue( $this->validTF->isBookable() ); + + // The filter can forbid it and receives the timeframe instance. + $received = null; + $filter = function ( $bookable, $timeframe ) use ( &$received ) { + $received = $timeframe; + return false; + }; + add_filter( 'commonsbooking_is_timeframe_bookable', $filter, 10, 2 ); + $this->assertFalse( $this->validTF->isBookable() ); + $this->assertInstanceOf( Timeframe::class, $received ); + remove_filter( 'commonsbooking_is_timeframe_bookable', $filter, 10 ); + } + /** * Tests all validity concerns of timeframes with manual repetition * @return void diff --git a/tests/php/Repository/ItemTest.php b/tests/php/Repository/ItemTest.php index 64baef6f6..d559fbe95 100644 --- a/tests/php/Repository/ItemTest.php +++ b/tests/php/Repository/ItemTest.php @@ -4,21 +4,44 @@ use CommonsBooking\Repository\Item; use CommonsBooking\Tests\Wordpress\CustomPostTypeTest; +use SlopeIt\ClockMock\ClockMock; class ItemTest extends CustomPostTypeTest { protected function setUp(): void { parent::setUp(); - - // Create timeframe with location and item, so that we can search vor it + ClockMock::freeze( new \DateTime( self::CURRENT_DATE ) ); + // Create timeframe with location and item, so that we can search for it $this->createTimeframe( $this->locationId, $this->itemId, - strtotime( 'midnight' ), - strtotime( '+90 days' ) + strtotime( 'midnight', strtotime( self::CURRENT_DATE ) ), + strtotime( '+90 days', strtotime( self::CURRENT_DATE ) ) ); } + public function testGetByCloakedId(): void { + $itemModel = new \CommonsBooking\Model\Item( $this->itemId ); + + // basic test, without any booking + $cloakedId = $itemModel->getCloakedId(); + $this->assertEquals( $this->itemId, Item::getByCloakedId( $cloakedId )->ID ); + + // with booking in past + $this->createBooking( + $this->locationId, + $this->itemId, + strtotime( '-2 days', strtotime( self::CURRENT_DATE ) ), + strtotime( '-1 day', strtotime( self::CURRENT_DATE ) ) + ); + $bookingCloakedId = $itemModel->getCloakedId(); + $this->assertNotEquals( $bookingCloakedId, $cloakedId ); + $this->assertEquals( $this->itemId, Item::getByCloakedId( $bookingCloakedId )->ID ); + + // test for invalid cloaked id + $this->assertNull( Item::getByCloakedId( 'invalid-cloaked-id' ) ); + } + public function testGetByLocation(): void { $this->assertEquals( [ $this->itemId ], diff --git a/tests/php/Repository/TimeframeTest.php b/tests/php/Repository/TimeframeTest.php index d65927e8c..00776ca3a 100644 --- a/tests/php/Repository/TimeframeTest.php +++ b/tests/php/Repository/TimeframeTest.php @@ -52,6 +52,27 @@ private function createOtherTimeframe( $start = null, $end = null ) { } + public function testBookableTimeframesFilterOverridesResult() { + $sentinel = array( 'sentinel' ); + $receivedLocations = null; + $receivedItems = null; + $filter = function ( array $timeframes, array $locations, array $items ) use ( $sentinel, &$receivedLocations, &$receivedItems ) { + $receivedLocations = $locations; + $receivedItems = $items; + return $sentinel; + }; + + add_filter( 'commonsbooking_bookable_timeframes', $filter, 10, 3 ); + $result = Timeframe::getBookable( array( $this->locationId ), array( $this->itemId ) ); + remove_filter( 'commonsbooking_bookable_timeframes', $filter, 10 ); + + // The returned value is exactly what the filter produced. + $this->assertSame( $sentinel, $result ); + // The filter received the location/item scope of the query. + $this->assertEquals( array( $this->locationId ), $receivedLocations ); + $this->assertEquals( array( $this->itemId ), $receivedItems ); + } + public function testGetInRange() { $inRangeTimeFrames = Timeframe::getInRange( $this->repetition_start, $this->repetition_end ); // All timeframes should be in range diff --git a/tests/php/Service/BookingLifecycleTest.php b/tests/php/Service/BookingLifecycleTest.php new file mode 100644 index 000000000..bbd84e889 --- /dev/null +++ b/tests/php/Service/BookingLifecycleTest.php @@ -0,0 +1,175 @@ +> + */ + private array $fired = array(); + + /** + * Inserts a booking post the way the plugin does (type meta as part of the + * insert), so that the `transition_post_status` action sees a real booking. + */ + private function insertBooking( string $status, int $end = null ): int { + $end = $end ?? ( time() + DAY_IN_SECONDS ); + + return wp_insert_post( + array( + 'post_title' => 'Lifecycle booking', + 'post_type' => BookingPostType::$postType, + 'post_status' => $status, + 'post_author' => self::USER_ID, + 'meta_input' => array( + 'type' => TimeframeCPT::BOOKING_ID, + Timeframe::META_LOCATION_ID => $this->locationId, + Timeframe::META_ITEM_ID => $this->itemId, + Timeframe::REPETITION_START => time(), + Timeframe::REPETITION_END => $end, + ), + ) + ); + } + + /** + * Registers a spy for a hook that records every invocation's arguments. + */ + private function spyOn( string $hook, int $args = 2 ): void { + $this->fired[ $hook ] = array(); + add_action( + $hook, + function ( ...$received ) use ( $hook ) { + $this->fired[ $hook ][] = $received; + }, + 10, + $args + ); + } + + public function testConfirmedHookFiresOnConfirmation() { + $this->spyOn( 'commonsbooking_booking_confirmed' ); + $bookingId = $this->insertBooking( 'unconfirmed' ); + + $this->assertCount( 0, $this->fired['commonsbooking_booking_confirmed'] ); + + wp_update_post( + array( + 'ID' => $bookingId, + 'post_status' => 'confirmed', + ) + ); + + $this->assertCount( 1, $this->fired['commonsbooking_booking_confirmed'] ); + [ $id, $booking ] = $this->fired['commonsbooking_booking_confirmed'][0]; + $this->assertEquals( $bookingId, $id ); + $this->assertInstanceOf( Booking::class, $booking ); + } + + public function testConfirmedHookFiresWhenInsertedAsConfirmed() { + $this->spyOn( 'commonsbooking_booking_confirmed' ); + $bookingId = $this->insertBooking( 'confirmed' ); + + $this->assertCount( 1, $this->fired['commonsbooking_booking_confirmed'] ); + $this->assertEquals( $bookingId, $this->fired['commonsbooking_booking_confirmed'][0][0] ); + } + + public function testStatusChangedHookCarriesOldAndNewStatus() { + $this->spyOn( 'commonsbooking_booking_status_changed', 4 ); + $bookingId = $this->insertBooking( 'unconfirmed' ); + + // Reset to only assert the confirm transition below. + $this->fired['commonsbooking_booking_status_changed'] = array(); + + wp_update_post( + array( + 'ID' => $bookingId, + 'post_status' => 'confirmed', + ) + ); + + $this->assertCount( 1, $this->fired['commonsbooking_booking_status_changed'] ); + [ $id, $old, $new, $booking ] = $this->fired['commonsbooking_booking_status_changed'][0]; + $this->assertEquals( $bookingId, $id ); + $this->assertEquals( 'unconfirmed', $old ); + $this->assertEquals( 'confirmed', $new ); + $this->assertInstanceOf( Booking::class, $booking ); + } + + public function testCreatedHookFiresExactlyOnce() { + $this->spyOn( 'commonsbooking_booking_created' ); + $bookingId = $this->insertBooking( 'unconfirmed' ); + + $this->assertCount( 1, $this->fired['commonsbooking_booking_created'] ); + $this->assertEquals( $bookingId, $this->fired['commonsbooking_booking_created'][0][0] ); + + // A later status change must not fire "created" again. + wp_update_post( + array( + 'ID' => $bookingId, + 'post_status' => 'confirmed', + ) + ); + + $this->assertCount( 1, $this->fired['commonsbooking_booking_created'] ); + } + + public function testCancelledHookFiresOnCancel() { + $this->spyOn( 'commonsbooking_booking_cancelled' ); + $bookingId = $this->insertBooking( 'confirmed' ); + $this->fired['commonsbooking_booking_cancelled'] = array(); + + $booking = new Booking( $bookingId ); + $booking->cancel(); + + $this->assertCount( 1, $this->fired['commonsbooking_booking_cancelled'] ); + [ $id, $cancelledBooking ] = $this->fired['commonsbooking_booking_cancelled'][0]; + $this->assertEquals( $bookingId, $id ); + $this->assertInstanceOf( Booking::class, $cancelledBooking ); + } + + public function testNoHooksForPostsWithoutBookingTypeMeta() { + $this->spyOn( 'commonsbooking_booking_created' ); + $this->spyOn( 'commonsbooking_booking_status_changed', 4 ); + + // A cb_booking post without the booking type meta is not a real booking. + $postId = wp_insert_post( + array( + 'post_title' => 'Not a booking', + 'post_type' => BookingPostType::$postType, + 'post_status' => 'confirmed', + 'post_author' => self::USER_ID, + ) + ); + + wp_update_post( + array( + 'ID' => $postId, + 'post_status' => 'draft', + ) + ); + + $this->assertCount( 0, $this->fired['commonsbooking_booking_created'] ); + $this->assertCount( 0, $this->fired['commonsbooking_booking_status_changed'] ); + } + + public function testCreatedMetaFlagConstantIsSet() { + $bookingId = $this->insertBooking( 'unconfirmed' ); + $this->assertEquals( 1, get_post_meta( $bookingId, BookingLifecycle::CREATED_FIRED_META, true ) ); + } +} diff --git a/tests/php/Service/BookingRuleTest.php b/tests/php/Service/BookingRuleTest.php index 50d0b9142..64f9f2a93 100644 --- a/tests/php/Service/BookingRuleTest.php +++ b/tests/php/Service/BookingRuleTest.php @@ -277,6 +277,58 @@ public function testCheckMaxBookingDays_earlyCancel() { $this->assertNull( BookingRule::checkMaxBookingDays( $allowedBooking, array( 4, 30 ) ) ); } + /** + * Regression test for #2158 + * @return void + */ + public function testCheckMaxBookingDays_unevenDays() { + ClockMock::freeze( new \DateTime( self::CURRENT_DATE ) ); + $testBookingOne = new Booking( + get_post( + $this->createBooking( + $this->locationId, + $this->itemId, + strtotime( '+1 day' ), + strtotime( '+2 days' ), + '8:00 AM', + '12:00 PM', + 'confirmed', + $this->subscriberId + ) + ) + ); + $testBookingTwo = new Booking( + get_post( + $this->createBooking( + $this->locationId, + $this->itemId, + strtotime( '+4 day' ), + strtotime( '+5 days' ), + '8:00 AM', + '12:00 PM', + 'confirmed', + $this->subscriberId + ) + ) + ); + + $testBookingThree = new Booking( + get_post( + $this->createBooking( + $this->locationId, + $this->itemId, + strtotime( '+6 day' ), + strtotime( '+7 days' ), + '8:00 AM', + '12:00 PM', + 'unconfirmed', + $this->subscriberId + ) + ) + ); + $this->assertBookingsPresent( array( $testBookingOne, $testBookingTwo ), BookingRule::checkMaxBookingDays( $testBookingThree, array( 2, 25 ) ) ); + } + public function testCheckMaxBookingDaysPerWeek() { // rule settings $allowedPerWeek = 2; diff --git a/tests/php/View/CalendarTest.php b/tests/php/View/CalendarTest.php index e84f5080a..2fe5e992f 100644 --- a/tests/php/View/CalendarTest.php +++ b/tests/php/View/CalendarTest.php @@ -7,6 +7,7 @@ use CommonsBooking\Wordpress\CustomPostType\Item; use CommonsBooking\View\Calendar; use DateTime; +use Override; use SlopeIt\ClockMock\ClockMock; /** @@ -43,6 +44,36 @@ public function testKeepDateRangeParam() { $this->assertTrue( $firstDay == $startDate ); } + public function testCalendarDataFilterAdjustsData() { + $startDate = date( 'Y-m-d', strtotime( self::CURRENT_DATE ) ); + + $receivedItem = null; + $receivedLocation = null; + $filter = function ( array $calendarData, $item, $location ) use ( &$receivedItem, &$receivedLocation ) { + $receivedItem = $item; + $receivedLocation = $location; + $calendarData['injectedByFilter'] = true; + return $calendarData; + }; + + add_filter( 'commonsbooking_calendar_data', $filter, 10, 3 ); + $jsonresponse = Calendar::getCalendarDataArray( + $this->itemId, + $this->locationId, + $startDate, + date( 'Y-m-d', strtotime( '+20 days', strtotime( self::CURRENT_DATE ) ) ), + true + ); + remove_filter( 'commonsbooking_calendar_data', $filter, 10 ); + + // The filter's change is reflected in the returned data. + $this->assertArrayHasKey( 'injectedByFilter', $jsonresponse ); + $this->assertTrue( $jsonresponse['injectedByFilter'] ); + // The filter received the item and location the calendar is for. + $this->assertEquals( $this->itemId, $receivedItem ); + $this->assertEquals( $this->locationId, $receivedLocation ); + } + public function testAdvancedBookingDays() { $startDate = date( 'Y-m-d', strtotime( 'midnight' ) ); $endDate = date( 'Y-m-d', strtotime( '+60 days midnight' ) ); @@ -229,6 +260,7 @@ public function testRepetition() { } public function testRenderTable() { + \CommonsBooking\Wordpress\CustomPostType\Item::registerPostTypeTaxonomy(); $calendar = Calendar::renderTable( [] ); $item = new \CommonsBooking\Model\Item( $this->itemId ); $location = new \CommonsBooking\Model\Location( $this->locationId ); @@ -241,15 +273,11 @@ public function testRenderTable() { $taxonomyItem = new \CommonsBooking\Model\Item( $taxonomyItem ); $term = wp_create_term( 'Test Category', Item::getTaxonomyName() ); wp_set_post_terms( $taxonomyItem->ID, [ $term['term_id'] ], Item::getTaxonomyName() ); - $termObj = get_term_by( 'id', $term['term_id'], Item::getTaxonomyName() ); - $slug = $termObj->slug; - $timeframe = $this->createTimeframe( - $this->locationId, - $taxonomyItem->ID, - strtotime( '+' . self::timeframeStart . ' days midnight', $this->now ), - strtotime( '+' . self::timeframeEnd . ' days midnight', $this->now ) - ); - $calendar = Calendar::renderTable( [ 'itemcat' => $slug ] ); + $termObj = get_term_by( 'id', $term['term_id'], Item::getTaxonomyName() ); + $slug = $termObj->slug; + $this->createBookableTimeFrameIncludingCurrentDay( $location->ID, $taxonomyItem->ID ); + wp_cache_flush(); // for some reason, the tests were failing without this + $calendar = Calendar::renderTable( [ 'itemcat' => $slug ] ); $this->assertStringContainsString( $taxonomyItem->post_title, $calendar ); $this->assertStringNotContainsString( $item->post_title, $calendar ); @@ -474,8 +502,9 @@ private function createTimeframeFromConfig( string $name, int $itemId, int $loca } protected function setUp(): void { - parent::setUp(); + ClockMock::freeze( new \DateTime( self::CURRENT_DATE ) ); + parent::setUp(); $this->now = time(); $this->timeframeId = $this->createTimeframe( $this->locationId, @@ -500,4 +529,9 @@ protected function setUp(): void { strtotime( '+300 days midnight' ) ); } + + protected function tearDown(): void { + ClockMock::reset(); + parent::tearDown(); + } } diff --git a/tests/php/View/ViewTest.php b/tests/php/View/ViewTest.php index 30982d9a3..0f59d0c9a 100644 --- a/tests/php/View/ViewTest.php +++ b/tests/php/View/ViewTest.php @@ -89,62 +89,54 @@ protected function setUp(): void { parent::setUp(); ClockMock::freeze( new \DateTime( self::CURRENT_DATE ) ); - $now = time(); - $this->now = $now; - $this->item = new Item( $this->itemId ); $this->location = new Location( $this->locationId ); - - $timeframeId = $this->createTimeframe( + $timeframeId = $this->createTimeframe( $this->locationId, $this->itemId, - strtotime( '+5 days midnight', $now ), - strtotime( '+6 days midnight', $now ), + strtotime( '+5 days midnight', strtotime( self::CURRENT_DATE ) ), + strtotime( '+6 days midnight', strtotime( self::CURRENT_DATE ) ), ); // set booking days in advance update_post_meta( $timeframeId, \CommonsBooking\Model\Timeframe::META_TIMEFRAME_ADVANCE_BOOKING_DAYS, self::bookingDaysInAdvance ); - $timeframeId = $this->createTimeframe( $this->locationId, $this->itemId, - strtotime( '+2 days midnight', $now ), - strtotime( '+3 days midnight', $now ), - );// set booking days in advance + strtotime( '+2 days midnight', strtotime( self::CURRENT_DATE ) ), + strtotime( '+3 days midnight', strtotime( self::CURRENT_DATE ) ), + ); + // set booking days in advance update_post_meta( $timeframeId, \CommonsBooking\Model\Timeframe::META_TIMEFRAME_ADVANCE_BOOKING_DAYS, self::bookingDaysInAdvance ); - $timeframeId = $this->createTimeframe( $this->locationId, $this->itemId, - strtotime( '+8 days midnight', $now ), - strtotime( '+9 days midnight', $now ), + strtotime( '+8 days midnight', strtotime( self::CURRENT_DATE ) ), + strtotime( '+9 days midnight', strtotime( self::CURRENT_DATE ) ), ); // set booking days in advance update_post_meta( $timeframeId, \CommonsBooking\Model\Timeframe::META_TIMEFRAME_ADVANCE_BOOKING_DAYS, self::bookingDaysInAdvance ); - $timeframeId = $this->createTimeframe( $this->locationId, $this->itemId, - strtotime( '+12 days midnight', $now ), - strtotime( '+13 days midnight', $now ), + strtotime( '+12 days midnight', strtotime( self::CURRENT_DATE ) ), + strtotime( '+13 days midnight', strtotime( self::CURRENT_DATE ) ), ); // set booking days in advance update_post_meta( $timeframeId, \CommonsBooking\Model\Timeframe::META_TIMEFRAME_ADVANCE_BOOKING_DAYS, self::bookingDaysInAdvance ); - $timeframeId = $this->createTimeframe( $this->locationId, $this->itemId, - strtotime( '+14 days midnight', $now ), - strtotime( '+15 days midnight', $now ), + strtotime( '+14 days midnight', strtotime( self::CURRENT_DATE ) ), + strtotime( '+15 days midnight', strtotime( self::CURRENT_DATE ) ), ); // set booking days in advance update_post_meta( $timeframeId, \CommonsBooking\Model\Timeframe::META_TIMEFRAME_ADVANCE_BOOKING_DAYS, self::bookingDaysInAdvance ); - // this timeframe should not be in shortcode data, because it's out of 30 days advanced booking range $timeframeId = $this->createTimeframe( $this->locationId, $this->itemId, - strtotime( '+32 days midnight', $now ), - strtotime( '+33 days midnight', $now ), + strtotime( '+32 days midnight', strtotime( self::CURRENT_DATE ) ), + strtotime( '+33 days midnight', strtotime( self::CURRENT_DATE ) ), ); // set booking days in advance update_post_meta( $timeframeId, \CommonsBooking\Model\Timeframe::META_TIMEFRAME_ADVANCE_BOOKING_DAYS, self::bookingDaysInAdvance ); @@ -152,5 +144,6 @@ protected function setUp(): void { protected function tearDown(): void { parent::tearDown(); + ClockMock::reset(); } } diff --git a/tests/php/Wordpress/CustomPostType/BookingTest.php b/tests/php/Wordpress/CustomPostType/BookingTest.php index b1a5eff59..9e9a4ac89 100644 --- a/tests/php/Wordpress/CustomPostType/BookingTest.php +++ b/tests/php/Wordpress/CustomPostType/BookingTest.php @@ -50,7 +50,7 @@ public function testHandleBookingRequest_Default() { $this->assertFalse( $bookingModel->isConfirmed() ); // Case 2: We now confirm the booking. The booking should be confirmed - $newBookingId = Booking::handleBookingRequest( + $confirmedBookingId = Booking::handleBookingRequest( $this->itemId, $this->locationId, 'confirmed', @@ -61,10 +61,10 @@ public function testHandleBookingRequest_Default() { $postName, null ); - $this->bookingIds[] = $newBookingId; + $this->bookingIds[] = $confirmedBookingId; // the id should be the same - $this->assertEquals( $bookingId, $newBookingId ); + $this->assertEquals( $bookingId, $confirmedBookingId ); // we create a new model, just to be sure $bookingModel = new \CommonsBooking\Model\Booking( $bookingId ); $this->assertTrue( $bookingModel->isConfirmed() ); @@ -97,6 +97,46 @@ public function testHandleBookingRequest_Default() { $this->assertEquals( $date->format( 'Y-m-d H:i:s' ), $cancelDate->format( 'Y-m-d H:i:s' ) ); } + /** + * The commonsbooking_booking_before_save filter can add meta to a booking + * before it is persisted, and receives null as the "existing booking" for a + * newly created booking. + */ + public function testBookingBeforeSaveFilterAddsMeta() { + $date = new \DateTime( self::CURRENT_DATE ); + $date->modify( '-1 day' ); + ClockMock::freeze( $date ); + + $received = 'unset'; + $filter = function ( array $postarr, $booking ) use ( &$received ) { + $received = $booking; + $postarr['meta_input']['my_external_ref'] = 'ext-123'; + return $postarr; + }; + add_filter( 'commonsbooking_booking_before_save', $filter, 10, 2 ); + + $bookingId = Booking::handleBookingRequest( + $this->itemId, + $this->locationId, + 'unconfirmed', + null, + null, + strtotime( self::CURRENT_DATE ), + strtotime( '+1 day', strtotime( self::CURRENT_DATE ) ), + null, + null + ); + $this->bookingIds[] = $bookingId; + + remove_filter( 'commonsbooking_booking_before_save', $filter, 10 ); + + $this->assertIsInt( $bookingId ); + // The meta added by the filter was persisted with the new booking. + $this->assertEquals( 'ext-123', get_post_meta( $bookingId, 'my_external_ref', true ) ); + // For a new booking, the existing-booking argument is null. + $this->assertNull( $received ); + } + public function testHandleBookingRequest_deleteUnconfirmed() { ClockMock::freeze( new \DateTime( self::CURRENT_DATE ) ); // We create an unconfirmed booking and then cancel the booking. The booking should be canceled @@ -163,7 +203,7 @@ public function testHandleBookingRequest_Overbooking() { $this->assertFalse( $bookingModel->isConfirmed() ); // The overbooked days are not present anymore when confirming the booking cause they are only calculated on the Litepicker screen - $newBookingId = Booking::handleBookingRequest( + $confirmedBookingId = Booking::handleBookingRequest( $this->itemId, $this->locationId, 'confirmed', @@ -174,10 +214,10 @@ public function testHandleBookingRequest_Overbooking() { $postName, null ); - $this->bookingIds[] = $newBookingId; + $this->bookingIds[] = $confirmedBookingId; // the id should be the same - $this->assertEquals( $bookingId, $newBookingId ); + $this->assertEquals( $bookingId, $confirmedBookingId ); // we create a new model, just to be sure $bookingModel = new \CommonsBooking\Model\Booking( $bookingId ); $this->assertTrue( $bookingModel->isConfirmed() ); @@ -347,6 +387,65 @@ public function testHandleBookingRequest_deleted_confirm() { ); } + /** + * Regression test for #2217 + * When a booking is confirmed and another booking is created in the exact same timeframe afterwards, + * the previously confirmed booking was set to unconfirmed again. This test is in place to ensure that + * this does not happen again. + * + * @return void + */ + public function testHandleBookingRequest_noRecreation() { + $date = new \DateTime( self::CURRENT_DATE ); + $date->modify( '-1 day' ); + ClockMock::freeze( $date ); + // create regular booking through unconfirmed -> confirmed route + $bookingId = Booking::handleBookingRequest( + $this->itemId, + $this->locationId, + 'unconfirmed', + null, + null, + strtotime( self::CURRENT_DATE ), + strtotime( '+1 day', strtotime( self::CURRENT_DATE ) ), + null, + null + ); + $this->bookingIds[] = $bookingId; + + $bookingModel = new \CommonsBooking\Model\Booking( $bookingId ); + $postName = $bookingModel->post_name; + $confirmedBookingId = Booking::handleBookingRequest( + $this->itemId, + $this->locationId, + 'confirmed', + $bookingId, + null, + strtotime( self::CURRENT_DATE ), + strtotime( '+1 day', strtotime( self::CURRENT_DATE ) ), + $postName, + null + ); + $this->bookingIds[] = $confirmedBookingId; + + // attempt to recreate the booking, should keep status as "confirmed" because it was not explicitly cancelled + $bookingId = Booking::handleBookingRequest( + $this->itemId, + $this->locationId, + 'unconfirmed', + null, + null, + strtotime( self::CURRENT_DATE ), + strtotime( '+1 day', strtotime( self::CURRENT_DATE ) ), + null, + null + ); + $this->bookingIds[] = $bookingId; + + $bookingModel = new \CommonsBooking\Model\Booking( $bookingId ); + $this->assertTrue( $bookingModel->isConfirmed() ); + } + /** * This will check if the bookings can be exported through the WordPress personal data export tool * @return void diff --git a/tests/php/Wordpress/CustomPostTypeTest.php b/tests/php/Wordpress/CustomPostTypeTest.php index 3543bee7f..671d4f9cd 100644 --- a/tests/php/Wordpress/CustomPostTypeTest.php +++ b/tests/php/Wordpress/CustomPostTypeTest.php @@ -110,6 +110,9 @@ public function createCBManager() { protected function setUp(): void { parent::setUp(); + Plugin::clearCache(); + wp_cache_flush(); + $this->dateFormatted = date( 'Y-m-d', strtotime( self::CURRENT_DATE ) ); $this->setUpBookingCodesTable(); @@ -142,6 +145,8 @@ protected function tearDown(): void { ClockMock::reset(); $this->tearDownAllPosts(); + Plugin::clearCache(); + wp_cache_flush(); $this->tearDownBookingCodesTable(); wp_logout();