This commit is contained in:
Javier Casares 2026-03-02 11:22:59 +00:00
commit 5cdf7f48e5
29 changed files with 6526 additions and 0 deletions

View file

@ -0,0 +1,282 @@
<?php
/**
* MultilingualPress 2 detection service.
*
* Detects whether MultilingualPress 2 is installed (active or inactive),
* reads its plugin metadata, and identifies its database tables and schema.
*
* Note: MLP2 does not need to be active for migration. Only its database
* tables need to exist. See docs/ROADMAP.md — Background section.
*
* @package Robotstxt_Mlp2_Mlp5
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class Robotstxt_Mlp2_Mlp5_Detector_Mlp2
*
* Instantiate once to detect MLP2; all detection results are cached in
* instance properties to avoid repeated file or database reads.
*
* Usage:
* $mlp2 = new Robotstxt_Mlp2_Mlp5_Detector_Mlp2();
* if ( $mlp2->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 );
}
}