MDL-31000 Added comment about the importance of repository array keys and hardened...
[moodle.git] / lib / form / dndupload.js
CommitLineData
f08fac7c
DS
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/>.
15
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 */
24
25M.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,
51
52
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;
72
73 if (!this.browser_supported()) {
74 return; // Browser does not support the required functionality
75 }
76
77 // try and retrieve enabled upload repository
78 this.repositoryid = this.get_upload_repositoryid(options.repositories);
79
80 if (!this.repositoryid) {
81 return; // no upload repository is enabled to upload to
82 }
83
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);
90
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) {
96
97 // Needed to tell the filepicker to update when a new
98 // file is uploaded
99 this.callback = options.formcallback;
100 } else {
6b37c2b0
SH
101 if (M.cfg.developerdebug) {
102 alert('dndupload: Need to define either options.filemanager or options.callback');
103 }
f08fac7c
DS
104 return;
105 }
106
107 this.init_events();
108 this.Y.one('#dndenabled-'+this.clientid).setStyle('display', 'inline');
109 },
110
111 /**
112 * Check the browser has the required functionality
113 * @return true if browser supports drag/drop upload
114 */
115 browser_supported: function() {
116
6b37c2b0 117 if (typeof FileReader == 'undefined') {
f08fac7c
DS
118 return false;
119 }
6b37c2b0 120 if (typeof FormData == 'undefined') {
f08fac7c
DS
121 return false;
122 }
123 return true;
124 },
125
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) {
6b37c2b0
SH
133 for (var i in repositories) {
134 if (repositories[i].type == "upload") {
135 return repositories[i].id;
f08fac7c
DS
136 }
137 }
138
139 return false;
140 },
141
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 },
152
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 }
165
166 if (this.reached_maxfiles()) {
167 return false;
168 }
169
170 e.preventDefault();
171 e.stopPropagation();
172
173 return true;
174 },
175
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 }
184
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 }
190
191 this.show_upload_ready();
192 return false;
193 },
194
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 }
203
204 this.entercount--;
205 if (this.entercount == 1) {
206 return false; // Just moved over a child element - nothing to do
207 }
208
209 this.entercount = 0;
210 this.hide_upload_ready();
211 return false;
212 },
213
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 }
222
223 return false;
224 },
225
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 }
234
235 this.entercount = 0;
236 this.hide_upload_ready();
237 this.show_progress_spinner();
238
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 }
255
256 return false;
257 },
258
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 },
274
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 },
288
289 /**
290 * Highlight the destination node
291 */
292 show_upload_ready: function() {
293 this.container.addClass('dndupload-over');
294 },
295
296 /**
297 * Remove highlight on destination node
298 */
299 hide_upload_ready: function() {
300 this.container.removeClass('dndupload-over');
301 },
302
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 },
312
313 /**
314 * Remove progress spinner in the destination node
315 */
316 hide_progress_spinner: function() {
317 this.Y.one('#dndprogresspinner-'+this.clientid).remove();
318 },
319
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 },
331
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 }
341
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 };
374
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 }
384
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 }
392
393 // Send the file & required details
394 xhr.open("POST", this.url, true);
395 xhr.send(formdata);
396 return true;
397 }
398};