Changeset 3759017
- Timestamp:
- 10/21/2026 08:09:03 PM (3 days ago)
- Location:
- easyrankly/trunk
- Files:
-
- 107 added
- 5 deleted
- 3 edited
-
admin (deleted)
-
assets/admin.css (added)
-
assets/css (deleted)
-
assets/js (deleted)
-
assets/src (added)
-
assets/src/editor (added)
-
assets/src/editor/index.js (added)
-
assets/src/import (added)
-
assets/src/import/index.js (added)
-
assets/src/language-panel (added)
-
assets/src/language-panel/index.js (added)
-
assets/src/language-switcher (added)
-
assets/src/language-switcher/index.js (added)
-
assets/src/media-field (added)
-
assets/src/media-field/index.js (added)
-
blocks (added)
-
blocks/language-switcher (added)
-
blocks/language-switcher/block.json (added)
-
build (added)
-
build/editor.asset.php (added)
-
build/editor.js (added)
-
build/import.asset.php (added)
-
build/import.js (added)
-
build/language-panel.asset.php (added)
-
build/language-panel.js (added)
-
build/language-switcher.asset.php (added)
-
build/language-switcher.js (added)
-
build/media-field.asset.php (added)
-
build/media-field.js (added)
-
easyrankly.php (modified) (1 diff)
-
includes (deleted)
-
languages (deleted)
-
package.json (added)
-
readme.txt (modified) (1 diff)
-
src (added)
-
src/Admin (added)
-
src/Admin/Assets.php (added)
-
src/Admin/MediaField.php (added)
-
src/Admin/RecordsPage.php (added)
-
src/Admin/RecordsTable.php (added)
-
src/Breadcrumbs (added)
-
src/Breadcrumbs/Breadcrumbs.php (added)
-
src/Canonical (added)
-
src/Canonical/Canonical.php (added)
-
src/Context (added)
-
src/Context/Context.php (added)
-
src/CustomCode (added)
-
src/CustomCode/Admin (added)
-
src/CustomCode/Admin/Page.php (added)
-
src/CustomCode/CustomCode.php (added)
-
src/CustomCode/PhpRunner.php (added)
-
src/CustomCode/RestController.php (added)
-
src/CustomCode/Runner.php (added)
-
src/Import (added)
-
src/Import/Admin (added)
-
src/Import/Admin/Page.php (added)
-
src/Import/Import.php (added)
-
src/Import/RankMath.php (added)
-
src/Import/Source.php (added)
-
src/Import/Yoast.php (added)
-
src/Meta (added)
-
src/Meta/Admin (added)
-
src/Meta/Admin/ClassicFields.php (added)
-
src/Meta/Admin/EditorPanel.php (added)
-
src/Meta/Admin/TermFields.php (added)
-
src/Meta/Meta.php (added)
-
src/Multilingual (added)
-
src/Multilingual/Admin (added)
-
src/Multilingual/Admin/ClassicPanel.php (added)
-
src/Multilingual/Admin/EditorPanel.php (added)
-
src/Multilingual/HomesSitemap.php (added)
-
src/Multilingual/Hreflang.php (added)
-
src/Multilingual/Languages.php (added)
-
src/Multilingual/Links.php (added)
-
src/Multilingual/Multilingual.php (added)
-
src/Multilingual/Requests.php (added)
-
src/Multilingual/Rest.php (added)
-
src/Multilingual/Routing.php (added)
-
src/Multilingual/SiteIdentity.php (added)
-
src/Multilingual/Switcher.php (added)
-
src/Multilingual/SyncedPatterns.php (added)
-
src/Multilingual/TemplateParts.php (added)
-
src/Multilingual/Translations.php (added)
-
src/Plugin.php (added)
-
src/Redirects (added)
-
src/Redirects/Admin (added)
-
src/Redirects/Admin/Page.php (added)
-
src/Redirects/Redirects.php (added)
-
src/Redirects/RestController.php (added)
-
src/Redirects/Rule.php (added)
-
src/Redirects/Runner.php (added)
-
src/Redirects/SlugChanges.php (added)
-
src/Robots (added)
-
src/Robots/Robots.php (added)
-
src/RobotsTxt (added)
-
src/RobotsTxt/RobotsTxt.php (added)
-
src/Schema (added)
-
src/Schema/Schema.php (added)
-
src/Settings (added)
-
src/Settings/Admin (added)
-
src/Settings/Admin/Fields.php (added)
-
src/Settings/Admin/Form.php (added)
-
src/Settings/Admin/LanguageFields.php (added)
-
src/Settings/Admin/Page.php (added)
-
src/Settings/Settings.php (added)
-
src/Sitemap (added)
-
src/Sitemap/Sitemap.php (added)
-
src/Social (added)
-
src/Social/Social.php (added)
-
src/Titles (added)
-
src/Titles/Template.php (added)
-
src/Titles/Titles.php (added)
-
src/autoload.php (added)
-
uninstall.php (modified) (1 diff)
-
webpack.config.js (added)
Legend:
- Unmodified
- Added
- Removed
-
easyrankly/trunk/easyrankly.php
r3588731 r3759017 1 1 <?php 2 2 /** 3 * Plugin Name: EasyRankly4 * Plugin URI: https://easyrankly.com5 * Description: Lightweight, modular, developer-first SEO essentials for WordPress.6 * Version: 1.0.07 * Requires at least: 6.58 * Requires PHP: 8.09 * Author: Enthony Sclafani10 * Text Domain: easyrankly11 * Domain Path: /languages12 * License : GPL-2.0-or-later13 * Network: true3 * Plugin Name: EasyRankly 4 * Plugin URI: https://easyrankly.com 5 * Description: Lightweight SEO built only on native WordPress APIs: metadata, redirects, sitemaps, custom code and multilingual. 6 * Version: 2.0.0 7 * Requires at least: 7.0 8 * Requires PHP: 8.0 9 * Author: EasyRankly 10 * Author URI: https://easyrankly.com/ 11 * License: GPL-2.0-or-later 12 * License URI: https://www.gnu.org/licenses/gpl-2.0.html 13 * Text Domain: easyrankly 14 14 * 15 15 * @package EasyRankly 16 16 */ 17 17 18 if ( ! defined( 'ABSPATH' ) ) { 19 exit; 20 } 18 namespace EasyRankly; 21 19 22 // Bail if WordPress loads this file twice in one request (e.g. during a ZIP update). 23 if ( defined( 'ERANKLY_VERSION' ) ) { 24 return; 25 } 20 defined( 'ABSPATH' ) || exit; 26 21 27 define( 'ERANKLY_VERSION', '1.0.0' ); 28 define( 'ERANKLY_FILE', __FILE__ ); 29 define( 'ERANKLY_PATH', plugin_dir_path( __FILE__ ) ); 30 define( 'ERANKLY_URL', plugin_dir_url( __FILE__ ) ); 31 define( 'ERANKLY_OPTION', 'erankly_settings' ); 32 define( 'ERANKLY_VERSION_OPTION', 'erankly_version' ); 33 define( 'ERANKLY_SETUP_STATUS_OPTION', 'erankly_setup_wizard_status' ); 34 define( 'ERANKLY_REWRITE_FLUSH_OPTION', 'erankly_flush_rewrite_rules' ); 35 define( 'ERANKLY_SITEMAP_TRANSIENT_PREFIX', 'erankly_sitemap_' ); 36 define( 'ERANKLY_SITEMAP_CACHE_VERSION_OPTION', 'erankly_sitemap_cache_version' ); 22 const VERSION = '2.0.0'; 23 const PLUGIN_FILE = __FILE__; 37 24 38 require_once ERANKLY_PATH . 'includes/helpers.php';25 require_once __DIR__ . '/src/autoload.php'; 39 26 40 /** 41 * Gets a plugin option using network storage on Multisite. 42 * 43 * @param string $key Option name. 44 * @param mixed $default_value Default value. 45 * @return mixed 46 */ 47 function erankly_get_plugin_option( string $key, mixed $default_value = false ): mixed { 48 return is_multisite() ? get_site_option( $key, $default_value ) : get_option( $key, $default_value ); 49 } 50 51 /** 52 * Updates a plugin option using network storage on Multisite. 53 * 54 * @param string $key Option name. 55 * @param mixed $value Value to store. 56 * @return void 57 */ 58 function erankly_update_plugin_option( string $key, mixed $value ): void { 59 if ( is_multisite() ) { 60 update_site_option( $key, $value ); 61 } else { 62 // The settings array is read on every request, so autoload it; other options aren't. 63 update_option( $key, $value, ERANKLY_OPTION === $key ); 64 } 65 66 if ( ERANKLY_OPTION === $key ) { 67 erankly_clear_settings_cache(); 68 } 69 } 70 require_once ERANKLY_PATH . 'includes/compatibility.php'; 71 require_once ERANKLY_PATH . 'includes/meta.php'; 72 require_once ERANKLY_PATH . 'includes/robots.php'; 73 require_once ERANKLY_PATH . 'includes/redirects.php'; 74 require_once ERANKLY_PATH . 'includes/seo-checklist.php'; 75 76 if ( is_admin() ) { 77 require_once ERANKLY_PATH . 'includes/admin.php'; 78 } 79 80 /** 81 * Boots the plugin after all plugins are available for compatibility checks. 82 * 83 * @return void 84 */ 85 function erankly_bootstrap(): void { 86 add_action( 'init', 'erankly_register_meta' ); 87 add_action( 'init', 'erankly_register_rewrites' ); 88 add_action( 'init', 'erankly_maybe_flush_after_upgrade', 20 ); 89 add_action( 'init', 'erankly_maybe_flush_rewrite_rules', 30 ); 90 91 if ( is_multisite() ) { 92 require_once ERANKLY_PATH . 'includes/multilingual.php'; 93 if ( erankly_multilingual_enabled() ) { 94 erankly_ml_boot(); 95 } 96 } 97 98 if ( erankly_bloat_enabled() ) { 99 require_once ERANKLY_PATH . 'includes/bloat.php'; 100 erankly_bloat_bootstrap(); 101 } 102 103 // These core sitemap filters run unless another SEO plugin owns sitemaps, so per-post 104 // "Hide from search results" still applies in /wp-sitemap.xml even when our Sitemap module is off. 105 if ( ! erankly_should_suppress_sitemaps() ) { 106 require_once ERANKLY_PATH . 'includes/sitemap.php'; 107 add_filter( 'wp_sitemaps_posts_query_args', 'erankly_filter_core_sitemap_posts_query_args', 20, 2 ); 108 add_filter( 'wp_sitemaps_taxonomies_query_args', 'erankly_filter_core_sitemap_terms_query_args', 20, 2 ); 109 add_filter( 'wp_sitemaps_post_types', 'erankly_filter_core_sitemap_post_types', 20 ); 110 add_filter( 'wp_sitemaps_taxonomies', 'erankly_filter_core_sitemap_taxonomies', 20 ); 111 add_filter( 'wp_sitemaps_add_provider', 'erankly_filter_core_sitemap_add_provider', 20, 2 ); 112 113 if ( erankly_sitemap_enabled() ) { 114 // Specialised sitemaps (image, video, news) that require non-standard XML 115 // namespaces are still served as EasyRankly virtual files. 116 require_once ERANKLY_PATH . 'includes/class-erankly-specialist-sitemaps-provider.php'; 117 add_action( 118 'init', 119 function () { 120 wp_register_sitemap_provider( 'erankly', new ERankly_Specialist_Sitemaps_Provider() ); 121 } 122 ); 123 add_action( 'template_redirect', 'erankly_maybe_render_virtual_files', 0 ); 124 } 125 } 126 127 if ( erankly_sitemap_enabled() ) { 128 // Cache invalidation for specialised sitemaps (image / video / news). 129 add_action( 'save_post', 'erankly_flush_sitemap_cache_for_post' ); 130 add_action( 'deleted_post', 'erankly_flush_sitemap_cache_for_deleted_post' ); 131 add_action( 'transition_post_status', 'erankly_flush_sitemap_cache_for_status', 10, 3 ); 132 add_action( 'profile_update', 'erankly_flush_sitemap_cache' ); 133 add_action( 'user_register', 'erankly_flush_sitemap_cache' ); 134 add_action( 'deleted_user', 'erankly_flush_sitemap_cache' ); 135 add_action( 'created_term', 'erankly_flush_sitemap_cache' ); 136 add_action( 'edited_term', 'erankly_flush_sitemap_cache' ); 137 add_action( 'delete_term', 'erankly_flush_sitemap_cache' ); 138 add_action( 'added_term_meta', 'erankly_flush_sitemap_cache_for_term_meta', 10, 3 ); 139 add_action( 'updated_term_meta', 'erankly_flush_sitemap_cache_for_term_meta', 10, 3 ); 140 add_action( 'deleted_term_meta', 'erankly_flush_sitemap_cache_for_term_meta', 10, 3 ); 141 } 142 143 if ( erankly_health_enabled() ) { 144 require_once ERANKLY_PATH . 'includes/health.php'; 145 erankly_health_boot(); 146 } 147 148 if ( is_admin() ) { 149 erankly_admin_bootstrap(); 150 } 151 152 erankly_seo_checklist_boot(); 153 154 if ( erankly_is_frontend_html_request() ) { 155 add_action( 'wp', 'erankly_bootstrap_frontend_modules', 1 ); 156 } 157 158 add_action( 'rest_api_init', 'erankly_register_user_search_route' ); 159 add_filter( 'robots_txt', 'erankly_filter_robots_txt', 20, 2 ); 160 add_action( 'parse_request', 'erankly_force_robots_txt_request' ); 161 add_action( 'pre_get_posts', 'erankly_filter_visibility_queries' ); 162 add_action( 'added_post_meta', 'erankly_invalidate_visibility_exclusion_cache', 10, 3 ); 163 add_action( 'updated_post_meta', 'erankly_invalidate_visibility_exclusion_cache', 10, 3 ); 164 add_action( 'deleted_post_meta', 'erankly_invalidate_visibility_exclusion_cache', 10, 3 ); 165 166 if ( is_multisite() ) { 167 add_action( 'update_site_option_' . ERANKLY_OPTION, 'erankly_handle_network_settings_updated', 10, 3 ); 168 add_action( 'wp_initialize_site', 'erankly_on_new_blog_site' ); 169 } else { 170 add_action( 'update_option_' . ERANKLY_OPTION, 'erankly_handle_settings_updated', 10, 2 ); 171 } 172 } 173 add_action( 'plugins_loaded', 'erankly_bootstrap', 5 ); 174 175 /** 176 * Loads frontend-only modules after WordPress has resolved an HTML request. 177 * 178 * REST requests normally terminate before the wp hook, so they do not parse the 179 * canonical, social, schema, or breadcrumb implementations. 180 */ 181 function erankly_bootstrap_frontend_modules(): void { 182 if ( (bool) erankly_get_setting( 'enable_breadcrumbs', 1 ) ) { 183 require_once ERANKLY_PATH . 'includes/breadcrumbs.php'; 184 } elseif ( ! function_exists( 'erankly_breadcrumbs' ) ) { 185 /** 186 * Preserves the public template API while the breadcrumb module is off. 187 * 188 * @param array<string,mixed> $args Ignored arguments. 189 * @return string 190 */ 191 function erankly_breadcrumbs( array $args = array() ): string { 192 unset( $args ); 193 return ''; 194 } 195 } 196 197 // template_redirect runs after this wp:1 action, so the callback is defined in time. 198 if ( 'none' !== (string) erankly_get_setting( 'attachment_redirect', 'none' ) ) { 199 add_action( 'template_redirect', 'erankly_redirect_attachment' ); 200 } 201 202 if ( ! erankly_should_output_head() ) { 203 return; 204 } 205 206 require_once ERANKLY_PATH . 'includes/canonical.php'; 207 require_once ERANKLY_PATH . 'includes/opengraph.php'; 208 require_once ERANKLY_PATH . 'includes/schema.php'; 209 210 remove_action( 'wp_head', 'rel_canonical' ); 211 remove_action( 'wp_head', 'wp_generator' ); 212 add_filter( 'the_generator', '__return_empty_string' ); 213 add_filter( 'pre_get_document_title', 'erankly_filter_document_title', 20 ); 214 add_filter( 'document_title_parts', 'erankly_filter_document_title_parts', 20 ); 215 add_action( 'wp_head', 'erankly_render_head', 1 ); 216 add_filter( 'wp_robots', 'erankly_filter_wp_robots', 20 ); 217 } 218 219 /** 220 * Runs on plugin activation. 221 * 222 * @return void 223 */ 224 function erankly_activate(): void { 225 $is_new_install = false === erankly_get_plugin_option( ERANKLY_OPTION, false ); 226 227 if ( is_multisite() ) { 228 if ( $is_new_install ) { 229 add_site_option( ERANKLY_OPTION, erankly_default_settings() ); 230 } 231 232 add_site_option( ERANKLY_VERSION_OPTION, ERANKLY_VERSION ); 233 } else { 234 if ( $is_new_install ) { 235 add_option( ERANKLY_OPTION, erankly_default_settings(), '', 'yes' ); 236 } 237 238 add_option( ERANKLY_VERSION_OPTION, ERANKLY_VERSION, '', 'no' ); 239 } 240 241 if ( $is_new_install ) { 242 erankly_update_plugin_option( ERANKLY_SETUP_STATUS_OPTION, 'pending' ); 243 } 244 245 erankly_register_rewrites(); 246 flush_rewrite_rules(); 247 } 248 register_activation_hook( ERANKLY_FILE, 'erankly_activate' ); 249 250 /** 251 * Schedules a rewrite-rules flush whenever the plugin version changes. 252 * 253 * Runs on every init but exits immediately when the stored version matches. 254 * On a version mismatch (i.e. after a plugin update) it updates the stored 255 * version and sets the flush flag so erankly_maybe_flush_rewrite_rules() 256 * rebuilds the rules on the same request. 257 * 258 * @return void 259 */ 260 function erankly_maybe_flush_after_upgrade(): void { 261 $stored = (string) erankly_get_plugin_option( ERANKLY_VERSION_OPTION, '' ); 262 263 if ( ERANKLY_VERSION === $stored ) { 264 return; 265 } 266 267 erankly_update_plugin_option( ERANKLY_VERSION_OPTION, ERANKLY_VERSION ); 268 269 if ( is_multisite() ) { 270 foreach ( get_sites( array( 'fields' => 'ids' ) ) as $site_id ) { 271 switch_to_blog( (int) $site_id ); 272 update_option( ERANKLY_REWRITE_FLUSH_OPTION, 1, false ); 273 restore_current_blog(); 274 } 275 } else { 276 update_option( ERANKLY_REWRITE_FLUSH_OPTION, 1, false ); 277 } 278 } 279 280 /** 281 * Adapter for the update_site_option_ hook, which passes args in a different order. 282 * 283 * @param string $option Option name. 284 * @param mixed $value New value. 285 * @param mixed $old_value Previous value. 286 * @return void 287 */ 288 function erankly_handle_network_settings_updated( string $option, mixed $value, mixed $old_value ): void { 289 erankly_handle_settings_updated( $old_value, $value ); 290 } 291 292 /** 293 * Initialises rewrite rules for a newly created network site. 294 * 295 * @param WP_Site $new_site The newly created site object. 296 * @return void 297 */ 298 function erankly_on_new_blog_site( WP_Site $new_site ): void { 299 if ( ! function_exists( 'is_plugin_active_for_network' ) ) { 300 require_once ABSPATH . 'wp-admin/includes/plugin.php'; 301 } 302 303 if ( ! is_plugin_active_for_network( plugin_basename( ERANKLY_FILE ) ) ) { 304 return; 305 } 306 307 switch_to_blog( (int) $new_site->blog_id ); 308 erankly_register_rewrites(); 309 flush_rewrite_rules( false ); 310 restore_current_blog(); 311 } 312 313 /** 314 * Handles settings updates that affect feature bootstrapping. 315 * 316 * @param mixed $old_value Previous option value. 317 * @param mixed $value New option value. 318 * @return void 319 */ 320 function erankly_handle_settings_updated( mixed $old_value, mixed $value ): void { 321 erankly_clear_settings_cache(); 322 323 $old_sitemap_enabled = is_array( $old_value ) && ! empty( $old_value['enable_sitemap'] ); 324 $new_sitemap_enabled = is_array( $value ) && ! empty( $value['enable_sitemap'] ); 325 326 if ( $old_sitemap_enabled !== $new_sitemap_enabled ) { 327 erankly_flush_sitemap_cache(); 328 329 if ( is_multisite() ) { 330 foreach ( get_sites( array( 'fields' => 'ids' ) ) as $site_id ) { 331 switch_to_blog( (int) $site_id ); 332 update_option( ERANKLY_REWRITE_FLUSH_OPTION, 1, false ); 333 restore_current_blog(); 334 } 335 } else { 336 update_option( ERANKLY_REWRITE_FLUSH_OPTION, 1, false ); 337 } 338 339 return; 340 } 341 342 if ( $new_sitemap_enabled ) { 343 erankly_flush_sitemap_cache(); 344 } 345 } 346 347 /** 348 * Flushes rewrite rules after feature settings have changed. 349 * 350 * @return void 351 */ 352 function erankly_maybe_flush_rewrite_rules(): void { 353 if ( ! (bool) get_option( ERANKLY_REWRITE_FLUSH_OPTION, 0 ) ) { 354 return; 355 } 356 357 delete_option( ERANKLY_REWRITE_FLUSH_OPTION ); 358 flush_rewrite_rules( false ); 359 } 360 361 /** 362 * Runs on plugin deactivation. 363 * 364 * @return void 365 */ 366 function erankly_deactivate(): void { 367 // Clear the daily 404-retention cron; erankly_health_boot() reschedules it when active. 368 wp_clear_scheduled_hook( 'erankly_health_prune_404_cron' ); 369 370 if ( is_multisite() ) { 371 foreach ( get_sites( array( 'fields' => 'ids' ) ) as $erankly_site_id ) { 372 switch_to_blog( (int) $erankly_site_id ); 373 erankly_flush_sitemap_cache(); 374 flush_rewrite_rules(); 375 restore_current_blog(); 376 } 377 } else { 378 erankly_flush_sitemap_cache(); 379 flush_rewrite_rules(); 380 } 381 } 382 register_deactivation_hook( ERANKLY_FILE, 'erankly_deactivate' ); 383 384 /** 385 * Registers the REST route for the admin user search autocomplete. 386 * 387 * @return void 388 */ 389 function erankly_register_user_search_route(): void { 390 register_rest_route( 391 'erankly/v1', 392 '/users/search', 393 array( 394 'methods' => WP_REST_Server::READABLE, 395 'callback' => 'erankly_rest_user_search', 396 'permission_callback' => static fn() => current_user_can( 'manage_options' ) || current_user_can( 'manage_network_options' ), 397 'args' => array( 398 'q' => array( 399 'default' => '', 400 'type' => 'string', 401 'sanitize_callback' => 'sanitize_text_field', 402 ), 403 ), 404 ) 405 ); 406 } 407 408 /** 409 * Handles the user search REST request. 410 * 411 * Returns up to 20 users matching the query. The search is scoped to the caller's 412 * capability: network administrators (manage_network_options) may search across the 413 * whole network, while site administrators are restricted to the current site's users 414 * to prevent cross-site user enumeration. On single-site the blog_id arg is ignored. 415 * 416 * @param WP_REST_Request $request Request object. 417 * @return WP_REST_Response 418 */ 419 function erankly_rest_user_search( WP_REST_Request $request ): WP_REST_Response { 420 $query = (string) $request->get_param( 'q' ); 421 422 $args = array( 423 'number' => 20, 424 'orderby' => 'display_name', 425 'order' => 'ASC', 426 'fields' => array( 'ID', 'display_name' ), 427 ); 428 429 // Only network admins may enumerate users across the whole network; everyone 430 // else (site admins) is restricted to the current site's users. 431 if ( is_multisite() && current_user_can( 'manage_network_options' ) ) { 432 $args['blog_id'] = 0; // 0 = network-wide on multisite. 433 } else { 434 $args['blog_id'] = get_current_blog_id(); // Restrict to the current site. 435 } 436 437 if ( '' !== $query ) { 438 $args['search'] = '*' . $query . '*'; 439 $args['search_columns'] = array( 'user_login', 'user_nicename', 'display_name', 'user_email' ); 440 } 441 442 $users = get_users( $args ); // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.get_users_get_users -- intentional admin-only user lookup with strict capability check. 443 $results = array(); 444 445 foreach ( $users as $user ) { 446 if ( ! isset( $user->ID, $user->display_name ) ) { 447 continue; 448 } 449 450 $results[] = array( 451 'id' => (int) $user->ID, 452 /* translators: 1: User display name, 2: User ID. */ 453 'text' => sprintf( __( '%1$s (ID: %2$d)', 'easyrankly' ), $user->display_name, $user->ID ), 454 ); 455 } 456 457 return new WP_REST_Response( $results, 200 ); 458 } 27 ( new Plugin() )->register(); -
easyrankly/trunk/readme.txt
r3588731 r3759017 1 1 === EasyRankly === 2 2 Contributors: easyrankly 3 Tags: seo, schema, sitemap, redirects, breadcrumbs4 Requires at least: 6.55 Tested up to: 7. 03 Tags: seo, redirects, sitemap, multilingual 4 Requires at least: 7.0 5 Tested up to: 7.1 6 6 Requires PHP: 8.0 7 Stable tag: 1.0.07 Stable tag: 2.0.0 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html 10 10 11 Lightweight , modular, developer-first SEO essentials for WordPress.11 Lightweight SEO built only on native WordPress APIs. No custom tables, no cron jobs, no frontend scripts. 12 12 13 13 == Description == 14 14 15 EasyRankly provides only the technical SEO primitives a lean WordPress site needs:15 EasyRankly covers the SEO basics of a WordPress site with the tools WordPress already has. It adds no database tables, schedules no background jobs and loads nothing extra for your visitors: the only thing it adds to your pages is their SEO tags and structured data. 16 16 17 * Document title integration. 18 * Meta description. 19 * Canonical URL. 20 * Robots meta through the native WordPress robots API. 21 * Open Graph and Twitter cards. 22 * Modular JSON-LD schema graph. 23 * Extended Organization identity details and optional single-location LocalBusiness schema. 24 * Optional XML sitemap index, post type sitemaps with image entries, taxonomy sitemaps, and SEO-aware author sitemap. 25 * Virtual robots.txt integration. 26 * Breadcrumb function. 27 * Granular noindex, nofollow, noarchive, and sitemap exclusion controls. 28 * Per-content search and archive query exclusion controls. 29 * Global Schema settings with repeatable custom JSON-LD blocks. 30 * Targeting rules for applying global schema blocks to front page, posts page, singular content, post type archives, and search results. 31 * Dynamic variables for content metadata, social metadata, and schema fields. 32 * Optional redirect management with exact, wildcard, and regex matching. 33 * Optional Health monitoring with frequent 404 detection (no user data is tracked; paths are anonymized and stored locally). 34 * Optional per-content breadcrumb names for shorter visible breadcrumbs and BreadcrumbList schema labels. 17 = What it does = 35 18 36 It does not include keyword scoring, readability analysis, AI, analytics, tracking, internal linking suggestions, marketing widgets, or upsells. 19 * **Titles and descriptions.** Templates with variables for every content type, archive and term, with an override on each post, page and term. 20 * **Robots and canonical.** Noindex where it belongs (search results, empty archives, password-protected posts, feeds), a canonical URL on every page, and a per-post choice. 21 * **Social sharing.** Open Graph and X (Twitter) tags, with an image per post, the featured image or a default image. 22 * **Structured data.** One JSON-LD graph per page: Organization, local business or Person, WebSite, WebPage, Article on posts, ProfilePage on author archives and BreadcrumbList. 23 * **Breadcrumb.** Uses the WordPress Breadcrumbs block; its structured data follows what the block shows. 24 * **Sitemap.** Uses the sitemap built into WordPress and keeps it consistent with the robots rules: only content that can be indexed is listed. 25 * **Robots.txt.** Adds your rules to the robots.txt generated by WordPress, without writing files. 26 * **Redirects.** 301, 302 and 307, plus 410 (gone) and 451 (unavailable for legal reasons); exact addresses or regular expressions. A redirect is created by itself when you change the address of a published page. 27 * **Custom code.** HTML and PHP snippets in the head, after the opening body tag, in the footer or, for PHP, everywhere, with syntax check, revisions and a safe mode. 28 * **Import.** Copies the SEO fields of posts and terms from Yoast SEO and Rank Math, and their redirects (Yoast SEO Premium and Rank Math), without overwriting what you already set in EasyRankly. 29 * **Multilingual.** Languages with their own address prefix, translations linked to each other, hreflang, a language switcher block, and per-language site title, tagline, template parts and synced patterns. Needs a block theme. 30 31 = What it does not do = 32 33 * No content score or real-time readability analysis. 34 * No 404 log or hit counters. 35 * No news or video sitemaps, no IndexNow or Google Indexing API. 36 * No AI features: they belong to EasyRankly Pro, a separate paid plugin. 37 * No multisite-specific features: on a network, each site has its own settings. 38 39 = Built on native WordPress APIs = 40 41 * **No custom tables.** Everything is stored with WordPress options, meta, post types and taxonomies. 42 * **No cron jobs.** Nothing runs in the background on your server. 43 * **No frontend scripts.** Your visitors download nothing extra. 44 45 = Source code = 46 47 The JavaScript of the admin screens and of the block editor panels ships compiled in the `build` folder. Its source code is included in the plugin, in the `assets/src` folder, with `package.json` and `webpack.config.js`. To build it yourself, run `npm install` and `npm run build` in the plugin folder. 37 48 38 49 == Installation == 39 50 40 1. Upload the `easyrankly` folder to `/wp-content/plugins/`. 41 2. Activate EasyRankly from the Plugins screen. 42 3. Complete the short setup wizard, or configure the plugin later under Settings > EasyRankly. 51 1. In your WordPress admin, go to Plugins > Add New and search for "EasyRankly", or upload the zip file there. 52 2. Activate the plugin. 53 3. Open EasyRankly in the admin menu: check the titles, the indexing choices and who the site represents (Schema and social). 54 4. Optional: add languages in EasyRankly > Languages, redirects in EasyRankly > Redirects and snippets in EasyRankly > Custom code. 55 5. Coming from Yoast SEO or Rank Math? Deactivate it, then EasyRankly > Import copies its SEO fields and redirects. 56 57 EasyRankly needs WordPress 7.0 and PHP 8.0 or later. Use one SEO plugin at a time: deactivate other SEO plugins before activating EasyRankly. 43 58 44 59 == Frequently Asked Questions == 45 60 46 = Can I run EasyRankly alongside another SEO plugin such as Yoast SEO or Rank Math? =61 = Can I use EasyRankly together with another SEO plugin? = 47 62 48 Yes. EasyRankly detects active SEO plugins and automatically steps back from overlapping output (document title, meta description, canonical, robots meta, sitemaps, and robots.txt) so you never get duplicate tags. You can force any of it back on with the `erankly_enable_head_output`, `erankly_enable_sitemaps_with_external_seo`, and `erankly_enable_robots_txt_with_external_seo` filters.63 No. Two SEO plugins print the same tags twice: title, description, canonical and structured data. Deactivate Yoast SEO, Rank Math or any other SEO plugin before activating EasyRankly; EasyRankly > Import copies their SEO fields and redirects. 49 64 50 = Does it support WooCommerce? =65 = Does it work on multisite? = 51 66 52 Yes. Ea syRankly can output Product JSON-LD structured data for WooCommerce products. It is controlled by the `erankly_woocommerce_structured_data_enabled` and `erankly_render_woocommerce_product_schema` filters.67 Yes. Each site of the network has its own settings, redirects, snippets and languages, with nothing to set up for the network. There are no network-wide features. Deleting the plugin removes its data from every site of the network. 53 68 54 = Does EasyRankly work on WordPress Multisite? =69 = Does EasyRankly collect personal data or send data to external servers? = 55 70 56 Yes. There is full Multisite support with network-level global settings, plus an optional multilingual module that links posts, pages, and terms across network sites and outputs hreflang alternates in the head and XML sitemaps.71 No. It connects to no external service, does not track visitors and stores nothing about them: what it saves is your settings and SEO fields, in your WordPress database. 57 72 58 = Does EasyRankly collect any personal data or phone home? =73 = How do header, footer and patterns change language? = 59 74 60 No. EasyRankly makes no outbound requests from your server: there is no analytics, tracking, or telemetry, and no data is sent anywhere. The optional Health 404 monitor stores only anonymized request paths in your own database; no user data is recorded. The only third-party reference is the public YouTube thumbnail URL added to the optional XML video sitemap for YouTube videos you have embedded yourself — see the External services section below.75 In block themes, create a template part or a synced pattern with the same name followed by the language prefix: "header-en" for "header", "Banner - EN" for "Banner". On the English pages it replaces the original. This is also how each language gets its own menu: the Navigation block in "header-en" shows the English menu. Unsynced patterns become a copy where they are inserted, so they are translated with the page of each language. 61 76 62 = How do I display breadcrumbs? =77 = Are categories and tags translated? = 63 78 64 Call `erankly_breadcrumbs()` in your theme template. You can customise the markup with the `erankly_breadcrumb_items` and `erankly_breadcrumbs_html` filters.79 No, each category or tag is the same in every language, with its address in each language. Its SEO title, description and social texts, and its description, are written in the default language: on the pages of the other languages they are ignored and the title and description templates of that language are used. 65 80 66 = Can I import my settings from Yoast SEO or Rank Math? =81 = Are classic themes supported? = 67 82 68 Yes. Open Settings > EasyRankly > Import/Export. It can import all plugin data from a JSON file and includes dedicated Yoast SEO and Rank Math importers for per-content meta.83 SEO, sitemap, redirects and custom code work with every theme. Two features need a block theme. The breadcrumb uses the WordPress Breadcrumbs block, and its structured data (BreadcrumbList) is printed only when the block is part of the template, which classic themes render after the page head. Multilingual needs a block theme too: with a classic theme, contents, addresses, site title and tagline change with the language, but menus, widgets and texts set in the Customizer show the same text in every language. 69 84 70 = = Developer API ==85 = What does the "Everywhere" position of Custom code do? = 71 86 72 Available filters include: 87 It runs a PHP snippet as soon as the plugins are loaded, on every request, admin included, like the functions.php of a theme: use it to add or remove hooks. It never runs on the Custom code screen when you open it, so a broken snippet can always be fixed there. What the snippet prints at that moment is dropped. If a snippet locks you out, add `define( 'EASYRANKLY_SAFE_MODE', true );` to wp-config.php: no snippet runs until you remove it. 73 88 74 * `erankly_breadcrumb_items` 75 * `erankly_breadcrumbs_html` 76 * `erankly_canonical` 77 * `erankly_description` 78 * `erankly_enable_head_output` 79 * `erankly_enable_robots_txt_with_external_seo` 80 * `erankly_enable_sitemaps_with_external_seo` 81 * `erankly_faq_items` 82 * `erankly_head_credit_name` 83 * `erankly_health_404_sample_rate` 84 * `erankly_hreflang_alternates` 85 * `erankly_image_sitemap_url` 86 * `erankly_include_user_sitemap` 87 * `erankly_local_business_types` 88 * `erankly_localized_url` 89 * `erankly_ml_default_notice` 90 * `erankly_ml_language_native_name` 91 * `erankly_navigable_hreflang_alternates` 92 * `erankly_news_sitemap_post_types` 93 * `erankly_news_sitemap_publication_language` 94 * `erankly_news_sitemap_publication_name` 95 * `erankly_news_sitemap_url` 96 * `erankly_og_description` 97 * `erankly_og_image` 98 * `erankly_og_title` 99 * `erankly_opengraph_tags` 100 * `erankly_organization_schema_details` 101 * `erankly_post_breadcrumb_name` 102 * `erankly_post_types` 103 * `erankly_redirect_hit_sample_rate` 104 * `erankly_render_woocommerce_product_schema` 105 * `erankly_robots` 106 * `erankly_robots_txt_lines` 107 * `erankly_schema` 108 * `erankly_schema_article` 109 * `erankly_schema_blogposting` 110 * `erankly_schema_breadcrumb_list` 111 * `erankly_schema_faq` 112 * `erankly_schema_local_business` 113 * `erankly_schema_localbusiness` 114 * `erankly_schema_organization` 115 * `erankly_schema_person` 116 * `erankly_schema_service` 117 * `erankly_schema_webpage` 118 * `erankly_schema_website` 119 * `erankly_seo_checklist_docs_url` 120 * `erankly_settings_tabs` 121 * `erankly_sitemap_images` 122 * `erankly_sitemap_post_types` 123 * `erankly_special_pages` 124 * `erankly_taxonomies` 125 * `erankly_title` 126 * `erankly_twitter_card_type` 127 * `erankly_twitter_description` 128 * `erankly_twitter_image` 129 * `erankly_twitter_site` 130 * `erankly_twitter_title` 131 * `erankly_video_sitemap_url` 132 * `erankly_woocommerce_structured_data_enabled` 89 = Who can use PHP snippets? = 133 90 134 Available actions include: 91 Only users who can edit plugin files (the `edit_plugins` capability, which on a standard site means administrators). PHP snippets are switched off on sites that define `DISALLOW_FILE_EDIT` or `DISALLOW_FILE_MODS` in wp-config.php: existing ones stop running and new ones cannot be saved. HTML snippets need the `unfiltered_html` capability, which administrators have on a single site and only super admins have on a multisite network. 135 92 136 * `erankly_redirects_caches_flushed` 137 * `erankly_render_settings_tab_{$slug}` 93 = What happens when a PHP snippet breaks? = 138 94 139 Use `erankly_breadcrumbs()` to render breadcrumbs.95 The code is checked for syntax errors before it is saved, and a snippet that does not parse can be saved only as inactive. If an active snippet throws an error, the error is shown to administrators and the snippet stays on, so visitors cannot switch it off; a fatal error, which breaks the page, switches it off. If a snippet locks you out anyway, add `define( 'EASYRANKLY_SAFE_MODE', true );` to wp-config.php: no snippet runs until you remove it. 140 96 141 When the Multisite multilingual module is enabled, the `[erankly_language_switcher]` and `[erankly_translation_notice]` shortcodes are available. 97 = How are password-protected posts handled? = 98 99 They are noindex and left out of the sitemap. Their automatic description is empty, so their text never appears in the page source; a description you write for the post yourself is still used. 100 101 = Are empty archives indexed? = 102 103 No. A category, tag, author or post type archive with nothing in it is noindex until it has content. A category or tag with its own description stays indexable, since the description is content. 104 105 = Are feeds and attachment pages indexed? = 106 107 Feeds are never indexed: they send an `X-Robots-Tag: noindex, follow` header, so search engines still follow their links. Attachment pages, which show only the uploaded file, are off by default on sites created with WordPress 6.4 or later. On older sites, turn them off in EasyRankly > Indexing: their address then leads to the file. This is the WordPress setting, so it stays as you set it if you remove EasyRankly. 108 109 = Does it work with WooCommerce and ACF? = 110 111 Yes, without conflicts. The shop page gives the product archive its SEO title, description and canonical, cart, checkout and account pages stay out of the sitemap, and WooCommerce keeps printing its own Product structured data. Products, and any content edited with the classic editor, get the SEO and Language fields in boxes under the editor. Not supported yet: multilingual stores. With ACF, title and description templates can use a custom field with {{cf:field_name}}; values saved inside ACF blocks are not read. 142 112 143 113 == External services == 144 114 145 EasyRankly does not call any external service from your server. It performs no analytics, tracking, or telemetry, and sends no data to any remote endpoint. 146 147 The only third-party reference appears in the optional XML video sitemap. When the video sitemap is enabled and a post embeds a YouTube video, EasyRankly adds that video's public thumbnail URL (https://img.youtube.com/vi/<video-id>/0.jpg) to the sitemap so search engines can show a preview. The video ID is taken from the YouTube video you embedded yourself; EasyRankly does not contact YouTube — the URL is only written into your sitemap and is fetched by search-engine crawlers, not by your site. No personal data is involved. 148 149 This thumbnail service is provided by YouTube (Google LLC). Terms of Service: https://www.youtube.com/t/terms — Privacy Policy: https://policies.google.com/privacy 115 EasyRankly does not connect to any external service. 150 116 151 117 == Changelog == 152 118 153 = 1.0.0 = 154 Release date: June 14, 2026 155 156 * First public release. 119 = 2.0.0 = 120 * Complete rewrite on native WordPress APIs: titles and descriptions, robots, canonical, social tags, structured data, breadcrumb, sitemap, robots.txt, redirects, custom code and multilingual. 121 * Requires WordPress 7.0 and PHP 8.0. 122 * Import of the SEO fields of posts and terms from Yoast SEO and Rank Math, and of their redirects (Yoast SEO Premium and Rank Math) (EasyRankly > Import). 123 * Settings, SEO fields and redirects of version 1.0.0 are not imported. 157 124 158 125 == Upgrade Notice == 159 126 160 = 1.0.0 =161 First public release of EasyRankly.127 = 2.0.0 = 128 Complete rewrite. Settings, SEO fields and redirects of 1.0.0 are not imported: set them again after updating. Requires WordPress 7.0 and PHP 8.0. -
easyrankly/trunk/uninstall.php
r3588731 r3759017 1 1 <?php 2 2 /** 3 * Plugin uninstall cleanup. 3 * Removes everything EasyRankly created on the site, or on every site of the network on multisite. 4 * 5 * Every option, meta key, post type and taxonomy listed in docs/data-model.md 6 * must be deleted here, and tests/UninstallTest.php must prove it. 4 7 * 5 8 * @package EasyRankly 6 9 */ 7 10 8 if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) { 9 exit; 11 defined( 'WP_UNINSTALL_PLUGIN' ) || exit; 12 13 // Everything EasyRankly stores lives in the tables of a single site, so the cleanup runs once per site. 14 $easyrankly_cleanup = static function (): void { 15 // Redirects (src/Redirects/Redirects.php) and snippets (src/CustomCode/CustomCode.php): posts with 16 // their meta and revisions first, then the caches built from them. Each post is tried once, so 17 // one that cannot be deleted does not stop the others: in ID order, the ones left behind are always 18 // the first ones, and the offset skips them. 19 $easyrankly_failed = 0; 20 do { 21 $easyrankly_ids = get_posts( 22 array( 23 'post_type' => array( 'erankly_redirect', 'erankly_snippet' ), 24 'post_status' => array_keys( get_post_stati() ), 25 'orderby' => 'ID', 26 'order' => 'ASC', 27 'offset' => $easyrankly_failed, 28 'posts_per_page' => 100, 29 'fields' => 'ids', 30 'no_found_rows' => true, 31 ) 32 ); 33 foreach ( $easyrankly_ids as $easyrankly_id ) { 34 if ( ! wp_delete_post( (int) $easyrankly_id, true ) ) { 35 ++$easyrankly_failed; 36 } 37 } 38 } while ( array() !== $easyrankly_ids ); 39 40 // Languages and translation groups (src/Multilingual/): the plugin is not loaded here, so the 41 // taxonomies are registered (if missing) just to delete their terms, which removes the relationships too. 42 foreach ( array( 'erankly_language', 'erankly_translation' ) as $easyrankly_taxonomy ) { 43 if ( ! taxonomy_exists( $easyrankly_taxonomy ) ) { 44 register_taxonomy( $easyrankly_taxonomy, array() ); 45 } 46 $easyrankly_failed = 0; 47 do { 48 $easyrankly_terms = get_terms( 49 array( 50 'taxonomy' => $easyrankly_taxonomy, 51 'hide_empty' => false, 52 'orderby' => 'term_id', 53 'order' => 'ASC', 54 'offset' => $easyrankly_failed, 55 'fields' => 'ids', 56 'number' => 100, 57 ) 58 ); 59 $easyrankly_terms = is_array( $easyrankly_terms ) ? $easyrankly_terms : array(); 60 foreach ( $easyrankly_terms as $easyrankly_term ) { 61 if ( true !== wp_delete_term( (int) $easyrankly_term, $easyrankly_taxonomy ) ) { 62 ++$easyrankly_failed; 63 } 64 } 65 } while ( array() !== $easyrankly_terms ); 66 } 67 68 // Notices about redirects not created after an address change (src/Redirects/SlugChanges.php): one 69 // transient per user, only for users whose role can change addresses. Others, if any, expire in a day 70 // and the core removes them with the other expired transients. 71 $easyrankly_offset = 0; 72 do { 73 $easyrankly_users = get_users( 74 array( 75 'capability__in' => array( 'edit_pages', 'edit_posts', 'manage_categories' ), 76 'fields' => 'ID', 77 'number' => 500, 78 'offset' => $easyrankly_offset, 79 'orderby' => 'ID', 80 'count_total' => false, 81 ) 82 ); 83 foreach ( $easyrankly_users as $easyrankly_user ) { 84 delete_transient( 'easyrankly_redirect_notice_' . (int) $easyrankly_user ); 85 } 86 $easyrankly_offset += 500; 87 } while ( array() !== $easyrankly_users ); 88 89 delete_option( 'easyrankly_settings' ); 90 delete_option( 'easyrankly_redirects_forced' ); 91 delete_option( 'easyrankly_redirects_regex' ); 92 delete_option( 'easyrankly_snippets' ); 93 delete_option( 'easyrankly_import' ); 94 95 // SEO meta of posts and terms (src/Meta/Meta.php). 96 foreach ( 97 array( 98 '_easyrankly_title', 99 '_easyrankly_description', 100 '_easyrankly_canonical', 101 '_easyrankly_noindex', 102 '_easyrankly_nofollow', 103 '_easyrankly_og_title', 104 '_easyrankly_og_description', 105 '_easyrankly_og_image', 106 ) as $easyrankly_key 107 ) { 108 delete_metadata( 'post', 0, $easyrankly_key, '', true ); 109 delete_metadata( 'term', 0, $easyrankly_key, '', true ); 110 } 111 }; 112 113 // On multisite the plugin can be active on any site of any network, and WordPress runs uninstall.php once, on the 114 // site where it is deleted: every site is cleaned, in batches so large networks do not load all sites at once. 115 if ( is_multisite() ) { 116 $easyrankly_offset = 0; 117 do { 118 $easyrankly_sites = get_sites( 119 array( 120 'fields' => 'ids', 121 'number' => 100, 122 'offset' => $easyrankly_offset, 123 'orderby' => 'id', 124 'update_site_meta_cache' => false, 125 ) 126 ); 127 foreach ( $easyrankly_sites as $easyrankly_site ) { 128 switch_to_blog( (int) $easyrankly_site ); 129 $easyrankly_cleanup(); 130 restore_current_blog(); 131 } 132 $easyrankly_offset += 100; 133 } while ( array() !== $easyrankly_sites ); 134 } else { 135 $easyrankly_cleanup(); 10 136 } 11 12 global $wpdb;13 14 /**15 * Removes per-site options, the redirect table, post meta and transients for one site.16 *17 * Global settings (erankly_settings, erankly_version) are handled separately18 * because on Multisite they are stored as network options and must be deleted once.19 *20 * @return void21 */22 function erankly_uninstall_site(): void {23 global $wpdb;24 25 wp_clear_scheduled_hook( 'erankly_health_prune_404_cron' );26 27 delete_option( 'erankly_redirects_db_version' );28 delete_option( 'erankly_redirects_runtime_rules' );29 delete_option( 'erankly_flush_rewrite_rules' );30 delete_option( 'erankly_sitemap_cache_version' );31 delete_option( 'erankly_health_404_candidates' );32 delete_option( 'erankly_health_404_frequent' );33 delete_option( 'erankly_health_thin_content' );34 35 $wpdb->query( $wpdb->prepare( 'DROP TABLE IF EXISTS %i', $wpdb->prefix . 'erankly_redirects' ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange -- Uninstall cleanup removes the plugin-owned redirects table.36 37 $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Uninstall cleanup removes plugin-owned post meta.38 $wpdb->prepare(39 "DELETE FROM {$wpdb->postmeta} WHERE meta_key LIKE %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared40 $wpdb->esc_like( '_erankly_' ) . '%'41 )42 );43 $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Uninstall cleanup removes plugin-owned term meta.44 $wpdb->prepare(45 "DELETE FROM {$wpdb->termmeta} WHERE meta_key LIKE %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared46 $wpdb->esc_like( '_erankly_' ) . '%'47 )48 );49 $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Uninstall cleanup removes plugin-owned sitemap transients.50 $wpdb->prepare(51 "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s OR option_name LIKE %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared52 $wpdb->esc_like( '_transient_erankly_sitemap_' ) . '%',53 $wpdb->esc_like( '_transient_timeout_erankly_sitemap_' ) . '%'54 )55 );56 }57 58 if ( is_multisite() ) {59 // Global settings are stored as network options — delete once for the entire network.60 delete_site_option( 'erankly_settings' );61 delete_site_option( 'erankly_version' );62 delete_site_option( 'erankly_setup_wizard_status' );63 64 // Multilingual module data: network options and the network-wide relations table.65 delete_site_option( 'erankly_ml_sites' );66 delete_site_option( 'erankly_ml_db_version' );67 $wpdb->query( $wpdb->prepare( 'DROP TABLE IF EXISTS %i', $wpdb->base_prefix . 'erankly_ml_relations' ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange -- Uninstall cleanup removes the plugin-owned multilingual relations table.68 69 // Per-site data must be cleaned up on every site.70 $erankly_site_ids = get_sites(71 array(72 'fields' => 'ids',73 )74 );75 76 foreach ( $erankly_site_ids as $erankly_site_id ) {77 switch_to_blog( (int) $erankly_site_id );78 erankly_uninstall_site();79 restore_current_blog();80 }81 } else {82 delete_option( 'erankly_settings' );83 delete_option( 'erankly_version' );84 delete_option( 'erankly_setup_wizard_status' );85 erankly_uninstall_site();86 }
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)