| 1 |
<?php |
| 2 |
// @codingStandardsIgnoreFile |
| 3 |
/** |
| 4 |
* Frontend asset utilities. |
| 5 |
* |
| 6 |
* Copyright: © 2009-2011 |
| 7 |
* {@link http://websharks-inc.com/ WebSharks, Inc.} |
| 8 |
* (coded in the USA) |
| 9 |
* |
| 10 |
* Released under the terms of the GNU General Public License. |
| 11 |
* You should have received a copy of the GNU General Public License, |
| 12 |
* along with this software. In the main directory, see: /licensing/ |
| 13 |
* If not, see: {@link http://www.gnu.org/licenses/}. |
| 14 |
* |
| 15 |
* @package s2Member\Utilities |
| 16 |
* @since 260903.0437 |
| 17 |
*/ |
| 18 |
if(!defined('WPINC')) |
| 19 |
exit("Do not access this file directly."); |
| 20 |
|
| 21 |
if(!class_exists('c_ws_plugin__s2member_utils_assets')) |
| 22 |
{ |
| 23 |
/** |
| 24 |
* Frontend asset utilities. |
| 25 |
* |
| 26 |
* @package s2Member\Utilities |
| 27 |
* @since 260903.0437 |
| 28 |
*/ |
| 29 |
class c_ws_plugin__s2member_utils_assets |
| 30 |
{ |
| 31 |
protected static $static_asset_cache = array(); |
| 32 |
protected static $static_assets_location_cache = array(); |
| 33 |
protected static $static_assets_health_cache; |
| 34 |
protected static $static_js_data_map_cache = array(); //260906.1530 Parsed shipped static JavaScript data maps, keyed by path. |
| 35 |
protected static $static_assets_rebuild_after_save = array(); //260911.1834 Relevant saved CSS/JS option changes queue enabled static types for an immediate post-save rebuild. |
| 36 |
protected static $asset_http_health_cache; |
| 37 |
protected static $page_asset_expectations = array(); |
| 38 |
protected static $asset_health_force_full_probe = FALSE; //260910.0630 The Health panel can request a fresh trusted current-delivery probe without changing saved delivery settings. |
| 39 |
|
| 40 |
/** |
| 41 |
* Handles CSS compression. |
| 42 |
* |
| 43 |
* @package s2Member\Utilities |
| 44 |
* @since 3.5 |
| 45 |
* |
| 46 |
* @param string $css A string of CSS. |
| 47 |
* @return string String of CSS, after compression. |
| 48 |
*/ |
| 49 |
public static function compress_css($css = FALSE) |
| 50 |
{ |
| 51 |
$c6 = "/(\:#| #)([A-Z0-9]{6})/i"; |
| 52 |
$css = preg_replace("/\/\*(.*?)\*\//s", "", $css); |
| 53 |
$css = preg_replace("/[\r\n\t]+/", "", $css); |
| 54 |
$css = preg_replace("/ {2,}/", " ", $css); |
| 55 |
$css = preg_replace("/ , | ,|, /", ",", $css); |
| 56 |
$css = preg_replace("/ \> | \>|\> /", ">", $css); |
| 57 |
$css = preg_replace("/\[ /", "[", $css); |
| 58 |
$css = preg_replace("/ \]/", "]", $css); |
| 59 |
$css = preg_replace("/ \!\= | \!\=|\!\= /", "!=", $css); |
| 60 |
$css = preg_replace("/ \|\= | \|\=|\|\= /", "|=", $css); |
| 61 |
$css = preg_replace("/ \^\= | \^\=|\^\= /", "^=", $css); |
| 62 |
$css = preg_replace("/ \$\= | \$\=|\$\= /", "$=", $css); |
| 63 |
$css = preg_replace("/ \*\= | \*\=|\*\= /", "*=", $css); |
| 64 |
$css = preg_replace("/ ~\= | ~\=|~\= /", "~=", $css); |
| 65 |
$css = preg_replace("/ \= | \=|\= /", "=", $css); |
| 66 |
$css = preg_replace("/ \+ | \+|\+ /", "+", $css); |
| 67 |
$css = preg_replace("/ ~ | ~|~ /", "~", $css); |
| 68 |
$css = preg_replace("/ \{ | \{|\{ /", "{", $css); |
| 69 |
$css = preg_replace("/ \} | \}|\} /", "}", $css); |
| 70 |
$css = preg_replace("/ \: | \:|\: /", ":", $css); |
| 71 |
$css = preg_replace("/ ; | ;|; /", ";", $css); |
| 72 |
$css = preg_replace("/;\}/", "}", $css); |
| 73 |
|
| 74 |
return preg_replace_callback($c6, 'c_ws_plugin__s2member_utils_assets::_compress_css_c3', $css); |
| 75 |
} |
| 76 |
|
| 77 |
/** |
| 78 |
* Compresses JavaScript using an s2Member-adapted implementation of JShrink 1.8.1. |
| 79 |
* |
| 80 |
* JShrink is Copyright (c) Robert Hafner and licensed under BSD-3-Clause. |
| 81 |
* See `/src/licensing/jshrink.txt` for the complete license and attribution. |
| 82 |
* @see https://github.com/tedious/JShrink |
| 83 |
* |
| 84 |
* @package s2Member\Utilities |
| 85 |
* @since 260903.0437 |
| 86 |
* |
| 87 |
* @param string $js JavaScript source. |
| 88 |
* @return string Minified JavaScript. |
| 89 |
* @throws RuntimeException If malformed JavaScript cannot be minified safely. |
| 90 |
*/ |
| 91 |
public static function compress_js($js = '') |
| 92 |
{ |
| 93 |
$minifier = new self(); |
| 94 |
try |
| 95 |
{ |
| 96 |
$js = $minifier->jshrink_lock((string)$js); |
| 97 |
$js = ltrim($minifier->jshrink_minify_to_string($js, array('flaggedComments' => TRUE))); |
| 98 |
$js = $minifier->jshrink_unlock($js); |
| 99 |
$minifier->jshrink_clean(); |
| 100 |
return $js; |
| 101 |
} |
| 102 |
catch(Exception $e) |
| 103 |
{ |
| 104 |
$minifier->jshrink_clean(); |
| 105 |
throw $e; |
| 106 |
} |
| 107 |
} |
| 108 |
|
| 109 |
/** |
| 110 |
* Returns the selected URL used whenever frontend CSS/JavaScript needs dynamic generation. |
| 111 |
* |
| 112 |
* The s2Member Dynamic Loader remains the default. If its file is missing or a trusted browser probe has confirmed that it is unreachable, the normal WordPress loader is used temporarily without changing the saved preference. |
| 113 |
* In the current UI this established route is named the s2Member-Only Dynamic Loader and is served by s2member-o.php. |
| 114 |
* |
| 115 |
* @package s2Member\Utilities |
| 116 |
* @since 260904.0221 |
| 117 |
* |
| 118 |
* @param bool $force_wordpress Force the full WordPress route for a compatibility fallback. |
| 119 |
* @return string Dynamic frontend asset URL without query arguments. |
| 120 |
*/ |
| 121 |
public static function dynamic_asset_url($force_wordpress = FALSE) |
| 122 |
{ |
| 123 |
if(!$force_wordpress && (empty($GLOBALS['WS_PLUGIN__']['s2member']['o']['dynamic_asset_loader']) || $GLOBALS['WS_PLUGIN__']['s2member']['o']['dynamic_asset_loader'] !== 'wordpress') |
| 124 |
&& is_file(self::s2o_file_path()) && !self::asset_http_target_failed('s2o', $GLOBALS['WS_PLUGIN__']['s2member']['c']['s2o_url'])) |
| 125 |
return $GLOBALS['WS_PLUGIN__']['s2member']['c']['s2o_url']; |
| 126 |
|
| 127 |
return self::wordpress_dynamic_asset_url(); |
| 128 |
} |
| 129 |
|
| 130 |
/** |
| 131 |
* Returns the normal WordPress front-controller URL for dynamic frontend assets. |
| 132 |
* |
| 133 |
* @package s2Member\Utilities |
| 134 |
* @since 260904.2110 |
| 135 |
* |
| 136 |
* @return string WordPress dynamic frontend asset URL without query arguments. |
| 137 |
*/ |
| 138 |
protected static function wordpress_dynamic_asset_url() |
| 139 |
{ |
| 140 |
global $wp_rewrite; |
| 141 |
|
| 142 |
$index = (is_object($wp_rewrite) && !empty($wp_rewrite->index)) ? ltrim((string)$wp_rewrite->index, '/') : ''; |
| 143 |
if($index === '') |
| 144 |
$index = 'index.php'; |
| 145 |
return home_url('/'.$index); |
| 146 |
} |
| 147 |
|
| 148 |
/** |
| 149 |
* Returns the local s2member-o.php path. |
| 150 |
* |
| 151 |
* @package s2Member\Utilities |
| 152 |
* @since 260904.2110 |
| 153 |
* |
| 154 |
* @return string Local filesystem path. |
| 155 |
*/ |
| 156 |
protected static function s2o_file_path() |
| 157 |
{ |
| 158 |
return $GLOBALS['WS_PLUGIN__']['s2member']['c']['dir'].'/'.preg_replace('/\.php$/', '-o.php', basename($GLOBALS['WS_PLUGIN__']['s2member']['l'])); |
| 159 |
} |
| 160 |
|
| 161 |
/** |
| 162 |
* Returns true when a trusted browser probe has confirmed that one current asset URL is unreachable. |
| 163 |
* |
| 164 |
* @package s2Member\Utilities |
| 165 |
* @since 260904.2110 |
| 166 |
* |
| 167 |
* @param string $id Logical health target ID. |
| 168 |
* @param string $url Current public asset URL. |
| 169 |
* @return bool True when the exact current URL has a recorded failure. |
| 170 |
*/ |
| 171 |
protected static function asset_http_target_failed($id = '', $url = '') |
| 172 |
{ |
| 173 |
$health = self::asset_http_health_state(); |
| 174 |
return !empty($health['failures'][$id]['url']) && (string)$health['failures'][$id]['url'] === (string)$url; |
| 175 |
} |
| 176 |
|
| 177 |
/** |
| 178 |
* Returns the browser-reported frontend asset health state once per PHP request. |
| 179 |
* |
| 180 |
* @package s2Member\Utilities |
| 181 |
* @since 260904.2110 |
| 182 |
* |
| 183 |
* @return array Stored HTTP health state. |
| 184 |
*/ |
| 185 |
protected static function asset_http_health_state() |
| 186 |
{ |
| 187 |
if(!isset(self::$asset_http_health_cache)) |
| 188 |
{ |
| 189 |
$health = get_option('ws_plugin__s2member_asset_http_health', array()); |
| 190 |
self::$asset_http_health_cache = (is_array($health)) ? $health : array(); |
| 191 |
} |
| 192 |
return self::$asset_http_health_cache; |
| 193 |
} |
| 194 |
|
| 195 |
/** |
| 196 |
* Returns true while a trusted Full WordPress fallback failure is still active. |
| 197 |
* |
| 198 |
* @package s2Member\Utilities |
| 199 |
* @since 260912.1959 |
| 200 |
* |
| 201 |
* @return bool True when CSS or JS fallback health is currently failed. |
| 202 |
*/ |
| 203 |
protected static function asset_health_fallback_problem_active() |
| 204 |
{ |
| 205 |
$health = self::asset_http_health_state(); |
| 206 |
$failures = (is_array($health) && !empty($health['failures']) && is_array($health['failures'])) ? $health['failures'] : array(); |
| 207 |
return !empty($health['fallback_problem_since']) && (!empty($failures['fallback:dynamic_css']) || !empty($failures['fallback:dynamic_js'])); |
| 208 |
} |
| 209 |
|
| 210 |
/** |
| 211 |
* Adds or updates compact recent per-asset issue details for the Health panel. |
| 212 |
* |
| 213 |
* This diagnostic summary is intentionally kept with the trusted HTTP-health state instead of |
| 214 |
* the frontend load log. It remains available when css-js.log is disabled, and one keyed entry |
| 215 |
* per affected physical target prevents the state from growing with traffic. |
| 216 |
* |
| 217 |
* @package s2Member\Utilities |
| 218 |
* @since 260910.2346 |
| 219 |
* |
| 220 |
* @param array $issues Existing recent issue map. |
| 221 |
* @param string $key Stable physical-target key. |
| 222 |
* @param string $result Issue result (`late` or `failed`). |
| 223 |
* @param string $label Human-readable physical asset/route label. |
| 224 |
* @param string $detail Concise failure/timing detail. |
| 225 |
* @param string $url Relevant asset URL. |
| 226 |
* @param string $delivery Delivery mode when known. |
| 227 |
* @return array Updated issue map. |
| 228 |
*/ |
| 229 |
protected static function add_asset_health_recent_issue($issues = array(), $key = '', $result = '', $label = '', $detail = '', $url = '', $delivery = '') |
| 230 |
{ |
| 231 |
$issues = (is_array($issues)) ? $issues : array(); |
| 232 |
$key = substr(sanitize_key((string)$key), 0, 120); |
| 233 |
$result = strtolower((string)$result); |
| 234 |
if($key === '' || !in_array($result, array('late', 'failed'), TRUE)) |
| 235 |
return $issues; |
| 236 |
|
| 237 |
$previous = (!empty($issues[$key]) && is_array($issues[$key])) ? $issues[$key] : array(); |
| 238 |
$issues[$key] = array( |
| 239 |
'label' => substr(sanitize_text_field((string)$label), 0, 120), |
| 240 |
'result' => $result, |
| 241 |
'delivery' => substr(sanitize_text_field((string)$delivery), 0, 80), |
| 242 |
'detail' => substr(sanitize_text_field((string)$detail), 0, 200), |
| 243 |
'url' => esc_url_raw((string)$url), |
| 244 |
'first_seen' => (!empty($previous['first_seen'])) ? (int)$previous['first_seen'] : time(), |
| 245 |
'last_seen' => time(), |
| 246 |
'count' => (!empty($previous['count'])) ? (int)$previous['count'] + 1 : 1, |
| 247 |
); |
| 248 |
return $issues; |
| 249 |
} |
| 250 |
|
| 251 |
|
| 252 |
/** |
| 253 |
* Returns the compact rolling CSS/JavaScript asset-load health log. |
| 254 |
* |
| 255 |
* The state keeps only the latest 10 individual asset loads, populated clock-minute |
| 256 |
* aggregates from the latest 10 minutes, and populated clock-aligned 10-minute aggregates |
| 257 |
* from the latest 6 hours since Health last became non-Green. Minute/block keys are their |
| 258 |
* clock-aligned ending timestamps. Buckets retain sum/count so an accepted Late report can |
| 259 |
* correct the earlier Okay rating exactly, even after that clock period ended. Empty periods |
| 260 |
* are never manufactured as health evidence. |
| 261 |
* |
| 262 |
* @package s2Member\Utilities |
| 263 |
* @since 260910.0630 |
| 264 |
* |
| 265 |
* @return array Stored rolling asset-load health state. |
| 266 |
*/ |
| 267 |
protected static function asset_health_log_state() |
| 268 |
{ |
| 269 |
//260910.2346 Keep the hot-path state small and self-describing; every retained collection has a fixed request/time horizon and the option does not autoload. |
| 270 |
$state = get_option('ws_plugin__s2member_assets_health_log', array()); |
| 271 |
$state = (is_array($state)) ? $state : array(); |
| 272 |
$state['last_10_asset_loads'] = (!empty($state['last_10_asset_loads']) && is_array($state['last_10_asset_loads'])) ? array_values($state['last_10_asset_loads']) : array(); |
| 273 |
$state['last_10min_minutes'] = (!empty($state['last_10min_minutes']) && is_array($state['last_10min_minutes'])) ? $state['last_10min_minutes'] : array(); |
| 274 |
$state['last_6hour_10min_blocks'] = (!empty($state['last_6hour_10min_blocks']) && is_array($state['last_6hour_10min_blocks'])) ? $state['last_6hour_10min_blocks'] : array(); |
| 275 |
//260911.1806 Keep one compact historical issue outside the rolling score windows so Last issue remains useful after busy healthy traffic or natural recovery. |
| 276 |
$state['last_issue'] = (!empty($state['last_issue']) && is_array($state['last_issue'])) ? $state['last_issue'] : array(); |
| 277 |
//260913.0041 Keep a bounded always-available troubleshooting summary independent of the rolling score and optional css-js.log. |
| 278 |
$state['latest_issues'] = (!empty($state['latest_issues']) && is_array($state['latest_issues'])) ? array_slice($state['latest_issues'], 0, 10, TRUE) : array(); |
| 279 |
$state['last_issue_cleared_at'] = (!empty($state['last_issue_cleared_at'])) ? (int)$state['last_issue_cleared_at'] : 0; |
| 280 |
$state['latest_issues_cleared_at'] = (!empty($state['latest_issues_cleared_at'])) ? (int)$state['latest_issues_cleared_at'] : 0; |
| 281 |
//260912.0258 Keep a small duplicate-processing safeguard inside the existing health log in case a queued event survives after its changes were already stored. |
| 282 |
$state['processed_event_times'] = (!empty($state['processed_event_times']) && is_array($state['processed_event_times'])) ? array_slice(array_values($state['processed_event_times']), -100) : array(); |
| 283 |
return $state; |
| 284 |
} |
| 285 |
|
| 286 |
/** |
| 287 |
* Returns the option-name prefix used by queued asset-health events. |
| 288 |
* |
| 289 |
* Frontend requests queue separate non-autoloaded events instead of rewriting the shared rolling |
| 290 |
* log. The Health Logkeeper later merges those events into the one persistent health-log option. |
| 291 |
* |
| 292 |
* @package s2Member\Utilities |
| 293 |
* @since 260912.0258 |
| 294 |
* |
| 295 |
* @return string Event option prefix. |
| 296 |
*/ |
| 297 |
protected static function asset_health_event_option_prefix() |
| 298 |
{ |
| 299 |
return 'ws_plugin__s2member_assets_health_event_'; |
| 300 |
} |
| 301 |
|
| 302 |
/** |
| 303 |
* Queues one asset-health event without waiting for or rewriting the shared rolling log. |
| 304 |
* |
| 305 |
* The option suffix combines the event time with the queue time in microseconds. That meaningful |
| 306 |
* pair gives chronological ordering, practical uniqueness, and duplicate-processing identity. |
| 307 |
* |
| 308 |
* @package s2Member\Utilities |
| 309 |
* @since 260912.0258 |
| 310 |
* |
| 311 |
* @param array $event Compact load/issue event. |
| 312 |
* @return string Queued event-times suffix, or an empty string on failure. |
| 313 |
*/ |
| 314 |
protected static function queue_asset_health_event($event = array()) |
| 315 |
{ |
| 316 |
$event = (is_array($event)) ? $event : array(); |
| 317 |
$event_time = (!empty($event['event_time'])) ? max(1, (int)$event['event_time']) : time(); |
| 318 |
$microtime = explode(' ', microtime(), 2); |
| 319 |
$queued_sec = (!empty($microtime[1])) ? max(1, (int)$microtime[1]) : time(); |
| 320 |
$queued_usec = (!empty($microtime[0])) ? (int)substr($microtime[0], 2, 6) : 0; |
| 321 |
$prefix = self::asset_health_event_option_prefix(); |
| 322 |
$event_time_order = str_pad((string)$event_time, 12, '0', STR_PAD_LEFT); |
| 323 |
|
| 324 |
//260912.0258 add_option() provides the atomic uniqueness check; an extraordinarily unlikely collision simply advances the queue time by one microsecond and retries. |
| 325 |
for($attempt = 0; $attempt < 3; $attempt++) |
| 326 |
{ |
| 327 |
$queued_time_order = str_pad((string)$queued_sec, 12, '0', STR_PAD_LEFT).str_pad((string)$queued_usec, 6, '0', STR_PAD_LEFT); |
| 328 |
$event_times = $event_time_order.'_'.$queued_time_order; |
| 329 |
if(add_option($prefix.$event_times, $event, '', 'no')) |
| 330 |
{ |
| 331 |
//260912.0258 Schedule the Health Logkeeper without making the visitor wait for health-log maintenance. |
| 332 |
if(!wp_next_scheduled('ws_plugin__s2member_assets_health_logkeeper')) |
| 333 |
wp_schedule_single_event(time() + 10, 'ws_plugin__s2member_assets_health_logkeeper'); |
| 334 |
return $event_times; |
| 335 |
} |
| 336 |
if(++$queued_usec > 999999) |
| 337 |
{ |
| 338 |
$queued_usec = 0; |
| 339 |
$queued_sec++; |
| 340 |
} |
| 341 |
} |
| 342 |
return ''; |
| 343 |
} |
| 344 |
|
| 345 |
/** |
| 346 |
* Acquires the Health Logkeeper lock without waiting. |
| 347 |
* |
| 348 |
* Only the Health Logkeeper writes the shared rolling health log. If another Logkeeper run is |
| 349 |
* active, this request exits immediately; frontend requests only queue events and never wait here. |
| 350 |
* |
| 351 |
* @package s2Member\Utilities |
| 352 |
* @since 260912.0258 |
| 353 |
* |
| 354 |
* @return string Unique lock value, or an empty string when another Logkeeper run owns it. |
| 355 |
*/ |
| 356 |
protected static function health_logkeeper_lock_acquire() |
| 357 |
{ |
| 358 |
global $wpdb; |
| 359 |
|
| 360 |
$option = 'ws_plugin__s2member_assets_health_logkeeper_lock'; |
| 361 |
$lock = time().':'.sha1(microtime(TRUE)."\0".wp_rand()); |
| 362 |
if(add_option($option, $lock, '', 'no')) |
| 363 |
return $lock; |
| 364 |
|
| 365 |
$current = (string)get_option($option, ''); |
| 366 |
$parts = explode(':', $current, 2); |
| 367 |
$locked_at = (!empty($parts[0]) && is_numeric($parts[0])) ? (int)$parts[0] : 0; |
| 368 |
//260913.0454 Delete only the stale lock version we inspected; another Logkeeper may replace it before this request reaches the delete. |
| 369 |
if(!$locked_at || $locked_at < time() - 2 * MINUTE_IN_SECONDS) |
| 370 |
{ |
| 371 |
$deleted = $wpdb->delete($wpdb->options, array('option_name' => $option, 'option_value' => maybe_serialize($current)), array('%s', '%s')); |
| 372 |
if($deleted) |
| 373 |
{ |
| 374 |
wp_cache_delete($option, 'options'); |
| 375 |
if(add_option($option, $lock, '', 'no')) |
| 376 |
return $lock; |
| 377 |
} |
| 378 |
} |
| 379 |
return ''; |
| 380 |
} |
| 381 |
|
| 382 |
/** |
| 383 |
* Releases the Health Logkeeper lock when this request still owns it. |
| 384 |
* |
| 385 |
* @package s2Member\Utilities |
| 386 |
* @since 260912.0258 |
| 387 |
* |
| 388 |
* @param string $lock Unique lock value returned by health_logkeeper_lock_acquire(). |
| 389 |
* @return null |
| 390 |
*/ |
| 391 |
protected static function health_logkeeper_lock_release($lock = '') |
| 392 |
{ |
| 393 |
global $wpdb; |
| 394 |
|
| 395 |
$option = 'ws_plugin__s2member_assets_health_logkeeper_lock'; |
| 396 |
$current = (string)get_option($option, ''); |
| 397 |
if($lock !== '' && $current !== '' && hash_equals($current, (string)$lock)) |
| 398 |
{ |
| 399 |
//260913.0454 Release only the exact lock version owned by this request; an expired owner must never delete a newer Logkeeper's lock. |
| 400 |
$deleted = $wpdb->delete($wpdb->options, array('option_name' => $option, 'option_value' => maybe_serialize($current)), array('%s', '%s')); |
| 401 |
if($deleted) |
| 402 |
wp_cache_delete($option, 'options'); |
| 403 |
} |
| 404 |
return; |
| 405 |
} |
| 406 |
|
| 407 |
/** |
| 408 |
* Replaces Last issue only when the candidate issue is at least as recent as the current one. |
| 409 |
* |
| 410 |
* @package s2Member\Utilities |
| 411 |
* @since 260911.2325 |
| 412 |
* |
| 413 |
* @param array $state Asset-health log state, passed by reference. |
| 414 |
* @param int $time Issue timestamp. |
| 415 |
* @param string $result Compact historical result key. |
| 416 |
* @param string $label Site-owner-friendly asset label. |
| 417 |
* @param string $detail Concise explanation. |
| 418 |
* @param string $load_id Optional related load ID. |
| 419 |
* @param int $page_id Optional WordPress post/page ID. |
| 420 |
* @param string $page_path Optional queryless frontend path. |
| 421 |
* @return bool True when Last issue was replaced. |
| 422 |
*/ |
| 423 |
protected static function set_asset_health_last_issue(&$state, $time = 0, $result = '', $label = '', $detail = '', $load_id = '', $page_id = 0, $page_path = '') |
| 424 |
{ |
| 425 |
$time = max(1, (int)$time); |
| 426 |
$result = strtolower((string)$result); |
| 427 |
$current_time = (!empty($state['last_issue']['time'])) ? (int)$state['last_issue']['time'] : 0; |
| 428 |
$cleared_at = (!empty($state['last_issue_cleared_at'])) ? (int)$state['last_issue_cleared_at'] : 0; |
| 429 |
if($result === '' || $current_time > $time || $cleared_at >= $time) |
| 430 |
return FALSE; |
| 431 |
|
| 432 |
//260911.2325 Delayed browser reports may arrive out of order; Last issue must follow event time, not whichever request happened to write last. |
| 433 |
$state['last_issue'] = array( |
| 434 |
'time' => $time, |
| 435 |
'result' => $result, |
| 436 |
'label' => substr((string)$label, 0, 100), |
| 437 |
'detail' => substr(wp_strip_all_tags((string)$detail), 0, 240), |
| 438 |
'load_id' => (string)$load_id, |
| 439 |
'page_id' => max(0, (int)$page_id), |
| 440 |
'page_path' => substr((string)$page_path, 0, 240), |
| 441 |
); |
| 442 |
return TRUE; |
| 443 |
} |
| 444 |
|
| 445 |
/** |
| 446 |
* Adds one occurrence to the bounded persistent Asset Health Latest Issues summary. |
| 447 |
* |
| 448 |
* Distinct issues are grouped by asset/result/page so repeats do not crowd out other problems. |
| 449 |
* Each group keeps a total count and up to 10 recent occurrence times. |
| 450 |
* |
| 451 |
* @package s2Member\Utilities |
| 452 |
* @since 260913.0041 |
| 453 |
* |
| 454 |
* @param array $state Asset-health log state, passed by reference. |
| 455 |
* @param int $time Original issue timestamp. |
| 456 |
* @param string $result Compact issue result. |
| 457 |
* @param array $issue Issue context. |
| 458 |
* @param array $page Page context with `page_id` and `page_path`. |
| 459 |
* @return bool True when the summary changed. |
| 460 |
*/ |
| 461 |
protected static function add_asset_health_latest_issue(&$state, $time = 0, $result = '', $issue = array(), $page = array()) |
| 462 |
{ |
| 463 |
$time = max(1, (int)$time); |
| 464 |
$result = strtolower((string)$result); |
| 465 |
$issue = (is_array($issue)) ? $issue : array(); |
| 466 |
$page = (is_array($page)) ? $page : array(); |
| 467 |
$cleared_at = (!empty($state['latest_issues_cleared_at'])) ? (int)$state['latest_issues_cleared_at'] : 0; |
| 468 |
if($result === '' || $cleared_at >= $time) |
| 469 |
return FALSE; |
| 470 |
|
| 471 |
$asset = substr(sanitize_text_field((!empty($issue['asset'])) ? (string)$issue['asset'] : ''), 0, 120); |
| 472 |
$label = substr(sanitize_text_field((!empty($issue['label'])) ? (string)$issue['label'] : ''), 0, 120); |
| 473 |
$detail = substr(wp_strip_all_tags((!empty($issue['detail'])) ? (string)$issue['detail'] : ''), 0, 240); |
| 474 |
$delivery = substr(sanitize_text_field((!empty($issue['delivery'])) ? (string)$issue['delivery'] : ''), 0, 80); |
| 475 |
$page_id = (!empty($page['page_id'])) ? max(0, (int)$page['page_id']) : 0; |
| 476 |
$page_path = (!empty($page['page_path'])) ? substr((string)$page['page_path'], 0, 240) : ''; |
| 477 |
if($label === '' && $detail === '' && $asset === '') |
| 478 |
return FALSE; |
| 479 |
|
| 480 |
$page_identity = ($page_id > 0) ? 'id:'.$page_id : 'path:'.$page_path; |
| 481 |
$key = sha1(($asset !== '' ? $asset : $label)."\0".$result."\0".$page_identity); |
| 482 |
$issues = (!empty($state['latest_issues']) && is_array($state['latest_issues'])) ? $state['latest_issues'] : array(); |
| 483 |
$previous = (!empty($issues[$key]) && is_array($issues[$key])) ? $issues[$key] : array(); |
| 484 |
$times = (!empty($previous['times']) && is_array($previous['times'])) ? array_values($previous['times']) : array(); |
| 485 |
array_unshift($times, $time); |
| 486 |
rsort($times, SORT_NUMERIC); //260913.0041 Delayed reports can arrive after newer issues; retain the 10 most recent occurrence times by event time, not processing order. |
| 487 |
$times = array_slice($times, 0, 10); |
| 488 |
$issues[$key] = array( |
| 489 |
'asset' => $asset, |
| 490 |
'label' => $label, |
| 491 |
'result' => $result, |
| 492 |
'delivery' => $delivery, |
| 493 |
'detail' => $detail, |
| 494 |
'page_id' => $page_id, |
| 495 |
'page_path' => $page_path, |
| 496 |
'first_seen' => (!empty($previous['first_seen'])) ? min((int)$previous['first_seen'], $time) : $time, |
| 497 |
'last_seen' => (!empty($previous['last_seen'])) ? max((int)$previous['last_seen'], $time) : $time, |
| 498 |
'count' => (!empty($previous['count'])) ? (int)$previous['count'] + 1 : 1, |
| 499 |
'times' => $times, |
| 500 |
); |
| 501 |
uasort($issues, function($a, $b) { |
| 502 |
$a_time = (!empty($a['last_seen'])) ? (int)$a['last_seen'] : 0; |
| 503 |
$b_time = (!empty($b['last_seen'])) ? (int)$b['last_seen'] : 0; |
| 504 |
return ($a_time === $b_time) ? 0 : (($a_time > $b_time) ? -1 : 1); |
| 505 |
}); |
| 506 |
$state['latest_issues'] = array_slice($issues, 0, 10, TRUE); |
| 507 |
return TRUE; |
| 508 |
} |
| 509 |
|
| 510 |
/** |
| 511 |
* Returns the numeric rating for one asset-load result. |
| 512 |
* |
| 513 |
* @package s2Member\Utilities |
| 514 |
* @since 260910.0630 |
| 515 |
* |
| 516 |
* @param string $result Asset-load result: `okay`, `late`, `fallback`, or `failed`. |
| 517 |
* @return int Rating from 1 through 4, or zero when invalid. |
| 518 |
*/ |
| 519 |
protected static function asset_health_load_rating($result = '') |
| 520 |
{ |
| 521 |
$ratings = array('okay' => 4, 'late' => 3, 'fallback' => 2, 'failed' => 1); //260910.2346 Persist full result words so the health log remains readable without an O/L/F/X legend; the numeric value is used only for scoring. |
| 522 |
$result = strtolower((string)$result); |
| 523 |
return isset($ratings[$result]) ? $ratings[$result] : 0; |
| 524 |
} |
| 525 |
|
| 526 |
/** |
| 527 |
* Returns the ending timestamp of the clock-aligned period containing a timestamp. |
| 528 |
* |
| 529 |
* A timestamp exactly on a boundary belongs to the period ending at that boundary. Thus a |
| 530 |
* 10-minute period ending 12:10:00 represents 12:00:01 through 12:10:00 at whole-second precision. |
| 531 |
* |
| 532 |
* @package s2Member\Utilities |
| 533 |
* @since 260910.2346 |
| 534 |
* |
| 535 |
* @param int $time Timestamp. |
| 536 |
* @param int $seconds Period size in seconds. |
| 537 |
* @return int Clock-aligned period ending timestamp. |
| 538 |
*/ |
| 539 |
protected static function asset_health_period_end($time = 0, $seconds = 0) |
| 540 |
{ |
| 541 |
$time = max(1, (int)$time); |
| 542 |
$seconds = max(1, (int)$seconds); |
| 543 |
return (int)(ceil($time / $seconds) * $seconds); |
| 544 |
} |
| 545 |
|
| 546 |
/** |
| 547 |
* Converts the final 1.00-4.00 health score to the site-owner status color. |
| 548 |
* |
| 549 |
* @package s2Member\Utilities |
| 550 |
* @since 260910.0630 |
| 551 |
* |
| 552 |
* @param float|null $score Final health score, or NULL when there is no evidence yet. |
| 553 |
* @param string $latest_result Retained for call-site compatibility; the weighted score now determines status by itself. |
| 554 |
* @return string Status-light key. |
| 555 |
*/ |
| 556 |
protected static function asset_health_status_from_score($score = NULL, $latest_result = '') |
| 557 |
{ |
| 558 |
if($score === NULL) |
| 559 |
return 'unknown'; |
| 560 |
$score = (float)$score; |
| 561 |
//260913.0046 Let the weighted score decide Health consistently; recency already gives a new non-Okay result the strongest influence without an extra status override. |
| 562 |
//260912.2005 Exact half-point boundaries belong to the less-healthy band; use the documented two-decimal cutoffs so 2.50, for example, is Working, review suggested rather than Recent issue. |
| 563 |
if($score >= 3.51) |
| 564 |
return 'healthy'; |
| 565 |
if($score >= 2.51) |
| 566 |
return 'delayed'; |
| 567 |
if($score >= 1.51) |
| 568 |
return 'attention'; |
| 569 |
return 'error'; |
| 570 |
} |
| 571 |
|
| 572 |
/** |
| 573 |
* Recalculates request, time, and final health scores from the retained asset-load log. |
| 574 |
* |
| 575 |
* Newer asset loads have importance 10 down through 1. Each populated clock minute first |
| 576 |
* averages all asset-load ratings inside it, then receives importance 10 for the current minute |
| 577 |
* down through 1 nine minutes ago. Empty minutes are skipped instead of inventing evidence. |
| 578 |
* |
| 579 |
* @package s2Member\Utilities |
| 580 |
* @since 260910.0630 |
| 581 |
* |
| 582 |
* @param array|null $state Optional already-loaded asset health log. |
| 583 |
* @return array Request/time/final scores and supporting counts. |
| 584 |
*/ |
| 585 |
protected static function asset_health_scores($state = NULL) |
| 586 |
{ |
| 587 |
$state = (is_array($state)) ? $state : self::asset_health_log_state(); |
| 588 |
$loads = (!empty($state['last_10_asset_loads']) && is_array($state['last_10_asset_loads'])) ? array_values($state['last_10_asset_loads']) : array(); |
| 589 |
$loads = array_slice($loads, -10); |
| 590 |
$request_total = 0.0; |
| 591 |
$request_importance = 0; |
| 592 |
$importance = 10; |
| 593 |
//260910.0709 Newest asset load matters most (10) and the oldest retained load least (1); divide by total importance below so the result stays on the same 1.00-4.00 scale. |
| 594 |
for($i = count($loads) - 1; $i >= 0 && $importance >= 1; $i--, $importance--) |
| 595 |
{ |
| 596 |
$rating = (!empty($loads[$i]['result'])) ? self::asset_health_load_rating($loads[$i]['result']) : 0; |
| 597 |
if(!$rating) |
| 598 |
continue; |
| 599 |
$request_total += $rating * $importance; |
| 600 |
$request_importance += $importance; |
| 601 |
} |
| 602 |
$request_score = ($request_importance) ? $request_total / $request_importance : NULL; |
| 603 |
|
| 604 |
$current_minute_end = self::asset_health_period_end(time(), MINUTE_IN_SECONDS); |
| 605 |
$time_total = 0.0; |
| 606 |
$time_importance = 0; |
| 607 |
$time_count = 0; |
| 608 |
//260910.0709 Time Health averages every load inside a populated clock minute before applying recency importance, so heavy traffic cannot dominate other minutes and mixed outcomes inside one minute are not discarded. |
| 609 |
foreach((!empty($state['last_10min_minutes']) && is_array($state['last_10min_minutes'])) ? $state['last_10min_minutes'] : array() as $minute_end => $bucket) |
| 610 |
{ |
| 611 |
$minute_end = (int)$minute_end; |
| 612 |
$age = (int)(($current_minute_end - $minute_end) / MINUTE_IN_SECONDS); |
| 613 |
$bucket_count = (!empty($bucket['count'])) ? (int)$bucket['count'] : 0; |
| 614 |
$bucket_sum = (isset($bucket['sum'])) ? (float)$bucket['sum'] : 0.0; |
| 615 |
if($age < 0 || $age > 9 || $bucket_count < 1) |
| 616 |
continue; |
| 617 |
$rating = $bucket_sum / $bucket_count; |
| 618 |
if($rating < 1 || $rating > 4) |
| 619 |
continue; |
| 620 |
$importance = 10 - $age; |
| 621 |
$time_total += $rating * $importance; |
| 622 |
$time_importance += $importance; |
| 623 |
$time_count++; |
| 624 |
} |
| 625 |
$time_score = ($time_importance) ? $time_total / $time_importance : NULL; |
| 626 |
//260910.0709 Request history and clock-time history get equal final influence when both exist; neither perspective can silently dominate the other. |
| 627 |
if($request_score !== NULL && $time_score !== NULL) |
| 628 |
$score = ($request_score + $time_score) / 2; |
| 629 |
else if($request_score !== NULL) |
| 630 |
$score = $request_score; |
| 631 |
else if($time_score !== NULL) |
| 632 |
$score = $time_score; |
| 633 |
else |
| 634 |
$score = NULL; |
| 635 |
|
| 636 |
$latest_result = ($loads && !empty($loads[count($loads) - 1]['result'])) ? (string)$loads[count($loads) - 1]['result'] : ''; |
| 637 |
|
| 638 |
return array( |
| 639 |
'request_score' => $request_score, |
| 640 |
'time_score' => $time_score, |
| 641 |
'score' => $score, |
| 642 |
'latest_result' => $latest_result, |
| 643 |
'request_count' => count($loads), |
| 644 |
'time_count' => $time_count, |
| 645 |
); |
| 646 |
} |
| 647 |
|
| 648 |
/** |
| 649 |
* Returns the equal-block average from populated clock-aligned 10-minute blocks in the last 6 hours. |
| 650 |
* |
| 651 |
* Each populated 10-minute block contributes one average regardless of traffic volume. Empty |
| 652 |
* blocks contribute nothing because absence of traffic is not health evidence. |
| 653 |
* |
| 654 |
* @package s2Member\Utilities |
| 655 |
* @since 260910.0630 |
| 656 |
* |
| 657 |
* @param array $state Asset health log. |
| 658 |
* @return float|null Rolling six-hour average, or NULL without retained non-Green evidence. |
| 659 |
*/ |
| 660 |
protected static function asset_health_six_hour_average($state = array()) |
| 661 |
{ |
| 662 |
$blocks = (!empty($state['last_6hour_10min_blocks']) && is_array($state['last_6hour_10min_blocks'])) ? $state['last_6hour_10min_blocks'] : array(); |
| 663 |
if(!$blocks) |
| 664 |
return NULL; |
| 665 |
$cutoff = time() - 6 * HOUR_IN_SECONDS; |
| 666 |
$total = 0.0; |
| 667 |
$count = 0; |
| 668 |
//260910.2346 The persistent-review calculation runs only when an admin request has already passed cheaper status/age checks; at most about 37 populated blocks can contribute. |
| 669 |
foreach($blocks as $block_end => $bucket) |
| 670 |
{ |
| 671 |
$block_end = (int)$block_end; |
| 672 |
$bucket_count = (!empty($bucket['count'])) ? (int)$bucket['count'] : 0; |
| 673 |
$bucket_sum = (isset($bucket['sum'])) ? (float)$bucket['sum'] : 0.0; |
| 674 |
if($block_end <= $cutoff || $bucket_count < 1) |
| 675 |
continue; |
| 676 |
$rating = $bucket_sum / $bucket_count; |
| 677 |
if($rating < 1 || $rating > 4) |
| 678 |
continue; |
| 679 |
$total += $rating; |
| 680 |
$count++; |
| 681 |
} |
| 682 |
return ($count) ? $total / $count : NULL; |
| 683 |
} |
| 684 |
|
| 685 |
/** |
| 686 |
* Returns a signature for compact page-load metadata used by a later Late correction. |
| 687 |
* |
| 688 |
* @package s2Member\Utilities |
| 689 |
* @since 260910.2346 |
| 690 |
* |
| 691 |
* @param array $load Asset-load metadata. |
| 692 |
* @return string Signature. |
| 693 |
*/ |
| 694 |
protected static function asset_health_load_signature($load = array()) |
| 695 |
{ |
| 696 |
$parts = array(); |
| 697 |
foreach(array('load_id', 'load_time', 'orig_result', 'orig_6hour') as $key) |
| 698 |
$parts[$key] = isset($load[$key]) ? (string)$load[$key] : ''; |
| 699 |
return hash_hmac('sha256', serialize($parts), wp_salt('nonce')); |
| 700 |
} |
| 701 |
|
| 702 |
/** |
| 703 |
* Returns the current frontend page context without retaining query-string data. |
| 704 |
* |
| 705 |
* @package s2Member\Utilities |
| 706 |
* @since 260913.0048 |
| 707 |
* |
| 708 |
* @return array Compact page ID/path context. |
| 709 |
*/ |
| 710 |
protected static function current_asset_health_page_context() |
| 711 |
{ |
| 712 |
$page_id = (function_exists('is_singular') && is_singular()) ? (int)get_queried_object_id() : 0; |
| 713 |
$request_uri = (!empty($_SERVER['REQUEST_URI'])) ? wp_unslash((string)$_SERVER['REQUEST_URI']) : ''; |
| 714 |
$page_path = ($request_uri !== '') ? (string)c_ws_plugin__s2member_utils_urls::parse_url($request_uri, PHP_URL_PATH) : ''; |
| 715 |
$page_path = substr('/'.ltrim($page_path, '/'), 0, 240); |
| 716 |
if($page_path === '/') |
| 717 |
$page_path = '/'; |
| 718 |
return array('page_id' => max(0, $page_id), 'page_path' => $page_path); |
| 719 |
} |
| 720 |
|
| 721 |
/** |
| 722 |
* Signs page context separately from legacy load metadata so already-cached pages remain compatible. |
| 723 |
* |
| 724 |
* @package s2Member\Utilities |
| 725 |
* @since 260913.0048 |
| 726 |
* |
| 727 |
* @param array $load Asset-load metadata. |
| 728 |
* @return string Signature. |
| 729 |
*/ |
| 730 |
protected static function asset_health_page_signature($load = array()) |
| 731 |
{ |
| 732 |
$parts = array(); |
| 733 |
foreach(array('load_id', 'load_time', 'page_id', 'page_path') as $key) |
| 734 |
$parts[$key] = isset($load[$key]) ? (string)$load[$key] : ''; |
| 735 |
return hash_hmac('sha256', serialize($parts), wp_salt('nonce')); |
| 736 |
} |
| 737 |
|
| 738 |
/** |
| 739 |
* Returns signed page context from browser-returned load metadata. |
| 740 |
* |
| 741 |
* @package s2Member\Utilities |
| 742 |
* @since 260913.0048 |
| 743 |
* |
| 744 |
* @param array $load Browser-returned load metadata. |
| 745 |
* @return array Verified page context, or empty context for legacy/tampered metadata. |
| 746 |
*/ |
| 747 |
protected static function verified_asset_health_page_context($load = array()) |
| 748 |
{ |
| 749 |
$load = (is_array($load)) ? $load : array(); |
| 750 |
if(empty($load['page_signature']) || empty($load['load_id']) || empty($load['load_time'])) |
| 751 |
return array('page_id' => 0, 'page_path' => ''); |
| 752 |
$signature = (string)$load['page_signature']; |
| 753 |
if(!hash_equals(self::asset_health_page_signature($load), $signature)) |
| 754 |
return array('page_id' => 0, 'page_path' => ''); |
| 755 |
$page_id = (!empty($load['page_id'])) ? max(0, (int)$load['page_id']) : 0; |
| 756 |
$page_path = (!empty($load['page_path'])) ? substr((string)$load['page_path'], 0, 240) : ''; |
| 757 |
return array('page_id' => $page_id, 'page_path' => $page_path); |
| 758 |
} |
| 759 |
|
| 760 |
/** |
| 761 |
* Queues one frontend asset load or signed Late correction for later collection. |
| 762 |
* |
| 763 |
* A WordPress-rendered frontend page queues one page-level result using the worst required |
| 764 |
* asset outcome: Okay=4, Late=3, Fallback=2, Failed=1. A signed Late report later corrects |
| 765 |
* that original load instead of counting the same page twice. |
| 766 |
* |
| 767 |
* @package s2Member\Utilities |
| 768 |
* @since 260910.0630 |
| 769 |
* |
| 770 |
* @param string $result Asset-load result: `okay`, `late`, `fallback`, or `failed`. |
| 771 |
* @param bool $reset_on_ok Reset prior active history when an explicit trusted recheck returns Okay. |
| 772 |
* @param array $load Optional signed original-load metadata for a Late correction. |
| 773 |
* @param array $issue Optional compact issue snapshot with `label` and `detail`. |
| 774 |
* @return array Compact metadata for the queued load; `scores` remains an empty compatibility field. |
| 775 |
*/ |
| 776 |
protected static function queue_asset_health_load($result = '', $reset_on_ok = FALSE, $load = array(), $issue = array()) |
| 777 |
{ |
| 778 |
$result = strtolower((string)$result); |
| 779 |
$rating = self::asset_health_load_rating($result); |
| 780 |
if(!$rating) |
| 781 |
return array('scores' => array(), 'load' => array()); |
| 782 |
|
| 783 |
$now = time(); |
| 784 |
$load = (is_array($load)) ? $load : array(); |
| 785 |
$issue = (is_array($issue)) ? $issue : array(); |
| 786 |
$is_late_correction = $result === 'late' && !empty($load['load_id']) && !empty($load['load_time']); |
| 787 |
if(!$is_late_correction) |
| 788 |
{ |
| 789 |
$load = array( |
| 790 |
'load_id' => sha1(microtime(TRUE)."\0".wp_rand()."\0".home_url('/')), |
| 791 |
'load_time' => $now, |
| 792 |
'orig_result' => $result, |
| 793 |
'orig_6hour' => 0, |
| 794 |
); |
| 795 |
//260913.0048 Frontend page identity is compact diagnostic context; keep query strings out and do not attach admin/AJAX request paths to explicit trusted rechecks. |
| 796 |
if(!$reset_on_ok && !is_admin() && !(function_exists('wp_doing_ajax') && wp_doing_ajax())) |
| 797 |
$load = array_merge($load, self::current_asset_health_page_context()); |
| 798 |
} |
| 799 |
|
| 800 |
$event_time = ($is_late_correction && !empty($load['load_time'])) ? (int)$load['load_time'] : $now; |
| 801 |
//260912.0258 Queue the event and return without reading, locking, or rewriting the shared rolling health log. |
| 802 |
self::queue_asset_health_event(array( |
| 803 |
'type' => 'load', |
| 804 |
'event_time' => $event_time, |
| 805 |
'result' => $result, |
| 806 |
'reset_on_ok' => (bool)$reset_on_ok, |
| 807 |
'load' => $load, |
| 808 |
'issue' => $issue, |
| 809 |
)); |
| 810 |
|
| 811 |
if(!$is_late_correction) |
| 812 |
{ |
| 813 |
$load['signature'] = self::asset_health_load_signature($load); |
| 814 |
if(isset($load['page_id']) || isset($load['page_path'])) |
| 815 |
$load['page_signature'] = self::asset_health_page_signature($load); |
| 816 |
} |
| 817 |
return array('scores' => array(), 'load' => $load); |
| 818 |
} |
| 819 |
|
| 820 |
/** |
| 821 |
* Queues a useful historical issue that did not itself degrade the page-level health score. |
| 822 |
* |
| 823 |
* @package s2Member\Utilities |
| 824 |
* @since 260911.1834 |
| 825 |
* |
| 826 |
* @param string $result Compact historical result key. |
| 827 |
* @param string $label Site-owner-friendly asset label. |
| 828 |
* @param string $detail Concise explanation. |
| 829 |
* @param array $issue Optional asset/delivery context. |
| 830 |
* @param array $page Optional page context. |
| 831 |
* @param int $event_time Optional original issue timestamp. |
| 832 |
* @return null |
| 833 |
*/ |
| 834 |
protected static function queue_asset_health_issue_snapshot($result = '', $label = '', $detail = '', $issue = array(), $page = array(), $event_time = 0) |
| 835 |
{ |
| 836 |
$result = strtolower((string)$result); |
| 837 |
if($result === '') |
| 838 |
return; |
| 839 |
|
| 840 |
$issue = (is_array($issue)) ? $issue : array(); |
| 841 |
$issue = array_merge($issue, array('label' => (string)$label, 'detail' => (string)$detail)); |
| 842 |
$page = (is_array($page)) ? $page : array(); |
| 843 |
if(!$page && !is_admin() && !(function_exists('wp_doing_ajax') && wp_doing_ajax())) |
| 844 |
$page = self::current_asset_health_page_context(); |
| 845 |
//260913.0048 Self-repair/fallback/trusted-failure snapshots may identify the frontend page where they were encountered, without retaining its query string. |
| 846 |
//260912.0258 Queue historical snapshots like scored loads so a simultaneous healthy page cannot erase them with stale state. |
| 847 |
self::queue_asset_health_event(array( |
| 848 |
'type' => 'issue', |
| 849 |
'event_time' => ($event_time > 0) ? (int)$event_time : time(), |
| 850 |
'result' => $result, |
| 851 |
'issue' => $issue, |
| 852 |
'page' => $page, |
| 853 |
)); |
| 854 |
return; |
| 855 |
} |
| 856 |
|
| 857 |
/** |
| 858 |
* Applies one queued scored-load event to an already-loaded health-log state. |
| 859 |
* |
| 860 |
* @package s2Member\Utilities |
| 861 |
* @since 260911.2356 |
| 862 |
* |
| 863 |
* @param array $state Rolling health-log state, passed by reference. |
| 864 |
* @param array $event Queued load event. |
| 865 |
* @return bool True when the event was valid and consumed. |
| 866 |
*/ |
| 867 |
protected static function apply_asset_health_load_event(&$state, $event = array()) |
| 868 |
{ |
| 869 |
$event = (is_array($event)) ? $event : array(); |
| 870 |
$result = (!empty($event['result'])) ? strtolower((string)$event['result']) : ''; |
| 871 |
$rating = self::asset_health_load_rating($result); |
| 872 |
if(!$rating) |
| 873 |
return FALSE; |
| 874 |
|
| 875 |
$now = time(); |
| 876 |
$event_time = (!empty($event['event_time'])) ? max(1, (int)$event['event_time']) : $now; |
| 877 |
$reset_on_ok = !empty($event['reset_on_ok']); |
| 878 |
$load = (!empty($event['load']) && is_array($event['load'])) ? $event['load'] : array(); |
| 879 |
$issue = (!empty($event['issue']) && is_array($event['issue'])) ? $event['issue'] : array(); |
| 880 |
$issues = (!empty($issue['items']) && is_array($issue['items'])) ? array_values($issue['items']) : (($issue) ? array($issue) : array()); |
| 881 |
$page = array('page_id' => 0, 'page_path' => ''); |
| 882 |
|
| 883 |
if($reset_on_ok && $result === 'okay') |
| 884 |
{ |
| 885 |
//260913.0041 A trusted successful recheck resets scoring only; durable issue summaries, clear watermarks, and duplicate-processing protection remain historical state. |
| 886 |
$last_issue = (!empty($state['last_issue']) && is_array($state['last_issue'])) ? $state['last_issue'] : array(); |
| 887 |
$latest_issues = (!empty($state['latest_issues']) && is_array($state['latest_issues'])) ? $state['latest_issues'] : array(); |
| 888 |
$last_issue_cleared_at = (!empty($state['last_issue_cleared_at'])) ? (int)$state['last_issue_cleared_at'] : 0; |
| 889 |
$latest_issues_cleared_at = (!empty($state['latest_issues_cleared_at'])) ? (int)$state['latest_issues_cleared_at'] : 0; |
| 890 |
$processed_event_times = (!empty($state['processed_event_times']) && is_array($state['processed_event_times'])) ? $state['processed_event_times'] : array(); |
| 891 |
$state = array('last_10_asset_loads' => array(), 'last_10min_minutes' => array(), 'last_6hour_10min_blocks' => array(), 'last_issue' => $last_issue, 'latest_issues' => $latest_issues, 'last_issue_cleared_at' => $last_issue_cleared_at, 'latest_issues_cleared_at' => $latest_issues_cleared_at, 'processed_event_times' => $processed_event_times, 'status' => 'unknown', 'not_green_since' => 0, 'history_reset_at' => $event_time); |
| 892 |
delete_option('ws_plugin__s2member_asset_notice_dismissed'); |
| 893 |
} |
| 894 |
|
| 895 |
$late_before_reset = FALSE; |
| 896 |
$is_late_correction = $result === 'late' && !empty($load['load_id']) && !empty($load['load_time']) && !empty($load['orig_result']) && isset($load['orig_6hour']) && !empty($load['signature']); |
| 897 |
if($is_late_correction) |
| 898 |
{ |
| 899 |
$page = self::verified_asset_health_page_context($load); //260913.0048 Browser-returned page context is useful only when its separate signature matches. |
| 900 |
$load['load_id'] = preg_replace('/[^a-f0-9]/', '', strtolower((string)$load['load_id'])); |
| 901 |
$load['load_time'] = (int)$load['load_time']; |
| 902 |
$load['orig_result'] = strtolower((string)$load['orig_result']); |
| 903 |
$load['orig_6hour'] = !empty($load['orig_6hour']) ? 1 : 0; |
| 904 |
$signature = (string)$load['signature']; |
| 905 |
unset($load['signature']); |
| 906 |
$is_late_correction = strlen($load['load_id']) === 40 && $load['load_time'] > 0 && in_array($load['orig_result'], array('okay', 'fallback'), TRUE) && hash_equals(self::asset_health_load_signature($load), $signature); |
| 907 |
if(!$is_late_correction) |
| 908 |
$page = array('page_id' => 0, 'page_path' => ''); |
| 909 |
if($is_late_correction && !empty($state['history_reset_at']) && $load['load_time'] < (int)$state['history_reset_at']) |
| 910 |
{ |
| 911 |
$late_before_reset = TRUE; |
| 912 |
$is_late_correction = FALSE; |
| 913 |
} |
| 914 |
} |
| 915 |
else |
| 916 |
{ |
| 917 |
$page['page_id'] = (!empty($load['page_id'])) ? max(0, (int)$load['page_id']) : 0; |
| 918 |
$page['page_path'] = (!empty($load['page_path'])) ? substr((string)$load['page_path'], 0, 240) : ''; |
| 919 |
} |
| 920 |
|
| 921 |
if($late_before_reset) |
| 922 |
{ |
| 923 |
foreach($issues as $late_issue) |
| 924 |
if(is_array($late_issue)) |
| 925 |
{ |
| 926 |
self::add_asset_health_latest_issue($state, $load['load_time'], 'late', $late_issue, $page); |
| 927 |
self::set_asset_health_last_issue($state, $load['load_time'], 'late', (!empty($late_issue['label'])) ? $late_issue['label'] : '', (!empty($late_issue['detail'])) ? $late_issue['detail'] : '', (!empty($load['load_id'])) ? $load['load_id'] : '', $page['page_id'], $page['page_path']); |
| 928 |
} |
| 929 |
return TRUE; //260911.2356 Old delayed reports remain useful history but never re-enter a newer scoring epoch. |
| 930 |
} |
| 931 |
|
| 932 |
$original_contributed_to_6hour = FALSE; |
| 933 |
if($is_late_correction) |
| 934 |
{ |
| 935 |
$orig_rating = self::asset_health_load_rating($load['orig_result']); |
| 936 |
if($rating >= $orig_rating) |
| 937 |
return TRUE; // Fallback is already worse than Late. |
| 938 |
$late_key = 'ws_plugin__s2member_asset_load_late_'.$load['load_id']; |
| 939 |
if(get_transient($late_key)) |
| 940 |
return TRUE; |
| 941 |
|
| 942 |
$found = FALSE; |
| 943 |
foreach($state['last_10_asset_loads'] as &$entry) |
| 944 |
if(!empty($entry['load_id']) && hash_equals((string)$entry['load_id'], $load['load_id'])) |
| 945 |
{ |
| 946 |
$original_contributed_to_6hour = !empty($entry['orig_6hour']); |
| 947 |
$entry['result'] = 'late'; |
| 948 |
$found = TRUE; |
| 949 |
break; |
| 950 |
} |
| 951 |
unset($entry); |
| 952 |
//260912.0258 A queued Late correction may arrive after its original load was processed, so derive six-hour membership from retained server state. |
| 953 |
if(!$found && !empty($state['not_green_since']) && (int)$state['not_green_since'] <= $load['load_time']) |
| 954 |
$original_contributed_to_6hour = TRUE; |
| 955 |
|
| 956 |
$minute_end = self::asset_health_period_end($load['load_time'], MINUTE_IN_SECONDS); |
| 957 |
$delta = $rating - $orig_rating; |
| 958 |
if(isset($state['last_10min_minutes'][$minute_end]) && !empty($state['last_10min_minutes'][$minute_end]['count'])) |
| 959 |
$state['last_10min_minutes'][$minute_end]['sum'] += $delta; |
| 960 |
set_transient($late_key, 1, HOUR_IN_SECONDS); |
| 961 |
} |
| 962 |
else |
| 963 |
{ |
| 964 |
$load_id = (!empty($load['load_id'])) ? preg_replace('/[^a-f0-9]/', '', strtolower((string)$load['load_id'])) : ''; |
| 965 |
$load_id = (strlen($load_id) === 40) ? $load_id : sha1(microtime(TRUE)."\0".wp_rand()."\0".home_url('/')); |
| 966 |
$load_time = (!empty($load['load_time'])) ? max(1, (int)$load['load_time']) : $event_time; |
| 967 |
$load = array('load_id' => $load_id, 'load_time' => $load_time, 'orig_result' => $result, 'orig_6hour' => 0, 'page_id' => $page['page_id'], 'page_path' => $page['page_path']); |
| 968 |
//260912.0551 The Health Logkeeper already processes queued loads in event-time/queue-time order; preserve that order so simultaneous same-second requests are not randomized by load ID. |
| 969 |
$state['last_10_asset_loads'][] = array('time' => $load_time, 'result' => $result, 'load_id' => $load_id, 'orig_6hour' => 0); |
| 970 |
$state['last_10_asset_loads'] = array_slice($state['last_10_asset_loads'], -10); |
| 971 |
|
| 972 |
$minute_end = self::asset_health_period_end($load_time, MINUTE_IN_SECONDS); |
| 973 |
if(empty($state['last_10min_minutes'][$minute_end]) || !is_array($state['last_10min_minutes'][$minute_end])) |
| 974 |
$state['last_10min_minutes'][$minute_end] = array('sum' => 0.0, 'count' => 0); |
| 975 |
$state['last_10min_minutes'][$minute_end]['sum'] += $rating; |
| 976 |
$state['last_10min_minutes'][$minute_end]['count']++; |
| 977 |
} |
| 978 |
|
| 979 |
$current_minute_end = self::asset_health_period_end($now, MINUTE_IN_SECONDS); |
| 980 |
foreach($state['last_10min_minutes'] as $minute_end => $bucket) |
| 981 |
if((int)$minute_end < $current_minute_end - 9 * MINUTE_IN_SECONDS || (int)$minute_end > $current_minute_end) |
| 982 |
unset($state['last_10min_minutes'][$minute_end]); |
| 983 |
|
| 984 |
$scores = self::asset_health_scores($state); |
| 985 |
$status = self::asset_health_status_from_score($scores['score'], $scores['latest_result']); |
| 986 |
$previous_status = (!empty($state['status'])) ? (string)$state['status'] : 'unknown'; |
| 987 |
$state['status'] = $status; |
| 988 |
|
| 989 |
if($status === 'healthy') |
| 990 |
{ |
| 991 |
$state['not_green_since'] = 0; |
| 992 |
$state['last_6hour_10min_blocks'] = array(); |
| 993 |
//260912.1959 Rolling delivery may be Healthy while a trusted standby fallback is still unavailable; keep that combined-health notice dismissal until the fallback recovers. |
| 994 |
if(!self::asset_health_fallback_problem_active()) |
| 995 |
delete_option('ws_plugin__s2member_asset_notice_dismissed'); |
| 996 |
} |
| 997 |
else |
| 998 |
{ |
| 999 |
if(empty($state['not_green_since'])) |
| 1000 |
$state['not_green_since'] = ($is_late_correction) ? $load['load_time'] : $event_time; |
| 1001 |
$block_time = ($is_late_correction) ? $load['load_time'] : $event_time; |
| 1002 |
$block_end = self::asset_health_period_end($block_time, 10 * MINUTE_IN_SECONDS); |
| 1003 |
if($is_late_correction && $original_contributed_to_6hour && isset($state['last_6hour_10min_blocks'][$block_end]) && !empty($state['last_6hour_10min_blocks'][$block_end]['count'])) |
| 1004 |
$state['last_6hour_10min_blocks'][$block_end]['sum'] += $rating - self::asset_health_load_rating($load['orig_result']); |
| 1005 |
else if(!$is_late_correction || !$original_contributed_to_6hour) |
| 1006 |
{ |
| 1007 |
if(empty($state['last_6hour_10min_blocks'][$block_end]) || !is_array($state['last_6hour_10min_blocks'][$block_end])) |
| 1008 |
$state['last_6hour_10min_blocks'][$block_end] = array('sum' => 0.0, 'count' => 0); |
| 1009 |
$state['last_6hour_10min_blocks'][$block_end]['sum'] += $rating; |
| 1010 |
$state['last_6hour_10min_blocks'][$block_end]['count']++; |
| 1011 |
if(!$is_late_correction) |
| 1012 |
foreach($state['last_10_asset_loads'] as &$entry) |
| 1013 |
if(!empty($entry['load_id']) && hash_equals((string)$entry['load_id'], (string)$load['load_id'])) |
| 1014 |
{ |
| 1015 |
$entry['orig_6hour'] = 1; |
| 1016 |
break; |
| 1017 |
} |
| 1018 |
unset($entry); |
| 1019 |
} |
| 1020 |
$cutoff = $now - 6 * HOUR_IN_SECONDS; |
| 1021 |
foreach($state['last_6hour_10min_blocks'] as $end => $bucket) |
| 1022 |
if((int)$end <= $cutoff) |
| 1023 |
unset($state['last_6hour_10min_blocks'][$end]); |
| 1024 |
} |
| 1025 |
|
| 1026 |
if($previous_status === 'healthy' && $status !== 'healthy') |
| 1027 |
delete_option('ws_plugin__s2member_asset_notice_dismissed'); |
| 1028 |
if($result !== 'okay') |
| 1029 |
{ |
| 1030 |
$issue_time = ($is_late_correction && !empty($load['load_time'])) ? (int)$load['load_time'] : $event_time; |
| 1031 |
foreach($issues as $health_issue) |
| 1032 |
if(is_array($health_issue)) |
| 1033 |
{ |
| 1034 |
self::add_asset_health_latest_issue($state, $issue_time, $result, $health_issue, $page); |
| 1035 |
self::set_asset_health_last_issue($state, $issue_time, $result, (!empty($health_issue['label'])) ? $health_issue['label'] : '', (!empty($health_issue['detail'])) ? $health_issue['detail'] : '', (!empty($load['load_id'])) ? $load['load_id'] : '', $page['page_id'], $page['page_path']); |
| 1036 |
} |
| 1037 |
} |
| 1038 |
return TRUE; |
| 1039 |
} |
| 1040 |
|
| 1041 |
/** |
| 1042 |
* Synchronizes time-derived status fields after queued events are merged. |
| 1043 |
* |
| 1044 |
* @package s2Member\Utilities |
| 1045 |
* @since 260911.2356 |
| 1046 |
* |
| 1047 |
* @param array $state Rolling health-log state, passed by reference. |
| 1048 |
* @return bool True when derived state changed. |
| 1049 |
*/ |
| 1050 |
protected static function sync_asset_health_derived_state(&$state) |
| 1051 |
{ |
| 1052 |
$scores = self::asset_health_scores($state); |
| 1053 |
$overall = self::asset_health_status_from_score($scores['score'], $scores['latest_result']); |
| 1054 |
$changed = (!isset($state['status']) || (string)$state['status'] !== $overall); |
| 1055 |
$state['status'] = $overall; |
| 1056 |
|
| 1057 |
if($overall === 'healthy') |
| 1058 |
{ |
| 1059 |
if(!empty($state['not_green_since']) || !empty($state['last_6hour_10min_blocks'])) |
| 1060 |
{ |
| 1061 |
$state['not_green_since'] = 0; |
| 1062 |
$state['last_6hour_10min_blocks'] = array(); |
| 1063 |
$changed = TRUE; |
| 1064 |
//260912.1959 Do not clear a dismissed combined-health notice while the independent fallback problem is still active. |
| 1065 |
if(!self::asset_health_fallback_problem_active()) |
| 1066 |
delete_option('ws_plugin__s2member_asset_notice_dismissed'); |
| 1067 |
} |
| 1068 |
} |
| 1069 |
else if($overall !== 'unknown' && empty($state['not_green_since'])) |
| 1070 |
{ |
| 1071 |
$state['not_green_since'] = time(); |
| 1072 |
$changed = TRUE; |
| 1073 |
} |
| 1074 |
return $changed; |
| 1075 |
} |
| 1076 |
|
| 1077 |
/** |
| 1078 |
* Runs the Health Logkeeper, merging queued frontend asset-health events into the rolling log. |
| 1079 |
* |
| 1080 |
* The Logkeeper never waits for another run. Frontend requests only queue separate event options, |
| 1081 |
* so page delivery is never serialized behind health-log maintenance. |
| 1082 |
* |
| 1083 |
* @package s2Member\Utilities |
| 1084 |
* @since 260912.0258 |
| 1085 |
* |
| 1086 |
* @attaches-to ``add_action('ws_plugin__s2member_assets_health_logkeeper');`` |
| 1087 |
* @return int Number of queued events handled. |
| 1088 |
*/ |
| 1089 |
public static function run_health_logkeeper() |
| 1090 |
{ |
| 1091 |
$lock = self::health_logkeeper_lock_acquire(); |
| 1092 |
if($lock === '') |
| 1093 |
{ |
| 1094 |
//260912.0258 Never wait for a live Logkeeper; leave a background retry so a one-off scheduling collision cannot strand queued events. |
| 1095 |
if(!wp_next_scheduled('ws_plugin__s2member_assets_health_logkeeper')) |
| 1096 |
wp_schedule_single_event(time() + 30, 'ws_plugin__s2member_assets_health_logkeeper'); |
| 1097 |
return 0; |
| 1098 |
} |
| 1099 |
|
| 1100 |
global $wpdb; |
| 1101 |
$prefix = self::asset_health_event_option_prefix(); |
| 1102 |
$like = $wpdb->esc_like($prefix).'%'; |
| 1103 |
//260912.0258 Fetch each queued option and its value in one indexed prefix query; the timestamp-based option names already provide chronological order. |
| 1104 |
$rows = $wpdb->get_results($wpdb->prepare("SELECT option_name, option_value FROM {$wpdb->options} WHERE option_name LIKE %s ORDER BY option_name ASC LIMIT 100", $like)); |
| 1105 |
$events = array(); |
| 1106 |
foreach((array)$rows as $row) |
| 1107 |
{ |
| 1108 |
$option_name = (!empty($row->option_name)) ? (string)$row->option_name : ''; |
| 1109 |
$event_times = ($option_name !== '' && strpos($option_name, $prefix) === 0) ? substr($option_name, strlen($prefix)) : ''; |
| 1110 |
$event = c_ws_plugin__s2member_utils_arrays::maybe_unserialize(isset($row->option_value) ? $row->option_value : NULL); |
| 1111 |
if(!preg_match('/^\\d{12}_\\d{18}$/D', $event_times) || !is_array($event)) |
| 1112 |
{ |
| 1113 |
//260912.0258 Malformed telemetry is disposable; delete it instead of carrying unexpected data into the health log. |
| 1114 |
if($option_name !== '') |
| 1115 |
delete_option($option_name); |
| 1116 |
continue; |
| 1117 |
} |
| 1118 |
$events[] = array('option_name' => $option_name, 'event_times' => $event_times, 'event' => $event); |
| 1119 |
} |
| 1120 |
|
| 1121 |
$state = self::asset_health_log_state(); |
| 1122 |
$processed = (!empty($state['processed_event_times']) && is_array($state['processed_event_times'])) ? array_fill_keys($state['processed_event_times'], TRUE) : array(); |
| 1123 |
$handled = 0; |
| 1124 |
$changed = FALSE; |
| 1125 |
foreach($events as $queued) |
| 1126 |
{ |
| 1127 |
$event = $queued['event']; |
| 1128 |
$event_times = $queued['event_times']; |
| 1129 |
if(empty($processed[$event_times])) |
| 1130 |
{ |
| 1131 |
$type = (!empty($event['type'])) ? strtolower((string)$event['type']) : ''; |
| 1132 |
$valid = FALSE; |
| 1133 |
if($type === 'load') |
| 1134 |
$valid = self::apply_asset_health_load_event($state, $event); |
| 1135 |
else if($type === 'issue' && !empty($event['result'])) |
| 1136 |
{ |
| 1137 |
$issue = (!empty($event['issue']) && is_array($event['issue'])) ? $event['issue'] : array(); |
| 1138 |
$page = (!empty($event['page']) && is_array($event['page'])) ? $event['page'] : array(); |
| 1139 |
$issue_time = (!empty($event['event_time'])) ? (int)$event['event_time'] : time(); |
| 1140 |
$changed = self::add_asset_health_latest_issue($state, $issue_time, (string)$event['result'], $issue, $page) || $changed; |
| 1141 |
$changed = self::set_asset_health_last_issue($state, $issue_time, (string)$event['result'], (!empty($issue['label'])) ? (string)$issue['label'] : '', (!empty($issue['detail'])) ? (string)$issue['detail'] : '', '', (!empty($page['page_id'])) ? (int)$page['page_id'] : 0, (!empty($page['page_path'])) ? (string)$page['page_path'] : '') || $changed; |
| 1142 |
$valid = TRUE; |
| 1143 |
} |
| 1144 |
if(!$valid) |
| 1145 |
{ |
| 1146 |
delete_option($queued['option_name']); |
| 1147 |
continue; |
| 1148 |
} |
| 1149 |
if($type === 'load') |
| 1150 |
$changed = TRUE; |
| 1151 |
$state['processed_event_times'][] = $event_times; |
| 1152 |
$state['processed_event_times'] = array_slice(array_values(array_unique($state['processed_event_times'])), -100); |
| 1153 |
$processed[$event_times] = TRUE; |
| 1154 |
} |
| 1155 |
$handled++; |
| 1156 |
} |
| 1157 |
$changed = self::sync_asset_health_derived_state($state) || $changed; |
| 1158 |
|
| 1159 |
$stored = TRUE; |
| 1160 |
if($changed || $events) |
| 1161 |
{ |
| 1162 |
$stored = update_option('ws_plugin__s2member_assets_health_log', $state, FALSE); |
| 1163 |
if(!$stored) |
| 1164 |
$stored = serialize(self::asset_health_log_state()) === serialize($state); //260912.0258 update_option() also returns false when the requested value is already stored; distinguish that harmless case from a failed write before deleting queue rows. |
| 1165 |
} |
| 1166 |
if($stored) |
| 1167 |
{ |
| 1168 |
//260912.0258 Delete only after the merged state and processed event-times are stored; if interrupted first, the next Logkeeper run can safely deduplicate the retained queue rows. |
| 1169 |
foreach($events as $queued) |
| 1170 |
delete_option($queued['option_name']); |
| 1171 |
} |
| 1172 |
|
| 1173 |
if((!$stored || count((array)$rows) >= 100) && !wp_next_scheduled('ws_plugin__s2member_assets_health_logkeeper')) |
| 1174 |
wp_schedule_single_event(time() + 5, 'ws_plugin__s2member_assets_health_logkeeper'); |
| 1175 |
self::health_logkeeper_lock_release($lock); |
| 1176 |
return ($stored) ? $handled : 0; |
| 1177 |
} |
| 1178 |
|
| 1179 |
/** |
| 1180 |
* Clears one administrator-selected Asset Health troubleshooting summary without changing scoring. |
| 1181 |
* |
| 1182 |
* @package s2Member\Utilities |
| 1183 |
* @since 260913.0056 |
| 1184 |
* |
| 1185 |
* @return null Exits through WordPress JSON helpers. |
| 1186 |
*/ |
| 1187 |
public static function ajax_clear_asset_health_details() |
| 1188 |
{ |
| 1189 |
if(!current_user_can('create_users')) |
| 1190 |
wp_send_json_error(array('message' => 'You do not have permission to clear Asset Health details.'), 403); |
| 1191 |
check_ajax_referer('ws-plugin--s2member-clear-asset-health-details'); |
| 1192 |
$scope = (!empty($_POST['scope'])) ? sanitize_key(wp_unslash($_POST['scope'])) : ''; |
| 1193 |
if(!in_array($scope, array('last_issue', 'latest_issues'), TRUE)) |
| 1194 |
wp_send_json_error(array('message' => 'Invalid Asset Health clear request.'), 400); |
| 1195 |
|
| 1196 |
$lock = self::health_logkeeper_lock_acquire(); |
| 1197 |
if($lock === '') |
| 1198 |
wp_send_json_error(array('message' => 'Asset Health is updating. Please try again.'), 409); |
| 1199 |
$state = self::asset_health_log_state(); |
| 1200 |
$now = time(); |
| 1201 |
if($scope === 'last_issue') |
| 1202 |
{ |
| 1203 |
$state['last_issue'] = array(); |
| 1204 |
$state['last_issue_cleared_at'] = $now; |
| 1205 |
} |
| 1206 |
else |
| 1207 |
{ |
| 1208 |
$state['latest_issues'] = array(); |
| 1209 |
$state['latest_issues_cleared_at'] = $now; |
| 1210 |
} |
| 1211 |
$stored = update_option('ws_plugin__s2member_assets_health_log', $state, FALSE); |
| 1212 |
if(!$stored) |
| 1213 |
$stored = serialize(self::asset_health_log_state()) === serialize($state); |
| 1214 |
self::health_logkeeper_lock_release($lock); |
| 1215 |
if(!$stored) |
| 1216 |
wp_send_json_error(array('message' => 'Asset Health details could not be cleared.'), 500); |
| 1217 |
wp_send_json_success(array('message' => ($scope === 'last_issue') ? 'Last issue cleared.' : 'Latest Issues cleared.')); |
| 1218 |
} |
| 1219 |
|
| 1220 |
/** |
| 1221 |
* Returns the page-level Okay/Fallback result for the delivery routes selected by WordPress. |
| 1222 |
* |
| 1223 |
* A normal configured route is Okay. Compatibility-required Full WordPress Dynamic delivery is |
| 1224 |
* also Okay because it is the correct route for the current request/configuration. WordPress Dynamic |
| 1225 |
* is Fallback only when a requested route unexpectedly could not be used. Browser activation is |
| 1226 |
* checked separately by the frontend activation monitor. |
| 1227 |
* |
| 1228 |
* @package s2Member\Utilities |
| 1229 |
* @since 260910.0630 |
| 1230 |
* |
| 1231 |
* @return string `okay` or `fallback`. |
| 1232 |
*/ |
| 1233 |
protected static function page_asset_health_load_result() |
| 1234 |
{ |
| 1235 |
$selected_s2o = empty($GLOBALS['WS_PLUGIN__']['s2member']['o']['dynamic_asset_loader']) || $GLOBALS['WS_PLUGIN__']['s2member']['o']['dynamic_asset_loader'] !== 'wordpress'; |
| 1236 |
//260910.0818 A page is Fallback only when WordPress had to choose Full WordPress Dynamic instead of a requested static or selected s2member-o.php route; intentionally selected Full WordPress Dynamic is Okay. |
| 1237 |
foreach(self::$page_asset_expectations as $expectation) |
| 1238 |
{ |
| 1239 |
$type = (!empty($expectation['type'])) ? (string)$expectation['type'] : ''; |
| 1240 |
if(!in_array($type, array('css', 'js'), TRUE)) |
| 1241 |
continue; |
| 1242 |
if(!empty($expectation['delivery']) && $expectation['delivery'] === 'dynamic-wordpress' && (!empty($GLOBALS['WS_PLUGIN__']['s2member']['o']['static_'.$type]) || $selected_s2o)) |
| 1243 |
{ |
| 1244 |
//260913.2001 Static delivery can be intentionally incompatible with current hooks/configuration; successful required Dynamic delivery is the correct route, not a degraded fallback. |
| 1245 |
if(!empty($expectation['dynamic_required']) && !empty($GLOBALS['WS_PLUGIN__']['s2member']['o']['static_'.$type])) |
| 1246 |
continue; |
| 1247 |
return 'fallback'; |
| 1248 |
} |
| 1249 |
} |
| 1250 |
return 'okay'; |
| 1251 |
} |
| 1252 |
|
| 1253 |
/** |
| 1254 |
* Returns compact context for a non-Okay page-level delivery result. |
| 1255 |
* |
| 1256 |
* @package s2Member\Utilities |
| 1257 |
* @since 260911.1806 |
| 1258 |
* |
| 1259 |
* @param string $result Page-level asset-load result. |
| 1260 |
* @return array Issue snapshot with `label` and `detail`. |
| 1261 |
*/ |
| 1262 |
protected static function page_asset_health_issue($result = '') |
| 1263 |
{ |
| 1264 |
$result = strtolower((string)$result); |
| 1265 |
if($result !== 'fallback') |
| 1266 |
return array(); |
| 1267 |
|
| 1268 |
$selected_s2o = empty($GLOBALS['WS_PLUGIN__']['s2member']['o']['dynamic_asset_loader']) || $GLOBALS['WS_PLUGIN__']['s2member']['o']['dynamic_asset_loader'] !== 'wordpress'; |
| 1269 |
foreach(self::$page_asset_expectations as $expectation) |
| 1270 |
{ |
| 1271 |
$type = (!empty($expectation['type'])) ? (string)$expectation['type'] : ''; |
| 1272 |
if(!in_array($type, array('css', 'js'), TRUE) || empty($expectation['delivery']) || $expectation['delivery'] !== 'dynamic-wordpress') |
| 1273 |
continue; |
| 1274 |
if(!empty($GLOBALS['WS_PLUGIN__']['s2member']['o']['static_'.$type])) |
| 1275 |
{ |
| 1276 |
$asset_id = (!empty($expectation['asset_id'])) ? (string)$expectation['asset_id'] : ''; |
| 1277 |
$health_id = ($asset_id !== '') ? self::asset_runtime_health_id($asset_id, $type, 'static') : ''; |
| 1278 |
return array( |
| 1279 |
'asset' => ($health_id !== '') ? $health_id : (string)$asset_id, |
| 1280 |
'label' => ($health_id !== '') ? self::asset_runtime_health_label($health_id) : strtoupper($type).' delivery', |
| 1281 |
'delivery' => 'Static → Full WordPress Dynamic fallback', |
| 1282 |
'detail' => (!empty($expectation['issue_detail'])) ? (string)$expectation['issue_detail'] : 'Requested static delivery was unavailable, so Full WordPress Dynamic fallback was used.', |
| 1283 |
); |
| 1284 |
} |
| 1285 |
if($selected_s2o) |
| 1286 |
return array( |
| 1287 |
'asset' => 'dynamic_'.$type, |
| 1288 |
'label' => 'Dynamic '.(($type === 'js') ? 'JS' : 'CSS'), |
| 1289 |
'delivery' => 's2Member-Only → Full WordPress Dynamic fallback', |
| 1290 |
'detail' => 'The selected s2Member-Only Dynamic Loader was unavailable, so Full WordPress Dynamic fallback was used.', |
| 1291 |
); |
| 1292 |
} |
| 1293 |
return array(); |
| 1294 |
} |
| 1295 |
|
| 1296 |
/** |
| 1297 |
* Returns Okay/Fallback/Failed for the site's currently required delivery using trusted failure state. |
| 1298 |
* |
| 1299 |
* @package s2Member\Utilities |
| 1300 |
* @since 260910.0630 |
| 1301 |
* |
| 1302 |
* @param array $failures Trusted current probe failures. |
| 1303 |
* @return string `okay`, `fallback`, or `failed`. |
| 1304 |
*/ |
| 1305 |
protected static function asset_health_current_delivery_result($failures = array()) |
| 1306 |
{ |
| 1307 |
$failures = (is_array($failures)) ? $failures : array(); |
| 1308 |
$selected_s2o = empty($GLOBALS['WS_PLUGIN__']['s2member']['o']['dynamic_asset_loader']) || $GLOBALS['WS_PLUGIN__']['s2member']['o']['dynamic_asset_loader'] !== 'wordpress'; |
| 1309 |
$local_health = self::static_assets_health(TRUE); |
| 1310 |
$location = self::static_assets_location(FALSE); |
| 1311 |
$overall_rating = 4; |
| 1312 |
|
| 1313 |
//260910.0818 Trusted current delivery takes the worse CSS/JS result: 4=preferred route works, 2=WordPress fallback works, 1=no usable route verifies; Late is browser timing evidence and is not manufactured here. |
| 1314 |
foreach(array('css', 'js') as $type) |
| 1315 |
{ |
| 1316 |
$type_rating = 4; |
| 1317 |
if(!empty($GLOBALS['WS_PLUGIN__']['s2member']['o']['static_'.$type])) |
| 1318 |
{ |
| 1319 |
$dynamic_requirement = self::static_type_dynamic_requirement($type); |
| 1320 |
if(!empty($dynamic_requirement['required'])) |
| 1321 |
$type_rating = (!empty($failures['dynamic:dynamic_'.$type])) ? 1 : 4; //260913.2001 Compatibility-required Dynamic delivery is the intended route; only failure of that route degrades Health. |
| 1322 |
else |
| 1323 |
{ |
| 1324 |
$fallback = !empty($local_health['location']); |
| 1325 |
foreach(self::static_asset_ids($type, 'all') as $id) |
| 1326 |
{ |
| 1327 |
$state = self::static_asset_build($id); |
| 1328 |
$definition = self::static_asset_definition($id, FALSE); |
| 1329 |
$generation_failure = get_transient('ws_plugin__s2member_static_asset_failure_'.str_replace('.', '_', $id)); |
| 1330 |
if(empty($definition['ok']) || ($generation_failure && $state <= 0) || isset($local_health[$id])) |
| 1331 |
$fallback = TRUE; |
| 1332 |
if($state > 0 && !empty($location['ok'])) |
| 1333 |
{ |
| 1334 |
$base = substr($id, 0, -strlen('.'.$type)); |
| 1335 |
$url = $location['url'].'/'.$base.'-'.$state.'.'.$type; |
| 1336 |
if(!empty($failures['static:'.$id]) && !empty($failures['static:'.$id]['url']) && (string)$failures['static:'.$id]['url'] === $url) |
| 1337 |
$fallback = TRUE; |
| 1338 |
} |
| 1339 |
} |
| 1340 |
if($fallback) |
| 1341 |
$type_rating = (!empty($failures['fallback:dynamic_'.$type])) ? 1 : 2; |
| 1342 |
} |
| 1343 |
} |
| 1344 |
else if($selected_s2o) |
| 1345 |
{ |
| 1346 |
$s2o_problem = !is_file(self::s2o_file_path()) || (!empty($failures['s2o'])); |
| 1347 |
if($s2o_problem) |
| 1348 |
$type_rating = (!empty($failures['fallback:dynamic_'.$type])) ? 1 : 2; |
| 1349 |
} |
| 1350 |
else if(!empty($failures['dynamic:dynamic_'.$type])) |
| 1351 |
$type_rating = 1; |
| 1352 |
|
| 1353 |
$overall_rating = min($overall_rating, $type_rating); |
| 1354 |
} |
| 1355 |
return ($overall_rating <= 1) ? 'failed' : (($overall_rating === 2) ? 'fallback' : 'okay'); |
| 1356 |
} |
| 1357 |
|
| 1358 |
/** |
| 1359 |
* Returns recent low-trust runtime suspicions reported by real frontend pages. |
| 1360 |
* |
| 1361 |
* Reports are only hints. They never change delivery by themselves. A trusted administrator-browser probe must confirm the exact asset response before persistent fallback or a confirmed notice is used. |
| 1362 |
* |
| 1363 |
* @package s2Member\Utilities |
| 1364 |
* @since 260904.2255 |
| 1365 |
* |
| 1366 |
* @return array Current runtime suspicions. |
| 1367 |
*/ |
| 1368 |
protected static function asset_runtime_suspicions() |
| 1369 |
{ |
| 1370 |
$suspicions = get_option('ws_plugin__s2member_asset_runtime_suspicions', array()); |
| 1371 |
$suspicions = (is_array($suspicions)) ? $suspicions : array(); |
| 1372 |
foreach($suspicions as $key => $suspicion) |
| 1373 |
if(empty($suspicion['reported']) || (int)$suspicion['reported'] < time() - HOUR_IN_SECONDS) |
| 1374 |
unset($suspicions[$key]); |
| 1375 |
return $suspicions; |
| 1376 |
} |
| 1377 |
|
| 1378 |
/** |
| 1379 |
* Returns the current public asset URLs that an administrator's browser should probe. |
| 1380 |
* |
| 1381 |
* Normal checks are deliberately cheap. Static files use HEAD and s2member-o.php has a special early health response that exits before loading WordPress. A real-page suspicion adds a one-time full activation-tag check for the exact asset that page expected. |
| 1382 |
* A full Health-panel/recheck probe additionally verifies activation tags for the configured dynamic route, |
| 1383 |
* the WordPress Dynamic fallback, and static files so it can produce a fresh Okay/Fallback/Failed asset-load result. |
| 1384 |
* |
| 1385 |
* @package s2Member\Utilities |
| 1386 |
* @since 260904.2110 |
| 1387 |
* |
| 1388 |
* @param bool $full Include current-delivery/fallback activation-tag checks for a fresh asset-load health result. |
| 1389 |
* @return array Health targets keyed by logical target ID. |
| 1390 |
*/ |
| 1391 |
protected static function asset_http_health_targets($full = FALSE) |
| 1392 |
{ |
| 1393 |
$targets = array(); |
| 1394 |
$selected_s2o = empty($GLOBALS['WS_PLUGIN__']['s2member']['o']['dynamic_asset_loader']) || $GLOBALS['WS_PLUGIN__']['s2member']['o']['dynamic_asset_loader'] !== 'wordpress'; |
| 1395 |
if($selected_s2o && is_file(self::s2o_file_path())) |
| 1396 |
$targets['s2o'] = array( |
| 1397 |
'id' => 's2o', |
| 1398 |
'url' => $GLOBALS['WS_PLUGIN__']['s2member']['c']['s2o_url'], |
| 1399 |
'probe_url' => add_query_arg('s2member_health_check', '1', $GLOBALS['WS_PLUGIN__']['s2member']['c']['s2o_url']), |
| 1400 |
'type' => 'health', |
| 1401 |
'mode' => 's2o-health', |
| 1402 |
'label' => 's2Member-Only Dynamic Loader', |
| 1403 |
'failure_id' => 's2o', |
| 1404 |
'failure_url' => $GLOBALS['WS_PLUGIN__']['s2member']['c']['s2o_url'], |
| 1405 |
); |
| 1406 |
|
| 1407 |
$location = self::static_assets_location(FALSE); |
| 1408 |
if(!empty($location['ok'])) |
| 1409 |
foreach(array('css' => 'static_css', 'js' => 'static_js') as $type => $option) |
| 1410 |
if(!empty($GLOBALS['WS_PLUGIN__']['s2member']['o'][$option])) |
| 1411 |
{ |
| 1412 |
$dynamic_requirement = self::static_type_dynamic_requirement($type); |
| 1413 |
if(!empty($dynamic_requirement['required'])) |
| 1414 |
continue; //260913.2001 Intentionally inactive static files are not trusted-probe targets while compatibility requires Dynamic delivery. |
| 1415 |
foreach(self::static_asset_ids($type, 'all') as $id) |
| 1416 |
{ |
| 1417 |
$build = self::static_asset_build($id); |
| 1418 |
if($build <= 0) |
| 1419 |
continue; |
| 1420 |
$base = substr($id, 0, -strlen('.'.$type)); |
| 1421 |
$url = $location['url'].'/'.$base.'-'.$build.'.'.$type; |
| 1422 |
if(is_file($location['dir'].'/'.$base.'-'.$build.'.'.$type)) |
| 1423 |
{ |
| 1424 |
$target = array( |
| 1425 |
'id' => 'static:'.$id, |
| 1426 |
'url' => $url, |
| 1427 |
'probe_url' => $url, |
| 1428 |
'type' => $type, |
| 1429 |
'mode' => ($full) ? 'activation-tag' : 'head', |
| 1430 |
'label' => self::asset_runtime_health_label(self::asset_runtime_health_id($id, $type, 'static')), |
| 1431 |
'failure_id' => 'static:'.$id, |
| 1432 |
'failure_url' => $url, |
| 1433 |
); |
| 1434 |
if($full) |
| 1435 |
{ |
| 1436 |
//260912.0522 Full probes validate the activation tag too; cheap background probes stay HEAD-only to avoid unnecessary body downloads. |
| 1437 |
$health_id = self::asset_runtime_health_id($id, $type, 'static'); |
| 1438 |
$tag_value = ($type === 'css') ? (string)(int)$build : 'static-'.(int)$build; |
| 1439 |
$target['activation_tags'] = array(self::activation_tag_snippet($health_id, $type, $tag_value)); |
| 1440 |
} |
| 1441 |
$targets['static:'.$id] = $target; |
| 1442 |
} |
| 1443 |
} |
| 1444 |
} |
| 1445 |
|
| 1446 |
if($full) |
| 1447 |
{ |
| 1448 |
//260910.0818 A full trusted check includes the actual dynamic response and its available WordPress fallback so Fallback can be distinguished from Failed instead of assuming a fallback works. |
| 1449 |
foreach(array('css', 'js') as $type) |
| 1450 |
{ |
| 1451 |
$health_id = 'dynamic_'.$type; |
| 1452 |
$wordpress_url = self::wordpress_dynamic_asset_url(); |
| 1453 |
$wordpress_url = ($type === 'css') |
| 1454 |
? add_query_arg(array('ws_plugin__s2member_css' => '1', 'qcABC' => '1'), $wordpress_url) |
| 1455 |
: add_query_arg(array('ws_plugin__s2member_js_w_globals' => '1', 'qcABC' => '1'), $wordpress_url); |
| 1456 |
$wordpress_tag_value = ($type === 'css') ? '2147483640' : 'dynamic-wordpress'; |
| 1457 |
$wordpress_target = array( |
| 1458 |
'url' => $wordpress_url, |
| 1459 |
'probe_url' => $wordpress_url, |
| 1460 |
'type' => $type, |
| 1461 |
'mode' => 'activation-tag', |
| 1462 |
'label' => 'WP Loader '.(($type === 'css') ? 'CSS' : 'JS'), |
| 1463 |
'activation_tags' => array(self::activation_tag_snippet($health_id, $type, $wordpress_tag_value)), |
| 1464 |
); |
| 1465 |
|
| 1466 |
if(!empty($GLOBALS['WS_PLUGIN__']['s2member']['o']['static_'.$type])) |
| 1467 |
{ |
| 1468 |
$dynamic_requirement = self::static_type_dynamic_requirement($type); |
| 1469 |
//260913.2001 Probe compatibility-required Full WordPress Dynamic as the active route; genuine static failures keep the existing fallback probe identity. |
| 1470 |
$target_id = (!empty($dynamic_requirement['required'])) ? 'active:'.$health_id : 'fallback:'.$health_id; |
| 1471 |
$wordpress_target['id'] = $target_id; |
| 1472 |
$wordpress_target['failure_id'] = (!empty($dynamic_requirement['required'])) ? 'dynamic:'.$health_id : $target_id; |
| 1473 |
$wordpress_target['failure_url'] = $wordpress_url; |
| 1474 |
$targets[$target_id] = $wordpress_target; |
| 1475 |
} |
| 1476 |
else if($selected_s2o) |
| 1477 |
{ |
| 1478 |
if(is_file(self::s2o_file_path())) |
| 1479 |
{ |
| 1480 |
$s2o_url = ($type === 'css') |
| 1481 |
? add_query_arg(array('ws_plugin__s2member_css' => '1', 'qcABC' => '1'), $GLOBALS['WS_PLUGIN__']['s2member']['c']['s2o_url']) |
| 1482 |
: add_query_arg(array('ws_plugin__s2member_js_w_globals' => '1', 'qcABC' => '1'), $GLOBALS['WS_PLUGIN__']['s2member']['c']['s2o_url']); |
| 1483 |
$s2o_tag_value = ($type === 'css') ? '2147483639' : 'dynamic-s2member-o'; |
| 1484 |
$target_id = 'active:s2member-o:'.$health_id; |
| 1485 |
$targets[$target_id] = array( |
| 1486 |
'id' => $target_id, |
| 1487 |
'url' => $s2o_url, |
| 1488 |
'probe_url' => $s2o_url, |
| 1489 |
'type' => $type, |
| 1490 |
'mode' => 'activation-tag', |
| 1491 |
'label' => 's2Member-Only '.(($type === 'css') ? 'CSS' : 'JS'), |
| 1492 |
'activation_tags' => array(self::activation_tag_snippet($health_id, $type, $s2o_tag_value)), |
| 1493 |
'failure_id' => 's2o', |
| 1494 |
'failure_url' => $GLOBALS['WS_PLUGIN__']['s2member']['c']['s2o_url'], |
| 1495 |
); |
| 1496 |
} |
| 1497 |
$target_id = 'fallback:'.$health_id; |
| 1498 |
$wordpress_target['id'] = $target_id; |
| 1499 |
$wordpress_target['failure_id'] = $target_id; |
| 1500 |
$wordpress_target['failure_url'] = $wordpress_url; |
| 1501 |
$targets[$target_id] = $wordpress_target; |
| 1502 |
} |
| 1503 |
else |
| 1504 |
{ |
| 1505 |
$target_id = 'active:'.$health_id; |
| 1506 |
$wordpress_target['id'] = $target_id; |
| 1507 |
$wordpress_target['failure_id'] = 'dynamic:'.$health_id; |
| 1508 |
$wordpress_target['failure_url'] = $wordpress_url; |
| 1509 |
$targets[$target_id] = $wordpress_target; |
| 1510 |
} |
| 1511 |
} |
| 1512 |
} |
| 1513 |
|
| 1514 |
//260912.0522 Real-page Late reports remain low-trust hints; add exact URL/activation-tag targets so the administrator-browser probe can confirm or reject them without changing delivery from the report alone. |
| 1515 |
foreach(self::asset_runtime_suspicions() as $key => $suspicion) |
| 1516 |
{ |
| 1517 |
//260912.0522 Ignore pre-rename in-flight suspicions instead of carrying a compatibility alias for this new Beta schema. |
| 1518 |
if(empty($suspicion['activation_tag']) || !self::asset_runtime_expectation_is_current($suspicion)) |
| 1519 |
continue; |
| 1520 |
$id = 'runtime:'.$key; |
| 1521 |
$failure_id = ''; |
| 1522 |
$failure_url = (string)$suspicion['url']; |
| 1523 |
if($suspicion['delivery'] === 'dynamic-s2member-o') |
| 1524 |
{ |
| 1525 |
$failure_id = 's2o'; |
| 1526 |
$failure_url = $GLOBALS['WS_PLUGIN__']['s2member']['c']['s2o_url']; |
| 1527 |
} |
| 1528 |
else if($suspicion['delivery'] === 'static' && !empty($suspicion['asset_id'])) |
| 1529 |
$failure_id = 'static:'.$suspicion['asset_id']; |
| 1530 |
else |
| 1531 |
$failure_id = 'dynamic:'.(string)$suspicion['id']; |
| 1532 |
|
| 1533 |
$targets[$id] = array( |
| 1534 |
'id' => $id, |
| 1535 |
'url' => (string)$suspicion['url'], |
| 1536 |
'probe_url' => (string)$suspicion['url'], |
| 1537 |
'type' => (string)$suspicion['type'], |
| 1538 |
'mode' => 'activation-tag', |
| 1539 |
'label' => self::asset_runtime_health_label((string)$suspicion['id']), |
| 1540 |
'activation_tags' => array((string)$suspicion['activation_tag']), |
| 1541 |
'failure_id' => $failure_id, |
| 1542 |
'failure_url' => $failure_url, |
| 1543 |
'suspicion_key' => $key, |
| 1544 |
'suspicion' => $suspicion, |
| 1545 |
); |
| 1546 |
} |
| 1547 |
return $targets; |
| 1548 |
} |
| 1549 |
|
| 1550 |
/** |
| 1551 |
* Returns a stable hash for the current browser health targets. |
| 1552 |
* |
| 1553 |
* @package s2Member\Utilities |
| 1554 |
* @since 260904.2110 |
| 1555 |
* |
| 1556 |
* @param array $targets Current health targets. |
| 1557 |
* @return string Target hash. |
| 1558 |
*/ |
| 1559 |
protected static function asset_http_health_target_hash($targets = array()) |
| 1560 |
{ |
| 1561 |
$hash = array(); |
| 1562 |
foreach((array)$targets as $id => $target) |
| 1563 |
$hash[$id] = array((string)$target['url'], (string)$target['type'], (string)$target['mode'], (!empty($target['activation_tags'])) ? array_values((array)$target['activation_tags']) : array()); |
| 1564 |
return md5(serialize($hash)); |
| 1565 |
} |
| 1566 |
|
| 1567 |
/** |
| 1568 |
* Upgrades the frontend Asset Health format once per site. |
| 1569 |
* |
| 1570 |
* Existing timestamped files remain available to already-cached HTML, while fresh pages |
| 1571 |
* regenerate active static assets with one activation tag per physical response. |
| 1572 |
* |
| 1573 |
* @package s2Member\Utilities |
| 1574 |
* @since 260909.2015 |
| 1575 |
* |
| 1576 |
* @return null |
| 1577 |
*/ |
| 1578 |
public static function maybe_upgrade_asset_health_format() |
| 1579 |
{ |
| 1580 |
if((string)get_option('ws_plugin__s2member_asset_health_format_version', '') === '3') |
| 1581 |
return; |
| 1582 |
|
| 1583 |
//260912.0522 Reset the first-v260909 activation-tag/build and health-history state together so old component tags and pre-score load history cannot bleed into the physical-file scoring model. |
| 1584 |
self::reset_static_asset_builds(); |
| 1585 |
delete_option('ws_plugin__s2member_asset_runtime_suspicions'); |
| 1586 |
delete_option('ws_plugin__s2member_asset_http_health'); |
| 1587 |
delete_option('ws_plugin__s2member_asset_attention_state'); |
| 1588 |
delete_option('ws_plugin__s2member_assets_health_log'); |
| 1589 |
delete_option('ws_plugin__s2member_asset_notice_dismissed'); |
| 1590 |
self::$asset_http_health_cache = NULL; |
| 1591 |
update_option('ws_plugin__s2member_asset_health_format_version', '3', FALSE); |
| 1592 |
return; |
| 1593 |
} |
| 1594 |
|
| 1595 |
/** |
| 1596 |
* Returns the runtime-health ID for one physical frontend asset response. |
| 1597 |
* |
| 1598 |
* @package s2Member\Utilities |
| 1599 |
* @since 260909.2015 |
| 1600 |
* |
| 1601 |
* @param string $asset_id Static logical filename, or an empty string for dynamic delivery. |
| 1602 |
* @param string $type `css` or `js`. |
| 1603 |
* @param string $delivery Delivery mode. |
| 1604 |
* @return string Runtime-health ID. |
| 1605 |
*/ |
| 1606 |
protected static function asset_runtime_health_id($asset_id = '', $type = '', $delivery = '') |
| 1607 |
{ |
| 1608 |
$type = strtolower((string)$type); |
| 1609 |
if(!in_array($type, array('css', 'js'), TRUE)) |
| 1610 |
return ''; |
| 1611 |
if($delivery === 'static') |
| 1612 |
{ |
| 1613 |
$base = substr((string)$asset_id, 0, -strlen('.'.$type)); |
| 1614 |
$base = str_replace('-', '_', strtolower($base)); |
| 1615 |
return ($base) ? $base.'_'.$type : ''; |
| 1616 |
} |
| 1617 |
return 'dynamic_'.$type; |
| 1618 |
} |
| 1619 |
|
| 1620 |
/** |
| 1621 |
* Returns a site-owner-friendly label for one runtime-health ID. |
| 1622 |
* |
| 1623 |
* @package s2Member\Utilities |
| 1624 |
* @since 260909.2015 |
| 1625 |
* |
| 1626 |
* @param string $id Runtime-health ID. |
| 1627 |
* @return string Human-readable label. |
| 1628 |
*/ |
| 1629 |
protected static function asset_runtime_health_label($id = '') |
| 1630 |
{ |
| 1631 |
$combined = !empty($GLOBALS['WS_PLUGIN__']['s2member']['o']['static_assets_combine']) && (defined('WS_PLUGIN__S2MEMBER_PRO_VERSION') || isset($GLOBALS['WS_PLUGIN__']['s2member_pro'])); |
| 1632 |
$labels = array( |
| 1633 |
's2member_css' => ($combined) ? 'Combined CSS' : 'Framework CSS', |
| 1634 |
's2member_pro_css' => 'Pro CSS', |
| 1635 |
's2member_js' => ($combined) ? 'Combined JS' : 'Framework JS', |
| 1636 |
's2member_pro_js' => 'Pro JS', |
| 1637 |
'dynamic_css' => 'Dynamic CSS', |
| 1638 |
'dynamic_js' => 'Dynamic JS', |
| 1639 |
); |
| 1640 |
return isset($labels[$id]) ? $labels[$id] : (string)$id; |
| 1641 |
} |
| 1642 |
|
| 1643 |
/** |
| 1644 |
* Returns the activation-tag snippet used to verify one physical frontend asset response. |
| 1645 |
* |
| 1646 |
* @package s2Member\Utilities |
| 1647 |
* @since 260909.2015 |
| 1648 |
* |
| 1649 |
* @param string $id Runtime-health ID. |
| 1650 |
* @param string $type `css` or `js`. |
| 1651 |
* @param string $tag_value Value the activation tag is expected to expose. |
| 1652 |
* @return string Activation-tag source snippet. |
| 1653 |
*/ |
| 1654 |
protected static function activation_tag_snippet($id = '', $type = '', $tag_value = '') |
| 1655 |
{ |
| 1656 |
if($type === 'css') |
| 1657 |
return '#ws-plugin--s2member-asset-health-'.str_replace('_', '-', (string)$id).'{z-index:'.(string)$tag_value.'!important}'; |
| 1658 |
return 'ws_plugin__s2member_asset_health["'.(string)$id.'"]="'.(string)$tag_value.'"'; |
| 1659 |
} |
| 1660 |
|
| 1661 |
/** |
| 1662 |
* Returns the activation-tag snippet appended to a generated static asset. |
| 1663 |
* |
| 1664 |
* @package s2Member\Utilities |
| 1665 |
* @since 260904.2255 |
| 1666 |
* |
| 1667 |
* @param string $id Stable generated asset identifier without extension. |
| 1668 |
* @param string $type `css` or `js`. |
| 1669 |
* @param int $build Generated build timestamp. |
| 1670 |
* @return string Activation-tag snippet. |
| 1671 |
*/ |
| 1672 |
protected static function static_activation_tag_snippet($id = '', $type = '', $build = 0) |
| 1673 |
{ |
| 1674 |
//260912.0522 Activation-tag identity follows the physical response, not Framework/Pro logical components, so a combined file produces one tag and one possible Late report. |
| 1675 |
$health_id = self::asset_runtime_health_id($id.'.'.$type, $type, 'static'); |
| 1676 |
$tag_value = ($type === 'css') ? (string)(int)$build : 'static-'.(int)$build; |
| 1677 |
$activation_tag = self::activation_tag_snippet($health_id, $type, $tag_value); |
| 1678 |
if($type === 'css') |
| 1679 |
return $activation_tag; |
| 1680 |
return ';window.ws_plugin__s2member_asset_health=window.ws_plugin__s2member_asset_health||{};window.'.$activation_tag.';'; |
| 1681 |
} |
| 1682 |
|
| 1683 |
/** |
| 1684 |
* Returns the activation-tag snippet appended to dynamically generated CSS or JavaScript. |
| 1685 |
* |
| 1686 |
* @package s2Member\Utilities |
| 1687 |
* @since 260904.2255 |
| 1688 |
* |
| 1689 |
* @param string $type `css` or `js`. |
| 1690 |
* @return string Activation-tag snippet. |
| 1691 |
*/ |
| 1692 |
public static function dynamic_activation_tag_snippet($type = '') |
| 1693 |
{ |
| 1694 |
$type = strtolower((string)$type); |
| 1695 |
if(!in_array($type, array('css', 'js'), TRUE)) |
| 1696 |
return ''; |
| 1697 |
$delivery = (defined('_WS_PLUGIN__S2MEMBER_ONLY')) ? 'dynamic-s2member-o' : 'dynamic-wordpress'; |
| 1698 |
$health_id = self::asset_runtime_health_id('', $type, $delivery); |
| 1699 |
$tag_value = ($type === 'css') ? (($delivery === 'dynamic-s2member-o') ? '2147483639' : '2147483640') : $delivery; |
| 1700 |
$activation_tag = self::activation_tag_snippet($health_id, $type, $tag_value); |
| 1701 |
if($type === 'css') |
| 1702 |
return "\n".$activation_tag."\n"; |
| 1703 |
return "\n;window.ws_plugin__s2member_asset_health=window.ws_plugin__s2member_asset_health||{};window.".$activation_tag.";\n"; |
| 1704 |
} |
| 1705 |
|
| 1706 |
/** |
| 1707 |
* Registers the exact CSS/JavaScript activation tags expected on the current frontend page. |
| 1708 |
* Each call represents one physical response, so combined Framework+Pro delivery registers one activation tag for that combined file instead of one tag per logical component. |
| 1709 |
* |
| 1710 |
* @package s2Member\Utilities |
| 1711 |
* @since 260904.2255 |
| 1712 |
* |
| 1713 |
* @param string $asset_id Logical static asset ID, or an empty string for dynamic delivery. |
| 1714 |
* @param string $type `css` or `js`. |
| 1715 |
* @param string $url Public URL emitted on this page. |
| 1716 |
* @param string $delivery `static`, `dynamic-s2member-o`, or `dynamic-wordpress`. |
| 1717 |
* @param int $build Static build timestamp, or zero for dynamic delivery. |
| 1718 |
* @param string $issue_detail Optional reason a preferred route fell back before this response was selected. |
| 1719 |
* @param bool $dynamic_required Whether Full WordPress Dynamic delivery is intentionally required for compatibility. |
| 1720 |
* @return null |
| 1721 |
*/ |
| 1722 |
public static function register_page_asset_expectations($asset_id = '', $type = '', $url = '', $delivery = '', $build = 0, $issue_detail = '', $dynamic_required = FALSE) |
| 1723 |
{ |
| 1724 |
$type = strtolower((string)$type); |
| 1725 |
$url = (string)$url; |
| 1726 |
if(!in_array($type, array('css', 'js'), TRUE) || !$url || !in_array($delivery, array('static', 'dynamic-s2member-o', 'dynamic-wordpress'), TRUE)) |
| 1727 |
return; |
| 1728 |
$id = self::asset_runtime_health_id($asset_id, $type, $delivery); |
| 1729 |
if(!$id) |
| 1730 |
return; |
| 1731 |
if($delivery === 'static') |
| 1732 |
$tag_value = ($type === 'css') ? (string)(int)$build : 'static-'.(int)$build; |
| 1733 |
else |
| 1734 |
$tag_value = ($type === 'css') ? (($delivery === 'dynamic-s2member-o') ? '2147483639' : '2147483640') : $delivery; |
| 1735 |
$expectation = array( |
| 1736 |
'id' => $id, |
| 1737 |
'asset_id' => (string)$asset_id, |
| 1738 |
'type' => $type, |
| 1739 |
'url' => $url, |
| 1740 |
'delivery' => $delivery, |
| 1741 |
'tag_value' => $tag_value, |
| 1742 |
'activation_tag' => self::activation_tag_snippet($id, $type, $tag_value), |
| 1743 |
//260911.1806 Server-side fallback context is not sent to the browser; it only supplies a useful Last issue snapshot for the page that selected fallback. |
| 1744 |
'issue_detail' => substr(wp_strip_all_tags((string)$issue_detail), 0, 240), |
| 1745 |
//260913.2001 Server-only compatibility context keeps intentional Full WordPress Dynamic delivery Healthy without changing the compact browser expectation/signature. |
| 1746 |
'dynamic_required' => (bool)$dynamic_required, |
| 1747 |
); |
| 1748 |
$expectation['signature'] = self::asset_runtime_expectation_signature($expectation); |
| 1749 |
self::$page_asset_expectations[$id] = $expectation; |
| 1750 |
return; |
| 1751 |
} |
| 1752 |
|
| 1753 |
/** |
| 1754 |
* Expands one compact browser runtime expectation into the full signed structure. |
| 1755 |
* |
| 1756 |
* Frontend pages only need a few fields to check asset activation. Reconstruct the |
| 1757 |
* descriptive fields here when a miss is actually reported, keeping healthy page source small. |
| 1758 |
* Recovery fields from the first v260909 monitor are intentionally no longer part of current expectations because Late results no longer trigger speculative fallback injection. |
| 1759 |
* |
| 1760 |
* @package s2Member\Utilities |
| 1761 |
* @since 260905.0106 |
| 1762 |
* |
| 1763 |
* @param array $compact Compact expectation fields. |
| 1764 |
* @return array Full expectation, or an empty array when invalid. |
| 1765 |
*/ |
| 1766 |
protected static function expand_asset_runtime_expectation($compact = array()) |
| 1767 |
{ |
| 1768 |
if(!is_array($compact) || count($compact) < 6) |
| 1769 |
return array(); |
| 1770 |
$id = isset($compact[0]) ? (string)$compact[0] : ''; |
| 1771 |
$asset_id = isset($compact[1]) ? (string)$compact[1] : ''; |
| 1772 |
$url = isset($compact[2]) ? (string)$compact[2] : ''; |
| 1773 |
$delivery = isset($compact[3]) ? (string)$compact[3] : ''; |
| 1774 |
$tag_value = isset($compact[4]) ? (string)$compact[4] : ''; |
| 1775 |
$signature = isset($compact[5]) ? (string)$compact[5] : ''; |
| 1776 |
if(!preg_match('/\A(?:s2member(?:_pro)?|dynamic)_(css|js)\z/', $id, $match)) |
| 1777 |
return array(); //260912.0522 Cached pages using the first-v260909 component-level activation-tag IDs are intentionally stale after the Asset Health format upgrade. |
| 1778 |
$type = $match[1]; |
| 1779 |
return array( |
| 1780 |
'id' => $id, |
| 1781 |
'asset_id' => $asset_id, |
| 1782 |
'type' => $type, |
| 1783 |
'url' => $url, |
| 1784 |
'delivery' => $delivery, |
| 1785 |
'tag_value' => $tag_value, |
| 1786 |
'activation_tag' => self::activation_tag_snippet($id, $type, $tag_value), |
| 1787 |
'signature' => $signature, |
| 1788 |
); |
| 1789 |
} |
| 1790 |
|
| 1791 |
/** |
| 1792 |
* Returns a signature for one low-trust runtime expectation report. |
| 1793 |
* |
| 1794 |
* @package s2Member\Utilities |
| 1795 |
* @since 260904.2255 |
| 1796 |
* |
| 1797 |
* @param array $expectation Runtime expectation fields. |
| 1798 |
* @return string Signature. |
| 1799 |
*/ |
| 1800 |
protected static function asset_runtime_expectation_signature($expectation = array()) |
| 1801 |
{ |
| 1802 |
$parts = array(); |
| 1803 |
foreach(array('id', 'asset_id', 'type', 'url', 'delivery', 'tag_value', 'activation_tag') as $key) |
| 1804 |
$parts[$key] = isset($expectation[$key]) ? (string)$expectation[$key] : ''; |
| 1805 |
return hash_hmac('sha256', serialize($parts), wp_salt('nonce')); |
| 1806 |
} |
| 1807 |
|
| 1808 |
/** |
| 1809 |
* Returns true when a reported expectation still describes the site's current delivery state. |
| 1810 |
* |
| 1811 |
* @package s2Member\Utilities |
| 1812 |
* @since 260904.2255 |
| 1813 |
* |
| 1814 |
* @param array $expectation Runtime expectation. |
| 1815 |
* @return bool True when current. |
| 1816 |
*/ |
| 1817 |
protected static function asset_runtime_expectation_is_current($expectation = array()) |
| 1818 |
{ |
| 1819 |
if(empty($expectation['url']) || empty($expectation['delivery']) || empty($expectation['type'])) |
| 1820 |
return FALSE; |
| 1821 |
if($expectation['delivery'] === 'static') |
| 1822 |
{ |
| 1823 |
$id = (string)$expectation['asset_id']; |
| 1824 |
$type = (string)$expectation['type']; |
| 1825 |
if(!in_array($id, self::static_asset_ids($type, 'all'), TRUE)) |
| 1826 |
return FALSE; |
| 1827 |
$build = self::static_asset_build($id); |
| 1828 |
$location = self::static_assets_location(FALSE); |
| 1829 |
$base = substr($id, 0, -strlen('.'.$type)); |
| 1830 |
return !empty($location['ok']) && $build > 0 && (string)$expectation['url'] === $location['url'].'/'.$base.'-'.$build.'.'.$type; |
| 1831 |
} |
| 1832 |
if($expectation['delivery'] === 'dynamic-s2member-o') |
| 1833 |
return (empty($GLOBALS['WS_PLUGIN__']['s2member']['o']['dynamic_asset_loader']) || $GLOBALS['WS_PLUGIN__']['s2member']['o']['dynamic_asset_loader'] !== 'wordpress') && strpos((string)$expectation['url'], $GLOBALS['WS_PLUGIN__']['s2member']['c']['s2o_url'].'?') === 0; |
| 1834 |
if($expectation['delivery'] === 'dynamic-wordpress') |
| 1835 |
return strpos((string)$expectation['url'], self::wordpress_dynamic_asset_url().'?') === 0; |
| 1836 |
return FALSE; |
| 1837 |
} |
| 1838 |
|
| 1839 |
/** |
| 1840 |
* Returns active generated frontend asset IDs for one type/component. |
| 1841 |
* |
| 1842 |
* Framework and Pro files stay separate by default. Combined mode reuses the Framework ID because that file becomes the combined representation. |
| 1843 |
* |
| 1844 |
* @package s2Member\Utilities |
| 1845 |
* @since 260903.1918 |
| 1846 |
* |
| 1847 |
* @param string $type `css` or `js`. |
| 1848 |
* @param string $component `all`, `framework`, or `pro`. |
| 1849 |
* @return array Logical generated filenames. |
| 1850 |
*/ |
| 1851 |
public static function static_asset_ids($type = '', $component = 'all') |
| 1852 |
{ |
| 1853 |
$type = strtolower((string)$type); |
| 1854 |
$component = strtolower((string)$component); |
| 1855 |
if(!in_array($type, array('css', 'js'), TRUE) || !in_array($component, array('all', 'framework', 'pro'), TRUE)) |
| 1856 |
return array(); |
| 1857 |
|
| 1858 |
$framework = 's2member.'.$type; |
| 1859 |
$pro = (!empty($GLOBALS['WS_PLUGIN__']['s2member']['o']['static_assets_combine'])) ? $framework : 's2member-pro.'.$type; |
| 1860 |
if($component === 'framework') |
| 1861 |
return array($framework); |
| 1862 |
if($component === 'pro') |
| 1863 |
return array($pro); |
| 1864 |
|
| 1865 |
$ids = array($framework); |
| 1866 |
if((defined('WS_PLUGIN__S2MEMBER_PRO_VERSION') || isset($GLOBALS['WS_PLUGIN__']['s2member_pro'])) && $pro !== $framework) |
| 1867 |
$ids[] = $pro; |
| 1868 |
return $ids; |
| 1869 |
} |
| 1870 |
|
| 1871 |
/** |
| 1872 |
* Returns how s2Member text used by static JavaScript should be delivered. |
| 1873 |
* |
| 1874 |
* @package s2Member\Utilities |
| 1875 |
* @since 260906.2049 |
| 1876 |
* |
| 1877 |
* @return string `static` to include text in generated JavaScript, or `page` to load it with each WordPress page. |
| 1878 |
*/ |
| 1879 |
public static function static_js_text_delivery() |
| 1880 |
{ |
| 1881 |
return (!empty($GLOBALS['WS_PLUGIN__']['s2member']['o']['static_js_text']) && $GLOBALS['WS_PLUGIN__']['s2member']['o']['static_js_text'] === 'page') ? 'page' : 'static'; |
| 1882 |
} |
| 1883 |
|
| 1884 |
/** |
| 1885 |
* Determines whether page-loaded JavaScript text is supported by the active Framework/Pro combination. |
| 1886 |
* |
| 1887 |
* Framework can always load its own text with the page. Pro explicitly advertises support because |
| 1888 |
* older Pro releases predate the shipped static-JS data map needed for this delivery mode. |
| 1889 |
* |
| 1890 |
* @package s2Member\Utilities |
| 1891 |
* @since 260906.2049 |
| 1892 |
* |
| 1893 |
* @return bool True if page-loaded JavaScript text is available. |
| 1894 |
*/ |
| 1895 |
public static function static_js_page_text_supported() |
| 1896 |
{ |
| 1897 |
if(!c_ws_plugin__s2member_utils_conds::pro_is_installed()) |
| 1898 |
return TRUE; |
| 1899 |
return (bool)apply_filters('ws_plugin__s2member_static_js_page_text_supported', FALSE); |
| 1900 |
} |
| 1901 |
|
| 1902 |
/** |
| 1903 |
* Determines whether the active Pro JavaScript hook contains only built-in callbacks safe to cache in a static file. |
| 1904 |
* |
| 1905 |
* This lets a newer Framework retain static-file delivery with older Pro releases that predate |
| 1906 |
* the static-source filters. Unknown, reordered, or deprecated gateway callbacks remain dynamic. |
| 1907 |
* |
| 1908 |
* @package s2Member\Utilities |
| 1909 |
* @since 260906.2049 |
| 1910 |
* |
| 1911 |
* @return bool True if the active Pro hook can be captured safely. |
| 1912 |
*/ |
| 1913 |
protected static function static_js_builtin_pro_callbacks_supported() |
| 1914 |
{ |
| 1915 |
if(!c_ws_plugin__s2member_utils_conds::pro_is_installed() || has_filter('ws_plugin__s2member_pro_available_gateways')) |
| 1916 |
return FALSE; |
| 1917 |
$built_ins = array( |
| 1918 |
'c_ws_plugin__s2member_pro_css_js::js_w_globals', |
| 1919 |
'c_ws_plugin__s2member_pro_paypal_css_js::paypal_js_w_globals', |
| 1920 |
'c_ws_plugin__s2member_pro_stripe_css_js::stripe_js_w_globals', |
| 1921 |
'c_ws_plugin__s2member_pro_authnet_css_js::authnet_js_w_globals', |
| 1922 |
'c_ws_plugin__s2member_pro_clickbank_css_js::clickbank_js_w_globals', |
| 1923 |
); |
| 1924 |
$callbacks = isset($GLOBALS['wp_filter']['ws_plugin__s2member_during_js_w_globals']) ? $GLOBALS['wp_filter']['ws_plugin__s2member_during_js_w_globals'] : array(); |
| 1925 |
if(is_object($callbacks) && isset($callbacks->callbacks)) |
| 1926 |
$callbacks = $callbacks->callbacks; |
| 1927 |
$found = FALSE; |
| 1928 |
foreach((array)$callbacks as $priority => $priority_callbacks) |
| 1929 |
foreach((array)$priority_callbacks as $callback) |
| 1930 |
{ |
| 1931 |
if((int)$priority !== 10 || !is_array($callback) || !isset($callback['function'], $callback['accepted_args']) || !in_array($callback['function'], $built_ins, TRUE) || (int)$callback['accepted_args'] !== 1) |
| 1932 |
return FALSE; |
| 1933 |
$found = TRUE; |
| 1934 |
} |
| 1935 |
return $found; |
| 1936 |
} |
| 1937 |
|
| 1938 |
/** |
| 1939 |
* Captures built-in Pro JavaScript from an older Pro release for static-file text delivery. |
| 1940 |
* |
| 1941 |
* @package s2Member\Utilities |
| 1942 |
* @since 260906.2049 |
| 1943 |
* |
| 1944 |
* @return string Captured Pro JavaScript, or an empty string if the hook is not safely cacheable. |
| 1945 |
*/ |
| 1946 |
protected static function static_js_builtin_pro_output() |
| 1947 |
{ |
| 1948 |
if(!self::static_js_builtin_pro_callbacks_supported()) |
| 1949 |
return ''; |
| 1950 |
//260906.2256 Match the template variables passed by the normal dynamic loader when capturing older Pro callbacks. |
| 1951 |
$u = $GLOBALS['WS_PLUGIN__']['s2member']['c']['dir_url']; |
| 1952 |
$i = $u.'/src/images'; |
| 1953 |
ob_start(); |
| 1954 |
do_action('ws_plugin__s2member_during_js_w_globals', get_defined_vars()); |
| 1955 |
return (string)ob_get_clean(); |
| 1956 |
} |
| 1957 |
|
| 1958 |
/** |
| 1959 |
* Returns shipped static JavaScript data maps used by one generated static JavaScript file. |
| 1960 |
* |
| 1961 |
* Framework owns its data map. Pro appends its independent data map through a filter so the two |
| 1962 |
* release packages never need cross-repo slot coordination. |
| 1963 |
* |
| 1964 |
* @package s2Member\Utilities |
| 1965 |
* @since 260906.0738 |
| 1966 |
* |
| 1967 |
* @param string $id Logical generated JavaScript filename. |
| 1968 |
* @return array Data-map paths keyed by the compact browser-data namespace. |
| 1969 |
*/ |
| 1970 |
protected static function static_js_data_map_paths($id = '') |
| 1971 |
{ |
| 1972 |
if(self::static_js_text_delivery() !== 'page') |
| 1973 |
return array(); |
| 1974 |
$id = strtolower((string)$id); |
| 1975 |
$paths = array(); |
| 1976 |
if($id === 's2member.js') |
| 1977 |
$paths['f'] = $GLOBALS['WS_PLUGIN__']['s2member']['c']['dir'].'/src/includes/s2member.js.php'; |
| 1978 |
$paths = (array)apply_filters('ws_plugin__s2member_static_js_data_map_paths', $paths, $id, get_defined_vars()); |
| 1979 |
foreach($paths as $key => $path) |
| 1980 |
if(!preg_match('/^[a-z][a-z0-9_]*$/i', (string)$key) || !(string)$path) |
| 1981 |
unset($paths[$key]); |
| 1982 |
return $paths; |
| 1983 |
} |
| 1984 |
|
| 1985 |
/** |
| 1986 |
* Parses one shipped static JavaScript data map into an exact expression-to-slot lookup. |
| 1987 |
* |
| 1988 |
* @package s2Member\Utilities |
| 1989 |
* @since 260906.0738 |
| 1990 |
* |
| 1991 |
* @param string $path Data-map path. |
| 1992 |
* @return array Parse result. |
| 1993 |
*/ |
| 1994 |
protected static function static_js_data_map($path = '') |
| 1995 |
{ |
| 1996 |
$path = (string)$path; |
| 1997 |
if(isset(self::$static_js_data_map_cache[$path])) |
| 1998 |
return self::$static_js_data_map_cache[$path]; |
| 1999 |
if(!$path || !is_readable($path) || ($source = file_get_contents($path)) === FALSE) |
| 2000 |
return self::$static_js_data_map_cache[$path] = array('ok' => FALSE, 'slots' => array(), 'hash' => '', 'error' => 'Static JavaScript data map is not readable: '.$path); |
| 2001 |
|
| 2002 |
$slots = array(); |
| 2003 |
if(!preg_match_all('/\\$data\\[(\\d+)\\]\\s*=\\s*\\/\\*d\\*\\/(.*?)\\/\\*b\\*\\/;/s', $source, $matches, PREG_SET_ORDER)) |
| 2004 |
return self::$static_js_data_map_cache[$path] = array('ok' => FALSE, 'slots' => array(), 'hash' => '', 'error' => 'Static JavaScript data map contains no marked entries: '.$path); |
| 2005 |
foreach($matches as $index => $match) |
| 2006 |
{ |
| 2007 |
$slot = (int)$match[1]; |
| 2008 |
$expression = trim((string)$match[2]); |
| 2009 |
if($slot !== $index || !$expression || isset($slots[$expression])) |
| 2010 |
return self::$static_js_data_map_cache[$path] = array('ok' => FALSE, 'slots' => array(), 'hash' => '', 'error' => 'Static JavaScript data-map slots are invalid or duplicated: '.$path); |
| 2011 |
$slots[$expression] = $slot; |
| 2012 |
} |
| 2013 |
return self::$static_js_data_map_cache[$path] = array('ok' => TRUE, 'slots' => $slots, 'hash' => hash('sha256', $source), 'error' => ''); |
| 2014 |
} |
| 2015 |
|
| 2016 |
/** |
| 2017 |
* Returns the current shipped data-map signature for one static JavaScript representation. |
| 2018 |
* |
| 2019 |
* @package s2Member\Utilities |
| 2020 |
* @since 260906.0738 |
| 2021 |
* |
| 2022 |
* @param string $id Logical generated JavaScript filename. |
| 2023 |
* @return array Signature result. |
| 2024 |
*/ |
| 2025 |
protected static function static_js_data_map_signature($id = '') |
| 2026 |
{ |
| 2027 |
$hashes = array(); |
| 2028 |
foreach(self::static_js_data_map_paths($id) as $key => $path) |
| 2029 |
{ |
| 2030 |
$data_map = self::static_js_data_map($path); |
| 2031 |
if(empty($data_map['ok'])) |
| 2032 |
return array('ok' => FALSE, 'signature' => '', 'error' => (string)$data_map['error']); |
| 2033 |
$hashes[(string)$key] = (string)$data_map['hash']; |
| 2034 |
} |
| 2035 |
if(!$hashes) |
| 2036 |
return array('ok' => FALSE, 'signature' => '', 'error' => 'No static JavaScript data map is available for '.$id); |
| 2037 |
return array('ok' => TRUE, 'signature' => hash('sha256', wp_json_encode($hashes)), 'error' => ''); |
| 2038 |
} |
| 2039 |
|
| 2040 |
/** |
| 2041 |
* Returns the data-map signature saved with the active generated JavaScript file. |
| 2042 |
* |
| 2043 |
* @package s2Member\Utilities |
| 2044 |
* @since 260906.0738 |
| 2045 |
* |
| 2046 |
* @param string $id Logical generated JavaScript filename. |
| 2047 |
* @return string Saved signature. |
| 2048 |
*/ |
| 2049 |
protected static function static_asset_data_map_signature($id = '') |
| 2050 |
{ |
| 2051 |
$signatures = get_option('ws_plugin__s2member_static_asset_data_map_signatures', array()); |
| 2052 |
return (is_array($signatures) && isset($signatures[$id])) ? (string)$signatures[$id] : ''; |
| 2053 |
} |
| 2054 |
|
| 2055 |
/** |
| 2056 |
* Saves the data-map signature paired with one generated JavaScript file. |
| 2057 |
* |
| 2058 |
* @package s2Member\Utilities |
| 2059 |
* @since 260906.0738 |
| 2060 |
* |
| 2061 |
* @param string $id Logical generated JavaScript filename. |
| 2062 |
* @param string $signature Current data-map signature. |
| 2063 |
* @return null |
| 2064 |
*/ |
| 2065 |
protected static function set_static_asset_data_map_signature($id = '', $signature = '') |
| 2066 |
{ |
| 2067 |
if(!in_array($id, array('s2member.js', 's2member-pro.js'), TRUE)) |
| 2068 |
return; |
| 2069 |
$signatures = get_option('ws_plugin__s2member_static_asset_data_map_signatures', array()); |
| 2070 |
$signatures = is_array($signatures) ? $signatures : array(); |
| 2071 |
$signatures[$id] = (string)$signature; |
| 2072 |
update_option('ws_plugin__s2member_static_asset_data_map_signatures', $signatures); |
| 2073 |
return; |
| 2074 |
} |
| 2075 |
|
| 2076 |
/** |
| 2077 |
* Returns the signed build timestamp for one generated frontend asset file. |
| 2078 |
* |
| 2079 |
* Positive values are current. Negative values preserve the previous timestamp while marking that exact file stale. |
| 2080 |
* |
| 2081 |
* @package s2Member\Utilities |
| 2082 |
* @since 260903.0525 |
| 2083 |
* |
| 2084 |
* @param string $id Logical generated filename, e.g. `s2member.js` or `s2member-pro.css`. |
| 2085 |
* @return int Signed build timestamp. |
| 2086 |
*/ |
| 2087 |
public static function static_asset_build($id = '') |
| 2088 |
{ |
| 2089 |
$id = strtolower((string)$id); |
| 2090 |
if(in_array($id, array('css', 'js'), TRUE)) |
| 2091 |
$id = 's2member.'.$id; |
| 2092 |
$builds = get_option('ws_plugin__s2member_static_asset_builds', array()); |
| 2093 |
return (in_array($id, array('s2member.css', 's2member-pro.css', 's2member.js', 's2member-pro.js'), TRUE) && is_array($builds) && isset($builds[$id])) ? (int)$builds[$id] : 0; |
| 2094 |
} |
| 2095 |
|
| 2096 |
/** |
| 2097 |
* Updates the signed build timestamp for one generated frontend asset file. |
| 2098 |
* |
| 2099 |
* @package s2Member\Utilities |
| 2100 |
* @since 260903.0525 |
| 2101 |
* |
| 2102 |
* @param string $id Logical generated filename. |
| 2103 |
* @param int $build Signed build timestamp. |
| 2104 |
* @return null |
| 2105 |
*/ |
| 2106 |
protected static function set_static_asset_build($id = '', $build = 0) |
| 2107 |
{ |
| 2108 |
$id = strtolower((string)$id); |
| 2109 |
if(!in_array($id, array('s2member.css', 's2member-pro.css', 's2member.js', 's2member-pro.js'), TRUE)) |
| 2110 |
return; |
| 2111 |
$builds = get_option('ws_plugin__s2member_static_asset_builds', array()); |
| 2112 |
$builds = is_array($builds) ? $builds : array(); |
| 2113 |
//260903.1918 Build state is keyed by the actual logical generated filename; old type-only beta keys are discarded on the next successful state write. |
| 2114 |
$builds = array_intersect_key($builds, array_flip(array('s2member.css', 's2member-pro.css', 's2member.js', 's2member-pro.js'))); |
| 2115 |
$builds[$id] = (int)$build; |
| 2116 |
update_option('ws_plugin__s2member_static_asset_builds', $builds); |
| 2117 |
unset(self::$static_asset_cache[$id]); |
| 2118 |
self::$static_assets_health_cache = NULL; |
| 2119 |
delete_option('ws_plugin__s2member_static_asset_health'); |
| 2120 |
return; |
| 2121 |
} |
| 2122 |
|
| 2123 |
/** |
| 2124 |
* Clears generated frontend asset build state when the active file representation changes. |
| 2125 |
* |
| 2126 |
* Existing timestamped files remain on disk for already-cached HTML; fresh requests generate only the newly active representation. |
| 2127 |
* |
| 2128 |
* @package s2Member\Utilities |
| 2129 |
* @since 260903.1918 |
| 2130 |
* |
| 2131 |
* @return null |
| 2132 |
*/ |
| 2133 |
protected static function reset_static_asset_builds() |
| 2134 |
{ |
| 2135 |
delete_option('ws_plugin__s2member_static_asset_builds'); |
| 2136 |
delete_option('ws_plugin__s2member_static_asset_data_map_signatures'); //260906.1530 Static JavaScript and its data-map slot layout must stay synchronized. |
| 2137 |
delete_option('ws_plugin__s2member_static_asset_health'); |
| 2138 |
self::$static_asset_cache = array(); |
| 2139 |
self::$static_js_data_map_cache = array(); |
| 2140 |
self::$static_assets_health_cache = NULL; |
| 2141 |
foreach(array('s2member.css', 's2member-pro.css', 's2member.js', 's2member-pro.js') as $id) |
| 2142 |
delete_transient('ws_plugin__s2member_static_asset_failure_'.str_replace('.', '_', $id)); |
| 2143 |
return; |
| 2144 |
} |
| 2145 |
|
| 2146 |
/** |
| 2147 |
* Invalidates selected generated frontend assets. |
| 2148 |
* |
| 2149 |
* Selectors may be `css`, `js`, `framework_css`, `framework_js`, `pro_css`, `pro_js`, or exact logical generated filenames. |
| 2150 |
* Existing files remain available for already-cached HTML. Current pages stop referencing a stale generation until its replacement succeeds. |
| 2151 |
* |
| 2152 |
* @package s2Member\Utilities |
| 2153 |
* @since 260903.0437 |
| 2154 |
* |
| 2155 |
* @param array|string $assets Asset selectors. |
| 2156 |
* @return null |
| 2157 |
*/ |
| 2158 |
public static function invalidate_static_assets($assets = array('css', 'js')) |
| 2159 |
{ |
| 2160 |
$assets = is_array($assets) ? $assets : array($assets); |
| 2161 |
$ids = array(); |
| 2162 |
foreach($assets as $asset) |
| 2163 |
{ |
| 2164 |
$asset = strtolower((string)$asset); |
| 2165 |
if(in_array($asset, array('css', 'js'), TRUE)) |
| 2166 |
$ids = array_merge($ids, self::static_asset_ids($asset, 'all')); |
| 2167 |
else if(preg_match('/^(framework|pro)_(css|js)$/', $asset, $match)) |
| 2168 |
$ids = array_merge($ids, self::static_asset_ids($match[2], $match[1])); |
| 2169 |
else if(in_array($asset, array('s2member.css', 's2member-pro.css', 's2member.js', 's2member-pro.js'), TRUE)) |
| 2170 |
$ids[] = $asset; |
| 2171 |
} |
| 2172 |
foreach(array_unique($ids) as $id) |
| 2173 |
{ |
| 2174 |
$build = self::static_asset_build($id); |
| 2175 |
//260903.1918 Preserve an existing file's timestamp while marking only that file stale; never create build-state entries for files that have not yet been generated. |
| 2176 |
if($build) |
| 2177 |
self::set_static_asset_build($id, -abs($build)); |
| 2178 |
else |
| 2179 |
unset(self::$static_asset_cache[$id]); |
| 2180 |
delete_transient('ws_plugin__s2member_static_asset_failure_'.str_replace('.', '_', $id)); |
| 2181 |
} |
| 2182 |
return; |
| 2183 |
} |
| 2184 |
|
| 2185 |
/** |
| 2186 |
* Invalidates generated assets when s2Member or WordPress values rendered into them change. |
| 2187 |
* |
| 2188 |
* @package s2Member\Utilities |
| 2189 |
* @since 260903.0437 |
| 2190 |
* |
| 2191 |
* @param string $option Option name. |
| 2192 |
* @param mixed $old_value Previous value. |
| 2193 |
* @param mixed $value New value. |
| 2194 |
* @return null |
| 2195 |
*/ |
| 2196 |
public static function maybe_invalidate_after_wp_option_update($option = '', $old_value = NULL, $value = NULL) |
| 2197 |
{ |
| 2198 |
if($old_value === $value) |
| 2199 |
return; |
| 2200 |
|
| 2201 |
if((string)$option === 'ws_plugin__s2member_options') |
| 2202 |
{ |
| 2203 |
if(!is_array($old_value) || !is_array($value)) |
| 2204 |
return; |
| 2205 |
$old = (array)$old_value; |
| 2206 |
$new = (array)$value; |
| 2207 |
|
| 2208 |
//260907.2203 Keep a concise operational history of CSS/JS configuration changes when s2Member logging is enabled. |
| 2209 |
$config_changes = array(); |
| 2210 |
//260912.0522 Include wait-time changes because they can explain a sudden change in Late asset loads even when delivery settings themselves did not change. |
| 2211 |
foreach(array('dynamic_asset_loader', 'static_css', 'static_css_minify', 'static_js', 'static_js_text', 'static_js_minify', 'static_assets_combine', 'asset_health_wait_seconds') as $key) |
| 2212 |
if(serialize(isset($old[$key]) ? $old[$key] : NULL) !== serialize(isset($new[$key]) ? $new[$key] : NULL)) |
| 2213 |
$config_changes[$key] = array('old' => isset($old[$key]) ? $old[$key] : NULL, 'new' => isset($new[$key]) ? $new[$key] : NULL); |
| 2214 |
if($config_changes) |
| 2215 |
c_ws_plugin__s2member_utils_logs::log_entry('css-js', array('event' => 'CSS/JS configuration changed', 'changes' => $config_changes)); |
| 2216 |
|
| 2217 |
if((string)(isset($old['static_assets_combine']) ? $old['static_assets_combine'] : '0') !== (string)(isset($new['static_assets_combine']) ? $new['static_assets_combine'] : '0')) |
| 2218 |
{ |
| 2219 |
//260911.1834 A combine-mode change resets both representations; queue enabled CSS/JS for immediate rebuilding after the complete new option set has finished saving. |
| 2220 |
self::$static_assets_rebuild_after_save = array('css', 'js'); |
| 2221 |
self::reset_static_asset_builds(); |
| 2222 |
return; |
| 2223 |
} |
| 2224 |
|
| 2225 |
$keys = array( |
| 2226 |
'css' => array('static_css', 'static_css_minify'), |
| 2227 |
'js' => array('static_js', 'static_js_text', 'static_js_minify'), |
| 2228 |
'framework_js' => array('custom_reg_force_personal_emails', 'custom_reg_password_min_length', 'custom_reg_password_min_strength'), |
| 2229 |
'pro_css' => array('pro_gateways_enabled'), |
| 2230 |
'pro_js' => array( |
| 2231 |
'pro_gateways_enabled', 'pro_stripe_api_publishable_key', 'pro_stripe_api_image', 'pro_stripe_api_allow_remember_me', |
| 2232 |
'paypal_checkout_enable', 'paypal_checkout_sandbox', 'paypal_checkout_client_id', 'paypal_checkout_sandbox_client_id', 'sec_encryption_key', |
| 2233 |
), |
| 2234 |
); |
| 2235 |
$keys = (array)apply_filters('ws_plugin__s2member_static_asset_option_keys', $keys, get_defined_vars()); |
| 2236 |
$invalidate = array(); |
| 2237 |
foreach($keys as $selector => $option_keys) |
| 2238 |
foreach((array)$option_keys as $key) |
| 2239 |
if((isset($old[$key]) || isset($new[$key])) && serialize(isset($old[$key]) ? $old[$key] : NULL) !== serialize(isset($new[$key]) ? $new[$key] : NULL)) |
| 2240 |
{ |
| 2241 |
$invalidate[] = $selector; |
| 2242 |
break; |
| 2243 |
} |
| 2244 |
if($invalidate) |
| 2245 |
{ |
| 2246 |
//260911.1834 Remember only the affected types until update_all_options has finished; rebuilding here could still see the request's old global option set. |
| 2247 |
foreach($invalidate as $_static_asset_selector) |
| 2248 |
foreach(array('css', 'js') as $_static_asset_type) |
| 2249 |
if(strpos((string)$_static_asset_selector, $_static_asset_type) !== FALSE) |
| 2250 |
self::$static_assets_rebuild_after_save[] = $_static_asset_type; |
| 2251 |
self::$static_assets_rebuild_after_save = array_values(array_unique(self::$static_assets_rebuild_after_save)); |
| 2252 |
self::invalidate_static_assets($invalidate); |
| 2253 |
} |
| 2254 |
return; |
| 2255 |
} |
| 2256 |
if(in_array((string)$option, array('siteurl', 'home'), TRUE)) |
| 2257 |
self::invalidate_static_assets(array('css', 'js')); |
| 2258 |
else if((string)$option === 'WPLANG' && self::static_js_text_delivery() !== 'page') |
| 2259 |
self::invalidate_static_assets('js'); |
| 2260 |
return; |
| 2261 |
} |
| 2262 |
|
| 2263 |
/** |
| 2264 |
* Rebuilds enabled static asset types after s2Member finishes saving relevant options. |
| 2265 |
* |
| 2266 |
* @package s2Member\Utilities |
| 2267 |
* @since 260911.1834 |
| 2268 |
* |
| 2269 |
* @param array $vars Variables passed by ws_plugin__s2member_after_update_all_options. |
| 2270 |
* @return null |
| 2271 |
*/ |
| 2272 |
public static function rebuild_static_assets_after_options_save($vars = array()) |
| 2273 |
{ |
| 2274 |
if(empty($vars['updated_all_options']) || !self::$static_assets_rebuild_after_save) |
| 2275 |
return; |
| 2276 |
|
| 2277 |
$types = array_values(array_unique(self::$static_assets_rebuild_after_save)); |
| 2278 |
self::$static_assets_rebuild_after_save = array(); |
| 2279 |
$options = (!empty($vars['options']) && is_array($vars['options'])) ? $vars['options'] : get_option('ws_plugin__s2member_options', array()); |
| 2280 |
if(!is_array($options)) |
| 2281 |
return; |
| 2282 |
|
| 2283 |
//260911.1834 Build from the complete newly saved option set; lazy frontend generation remains the recovery path for later upgrades, deletions, or transient failures. |
| 2284 |
$previous_options = $GLOBALS['WS_PLUGIN__']['s2member']['o']; |
| 2285 |
$GLOBALS['WS_PLUGIN__']['s2member']['o'] = $options; |
| 2286 |
$results = array(); |
| 2287 |
foreach($types as $type) |
| 2288 |
if(in_array($type, array('css', 'js'), TRUE) && !empty($options['static_'.$type])) |
| 2289 |
$results[$type] = self::ensure_static_assets($type); |
| 2290 |
$GLOBALS['WS_PLUGIN__']['s2member']['o'] = $previous_options; |
| 2291 |
|
| 2292 |
if($results) |
| 2293 |
c_ws_plugin__s2member_utils_logs::log_entry('css-js', array('event' => 'Static CSS/JS rebuilt after option save', 'result' => 'completed', 'types' => array_keys($results))); |
| 2294 |
return; |
| 2295 |
} |
| 2296 |
|
| 2297 |
/** |
| 2298 |
* Rebuilds enabled static assets opportunistically on normal privileged administrator page-loads. |
| 2299 |
* |
| 2300 |
* Routine requests only perform a few build-state/filesystem checks. Actual generation runs only when an active asset is pending, has never been generated, or its current local file is missing. |
| 2301 |
* |
| 2302 |
* @package s2Member\Utilities |
| 2303 |
* @since 260911.1924 |
| 2304 |
* |
| 2305 |
* @return null |
| 2306 |
*/ |
| 2307 |
public static function maybe_rebuild_static_assets_on_admin_request() |
| 2308 |
{ |
| 2309 |
if(!is_admin() || !current_user_can('create_users') || (defined('DOING_AJAX') && DOING_AJAX)) |
| 2310 |
return; |
| 2311 |
if(empty($GLOBALS['WS_PLUGIN__']['s2member']['o']['static_css']) && empty($GLOBALS['WS_PLUGIN__']['s2member']['o']['static_js'])) |
| 2312 |
return; |
| 2313 |
|
| 2314 |
$location = self::static_assets_location(FALSE); |
| 2315 |
foreach(array('css' => 'static_css', 'js' => 'static_js') as $type => $option) |
| 2316 |
{ |
| 2317 |
if(empty($GLOBALS['WS_PLUGIN__']['s2member']['o'][$option])) |
| 2318 |
continue; |
| 2319 |
$needs_rebuild = FALSE; |
| 2320 |
foreach(self::static_asset_ids($type, 'all') as $id) |
| 2321 |
{ |
| 2322 |
$build = self::static_asset_build($id); |
| 2323 |
if($build <= 0) |
| 2324 |
{ |
| 2325 |
$needs_rebuild = TRUE; |
| 2326 |
continue; |
| 2327 |
} |
| 2328 |
if(!empty($location['ok'])) |
| 2329 |
{ |
| 2330 |
$base = substr($id, 0, -strlen('.'.$type)); |
| 2331 |
if(!is_file($location['dir'].'/'.$base.'-'.$build.'.'.$type)) |
| 2332 |
{ |
| 2333 |
$needs_rebuild = TRUE; |
| 2334 |
} |
| 2335 |
} |
| 2336 |
} |
| 2337 |
//260911.2325 Generation/failure cooldown, missing-file repair locking, and repaired-issue history are centralized in ensure_static_asset(); avoid a second history write from the admin recovery wrapper. |
| 2338 |
if($needs_rebuild) |
| 2339 |
self::ensure_static_assets($type); |
| 2340 |
} |
| 2341 |
return; |
| 2342 |
} |
| 2343 |
|
| 2344 |
/** |
| 2345 |
* Invalidates generated frontend assets when plugin activation/deactivation can change frontend integrations. |
| 2346 |
* |
| 2347 |
* @package s2Member\Utilities |
| 2348 |
* @since 260903.0437 |
| 2349 |
* |
| 2350 |
* @return null |
| 2351 |
*/ |
| 2352 |
public static function invalidate_after_plugin_change($plugin = '') |
| 2353 |
{ |
| 2354 |
$plugin = (string)$plugin; |
| 2355 |
if($plugin === 's2member-pro/s2member-pro.php') |
| 2356 |
self::reset_static_asset_builds(); |
| 2357 |
else if($plugin === 'buddypress/bp-loader.php') |
| 2358 |
self::invalidate_static_assets('framework_js'); |
| 2359 |
return; |
| 2360 |
} |
| 2361 |
|
| 2362 |
/** |
| 2363 |
* Invalidates generated frontend assets after plugin/translation upgrades. |
| 2364 |
* |
| 2365 |
* @package s2Member\Utilities |
| 2366 |
* @since 260903.0437 |
| 2367 |
* |
| 2368 |
* @param object $upgrader WordPress upgrader instance. |
| 2369 |
* @param array $options Upgrade details. |
| 2370 |
* @return null |
| 2371 |
*/ |
| 2372 |
public static function maybe_invalidate_after_upgrade($upgrader = NULL, $options = array()) |
| 2373 |
{ |
| 2374 |
if(!is_array($options) || empty($options['type'])) |
| 2375 |
return; |
| 2376 |
if($options['type'] === 'translation') |
| 2377 |
{ |
| 2378 |
if(self::static_js_text_delivery() !== 'page') |
| 2379 |
self::invalidate_static_assets('js'); //260906.2049 Page-loaded JavaScript text follows the current translation without rebuilding the external static file. |
| 2380 |
return; |
| 2381 |
} |
| 2382 |
if($options['type'] !== 'plugin') |
| 2383 |
return; |
| 2384 |
|
| 2385 |
$plugins = array(); |
| 2386 |
if(!empty($options['plugin'])) |
| 2387 |
$plugins[] = (string)$options['plugin']; |
| 2388 |
if(!empty($options['plugins']) && is_array($options['plugins'])) |
| 2389 |
$plugins = array_merge($plugins, $options['plugins']); |
| 2390 |
foreach(array_unique($plugins) as $plugin) |
| 2391 |
{ |
| 2392 |
if($plugin === 's2member/s2member.php') |
| 2393 |
self::invalidate_static_assets(array('framework_css', 'framework_js')); |
| 2394 |
else if($plugin === 's2member-pro/s2member-pro.php') |
| 2395 |
self::invalidate_static_assets(array('pro_css', 'pro_js')); |
| 2396 |
else if($plugin === 'buddypress/bp-loader.php') |
| 2397 |
self::invalidate_static_assets('framework_js'); |
| 2398 |
} |
| 2399 |
return; |
| 2400 |
} |
| 2401 |
|
| 2402 |
/** |
| 2403 |
* Deletes a static-asset repair lock only when the stored value still belongs to the expected owner. |
| 2404 |
* |
| 2405 |
* @package s2Member\Utilities |
| 2406 |
* @since 260913.0704 |
| 2407 |
* |
| 2408 |
* @param string $option Repair-lock option name. |
| 2409 |
* @param string $lock Expected lock-owner value. |
| 2410 |
* @return bool True when this exact lock was deleted. |
| 2411 |
*/ |
| 2412 |
protected static function static_asset_repair_lock_delete($option = '', $lock = '') |
| 2413 |
{ |
| 2414 |
global $wpdb; |
| 2415 |
|
| 2416 |
$option = (string)$option; |
| 2417 |
$lock = (string)$lock; |
| 2418 |
if($option === '' || $lock === '') |
| 2419 |
return FALSE; |
| 2420 |
|
| 2421 |
//260913.0704 Delete only the lock version this request observed or acquired; another request may have replaced it in the meantime. |
| 2422 |
$deleted = $wpdb->delete($wpdb->options, array('option_name' => $option, 'option_value' => maybe_serialize($lock)), array('%s', '%s')); |
| 2423 |
if($deleted) |
| 2424 |
wp_cache_delete($option, 'options'); |
| 2425 |
return (bool)$deleted; |
| 2426 |
} |
| 2427 |
|
| 2428 |
/** |
| 2429 |
* Returns one current generated frontend asset URL, building it when stale/uninitialized. |
| 2430 |
* |
| 2431 |
* Active timestamped files are existence-checked before their URLs are emitted. A missing or |
| 2432 |
* trusted-browser-confirmed unreachable file returns a failure so callers can use dynamic delivery immediately. |
| 2433 |
* |
| 2434 |
* @package s2Member\Utilities |
| 2435 |
* @since 260903.0525 |
| 2436 |
* |
| 2437 |
* @param string $id Logical generated filename. |
| 2438 |
* @param bool $force Force a new build timestamp immediately. |
| 2439 |
* @return array Result with `ok`, `url`, `build`, and `error` keys. |
| 2440 |
*/ |
| 2441 |
public static function ensure_static_asset($id = '', $force = FALSE) |
| 2442 |
{ |
| 2443 |
$id = strtolower((string)$id); |
| 2444 |
if(in_array($id, array('css', 'js'), TRUE)) |
| 2445 |
$id = 's2member.'.$id; |
| 2446 |
if(!in_array($id, array('s2member.css', 's2member-pro.css', 's2member.js', 's2member-pro.js'), TRUE)) |
| 2447 |
return array('ok' => FALSE, 'url' => '', 'build' => 0, 'error' => 'Invalid static asset ID'); |
| 2448 |
$type = substr(strrchr($id, '.'), 1); |
| 2449 |
if(!in_array($id, self::static_asset_ids($type, 'all'), TRUE)) |
| 2450 |
return array('ok' => FALSE, 'url' => '', 'build' => 0, 'error' => 'Static asset is not active in the current delivery mode'); |
| 2451 |
if(!$force && isset(self::$static_asset_cache[$id])) |
| 2452 |
return self::$static_asset_cache[$id]; |
| 2453 |
|
| 2454 |
//260903.0544 Normal requests only check whether current hooks/configuration permit static delivery; source assembly and filesystem work wait until a build is actually needed. |
| 2455 |
$compatibility = self::static_asset_definition($id, FALSE); |
| 2456 |
if(empty($compatibility['ok'])) |
| 2457 |
return self::$static_asset_cache[$id] = array('ok' => FALSE, 'url' => '', 'build' => abs(self::static_asset_build($id)), 'error' => (string)$compatibility['error'], 'dynamic_required' => !empty($compatibility['dynamic_required'])); |
| 2458 |
|
| 2459 |
$state = self::static_asset_build($id); |
| 2460 |
$dirty = $state < 0; |
| 2461 |
$active_build = abs($state); |
| 2462 |
$base = substr($id, 0, -strlen('.'.$type)); |
| 2463 |
$failure_key = 'ws_plugin__s2member_static_asset_failure_'.str_replace('.', '_', $id); |
| 2464 |
$repair_lock_key = ''; |
| 2465 |
$repair_lock_value = ''; |
| 2466 |
$data_map_signature = array('ok' => TRUE, 'signature' => '', 'error' => ''); |
| 2467 |
$uses_data_map = $type === 'js' && self::static_js_text_delivery() === 'page'; |
| 2468 |
if($uses_data_map) |
| 2469 |
{ |
| 2470 |
$data_map_signature = self::static_js_data_map_signature($id); |
| 2471 |
if(empty($data_map_signature['ok'])) |
| 2472 |
return self::$static_asset_cache[$id] = array('ok' => FALSE, 'url' => '', 'build' => $active_build, 'error' => (string)$data_map_signature['error']); |
| 2473 |
//260906.1530 Regenerate static JavaScript when its shipped data-map layout changes so slot numbers stay synchronized. |
| 2474 |
if($active_build && self::static_asset_data_map_signature($id) !== (string)$data_map_signature['signature']) |
| 2475 |
{ |
| 2476 |
$dirty = TRUE; |
| 2477 |
$state = -$active_build; |
| 2478 |
self::set_static_asset_build($id, $state); |
| 2479 |
} |
| 2480 |
} |
| 2481 |
if(!$force && !$dirty && $active_build) |
| 2482 |
{ |
| 2483 |
$location = self::static_assets_location(FALSE); |
| 2484 |
if(empty($location['ok'])) |
| 2485 |
return self::$static_asset_cache[$id] = array('ok' => FALSE, 'url' => '', 'build' => $active_build, 'error' => $location['error']); |
| 2486 |
$url = $location['url'].'/'.$base.'-'.$active_build.'.'.$type; |
| 2487 |
$path = $location['dir'].'/'.$base.'-'.$active_build.'.'.$type; |
| 2488 |
if(!is_file($path)) |
| 2489 |
{ |
| 2490 |
//260911.1806 A configured static file that vanished locally is not an admin preference: try one guarded synchronous repair, then let normal dynamic fallback handle this request if repair cannot complete. |
| 2491 |
if($failure = get_transient($failure_key)) |
| 2492 |
return self::$static_asset_cache[$id] = array('ok' => FALSE, 'url' => '', 'build' => $active_build, 'error' => (string)$failure); |
| 2493 |
$repair_lock_key = 'ws_plugin__s2member_static_asset_repair_lock_'.str_replace('.', '_', $id); |
| 2494 |
$repair_lock_current = (string)get_option($repair_lock_key, ''); |
| 2495 |
$repair_lock_parts = explode(':', $repair_lock_current, 2); |
| 2496 |
$repair_lock_time = (!empty($repair_lock_parts[0]) && is_numeric($repair_lock_parts[0])) ? (int)$repair_lock_parts[0] : 0; |
| 2497 |
//260913.0704 Preserve compatibility with older timestamp-only locks while making stale takeover conditional on the exact lock value this request inspected. |
| 2498 |
if($repair_lock_current !== '' && (!$repair_lock_time || $repair_lock_time < time() - 30)) |
| 2499 |
self::static_asset_repair_lock_delete($repair_lock_key, $repair_lock_current); |
| 2500 |
$repair_lock_value = time().':'.sha1(microtime(TRUE)."\0".wp_rand()); |
| 2501 |
if(!add_option($repair_lock_key, $repair_lock_value, '', 'no')) |
| 2502 |
return self::$static_asset_cache[$id] = array('ok' => FALSE, 'url' => '', 'build' => $active_build, 'error' => 'Expected static asset '.$id.' is missing; another request is already rebuilding it.'); |
| 2503 |
$dirty = TRUE; |
| 2504 |
} |
| 2505 |
else |
| 2506 |
{ |
| 2507 |
//260904.2110 A local file check is cheaper than sending a broken static URL; browser-confirmed public-URL failures still fall back without rebuilding a valid local file. |
| 2508 |
if(self::asset_http_target_failed('static:'.$id, $url)) |
| 2509 |
return self::$static_asset_cache[$id] = array('ok' => FALSE, 'url' => '', 'build' => $active_build, 'error' => 'Static asset '.$id.' could not be loaded from its public URL.'); |
| 2510 |
return self::$static_asset_cache[$id] = array('ok' => TRUE, 'url' => $url, 'build' => $active_build, 'error' => ''); |
| 2511 |
} |
| 2512 |
} |
| 2513 |
|
| 2514 |
if(!$force && $dirty && ($failure = get_transient($failure_key))) |
| 2515 |
return self::$static_asset_cache[$id] = array('ok' => FALSE, 'url' => '', 'build' => $active_build, 'error' => (string)$failure); |
| 2516 |
|
| 2517 |
$definition = self::static_asset_definition($id, TRUE); |
| 2518 |
if(empty($definition['ok'])) |
| 2519 |
{ |
| 2520 |
if($repair_lock_key !== '') |
| 2521 |
{ |
| 2522 |
set_transient($failure_key, (string)$definition['error'], 5 * MINUTE_IN_SECONDS); |
| 2523 |
self::static_asset_repair_lock_delete($repair_lock_key, $repair_lock_value); |
| 2524 |
} |
| 2525 |
return self::$static_asset_cache[$id] = array('ok' => FALSE, 'url' => '', 'build' => $active_build, 'error' => (string)$definition['error']); |
| 2526 |
} |
| 2527 |
|
| 2528 |
$build = max(time(), $active_build + 1); |
| 2529 |
$result = self::build_static_asset($base, $build, $type, $definition['sources'], !empty($definition['minify'])); |
| 2530 |
if(!empty($result['ok'])) |
| 2531 |
{ |
| 2532 |
if($uses_data_map) |
| 2533 |
self::set_static_asset_data_map_signature($id, (string)$data_map_signature['signature']); |
| 2534 |
self::set_static_asset_build($id, $build); |
| 2535 |
|
| 2536 |
//260907.2203 Record successful generation so automatic and manual rebuilds remain visible later. |
| 2537 |
c_ws_plugin__s2member_utils_logs::log_entry('css-js', array( |
| 2538 |
'event' => 'Static CSS/JS asset generated', 'result' => 'success', 'asset' => $id, 'build' => $build, |
| 2539 |
'trigger' => $force ? 'forced refresh' : 'automatic generation', 'minified' => !empty($definition['minify']), 'url' => $result['url'], |
| 2540 |
)); |
| 2541 |
|
| 2542 |
//260905.0106 Prune only after the new timestamp is current so the previous generation is treated as stale instead of protected. |
| 2543 |
self::prune_static_asset_generations(dirname($result['path']), $result['path']); |
| 2544 |
delete_transient($failure_key); |
| 2545 |
if($repair_lock_key !== '') |
| 2546 |
{ |
| 2547 |
//260911.1834 A missing active file that repaired successfully is still useful history, but it must not lower the health score because this request retained static delivery. |
| 2548 |
$health_id = self::asset_runtime_health_id($id, $type, 'static'); |
| 2549 |
self::queue_asset_health_issue_snapshot('repaired', self::asset_runtime_health_label($health_id), 'Expected static asset '.$id.' was missing and was rebuilt automatically.'); |
| 2550 |
self::static_asset_repair_lock_delete($repair_lock_key, $repair_lock_value); |
| 2551 |
} |
| 2552 |
return self::$static_asset_cache[$id] = array('ok' => TRUE, 'url' => $result['url'], 'build' => $build, 'error' => ''); |
| 2553 |
} |
| 2554 |
set_transient($failure_key, (string)$result['error'], 5 * MINUTE_IN_SECONDS); |
| 2555 |
if($repair_lock_key !== '') |
| 2556 |
self::static_asset_repair_lock_delete($repair_lock_key, $repair_lock_value); |
| 2557 |
|
| 2558 |
//260907.2203 Preserve failed generation details even when delivery later falls back or recovers automatically. |
| 2559 |
c_ws_plugin__s2member_utils_logs::log_entry('css-js', array( |
| 2560 |
'event' => 'Static CSS/JS asset generation failed', 'result' => 'failure', 'asset' => $id, 'attempted_build' => $build, |
| 2561 |
'previous_build' => $active_build, 'trigger' => $force ? 'forced refresh' : 'automatic generation', 'error' => (string)$result['error'], |
| 2562 |
)); |
| 2563 |
|
| 2564 |
return self::$static_asset_cache[$id] = array('ok' => FALSE, 'url' => '', 'build' => $active_build, 'error' => $result['error']); |
| 2565 |
} |
| 2566 |
|
| 2567 |
/** |
| 2568 |
* Returns all active generated frontend assets for one type. |
| 2569 |
* |
| 2570 |
* If any active file cannot be generated safely, callers fall back to the legacy dynamic asset for the entire type rather than mixing static and dynamic representations. |
| 2571 |
* |
| 2572 |
* @package s2Member\Utilities |
| 2573 |
* @since 260903.1918 |
| 2574 |
* |
| 2575 |
* @param string $type `css` or `js`. |
| 2576 |
* @param bool $force Force fresh timestamps for every active file of this type. |
| 2577 |
* @return array Aggregate result with individual assets keyed by logical filename. |
| 2578 |
*/ |
| 2579 |
public static function ensure_static_assets($type = '', $force = FALSE) |
| 2580 |
{ |
| 2581 |
$type = strtolower((string)$type); |
| 2582 |
if(!in_array($type, array('css', 'js'), TRUE)) |
| 2583 |
return array('ok' => FALSE, 'assets' => array(), 'error' => 'Invalid static asset type'); |
| 2584 |
$option = 'static_'.$type; |
| 2585 |
if(empty($GLOBALS['WS_PLUGIN__']['s2member']['o'][$option])) |
| 2586 |
return array('ok' => FALSE, 'assets' => array(), 'error' => 'Static '.strtoupper($type).' Delivery is disabled'); |
| 2587 |
|
| 2588 |
$assets = array(); |
| 2589 |
foreach(self::static_asset_ids($type, 'all') as $id) |
| 2590 |
{ |
| 2591 |
$assets[$id] = self::ensure_static_asset($id, $force); |
| 2592 |
if(empty($assets[$id]['ok'])) |
| 2593 |
return array('ok' => FALSE, 'assets' => $assets, 'error' => (string)$assets[$id]['error'], 'dynamic_required' => !empty($assets[$id]['dynamic_required'])); |
| 2594 |
} |
| 2595 |
return array('ok' => (bool)$assets, 'assets' => $assets, 'error' => ''); |
| 2596 |
} |
| 2597 |
|
| 2598 |
/** |
| 2599 |
* Refreshes all currently enabled static frontend asset types immediately. |
| 2600 |
* |
| 2601 |
* CSS and JS remain independent; within each type only files active in the current separate/combined representation are rebuilt. |
| 2602 |
* |
| 2603 |
* @package s2Member\Utilities |
| 2604 |
* @since 260903.0525 |
| 2605 |
* |
| 2606 |
* @return array Results keyed by `css` and/or `js`. |
| 2607 |
*/ |
| 2608 |
public static function refresh_static_assets() |
| 2609 |
{ |
| 2610 |
$results = array(); |
| 2611 |
foreach(array('css' => 'static_css', 'js' => 'static_js') as $type => $option) |
| 2612 |
if(!empty($GLOBALS['WS_PLUGIN__']['s2member']['o'][$option])) |
| 2613 |
{ |
| 2614 |
foreach(self::static_asset_ids($type, 'all') as $id) |
| 2615 |
{ |
| 2616 |
unset(self::$static_asset_cache[$id]); |
| 2617 |
delete_transient('ws_plugin__s2member_static_asset_failure_'.str_replace('.', '_', $id)); |
| 2618 |
} |
| 2619 |
$results[$type] = self::ensure_static_assets($type, TRUE); |
| 2620 |
} |
| 2621 |
return $results; |
| 2622 |
} |
| 2623 |
|
| 2624 |
/** |
| 2625 |
* AJAX handler for the General Options “Refresh Static Assets” button. |
| 2626 |
* |
| 2627 |
* @package s2Member\Utilities |
| 2628 |
* @since 260903.0437 |
| 2629 |
* |
| 2630 |
* @return null Exits through WordPress JSON helpers. |
| 2631 |
*/ |
| 2632 |
public static function ajax_refresh_static_assets() |
| 2633 |
{ |
| 2634 |
check_ajax_referer('ws-plugin--s2member-refresh-static-assets'); |
| 2635 |
if(!current_user_can('create_users')) |
| 2636 |
wp_send_json_error(array('message' => 'You do not have permission to refresh s2Member static assets.'), 403); |
| 2637 |
|
| 2638 |
$results = self::refresh_static_assets(); |
| 2639 |
if(!$results) |
| 2640 |
wp_send_json_error(array('message' => 'Enable Static CSS Delivery or Static JS Delivery and save the options first.'), 400); |
| 2641 |
|
| 2642 |
$success = $errors = array(); |
| 2643 |
foreach($results as $type => $result) |
| 2644 |
if(!empty($result['ok'])) |
| 2645 |
$success[] = strtoupper($type); |
| 2646 |
else |
| 2647 |
$errors[] = strtoupper($type).': '.((!empty($result['error'])) ? $result['error'] : 'unknown build error'); |
| 2648 |
|
| 2649 |
if(!$errors) |
| 2650 |
{ |
| 2651 |
//260907.2203 Record the administrator-triggered refresh result. |
| 2652 |
c_ws_plugin__s2member_utils_logs::log_entry('css-js', array('event' => 'Manual Static CSS/JS refresh', 'result' => 'success', 'refreshed' => $success)); |
| 2653 |
|
| 2654 |
wp_send_json_success(array('message' => 'Static '.implode(' + ', $success).' refreshed. New timestamped files are active.')); |
| 2655 |
} |
| 2656 |
|
| 2657 |
//260907.2203 Record partial and failed administrator-triggered refreshes too. |
| 2658 |
c_ws_plugin__s2member_utils_logs::log_entry('css-js', array('event' => 'Manual Static CSS/JS refresh', 'result' => ($success ? 'partial failure' : 'failure'), 'refreshed' => $success, 'errors' => $errors)); |
| 2659 |
|
| 2660 |
$message = (($success) ? 'Refreshed '.implode(' + ', $success).'. ' : '').'Could not refresh '.implode('; ', $errors).'. Failed types continue with their previous valid files when still current, or dynamic delivery when stale.'; |
| 2661 |
wp_send_json_error(array('message' => $message), 500); |
| 2662 |
} |
| 2663 |
|
| 2664 |
/** |
| 2665 |
* Checks the few currently active generated files for local filesystem availability. |
| 2666 |
* |
| 2667 |
* This runs on administrator requests and is also mirrored by the per-file check immediately before a static frontend URL is used. With at most four active generated files, direct existence checks avoid stale health results with a small, bounded cost. |
| 2668 |
* |
| 2669 |
* @package s2Member\Utilities |
| 2670 |
* @since 260903.0525 |
| 2671 |
* |
| 2672 |
* @param bool $force Recheck even if this request already has a cached result. |
| 2673 |
* @return array Missing active asset files keyed by logical filename. |
| 2674 |
*/ |
| 2675 |
public static function static_assets_health($force = FALSE) |
| 2676 |
{ |
| 2677 |
if(!$force && isset(self::$static_assets_health_cache)) |
| 2678 |
return self::$static_assets_health_cache; |
| 2679 |
if(empty($GLOBALS['WS_PLUGIN__']['s2member']['o']['static_css']) && empty($GLOBALS['WS_PLUGIN__']['s2member']['o']['static_js'])) |
| 2680 |
return self::$static_assets_health_cache = array(); |
| 2681 |
|
| 2682 |
$missing = array(); |
| 2683 |
$checked_types = array(); |
| 2684 |
foreach(array('css' => 'static_css', 'js' => 'static_js') as $type => $option) |
| 2685 |
if(!empty($GLOBALS['WS_PLUGIN__']['s2member']['o'][$option])) |
| 2686 |
{ |
| 2687 |
$dynamic_requirement = self::static_type_dynamic_requirement($type); |
| 2688 |
if(empty($dynamic_requirement['required'])) |
| 2689 |
$checked_types[] = $type; //260913.2001 Only currently active static routes participate in local static-file health. |
| 2690 |
} |
| 2691 |
$location = self::static_assets_location(FALSE); |
| 2692 |
if($checked_types && empty($location['ok'])) |
| 2693 |
$missing['location'] = $location['error']; |
| 2694 |
else if(!empty($location['ok'])) |
| 2695 |
foreach($checked_types as $type) |
| 2696 |
foreach(self::static_asset_ids($type, 'all') as $id) |
| 2697 |
{ |
| 2698 |
$build = self::static_asset_build($id); |
| 2699 |
$base = substr($id, 0, -strlen('.'.$type)); |
| 2700 |
if($build > 0 && !is_file($location['dir'].'/'.$base.'-'.$build.'.'.$type)) |
| 2701 |
$missing[$id] = 'Expected static asset '.$id.' is missing.'; |
| 2702 |
} |
| 2703 |
|
| 2704 |
//260907.2203 Log only local-health transitions so recurring admin checks do not repeat the same event. |
| 2705 |
$previous = get_option('ws_plugin__s2member_static_asset_health', array()); |
| 2706 |
$previous = is_array($previous) ? $previous : array(); |
| 2707 |
if(serialize($previous) !== serialize($missing)) |
| 2708 |
{ |
| 2709 |
update_option('ws_plugin__s2member_static_asset_health', $missing, FALSE); |
| 2710 |
if($missing) |
| 2711 |
c_ws_plugin__s2member_utils_logs::log_entry('css-js', array('event' => 'Static CSS/JS local health issue', 'result' => 'failure', 'issues' => $missing)); |
| 2712 |
else if($previous) |
| 2713 |
c_ws_plugin__s2member_utils_logs::log_entry('css-js', array('event' => 'Static CSS/JS local health recovered', 'result' => 'recovered', 'previous_issues' => $previous)); |
| 2714 |
} |
| 2715 |
|
| 2716 |
return self::$static_assets_health_cache = $missing; |
| 2717 |
} |
| 2718 |
|
| 2719 |
/** |
| 2720 |
* Returns timing status for one physical frontend CSS/JavaScript response. |
| 2721 |
* |
| 2722 |
* A pending real-page activation delay is Late/Yellow until the trusted browser check decides |
| 2723 |
* whether the response is valid. Confirmed historical timing evidence remains in Health scoring |
| 2724 |
* and Latest Issues instead of making a recovered asset row look currently unhealthy. |
| 2725 |
* |
| 2726 |
* @package s2Member\Utilities |
| 2727 |
* @since 260909.2021 |
| 2728 |
* |
| 2729 |
* @param string $id Runtime-health ID. |
| 2730 |
* @return array Status details. |
| 2731 |
*/ |
| 2732 |
protected static function asset_runtime_health_event($id = '') |
| 2733 |
{ |
| 2734 |
$id = (string)$id; |
| 2735 |
$latest = 0; |
| 2736 |
$pending = FALSE; |
| 2737 |
|
| 2738 |
//260913.0059 Current rows describe current known state only; a trusted-successful follow-up leaves the Late event in scoring/Latest Issues instead of holding this row Yellow for an hour. |
| 2739 |
foreach(self::asset_runtime_suspicions() as $suspicion) |
| 2740 |
if(!empty($suspicion['id']) && (string)$suspicion['id'] === $id) |
| 2741 |
{ |
| 2742 |
$pending = TRUE; |
| 2743 |
$latest = max($latest, (int)$suspicion['reported']); |
| 2744 |
} |
| 2745 |
|
| 2746 |
if($pending) |
| 2747 |
return array( |
| 2748 |
'status' => 'delayed', |
| 2749 |
'label' => 'Late', |
| 2750 |
'detail' => 'A frontend page could not confirm that this asset became active within the configured wait time. A trusted browser check will verify the asset response.', |
| 2751 |
'reported' => $latest, |
| 2752 |
); |
| 2753 |
return array('status' => 'healthy', 'label' => 'Healthy', 'detail' => '', 'reported' => 0); |
| 2754 |
} |
| 2755 |
|
| 2756 |
/** |
| 2757 |
* Returns consolidated site-owner health for active frontend CSS/JavaScript delivery. |
| 2758 |
* |
| 2759 |
* Current rows explain the actual configured/preferred route and any fallback. The |
| 2760 |
* headline color comes from the Okay/Late/Fallback/Failed asset-load score: the latest 10 |
| 2761 |
* individual loads plus populated clock minutes from the latest 10 minutes, with newer evidence more important. |
| 2762 |
* |
| 2763 |
* @package s2Member\Utilities |
| 2764 |
* @since 260909.2021 |
| 2765 |
* |
| 2766 |
* @param bool $force Recheck local static-file health and request a full trusted browser probe on this admin page. |
| 2767 |
* @return array Overall status, score details, rows, notice level/items, and notice signature. |
| 2768 |
*/ |
| 2769 |
public static function frontend_asset_health($force = FALSE) |
| 2770 |
{ |
| 2771 |
if($force) |
| 2772 |
self::$asset_health_force_full_probe = TRUE; //260912.0522 Opening the Health panel asks the footer probe for full current-route activation checks, not only the cheap background reachability checks. |
| 2773 |
|
| 2774 |
//260912.0258 Run the Health Logkeeper before rendering admin health so queued frontend evidence is reflected without requiring another refresh. |
| 2775 |
self::run_health_logkeeper(); |
| 2776 |
$rows = array(); |
| 2777 |
$error_notice_items = array(); |
| 2778 |
$attention_items = array(); |
| 2779 |
//260910.0709 Rows describe the actual route in use; notice item lists are separate so Yellow/Orange status can remain informative without automatically becoming an admin-wide alarm. |
| 2780 |
$http_health = self::asset_http_health_state(); |
| 2781 |
$failures = (is_array($http_health) && !empty($http_health['failures']) && is_array($http_health['failures'])) ? $http_health['failures'] : array(); |
| 2782 |
$runtime_warnings = (is_array($http_health) && !empty($http_health['runtime_warnings']) && is_array($http_health['runtime_warnings'])) ? $http_health['runtime_warnings'] : array(); |
| 2783 |
$local_health = self::static_assets_health($force); |
| 2784 |
$location = self::static_assets_location(FALSE); |
| 2785 |
$selected_s2o = empty($GLOBALS['WS_PLUGIN__']['s2member']['o']['dynamic_asset_loader']) || $GLOBALS['WS_PLUGIN__']['s2member']['o']['dynamic_asset_loader'] !== 'wordpress'; |
| 2786 |
$dynamic_normal = array('css' => FALSE, 'js' => FALSE); |
| 2787 |
$wp_loader_active = array('css' => FALSE, 'js' => FALSE); |
| 2788 |
$wp_loader_fallback = array('css' => FALSE, 'js' => FALSE); |
| 2789 |
$wp_loader_required = array('css' => FALSE, 'js' => FALSE); |
| 2790 |
|
| 2791 |
foreach(array('css' => 'CSS', 'js' => 'JS') as $type => $type_label) |
| 2792 |
{ |
| 2793 |
$static_requested = !empty($GLOBALS['WS_PLUGIN__']['s2member']['o']['static_'.$type]); |
| 2794 |
if(!$static_requested) |
| 2795 |
{ |
| 2796 |
$dynamic_normal[$type] = TRUE; |
| 2797 |
$using_wordpress = self::dynamic_asset_url(FALSE) === self::wordpress_dynamic_asset_url(); |
| 2798 |
$wp_loader_active[$type] = $using_wordpress; |
| 2799 |
$wp_loader_fallback[$type] = $selected_s2o; |
| 2800 |
if($selected_s2o) |
| 2801 |
{ |
| 2802 |
//260912.1956 Show the configured s2Member-Only route separately from its WordPress fallback so each route's current health is understandable at a glance. |
| 2803 |
$s2o_url = ($type === 'css') |
| 2804 |
? add_query_arg(array('ws_plugin__s2member_css' => '1', 'qcABC' => '1'), $GLOBALS['WS_PLUGIN__']['s2member']['c']['s2o_url']) |
| 2805 |
: add_query_arg(array('ws_plugin__s2member_js_w_globals' => (defined('WS_PLUGIN__S2MEMBER_API_CONSTANTS_MD5') ? WS_PLUGIN__S2MEMBER_API_CONSTANTS_MD5 : '1'), 'qcABC' => '1'), $GLOBALS['WS_PLUGIN__']['s2member']['c']['s2o_url']); |
| 2806 |
$event = self::asset_runtime_health_event('dynamic_'.$type); |
| 2807 |
if($using_wordpress) |
| 2808 |
{ |
| 2809 |
$status = 'error'; |
| 2810 |
$status_label = 'Failed'; |
| 2811 |
$detail = 'The selected s2Member-Only Dynamic Loader could not be used; Full WordPress Dynamic fallback is serving this asset.'; |
| 2812 |
$attention_items['fallback:'.$type] = $type_label.' is using Full WordPress Dynamic Loader because the selected s2Member-Only Dynamic Loader could not be used.'; |
| 2813 |
} |
| 2814 |
else |
| 2815 |
{ |
| 2816 |
$status = $event['status']; |
| 2817 |
$status_label = $event['label']; |
| 2818 |
$detail = 's2Member-Only Dynamic Loader.'.(($event['detail']) ? ' '.$event['detail'] : ''); |
| 2819 |
} |
| 2820 |
$rows[] = array('label' => 's2Member-Only '.$type_label, 'delivery' => 'Dynamic', 'status' => $status, 'status_label' => $status_label, 'detail' => $detail, 'url' => $s2o_url); |
| 2821 |
} |
| 2822 |
continue; |
| 2823 |
} |
| 2824 |
|
| 2825 |
$ids = self::static_asset_ids($type, 'all'); |
| 2826 |
$dynamic_requirement = self::static_type_dynamic_requirement($type); |
| 2827 |
if(!empty($dynamic_requirement['required'])) |
| 2828 |
{ |
| 2829 |
//260913.2001 A compatibility-required Dynamic route is expected delivery, not an Orange fallback; Full WordPress remains mandatory so the triggering hooks/configuration are present. |
| 2830 |
$wp_loader_active[$type] = TRUE; |
| 2831 |
$wp_loader_fallback[$type] = FALSE; |
| 2832 |
$wp_loader_required[$type] = TRUE; |
| 2833 |
$delivery_url = self::wordpress_dynamic_asset_url(); |
| 2834 |
$delivery_url = ($type === 'css') ? add_query_arg(array('ws_plugin__s2member_css' => '1', 'qcABC' => '1'), $delivery_url) : add_query_arg(array('ws_plugin__s2member_js_w_globals' => (defined('WS_PLUGIN__S2MEMBER_API_CONSTANTS_MD5') ? WS_PLUGIN__S2MEMBER_API_CONSTANTS_MD5 : '1'), 'qcABC' => '1'), $delivery_url); |
| 2835 |
$failed = !empty($failures['dynamic:dynamic_'.$type]); |
| 2836 |
$status = ($failed) ? 'error' : 'healthy'; |
| 2837 |
$status_label = ($failed) ? 'Delivery check failed' : 'Healthy'; |
| 2838 |
//260913.2111 The requirement detail now carries concise, actionable guidance; do not append a generic compatibility sentence to every row. |
| 2839 |
$detail = (string)$dynamic_requirement['detail']; |
| 2840 |
if($failed) |
| 2841 |
{ |
| 2842 |
$detail .= ' The required Full WordPress Dynamic Loader could not be loaded or confirmed active.'; |
| 2843 |
$error_notice_items['delivery:'.$type] = $type_label.' requires Full WordPress Dynamic delivery, but that route could not be loaded or verified.'; |
| 2844 |
} |
| 2845 |
$rows[] = array('label' => $type_label.' Delivery', 'delivery' => 'Dynamic required', 'status' => $status, 'status_label' => $status_label, 'detail' => $detail, 'url' => $delivery_url); |
| 2846 |
continue; |
| 2847 |
} |
| 2848 |
|
| 2849 |
$fallback = FALSE; |
| 2850 |
$fallback_reasons = array(); |
| 2851 |
$states = array(); |
| 2852 |
$generation_failures = array(); |
| 2853 |
|
| 2854 |
//260910.0630 A compatibility/build fallback applies to the whole asset type; stale failures for static files that are no longer being served must not masquerade as current delivery failures. |
| 2855 |
foreach($ids as $id) |
| 2856 |
{ |
| 2857 |
$states[$id] = self::static_asset_build($id); |
| 2858 |
$generation_failures[$id] = get_transient('ws_plugin__s2member_static_asset_failure_'.str_replace('.', '_', $id)); |
| 2859 |
$definition = self::static_asset_definition($id, FALSE); |
| 2860 |
if(empty($definition['ok'])) |
| 2861 |
{ |
| 2862 |
$fallback = TRUE; |
| 2863 |
$fallback_reasons[] = (string)$definition['error']; |
| 2864 |
} |
| 2865 |
else if($generation_failures[$id] && $states[$id] <= 0) |
| 2866 |
{ |
| 2867 |
$fallback = TRUE; |
| 2868 |
$fallback_reasons[] = $id.': '.(string)$generation_failures[$id]; |
| 2869 |
} |
| 2870 |
} |
| 2871 |
|
| 2872 |
if(!$fallback) |
| 2873 |
foreach($ids as $id) |
| 2874 |
{ |
| 2875 |
$state = $states[$id]; |
| 2876 |
$build = abs($state); |
| 2877 |
if(!empty($local_health['location'])) |
| 2878 |
{ |
| 2879 |
$fallback = TRUE; |
| 2880 |
$fallback_reasons[] = (string)$local_health['location']; |
| 2881 |
continue; |
| 2882 |
} |
| 2883 |
if(isset($local_health[$id])) |
| 2884 |
{ |
| 2885 |
$fallback = TRUE; |
| 2886 |
$fallback_reasons[] = (string)$local_health[$id]; |
| 2887 |
continue; |
| 2888 |
} |
| 2889 |
if($state > 0) |
| 2890 |
{ |
| 2891 |
$base = substr($id, 0, -strlen('.'.$type)); |
| 2892 |
$url = (!empty($location['ok'])) ? $location['url'].'/'.$base.'-'.$build.'.'.$type : ''; |
| 2893 |
if($url && self::asset_http_target_failed('static:'.$id, $url)) |
| 2894 |
{ |
| 2895 |
$fallback = TRUE; |
| 2896 |
$fallback_reasons[] = $id.' could not be loaded from its public URL.'; |
| 2897 |
} |
| 2898 |
} |
| 2899 |
} |
| 2900 |
|
| 2901 |
if($fallback) |
| 2902 |
{ |
| 2903 |
$wp_loader_active[$type] = TRUE; |
| 2904 |
$wp_loader_fallback[$type] = TRUE; |
| 2905 |
$status = 'attention'; |
| 2906 |
$status_label = 'Using dynamic fallback'; |
| 2907 |
$detail = 'Full WordPress Dynamic Loader is being used instead of the requested static '.$type_label.' delivery.'; |
| 2908 |
if($fallback_reasons) |
| 2909 |
$detail .= ' '.implode(' ', array_unique($fallback_reasons)); |
| 2910 |
$attention_items['fallback:'.$type] = 'Requested static '.$type_label.' delivery is unavailable; Full WordPress Dynamic Loader is being used instead.'; |
| 2911 |
$delivery_url = self::wordpress_dynamic_asset_url(); |
| 2912 |
$delivery_url = ($type === 'css') ? add_query_arg(array('ws_plugin__s2member_css' => '1', 'qcABC' => '1'), $delivery_url) : add_query_arg(array('ws_plugin__s2member_js_w_globals' => (defined('WS_PLUGIN__S2MEMBER_API_CONSTANTS_MD5') ? WS_PLUGIN__S2MEMBER_API_CONSTANTS_MD5 : '1'), 'qcABC' => '1'), $delivery_url); |
| 2913 |
$event = self::asset_runtime_health_event('dynamic_'.$type); |
| 2914 |
if($event['detail']) |
| 2915 |
$detail .= ' '.$event['detail']; |
| 2916 |
if(!empty($failures['fallback:dynamic_'.$type]) || !empty($failures['dynamic:dynamic_'.$type])) |
| 2917 |
{ |
| 2918 |
$status = 'error'; |
| 2919 |
$status_label = 'Fallback check failed'; |
| 2920 |
$detail = 'The requested static '.$type_label.' delivery is unavailable, and the Full WordPress Dynamic fallback could not be loaded or verified.'; |
| 2921 |
$error_notice_items['delivery:'.$type] = $type_label.' preferred delivery is unavailable and the Full WordPress Dynamic fallback could not be loaded or verified.'; |
| 2922 |
} |
| 2923 |
$rows[] = array('label' => $type_label.' Delivery', 'delivery' => 'Dynamic fallback', 'status' => $status, 'status_label' => $status_label, 'detail' => $detail, 'url' => $delivery_url); |
| 2924 |
continue; |
| 2925 |
} |
| 2926 |
|
| 2927 |
$wp_loader_fallback[$type] = TRUE; |
| 2928 |
foreach($ids as $id) |
| 2929 |
{ |
| 2930 |
$state = self::static_asset_build($id); |
| 2931 |
$build = abs($state); |
| 2932 |
$failure = get_transient('ws_plugin__s2member_static_asset_failure_'.str_replace('.', '_', $id)); |
| 2933 |
$health_id = self::asset_runtime_health_id($id, $type, 'static'); |
| 2934 |
$event = self::asset_runtime_health_event($health_id); |
| 2935 |
$status = $event['status']; |
| 2936 |
$status_label = $event['label']; |
| 2937 |
if($state < 0) |
| 2938 |
{ |
| 2939 |
$status = ($status === 'healthy') ? 'delayed' : $status; |
| 2940 |
$status_label = ($status === 'delayed' && $event['status'] === 'healthy') ? 'Pending rebuild' : $status_label; |
| 2941 |
$detail = 'Static file is pending rebuild; its previous timestamp remains only for already-cached HTML.'; |
| 2942 |
} |
| 2943 |
else |
| 2944 |
$detail = ($build > 0) ? 'Static file is current (build '.date_i18n('Y-m-d H:i:s', $build).').' : 'Static file has not been created yet; s2Member will build it automatically.'; |
| 2945 |
if($failure && $state > 0) |
| 2946 |
{ |
| 2947 |
$status = ($status === 'healthy') ? 'delayed' : $status; |
| 2948 |
$status_label = ($status === 'delayed' && $event['status'] === 'healthy') ? 'Rebuild issue' : $status_label; |
| 2949 |
$detail .= ' A recent rebuild failed, but the previous valid static file remains active. '.(string)$failure; |
| 2950 |
} |
| 2951 |
if($event['detail']) |
| 2952 |
$detail .= ' '.$event['detail']; |
| 2953 |
$base = substr($id, 0, -strlen('.'.$type)); |
| 2954 |
$url = ($build > 0 && !empty($location['ok'])) ? $location['url'].'/'.$base.'-'.$build.'.'.$type : ''; |
| 2955 |
$rows[] = array('label' => self::asset_runtime_health_label($health_id), 'delivery' => 'Static', 'status' => $status, 'status_label' => $status_label, 'detail' => $detail, 'url' => $url); |
| 2956 |
} |
| 2957 |
} |
| 2958 |
|
| 2959 |
$s2o_missing = $selected_s2o && !is_file(self::s2o_file_path()); |
| 2960 |
$s2o_failed = $selected_s2o && !$s2o_missing && self::asset_http_target_failed('s2o', $GLOBALS['WS_PLUGIN__']['s2member']['c']['s2o_url']); |
| 2961 |
$s2o_needed = $selected_s2o && ($dynamic_normal['css'] || $dynamic_normal['js']); |
| 2962 |
$s2o_problem = $s2o_missing || $s2o_failed; |
| 2963 |
|
| 2964 |
if($selected_s2o && $s2o_problem && $s2o_needed) |
| 2965 |
$attention_items['s2o'] = ($s2o_missing) ? 'The selected s2Member-Only Dynamic Loader file <code>s2member-o.php</code> is missing; Full WordPress Dynamic Loader is being used automatically.' : 'The selected s2Member-Only Dynamic Loader could not be reached; Full WordPress Dynamic Loader is being used automatically.'; |
| 2966 |
|
| 2967 |
//260907.2203 Track missing/recovered loader transitions without logging every admin health check. |
| 2968 |
//260909.2021 Keep those transitions in css-js.log even when the loader is not currently needed by fully static delivery. |
| 2969 |
$s2o_missing_logged = (bool)get_option('ws_plugin__s2member_css_js_s2o_missing', FALSE); |
| 2970 |
if($s2o_missing && !$s2o_missing_logged) |
| 2971 |
{ |
| 2972 |
update_option('ws_plugin__s2member_css_js_s2o_missing', 1, FALSE); |
| 2973 |
c_ws_plugin__s2member_utils_logs::log_entry('css-js', array('event' => 's2Member-Only Dynamic Loader file missing', 'result' => 'failure', 'file' => self::s2o_file_path(), 'fallback' => 'Full WordPress Dynamic Loader')); |
| 2974 |
} |
| 2975 |
else if($selected_s2o && !$s2o_missing && $s2o_missing_logged) |
| 2976 |
{ |
| 2977 |
delete_option('ws_plugin__s2member_css_js_s2o_missing'); |
| 2978 |
c_ws_plugin__s2member_utils_logs::log_entry('css-js', array('event' => 's2Member-Only Dynamic Loader file recovered', 'result' => 'recovered', 'file' => self::s2o_file_path())); |
| 2979 |
} |
| 2980 |
|
| 2981 |
//260912.1956 Full WordPress is always either the configured dynamic route or the safety-net fallback, so keep its CSS/JS health visible even when static delivery is currently healthy. |
| 2982 |
$full_checked = (!empty($http_health['full_checked'])) ? (int)$http_health['full_checked'] : 0; |
| 2983 |
foreach(array('css' => 'CSS', 'js' => 'JS') as $type => $type_label) |
| 2984 |
{ |
| 2985 |
$is_fallback = !empty($wp_loader_fallback[$type]); |
| 2986 |
$failure_id = ($is_fallback) ? 'fallback:dynamic_'.$type : 'dynamic:dynamic_'.$type; |
| 2987 |
$failed = !empty($failures[$failure_id]); |
| 2988 |
$delivery_url = self::wordpress_dynamic_asset_url(); |
| 2989 |
$delivery_url = ($type === 'css') |
| 2990 |
? add_query_arg(array('ws_plugin__s2member_css' => '1', 'qcABC' => '1'), $delivery_url) |
| 2991 |
: add_query_arg(array('ws_plugin__s2member_js_w_globals' => (defined('WS_PLUGIN__S2MEMBER_API_CONSTANTS_MD5') ? WS_PLUGIN__S2MEMBER_API_CONSTANTS_MD5 : '1'), 'qcABC' => '1'), $delivery_url); |
| 2992 |
if($failed) |
| 2993 |
{ |
| 2994 |
$status = 'error'; |
| 2995 |
$status_label = 'Failed'; |
| 2996 |
$detail = ($is_fallback) ? 'The Full WordPress Dynamic fallback could not be loaded or confirmed active.' : 'The Full WordPress Dynamic response could not be loaded or confirmed active.'; |
| 2997 |
} |
| 2998 |
else if(!$full_checked) |
| 2999 |
{ |
| 3000 |
$status = 'disabled'; |
| 3001 |
$status_label = 'Not checked yet'; |
| 3002 |
$detail = 'Recheck Asset Health to verify this delivery route.'; |
| 3003 |
} |
| 3004 |
else if(!empty($wp_loader_active[$type])) |
| 3005 |
{ |
| 3006 |
$event = self::asset_runtime_health_event('dynamic_'.$type); |
| 3007 |
$status = $event['status']; |
| 3008 |
$status_label = $event['label']; |
| 3009 |
if(!empty($wp_loader_required[$type])) |
| 3010 |
$detail = 'Full WordPress Dynamic Loader is the active compatibility-required route.'; |
| 3011 |
else |
| 3012 |
$detail = ($is_fallback) ? 'Full WordPress Dynamic Loader is currently serving this asset as fallback.' : 'Full WordPress Dynamic Loader is the configured delivery route.'; |
| 3013 |
if($event['detail']) |
| 3014 |
$detail .= ' '.$event['detail']; |
| 3015 |
} |
| 3016 |
else |
| 3017 |
{ |
| 3018 |
$status = 'healthy'; |
| 3019 |
$status_label = 'Healthy'; |
| 3020 |
$detail = 'Full WordPress Dynamic fallback is available if the preferred delivery route cannot be used.'; |
| 3021 |
} |
| 3022 |
$rows[] = array('label' => 'WP Loader '.$type_label, 'delivery' => ($is_fallback) ? 'Dynamic fallback' : 'Dynamic', 'status' => $status, 'status_label' => $status_label, 'detail' => $detail, 'url' => $delivery_url); |
| 3023 |
} |
| 3024 |
|
| 3025 |
//260912.0258 Only the Health Logkeeper mutates the rolling health log; this view reads the merged state without another read/modify/write race. |
| 3026 |
$state = self::asset_health_log_state(); |
| 3027 |
$scores = self::asset_health_scores($state); |
| 3028 |
$rolling_score = $scores['score']; |
| 3029 |
$current_delivery_result = self::asset_health_current_delivery_result($failures); |
| 3030 |
$standby_fallback_failures = array(); |
| 3031 |
if($full_checked && $current_delivery_result === 'okay') |
| 3032 |
foreach(array('css' => 'CSS', 'js' => 'JS') as $type => $type_label) |
| 3033 |
if(!empty($wp_loader_fallback[$type]) && !empty($failures['fallback:dynamic_'.$type])) |
| 3034 |
{ |
| 3035 |
$standby_fallback_failures[$type] = TRUE; |
| 3036 |
$attention_items['standby-fallback:'.$type] = 'WP Loader '.$type_label.' fallback is unavailable while the preferred '.$type_label.' delivery is still working.'; |
| 3037 |
} |
| 3038 |
//260912.1956 A broken standby fallback can never improve Health: average its failed score with the established rolling score only while preferred delivery itself still works. |
| 3039 |
if($standby_fallback_failures && $rolling_score !== NULL) |
| 3040 |
$scores['score'] = ($rolling_score + 1.0) / 2; |
| 3041 |
$overall = self::asset_health_status_from_score($scores['score'], $scores['latest_result']); |
| 3042 |
$recent_issues = (!empty($http_health['recent_issues']) && is_array($http_health['recent_issues'])) ? $http_health['recent_issues'] : array(); |
| 3043 |
//260910.2350 Recent per-asset details explain a non-Green rolling score even when css-js.log is disabled; clear them only after the overall calculated Health is Green and no trusted/pending problem remains. |
| 3044 |
if($overall === 'healthy' && !$failures && !$runtime_warnings && !self::asset_runtime_suspicions() && $recent_issues) |
| 3045 |
{ |
| 3046 |
unset($http_health['recent_issues']); |
| 3047 |
update_option('ws_plugin__s2member_asset_http_health', $http_health, FALSE); |
| 3048 |
self::$asset_http_health_cache = $http_health; |
| 3049 |
$recent_issues = array(); |
| 3050 |
} |
| 3051 |
$labels = array( |
| 3052 |
'unknown' => 'Not checked yet', |
| 3053 |
'healthy' => 'Healthy', |
| 3054 |
'delayed' => 'Recent issue', |
| 3055 |
'attention' => 'Working, review suggested', |
| 3056 |
'error' => 'Needs attention', |
| 3057 |
); |
| 3058 |
$summaries = array( |
| 3059 |
'unknown' => 'No recent frontend asset-load health is available yet. This panel will run a trusted current-delivery check.', |
| 3060 |
'healthy' => 'Recent frontend CSS/JavaScript asset loads are healthy.', |
| 3061 |
'delayed' => 'Recent asset loads are mixed or include late activation, but they do not currently average into degraded delivery.', |
| 3062 |
'attention' => 'Frontend asset delivery is working, but recent results or an unavailable fallback route suggest that the configuration should be reviewed.', |
| 3063 |
'error' => 'Recent asset loads average into serious delivery failure. s2Member forms, buttons, behavior, or styling may currently be affected.', |
| 3064 |
); |
| 3065 |
|
| 3066 |
$not_green_since = (!empty($state['not_green_since'])) ? (int)$state['not_green_since'] : 0; |
| 3067 |
$fallback_problem_since = ($standby_fallback_failures && !empty($http_health['fallback_problem_since'])) ? (int)$http_health['fallback_problem_since'] : 0; |
| 3068 |
//260912.1956 Keep the existing rolling-health age, but let a continuously unavailable standby fallback start/extend the same non-Healthy review period without creating synthetic page-load events. |
| 3069 |
if($overall !== 'healthy' && $fallback_problem_since > 0 && ($not_green_since <= 0 || $fallback_problem_since < $not_green_since)) |
| 3070 |
$not_green_since = $fallback_problem_since; |
| 3071 |
if($overall === 'healthy') |
| 3072 |
$not_green_since = 0; |
| 3073 |
$six_hour_average = NULL; |
| 3074 |
$notice_level = ''; |
| 3075 |
$notice_items = array(); |
| 3076 |
$notice_signature = ''; |
| 3077 |
|
| 3078 |
if($overall === 'error' && $not_green_since > 0) |
| 3079 |
{ |
| 3080 |
//260910.0709 Red is immediate because the recent score says delivery is failing badly enough to threaten frontend behavior; no persistence delay is added. |
| 3081 |
$notice_level = 'error'; |
| 3082 |
$notice_items = ($error_notice_items) ? $error_notice_items : array('score' => 'Recent CSS/JavaScript asset loads show repeated delivery failures severe enough that frontend s2Member functionality may be affected.'); |
| 3083 |
$notice_signature = 'error:'.$not_green_since; |
| 3084 |
} |
| 3085 |
else if($not_green_since > 0 && $not_green_since <= time() - 6 * HOUR_IN_SECONDS) |
| 3086 |
{ |
| 3087 |
//260912.1956 Reuse the established six-hour review logic; when the standby fallback itself has stayed unavailable for the full period, average its failed score into the retained delivery history just as Current Health does. |
| 3088 |
$six_hour_average = self::asset_health_six_hour_average($state); |
| 3089 |
if($standby_fallback_failures && $fallback_problem_since > 0 && $fallback_problem_since <= time() - 6 * HOUR_IN_SECONDS) |
| 3090 |
{ |
| 3091 |
if($six_hour_average === NULL) |
| 3092 |
$six_hour_average = $rolling_score; |
| 3093 |
if($six_hour_average !== NULL) |
| 3094 |
$six_hour_average = ($six_hour_average + 1.0) / 2; |
| 3095 |
} |
| 3096 |
if($six_hour_average !== NULL && $six_hour_average <= 2.5) |
| 3097 |
{ |
| 3098 |
$notice_level = 'attention'; |
| 3099 |
//260911.1705 Keep admin-facing health wording understandable without requiring familiarity with the internal Green/Yellow/Orange/Red state model. |
| 3100 |
$notice_items = ($attention_items) ? $attention_items : array('score' => 'CSS/JavaScript asset-load health has remained substantially degraded across the latest six hours without returning to normal.'); |
| 3101 |
$notice_signature = 'attention:'.$not_green_since; |
| 3102 |
} |
| 3103 |
} |
| 3104 |
|
| 3105 |
//260910.0709 Dismissal is scoped to severity + one continuous non-Green period; changing Orange-review severity to Red surfaces again, while Green clears the old dismissal before another period can begin. |
| 3106 |
$dismissed = (string)get_option('ws_plugin__s2member_asset_notice_dismissed', ''); |
| 3107 |
$active_signatures = array_values(array_filter(array(($not_green_since > 0) ? 'error:'.$not_green_since : '', ($not_green_since > 0) ? 'attention:'.$not_green_since : ''))); |
| 3108 |
if($dismissed !== '' && !in_array($dismissed, $active_signatures, TRUE)) |
| 3109 |
delete_option('ws_plugin__s2member_asset_notice_dismissed'); |
| 3110 |
|
| 3111 |
return array( |
| 3112 |
'status' => $overall, |
| 3113 |
'status_label' => $labels[$overall], |
| 3114 |
'summary' => $summaries[$overall], |
| 3115 |
'rows' => $rows, |
| 3116 |
'score' => $scores['score'], |
| 3117 |
'rolling_score' => $rolling_score, |
| 3118 |
'standby_fallback_failures' => array_keys($standby_fallback_failures), |
| 3119 |
'request_score' => $scores['request_score'], |
| 3120 |
'time_score' => $scores['time_score'], |
| 3121 |
'request_count' => $scores['request_count'], |
| 3122 |
'time_count' => $scores['time_count'], |
| 3123 |
'recent_issues' => $recent_issues, |
| 3124 |
'latest_issues' => (!empty($state['latest_issues']) && is_array($state['latest_issues'])) ? $state['latest_issues'] : array(), |
| 3125 |
'six_hour_average' => $six_hour_average, |
| 3126 |
'not_green_since' => $not_green_since, |
| 3127 |
'notice_level' => $notice_level, |
| 3128 |
'notice_items' => $notice_items, |
| 3129 |
'notice_signature' => $notice_signature, |
| 3130 |
); |
| 3131 |
} |
| 3132 |
|
| 3133 |
/** |
| 3134 |
* Dismisses the current frontend-asset notice for the current continuous non-Green health period. |
| 3135 |
* |
| 3136 |
* @package s2Member\Utilities |
| 3137 |
* @since 260909.2021 |
| 3138 |
* |
| 3139 |
* @attaches-to ``add_action('admin_init');`` |
| 3140 |
* @return null |
| 3141 |
*/ |
| 3142 |
public static function dismiss_static_assets_admin_notice() |
| 3143 |
{ |
| 3144 |
if(!is_admin() || !current_user_can('create_users') || empty($_GET['s2member-dismiss-asset-health-notice'])) |
| 3145 |
return; |
| 3146 |
|
| 3147 |
check_admin_referer('s2member-dismiss-asset-health-notice'); |
| 3148 |
$health = self::frontend_asset_health(TRUE); |
| 3149 |
if(!empty($health['notice_signature'])) |
| 3150 |
update_option('ws_plugin__s2member_asset_notice_dismissed', (string)$health['notice_signature'], FALSE); |
| 3151 |
|
| 3152 |
wp_safe_redirect(wp_get_referer() ? wp_get_referer() : admin_url()); |
| 3153 |
exit; |
| 3154 |
} |
| 3155 |
|
| 3156 |
/** |
| 3157 |
* Displays one non-Green-period-scoped admin-wide notice for red failures or persistent degraded health. |
| 3158 |
* |
| 3159 |
* Missing or browser-confirmed unreachable frontend assets remain part of the Red diagnosis when usable delivery/fallback also fails; preferred-route failures with a working fallback are not treated as Red. |
| 3160 |
* Red means the recent weighted asset loads average into serious failure and is immediate. |
| 3161 |
* Orange remains non-alarming unless six hours have passed without Green and the equal-block |
| 3162 |
* rolling six-hour average remains below Yellow territory. Current Yellow may therefore still |
| 3163 |
* surface the calm review notice when the longer recent history remains substantially degraded. |
| 3164 |
* |
| 3165 |
* @package s2Member\Utilities |
| 3166 |
* @since 260903.0612 |
| 3167 |
* |
| 3168 |
* @attaches-to ``add_action('admin_notices');`` |
| 3169 |
* @return null |
| 3170 |
*/ |
| 3171 |
public static function static_assets_admin_notice() |
| 3172 |
{ |
| 3173 |
if(!current_user_can('create_users') || (defined('DOING_AJAX') && DOING_AJAX)) |
| 3174 |
return; |
| 3175 |
|
| 3176 |
$health = self::frontend_asset_health(); |
| 3177 |
if(empty($health['notice_items']) || empty($health['notice_signature']) || empty($health['notice_level'])) |
| 3178 |
return; |
| 3179 |
$dismissed = (string)get_option('ws_plugin__s2member_asset_notice_dismissed', ''); |
| 3180 |
if($dismissed === (string)$health['notice_signature']) |
| 3181 |
return; |
| 3182 |
//260911.0012 A dismissed Red problem also suppresses the later calmer Orange review notice in the same non-Green period. A dismissed Orange notice never suppresses a later Red escalation. |
| 3183 |
if($health['notice_level'] !== 'error' && !empty($health['not_green_since']) && $dismissed === 'error:'.(int)$health['not_green_since']) |
| 3184 |
return; |
| 3185 |
|
| 3186 |
//260911.1707 Give the specific asset-health reason a compact, visually distinct line without requiring familiarity with the internal health-state colors. |
| 3187 |
$items = array(); |
| 3188 |
foreach($health['notice_items'] as $item) |
| 3189 |
$items[] = '• <strong><em>'.$item.'</em></strong>'; |
| 3190 |
$settings_url = add_query_arg('s2member-open-panel', 'frontend-static-assets', admin_url('/admin.php?page=ws-plugin--s2member-gen-ops')).'#ws-plugin--s2member-asset-health'; |
| 3191 |
$dismiss_url = wp_nonce_url(add_query_arg('s2member-dismiss-asset-health-notice', '1', admin_url()), 's2member-dismiss-asset-health-notice'); |
| 3192 |
|
| 3193 |
$_notice_items = '<span style="display:block; margin:.4em 0 .45em .65em;">'.implode('<br />', $items).'</span>'; |
| 3194 |
if($health['notice_level'] === 'error') |
| 3195 |
{ |
| 3196 |
$message = 'Recent frontend CSS/JavaScript asset loads average into serious delivery failure. This can affect s2Member forms, buttons, behavior, or styling.'.$_notice_items.'<a href="'.esc_url($settings_url).'">Open CSS/JS Asset Health</a> for the current delivery details and troubleshooting.'; |
| 3197 |
c_ws_plugin__s2member_admin_notices::display_branded_notice('s2Member CSS/JS Asset Delivery Problem', $message, TRUE, $dismiss_url); |
| 3198 |
} |
| 3199 |
else |
| 3200 |
{ |
| 3201 |
$message = 'Frontend CSS/JavaScript asset-load health has remained substantially degraded across the latest six hours without returning to normal. Delivery may currently be improving or may still be working through fallback. This is a suggestion to review the configuration, not an emergency.'.$_notice_items.'<a href="'.esc_url($settings_url).'">Open CSS/JS Asset Health</a> to review the current delivery details.'; |
| 3202 |
c_ws_plugin__s2member_admin_notices::display_branded_notice('s2Member CSS/JS Asset Health: Review Suggested', $message, FALSE, $dismiss_url); |
| 3203 |
} |
| 3204 |
unset($_notice_items); |
| 3205 |
return; |
| 3206 |
} |
| 3207 |
|
| 3208 |
/** |
| 3209 |
* Prints an infrequent trusted browser-side reachability probe for active frontend assets. |
| 3210 |
* |
| 3211 |
* Healthy static files use HEAD. |
| 3212 |
* The s2Member-Only Dynamic Loader (s2member-o.php) uses its tiny pre-WordPress health mode. |
| 3213 |
* A frontend runtime suspicion forces one full cache-busted activation check for that exact URL. |
| 3214 |
* |
| 3215 |
* @package s2Member\Utilities |
| 3216 |
* @since 260904.2110 |
| 3217 |
* |
| 3218 |
* @attaches-to ``add_action('admin_footer');`` |
| 3219 |
* @attaches-to ``add_action('wp_footer');`` |
| 3220 |
* @return null |
| 3221 |
*/ |
| 3222 |
public static function asset_http_health_probe() |
| 3223 |
{ |
| 3224 |
if(!current_user_can('create_users') || (defined('DOING_AJAX') && DOING_AJAX)) |
| 3225 |
return; |
| 3226 |
|
| 3227 |
$health_log = self::asset_health_log_state(); |
| 3228 |
$scores = self::asset_health_scores($health_log); |
| 3229 |
$health = self::asset_http_health_state(); |
| 3230 |
$has_failures = is_array($health) && !empty($health['failures']); |
| 3231 |
$has_suspicions = (bool)self::asset_runtime_suspicions(); |
| 3232 |
$current_status = (!empty($health_log['status'])) ? (string)$health_log['status'] : 'unknown'; |
| 3233 |
$full = self::$asset_health_force_full_probe || !$scores['time_count'] || $has_failures || $has_suspicions || !in_array($current_status, array('unknown', 'healthy'), TRUE); |
| 3234 |
$targets = self::asset_http_health_targets($full); |
| 3235 |
if(!$targets) |
| 3236 |
return; |
| 3237 |
|
| 3238 |
$target_hash = self::asset_http_health_target_hash($targets); |
| 3239 |
$interval = ($full || $has_failures || $has_suspicions) ? MINUTE_IN_SECONDS : 10 * MINUTE_IN_SECONDS; |
| 3240 |
$auto_due = $has_suspicions || !is_array($health) || empty($health['checked']) || empty($health['target_hash']) || (string)$health['target_hash'] !== $target_hash || (int)$health['checked'] < time() - $interval; |
| 3241 |
if(!$auto_due && !self::$asset_health_force_full_probe) |
| 3242 |
return; |
| 3243 |
|
| 3244 |
$config = array( |
| 3245 |
'targets' => array_values($targets), |
| 3246 |
'target_hash' => $target_hash, |
| 3247 |
'ajax_url' => admin_url('admin-ajax.php'), |
| 3248 |
'nonce' => wp_create_nonce('ws-plugin--s2member-asset-http-health'), |
| 3249 |
'reload_on_change' => is_admin(), |
| 3250 |
'full' => (bool)$full, |
| 3251 |
'auto_run' => (bool)$auto_due, |
| 3252 |
); |
| 3253 |
|
| 3254 |
//260910.0818 The same trusted probe can run automatically for stale/no-current evidence and on demand from the Health panel; only an explicit recheck is allowed to reset active score history after a clean Okay result. |
| 3255 |
echo '<script type="text/javascript">(function(c){if(!window.fetch||!window.URL||!window.Promise)return;var b=document.getElementById("ws-plugin--s2member-recheck-asset-health"),s=document.getElementById("ws-plugin--s2member-recheck-asset-health-status"),busy=false;function u(t,i){var x=new URL(t.probe_url,window.location.href),n=Date.now().toString(36)+"-"+i+"-"+Math.random().toString(36).slice(2);x.searchParams.set("s2member_asset_health",n);if(t.mode==="s2o-health")x.searchParams.set("s2member_health_token",n);return{x:x.toString(),n:n}}function ct(r,t){var v=(r.headers.get("content-type")||"").toLowerCase();if(t.type==="css")return v.indexOf("text/css")!==-1;if(t.type==="js")return /(javascript|ecmascript)/.test(v);return v.indexOf("text/plain")!==-1}function f(t,m,i,body){var z=u(t,i);return fetch(z.x,{method:m,cache:"no-store",credentials:"same-origin",headers:{"Cache-Control":"no-cache, no-store, max-age=0","Pragma":"no-cache"}}).then(function(r){var h=r.headers.get("x-s2member-health-token")||"",tm=r.headers.get("x-s2member-health-time")||"";if(!body)return{ok:r.ok&&ct(r,t),status:r.status,content_type:r.headers.get("content-type")||"",text:"",token:z.n,health_token:h,health_time:tm};return r.text().then(function(x){return{ok:r.ok&&ct(r,t),status:r.status,content_type:r.headers.get("content-type")||"",text:x,token:z.n,health_token:h,health_time:tm}})}).catch(function(){return{ok:false,status:0,content_type:"",text:"",token:z.n,health_token:"",health_time:""}})}function p(t,i){if(t.mode==="s2o-health")return f(t,"GET",i,true).then(function(r){r.ok=r.ok&&r.health_token===r.token&&r.health_time!==""&&r.text.indexOf("s2member-o-health:"+r.token+":"+r.health_time)===0;return{id:t.id,ok:r.ok,status:r.status,content_type:r.content_type,detail:r.ok?"":"Dynamic Loader health response could not be verified."}});if(t.mode==="activation-tag")return f(t,"GET",i,true).then(function(r){if(r.ok&&t.activation_tags)for(var j=0;j<t.activation_tags.length;j++)if(r.text.indexOf(t.activation_tags[j])===-1){r.ok=false;break}return{id:t.id,ok:r.ok,status:r.status,content_type:r.content_type,detail:r.ok?"":"Expected asset could not be verified as active."}});return f(t,"HEAD",i,false).then(function(r){if(r.ok)return{id:t.id,ok:true,status:r.status,content_type:r.content_type,detail:""};return f(t,"GET",i+"g",false).then(function(g){return{id:t.id,ok:g.ok,status:g.status,content_type:g.content_type,detail:g.ok?"":"Public URL check failed"}})})}function run(reset,retried){if(busy)return;busy=true;if(reset&&b)b.disabled=true;if(reset&&s)s.textContent="Checking current asset delivery...";Promise.all(c.targets.map(p)).then(function(results){var body="action="+encodeURIComponent("ws_plugin__s2member_asset_http_health")+"&_ajax_nonce="+encodeURIComponent(c.nonce)+"&target_hash="+encodeURIComponent(c.target_hash)+"&full="+(c.full?"1":"0")+"&reset_health="+(reset?"1":"0")+"&results="+encodeURIComponent(JSON.stringify(results));return fetch(c.ajax_url,{method:"POST",cache:"no-store",credentials:"same-origin",headers:{"Content-Type":"application/x-www-form-urlencoded;charset=UTF-8","Cache-Control":"no-cache, no-store, max-age=0","Pragma":"no-cache"},body:body})}).then(function(r){return r.json()}).then(function(j){busy=false;if(j&&j.success&&j.data&&j.data.stale){if(!retried&&j.data.targets&&j.data.target_hash){c.targets=j.data.targets;c.target_hash=j.data.target_hash;if(reset&&s)s.textContent=j.data.recheck_message||"Delivery changed. Rechecking...";return run(reset,true)}if(reset&&s)s.textContent="Delivery changed again. Refreshing...";window.location.reload();return}if(reset&&s)s.textContent=(j&&j.success&&j.data&&j.data.recheck_message)?j.data.recheck_message:"Check complete.";if(reset&&b)b.disabled=false;if((reset||c.reload_on_change)&&j&&j.success&&j.data&&j.data.reload)window.location.reload()}).catch(function(){busy=false;if(reset&&b)b.disabled=false;if(reset&&s)s.textContent="The check could not be completed."})}if(b)b.addEventListener("click",function(){run(true,false)},false);if(c.auto_run)run(false,false)})('.wp_json_encode($config).');</script>' . "\n"; |
| 3256 |
return; |
| 3257 |
} |
| 3258 |
|
| 3259 |
/** |
| 3260 |
* Stores trusted administrator-browser asset probe results. |
| 3261 |
* |
| 3262 |
* @package s2Member\Utilities |
| 3263 |
* @since 260904.2110 |
| 3264 |
* |
| 3265 |
* @return null Exits through WordPress JSON helpers. |
| 3266 |
*/ |
| 3267 |
public static function ajax_asset_http_health_report() |
| 3268 |
{ |
| 3269 |
check_ajax_referer('ws-plugin--s2member-asset-http-health'); |
| 3270 |
if(!current_user_can('create_users')) |
| 3271 |
wp_send_json_error(array('message' => 'You do not have permission to report s2Member asset health.'), 403); |
| 3272 |
|
| 3273 |
$full = !empty($_POST['full']); |
| 3274 |
$reset_health = $full && !empty($_POST['reset_health']); |
| 3275 |
$targets = self::asset_http_health_targets($full); |
| 3276 |
$target_hash = self::asset_http_health_target_hash($targets); |
| 3277 |
if(empty($_POST['target_hash']) || (string)wp_unslash($_POST['target_hash']) !== $target_hash) |
| 3278 |
wp_send_json_success(array('stale' => TRUE, 'reload' => FALSE, 'targets' => array_values($targets), 'target_hash' => $target_hash, 'recheck_message' => 'Delivery targets changed. Rechecking current routes...')); |
| 3279 |
|
| 3280 |
$results = (!empty($_POST['results'])) ? json_decode(wp_unslash($_POST['results']), TRUE) : array(); |
| 3281 |
$by_id = array(); |
| 3282 |
if(is_array($results)) |
| 3283 |
foreach($results as $result) |
| 3284 |
if(is_array($result) && !empty($result['id'])) |
| 3285 |
$by_id[(string)$result['id']] = $result; |
| 3286 |
|
| 3287 |
$old = self::asset_http_health_state(); |
| 3288 |
$old_failures = (is_array($old) && !empty($old['failures']) && is_array($old['failures'])) ? $old['failures'] : array(); |
| 3289 |
$runtime_warnings = (is_array($old) && !empty($old['runtime_warnings']) && is_array($old['runtime_warnings'])) ? $old['runtime_warnings'] : array(); |
| 3290 |
$recent_issues = (is_array($old) && !empty($old['recent_issues']) && is_array($old['recent_issues'])) ? $old['recent_issues'] : array(); //260910.2346 Preserve compact per-asset troubleshooting context across probes until the calculated overall Health returns Green. |
| 3291 |
foreach($runtime_warnings as $key => $warning) |
| 3292 |
if(empty($warning['reported']) || (int)$warning['reported'] < time() - HOUR_IN_SECONDS) |
| 3293 |
unset($runtime_warnings[$key]); |
| 3294 |
$old_runtime_warnings = $runtime_warnings; //260907.2203 Preserve prior warning state so only new trusted transitions are logged. |
| 3295 |
$old_health_log = self::asset_health_log_state(); |
| 3296 |
$old_health_status = (!empty($old_health_log['status'])) ? (string)$old_health_log['status'] : 'unknown'; |
| 3297 |
|
| 3298 |
$failures = array(); |
| 3299 |
$failure_contexts = array(); |
| 3300 |
$suspicions = self::asset_runtime_suspicions(); |
| 3301 |
|
| 3302 |
foreach($targets as $id => $target) |
| 3303 |
{ |
| 3304 |
$result = (isset($by_id[$id]) && is_array($by_id[$id])) ? $by_id[$id] : array(); |
| 3305 |
if(empty($result['ok'])) |
| 3306 |
{ |
| 3307 |
$failure_id = (!empty($target['failure_id'])) ? (string)$target['failure_id'] : $id; |
| 3308 |
$failures[$failure_id] = array( |
| 3309 |
'url' => (!empty($target['failure_url'])) ? (string)$target['failure_url'] : (string)$target['url'], |
| 3310 |
'label' => (string)$target['label'], |
| 3311 |
'status' => (!empty($result['status'])) ? (int)$result['status'] : 0, |
| 3312 |
'content_type' => (!empty($result['content_type'])) ? substr(sanitize_text_field((string)$result['content_type']), 0, 100) : '', |
| 3313 |
'detail' => (!empty($result['detail'])) ? substr(sanitize_text_field((string)$result['detail']), 0, 160) : '', |
| 3314 |
); |
| 3315 |
$recent_issues = self::add_asset_health_recent_issue($recent_issues, 'failure-'.$failure_id, 'failed', (string)$target['label'], (!empty($failures[$failure_id]['detail'])) ? (string)$failures[$failure_id]['detail'] : 'Trusted browser check failed.', (string)$failures[$failure_id]['url']); |
| 3316 |
if(!empty($target['suspicion']) && is_array($target['suspicion'])) |
| 3317 |
$failure_contexts[$failure_id] = array( |
| 3318 |
'event_time' => (!empty($target['suspicion']['event_time'])) ? (int)$target['suspicion']['event_time'] : time(), |
| 3319 |
'page_id' => (!empty($target['suspicion']['page_id'])) ? (int)$target['suspicion']['page_id'] : 0, |
| 3320 |
'page_path' => (!empty($target['suspicion']['page_path'])) ? (string)$target['suspicion']['page_path'] : '', |
| 3321 |
); |
| 3322 |
if(strpos($failure_id, 'fallback:') !== 0 && (!isset($old_failures[$failure_id]) || serialize($old_failures[$failure_id]) !== serialize($failures[$failure_id]))) |
| 3323 |
{ |
| 3324 |
$_failure_suspicion = (!empty($target['suspicion']) && is_array($target['suspicion'])) ? $target['suspicion'] : array(); |
| 3325 |
$_failure_page = array('page_id' => (!empty($_failure_suspicion['page_id'])) ? (int)$_failure_suspicion['page_id'] : 0, 'page_path' => (!empty($_failure_suspicion['page_path'])) ? (string)$_failure_suspicion['page_path'] : ''); |
| 3326 |
$_failure_event_time = (!empty($_failure_suspicion['event_time'])) ? (int)$_failure_suspicion['event_time'] : time(); |
| 3327 |
self::queue_asset_health_issue_snapshot('failed', (string)$target['label'], (!empty($failures[$failure_id]['detail'])) ? (string)$failures[$failure_id]['detail'] : 'Trusted browser check failed.', array('asset' => $failure_id, 'delivery' => (!empty($_failure_suspicion['delivery'])) ? (string)$_failure_suspicion['delivery'] : ''), $_failure_page, $_failure_event_time); |
| 3328 |
} |
| 3329 |
} |
| 3330 |
else if(!empty($target['suspicion_key']) && !empty($target['suspicion'])) |
| 3331 |
{ |
| 3332 |
$key = (string)$target['suspicion_key']; |
| 3333 |
$previous_warning = (isset($runtime_warnings[$key]) && is_array($runtime_warnings[$key])) ? $runtime_warnings[$key] : array(); |
| 3334 |
$previous_count = (!empty($previous_warning['count'])) ? max(1, (int)$previous_warning['count']) : (($previous_warning) ? 1 : 0); |
| 3335 |
$runtime_warnings[$key] = array( |
| 3336 |
'id' => (string)$target['suspicion']['id'], |
| 3337 |
'url' => (string)$target['suspicion']['url'], |
| 3338 |
'delivery' => (string)$target['suspicion']['delivery'], |
| 3339 |
'event_time' => (!empty($target['suspicion']['event_time'])) ? (int)$target['suspicion']['event_time'] : time(), |
| 3340 |
'page_id' => (!empty($target['suspicion']['page_id'])) ? (int)$target['suspicion']['page_id'] : 0, |
| 3341 |
'page_path' => (!empty($target['suspicion']['page_path'])) ? (string)$target['suspicion']['page_path'] : '', |
| 3342 |
'first_reported' => (!empty($previous_warning['first_reported'])) ? (int)$previous_warning['first_reported'] : ((!empty($previous_warning['reported'])) ? (int)$previous_warning['reported'] : time()), |
| 3343 |
'reported' => time(), |
| 3344 |
'count' => $previous_count + 1, |
| 3345 |
); |
| 3346 |
$recent_issues = self::add_asset_health_recent_issue($recent_issues, 'late-'.(string)$target['suspicion']['id'], 'late', self::asset_runtime_health_label((string)$target['suspicion']['id']), 'A frontend page could not confirm this asset within the configured wait time, but the trusted follow-up check verified the expected asset response.', (string)$target['suspicion']['url'], (string)$target['suspicion']['delivery']); |
| 3347 |
} |
| 3348 |
if(!empty($target['suspicion_key'])) |
| 3349 |
unset($suspicions[(string)$target['suspicion_key']]); |
| 3350 |
} |
| 3351 |
|
| 3352 |
update_option('ws_plugin__s2member_asset_runtime_suspicions', $suspicions, FALSE); |
| 3353 |
|
| 3354 |
//260907.2203 Keep an operational history of newly confirmed failures, recoveries, and runtime warnings. |
| 3355 |
//260910.0818 Routine Okay/Fallback/Late/Failed asset loads stay only in the fixed-size non-autoloaded health log so operational history is not flooded by normal frontend traffic. |
| 3356 |
foreach($failures as $id => $failure) |
| 3357 |
if(!isset($old_failures[$id]) || serialize($old_failures[$id]) !== serialize($failure)) |
| 3358 |
{ |
| 3359 |
$_failure_log_context = (!empty($failure_contexts[$id]) && is_array($failure_contexts[$id])) ? $failure_contexts[$id] : array(); |
| 3360 |
c_ws_plugin__s2member_utils_logs::log_entry('css-js', array_merge(array( |
| 3361 |
'event' => 'CSS/JS delivery health failure', 'result' => 'failure', 'target' => $id, 'details' => $failure, |
| 3362 |
'fallback' => ($id === 's2o' || strpos((string)$id, 'static:') === 0) ? 'Full WordPress Dynamic Loader' : 'none', |
| 3363 |
), $_failure_log_context)); |
| 3364 |
} |
| 3365 |
foreach($old_failures as $id => $failure) |
| 3366 |
if(!isset($failures[$id])) |
| 3367 |
c_ws_plugin__s2member_utils_logs::log_entry('css-js', array('event' => 'CSS/JS delivery health recovered', 'result' => 'recovered', 'target' => $id, 'previous_details' => $failure)); |
| 3368 |
foreach($runtime_warnings as $key => $warning) |
| 3369 |
if(!isset($old_runtime_warnings[$key])) |
| 3370 |
c_ws_plugin__s2member_utils_logs::log_entry('css-js', array('event' => 'CSS/JS runtime warning confirmed', 'result' => 'warning', 'details' => $warning, 'delivery_changed' => FALSE)); |
| 3371 |
|
| 3372 |
$full_checked = ($full) ? time() : ((!empty($old['full_checked'])) ? (int)$old['full_checked'] : 0); |
| 3373 |
$fallback_problem = !empty($failures['fallback:dynamic_css']) || !empty($failures['fallback:dynamic_js']); |
| 3374 |
$fallback_problem_since = 0; |
| 3375 |
if($full && $fallback_problem) |
| 3376 |
$fallback_problem_since = (!empty($old['fallback_problem_since'])) ? (int)$old['fallback_problem_since'] : time(); |
| 3377 |
else if(!$full && !empty($old['fallback_problem_since'])) |
| 3378 |
$fallback_problem_since = (int)$old['fallback_problem_since']; |
| 3379 |
//260912.1956 Preserve when the complete route set was last checked, and how long a WordPress fallback has stayed unavailable, without turning standby-route health into extra page-load records. |
| 3380 |
$new_health = array('checked' => time(), 'full_checked' => $full_checked, 'fallback_problem_since' => $fallback_problem_since, 'target_hash' => $target_hash, 'failures' => $failures, 'runtime_warnings' => $runtime_warnings, 'recent_issues' => $recent_issues); |
| 3381 |
update_option('ws_plugin__s2member_asset_http_health', $new_health, FALSE); |
| 3382 |
self::$asset_http_health_cache = $new_health; |
| 3383 |
|
| 3384 |
$load_result = ''; |
| 3385 |
$recheck_message = ''; |
| 3386 |
$new_health_status = $old_health_status; |
| 3387 |
if($full) |
| 3388 |
{ |
| 3389 |
//260910.0818 A full trusted probe contributes one Okay/Fallback/Failed asset load through the same scoring path as frontend evidence; only an explicit successful admin recheck may start a clean epoch. |
| 3390 |
$load_result = self::asset_health_current_delivery_result($failures); |
| 3391 |
$standby_fallback_failures = ($load_result === 'okay') ? array_intersect_key($failures, array('fallback:dynamic_css' => TRUE, 'fallback:dynamic_js' => TRUE)) : array(); |
| 3392 |
$reset_on_ok = $reset_health && $load_result === 'okay' && !$standby_fallback_failures; |
| 3393 |
//260913.0102 Trusted failure transitions are queued separately as issue snapshots, so this scored recheck does not duplicate them in Latest Issues. |
| 3394 |
self::queue_asset_health_load($load_result, $reset_on_ok); |
| 3395 |
//260912.1956 A standby fallback outage is historical issue context, not an extra page-load; queue it once on the failure transition while Current Health applies the separate safety-net score. |
| 3396 |
foreach($standby_fallback_failures as $failure_id => $failure) |
| 3397 |
if(empty($old_failures[$failure_id])) |
| 3398 |
{ |
| 3399 |
$type_label = (substr($failure_id, -3) === '_js') ? 'JS' : 'CSS'; |
| 3400 |
self::queue_asset_health_issue_snapshot('fallback-unavailable', 'WP Loader '.$type_label, 'The Full WordPress Dynamic fallback could not be loaded or confirmed active while preferred '.$type_label.' delivery was still working.'); |
| 3401 |
} |
| 3402 |
//260912.0258 Trusted administrator probes run the Health Logkeeper immediately so their response reflects the event just queued; frontend pages remain queue-only. |
| 3403 |
self::run_health_logkeeper(); |
| 3404 |
$new_health_log = self::asset_health_log_state(); |
| 3405 |
$new_health_status = (!empty($new_health_log['status'])) ? (string)$new_health_log['status'] : 'unknown'; |
| 3406 |
if($reset_health) |
| 3407 |
$recheck_message = ($reset_on_ok) ? 'Current delivery and its fallback are healthy. Recent scoring history was reset.' : (($standby_fallback_failures) ? 'Current delivery is working, but a fallback route is unavailable. Recent health history was kept.' : (($load_result === 'fallback') ? 'Current delivery is working through fallback. Recent health history was kept.' : 'Current delivery still has a failure. Recent health history was kept.')); |
| 3408 |
} |
| 3409 |
|
| 3410 |
$health_changed = serialize($old_failures) !== serialize($failures) || serialize($old_runtime_warnings) !== serialize($runtime_warnings) || $old_health_status !== $new_health_status; |
| 3411 |
wp_send_json_success(array( |
| 3412 |
'failures' => count($failures), |
| 3413 |
'runtime_warnings' => count($runtime_warnings), |
| 3414 |
'load_result' => $load_result, |
| 3415 |
'recheck_message' => $recheck_message, |
| 3416 |
'reload' => $health_changed || $reset_health, |
| 3417 |
)); |
| 3418 |
} |
| 3419 |
|
| 3420 |
/** |
| 3421 |
* Prints the real-page asset activation monitor. |
| 3422 |
* |
| 3423 |
* A short configurable wait after window.load avoids racing normal delivery; healthy pages make no runtime-suspicion request. |
| 3424 |
* An asset that cannot be confirmed active after that wait is a low-trust timing suspicion only. |
| 3425 |
* The real page reports it for a later trusted browser check; unlike the first v260909 monitor, a recoverable miss no longer injects a WordPress fallback request merely because an optimizer may have delayed execution. |
| 3426 |
* |
| 3427 |
* @package s2Member\Utilities |
| 3428 |
* @since 260904.2255 |
| 3429 |
* |
| 3430 |
* @attaches-to ``add_action('wp_footer');`` |
| 3431 |
* @return null |
| 3432 |
*/ |
| 3433 |
public static function page_asset_runtime_monitor() |
| 3434 |
{ |
| 3435 |
if(is_admin() || !self::$page_asset_expectations) |
| 3436 |
return; |
| 3437 |
|
| 3438 |
//260912.0304 Queue one page-level asset load now, preserving compact fallback context before healthy traffic can push it out of the rolling score window. |
| 3439 |
$page_result = self::page_asset_health_load_result(); |
| 3440 |
$load_record = self::queue_asset_health_load($page_result, FALSE, array(), self::page_asset_health_issue($page_result)); |
| 3441 |
$load = (!empty($load_record['load']) && is_array($load_record['load'])) ? $load_record['load'] : array(); |
| 3442 |
|
| 3443 |
$expectations = array(); |
| 3444 |
foreach(self::$page_asset_expectations as $expectation) |
| 3445 |
$expectations[] = array( |
| 3446 |
(string)$expectation['id'], |
| 3447 |
(string)$expectation['asset_id'], |
| 3448 |
(string)$expectation['url'], |
| 3449 |
(string)$expectation['delivery'], |
| 3450 |
(string)$expectation['tag_value'], |
| 3451 |
(string)$expectation['signature'], |
| 3452 |
); |
| 3453 |
//260912.0522 The wait setting changes only when an asset becomes a Late suspicion; it never delays loading and does not trigger speculative fallback injection. |
| 3454 |
$wait_seconds = (!empty($GLOBALS['WS_PLUGIN__']['s2member']['o']['asset_health_wait_seconds'])) ? (int)$GLOBALS['WS_PLUGIN__']['s2member']['o']['asset_health_wait_seconds'] : 3; |
| 3455 |
$wait_seconds = max(1, min(60, $wait_seconds)); |
| 3456 |
$config = array('ajax_url' => admin_url('admin-ajax.php'), 'expectations' => $expectations, 'delay' => $wait_seconds * 1000, 'load' => $load); |
| 3457 |
|
| 3458 |
//260912.0522 A Late activation is diagnostic evidence, not proof that loading failed; report it without racing an optimizer with a second CSS/JS response. |
| 3459 |
echo '<script type="text/javascript" id="ws-plugin--s2member-asset-runtime-monitor">(function(c){function t(e){return /_js$/.test(e[0])?"js":"css"}function n(e){var i="ws-plugin--s2member-asset-health-"+e[0].replace(/_/g,"-"),o=document.getElementById(i);if(!o){o=document.createElement("span");o.id=i;o.style.cssText="position:absolute;left:-99999px;top:-99999px;width:1px;height:1px;visibility:hidden";(document.body||document.documentElement).appendChild(o)}return o}function ok(e){if(t(e)==="js")return !!(window.ws_plugin__s2member_asset_health&&window.ws_plugin__s2member_asset_health[e[0]]===e[4]);return !window.getComputedStyle||String(getComputedStyle(n(e)).zIndex)===e[4]}function report(m){if(!window.fetch||!m.length)return;fetch(c.ajax_url,{method:"POST",cache:"no-store",credentials:"same-origin",keepalive:true,headers:{"Content-Type":"application/x-www-form-urlencoded;charset=UTF-8"},body:"action=ws_plugin__s2member_asset_runtime_suspect&missing="+encodeURIComponent(JSON.stringify(m))+"&load="+encodeURIComponent(JSON.stringify(c.load||{}))}).catch(function(){})}function check(){var m=c.expectations.filter(function(e){return !ok(e)});if(m.length)report(m)}c.expectations.filter(function(e){return t(e)==="css"}).forEach(n);function go(){setTimeout(check,c.delay)}document.readyState==="complete"?go():addEventListener("load",go,false)})('.wp_json_encode($config).');</script>' . "\n"; |
| 3460 |
return; |
| 3461 |
} |
| 3462 |
|
| 3463 |
/** |
| 3464 |
* Records signed low-trust frontend runtime suspicions without changing delivery state. |
| 3465 |
* |
| 3466 |
* Reports are rate-limited and only force a later trusted administrator-browser confirmation. |
| 3467 |
* The standalone AJAX reporter is now the normal path; the recovery-query validator remains for compatibility with already-cached pages from the first v260909 monitor. |
| 3468 |
* |
| 3469 |
* @package s2Member\Utilities |
| 3470 |
* @since 260905.0009 |
| 3471 |
* |
| 3472 |
* @param array $missing Missing runtime expectations. |
| 3473 |
* @param array $load Optional signed metadata for the page asset load being corrected to Late. |
| 3474 |
* @return int Number of newly recorded suspicions. |
| 3475 |
*/ |
| 3476 |
protected static function record_asset_runtime_suspicions($missing = array(), $load = array()) |
| 3477 |
{ |
| 3478 |
if(!is_array($missing) || !$missing) |
| 3479 |
return 0; |
| 3480 |
$missing = array_slice($missing, 0, 4); |
| 3481 |
$suspicions = self::asset_runtime_suspicions(); |
| 3482 |
$recorded = 0; |
| 3483 |
$valid_late_page = FALSE; |
| 3484 |
$late_issues = array(); |
| 3485 |
$page_context = self::verified_asset_health_page_context($load); |
| 3486 |
$event_time = (!empty($page_context['page_id']) || !empty($page_context['page_path'])) && !empty($load['load_time']) ? (int)$load['load_time'] : time(); |
| 3487 |
foreach($missing as $expectation) |
| 3488 |
{ |
| 3489 |
if(is_array($expectation) && isset($expectation[0]) && !isset($expectation['id'])) |
| 3490 |
$expectation = self::expand_asset_runtime_expectation($expectation); |
| 3491 |
if(!is_array($expectation) || empty($expectation['signature'])) |
| 3492 |
continue; |
| 3493 |
$signature = (string)$expectation['signature']; |
| 3494 |
unset($expectation['signature']); |
| 3495 |
if(!hash_equals(self::asset_runtime_expectation_signature($expectation), $signature) || !self::asset_runtime_expectation_is_current($expectation)) |
| 3496 |
continue; |
| 3497 |
$valid_late_page = TRUE; |
| 3498 |
$late_issues[] = array( |
| 3499 |
'asset' => (string)$expectation['id'], |
| 3500 |
'label' => self::asset_runtime_health_label((string)$expectation['id']), |
| 3501 |
'delivery' => (!empty($expectation['delivery'])) ? (string)$expectation['delivery'] : '', |
| 3502 |
'detail' => 's2Member could not confirm that this asset became active within the configured wait time.', |
| 3503 |
); |
| 3504 |
$key = md5((string)$expectation['id']."\0".(string)$expectation['url']."\0".(string)$expectation['activation_tag']); |
| 3505 |
if(get_transient('ws_plugin__s2member_asset_runtime_suspect_'.$key)) |
| 3506 |
continue; |
| 3507 |
|
| 3508 |
$first_report = empty($suspicions[$key]); //260907.2203 Avoid duplicating the same low-trust runtime suspicion in the log. |
| 3509 |
|
| 3510 |
set_transient('ws_plugin__s2member_asset_runtime_suspect_'.$key, 1, MINUTE_IN_SECONDS); |
| 3511 |
$expectation['reported'] = time(); |
| 3512 |
$expectation['event_time'] = $event_time; |
| 3513 |
$expectation['page_id'] = (!empty($page_context['page_id'])) ? (int)$page_context['page_id'] : 0; |
| 3514 |
$expectation['page_path'] = (!empty($page_context['page_path'])) ? (string)$page_context['page_path'] : ''; |
| 3515 |
$expectation['signature'] = $signature; |
| 3516 |
$suspicions[$key] = $expectation; |
| 3517 |
|
| 3518 |
//260913.0048 Record accepted signed page context with the operational issue so css-js.log can correlate intermittent failures without storing query strings. |
| 3519 |
if($first_report) |
| 3520 |
c_ws_plugin__s2member_utils_logs::log_entry('css-js', array( |
| 3521 |
'event' => 'Frontend CSS/JS runtime issue reported', 'result' => 'suspected', 'asset' => (string)$expectation['id'], |
| 3522 |
'delivery' => (string)$expectation['delivery'], 'url' => (string)$expectation['url'], 'event_time' => $event_time, |
| 3523 |
'page_id' => (!empty($page_context['page_id'])) ? (int)$page_context['page_id'] : 0, 'page_path' => (!empty($page_context['page_path'])) ? (string)$page_context['page_path'] : '', 'trusted_confirmation_pending' => TRUE, |
| 3524 |
)); |
| 3525 |
|
| 3526 |
$recorded++; |
| 3527 |
} |
| 3528 |
if($recorded) |
| 3529 |
update_option('ws_plugin__s2member_asset_runtime_suspicions', $suspicions, FALSE); |
| 3530 |
if($valid_late_page) |
| 3531 |
{ |
| 3532 |
//260913.0048 One page still contributes one Late score, while all affected physical assets can share that signed page/time context in bounded Latest Issues. |
| 3533 |
//260910.2346 Current pages send signed load metadata so Late corrects the original page instead of becoming a second load. Cached first-v260909 pages have no load metadata, so only a newly accepted/rate-limited suspicion contributes standalone Late evidence. |
| 3534 |
if($load || $recorded) |
| 3535 |
self::queue_asset_health_load('late', FALSE, $load, array('items' => $late_issues)); |
| 3536 |
} |
| 3537 |
return $recorded; |
| 3538 |
} |
| 3539 |
|
| 3540 |
/** |
| 3541 |
* Records signed runtime suspicions carried by a page-local WordPress recovery request. |
| 3542 |
* |
| 3543 |
* @package s2Member\Utilities |
| 3544 |
* @since 260905.0009 |
| 3545 |
* |
| 3546 |
* @return int Number of newly recorded suspicions. |
| 3547 |
*/ |
| 3548 |
public static function record_asset_runtime_recovery_suspicion() |
| 3549 |
{ |
| 3550 |
if(empty($_GET['s2member_asset_runtime_suspect'])) |
| 3551 |
return 0; |
| 3552 |
$missing = json_decode(wp_unslash($_GET['s2member_asset_runtime_suspect']), TRUE); |
| 3553 |
return self::record_asset_runtime_suspicions($missing); |
| 3554 |
} |
| 3555 |
|
| 3556 |
/** |
| 3557 |
* Records a low-trust frontend runtime suspicion without changing delivery state. |
| 3558 |
* |
| 3559 |
* This endpoint remains available for misses where loading a full fallback could duplicate JavaScript that already ran. |
| 3560 |
* |
| 3561 |
* @package s2Member\Utilities |
| 3562 |
* @since 260904.2255 |
| 3563 |
* |
| 3564 |
* @return null Exits through WordPress JSON helpers. |
| 3565 |
*/ |
| 3566 |
public static function ajax_asset_runtime_suspicion() |
| 3567 |
{ |
| 3568 |
$missing = (!empty($_POST['missing'])) ? json_decode(wp_unslash($_POST['missing']), TRUE) : array(); |
| 3569 |
$load = (!empty($_POST['load'])) ? json_decode(wp_unslash($_POST['load']), TRUE) : array(); |
| 3570 |
wp_send_json_success(array('recorded' => self::record_asset_runtime_suspicions($missing, $load))); |
| 3571 |
} |
| 3572 |
|
| 3573 |
/** |
| 3574 |
* Returns whether an enabled static asset type intentionally requires dynamic delivery for compatibility. |
| 3575 |
* |
| 3576 |
* A source/build/filesystem failure is not an intentional requirement and remains a real fallback condition. |
| 3577 |
* |
| 3578 |
* @package s2Member\Utilities |
| 3579 |
* @since 260913.2001 |
| 3580 |
* |
| 3581 |
* @param string $type `css` or `js`. |
| 3582 |
* @return array Requirement state and site-owner detail. |
| 3583 |
*/ |
| 3584 |
protected static function static_type_dynamic_requirement($type = '') |
| 3585 |
{ |
| 3586 |
$type = strtolower((string)$type); |
| 3587 |
if(!in_array($type, array('css', 'js'), TRUE)) |
| 3588 |
return array('required' => FALSE, 'detail' => ''); |
| 3589 |
|
| 3590 |
$required = FALSE; |
| 3591 |
$details = array(); |
| 3592 |
foreach(self::static_asset_ids($type, 'all') as $id) |
| 3593 |
{ |
| 3594 |
$definition = self::static_asset_definition($id, FALSE); |
| 3595 |
if(!empty($definition['ok'])) |
| 3596 |
continue; |
| 3597 |
if(empty($definition['dynamic_required'])) |
| 3598 |
return array('required' => FALSE, 'detail' => ''); |
| 3599 |
$required = TRUE; |
| 3600 |
if(!empty($definition['health_detail'])) |
| 3601 |
$details[] = (string)$definition['health_detail']; |
| 3602 |
else if(!empty($definition['error'])) |
| 3603 |
$details[] = (string)$definition['error']; |
| 3604 |
} |
| 3605 |
return array('required' => $required, 'detail' => implode(' ', array_unique($details))); |
| 3606 |
} |
| 3607 |
|
| 3608 |
/** |
| 3609 |
* Returns the source definition for one currently enabled/compatible generated frontend asset file. |
| 3610 |
* |
| 3611 |
* @package s2Member\Utilities |
| 3612 |
* @since 260903.0525 |
| 3613 |
* |
| 3614 |
* @param string $id Logical generated filename. |
| 3615 |
* @param bool $include_sources Build ordered source definitions only when an asset actually needs generation. |
| 3616 |
* @return array Definition result. |
| 3617 |
*/ |
| 3618 |
protected static function static_asset_definition($id = '', $include_sources = TRUE) |
| 3619 |
{ |
| 3620 |
$id = strtolower((string)$id); |
| 3621 |
if(!in_array($id, array('s2member.css', 's2member-pro.css', 's2member.js', 's2member-pro.js'), TRUE)) |
| 3622 |
return array('ok' => FALSE, 'sources' => array(), 'minify' => FALSE, 'error' => 'Invalid static asset ID'); |
| 3623 |
$type = substr(strrchr($id, '.'), 1); |
| 3624 |
$pro_file = strpos($id, 's2member-pro.') === 0; |
| 3625 |
$combine = !$pro_file && !empty($GLOBALS['WS_PLUGIN__']['s2member']['o']['static_assets_combine']); |
| 3626 |
$o = $GLOBALS['WS_PLUGIN__']['s2member']['o']; |
| 3627 |
$c = $GLOBALS['WS_PLUGIN__']['s2member']['c']; |
| 3628 |
if($pro_file && !in_array($id, self::static_asset_ids($type, 'all'), TRUE)) |
| 3629 |
return array('ok' => FALSE, 'sources' => array(), 'minify' => FALSE, 'error' => 'Separate Pro static asset is not active'); |
| 3630 |
|
| 3631 |
if($type === 'css') |
| 3632 |
{ |
| 3633 |
if(empty($o['static_css'])) |
| 3634 |
return array('ok' => FALSE, 'sources' => array(), 'minify' => FALSE, 'error' => 'Static CSS Delivery is disabled'); |
| 3635 |
|
| 3636 |
//260903.0729 Framework-level dynamic requirements are authoritative; Pro may narrow only the generic during-CSS hook requirement when all callbacks are known static-compatible built-ins. |
| 3637 |
$framework_dynamic = has_action('ws_plugin__s2member_before_css') || isset($GLOBALS['wp_filter']['all']); |
| 3638 |
$hook_dynamic = has_action('ws_plugin__s2member_during_css'); |
| 3639 |
$hook_dynamic = (bool)apply_filters('ws_plugin__s2member_dynamic_css_required', $hook_dynamic, get_defined_vars()); |
| 3640 |
|
| 3641 |
if($framework_dynamic || $hook_dynamic) |
| 3642 |
{ |
| 3643 |
//260913.2001 Explain which compatibility condition makes Dynamic delivery intentional instead of collapsing all such cases into a generic fallback error. |
| 3644 |
$reasons = array(); |
| 3645 |
if(has_action('ws_plugin__s2member_before_css')) |
| 3646 |
$reasons[] = 'the "ws_plugin__s2member_before_css" hook has a customization'; |
| 3647 |
if(isset($GLOBALS['wp_filter']['all'])) |
| 3648 |
$reasons[] = 'WordPress\'s global "all" hook is active'; |
| 3649 |
if($hook_dynamic) |
| 3650 |
$reasons[] = (has_action('ws_plugin__s2member_during_css')) ? 'the "ws_plugin__s2member_during_css" hook has a customization that must remain dynamic' : 'the "ws_plugin__s2member_dynamic_css_required" filter requires dynamic CSS'; |
| 3651 |
$error = 'Static CSS cannot be used with the current request/configuration because '.implode('; ', array_unique($reasons)).'. Full WordPress Dynamic Loader is required so the current hooks and customizations remain available.'; |
| 3652 |
return array('ok' => FALSE, 'sources' => array(), 'minify' => FALSE, 'error' => $error, 'dynamic_required' => TRUE); |
| 3653 |
} |
| 3654 |
if(!$include_sources) |
| 3655 |
return array('ok' => TRUE, 'sources' => array(), 'minify' => !empty($o['static_css_minify']), 'error' => ''); |
| 3656 |
|
| 3657 |
$sources = ($pro_file) ? array() : array(array('file' => $c['dir'].'/src/includes/s2member.css', 'preserve_header' => TRUE)); |
| 3658 |
if(!$pro_file) |
| 3659 |
$sources = (array)apply_filters('ws_plugin__s2member_static_css_sources', $sources, get_defined_vars()); |
| 3660 |
if($pro_file || $combine) |
| 3661 |
$sources = (array)apply_filters('ws_plugin__s2member_static_pro_css_sources', $sources, get_defined_vars()); |
| 3662 |
if(!$sources) |
| 3663 |
return array('ok' => FALSE, 'sources' => array(), 'minify' => FALSE, 'error' => 'No static CSS sources are available for '.$id); |
| 3664 |
return array('ok' => TRUE, 'sources' => $sources, 'minify' => !empty($o['static_css_minify']), 'error' => ''); |
| 3665 |
} |
| 3666 |
if($type === 'js') |
| 3667 |
{ |
| 3668 |
if(empty($o['static_js'])) |
| 3669 |
return array('ok' => FALSE, 'sources' => array(), 'minify' => FALSE, 'error' => 'Static JS Delivery is disabled'); |
| 3670 |
if(!function_exists('wp_add_inline_script')) |
| 3671 |
return array('ok' => FALSE, 'sources' => array(), 'minify' => FALSE, 'error' => 'Static JS requires WordPress 4.5+'); |
| 3672 |
|
| 3673 |
$page_text = self::static_js_text_delivery() === 'page'; |
| 3674 |
if($page_text && !self::static_js_page_text_supported()) |
| 3675 |
return array('ok' => FALSE, 'sources' => array(), 'minify' => FALSE, 'error' => 'Loading JavaScript text with each WordPress page requires a current s2Member Pro version'); |
| 3676 |
|
| 3677 |
$js_api_constants_enabled = (bool)apply_filters('ws_plugin__s2member_js_api_constants_enable', FALSE); |
| 3678 |
if($page_text) |
| 3679 |
{ |
| 3680 |
//260906.2049 Text and other page-specific values resolve in the normal HTML request, so they do not make the external JavaScript dynamic. |
| 3681 |
$framework_dynamic = $js_api_constants_enabled || has_action('ws_plugin__s2member_before_js_w_globals') || isset($GLOBALS['wp_filter']['all']); |
| 3682 |
} |
| 3683 |
else |
| 3684 |
{ |
| 3685 |
$site_locale = (string)get_option('WPLANG'); |
| 3686 |
if(!$site_locale && defined('WPLANG')) |
| 3687 |
$site_locale = (string)WPLANG; |
| 3688 |
$site_locale = ($site_locale) ? $site_locale : 'en_US'; |
| 3689 |
$current_locale = (function_exists('determine_locale')) ? (string)determine_locale() : (string)get_locale(); |
| 3690 |
$framework_dynamic = $js_api_constants_enabled || has_action('ws_plugin__s2member_before_js_w_globals') || $current_locale !== $site_locale || has_filter('ws_plugin__s2member_files_dir') |
| 3691 |
|| has_filter('ws_plugin__s2member_min_password_length') || has_filter('ws_plugin__s2member_min_password_strength_code') || has_filter('ws_plugin__s2member_min_password_strength_score') |
| 3692 |
|| isset($GLOBALS['wp_filter']['all']); |
| 3693 |
} |
| 3694 |
|
| 3695 |
$hook_dynamic = has_action('ws_plugin__s2member_during_js_w_globals'); |
| 3696 |
$hook_dynamic = (bool)apply_filters('ws_plugin__s2member_dynamic_js_required', $hook_dynamic, get_defined_vars()); |
| 3697 |
if($hook_dynamic && !$page_text && self::static_js_builtin_pro_callbacks_supported()) |
| 3698 |
$hook_dynamic = FALSE; //260906.2049 Older Pro releases can still use static-file text when their JavaScript hook contains only known built-ins. |
| 3699 |
|
| 3700 |
if($framework_dynamic || $hook_dynamic) |
| 3701 |
{ |
| 3702 |
//260913.2111 Separate page-specific values that JavaScript Text Delivery can move into the page from requirements that still need Full WordPress Dynamic JS. |
| 3703 |
$text_delivery_reasons = array(); |
| 3704 |
$other_reasons = array(); |
| 3705 |
if($js_api_constants_enabled) |
| 3706 |
$other_reasons[] = 'full s2Member JavaScript API constants are enabled by the "ws_plugin__s2member_js_api_constants_enable" filter'; |
| 3707 |
if(has_action('ws_plugin__s2member_before_js_w_globals')) |
| 3708 |
$other_reasons[] = 'the "ws_plugin__s2member_before_js_w_globals" hook has a customization'; |
| 3709 |
if(!$page_text && isset($current_locale, $site_locale) && $current_locale !== $site_locale) |
| 3710 |
$text_delivery_reasons[] = 'the current page language differs from the site default ('.$current_locale.' vs '.$site_locale.')'; |
| 3711 |
foreach(array( |
| 3712 |
'ws_plugin__s2member_files_dir' => 'the s2Member files directory is generated dynamically', |
| 3713 |
'ws_plugin__s2member_min_password_length' => 'the minimum password length is generated dynamically', |
| 3714 |
'ws_plugin__s2member_min_password_strength_code' => 'the password-strength code is generated dynamically', |
| 3715 |
'ws_plugin__s2member_min_password_strength_score' => 'the password-strength score is generated dynamically', |
| 3716 |
) as $filter => $reason) |
| 3717 |
if(!$page_text && has_filter($filter)) |
| 3718 |
$text_delivery_reasons[] = $reason.' (via hook '.$filter.')'; |
| 3719 |
if(isset($GLOBALS['wp_filter']['all'])) |
| 3720 |
$other_reasons[] = 'WordPress\'s global "all" hook is active'; |
| 3721 |
if(has_filter('ws_plugin__s2member_pro_available_gateways')) |
| 3722 |
$other_reasons[] = 'the available Pro gateways are filtered dynamically by "ws_plugin__s2member_pro_available_gateways"'; |
| 3723 |
if($hook_dynamic && has_action('ws_plugin__s2member_during_js_w_globals') && !self::static_js_builtin_pro_callbacks_supported()) |
| 3724 |
$other_reasons[] = 'the "ws_plugin__s2member_during_js_w_globals" hook contains a custom, reordered, or unsupported callback'; |
| 3725 |
if($hook_dynamic && !$text_delivery_reasons && !$other_reasons) |
| 3726 |
$other_reasons[] = 'the "ws_plugin__s2member_dynamic_js_required" filter explicitly requires Dynamic JS'; |
| 3727 |
|
| 3728 |
$text_delivery_reasons = array_unique($text_delivery_reasons); |
| 3729 |
$other_reasons = array_unique($other_reasons); |
| 3730 |
$reasons = array_merge($text_delivery_reasons, $other_reasons); |
| 3731 |
$error = 'Static JavaScript requires Dynamic delivery because '.implode('; ', $reasons).'. Full WordPress Dynamic Loader is required so the current hooks, values, and customizations remain available.'; |
| 3732 |
if($text_delivery_reasons && !$other_reasons) |
| 3733 |
$health_detail = ucfirst(implode('; ', $text_delivery_reasons)).'. To keep the external JavaScript static, set <a href="#ws-plugin--s2member-static-js-text-setting">JavaScript Text Delivery</a> to "Load JavaScript text with each WordPress page".'; |
| 3734 |
else if($text_delivery_reasons && $other_reasons) |
| 3735 |
$health_detail = 'Some page-specific JavaScript values require Dynamic JS: '.implode('; ', $text_delivery_reasons).'. Setting <a href="#ws-plugin--s2member-static-js-text-setting">JavaScript Text Delivery</a> to "Load JavaScript text with each WordPress page" lets pages affected only by those values keep using Static JS. Pages where another detected requirement applies will still use Full WordPress Dynamic JS: '.implode('; ', $other_reasons).'.'; |
| 3736 |
else |
| 3737 |
$health_detail = 'Static JavaScript requires Dynamic delivery because '.implode('; ', $other_reasons).'. Full WordPress Dynamic Loader is used so the required hooks and customizations remain available.'; |
| 3738 |
return array('ok' => FALSE, 'sources' => array(), 'minify' => FALSE, 'error' => $error, 'health_detail' => $health_detail, 'dynamic_required' => TRUE); |
| 3739 |
} |
| 3740 |
if($page_text) |
| 3741 |
{ |
| 3742 |
$data_map_signature = self::static_js_data_map_signature($id); |
| 3743 |
if(empty($data_map_signature['ok'])) |
| 3744 |
return array('ok' => FALSE, 'sources' => array(), 'minify' => FALSE, 'error' => (string)$data_map_signature['error']); |
| 3745 |
} |
| 3746 |
if(!$include_sources) |
| 3747 |
return array('ok' => TRUE, 'sources' => array(), 'minify' => !empty($o['static_js_minify']), 'error' => ''); |
| 3748 |
|
| 3749 |
$sources = ($pro_file) ? array() : array( |
| 3750 |
array('file' => $c['dir'].'/src/includes/jquery/jquery.sprintf/jquery.sprintf.js', 'preserve_header' => TRUE), |
| 3751 |
($page_text) |
| 3752 |
? array('file' => $c['dir'].'/src/includes/s2member.js', 'data_map' => $c['dir'].'/src/includes/s2member.js.php', 'data_key' => 'f') |
| 3753 |
: array('file' => $c['dir'].'/src/includes/s2member.js', 'render' => TRUE), |
| 3754 |
); |
| 3755 |
if(!$pro_file) |
| 3756 |
$sources = (array)apply_filters('ws_plugin__s2member_static_js_sources', $sources, get_defined_vars()); |
| 3757 |
if($pro_file || $combine) |
| 3758 |
{ |
| 3759 |
$source_count = count($sources); |
| 3760 |
$sources = (array)apply_filters('ws_plugin__s2member_static_pro_js_sources', $sources, get_defined_vars()); |
| 3761 |
if(!$page_text && c_ws_plugin__s2member_utils_conds::pro_is_installed() && count($sources) === $source_count) |
| 3762 |
{ |
| 3763 |
$pro_output = self::static_js_builtin_pro_output(); |
| 3764 |
if($pro_output === '') |
| 3765 |
return array('ok' => FALSE, 'sources' => array(), 'minify' => FALSE, 'error' => 'No compatible static JavaScript source is available from the installed s2Member Pro version'); |
| 3766 |
$sources[] = array('contents' => $pro_output); |
| 3767 |
} |
| 3768 |
} |
| 3769 |
if(!$sources) |
| 3770 |
return array('ok' => FALSE, 'sources' => array(), 'minify' => FALSE, 'error' => 'No static JavaScript sources are available for '.$id); |
| 3771 |
return array('ok' => TRUE, 'sources' => $sources, 'minify' => !empty($o['static_js_minify']), 'error' => ''); |
| 3772 |
} |
| 3773 |
return array('ok' => FALSE, 'sources' => array(), 'minify' => FALSE, 'error' => 'Invalid static asset type'); |
| 3774 |
} |
| 3775 |
|
| 3776 |
/** |
| 3777 |
* Replaces marked PHP interpolations in a canonical JavaScript source with compact data-map slots. |
| 3778 |
* |
| 3779 |
* The current canonical sources place every marked interpolation inside a single-quoted JavaScript |
| 3780 |
* string. Replacing only the PHP block with `'+d[n]+'` preserves that historical string coercion. |
| 3781 |
* |
| 3782 |
* @package s2Member\Utilities |
| 3783 |
* @since 260906.0738 |
| 3784 |
* |
| 3785 |
* @param string $source Canonical mixed JS/PHP source. |
| 3786 |
* @param string $data_map_path Shipped data-map path. |
| 3787 |
* @param string $data_key Browser namespace key (`f` or `p`). |
| 3788 |
* @return array Transform result. |
| 3789 |
*/ |
| 3790 |
protected static function static_js_data_source($source = '', $data_map_path = '', $data_key = '') |
| 3791 |
{ |
| 3792 |
$data_map = self::static_js_data_map($data_map_path); |
| 3793 |
if(empty($data_map['ok'])) |
| 3794 |
return array('ok' => FALSE, 'source' => '', 'error' => (string)$data_map['error']); |
| 3795 |
if(!preg_match('/^[a-z][a-z0-9_]*$/i', (string)$data_key)) |
| 3796 |
return array('ok' => FALSE, 'source' => '', 'error' => 'Invalid JavaScript data namespace key'); |
| 3797 |
$slots = $data_map['slots']; |
| 3798 |
$errors = array(); |
| 3799 |
$source = preg_replace_callback('/<\\?php.*?\\?>/s', function($match) use ($slots, &$errors) { |
| 3800 |
if(!preg_match('/\\/\\*d\\*\\/(.*?)\\/\\*b\\*\\//s', $match[0], $data_match)) |
| 3801 |
{ |
| 3802 |
$errors[] = 'An unmarked PHP interpolation remains in a static JavaScript data source'; |
| 3803 |
return $match[0]; |
| 3804 |
} |
| 3805 |
$expression = trim((string)$data_match[1]); |
| 3806 |
if(!isset($slots[$expression])) |
| 3807 |
{ |
| 3808 |
$errors[] = 'A marked JavaScript expression is missing from its shipped data map'; |
| 3809 |
return $match[0]; |
| 3810 |
} |
| 3811 |
return "'+d[".(int)$slots[$expression]."]+'"; |
| 3812 |
}, (string)$source); |
| 3813 |
if($errors || strpos($source, '<?php') !== FALSE || strpos($source, '?>') !== FALSE) |
| 3814 |
return array('ok' => FALSE, 'source' => '', 'error' => ($errors) ? implode('; ', array_unique($errors)) : 'PHP remained after static JavaScript data transformation'); |
| 3815 |
//260906.0738 Keep the short alias lexical to this source so Framework and Pro slots cannot overwrite one another in separate or combined files. |
| 3816 |
return array('ok' => TRUE, 'source' => "(function(d){\n".$source."\n})(window.s2_data.".$data_key.");", 'error' => ''); |
| 3817 |
} |
| 3818 |
|
| 3819 |
/** |
| 3820 |
* Includes one trusted shipped static JavaScript data map in normal WordPress page context. |
| 3821 |
* |
| 3822 |
* @package s2Member\Utilities |
| 3823 |
* @since 260906.0738 |
| 3824 |
* |
| 3825 |
* @param string $path Data-map path. |
| 3826 |
* @param array|null $keys Optional stable slot IDs; NULL evaluates every value in the data map. |
| 3827 |
* @return array|false Data-map values, or FALSE on failure. |
| 3828 |
*/ |
| 3829 |
protected static function load_static_js_data_map($path = '', $keys = NULL) |
| 3830 |
{ |
| 3831 |
if(!$path || !is_readable($path)) |
| 3832 |
return FALSE; |
| 3833 |
$s2_data_keys = (is_array($keys)) ? array_fill_keys(array_map('intval', $keys), TRUE) : NULL; |
| 3834 |
$data = include $path; |
| 3835 |
return (is_array($data)) ? $data : FALSE; |
| 3836 |
} |
| 3837 |
|
| 3838 |
/** |
| 3839 |
* Returns page-local JavaScript data for the active generated static files. |
| 3840 |
* |
| 3841 |
* Complete data maps are emitted for now. TO-DO: pass page-specific sparse slot sets once feature requirements can be determined safely. |
| 3842 |
* |
| 3843 |
* @package s2Member\Utilities |
| 3844 |
* @since 260906.0738 |
| 3845 |
* |
| 3846 |
* @param array $assets Active generated JavaScript assets keyed by logical filename. |
| 3847 |
* @return string Inline JavaScript, or an empty string when data-map loading fails. |
| 3848 |
*/ |
| 3849 |
public static function static_js_inline_data($assets = array()) |
| 3850 |
{ |
| 3851 |
if(self::static_js_text_delivery() !== 'page') |
| 3852 |
return ''; |
| 3853 |
$paths = array(); |
| 3854 |
foreach(array_keys((array)$assets) as $id) |
| 3855 |
foreach(self::static_js_data_map_paths($id) as $key => $path) |
| 3856 |
$paths[$key] = $path; |
| 3857 |
if(!$paths) |
| 3858 |
return ''; |
| 3859 |
|
| 3860 |
$data = array(); |
| 3861 |
foreach($paths as $key => $path) |
| 3862 |
{ |
| 3863 |
$data_map = self::load_static_js_data_map($path); |
| 3864 |
if($data_map === FALSE) |
| 3865 |
return ''; |
| 3866 |
$data[$key] = $data_map; |
| 3867 |
} |
| 3868 |
$json = wp_json_encode($data); |
| 3869 |
if(!is_string($json) || $json === '') |
| 3870 |
return ''; |
| 3871 |
//260906.0738 wp_add_inline_script() prints this in HTML; neutralize user-translatable closing-script sequences just like existing inline current-user globals. |
| 3872 |
$inline = 'window.s2_data='.str_ireplace('</', '<\\/', $json).';'; |
| 3873 |
$extra = (string)apply_filters('ws_plugin__s2member_static_js_inline_globals', '', $assets, get_defined_vars()); |
| 3874 |
$extra = str_ireplace('</', '<\\/', $extra); //260906.0738 Pro gateway globals may contain translated text too, so apply the same closing-script protection. |
| 3875 |
return $inline.(($extra !== '') ? "\n".$extra : ''); |
| 3876 |
} |
| 3877 |
|
| 3878 |
/** |
| 3879 |
* Formats one preserved source notice compactly without turning it into an unreadable single line. |
| 3880 |
* |
| 3881 |
* @package s2Member\Utilities |
| 3882 |
* @since 260905.0106 |
| 3883 |
* |
| 3884 |
* @param string $comment Original leading source docblock. |
| 3885 |
* @return string Compact readable preserved notice. |
| 3886 |
*/ |
| 3887 |
protected static function preserved_asset_header($comment = '') |
| 3888 |
{ |
| 3889 |
$header = trim((string)$comment); |
| 3890 |
$header = preg_replace('/\A\/\*\*|\*\/\z/', '', $header); |
| 3891 |
$header = preg_replace('/^\s*\*\s?/m', '', $header); |
| 3892 |
$header = preg_replace('/\s+/', ' ', trim($header)); |
| 3893 |
$header = str_replace(array('©', '©'), '(c)', $header); |
| 3894 |
return "/*!\n * ".wordwrap($header, 140, "\n * ", FALSE)."\n */"; |
| 3895 |
} |
| 3896 |
|
| 3897 |
/** |
| 3898 |
* Prunes stale generated generations after a successful build. |
| 3899 |
* |
| 3900 |
* Keep current build files protected, retain up to ten older generations for cached HTML, |
| 3901 |
* and remove anything older than 30 days. This bounds normal disk use without deleting the |
| 3902 |
* previous timestamp immediately after a refresh. |
| 3903 |
* |
| 3904 |
* @package s2Member\Utilities |
| 3905 |
* @since 260905.0106 |
| 3906 |
* |
| 3907 |
* @param string $dir Current generated-asset directory. |
| 3908 |
* @param string $new_path Newly generated file path that must be preserved. |
| 3909 |
* @return null |
| 3910 |
*/ |
| 3911 |
protected static function prune_static_asset_generations($dir = '', $new_path = '') |
| 3912 |
{ |
| 3913 |
$dir = rtrim((string)$dir, '/\\'); |
| 3914 |
if(!$dir || !is_dir($dir)) |
| 3915 |
return; |
| 3916 |
$protected = array(); |
| 3917 |
$builds = get_option('ws_plugin__s2member_static_asset_builds', array()); |
| 3918 |
if(is_array($builds)) |
| 3919 |
foreach($builds as $id => $build) |
| 3920 |
if(in_array($id, array('s2member.css', 's2member-pro.css', 's2member.js', 's2member-pro.js'), TRUE) && ($build = abs((int)$build))) |
| 3921 |
{ |
| 3922 |
$type = substr(strrchr($id, '.'), 1); |
| 3923 |
$base = substr($id, 0, -strlen('.'.$type)); |
| 3924 |
$protected[$dir.'/'.$base.'-'.$build.'.'.$type] = TRUE; |
| 3925 |
} |
| 3926 |
if($new_path) |
| 3927 |
$protected[(string)$new_path] = TRUE; |
| 3928 |
|
| 3929 |
$groups = array(); |
| 3930 |
foreach(array('css', 'js') as $type) |
| 3931 |
foreach((array)glob($dir.'/s2member*.'.$type) as $path) |
| 3932 |
if(preg_match('/\/(s2member(?:-pro)?)-\d+\.(css|js)\z/', str_replace('\\', '/', $path), $match)) |
| 3933 |
$groups[$match[1].'.'.$match[2]][] = $path; |
| 3934 |
|
| 3935 |
$removed = array(); //260907.2203 Collect only files actually removed so cleanup logging stays accurate. |
| 3936 |
|
| 3937 |
foreach($groups as $paths) |
| 3938 |
{ |
| 3939 |
usort($paths, function($a, $b) { |
| 3940 |
return (int)@filemtime($b) - (int)@filemtime($a); |
| 3941 |
}); |
| 3942 |
$stale_kept = 0; |
| 3943 |
foreach($paths as $old_path) |
| 3944 |
{ |
| 3945 |
if(isset($protected[$old_path])) |
| 3946 |
continue; |
| 3947 |
$stale_kept++; |
| 3948 |
if((int)@filemtime($old_path) < time() - 30 * DAY_IN_SECONDS || $stale_kept > 10) |
| 3949 |
if(@unlink($old_path)) |
| 3950 |
$removed[] = basename($old_path); |
| 3951 |
} |
| 3952 |
} |
| 3953 |
|
| 3954 |
//260907.2203 Record cleanup only when stale files were actually deleted. |
| 3955 |
if($removed) |
| 3956 |
c_ws_plugin__s2member_utils_logs::log_entry('css-js', array('event' => 'Stale Static CSS/JS files pruned', 'result' => 'success', 'removed' => $removed)); |
| 3957 |
|
| 3958 |
return; |
| 3959 |
} |
| 3960 |
|
| 3961 |
/** |
| 3962 |
* Builds one timestamped CSS/JS file in the WordPress uploads tree. |
| 3963 |
* |
| 3964 |
* @package s2Member\Utilities |
| 3965 |
* @since 260903.0525 |
| 3966 |
* |
| 3967 |
* @param string $id Stable asset identifier. |
| 3968 |
* @param int $build Timestamp used in the generated filename. |
| 3969 |
* @param string $type `css` or `js`. |
| 3970 |
* @param array $sources Ordered source definitions/files. |
| 3971 |
* @param bool $minify Whether generated output should be minified. |
| 3972 |
* @return array Build result with URL/path or error. |
| 3973 |
*/ |
| 3974 |
protected static function build_static_asset($id = '', $build = 0, $type = '', $sources = array(), $minify = FALSE) |
| 3975 |
{ |
| 3976 |
$id = trim(preg_replace('/[^a-z0-9_\-]/i', '-', (string)$id), '-'); |
| 3977 |
$type = strtolower((string)$type); |
| 3978 |
if(!$id || !$build || !in_array($type, array('css', 'js'), TRUE) || !$sources) |
| 3979 |
return array('ok' => FALSE, 'url' => '', 'path' => '', 'error' => 'Invalid generated asset parameters'); |
| 3980 |
|
| 3981 |
$location = self::static_assets_location(TRUE); |
| 3982 |
if(empty($location['ok'])) |
| 3983 |
return array('ok' => FALSE, 'url' => '', 'path' => '', 'error' => $location['error']); |
| 3984 |
$filename = $id.'-'.(int)$build.'.'.$type; |
| 3985 |
$path = $location['dir'].'/'.$filename; |
| 3986 |
$url = $location['url'].'/'.$filename; |
| 3987 |
$headers = array(); |
| 3988 |
$body = ''; |
| 3989 |
foreach($sources as $source) |
| 3990 |
{ |
| 3991 |
$source = is_array($source) ? $source : array('file' => $source); |
| 3992 |
if(array_key_exists('contents', $source)) |
| 3993 |
$chunk = (string)$source['contents']; |
| 3994 |
else if(empty($source['file']) || !is_readable($source['file'])) |
| 3995 |
return array('ok' => FALSE, 'url' => '', 'path' => '', 'error' => 'Source file is not readable: '.((!empty($source['file'])) ? $source['file'] : '(missing path)')); |
| 3996 |
else if(!empty($source['data_map'])) |
| 3997 |
{ |
| 3998 |
if(($chunk = file_get_contents($source['file'])) === FALSE) |
| 3999 |
return array('ok' => FALSE, 'url' => '', 'path' => '', 'error' => 'Could not read source file: '.$source['file']); |
| 4000 |
$transformed = self::static_js_data_source($chunk, (string)$source['data_map'], (!empty($source['data_key'])) ? (string)$source['data_key'] : ''); |
| 4001 |
if(empty($transformed['ok'])) |
| 4002 |
return array('ok' => FALSE, 'url' => '', 'path' => '', 'error' => (string)$transformed['error'].' in '.$source['file']); |
| 4003 |
$chunk = $transformed['source']; |
| 4004 |
} |
| 4005 |
else if(!empty($source['render'])) |
| 4006 |
{ |
| 4007 |
$template_vars = (!empty($source['vars']) && is_array($source['vars'])) ? $source['vars'] : array(); |
| 4008 |
extract($template_vars, EXTR_SKIP); |
| 4009 |
ob_start(); |
| 4010 |
include $source['file']; |
| 4011 |
$chunk = ob_get_clean(); |
| 4012 |
} |
| 4013 |
else if(($chunk = file_get_contents($source['file'])) === FALSE) |
| 4014 |
return array('ok' => FALSE, 'url' => '', 'path' => '', 'error' => 'Could not read source file: '.$source['file']); |
| 4015 |
|
| 4016 |
if(preg_match('/\A\s*(\/\*\*.*?\*\/)\s*/s', $chunk, $match)) |
| 4017 |
{ |
| 4018 |
if(!empty($source['preserve_header'])) |
| 4019 |
{ |
| 4020 |
//260905.0106 Preserve the complete notice in a compact wrapped block instead of a huge original banner or an unreadable single line. |
| 4021 |
$header = self::preserved_asset_header($match[1]); |
| 4022 |
if(!in_array($header, $headers, TRUE)) |
| 4023 |
$headers[] = $header; |
| 4024 |
} |
| 4025 |
$chunk = preg_replace('/\A\s*\/\*\*.*?\*\/\s*/s', '', $chunk, 1); |
| 4026 |
} |
| 4027 |
if(!empty($source['replacements']) && is_array($source['replacements'])) |
| 4028 |
$chunk = str_replace(array_keys($source['replacements']), array_values($source['replacements']), $chunk); |
| 4029 |
$body .= "\n".((!empty($source['prefix'])) ? $source['prefix']."\n" : '').$chunk.((!empty($source['suffix'])) ? "\n".$source['suffix'] : ''); |
| 4030 |
} |
| 4031 |
|
| 4032 |
//260906.2219 Personal/member globals are page-specific by design and must never be written into a publicly cacheable static JavaScript file. |
| 4033 |
if($type === 'js' && preg_match('/\bS2MEMBER_CURRENT_USER_[A-Z0-9_]+\s*=(?!=)/', $body)) |
| 4034 |
return array('ok' => FALSE, 'url' => '', 'path' => '', 'error' => 'Page-specific member globals cannot be stored in static JavaScript'); |
| 4035 |
|
| 4036 |
try |
| 4037 |
{ |
| 4038 |
$body = ($minify) ? (($type === 'css') ? self::compress_css($body) : self::compress_js($body)) : trim($body); |
| 4039 |
} |
| 4040 |
catch(Exception $e) |
| 4041 |
{ |
| 4042 |
return array('ok' => FALSE, 'url' => '', 'path' => '', 'error' => 'JavaScript minification failed: '.$e->getMessage()); |
| 4043 |
} |
| 4044 |
$activation_tag = self::static_activation_tag_snippet($id, $type, $build); |
| 4045 |
$output = (($headers) ? implode("\n", $headers)."\n" : '').$body."\n".$activation_tag."\n"; |
| 4046 |
$tmp = $path.'.tmp-'.uniqid('', TRUE); |
| 4047 |
if(file_put_contents($tmp, $output, LOCK_EX) === FALSE || (!@rename($tmp, $path) && !is_file($path))) |
| 4048 |
{ |
| 4049 |
@unlink($tmp); |
| 4050 |
return array('ok' => FALSE, 'url' => '', 'path' => '', 'error' => 'Could not write generated asset: '.$path); |
| 4051 |
} |
| 4052 |
@unlink($tmp); |
| 4053 |
|
| 4054 |
return array('ok' => TRUE, 'url' => $url, 'path' => $path, 'error' => ''); |
| 4055 |
} |
| 4056 |
|
| 4057 |
/** |
| 4058 |
* Resolves the writable/public directory used for generated static frontend assets. |
| 4059 |
* |
| 4060 |
* @package s2Member\Utilities |
| 4061 |
* @since 260903.0437 |
| 4062 |
* |
| 4063 |
* @param bool $for_write Create/validate the directory for a write operation. |
| 4064 |
* @return array Location result. |
| 4065 |
*/ |
| 4066 |
protected static function static_assets_location($for_write = FALSE) |
| 4067 |
{ |
| 4068 |
$key = ($for_write) ? 'write' : 'read'; |
| 4069 |
if(isset(self::$static_assets_location_cache[$key])) |
| 4070 |
return self::$static_assets_location_cache[$key]; |
| 4071 |
$uploads = wp_upload_dir(NULL, (bool)$for_write); |
| 4072 |
if(!empty($uploads['error']) || empty($uploads['basedir']) || empty($uploads['baseurl'])) |
| 4073 |
return self::$static_assets_location_cache[$key] = array('ok' => FALSE, 'dir' => '', 'url' => '', 'error' => 'WordPress could not resolve a usable uploads directory'.((!empty($uploads['error'])) ? ': '.$uploads['error'] : '')); |
| 4074 |
|
| 4075 |
$dir = untrailingslashit((string)apply_filters('ws_plugin__s2member_static_assets_dir', trailingslashit($uploads['basedir']).'s2member-assets', $uploads)); |
| 4076 |
$url = untrailingslashit((string)apply_filters('ws_plugin__s2member_static_assets_url', trailingslashit($uploads['baseurl']).'s2member-assets', $uploads)); |
| 4077 |
if(!$dir || !$url) |
| 4078 |
return self::$static_assets_location_cache[$key] = array('ok' => FALSE, 'dir' => '', 'url' => '', 'error' => 'The static-assets directory or URL filter returned an empty value'); |
| 4079 |
if($for_write) |
| 4080 |
{ |
| 4081 |
if(!is_dir($dir) && !wp_mkdir_p($dir)) |
| 4082 |
return self::$static_assets_location_cache[$key] = array('ok' => FALSE, 'dir' => $dir, 'url' => $url, 'error' => 'Could not create static-assets directory: '.$dir); |
| 4083 |
if(!is_writable($dir)) |
| 4084 |
return self::$static_assets_location_cache[$key] = array('ok' => FALSE, 'dir' => $dir, 'url' => $url, 'error' => 'Static-assets directory is not writable: '.$dir); |
| 4085 |
|
| 4086 |
//260905.0158 Discourage casual directory listing without adding executable PHP to the public uploads directory. |
| 4087 |
$index_file = $dir.'/index.html'; |
| 4088 |
if(!is_file($index_file)) |
| 4089 |
@file_put_contents($index_file, '<!-- Silence is golden. -->'."\n", LOCK_EX); |
| 4090 |
} |
| 4091 |
return self::$static_assets_location_cache[$key] = array('ok' => TRUE, 'dir' => $dir, 'url' => $url, 'error' => ''); |
| 4092 |
} |
| 4093 |
|
| 4094 |
/** |
| 4095 |
* Handles CSS compression of hex colors. |
| 4096 |
* |
| 4097 |
* @package s2Member\Utilities |
| 4098 |
* @since 3.5 |
| 4099 |
* |
| 4100 |
* @param array $m Array of matches from ``preg_replace_callback()``. |
| 4101 |
* @return string Shortened hex code when possible, full hex code otherwise. |
| 4102 |
*/ |
| 4103 |
public static function _compress_css_c3($m = FALSE) |
| 4104 |
{ |
| 4105 |
if($m[2][0] === $m[2][1] && $m[2][2] === $m[2][3] && $m[2][4] === $m[2][5]) |
| 4106 |
return $m[1].$m[2][0].$m[2][2].$m[2][4]; |
| 4107 |
return $m[0]; |
| 4108 |
} |
| 4109 |
|
| 4110 |
/** |
| 4111 |
* JShrink 1.8.1 adaptation used for generated JavaScript minification. |
| 4112 |
* |
| 4113 |
* JShrink is Copyright (c) Robert Hafner and licensed under BSD-3-Clause. |
| 4114 |
* See `/src/licensing/jshrink.txt` for the complete license and attribution. |
| 4115 |
* @see https://github.com/tedious/JShrink |
| 4116 |
* |
| 4117 |
* The upstream parser is kept intentionally isolated behind `jshrink_*` names. |
| 4118 |
* The only PHP 5.6 compatibility change avoids PHP 7.1+ negative string offsets |
| 4119 |
* when tracking the last character. |
| 4120 |
* |
| 4121 |
* @package s2Member\Utilities |
| 4122 |
* @since 260903.0437 |
| 4123 |
*/ |
| 4124 |
protected $jshrink_input; |
| 4125 |
protected $jshrink_len = 0; |
| 4126 |
protected $jshrink_index = 0; |
| 4127 |
protected $jshrink_a = ''; |
| 4128 |
protected $jshrink_b = ''; |
| 4129 |
protected $jshrink_c; |
| 4130 |
protected $jshrink_last_char; |
| 4131 |
protected $jshrink_output = ''; |
| 4132 |
protected $jshrink_options = array(); |
| 4133 |
protected $jshrink_string_delimiters = array("'" => TRUE, '"' => TRUE, '`' => TRUE); |
| 4134 |
protected $jshrink_no_new_line_characters = array('(' => TRUE, '-' => TRUE, '+' => TRUE, '[' => TRUE, '#' => TRUE, '@' => TRUE); |
| 4135 |
protected static $jshrink_default_options = array('flaggedComments' => TRUE); |
| 4136 |
protected static $jshrink_keywords = array('delete', 'do', 'for', 'in', 'instanceof', 'return', 'typeof', 'yield'); |
| 4137 |
protected $jshrink_max_keyword_len = 0; |
| 4138 |
protected $jshrink_locks = array(); |
| 4139 |
|
| 4140 |
protected function jshrink_minify_to_string($js, $options) |
| 4141 |
{ |
| 4142 |
$this->jshrink_initialize($js, $options); |
| 4143 |
$this->jshrink_loop(); |
| 4144 |
$output = $this->jshrink_output; |
| 4145 |
$this->jshrink_clean(); |
| 4146 |
return $output; |
| 4147 |
} |
| 4148 |
|
| 4149 |
protected function jshrink_initialize($js, $options) |
| 4150 |
{ |
| 4151 |
$this->jshrink_options = array_merge(self::$jshrink_default_options, $options); |
| 4152 |
$this->jshrink_input = $js.PHP_EOL; |
| 4153 |
$this->jshrink_len = strlen($this->jshrink_input); |
| 4154 |
$this->jshrink_a = "\n"; |
| 4155 |
$this->jshrink_b = "\n"; |
| 4156 |
$this->jshrink_last_char = "\n"; |
| 4157 |
$this->jshrink_output = ''; |
| 4158 |
$this->jshrink_max_keyword_len = max(array_map('strlen', self::$jshrink_keywords)); |
| 4159 |
} |
| 4160 |
|
| 4161 |
protected function jshrink_echo($char) |
| 4162 |
{ |
| 4163 |
$this->jshrink_output .= $char; |
| 4164 |
//260903.0437 JShrink 1.8.1 uses `$char[-1]`; `substr()` preserves that behavior on s2Member's PHP 5.6 minimum. |
| 4165 |
$this->jshrink_last_char = substr($char, -1); |
| 4166 |
} |
| 4167 |
|
| 4168 |
protected function jshrink_loop() |
| 4169 |
{ |
| 4170 |
while($this->jshrink_a !== FALSE && !is_null($this->jshrink_a) && $this->jshrink_a !== '') |
| 4171 |
{ |
| 4172 |
switch($this->jshrink_a) |
| 4173 |
{ |
| 4174 |
case "\r": |
| 4175 |
case "\n": |
| 4176 |
if($this->jshrink_b !== FALSE && isset($this->jshrink_no_new_line_characters[$this->jshrink_b])) |
| 4177 |
{ |
| 4178 |
$this->jshrink_echo($this->jshrink_a); |
| 4179 |
$this->jshrink_save_string(); |
| 4180 |
break; |
| 4181 |
} |
| 4182 |
if($this->jshrink_b === ' ') |
| 4183 |
break; |
| 4184 |
case ' ': |
| 4185 |
if(self::jshrink_is_alphanumeric($this->jshrink_b)) |
| 4186 |
$this->jshrink_echo($this->jshrink_a); |
| 4187 |
$this->jshrink_save_string(); |
| 4188 |
break; |
| 4189 |
default: |
| 4190 |
switch($this->jshrink_b) |
| 4191 |
{ |
| 4192 |
case "\r": |
| 4193 |
case "\n": |
| 4194 |
if(strpos('}])+-"\'', $this->jshrink_a) !== FALSE) |
| 4195 |
{ |
| 4196 |
$this->jshrink_echo($this->jshrink_a); |
| 4197 |
$this->jshrink_save_string(); |
| 4198 |
break; |
| 4199 |
} |
| 4200 |
else if(self::jshrink_is_alphanumeric($this->jshrink_a)) |
| 4201 |
{ |
| 4202 |
$this->jshrink_echo($this->jshrink_a); |
| 4203 |
$this->jshrink_save_string(); |
| 4204 |
} |
| 4205 |
break; |
| 4206 |
case ' ': |
| 4207 |
if(!self::jshrink_is_alphanumeric($this->jshrink_a)) |
| 4208 |
break; |
| 4209 |
default: |
| 4210 |
if($this->jshrink_a === '/' && ($this->jshrink_b === "'" || $this->jshrink_b === '"')) |
| 4211 |
{ |
| 4212 |
$this->jshrink_save_regex(); |
| 4213 |
continue 3; |
| 4214 |
} |
| 4215 |
$this->jshrink_echo($this->jshrink_a); |
| 4216 |
$this->jshrink_save_string(); |
| 4217 |
break; |
| 4218 |
} |
| 4219 |
} |
| 4220 |
|
| 4221 |
$this->jshrink_b = $this->jshrink_get_real(); |
| 4222 |
if($this->jshrink_b == '/') |
| 4223 |
{ |
| 4224 |
$valid_tokens = "(,=:[!&|?\n"; |
| 4225 |
$last_token = ($this->jshrink_a == ' ') ? $this->jshrink_last_char : $this->jshrink_a; |
| 4226 |
if(strpos($valid_tokens, $last_token) !== FALSE || $this->jshrink_ends_in_keyword()) |
| 4227 |
$this->jshrink_save_regex(); |
| 4228 |
} |
| 4229 |
} |
| 4230 |
} |
| 4231 |
|
| 4232 |
protected function jshrink_clean() |
| 4233 |
{ |
| 4234 |
unset($this->jshrink_input, $this->jshrink_c, $this->jshrink_options); |
| 4235 |
$this->jshrink_len = $this->jshrink_index = 0; |
| 4236 |
$this->jshrink_a = $this->jshrink_b = ''; |
| 4237 |
$this->jshrink_output = ''; |
| 4238 |
} |
| 4239 |
|
| 4240 |
protected function jshrink_get_char() |
| 4241 |
{ |
| 4242 |
if(isset($this->jshrink_c)) |
| 4243 |
{ |
| 4244 |
$char = $this->jshrink_c; |
| 4245 |
unset($this->jshrink_c); |
| 4246 |
} |
| 4247 |
else |
| 4248 |
{ |
| 4249 |
$char = ($this->jshrink_index < $this->jshrink_len) ? $this->jshrink_input[$this->jshrink_index] : FALSE; |
| 4250 |
if($char === FALSE) |
| 4251 |
return FALSE; |
| 4252 |
$this->jshrink_index++; |
| 4253 |
} |
| 4254 |
if($char == "\r") |
| 4255 |
$char = "\n"; |
| 4256 |
if($char !== "\n" && $char < "\x20") |
| 4257 |
return ' '; |
| 4258 |
return $char; |
| 4259 |
} |
| 4260 |
|
| 4261 |
protected function jshrink_peek() |
| 4262 |
{ |
| 4263 |
if($this->jshrink_index >= $this->jshrink_len) |
| 4264 |
return FALSE; |
| 4265 |
$char = $this->jshrink_input[$this->jshrink_index]; |
| 4266 |
if($char == "\r") |
| 4267 |
$char = "\n"; |
| 4268 |
if($char !== "\n" && $char < "\x20") |
| 4269 |
return ' '; |
| 4270 |
return $char; |
| 4271 |
} |
| 4272 |
|
| 4273 |
protected function jshrink_get_real() |
| 4274 |
{ |
| 4275 |
$start_index = $this->jshrink_index; |
| 4276 |
$char = $this->jshrink_get_char(); |
| 4277 |
if($char !== '/') |
| 4278 |
return $char; |
| 4279 |
$this->jshrink_c = $this->jshrink_get_char(); |
| 4280 |
if($this->jshrink_c === '/') |
| 4281 |
{ |
| 4282 |
$this->jshrink_process_one_line_comments($start_index); |
| 4283 |
return $this->jshrink_get_real(); |
| 4284 |
} |
| 4285 |
else if($this->jshrink_c === '*') |
| 4286 |
{ |
| 4287 |
$this->jshrink_process_multi_line_comments($start_index); |
| 4288 |
return $this->jshrink_get_real(); |
| 4289 |
} |
| 4290 |
return $char; |
| 4291 |
} |
| 4292 |
|
| 4293 |
protected function jshrink_process_one_line_comments($start_index) |
| 4294 |
{ |
| 4295 |
$third = ($this->jshrink_index < $this->jshrink_len) ? $this->jshrink_input[$this->jshrink_index] : FALSE; |
| 4296 |
$this->jshrink_get_next("\n"); |
| 4297 |
unset($this->jshrink_c); |
| 4298 |
if($third == '@') |
| 4299 |
{ |
| 4300 |
$end = $this->jshrink_index - $start_index; |
| 4301 |
$this->jshrink_c = "\n".substr($this->jshrink_input, $start_index, $end); |
| 4302 |
} |
| 4303 |
} |
| 4304 |
|
| 4305 |
protected function jshrink_process_multi_line_comments($start_index) |
| 4306 |
{ |
| 4307 |
$this->jshrink_get_char(); |
| 4308 |
$third = $this->jshrink_get_char(); |
| 4309 |
if($third == '*' && $this->jshrink_peek() == '/') |
| 4310 |
{ |
| 4311 |
$this->jshrink_index++; |
| 4312 |
return; |
| 4313 |
} |
| 4314 |
if($this->jshrink_get_next('*/')) |
| 4315 |
{ |
| 4316 |
$this->jshrink_get_char(); |
| 4317 |
$this->jshrink_get_char(); |
| 4318 |
$char = $this->jshrink_get_char(); |
| 4319 |
if((!empty($this->jshrink_options['flaggedComments']) && $third === '!') || $third === '@') |
| 4320 |
{ |
| 4321 |
if($start_index > 0) |
| 4322 |
{ |
| 4323 |
$this->jshrink_echo($this->jshrink_a); |
| 4324 |
$this->jshrink_a = ' '; |
| 4325 |
if($this->jshrink_input[$start_index - 1] === "\n") |
| 4326 |
$this->jshrink_echo("\n"); |
| 4327 |
} |
| 4328 |
$end = ($this->jshrink_index - 1) - $start_index; |
| 4329 |
$this->jshrink_echo(substr($this->jshrink_input, $start_index, $end)); |
| 4330 |
$this->jshrink_c = $char; |
| 4331 |
return; |
| 4332 |
} |
| 4333 |
} |
| 4334 |
else |
| 4335 |
$char = FALSE; |
| 4336 |
if($char === FALSE) |
| 4337 |
throw new RuntimeException('Unclosed multiline comment at position: '.($this->jshrink_index - 2)); |
| 4338 |
$this->jshrink_c = $char; |
| 4339 |
} |
| 4340 |
|
| 4341 |
protected function jshrink_get_next($string) |
| 4342 |
{ |
| 4343 |
$pos = strpos($this->jshrink_input, $string, $this->jshrink_index); |
| 4344 |
if($pos === FALSE) |
| 4345 |
return FALSE; |
| 4346 |
$this->jshrink_index = $pos; |
| 4347 |
return ($this->jshrink_index < $this->jshrink_len) ? $this->jshrink_input[$this->jshrink_index] : FALSE; |
| 4348 |
} |
| 4349 |
|
| 4350 |
protected function jshrink_save_string() |
| 4351 |
{ |
| 4352 |
$start = $this->jshrink_index; |
| 4353 |
$this->jshrink_a = $this->jshrink_b; |
| 4354 |
if(!isset($this->jshrink_string_delimiters[$this->jshrink_a])) |
| 4355 |
return; |
| 4356 |
$type = $this->jshrink_a; |
| 4357 |
$this->jshrink_echo($this->jshrink_a); |
| 4358 |
while(($this->jshrink_a = $this->jshrink_get_char()) !== FALSE) |
| 4359 |
{ |
| 4360 |
switch($this->jshrink_a) |
| 4361 |
{ |
| 4362 |
case $type: |
| 4363 |
break 2; |
| 4364 |
case "\n": |
| 4365 |
if($type === '`') |
| 4366 |
$this->jshrink_echo($this->jshrink_a); |
| 4367 |
else |
| 4368 |
throw new RuntimeException('Unclosed string at position: '.$start); |
| 4369 |
break; |
| 4370 |
case '\\': |
| 4371 |
$this->jshrink_b = $this->jshrink_get_char(); |
| 4372 |
if($this->jshrink_b !== "\n") |
| 4373 |
$this->jshrink_echo($this->jshrink_a.$this->jshrink_b); |
| 4374 |
break; |
| 4375 |
default: |
| 4376 |
$this->jshrink_echo($this->jshrink_a); |
| 4377 |
} |
| 4378 |
} |
| 4379 |
} |
| 4380 |
|
| 4381 |
protected function jshrink_save_regex() |
| 4382 |
{ |
| 4383 |
if($this->jshrink_a != ' ') |
| 4384 |
$this->jshrink_echo($this->jshrink_a); |
| 4385 |
$this->jshrink_echo($this->jshrink_b); |
| 4386 |
$character_class = FALSE; |
| 4387 |
$character_class_index = NULL; |
| 4388 |
while(($this->jshrink_a = $this->jshrink_get_char()) !== FALSE) |
| 4389 |
{ |
| 4390 |
if($this->jshrink_a === '/' && !$character_class) |
| 4391 |
break; |
| 4392 |
if($this->jshrink_a === '[') |
| 4393 |
{ |
| 4394 |
$character_class = TRUE; |
| 4395 |
$character_class_index = $this->jshrink_index; |
| 4396 |
} |
| 4397 |
else if($this->jshrink_a === ']') |
| 4398 |
$character_class = FALSE; |
| 4399 |
if($this->jshrink_a === '\\') |
| 4400 |
{ |
| 4401 |
$this->jshrink_echo($this->jshrink_a); |
| 4402 |
$this->jshrink_a = $this->jshrink_get_char(); |
| 4403 |
} |
| 4404 |
if($this->jshrink_a === "\n") |
| 4405 |
{ |
| 4406 |
if($character_class) |
| 4407 |
throw new RuntimeException('Unclosed character class at position: '.$character_class_index); |
| 4408 |
throw new RuntimeException('Unclosed regex pattern at position: '.$this->jshrink_index); |
| 4409 |
} |
| 4410 |
$this->jshrink_echo($this->jshrink_a); |
| 4411 |
} |
| 4412 |
$this->jshrink_b = $this->jshrink_get_real(); |
| 4413 |
} |
| 4414 |
|
| 4415 |
protected static function jshrink_is_alphanumeric($char) |
| 4416 |
{ |
| 4417 |
return preg_match('/^[\w\$\pL]$/', $char) === 1 || $char == '/'; |
| 4418 |
} |
| 4419 |
|
| 4420 |
protected function jshrink_ends_in_keyword() |
| 4421 |
{ |
| 4422 |
$test = substr($this->jshrink_output.$this->jshrink_a, -1 * ($this->jshrink_max_keyword_len + 10)); |
| 4423 |
foreach(self::$jshrink_keywords as $keyword) |
| 4424 |
if(preg_match('/[^\w]'.$keyword.'[ ]?$/i', $test) === 1) |
| 4425 |
return TRUE; |
| 4426 |
return FALSE; |
| 4427 |
} |
| 4428 |
|
| 4429 |
protected function jshrink_lock($js) |
| 4430 |
{ |
| 4431 |
$lock = '"LOCK---'.crc32(time()).'"'; |
| 4432 |
$matches = array(); |
| 4433 |
preg_match('/([+-])(\s+)([+-])/S', $js, $matches); |
| 4434 |
if(empty($matches)) |
| 4435 |
return $js; |
| 4436 |
$this->jshrink_locks[$lock] = $matches[2]; |
| 4437 |
return preg_replace('/([+-])\s+([+-])/S', '$1'.$lock.'$2', $js); |
| 4438 |
} |
| 4439 |
|
| 4440 |
protected function jshrink_unlock($js) |
| 4441 |
{ |
| 4442 |
foreach($this->jshrink_locks as $lock => $replacement) |
| 4443 |
$js = str_replace($lock, $replacement, $js); |
| 4444 |
return $js; |
| 4445 |
} |
| 4446 |
} |
| 4447 |
} |
| 4448 |
|