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