> */ private $option_rules = array( 'exact' => array( // Divi design data. 'et_divi' => self::DESIGN, 'builder_global_presets_d5' => self::DESIGN, 'builder_global_presets_ng' => self::DESIGN, 'builder_global_presets' => self::DESIGN, 'global_variables' => self::DESIGN, 'et_global_data' => self::DESIGN, 'et_global_colors' => self::DESIGN, 'et_uploaded_fonts' => self::DESIGN, // Order of Divi Library items in the component manager (lists of layout IDs). '_et_pb_component_manager_order' => self::LAYOUT, 'custom_css_post_id' => self::DESIGN, // Site settings that are meant to move with a redesign. 'page_on_front' => self::SITE, 'page_for_posts' => self::SITE, 'show_on_front' => self::SITE, 'blogname' => self::SITE, 'blogdescription' => self::SITE, 'sidebars_widgets' => self::SITE, 'nav_menu_options' => self::SITE, 'site_icon' => self::SITE, 'posts_per_page' => self::SITE, 'date_format' => self::SITE, 'time_format' => self::SITE, 'timezone_string' => self::SITE, 'gmt_offset' => self::SITE, 'WPLANG' => self::SITE, // Environment: belongs to the installation, never synced. 'siteurl' => self::ENVIRONMENT, 'home' => self::ENVIRONMENT, 'admin_email' => self::ENVIRONMENT, 'new_admin_email' => self::ENVIRONMENT, 'active_plugins' => self::ENVIRONMENT, 'template' => self::ENVIRONMENT, 'stylesheet' => self::ENVIRONMENT, 'users_can_register' => self::ENVIRONMENT, 'default_role' => self::ENVIRONMENT, 'db_version' => self::ENVIRONMENT, 'initial_db_version' => self::ENVIRONMENT, 'wp_user_roles' => self::ENVIRONMENT, 'cron' => self::ENVIRONMENT, 'current_theme' => self::ENVIRONMENT, // Divi Visual Builder workspace layout of the editor, not of the website. 'et_divi_builder_workspaces' => self::ENVIRONMENT, // Visual Builder settings of the editors (product tour, post type integration). 'et_pb_builder_options' => self::ENVIRONMENT, 'wp_calendar_block_has_published_posts' => self::CACHE, // Divi's news feed for the builder, fetched from Elegant Themes. 'et_divi_builder_announcements_cache' => self::CACHE, 'recently_activated' => self::ENVIRONMENT, 'uninstall_plugins' => self::ENVIRONMENT, 'auth_key' => self::ENVIRONMENT, 'auth_salt' => self::ENVIRONMENT, 'logged_in_key' => self::ENVIRONMENT, 'logged_in_salt' => self::ENVIRONMENT, 'nonce_key' => self::ENVIRONMENT, 'nonce_salt' => self::ENVIRONMENT, 'secure_auth_key' => self::ENVIRONMENT, 'secure_auth_salt' => self::ENVIRONMENT, 'mailserver_url' => self::ENVIRONMENT, 'mailserver_login' => self::ENVIRONMENT, 'mailserver_pass' => self::ENVIRONMENT, 'mailserver_port' => self::ENVIRONMENT, 'upload_path' => self::ENVIRONMENT, 'upload_url_path' => self::ENVIRONMENT, 'et_automatic_updates_options' => self::ENVIRONMENT, 'wonderful_sync_settings' => self::ENVIRONMENT, 'wonderful_sync_baseline' => self::ENVIRONMENT, // Permalinks and discussion rules (moderation, registration, spam lists) are // administration of each site: they are never transferred. 'permalink_structure' => self::ENVIRONMENT, 'category_base' => self::ENVIRONMENT, 'tag_base' => self::ENVIRONMENT, 'close_comments_days_old' => self::ENVIRONMENT, 'close_comments_for_old_posts' => self::ENVIRONMENT, 'comment_max_links' => self::ENVIRONMENT, 'comment_moderation' => self::ENVIRONMENT, 'comment_previously_approved' => self::ENVIRONMENT, 'comment_registration' => self::ENVIRONMENT, 'comments_notify' => self::ENVIRONMENT, 'default_comment_status' => self::ENVIRONMENT, 'default_ping_status' => self::ENVIRONMENT, 'default_pingback_flag' => self::ENVIRONMENT, 'disallowed_keys' => self::ENVIRONMENT, 'moderation_keys' => self::ENVIRONMENT, 'moderation_notify' => self::ENVIRONMENT, 'require_name_email' => self::ENVIRONMENT, 'show_comments_cookies_opt_in' => self::ENVIRONMENT, 'use_trackback' => self::ENVIRONMENT, // Core settings screens (reading, discussion, media, writing). 'comment_order' => self::SITE, 'comments_per_page' => self::SITE, 'default_category' => self::SITE, 'default_comments_page' => self::SITE, 'default_email_category' => self::SITE, 'default_link_category' => self::SITE, 'default_post_format' => self::SITE, 'image_default_align' => self::SITE, 'image_default_link_type' => self::SITE, 'image_default_size' => self::SITE, 'large_size_h' => self::SITE, 'large_size_w' => self::SITE, 'medium_large_size_h' => self::SITE, 'medium_large_size_w' => self::SITE, 'medium_size_h' => self::SITE, 'medium_size_w' => self::SITE, 'thumbnail_crop' => self::SITE, 'thumbnail_size_h' => self::SITE, 'thumbnail_size_w' => self::SITE, 'page_comments' => self::SITE, 'posts_per_rss' => self::SITE, 'rss_use_excerpt' => self::SITE, 'show_avatars' => self::SITE, 'avatar_default' => self::SITE, 'avatar_rating' => self::SITE, 'start_of_week' => self::SITE, 'sticky_posts' => self::SITE, 'thread_comments' => self::SITE, 'thread_comments_depth' => self::SITE, 'use_smilies' => self::SITE, 'use_balanceTags' => self::SITE, 'wp_attachment_pages_enabled' => self::SITE, 'wp_page_for_privacy_policy' => self::SITE, 'uploads_use_yearmonth_folders' => self::SITE, 'links_updated_date_format' => self::SITE, 'link_manager_enabled' => self::SITE, 'wp_notes_notify' => self::SITE, // blog_public is 0 on every staging copy – syncing it would deindex the live site. 'blog_public' => self::ENVIRONMENT, // WordPress empties the ping list when search engines are discouraged (staging). 'ping_sites' => self::ENVIRONMENT, // WooCommerce image sizes the theme writes on its own when it runs. 'shop_catalog_image_size' => self::ENVIRONMENT, 'shop_single_image_size' => self::ENVIRONMENT, 'shop_thumbnail_image_size' => self::ENVIRONMENT, 'admin_email_lifespan' => self::ENVIRONMENT, 'auto_plugin_theme_update_emails' => self::ENVIRONMENT, 'blog_charset' => self::ENVIRONMENT, 'html_type' => self::ENVIRONMENT, 'fresh_site' => self::ENVIRONMENT, 'recovery_keys' => self::ENVIRONMENT, 'recovery_mode_email_last_sent' => self::ENVIRONMENT, 'hack_file' => self::ENVIRONMENT, 'finished_splitting_shared_terms' => self::ENVIRONMENT, 'wp_force_deactivated_plugins' => self::ENVIRONMENT, 'can_compress_scripts' => self::ENVIRONMENT, // Rebuilt by WordPress on its own. 'rewrite_rules' => self::CACHE, 'user_count' => self::CACHE, 'recently_edited' => self::CACHE, ), 'prefix' => array( 'theme_mods_' => self::DESIGN, 'widget_' => self::SITE, 'et_divi_' => self::DESIGN, 'et_pb_' => self::DESIGN, '_transient_' => self::CACHE, // Wonderful Sync's own state (connection, log, baseline) belongs to each site. 'wonderful_sync_' => self::ENVIRONMENT, '_site_transient_' => self::CACHE, 'et_core_' => self::CACHE, '_et_' => self::CACHE, // Per-form state flags Divi writes on its own (hash suffix). 'et_pb_contact_form_' => self::CACHE, 'et_pb_signup_' => self::CACHE, 'et_pb_cache_notice' => self::CACHE, // Written by ARMember on every request, with a timestamp in the name. 'arm_is_another_sort_data_mismatched_' => self::CACHE, 'et_schedule_' => self::CACHE, 'et_images_temp_' => self::CACHE, 'theme_switched' => self::CACHE, // Divi account, support centre and safe mode belong to the installation. 'divi_' => self::ENVIRONMENT, 'et_account_' => self::ENVIRONMENT, 'et_support_' => self::ENVIRONMENT, 'et_safe_mode_' => self::ENVIRONMENT, 'et_google_api_' => self::ENVIRONMENT, 'et_bfb_' => self::DESIGN, 'shop_' => self::SITE, // Clone tooling (do-tools) and installation-bound plugins. 'dt_' => self::ENVIRONMENT, 'wfls_' => self::ENVIRONMENT, 'fs_' => self::ENVIRONMENT, 'postman_' => self::ENVIRONMENT, 'jetpack_' => self::ENVIRONMENT, 'crontrol_' => self::ENVIRONMENT, 'mpsum' => self::ENVIRONMENT, 'MPSUM' => self::ENVIRONMENT, 'swift_' => self::ENVIRONMENT, // Caches and licences of common plugins. 'autoptimize_' => self::ENVIRONMENT, 'wpseo' => self::SITE, 'rank_math' => self::SITE, 'woocommerce_' => self::SITE, 'wordfence' => self::ENVIRONMENT, 'wf_' => self::ENVIRONMENT, 'wp_mail_smtp' => self::ENVIRONMENT, 'wonderful_smtp_' => self::ENVIRONMENT, 'ai1wm_' => self::ENVIRONMENT, 'action_scheduler_' => self::USER_DATA, '_wc_session_' => self::USER_DATA, ), ); /** * Well-known plugins: option/table prefix => [plugin name, class, plugin slug]. * * Lets the inventory name the owner even when the plugin is no longer * installed (orphaned options are common on long-running sites). * * @var array */ private $known_plugins = array( 'updraft_' => array( 'UpdraftPlus', self::ENVIRONMENT, 'updraftplus' ), 'updraftplus_' => array( 'UpdraftPlus', self::ENVIRONMENT, 'updraftplus' ), 'siteground_' => array( 'SiteGround Optimizer', self::ENVIRONMENT, 'sg-cachepress' ), 'sg_' => array( 'SiteGround Security', self::ENVIRONMENT, 'sg-security' ), 'sgo_' => array( 'SiteGround Optimizer', self::ENVIRONMENT, 'sg-cachepress' ), 'sgs_' => array( 'SiteGround Security', self::ENVIRONMENT, 'sg-security' ), 'rsssl_' => array( 'Really Simple Security', self::ENVIRONMENT, 'really-simple-ssl' ), 'wpsupercache_' => array( 'WP Super Cache', self::ENVIRONMENT, 'wp-super-cache' ), 'wpsc_' => array( 'WP Super Cache', self::ENVIRONMENT, 'wp-super-cache' ), 'supercache_' => array( 'WP Super Cache', self::ENVIRONMENT, 'wp-super-cache' ), 'ossdl_' => array( 'WP Super Cache (CDN)', self::ENVIRONMENT, 'wp-super-cache' ), 'acui_' => array( 'Import Users from CSV', self::ENVIRONMENT, 'import-users-from-csv-with-meta' ), 'real_queue' => array( 'devowl.io (Real Cookie Banner)', self::ENVIRONMENT, 'real-cookie-banner' ), 'real_utils' => array( 'devowl.io (Real Cookie Banner)', self::ENVIRONMENT, 'real-cookie-banner' ), 'eum_' => array( 'Easy Updates Manager', self::ENVIRONMENT, 'stops-core-theme-and-plugin-updates' ), 'mpsum' => array( 'Easy Updates Manager', self::ENVIRONMENT, 'stops-core-theme-and-plugin-updates' ), 'wp-piwik' => array( 'Connect Matomo', self::ENVIRONMENT, 'wp-piwik' ), 'cmplz_' => array( 'Complianz', self::SITE, 'complianz-gdpr' ), 'rcb-' => array( 'Real Cookie Banner', self::SITE, 'real-cookie-banner' ), 'rcb_' => array( 'Real Cookie Banner', self::SITE, 'real-cookie-banner' ), 'cookielawinfo_' => array( 'CookieYes', self::SITE, 'cookie-law-info' ), 'cookie_notice_' => array( 'Cookie Notice', self::SITE, 'cookie-notice' ), 'duplicate_post_' => array( 'Yoast Duplicate Post', self::SITE, 'duplicate-post' ), 'arm_' => array( 'ARMember', self::SITE, 'armember-membership' ), 'newsletter_' => array( 'Newsletter', self::SITE, 'newsletter' ), 'wpcode_' => array( 'WPCode', self::SITE, 'insert-headers-and-footers' ), 'autodescription' => array( 'The SEO Framework', self::SITE, 'autodescription' ), 'the_seo_framework' => array( 'The SEO Framework', self::SITE, 'autodescription' ), 'omgf_' => array( 'OMGF', self::SITE, 'host-webfonts-local' ), 'yydev_tagmanager' => array( 'Tag Manager (yydev)', self::SITE, 'yydev-tag-manager' ), 'safelayout_' => array( 'Safelayout Preloader', self::SITE, 'safelayout-elegant-preloader' ), 'login_designer' => array( 'Login Designer', self::SITE, 'login-designer' ), 'monarch_' => array( 'Monarch', self::SITE, 'monarch' ), 'foogallery' => array( 'FooGallery', self::SITE, 'foogallery' ), 'wc_blocks_' => array( 'WooCommerce', self::CACHE, 'woocommerce' ), ); /** * Tables of known plugins that hold user data, by prefix after the database prefix. * * @var array */ private $known_user_tables = array( 'arm_' => 'ARMember', 'newsletter' => 'Newsletter', 'rcb_consent' => 'Real Cookie Banner', 'rcb_stats' => 'Real Cookie Banner', 'cmplz_' => 'Complianz', 'sgs_log' => 'SiteGround Security', 'eum_logs' => 'Easy Updates Manager', ); /** * Option names that look like secrets are always environment, whatever prefix they carry. */ /** * Top-level keys of an option that describe the installation, not the design: Divi * writes them itself whenever it runs (e.g. a newer Divi on staging). Bound to the * option; never sent, the receiving site keeps its own values. */ const ENVIRONMENT_KEYS = array( 'et_divi' => array( 'divi_previous_installed_version', 'divi_latest_installed_version', 'divi_1_3_images', 'et_pb_layouts_updated', 'library_removed_legacy_layouts', 'divi_email_provider_credentials_migrated', ), 'theme_mods_Divi' => array( 'et_updated_layouts_built_for_post_types', ), ); /** * Key prefixes per option that belong to the installation or to one editor (Divi keeps * every Visual Builder user preference – toolbar, settings bar … – in et_divi). */ const ENVIRONMENT_PREFIXES = array( 'et_divi' => array( 'et_fb_pref_' ), ); const SECRET_PATTERN = '/(licen[cs]e|api[_-]?key|secret|token|password|passwd|_pass$|smtp|private[_-]?key|webhook|stripe|paypal|ppcp|mollie|klarna|braintree|square|gateway|merchant|client[_-]?id)/i'; /** * Post type rules. * * @var array */ private $post_type_rules = array( 'page' => self::CONTENT, 'post' => self::CONTENT, 'attachment' => self::CONTENT, 'wp_block' => self::LAYOUT, 'wp_template' => self::LAYOUT, 'wp_template_part' => self::LAYOUT, 'wp_navigation' => self::LAYOUT, 'wp_global_styles' => self::DESIGN, 'custom_css' => self::DESIGN, 'nav_menu_item' => self::LAYOUT, // Divi. 'et_pb_layout' => self::LAYOUT, 'et_theme_builder' => self::LAYOUT, 'et_template' => self::LAYOUT, 'et_header_layout' => self::LAYOUT, 'et_body_layout' => self::LAYOUT, 'et_footer_layout' => self::LAYOUT, 'et_code_snippet' => self::LAYOUT, 'et_tb_item' => self::LAYOUT, // Flatsome UX Builder blocks. 'blocks' => self::LAYOUT, 'et_theme_options' => self::DESIGN, // Revisions and drafts the editor creates on its own. 'revision' => self::CACHE, '_et_pb_speculation' => self::CACHE, 'customize_changeset' => self::CACHE, 'oembed_cache' => self::CACHE, 'user_request' => self::USER_DATA, 'wp_font_family' => self::DESIGN, 'wp_font_face' => self::DESIGN, // Shop and form data live on the live site only. 'shop_order' => self::USER_DATA, 'shop_order_refund' => self::USER_DATA, 'shop_order_placehold' => self::USER_DATA, 'shop_subscription' => self::USER_DATA, 'shop_coupon' => self::USER_DATA, 'flamingo_inbound' => self::USER_DATA, 'flamingo_contact' => self::USER_DATA, 'nf_sub' => self::USER_DATA, 'wpcf7_contact_form' => self::SITE, 'product' => self::CONTENT, 'product_variation' => self::CONTENT, ); /** * Post meta keys that are caches and must never travel. * * @var string[] */ private $cache_meta_prefixes = array( '_wonderful_sync_release', '_et_builder_post_features_cache', '_et_dynamic_cached_', '_divi_dynamic_assets_', '_et_pb_first_image', '_et_pb_truncate_post', 'et_enqueued_post_fonts', '_et_builder_dynamic_assets_', '_et_pb_ab_', '_edit_lock', '_edit_last', '_wp_old_slug', '_wp_old_date', '_oembed_', '_encloseme', '_pingme', ); /** * Post meta written by live traffic (stock, sales, ratings). Kept on the target, never synced. * * @var string[] */ private $user_meta_keys = array( '_stock', '_stock_status', 'total_sales', '_wc_average_rating', '_wc_rating_count', '_wc_review_count', '_wp_trash_meta_status', '_wp_trash_meta_time', '_wp_desired_post_slug', ); /** * Table rules without the database prefix. * * @var array */ private $table_rules = array( 'options' => self::SITE, 'posts' => self::CONTENT, 'postmeta' => self::CONTENT, 'terms' => self::CONTENT, 'term_taxonomy' => self::CONTENT, 'term_relationships' => self::CONTENT, 'termmeta' => self::CONTENT, 'links' => self::CONTENT, 'users' => self::USER_DATA, 'usermeta' => self::USER_DATA, 'comments' => self::USER_DATA, 'commentmeta' => self::USER_DATA, 'wc_orders' => self::USER_DATA, 'wc_orders_meta' => self::USER_DATA, 'wc_order_addresses' => self::USER_DATA, 'wc_order_operational_data' => self::USER_DATA, 'wc_order_stats' => self::USER_DATA, 'wc_order_product_lookup' => self::USER_DATA, 'wc_customer_lookup' => self::USER_DATA, 'woocommerce_sessions' => self::USER_DATA, 'woocommerce_order_items' => self::USER_DATA, 'woocommerce_order_itemmeta' => self::USER_DATA, 'woocommerce_payment_tokens' => self::USER_DATA, 'woocommerce_api_keys' => self::ENVIRONMENT, 'actionscheduler_actions' => self::USER_DATA, 'actionscheduler_logs' => self::USER_DATA, 'actionscheduler_claims' => self::USER_DATA, 'actionscheduler_groups' => self::USER_DATA, ); /** * Table prefixes (after the database prefix) for plugin tables. * * @var array */ private $table_prefix_rules = array( 'wf' => self::USER_DATA, 'gf_' => self::USER_DATA, 'rg_' => self::USER_DATA, 'wpforms_' => self::USER_DATA, 'e_submissions' => self::USER_DATA, 'yoast_' => self::CACHE, 'rank_math_' => self::CACHE, 'redirection_' => self::SITE, 'wc_' => self::USER_DATA, ); /** * Classifies an option by name. * * @param string $name Option name. */ public function option( string $name ): string { if ( isset( $this->option_rules['exact'][ $name ] ) ) { $class = $this->option_rules['exact'][ $name ]; } elseif ( preg_match( self::SECRET_PATTERN, $name ) ) { $class = self::ENVIRONMENT; } else { $class = $this->match_prefix( $name, $this->option_rules['prefix'] ); if ( self::UNASSIGNED === $class ) { $known = $this->known_plugin( $name ); $class = $known ? $known[1] : self::UNASSIGNED; } } /** * Filters the sync class of an option. * * @param string $class One of the Classifier constants. * @param string $name Option name. * @param string $kind 'option', 'post_type', 'meta' or 'table'. */ return (string) apply_filters( 'wonderful_sync_classify', $class, $name, 'option' ); } /** * Classifies a post type. Unknown public post types count as content. * * @param string $post_type Post type name. */ public function post_type( string $post_type ): string { if ( in_array( $post_type, Pods::STRUCTURE, true ) ) { // Pods, groups and fields: layouts, unless switched off in the settings. $class = Pods::structure_enabled() ? self::LAYOUT : self::UNASSIGNED; } elseif ( isset( $this->post_type_rules[ $post_type ] ) ) { $class = $this->post_type_rules[ $post_type ]; } else { $object = get_post_type_object( $post_type ); $class = ( $object && $object->public ) ? self::CONTENT : self::UNASSIGNED; } return (string) apply_filters( 'wonderful_sync_classify', $class, $post_type, 'post_type' ); } /** * Classifies a post meta key. Everything that is not a cache or live data travels with its post. * * @param string $key Meta key. */ public function meta( string $key ): string { // MySQL compares meta keys case-insensitively, so the rules do too. $lower = strtolower( trim( $key ) ); $class = in_array( $lower, $this->user_meta_keys, true ) ? self::USER_DATA : self::CONTENT; foreach ( $this->cache_meta_prefixes as $prefix ) { if ( 0 === strpos( $lower, strtolower( $prefix ) ) ) { $class = self::CACHE; break; } } return (string) apply_filters( 'wonderful_sync_classify', $class, $key, 'meta' ); } /** * Classifies a database table. * * @param string $table Full table name. * @param string $db_prefix Database prefix of this site. */ public function table( string $table, string $db_prefix ): string { if ( '' !== $db_prefix && 0 !== strpos( $table, $db_prefix ) ) { $class = self::UNASSIGNED; } else { $short = substr( $table, strlen( $db_prefix ) ); $class = $this->table_rules[ $short ] ?? $this->match_prefix( $short, $this->table_prefix_rules ); if ( self::UNASSIGNED === $class ) { $user = $this->match_prefix( $short, array_fill_keys( array_keys( $this->known_user_tables ), self::USER_DATA ) ); $known = $this->known_plugin( $short ); $class = self::UNASSIGNED !== $user ? $user : ( $known ? $known[1] : self::UNASSIGNED ); } } return (string) apply_filters( 'wonderful_sync_classify', $class, $table, 'table' ); } /** * Well-known plugin that owns this option or table name, if any. * * @param string $name Option name or table name without the database prefix. * @return array{0: string, 1: string, 2: string}|null [plugin name, class, plugin slug]. */ public function known_plugin( string $name ): ?array { $best = ''; foreach ( array_keys( $this->known_plugins ) as $prefix ) { if ( 0 === strpos( $name, $prefix ) && strlen( $prefix ) > strlen( $best ) ) { $best = $prefix; } } return '' === $best ? null : $this->known_plugins[ $best ]; } /** * Classifies a must-use plugin. The do-tools clone helpers belong to the clone only. * * @param string $file Must-use plugin file name. */ public function mu_plugin( string $file ): string { $class = ( 0 === strpos( $file, 'dt-' ) || 'docker-wp-cron-loopback.php' === $file ) ? self::ENVIRONMENT : self::CODE; return (string) apply_filters( 'wonderful_sync_classify', $class, $file, 'mu_plugin' ); } /** * Classifies a taxonomy: menus are layout, public taxonomies content, everything else * (internal taxonomies of plugins, product visibility flags …) is never synced. * * @param string $taxonomy Taxonomy name. */ public function taxonomy( string $taxonomy ): string { if ( 'nav_menu' === $taxonomy ) { $class = self::LAYOUT; } elseif ( in_array( $taxonomy, array( 'category', 'post_tag', 'post_format', 'wp_pattern_category', 'layout_category', 'layout_tag', 'layout_pack', 'layout_type', 'scope', 'module_width' ), true ) ) { $class = self::CONTENT; } else { $object = get_taxonomy( $taxonomy ); $class = ( $object && $object->public ) ? self::CONTENT : self::UNASSIGNED; } return (string) apply_filters( 'wonderful_sync_classify', $class, $taxonomy, 'taxonomy' ); } /** * Whether a nested array key looks like a secret (API key, licence, password …). * * @param string $key Array key. */ public static function is_secret_key( string $key ): bool { return (bool) preg_match( self::SECRET_PATTERN, $key ); } /** * Installation keys of an option (see ENVIRONMENT_KEYS). * * @param string $option Option name. * @return string[] */ public static function environment_keys( string $option, array $values = array() ): array { $keys = self::ENVIRONMENT_KEYS[ $option ] ?? array(); foreach ( self::ENVIRONMENT_PREFIXES[ $option ] ?? array() as $prefix ) { foreach ( $values as $value ) { foreach ( is_array( $value ) ? array_keys( $value ) : array() as $key ) { if ( is_string( $key ) && 0 === strpos( $key, $prefix ) ) { $keys[] = $key; } } } } return array_values( array_unique( $keys ) ); } /** * Keys whose masked value means "never sent": secret-looking keys and the installation * keys above. Used where a mask is ignored for comparisons. * * @param string $key Array key. */ public static function is_masked_key( string $key ): bool { if ( self::is_secret_key( $key ) ) { return true; } foreach ( self::ENVIRONMENT_KEYS as $keys ) { if ( in_array( $key, $keys, true ) ) { return true; } } foreach ( self::ENVIRONMENT_PREFIXES as $prefixes ) { foreach ( $prefixes as $prefix ) { if ( 0 === strpos( $key, $prefix ) ) { return true; } } } return false; } /** * Whether objects of this class may be part of a changeset. * * @param string $class Sync class. */ public static function is_syncable( string $class ): bool { return in_array( $class, self::SYNCABLE, true ); } /** * Human readable labels, in display order. * * @return array */ public static function labels(): array { return array( self::CODE => __( 'Code (themes, plugins)', 'wonderful-sync' ), self::DESIGN => __( 'Design (Divi options, presets, global colors)', 'wonderful-sync' ), self::LAYOUT => __( 'Layouts (Theme Builder, Divi Library, menus)', 'wonderful-sync' ), self::CONTENT => __( 'Content (pages, posts, media)', 'wonderful-sync' ), self::SITE => __( 'Site settings', 'wonderful-sync' ), self::ENVIRONMENT => __( 'Environment (never transferred)', 'wonderful-sync' ), self::USER_DATA => __( 'User data (never transferred)', 'wonderful-sync' ), self::CACHE => __( 'Caches (cleared, never transferred)', 'wonderful-sync' ), self::UNASSIGNED => __( 'Unassigned (never transferred automatically)', 'wonderful-sync' ), ); } /** * Returns the class of the longest matching prefix, or UNASSIGNED. * * @param string $name Name to test. * @param array $rules Prefix => class. */ private function match_prefix( string $name, array $rules ): string { $best = ''; foreach ( array_keys( $rules ) as $prefix ) { if ( 0 === strpos( $name, $prefix ) && strlen( $prefix ) > strlen( $best ) ) { $best = $prefix; } } return '' === $best ? self::UNASSIGNED : $rules[ $best ]; } }