MDL-31000 Added comment about the importance of repository array keys and hardened...
[moodle.git] / lib / form / dndupload.js
1 // This file is part of Moodle - http://moodle.org/
2 //
3 // Moodle is free software: you can redistribute it and/or modify
4 // it under the terms of the GNU General Public License as published by
5 // the Free Software Foundation, either version 3 of the License, or
6 // (at your option) any later version.
7 //
8 // Moodle is distributed in the hope that it will be useful,
9 // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 // GNU General Public License for more details.
12 //
13 // You should have received a copy of the GNU General Public License
14 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16 /**
17  * Javascript library for enableing a drag and drop upload interface
18  *
19  * @package    moodlecore
20  * @subpackage form
21  * @copyright  2011 Davo Smith
22  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
25 M.form_dndupload = {
26     // YUI object.
27     Y: null,
28     // URL for upload requests
29     url: M.cfg.wwwroot + '/repository/repository_ajax.php?action=upload',
30     // itemid used for repository upload
31     itemid: null,
32     // accepted filetypes accepted by this form passed to repository
33     acceptedtypes: [],
34     // maximum number of files this form allows
35     maxfiles: 0,
36     // maximum size of files allowed in this form
37     maxbytes: 0,
38     // unqiue id of this form field used for html elements
39     clientid: '',
40     // upload repository id, used for upload
41     repositoryid: 0,
42     // container which holds the node which recieves drag events
43     container: null,
44     // filemanager element we are working with
45     filemanager: null,
46     // callback  to filepicker element to refesh when uploaded
47     callback: null,
48     // Nasty hack to distinguish between dragenter(first entry),
49     // dragenter+dragleave(moving between child elements) and dragleave (leaving element)
50     entercount: 0,
53     /**
54      * Initalise the drag and drop upload interface
55      * Note: one and only one of options.filemanager and options.formcallback must be defined
56      *
57      * @param Y the YUI object
58      * @param object options {
59      *            itemid: itemid used for repository upload in this form
60      *            acceptdtypes: accepted filetypes by this form
61      *            maxfiles: maximum number of files this form allows
62      *            maxbytes: maximum size of files allowed in this form
63      *            clientid: unqiue id of this form field used for html elements
64      *            containerprefix: prefix of htmlid of container
65      *            repositories: array of repository objects passed from filepicker
66      *            filemanager: filemanager element we are working with
67      *            callback: callback  to filepicker element to refesh when uploaded
68      *          }
69      */
70     init: function(Y, options) {
71         this.Y = Y;
73         if (!this.browser_supported()) {
74             return; // Browser does not support the required functionality
75         }
77         // try and retrieve enabled upload repository
78         this.repositoryid = this.get_upload_repositoryid(options.repositories);
80         if (!this.repositoryid) {
81             return; // no upload repository is enabled to upload to
82         }
84         this.acceptedtypes = options.acceptedtypes;
85         this.clientid = options.clientid;
86         this.maxfiles = options.maxfiles;
87         this.maxbytes = options.maxbytes;
88         this.itemid = options.itemid;
89         this.container = this.Y.one(options.containerprefix + this.clientid);
91         if (options.filemanager) {
92             // Needed to tell the filemanager to redraw when files uploaded
93             // and to check how many files are already uploaded
94             this.filemanager = options.filemanager;
95         } else if (options.formcallback) {
97             // Needed to tell the filepicker to update when a new
98             // file is uploaded
99             this.callback = options.formcallback;
100         } else {
101             if (M.cfg.developerdebug) {
102                 alert('dndupload: Need to define either options.filemanager or options.callback');
103             }
104             return;
105         }
107         this.init_events();
108         this.Y.one('#dndenabled-'+this.clientid).setStyle('display', 'inline');
109     },
111     /**
112      * Check the browser has the required functionality
113      * @return true if browser supports drag/drop upload
114      */
115     browser_supported: function() {
117         if (typeof FileReader == 'undefined') {
118             return false;
119         }
120         if (typeof FormData == 'undefined') {
121             return false;
122         }
123         return true;
124     },
126     /**
127      * Get upload repoistory from array of enabled repositories
128      *
129      * @param array repositories repository objects passed from filepicker
130      * @param returns int id of upload repository or false if not found
131      */
132     get_upload_repositoryid: function(repositories) {
133          for (var i in repositories) {
134              if (repositories[i].type == "upload") {
135                  return repositories[i].id;
136              }
137          }
139          return false;
140     },
142     /**
143      * Initialise drag events on node container, all events need
144      * to be processed for drag and drop to work
145      */
146     init_events: function() {
147         this.Y.on('dragenter', this.drag_enter, this.container, this);
148         this.Y.on('dragleave', this.drag_leave, this.container, this);
149         this.Y.on('dragover',  this.drag_over,  this.container, this);
150         this.Y.on('drop',      this.drop,      this.container, this);
151     },
153     /**
154      * Check if the drag contents are valid and then call
155      * preventdefault / stoppropagation to let the browser know
156      * we will handle this drag/drop
157      *
158      * @param e event object
159      * @return boolean true if a valid file drag event
160      */
161     check_drag: function(e) {
162         if (!this.has_files(e)) {
163             return false;
164         }
166         if (this.reached_maxfiles()) {
167             return false;
168         }
170         e.preventDefault();
171         e.stopPropagation();
173         return true;
174     },
176     /**
177      * Handle a dragenter event, highlight the destination node
178      * when a suitable drag event occurs
179      */
180     drag_enter: function(e) {
181         if (!this.check_drag(e)) {
182             return true;
183         }
185         this.entercount++;
186         if (this.entercount >= 2) {
187             this.entercount = 2; // Just moved over a child element - nothing to do
188             return false;
189         }
191         this.show_upload_ready();
192         return false;
193     },
195     /**
196      * Handle a dragleave event, Remove the highlight if dragged from
197      * node
198      */
199     drag_leave: function(e) {
200         if (!this.check_drag(e)) {
201             return true;
202         }
204         this.entercount--;
205         if (this.entercount == 1) {
206             return false; // Just moved over a child element - nothing to do
207         }
209         this.entercount = 0;
210         this.hide_upload_ready();
211         return false;
212     },
214     /**
215      * Handle a dragover event. Required to intercept to prevent the browser from
216      * handling the drag and drop event as normal
217      */
218     drag_over: function(e) {
219         if (!this.check_drag(e)) {
220             return true;
221         }
223         return false;
224     },
226     /**
227      * Handle a drop event.  Remove the highlight and then upload each
228      * of the files (until we reach the file limit, or run out of files)
229      */
230     drop: function(e) {
231         if (!this.check_drag(e)) {
232             return true;
233         }
235         this.entercount = 0;
236         this.hide_upload_ready();
237         this.show_progress_spinner();
239         var files = e._event.dataTransfer.files;
240         if (this.filemanager) {
241             var currentfilecount = this.filemanager.filecount;
242             for (var i=0, f; f=files[i]; i++) {
243                 if (currentfilecount >= this.maxfiles && this.maxfiles != -1) {
244                     break;
245                 }
246                 if (this.upload_file(f)) {
247                     currentfilecount++;
248                 }
249             }
250         } else {
251             if (files.length >= 1) {
252                 this.upload_file(files[0]);
253             }
254         }
256         return false;
257     },
259     /**
260      * Check to see if the drag event has any files in it
261      *
262      * @param e event object
263      * @return boolean true if event has files
264      */
265     has_files: function(e) {
266         var types = e._event.dataTransfer.types;
267         for (var i=0; i<types.length; i++) {
268             if (types[i] == 'Files') {
269                 return true;
270             }
271         }
272         return false;
273     },
275     /**
276      * Check if reached the maximumum number of allowed files
277      *
278      * @return boolean true if reached maximum number of files
279      */
280     reached_maxfiles: function() {
281         if (this.filemanager) {
282             if (this.filemanager.filecount >= this.maxfiles && this.maxfiles != -1) {
283                 return true;
284             }
285         }
286         return false;
287     },
289     /**
290      * Highlight the destination node
291      */
292     show_upload_ready: function() {
293         this.container.addClass('dndupload-over');
294     },
296     /**
297      * Remove highlight on destination node
298      */
299     hide_upload_ready: function() {
300         this.container.removeClass('dndupload-over');
301     },
303     /**
304      * Display a progress spinner in the destination node
305      */
306     show_progress_spinner: function() {
307         // add a loading spinner to show something is happening
308         var loadingspinner = this.Y.Node.create('<div id="dndprogresspinner-'+this.clientid+'" style="text-align: center">');
309         loadingspinner.append('<img src="'+M.util.image_url('i/loading_small')+'" />');
310         this.container.append(loadingspinner);
311     },
313     /**
314      * Remove progress spinner in the destination node
315      */
316     hide_progress_spinner: function() {
317         this.Y.one('#dndprogresspinner-'+this.clientid).remove();
318     },
320     /**
321      * Tell the attached filemanager element (if any) to refresh on file
322      * upload
323      */
324     update_filemanager: function() {
325         if (this.filemanager) {
326             // update the filemanager that we've uploaded the files
327             this.hide_progress_spinner();
328             this.filemanager.filepicker_callback();
329         }
330     },
332     /**
333      * Upload a single file via an AJAX call to the 'upload' repository
334      */
335     upload_file: function(file) {
336         if (file.size > this.maxbytes && this.maxbytes > 0) {
337             // Check filesize before attempting to upload
338             alert(M.util.get_string('uploadformlimit', 'moodle')+"\n'"+file.name+"'");
339             return false;
340         }
342         // This would be an ideal place to use the Y.io function
343         // however, this does not support data encoded using the
344         // FormData object, which is needed to transfer data from
345         // the DataTransfer object into an XMLHTTPRequest
346         // This can be converted when the YUI issue has been integrated:
347         // http://yuilibrary.com/projects/yui3/ticket/2531274
348         var xhr = new XMLHttpRequest();
349         var self = this;
350         xhr.onreadystatechange = function() { // Process the server response
351             if (xhr.readyState == 4 && xhr.status == 200) {
352                 var result = JSON.parse(xhr.responseText);
353                 if (result) {
354                     if (result.error) {
355                         self.hide_progress_spinner();
356                         alert(result.error);
357                     } else if (self.callback) {
358                         // Only update the filepicker if there were no errors
359                         self.hide_progress_spinner();
360                         if (result.event == 'fileexists') {
361                             // Do not worry about this, as we only care about the last
362                             // file uploaded, with the filepicker
363                             result.file = result.newfile.filename;
364                             result.url = result.newfile.url;
365                         }
366                         result.client_id = self.clientid;
367                         self.callback(result);
368                     } else {
369                         self.update_filemanager();
370                     }
371                 }
372             }
373         };
375         // Prepare the data to send
376         var formdata = new FormData();
377         formdata.append('repo_upload_file', file); // The FormData class allows us to attach a file
378         formdata.append('sesskey', M.cfg.sesskey);
379         formdata.append('repo_id', this.repositoryid);
380         formdata.append('itemid', this.itemid);
381         if (this.filemanager) { // Filepickers do not have folders
382             formdata.append('savepath', this.filemanager.currentpath);
383         }
385         if (this.acceptedtypes.constructor == Array) {
386             for (var i=0; i<this.acceptedtypes.length; i++) {
387                 formdata.append('accepted_types[]', this.acceptedtypes[i]);
388             }
389         } else {
390             formdata.append('accepted_types[]', this.acceptedtypes);
391         }
393         // Send the file & required details
394         xhr.open("POST", this.url, true);
395         xhr.send(formdata);
396         return true;
397     }
398 };