PluginProbe ʕ •ᴥ•ʔ
WP STAGING – WordPress Backup, Restore, Migration & Clone / 4.9.0
WP STAGING – WordPress Backup, Restore, Migration & Clone v4.9.0
4.9.1 4.9.0 4.8.1 trunk 3.0.0 3.0.1 3.0.2 3.0.3 3.0.4 3.0.5 3.0.6 3.1.0 3.1.1 3.1.2 3.1.3 3.1.4 3.10.0 3.2.0 3.3.1 3.3.2 3.3.3 3.4.1 3.4.3 3.5.0 3.6.0 3.7.1 3.8.0 3.8.1 3.8.2 3.8.3 3.8.4 3.8.5 3.8.6 3.8.7 3.9.0 3.9.1 3.9.2 3.9.3 3.9.4 4.0.0 4.1.0 4.1.1 4.1.2 4.1.3 4.1.4 4.2.0 4.2.1 4.3.0 4.3.1 4.3.2 4.4.0 4.5.0 4.6.0 4.7.0 4.7.1 4.7.2 4.7.3 4.8.0
wp-staging / Framework / BackgroundProcessing / FeatureDetection.php
wp-staging / Framework / BackgroundProcessing Last commit date
Exceptions 5 years ago Job 1 month ago Action.php 2 years ago BackgroundProcessingServiceProvider.php 1 week ago Demo.php 4 years ago FeatureDetection.php 3 months ago Queue.php 1 month ago QueueActionAware.php 6 months ago QueueProcessor.php 1 month ago WithQueueAwareness.php 1 month ago
FeatureDetection.php
236 lines
1 <?php
2
3 /**
4 * Provides feature detection for the Queue system.
5 *
6 * @package WPStaging\Framework\BackgroundProcessing
7 */
8
9 namespace WPStaging\Framework\BackgroundProcessing;
10
11 use WP_Error;
12 use WPStaging\Framework\Adapter\WpAdapter;
13 use WPStaging\Framework\Network\HttpBasicAuth;
14 use WPStaging\Framework\Notices\Notices;
15
16 use function WPStaging\functions\debug_log;
17
18 /**
19 * Class FeatureDetection
20 *
21 * @package WPStaging\Framework\BackgroundProcessing
22 */
23 class FeatureDetection
24 {
25 use HttpBasicAuth;
26
27 /** @var string */
28 const ACTION_AJAX_TEST = 'wpstg_ajax_test_action';
29
30 /** @var string */
31 const ACTION_AJAX_SUPPORT_FEATURE_DETECTION = 'wpstg_q_ajax_support_feature_detection';
32
33 /** @var string */
34 const FILTER_HTTPS_LOCAL_SSL_VERIFY = 'https_local_ssl_verify';
35
36 /** @var string */
37 const AJAX_OPTION_NAME = 'wpstg_q_feature_detection_ajax_available';
38
39 /** @var string */
40 const AJAX_REQUEST_QUERY_VAR = 'wpstg_q_ajax_check';
41
42 /**
43 * A cache property to store the result of the AJAX support check.
44 *
45 * @var bool
46 */
47 protected $isAjaxAvailableCache;
48
49 /**
50 * Returns whether the AJAX based queue processing system is available or not.
51 *
52 * @param bool $showAdminNotice Whether to show an admin notice on missing support or not.
53 *
54 * @return bool Whether the AJAX based queue processing system is available or not.
55 */
56 public function isAjaxAvailable($showAdminNotice = true)
57 {
58 //debug_log('isAjaxAvailable start');
59 if ($this->isAjaxAvailableCache === null) {
60 // Run this check only on Admin UI and on PHP initial state.
61 // TODO: inject WpAdapter using DI
62 $notRightContext = wp_installing() || (defined('REST_REQUEST') && REST_REQUEST) || (new WpAdapter())->doingAjax()
63 || wp_doing_cron() || !is_admin();
64
65 if ($notRightContext) {
66 // Default to say that it's supported if we cannot exclude it.
67 debug_log(sprintf(
68 "isAjaxAvailable not right context: Is WP Installing? %s - Is Rest? %s - Is Ajax? %s - Is Cron? %s - Is admin? %s",
69 wp_installing() ? 'true' : 'false',
70 (defined('REST_REQUEST') && REST_REQUEST) ? 'true' : 'false',
71 (new WpAdapter())->doingAjax() ? 'true' : 'false',
72 wp_doing_cron() ? 'true' : 'false',
73 is_admin() ? 'true' : 'false'
74 ), 'info', false);
75 return true;
76 }
77
78 $availableOptionValue = get_option(self::AJAX_OPTION_NAME, null);
79
80 if (!in_array($availableOptionValue, ['y', 'n'], true)) {
81 $available = $this->runAjaxFeatureTest();
82 $availableOptionValue = $available ? 'y' : 'n';
83 update_option(self::AJAX_OPTION_NAME, $availableOptionValue, false);
84 }
85
86 $this->isAjaxAvailableCache = $availableOptionValue === 'y';
87 }
88
89 if (!$this->isAjaxAvailableCache && $showAdminNotice) {
90 add_action(Notices::ACTION_ADMIN_NOTICES, [$this, 'ajaxSupportMissingAdminNotice']);
91 }
92
93 //debug_log('isAjaxAvailable end. Result: ' . $this->isAjaxAvailableCache);
94
95 return $this->isAjaxAvailableCache;
96 }
97
98 /**
99 * Runs the AJAX support feature detection test.
100 *
101 * This method will fire a non-blocking POST request
102 * to the `admin-ajax` endpoint.
103 * In response, the `updateAjaxTestOption` will update
104 * the flag option value and set it to `y` or not set it at all.
105 * This method will wait for some time for its counter-part, the
106 * `updateAjaxTestOption` running in the other request, to update
107 * the option. If the time runs out and the option is not there,
108 * the we know AJAX is either not working or not reliable enough.
109 * The method uses an option, and not a transient, as flag value to
110 * be able to force re-fetch it from the database.
111 *
112 * @return bool Whether the AJAX-based system is supported or not.
113 *
114 * @see FeatureDetection::updateAjaxTestOption()
115 */
116 public function runAjaxFeatureTest()
117 {
118 // Start from a clean state.
119 debug_log('Starting from a clean state...', 'info', false);
120 delete_option(self::AJAX_OPTION_NAME);
121
122 $ajaxUrl = add_query_arg([
123 'action' => self::ACTION_AJAX_TEST,
124 '_ajax_nonce' => wp_create_nonce(self::ACTION_AJAX_TEST),
125 ], admin_url('admin-ajax.php'));
126
127 $hash = md5(uniqid(__CLASS__, true));
128
129 debug_log('Sending request to: ' . $ajaxUrl, 'info', false);
130
131 $response = wp_remote_post(esc_url_raw($ajaxUrl), [
132 'headers' => array_merge(
133 ['X-WPSTG-Request' => self::ACTION_AJAX_TEST],
134 $this->getHttpAuthHeaders()
135 ),
136 'blocking' => false,
137 'timeout' => 0.01,
138 'cookies' => !empty($_COOKIE) ? $_COOKIE : [],
139 'sslverify' => apply_filters(self::FILTER_HTTPS_LOCAL_SSL_VERIFY, false),
140 'body' => [self::AJAX_OPTION_NAME => $hash],
141 ]);
142
143 debug_log(wp_json_encode($response), 'info', false);
144
145 if ($response instanceof WP_Error) {
146 return false;
147 }
148
149 $test = static function () {
150 // Run a direct query to force the re-fetch and not hit the cache.
151 global $wpdb;
152 $fetched = $wpdb->get_var(
153 $wpdb->prepare(
154 "SELECT `option_value` from `{$wpdb->options}` WHERE `option_name` = '%s' AND `option_value` = 'y'",
155 self::AJAX_OPTION_NAME
156 )
157 );
158
159 debug_log('Fetched is equal to: ' . $fetched, 'info', false);
160 debug_log('Fetched is equal to (get_option): ' . get_option(self::AJAX_OPTION_NAME), 'info', false);
161
162 return $fetched === 'y';
163 };
164
165 $waited = 0;
166 $waitStep = .5 * 1e6; // 0.5 second
167 $timeout = 10 * 1e6; // 10 seconds
168
169 do {
170 debug_log('runAjaxFeatureTest waited ' . number_format($waited / 1e6, 1) . ' seconds...', 'info', false);
171 $waited += $waitStep;
172 usleep((int)$waitStep);
173
174 if ($test()) {
175 // Look no further, it worked.
176 debug_log('runAjaxFeatureTest worked', 'info', false);
177 return true;
178 }
179 } while ($waited <= $timeout);
180
181 // We waited enough: either the AJAX system is not available or is not reliable.
182 debug_log('runAjaxFeatureTest did not work', 'info', false);
183 return false;
184 }
185
186 /**
187 * Writes `y` to the feature detection option.
188 *
189 * This method will be called in response to the AJAX request
190 * fired by the `runAjaxFeatureTest` method.
191 * That method will wait, in its PHP process, for this method to
192 * udpate the option value and deem the AJAX support as "working".
193 *
194 * @return void The method does not return any value and will have the
195 * side effect of updating the option.
196 *
197 * @see FeatureDetection::runAjaxFeatureTest()
198 */
199 public function updateAjaxTestOption()
200 {
201 debug_log('Running updateAjaxTestOption', 'info', false);
202 check_ajax_referer(self::ACTION_AJAX_TEST);
203
204 if (!update_option(self::AJAX_OPTION_NAME, 'y', false)) {
205 debug_log('updateAjaxTestOption update_option returned false', 'info', false);
206 }
207
208 debug_log('Complete updateAjaxTestOption. New value: ' . get_option(self::AJAX_OPTION_NAME), 'info', false);
209 }
210
211 /**
212 * Displays a dismissible admin notice to let the user know the BG Processing system will not
213 * perform at its best due to lack of AJAX support.
214 *
215 * @return void The method will have the side effect of echoing HTML to the page.
216 */
217 public function ajaxSupportMissingAdminNotice()
218 {
219 $message = __(
220 'WP STAGING Background Processing system cannot use AJAX: this will prevent it from performing at its best.',
221 'wp-staging'
222 );
223 $checkLink = sprintf(
224 '<a href="%s">%s</a>',
225 esc_url(admin_url('/?' . self::AJAX_REQUEST_QUERY_VAR . '=1')),
226 __('Click here to check again now.', 'wp-staging')
227 );
228 ?>
229 <div class="notice notice-warning is-dismissible wpstg__notice wpstg__notice--warning">
230 <p><?php echo wp_kses_post($message); ?></p>
231 <p><?php echo wp_kses_post($checkLink); ?></p>
232 </div>
233 <?php
234 }
235 }
236