Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/latepoint/latepoint.php
+++ b/latepoint/latepoint.php
@@ -2,7 +2,7 @@
/**
* Plugin Name: LatePoint
* Description: Appointment Scheduling Software for WordPress
- * Version: 5.5.1
+ * Version: 5.5.2
* Author: LatePoint
* Author URI: https://latepoint.com
* Plugin URI: https://latepoint.com
@@ -29,7 +29,7 @@
* LatePoint version.
*
*/
- public $version = '5.5.1';
+ public $version = '5.5.2';
public $db_version = '2.3.0';
@@ -905,7 +905,8 @@
include_once LATEPOINT_ABSPATH . 'lib/mailers/customer_mailer.php';
// ABILITIES (WordPress 6.9+ Abilities API)
- if ( apply_filters( 'latepoint_enable_abilities', false ) && function_exists( 'wp_register_ability' ) ) {
+ $abilities_enabled = OsSettingsHelper::is_on( 'latepoint_abilities_api' );
+ if ( $abilities_enabled && function_exists( 'wp_register_ability' ) ) {
include_once LATEPOINT_ABSPATH . 'lib/abilities/class-latepoint-abilities.php';
}
@@ -1158,7 +1159,7 @@
if ( $customer->wordpress_user_id ) {
// has connected wp user
$wp_user = get_user_by( 'id', $customer->wordpress_user_id );
- if ( $wp_user && ! is_super_admin( $wp_user->ID ) ) {
+ if ( $wp_user && OsCustomerHelper::is_wp_user_safe_for_customer_link( $wp_user->ID ) ) {
// Only sync to the linked WP user when request comes from the account owner or an admin.
// This prevents unauthenticated guest booking requests from overwriting WP user data.
$current_user_id = get_current_user_id();
--- a/latepoint/lib/abilities/abstract-ability.php
+++ b/latepoint/lib/abilities/abstract-ability.php
@@ -44,7 +44,28 @@
return $this->id;
}
+ public function is_read_only(): bool {
+ return $this->read_only;
+ }
+
+ public function is_destructive(): bool {
+ return $this->destructive;
+ }
+
public function check_permission(): bool {
+ // Master gate. In case the ability was registered
+ // while the master toggle was on but has since been disabled.
+ if ( ! OsSettingsHelper::is_on( 'latepoint_abilities_api' ) ) {
+ return false;
+ }
+ if ( $this->destructive && ! OsSettingsHelper::is_on( 'latepoint_abilities_api_delete' ) ) {
+ return false;
+ }
+ if ( ! $this->read_only && ! $this->destructive
+ && ! OsSettingsHelper::is_on( 'latepoint_abilities_api_edit' ) ) {
+ return false;
+ }
+
return OsRolesHelper::can_user( $this->permission );
}
--- a/latepoint/lib/abilities/class-latepoint-abilities.php
+++ b/latepoint/lib/abilities/class-latepoint-abilities.php
@@ -69,10 +69,23 @@
/**
* Register all abilities from every module.
+ *
+ * Read-only abilities register whenever the master toggle is on.
+ * Mutating abilities require the write toggle; destructive ones
+ * require the delete toggle.
*/
public static function register_all(): void {
+ $allow_write = OsSettingsHelper::is_on( 'latepoint_abilities_api_edit' );
+ $allow_delete = OsSettingsHelper::is_on( 'latepoint_abilities_api_delete' );
+
foreach ( self::$config_modules as $class ) {
foreach ( $class::get_abilities() as $ability ) {
+ if ( $ability->is_destructive() && ! $allow_delete ) {
+ continue;
+ }
+ if ( ! $ability->is_read_only() && ! $ability->is_destructive() && ! $allow_write ) {
+ continue;
+ }
wp_register_ability( $ability->get_id(), $ability->to_definition() );
}
}
--- a/latepoint/lib/helpers/customer_helper.php
+++ b/latepoint/lib/helpers/customer_helper.php
@@ -235,6 +235,22 @@
return $customer->where( [ 'account_nonse' => $account_nonse ] )->set_limit( 1 )->get_results_as_models();
}
+ /**
+ * Check whether a WP user has only customer-safe roles (subscriber, customer, latepoint_customer).
+ * Used to prevent linking a LatePoint customer record to a privileged WordPress account.
+ *
+ * @param int $wp_user_id WordPress user ID to check.
+ * @return bool True if the user exists and has only allowlisted roles; false otherwise.
+ */
+ public static function is_wp_user_safe_for_customer_link( int $wp_user_id ): bool {
+ $wp_user = get_userdata( $wp_user_id );
+ if ( ! $wp_user || empty( $wp_user->roles ) ) {
+ return false;
+ }
+ $allowed_roles = array( LATEPOINT_WP_CUSTOMER_ROLE, 'subscriber', 'customer' );
+ return empty( array_diff( (array) $wp_user->roles, $allowed_roles ) );
+ }
+
public static function create_wp_user_for_customer( $customer ) {
// NO connected wp user, create one
// check if wp user with this customer email already exists
@@ -243,19 +259,25 @@
$wp_user_id = username_exists( $customer->email );
}
if ( $wp_user_id ) {
- // wp user with this email or username exists - check if its linked to another customer already - if not link it to current customer
- $linked_customer = new OsCustomerModel();
- $linked_customer = $linked_customer->where( [ 'wordpress_user_id' => $wp_user_id ] )->set_limit( 1 )->get_results_as_models();
- if ( $linked_customer ) {
- // wp user with this email exists and is linked already to a different latepoint customer
- $customer->add_error( 'customer_exists', __( 'Customer with this email already exists', 'latepoint' ) );
+ // wp user with this email or username exists
+ if ( ! self::is_wp_user_safe_for_customer_link( $wp_user_id ) ) {
+ // Do not link to privileged WP accounts (admin, editor, etc.)
+ $customer->add_error( 'privileged_user', __( 'Cannot link to a privileged WordPress account.', 'latepoint' ) );
} else {
- $customer->update_attributes(
- [
- 'wordpress_user_id' => $wp_user_id,
- 'is_guest' => false,
- ]
- );
+ // check if its linked to another customer already - if not link it to current customer
+ $linked_customer = new OsCustomerModel();
+ $linked_customer = $linked_customer->where( [ 'wordpress_user_id' => $wp_user_id ] )->set_limit( 1 )->get_results_as_models();
+ if ( $linked_customer ) {
+ // wp user with this email exists and is linked already to a different latepoint customer
+ $customer->add_error( 'customer_exists', __( 'Customer with this email already exists', 'latepoint' ) );
+ } else {
+ $customer->update_attributes(
+ [
+ 'wordpress_user_id' => $wp_user_id,
+ 'is_guest' => false,
+ ]
+ );
+ }
}
} else {
--- a/latepoint/lib/misc/process_action.php
+++ b/latepoint/lib/misc/process_action.php
@@ -46,6 +46,28 @@
return ! empty( $this->settings['attach_calendar'] ) && OsUtilHelper::is_on( $this->settings['attach_calendar'] );
}
+ public function get_ics_filename_prefix(): string {
+ return isset( $this->settings['ics_filename_prefix'] )
+ ? trim( (string) $this->settings['ics_filename_prefix'] )
+ : '';
+ }
+
+ private function resolve_ics_filename_basename(): string {
+ $raw = $this->get_ics_filename_prefix();
+ if ( $raw === '' ) {
+ return '';
+ }
+ $resolved = OsReplacerHelper::replace_all_vars( $raw, $this->replacement_vars );
+ // Strip any leftover {{...}} placeholders that the workflow context didn't supply.
+ $resolved = (string) preg_replace( '/{{[^}]*}}/', '', $resolved );
+ $resolved = sanitize_file_name( trim( $resolved ) );
+ // User typed `.ics` themselves — drop it; we always append our own extension.
+ if ( strtolower( substr( $resolved, -4 ) ) === '.ics' ) {
+ $resolved = substr( $resolved, 0, -4 );
+ }
+ return $resolved;
+ }
+
public function get_nice_type_name() {
return self::get_action_name_for_type( $this->type );
}
@@ -218,9 +240,33 @@
[
'id' => 'process_actions_' . $action->id . '_settings_content',
'class' => 'os-wp-editor-textarea',
- ]
+ ]
+ );
+
+ $attach_calendar_settings_id = 'process_action_' . $action->id . '_attach_calendar_settings';
+
+ $html .= '<div class="os-row pa-attach-calendar-row">';
+ $html .= '<div class="os-col-4">';
+ $html .= OsFormHelper::toggler_field(
+ 'process[actions][' . $action->id . '][settings][attach_calendar]',
+ __( 'Attach Booking Calendar', 'latepoint' ),
+ $action->is_attach_calendar(),
+ $attach_calendar_settings_id
+ );
+ $html .= '</div>';
+ $html .= '<div class="os-col-8" id="' . esc_attr( $attach_calendar_settings_id ) . '"' . ( $action->is_attach_calendar() ? '' : ' style="display:none"' ) . '>';
+ $html .= OsFormHelper::text_field(
+ 'process[actions][' . $action->id . '][settings][ics_filename_prefix]',
+ __( 'Calendar attachment filename', 'latepoint' ),
+ $action->settings['ics_filename_prefix'] ?? '',
+ [
+ 'theme' => 'simple',
+ 'placeholder' => __( 'e.g. MyBusiness_{{service_name}}_{{booking_id}}', 'latepoint' ),
+ 'sub_label' => __( 'Optional. Use the "Show smart variables" button above to insert placeholders. Leave blank to use the default.', 'latepoint' ),
+ ]
);
- $html .= OsFormHelper::toggler_field( 'process[actions][' . $action->id . '][settings][attach_calendar]', __( 'Attach Booking Calendar', 'latepoint' ), $action->is_attach_calendar() );
+ $html .= '</div>';
+ $html .= '</div>';
$html .= OsFormHelper::multiple_files_uploader_field( 'process[actions][' . $action->id . '][settings][attachments]', esc_html__( '+ Attach File', 'latepoint' ), esc_html__( 'Remove File', 'latepoint' ), $action->get_attachments() );
break;
case 'send_sms':
@@ -671,9 +717,17 @@
throw new Exception( 'iCal content is empty' );
}
- $temp_file = tempnam( sys_get_temp_dir(), 'latepoint_ical_' );
- $ical_temp_file = $temp_file . '.ics';
- rename( $temp_file, $ical_temp_file );
+ $resolved_basename = $this->resolve_ics_filename_basename();
+
+ if ( $resolved_basename !== '' ) {
+ $temp_file = tempnam( sys_get_temp_dir(), $resolved_basename . '_' );
+ $ical_temp_file = $temp_file . '.ics';
+ rename( $temp_file, $ical_temp_file );
+ } else {
+ $temp_file = tempnam( sys_get_temp_dir(), 'latepoint_ical_' );
+ $ical_temp_file = $temp_file . '.ics';
+ rename( $temp_file, $ical_temp_file );
+ }
if ( file_put_contents( $ical_temp_file, $ical_content ) !== false ) {
return $ical_temp_file;
--- a/latepoint/lib/models/customer_model.php
+++ b/latepoint/lib/models/customer_model.php
@@ -315,6 +315,16 @@
public function update_password( $password ) {
// update connected wp user password
if ( OsAuthHelper::can_wp_users_login_as_customers() && $this->wordpress_user_id ) {
+ // Only reset the WP password for non-privileged accounts to prevent takeover of admin/editor users
+ if ( ! OsCustomerHelper::is_wp_user_safe_for_customer_link( (int) $this->wordpress_user_id ) ) {
+ return $this->update_attributes(
+ [
+ 'password' => wp_hash_password( $password ),
+ 'is_guest' => false,
+ ]
+ );
+ }
+
$is_logged_in = OsWpUserHelper::get_current_user_id() == $this->wordpress_user_id;
$logged_in_wp_user = $is_logged_in ? OsWpUserHelper::get_current_user() : false;
--- a/latepoint/lib/views/settings/general.php
+++ b/latepoint/lib/views/settings/general.php
@@ -392,6 +392,75 @@
</div>
</div>
</div>
+ <div class="white-box section-anchor" id="stickySectionAbilities">
+ <div class="white-box-header">
+ <div class="os-form-sub-header"><h3><?php esc_html_e( 'MCP', 'latepoint' ); ?></h3></div>
+ </div>
+ <div class="white-box-content no-padding">
+ <?php if ( ! function_exists( 'wp_register_ability' ) ) : ?>
+ <div class="sub-section-row">
+ <div class="sub-section-content">
+ <div class="latepoint-message latepoint-message-subtle">
+ <?php esc_html_e( 'MCP requires WordPress 6.9 or newer. Your changes will be saved but will not take effect until WordPress is updated.', 'latepoint' ); ?>
+ </div>
+ </div>
+ </div>
+ <?php endif; ?>
+ <div class="sub-section-row">
+ <div class="sub-section-label">
+ <h3><?php esc_html_e( 'Enable Abilities', 'latepoint' ); ?></h3>
+ </div>
+ <div class="sub-section-content">
+ <?php
+ echo OsFormHelper::toggler_field(
+ 'settings[latepoint_abilities_api]',
+ __( 'Enable Abilities', 'latepoint' ),
+ OsSettingsHelper::is_on( 'latepoint_abilities_api' ),
+ 'abilitiesPermissionsToggle',
+ false,
+ [ 'sub_label' => __( 'Register LatePoint abilities with the WordPress Abilities API. When enabled, AI clients can list, read, create, edit, and delete your bookings, customers, services, agents, and orders. When disabled, no abilities are registered and AI clients cannot perform any actions on your LatePoint data.', 'latepoint' ) ]
+ );
+ ?>
+ </div>
+ </div>
+ <div id="abilitiesPermissionsToggle" style="border-top: 1px solid rgb(220, 218, 215); <?php echo OsSettingsHelper::is_on( 'latepoint_abilities_api' ) ? '' : 'display: none;'; ?>">
+ <div class="sub-section-row">
+ <div class="sub-section-label">
+ <h3><?php esc_html_e( 'Enable Edit Abilities', 'latepoint' ); ?></h3>
+ </div>
+ <div class="sub-section-content">
+ <?php
+ echo OsFormHelper::toggler_field(
+ 'settings[latepoint_abilities_api_edit]',
+ __( 'Enable Edit Abilities', 'latepoint' ),
+ OsSettingsHelper::is_on( 'latepoint_abilities_api_edit' ),
+ false,
+ false,
+ [ 'sub_label' => __( 'When enabled, AI clients can create new bookings, update customers, services, agents, and locations, and change appointment statuses (approve, cancel, reschedule). When disabled, these abilities are unregistered and AI clients can only read your data.', 'latepoint' ) ]
+ );
+ ?>
+ </div>
+ </div>
+ <div class="sub-section-row">
+ <div class="sub-section-label">
+ <h3><?php esc_html_e( 'Enable Delete Abilities', 'latepoint' ); ?></h3>
+ </div>
+ <div class="sub-section-content">
+ <?php
+ echo OsFormHelper::toggler_field(
+ 'settings[latepoint_abilities_api_delete]',
+ __( 'Enable Delete Abilities', 'latepoint' ),
+ OsSettingsHelper::is_on( 'latepoint_abilities_api_delete' ),
+ false,
+ false,
+ [ 'sub_label' => __( 'When enabled, AI clients can permanently delete bookings, customers, services, agents, and locations, and process refunds. Deleted data cannot be recovered. When disabled, delete abilities are unregistered and AI clients cannot remove any data.', 'latepoint' ) ]
+ );
+ ?>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
<?php
/**
* Plug before "Other" section in general settings
@@ -674,6 +743,7 @@
echo '<div><a href="#'.esc_attr($item['href']).'">'.esc_html( $item['label'] ).'</a></div>';
}
?>
+ <div><a href="#stickySectionAbilities"><?php esc_html_e( 'MCP', 'latepoint' ); ?></a></div>
<div><a href="#stickySectionOther"><?php esc_html_e( 'Other', 'latepoint' ); ?></a></div>
</div>