prefix . 'brverify_jobs';
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$has_table = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $jobs ) ) );
if ( $has_table ) {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
$active = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$jobs} WHERE status IN ('pending','running')" );
if ( $active > 0 ) {
return; // Try again on a later request.
}
}
$parent = dirname( $new );
if ( ! is_dir( $parent ) && ! wp_mkdir_p( $parent ) ) {
update_option( 'brverify_dir_relocate_failed', 'uploads_not_writable', false );
return;
}
if ( ! is_dir( $new ) ) {
// phpcs:ignore WordPress.WP.AlternativeFunctions.rename_rename, WordPress.PHP.NoSilencedErrors.Discouraged
$moved = @rename( $old, $new );
} else {
// Both exist (for example after restoring an older plugin version): merge entry by entry, never overwrite.
$moved = self::merge_dir( $old, $new );
if ( $moved ) {
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_rmdir, WordPress.PHP.NoSilencedErrors.Discouraged
@rmdir( $old );
}
}
$old_n = rtrim( str_replace( '\\', '/', $old ), '/' );
$new_n = rtrim( str_replace( '\\', '/', $new ), '/' );
if ( ! $moved ) {
// Point any artifact that did move at its new place; the rest stays where it was.
if ( $has_table ) {
$arts = $wpdb->prefix . 'brverify_artifacts';
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
$rows = $wpdb->get_results( $wpdb->prepare( "SELECT id, path FROM {$arts} WHERE path LIKE %s", $wpdb->esc_like( $old_n . '/' ) . '%' ) );
foreach ( is_array( $rows ) ? $rows : array() as $row ) {
$moved_path = $new_n . substr( (string) $row->path, strlen( $old_n ) );
if ( ! file_exists( (string) $row->path ) && file_exists( $moved_path ) ) {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$wpdb->update( $arts, array( 'path' => $moved_path ), array( 'id' => (int) $row->id ), array( '%s' ), array( '%d' ) );
}
}
}
update_option( 'brverify_dir_relocate_failed', 'move_failed', false );
return;
}
$cols = array(
'jobs' => array( 'cursor_json', 'config_json', 'result_json' ),
'artifacts' => array( 'path', 'meta_json' ),
);
if ( $has_table ) {
foreach ( $cols as $t => $list ) {
$table = $wpdb->prefix . 'brverify_' . $t;
foreach ( $list as $col ) {
foreach ( array( array( $old_n, $new_n ), array( str_replace( '/', '\\/', $old_n ), str_replace( '/', '\\/', $new_n ) ) ) as $pair ) {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
$wpdb->query( $wpdb->prepare( "UPDATE {$table} SET {$col} = REPLACE({$col}, %s, %s) WHERE {$col} LIKE %s", $pair[0], $pair[1], '%' . $wpdb->esc_like( $pair[0] ) . '%' ) );
}
}
}
}
delete_option( 'brverify_dir_relocate_failed' );
update_option( 'brverify_dir_relocated', '1', true );
}
/**
* Move every entry of $from into $to. Same-named directories are merged; same-named
* files are only dropped from $from when they are identical deny / index files,
* otherwise they are left in place and the merge reports failure.
*
* @param string $from Source directory.
* @param string $to Target directory.
*/
private static function merge_dir( string $from, string $to ): bool {
$items = @scandir( $from ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
if ( ! is_array( $items ) ) {
return false;
}
$ok = true;
foreach ( $items as $item ) {
if ( '.' === $item || '..' === $item ) {
continue;
}
$src = $from . '/' . $item;
$dst = $to . '/' . $item;
if ( is_link( $src ) ) {
$ok = false;
continue;
}
if ( ! file_exists( $dst ) ) {
// phpcs:ignore WordPress.WP.AlternativeFunctions.rename_rename, WordPress.PHP.NoSilencedErrors.Discouraged
$ok = @rename( $src, $dst ) && $ok;
} elseif ( is_dir( $src ) && is_dir( $dst ) ) {
if ( self::merge_dir( $src, $dst ) ) {
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_rmdir, WordPress.PHP.NoSilencedErrors.Discouraged
@rmdir( $src );
} else {
$ok = false;
}
} elseif ( in_array( $item, array( '.htaccess', 'index.php', 'web.config' ), true ) ) {
wp_delete_file( $src );
} else {
$ok = false;
}
}
return $ok;
}
/**
* Storage directory for completed artifacts.
*/
public static function storage(): string {
return trailingslashit( self::root() ) . 'storage';
}
/**
* Per-job scratch directory.
*
* @param int $job_id Job id.
*/
public static function job( int $job_id ): string {
return trailingslashit( self::root() ) . 'jobs/' . absint( $job_id );
}
/**
* Ensure root, storage, jobs, and deny files exist.
*/
public static function ensure(): bool {
$paths = array(
self::root(),
self::storage(),
trailingslashit( self::root() ) . 'jobs',
);
foreach ( $paths as $path ) {
if ( ! is_dir( $path ) && ! wp_mkdir_p( $path ) ) {
return false;
}
self::harden( $path );
}
return true;
}
/**
* Ensure a specific job directory exists and is hardened.
*
* @param int $job_id Job id.
*/
public static function ensure_job( int $job_id ): string {
self::ensure();
$dir = self::job( $job_id );
if ( ! is_dir( $dir ) ) {
wp_mkdir_p( $dir );
}
self::harden( $dir );
$database = trailingslashit( $dir ) . 'database';
$meta = trailingslashit( $dir ) . 'meta';
if ( ! is_dir( $database ) ) {
wp_mkdir_p( $database );
}
if ( ! is_dir( $meta ) ) {
wp_mkdir_p( $meta );
}
return $dir;
}
/**
* Write web-deny files into a directory.
*
* @param string $dir Absolute directory.
*/
public static function harden( string $dir ): void {
$deny_htaccess = "Options -Indexes\n\nRequire all denied\n\n\nDeny from all\n\n";
$index = "\n\n \n \n \n \n \n\n";
self::write_if_missing( trailingslashit( $dir ) . '.htaccess', $deny_htaccess );
self::write_if_missing( trailingslashit( $dir ) . 'index.php', $index );
self::write_if_missing( trailingslashit( $dir ) . 'web.config', $webconfig );
}
/**
* Default exclusion patterns (basename / path fragment matches).
*
* @return list
*/
public static function default_exclusions(): array {
/**
* Filter default exclusion patterns applied when packing files.
*
* @param list $patterns Patterns.
*/
return apply_filters(
'brverify_exclusion_patterns',
array(
'cache',
'object-cache',
'*.log',
'node_modules',
'.git',
'.svn',
'updraft',
'backups',
'ai1wm-backups',
'brverify-working',
'brverify-artifacts',
'debug.log',
'*.sql.gz',
)
);
}
/**
* Whether a relative path under an allowed root should be skipped.
*
* @param string $relative Relative path using forward slashes.
* @param list $patterns Exclusion patterns.
*/
public static function is_excluded( string $relative, array $patterns ): bool {
$normalized = str_replace( '\\', '/', $relative );
$parts = explode( '/', $normalized );
$basename = basename( $normalized );
foreach ( $patterns as $pattern ) {
$pattern = (string) $pattern;
if ( '' === $pattern ) {
continue;
}
if ( \brverify_str_contains( $pattern, '*' ) ) {
if ( fnmatch( $pattern, $basename ) || fnmatch( $pattern, $normalized ) ) {
return true;
}
continue;
}
if ( $basename === $pattern ) {
return true;
}
foreach ( $parts as $part ) {
if ( $part === $pattern ) {
return true;
}
}
if ( \brverify_str_contains( $normalized, '/' . $pattern . '/' ) || \brverify_str_starts_with( $normalized, $pattern . '/' ) ) {
return true;
}
}
return false;
}
/**
* Recursively remove a directory tree (job scratch cleanup).
*
* @param string $dir Absolute directory.
*/
public static function rrmdir( string $dir ): void {
if ( ! is_dir( $dir ) ) {
return;
}
$root = realpath( self::root() );
$real = realpath( $dir );
if ( false === $root || false === $real || ! \brverify_str_starts_with( $real, $root ) ) {
return;
}
$items = scandir( $real );
if ( false === $items ) {
return;
}
foreach ( $items as $item ) {
if ( '.' === $item || '..' === $item ) {
continue;
}
$path = $real . DIRECTORY_SEPARATOR . $item;
if ( is_dir( $path ) && ! is_link( $path ) ) {
self::rrmdir( $path );
} else {
// phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink
@unlink( $path );
}
}
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_rmdir
@rmdir( $real );
}
/**
* Write a file only if missing.
*
* @param string $path Path.
* @param string $contents Contents.
*/
private static function write_if_missing( string $path, string $contents ): void {
if ( ! file_exists( $path ) ) {
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
file_put_contents( $path, $contents );
}
}
}