post id. */ public const MEDIA_OPTION = 'cryptoway_button_paid_media'; /** Done once the posts that existed before this version were indexed. */ public const SCAN_OPTION = 'cryptoway_button_paid_scan'; /** Block attributes that name a media file, per block. */ private const MEDIA_ATTRS = [ 'core/image' => [ 'id' ], 'core/file' => [ 'id' ], 'core/video' => [ 'id' ], 'core/audio' => [ 'id' ], 'core/cover' => [ 'id' ], 'core/media-text' => [ 'mediaId' ], 'core/gallery' => [ 'ids' ], ]; private function __construct() {} public static function register(): void { add_action( 'wp_after_insert_post', [ self::class, 'index_post' ], 10, 2 ); add_action( 'deleted_post', [ self::class, 'forget_post' ] ); add_filter( 'posts_search', [ self::class, 'search' ] ); add_filter( 'rest_attachment_query', [ self::class, 'media_query' ] ); add_filter( 'rest_request_before_callbacks', [ self::class, 'media_item' ], 10, 3 ); add_action( 'template_redirect', [ self::class, 'attachment_page' ], 1 ); add_action( 'admin_init', [ self::class, 'scan_existing' ] ); add_filter( 'enclosure_links', [ self::class, 'enclosure_links' ], 10, 2 ); add_filter( 'rss_enclosure', [ self::class, 'feed_enclosure' ] ); add_filter( 'atom_enclosure', [ self::class, 'feed_enclosure' ] ); add_filter( 'render_block_core/latest-posts', [ self::class, 'latest_posts' ] ); add_filter( 'oembed_request_post_id', [ self::class, 'oembed_post_id' ] ); } // ------------------------------------------------------------------ the index /** Keep the searchable copy and the paid media of a saved post up to date. */ public static function index_post( $post_id, $post ): void { if ( ! $post instanceof \WP_Post || wp_is_post_revision( $post ) || wp_is_post_autosave( $post ) ) { return; } $content = (string) $post->post_content; $public = self::public_copy( $content ); $media = null === $public ? [] : self::paid_media( $content ); if ( null === $public ) { delete_post_meta( (int) $post_id, self::META_PUBLIC ); delete_post_meta( (int) $post_id, self::META_MEDIA ); } else { // Meta values are unslashed on the way in. update_post_meta( (int) $post_id, self::META_PUBLIC, wp_slash( $public ) ); update_post_meta( (int) $post_id, self::META_MEDIA, $media ); if ( 'yes' !== get_option( self::FLAG ) ) { update_option( self::FLAG, 'yes', true ); } } self::set_media_of( (int) $post_id, $media ); } public static function forget_post( $post_id ): void { self::set_media_of( (int) $post_id, [] ); } /** * The media files of a paid part drawn outside post content (a template part, a widget), kept * under the button's key while Button::remember_paid() keeps the part itself; null forgets them. * * @param array{kind: string, raw: string}|null $paid */ public static function remember_media( string $key, ?array $paid ): void { $ids = []; if ( null !== $paid ) { $raw = (string) ( $paid['raw'] ?? '' ); if ( 'blocks' === ( $paid['kind'] ?? '' ) ) { self::collect_media( parse_blocks( $raw ), true, $ids ); } elseif ( preg_match_all( '/wp-image-(\d+)|attachment_id=(\d+)|\bids="([\d,\s]+)"/', $raw, $found, PREG_SET_ORDER ) ) { foreach ( $found as $f ) { foreach ( preg_split( '/[\s,]+/', implode( ',', array_slice( $f, 1 ) ) ) ?: [] as $id ) { $ids[] = (int) $id; } } } $ids = array_merge( $ids, self::ids_from_urls( wp_extract_urls( $raw ) ) ); } self::set_media_of( 'b:' . md5( $key ), array_values( array_unique( array_filter( $ids ) ) ) ); } /** * Record which hidden media files belong to an owner, replacing what it had before. * * @param int|string $owner A post id, or 'b:'. */ private static function set_media_of( $owner, array $media ): void { $all = get_option( self::MEDIA_OPTION, [] ); $all = is_array( $all ) ? $all : []; $old = $all; $owner = (string) $owner; $all = array_filter( $all, static fn( $held_by ): bool => (string) $held_by !== $owner ); foreach ( $media as $attachment_id ) { $all[ (int) $attachment_id ] = is_numeric( $owner ) ? (int) $owner : $owner; } if ( $all !== $old ) { update_option( self::MEDIA_OPTION, $all, false ); } } /** * The content with every paid part emptied, or null when it has none: the inner blocks of the * button block and the text a [cryptoway] shortcode encloses (inner blocks of the button are * never shown otherwise, whatever "After payment" says). */ public static function public_copy( string $content ): ?string { $changed = false; if ( str_contains( $content, '