prefix . 'coreseal_vault'; } /** * Absolute path of the vault, created and protected on first use. * * The vault is a folder with a random name in the uploads folder. Vaults * created by earlier versions in the content folder are moved there. * * @return string|WP_Error */ public function get_dir() { $uploads = Coreseal_Paths::uploads_dir(); if ( '' === $uploads ) { return new WP_Error( 'coreseal_vault_dir', __( 'The uploads folder could not be found, so the backup folder could not be created.', 'coreseal' ) ); } $name = get_option( self::OPTION_DIR ); if ( ! $name || 0 !== strpos( (string) $name, 'coreseal-vault-' ) ) { $name = 'coreseal-vault-' . wp_generate_password( 12, false, false ); update_option( self::OPTION_DIR, $name, false ); } $dir = $uploads . $name; if ( ! is_dir( $dir ) ) { $this->move_legacy_dir( $name, $dir ); } if ( ! is_dir( $dir ) && ! wp_mkdir_p( $dir ) ) { return new WP_Error( 'coreseal_vault_dir', __( 'The backup folder could not be created in the uploads folder. Check the folder permissions.', 'coreseal' ) ); } $this->protect( $dir ); return $dir; } /** * Moves a vault created by CoreSeal 1.0 (in the content folder) to the * uploads folder, keeping the saved copies. */ private function move_legacy_dir( string $name, string $dir ) { $content = Coreseal_Paths::content_dir(); if ( '' === $content || ! is_dir( $content . $name ) ) { return; } @rename( $content . $name, $dir ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.rename_rename } /** * Copies a file into the vault. * * @param string $path Absolute path of the file. * @param string $relative Path shown to the user (relative to the site root). * @param string $reason self::REASON_*. * @return int|WP_Error Vault entry ID. */ public function store( string $path, string $relative, string $reason ) { $dir = $this->get_dir(); if ( is_wp_error( $dir ) ) { return $dir; } $stored = wp_generate_password( 24, false, false ) . '.vault'; if ( ! @copy( $path, $dir . '/' . $stored ) ) { // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged return new WP_Error( 'coreseal_vault_copy', __( 'A backup copy of the file could not be saved, so nothing was changed.', 'coreseal' ) ); } global $wpdb; $wpdb->insert( $this->table(), array( 'original_path' => $relative, 'stored_name' => $stored, 'reason' => $reason, 'md5' => (string) md5_file( $path ), 'size' => (int) filesize( $path ), 'created_at' => current_time( 'mysql', true ), 'user_id' => get_current_user_id(), 'status' => 'stored', ), array( '%s', '%s', '%s', '%s', '%d', '%s', '%d', '%s' ) ); return (int) $wpdb->insert_id; } /** * @return array|null */ public function get( int $id ) { global $wpdb; return $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$this->table()} WHERE id = %d", $id ), ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared } /** * Absolute path of a stored copy. * * @return string|WP_Error */ public function get_stored_path( array $entry ) { $dir = $this->get_dir(); if ( is_wp_error( $dir ) ) { return $dir; } return $dir . '/' . basename( $entry['stored_name'] ); } /** * Copies a stored file back to its original location. * * @return true|WP_Error */ public function put_back( int $id ) { $entry = $this->get( $id ); if ( ! $entry || 'stored' !== $entry['status'] ) { return new WP_Error( 'coreseal_vault_entry', __( 'This saved copy no longer exists.', 'coreseal' ) ); } $stored = $this->get_stored_path( $entry ); if ( is_wp_error( $stored ) || ! is_file( $stored ) ) { return new WP_Error( 'coreseal_vault_missing', __( 'The saved copy could not be found in the backup folder.', 'coreseal' ) ); } $target = Coreseal_Paths::target( (string) $entry['original_path'] ); if ( ! $target ) { return new WP_Error( 'coreseal_vault_path', __( 'Invalid file path.', 'coreseal' ) ); } if ( ! wp_mkdir_p( dirname( $target ) ) || ! @copy( $stored, $target ) ) { // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged return new WP_Error( 'coreseal_vault_write', __( 'The file could not be put back (check the folder permissions).', 'coreseal' ) ); } // The file is back in place: the copy in the vault is no longer needed. wp_delete_file( $stored ); global $wpdb; $wpdb->update( $this->table(), array( 'status' => 'put_back' ), array( 'id' => $id ), array( '%s' ), array( '%d' ) ); return true; } /** * Deletes a stored copy for good. * * @return true|WP_Error */ public function delete( int $id ) { $entry = $this->get( $id ); if ( ! $entry || 'stored' !== $entry['status'] ) { return new WP_Error( 'coreseal_vault_entry', __( 'This saved copy no longer exists.', 'coreseal' ) ); } $stored = $this->get_stored_path( $entry ); if ( is_string( $stored ) && is_file( $stored ) ) { wp_delete_file( $stored ); } global $wpdb; $wpdb->update( $this->table(), array( 'status' => 'deleted' ), array( 'id' => $id ), array( '%s' ), array( '%d' ) ); return true; } /** * Most recent entries. */ public function latest( int $limit = 50 ): array { global $wpdb; return (array) $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$this->table()} ORDER BY id DESC LIMIT %d", $limit ), // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared ARRAY_A ); } /** * Closes the folder to visitors on Apache and IIS. On nginx the random * folder name and the ".vault" extension keep the files unreachable * and never executed. */ private function protect( string $dir ) { $files = array( '.htaccess' => "# CoreSeal vault\n\n\tRequire all denied\n\n\n\tOrder allow,deny\n\tDeny from all\n\n", 'web.config' => "\n\n", 'index.php' => " $content ) { if ( ! file_exists( $dir . '/' . $name ) ) { @file_put_contents( $dir . '/' . $name, $content ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents } } } }