volumes = self::volumes( $base ); $this->open_manifest(); } /** * Every volume for a base path, in order. * * @param string $base Base path. * @return string[] */ public static function volumes( $base ) { $vols = array( $base ); for ( $i = 2; ; $i++ ) { $p = $base . '.' . $i; if ( ! is_file( $p ) ) { break; } $vols[] = $p; } return $vols; } /** * Read + validate the manifest (always the first, unencrypted, entry). * * @return void * @throws RuntimeException On failure. */ private function open_manifest() { $this->rewind(); $h = $this->next_header(); if ( ! $h || ( $h['header']['type'] ?? '' ) !== 'manifest' ) { throw new RuntimeException( 'This file is not an AI-SiteArk backup (no manifest).' ); } $this->manifest = json_decode( $this->read( $h['len'] ), true ); if ( ! is_array( $this->manifest ) ) { throw new RuntimeException( 'The backup manifest could not be read.' ); } } /** @return bool */ public function is_encrypted() { return ! empty( $this->manifest['encrypted'] ); } /** * Verify a password and arm decryption. Returns true on success. * * @param string $password Password. * @return bool */ public function unlock( $password ) { if ( ! $this->is_encrypted() ) { return true; } if ( ! function_exists( 'openssl_decrypt' ) || '' === (string) $password ) { return false; } $key = hash_pbkdf2( 'sha256', (string) $password, base64_decode( $this->manifest['salt'] ?? '' ), AISV_ITER, 32, true ); $ok = openssl_decrypt( base64_decode( $this->manifest['verify'] ?? '' ), 'aes-256-ctr', $key, OPENSSL_RAW_DATA, base64_decode( $this->manifest['verify_iv'] ?? '' ) ); if ( 'AISV-OK' !== $ok ) { return false; } $this->key = $key; return true; } /** * Iterate every non-manifest entry, calling $cb( $header, $reader ) for each. * $reader is $this; use read_payload()/copy_payload_to() inside the callback. * * @param callable $cb Callback. * @return void */ public function each( callable $cb ) { $this->rewind(); while ( null !== ( $h = $this->next_header() ) ) { $start = $this->payload_start; $len = $this->cur_len; if ( ( $h['header']['type'] ?? '' ) !== 'manifest' ) { $cb( $h['header'], $this ); } // Always resynchronise to the exact next entry, no matter how much (or how // little) the callback consumed. Without this, a callback that ignores an // entry's payload leaves the pointer mid-payload and the next header read // interprets random bytes as a gigantic length. fseek( $this->fh, $start + $len, SEEK_SET ); } } /* ---- payload helpers (valid only inside each()'s callback) ---------- */ private $cur_len = 0; private $payload_start = 0; /** * Read + decrypt + gunzip a small payload (manifest/db entries). * * @param array $header Entry header. * @return string */ public function payload( array $header ) { $data = $this->read( $this->cur_len ); if ( null !== $this->key && isset( $header['iv'] ) ) { $data = openssl_decrypt( $data, 'aes-256-ctr', $this->key, OPENSSL_RAW_DATA, base64_decode( $header['iv'] ) ); } if ( 'gzip' === ( $header['enc'] ?? '' ) ) { $plain = @gzdecode( $data ); if ( false === $plain ) { throw new RuntimeException( 'A database entry could not be decompressed (wrong password, or a damaged backup).' ); } $data = $plain; } return $data; } /** * Stream a file payload to disk (decrypting as it goes). * * @param array $header Entry header. * @param string $dest Local path. * @return void */ public function copy_to( array $header, $dest ) { $out = fopen( $dest, 'wb' ); if ( ! $out ) { throw new RuntimeException( 'Cannot write ' . $dest ); } $iv = ( null !== $this->key && isset( $header['iv'] ) ) ? base64_decode( $header['iv'] ) : null; $blocks = 0; $remaining = $this->cur_len; while ( $remaining > 0 ) { $buf = $this->read( (int) min( AISV_CHUNK, $remaining ) ); if ( '' === $buf ) { break; } $read = strlen( $buf ); $remaining -= $read; if ( null !== $iv ) { $buf = openssl_decrypt( $buf, 'aes-256-ctr', $this->key, OPENSSL_RAW_DATA, self::iv_add( $iv, $blocks ) ); $blocks += intdiv( $read, 16 ) + ( ( $read % 16 ) ? 1 : 0 ); } fwrite( $out, $buf ); } fclose( $out ); } /* ---- low-level volume spanning ------------------------------------- */ private function rewind() { $this->close(); $this->vi = 0; $this->open_volume( 0 ); } private function open_volume( $i ) { $this->close(); $this->fh = fopen( $this->volumes[ $i ], 'rb' ); if ( ! $this->fh || fread( $this->fh, strlen( AISV_MAGIC ) ) !== AISV_MAGIC ) { throw new RuntimeException( 'Bad or missing volume: ' . basename( $this->volumes[ $i ] ) ); } $this->vi = $i; } private function close() { if ( $this->fh ) { fclose( $this->fh ); $this->fh = null; } } /** * Next entry header, rolling to the next volume at EOF. Sets $cur_len. * * @return array{header:array,len:int}|null */ private function next_header() { $lb = $this->raw( 4 ); if ( strlen( $lb ) < 4 ) { // Try the next volume. if ( $this->vi + 1 < count( $this->volumes ) ) { $this->open_volume( $this->vi + 1 ); $lb = $this->raw( 4 ); if ( strlen( $lb ) < 4 ) { return null; } } else { return null; } } $hlen = unpack( 'N', $lb )[1]; $hjson = $this->raw( $hlen ); $plb = $this->raw( 8 ); if ( strlen( $plb ) < 8 ) { return null; } $plen = unpack( 'J', $plb )[1]; $this->cur_len = (int) $plen; $this->payload_start = ftell( $this->fh ); $header = json_decode( $hjson, true ); return array( 'header' => is_array( $header ) ? $header : array(), 'len' => (int) $plen ); } /** * Read exactly $n payload bytes (never spans a volume — entries don't). * * @param int $n Bytes. * @return string */ private function read( $n ) { return $this->raw( $n ); } private function raw( $n ) { $out = ''; while ( $n > 0 && $this->fh ) { $buf = fread( $this->fh, $n ); if ( false === $buf || '' === $buf ) { break; } $out .= $buf; $n -= strlen( $buf ); } return $out; } private static function iv_add( $iv, $blocks ) { $b = array_values( unpack( 'C16', $iv ) ); $c = (int) $blocks; for ( $i = 15; $i >= 0 && $c > 0; $i-- ) { $sum = $b[ $i ] + ( $c & 0xFF ); $b[ $i ] = $sum & 0xFF; $c = ( $c >> 8 ) + ( $sum >> 8 ); } return pack( 'C16', ...$b ); } } /* ========================================================================= * Serialization-safe search-replace (ports Restore\Search_Replace) * ====================================================================== */ /** * Replace inside a single value, re-serializing serialized data so string lengths * stay valid. * * @param string $val Value. * @param array $pairs old => new. * @return string */ function aisv_replace_value( $val, array $pairs ) { if ( preg_match( '/^[aOs]:\d+:/', $val ) || 'b:0;' === $val || 'N;' === $val ) { $un = @unserialize( $val ); if ( false !== $un || 'b:0;' === $val ) { return serialize( aisv_recurse( $un, $pairs ) ); } } return strtr( $val, $pairs ); } /** * Recurse through decoded serialized data. * * @param mixed $data Data. * @param array $pairs Map. * @return mixed */ function aisv_recurse( $data, array $pairs ) { if ( is_array( $data ) ) { $out = array(); foreach ( $data as $k => $v ) { $out[ is_string( $k ) ? strtr( $k, $pairs ) : $k ] = aisv_recurse( $v, $pairs ); } return $out; } if ( is_object( $data ) ) { foreach ( $data as $k => $v ) { $data->$k = aisv_recurse( $v, $pairs ); } return $data; } if ( is_string( $data ) ) { return strtr( $data, $pairs ); } return $data; } /* ========================================================================= * Helpers * ====================================================================== */ /** @return string The first-volume .aisv beside this script, or ''. */ function aisv_find_archive() { foreach ( glob( __DIR__ . '/*.aisv' ) as $f ) { return $f; // First-volume names never end in .aisv.N. } return ''; } /** @return string A random 64-char salt line value. */ function aisv_salt() { $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 !@#$%^&*()-_ []{}<>~`+=,.;:/?|'; $s = ''; for ( $i = 0; $i < 64; $i++ ) { $s .= $chars[ random_int( 0, strlen( $chars ) - 1 ) ]; } return $s; } /** * Compose a wp-config.php from a template's constants. * * @param array $c { DB_NAME, DB_USER, DB_PASSWORD, DB_HOST, prefix }. * @return string */ function aisv_wp_config( array $c ) { $keys = array( 'AUTH_KEY', 'SECURE_AUTH_KEY', 'LOGGED_IN_KEY', 'NONCE_KEY', 'AUTH_SALT', 'SECURE_AUTH_SALT', 'LOGGED_IN_SALT', 'NONCE_SALT' ); $salt = ''; foreach ( $keys as $k ) { $salt .= "define( '{$k}', '" . str_replace( array( '\\', "'" ), array( '\\\\', "\\'" ), aisv_salt() ) . "' );\n"; } $q = static function ( $v ) { return str_replace( array( '\\', "'" ), array( '\\\\', "\\'" ), (string) $v ); }; return " $fh, CURLOPT_FOLLOWLOCATION => true, CURLOPT_TIMEOUT => 300 ) ); $ok = curl_exec( $ch ); curl_close( $ch ); fclose( $fh ); } if ( ! $ok || ! is_file( $zip ) ) { return 'Could not download WordPress core from wordpress.org. Upload the WordPress files manually, then re-run.'; } if ( ! class_exists( 'ZipArchive' ) ) { @unlink( $zip ); return 'PHP has no Zip support to unpack WordPress core. Upload the WordPress files manually.'; } $za = new ZipArchive(); if ( true !== $za->open( $zip ) ) { @unlink( $zip ); return 'The downloaded WordPress core zip could not be opened.'; } // The zip contains a top-level wordpress/ folder — extract, then flatten. $tmp = $dir . '/.aisv-core-tmp'; @mkdir( $tmp, 0755, true ); $za->extractTo( $tmp ); $za->close(); @unlink( $zip ); $src = $tmp . '/wordpress'; if ( is_dir( $src ) ) { aisv_move_tree( $src, $dir ); } aisv_rmtree( $tmp ); return is_file( $dir . '/wp-includes/version.php' ) ? '' : 'WordPress core did not unpack correctly.'; } /** Move a directory tree into $dst without overwriting existing files. */ function aisv_move_tree( $src, $dst ) { foreach ( scandir( $src ) as $e ) { if ( '.' === $e || '..' === $e ) { continue; } $from = $src . '/' . $e; $to = $dst . '/' . $e; if ( is_dir( $from ) ) { @mkdir( $to, 0755, true ); aisv_move_tree( $from, $to ); } elseif ( ! is_file( $to ) ) { @rename( $from, $to ); } } } /** Recursively delete a directory. */ function aisv_rmtree( $dir ) { if ( ! is_dir( $dir ) ) { return; } foreach ( scandir( $dir ) as $e ) { if ( '.' === $e || '..' === $e ) { continue; } $p = $dir . '/' . $e; is_dir( $p ) ? aisv_rmtree( $p ) : @unlink( $p ); } @rmdir( $dir ); } /** Reject path traversal from archive file entries. */ function aisv_safe_rel( $rel ) { $rel = ltrim( str_replace( '\\', '/', (string) $rel ), '/' ); if ( '' === $rel || false !== strpos( $rel, '../' ) || '..' === $rel || preg_match( '#^[a-zA-Z]:/#', $rel ) ) { return false; } return $rel; } /* ========================================================================= * The install run (POST) * ====================================================================== */ /** * Do the whole install and return a log of steps. Throws on a fatal problem. * * @param string $archive_path Archive path. * @param array $in POST fields. * @return string[] Log lines. * @throws RuntimeException On failure. */ function aisv_run_install( $archive_path, array $in ) { $log = array(); $dir = __DIR__; $arc = new AISV_Archive( $archive_path ); if ( $arc->is_encrypted() && ! $arc->unlock( $in['password'] ?? '' ) ) { throw new RuntimeException( 'The backup is encrypted and the password is missing or wrong.' ); } $man = $arc->manifest; $old_url = isset( $man['site_url'] ) ? rtrim( (string) $man['site_url'], '/' ) : ''; $old_path = isset( $man['abspath'] ) ? (string) $man['abspath'] : ''; $old_prefix = isset( $man['table_prefix'] ) ? (string) $man['table_prefix'] : 'wp_'; $new_url = rtrim( (string) $in['new_url'], '/' ); $new_path = rtrim( str_replace( '\\', '/', $dir ), '/' ) . '/'; $prefix = preg_replace( '/[^A-Za-z0-9_$]/', '', (string) ( $in['prefix'] ?: $old_prefix ) ); /* ---- 1. Connect to the (empty) database ---- */ $host = (string) $in['db_host']; $port = 0; $sock = null; if ( false !== strpos( $host, ':' ) ) { list( $host, $tail ) = explode( ':', $host, 2 ); if ( ctype_digit( $tail ) ) { $port = (int) $tail; } else { $sock = $tail; } } mysqli_report( MYSQLI_REPORT_OFF ); $db = @mysqli_connect( $host, $in['db_user'], $in['db_pass'], $in['db_name'], $port ?: 3306, $sock ); if ( ! $db ) { throw new RuntimeException( 'Could not connect to the database: ' . mysqli_connect_error() ); } $db->set_charset( 'utf8mb4' ); $log[] = 'Connected to database "' . $in['db_name'] . '".'; /* ---- 2. Import the database ---- */ $tables = 0; $rows = 0; $arc->each( function ( $header, $reader ) use ( $db, $old_prefix, $prefix, &$tables, &$rows ) { if ( ( $header['type'] ?? '' ) !== 'db' ) { return; } $sql = $reader->payload( $header ); $kind = $header['kind'] ?? 'rows'; // Rewrite the table prefix if the operator chose a new one. if ( $prefix !== $old_prefix && ! empty( $header['table'] ) ) { $oldt = (string) $header['table']; $newt = $prefix . substr( $oldt, strlen( $old_prefix ) ); $sql = str_replace( '`' . $oldt . '`', '`' . $newt . '`', $sql ); } if ( 'create' === $kind ) { foreach ( array_filter( array_map( 'trim', explode( ";\n", $sql ) ) ) as $stmt ) { if ( '' !== $stmt ) { $db->query( $stmt ); } } ++$tables; } elseif ( 'rows' === $kind ) { $stmt = rtrim( trim( $sql ), ';' ); if ( '' !== $stmt && $db->query( $stmt ) ) { $rows += $db->affected_rows; } } else { // view / trigger / event — best effort, DEFINER already stripped at backup. foreach ( array_filter( array_map( 'trim', explode( ";\n", $sql ) ) ) as $stmt ) { if ( '' !== $stmt ) { $db->query( $stmt ); } } } } ); $log[] = "Imported {$tables} tables ({$rows} rows)."; /* ---- 3. Serialization-safe URL / path rewrite ---- */ $pairs = array(); if ( '' !== $old_url && $old_url !== $new_url ) { $pairs[ $old_url ] = $new_url; } if ( '' !== $old_path && rtrim( $old_path, '/' ) !== rtrim( $new_path, '/' ) ) { $pairs[ $old_path ] = $new_path; $pairs[ rtrim( $old_path, '/' ) ] = rtrim( $new_path, '/' ); } if ( $pairs ) { $changed = aisv_db_replace( $db, $prefix, $pairs ); $log[] = "Rewrote the site address for its new location ({$changed} values updated)."; } // Belt-and-braces: make sure the core options point at the new URL. $db->query( "UPDATE `{$prefix}options` SET option_value='" . $db->real_escape_string( $new_url ) . "' WHERE option_name IN ('siteurl','home')" ); /* ---- 4. Extract the files ---- */ $files = 0; $arc->each( function ( $header, $reader ) use ( $dir, &$files ) { if ( ( $header['type'] ?? '' ) !== 'file' ) { return; } $rel = aisv_safe_rel( $header['path'] ?? '' ); if ( false === $rel ) { return; } // Backups store paths relative to wp-content. $dest = $dir . '/wp-content/' . $rel; $sub = dirname( $dest ); if ( ! is_dir( $sub ) ) { @mkdir( $sub, 0755, true ); } $reader->copy_to( $header, $dest ); ++$files; } ); $log[] = "Extracted {$files} files."; /* ---- 5. WordPress core (not in the backup) ---- */ $core_err = aisv_fetch_core( $dir, isset( $man['wp_version'] ) ? preg_replace( '/[^0-9.]/', '', (string) $man['wp_version'] ) : '' ); $log[] = '' === $core_err ? 'WordPress core is in place.' : ( 'NOTE: ' . $core_err ); /* ---- 6. wp-config.php ---- */ $cfg = aisv_wp_config( array( 'DB_NAME' => $in['db_name'], 'DB_USER' => $in['db_user'], 'DB_PASSWORD' => $in['db_pass'], 'DB_HOST' => $in['db_host'], 'prefix' => $prefix, ) ); if ( false === @file_put_contents( $dir . '/wp-config.php', $cfg ) ) { throw new RuntimeException( 'The database and files were restored, but wp-config.php could not be written. Create it by hand from wp-config-sample.php.' ); } $log[] = 'Wrote wp-config.php.'; $db->close(); return $log; } /** * Walk every table's string columns and apply the serialization-safe replace. * * @param mysqli $db Connection. * @param string $prefix Table prefix. * @param array $pairs old => new. * @return int Values changed. */ function aisv_db_replace( $db, $prefix, array $pairs ) { $changed = 0; $res = $db->query( "SHOW TABLES LIKE '" . $db->real_escape_string( $prefix ) . "%'" ); if ( ! $res ) { return 0; } $tables = array(); while ( $row = $res->fetch_row() ) { $tables[] = $row[0]; } $res->free(); foreach ( $tables as $table ) { // Primary key. $pk = ''; $kr = $db->query( "SHOW KEYS FROM `{$table}` WHERE Key_name='PRIMARY'" ); if ( $kr && ( $k = $kr->fetch_assoc() ) ) { $pk = $k['Column_name']; } if ( $kr ) { $kr->free(); } if ( '' === $pk ) { continue; // Can't safely update rows without a key. } $offset = 0; do { $rows = $db->query( "SELECT * FROM `{$table}` LIMIT {$offset}, 200" ); if ( ! $rows ) { break; } $batch = 0; while ( $r = $rows->fetch_assoc() ) { ++$batch; $sets = array(); foreach ( $r as $col => $val ) { if ( ! is_string( $val ) || '' === $val ) { continue; } $new = aisv_replace_value( $val, $pairs ); if ( $new !== $val ) { $sets[] = "`{$col}`='" . $db->real_escape_string( $new ) . "'"; ++$changed; } } if ( $sets ) { $db->query( "UPDATE `{$table}` SET " . implode( ',', $sets ) . " WHERE `{$pk}`='" . $db->real_escape_string( $r[ $pk ] ) . "'" ); } } $rows->free(); $offset += 200; } while ( $batch === 200 ); } return $changed; } /* ========================================================================= * Web wizard * ====================================================================== */ $archive = aisv_find_archive(); $error = ''; $done = null; $manifest = array(); if ( '' !== $archive ) { try { $probe = new AISV_Archive( $archive ); $manifest = $probe->manifest; } catch ( Throwable $e ) { $error = $e->getMessage(); } } if ( 'POST' === ( $_SERVER['REQUEST_METHOD'] ?? '' ) && '' !== $archive && '' === $error ) { $in = array( 'db_host' => trim( $_POST['db_host'] ?? 'localhost' ), 'db_name' => trim( $_POST['db_name'] ?? '' ), 'db_user' => trim( $_POST['db_user'] ?? '' ), 'db_pass' => (string) ( $_POST['db_pass'] ?? '' ), 'prefix' => trim( $_POST['prefix'] ?? '' ), 'new_url' => trim( $_POST['new_url'] ?? '' ), 'password' => (string) ( $_POST['password'] ?? '' ), ); try { if ( '' === $in['db_name'] || '' === $in['db_user'] || '' === $in['new_url'] ) { throw new RuntimeException( 'Please fill in the database name, database user and the new site address.' ); } $done = aisv_run_install( $archive, $in ); } catch ( Throwable $e ) { $error = $e->getMessage(); } } $default_url = ''; if ( isset( $_SERVER['HTTP_HOST'] ) ) { $scheme = ( ! empty( $_SERVER['HTTPS'] ) && 'off' !== $_SERVER['HTTPS'] ) ? 'https' : 'http'; $default_url = $scheme . '://' . $_SERVER['HTTP_HOST'] . rtrim( dirname( $_SERVER['SCRIPT_NAME'] ?? '' ), '/\\' ); } ?> AI-SiteArk — Recovery Installer

AI-SiteArk Recovery Installer

Rebuild a WordPress site from a .aisv backup — no existing WordPress required.

No backup found. Put a .aisv file in the same folder as this installer, then reload.
Done — your site has been restored.
Now, for security, delete this installer and the backup file(s). They contain your data and database credentials.
Go to your dashboard →
Could not finish:
Backup of
Created
Table prefix
Encrypted
This backup is encrypted — enter the password you set when creating it.
Where this site will live now. Links and settings are rewritten to match.
Leave as-is unless you know you need to change it.
The database you enter must already exist and should be empty. This installer will import the backup into it and overwrite files in this folder.