the extension (see the filter). */ private static function extensions(): array { static $out = null; if ( null !== $out ) { return $out; } /** * Filters the reviewed actions that code extending Image Manager adds, by the prefix of their names. * * @since 1.9.0 * * @param array $extensions Prefix (lowercase letters and a closing underscore) => array( 'catalog' => callable( * array $items ) returning the catalog with its actions, 'owner' => the owner each * action's plan names, 'secret_kind' => the input kind of an action's secret_reference, * 'prepare' => callable( string $op, array &$dependencies, array &$expected ), 'execute' * => callable( string $op, array $p, array $context, array &$e ) returning true or a * WP_Error, 'verify' => callable( array $e ) returning the observed results, 'section' * => optional inspection section name, 'inspect' => callable returning that section, * 'recovery' => optional names of its actions that may run while an earlier uncertain * change still reserves a target ). */ $given = apply_filters( 'afsd_im_reviewed_extensions', array() ); $own = array_column( self::own_catalog(), 'operation' ); $out = array(); foreach ( is_array( $given ) ? $given : array() as $prefix => $extension ) { if ( ! is_string( $prefix ) || ! preg_match( '/^[a-z]{2,20}_$/D', $prefix ) || ! is_array( $extension ) || ! is_string( $extension['owner'] ?? null ) || ! is_string( $extension['secret_kind'] ?? null ) ) { continue; } foreach ( array( 'catalog', 'prepare', 'execute', 'verify' ) as $key ) { if ( ! is_callable( $extension[ $key ] ?? null ) ) { continue 2; } } foreach ( $own as $op ) { if ( str_starts_with( (string) $op, $prefix ) ) { continue 2; } } $out[ $prefix ] = $extension; } return $out; } /** * The extensions' recovery actions: those that may run while an earlier uncertain change still reserves a target. * * @return array Action names. */ public static function extension_recovery(): array { $out = array(); foreach ( self::extensions() as $prefix => $extension ) { foreach ( (array) ( $extension['recovery'] ?? array() ) as $op ) { if ( is_string( $op ) && str_starts_with( $op, $prefix ) ) { $out[] = $op; } } } return $out; } /** * The extension an action belongs to. * * @param string $op The action. * @return array|null The extension, or null for Image Manager's own actions. */ private static function extension( string $op ): ?array { foreach ( self::extensions() as $prefix => $extension ) { if ( str_starts_with( $op, $prefix ) ) { return $extension; } } return null; } /** * One action's definition. * * @param string $op The action. * @return array The definition. */ public static function definition( string $op ): array { foreach ( self::catalog() as $d ) { if ( $d['operation'] === $op ) { return $d; } }self::fail( 'reviewed_unknown_action' ); } /** * The one attachment a value names (an ID or #ID, its title in any case, its file or file name, or else a title * containing the value) that the current user may edit. * * @param string $value The value. * @return int The attachment's ID. * @throws \RuntimeException With reviewed_ambiguous_attachment or reviewed_attachment_missing. */ public static function attachment( string $value ): int { global $wpdb; $value = trim( $value ); $ids = array(); if ( preg_match( '/^#?([1-9][0-9]*)$/D', $value, $m ) ) { $ids = array( (int) $m[1] ); } else { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- bounded live target lookup for an exact reviewed action; must reflect current media rows $ids = $wpdb->get_col( $wpdb->prepare( "SELECT DISTINCT p.ID FROM {$wpdb->posts} p LEFT JOIN {$wpdb->postmeta} m ON m.post_id=p.ID AND m.meta_key='_wp_attached_file' WHERE p.post_type='attachment' AND p.post_status<>'trash' AND (LOWER(p.post_title)=LOWER(%s) OR m.meta_value=%s OR m.meta_value LIKE %s) ORDER BY p.ID LIMIT 21", $value, $value, '%/' . $wpdb->esc_like( basename( $value ) ) ) ); if ( ! $ids ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- bounded live target lookup for an exact reviewed action; must reflect current media rows $ids = $wpdb->get_col( $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE post_type='attachment' AND post_status<>'trash' AND post_title LIKE %s ORDER BY ID LIMIT 21", '%' . $wpdb->esc_like( $value ) . '%' ) ); } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- bounded live target lookup for an exact reviewed action; must reflect current media rows } $ids = array_values( array_filter( array_map( 'intval', $ids ), static fn( $id ) => 'attachment' === get_post_type( $id ) && current_user_can( 'edit_post', $id ) ) ); if ( count( $ids ) !== 1 ) { throw new \RuntimeException( $ids ? 'reviewed_ambiguous_attachment' : 'reviewed_attachment_missing' ); } Media_Actions::authorize( $ids[0] ); return $ids[0]; } /** * The one source a value names (an ID, #ID or source:ID, its attachment's title in any case, or text in its details) * that the current user may manage. * * @param string $value The value. * @return int The source row's ID. */ public static function source( string $value ): int { global $wpdb; $value = trim( $value ); if ( preg_match( '/^(?:source:|#)?([1-9][0-9]*)$/D', $value, $m ) ) { $id = (int) $m[1]; Preview::row( $id ); return $id; } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- bounded live target lookup for an exact reviewed action; must reflect current media rows $ids = $wpdb->get_col( $wpdb->prepare( 'SELECT s.id FROM %i s LEFT JOIN %i p ON s.attachment_id=p.ID WHERE s.state<>%s AND (LOWER(p.post_title)=LOWER(%s) OR s.details LIKE %s) ORDER BY s.id LIMIT 21', Repository::table( 'sources' ), $wpdb->posts, 'grouped', $value, '%' . $wpdb->esc_like( $value ) . '%' ) ); $allowed = array(); foreach ( $ids as $id ) { try { Preview::row( (int) $id ); $allowed[] = (int) $id; } catch ( \Throwable $e ) { // A source the current user may not manage is left out of the matches. } } if ( count( $allowed ) !== 1 ) { self::fail( $allowed ? 'reviewed_ambiguous_source' : 'reviewed_source_missing' ); }return $allowed[0]; } /** * The one folder or collection a value names (its ID or #ID, or its name in any case). * * @param string $value The value. * @param string $tax The taxonomy. * @param bool $empty Whether an empty value means none (0). * @return int The term's ID, or 0. */ public static function term( string $value, string $tax, bool $empty = false ): int { if ( $empty && '' === trim( $value ) ) { return 0; }global $wpdb; $value = trim( $value ); if ( preg_match( '/^#?([1-9][0-9]*)$/D', $value, $m ) ) { $ids = array( (int) $m[1] ); } else { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- bounded live target lookup for an exact reviewed action; must reflect current media rows $ids = $wpdb->get_col( $wpdb->prepare( "SELECT t.term_id FROM {$wpdb->terms} t INNER JOIN {$wpdb->term_taxonomy} x ON x.term_id=t.term_id WHERE x.taxonomy=%s AND LOWER(t.name)=LOWER(%s) ORDER BY t.term_id LIMIT 21", $tax, $value ) ); } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- bounded live target lookup for an exact reviewed action; must reflect current media rows $ids = array_values( array_filter( array_map( 'intval', $ids ), static function ( $id ) use ( $tax ) { $t = get_term( $id, $tax ); return $t && ! is_wp_error( $t ); } ) ); if ( count( $ids ) !== 1 ) { self::fail( $ids ? 'reviewed_ambiguous_term' : 'reviewed_term_missing' ); }return $ids[0]; } /** * The one backup a value names (its ID or its scope label). * * @param string $value The value. * @return string The backup's ID. */ public static function snapshot_id( string $value ): string { $found = array(); foreach ( Backup::index() as $r ) { if ( $r['id'] === $value || ( $r['scope_label'] ?? '' ) === $value ) { $found[] = $r['id']; } } if ( count( $found ) !== 1 ) { self::fail( $found ? 'reviewed_ambiguous_snapshot' : 'backup_snapshot_missing' ); }return $found[0]; } /** * One item of an admin list by ID (#12 or 12) or exact title, of that type, editable by the current user. * * @param string $value The value. * @param string $type The post type. * @return int The item's ID. */ public static function content_item( string $value, string $type ): int { global $wpdb; $value = trim( $value ); if ( preg_match( '/^#?([1-9][0-9]*)$/D', $value, $m ) ) { $ids = array( (int) $m[1] ); } else { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- bounded live target lookup for an exact reviewed action; must reflect current content rows $ids = $wpdb->get_col( $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE post_type=%s AND post_status NOT IN ('trash','auto-draft','inherit') AND post_title=%s ORDER BY ID LIMIT 21", $type, $value ) ); } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- bounded live target lookup for an exact reviewed action; must reflect current content rows $ids = array_values( array_filter( array_map( 'intval', $ids ), static fn( $id ) => get_post_type( $id ) === $type && 'trash' !== get_post_status( $id ) && current_user_can( 'edit_post', $id ) ) ); if ( count( $ids ) !== 1 ) { self::fail( $ids ? 'reviewed_ambiguous_item' : 'reviewed_item_missing' ); }return $ids[0]; } /** * A folder of an admin list by ID, name or full path (Landing › Spring); '' is no folder (the top level). * * @param array $folders The type's folders. * @param string $value The value. * @return int The folder's ID, or 0 for the top level. */ private static function content_folder( array $folders, string $value ): int { $value = trim( $value ); if ( '' === $value ) { return 0; } if ( preg_match( '/^#?([1-9][0-9]*)$/D', $value, $m ) && isset( $folders[ (int) $m[1] ] ) ) { return (int) $m[1]; } $hits = array(); foreach ( $folders as $f ) { if ( 0 === strcasecmp( $f['path'], $value ) || 0 === strcasecmp( $f['name'], $value ) ) { $hits[] = $f['id']; } } $hits = array_values( array_unique( $hits ) ); if ( count( $hits ) !== 1 ) { self::fail( $hits ? 'reviewed_ambiguous_term' : 'reviewed_term_missing' ); }return $hits[0]; } /** * The sources or attachments values name, each once, sorted by ID. * * @param array $values The values. * @param string $kind source or attachment. * @return array The IDs. */ private static function unique_ids( array $values, string $kind ): array { $ids = array(); foreach ( $values as $v ) { $ids[] = 'source' === $kind ? self::source( $v ) : self::attachment( $v ); } $ids = array_values( array_unique( $ids ) ); sort( $ids, SORT_NUMERIC ); return $ids; } /** * The saved settings over the defaults, read straight from the database. * * @return array The settings. */ private static function settings(): array { return array_merge( Settings::defaults(), (array) Reviewed_Store::raw( Settings::OPTION, array() ) ); } /** * A fingerprint of an option's stored value. * * @param string $option The option. * @return string The fingerprint. */ public static function option_token( string $option ): string { return Reviewed_Store::hash( Reviewed_Store::raw( $option ) ); } /** 1.8.2 (F-LS-29 class): whether Image Manager is active and what a reviewed change runs on (WordPress, PHP, WooCommerce, * Image Manager's schema and site), * not every active plugin, mu-plugin and the theme: installing, activating or updating an unrelated plugin or the theme no * longer makes a reviewed change out of date. Image Manager's own version is checked with the proposal; what a change reads * of pictures, theme image sources, folders and jobs is in its state. */ public static function environment(): array { global $wp_version; $active = in_array( plugin_basename( FILE ), array_merge( (array) Reviewed_Store::raw( 'active_plugins', array() ), array_keys( (array) get_site_option( 'active_sitewide_plugins', array() ) ) ), true ); return array( 'active' => $active, 'wordpress' => $wp_version, 'php' => PHP_VERSION, 'woocommerce' => defined( 'WC_VERSION' ) ? WC_VERSION : '', 'schema' => SCHEMA, 'site' => Reviewed_Inputs::site(), ); } /** * An attachment's state for a prepared change (its title, type, file size and hash and its other fields), read past * the caches. * * @param int $id The attachment. * @return array The state. */ private static function attachment_state( int $id ): array { clean_post_cache( $id ); wp_cache_delete( $id, 'post_meta' ); $p = get_post( $id ); Media_Actions::authorize( $id ); $path = get_attached_file( $id, true ); $file = array( 'available' => false ); if ( is_string( $path ) && is_file( $path ) && ! is_link( $path ) && is_readable( $path ) ) { $file = array( 'available' => true, 'bytes' => (int) filesize( $path ), 'sha256' => hash_file( 'sha256', $path ), ); } return array( 'id' => $id, 'title' => $p->post_title, 'mime' => $p->post_mime_type, 'file' => $file, 'fingerprint' => Reviewed_Store::hash( array( $p->post_title, $p->post_excerpt, $p->post_content, $p->post_parent, $p->post_status, self::owned_meta( $id ), Library::term_ids( $id, Library::FOLDER ), Library::term_ids( $id, Library::COLLECTION ) ) ), ); } /** * 1.8.2 (F-LS-28 class): the picture's meta that Image Manager's actions read or write: its file, sizes, backup sizes and * alternative text, and Image Manager's own keys. The editor lock and other plugins' or tools' meta on a picture do not * make a reviewed change out of date. * * @param int $id The attachment. * @return array Meta key => value. */ private static function owned_meta( int $id ): array { $out = array(); foreach ( get_post_meta( $id ) as $k => $v ) { if ( in_array( $k, array( '_wp_attached_file', '_wp_attachment_metadata', '_wp_attachment_backup_sizes', '_wp_attachment_image_alt' ), true ) || str_starts_with( $k, '_afsd_im_' ) ) { $out[ $k ] = $v; } } ksort( $out ); return $out; } /** * A source's state for a prepared change: its row and its files' hashes. * * @param int $id The source row. * @return array The state. */ private static function source_state( int $id ): array { $r = Preview::row( $id ); $files = Sources::row_files( $r ); $hashes = array(); foreach ( $files as $rel => $f ) { if ( is_file( $f['path'] ) && ! is_link( $f['path'] ) ) { $hashes[ $rel ] = hash_file( 'sha256', $f['path'] ); } } return array( 'id' => $id, 'attachment_id' => (int) $r['attachment_id'], 'revision' => (int) $r['revision'], 'state' => $r['state'], 'requested' => (int) $r['requested'], 'excluded' => (int) $r['excluded'], 'delivery' => (int) $r['delivery'], 'fingerprint' => Reviewed_Store::hash( array( $r, $hashes, Sources::eligible( $r ) ) ), 'title' => $r['attachment_id'] ? get_the_title( $r['attachment_id'] ) : (string) ( $r['details']['relative'] ?? ( 'Theme image #' . $id ) ), ); } /** * A run's state for a prepared change: the image run, or the backup job. * * @param string $kind image or backup. * @return array The state. */ private static function job_state( string $kind ): array { if ( 'image' === $kind ) { $j = Workflow::job(); return array( 'id' => $j['id'], 'active' => (bool) $j['active'], 'mode' => $j['mode'], 'cancelled' => (bool) $j['cancelled'], 'paused' => (bool) self::settings()['paused'], ); } $j = 'backup' === $kind ? Backup::job() : Remote_Backup::job(); return array( 'id' => $j['id'], 'active' => (bool) $j['active'], 'paused' => (bool) $j['paused'], 'stage' => $j['stage'], 'actor' => (int) ( $j['actor'] ?? get_current_user_id() ), ); } /** * The remote backup settings an action would save: the saved ones with the action's parameters over them, its folder * or collection resolved and normalised as the save does. * * @param array $p The action's parameters. * @return array The remote settings. */ private static function native_remote( array $p ): array { $s = Remote_Backup::settings(); $out = array( 'remote_provider' => $s['provider'], 'remote_schedule' => $s['schedule'], 'remote_profile' => $s['profile'], 'remote_scope' => $s['scope'], 'remote_scope_id' => $s['scope_id'], 'remote_retention' => $s['retention'], 'remote_keep_local' => $s['keep_local'], 'remote_schedule_encrypt' => $s['schedule_encrypt'], 'google_client_id' => $s['google']['client_id'], 's3_endpoint' => $s['s3']['endpoint'], 's3_region' => $s['s3']['region'], 's3_bucket' => $s['s3']['bucket'], 's3_prefix' => $s['s3']['prefix'], 's3_path_style' => (bool) $s['s3']['path_style'], ); if ( ! $p ) { return $out; } foreach ( array( 'provider', 'schedule', 'profile', 'scope', 'retention', 'keep_local', 'schedule_encrypt' ) as $k ) { if ( array_key_exists( $k, $p ) ) { $out[ 'remote_' . $k ] = $p[ $k ]; } } foreach ( array( 'google_client_id', 's3_endpoint', 's3_region', 's3_bucket', 's3_prefix', 's3_path_style' ) as $k ) { if ( array_key_exists( $k, $p ) ) { $out[ $k ] = $p[ $k ]; } } if ( 'all' === $out['remote_scope'] ) { $out['remote_scope_id'] = 0; } elseif ( isset( $p['term'] ) ) { $out['remote_scope_id'] = self::term( $p['term'], 'folder' === $out['remote_scope'] ? Library::FOLDER : Library::COLLECTION ); } if ( ! $out['remote_scope_id'] && 'all' !== $out['remote_scope'] ) { self::fail( 'backup_scope_missing' ); } if ( isset( $p['secret_reference'] ) ) { $out['secret_reference'] = $p['secret_reference']; } // Normalize exactly as the owner does, without calling its save routine during planning. foreach ( array( 'google_client_id', 's3_endpoint', 's3_region', 's3_bucket', 's3_prefix' ) as $k ) { $out[ $k ] = sanitize_text_field( $out[ $k ] ); } $out['google_client_id'] = substr( trim( $out['google_client_id'] ), 0, 255 ); $ep = wp_parse_url( trim( $out['s3_endpoint'] ?: 'https://s3.amazonaws.com' ) ); // phpcs:ignore PluginCheck.CodeAnalysis.Offloading.OffloadedContent -- default S3 API endpoint for the optional remote backup provider; no asset is loaded from it if ( ! is_array( $ep ) || 'https' !== strtolower( $ep['scheme'] ?? '' ) || empty( $ep['host'] ) || isset( $ep['user'] ) || isset( $ep['pass'] ) || ! empty( $ep['query'] ) || ! empty( $ep['fragment'] ) || ! in_array( $ep['path'] ?? '', array( '', '/' ), true ) || 'localhost' === strtolower( $ep['host'] ?? '' ) || filter_var( $ep['host'] ?? '', FILTER_VALIDATE_IP ) ) { self::fail( 'remote_endpoint_invalid' ); } $out['s3_endpoint'] = 'https://' . strtolower( $ep['host'] ) . ( isset( $ep['port'] ) ? ':' . (int) $ep['port'] : '' ); if ( ! preg_match( '/^[a-z0-9-]{2,32}$/D', $out['s3_region'] ) ) { $out['s3_region'] = 'us-east-1'; } if ( '' !== $out['s3_bucket'] && ! preg_match( '/^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/D', $out['s3_bucket'] ) ) { self::fail( 'remote_s3_bucket_invalid' ); } $parts = array(); foreach ( explode( '/', trim( str_replace( '\\', '/', $out['s3_prefix'] ), '/' ) ) as $part ) { $part = trim( (string) preg_replace( '/[^A-Za-z0-9._-]/', '-', $part ), '.-' ); if ( '' !== $part ) { $parts[] = substr( $part, 0, 80 ); }if ( count( $parts ) >= 12 ) { break; } }$out['s3_prefix'] = ( $parts ? implode( '/', $parts ) : 'affstackdev-image-manager' ) . '/'; return $out; } /** * A draft of an attachment that an action names, not older than a day; a prepared media draft is also checked to match * the attachment still. * * @param int $id The attachment. * @param string $kind edit, pdf, pdf_copy or replace. * @param string $token The draft's ID. * @return array The draft. */ private static function draft( int $id, string $kind, string $token ): array { $d = 'edit' === $kind ? (array) get_post_meta( $id, Editor::DRAFT_META, true ) : Media_Draft::get( $id, array( 'pdf' => 'convert', 'pdf_copy' => 'compress' )[ $kind ] ?? 'replace' ); if ( ! $d || ( $d['id'] ?? '' ) !== $token || ( ! empty( $d['created'] ) && $d['created'] < time() - DAY_IN_SECONDS ) ) { self::fail( 'reviewed_draft_missing' ); } if ( 'edit' !== $kind && 'prepared' === ( $d['state'] ?? '' ) ) { Media_Draft::path( $d ); Media_Actions::unchanged( $id, $d['source'] ); } return $d; } /** * An attachment's text fields. * * @param int $id The attachment. * @return array Its title, caption and alt text. */ private static function fields( int $id ): array { $p = get_post( $id ); return array( 'title' => $p->post_title, 'caption' => $p->post_excerpt, 'alt' => (string) get_post_meta( $id, '_wp_attachment_image_alt', true ), ); } /** * A folder import's state as an action reads it. * * @param array $s The import. * @return array Its summary, or an empty array. */ private static function import_summary( array $s ): array { return $s ? array( 'source' => (string) ( $s['source'] ?? '' ), 'status' => (string) ( $s['status'] ?? '' ), 'folders_made' => (int) ( $s['created'] ?? 0 ), 'folders_already_there' => (int) ( $s['reused'] ?? 0 ), 'file_placements_done' => (int) ( $s['placed'] ?? 0 ), 'file_placements_total' => (int) ( $s['total'] ?? 0 ), 'skipped' => (int) ( $s['missing'] ?? 0 ), ) : array(); } /** * The no-use check's state as an action reads it. * * @param array $s The check. * @return array Its summary, or an empty array. */ private static function unused_summary( array $s ): array { return $s ? array( 'status' => (string) ( $s['status'] ?? '' ), 'phase' => (string) ( $s['phase'] ?? '' ), 'started_utc' => gmdate( 'c', (int) ( $s['started'] ?? 0 ) ), 'content_checked' => (int) ( $s['checked'] ?? 0 ), 'files_checked' => (int) ( $s['files_checked'] ?? 0 ), 'files_listed' => count( (array) ( $s['listed'] ?? array() ) ), 'more_not_listed' => (int) ( $s['more_listed'] ?? 0 ), 'skipped' => (int) ( $s['skipped'] ?? 0 ), 'collection' => (int) ( $s['collection'] ?? 0 ), 'collected' => (int) ( $s['collected'] ?? 0 ), ) : array(); } /** * Plan an action: resolve its targets and inputs, check it can run, and record the current state, the dependencies and * the expected result. * * @param string $op The action. * @param array $p Its parameters. * @return array The plan. */ public static function plan( string $op, array $p ): array { self::definition( $op ); $s = self::settings(); $native = $p; $targets = array( 'attachments' => array(), 'sources' => array(), 'terms' => array(), ); $before = array(); $expected = array( 'operation' => $op ); $dependencies = array( 'settings' => Reviewed_Store::hash( $s ) ); if ( isset( $p['attachment'] ) ) { $native['attachment_id'] = self::attachment( $p['attachment'] ); $targets['attachments'][] = $native['attachment_id']; } if ( isset( $p['attachments'] ) ) { $native['attachment_ids'] = self::unique_ids( $p['attachments'], 'attachment' ); $targets['attachments'] = $native['attachment_ids']; } if ( isset( $p['source'] ) ) { $native['source_id'] = self::source( $p['source'] ); $targets['sources'][] = $native['source_id']; } if ( isset( $p['sources'] ) ) { $native['source_ids'] = self::unique_ids( $p['sources'], 'source' ); $targets['sources'] = $native['source_ids']; } if ( isset( $p['snapshot'] ) ) { $native['snapshot_id'] = self::snapshot_id( $p['snapshot'] ); $snap = Backup::snapshot( $native['snapshot_id'] ); $dependencies['snapshot'] = Reviewed_Store::hash( $snap ); } if ( preg_match( '/^(folder|collection)_/', $op, $m ) ) { $native['taxonomy'] = 'folder' === $m[1] ? Library::FOLDER : Library::COLLECTION; if ( isset( $p['term'] ) ) { $native['term_id'] = self::term( $p['term'], $native['taxonomy'] ); $targets['terms'][] = array( 'id' => $native['term_id'], 'taxonomy' => $native['taxonomy'], ); } if ( isset( $p['terms'] ) ) { $native['term_ids'] = array(); foreach ( $p['terms'] as $t ) { $native['term_ids'][] = self::term( $t, $native['taxonomy'] ); }$native['term_ids'] = array_values( array_unique( $native['term_ids'] ) ); sort( $native['term_ids'] ); foreach ( $native['term_ids'] as $t ) { $targets['terms'][] = array( 'id' => $t, 'taxonomy' => $native['taxonomy'], ); } } if ( isset( $p['parent'] ) && '' !== $p['parent'] ) { $native['parent_id'] = self::term( $p['parent'], Library::FOLDER ); $targets['terms'][] = array( 'id' => $native['parent_id'], 'taxonomy' => Library::FOLDER, ); } } if ( isset( $p['input_reference'] ) ) { $kind = 'media_upload' === $op ? 'media_upload' : ( 'replace_prepare' === $op ? 'replacement' : 'backup_import' ); $dependencies['input'] = Reviewed_Inputs::identity( $p['input_reference'], $kind ); } if ( isset( $p['secret_reference'] ) ) { $kind = self::extension( $op )['secret_kind'] ?? ( 'remote_settings_update' === $op ? 'remote_credentials' : 'backup_password' ); $dependencies['secret'] = Reviewed_Inputs::identity( $p['secret_reference'], $kind ); } if ( isset( $p['remote_id'] ) ) { $entry = Remote_Backup::entry( $p['remote_id'] ); if ( ! $entry || ! Remote_Backup::can_manage_entry( $entry ) ) { self::fail( 'remote_destination_mismatch' ); }$dependencies['remote_entry'] = Reviewed_Store::hash( $entry ); } if ( 'settings_update' === $op ) { if ( ! $p['settings'] ) { self::fail( 'reviewed_no_fields' ); }$values = $p['settings']; if ( isset( $values['compression'] ) ) { $values['compression'] = Policy::clean( array_merge( Policy::current(), $values['compression'] ) ); } if ( isset( $values['content_folders'] ) ) { $clean = Content_Folders::clean_types( $values['content_folders'] ); if ( $clean !== array_values( array_unique( $values['content_folders'] ) ) ) { self::fail( 'content_folder_type_unknown' ); }$values['content_folders'] = $clean; } // 1.9.0 (D-500 gap 14): SVG uploads need roles that exist and may upload files, and the extensions cleaning needs. if ( isset( $values['svg_roles'] ) ) { $clean = SVG::clean_roles( $values['svg_roles'] ); if ( count( $clean ) !== count( array_unique( $values['svg_roles'] ) ) ) { self::fail( 'svg_role_unknown' ); }$values['svg_roles'] = $clean; } $next = array_merge( $s, $values ); if ( ! empty( $next['svg_uploads'] ) && ! SVG::available() ) { self::fail( 'svg_unavailable' ); } if ( ! empty( $next['svg_uploads'] ) && ! SVG::clean_roles( $next['svg_roles'] ) ) { self::fail( 'svg_roles_required' ); } // 1.9.0 (D-500 gap 6): a watermark needs its picture, a still PNG or WebP from the Media Library. if ( ( ! empty( $values['watermark_image'] ) || ! empty( $next['watermark'] ) ) && ! Watermark::picture( (int) $next['watermark_image'] ) ) { self::fail( 'watermark_picture_unavailable' ); } if ( ( $next['delivery'] || $next['uploads'] || $next['auto_discovery'] || ! empty( $next['fit'] ) || ! empty( $next['dimensions'] ) || ! empty( $next['watermark'] ) ) && ! $next['public_policy'] ) { self::fail( 'preview_public_required' ); } $native['settings'] = $values; $before['settings'] = array_intersect_key( $s, $values ); $expected['settings'] = $values; } elseif ( 'scan_start' === $op || 'optimization_start' === $op || 'bulk_run' === $op ) { if ( Workflow::job()['active'] || Scanner::pending() ) { self::fail( 'reviewed_run_active' ); } $dependencies['job'] = self::job_state( 'image' ); $dependencies['image_job_hash'] = Reviewed_Store::hash( Workflow::job() ); if ( 'scan_start' === $op ) { $expected['mode'] = 'scan'; $expected['job_active'] = true; } else { $native['compression'] = Policy::clean( array_merge( Policy::current(), $p['compression'] ?? array() ) ); $native['bulk_intent'] = $p['bulk_intent'] ?? 'apply'; $native['sizing'] = $p['sizing'] ?? 'existing'; $native['target'] = $p['target'] ?? 'all'; $native['delivery'] = $p['delivery'] ?? $s['delivery']; $bulk = 'optimization_start' === $op ? 'optimize' : $p['bulk']; if ( in_array( $bulk, array( 'optimize', 'retry' ), true ) && ! $s['public_policy'] && empty( $p['public_policy'] ) ) { self::fail( 'preview_public_required' ); } $expected += array( 'mode' => 'bulk', 'job_active' => true, 'action' => $bulk, ); if ( 'optimize' === $bulk ) { $expected['compression'] = $native['compression']; $expected['intent'] = $native['bulk_intent']; $expected['sizing'] = $native['sizing']; } if ( 'bulk_run' === $op ) { $native['ids'] = $native['source_ids'] ?? array(); $native['im_filter'] = $p['filter'] ?? ''; $native['im_search'] = $p['search'] ?? ''; $native['im_page'] = $p['page'] ?? 1; $selection = self::selection( $native ); if ( ! $selection ) { self::fail( 'reviewed_empty_selection' ); }$dependencies['selection'] = Reviewed_Store::hash( $selection ); $before['selection'] = $selection; } } } elseif ( str_starts_with( $op, 'workflow_' ) ) { $dependencies['job'] = self::job_state( 'image' ); $dependencies['image_job_hash'] = Reviewed_Store::hash( Workflow::job() ); $before['job'] = $dependencies['job']; if ( 'workflow_pause' === $op ) { $expected['paused'] = true; } elseif ( 'workflow_resume' === $op ) { $expected['paused'] = false; } elseif ( 'workflow_cancel' === $op ) { $expected['job_active'] = false; $expected['cancelled'] = true; $expected['paused'] = true; } else { $expected['batch_saved'] = true; $native['checkpoint_before'] = Reviewed_Store::raw( 'afsd_im_last_batch' ); } } elseif ( 'background_check' === $op ) { $expected['probe_queued'] = true; } elseif ( 'codec_check' === $op ) { $expected['codec_report_saved'] = true; } elseif ( str_starts_with( $op, 'source_' ) ) { if ( in_array( $op, array( 'source_optimize', 'source_retry' ), true ) && ! $s['public_policy'] ) { self::fail( 'preview_public_required' ); } if ( 'source_cleanup' === $op && $s['delivery'] ) { self::fail( 'reviewed_delivery_enabled' ); } $expected['sources'] = array(); foreach ( $native['source_ids'] as $id ) { $r = Preview::row( $id ); $change = array( 'id' => $id ); if ( in_array( $op, array( 'source_exclude', 'source_include' ), true ) ) { $change['excluded'] = 'source_exclude' === $op ? 1 : 0; } elseif ( in_array( $op, array( 'source_original', 'source_optimized' ), true ) ) { $change['delivery'] = 'source_optimized' === $op ? 1 : 0; } elseif ( 'source_cleanup' === $op ) { $change['copies'] = 0; } else { $change['requested'] = 1; $change['state'] = 'queued'; } $expected['sources'][] = $change; } } elseif ( str_starts_with( $op, 'comparison_' ) ) { $r = Preview::row( $native['source_id'] ); if ( ! $s['public_policy'] || $r['excluded'] || ! Sources::eligible( $r ) ) { self::fail( 'preview_public_required' ); } if ( 'comparison_prepare' === $op ) { if ( $r['requested'] ) { self::fail( 'preview_work_pending' ); }$native['compression'] = Policy::clean( array_merge( Policy::current( $r ), $p['compression'] ) ); $expected['comparison_requested'] = true; $expected['compression'] = $native['compression']; } else { $d = $r['manifest']['draft'] ?? array(); if ( ( $d['id'] ?? '' ) !== $p['draft'] ) { self::fail( 'preview_expired' ); }$before['comparison'] = array( 'draft' => $d['id'], 'recipe' => $d['recipe'] ?? array(), 'complete' => (bool) ( $d['complete'] ?? false ), ); if ( 'comparison_apply' === $op ) { if ( empty( $d['complete'] ) || ! empty( $d['requested'] ) || $r['requested'] ) { self::fail( 'preview_not_ready' ); }Preview::assert_ready( $r, $d ); $expected['state'] = 'ready'; $expected['compression'] = Policy::clean( $d['recipe'] ); $expected['published_copies'] = self::copies( $d ); } else { $expected['draft_absent'] = true; } } } elseif ( preg_match( '/^(folder|collection)_(create|rename|delete)$/', $op, $m ) ) { if ( 'delete' === $m[2] ) { $expected['term_absent'] = true; $before['term'] = get_term( $native['term_id'], $native['taxonomy'] )->name; } else { $name = trim( sanitize_text_field( $p['name'] ) ); if ( '' === $name || strlen( $name ) > 120 ) { self::fail( 'invalid_name' ); }$native['name'] = $name; $expected['term_name'] = $name; $expected['taxonomy'] = $native['taxonomy']; if ( 'create' === $m[2] ) { $native['parent_id'] = $native['parent_id'] ?? 0; $dependencies['name_absent'] = term_exists( $name, $native['taxonomy'], $native['parent_id'] ); if ( $dependencies['name_absent'] ) { self::fail( 'reviewed_term_exists' ); }$expected['parent'] = $native['parent_id']; } else { $before['term'] = get_term( $native['term_id'], $native['taxonomy'] )->name; } } } elseif ( preg_match( '/^(folder|collection)_(add|remove|set)$/', $op, $m ) ) { if ( ! $native['term_ids'] && 'set' !== $m[2] ) { self::fail( 'reviewed_empty_terms' ); }$expected['memberships'] = array(); foreach ( $native['attachment_ids'] as $id ) { $old = Library::term_ids( $id, $native['taxonomy'] ); sort( $old ); $after = 'set' === $m[2] ? $native['term_ids'] : ( 'remove' === $m[2] ? array_values( array_diff( $old, $native['term_ids'] ) ) : array_values( array_unique( array_merge( $old, $native['term_ids'] ) ) ) ); sort( $after ); $before['memberships'][] = array( 'attachment' => $id, 'terms' => $old, ); $expected['memberships'][] = array( 'attachment' => $id, 'terms' => $after, ); } } elseif ( 'metadata_update' === $op ) { if ( ! $p['fields'] ) { self::fail( 'reviewed_no_fields' ); }$fields = $p['fields']; foreach ( $fields as $k => $v ) { $fields[ $k ] = 'caption' === $k ? sanitize_textarea_field( $v ) : sanitize_text_field( $v ); }$native['fields'] = $fields; $expected['metadata'] = array(); foreach ( $native['attachment_ids'] as $id ) { $old = self::fields( $id ); $wanted = $fields; if ( ! str_starts_with( (string) get_post_mime_type( $id ), 'image/' ) ) { unset( $wanted['alt'] ); }$before['metadata'][] = array( 'attachment' => $id, 'fields' => array_intersect_key( $old, $wanted ), ); $expected['metadata'][] = array( 'attachment' => $id, 'fields' => $wanted, ); } } elseif ( 'media_upload' === $op ) { $native['folder_id'] = self::term( $p['folder'] ?? '', Library::FOLDER, true ); $expected['uploaded_count'] = count( $dependencies['input']['files'] ); $expected['folder'] = $native['folder_id']; $expected['file_hashes'] = array_column( $dependencies['input']['files'], 'sha256' ); } elseif ( 'edit_prepare' === $op ) { if ( ! Editor::supported( $native['attachment_id'] ) ) { self::fail( 'editor_source_invalid' ); }$native['editor'] = Editor::clean_recipe( $p['editor'] ); $info = Media_Actions::image_info( Media_Actions::source( $native['attachment_id'] )['path'] ); $recipe = $native['editor']; $w = $info['width']; $h = $info['height']; if ( in_array( $recipe['rotate'], array( 'cw90', 'ccw90' ), true ) ) { [$w, $h] = array( $h, $w ); }$box = Editor::crop_box( $w, $h, $recipe ); if ( $box ) { $w = $box[2]; $h = $box[3]; } // Larger or smaller, within the editor's size (Editor::resized() stops with editor_size_too_large). $resized = Editor::resized( $w, $h, $recipe['resize_width'], $recipe['resize_height'] ); if ( 'none' === $recipe['crop'] && 'none' === $recipe['rotate'] && 'none' === $recipe['flip'] && ! $resized ) { self::fail( 'editor_no_changes' ); }$expected['draft_recipe'] = $recipe; $expected['draft_ready'] = true; } elseif ( in_array( $op, array( 'edit_save', 'edit_discard', 'pdf_save', 'pdf_discard', 'pdf_copy_save', 'pdf_copy_discard', 'replace_apply', 'replace_discard' ), true ) ) { $kind = str_starts_with( $op, 'edit_' ) ? 'edit' : ( str_starts_with( $op, 'pdf_copy_' ) ? 'pdf_copy' : ( str_starts_with( $op, 'pdf_' ) ? 'pdf' : 'replace' ) ); $d = self::draft( $native['attachment_id'], $kind, $p['draft'] ); $dependencies['draft'] = Reviewed_Store::hash( $d ); if ( str_ends_with( $op, 'discard' ) ) { $expected['draft_absent'] = true; } elseif ( 'replace_apply' === $op ) { $expected['replacement_state'] = 'applied'; $expected['attachment_id'] = $native['attachment_id']; $expected['file_sha256'] = $d['image']['sha256']; $expected['recovery_ready'] = true; } elseif ( 'pdf_copy_save' === $op ) { // 1.9.0 (D-500 gap 15): a new PDF beside the source, the exact prepared copy. $post = get_post( $native['attachment_id'] ); $expected['new_attachment_saved'] = true; $expected['source_attachment'] = $native['attachment_id']; $expected['mime'] = 'application/pdf'; $expected['file_sha256'] = $d['image']['sha256']; $expected['caption'] = $post->post_excerpt; $expected['folders'] = Library::term_ids( $native['attachment_id'], Library::FOLDER ); $expected['collections'] = Library::term_ids( $native['attachment_id'], Library::COLLECTION ); } else { $expected['new_attachment_saved'] = true; $expected['source_attachment'] = $native['attachment_id']; $expected['mime'] = 'pdf_save' === $op ? 'image/png' : $d['mime']; $expected['width'] = 'pdf_save' === $op ? $d['image']['width'] : $d['width']; $expected['height'] = 'pdf_save' === $op ? $d['image']['height'] : $d['height']; $expected['file_sha256'] = 'pdf_save' === $op ? $d['image']['sha256'] : self::edit_hash( $d ); $post = get_post( $native['attachment_id'] ); $expected['caption'] = $post->post_excerpt; $expected['folders'] = Library::term_ids( $native['attachment_id'], Library::FOLDER ); $expected['collections'] = Library::term_ids( $native['attachment_id'], Library::COLLECTION ); if ( 'edit_save' === $op ) { $expected['description'] = $post->post_content; $expected['alt'] = (string) get_post_meta( $native['attachment_id'], '_wp_attachment_image_alt', true ); $expected['parent'] = (int) $post->post_parent; } } } elseif ( 'pdf_prepare' === $op ) { if ( ! PDF::available() ) { self::fail( 'media_pdf_backend' ); }Media_Actions::source( $native['attachment_id'], 'application/pdf' ); $expected['draft_ready'] = true; $expected['pdf_page'] = $p['page']; $expected['maximum_edge'] = $p['maximum_edge']; } elseif ( 'pdf_copy_prepare' === $op ) { if ( ! PDF_Copy::available() ) { self::fail( 'media_pdf_copy_backend' ); } Media_Actions::source( $native['attachment_id'], 'application/pdf' ); $expected['draft_ready'] = true; $expected['pdf_preset'] = $p['preset']; } elseif ( 'replace_prepare' === $op ) { $src = Media_Actions::source( $native['attachment_id'] ); $f = Reviewed_Inputs::file( $p['input_reference'], 'replacement' ); if ( $f['mime'] !== $src['mime'] ) { self::fail( 'media_replace_type' ); }$expected['draft_ready'] = true; $expected['file_sha256'] = $f['sha256']; } elseif ( in_array( $op, array( 'replace_undo', 'replace_recover' ), true ) ) { $r = Media_Replacement::revision( $native['attachment_id'] ); if ( ( $r['id'] ?? '' ) !== $p['draft'] || ! in_array( $r['state'] ?? '', array( 'applied', 'applying', 'undoing' ), true ) ) { self::fail( 'media_replace_old_changed' ); }$dependencies['replacement'] = Reviewed_Store::hash( $r ); $expected['replacement_state'] = 'replace_undo' === $op ? 'undone' : 'recovered'; $expected['attachment_id'] = $native['attachment_id']; } elseif ( 'backup_start' === $op ) { if ( Backup::job()['active'] || ( 'remote' === ( $p['destination'] ?? 'local' ) && Remote_Backup::job()['active'] ) ) { self::fail( 'backup_job_active' ); }if ( ! class_exists( '\ZipArchive' ) ) { self::fail( 'backup_zip_unavailable' ); } $native['backup_profile'] = $p['profile'] ?? 'essential'; $native['backup_scope'] = $p['scope'] ?? 'all'; $native['backup_scope_id'] = 'all' === $native['backup_scope'] ? 0 : self::term( $p['term'] ?? '', 'folder' === $native['backup_scope'] ? Library::FOLDER : Library::COLLECTION ); $native['backup_encrypt'] = $p['encrypted'] ?? false; if ( $native['backup_encrypt'] && ! isset( $p['secret_reference'] ) ) { self::fail( 'reviewed_password_required' ); } if ( 'remote' === ( $p['destination'] ?? 'local' ) && ! Remote_Backup::connection()['ready'] ) { self::fail( 'remote_not_ready' ); }$dependencies['backup_job'] = self::job_state( 'backup' ); $dependencies['backup_job_hash'] = Reviewed_Store::hash( Backup::job() ); $expected += array( 'job_active' => true, 'profile' => $native['backup_profile'], 'scope' => $native['backup_scope'], 'scope_id' => $native['backup_scope_id'], 'encrypted' => (bool) $native['backup_encrypt'], 'destination' => $p['destination'] ?? 'local', ); } elseif ( str_starts_with( $op, 'backup_' ) && in_array( substr( $op, 7 ), array( 'pause', 'resume', 'cancel', 'advance' ), true ) ) { $dependencies['backup_job'] = self::job_state( 'backup' ); $dependencies['backup_job_hash'] = Reviewed_Store::hash( Backup::job() ); $before['job'] = $dependencies['backup_job']; if ( ! Backup::job()['active'] ) { self::fail( 'reviewed_backup_inactive' ); } if ( 'backup_cancel' === $op ) { $expected['job_active'] = false; $expected['stage'] = 'cancelled'; } elseif ( 'backup_advance' === $op ) { $expected['batch_observed'] = true; } else { $expected['paused'] = 'backup_pause' === $op; } } elseif ( 'backup_delete' === $op ) { $expected['snapshot_absent'] = true; } elseif ( 'backup_restore' === $op ) { $r = Backup::record( $native['snapshot_id'], $p['portable_id'] ); if ( ! $r ) { self::fail( 'backup_record_missing' ); }$dependencies['record'] = Reviewed_Store::hash( $r ); $before['record'] = array( 'title' => $r['title'] ?? '', 'mime' => $r['mime'] ?? '', 'original_sha256' => $r['original']['sha256'] ?? '', ); if ( $snap['encrypted'] && ! isset( $p['secret_reference'] ) ) { self::fail( 'reviewed_password_required' ); }if ( 'in_place' === $p['mode'] ) { if ( ( $snap['site_uuid'] ?? '' ) !== Reviewed_Store::raw( Backup::SITE_OPTION, '' ) ) { self::fail( 'backup_cross_site_in_place_forbidden' ); }$targets['attachments'][] = (int) $r['original_attachment_id']; Media_Actions::authorize( (int) $r['original_attachment_id'] ); } $expected['restored'] = true; $expected['mode'] = $p['mode']; $expected['file_sha256'] = $r['original']['sha256']; $expected['mime'] = $r['mime']; $expected['title'] = sanitize_text_field( $r['title'] ?? '' ); $expected['caption'] = sanitize_textarea_field( $r['caption'] ?? '' ); $expected['alt'] = sanitize_text_field( $r['alt'] ?? '' ); } elseif ( 'backup_import' === $op ) { $expected['snapshot_imported'] = true; $expected['archive_sha256'] = $dependencies['input']['files'][0]['sha256']; } elseif ( 'remote_settings_update' === $op ) { $native = self::native_remote( $p ); $dependencies['remote_settings'] = self::option_token( Remote_Backup::SETTINGS_OPTION ); $expected['remote_settings'] = array_diff_key( $native, array( 'secret_reference' => true ) ); $before['remote_settings'] = self::native_remote( array() ); if ( $native['remote_schedule_encrypt'] && empty( Remote_Backup::settings()['schedule_password'] ) && ! isset( $p['secret_reference'] ) ) { self::fail( 'remote_schedule_password_required' ); } } elseif ( str_starts_with( $op, 'remote_' ) ) { $dependencies['remote_settings'] = self::option_token( Remote_Backup::SETTINGS_OPTION ); $dependencies['remote_job'] = self::job_state( 'remote' ); $dependencies['remote_job_hash'] = Reviewed_Store::hash( Remote_Backup::job() ); $before['remote_connection'] = Remote_Backup::connection(); if ( in_array( $op, array( 'remote_pause', 'remote_resume', 'remote_cancel', 'remote_advance' ), true ) && ! Remote_Backup::job()['active'] ) { self::fail( 'reviewed_remote_inactive' ); } if ( 'remote_google_disconnect' === $op ) { $expected['google_disconnected'] = true; } elseif ( 'remote_s3_test' === $op ) { $expected['s3_ready'] = true; $expected['test_cleanup_confirmed'] = true; } elseif ( 'remote_upload' === $op ) { if ( ! Remote_Backup::connection()['ready'] || Remote_Backup::job()['active'] ) { self::fail( 'remote_not_ready' ); }Backup::archive_info( $native['snapshot_id'] ); $expected['job_active'] = true; $expected['snapshot_id'] = $native['snapshot_id']; $expected['queued'] = true; } elseif ( 'remote_fetch' === $op ) { $expected['snapshot_imported'] = true; $expected['archive_sha256'] = $entry['sha256']; } elseif ( 'remote_delete' === $op ) { $expected['remote_absent'] = true; $expected['provider_delete_accepted'] = true; } elseif ( 'remote_cancel' === $op ) { $expected['job_active'] = false; $expected['stage'] = 'cancelled'; $j = Remote_Backup::job(); $expected['multipart_abort'] = ( 's3' === $j['provider'] && ! empty( $j['upload_id'] ) && ! empty( $j['remote_ref'] ) ) ? 'confirmed' : 'not_required'; } elseif ( 'remote_advance' === $op ) { $expected['batch_observed'] = true; } else { $expected['paused'] = 'remote_pause' === $op; } } elseif ( str_starts_with( $op, 'usage_' ) ) { $usage = Media_Usage::snapshot( $native['attachment_id'] ); $dependencies['usage'] = Reviewed_Store::hash( $usage ); $native['usage_run'] = $usage['run'] ?? ''; $native['usage_cursor'] = (int) ( $usage['cursor'] ?? 0 ); if ( 'usage_continue' === $op && 'more' !== ( $usage['status'] ?? '' ) ) { self::fail( 'reviewed_usage_complete' ); }$expected['usage_report_saved'] = true; $expected['attachment_id'] = $native['attachment_id']; } elseif ( 'delivery_check' === $op ) { $expected['delivery_report_saved'] = true; $expected['source_id'] = $native['source_id']; } elseif ( null !== self::extension( $op ) ) { call_user_func_array( self::extension( $op )['prepare'], array( $op, &$dependencies, &$expected ) ); } elseif ( 'intent_release' === $op ) { $record = Reviewed_Store::record( $p['marker'] ); if ( ! $record || $record['actor'] !== get_current_user_id() || $record['site'] !== Reviewed_Inputs::site() || ! in_array( $record['status'], array( 'executing', 'unknown', 'rejected' ), true ) ) { self::fail( 'reviewed_intent_not_uncertain' ); } if ( 'unknown' !== Reviewed_Store::project_state( $record['project_id'] ) ) { self::fail( 'reviewed_intent_still_active' ); }$dependencies['prior_intent'] = Reviewed_Store::hash( $record ); $before['uncertain_operation'] = array( 'marker' => $record['marker'], 'project' => $record['project_id'], 'operation' => $record['operation'], 'status' => $record['status'], 'evidence' => $record['evidence'], 'failure' => $record['failure_code'] ?? '', ); foreach ( $record['fences'] ?? array() as $key ) { if ( str_starts_with( $key, 'attachment:' ) ) { $targets['attachments'][] = (int) substr( $key, 11 ); }if ( str_starts_with( $key, 'source:' ) ) { $targets['sources'][] = (int) substr( $key, 7 ); } }$expected['reservation_released'] = true; $expected['prior_marker'] = $p['marker']; $expected['prior_outcome_unconfirmed'] = true; $native['prior_fences'] = $record['fences'] ?? array(); } elseif ( 'folder_gallery_show' === $op || 'folder_gallery_hide' === $op ) { $on = 'folder_gallery_show' === $op; wp_cache_delete( $native['term_id'], 'term_meta' ); $shown = Folder_Gallery::shown( $native['term_id'] ); if ( $shown === $on ) { self::fail( 'reviewed_gallery_unchanged' ); } $dependencies['website_gallery'] = $shown; $before['website_gallery'] = $shown; $native['website_gallery'] = $on; $expected['term_id'] = $native['term_id']; $expected['website_gallery'] = $on; } elseif ( 'folder_import_start' === $op || 'folder_import_continue' === $op ) { $key = $p['plugin']; $src = Folder_Import::sources()[ $key ] ?? null; if ( ! $src ) { self::fail( 'folder_import_source' ); } $state = Folder_Import::state(); $folders = Folder_Import::folders( $key ); $native['plugin'] = $key; $dependencies['import_state'] = Reviewed_Store::hash( $state ); $dependencies['import_folders'] = Reviewed_Store::hash( $folders ); $before['source_plugin'] = $src['name']; $before['saved_import'] = $state ? self::import_summary( $state ) : 'None'; if ( 'folder_import_start' === $op ) { if ( ! $folders ) { self::fail( 'folder_import_source' ); }if ( count( $folders ) > Folder_Import::MAX_FOLDERS ) { self::fail( 'folder_import_too_many' ); }if ( $state && 'more' === $state['status'] && $state['source'] !== $key ) { self::fail( 'folder_import_other' ); } $dependencies['import_placements'] = Folder_Import::pair_count( $key ); $before['folders_in_source'] = count( $folders ); $before['file_placements_in_source'] = $dependencies['import_placements']; $expected['tree_complete'] = true; } else { if ( ! $state || $state['source'] !== $key || 'more' !== $state['status'] ) { self::fail( 'folder_import_stale' ); }$native['position_before'] = (int) $state['cursor']; $expected['progress_advanced'] = true; } $expected['import_source'] = $key; $expected['import_saved'] = true; } elseif ( str_starts_with( $op, 'unused_' ) ) { $s = Unused_Media::state(); $native['previous'] = Reviewed_Store::hash( $s ); $dependencies['unused_state'] = $native['previous']; $before['saved_check'] = $s ? self::unused_summary( $s ) : 'None'; if ( 'unused_check_continue' === $op && ( ! $s || 'more' !== $s['status'] ) ) { self::fail( 'unused_stale' ); } if ( 'unused_collect' === $op ) { if ( ! $s || 'done' !== $s['status'] || ! $s['listed'] ) { self::fail( 'unused_stale' ); }$t = $s['collection'] ? get_term( (int) $s['collection'], Library::COLLECTION ) : null; if ( $t && ! is_wp_error( $t ) && (int) $s['collected'] >= count( $s['listed'] ) ) { self::fail( 'reviewed_unused_collected' ); } $expected['collection_saved'] = true; $expected['members_confirmed'] = true; } else { $expected['check_saved'] = true; $expected['progress_advanced'] = true; } } elseif ( str_starts_with( $op, 'content_folder_' ) ) { $type = (string) ( $p['post_type'] ?? '' ); if ( ! in_array( $type, Content_Folders::enabled(), true ) ) { self::fail( 'content_folder_type_off' ); } if ( ! Content_Folders::can_use( $type ) || ( 'content_folder_move' !== $op && ! Content_Folders::can_manage( $type ) ) ) { self::fail( 'content_folder_forbidden' ); } Content_Folders::forget(); $folders = Content_Folders::folders( $type ); $native['post_type'] = $type; $dependencies['content_folders'] = Reviewed_Store::hash( $folders ); $expected['post_type'] = $type; $name = isset( $p['name'] ) ? trim( sanitize_text_field( $p['name'] ) ) : ''; if ( isset( $p['name'] ) && ( '' === $name || strlen( $name ) > 120 ) ) { self::fail( 'content_folder_name' ); } $sibling = static function ( int $parent, string $n, int $self = 0 ) use ( $folders ): bool { foreach ( $folders as $f ) { if ( $f['id'] !== $self && $f['parent'] === $parent && 0 === strcasecmp( $f['name'], $n ) ) { return true; } }return false; }; if ( 'content_folder_create' === $op ) { $parent = self::content_folder( $folders, (string) ( $p['parent'] ?? '' ) ); if ( $sibling( $parent, $name ) ) { self::fail( 'content_folder_exists' ); }if ( count( $folders ) >= Content_Folders::MAX_FOLDERS ) { self::fail( 'content_folder_limit' ); } $native += array( 'name' => $name, 'parent_id' => $parent, ); $expected += array( 'folder_name' => $name, 'parent' => $parent, ); $before['folders_in_this_list'] = count( $folders ); $before['parent'] = $parent ? $folders[ $parent ]['path'] : 'The top level'; } elseif ( 'content_folder_move' === $op ) { $folder = self::content_folder( $folders, (string) $p['folder'] ); $ids = array(); foreach ( $p['items'] as $item ) { $ids[] = self::content_item( (string) $item, $type ); } $ids = array_values( array_unique( $ids ) ); sort( $ids, SORT_NUMERIC ); $native += array( 'folder_id' => $folder, 'item_ids' => $ids, ); $changed = 0; $expected['memberships'] = array(); $before['folder'] = $folder ? $folders[ $folder ]['path'] : 'Not in a folder'; foreach ( $ids as $id ) { $old = Content_Folders::folder_of( $id, $type ); if ( $old !== $folder ) { ++$changed; }$before['memberships'][] = array( 'item' => $id, 'title' => get_the_title( $id ), 'folder' => $old ? ( $folders[ $old ]['path'] ?? $old ) : 'Not in a folder', ); $expected['memberships'][] = array( 'item' => $id, 'folder' => $folder, ); } if ( ! $changed ) { self::fail( 'reviewed_content_folder_unchanged' ); }$dependencies['content_items'] = Reviewed_Store::hash( $before['memberships'] ); } else { $id = self::content_folder( $folders, (string) $p['folder'] ); if ( ! $id ) { self::fail( 'reviewed_term_missing' ); }$native['folder_id'] = $id; $before['folder'] = $folders[ $id ]['path']; $expected['folder_id'] = $id; if ( 'content_folder_rename' === $op ) { if ( $name === $folders[ $id ]['name'] ) { self::fail( 'reviewed_content_folder_unchanged' ); }if ( $sibling( $folders[ $id ]['parent'], $name, $id ) ) { self::fail( 'content_folder_exists' ); }$native['name'] = $name; $expected['folder_name'] = $name; } elseif ( 'content_folder_nest' === $op ) { $parent = self::content_folder( $folders, (string) $p['parent'] ); if ( $parent === $folders[ $id ]['parent'] ) { self::fail( 'reviewed_content_folder_unchanged' ); } for ( $q = $parent, $g = 0; $q && $g < 50; $q = $folders[ $q ]['parent'] ?? 0, $g++ ) { if ( $q === $id ) { self::fail( 'content_folder_cycle' ); } } if ( $sibling( $parent, $folders[ $id ]['name'], $id ) ) { self::fail( 'content_folder_exists' ); }$native['parent_id'] = $parent; $expected['parent'] = $parent; $before['new_parent'] = $parent ? $folders[ $parent ]['path'] : 'The top level'; } else { $expected['folder_absent'] = true; $before['items_in_folder'] = Content_Folders::counts( $type )['folders'][ $id ] ?? 0; } } } elseif ( 'fit_sizes' === $op ) { if ( Workflow::job()['active'] || Scanner::pending() ) { self::fail( 'reviewed_run_active' ); }if ( ! $s['public_policy'] ) { self::fail( 'preview_public_required' ); } $ids = Fit::measured_sources(); if ( ! $ids ) { self::fail( 'fit_nothing_measured' ); } $native['ids'] = $ids; $native['compression'] = Policy::current(); $selection = self::selection( array( 'scope' => 'selected', 'ids' => $ids, ) ); if ( ! $selection ) { self::fail( 'fit_nothing_measured' ); } $dependencies['job'] = self::job_state( 'image' ); $dependencies['image_job_hash'] = Reviewed_Store::hash( Workflow::job() ); $dependencies['selection'] = Reviewed_Store::hash( $selection ); $before['selection'] = $selection; $expected += array( 'mode' => 'bulk', 'job_active' => true, 'action' => 'optimize', 'sizing' => 'fit', 'compression' => $native['compression'], ); } elseif ( 'fit_forget' === $op ) { $index = Fit::index(); if ( ! $index ) { self::fail( 'fit_nothing_measured' ); }$dependencies['fit_index'] = Reviewed_Store::hash( $index ); $before['measured_pages'] = count( $index ); $expected['measurements_absent'] = true; } elseif ( 'cdn_check' === $op ) { $cdn = CDN::clean_url( (string) $p['address'] ); $deliver = (bool) $p['deliver']; if ( $deliver && '' === $cdn ) { self::fail( 'cdn_address' ); }if ( $deliver && ! $s['public_policy'] ) { self::fail( 'preview_public_required' ); }if ( $deliver && CDN::jetpack() ) { self::fail( 'cdn_jetpack' ); } if ( ! $deliver && ! $s['cdn'] && $s['cdn_url'] === $cdn ) { self::fail( 'reviewed_cdn_unchanged' ); } $native['address'] = $cdn; $native['deliver'] = $deliver; $before['cdn'] = array( 'on' => (bool) $s['cdn'], 'address' => (string) $s['cdn_url'], ); $dependencies['cdn'] = Reviewed_Store::hash( array( $s['cdn'], $s['cdn_url'], $s['cdn_state'] ) ); $expected += array( 'cdn_address' => $cdn, 'cdn_checked' => $deliver, ); if ( ! $deliver ) { $expected['cdn_on'] = false; } } elseif ( 'inspect' === $op ) { $expected['read_only'] = true; } else { self::fail( 'reviewed_unimplemented_action' ); } $attachment_states = array(); foreach ( array_values( array_unique( $targets['attachments'] ) ) as $id ) { $attachment_states[] = self::attachment_state( $id ); } $source_states = array(); foreach ( $targets['sources'] as $id ) { $ss = self::source_state( $id ); $source_states[] = $ss; if ( $ss['attachment_id'] && ! in_array( $ss['attachment_id'], $targets['attachments'], true ) ) { $targets['attachments'][] = $ss['attachment_id']; $attachment_states[] = self::attachment_state( $ss['attachment_id'] ); } } $term_states = array(); foreach ( $targets['terms'] as $t ) { $term = get_term( $t['id'], $t['taxonomy'] ); $term_states[] = array( 'id' => $t['id'], 'taxonomy' => $t['taxonomy'], 'name' => $term->name, 'parent' => (int) $term->parent, ); } $state = array( 'operation' => $op, 'environment' => self::environment(), 'targets' => $targets, 'attachments' => $attachment_states, 'sources' => $source_states, 'terms' => $term_states, 'dependencies' => $dependencies, 'native' => $native, ); if ( strlen( wp_json_encode( $state ) ) > 32768 || strlen( wp_json_encode( $expected ) ) > 32768 ) { self::fail( 'reviewed_state_limit' ); } return array( 'state' => $state, 'expected_after' => $expected, 'before' => $before, ); } /** * A fingerprint of a manifest's copies (each one's file, hash, source and type). * * @param array $manifest The manifest. * @return array The count and the fingerprint. */ private static function copies( array $manifest ): array { $copies = Variants::flatten( $manifest ); $rows = array(); foreach ( $copies as $v ) { $path = Settings::root() . '/' . $v['file']; $rows[] = array( 'file' => $v['file'], 'sha256' => is_file( $path ) && ! is_link( $path ) ? hash_file( 'sha256', $path ) : '', 'source' => $v['source_hash'] ?? '', 'mime' => Variants::mime( $v ), ); }return array( 'count' => count( $rows ), 'fingerprint' => Reviewed_Store::hash( $rows ), ); } /** * The hash of an edit draft's file in the private edits folder, its name checked first. * * @param array $d The edit draft. * @return string SHA-256. */ private static function edit_hash( array $d ): string { $name = $d['file'] ?? ''; if ( ! preg_match( '/^edit-[a-f0-9]{32}\.(jpg|png|webp|avif)$/D', $name ) ) { self::fail( 'editor_draft_missing' ); }$p = Private_Storage::directory( trailingslashit( get_temp_dir() ) . 'affstackdev-image-manager-edits', false ) . '/' . $name; if ( ! is_file( $p ) || is_link( $p ) ) { self::fail( 'editor_draft_missing' ); }return hash_file( 'sha256', $p ); } /** * The sources a bulk action selects: the chosen ones, or those an Images filter and search match (one page of 20, or * all). * * @param array $p The action's parameters. * @return array The count, a fingerprint and the first 20 sources, or an empty array. */ public static function selection( array $p ): array { global $wpdb; $scope = $p['scope'] ?? 'selected'; if ( 'selected' === $scope ) { $ids = array_map( 'intval', $p['ids'] ?? array() ); if ( ! $ids ) { return array(); } // Read current plugin source rows: writes happen in independent workers, and cached rows would misreport them. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- see the note above. $rows = $wpdb->get_results( $wpdb->prepare( 'SELECT id,revision,attachment_id,excluded,state,manifest,details FROM %i WHERE id IN (' . implode( ',', array_fill( 0, count( $ids ), '%d' ) ) . ") AND state<>'grouped' ORDER BY priority ASC,id ASC", array_merge( array( Repository::table( 'sources' ) ), $ids ) ), ARRAY_A ); $rows = is_array( $rows ) && ! $wpdb->last_error ? $rows : null; } else { $rows = Batch::select_matching( (string) ( $p['im_filter'] ?? '' ), (string) ( $p['im_search'] ?? '' ), 'page' === $scope ? ( (int) ( $p['im_page'] ?? 1 ) - 1 ) * 20 : null ); } if ( null === $rows ) { self::fail( 'reviewed_selection_failed' ); } // Bind the whole native all-page selection with a bounded digest and count; inspect pages reveal identities. $hash = hash_init( 'sha256' ); $samples = array(); foreach ( $rows as $r ) { hash_update( $hash, wp_json_encode( $r ) ); if ( count( $samples ) < 20 ) { $samples[] = array( 'source_id' => (int) $r['id'], 'attachment_id' => (int) $r['attachment_id'], 'title' => $r['attachment_id'] ? get_the_title( $r['attachment_id'] ) : 'Theme image #' . $r['id'], ); } } return $rows ? array( 'count' => count( $rows ), 'fingerprint' => hash_final( $hash ), 'first_page' => $samples, 'scope' => $scope, 'filter' => $p['im_filter'] ?? '', 'search' => $p['im_search'] ?? '', 'page' => $p['im_page'] ?? 1, ) : array(); } /** * What an execution locks: the attachments and sources it targets, or the owner area it changes. * * @param array $state The prepared state. * @return array Lock names, such as attachment:12, source:3 or owner:backup. */ public static function fences( array $state ): array { $op = $state['operation']; if ( 'intent_release' === $op ) { return $state['native']['prior_fences']; }$out = array(); foreach ( $state['targets']['attachments'] as $id ) { $out[] = 'attachment:' . $id; }foreach ( $state['targets']['sources'] as $id ) { $out[] = 'source:' . $id; } if ( str_starts_with( $op, 'unused_' ) ) { $out[] = 'owner:unused'; if ( 'unused_collect' === $op ) { $out[] = 'owner:organization'; } } if ( ! $out ) { $out[] = 'owner:' . ( self::extension( $op )['owner'] ?? ( str_starts_with( $op, 'backup_' ) ? 'backup' : ( str_starts_with( $op, 'remote_' ) ? 'remote' : ( preg_match( '/^(folder|collection)_/', $op ) ? 'organization' : ( str_starts_with( $op, 'content_folder_' ) ? 'content_folders' : ( 'cdn_check' === $op ? 'cdn' : ( 'fit_forget' === $op ? 'fit' : 'workflow' ) ) ) ) ) ) ); } return array_values( array_unique( $out ) ); } /** * Run an approved action through Image Manager's own code. * * @param array $prepared The prepared change. * @param array $context The execution's context (its marker). * @return mixed What the execution produced, for observe(). */ public static function apply( array $prepared, array $context ) { $op = $prepared['snapshot']['state']['operation']; $p = $prepared['snapshot']['state']['native']; $e = array(); Reviewed_Store::guard(); if ( 'settings_update' === $op ) { $before = Settings::fresh(); $values = $p['settings']; $values['generation'] = $before['generation'] + 1; Settings::patch( $values ); Reviewed_Store::guard(); Auto_Discovery::configured( $before ); Reviewed_Store::guard(); Engine::ensure(); } elseif ( 'scan_start' === $op ) { Workflow::start( false, false, false ); } elseif ( 'optimization_start' === $op || 'bulk_run' === $op ) { $p += array( 'bulk_confirm' => true, 'compression_confirm' => true, 'resize_confirm' => true, ); $e['selected'] = Batch::start( $p, 'optimization_start' === $op ); $e['job_id'] = Workflow::job()['id']; } elseif ( 'workflow_pause' === $op ) { Workflow::pause(); } elseif ( 'workflow_resume' === $op ) { Workflow::resume(); } elseif ( 'workflow_cancel' === $op ) { Workflow::cancel(); } elseif ( 'workflow_advance' === $op ) { Workflow::run_batch(); $e['batch'] = Reviewed_Store::raw( 'afsd_im_last_batch' ); } elseif ( 'background_check' === $op ) { Scheduler::check( true ); $e['health'] = Reviewed_Store::raw( Scheduler::HEALTH ); } elseif ( 'codec_check' === $op ) { $e['capabilities'] = Compression::capabilities( true ); } elseif ( str_starts_with( $op, 'source_' ) ) { self::source_change( substr( $op, 7 ), $p['source_ids'] ); } elseif ( 'comparison_prepare' === $op ) { Preview::start( $p['source_id'], $p['compression'], true ); } elseif ( 'comparison_apply' === $op ) { Preview::apply( $p['source_id'], $p['draft'], true ); } elseif ( 'comparison_discard' === $op ) { Preview::discard( $p['source_id'], $p['draft'] ); } elseif ( preg_match( '/^(folder|collection)_(create|rename|delete)$/', $op, $m ) ) { if ( 'create' === $m[2] ) { $result = Library::create_term( $p['taxonomy'], $p['name'], $p['parent_id'] ?? 0 ); if ( is_wp_error( $result ) ) { return $result; }$e['term_id'] = (int) $result['term_id']; } else { if ( ! Library::change_term( $p['taxonomy'], $p['term_id'], $m[2], $p['name'] ?? '' ) ) { self::fail( 'reviewed_term_failed' ); }$e['term_id'] = $p['term_id']; } } elseif ( preg_match( '/^(folder|collection)_(add|remove|set)$/', $op, $m ) ) { foreach ( $p['attachment_ids'] as $id ) { Reviewed_Store::guard(); if ( 'remove' === $m[2] ) { $n = Library::detach( array( $id ), $p['taxonomy'], $p['term_ids'] ); } else { $n = Library::assign( array( $id ), $p['taxonomy'], $p['term_ids'], 'set' === $m[2] ); }if ( $n !== 1 ) { self::fail( 'reviewed_membership_failed' ); } } } elseif ( 'metadata_update' === $op ) { $native = array(); foreach ( $p['fields'] as $k => $v ) { $native[ 'set_' . $k ] = true; $native[ 'media_' . $k ] = $v; }foreach ( $p['attachment_ids'] as $id ) { Reviewed_Store::guard(); if ( Library::update_metadata( array( $id ), $native ) !== 1 ) { self::fail( 'reviewed_metadata_failed' ); } } } elseif ( 'media_upload' === $op ) { $e = Reviewed_Inputs::upload( $p['input_reference'], $p['folder_id'], $context['marker'] ); } elseif ( 'edit_prepare' === $op ) { $d = Editor::prepare( $p['attachment_id'], $p['editor'], true ); $e['draft'] = $d['id']; } elseif ( 'edit_save' === $op ) { $e['attachment_id'] = Editor::save_copy( $p['attachment_id'], $p['draft'], true ); } elseif ( 'edit_discard' === $op ) { Editor::discard( $p['attachment_id'], $p['draft'] ); } elseif ( 'pdf_prepare' === $op ) { $d = PDF::prepare( $p['attachment_id'], $p['page'], $p['maximum_edge'], true ); $e['draft'] = $d['id']; } elseif ( 'pdf_save' === $op ) { $e['attachment_id'] = PDF::save( $p['attachment_id'], $p['draft'], true ); } elseif ( 'pdf_discard' === $op ) { Media_Draft::discard( $p['attachment_id'], 'convert', $p['draft'] ); } elseif ( 'pdf_copy_prepare' === $op ) { $d = PDF_Copy::prepare( $p['attachment_id'], $p['preset'], true ); $e['draft'] = $d['id']; } elseif ( 'pdf_copy_save' === $op ) { $e['attachment_id'] = PDF_Copy::save( $p['attachment_id'], $p['draft'], true ); } elseif ( 'pdf_copy_discard' === $op ) { Media_Draft::discard( $p['attachment_id'], 'compress', $p['draft'] ); } elseif ( 'replace_prepare' === $op ) { $d = Reviewed_Inputs::replacement( $p['attachment_id'], $p['input_reference'], $context['marker'] ); $e['draft'] = $d['id']; } elseif ( 'replace_apply' === $op ) { $e['attachment_id'] = Media_Replacement::apply( $p['attachment_id'], $p['draft'], true ); } elseif ( 'replace_undo' === $op || 'replace_recover' === $op ) { Media_Replacement::undo( $p['attachment_id'], $p['draft'], true, 'replace_recover' === $op ); } elseif ( 'replace_discard' === $op ) { Media_Draft::discard( $p['attachment_id'], 'replace', $p['draft'] ); } elseif ( 'backup_start' === $op ) { if ( ! empty( $p['backup_encrypt'] ) ) { $p['backup_password'] = self::password( $p, $context ); $p['backup_password_confirm'] = $p['backup_password']; }if ( 'remote' === ( $p['destination'] ?? 'local' ) ) { Remote_Backup::start_backup( $p ); } else { Backup::start( $p ); }$e['job_id'] = Backup::job()['id']; } elseif ( 'backup_pause' === $op ) { Backup::pause(); } elseif ( 'backup_resume' === $op ) { Backup::resume(); } elseif ( 'backup_cancel' === $op ) { Backup::cancel(); } elseif ( 'backup_advance' === $op ) { Backup::work( 'browser' ); $e['job'] = self::safe_job( Backup::job() ); } elseif ( 'backup_delete' === $op ) { Backup::delete_snapshot( $p['snapshot_id'] ); } elseif ( 'backup_restore' === $op ) { $password = isset( $p['secret_reference'] ) ? self::password( $p, $context ) : ''; $e['attachment_id'] = Backup::restore( $p['snapshot_id'], $p['portable_id'], $p['mode'], $password, true ); } elseif ( 'backup_import' === $op ) { Reviewed_Inputs::consume( $p['input_reference'], 'backup_import', $context['marker'] ); $file = Reviewed_Inputs::file( $p['input_reference'], 'backup_import' ); $snap = Backup::import_file_path( $file['path'], isset( $p['secret_reference'] ) ? self::password( $p, $context ) : '', 'reviewed_human_upload' ); $e['snapshot_id'] = $snap['id']; } elseif ( 'remote_settings_update' === $op ) { if ( isset( $p['secret_reference'] ) ) { Reviewed_Inputs::consume( $p['secret_reference'], 'remote_credentials', $context['marker'] ); $p = array_merge( $p, Reviewed_Inputs::secret( $p['secret_reference'], 'remote_credentials' ) ); }Remote_Backup::save_settings( $p ); } elseif ( 'remote_google_disconnect' === $op ) { Remote_Backup::disconnect_google(); } elseif ( 'remote_s3_test' === $op ) { $r = Remote_Backup::test_s3(); $e['connection'] = $r; $e['cleanup'] = Reviewed_Store::raw( 'afsd_im_s3_test_cleanup', array() ); } elseif ( 'remote_upload' === $op ) { Remote_Backup::queue_existing( $p['snapshot_id'] ); $e['job_id'] = Remote_Backup::job()['id']; } elseif ( 'remote_pause' === $op ) { Remote_Backup::pause(); } elseif ( 'remote_resume' === $op ) { Remote_Backup::resume(); } elseif ( 'remote_cancel' === $op ) { Remote_Backup::cancel(); } elseif ( 'remote_advance' === $op ) { Remote_Backup::work( 'browser' ); $e['job'] = self::safe_job( Remote_Backup::job() ); } elseif ( 'remote_fetch' === $op ) { $snap = Remote_Backup::fetch_local( $p['remote_id'], isset( $p['secret_reference'] ) ? self::password( $p, $context ) : '' ); $e['snapshot_id'] = $snap['id']; } elseif ( 'remote_delete' === $op ) { Remote_Backup::delete_remote( $p['remote_id'], true ); $e['provider_delete_accepted'] = true; } elseif ( 'usage_start' === $op || 'usage_continue' === $op ) { $e['report'] = Media_Usage::step( $p['attachment_id'], 'usage_start' === $op ? 'start' : 'continue', $p['usage_run'], $p['usage_cursor'] ); } elseif ( 'delivery_check' === $op ) { $e['report'] = Verification::check_row( Preview::row( $p['source_id'] ) ); } elseif ( null !== self::extension( $op ) ) { $done = call_user_func_array( self::extension( $op )['execute'], array( $op, $p, $context, &$e ) ); if ( is_wp_error( $done ) ) { return $done; } } elseif ( 'folder_gallery_show' === $op || 'folder_gallery_hide' === $op ) { if ( ! Folder_Gallery::set_shown( $p['term_id'], 'folder_gallery_show' === $op ) ) { self::fail( 'reviewed_gallery_failed' ); }$e['term_id'] = $p['term_id']; } elseif ( 'folder_import_start' === $op || 'folder_import_continue' === $op ) { $s = Folder_Import::step( $p['plugin'], 'folder_import_start' === $op, true ); $e['state'] = Reviewed_Store::hash( $s ); $e['status'] = $s['status']; } elseif ( 'unused_check_start' === $op || 'unused_check_continue' === $op ) { $s = Unused_Media::step( 'unused_check_start' === $op ); $e['state'] = Reviewed_Store::hash( $s ); $e['status'] = $s['status']; } elseif ( 'unused_collect' === $op ) { $s = Unused_Media::collect(); $e['state'] = Reviewed_Store::hash( $s ); $e['collection'] = (int) $s['collection']; } elseif ( 'content_folder_create' === $op ) { $e['folder_id'] = Content_Folders::create( $p['post_type'], $p['name'], $p['parent_id'] ); } elseif ( 'content_folder_rename' === $op ) { Content_Folders::rename( $p['post_type'], $p['folder_id'], $p['name'] ); } elseif ( 'content_folder_nest' === $op ) { Content_Folders::nest( $p['post_type'], $p['folder_id'], $p['parent_id'] ); } elseif ( 'content_folder_delete' === $op ) { Content_Folders::delete( $p['post_type'], $p['folder_id'] ); } elseif ( 'content_folder_move' === $op ) { foreach ( $p['item_ids'] as $id ) { Reviewed_Store::guard(); Content_Folders::move( $p['post_type'], array( $id ), $p['folder_id'] ); } } elseif ( 'fit_sizes' === $op ) { $e['selected'] = Batch::start( array( 'bulk' => 'optimize', 'scope' => 'selected', 'ids' => $p['ids'], 'sizing' => 'fit', 'resize_confirm' => true, 'bulk_confirm' => true, 'compression_confirm' => true, 'compression' => $p['compression'], 'bulk_intent' => 'apply', ) ); $e['job_id'] = Workflow::job()['id']; } elseif ( 'fit_forget' === $op ) { $e['forgotten'] = Fit::forget(); } elseif ( 'cdn_check' === $op ) { $e['state'] = CDN::save( $p['address'], $p['deliver'] ); } elseif ( 'intent_release' === $op ) { Reviewed_Store::release_intent( $p['marker'], $context['marker'] ); $e['released_by'] = $context['marker']; } elseif ( 'inspect' === $op ) { $e['read_only'] = true; } else { self::fail( 'reviewed_unimplemented_action' ); } return $e; } /** * The backup password from its staged input, claimed for this execution. * * @param array $p The action's parameters. * @param array $context The execution's context. * @return string The password. */ private static function password( array $p, array $context ): string { Reviewed_Inputs::consume( $p['secret_reference'], 'backup_password', $context['marker'] ); return Reviewed_Inputs::secret( $p['secret_reference'], 'backup_password' )['value']; } /** * A backup job's fields an action may show. * * @param array $j The job. * @return array Its ID, state, progress, last error and update time. */ private static function safe_job( array $j ): array { return array_intersect_key( $j, array_fill_keys( array( 'id', 'active', 'paused', 'stage', 'offset', 'processed', 'selected', 'skipped', 'last_error', 'updated' ), true ) ); } /** * Apply a source action to sources under the worker's lease: optimize, retry, use originals or copies, clean up, * exclude or include. * * @param string $action The action. * @param array $ids The source rows. */ public static function source_change( string $action, array $ids ): void { $lease = Repository::acquire(); if ( ! $lease ) { self::fail( 'worker_busy' ); } try { foreach ( $ids as $id ) { Reviewed_Store::guard(); $r = Preview::row( $id ); if ( 'optimize' === $action ) { Repository::request( $id ); } elseif ( 'retry' === $action ) { Engine::retry( $id ); } elseif ( in_array( $action, array( 'original', 'optimized' ), true ) ) { Repository::update( $id, array( 'delivery' => 'optimized' === $action ? 1 : 0 ), (int) $r['revision'] ); } elseif ( 'cleanup' === $action ) { Engine::invalidate_source( $id, true ); Reviewed_Store::guard(); Repository::update( $id, array( 'state' => 'discovered', 'reason' => 'Managed copies removed or recorded for cleanup review. Original unchanged.', ) ); } else { $rev = (int) $r['revision'] + 1; $m = $r['manifest']; if ( isset( $m['revision'] ) ) { $m['revision'] = $rev; }Repository::update( $id, array( 'excluded' => 'exclude' === $action ? 1 : 0, 'state' => 'exclude' === $action ? 'excluded' : ( ! empty( $m['variants'] ) ? 'ready' : 'discovered' ), 'revision' => $rev, 'requested' => 0, 'manifest' => $m, ), (int) $r['revision'] ); } } } finally { Repository::release( $lease ); } } /** * Read back what an executed action did, in the shape of its expected result. * * @param array $prepared The prepared change. * @param array $e What apply() produced. * @return array The observed result. */ public static function observe( array $prepared, array $e ): array { $op = $prepared['snapshot']['state']['operation']; $p = $prepared['snapshot']['state']['native']; $out = array( 'operation' => $op ); if ( 'settings_update' === $op ) { $out['settings'] = array_intersect_key( self::settings(), $p['settings'] ); } elseif ( in_array( $op, array( 'scan_start', 'optimization_start', 'bulk_run' ), true ) ) { $j = Workflow::job(); $out += array( 'mode' => $j['mode'], 'job_active' => (bool) $j['active'], 'job_id' => $j['id'], ); if ( 'bulk' === $j['mode'] ) { $out['action'] = $j['action']; $out['compression'] = $j['recipe']; $out['intent'] = $j['intent']; $out['sizing'] = $j['sizing']; } } elseif ( str_starts_with( $op, 'workflow_' ) ) { $j = Workflow::job(); $out += array( 'paused' => (bool) self::settings()['paused'], 'job_active' => (bool) $j['active'], 'cancelled' => (bool) $j['cancelled'], ); if ( 'workflow_advance' === $op ) { $out['batch_saved'] = is_array( $e['batch'] ?? null ) && Reviewed_Store::raw( 'afsd_im_last_batch' ) === $e['batch']; $out['batch'] = $e['batch'] ?? array(); } } elseif ( 'background_check' === $op ) { $out['probe_queued'] = is_array( $e['health'] ?? null ) && Reviewed_Store::raw( Scheduler::HEALTH ) === $e['health'] && ( ( ( $e['health']['backend'] ?? '' ) === 'WP-Cron' && wp_next_scheduled( Scheduler::PROBE, array( $e['health']['token'], $e['health']['backend'] ) ) !== false ) || ( ( $e['health']['backend'] ?? '' ) === 'Action Scheduler' && function_exists( 'as_has_scheduled_action' ) && as_has_scheduled_action( Scheduler::PROBE, array( $e['health']['token'], $e['health']['backend'] ), Engine::GROUP ) ) ); } elseif ( 'codec_check' === $op ) { $out['codec_report_saved'] = is_array( $e['capabilities'] ?? null ) && Compression::capabilities() === $e['capabilities']; } elseif ( str_starts_with( $op, 'source_' ) ) { $out['sources'] = array(); foreach ( $p['source_ids'] as $id ) { $r = Repository::row( $id ); $v = array( 'id' => $id ); if ( in_array( $op, array( 'source_exclude', 'source_include' ), true ) ) { $v['excluded'] = (int) $r['excluded']; } elseif ( in_array( $op, array( 'source_original', 'source_optimized' ), true ) ) { $v['delivery'] = (int) $r['delivery']; } elseif ( 'source_cleanup' === $op ) { $v['copies'] = count( Variants::flatten( $r['manifest'], true ) ); } else { $v['requested'] = (int) $r['requested']; $v['state'] = $r['state']; }$out['sources'][] = $v; } } elseif ( str_starts_with( $op, 'comparison_' ) ) { $r = Repository::row( $p['source_id'] ); $d = $r['manifest']['draft'] ?? array(); if ( 'comparison_prepare' === $op ) { $out['comparison_requested'] = ! empty( $d['requested'] ); $out['compression'] = $d['recipe'] ?? array(); $out['draft'] = $d['id'] ?? ''; } elseif ( 'comparison_apply' === $op ) { $out['state'] = $r['state']; $out['compression'] = Policy::current( $r ); $out['published_copies'] = self::copies( $r['manifest'] ); } else { $out['draft_absent'] = ! isset( $d['id'] ); } } elseif ( preg_match( '/^(folder|collection)_(create|rename|delete)$/', $op, $m ) ) { $id = (int) ( $e['term_id'] ?? $p['term_id'] ?? 0 ); clean_term_cache( $id, $p['taxonomy'] ); $t = get_term( $id, $p['taxonomy'] ); $out['term_id'] = $id; if ( 'delete' === $m[2] ) { $out['term_absent'] = ! $t || is_wp_error( $t ); } elseif ( $t && ! is_wp_error( $t ) ) { $out['term_name'] = $t->name; $out['taxonomy'] = $t->taxonomy; $out['parent'] = (int) $t->parent; } } elseif ( preg_match( '/^(folder|collection)_(add|remove|set)$/', $op ) ) { $out['memberships'] = array(); foreach ( $p['attachment_ids'] as $id ) { clean_object_term_cache( $id, 'attachment' ); $terms = Library::term_ids( $id, $p['taxonomy'] ); sort( $terms ); $out['memberships'][] = array( 'attachment' => $id, 'terms' => $terms, ); } } elseif ( 'metadata_update' === $op ) { $out['metadata'] = array(); foreach ( $p['attachment_ids'] as $id ) { clean_post_cache( $id ); wp_cache_delete( $id, 'post_meta' ); $wanted = $p['fields']; if ( ! str_starts_with( (string) get_post_mime_type( $id ), 'image/' ) ) { unset( $wanted['alt'] ); }$out['metadata'][] = array( 'attachment' => $id, 'fields' => array_intersect_key( self::fields( $id ), $wanted ), ); } } elseif ( 'media_upload' === $op ) { $out['uploaded_count'] = count( $e['ids'] ?? array() ); $out['folder'] = $p['folder_id']; $out['file_hashes'] = array(); $out['attachment_ids'] = $e['ids'] ?? array(); foreach ( $out['attachment_ids'] as $id ) { Media_Actions::authorize( $id ); $path = function_exists( 'wp_get_original_image_path' ) ? wp_get_original_image_path( $id ) : get_attached_file( $id ); $path = $path ?: get_attached_file( $id ); $out['file_hashes'][] = is_string( $path ) && is_file( $path ) ? hash_file( 'sha256', $path ) : ''; if ( $p['folder_id'] && ! in_array( $p['folder_id'], Library::term_ids( $id, Library::FOLDER ), true ) ) { $out['folder'] = -1; } } } elseif ( in_array( $op, array( 'edit_prepare', 'pdf_prepare', 'replace_prepare' ), true ) ) { $id = $p['attachment_id']; $d = 'edit_prepare' === $op ? (array) get_post_meta( $id, Editor::DRAFT_META, true ) : Media_Draft::get( $id, 'pdf_prepare' === $op ? 'convert' : 'replace' ); $out['draft_ready'] = ( $d['id'] ?? '' ) === ( $e['draft'] ?? '' ) && '' !== ( $d['id'] ?? '' ); $out['draft'] = $d['id'] ?? ''; if ( 'edit_prepare' === $op ) { $out['draft_recipe'] = $d['recipe'] ?? array(); self::edit_hash( $d ); } elseif ( 'pdf_prepare' === $op ) { Media_Draft::path( $d ); $out['pdf_page'] = $d['recipe']['page'] ?? 0; $out['maximum_edge'] = $d['recipe']['maximum_edge'] ?? 0; } else { Media_Draft::path( $d ); $out['file_sha256'] = $d['image']['sha256'] ?? ''; } } elseif ( in_array( $op, array( 'edit_save', 'pdf_save' ), true ) ) { $id = (int) ( $e['attachment_id'] ?? 0 ); Media_Actions::authorize( $id ); $post = get_post( $id ); $meta = wp_get_attachment_metadata( $id ); $path = get_attached_file( $id ); $out += array( 'new_attachment_saved' => $id > 0 && 'attachment' === get_post_type( $id ), 'source_attachment' => (int) get_post_meta( $id, Editor::SOURCE_META, true ), 'mime' => $post->post_mime_type, 'width' => (int) ( $meta['width'] ?? 0 ), 'height' => (int) ( $meta['height'] ?? 0 ), 'file_sha256' => is_string( $path ) && is_file( $path ) ? hash_file( 'sha256', $path ) : '', 'attachment_id' => $id, 'caption' => $post->post_excerpt, 'description' => $post->post_content, 'alt' => (string) get_post_meta( $id, '_wp_attachment_image_alt', true ), 'parent' => (int) $post->post_parent, 'folders' => Library::term_ids( $id, Library::FOLDER ), 'collections' => Library::term_ids( $id, Library::COLLECTION ), ); } elseif ( 'pdf_copy_prepare' === $op ) { $d = Media_Draft::get( $p['attachment_id'], 'compress' ); $out['draft_ready'] = ( $d['id'] ?? '' ) === ( $e['draft'] ?? '' ) && '' !== ( $d['id'] ?? '' ); $out['draft'] = $d['id'] ?? ''; if ( $out['draft_ready'] ) { Media_Draft::path( $d ); } $out['pdf_preset'] = $d['recipe']['preset'] ?? ''; } elseif ( 'pdf_copy_save' === $op ) { $id = (int) ( $e['attachment_id'] ?? 0 ); Media_Actions::authorize( $id ); $post = get_post( $id ); $path = get_attached_file( $id ); $out += array( 'new_attachment_saved' => $id > 0 && 'attachment' === get_post_type( $id ), 'source_attachment' => (int) get_post_meta( $id, Editor::SOURCE_META, true ), 'mime' => $post->post_mime_type, 'file_sha256' => is_string( $path ) && is_file( $path ) ? hash_file( 'sha256', $path ) : '', 'attachment_id' => $id, 'caption' => $post->post_excerpt, 'folders' => Library::term_ids( $id, Library::FOLDER ), 'collections' => Library::term_ids( $id, Library::COLLECTION ), ); } elseif ( 'pdf_copy_discard' === $op ) { $out['draft_absent'] = empty( Media_Draft::get( $p['attachment_id'], 'compress' )['id'] ); } elseif ( in_array( $op, array( 'edit_discard', 'pdf_discard', 'replace_discard' ), true ) ) { $d = 'edit_discard' === $op ? (array) get_post_meta( $p['attachment_id'], Editor::DRAFT_META, true ) : Media_Draft::get( $p['attachment_id'], 'pdf_discard' === $op ? 'convert' : 'replace' ); $out['draft_absent'] = empty( $d['id'] ); } elseif ( in_array( $op, array( 'replace_apply', 'replace_undo', 'replace_recover' ), true ) ) { $r = Media_Replacement::revision( $p['attachment_id'] ); $out['replacement_state'] = $r['state'] ?? ''; $out['attachment_id'] = $p['attachment_id']; if ( 'replace_apply' === $op ) { $out['file_sha256'] = Media_Actions::source( $p['attachment_id'] )['sha256']; $snap = Backup::snapshot( $r['backup'] ?? '' ); $out['recovery_ready'] = $snap && Backup::archive_info( $snap['id'] )['sha256'] === $snap['sha256']; } } elseif ( 'backup_start' === $op ) { $j = Backup::job(); $out += array( 'job_active' => (bool) $j['active'], 'profile' => $j['profile'], 'scope' => $j['scope'], 'scope_id' => (int) $j['scope_id'], 'encrypted' => (bool) $j['encrypted'], 'destination' => Reviewed_Store::raw( Remote_Backup::WAIT_OPTION, array() )['backup_job_id'] ?? null, ); $out['destination'] = $out['destination'] === $j['id'] ? 'remote' : 'local'; $out['job_id'] = $j['id']; } elseif ( in_array( $op, array( 'backup_pause', 'backup_resume', 'backup_cancel', 'backup_advance' ), true ) ) { $j = Backup::job(); $out += array( 'job_active' => (bool) $j['active'], 'stage' => $j['stage'], 'paused' => (bool) $j['paused'], ); if ( 'backup_advance' === $op ) { $out['batch_observed'] = isset( $e['job'] ) && self::safe_job( $j ) === $e['job']; $out['job'] = $e['job'] ?? array(); } } elseif ( 'backup_delete' === $op ) { $out['snapshot_absent'] = null === Backup::snapshot( $p['snapshot_id'] ); } elseif ( 'backup_restore' === $op ) { $id = (int) ( $e['attachment_id'] ?? 0 ); $path = get_attached_file( $id ); $f = self::fields( $id ); $out += array( 'restored' => 'attachment' === get_post_type( $id ), 'mode' => $p['mode'], 'file_sha256' => is_string( $path ) && is_file( $path ) ? hash_file( 'sha256', $path ) : '', 'mime' => (string) get_post_mime_type( $id ), 'title' => $f['title'], 'caption' => $f['caption'], 'alt' => $f['alt'], 'attachment_id' => $id, ); } elseif ( in_array( $op, array( 'backup_import', 'remote_fetch' ), true ) ) { $snap = Backup::snapshot( $e['snapshot_id'] ?? '' ); $out['snapshot_imported'] = null !== $snap; $out['archive_sha256'] = $snap ? Backup::archive_info( $snap['id'] )['sha256'] : ''; $out['snapshot_id'] = $snap['id'] ?? ''; } elseif ( 'remote_settings_update' === $op ) { $out['remote_settings'] = self::native_remote( array() ); } elseif ( 'remote_google_disconnect' === $op ) { $g = Remote_Backup::settings()['google']; $out['google_disconnected'] = empty( $g['refresh_token'] ) && empty( $g['folder_id'] ) && 0 === $g['connected_at']; } elseif ( 'remote_s3_test' === $op ) { $out['s3_ready'] = Remote_Backup::connection()['ready']; $out['test_cleanup_confirmed'] = true === ( $e['cleanup']['confirmed'] ?? false ); } elseif ( 'remote_upload' === $op ) { $j = Remote_Backup::job(); $out += array( 'job_active' => (bool) $j['active'], 'snapshot_id' => $j['snapshot_id'], 'queued' => in_array( $j['stage'], array( 'init', 'uploading', 'completing', 'verifying' ), true ), 'job_id' => $j['id'], ); } elseif ( in_array( $op, array( 'remote_pause', 'remote_resume', 'remote_cancel', 'remote_advance' ), true ) ) { $j = Remote_Backup::job(); $out += array( 'job_active' => (bool) $j['active'], 'paused' => (bool) $j['paused'], 'stage' => $j['stage'], ); if ( 'remote_cancel' === $op ) { $out['multipart_abort'] = $j['multipart_abort'] ?? ''; }if ( 'remote_advance' === $op ) { $out['batch_observed'] = isset( $e['job'] ) && self::safe_job( $j ) === $e['job']; $out['job'] = $e['job'] ?? array(); } } elseif ( 'remote_delete' === $op ) { $out['remote_absent'] = null === Remote_Backup::entry( $p['remote_id'] ); $out['provider_delete_accepted'] = true === ( $e['provider_delete_accepted'] ?? false ); } elseif ( str_starts_with( $op, 'usage_' ) ) { $r = Media_Usage::snapshot( $p['attachment_id'] ); $out['usage_report_saved'] = $r === ( $e['report'] ?? null ) && ! empty( $r['run'] ); $out['attachment_id'] = (int) ( $r['id'] ?? 0 ); $out['checked'] = (int) ( $r['checked'] ?? 0 ); $out['status'] = $r['status'] ?? ''; } elseif ( 'delivery_check' === $op ) { $r = Repository::row( $p['source_id'] ); $saved = $r['details']['delivery_check'] ?? array(); $out['delivery_report_saved'] = ! empty( $saved ) && ( $saved['signature'] ?? '' ) === Verification::signature( $r ); $out['source_id'] = $p['source_id']; $out['report'] = array_intersect_key( $saved, array_fill_keys( array( 'time', 'status', 'kind', 'matches' ), true ) ); } elseif ( null !== self::extension( $op ) ) { $out = array_merge( $out, (array) call_user_func( self::extension( $op )['verify'], $e ) ); } elseif ( 'folder_gallery_show' === $op || 'folder_gallery_hide' === $op ) { $id = (int) $p['term_id']; wp_cache_delete( $id, 'term_meta' ); $out['term_id'] = $id; $out['website_gallery'] = Folder_Gallery::shown( $id ); } elseif ( 'folder_import_start' === $op || 'folder_import_continue' === $op ) { $s = Folder_Import::state(); $out['import_source'] = (string) ( $s['source'] ?? '' ); $out['import_saved'] = $s && Reviewed_Store::hash( $s ) === ( $e['state'] ?? '' ) && (int) ( $s['actor'] ?? 0 ) === get_current_user_id(); $out += self::import_summary( $s ); // Every folder of the other plugin has its Image Manager folder, and each of those exists now. if ( 'folder_import_start' === $op ) { $map = array_values( array_unique( array_map( 'intval', (array) ( $s['map'] ?? array() ) ) ) ); $found = $map ? get_terms( array( 'taxonomy' => Library::FOLDER, 'include' => $map, 'hide_empty' => false, 'fields' => 'ids', 'number' => 0, ) ) : array(); $out['tree_complete'] = $map && is_array( $found ) && count( $found ) === count( $map ) && ! array_diff_key( Folder_Import::folders( $p['plugin'] ), (array) $s['map'] ); } else { $out['progress_advanced'] = $s && ( (int) $s['cursor'] > (int) $p['position_before'] || 'done' === $s['status'] ); } } elseif ( str_starts_with( $op, 'unused_' ) ) { $s = Unused_Media::state(); $saved = $s && Reviewed_Store::hash( $s ) === ( $e['state'] ?? '' ) && (int) ( $s['actor'] ?? 0 ) === get_current_user_id(); $out += self::unused_summary( $s ); // Each listed file put in so far that still exists and may be edited is in the collection. if ( 'unused_collect' === $op ) { $id = (int) ( $s['collection'] ?? 0 ); $t = $id ? get_term( $id, Library::COLLECTION ) : null; $out['collection_saved'] = $saved && $t && ! is_wp_error( $t ); $done = array_map( 'intval', array_slice( (array) ( $s['listed'] ?? array() ), 0, (int) ( $s['collected'] ?? 0 ) ) ); $members = $id ? get_objects_in_term( $id, Library::COLLECTION ) : array(); $members = is_array( $members ) ? array_flip( array_map( 'intval', $members ) ) : array(); $missing = 0; if ( $done ) { _prime_post_caches( $done, false, false ); }foreach ( $done as $a ) { if ( 'attachment' === get_post_type( $a ) && current_user_can( 'edit_post', $a ) && ! isset( $members[ $a ] ) ) { ++$missing; } }$out['members_confirmed'] = $id > 0 && count( $done ) > 0 && 0 === $missing; } else { $out['check_saved'] = $saved; $out['progress_advanced'] = $saved && Reviewed_Store::hash( $s ) !== $p['previous']; } } elseif ( str_starts_with( $op, 'content_folder_' ) ) { Content_Folders::forget(); $type = $p['post_type']; $folders = Content_Folders::folders( $type ); $out['post_type'] = $type; if ( 'content_folder_create' === $op ) { $id = (int) ( $e['folder_id'] ?? 0 ); $out['folder_id'] = $id; $out['folder_name'] = $folders[ $id ]['name'] ?? null; $out['parent'] = $folders[ $id ]['parent'] ?? null; } elseif ( 'content_folder_move' === $op ) { $out['memberships'] = array(); clean_object_term_cache( $p['item_ids'], $type ); foreach ( $p['item_ids'] as $id ) { $out['memberships'][] = array( 'item' => $id, 'folder' => Content_Folders::folder_of( $id, $type ), ); } } else { $id = $p['folder_id']; $out['folder_id'] = $id; if ( 'content_folder_delete' === $op ) { $out['folder_absent'] = ! isset( $folders[ $id ] ); } elseif ( 'content_folder_rename' === $op ) { $out['folder_name'] = $folders[ $id ]['name'] ?? null; } else { $out['parent'] = $folders[ $id ]['parent'] ?? null; } } } elseif ( 'fit_sizes' === $op ) { $j = Workflow::job(); $out += array( 'mode' => $j['mode'], 'job_active' => (bool) $j['active'], 'job_id' => $j['id'], 'action' => $j['action'] ?? '', 'sizing' => $j['sizing'] ?? '', 'compression' => $j['recipe'] ?? array(), ); } elseif ( 'fit_forget' === $op ) { $out['measurements_absent'] = ! Fit::index() && ! Fit::measured_sources(); } elseif ( 'cdn_check' === $op ) { $st = Settings::fresh(); $state = is_array( $st['cdn_state'] ) ? $st['cdn_state'] : array(); $out['cdn_address'] = (string) $st['cdn_url']; $out['cdn_checked'] = $p['deliver'] && (int) ( $state['checked'] ?? 0 ) > 0 && (int) ( $state['checked'] ?? 0 ) === (int) ( $e['state']['checked'] ?? -1 ) && ( $state['url'] ?? '' ) === $p['address']; $out['cdn_on'] = (bool) $st['cdn']; $out['check_result'] = (string) ( $state['reason'] ?? '' ); } elseif ( 'intent_release' === $op ) { $r = Reviewed_Store::record( $p['marker'] ); $out['reservation_released'] = ( $r['released_by'] ?? '' ) === ( $e['released_by'] ?? null ); $out['prior_marker'] = $p['marker']; $out['prior_outcome_unconfirmed'] = in_array( $r['status'] ?? '', array( 'executing', 'unknown', 'rejected' ), true ); } elseif ( 'inspect' === $op ) { $out['read_only'] = true; } return $out; } /** * Read one section of Image Manager's state for an AI (settings, workflow, sources, library, drafts, backups, remote, * health, inputs, imports, unused, content folders, fit, CDN, intent or a section an extension adds), in pages. * * @param array $p The inspection's parameters. * @return array The section's state. */ public static function inspect( array $p ): array { $section = $p['section'] ?? 'settings'; $limit = $p['limit'] ?? 10; $offset = $p['offset'] ?? 0; $query = $p['query'] ?? ''; global $wpdb; if ( 'settings' === $section ) { return array( 'settings' => self::settings(), 'compression_choices' => array( 'mode' => array( 'lossless', 'high', 'maximum' ), 'content' => array( 'auto', 'photo', 'graphics' ), 'formats' => array( 'auto', 'native', 'webp', 'avif' ), 'effort' => array( 'fast', 'balanced' ), 'metadata' => array( 'preserve', 'remove_gps', 'remove_all' ), ), 'large_pictures_choices' => array_keys( Settings::LARGE ), 'upload_limit_choices' => Upload_Limit::CHOICES, 'largest_picture_now' => Settings::largest_now(), ); } if ( 'workflow' === $section ) { return array( 'image' => self::job_state( 'image' ), 'local_backup' => self::safe_job( Backup::job() ), 'remote_transfer' => self::safe_job( Remote_Backup::job() ), 'progress' => (array) Reviewed_Store::raw( Progress::OPTION, array() ), ); } if ( 'sources' === $section ) { $rows = Batch::matching_ids( $query, $limit + 1, $offset ); $items = array(); foreach ( array_slice( $rows, 0, $limit ) as $r ) { try { $items[] = self::source_state( (int) $r['id'] ); } catch ( \Throwable $e ) { // A source the current user may not read is left out of the page. } }return array( 'items' => $items, 'offset' => $offset, 'next_offset' => count( $rows ) > $limit ? $offset + $limit : null, ); } if ( 'library' === $section ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- the title search is prepared; the listing must be live. $rows = $wpdb->get_col( $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE post_type='attachment' AND post_status<>'trash' AND post_title LIKE %s ORDER BY ID LIMIT %d OFFSET %d", '%' . $wpdb->esc_like( $query ) . '%', $limit + 1, $offset ) ); $items = array(); foreach ( array_slice( $rows, 0, $limit ) as $id ) { if ( current_user_can( 'edit_post', $id ) ) { $v = self::attachment_state( (int) $id ); $v['metadata'] = self::fields( (int) $id ); $v['folders'] = Library::term_ids( (int) $id, Library::FOLDER ); $v['collections'] = Library::term_ids( (int) $id, Library::COLLECTION ); $items[] = $v; } }$terms = array(); $more = count( $rows ) > $limit; foreach ( array( Library::FOLDER, Library::COLLECTION ) as $tax ) { $all = Library::terms( $tax ); $more = $more || count( $all ) > $offset + $limit; $terms[ $tax ] = array_map( static fn( $t ) => array( 'id' => (int) $t->term_id, 'name' => $t->name, 'parent' => (int) $t->parent, ) + ( Library::FOLDER === $tax ? array( 'website_gallery' => Folder_Gallery::shown( (int) $t->term_id ) ) : array() ), array_slice( $all, $offset, $limit ) ); }return array( 'items' => $items, 'terms' => $terms, 'offset' => $offset, 'next_offset' => $more ? $offset + $limit : null, ); } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- bounded live media listing for reviewed-action inspection if ( 'drafts' === $section ) { $id = self::attachment( $p['attachment'] ?? '' ); $edit = (array) get_post_meta( $id, Editor::DRAFT_META, true ); $replace = Media_Draft::get( $id, 'replace' ); $pdf = Media_Draft::get( $id, 'convert' ); $safe = static fn( $d ) => array_intersect_key( $d, array_fill_keys( array( 'id', 'state', 'created', 'width', 'height', 'mime', 'recipe', 'image' ), true ) ); return array( 'attachment' => $id, 'edit' => $safe( $edit ), 'replacement' => $safe( $replace ), 'pdf' => $safe( $pdf ), 'pdf_copy' => $safe( Media_Draft::get( $id, 'compress' ) ), 'replacement_revision' => array_intersect_key( Media_Replacement::revision( $id ), array_fill_keys( array( 'id', 'state', 'created', 'backup' ), true ) ), ); } if ( 'backups' === $section ) { $items = array_slice( Backup::index(), $offset, $limit ); foreach ( $items as &$r ) { unset( $r['archive'], $r['records'], $r['header'] ); }unset( $r ); $out = array( 'items' => $items, 'offset' => $offset, 'next_offset' => count( Backup::index() ) > $offset + $limit ? $offset + $limit : null, 'format' => Backup::FORMAT, 'format_version' => Backup::FORMAT_VERSION, ); if ( isset( $p['snapshot'] ) ) { $id = self::snapshot_id( $p['snapshot'] ); $page = Backup::record_page( $id, (int) floor( $offset / $limit ) + 1, $limit ); $out['records'] = array_map( static fn( $r ) => array( 'portable_id' => $r['portable_id'], 'title' => $r['title'], 'mime' => $r['mime'], 'original_attachment_id' => $r['original_attachment_id'], 'original' => $r['original'], 'organization' => $r['organization'] ?? array(), ), $page['items'] ); $out['record_total'] = $page['total']; }return $out; } if ( 'remote' === $section ) { $s = Remote_Backup::settings(); return array( 'connection' => Remote_Backup::connection(), 'settings' => self::native_remote( array() ), 'credential_readiness' => array( 'google_client_secret' => ! empty( $s['google']['client_secret'] ) || defined( 'AFSD_IM_GOOGLE_CLIENT_SECRET' ), 's3_access_key' => ! empty( $s['s3']['access_key'] ) || defined( 'AFSD_IM_S3_ACCESS_KEY' ), 's3_secret_key' => ! empty( $s['s3']['secret_key'] ) || defined( 'AFSD_IM_S3_SECRET_KEY' ), 'scheduled_password' => ! empty( $s['schedule_password'] ), ), 'items' => array_map( static fn( $r ) => array_intersect_key( $r, array_fill_keys( array( 'id', 'provider', 'snapshot_id', 'name', 'bytes', 'sha256', 'uploaded', 'integrity', 'encrypted' ), true ) ), array_slice( Remote_Backup::index(), $offset, $limit ) ), 'next_offset' => count( Remote_Backup::index() ) > $offset + $limit ? $offset + $limit : null, ); } foreach ( self::extensions() as $extension ) { if ( ( $extension['section'] ?? null ) === $section && is_callable( $extension['inspect'] ?? null ) ) { return (array) call_user_func( $extension['inspect'] ); } } if ( 'health' === $section ) { return array( 'plugin' => VERSION, 'php' => PHP_VERSION, 'codec_backend' => Codec::backend(), 'pdf_backend' => PDF::available(), 'pdf_copy_backend' => PDF_Copy::available(), 'zip' => class_exists( '\ZipArchive' ), 'reviewed_database' => Reviewed_Store::supported(), 'memory_budget' => Settings::memory_budget(), 'scheduler' => Engine::scheduler(), 'background_check' => (array) Reviewed_Store::raw( Scheduler::HEALTH, array() ), 'backup_storage_ready' => ! empty( Backup::storage_info( false )['available'] ), 'delivery_integrations' => Integrations::matrix(), ); } if ( 'inputs' === $section ) { return array( 'secure_native_entry' => Reviewed_Inputs::url(), 'file_reference_lifetime' => '24 hours', 'secret_reference_lifetime' => '10 minutes', 'actor_bound' => true, 'site_bound' => true, 'secrets_returned' => false, ); } if ( 'imports' === $section ) { $s = Folder_Import::state(); return array( 'sources' => array_map( static fn( $v ) => $v['name'], Folder_Import::sources() ), 'found' => Folder_Import::found(), 'saved_import' => $s ? self::import_summary( $s ) : null, ); } if ( 'unused' === $section ) { $s = Unused_Media::state(); if ( ! $s ) { return array( 'saved_check' => null, 'items' => array(), 'offset' => $offset, 'next_offset' => null, ); }$listed = array_map( 'intval', (array) $s['listed'] ); $items = array(); foreach ( array_slice( $listed, $offset, $limit ) as $id ) { if ( 'attachment' === get_post_type( $id ) && current_user_can( 'edit_post', $id ) ) { $items[] = array( 'id' => $id, 'title' => get_the_title( $id ), 'mime' => (string) get_post_mime_type( $id ), 'file' => (string) get_post_meta( $id, '_wp_attached_file', true ), ); } }return array( 'saved_check' => self::unused_summary( $s ), 'items' => $items, 'offset' => $offset, 'next_offset' => count( $listed ) > $offset + $limit ? $offset + $limit : null, ); } if ( 'content_folders' === $section ) { $types = Content_Folders::available(); $on = Content_Folders::enabled(); $flat = array(); foreach ( $on as $type ) { $counts = Content_Folders::counts( $type ); foreach ( Content_Folders::folders( $type ) as $f ) { $flat[] = array( 'post_type' => $type, 'id' => $f['id'], 'name' => $f['name'], 'path' => $f['path'], 'parent' => $f['parent'], 'items' => $counts['folders'][ $f['id'] ] ?? 0, ); } } $lists = array(); foreach ( $on as $type ) { $c = Content_Folders::counts( $type ); $lists[ $type ] = array( 'label' => $types[ $type ] ?? $type, 'items' => $c['all'], 'not_in_a_folder' => $c['none'], 'can_manage' => Content_Folders::can_manage( $type ), ); } return array( 'available_types' => $types, 'switched_on' => $on, 'lists' => $lists, 'folders' => array_slice( $flat, $offset, $limit ), 'offset' => $offset, 'next_offset' => count( $flat ) > $offset + $limit ? $offset + $limit : null, ); } if ( 'fit' === $section ) { $r = Fit::report(); $list = array_slice( $r['list'], $offset, $limit ); unset( $r['list'] ); return array( 'fit_switched_on' => (bool) self::settings()['fit'], 'summary' => $r, 'pages' => $list, 'pages_to_measure' => count( Fit::pages() ), 'measured_pictures' => count( Fit::measured_sources() ), 'offset' => $offset, 'next_offset' => $r['pages'] > $offset + $limit ? $offset + $limit : null, 'measuring' => 'Measuring runs in the store owner\'s browser on the Optimize tab (Sizes for the page); it is not an AI action.', ); } if ( 'cdn' === $section ) { $st = self::settings(); return array( 'cdn_switched_on' => (bool) $st['cdn'], 'address' => (string) $st['cdn_url'], 'last_check' => is_array( $st['cdn_state'] ) ? $st['cdn_state'] : array(), 'in_use' => CDN::active(), 'jetpack_image_cdn' => CDN::jetpack(), ); } if ( 'intent' === $section ) { $r = Reviewed_Store::record( $p['marker'] ?? '' ); if ( ! $r || $r['actor'] !== get_current_user_id() || $r['site'] !== Reviewed_Inputs::site() ) { self::fail( 'reviewed_intent_missing' ); }return array_intersect_key( $r, array_fill_keys( array( 'marker', 'project_id', 'operation', 'status', 'started', 'finished', 'failure_code', 'result' ), true ) ); } self::fail( 'reviewed_unknown_section' ); } }