| @@ -13,14 +13,52 @@ | ||
| 13 | 13 | * @package PropertyHive/Classes |
| 14 | 14 | * @category Class |
| 15 | 15 | * @author PropertyHive |
| 16 | 16 | */ |
| 17 | +// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedClassFound -- Legacy public PH_Post_types class; preserve its existing name for plugin and extension compatibility. | |
| 17 | 18 | class PH_Post_types { |
| 18 | 19 | |
| 20 | + | |
| 21 | + /** | |
| 22 | + * Keep native CRM screens separate from ordinary WordPress post access. | |
| 23 | + * Existing post permissions remain required, alongside Property Hive access. | |
| 24 | + * | |
| 25 | + * @return array Dedicated primitive capabilities. | |
| 26 | + */ | |
| 27 | + public static function record_capabilities() { | |
| 28 | + $capabilities = array(); | |
| 29 | + foreach ( array( 'edit_posts', 'create_posts', 'edit_others_posts', 'edit_private_posts', 'edit_published_posts', 'publish_posts', 'delete_posts', 'delete_others_posts', 'delete_private_posts', 'delete_published_posts', 'read_private_posts' ) as $capability ) { | |
| 30 | + $capabilities[$capability] = 'propertyhive_' . $capability; | |
| 31 | + } | |
| 32 | + return $capabilities; | |
| 33 | + } | |
| 34 | + | |
| 35 | + /** | |
| 36 | + * Resolve both direct checks and capabilities produced by core's post mapper. | |
| 37 | + * This preserves custom roles' existing edit/publish/delete restrictions. | |
| 38 | + * | |
| 39 | + * @param array $caps Required primitive capabilities. | |
| 40 | + * @return array Required capabilities. | |
| 41 | + */ | |
| 42 | + public static function map_record_capabilities( $caps ) { | |
| 43 | + $mapped = array(); | |
| 44 | + $record_caps = array_flip( self::record_capabilities() ); | |
| 45 | + foreach ( $caps as $capability ) { | |
| 46 | + if ( isset( $record_caps[$capability] ) ) { | |
| 47 | + $mapped[] = 'manage_propertyhive'; | |
| 48 | + $mapped[] = 'create_posts' === $record_caps[$capability] ? 'edit_posts' : $record_caps[$capability]; | |
| 49 | + } else { | |
| 50 | + $mapped[] = $capability; | |
| 51 | + } | |
| 52 | + } | |
| 53 | + return array_values( array_unique( $mapped ) ); | |
| 54 | + } | |
| 55 | + | |
| 19 | 56 | /** |
| 20 | 57 | * Constructor |
| 21 | 58 | */ |
| 22 | 59 | public function __construct() { |
| 60 | + add_filter( 'map_meta_cap', array( __CLASS__, 'map_record_capabilities' ) ); | |
| 23 | 61 | add_action( 'init', array( __CLASS__, 'register_taxonomies' ), 5 ); |
| 24 | 62 | add_action( 'init', array( __CLASS__, 'register_post_types' ), 5 ); |
| 25 | 63 | add_action( 'init', array( __CLASS__, 'register_post_statuses' ), 5 ); |
| 26 | 64 | |
| @@ -59,16 +97,24 @@ | ||
| 59 | 97 | } |
| 60 | 98 | |
| 61 | 99 | public static function register_post_statuses() |
| 62 | 100 | { |
| 63 | - register_post_status('archive', array( | |
| 64 | - 'label' => _x('Archived', 'post', 'propertyhive'), | |
| 65 | - 'public' => false, | |
| 66 | - 'exclude_from_search' => true, | |
| 67 | - 'show_in_admin_all_list' => false, | |
| 68 | - 'show_in_admin_status_list' => true, | |
| 69 | - 'label_count' => _n_noop('Archived <span class="count">(%s)</span>', 'Archived <span class="count">(%s)</span>', 'propertyhive'), | |
| 70 | - )); | |
| 101 | + register_post_status( | |
| 102 | + 'archive', | |
| 103 | + array( | |
| 104 | + 'label' => _x('Archived', 'post status label', 'propertyhive'), | |
| 105 | + 'public' => false, | |
| 106 | + 'exclude_from_search' => true, | |
| 107 | + 'show_in_admin_all_list' => false, | |
| 108 | + 'show_in_admin_status_list' => true, | |
| 109 | + /* translators: %s: Number of archived posts. */ | |
| 110 | + 'label_count' => _n_noop( | |
| 111 | + 'Archived <span class="count">(%s)</span>', | |
| 112 | + 'Archived <span class="count">(%s)</span>', | |
| 113 | + 'propertyhive' | |
| 114 | + ), | |
| 115 | + ) | |
| 116 | + ); | |
| 71 | 117 | } |
| 72 | 118 | |
| 73 | 119 | public static function update_property_indexed_owner_names( $post_id, $post, $update ) |
| 74 | 120 | { |
| @@ -128,8 +174,9 @@ | ||
| 128 | 174 | { |
| 129 | 175 | // Find all properties linked to this contact |
| 130 | 176 | $args = array( |
| 131 | 177 | 'post_type' => 'property', |
| 178 | + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Owner search metadata must be refreshed on properties linked through the existing serialized owner IDs; retain the relationship match. | |
| 132 | 179 | 'meta_query' => array( |
| 133 | 180 | array( |
| 134 | 181 | 'key' => '_owner_contact_id', |
| 135 | 182 | 'value' => '"' . $post_id . '"', |
| @@ -509,8 +556,9 @@ | ||
| 509 | 556 | ) |
| 510 | 557 | ) |
| 511 | 558 | ); |
| 512 | 559 | |
| 560 | + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Existing public Property Hive extension hook do_action_after_register_taxonomies; changing the established name would detach installed callbacks. | |
| 513 | 561 | do_action( 'do_action_after_register_taxonomies' ); |
| 514 | 562 | } |
| 515 | 563 | |
| 516 | 564 | /** |
| @@ -548,8 +596,9 @@ | ||
| 548 | 596 | 'description' => __( 'This is where you can add new properties to your site.', 'propertyhive' ), |
| 549 | 597 | 'public' => true, |
| 550 | 598 | 'show_ui' => true, |
| 551 | 599 | 'capability_type' => 'post', |
| 600 | + 'capabilities' => self::record_capabilities(), | |
| 552 | 601 | 'map_meta_cap' => true, |
| 553 | 602 | 'publicly_queryable' => true, |
| 554 | 603 | 'exclude_from_search' => false, |
| 555 | 604 | 'hierarchical' => false, // Hierarchical causes memory issues - WP loads all records! |
| @@ -587,8 +636,9 @@ | ||
| 587 | 636 | 'description' => __( 'This is where you can add new contacts to your site.', 'propertyhive' ), |
| 588 | 637 | 'public' => false, |
| 589 | 638 | 'show_ui' => true, |
| 590 | 639 | 'capability_type' => 'post', |
| 640 | + 'capabilities' => self::record_capabilities(), | |
| 591 | 641 | 'map_meta_cap' => true, |
| 592 | 642 | 'publicly_queryable' => false, |
| 593 | 643 | 'exclude_from_search' => true, |
| 594 | 644 | 'hierarchical' => false, // Hierarchical causes memory issues - WP loads all records! |
| @@ -621,8 +671,9 @@ | ||
| 621 | 671 | ), |
| 622 | 672 | 'public' => true, |
| 623 | 673 | 'show_ui' => false, |
| 624 | 674 | 'capability_type' => 'post', |
| 675 | + 'capabilities' => self::record_capabilities(), | |
| 625 | 676 | 'map_meta_cap' => true, |
| 626 | 677 | 'publicly_queryable' => false, |
| 627 | 678 | 'exclude_from_search' => true, |
| 628 | 679 | 'hierarchical' => false, // Hierarchical causes memory issues - WP loads all records! |
| @@ -656,8 +707,9 @@ | ||
| 656 | 707 | ), |
| 657 | 708 | 'public' => false, |
| 658 | 709 | 'show_ui' => true, |
| 659 | 710 | 'capability_type' => 'post', |
| 711 | + 'capabilities' => self::record_capabilities(), | |
| 660 | 712 | 'map_meta_cap' => true, |
| 661 | 713 | 'publicly_queryable' => false, |
| 662 | 714 | 'exclude_from_search' => true, |
| 663 | 715 | 'hierarchical' => false, // Hierarchical causes memory issues - WP loads all records! |
| @@ -691,8 +743,9 @@ | ||
| 691 | 743 | ), |
| 692 | 744 | 'public' => false, |
| 693 | 745 | 'show_ui' => true, |
| 694 | 746 | 'capability_type' => 'post', |
| 747 | + 'capabilities' => self::record_capabilities(), | |
| 695 | 748 | 'map_meta_cap' => true, |
| 696 | 749 | 'publicly_queryable' => false, |
| 697 | 750 | 'exclude_from_search' => true, |
| 698 | 751 | 'hierarchical' => false, // Hierarchical causes memory issues - WP loads all records! |
| @@ -725,8 +778,9 @@ | ||
| 725 | 778 | ), |
| 726 | 779 | 'public' => false, |
| 727 | 780 | 'show_ui' => true, |
| 728 | 781 | 'capability_type' => 'post', |
| 782 | + 'capabilities' => self::record_capabilities(), | |
| 729 | 783 | 'map_meta_cap' => true, |
| 730 | 784 | 'publicly_queryable' => false, |
| 731 | 785 | 'exclude_from_search' => true, |
| 732 | 786 | 'hierarchical' => false, // Hierarchical causes memory issues - WP loads all records! |
| @@ -759,8 +813,9 @@ | ||
| 759 | 813 | ), |
| 760 | 814 | 'public' => false, |
| 761 | 815 | 'show_ui' => true, |
| 762 | 816 | 'capability_type' => 'post', |
| 817 | + 'capabilities' => self::record_capabilities(), | |
| 763 | 818 | 'map_meta_cap' => true, |
| 764 | 819 | 'publicly_queryable' => false, |
| 765 | 820 | 'exclude_from_search' => true, |
| 766 | 821 | 'hierarchical' => false, // Hierarchical causes memory issues - WP loads all records! |
| @@ -793,11 +848,11 @@ | ||
| 793 | 848 | ), |
| 794 | 849 | 'public' => false, |
| 795 | 850 | 'show_ui' => true, |
| 796 | 851 | 'capability_type' => 'post', |
| 797 | - 'capabilities' => array( | |
| 852 | + 'capabilities' => array_merge( self::record_capabilities(), array( | |
| 798 | 853 | 'create_posts' => false |
| 799 | - ), | |
| 854 | + ) ), | |
| 800 | 855 | 'map_meta_cap' => true, |
| 801 | 856 | 'publicly_queryable' => false, |
| 802 | 857 | 'exclude_from_search' => true, |
| 803 | 858 | 'hierarchical' => false, // Hierarchical causes memory issues - WP loads all records! |
| @@ -830,8 +885,9 @@ | ||
| 830 | 885 | ), |
| 831 | 886 | 'public' => false, |
| 832 | 887 | 'show_ui' => true, |
| 833 | 888 | 'capability_type' => 'post', |
| 889 | + 'capabilities' => self::record_capabilities(), | |
| 834 | 890 | 'map_meta_cap' => true, |
| 835 | 891 | 'publicly_queryable' => false, |
| 836 | 892 | 'exclude_from_search' => true, |
| 837 | 893 | 'hierarchical' => false, // Hierarchical causes memory issues - WP loads all records! |
| @@ -872,11 +928,11 @@ | ||
| 872 | 928 | 'query_var' => true, |
| 873 | 929 | 'supports' => false, |
| 874 | 930 | 'show_in_nav_menus' => false, |
| 875 | 931 | 'show_in_menu' => false, |
| 876 | - 'capabilities' => array( | |
| 932 | + 'capabilities' => array_merge( self::record_capabilities(), array( | |
| 877 | 933 | 'create_posts' => 'do_not_allow' |
| 878 | - ), | |
| 934 | + ) ), | |
| 879 | 935 | ) |
| 880 | 936 | ) |
| 881 | 937 | ); |
| 882 | 938 | do_action( 'propertyhive_after_register_post_types' ); |
| @@ -917,8 +973,9 @@ | ||
| 917 | 973 | $args = array( |
| 918 | 974 | 'post_type' => 'enquiry', |
| 919 | 975 | 'nopaging' => true, |
| 920 | 976 | 'post_status' => array('publish', 'pending', 'private', 'draft', 'auto-draft', 'future', 'trash'), |
| 977 | + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Both legacy and current property relationship metadata must match when the explicit enquiry-trash option is enabled. | |
| 921 | 978 | 'meta_query' => array( |
| 922 | 979 | 'relation' => 'OR', |
| 923 | 980 | array( |
| 924 | 981 | 'key' => '_property_id', |
| @@ -977,9 +1034,11 @@ | ||
| 977 | 1034 | 'post_type' => 'property', |
| 978 | 1035 | 'fields' => 'ids', |
| 979 | 1036 | 'suppress_filters' => TRUE, |
| 980 | 1037 | 'posts_per_page' => 1, |
| 1038 | + // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in -- Excludes only the property being deleted so its own media reference cannot prevent attachment cleanup. | |
| 981 | 1039 | 'post__not_in' => array( $post_id ), |
| 1040 | + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Before deleting an attachment, check all four serialized property media relationships; the query stops at one matching property to preserve shared media. | |
| 982 | 1041 | 'meta_query' => array( |
| 983 | 1042 | 'relation' => 'OR', |
| 984 | 1043 | array( |
| 985 | 1044 | 'key' => '_photos', |
| @@ -1070,8 +1129,9 @@ | ||
| 1070 | 1129 | $args = array( |
| 1071 | 1130 | 'post_type' => 'enquiry', |
| 1072 | 1131 | 'nopaging' => true, |
| 1073 | 1132 | 'post_status' => array('publish', 'pending', 'private', 'draft', 'auto-draft', 'future', 'trash'), |
| 1133 | + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Both legacy and current property relationship metadata must match when the explicit enquiry-delete option is enabled. | |
| 1074 | 1134 | 'meta_query' => array( |
| 1075 | 1135 | 'relation' => 'OR', |
| 1076 | 1136 | array( |
| 1077 | 1137 | 'key' => '_property_id', |
| @@ -1127,8 +1187,9 @@ | ||
| 1127 | 1187 | |
| 1128 | 1188 | $args = array( |
| 1129 | 1189 | 'post_type' => 'contact', |
| 1130 | 1190 | 'nopaging' => true, |
| 1191 | + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Remove only contacts linked to the deleted user through their stored user ID. | |
| 1131 | 1192 | 'meta_query' => array( |
| 1132 | 1193 | array( |
| 1133 | 1194 | 'key' => '_user_id', |
| 1134 | 1195 | 'value' => (int)$user_id, |
| @@ -1249,8 +1310,10 @@ | ||
| 1249 | 1310 | $args = array( |
| 1250 | 1311 | 'post_type' => 'property', |
| 1251 | 1312 | 'fields' => 'ids', |
| 1252 | 1313 | 'post_status' => array( 'publish', 'draft'), |
| 1314 | + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- This maintenance backfill must select records missing the destination metadata; removing the predicate would overwrite existing values. | |
| 1315 | + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Owner search metadata must be refreshed on properties linked through the existing serialized owner IDs; retain the relationship match. | |
| 1253 | 1316 | 'meta_query' => array( |
| 1254 | 1317 | array( |
| 1255 | 1318 | 'key' => '_address_concatenated', |
| 1256 | 1319 | 'compare' => 'NOT EXISTS' |
| @@ -1257,8 +1320,9 @@ | ||
| 1257 | 1320 | ) |
| 1258 | 1321 | ), |
| 1259 | 1322 | 'nopaging' => true, |
| 1260 | 1323 | 'orderby' => 'rand', |
| 1324 | + // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.SuppressFilters_suppress_filters -- Maintenance must cover every language; front-end language filters would omit records from this backfill. | |
| 1261 | 1325 | 'suppress_filters' => true, |
| 1262 | 1326 | ); |
| 1263 | 1327 | $property_query = new WP_Query($args); |
| 1264 | 1328 | |
| @@ -1280,8 +1344,10 @@ | ||
| 1280 | 1344 | $args = array( |
| 1281 | 1345 | 'post_type' => 'contact', |
| 1282 | 1346 | 'fields' => 'ids', |
| 1283 | 1347 | 'post_status' => 'publish', |
| 1348 | + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- This maintenance backfill must select records missing the destination metadata; removing the predicate would overwrite existing values. | |
| 1349 | + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Owner search metadata must be refreshed on properties linked through the existing serialized owner IDs; retain the relationship match. | |
| 1284 | 1350 | 'meta_query' => array( |
| 1285 | 1351 | array( |
| 1286 | 1352 | 'key' => '_address_concatenated', |
| 1287 | 1353 | 'compare' => 'NOT EXISTS' |
| @@ -1288,8 +1354,9 @@ | ||
| 1288 | 1354 | ) |
| 1289 | 1355 | ), |
| 1290 | 1356 | 'nopaging' => true, |
| 1291 | 1357 | 'orderby' => 'rand', |
| 1358 | + // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.SuppressFilters_suppress_filters -- Maintenance must cover every language; front-end language filters would omit records from this backfill. | |
| 1292 | 1359 | 'suppress_filters' => true, |
| 1293 | 1360 | ); |
| 1294 | 1361 | $contact_query = new WP_Query($args); |
| 1295 | 1362 | |
| @@ -1351,8 +1418,9 @@ | ||
| 1351 | 1418 | 'fields' => 'ids', |
| 1352 | 1419 | 'post_type' => 'viewing', |
| 1353 | 1420 | 'nopaging' => true, |
| 1354 | 1421 | 'post_status' => 'publish', |
| 1422 | + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Recompute related-viewing metadata only for viewings linked to the changed property or applicant; retain existing relationship and status semantics. | |
| 1355 | 1423 | 'meta_query' => $meta_query, |
| 1356 | 1424 | 'orderby' => 'none' |
| 1357 | 1425 | ); |
| 1358 | 1426 | |
| @@ -1385,8 +1453,9 @@ | ||
| 1385 | 1453 | 'fields' => 'ids', |
| 1386 | 1454 | 'post_type' => 'viewing', |
| 1387 | 1455 | 'nopaging' => true, |
| 1388 | 1456 | 'post_status' => 'publish', |
| 1457 | + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Recompute related-viewing metadata only for viewings linked to the changed property or applicant; retain existing relationship and status semantics. | |
| 1389 | 1458 | 'meta_query' => $meta_query, |
| 1390 | 1459 | 'orderby' => 'none' |
| 1391 | 1460 | ); |
| 1392 | 1461 | |