Published : June 28, 2026

CVE-2026-54826: SupportCandy – Helpdesk & Customer Support Ticket System <= 3.4.6 Authenticated (Subscriber+) Insecure Direct Object Reference PoC, Patch Analysis & Rule

Plugin supportcandy
Severity Medium (CVSS 4.3)
CWE 639
Vulnerable Version 3.4.6
Patched Version 3.4.7
Disclosed June 16, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-54826:
CVE-2026-54826 describes an Insecure Direct Object Reference (IDOR) vulnerability in the SupportCandy – Helpdesk & Customer Support Ticket System plugin for WordPress, affecting all versions up to and including 3.4.6. The vulnerability allows authenticated attackers with Subscriber-level access or higher to perform unauthorized actions by manipulating a user-controlled key without proper validation. The CVSS score is 4.3 (medium severity), reflecting the need for authentication but potential for unauthorized data access or operation.

The root cause lies in missing validation of a user-controlled key across multiple AJAX handlers and internal functions. The code diff reveals extensive changes to authorization checks in files such as class-wpsc-customers.php, class-wpsc-individual-ticket.php, and various custom-field admin pages. For example, in class-wpsc-customers.php, functions like customer_ticket_count_after_ticket_merge, customer_ticket_count_after_change_raised_by, reset_customer_ticket_count, and customer_ticket_count previously called $ticket->customer->update_ticket_count() without verifying that the $ticket->customer property was a valid object or that the current user had permission to access that ticket. Similarly, in class-wpsc-individual-ticket.php, the vulnerability involves ticket-specific actions (view, edit, delete, etc.) where the ticket ID is passed as a parameter and the plugin fails to verify that the ticket belongs to the current user or that the user has appropriate capabilities. The get_live_agents and check_live_agents functions were removed entirely, indicating a restructuring of how agent collision data is handled, likely introducing more strict authorization.

Exploitation requires an authenticated user with Subscriber-level access or higher. The attacker sends crafted AJAX requests to WordPress admin-ajax.php with specific action parameters (e.g., wpsc_it_view_ticket, wpsc_it_delete_ticket, wpsc_it_block_unblock_notifications) and a ticket ID that does not belong to them. By manipulating the ticket_id parameter in POST or GET data, the attacker can access, modify, or delete tickets belonging to other users. The attack vector is entirely in the browser or via HTTP requests, as the plugin uses WordPress AJAX hooks (wp_ajax_* actions) for ticket management. No nonce validation was applied to many of these actions before the patch, allowing unauthorized requests.

The patch, as shown in the diff, adds multiple security layers. Key changes include: (1) In class-wpsc-customers.php, all customer_ticket_count* functions now check if $ticket->customer exists and is an object before calling update_ticket_count(), preventing errors when the ticket or customer is invalid. (2) The new set_block_unblock_notifications function in class-wpsc-individual-ticket.php includes nonce verification (check_ajax_referer), user capability checks (current user must be agent), and ticket existence/restriction checks (self::$is_restricted). (3) In multiple admin files (class-wpsc-aof.php, class-wpsc-cf.php, class-wpsc-tf.php, class-wpsc-tff.php), authorization checks (is_site_admin()) were added before rendering settings pages. (4) The agent-collision widget is now registered only during plugin installation/upgrade, removing the legacy check_live_agents AJAX action that lacked proper ticket ownership validation. The diff also fixes variable handling (e.g., get_option with default array to avoid warnings) and adds structured validation for ticket properties.

Successful exploitation could allow an authenticated Subscriber to view tickets they should not have access to (private customer data), modify or delete tickets, change ticket status, or block/unblock notifications. The impact is primarily confidentiality and integrity violation, as an attacker could read sensitive support conversations or disrupt support operations. The vulnerability does not directly lead to remote code execution or privilege escalation, but combined with other flaws, it could facilitate further attacks. The CVSS score of 4.3 reflects the limited scope but real risk of unauthorized data access.

Differential between vulnerable and patched code

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

Code Diff
--- a/supportcandy/class-wpsc-installation.php
+++ b/supportcandy/class-wpsc-installation.php
@@ -1539,6 +1539,7 @@

 			// ticket widgets.
 			$labels = array(
+				'agent-collision'       => __( 'Currently viewing', 'supportcandy' ),
 				'change-status'         => __( 'Ticket status', 'supportcandy' ),
 				'raised-by'             => __( 'Customer', 'supportcandy' ),
 				'ticket-info'           => __( 'Ticket info', 'supportcandy' ),
@@ -1555,14 +1556,22 @@
 			update_option(
 				'wpsc-ticket-widget',
 				array(
-					'change-status'         => array(
-						'title'                     => $labels['change-status'],
+					'agent-collision'       => array(
+						'title'                     => $labels['agent-collision'],
 						'is_enable'                 => 1,
-						'allow-customer'            => 1,
+						'allow-customer'            => 0,
 						'allowed-agent-roles'       => array( 1, 2 ),
 						'show-priority-to-customer' => 0,
-						'callback'                  => 'wpsc_get_tw_ticket_status()',
-						'class'                     => 'WPSC_ITW_Change_Status',
+						'callback'                  => 'wpsc_get_tw_agent_collision()',
+						'class'                     => 'WPSC_ITW_Agent_Collision',
+					),
+					'change-status'         => array(
+						'title'               => $labels['change-status'],
+						'is_enable'           => 1,
+						'allow-customer'      => 1,
+						'allowed-agent-roles' => array( 1, 2 ),
+						'callback'            => 'wpsc_get_tw_ticket_status()',
+						'class'               => 'WPSC_ITW_Change_Status',
 					),
 					'raised-by'             => array(
 						'title'               => $labels['raised-by'],
@@ -2252,7 +2261,7 @@
 			if ( version_compare( self::$current_version, '3.2.4', '<' ) ) {

 				// add default true to admin and agent dashboard access.
-				$roles = get_option( 'wpsc-agent-roles' );
+				$roles = get_option( 'wpsc-agent-roles', array() );
 				$role_keys = array();
 				foreach ( $roles as $key => $role ) {
 					$role['caps']['dash-access'] = true;
@@ -2652,6 +2661,41 @@
 				wp_clear_scheduled_hook( 'wpsc_auto_archive_closed_tickets' );
 			}

+			if ( version_compare( self::$current_version, '3.4.7', '<' ) ) {
+
+				$widgets = get_option( 'wpsc-ticket-widget', array() );
+				if ( ! isset( $widgets['agent-collision'] ) ) {
+
+					$roles = get_option( 'wpsc-agent-roles', array() );
+					$role_keys = array();
+					foreach ( $roles as $key => $role ) {
+						$role_keys[] = $key;
+					}
+
+					$label = esc_attr__( 'Currently viewing', 'supportcandy' );
+					$agent_collision = array(
+						'agent-collision' => array(
+							'title'                     => $label,
+							'is_enable'                 => 1,
+							'allow-customer'            => 0,
+							'allowed-agent-roles'       => $role_keys,
+							'show-priority-to-customer' => 0,
+							'callback'                  => 'wpsc_get_tw_agent_collision()',
+							'class'                     => 'WPSC_ITW_Agent_Collision',
+						),
+					);
+
+					// append the widget on the top.
+					$widgets = array_merge( $agent_collision, $widgets );
+					update_option( 'wpsc-ticket-widget', $widgets );
+
+					// string translations.
+					$string_translations = get_option( 'wpsc-string-translation' );
+					$string_translations['wpsc-twt-agent-collision'] = $label;
+					update_option( 'wpsc-string-translation', $string_translations );
+				}
+			}
+
 			update_option( 'wpsc-string-translation', $string_translations );
 			self::set_upgrade_complete();
 		}
--- a/supportcandy/framework/class-wpsc-framework.php
+++ b/supportcandy/framework/class-wpsc-framework.php
@@ -430,6 +430,8 @@
 			$localizations['translations']['req_term_cond']           = esc_attr__( 'Please accept terms and conditions!', 'supportcandy' );
 			$localizations['translations']['req_gdpr']           = esc_attr__( 'Please accept GDPR policy!', 'supportcandy' );
 			$localizations['translations']['delete_permanently'] = esc_attr__( 'Deleting a ticket will permanently remove all associated information and cannot be undone!', 'supportcandy' );
+			$localizations['translations']['block_notifications'] = esc_attr__( 'Blocking notifications will stop all email alerts for this ticket.', 'supportcandy' );
+			$localizations['translations']['unblock_notifications'] = esc_attr__( 'Unblocking notifications will resume email alerts for this ticket.', 'supportcandy' );
 			return $localizations;
 		}

--- a/supportcandy/includes/admin/custom-fields/class-wpsc-aof.php
+++ b/supportcandy/includes/admin/custom-fields/class-wpsc-aof.php
@@ -63,7 +63,9 @@

 			if ( ! WPSC_Functions::is_site_admin() ) {
 				wp_send_json_error( __( 'Unauthorized access!', 'supportcandy' ), 401 );
-			}?>
+			}
+			$custom_fields_types = array_merge( WPSC_Custom_Field::$cf_types, WPSC_Custom_Field::$default_cf_types );
+			?>

 			<div class="wpsc-setting-header">
 				<h2><?php esc_attr_e( 'Agent Only Fields', 'supportcandy' ); ?></h2>
@@ -83,6 +85,8 @@
 						<tr>
 							<th><?php esc_attr_e( 'Field', 'supportcandy' ); ?></th>
 							<th><?php esc_attr_e( 'Extra info', 'supportcandy' ); ?></th>
+							<th><?php esc_attr_e( 'Type', 'supportcandy' ); ?></th>
+							<th><?php esc_attr_e( 'Personal Info', 'supportcandy' ); ?></th>
 							<th><?php esc_attr_e( 'Actions', 'supportcandy' ); ?></th>
 						</tr>
 					</thead>
@@ -96,6 +100,8 @@
 							<tr>
 								<td><?php echo esc_attr( $cf->name ); ?></td>
 								<td><?php echo esc_attr( $cf->extra_info ); ?></td>
+								<td><?php echo esc_attr( $custom_fields_types[ $cf->type::$slug ]['label'] ); ?></td>
+								<td><?php echo esc_attr( $cf->is_personal_info ? __( 'Yes', 'supportcandy' ) : __( 'No', 'supportcandy' ) ); ?></td>
 								<td>
 									<span class="wpsc-link" onclick="wpsc_get_edit_custom_field(<?php echo esc_attr( $cf->id ); ?>, '<?php echo esc_attr( wp_create_nonce( 'wpsc_get_edit_custom_field' ) ); ?>');"><?php esc_attr_e( 'Edit', 'supportcandy' ); ?></span>
 									<?php
--- a/supportcandy/includes/admin/custom-fields/class-wpsc-cf.php
+++ b/supportcandy/includes/admin/custom-fields/class-wpsc-cf.php
@@ -64,7 +64,9 @@

 			if ( ! WPSC_Functions::is_site_admin() ) {
 				wp_send_json_error( __( 'Unauthorized access!', 'supportcandy' ), 401 );
-			}?>
+			}
+			$custom_fields_types = array_merge( WPSC_Custom_Field::$cf_types, WPSC_Custom_Field::$default_cf_types );
+			?>

 			<div class="wpsc-setting-header">
 				<h2><?php esc_attr_e( 'Customer Fields', 'supportcandy' ); ?></h2>
@@ -84,6 +86,11 @@
 						<tr>
 							<th><?php esc_attr_e( 'Field', 'supportcandy' ); ?></th>
 							<th><?php esc_attr_e( 'Extra info', 'supportcandy' ); ?></th>
+							<th><?php esc_attr_e( 'Type', 'supportcandy' ); ?></th>
+							<th><?php esc_attr_e( 'Auto Fill', 'supportcandy' ); ?></th>
+							<th><?php esc_attr_e( 'Personal Info', 'supportcandy' ); ?></th>
+							<th><?php esc_attr_e( 'Allow in My Profile', 'supportcandy' ); ?></th>
+							<th><?php esc_attr_e( 'Allow in Ticket Form', 'supportcandy' ); ?></th>
 							<th><?php esc_attr_e( 'Actions', 'supportcandy' ); ?></th>
 						</tr>
 					</thead>
@@ -97,6 +104,11 @@
 							<tr>
 								<td><?php echo esc_attr( $cf->name ); ?></td>
 								<td><?php echo esc_attr( $cf->extra_info ); ?></td>
+								<td><?php echo esc_attr( $custom_fields_types[ $cf->type::$slug ]['label'] ); ?></td>
+								<td><?php echo esc_attr( $cf->is_auto_fill ? __( 'Yes', 'supportcandy' ) : __( 'No', 'supportcandy' ) ); ?></td>
+								<td><?php echo esc_attr( $cf->is_personal_info ? __( 'Yes', 'supportcandy' ) : __( 'No', 'supportcandy' ) ); ?></td>
+								<td><?php echo esc_attr( $cf->allow_my_profile ? __( 'Yes', 'supportcandy' ) : __( 'No', 'supportcandy' ) ); ?></td>
+								<td><?php echo esc_attr( $cf->allow_ticket_form ? __( 'Yes', 'supportcandy' ) : __( 'No', 'supportcandy' ) ); ?></td>
 								<td>
 									<span class="wpsc-link" onclick="wpsc_get_edit_custom_field(<?php echo esc_attr( $cf->id ); ?>, '<?php echo esc_attr( wp_create_nonce( 'wpsc_get_edit_custom_field' ) ); ?>');"><?php esc_attr_e( 'Edit', 'supportcandy' ); ?></span>
 									<?php
--- a/supportcandy/includes/admin/custom-fields/class-wpsc-tf.php
+++ b/supportcandy/includes/admin/custom-fields/class-wpsc-tf.php
@@ -59,7 +59,9 @@

 			if ( ! WPSC_Functions::is_site_admin() ) {
 				wp_send_json_error( __( 'Unauthorized access!', 'supportcandy' ), 401 );
-			}?>
+			}
+			$custom_fields_types = array_merge( WPSC_Custom_Field::$cf_types, WPSC_Custom_Field::$default_cf_types );
+			?>

 			<div class="wpsc-setting-header">
 				<h2><?php esc_attr_e( 'Ticket Fields', 'supportcandy' ); ?></h2>
@@ -80,6 +82,9 @@
 						<tr>
 							<th><?php esc_attr_e( 'Field', 'supportcandy' ); ?></th>
 							<th><?php esc_attr_e( 'Extra info', 'supportcandy' ); ?></th>
+							<th><?php esc_attr_e( 'Type', 'supportcandy' ); ?></th>
+							<th><?php esc_attr_e( 'Auto Fill', 'supportcandy' ); ?></th>
+							<th><?php esc_attr_e( 'Personal Info', 'supportcandy' ); ?></th>
 							<th><?php esc_attr_e( 'Actions', 'supportcandy' ); ?></th>
 						</tr>
 					</thead>
@@ -93,6 +98,9 @@
 							<tr>
 								<td><?php echo esc_attr( $cf->name ); ?></td>
 								<td><?php echo esc_attr( $cf->extra_info ); ?></td>
+								<td><?php echo esc_attr( $custom_fields_types[ $cf->type::$slug ]['label'] ); ?></td>
+								<td><?php echo esc_attr( $cf->is_auto_fill ? __( 'Yes', 'supportcandy' ) : __( 'No', 'supportcandy' ) ); ?></td>
+								<td><?php echo esc_attr( $cf->is_personal_info ? __( 'Yes', 'supportcandy' ) : __( 'No', 'supportcandy' ) ); ?></td>
 								<td>
 									<span class="wpsc-link" onclick="wpsc_get_edit_custom_field(<?php echo esc_attr( $cf->id ); ?>, '<?php echo esc_attr( wp_create_nonce( 'wpsc_get_edit_custom_field' ) ); ?>');"><?php esc_attr_e( 'Edit', 'supportcandy' ); ?></span>
 									<?php
--- a/supportcandy/includes/admin/custom-fields/class-wpsc-tff.php
+++ b/supportcandy/includes/admin/custom-fields/class-wpsc-tff.php
@@ -52,6 +52,11 @@
 			}

 			$tff = get_option( 'wpsc-tff', array() );
+			$width = array(
+				'1/3'  => __( '1/3rd of row', 'supportcandy' ),
+				'half' => __( 'Half width', 'supportcandy' ),
+				'full' => __( 'Full width', 'supportcandy' ),
+			);
 			?>

 			<div class="wpsc-setting-header">
@@ -71,6 +76,9 @@
 					<thead>
 						<tr>
 							<th><?php esc_attr_e( 'Field', 'supportcandy' ); ?></th>
+							<th><?php esc_attr_e( 'Required', 'supportcandy' ); ?></th>
+							<th><?php esc_attr_e( 'Visibility Conditions', 'supportcandy' ); ?></th>
+							<th><?php esc_attr_e( 'Width', 'supportcandy' ); ?></th>
 							<th><?php esc_attr_e( 'Actions', 'supportcandy' ); ?></th>
 						</tr>
 					</thead>
@@ -81,9 +89,14 @@
 							if ( ! $cf ) {
 								continue;
 							}
+							$vis_decoded = is_string( $settings['visibility'] ) ? json_decode( $settings['visibility'], true ) : $settings['visibility'];
+							$visibility = ( ! empty( $vis_decoded ) ) ? __( 'Yes', 'supportcandy' ) : __( 'No', 'supportcandy' );
 							?>
 							<tr>
 								<td><?php echo esc_attr( $cf->name ); ?></td>
+								<td><?php echo esc_attr( $settings['is-required'] ? __( 'Yes', 'supportcandy' ) : __( 'No', 'supportcandy' ) ); ?></td>
+								<td><?php echo esc_attr( $visibility ); ?></td>
+								<td><?php echo esc_attr( isset( $width[ $settings['width'] ] ) ? $width[ $settings['width'] ] : $settings['width'] ); ?></td>
 								<td>
 									<span class="wpsc-link" onclick="wpsc_get_edit_tff(<?php echo esc_attr( $cf->id ); ?>, '<?php echo esc_attr( wp_create_nonce( 'wpsc_get_edit_tff' ) ); ?>');"><?php esc_attr_e( 'Edit', 'supportcandy' ); ?></span>
 									<?php
--- a/supportcandy/includes/admin/customers/class-wpsc-customers.php
+++ b/supportcandy/includes/admin/customers/class-wpsc-customers.php
@@ -31,14 +31,14 @@
 			add_action( 'wp_ajax_wpsc_delete_customer', array( __CLASS__, 'delete_customer_info' ) );

 			// calculate customer ticket count.
-			add_action( 'wpsc_create_new_ticket', array( __CLASS__, 'customer_ticket_count' ) );
-			add_action( 'wpsc_delete_ticket', array( __CLASS__, 'customer_ticket_count' ) );
-			add_action( 'wpsc_ticket_restore', array( __CLASS__, 'customer_ticket_count' ) );
-			add_action( 'wpsc_ticket_delete_permanently', array( __CLASS__, 'customer_ticket_count' ) );
-			add_action( 'wpsc_change_raised_by', array( __CLASS__, 'customer_ticket_count_after_change_raised_by' ), 200, 4 );
-			add_action( 'wpsc_ticket_archive', array( __CLASS__, 'reset_customer_ticket_count' ), 200, 2 );
-			add_action( 'wpsc_archive_ticket_restore', array( __CLASS__, 'reset_customer_ticket_count' ), 200, 2 );
-			add_action( 'wpsc_after_ticket_merge', array( __CLASS__, 'customer_ticket_count_after_ticket_merge' ), 200, 2 );
+			add_action( 'wpsc_create_new_ticket', array( __CLASS__, 'customer_ticket_count' ), 500 );
+			add_action( 'wpsc_delete_ticket', array( __CLASS__, 'customer_ticket_count' ), 500 );
+			add_action( 'wpsc_ticket_restore', array( __CLASS__, 'customer_ticket_count' ), 500 );
+			add_action( 'wpsc_ticket_delete_permanently', array( __CLASS__, 'customer_ticket_count' ), 500 );
+			add_action( 'wpsc_change_raised_by', array( __CLASS__, 'customer_ticket_count_after_change_raised_by' ), 500, 4 );
+			add_action( 'wpsc_ticket_archive', array( __CLASS__, 'reset_customer_ticket_count' ), 500, 2 );
+			add_action( 'wpsc_archive_ticket_restore', array( __CLASS__, 'reset_customer_ticket_count' ), 500, 2 );
+			add_action( 'wpsc_after_ticket_merge', array( __CLASS__, 'customer_ticket_count_after_ticket_merge' ), 500, 2 );

 			// view customer profile info.
 			add_action( 'wp_ajax_wpsc_view_customer_detailed_info', array( __CLASS__, 'view_customer_detailed_info' ) );
@@ -684,7 +684,19 @@
 		 */
 		public static function customer_ticket_count_after_ticket_merge( $prev_ticket, $new_ticket ) {

-			$prev_ticket->customer->update_ticket_count();
+			if ( empty( $prev_ticket->customer ) ) {
+				return;
+			}
+
+			$customer = $prev_ticket->customer;
+
+			if ( ! is_object( $customer ) ) {
+				$customer = new WPSC_Customer( $customer );
+			}
+
+			if ( isset( $customer->id ) && method_exists( $customer, 'update_ticket_count' ) ) {
+				$customer->update_ticket_count();
+			}
 		}

 		/**
@@ -698,7 +710,19 @@
 		 */
 		public static function customer_ticket_count_after_change_raised_by( $ticket, $prev, $new, $customer_id ) {

-			$ticket->customer->update_ticket_count();
+			if ( empty( $ticket->customer ) ) {
+				return;
+			}
+
+			$customer = $ticket->customer;
+
+			if ( ! is_object( $customer ) ) {
+				$customer = new WPSC_Customer( $customer );
+			}
+
+			if ( isset( $customer->id ) && method_exists( $customer, 'update_ticket_count' ) ) {
+				$customer->update_ticket_count();
+			}
 		}

 		/**
@@ -710,7 +734,19 @@
 		 */
 		public static function reset_customer_ticket_count( $ticket, $ar_ticket ) {

-			$ticket->customer->update_ticket_count();
+			if ( empty( $ticket->customer ) ) {
+				return;
+			}
+
+			$customer = $ticket->customer;
+
+			if ( ! is_object( $customer ) ) {
+				$customer = new WPSC_Customer( $customer );
+			}
+
+			if ( isset( $customer->id ) && method_exists( $customer, 'update_ticket_count' ) ) {
+				$customer->update_ticket_count();
+			}
 		}

 		/**
@@ -721,7 +757,19 @@
 		 */
 		public static function customer_ticket_count( $ticket ) {

-			$ticket->customer->update_ticket_count();
+			if ( empty( $ticket->customer ) ) {
+				return;
+			}
+
+			$customer = $ticket->customer;
+
+			if ( ! is_object( $customer ) ) {
+				$customer = new WPSC_Customer( $customer );
+			}
+
+			if ( isset( $customer->id ) && method_exists( $customer, 'update_ticket_count' ) ) {
+				$customer->update_ticket_count();
+			}
 		}

 		/**
--- a/supportcandy/includes/admin/settings/miscellaneous-settings/class-wpsc-ms-gdpr.php
+++ b/supportcandy/includes/admin/settings/miscellaneous-settings/class-wpsc-ms-gdpr.php
@@ -332,10 +332,9 @@
 			?>

 			case 'gdpr':
-				var checkbox = customField.find('input:checked');
-				if (checkbox.length === 0) {
+				if (customField.find('input:checked').length === 0) {
+					showFieldError(customField, supportcandy.translations.req_gdpr);
 					isValid = false;
-					alert(supportcandy.translations.req_gdpr);
 				}
 				break;
 			<?php
--- a/supportcandy/includes/admin/settings/miscellaneous-settings/class-wpsc-ms-recaptcha.php
+++ b/supportcandy/includes/admin/settings/miscellaneous-settings/class-wpsc-ms-recaptcha.php
@@ -199,7 +199,6 @@
 					var recaptcha = jQuery("#g-recaptcha-response").val();
 					if (recaptcha === "") {
 						isValid = false;
-						alert("<?php esc_attr_e( 'Captcha not set!', 'supportcandy' ); ?>");
 					}
 					break;
 				<?php
--- a/supportcandy/includes/admin/settings/miscellaneous-settings/class-wpsc-ms-tac.php
+++ b/supportcandy/includes/admin/settings/miscellaneous-settings/class-wpsc-ms-tac.php
@@ -296,10 +296,9 @@
 			?>

 			case 'term-and-conditions':
-				var checkbox = customField.find('input:checked');
-				if (checkbox.length === 0) {
+				if (customField.find('input:checked').length === 0) {
+					showFieldError(customField, supportcandy.translations.req_term_cond);
 					isValid = false;
-					alert(supportcandy.translations.req_term_cond);
 				}
 				break;
 			<?php
--- a/supportcandy/includes/admin/settings/text-editor-settings/class-wpsc-text-editor.php
+++ b/supportcandy/includes/admin/settings/text-editor-settings/class-wpsc-text-editor.php
@@ -331,6 +331,16 @@
 								wpsc_clear_saved_draft_reply( ticket_id );
 							}
 						});
+						editor.on('keyup change input', function () {
+							var field = jQuery('#' + editor.id).closest('.wpsc-tff');
+
+							// Check if editor actually has content
+							var content = editor.getContent({ format: 'text' }).trim();
+
+							if (content !== '') {
+								field.find('.wpsc-error-msg').remove();
+							}
+						});
 					}
 				});
 				<?php
--- a/supportcandy/includes/admin/tickets/class-wpsc-individual-ticket.php
+++ b/supportcandy/includes/admin/tickets/class-wpsc-individual-ticket.php
@@ -108,6 +108,8 @@
 			add_action( 'wp_ajax_nopriv_wpsc_it_ticket_restore', array( __CLASS__, 'ticket_restore' ) );
 			add_action( 'wp_ajax_wpsc_it_delete_permanently', array( __CLASS__, 'set_delete_ticket_permanently' ) );
 			add_action( 'wp_ajax_nopriv_wpsc_it_delete_permanently', array( __CLASS__, 'set_delete_ticket_permanently' ) );
+			add_action( 'wp_ajax_wpsc_it_block_unblock_notifications', array( __CLASS__, 'set_block_unblock_notifications' ) );
+			add_action( 'wp_ajax_nopriv_wpsc_it_block_unblock_notifications', array( __CLASS__, 'set_block_unblock_notifications' ) );

 			// Thread actions.
 			add_action( 'wp_ajax_wpsc_it_thread_info', array( __CLASS__, 'it_thread_info' ) );
@@ -136,10 +138,6 @@
 			// Load older threads.
 			add_action( 'wp_ajax_wpsc_load_older_threads', array( __CLASS__, 'load_older_threads' ) );
 			add_action( 'wp_ajax_nopriv_wpsc_load_older_threads', array( __CLASS__, 'load_older_threads' ) );
-
-			// agent collision.
-			add_filter( 'wp_ajax_wpsc_check_live_agents', array( __CLASS__, 'check_live_agents' ) );
-			add_action( 'wp_ajax_nopriv_wpsc_check_live_agents', array( __CLASS__, 'check_live_agents' ) );
 		}

 		/**
@@ -160,7 +158,6 @@
 				<?php
 					self::get_actions();
 					self::get_subject();
-					self::get_live_agents();
 					self::get_mobile_widgets();
 				if ( $gs['reply-form-position'] == 'top' ) {
 					self::get_reply_section();
@@ -382,6 +379,25 @@
 				);
 			}

+			// Block Notifications.
+			$misc        = is_array( self::$ticket->misc ) ? self::$ticket->misc : array();
+			$is_blocked = ! empty( $misc['block_notifications'] );
+			$action_label = $is_blocked
+				? esc_html__( 'Unblock Notifications', 'supportcandy' )
+				: esc_html__( 'Block Notifications', 'supportcandy' );
+
+			if ( $current_user->is_agent && WPSC_Functions::is_site_admin() && self::$ticket->is_active ) {
+				$actions['block-notifications'] = array(
+					'label'    => $action_label,
+					'callback' => sprintf(
+						"wpsc_it_block_unblock_notifications(%d, %s, '%s');",
+						(int) self::$ticket->id,
+						$is_blocked ? 'false' : 'true',
+						wp_create_nonce( 'wpsc_it_block_unblock_notifications' )
+					),
+				);
+			}
+
 			self::$actions = apply_filters( 'wpsc_individual_ticket_actions', $actions, self::$ticket );

 			if ( self::$view_profile == 'agent' ) {
@@ -2450,6 +2466,43 @@
 		}

 		/**
+		 * Block/Unblock ticket notifications ajax request
+		 *
+		 * @return void
+		 */
+		public static function set_block_unblock_notifications() {
+
+			if ( ! check_ajax_referer( 'wpsc_it_block_unblock_notifications', '_ajax_nonce', false ) ) {
+				wp_send_json_error( __( 'Unauthorized request.', 'supportcandy' ), 401 );
+			}
+
+			$current_user = WPSC_Current_User::$current_user;
+			if ( ! $current_user || ! $current_user->is_agent ) {
+				wp_send_json_error( __( 'Unauthorized request.', 'supportcandy' ), 401 );
+			}
+
+			self::load_current_ticket();
+			if ( self::$is_restricted || ! self::$ticket ) {
+				wp_send_json_error( __( 'Unauthorized request.', 'supportcandy' ), 401 );
+			}
+
+			$is_block = isset( $_POST['is_block'] ) ? (int) $_POST['is_block'] : 0;
+			$is_block = $is_block === 1 ? 1 : 0;
+
+			$misc = is_array( self::$ticket->misc ) ? self::$ticket->misc : array();
+			if ( $is_block ) {
+				$misc['block_notifications'] = 1;
+			} else {
+				unset( $misc['block_notifications'] );
+			}
+
+			self::$ticket->misc = $misc;
+			self::$ticket->save();
+			do_action( 'wpsc_ticket_block_notifications', self::$ticket, $is_block );
+			wp_die();
+		}
+
+		/**
 		 * Permanently delete current ticket
 		 *
 		 * @return void
@@ -3560,193 +3613,6 @@
 			wp_send_json( $html_response );
 			wp_die();
 		}
-
-		/**
-		 * Show the list of live agents.
-		 *
-		 * @return void
-		 */
-		public static function get_live_agents() {
-
-			$current_user = WPSC_Current_User::$current_user;
-			$ms_advanced = get_option( 'wpsc-ms-advanced-settings' );
-			if ( ! ( $current_user->is_agent && $ms_advanced['agent-collision'] ) ) {
-				return;
-			}
-			?>
-			<div class="wpsc-it-body-item wpsc-agent-collision wpsc-it-widget">
-				<div class="wpsc-widget-header">
-					<h2><?php esc_attr_e( 'Currently viewing', 'supportcandy' ); ?></h2>
-				</div>
-				<div class="wpsc-widget-body wpsc-live-agents">
-				</div>
-			</div>
-
-			<script>
-				supportcandy.agent_collision = true;
-				wpsc_get_live_agents(<?php echo intval( $current_user->agent->id ); ?>, <?php echo intval( self::$ticket->id ); ?>);
-				function wpsc_get_live_agents( agent_id, ticket_id ){
-
-					jQuery('.wpsc-live-agents').html('');
-					jQuery('.wpsc-agent-collision').hide();
-					var current_tid = jQuery('#wpsc-current-ticket').val();
-					if( current_tid != ticket_id ){
-						return;
-					}
-					const urlParams = new URLSearchParams(window.location.search);
-					if( supportcandy.is_frontend === '0' ) {
-						section = urlParams.get('section');
-					}else{
-						section = urlParams.get('wpsc-section');
-					}
-					if( ! ( section == 'ticket-list' && ( urlParams.has('id') || urlParams.has('ticket-id')) ) ){
-						return;
-					}
-					var data = { action: 'wpsc_check_live_agents', agent_id, ticket_id, operation: 'check', _ajax_nonce: supportcandy.nonce };
-					jQuery.post(
-						supportcandy.ajax_url,
-						data,
-						function (response) {
-							if ( response.agents ) {
-								jQuery('.wpsc-live-agents').html(response.agents);
-								jQuery('.wpsc-agent-collision').show();
-							} else {
-								jQuery('.wpsc-live-agents').html('');
-								jQuery('.wpsc-agent-collision').hide();
-							}
-						}
-					);
-					setTimeout(
-						function () {
-							wpsc_get_live_agents( agent_id, ticket_id );
-						},
-						60000
-					);
-				}
-
-				jQuery(document).ready(function(){
-					window.addEventListener('beforeunload', function () {
-						const urlParams = new URLSearchParams(window.location.search);
-
-						// Determine section
-						let section = supportcandy.is_frontend === '0'
-							? urlParams.get('section')
-							: urlParams.get('wpsc-section');
-
-						// Only trigger if we are on the ticket detail page
-						if (!(section === 'ticket-list' && (urlParams.has('id') || urlParams.has('ticket-id')))) {
-							return;
-						}
-
-						// Prepare data
-						const data = new FormData();
-						data.append('action', 'wpsc_check_live_agents');
-						data.append('agent_id', '<?php echo intval( $current_user->agent->id ); ?>');
-						data.append('ticket_id', '<?php echo intval( self::$ticket->id ); ?>');
-						data.append('operation', 'leave');
-						data.append('_ajax_nonce', supportcandy.nonce);
-
-						// Send reliably on tab close
-						navigator.sendBeacon(supportcandy.ajax_url, data);
-					});
-				});
-			</script>
-			<?php
-		}
-
-		/**
-		 * Get list of live agents.
-		 *
-		 * @return void
-		 */
-		public static function check_live_agents() {
-
-			if ( check_ajax_referer( 'general', '_ajax_nonce', false ) != 1 ) {
-				wp_send_json_error( 'Unauthorized request!', 401 );
-			}
-
-			$current_user = WPSC_Current_User::$current_user;
-			$ms_advanced = get_option( 'wpsc-ms-advanced-settings' );
-			if ( ! ( $current_user->is_agent && $ms_advanced['agent-collision'] ) ) {
-				wp_send_json_error( new WP_Error( '001', 'Unauthorized!' ), 401 );
-			}
-
-			$id = isset( $_POST['ticket_id'] ) ? intval( $_POST['ticket_id'] ) : 0; // phpcs:ignore
-			if ( ! $id ) {
-				wp_send_json_error( new WP_Error( '001', 'Unauthorized!' ), 401 );
-			}
-
-			$ticket = new WPSC_Ticket( $id );
-			if ( ! $ticket->id ) {
-				wp_send_json_error( new WP_Error( '002', 'Something went wrong!' ), 400 );
-			}
-
-			self::$ticket = $ticket;
-			if ( ! self::has_ticket_cap( 'view' ) ) {
-				wp_send_json_error( new WP_Error( '003', 'Unauthorized!' ), 401 );
-			}
-
-			$agent_id = isset( $_POST['agent_id'] ) ? intval( $_POST['agent_id'] ) : 0; // phpcs:ignore
-			if ( ! $agent_id ) {
-				wp_send_json_error( new WP_Error( '001', 'Unauthorized!' ), 401 );
-			}
-
-			$agent = new WPSC_Agent( $agent_id );
-			if ( ! $agent->id ) {
-				wp_send_json_error( new WP_Error( '002', 'Something went wrong!' ), 400 );
-			}
-
-			$operation = isset( $_POST['operation'] ) ? esc_attr( $_POST['operation'] ) : 'check'; // phpcs:ignore
-
-			$agents = json_decode( $ticket->live_agents, true );
-			$agents = $agents ? $agents : array();
-
-			if ( $operation == 'leave' ) {
-
-				if ( array_key_exists( $agent->id, $agents ) ) {
-					unset( $agents[ $agent->id ] );
-					$agents = wp_json_encode( $agents );
-					$ticket->live_agents = $agents;
-					$ticket->save();
-				}
-				wp_die();
-			}
-			// check inactive agents.
-			foreach ( $agents as $key => $tm ) {
-				if ( $key == $agent->id ) {
-					continue;
-				}
-
-				$time = DateTime::createFromFormat( 'Y-m-d H:i:s', $tm );
-				$now = new DateTime();
-				$interval = $now->diff( $time );
-				if ( $interval->i >= 1 && $interval->s > 1 ) {
-					unset( $agents[ $key ] );
-				}
-			}
-
-			$agents[ $agent->id ] = ( new DateTime() )->format( 'Y-m-d H:i:s' );
-
-			$html = '';
-			foreach ( $agents as $ag_id => $tmp ) {
-				if ( $ag_id == $agent->id ) {
-					continue;
-				}
-				$agt = new WPSC_Agent( $ag_id );
-
-				$html .= '<div class="wpsc-ac-agent"> ' .
-							get_avatar( $agt->customer->email, 20 ) .
-							'<span class="ac-name">' . $agt->name . '</span></div>';
-			}
-
-			$agents = wp_json_encode( $agents );
-			$ticket->live_agents = $agents;
-			$ticket->save();
-			$response = array(
-				'agents' => $html,
-			);
-			wp_send_json( $response );
-		}
 	}
 endif;

--- a/supportcandy/includes/admin/tickets/class-wpsc-new-ticket.php
+++ b/supportcandy/includes/admin/tickets/class-wpsc-new-ticket.php
@@ -322,22 +322,58 @@

 					var customFields = jQuery('.wpsc-tff.wpsc-visible');
 					var flag = true;
+					var firstInvalidField = null;
+
+					// Remove old errors
+					clearAllFieldErrors();
 					jQuery.each(customFields, function(index, customField){

 						customField = jQuery(customField);
 						var customFieldType = customField.data('cft');
 						var isValid = true;
+						var isRequired = customField.hasClass('required');
+
+						var fieldLabel = customField.find('.wpsc-tff-label .name').text().trim() || 'This field';
 						switch (customFieldType) {
 							<?php do_action( 'wpsc_js_validate_ticket_form' ); ?>
 						}
+						//Common error handling
 						if (!isValid) {
+
 							flag = false;
-							return false;
+
+							// Show error message
+							showFieldError(customField, fieldLabel + ' is required');
+
+							// Track first invalid field
+							if (!firstInvalidField) {
+								firstInvalidField = customField;
+							}
 						}
 					});
+
+					// Scroll to first error
+					if (firstInvalidField) {
+						firstInvalidField[0].scrollIntoView({ behavior: 'smooth', block: 'center' });
+					}
 					return flag;
 				}

+				function showFieldError(field, message) {
+					if (field.find('.wpsc-error-msg').length === 0) {
+						field.append('<div class="wpsc-error-msg">' + message + '</div>');
+					}
+				}
+
+				function clearAllFieldErrors() {
+					jQuery('.wpsc-error-msg').remove();
+				}
+
+				jQuery(document).on('input change', '.wpsc-tff input, .wpsc-tff textarea, .wpsc-tff select', function(){
+					var field = jQuery(this).closest('.wpsc-tff');
+					field.find('.wpsc-error-msg').remove();
+				});
+
 				function wpsc_clear_hidden_fields() {
 					var customFields = jQuery('.wpsc-tff.wpsc-hidden');
 					jQuery.each(customFields, function(index, customField){
--- a/supportcandy/includes/admin/tickets/widgets/class-wpsc-itw-agent-collision.php
+++ b/supportcandy/includes/admin/tickets/widgets/class-wpsc-itw-agent-collision.php
@@ -0,0 +1,347 @@
+<?php
+
+if ( ! defined( 'ABSPATH' ) ) {
+	exit; // Exit if accessed directly!
+}
+
+if ( ! class_exists( 'WPSC_ITW_Agent_Collision' ) ) :
+
+	final class WPSC_ITW_Agent_Collision {
+
+		/**
+		 * Initialization
+		 *
+		 * @return void
+		 */
+		public static function init() {
+
+			// agent collision.
+			add_action( 'wp_ajax_wpsc_check_live_agents', array( __CLASS__, 'check_live_agents' ) );
+			add_action( 'wp_ajax_nopriv_wpsc_check_live_agents', array( __CLASS__, 'check_live_agents' ) );
+
+			// edit widget settings.
+			add_action( 'wp_ajax_wpsc_get_tw_agent_collision', array( __CLASS__, 'get_tw_agent_collision' ) );
+			add_action( 'wp_ajax_wpsc_set_tw_agent_collision', array( __CLASS__, 'set_tw_agent_collision' ) );
+
+			add_action( 'wpsc_it_layout_section', array( __CLASS__, 'print_agent_collision_scripts' ) );
+		}
+
+		/**
+		 * Print body of current widget
+		 *
+		 * @param WPSC_Ticket $ticket - ticket object.
+		 * @param array       $settings - setting array.
+		 * @return void
+		 */
+		public static function print_widget( $ticket, $settings ) {
+
+			$current_user = WPSC_Current_User::$current_user;
+			$ms_advanced = get_option( 'wpsc-ms-advanced-settings' );
+
+			if (
+				! $current_user->is_agent ||
+				! $ms_advanced['agent-collision'] ||
+				WPSC_Individual_Ticket::$view_profile !== 'agent'
+			) {
+				return;
+			}
+
+			WPSC_Individual_Ticket::$ticket = $ticket;
+			if ( WPSC_Individual_Ticket::$is_restricted ) {
+				return;
+			}
+
+			$title = $settings['title']
+					? WPSC_Translations::get( 'wpsc-twt-agent-collision', stripslashes( $settings['title'] ) )
+					: stripslashes( $settings['title'] );
+			$no_agent_html = '<div class="wpsc-no-agents">No live agent found</div>';
+			?>
+			<div class="wpsc-it-widget wpsc-itw-ac">
+				<div class="wpsc-widget-header">
+					<h2><?php echo esc_html( $title ); ?></h2>
+					<span class="wpsc-itw-toggle" data-widget="wpsc-itw-ac"><?php WPSC_Icons::get( 'chevron-up' ); ?></span>
+				</div>
+				<div class="wpsc-widget-body" style="max-height: 100px;">
+					<div class="wpsc-widget-default">
+						<div class="wpsc-agent-collision">
+							<div class="wpsc-live-agents"></div>
+						</div>
+					</div>
+				</div>
+			</div>
+			<?php
+		}
+
+		/**
+		 * Print necessary scripts for agent collision widget
+		 *
+		 * @param WPSC_Ticket $ticket - ticket object.
+		 * @return void
+		 */
+		public static function print_agent_collision_scripts( $ticket ) {
+			$current_user = WPSC_Current_User::$current_user;
+			?>
+			<script>
+				supportcandy.agent_collision = true;
+				wpsc_get_live_agents(<?php echo intval( $current_user->agent->id ); ?>, <?php echo intval( $ticket->id ); ?>);
+				function wpsc_get_live_agents(agent_id, ticket_id){
+
+					// ALWAYS re-select DOM (important)
+					let container = jQuery('.wpsc-live-agents');
+					let parent = jQuery('.wpsc-agent-collision');
+
+					var current_tid = jQuery('#wpsc-current-ticket').val();
+					if (current_tid != ticket_id){
+						schedule_next(agent_id, ticket_id);
+						return;
+					}
+
+					const urlParams = new URLSearchParams(window.location.search);
+					let section = supportcandy.is_frontend === '0'
+						? urlParams.get('section')
+						: urlParams.get('wpsc-section');
+
+					if (!(section == 'ticket-list' && (urlParams.has('id') || urlParams.has('ticket-id')))){
+						schedule_next(agent_id, ticket_id);
+						return;
+					}
+
+					var data = {
+						action: 'wpsc_check_live_agents',
+						agent_id,
+						ticket_id,
+						operation: 'check',
+						_ajax_nonce: supportcandy.nonce
+					};
+
+					jQuery.post(supportcandy.ajax_url, data, function (response) {
+						if (response && response.agents){
+							if (container.html() !== response.agents){
+								container.html(response.agents);
+							}
+							parent.stop(true, true).fadeIn(200);
+						} else {
+
+							let noAgentHTML = '<div class="wpsc-no-agents"><?php echo esc_attr__( 'No live agent found', 'supportcandy' ); ?></div>';
+							if (container.html() !== noAgentHTML){
+								container.html(noAgentHTML);
+							}
+							parent.stop(true, true).fadeIn(200);
+						}
+					});
+					schedule_next(agent_id, ticket_id);
+				}
+
+				// Keep polling simple & reliable
+				function schedule_next(agent_id, ticket_id){
+					setTimeout(function(){
+						wpsc_get_live_agents(agent_id, ticket_id);
+					}, 60000);
+				}
+
+				jQuery(document).ready(function(){
+					window.addEventListener('beforeunload', function () {
+						const urlParams = new URLSearchParams(window.location.search);
+
+						// Determine section
+						let section = supportcandy.is_frontend === '0'
+							? urlParams.get('section')
+							: urlParams.get('wpsc-section');
+
+						// Only trigger if we are on the ticket detail page
+						if (!(section === 'ticket-list' && (urlParams.has('id') || urlParams.has('ticket-id')))) {
+							return;
+						}
+
+						// Prepare data
+						const data = new FormData();
+						data.append('action', 'wpsc_check_live_agents');
+						data.append('agent_id', '<?php echo intval( $current_user->agent->id ); ?>');
+						data.append('ticket_id', '<?php echo intval( $ticket->id ); ?>');
+						data.append('operation', 'leave');
+						data.append('_ajax_nonce', supportcandy.nonce);
+
+						// Send reliably on tab close
+						navigator.sendBeacon(supportcandy.ajax_url, data);
+					});
+				});
+			</script>
+			<?php
+		}
+
+		/**
+		 * Get list of live agents.
+		 *
+		 * @return void
+		 */
+		public static function check_live_agents() {
+
+			if ( check_ajax_referer( 'general', '_ajax_nonce', false ) != 1 ) {
+				wp_send_json_error( 'Unauthorized request!', 401 );
+			}
+
+			$current_user = WPSC_Current_User::$current_user;
+			$ms_advanced = get_option( 'wpsc-ms-advanced-settings' );
+			if ( ! ( $current_user->is_agent && $ms_advanced['agent-collision'] ) ) {
+				wp_send_json_error( new WP_Error( '001', 'Unauthorized!' ), 401 );
+			}
+
+			$id = isset( $_POST['ticket_id'] ) ? intval( $_POST['ticket_id'] ) : 0; // phpcs:ignore
+			if ( ! $id ) {
+				wp_send_json_error( new WP_Error( '001', 'Unauthorized!' ), 401 );
+			}
+
+			// Always use the authenticated user's agent ID, not the request's agent_id.
+			if ( ! isset( $current_user->agent ) || ! $current_user->agent->id ) {
+				wp_send_json_error( new WP_Error( '001', 'Unauthorized!' ), 401 );
+			}
+
+			// If agent_id is present in the request, it must match the authenticated user's agent ID.
+			if ( isset( $_POST['agent_id'] ) && intval( $_POST['agent_id'] ) !== intval( $current_user->agent->id ) ) {
+				wp_send_json_error( new WP_Error( '004', 'Agent ID mismatch!' ), 401 );
+			}
+
+			$agent_id = intval( $current_user->agent->id );
+			$agent = new WPSC_Agent( $agent_id );
+			if ( ! $agent->id ) {
+				wp_send_json_error( new WP_Error( '002', 'Something went wrong!' ), 400 );
+			}
+
+			$ticket = new WPSC_Ticket( $id );
+			if ( ! $ticket->id ) {
+				wp_send_json_error( new WP_Error( '002', 'Something went wrong!' ), 400 );
+			}
+
+			WPSC_Individual_Ticket::$ticket = $ticket;
+			if ( ! WPSC_Individual_Ticket::has_ticket_cap( 'view' ) ) {
+				wp_send_json_error( new WP_Error( '003', 'Unauthorized!' ), 401 );
+			}
+
+			$operation = isset( $_POST['operation'] ) ? esc_attr( $_POST['operation'] ) : 'check'; // phpcs:ignore
+
+			$agents = json_decode( $ticket->live_agents, true );
+			$agents = $agents ? $agents : array();
+
+			if ( $operation == 'leave' ) {
+
+				if ( array_key_exists( $agent->id, $agents ) ) {
+					unset( $agents[ $agent->id ] );
+					$agents = wp_json_encode( $agents );
+					$ticket->live_agents = $agents;
+					$ticket->save();
+				}
+				wp_die();
+			}
+			// check inactive agents using timestamp comparison; remove if parsing fails or inactive > 60s.
+			foreach ( $agents as $key => $tm ) {
+				if ( $key == $agent->id ) {
+					continue;
+				}
+				$timestamp = strtotime( $tm );
+				if ( $timestamp === false || $timestamp < ( time() - 60 ) ) {
+					unset( $agents[ $key ] );
+				}
+			}
+
+			$agents[ $agent->id ] = ( new DateTime() )->format( 'Y-m-d H:i:s' );
+
+			$html = '';
+			foreach ( $agents as $ag_id => $tmp ) {
+				if ( $ag_id == $agent->id ) {
+					continue;
+				}
+				$agt = new WPSC_Agent( $ag_id );
+
+				$html .= '<div class="wpsc-agent-avatar" title="' . esc_attr( $agt->name ) . '">' .
+					get_avatar( $agt->customer->email, 28 ) .
+				'</div>';
+			}
+
+			$agents = wp_json_encode( $agents );
+			$ticket->live_agents = $agents;
+			$ticket->save();
+			$response = array(
+				'agents' => $html,
+			);
+			wp_send_json( $response );
+		}
+
+		/**
+		 * Add/edit Agent Collision title
+		 *
+		 * @return void
+		 */
+		public static function get_tw_agent_collision() {
+
+			if ( ! WPSC_Functions::is_site_admin() ) {
+				wp_send_json_error( __( 'Unauthorized access!', 'supportcandy' ), 401 );
+			}
+
+			$ticket_widgets = get_option( 'wpsc-ticket-widget', array() );
+			$agent_collision = $ticket_widgets['agent-collision'];
+			$title = $agent_collision['title'];
+			ob_start();
+			?>
+			<form action="#" onsubmit="return false;" class="wpsc-frm-edit-ac">
+				<div class="wpsc-input-group">
+					<div class="label-container">
+						<label for=""><?php echo esc_attr( wpsc__( 'Title', 'supportcandy' ) ); ?></label>
+					</div>
+					<input name="label" type="text" value="<?php echo esc_attr( $agent_collision['title'] ); ?>" autocomplete="off">
+				</div>
+				<input type="hidden" name="action" value="wpsc_set_tw_agent_collision">
+				<input type="hidden" name="_ajax_nonce" value="<?php echo esc_attr( wp_create_nonce( 'wpsc_set_tw_agent_collision' ) ); ?>">
+			</form>
+			<?php
+			$body = ob_get_clean();
+			ob_start();
+			?>
+			<button class="wpsc-button small primary" onclick="wpsc_set_tw_agent_collision(this);">
+				<?php echo esc_attr( wpsc__( 'Submit', 'supportcandy' ) ); ?>
+			</button>
+			<button class="wpsc-button small secondary" onclick="wpsc_close_modal();">
+				<?php echo esc_attr( wpsc__( 'Cancel', 'supportcandy' ) ); ?>
+			</button>
+			<?php
+			$footer   = ob_get_clean();
+			$response = array(
+				'title'  => $title,
+				'body'   => $body,
+				'footer' => $footer,
+			);
+			wp_send_json( $response );
+		}
+
+		/**
+		 * Save Agent Collision label
+		 *
+		 * @return void
+		 */
+		public static function set_tw_agent_collision() {
+
+			if ( check_ajax_referer( 'wpsc_set_tw_agent_collision', '_ajax_nonce', false ) != 1 ) {
+				wp_send_json_error( 'Unauthorized request!', 400 );
+			}
+
+			if ( ! WPSC_Functions::is_site_admin() ) {
+				wp_send_json_error( __( 'Unauthorized access!', 'supportcandy' ), 401 );
+			}
+
+			$label = isset( $_POST['label'] ) ? sanitize_text_field( wp_unslash( $_POST['label'] ) ) : '';
+			if ( ! $label ) {
+				wp_send_json_error( 'Bad Request', 400 );
+			}
+
+			$ticket_widgets = get_option( 'wpsc-ticket-widget', array() );
+			$ticket_widgets['agent-collision']['title'] = $label;
+			update_option( 'wpsc-ticket-widget', $ticket_widgets );
+
+			// remove string translations.
+			WPSC_Translations::remove( 'wpsc-twt-agent-collision' );
+			WPSC_Translations::add( 'wpsc-twt-agent-collision', stripslashes( $label ) );
+			wp_die();
+		}
+	}
+endif;
+
+WPSC_ITW_Agent_Collision::init();
--- a/supportcandy/includes/class-wpsc-email-notifications.php
+++ b/supportcandy/includes/class-wpsc-email-notifications.php
@@ -249,6 +249,12 @@
 				return false;
 			}

+			// check block notification flag in misc to skip notifications if it is set for the ticket.
+			$misc_array = $this->ticket->misc ? $this->ticket->misc : array();
+			if ( ! empty( $misc_array['block_notifications'] ) ) {
+				return false;
+			}
+
 			// from name & email.
 			$en_general = get_option( 'wpsc-en-general' );
 			if ( ! $en_general['from-name'] || ! $en_general['from-email'] ) {
--- a/supportcandy/includes/custom-field-types/class-wpsc-cf-checkbox.php
+++ b/supportcandy/includes/custom-field-types/class-wpsc-cf-checkbox.php
@@ -512,10 +512,8 @@
 			?>

 			case '<?php echo esc_attr( self::$slug ); ?>':
-				var checkbox = customField.find('input:checked');
-				if (customField.hasClass('required') && checkbox.length === 0) {
+				if (isRequired && customField.find('input:checked').length === 0) {
 					isValid = false;
-					alert(supportcandy.translations.req_fields_missing);
 				}
 				break;
 			<?php
--- a/supportcandy/includes/custom-field-types/class-wpsc-cf-date.php
+++ b/supportcandy/includes/custom-field-types/class-wpsc-cf-date.php
@@ -618,9 +618,8 @@

 			case '<?php echo esc_attr( self::$slug ); ?>':
 				var val = customField.find('input').first().val().trim();
-				if (customField.hasClass('required') && !val) {
+				if (isRequired && !val) {
 					isValid = false;
-					alert(supportcandy.translations.req_fields_missing);
 				}
 				break;
 			<?php
--- a/supportcandy/includes/custom-field-types/class-wpsc-cf-datetime.php
+++ b/supportcandy/includes/custom-field-types/class-wpsc-cf-datetime.php
@@ -580,9 +580,8 @@

 			case '<?php echo esc_attr( self::$slug ); ?>':
 				var val = customField.find('input').first().val().trim();
-				if (customField.hasClass('required') && !val) {
+				if (isRequired && !val) {
 					isValid = false;
-					alert(supportcandy.translations.req_fields_missing);
 				}
 				break;
 			<?php
--- a/supportcandy/includes/custom-field-types/class-wpsc-cf-email.php
+++ b/supportcandy/includes/custom-field-types/class-wpsc-cf-email.php
@@ -575,15 +575,11 @@

 			case '<?php echo esc_attr( self::$slug ); ?>':
 				var val = customField.find('input').first().val().trim();
-				if (customField.hasClass('required') && !val) {
+				if (isRequired && !val) {
 					isValid = false;
-					alert(supportcandy.translations.req_fields_missing);
-					break;
-				}
-				if (val && !validateEmail(val)) {
+				} else if (val && !validateEmail(val)) {
+					showFieldError(customField, 'Invalid email address');
 					isValid = false;
-					alert('<?php esc_attr_e( 'Invalid email address!', 'supportcandy' ); ?>');
-					break;
 				}
 				break;
 			<?php
--- a/supportcandy/includes/custom-field-types/class-wpsc-cf-file-attachment-multiple.php
+++ b/supportcandy/includes/custom-field-types/class-wpsc-cf-file-attachment-multiple.php
@@ -279,10 +279,8 @@
 			?>

 			case '<?php echo esc_attr( self::$slug ); ?>':
-				var count = customField.find('input[type=hidden]').length;
-				if (customField.hasClass('required') && count === 0) {
+				if (isRequired && customField.find('input[type=hidden]').length === 0) {
 					isValid = false;
-					alert(supportcandy.translations.req_fields_missing);
 				}
 				break;
 			<?php
--- a/supportcandy/includes/custom-field-types/class-wpsc-cf-file-attachment-single.php
+++ b/supportcandy/includes/custom-field-types/class-wpsc-cf-file-attachment-single.php
@@ -285,10 +285,8 @@
 			?>

 			case '<?php echo esc_attr( self::$slug ); ?>':
-				var count = customField.find('input[type=hidden]').length;
-				if (customField.hasClass('required') && count === 0) {
+				if (isRequired && customField.find('input[type=hidden]').length === 0) {
 					isValid = false;
-					alert(supportcandy.translations.req_fields_missing);
 				}
 				break;
 			<?php
--- a/supportcandy/includes/custom-field-types/class-wpsc-cf-multi-select.php
+++ b/supportcandy/includes/custom-field-types/class-wpsc-cf-multi-select.php
@@ -526,9 +526,8 @@

 			case '<?php echo esc_attr( self::$slug ); ?>':
 				var val = customField.find('select').first().val();
-				if (customField.hasClass('required') && val.length === 0) {
+				if (isRequired && (!val || val.length === 0)) {
 					isValid = false;
-					alert(supportcandy.translations.req_fields_missing);
 				}
 				break;
 			<?php
--- a/supportcandy/includes/custom-field-types/class-wpsc-cf-number.php
+++ b/supportcandy/includes/custom-field-types/class-wpsc-cf-number.php
@@ -670,10 +670,8 @@

 			case '<?php echo esc_attr( self::$slug ); ?>':
 				var val = customField.find('input').first().val().trim();
-				if (customField.hasClass('required') && !val) {
+				if (isRequired && !val) {
 					isValid = false;
-					alert(supportcandy.translations.req_fields_missing);
-					break;
 				}
 				break;
 			<?php
--- a/supportcandy/includes/custom-field-types/class-wpsc-cf-radio-button.php
+++ b/supportcandy/includes/custom-field-types/class-wpsc-cf-radio-button.php
@@ -476,10 +476,8 @@
 			?>

 			case '<?php echo esc_attr( self::$slug ); ?>':
-				var rb = customField.find('input:checked');
-				if (customField.hasClass('required') && rb.length === 0) {
+				if (isRequired && customField.find('input:checked').length === 0) {
 					isValid = false;
-					alert(supportcandy.translations.req_fields_missing);
 				}
 				break;
 			<?php
--- a/supportcandy/includes/custom-field-types/class-wpsc-cf-single-select.php
+++ b/supportcandy/includes/custom-field-types/class-wpsc-cf-single-select.php
@@ -488,9 +488,8 @@

 			case '<?php echo esc_attr( self::$slug ); ?>':
 				var val = customField.find('select').first().val();
-				if (customField.hasClass('required') && !val) {
+				if (isRequired && !val) {
 					isValid = false;
-					alert(supportcandy.translations.req_fields_missing);
 				}
 				break;
 			<?php
--- a/supportcandy/includes/custom-field-types/class-wpsc-cf-text-field.php
+++ b/supportcandy/includes/custom-field-types/class-wpsc-cf-text-field.php
@@ -569,9 +569,8 @@

 			case '<?php echo esc_attr( self::$slug ); ?>':
 				var val = customField.find('input').first().val().trim();
-				if (customField.hasClass('required') && !val) {
+				if (isRequired && !val) {
 					isValid = false;
-					alert(supportcandy.translations.req_fields_missing);
 				}
 				break;
 			<?php
--- a/supportcandy/includes/custom-field-types/class-wpsc-cf-textarea.php
+++ b/supportcandy/includes/custom-field-types/class-wpsc-cf-textarea.php
@@ -542,9 +542,8 @@

 			case '<?php echo esc_attr( self::$slug ); ?>':
 				var val = customField.find('textarea').first().val().trim();
-				if (customField.hasClass('required') && !val) {
+				if (isRequired && !val) {
 					isValid = false;
-					alert(supportcandy.translations.req_fields_missing);
 				}
 				break;
 			<?php
--- a/supportcandy/includes/custom-field-types/class-wpsc-cf-time.php
+++ b/supportcandy/includes/custom-field-types/class-wpsc-cf-time.php
@@ -272,9 +272,8 @@

 			case '<?php echo esc_attr( self::$slug ); ?>':
 				var val = customField.find('input').first().val().trim();
-				if (customField.hasClass('required') && !val) {
+				if (isRequired && !val) {
 					isValid = false;
-					alert(supportcandy.translations.req_fields_missing);
 				}
 				break;
 			<?php
--- a/supportcandy/includes/custom-field-types/class-wpsc-cf-url.php
+++ b/supportcandy/includes/custom-field-types/class-wpsc-cf-url.php
@@ -555,15 +555,11 @@

 			case '<?php echo esc_attr( self::$slug ); ?>':
 				var val = customField.find('input').first().val().trim();
-				if (customField.hasClass('required') && !val) {
+				if (isRequired && !val) {
 					isValid = false;
-					alert(supportcandy.translations.req_fields_missing);
-					break;
-				}
-				if (val && !validateURL(val)) {
+				} else if (val && !validateURL(val)) {
+					showFieldError(customField, 'Invalid URL');
 					isValid = false;
-					alert('<?php esc_attr_e( 'Invalid URL!', 'supportcandy' ); ?>');
-					break;
 				}
 				break;
 			<?php
--- a/supportcandy/includes/custom-field-types/class-wpsc-df-additional-recipients.php
+++ b/supportcandy/includes/custom-field-types/class-wpsc-df-additional-recipients.php
@@ -175,6 +175,9 @@

 			// create ticket data for rest api.
 			add_filter( 'wpsc_rest_create_ticket', array( __CLASS__, 'set_rest_ticket_data' ), 10, 3 );
+
+			// Set custom field type.
+			add_filter( 'wpsc_default_cf_types', array( __CLASS__, 'default_cf_types' ), 4 );
 		}

 		/**
@@ -193,6 +196,21 @@
 		}

 		/**
+		 * Add default custom field type to list
+		 *
+		 * @param array $default_cf_types - default custom field types array.
+		 * @return array
+		 */
+		public static function default_cf_types( $default_cf_types ) {
+
+			$default_cf_types[ self::$slug ] = array(
+				'label' => esc_attr__( 'Additional Recipients', 'supportcandy' ),
+				'class' => __CLASS__,
+			);
+			return $default_cf_types;
+		}
+
+		/**
 		 * Add ticket search compatibility for fields of this custom field type.
 		 *
 		 * @param array  $sql - Array of sql peices that can be joined later.
@@ -258,9 +276,8 @@

 			case '<?php echo esc_attr( self::$slug ); ?>':
 				var val = customField.find('textarea').first().val().trim();
-				if (customField.hasClass('required') && !val) {
+				if (isRequired && !val) {
 					isValid = false;
-					alert(supportcandy.translations.req_fields_missing);
 				}
 				break;
 			<?php
--- a/supportcandy/includes/custom-field-types/class-wpsc-df-agent-created.php
+++ b/supportcandy/includes/custom-field-types/class-wpsc-df-agent-created.php
@@ -176,6 +176,9 @@

 			// rest api.
 			add_filter( 'wpsc_rest_prevent_ticket_data', array( __CLASS__, 'rest_prevent_ticket_data' ) );
+
+			// Set custom field type.
+			add_filter( 'wpsc_default_cf_types', array( __CLASS__, 'default_cf_types' ), 4 );
 		}

 		/**
@@ -194,6 +197,20 @@
 		}

 		/**
+		 * Add default custom field type to list
+		 *
+		 * @param array $default_cf_types - default custom field types array.
+		 * @return array
+		 */
+		public static function default_cf_types( $default_cf_types ) {
+			$default_cf_types[ self::$slug ] = array(
+				'label' => esc_attr__( 'Agent Created', 'supportcandy' ),
+				'class' => __CLASS__,
+			);
+			return $default_cf_types;
+		}
+
+		/**
 		 * Print operators for ticket form filter
 		 *
 		 * @param WPSC_Custom_Field $cf - custom field object.
--- a/supportcandy/includes/custom-field-types/class-wpsc-df-assigned-agent.php
+++ b/supportcandy/includes/custom-field-types/class-wpsc-df-assigned-agent.php
@@ -196,6 +196,9 @@
 			// Agent assign ticket to self.
 			add_action( 'wp_ajax_wpsc_self_assign_ticket', array( __CLASS__, 'self_assign_ticket' ) );
 			add_action( 'wp_ajax_nopriv_wpsc_self_assign_ticket', array( __CLASS__, 'self_assign_ticket' ) );
+
+			// Set custom field type.
+			add_filter( 'wpsc_default_cf_types', array( __CLASS__, 'default_cf_types' ), 4 );
 		}

 		/**
@@ -214,6 +217,20 @@
 		}

 		/**
+		 * Add default custom field type to list
+		 *
+		 * @param array $default_cf_types - default custom field types array.
+		 * @return array
+		 */
+		public static function default_cf_types( $default_cf_types ) {
+			$default_cf_types[ self::$slug ] = array(
+				'label' => esc_attr__( 'Assigned Agent', 'supportcandy' ),
+				'class' => __CLASS__,
+			);
+			return $default_cf_types;
+		}
+
+		/**
 		 * Print operators for ticket form filter
 		 *
 		 * @param WPSC_Custom_Field $cf - custom field object.
@@ -656,9 +673,8 @@

 			case '<?php echo esc_attr( self::$slug ); ?>':
 				var val = customField.find('select').first().val();
-				if (customField.hasClass('required') && val.length === 0) {
+				if (isRequired && (!val || val.length === 0)) {
 					isValid = false;
-					alert(supportcandy.translations.req_fields_missing);
 				}
 				break;
 			<?php
--- a/supportcandy/includes/custom-field-types/class-wpsc-df-browser.php
+++ b/supportcandy/includes/custom-field-types/class-wpsc-df-browser.php
@@ -166,6 +166,9 @@

 			// TFF!
 			add_action( 'wpsc_create_ticket_data', array( __CLASS__, 'set_create_ticket_data' ), 10, 3 );
+
+			// Set custom field type.
+			add_filter( 'wpsc_default_cf_types', array( __CLASS__, 'default_cf_types' ), 4 );
 		}

 		/**
@@ -184,6 +187,20 @@
 		}

 		/**
+		 * Add default custom field type to list
+		 *
+		 * @param array $default_cf_types - default custom field types array.
+		 * @return array
+		 */
+		public static function default_cf_types( $default_cf_types ) {
+			$default_cf_types[ self::$slug ] = array(
+				'label' => esc_attr__( 'Browser', 'supportcandy' ),
+				'class' => __CLASS__,
+			);
+			return $default_cf_types;
+		}
+
+		/**
 		 * Print operators for ticket form filter
 		 *
 		 * @param WPSC_Custom_Field $cf - custom field object.
--- a/supportcandy/includes/custom-field-types/class-wpsc-df-category.php
+++ b/supportcandy/includes/custom-field-types/class-wpsc-df-category.php
@@ -175,6 +175,9 @@
 			// Ticket model.
 			add_filter( 'wpsc_ticket_joins', array( __CLASS__, 'ticket_join' ), 10, 2 );
 			add_filter( 'wpsc_archive_ticket_joins', array( __CLASS__, 'ticket_join' ), 10, 2 );
+
+			// Set custom field type.
+			add_filter( 'wpsc_default_cf_types', array( __CLASS__, 'default_cf_types' ), 4 );
 		}

 		/**
@@ -193,6 +196,20 @@
 		}

 		/**
+		 * Add default custom field type to list
+		 *
+		 * @param array $default_cf_types - default custom field types array.
+		 * @return array
+		 */
+		public static function default_cf_types( $default_cf_types ) {
+			$default_cf_types[ self::$slug ] = array(
+				'label' => esc_attr__( 'Category', 'supportcandy' ),
+				'class' => __CLASS__,
+			);
+			return $default_cf_types;
+		}
+
+		/**
 		 * Print operators for ticket form filter
 		 *
 		 * @param WPSC_Custom_Field $cf - custom field object.
@@ -451,9 +468,8 @@

 			case '<?php echo esc_attr( self::$slug ); ?>':
 				var val = customField.find('select').first().val();
-				if (customField.hasClass('required') && !val) {
+				if (isRequired && !val) {
 					isValid = false;
-					alert(supportcandy.translations.req_fields_missing);
 				}
 				break;
 			<?php
--- a/supportcandy/includes/custom-field-types/class-wpsc-df-customer-email.php
+++ b/supportcandy/includes/custom-field-types/class-wpsc-df-customer-email.php
@@ -167,6 +167,9 @@

 			// TFF!
 			add_action( 'wpsc_js_validate_ticket_form', array( __CLASS__, 'js_validate_ticket_form' ) );
+
+			// Set custom field type.
+			add_filter( 'wpsc_default_cf_types', array( __CLASS__, 'default_cf_types' ), 4 );
 		}

 		/**
@@ -185,6 +188,21 @@
 		}

 		/**
+		 * Add default custom field type to list
+		 *
+		 * @param array $default_cf_types - default custom field types array.
+		 * @return array
+		 */
+		public static function default_cf_types( $default_cf_types ) {
+
+			$default_cf_types[ self::$slug ] = array(
+				'label' => esc_attr__( 'Customer Email', 'supportcandy' ),
+				'class' => __CLASS__,
+			);
+			return $default_cf_types;
+		}
+
+		/**
 		 * Print ticket form field
 		 *
 		 * @param WPSC_Custom_Field $cf - Custom field object.
@@ -243,15 +261,11 @@

 			case '<?php echo esc_attr( self::$slug ); ?>':
 				var val = customField.find('input').first().val().trim();
-				if (customField.hasClass('required') && !val) {
+				if (isRequired && !val) {
 					isValid = false;
-					alert(supportcandy.translations.req_fields_missing);
-					break;
-				}
-				if (val && !validateEmail(val)) {
+				} else if (val && !validateEmail(val)) {
+					showFieldError(customField, '<?php esc_attr_e( 'Invalid email address!', 'supportcandy' ); ?>');
 					isValid = false;
-					alert('<?php esc_attr_e( 'Invalid email address!', 'supportcandy' ); ?>');
-					break;
 				}
 				break;
 			<?php
--- a/supportcandy/includes/custom-field-types/class-wpsc-df-customer-name.php
+++ b/supportcandy/includes/custom-field-types/class-wpsc-df-customer-name.php
@@ -166,6 +166,9 @@

 			// TFF!
 			add_action( 'wpsc_js_validate_ticket_form', array( __CLASS__, 'js_validate_ticket_form' ) );
+
+			// Set custom field type.
+			add_filter( 'wpsc_default_cf_types', array( __CLASS__, 'default_cf_types' ), 4 );
 		}

 		/**
@@ -184,6 +187,20 @@
 		}

 		/**
+		 * Add default custom field type to list
+		 *
+		 * @param array $default_cf_types - default custom field types array.
+		 * @return array
+		 */
+		public static function default_cf_types( $default_cf_types ) {
+			$default_cf_types[ self::$slug ] = array(
+				'label' => esc_attr__( 'Customer Name', 'supportcandy' ),
+				'class' => __CLASS__,
+			);
+			return $default_cf_types;
+		}
+
+		/**
 		 * Print ticket form field
 		 *
 		 * @param WPSC_Custom_Field $cf - Custom field object.
@@ -240,9 +257,8 @@

 			case '<?php echo esc_attr( self::$slug ); ?>':
 				var val = customField.find('input').first().val().trim();
-				if (customField.hasClass('required') && !val) {
+				if (isRequired && !val) {
 					isValid = false;
-					alert(supportcandy.translations.req_fields_missing);
 				}
 				break;
 			<?php
--- a/supportcandy/includes/custom-field-types/class-wpsc-df-customer.php
+++ b/supportcandy/includes/custom-field-types/class-wpsc-df-customer.php
@@ -190,6 +190,9 @@

 			// Customer filter autocomplete.
 			add_action( 'wp_ajax_wpsc_customer_filter_autocomplete', array( __CLASS__, 'customer_filter_autocomplete' ) );
+
+			// Set custom field type.
+			add_filter( 'wpsc_default_cf_types', array( __CLASS__, 'default_cf_types' ), 4 );
 		}

 		/**
@@ -208,6 +211,20 @@
 		}

 		/**
+		 * Add default custom field type to list
+		 *
+		 * @param array $default_cf_types - default custom field types array.
+		 * @return array
+		 */
+		public static function default_cf_types( $default_cf_types ) {
+			$default_cf_types[ self::$slug ] = array(
+				'label' => esc_attr__( 'Customer', 'supportcandy' ),
+				'class' => __CLASS__,
+			);
+			return $default_cf_types;
+		}
+
+		/**
 		 * Print operators for ticket form filter
 		 *
 		 * @param WPSC_Custom_Field $cf - custom field object.
--- a/supportcandy/includes/custom-field-types/class-wpsc-df-date-closed.php
+++ b/supportcandy/includes/custom-field-types/class-wpsc-df-date-closed.php
@@ -163,6 +163,9 @@

 			// Get object of this class.
 			add_filter( 'wpsc_load_ref_classes', array( __CLASS__, 'load_ref_class' ) );
+
+			// Set custom field type.
+			add_filter( 'wpsc_default_cf_types', array( __CLASS__, 'default_cf_types' ), 4 );
 		}

 		/**
@@ -181,6 +184,20 @@
 		}

 		/**
+		 * Add default custom field type to list
+		 *
+		 * @param array $default_cf_types - default custom field types array.
+		 * @return array

ModSecurity Protection Against This CVE

Here you will find our ModSecurity compatible rule to protect against this particular CVE.

ModSecurity
SecRule REQUEST_URI "@streq /wp-admin/admin-ajax.php" "id:20260001,phase:2,deny,status:403,chain,log,msg:'CVE-2026-54826 via SupportCandy AJAX ticket action',severity:'CRITICAL',tag:'CVE-2026-54826'"
SecRule ARGS_POST:action "@rx ^wpsc_it_" "chain"
SecRule ARGS_POST:ticket_id "@rx ^[0-9]+$" "t:none"

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