0

This is a very common issue, but all the answers I read don't relate to what's happening in my case.

I'm writing a custom plugin that reads and writes/updates data to a custom database table (it's very simple).

However, when an update is made, I wish to redirect to the main page, and then display a success admin notice on the page. However, when submitting the form, and attempting wp_safe_redirect I get the following:

Warning: Cannot modify header information - headers already sent by (output started at C:\wamp64\www\bellavou\wp-includes\formatting.php:4593) in C:\wamp64\www\bellavou\wp-includes\pluggable.php on line 1167

I don't have any blank spaces in my plugin files, and my call to the redirect, within the wpdb->update call looks like this:

if($wpdb->update( 
    'wp_before_after', 
    $bv_before_after_data, 
    array( 'id' => $bv_id ), 
    array( 
        '%s',   // value1
        '%s',   // value2
        '%s',   // value3
        '%d',   // value4
        '%d',   // value5
        '%d',   // value6
        '%d',   // value7
        '%d',   // value8
        '%s',   // value9
        '%s',   // value10
        '%s',   // value11
        '%s',   // value12
    ), 
    array( '%d' ) 
    )) {
        // Success
        wp_safe_redirect('/wp-admin/admin.php?page=bv_before_afters_main&updated=1');
    } else {
        // Error
        echo '<div class="notice notice-error is-dismissible"><p>Could not be updated! (Maybe there was nothing to update?)</p></div>';
    }

How can I order this correctly so the redirect, and admin notice will work correctly?

UPDATE:

My main plugin file looks like this (with the includes to other pages):

// Create page in WP Admin
function my_admin_menu() {
    add_menu_page( 'Before & After Photos', 'Before & Afters', 'edit_posts', 'bv_before_afters_main', 'bv_before_afters_main', 'dashicons-format-gallery', 58  );
    add_submenu_page( 'bv_before_afters_main', 'Add set', 'Add set', 'edit_posts', 'bv_before_afters_add', 'bv_before_afters_add' );
    add_submenu_page( 'bv_before_afters_main', 'Edit set', 'Edit set', 'edit_posts', 'bv_before_afters_edit', 'bv_before_afters_edit' );    
}
add_action( 'admin_menu', 'my_admin_menu' );

// Apply stylesheets
function bv_before_afters_styles(){
    wp_enqueue_style( 'bv_before_after_styles', plugins_url( '/bv_before_afters.css', __FILE__ ) );
}
add_action('admin_print_styles', 'bv_before_afters_styles');

// Display the main page
function bv_before_afters_main(){
    include_once plugin_dir_path( __FILE__ ).'/views/view_all.php';
}

// Display the edit page
function bv_before_afters_edit(){
    include_once plugin_dir_path( __FILE__ ).'/views/edit.php';
}

// Display the add page
function bv_before_afters_add(){
    include_once plugin_dir_path( __FILE__ ).'/views/add.php';
}

(the redirect code is within edit.php)

9
  • What hook are you attaching the posted code to?
    – bosco
    Aug 1, 2016 at 14:38
  • Not sure what you're asking, but that code is from one of my plugin PHP files. I have a main file of my plugin, and within that I have a function that includes each of the individual page contents (view, edit, add). The code above, is within the edit file.
    – Lee
    Aug 1, 2016 at 14:39
  • The large majority of custom code should be "hooked" to an action or filter (using add_action() or add_filter(), respectively). Your edit addresses my question =]
    – bosco
    Aug 1, 2016 at 14:43
  • Should I be doing something differently when calling my individual pages then?
    – Lee
    Aug 1, 2016 at 14:45
  • 1
    In terms of general structure, you're fine. The problem is that the add_submenu_page() callback is intended only to display markup - by time WordPress gets around to calling your bv_before_afters_edit() function, part of the admin page has already rendered (sidebar and floating toolbar). The solution will be to remove the $wpdb->update() and wp_safe_redirect() functionality from the file and attach it to an earlier hook - probably admin_init, current_screen, or load-(page). If the question still stands in a few hours, I'll dig around a little
    – bosco
    Aug 1, 2016 at 15:22

2 Answers 2

6

Background

The infamous "Headers already sent" error rears it's ugly head in circumstances where something attempts to modify the HTTP headers for the server's response after they have already been dispatched to the browser - that is to say, when the server should only be generating the body of the response.

This often happens in one of two ways:

  • Code prints things too early before WordPress has finished composing headers - this pushes the HTTP response to the browser and forces the HTTP headers to be dispatched in the process. Any attempts by WordPress or third-party code that would normally properly modify the headers will subsequently produce the error.
  • Code attempts to alter HTTP headers too late after WordPress has already sent them and begun to render HTML.

Problem & Solution Overview

This instance, the problem is the latter. This is because the wp_safe_redirect() function works by setting a 300-range HTTP status header. Since the callback function arguments for add_menu_page() and add_submenu_page() are used to print custom markup after the dashboard sidebar and floating toolbar have already been generated, the callbacks are too late in WordPress's execution to use the wp_safe_redirect() function. As discussed in the comments, the solution is to move the $wpdb->update() and wp_safe_redirect() calls outside of your views/edit.php view template and "hook" them to an action that occurs before the HTTP headers are sent (before any HTML is sent to the browser).


Solving the Problem

Looking at the action reference for the typical admin-page request, it's reasonable to assume that the 'send_headers' action is the absolute latest that HTTP headers can be reliably modified. So your desired outcome could be achieved by attaching a function to that action or one before it containing your redirect logic - but you'd need to manually check which admin page is being displayed to ensure that you only process the $wpdb->update() and subsequent redirect for your views/edit.php template.

Conveniently, WordPress provides a shortcut for this contextual functionality with the 'load-{page hook}' action, which is dynamically triggered for each admin page loaded. While your call to add_submenu_page() will return the {page hook} value needed to hook an action for execution when your custom templates load, page hooks can be inferred in the format {parent page slug}_page_{child page slug}. Taking the values from your code, then, we end up with the 'load-bv_before_afters_main_page_bv_before_afters_edit' action, which will only execute while loading your views/edit.php template.

The final issue becomes the matter of passing along whether or not $wpdb->update() produced an error to your views/edit.php template to determine whether or not to display the error message. This could be done using global variables or a custom action, however a better solution is to attach a function that will print your error to the 'admin_notices' action which exists for exactly this reason. 'admin_notices' will trigger right before WordPress renders your custom view template, ensuring that your error message ends up on the top of the page.

Note

The $wpdb->update() method returns the number of rows updated or false if an error is encountered - this means that a successful update query can change no rows and return 0, which your if() conditional will lazily evaluate as false and display your error message. For this reason, if you wish to only display your error when the call actually fails, you should compare $wpdb->update()'s return value against false using the identity comparison operator !==.


Implementation

Combining all of the above, one solution might look as follows. Here I've used an extra array to hold the page slugs for your admin pages for the sake of easy reference, ensuring no typo-related issues and making the slugs easier to change, if necessary.

The entire portion of the views/edit.php file that you originally posted would now reside in the action hook, as well as any other business logic used to process the edit form.

Plugin File:

$bv_admin_page_slugs = [
  'main' => 'bv_before_afters_main',
  'edit' => 'bv_before_afters_edit',
  'add'  => 'bv_before_afters_add'
];

add_action( 'admin_menu', 'bv_register_admin_pages' );

// Create page in WP Admin
function bv_register_admin_pages() {
    add_menu_page(
      'Before & After Photos',
      'Before & Afters',
      'edit_posts',
      $bv_admin_page_slugs['main'],
      'bv_before_afters_main',
      'dashicons-format-gallery',
      58
    );

    add_submenu_page(
      $bv_admin_page_slugs['main'],
      'Add set',
      'Add set',
      'edit_posts',
      $bv_admin_page_slugs['add'],
      'bv_before_afters_add'
    );

    add_submenu_page(
      $bv_admin_page_slugs['main'],
      'Edit set',
      'Edit set',
      'edit_posts',
      $bv_admin_page_slugs['edit'],
      'bv_before_afters_edit'
    );    
}

// Process the update/redirect logic whenever the the custom edit page is visited
add_action(
  'load-' . $bv_admin_page_slugs['main'] . '_page_' . $bv_admin_page_slugs['edit'],
  'bv_update_before_after'
);

// Performs a redirect on a successful update, adds an error message otherwise
function bv_update_before_after() {
  // Don't process an edit if one was not submitted
  if( /* (An edit was NOT submitted) */ )
    return;

  $result = $wpdb->update( /* (Your Arguments) */ );

  if( $result !== false ) {
    wp_safe_redirect('/wp-admin/admin.php?page=' . $bv_admin_page_slugs['main'] . '&updated=1');
    exit;
  }
  else {
    // Tell WordPress to print the error when it usually prints errors
    add_action(
      'admin_notices',
      function() {
        echo '<div class="notice notice-error is-dismissible"><p>Could not be updated!</p></div>';
      }
    );
  } 
}

// Apply stylesheets
function bv_before_afters_styles(){
    wp_enqueue_style( 'bv_before_after_styles', plugins_url( '/bv_before_afters.css', __FILE__ ) );
}
add_action('admin_print_styles', 'bv_before_afters_styles');

// Display the main page
function bv_before_afters_main(){
    include_once plugin_dir_path( __FILE__ ).'/views/view_all.php';
}

// Display the edit page
function bv_before_afters_edit(){
    include_once plugin_dir_path( __FILE__ ).'/views/edit.php';
}

// Display the add page
function bv_before_afters_add(){
    include_once plugin_dir_path( __FILE__ ).'/views/add.php';
}
7
  • So on the edit page, where I originally had the wp safe redirect action, what should go there now? The function call that's now in the main file?
    – Lee
    Aug 1, 2016 at 18:24
  • @Lee I've simplified my solution, somewhat... Normally I'd imagine you'd put the markup for a custom editing interface in your views/edit.php template - but the way I read your code, it looked as though you wanted to call $wpdb->update() and perform the redirect/error logic as soon as someone clicked "Edit set", only displaying the template in the case of an error. Since the redirect logic is attached to the 'load-bv_before_afters_main_page_bv_before_afters_edit' action, you don't need to call it from the template - it will execute whenever a user visits the "Edit set" menu item.
    – bosco
    Aug 1, 2016 at 19:04
  • @Lee compared to the contents of the views/edit.php template that you originally posted, the file would now be empty (since the error is now displayed using the 'admin_notices' action). If I've still confused you or you have more questions, let me know and we can hash it out in chat.
    – bosco
    Aug 1, 2016 at 19:05
  • So where would the contents of my edit file go, if that file is now empty? That file can't be empty, because that still has to hold all the data I want to show on that page, right?
    – Lee
    Aug 2, 2016 at 7:36
  • I've just read through some comments, and think you had misunderstood a little bit. The editing form is always displayed when that page is viewed. The wpdb update code ran when the submit button at the bottom of the page is pressed. This is simply to take all the values in the form, and update that row. ONce it's updated, I'd like to return to the main page, and display an admin water.
    – Lee
    Aug 2, 2016 at 7:55
0

I didn't send any new headers, so had a hard time finding this.

Tried inspecting the DOM. In the top of my DOM, i had a wild comment section standing at the top for some reason, which ruined the rest of the header.

I removed the comment section, which was for some reason rendered at the top of my DOM.

Hope this helps someone else

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.