a493b377fc09bfe035dbe15d6e6e907b1b3ecbb1
[bse.git] / site / htdocs / js / bse_api.js
1 // requires prototype.js for now
2
3 // true to use the File API if available
4 // TODO: progress reporting
5 // TODO: start reporting
6 // TODO: utf8 filenames
7 // TODO: quotes in filenames(?)
8 var bse_use_file_api = true;
9
10 var BSEAPI = Class.create
11   ({
12      initialize: function(parameters) {
13           if (!parameters) parameters = {};
14        this.initialized = true;
15        this.onException = function(e) {
16                             alert(e);
17                             };
18        this.onFailure = function(error) { alert(error.message); };
19        this._load_csrfp();
20        this.onConfig = parameters.onConfig;
21        this._load_config();
22      },
23      _load_csrfp: function () {
24        this.get_csrfp
25        ({
26          id: -1,
27           name: this._csrfp_names,
28          onSuccess: function(csrfp) {
29            this._csrfp = csrfp;
30            window.setTimeout(this._load_csrfp.bind(this), 600000);
31          }.bind(this),
32          onFailure: function() {
33            // ignore this
34            this._csrfp = null;
35          }
36        });
37      },
38      _load_config: function() {
39           this.get_base_config
40           ({
41               onSuccess:function(conf) {
42                   this.conf = conf;
43                   if (this.onConfig)
44                       this.onConfig(conf);
45               }.bind(this),
46               onFailure: function(err) {
47               }
48           });
49       },
50      // logon to the server
51      // logon - logon name of user
52      // password - password of user
53      // onSuccess - called on successful logon (no parameters)
54      // onFailure - called with an error object on failure.
55      logon: function(parameters) {
56        var success = parameters.onSuccess;
57        if (!success) this._badparm("logon() missing onSuccess parameter");
58        var failure = parameters.onFailure;
59        if (!failure) failure = this.onFailure;
60        if (parameters.logon == null)
61          this._badparm("logon() Missing logon parameter");
62        if (parameters.password == null)
63          this._badparm("logon() Missing password parameter");
64        new Ajax.Request('/cgi-bin/admin/logon.pl',
65        {
66          parameters: {
67            a_logon: 1,
68            logon: parameters.logon,
69            password: parameters.password
70          },
71          onSuccess: function (success, failure, resp) {
72            if (resp.responseJSON) {
73              if(resp.responseJSON.success != 0) {
74                this._load_csrfp();
75                success(resp.responseJSON.user);
76              }
77              else {
78                failure(this._wrap_json_failure(resp), resp);
79              }
80            }
81            else {
82              failure(this._wrap_nojson_failure(resp), resp);
83            }
84          }.bind(this, success, failure),
85          onFailure: function (failure, resp) {
86            failure(this._wrap_req_failure(resp), resp);
87          }.bind(this, failure),
88          onException: this.onException
89        });
90      },
91      userinfo: function(parameters) {
92        var success = parameters.onSuccess;
93        if (!success) this._badparm("logon() missing onSuccess parameter");
94        var failure = parameters.onFailure;
95        if (!failure) failure = this.onFailure;
96        new Ajax.Request('/cgi-bin/admin/logon.pl',
97        {
98          parameters: {
99            a_userinfo: 1
100          },
101          onSuccess: function (success, failure, resp) {
102            if (resp.responseJSON) {
103              if(resp.responseJSON.success != 0) {
104                success(resp.responseJSON);
105              }
106              else {
107                failure(this._wrap_json_failure(resp), resp);
108              }
109            }
110            else {
111              failure(this._wrap_nojson_failure(resp), resp);
112            }
113          }.bind(this, success, failure),
114          onFailure: function (failure, resp) {
115            failure(this._wrap_req_failure(resp), resp);
116          }.bind(this, failure),
117          onException: this.onException
118        });
119      },
120      logoff: function(parameters) {
121        var success = parameters.onSuccess;
122        if (!success) this._badparm("logon() missing onSuccess parameter");
123        var failure = parameters.onFailure;
124        if (!failure) failure = this.onFailure;
125        new Ajax.Request('/cgi-bin/admin/logon.pl',
126        {
127          parameters: {
128            a_logoff: 1
129          },
130          onSuccess: function (success, failure, resp) {
131            if (resp.responseJSON) {
132              if(resp.responseJSON.success != 0) {
133                success();
134              }
135              else {
136                failure(this._wrap_json_failure(resp), resp);
137              }
138            }
139            else {
140              failure(this._wrap_nojson_failure(resp), resp);
141            }
142          }.bind(this, success, failure),
143          onFailure: function (failure, resp) {
144            failure(this._wrap_req_failure(resp), resp);
145          }.bind(this, failure),
146          onException: this.onException
147        });
148      },
149      // fetch a tree of articles;
150      // id - parent of tree to fetch
151      // depth - optional depth of tree to fetch (default is large)
152      // onSuccess - called with tree on success
153      // onFailure - called with error object on failure
154      tree: function(parameters) {
155        var success = parameters.onSuccess;
156        if (!success) this._badparm("tree() missing onSuccess parameter");
157        var failure = parameters.onFailure;
158        if (!failure) failure = this.onFailure;
159        var req_parms = { id: -1, a_tree: 1 };
160        if (parameters.id)
161          req_parms.id = parameters.id;
162        if (parameters.depth)
163          req_parms.depth = parameters.depth;
164        new Ajax.Request('/cgi-bin/admin/add.pl',
165        {
166          parameters: req_parms,
167          onSuccess: function(success, failure, resp) {
168            if (resp.responseJSON) {
169              if (resp.responseJSON.success != 0) {
170                success(resp.responseJSON.articles);
171              }
172              else {
173                failure(this._wrap_json_failure(resp), resp);
174              }
175            }
176            else {
177              failure(this._wrap_nojson_failure(resp), resp);
178            }
179          }.bind(this, success, failure),
180          onFailure: function(failure, resp) {
181            failure(this._wrap_req_failure(resp), resp);
182          }.bind(this, failure),
183          onException: this.onException
184        });
185      },
186      article: function(parameters) {
187        var success = parameters.onSuccess;
188        if (!success) this._badparm("tree() missing onSuccess parameter");
189        var failure = parameters.onFailure;
190        if (!failure) failure = this.onFailure;
191        if (parameters.id == null)
192          this._badparm("article() missing id parameter");
193        var req_parms = { a_article: 1, id: parameters.id };
194        new Ajax.Request('/cgi-bin/admin/add.pl',
195        {
196          parameters: req_parms,
197          onSuccess: function(success, failure, resp) {
198            if (resp.responseJSON) {
199              if (resp.responseJSON.success != 0) {
200                success(resp.responseJSON.article);
201              }
202              else {
203                failure(this._wrap_json_failure(resp), resp);
204              }
205            }
206            else {
207              failure(this._wrap_nojson_failure(resp), resp);
208            }
209          }.bind(this, success, failure),
210          onFailure: function(failure, resp) {
211            failure(this._wrap_req_failure(resp), resp);
212          }.bind(this, failure),
213          onException: this.onException
214        });
215      },
216      // create a new article, accepts all article fields except id
217      new_article: function(parameters) {
218        var success = parameters.onSuccess;
219        if (!success) this._badparm("tree() missing onSuccess parameter");
220        var failure = parameters.onFailure;
221        if (!failure) failure = this.onFailure;
222        if (parameters.title == null)
223          this._badparm("new_article() missing title parameter");
224        if (parameters.parentid == null)
225          this._badparm("new_article() missing parentid parameter");
226        if (parameters.id != null)
227          this._badparm("new_article() can't accept an id parameter");
228        delete parameters.onSuccess;
229        delete parameters.onFailure;
230        this._do_add_request("save", parameters,
231          function(success, resp) {
232            success(resp.article);
233          }.bind(this, success),
234          failure);
235      },
236      save_article: function(parameters) {
237        var success = parameters.onSuccess;
238        if (!success) this._badparm("tree() missing onSuccess parameter");
239        var failure = parameters.onFailure;
240        if (!failure) failure = this.onFailure;
241        if (parameters.id == null)
242          this._badparm("save_article() missing id parameter");
243        if (parameters.lastModified == null)
244          this._badparm("save_article() missing lastModified parameter");
245        delete parameters.onSuccess;
246        delete parameters.onFailure;
247        this._do_add_request("save", parameters,
248          function(success, result) {
249            success(result.article);
250          }.bind(this, success),
251          failure);
252      },
253      get_base_config: function(parameters) {
254        var success = parameters.onSuccess;
255        if (!success) this._badparm("get_config() missing onSuccess parameter");
256        var failure = parameters.onFailure;
257        if (!failure) failure = this.onFailure;
258        delete parameters.onSuccess;
259        delete parameters.onFailure;
260        this._do_api_request("a_config", parameters, success, failure);
261       },
262      get_config: function(parameters) {
263        var success = parameters.onSuccess;
264        if (!success) this._badparm("get_config() missing onSuccess parameter");
265        var failure = parameters.onFailure;
266        if (!failure) failure = this.onFailure;
267        delete parameters.onSuccess;
268        delete parameters.onFailure;
269        if (parameters.id == null && parameters.parentid == null)
270          this._badparm("get_config() missing both id and parentid");
271        this._do_add_request("a_config", parameters, success, failure);
272      },
273      get_csrfp: function(parameters) {
274        var success = parameters.onSuccess;
275        if (!success) this._badparm("get_csrfp() missing onSuccess parameter");
276        var failure = parameters.onFailure;
277        if (!failure) failure = this.onFailure;
278        delete parameters.onSuccess;
279        delete parameters.onFailure;
280        if (parameters.id == null && parameters.id == null)
281          this._badparm("get_csrfp() missing both id and parentid");
282        this._do_add_request("a_csrfp", parameters,
283         function(success, result) {
284           success(result.tokens);
285         }.bind(this, success),
286         failure);
287      },
288      get_file_progress: function(parameters) {
289        var success = parameters.onSuccess;
290        if (!success) this._badparm("get_file_progress() missing onSuccess parameter");
291        var failure = parameters.onFailure;
292        if (!failure) failure = this.onFailure;
293        delete parameters.onSuccess;
294        delete parameters.onFailure;
295        if (parameters._upload == null)
296          this._badparm("get_file_progress() missing _upload");
297        this._do_request("/cgi-bin/fileprogress.pl", null, parameters,
298         function(success, result) {
299           success(result.progress);
300         }.bind(this, success),
301         failure);
302      },
303      thumb_link: function(im, geoid) {
304        return "/cgi-bin/thumb.pl?image="+im.id+"&g="+geoid+"&page="+im.articleId+"&f="+encodeURIComponent(im.image);
305      },
306      can_drag_and_drop: function() {
307        // hopefully they're implemented at the same time
308        if (window.FormData != null)
309          return true;
310
311        if (bse_use_file_api && window.FileReader != null)
312          return true;
313
314        return false;
315      },
316      make_drop_zone: function(options) {
317        options.element.addEventListener
318        (
319          "dragenter",
320          function(options, e) {
321            e.stopPropagation();
322            e.preventDefault();
323          }.bind(this, options),
324          false
325        );
326        options.element.addEventListener
327        (
328          "dragover",
329          function(options, e) {
330            e.stopPropagation();
331            e.preventDefault();
332          }.bind(this, options),
333          false
334        );
335        options.element.addEventListener
336        (
337          "drop",
338          function(options, e) {
339            e.stopPropagation();
340            e.preventDefault();
341
342            options.onDrop(e.dataTransfer.files);
343          }.bind(this, options),
344          false
345        );
346      },
347      // parameters:
348      //  image - file input element (required)
349      //  id - owner article of the new image (required)
350      //  name - name of the image to add (default: "")
351      //  alt - alt text for the image (default: "")
352      //  url - url for the image (default: "")
353      //  storage - storage for the image (default: auto)
354      //  onSuccess: called on success in adding the image, with the image object
355      //    (required)
356      //  onFailure: called on failure (optional)
357      //  onStart: called when the image upload starts (optional)
358      //  onComplete: called when the image upload is complete (success
359      //    or failure) (optional)
360      //  onProgress: called occasionally during the image upload with
361      //    the approximate amount sent and the total to be sent (optional)
362      add_image_file: function(parameters) {
363           parameters._csrfp = this._csrfp.admin_add_image;
364        var success = parameters.onSuccess;
365        parameters.onSuccess = function(success, result) {
366          success(result.image);
367        }.bind(this, success);
368           this._do_complex_request("/cgi-bin/admin/add.pl", "addimg", parameters);
369       },
370      save_image_file: function(parameters) {
371        parameters._csrfp = this._csrfp.admin_save_image;
372        var success = parameters.onSuccess;
373        parameters.onSuccess = function(success, result) {
374          success(result.image);
375        }.bind(this, success);
376        this._do_complex_request("/cgi-bin/admin/add.pl", "a_save_image", parameters);
377      },
378      remove_image_file: function(parameters) {
379        var success = parameters.onSuccess;
380        if (!success) this._badparm("remove_image_file() missing onSuccess parameter");
381        var failure = parameters.onFailure;
382        if (!failure) failure = this.onFailure;
383        var im = parameters.image;
384        if (!im) this._badparm("remove_image_file() missing image parameter");
385        this._do_add_request
386          (
387          "removeimg_"+im.id,
388          {
389            id: im.articleId
390          },
391          success, failure
392          );
393      },
394      images_set_order: function(parameters) {
395        var success = parameters.onSuccess;
396        if (!success) this._badparm("remove_image_file() missing onSuccess parameter");
397        var failure = parameters.onFailure;
398        if (!failure) failure = this.onFailure;
399        var id = parameters.id;
400        if (!id) this._badparm("images_set_order() missing id parameter");
401        var order = parameters.order.join(",");
402        this._do_add_request("a_order_images", { id: id, order: order }, success, failure);
403      },
404
405      // Message catalog functions
406      message_catalog: function(parameters) {
407        var success = parameters.onSuccess;
408        if (!success) this._badparm("remove_image_file() missing onSuccess parameter");
409        var failure = parameters.onFailure;
410        if (!failure) failure = this.onFailure;
411        this._do_request
412          (
413          "/cgi-bin/admin/messages.pl", "a_catalog", { },
414          function(success, resp) {
415            success(resp.messages);
416          }.bind(this, success),
417          failure
418          );
419      },
420      message_detail: function(parameters) {
421        var success = parameters.onSuccess;
422        if (!success) this._badparm("message_detail() missing onSuccess parameter");
423        var failure = parameters.onFailure;
424        if (!failure) failure = this.onFailure;
425        var id = parameters.id;
426        if (id == null) this._badparm("message_detail() missing id parameter");
427        this._do_request
428          (
429            "/cgi-bin/admin/messages.pl", "a_detail", { id: id }, success, failure
430          );
431      },
432      // requires id, language_code, message
433      message_save: function(parameters) {
434        var success = parameters.onSuccess;
435        if (!success) this._badparm("message_save() missing onSuccess parameter");
436        var my_success = function(success, resp) {
437          success(resp.definition);
438        }.bind(this, success);
439        delete parameters.success;
440        var failure = parameters.onFailure;
441        if (!failure) failure = this.onFailure;
442        delete parameters.failure;
443        this._do_request("/cgi-bin/admin/messages.pl", "a_save", parameters,
444                         my_success, failure);
445      },
446      // requires id, language_code
447      message_delete: function(parameters) {
448        var success = parameters.onSuccess;
449        if (!success) this._badparm("message_delete() missing onSuccess parameter");
450        delete parameters.success;
451        var failure = parameters.onFailure;
452        if (!failure) failure = this.onFailure;
453        delete parameters.failure;
454        this._do_request("/cgi-bin/admin/messages.pl", "a_delete", parameters,
455                         success, failure);
456      },
457
458      // requires name, value
459      set_state: function(parameters) {
460        var success = parameters.onSuccess || function() {};
461        var failure = parameters.onFailure || this.onFailure;
462        delete parameters.onSuccess;
463        delete parameters.onFailure;
464        this._do_request("/cgi-bin/admin/menu.pl", "a_set_state", parameters, success, failure);
465      },
466      // requires name
467      get_state: function(parameters) {
468        var success = parameters.onSuccess;
469        if (!success) this._badparm("get_state() missing onSuccess parameter");
470        var my_success = function(success, result) {
471          success(result.value);
472        }.bind(this, success);
473        var failure = parameters.onFailure || this.onFailure;
474        delete parameters.onSuccess;
475        delete parameters.onFailure;
476        this._do_request("/cgi-bin/admin/menu.pl", "a_get_state", parameters, my_success, failure);
477      },
478      // requires name
479      delete_state: function(parameters) {
480        var success = parameters.onSuccess || function() {};
481        var failure = parameters.onFailure || this.onFailure;
482        delete parameters.onSuccess;
483        delete parameters.onFailure;
484        this._do_request("/cgi-bin/admin/menu.pl", "a_delete_state", parameters, success, failure);
485      },
486
487      // requires name, a prefix for the state entries we want
488      get_matching_state: function(parameters) {
489        var success = parameters.onSuccess;
490        if (!success) this._badparm("get_matching_state() missing onSuccess parameter");
491        var my_success = function(success, result) {
492          success(result.entries);
493        }.bind(this, success);
494        var failure = parameters.onFailure || this.onFailure;
495        delete parameters.onSuccess;
496        delete parameters.onFailure;
497        this._do_request("/cgi-bin/admin/menu.pl", "a_get_matching_state", parameters, my_success, failure);
498
499      },
500
501      // requires name, a prefix for the state entries we want
502      delete_matching_state: function(parameters) {
503        var success = parameters.onSuccess || function() {};
504        var failure = parameters.onFailure || this.onFailure;
505        delete parameters.onSuccess;
506        delete parameters.onFailure;
507        this._do_request("/cgi-bin/admin/menu.pl", "a_delete_matching_state", parameters, success, failure);
508
509      },
510
511      _progress_handler: function(parms) {
512           if (parms.finished) return;
513        this.get_file_progress(
514        {
515          _upload: parms.up_id,
516          onSuccess: function(parms, prog) {
517            if (!parms.finished) {
518              if (prog) {
519                  if (prog.total)
520                parms.total = prog.total;
521                parms.progress(prog);
522              }
523              parms.updates += 1;
524              parms.timeout = window.setTimeout
525                (this._progress_handler.bind(this, parms),
526                  parms.updates > 5 ? 6000 : 1500);
527            }
528          }.bind(this, parms)
529        });
530      },
531      _hidden: function(name, value) {
532        var hidden = document.createElement("input");
533        hidden.type = "hidden";
534        hidden.name = name;
535        hidden.value = value;
536
537        return hidden;
538      },
539      _wrap_json_failure: function(resp) {
540        return resp.responseJSON;
541      },
542      _wrap_nojson_failure: function(resp) {
543        return {
544            success: 0,
545            message: "Unexpected non-JSON response from server",
546            errors: {},
547            error_code: "NOTJSON"
548          };
549      },
550      _wrap_req_failure: function(resp) {
551        return {
552          success: 0,
553          message: "Server error requesing content: " + resp.statusText,
554          errors: {},
555          error_code: "SERVFAIL"
556        };
557      },
558      _badparm: function(msg) {
559        this.onException(msg);
560      },
561      _add_complex_item: function(form, key, val, clone) {
562        if (typeof(val) == "string" || typeof(val) == "number") {
563          form.appendChild(this._hidden(key, val));
564        }
565        else if (typeof(val) == "object") {
566          if (val.constructor == Array) {
567            for (var i = 0; i < val.length; ++i) {
568              this._add_complex_item(form, key, val[i], clone);
569            }
570          }
571          else {
572            // presumed to be a file field
573            if (clone) {
574              var cloned = val.cloneNode(true);
575              val.parentNode.insertBefore(cloned, val);
576            }
577            val.name = key;
578            form.appendChild(val);
579          }
580        }
581      },
582      _populate_complex_form: function(form, req_parms, clone) {
583        for (var key in req_parms) {
584          this._add_complex_item(form, key, req_parms[key], clone);
585        }
586      },
587      // perform a request through an iframe
588      // parameters can contain:
589      // onSuccess: callback called on successful processs
590      // onFailure: called on failed processing
591      // onStart: called when the form is submitted
592      // onProgress: called occasionally with submission progres info
593      // onComplete: called on completion (before onSuccess/onFailure)
594      // clone: if true, clone any file objects supplied
595      //
596      // all other parameters are treated as form fields.
597      // if a value is an array, it is treated as multiple values for
598      // that field
599      //
600      // Bugs: should fallback to Ajax if there are no form fields
601      _do_complex_request: function(url, action, parameters) {
602        var success = parameters.onSuccess;
603        if (!success) this._badparm("tree() missing onSuccess parameter");
604        var failure = parameters.onFailure;
605        if (!failure) failure = this.onFailure;
606        var on_complete = parameters.onComplete;
607        var on_start = parameters.onStart;
608        var on_progress = parameters.onProgress;
609        var clone = parameters.clone;
610
611        delete parameters.onSuccess;
612        delete parameters.onFailure;
613        delete parameters.onComplete;
614        delete parameters.onProgress;
615        delete parameters.onStart;
616        delete parameters.clone;
617
618        // stuff we use in the callbacks
619        var parms =
620          {
621          success: success,
622          failure: failure,
623          start: on_start,
624          progress: on_progress,
625          complete: on_complete,
626          // track the number of progress updates done
627          updates: 0,
628          finished: 0
629          };
630
631        parms.up_id = this._new_upload_id();
632        if (url.match(/\?/))
633          url += "&";
634        else
635          url += "?";
636        url += "_upload=" + parms.up_id;
637
638        if (window.FormData) {
639          if (this._do_complex_formdata(url, action, parms, parameters))
640            return;
641        }
642
643        if (window.FileReader && bse_use_file_api) {
644          if (this._do_complex_file_api(url, action, parms, parameters))
645            return;
646        }
647
648        // setup the iframe
649        parms.ifr = new Element("iframe", {
650          src: "about:blank",
651          id: "bseiframe"+parms.up_id,
652          name: "bseiframe"+parms.up_id,
653          width: 400,
654          height: 100
655          });
656        parms.ifr.style.display = "none";
657
658        // setup the form
659        var form = new Element
660        ("form",
661        {
662          method: "post",
663          action: url,
664          enctype: "multipart/form-data",
665          // the following for IE
666          encoding: "multipart/form-data",
667          id: "bseform"+parms.up_id,
668          target: "bseiframe"+parms.up_id
669        });
670        parms.form = form;
671        form.style.display = "none";
672        // _upload must come before anything large
673        //form.appendChild(this._hidden("_upload", parms.up_id));
674        form.appendChild(this._hidden("_",1));
675        this._populate_complex_form(form, parameters, clone);
676        // trigger BSE's alternative JSON return handling
677        form.appendChild(this._hidden("_", 1));
678        form.appendChild(this._hidden(action, 1));
679
680        document.body.appendChild(parms.ifr);
681        document.body.appendChild(form);
682        var onLoad = function(parms) {
683          // we should get json back in the body
684          var ifr = parms.ifr;
685          var form = parms.form;
686          var text = Try.these(
687            function(ifr) {
688              var text = ifr.contentDocument.body.textContent;
689              ifr.contentDocument.close();
690              return text;
691            }.bind(this, ifr),
692            function(ifr) {
693              var text = ifr.contentWindow.document.body.innerText;
694              ifr.contentWindow.document.close();
695              return text;
696            }.bind(this, ifr)
697          );
698          var data;
699          eval("data = " + text + ";");
700          document.body.removeChild(ifr);
701          document.body.removeChild(form);
702          if (parms.progress != null && parms.total != null)
703            parms.progress({ done: parms.total, total: parms.total});
704          if (parms.complete != null)
705            parms.complete();
706          parms.finished = 1;
707          if (data != null) {
708            if (data.success != null && data.success != 0) {
709              parms.success(data);
710            }
711            else {
712              parms.failure(data);
713            }
714          }
715          else {
716            parms.failure(this._wrap_req_failure({statusText: "Unknown"}));
717          }
718        }.bind(this, parms);
719        if (window.attachEvent) {
720          parms.ifr.attachEvent("onload", onLoad);
721        }
722        else {
723          parms.ifr.addEventListener("load", onLoad, false);
724        }
725
726        if (on_start != null)
727          on_start();
728
729        if (on_progress != null) {
730          parms.timeout = window.setTimeout ( this._progress_handler.bind(this, parms), 200 );
731        }
732
733        form.submit();
734      },
735      // flatten the parameters
736      _flat_parms: function(flat, key, val) {
737        if (typeof(val) == "string" || typeof(val) == "number") {
738          flat.push([ key, val, false ]);
739        }
740        else if (typeof(val) == "object") {
741          if (val.constructor == Array) {
742            for (var i = 0; i < val.length; ++i) {
743              this._flat_parms(flat, key, val[i]);
744            }
745          }
746          else if (val.constructor == File) {
747            // File object from drag and drop
748            flat.push([key, val, true]);
749          }
750          else {
751            // this should handle File objects, not just elements
752            // or perhaps data transfer objects
753            // push the individual files if there's multiple
754            for (var i = 0; i < val.files.length; ++i) {
755              flat.push([key, val.files[i], true]);
756            }
757          }
758        }
759      },
760     _file_progress_event: function(state, evt) {
761       if (evt.lengthComputable) {
762         var filename;
763         for (var i = 0; i < state.fileoffsets.length; ++i) {
764           if (evt.loaded > state.fileoffsets[i][0])
765             filename = state.fileoffsets[i][1];
766         }
767         state.last_filename = filename;
768         state.progress
769         (
770           {
771             done: evt.loaded,
772             total: evt.total,
773             filename: filename,
774             complete: 0
775           }
776         );
777       }
778     },
779     _file_load_event: function(state, evt) {
780       if (evt.lengthComputable) {
781         state.progress
782         (
783           {
784             done: evt.total,
785             total: evt.total,
786             filename: state.last_filename,
787             complete: 1
788           }
789         );
790       }
791     },
792     _file_readystatechange_event: function(state, event) {
793          if (state.xhr.readyState == 4) {
794            if (state.complete)
795              state.complete();
796            if (state.xhr.status == 200) {
797              var data;
798              try {
799                data = state.xhr.responseText.evalJSON(false);
800              } catch (e) {
801                state.failure(this._wrap_nojson_failure(state.xhr));
802                return;
803              }
804
805              if (data.success != null && data.success != 0 ) {
806                state.success(data);
807              }
808              else {
809                state.failure(this._wrap_json_failure({ responseJSON: data}));
810              }
811            }
812            else {
813              state.failure(this._wrap_req_failure(state.xhr));
814            }
815          }
816     },
817      _build_api_req_data: function(state) {
818        while (state.index < state.flat.length) {
819          var entry = state.flat[state.index];
820          if (entry[2]) {
821            // file object
822            var fr  = new FileReader;
823            fr.addEventListener
824            ("loadend", function(state, fr, event) {
825               var entry = state.flat[state.index];
826               state.req_data += "--" + state.sep + "\r\n";
827               // TODO: filenames with quotes
828               state.fileoffsets.push([ state.req_data.length, entry[1].fileName]);
829               state.req_data += "Content-Disposition: form-data; name=\"" + entry[0] + "\"; filename=\"" + this._encode_utf8(entry[1].fileName) + "\"\r\n\r\n";
830               state.req_data += event.target.result + "\r\n";
831               ++state.index;
832               this._build_api_req_data(state);
833             }.bind(this, state, fr), false);
834            fr.readAsBinaryString(entry[1]);
835            return;
836          }
837          else {
838            // just plain data
839            state.req_data += "--" + state.sep;
840            state.req_data += "Content-Disposition: form-data; name=\"" + entry[0] + "\"\r\n\r\n";
841            state.req_data += this._encode_utf8(entry[1]) + "\r\n";
842            ++state.index;
843          }
844        }
845
846        // everything should be state.req_data now
847        state.req_data += "--"  + state.sep + "--\r\n";
848
849        state.xhr = new XMLHttpRequest();
850        if (state.start)
851          state.start();
852        if (state.progress && state.xhr.upload) {
853          state.xhr.upload.addEventListener
854            (
855            "progress",
856             this._file_progress_event.bind(this, state),
857            false
858            );
859          state.xhr.upload.addEventListener
860            (
861            "load",
862              this._file_load_event.bind(this, state),
863            false
864            );
865        }
866        state.xhr.open("POST", state.url, true);
867        state.xhr.onreadystatechange = this._file_readystatechange_event.bind(this, state);
868        state.xhr.setRequestHeader("Content-Type", "multipart/form-data; boundary="+state.sep);
869        state.xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
870        state.xhr.sendAsBinary(state.req_data);
871      },
872      // use the HTML5 file API to perform the upload
873      _do_complex_file_api: function(url, action, state, req_parms) {
874        state.url = url;
875        //state.url = "/cgi-bin/dump.pl";
876        if (action != null)
877          req_parms[action] = 1;
878        state.sep = "x" + state.up_id + "x";
879        state.fileoffsets = new Array;
880
881        // flatten the request parameters
882        var flat = new Array;
883        for (var key in req_parms) {
884          this._flat_parms(flat, key, req_parms[key]);
885        }
886        state.index = 0;
887        state.flat = flat;
888        state.req_data = '';
889        this._build_api_req_data(state);
890        // the rest happens elsewhere
891
892        return true;
893      },
894     _do_complex_formdata: function(url, action, state, params) {
895       state.url = url;
896       if (action != null)
897         params[action] = 1;
898
899       state.fileoffsets = new Array();
900
901       var offset = 0;
902       var fd = new FormData();
903       for (var key in params) {
904         var val = params[key];
905         if (typeof(val) == "string" || typeof(val) == "number") {
906           fd.append(key, val);
907         }
908         else {
909           if (val.constructor == Array) {
910             for (var i = 0; i < val.length; ++i) {
911               fd.append(key, val);
912             }
913           }
914           else if (val.constructor == File) {
915             // file object
916             fd.append(key, val);
917           }
918           else {
919             // hopefully a file input
920             for (var i = 0; i < val.files.length; ++i) {
921               var file = val.files[i];
922               state.fileoffsets.push([ offset, file.fileName ]);
923               fd.append(key, file);
924               offset += file.fileSize;
925             }
926           }
927         }
928       }
929
930       // FIXME: duplicate code (mostly)
931       state.xhr = new XMLHttpRequest();
932       if (state.start)
933         state.start();
934        if (state.progress && state.xhr.upload) {
935          state.xhr.upload.addEventListener
936            (
937            "progress",
938              this._file_progress_event.bind(this, state),
939            false
940            );
941          state.xhr.upload.addEventListener
942            (
943            "load",
944              this._file_load_event.bind(this, state),
945            false
946            );
947        }
948        state.xhr.open("POST", state.url, true);
949       state.xhr.onreadystatechange = this._file_readystatechange_event.bind(this, state);
950       // is this needed?
951       //state.xhr.setRequestHeader("Content-Type", "multipart/form-data; boundary="+state.sep);
952        state.xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
953        state.xhr.send(fd);
954
955         return true;
956     },
957      // in the future this might call a proxy
958      _do_add_request: function(action, other_parms, success, failure) {
959        this._do_request("/cgi-bin/admin/add.pl", action, other_parms, success, failure);
960      },
961      _do_api_request: function(action, other_parms, success, failure) {
962        this._do_request("/cgi-bin/api.pl", action, other_parms, success, failure);
963      },
964      _do_request: function(url, action, other_parms, success, failure) {
965        if (action != null)
966          other_parms[action] = 1;
967        new Ajax.Request(url,
968        {
969          parameters: other_parms,
970          onSuccess: function (success, failure, resp) {
971            if (resp.responseJSON) {
972              if (resp.responseJSON.success != null && resp.responseJSON.success != 0) {
973                success(resp.responseJSON);
974              }
975              else {
976                failure(this._wrap_json_failure(resp), resp);
977              }
978            }
979            else {
980              failure(this._wrap_nojson_failure(resp), resp);
981            }
982          }.bind(this, success, failure),
983          onFailure: function(failure, resp) {
984            failure(this._wrap_req_failure(resp), resp);
985          }.bind(this, failure),
986          onException: this.onException
987        });
988      },
989      _new_upload_id: function () {
990        this._upload_id += 1;
991        return new Date().valueOf() + "_" + this._upload_id;
992      },
993      _encode_utf8: function(str) {
994        return unescape(encodeURIComponent(str));
995      },
996      // we request these names on startup, on login
997      // and occasionally otherwise, to avoid them going stale
998      _csrfp_names:
999        [
1000          "admin_add_image",
1001          "admin_save_image"
1002        ],
1003      _upload_id: 0
1004    });
1005