Skip to content

Commit a5e5d97

Browse files
Release 2.5.13: save-validation and assignment cookie path fixes
Port admin save fail-closed behavior and the assignment proof cookie path fix so admin-ajax receives signed cookies on split home/siteurl installs. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 33e5d4e commit a5e5d97

5 files changed

Lines changed: 99 additions & 11 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22

33
`readme.txt` remains the canonical WordPress.org release history for this plugin. This file mirrors the shipped release notes in a GitHub-friendly format.
44

5+
## 2.5.13
6+
7+
> Admin save-validation hardening and assignment proof cookie path fix for split home/siteurl installs. JS `VERSION` synced to 2.5.13.
8+
9+
- Fix: **Admin save fail-closed** for invalid existing variants/goals (ported from ElementTest-WP PR #4).
10+
- Fix: **Assignment proof cookie path** set to `/` so admin-ajax.php receives the signed cookie on split home/siteurl installs.
11+
512
## 2.5.12
613

714
> Update-source visibility: each real GitHub release check records its outcome (`ok`, `http_<code>`, `network_error`, etc.) in the non-autoloaded `elementtest_update_status` option. The Plugins-screen row shows a colored status line ("Updates: GitHub ✓ latest 2.5.12, checked 2 hours ago" / "check failing"), and a warning notice appears on plugins.php for users with `update_plugins` when checks fail. On installs updating from a private mirror, the notice distinguishes a missing access token from an expired/revoked one; the token value is never rendered anywhere. JS `VERSION` synced to 2.5.12.

‎assets/js/frontend.js‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ try {
3131
// Constants & State
3232
// =========================================================================
3333

34-
var VERSION = '2.5.12';
34+
var VERSION = '2.5.13';
3535
var OBSERVER_TIMEOUT = 8000; // Max time (ms) to wait for elements via MutationObserver.
3636
var ANTIFLICKER_TIMEOUT = 3000; // Max time (ms) before forcing anti-flicker removal.
3737

‎elementtest-pro.php‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Plugin Name: ElementTest Pro
44
* Plugin URI: https://github.com/DougState/elementtest-pro
55
* Description: A/B test various elements (CSS, copy, JS, images) of your pages and track conversion data to measure performance.
6-
* Version: 2.5.12
6+
* Version: 2.5.13
77
* Requires at least: 5.6
88
* Tested up to: 7.0
99
* Requires PHP: 7.4
@@ -24,7 +24,7 @@
2424
}
2525

2626
// Define plugin constants
27-
define( 'ELEMENTTEST_VERSION', '2.5.12' );
27+
define( 'ELEMENTTEST_VERSION', '2.5.13' );
2828
define( 'ELEMENTTEST_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
2929
define( 'ELEMENTTEST_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
3030
define( 'ELEMENTTEST_PLUGIN_BASENAME', plugin_basename( __FILE__ ) );

‎includes/class-ajax-handler.php‎

Lines changed: 83 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,10 @@ private function set_assignment_proof_cookie( $test_id, $variant_id, $user_hash
288288
$expires_at = time() + $ttl;
289289
$token = $this->create_assignment_token( $test_id, $variant_id, $user_hash, $expires_at );
290290
$name = $this->get_assignment_cookie_name( $test_id );
291-
$path = ( defined( 'COOKIEPATH' ) && COOKIEPATH ) ? COOKIEPATH : '/';
291+
// Public tracking writes go through admin-ajax.php, which may live
292+
// outside COOKIEPATH on split home/siteurl installs. Scope the signed
293+
// proof cookie broadly so the AJAX verifier can receive it.
294+
$path = '/';
292295
$options = array(
293296
'expires' => $expires_at,
294297
'path' => $path,
@@ -540,6 +543,11 @@ public function save_test() {
540543
$v_control = isset( $variant_data['is_control'] ) ? absint( $variant_data['is_control'] ) : 0;
541544

542545
if ( empty( $v_name ) ) {
546+
if ( $v_id > 0 ) {
547+
wp_send_json_error(
548+
array( 'message' => __( 'Variant name is required.', 'elementtest-pro' ) )
549+
);
550+
}
543551
continue;
544552
}
545553

@@ -553,23 +561,50 @@ public function save_test() {
553561

554562
if ( $v_id > 0 ) {
555563
// Update existing variant (preserves variant_id for event data).
556-
$wpdb->update(
564+
$variant_exists = $wpdb->get_var(
565+
$wpdb->prepare(
566+
"SELECT variant_id FROM {$variants_table} WHERE variant_id = %d AND test_id = %d",
567+
$v_id,
568+
$test_id
569+
)
570+
);
571+
572+
if ( ! $variant_exists ) {
573+
wp_send_json_error(
574+
array( 'message' => __( 'Variant not found for this test.', 'elementtest-pro' ) )
575+
);
576+
}
577+
578+
$variant_result = $wpdb->update(
557579
$variants_table,
558580
$v_data,
559581
array( 'variant_id' => $v_id, 'test_id' => $test_id ),
560582
array( '%d', '%s', '%s', '%d', '%d' ),
561583
array( '%d', '%d' )
562584
);
585+
586+
if ( false === $variant_result ) {
587+
wp_send_json_error(
588+
array( 'message' => __( 'Failed to update variant.', 'elementtest-pro' ) )
589+
);
590+
}
563591
$submitted_variant_ids[] = $v_id;
564592
} else {
565593
// Insert new variant.
566594
$v_data['created_at'] = current_time( 'mysql', true );
567-
$wpdb->insert(
595+
$variant_result = $wpdb->insert(
568596
$variants_table,
569597
$v_data,
570598
array( '%d', '%s', '%s', '%d', '%d', '%s' )
571599
);
572-
$submitted_variant_ids[] = $wpdb->insert_id;
600+
601+
if ( false === $variant_result || ! $wpdb->insert_id ) {
602+
wp_send_json_error(
603+
array( 'message' => __( 'Failed to create variant.', 'elementtest-pro' ) )
604+
);
605+
}
606+
607+
$submitted_variant_ids[] = absint( $wpdb->insert_id );
573608
}
574609
}
575610

@@ -605,11 +640,25 @@ public function save_test() {
605640
$g_revenue = isset( $goal_data['revenue_value'] ) ? floatval( $goal_data['revenue_value'] ) : 0.00;
606641

607642
if ( empty( $g_name ) || empty( $g_type ) ) {
643+
$is_blank_new_goal = 0 === $g_id
644+
&& empty( $g_name )
645+
&& empty( $g_selector )
646+
&& empty( $g_url )
647+
&& empty( $g_event )
648+
&& 0.0 === $g_revenue;
649+
650+
if ( ! $is_blank_new_goal ) {
651+
wp_send_json_error(
652+
array( 'message' => __( 'Conversion goal name and trigger type are required.', 'elementtest-pro' ) )
653+
);
654+
}
608655
continue;
609656
}
610657

611658
if ( ! in_array( $g_type, $this->valid_trigger_types, true ) ) {
612-
continue;
659+
wp_send_json_error(
660+
array( 'message' => __( 'Invalid conversion goal trigger type.', 'elementtest-pro' ) )
661+
);
613662
}
614663

615664
// Determine trigger_event based on type.
@@ -631,13 +680,33 @@ public function save_test() {
631680

632681
if ( $g_id > 0 ) {
633682
// Update existing conversion goal.
634-
$wpdb->update(
683+
$goal_exists = $wpdb->get_var(
684+
$wpdb->prepare(
685+
"SELECT conversion_id FROM {$conversions_table} WHERE conversion_id = %d AND test_id = %d",
686+
$g_id,
687+
$test_id
688+
)
689+
);
690+
691+
if ( ! $goal_exists ) {
692+
wp_send_json_error(
693+
array( 'message' => __( 'Conversion goal not found for this test.', 'elementtest-pro' ) )
694+
);
695+
}
696+
697+
$goal_result = $wpdb->update(
635698
$conversions_table,
636699
$g_data,
637700
array( 'conversion_id' => $g_id, 'test_id' => $test_id ),
638701
array( '%d', '%s', '%s', '%s', '%s', '%f' ),
639702
array( '%d', '%d' )
640703
);
704+
705+
if ( false === $goal_result ) {
706+
wp_send_json_error(
707+
array( 'message' => __( 'Failed to update conversion goal.', 'elementtest-pro' ) )
708+
);
709+
}
641710
$submitted_goal_ids[] = $g_id;
642711
} else {
643712
// Insert new conversion goal.
@@ -651,7 +720,14 @@ public function save_test() {
651720
error_log( '[ElementTest] Goal insert failed. DB Error: ' . $wpdb->last_error ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Gated behind WP_DEBUG; silent in production.
652721
error_log( '[ElementTest] Goal data: ' . wp_json_encode( $g_data ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Gated behind WP_DEBUG; silent in production.
653722
}
654-
$submitted_goal_ids[] = $wpdb->insert_id;
723+
724+
if ( false === $goal_result || ! $wpdb->insert_id ) {
725+
wp_send_json_error(
726+
array( 'message' => __( 'Failed to create conversion goal.', 'elementtest-pro' ) )
727+
);
728+
}
729+
730+
$submitted_goal_ids[] = absint( $wpdb->insert_id );
655731
}
656732
}
657733

‎readme.txt‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ Contributors: Doug Wagner
33
Tags: ab-testing, split-testing, conversion, optimization, analytics
44
Requires at least: 5.6
55
Tested up to: 7.0
6-
Stable tag: 2.5.12
6+
Stable tag: 2.5.13
77
Requires PHP: 7.4
88
License: GPLv2 or later
99
License URI: https://www.gnu.org/licenses/gpl-2.0.html
@@ -63,6 +63,11 @@ All testing data is stored in your WordPress database. No external services are
6363

6464
== Changelog ==
6565

66+
= 2.5.13 =
67+
* Fix: Admin test saves now fail closed when an existing variant or conversion goal row is invalid or cannot be written. Previously, malformed rows could be silently skipped and the editor could still show "Test saved successfully."
68+
* Fix: Assignment proof cookie path is now `/` so admin-ajax.php can receive the signed cookie on split home/siteurl installs.
69+
* Internal: JS `VERSION` constant in `assets/js/frontend.js` synced to 2.5.13.
70+
6671
= 2.5.12 =
6772
* Add: update-source status indicator on the Plugins screen row (last check result and latest available version) and a warning notice when the release check fails — so update problems are never silent. On private mirrors, the notice also flags a missing or expired access token; the token value itself is never displayed.
6873

0 commit comments

Comments
 (0)