to, applied in a single pass with strtr() so a replacement * is never replaced again (safe when the new URL contains the old one). * * @var array */ private $map = array(); /** * Constructor. * * @param array $pairs from => to. */ public function __construct( array $pairs ) { uksort( $pairs, static function ( $a, $b ) { return strlen( $b ) - strlen( $a ); } ); foreach ( $pairs as $from => $to ) { $from = (string) $from; if ( '' !== $from && $from !== (string) $to ) { $this->from[] = $from; $this->map[ $from ] = (string) $to; } } } /** * Whether there is anything to replace at all. * * @return bool */ public function is_empty() { return empty( $this->from ); } /** * Whether a value contains any search string. * * @param string $value Value. * @return bool */ public function contains( $value ) { foreach ( $this->from as $needle ) { if ( false !== strpos( $value, $needle ) ) { return true; } } return false; } /** * Replaces in a value, keeping serialized lengths valid. * * @param mixed $value Value. * @return mixed */ public function run( $value ) { if ( ! is_string( $value ) || '' === $value || ! $this->contains( $value ) ) { return $value; } if ( is_serialized( $value, false ) ) { try { $i = 0; $out = $this->parse( $value, $i, 0 ); if ( strlen( $value ) === $i ) { return $out; } } catch ( Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch // Broken serialized data: leave it exactly as it was. } return $value; } return strtr( $value, $this->map ); } /** * Parses and rebuilds one serialized value starting at $i. * * @param string $s Serialized string. * @param int $i Position, advanced by reference. * @param int $depth Nesting depth. * @return string * @throws UnexpectedValueException On malformed data. */ private function parse( $s, &$i, $depth ) { if ( $depth > 512 || ! isset( $s[ $i ] ) ) { throw new UnexpectedValueException( 'depth' ); } $type = $s[ $i ]; switch ( $type ) { case 'N': $this->expect( $s, $i + 1, ';' ); $i += 2; return 'N;'; case 'b': case 'i': case 'd': case 'r': case 'R': $this->expect( $s, $i + 1, ':' ); $end = strpos( $s, ';', $i ); if ( false === $end ) { throw new UnexpectedValueException( 'scalar' ); } $token = substr( $s, $i, $end - $i + 1 ); $i = $end + 1; return $token; case 's': $len = $this->read_int( $s, $i + 2, $colon ); $start = $colon + 2; $this->expect( $s, $colon + 1, '"' ); $this->expect( $s, $start + $len, '"' ); $this->expect( $s, $start + $len + 1, ';' ); $str = substr( $s, $start, $len ); $i = $start + $len + 2; $new = $this->run( $str ); return 's:' . strlen( $new ) . ':"' . $new . '";'; case 'E': $len = $this->read_int( $s, $i + 2, $colon ); $end = $colon + 2 + $len + 2; $this->expect( $s, $end - 1, ';' ); $token = substr( $s, $i, $end - $i ); $i = $end; return $token; case 'a': $count = $this->read_int( $s, $i + 2, $colon ); $this->expect( $s, $colon + 1, '{' ); $i = $colon + 2; $out = 'a:' . $count . ':{'; for ( $n = 0; $n < $count * 2; $n++ ) { $out .= $this->parse( $s, $i, $depth + 1 ); } $this->expect( $s, $i, '}' ); ++$i; return $out . '}'; case 'O': $clen = $this->read_int( $s, $i + 2, $colon ); $this->expect( $s, $colon + 1, '"' ); $class = substr( $s, $colon + 2, $clen ); $p = $colon + 2 + $clen; $this->expect( $s, $p, '"' ); $this->expect( $s, $p + 1, ':' ); $count = $this->read_int( $s, $p + 2, $colon2 ); $this->expect( $s, $colon2 + 1, '{' ); $i = $colon2 + 2; $out = 'O:' . $clen . ':"' . $class . '":' . $count . ':{'; for ( $n = 0; $n < $count * 2; $n++ ) { $out .= $this->parse( $s, $i, $depth + 1 ); } $this->expect( $s, $i, '}' ); ++$i; return $out . '}'; case 'C': $clen = $this->read_int( $s, $i + 2, $colon ); $p = $colon + 2 + $clen; $this->expect( $s, $p, '"' ); $dlen = $this->read_int( $s, $p + 2, $colon2 ); $this->expect( $s, $colon2 + 1, '{' ); $end = $colon2 + 2 + $dlen; $this->expect( $s, $end, '}' ); $token = substr( $s, $i, $end - $i + 1 ); $i = $end + 1; return $token; } throw new UnexpectedValueException( 'type' ); } /** * Reads a non-negative integer that ends with a colon. * * @param string $s String. * @param int $from Start. * @param int $colon Position of the ending colon (out). * @return int * @throws UnexpectedValueException On malformed data. */ private function read_int( $s, $from, &$colon ) { $colon = strpos( $s, ':', $from ); if ( false === $colon ) { throw new UnexpectedValueException( 'int' ); } $num = substr( $s, $from, $colon - $from ); if ( '' === $num || ! ctype_digit( $num ) ) { throw new UnexpectedValueException( 'int' ); } return (int) $num; } /** * Asserts a character at a position. * * @param string $s String. * @param int $pos Position. * @param string $char Expected character. * @return void * @throws UnexpectedValueException On mismatch. */ private function expect( $s, $pos, $char ) { if ( ! isset( $s[ $pos ] ) || $s[ $pos ] !== $char ) { throw new UnexpectedValueException( 'expect' ); } } /** * Builds replacement pairs for a move between two sites. * * @param array $old home, siteurl, abspath, content_dir, uploads_dir, uploads_url. * @param array $new Same keys for this site. * @return array from => to. */ public static function pairs_for_move( array $old, array $new ) { $pairs = array(); foreach ( array( 'uploads_url', 'siteurl', 'home' ) as $key ) { if ( empty( $old[ $key ] ) || empty( $new[ $key ] ) ) { continue; } $o = untrailingslashit( $old[ $key ] ); $n = untrailingslashit( $new[ $key ] ); if ( $o === $n ) { continue; } $o_bare = preg_replace( '#^https?:#i', '', $o ); $n_bare = preg_replace( '#^https?:#i', '', $n ); $n_full = $n; foreach ( array( 'http:', 'https:' ) as $scheme ) { $pairs[ $scheme . $o_bare ] = $n_full; $pairs[ str_replace( '/', '\\/', $scheme . $o_bare ) ] = str_replace( '/', '\\/', $n_full ); $pairs[ rawurlencode( $scheme . $o_bare ) ] = rawurlencode( $n_full ); } $pairs[ $o_bare ] = $n_bare; $pairs[ str_replace( '/', '\\/', $o_bare ) ] = str_replace( '/', '\\/', $n_bare ); } foreach ( array( 'uploads_dir', 'content_dir', 'abspath' ) as $key ) { if ( empty( $old[ $key ] ) || empty( $new[ $key ] ) ) { continue; } $o = untrailingslashit( $old[ $key ] ); $n = untrailingslashit( $new[ $key ] ); if ( $o !== $n && strlen( $o ) > 3 ) { $pairs[ $o ] = $n; $pairs[ str_replace( '/', '\\/', $o ) ] = str_replace( '/', '\\/', $n ); } } /** * Filters the search/replace pairs used during a restore. * * @param array $pairs from => to. * @param array $old Source site details. * @param array $new This site's details. */ return apply_filters( 'shiftbox_replace_pairs', $pairs, $old, $new ); } }