Skip to content

Commit

Permalink
MDL-66679 Forms: Submit button remains disabled after file download
Browse files Browse the repository at this point in the history
When you download a file directly from a Moodle form submit button,
the submit button disables when you click it, but you remain on that
page so we need to re-enable the button.

This commit causes it to re-enable once the file download finishes,
setting a temporary cookie to indicate this to the JavaScript code.

It also adds a method to disable the system on a given form by
setting data-double-submit-protection="off".
  • Loading branch information
sammarshallou committed Oct 30, 2019
1 parent d769970 commit 225eb7b
Show file tree
Hide file tree
Showing 11 changed files with 162 additions and 3 deletions.
3 changes: 3 additions & 0 deletions grade/export/xls/grade_export_xls.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ public function print_grades() {

$strgrades = get_string('grades');

// If this file was requested from a form, then mark download as complete (before sending headers).
\core_form\util::form_download_complete();

// Calculate file name
$shortname = format_string($this->course->shortname, true, array('context' => context_course::instance($this->course->id)));
$downloadfilename = clean_filename("$shortname $strgrades.xls");
Expand Down
3 changes: 3 additions & 0 deletions lib/csvlib.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,9 @@ protected function send_header() {
* Download the csv file.
*/
public function download_file() {
// If this file was requested from a form, then mark download as complete.
\core_form\util::form_download_complete();

$this->send_header();
$this->print_csv_data();
exit;
Expand Down
3 changes: 3 additions & 0 deletions lib/dataformatlib.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ function download_as_dataformat($filename, $dataformat, $columns, $iterator, $ca
// Close the session so that the users other tabs in the same session are not blocked.
\core\session\manager::write_close();

// If this file was requested from a form, then mark download as complete (before sending headers).
\core_form\util::form_download_complete();

$format->set_filename($filename);
$format->send_http_headers();
// This exists to support all dataformats - see MDL-56046.
Expand Down
6 changes: 6 additions & 0 deletions lib/filelib.php
Original file line number Diff line number Diff line change
Expand Up @@ -2299,6 +2299,9 @@ function send_temp_file($path, $filename, $pathisstring=false) {
$filename = urlencode($filename);
}

// If this file was requested from a form, then mark download as complete.
\core_form\util::form_download_complete();

header('Content-Disposition: attachment; filename="'.$filename.'"');
if (is_https()) { // HTTPS sites - watch out for IE! KB812935 and KB316431.
header('Cache-Control: private, max-age=10, no-transform');
Expand Down Expand Up @@ -2450,6 +2453,9 @@ function send_file($path, $filename, $lifetime = null , $filter=0, $pathisstring

if ($forcedownload) {
header('Content-Disposition: attachment; filename="'.$filename.'"');

// If this file was requested from a form, then mark download as complete.
\core_form\util::form_download_complete();
} else if ($mimetype !== 'application/x-shockwave-flash') {
// If this is an swf don't pass content-disposition with filename as this makes the flash player treat the file
// as an upload and enforces security that may prevent the file from being loaded.
Expand Down
2 changes: 1 addition & 1 deletion lib/form/amd/build/submit.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion lib/form/amd/build/submit.min.js.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

67 changes: 66 additions & 1 deletion lib/form/amd/src/submit.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,83 @@
* @since 3.8
*/

/** @type {number} ID for setInterval used when polling for download cookie */
let cookieListener = 0;

/** @type {Array} Array of buttons that need re-enabling if we get a download cookie */
const cookieListeningButtons = [];

/**
* Listens in case a download cookie is provided.
*
* This function is used to detect file downloads. If there is a file download then we get a
* beforeunload event, but the page is never unloaded and when the file download completes we
* should re-enable the buttons. We detect this by watching for a specific cookie.
*
* PHP function \core_form\util::form_download_complete() can be used to send this cookie.
*
* @param {HTMLElement} button Button to re-enable
*/
const listenForDownloadCookie = (button) => {
cookieListeningButtons.push(button);
if (!cookieListener) {
cookieListener = setInterval(() => {
// Look for cookie.
const parts = document.cookie.split(getCookieName() + '=');
if (parts.length == 2) {
// We found the cookie, so the file is ready. Expire the cookie and cancel polling.
clearDownloadCookie();
clearInterval(cookieListener);
cookieListener = 0;

// Re-enable all the buttons.
cookieListeningButtons.forEach((button) => {
button.disabled = false;
});
}
}, 500);
}
};

/**
* Gets a unique name for the download cookie.
*
* @returns {string} Cookie name
*/
const getCookieName = () => {
return 'moodledownload_' + M.cfg.sesskey;
};

/**
* Clears the download cookie if there is one.
*/
const clearDownloadCookie = () => {
document.cookie = encodeURIComponent(getCookieName()) + '=deleted; expires=' + new Date(0).toUTCString();
};

/**
* Initialises submit buttons.
*
* @param {String} elementId Form element
*/
export const init = (elementId) => {
const button = document.getElementById(elementId);
button.form.addEventListener('submit', function() {
// If the form has double submit protection disabled, do nothing.
if (button.form.dataset.doubleSubmitProtection === 'off') {
return;
}
button.form.addEventListener('submit', function(event) {
// Only disable it if the browser is really going to another page as a result of the
// submit.
const disableAction = function() {
// If the submit was cancelled, or the button is already disabled, don't do anything.
if (event.defaultPrevented || button.disabled) {
return;
}

button.disabled = true;
clearDownloadCookie();
listenForDownloadCookie(button);
};
window.addEventListener('beforeunload', disableAction);
// If there is no beforeunload event as a result of this form submit, then the form
Expand Down
64 changes: 64 additions & 0 deletions lib/form/classes/util.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.

/**
* Provides the {@link core_form\util} class.
*
* @package core_form
* @copyright 2019 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/

namespace core_form;

defined('MOODLE_INTERNAL') || die();

/**
* General utility class for form-related methods.
*
* @copyright 2019 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class util {
/**
* This function should be called if a form submit results in a file download (i.e. with the
* Content-Disposition: attachment header) instead of navigating to a new page, before the
* file download is sent. It will set a cookie which is used to inform page javascript in
* submit.js.
*
* You may call this function in scripts which might not necessarily be called from forms; it
* will only set the cookie if there is a POST request from a form.
*
* This is automatically called from various points in Moodle such as send_file_xx functions
* in filelib.php.
*/
public static function form_download_complete() {
// If this doesn't look like a Moodle QuickForms request, ignore.
$quickform = false;
foreach ($_POST as $name => $value) {
if (preg_match('~^_qf__~', $name)) {
$quickform = true;
break;
}
}
if (!$quickform) {
return;
}

// Set a session cookie.
setcookie('moodledownload_' . sesskey(), time());
}
}
4 changes: 4 additions & 0 deletions lib/formslib.php
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,10 @@ abstract class moodleform {
* @param mixed $attributes you can pass a string of html attributes here or an array.
* Special attribute 'data-random-ids' will randomise generated elements ids. This
* is necessary when there are several forms on the same page.
* Special attribute 'data-double-submit-protection' set to 'off' will turn off
* double-submit protection JavaScript - this may be necessary if your form sends
* downloadable files in response to a submit button, and can't call
* \core_form\util::form_download_complete();
* @param bool $editable
* @param array $ajaxformdata Forms submitted via ajax, must pass their data here, instead of relying on _GET and _POST.
*/
Expand Down
7 changes: 7 additions & 0 deletions lib/upgrade.txt
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,13 @@ validation against and defaults to null (so, no user needed) if not provided.
the itemid and filepath for the filearea and path defined in $args. It has been added in order to get the correct itemid and
filepath because some components, such as mod_page or mod_resource, add the revision to the URL where the itemid should be placed
(to prevent caching problems), but then they don't store it in database.
* New utility function \core_form\util::form_download_complete should be called if your code sends
a file with Content-Disposition: Attachment in response to a Moodle form submit button (to ensure
that disabled submit buttons get re-enabled in that case). It is automatically called by the
filelib.php send_xx functions.
* If you have a form which sends a file in response to a Moodle form submit button, but you cannot
call the above function because the file is sent by a third party library, then you should add
the attribute data-double-submit-protection="off" to your form.

=== 3.7 ===

Expand Down
4 changes: 4 additions & 0 deletions mod/data/preset.php
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,10 @@
header('Expires: 0');
header('Cache-Control: must-revalidate,post-check=0,pre-check=0');
header('Pragma: public');

// If this file was requested from a form, then mark download as complete.
\core_form\util::form_download_complete();

$exportfilehandler = fopen($exportfile, 'rb');
print fread($exportfilehandler, filesize($exportfile));
fclose($exportfilehandler);
Expand Down

0 comments on commit 225eb7b

Please sign in to comment.