| @@ -130,12 +130,23 @@ | ||
| 130 | 130 | { |
| 131 | 131 | /** @var wpdb $wpdb WordPress DB object instance. */ |
| 132 | 132 | global $wpdb; // Global DB object reference. |
| 133 | 133 | |
| 134 | - if(is_array($post_ids = $wpdb->get_col("SELECT `ID` FROM `".$wpdb->posts."` WHERE `post_status` = 'publish' AND ".(($post_type) ? "`post_type` = '".esc_sql((string)$post_type)."'" : "`post_type` NOT IN('page','attachment','nav_menu_item','revision')")))) | |
| 135 | - $post_ids = c_ws_plugin__s2member_utils_arrays::force_integers($post_ids); | |
| 134 | + //260914.1643 Query-level access checks can request the same published Post IDs many times per page load, so cache each Posts-table/Post-Type combination for this request; including the table keeps switched Multisite blogs isolated. | |
| 135 | + static $_post_ids = array(), $_post_changes = array(); | |
| 136 | + $_cache_key = $wpdb->posts.'|'.(string)$post_type; | |
| 136 | 137 | |
| 137 | - return (!empty($post_ids) && is_array($post_ids)) ? array_unique($post_ids) : array(); | |
| 138 | + //260914.1643 Refresh after normal WordPress post mutations so a write followed by another access check in the same request cannot reuse stale published Post IDs. | |
| 139 | + $_changes = did_action('save_post') + did_action('deleted_post'); | |
| 140 | + if(!isset($_post_ids[$_cache_key]) || !isset($_post_changes[$_cache_key]) || $_post_changes[$_cache_key] !== $_changes) | |
| 141 | + { | |
| 142 | + $post_ids = $wpdb->get_col("SELECT `ID` FROM `".$wpdb->posts."` WHERE `post_status` = 'publish' AND ".(($post_type) ? "`post_type` = '".esc_sql((string)$post_type)."'" : "`post_type` NOT IN('page','attachment','nav_menu_item','revision')")); | |
| 143 | + if(is_array($post_ids)) $post_ids = c_ws_plugin__s2member_utils_arrays::force_integers($post_ids); | |
| 144 | + | |
| 145 | + $_post_ids[$_cache_key] = (!empty($post_ids) && is_array($post_ids)) ? array_unique($post_ids) : array(); | |
| 146 | + $_post_changes[$_cache_key] = $_changes; | |
| 147 | + } | |
| 148 | + return $_post_ids[$_cache_key]; | |
| 138 | 149 | } |
| 139 | 150 | |
| 140 | 151 | /** |
| 141 | 152 | * Retrieves a unique array of all published Child Post IDs in the database. |
| @@ -176,12 +187,23 @@ | ||
| 176 | 187 | { |
| 177 | 188 | /** @var wpdb $wpdb WordPress DB object instance. */ |
| 178 | 189 | global $wpdb; // Global DB object reference. |
| 179 | 190 | |
| 180 | - if(is_array($page_ids = $wpdb->get_col("SELECT `ID` FROM `".$wpdb->posts."` WHERE `post_status` = 'publish' AND `post_type` = 'page'"))) | |
| 181 | - $page_ids = c_ws_plugin__s2member_utils_arrays::force_integers($page_ids); | |
| 191 | + //260914.1643 Query-level access checks can request the complete published Page-ID list repeatedly in one page load, so cache it per Posts table for this request; the table key also isolates switched Multisite blogs. | |
| 192 | + static $_page_ids = array(), $_post_changes = array(); | |
| 193 | + $_cache_key = $wpdb->posts; | |
| 182 | 194 | |
| 183 | - return (!empty($page_ids) && is_array($page_ids)) ? array_unique($page_ids) : array(); | |
| 195 | + //260914.1643 Refresh after normal WordPress post mutations so Pages created, deleted, or changed earlier in this request are reflected by later access checks. | |
| 196 | + $_changes = did_action('save_post') + did_action('deleted_post'); | |
| 197 | + if(!isset($_page_ids[$_cache_key]) || !isset($_post_changes[$_cache_key]) || $_post_changes[$_cache_key] !== $_changes) | |
| 198 | + { | |
| 199 | + $page_ids = $wpdb->get_col("SELECT `ID` FROM `".$wpdb->posts."` WHERE `post_status` = 'publish' AND `post_type` = 'page'"); | |
| 200 | + if(is_array($page_ids)) $page_ids = c_ws_plugin__s2member_utils_arrays::force_integers($page_ids); | |
| 201 | + | |
| 202 | + $_page_ids[$_cache_key] = (!empty($page_ids) && is_array($page_ids)) ? array_unique($page_ids) : array(); | |
| 203 | + $_post_changes[$_cache_key] = $_changes; | |
| 204 | + } | |
| 205 | + return $_page_ids[$_cache_key]; | |
| 184 | 206 | } |
| 185 | 207 | |
| 186 | 208 | /** |
| 187 | 209 | * Retrieves a unique array of all Singular IDs in the database that require Custom Capabilities. |
| @@ -219,30 +241,55 @@ | ||
| 219 | 241 | { |
| 220 | 242 | /** @var wpdb $wpdb WordPress DB object instance. */ |
| 221 | 243 | global $wpdb; // Global DB object reference. |
| 222 | 244 | |
| 223 | - if(is_array($results = $wpdb->get_results("SELECT `".$wpdb->postmeta."`.`post_id`, `".$wpdb->postmeta."`.`meta_value`, `".$wpdb->posts."`.`post_type`". | |
| 224 | - " FROM `".$wpdb->posts."`, `".$wpdb->postmeta."` WHERE `".$wpdb->posts."`.`ID` = `".$wpdb->postmeta."`.`post_id`". | |
| 225 | - " AND `".$wpdb->postmeta."`.`meta_key` = 's2member_ccaps_req' AND `".$wpdb->postmeta."`.`meta_value` != ''"))) | |
| 245 | + //260914.2132 Keep both the SQL rows and their lazily unserialized CCAP requirements for this request. The parsed cache is reset whenever post-meta changes invalidate the SQL rows, and anonymous visitors never pay the unserialization cost. | |
| 246 | + static $_results = array(), $_result_ccaps = array(), $_meta_changes = array(); | |
| 247 | + $_cache_key = $wpdb->posts.'|'.$wpdb->postmeta; | |
| 248 | + $_changes = did_action('added_post_meta') + did_action('updated_post_meta') + did_action('deleted_post_meta'); | |
| 249 | + if(!isset($_results[$_cache_key]) || !isset($_meta_changes[$_cache_key]) || $_meta_changes[$_cache_key] !== $_changes) | |
| 226 | 250 | { |
| 251 | + $_results[$_cache_key] = $wpdb->get_results("SELECT `".$wpdb->postmeta."`.`post_id`, `".$wpdb->postmeta."`.`meta_value`, `".$wpdb->posts."`.`post_type`". | |
| 252 | + " FROM `".$wpdb->posts."`, `".$wpdb->postmeta."` WHERE `".$wpdb->posts."`.`ID` = `".$wpdb->postmeta."`.`post_id`". | |
| 253 | + " AND `".$wpdb->postmeta."`.`meta_key` = 's2member_ccaps_req' AND `".$wpdb->postmeta."`.`meta_value` != ''"); | |
| 254 | + $_result_ccaps[$_cache_key] = array(); | |
| 255 | + $_meta_changes[$_cache_key] = $_changes; | |
| 256 | + } | |
| 257 | + $results = $_results[$_cache_key]; | |
| 258 | + $result_ccaps = &$_result_ccaps[$_cache_key]; | |
| 259 | + unset($_cache_key, $_changes); //260901 Request-local SQL cache. | |
| 260 | + | |
| 261 | + if(is_array($results)) | |
| 262 | + { | |
| 227 | 263 | $bbpress_restrictions_enable = apply_filters('ws_plugin__s2member_bbpress_restrictions_enable', TRUE); |
| 228 | 264 | $bbpress_installed = c_ws_plugin__s2member_utils_conds::bbp_is_installed(); // bbPress is installed? |
| 229 | 265 | $bbpress_forum_post_type = $bbpress_installed ? bbp_get_forum_post_type() : ''; // Acquire the current post type for forums. |
| 230 | 266 | $bbpress_topic_post_type = $bbpress_installed ? bbp_get_topic_post_type() : ''; // Acquire the current post type for topics. |
| 267 | + //260914.2132 Cache each distinct CCAP decision only for this helper invocation; many protected Posts share the same CCAP, but a later query pass can still receive a different filtered capability result. | |
| 268 | + $ccap_access = array(); | |
| 231 | 269 | |
| 232 | - foreach($results as $r) // Now we need to check Custom Capabilities against ``$user``. If ``$user`` is a valid `WP_User` object, else all are unavailable. | |
| 270 | + foreach($results as $_result_index => $r) // Now we need to check Custom Capabilities against ``$user``. If ``$user`` is a valid `WP_User` object, else all are unavailable. | |
| 233 | 271 | { |
| 234 | 272 | if(!is_object($user) || empty($user->ID)) // No ``$user`` object? Maybe not logged-in?. |
| 235 | 273 | $singular_ids[] = (int)$r->post_id; // It's NOT available. There is no ``$user``. |
| 236 | 274 | |
| 237 | - else if(is_array($ccaps = c_ws_plugin__s2member_utils_arrays::maybe_unserialize($r->meta_value))) // Make sure we unserialize. | |
| 275 | + else | |
| 238 | 276 | { |
| 239 | - foreach($ccaps as $ccap) // Test for Custom Capability Restrictions now. | |
| 240 | - if(strlen($ccap) && !$user->has_cap('access_s2member_ccap_'.$ccap)) | |
| 277 | + if(!array_key_exists($_result_index, $result_ccaps)) | |
| 278 | + $result_ccaps[$_result_index] = c_ws_plugin__s2member_utils_arrays::maybe_unserialize($r->meta_value); | |
| 279 | + | |
| 280 | + if(is_array($ccaps = $result_ccaps[$_result_index])) | |
| 281 | + { | |
| 282 | + foreach($ccaps as $ccap) // Test for Custom Capability Restrictions now. | |
| 241 | 283 | { |
| 242 | - $singular_ids[] = (int)$r->post_id; // It's NOT available. | |
| 243 | - break; // Break now, no need to continue in this loop. | |
| 284 | + $ccap = (string)$ccap; | |
| 285 | + if(strlen($ccap) && !(isset($ccap_access[$ccap]) ? $ccap_access[$ccap] : ($ccap_access[$ccap] = (bool)$user->has_cap('access_s2member_ccap_'.$ccap)))) | |
| 286 | + { | |
| 287 | + $singular_ids[] = (int)$r->post_id; // It's NOT available. | |
| 288 | + break; // Break now, no need to continue in this loop. | |
| 289 | + } | |
| 244 | 290 | } |
| 291 | + } | |
| 245 | 292 | } |
| 246 | 293 | if($bbpress_restrictions_enable && $bbpress_installed && $r->post_type === $bbpress_forum_post_type) |
| 247 | 294 | if(!empty($singular_ids) && in_array((int)$r->post_id, $singular_ids, TRUE)) |
| 248 | 295 | { |
| @@ -355,12 +402,39 @@ | ||
| 355 | 402 | * @return array Unique array of all Singular IDs *(as integers)* NOT available to current Visitor, due to Specific Post/Page Restrictions. |
| 356 | 403 | */ |
| 357 | 404 | public static function get_unavailable_singular_ids_with_sp($exclude_conflicts = FALSE) |
| 358 | 405 | { |
| 359 | - if($GLOBALS['WS_PLUGIN__']['s2member']['o']['specific_ids'] && is_array($_singular_ids = preg_split('/['."\r\n\t".'\s;,]+/', $GLOBALS['WS_PLUGIN__']['s2member']['o']['specific_ids']))) | |
| 360 | - foreach($_singular_ids as $_singular_id) // Now check access to this Singular, against the current Visitor, via read-only ``c_ws_plugin__s2member_sp_access::sp_access()``. | |
| 361 | - if(is_numeric($_singular_id) && !c_ws_plugin__s2member_sp_access::sp_access($_singular_id, 'read-only')) | |
| 406 | + if($GLOBALS['WS_PLUGIN__']['s2member']['o']['specific_ids']) | |
| 407 | + { | |
| 408 | + //260915.0105 Parse/normalize configured Specific Post/Page IDs only once per distinct option value in this request; Alternative View filtering can call this helper many times on one page load. | |
| 409 | + static $_specific_ids_cache = array(); | |
| 410 | + $_specific_ids = (string)$GLOBALS['WS_PLUGIN__']['s2member']['o']['specific_ids']; | |
| 411 | + if(!isset($_specific_ids_cache[$_specific_ids])) | |
| 412 | + { | |
| 413 | + $_specific_ids_cache[$_specific_ids] = array(); | |
| 414 | + foreach((array)preg_split('/['."\r\n\t".'\s;,]+/', $_specific_ids) as $_specific_id) | |
| 415 | + if(is_numeric($_specific_id)) $_specific_ids_cache[$_specific_ids][] = (int)$_specific_id; | |
| 416 | + $_specific_ids_cache[$_specific_ids] = array_values(array_unique($_specific_ids_cache[$_specific_ids])); | |
| 417 | + } | |
| 418 | + $_singular_ids = $_specific_ids_cache[$_specific_ids]; | |
| 419 | + | |
| 420 | + //260915.0122 Most Alternative View requests have no Specific Access credential. Use WordPress's listener APIs instead of testing `$wp_filter` keys directly, because an empty retained hook object must not disable this fast path. | |
| 421 | + $_sp_access_customized = has_action('ws_plugin__s2member_before_sp_access') | |
| 422 | + || has_filter('ws_plugin__s2member_sp_access_excluded') | |
| 423 | + || has_filter('ws_plugin__s2member_sp_access_excluded_cap') | |
| 424 | + || has_filter('ws_plugin__s2member_sp_access') | |
| 425 | + || has_action('ws_plugin__s2member_before_sp_access_session') | |
| 426 | + || has_filter('ws_plugin__s2member_sp_access_session'); | |
| 427 | + $_sp_access_credential = !empty($_GET['s2member_sp_access']) || !empty($_COOKIE['s2member_sp_access']); | |
| 428 | + | |
| 429 | + if(!$_sp_access_customized && !$_sp_access_credential) | |
| 430 | + { | |
| 431 | + if(!current_user_can('edit_posts')) $singular_ids = $_singular_ids; | |
| 432 | + } | |
| 433 | + else foreach($_singular_ids as $_singular_id) // A link/session or SP customization requires the established per-ID access routine. | |
| 434 | + if(!c_ws_plugin__s2member_sp_access::sp_access($_singular_id, 'read-only')) | |
| 362 | 435 | $singular_ids[] = (int)$_singular_id; |
| 436 | + } | |
| 363 | 437 | |
| 364 | 438 | if(!empty($singular_ids) && is_array($singular_ids) && $exclude_conflicts) |
| 365 | 439 | { |
| 366 | 440 | $all_singular_ids_not_conflicting = c_ws_plugin__s2member_utils_gets::get_all_singular_ids_with_sp('exclude-conflicts'); |
| @@ -416,11 +490,26 @@ | ||
| 416 | 490 | { |
| 417 | 491 | /** @var wpdb $wpdb WordPress DB object instance. */ |
| 418 | 492 | global $wpdb; // Global DB object reference. |
| 419 | 493 | |
| 420 | - if(!empty($terms) && is_array($terms) && is_array($singular_ids = $wpdb->get_col("SELECT `object_id` FROM `".$wpdb->term_relationships."` WHERE `term_taxonomy_id` IN (SELECT `term_taxonomy_id` FROM `".$wpdb->term_taxonomy."` WHERE `term_id` IN('".implode("','", $terms)."'))"))) | |
| 421 | - $singular_ids = c_ws_plugin__s2member_utils_arrays::force_integers($singular_ids); | |
| 494 | + if(empty($terms) || !is_array($terms)) return array(); | |
| 422 | 495 | |
| 423 | - return (!empty($singular_ids) && is_array($singular_ids)) ? array_unique($singular_ids) : array(); | |
| 496 | + //260914.1643 Term restrictions can request the same term-to-Singular lookup repeatedly in one page load. Normalize only the cache key so equivalent term sets share one result regardless of order or duplicates, while leaving the legacy SQL input unchanged. | |
| 497 | + static $_singular_ids = array(), $_term_changes = array(); | |
| 498 | + $_cache_terms = array_values(array_unique(c_ws_plugin__s2member_utils_arrays::force_integers($terms))); | |
| 499 | + sort($_cache_terms, SORT_NUMERIC); | |
| 500 | + $_cache_key = $wpdb->term_relationships.'|'.$wpdb->term_taxonomy.'|'.implode(',', $_cache_terms); | |
| 501 | + | |
| 502 | + //260914.1643 Refresh after normal WordPress object-term mutations so a relationship changed earlier in this request is visible to later access checks; table names in the key isolate switched Multisite blogs. | |
| 503 | + $_changes = did_action('set_object_terms') + did_action('deleted_term_relationships'); | |
| 504 | + if(!isset($_singular_ids[$_cache_key]) || !isset($_term_changes[$_cache_key]) || $_term_changes[$_cache_key] !== $_changes) | |
| 505 | + { | |
| 506 | + $singular_ids = $wpdb->get_col("SELECT `object_id` FROM `".$wpdb->term_relationships."` WHERE `term_taxonomy_id` IN (SELECT `term_taxonomy_id` FROM `".$wpdb->term_taxonomy."` WHERE `term_id` IN('".implode("','", $terms)."'))"); | |
| 507 | + if(is_array($singular_ids)) $singular_ids = c_ws_plugin__s2member_utils_arrays::force_integers($singular_ids); | |
| 508 | + | |
| 509 | + $_singular_ids[$_cache_key] = (!empty($singular_ids) && is_array($singular_ids)) ? array_unique($singular_ids) : array(); | |
| 510 | + $_term_changes[$_cache_key] = $_changes; | |
| 511 | + } | |
| 512 | + return $_singular_ids[$_cache_key]; | |
| 424 | 513 | } |
| 425 | 514 | } |
| 426 | 515 | } |