diff --git a/deluge/ui/webui/deluge_webserver.py b/deluge/ui/webui/deluge_webserver.py index 8466565a8..6c03673a1 100644 --- a/deluge/ui/webui/deluge_webserver.py +++ b/deluge/ui/webui/deluge_webserver.py @@ -67,16 +67,18 @@ web.webapi.internalerror = deluge_debugerror import pages import config_tabs_webui #auto registers in ConfigUiManager import config_tabs_deluge #auto registers in ConfigUiManager -import register_menu -# +import register_menu #auto registers. +#manual register: import torrent_add -import torrent_options -import torrent_move -import config_forms torrent_add.register() +import torrent_options torrent_options.register() +import torrent_move torrent_move.register() +import config_forms config_forms.register() +import json_api +json_api.register() #/self registering pages. diff --git a/deluge/ui/webui/json_api.py b/deluge/ui/webui/json_api.py index 9459a3f4c..4f238e8b0 100644 --- a/deluge/ui/webui/json_api.py +++ b/deluge/ui/webui/json_api.py @@ -3,7 +3,7 @@ # # webserver_framework.py # -# Copyright (C) Martijn Voncken 2007 +# Copyright (C) Martijn Voncken 2008 # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -32,180 +32,103 @@ # statement from all source files in the program, then also delete it here. """ json api. -only used for XUL and/or external scripts -it would be possible not to include the python-json dependency. + +design: +== Full client api == + * url : /json/rpc/ + * rpc-api : http://en.wikipedia.org/wiki/JSON-RPC + * methods : http://dev.deluge-torrent.org/wiki/Development/UiClient#Remoteapi + """ - -from new import instancemethod -from inspect import getargspec -from utils import get_torrent_status,get_category_choosers, get_stats,filter_torrent_state,fsize,fspeed -from page_decorators import remote -from operator import attrgetter +from traceback import format_exc import web -from webserver_common import proxy, log +from web import webapi +from deluge.ui.client import sclient +from deluge.log import LOG as log +from deluge import component -def to_json(obj): - from lib.pythonize import pythonize - obj = pythonize(obj) - try: - import json - return json.write(obj) - except ImportError: - raise ImportError("""Install python-json using your package-manager - http://sourceforge.net/projects/json-py/""") +from page_decorators import check_session -class json_api: + +class json_rpc: """ - eperimental json api - generic proxy for all methods onm self. + == Full client api == + * url : /json/rpc + * rpc-api : http://en.wikipedia.org/wiki/JSON-RPC#Version_1.0 + * methods : http://dev.deluge-torrent.org/wiki/Development/UiClient#Remoteapi """ - illegal_methods = ['shutdown', 'socket', 'xmlrpclib','pickle','os', - 'is_localhost','CoreProxy','connect_on_new_core', 'connect_on_no_core', - 'connected','deluge','GET','POST'] - def __init__(self): - self._add_proxy_methods() + def GET(self): + print '{"error":"only POST is supported"}' - #extra exposed: - get_torrent_status = get_torrent_status + #security bug: does not check session!! + def POST(self): + web.header("Content-Type", "application/x-json") + id = 0 + try: + import json #not listed in dependency's. + except: + log.error('{"error":"python-json is not installed"}') + return '{"error":"python-json is not installed"}' + try: + log.debug("json-data:") + log.debug(webapi.data()) + json_data = json.read(webapi.data()) + id = json_data["id"] + method = json_data["method"] + params = json_data["params"] - @remote - def POST(self,name): - import json - if name.startswith('_'): - raise AttributeError('_ methods are illegal.') - if name in self.illegal_methods: - raise AttributeError('Illegal method.') - if not(hasattr(self,name)): - raise AttributeError('No such method') + log.debug("PP==%s" % json_data["params"]) - method = getattr(self,name) - vars = web.input(kwargs= None) - log.debug('vars=%s' % vars) - if vars.kwargs: - kwargs = json.read(vars.kwargs) - else: - kwargs = {} + if method.startswith('_'): + raise Exception('_ methods are illegal.') - result = method(**kwargs) + if method.startswith("system."): + result = self.exec_system_method(method, params,id) + else: + result = self.exec_client_method(method, params,id) - return "(" + to_json(result) + ")" + log.debug("JSON-result:%s(%s)[%s] = %s" % (method, params, id, result)) + print json.write(result) + except Exception,e: + #verbose because you don't want exeptions in the error-handler. + message = "" + if hasattr(e,"message"): + message = e.message + log.debug(format_exc()) + log.error("JSON-error:%s:%s %s" % (e, message, id)) + print json.write({ + "version":"1.1", + "id":id, + "error":{ + "number":123, + "message":"%s:%s %s" % (e, message, id), + "error":format_exc() + } + }) - def list_methods(self): - """ - list all json methods - returns a dict of {methodname:{args:[list of kwargs],doc:'string'},..} - """ - methods = [getattr(self,m) for m in dir(self) - if not m.startswith('_') - and (not m in self.illegal_methods) - and callable(getattr(self,m)) - ] + def exec_client_method(self, method, params, id): + if not hasattr(sclient,method): + raise Exception('Unknown method:%s', method) - return dict([(f.__name__, - {'args':getargspec(f)[0],'doc':(f.__doc__ or '').strip()}) - for f in methods]) - - def _add_proxy_methods(self): - methods = [getattr(proxy,m) for m in dir(proxy) - if not m.startswith('_') - and (not m in self.illegal_methods) - and callable(getattr(proxy,m)) - ] - for m in methods: - setattr(self,m.__name__,m) - - #extra's: - def list_torrents(self): - return [get_torrent_status(torrent_id) - for torrent_id in proxy.get_session_state()] - - def simplify_torrent_status(self, torrent): - """smaller subset and preformatted data for the treelist""" - data = { - "id":torrent.id, - "message":torrent.message, - "name":torrent.name, - "total_size":fsize(torrent.total_size), - "progress":torrent.progress, - "category":torrent.category, - "seeds":"", - "peers":"", - "download_rate":"", - "upload_rate":"", - "eta":"", - "distributed_copies":"", - "ratio":"", - "calc_state_str":torrent.calc_state_str, - "queue_pos":torrent.queue_pos - } - if torrent.total_seeds > 0: - data['seeds'] = "%s (%s)" % (torrent.num_seeds, torrent.total_seeds) - if torrent.total_peers > 0: - data['peers'] = "%s (%s)" % (torrent.num_peers, torrent.total_peers) - if torrent.download_rate > 0: - data['download_rate'] = fspeed(torrent.download_rate) - if torrent.upload_rate > 0: - data['upload_rate'] = fspeed(torrent.upload_rate) - if torrent.eta > 0: - data['eta'] = ("%.3f" % torrent.eta) - if torrent.distributed_copies > 0: - data['distributed_copies'] = "%.3f" % torrent.distributed_copies - if torrent.ratio > 0: - data['ratio'] = "%.3f" % torrent.ratio - return data - - def update_ui(self, filter=None, category=None ,sort='name' ,order='down'): - """ - Combines the most important ui calls into 1 composite call. - xmlhttp requests are expensive,max 2 running at the same time. - and performance over the internet is mostly related to the number - of requests (low ping) - returns : - {torrent_list:[{},..],'categories':[],'filters':'','stats':{}} - """ - torrent_list = self.list_torrents(); - filter_tabs, category_tabs = get_category_choosers(torrent_list) - - - #filter-state - if filter: - torrent_list = filter_torrent_state(torrent_list, filter) - - #filter-cat - if category: - torrent_list = [t for t in torrent_list if t.category == category] - - #sorting - if sort: - torrent_list.sort(key=attrgetter(sort)) - if order == 'up': - torrent_list = reversed(torrent_list) - - torrent_list = [self.simplify_torrent_status(t) for t in torrent_list] + #Call: + func = getattr(sclient, method) + log.debug("PARAMS=%s" % params) + result = func(*params) return { - 'torrent_list':torrent_list, - 'categories':category_tabs, - 'filters':filter_tabs, - 'stats':get_stats() + "version":"1.1", + "result":result, + "id":id } + def exec_system_method(self, method, params, id): + if method == "system.listMethods": + return self.exec_client_method("list_methods", params, id) + raise Exception('Unknown method:%s', method) +def register(): + component.get("PageManager").register_page("/json/rpc",json_rpc) if __name__ == '__main__': - from pprint import pprint - #proxy.set_core_uri('http://localhost:58846') #How to configure this? - j = json_api() - if True: - print 'list-methods:' - methods = j.list_methods() - names = methods.keys() - names.sort() - for name in names: - m = methods[name] - print "%s(%s)\n %s\n" % (name , m['args'] , m['doc']) - - #j.GET('list_torrents') - j.POST('list_torrents') - + print "todo: tests" diff --git a/deluge/ui/webui/templates/ajax_demo/index.html b/deluge/ui/webui/templates/ajax_demo/index.html new file mode 100644 index 000000000..d85f7da4d --- /dev/null +++ b/deluge/ui/webui/templates/ajax_demo/index.html @@ -0,0 +1,84 @@ +$def with (torrent_list, organize_filters) + + +$:render.header(_("AJAX index"), 'home') + + + + +
Just a demo template for damoxc +(or anyone else who wants to make an ajax template)
+ + + + + + + + + +
+ +
+ +$:render.footer() + + diff --git a/deluge/ui/webui/templates/ajax_demo/meta.cfg b/deluge/ui/webui/templates/ajax_demo/meta.cfg new file mode 100644 index 000000000..83408b046 --- /dev/null +++ b/deluge/ui/webui/templates/ajax_demo/meta.cfg @@ -0,0 +1,8 @@ +{ + 'authors':['Martijn Voncken '], + 'inherits':['deluge','advanced','white'], + 'comment':""" + Demo template for external ajax devs. + """ +} + diff --git a/deluge/ui/webui/templates/ajax_demo/static/js/rpc.js b/deluge/ui/webui/templates/ajax_demo/static/js/rpc.js new file mode 100644 index 000000000..5e01528a2 --- /dev/null +++ b/deluge/ui/webui/templates/ajax_demo/static/js/rpc.js @@ -0,0 +1,1030 @@ +/* + * JSON/XML-RPC Client + * Version: 0.8.0.2 (2007-12-06) + * Copyright: 2007, Weston Ruter + * License: GNU General Public License, Free Software Foundation + * + * + * Original inspiration for the design of this implementation is from jsolait, from which + * are taken the "ServiceProxy" name and the interface for synchronous method calls. + * + * See the following specifications: + * - XML-RPC: + * - JSON-RPC 1.0: + * - JSON-RPC 1.1 (draft): + * + * Usage: + * var service = new rpc.ServiceProxy("/app/service", { + * asynchronous: true, //default: true + * sanitize: true, //default: true + * methods: ['greet'], //default: null (synchronous introspection populates) + * protocol: 'JSON-RPC', //default: JSON-RPC + * }); + * service.greet({ + * params:{name:"World"}, + * onSuccess:function(message){ + * alert(message); + * }, + * onException:function(e){ + * alert("Unable to greet because: " + e); + * return true; + * } + * }); + * + * If you create the service proxy with asynchronous set to false you may execute + * the previous as follows: + * + * try { + * var message = service.greet("World"); + * alert(message); + * } + * catch(e){ + * alert("Unable to greet because: " + e); + * } + * + * Finally, if the URL provided is on a site that violates the same origin policy, + * then you may only create an asynchronous proxy, the resultant data may not be + * sanitized, and you must provide the methods yourself. In order to obtain the + * method response, the JSON-RPC server must be provided the name of a callback + * function which will be generated in the JavaScript (json-in-script) response. The HTTP GET + * parameter for passing the callback function is currently non-standardized and so + * varies from server to server. Create a service proxy with the option + * 'callbackParamName' in order to specify the callback function name parameter; + * the default is 'JSON-response-callback', as used by associated JSON/XML-RPC + * Server project. For example, getting Google Calendar data: + * + * var gcalService = new rpc.ServiceProxy("http://www.google.com/calendar/feeds/myemail%40gmail.com/public", { + * asynchronous: true, //true (default) required, otherwise error raised + * sanitize: false, //explicit false required, otherwise error raised + * methods: ['full'] //explicit list required, otherwise error raised + * callbackParamName: 'callback' + * }); + * gcalService.full({ + * params:{ + * alt:'json-in-script' //required for this to work + * 'start-min':new Date() //automatically converted to ISO8601 + * //other Google Calendar parameters + * }, + * onSuccess:function(json){ + * json.feed.entry.each(function(entry){ + * //do something + * }); + * } + * }); + */ + +var rpc = { + version:"0.8.0.2", + requestCount: 0 +}; + +rpc.ServiceProxy = function(serviceURL, options){ + //if(typeof Prototype == 'undefined') + // throw Error("The RPC client currently requires the use of Prototype."); + this.__serviceURL = serviceURL; + + //Determine if accessing the server would violate the same origin policy + this.__isCrossSite = false; + var urlParts = this.__serviceURL.match(/^(\w+:)\/\/([^\/]+?)(?::(\d+))?(?:$|\/)/); + if(urlParts){ + this.__isCrossSite = ( + location.protocol != urlParts[1] || + document.domain != urlParts[2] || + location.port != (urlParts[3] || "") + ); + } + + //Set other default options + var providedMethodList; + this.__isAsynchronous = true; + this.__isResponseSanitized = true; + this.__authUsername = null; + this.__authPassword = null; + this.__callbackParamName = 'JSON-response-callback'; + this.__protocol = 'JSON-RPC'; + this.__dateEncoding = 'ISO8601'; // ("@timestamp@" || "@ticks@") || "classHinting" || "ASP.NET" + this.__decodeISO8601 = true; //JSON only + + //Get the provided options + if(options instanceof Object){ + if(options.asynchronous !== undefined){ + this.__isAsynchronous = !!options.asynchronous; + if(!this.__isAsynchronous && this.__isCrossSite) + throw Error("It is not possible to establish a synchronous connection to a cross-site RPC service."); + } + if(options.sanitize != undefined) + this.__isResponseSanitized = !!options.sanitize; + if(options.user != undefined) + this.__authUsername = options.user; + if(options.password != undefined) + this.__authPassword = options.password; + if(options.callbackParamName != undefined) + this.__callbackParamName = options.callbackParamName; + if(String(options.protocol).toUpperCase() == 'XML-RPC') + this.__protocol = 'XML-RPC'; + if(options.dateEncoding != undefined) + this.__dateEncoding = options.dateEncoding; + if(options.decodeISO8601 != undefined) + this.__decodeISO8601 = !!options.decodeISO8601; + providedMethodList = options.methods; + } + if(this.__isCrossSite){ + if(this.__isResponseSanitized){ + throw Error("You are attempting to access a service on another site, and the JSON data returned " + + "by cross-site requests cannot be sanitized. You must therefore explicitly set the " + + "'sanitize' option to false (it is true by default) in order to proceed with making " + + "potentially insecure cross-site rpc calls."); + } + else if(this.__protocol == 'XML-RPC') + throw Error("Unable to use the XML-RPC protocol to access services on other domains."); + } + + //Obtain the list of methods made available by the server + if(this.__isCrossSite && !providedMethodList) + throw Error("You must manually supply the service's method names since auto-introspection is not permitted for cross-site services."); + if(providedMethodList) + this.__methodList = providedMethodList; + else { + //Introspection must be performed synchronously + var async = this.__isAsynchronous; + this.__isAsynchronous = false; + this.__methodList = this.__callMethod("system.listMethods", []); + this.__isAsynchronous = async; + } + this.__methodList.push('system.listMethods'); + this.__methodList.push('system.describe'); + + //Create local "wrapper" functions which reference the methods obtained above + for(var methodName, i = 0; methodName = this.__methodList[i]; i++){ + //Make available the received methods in the form of chained property lists (eg. "parent.child.methodName") + var methodObject = this; + var propChain = methodName.split(/\./); + for(var j = 0; j+1 < propChain.length; j++){ + if(!methodObject[propChain[j]]) + methodObject[propChain[j]] = {}; + methodObject = methodObject[propChain[j]]; + } + + //Create a wrapper to this.__callMethod with this instance and this methodName bound + var wrapper = (function(instance, methodName){ + var call = {instance:instance, methodName:methodName}; //Pass parameters into closure + return function(){ + if(call.instance.__isAsynchronous){ + if(arguments.length == 1 && arguments[0] instanceof Object){ + call.instance.__callMethod(call.methodName, + arguments[0].params, + arguments[0].onSuccess, + arguments[0].onException, + arguments[0].onComplete); + } + else { + call.instance.__callMethod(call.methodName, + arguments[0], + arguments[1], + arguments[2], + arguments[3]); + } + return undefined; + } + else return call.instance.__callMethod(call.methodName, rpc.toArray(arguments)); + }; + })(this, methodName); + + methodObject[propChain[propChain.length-1]] = wrapper; + } +}; + +rpc.setAsynchronous = function(serviceProxy, isAsynchronous){ + if(!isAsynchronous && serviceProxy.__isCrossSite) + throw Error("It is not possible to establish a synchronous connection to a cross-site RPC service."); + serviceProxy.__isAsynchronous = !!isAsynchronous; +}; + +rpc.ServiceProxy.prototype.__callMethod = function(methodName, params, successHandler, exceptionHandler, completeHandler){ + rpc.requestCount++; + + //Verify that successHandler, exceptionHandler, and completeHandler are functions + if(this.__isAsynchronous){ + if(successHandler && typeof successHandler != 'function') + throw Error('The asynchronous onSuccess handler callback function you provided is invalid; the value you provided (' + successHandler.toString() + ') is of type "' + typeof(successHandler) + '".'); + if(exceptionHandler && typeof exceptionHandler != 'function') + throw Error('The asynchronous onException handler callback function you provided is invalid; the value you provided (' + exceptionHandler.toString() + ') is of type "' + typeof(exceptionHandler) + '".'); + if(completeHandler && typeof completeHandler != 'function') + throw Error('The asynchronous onComplete handler callback function you provided is invalid; the value you provided (' + completeHandler.toString() + ') is of type "' + typeof(completeHandler) + '".'); + } + + try { + //Assign the provided callback function to the response lookup table + if(this.__isAsynchronous || this.__isCrossSite){ + rpc.pendingRequests[String(rpc.requestCount)] = { + //method:methodName, + onSuccess:successHandler, + onException:exceptionHandler, + onComplete:completeHandler + }; + } + + //Asynchronous cross-domain call (JSON-in-Script) ----------------------------------------------------- + if(this.__isCrossSite){ //then this.__isAsynchronous is implied + + //Create an ad hoc function specifically for this cross-site request; this is necessary because it is + // not possible pass an JSON-RPC request object with an id over HTTP Get requests. + rpc.callbacks['r' + String(rpc.requestCount)] = (function(instance, id){ + var call = {instance: instance, id: id}; //Pass parameter into closure + return function(response){ + if(response instanceof Object && (response.result || response.error)){ + response.id = call.id; + instance.__doCallback(response); + } + else {//Allow data without response wrapper (i.e. GData) + instance.__doCallback({id: call.id, result: response}); + } + } + })(this, rpc.requestCount); + //rpc.callbacks['r' + String(rpc.requestCount)] = new Function("response", 'response.id = ' + rpc.requestCount + '; this.__doCallback(response);'); + + //Make the request by adding a SCRIPT element to the page + var script = document.createElement('script'); + script.setAttribute('type', 'text/javascript'); + var src = this.__serviceURL + + '/' + methodName + + '?' + this.__callbackParamName + '=rpc.callbacks.r' + (rpc.requestCount); + if(params) + src += '&' + rpc.toQueryString(params); + script.setAttribute('src', src); + script.setAttribute('id', 'rpc' + rpc.requestCount); + var head = document.getElementsByTagName('head')[0]; + rpc.pendingRequests[rpc.requestCount].scriptElement = script; + head.appendChild(script); + + return undefined; + } + //Calls made with XMLHttpRequest ------------------------------------------------------------ + else { + //Obtain and verify the parameters + if(params){ + if(!(params instanceof Object) || params instanceof Date) //JSON-RPC 1.1 allows params to be a hash not just an array + throw Error('When making asynchronous calls, the parameters for the method must be passed as an array (or a hash); the value you supplied (' + String(params) + ') is of type "' + typeof(params) + '".'); + //request.params = params; + } + + //Prepare the XML-RPC request + var request,postData; + if(this.__protocol == 'XML-RPC'){ + if(!(params instanceof Array)) + throw Error("Unable to pass associative arrays to XML-RPC services."); + + var xml = ['' + methodName + '']; + if(params){ + xml.push(''); + for(var i = 0; i < params.length; i++) + xml.push('' + this.__toXMLRPC(params[i]) + ''); + xml.push(''); + } + xml.push(''); + postData = xml.join(''); + + //request = new Document(); + //var methodCallEl = document.createElement('methodCall'); + //var methodNameEl = document.createElement('methodName'); + //methodNameEl.appendChild(document.createTextNode(methodName)); + //methodCallEl.appendChild(methodNameEl); + //if(params){ + // var paramsEl = document.createElement('params'); + // for(var i = 0; i < params.length; i++){ + // var paramEl = document.createElement('param'); + // paramEl.appendChild(this.__toXMLRPC(params[i])); + // paramsEl.appendChild(paramEl); + // } + // methodCallEl.appendChild(paramsEl); + //} + //request.appendChild(methodCallEl); + //postData = request.serializeXML(); + } + //Prepare the JSON-RPC request + else { + request = { + version:"1.1", + method:methodName, + id:rpc.requestCount + }; + if(params) + request.params = params; + postData = this.__toJSON(request); + } + + //XMLHttpRequest chosen (over Ajax.Request) because it propogates uncaught exceptions + var xhr; + if(window.XMLHttpRequest) + xhr = new XMLHttpRequest(); + else if(window.ActiveXObject){ + try { + xhr = new ActiveXObject('Msxml2.XMLHTTP'); + } catch(err){ + xhr = new ActiveXObject('Microsoft.XMLHTTP'); + } + } + xhr.open('POST', this.__serviceURL, this.__isAsynchronous, this.__authUsername, this.__authPassword); + if(this.__protocol == 'XML-RPC'){ + xhr.setRequestHeader('Content-Type', 'text/xml'); + xhr.setRequestHeader('Accept', 'text/xml'); + } + else { + xhr.setRequestHeader('Content-Type', 'application/json'); + xhr.setRequestHeader('Accept', 'application/json'); + } + + //Asynchronous same-domain call ----------------------------------------------------- + if(this.__isAsynchronous){ + //Send the request + xhr.send(postData); + + //Handle the response + var instance = this; + var requestInfo = {id:rpc.requestCount}; //for XML-RPC since the 'request' object cannot contain request ID + xhr.onreadystatechange = function(){ + //QUESTION: Why can't I use this.readyState? + if(xhr.readyState == 4){ + //XML-RPC + if(instance.__protocol == 'XML-RPC'){ + var response = instance.__getXMLRPCResponse(xhr, requestInfo.id); + instance.__doCallback(response); + } + //JSON-RPC + else { + var response = instance.__evalJSON(xhr.responseText, instance.__isResponseSanitized); + if(!response.id) + response.id = requestInfo.id; + instance.__doCallback(response); + } + } + }; + + return undefined; + } + //Synchronous same-domain call ----------------------------------------------------- + else { + //Send the request + xhr.send(postData); + var response; + if(this.__protocol == 'XML-RPC') + response = this.__getXMLRPCResponse(xhr, rpc.requestCount); + else + response = this.__evalJSON(xhr.responseText, this.__isResponseSanitized); + + //Note that this error must be caught with a try/catch block instead of by passing a onException callback + if(response.error) + throw Error('Unable to call "' + methodName + '". Server responsed with error (code ' + response.error.code + '): ' + response.error.message); + + this.__upgradeValuesFromJSON(response); + return response.result; + } + } + } + catch(err){ + //err.locationCode = PRE-REQUEST Cleint + var isCaught = false; + if(exceptionHandler) + isCaught = exceptionHandler(err); //add error location + if(completeHandler) + completeHandler(); + + if(!isCaught) + throw err; + } +}; + +//This acts as a lookup table for the response callback to execute the user-defined +// callbacks and to clean up after a request +rpc.pendingRequests = {}; + +//Ad hoc cross-site callback functions keyed by request ID; when a cross-site request +// is made, a function is created +rpc.callbacks = {}; + +//Called by asychronous calls when their responses have loaded +rpc.ServiceProxy.prototype.__doCallback = function(response){ + if(typeof response != 'object') + throw Error('The server did not respond with a response object.'); + if(!response.id) + throw Error('The server did not respond with the required response id for asynchronous calls.'); + + if(!rpc.pendingRequests[response.id]) + throw Error('Fatal error with RPC code: no ID "' + response.id + '" found in pendingRequests.'); + + //Remove the SCRIPT element from the DOM tree for cross-site (JSON-in-Script) requests + if(rpc.pendingRequests[response.id].scriptElement){ + var script = rpc.pendingRequests[response.id].scriptElement; + script.parentNode.removeChild(script); + } + //Remove the ad hoc cross-site callback function + if(rpc.callbacks[response.id]) + delete rpc.callbacks['r' + response.id]; + + var uncaughtExceptions = []; + + //Handle errors returned by the server + if(response.error !== undefined){ + var err = new Error(response.error.message); + err.code = response.error.code; + //err.locationCode = SERVER + if(rpc.pendingRequests[response.id].onException){ + try{ + if(!rpc.pendingRequests[response.id].onException(err)) + uncaughtExceptions.push(err); + } + catch(err2){ //If the onException handler also fails + uncaughtExceptions.push(err); + uncaughtExceptions.push(err2); + } + } + else uncaughtExceptions.push(err); + } + + //Process the valid result + else if(response.result !== undefined){ + //iterate over all values and substitute date strings with Date objects + //Note that response.result is not passed because the values contained + // need to be modified by reference, and the only way to do so is + // but accessing an object's properties. Thus an extra level of + // abstraction allows for accessing all of the results members by reference. + this.__upgradeValuesFromJSON(response); + + if(rpc.pendingRequests[response.id].onSuccess){ + try { + rpc.pendingRequests[response.id].onSuccess(response.result); + } + //If the onSuccess callback itself fails, then call the onException handler as above + catch(err){ + //err3.locationCode = CLIENT; + if(rpc.pendingRequests[response.id].onException){ + try { + if(!rpc.pendingRequests[response.id].onException(err)) + uncaughtExceptions.push(err); + } + catch(err2){ //If the onException handler also fails + uncaughtExceptions.push(err); + uncaughtExceptions.push(err2); + } + } + else uncaughtExceptions.push(err); + } + } + } + + //Call the onComplete handler + try { + if(rpc.pendingRequests[response.id].onComplete) + rpc.pendingRequests[response.id].onComplete(response); + } + catch(err){ //If the onComplete handler fails + //err3.locationCode = CLIENT; + if(rpc.pendingRequests[response.id].onException){ + try { + if(!rpc.pendingRequests[response.id].onException(err)) + uncaughtExceptions.push(err); + } + catch(err2){ //If the onException handler also fails + uncaughtExceptions.push(err); + uncaughtExceptions.push(err2); + } + } + else uncaughtExceptions.push(err); + } + + delete rpc.pendingRequests[response.id]; + + //Merge any exception raised by onComplete into the previous one(s) and throw it + if(uncaughtExceptions.length){ + var code; + var message = 'There ' + (uncaughtExceptions.length == 1 ? + 'was 1 uncaught exception' : + 'were ' + uncaughtExceptions.length + ' uncaught exceptions') + ': '; + for(var i = 0; i < uncaughtExceptions.length; i++){ + if(i) + message += "; "; + message += uncaughtExceptions[i].message; + if(uncaughtExceptions[i].code) + code = uncaughtExceptions[i].code; + } + var err = new Error(message); + err.code = code; + throw err; + } +}; + + +/******************************************************************************************* + * JSON-RPC Specific Functions + ******************************************************************************************/ +rpc.ServiceProxy.prototype.__toJSON = function(value){ + switch(typeof value){ + case 'number': + return isFinite(value) ? value.toString() : 'null'; + case 'boolean': + return value.toString(); + case 'string': + //Taken from Ext JSON.js + var specialChars = { + "\b": '\\b', + "\t": '\\t', + "\n": '\\n', + "\f": '\\f', + "\r": '\\r', + '"' : '\\"', + "\\": '\\\\', + "/" : '\/' + }; + return '"' + value.replace(/([\x00-\x1f\\"])/g, function(a, b) { + var c = specialChars[b]; + if(c) + return c; + c = b.charCodeAt(); + //return "\\u00" + Math.floor(c / 16).toString(16) + (c % 16).toString(16); + return '\\u00' + rpc.zeroPad(c.toString(16)); + }) + '"'; + case 'object': + if(value === null) + return 'null'; + else if(value instanceof Array){ + var json = ['[']; //Ext's JSON.js reminds me that Array.join is faster than += in MSIE + for(var i = 0; i < value.length; i++){ + if(i) + json.push(','); + json.push(this.__toJSON(value[i])); + } + json.push(']'); + return json.join(''); + } + else if(value instanceof Date){ + switch(this.__dateEncoding){ + case 'classHinting': //{"__jsonclass__":["constructor", [param1,...]], "prop1": ...} + return '{"__jsonclass__":["Date",[' + value.valueOf() + ']]}'; + case '@timestamp@': + case '@ticks@': + return '"@' + value.valueOf() + '@"'; + case 'ASP.NET': + return '"\\/Date(' + value.valueOf() + ')\\/"'; + default: + return '"' + rpc.dateToISO8601(value) + '"'; + } + } + else if(value instanceof Number || value instanceof String || value instanceof Boolean) + return this.__toJSON(value.valueOf()); + else { + var useHasOwn = {}.hasOwnProperty ? true : false; //From Ext's JSON.js + var json = ['{']; + for(var key in value){ + if(!useHasOwn || value.hasOwnProperty(key)){ + if(json.length > 1) + json.push(','); + json.push(this.__toJSON(key) + ':' + this.__toJSON(value[key])); + } + } + json.push('}'); + return json.join(''); + } + //case 'undefined': + //case 'function': + //case 'unknown': + //default: + } + throw new TypeError('Unable to convert the value of type "' + typeof(value) + '" to JSON.'); //(' + String(value) + ') +}; + +rpc.isJSON = function(string){ //from Prototype String.isJSON() + var testStr = string.replace(/\\./g, '@').replace(/"[^"\\\n\r]*"/g, ''); + return (/^[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]*$/).test(testStr); +}; + +rpc.ServiceProxy.prototype.__evalJSON = function(json, sanitize){ //from Prototype String.evalJSON() + //Remove security comment delimiters + json = json.replace(/^\/\*-secure-([\s\S]*)\*\/\s*$/, "$1"); + var err; + try { + if(!sanitize || rpc.isJSON(json)) + return eval('(' + json + ')'); + } + catch(e){err = e;} + throw new SyntaxError('Badly formed JSON string: ' + json + " ... " + (err ? err.message : '')); +}; + +//This function iterates over the properties of the passed object and converts them +// into more appropriate data types, i.e. ISO8601 strings are converted to Date objects. +rpc.ServiceProxy.prototype.__upgradeValuesFromJSON = function(obj){ + var matches, useHasOwn = {}.hasOwnProperty ? true : false; + for(var key in obj){ + if(!useHasOwn || obj.hasOwnProperty(key)){ + //Parse date strings + if(typeof obj[key] == 'string'){ + //ISO8601 + if(this.__decodeISO8601 && (matches = obj[key].match(/^(?:(\d\d\d\d)-(\d\d)(?:-(\d\d)(?:T(\d\d)(?::(\d\d)(?::(\d\d)(?:\.(\d+))?)?)?)?)?)$/))){ + obj[key] = new Date(0); + if(matches[1]) obj[key].setUTCFullYear(parseInt(matches[1])); + if(matches[2]) obj[key].setUTCMonth(parseInt(matches[2]-1)); + if(matches[3]) obj[key].setUTCDate(parseInt(matches[3])); + if(matches[4]) obj[key].setUTCHours(parseInt(matches[4])); + if(matches[5]) obj[key].setUTCMinutes(parseInt(matches[5])); + if(matches[6]) obj[key].setUTCMilliseconds(parseInt(matches[6])); + } + //@timestamp@ / @ticks@ + else if(matches = obj[key].match(/^@(\d+)@$/)){ + obj[key] = new Date(parseInt(matches[1])) + } + //ASP.NET + else if(matches = obj[key].match(/^\/Date\((\d+)\)\/$/)){ + obj[key] = new Date(parseInt(matches[1])) + } + } + else if(obj[key] instanceof Object){ + + //JSON 1.0 Class Hinting: {"__jsonclass__":["constructor", [param1,...]], "prop1": ...} + if(obj[key].__jsonclass__ instanceof Array){ + //console.info('good1'); + if(obj[key].__jsonclass__[0] == 'Date'){ + //console.info('good2'); + if(obj[key].__jsonclass__[1] instanceof Array && obj[key].__jsonclass__[1][0]) + obj[key] = new Date(obj[key].__jsonclass__[1][0]); + else + obj[key] = new Date(); + } + } + else this.__upgradeValuesFromJSON(obj[key]); + } + } + } +}; + + +/******************************************************************************************* + * XML-RPC Specific Functions + ******************************************************************************************/ + +rpc.ServiceProxy.prototype.__toXMLRPC = function(value){ + var xml = ['']; + switch(typeof value){ + case 'number': + if(!isFinite(value)) + xml.push(''); + else if(parseInt(value) == Math.ceil(value)){ + xml.push(''); + xml.push(value.toString()); + xml.push(''); + } + else { + xml.push(''); + xml.push(value.toString()); + xml.push(''); + } + break; + case 'boolean': + xml.push(''); + xml.push(value ? '1' : '0'); + xml.push(''); + break; + case 'string': + xml.push(''); + xml.push(value.replace(/[<>&]/, function(ch){ + + })); //escape for XML! + xml.push(''); + break; + case 'object': + if(value === null) + xml.push(''); + else if(value instanceof Array){ + xml.push(''); + for(var i = 0; i < value.length; i++) + xml.push(this.__toXMLRPC(value[i])); + xml.push(''); + } + else if(value instanceof Date){ + xml.push('' + rpc.dateToISO8601(value) + ''); + } + else if(value instanceof Number || value instanceof String || value instanceof Boolean) + return rpc.dateToISO8601(value.valueOf()); + else { + xml.push(''); + var useHasOwn = {}.hasOwnProperty ? true : false; //From Ext's JSON.js + for(var key in value){ + if(!useHasOwn || value.hasOwnProperty(key)){ + xml.push(''); + xml.push('' + key + ''); //Excape XML! + xml.push(this.__toXMLRPC(value[key])); + xml.push(''); + } + } + xml.push(''); + } + break; + //case 'undefined': + //case 'function': + //case 'unknown': + default: + throw new TypeError('Unable to convert the value of type "' + typeof(value) + '" to XML-RPC.'); //(' + String(value) + ') + } + xml.push(''); + return xml.join(''); +}; + +//rpc.ServiceProxy.prototype.toXMLRPC = function(value){ //documentNode +// var valueEl = document.createElement('value'); +// //var xml = ['']; +// switch(typeof value){ +// case 'number': +// if(!isFinite(value)) +// //xml.push(''); +// valueEl.appendChild(document.createElement('nil')); +// //else if(parseInt(value) == Math.ceil(value)){ +// // var intEl = document.createElement('int'); +// // intEl.appendChild(document.createTextNode(value.toString())); +// // valueEl.appendChild(intEl); +// // //xml.push(''); +// // //xml.push(value.toString()); +// // //xml.push(''); +// //} +// //else { +// // var doubleEl = document.createElement('double'); +// // doubleEl.appendChild(document.createTextNode(value.toString())); +// // valueEl.appendChild(doubleEl); +// // //xml.push(''); +// // //xml.push(value.toString()); +// // //xml.push(''); +// //} +// else { +// var numEl = document.createElement(parseInt(value) == Math.ceil(value) ? 'int' : 'double'); +// numEl.appendChild(document.createTextNode(value.toString())); +// valueEl.appendChild(numEl); +// } +// return valueEl; +// case 'boolean': +// var boolEl = document.createElement('boolean'); +// boolEl.appendChild(document.createTextNode(value ? '1' : '0')); +// valueEl.appendChild(boolEl); +// return valueEl; +// //xml.push(''); +// //xml.push(value ? '1' : '0'); +// //xml.push(''); +// case 'string': +// var stringEl = document.createElement('string'); +// stringEl.appendChild(document.createTextNode(value)); +// valueEl.appendChild(stringEl); +// return valueEl; +// case 'object': +// if(value === null) +// valueEl.appendChild(document.createElement('nil')); +// else if(value instanceof Array){ +// var arrayEl = document.createElement('array'); +// var dataEl = document.createElement('data'); +// for(var i = 0; i < value.length; i++) +// dataEl.appendChild(this.__toXMLRPC(value[i])); +// arrayEl.appendChild(dataEl); +// valueEl.appendChild(arrayEl); +// } +// else if(value instanceof Date){ +// var dateEl = document.createElement('datetime.ISO8601'); +// dateEl.appendChild(document.createTextNode(rpc.dateToISO8601(value))); +// valueEl.appendChild(dateEl); +// } +// else if(value instanceof Number || value instanceof String || value instanceof Boolean) +// return rpc.dateToISO8601(value.valueOf()); +// else { +// var structEl = document.createElement('struct'); +// var useHasOwn = {}.hasOwnProperty ? true : false; //From Ext's JSON.js +// for(var key in value){ +// if(!useHasOwn || value.hasOwnProperty(key)){ +// var memberEl = document.createElement('member'); +// var nameEl = document.createElement('name') +// nameEl.appendChild(document.createTextNode(key)); +// memberEl.appendChild(nameEl); +// memberEl.appendChild(this.__toXMLRPC(value[key])); +// structEl.appendChild(memberEl); +// } +// } +// valueEl.appendChild(structEl); +// } +// return valueEl; +// //case 'undefined': +// //case 'function': +// //case 'unknown': +// //default: +// } +// throw new TypeError('Unable to convert the value of type "' + typeof(value) + '" to XML-RPC.'); //(' + String(value) + ') +//}; + +rpc.ServiceProxy.prototype.__parseXMLRPC = function(valueEl){ + if(valueEl.childNodes.length == 1 && + valueEl.childNodes.item(0).nodeType == 3) + { + return valueEl.childNodes.item(0).nodeValue; + } + for(var i = 0; i < valueEl.childNodes.length; i++){ + if(valueEl.childNodes.item(i).nodeType == 1){ + var typeEL = valueEl.childNodes.item(i); + switch(typeEL.nodeName.toLowerCase()){ + case 'i4': + case 'int': + //An integer is a 32-bit signed number. You can include a plus or minus at the + // beginning of a string of numeric characters. Leading zeros are collapsed. + // Whitespace is not permitted. Just numeric characters preceeded by a plus or minus. + var intVal = parseInt(typeEL.firstChild.nodeValue); + if(isNaN(intVal)) + throw Error("XML-RPC Parse Error: The value provided as an integer '" + typeEL.firstChild.nodeValue + "' is invalid."); + return intVal; + case 'double': + //There is no representation for infinity or negative infinity or "not a number". + // At this time, only decimal point notation is allowed, a plus or a minus, + // followed by any number of numeric characters, followed by a period and any + // number of numeric characters. Whitespace is not allowed. The range of + // allowable values is implementation-dependent, is not specified. + var floatVal = parseFloat(typeEL.firstChild.nodeValue); + if(isNaN(floatVal)) + throw Error("XML-RPC Parse Error: The value provided as a double '" + typeEL.firstChild.nodeValue + "' is invalid."); + return floatVal; + case 'boolean': + if(typeEL.firstChild.nodeValue != '0' && typeEL.firstChild.nodeValue != '1') + throw Error("XML-RPC Parse Error: The value provided as a boolean '" + typeEL.firstChild.nodeValue + "' is invalid."); + return Boolean(parseInt(typeEL.firstChild.nodeValue)); + case 'string': + if(!typeEL.firstChild) + return ""; + return typeEL.firstChild.nodeValue; + case 'datetime.iso8601': + var matches, date = new Date(0); + if(matches = typeEL.firstChild.nodeValue.match(/^(?:(\d\d\d\d)-(\d\d)(?:-(\d\d)(?:T(\d\d)(?::(\d\d)(?::(\d\d)(?:\.(\d+))?)?)?)?)?)$/)){ + if(matches[1]) date.setUTCFullYear(parseInt(matches[1])); + if(matches[2]) date.setUTCMonth(parseInt(matches[2]-1)); + if(matches[3]) date.setUTCDate(parseInt(matches[3])); + if(matches[4]) date.setUTCHours(parseInt(matches[4])); + if(matches[5]) date.setUTCMinutes(parseInt(matches[5])); + if(matches[6]) date.setUTCMilliseconds(parseInt(matches[6])); + return date; + } + throw Error("XML-RPC Parse Error: The provided value does not match ISO8601."); + case 'base64': + throw Error("Not able to parse base64 data yet."); + //return base64_decode(typeEL.firstChild.nodeValue); + case 'nil': + return null; + case 'struct': + //A contains s and each contains a and a . + var obj = {}; + for(var memberEl, j = 0; memberEl = typeEL.childNodes.item(j); j++){ + if(memberEl.nodeType == 1 && memberEl.nodeName == 'member'){ + var name = ''; + valueEl = null; + for(var child, k = 0; child = memberEl.childNodes.item(k); k++){ + if(child.nodeType == 1){ + if(child.nodeName == 'name') + name = child.firstChild.nodeValue; + else if(child.nodeName == 'value') + valueEl = child; + } + } + //s can be recursive, any may contain a or + // any other type, including an , described below. + if(name && valueEl) + obj[name] = this.__parseXMLRPC(valueEl); + } + } + return obj; + case 'array': + //An contains a single element, which can contain any number of s. + var arr = []; + var dataEl = typeEL.firstChild; + while(dataEl && (dataEl.nodeType != 1 || dataEl.nodeName != 'data')) + dataEl = dataEl.nextSibling; + + if(!dataEl) + new Error("XML-RPC Parse Error: Expected 'data' element as sole child element of 'array'."); + + valueEl = dataEl.firstChild; + while(valueEl){ + if(valueEl.nodeType == 1){ + //s can be recursive, any value may contain an or + // any other type, including a , described above. + if(valueEl.nodeName == 'value') + arr.push(this.__parseXMLRPC(valueEl)); + else + throw Error("XML-RPC Parse Error: Illegal element child '" + valueEl.nodeName + "' of an array's 'data' element."); + } + valueEl = valueEl.nextSibling; + } + return arr; + default: + throw Error("XML-RPC Parse Error: Illegal element '" + typeEL.nodeName + "' child of the 'value' element."); + } + } + } + return ''; +} + +rpc.ServiceProxy.prototype.__getXMLRPCResponse = function(xhr, id){ + var response = {}; + if(!xhr.responseXML) + throw Error("Malformed XML document."); + var doc = xhr.responseXML.documentElement; + if(doc.nodeName != 'methodResponse') + throw Error("Invalid XML-RPC document."); + + var valueEl = doc.getElementsByTagName('value')[0]; + if(valueEl.parentNode.nodeName == 'param' && + valueEl.parentNode.parentNode.nodeName == 'params') + { + response.result = this.__parseXMLRPC(valueEl); + } + else if(valueEl.parentNode.nodeName == 'fault'){ + var fault = this.__parseXMLRPC(valueEl); + response.error = { + code: fault.faultCode, + message: fault.faultString + }; + } + else throw Error("Invalid XML-RPC document."); + + if(!response.result && !response.error) + throw Error("Malformed XML-RPC methodResponse document."); + + response.id = id; //XML-RPC cannot pass and return request IDs + return response; +}; + +/******************************************************************************************* + * Other helper functions + ******************************************************************************************/ + +//Takes an array or hash and coverts it into a query string, converting dates to ISO8601 +// and throwing an exception if nested hashes or nested arrays appear. +rpc.toQueryString = function(params){ + if(!(params instanceof Object || params instanceof Array) || params instanceof Date) + throw Error('You must supply either an array or object type to convert into a query string. You supplied: ' + params.constructor); + + var str = ''; + var useHasOwn = {}.hasOwnProperty ? true : false; + + for(var key in params){ + if(useHasOwn && params.hasOwnProperty(key)){ + //Process an array + if(params[key] instanceof Array){ + for(var i = 0; i < params[key].length; i++){ + if(str) + str += '&'; + str += encodeURIComponent(key) + "="; + if(params[key][i] instanceof Date) + str += encodeURIComponent(rpc.dateToISO8601(params[key][i])); + else if(params[key][i] instanceof Object) + throw Error('Unable to pass nested arrays nor objects as parameters while in making a cross-site request. The object in question has this constructor: ' + params[key][i].constructor); + else str += encodeURIComponent(String(params[key][i])); + } + } + else { + if(str) + str += '&'; + str += encodeURIComponent(key) + "="; + if(params[key] instanceof Date) + str += encodeURIComponent(rpc.dateToISO8601(params[key])); + else if(params[key] instanceof Object) + throw Error('Unable to pass objects as parameters while in making a cross-site request. The object in question has this constructor: ' + params[key].constructor); + else str += encodeURIComponent(String(params[key])); + } + } + } + return str; +}; + +//Converts an iterateable value into an array; similar to Prototype's $A function +rpc.toArray = function(value){ + //if(value && value.length){ + if(value instanceof Array) + return value; + var array = []; + for(var i = 0; i < value.length; i++) + array.push(value[i]); + return array; + //} + //throw Error("Unable to convert to an array the value: " + String(value)); +}; + +//Returns an ISO8601 string *in UTC* for the provided date (Prototype's Date.toJSON() returns localtime) +rpc.dateToISO8601 = function(date){ + //var jsonDate = date.toJSON(); + //return jsonDate.substring(1, jsonDate.length-1); //strip double quotes + + return date.getUTCFullYear() + '-' + + rpc.zeroPad(date.getUTCMonth()+1) + '-' + + rpc.zeroPad(date.getUTCDate()) + 'T' + + rpc.zeroPad(date.getUTCHours()) + ':' + + rpc.zeroPad(date.getUTCMinutes()) + ':' + + rpc.zeroPad(date.getUTCSeconds()) + '.' + + //Prototype's Date.toJSON() method does not include milliseconds + rpc.zeroPad(date.getUTCMilliseconds(), 3); +}; + +rpc.zeroPad = function(value, width){ + if(!width) + width = 2; + value = (value == undefined ? '' : String(value)) + while(value.length < width) + value = '0' + value; + return value; +}; \ No newline at end of file diff --git a/deluge/ui/webui/templates/white/part_organize.html b/deluge/ui/webui/templates/white/part_organize.html index 88b1fe8b4..0d993455b 100644 --- a/deluge/ui/webui/templates/white/part_organize.html +++ b/deluge/ui/webui/templates/white/part_organize.html @@ -20,6 +20,9 @@ $if get('keyword'): $for state, num in filters.state: $if (num > 0) or (state == get('state')):