Saving Embed Code From Dashboard Widget

Ok, so the main problem here is ultimately just that wp_filter_post_kses is probably filtering out iframes. But I also want to address the round about way you’re handling the form. You’re not doing AJAX, which is sending the request to the server via Javascript, but you’re trying to hook into the AJAX hooks and then run them manually on admin-post.php? It’s all a huge mess.

Here’s working code that will do what you want with actual AJAX, and I’ll go over some things to pay attention to at the bottom:

function wpse_211908_add_dashboard_widgets() {
    wp_add_dashboard_widget(
        'wpse_211908_stream_options',
        'Stream Settings',
        'wpse_211908_stream_options_widget'
    );
}
add_action('wp_dashboard_setup', 'wpse_211908_add_dashboard_widgets' );

function wpse_211908_stream_options_widget() {
    $code = stripcslashes( get_option( 'stream_code' ) );
    $status = get_option( 'stream_status' );
    ?>

    <form id="stream_options" method="post">
        <p><textarea name="stream_code" rows="5" class="widefat"><?php echo esc_textarea( $code ); ?></textarea>

        <p><label><input type="checkbox" name="stream_status" value="1" <?php checked( $status, '1' ); ?>> Live</label>

        <?php wp_nonce_field( 'stream_options_ajax_call' ); ?>
        <input type="hidden" name="action" value="wpse_211908_save_stream_options">

        <p><input type="submit" class="button button-primary" value="Save">
    </form>

    <script>
        ( function($) {
            var $form = $( '#stream_options' );
            var $button = $form.find( 'input[type="submit"]' );

            $form.on( 'submit', function(e) {
                e.preventDefault();

                $button.prop( 'disabled', true ).attr( 'value', 'Saving...' );

                $.ajax( {
                    url: window.ajaxurl,
                    method: 'post',
                    data: $form.serialize(),
                    complete: function() {
                        $button.prop( 'disabled', false );
                    },
                    success: function() {
                        $button.attr( 'value', 'Saved' );
                    },
                    error: function() {
                        $button.attr( 'value', 'Save' );
                    },
                } );
            } );

            $form.on( 'input change', function() {
                $button.attr( 'value', 'Save' );
            } );
        } )( jQuery );
    </script>

    <?php
}

function wpse_211908_save_stream_options() {
    if ( empty( $_POST['_wpnonce'] ) || ! wp_verify_nonce( $_POST['_wpnonce'], 'stream_options_ajax_call' ) ) {
        http_response_code(403);
        exit();
    }

    if ( ! current_user_can( 'manage_options' ) ) {
        http_response_code(403);
        exit();
    }

    update_option( 'stream_code', $_POST['stream_code'] );

    $status = isset( $_POST['stream_status'] ) ? '1' : '0';
    update_option( 'stream_status', $status );

    wp_die();
}
add_action( 'wp_ajax_wpse_211908_save_stream_options', 'wpse_211908_save_stream_options', 99 );

Firstly, notice that I have prefixed all my functions and any variables that are going to be shared with WordPress to avoid conflicts. This is a good practice and I recommend it. Names like add_dashboard_widgets are just begging to
cause conflicts.

Notice the script tag in the widget. This is the JavaScript that will send the AJAX request. Because I have a hidden field named action with the value wpse_211908_save_stream_options, this means that it will become $_POST['action'] and I can handle it by using the wp_ajax_wpse_211908_save_stream_options hook. Using Javascript for the submission means that you don’t need to have a page load when submitting.

Also in the widget notice that I have used esc_textarea() to make sure HTML in the $code variable doesn’t break the HTML of the widget. And notice my use of checked() as a simpler way of outputting the checked attribute.

The second function is what handles the AJAX request. Notice that I can handle everything in the one function rather than adding actions which check fields and trigger actions. In here I send 403 errors if the nonce doesn’t verify, or if the user does not have permission to update settings.

One thing you’ll notice is that I don’t do anything special to $_POST['stream_code']. Filtering submitted HTML isn’t necessary when we’re already checking that it’s a trusted user. If you want to lock this down more, you can use wp_kses, but you need to provide your own list of allowed elements since iframes will get stripped by wp_filter_post_kses().

Then notice that for the $_POST['stream_status'] I simply check that it exists (with AJAX it won’t come through at all when not checked) and manually set to '0' or '1'. wp_kses() is considerable overkill for sanitizing a checkbox.

Finally, to use the HTML on the front end, just use: