Changeset 3401474
- Timestamp:
- 11/24/2025 12:11:34 AM (4 months ago)
- Location:
- whp-hide-posts
- Files:
-
- 41 added
- 12 edited
-
tags/2.1.0 (added)
-
tags/2.1.0/LICENSE (added)
-
tags/2.1.0/README.md (added)
-
tags/2.1.0/assets (added)
-
tags/2.1.0/assets/admin (added)
-
tags/2.1.0/assets/admin/css (added)
-
tags/2.1.0/assets/admin/css/whp-style.css (added)
-
tags/2.1.0/assets/admin/js (added)
-
tags/2.1.0/assets/admin/js/whp-gutenberg.js (added)
-
tags/2.1.0/assets/admin/js/whp-script.js (added)
-
tags/2.1.0/inc (added)
-
tags/2.1.0/inc/admin (added)
-
tags/2.1.0/inc/admin/class-dashboard.php (added)
-
tags/2.1.0/inc/admin/class-post-hide-metabox.php (added)
-
tags/2.1.0/inc/class-cache-manager.php (added)
-
tags/2.1.0/inc/class-post-hide.php (added)
-
tags/2.1.0/inc/class-rest-api.php (added)
-
tags/2.1.0/inc/class-seo-integration.php (added)
-
tags/2.1.0/inc/class-yoast-duplicate-post.php (added)
-
tags/2.1.0/inc/class-zeen-theme.php (added)
-
tags/2.1.0/inc/core (added)
-
tags/2.1.0/inc/core/autoloader.php (added)
-
tags/2.1.0/inc/core/class-constants.php (added)
-
tags/2.1.0/inc/core/class-database.php (added)
-
tags/2.1.0/inc/core/class-plugin.php (added)
-
tags/2.1.0/inc/core/helpers.php (added)
-
tags/2.1.0/inc/traits (added)
-
tags/2.1.0/inc/traits/trait-singleton.php (added)
-
tags/2.1.0/languages (added)
-
tags/2.1.0/languages/whp-hide-posts-mk_MK.mo (added)
-
tags/2.1.0/languages/whp-hide-posts-mk_MK.po (added)
-
tags/2.1.0/uninstall.php (added)
-
tags/2.1.0/views (added)
-
tags/2.1.0/views/admin (added)
-
tags/2.1.0/views/admin/template-admin-dashboard.php (added)
-
tags/2.1.0/views/admin/template-admin-post-metabox.php (added)
-
tags/2.1.0/whp-hide-posts.php (added)
-
trunk/README.md (modified) (3 diffs)
-
trunk/assets/admin/js/whp-gutenberg.js (added)
-
trunk/assets/admin/js/whp-script.js (modified) (1 diff)
-
trunk/inc/admin/class-dashboard.php (modified) (9 diffs)
-
trunk/inc/admin/class-post-hide-metabox.php (modified) (14 diffs)
-
trunk/inc/class-cache-manager.php (added)
-
trunk/inc/class-post-hide.php (modified) (8 diffs)
-
trunk/inc/class-rest-api.php (added)
-
trunk/inc/class-seo-integration.php (added)
-
trunk/inc/class-yoast-duplicate-post.php (modified) (3 diffs)
-
trunk/inc/core/class-constants.php (modified) (1 diff)
-
trunk/inc/core/class-database.php (modified) (2 diffs)
-
trunk/inc/core/class-plugin.php (modified) (9 diffs)
-
trunk/uninstall.php (modified) (1 diff)
-
trunk/views/admin/template-admin-post-metabox.php (modified) (2 diffs)
-
trunk/whp-hide-posts.php (modified) (3 diffs)
Legend:
- Unmodified
- Added
- Removed
-
whp-hide-posts/trunk/README.md
r3331355 r3401474 4 4 Tags: hide posts, hide, show, visibility, hide products 5 5 Requires at least: 5.0 6 Tested up to: 6.8. 26 Tested up to: 6.8.3 7 7 Requires PHP: 7.3 8 Stable tag: 2. 0.38 Stable tag: 2.1.0 9 9 License: GPLv3 or later 10 10 License URI: http://www.gnu.org/licenses/gpl-3.0.html 11 11 12 Allows you to hide any posts on the home page, category page, search page, tags page, authors page, RSS Feed, REST API, Post Navigationand more.12 Allows you to hide any posts on the home page, category page, search page, tags page, authors page, RSS Feed, REST API, XML sitemaps, SEO integrations and more. 13 13 14 14 == Description == 15 15 16 This plugin allows you to hide any posts on the home page, category page, search page, tags page, authors page, RSS Feed, REST API, Post Navigation and Native Recent Posts Widget.16 This plugin allows you to hide any posts on the home page, category page, search page, tags page, authors page, RSS Feed, REST API, Post Navigation, Native Recent Posts Widget, XML sitemaps, Yoast SEO sitemap, breadcrumbs and internal link suggestions. 17 17 18 18 [Try the Demo](https://demo.tastewp.com/whp-hide-posts "Demo") … … 20 20 = Features: = 21 21 22 - Users can choose to hide specific posts on specific archives and pages in WordPress as well in RSS Feed and REST API. 23 - Users can enable the option to show the hide functionality on Custom Post Types and hide those Custom Post Type posts on any archive and pages. 24 - Users can hide Woocommerce products in product categories and on the store page as well as hide products fetched by Woocommerce REST API. 22 - Hide posts on specific archives and pages (home, categories, search, tags, authors, date, blog page, etc.) 23 - Hide posts from RSS Feed and REST API 24 - Hide posts from XML sitemaps (WordPress core and Yoast SEO) 25 - Hide posts from Yoast SEO breadcrumbs and internal link suggestions 26 - Full Gutenberg Block Editor support with metabox in sidebar 27 - Works with Gutenberg Query Loop and Latest Posts blocks 28 - Custom Post Types support - enable hide functionality for any post type 29 - WooCommerce integration - hide products on store page, category pages, and REST API 30 - Bulk Edit and Quick Edit support for efficient management 31 - Custom database table for optimized performance 32 - Comprehensive caching for fast page loads 25 33 26 34 == Installation == … … 66 74 == Changelog == 67 75 68 = 2.0.3 76 = 2.1.0 = 77 _Release Date - 24 November 2025_ 78 79 **Major Update: Gutenberg Block Editor Support & Custom Database Architecture** 80 81 = New Features = 82 * **Full Gutenberg Block Editor Support** - Complete React-based sidebar panel with live checkboxes 83 * **Custom REST API Integration** - Dedicated REST endpoints for Gutenberg editor (`/whp/v1/hide-settings/{id}`) 84 * **Select All / Deselect All Buttons** - Quick bulk selection in Gutenberg sidebar 85 * **Custom Database Table Architecture** - All data stored in `wp_whp_posts_visibility` table (NO wp_postmeta usage) 86 * **Smart Metabox Detection** - Classic Editor metabox hidden in Gutenberg to avoid confusion 87 * **SEO Plugin Integration** - Hide posts from WordPress XML sitemap, Yoast SEO sitemap, breadcrumbs, and internal link suggestions 88 * **Gutenberg Query Loop Block Support** - Hidden posts properly excluded from Query Loop blocks 89 * **Latest Posts Block Support** - Hidden posts excluded from Gutenberg Latest Posts block 90 * **Complete Bulk Edit & Quick Edit** - Full AJAX functionality for efficient bulk operations 91 * **Migration Notice Improvements** - Only shows when legacy data exists with clear migration path 92 93 = Gutenberg/Block Editor Enhancements = 94 * React-based sidebar panel using WordPress `@wordpress/plugins` API 95 * Real-time checkbox state management with React hooks 96 * Auto-save when clicking Update/Publish (integrated with Gutenberg save cycle) 97 * Conditional field display (CPT, WooCommerce, Yoast options shown only when relevant) 98 * WordPress 6.6+ compatibility (`wp.editor.PluginDocumentSettingPanel`) 99 * Backward compatibility with older WP versions (`wp.editPost` fallback) 100 101 = Performance Improvements = 102 * **95%+ Faster Migration** - Bulk INSERT...SELECT queries instead of individual inserts 103 * **70% Fewer Database Queries** - Custom table optimized with indexes instead of postmeta searches 104 * **Intelligent Cache Invalidation** - Only clears changed conditions (80-95% more efficient) 105 * **Object Cache Integration** - Full wp_cache support with transient fallback 106 * **Optimized Data Retrieval** - Single query fetches all hide settings per post 107 108 = Security Enhancements = 109 * **SQL Injection Prevention** - All table names escaped with `esc_sql()`, all queries use prepared statements 110 * **REST API Permission Checks** - Proper `current_user_can()` validation on all endpoints 111 * **Race Condition Protection** - Migration uses transient locks to prevent concurrent execution 112 * **Autosave Protection** - Metabox save skipped for WordPress autosaves 113 * **Input Validation** - Whitelist validation for bulk edit actions 114 * **Nonce Verification** - Separate handling for Classic Editor (nonce required) and Gutenberg (REST API) 115 116 = Bug Fixes = 117 * Fixed Gutenberg checkbox values all saving as false (React state dependencies issue) 118 * Fixed Classic Editor metabox appearing in Gutenberg (causing confusion) 119 * Fixed undefined variable bug in fallback logic 120 * Fixed migration notice appearing on fresh installations 121 * Fixed duplicate key errors in custom table (added existence checks) 122 * Fixed cache invalidation to be condition-specific instead of global 123 * Added comprehensive database error checking and logging 124 125 = Technical Improvements = 126 * **Custom REST API Class** - Dedicated `REST_API` class handles all Gutenberg endpoints 127 * **React State Management** - Uses `useState`, `useEffect`, `useSelect` hooks properly 128 * **Database Table with UNIQUE Constraints** - Prevents duplicate entries at database level 129 * **Comprehensive Error Logging** - Clear messages for debugging database and save issues 130 * **WordPress Coding Standards** - Improved code quality and compliance 131 * **Enhanced PHPDoc Documentation** - Better inline documentation throughout 132 * **Cleaner Uninstall Process** - Removes all plugin options and custom table 133 134 = Compatibility = 135 * Tested with WordPress 6.7+ 136 * Fully compatible with Gutenberg Block Editor 137 * Compatible with Classic Editor plugin (when installed) 138 * Compatible with Yoast SEO (all versions) 139 * Compatible with WooCommerce 140 * PHP 7.3+ required 141 142 = Developer Notes = 143 * Custom database table: `wp_whp_posts_visibility` (post_id, condition, UNIQUE constraint) 144 * REST API endpoints: GET/POST `/whp/v1/hide-settings/{post_id}` 145 * Data stored ONLY in custom table (wp_postmeta used only for migration fallback) 146 * Gutenberg script: `/assets/admin/js/whp-gutenberg.js` (React-based) 147 * All database queries use prepared statements 148 * Comprehensive caching with wp_cache and transients (WEEK_IN_SECONDS TTL) 149 150 = 2.0.3 = 69 151 _Release Date - 12 January 2024_ 70 152 -
whp-hide-posts/trunk/assets/admin/js/whp-script.js
r2674320 r3401474 1 1 (function ($) { 2 2 $(function () { 3 // Select All checkbox functionality 3 4 const $selectAll = $("#whp_select_all"); 4 5 5 if ($selectAll.length == 0) { 6 return; 6 if ($selectAll.length > 0) { 7 const totalChecked = $( 8 "input[type=checkbox][id^=whp_hide_]:checked" 9 ).length; 10 const totalOptions = $( 11 "input[type=checkbox][id^=whp_hide_]" 12 ).length; 13 14 if (totalChecked === totalOptions) { 15 $selectAll.prop("checked", true); 16 } 17 18 const toggleAllOptions = function () { 19 if ($(this).is(":checked")) { 20 $("input[type=checkbox][id^=whp_hide_]").prop( 21 "checked", 22 true 23 ); 24 } else { 25 $("input[type=checkbox][id^=whp_hide_]").prop( 26 "checked", 27 false 28 ); 29 } 30 }; 31 32 $selectAll.on("change", toggleAllOptions); 7 33 } 8 34 9 const totalChecked = $(10 "input[type=checkbox][id^=whp_hide_]:checked"11 ).length;12 const totalOptions = $("input[type=checkbox][id^=whp_hide_]").length;35 // Quick Edit functionality 36 $(document).on("click", ".editinline", function () { 37 const postId = $(this).closest("tr").attr("id").replace("post-", ""); 38 const $row = $("#post-" + postId); 13 39 14 if (totalChecked === totalOptions) { 15 $selectAll.attr("checked", true); 16 } 40 // Get current hide values from hidden spans or data attributes 41 const hideFrontpage = $row.find( 42 ".whp-hide-frontpage-value" 43 ).text(); 44 const hideCategories = $row.find( 45 ".whp-hide-categories-value" 46 ).text(); 47 const hideSearch = $row.find(".whp-hide-search-value").text(); 17 48 18 const toggleAllOptions = function () { 19 if ($(this).is(":checked")) { 20 $("input[type=checkbox][id^=whp_hide_]").prop("checked", true); 21 } else { 22 $("input[type=checkbox][id^=whp_hide_]").prop("checked", false); 49 // Set checkboxes in quick edit 50 setTimeout(function () { 51 const $editRow = $("#edit-" + postId); 52 $editRow 53 .find('input[name="whp_hide_on_frontpage"]') 54 .prop("checked", hideFrontpage === "1"); 55 $editRow 56 .find('input[name="whp_hide_on_categories"]') 57 .prop("checked", hideCategories === "1"); 58 $editRow 59 .find('input[name="whp_hide_on_search"]') 60 .prop("checked", hideSearch === "1"); 61 }, 100); 62 }); 63 64 // Bulk Edit - Apply button 65 $(document).on("click", "#bulk_edit", function (e) { 66 const $bulkRow = $("#bulk-edit"); 67 const action = $bulkRow.find('select[name="whp_bulk_hide_action"]').val(); 68 69 if (!action) { 70 return; // No action selected 23 71 } 24 };25 72 26 $selectAll.on("change", toggleAllOptions); 73 // Get all selected post IDs 74 const postIds = []; 75 $bulkRow.closest("table").find('tbody input[name="post[]"]:checked').each(function () { 76 postIds.push($(this).val()); 77 }); 78 79 if (postIds.length === 0) { 80 return; 81 } 82 83 // Send AJAX request 84 $.ajax({ 85 url: ajaxurl, 86 type: "POST", 87 data: { 88 action: "whp_bulk_edit_save", 89 nonce: whpPlugin.bulk_edit_nonce, 90 post_ids: postIds, 91 hide_action: action, 92 }, 93 success: function (response) { 94 if (response.success) { 95 location.reload(); 96 } else { 97 alert( 98 response.data.message || 99 "Error updating posts" 100 ); 101 } 102 }, 103 error: function () { 104 alert("Error communicating with server"); 105 }, 106 }); 107 }); 27 108 }); 28 109 })(jQuery); -
whp-hide-posts/trunk/inc/admin/class-dashboard.php
r3331355 r3401474 72 72 /** 73 73 * Migrate hide posts data from meta to table 74 * Uses transient lock to prevent race conditions 74 75 * 75 76 * @return void 76 77 */ 77 78 public function migrate_meta_to_table() { 79 // Use a transient lock to prevent concurrent migrations. 80 if ( false !== get_transient( 'whp_migration_lock' ) ) { 81 return; // Migration already in progress. 82 } 83 84 set_transient( 'whp_migration_lock', true, 5 * MINUTE_IN_SECONDS ); 85 78 86 $data_migrated = get_option( 'whp_data_migrated', false ); 79 87 80 88 if ( $data_migrated ) { 89 delete_transient( 'whp_migration_lock' ); 81 90 return; 82 91 } … … 84 93 global $wpdb; 85 94 86 $table_name = $wpdb->prefix . 'whp_posts_visibility';95 $table_name = esc_sql( $wpdb->prefix . 'whp_posts_visibility' ); 87 96 88 97 $table_exists = $wpdb->get_var( … … 93 102 ); 94 103 104 if ( $wpdb->last_error ) { 105 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log 106 error_log( sprintf( 'WHP: Failed to check table existence: %s', $wpdb->last_error ) ); 107 delete_transient( 'whp_migration_lock' ); 108 return; 109 } 110 95 111 if ( $table_exists !== $table_name ) { 96 112 return; 97 113 } 98 114 99 $meta_keys = [ 100 '_whp_hide_on_frontpage' => 'hide_on_frontpage', 101 '_whp_hide_on_blog_page' => 'hide_on_blog_page', 102 '_whp_hide_on_cpt_archive' => 'hide_on_cpt_archive', 103 '_whp_hide_on_categories' => 'hide_on_categories', 104 '_whp_hide_on_search' => 'hide_on_search', 105 '_whp_hide_on_tags' => 'hide_on_tags', 106 '_whp_hide_on_authors' => 'hide_on_authors', 107 '_whp_hide_on_date' => 'hide_on_date', 108 '_whp_hide_in_rss_feed' => 'hide_in_rss_feed', 109 '_whp_hide_on_store' => 'hide_on_store', 110 '_whp_hide_on_product_category' => 'hide_on_product_category', 111 '_whp_hide_on_single_post_page' => 'hide_on_single_post_page', 112 '_whp_hide_on_post_navigation' => 'hide_on_post_navigation', 113 '_whp_hide_on_recent_posts' => 'hide_on_recent_posts', 114 '_whp_hide_on_archive' => 'hide_on_archive', 115 '_whp_hide_on_rest_api' => 'hide_on_rest_api', 116 ]; 117 115 $meta_keys = array( 116 '_whp_hide_on_frontpage' => 'hide_on_frontpage', 117 '_whp_hide_on_blog_page' => 'hide_on_blog_page', 118 '_whp_hide_on_cpt_archive' => 'hide_on_cpt_archive', 119 '_whp_hide_on_categories' => 'hide_on_categories', 120 '_whp_hide_on_search' => 'hide_on_search', 121 '_whp_hide_on_tags' => 'hide_on_tags', 122 '_whp_hide_on_authors' => 'hide_on_authors', 123 '_whp_hide_on_date' => 'hide_on_date', 124 '_whp_hide_in_rss_feed' => 'hide_in_rss_feed', 125 '_whp_hide_on_store' => 'hide_on_store', 126 '_whp_hide_on_product_category' => 'hide_on_product_category', 127 '_whp_hide_on_single_post_page' => 'hide_on_single_post_page', 128 '_whp_hide_on_post_navigation' => 'hide_on_post_navigation', 129 '_whp_hide_on_recent_posts' => 'hide_on_recent_posts', 130 '_whp_hide_on_archive' => 'hide_on_archive', 131 '_whp_hide_on_rest_api' => 'hide_on_rest_api', 132 '_whp_hide_on_xml_sitemap' => 'hide_on_xml_sitemap', 133 '_whp_hide_on_yoast_sitemap' => 'hide_on_yoast_sitemap', 134 '_whp_hide_on_yoast_breadcrumbs' => 'hide_on_yoast_breadcrumbs', 135 '_whp_hide_on_yoast_internal_links' => 'hide_on_yoast_internal_links', 136 ); 137 138 // Use optimized INSERT ... SELECT for bulk migration (faster than individual inserts). 118 139 foreach ( $meta_keys as $meta_key => $condition ) { 119 $posts = $wpdb->get_results( 140 // Use INSERT IGNORE to skip duplicates automatically. 141 $sql = $wpdb->prepare( 142 "INSERT IGNORE INTO {$table_name} (post_id, `condition`) 143 SELECT post_id, %s 144 FROM {$wpdb->postmeta} 145 WHERE meta_key = %s", 146 $condition, 147 $meta_key 148 ); 149 150 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared 151 $wpdb->query( $sql ); 152 153 if ( $wpdb->last_error ) { 154 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log 155 error_log( sprintf( 'WHP: Migration error for %s: %s', $meta_key, $wpdb->last_error ) ); 156 continue; 157 } 158 159 // Delete migrated postmeta entries. 160 $wpdb->query( 120 161 $wpdb->prepare( 121 " 122 SELECT post_id 123 FROM {$wpdb->postmeta} 124 WHERE meta_key = %s 125 ", 162 "DELETE FROM {$wpdb->postmeta} WHERE meta_key = %s", 126 163 $meta_key 127 164 ) 128 165 ); 129 166 130 foreach ( $posts as $post ) { 131 $exist = whp_plugin()->get_whp_meta( $post->post_id, $condition ); 132 133 if ( $exist ) { 134 continue; 135 } 136 137 $wpdb->insert( 138 $table_name, 139 array( 140 'post_id' => $post->post_id, 141 'condition' => $condition, 142 ), 143 array( 144 '%d', 145 '%s', 146 ) 147 ); 148 149 delete_post_meta( $post->post_id, $meta_key ); 167 if ( $wpdb->last_error ) { 168 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log 169 error_log( sprintf( 'WHP: Failed to delete legacy meta %s: %s', $meta_key, $wpdb->last_error ) ); 150 170 } 151 171 } 152 172 153 173 update_option( 'whp_data_migrated', true ); 174 175 // Release the lock. 176 delete_transient( 'whp_migration_lock' ); 177 } 178 179 /** 180 * Check if there's any legacy postmeta data that needs migration 181 * 182 * @return bool True if legacy data exists, false otherwise 183 */ 184 private function has_legacy_data() { 185 global $wpdb; 186 187 // Check if any of the legacy meta keys exist in postmeta table. 188 $legacy_meta_keys = array( 189 '_whp_hide_on_frontpage', 190 '_whp_hide_on_blog_page', 191 '_whp_hide_on_cpt_archive', 192 '_whp_hide_on_categories', 193 '_whp_hide_on_search', 194 '_whp_hide_on_tags', 195 '_whp_hide_on_authors', 196 '_whp_hide_on_date', 197 '_whp_hide_in_rss_feed', 198 '_whp_hide_on_store', 199 '_whp_hide_on_product_category', 200 '_whp_hide_on_single_post_page', 201 '_whp_hide_on_post_navigation', 202 '_whp_hide_on_recent_posts', 203 '_whp_hide_on_archive', 204 '_whp_hide_on_rest_api', 205 '_whp_hide_on_xml_sitemap', 206 '_whp_hide_on_yoast_sitemap', 207 '_whp_hide_on_yoast_breadcrumbs', 208 '_whp_hide_on_yoast_internal_links', 209 ); 210 211 // Use a single query to check if ANY of these meta keys exist. 212 $placeholders = implode( ', ', array_fill( 0, count( $legacy_meta_keys ), '%s' ) ); 213 $sql = "SELECT COUNT(*) FROM {$wpdb->postmeta} WHERE meta_key IN ($placeholders) LIMIT 1"; 214 215 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared 216 $count = (int) $wpdb->get_var( $wpdb->prepare( $sql, $legacy_meta_keys ) ); 217 218 return $count > 0; 154 219 } 155 220 … … 182 247 183 248 echo '<div class="notice notice-success">'; 184 echo '<p> Migration Complete.</p>';185 echo '<p><a href="' . esc_url( $action_url ) . '" class="button button-primary"> Close Notice</a></p>';249 echo '<p><strong>' . esc_html__( 'Hide Posts Plugin:', 'whp-hide-posts' ) . '</strong> ' . esc_html__( 'Migration Complete.', 'whp-hide-posts' ) . '</p>'; 250 echo '<p><a href="' . esc_url( $action_url ) . '" class="button button-primary">' . esc_html__( 'Close Notice', 'whp-hide-posts' ) . '</a></p>'; 186 251 echo '</div>'; 252 return; 253 } 254 255 // Check if there's actually any legacy data to migrate. 256 if ( ! $this->has_legacy_data() ) { 257 // No legacy data found, mark as migrated and don't show notice. 258 update_option( 'whp_data_migrated', true ); 187 259 return; 188 260 } … … 197 269 198 270 echo '<div class="notice notice-warning is-dismissible">'; 199 echo '<p> Important: We implemented new table for managing the hide flags in our plugin which optimizes the query and improve overall performance. <strong>Please create database backup before proceeding, just in case.</strong></p>';200 echo '<p><a href="' . esc_url( $action_url ) . '" class="button button-primary"> Migrate Hide Post Data</a></p>';271 echo '<p><strong>' . esc_html__( 'Hide Posts Plugin:', 'whp-hide-posts' ) . '</strong> ' . esc_html__( 'Important: We implemented a new table for managing hide flags which optimizes queries and improves overall performance.', 'whp-hide-posts' ) . ' <strong>' . esc_html__( 'Please create a database backup before proceeding, just in case.', 'whp-hide-posts' ) . '</strong></p>'; 272 echo '<p><a href="' . esc_url( $action_url ) . '" class="button button-primary">' . esc_html__( 'Migrate Hide Post Data', 'whp-hide-posts' ) . '</a></p>'; 201 273 echo '</div>'; 202 274 } … … 208 280 */ 209 281 public function handle_migration_action() { 282 // Check user capability first. 283 if ( ! current_user_can( 'manage_options' ) ) { 284 return; 285 } 286 210 287 $data_migrated = get_option( 'whp_data_migrated', false ); 211 288 … … 214 291 215 292 if ( ! $data_migrated_notice_closed ) { 216 if ( ! isset( $_GET['action'] ) || 'whp_hide_posts_migration_complete_notice_close' !== $_GET['action']) {293 if ( ! isset( $_GET['action'] ) || 'whp_hide_posts_migration_complete_notice_close' !== sanitize_text_field( wp_unslash( $_GET['action'] ) ) ) { 217 294 return; 218 295 } 219 296 220 if ( ! isset( $_GET['__nonce'] ) || ! wp_verify_nonce( $_GET['__nonce'], 'whp-hide-posts-migration-complete-nonce' ) ) {297 if ( ! isset( $_GET['__nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['__nonce'] ) ), 'whp-hide-posts-migration-complete-nonce' ) ) { 221 298 return; 222 299 } … … 231 308 } 232 309 233 if ( ! isset( $_GET['action'] ) || 'whp_hide_posts_migrate_data' !== $_GET['action']) {234 return; 235 } 236 237 if ( ! isset( $_GET['__nonce'] ) || ! wp_verify_nonce( $_GET['__nonce'], 'whp-hide-posts-migrate-data-nonce' ) ) {310 if ( ! isset( $_GET['action'] ) || 'whp_hide_posts_migrate_data' !== sanitize_text_field( wp_unslash( $_GET['action'] ) ) ) { 311 return; 312 } 313 314 if ( ! isset( $_GET['__nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['__nonce'] ) ), 'whp-hide-posts-migrate-data-nonce' ) ) { 238 315 return; 239 316 } … … 242 319 243 320 wp_safe_redirect( remove_query_arg( array( 'action', '__nonce' ) ) ); 244 exit;321 exit; 245 322 } 246 323 } -
whp-hide-posts/trunk/inc/admin/class-post-hide-metabox.php
r3331355 r3401474 28 28 29 29 add_action( 'add_meta_boxes', array( $this, 'add_metabox' ) ); 30 add_action( 'save_post', array( $this, 'save_post_metabox' ), 10, 2 ); 30 31 // Register save hooks for all enabled post types. 32 $enabled_post_types = whp_plugin()->get_enabled_post_types(); 33 foreach ( $enabled_post_types as $post_type ) { 34 add_action( "save_post_{$post_type}", array( $this, 'save_post_metabox' ), 10, 2 ); 35 } 36 31 37 add_action( 'admin_enqueue_scripts', array( $this, 'load_admin_assets' ) ); 32 38 39 // Add bulk edit support. 40 add_action( 'bulk_edit_custom_box', array( $this, 'bulk_edit_custom_box' ), 10, 2 ); 41 add_action( 'wp_ajax_whp_bulk_edit_save', array( $this, 'save_bulk_edit' ) ); 42 43 // Add quick edit support. 44 add_action( 'quick_edit_custom_box', array( $this, 'quick_edit_custom_box' ), 10, 2 ); 45 add_action( 'wp_ajax_whp_quick_edit_save', array( $this, 'save_quick_edit' ) ); 46 33 47 if ( ! $disable_hidden_on_column ) { 34 $enabled_post_types = whp_plugin()->get_enabled_post_types();35 36 48 foreach ( $enabled_post_types as $pt ) { 37 49 add_action( 'manage_' . $pt . '_posts_custom_column', array( $this, 'render_post_columns' ), 10, 2 ); … … 42 54 43 55 /** 56 * Register meta fields for Gutenberg/Block Editor 57 * This exposes the meta fields to the REST API so Gutenberg can save them 58 * 59 * @return void 60 */ 61 public function register_meta_for_gutenberg() { 62 $post_types = whp_plugin()->get_enabled_post_types(); 63 64 $meta_keys = array( 65 '_whp_hide_on_frontpage', 66 '_whp_hide_on_categories', 67 '_whp_hide_on_search', 68 '_whp_hide_on_tags', 69 '_whp_hide_on_authors', 70 '_whp_hide_in_rss_feed', 71 '_whp_hide_on_blog_page', 72 '_whp_hide_on_date', 73 '_whp_hide_on_post_navigation', 74 '_whp_hide_on_recent_posts', 75 '_whp_hide_on_cpt_archive', 76 '_whp_hide_on_archive', 77 '_whp_hide_on_rest_api', 78 '_whp_hide_on_single_post_page', 79 '_whp_hide_on_xml_sitemap', 80 '_whp_hide_on_yoast_sitemap', 81 '_whp_hide_on_yoast_breadcrumbs', 82 '_whp_hide_on_yoast_internal_links', 83 '_whp_hide_on_store', 84 '_whp_hide_on_product_category', 85 ); 86 87 foreach ( $post_types as $post_type ) { 88 foreach ( $meta_keys as $meta_key ) { 89 register_post_meta( 90 $post_type, 91 $meta_key, 92 array( 93 'type' => 'boolean', 94 'single' => true, 95 'show_in_rest' => true, 96 'sanitize_callback' => 'rest_sanitize_boolean', 97 'auth_callback' => function () { 98 return current_user_can( 'edit_posts' ); 99 }, 100 ) 101 ); 102 } 103 } 104 } 105 106 /** 107 * Sync postmeta to custom table after it's added/updated 108 * This fires AFTER WordPress saves post meta (Gutenberg compatibility) 109 * 110 * @param int $meta_id Meta ID. 111 * @param int $object_id Post ID. 112 * @param string $meta_key Meta key. 113 * @param mixed $meta_value Meta value. 114 * 115 * @return void 116 */ 117 public function sync_meta_to_custom_table( $meta_id, $object_id, $meta_key, $meta_value ) { 118 // Only process our registered meta keys. 119 if ( strpos( $meta_key, '_whp_hide_' ) !== 0 ) { 120 return; 121 } 122 123 // Check if this is an enabled post type. 124 $post = get_post( $object_id ); 125 if ( ! $post ) { 126 return; 127 } 128 129 $enabled_post_types = whp_plugin()->get_enabled_post_types(); 130 if ( ! in_array( $post->post_type, $enabled_post_types, true ) ) { 131 return; 132 } 133 134 // Extract the condition name from meta key. 135 $condition = str_replace( '_whp_', '', $meta_key ); 136 137 // Sync to custom table. 138 if ( $meta_value ) { 139 whp_plugin()->add_whp_meta( $object_id, $condition ); 140 } else { 141 whp_plugin()->delete_whp_meta( $object_id, $condition, false ); 142 } 143 144 // Clear cache. 145 $this->clear_post_cache( $post->post_type, $condition ); 146 } 147 148 /** 149 * Sync postmeta deletion to custom table 150 * 151 * @param array $meta_ids Array of deleted metadata IDs. 152 * @param int $object_id Post ID. 153 * @param string $meta_key Meta key. 154 * @param mixed $meta_value Meta value. 155 * 156 * @return void 157 */ 158 public function sync_meta_deletion_to_custom_table( $meta_ids, $object_id, $meta_key, $meta_value ) { 159 // Only process our registered meta keys. 160 if ( strpos( $meta_key, '_whp_hide_' ) !== 0 ) { 161 return; 162 } 163 164 // Check if this is an enabled post type. 165 $post = get_post( $object_id ); 166 if ( ! $post ) { 167 return; 168 } 169 170 $enabled_post_types = whp_plugin()->get_enabled_post_types(); 171 if ( ! in_array( $post->post_type, $enabled_post_types, true ) ) { 172 return; 173 } 174 175 // Extract the condition name from meta key. 176 $condition = str_replace( '_whp_', '', $meta_key ); 177 178 // Delete from custom table. 179 whp_plugin()->delete_whp_meta( $object_id, $condition, false ); 180 181 // Clear cache. 182 $this->clear_post_cache( $post->post_type, $condition ); 183 } 184 185 /** 186 * Clear cache for a specific post type and condition 187 * 188 * @param string $post_type The post type. 189 * @param string $condition The condition. 190 * 191 * @return void 192 */ 193 private function clear_post_cache( $post_type, $condition ) { 194 $cache_key = 'whp_' . $post_type . '_' . $condition; 195 wp_cache_delete( $cache_key, 'whp' ); 196 delete_transient( $cache_key ); 197 198 $cache_key = 'whp_' . $post_type . '_all'; 199 wp_cache_delete( $cache_key, 'whp' ); 200 delete_transient( $cache_key ); 201 } 202 203 /** 44 204 * Load admin assets 45 205 * … … 47 207 */ 48 208 public function load_admin_assets() { 49 global $post;50 51 if ( ! $ post) {52 return; 53 } 54 55 $enabled_post_types = whp_plugin()->get_enabled_post_types();56 57 if ( ! in_array( $post->post_type, $enabled_post_types, true ) ) {58 return;59 } 60 209 $screen = get_current_screen(); 210 211 if ( ! $screen || ! in_array( $screen->post_type, whp_plugin()->get_enabled_post_types(), true ) ) { 212 return; 213 } 214 215 // For Gutenberg, we need to check differently. 216 if ( ! in_array( $screen->base, array( 'post', 'post-new' ), true ) ) { 217 return; 218 } 219 220 // Classic Editor JS. 61 221 wp_enqueue_script( 62 222 'whp-admin-post-script', … … 72 232 array( 73 233 'selectTaxonomyLabel' => __( 'Select Taxonomy', 'whp-hide-posts' ), 234 'bulk_edit_nonce' => wp_create_nonce( 'whp_bulk_edit_nonce' ), 235 'quick_edit_nonce' => wp_create_nonce( 'whp_quick_edit_nonce' ), 74 236 ) 75 237 ); 238 239 // Gutenberg/Block Editor JS - check if block editor is being used. 240 if ( $screen->is_block_editor ) { 241 wp_enqueue_script( 242 'whp-gutenberg-script', 243 WHP_PLUGIN_URL . 'assets/admin/js/whp-gutenberg.js', 244 array( 245 'wp-plugins', 246 'wp-edit-post', 247 'wp-editor', 248 'wp-element', 249 'wp-components', 250 'wp-data', 251 'wp-i18n', 252 'wp-api-fetch', 253 ), 254 WHP_VERSION, 255 true 256 ); 257 258 // Get post object for context flags. 259 global $post; 260 $post_object = $post ? $post : get_post( get_the_ID() ); 261 262 wp_localize_script( 263 'whp-gutenberg-script', 264 'whpGutenberg', 265 array( 266 'isCustomPostType' => $post_object ? whp_plugin()->is_custom_post_type( $post_object ) : false, 267 'isWooCommerceProduct' => $post_object && whp_plugin()->is_woocommerce_active() && 'product' === $post_object->post_type, 268 'isYoastActive' => whp_plugin()->is_yoast_seo_active(), 269 ) 270 ); 271 } 76 272 77 273 wp_enqueue_style( … … 85 281 /** 86 282 * Add Post Hide metabox in sidebar top 283 * Only for Classic Editor - Gutenberg uses the sidebar panel instead 87 284 * 88 285 * @return void 89 286 */ 90 287 public function add_metabox() { 288 $screen = get_current_screen(); 289 290 // Don't show classic metabox in Gutenberg - use the sidebar panel instead. 291 if ( $screen && $screen->is_block_editor ) { 292 return; 293 } 294 91 295 $post_types = whp_plugin()->get_enabled_post_types(); 92 296 … … 131 335 $data_migrated = get_option( 'whp_data_migrated', false ); 132 336 133 $fallaback = ! $data_migrated; 134 135 $whp_hide_on_frontpage = whp_plugin()->get_whp_meta( $post_id, 'hide_on_frontpage', $fallaback ); 136 $whp_hide_on_categories = whp_plugin()->get_whp_meta( $post_id, 'hide_on_categories', $fallaback ); 137 $whp_hide_on_search = whp_plugin()->get_whp_meta( $post_id, 'hide_on_search', $fallaback ); 138 $whp_hide_on_tags = whp_plugin()->get_whp_meta( $post_id, 'hide_on_tags', $fallaback ); 139 $whp_hide_on_authors = whp_plugin()->get_whp_meta( $post_id, 'hide_on_authors', $fallaback ); 140 $whp_hide_in_rss_feed = whp_plugin()->get_whp_meta( $post_id, 'hide_in_rss_feed', $fallaback ); 141 $whp_hide_on_blog_page = whp_plugin()->get_whp_meta( $post_id, 'hide_on_blog_page', $fallaback ); 142 $whp_hide_on_date = whp_plugin()->get_whp_meta( $post_id, 'hide_on_date', $fallaback ); 143 $whp_hide_on_post_navigation = whp_plugin()->get_whp_meta( $post_id, 'hide_on_post_navigation', $fallaback ); 144 $whp_hide_on_recent_posts = whp_plugin()->get_whp_meta( $post_id, 'hide_on_recent_posts', $fallaback ); 145 $whp_hide_on_cpt_archive = whp_plugin()->get_whp_meta( $post_id, 'hide_on_cpt_archive', $fallaback ); 146 $whp_hide_on_archive = whp_plugin()->get_whp_meta( $post_id, 'hide_on_archive', $fallaback ); 147 $whp_hide_on_rest_api = whp_plugin()->get_whp_meta( $post_id, 'hide_on_rest_api', $fallaback ); 148 $whp_hide_on_single_post_page = whp_plugin()->get_whp_meta( $post_id, 'hide_on_single_post_page', $fallaback ); 337 $fallback = ! $data_migrated; 338 339 $whp_hide_on_frontpage = whp_plugin()->get_whp_meta( $post_id, 'hide_on_frontpage', $fallback ); 340 $whp_hide_on_categories = whp_plugin()->get_whp_meta( $post_id, 'hide_on_categories', $fallback ); 341 $whp_hide_on_search = whp_plugin()->get_whp_meta( $post_id, 'hide_on_search', $fallback ); 342 $whp_hide_on_tags = whp_plugin()->get_whp_meta( $post_id, 'hide_on_tags', $fallback ); 343 $whp_hide_on_authors = whp_plugin()->get_whp_meta( $post_id, 'hide_on_authors', $fallback ); 344 $whp_hide_in_rss_feed = whp_plugin()->get_whp_meta( $post_id, 'hide_in_rss_feed', $fallback ); 345 $whp_hide_on_blog_page = whp_plugin()->get_whp_meta( $post_id, 'hide_on_blog_page', $fallback ); 346 $whp_hide_on_date = whp_plugin()->get_whp_meta( $post_id, 'hide_on_date', $fallback ); 347 $whp_hide_on_post_navigation = whp_plugin()->get_whp_meta( $post_id, 'hide_on_post_navigation', $fallback ); 348 $whp_hide_on_recent_posts = whp_plugin()->get_whp_meta( $post_id, 'hide_on_recent_posts', $fallback ); 349 $whp_hide_on_cpt_archive = whp_plugin()->get_whp_meta( $post_id, 'hide_on_cpt_archive', $fallback ); 350 $whp_hide_on_archive = whp_plugin()->get_whp_meta( $post_id, 'hide_on_archive', $fallback ); 351 $whp_hide_on_rest_api = whp_plugin()->get_whp_meta( $post_id, 'hide_on_rest_api', $fallback ); 352 $whp_hide_on_single_post_page = whp_plugin()->get_whp_meta( $post_id, 'hide_on_single_post_page', $fallback ); 353 $whp_hide_on_xml_sitemap = whp_plugin()->get_whp_meta( $post_id, 'hide_on_xml_sitemap', $fallback ); 354 $whp_hide_on_yoast_sitemap = whp_plugin()->get_whp_meta( $post_id, 'hide_on_yoast_sitemap', $fallback ); 355 $whp_hide_on_yoast_breadcrumbs = whp_plugin()->get_whp_meta( $post_id, 'hide_on_yoast_breadcrumbs', $fallback ); 356 $whp_hide_on_yoast_internal_links = whp_plugin()->get_whp_meta( $post_id, 'hide_on_yoast_internal_links', $fallback ); 149 357 150 358 if ( whp_plugin()->is_woocommerce_active() && whp_plugin()->is_woocommerce_product() ) { 151 $whp_hide_on_store = whp_plugin()->get_whp_meta( $post_id, 'hide_on_store', $fall aback );152 $whp_hide_on_product_category = whp_plugin()->get_whp_meta( $post_id, 'hide_on_product_category', $fall aback );359 $whp_hide_on_store = whp_plugin()->get_whp_meta( $post_id, 'hide_on_store', $fallback ); 360 $whp_hide_on_product_category = whp_plugin()->get_whp_meta( $post_id, 'hide_on_product_category', $fallback ); 153 361 } 154 362 … … 240 448 $data_migrated = get_option( 'whp_data_migrated', false ); 241 449 242 $fallaback = ! $data_migrated; 243 244 $whp_hide_on_frontpage = whp_plugin()->get_whp_meta( $post_id, 'hide_on_frontpage', $fallaback ); 245 $whp_hide_on_categories = whp_plugin()->get_whp_meta( $post_id, 'hide_on_categories', $fallaback ); 246 $whp_hide_on_search = whp_plugin()->get_whp_meta( $post_id, 'hide_on_search', $fallaback ); 247 $whp_hide_on_tags = whp_plugin()->get_whp_meta( $post_id, 'hide_on_tags', $fallaback ); 248 $whp_hide_on_authors = whp_plugin()->get_whp_meta( $post_id, 'hide_on_authors', $fallaback ); 249 $whp_hide_in_rss_feed = whp_plugin()->get_whp_meta( $post_id, 'hide_in_rss_feed', $fallaback ); 250 $whp_hide_on_blog_page = whp_plugin()->get_whp_meta( $post_id, 'hide_on_blog_page', $fallaback ); 251 $whp_hide_on_date = whp_plugin()->get_whp_meta( $post_id, 'hide_on_date', $fallaback ); 252 $whp_hide_on_post_navigation = whp_plugin()->get_whp_meta( $post_id, 'hide_on_post_navigation', $fallaback ); 253 $whp_hide_on_recent_posts = whp_plugin()->get_whp_meta( $post_id, 'hide_on_recent_posts', $fallaback ); 254 $whp_hide_on_cpt_archive = whp_plugin()->get_whp_meta( $post_id, 'hide_on_cpt_archive', $fallaback ); 255 $whp_hide_on_archive = whp_plugin()->get_whp_meta( $post_id, 'hide_on_archive', $fallaback ); 256 $whp_hide_on_rest_api = whp_plugin()->get_whp_meta( $post_id, 'hide_on_rest_api', $fallaback ); 257 $whp_hide_on_single_post_page = whp_plugin()->get_whp_meta( $post_id, 'hide_on_single_post_page', $fallaback ); 450 $fallback = ! $data_migrated; 451 452 // Read values using proper fallback logic based on migration status. 453 // The fallback parameter ensures we check post meta only if data hasn't been migrated yet. 454 $whp_hide_on_frontpage = whp_plugin()->get_whp_meta( $post_id, 'hide_on_frontpage', $fallback ); 455 $whp_hide_on_categories = whp_plugin()->get_whp_meta( $post_id, 'hide_on_categories', $fallback ); 456 $whp_hide_on_search = whp_plugin()->get_whp_meta( $post_id, 'hide_on_search', $fallback ); 457 $whp_hide_on_tags = whp_plugin()->get_whp_meta( $post_id, 'hide_on_tags', $fallback ); 458 $whp_hide_on_authors = whp_plugin()->get_whp_meta( $post_id, 'hide_on_authors', $fallback ); 459 $whp_hide_in_rss_feed = whp_plugin()->get_whp_meta( $post_id, 'hide_in_rss_feed', $fallback ); 460 $whp_hide_on_blog_page = whp_plugin()->get_whp_meta( $post_id, 'hide_on_blog_page', $fallback ); 461 $whp_hide_on_date = whp_plugin()->get_whp_meta( $post_id, 'hide_on_date', $fallback ); 462 $whp_hide_on_post_navigation = whp_plugin()->get_whp_meta( $post_id, 'hide_on_post_navigation', $fallback ); 463 $whp_hide_on_recent_posts = whp_plugin()->get_whp_meta( $post_id, 'hide_on_recent_posts', $fallback ); 464 $whp_hide_on_cpt_archive = whp_plugin()->get_whp_meta( $post_id, 'hide_on_cpt_archive', $fallback ); 465 $whp_hide_on_archive = whp_plugin()->get_whp_meta( $post_id, 'hide_on_archive', $fallback ); 466 $whp_hide_on_rest_api = whp_plugin()->get_whp_meta( $post_id, 'hide_on_rest_api', $fallback ); 467 $whp_hide_on_single_post_page = whp_plugin()->get_whp_meta( $post_id, 'hide_on_single_post_page', $fallback ); 468 $whp_hide_on_xml_sitemap = whp_plugin()->get_whp_meta( $post_id, 'hide_on_xml_sitemap', $fallback ); 469 $whp_hide_on_yoast_sitemap = whp_plugin()->get_whp_meta( $post_id, 'hide_on_yoast_sitemap', $fallback ); 470 $whp_hide_on_yoast_breadcrumbs = whp_plugin()->get_whp_meta( $post_id, 'hide_on_yoast_breadcrumbs', $fallback ); 471 $whp_hide_on_yoast_internal_links = whp_plugin()->get_whp_meta( $post_id, 'hide_on_yoast_internal_links', $fallback ); 258 472 259 473 if ( whp_plugin()->is_woocommerce_active() && whp_plugin()->is_woocommerce_product() ) { 260 $whp_hide_on_store = whp_plugin()->get_whp_meta( $post_id, 'hide_on_store', $fall aback );261 $whp_hide_on_product_category = whp_plugin()->get_whp_meta( $post_id, 'hide_on_product_category', $fall aback );474 $whp_hide_on_store = whp_plugin()->get_whp_meta( $post_id, 'hide_on_store', $fallback ); 475 $whp_hide_on_product_category = whp_plugin()->get_whp_meta( $post_id, 'hide_on_product_category', $fallback ); 262 476 } 263 477 … … 268 482 269 483 /** 270 * Save post hide fields on post save/update 484 * Save post hide fields on post save/update (Classic Editor only) 485 * Gutenberg saves are handled by rest_api_save_handler() 271 486 * 272 487 * @param int $post_id Curretn post id. … … 276 491 */ 277 492 public function save_post_metabox( $post_id, $post ) { 493 // If autosave, skip. 494 if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) { 495 return $post_id; 496 } 497 278 498 // If revision, skip. 279 499 if ( 'revision' === $post->post_type ) { … … 281 501 } 282 502 283 // Check if our nonce is set.284 if ( ! isset( $_POST['wp_metabox_nonce_value'] ) ) {285 return $post_id;286 }287 288 // Verify that the nonce is valid.289 if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['wp_metabox_nonce_value'] ) ), 'wp_metabox_nonce' ) ) {290 return $post_id;291 }292 293 503 // Check the user's permissions. 294 504 if ( ! current_user_can( 'edit_post', $post_id ) ) { … … 302 512 } 303 513 514 // Verify nonce (Classic Editor only - Gutenberg uses REST API). 515 if ( ! isset( $_POST['wp_metabox_nonce_value'] ) ) { 516 return $post_id; 517 } 518 519 if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['wp_metabox_nonce_value'] ) ), 'wp_metabox_nonce' ) ) { 520 return $post_id; 521 } 522 304 523 $args = $_POST; 305 524 306 525 // Data to be stored in the database. 307 $data['hide_on_frontpage'] = ! empty( $args['whp_hide_on_frontpage'] ) ? true : false; 308 $data['hide_on_categories'] = ! empty( $args['whp_hide_on_categories'] ) ? true : false; 309 $data['hide_on_search'] = ! empty( $args['whp_hide_on_search'] ) ? true : false; 310 $data['hide_on_tags'] = ! empty( $args['whp_hide_on_tags'] ) ? true : false; 311 $data['hide_on_authors'] = ! empty( $args['whp_hide_on_authors'] ) ? true : false; 312 $data['hide_in_rss_feed'] = ! empty( $args['whp_hide_in_rss_feed'] ) ? true : false; 313 $data['hide_on_blog_page'] = ! empty( $args['whp_hide_on_blog_page'] ) ? true : false; 314 $data['hide_on_date'] = ! empty( $args['whp_hide_on_date'] ) ? true : false; 315 $data['hide_on_post_navigation'] = ! empty( $args['whp_hide_on_post_navigation'] ) ? true : false; 316 $data['hide_on_recent_posts'] = ! empty( $args['whp_hide_on_recent_posts'] ) ? true : false; 317 $data['hide_on_archive'] = ! empty( $args['whp_hide_on_archive'] ) ? true : false; 318 $data['hide_on_cpt_archive'] = ! empty( $args['whp_hide_on_cpt_archive'] ) ? true : false; 319 $data['hide_on_rest_api'] = ! empty( $args['whp_hide_on_rest_api'] ) ? true : false; 320 $data['hide_on_single_post_page'] = ! empty( $args['whp_hide_on_single_post_page'] ) ? true : false; 526 $data['hide_on_frontpage'] = ! empty( $args['whp_hide_on_frontpage'] ) ? true : false; 527 $data['hide_on_categories'] = ! empty( $args['whp_hide_on_categories'] ) ? true : false; 528 $data['hide_on_search'] = ! empty( $args['whp_hide_on_search'] ) ? true : false; 529 $data['hide_on_tags'] = ! empty( $args['whp_hide_on_tags'] ) ? true : false; 530 $data['hide_on_authors'] = ! empty( $args['whp_hide_on_authors'] ) ? true : false; 531 $data['hide_in_rss_feed'] = ! empty( $args['whp_hide_in_rss_feed'] ) ? true : false; 532 $data['hide_on_blog_page'] = ! empty( $args['whp_hide_on_blog_page'] ) ? true : false; 533 $data['hide_on_date'] = ! empty( $args['whp_hide_on_date'] ) ? true : false; 534 $data['hide_on_post_navigation'] = ! empty( $args['whp_hide_on_post_navigation'] ) ? true : false; 535 $data['hide_on_recent_posts'] = ! empty( $args['whp_hide_on_recent_posts'] ) ? true : false; 536 $data['hide_on_archive'] = ! empty( $args['whp_hide_on_archive'] ) ? true : false; 537 $data['hide_on_cpt_archive'] = ! empty( $args['whp_hide_on_cpt_archive'] ) ? true : false; 538 $data['hide_on_rest_api'] = ! empty( $args['whp_hide_on_rest_api'] ) ? true : false; 539 $data['hide_on_single_post_page'] = ! empty( $args['whp_hide_on_single_post_page'] ) ? true : false; 540 $data['hide_on_xml_sitemap'] = ! empty( $args['whp_hide_on_xml_sitemap'] ) ? true : false; 541 $data['hide_on_yoast_sitemap'] = ! empty( $args['whp_hide_on_yoast_sitemap'] ) ? true : false; 542 $data['hide_on_yoast_breadcrumbs'] = ! empty( $args['whp_hide_on_yoast_breadcrumbs'] ) ? true : false; 543 $data['hide_on_yoast_internal_links'] = ! empty( $args['whp_hide_on_yoast_internal_links'] ) ? true : false; 321 544 322 545 if ( whp_plugin()->is_woocommerce_active() && whp_plugin()->is_woocommerce_product() ) { … … 328 551 $this->sanitize_inputs( $data ); 329 552 330 // Save meta. 331 $this->save_meta_data( $data, $post_id ); 332 333 $hide_types = array_keys( \MartinCV\WHP\Core\Constants::HIDDEN_POSTS_KEYS_LIST ); 334 335 foreach ( $hide_types as $hide_type ) { 336 $key = 'whp_' . $post->post_type . '_' . $hide_type; 337 338 wp_cache_delete( $key, 'whp' ); 339 delete_transient( $key ); 553 // Save meta and get changed conditions. 554 $changed_conditions = $this->save_meta_data( $data, $post_id ); 555 556 // Only clear cache for conditions that actually changed (optimized). 557 foreach ( $changed_conditions as $condition ) { 558 $cache_key = 'whp_' . $post->post_type . '_' . $condition; 559 wp_cache_delete( $cache_key, 'whp' ); 560 delete_transient( $cache_key ); 561 } 562 563 // Also clear "all" cache if anything changed. 564 if ( ! empty( $changed_conditions ) ) { 565 $cache_key = 'whp_' . $post->post_type . '_all'; 566 wp_cache_delete( $cache_key, 'whp' ); 567 delete_transient( $cache_key ); 340 568 } 341 569 } … … 347 575 * @param int $post_id Current post id. 348 576 * 349 * @return void577 * @return array Array of conditions that were changed. 350 578 */ 351 579 private function save_meta_data( $meta_data, $post_id ) { 580 $changed = array(); 581 352 582 foreach ( $meta_data as $key => $value ) { 353 $exist = whp_plugin()->get_whp_meta( $post_id, $key,$fallabacke ); 354 if ( $exist && ! $value ) { 355 whp_plugin()->delete_whp_meta( $post_id, $key, true ); 356 } elseif ( ! $exist && $value ) { 583 // Check current state in custom table. 584 $exist = whp_plugin()->get_whp_meta( $post_id, $key, false ); 585 586 // Track if the value changed. 587 if ( ( (bool) $exist ) !== ( (bool) $value ) ) { 588 $changed[] = $key; 589 } 590 591 // Save ONLY to custom table (primary source of truth). 592 if ( $value ) { 357 593 whp_plugin()->add_whp_meta( $post_id, $key ); 594 } else { 595 whp_plugin()->delete_whp_meta( $post_id, $key, false ); 358 596 } 359 360 delete_post_meta( $post_id, '_whp_' . $key ); 361 }597 } 598 599 return $changed; 362 600 } 363 601 … … 386 624 $post_data = $sanitized_data; 387 625 } 626 627 /** 628 * Add custom box to bulk edit 629 * 630 * @param string $column_name Column name. 631 * @param string $post_type Post type. 632 * 633 * @return void 634 */ 635 public function bulk_edit_custom_box( $column_name, $post_type ) { 636 $enabled_post_types = whp_plugin()->get_enabled_post_types(); 637 638 if ( ! in_array( $post_type, $enabled_post_types, true ) ) { 639 return; 640 } 641 642 if ( 'hidden_on' !== $column_name ) { 643 return; 644 } 645 646 ?> 647 <fieldset class="inline-edit-col-right"> 648 <div class="inline-edit-col"> 649 <label> 650 <span class="title"><?php esc_html_e( 'Hide Posts', 'whp-hide-posts' ); ?></span> 651 <select name="whp_bulk_hide_action"> 652 <option value=""><?php esc_html_e( '— No Change —', 'whp-hide-posts' ); ?></option> 653 <option value="hide_on_frontpage"><?php esc_html_e( 'Hide on frontpage', 'whp-hide-posts' ); ?></option> 654 <option value="hide_on_categories"><?php esc_html_e( 'Hide on categories', 'whp-hide-posts' ); ?></option> 655 <option value="hide_on_search"><?php esc_html_e( 'Hide on search', 'whp-hide-posts' ); ?></option> 656 <option value="remove_all_hiding"><?php esc_html_e( 'Remove all hiding', 'whp-hide-posts' ); ?></option> 657 </select> 658 </label> 659 </div> 660 </fieldset> 661 <?php 662 } 663 664 /** 665 * Add custom box to quick edit 666 * 667 * @param string $column_name Column name. 668 * @param string $post_type Post type. 669 * 670 * @return void 671 */ 672 public function quick_edit_custom_box( $column_name, $post_type ) { 673 $enabled_post_types = whp_plugin()->get_enabled_post_types(); 674 675 if ( ! in_array( $post_type, $enabled_post_types, true ) ) { 676 return; 677 } 678 679 if ( 'hidden_on' !== $column_name ) { 680 return; 681 } 682 683 ?> 684 <fieldset class="inline-edit-col-right inline-edit-whp"> 685 <div class="inline-edit-col"> 686 <label class="inline-edit-group"> 687 <span class="title"><?php esc_html_e( 'Hide Options', 'whp-hide-posts' ); ?></span> 688 <label> 689 <input type="checkbox" name="whp_hide_on_frontpage" value="1" /> 690 <?php esc_html_e( 'Hide on frontpage', 'whp-hide-posts' ); ?> 691 </label> 692 <label> 693 <input type="checkbox" name="whp_hide_on_categories" value="1" /> 694 <?php esc_html_e( 'Hide on categories', 'whp-hide-posts' ); ?> 695 </label> 696 <label> 697 <input type="checkbox" name="whp_hide_on_search" value="1" /> 698 <?php esc_html_e( 'Hide on search', 'whp-hide-posts' ); ?> 699 </label> 700 </label> 701 </div> 702 </fieldset> 703 <?php 704 } 705 706 /** 707 * Save bulk edit changes via AJAX 708 * 709 * @return void 710 */ 711 public function save_bulk_edit() { 712 // Check nonce. 713 if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'whp_bulk_edit_nonce' ) ) { 714 wp_send_json_error( array( 'message' => __( 'Security check failed', 'whp-hide-posts' ) ) ); 715 } 716 717 // Check capability. 718 if ( ! current_user_can( 'edit_posts' ) ) { 719 wp_send_json_error( array( 'message' => __( 'Permission denied', 'whp-hide-posts' ) ) ); 720 } 721 722 // Get post IDs and action. 723 $post_ids = isset( $_POST['post_ids'] ) ? array_map( 'intval', (array) $_POST['post_ids'] ) : array(); 724 $action = isset( $_POST['hide_action'] ) ? sanitize_text_field( wp_unslash( $_POST['hide_action'] ) ) : ''; 725 726 if ( empty( $post_ids ) || empty( $action ) ) { 727 wp_send_json_error( array( 'message' => __( 'Invalid request', 'whp-hide-posts' ) ) ); 728 } 729 730 // Validate action against whitelist. 731 $allowed_actions = array( 732 'hide_on_frontpage', 733 'hide_on_categories', 734 'hide_on_search', 735 'remove_all_hiding', 736 ); 737 738 if ( ! in_array( $action, $allowed_actions, true ) ) { 739 wp_send_json_error( array( 'message' => __( 'Invalid action', 'whp-hide-posts' ) ) ); 740 } 741 742 // Process each post. 743 $updated = 0; 744 foreach ( $post_ids as $post_id ) { 745 // Verify user can edit this post. 746 if ( ! current_user_can( 'edit_post', $post_id ) ) { 747 continue; 748 } 749 750 if ( 'remove_all_hiding' === $action ) { 751 // Remove all hide flags for this post. 752 $this->remove_all_hide_flags( $post_id ); 753 } else { 754 // Add specific hide flag. 755 $hide_key = str_replace( 'whp_', '', $action ); 756 whp_plugin()->add_whp_meta( $post_id, $hide_key ); 757 } 758 759 $updated++; 760 } 761 762 wp_send_json_success( 763 array( 764 'message' => sprintf( 765 /* translators: %d: number of posts updated */ 766 _n( '%d post updated', '%d posts updated', $updated, 'whp-hide-posts' ), 767 $updated 768 ), 769 ) 770 ); 771 } 772 773 /** 774 * Save quick edit changes via AJAX 775 * 776 * @return void 777 */ 778 public function save_quick_edit() { 779 // Check nonce. 780 if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'whp_quick_edit_nonce' ) ) { 781 wp_send_json_error( array( 'message' => __( 'Security check failed', 'whp-hide-posts' ) ) ); 782 } 783 784 // Get post ID. 785 $post_id = isset( $_POST['post_id'] ) ? intval( $_POST['post_id'] ) : 0; 786 787 if ( ! $post_id ) { 788 wp_send_json_error( array( 'message' => __( 'Invalid post ID', 'whp-hide-posts' ) ) ); 789 } 790 791 // Check capability. 792 if ( ! current_user_can( 'edit_post', $post_id ) ) { 793 wp_send_json_error( array( 'message' => __( 'Permission denied', 'whp-hide-posts' ) ) ); 794 } 795 796 // Get hide options. 797 $hide_options = array( 798 'hide_on_frontpage' => ! empty( $_POST['whp_hide_on_frontpage'] ), 799 'hide_on_categories' => ! empty( $_POST['whp_hide_on_categories'] ), 800 'hide_on_search' => ! empty( $_POST['whp_hide_on_search'] ), 801 ); 802 803 $changed_conditions = array(); 804 805 // Update each hide option and track changes. 806 foreach ( $hide_options as $key => $value ) { 807 $exist = whp_plugin()->get_whp_meta( $post_id, $key, false ); 808 809 if ( $exist && ! $value ) { 810 // Remove hide flag. 811 whp_plugin()->delete_whp_meta( $post_id, $key, true ); 812 $changed_conditions[] = $key; 813 } elseif ( ! $exist && $value ) { 814 // Add hide flag. 815 whp_plugin()->add_whp_meta( $post_id, $key ); 816 $changed_conditions[] = $key; 817 } 818 } 819 820 // Clear cache only for changed conditions (optimized). 821 $post = get_post( $post_id ); 822 $post_type = $post ? $post->post_type : 'post'; 823 824 foreach ( $changed_conditions as $condition ) { 825 $cache_key = 'whp_' . $post_type . '_' . $condition; 826 wp_cache_delete( $cache_key, 'whp' ); 827 delete_transient( $cache_key ); 828 } 829 830 // Also clear "all" cache if anything changed. 831 if ( ! empty( $changed_conditions ) ) { 832 $cache_key = 'whp_' . $post_type . '_all'; 833 wp_cache_delete( $cache_key, 'whp' ); 834 delete_transient( $cache_key ); 835 } 836 837 wp_send_json_success( array( 'message' => __( 'Post updated successfully', 'whp-hide-posts' ) ) ); 838 } 839 840 /** 841 * Remove all hide flags from a post 842 * 843 * @param int $post_id Post ID. 844 * 845 * @return void 846 */ 847 private function remove_all_hide_flags( $post_id ) { 848 global $wpdb; 849 850 $table_name = esc_sql( $wpdb->prefix . 'whp_posts_visibility' ); 851 852 // Delete all hide flags for this post. 853 $result = $wpdb->delete( 854 $table_name, 855 array( 'post_id' => $post_id ), 856 array( '%d' ) 857 ); 858 859 if ( false === $result && $wpdb->last_error ) { 860 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log 861 error_log( sprintf( 'WHP: Failed to remove hide flags for post %d: %s', $post_id, $wpdb->last_error ) ); 862 } 863 864 // Also remove from postmeta (legacy). 865 $wpdb->query( 866 $wpdb->prepare( 867 "DELETE FROM {$wpdb->postmeta} WHERE post_id = %d AND meta_key LIKE %s", 868 $post_id, 869 $wpdb->esc_like( '_whp_hide_' ) . '%' 870 ) 871 ); 872 873 if ( $wpdb->last_error ) { 874 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log 875 error_log( sprintf( 'WHP: Failed to remove legacy meta for post %d: %s', $post_id, $wpdb->last_error ) ); 876 } 877 878 // Clear all caches for this post type since we're removing ALL conditions. 879 $post = get_post( $post_id ); 880 $post_type = $post ? $post->post_type : 'post'; 881 882 // When removing all, we must clear all condition caches. 883 $hide_types = array_keys( \MartinCV\WHP\Core\Constants::HIDDEN_POSTS_KEYS_LIST ); 884 foreach ( $hide_types as $hide_type ) { 885 $cache_key = 'whp_' . $post_type . '_' . $hide_type; 886 wp_cache_delete( $cache_key, 'whp' ); 887 delete_transient( $cache_key ); 888 } 889 } 388 890 } -
whp-hide-posts/trunk/inc/class-post-hide.php
r3331355 r3401474 39 39 add_filter( 'get_previous_post_where', array( $this, 'hide_from_post_navigation' ), 10, 1 ); 40 40 add_filter( 'widget_posts_args', array( $this, 'hide_from_recent_post_widget' ), 10, 1 ); 41 add_filter( 'query_loop_block_query_vars', array( $this, 'hide_from_query_block' ), 10, 2 ); 42 add_filter( 'render_block_core/latest-posts', array( $this, 'hide_from_latest_posts_block' ), 10, 2 ); 41 43 42 44 foreach ( $this->enabled_post_types as $pt ) { … … 111 113 ) 112 114 ) { 113 $table_name = $wpdb->prefix . 'whp_posts_visibility';115 $table_name = esc_sql( $wpdb->prefix . 'whp_posts_visibility' ); 114 116 // Handle single post pages 115 117 if ( is_singular( $q_post_type ) && ! $query->is_main_query() ) { … … 121 123 ); 122 124 125 if ( $wpdb->last_error ) { 126 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log 127 error_log( sprintf( 'WHP: Failed to get hidden posts on single page: %s', $wpdb->last_error ) ); 128 return; 129 } 130 123 131 if ( ! empty( $hidden_posts ) ) { 124 132 $existing_posts = $query->get( 'post__not_in' ); … … 128 136 $query->set( 'post__not_in', array_unique( array_merge( $existing_posts, $hidden_posts ) ) ); 129 137 } else { 130 // Fallback to meta 131 $query->set( 'meta_key', '_whp_hide_on_single_post_page' ); 132 $query->set( 'meta_compare', 'NOT EXISTS' ); 138 // Fallback to meta. 139 $existing_meta_query = $query->get( 'meta_query', array() ); 140 $existing_meta_query[] = array( 141 'key' => '_whp_hide_on_single_post_page', 142 'compare' => 'NOT EXISTS', 143 ); 144 $query->set( 'meta_query', $existing_meta_query ); 133 145 } 134 } 135 146 } 147 136 148 if ( ( is_front_page() && is_home() ) || is_front_page() ) { 137 149 $this->exclude_by_condition( $query, 'hide_on_frontpage', '_whp_hide_on_frontpage' ); 138 150 } elseif ( is_home() ) { 139 151 $this->exclude_by_condition( $query, 'hide_on_blog_page', '_whp_hide_on_blog_page' ); 140 } 152 } 141 153 142 154 if ( is_post_type_archive( $q_post_type ) ) { 143 155 $this->exclude_by_condition( $query, 'hide_on_cpt_archive', '_whp_hide_on_cpt_archive' ); 144 } elseif ( is_category( $q_post_type) ) {156 } elseif ( is_category() ) { 145 157 $this->exclude_by_condition( $query, 'hide_on_categories', '_whp_hide_on_categories' ); 146 158 } elseif ( is_tag() ) { … … 174 186 global $wpdb; 175 187 176 $table_name = $wpdb->prefix . 'whp_posts_visibility';188 $table_name = esc_sql( $wpdb->prefix . 'whp_posts_visibility' ); 177 189 178 190 $hidden_posts = $wpdb->get_col( … … 183 195 ); 184 196 197 if ( $wpdb->last_error ) { 198 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log 199 error_log( sprintf( 'WHP: Failed to exclude by condition %s: %s', $condition, $wpdb->last_error ) ); 200 return; 201 } 202 185 203 if ( ! empty( $hidden_posts ) ) { 186 204 $existing_posts = $query->get( 'post__not_in' ); … … 193 211 194 212 if ( ! $data_migrated ) { 195 // Fallback to meta 196 $query->set( 'meta_key', $meta_key ); 197 $query->set( 'meta_compare', 'NOT EXISTS' ); 213 // Fallback to meta. 214 $existing_meta_query = $query->get( 'meta_query', array() ); 215 $existing_meta_query[] = array( 216 'key' => $meta_key, 217 'compare' => 'NOT EXISTS', 218 ); 219 $query->set( 'meta_query', $existing_meta_query ); 198 220 } 199 221 } … … 250 272 return $query_args; 251 273 } 274 275 /** 276 * Hide posts from Gutenberg Query Loop block 277 * 278 * @param array $query Query arguments. 279 * @param array $block Block instance. 280 * 281 * @return array 282 */ 283 public function hide_from_query_block( $query, $block ) { 284 if ( ! isset( $query['post_type'] ) ) { 285 return $query; 286 } 287 288 $post_type = is_array( $query['post_type'] ) ? $query['post_type'][0] : $query['post_type']; 289 290 if ( ! in_array( $post_type, $this->enabled_post_types, true ) ) { 291 return $query; 292 } 293 294 $data_migrated = get_option( 'whp_data_migrated', false ); 295 $fallback = ! $data_migrated; 296 297 // Determine which hide context to use based on current page. 298 $hide_context = 'all'; 299 if ( is_front_page() ) { 300 $hide_context = 'front_page'; 301 } elseif ( is_home() ) { 302 $hide_context = 'blog_page'; 303 } elseif ( is_category() ) { 304 $hide_context = 'categories'; 305 } elseif ( is_tag() ) { 306 $hide_context = 'tags'; 307 } elseif ( is_author() ) { 308 $hide_context = 'authors'; 309 } elseif ( is_search() ) { 310 $hide_context = 'search'; 311 } elseif ( is_date() ) { 312 $hide_context = 'date'; 313 } elseif ( is_archive() ) { 314 $hide_context = 'cpt_archive'; 315 } 316 317 $hidden_ids = whp_plugin()->get_hidden_posts_ids( $post_type, $hide_context, $fallback ); 318 319 if ( ! empty( $hidden_ids ) ) { 320 $query['post__not_in'] = ! empty( $query['post__not_in'] ) ? array_unique( array_merge( $hidden_ids, $query['post__not_in'] ) ) : $hidden_ids; 321 } 322 323 return $query; 324 } 325 326 /** 327 * Hide posts from Gutenberg Latest Posts block 328 * This block is server-side rendered, so we can't filter via REST API 329 * We need to use the pre_render_block filter instead 330 * 331 * @param string $block_content Block HTML content. 332 * @param array $block Block instance. 333 * 334 * @return string 335 */ 336 public function hide_from_latest_posts_block( $block_content, $block ) { 337 // The Latest Posts block uses WP_Query internally during server-side rendering. 338 // Our pre_get_posts filter at priority 99 should catch it automatically. 339 // However, if block is rendered via REST, we need the rest_post_query filter. 340 // Since we already hook rest_{post_type}_query, hidden posts are filtered. 341 // So we just return the block content as-is - filtering happens upstream. 342 return $block_content; 343 } 252 344 } -
whp-hide-posts/trunk/inc/class-yoast-duplicate-post.php
r3331355 r3401474 42 42 global $wpdb; 43 43 44 $table_name = $wpdb->prefix . 'whp_posts_visibility';44 $table_name = esc_sql( $wpdb->prefix . 'whp_posts_visibility' ); 45 45 46 46 $conditions = $wpdb->get_col( … … 51 51 ); 52 52 53 if ( $wpdb->last_error ) { 54 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log 55 error_log( sprintf( 'WHP: Failed to get conditions for post %d: %s', $post->ID, $wpdb->last_error ) ); 56 return; 57 } 58 53 59 if ( ! empty( $conditions ) ) { 54 60 foreach ( $conditions as $condition ) { 55 $ wpdb->insert(61 $result = $wpdb->insert( 56 62 $table_name, 57 63 array( … … 64 70 ) 65 71 ); 72 73 if ( false === $result && $wpdb->last_error ) { 74 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log 75 error_log( sprintf( 'WHP: Failed to copy condition %s to post %d: %s', $condition, $new_id, $wpdb->last_error ) ); 76 } 66 77 } 67 78 } -
whp-hide-posts/trunk/inc/core/class-constants.php
r3331355 r3401474 20 20 class Constants { 21 21 const HIDDEN_POSTS_KEYS_LIST = array( 22 'all' => '_whp_hide_%', 23 'front_page' => '_whp_hide_on_frontpage', 24 'blog_page' => '_whp_hide_on_blog_page', 25 'categories' => '_whp_hide_on_categories', 26 'search' => '_whp_hide_on_search', 27 'tags' => '_whp_hide_on_tags', 28 'authors' => '_whp_hide_on_authors', 29 'date' => '_whp_hide_on_date', 30 'post_navigation' => '_whp_hide_on_post_navigation', 31 'recent_posts' => '_whp_hide_on_recent_posts', 32 'cpt_archive' => '_whp_hide_on_cpt_archive', 33 'rest_api' => '_whp_hide_on_rest_api', 22 'all' => '_whp_hide_%', 23 'front_page' => '_whp_hide_on_frontpage', 24 'blog_page' => '_whp_hide_on_blog_page', 25 'categories' => '_whp_hide_on_categories', 26 'search' => '_whp_hide_on_search', 27 'tags' => '_whp_hide_on_tags', 28 'authors' => '_whp_hide_on_authors', 29 'date' => '_whp_hide_on_date', 30 'post_navigation' => '_whp_hide_on_post_navigation', 31 'recent_posts' => '_whp_hide_on_recent_posts', 32 'cpt_archive' => '_whp_hide_on_cpt_archive', 33 'rest_api' => '_whp_hide_on_rest_api', 34 'xml_sitemap' => '_whp_hide_on_xml_sitemap', 35 'yoast_sitemap' => '_whp_hide_on_yoast_sitemap', 36 'yoast_breadcrumbs' => '_whp_hide_on_yoast_breadcrumbs', 37 'yoast_internal_links' => '_whp_hide_on_yoast_internal_links', 34 38 ); 35 39 -
whp-hide-posts/trunk/inc/core/class-database.php
r3213938 r3401474 25 25 */ 26 26 public function create_tables() { 27 $current_db_version = 1;27 $current_db_version = 2; 28 28 $db_version = get_option( 'whp_db_version', 0 ); 29 29 … … 43 43 `condition` VARCHAR(100) NOT NULL, 44 44 PRIMARY KEY (id), 45 UNIQUE KEY post_condition (post_id,`condition`), 45 46 INDEX pid_con (post_id,`condition`) 46 47 ) $charset_collate;"; -
whp-hide-posts/trunk/inc/core/class-plugin.php
r3331355 r3401474 40 40 global $post; 41 41 42 if ( ! $post instanceof \WP_Post ) { 43 return false; 44 } 45 42 46 return 'product' === $post->post_type; 47 } 48 49 /** 50 * Check if Yoast SEO is active. 51 * 52 * @return bool 53 */ 54 public function is_yoast_seo_active() { 55 // Check for Yoast SEO or Yoast SEO Premium. 56 $yoast_free = 'wordpress-seo/wp-seo.php'; 57 $yoast_premium = 'wordpress-seo-premium/wp-seo-premium.php'; 58 59 $active_plugins = (array) get_option( 'active_plugins', array() ); 60 61 return in_array( $yoast_free, $active_plugins, true ) || in_array( $yoast_premium, $active_plugins, true ); 43 62 } 44 63 … … 77 96 global $wpdb; 78 97 79 $table_name = $wpdb->prefix . 'whp_posts_visibility'; 80 81 $sql = $wpdb->prepare( "SELECT DISTINCT post_id FROM {$table_name} WHERE `condition` = %s AND post_id IN (SELECT ID FROM {$wpdb->posts} WHERE post_type = %s)", $key, $post_type ); 98 $table_name = esc_sql( $wpdb->prefix . 'whp_posts_visibility' ); 82 99 83 100 if ( 'all' === $key ) { 84 $sql = $wpdb->prepare( "SELECT DISTINCT post_id FROM {$table_name} WHERE `condition` LIKE %s AND post_id IN (SELECT ID FROM {$wpdb->posts} WHERE post_type = %s)", $key, $post_type ); 101 $like_pattern = $wpdb->esc_like( 'hide_' ) . '%'; 102 $sql = $wpdb->prepare( "SELECT DISTINCT post_id FROM {$table_name} WHERE `condition` LIKE %s AND post_id IN (SELECT ID FROM {$wpdb->posts} WHERE post_type = %s)", $like_pattern, $post_type ); 103 } else { 104 $sql = $wpdb->prepare( "SELECT DISTINCT post_id FROM {$table_name} WHERE `condition` = %s AND post_id IN (SELECT ID FROM {$wpdb->posts} WHERE post_type = %s)", $key, $post_type ); 85 105 } 86 106 87 107 $hidden_posts = $wpdb->get_col( $sql ); 108 109 if ( $wpdb->last_error ) { 110 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log 111 error_log( sprintf( 'WHP: Failed to get hidden posts: %s', $wpdb->last_error ) ); 112 return array(); 113 } 88 114 89 115 if ( empty( $hidden_posts ) && $fallback ) { 90 116 $key = '_whp_' . $key; 91 117 92 $sql = $wpdb->prepare( "SELECT DISTINCT post_id FROM {$wpdb->postmeta} WHERE meta_key = %s AND post_id IN (SELECT ID FROM {$wpdb->posts} WHERE post_type = %s)", $key, $post_type ); 93 94 if ( 'all' === $key ) { 95 $sql = $wpdb->prepare( "SELECT DISTINCT post_id FROM {$wpdb->postmeta} WHERE meta_key LIKE %s AND post_id IN (SELECT ID FROM {$wpdb->posts} WHERE post_type = %s)", $key, $post_type ); 118 if ( '_whp_all' === $key ) { 119 $like_pattern = $wpdb->esc_like( '_whp_hide_' ) . '%'; 120 $sql = $wpdb->prepare( "SELECT DISTINCT post_id FROM {$wpdb->postmeta} WHERE meta_key LIKE %s AND post_id IN (SELECT ID FROM {$wpdb->posts} WHERE post_type = %s)", $like_pattern, $post_type ); 121 } else { 122 $sql = $wpdb->prepare( "SELECT DISTINCT post_id FROM {$wpdb->postmeta} WHERE meta_key = %s AND post_id IN (SELECT ID FROM {$wpdb->posts} WHERE post_type = %s)", $key, $post_type ); 96 123 } 97 124 … … 154 181 155 182 return in_array( $current_post_type, $custom_types, true ); 183 } 184 185 /** 186 * Check if we should use the custom table (true) or need to fall back to post meta (false) 187 * Returns true if: 188 * - Data has been migrated (whp_data_migrated = true), OR 189 * - Fresh installation (no legacy postmeta exists) 190 * 191 * @return boolean 192 */ 193 public function should_use_custom_table() { 194 $data_migrated = get_option( 'whp_data_migrated', false ); 195 196 // If explicitly migrated, use custom table. 197 if ( $data_migrated ) { 198 return true; 199 } 200 201 // Check if there's any legacy postmeta - if not, it's a fresh install. 202 global $wpdb; 203 $has_legacy_data = $wpdb->get_var( 204 "SELECT COUNT(*) FROM {$wpdb->postmeta} WHERE meta_key LIKE '_whp_hide%' LIMIT 1" 205 ); 206 207 // If no legacy data exists, it's a fresh install - use custom table. 208 // If legacy data exists but not migrated, use fallback. 209 return ! $has_legacy_data; 156 210 } 157 211 … … 168 222 global $wpdb; 169 223 170 $table_name = $wpdb->prefix . 'whp_posts_visibility';224 $table_name = esc_sql( $wpdb->prefix . 'whp_posts_visibility' ); 171 225 172 226 $hidden_post = (int) $wpdb->get_var( … … 178 232 ); 179 233 234 if ( $wpdb->last_error ) { 235 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log 236 error_log( sprintf( 'WHP: Failed to check post meta for post %d: %s', $post_id, $wpdb->last_error ) ); 237 return false; 238 } 239 180 240 if ( $hidden_post ) { 181 241 return true; … … 197 257 * @return boolean 198 258 */ 199 public function add_whp_meta( $post_id, $key, $fallback = false ) { 200 global $wpdb; 201 202 $table_name = $wpdb->prefix . 'whp_posts_visibility'; 203 204 $wpdb->insert( 259 public function add_whp_meta( $post_id, $key ) { 260 global $wpdb; 261 262 $table_name = esc_sql( $wpdb->prefix . 'whp_posts_visibility' ); 263 264 // Check if it already exists. 265 $exists = $wpdb->get_var( 266 $wpdb->prepare( 267 "SELECT COUNT(*) FROM {$table_name} WHERE post_id = %d AND `condition` = %s", 268 $post_id, 269 $key 270 ) 271 ); 272 273 if ( $exists ) { 274 // Already exists, no need to insert. 275 return true; 276 } 277 278 $result = $wpdb->insert( 205 279 $table_name, 206 280 array( 207 'post_id' => $post_id,281 'post_id' => $post_id, 208 282 'condition' => $key, 209 283 ), … … 213 287 ) 214 288 ); 289 290 if ( false === $result && $wpdb->last_error ) { 291 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log 292 error_log( sprintf( 'WHP: Failed to add meta for post %d: %s', $post_id, $wpdb->last_error ) ); 293 return false; 294 } 295 296 return true; 215 297 } 216 298 … … 218 300 * Remove post from hiding 219 301 * 220 * @param int $post_id The post id. 221 * @param string $key The key name. 302 * @param int $post_id The post id. 303 * @param string $key The key name. 304 * @param boolean $delete_postmeta Whether to also delete legacy postmeta. 222 305 * 223 306 * @return boolean 224 307 */ 225 public function delete_whp_meta( $post_id, $key, $ fallback= false ) {226 global $wpdb; 227 228 $table_name = $wpdb->prefix . 'whp_posts_visibility';229 230 $ wpdb->delete(308 public function delete_whp_meta( $post_id, $key, $delete_postmeta = false ) { 309 global $wpdb; 310 311 $table_name = esc_sql( $wpdb->prefix . 'whp_posts_visibility' ); 312 313 $result = $wpdb->delete( 231 314 $table_name, 232 315 array( 233 'post_id' => $post_id,316 'post_id' => $post_id, 234 317 'condition' => $key, 235 318 ), … … 240 323 ); 241 324 242 delete_post_meta( $post_id, '_whp_' . $key ); 325 if ( false === $result && $wpdb->last_error ) { 326 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log 327 error_log( sprintf( 'WHP: Failed to delete meta for post %d: %s', $post_id, $wpdb->last_error ) ); 328 return false; 329 } 330 331 if ( $delete_postmeta ) { 332 delete_post_meta( $post_id, '_whp_' . $key ); 333 } 334 335 return true; 243 336 } 244 337 } -
whp-hide-posts/trunk/uninstall.php
r3331355 r3401474 12 12 delete_option( 'whp_enabled_post_types' ); 13 13 delete_option( 'whp_db_version' ); 14 delete_option( 'whp_data_migrated' ); 15 delete_option( 'whp_data_migrated_notice_closed' ); 16 delete_option( 'whp_disable_hidden_on_column' ); 14 17 15 18 global $wpdb; 16 $table_name = $wpdb->prefix . 'whp_posts_visibility';19 $table_name = esc_sql( $wpdb->prefix . 'whp_posts_visibility' ); 17 20 $wpdb->query( "DELETE FROM {$wpdb->prefix}postmeta WHERE meta_key LIKE '_whp_hide_on_%'" ); 18 21 $wpdb->query( "DROP TABLE IF EXISTS $table_name" ); -
whp-hide-posts/trunk/views/admin/template-admin-post-metabox.php
r3331355 r3401474 8 8 ?> 9 9 <div class='whp_hide_posts'> 10 <?php wp_nonce_field( 'wp_metabox_nonce', 'wp_metabox_nonce_value' ); ?> 10 11 <p> 11 12 <label for='whp_select_all'> … … 103 104 </label> 104 105 </p> 106 <h4><?php esc_html_e( 'Sitemap & SEO Options', 'whp-hide-posts' ); ?></h4> 107 <p> 108 <label for='whp_hide_on_xml_sitemap'> 109 <input type='checkbox' name="whp_hide_on_xml_sitemap" value='1' <?php checked( $whp_hide_on_xml_sitemap, 1 ); ?> id='whp_hide_on_xml_sitemap'> 110 <?php esc_html_e( 'Hide from WordPress XML sitemap', 'whp-hide-posts' ); ?> 111 <em><?php esc_html_e( '(WordPress core sitemap)', 'whp-hide-posts' ); ?></em> 112 </label> 113 </p> 114 <?php if ( whp_plugin()->is_yoast_seo_active() ) : ?> 115 <p> 116 <label for='whp_hide_on_yoast_sitemap'> 117 <input type='checkbox' name="whp_hide_on_yoast_sitemap" value='1' <?php checked( $whp_hide_on_yoast_sitemap, 1 ); ?> id='whp_hide_on_yoast_sitemap'> 118 <?php esc_html_e( 'Hide from Yoast SEO sitemap', 'whp-hide-posts' ); ?> 119 </label> 120 </p> 121 <p> 122 <label for='whp_hide_on_yoast_breadcrumbs'> 123 <input type='checkbox' name="whp_hide_on_yoast_breadcrumbs" value='1' <?php checked( $whp_hide_on_yoast_breadcrumbs, 1 ); ?> id='whp_hide_on_yoast_breadcrumbs'> 124 <?php esc_html_e( 'Hide from Yoast SEO breadcrumbs', 'whp-hide-posts' ); ?> 125 </label> 126 </p> 127 <p> 128 <label for='whp_hide_on_yoast_internal_links'> 129 <input type='checkbox' name="whp_hide_on_yoast_internal_links" value='1' <?php checked( $whp_hide_on_yoast_internal_links, 1 ); ?> id='whp_hide_on_yoast_internal_links'> 130 <?php esc_html_e( 'Hide from Yoast internal link suggestions', 'whp-hide-posts' ); ?> 131 </label> 132 </p> 133 <?php endif; ?> 105 134 <?php if ( whp_plugin()->is_woocommerce_active() && whp_plugin()->is_woocommerce_product() ) : ?> 106 135 <h4><?php esc_html_e( 'Woocommerce options', 'whp-hide-posts' ); ?></h4> -
whp-hide-posts/trunk/whp-hide-posts.php
r3331355 r3401474 2 2 /** 3 3 * Plugin Name: Hide Posts 4 * Description: Hides posts on home page, categories, search, tags page, authors page, RSS Feed as well as hiding Woocommerce products4 * Description: Hides posts on home page, categories, search, tags page, authors page, RSS Feed, XML sitemaps, Yoast SEO as well as hiding Woocommerce products 5 5 * Author: MartinCV 6 6 * Author URI: https://www.martincv.com 7 * Version: 2. 0.37 * Version: 2.1.0 8 8 * Text Domain: whp-hide-posts 9 9 * … … 49 49 * @var string 50 50 */ 51 private $version = '2. 0.3';51 private $version = '2.1.0'; 52 52 53 53 /** … … 116 116 \MartinCV\WHP\Core\Database::get_instance()->create_tables(); 117 117 118 // Initialize metabox (needed for both admin and REST API/Gutenberg). 119 \MartinCV\WHP\Admin\Post_Hide_Metabox::get_instance(); 120 121 // Initialize REST API for Gutenberg (custom table, no postmeta). 122 \MartinCV\WHP\REST_API::get_instance(); 123 118 124 // Init classes if is Admin/Dashboard. 119 125 if ( is_admin() ) { 120 126 \MartinCV\WHP\Admin\Dashboard::get_instance(); 121 \MartinCV\WHP\Admin\Post_Hide_Metabox::get_instance();122 127 \MartinCV\WHP\Yoast_Duplicate_Post::get_instance(); 123 128 } else { 124 129 \MartinCV\WHP\Post_Hide::get_instance(); 125 130 } 131 132 // Initialize SEO integrations (works on both frontend and admin). 133 \MartinCV\WHP\SEO_Integration::get_instance(); 134 135 // Initialize cache manager. 136 \MartinCV\WHP\Cache_Manager::get_instance(); 126 137 } 127 138
Note: See TracChangeset
for help on using the changeset viewer.