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(
'
',
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' ),
)
);
}
}