db = $db; $this->head = 'INSERT INTO ' . Connection::ident( $source ) . ' ('; $this->temp_head = 'INSERT INTO ' . Connection::ident( $temp ) . ' ('; $this->replacer = ( $replacer && $replacer->active() ) ? $replacer : null; $this->skip = $skip; } /** * Transforms one statement. * * @param string $stmt Statement (without trailing semicolon). * @throws \RuntimeException For statements that are not INSERTs into the expected table. */ public function transform( $stmt ) { if ( 0 !== strpos( $stmt, $this->head ) ) { throw new \RuntimeException( 'Unexpected statement in backup: ' . substr( $stmt, 0, 60 ) ); } $stmt = $this->temp_head . substr( $stmt, strlen( $this->head ) ); if ( null === $this->replacer || ! $this->replacer->may_match( $stmt ) ) { return $stmt; } // Column list, for skipped columns. $list_start = strlen( $this->temp_head ); $list_end = strpos( $stmt, ') VALUES ', $list_start ); if ( false === $list_end ) { throw new \RuntimeException( 'Malformed INSERT in backup.' ); } $skip_idx = array(); if ( $this->skip ) { $names = explode( ',', substr( $stmt, $list_start, $list_end - $list_start ) ); foreach ( $names as $i => $name ) { if ( in_array( trim( $name, '` ' ), $this->skip, true ) ) { $skip_idx[ $i ] = true; } } } $len = strlen( $stmt ); $p = $list_end + 9; $last = 0; $out = array(); $depth = 0; $col = 0; while ( $p < $len ) { $p += strcspn( $stmt, "'(),", $p ); if ( $p >= $len ) { break; } $c = $stmt[ $p ]; if ( '(' === $c ) { ++$depth; if ( 1 === $depth ) { $col = 0; } ++$p; } elseif ( ')' === $c ) { --$depth; ++$p; } elseif ( ',' === $c ) { if ( 1 === $depth ) { ++$col; } ++$p; } else { $end = self::string_end( $stmt, $p ); if ( ! isset( $skip_idx[ $col ] ) ) { $raw = substr( $stmt, $p + 1, $end - $p - 1 ); if ( $this->replacer->may_match( $raw ) ) { $value = self::unescape( $raw ); $new = $this->replacer->replace( $value ); if ( $new !== $value ) { $out[] = substr( $stmt, $last, $p - $last ); $out[] = Connection::literal( $new ); $last = $end + 1; } } } $p = $end + 1; } } if ( ! $out ) { return $stmt; } $out[] = substr( $stmt, $last ); return implode( '', $out ); } /** * Index of the quote closing the string literal that starts at $p. * * @param string $s SQL. * @param int $p Position of the opening quote. * @throws \RuntimeException On unterminated strings. */ private static function string_end( $s, $p ) { $len = strlen( $s ); $i = $p + 1; while ( $i < $len ) { $i += strcspn( $s, "\\'", $i ); if ( $i >= $len ) { break; } if ( '\\' === $s[ $i ] ) { $i += 2; continue; } if ( "'" === ( $s[ $i + 1 ] ?? '' ) ) { $i += 2; // Doubled quote. continue; } return $i; } throw new \RuntimeException( 'Unterminated string in backup SQL.' ); } /** * Decodes a MySQL string literal body. * * @param string $raw Literal contents without quotes. */ public static function unescape( $raw ) { if ( false === strpos( $raw, '\\' ) && false === strpos( $raw, "''" ) ) { return $raw; } return strtr( $raw, array( '\\0' => "\0", '\\n' => "\n", '\\r' => "\r", '\\t' => "\t", '\\b' => "\x08", '\\Z' => "\x1a", '\\\\' => '\\', "\\'" => "'", '\\"' => '"', "''" => "'", ) ); } }