get_site_relations_table() !== '' ) { ... } */ class Robotstxt_Mlp2_Mlp5_Detector_Mlp2 { /** * Known plugin basename for MultilingualPress 2 (WordPress.org slug). * * @var string */ const PLUGIN_BASENAME = 'multilingual-press/multilingual-press.php'; /** * Resolved MLP2 plugin basename, or empty string if not found on disk. * * @var string */ private string $plugin_file; /** * Plugin header data from get_plugin_data(), or empty array if unavailable. * * @var array */ private array $plugin_data; /** * Constructor. Locates the MLP2 plugin file and loads its header data. */ public function __construct() { $this->plugin_file = $this->find_plugin_file(); $this->plugin_data = $this->plugin_file ? $this->load_plugin_data() : array(); } /** * Whether MultilingualPress 2 is currently network-active. * * Informational only. MLP2 may be deactivated or uninstalled while its * database tables still exist. Table presence is the migration gate, not * plugin activity. * * @return bool */ public function is_active(): bool { if ( ! function_exists( 'is_plugin_active_for_network' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } return '' !== $this->plugin_file && is_plugin_active_for_network( $this->plugin_file ); } /** * Return the MLP2 version string, or empty string if unavailable. * * @return string */ public function get_version(): string { return isset( $this->plugin_data['Version'] ) ? (string) $this->plugin_data['Version'] : ''; } /** * Return the resolved MLP2 plugin basename, or empty string if not on disk. * * @return string */ public function get_plugin_file(): string { return $this->plugin_file; } /** * Check whether a given table exists in the database. * * @param string $table Full table name (including prefix). * @return bool */ public function table_exists( string $table ): bool { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching return (string) $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ) === $table; } /** * Return the full MLP2 site relations table name, or empty string if absent. * * Checks the original name first. If the original has been renamed by the * Table_Preparer, returns the backup name instead so that later phases can * read the data regardless of whether preparation has run. * * @return string */ public function get_site_relations_table(): string { global $wpdb; $table = $wpdb->base_prefix . 'mlp_site_relations'; $backup = Robotstxt_Mlp2_Mlp5_Table_Preparer::backup_name( $table ); // Check backup first: after preparation the original name belongs to MLP5. if ( '' !== $backup && $this->table_exists( $backup ) ) { return $backup; } // Fall back to original only when no backup tables exist (preparation not done yet). if ( ! Robotstxt_Mlp2_Mlp5_Table_Preparer::has_backup_tables() && $this->table_exists( $table ) ) { return $table; } return ''; } /** * Return the full MLP2 content relations table name, or empty string if absent. * * MLP2 uses `mlp_relationships` in older minor versions, * `mlp_content_relations` in newer ones, and `multilingual_linked` in some * installations (table name lacks the mlp_ prefix and is never renamed by * the Table_Preparer). All three are checked. If the original table has been * renamed by the Table_Preparer, its backup name is returned. * * @return string */ public function get_content_relations_table(): string { global $wpdb; // After preparation, original names belong to MLP5; only look at backups. $preparation_done = Robotstxt_Mlp2_Mlp5_Table_Preparer::has_backup_tables(); foreach ( array( 'mlp_relationships', 'mlp_content_relations' ) as $candidate ) { $table = $wpdb->base_prefix . $candidate; $backup = Robotstxt_Mlp2_Mlp5_Table_Preparer::backup_name( $table ); if ( '' !== $backup && $this->table_exists( $backup ) ) { return $backup; } // Fall back to original only when preparation has not run yet. if ( ! $preparation_done && $this->table_exists( $table ) ) { return $table; } } // Some MLP2 installations store content relations in `multilingual_linked` // (no mlp_ prefix). This table is never touched by Table_Preparer, so it // remains accessible regardless of whether preparation has run. $linked = $wpdb->base_prefix . 'multilingual_linked'; if ( $this->table_exists( $linked ) ) { return $linked; } return ''; } /** * Return all tables in the database that match the MLP table prefix pattern. * * This may include tables belonging to MLP5 if it uses the same prefix. * Use get_site_relations_table() and get_content_relations_table() for * precise MLP2-specific detection. * * @return string[] Array of full table names. */ public function get_all_tables(): array { global $wpdb; $like = $wpdb->esc_like( $wpdb->base_prefix . 'mlp_' ) . '%'; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $tables = $wpdb->get_col( $wpdb->prepare( 'SHOW TABLES LIKE %s', $like ) ); return is_array( $tables ) ? $tables : array(); } /** * Return column definitions for a table. * * @param string $table Full table name (must start with the DB base prefix). * @return array Associative array keyed by column name (Field), or empty array * if the table does not exist or the name fails validation. */ public function describe_table( string $table ): array { global $wpdb; // Validate: table name must start with the base prefix. if ( ! str_starts_with( $table, $wpdb->base_prefix ) ) { return array(); } if ( ! $this->table_exists( $table ) ) { return array(); } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $columns = $wpdb->get_results( "SHOW COLUMNS FROM {$table}", ARRAY_A ); if ( empty( $columns ) || ! is_array( $columns ) ) { return array(); } $result = array(); foreach ( $columns as $column ) { if ( isset( $column['Field'] ) ) { $result[ $column['Field'] ] = $column; } } return $result; } /** * Locate the MLP2 plugin file on disk. * * Checks the known canonical basename first. If not found, scans all * installed plugins for a plugin named "MultilingualPress" with a * version below 3.0.0 (which marks the MLP3+ series). * * @return string Plugin basename, or empty string if not found. */ private function find_plugin_file(): string { if ( file_exists( WP_PLUGIN_DIR . '/' . self::PLUGIN_BASENAME ) ) { return self::PLUGIN_BASENAME; } if ( ! function_exists( 'get_plugins' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } foreach ( get_plugins() as $basename => $data ) { if ( isset( $data['Name'], $data['Version'] ) && 'MultilingualPress' === $data['Name'] && version_compare( (string) $data['Version'], '3.0.0', '<' ) ) { return $basename; } } return ''; } /** * Load plugin header data from the MLP2 main file. * * @return array Plugin data from get_plugin_data(), or empty array on failure. */ private function load_plugin_data(): array { if ( ! function_exists( 'get_plugin_data' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } $path = WP_PLUGIN_DIR . '/' . $this->plugin_file; if ( ! file_exists( $path ) ) { return array(); } return get_plugin_data( $path, false, false ); } }