PluginProbe
s2Member – Excellent for All Kinds of Memberships, Content Restriction Paywalls & Member Access Subscriptions / 260927
s2Member – Excellent for All Kinds of Memberships, Content Restriction Paywalls & Member Access Subscriptions v260927
260927 260917 260913 260909 260829 260814 260805 110710 110731 110812 110815 110912 110913 110915 110926 110927 111002 111003 111011 111017 111029 111105 111206 111216 111220 All 190 releases
← All changes | src/includes/menu-pages/gen-ops.inc.php +329 -4 260829 → 260927 View file →
@@ -166,9 +166,10 @@
166 166 if(apply_filters("ws_plugin__s2member_during_gen_ops_page_during_left_sections_display_lazy_load", (!is_multisite() || !c_ws_plugin__s2member_utils_conds::is_multisite_farm() || is_main_site() || is_super_admin()), get_defined_vars()))
167 167 {
168 168 do_action("ws_plugin__s2member_during_gen_ops_page_during_left_sections_before_lazy_load", get_defined_vars());
169 169
170 - echo '<div class="ws-menu-page-group" title="Performance &amp; Caching">'."\n";
170 + $open_performance_panel = !empty($_GET['s2member-open-panel']) && in_array((string)$_GET['s2member-open-panel'], array('frontend-static-assets', 'dynamic-asset-loader'), TRUE);
171 + echo '<div class="ws-menu-page-group" title="Performance &amp; Caching"'.(($open_performance_panel) ? ' default-state="open"' : '').'>'."\n";
171 172
172 173 echo '<div class="ws-menu-page-section ws-plugin--s2member-lazy-load-section">'."\n";
173 174 echo '<h3>CSS/JS Lazy Loading (Client-Side Libraries)</h3>'."\n";
174 175 echo '<p>By default, s2Member will load it\'s CSS/JS libraries on every page of your site. However, you may wish to enable lazy-loading here (i.e., only load when absolutely necessary).</p>'."\n";
@@ -203,8 +204,332 @@
203 204 echo '<div style="margin:1em 0;">'."\n";
204 205 echo '<div class="ws-menu-page-hr"></div>'."\n";
205 206 echo '</div>'."\n";
206 207
208 + //260904.2014 Dynamic delivery applies independently of static delivery, so give its loader choice a separate section.
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 + echo '<h3>Dynamic CSS/JS Loader</h3>'."\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 + echo '<table class="form-table">'."\n";
213 + echo '<tbody>'."\n";
214 +
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 + echo '<tr><th><label for="ws-plugin--s2member-dynamic-asset-loader">Dynamic CSS/JS Loader</label></th></tr>'."\n";
217 + echo '<tr><td>'."\n";
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-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 + echo '</select><br />'."\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";
224 + echo '</td></tr>'."\n";
225 +
226 + echo '</tbody>'."\n";
227 + echo '</table>'."\n";
228 + echo '</div>'."\n";
229 +
230 + echo '<div style="margin:1em 0;">'."\n";
231 + echo '<div class="ws-menu-page-hr"></div>'."\n";
232 + echo '</div>'."\n";
233 +
234 + //260904.2014 Keep static CSS/JS delivery and its related optimizations together in a clearly named section.
235 + echo '<div id="ws-plugin--s2member-static-assets" class="ws-menu-page-section ws-plugin--s2member-static-assets-section">'."\n";
236 + echo '<h3>CSS/JS Delivery &amp; Optimization (Beta)</h3>'."\n";
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";
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";
239 +
240 + echo '<table class="form-table">'."\n";
241 + echo '<tbody>'."\n";
242 + echo '<tr><th><label for="ws-plugin--s2member-static-css">Static CSS Delivery?</label></th></tr>'."\n";
243 + echo '<tr><td>'."\n";
244 + echo '<select name="ws_plugin__s2member_static_css" id="ws-plugin--s2member-static-css" onchange="jQuery(\'#ws-plugin--s2member-static-css-minify\').prop(\'disabled\', this.value !== \'1\');">'."\n";
245 + echo '<option value="0"'.((!$GLOBALS['WS_PLUGIN__']['s2member']['o']['static_css']) ? ' selected="selected"' : '').'>No (use dynamic CSS)</option>'."\n";
246 + echo '<option value="1"'.(($GLOBALS['WS_PLUGIN__']['s2member']['o']['static_css']) ? ' selected="selected"' : '').'>Yes (beta; generate and serve static CSS)</option>'."\n";
247 + echo '</select><br />'."\n";
248 + echo '<em>Generates timestamped Framework CSS and, when Pro is active, a separate Pro/gateway stylesheet by default. Generation failures or incompatible custom CSS hooks use legacy dynamic CSS.</em>'."\n";
249 + echo '</td></tr>'."\n";
250 +
251 + echo '<tr><th><label for="ws-plugin--s2member-static-css-minify">Minify Static CSS?</label></th></tr>'."\n";
252 + echo '<tr><td>'."\n";
253 + echo '<select name="ws_plugin__s2member_static_css_minify" id="ws-plugin--s2member-static-css-minify"'.((!$GLOBALS['WS_PLUGIN__']['s2member']['o']['static_css']) ? ' disabled="disabled"' : '').'>'."\n";
254 + echo '<option value="0"'.((empty($GLOBALS['WS_PLUGIN__']['s2member']['o']['static_css']) || empty($GLOBALS['WS_PLUGIN__']['s2member']['o']['static_css_minify'])) ? ' selected="selected"' : '').'>No (keep generated CSS readable)</option>'."\n";
255 + echo '<option value="1"'.((!empty($GLOBALS['WS_PLUGIN__']['s2member']['o']['static_css']) && !empty($GLOBALS['WS_PLUGIN__']['s2member']['o']['static_css_minify'])) ? ' selected="selected"' : '').'>Yes (beta; minify generated CSS)</option>'."\n";
256 + echo '</select><br />'."\n";
257 + echo '<em>Used only with Static CSS Delivery. Original source files remain readable and unchanged.</em>'."\n";
258 + echo '</td></tr>'."\n";
259 +
260 + echo '<tr><th><label for="ws-plugin--s2member-static-js">Static JS Delivery?</label></th></tr>'."\n";
261 + echo '<tr><td>'."\n";
262 + echo '<select name="ws_plugin__s2member_static_js" id="ws-plugin--s2member-static-js" onchange="jQuery(\'#ws-plugin--s2member-static-js-minify\').prop(\'disabled\', this.value !== \'1\');">'."\n";
263 + echo '<option value="0"'.((!$GLOBALS['WS_PLUGIN__']['s2member']['o']['static_js']) ? ' selected="selected"' : '').'>No (use dynamic JavaScript)</option>'."\n";
264 + echo '<option value="1"'.(($GLOBALS['WS_PLUGIN__']['s2member']['o']['static_js']) ? ' selected="selected"' : '').'>Yes (beta; generate and serve static JavaScript)</option>'."\n";
265 + echo '</select><br />'."\n";
266 + echo '<em>Generates timestamped Framework JavaScript and, when Pro is active, a separate Pro/gateway script by default. Personal/member details always remain page-specific and are never stored in static files. WordPress 4.2–4.4 automatically keep dynamic JavaScript.</em>'."\n";
267 + echo '</td></tr>'."\n";
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';
270 + $static_js_page_text_supported = c_ws_plugin__s2member_utils_assets::static_js_page_text_supported();
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";
273 + echo '<tr><td>'."\n";
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";
275 + echo '<select name="ws_plugin__s2member_static_js_text" id="ws-plugin--s2member-static-js-text">'."\n";
276 + echo '<option value="static"'.(($static_js_text === 'static') ? ' selected="selected"' : '').'>Load JavaScript text with the static files</option>'."\n";
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";
278 + echo '</select><br />'."\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";
281 + if(!$static_js_page_text_supported)
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";
283 + echo '</td></tr>'."\n";
284 +
285 + echo '<tr><th><label for="ws-plugin--s2member-static-js-minify">Minify Static JS?</label></th></tr>'."\n";
286 + echo '<tr><td>'."\n";
287 + echo '<select name="ws_plugin__s2member_static_js_minify" id="ws-plugin--s2member-static-js-minify"'.((!$GLOBALS['WS_PLUGIN__']['s2member']['o']['static_js']) ? ' disabled="disabled"' : '').'>'."\n";
288 + echo '<option value="0"'.((empty($GLOBALS['WS_PLUGIN__']['s2member']['o']['static_js']) || empty($GLOBALS['WS_PLUGIN__']['s2member']['o']['static_js_minify'])) ? ' selected="selected"' : '').'>No (keep generated JavaScript readable)</option>'."\n";
289 + echo '<option value="1"'.((!empty($GLOBALS['WS_PLUGIN__']['s2member']['o']['static_js']) && !empty($GLOBALS['WS_PLUGIN__']['s2member']['o']['static_js_minify'])) ? ' selected="selected"' : '').'>Yes (beta; minify generated JavaScript)</option>'."\n";
290 + echo '</select><br />'."\n";
291 + echo '<em>Used only with Static JS Delivery. s2Member generates from readable JavaScript sources; developers do not maintain parallel minified files.</em>'."\n";
292 + echo '</td></tr>'."\n";
293 +
294 + echo '<tr><th><label for="ws-plugin--s2member-static-assets-combine">Combine Framework + Pro Static Assets?</label></th></tr>'."\n";
295 + echo '<tr><td>'."\n";
296 + echo '<select name="ws_plugin__s2member_static_assets_combine" id="ws-plugin--s2member-static-assets-combine">'."\n";
297 + echo '<option value="0"'.((empty($GLOBALS['WS_PLUGIN__']['s2member']['o']['static_assets_combine'])) ? ' selected="selected"' : '').'>No (keep Framework and Pro files separate)</option>'."\n";
298 + echo '<option value="1"'.((!empty($GLOBALS['WS_PLUGIN__']['s2member']['o']['static_assets_combine'])) ? ' selected="selected"' : '').'>Yes (combine each enabled type to reduce requests)</option>'."\n";
299 + echo '</select><br />'."\n";
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";
301 + echo '</td></tr>'."\n";
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 +
312 + $static_assets_enabled = !empty($GLOBALS['WS_PLUGIN__']['s2member']['o']['static_css']) || !empty($GLOBALS['WS_PLUGIN__']['s2member']['o']['static_js']);
313 + echo '<table class="form-table">'."\n";
314 + echo '<tbody>'."\n";
315 + echo '<tr><th>Refresh Static Assets</th></tr>'."\n";
316 + echo '<tr><td>'."\n";
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";
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";
319 +
320 + //260907.2203 Point site owners to the CSS/JS operational history from the asset controls.
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";
322 +
323 + echo '</td></tr>'."\n";
324 + echo '</tbody>'."\n";
325 + echo '</table>'."\n";
326 + //260904.0649 Keep Refresh tied to saved settings: disable it when no static type is active and while related controls have unsaved changes.
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 &ndash; 3.51 - Healthy</strong></em></li>'."\n";
518 + echo '<li><em><strong>3.50 &ndash; 2.51 - Recent issue</strong></em></li>'."\n";
519 + echo '<li><em><strong>2.50 &ndash; 1.51 - Working, review suggested</strong></em></li>'."\n";
520 + echo '<li><em><strong>1.50 &ndash; 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";
525 + echo '</div>'."\n";
526 + echo '</div>'."\n";
527 +
528 + echo '<div style="margin:1em 0;">'."\n";
529 + echo '<div class="ws-menu-page-hr"></div>'."\n";
530 + echo '</div>'."\n";
531 +
207 532 echo '<div class="ws-menu-page-section ws-plugin--s2member-no-cache-headers-section">'."\n";
208 533 echo '<h3>No-Cache Headers Behavior (beta)</h3>'."\n";
209 534 echo '<p>s2Member can render different output depending on logged-in state, access level, conditionals (e.g., <code>[s2If]</code>), and nonce-based forms. If a cache stores a page generated under one context and serves it to another, visitors may see incorrect content or stale nonces. These options control when s2Member sends HTTP no-cache headers (<code>Cache-Control: no-cache</code>) to reduce that risk while allowing caching where appropriate.</p>'."\n";
210 535 do_action("ws_plugin__s2member_during_gen_ops_page_during_left_sections_during_no_cache_headers", get_defined_vars());
@@ -523,9 +848,9 @@
523 848 echo '<code>custom="'.esc_html($_SERVER["HTTP_HOST"]).'|christmas-promo"</code>'."\n";
524 849
525 850 echo (!is_multisite() || !c_ws_plugin__s2member_utils_conds::is_multisite_farm() || is_main_site()) ?
526 851 '<div class="ws-menu-page-hr"></div>'."\n".
527 - '<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>&lt;?php print_r(get_defined_vars()); ?&gt;</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"
528 853 : '';
529 854 echo '</td>'."\n";
530 855
531 856 echo '</tr>'."\n";
@@ -632,9 +957,9 @@
632 957 echo '<code>custom="'.esc_html($_SERVER["HTTP_HOST"]).'|christmas-promo"</code>'."\n";
633 958
634 959 echo (!is_multisite() || !c_ws_plugin__s2member_utils_conds::is_multisite_farm() || is_main_site()) ?
635 960 '<div class="ws-menu-page-hr"></div>'."\n".
636 - '<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>&lt;?php print_r(get_defined_vars()); ?&gt;</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"
637 962 : '';
638 963 echo '</td>'."\n";
639 964
640 965 echo '</tr>'."\n";
@@ -1388,9 +1713,9 @@
1388 1713
1389 1714 echo '<div class="ws-menu-page-section ws-plugin--s2member-sc-get-shortcode-section">'."\n";
1390 1715 echo '<h3>Shortcode User Fields Whitelist (optional)</h3>'."\n";
1391 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";
1392 - 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";
1393 1718 do_action("ws_plugin__s2member_during_gen_ops_page_during_left_sections_during_sc_s2get_shortcode", get_defined_vars());
1394 1719
1395 1720 echo '<table class="form-table">'."\n";
1396 1721 echo '<tbody>'."\n";