post_type ) { return; } $key = 'dadan_instance_save_notices_' . get_current_user_id(); $notices = get_transient( $key ); if ( ! is_array( $notices ) || empty( $notices ) ) { return; } delete_transient( $key ); foreach ( $notices as $notice ) { $message = isset( $notice['message'] ) ? (string) $notice['message'] : ''; if ( '' === $message ) { continue; } $type = isset( $notice['type'] ) ? (string) $notice['type'] : 'error'; printf( '

%2$s

', esc_attr( $type ), esc_html( $message ) ); } } /** * CPT for instance list + edit screens. */ public function register_post_type() { register_post_type( self::POST_TYPE, array( 'labels' => array( 'name' => __( 'Dadan Instances', 'dadan' ), 'singular_name' => __( 'Dadan Instance', 'dadan' ), 'add_new' => __( 'Add Instance', 'dadan' ), 'add_new_item' => __( 'Add Dadan Instance', 'dadan' ), 'edit_item' => __( 'Edit Dadan Instance', 'dadan' ), 'new_item' => __( 'New Dadan Instance', 'dadan' ), 'view_item' => __( 'View Dadan Instance', 'dadan' ), 'search_items' => __( 'Search Instances', 'dadan' ), 'not_found' => __( 'No instances found.', 'dadan' ), 'not_found_in_trash' => __( 'No instances found in Trash.', 'dadan' ), 'menu_name' => __( 'Instances', 'dadan' ), 'all_items' => __( 'Instances', 'dadan' ), ), 'public' => false, 'show_ui' => true, 'show_in_menu' => 'dadan', 'show_in_rest' => false, 'capability_type' => 'post', 'map_meta_cap' => false, 'capabilities' => array( 'edit_post' => 'manage_options', 'read_post' => 'manage_options', 'delete_post' => 'manage_options', 'edit_posts' => 'manage_options', 'edit_others_posts' => 'manage_options', 'publish_posts' => 'manage_options', 'read_private_posts' => 'manage_options', 'delete_posts' => 'manage_options', 'delete_private_posts' => 'manage_options', 'delete_published_posts' => 'manage_options', 'delete_others_posts' => 'manage_options', 'edit_private_posts' => 'manage_options', 'edit_published_posts' => 'manage_options', 'create_posts' => 'manage_options', ), 'hierarchical' => false, 'supports' => array( 'title' ), 'has_archive' => false, 'rewrite' => false, 'query_var' => false, 'can_export' => true, 'delete_with_user' => false, ) ); } /** * Ensure "Instances" sits where Settings used to. */ public function relabel_submenu() { global $submenu; if ( empty( $submenu['dadan'] ) || ! is_array( $submenu['dadan'] ) ) { return; } foreach ( $submenu['dadan'] as &$item ) { if ( isset( $item[2] ) && 'edit.php?post_type=' . self::POST_TYPE === $item[2] ) { $item[0] = __( 'Instances', 'dadan' ); } } unset( $item ); } /** * One-time: move legacy option into a Default instance. */ public static function maybe_migrate() { // Already migrated: do not re-run on every admin_init (DADAN-17). if ( get_option( self::MIGRATED_OPT ) ) { return; } // First-run create must never happen on a front-end GET (DADAN-17). // Activation fires "activate_{basename}" (not "activate_plugin", which has // already finished by then); CLI activate is also allowed. $activating = doing_action( 'activate_' . plugin_basename( DADAN_VIDEO_FILE ) ) || ( defined( 'WP_CLI' ) && WP_CLI ); if ( ! $activating && ( ! is_admin() || ! current_user_can( 'manage_options' ) ) ) { return; } $stored = get_option( Dadan_Settings::OPTION_KEY, array() ); if ( ! is_array( $stored ) ) { $stored = array(); } $existing = get_posts( array( 'post_type' => self::POST_TYPE, 'post_status' => array( 'publish', 'draft', 'pending', 'private' ), 'posts_per_page' => 1, 'fields' => 'ids', ) ); if ( empty( $existing ) ) { // Same lock as ensure_default_exists(): concurrent admin requests must not both insert. if ( ! self::acquire_create_lock() ) { return; } $post_id = wp_insert_post( array( 'post_type' => self::POST_TYPE, 'post_status' => 'publish', 'post_title' => __( 'Default', 'dadan' ), ), true ); delete_option( self::CREATE_LOCK_OPT ); if ( ! is_wp_error( $post_id ) && $post_id ) { // DADAN-09: re-sanitize legacy option values before storing. $settings = Dadan_Settings::sanitize_array( array_merge( Dadan_Settings::defaults(), $stored ) ); update_post_meta( $post_id, self::META_KEY, $settings ); update_option( self::DEFAULT_OPT, (int) $post_id, false ); } } else { update_option( self::DEFAULT_OPT, (int) $existing[0], false ); if ( ! empty( $stored ) ) { $current = get_post_meta( (int) $existing[0], self::META_KEY, true ); if ( ! is_array( $current ) || empty( $current ) ) { update_post_meta( (int) $existing[0], self::META_KEY, Dadan_Settings::sanitize_array( array_merge( Dadan_Settings::defaults(), $stored ) ) ); } } } update_option( self::MIGRATED_OPT, 1, false ); } /** * Atomic lock for creating the Default instance (add_option() fails if the row exists). * A lock older than a minute is left over from a crashed request and is taken over. * * @return bool */ private static function acquire_create_lock() { if ( add_option( self::CREATE_LOCK_OPT, time(), '', false ) ) { return true; } $since = (int) get_option( self::CREATE_LOCK_OPT, 0 ); if ( time() - $since < MINUTE_IN_SECONDS ) { return false; } delete_option( self::CREATE_LOCK_OPT ); return add_option( self::CREATE_LOCK_OPT, time(), '', false ); } /** * If all instances were deleted, recreate an empty Default. */ public static function ensure_default_exists() { $default = (int) get_option( self::DEFAULT_OPT, 0 ); // DADAN-17: trashed Default must not stay active. if ( $default && self::POST_TYPE === get_post_type( $default ) && 'trash' !== get_post_status( $default ) ) { return; } $existing = get_posts( array( 'post_type' => self::POST_TYPE, 'post_status' => array( 'publish', 'private' ), 'posts_per_page' => 1, 'orderby' => 'ID', 'order' => 'ASC', 'fields' => 'ids', ) ); if ( ! empty( $existing ) ) { update_option( self::DEFAULT_OPT, (int) $existing[0], false ); return; } // Only create from admin with manage_options — never front-end / cron (DADAN-17). if ( ! is_admin() || ! current_user_can( 'manage_options' ) ) { return; } if ( ! self::acquire_create_lock() ) { return; } $post_id = wp_insert_post( array( 'post_type' => self::POST_TYPE, 'post_status' => 'publish', 'post_title' => __( 'Default', 'dadan' ), ), true ); delete_option( self::CREATE_LOCK_OPT ); if ( ! is_wp_error( $post_id ) && $post_id ) { update_post_meta( $post_id, self::META_KEY, Dadan_Settings::defaults() ); update_option( self::DEFAULT_OPT, (int) $post_id, false ); } } /** * Default instance ID (0 if none). * * @return int */ public static function default_id() { self::maybe_migrate(); $id = (int) get_option( self::DEFAULT_OPT, 0 ); if ( $id && self::POST_TYPE === get_post_type( $id ) && 'trash' !== get_post_status( $id ) ) { return $id; } self::ensure_default_exists(); $id = (int) get_option( self::DEFAULT_OPT, 0 ); if ( $id && 'trash' === get_post_status( $id ) ) { return 0; } return $id; } /** * Resolve a shortcode/block id (empty → default). * * @param int|string $id Raw id. * @return int */ public static function resolve_id( $id ) { if ( '' === $id || null === $id || false === $id ) { return self::default_id(); } $id = absint( $id ); if ( $id <= 0 ) { return self::default_id(); } $status = get_post_status( $id ); if ( self::POST_TYPE === get_post_type( $id ) && in_array( $status, array( 'publish', 'private' ), true ) ) { return $id; } // Explicit but invalid / draft / trash — do not fall back (DADAN-11). return 0; } /** * Merged settings for an instance. * * @param int $post_id Instance post ID. * @return array */ public static function get_settings( $post_id ) { $post_id = (int) $post_id; $stored = get_post_meta( $post_id, self::META_KEY, true ); if ( ! is_array( $stored ) ) { $stored = array(); } if ( ! isset( $stored['cta_preset'] ) && isset( $stored['trigger_style'] ) ) { if ( 'text' === $stored['trigger_style'] ) { $stored['cta_preset'] = 'custom'; $stored['cta_variant'] = 'button'; if ( ! empty( $stored['trigger_label'] ) ) { $stored['cta_label'] = $stored['trigger_label']; } } else { $stored['cta_preset'] = 'dadan'; } } return array_merge( Dadan_Settings::defaults(), $stored ); } /** * One-time: turn System / tab audio on for existing Instances that never set it * (old default was off) so the browser share dialog can show mute / “Share tab audio”. * * NEW-3: admin_init only, add_option lock, never overwrite an explicit choice. */ public static function maybe_enable_system_audio_default() { if ( ! is_admin() || ! current_user_can( 'manage_options' ) ) { return; } // Claim lock first (add_option is atomic); skip if already done or in progress. if ( ! add_option( 'dadan_sys_audio_default_on_v1', 'running', '', false ) ) { return; } $posts = get_posts( array( 'post_type' => self::POST_TYPE, 'post_status' => 'any', 'posts_per_page' => -1, 'fields' => 'ids', 'nopaging' => true, ) ); foreach ( $posts as $pid ) { $meta = get_post_meta( (int) $pid, self::META_KEY, true ); if ( ! is_array( $meta ) ) { $meta = array(); } // Preserve explicit on/off; only fill when the key was never stored. if ( array_key_exists( 'feature_system_audio', $meta ) ) { continue; } $meta['feature_system_audio'] = true; update_post_meta( (int) $pid, self::META_KEY, $meta ); } $legacy = get_option( Dadan_Settings::OPTION_KEY, array() ); if ( is_array( $legacy ) && ! array_key_exists( 'feature_system_audio', $legacy ) ) { $legacy['feature_system_audio'] = true; update_option( Dadan_Settings::OPTION_KEY, $legacy, false ); } update_option( 'dadan_sys_audio_default_on_v1', 1, false ); } /** * Shortcode string for an instance. * * @param int $post_id Instance ID. * @return string */ public static function shortcode_for( $post_id ) { return sprintf( '[dadan-recording id="%d"]', (int) $post_id ); } /** * Published instances as id => title (for block picker). * * @return array */ public static function choices() { $posts = get_posts( array( 'post_type' => self::POST_TYPE, 'post_status' => array( 'publish', 'private' ), 'posts_per_page' => 100, 'orderby' => 'title', 'order' => 'ASC', ) ); $out = array(); foreach ( $posts as $post ) { $out[ (int) $post->ID ] = $post->post_title ? $post->post_title : ( '#' . $post->ID ); } return $out; } /** * Title field placeholder. * * @param string $text Placeholder. * @param WP_Post $post Post. * @return string */ public function title_placeholder( $text, $post ) { if ( $post instanceof WP_Post && self::POST_TYPE === $post->post_type ) { return __( 'Instance name (e.g. Homepage CTA)', 'dadan' ); } return $text; } /** * List columns. * * @param array $columns Columns. * @return array */ public function columns( $columns ) { $new = array(); foreach ( $columns as $key => $label ) { $new[ $key ] = $label; if ( 'title' === $key ) { $new['dadan_status'] = __( 'Status', 'dadan' ); $new['dadan_app_id'] = __( 'Public App ID', 'dadan' ); $new['dadan_shortcode'] = __( 'Shortcode', 'dadan' ); } } unset( $new['date'] ); $new['date'] = __( 'Date', 'dadan' ); return $new; } /** * Column cells. * * @param string $column Column key. * @param int $post_id Post ID. */ public function column_content( $column, $post_id ) { $s = self::get_settings( $post_id ); $id = isset( $s['public_app_id'] ) ? trim( (string) $s['public_app_id'] ) : ''; if ( 'dadan_status' === $column ) { echo ''; if ( Dadan_Settings::is_valid_public_app_id( $id ) ) { echo '' . esc_html__( 'Ready', 'dadan' ) . ''; } else { echo '' . esc_html__( 'Needs App ID', 'dadan' ) . ''; } if ( (int) $post_id === self::default_id() ) { echo ' ' . esc_html__( 'Default', 'dadan' ) . ''; } echo ''; return; } if ( 'dadan_app_id' === $column ) { if ( '' !== $id ) { echo '' . esc_html( $id ) . ''; } else { echo '' . esc_html__( 'Not set', 'dadan' ) . ''; } return; } if ( 'dadan_shortcode' === $column ) { $code = self::shortcode_for( $post_id ); ?> > $messages Messages by post type. * @return array> */ public function updated_messages( $messages ) { $saved = __( 'Instance saved.', 'dadan' ); $messages[ self::POST_TYPE ] = array( 0 => '', 1 => $saved, 4 => $saved, 6 => __( 'Instance created. Add your App ID to start recording.', 'dadan' ), 7 => $saved, 8 => $saved, 10 => __( 'Draft saved.', 'dadan' ), ); return $messages; } /** * Row actions — no "View". * * @param array $actions Actions. * @param WP_Post $post Post. * @return array */ public function row_actions( $actions, $post ) { if ( self::POST_TYPE !== $post->post_type ) { return $actions; } unset( $actions['view'], $actions['inline hide-if-no-js'] ); return $actions; } /** * Settings metabox on edit screen. */ public function register_meta_boxes() { add_meta_box( 'dadan-instance-shortcode', __( 'Add to your site', 'dadan' ), array( $this, 'render_shortcode_box' ), self::POST_TYPE, 'side', 'high' ); add_meta_box( 'dadan-instance-settings', __( 'Instance settings', 'dadan' ), array( $this, 'render_settings_box' ), self::POST_TYPE, 'normal', 'high' ); } /** * Side box: copyable shortcode. * * @param WP_Post $post Post. */ public function render_shortcode_box( $post ) { $code = self::shortcode_for( (int) $post->ID ); $is_default = ( (int) $post->ID === self::default_id() ); ?>
render_instance_form( (int) $post->ID ); } /** * Persist settings meta. * * @param int $post_id Post ID. * @param WP_Post $post Post. */ public function save_post( $post_id, $post ) { if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) { return; } if ( wp_is_post_revision( $post_id ) ) { return; } if ( ! current_user_can( 'edit_post', $post_id ) ) { return; } if ( ! isset( $_POST['dadan_instance_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['dadan_instance_nonce'] ) ), 'dadan_instance_save_' . $post_id ) ) { return; } if ( ! isset( $_POST[ self::FORM_FIELD_KEY ] ) || ! is_array( $_POST[ self::FORM_FIELD_KEY ] ) ) { return; } // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitized via Dadan_Settings::sanitize(). $raw = wp_unslash( $_POST[ self::FORM_FIELD_KEY ] ); $settings_obj = new Dadan_Settings(); // DADAN-24: sanitize under this instance's context so fallbacks read the right meta. $clean = Dadan_Settings::with_instance( (int) $post_id, static function () use ( $settings_obj, $raw ) { return $settings_obj->sanitize( is_array( $raw ) ? $raw : array() ); } ); update_post_meta( $post_id, self::META_KEY, $clean ); // Keep legacy option in sync with the default instance for older readers. if ( (int) $post_id === self::default_id() ) { update_option( Dadan_Settings::OPTION_KEY, $clean, false ); } // Return to the tab the user was on (nonce verified above). $tab = isset( $_POST['dadan_settings_tab'] ) ? sanitize_key( wp_unslash( $_POST['dadan_settings_tab'] ) ) : ''; if ( '' !== $tab && array_key_exists( $tab, Dadan_Settings::tabs() ) ) { add_filter( 'redirect_post_location', static function ( $location ) use ( $tab ) { return add_query_arg( 'tab', $tab, $location ); } ); } // DADAN-07: surface sanitize errors after CPT redirect (settings errors are lost otherwise). $errors = get_settings_errors( 'dadan_video' ); if ( ! empty( $errors ) ) { set_transient( 'dadan_instance_save_notices_' . get_current_user_id(), $errors, 60 ); add_filter( 'redirect_post_location', static function ( $location ) { return add_query_arg( 'dadan_notice', '1', $location ); } ); } } /** * Media + CTA admin scripts on instance edit. * * @param string $hook Hook. */ public function enqueue_edit_assets( $hook ) { $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null; if ( ! $screen || self::POST_TYPE !== $screen->post_type ) { return; } // List table: badges + copy buttons. if ( 'edit.php' === $hook ) { wp_enqueue_style( 'dadan-video-admin', DADAN_VIDEO_URL . 'assets/admin.css', array(), DADAN_VIDEO_VERSION ); wp_enqueue_script( 'dadan-video-admin-cta', DADAN_VIDEO_URL . 'assets/admin-cta.js', array(), DADAN_VIDEO_VERSION, true ); return; } if ( ! in_array( $hook, array( 'post.php', 'post-new.php' ), true ) ) { return; } wp_enqueue_media(); wp_enqueue_style( 'dadan-video-public-record', DADAN_VIDEO_URL . 'assets/public-record.css', array(), DADAN_VIDEO_VERSION ); wp_enqueue_style( 'dadan-video-admin', DADAN_VIDEO_URL . 'assets/admin.css', array(), DADAN_VIDEO_VERSION ); wp_enqueue_script( 'dadan-video-admin-cta', DADAN_VIDEO_URL . 'assets/admin-cta.js', array( 'jquery', 'media-editor', 'media-views' ), DADAN_VIDEO_VERSION, true ); wp_localize_script( 'dadan-video-admin-cta', 'dadanCtaAdmin', array( 'fieldKey' => self::FORM_FIELD_KEY, 'dadanIconUrl' => Dadan_Settings::dadan_icon_url(), 'i18nChoose' => __( 'Choose CTA image', 'dadan' ), 'i18nUse' => __( 'Use image', 'dadan' ), 'i18nMediaMissing' => __( 'WordPress media library failed to load. Refresh the page and try again.', 'dadan' ), ) ); } }