| @@ -207,9 +207,9 @@ | ||
| 207 | 207 | |
| 208 | 208 | //260904.2014 Dynamic delivery applies independently of static delivery, so give its loader choice a separate section. |
| 209 | 209 | echo '<div id="ws-plugin--s2member-dynamic-asset-loader-section" class="ws-menu-page-section ws-plugin--s2member-dynamic-asset-loader-section">'."\n"; |
| 210 | 210 | echo '<h3>Dynamic CSS/JS Loader</h3>'."\n"; |
| 211 | - echo '<p>When s2Member needs to generate CSS or JavaScript dynamically, this controls how that request is loaded.</p>'."\n"; | |
| 211 | + echo '<p>When s2Member needs to generate CSS or JavaScript dynamically, this controls the preferred dynamic route. If Static CSS/JS Delivery is enabled but current hooks or configuration specifically require the normal WordPress environment, s2Member uses the Full WordPress Dynamic Loader for compatibility instead.</p>'."\n"; | |
| 212 | 212 | echo '<table class="form-table">'."\n"; |
| 213 | 213 | echo '<tbody>'."\n"; |
| 214 | 214 | |
| 215 | 215 | $dynamic_asset_loader = (!empty($GLOBALS['WS_PLUGIN__']['s2member']['o']['dynamic_asset_loader']) && $GLOBALS['WS_PLUGIN__']['s2member']['o']['dynamic_asset_loader'] === 'wordpress') ? 'wordpress' : 's2o'; |
| @@ -215,12 +215,13 @@ | ||
| 215 | 215 | $dynamic_asset_loader = (!empty($GLOBALS['WS_PLUGIN__']['s2member']['o']['dynamic_asset_loader']) && $GLOBALS['WS_PLUGIN__']['s2member']['o']['dynamic_asset_loader'] === 'wordpress') ? 'wordpress' : 's2o'; |
| 216 | 216 | echo '<tr><th><label for="ws-plugin--s2member-dynamic-asset-loader">Dynamic CSS/JS Loader</label></th></tr>'."\n"; |
| 217 | 217 | echo '<tr><td>'."\n"; |
| 218 | 218 | echo '<select name="ws_plugin__s2member_dynamic_asset_loader" id="ws-plugin--s2member-dynamic-asset-loader">'."\n"; |
| 219 | - echo '<option value="s2o"'.(($dynamic_asset_loader === 's2o') ? ' selected="selected"' : '').'>s2Member Dynamic Loader (recommended)</option>'."\n"; | |
| 220 | - echo '<option value="wordpress"'.(($dynamic_asset_loader === 'wordpress') ? ' selected="selected"' : '').'>WordPress Dynamic Loader (compatibility)</option>'."\n"; | |
| 219 | + echo '<option value="s2o"'.(($dynamic_asset_loader === 's2o') ? ' selected="selected"' : '').'>s2Member-Only Dynamic Loader (recommended)</option>'."\n"; | |
| 220 | + echo '<option value="wordpress"'.(($dynamic_asset_loader === 'wordpress') ? ' selected="selected"' : '').'>Full WordPress Dynamic Loader (compatibility)</option>'."\n"; | |
| 221 | 221 | echo '</select><br />'."\n"; |
| 222 | - echo '<em>The s2Member Dynamic Loader uses a lighter WordPress load for better performance. The WordPress Dynamic Loader uses the site\'s normal WordPress request and can help when a server or security tool blocks direct <code>s2member-o.php</code> requests. See <a href="https://s2member.com/kb-article/mod-security-odd-403-503-500-errors/" target="_blank" rel="external">Mod Security (Odd 403, 503, 500 Errors)</a>.</em>'."\n"; | |
| 222 | + //260910.0709 Keep the familiar s2member-o.php identity visible, explain the lighter WordPress load for new users, and link the two KB references needed when security rules block direct access to that plugin PHP file. | |
| 223 | + echo '<em>The s2Member-Only Dynamic Loader uses a lighter WordPress load for better performance through <code>s2member-o.php</code>. See <a href="https://s2member.com/kb-article/s2member-only-mode/" target="_blank" rel="noopener noreferrer external">s2Member-Only Mode</a>. Because this PHP file is inside the s2Member plugin directory, some server or security configurations need to explicitly allow web access to it. If it returns a 403, 503, or 500 error, see <a href="https://s2member.com/kb-article/mod-security-odd-403-503-500-errors/" target="_blank" rel="noopener noreferrer external">Mod Security (Odd 403, 503, 500 Errors)</a>. The Full WordPress Dynamic Loader uses the site\'s normal WordPress request as a compatibility alternative.</em>'."\n"; | |
| 223 | 224 | echo '</td></tr>'."\n"; |
| 224 | 225 | |
| 225 | 226 | echo '</tbody>'."\n"; |
| 226 | 227 | echo '</table>'."\n"; |
| @@ -231,11 +232,11 @@ | ||
| 231 | 232 | echo '</div>'."\n"; |
| 232 | 233 | |
| 233 | 234 | //260904.2014 Keep static CSS/JS delivery and its related optimizations together in a clearly named section. |
| 234 | 235 | echo '<div id="ws-plugin--s2member-static-assets" class="ws-menu-page-section ws-plugin--s2member-static-assets-section">'."\n"; |
| 235 | - echo '<h3>Static CSS/JS Optimization (beta)</h3>'."\n"; | |
| 236 | + echo '<h3>CSS/JS Delivery & Optimization (Beta)</h3>'."\n"; | |
| 236 | 237 | echo '<p>These beta options build CSS and JavaScript files ahead of time under the WordPress uploads directory, so the web server can serve them directly without starting PHP and WordPress for each request. Framework and Pro files remain separate by default, with optional combining and minification for further optimization.</p>'."\n"; |
| 237 | - echo '<p><em>After enabling these options, test your membership, registration, profile, and payment pages. If a static file cannot be used, s2Member automatically uses the selected Dynamic CSS/JS Loader instead.</em></p>'."\n"; | |
| 238 | + echo '<p><em>After enabling these options, test your membership, registration, profile, and payment pages. If static delivery cannot be used, s2Member switches to dynamic delivery automatically. Compatibility requirements that need normal WordPress hooks use the Full WordPress Dynamic Loader; Asset Health explains the active route and reason.</em></p>'."\n"; | |
| 238 | 239 | |
| 239 | 240 | echo '<table class="form-table">'."\n"; |
| 240 | 241 | echo '<tbody>'."\n"; |
| 241 | 242 | echo '<tr><th><label for="ws-plugin--s2member-static-css">Static CSS Delivery?</label></th></tr>'."\n"; |
| @@ -266,9 +267,10 @@ | ||
| 266 | 267 | echo '</td></tr>'."\n"; |
| 267 | 268 | |
| 268 | 269 | $static_js_text = (isset($GLOBALS['WS_PLUGIN__']['s2member']['o']['static_js_text']) && $GLOBALS['WS_PLUGIN__']['s2member']['o']['static_js_text'] === 'page') ? 'page' : 'static'; |
| 269 | 270 | $static_js_page_text_supported = c_ws_plugin__s2member_utils_assets::static_js_page_text_supported(); |
| 270 | - echo '<tr><th><label for="ws-plugin--s2member-static-js-text">JavaScript Text Delivery</label></th></tr>'."\n"; | |
| 271 | + //260913.2140 Link Asset Health guidance to the full setting row and leave room above it so the setting label remains visible below the WordPress admin bar. | |
| 272 | + echo '<tr id="ws-plugin--s2member-static-js-text-setting" style="scroll-margin-top:64px;"><th><label for="ws-plugin--s2member-static-js-text">JavaScript Text Delivery</label></th></tr>'."\n"; | |
| 271 | 273 | echo '<tr><td>'."\n"; |
| 272 | 274 | echo '<p style="margin-top:0;"><em>Some s2Member messages and labels are shown or updated by JavaScript—for example, form validation and password feedback—so JavaScript needs access to that text too.</em></p>'."\n"; |
| 273 | 275 | echo '<select name="ws_plugin__s2member_static_js_text" id="ws-plugin--s2member-static-js-text">'."\n"; |
| 274 | 276 | echo '<option value="static"'.(($static_js_text === 'static') ? ' selected="selected"' : '').'>Load JavaScript text with the static files</option>'."\n"; |
| @@ -273,9 +275,10 @@ | ||
| 273 | 275 | echo '<select name="ws_plugin__s2member_static_js_text" id="ws-plugin--s2member-static-js-text">'."\n"; |
| 274 | 276 | echo '<option value="static"'.(($static_js_text === 'static') ? ' selected="selected"' : '').'>Load JavaScript text with the static files</option>'."\n"; |
| 275 | 277 | echo '<option value="page"'.(($static_js_text === 'page') ? ' selected="selected"' : '').((!$static_js_page_text_supported) ? ' disabled="disabled"' : '').'>Load JavaScript text with each WordPress page</option>'."\n"; |
| 276 | 278 | echo '</select><br />'."\n"; |
| 277 | - echo '<em><strong>Static files:</strong> best for sites that use one language throughout; the text is cached with the generated JavaScript and does not add the larger text block to every page. <strong>WordPress page:</strong> use this for multilingual sites where the language can change between pages or visitors; the same external JavaScript can then be reused across languages. Personal/member details are always loaded with the page and are never stored in static files.</em>'."\n"; | |
| 279 | + //260913.2111 Explain that page-loaded text can also preserve Static JS when request-specific values would otherwise require Dynamic delivery. | |
| 280 | + echo '<em><strong>Static files:</strong> best for sites that use one language throughout; the text is cached with the generated JavaScript and does not add the larger text block to every page. <strong>WordPress page:</strong> use this for multilingual sites or when page-specific JavaScript values vary by request; those values stay with the page so the same external JavaScript can remain static whenever no other compatibility requirement needs Dynamic delivery. Personal/member details are always loaded with the page and are never stored in static files.</em>'."\n"; | |
| 278 | 281 | if(!$static_js_page_text_supported) |
| 279 | 282 | echo '<p class="ws-menu-page-error" style="margin:.75em 0 0;"><em><strong>Load with each WordPress page is unavailable:</strong> update s2Member Pro to a version that supports page-loaded JavaScript text. If your site needs different JavaScript text between languages before updating Pro, set Static JS Delivery to No so WordPress generates the JavaScript dynamically.</em></p>'."\n"; |
| 280 | 283 | echo '</td></tr>'."\n"; |
| 281 | 284 | |
| @@ -296,11 +299,21 @@ | ||
| 296 | 299 | echo '</select><br />'."\n"; |
| 297 | 300 | echo '<em>Applies when s2Member Pro is active. Separate delivery is the default; combining is an optional optimization that serves one generated CSS file and/or one generated JavaScript file instead of separate Framework and Pro files.</em>'."\n"; |
| 298 | 301 | echo '</td></tr>'."\n"; |
| 299 | 302 | |
| 303 | + //260912.0522 Expose the frontend-asset wait as a 1–60-second diagnostic control; changing it must not alter when the CSS/JS itself is enqueued or executed. | |
| 304 | + $asset_health_wait_seconds = (!empty($GLOBALS['WS_PLUGIN__']['s2member']['o']['asset_health_wait_seconds'])) ? max(1, min(60, (int)$GLOBALS['WS_PLUGIN__']['s2member']['o']['asset_health_wait_seconds'])) : 3; | |
| 305 | + echo '<tr><th><label for="ws-plugin--s2member-asset-health-wait-seconds">Wait Before Checking Frontend Assets (in seconds)</label></th></tr>'."\n"; | |
| 306 | + echo '<tr><td>'."\n"; | |
| 307 | + echo '<input type="number" min="1" max="60" step="1" name="ws_plugin__s2member_asset_health_wait_seconds" id="ws-plugin--s2member-asset-health-wait-seconds" value="'.esc_attr($asset_health_wait_seconds).'" /><br />'."\n"; | |
| 308 | + echo '<em>After the page finishes loading, s2Member waits this many seconds before checking that its frontend CSS and JavaScript have loaded and become active. This helps avoid false warnings when caching or optimization tools delay CSS/JS execution. Default: 3 seconds. Allowed range: 1–60.</em>'."\n"; | |
| 309 | + echo '</td></tr>'."\n"; | |
| 310 | + echo '</tbody></table>'."\n"; | |
| 311 | + | |
| 300 | 312 | $static_assets_enabled = !empty($GLOBALS['WS_PLUGIN__']['s2member']['o']['static_css']) || !empty($GLOBALS['WS_PLUGIN__']['s2member']['o']['static_js']); |
| 301 | - $static_asset_health = ($static_assets_enabled) ? c_ws_plugin__s2member_utils_assets::static_assets_health(TRUE) : array(); | |
| 302 | - echo '<tr><th>Static Asset Files</th></tr>'."\n"; | |
| 313 | + echo '<table class="form-table">'."\n"; | |
| 314 | + echo '<tbody>'."\n"; | |
| 315 | + echo '<tr><th>Refresh Static Assets</th></tr>'."\n"; | |
| 303 | 316 | echo '<tr><td>'."\n"; |
| 304 | 317 | echo '<span id="ws-plugin--s2member-refresh-static-assets-feedback" style="display:flex; align-items:center; gap:.5em; max-width:100%;"><button type="button" class="button" id="ws-plugin--s2member-refresh-static-assets" style="flex:0 0 auto;"'.((!$static_assets_enabled) ? ' disabled="disabled"' : '').'>Refresh Static Assets</button><span id="ws-plugin--s2member-refresh-static-assets-status" aria-live="polite" style="display:block; flex:1 1 auto; min-width:0;"></span></span><br />'."\n"; |
| 305 | 318 | echo '<em>'.(($static_assets_enabled) ? 'Creates new timestamps immediately for every active generated file. Separate Framework/Pro files keep independent build timestamps, while combined mode uses one timestamp per enabled asset type. Save option changes before using this button.' : 'Enable Static CSS Delivery or Static JS Delivery and save the options before using this button.').'</em>'."\n"; |
| 306 | 319 | |
| @@ -306,16 +319,212 @@ | ||
| 306 | 319 | |
| 307 | 320 | //260907.2203 Point site owners to the CSS/JS operational history from the asset controls. |
| 308 | 321 | echo '<p style="margin:.75em 0 0;"><em>When s2Member logging is enabled, important static and dynamic CSS/JS events such as generation, refreshes, delivery problems, fallbacks, and recovery are recorded in <code>css-js.log</code>. Routine page loads are not logged.</em></p>'."\n"; |
| 309 | 322 | |
| 310 | - if($static_asset_health) | |
| 311 | - echo '<p class="ws-menu-page-error" style="margin:.75em 0 0;"><em><strong>Static asset health:</strong> '.esc_html(implode(' ', $static_asset_health)).' Use Refresh Static Assets to recreate missing files.</em></p>'."\n"; | |
| 312 | 323 | echo '</td></tr>'."\n"; |
| 313 | 324 | echo '</tbody>'."\n"; |
| 314 | 325 | echo '</table>'."\n"; |
| 315 | 326 | //260904.0649 Keep Refresh tied to saved settings: disable it when no static type is active and while related controls have unsaved changes. |
| 316 | - echo '<script type="text/javascript">jQuery(function($){var $b=$("#ws-plugin--s2member-refresh-static-assets"),$s=$("#ws-plugin--s2member-refresh-static-assets-status"),$controls=$("#ws-plugin--s2member-static-css,#ws-plugin--s2member-static-css-minify,#ws-plugin--s2member-static-js,#ws-plugin--s2member-static-js-text,#ws-plugin--s2member-static-js-minify,#ws-plugin--s2member-static-assets-combine"),savedEnabled='.($static_assets_enabled ? 'true' : 'false').',dirtyMessage="Save option changes before refreshing.";function values(){return $controls.map(function(){return this.value;}).get().join("|");}var initial=values();function sync(){var changed=values()!==initial;$b.prop("disabled",!savedEnabled||changed);if(changed)$s.text(dirtyMessage);else if($s.text()===dirtyMessage)$s.text("");}$controls.on("change",sync);$b.on("click",function(){$b.prop("disabled",true);$s.text("Refreshing…");$.post(ajaxurl,{action:"ws_plugin__s2member_refresh_static_assets",_ajax_nonce:"'.esc_js(wp_create_nonce('ws-plugin--s2member-refresh-static-assets')).'"}).done(function(r){$s.text(r&&r.data&&r.data.message?r.data.message:"Static assets refreshed.");}).fail(function(xhr){var r=xhr.responseJSON;$s.text(r&&r.data&&r.data.message?r.data.message:"Static assets could not be refreshed.");}).always(function(){sync();});});sync();});</script>'."\n"; | |
| 327 | + //260911.1834 Refresh is an explicit admin recovery action; follow a completed server-side rebuild attempt with the existing trusted browser recheck so the displayed Health state updates without another click. | |
| 328 | + echo '<script type="text/javascript">jQuery(function($){var $b=$("#ws-plugin--s2member-refresh-static-assets"),$s=$("#ws-plugin--s2member-refresh-static-assets-status"),$controls=$("#ws-plugin--s2member-static-css,#ws-plugin--s2member-static-css-minify,#ws-plugin--s2member-static-js,#ws-plugin--s2member-static-js-text,#ws-plugin--s2member-static-js-minify,#ws-plugin--s2member-static-assets-combine"),savedEnabled='.($static_assets_enabled ? 'true' : 'false').',dirtyMessage="Save option changes before refreshing.";function values(){return $controls.map(function(){return this.value;}).get().join("|");}var initial=values();function sync(){var changed=values()!==initial;$b.prop("disabled",!savedEnabled||changed);if(changed)$s.text(dirtyMessage);else if($s.text()===dirtyMessage)$s.text("");}function recheck(message){var r=document.getElementById("ws-plugin--s2member-recheck-asset-health");$s.text(message+(r?" Rechecking delivery…":""));if(r)r.click();}$controls.on("change",sync);$b.on("click",function(){$b.prop("disabled",true);$s.text("Refreshing…");$.post(ajaxurl,{action:"ws_plugin__s2member_refresh_static_assets",_ajax_nonce:"'.esc_js(wp_create_nonce('ws-plugin--s2member-refresh-static-assets')).'"}).done(function(r){recheck(r&&r.data&&r.data.message?r.data.message:"Static assets refreshed.");}).fail(function(xhr){var r=xhr.responseJSON,m=r&&r.data&&r.data.message?r.data.message:"Static assets could not be refreshed.";if(r&&r.data)recheck(m);else $s.text(m);}).always(function(){sync();});});sync();});</script>'."\n"; | |
| 329 | + | |
| 330 | + $asset_health = c_ws_plugin__s2member_utils_assets::frontend_asset_health(TRUE); | |
| 331 | + $asset_health_http = get_option('ws_plugin__s2member_asset_http_health', array()); | |
| 332 | + $asset_health_log = get_option('ws_plugin__s2member_assets_health_log', array()); | |
| 333 | + $asset_health_last_generation = 0; | |
| 334 | + $asset_health_pending_rebuild = FALSE; | |
| 335 | + $asset_health_age = function($timestamp) | |
| 336 | + { | |
| 337 | + $seconds = max(0, time() - (int)$timestamp); | |
| 338 | + if($seconds < MINUTE_IN_SECONDS) | |
| 339 | + return '<1 min ago'; | |
| 340 | + if($seconds < HOUR_IN_SECONDS) | |
| 341 | + return floor($seconds / MINUTE_IN_SECONDS).' min ago'; | |
| 342 | + if($seconds < DAY_IN_SECONDS) | |
| 343 | + return floor($seconds / HOUR_IN_SECONDS).' hr ago'; | |
| 344 | + return floor($seconds / DAY_IN_SECONDS).' d ago'; | |
| 345 | + }; | |
| 346 | + foreach(array('css', 'js') as $_asset_type) | |
| 347 | + if(!empty($GLOBALS['WS_PLUGIN__']['s2member']['o']['static_'.$_asset_type])) | |
| 348 | + foreach(c_ws_plugin__s2member_utils_assets::static_asset_ids($_asset_type, 'all') as $_asset_id) | |
| 349 | + { | |
| 350 | + $_asset_build = abs((int)c_ws_plugin__s2member_utils_assets::static_asset_build($_asset_id)); | |
| 351 | + if($_asset_build > $asset_health_last_generation) | |
| 352 | + $asset_health_last_generation = $_asset_build; | |
| 353 | + } | |
| 354 | + $asset_health_last_generation_label = (!$static_assets_enabled) ? 'Not in use' : (($asset_health_last_generation > 0) ? $asset_health_age($asset_health_last_generation) : 'Not generated yet'); | |
| 355 | + //260912.1956 Loader rows depend on the complete trusted route check, so report that timestamp instead of a newer cheap partial probe when available. | |
| 356 | + $asset_health_last_check = (!empty($asset_health_http['full_checked'])) ? (int)$asset_health_http['full_checked'] : ((!empty($asset_health_http['checked'])) ? (int)$asset_health_http['checked'] : 0); | |
| 357 | + $asset_health_last_check_label = ($asset_health_last_check > 0) ? $asset_health_age($asset_health_last_check) : 'Not checked yet'; | |
| 358 | + $asset_health_last_issue_time = 0; | |
| 359 | + $asset_health_last_issue_result = ''; | |
| 360 | + $asset_health_last_issue_label = ''; | |
| 361 | + $asset_health_last_issue_detail = ''; | |
| 362 | + $asset_health_last_issue_page_id = 0; | |
| 363 | + $asset_health_last_issue_page_path = ''; | |
| 364 | + if(!empty($asset_health_log['last_issue']) && is_array($asset_health_log['last_issue'])) | |
| 365 | + { | |
| 366 | + //260911.1806 Prefer the dedicated historical snapshot so natural recovery and busy healthy traffic do not erase the useful cause from Last issue. | |
| 367 | + $asset_health_last_issue_time = (!empty($asset_health_log['last_issue']['time'])) ? (int)$asset_health_log['last_issue']['time'] : 0; | |
| 368 | + $asset_health_last_issue_result = (!empty($asset_health_log['last_issue']['result'])) ? (string)$asset_health_log['last_issue']['result'] : ''; | |
| 369 | + $asset_health_last_issue_label = (!empty($asset_health_log['last_issue']['label'])) ? (string)$asset_health_log['last_issue']['label'] : ''; | |
| 370 | + $asset_health_last_issue_detail = (!empty($asset_health_log['last_issue']['detail'])) ? (string)$asset_health_log['last_issue']['detail'] : ''; | |
| 371 | + $asset_health_last_issue_page_id = (!empty($asset_health_log['last_issue']['page_id'])) ? (int)$asset_health_log['last_issue']['page_id'] : 0; | |
| 372 | + $asset_health_last_issue_page_path = (!empty($asset_health_log['last_issue']['page_path'])) ? (string)$asset_health_log['last_issue']['page_path'] : ''; | |
| 373 | + } | |
| 374 | + else if(empty($asset_health_log['last_issue_cleared_at']) && !empty($asset_health_log['last_10_asset_loads']) && is_array($asset_health_log['last_10_asset_loads'])) | |
| 375 | + foreach(array_reverse($asset_health_log['last_10_asset_loads']) as $_asset_health_load) | |
| 376 | + if(!empty($_asset_health_load['time']) && !empty($_asset_health_load['result']) && (string)$_asset_health_load['result'] !== 'okay') | |
| 377 | + { | |
| 378 | + $asset_health_last_issue_time = (int)$_asset_health_load['time']; | |
| 379 | + $asset_health_last_issue_result = (string)$_asset_health_load['result']; | |
| 380 | + break; | |
| 381 | + } | |
| 382 | + $asset_health_last_issue_label = str_replace(array('Full WordPress Dynamic ', 's2Member-Only Dynamic ', 'JavaScript'), array('WordPress ', 's2Member-Only ', 'JS'), $asset_health_last_issue_label); | |
| 383 | + $asset_health_last_issue = 'None'; | |
| 384 | + if($asset_health_last_issue_time > 0 && $asset_health_last_issue_result !== '') | |
| 385 | + { | |
| 386 | + //260911.1834 Self-repaired missing files are historical issues, not degraded scored loads. | |
| 387 | + $_asset_health_result_labels = array('late' => 'late', 'fallback' => 'fallback', 'failed' => 'failed', 'fallback-unavailable' => 'fallback unavailable', 'repaired' => 'static asset missing'); | |
| 388 | + $_asset_health_result_label = (isset($_asset_health_result_labels[$asset_health_last_issue_result])) ? $_asset_health_result_labels[$asset_health_last_issue_result] : $asset_health_last_issue_result; | |
| 389 | + $asset_health_last_issue = $asset_health_age($asset_health_last_issue_time).' — '.(($asset_health_last_issue_label !== '') ? $asset_health_last_issue_label.' ' : '').$_asset_health_result_label; | |
| 390 | + } | |
| 391 | + $asset_health_last_issue_page_label = ''; | |
| 392 | + $asset_health_last_issue_page_url = ''; | |
| 393 | + if($asset_health_last_issue_page_id > 0) | |
| 394 | + { | |
| 395 | + $_asset_health_page_title = (string)get_the_title($asset_health_last_issue_page_id); | |
| 396 | + $asset_health_last_issue_page_label = (($_asset_health_page_title !== '') ? $_asset_health_page_title.' ' : '').'(#'.$asset_health_last_issue_page_id.')'; | |
| 397 | + $asset_health_last_issue_page_url = (string)get_permalink($asset_health_last_issue_page_id); | |
| 398 | + } | |
| 399 | + if($asset_health_last_issue_page_path !== '') | |
| 400 | + { | |
| 401 | + $asset_health_last_issue_page_label .= (($asset_health_last_issue_page_label !== '') ? ' — ' : '').$asset_health_last_issue_page_path; | |
| 402 | + if($asset_health_last_issue_page_url === '') | |
| 403 | + { | |
| 404 | + //260913.0454 REQUEST_URI already includes any WordPress subdirectory, so append it to the site origin instead of duplicating the home path. | |
| 405 | + $_asset_health_home_parts = c_ws_plugin__s2member_utils_urls::parse_url(home_url('/')); | |
| 406 | + if(is_array($_asset_health_home_parts) && !empty($_asset_health_home_parts['scheme']) && !empty($_asset_health_home_parts['host'])) | |
| 407 | + $asset_health_last_issue_page_url = $_asset_health_home_parts['scheme'].'://'.$_asset_health_home_parts['host'].((!empty($_asset_health_home_parts['port'])) ? ':'.(int)$_asset_health_home_parts['port'] : '').'/'.ltrim($asset_health_last_issue_page_path, '/'); | |
| 408 | + } | |
| 409 | + } | |
| 410 | + unset($_asset_type, $_asset_id, $_asset_build, $_asset_health_load, $_asset_health_result_labels, $_asset_health_result_label, $_asset_health_page_title, $_asset_health_home_parts); | |
| 411 | + //260910.0638 New Health Status section for easier review. | |
| 412 | + echo '<div class="ws-menu-page-hr"></div>'."\n"; | |
| 413 | + echo '<h3 id="ws-plugin--s2member-asset-health">CSS/JS Asset Health: <span class="ws-plugin--s2member-status-light ws-plugin--s2member-status-light-'.esc_attr($asset_health['status']).'" aria-hidden="true"></span>'.esc_html($asset_health['status_label']).'</h3>'."\n"; | |
| 414 | + echo '<table class="ws-plugin--s2member-status-table"><tbody>'."\n"; | |
| 415 | + //260911.0627 Keep healthy rows compact: the status light carries health while the value names the active delivery method; only exceptions add status text. | |
| 416 | + foreach($asset_health['rows'] as $_asset_health_row) | |
| 417 | + { | |
| 418 | + $_asset_health_pending = ((string)$_asset_health_row['status_label'] === 'Pending rebuild'); | |
| 419 | + $_asset_health_url = (!empty($_asset_health_row['url'])) ? (string)$_asset_health_row['url'] : ''; | |
| 420 | + $_asset_health_delivery = (!empty($_asset_health_row['delivery'])) ? (string)$_asset_health_row['delivery'] : ''; | |
| 421 | + $_asset_health_exception = ''; | |
| 422 | + if((string)$_asset_health_row['status_label'] !== 'Healthy' && (string)$_asset_health_row['status_label'] !== 'Using dynamic fallback') | |
| 423 | + { | |
| 424 | + $_asset_health_exception_labels = array('Rebuild issue' => 'Rebuild issue', 'Fallback check failed' => 'Check failed', 'Delivery check failed' => 'Check failed'); | |
| 425 | + $_asset_health_exception = (isset($_asset_health_exception_labels[$_asset_health_row['status_label']])) ? $_asset_health_exception_labels[$_asset_health_row['status_label']] : (string)$_asset_health_row['status_label']; | |
| 426 | + } | |
| 427 | + $_asset_health_value = $_asset_health_delivery.(($_asset_health_exception !== '') ? ' — '.$_asset_health_exception : ''); | |
| 428 | + $_asset_health_not_generated = ($_asset_health_delivery === 'Static' && $_asset_health_url === '' && $_asset_health_exception === '' && !$_asset_health_pending); | |
| 429 | + if($_asset_health_not_generated) | |
| 430 | + $_asset_health_value .= ' — Not generated yet'; | |
| 431 | + $_asset_health_light = ($_asset_health_pending || $_asset_health_not_generated) ? 'disabled' : (string)$_asset_health_row['status']; | |
| 432 | + $_asset_health_light_label = ($_asset_health_not_generated) ? 'Not generated yet' : (string)$_asset_health_row['status_label']; | |
| 433 | + if($_asset_health_pending || $_asset_health_not_generated) | |
| 434 | + $asset_health_pending_rebuild = TRUE; //260911.1924 Both neutral waiting states benefit from the shared automatic-rebuild explanation. | |
| 435 | + //260911.1924 A Pending rebuild URL names the previous generation, so hiding [open] avoids implying that it is the asset waiting to be created. | |
| 436 | + echo '<tr><th scope="row">'.esc_html($_asset_health_row['label']).':</th><td><span class="ws-plugin--s2member-status-light ws-plugin--s2member-status-light-'.esc_attr($_asset_health_light).'" style="margin-left:0; margin-right:.3em;" role="img" aria-label="'.esc_attr($_asset_health_light_label).'"></span>'.esc_html($_asset_health_value).((!$_asset_health_pending && $_asset_health_url !== '') ? '<span style="margin-left:.3em;">[<a href="'.esc_url($_asset_health_url).'" target="_blank" rel="noopener noreferrer">open</a>]</span>' : '').((!$_asset_health_pending && ($_asset_health_row['status'] !== 'healthy' || $_asset_health_delivery === 'Dynamic required') && (string)$_asset_health_row['detail'] !== '') ? '<br /><em>'.wp_kses_post($_asset_health_row['detail']).'</em>' : '').'</td></tr>'."\n"; | |
| 437 | + } | |
| 438 | + echo '<tr><th scope="row">Last generation:</th><td>'.esc_html($asset_health_last_generation_label).'</td></tr>'."\n"; | |
| 439 | + echo '<tr><th scope="row">Last trusted check:</th><td>'.esc_html($asset_health_last_check_label).'</td></tr>'."\n"; | |
| 440 | + $_asset_health_last_issue_clear = ($asset_health_last_issue_time > 0) ? ' <span>[<a href="#" class="ws-plugin--s2member-clear-asset-health" data-scope="last_issue">clear</a>]</span>' : ''; | |
| 441 | + $_asset_health_last_issue_page = ($asset_health_last_issue_page_label !== '') ? '<br />Page: '.esc_html($asset_health_last_issue_page_label).(($asset_health_last_issue_page_url !== '') ? ' [<a href="'.esc_url($asset_health_last_issue_page_url).'" target="_blank" rel="noopener noreferrer">open</a>]' : '') : ''; | |
| 442 | + echo '<tr><th scope="row">Last issue'.$_asset_health_last_issue_clear.':</th><td>'.esc_html($asset_health_last_issue).(($asset_health_last_issue_detail !== '') ? '<br />'.esc_html($asset_health_last_issue_detail) : '').$_asset_health_last_issue_page.'</td></tr>'."\n"; | |
| 443 | + echo '</tbody></table>'."\n"; | |
| 444 | + unset($_asset_health_row, $_asset_health_pending, $_asset_health_not_generated, $_asset_health_light, $_asset_health_light_label, $_asset_health_url, $_asset_health_delivery, $_asset_health_exception, $_asset_health_exception_labels, $_asset_health_value); | |
| 445 | + if($asset_health_pending_rebuild) | |
| 446 | + //260911.1924 Keep neutral waiting states easy to understand; normal admin use now also provides an automatic recovery opportunity before frontend traffic is needed. | |
| 447 | + echo '<p style="margin:.75em 0 0;"><em style="background:#e8f8fb; padding:2px 4px;">* Static assets marked Not generated yet or Pending rebuild will be created or rebuilt automatically on a later admin or frontend page-load, or immediately with Refresh Static Assets.</em></p>'."\n"; | |
| 448 | + //260910.0818 Recheck gives immediate confirmation after an admin fixes routing/access; preserve this panel/anchor so the refreshed result returns here. | |
| 449 | + echo '<p><button type="button" class="button" id="ws-plugin--s2member-recheck-asset-health">Recheck Asset Health</button> <span id="ws-plugin--s2member-recheck-asset-health-status" aria-live="polite"></span></p>'."\n"; | |
| 450 | + echo '<script type="text/javascript">(function(){var b=document.getElementById("ws-plugin--s2member-recheck-asset-health");if(!b||!window.URL||!window.history||!window.history.replaceState)return;b.addEventListener("click",function(){var u=new URL(window.location.href);u.searchParams.set("s2member-open-panel","frontend-static-assets");u.hash="ws-plugin--s2member-asset-health";window.history.replaceState(null,"",u.toString());},true);})();</script>'."\n"; | |
| 451 | + $asset_health_latest_issues = (!empty($asset_health['latest_issues']) && is_array($asset_health['latest_issues'])) ? $asset_health['latest_issues'] : array(); | |
| 452 | + $asset_health_latest_issues_blocks = array(); | |
| 453 | + $_asset_health_result_labels = array('late' => 'Late', 'fallback' => 'Fallback', 'failed' => 'Failed', 'fallback-unavailable' => 'Fallback unavailable', 'repaired' => 'Static asset missing / repaired'); | |
| 454 | + foreach($asset_health_latest_issues as $_asset_health_issue) | |
| 455 | + { | |
| 456 | + if(!is_array($_asset_health_issue)) | |
| 457 | + continue; | |
| 458 | + $_issue_result = (!empty($_asset_health_issue['result'])) ? (string)$_asset_health_issue['result'] : 'issue'; | |
| 459 | + $_issue_result_label = (isset($_asset_health_result_labels[$_issue_result])) ? $_asset_health_result_labels[$_issue_result] : ucfirst(str_replace('-', ' ', $_issue_result)); | |
| 460 | + $_issue_label = (!empty($_asset_health_issue['label'])) ? (string)$_asset_health_issue['label'] : 'Asset'; | |
| 461 | + $_issue_lines = array($_issue_label.' — '.$_issue_result_label); | |
| 462 | + if(!empty($_asset_health_issue['detail'])) | |
| 463 | + $_issue_lines[] = (string)$_asset_health_issue['detail']; | |
| 464 | + //260913.0305 Keep Latest Issues compact and copyable: show the full queryless frontend URL, then one precise occurrence timestamp per line. | |
| 465 | + $_issue_page_path = (!empty($_asset_health_issue['page_path'])) ? (string)$_asset_health_issue['page_path'] : ''; | |
| 466 | + $_issue_page_url = ''; | |
| 467 | + if($_issue_page_path !== '') | |
| 468 | + { | |
| 469 | + $_issue_home_parts = c_ws_plugin__s2member_utils_urls::parse_url(home_url('/')); | |
| 470 | + if(is_array($_issue_home_parts) && !empty($_issue_home_parts['scheme']) && !empty($_issue_home_parts['host'])) | |
| 471 | + $_issue_page_url = $_issue_home_parts['scheme'].'://'.$_issue_home_parts['host'].((!empty($_issue_home_parts['port'])) ? ':'.(int)$_issue_home_parts['port'] : '').'/'.ltrim($_issue_page_path, '/'); | |
| 472 | + } | |
| 473 | + if($_issue_page_url !== '') | |
| 474 | + $_issue_lines[] = 'Page: '.$_issue_page_url; | |
| 475 | + $_issue_count = (!empty($_asset_health_issue['count'])) ? max(1, (int)$_asset_health_issue['count']) : 1; | |
| 476 | + $_issue_times = array(); | |
| 477 | + if(!empty($_asset_health_issue['times']) && is_array($_asset_health_issue['times'])) | |
| 478 | + foreach($_asset_health_issue['times'] as $_issue_time) | |
| 479 | + if((int)$_issue_time > 0) | |
| 480 | + $_issue_times[] = date_i18n('Y-m-d H:i:s', (int)$_issue_time); | |
| 481 | + if($_issue_times) | |
| 482 | + { | |
| 483 | + if($_issue_count > count($_issue_times)) | |
| 484 | + $_issue_lines[] = 'Seen '.number_format_i18n($_issue_count).' times. Latest '.number_format_i18n(count($_issue_times)).' occurrences:'; | |
| 485 | + else | |
| 486 | + $_issue_lines[] = 'Seen '.number_format_i18n($_issue_count).' '.(($_issue_count === 1) ? 'time' : 'times').':'; | |
| 487 | + $_issue_lines = array_merge($_issue_lines, $_issue_times); | |
| 488 | + } | |
| 489 | + else | |
| 490 | + $_issue_lines[] = 'Seen '.number_format_i18n($_issue_count).' '.(($_issue_count === 1) ? 'time.' : 'times.'); | |
| 491 | + $asset_health_latest_issues_blocks[] = implode("\n", $_issue_lines); | |
| 492 | + } | |
| 493 | + $asset_health_latest_issues_text = ($asset_health_latest_issues_blocks) ? implode("\n\n", $asset_health_latest_issues_blocks) : 'No Asset Health issues are currently recorded.'; | |
| 494 | + echo '<h3 style="margin:.75em 0 0;">Latest Issues'.(($asset_health_latest_issues) ? ' <span>[<a href="#" class="ws-plugin--s2member-clear-asset-health" data-scope="latest_issues">clear</a>]</span>' : '').'</h3>'."\n"; | |
| 495 | + echo '<textarea id="ws-plugin--s2member-asset-health-latest-issues" rows="5" wrap="on" spellcheck="false" readonly="readonly" style="width:100%; max-width:100%; box-sizing:border-box; resize:vertical; box-shadow:inset 0 0 5px rgba(0,0,0,0.5); background:#EEEEEE; color:#000000; overflow-y:scroll;">'.esc_textarea($asset_health_latest_issues_text).'</textarea>'."\n"; | |
| 496 | + echo '<p style="margin:.5em 0 0;"><em>s2Member keeps the latest 10 distinct Asset Health issues here until cleared. Repeated occurrences of the same issue on the same page are grouped together and keep up to 10 recent occurrence times. Clearing Last issue or Latest Issues does not change Current Health or its recent scoring history. Recheck Asset Health safely resets recent scoring history after current delivery and its fallback verify healthy. Enable s2Member logging for more detailed CSS/JavaScript troubleshooting history.</em></p>'."\n"; | |
| 497 | + //260913.0056 Keep explicit historical clearing separate from Recheck; neither clear action changes rolling Health or current delivery state. | |
| 498 | + echo '<script type="text/javascript">jQuery(function($){$(document).on("click","a.ws-plugin--s2member-clear-asset-health",function(e){e.preventDefault();var $a=$(this),scope=$a.data("scope");if(!scope)return;$a.css("pointer-events","none");$.post(ajaxurl,{action:"ws_plugin__s2member_clear_asset_health_details",_ajax_nonce:"'.esc_js(wp_create_nonce('ws-plugin--s2member-clear-asset-health-details')).'",scope:scope}).done(function(){window.location.reload();}).fail(function(xhr){var r=xhr.responseJSON,m=r&&r.data&&r.data.message?r.data.message:"Asset Health details could not be cleared.";window.alert(m);$a.css("pointer-events","");});});});</script>'."\n"; | |
| 499 | + unset($asset_health_latest_issues, $asset_health_latest_issues_blocks, $asset_health_latest_issues_text, $_asset_health_issue, $_asset_health_result_labels, $_issue_result, $_issue_result_label, $_issue_label, $_issue_lines, $_issue_page_path, $_issue_page_url, $_issue_home_parts, $_issue_count, $_issue_times, $_issue_time); | |
| 500 | + //260911.2004 Explain Asset Health in site-owner language, with scores first so the relationship between each result/status and its score is easy to scan. | |
| 501 | + echo '<h3 style="margin:1.5em 0 0;">How is this health status calculated? (<a href="#" onclick="jQuery(\'div#ws-plugin--s2member-asset-health-calculation\').toggle(); return false;" class="ws-dotted-link">click here</a>)</h3>'."\n"; | |
| 502 | + echo '<div id="ws-plugin--s2member-asset-health-calculation" style="margin-top:10px; display:none;">'."\n"; | |
| 503 | + echo '<p><em><strong>Page-load results:</strong> Each relevant frontend page-load gets one result with a score from 1 to 4. If a page uses several s2Member CSS or JavaScript assets, the least healthy result is used for that page.</em></p>'."\n"; | |
| 504 | + echo '<ul style="margin:.5em 0 1em 2em;">'."\n"; | |
| 505 | + echo '<li><em><strong>4 - Okay:</strong> The expected CSS and JavaScript became active normally.</em></li>'."\n"; | |
| 506 | + echo '<li><em><strong>3 - Late:</strong> s2Member could not confirm all expected assets within the configured wait time. They may still have loaded afterward, so this is a warning rather than a failure.</em></li>'."\n"; | |
| 507 | + echo '<li><em><strong>2 - Fallback:</strong> The preferred delivery method was unavailable, but s2Member recovered using a working fallback.</em></li>'."\n"; | |
| 508 | + echo '<li><em><strong>1 - Failed:</strong> Working delivery could not be confirmed.</em></li>'."\n"; | |
| 509 | + echo '</ul>'."\n"; | |
| 510 | + //260911.2004 Keep neutral static-asset maintenance states separate from delivery problems and identify the manual control explicitly. | |
| 511 | + echo '<p><em><strong>Not generated yet / Pending rebuild:</strong> These are not delivery failures. The static asset either has not been created yet or needs to be rebuilt after a configuration, plugin, upgrade, or other change. s2Member normally handles this automatically on an admin or frontend page-load. You can also rebuild them immediately with the <strong>Refresh Static Assets</strong> button in this section.</em></p>'."\n"; | |
| 512 | + echo '<p><em><strong>Current Health:</strong> s2Member combines recent results from the last 10 page-loads with requests from the last 10 minutes. Newer activity carries more weight, so Health can improve again as successful requests replace older problems.</em></p>'."\n"; | |
| 513 | + //260912.1956 The Full WordPress route is the safety net for static and s2Member-Only delivery, so a trusted failure there should lower Health without pretending that visitors actually used a fallback. | |
| 514 | + echo '<p><em><strong>Fallback health:</strong> When preferred delivery is working but its Full WordPress fallback is unavailable, that failed fallback check is averaged into Current Health because the safety net would not be available if needed.</em></p>'."\n"; | |
| 515 | + echo '<p><em><strong>Health status:</strong> The combined result is shown as one of these health levels:</em></p>'."\n"; | |
| 516 | + echo '<ul style="margin:.5em 0 1em 2em;">'."\n"; | |
| 517 | + echo '<li><em><strong>4.00 – 3.51 - Healthy</strong></em></li>'."\n"; | |
| 518 | + echo '<li><em><strong>3.50 – 2.51 - Recent issue</strong></em></li>'."\n"; | |
| 519 | + echo '<li><em><strong>2.50 – 1.51 - Working, review suggested</strong></em></li>'."\n"; | |
| 520 | + echo '<li><em><strong>1.50 – 1.00 - Needs attention</strong></em></li>'."\n"; | |
| 521 | + echo '</ul>'."\n"; | |
| 522 | + echo '<p><em><strong>Examples:</strong> One Late result followed by normal page-loads may briefly show Recent issue and then return to Healthy. Repeated Fallback results can move Health to Working, review suggested. Repeated Failed results can move it to Needs attention.</em></p>'."\n"; | |
| 523 | + //260911.2004 Describe the six-hour review using the visible Healthy status instead of internal color/score terminology. | |
| 524 | + echo '<p><em><strong>Longer-term review:</strong> A Late result is only a diagnostic signal; it does not make s2Member load another copy of the CSS or JavaScript. A current Needs attention status can trigger an immediate admin notice. If Health does not return to Healthy within six hours, s2Member also reviews activity across that period and may suggest a review when the longer-term result remains substantially degraded.</em></p>'."\n"; | |
| 317 | 525 | echo '</div>'."\n"; |
| 526 | + echo '</div>'."\n"; | |
| 318 | 527 | |
| 319 | 528 | echo '<div style="margin:1em 0;">'."\n"; |
| 320 | 529 | echo '<div class="ws-menu-page-hr"></div>'."\n"; |
| 321 | 530 | echo '</div>'."\n"; |
| @@ -639,9 +848,9 @@ | ||
| 639 | 848 | echo '<code>custom="'.esc_html($_SERVER["HTTP_HOST"]).'|christmas-promo"</code>'."\n"; |
| 640 | 849 | |
| 641 | 850 | echo (!is_multisite() || !c_ws_plugin__s2member_utils_conds::is_multisite_farm() || is_main_site()) ? |
| 642 | 851 | '<div class="ws-menu-page-hr"></div>'."\n". |
| 643 | - '<p style="margin:0;"><strong>PHP Code:</strong> It is also possible to use PHP tags—optional (for developers). If you use PHP tags, please run a test email with <code><?php print_r(get_defined_vars()); ?></code>. This will give you a full list of all PHP variables available to you in this email. The <code>$user</code> variable is the most important one. It\'s an instance of the <a href="https://s2member.com/r/wordpress-codex-wp_user/" target="_blank" rel="external"><code>WP_User</code></a> class (e.g., <code>$user->ID</code>, <code>$user->has_cap()</code>, etc). Please remember that emails are sent in plain text format.</p>'."\n" | |
| 852 | + '<p style="margin:0;"><strong>Advanced Customization:</strong> Further customization is possible with the <code>ws_plugin__s2member_welcome_email_sbj</code> and <code>ws_plugin__s2member_welcome_email_msg</code> filters from a plugin or theme.</p>'."\n" | |
| 644 | 853 | : ''; |
| 645 | 854 | echo '</td>'."\n"; |
| 646 | 855 | |
| 647 | 856 | echo '</tr>'."\n"; |
| @@ -748,9 +957,9 @@ | ||
| 748 | 957 | echo '<code>custom="'.esc_html($_SERVER["HTTP_HOST"]).'|christmas-promo"</code>'."\n"; |
| 749 | 958 | |
| 750 | 959 | echo (!is_multisite() || !c_ws_plugin__s2member_utils_conds::is_multisite_farm() || is_main_site()) ? |
| 751 | 960 | '<div class="ws-menu-page-hr"></div>'."\n". |
| 752 | - '<p style="margin:0;"><strong>PHP Code:</strong> It is also possible to use PHP tags—optional (for developers). If you use PHP tags, please run a test email with <code><?php print_r(get_defined_vars()); ?></code>. This will give you a full list of all PHP variables available to you in this email. The <code>$user</code> variable is the most important one. It\'s an instance of the <a href="https://s2member.com/r/wordpress-codex-wp_user/" target="_blank" rel="external"><code>WP_User</code></a> class (e.g., <code>$user->ID</code>, <code>$user->has_cap()</code>, etc). Please remember that emails are sent in plain text format.</p>'."\n" | |
| 961 | + '<p style="margin:0;"><strong>Advanced Customization:</strong> Further customization of the recipients, subject, and message is possible with the <code>ws_plugin__s2member_admin_new_user_email_recipients</code>, <code>ws_plugin__s2member_admin_new_user_email_sbj</code>, and <code>ws_plugin__s2member_admin_new_user_email_msg</code> filters from a plugin or theme.</p>'."\n" | |
| 753 | 962 | : ''; |
| 754 | 963 | echo '</td>'."\n"; |
| 755 | 964 | |
| 756 | 965 | echo '</tr>'."\n"; |
| @@ -1504,9 +1713,9 @@ | ||
| 1504 | 1713 | |
| 1505 | 1714 | echo '<div class="ws-menu-page-section ws-plugin--s2member-sc-get-shortcode-section">'."\n"; |
| 1506 | 1715 | echo '<h3>Shortcode User Fields Whitelist (optional)</h3>'."\n"; |
| 1507 | 1716 | echo '<p>Enter a comma-separated list of user fields that these shortcodes may display from a user account other than the person currently viewing the page. This applies to <code>[s2Get user_id="" /]</code> and <code>[s2Member-List show_fields="" /]</code>.</p>'."\n"; |
| 1508 | - echo '<p>Only allow fields that are appropriate to reveal about other user accounts wherever these shortcodes are used. If either shortcode tries to display an unlisted field from another user account, s2Member will show administrators a security notice with the fields that need review.</p>'."\n"; | |
| 1717 | + echo '<p>Only allow fields that are appropriate to reveal about other user accounts wherever these shortcodes are used. If either shortcode tries to display an unlisted field from another user account, that cross-user field value will be blocked and s2Member will show administrators a security notice with the fields that need review.</p>'."\n"; | |
| 1509 | 1718 | do_action("ws_plugin__s2member_during_gen_ops_page_during_left_sections_during_sc_s2get_shortcode", get_defined_vars()); |
| 1510 | 1719 | |
| 1511 | 1720 | echo '<table class="form-table">'."\n"; |
| 1512 | 1721 | echo '<tbody>'."\n"; |