Skip to content

Commit b01fc9b

Browse files
authored
fix: add AnsPress compatibility guard for wu-ajax product search (issue #171) (#396)
AnsPress registers an AJAX dispatcher on the WordPress `init` hook (AP_Ajax::init()) that calls die() after processing any request it recognises. Ultimate Multisite's Light Ajax also fires on `init` (when wu-when=init) and relies on reaching its own action hooks without interference. When both plugins are active, AnsPress's handler runs first and terminates the request before Ultimate Multisite can serve the product-search JSON, producing a fatal/empty-response error in the membership product-selection modal (Add Product). Fix: add `WP_Ultimo\Compat\AnsPress_Compat` which detects wu-ajax requests and removes AnsPress's conflicting init-time AJAX hooks (both the class-based AP_Ajax::init() used in AnsPress 4.x and the legacy anspress_ajax() function used in older versions) before they can intercept and terminate the request. The guard is a no-op when AnsPress is not active, so there is no performance impact for sites that do not use AnsPress. Also updates the known-incompatibilities wiki page to document that this conflict is now automatically resolved.
1 parent 922d85c commit b01fc9b

3 files changed

Lines changed: 182 additions & 0 deletions

File tree

.wiki/known-incompatibilities-with-other-plugins.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,5 @@ Here is a list of them and what you should do in these cases:
2121
* Anti-Splog: Although less powerful, this is also _built into Ultimate Multisite._
2222

2323
**Note** : All other WPMU Dev plugins can be used normally alongside Ultimate Multisite. Examples include _Smush_ , _Forminator_ , _Defender,_ etc.
24+
25+
**AnsPress – Question and Answer** AnsPress registers an AJAX dispatcher on the WordPress `init` hook that intercepts requests and calls `die()` after processing. This conflicts with Ultimate Multisite's Light Ajax system (which also fires on `init`) and causes a fatal error when selecting a product to add to a membership. As of Ultimate Multisite 2.4.3, a compatibility fix is included that automatically removes AnsPress's conflicting hook during Ultimate Multisite AJAX requests. No manual action is required — both plugins can be used together.

inc/class-wp-ultimo.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -661,6 +661,13 @@ function () {
661661

662662
\WP_Ultimo\Compat\Honeypot_Compat::get_instance();
663663

664+
/*
665+
* AnsPress compatibility — prevents AnsPress from intercepting
666+
* wu-ajax requests and causing a fatal error in the membership
667+
* product-selection modal.
668+
*/
669+
\WP_Ultimo\Compat\AnsPress_Compat::get_instance();
670+
664671
/*
665672
* WooCommerce Subscriptions compatibility
666673
*/
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
<?php
2+
/**
3+
* AnsPress Compatibility Layer
4+
*
5+
* Prevents AnsPress from intercepting Ultimate Multisite's Light Ajax
6+
* requests and causing a fatal error when selecting a product to add
7+
* to a membership.
8+
*
9+
* AnsPress hooks an AJAX dispatcher onto the `init` action (via
10+
* AP_Ajax::init()) that calls die() after processing any request it
11+
* recognises. Ultimate Multisite's Light Ajax also fires on `init`
12+
* (when wu-when=init) and relies on reaching its own action hooks
13+
* without interference. When both plugins are active, AnsPress's
14+
* handler runs first and terminates the request before Ultimate
15+
* Multisite can serve the product-search JSON, producing a fatal /
16+
* empty-response error in the membership product-selection modal.
17+
*
18+
* The fix: when a wu-ajax request is detected, remove AnsPress's
19+
* init-time AJAX handler so Ultimate Multisite can complete normally.
20+
*
21+
* @package WP_Ultimo
22+
* @subpackage Compat/AnsPress_Compat
23+
* @since 2.4.3
24+
*/
25+
26+
namespace WP_Ultimo\Compat;
27+
28+
// Exit if accessed directly
29+
defined('ABSPATH') || exit;
30+
31+
/**
32+
* AnsPress compatibility class.
33+
*
34+
* @since 2.4.3
35+
*/
36+
class AnsPress_Compat {
37+
38+
use \WP_Ultimo\Traits\Singleton;
39+
40+
/**
41+
* Instantiate the necessary hooks.
42+
*
43+
* We hook as early as possible (plugins_loaded priority 5) so we
44+
* can remove AnsPress's init handler before it fires.
45+
*
46+
* @since 2.4.3
47+
* @return void
48+
*/
49+
public function init(): void {
50+
51+
/*
52+
* Only act when this is a wu-ajax request.
53+
* phpcs:ignore WordPress.Security.NonceVerification.Recommended
54+
*/
55+
if ( ! isset($_REQUEST['wu-ajax'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
56+
return;
57+
}
58+
59+
/*
60+
* Guard: only apply the fix when AnsPress is actually active.
61+
* We check for the AP_Ajax class (present in all AnsPress
62+
* versions ≥ 4.x) and the legacy anspress_ajax() function
63+
* (versions < 4.x).
64+
*/
65+
if ( ! class_exists('AP_Ajax') && ! function_exists('anspress_ajax')) {
66+
return;
67+
}
68+
69+
/*
70+
* Remove AnsPress's init-time AJAX dispatcher so it cannot
71+
* intercept and terminate our wu-ajax request.
72+
*
73+
* AnsPress 4.x registers AP_Ajax::init() on `init` at
74+
* priority 1. We remove it here (before `init` fires) so
75+
* Ultimate Multisite's Light Ajax handler can run cleanly.
76+
*/
77+
add_action(
78+
'plugins_loaded',
79+
[$this, 'remove_anspress_ajax_hooks'],
80+
PHP_INT_MAX
81+
);
82+
}
83+
84+
/**
85+
* Removes AnsPress's AJAX hooks that conflict with wu-ajax requests.
86+
*
87+
* Called late on plugins_loaded (after AnsPress has registered its
88+
* own hooks) so we can safely remove them before `init` fires.
89+
*
90+
* @since 2.4.3
91+
* @return void
92+
*/
93+
public function remove_anspress_ajax_hooks(): void {
94+
95+
/*
96+
* AnsPress 4.x — class-based AJAX handler.
97+
*
98+
* AP_Ajax::init() is registered on `init` at priority 1.
99+
* It calls ap_send_json() / die() after handling any request
100+
* it recognises, which terminates our wu-ajax response early.
101+
*/
102+
if (class_exists('AP_Ajax')) {
103+
$this->remove_class_action('init', 'AP_Ajax', 'init', 1);
104+
$this->remove_class_action('init', 'AP_Ajax', 'init', 2);
105+
$this->remove_class_action('init', 'AP_Ajax', 'init', 10);
106+
}
107+
108+
/*
109+
* AnsPress < 4.x — function-based AJAX handler.
110+
*
111+
* Older versions registered a global anspress_ajax() function
112+
* directly on `init`.
113+
*/
114+
if (function_exists('anspress_ajax')) {
115+
remove_action('init', 'anspress_ajax', 1);
116+
remove_action('init', 'anspress_ajax', 2);
117+
remove_action('init', 'anspress_ajax', 10);
118+
}
119+
}
120+
121+
/**
122+
* Removes an action registered by a class instance or statically.
123+
*
124+
* WordPress stores hook callbacks keyed by a string that includes
125+
* the class name and method. When the callback was registered via
126+
* an instance (not a static call) we cannot use remove_action()
127+
* directly because we don't have the original instance. This
128+
* helper iterates the global $wp_filter array to find and remove
129+
* the matching entry.
130+
*
131+
* @since 2.4.3
132+
*
133+
* @param string $tag The action hook name.
134+
* @param string $class The fully-qualified class name.
135+
* @param string $method The method name.
136+
* @param int $priority The priority the action was registered at.
137+
* @return void
138+
*/
139+
protected function remove_class_action(string $tag, string $class, string $method, int $priority): void {
140+
141+
global $wp_filter;
142+
143+
if ( ! isset($wp_filter[ $tag ][ $priority ])) {
144+
return;
145+
}
146+
147+
foreach ($wp_filter[ $tag ][ $priority ] as $hook_key => $hook_data) {
148+
$callback = $hook_data['function'];
149+
150+
// Static call: ['ClassName', 'method']
151+
if (
152+
is_array($callback)
153+
&& is_string($callback[0])
154+
&& $callback[0] === $class
155+
&& $callback[1] === $method
156+
) {
157+
unset($wp_filter[ $tag ][ $priority ][ $hook_key ]);
158+
return;
159+
}
160+
161+
// Instance call: [$object, 'method']
162+
if (
163+
is_array($callback)
164+
&& is_object($callback[0])
165+
&& is_a($callback[0], $class)
166+
&& $callback[1] === $method
167+
) {
168+
unset($wp_filter[ $tag ][ $priority ][ $hook_key ]);
169+
return;
170+
}
171+
}
172+
}
173+
}

0 commit comments

Comments
 (0)