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
}
}
}
}