1
/* IVLE - Informatics Virtual Learning Environment
2
* Copyright (C) 2007-2008 The University of Melbourne
4
* This program is free software; you can redistribute it and/or modify
5
* it under the terms of the GNU General Public License as published by
6
* the Free Software Foundation; either version 2 of the License, or
7
* (at your option) any later version.
9
* This program is distributed in the hope that it will be useful,
10
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
* GNU General Public License for more details.
14
* You should have received a copy of the GNU General Public License
15
* along with this program; if not, write to the Free Software
16
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
18
* Module: File Browser (client)
23
/* Url names for apps */
26
service_app = "fileservice";
28
download_app = "download";
30
/* Mapping MIME types onto handlers.
31
* "text" : When navigating to a text file, the text editor is opened.
32
* "image" : When navigating to an image, the image is displayed (rather than
33
* going to the text editor).
34
* "audio" : When navigating to an audio file, a "play" button is presented.
35
* "binary" : When navigating to a binary file, offer it as a download through
38
* If a file is not on the list, its default action is determined by the first
39
* part of its content type, where "text/*", "image/*" and "audio/*" are
40
* treated as above, and other types are simply treated as binary.
43
"application/x-javascript" : "text",
44
"application/javascript" : "text",
45
"application/json" : "text",
46
"application/xml" : "text"
49
/* Mapping MIME types to icons, just the file's basename */
51
"text/directory": "dir.png",
52
"text/x-python": "py.png"
55
default_type_icon = "txt.png";
57
/* Relative to IVLE root */
58
type_icons_path = "media/images/mime";
59
type_icons_path_large = "media/images/mime/large";
61
/* Mapping SVN status to icons, just the file's basename */
64
"normal": "normal.png",
66
"missing": "missing.png",
67
"deleted": "deleted.png",
68
"modified": "modified.png",
69
"revision": "revision.png"
72
/* Mapping SVN status to "nice" strings */
74
"unversioned": "Temporary file",
75
"normal": "Permanent file",
76
"added": "Temporary file (scheduled to be added)",
77
"missing": "Permanent file (missing)",
78
"deleted": "Permanent file (scheduled for deletion)",
79
"replaced": "Permanent file (replaced)",
80
"modified": "Permanent file (modified)",
81
"merged": "Permanent file (merged)",
82
"conflicted": "Permanent file (conflicted)",
83
"revision": "Past Permanent file (revision)"
86
default_svn_icon = null;
87
default_svn_nice = "Unknown status";
89
svn_icons_path = "media/images/svn";
91
published_icon = "media/images/interface/published.png";
93
/* List of MIME types considered "executable" by the system.
94
* Executable files offer a "run" link, implying that the "serve"
95
* application can interpret them.
102
/* Global variables */
104
/** The listing object returned by the server as JSON */
109
/** Filenames of all files selected
110
* (Only used by dir listings, but still needs to be [] for files, so that
111
* update_actions knows that nothing is selected).
115
upload_callback_count = 0; /* See upload_callback */
117
/** Calls the server using Ajax, performing an action on the server side.
118
* Receives the response from the server and performs a refresh of the page
119
* contents, updating it to display the returned data (such as a directory
120
* listing, file preview, or editor pane).
121
* Always makes a POST request.
124
* \param action String. Name of the action to perform, as defined in the
126
* \param path URL path to make the request to, within the application.
127
* \param args Argument object, as described in util.parse_url and friends.
128
* This should contain the arguments to the action, but NOT the action
129
* itself. (Also a minor side-effect; the "args" object will be mutated
130
* to include the action attribute).
131
* \param content_type String, optional.
132
* May be "application/x-www-form-urlencoded" or "multipart/form-data".
133
* Defaults to "application/x-www-form-urlencoded".
134
* "multipart/form-data" is recommended for large uploads.
136
function do_action(action, path, args, content_type, ignore_response)
138
args.action = action;
139
/* Callback action, when the server returns */
140
var callback = function(response)
142
/* Check for action errors reported by the server, and report them
144
var error = response.getResponseHeader("X-IVLE-Action-Error");
146
alert("Error: " + error.toString() + ".");
147
/* Now read the response and set up the page accordingly */
148
if (ignore_response != true)
149
handle_response(path, response);
151
/* Call the server and perform the action. This mutates the server. */
152
ajax_call(callback, service_app, path, args, "POST", content_type);
155
/** Calls the server using Ajax, requesting a directory listing. This should
156
* not modify the server in any way. Receives the response from the server and
157
* performs a refresh of the page contents, updating it to display the
158
* returned data (such as a directory listing, file preview, or editor pane).
159
* Called "navigate", can also be used for a simple refresh.
160
* Always makes a GET request.
163
function navigate(path)
165
callback = function(response)
167
/* Read the response and set up the page accordingly */
168
handle_response(path, response, url.args);
170
/* Get any query strings */
171
url = parse_url(window.location.href);
173
/* Call the server and request the listing. */
174
ajax_call(callback, service_app, path, url.args, "GET");
177
/* Refreshes the current view.
178
* Calls navigate on the current path.
182
navigate(current_path);
185
/** Determines the "handler type" from a MIME type.
186
* The handler type is a string, either "text", "image", "audio" or "binary".
188
function get_handler_type(content_type)
192
if (content_type in type_handlers)
193
return type_handlers[content_type];
195
{ /* Based on the first part of the MIME type */
196
var handler_type = content_type.split('/')[0];
197
if (handler_type != "text" && handler_type != "image" &&
198
handler_type != "audio")
199
handler_type = "binary";
204
/** Given an HTTP response object, cleans up and rebuilds the contents of the
205
* page using the response data. This does not navigate away from the page, it
206
* merely rebuilds most of the data.
207
* Note that depending on the type of data returned, this could result in a
208
* directory listing, an image preview, an editor pane, etc.
209
* Figures out the type and calls the appropriate function.
210
* \param path URL path which the request was made for. This can (among other
211
* things) be used to update the URL in the location bar.
212
* \param response XMLHttpRequest object returned by the server. Should
213
* contain all the response data.
214
* \param url_args Arguments dict, for the arguments passed to the URL
215
* in the browser's address bar (will be forwarded along).
217
function handle_response(path, response, url_args)
219
/* TODO: Set location bar to "path" */
222
/* Clear away the existing page contents */
225
/* Check the status, and if not 200, read the error and handle this as an
227
if (response.status != 200)
229
var error = response.getResponseHeader("X-IVLE-Return-Error");
231
error = response.statusText;
236
/* This will always return a listing, whether it is a dir or a file.
238
var listing = response.responseText;
239
/* The listing SHOULD be valid JSON text. Parse it into an object. */
242
listing = JSON.parse(listing);
243
file_listing = listing.listing; /* Global */
247
handle_error("The server returned an invalid directory listing");
250
/* Get "." out, it's special */
251
current_file = file_listing["."]; /* Global */
252
delete file_listing["."];
254
/* Check if this is a directory listing or file contents */
255
var isdir = response.getResponseHeader("X-IVLE-Return") == "Dir";
258
handle_dir_listing(path, listing);
262
/* Need to make a 2nd ajax call, this time get the actual file
264
callback = function(response)
266
/* Read the response and set up the page accordingly */
267
handle_contents_response(path, response);
269
/* Call the server and request the listing. */
271
args = shallow_clone_object(url_args);
274
/* This time, get the contents of the file, not its metadata */
275
args['return'] = "contents";
276
ajax_call(callback, service_app, path, args, "GET");
278
update_actions(isdir);
281
function handle_contents_response(path, response)
283
/* Treat this as an ordinary file. Get the file type. */
284
var content_type = response.getResponseHeader("Content-Type");
285
var handler_type = get_handler_type(content_type);
286
would_be_handler_type = handler_type;
287
/* handler_type should now be set to either
288
* "text", "image", "audio" or "binary". */
289
switch (handler_type)
292
handle_text(path, response.responseText,
293
would_be_handler_type);
296
/* TODO: Custom image handler */
297
handle_binary(path, response.responseText);
300
/* TODO: Custom audio handler */
301
handle_binary(path, response.responseText);
309
/* Called when a form upload comes back (from an iframe).
310
* Refreshes the page.
312
function upload_callback()
314
/* This has a pretty nasty hack, which happens to work.
315
* upload_callback is set as the "onload" callback for the iframe which
316
* receives the response from the server for uploading a file.
317
* This means it gets called twice. Once when initialising the iframe, and
318
* a second time when the actual response comes back.
319
* All we want to do is call navigate to refresh the page. But we CAN'T do
320
* that on the first load or it will just go into an infinite cycle of
321
* refreshing. We need to refresh the page ONLY on the second refresh.
322
* upload_callback_count is reset to 0 just before the iframe is created.
324
upload_callback_count++;
325
if (upload_callback_count >= 2)
329
/** Deletes all "dynamic" content on the page.
330
* This returns the page back to the state it is in when the HTML arrives to
331
* the browser, ready for another handler to populate it.
335
dom_removechildren(document.getElementById("filesbody"));
338
/** Deletes all "dynamic" content on the page necessary to navigate from
339
* one directory listing to another (does not clear as much as clearpage
341
* This is the equivalent of calling clearpage() then
342
* setup_for_dir_listing(), assuming the page is already on a dir listing.
344
function clearpage_dir()
346
dom_removechildren(document.getElementById("path"));
347
dom_removechildren(document.getElementById("files"));
348
dom_removechildren(document.getElementById("sidepanel"));
351
/*** HANDLERS for different types of responses (such as dir listing, file,
354
function handle_error(message)
356
var files = document.getElementById("filesbody");
357
var txt_elem = dom_make_text_elem("div", "Error: "
358
+ message.toString() + ".")
359
txt_elem.setAttribute("class", "padding error");
360
files.appendChild(txt_elem);
363
/** Given a mime type, returns the path to the icon.
364
* \param type String, Mime type.
365
* \param sizelarge Boolean, optional.
366
* \return Path to the icon. Has applied make_path, so it is relative to site
369
function mime_type_to_icon(type, sizelarge)
372
if (type in type_icons)
373
filename = type_icons[type];
375
filename = default_type_icon;
377
return make_path(path_join(type_icons_path_large, filename));
379
return make_path(path_join(type_icons_path, filename));
382
/** Given an svnstatus, returns the path to the icon.
383
* \param type String, svn status.
384
* \return Path to the icon. Has applied make_path, so it is relative to site
385
* root. May return null to indicate no SVN icon.
387
function svnstatus_to_icon(svnstatus)
390
if (svnstatus in svn_icons)
391
filename = svn_icons[svnstatus];
393
filename = default_svn_icon;
394
if (filename == null) return null;
395
return make_path(path_join(svn_icons_path, filename));
398
/** Given an svnstatus, returns the "nice" string.
400
function svnstatus_to_string(svnstatus)
402
if (svnstatus in svn_nice)
403
return svn_nice[svnstatus];
405
return default_svn_nice;
408
/** Displays a download link to the binary file.
410
function handle_binary(path)
412
var files = document.getElementById("filesbody");
413
var div = document.createElement("div");
414
files.appendChild(div);
415
div.setAttribute("class", "padding");
416
var download_link = app_path(download_app, path);
417
var par1 = dom_make_text_elem("p",
418
"The file " + path + " is a binary file. To download this file, " +
419
"click the following link:");
420
var par2 = dom_make_link_elem("p",
421
"Download " + path, "Download " + path, download_link);
422
div.appendChild(par1);
423
div.appendChild(par2);
426
function update_actions()
429
var numsel = selected_files.length;
434
/* Display information about the current directory instead */
435
filename = path_basename(current_path);
438
else if (numsel == 1)
440
filename = selected_files[0];
441
file = file_listing[filename];
444
/* Update each action node in the topbar.
445
* This includes enabling/disabling actions as appropriate, and
446
* setting href/onclick attributes. */
450
/* Available if exactly one file is selected */
451
var open = document.getElementById("act_open");
454
open.setAttribute("class", "choice");
456
open.setAttribute("title",
457
"Navigate to this directory in the file browser");
459
open.setAttribute("title",
460
"Edit or view this file");
461
open.setAttribute("href", app_path(this_app, current_path, filename));
465
open.setAttribute("class", "disabled");
466
open.removeAttribute("title");
467
open.removeAttribute("href");
471
/* Available if exactly one file is selected,
472
* and only if this is a file, not a directory */
473
var serve = document.getElementById("act_serve");
474
if (numsel == 1 && !file.isdir)
476
serve.setAttribute("class", "choice");
477
serve.setAttribute("href",
478
app_path(serve_app, current_path, filename));
482
serve.setAttribute("class", "disabled");
483
serve.removeAttribute("href");
487
/* Available if exactly one file is selected,
488
* and it is a Python file.
494
* If 0 files selected, download the current file or directory as a ZIP.
495
* If 1 directory selected, download it as a ZIP.
496
* If 1 non-directory selected, download it.
497
* If >1 files selected, download them all as a ZIP.
499
var download = document.getElementById("act_download");
504
download.setAttribute("href",
505
app_path(download_app, current_path));
507
download.setAttribute("title",
508
"Download the current directory as a ZIP file");
510
download.setAttribute("title",
511
"Download the current file");
515
download.setAttribute("href",
516
app_path(download_app, current_path, filename));
518
download.setAttribute("title",
519
"Download the selected directory as a ZIP file");
521
download.setAttribute("title",
522
"Download the selected file");
527
/* Make a query string with all the files to download */
528
var dlpath = urlencode_path(app_path(download_app, current_path)) + "?";
529
for (var i=0; i<numsel; i++)
530
dlpath += "path=" + encodeURIComponent(selected_files[i]) + "&";
531
dlpath = dlpath.substr(0, dlpath.length-1);
532
download.setAttribute("href", dlpath);
533
download.setAttribute("title",
534
"Download the selected files as a ZIP file");
537
/* Refresh - No changes required */
539
/* Publish and Submit */
540
/* If this directory is under subversion and selected/unselected file is a
542
var publish = document.getElementById("act_publish");
543
var submit = document.getElementById("act_submit");
544
if (numsel <= 1 && file.isdir)
546
/* TODO: Work out of file is svn'd */
547
/* TODO: If this dir is already published, call it "Unpublish" */
548
publish.setAttribute("class", "choice");
549
publish.removeAttribute("disabled");
550
submit.setAttribute("class", "choice");
551
submit.removeAttribute("disabled");
555
publish.setAttribute("class", "disabled");
556
publish.setAttribute("disabled", "disabled");
557
submit.setAttribute("class", "disabled");
558
submit.setAttribute("disabled", "disabled");
562
/* If exactly 1 non-directory file is selected/opened, and its parent
563
* directory is published.
565
var share = document.getElementById("act_share");
566
if (numsel <= 1 && !file.isdir)
568
/* TODO: Work out if parent dir is published */
569
share.setAttribute("class", "choice");
570
share.removeAttribute("disabled");
574
share.setAttribute("class", "disabled");
575
share.setAttribute("disabled", "disabled");
579
/* If exactly 1 file is selected */
580
var rename = document.getElementById("act_rename");
583
rename.setAttribute("class", "choice");
584
rename.removeAttribute("disabled");
588
rename.setAttribute("class", "disabled");
589
rename.setAttribute("disabled", "disabled");
592
/* Delete, cut, copy */
593
/* If >= 1 file is selected */
594
var act_delete = document.getElementById("act_delete");
595
var cut = document.getElementById("act_cut");
596
var copy = document.getElementById("act_copy");
599
act_delete.setAttribute("class", "choice");
600
act_delete.removeAttribute("disabled");
601
cut.setAttribute("class", "choice");
602
cut.removeAttribute("disabled");
603
copy.setAttribute("class", "choice");
604
copy.removeAttribute("disabled");
608
act_delete.setAttribute("class", "disabled");
609
act_delete.setAttribute("disabled", "disabled");
610
cut.setAttribute("class", "disabled");
611
cut.setAttribute("disabled", "disabled");
612
copy.setAttribute("class", "disabled");
613
copy.setAttribute("disabled", "disabled");
616
/* Paste, new file, new directory, upload */
617
/* Always enabled (assuming this is a directory) */
619
/* Subversion actions */
620
/* TODO: Work out when these are appropriate */
621
var svnadd = document.getElementById("act_svnadd");
622
var svnrevert = document.getElementById("act_svnrevert");
623
var svncommit = document.getElementById("act_svncommit");
626
svnadd.setAttribute("class", "choice");
627
svnadd.removeAttribute("disabled");
628
svnrevert.setAttribute("class", "choice");
629
svnrevert.removeAttribute("disabled");
630
svncommit.setAttribute("class", "choice");
631
svncommit.removeAttribute("disabled");
633
var svncheckout = document.getElementById("act_svncheckout");
634
/* current_path == username: We are at the top level */
635
if (current_path == username)
637
svncheckout.setAttribute("class", "choice");
638
svncheckout.removeAttribute("disabled");
642
svncheckout.setAttribute("class", "disabled");
643
svncheckout.setAttribute("disabled", "disabled");
649
/** Event handler for when an item of the "More actions..." dropdown box is
650
* selected. Performs the selected action. */
651
function handle_moreactions()
653
var moreactions = document.getElementById("moreactions");
654
if (moreactions.value == "top")
656
var selectedaction = moreactions.value;
657
/* Reset to "More actions..." */
658
moreactions.selectedIndex = 0;
660
/* If 0 files selected, filename is the name of the current dir.
661
* If 1 file selected, filename is that file.
663
if (selected_files.length == 0)
664
filename = path_basename(current_path);
665
else if (selected_files.length == 1)
666
filename = selected_files[0];
670
/* Now handle the selected action */
671
switch(selectedaction)
674
action_publish(selected_files);
677
action_unpublish(selected_files);
681
alert("Not yet implemented: Sharing files");
685
alert("Not yet implemented: Submit");
688
action_rename(filename);
691
action_remove(selected_files);
694
action_copy(selected_files);
697
action_cut(selected_files);
709
show_uploadpanel(true);
712
action_add(selected_files);
715
action_revert(selected_files);
718
action_commit(selected_files);
726
/** Called when the page loads initially.
728
window.onload = function()
730
/* Navigate (internally) to the path in the URL bar.
731
* This causes the page to be populated with whatever is at that address,
732
* whether it be a directory or a file.
734
var path = parse_url(window.location.href).path;
735
/* Strip out root_dir + "/files" from the front of the path */
736
var strip = make_path(this_app);
737
if (path.substr(0, strip.length) == strip)
738
path = path.substr(strip.length+1);
741
/* See if this is an edit path */
742
strip = make_path(edit_app);
743
if (path.substr(0, strip.length) == strip)
745
path = path.substr(strip.length+1);
749
if (path.length == 0)
751
/* Navigate to the user's home directory by default */