| @@ -94,8 +94,216 @@ | ||
| 94 | 94 | do_action('ws_plugin__s2member_after_display_admin_notice', get_defined_vars()); |
| 95 | 95 | } |
| 96 | 96 | |
| 97 | 97 | /** |
| 98 | + * Displays a branded s2Member administrative notice. | |
| 99 | + * | |
| 100 | + * @package s2Member\Admin_Notices | |
| 101 | + * @since 260904.1923 | |
| 102 | + * | |
| 103 | + * @param string $title Notice title. | |
| 104 | + * @param string $message Main notice message. | |
| 105 | + * @param bool $error Optional. True for an error notice; otherwise an informational notice. | |
| 106 | + * @param string $dismiss_url Optional explicit URL that dismisses the current notice incident. | |
| 107 | + */ | |
| 108 | + public static function display_branded_notice($title = '', $message = '', $error = FALSE, $dismiss_url = '') | |
| 109 | + { | |
| 110 | + $title = trim((string)$title); | |
| 111 | + $message = trim((string)$message); | |
| 112 | + $dismiss_url = trim((string)$dismiss_url); | |
| 113 | + if(!$message) | |
| 114 | + return; | |
| 115 | + | |
| 116 | + $_logo_url = $GLOBALS['WS_PLUGIN__']['s2member']['c']['dir_url'].'/src/images/logo-square-big.png'; | |
| 117 | + $_notice_class = ($error) ? 'notice notice-error' : 'notice notice-info'; //260910.0818 Use WordPress's standard error style for the immediate Red problem and the quieter informational style for the persistent Orange review notice. | |
| 118 | + $_dismiss = (($dismiss_url !== '') ? '<a href="'.esc_url($dismiss_url).'" title="Dismiss this incident" style="position:absolute; top:8px; right:10px; text-decoration:none;">Dismiss</a>' : ''); //260910.0709 The caller owns the nonce-protected incident URL so dismissal follows incident identity, not mutable notice text. | |
| 119 | + | |
| 120 | + //260909.2021 Keep incident dismissal explicit so changing notice text cannot accidentally make a persistent problem look like a new one. | |
| 121 | + echo '<div class="'.esc_attr($_notice_class).'" style="position:relative; margin:0 0 15px 2px !important; padding:8px '.(($dismiss_url !== '') ? '60px' : '8px').' 8px 8px !important;">'.$_dismiss.'<table cellspacing="0" cellpadding="0"><tr><td style="vertical-align:top; padding:0 10px 0 0;"><img src="'.esc_url($_logo_url).'" alt="" width="40" height="40" style="border:0;" /></td><td style="vertical-align:top;">'.(($title !== '') ? '<strong>'.esc_html($title).'</strong><br />' : '').wp_kses_post($message).'</td></tr></table></div>'; | |
| 122 | + } | |
| 123 | + | |
| 124 | + /** | |
| 125 | + * Warns administrators when Pro is too old to contain the current Pro updater behavior. | |
| 126 | + * | |
| 127 | + * @package s2Member\Admin_Notices | |
| 128 | + * @since 260917.0425 | |
| 129 | + * | |
| 130 | + * @attaches-to `add_action('admin_notices');` | |
| 131 | + * @attaches-to `add_action('user_admin_notices');` | |
| 132 | + * @attaches-to `add_action('network_admin_notices');` | |
| 133 | + */ | |
| 134 | + public static function outdated_pro_notice() | |
| 135 | + { | |
| 136 | + if(!current_user_can('update_plugins') || !defined('WS_PLUGIN__S2MEMBER_PRO_VERSION') || !defined('WS_PLUGIN__S2MEMBER_VERSION')) | |
| 137 | + return; | |
| 138 | + | |
| 139 | + //260917.0425 v260913 introduced the current background Pro updater flow; Framework owns this fallback warning only for older Pro releases. | |
| 140 | + $_current_updater_version = '260913'; | |
| 141 | + if(version_compare(WS_PLUGIN__S2MEMBER_PRO_VERSION, $_current_updater_version, '>=') || !version_compare(WS_PLUGIN__S2MEMBER_PRO_VERSION, WS_PLUGIN__S2MEMBER_VERSION, '<')) | |
| 142 | + return; | |
| 143 | + | |
| 144 | + $_account_url = 'https://s2member.com/account/'; | |
| 145 | + //260917.2113 s2Member versions begin with yymmdd; show the installed Pro version's approximate age so administrators can immediately see how far behind it is. | |
| 146 | + $_pro_release_age = ''; | |
| 147 | + if(preg_match('/^(\d{2})(\d{2})(\d{2})/', WS_PLUGIN__S2MEMBER_PRO_VERSION, $_pro_version_parts)) | |
| 148 | + { | |
| 149 | + $_pro_release_timestamp = mktime(0, 0, 0, (int) $_pro_version_parts[2], (int) $_pro_version_parts[3], 2000 + (int) $_pro_version_parts[1]); | |
| 150 | + if($_pro_release_timestamp) | |
| 151 | + $_pro_release_age = human_time_diff($_pro_release_timestamp, current_time('timestamp')); | |
| 152 | + } | |
| 153 | + | |
| 154 | + //260917.1937 Keep this urgent notice compact and skimmable: short paragraphs, prominent version age/security risk, and a clearly separated update action. | |
| 155 | + $_message = '<p style="line-height:1.3em; margin:.3em 0;"><strong>Your s2Member Pro v'.esc_html(WS_PLUGIN__S2MEMBER_PRO_VERSION).($_pro_release_age ? ' is '.esc_html($_pro_release_age).' old and' : '').' is missing important security fixes</strong>.</p>'; | |
| 156 | + $_message .= '<p style="line-height:1.3em; margin:.3em 0;"><em>Please install the latest ZIP from WP Admin > Plugins > Add Plugin > Upload Plugin.</em></p>'; | |
| 157 | + $_update_button = '<a class="button button-primary" style="margin-top:.3em; background:darkred; border-color:darkred;" href="'.esc_url($_account_url).'" target="_blank" rel="external noopener">Download the Latest s2Member Pro Now</a>'; | |
| 158 | + | |
| 159 | + //260917.1937 Keep this Framework-owned warning persistent and red; include the action in the message instead of the helper's review slot so no extra <br> is inserted before it. | |
| 160 | + c_ws_plugin__s2member_admin_notices::display_security_notice($_message.$_update_button, '', array(), '', 'notice-error'); | |
| 161 | + } | |
| 162 | + | |
| 163 | + /** | |
| 164 | + * Displays a branded s2Member security notice. | |
| 165 | + * | |
| 166 | + * @package s2Member\Admin_Notices | |
| 167 | + * @since 260813 | |
| 168 | + * | |
| 169 | + * @param string $message Main notice message. | |
| 170 | + * @param string $review Review prompt shown above the items. | |
| 171 | + * @param array $items Notice items, with safe HTML allowed. | |
| 172 | + * @param string $dismiss_url Optional dismissal URL. | |
| 173 | + * @param string $notice_class Optional WordPress notice severity class. | |
| 174 | + */ | |
| 175 | + public static function display_security_notice($message = '', $review = '', $items = array(), $dismiss_url = '', $notice_class = 'notice-warning') | |
| 176 | + { | |
| 177 | + $message = trim((string)$message); | |
| 178 | + $review = trim((string)$review); | |
| 179 | + $items = (array)$items; | |
| 180 | + if(!$message) | |
| 181 | + return; | |
| 182 | + | |
| 183 | + //260917.0513 Preserve the existing warning style by default, while allowing especially urgent security notices to use WordPress's stronger error styling. | |
| 184 | + $_notice_class = (($notice_class === 'notice-error') ? 'notice notice-error' : 'notice notice-warning'); | |
| 185 | + | |
| 186 | + $_items = array(); | |
| 187 | + foreach($items as $_item) | |
| 188 | + if(is_string($_item) && trim($_item) !== '') | |
| 189 | + $_items[] = '<em>• '.wp_kses_post($_item).'</em>'; | |
| 190 | + | |
| 191 | + $_logo_url = $GLOBALS['WS_PLUGIN__']['s2member']['c']['dir_url'].'/src/images/logo-square-big.png'; | |
| 192 | + $_dismiss = (($dismiss_url !== '') ? '<a href="'.esc_url($dismiss_url).'" title="Dismiss until detected again" style="position:absolute; top:8px; right:10px; text-decoration:none;">Dismiss</a>' : ''); | |
| 193 | + | |
| 194 | + //260917.1937 Give urgent red security notices a stronger heading without changing the existing presentation of normal yellow security notices. | |
| 195 | + $_title = (($notice_class === 'notice-error') ? '<h2 style="margin:0 0 .3em; color:darkred;">s2Member Security Notice</h2>' : '<strong>s2Member Security Notice</strong><br />'); | |
| 196 | + echo '<div class="'.esc_attr($_notice_class).'" style="position:relative; margin:0 0 15px 2px !important; padding:8px 60px 8px 8px !important;">'.$_dismiss.'<table cellspacing="0" cellpadding="0"><tr><td style="vertical-align:top; padding:0 10px 0 0;"><img src="'.esc_url($_logo_url).'" alt="" width="40" height="40" style="border:0;" /></td><td style="vertical-align:top;">'.$_title.wp_kses_post($message).(($review !== '') ? '<br />'.wp_kses_post($review) : '').(($_items) ? '<br />'.implode('<br />', $_items) : '').'</td></tr></table></div>'; | |
| 197 | + } | |
| 198 | + | |
| 199 | + /** | |
| 200 | + * Records a shortcode user field that is not approved for cross-user display. | |
| 201 | + * | |
| 202 | + * @package s2Member\Admin_Notices | |
| 203 | + * @since 260813 | |
| 204 | + * | |
| 205 | + * @param string $field User field ID. | |
| 206 | + * @param string $shortcode Shortcode name. | |
| 207 | + * @param int $post_id Post/Page ID. | |
| 208 | + */ | |
| 209 | + public static function shortcode_user_field_unapproved($field = '', $shortcode = '', $post_id = 0) | |
| 210 | + { | |
| 211 | + $field = trim((string)$field); | |
| 212 | + $shortcode = trim((string)$shortcode); | |
| 213 | + $post_id = (int)$post_id; | |
| 214 | + if(!$field || !$shortcode) | |
| 215 | + return; | |
| 216 | + | |
| 217 | + $_fields = (array)get_option('ws_plugin__s2member_shortcode_user_fields_transition_fields', array()); | |
| 218 | + $_entry_key = md5(strtolower($field)."\0".strtolower($shortcode)."\0".$post_id); | |
| 219 | + | |
| 220 | + //260813 Keep each detected shortcode location separate, while limiting stored warning data. | |
| 221 | + if(!isset($_fields[$_entry_key]) && count($_fields) >= 40) | |
| 222 | + return; | |
| 223 | + $_old_fields = $_fields; | |
| 224 | + $_fields[$_entry_key] = array('field' => $field, 'shortcode' => $shortcode, 'post_id' => $post_id); | |
| 225 | + | |
| 226 | + if($_fields !== $_old_fields) | |
| 227 | + update_option('ws_plugin__s2member_shortcode_user_fields_transition_fields', $_fields, FALSE); | |
| 228 | + } | |
| 229 | + | |
| 230 | + /** | |
| 231 | + * Dismisses the shortcode user-fields security notice until another affected shortcode is detected. | |
| 232 | + * | |
| 233 | + * @package s2Member\Admin_Notices | |
| 234 | + * @since 260813 | |
| 235 | + */ | |
| 236 | + public static function dismiss_shortcode_user_fields_notice() | |
| 237 | + { | |
| 238 | + if(!is_admin() || !current_user_can('create_users') || empty($_GET['s2member-dismiss-shortcode-user-fields-notice'])) | |
| 239 | + return; | |
| 240 | + | |
| 241 | + check_admin_referer('s2member-dismiss-shortcode-user-fields-notice'); | |
| 242 | + delete_option('ws_plugin__s2member_shortcode_user_fields_transition_fields'); | |
| 243 | + | |
| 244 | + wp_safe_redirect(wp_get_referer() ? wp_get_referer() : admin_url()); | |
| 245 | + exit; | |
| 246 | + } | |
| 247 | + | |
| 248 | + /** | |
| 249 | + * Displays the shared shortcode user-fields security notice. | |
| 250 | + * | |
| 251 | + * @package s2Member\Admin_Notices | |
| 252 | + * @since 260813 | |
| 253 | + */ | |
| 254 | + public static function shortcode_user_fields_notice() | |
| 255 | + { | |
| 256 | + if(!current_user_can('create_users')) | |
| 257 | + return; | |
| 258 | + | |
| 259 | + $_fields = (array)get_option('ws_plugin__s2member_shortcode_user_fields_transition_fields', array()); | |
| 260 | + if(!$_fields) | |
| 261 | + return; | |
| 262 | + | |
| 263 | + //260813 Use the submitted whitelist on save so the notice updates immediately. | |
| 264 | + $_using_submitted_whitelist = !empty($_POST['ws_plugin__s2member_options_save']) && is_string($_POST['ws_plugin__s2member_options_save']) && wp_verify_nonce($_POST['ws_plugin__s2member_options_save'], 'ws-plugin--s2member-options-save') && isset($_POST['ws_plugin__s2member_sc_user_fields_whitelist']) && is_string($_POST['ws_plugin__s2member_sc_user_fields_whitelist']); | |
| 265 | + $_field_whitelist = ($_using_submitted_whitelist) ? trim((string)wp_unslash($_POST['ws_plugin__s2member_sc_user_fields_whitelist'])) : trim((string)$GLOBALS['WS_PLUGIN__']['s2member']['o']['sc_user_fields_whitelist']); | |
| 266 | + $_field_whitelist = ($_field_whitelist !== '') ? preg_split('/\s*,\s*/', strtolower($_field_whitelist), -1, PREG_SPLIT_NO_EMPTY) : array(); | |
| 267 | + $_field_whitelist = array_flip($_field_whitelist); | |
| 268 | + foreach($_fields as $_key => $_details) | |
| 269 | + { | |
| 270 | + if(!is_array($_details) || empty($_details['field']) || empty($_details['shortcode'])) | |
| 271 | + unset($_fields[$_key]); | |
| 272 | + else if(isset($_field_whitelist[strtolower($_details['field'])])) | |
| 273 | + unset($_fields[$_key]); | |
| 274 | + } | |
| 275 | + | |
| 276 | + if(!$_fields) | |
| 277 | + { | |
| 278 | + delete_option('ws_plugin__s2member_shortcode_user_fields_transition_fields'); | |
| 279 | + return; | |
| 280 | + } | |
| 281 | + update_option('ws_plugin__s2member_shortcode_user_fields_transition_fields', $_fields, FALSE); | |
| 282 | + | |
| 283 | + // Build a useful field list with a separate entry for each detected shortcode location. | |
| 284 | + $_field_items = array(); | |
| 285 | + foreach($_fields as $_details) | |
| 286 | + { | |
| 287 | + $_item = esc_html($_details['field']).' — ['.esc_html($_details['shortcode']).']'; | |
| 288 | + $_post_id = (!empty($_details['post_id'])) ? (int)$_details['post_id'] : 0; | |
| 289 | + if($_post_id > 0 && ($_edit_link = get_edit_post_link($_post_id, ''))) | |
| 290 | + { | |
| 291 | + $_post_title = get_the_title($_post_id); | |
| 292 | + $_post_title = ($_post_title !== '') ? $_post_title : '(no title)'; | |
| 293 | + $_item .= ' — <a href="'.esc_url($_edit_link).'">'.esc_html($_post_title).' (#'.$_post_id.')</a>'; | |
| 294 | + } | |
| 295 | + $_field_items[] = $_item; | |
| 296 | + } | |
| 297 | + unset($_details, $_item, $_post_id, $_edit_link, $_post_title); | |
| 298 | + | |
| 299 | + $_settings_url = add_query_arg('s2member-open-panel', 'shortcode-user-fields-whitelist', admin_url('/admin.php?page=ws-plugin--s2member-gen-ops')).'#ws-plugin--s2member-shortcode-user-fields-whitelist'; | |
| 300 | + $_dismiss_url = wp_nonce_url(add_query_arg('s2member-dismiss-shortcode-user-fields-notice', '1', admin_url()), 's2member-dismiss-shortcode-user-fields-notice'); | |
| 301 | + $_message = 'Some s2Member shortcodes attempted to display user fields from other accounts that are not in <em><a href="'.esc_url($_settings_url).'">s2Member → General Options → Shortcode User Fields Whitelist</a></em>. Those cross-user field values were blocked.'; | |
| 302 | + c_ws_plugin__s2member_admin_notices::display_security_notice($_message, 'Review the blocked fields below and allow the ones that are okay for other users to see:', $_field_items, $_dismiss_url); | |
| 303 | + } | |
| 304 | + | |
| 305 | + /** | |
| 98 | 306 | * Processes all administrative notices. |
| 99 | 307 | * |
| 100 | 308 | * @package s2Member\Admin_Notices |
| 101 | 309 | * @since 3.5 |
| @@ -122,9 +330,9 @@ | ||
| 122 | 330 | { |
| 123 | 331 | //250510 Fixed for PHP 8.1+: safely normalize on_pages before foreach |
| 124 | 332 | $notice = (array)$notice; |
| 125 | 333 | $notice['on_pages'] = empty($notice['on_pages']) ? array('*') : (array)$notice['on_pages']; |
| 126 | - foreach($notice['on_pages'] as $page) | |
| 334 | + foreach($notice['on_pages'] as $page) | |
| 127 | 335 | { |
| 128 | 336 | if(!preg_match('/^(.+?)\:/', $page)) // NO prefix? |
| 129 | 337 | $page = 'blog:'.ltrim($page, ':'); // `blog:` |
| 130 | 338 | |