0, 'delimiter' => ';', 'tables' => [], 'options' => [], 'databases' => [], 'statements' => 0, 'done' => false]; } /** SiteRelay's export header (first lines of the file): site and home URL, path, table prefix. */ public static function header(string $path): array { $h = Fs::open($path, 'rb'); $head = $h !== false ? (string) Fs::read($h, 8192) : ''; if ($h !== false) { Fs::close($h); } if (strpos($head, '-- SiteRelay SQL export') !== 0 && strpos($head, '-- Relay SQL export') !== 0) { // the second: before the plugin was called SiteRelay return []; } $out = []; $labels = [ 'Site URL' => 'site_url', 'Home URL' => 'home_url', 'Path' => 'path', 'Table prefix' => 'prefix', 'Uploads URL' => 'uploads_url', 'Uploads path' => 'uploads_dir', 'Content URL' => 'content_url', 'Main site uploads URL' => 'alt_uploads_url', 'Main site content URL' => 'alt_content_url', ]; foreach ($labels as $label => $key) { if (preg_match('/^-- ' . $label . ': (.+)$/m', $head, $m)) { $out[$key] = trim($m[1]); } } return $out; } public static function run(string $path, array $cursor, Budget $budget): array { $reader = new SqlReader($path, (int) $cursor['offset'], (string) $cursor['delimiter']); while (!$budget->exhausted()) { $s = $reader->next(); if ($s === null) { $cursor['done'] = true; break; } $sql = $s['sql']; if (preg_match('/^CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?`((?:[^`]|``)+)`/i', $sql, $m)) { $t = str_replace('``', '`', $m[1]); if (!in_array($t, $cursor['tables'], true)) { $cursor['tables'][] = $t; } } elseif (preg_match('/^USE\s+(?:`((?:[^`]|``)+)`|(\S+))\s*$/i', $sql, $m)) { $db = $m[1] !== '' ? str_replace('``', '`', $m[1]) : $m[2]; if (!in_array($db, $cursor['databases'] ?? [], true)) { $cursor['databases'][] = $db; } } elseif (preg_match('/^(?:INSERT|REPLACE)\s+(?:IGNORE\s+)?INTO\s+`((?:[^`]|``)+options)`/i', $sql, $m)) { $t = str_replace('``', '`', $m[1]); foreach (['siteurl', 'home'] as $name) { // Only an address: with an unusual column order the value after the name is something else. if (!isset($cursor['options'][$t][$name]) && preg_match("/'" . $name . "'\\s*,\\s*'(https?:(?:[^'\\\\]|\\\\.|'')*)'/i", $sql, $v)) { $cursor['options'][$t][$name] = self::unescape($v[1]); } } } $cursor['statements']++; $cursor['offset'] = $s['end']; $cursor['delimiter'] = $reader->delimiter(); $budget->consume(strlen($sql)); } return $cursor; } /** * The WordPress site in the dump. Throws when there is none, when it is a multisite network, or when the dump * holds several databases (mysqldump --all-databases): their tables would land on top of each other. * * @return array{prefix: string, site_url: string, home_url: string, path: string, tables: string[]} */ public static function site(array $cursor, array $header): array { $databases = $cursor['databases'] ?? []; if (count($databases) > 1) { throw new RelayError( 'import.several_databases', /* translators: %s: database names */ esc_html(sprintf(__('This SQL file holds several databases (%s).', 'siterelay'), implode(', ', array_slice($databases, 0, 5)))), esc_html__('Export only the WordPress site\'s database and import that file.', 'siterelay') ); } $tables = $cursor['tables']; $candidates = []; foreach ($tables as $t) { if (substr($t, -7) === 'options') { $p = substr($t, 0, -7); if (in_array($p . 'posts', $tables, true)) { $candidates[] = $p; } } } $prefix = null; if (isset($header['prefix']) && in_array($header['prefix'], $candidates, true)) { $prefix = $header['prefix']; } elseif ($candidates !== []) { // The site whose URL is known first, then the shortest prefix (wp_ before wp_2_). usort($candidates, static function ($a, $b) use ($cursor) { $ka = isset($cursor['options'][$a . 'options']) ? 0 : 1; $kb = isset($cursor['options'][$b . 'options']) ? 0 : 1; return $ka <=> $kb ?: strlen($a) <=> strlen($b) ?: strcmp($a, $b); }); $prefix = $candidates[0]; } if ($prefix === null) { throw new RelayError('import.no_wordpress', esc_html__('This SQL file has no WordPress tables (an options and a posts table with the same prefix).', 'siterelay'), esc_html__('Choose a database export of a WordPress site.', 'siterelay')); } if (in_array($prefix . 'blogs', $tables, true) || in_array($prefix . 'site', $tables, true)) { throw new RelayError('import.multisite', esc_html__('This SQL file holds a whole multisite network, which SiteRelay can\'t import.', 'siterelay'), esc_html__('Export the one subsite you need from that network, and import that file.', 'siterelay')); } $options = $cursor['options'][$prefix . 'options'] ?? []; $home = (string) ($header['home_url'] ?? ($options['home'] ?? ($options['siteurl'] ?? ''))); $extra = array_intersect_key($header, array_flip(['uploads_url', 'uploads_dir', 'content_url', 'alt_uploads_url', 'alt_content_url'])); return [ 'prefix' => $prefix, 'site_url' => (string) ($header['site_url'] ?? ($options['siteurl'] ?? $home)), 'home_url' => $home, 'path' => (string) ($header['path'] ?? ''), 'tables' => array_values(array_filter($tables, static fn ($t) => strpos($t, $prefix) === 0)), ] + $extra; } /** MySQL string escapes in a quoted value. */ private static function unescape(string $v): string { return strtr(str_replace("''", "'", $v), ['\\\\' => '\\', "\\'" => "'", '\\"' => '"', '\\n' => "\n", '\\r' => "\r", '\\t' => "\t", '\\0' => "\0", '\\/' => '/']); } }