Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/bookly-responsive-appointment-booking-tool/backend/components/dialogs/appointment/edit/Ajax.php
+++ b/bookly-responsive-appointment-booking-tool/backend/components/dialogs/appointment/edit/Ajax.php
@@ -437,80 +437,15 @@
*/
public static function getDaySchedule()
{
- $staff_ids = array( self::parameter( 'staff_id' ) );
- $service_id = self::parameter( 'service_id' );
- $service = Service::find( $service_id );
- $date = self::parameter( 'date' );
-
- $appointment_id = self::parameter( 'appointment_id' );
- $location_id = self::parameter( 'location_id' );
- $nop = max( 1, self::parameter( 'nop', 1 ) );
-
- // Get array of extras with max duration
- $extras = ProxyExtras::getMaxDurationExtras( self::parameter( 'extras', array() ) );
-
- $chain_item = new LibChainItem();
- $chain_item
- ->setStaffIds( $staff_ids )
- ->setServiceId( $service_id )
- ->setLocationId( $location_id )
- ->setNumberOfPersons( $nop )
- ->setQuantity( 1 )
- ->setLocationId( $location_id )
- ->setUnits( 1 )
- ->setExtras( $extras );
-
- $chain = new LibChain();
- $chain->add( $chain_item );
-
- $custom_slot = array();
- $ignore_appointments = array();
- if ( $appointment_id ) {
- $appointment = Appointment::find( $appointment_id );
- if ( date_create( $appointment->getStartDate() )->format( 'Y-m-d' ) === date_create( $date )->format( 'Y-m-d' ) ) {
- $custom_slot = array(
- 'title' => DatePoint::fromStr( $appointment->getStartDate() )->formatI18n( get_option( 'time_format' ) ),
- 'value' => date_create( $appointment->getStartDate() )->format( 'H:i' ),
- );
- }
- $ignore_appointments[] = $appointment_id;
- }
-
- $scheduler = new LibScheduler( $chain, date_create( $date )->format( 'Y-m-d 00:00' ), date_create( $date )->format( 'Y-m-d' ), 'daily', array( 'every' => 1 ), array(), false, $ignore_appointments );
- $schedule = $scheduler->scheduleForFrontend( 1 );
- $result = array();
- $time_format = get_option( 'time_format' );
- if ( isset( $schedule[0]['options'] ) ) {
- foreach ( $schedule[0]['options'] as $slot ) {
- $value = json_decode( $slot['value'], true );
- $date = date_create( $value[0][2] );
- $value = $date->format( 'H:i' );
- if ( ! empty( $custom_slot ) && $value === $custom_slot['value'] ) {
- $custom_slot = array();
- }
- if ( ! empty( $custom_slot ) && strcmp( $value, $custom_slot['value'] ) > 0 ) {
- $result[] = $custom_slot;
- $custom_slot = array();
- }
- $end_date = clone $date;
- $end_date = $end_date->modify( $service->getDuration() . ' seconds' );
- $result['start'][] = array(
- 'title' => $slot['title'],
- 'value' => $value,
- 'disabled' => $slot['disabled'],
- );
-
- $result['end'][] = array(
- 'title_time' => date_i18n( $time_format, $end_date->getTimestamp() ),
- 'value' => $end_date->getTimestamp() - $date->modify( 'midnight' )->getTimestamp() >= DAY_IN_SECONDS ? ( (int) $end_date->format( 'H' ) + 24 ) . ':' . $end_date->format( 'i' ) : $end_date->format( 'H:i' ),
- 'disabled' => $slot['disabled'],
- );
- }
- }
-
- if ( ! empty( $custom_slot ) ) {
- $result[] = $custom_slot;
- }
+ $result = LibUtilsAppointment::getDaySchedule(
+ array( self::parameter( 'staff_id' ) ),
+ self::parameter( 'service_id' ),
+ self::parameter( 'date' ),
+ self::parameter( 'appointment_id' ),
+ self::parameter( 'location_id' ),
+ self::parameter( 'extras', array() ),
+ max( 1, self::parameter( 'nop', 1 ) )
+ );
wp_send_json_success( $result );
}
--- a/bookly-responsive-appointment-booking-tool/backend/components/dialogs/sms/templates/_settings.php
+++ b/bookly-responsive-appointment-booking-tool/backend/components/dialogs/sms/templates/_settings.php
@@ -159,7 +159,7 @@
</select>
</div>
<div class="align-self-center mx-2">
- <?php esc_html_e( 'at', 'bookly' ) ?>
+ <?php echo esc_html_x( 'at', 'at time', 'bookly' ) ?>
</div>
<div>
<select class="form-control custom-select" name="notification[settings][at_hour]">
@@ -190,7 +190,7 @@
</select>
</div>
<div class="align-self-center mx-2">
- <?php esc_html_e( 'at', 'bookly' ) ?>
+ <?php echo esc_html_x( 'at', 'at time', 'bookly' ) ?>
</div>
<div>
<select class="form-control custom-select" name="notification[settings][before_at_hour]">
--- a/bookly-responsive-appointment-booking-tool/backend/components/schedule/Select.php
+++ b/bookly-responsive-appointment-booking-tool/backend/components/schedule/Select.php
@@ -40,7 +40,7 @@
// Insert empty value if required.
if ( $options['use_empty'] ) {
- $this->values[ null ] = $options['empty_value'];
+ $this->values[''] = $options['empty_value'];
}
$ts_length = LibConfig::getTimeSlotLength();
--- a/bookly-responsive-appointment-booking-tool/backend/modules/appointments/Ajax.php
+++ b/bookly-responsive-appointment-booking-tool/backend/modules/appointments/Ajax.php
@@ -112,9 +112,10 @@
* @param array $columns
* @param array $order
* @param bool $export
+ * @param string|null $display_tz
* @return array
*/
- public static function getAppointmentsTableData( $filter = array(), $limits = array(), $columns = array(), $order = array(), $export = false )
+ public static function getAppointmentsTableData( $filter = array(), $limits = array(), $columns = array(), $order = array(), $export = false, $display_tz = null )
{
$postfix_any = sprintf( ' (%s)', get_option( 'bookly_l10n_option_employee' ) );
$postfix_archived = sprintf( ' (%s)', __( 'Archived', 'bookly' ) );
@@ -254,13 +255,18 @@
$locations_active = LibConfig::locationsActive();
+ if ( $display_tz === null ) {
+ $convert_tz = false;
+ } else {
+ $wp_tz = LibConfig::getWPTimeZone();
+ $convert_tz = $display_tz != $wp_tz;
+ }
+
$data = array();
foreach ( $query->fetchArray() as $row ) {
- // Service duration.
$service_duration = $export
? (int) ( $row['service_duration'] / MINUTE_IN_SECONDS )
: LibUtilsDateTime::secondsToInterval( $row['service_duration'] );
- // Payment title.
$payment_title = '';
$payment_raw_title = '';
if ( $row['payment'] !== null && $row['status'] !== LibEntitiesCustomerAppointment::STATUS_WAITLISTED ) {
@@ -285,9 +291,9 @@
}
// Appointment status.
$row['status'] = LibEntitiesCustomerAppointment::statusToString( $row['status'] );
- // Custom fields
$customer_appointment = new LibEntitiesCustomerAppointment();
$customer_appointment->load( $row['ca_id'] );
+ // Custom fields
foreach ( LibProxyCustomFields::getForCustomerAppointment( $customer_appointment ) ?: array() as $custom_field ) {
$custom_fields[ $custom_field['id'] ] = $custom_field['value'];
}
@@ -312,6 +318,11 @@
$online_meeting_start_url = $row['online_meeting_id'];
}
+ if ( $convert_tz ) {
+ $row['start_date'] = $row['start_date'] ? LibUtilsDateTime::convertTimeZone( $row['start_date'], $wp_tz, $display_tz ) : null;
+ $row['created_date'] = LibUtilsDateTime::convertTimeZone( $row['created_date'], $wp_tz, $display_tz );
+ }
+
$data[] = array(
'id' => $row['id'],
'no' => LibConfig::groupBookingActive() && $row['ca_id'] ? $row['id'] . '-' . $row['ca_id'] : $row['ca_id'],
--- a/bookly-responsive-appointment-booking-tool/backend/modules/calendar/Page.php
+++ b/bookly-responsive-appointment-booking-tool/backend/modules/calendar/Page.php
@@ -16,8 +16,10 @@
*/
public static function render()
{
+ $calendar_version = get_option( 'bookly_legacy_calendar' ) ? 'legacy' : 'latest';
+
self::enqueueStyles( array(
- 'module' => array( 'css/event-calendar.min.css' => array( 'bookly-backend-globals' ) ),
+ 'module' => array( 'css/' . ( $calendar_version !== 'latest' ? 'event-calendar-4.min.css' : 'event-calendar.min.css' ) => array( 'bookly-backend-globals' ) ),
) );
$id = LibEntitiesAppointment::query()->fetchVar( 'MAX(id)' );
@@ -57,8 +59,8 @@
$staff_members ?
array(
'module' => array(
- 'js/event-calendar.min.js' => array( 'bookly-backend-globals' ),
- 'js/calendar-common.js' => array( 'bookly-event-calendar.min.js' ),
+ 'js/' . ( $calendar_version !== 'latest' ? 'event-calendar-4.min.js' : 'event-calendar.min.js' ) => array( 'bookly-backend-globals' ),
+ 'js/calendar-common.js' => array( 'bookly-' . ( $calendar_version !== 'latest' ? 'event-calendar-4.min.js' : 'event-calendar.min.js' ) ),
'js/calendar.js' => array( 'bookly-calendar-common.js', 'bookly-dropdown.js' ),
),
'backend' => array(
@@ -76,6 +78,7 @@
wp_localize_script( 'bookly-calendar.js', 'BooklyL10n', array_merge(
LibUtilsCommon::getCalendarSettings(),
array(
+ 'calendar_version' => $calendar_version,
'delete' => __( 'Delete', 'bookly' ),
'are_you_sure' => __( 'Are you sure?', 'bookly' ),
'filterResourcesWithEvents' => Config::showOnlyStaffWithAppointmentsInCalendarDayView(),
--- a/bookly-responsive-appointment-booking-tool/backend/modules/diagnostics/QueryBuilder.php
+++ b/bookly-responsive-appointment-booking-tool/backend/modules/diagnostics/QueryBuilder.php
@@ -545,6 +545,7 @@
'bookly_event_ticket_types.reserved' => array( 'type' => "int", 'is_nullabe' => 0, 'extra' => "", 'default' => null, 'key' => "" ),
'bookly_event_ticket_types.reserved_ps' => array( 'type' => "int", 'is_nullabe' => 0, 'extra' => "", 'default' => null, 'key' => "" ),
'bookly_event_ticket_types.price' => array( 'type' => "decimal(10,2)", 'is_nullabe' => 0, 'extra' => "", 'default' => "0.00", 'key' => "" ),
+ 'bookly_event_ticket_types.position' => array( 'type' => "int", 'is_nullabe' => 0, 'extra' => "", 'default' => "9999", 'key' => "" ),
'bookly_events.id' => array( 'type' => "int unsigned", 'is_nullabe' => 0, 'extra' => "auto_increment", 'default' => null, 'key' => "PRI" ),
'bookly_events.location_id' => array( 'type' => "int unsigned", 'is_nullabe' => 1, 'extra' => "", 'default' => null, 'key' => "MUL" ),
'bookly_events.title' => array( 'type' => "varchar(64)", 'is_nullabe' => 1, 'extra' => "", 'default' => null, 'key' => "" ),
@@ -570,6 +571,10 @@
'bookly_files.path' => array( 'type' => "text", 'is_nullabe' => 0, 'extra' => "", 'default' => null, 'key' => "" ),
'bookly_files.custom_field_id' => array( 'type' => "int", 'is_nullabe' => 1, 'extra' => "", 'default' => null, 'key' => "" ),
'bookly_files.ci_id' => array( 'type' => "int", 'is_nullabe' => 1, 'extra' => "", 'default' => null, 'key' => "" ),
+ 'bookly_form_sessions.id' => array( 'type' => "int unsigned", 'is_nullabe' => 0, 'extra' => "auto_increment", 'default' => null, 'key' => "PRI" ),
+ 'bookly_form_sessions.token' => array( 'type' => "varchar(255)", 'is_nullabe' => 0, 'extra' => "", 'default' => null, 'key' => "MUL" ),
+ 'bookly_form_sessions.value' => array( 'type' => "text", 'is_nullabe' => 1, 'extra' => "", 'default' => null, 'key' => "" ),
+ 'bookly_form_sessions.expire' => array( 'type' => "datetime", 'is_nullabe' => 0, 'extra' => "", 'default' => null, 'key' => "MUL" ),
'bookly_forms.id' => array( 'type' => "int unsigned", 'is_nullabe' => 0, 'extra' => "auto_increment", 'default' => null, 'key' => "PRI" ),
'bookly_forms.type' => array( 'type' => "enum('search-form','services-form','staff-form','cancellation-confirmation','tags-form','events-form','checkout-form')", 'is_nullabe' => 0, 'extra' => "", 'default' => "search-form", 'key' => "" ),
'bookly_forms.name' => array( 'type' => "varchar(255)", 'is_nullabe' => 0, 'extra' => "", 'default' => null, 'key' => "" ),
--- a/bookly-responsive-appointment-booking-tool/backend/modules/diagnostics/tests/Sessions.php
+++ b/bookly-responsive-appointment-booking-tool/backend/modules/diagnostics/tests/Sessions.php
@@ -1,50 +0,0 @@
-<?php
-namespace BooklyBackendModulesDiagnosticsTests;
-
-use BooklyLib;
-
-class Sessions extends Test
-{
- protected $slug = 'check-sessions';
- protected $hidden = true;
-
- protected $session_value1 = '0123456789';
- protected $session_value2 = '9876543210';
-
- public function __construct()
- {
- $this->title = __( 'PHP Sessions', 'bookly' );
- $this->description = sprintf( __( 'This test checks if PHP sessions are enabled. Bookly needs PHP sessions to work correctly. For more information about PHP sessions, please check the official PHP documentation %s.', 'bookly' ), '<a href="https://www.php.net/manual/en/intro.session.php">php.net/manual/en/intro.session.php</a>' );
- }
-
- /**
- * @inheritDoc
- */
- public function run()
- {
- LibSession::set( 'test-session-value', $this->session_value1 );
-
- return true;
- }
-
- public function ajax1()
- {
- if ( LibSession::get( 'test-session-value' ) === $this->session_value1 ) {
- LibSession::set( 'test-session-value', $this->session_value2 );
- wp_send_json_success();
- }
- $error = 'To enable PHP sessions, please check the official PHP documentation';
- wp_send_json_error( array( 'errors' => array( $error ) ) );
- }
-
- public function ajax2()
- {
- if ( LibSession::get( 'test-session-value' ) === $this->session_value2 ) {
- LibSession::destroy( 'test-session-value' );
- wp_send_json_success();
- }
-
- $error = 'To enable PHP sessions, please check the official PHP documentation';
- wp_send_json_error( array( 'errors' => array( $error ) ) );
- }
-}
No newline at end of file
--- a/bookly-responsive-appointment-booking-tool/backend/modules/diagnostics/tools/FormsData.php
+++ b/bookly-responsive-appointment-booking-tool/backend/modules/diagnostics/tools/FormsData.php
@@ -1,40 +0,0 @@
-<?php
-namespace BooklyBackendModulesDiagnosticsTools;
-
-use BooklyLibSession;
-
-class FormsData extends Tool
-{
- protected $slug = 'forms-data';
- protected $hidden = true;
- protected $template = '_forms_data';
-
-
- public function __construct()
- {
- $this->title = 'Forms data';
- }
-
- public function render()
- {
- $all_forms_data = Session::getAllFormsData();
- $last_touched_form_id = 0;
- $last_touched = 0;
- foreach ( $all_forms_data as $form_id => $data ) {
- if ( isset( $data['last_touched'] ) && $last_touched < $data['last_touched'] ) {
- $last_touched = $data['last_touched'];
- $last_touched_form_id = $form_id;
- }
- }
-
- return self::renderTemplate( '_forms_data', array( 'forms' => $all_forms_data, 'active' => $last_touched_form_id ), false );
- }
-
- public function destroy()
- {
- $form_id = self::parameter( 'form_id' );
- Session::destroyFormData( $form_id );
-
- wp_send_json_success();
- }
-}
No newline at end of file
--- a/bookly-responsive-appointment-booking-tool/backend/modules/settings/Page.php
+++ b/bookly-responsive-appointment-booking-tool/backend/modules/settings/Page.php
@@ -64,6 +64,7 @@
update_option( 'bookly_cal_show_new_appointments_badge', self::parameter( 'bookly_cal_show_new_appointments_badge' ) );
update_option( 'bookly_cal_last_seen_appointment', self::parameter( 'bookly_cal_last_seen_appointment' ) );
update_option( 'bookly_cal_scrollable_calendar', self::parameter( 'bookly_cal_scrollable_calendar' ) );
+ update_option( 'bookly_legacy_calendar', self::parameter( 'bookly_legacy_calendar' ) );
foreach ( self::parameter( 'status' ) as $status => $color ) {
if ( in_array( $status, array( CustomerAppointment::STATUS_PENDING, CustomerAppointment::STATUS_APPROVED, CustomerAppointment::STATUS_CANCELLED, CustomerAppointment::STATUS_REJECTED, 'mixed' ) ) ) {
update_option( sprintf( 'bookly_appointment_status_%s_color', $status ), $color );
@@ -121,9 +122,7 @@
update_option( 'bookly_gen_collect_stats', self::parameter( 'bookly_gen_collect_stats' ) );
update_option( 'bookly_gen_show_powered_by', self::parameter( 'bookly_gen_show_powered_by' ) );
update_option( 'bookly_gen_prevent_caching', (int) self::parameter( 'bookly_gen_prevent_caching' ) );
- update_option( 'bookly_gen_prevent_session_locking', (int) self::parameter( 'bookly_gen_prevent_session_locking' ) );
update_option( 'bookly_gen_badge_consider_news', (int) self::parameter( 'bookly_gen_badge_consider_news' ) );
- update_option( 'bookly_gen_session_type', self::parameter( 'bookly_gen_session_type' ) );
update_option( 'bookly_email_gateway', self::parameter( 'bookly_email_gateway' ) );
update_option( 'bookly_smtp_host', self::parameter( 'bookly_smtp_host' ) );
update_option( 'bookly_smtp_port', self::parameter( 'bookly_smtp_port' ) );
--- a/bookly-responsive-appointment-booking-tool/backend/modules/settings/templates/_calendarForm.php
+++ b/bookly-responsive-appointment-booking-tool/backend/modules/settings/templates/_calendarForm.php
@@ -13,6 +13,7 @@
<?php SettingsSelects::renderSingle( 'bookly_cal_show_only_business_hours', __( 'Show only business hours in the calendar', 'bookly' ), __( 'If this setting is enabled then the visible hours in the calendar will be limited to the company's business hours', 'bookly' ) ) ?>
<?php SettingsSelects::renderSingle( 'bookly_cal_show_only_staff_with_appointments', __( 'Show only staff members with appointments in Day view', 'bookly' ), __( 'If this setting is enabled then only staff members who have associated appointments will be displayed in the Day view', 'bookly' ) ) ?>
<?php SettingsSelects::renderSingle( 'bookly_cal_show_new_appointments_badge', __( 'Show new appointments notifications', 'bookly' ), __( 'If enabled, you will see an indicator near 'Calendar' for newly created appointments', 'bookly' ) ) ?>
+ <?php SettingsSelects::renderSingle( 'bookly_legacy_calendar', __( 'Legacy mode', 'bookly' ), __( 'If enabled, the calendar will use Legacy mode, which improves compatibility with older browsers.', 'bookly' ) ) ?>
<?php SettingsSelects::renderSingle( 'bookly_cal_scrollable_calendar', __( 'Scrollable calendar', 'bookly' ), __( 'If enabled, the backend calendar will occupy part of the screen and remain scrollable. If disabled, it will take up more space and scroll along with the entire page.', 'bookly' ), array(), array( 'data-expand' => '1' ) ) ?>
<div class="border-left mt-3 ml-4 pl-3 bookly_cal_scrollable_calendar-expander"<?php if ( get_option( 'bookly_cal_scrollable_calendar', '1' ) !== '0' ) : ?> style="display:none;"<?php endif ?>>
<?php SettingsSelects::renderSingle( 'bookly_cal_month_view_style', __( 'Month view style', 'bookly' ), __( 'Select the style for displaying appointments in Month view', 'bookly' ), array( array( 'classic', __( 'Classic', 'bookly' ) ), array( 'minimalistic', __( 'Minimalistic', 'bookly' ) ) ) ) ?>
--- a/bookly-responsive-appointment-booking-tool/backend/modules/settings/templates/_generalForm.php
+++ b/bookly-responsive-appointment-booking-tool/backend/modules/settings/templates/_generalForm.php
@@ -20,24 +20,15 @@
Selects::renderSingle( 'bookly_gen_use_client_time_zone', __( 'Display available time slots in client's time zone', 'bookly' ), __( 'The value is taken from client's browser.', 'bookly' ) );
Selects::renderSingle( 'bookly_gen_allow_staff_edit_profile', __( 'Allow staff members to edit their profiles', 'bookly' ), __( 'If this option is enabled then all staff members who are associated with WordPress users will be able to edit their own profiles, services, schedule and days off.', 'bookly' ) );
Selects::renderSingle( 'bookly_gen_link_assets_method', __( 'Method to include Bookly JavaScript and CSS files on the page', 'bookly' ), sprintf( __( 'Select method how to include Bookly JavaScript and CSS files on the page. For more information, see the <a href="%s" target="_blank">documentation</a> page.', 'bookly' ), 'https://hub.bookly.pro/go/bookly-settings-general' ), array(
- array(
- 'enqueue',
- __( 'All pages', 'bookly' ),
- ),
- array( 'print', __( 'Pages with Bookly form', 'bookly' ) ),
+ array(
+ 'enqueue',
+ __( 'All pages', 'bookly' ),
+ ),
+ array( 'print', __( 'Pages with Bookly form', 'bookly' ) ),
) );
Selects::renderSingle( 'bookly_gen_collect_stats', __( 'Help us improve Bookly by sending anonymous usage stats', 'bookly' ) );
Selects::renderSingle( 'bookly_gen_show_powered_by', __( 'Powered by Bookly' ), __( 'Allow the plugin to set a Powered by Bookly notice on the booking widget to spread information about the plugin. This will allow the team to improve the product and enhance its functionality', 'bookly' ) );
Selects::renderSingle( 'bookly_gen_prevent_caching', __( 'Prevent caching of pages with booking form', 'bookly' ), __( 'Select "Enabled" if you want Bookly to prevent caching by third-party caching plugins by adding a DONOTCACHEPAGE constant on pages with booking form', 'bookly' ) );
- Selects::renderSingle( 'bookly_gen_session_type', __( 'Session storage mode', 'bookly' ), __( 'Select where to store session data', 'bookly' ), array( array( 'php', 'PHP', 0 ), array( 'db', 'Database', 0 ) ), array( 'data-expand' => 'php' ) );
- ?>
- <div
- class="border-left mt-3 ml-4 pl-3 bookly_gen_session_type-expander"<?php if ( get_option( 'bookly_gen_session_type', 'db' ) === 'db' ) : ?> style="display:none;"<?php endif ?>>
- <?php
- Selects::renderSingle( 'bookly_gen_prevent_session_locking', __( 'Prevent PHP session locking', 'bookly' ), __( 'Enable this option to make Bookly close the PHP session as soon as it is done with it. This should prevent locking the session, which could cause various other processes to timeout or fail', 'bookly' ) );
- ?>
- </div>
- <?php
Selects::renderSingle( 'bookly_gen_badge_consider_news', __( 'Show news notifications', 'bookly' ), __( 'If enabled, News notification icon will be displayed', 'bookly' ) );
Selects::renderSingle( 'bookly_email_gateway', __( 'Mail gateway', 'bookly' ), sprintf( __( 'Select a mail gateway that will be used to send email notifications. For more information, see the <a href="%s" target="_blank">documentation</a> page.', 'bookly' ), 'https://hub.bookly.pro/go/bookly-settings-smtp' ), array( array( 'wp', __( 'WordPress mail', 'bookly' ), 0 ), array( 'smtp', 'SMTP', 0 ) ), array( 'data-expand' => 'smtp' ) );
?>
--- a/bookly-responsive-appointment-booking-tool/backend/modules/staff/Page.php
+++ b/bookly-responsive-appointment-booking-tool/backend/modules/staff/Page.php
@@ -23,7 +23,7 @@
// Allow add-ons to enqueue their assets.
ProxyShared::enqueueStaffProfileStyles();
ProxyShared::enqueueStaffProfileScripts();
- ProxyShared::renderStaffPage( self::parameters() );
+ $errors = ProxyShared::prepareCalendarErrors( array(), self::parameters() );
$categories = ProxyPro::getCategoriesList() ?: array();
foreach ( $categories as &$category ) {
@@ -45,6 +45,7 @@
'processing' => esc_attr__( 'Processing', 'bookly' ) . '…',
'emptyTable' => __( 'No data available in table', 'bookly' ),
'loadingRecords' => __( 'Loading...', 'bookly' ),
+ 'errors' => $errors,
'datatables' => $datatables,
) );
--- a/bookly-responsive-appointment-booking-tool/backend/modules/staff/forms/widgets/TimeChoice.php
+++ b/bookly-responsive-appointment-booking-tool/backend/modules/staff/forms/widgets/TimeChoice.php
@@ -26,7 +26,7 @@
// Insert empty value if required.
if ( $options['use_empty'] ) {
- $this->values[ null ] = $options['empty_value'];
+ $this->values[''] = $options['empty_value'];
}
$ts_length = LibConfig::getTimeSlotLength();
--- a/bookly-responsive-appointment-booking-tool/backend/modules/staff/proxy/Shared.php
+++ b/bookly-responsive-appointment-booking-tool/backend/modules/staff/proxy/Shared.php
@@ -8,7 +8,7 @@
* @method static void enqueueStaffProfileStyles() Enqueue styles for page Staff.
* @method static string getAffectedAppointmentsFilter( string $filter_url, int[] $staff_ids ) Get link with filter for appointments page.
* @method static LibQuery prepareGetStaffQuery( LibQuery $query ) Prepare get staff list query.
- * @method static array renderStaffPage( array $params ) Do stuff on staff page render.
+ * @method static array prepareCalendarErrors( array $errors, array $params ) Prepare errors for calendar page.
* @method static array searchStaff( array $fields, array $columns, LibQuery $query ) Search staff, prepare query and fields.
*/
abstract class Shared extends LibBaseProxy
--- a/bookly-responsive-appointment-booking-tool/frontend/modules/booking/Ajax.php
+++ b/bookly-responsive-appointment-booking-tool/frontend/modules/booking/Ajax.php
@@ -17,6 +17,38 @@
return array( '_default' => 'anonymous' );
}
+ public static function getFormId()
+ {
+ $status = array( 'booking' => 'new' );
+ $form_processed = false;
+ if ( self::hasParameter( 'form_id' ) ) {
+ $form_id = self::parameter( 'form_id' );
+ $data = LibFormSession::loadSession( $form_id );
+ if ( $data && isset( $data['payment'] ) && ! isset ( $data['payment']['processed'] ) ) {
+ switch ( $data['payment']['status'] ) {
+ case LibBaseGateway::STATUS_COMPLETED:
+ case LibBaseGateway::STATUS_PROCESSING:
+ $status = array( 'booking' => 'finished' );
+ break;
+ case LibBaseGateway::STATUS_FAILED:
+ end( $data['cart'] );
+ $status = array( 'booking' => 'cancelled', 'cart_key' => key( $data['cart'] ) );
+ break;
+ }
+ // Mark this form as processed for cases when there are more than 1 booking form on the page.
+ $data['payment']['processed'] = true;
+ LibFormSession::saveSession( $form_id, $data );
+ $form_processed = true;
+ }
+ }
+ if ( ! $form_processed ) {
+ $form_id = LibFormSession::createSession();
+ LibFormSession::saveSession( $form_id, self::parameter( 'form_data' ) );
+ }
+
+ wp_send_json( array( 'success' => true, 'form_id' => $form_id, 'status' => $status ) );
+ }
+
/**
* 1. Step service.
* response JSON
@@ -1221,13 +1253,14 @@
}
}
$form_id = self::parameter( 'form_id' );
- $stepper_add_step = ! LibSession::getFormVar( $form_id, 'skip_service_step', 0 )
- && ( LibSession::getFormVar( $form_id, 'hide_service_part1', 0 ) + LibSession::getFormVar( $form_id, 'hide_service_part2', 0 ) ) === 0;
+ $session = LibFormSession::loadSession( $form_id );
+ $stepper_add_step = ( ! isset( $session['skip_service_step'] ) || ! $session['skip_service_step'] )
+ && ( $session['hide_service_part1'] + $session['hide_service_part2'] === 0 );
$result = self::renderTemplate( '_progress_tracker', array(
'step' => $step,
'skip_steps' => array(
- 'service' => LibSession::hasFormVar( $form_id, 'skip_service_step' ),
+ 'service' => isset( $session['skip_service_step'] ) && $session['skip_service_step'],
'extras' => ! ( LibConfig::serviceExtrasActive() && get_option( 'bookly_service_extras_enabled' ) ),
'time' => $skip_time_step,
'repeat' => $skip_time_step || ! LibConfig::recurringAppointmentsActive() || ! get_option( 'bookly_recurring_appointments_enabled' ) || LibConfig::showSingleTimeSlot(),
@@ -1261,8 +1294,9 @@
*/
private static function _setDataForSkippedServiceStep( LibUserBookingData $userData )
{
+ $session_data = LibFormSession::loadSession( self::parameter( 'form_id' ) );
// Staff ids.
- $defaults = LibSession::getFormVar( self::parameter( 'form_id' ), 'defaults' );
+ $defaults = $session_data['defaults'];
if ( $defaults !== null ) {
$service_id = $defaults['service_id'];
$service = LibEntitiesService::find( $defaults['service_id'] );
--- a/bookly-responsive-appointment-booking-tool/frontend/modules/booking/ShortCode.php
+++ b/bookly-responsive-appointment-booking-tool/frontend/modules/booking/ShortCode.php
@@ -94,36 +94,6 @@
// Disable caching.
LibUtilsCommon::noCache();
- // Generate unique form id.
- $form_id = uniqid();
-
- // Find bookings with any of payment statuses ( PayPal, 2Checkout, PayU Latam ).
- $status = array( 'booking' => 'new' );
- foreach ( LibSession::getAllFormsData() as $saved_form_id => $data ) {
- if ( isset ( $data['payment'] ) ) {
- if ( ! isset ( $data['payment']['processed'] ) ) {
- switch ( $data['payment']['status'] ) {
- case LibBaseGateway::STATUS_COMPLETED:
- case LibBaseGateway::STATUS_PROCESSING:
- $form_id = $saved_form_id;
- $status = array( 'booking' => 'finished' );
- break;
- case LibBaseGateway::STATUS_FAILED:
- $form_id = $saved_form_id;
- end( $data['cart'] );
- $status = array( 'booking' => 'cancelled', 'cart_key' => key( $data['cart'] ) );
- break;
- }
- // Mark this form as processed for cases when there are more than 1 booking form on the page.
- $data['payment']['processed'] = true;
- LibSession::setFormVar( $saved_form_id, 'payment', $data['payment'] );
- }
- } elseif ( isset( $data['last_touched'] ) && $data['last_touched'] + 30 * MINUTE_IN_SECONDS < time() ) {
- // Destroy forms older than 30 min.
- LibSession::destroyFormData( $saved_form_id );
- }
- }
-
// Check if predefined short code is rendering
if ( isset( $attributes['id'] ) ) {
$attributes = apply_filters( 'bookly_form_attributed', $attributes );
@@ -188,17 +158,6 @@
return esc_html( 'The preselected service for shortcode is not available anymore. Please check your shortcode settings.' );
}
- if ( $hide_service_part1 && $hide_service_part2 ) {
- LibSession::setFormVar( $form_id, 'skip_service_step', true );
- } else {
- LibSession::setFormVar( $form_id, 'hide_service_part1', $hide_service_part1 );
- LibSession::setFormVar( $form_id, 'hide_service_part2', $hide_service_part2 );
- }
-
- // Store parameters in session for later use.
- LibSession::setFormVar( $form_id, 'defaults', compact( 'service_id', 'staff_id', 'location_id', 'category_id', 'units', 'date_from', 'time_from', 'time_to' ) );
- LibSession::setFormVar( $form_id, 'last_touched', time() );
-
// Errors.
$errors = array(
Errors::SESSION_ERROR => __( 'Session error.', 'bookly' ),
@@ -209,11 +168,19 @@
Errors::PAYMENT_ERROR => __( 'Error', 'bookly' ) . '.',
Errors::INCORRECT_USERNAME_PASSWORD => __( 'Incorrect username or password.' ),
);
-
+ $form_container_id = uniqid();
+ $form_token = isset( $_GET['bookly-form-id'] ) ? $_GET['bookly-form-id'] : null;
// Set parameters for bookly form.
$bookly_options = array(
- 'form_id' => $form_id,
- 'status' => $status,
+ 'form_id' => $form_container_id,
+ 'form_data' => array(
+ 'skip_service_step' => (int) ( $hide_service_part1 && $hide_service_part2 ),
+ 'hide_service_part1' => (int) $hide_service_part1,
+ 'hide_service_part2' => (int) $hide_service_part2,
+ 'defaults' => compact( 'service_id', 'staff_id', 'location_id', 'category_id', 'units', 'date_from', 'time_from', 'time_to' ),
+ ),
+ 'status' => array( 'booking' => 'new' ),
+ 'form_token' => $form_token,
'skip_steps' => array(
/**
* [extras,time,repeat]
@@ -237,7 +204,7 @@
return self::renderTemplate(
'short_code',
- compact( 'form_id', 'bookly_options' ),
+ compact( 'form_token', 'form_container_id', 'bookly_options' ),
false
);
}
--- a/bookly-responsive-appointment-booking-tool/frontend/modules/booking/templates/short_code.php
+++ b/bookly-responsive-appointment-booking-tool/frontend/modules/booking/templates/short_code.php
@@ -9,7 +9,7 @@
-->
<?php include '_css.php' ?>
<div class="bookly-css-root">
- <div id="bookly-form-<?php echo esc_attr( $form_id ) ?>" class="bookly-form" data-form_id="<?php echo esc_attr( $form_id ) ?>" aria-live="polite">
+ <div id="bookly-form-container-<?php echo esc_attr( $form_container_id ) ?>" class="bookly-form" data-form_id="<?php echo esc_attr( $form_token ) ?>" aria-live="polite">
<div style="text-align: center"><img src="<?php echo includes_url( 'js/tinymce/skins/lightgray/img/loader.gif' ) ?>" alt="<?php esc_attr_e( 'Loading...', 'bookly' ) ?>"/></div>
</div>
</div>
--- a/bookly-responsive-appointment-booking-tool/frontend/modules/mobile_staff_cabinet/Ajax.php
+++ b/bookly-responsive-appointment-booking-tool/frontend/modules/mobile_staff_cabinet/Ajax.php
@@ -2,6 +2,7 @@
namespace BooklyFrontendModulesMobileStaffCabinet;
use BooklyLib;
+use BooklyFrontendModulesMobileStaffCabinetApiExceptions;
class Ajax extends LibBaseAjax
{
@@ -13,16 +14,18 @@
return array( '_default' => 'anonymous' );
}
- /**
- * Get resources
- */
public static function mobileStaffCabinet()
{
try {
$auth = LibEntitiesAuth::query()->where( 'token', self::parameter( 'access_key' ) )->findOne();
+
+ $staff_or_wp_user = self::findUserByAuth( $auth );
+
$request = new LibBaseRequest();
- $handler = ApiHandlerFactory::create( $auth, $request );
- $response = $handler->process();
+ $handler = ApiHandlerFactory::createLocator()
+ ->getHandler( $staff_or_wp_user, $request );
+
+ $response = $handler( $request );
get_option( LibUtilsLog::OPTION_MOBILE_STAFF_CABINET ) && self::logDebug( $handler, $request, $response );
} catch ( Error $e ) {
@@ -56,22 +59,22 @@
}
/**
- * @param ApiApiHandler $handler
+ * @param ApiHandlersHandlerInterface $handler
* @param LibBaseRequest $request
- * @param ApiIResponse $response
+ * @param ApiResponse $response
* @return void
*/
- protected static function logDebug( ApiApiHandler $handler, LibBaseRequest $request, ApiIResponse $response )
+ protected static function logDebug( ApiHandlersHandlerInterface $handler, LibBaseRequest $request, ApiResponse $response )
{
try {
$class = get_class( $handler );
- LibUtilsLog::tempPut( LibUtilsLog::OPTION_MOBILE_STAFF_CABINET, $class . '::' . $handler->getProcessMethod(), null, '<pre>' . json_encode( array(
+ LibUtilsLog::tempPut( LibUtilsLog::OPTION_MOBILE_STAFF_CABINET, $class . '::' . $handler->getResolverMethodName(), null, '<pre>' . json_encode( array(
'API' => $request->getHeaders()->getGreedy( 'X-Bookly-Api-Version' ),
'role' => $handler->getRole(),
- 'request' => $request->getAll(),
+ 'request.body' => $request->getAll(),
'request.headers' => $request->getHeaders()->getAll(),
- 'response' => $response->getData(),
+ 'response.body' => $response->getData(),
), 128 ) . '</pre>' );
} catch ( Exception $e ) {
}
@@ -83,7 +86,7 @@
*/
protected static function logException( $e )
{
- if ( $e instanceof ApiExceptionsHandleException ) {
+ if ( $e instanceof ExceptionsHandleException ) {
try {
LibUtilsLog::put( LibUtilsLog::ACTION_ERROR,
$e->getClassName() ?: 'Mobile Staff Cabinet API',
@@ -99,11 +102,11 @@
/**
* @param Throwable $throwable
- * @return ApiIResponse
+ * @return ApiResponse
*/
protected static function getThrowableResponse( $throwable )
{
- $response = new ApiResponse();
+ $response = new ApiResponse( null );
$response->setHttpStatus( 400 );
$data = array(
@@ -112,14 +115,14 @@
'message' => $throwable->getMessage(),
),
);
- if ( $throwable instanceof ApiExceptionsApiException ) {
+ if ( $throwable instanceof ExceptionsApiException ) {
$response->setHttpStatus( $throwable->getHttpStatus() );
if ( $throwable->getErrorData() ) {
$data['error']['data'] = $throwable->getErrorData();
}
- } elseif ( $throwable instanceof ApiExceptionsParameterException ) {
+ } elseif ( $throwable instanceof ExceptionsParameterException ) {
$data['error']['data'] = $throwable->getParameter();
- } elseif ( ( $throwable instanceof ApiExceptionsBooklyException ) || ( $throwable instanceof ApiExceptionsHandleException ) ) {
+ } elseif ( ( $throwable instanceof ExceptionsBooklyException ) || ( $throwable instanceof ExceptionsHandleException ) ) {
$data['error']['message'] = $throwable->getMessage();
} else {
$data['error']['message'] = 'ERROR';
@@ -128,4 +131,35 @@
return $response;
}
+
+ /**
+ * @param LibEntitiesAuth|null $auth
+ * @return WP_User|LibEntitiesStaff
+ */
+ protected static function findUserByAuth( $auth )
+ {
+ if ( $auth === null ) {
+ throw new ExceptionsApiException( 'Unauthorized', 401 );
+ }
+
+ // Check staff access
+ if ( $auth->getStaffId() ) {
+ $staff = LibEntitiesStaff::find( $auth->getStaffId() );
+ if ( $staff ) {
+ return $staff;
+ }
+ } // Check admin/supervisor access
+ elseif ( $auth->getWpUserId() ) {
+ $wp_user = get_user_by( 'id', $auth->getWpUserId() );
+ $user_id = $auth->getWpUserId();
+
+ if ( user_can( $user_id, 'manage_bookly' ) ||
+ user_can( $user_id, 'manage_options' ) ||
+ user_can( $user_id, 'manage_bookly_appointments' ) ) {
+ return $wp_user;
+ }
+ }
+
+ throw new ExceptionsApiException( 'Unauthorized', 401 );
+ }
}
No newline at end of file
--- a/bookly-responsive-appointment-booking-tool/frontend/modules/mobile_staff_cabinet/api/ApiHandler.php
+++ b/bookly-responsive-appointment-booking-tool/frontend/modules/mobile_staff_cabinet/api/ApiHandler.php
@@ -1,113 +0,0 @@
-<?php
-namespace BooklyFrontendModulesMobileStaffCabinetApi;
-
-use BooklyLib;
-
-/**
- * Abstract class for API
- */
-abstract class ApiHandler
-{
- const ROLE_SUPERVISOR = 'supervisor';
- const ROLE_STAFF = 'staff';
-
- /** @var LibEntitiesStaff */
- protected $staff;
- /** @var WP_User */
- protected $wp_user;
- /** @var array */
- protected $result = array();
-
- /** @var string */
- protected $role;
- /** @var LibBaseRequest */
- protected $request;
- /** @var LibUtilsCollection */
- protected $params;
- /** @var callable */
- protected $processMethod;
- /** @var IResponse */
- protected $response;
-
- /**
- * Sets resource method to execute
- *
- * @param string $method
- * @return void
- */
- public function setProcessMethod( $method )
- {
- $this->processMethod = $method;
- }
-
- /**
- * @return string|null
- */
- public function getProcessMethod()
- {
- return $this->processMethod;
- }
-
- /**
- * @return string|null
- */
- public function getRole()
- {
- return $this->role;
- }
-
- /**
- * Handles the API request by executing the appropriate resource method
- *
- * @return IResponse
- */
- public function process()
- {
- if ( method_exists( $this, $this->processMethod ) ) {
- $this->{$this->processMethod}();
- }
-
- $data = $this->getResponseData();
-
- $this->response
- ->setData( $data )
- ->addHeader( 'X-Bookly-V', LibPlugin::getVersion() );
-
- return $this->response;
- }
-
- /**
- * Sets request parameters
- *
- * @param mixed $params
- * @return void
- */
- protected function setParams( $params )
- {
- $this->params = new LibUtilsCollection( is_array( $params ) ? $params : array() );
- }
-
- /**
- * Gets parameter value by name
- *
- * @param string $name
- * @param mixed $default
- * @return mixed
- */
- protected function param( $name, $default = null )
- {
- return $this->params->has( $name )
- ? stripslashes_deep( $this->params->get( $name ) )
- : $default;
- }
-
- /**
- * Gets response data
- *
- * @return array
- */
- protected function getResponseData()
- {
- return array( 'result' => $this->result );
- }
-}
--- a/bookly-responsive-appointment-booking-tool/frontend/modules/mobile_staff_cabinet/api/HandlerFactory.php
+++ b/bookly-responsive-appointment-booking-tool/frontend/modules/mobile_staff_cabinet/api/HandlerFactory.php
@@ -1,183 +1,18 @@
<?php
namespace BooklyFrontendModulesMobileStaffCabinetApi;
-use BooklyLib;
-
class HandlerFactory
{
/**
- * Creates appropriate handler object based on API version
- *
- * @param LibEntitiesAuth|null $auth Authentication entity
- * @param LibBaseRequest $request Request object
- * @return ApiHandler
+ * @return HandlerLocator
*/
- public static function create( $auth, LibBaseRequest $request )
+ public static function createLocator()
{
- list( $role, $staff_or_wp_user ) = self::determineUserRole( $auth );
-
- if ( $staff_or_wp_user === null ) {
- throw new ExceptionsApiException( 'Unauthorized', 401, array(), $request );
- }
-
- // Create handler based on protocol version and requested method
- $handler = self::findCompatibleHandler( $request, $role );
-
- // Set user data based on role
- if ( $role === ApiHandler::ROLE_SUPERVISOR && method_exists( $handler, 'setWpUser' ) ) {
- $handler->setWpUser( $staff_or_wp_user );
- } elseif ( $role === ApiHandler::ROLE_STAFF && method_exists( $handler, 'setStaff' ) ) {
- $handler->setStaff( $staff_or_wp_user );
- }
-
- return $handler;
- }
-
- /**
- * Determines user role based on authentication data
- *
- * @param LibEntitiesAuth|null $auth Authentication entity
- * @return array
- */
- private static function determineUserRole( $auth )
- {
- $role = null;
-
- if ( $auth === null ) {
- return array( $role, null );
- }
-
- // Check staff access
- if ( $auth->getStaffId() ) {
- $staff = LibEntitiesStaff::find( $auth->getStaffId() );
- if ( $staff ) {
- return array( ApiHandler::ROLE_STAFF, $staff );
- }
- } // Check admin/supervisor access
- elseif ( $auth->getWpUserId() ) {
- $wp_user = get_user_by( 'id', $auth->getWpUserId() );
- $user_id = $auth->getWpUserId();
-
- if ( user_can( $user_id, 'manage_bookly' ) ||
- user_can( $user_id, 'manage_options' ) ||
- user_can( $user_id, 'manage_bookly_appointments' ) ) {
- return array( ApiHandler::ROLE_SUPERVISOR, $wp_user );
- }
- }
-
- return array( $role, null );
- }
-
- /**
- * Creates handler object for specific API version with method support check
- *
- * @param LibBaseRequest $request
- * @param string|null $role User role
- * @return ApiHandler
- */
- private static function findCompatibleHandler( LibBaseRequest $request, $role )
- {
- // Get API version from headers (default: 1.0)
- $version = $request->getHeaders()->getGreedy( 'X-Bookly-Api-Version', '1.0' );
-
- $compatible_classes = self::findCompatibleHandlerClasses( $version );
-
- if ( empty( $compatible_classes ) ) {
- throw new ExceptionsHandleException( 'UNKNOWN_REQUEST', $request, null, 'No compatible response classes found' );
- }
-
- $method = self::buildMethodNameFromRequest( $request );
-
- foreach ( $compatible_classes as $class_name ) {
- if ( method_exists( $class_name, $method ) ) {
- try {
- /** @var ApiHandler $handler */
- $handler = new $class_name( $role, $request, new Response() );
- $handler->setProcessMethod( $method );
-
- return $handler;
- } catch ( Error $e ) {
- throw new ExceptionsHandleException( 'UNKNOWN_REQUEST', $request, $class_name, 'Method ' . $method . ' has error ' . $e->getMessage() );
- } catch ( Exception $e ) {
- throw new ExceptionsHandleException( 'UNKNOWN_REQUEST', $request, $class_name, 'Method ' . $method . ' has exception ' . $e->getMessage() );
- }
- }
- }
-
- throw new ExceptionsHandleException( 'UNKNOWN_REQUEST', $request, null, 'Method ' . $method . ' — not found' );
- }
-
- /**
- * Finds all handler classes compatible with requested version
- *
- * @param string $version Requested API version
- * @return string[] Array of compatible handler class names
- */
- private static function findCompatibleHandlerClasses( $version )
- {
- $version = trim( $version );
- $fs = LibUtilsCommon::getFilesystem();
- $base_dir = __DIR__;
- $dirs = $fs->dirlist( $base_dir, false );
- $classes = array();
-
- foreach ( $dirs as $dir ) {
- if ( $dir['type'] === 'd' && preg_match( '/^v(d+_d+)$/', $dir['name'], $matches ) ) {
- $folder_version = str_replace( '_', '.', $matches[1] );
- if ( version_compare( $folder_version, $version, '>' ) ) {
- continue;
- }
- $handler_path = $base_dir . '/' . $dir['name'] . '/Handler.php';
- if ( file_exists( $handler_path ) ) {
- $class_name = __NAMESPACE__ . '\' . strtoupper( $dir['name'] ) . '\Handler';
- if ( class_exists( $class_name ) ) {
- $classes[ $folder_version ] = $class_name;
- }
- }
- }
- }
-
- if ( count( $classes ) > 1 ) {
- uksort( $classes, function( $a, $b ) {
- return version_compare( $b, $a );
- } );
- }
-
- return $classes;
- }
-
- /**
- * Builds method name from request parameters
- * Converts kebab-case resource names to camelCase method names
- * Example: 'resource-name' with action 'save' => 'saveResourceName'
- *
- * @param LibBaseRequest $request
- * @return string|null
- */
- private static function buildMethodNameFromRequest( LibBaseRequest $request )
- {
- $resource = $request->get( 'resource' );
- $action = $request->get( 'action' );
-
- if ( empty( $resource ) ) {
- return null;
- }
-
- // Convert kebab-case to camelCase
- $parts = explode( '-', $resource );
- $method = $parts[0];
-
- // Convert remaining parts to PascalCase and append
- $parts_count = count( $parts );
- for ( $i = 1; $i < $parts_count; $i++ ) {
- $method .= ucfirst( $parts[ $i ] );
- }
+ $locator = new HandlerLocator();
- // Append action if present
- if ( ! empty( $action ) ) {
- $method = $action . ucfirst( $method );
- }
+ $locator->register('BooklyFrontendModulesMobileStaffCabinetApiHandlersHandler1_0');
+ $locator->register('BooklyFrontendModulesMobileStaffCabinetApiHandlersHandler1_1');
- return $method;
+ return $locator;
}
-}
+}
No newline at end of file
--- a/bookly-responsive-appointment-booking-tool/frontend/modules/mobile_staff_cabinet/api/HandlerLocator.php
+++ b/bookly-responsive-appointment-booking-tool/frontend/modules/mobile_staff_cabinet/api/HandlerLocator.php
@@ -0,0 +1,95 @@
+<?php
+namespace BooklyFrontendModulesMobileStaffCabinetApi;
+
+use BooklyLib;
+
+final class HandlerLocator
+{
+ /** @var array<string, HandlersHandlerInterface> */
+ private $handlers = array();
+
+ /**
+ * @param string $class_name
+ */
+ public function register( $class_name )
+ {
+ /** @var HandlersHandlerInterface $class_name */
+ $this->handlers[ $class_name::getVersion() ] = $class_name;
+ }
+
+ /**
+ * @param WP_User | LibEntitiesStaff $staff_or_wp_user
+ * @param LibBaseRequest $request
+ *
+ * @return HandlersHandlerInterface
+ * @throws ExceptionsHandleException
+ */
+ public function getHandler( $staff_or_wp_user, LibBaseRequest $request )
+ {
+ $version = $request->getHeaders()->getGreedy( 'X-Bookly-Api-Version', '1.0' );
+
+ if ( ! array_key_exists( $version, $this->handlers ) ) {
+ throw new ExceptionsHandleException( 'UNKNOWN_REQUEST', $request, null, 'API ' . $version . ' — not found' );
+ }
+ $compatible_classes = $this->findCompatibleHandlerClasses( $version );
+ $method = $this->buildMethodNameFromRequest( $request );
+
+ foreach ( $compatible_classes as $class_name ) {
+ if ( method_exists( $class_name, $method ) ) {
+ try {
+ /** @var HandlersHandlerInterface $class_name */
+ return new $class_name( $staff_or_wp_user, $method );
+ } catch ( Error $e ) {
+ throw new ExceptionsHandleException( 'UNKNOWN_REQUEST', $request, $class_name, 'Method ' . $method . ' has error ' . $e->getMessage() );
+ } catch ( Exception $e ) {
+ throw new ExceptionsHandleException( 'UNKNOWN_REQUEST', $request, $class_name, 'Method ' . $method . ' has exception ' . $e->getMessage() );
+ }
+ }
+ }
+ throw new ExceptionsHandleException( 'UNKNOWN_REQUEST', $request, null, 'Method ' . $method . ' — not found' );
+ }
+
+ private function findCompatibleHandlerClasses( $version )
+ {
+ $compatible_handlers = array();
+ foreach ( $this->handlers as $handler_version => $handler ) {
+ if ( version_compare( $handler_version, $version, '<=' ) ) {
+ $compatible_handlers[ $handler_version ] = $handler;
+ }
+ }
+
+ // Sort handlers by version descending
+ uksort( $compatible_handlers, static function ( $a, $b ) {
+ return version_compare( $b, $a );
+ } );
+
+ return $compatible_handlers;
+ }
+
+ private function buildMethodNameFromRequest( LibBaseRequest $request )
+ {
+ $resource = $request->get( 'resource' );
+ $action = $request->get( 'action' );
+
+ if ( empty( $resource ) ) {
+ throw new ExceptionsHandleException( 'UNKNOWN_REQUEST', $request, null, 'Resouce — is empty' );
+ }
+
+ // Convert kebab-case to camelCase
+ $parts = explode( '-', $resource );
+ $method = $parts[0];
+
+ // Convert remaining parts to PascalCase and append
+ $parts_count = count( $parts );
+ for ( $i = 1; $i < $parts_count; $i++ ) {
+ $method .= ucfirst( $parts[ $i ] );
+ }
+
+ // Append action if present
+ if ( ! empty( $action ) ) {
+ $method = $action . ucfirst( $method );
+ }
+
+ return $method;
+ }
+}
No newline at end of file
--- a/bookly-responsive-appointment-booking-tool/frontend/modules/mobile_staff_cabinet/api/IResponse.php
+++ b/bookly-responsive-appointment-booking-tool/frontend/modules/mobile_staff_cabinet/api/IResponse.php
@@ -1,30 +0,0 @@
-<?php
-namespace BooklyFrontendModulesMobileStaffCabinetApi;
-
-interface IResponse
-{
- /**
- * @return $this
- */
- public function setData( $data );
-
- /**
- * @return mixed
- */
- public function getData();
-
- /**
- * @return $this
- */
- public function setHttpStatus( $http_status );
-
- /**
- * @return $this
- */
- public function addHeader( $name, $value );
-
- /**
- * @return void
- */
- public function render();
-}
--- a/bookly-responsive-appointment-booking-tool/frontend/modules/mobile_staff_cabinet/api/Response.php
+++ b/bookly-responsive-appointment-booking-tool/frontend/modules/mobile_staff_cabinet/api/Response.php
@@ -1,13 +1,18 @@
<?php
namespace BooklyFrontendModulesMobileStaffCabinetApi;
-class Response implements IResponse
+class Response
{
protected $http_status = 200;
protected $data = '';
protected $headers = array();
protected $contentType = 'application/json';
+ public function __construct( $data )
+ {
+ $this->setData( $data );
+ }
+
public function render()
{
$body = $this->getBody();
--- a/bookly-responsive-appointment-booking-tool/frontend/modules/mobile_staff_cabinet/api/exceptions/ParameterException.php
+++ b/bookly-responsive-appointment-booking-tool/frontend/modules/mobile_staff_cabinet/api/exceptions/ParameterException.php
@@ -12,7 +12,7 @@
{
$this->parameter = $parameter;
$this->value = $value;
- parent::__construct( '', $code );
+ parent::__construct( 'INVALID_PARAMETER', $code );
}
/**
--- a/bookly-responsive-appointment-booking-tool/frontend/modules/mobile_staff_cabinet/api/handlers/Handler.php
+++ b/bookly-responsive-appointment-booking-tool/frontend/modules/mobile_staff_cabinet/api/handlers/Handler.php
@@ -0,0 +1,119 @@
+<?php
+namespace BooklyFrontendModulesMobileStaffCabinetApiHandlers;
+
+use BooklyLib;
+use BooklyLibEntitiesStaff;
+use BooklyFrontendModulesMobileStaffCabinetApi;
+
+abstract class Handler implements HandlerInterface
+{
+ const ROLE_SUPERVISOR = 'supervisor';
+ const ROLE_STAFF = 'staff';
+
+ /** @var string */
+ protected $role;
+ /** @var Staff */
+ protected $staff;
+ /** @var WP_User */
+ protected $wp_user;
+ /** @var LibUtilsCollection */
+ protected $params;
+ /** @var string */
+ protected $resolver_method_name = '';
+ /** @var LibBaseRequest */
+ protected $request;
+
+ /**
+ * @param WP_User | Staff $staff_or_wp_user
+ * @param string $resolver
+ */
+ public function __construct( $staff_or_wp_user, $resolver )
+ {
+ if ( $staff_or_wp_user instanceof WP_User ) {
+ $this->role = self::ROLE_SUPERVISOR;
+ $this->wp_user = $staff_or_wp_user;
+ LibUtilsLog::setAuthor( $staff_or_wp_user->display_name );
+ } elseif ( $staff_or_wp_user instanceof Staff ) {
+ $this->role = self::ROLE_STAFF;
+ $this->staff = $staff_or_wp_user;
+ $this->staff && LibUtilsLog::setAuthor( $staff_or_wp_user->getFullName() );
+ }
+
+ $this->resolver_method_name = $resolver;
+ }
+
+ /**
+ * @param LibBaseRequest $request
+ * @return ApiResponse
+ * @throws ApiExceptionsParameterException
+ */
+ public function __invoke( LibBaseRequest $request )
+ {
+ $this->request = $request;
+ $this->params = new LibUtilsCollection( $request->get( 'params', array() ) );
+
+ $data = $this->{$this->resolver_method_name}();
+ $response = new ApiResponse( array( 'result' => $data ) );
+
+ return $response->addHeader( 'X-Bookly-V', LibPlugin::getVersion() );
+ }
+
+ /**
+ * @return string
+ */
+ public function getResolverMethodName()
+ {
+ return $this->resolver_method_name;
+ }
+
+ /**
+ * @return string
+ */
+ public function getRole()
+ {
+ return $this->role;
+ }
+
+ /**
+ * @param string $name
+ * @param $default
+ * @return mixed|null
+ */
+ protected function param