I noticed the URL of the custom Dashboard page is this…
themes.php?page=functions-test.php
Notice how the page is the name of my external functions in includes/functions-test.php.
The problem is in this line, the one that refreshes the page just before the permission error…
echo '<meta http-equiv="refresh" content="0;url=themes.php?page=functions.php&saved=true">';
Now it seems so obvious. Look at the page parameter in the URL… it’s functions.php where it should be the name of my includes file, functions-test.php.
This is the fix…
echo '<meta http-equiv="refresh" content="0;url=themes.php?page=functions-test.php&saved=true">';
Now I wonder if there’s a more robust solution than having a hard coded file name.
EDIT:
And without a hard coded filename… in other words, using basename(__FILE__) instead and I could have moved this code anywhere without the headache.
echo '<meta http-equiv="refresh" content="0;url=themes.php?page=" . basename(__FILE__) . "&saved=true">';
Thank-you to troubleshooting suggestions by @ChipBennett.
EDIT 2:
Taking it one step further, I was able to completely remove the meta refresh; and die;, where instead I use the action of the form element as it was intended. This is the same form action as used in the default WordPress Appearance pages.
<form action="<?php echo esc_url( add_query_arg( 'saved', true ) ) ?>" method="post">
add_query_arg( 'saved', true ) means that when the page redirects back to itself, on form submission, it will have &saved=1 appended into its query string. This is the only part this code needs in order to display the "options saved" message. The very sloppy meta refresh; die with its hard coded URL, the root of the original problem, has been fully eradicated.
FYI: This is a project where I must extract the client’s original options and functionality from their “custom” theme and move them into a child theme of Twenty Thirteen. The original broken theme that uses these sloppy coding practices is called Delicate.