to */ private $map = array(); /** @var string */ private $regex = ''; /** @var array */ private $quick = array(); /** * @param array $pairs [ [from, to], ... ] or [from => to]. * @param bool $boundary Do not match inside longer host names / paths (URL mode). False = any substring. */ public function __construct( $pairs, $boundary = true ) { foreach ( $pairs as $k => $v ) { if ( is_array( $v ) ) { $from = (string) $v[0]; $to = (string) $v[1]; } else { $from = (string) $k; $to = (string) $v; } if ( '' === $from || $from === $to ) { continue; } if ( ! isset( $this->map[ $from ] ) ) { $this->map[ $from ] = $to; } } $keys = array_keys( $this->map ); usort( $keys, function ( $a, $b ) { return strlen( $b ) - strlen( $a ); } ); $parts = array(); foreach ( $keys as $k ) { $parts[] = preg_quote( $k, '~' ); } if ( $parts ) { // Do not match when the needle continues as a longer host name / path segment. $this->regex = '~(?:' . implode( '|', $parts ) . ')' . ( $boundary ? '(?![A-Za-z0-9_\-]|\.[A-Za-z0-9])' : '' ) . '~'; } $this->quick = $this->quick_needles( $keys ); } /** * Minimal set of substrings — if none occurs in a value, nothing can match. * * @param array $keys Needles. * @return array */ private function quick_needles( $keys ) { $out = array(); foreach ( $keys as $k ) { $covered = false; foreach ( $keys as $o ) { if ( $o !== $k && strlen( $o ) < strlen( $k ) && false !== strpos( $k, $o ) ) { $covered = true; break; } } if ( ! $covered ) { $out[ $k ] = true; } } return array_keys( $out ); } /** * Needles for SQL pre-filtering (LIKE '%needle%'). * * @return array */ public function needles() { return $this->quick; } /** @return bool */ public function is_empty() { return empty( $this->map ); } /** * Does a string possibly contain a needle? * * @param string $s String. * @return bool */ private function maybe( $s ) { foreach ( $this->quick as $n ) { if ( false !== strpos( $s, $n ) ) { return true; } } return false; } /** * Replace in a plain string. * * @param string $s String. * @return string */ private function plain( $s ) { $map = $this->map; $r = preg_replace_callback( $this->regex, function ( $m ) use ( $map ) { return isset( $map[ $m[0] ] ) ? $map[ $m[0] ] : $m[0]; }, $s ); return null === $r ? $s : $r; } /** * Replace inside any value (plain, serialized, nested serialized, JSON). * * @param string $value Value. * @param int $depth Recursion depth. * @return string */ public function replace( $value, $depth = 0 ) { if ( ! is_string( $value ) || '' === $value || ! $this->regex || ! $this->maybe( $value ) ) { return $value; } if ( $depth < 8 && self::looks_serialized( $value ) ) { $pos = 0; $out = $this->walk( $value, $pos, $depth ); if ( null !== $out && strlen( $value ) === $pos ) { return $out; } } return $this->plain( $value ); } /** * Quick structural check. * * @param string $s String. * @return bool */ public static function looks_serialized( $s ) { $s = trim( $s ); if ( strlen( $s ) < 4 ) { return 'N;' === $s; } return (bool) preg_match( '/^(?:[aOC]:\d+:|s:\d+:"|[bid]:[^;]*;$|N;$|E:\d+:")/', $s ) && in_array( substr( $s, -1 ), array( ';', '}' ), true ); } /** * Walk one serialized value starting at $pos, returning its rewritten form. * * @param string $s Serialized data. * @param int $pos Position (in/out). * @param int $depth Depth. * @return string|null Null on parse error. */ private function walk( $s, &$pos, $depth ) { $len = strlen( $s ); if ( $pos >= $len ) { return null; } $t = $s[ $pos ]; switch ( $t ) { case 'N': if ( ';' !== substr( $s, $pos + 1, 1 ) ) { return null; } $pos += 2; return 'N;'; case 'b': case 'i': case 'd': case 'r': case 'R': $end = strpos( $s, ';', $pos ); if ( false === $end || ':' !== substr( $s, $pos + 1, 1 ) ) { return null; } $tok = substr( $s, $pos, $end - $pos + 1 ); $pos = $end + 1; return $tok; case 's': case 'E': if ( ! preg_match( '/\G[sE]:(\d+):"/', $s, $m, 0, $pos ) ) { return null; } $n = (int) $m[1]; $start = $pos + strlen( $m[0] ); if ( $start + $n + 2 > $len || '";' !== substr( $s, $start + $n, 2 ) ) { return null; } $str = substr( $s, $start, $n ); $pos = $start + $n + 2; if ( 'E' === $t ) { return 'E:' . $n . ':"' . $str . '";'; } $new = $this->replace( $str, $depth + 1 ); return 's:' . strlen( $new ) . ':"' . $new . '";'; case 'a': if ( ! preg_match( '/\Ga:(\d+):\{/', $s, $m, 0, $pos ) ) { return null; } $count = (int) $m[1]; $pos += strlen( $m[0] ); $out = $m[0]; for ( $i = 0; $i < $count * 2; $i++ ) { $part = $this->walk( $s, $pos, $depth ); if ( null === $part ) { return null; } $out .= $part; } if ( '}' !== substr( $s, $pos, 1 ) ) { return null; } $pos++; return $out . '}'; case 'O': if ( ! preg_match( '/\GO:(\d+):"/', $s, $m, 0, $pos ) ) { return null; } $cl = (int) $m[1]; $start = $pos + strlen( $m[0] ); $class = substr( $s, $start, $cl ); $p2 = $start + $cl; if ( ! preg_match( '/\G":(\d+):\{/', $s, $m2, 0, $p2 ) ) { return null; } $count = (int) $m2[1]; $pos = $p2 + strlen( $m2[0] ); $out = 'O:' . $cl . ':"' . $class . $m2[0]; for ( $i = 0; $i < $count * 2; $i++ ) { $part = $this->walk( $s, $pos, $depth ); if ( null === $part ) { return null; } $out .= $part; } if ( '}' !== substr( $s, $pos, 1 ) ) { return null; } $pos++; return $out . '}'; case 'C': if ( ! preg_match( '/\GC:(\d+):"/', $s, $m, 0, $pos ) ) { return null; } $cl = (int) $m[1]; $start = $pos + strlen( $m[0] ); $class = substr( $s, $start, $cl ); $p2 = $start + $cl; if ( ! preg_match( '/\G":(\d+):\{/', $s, $m2, 0, $p2 ) ) { return null; } $dl = (int) $m2[1]; $ds = $p2 + strlen( $m2[0] ); $data = substr( $s, $ds, $dl ); if ( '}' !== substr( $s, $ds + $dl, 1 ) ) { return null; } $pos = $ds + $dl + 1; $new = $this->replace( $data, $depth + 1 ); return 'C:' . $cl . ':"' . $class . '":' . strlen( $new ) . ':{' . $new . '}'; } return null; } /** * Build replacement pairs for a site move. * * @param array $old [home, siteurl, abspath, content_dir, uploads_url?] * @param array $new Same keys for the destination. * @return array [from => to] */ public static function migration_pairs( $old, $new ) { $pairs = array(); foreach ( array( 'home', 'siteurl' ) as $k ) { if ( empty( $old[ $k ] ) || empty( $new[ $k ] ) ) { continue; } $from = untrailingslashit( $old[ $k ] ); $to = untrailingslashit( $new[ $k ] ); if ( $from === $to ) { continue; } foreach ( self::url_variants( $from, $to ) as $f => $t ) { if ( ! isset( $pairs[ $f ] ) ) { $pairs[ $f ] = $t; } } } foreach ( array( 'abspath', 'content_dir' ) as $k ) { if ( empty( $old[ $k ] ) || empty( $new[ $k ] ) ) { continue; } $from = untrailingslashit( str_replace( '\\', '/', $old[ $k ] ) ); $to = untrailingslashit( str_replace( '\\', '/', $new[ $k ] ) ); if ( $from === $to || strlen( $from ) < 4 ) { continue; } if ( ! isset( $pairs[ $from ] ) ) { $pairs[ $from ] = $to; $pairs[ str_replace( '/', '\\/', $from ) ] = str_replace( '/', '\\/', $to ); } } return $pairs; } /** * All the ways a site URL is stored in a database. * * @param string $from Old URL (no trailing slash). * @param string $to New URL. * @return array */ public static function url_variants( $from, $to ) { $out = array(); $fp = wp_parse_url( $from ); $tp = wp_parse_url( $to ); if ( empty( $fp['host'] ) || empty( $tp['host'] ) ) { $out[ $from ] = $to; return $out; } $fhost = $fp['host'] . ( isset( $fp['port'] ) ? ':' . $fp['port'] : '' ); $thost = $tp['host'] . ( isset( $tp['port'] ) ? ':' . $tp['port'] : '' ); $fpath = isset( $fp['path'] ) ? rtrim( $fp['path'], '/' ) : ''; $tpath = isset( $tp['path'] ) ? rtrim( $tp['path'], '/' ) : ''; $scheme = isset( $tp['scheme'] ) ? $tp['scheme'] : 'https'; $hosts = array( $fhost ); if ( 0 === strpos( $fhost, 'www.' ) ) { $hosts[] = substr( $fhost, 4 ); } elseif ( ! preg_match( '/^(\d+\.){3}\d+/', $fhost ) && false === strpos( $fhost, 'localhost' ) ) { $hosts[] = 'www.' . $fhost; } $target = $scheme . '://' . $thost . $tpath; foreach ( $hosts as $h ) { foreach ( array( 'https://', 'http://' ) as $s ) { $f = $s . $h . $fpath; $out[ $f ] = $target; // JSON-escaped. $out[ str_replace( '/', '\\/', $f ) ] = str_replace( '/', '\\/', $target ); // URL-encoded. $out[ rawurlencode( $f ) ] = rawurlencode( $target ); } // Protocol-relative. $out[ '//' . $h . $fpath ] = '//' . $thost . $tpath; $out[ '\\/\\/' . $h . str_replace( '/', '\\/', $fpath ) ] = '\\/\\/' . $thost . str_replace( '/', '\\/', $tpath ); } return $out; } }