We did not manage to convert Batch API to fully leverage the AJAX framework for D7. The current page callback of the /batch path is a weird mix of HTML and JSON. If we would have fully converted Batch API already, then the /batch path would conditionally use ajax_deliver() when needed, and drupal_deliver_html_page() when returning HTML.
Right now, the documented way to alter the delivery callback for AJAX requests breaks all batches:
// @todo Should totally live in core instead of in an API example?
// Without this, any AJAX stuff returns a full HTML page...
function core_tabs_page_delivery_callback_alter(&$callback) {
// @todo Fix Batch API.
if ($_GET['q'] == 'batch') {
return;
}
// jQuery sets a HTTP_X_REQUESTED_WITH header of 'XMLHttpRequest'.
// If a page would normally be delivered as an html page, and it is called
// from jQuery, deliver it instead as an AJAX response.
if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest' && $callback == 'drupal_deliver_html_page') {
$callback = 'ajax_deliver';
}
}
Attached patch is an initial attempt to resolve the problem of batches breaking when universally altering the delivery callback as in the documented example.
Comments
Comment #1
yched commentedsubscribe
I did not really look into D7's page_delivery_callback mechanism so far.
Should we use proper / separate delivery callbacks for 'batch - JS mode' and 'batch - noJS mode' ?
Comment #2
sunBy default (i.e., without additional drupal_alter()), a single menu router item only has a single delivery callback. Basically meaning that one router path is expected to result in a certain output.
Drupal core implements two delivery callbacks as of now:
1) HTML (page)
2) AJAX (JSON command array)
The /batch path and page callback http://api.drupal.org/api/function/_batch_page/7 is somewhat special, as it
a) Outputs the initial Batch HTML page
http://api.drupal.org/api/function/_batch_start/7
b) If the client has the has_js cookie, seems to respond with JSON batch status data for progress.js
http://api.drupal.org/api/function/_batch_progress_page_js/7
http://api.drupal.org/api/function/_batch_do/7
c) If the client has no has_js cookie, seems to return a full HTML page
http://api.drupal.org/api/function/_batch_progress_page_nojs/7
d) Outputs the finished Batch HTML page
http://api.drupal.org/api/function/_batch_finished/7
Normally, one would likely say that a clear separation would use two different router items:
batch/page - All HTML pages that are output, using the regular HTML delivery callback.
batch/json - JSON output for the has_js cookie case.
"/page" only added for clarity. Technically doable would also be:
batch
batch/json
Note that batch/json would need a new built-in delivery callback for plain JSON, since http://api.drupal.org/api/function/ajax_deliver/7 relies on AJAX framework and command structures.
Comment #3
yched commentedYup. Put that way, that's not too nice :-).
IIRC, this in fact hasn't changed much since the 'progressive processing roundtrip' code that was taken out of D5 update.php and generalized into D6 batch processing.
The constraint is also that standalone scripts (install.php, update.php, D7's authorize.php if I'm not mistaken) also need to use batch API and output a progress page at their own URL outside of the 'system/batch' menu entry. This is currently done by simply including the output of _batch_page(), which internally takes care of doing the right stuff depending on the batch 'state' (start, do, finish, error)
Comment #4
sunThinking through this, I think that the current patch is all we can do for D7.
Comment #5
yched commentedIf limited to this scope, I'm not sure I get what the patch buys us, then.
What it does is just move the 'force page display without messages' out of the page callback and into a delivery callback. Does it mean that page delivery callback is the official 'right' place if you want to display a page without messages (or sidebars, or both) ?
Comment #6
sunThe primary problem is that current HEAD
The idea is to do a stopgap fix by adding a dedicated/special delivery callback for the /batch path. This delivery callback can return mixed data formats to its liking without breaking and invalidating the unique purpose of dedicated standard delivery callbacks.
In D8, we can clean this up properly by adding a standard delivery callback for plain JSON and ideally also rewrite the Batch API/UI to remove the current arbitration that happens in _batch_page().
Comment #7
yched commentedOK - tested the patch with and without JS with the batch_test module that supports the test suite (manual test because the test suite cannot test the JS mode), everything works fine. Standalone scripts are OK, the patch doesn't touch anything in there (tested with updates for peace of mind).
RTBC, just added minor changes :
- Added some comments documenting the rationale above.
- renamed a variable to $page in batch_deliver_page(), to avoid $page_callback_result being one thing and then another
- in case $page_callback_result === FALSE, do not run drupal_deliver_html_page() twice
Comment #8
sunThanks!
Comment #9
webchickI'm sorry, but no. It's too late now for polish stuff like this.
To reiterate, Drupal 7 is effectively done. The only thing we do at this point is fix bugs that get us to release. This is a nice improvement, but no one is actively blocked from using the batch API at this point in time, so refactoring it is not D7 material.
Comment #10
sunRight, that's what this patch does. I had to learn the hard way that my module - which is strictly following API docs and examples - unintentionally breaks Batch API's processing.
It took me a couple of hours to figure out what on earth broke all kind of batches I tried to run (tests, updates, etc; in short: all batches). All I got was a JavaScript alert stating "An error occurred.", followed by a dump of some response garbage.
Since debugging is close to impossible, I had to disable all modules one after the other to figure out which code or module is guilty for that. I totally did not expect my module to be guilty, because as mentioned, its hook implementation is 1:1 copypasted from the API documentation.
And it turns out that both my module as well as the API documentation is correct. Batch API's page callback is what is not correct, because it sometimes returns plain JSON within a HTML context; i.e., without ending the request. In turn, the JSON page callback result is handed over to the default delivery callback, drupal_deliver_html_page(), which, as the name implies, is expected to return a renderable array or a string to output as HTML.
Handing a plain JSON data array to drupal_deliver_html_page() is technically and conceptually invalid.
Lastly, we already discussed the "proper fix" and "nice improvement" earlier in this issue, but that is definitely D8 material at this point, as it would require to rewrite most of batch.inc.
Thus, this patch is a stopgap fix to make Batch API's page callback not hi-jack the intended purpose and expected API behavior of page callbacks and delivery callbacks.
Comment #11
webchickIMO then we should merely fix the confusing documentation. But in any case, we can't remove/rename API functions at this point.
Comment #12
yched commented@webchick : I'll let sun confirm, but if I got this right, this is not about the documentation being confusing - the doc is correct, and shows the right way to use hook_page_delivery_callback_alter().
The problem is that the page callback for the 'batch/' menu entry breaks the contract by using drupal_deliver_html_page() to deliver non-HTML data. Any module willing to use hook_page_delivery_callback_alter() to deliver HTML content in a different way is then bound to choke on batches.
The only func remove here is system_batch_page(), the current page callback for 'batch/'. I don't think this qualifies as an API change ?
Comment #13
webchickWe've been bitten already a few times in beta for changing names of things that shouldn't be a big deal. There's no bug-fix reason to do it here; it's merely polish.
Is this situation a regression from 6.x?
Comment #14
sunThe entire concept of delivery callbacks does not exist in D6, it is new in D7.
yched's summary in #12 is correct, especially the part about breaking the "contract" through having a delivery callback, which is intended to deliver HTML, returning JSON.
But of course, this change technically must be considered as an API change. Even though I don't know of any code or module that is trying to re-use any functionality of the /batch menu router item resp. page callback. The only actual API change is that the system_batch_page() callback no longer exists.
AFAICS, the only other way out would be to preemptively end the page request in _batch_do(); i.e., right after the JSON has been generated, so the entire regular page delivery mechanism is not even invoked in the first place. That, however, should be considered as a hack, disallowing any further request modifications by contributed modules, as the request is ended preemptively.
Comment #15
sunI hope that the additional reasoning provided in #14 sufficiently explains the need for this patch.
As already mentioned in there, we can also squeeze a drupal_exit() right after the JSON output is generated, but that would be a hacky resolution compared to the existing patch.
Comment #16
cyberwolf commentedSubscribing.
Comment #17
nod_What's the status of this taking into consideration the patch removing has_js cookie #229825: backport "$_COOKIE['has_js'] must die" patch to 7.x?
Comment #18
sunRe-rolled against HEAD - including HttpKernel changes.
Comment #20
Crell commentedDelivery callbacks are about to be removed entirely: http://drupal.org/node/1677304
-16 days to next Drupal core point release.
Comment #21
sunSo if I get this right, then attached patch should actually work.
Comment #23
sunae71db1 Fixed JavaScript progress callback response contains Ajax settings.
Comment #25
sunI'd like to even go one step further, and also change the router item definition for /batch to define proper callbacks and arguments.
@Crell: What's the new/post-kernel way to handle the
#show_messagesflag that exists fortheme_page()?Batch API uses that to mute any messages that may be set during the operations, and to instead display them at the end of the batch run.
d6360da - #929506: Menu router does not load the 'file' before attempting to invoke callbacks, and fails to find/register them during building
47225ec Replaced custom batch loading with proper router item definition.
a3a6837 Fixed install_run_task() to properly handle _batch_page() return values.
The installer works flawlessly with attached patch - if this fails the testbot again, then I'm not really sure what's going on with the testbot.
Comment #27
Crell commentedWhat is #show_messages supposed to do now? "Flag that causes something to change vastly elsewhere" is not a design that is encouraged in the post-kernel world, so I'm unclear what the use case is.
Comment #28
marvil07 commentedThe bot fail because of "Missing argument 1 for _batch_page(), called in /var/lib/drupaltestbot/sites/default/files/checkout/core/update.php on line 500 and defined" several times, so I just added a line to pass it the required parameter to _batch_page().
Let's see what bot thinks.
Comment #29
sunCoolio, thanks @marvil07!
I guess that leaves us with the
#show_messagesissue only.In a progressive batch, Batch API renders the intermediate pages being output for batch operation sets/steps and enforces no messages to be shown for those (as they wouldn't be noticeable, since the page immediately redirects), so that any messages being set are collected during the batch processing, and only output on the final batch finished page.
To experience this behavior, I think one has to disable JavaScript and then execute any progressive batch through the UI; e.g., installing Drupal, importing translations, running update.php, or possibly the most simple, checking for module updates with Update module. (not sure whether there is a simpler one) — OH. Running tests! :-D
However, you'd have to manually implant a call to drupal_set_message() into the batch operation step callback(s), so as to actually make sure that there are messages that should not be output until the batch has finished.
If that still works with this patch, then I think we're ready to go.
Comment #30
yched commentedCurrent patch means token checks only happen in a new separate menu access callback for ?batch/%bid', meaning standalone scripts like update.php or install.php do no tken checks anymore, which sounds annoying.
I can't remember whether pre-D6 update.php (which batch API derived from) had those checks, or whether they were added by Batch API for the more generic use cases.
Comment #33
mgiffordThis still a concern in D8? Unassigned issue too.
Comment #45
catch