diff --git a/plugins/WebUi/LICENSE b/plugins/WebUi/LICENSE new file mode 100644 index 000000000..4856598ea --- /dev/null +++ b/plugins/WebUi/LICENSE @@ -0,0 +1,350 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + 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 + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General +Public License instead of this License. + + In addition, as a special exception, the copyright holders give + permission to link the code of portions of this program with the OpenSSL + library. + You must obey the GNU General Public License in all respects for all of + the code used other than OpenSSL. If you modify file(s) with this + exception, you may extend this exception to your version of the file(s), + but you are not obligated to do so. If you do not wish to do so, delete + this exception statement from your version. If you delete this exception + statement from all source files in the program, then also delete it here. diff --git a/plugins/WebUi/TODO b/plugins/WebUi/TODO new file mode 100644 index 000000000..e0944ed43 --- /dev/null +++ b/plugins/WebUi/TODO @@ -0,0 +1,20 @@ +0.5.7 +SSL +torrent/add http-post for private sites +rename reannounce->update-tracker. +queued displays as seeding/downloading + + +0.5.7 advanced layout +fonts +fix auto-refresh-layout +buttons +hide 0.0 kbps like in gtk-ui +update-tracker. + +0.6 +prepare for cat: + filters on status (prepare for cat) + filters on tracker +categories +greasemonkey : private sites. \ No newline at end of file diff --git a/plugins/WebUi/__init__.py b/plugins/WebUi/__init__.py new file mode 100644 index 000000000..69b1f999d --- /dev/null +++ b/plugins/WebUi/__init__.py @@ -0,0 +1,281 @@ +# -*- coding: utf-8 -*- +# +# +# Copyright (C) Martijn Voncken 2007 +# +# 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 +# the Free Software Foundation; either version 2, or (at your option) +# any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, write to: +# The Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor +# Boston, MA 02110-1301, USA. +# +# In addition, as a special exception, the copyright holders give +# permission to link the code of portions of this program with the OpenSSL +# library. +# You must obey the GNU General Public License in all respects for all of +# the code used other than OpenSSL. If you modify file(s) with this +# exception, you may extend this exception to your version of the file(s), +# but you are not obligated to do so. If you do not wish to do so, delete +# this exception statement from your version. If you delete this exception + +plugin_name = "Web User Interface" +plugin_author = "Martijn Voncken" +plugin_version = "rev." +plugin_description = """A Web based User Interface + +Firefox greasemonkey script: http://userscripts.org/scripts/show/12639 + +Remotely add a file: "curl -F torrent=@./test1.torrent -F pwd=deluge http://localhost:8112/remote/torrent/add" + +Advanced template is only tested on firefox and garanteed not to work in IE6 + +ssl keys are located in WebUi/ssl/ + +Other contributors: +*somedude : template enhancements. +*markybob : stability : synced with his changes in deluge-svn. +""" + +import deluge.common +try: + import deluge.pref + from deluge.dialogs import show_popup_warning + import webserver_common +except ImportError: + print 'WebUi:not imported as a plugin' + + + +try: + from dbus_interface import get_dbus_manager +except: + print 'error importing dbus-interface!' + pass #for unit-test. + +import time + +import gtk +import os +from subprocess import Popen +from md5 import md5 +from threading import Thread +import random +random.seed() + +plugin_version += open(os.path.join(os.path.dirname(__file__),'revno')).read() +plugin_description += ( + open(os.path.join(os.path.dirname(__file__),'version')).read()) + +def deluge_init(deluge_path): + global path + path = deluge_path + +def enable(core, interface): + global path + return plugin_WebUi(path, core, interface) + +class plugin_WebUi(object): + def __init__(self, path, deluge_core, deluge_interface): + self.path = path + self.core = deluge_core + self.interface = deluge_interface + self.proc = None + self.web_server = None + if not deluge.common.windows_check(): + import commands + status = commands.getstatusoutput( + 'ps x |grep -v grep |grep run_webserver') + if status[0] == 0: + os.kill(int(status[1].split()[0]), 9) + time.sleep(1) #safe time to wait for kill to finish. + self.config_file = deluge.common.CONFIG_DIR + "/webui.conf" + self.config = deluge.pref.Preferences(self.config_file, False) + try: + self.config.load() + except IOError: + # File does not exist + pass + + if not self.config.get('port'): #ugly way to detect new config file. + #set default values: + self.config.set("port", 8112) + self.config.set("button_style", 2) + self.config.set("auto_refresh", False) + self.config.set("auto_refresh_secs", 4) + self.config.set("template", "deluge") + self.config.save(self.config_file) + + if not self.config.get("pwd_salt"): + self.config.set("pwd_salt", "invalid") + self.config.set("pwd_md5", "invalid") + + if self.config.get("cache_templates") == None: + self.config.set("cache_templates", True) + + if deluge.common.windows_check(): + self.config.set("run_in_thread", True) + else: + self.config.set("run_in_thread", False) + + if self.config.get("use_https") == None: + self.config.set("use_https", False) + + self.dbus_manager = get_dbus_manager(deluge_core, deluge_interface, + self.config, self.config_file) + + self.start_server() + + def unload(self): + print 'WebUI:unload..' + self.kill_server() + + def update(self): + pass + + ## This will be only called if your plugin is configurable + def configure(self,parent_dialog): + d = ConfigDialog(self.config, self, parent_dialog) + if d.run() == gtk.RESPONSE_OK: + d.save_config() + d.destroy() + + def start_server(self): + self.kill_server() + + if self.config.get("run_in_thread"): + print 'Start Webui(inside gtk)..' + webserver_common.init_gtk_05() #reload changed config. + from deluge_webserver import WebServer #only import in threaded mode + + + self.web_server = WebServer() + self.web_server.start_gtk() + + else: + print 'Start Webui(in process)..' + server_bin = os.path.dirname(__file__) + '/run_webserver' + self.proc = Popen((server_bin,'env=0.5')) + + def kill_server(self): + if self.web_server: + print "webserver: stop" + self.web_server.stop_gtk() + self.web_server = None + if self.proc: + print "webserver: kill %i" % self.proc.pid + os.system("kill -9 %i" % self.proc.pid) + time.sleep(1) #safe time to wait for kill to finish. + self.proc = None + + def __del__(self): + self.kill_server() + +class ConfigDialog(gtk.Dialog): + """ + sorry, can't get used to gui builders. + from what I read glade is better, but i dont want to invest time in them. + """ + def __init__(self, config, plugin, parent): + gtk.Dialog.__init__(self ,parent=parent) + self.config = config + self.plugin = plugin + self.vb = gtk.VBox() + self.set_title(_("WebUi Config")) + + template_path = os.path.join(os.path.dirname(__file__), 'templates') + self.templates = [dirname for dirname + in os.listdir(template_path) + if os.path.isdir(os.path.join(template_path, dirname)) + and not dirname.startswith('.')] + + self.port = self.add_widget(_('Port Number'), gtk.SpinButton()) + self.pwd1 = self.add_widget(_('New Password'), gtk.Entry()) + self.pwd2 = self.add_widget(_('New Password(confirm)'), gtk.Entry()) + self.template = self.add_widget(_('Template'), gtk.combo_box_new_text()) + self.button_style = self.add_widget(_('Button Style'), + gtk.combo_box_new_text()) + self.cache_templates = self.add_widget(_('Cache Templates'), + gtk.CheckButton()) + self.use_https = self.add_widget(_('https://'), + gtk.CheckButton()) + + #self.share_downloads = self.add_widget(_('Share Download Directory'), + # gtk.CheckButton()) + + self.port.set_range(80, 65536) + self.port.set_increments(1, 10) + self.pwd1.set_visibility(False) + self.pwd2.set_visibility(False) + + for item in self.templates: + self.template.append_text(item) + + if not self.config.get("template") in self.templates: + self.config.set("template","deluge") + + for item in [_('Text and image'), _('Image Only'), _('Text Only')]: + self.button_style.append_text(item) + if self.config.get("button_style") == None: + self.config.set("button_style", 2) + + self.port.set_value(int(self.config.get("port"))) + self.template.set_active( + self.templates.index(self.config.get("template"))) + self.button_style.set_active(self.config.get("button_style")) + #self.share_downloads.set_active( + # bool(self.config.get("share_downloads"))) + + self.cache_templates.set_active(self.config.get("cache_templates")) + self.use_https.set_active(self.config.get("use_https")) + + self.vbox.pack_start(self.vb, True, True, 0) + self.vb.show_all() + + self.add_buttons(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL + ,gtk.STOCK_OK, gtk.RESPONSE_OK) + + def add_widget(self,label,w=None): + hb = gtk.HBox() + lbl = gtk.Label(label) + lbl.set_size_request(200,20) + hb.pack_start(lbl,False,False, 0) + hb.pack_start(w,True,True, 0) + + self.vb.pack_start(hb,False,False, 0) + return w + self.add_buttons(dgtk.STOCK_CLOSE, dgtk.RESPONSE_CLOSE) + + def save_config(self): + if self.pwd1.get_text() > '': + if self.pwd1.get_text() <> self.pwd2.get_text(): + show_popup_warning(self,_("Confirmed Password <> New Password\n" + + "Password was not changed")) + else: + sm = md5() + sm.update(str(random.getrandbits(5000))) + salt = sm.digest() + self.config.set("pwd_salt", salt) + # + m = md5() + m.update(salt) + m.update(unicode(self.pwd1.get_text())) + self.config.set("pwd_md5", m.digest()) + + self.config.set("port", int(self.port.get_value())) + self.config.set("template", self.template.get_active_text()) + self.config.set("button_style", self.button_style.get_active()) + self.config.set("cache_templates", self.cache_templates.get_active()) + self.config.set("use_https", self.use_https.get_active()) + #self.config.set("share_downloads", self.share_downloads.get_active()) + self.config.save(self.plugin.config_file) + self.plugin.start_server() #restarts server diff --git a/plugins/WebUi/dbus_interface.py b/plugins/WebUi/dbus_interface.py new file mode 100644 index 000000000..4743b77a0 --- /dev/null +++ b/plugins/WebUi/dbus_interface.py @@ -0,0 +1,283 @@ +# -*- coding: utf-8 -*- +# Dbus Ipc for experimental web interface +# +# dbus_interface.py +# +# Copyright (C) Martijn Voncken 2007 +# Contains copy and pasted code from other parts of deluge,see deluge AUTHORS +# +# 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 +# the Free Software Foundation; either version 2, or (at your option) +# any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, write to: +# The Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor +# Boston, MA 02110-1301, USA. +# +# In addition, as a special exception, the copyright holders give +# permission to link the code of portions of this program with the OpenSSL +# library. +# You must obey the GNU General Public License in all respects for all of +# the code used other than OpenSSL. If you modify file(s) with this +# exception, you may extend this exception to your version of the file(s), +# but you are not obligated to do so. If you do not wish to do so, delete +# this exception statement from your version. If you delete this exception +# statement from all source files in the program, then also delete it here. + +import os +import gtk +import dbus +import deluge.common as common +from lib.pythonize import pythonize +import base64 +import random +random.seed() + +dbus_interface="org.deluge_torrent.dbusplugin" +dbus_service="/org/deluge_torrent/DelugeDbusPlugin" + +dbus_manager = None +def get_dbus_manager(*args): + #another way to make a singleton. + global dbus_manager + if not dbus_manager: + dbus_manager = DbusManager(*args) + return dbus_manager + +class DbusManager(dbus.service.Object): + def __init__(self, core, interface,config,config_file): + self.core = core + self.interface = interface + self.config = config + self.config_file = config_file + self.bus = dbus.SessionBus() + bus_name = dbus.service.BusName(dbus_interface,bus=self.bus) + dbus.service.Object.__init__(self, bus_name,dbus_service) + + # + #todo : add: get_interface_version in=i,get_config_value in=s out=s + # + + @dbus.service.method(dbus_interface=dbus_interface, + in_signature="",out_signature="as") + def get_session_state(self): + """Returns a list of torrent_ids in the session. + same as 0.6, but returns type "as" instead of a pickle + """ + torrent_list = [str(key) for key in self.core.unique_IDs] + return torrent_list + + @dbus.service.method(dbus_interface=dbus_interface, + in_signature="sas",out_signature="a{sv}") + def get_torrent_status(self, torrent_id, keys): + """return torrent metadata of a single torrent as a dict + 0.6 returns a pickle, this returns a dbus-type. + +added some more values to the dict + """ + + torrent_id = int(torrent_id) + # Convert the array of strings to a python list of strings + nkeys = [str(key) for key in keys] + + state = self.core.get_torrent_state(torrent_id) + torrent = self.core.unique_IDs[torrent_id] + + status = { + "name": state["name"], + "total_size": state["total_size"], + "num_pieces": state["num_pieces"], + "state": state['state'], + "user_paused": self.core.is_user_paused(torrent_id), + "paused":state['is_paused'], + "progress": int(state["progress"] * 100), + "next_announce": state["next_announce"], + "total_payload_download":state["total_payload_download"], + "total_payload_upload": state["total_payload_upload"], + "download_payload_rate": state["download_rate"], + "upload_payload_rate": state["upload_rate"], + "num_peers": state["num_peers"], + "num_seeds": state["num_seeds"], + "total_wanted": state["total_wanted"], + "eta": common.estimate_eta(state), + "ratio": self.interface.manager.calc_ratio(torrent_id,state), + #non 0.6 values follow here: + "tracker_status": state.get("tracker_status","?"), + "uploaded_memory": torrent.uploaded_memory, + } + #more non 0.6 values + for key in ["total_seeds", "total_peers","is_seed", "total_done", + "total_download", "total_upload" + #, "download_rate","upload_rate" + , "num_files", "piece_length", "distributed_copies" + ,"next_announce","tracker","queue_pos"]: + status[key] = state[key] + + #print 'all_keys:',sorted(status.keys()) + + status_subset = {} + for key in keys: + if key in status: + status_subset[key] = status[key] + else: + print 'mbus error,no key named:', key + return status_subset + + @dbus.service.method(dbus_interface=dbus_interface, + in_signature="as",out_signature="") + def pause_torrent(self, torrents): + """same as 0.6 interface""" + for torrent_id in torrents: + torrent_id = int(torrent_id) + self.core.set_user_pause(torrent_id, True) + + @dbus.service.method(dbus_interface=dbus_interface, + in_signature="as", out_signature="") + def resume_torrent(self, torrents): + """same as 0.6 interface""" + for torrent_id in torrents: + torrent_id = int(torrent_id) + self.core.set_user_pause(torrent_id, False) + + @dbus.service.method(dbus_interface=dbus_interface, + in_signature="as", out_signature="") + def force_reannounce(self, torrents): + """same as 0.6 interface""" + for torrent_id in torrents: + torrent_id = int(torrent_id) + self.core.update_tracker(torrent_id) + + @dbus.service.method(dbus_interface=dbus_interface, + in_signature="asbb", out_signature="") + def remove_torrent(self, torrent_ids, data_also, torrent_also): + """remove a torrent,and optionally data and torrent + additions compared to 0.6 interface: (data_also, torrent_also) + """ + for torrent_id in torrent_ids: + torrent_id = int(torrent_id) + self.core.remove_torrent(torrent_id, bool(data_also) + ,bool( torrent_also)) + + #this should not be needed: + gtk.gdk.threads_enter() + try: + self.interface.torrent_model_remove(torrent_id) + except: + pass + + @dbus.service.method(dbus_interface=dbus_interface, + in_signature="s", out_signature="b") + def add_torrent_url(self, url): + filename = fetch_url(url) + self._add_torrent(filename) + return True + + @dbus.service.method(dbus_interface=dbus_interface, + in_signature="s", out_signature="b") + def queue_up(self, torrent_id): + self.core.queue_up(int(torrent_id)) + return True + + @dbus.service.method(dbus_interface=dbus_interface, + in_signature="s", out_signature="b") + def queue_down(self, torrent_id): + self.core.queue_down(int(torrent_id)) + return True + + @dbus.service.method(dbus_interface=dbus_interface, + in_signature="ss", out_signature="b") + def add_torrent_filecontent(self, name, filecontent_b64): + """not available in deluge 0.6 interface""" + #name = fillename without directory + name = name.replace('\\','/') + name = 'deluge_' + str(random.random()) + '_' + name.split('/')[-1] + filename = os.path.join(self.core.config.get("default_download_path"), name) + + filecontent = base64.b64decode(filecontent_b64) + f = open(filename,"wb") #no with statement, that's py 2.5+ + f.write(filecontent) + f.close() + print 'write:',filename + self._add_torrent(filename) + return True + + + @dbus.service.method(dbus_interface=dbus_interface, + in_signature="", out_signature="a{sv}") + def get_config(self): + return self.core.config.mapping + + @dbus.service.method(dbus_interface=dbus_interface, + in_signature="s", out_signature="v") + def get_config_value(self,key): + return self.core.config.mapping[pythonize(key)] #ugly! + + @dbus.service.method(dbus_interface=dbus_interface, + in_signature="a{sv}", out_signature="") + def set_config(self, config): + """Set the config with values from dictionary""" + config = deluge.common.pythonize(config) + # Load all the values into the configuration + for key in self.core.config.keys(): + self.core.config[key] = config[key] + self.core.apply_prefs() + + @dbus.service.method(dbus_interface=dbus_interface, + in_signature="", out_signature="v") + def get_download_rate(self): + return self.core.get_state()['download_rate'] + + @dbus.service.method(dbus_interface=dbus_interface, + in_signature="", out_signature="v") + def get_upload_rate(self): + return self.core.get_state()['upload_rate'] + + @dbus.service.method(dbus_interface=dbus_interface, + in_signature="", out_signature="v") + def get_num_connections(self): + core_state = self.core.get_state() + return core_state['num_connections'] + + #internal + def _add_torrent(self, filename): + filename = unicode(filename) + target = self.core.config.get("default_download_path") + + torrent_id = self.core.add_torrent(filename, target, + self.interface.config.get("use_compact_storage")) + + #update gtk-ui This should not be needed!! + gtk.gdk.threads_enter() + try: + self.interface.torrent_model_append(torrent_id) + except: + pass + #finally is 2.5 only! + gtk.gdk.threads_leave() + + return True + +def fetch_url(url): + import urllib + + try: + filename, headers = urllib.urlretrieve(url) + except IOError: + raise Exception( "Network error while trying to fetch torrent from %s" + % url) + else: + if (filename.endswith(".torrent") or + headers["content-type"]=="application/x-bittorrent"): + return filename + else: + raise Exception("URL doesn't appear to be a valid torrent file:%s" + % url) + + return None diff --git a/plugins/WebUi/debugerror.py b/plugins/WebUi/debugerror.py new file mode 100644 index 000000000..259dcac72 --- /dev/null +++ b/plugins/WebUi/debugerror.py @@ -0,0 +1,373 @@ +""" +pretty debug errors +(part of web.py) + +adapted from Django +Copyright (c) 2005, the Lawrence Journal-World +Used under the modified BSD license: +http://www.xfree86.org/3.3.6/COPYRIGHT2.html#5 +""" + +__all__ = ["debugerror", "djangoerror"] + +import sys, urlparse, pprint +from lib.webpy022.net import websafe +from lib.webpy022.template import Template +import lib.webpy022.webapi as web +import webserver_common as ws +from traceback import format_tb + +import os, os.path +whereami = os.path.join(os.getcwd(), __file__) +whereami = os.path.sep.join(whereami.split(os.path.sep)[:-1]) +djangoerror_t = """\ +$def with (exception_type, exception_value, frames, exception_message, version_info, tback_txt) + + + + + + $exception_type at $ctx.path + + + + + +
+

$exception_type : $exception_value

+
+
+

+ + Oops, Deluge Broke :-( , You might have found a bug, or you did something really stupid ;-). +
If the error persists :
+ Read the Faq.
+ Try downloading the latest version at + deluge-torrent.org +
Visit the forum + or the buglist for more info. +

+
+ + +
+Paste the contents of this text-box when you are asked for a traceback:
+ + +
+Use a pastebin on IRC!
+ + +
+ + +
+

Traceback (innermost first)

+
    +$for frame in frames: +
  • + $frame.filename in $frame.function + $if frame.context_line: +
    + $if frame.pre_context: +
      + $for line in frame.pre_context: +
    1. $line
    2. +
    +
    1. $frame.context_line ...
    + $if frame.post_context: +
      + $for line in frame.post_context: +
    1. $line
    2. +
    +
    + + $if frame.vars: +
    + Local vars + $# $inspect.formatargvalues(*inspect.getargvalues(frame['tb'].tb_frame)) +
    + $:dicttable(frame.vars, kls='vars', id=('v' + str(frame.id))) +
  • +
+
+ +
+$if ctx.output or ctx.headers: +

Response so far

+

HEADERS

+

+ $for kv in ctx.headers: + $kv[0]: $kv[1]
+ $else: + [no headers] +

+ +

BODY

+

+ $ctx.output +

+ +

Request information

+ +

INPUT

+$:dicttable(web.input()) + + +$:dicttable(web.cookies()) + +

META

+$ newctx = [] +$# ) and (k not in ['env', 'output', 'headers', 'environ', 'status', 'db_execute']): +$for k, v in ctx.iteritems(): + $if not k.startswith('_') and (k in x): + $newctx.append(kv) +$:dicttable(dict(newctx)) + +

ENVIRONMENT

+$:dicttable(ctx.env) +
+ + + +""" + +dicttable_t = r"""$def with (d, kls='req', id=None) +$if d: + + + $ temp = d.items() + $temp.sort() + $for kv in temp: + + +
VariableValue
$kv[0]
$prettify(kv[1])
+$else: +

No data.

+""" + +dicttable_r = Template(dicttable_t, filter=websafe) +djangoerror_r = Template(djangoerror_t, filter=websafe) + +def djangoerror(): + def _get_lines_from_file(filename, lineno, context_lines): + """ + Returns context_lines before and after lineno from file. + Returns (pre_context_lineno, pre_context, context_line, post_context). + """ + try: + source = open(filename).readlines() + lower_bound = max(0, lineno - context_lines) + upper_bound = lineno + context_lines + + pre_context = \ + [line.strip('\n') for line in source[lower_bound:lineno]] + context_line = source[lineno].strip('\n') + post_context = \ + [line.strip('\n') for line in source[lineno + 1:upper_bound]] + + return lower_bound, pre_context, context_line, post_context + except (OSError, IOError): + return None, [], None, [] + + exception_type, exception_value, tback = sys.exc_info() + + exception_message = 'Error' + try: + exception_message = exception_value.message + except AttributeError: + exception_message = 'no message' + exception_type = exception_type.__name__ + + version_info = ( + "WebUi : rev." + ws.REVNO + + "Python : " + str(sys.version) + ) + try: + import dbus + version_info += '\ndbus:' + str(dbus.__version__) + except: + pass + + tback_txt = ''.join(format_tb(tback)) + + + frames = [] + while tback is not None: + filename = tback.tb_frame.f_code.co_filename + function = tback.tb_frame.f_code.co_name + lineno = tback.tb_lineno - 1 + pre_context_lineno, pre_context, context_line, post_context = \ + _get_lines_from_file(filename, lineno, 7) + frames.append(web.storage({ + 'tback': tback, + 'filename': filename, + 'function': function, + 'lineno': lineno, + 'vars': tback.tb_frame.f_locals, + 'id': id(tback), + 'pre_context': pre_context, + 'context_line': context_line, + 'post_context': post_context, + 'pre_context_lineno': pre_context_lineno, + })) + tback = tback.tb_next + frames.reverse() + urljoin = urlparse.urljoin + def prettify(x): + try: + out = pprint.pformat(x) + except Exception, e: + out = '[could not display: <' + e.__class__.__name__ + \ + ': '+str(e)+'>]' + return out + dt = dicttable_r + dt.globals = {'prettify': prettify} + t = djangoerror_r + t.globals = {'ctx': web.ctx, 'web':web, 'dicttable':dt, 'dict':dict, 'str':str} + return t(exception_type, exception_value, frames, exception_message, version_info, tback_txt) + +def deluge_debugerror(): + """ + A replacement for `internalerror` that presents a nice page with lots + of debug information for the programmer. + + (Based on the beautiful 500 page from [Django](http://djangoproject.com/), + designed by [Wilson Miner](http://wilsonminer.com/).) + """ + web.ctx.headers = [ + ('Content-Type', 'text/html') + ] + web.ctx.output = djangoerror() + +if __name__ == "__main__": + urls = ( + '/', 'index' + ) + + class index: + def GET(self): + thisdoesnotexist + + web.internalerror = web.debugerror + web.run(urls) diff --git a/plugins/WebUi/deluge_webserver.py b/plugins/WebUi/deluge_webserver.py new file mode 100644 index 000000000..e8fd839b9 --- /dev/null +++ b/plugins/WebUi/deluge_webserver.py @@ -0,0 +1,382 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# deluge_webserver.py +# +# Copyright (C) Martijn Voncken 2007 +# +# 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 +# the Free Software Foundation; either version 2, or (at your option) +# any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, write to: +# The Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor +# Boston, MA 02110-1301, USA. +# +# In addition, as a special exception, the copyright holders give +# permission to link the code of portions of this program with the OpenSSL +# library. +# You must obey the GNU General Public License in all respects for all of +# the code used other than OpenSSL. If you modify file(s) with this +# exception, you may extend this exception to your version of the file(s), +# but you are not obligated to do so. If you do not wish to do so, delete +# this exception statement from your version. If you delete this exception +# statement from all source files in the program, then also delete it here. + +import webserver_common as ws +from webserver_framework import * + +import lib.webpy022 as web +from lib.webpy022.http import seeother, url + +import base64 +from operator import attrgetter +import os + +from json_api import json_api + +#routing: +urls = ( + "/login", "login", + "/index", "index", + "/torrent/info/(.*)", "torrent_info", + "/torrent/info_inner/(.*)", "torrent_info_inner", + "/torrent/stop/(.*)", "torrent_stop", + "/torrent/start/(.*)", "torrent_start", + "/torrent/reannounce/(.*)", "torrent_reannounce", + "/torrent/add(.*)", "torrent_add", + "/torrent/delete/(.*)", "torrent_delete", + "/torrent/queue/up/(.*)", "torrent_queue_up", + "/torrent/queue/down/(.*)", "torrent_queue_down", + "/pause_all", "pause_all", + "/resume_all", "resume_all", + "/refresh/set", "refresh_set", + "/refresh/(.*)", "refresh", + "/config", "config_", + "/home", "home", + "/about", "about", + "/logout", "logout", + #remote-api: + "/remote/torrent/add(.*)", "remote_torrent_add", + "/json/(.*)","json_api", + #static: + "/static/(.*)", "static", + "/template/static/(.*)", "template_static", + #"/downloads/(.*)","downloads" disabled until it can handle large downloads + #default-pages + "/", "home", + "", "home" +) +#/routing + +#pages: +class login: + @deluge_page_noauth + def GET(self, name): + vars = web.input(error = None) + return ws.render.login(vars.error) + + def POST(self): + vars = web.input(pwd = None, redir = None) + + if check_pwd(vars.pwd): + #start new session + start_session() + do_redirect() + elif vars.redir: + seeother(url('/login', error=1, redir=vars.redir)) + else: + seeother('/login?error=1') + +class index: + "page containing the torrent list." + @deluge_page + @auto_refreshed + def GET(self, name): + vars = web.input(sort=None, order=None ,filter=None , category=None) + torrent_list = [get_torrent_status(torrent_id) + for torrent_id in ws.proxy.get_session_state()] + all_torrents = torrent_list[:] + + #filter-state + if vars.filter: + torrent_list = filter_torrent_state(torrent_list, vars.filter) + setcookie("filter", vars.filter) + else: + setcookie("filter", "") + + #filter-cat + if vars.category: + torrent_list = [t for t in torrent_list if t.category == vars.category] + setcookie("category", vars.category) + else: + setcookie("category", "") + + #sorting: + if vars.sort: + torrent_list.sort(key=attrgetter(vars.sort)) + if vars.order == 'up': + torrent_list = reversed(torrent_list) + + setcookie("order", vars.order) + setcookie("sort", vars.sort) + + return ws.render.index(torrent_list, all_torrents) + +class torrent_info: + @deluge_page + @auto_refreshed + def GET(self, name): + torrent_id = name.split(',')[0] + return ws.render.torrent_info(get_torrent_status(torrent_id)) + +class torrent_info_inner: + @deluge_page + def GET(self, torrent_ids): + torrent_ids = torrent_ids.split(',') + info = get_torrent_status(torrent_ids[0]) + if len(torrent_ids) > 1: + #todo : hmm, lots of manual stuff here :( + pass + + + return ws.render.torrent_info_inner(info) + +class torrent_start: + @check_session + def POST(self, name): + torrent_ids = name.split(',') + ws.proxy.resume_torrent(torrent_ids) + do_redirect() + +class torrent_stop: + @check_session + def POST(self, name): + torrent_ids = name.split(',') + ws.proxy.pause_torrent(torrent_ids) + do_redirect() + +class torrent_reannounce: + @check_session + def POST(self, torrent_id): + ws.proxy.force_reannounce([torrent_id]) + do_redirect() + +class torrent_add: + @deluge_page + def GET(self, name): + return ws.render.torrent_add() + + @check_session + def POST(self, name): + """ + allows: + *posting of url + *posting file-upload + *posting of data as string(for greasemonkey-private) + """ + + vars = web.input(url = None, torrent = {}) + + torrent_name = None + torrent_data = None + if vars.torrent.filename: + torrent_name = vars.torrent.filename + torrent_data = vars.torrent.file.read() + + if vars.url and torrent_name: + error_page(_("Choose an url or a torrent, not both.")) + if vars.url: + ws.proxy.add_torrent_url(vars.url) + do_redirect() + elif torrent_name: + data_b64 = base64.b64encode(torrent_data) + #b64 because of strange bug-reports related to binary data + ws.proxy.add_torrent_filecontent(vars.torrent.filename, data_b64) + do_redirect() + else: + error_page(_("no data.")) + +class remote_torrent_add: + """ + For use in remote scripts etc. + curl ->POST pwd and torrent as file + greasemonkey: POST pwd torrent_name and data_b64 + """ + @remote + def POST(self, name): + vars = web.input(pwd = None, torrent = {}, + data_b64 = None , torrent_name= None) + + if not check_pwd(vars.pwd): + return 'error:wrong password' + + if vars.data_b64: #b64 post (greasemonkey) + data_b64 = unicode(vars.data_b64) + torrent_name = vars.torrent_name + else: #file-post (curl) + data_b64 = base64.b64encode(vars.torrent.file.read()) + torrent_name = vars.torrent.filename + + ws.proxy.add_torrent_filecontent(torrent_name, data_b64) + return 'ok' + +class torrent_delete: + @deluge_page + def GET(self, name): + torrent_ids = name.split(',') + torrent_list = [get_torrent_status(id) for id in torrent_ids] + return ws.render.torrent_delete(name, torrent_list) + + @check_session + def POST(self, name): + torrent_ids = name.split(',') + vars = web.input(data_also = None, torrent_also = None) + data_also = bool(vars.data_also) + torrent_also = bool(vars.torrent_also) + ws.proxy.remove_torrent(torrent_ids, data_also, torrent_also) + do_redirect() + +class torrent_queue_up: + @check_session + def POST(self, name): + #a bit too verbose.. + torrent_ids = name.split(',') + torrents = [get_torrent_status(id) for id in torrent_ids] + torrents.sort(lambda x, y : x.queue_pos - y.queue_pos) + torrent_ids = [t.id for t in torrents] + for torrent_id in torrent_ids: + ws.proxy.queue_up(torrent_id) + do_redirect() + +class torrent_queue_down: + @check_session + def POST(self, name): + #a bit too verbose.. + torrent_ids = name.split(',') + torrents = [get_torrent_status(id) for id in torrent_ids] + torrents.sort(lambda x, y : x.queue_pos - y.queue_pos) + torrent_ids = [t.id for t in torrents] + for torrent_id in reversed(torrent_ids): + ws.proxy.queue_down(torrent_id) + do_redirect() + +class pause_all: + @check_session + def POST(self, name): + ws.proxy.pause_torrent(ws.proxy.get_session_state()) + do_redirect() + +class resume_all: + @check_session + def POST(self, name): + ws.proxy.resume_torrent(ws.proxy.get_session_state()) + do_redirect() + +class refresh: + @check_session + def POST(self, name): + auto_refresh = {'off': '0', 'on': '1'}[name] + setcookie('auto_refresh', auto_refresh) + if not getcookie('auto_refresh_secs'): + setcookie('auto_refresh_secs', 10) + do_redirect() + +class refresh_set: + @deluge_page + def GET(self, name): + return ws.render.refresh_form() + + @check_session + def POST(self, name): + vars = web.input(refresh = 0) + refresh = int(vars.refresh) + if refresh > 0: + setcookie('auto_refresh', '1') + setcookie('auto_refresh_secs', str(refresh)) + do_redirect() + else: + error_page(_('refresh must be > 0')) + +class config_: #namespace clash? + """core config + TODO:good validation. + """ + """ + SOMEHOW ONLY BREAKS 0.6 ?? + cfg_form = web.form.Form( + web.form.Dropdown('max_download', ws.SPEED_VALUES, + description=_('Download Speed Limit'), + post='%s Kib/sec' % ws.proxy.get_config_value('max_download_speed') + ) + ,web.form.Dropdown('max_upload', ws.SPEED_VALUES, + description=_('Upload Speed Limit'), + post='%s Kib/sec' % ws.proxy.get_config_value('max_upload_speed') + ) + ) + + @deluge_page + def GET(self, name): + return ws.render.config(self.cfg_form()) + + def POST(self, name): + vars = web.input(max_download=None, max_upload=None) + + #self.config.set("max_download_speed", float(str_bwdown)) + raise NotImplementedError('todo') + """ + +class home: + @check_session + def GET(self, name): + do_redirect() + +class about: + @deluge_page_noauth + def GET(self, name): + return ws.render.about() + +class logout: + @check_session + def POST(self, name): + end_session() + seeother('/login') + +class static(static_handler): + base_dir = os.path.join(os.path.dirname(__file__), 'static') + +class template_static(static_handler): + def get_base_dir(self): + return os.path.join(os.path.dirname(__file__), + 'templates/%s/static' % ws.config.get('template')) + +class downloads(static_handler): + def GET(self, name): + self.base_dir = ws.proxy.get_config_value('default_download_path') + if not ws.config.get('share_downloads'): + raise Exception('Access to downloads is forbidden.') + return static_handler.GET(self, name) +#/pages + + +def WebServer(): + return create_webserver(urls, globals()) + + +def run(): + server = WebServer() + try: + server.start() + except KeyboardInterrupt: + server.stop() + +if __name__ == "__main__": + run() diff --git a/plugins/WebUi/json_api.py b/plugins/WebUi/json_api.py new file mode 100644 index 000000000..720aec91b --- /dev/null +++ b/plugins/WebUi/json_api.py @@ -0,0 +1,210 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# webserver_framework.py +# +# Copyright (C) Martijn Voncken 2007 +# +# 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 +# the Free Software Foundation; either version 2, or (at your option) +# any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, write to: +# The Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor +# Boston, MA 02110-1301, USA. +# +# In addition, as a special exception, the copyright holders give +# permission to link the code of portions of this program with the OpenSSL +# library. +# You must obey the GNU General Public License in all respects for all of +# the code used other than OpenSSL. If you modify file(s) with this +# exception, you may extend this exception to your version of the file(s), +# but you are not obligated to do so. If you do not wish to do so, delete +# this exception statement from your version. If you delete this exception +# 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 incluse the python-json dependency. +""" + +from new import instancemethod +from inspect import getargspec +from webserver_framework import remote,ws,get_torrent_status,log,get_category_choosers, get_stats,log,filter_torrent_state,fsize,fspeed +from operator import attrgetter +import lib.webpy022 as web +proxy = ws.proxy + +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/""") + +class json_api: + """ + eperimental json api + generic proxy for all methods onm self. + """ + 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() + + #extra exposed: + get_torrent_status = get_torrent_status + + @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') + + method = getattr(self,name) + vars = web.input(kwargs= None) + log.debug('vars=%s' % vars) + if vars.kwargs: + kwargs = json.read(vars.kwargs) + else: + kwargs = {} + + result = method(**kwargs) + + return "(" + to_json(result) + ")" + + + 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)) + ] + + 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 ws.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] + + return { + 'torrent_list':torrent_list, + 'categories':category_tabs, + 'filters':filter_tabs, + 'stats':get_stats() + } + + + +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') + diff --git a/plugins/WebUi/lib/__init__.py b/plugins/WebUi/lib/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/plugins/WebUi/lib/gtk_cherrypy_wsgiserver.py b/plugins/WebUi/lib/gtk_cherrypy_wsgiserver.py new file mode 100644 index 000000000..ce55b3fa7 --- /dev/null +++ b/plugins/WebUi/lib/gtk_cherrypy_wsgiserver.py @@ -0,0 +1,1077 @@ +""" +mvoncken: +Modified this to integrate into the gtk main-loop. +*split start() into start_common(),start,start_gtk(),start() +*add stop_gtk() +*add CherryPy license in comment +---- +Copyright (c) 2004-2007, CherryPy Team (team@cherrypy.org) +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name of the CherryPy Team nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +A high-speed, production ready, thread pooled, generic WSGI server. + +Simplest example on how to use this module directly +(without using CherryPy's application machinery): + + from cherrypy import wsgiserver + + def my_crazy_app(environ, start_response): + status = '200 OK' + response_headers = [('Content-type','text/plain')] + start_response(status, response_headers) + return ['Hello world!\n'] + + # Here we set our application to the script_name '/' + wsgi_apps = [('/', my_crazy_app)] + + server = wsgiserver.CherryPyWSGIServer(('localhost', 8070), wsgi_apps, + server_name='localhost') + + # Want SSL support? Just set these attributes + # server.ssl_certificate = + # server.ssl_private_key = + + if __name__ == '__main__': + try: + server.start() + except KeyboardInterrupt: + server.stop() + +This won't call the CherryPy engine (application side) at all, only the +WSGI server, which is independant from the rest of CherryPy. Don't +let the name "CherryPyWSGIServer" throw you; the name merely reflects +its origin, not it's coupling. + +The CherryPy WSGI server can serve as many WSGI applications +as you want in one instance: + + wsgi_apps = [('/', my_crazy_app), ('/blog', my_blog_app)] + +""" +import base64 +import Queue +import os +import re +quoted_slash = re.compile("(?i)%2F") +import rfc822 +import socket +try: + import cStringIO as StringIO +except ImportError: + import StringIO +import sys +import threading +import time +import traceback +from urllib import unquote +from urlparse import urlparse +try: + import gobject +except ImportError: + pass + +try: + from OpenSSL import SSL + from OpenSSL import crypto +except ImportError: + SSL = None + +import errno +socket_errors_to_ignore = [] +# Not all of these names will be defined for every platform. +for _ in ("EPIPE", "ETIMEDOUT", "ECONNREFUSED", "ECONNRESET", + "EHOSTDOWN", "EHOSTUNREACH", + "WSAECONNABORTED", "WSAECONNREFUSED", "WSAECONNRESET", + "WSAENETRESET", "WSAETIMEDOUT"): + if _ in dir(errno): + socket_errors_to_ignore.append(getattr(errno, _)) +# de-dupe the list +socket_errors_to_ignore = dict.fromkeys(socket_errors_to_ignore).keys() +socket_errors_to_ignore.append("timed out") + +comma_separated_headers = ['ACCEPT', 'ACCEPT-CHARSET', 'ACCEPT-ENCODING', + 'ACCEPT-LANGUAGE', 'ACCEPT-RANGES', 'ALLOW', 'CACHE-CONTROL', + 'CONNECTION', 'CONTENT-ENCODING', 'CONTENT-LANGUAGE', 'EXPECT', + 'IF-MATCH', 'IF-NONE-MATCH', 'PRAGMA', 'PROXY-AUTHENTICATE', 'TE', + 'TRAILER', 'TRANSFER-ENCODING', 'UPGRADE', 'VARY', 'VIA', 'WARNING', + 'WWW-AUTHENTICATE'] + +class HTTPRequest(object): + """An HTTP Request (and response). + + A single HTTP connection may consist of multiple request/response pairs. + + connection: the HTTP Connection object which spawned this request. + rfile: the 'read' fileobject from the connection's socket + ready: when True, the request has been parsed and is ready to begin + generating the response. When False, signals the calling Connection + that the response should not be generated and the connection should + close. + close_connection: signals the calling Connection that the request + should close. This does not imply an error! The client and/or + server may each request that the connection be closed. + chunked_write: if True, output will be encoded with the "chunked" + transfer-coding. This value is set automatically inside + send_headers. + """ + + def __init__(self, connection): + self.connection = connection + self.rfile = self.connection.rfile + self.sendall = self.connection.sendall + self.environ = connection.environ.copy() + + self.ready = False + self.started_response = False + self.status = "" + self.outheaders = [] + self.sent_headers = False + self.close_connection = False + self.chunked_write = False + + def parse_request(self): + """Parse the next HTTP request start-line and message-headers.""" + # HTTP/1.1 connections are persistent by default. If a client + # requests a page, then idles (leaves the connection open), + # then rfile.readline() will raise socket.error("timed out"). + # Note that it does this based on the value given to settimeout(), + # and doesn't need the client to request or acknowledge the close + # (although your TCP stack might suffer for it: cf Apache's history + # with FIN_WAIT_2). + request_line = self.rfile.readline() + if not request_line: + # Force self.ready = False so the connection will close. + self.ready = False + return + + if request_line == "\r\n": + # RFC 2616 sec 4.1: "...if the server is reading the protocol + # stream at the beginning of a message and receives a CRLF + # first, it should ignore the CRLF." + # But only ignore one leading line! else we enable a DoS. + request_line = self.rfile.readline() + if not request_line: + self.ready = False + return + + server = self.connection.server + environ = self.environ + environ["SERVER_SOFTWARE"] = "%s WSGI Server" % server.version + + method, path, req_protocol = request_line.strip().split(" ", 2) + environ["REQUEST_METHOD"] = method + + # path may be an abs_path (including "http://host.domain.tld"); + scheme, location, path, params, qs, frag = urlparse(path) + + if frag: + self.simple_response("400 Bad Request", + "Illegal #fragment in Request-URI.") + return + + if scheme: + environ["wsgi.url_scheme"] = scheme + if params: + path = path + ";" + params + + # Unquote the path+params (e.g. "/this%20path" -> "this path"). + # http://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5.1.2 + # + # But note that "...a URI must be separated into its components + # before the escaped characters within those components can be + # safely decoded." http://www.ietf.org/rfc/rfc2396.txt, sec 2.4.2 + atoms = [unquote(x) for x in quoted_slash.split(path)] + path = "%2F".join(atoms) + + if path == "*": + # This means, of course, that the last wsgi_app (shortest path) + # will always handle a URI of "*". + environ["SCRIPT_NAME"] = "" + environ["PATH_INFO"] = "*" + self.wsgi_app = server.mount_points[-1][1] + else: + for mount_point, wsgi_app in server.mount_points: + # The mount_points list should be sorted by length, descending. + if path.startswith(mount_point + "/") or path == mount_point: + environ["SCRIPT_NAME"] = mount_point + environ["PATH_INFO"] = path[len(mount_point):] + self.wsgi_app = wsgi_app + break + else: + self.simple_response("404 Not Found") + return + + # Note that, like wsgiref and most other WSGI servers, + # we unquote the path but not the query string. + environ["QUERY_STRING"] = qs + + # Compare request and server HTTP protocol versions, in case our + # server does not support the requested protocol. Limit our output + # to min(req, server). We want the following output: + # request server actual written supported response + # protocol protocol response protocol feature set + # a 1.0 1.0 1.0 1.0 + # b 1.0 1.1 1.1 1.0 + # c 1.1 1.0 1.0 1.0 + # d 1.1 1.1 1.1 1.1 + # Notice that, in (b), the response will be "HTTP/1.1" even though + # the client only understands 1.0. RFC 2616 10.5.6 says we should + # only return 505 if the _major_ version is different. + rp = int(req_protocol[5]), int(req_protocol[7]) + sp = int(server.protocol[5]), int(server.protocol[7]) + if sp[0] != rp[0]: + self.simple_response("505 HTTP Version Not Supported") + return + # Bah. "SERVER_PROTOCOL" is actually the REQUEST protocol. + environ["SERVER_PROTOCOL"] = req_protocol + # set a non-standard environ entry so the WSGI app can know what + # the *real* server protocol is (and what features to support). + # See http://www.faqs.org/rfcs/rfc2145.html. + environ["ACTUAL_SERVER_PROTOCOL"] = server.protocol + self.response_protocol = "HTTP/%s.%s" % min(rp, sp) + + # If the Request-URI was an absoluteURI, use its location atom. + if location: + environ["SERVER_NAME"] = location + + # then all the http headers + try: + self.read_headers() + except ValueError, ex: + self.simple_response("400 Bad Request", repr(ex.args)) + return + + creds = environ.get("HTTP_AUTHORIZATION", "").split(" ", 1) + environ["AUTH_TYPE"] = creds[0] + if creds[0].lower() == 'basic': + user, pw = base64.decodestring(creds[1]).split(":", 1) + environ["REMOTE_USER"] = user + + # Persistent connection support + if self.response_protocol == "HTTP/1.1": + if environ.get("HTTP_CONNECTION", "") == "close": + self.close_connection = True + else: + # HTTP/1.0 + if environ.get("HTTP_CONNECTION", "") != "Keep-Alive": + self.close_connection = True + + # Transfer-Encoding support + te = None + if self.response_protocol == "HTTP/1.1": + te = environ.get("HTTP_TRANSFER_ENCODING") + if te: + te = [x.strip().lower() for x in te.split(",") if x.strip()] + + read_chunked = False + + if te: + for enc in te: + if enc == "chunked": + read_chunked = True + else: + # Note that, even if we see "chunked", we must reject + # if there is an extension we don't recognize. + self.simple_response("501 Unimplemented") + self.close_connection = True + return + + if read_chunked: + if not self.decode_chunked(): + return + + # From PEP 333: + # "Servers and gateways that implement HTTP 1.1 must provide + # transparent support for HTTP 1.1's "expect/continue" mechanism. + # This may be done in any of several ways: + # 1. Respond to requests containing an Expect: 100-continue request + # with an immediate "100 Continue" response, and proceed normally. + # 2. Proceed with the request normally, but provide the application + # with a wsgi.input stream that will send the "100 Continue" + # response if/when the application first attempts to read from + # the input stream. The read request must then remain blocked + # until the client responds. + # 3. Wait until the client decides that the server does not support + # expect/continue, and sends the request body on its own. + # (This is suboptimal, and is not recommended.) + # + # We used to do 3, but are now doing 1. Maybe we'll do 2 someday, + # but it seems like it would be a big slowdown for such a rare case. + if environ.get("HTTP_EXPECT", "") == "100-continue": + self.simple_response(100) + + self.ready = True + + def read_headers(self): + """Read header lines from the incoming stream.""" + environ = self.environ + + while True: + line = self.rfile.readline() + if not line: + # No more data--illegal end of headers + raise ValueError("Illegal end of headers.") + + if line == '\r\n': + # Normal end of headers + break + + if line[0] in ' \t': + # It's a continuation line. + v = line.strip() + else: + k, v = line.split(":", 1) + k, v = k.strip().upper(), v.strip() + envname = "HTTP_" + k.replace("-", "_") + + if k in comma_separated_headers: + existing = environ.get(envname) + if existing: + v = ", ".join((existing, v)) + environ[envname] = v + + ct = environ.pop("HTTP_CONTENT_TYPE", None) + if ct: + environ["CONTENT_TYPE"] = ct + cl = environ.pop("HTTP_CONTENT_LENGTH", None) + if cl: + environ["CONTENT_LENGTH"] = cl + + def decode_chunked(self): + """Decode the 'chunked' transfer coding.""" + cl = 0 + data = StringIO.StringIO() + while True: + line = self.rfile.readline().strip().split(";", 1) + chunk_size = int(line.pop(0), 16) + if chunk_size <= 0: + break +## if line: chunk_extension = line[0] + cl += chunk_size + data.write(self.rfile.read(chunk_size)) + crlf = self.rfile.read(2) + if crlf != "\r\n": + self.simple_response("400 Bad Request", + "Bad chunked transfer coding " + "(expected '\\r\\n', got %r)" % crlf) + return + + # Grab any trailer headers + self.read_headers() + + data.seek(0) + self.environ["wsgi.input"] = data + self.environ["CONTENT_LENGTH"] = str(cl) or "" + return True + + def respond(self): + """Call the appropriate WSGI app and write its iterable output.""" + response = self.wsgi_app(self.environ, self.start_response) + try: + for chunk in response: + # "The start_response callable must not actually transmit + # the response headers. Instead, it must store them for the + # server or gateway to transmit only after the first + # iteration of the application return value that yields + # a NON-EMPTY string, or upon the application's first + # invocation of the write() callable." (PEP 333) + if chunk: + self.write(chunk) + finally: + if hasattr(response, "close"): + response.close() + if (self.ready and not self.sent_headers + and not self.connection.server.interrupt): + self.sent_headers = True + self.send_headers() + if self.chunked_write: + self.sendall("0\r\n\r\n") + + def simple_response(self, status, msg=""): + """Write a simple response back to the client.""" + status = str(status) + buf = ["%s %s\r\n" % (self.connection.server.protocol, status), + "Content-Length: %s\r\n" % len(msg)] + + if status[:3] == "413" and self.response_protocol == 'HTTP/1.1': + # Request Entity Too Large + self.close_connection = True + buf.append("Connection: close\r\n") + + buf.append("\r\n") + if msg: + buf.append(msg) + self.sendall("".join(buf)) + + def start_response(self, status, headers, exc_info = None): + """WSGI callable to begin the HTTP response.""" + if self.started_response: + if not exc_info: + raise AssertionError("WSGI start_response called a second " + "time with no exc_info.") + else: + try: + raise exc_info[0], exc_info[1], exc_info[2] + finally: + exc_info = None + self.started_response = True + self.status = status + self.outheaders.extend(headers) + return self.write + + def write(self, chunk): + """WSGI callable to write unbuffered data to the client. + + This method is also used internally by start_response (to write + data from the iterable returned by the WSGI application). + """ + if not self.started_response: + raise AssertionError("WSGI write called before start_response.") + + if not self.sent_headers: + self.sent_headers = True + self.send_headers() + + if self.chunked_write and chunk: + buf = [hex(len(chunk))[2:], "\r\n", chunk, "\r\n"] + self.sendall("".join(buf)) + else: + self.sendall(chunk) + + def send_headers(self): + """Assert, process, and send the HTTP response message-headers.""" + hkeys = [key.lower() for key, value in self.outheaders] + status = int(self.status[:3]) + + if status == 413: + # Request Entity Too Large. Close conn to avoid garbage. + self.close_connection = True + elif "content-length" not in hkeys: + # "All 1xx (informational), 204 (no content), + # and 304 (not modified) responses MUST NOT + # include a message-body." So no point chunking. + if status < 200 or status in (204, 205, 304): + pass + else: + if self.response_protocol == 'HTTP/1.1': + # Use the chunked transfer-coding + self.chunked_write = True + self.outheaders.append(("Transfer-Encoding", "chunked")) + else: + # Closing the conn is the only way to determine len. + self.close_connection = True + + if "connection" not in hkeys: + if self.response_protocol == 'HTTP/1.1': + if self.close_connection: + self.outheaders.append(("Connection", "close")) + else: + if not self.close_connection: + self.outheaders.append(("Connection", "Keep-Alive")) + + if "date" not in hkeys: + self.outheaders.append(("Date", rfc822.formatdate())) + + server = self.connection.server + + if "server" not in hkeys: + self.outheaders.append(("Server", server.version)) + + buf = [server.protocol, " ", self.status, "\r\n"] + try: + buf += [k + ": " + v + "\r\n" for k, v in self.outheaders] + except TypeError: + if not isinstance(k, str): + raise TypeError("WSGI response header key %r is not a string.") + if not isinstance(v, str): + raise TypeError("WSGI response header value %r is not a string.") + else: + raise + buf.append("\r\n") + self.sendall("".join(buf)) + + +class NoSSLError(Exception): + """Exception raised when a client speaks HTTP to an HTTPS socket.""" + pass + + +def _ssl_wrap_method(method, is_reader=False): + """Wrap the given method with SSL error-trapping. + + is_reader: if False (the default), EOF errors will be raised. + If True, EOF errors will return "" (to emulate normal sockets). + """ + def ssl_method_wrapper(self, *args, **kwargs): +## print (id(self), method, args, kwargs) + start = time.time() + while True: + try: + return method(self, *args, **kwargs) + except (SSL.WantReadError, SSL.WantWriteError): + # Sleep and try again. This is dangerous, because it means + # the rest of the stack has no way of differentiating + # between a "new handshake" error and "client dropped". + # Note this isn't an endless loop: there's a timeout below. + time.sleep(self.ssl_retry) + except SSL.SysCallError, e: + if is_reader and e.args == (-1, 'Unexpected EOF'): + return "" + + errno = e.args[0] + if is_reader and errno in socket_errors_to_ignore: + return "" + raise socket.error(errno) + except SSL.Error, e: + if is_reader and e.args == (-1, 'Unexpected EOF'): + return "" + + thirdarg = None + try: + thirdarg = e.args[0][0][2] + except IndexError: + pass + + if is_reader and thirdarg == 'ssl handshake failure': + return "" + if thirdarg == 'http request': + # The client is talking HTTP to an HTTPS server. + raise NoSSLError() + raise + if time.time() - start > self.ssl_timeout: + raise socket.timeout("timed out") + return ssl_method_wrapper + +class SSL_fileobject(socket._fileobject): + """Faux file object attached to a socket object.""" + + ssl_timeout = 3 + ssl_retry = .01 + + close = _ssl_wrap_method(socket._fileobject.close) + flush = _ssl_wrap_method(socket._fileobject.flush) + write = _ssl_wrap_method(socket._fileobject.write) + writelines = _ssl_wrap_method(socket._fileobject.writelines) + read = _ssl_wrap_method(socket._fileobject.read, is_reader=True) + readline = _ssl_wrap_method(socket._fileobject.readline, is_reader=True) + readlines = _ssl_wrap_method(socket._fileobject.readlines, is_reader=True) + + +class HTTPConnection(object): + """An HTTP connection (active socket). + + socket: the raw socket object (usually TCP) for this connection. + addr: the "bind address" for the remote end of the socket. + For IP sockets, this is a tuple of (REMOTE_ADDR, REMOTE_PORT). + For UNIX domain sockets, this will be a string. + server: the HTTP Server for this Connection. Usually, the server + object possesses a passive (server) socket which spawns multiple, + active (client) sockets, one for each connection. + + environ: a WSGI environ template. This will be copied for each request. + rfile: a fileobject for reading from the socket. + sendall: a function for writing (+ flush) to the socket. + """ + + rbufsize = -1 + RequestHandlerClass = HTTPRequest + environ = {"wsgi.version": (1, 0), + "wsgi.url_scheme": "http", + "wsgi.multithread": True, + "wsgi.multiprocess": False, + "wsgi.run_once": False, + "wsgi.errors": sys.stderr, + } + + def __init__(self, sock, addr, server): + self.socket = sock + self.addr = addr + self.server = server + + # Copy the class environ into self. + self.environ = self.environ.copy() + + if SSL and isinstance(sock, SSL.ConnectionType): + timeout = sock.gettimeout() + self.rfile = SSL_fileobject(sock, "r", self.rbufsize) + self.rfile.ssl_timeout = timeout + self.sendall = _ssl_wrap_method(sock.sendall) + self.environ["wsgi.url_scheme"] = "https" + self.environ["HTTPS"] = "on" + sslenv = getattr(server, "ssl_environ", None) + if sslenv: + self.environ.update(sslenv) + else: + self.rfile = sock.makefile("rb", self.rbufsize) + self.sendall = sock.sendall + + self.environ.update({"wsgi.input": self.rfile, + "SERVER_NAME": self.server.server_name, + }) + + if isinstance(self.server.bind_addr, basestring): + # AF_UNIX. This isn't really allowed by WSGI, which doesn't + # address unix domain sockets. But it's better than nothing. + self.environ["SERVER_PORT"] = "" + else: + self.environ["SERVER_PORT"] = str(self.server.bind_addr[1]) + # optional values + # Until we do DNS lookups, omit REMOTE_HOST + self.environ["REMOTE_ADDR"] = self.addr[0] + self.environ["REMOTE_PORT"] = str(self.addr[1]) + + def communicate(self): + """Read each request and respond appropriately.""" + try: + while True: + # (re)set req to None so that if something goes wrong in + # the RequestHandlerClass constructor, the error doesn't + # get written to the previous request. + req = None + req = self.RequestHandlerClass(self) + # This order of operations should guarantee correct pipelining. + req.parse_request() + if not req.ready: + return + req.respond() + if req.close_connection: + return + except socket.error, e: + errno = e.args[0] + if errno not in socket_errors_to_ignore: + if req: + req.simple_response("500 Internal Server Error", + format_exc()) + return + except (KeyboardInterrupt, SystemExit): + raise + except NoSSLError: + # Unwrap our sendall + req.sendall = self.socket._sock.sendall + req.simple_response("400 Bad Request", + "The client sent a plain HTTP request, but " + "this server only speaks HTTPS on this port.") + except: + if req: + req.simple_response("500 Internal Server Error", format_exc()) + + def close(self): + """Close the socket underlying this connection.""" + self.rfile.close() + self.socket.close() + + +def format_exc(limit=None): + """Like print_exc() but return a string. Backport for Python 2.3.""" + try: + etype, value, tb = sys.exc_info() + return ''.join(traceback.format_exception(etype, value, tb, limit)) + finally: + etype = value = tb = None + + +_SHUTDOWNREQUEST = None + +class WorkerThread(threading.Thread): + """Thread which continuously polls a Queue for Connection objects. + + server: the HTTP Server which spawned this thread, and which owns the + Queue and is placing active connections into it. + ready: a simple flag for the calling server to know when this thread + has begun polling the Queue. + + Due to the timing issues of polling a Queue, a WorkerThread does not + check its own 'ready' flag after it has started. To stop the thread, + it is necessary to stick a _SHUTDOWNREQUEST object onto the Queue + (one for each running WorkerThread). + """ + + def __init__(self, server): + self.ready = False + self.server = server + threading.Thread.__init__(self) + + def run(self): + try: + self.ready = True + while True: + conn = self.server.requests.get() + if conn is _SHUTDOWNREQUEST: + return + + try: + conn.communicate() + finally: + conn.close() + except (KeyboardInterrupt, SystemExit), exc: + self.server.interrupt = exc + + +class SSLConnection: + """A thread-safe wrapper for an SSL.Connection. + + *args: the arguments to create the wrapped SSL.Connection(*args). + """ + + def __init__(self, *args): + self._ssl_conn = SSL.Connection(*args) + self._lock = threading.RLock() + + for f in ('get_context', 'pending', 'send', 'write', 'recv', 'read', + 'renegotiate', 'bind', 'listen', 'connect', 'accept', + 'setblocking', 'fileno', 'shutdown', 'close', 'get_cipher_list', + 'getpeername', 'getsockname', 'getsockopt', 'setsockopt', + 'makefile', 'get_app_data', 'set_app_data', 'state_string', + 'sock_shutdown', 'get_peer_certificate', 'want_read', + 'want_write', 'set_connect_state', 'set_accept_state', + 'connect_ex', 'sendall', 'settimeout'): + exec """def %s(self, *args): + self._lock.acquire() + try: + return self._ssl_conn.%s(*args) + finally: + self._lock.release() +""" % (f, f) + + +class CherryPyWSGIServer(object): + """An HTTP server for WSGI. + + bind_addr: a (host, port) tuple if TCP sockets are desired; + for UNIX sockets, supply the filename as a string. + wsgi_app: the WSGI 'application callable'; multiple WSGI applications + may be passed as (script_name, callable) pairs. + numthreads: the number of worker threads to create (default 10). + server_name: the string to set for WSGI's SERVER_NAME environ entry. + Defaults to socket.gethostname(). + max: the maximum number of queued requests (defaults to -1 = no limit). + request_queue_size: the 'backlog' argument to socket.listen(); + specifies the maximum number of queued connections (default 5). + timeout: the timeout in seconds for accepted connections (default 10). + + protocol: the version string to write in the Status-Line of all + HTTP responses. For example, "HTTP/1.1" (the default). This + also limits the supported features used in the response. + + + SSL/HTTPS + --------- + The OpenSSL module must be importable for SSL functionality. + You can obtain it from http://pyopenssl.sourceforge.net/ + + ssl_certificate: the filename of the server SSL certificate. + ssl_privatekey: the filename of the server's private key file. + + If either of these is None (both are None by default), this server + will not use SSL. If both are given and are valid, they will be read + on server start and used in the SSL context for the listening socket. + """ + + protocol = "HTTP/1.1" + version = "CherryPy/3.0.2" + ready = False + _interrupt = None + ConnectionClass = HTTPConnection + + # Paths to certificate and private key files + ssl_certificate = None + ssl_private_key = None + + def __init__(self, bind_addr, wsgi_app, numthreads=10, server_name=None, + max=-1, request_queue_size=5, timeout=10): + self.requests = Queue.Queue(max) + + if callable(wsgi_app): + # We've been handed a single wsgi_app, in CP-2.1 style. + # Assume it's mounted at "". + self.mount_points = [("", wsgi_app)] + else: + # We've been handed a list of (mount_point, wsgi_app) tuples, + # so that the server can call different wsgi_apps, and also + # correctly set SCRIPT_NAME. + self.mount_points = wsgi_app + self.mount_points.sort() + self.mount_points.reverse() + + self.bind_addr = bind_addr + self.numthreads = numthreads or 1 + if not server_name: + server_name = socket.gethostname() + self.server_name = server_name + self.request_queue_size = request_queue_size + self._workerThreads = [] + self.gtk_idle_id = None + + self.timeout = timeout + + def start_common(self): + """Run the server forever.""" + # We don't have to trap KeyboardInterrupt or SystemExit here, + # because cherrpy.server already does so, calling self.stop() for us. + # If you're using this server with another framework, you should + # trap those exceptions in whatever code block calls start(). + self._interrupt = None + + # Select the appropriate socket + if isinstance(self.bind_addr, basestring): + # AF_UNIX socket + + # So we can reuse the socket... + try: os.unlink(self.bind_addr) + except: pass + + # So everyone can access the socket... + try: os.chmod(self.bind_addr, 0777) + except: pass + + info = [(socket.AF_UNIX, socket.SOCK_STREAM, 0, "", self.bind_addr)] + else: + # AF_INET or AF_INET6 socket + # Get the correct address family for our host (allows IPv6 addresses) + host, port = self.bind_addr + flags = 0 + if host == '': + # Despite the socket module docs, using '' does not + # allow AI_PASSIVE to work. Passing None instead + # returns '0.0.0.0' like we want. In other words: + # host AI_PASSIVE result + # '' Y 192.168.x.y + # '' N 192.168.x.y + # None Y 0.0.0.0 + # None N 127.0.0.1 + host = None + flags = socket.AI_PASSIVE + try: + info = socket.getaddrinfo(host, port, socket.AF_UNSPEC, + socket.SOCK_STREAM, 0, flags) + except socket.gaierror: + # Probably a DNS issue. Assume IPv4. + info = [(socket.AF_INET, socket.SOCK_STREAM, 0, "", self.bind_addr)] + + self.socket = None + msg = "No socket could be created" + for res in info: + af, socktype, proto, canonname, sa = res + try: + self.bind(af, socktype, proto) + except socket.error, msg: + if self.socket: + self.socket.close() + self.socket = None + continue + break + if not self.socket: + raise socket.error, msg + + # Timeout so KeyboardInterrupt can be caught on Win32 + self.socket.settimeout(1) + self.socket.listen(self.request_queue_size) + + # Create worker threads + for i in xrange(self.numthreads): + self._workerThreads.append(WorkerThread(self)) + for worker in self._workerThreads: + worker.setName("CP WSGIServer " + worker.getName()) + worker.start() + for worker in self._workerThreads: + while not worker.ready: + time.sleep(.1) + self.ready = True + + def start(self): + self.start_common() + while self.ready: + self.tick() + if self.interrupt: + while self.interrupt is True: + # Wait for self.stop() to complete. See _set_interrupt. + time.sleep(0.1) + raise self.interrupt + + def start_gtk(self): + self.start_common() + self.socket.settimeout(0.0001) + self.timeout = 0.3 + self.gtk_idle_id = gobject.idle_add(self.tick) + #self.gtk_idle_id = gobject.timeout_add(100, self.tick) #needs tweaking! + + + def bind(self, family, type, proto=0): + """Create (or recreate) the actual socket object.""" + self.socket = socket.socket(family, type, proto) + self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) +## self.socket.setsockopt(socket.SOL_SOCKET, socket.TCP_NODELAY, 1) + if self.ssl_certificate and self.ssl_private_key: + if SSL is None: + raise ImportError("You must install pyOpenSSL to use HTTPS.") + + # See http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/442473 + ctx = SSL.Context(SSL.SSLv23_METHOD) + ctx.use_privatekey_file(self.ssl_private_key) + ctx.use_certificate_file(self.ssl_certificate) + self.socket = SSLConnection(ctx, self.socket) + self.populate_ssl_environ() + self.socket.bind(self.bind_addr) + + def tick(self): + """Accept a new connection and put it on the Queue.""" + #print 'tick!' + try: + s, addr = self.socket.accept() + if not self.ready: + return True + if hasattr(s, 'settimeout'): + s.settimeout(self.timeout) + conn = self.ConnectionClass(s, addr, self) + self.requests.put(conn) + except socket.timeout: + # The only reason for the timeout in start() is so we can + # notice keyboard interrupts on Win32, which don't interrupt + # accept() by default + # mvoncken and i'ts usefull for gtk too. + return True + except socket.error, x: + msg = x.args[1] + if msg in ("Bad file descriptor", "Socket operation on non-socket"): + # Our socket was closed. + return True + if msg == "Resource temporarily unavailable": + # Just try again. See http://www.cherrypy.org/ticket/479. + return True + raise #mvoncken:should it raise here? + return True + + def _get_interrupt(self): + return self._interrupt + def _set_interrupt(self, interrupt): + self._interrupt = True + self.stop() + self._interrupt = interrupt + interrupt = property(_get_interrupt, _set_interrupt, + doc="Set this to an Exception instance to " + "interrupt the server.") + + def stop(self): + """Gracefully shutdown a server that is serving forever.""" + self.ready = False + + sock = getattr(self, "socket", None) + if sock: + if not isinstance(self.bind_addr, basestring): + # Touch our own socket to make accept() return immediately. + try: + host, port = sock.getsockname()[:2] + except socket.error, x: + if x.args[1] != "Bad file descriptor": + raise + else: + # Note that we're explicitly NOT using AI_PASSIVE, + # here, because we want an actual IP to touch. + # localhost won't work if we've bound to a public IP, + # but it would if we bound to INADDR_ANY via host = ''. + for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC, + socket.SOCK_STREAM): + af, socktype, proto, canonname, sa = res + s = None + try: + s = socket.socket(af, socktype, proto) + # See http://groups.google.com/group/cherrypy-users/ + # browse_frm/thread/bbfe5eb39c904fe0 + s.settimeout(1.0) + s.connect((host, port)) + s.close() + except socket.error: + if s: + s.close() + if hasattr(sock, "close"): + sock.close() + self.socket = None + + # Must shut down threads here so the code that calls + # this method can know when all threads are stopped. + for worker in self._workerThreads: + self.requests.put(_SHUTDOWNREQUEST) + + # Don't join currentThread (when stop is called inside a request). + current = threading.currentThread() + while self._workerThreads: + worker = self._workerThreads.pop() + if worker is not current and worker.isAlive: + try: + worker.join() + except AssertionError: + pass + + def stop_gtk(self): + self.stop() + if self.gtk_idle_id == None: + raise Exception('gtk_idle_id == None in stop_gtk') + gobject.source_remove(self.gtk_idle_id) + self.gtk_idle_id = None + + + def populate_ssl_environ(self): + """Create WSGI environ entries to be merged into each request.""" + cert = open(self.ssl_certificate).read() + cert = crypto.load_certificate(crypto.FILETYPE_PEM, cert) + self.ssl_environ = { + # pyOpenSSL doesn't provide access to any of these AFAICT +## 'SSL_PROTOCOL': 'SSLv2', +## SSL_CIPHER string The cipher specification name +## SSL_VERSION_INTERFACE string The mod_ssl program version +## SSL_VERSION_LIBRARY string The OpenSSL program version + } + + # Server certificate attributes + self.ssl_environ.update({ + 'SSL_SERVER_M_VERSION': cert.get_version(), + 'SSL_SERVER_M_SERIAL': cert.get_serial_number(), +## 'SSL_SERVER_V_START': Validity of server's certificate (start time), +## 'SSL_SERVER_V_END': Validity of server's certificate (end time), + }) + + for prefix, dn in [("I", cert.get_issuer()), + ("S", cert.get_subject())]: + # X509Name objects don't seem to have a way to get the + # complete DN string. Use str() and slice it instead, + # because str(dn) == "" + dnstr = str(dn)[18:-2] + + wsgikey = 'SSL_SERVER_%s_DN' % prefix + self.ssl_environ[wsgikey] = dnstr + + # The DN should be of the form: /k1=v1/k2=v2, but we must allow + # for any value to contain slashes itself (in a URL). + while dnstr: + pos = dnstr.rfind("=") + dnstr, value = dnstr[:pos], dnstr[pos + 1:] + pos = dnstr.rfind("/") + dnstr, key = dnstr[:pos], dnstr[pos + 1:] + if key and value: + wsgikey = 'SSL_SERVER_%s_DN_%s' % (prefix, key) + self.ssl_environ[wsgikey] = value + + diff --git a/plugins/WebUi/lib/pythonize.py b/plugins/WebUi/lib/pythonize.py new file mode 100644 index 000000000..699c61dac --- /dev/null +++ b/plugins/WebUi/lib/pythonize.py @@ -0,0 +1,38 @@ +""" +some dbus to python type conversions +-decorator for interface +-wrapper class for proxy +""" +def pythonize(var): + """translates dbus types back to basic python types.""" + if isinstance(var, list): + return [pythonize(value) for value in var] + if isinstance(var, tuple): + return tuple([pythonize(value) for value in var]) + if isinstance(var, dict): + return dict( + [(pythonize(key), pythonize(value)) for key, value in var.iteritems()] + ) + + for klass in [unicode, str, bool, int, float, long]: + if isinstance(var,klass): + return klass(var) + return var + +def pythonize_call(func): + def deco(*args,**kwargs): + return pythonize(func(*args, **kwargs)) + return deco + +def pythonize_interface(func): + def deco(*args, **kwargs): + args = pythonize(args) + kwargs = pythonize(kwargs) + return func(*args, **kwargs) + return deco + +class PythonizeProxy(object): + def __init__(self,proxy): + self.proxy = proxy + def __getattr__(self, key): + return pythonize_call(getattr(self.proxy, key)) diff --git a/plugins/WebUi/lib/readme.txt b/plugins/WebUi/lib/readme.txt new file mode 100644 index 000000000..16c5eee85 --- /dev/null +++ b/plugins/WebUi/lib/readme.txt @@ -0,0 +1,8 @@ +This folder may only contain general purpose utilities/files/tools. +They should be usable outside of deluge. + +Disclaimer: + +Some may have been adapted to work better with deluge. +But they will import other parts of deluge or Webui. + diff --git a/plugins/WebUi/lib/static_handler.py b/plugins/WebUi/lib/static_handler.py new file mode 100644 index 000000000..d5b706cca --- /dev/null +++ b/plugins/WebUi/lib/static_handler.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python +#(c) Martijn Voncken, mvoncken@gmail.com +#Same Licence as web.py 0.22 ->Public Domain +# +""" +static fileserving for web.py +without the need for wsgi wrapper magic. +""" +import webpy022 as web +from webpy022.http import seeother, url + +import posixpath +import urlparse +import urllib +import mimetypes +import os +import datetime +import cgi +from StringIO import StringIO +mimetypes.init() # try to read system mime.types + +class static_handler: + """ + mostly c&p from SimpleHttpServer + serves relative from start location + """ + base_dir = './' + extensions_map = mimetypes.types_map + + def get_base_dir(self): + #override this if you have a config that changes the base dir at runtime + #deluge on windows :( + return self.base_dir + + def GET(self, path): + path = self.translate_path(path) + if os.path.isdir(path): + if not path.endswith('/'): + path += "/" + return self.list_directory(path) + + ctype = self.guess_type(path) + + try: + f = open(path, 'rb') + except IOError: + raise Exception('file not found:%s' % path) + #web.header("404", "File not found") + #return + web.header("Content-type", ctype) + fs = os.fstat(f.fileno()) + web.header("Content-Length", str(fs[6])) + web.lastmodified(datetime.datetime.fromtimestamp(fs.st_mtime)) + print f.read() + + def translate_path(self, path): + """Translate a /-separated PATH to the local filename syntax. + + Components that mean special things to the local file system + (e.g. drive or directory names) are ignored. (XXX They should + probably be diagnosed.) + + """ + # abandon query parameters + path = urlparse.urlparse(path)[2] + path = posixpath.normpath(urllib.unquote(path)) + words = path.split('/') + words = filter(None, words) + path = self.get_base_dir() + for word in words: + drive, word = os.path.splitdrive(word) + head, word = os.path.split(word) + if word in (os.curdir, os.pardir): continue + path = os.path.join(path, word) + return path + + def guess_type(self, path): + base, ext = posixpath.splitext(path) + if ext in self.extensions_map: + return self.extensions_map[ext] + ext = ext.lower() + if ext in self.extensions_map: + return self.extensions_map[ext] + else: + return 'application/octet-stream' + + + def list_directory(self, path): + """Helper to produce a directory listing (absent index.html). + + Return value is either a file object, or None (indicating an + error). In either case, the headers are sent, making the + interface the same as for send_head(). + #TODO ->use web.py +template! + """ + try: + list = os.listdir(path) + except os.error: + web.header('404', "No permission to list directory") + return None + list.sort(key=lambda a: a.lower()) + f = StringIO() + displaypath = cgi.escape(urllib.unquote(path)) + f.write("Directory listing for %s\n" % displaypath) + f.write("

Directory listing for %s

\n" % displaypath) + f.write("
\n
    \n") + for name in list: + fullname = os.path.join(path, name) + displayname = linkname = name + # Append / for directories or @ for symbolic links + if os.path.isdir(fullname): + displayname = name + "/" + linkname = name + "/" + if os.path.islink(fullname): + displayname = name + "@" + # Note: a link to a directory displays with @ and links with / + f.write('
  • %s\n' + % (urllib.quote(linkname), cgi.escape(displayname))) + f.write("
\n
\n") + length = f.tell() + f.seek(0) + + web.header("Content-type", "text/html") + web.header("Content-Length", str(length)) + print f.read() + + +if __name__ == '__main__': + #example: + class usr_static(static_handler): + base_dir = os.path.expanduser('~') + + urls = ('/relative/(.*)','static_handler', + '/(.*)','usr_static') + + web.run(urls,globals()) diff --git a/plugins/WebUi/lib/webpy022/Dependency-not-really part of webui.txt b/plugins/WebUi/lib/webpy022/Dependency-not-really part of webui.txt new file mode 100644 index 000000000..54e2330e9 --- /dev/null +++ b/plugins/WebUi/lib/webpy022/Dependency-not-really part of webui.txt @@ -0,0 +1 @@ + http://webpy.org/ \ No newline at end of file diff --git a/plugins/WebUi/lib/webpy022/__init__.py b/plugins/WebUi/lib/webpy022/__init__.py new file mode 100644 index 000000000..25e03d137 --- /dev/null +++ b/plugins/WebUi/lib/webpy022/__init__.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python +from __future__ import generators + +"""web.py: makes web apps (http://webpy.org)""" +__version__ = "0.22" +__revision__ = "$Rev: 183 $" +__author__ = "Aaron Swartz " +__license__ = "public domain" +__contributors__ = "see http://webpy.org/changes" + +# todo: +# - some sort of accounts system + +import utils, db, net, wsgi, http, webapi, request, httpserver, debugerror +import template, form + +from utils import * +from db import * +from net import * +from wsgi import * +from http import * +from webapi import * +from request import * +from httpserver import * +from debugerror import * + +try: + import cheetah + from cheetah import * +except ImportError: + pass + +def main(): + import doctest + + doctest.testmod(utils) + doctest.testmod(db) + doctest.testmod(net) + doctest.testmod(wsgi) + doctest.testmod(http) + doctest.testmod(webapi) + doctest.testmod(request) + + try: + doctest.testmod(cheetah) + except NameError: + pass + + template.test() + + import sys + urls = ('/web.py', 'source') + class source: + def GET(self): + header('Content-Type', 'text/python') + print open(sys.argv[0]).read() + + if listget(sys.argv, 1) != 'test': + run(urls, locals()) + +if __name__ == "__main__": main() + diff --git a/plugins/WebUi/lib/webpy022/changes.txt b/plugins/WebUi/lib/webpy022/changes.txt new file mode 100644 index 000000000..326e8a177 --- /dev/null +++ b/plugins/WebUi/lib/webpy022/changes.txt @@ -0,0 +1,5 @@ +1:Commented out some code to enable a relative redirect. +This is not according to HTTP/1.1 Spec +But many deluge users will want to route the webui through firewalls/routers or use apache redirects. + +2:Disabled logging in the builtin http-server. diff --git a/plugins/WebUi/lib/webpy022/cheetah.py b/plugins/WebUi/lib/webpy022/cheetah.py new file mode 100644 index 000000000..db9fbf305 --- /dev/null +++ b/plugins/WebUi/lib/webpy022/cheetah.py @@ -0,0 +1,98 @@ +""" +Cheetah API +(from web.py) +""" + +__all__ = ["render"] + +import re, urlparse, pprint, traceback, sys +from Cheetah.Compiler import Compiler +from Cheetah.Filters import Filter +from utils import re_compile, memoize, dictadd +from net import htmlquote, websafe +from webapi import ctx, header, output, input, cookies, loadhooks + +def upvars(level=2): + """Guido van Rossum sez: don't use this function.""" + return dictadd( + sys._getframe(level).f_globals, + sys._getframe(level).f_locals) + +r_include = re_compile(r'(?!\\)#include \"(.*?)\"($|#)', re.M) +def __compiletemplate(template, base=None, isString=False): + if isString: + text = template + else: + text = open('templates/'+template).read() + # implement #include at compile-time + def do_include(match): + text = open('templates/'+match.groups()[0]).read() + return text + while r_include.findall(text): + text = r_include.sub(do_include, text) + + execspace = _compiletemplate.bases.copy() + tmpl_compiler = Compiler(source=text, mainClassName='GenTemplate') + tmpl_compiler.addImportedVarNames(execspace.keys()) + exec str(tmpl_compiler) in execspace + if base: + _compiletemplate.bases[base] = execspace['GenTemplate'] + + return execspace['GenTemplate'] + +_compiletemplate = memoize(__compiletemplate) +_compiletemplate.bases = {} + +def render(template, terms=None, asTemplate=False, base=None, + isString=False): + """ + Renders a template, caching where it can. + + `template` is the name of a file containing the a template in + the `templates/` folder, unless `isString`, in which case it's the + template itself. + + `terms` is a dictionary used to fill the template. If it's None, then + the caller's local variables are used instead, plus context, if it's not + already set, is set to `context`. + + If asTemplate is False, it `output`s the template directly. Otherwise, + it returns the template object. + + If the template is a potential base template (that is, something other templates) + can extend, then base should be a string with the name of the template. The + template will be cached and made available for future calls to `render`. + + Requires [Cheetah](http://cheetahtemplate.org/). + """ + # terms=['var1', 'var2'] means grab those variables + if isinstance(terms, list): + new = {} + old = upvars() + for k in terms: + new[k] = old[k] + terms = new + # default: grab all locals + elif terms is None: + terms = {'context': ctx, 'ctx':ctx} + terms.update(sys._getframe(1).f_locals) + # terms=d means use d as the searchList + if not isinstance(terms, tuple): + terms = (terms,) + + if 'headers' in ctx and not isString and template.endswith('.html'): + header('Content-Type','text/html; charset=utf-8', unique=True) + + if loadhooks.has_key('reloader'): + compiled_tmpl = __compiletemplate(template, base=base, isString=isString) + else: + compiled_tmpl = _compiletemplate(template, base=base, isString=isString) + compiled_tmpl = compiled_tmpl(searchList=terms, filter=WebSafe) + if asTemplate: + return compiled_tmpl + else: + return output(str(compiled_tmpl)) + +class WebSafe(Filter): + def filter(self, val, **keywords): + return websafe(val) diff --git a/plugins/WebUi/lib/webpy022/db.py b/plugins/WebUi/lib/webpy022/db.py new file mode 100644 index 000000000..2438a162f --- /dev/null +++ b/plugins/WebUi/lib/webpy022/db.py @@ -0,0 +1,703 @@ +""" +Database API +(part of web.py) +""" + +# todo: +# - test with sqlite +# - a store function? + +__all__ = [ + "UnknownParamstyle", "UnknownDB", + "sqllist", "sqlors", "aparam", "reparam", + "SQLQuery", "sqlquote", + "SQLLiteral", "sqlliteral", + "connect", + "TransactionError", "transaction", "transact", "commit", "rollback", + "query", + "select", "insert", "update", "delete" +] + +import time +try: import datetime +except ImportError: datetime = None + +from utils import storage, iters, iterbetter +import webapi as web + +try: + from DBUtils import PooledDB + web.config._hasPooling = True +except ImportError: + web.config._hasPooling = False + +class _ItplError(ValueError): + def __init__(self, text, pos): + ValueError.__init__(self) + self.text = text + self.pos = pos + def __str__(self): + return "unfinished expression in %s at char %d" % ( + repr(self.text), self.pos) + +def _interpolate(format): + """ + Takes a format string and returns a list of 2-tuples of the form + (boolean, string) where boolean says whether string should be evaled + or not. + + from (public domain, Ka-Ping Yee) + """ + from tokenize import tokenprog + + def matchorfail(text, pos): + match = tokenprog.match(text, pos) + if match is None: + raise _ItplError(text, pos) + return match, match.end() + + namechars = "abcdefghijklmnopqrstuvwxyz" \ + "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"; + chunks = [] + pos = 0 + + while 1: + dollar = format.find("$", pos) + if dollar < 0: + break + nextchar = format[dollar + 1] + + if nextchar == "{": + chunks.append((0, format[pos:dollar])) + pos, level = dollar + 2, 1 + while level: + match, pos = matchorfail(format, pos) + tstart, tend = match.regs[3] + token = format[tstart:tend] + if token == "{": + level = level + 1 + elif token == "}": + level = level - 1 + chunks.append((1, format[dollar + 2:pos - 1])) + + elif nextchar in namechars: + chunks.append((0, format[pos:dollar])) + match, pos = matchorfail(format, dollar + 1) + while pos < len(format): + if format[pos] == "." and \ + pos + 1 < len(format) and format[pos + 1] in namechars: + match, pos = matchorfail(format, pos + 1) + elif format[pos] in "([": + pos, level = pos + 1, 1 + while level: + match, pos = matchorfail(format, pos) + tstart, tend = match.regs[3] + token = format[tstart:tend] + if token[0] in "([": + level = level + 1 + elif token[0] in ")]": + level = level - 1 + else: + break + chunks.append((1, format[dollar + 1:pos])) + + else: + chunks.append((0, format[pos:dollar + 1])) + pos = dollar + 1 + (nextchar == "$") + + if pos < len(format): + chunks.append((0, format[pos:])) + return chunks + +class UnknownParamstyle(Exception): + """ + raised for unsupported db paramstyles + + (currently supported: qmark, numeric, format, pyformat) + """ + pass + +def aparam(): + """ + Returns the appropriate string to be used to interpolate + a value with the current `web.ctx.db_module` or simply %s + if there isn't one. + + >>> aparam() + '%s' + """ + if hasattr(web.ctx, 'db_module'): + style = web.ctx.db_module.paramstyle + else: + style = 'pyformat' + + if style == 'qmark': + return '?' + elif style == 'numeric': + return ':1' + elif style in ['format', 'pyformat']: + return '%s' + raise UnknownParamstyle, style + +def reparam(string_, dictionary): + """ + Takes a string and a dictionary and interpolates the string + using values from the dictionary. Returns an `SQLQuery` for the result. + + >>> reparam("s = $s", dict(s=True)) + + """ + vals = [] + result = [] + for live, chunk in _interpolate(string_): + if live: + result.append(aparam()) + vals.append(eval(chunk, dictionary)) + else: result.append(chunk) + return SQLQuery(''.join(result), vals) + +def sqlify(obj): + """ + converts `obj` to its proper SQL version + + >>> sqlify(None) + 'NULL' + >>> sqlify(True) + "'t'" + >>> sqlify(3) + '3' + """ + + # because `1 == True and hash(1) == hash(True)` + # we have to do this the hard way... + + if obj is None: + return 'NULL' + elif obj is True: + return "'t'" + elif obj is False: + return "'f'" + elif datetime and isinstance(obj, datetime.datetime): + return repr(obj.isoformat()) + else: + return repr(obj) + +class SQLQuery: + """ + You can pass this sort of thing as a clause in any db function. + Otherwise, you can pass a dictionary to the keyword argument `vars` + and the function will call reparam for you. + """ + # tested in sqlquote's docstring + def __init__(self, s='', v=()): + self.s, self.v = str(s), tuple(v) + + def __getitem__(self, key): # for backwards-compatibility + return [self.s, self.v][key] + + def __add__(self, other): + if isinstance(other, str): + self.s += other + elif isinstance(other, SQLQuery): + self.s += other.s + self.v += other.v + return self + + def __radd__(self, other): + if isinstance(other, str): + self.s = other + self.s + return self + else: + return NotImplemented + + def __str__(self): + try: + return self.s % tuple([sqlify(x) for x in self.v]) + except (ValueError, TypeError): + return self.s + + def __repr__(self): + return '' % repr(str(self)) + +class SQLLiteral: + """ + Protects a string from `sqlquote`. + + >>> insert('foo', time=SQLLiteral('NOW()'), _test=True) + + """ + def __init__(self, v): + self.v = v + + def __repr__(self): + return self.v + +sqlliteral = SQLLiteral + +def sqlquote(a): + """ + Ensures `a` is quoted properly for use in a SQL query. + + >>> 'WHERE x = ' + sqlquote(True) + ' AND y = ' + sqlquote(3) + + """ + return SQLQuery(aparam(), (a,)) + +class UnknownDB(Exception): + """raised for unsupported dbms""" + pass + +def connect(dbn, **keywords): + """ + Connects to the specified database. + + `dbn` currently must be "postgres", "mysql", or "sqlite". + + If DBUtils is installed, connection pooling will be used. + """ + if dbn == "postgres": + try: + import psycopg2 as db + except ImportError: + try: + import psycopg as db + except ImportError: + import pgdb as db + if 'pw' in keywords: + keywords['password'] = keywords['pw'] + del keywords['pw'] + keywords['database'] = keywords['db'] + del keywords['db'] + + elif dbn == "mysql": + import MySQLdb as db + if 'pw' in keywords: + keywords['passwd'] = keywords['pw'] + del keywords['pw'] + db.paramstyle = 'pyformat' # it's both, like psycopg + + elif dbn == "sqlite": + try: + import sqlite3 as db + db.paramstyle = 'qmark' + except ImportError: + try: + from pysqlite2 import dbapi2 as db + db.paramstyle = 'qmark' + except ImportError: + import sqlite as db + web.config._hasPooling = False + keywords['database'] = keywords['db'] + del keywords['db'] + + elif dbn == "firebird": + import kinterbasdb as db + if 'pw' in keywords: + keywords['passwd'] = keywords['pw'] + del keywords['pw'] + keywords['database'] = keywords['db'] + del keywords['db'] + + else: + raise UnknownDB, dbn + + web.ctx.db_name = dbn + web.ctx.db_module = db + web.ctx.db_transaction = 0 + web.ctx.db = keywords + + def _PooledDB(db, keywords): + # In DBUtils 0.9.3, `dbapi` argument is renamed as `creator` + # see Bug#122112 + if PooledDB.__version__.split('.') < '0.9.3'.split('.'): + return PooledDB.PooledDB(dbapi=db, **keywords) + else: + return PooledDB.PooledDB(creator=db, **keywords) + + def db_cursor(): + if isinstance(web.ctx.db, dict): + keywords = web.ctx.db + if web.config._hasPooling: + if 'db' not in globals(): + globals()['db'] = _PooledDB(db, keywords) + web.ctx.db = globals()['db'].connection() + else: + web.ctx.db = db.connect(**keywords) + return web.ctx.db.cursor() + web.ctx.db_cursor = db_cursor + + web.ctx.dbq_count = 0 + + def db_execute(cur, sql_query, dorollback=True): + """executes an sql query""" + + web.ctx.dbq_count += 1 + + try: + a = time.time() + out = cur.execute(sql_query.s, sql_query.v) + b = time.time() + except: + if web.config.get('db_printing'): + print >> web.debug, 'ERR:', str(sql_query) + if dorollback: rollback(care=False) + raise + + if web.config.get('db_printing'): + print >> web.debug, '%s (%s): %s' % (round(b-a, 2), web.ctx.dbq_count, str(sql_query)) + + return out + web.ctx.db_execute = db_execute + return web.ctx.db + +class TransactionError(Exception): pass + +class transaction: + """ + A context that can be used in conjunction with "with" statements + to implement SQL transactions. Starts a transaction on enter, + rolls it back if there's an error; otherwise it commits it at the + end. + """ + def __enter__(self): + transact() + + def __exit__(self, exctype, excvalue, traceback): + if exctype is not None: + rollback() + else: + commit() + +def transact(): + """Start a transaction.""" + if not web.ctx.db_transaction: + # commit everything up to now, so we don't rollback it later + if hasattr(web.ctx.db, 'commit'): + web.ctx.db.commit() + else: + db_cursor = web.ctx.db_cursor() + web.ctx.db_execute(db_cursor, + SQLQuery("SAVEPOINT webpy_sp_%s" % web.ctx.db_transaction)) + web.ctx.db_transaction += 1 + +def commit(): + """Commits a transaction.""" + web.ctx.db_transaction -= 1 + if web.ctx.db_transaction < 0: + raise TransactionError, "not in a transaction" + + if not web.ctx.db_transaction: + if hasattr(web.ctx.db, 'commit'): + web.ctx.db.commit() + else: + db_cursor = web.ctx.db_cursor() + web.ctx.db_execute(db_cursor, + SQLQuery("RELEASE SAVEPOINT webpy_sp_%s" % web.ctx.db_transaction)) + +def rollback(care=True): + """Rolls back a transaction.""" + web.ctx.db_transaction -= 1 + if web.ctx.db_transaction < 0: + web.db_transaction = 0 + if care: + raise TransactionError, "not in a transaction" + else: + return + + if not web.ctx.db_transaction: + if hasattr(web.ctx.db, 'rollback'): + web.ctx.db.rollback() + else: + db_cursor = web.ctx.db_cursor() + web.ctx.db_execute(db_cursor, + SQLQuery("ROLLBACK TO SAVEPOINT webpy_sp_%s" % web.ctx.db_transaction), + dorollback=False) + +def query(sql_query, vars=None, processed=False, _test=False): + """ + Execute SQL query `sql_query` using dictionary `vars` to interpolate it. + If `processed=True`, `vars` is a `reparam`-style list to use + instead of interpolating. + + >>> query("SELECT * FROM foo", _test=True) + + >>> query("SELECT * FROM foo WHERE x = $x", vars=dict(x='f'), _test=True) + + >>> query("SELECT * FROM foo WHERE x = " + sqlquote('f'), _test=True) + + """ + if vars is None: vars = {} + + if not processed and not isinstance(sql_query, SQLQuery): + sql_query = reparam(sql_query, vars) + + if _test: return sql_query + + db_cursor = web.ctx.db_cursor() + web.ctx.db_execute(db_cursor, sql_query) + + if db_cursor.description: + names = [x[0] for x in db_cursor.description] + def iterwrapper(): + row = db_cursor.fetchone() + while row: + yield storage(dict(zip(names, row))) + row = db_cursor.fetchone() + out = iterbetter(iterwrapper()) + if web.ctx.db_name != "sqlite": + out.__len__ = lambda: int(db_cursor.rowcount) + out.list = lambda: [storage(dict(zip(names, x))) \ + for x in db_cursor.fetchall()] + else: + out = db_cursor.rowcount + + if not web.ctx.db_transaction: web.ctx.db.commit() + return out + +def sqllist(lst): + """ + Converts the arguments for use in something like a WHERE clause. + + >>> sqllist(['a', 'b']) + 'a, b' + >>> sqllist('a') + 'a' + + """ + if isinstance(lst, str): + return lst + else: + return ', '.join(lst) + +def sqlors(left, lst): + """ + `left is a SQL clause like `tablename.arg = ` + and `lst` is a list of values. Returns a reparam-style + pair featuring the SQL that ORs together the clause + for each item in the lst. + + >>> sqlors('foo = ', []) + + >>> sqlors('foo = ', [1]) + + >>> sqlors('foo = ', 1) + + >>> sqlors('foo = ', [1,2,3]) + + """ + if isinstance(lst, iters): + lst = list(lst) + ln = len(lst) + if ln == 0: + return SQLQuery("2+2=5", []) + if ln == 1: + lst = lst[0] + + if isinstance(lst, iters): + return SQLQuery('(' + left + + (' OR ' + left).join([aparam() for param in lst]) + ")", lst) + else: + return SQLQuery(left + aparam(), [lst]) + +def sqlwhere(dictionary, grouping=' AND '): + """ + Converts a `dictionary` to an SQL WHERE clause `SQLQuery`. + + >>> sqlwhere({'cust_id': 2, 'order_id':3}) + + >>> sqlwhere({'cust_id': 2, 'order_id':3}, grouping=', ') + + """ + + return SQLQuery(grouping.join([ + '%s = %s' % (k, aparam()) for k in dictionary.keys() + ]), dictionary.values()) + +def select(tables, vars=None, what='*', where=None, order=None, group=None, + limit=None, offset=None, _test=False): + """ + Selects `what` from `tables` with clauses `where`, `order`, + `group`, `limit`, and `offset`. Uses vars to interpolate. + Otherwise, each clause can be a SQLQuery. + + >>> select('foo', _test=True) + + >>> select(['foo', 'bar'], where="foo.bar_id = bar.id", limit=5, _test=True) + + """ + if vars is None: vars = {} + qout = "" + + def gen_clause(sql, val): + if isinstance(val, (int, long)): + if sql == 'WHERE': + nout = 'id = ' + sqlquote(val) + else: + nout = SQLQuery(val) + elif isinstance(val, (list, tuple)) and len(val) == 2: + nout = SQLQuery(val[0], val[1]) # backwards-compatibility + elif isinstance(val, SQLQuery): + nout = val + elif val: + nout = reparam(val, vars) + else: + return "" + + out = "" + if qout: out += " " + out += sql + " " + nout + return out + + if web.ctx.get('db_name') == "firebird": + for (sql, val) in ( + ('FIRST', limit), + ('SKIP', offset) + ): + qout += gen_clause(sql, val) + if qout: + SELECT = 'SELECT ' + qout + else: + SELECT = 'SELECT' + qout = "" + sql_clauses = ( + (SELECT, what), + ('FROM', sqllist(tables)), + ('WHERE', where), + ('GROUP BY', group), + ('ORDER BY', order) + ) + else: + sql_clauses = ( + ('SELECT', what), + ('FROM', sqllist(tables)), + ('WHERE', where), + ('GROUP BY', group), + ('ORDER BY', order), + ('LIMIT', limit), + ('OFFSET', offset) + ) + + for (sql, val) in sql_clauses: + qout += gen_clause(sql, val) + + if _test: return qout + return query(qout, processed=True) + +def insert(tablename, seqname=None, _test=False, **values): + """ + Inserts `values` into `tablename`. Returns current sequence ID. + Set `seqname` to the ID if it's not the default, or to `False` + if there isn't one. + + >>> insert('foo', joe='bob', a=2, _test=True) + + """ + + if values: + sql_query = SQLQuery("INSERT INTO %s (%s) VALUES (%s)" % ( + tablename, + ", ".join(values.keys()), + ', '.join([aparam() for x in values]) + ), values.values()) + else: + sql_query = SQLQuery("INSERT INTO %s DEFAULT VALUES" % tablename) + + if _test: return sql_query + + db_cursor = web.ctx.db_cursor() + if seqname is False: + pass + elif web.ctx.db_name == "postgres": + if seqname is None: + seqname = tablename + "_id_seq" + sql_query += "; SELECT currval('%s')" % seqname + elif web.ctx.db_name == "mysql": + web.ctx.db_execute(db_cursor, sql_query) + sql_query = SQLQuery("SELECT last_insert_id()") + elif web.ctx.db_name == "sqlite": + web.ctx.db_execute(db_cursor, sql_query) + # not really the same... + sql_query = SQLQuery("SELECT last_insert_rowid()") + + web.ctx.db_execute(db_cursor, sql_query) + try: + out = db_cursor.fetchone()[0] + except Exception: + out = None + + if not web.ctx.db_transaction: web.ctx.db.commit() + + return out + +def update(tables, where, vars=None, _test=False, **values): + """ + Update `tables` with clause `where` (interpolated using `vars`) + and setting `values`. + + >>> joe = 'Joseph' + >>> update('foo', where='name = $joe', name='bob', age=5, + ... vars=locals(), _test=True) + + """ + if vars is None: vars = {} + + if isinstance(where, (int, long)): + where = "id = " + sqlquote(where) + elif isinstance(where, (list, tuple)) and len(where) == 2: + where = SQLQuery(where[0], where[1]) + elif isinstance(where, SQLQuery): + pass + else: + where = reparam(where, vars) + + query = ( + "UPDATE " + sqllist(tables) + + " SET " + sqlwhere(values, ', ') + + " WHERE " + where) + + if _test: return query + + db_cursor = web.ctx.db_cursor() + web.ctx.db_execute(db_cursor, query) + + if not web.ctx.db_transaction: web.ctx.db.commit() + return db_cursor.rowcount + +def delete(table, where=None, using=None, vars=None, _test=False): + """ + Deletes from `table` with clauses `where` and `using`. + + >>> name = 'Joe' + >>> delete('foo', where='name = $name', vars=locals(), _test=True) + + """ + if vars is None: vars = {} + + if isinstance(where, (int, long)): + where = "id = " + sqlquote(where) + elif isinstance(where, (list, tuple)) and len(where) == 2: + where = SQLQuery(where[0], where[1]) + elif isinstance(where, SQLQuery): + pass + elif where is None: + pass + else: + where = reparam(where, vars) + + q = 'DELETE FROM ' + table + if where: + q += ' WHERE ' + where + if using and web.ctx.get('db_name') != "firebird": + q += ' USING ' + sqllist(using) + + if _test: return q + + db_cursor = web.ctx.db_cursor() + web.ctx.db_execute(db_cursor, q) + + if not web.ctx.db_transaction: web.ctx.db.commit() + return db_cursor.rowcount + +if __name__ == "__main__": + import doctest + doctest.testmod() diff --git a/plugins/WebUi/lib/webpy022/debugerror.py b/plugins/WebUi/lib/webpy022/debugerror.py new file mode 100644 index 000000000..1de465a81 --- /dev/null +++ b/plugins/WebUi/lib/webpy022/debugerror.py @@ -0,0 +1,316 @@ +""" +pretty debug errors +(part of web.py) + +adapted from Django +Copyright (c) 2005, the Lawrence Journal-World +Used under the modified BSD license: +http://www.xfree86.org/3.3.6/COPYRIGHT2.html#5 +""" + +__all__ = ["debugerror", "djangoerror"] + +import sys, urlparse, pprint +from net import websafe +from template import Template +import webapi as web + +import os, os.path +whereami = os.path.join(os.getcwd(), __file__) +whereami = os.path.sep.join(whereami.split(os.path.sep)[:-1]) +djangoerror_t = """\ +$def with (exception_type, exception_value, frames) + + + + + + $exception_type at $ctx.path + + + + + +
+

$exception_type at $ctx.path

+

$exception_value

+ + + + + + +
Python$frames[0].filename in $frames[0].function, line $frames[0].lineno
Web$ctx.method $ctx.home$ctx.path
+
+
+

Traceback (innermost first)

+
    +$for frame in frames: +
  • + $frame.filename in $frame.function + $if frame.context_line: +
    + $if frame.pre_context: +
      + $for line in frame.pre_context: +
    1. $line
    2. +
    +
    1. $frame.context_line ...
    + $if frame.post_context: +
      + $for line in frame.post_context: +
    1. $line
    2. +
    +
    + + $if frame.vars: +
    + Local vars + $# $inspect.formatargvalues(*inspect.getargvalues(frame['tb'].tb_frame)) +
    + $:dicttable(frame.vars, kls='vars', id=('v' + str(frame.id))) +
  • +
+
+ +
+$if ctx.output or ctx.headers: +

Response so far

+

HEADERS

+

+ $for kv in ctx.headers: + $kv[0]: $kv[1]
+ $else: + [no headers] +

+ +

BODY

+

+ $ctx.output +

+ +

Request information

+ +

INPUT

+$:dicttable(web.input()) + + +$:dicttable(web.cookies()) + +

META

+$ newctx = [] +$# ) and (k not in ['env', 'output', 'headers', 'environ', 'status', 'db_execute']): +$for k, v in ctx.iteritems(): + $if not k.startswith('_') and (k in x): + $newctx.append(kv) +$:dicttable(dict(newctx)) + +

ENVIRONMENT

+$:dicttable(ctx.env) +
+ +
+

+ You're seeing this error because you have web.internalerror + set to web.debugerror. Change that if you want a different one. +

+
+ + + +""" + +dicttable_t = r"""$def with (d, kls='req', id=None) +$if d: + + + $ temp = d.items() + $temp.sort() + $for kv in temp: + + +
VariableValue
$kv[0]
$prettify(kv[1])
+$else: +

No data.

+""" + +dicttable_r = Template(dicttable_t, filter=websafe) +djangoerror_r = Template(djangoerror_t, filter=websafe) + +def djangoerror(): + def _get_lines_from_file(filename, lineno, context_lines): + """ + Returns context_lines before and after lineno from file. + Returns (pre_context_lineno, pre_context, context_line, post_context). + """ + try: + source = open(filename).readlines() + lower_bound = max(0, lineno - context_lines) + upper_bound = lineno + context_lines + + pre_context = \ + [line.strip('\n') for line in source[lower_bound:lineno]] + context_line = source[lineno].strip('\n') + post_context = \ + [line.strip('\n') for line in source[lineno + 1:upper_bound]] + + return lower_bound, pre_context, context_line, post_context + except (OSError, IOError): + return None, [], None, [] + + exception_type, exception_value, tback = sys.exc_info() + frames = [] + while tback is not None: + filename = tback.tb_frame.f_code.co_filename + function = tback.tb_frame.f_code.co_name + lineno = tback.tb_lineno - 1 + pre_context_lineno, pre_context, context_line, post_context = \ + _get_lines_from_file(filename, lineno, 7) + frames.append(web.storage({ + 'tback': tback, + 'filename': filename, + 'function': function, + 'lineno': lineno, + 'vars': tback.tb_frame.f_locals, + 'id': id(tback), + 'pre_context': pre_context, + 'context_line': context_line, + 'post_context': post_context, + 'pre_context_lineno': pre_context_lineno, + })) + tback = tback.tb_next + frames.reverse() + urljoin = urlparse.urljoin + def prettify(x): + try: + out = pprint.pformat(x) + except Exception, e: + out = '[could not display: <' + e.__class__.__name__ + \ + ': '+str(e)+'>]' + return out + dt = dicttable_r + dt.globals = {'prettify': prettify} + t = djangoerror_r + t.globals = {'ctx': web.ctx, 'web':web, 'dicttable':dt, 'dict':dict, 'str':str} + return t(exception_type, exception_value, frames) + +def debugerror(): + """ + A replacement for `internalerror` that presents a nice page with lots + of debug information for the programmer. + + (Based on the beautiful 500 page from [Django](http://djangoproject.com/), + designed by [Wilson Miner](http://wilsonminer.com/).) + """ + + web.ctx.headers = [('Content-Type', 'text/html')] + web.ctx.output = djangoerror() + +if __name__ == "__main__": + urls = ( + '/', 'index' + ) + + class index: + def GET(self): + thisdoesnotexist + + web.internalerror = web.debugerror + web.run(urls) \ No newline at end of file diff --git a/plugins/WebUi/lib/webpy022/form.py b/plugins/WebUi/lib/webpy022/form.py new file mode 100644 index 000000000..b1b808e49 --- /dev/null +++ b/plugins/WebUi/lib/webpy022/form.py @@ -0,0 +1,215 @@ +""" +HTML forms +(part of web.py) +""" + +import copy, re +import webapi as web +import utils, net + +def attrget(obj, attr, value=None): + if hasattr(obj, 'has_key') and obj.has_key(attr): return obj[attr] + if hasattr(obj, attr): return getattr(obj, attr) + return value + +class Form: + def __init__(self, *inputs, **kw): + self.inputs = inputs + self.valid = True + self.note = None + self.validators = kw.pop('validators', []) + + def __call__(self, x=None): + o = copy.deepcopy(self) + if x: o.validates(x) + return o + + def render(self): + out = '' + out += self.rendernote(self.note) + out += '\n' + for i in self.inputs: + out += ' ' % (i.id, i.description) + out += "" + out += '\n' % (i.id, self.rendernote(i.note)) + out += "
"+i.pre+i.render()+i.post+"%s
" + return out + + def rendernote(self, note): + if note: return '%s' % note + else: return "" + + def validates(self, source=None, _validate=True, **kw): + source = source or kw or web.input() + out = True + for i in self.inputs: + v = attrget(source, i.name) + if _validate: + out = i.validate(v) and out + else: + i.value = v + if _validate: + out = out and self._validate(source) + self.valid = out + return out + + def _validate(self, value): + self.value = value + for v in self.validators: + if not v.valid(value): + self.note = v.msg + return False + return True + + def fill(self, source=None, **kw): + return self.validates(source, _validate=False, **kw) + + def __getitem__(self, i): + for x in self.inputs: + if x.name == i: return x + raise KeyError, i + + def _get_d(self): #@@ should really be form.attr, no? + return utils.storage([(i.name, i.value) for i in self.inputs]) + d = property(_get_d) + +class Input(object): + def __init__(self, name, *validators, **attrs): + self.description = attrs.pop('description', name) + self.value = attrs.pop('value', None) + self.pre = attrs.pop('pre', "") + self.post = attrs.pop('post', "") + self.id = attrs.setdefault('id', name) + if 'class_' in attrs: + attrs['class'] = attrs['class_'] + del attrs['class_'] + self.name, self.validators, self.attrs, self.note = name, validators, attrs, None + + def validate(self, value): + self.value = value + for v in self.validators: + if not v.valid(value): + self.note = v.msg + return False + return True + + def render(self): raise NotImplementedError + + def addatts(self): + str = "" + for (n, v) in self.attrs.items(): + str += ' %s="%s"' % (n, net.websafe(v)) + return str + +#@@ quoting + +class Textbox(Input): + def render(self): + x = '' + +class Checkbox(Input): + def render(self): + x = 'moved permanently') + +def found(url): + """A `302 Found` redirect.""" + return redirect(url, '302 Found') + +def seeother(url): + """A `303 See Other` redirect.""" + return redirect(url, '303 See Other') + +def tempredirect(url): + """A `307 Temporary Redirect` redirect.""" + return redirect(url, '307 Temporary Redirect') + +def write(cgi_response): + """ + Converts a standard CGI-style string response into `header` and + `output` calls. + """ + cgi_response = str(cgi_response) + cgi_response.replace('\r\n', '\n') + head, body = cgi_response.split('\n\n', 1) + lines = head.split('\n') + + for line in lines: + if line.isspace(): + continue + hdr, value = line.split(":", 1) + value = value.strip() + if hdr.lower() == "status": + web.ctx.status = value + else: + web.header(hdr, value) + + web.output(body) + +def urlencode(query): + """ + Same as urllib.urlencode, but supports unicode strings. + + >>> urlencode({'text':'foo bar'}) + 'text=foo+bar' + """ + query = dict([(k, utils.utf8(v)) for k, v in query.items()]) + return urllib.urlencode(query) + +def changequery(query=None, **kw): + """ + Imagine you're at `/foo?a=1&b=2`. Then `changequery(a=3)` will return + `/foo?a=3&b=2` -- the same URL but with the arguments you requested + changed. + """ + if query is None: + query = web.input(_method='get') + for k, v in kw.iteritems(): + if v is None: + query.pop(k, None) + else: + query[k] = v + out = web.ctx.path + if query: + out += '?' + urlencode(query) + return out + +def url(path=None, **kw): + """ + Makes url by concatinating web.ctx.homepath and path and the + query string created using the arguments. + """ + if path is None: + path = web.ctx.path + if path.startswith("/"): + out = web.ctx.homepath + path + else: + out = path + + if kw: + out += '?' + urlencode(kw) + + return out + +def background(func): + """A function decorator to run a long-running function as a background thread.""" + def internal(*a, **kw): + web.data() # cache it + + tmpctx = web._context[threading.currentThread()] + web._context[threading.currentThread()] = utils.storage(web.ctx.copy()) + + def newfunc(): + web._context[threading.currentThread()] = tmpctx + func(*a, **kw) + myctx = web._context[threading.currentThread()] + for k in myctx.keys(): + if k not in ['status', 'headers', 'output']: + try: del myctx[k] + except KeyError: pass + + t = threading.Thread(target=newfunc) + background.threaddb[id(t)] = t + t.start() + web.ctx.headers = [] + return seeother(changequery(_t=id(t))) + return internal +background.threaddb = {} + +def backgrounder(func): + def internal(*a, **kw): + i = web.input(_method='get') + if '_t' in i: + try: + t = background.threaddb[int(i._t)] + except KeyError: + return web.notfound() + web._context[threading.currentThread()] = web._context[t] + return + else: + return func(*a, **kw) + return internal + +class Reloader: + """ + Before every request, checks to see if any loaded modules have changed on + disk and, if so, reloads them. + """ + def __init__(self, func): + self.func = func + self.mtimes = {} + # cheetah: + # b = _compiletemplate.bases + # _compiletemplate = globals()['__compiletemplate'] + # _compiletemplate.bases = b + + web.loadhooks['reloader'] = self.check + # todo: + # - replace relrcheck with a loadhook + #if reloader in middleware: + # relr = reloader(None) + # relrcheck = relr.check + # middleware.remove(reloader) + #else: + # relr = None + # relrcheck = lambda: None + # if relr: + # relr.func = wsgifunc + # return wsgifunc + # + + + def check(self): + for mod in sys.modules.values(): + try: + mtime = os.stat(mod.__file__).st_mtime + except (AttributeError, OSError, IOError): + continue + if mod.__file__.endswith('.pyc') and \ + os.path.exists(mod.__file__[:-1]): + mtime = max(os.stat(mod.__file__[:-1]).st_mtime, mtime) + if mod not in self.mtimes: + self.mtimes[mod] = mtime + elif self.mtimes[mod] < mtime: + try: + reload(mod) + self.mtimes[mod] = mtime + except ImportError: + pass + return True + + def __call__(self, e, o): + self.check() + return self.func(e, o) + +reloader = Reloader + +def profiler(app): + """Outputs basic profiling information at the bottom of each response.""" + from utils import profile + def profile_internal(e, o): + out, result = profile(app)(e, o) + return out + ['
' + net.websafe(result) + '
'] + return profile_internal + +if __name__ == "__main__": + import doctest + doctest.testmod() diff --git a/plugins/WebUi/lib/webpy022/httpserver.py b/plugins/WebUi/lib/webpy022/httpserver.py new file mode 100644 index 000000000..6df60b1d8 --- /dev/null +++ b/plugins/WebUi/lib/webpy022/httpserver.py @@ -0,0 +1,227 @@ +__all__ = ["runsimple"] + +import sys, os +import webapi as web +import net + +def runbasic(func, server_address=("0.0.0.0", 8080)): + """ + Runs a simple HTTP server hosting WSGI app `func`. The directory `static/` + is hosted statically. + + Based on [WsgiServer][ws] from [Colin Stewart][cs]. + + [ws]: http://www.owlfish.com/software/wsgiutils/documentation/wsgi-server-api.html + [cs]: http://www.owlfish.com/ + """ + # Copyright (c) 2004 Colin Stewart (http://www.owlfish.com/) + # Modified somewhat for simplicity + # Used under the modified BSD license: + # http://www.xfree86.org/3.3.6/COPYRIGHT2.html#5 + + import SimpleHTTPServer, SocketServer, BaseHTTPServer, urlparse + import socket, errno + import traceback + + class WSGIHandler(SimpleHTTPServer.SimpleHTTPRequestHandler): + def run_wsgi_app(self): + protocol, host, path, parameters, query, fragment = \ + urlparse.urlparse('http://dummyhost%s' % self.path) + + # we only use path, query + env = {'wsgi.version': (1, 0) + ,'wsgi.url_scheme': 'http' + ,'wsgi.input': self.rfile + ,'wsgi.errors': sys.stderr + ,'wsgi.multithread': 1 + ,'wsgi.multiprocess': 0 + ,'wsgi.run_once': 0 + ,'REQUEST_METHOD': self.command + ,'REQUEST_URI': self.path + ,'PATH_INFO': path + ,'QUERY_STRING': query + ,'CONTENT_TYPE': self.headers.get('Content-Type', '') + ,'CONTENT_LENGTH': self.headers.get('Content-Length', '') + ,'REMOTE_ADDR': self.client_address[0] + ,'SERVER_NAME': self.server.server_address[0] + ,'SERVER_PORT': str(self.server.server_address[1]) + ,'SERVER_PROTOCOL': self.request_version + } + + for http_header, http_value in self.headers.items(): + env ['HTTP_%s' % http_header.replace('-', '_').upper()] = \ + http_value + + # Setup the state + self.wsgi_sent_headers = 0 + self.wsgi_headers = [] + + try: + # We have there environment, now invoke the application + result = self.server.app(env, self.wsgi_start_response) + try: + try: + for data in result: + if data: + self.wsgi_write_data(data) + finally: + if hasattr(result, 'close'): + result.close() + except socket.error, socket_err: + # Catch common network errors and suppress them + if (socket_err.args[0] in \ + (errno.ECONNABORTED, errno.EPIPE)): + return + except socket.timeout, socket_timeout: + return + except: + print >> web.debug, traceback.format_exc(), + + if (not self.wsgi_sent_headers): + # We must write out something! + self.wsgi_write_data(" ") + return + + do_POST = run_wsgi_app + do_PUT = run_wsgi_app + do_DELETE = run_wsgi_app + + def do_GET(self): + if self.path.startswith('/static/'): + SimpleHTTPServer.SimpleHTTPRequestHandler.do_GET(self) + else: + self.run_wsgi_app() + + def wsgi_start_response(self, response_status, response_headers, + exc_info=None): + if (self.wsgi_sent_headers): + raise Exception \ + ("Headers already sent and start_response called again!") + # Should really take a copy to avoid changes in the application.... + self.wsgi_headers = (response_status, response_headers) + return self.wsgi_write_data + + def wsgi_write_data(self, data): + if (not self.wsgi_sent_headers): + status, headers = self.wsgi_headers + # Need to send header prior to data + status_code = status[:status.find(' ')] + status_msg = status[status.find(' ') + 1:] + self.send_response(int(status_code), status_msg) + for header, value in headers: + self.send_header(header, value) + self.end_headers() + self.wsgi_sent_headers = 1 + # Send the data + self.wfile.write(data) + + class WSGIServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer): + def __init__(self, func, server_address): + BaseHTTPServer.HTTPServer.__init__(self, + server_address, + WSGIHandler) + self.app = func + self.serverShuttingDown = 0 + + print "http://%s:%d/" % server_address + WSGIServer(func, server_address).serve_forever() + +def runsimple(func, server_address=("0.0.0.0", 8080)): + """ + Runs [CherryPy][cp] WSGI server hosting WSGI app `func`. + The directory `static/` is hosted statically. + + [cp]: http://www.cherrypy.org + """ + from wsgiserver import CherryPyWSGIServer + from SimpleHTTPServer import SimpleHTTPRequestHandler + from BaseHTTPServer import BaseHTTPRequestHandler + + class StaticApp(SimpleHTTPRequestHandler): + """WSGI application for serving static files.""" + def __init__(self, environ, start_response): + self.headers = [] + self.environ = environ + self.start_response = start_response + + def send_response(self, status, msg=""): + self.status = str(status) + " " + msg + + def send_header(self, name, value): + self.headers.append((name, value)) + + def end_headers(self): + pass + + def log_message(*a): pass + + def __iter__(self): + environ = self.environ + + self.path = environ.get('PATH_INFO', '') + self.client_address = environ.get('REMOTE_ADDR','-'), \ + environ.get('REMOTE_PORT','-') + self.command = environ.get('REQUEST_METHOD', '-') + + from cStringIO import StringIO + self.wfile = StringIO() # for capturing error + + f = self.send_head() + self.start_response(self.status, self.headers) + + if f: + block_size = 16 * 1024 + while True: + buf = f.read(block_size) + if not buf: + break + yield buf + f.close() + else: + value = self.wfile.getvalue() + yield value + + class WSGIWrapper(BaseHTTPRequestHandler): + """WSGI wrapper for logging the status and serving static files.""" + def __init__(self, app): + self.app = app + self.format = '%s - - [%s] "%s %s %s" - %s' + + def __call__(self, environ, start_response): + def xstart_response(status, response_headers, *args): + write = start_response(status, response_headers, *args) + self.log(status, environ) + return write + + path = environ.get('PATH_INFO', '') + if path.startswith('/static/'): + return StaticApp(environ, xstart_response) + else: + return self.app(environ, xstart_response) + + def log(self, status, environ): + #mvoncken,no logging.. + return + + outfile = environ.get('wsgi.errors', web.debug) + req = environ.get('PATH_INFO', '_') + protocol = environ.get('ACTUAL_SERVER_PROTOCOL', '-') + method = environ.get('REQUEST_METHOD', '-') + host = "%s:%s" % (environ.get('REMOTE_ADDR','-'), + environ.get('REMOTE_PORT','-')) + + #@@ It is really bad to extend from + #@@ BaseHTTPRequestHandler just for this method + time = self.log_date_time_string() + + print >> outfile, self.format % (host, time, protocol, + method, req, status) + + func = WSGIWrapper(func) + server = CherryPyWSGIServer(server_address, func, server_name="localhost") + + print "http://%s:%d/" % server_address + try: + server.start() + except KeyboardInterrupt: + server.stop() diff --git a/plugins/WebUi/lib/webpy022/net.py b/plugins/WebUi/lib/webpy022/net.py new file mode 100644 index 000000000..b97d4e155 --- /dev/null +++ b/plugins/WebUi/lib/webpy022/net.py @@ -0,0 +1,155 @@ +""" +Network Utilities +(from web.py) +""" + +__all__ = [ + "validipaddr", "validipport", "validip", "validaddr", + "urlquote", + "httpdate", "parsehttpdate", + "htmlquote", "websafe", +] + +import urllib, time +try: import datetime +except ImportError: pass + +def validipaddr(address): + """returns True if `address` is a valid IPv4 address""" + try: + octets = address.split('.') + assert len(octets) == 4 + for x in octets: + assert 0 <= int(x) <= 255 + except (AssertionError, ValueError): + return False + return True + +def validipport(port): + """returns True if `port` is a valid IPv4 port""" + try: + assert 0 <= int(port) <= 65535 + except (AssertionError, ValueError): + return False + return True + +def validip(ip, defaultaddr="0.0.0.0", defaultport=8080): + """returns `(ip_address, port)` from string `ip_addr_port`""" + addr = defaultaddr + port = defaultport + + ip = ip.split(":", 1) + if len(ip) == 1: + if not ip[0]: + pass + elif validipaddr(ip[0]): + addr = ip[0] + elif validipport(ip[0]): + port = int(ip[0]) + else: + raise ValueError, ':'.join(ip) + ' is not a valid IP address/port' + elif len(ip) == 2: + addr, port = ip + if not validipaddr(addr) and validipport(port): + raise ValueError, ':'.join(ip) + ' is not a valid IP address/port' + port = int(port) + else: + raise ValueError, ':'.join(ip) + ' is not a valid IP address/port' + return (addr, port) + +def validaddr(string_): + """ + returns either (ip_address, port) or "/path/to/socket" from string_ + + >>> validaddr('/path/to/socket') + '/path/to/socket' + >>> validaddr('8000') + ('0.0.0.0', 8000) + >>> validaddr('127.0.0.1') + ('127.0.0.1', 8080) + >>> validaddr('127.0.0.1:8000') + ('127.0.0.1', 8000) + >>> validaddr('fff') + Traceback (most recent call last): + ... + ValueError: fff is not a valid IP address/port + """ + if '/' in string_: + return string_ + else: + return validip(string_) + +def urlquote(val): + """ + Quotes a string for use in a URL. + + >>> urlquote('://?f=1&j=1') + '%3A//%3Ff%3D1%26j%3D1' + >>> urlquote(None) + '' + >>> urlquote(u'\u203d') + '%E2%80%BD' + """ + if val is None: return '' + if not isinstance(val, unicode): val = str(val) + else: val = val.encode('utf-8') + return urllib.quote(val) + +def httpdate(date_obj): + """ + Formats a datetime object for use in HTTP headers. + + >>> import datetime + >>> httpdate(datetime.datetime(1970, 1, 1, 1, 1, 1)) + 'Thu, 01 Jan 1970 01:01:01 GMT' + """ + return date_obj.strftime("%a, %d %b %Y %H:%M:%S GMT") + +def parsehttpdate(string_): + """ + Parses an HTTP date into a datetime object. + + >>> parsehttpdate('Thu, 01 Jan 1970 01:01:01 GMT') + datetime.datetime(1970, 1, 1, 1, 1, 1) + """ + try: + t = time.strptime(string_, "%a, %d %b %Y %H:%M:%S %Z") + except ValueError: + return None + return datetime.datetime(*t[:6]) + +def htmlquote(text): + """ + Encodes `text` for raw use in HTML. + + >>> htmlquote("<'&\\">") + '<'&">' + """ + text = text.replace("&", "&") # Must be done first! + text = text.replace("<", "<") + text = text.replace(">", ">") + text = text.replace("'", "'") + text = text.replace('"', """) + return text + +def websafe(val): + """ + Converts `val` so that it's safe for use in UTF-8 HTML. + + >>> websafe("<'&\\">") + '<'&">' + >>> websafe(None) + '' + >>> websafe(u'\u203d') + '\\xe2\\x80\\xbd' + """ + if val is None: + return '' + if isinstance(val, unicode): + val = val.encode('utf-8') + val = str(val) + return htmlquote(val) + +if __name__ == "__main__": + import doctest + doctest.testmod() diff --git a/plugins/WebUi/lib/webpy022/request.py b/plugins/WebUi/lib/webpy022/request.py new file mode 100644 index 000000000..0826d822a --- /dev/null +++ b/plugins/WebUi/lib/webpy022/request.py @@ -0,0 +1,153 @@ +""" +Request Delegation +(from web.py) +""" + +__all__ = ["handle", "nomethod", "autodelegate", "webpyfunc", "run"] + +import sys, re, types, os.path, urllib + +import http, wsgi, utils, webapi +import webapi as web + +def handle(mapping, fvars=None): + """ + Call the appropriate function based on the url to function mapping in `mapping`. + If no module for the function is specified, look up the function in `fvars`. If + `fvars` is empty, using the caller's context. + + `mapping` should be a tuple of paired regular expressions with function name + substitutions. `handle` will import modules as necessary. + """ + for url, ofno in utils.group(mapping, 2): + if isinstance(ofno, tuple): + ofn, fna = ofno[0], list(ofno[1:]) + else: + ofn, fna = ofno, [] + fn, result = utils.re_subm('^' + url + '$', ofn, web.ctx.path) + if result: # it's a match + if fn.split(' ', 1)[0] == "redirect": + url = fn.split(' ', 1)[1] + if web.ctx.method == "GET": + x = web.ctx.env.get('QUERY_STRING', '') + if x: + url += '?' + x + return http.redirect(url) + elif '.' in fn: + x = fn.split('.') + mod, cls = '.'.join(x[:-1]), x[-1] + mod = __import__(mod, globals(), locals(), [""]) + cls = getattr(mod, cls) + else: + cls = fn + mod = fvars + if isinstance(mod, types.ModuleType): + mod = vars(mod) + try: + cls = mod[cls] + except KeyError: + return web.notfound() + + meth = web.ctx.method + if meth == "HEAD": + if not hasattr(cls, meth): + meth = "GET" + if not hasattr(cls, meth): + return nomethod(cls) + tocall = getattr(cls(), meth) + args = list(result.groups()) + for d in re.findall(r'\\(\d+)', ofn): + args.pop(int(d) - 1) + return tocall(*([x and urllib.unquote(x) for x in args] + fna)) + + return web.notfound() + +def nomethod(cls): + """Returns a `405 Method Not Allowed` error for `cls`.""" + web.ctx.status = '405 Method Not Allowed' + web.header('Content-Type', 'text/html') + web.header('Allow', \ + ', '.join([method for method in \ + ['GET', 'HEAD', 'POST', 'PUT', 'DELETE'] \ + if hasattr(cls, method)])) + + # commented out for the same reason redirect is + # return output('method not allowed') + +def autodelegate(prefix=''): + """ + Returns a method that takes one argument and calls the method named prefix+arg, + calling `notfound()` if there isn't one. Example: + + urls = ('/prefs/(.*)', 'prefs') + + class prefs: + GET = autodelegate('GET_') + def GET_password(self): pass + def GET_privacy(self): pass + + `GET_password` would get called for `/prefs/password` while `GET_privacy` for + `GET_privacy` gets called for `/prefs/privacy`. + + If a user visits `/prefs/password/change` then `GET_password(self, '/change')` + is called. + """ + def internal(self, arg): + if '/' in arg: + first, rest = arg.split('/', 1) + func = prefix + first + args = ['/' + rest] + else: + func = prefix + arg + args = [] + + if hasattr(self, func): + try: + return getattr(self, func)(*args) + except TypeError: + return web.notfound() + else: + return web.notfound() + return internal + +def webpyfunc(inp, fvars, autoreload=False): + """If `inp` is a url mapping, returns a function that calls handle.""" + if not hasattr(inp, '__call__'): + if autoreload: + def modname(): + """find name of the module name from fvars.""" + file, name = fvars['__file__'], fvars['__name__'] + if name == '__main__': + # Since the __main__ module can't be reloaded, the module has + # to be imported using its file name. + name = os.path.splitext(os.path.basename(file))[0] + return name + + mod = __import__(modname(), None, None, [""]) + #@@probably should replace this with some inspect magic + name = utils.dictfind(fvars, inp) + func = lambda: handle(getattr(mod, name), mod) + else: + func = lambda: handle(inp, fvars) + else: + func = inp + return func + +def run(inp, fvars, *middleware): + """ + Starts handling requests. If called in a CGI or FastCGI context, it will follow + that protocol. If called from the command line, it will start an HTTP + server on the port named in the first command line argument, or, if there + is no argument, on port 8080. + + `input` is a callable, then it's called with no arguments. + Otherwise, it's a `mapping` object to be passed to `handle(...)`. + + **Caveat:** So that `reloader` will work correctly, input has to be a variable, + it can't be a tuple passed in directly. + + `middleware` is a list of WSGI middleware which is applied to the resulting WSGI + function. + """ + autoreload = http.reloader in middleware + return wsgi.runwsgi(webapi.wsgifunc(webpyfunc(inp, fvars, autoreload), *middleware)) diff --git a/plugins/WebUi/lib/webpy022/template.py b/plugins/WebUi/lib/webpy022/template.py new file mode 100644 index 000000000..b21903401 --- /dev/null +++ b/plugins/WebUi/lib/webpy022/template.py @@ -0,0 +1,878 @@ +""" +simple, elegant templating +(part of web.py) +""" + +import re, glob, os, os.path +from types import FunctionType as function +from utils import storage, group, utf8 +from net import websafe + +# differences from python: +# - for: has an optional else: that gets called if the loop never runs +# differences to add: +# - you can use the expression inside if, while blocks +# - special for loop attributes, like django? +# - you can check to see if a variable is defined (perhaps w/ get func?) +# all these are probably good ideas for python... + +# todo: +# inline tuple +# relax constraints on spacing +# continue, break, etc. +# tracebacks + +global_globals = {'None':None, 'False':False, 'True': True} +MAX_ITERS = 100000 + +WHAT = 0 +ARGS = 4 +KWARGS = 6 +NAME = 2 +BODY = 4 +CLAUSE = 2 +ELIF = 6 +ELSE = 8 +IN = 6 +NAME = 2 +EXPR = 4 +FILTER = 4 +THING = 2 +ATTR = 4 +ITEM = 4 +NEGATE = 4 +X = 2 +OP = 4 +Y = 6 +LINENO = -1 + +# http://docs.python.org/ref/identifiers.html +r_var = '[a-zA-Z_][a-zA-Z0-9_]*' + +class ParseError(Exception): pass +class Parser: + def __init__(self, text, name=""): + self.t = text + self.p = 0 + self._lock = [False] + self.name = name + + def lock(self): + self._lock[-1] = True + + def curline(self): + return self.t[:self.p].count('\n')+1 + + def csome(self): + return repr(self.t[self.p:self.p+5]+'...') + + def Error(self, x, y=None): + if y is None: y = self.csome() + raise ParseError, "%s: expected %s, got %s (line %s)" % (self.name, x, y, self.curline()) + + def q(self, f): + def internal(*a, **kw): + checkp = self.p + self._lock.append(False) + try: + q = f(*a, **kw) + except ParseError: + if self._lock[-1]: + raise + self.p = checkp + self._lock.pop() + return False + self._lock.pop() + return q or True + return internal + + def tokr(self, t): + text = self.c(len(t)) + if text != t: + self.Error(repr(t), repr(text)) + return t + + def ltokr(self, *l): + for x in l: + o = self.tokq(x) + if o: return o + self.Error('one of '+repr(l)) + + def rer(self, r): + x = re.match(r, self.t[self.p:]) #@@re_compile + if not x: + self.Error('r'+repr(r)) + return self.tokr(x.group()) + + def endr(self): + if self.p != len(self.t): + self.Error('EOF') + + def c(self, n=1): + out = self.t[self.p:self.p+n] + if out == '' and n != 0: + self.Error('character', 'EOF') + self.p += n + return out + + def lookbehind(self, t): + return self.t[self.p-len(t):self.p] == t + + def __getattr__(self, a): + if a.endswith('q'): + return self.q(getattr(self, a[:-1]+'r')) + raise AttributeError, a + +class TemplateParser(Parser): + def __init__(self, *a, **kw): + Parser.__init__(self, *a, **kw) + self.curws = '' + self.curind = '' + + def o(self, *a): + return a+('lineno', self.curline()) + + def go(self): + # maybe try to do some traceback parsing/hacking + return self.gor() + + def gor(self): + header = self.defwithq() + results = self.lines(start=True) + self.endr() + return header, results + + def ws(self): + n = 0 + while self.tokq(" "): n += 1 + return " " * n + + def defwithr(self): + self.tokr('$def with ') + self.lock() + self.tokr('(') + args = [] + kw = [] + x = self.req(r_var) + while x: + if self.tokq('='): + v = self.exprr() + kw.append((x, v)) + else: + args.append(x) + x = self.tokq(', ') and self.req(r_var) + self.tokr(')\n') + return self.o('defwith', 'null', None, 'args', args, 'kwargs', kw) + + def literalr(self): + o = ( + self.req('"[^"]*"') or #@@ no support for escapes + self.req("'[^']*'") + ) + if o is False: + o = self.req('\-?[0-9]+(\.[0-9]*)?') + if o is not False: + if '.' in o: o = float(o) + else: o = int(o) + + if o is False: self.Error('literal') + return self.o('literal', 'thing', o) + + def listr(self): + self.tokr('[') + self.lock() + x = [] + if not self.tokq(']'): + while True: + t = self.exprr() + x.append(t) + if not self.tokq(', '): break + self.tokr(']') + return self.o('list', 'thing', x) + + def dictr(self): + self.tokr('{') + self.lock() + x = {} + if not self.tokq('}'): + while True: + k = self.exprr() + self.tokr(': ') + v = self.exprr() + x[k] = v + if not self.tokq(', '): break + self.tokr('}') + return self.o('dict', 'thing', x) + + def parenr(self): + self.tokr('(') + self.lock() + o = self.exprr() # todo: allow list + self.tokr(')') + return self.o('paren', 'thing', o) + + def atomr(self): + """returns var, literal, paren, dict, or list""" + o = ( + self.varq() or + self.parenq() or + self.dictq() or + self.listq() or + self.literalq() + ) + if o is False: self.Error('atom') + return o + + def primaryr(self): + """returns getattr, call, or getitem""" + n = self.atomr() + while 1: + if self.tokq('.'): + v = self.req(r_var) + if not v: + self.p -= 1 # get rid of the '.' + break + else: + n = self.o('getattr', 'thing', n, 'attr', v) + elif self.tokq('('): + args = [] + kw = [] + + while 1: + # need to see if we're doing a keyword argument + checkp = self.p + k = self.req(r_var) + if k and self.tokq('='): # yup + v = self.exprr() + kw.append((k, v)) + else: + self.p = checkp + x = self.exprq() + if x: # at least it's something + args.append(x) + else: + break + + if not self.tokq(', '): break + self.tokr(')') + n = self.o('call', 'thing', n, 'args', args, 'kwargs', kw) + elif self.tokq('['): + v = self.exprr() + self.tokr(']') + n = self.o('getitem', 'thing', n, 'item', v) + else: + break + + return n + + def exprr(self): + negate = self.tokq('not ') + x = self.primaryr() + if self.tokq(' '): + operator = self.ltokr('not in', 'in', 'is not', 'is', '==', '!=', '>=', '<=', '<', '>', 'and', 'or', '*', '+', '-', '/', '%') + self.tokr(' ') + y = self.exprr() + x = self.o('test', 'x', x, 'op', operator, 'y', y) + + return self.o('expr', 'thing', x, 'negate', negate) + + def varr(self): + return self.o('var', 'name', self.rer(r_var)) + + def liner(self): + out = [] + o = self.curws + while 1: + c = self.c() + self.lock() + if c == '\n': + self.p -= 1 + break + if c == '$': + if self.lookbehind('\\$'): + o = o[:-1] + c + else: + filter = not bool(self.tokq(':')) + + if self.tokq('{'): + out.append(o) + out.append(self.o('itpl', 'name', self.exprr(), 'filter', filter)) + self.tokr('}') + o = '' + else: + g = self.primaryq() + if g: + out.append(o) + out.append(self.o('itpl', 'name', g, 'filter', filter)) + o = '' + else: + o += c + else: + o += c + self.tokr('\n') + if not self.lookbehind('\\\n'): + o += '\n' + else: + o = o[:-1] + out.append(o) + return self.o('line', 'thing', out) + + def varsetr(self): + self.tokr('$var ') + self.lock() + what = self.rer(r_var) + self.tokr(':') + body = self.lines() + return self.o('varset', 'name', what, 'body', body) + + def ifr(self): + self.tokr("$if ") + self.lock() + expr = self.exprr() + self.tokr(":") + ifc = self.lines() + + elifs = [] + while self.tokq(self.curws + self.curind + '$elif '): + v = self.exprr() + self.tokr(':') + c = self.lines() + elifs.append(self.o('elif', 'clause', v, 'body', c)) + + if self.tokq(self.curws + self.curind + "$else:"): + elsec = self.lines() + else: + elsec = None + + return self.o('if', 'clause', expr, 'then', ifc, 'elif', elifs, 'else', elsec) + + def forr(self): + self.tokr("$for ") + self.lock() + v = self.setabler() + self.tokr(" in ") + g = self.exprr() + self.tokr(":") + l = self.lines() + + if self.tokq(self.curws + self.curind + '$else:'): + elsec = self.lines() + else: + elsec = None + + return self.o('for', 'name', v, 'body', l, 'in', g, 'else', elsec) + + def whiler(self): + self.tokr('$while ') + self.lock() + v = self.exprr() + self.tokr(":") + l = self.lines() + + if self.tokq(self.curws + self.curind + '$else:'): + elsec = self.lines() + else: + elsec = None + + return self.o('while', 'clause', v, 'body', l, 'null', None, 'else', elsec) + + def assignr(self): + self.tokr('$ ') + assign = self.rer(r_var) # NOTE: setable + self.tokr(' = ') + expr = self.exprr() + self.tokr('\n') + + return self.o('assign', 'name', assign, 'expr', expr) + + def commentr(self): + self.tokr('$#') + self.lock() + while self.c() != '\n': pass + return self.o('comment') + + def setabler(self): + out = [self.varr()] #@@ not quite right + while self.tokq(', '): + out.append(self.varr()) + return out + + def lines(self, start=False): + """ + This function gets called from two places: + 1. at the start, where it's matching the document itself + 2. after any command, where it matches one line or an indented block + """ + o = [] + if not start: # try to match just one line + singleline = self.tokq(' ') and self.lineq() + if singleline: + return [singleline] + else: + self.rer(' *') #@@slurp space? + self.tokr('\n') + oldind = self.curind + self.curind += ' ' + while 1: + oldws = self.curws + t = self.tokq(oldws + self.curind) + if not t: break + + self.curws += self.ws() + x = t and ( + self.varsetq() or + self.ifq() or + self.forq() or + self.whileq() or + self.assignq() or + self.commentq() or + self.lineq()) + self.curws = oldws + if not x: + break + elif x[WHAT] == 'comment': + pass + else: + o.append(x) + + if not start: self.curind = oldind + return o + +class Stowage(storage): + def __str__(self): return self.get('_str') + #@@ edits in place + def __add__(self, other): + if isinstance(other, (unicode, str)): + self._str += other + return self + else: + raise TypeError, 'cannot add' + def __radd__(self, other): + if isinstance(other, (unicode, str)): + self._str = other + self._str + return self + else: + raise TypeError, 'cannot add' + +class WTF(AssertionError): pass +class SecurityError(Exception): + """The template seems to be trying to do something naughty.""" + pass + + + + +Required = object() +class Template: + globals = {} + content_types = { + '.html' : 'text/html; charset=utf-8', + '.txt' : 'text/plain', + } + + def __init__(self, text, filter=None, filename=""): + self.filter = filter + self.filename = filename + # universal newlines: + text = text.replace('\r\n', '\n').replace('\r', '\n').expandtabs() + if not text.endswith('\n'): text += '\n' + header, tree = TemplateParser(text, filename).go() + self.tree = tree + if header: + self.h_defwith(header) + else: + self.args, self.kwargs = (), {} + + def __call__(self, *a, **kw): + d = self.globals.copy() + d.update(self._parseargs(a, kw)) + f = Fill(self.tree, d=d) + if self.filter: f.filter = self.filter + + import webapi as web + if 'headers' in web.ctx and self.filename: + content_type = self.find_content_type() + if content_type: + web.header('Content-Type', content_type, unique=True) + + return f.go() + + def find_content_type(self): + for ext, content_type in self.content_types.iteritems(): + if self.filename.endswith(ext): + return content_type + + def _parseargs(self, inargs, inkwargs): + # difference from Python: + # no error on setting a keyword arg twice + d = {} + for arg in self.args: + d[arg] = Required + for kw, val in self.kwargs: + d[kw] = val + + for n, val in enumerate(inargs): + if n < len(self.args): + d[self.args[n]] = val + elif n < len(self.args)+len(self.kwargs): + kw = self.kwargs[n - len(self.args)][0] + d[kw] = val + + for kw, val in inkwargs.iteritems(): + d[kw] = val + + unset = [] + for k, v in d.iteritems(): + if v is Required: + unset.append(k) + if unset: + raise TypeError, 'values for %s are required' % unset + + return d + + def h_defwith(self, header): + assert header[WHAT] == 'defwith' + f = Fill(self.tree, d={}) + + self.args = header[ARGS] + self.kwargs = [] + for var, valexpr in header[KWARGS]: + self.kwargs.append((var, f.h(valexpr))) + + def __repr__(self): + return "" % self.filename + +class Handle: + def __init__(self, parsetree, **kw): + self._funccache = {} + self.parsetree = parsetree + for (k, v) in kw.iteritems(): setattr(self, k, v) + + def h(self, item): + return getattr(self, 'h_' + item[WHAT])(item) + +class Fill(Handle): + builtins = global_globals + def filter(self, text): + if text is None: return '' + else: return utf8(text) + # often replaced with stuff like net.websafe + + def h_literal(self, i): + item = i[THING] + if isinstance(item, (unicode, str)) and item[0] in ['"', "'"]: + item = item[1:-1] + elif isinstance(item, (float, int)): + pass + return item + + def h_list(self, i): + x = i[THING] + out = [] + for item in x: + out.append(self.h(item)) + return out + + def h_dict(self, i): + x = i[THING] + out = {} + for k, v in x.iteritems(): + out[self.h(k)] = self.h(v) + return out + + def h_paren(self, i): + item = i[THING] + if isinstance(item, list): + raise NotImplementedError, 'tuples' + return self.h(item) + + def h_getattr(self, i): + thing, attr = i[THING], i[ATTR] + thing = self.h(thing) + if attr.startswith('_') or attr.startswith('func_') or attr.startswith('im_'): + raise SecurityError, 'tried to get ' + attr + try: + if thing in self.builtins: + raise SecurityError, 'tried to getattr on ' + repr(thing) + except TypeError: + pass # raised when testing an unhashable object + try: + return getattr(thing, attr) + except AttributeError: + if isinstance(thing, list) and attr == 'join': + return lambda s: s.join(thing) + else: + raise + + def h_call(self, i): + call = self.h(i[THING]) + args = [self.h(x) for x in i[ARGS]] + kw = dict([(x, self.h(y)) for (x, y) in i[KWARGS]]) + return call(*args, **kw) + + def h_getitem(self, i): + thing, item = i[THING], i[ITEM] + thing = self.h(thing) + item = self.h(item) + return thing[item] + + def h_expr(self, i): + item = self.h(i[THING]) + if i[NEGATE]: + item = not item + return item + + def h_test(self, item): + ox, op, oy = item[X], item[OP], item[Y] + # for short-circuiting to work, we can't eval these here + e = self.h + if op == 'is': + return e(ox) is e(oy) + elif op == 'is not': + return e(ox) is not e(oy) + elif op == 'in': + return e(ox) in e(oy) + elif op == 'not in': + return e(ox) not in e(oy) + elif op == '==': + return e(ox) == e(oy) + elif op == '!=': + return e(ox) != e(oy) + elif op == '>': + return e(ox) > e(oy) + elif op == '<': + return e(ox) < e(oy) + elif op == '<=': + return e(ox) <= e(oy) + elif op == '>=': + return e(ox) >= e(oy) + elif op == 'and': + return e(ox) and e(oy) + elif op == 'or': + return e(ox) or e(oy) + elif op == '+': + return e(ox) + e(oy) + elif op == '-': + return e(ox) - e(oy) + elif op == '*': + return e(ox) * e(oy) + elif op == '/': + return e(ox) / e(oy) + elif op == '%': + return e(ox) % e(oy) + else: + raise WTF, 'op ' + op + + def h_var(self, i): + v = i[NAME] + if v in self.d: + return self.d[v] + elif v in self.builtins: + return self.builtins[v] + elif v == 'self': + return self.output + else: + raise NameError, 'could not find %s (line %s)' % (repr(i[NAME]), i[LINENO]) + + def h_line(self, i): + out = [] + for x in i[THING]: + #@@ what if x is unicode + if isinstance(x, str): + out.append(x) + elif x[WHAT] == 'itpl': + o = self.h(x[NAME]) + if x[FILTER]: + o = self.filter(o) + else: + o = (o is not None and utf8(o)) or "" + out.append(o) + else: + raise WTF, x + return ''.join(out) + + def h_varset(self, i): + self.output[i[NAME]] = ''.join(self.h_lines(i[BODY])) + return '' + + def h_if(self, i): + expr = self.h(i[CLAUSE]) + if expr: + do = i[BODY] + else: + for e in i[ELIF]: + expr = self.h(e[CLAUSE]) + if expr: + do = e[BODY] + break + else: + do = i[ELSE] + return ''.join(self.h_lines(do)) + + def h_for(self, i): + out = [] + assert i[IN][WHAT] == 'expr' + invar = self.h(i[IN]) + forvar = i[NAME] + if invar: + for nv in invar: + if len(forvar) == 1: + fv = forvar[0] + assert fv[WHAT] == 'var' + self.d[fv[NAME]] = nv # same (lack of) scoping as Python + else: + for x, y in zip(forvar, nv): + assert x[WHAT] == 'var' + self.d[x[NAME]] = y + + out.extend(self.h_lines(i[BODY])) + else: + if i[ELSE]: + out.extend(self.h_lines(i[ELSE])) + return ''.join(out) + + def h_while(self, i): + out = [] + expr = self.h(i[CLAUSE]) + if not expr: + return ''.join(self.h_lines(i[ELSE])) + c = 0 + while expr: + c += 1 + if c >= MAX_ITERS: + raise RuntimeError, 'too many while-loop iterations (line %s)' % i[LINENO] + out.extend(self.h_lines(i[BODY])) + expr = self.h(i[CLAUSE]) + return ''.join(out) + + def h_assign(self, i): + self.d[i[NAME]] = self.h(i[EXPR]) + return '' + + def h_comment(self, i): pass + + def h_lines(self, lines): + if lines is None: return [] + return map(self.h, lines) + + def go(self): + self.output = Stowage() + self.output._str = ''.join(map(self.h, self.parsetree)) + if self.output.keys() == ['_str']: + self.output = self.output['_str'] + return self.output + +class render: + def __init__(self, loc='templates/', cache=True): + self.loc = loc + if cache: + self.cache = {} + else: + self.cache = False + + def _do(self, name, filter=None): + if self.cache is False or name not in self.cache: + + tmplpath = os.path.join(self.loc, name) + p = [f for f in glob.glob(tmplpath + '.*') if not f.endswith('~')] # skip backup files + if not p and os.path.isdir(tmplpath): + return render(tmplpath, cache=self.cache) + elif not p: + raise AttributeError, 'no template named ' + name + + p = p[0] + c = Template(open(p).read(), filename=p) + if self.cache is not False: self.cache[name] = (p, c) + + if self.cache is not False: p, c = self.cache[name] + + if p.endswith('.html') or p.endswith('.xml'): + if not filter: c.filter = websafe + return c + + def __getattr__(self, p): + return self._do(p) + +def frender(fn, *a, **kw): + return Template(open(fn).read(), *a, **kw) + +def test(): + import sys + verbose = '-v' in sys.argv + def assertEqual(a, b): + if a == b: + if verbose: + sys.stderr.write('.') + sys.stderr.flush() + else: + assert a == b, "\nexpected: %s\ngot: %s" % (repr(b), repr(a)) + + from utils import storage, group + + class t: + def __init__(self, text): + self.text = text + + def __call__(self, *a, **kw): + return TestResult(self.text, Template(self.text)(*a, **kw)) + + class TestResult: + def __init__(self, source, value): + self.source = source + self.value = value + + def __eq__(self, other): + if self.value == other: + if verbose: + sys.stderr.write('.') + else: + print >> sys.stderr, 'FAIL:', repr(self.source), 'expected', repr(other), ', got', repr(self.value) + sys.stderr.flush() + + t('1')() == '1\n' + t('$def with ()\n1')() == '1\n' + t('$def with (a)\n$a')(1) == '1\n' + t('$def with (a=0)\n$a')(1) == '1\n' + t('$def with (a=0)\n$a')(a=1) == '1\n' + t('$if 1: 1')() == '1\n' + t('$if 1:\n 1')() == '1\n' + t('$if 0: 0\n$elif 1: 1')() == '1\n' + t('$if 0: 0\n$elif None: 0\n$else: 1')() == '1\n' + t('$if (0 < 1) and (1 < 2): 1')() == '1\n' + t('$for x in [1, 2, 3]: $x')() == '1\n2\n3\n' + t('$for x in []: 0\n$else: 1')() == '1\n' + t('$def with (a)\n$while a and a.pop(): 1')([1, 2, 3]) == '1\n1\n1\n' + t('$while 0: 0\n$else: 1')() == '1\n' + t('$ a = 1\n$a')() == '1\n' + t('$# 0')() == '' + t('$def with (d)\n$for k, v in d.iteritems(): $k')({1: 1}) == '1\n' + t('$def with (a)\n$(a)')(1) == '1\n' + t('$def with (a)\n$a')(1) == '1\n' + t('$def with (a)\n$a.b')(storage(b=1)) == '1\n' + t('$def with (a)\n$a[0]')([1]) == '1\n' + t('${0 or 1}')() == '1\n' + t('$ a = [1]\n$a[0]')() == '1\n' + t('$ a = {1: 1}\n$a.keys()[0]')() == '1\n' + t('$ a = []\n$if not a: 1')() == '1\n' + t('$ a = {}\n$if not a: 1')() == '1\n' + t('$ a = -1\n$a')() == '-1\n' + t('$ a = "1"\n$a')() == '1\n' + t('$if 1 is 1: 1')() == '1\n' + t('$if not 0: 1')() == '1\n' + t('$if 1:\n $if 1: 1')() == '1\n' + t('$ a = 1\n$a')() == '1\n' + t('$ a = 1.\n$a')() == '1.0\n' + t('$({1: 1}.keys()[0])')() == '1\n' + t('$for x in [1, 2, 3]:\n\t$x')() == ' 1\n 2\n 3\n' + t('$def with (a)\n$:a')(1) == '1\n' + t('$def with (a)\n$a')(u'\u203d') == '\xe2\x80\xbd\n' + t(u'$def with (f)\n$:f("x")')(lambda x: x) == 'x\n' + + j = Template("$var foo: bar")() + assertEqual(str(j), '') + assertEqual(j.foo, 'bar\n') + if verbose: sys.stderr.write('\n') + + +if __name__ == "__main__": + test() diff --git a/plugins/WebUi/lib/webpy022/utils.py b/plugins/WebUi/lib/webpy022/utils.py new file mode 100644 index 000000000..5b6187583 --- /dev/null +++ b/plugins/WebUi/lib/webpy022/utils.py @@ -0,0 +1,796 @@ +""" +General Utilities +(part of web.py) +""" + +__all__ = [ + "Storage", "storage", "storify", + "iters", + "rstrips", "lstrips", "strips", "utf8", + "TimeoutError", "timelimit", + "Memoize", "memoize", + "re_compile", "re_subm", + "group", + "IterBetter", "iterbetter", + "dictreverse", "dictfind", "dictfindall", "dictincr", "dictadd", + "listget", "intget", "datestr", + "numify", "denumify", "dateify", + "CaptureStdout", "capturestdout", "Profile", "profile", + "tryall", + "ThreadedDict", + "autoassign", + "to36", + "safemarkdown" +] + +import re, sys, time, threading +try: import datetime +except ImportError: pass + +class Storage(dict): + """ + A Storage object is like a dictionary except `obj.foo` can be used + in addition to `obj['foo']`. + + >>> o = storage(a=1) + >>> o.a + 1 + >>> o['a'] + 1 + >>> o.a = 2 + >>> o['a'] + 2 + >>> del o.a + >>> o.a + Traceback (most recent call last): + ... + AttributeError: 'a' + + """ + def __getattr__(self, key): + try: + return self[key] + except KeyError, k: + raise AttributeError, k + + def __setattr__(self, key, value): + self[key] = value + + def __delattr__(self, key): + try: + del self[key] + except KeyError, k: + raise AttributeError, k + + def __repr__(self): + return '' + +storage = Storage + +def storify(mapping, *requireds, **defaults): + """ + Creates a `storage` object from dictionary `mapping`, raising `KeyError` if + d doesn't have all of the keys in `requireds` and using the default + values for keys found in `defaults`. + + For example, `storify({'a':1, 'c':3}, b=2, c=0)` will return the equivalent of + `storage({'a':1, 'b':2, 'c':3})`. + + If a `storify` value is a list (e.g. multiple values in a form submission), + `storify` returns the last element of the list, unless the key appears in + `defaults` as a list. Thus: + + >>> storify({'a':[1, 2]}).a + 2 + >>> storify({'a':[1, 2]}, a=[]).a + [1, 2] + >>> storify({'a':1}, a=[]).a + [1] + >>> storify({}, a=[]).a + [] + + Similarly, if the value has a `value` attribute, `storify will return _its_ + value, unless the key appears in `defaults` as a dictionary. + + >>> storify({'a':storage(value=1)}).a + 1 + >>> storify({'a':storage(value=1)}, a={}).a + + >>> storify({}, a={}).a + {} + + """ + def getvalue(x): + if hasattr(x, 'value'): + return x.value + else: + return x + + stor = Storage() + for key in requireds + tuple(mapping.keys()): + value = mapping[key] + if isinstance(value, list): + if isinstance(defaults.get(key), list): + value = [getvalue(x) for x in value] + else: + value = value[-1] + if not isinstance(defaults.get(key), dict): + value = getvalue(value) + if isinstance(defaults.get(key), list) and not isinstance(value, list): + value = [value] + setattr(stor, key, value) + + for (key, value) in defaults.iteritems(): + result = value + if hasattr(stor, key): + result = stor[key] + if value == () and not isinstance(result, tuple): + result = (result,) + setattr(stor, key, result) + + return stor + +iters = [list, tuple] +import __builtin__ +if hasattr(__builtin__, 'set'): + iters.append(set) +try: + from sets import Set + iters.append(Set) +except ImportError: + pass + +class _hack(tuple): pass +iters = _hack(iters) +iters.__doc__ = """ +A list of iterable items (like lists, but not strings). Includes whichever +of lists, tuples, sets, and Sets are available in this version of Python. +""" + +def _strips(direction, text, remove): + if direction == 'l': + if text.startswith(remove): + return text[len(remove):] + elif direction == 'r': + if text.endswith(remove): + return text[:-len(remove)] + else: + raise ValueError, "Direction needs to be r or l." + return text + +def rstrips(text, remove): + """ + removes the string `remove` from the right of `text` + + >>> rstrips("foobar", "bar") + 'foo' + + """ + return _strips('r', text, remove) + +def lstrips(text, remove): + """ + removes the string `remove` from the left of `text` + + >>> lstrips("foobar", "foo") + 'bar' + + """ + return _strips('l', text, remove) + +def strips(text, remove): + """removes the string `remove` from the both sides of `text` + + >>> strips("foobarfoo", "foo") + 'bar' + + """ + return rstrips(lstrips(text, remove), remove) + +def utf8(text): + """Encodes text in utf-8. + + >> utf8(u'\u1234') # doctest doesn't seem to like utf-8 + '\xe1\x88\xb4' + + >>> utf8('hello') + 'hello' + >>> utf8(42) + '42' + """ + if isinstance(text, unicode): + return text.encode('utf-8') + elif isinstance(text, str): + return text + else: + return str(text) + +class TimeoutError(Exception): pass +def timelimit(timeout): + """ + A decorator to limit a function to `timeout` seconds, raising `TimeoutError` + if it takes longer. + + >>> import time + >>> def meaningoflife(): + ... time.sleep(.2) + ... return 42 + >>> + >>> timelimit(.1)(meaningoflife)() + Traceback (most recent call last): + ... + TimeoutError: took too long + >>> timelimit(1)(meaningoflife)() + 42 + + _Caveat:_ The function isn't stopped after `timeout` seconds but continues + executing in a separate thread. (There seems to be no way to kill a thread.) + + inspired by + """ + def _1(function): + def _2(*args, **kw): + class Dispatch(threading.Thread): + def __init__(self): + threading.Thread.__init__(self) + self.result = None + self.error = None + + self.setDaemon(True) + self.start() + + def run(self): + try: + self.result = function(*args, **kw) + except: + self.error = sys.exc_info() + + c = Dispatch() + c.join(timeout) + if c.isAlive(): + raise TimeoutError, 'took too long' + if c.error: + raise c.error[0], c.error[1] + return c.result + return _2 + return _1 + +class Memoize: + """ + 'Memoizes' a function, caching its return values for each input. + + >>> import time + >>> def meaningoflife(): + ... time.sleep(.2) + ... return 42 + >>> fastlife = memoize(meaningoflife) + >>> meaningoflife() + 42 + >>> timelimit(.1)(meaningoflife)() + Traceback (most recent call last): + ... + TimeoutError: took too long + >>> fastlife() + 42 + >>> timelimit(.1)(fastlife)() + 42 + + """ + def __init__(self, func): + self.func = func + self.cache = {} + def __call__(self, *args, **keywords): + key = (args, tuple(keywords.items())) + if key not in self.cache: + self.cache[key] = self.func(*args, **keywords) + return self.cache[key] + +memoize = Memoize + +re_compile = memoize(re.compile) #@@ threadsafe? +re_compile.__doc__ = """ +A memoized version of re.compile. +""" + +class _re_subm_proxy: + def __init__(self): + self.match = None + def __call__(self, match): + self.match = match + return '' + +def re_subm(pat, repl, string): + """ + Like re.sub, but returns the replacement _and_ the match object. + + >>> t, m = re_subm('g(oo+)fball', r'f\\1lish', 'goooooofball') + >>> t + 'foooooolish' + >>> m.groups() + ('oooooo',) + """ + compiled_pat = re_compile(pat) + proxy = _re_subm_proxy() + compiled_pat.sub(proxy.__call__, string) + return compiled_pat.sub(repl, string), proxy.match + +def group(seq, size): + """ + Returns an iterator over a series of lists of length size from iterable. + + >>> list(group([1,2,3,4], 2)) + [[1, 2], [3, 4]] + """ + if not hasattr(seq, 'next'): + seq = iter(seq) + while True: + yield [seq.next() for i in xrange(size)] + +class IterBetter: + """ + Returns an object that can be used as an iterator + but can also be used via __getitem__ (although it + cannot go backwards -- that is, you cannot request + `iterbetter[0]` after requesting `iterbetter[1]`). + + >>> import itertools + >>> c = iterbetter(itertools.count()) + >>> c[1] + 1 + >>> c[5] + 5 + >>> c[3] + Traceback (most recent call last): + ... + IndexError: already passed 3 + """ + def __init__(self, iterator): + self.i, self.c = iterator, 0 + def __iter__(self): + while 1: + yield self.i.next() + self.c += 1 + def __getitem__(self, i): + #todo: slices + if i < self.c: + raise IndexError, "already passed "+str(i) + try: + while i > self.c: + self.i.next() + self.c += 1 + # now self.c == i + self.c += 1 + return self.i.next() + except StopIteration: + raise IndexError, str(i) +iterbetter = IterBetter + +def dictreverse(mapping): + """ + >>> dictreverse({1: 2, 3: 4}) + {2: 1, 4: 3} + """ + return dict([(value, key) for (key, value) in mapping.iteritems()]) + +def dictfind(dictionary, element): + """ + Returns a key whose value in `dictionary` is `element` + or, if none exists, None. + + >>> d = {1:2, 3:4} + >>> dictfind(d, 4) + 3 + >>> dictfind(d, 5) + """ + for (key, value) in dictionary.iteritems(): + if element is value: + return key + +def dictfindall(dictionary, element): + """ + Returns the keys whose values in `dictionary` are `element` + or, if none exists, []. + + >>> d = {1:4, 3:4} + >>> dictfindall(d, 4) + [1, 3] + >>> dictfindall(d, 5) + [] + """ + res = [] + for (key, value) in dictionary.iteritems(): + if element is value: + res.append(key) + return res + +def dictincr(dictionary, element): + """ + Increments `element` in `dictionary`, + setting it to one if it doesn't exist. + + >>> d = {1:2, 3:4} + >>> dictincr(d, 1) + 3 + >>> d[1] + 3 + >>> dictincr(d, 5) + 1 + >>> d[5] + 1 + """ + dictionary.setdefault(element, 0) + dictionary[element] += 1 + return dictionary[element] + +def dictadd(*dicts): + """ + Returns a dictionary consisting of the keys in the argument dictionaries. + If they share a key, the value from the last argument is used. + + >>> dictadd({1: 0, 2: 0}, {2: 1, 3: 1}) + {1: 0, 2: 1, 3: 1} + """ + result = {} + for dct in dicts: + result.update(dct) + return result + +def listget(lst, ind, default=None): + """ + Returns `lst[ind]` if it exists, `default` otherwise. + + >>> listget(['a'], 0) + 'a' + >>> listget(['a'], 1) + >>> listget(['a'], 1, 'b') + 'b' + """ + if len(lst)-1 < ind: + return default + return lst[ind] + +def intget(integer, default=None): + """ + Returns `integer` as an int or `default` if it can't. + + >>> intget('3') + 3 + >>> intget('3a') + >>> intget('3a', 0) + 0 + """ + try: + return int(integer) + except (TypeError, ValueError): + return default + +def datestr(then, now=None): + """ + Converts a (UTC) datetime object to a nice string representation. + + >>> from datetime import datetime, timedelta + >>> d = datetime(1970, 5, 1) + >>> datestr(d, now=d) + '0 microseconds ago' + >>> for t, v in { + ... timedelta(microseconds=1): '1 microsecond ago', + ... timedelta(microseconds=2): '2 microseconds ago', + ... -timedelta(microseconds=1): '1 microsecond from now', + ... -timedelta(microseconds=2): '2 microseconds from now', + ... timedelta(microseconds=2000): '2 milliseconds ago', + ... timedelta(seconds=2): '2 seconds ago', + ... timedelta(seconds=2*60): '2 minutes ago', + ... timedelta(seconds=2*60*60): '2 hours ago', + ... timedelta(days=2): '2 days ago', + ... }.iteritems(): + ... assert datestr(d, now=d+t) == v + >>> datestr(datetime(1970, 1, 1), now=d) + 'January 1' + >>> datestr(datetime(1969, 1, 1), now=d) + 'January 1, 1969' + >>> datestr(datetime(1970, 6, 1), now=d) + 'June 1, 1970' + """ + def agohence(n, what, divisor=None): + if divisor: n = n // divisor + + out = str(abs(n)) + ' ' + what # '2 day' + if abs(n) != 1: out += 's' # '2 days' + out += ' ' # '2 days ' + if n < 0: + out += 'from now' + else: + out += 'ago' + return out # '2 days ago' + + oneday = 24 * 60 * 60 + + if not now: now = datetime.datetime.utcnow() + if type(now).__name__ == "DateTime": + now = datetime.datetime.fromtimestamp(now) + if type(then).__name__ == "DateTime": + then = datetime.datetime.fromtimestamp(then) + delta = now - then + deltaseconds = int(delta.days * oneday + delta.seconds + delta.microseconds * 1e-06) + deltadays = abs(deltaseconds) // oneday + if deltaseconds < 0: deltadays *= -1 # fix for oddity of floor + + if deltadays: + if abs(deltadays) < 4: + return agohence(deltadays, 'day') + + out = then.strftime('%B %e') # e.g. 'June 13' + if then.year != now.year or deltadays < 0: + out += ', %s' % then.year + return out + + if int(deltaseconds): + if abs(deltaseconds) > (60 * 60): + return agohence(deltaseconds, 'hour', 60 * 60) + elif abs(deltaseconds) > 60: + return agohence(deltaseconds, 'minute', 60) + else: + return agohence(deltaseconds, 'second') + + deltamicroseconds = delta.microseconds + if delta.days: deltamicroseconds = int(delta.microseconds - 1e6) # datetime oddity + if abs(deltamicroseconds) > 1000: + return agohence(deltamicroseconds, 'millisecond', 1000) + + return agohence(deltamicroseconds, 'microsecond') + +def numify(string): + """ + Removes all non-digit characters from `string`. + + >>> numify('800-555-1212') + '8005551212' + >>> numify('800.555.1212') + '8005551212' + + """ + return ''.join([c for c in str(string) if c.isdigit()]) + +def denumify(string, pattern): + """ + Formats `string` according to `pattern`, where the letter X gets replaced + by characters from `string`. + + >>> denumify("8005551212", "(XXX) XXX-XXXX") + '(800) 555-1212' + + """ + out = [] + for c in pattern: + if c == "X": + out.append(string[0]) + string = string[1:] + else: + out.append(c) + return ''.join(out) + +def dateify(datestring): + """ + Formats a numified `datestring` properly. + """ + return denumify(datestring, "XXXX-XX-XX XX:XX:XX") + +class CaptureStdout: + """ + Captures everything `func` prints to stdout and returns it instead. + + >>> def idiot(): + ... print "foo" + >>> capturestdout(idiot)() + 'foo\\n' + + **WARNING:** Not threadsafe! + """ + def __init__(self, func): + self.func = func + def __call__(self, *args, **keywords): + from cStringIO import StringIO + # Not threadsafe! + out = StringIO() + oldstdout = sys.stdout + sys.stdout = out + try: + self.func(*args, **keywords) + finally: + sys.stdout = oldstdout + return out.getvalue() + +capturestdout = CaptureStdout + +class Profile: + """ + Profiles `func` and returns a tuple containing its output + and a string with human-readable profiling information. + + >>> import time + >>> out, inf = profile(time.sleep)(.001) + >>> out + >>> inf[:10].strip() + 'took 0.0' + """ + def __init__(self, func): + self.func = func + def __call__(self, *args): ##, **kw): kw unused + import hotshot, hotshot.stats, tempfile ##, time already imported + temp = tempfile.NamedTemporaryFile() + prof = hotshot.Profile(temp.name) + + stime = time.time() + result = prof.runcall(self.func, *args) + stime = time.time() - stime + + prof.close() + stats = hotshot.stats.load(temp.name) + stats.strip_dirs() + stats.sort_stats('time', 'calls') + x = '\n\ntook '+ str(stime) + ' seconds\n' + x += capturestdout(stats.print_stats)(40) + x += capturestdout(stats.print_callers)() + return result, x + +profile = Profile + + +import traceback +# hack for compatibility with Python 2.3: +if not hasattr(traceback, 'format_exc'): + from cStringIO import StringIO + def format_exc(limit=None): + strbuf = StringIO() + traceback.print_exc(limit, strbuf) + return strbuf.getvalue() + traceback.format_exc = format_exc + +def tryall(context, prefix=None): + """ + Tries a series of functions and prints their results. + `context` is a dictionary mapping names to values; + the value will only be tried if it's callable. + + >>> tryall(dict(j=lambda: True)) + j: True + ---------------------------------------- + results: + True: 1 + + For example, you might have a file `test/stuff.py` + with a series of functions testing various things in it. + At the bottom, have a line: + + if __name__ == "__main__": tryall(globals()) + + Then you can run `python test/stuff.py` and get the results of + all the tests. + """ + context = context.copy() # vars() would update + results = {} + for (key, value) in context.iteritems(): + if not hasattr(value, '__call__'): + continue + if prefix and not key.startswith(prefix): + continue + print key + ':', + try: + r = value() + dictincr(results, r) + print r + except: + print 'ERROR' + dictincr(results, 'ERROR') + print ' ' + '\n '.join(traceback.format_exc().split('\n')) + + print '-'*40 + print 'results:' + for (key, value) in results.iteritems(): + print ' '*2, str(key)+':', value + +class ThreadedDict: + """ + Takes a dictionary that maps threads to objects. + When a thread tries to get or set an attribute or item + of the threadeddict, it passes it on to the object + for that thread in dictionary. + """ + def __init__(self, dictionary): + self.__dict__['_ThreadedDict__d'] = dictionary + + def __getattr__(self, attr): + return getattr(self.__d[threading.currentThread()], attr) + + def __getitem__(self, item): + return self.__d[threading.currentThread()][item] + + def __setattr__(self, attr, value): + if attr == '__doc__': + self.__dict__[attr] = value + else: + return setattr(self.__d[threading.currentThread()], attr, value) + + def __delattr__(self, item): + try: + del self.__d[threading.currentThread()][item] + except KeyError, k: + raise AttributeError, k + + def __delitem__(self, item): + del self.__d[threading.currentThread()][item] + + def __setitem__(self, item, value): + self.__d[threading.currentThread()][item] = value + + def __hash__(self): + return hash(self.__d[threading.currentThread()]) + +threadeddict = ThreadedDict + +def autoassign(self, locals): + """ + Automatically assigns local variables to `self`. + + >>> self = storage() + >>> autoassign(self, dict(a=1, b=2)) + >>> self + + + Generally used in `__init__` methods, as in: + + def __init__(self, foo, bar, baz=1): autoassign(self, locals()) + """ + for (key, value) in locals.iteritems(): + if key == 'self': + continue + setattr(self, key, value) + +def to36(q): + """ + Converts an integer to base 36 (a useful scheme for human-sayable IDs). + + >>> to36(35) + 'z' + >>> to36(119292) + '2k1o' + >>> int(to36(939387374), 36) + 939387374 + >>> to36(0) + '0' + >>> to36(-393) + Traceback (most recent call last): + ... + ValueError: must supply a positive integer + + """ + if q < 0: raise ValueError, "must supply a positive integer" + letters = "0123456789abcdefghijklmnopqrstuvwxyz" + converted = [] + while q != 0: + q, r = divmod(q, 36) + converted.insert(0, letters[r]) + return "".join(converted) or '0' + + +r_url = re_compile('(?', text) + text = markdown(text) + return text + + +if __name__ == "__main__": + import doctest + doctest.testmod() diff --git a/plugins/WebUi/lib/webpy022/webapi.py b/plugins/WebUi/lib/webpy022/webapi.py new file mode 100644 index 000000000..39d3be873 --- /dev/null +++ b/plugins/WebUi/lib/webpy022/webapi.py @@ -0,0 +1,369 @@ +""" +Web API (wrapper around WSGI) +(from web.py) +""" + +__all__ = [ + "config", + "badrequest", "notfound", "gone", "internalerror", + "header", "output", "flush", "debug", + "input", "data", + "setcookie", "cookies", + "ctx", + "loadhooks", "load", "unloadhooks", "unload", "_loadhooks", + "wsgifunc" +] + +import sys, os, cgi, threading, Cookie, pprint, traceback +try: import itertools +except ImportError: pass +from utils import storage, storify, threadeddict, dictadd, intget, lstrips, utf8 + +config = storage() +config.__doc__ = """ +A configuration object for various aspects of web.py. + +`db_parameters` + : A dictionary containing the parameters to be passed to `connect` + when `load()` is called. +`db_printing` + : Set to `True` if you would like SQL queries and timings to be + printed to the debug output. + +""" + +def badrequest(): + """Return a `400 Bad Request` error.""" + ctx.status = '400 Bad Request' + header('Content-Type', 'text/html') + return output('bad request') + +def notfound(): + """Returns a `404 Not Found` error.""" + ctx.status = '404 Not Found' + header('Content-Type', 'text/html') + return output('not found') + +def gone(): + """Returns a `410 Gone` error.""" + ctx.status = '410 Gone' + header('Content-Type', 'text/html') + return output("gone") + +def internalerror(): + """Returns a `500 Internal Server` error.""" + ctx.status = "500 Internal Server Error" + ctx.headers = [('Content-Type', 'text/html')] + ctx.output = "internal server error" + +def header(hdr, value, unique=False): + """ + Adds the header `hdr: value` with the response. + + If `unique` is True and a header with that name already exists, + it doesn't add a new one. + """ + hdr, value = utf8(hdr), utf8(value) + # protection against HTTP response splitting attack + if '\n' in hdr or '\r' in hdr or '\n' in value or '\r' in value: + raise ValueError, 'invalid characters in header' + + if unique is True: + for h, v in ctx.headers: + if h.lower() == hdr.lower(): return + + ctx.headers.append((hdr, value)) + +def output(string_): + """Appends `string_` to the response.""" + if isinstance(string_, unicode): string_ = string_.encode('utf8') + if ctx.get('flush'): + ctx._write(string_) + else: + ctx.output += str(string_) + +def flush(): + ctx.flush = True + return flush + +def input(*requireds, **defaults): + """ + Returns a `storage` object with the GET and POST arguments. + See `storify` for how `requireds` and `defaults` work. + """ + from cStringIO import StringIO + def dictify(fs): return dict([(k, fs[k]) for k in fs.keys()]) + + _method = defaults.pop('_method', 'both') + + e = ctx.env.copy() + a = b = {} + + if _method.lower() in ['both', 'post']: + if e['REQUEST_METHOD'] == 'POST': + a = cgi.FieldStorage(fp = StringIO(data()), environ=e, + keep_blank_values=1) + a = dictify(a) + + if _method.lower() in ['both', 'get']: + e['REQUEST_METHOD'] = 'GET' + b = dictify(cgi.FieldStorage(environ=e, keep_blank_values=1)) + + out = dictadd(b, a) + try: + return storify(out, *requireds, **defaults) + except KeyError: + badrequest() + raise StopIteration + +def data(): + """Returns the data sent with the request.""" + if 'data' not in ctx: + cl = intget(ctx.env.get('CONTENT_LENGTH'), 0) + ctx.data = ctx.env['wsgi.input'].read(cl) + return ctx.data + +def setcookie(name, value, expires="", domain=None): + """Sets a cookie.""" + if expires < 0: + expires = -1000000000 + kargs = {'expires': expires, 'path':'/'} + if domain: + kargs['domain'] = domain + # @@ should we limit cookies to a different path? + cookie = Cookie.SimpleCookie() + cookie[name] = value + for key, val in kargs.iteritems(): + cookie[name][key] = val + header('Set-Cookie', cookie.items()[0][1].OutputString()) + +def cookies(*requireds, **defaults): + """ + Returns a `storage` object with all the cookies in it. + See `storify` for how `requireds` and `defaults` work. + """ + cookie = Cookie.SimpleCookie() + cookie.load(ctx.env.get('HTTP_COOKIE', '')) + try: + return storify(cookie, *requireds, **defaults) + except KeyError: + badrequest() + raise StopIteration + +def debug(*args): + """ + Prints a prettyprinted version of `args` to stderr. + """ + try: + out = ctx.environ['wsgi.errors'] + except: + out = sys.stderr + for arg in args: + print >> out, pprint.pformat(arg) + return '' + +def _debugwrite(x): + try: + out = ctx.environ['wsgi.errors'] + except: + out = sys.stderr + out.write(x) +debug.write = _debugwrite + +class _outputter: + """Wraps `sys.stdout` so that print statements go into the response.""" + def __init__(self, file): self.file = file + def write(self, string_): + if hasattr(ctx, 'output'): + return output(string_) + else: + self.file.write(string_) + def __getattr__(self, attr): return getattr(self.file, attr) + def __getitem__(self, item): return self.file[item] + +def _capturedstdout(): + sysstd = sys.stdout + while hasattr(sysstd, 'file'): + if isinstance(sys.stdout, _outputter): return True + sysstd = sysstd.file + if isinstance(sys.stdout, _outputter): return True + return False + +if not _capturedstdout(): + sys.stdout = _outputter(sys.stdout) + +_context = {threading.currentThread(): storage()} +ctx = context = threadeddict(_context) + +ctx.__doc__ = """ +A `storage` object containing various information about the request: + +`environ` (aka `env`) + : A dictionary containing the standard WSGI environment variables. + +`host` + : The domain (`Host` header) requested by the user. + +`home` + : The base path for the application. + +`ip` + : The IP address of the requester. + +`method` + : The HTTP method used. + +`path` + : The path request. + +`query` + : If there are no query arguments, the empty string. Otherwise, a `?` followed + by the query string. + +`fullpath` + : The full path requested, including query arguments (`== path + query`). + +### Response Data + +`status` (default: "200 OK") + : The status code to be used in the response. + +`headers` + : A list of 2-tuples to be used in the response. + +`output` + : A string to be used as the response. +""" + +loadhooks = {} +_loadhooks = {} + +def load(): + """ + Loads a new context for the thread. + + You can ask for a function to be run at loadtime by + adding it to the dictionary `loadhooks`. + """ + _context[threading.currentThread()] = storage() + ctx.status = '200 OK' + ctx.headers = [] + if config.get('db_parameters'): + import db + db.connect(**config.db_parameters) + + for x in loadhooks.values(): x() + +def _load(env): + load() + ctx.output = '' + ctx.environ = ctx.env = env + ctx.host = env.get('HTTP_HOST') + ctx.homedomain = 'http://' + env.get('HTTP_HOST', '[unknown]') + ctx.homepath = os.environ.get('REAL_SCRIPT_NAME', env.get('SCRIPT_NAME', '')) + ctx.home = ctx.homedomain + ctx.homepath + ctx.ip = env.get('REMOTE_ADDR') + ctx.method = env.get('REQUEST_METHOD') + ctx.path = env.get('PATH_INFO') + # http://trac.lighttpd.net/trac/ticket/406 requires: + if env.get('SERVER_SOFTWARE', '').startswith('lighttpd/'): + ctx.path = lstrips(env.get('REQUEST_URI').split('?')[0], + os.environ.get('REAL_SCRIPT_NAME', env.get('SCRIPT_NAME', ''))) + + if env.get('QUERY_STRING'): + ctx.query = '?' + env.get('QUERY_STRING', '') + else: + ctx.query = '' + + ctx.fullpath = ctx.path + ctx.query + for x in _loadhooks.values(): x() + +unloadhooks = {} + +def unload(): + """ + Unloads the context for the thread. + + You can ask for a function to be run at loadtime by + adding it ot the dictionary `unloadhooks`. + """ + for x in unloadhooks.values(): x() + # ensures db cursors and such are GCed promptly + del _context[threading.currentThread()] + +def _unload(): + unload() + +def wsgifunc(func, *middleware): + """Returns a WSGI-compatible function from a webpy-function.""" + middleware = list(middleware) + + def wsgifunc(env, start_resp): + _load(env) + try: + result = func() + except StopIteration: + result = None + except: + print >> debug, traceback.format_exc() + result = internalerror() + + is_generator = result and hasattr(result, 'next') + if is_generator: + # wsgi requires the headers first + # so we need to do an iteration + # and save the result for later + try: + firstchunk = result.next() + except StopIteration: + firstchunk = '' + + status, headers, output = ctx.status, ctx.headers, ctx.output + ctx._write = start_resp(status, headers) + + # and now, the fun: + + def cleanup(): + # we insert this little generator + # at the end of our itertools.chain + # so that it unloads the request + # when everything else is done + + yield '' # force it to be a generator + _unload() + + # result is the output of calling the webpy function + # it could be a generator... + + if is_generator: + if firstchunk is flush: + # oh, it's just our special flush mode + # ctx._write is set up, so just continue execution + try: + result.next() + except StopIteration: + pass + + _unload() + return [] + else: + return itertools.chain([firstchunk], result, cleanup()) + + # ... but it's usually just None + # + # output is the stuff in ctx.output + # it's usually a string... + if isinstance(output, str): #@@ other stringlikes? + _unload() + return [output] + # it could be a generator... + elif hasattr(output, 'next'): + return itertools.chain(output, cleanup()) + else: + _unload() + raise Exception, "Invalid ctx.output" + + for mw_func in middleware: + wsgifunc = mw_func(wsgifunc) + + return wsgifunc diff --git a/plugins/WebUi/lib/webpy022/wsgi.py b/plugins/WebUi/lib/webpy022/wsgi.py new file mode 100644 index 000000000..26abf9291 --- /dev/null +++ b/plugins/WebUi/lib/webpy022/wsgi.py @@ -0,0 +1,54 @@ +""" +WSGI Utilities +(from web.py) +""" + +import os, sys + +import http +import webapi as web +from utils import listget +from net import validaddr, validip +import httpserver + +def runfcgi(func, addr=('localhost', 8000)): + """Runs a WSGI function as a FastCGI server.""" + import flup.server.fcgi as flups + return flups.WSGIServer(func, multiplexed=True, bindAddress=addr).run() + +def runscgi(func, addr=('localhost', 4000)): + """Runs a WSGI function as an SCGI server.""" + import flup.server.scgi as flups + return flups.WSGIServer(func, bindAddress=addr).run() + +def runwsgi(func): + """ + Runs a WSGI-compatible `func` using FCGI, SCGI, or a simple web server, + as appropriate based on context and `sys.argv`. + """ + + if os.environ.has_key('SERVER_SOFTWARE'): # cgi + os.environ['FCGI_FORCE_CGI'] = 'Y' + + if (os.environ.has_key('PHP_FCGI_CHILDREN') #lighttpd fastcgi + or os.environ.has_key('SERVER_SOFTWARE')): + return runfcgi(func, None) + + if 'fcgi' in sys.argv or 'fastcgi' in sys.argv: + args = sys.argv[1:] + if 'fastcgi' in args: args.remove('fastcgi') + elif 'fcgi' in args: args.remove('fcgi') + if args: + return runfcgi(func, validaddr(args[0])) + else: + return runfcgi(func, None) + + if 'scgi' in sys.argv: + args = sys.argv[1:] + args.remove('scgi') + if args: + return runscgi(func, validaddr(args[0])) + else: + return runscgi(func) + + return httpserver.runsimple(func, validip(listget(sys.argv, 1, ''))) diff --git a/plugins/WebUi/lib/webpy022/wsgiserver/__init__.py b/plugins/WebUi/lib/webpy022/wsgiserver/__init__.py new file mode 100644 index 000000000..1fe1c71ec --- /dev/null +++ b/plugins/WebUi/lib/webpy022/wsgiserver/__init__.py @@ -0,0 +1,1019 @@ +"""A high-speed, production ready, thread pooled, generic WSGI server. + +Simplest example on how to use this module directly +(without using CherryPy's application machinery): + + from cherrypy import wsgiserver + + def my_crazy_app(environ, start_response): + status = '200 OK' + response_headers = [('Content-type','text/plain')] + start_response(status, response_headers) + return ['Hello world!\n'] + + # Here we set our application to the script_name '/' + wsgi_apps = [('/', my_crazy_app)] + + server = wsgiserver.CherryPyWSGIServer(('localhost', 8070), wsgi_apps, + server_name='localhost') + + # Want SSL support? Just set these attributes + # server.ssl_certificate = + # server.ssl_private_key = + + if __name__ == '__main__': + try: + server.start() + except KeyboardInterrupt: + server.stop() + +This won't call the CherryPy engine (application side) at all, only the +WSGI server, which is independant from the rest of CherryPy. Don't +let the name "CherryPyWSGIServer" throw you; the name merely reflects +its origin, not it's coupling. + +The CherryPy WSGI server can serve as many WSGI applications +as you want in one instance: + + wsgi_apps = [('/', my_crazy_app), ('/blog', my_blog_app)] + +""" + +import base64 +import Queue +import os +import re +quoted_slash = re.compile("(?i)%2F") +import rfc822 +import socket +try: + import cStringIO as StringIO +except ImportError: + import StringIO +import sys +import threading +import time +import traceback +from urllib import unquote +from urlparse import urlparse + +try: + from OpenSSL import SSL + from OpenSSL import crypto +except ImportError: + SSL = None + +import errno +socket_errors_to_ignore = [] +# Not all of these names will be defined for every platform. +for _ in ("EPIPE", "ETIMEDOUT", "ECONNREFUSED", "ECONNRESET", + "EHOSTDOWN", "EHOSTUNREACH", + "WSAECONNABORTED", "WSAECONNREFUSED", "WSAECONNRESET", + "WSAENETRESET", "WSAETIMEDOUT"): + if _ in dir(errno): + socket_errors_to_ignore.append(getattr(errno, _)) +# de-dupe the list +socket_errors_to_ignore = dict.fromkeys(socket_errors_to_ignore).keys() +socket_errors_to_ignore.append("timed out") + + +comma_separated_headers = ['ACCEPT', 'ACCEPT-CHARSET', 'ACCEPT-ENCODING', + 'ACCEPT-LANGUAGE', 'ACCEPT-RANGES', 'ALLOW', 'CACHE-CONTROL', + 'CONNECTION', 'CONTENT-ENCODING', 'CONTENT-LANGUAGE', 'EXPECT', + 'IF-MATCH', 'IF-NONE-MATCH', 'PRAGMA', 'PROXY-AUTHENTICATE', 'TE', + 'TRAILER', 'TRANSFER-ENCODING', 'UPGRADE', 'VARY', 'VIA', 'WARNING', + 'WWW-AUTHENTICATE'] + +class HTTPRequest(object): + """An HTTP Request (and response). + + A single HTTP connection may consist of multiple request/response pairs. + + connection: the HTTP Connection object which spawned this request. + rfile: the 'read' fileobject from the connection's socket + ready: when True, the request has been parsed and is ready to begin + generating the response. When False, signals the calling Connection + that the response should not be generated and the connection should + close. + close_connection: signals the calling Connection that the request + should close. This does not imply an error! The client and/or + server may each request that the connection be closed. + chunked_write: if True, output will be encoded with the "chunked" + transfer-coding. This value is set automatically inside + send_headers. + """ + + def __init__(self, connection): + self.connection = connection + self.rfile = self.connection.rfile + self.sendall = self.connection.sendall + self.environ = connection.environ.copy() + + self.ready = False + self.started_response = False + self.status = "" + self.outheaders = [] + self.sent_headers = False + self.close_connection = False + self.chunked_write = False + + def parse_request(self): + """Parse the next HTTP request start-line and message-headers.""" + # HTTP/1.1 connections are persistent by default. If a client + # requests a page, then idles (leaves the connection open), + # then rfile.readline() will raise socket.error("timed out"). + # Note that it does this based on the value given to settimeout(), + # and doesn't need the client to request or acknowledge the close + # (although your TCP stack might suffer for it: cf Apache's history + # with FIN_WAIT_2). + request_line = self.rfile.readline() + if not request_line: + # Force self.ready = False so the connection will close. + self.ready = False + return + + if request_line == "\r\n": + # RFC 2616 sec 4.1: "...if the server is reading the protocol + # stream at the beginning of a message and receives a CRLF + # first, it should ignore the CRLF." + # But only ignore one leading line! else we enable a DoS. + request_line = self.rfile.readline() + if not request_line: + self.ready = False + return + + server = self.connection.server + environ = self.environ + environ["SERVER_SOFTWARE"] = "%s WSGI Server" % server.version + + method, path, req_protocol = request_line.strip().split(" ", 2) + environ["REQUEST_METHOD"] = method + + # path may be an abs_path (including "http://host.domain.tld"); + scheme, location, path, params, qs, frag = urlparse(path) + + if frag: + self.simple_response("400 Bad Request", + "Illegal #fragment in Request-URI.") + return + + if scheme: + environ["wsgi.url_scheme"] = scheme + if params: + path = path + ";" + params + + # Unquote the path+params (e.g. "/this%20path" -> "this path"). + # http://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5.1.2 + # + # But note that "...a URI must be separated into its components + # before the escaped characters within those components can be + # safely decoded." http://www.ietf.org/rfc/rfc2396.txt, sec 2.4.2 + atoms = [unquote(x) for x in quoted_slash.split(path)] + path = "%2F".join(atoms) + + if path == "*": + # This means, of course, that the last wsgi_app (shortest path) + # will always handle a URI of "*". + environ["SCRIPT_NAME"] = "" + environ["PATH_INFO"] = "*" + self.wsgi_app = server.mount_points[-1][1] + else: + for mount_point, wsgi_app in server.mount_points: + # The mount_points list should be sorted by length, descending. + if path.startswith(mount_point + "/") or path == mount_point: + environ["SCRIPT_NAME"] = mount_point + environ["PATH_INFO"] = path[len(mount_point):] + self.wsgi_app = wsgi_app + break + else: + self.simple_response("404 Not Found") + return + + # Note that, like wsgiref and most other WSGI servers, + # we unquote the path but not the query string. + environ["QUERY_STRING"] = qs + + # Compare request and server HTTP protocol versions, in case our + # server does not support the requested protocol. Limit our output + # to min(req, server). We want the following output: + # request server actual written supported response + # protocol protocol response protocol feature set + # a 1.0 1.0 1.0 1.0 + # b 1.0 1.1 1.1 1.0 + # c 1.1 1.0 1.0 1.0 + # d 1.1 1.1 1.1 1.1 + # Notice that, in (b), the response will be "HTTP/1.1" even though + # the client only understands 1.0. RFC 2616 10.5.6 says we should + # only return 505 if the _major_ version is different. + rp = int(req_protocol[5]), int(req_protocol[7]) + sp = int(server.protocol[5]), int(server.protocol[7]) + if sp[0] != rp[0]: + self.simple_response("505 HTTP Version Not Supported") + return + # Bah. "SERVER_PROTOCOL" is actually the REQUEST protocol. + environ["SERVER_PROTOCOL"] = req_protocol + # set a non-standard environ entry so the WSGI app can know what + # the *real* server protocol is (and what features to support). + # See http://www.faqs.org/rfcs/rfc2145.html. + environ["ACTUAL_SERVER_PROTOCOL"] = server.protocol + self.response_protocol = "HTTP/%s.%s" % min(rp, sp) + + # If the Request-URI was an absoluteURI, use its location atom. + if location: + environ["SERVER_NAME"] = location + + # then all the http headers + try: + self.read_headers() + except ValueError, ex: + self.simple_response("400 Bad Request", repr(ex.args)) + return + + creds = environ.get("HTTP_AUTHORIZATION", "").split(" ", 1) + environ["AUTH_TYPE"] = creds[0] + if creds[0].lower() == 'basic': + user, pw = base64.decodestring(creds[1]).split(":", 1) + environ["REMOTE_USER"] = user + + # Persistent connection support + if self.response_protocol == "HTTP/1.1": + if environ.get("HTTP_CONNECTION", "") == "close": + self.close_connection = True + else: + # HTTP/1.0 + if environ.get("HTTP_CONNECTION", "") != "Keep-Alive": + self.close_connection = True + + # Transfer-Encoding support + te = None + if self.response_protocol == "HTTP/1.1": + te = environ.get("HTTP_TRANSFER_ENCODING") + if te: + te = [x.strip().lower() for x in te.split(",") if x.strip()] + + read_chunked = False + + if te: + for enc in te: + if enc == "chunked": + read_chunked = True + else: + # Note that, even if we see "chunked", we must reject + # if there is an extension we don't recognize. + self.simple_response("501 Unimplemented") + self.close_connection = True + return + + if read_chunked: + if not self.decode_chunked(): + return + + # From PEP 333: + # "Servers and gateways that implement HTTP 1.1 must provide + # transparent support for HTTP 1.1's "expect/continue" mechanism. + # This may be done in any of several ways: + # 1. Respond to requests containing an Expect: 100-continue request + # with an immediate "100 Continue" response, and proceed normally. + # 2. Proceed with the request normally, but provide the application + # with a wsgi.input stream that will send the "100 Continue" + # response if/when the application first attempts to read from + # the input stream. The read request must then remain blocked + # until the client responds. + # 3. Wait until the client decides that the server does not support + # expect/continue, and sends the request body on its own. + # (This is suboptimal, and is not recommended.) + # + # We used to do 3, but are now doing 1. Maybe we'll do 2 someday, + # but it seems like it would be a big slowdown for such a rare case. + if environ.get("HTTP_EXPECT", "") == "100-continue": + self.simple_response(100) + + self.ready = True + + def read_headers(self): + """Read header lines from the incoming stream.""" + environ = self.environ + + while True: + line = self.rfile.readline() + if not line: + # No more data--illegal end of headers + raise ValueError("Illegal end of headers.") + + if line == '\r\n': + # Normal end of headers + break + + if line[0] in ' \t': + # It's a continuation line. + v = line.strip() + else: + k, v = line.split(":", 1) + k, v = k.strip().upper(), v.strip() + envname = "HTTP_" + k.replace("-", "_") + + if k in comma_separated_headers: + existing = environ.get(envname) + if existing: + v = ", ".join((existing, v)) + environ[envname] = v + + ct = environ.pop("HTTP_CONTENT_TYPE", None) + if ct: + environ["CONTENT_TYPE"] = ct + cl = environ.pop("HTTP_CONTENT_LENGTH", None) + if cl: + environ["CONTENT_LENGTH"] = cl + + def decode_chunked(self): + """Decode the 'chunked' transfer coding.""" + cl = 0 + data = StringIO.StringIO() + while True: + line = self.rfile.readline().strip().split(";", 1) + chunk_size = int(line.pop(0), 16) + if chunk_size <= 0: + break +## if line: chunk_extension = line[0] + cl += chunk_size + data.write(self.rfile.read(chunk_size)) + crlf = self.rfile.read(2) + if crlf != "\r\n": + self.simple_response("400 Bad Request", + "Bad chunked transfer coding " + "(expected '\\r\\n', got %r)" % crlf) + return + + # Grab any trailer headers + self.read_headers() + + data.seek(0) + self.environ["wsgi.input"] = data + self.environ["CONTENT_LENGTH"] = str(cl) or "" + return True + + def respond(self): + """Call the appropriate WSGI app and write its iterable output.""" + response = self.wsgi_app(self.environ, self.start_response) + try: + for chunk in response: + # "The start_response callable must not actually transmit + # the response headers. Instead, it must store them for the + # server or gateway to transmit only after the first + # iteration of the application return value that yields + # a NON-EMPTY string, or upon the application's first + # invocation of the write() callable." (PEP 333) + if chunk: + self.write(chunk) + finally: + if hasattr(response, "close"): + response.close() + if (self.ready and not self.sent_headers + and not self.connection.server.interrupt): + self.sent_headers = True + self.send_headers() + if self.chunked_write: + self.sendall("0\r\n\r\n") + + def simple_response(self, status, msg=""): + """Write a simple response back to the client.""" + status = str(status) + buf = ["%s %s\r\n" % (self.connection.server.protocol, status), + "Content-Length: %s\r\n" % len(msg)] + + if status[:3] == "413" and self.response_protocol == 'HTTP/1.1': + # Request Entity Too Large + self.close_connection = True + buf.append("Connection: close\r\n") + + buf.append("\r\n") + if msg: + buf.append(msg) + self.sendall("".join(buf)) + + def start_response(self, status, headers, exc_info = None): + """WSGI callable to begin the HTTP response.""" + if self.started_response: + if not exc_info: + raise AssertionError("WSGI start_response called a second " + "time with no exc_info.") + else: + try: + raise exc_info[0], exc_info[1], exc_info[2] + finally: + exc_info = None + self.started_response = True + self.status = status + self.outheaders.extend(headers) + return self.write + + def write(self, chunk): + """WSGI callable to write unbuffered data to the client. + + This method is also used internally by start_response (to write + data from the iterable returned by the WSGI application). + """ + if not self.started_response: + raise AssertionError("WSGI write called before start_response.") + + if not self.sent_headers: + self.sent_headers = True + self.send_headers() + + if self.chunked_write and chunk: + buf = [hex(len(chunk))[2:], "\r\n", chunk, "\r\n"] + self.sendall("".join(buf)) + else: + self.sendall(chunk) + + def send_headers(self): + """Assert, process, and send the HTTP response message-headers.""" + hkeys = [key.lower() for key, value in self.outheaders] + status = int(self.status[:3]) + + if status == 413: + # Request Entity Too Large. Close conn to avoid garbage. + self.close_connection = True + elif "content-length" not in hkeys: + # "All 1xx (informational), 204 (no content), + # and 304 (not modified) responses MUST NOT + # include a message-body." So no point chunking. + if status < 200 or status in (204, 205, 304): + pass + else: + if self.response_protocol == 'HTTP/1.1': + # Use the chunked transfer-coding + self.chunked_write = True + self.outheaders.append(("Transfer-Encoding", "chunked")) + else: + # Closing the conn is the only way to determine len. + self.close_connection = True + + if "connection" not in hkeys: + if self.response_protocol == 'HTTP/1.1': + if self.close_connection: + self.outheaders.append(("Connection", "close")) + else: + if not self.close_connection: + self.outheaders.append(("Connection", "Keep-Alive")) + + if "date" not in hkeys: + self.outheaders.append(("Date", rfc822.formatdate())) + + server = self.connection.server + + if "server" not in hkeys: + self.outheaders.append(("Server", server.version)) + + buf = [server.protocol, " ", self.status, "\r\n"] + try: + buf += [k + ": " + v + "\r\n" for k, v in self.outheaders] + except TypeError: + if not isinstance(k, str): + raise TypeError("WSGI response header key %r is not a string.") + if not isinstance(v, str): + raise TypeError("WSGI response header value %r is not a string.") + else: + raise + buf.append("\r\n") + self.sendall("".join(buf)) + + +class NoSSLError(Exception): + """Exception raised when a client speaks HTTP to an HTTPS socket.""" + pass + + +def _ssl_wrap_method(method, is_reader=False): + """Wrap the given method with SSL error-trapping. + + is_reader: if False (the default), EOF errors will be raised. + If True, EOF errors will return "" (to emulate normal sockets). + """ + def ssl_method_wrapper(self, *args, **kwargs): +## print (id(self), method, args, kwargs) + start = time.time() + while True: + try: + return method(self, *args, **kwargs) + except (SSL.WantReadError, SSL.WantWriteError): + # Sleep and try again. This is dangerous, because it means + # the rest of the stack has no way of differentiating + # between a "new handshake" error and "client dropped". + # Note this isn't an endless loop: there's a timeout below. + time.sleep(self.ssl_retry) + except SSL.SysCallError, e: + if is_reader and e.args == (-1, 'Unexpected EOF'): + return "" + + errno = e.args[0] + if is_reader and errno in socket_errors_to_ignore: + return "" + raise socket.error(errno) + except SSL.Error, e: + if is_reader and e.args == (-1, 'Unexpected EOF'): + return "" + + thirdarg = None + try: + thirdarg = e.args[0][0][2] + except IndexError: + pass + + if is_reader and thirdarg == 'ssl handshake failure': + return "" + if thirdarg == 'http request': + # The client is talking HTTP to an HTTPS server. + raise NoSSLError() + raise + if time.time() - start > self.ssl_timeout: + raise socket.timeout("timed out") + return ssl_method_wrapper + +class SSL_fileobject(socket._fileobject): + """Faux file object attached to a socket object.""" + + ssl_timeout = 3 + ssl_retry = .01 + + close = _ssl_wrap_method(socket._fileobject.close) + flush = _ssl_wrap_method(socket._fileobject.flush) + write = _ssl_wrap_method(socket._fileobject.write) + writelines = _ssl_wrap_method(socket._fileobject.writelines) + read = _ssl_wrap_method(socket._fileobject.read, is_reader=True) + readline = _ssl_wrap_method(socket._fileobject.readline, is_reader=True) + readlines = _ssl_wrap_method(socket._fileobject.readlines, is_reader=True) + + +class HTTPConnection(object): + """An HTTP connection (active socket). + + socket: the raw socket object (usually TCP) for this connection. + addr: the "bind address" for the remote end of the socket. + For IP sockets, this is a tuple of (REMOTE_ADDR, REMOTE_PORT). + For UNIX domain sockets, this will be a string. + server: the HTTP Server for this Connection. Usually, the server + object possesses a passive (server) socket which spawns multiple, + active (client) sockets, one for each connection. + + environ: a WSGI environ template. This will be copied for each request. + rfile: a fileobject for reading from the socket. + sendall: a function for writing (+ flush) to the socket. + """ + + rbufsize = -1 + RequestHandlerClass = HTTPRequest + environ = {"wsgi.version": (1, 0), + "wsgi.url_scheme": "http", + "wsgi.multithread": True, + "wsgi.multiprocess": False, + "wsgi.run_once": False, + "wsgi.errors": sys.stderr, + } + + def __init__(self, sock, addr, server): + self.socket = sock + self.addr = addr + self.server = server + + # Copy the class environ into self. + self.environ = self.environ.copy() + + if SSL and isinstance(sock, SSL.ConnectionType): + timeout = sock.gettimeout() + self.rfile = SSL_fileobject(sock, "r", self.rbufsize) + self.rfile.ssl_timeout = timeout + self.sendall = _ssl_wrap_method(sock.sendall) + self.environ["wsgi.url_scheme"] = "https" + self.environ["HTTPS"] = "on" + sslenv = getattr(server, "ssl_environ", None) + if sslenv: + self.environ.update(sslenv) + else: + self.rfile = sock.makefile("r", self.rbufsize) + self.sendall = sock.sendall + + self.environ.update({"wsgi.input": self.rfile, + "SERVER_NAME": self.server.server_name, + }) + + if isinstance(self.server.bind_addr, basestring): + # AF_UNIX. This isn't really allowed by WSGI, which doesn't + # address unix domain sockets. But it's better than nothing. + self.environ["SERVER_PORT"] = "" + else: + self.environ["SERVER_PORT"] = str(self.server.bind_addr[1]) + # optional values + # Until we do DNS lookups, omit REMOTE_HOST + self.environ["REMOTE_ADDR"] = self.addr[0] + self.environ["REMOTE_PORT"] = str(self.addr[1]) + + def communicate(self): + """Read each request and respond appropriately.""" + try: + while True: + # (re)set req to None so that if something goes wrong in + # the RequestHandlerClass constructor, the error doesn't + # get written to the previous request. + req = None + req = self.RequestHandlerClass(self) + # This order of operations should guarantee correct pipelining. + req.parse_request() + if not req.ready: + return + req.respond() + if req.close_connection: + return + except socket.error, e: + errno = e.args[0] + if errno not in socket_errors_to_ignore: + if req: + req.simple_response("500 Internal Server Error", + format_exc()) + return + except (KeyboardInterrupt, SystemExit): + raise + except NoSSLError: + # Unwrap our sendall + req.sendall = self.socket._sock.sendall + req.simple_response("400 Bad Request", + "The client sent a plain HTTP request, but " + "this server only speaks HTTPS on this port.") + except: + if req: + req.simple_response("500 Internal Server Error", format_exc()) + + def close(self): + """Close the socket underlying this connection.""" + self.rfile.close() + self.socket.close() + + +def format_exc(limit=None): + """Like print_exc() but return a string. Backport for Python 2.3.""" + try: + etype, value, tb = sys.exc_info() + return ''.join(traceback.format_exception(etype, value, tb, limit)) + finally: + etype = value = tb = None + + +_SHUTDOWNREQUEST = None + +class WorkerThread(threading.Thread): + """Thread which continuously polls a Queue for Connection objects. + + server: the HTTP Server which spawned this thread, and which owns the + Queue and is placing active connections into it. + ready: a simple flag for the calling server to know when this thread + has begun polling the Queue. + + Due to the timing issues of polling a Queue, a WorkerThread does not + check its own 'ready' flag after it has started. To stop the thread, + it is necessary to stick a _SHUTDOWNREQUEST object onto the Queue + (one for each running WorkerThread). + """ + + def __init__(self, server): + self.ready = False + self.server = server + threading.Thread.__init__(self) + + def run(self): + try: + self.ready = True + while True: + conn = self.server.requests.get() + if conn is _SHUTDOWNREQUEST: + return + + try: + conn.communicate() + finally: + conn.close() + except (KeyboardInterrupt, SystemExit), exc: + self.server.interrupt = exc + + +class SSLConnection: + """A thread-safe wrapper for an SSL.Connection. + + *args: the arguments to create the wrapped SSL.Connection(*args). + """ + + def __init__(self, *args): + self._ssl_conn = SSL.Connection(*args) + self._lock = threading.RLock() + + for f in ('get_context', 'pending', 'send', 'write', 'recv', 'read', + 'renegotiate', 'bind', 'listen', 'connect', 'accept', + 'setblocking', 'fileno', 'shutdown', 'close', 'get_cipher_list', + 'getpeername', 'getsockname', 'getsockopt', 'setsockopt', + 'makefile', 'get_app_data', 'set_app_data', 'state_string', + 'sock_shutdown', 'get_peer_certificate', 'want_read', + 'want_write', 'set_connect_state', 'set_accept_state', + 'connect_ex', 'sendall', 'settimeout'): + exec """def %s(self, *args): + self._lock.acquire() + try: + return self._ssl_conn.%s(*args) + finally: + self._lock.release() +""" % (f, f) + + +class CherryPyWSGIServer(object): + """An HTTP server for WSGI. + + bind_addr: a (host, port) tuple if TCP sockets are desired; + for UNIX sockets, supply the filename as a string. + wsgi_app: the WSGI 'application callable'; multiple WSGI applications + may be passed as (script_name, callable) pairs. + numthreads: the number of worker threads to create (default 10). + server_name: the string to set for WSGI's SERVER_NAME environ entry. + Defaults to socket.gethostname(). + max: the maximum number of queued requests (defaults to -1 = no limit). + request_queue_size: the 'backlog' argument to socket.listen(); + specifies the maximum number of queued connections (default 5). + timeout: the timeout in seconds for accepted connections (default 10). + + protocol: the version string to write in the Status-Line of all + HTTP responses. For example, "HTTP/1.1" (the default). This + also limits the supported features used in the response. + + + SSL/HTTPS + --------- + The OpenSSL module must be importable for SSL functionality. + You can obtain it from http://pyopenssl.sourceforge.net/ + + ssl_certificate: the filename of the server SSL certificate. + ssl_privatekey: the filename of the server's private key file. + + If either of these is None (both are None by default), this server + will not use SSL. If both are given and are valid, they will be read + on server start and used in the SSL context for the listening socket. + """ + + protocol = "HTTP/1.1" + version = "CherryPy/3.0.1" + ready = False + _interrupt = None + ConnectionClass = HTTPConnection + + # Paths to certificate and private key files + ssl_certificate = None + ssl_private_key = None + + def __init__(self, bind_addr, wsgi_app, numthreads=10, server_name=None, + max=-1, request_queue_size=5, timeout=10): + self.requests = Queue.Queue(max) + + if callable(wsgi_app): + # We've been handed a single wsgi_app, in CP-2.1 style. + # Assume it's mounted at "". + self.mount_points = [("", wsgi_app)] + else: + # We've been handed a list of (mount_point, wsgi_app) tuples, + # so that the server can call different wsgi_apps, and also + # correctly set SCRIPT_NAME. + self.mount_points = wsgi_app + self.mount_points.sort() + self.mount_points.reverse() + + self.bind_addr = bind_addr + self.numthreads = numthreads or 1 + if not server_name: + server_name = socket.gethostname() + self.server_name = server_name + self.request_queue_size = request_queue_size + self._workerThreads = [] + + self.timeout = timeout + + def start(self): + """Run the server forever.""" + # We don't have to trap KeyboardInterrupt or SystemExit here, + # because cherrpy.server already does so, calling self.stop() for us. + # If you're using this server with another framework, you should + # trap those exceptions in whatever code block calls start(). + self._interrupt = None + + # Select the appropriate socket + if isinstance(self.bind_addr, basestring): + # AF_UNIX socket + + # So we can reuse the socket... + try: os.unlink(self.bind_addr) + except: pass + + # So everyone can access the socket... + try: os.chmod(self.bind_addr, 0777) + except: pass + + info = [(socket.AF_UNIX, socket.SOCK_STREAM, 0, "", self.bind_addr)] + else: + # AF_INET or AF_INET6 socket + # Get the correct address family for our host (allows IPv6 addresses) + host, port = self.bind_addr + flags = 0 + if host == '': + # Despite the socket module docs, using '' does not + # allow AI_PASSIVE to work. Passing None instead + # returns '0.0.0.0' like we want. In other words: + # host AI_PASSIVE result + # '' Y 192.168.x.y + # '' N 192.168.x.y + # None Y 0.0.0.0 + # None N 127.0.0.1 + host = None + flags = socket.AI_PASSIVE + try: + info = socket.getaddrinfo(host, port, socket.AF_UNSPEC, + socket.SOCK_STREAM, 0, flags) + except socket.gaierror: + # Probably a DNS issue. Assume IPv4. + info = [(socket.AF_INET, socket.SOCK_STREAM, 0, "", self.bind_addr)] + + self.socket = None + msg = "No socket could be created" + for res in info: + af, socktype, proto, canonname, sa = res + try: + self.bind(af, socktype, proto) + except socket.error, msg: + if self.socket: + self.socket.close() + self.socket = None + continue + break + if not self.socket: + raise socket.error, msg + + # Timeout so KeyboardInterrupt can be caught on Win32 + self.socket.settimeout(1) + self.socket.listen(self.request_queue_size) + + # Create worker threads + for i in xrange(self.numthreads): + self._workerThreads.append(WorkerThread(self)) + for worker in self._workerThreads: + worker.setName("CP WSGIServer " + worker.getName()) + worker.start() + for worker in self._workerThreads: + while not worker.ready: + time.sleep(.1) + + self.ready = True + while self.ready: + self.tick() + if self.interrupt: + while self.interrupt is True: + # Wait for self.stop() to complete. See _set_interrupt. + time.sleep(0.1) + raise self.interrupt + + def bind(self, family, type, proto=0): + """Create (or recreate) the actual socket object.""" + self.socket = socket.socket(family, type, proto) + self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) +## self.socket.setsockopt(socket.SOL_SOCKET, socket.TCP_NODELAY, 1) + if self.ssl_certificate and self.ssl_private_key: + if SSL is None: + raise ImportError("You must install pyOpenSSL to use HTTPS.") + + # See http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/442473 + ctx = SSL.Context(SSL.SSLv23_METHOD) + ctx.use_privatekey_file(self.ssl_private_key) + ctx.use_certificate_file(self.ssl_certificate) + self.socket = SSLConnection(ctx, self.socket) + self.populate_ssl_environ() + self.socket.bind(self.bind_addr) + + def tick(self): + """Accept a new connection and put it on the Queue.""" + try: + s, addr = self.socket.accept() + if not self.ready: + return + if hasattr(s, 'settimeout'): + s.settimeout(self.timeout) + conn = self.ConnectionClass(s, addr, self) + self.requests.put(conn) + except socket.timeout: + # The only reason for the timeout in start() is so we can + # notice keyboard interrupts on Win32, which don't interrupt + # accept() by default + return + except socket.error, x: + msg = x.args[1] + if msg in ("Bad file descriptor", "Socket operation on non-socket"): + # Our socket was closed. + return + if msg == "Resource temporarily unavailable": + # Just try again. See http://www.cherrypy.org/ticket/479. + return + raise + + def _get_interrupt(self): + return self._interrupt + def _set_interrupt(self, interrupt): + self._interrupt = True + self.stop() + self._interrupt = interrupt + interrupt = property(_get_interrupt, _set_interrupt, + doc="Set this to an Exception instance to " + "interrupt the server.") + + def stop(self): + """Gracefully shutdown a server that is serving forever.""" + self.ready = False + + sock = getattr(self, "socket", None) + if sock: + if not isinstance(self.bind_addr, basestring): + # Touch our own socket to make accept() return immediately. + try: + host, port = sock.getsockname()[:2] + except socket.error, x: + if x.args[1] != "Bad file descriptor": + raise + else: + # Note that we're explicitly NOT using AI_PASSIVE, + # here, because we want an actual IP to touch. + # localhost won't work if we've bound to a public IP, + # but it would if we bound to INADDR_ANY via host = ''. + for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC, + socket.SOCK_STREAM): + af, socktype, proto, canonname, sa = res + s = None + try: + s = socket.socket(af, socktype, proto) + # See http://groups.google.com/group/cherrypy-users/ + # browse_frm/thread/bbfe5eb39c904fe0 + s.settimeout(1.0) + s.connect((host, port)) + s.close() + except socket.error: + if s: + s.close() + if hasattr(sock, "close"): + sock.close() + self.socket = None + + # Must shut down threads here so the code that calls + # this method can know when all threads are stopped. + for worker in self._workerThreads: + self.requests.put(_SHUTDOWNREQUEST) + + # Don't join currentThread (when stop is called inside a request). + current = threading.currentThread() + while self._workerThreads: + worker = self._workerThreads.pop() + if worker is not current and worker.isAlive: + try: + worker.join() + except AssertionError: + pass + + def populate_ssl_environ(self): + """Create WSGI environ entries to be merged into each request.""" + cert = open(self.ssl_certificate).read() + cert = crypto.load_certificate(crypto.FILETYPE_PEM, cert) + self.ssl_environ = { + # pyOpenSSL doesn't provide access to any of these AFAICT +## 'SSL_PROTOCOL': 'SSLv2', +## SSL_CIPHER string The cipher specification name +## SSL_VERSION_INTERFACE string The mod_ssl program version +## SSL_VERSION_LIBRARY string The OpenSSL program version + } + + # Server certificate attributes + self.ssl_environ.update({ + 'SSL_SERVER_M_VERSION': cert.get_version(), + 'SSL_SERVER_M_SERIAL': cert.get_serial_number(), +## 'SSL_SERVER_V_START': Validity of server's certificate (start time), +## 'SSL_SERVER_V_END': Validity of server's certificate (end time), + }) + + for prefix, dn in [("I", cert.get_issuer()), + ("S", cert.get_subject())]: + # X509Name objects don't seem to have a way to get the + # complete DN string. Use str() and slice it instead, + # because str(dn) == "" + dnstr = str(dn)[18:-2] + + wsgikey = 'SSL_SERVER_%s_DN' % prefix + self.ssl_environ[wsgikey] = dnstr + + # The DN should be of the form: /k1=v1/k2=v2, but we must allow + # for any value to contain slashes itself (in a URL). + while dnstr: + pos = dnstr.rfind("=") + dnstr, value = dnstr[:pos], dnstr[pos + 1:] + pos = dnstr.rfind("/") + dnstr, key = dnstr[:pos], dnstr[pos + 1:] + if key and value: + wsgikey = 'SSL_SERVER_%s_DN_%s' % (prefix, key) + self.ssl_environ[wsgikey] = value + diff --git a/plugins/WebUi/run_webserver b/plugins/WebUi/run_webserver new file mode 100755 index 000000000..b8e9b47f5 --- /dev/null +++ b/plugins/WebUi/run_webserver @@ -0,0 +1,3 @@ +#!/usr/bin/env python +import deluge_webserver +deluge_webserver.run() diff --git a/plugins/WebUi/scripts/add_torrent_to_deluge_webui b/plugins/WebUi/scripts/add_torrent_to_deluge_webui new file mode 100755 index 000000000..740857ccb --- /dev/null +++ b/plugins/WebUi/scripts/add_torrent_to_deluge_webui @@ -0,0 +1,10 @@ +#!/bin/bash + +pwd=deluge +url=http://localhost:8112 + +for arg in "$@" +do + curl -F torrent=@"$arg" -F pwd=$pwd $url/remote/torrent/add +done + diff --git a/plugins/WebUi/scripts/add_torrents_to_deluge.user.js b/plugins/WebUi/scripts/add_torrents_to_deluge.user.js new file mode 100644 index 000000000..ca6c59efc --- /dev/null +++ b/plugins/WebUi/scripts/add_torrents_to_deluge.user.js @@ -0,0 +1,207 @@ +// ==UserScript== +// @name Add Torrents To Deluge +// @namespace http://blog.monstuff.com/archives/cat_greasemonkey.html +// @description Let's you add torrents to the deluge WebUi +// @include http://isohunt.com/torrent_details/* +// @include http://thepiratebay.org/details.php?* +// @include http://torrentreactor.net/view.php?* +// @include http://www.mininova.org/* +// @include http://www.torrentspy.com/* +// @include http://ts.searching.com/* +// @include * +// ==/UserScript== + +//url-based submit and parsing based on : "Add Torrents To utorrent" by Julien Couvreur +//binary magic,contains from http://mgran.blogspot.com/2006/08/downloading-binary-streams-with.html + +//these parameters need to be edited before using the script + +// Server address +var host = "localhost"; +// Server port +var port = "8112"; +//open_page: "_blank" for a new window or "deluge_webui" for window re-use +//(not for private=1) +var open_page = "_blank" +//Private-trackers 0/1 +//different behavior, gets torrent-data from (private) site and pops up a message. +var private_submit = 1; +//deluge_password, only needed if private_submit = 1. +var deluge_password = 'deluge'; +//======================== + + +if (host == "") { alert('You need to configure the "Add Torrents To Deluge" user script with your WebUI parameters before using it.'); } + + + +function scanLinks() { + var links = getLinks(); + + for (var i=0; i < links.length; i++){ + var link = links[i]; + if (match(link.href)) { + if (private_submit) { + makeUTorrentLink_private(link,i); + } + else { + makeUTorrentLink(link); + } + } + } +} + +function makeUTorrentLink(link) { + var uTorrentLink = document.createElement('a'); + uTorrentLink.setAttribute("href", makeUTorrentUrl(link.href)); + uTorrentLink.setAttribute("target", open_page); + uTorrentLink.style.paddingLeft = "5px"; + uTorrentLink.innerHTML = ""; + link.parentNode.insertBefore(uTorrentLink, link.nextSibling); + return uTorrentLink +} + +function makeUTorrentUrl(url) { + var uTorrentUrl = "http://"+host+":"+port+"/torrent/add?redir_after_login=1"; + return uTorrentUrl + "&url=" + escape(url); +} + +function makeUTorrentLink_private(link,i) { + var id = 'deluge_link' + i; + var uTorrentLink = document.createElement('a'); + uTorrentLink.setAttribute("href", '#'); + uTorrentLink.setAttribute("id", id); + uTorrentLink.style.paddingLeft = "5px"; + uTorrentLink.innerHTML = ""; + link.parentNode.insertBefore(uTorrentLink, link.nextSibling); + + ulink = document.getElementById(id) + ulink.addEventListener("click", evt_private_submit_factory(link.href),false); + + return uTorrentLink +} + +function evt_private_submit_factory(url) { + //can this be done without magic? + function evt_private_submit(evt) { + GM_xmlhttpRequest({ method: 'GET', url: url, + overrideMimeType: 'text/plain; charset=x-user-defined', + onload: function(xhr) { + var stream = translateToBinaryString(xhr.responseText); + var data_b64 = window.btoa(stream); + post_to_webui(url, data_b64); + }, + onerror:function(xhr) { + alert('error fetching torrent file'); + } + }); + return false; + } + return evt_private_submit; +} + + +function post_to_webui(url,data_b64){ + //alert('here1'); + //data contains the content of the .torrent-file. + var POST_data = ('pwd=' + encodeURIComponent(deluge_password) + + '&torrent_name=' + encodeURIComponent(url) + '.torrent' + //+.torrent is a clutch! + '&data_b64=' + encodeURIComponent(data_b64) ); + //alert(POST_data); + + GM_xmlhttpRequest({ method: 'POST', + url: "http://"+host+":"+port+"/remote/torrent/add", + headers:{'Content-type':'application/x-www-form-urlencoded'}, + data: POST_data, + onload: function(xhr) { + if (xhr.responseText == 'ok\n') { + alert('Added torrent to webui : \n' + url); + } + else { + alert('Error adding torrent to webui:\n"' + xhr.responseText + '"'); + } + + }, + onerror:function(xhr) { + alert('error submitting torrent file'); + } + + }); +} + + + + + +function match(url) { + + // isohunt format + if (url.match(/http:\/\/.*isohunt\.com\/download\//i)) { + return true; + } + + if (url.match(/\.torrent$/)) { + return true; + } + + if (url.match(/http:\/\/.*bt-chat\.com\/download\.php/)) { + return true; + } + + // TorrentReactor + if (url.match(/http:\/\/dl\.torrentreactor\.net\/download.php\?/i)) { + return true; + } + + // Mininova + if (url.match(/http:\/\/www\.mininova\.org\/get\//i)) { + return true; + } + + // Mininova + if (url.match(/http:\/\/www\.mininova\.org\/get\//i)) { + return true; + } + + // TorrentSpy + if (url.match(/http:\/\/ts\.searching\.com\/download\.asp\?/i)) { + return true; + } + if (url.match(/http:\/\/www\.torrentspy\.com\/download.asp\?/i)) { + return true; + } + + // Seedler + if (url.match(/http:\/\/.*seedler\.org\/download\.x\?/i)) { + return true; + } + return false; +} + + +function getLinks() { + var doc_links = document.links; + var links = new Array(); + for (var i=0; i < doc_links.length; i++){ + links.push(doc_links[i]); + } + return links; +} + +var image = "data:image/gif;base64,R0lGODlhEAAQAMZyAB1CdihAYx5CdiBEeCJGeSZJfChKfChLfSpPgTBRgThRdDRUgzRVhDVWhDZWhThYhjtbiD1ciD5diT5eiz9eikBeiUFeiT5fjT1gjkBfjERijkdjiUhljkVnlEdolUxokExqkk5qkU9rklBrklFtk1BullFulk5vmlZymFx3nE97rVZ5pUx8sl54nlt5oVl6pE5/tWJ6nVp9qFqArWOEq1uIuW6EpGCItl2Ku26Gp2KKuGuIrF+MvWaLtl+Nv3KJqG+KrGaOu2aQv2SRwnGOs2uQvGqSwICOpoCQqm6Ww3OVvHKWv3iWuoKWsn+XtnacxXaeynifyXigzICewn2gxnqizoqfunujzpWesX6l0IyivYijw4+jvpOiuoOp0puktY2x2I6y2Y+z2pG02pW43Ze42pa43Z/A4qjG56jH56nI6KzJ6a/M67nR67zW8sLa9cff+M/k+P///////////////////////////////////////////////////////yH+FUNyZWF0ZWQgd2l0aCBUaGUgR0lNUAAh+QQBCgB/ACwAAAAAEAAQAAAHkIB/goOEhYaCX1iHhkdIXU2LgzFARExbkYInCBcvRVSRHgQNEiYoPUmHGAkjO1FSSilBNYYQFTllY2BeSzJChg4iWmhpZ2JXOjgqhBMFH1xvbmtmWUMwM4QZBws/cXBsZFU+LCuFDwIhVm1qYVA8Nx2FEQQDHDZOU09GNIcWDAAGFEC0cBEpwAYNJUgowMQwEAA7"; + +scanLinks(); + +/* +binary magic,contains code taken from +http://mgran.blogspot.com/2006/08/downloading-binary-streams-with.html +*/ +function translateToBinaryString(text){ + var out; + out=''; + for(i=0;i revno +bzr version-info > version +rm ~/prj/WebUi/WebUi.tgz +cd ~/prj +tar -zcvf ~/prj/WebUi/WebUi.tgz WebUi/ --exclude '.*' --exclude '*.pyc' --exclude '*.tgz' --exclude 'attic' --exclude 'xul' --exclude '*.sh' --exclude '*.*~' \ No newline at end of file diff --git a/plugins/WebUi/scripts/curl-example b/plugins/WebUi/scripts/curl-example new file mode 100644 index 000000000..f6fb0821a --- /dev/null +++ b/plugins/WebUi/scripts/curl-example @@ -0,0 +1 @@ +curl -F torrent=@./test1.torrent -F pwd=deluge http://localhost:8112/remote/torrent/add diff --git a/plugins/WebUi/scripts/extract_template_strings.py b/plugins/WebUi/scripts/extract_template_strings.py new file mode 100644 index 000000000..b1a894b31 --- /dev/null +++ b/plugins/WebUi/scripts/extract_template_strings.py @@ -0,0 +1,29 @@ +from __future__ import with_statement +import os +import re +template_dirs = ['~/prj/WebUi/templates/deluge', + '~/prj/WebUi/templates/advanced'] + +template_dirs = [os.path.expanduser(template_dir ) for template_dir in template_dirs] + + +files = [] +for template_dir in template_dirs: + files += [os.path.join(template_dir,fname) + for fname in os.listdir(template_dir) + if fname.endswith('.html')] + + +all_strings = [] +for filename in files: + with open(filename,'r') as f: + content = f.read() + all_strings += re.findall("_\(\"(.*?)\"\)",content) + all_strings += re.findall("_\(\'(.*?)\'\)",content) + +all_strings = sorted(set(all_strings)) + +with open ('./template_strings.py','w') as f: + for value in all_strings: + f.write("_('%s')\n" % value ) + diff --git a/plugins/WebUi/scripts/template_strings.py b/plugins/WebUi/scripts/template_strings.py new file mode 100644 index 000000000..925db255d --- /dev/null +++ b/plugins/WebUi/scripts/template_strings.py @@ -0,0 +1,65 @@ +_('# Of Files') +_('About') +_('Add') +_('Add Torrent') +_('Add torrent') +_('Apply') +_('Auto refresh:') +_('Ava') +_('Availability') +_('Config') +_('Connections') +_('Debug:Data Dump') +_('Delete .torrent file') +_('Delete downloaded files.') +_('Details') +_('Disable') +_('Down') +_('Down Speed') +_('Download') +_('Downloaded') +_('ETA') +_('Enable') +_('Error') +_('Eta') +_('Login') +_('Logout') +_('Name') +_('Next Announce') +_('Off') +_('Password') +_('Password is invalid,try again') +_('Pause') +_('Pause all') +_('Peers') +_('Pieces') +_('Progress') +_('Queue Down') +_('Queue Position') +_('Queue Up') +_('Ratio') +_('Reannounce') +_('Refresh page every:') +_('Remove') +_('Remove torrent') +_('Resume') +_('Resume all') +_('Seeders') +_('Set') +_('Set Timeout') +_('Share Ratio') +_('Size') +_('Speed') +_('Start') +_('Submit') +_('Torrent list') +_('Total Size') +_('Tracker') +_('Tracker Status') +_('Up') +_('Up Speed') +_('Upload') +_('Upload torrent') +_('Uploaded') +_('Url') +_('seconds') diff --git a/plugins/WebUi/ssl/deluge.key b/plugins/WebUi/ssl/deluge.key new file mode 100644 index 000000000..a9d5db5ce --- /dev/null +++ b/plugins/WebUi/ssl/deluge.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEA1sPXr1O6l2J9NAEvEYQ/JFDSVcJHh9YxP7kPdjsu7k9Ih845 +BHMX52A3Ypbe5MHe2bCj/8dRYCixRdF1KUTAKXdzc7mw9prgf3sS3RvmfcRsln6u +x7XRg7YprZJ46hFmcHiUPRgtTFLuFO2YWBnqxu/caTtAxx3PdoK6LDVnuVjHYofC +8uD4A9k6yL/jj3Yrkf8WYQqJ6pJcMAz/2c8ZXlBuiUCb9j5xKTzYoJaiUkKN2YrA +hoxRxfI7Zc7MH2yWw8/fTZJbGXo8nrfek7coSE7yQS1M6ciwkYk5VO2mBVJBJgAT +QUR/jGfLzEqNKXghQ564v9wmuFmUMd99a0tkVwIDAQABAoIBACID6sluLYOEqefu +uBHCLG4IDwheOQ4esrYxDW3gedJs5EP+ObGmuQaAisUmuC7rNeysuYzteMoOJ+Wz +AyeCKB1pOfP+WTT12tDWIWq73InW7ov3jJ89AO4nj/pZ1KTeFKeDsZbrmWEZUXQn +HZX2pOTVYMeaBuyCoDVZBzuxSbhlON4wS6ClMhem+eBOxg351CDTZa2cbq7Ffcos +VP7LY2ORQYNDTQSLguV/dJrFSotB8Eoz2xIpg5XR7msp6lzPzyAd+Aoz/T1lYxCY +IFZCJYKnIpgoYQvmtUlhQrdD8P0J4Kth7I8NgkWvXCKazQjhpUm+wojLKD0G7Kcz +9znIV+ECgYEA+qfp1C8jWbaAn1yAeORUA9aB6aGIURfOpZjnCvtMWM0Nu0nAJYDv +X7L5GRa1ulfKhfUG1Jv/ynMKXYuBUDhyccYLpP7BHpd29Arr7YAgb52KaD1PoKNa +Z45c61dj4sFoCmJEbDoL21UGb0LX3mc4XzPzwWs8AKfLW4aZh1NwCisCgYEA21gJ +Hy3egBgMT9+nVjqsgtIXgJOnzQRhvRwT7IFf392ZyFi8iM+pDUsx1yj0zSG4XNPw +NY8VtZuTBUlG73RKcrrz31jhCMfLCnoRkQeweZv0QWzbLU3V8DleUYdjFc/t0me5 +4NBR9lBlwYHgyU3GQ814vum+m0IAH0Ng1UxAVIUCgYAFOHwZTEYLN07kgtO2MOND +FTOtfwzMy5clQdMGGofTjanMjdOvtEjIEH05tYxhbjSsp5bV1M32FIFRw3cVCafw +kLRrYlb5YSQ8HwIc9z81s+1PEH/ZE63tXDy5Nh/BeE/Hb5aHPopCrjmtFZJTcojt +CrL4A1jDlrsYk+wcsnMx8wKBgEhJJQhvd2pDgps4G8+hGoUqc7Bd+OjpzsQh4rcI +k+4U+7847zkvJolJBK3hw3tu53FAL2OXOhJVqQgO9B+p9XcGAaTTh6X7IgDb5bok +DJanPMHq+/hcNGssnNbFhXQEyF2U7X8XaEuCh2ZURR5SUUq7BlX0dmp4P84NyHXC +4Vh5AoGAZYWkXxQUGzVm+H3fPpmETWGRNFDTimzi+6N+/uHkqkiDa3LGSnabmKh+ +voKm//DUjEVGlAZ3CGOjO/5SlZc/zjkgh1vg7KOU4x7DqVOuZjom5Tx3ZI4xVVVt +tVtvK0qjzUTVcwAQALN/PNak+gs9534e954rmA9kmc3xBe4ho9M= +-----END RSA PRIVATE KEY----- diff --git a/plugins/WebUi/ssl/deluge.pem b/plugins/WebUi/ssl/deluge.pem new file mode 100644 index 000000000..effef476e --- /dev/null +++ b/plugins/WebUi/ssl/deluge.pem @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDlzCCAn+gAwIBAgIJAPnW/GEzRy8xMA0GCSqGSIb3DQEBBQUAMDsxCzAJBgNV +BAYTAkFVMRUwEwYDVQQIEwxUaGUgSW50ZXJuZXQxFTATBgNVBAoTDERlbHVnZSBX +ZWJ1aTAeFw0wNzExMjQxMDAzNDRaFw0wODExMjMxMDAzNDRaMDsxCzAJBgNVBAYT +AkFVMRUwEwYDVQQIEwxUaGUgSW50ZXJuZXQxFTATBgNVBAoTDERlbHVnZSBXZWJ1 +aTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANbD169TupdifTQBLxGE +PyRQ0lXCR4fWMT+5D3Y7Lu5PSIfOOQRzF+dgN2KW3uTB3tmwo//HUWAosUXRdSlE +wCl3c3O5sPaa4H97Et0b5n3EbJZ+rse10YO2Ka2SeOoRZnB4lD0YLUxS7hTtmFgZ +6sbv3Gk7QMcdz3aCuiw1Z7lYx2KHwvLg+APZOsi/4492K5H/FmEKieqSXDAM/9nP +GV5QbolAm/Y+cSk82KCWolJCjdmKwIaMUcXyO2XOzB9slsPP302SWxl6PJ633pO3 +KEhO8kEtTOnIsJGJOVTtpgVSQSYAE0FEf4xny8xKjSl4IUOeuL/cJrhZlDHffWtL +ZFcCAwEAAaOBnTCBmjAdBgNVHQ4EFgQU1BbX1/4WtAKRKmWI1gqryIoj7BQwawYD +VR0jBGQwYoAU1BbX1/4WtAKRKmWI1gqryIoj7BShP6Q9MDsxCzAJBgNVBAYTAkFV +MRUwEwYDVQQIEwxUaGUgSW50ZXJuZXQxFTATBgNVBAoTDERlbHVnZSBXZWJ1aYIJ +APnW/GEzRy8xMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAEoiSz5x +hRCplxUG34g3F5yJe0QboqzJ/XmECfO80a980C/WVeivM2Kb1uafsKNp+WK7wD8g +mei+todYXG+fD8WmG41LG87Xi2Xe4SlAcemEpGcC5F1bpCdvqnVAWFnqoF88FOHx +NDlrq5H5lhMH9wVrX9qJvxL+StaDJ0sFk4kMGWEN+bdSYfFdBQzF903nPtm+PlvO +1Uo6gCuRTMYM5J1DC/GpNpo/Fzrkgm8mMf1MYy3rljiNgMt2rnxhtwi6jugwyMui +id6Of6gYAtvhi7kmaUpdI5PHO35dqRK7pHXH+YXaulosCPw/+bSRptFTykeEMrBj +CzotqJ+74MwXZyM= +-----END CERTIFICATE----- diff --git a/plugins/WebUi/static/images/deluge_icon.gif b/plugins/WebUi/static/images/deluge_icon.gif new file mode 100644 index 000000000..f00149312 Binary files /dev/null and b/plugins/WebUi/static/images/deluge_icon.gif differ diff --git a/plugins/WebUi/static/images/downloading16.png b/plugins/WebUi/static/images/downloading16.png new file mode 100644 index 000000000..53a2b6e61 Binary files /dev/null and b/plugins/WebUi/static/images/downloading16.png differ diff --git a/plugins/WebUi/static/images/inactive16.png b/plugins/WebUi/static/images/inactive16.png new file mode 100644 index 000000000..10342be18 Binary files /dev/null and b/plugins/WebUi/static/images/inactive16.png differ diff --git a/plugins/WebUi/static/images/seeding16.png b/plugins/WebUi/static/images/seeding16.png new file mode 100644 index 000000000..6994323de Binary files /dev/null and b/plugins/WebUi/static/images/seeding16.png differ diff --git a/plugins/WebUi/static/images/simple_bg.jpg b/plugins/WebUi/static/images/simple_bg.jpg new file mode 100755 index 000000000..7371fd9a0 Binary files /dev/null and b/plugins/WebUi/static/images/simple_bg.jpg differ diff --git a/plugins/WebUi/static/images/simple_line.jpg b/plugins/WebUi/static/images/simple_line.jpg new file mode 100755 index 000000000..9054b9816 Binary files /dev/null and b/plugins/WebUi/static/images/simple_line.jpg differ diff --git a/plugins/WebUi/static/images/simple_logo.jpg b/plugins/WebUi/static/images/simple_logo.jpg new file mode 100755 index 000000000..7ddc33455 Binary files /dev/null and b/plugins/WebUi/static/images/simple_logo.jpg differ diff --git a/plugins/WebUi/static/images/tango/details.png b/plugins/WebUi/static/images/tango/details.png new file mode 100644 index 000000000..8dd48c494 Binary files /dev/null and b/plugins/WebUi/static/images/tango/details.png differ diff --git a/plugins/WebUi/static/images/tango/down.png b/plugins/WebUi/static/images/tango/down.png new file mode 100644 index 000000000..732d46f37 Binary files /dev/null and b/plugins/WebUi/static/images/tango/down.png differ diff --git a/plugins/WebUi/static/images/tango/list-add.png b/plugins/WebUi/static/images/tango/list-add.png new file mode 100644 index 000000000..1aa7f095c Binary files /dev/null and b/plugins/WebUi/static/images/tango/list-add.png differ diff --git a/plugins/WebUi/static/images/tango/list-remove.png b/plugins/WebUi/static/images/tango/list-remove.png new file mode 100644 index 000000000..00b654e8c Binary files /dev/null and b/plugins/WebUi/static/images/tango/list-remove.png differ diff --git a/plugins/WebUi/static/images/tango/pause.png b/plugins/WebUi/static/images/tango/pause.png new file mode 100644 index 000000000..c8b4fe225 Binary files /dev/null and b/plugins/WebUi/static/images/tango/pause.png differ diff --git a/plugins/WebUi/static/images/tango/preferences-system.png b/plugins/WebUi/static/images/tango/preferences-system.png new file mode 100644 index 000000000..9460dfc74 Binary files /dev/null and b/plugins/WebUi/static/images/tango/preferences-system.png differ diff --git a/plugins/WebUi/static/images/tango/process-stop.png b/plugins/WebUi/static/images/tango/process-stop.png new file mode 100644 index 000000000..ab6808fba Binary files /dev/null and b/plugins/WebUi/static/images/tango/process-stop.png differ diff --git a/plugins/WebUi/static/images/tango/queue-down.png b/plugins/WebUi/static/images/tango/queue-down.png new file mode 100644 index 000000000..3dd7fccdf Binary files /dev/null and b/plugins/WebUi/static/images/tango/queue-down.png differ diff --git a/plugins/WebUi/static/images/tango/queue-up.png b/plugins/WebUi/static/images/tango/queue-up.png new file mode 100644 index 000000000..fa9a7d71b Binary files /dev/null and b/plugins/WebUi/static/images/tango/queue-up.png differ diff --git a/plugins/WebUi/static/images/tango/start.png b/plugins/WebUi/static/images/tango/start.png new file mode 100644 index 000000000..a7de0feb0 Binary files /dev/null and b/plugins/WebUi/static/images/tango/start.png differ diff --git a/plugins/WebUi/static/images/tango/stop.png b/plugins/WebUi/static/images/tango/stop.png new file mode 100644 index 000000000..ede2815e5 Binary files /dev/null and b/plugins/WebUi/static/images/tango/stop.png differ diff --git a/plugins/WebUi/static/images/tango/system-log-out.png b/plugins/WebUi/static/images/tango/system-log-out.png new file mode 100644 index 000000000..0010931e2 Binary files /dev/null and b/plugins/WebUi/static/images/tango/system-log-out.png differ diff --git a/plugins/WebUi/static/images/tango/up.png b/plugins/WebUi/static/images/tango/up.png new file mode 100644 index 000000000..c4fae73de Binary files /dev/null and b/plugins/WebUi/static/images/tango/up.png differ diff --git a/plugins/WebUi/static/images/tango/user-trash.png b/plugins/WebUi/static/images/tango/user-trash.png new file mode 100644 index 000000000..0e0953c73 Binary files /dev/null and b/plugins/WebUi/static/images/tango/user-trash.png differ diff --git a/plugins/WebUi/static/images/tango/view-refresh.png b/plugins/WebUi/static/images/tango/view-refresh.png new file mode 100644 index 000000000..3fd71d6e5 Binary files /dev/null and b/plugins/WebUi/static/images/tango/view-refresh.png differ diff --git a/plugins/WebUi/static/simple_site_style.css b/plugins/WebUi/static/simple_site_style.css new file mode 100755 index 000000000..3776994e8 --- /dev/null +++ b/plugins/WebUi/static/simple_site_style.css @@ -0,0 +1,91 @@ +/* ----------------------------------------------------------- Theme Name: Simple Theme URI: http://deluge-torrent.org Description: Deluge Theme Version: 1.0 ----------------------------------------------------------- */ BODY { background: #304663 url(images/simple_bg.jpg) repeat-x; font-family: trebuchet ms; font-size: 10pt; margin: 0; } /* GENERIC STYLES */ a img {border: 0px} hr {color: #627082; margin: 15px 0 15px 0;} /* STRUCTURE */ #page { min-width: 800px; margin-left: auto; margin-right: auto; } #main_content { background:url(images/simple_line.jpg) repeat-x; } #simple_logo { background:url(images/simple_logo.jpg) no-repeat; } #main { padding-top: 20px; padding-left: 20px; color: #fff; } #main form table { border: #2a425c 1px solid; } #main form table tr { border: 0px; } #main form table tr th { background: #1f3044; font-size: 16px; border: 0px; + white-space: nowrap; } #main form table tr td{ border: 0px; color: #fff; font-size: 12px; white-space: nowrap; } #main form table tr th a { color: #8fa6c3; font-size: 16px; white-space: nowrap; } #main form table tr th a, a:active, a:visited { color: #8fa6c3; text-decoration: none; } #main form table tr th a:hover {color: #fff; text-decoration: underline;} #main form table tr td a { color: #fff; font-size: 12px; white-space: nowrap; } #main form table tr td a, a:active, a:visited { color: #fff; text-decoration: none;} #main form table tr td a:hover {color: #fff; text-decoration: underline;} #main a { color: #fff; font-size: 12px; } #main a, a:active, a:visited { color: #fff; text-decoration: none;} #main a:hover {color: #fff; text-decoration: underline;} .info { text-align: right; padding: 0 50px 0 0; color: #8fa6c3; font-size: 16px; letter-spacing: 4px; font-weight: bold; } .title { color: #dce4ee; font-size: 32px; padding: 10px 50px 0 0; text-align: right; } .title a, a:active, a:visited { color: #dce4ee; text-decoration: none;} .title a:hover {color: #fff; text-decoration: underline;} #button { border:1px solid #23344b; background: #99acc3; color: #000; font-family:verdana, arial, helvetica, sans-serif; font-size:10px; margin-top:5px; } INPUT{ border:1px solid #23344b; background: #99acc3; color: #000; } TEXTAREA{ border:1px solid #23344b; background: #99acc3; width:480px; } .footertext a { color: #c0c0c0; text-decoration:none;} .footertext a:visited { color: #c0c0c0; text-decoration:none;} .footertext a:active { color: #c0c0c0; text-decoration:none;} .footertext a:hover {color: #fff; text-decoration: underline;} .footertext { text-align: center; padding: 60px 0 0 0; font-size: 8pt; left: -100px; font-family: trebuchet MS; color: #fff; position: relative; } .clearfix:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } div.progress_bar{ background-color:#4573a5; /*color:blue;*/ -moz-border-radius:5px; /*ff only setting*/ } + +div.progress_bar_outer { /*used in table-view*/ + width:150px; +} + td.progress_bar { white-space: nowrap; } td.info_label { font-weight: bold; } td { font-size: 10pt; color: #d1dae5; white-space: nowrap; } tr { + font-size: 10pt; + color: #d1dae5; +} + +div.panel { + padding:10px; + width:750px; + background-color: #37506f; + -moz-border-radius:10px; /*ff-only!*/ + margin-top:10px; + margin-bottom:10px; +} + + +/*New styles:*/ + +div.deluge_button { + display:inline; +} +form.deluge_button { + display:inline; +} +button.deluge_button { + background-color: #37506f; + border:1px solid #68a; + + background: #99acc3; + color: #000; + vertical-align:middle; + -moz-border-radius:7px; +} +button.deluge_button:hover { + background-color:#68a; +} +div.error { + background-color:#FFFFFF; + color:#AA0000; + font-weight:bold; + -moz-border-radius:10px; + width:200px; + margin-bottom:20px; + padding:10px; + +} + +/*tr.torrent_table:hover { + background-color:#68a; +}*/ + +tr.torrent_table_selected { + background-color:#900; +} + + +img.button { + margin-bottom:0px; + padding:0px; + position:relative; + top:2px; +} + +body.inner { + background:none; +} + + +form.pause_resume { + margin:0; + padding:0; + border:0; +} + +th { + background: #1f3044; + font-size: 14px; + border: 0px; + white-space: nowrap; +} + +#torrent_table { + border: #2a425c 1px solid; +} + + /* Hides from IE-mac \*/ * html .clearfix {height: 1%;} .clearfix {display: block;} /* End hide from IE-mac */ diff --git a/plugins/WebUi/templates/advanced/header.html b/plugins/WebUi/templates/advanced/header.html new file mode 100644 index 000000000..3d8f4cda3 --- /dev/null +++ b/plugins/WebUi/templates/advanced/header.html @@ -0,0 +1,28 @@ +$def with (title) + + + Deluge:$title + + + + + + + + + + + +
+ + + + +
+ +
+
diff --git a/plugins/WebUi/templates/advanced/index.html b/plugins/WebUi/templates/advanced/index.html new file mode 100644 index 000000000..bb84f9d52 --- /dev/null +++ b/plugins/WebUi/templates/advanced/index.html @@ -0,0 +1,147 @@ +$def with (torrent_list, all_torrents) +$:render.header(_('Torrent list')) + +
+ + + + + + + + + + + + + + + + + + $:category_tabs(all_torrents) + +
+ + +
+ + + + $:(sort_head('calc_state_str', 'S')) + $:(sort_head('queue_pos', '#')) + $:(sort_head('name', _('Name'))) + $:(sort_head('total_size', _('Size'))) + $:(sort_head('progress', _('Progress'))) + $if (not get('category')): + $:(sort_head('category', _('Tracker'))) + $:(sort_head('num_seeds', _('Seeders'))) + $:(sort_head('num_peers', _('Peers'))) + $:(sort_head('download_rate', _('Download'))) + $:(sort_head('upload_rate', _('Upload'))) + $:(sort_head('eta', _('Eta'))) + $:(sort_head('distributed_copies', _('Ava'))) + $:(sort_head('ratio', _('Ratio'))) + + + +$altrow(True) +$#4-space indentation is mandatory for for-loops in templetor! +$for torrent in torrent_list: + + + + + + + $if (not get('category')): + + + + + + + + + +
+
+ +
+
$torrent.queue_pos + $(crop(torrent.name, 40))$fsize(torrent.total_size) +
+
+ $torrent.message $int(torrent.progress) % +
+
+
$torrent.category$torrent.num_seeds ($torrent.total_seeds)$torrent.num_peers ($torrent.total_peers) + $if (torrent.download_rate): + $fspeed(torrent.download_rate) + $else: +   + + $if (torrent.upload_rate): + $fspeed(torrent.upload_rate) + $else: +   + $torrent.eta$("%.3f" % torrent.distributed_copies)$("%.3f" % torrent.ratio) +
+
+ + +$:part_stats() + +
+ +
+ +
+ + + + + +
+ +
+ + +$:render.footer() + diff --git a/plugins/WebUi/templates/advanced/part_categories.html b/plugins/WebUi/templates/advanced/part_categories.html new file mode 100644 index 000000000..3e8bbf806 --- /dev/null +++ b/plugins/WebUi/templates/advanced/part_categories.html @@ -0,0 +1,37 @@ +$def with (filter_tabs, category_tabs) +
+ + + + + + + +
+ + + + diff --git a/plugins/WebUi/templates/advanced/part_stats.html b/plugins/WebUi/templates/advanced/part_stats.html new file mode 100644 index 000000000..6ce594919 --- /dev/null +++ b/plugins/WebUi/templates/advanced/part_stats.html @@ -0,0 +1,36 @@ +$def with (stats) + + +
+ +$_('Auto refresh:') +$if getcookie('auto_refresh') == '1': + ($getcookie('auto_refresh_secs')) $_('seconds')   + $:render.part_button('GET', '/refresh/set', _('Set'), 'tango/preferences-system.png') + $:render.part_button('POST', '/refresh/off', _('Disable'), 'tango/process-stop.png') +$else: + $_('Off')   + $:render.part_button('POST', '/refresh/on', _('Enable'), 'tango/view-refresh.png') +$#end +
+ +
+ + + $_('Connections') : $stats.num_connections ($stats.max_num_connections) + + $_('Down Speed') : $stats.download_rate ($stats.max_download) + + $_('Up Speed') : $stats.upload_rate ($stats.max_upload) + + + + +
+ + + + + diff --git a/plugins/WebUi/templates/advanced/part_tb_button.html b/plugins/WebUi/templates/advanced/part_tb_button.html new file mode 100644 index 000000000..bc5ec9a21 --- /dev/null +++ b/plugins/WebUi/templates/advanced/part_tb_button.html @@ -0,0 +1,35 @@ +$def with (method, func, title, image='') +
+
+ +$if (get_config('button_style') == 0): + + +$if (get_config('button_style') == 1): + $if image: + + $else: + + +$if (get_config('button_style') == 2): + + +
+
+ \ No newline at end of file diff --git a/plugins/WebUi/templates/advanced/static/advanced.css b/plugins/WebUi/templates/advanced/static/advanced.css new file mode 100644 index 000000000..b4fa9db10 --- /dev/null +++ b/plugins/WebUi/templates/advanced/static/advanced.css @@ -0,0 +1,263 @@ +/* ----------------------------------------------------------- Theme Name: Simple Theme URI: http://deluge-torrent.org Description: Deluge Theme Version: 1.0 ----------------------------------------------------------- */ BODY { background: #304663 url(../../static/images/simple_bg.jpg) repeat-x; font-family: Bitstream Vera,Verdana; font-size: 10pt; margin: 0; + padding:0; + border:0; } /* GENERIC STYLES */ a img {border: 0px} hr {color: #627082; margin: 15px 0 15px 0;} +td {font-family: Bitstream Vera,Verdana;} +tr {font-family: Bitstream Vera,Verdana;} +table {font-family: Bitstream Vera,Verdana;} div {font-family: Bitstream Vera,Verdana;} /* STRUCTURE */ #page { min-width: 800px; margin-left: auto; margin-right: auto; + margin: 0; + padding:0; + font-family: Bitstream Vera,Verdana; } #main_content { background:url(../../static/images/simple_line.jpg) repeat-x; + margin: 0; + padding:0; } #simple_logo { background:url(../../static/images/simple_logo.jpg) no-repeat; } #main { + margin: 0; + padding:0; padding-top: 6px; color: #fff; } #main form table { border: #2a425c 1px solid; } #main form table tr { border: 0px; } #main form table tr th { background: #1f3044; font-size: 16px; border: 0px; + white-space: nowrap; } #main form table tr td{ border: 0px; color: #fff; font-size: 12px; white-space: nowrap; + font-family: Bitstream Vera,Verdana; } #main form table tr th a { color: #8fa6c3; font-size: 16px; white-space: nowrap; } #main form table tr th a, a:active, a:visited { color: #8fa6c3; text-decoration: none; } #main form table tr th a:hover {color: #fff; text-decoration: underline;} #main form table tr td a { color: #fff; font-size: 12px; white-space: nowrap; + font-family: Bitstream Vera,Verdana; } #main form table tr td a, a:active, a:visited { color: #fff; text-decoration: none;} #main form table tr td a:hover {color: #fff; text-decoration: underline;} #main a { color: #fff; font-size: 12px; } #main a, a:active, a:visited { color: #fff; text-decoration: none;} #main a:hover {color: #fff; text-decoration: underline;} .info { text-align: right; padding: 0 50px 0 0; color: #8fa6c3; font-size: 16px; letter-spacing: 4px; font-weight: bold; } .title { color: #dce4ee; font-size: 32px; padding: 10px 50px 0 0; text-align: right; } .title a, a:active, a:visited { color: #dce4ee; text-decoration: none;} .title a:hover {color: #fff; text-decoration: underline;} input{ + background-color: #37506f; + border:1px solid #68a; + + background: #99acc3; + color: #000; + /*vertical-align:middle;*/ + -moz-border-radius:5px; + /*margin-top:5px;*/ + } + +input:hover { + background-color:#68a; +} TEXTAREA{ border:1px solid #23344b; background: #99acc3; width:480px; } .footertext a { color: #c0c0c0; text-decoration:none;} .footertext a:visited { color: #c0c0c0; text-decoration:none;} .footertext a:active { color: #c0c0c0; text-decoration:none;} .footertext a:hover {color: #fff; text-decoration: underline;} .footertext { text-align: center; padding: 60px 0 0 0; font-size: 8pt; left: -100px; font-family: Bitstream Vera,Verdana; color: #fff; position: relative; } .clearfix:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } div.progress_bar{ background-color:#4573a5; /*color:blue;*/ -moz-border-radius:5px; /*ff only setting*/ } + +div.progress_bar_outer { /*used in table-view*/ + width:150px; +} + td.progress_bar { white-space: nowrap; } td.info_label { font-weight: bold; } td { font-size: 10pt; color: #d1dae5; white-space: nowrap; } tr { font-size: 10pt; color: #d1dae5; } + +div.panel { + padding:10px; + width:750px; + background-color: #37506f; + -moz-border-radius:10px; /*ff-only!*/ + margin-top:10px; + margin-bottom:10px; +} + + +/*New styles:*/ + +div.deluge_button { + display:inline; +} +form.deluge_button { + display:inline; +} +button.deluge_button { + background-color: #37506f; + border:1px solid #68a; + + background: #99acc3; + color: #000; + vertical-align:middle; + -moz-border-radius:7px; +} +button.deluge_button:hover { + background-color:#68a; +} +div.error { + background-color:#FFFFFF; + color:#AA0000; + font-weight:bold; + -moz-border-radius:10px; + width:200px; + margin-bottom:20px; + padding:10px; + +} + +tr.torrent_table:hover { + background-color:#68a; +} + +tr.altrow0:hover { + background-color:#68a; +} +tr.altrow1:hover { + background-color:#68a; +} + +tr.altrow1{ + background-color: #37506f; +} + + + +tr.torrent_table_selected { + background-color:#900; +} + +th.torrent_table:hover { + background-color:#68a; +} +th.torrent_table { + background-color: #37506f; +} + +img.button { + margin-bottom:0px; + padding:0px; + position:relative; + top:2px; +} + +body.inner { + background:none; +} + +#stats_panel { + -moz-border-radius:0px; + width:100%; + position:fixed; + bottom:0px; + left:0px; + background-color:#304663; + margin: 0; + padding:0; + text-align:left; + height:20px; + background-color:#ddd; + color:#000; + border-style:solid; + border:0; + border-top:1px; + border-color:#000; +} + +#about { + position:fixed; + bottom:0px; + right:10px; +} + +#info_panel_div2 { + position:fixed; + bottom:10px; + right:0px; + width:100%; + background-color:#304663; +} + +#refresh_panel { + -moz-border-radius:0px; + width:350px; + position:fixed; + bottom:0px; + right:0px; + background-color:#304663; + margin: 0; + padding:0; + text-align:right; + height:20px; + background-color:#ddd; + color:#000; + z-index:999; +} + +#refresh_panel button { + background-color:#304663; + color:#FFFFFF; + border:0; + position:relative; + top:0px; + height:20px; + background-color:#ddd; + color:#000; +} +#refresh_panel button:hover { + text-decoration: underline; +} + +#category_panel { + margin-bottom:0; + padding-bottom:0; + -moz-border-radius-bottomleft:0px; + -moz-border-radius-bottomright:0px; + padding-right:32px; +} + +#toolbar { + text-align:left; + margin-top:0; + padding-top:0; + margin-bottom: 30px; + -moz-border-radius-topleft:0px; + -moz-border-radius-topright:0px; + padding-top:5px; + padding-bottom:5px; + margin-bottom: 15px; + padding-left:32px; + height:20px; +} + +#toolbar select{ + /*border:1px solid #68a;*/ + border:0; + background-color: #37506f; + color: #FFF; +} +#toolbar select:hover{ + background-color:#68a; +} + +a.toolbar_btn { + width:20px; + height:20px; + padding-left:3px; + padding-top:7px; + padding-right:3px; + text-decoration: none; + margin-bottom:3px; +} +a.toolbar_btn:hover { + background-color:#68a; + -moz-border-radius:5px; + text-decoration: none; +} + + +#toolbar_refresh { + margin:0; + border:0; + background-color:none; + padding-left:2px; + padding-top:2px; + padding-right:2px; + text-decoration: none; + background-color: #37506f; + position:relative; + top:5px; +} +#toolbar_refresh:hover { + background-color:#68a; + -moz-border-radius:5px; + text-decoration: none; +} +#category_form{ + display:inline; + position:relative; + top:-3px; + padding-left:20px; +} + + +form { /*all forms!*/ + margin:0; + padding:0; + border:0; +} + +#torrent_list { + -moz-border-radius:7px; +} + /* Hides from IE-mac \*/ * html .clearfix {height: 1%;} .clearfix {display: block;} /* End hide from IE-mac */ + + diff --git a/plugins/WebUi/templates/advanced/static/deluge.js b/plugins/WebUi/templates/advanced/static/deluge.js new file mode 100644 index 000000000..da6be06e1 --- /dev/null +++ b/plugins/WebUi/templates/advanced/static/deluge.js @@ -0,0 +1,148 @@ +/* +all javascript is optional, everything should work web 1.0 +but javascript may/will enhance the experience. +i'm not a full time web-dev so don't expect beautifull patterns. +There's so much crap out there,i can't find good examples. +so i'd rather start from scratch, +Probably broken in an unexpected way , but worksforme. +*/ +state = { + 'row_js_continue':true + ,'selected_rows': new Array() +}; + +function $(el_id){ + return document.getElementById(el_id) +} +function get_row(id){ + return $('torrent_' + id); +} + +function on_click_row(e,id) { + /*filter out web 1.0 events for detail-link and pause*/ + if (state.row_js_continue) { + on_click_action(e,id); + } + state.row_js_continue = true; +} + +function on_click_row_js(e, id) { + /*real onClick event*/ + if (!e.ctrlKey) { + deselect_all_rows(); + select_row(id); + open_inner_details(id); + } + else if (state.selected_rows.indexOf(id) != -1) { + deselect_row(id); + } + else{ + select_row(id); + open_inner_details(id); + } +} + +function select_row(id){ + var row = get_row(id); + if (row) { + if (!(row.default_class_name)) { + row.default_class_name = row.className; + } + row.className = 'torrent_table_selected'; + state.selected_rows[state.selected_rows.length] = id; + setCookie('selected_rows',state.selected_rows); + } +} + +function deselect_row(id){ + var row = get_row(id); + if (row) { + row.className = row.default_class_name + /*remove from state.selected_rows*/ + var idx = state.selected_rows.indexOf(id); + state.selected_rows.splice(idx,1); + setCookie('selected_rows',state.selected_rows); + } +} + +function deselect_all_rows(){ + /*unbind state.selected_rows from for..in: + there must be a better way to do this*/ + var a = new Array() + for (i in state.selected_rows) { + a[a.length] = state.selected_rows[i]; + } + for (i in a){ + deselect_row(a[i]); + } +} + +function reselect_rows(){ + var selected_rows = getCookie('selected_rows').split(','); + for (i in getCookie('selected_rows')) { + select_row(selected_rows[i]); + } +} + +function open_details(e, id){ + alert(id); + window.location.href = '/torrent/info/' + id; +} + +function open_inner_details(id){ + /*probably broken for IE, use FF!*/ + $('torrent_info').src = '/torrent/info_inner/' + id; +} + +function on_click_do_nothing(e, id){ +} + +on_click_action = on_click_do_nothing; + +/*toobar buttons, */ +function toolbar_post(url, selected) { + if ((!selected) || (state.selected_rows.length > 0)) { + var ids = state.selected_rows.join(','); + var form = $('toolbar_form'); + form.action = url +ids; + form.submit(); + } + return false; +} + +function toolbar_get(url , selected) { + if (!selected) { + window.location.href = url + } + else if (state.selected_rows.length > 0) { + var ids = state.selected_rows.join(','); + window.location.href = url +ids; + } + return false; +} + +/*stuff copied from various places:*/ +/*http://www.w3schools.com/js/js_cookies.asp*/ +function setCookie(c_name,value,expiredays) +{ + var exdate=new Date() + exdate.setDate(exdate.getDate()+expiredays) + document.cookie=c_name+ "=" +escape(value)+ + ((expiredays==null) ? "" : ";expires="+exdate.toGMTString()) +} + +function getCookie(c_name) +{ +if (document.cookie.length>0) + { + c_start=document.cookie.indexOf(c_name + "=") + if (c_start!=-1) + { + c_start=c_start + c_name.length+1 + c_end=document.cookie.indexOf(";",c_start) + if (c_end==-1) c_end=document.cookie.length + return unescape(document.cookie.substring(c_start,c_end)) + } + } +return "" +} \ No newline at end of file diff --git a/plugins/WebUi/templates/advanced/static/scrolling_table.css b/plugins/WebUi/templates/advanced/static/scrolling_table.css new file mode 100644 index 000000000..b046bd9c4 --- /dev/null +++ b/plugins/WebUi/templates/advanced/static/scrolling_table.css @@ -0,0 +1,106 @@ +/*Taken from: +http://www.imaputz.com/cssStuff/bigFourVersion.html +*/ + +/* define height and width of scrollable area. Add 16px to width for scrollbar */ +div.tableContainer { + clear: both; + /*border: 1px solid #963;*/ + height: 285px; + overflow: auto; + width: 756px; +} + +/* Reset overflow value to hidden for all non-IE browsers. */ +html>body div.tableContainer { + overflow: hidden; + width: 756px +} + +/* define width of table. IE browsers only */ +div.tableContainer table { + float: left; + width: 740px; +} + +/* define width of table. Add 16px to width for scrollbar. */ +/* All other non-IE browsers. */ +html>body div.tableContainer table { + width: 756px +} + +/* set table header to a fixed position. WinIE 6.x only */ +/* In WinIE 6.x, any element with a position property set to relative and is a child of */ +/* an element that has an overflow property set, the relative value translates into fixed. */ +/* Ex: parent element DIV with a class of tableContainer has an overflow property set to auto */ +thead.fixedHeader tr { + position: relative +} + +/* set THEAD element to have block level attributes. All other non-IE browsers */ +/* this enables overflow to work on TBODY element. All other non-IE, non-Mozilla browsers */ +html>body thead.fixedHeader tr { + display: block +} + +/* define the table content to be scrollable */ +/* set TBODY element to have block level attributes. All other non-IE browsers */ +/* this enables overflow to work on TBODY element. All other non-IE, non-Mozilla browsers */ +/* induced side effect is that child TDs no longer accept width: auto */ +html>body tbody.scrollContent { + display: block; + height: 262px; + overflow: auto; + width: 100% +} + +/* make TD elements pretty. Provide alternating classes for striping the table */ +/* http://www.alistapart.com/articles/zebratables/ */ +tbody.scrollContent td, tbody.scrollContent tr.normalRow td { + /*background: #FFF;*/ + + border-bottom: none; + border-left: none; + /*border-right: 1px solid #CCC; + border-top: 1px solid #DDD;*/ + padding: 2px 3px 3px 4px +} + +tbody.scrollContent tr.alternateRow td { + /*background: #EEE;*/ + border-bottom: none; + border-left: none; + /*border-right: 1px solid #CCC; + border-top: 1px solid #DDD;*/ + padding: 2px 3px 3px 4px +} + +/* define width of TH elements: 1st, 2nd, and 3rd respectively. */ +/* Add 16px to last TH for scrollbar padding. All other non-IE browsers. */ +/* http://www.w3.org/TR/REC-CSS2/selector.html#adjacent-selectors */ +html>body thead.fixedHeader th { + width: 200px +} + +html>body thead.fixedHeader th + th { + width: 240px +} + +html>body thead.fixedHeader th + th + th { + width: 316px +} + +/* define width of TD elements: 1st, 2nd, and 3rd respectively. */ +/* All other non-IE browsers. */ +/* http://www.w3.org/TR/REC-CSS2/selector.html#adjacent-selectors */ +html>body tbody.scrollContent td { + width: 200px +} + +html>body tbody.scrollContent td + td { + width: 240px +} + +html>body tbody.scrollContent td + td + td { + width: 300px +} \ No newline at end of file diff --git a/plugins/WebUi/templates/advanced/torrent_info_inner.html b/plugins/WebUi/templates/advanced/torrent_info_inner.html new file mode 100644 index 000000000..907bfcf55 --- /dev/null +++ b/plugins/WebUi/templates/advanced/torrent_info_inner.html @@ -0,0 +1,15 @@ +$def with (torrent) + + + + + Deluge:$torrent.name + + + + + + +$:render.tab_meta(torrent) + +$:render.footer() diff --git a/plugins/WebUi/templates/deluge/about.html b/plugins/WebUi/templates/deluge/about.html new file mode 100644 index 000000000..b34b7877a --- /dev/null +++ b/plugins/WebUi/templates/deluge/about.html @@ -0,0 +1,49 @@ +$:render.header(_('About')) +
+

Version

+
$version 
+

Links

+ + +

Authors

+
    +

    WebUi

    +
      +
    • Martijn Voncken
    • +
    + +

    Template

    +
      +
    • Martijn Voncken
    • +
    • somedude
    • +
    + +

    Deluge

    +
      +
    • Zach Tibbitts
    • +
    • Alon Zakai
    • + +
    • Alon Zakai
    • +
    • Marcos Pinto
    • +
    • Andrew Resch
    • +
    • Alex Dedul
    • +
    + +

    Windows Port

    +
      +
    • Slurdge
    • +
    + +
+*and all other authors/helpers/contributors I forgot to mention. +
+ +$:render.footer() diff --git a/plugins/WebUi/templates/deluge/authors.txt b/plugins/WebUi/templates/deluge/authors.txt new file mode 100644 index 000000000..878772037 --- /dev/null +++ b/plugins/WebUi/templates/deluge/authors.txt @@ -0,0 +1,5 @@ +-first layout taken from deluge website +improved by: +-mvoncken +-somedude + diff --git a/plugins/WebUi/templates/deluge/config.html b/plugins/WebUi/templates/deluge/config.html new file mode 100644 index 000000000..e2670d0ae --- /dev/null +++ b/plugins/WebUi/templates/deluge/config.html @@ -0,0 +1,10 @@ +$def with (form) +$:render.header(_('Config')) + +
Not Implemented!
+
+$:form.render() + +
+ +$:render.footer() diff --git a/plugins/WebUi/templates/deluge/error.html b/plugins/WebUi/templates/deluge/error.html new file mode 100644 index 000000000..002cf3fc2 --- /dev/null +++ b/plugins/WebUi/templates/deluge/error.html @@ -0,0 +1,6 @@ +$def with (error_msg) +$:render.header(_('Error')) +
+    $error_msg
+
+$:render.footer() diff --git a/plugins/WebUi/templates/deluge/footer.html b/plugins/WebUi/templates/deluge/footer.html new file mode 100644 index 000000000..ca03a0154 --- /dev/null +++ b/plugins/WebUi/templates/deluge/footer.html @@ -0,0 +1,6 @@ +
+
+
+
+ + diff --git a/plugins/WebUi/templates/deluge/header.html b/plugins/WebUi/templates/deluge/header.html new file mode 100644 index 000000000..42cc62eed --- /dev/null +++ b/plugins/WebUi/templates/deluge/header.html @@ -0,0 +1,23 @@ +$def with (title) + + + Deluge:$title + + + + + + + +
+ + + + +
+ +
+
diff --git a/plugins/WebUi/templates/deluge/index.html b/plugins/WebUi/templates/deluge/index.html new file mode 100644 index 000000000..4c29278fc --- /dev/null +++ b/plugins/WebUi/templates/deluge/index.html @@ -0,0 +1,61 @@ +$def with (torrent_list, all_torrents) +$:render.header(_('Torrent list')) + + + + $:(sort_head('calc_state_str', 'S')) + $:(sort_head('queue_pos', '#')) + $:(sort_head('name', _('Name'))) + $:(sort_head('total_size', _('Size'))) + $:(sort_head('progress', _('Progress'))) + $:(sort_head('num_seeds', _('Seeders'))) + $:(sort_head('num_peers', _('Peers'))) + $:(sort_head('download_rate', _('Download'))) + $:(sort_head('upload_rate', _('Upload'))) + $:(sort_head('eta', _('Eta'))) + $:(sort_head('distributed_copies', _('Ava'))) + $:(sort_head('ratio', _('Ratio'))) + +$#4-space indentation is mandatory for for-loops in templetor! +$for torrent in torrent_list: + + + + + + + + + + + + + +
+
+
$torrent.queue_pos + + $(crop(torrent.name, 40))$fsize(torrent.total_size) +
+
+ $torrent.message $int(torrent.progress) % +
+
+
$torrent.num_seeds ($torrent.total_seeds)$torrent.num_peers ($torrent.total_peers)$fspeed(torrent.download_rate)$fspeed(torrent.upload_rate)$torrent.eta$("%.3f" % torrent.distributed_copies)$("%.3f" % torrent.ratio) +
+ + + +
+$:render.part_button('GET', '/torrent/add', _('Add torrent'), 'tango/list-add.png') +$:render.part_button('POST', '/pause_all', _('Pause all'), 'tango/pause.png') +$:render.part_button('POST', '/resume_all', _('Resume all'), 'tango/start.png') + +
+ +$:part_stats() + +$:render.footer() + diff --git a/plugins/WebUi/templates/deluge/login.html b/plugins/WebUi/templates/deluge/login.html new file mode 100644 index 000000000..0436e9076 --- /dev/null +++ b/plugins/WebUi/templates/deluge/login.html @@ -0,0 +1,25 @@ +$def with (error) +$:render.header(_('Login')) +
+$if error > 0: +
$_("Password is invalid,try again")
+ +
+ +
+
+ $_('Password') + +
+
+ + +
+ +
+ $_('About') +
+
+
+$:render.footer() diff --git a/plugins/WebUi/templates/deluge/part_button.html b/plugins/WebUi/templates/deluge/part_button.html new file mode 100644 index 000000000..8c420560f --- /dev/null +++ b/plugins/WebUi/templates/deluge/part_button.html @@ -0,0 +1,26 @@ +$def with (method, url, title, image='') +
+
+ +$if (get_config('button_style') == 0): + + +$if (get_config('button_style') == 1): + $if image: + + $else: + + +$if (get_config('button_style') == 2): + + +
+
diff --git a/plugins/WebUi/templates/deluge/part_stats.html b/plugins/WebUi/templates/deluge/part_stats.html new file mode 100644 index 000000000..342b8049f --- /dev/null +++ b/plugins/WebUi/templates/deluge/part_stats.html @@ -0,0 +1,35 @@ +$def with (stats) + + +
+ +$_('Auto refresh:') +$if getcookie('auto_refresh') == '1': + ($getcookie('auto_refresh_secs')) $_('seconds')   + $:render.part_button('GET', '/refresh/set', _('Set'), 'tango/preferences-system.png') + $:render.part_button('POST', '/refresh/off', _('Disable'), 'tango/process-stop.png') +$else: + $_('Off')   + $:render.part_button('POST', '/refresh/on', _('Enable'), 'tango/view-refresh.png') +$#end +
+ +
+ + + $_('Connections') : $stats.num_connections ($stats.max_num_connections) + + $_('Down Speed') : $stats.download_rate ($stats.max_download) + + $_('Up Speed') : $stats.upload_rate ($stats.max_upload) + + + + + + ($_('About')) + + +
+ + diff --git a/plugins/WebUi/templates/deluge/refresh_form.html b/plugins/WebUi/templates/deluge/refresh_form.html new file mode 100644 index 000000000..0ac2cffda --- /dev/null +++ b/plugins/WebUi/templates/deluge/refresh_form.html @@ -0,0 +1,11 @@ +$:render.header(_('Set Timeout')) +
+
+ $_('Refresh page every:') + + $_('seconds') + +
+
+$:render.footer() diff --git a/plugins/WebUi/templates/deluge/sort_column_head.html b/plugins/WebUi/templates/deluge/sort_column_head.html new file mode 100644 index 000000000..d354a6c4f --- /dev/null +++ b/plugins/WebUi/templates/deluge/sort_column_head.html @@ -0,0 +1,12 @@ +$def with (column_id, column_name, order, active_up, active_down) + + +$column_name\ +$if active_up: + +$if active_down: + + + + + diff --git a/plugins/WebUi/templates/deluge/tab_meta.html b/plugins/WebUi/templates/deluge/tab_meta.html new file mode 100644 index 000000000..e7f7f4bf2 --- /dev/null +++ b/plugins/WebUi/templates/deluge/tab_meta.html @@ -0,0 +1,85 @@ +$def with (torrent) + + +
+ +
+ + $torrent.progress %
+
+ + + + + + + + + + + + + + + + + + + + + + + +
$_('Downloaded'):$torrent.calc_total_downloaded
$_('Uploaded'):$torrent.calc_total_uploaded
$_('Seeders'):$torrent.num_seeds ($torrent.total_seeds )
$_('Share Ratio'):$("%.3f" % torrent.ratio)
$_('Pieces'):$torrent.num_pieces x $fsize(torrent.piece_length)
  
+
+ + + + + + + + + + + + + + + + + + + + + +
$_('Speed'): +$fspeed(torrent.download_rate)
$_('Speed'):$fspeed(torrent.upload_rate)
$_('Peers'):$torrent.num_peers ($torrent.total_peers )
$_('ETA'):$torrent.eta
$_('Availability'):$("%.3f" % torrent.distributed_copies)
  
+ +
+ + + + + + + + + + + + + + + + + + + + + + +
$_('Total Size'):$fspeed(torrent.total_size)
$_('# Of Files'):$torrent.num_files
$_('Tracker'):$(crop(torrent.tracker, 30))
$_('Tracker Status'):$(crop(torrent.tracker_status, 30))
$_('Next Announce'):$torrent.next_announce
$_('Queue Position'):$torrent.queue_pos
+ +
diff --git a/plugins/WebUi/templates/deluge/torrent_add.html b/plugins/WebUi/templates/deluge/torrent_add.html new file mode 100644 index 000000000..0ceb15aae --- /dev/null +++ b/plugins/WebUi/templates/deluge/torrent_add.html @@ -0,0 +1,23 @@ +$:render.header(_("Add Torrent")) +
+
+ +
+
+ $_('Url') + +
+
+ $_('Upload torrent') + +
+
+ + +
+
+
+
+$:render.footer() diff --git a/plugins/WebUi/templates/deluge/torrent_delete.html b/plugins/WebUi/templates/deluge/torrent_delete.html new file mode 100644 index 000000000..9c1c89e69 --- /dev/null +++ b/plugins/WebUi/templates/deluge/torrent_delete.html @@ -0,0 +1,32 @@ +$def with (torrent_ids, torrent_list) +$:render.header(_("Remove torrent")) +
+
+ +
+ +

$_("Remove torrent")

+
    +$for torrent in torrent_list: +
  • $torrent.name
  • +
+ +
+ + $_('Delete .torrent file') +
+
+ + $_('Delete downloaded files.') +
+
+ + +
+
+
+
+$:render.footer() \ No newline at end of file diff --git a/plugins/WebUi/templates/deluge/torrent_info.html b/plugins/WebUi/templates/deluge/torrent_info.html new file mode 100644 index 000000000..660273e1a --- /dev/null +++ b/plugins/WebUi/templates/deluge/torrent_info.html @@ -0,0 +1,50 @@ +$def with (torrent) + +$:(render.header(torrent.message + '/' + torrent.name)) +
+

$_('Details')

+ +$:render.tab_meta(torrent) + +$if (torrent.action == 'start'): + $:render.part_button('POST', '/torrent/start/' + str(torrent.id), _('Resume'), 'tango/start.png') +$else: + $:render.part_button('POST', '/torrent/stop/' + str(torrent.id), _('Pause'), 'tango/pause.png') + + +$:render.part_button('GET', '/torrent/delete/' + str(torrent.id), _('Remove'), 'tango/list-remove.png') +$:render.part_button('POST', '/torrent/reannounce/' + str(torrent.id), _('Reannounce'), 'tango/view-refresh.png') + +$:render.part_button('POST', '/torrent/queue/up/' + str(torrent.id), _('Queue Up'), 'tango/queue-up.png') +$:render.part_button('POST', '/torrent/queue/down/' + str(torrent.id), _('Queue Down'), 'tango/queue-down.png') + +
+ + +
+ + + + +$:part_stats() + +$:render.footer() diff --git a/plugins/WebUi/templates/hacking-templates.txt b/plugins/WebUi/templates/hacking-templates.txt new file mode 100644 index 000000000..600ba907e --- /dev/null +++ b/plugins/WebUi/templates/hacking-templates.txt @@ -0,0 +1,39 @@ +Quickstart: +Just copy and rename an existing template. +-The settings panel will see all directory's in this folder ,and let you choose your new template. +-Clicking Ok in the settings panel will restart the webserver and reload your template. + +Limited "Subclassing": +All templates are "subclassed" from the /deluge/ template. +If a html file is not found in the template dir, the file from /deluge/ will be used. + + +Notes: +Please configure your editor to use 4-space indents instead of tabs. +Or use scite and my config: http://mvoncken.sohosted.com/deluge/SciTEUser.properties.txt + +template language: http://webpy.org/templetor + +Exposed methods and variables (c&p from webserver_framework.py): + template.Template.globals.update({ + 'sort_head': template_sort_head, + 'part_stats':template_part_stats, + 'crop': template_crop, + '_': _ , #gettext/translations + 'str': str, #because % in templetor is broken. + 'sorted': sorted, + 'get_config': get_config, + 'self_url': self_url, + 'fspeed': common.fspeed, + 'fsize': common.fsize, + 'render': ws.render, #for easy resuse of templates + 'rev': 'rev.%s' % (REVNO, ), + 'version': VERSION, + 'getcookie':getcookie, + 'get': lambda (var): getattr(web.input(**{var:None}), var) # unreadable :-( +}) + +I will update this file if there is interest in making templates. + + + diff --git a/plugins/WebUi/tests/test_all.py b/plugins/WebUi/tests/test_all.py new file mode 100644 index 000000000..f6c750fea --- /dev/null +++ b/plugins/WebUi/tests/test_all.py @@ -0,0 +1,382 @@ +""" +Testing the REST api, not the units. +unittest the right way feels so unpythonic :( +!! BIG FAT WARNING !!: this test deletes active torrents . +!! BIG FAT WARNING 2!!: this test hammers the tracker that is tested against. +""" +import unittest +import cookielib, urllib2 , urllib +import WebUi.webserver_common as ws +import operator + + +ws.init_05() +print 'test-env=',ws.ENV + + + +#CONFIG: +BASE_URL = 'http://localhost:8112' +PWD = 'deluge' + +def get_status(id): + return ws.proxy.get_torrent_status(id,ws.TORRENT_KEYS) + +#BASE: +#303 = see other +#404 = not found +#500 = server error +#200 = OK, page exists. +class TestWebUiBase(unittest.TestCase): + def setUp(self): + #cookie aware-opener that DOES NOT use redirects. + opener = urllib2.OpenerDirector() + self.cj = cookielib.CookieJar() + for handler in [urllib2.HTTPHandler(),urllib2.HTTPDefaultErrorHandler(), + urllib2.FileHandler(),urllib2.HTTPErrorProcessor(), + urllib2.HTTPCookieProcessor(self.cj)]: + opener.add_handler(handler) + #/opener + self.opener = opener + + def open_url(self, page, post=None): + url = BASE_URL + page + + if post == 1: + post = {'Force_a_post' : 'spam'} + if post: + post = urllib.urlencode(post) + r = self.opener.open(url , data = post) + + + #BUG: error-page does not return status 500, but status 200 + #workaround... + data = r.read() + if '' in data: + error = IOError() + error.code = 500 + #print data + raise error + if r.code <> 200: + fail('no code 200, error-code=%s' % r.code) + return r + + def get_cookies(self): + return dict((c.name,c.value) for c in self.cj) + cookies = property(get_cookies) + + def assert_status(self,status, page, post): + try : + r = self.open_url(page, post) + except IOError,e: + self.assertEqual(e.code, status) + else: + self.fail('page was found "%s" (%s)' % (page, r.code )) + + def assert_404(self, page, post = None): + self.assert_status(404, page, post) + + def assert_500(self, page, post = None): + self.assert_status(500, page, post) + + def assert_303(self, page, redirect_to, post=None): + try : + r = self.open_url(page, post) + except IOError,e: + self.assertEqual(e.code, 303) + self.assertEqual(e.headers['Location'], redirect_to) + else: + #print r + self.fail('No 303!') + + def assert_exists(self, page, post = None): + try : + r = self.open_url(page, post) + except IOError,e: + self.fail('page was not found "%s" (%s)' % (page, e.code)) + else: + pass + + first_torrent_id = property(lambda self: ws.proxy.get_session_state()[0]) + first_torrent = property(lambda self: get_status(self.first_torrent_id)) + + +class TestNoAuth(TestWebUiBase): + def test303(self): + self.assert_303('/','/login') + self.assert_303('','/login') + self.assert_303('/index','/login') + #self.assert_303('/torrent/pause/','/login') + self.assert_303('/config','/login') + self.assert_303('/torrent/info/','/login') + + def test404(self): + self.assert_404('/torrent/info') + self.assert_404('/garbage') + #self.assert_404('/static/garbage') + #self.assert_404('/template/static/garbage') + self.assert_404('/torrent/pause/', post=1) + + def testOpen(self): + self.assert_exists('/login') + self.assert_exists('/about') + + def testStatic(self): + self.assert_exists('/static/images/simple_line.jpg') + self.assert_exists('/static/images/tango/up.png') + #test 404 + + #test template-static + + + +class TestSession(TestWebUiBase): + def testLogin(self): + self.assert_303('/home','/login') + #invalid pwd: + self.assert_303('/login','/login?error=1',{'pwd':'invalid'}) + #login + self.assert_303('/login','/index',{'pwd':PWD}) + #now i'm logged-in! + #there are no sort-coockies yet so the default page is /index. + self.assert_303('/home','/index') + self.assert_exists('/index') + self.assert_exists('/config') + self.assert_exists('/torrent/add') + self.assert_303('/','/index') + self.assert_303('','/index') + + #logout + self.assert_303('/logout','/login', post=1) + #really logged out? + self.assert_303('/','/login') + self.assert_303('','/login') + self.assert_303('/index','/login') + self.assert_303('/torrent/add','/login') + self.assert_exists('/about') + + + def testRefresh(self): + #starting pos + self.assert_303('/login','/index',{'pwd':PWD}) + r = self.open_url('/index') + assert not 'auto_refresh' in self.cookies + assert not 'auto_refresh_secs' in self.cookies + assert not r.headers.has_key('Refresh') + + #on: + self.assert_303('/refresh/on','/index', post=1) + + assert 'auto_refresh' in self.cookies + assert 'auto_refresh_secs' in self.cookies + self.assertEqual(self.cookies['auto_refresh'],'1') + self.assertEqual(self.cookies['auto_refresh_secs'],'10') + + r = self.open_url('/index') + assert r.headers['Refresh'] == '10 ; url=/index' + + #set: + self.assert_303('/refresh/set','/index',{'refresh':'5'}) + self.assertEqual(self.cookies['auto_refresh_secs'],'5') + + r = self.open_url('/index') + assert r.headers['Refresh'] == '5 ; url=/index' + self.assert_500('/refresh/set',{'refresh':'a string'}) + + #off: + self.assert_303('/refresh/off','/index', post=1) + self.assertEqual(self.cookies['auto_refresh'],'0') + self.assertEqual(self.cookies['auto_refresh_secs'],'5') + + r = self.open_url('/index') + assert not 'Refresh' in r.headers + +class TestIntegration(TestWebUiBase): + initialized = False + def setUp(self): + TestWebUiBase.setUp(self) + + self.assert_303('/login','/index',{'pwd':PWD}) + self.urls = sorted([ + 'http://torrents.aelitis.com:88/torrents/azplatform2_1.13.zip.torrent', + 'http://torrents.aelitis.com:88/torrents/azplugins_2.1.4.jar.torrent', + 'http://torrents.aelitis.com:88/torrents/azautoseeder_0.1.1.jar.torrent' + ]) + + torrent_ids = ws.proxy.get_session_state() + + #avoid hammering, investigate current torrent-list and do not re-add. + #correct means : 3 torrent's in list (for now) + if len(torrent_ids) <> 3: + #delete all, nice use case for refactoring delete.. + torrent_ids = ws.proxy.get_session_state() + for torrent in torrent_ids: + ws.proxy.remove_torrent([torrent], False, False) + + torrent_ids = ws.proxy.get_session_state() + self.assertEqual(torrent_ids, []) + + #add 3 using url. + for url in self.urls: + self.assert_303('/torrent/add','/index',{'url':url,'torrent':None}) + + #added? + self.torrent_ids = ws.proxy.get_session_state() + self.assertEqual(len(self.torrent_ids), 3) + + else: + #test correctness of existing-list + #The setup makes 0.6 fail everything, added an else.. + for url in self.urls: + if ws.ENV.startswith('0.5'): + self.assert_500('/torrent/add',{'url':url,'torrent':None}) + else: + self.assert_303('/torrent/add','/index',{'url':url,'torrent':None}) + + def testPauseResume(self): + #pause all + self.assert_303('/pause_all','/index', post=1) + #pause worked? + pause_status = [get_status(id)["user_paused"] for id in ws.proxy.get_session_state()] + for paused in pause_status: + self.assertEqual(paused, True) + + #resume all + self.assert_303('/resume_all','/index', post=1) + #resume worked? + pause_status = [get_status(id)["user_paused"] for id in ws.proxy.get_session_state()] + for paused in pause_status: + self.assertEqual(paused,False) + #pause again. + self.assert_303('/pause_all','/index', post=1) + + torrent_id = self.first_torrent_id + #single resume. + self.assert_303('/torrent/start/%s' % torrent_id ,'/index', post=1) + self.assertEqual(get_status(torrent_id)["user_paused"] ,False) + #single pause + self.assert_303('/torrent/stop/%s' % torrent_id,'/index', post=1) + self.assertEqual(get_status(torrent_id)["user_paused"] , True) + + def testQueue(self): + #find last: + torrent_id = [id for id in ws.proxy.get_session_state() + if (get_status(id)['queue_pos'] ==3 )][0] + + #queue + torrent = get_status(torrent_id) + self.assertEqual(torrent['queue_pos'], 3) + #up: + self.assert_303('/torrent/queue/up/%s' % torrent_id,'/index', post=1) + torrent = get_status(torrent_id) + self.assertEqual(torrent['queue_pos'], 2) + self.assert_303('/torrent/queue/up/%s' % torrent_id,'/index', post=1) + torrent = get_status(torrent_id) + self.assertEqual(torrent['queue_pos'], 1) + self.assert_303('/torrent/queue/up/%s' % torrent_id,'/index', post=1) + #upper limit + torrent = get_status(torrent_id) + self.assertEqual(torrent['queue_pos'], 1) + #down: + self.assert_303('/torrent/queue/down/%s' % torrent_id,'/index', post=1) + torrent = get_status(torrent_id) + self.assertEqual(torrent['queue_pos'], 2) + self.assert_303('/torrent/queue/down/%s' % torrent_id,'/index', post=1) + torrent = get_status(torrent_id) + self.assertEqual(torrent['queue_pos'], 3) + self.assert_303('/torrent/queue/down/%s' % torrent_id,'/index', post=1) + #down limit + torrent = get_status(torrent_id) + self.assertEqual(torrent['queue_pos'], 3) + + def testMeta(self): + #info available? + for torrent_id in ws.proxy.get_session_state(): + self.assert_exists('/torrent/info/%s' % torrent_id) + self.assert_exists('/torrent/delete/%s' % torrent_id) + + #no info: + self.assert_500('/torrent/info/99999999') + self.assert_500('/torrent/delete/99999999') + + def testAddRemove(self): + #add a duplicate: + self.assert_500('/torrent/add', post={'url':self.urls[0],'torrent':None}) + + #add a 4th using url + + #delete + + #add torrrent-file + #./test01.torrent + + + def test_do_redirect(self): + self.assert_303('/home','/index') + #1 + self.assert_exists('/index?sort=download_rate&order=down') + self.assert_303('/home','/index?sort=download_rate&order=down') + assert self.cookies['sort'] == 'download_rate' + assert self.cookies['order'] == 'down' + #2 + self.assert_exists('/index?sort=progress&order=up') + self.assert_303('/home','/index?sort=progress&order=up') + assert self.cookies['sort'] == 'progress' + assert self.cookies['order'] == 'up' + #redir after pause-POST? in /index. + self.assert_exists('/index?sort=name&order=down') + torrent_id = self.first_torrent_id + self.assert_303('/torrent/stop/%s' % torrent_id, + '/index?sort=name&order=down', post=1) + #redir in details 1 + self.assert_303('/torrent/stop/%s?redir=/torrent/info/%s' %(torrent_id,torrent_id) + ,'/torrent/info/' + torrent_id, post = 1) + #redir in details 2 + self.assert_303('/torrent/stop/%s' % torrent_id + ,'/torrent/info/' + torrent_id , + post={'redir': '/torrent/info/' + torrent_id}) + + def testRemote(self): + pass + + def test_redir_after_login(self): + pass + + def testReannounce(self): + torrent_id = self.first_torrent_id + self.assert_303( + '/torrent/reannounce/%(id)s?redir=/torrent/info/%(id)s' + % {'id':torrent_id} + ,'/torrent/info/' + torrent_id, post = 1) + + def testRecheck(self): + #add test before writing code.. + #RELEASE-->disable + """ + torrent_id = self.first_torrent_id + self.assert_303( + '/torrent/recheck/%(id)s?redir=/torrent/info/%(id)s' + % {'id':torrent_id} + ,'/torrent/info/' + torrent_id, post = 1) + """ + + + +# + +if False: + suiteFew = unittest.TestSuite() + + suiteFew.addTest(TestSession("testRefresh")) + + unittest.TextTestRunner(verbosity=2).run(suiteFew) + +elif False: + suiteFew = unittest.TestSuite() + suiteFew.addTest(TestIntegration("testDoRedirect")) + unittest.TextTestRunner(verbosity=2).run(suiteFew) + + +else: + unittest.main() + diff --git a/plugins/WebUi/webserver_common.py b/plugins/WebUi/webserver_common.py new file mode 100644 index 000000000..a169159fc --- /dev/null +++ b/plugins/WebUi/webserver_common.py @@ -0,0 +1,225 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright (C) Martijn Voncken 2007 +# +# 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 +# the Free Software Foundation; either version 2, or (at your option) +# any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, write to: +# The Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor +# Boston, MA 02110-1301, USA. +# +# In addition, as a special exception, the copyright holders give +# permission to link the code of portions of this program with the OpenSSL +# library. +# You must obey the GNU General Public License in all respects for all of +# the code used other than OpenSSL. If you modify file(s) with this +# exception, you may extend this exception to your version of the file(s), +# but you are not obligated to do so. If you do not wish to do so, delete +# this exception statement from your version. If you delete this exception +# statement from all source files in the program, then also delete it here. + +""" +initializes config,render and proxy. +All hacks go here, so this is a really ugly source-file.. +Support running in process0.5 ,run inside-gtk0.5 and run in process0.6 +""" + +import os +import deluge +import random +import pickle +import sys +import base64 +from lib.webpy022 import template + +random.seed() +webui_path = os.path.dirname(__file__) +ENV = 'UNKNOWN' +config_defaults = { + "port":8112, + "button_style":2, + "auto_refresh":False, + "auto_refresh_secs": 10, + "template":"advanced", + "pwd_salt":"2540626806573060601127357001536142078273646936492343724296134859793541603059837926595027859394922651189016967573954758097008242073480355104215558310954", + "pwd_md5":"\xea\x8d\x90\x98^\x9f\xa9\xe2\x19l\x7f\x1a\xca\x82u%", + "cache_templates":False, + "use_https":False +} + +try: + _('translate something') +except: + import gettext + gettext.install('~/') + #log.error('no translations :(') + +try: + config_dir = deluge.common.CONFIG_DIR +except: + config_dir = os.path.expanduser("~/.config/deluge") + +config_file = os.path.join(config_dir,'webui.conf') +session_file = os.path.join(config_dir,'webui.sessions') + + +class subclassed_render(object): + """ + try to use the html template in configured dir. + not available : use template in /deluge/ + """ + def __init__(self, template_dirname, cache=False): + self.base_template = template.render( + os.path.join(webui_path, 'templates/deluge/'), + cache=cache) + + self.sub_template = template.render( + os.path.join(webui_path, 'templates/%s/' % template_dirname), + cache=cache) + + def __getattr__(self, attr): + if hasattr(self.sub_template, attr): + return getattr(self.sub_template, attr) + else: + return getattr(self.base_template, attr) + +def init_process(): + globals()['config'] = pickle.load(open(config_file)) + globals()['render'] = subclassed_render(config.get('template'), + config.get('cache_templates')) + +def init_06(): + import deluge.ui.client as proxy + from deluge.log import LOG as log + globals()['log'] = log + + proxy.set_core_uri('http://localhost:58846') #How to configure this? + + def add_torrent_filecontent(name , data_b64): + log.debug('monkeypatched add_torrent_filecontent:%s,len(data:%s))' % + (name , len(data_b64))) + + name = name.replace('\\','/') + name = 'deluge06_' + str(random.random()) + '_' + name.split('/')[-1] + filename = os.path.join('/tmp', name) + + log.debug('write: %s' % filename) + f = open(filename,"wb") + f.write(base64.b64decode(data_b64)) + f.close() + + proxy.add_torrent_file([filename]) + + + + + proxy.add_torrent_filecontent = add_torrent_filecontent + log.debug('cfg-file %s' % config_file) + if not os.path.exists(config_file): + log.debug('create cfg file %s' % config_file) + #load&save defaults. + f = file(config_file,'wb') + pickle.dump(config_defaults,f) + f.close() + + init_process() + globals()['proxy'] = proxy + globals()['ENV'] = '0.6' + + + +def init_05(): + import dbus + init_process() + bus = dbus.SessionBus() + proxy = bus.get_object("org.deluge_torrent.dbusplugin" + , "/org/deluge_torrent/DelugeDbusPlugin") + + globals()['proxy'] = proxy + globals()['ENV'] = '0.5_process' + init_logger() + +def init_gtk_05(): + #appy possibly changed config-vars, only called in when runing inside gtk. + from dbus_interface import get_dbus_manager + globals()['proxy'] = get_dbus_manager() + globals()['config'] = deluge.pref.Preferences(config_file, False) + globals()['render'] = subclassed_render(config.get('template'), + config.get('cache_templates')) + globals()['ENV'] = '0.5_gtk' + init_logger() + +def init_logger(): + #only for 0.5.. + import logging + logging.basicConfig(level=logging.DEBUG, + format="[%(levelname)s] %(message)s") + globals()['log'] = logging + + +#hacks to determine environment, TODO: clean up. +if 'env=0.5' in sys.argv: + init_05() +elif 'env=0.6' in sys.argv: + init_06() +elif hasattr(deluge, 'ui'): + init_06() +elif not hasattr(deluge,'pref'): + init_05() + + +#constants +REVNO = open(os.path.join(os.path.dirname(__file__),'revno')).read() +VERSION = open(os.path.join(os.path.dirname(__file__),'version')).read() + +TORRENT_KEYS = ['distributed_copies', 'download_payload_rate', + 'eta', 'is_seed', 'name', 'next_announce', + 'num_files', 'num_peers', 'num_pieces', 'num_seeds', 'paused', + 'piece_length','progress', 'ratio', 'total_done', 'total_download', + 'total_payload_download', 'total_payload_upload', 'total_peers', + 'total_seeds', 'total_size', 'total_upload', 'total_wanted', + 'tracker_status', 'upload_payload_rate', + 'uploaded_memory','tracker','state','queue_pos','user_paused'] + +STATE_MESSAGES = (_("Queued"), + _("Checking"), + _("Connecting"), + _("Downloading Metadata"), + _("Downloading"), + _("Finished"), + _("Seeding"), + _("Allocating")) + +SPEED_VALUES = [ + (-1, 'Unlimited'), + (5, '5.0 Kib/sec'), + (10, '10.0 Kib/sec'), + (15, '15.0 Kib/sec'), + (25, '25.0 Kib/sec'), + (30, '30.0 Kib/sec'), + (50, '50.0 Kib/sec'), + (80, '80.0 Kib/sec'), + (300, '300.0 Kib/sec'), + (500, '500.0 Kib/sec') + ] + +#try: +# SESSIONS = pickle.load(open(session_file)) +#except: +SESSIONS = [] + + + + + diff --git a/plugins/WebUi/webserver_framework.py b/plugins/WebUi/webserver_framework.py new file mode 100644 index 000000000..b72acab66 --- /dev/null +++ b/plugins/WebUi/webserver_framework.py @@ -0,0 +1,424 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# webserver_framework.py +# +# Copyright (C) Martijn Voncken 2007 +# +# 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 +# the Free Software Foundation; either version 2, or (at your option) +# any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, write to: +# The Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor +# Boston, MA 02110-1301, USA. +# +# In addition, as a special exception, the copyright holders give +# permission to link the code of portions of this program with the OpenSSL +# library. +# You must obey the GNU General Public License in all respects for all of +# the code used other than OpenSSL. If you modify file(s) with this +# exception, you may extend this exception to your version of the file(s), +# but you are not obligated to do so. If you do not wish to do so, delete +# this exception statement from your version. If you delete this exception +# statement from all source files in the program, then also delete it here. + +""" +Todo's before stable: +-__init__:kill->restart is not waiting for kill to be finished. +--later/features:--- +-alternating rows? +-set prio +-clear finished? +-torrent files. +""" +import lib.webpy022 as web + +from lib.webpy022.webapi import cookies, setcookie as w_setcookie +from lib.webpy022.http import seeother, url +from lib.webpy022 import template,changequery as self_url +from lib.webpy022.utils import Storage +from lib.static_handler import static_handler + +from deluge.common import fsize,fspeed + +import traceback +import random +from operator import attrgetter +import datetime +import pickle +from md5 import md5 +from urlparse import urlparse + +from deluge import common +from webserver_common import REVNO, VERSION, log +import webserver_common as ws +from debugerror import deluge_debugerror + +#init: +web.webapi.internalerror = deluge_debugerror +#/init +debug_unicode = False +#methods: +def setcookie(key, val): + """add 30 days expires header for persistent cookies""" + return w_setcookie(key, val , expires=2592000) + +#really simple sessions, to bad i had to implement them myself. +def start_session(): + log.debug('start session') + session_id = str(random.random()) + ws.SESSIONS.append(session_id) + #if len(ws.SESSIONS) > 20: #save max 20 sessions? + # ws.SESSIONS = ws.SESSIONS[-20:] + #not thread safe! , but a verry rare bug. + #f = open(ws.session_file,'wb') + #pickle.dump(ws.SESSIONS, f) + #f.close() + setcookie("session_id", session_id) + +def end_session(): + session_id = getcookie("session_id") + #if session_id in ws.SESSIONS: + # ws.SESSIONS.remove(session_id) + #not thread safe! , but a verry rare bug. + #f = open(ws.session_file,'wb') + #pickle.dump(ws.SESSIONS, f) + #f.close() + setcookie("session_id","") + +def do_redirect(): + """for redirects after a POST""" + vars = web.input(redir = None) + ck = cookies() + url_vars = {} + + if vars.redir: + seeother(vars.redir) + return + #todo:cleanup + if ("order" in ck and "sort" in ck): + url_vars.update({'sort':ck['sort'] ,'order':ck['order'] }) + if ("filter" in ck) and ck['filter']: + url_vars['filter'] = ck['filter'] + if ("category" in ck) and ck['category']: + url_vars['category'] = ck['category'] + + seeother(url("/index", **url_vars)) + +def error_page(error): + web.header("Content-Type", "text/html; charset=utf-8") + web.header("Cache-Control", "no-cache, must-revalidate") + print ws.render.error(error) + +def getcookie(key, default = None): + key = str(key).strip() + ck = cookies() + return ck.get(key, default) + +#deco's: +def deluge_page_noauth(func): + """ + add http headers + print result of func + """ + def deco(self, name = None): + web.header("Content-Type", "text/html; charset=utf-8") + web.header("Cache-Control", "no-cache, must-revalidate") + res = func(self, name) + print res + deco.__name__ = func.__name__ + return deco + +def check_session(func): + """ + a decorator + return func if session is valid, else redirect to login page. + """ + def deco(self, name = None): + log.debug('%s.%s(name=%s)' % (self.__class__.__name__, func.__name__, + name)) + vars = web.input(redir_after_login = None) + ck = cookies() + if ck.has_key("session_id") and ck["session_id"] in ws.SESSIONS: + return func(self, name) #ok, continue.. + elif vars.redir_after_login: + seeother(url("/login",redir=self_url())) + else: + seeother("/login") #do not continue, and redirect to login page + return deco + +def deluge_page(func): + return check_session(deluge_page_noauth(func)) + +#combi-deco's: +def auto_refreshed(func): + "decorator:adds a refresh header" + def deco(self, name = None): + if getcookie('auto_refresh') == '1': + web.header("Refresh", "%i ; url=%s" % + (int(getcookie('auto_refresh_secs',10)),self_url())) + return func(self, name) + deco.__name__ = func.__name__ + return deco + +def remote(func): + "decorator for remote api's" + def deco(self, name = None): + try: + log.debug('%s.%s(%s)' ,self.__class__.__name__, func.__name__,name ) + print func(self, name) + except Exception, e: + print 'error:%s' % e.message + print '-'*20 + print traceback.format_exc() + deco.__name__ = func.__name__ + return deco + +#utils: +def check_pwd(pwd): + m = md5() + m.update(ws.config.get('pwd_salt')) + m.update(pwd) + return (m.digest() == ws.config.get('pwd_md5')) + +def get_stats(): + stats = Storage({ + 'download_rate':fspeed(ws.proxy.get_download_rate()), + 'upload_rate':fspeed(ws.proxy.get_upload_rate()), + 'max_download':ws.proxy.get_config_value('max_download_speed_bps'), + 'max_upload':ws.proxy.get_config_value('max_upload_speed_bps'), + 'num_connections':ws.proxy.get_num_connections(), + 'max_num_connections':ws.proxy.get_config_value('max_connections_global') + }) + if stats.max_upload < 0: + stats.max_upload = _("Unlimited") + else: + stats.max_upload = fspeed(stats.max_upload) + + if stats.max_download < 0: + stats.max_download = _("Unlimited") + else: + stats.max_download = fspeed(stats.max_download) + + return stats + + +def get_torrent_status(torrent_id): + """ + helper method. + enhance ws.proxy.get_torrent_status with some extra data + """ + status = Storage(ws.proxy.get_torrent_status(torrent_id,ws.TORRENT_KEYS)) + + #add missing values for deluge 0.6: + for key in ws.TORRENT_KEYS: + if not key in status: + status[key] = 0 + + status["id"] = torrent_id + + url = urlparse(status.tracker) + if hasattr(url,'hostname'): + status.category = url.hostname or 'unknown' + else: + status.category = 'No-tracker' + + #0.5-->0.6 + status.download_rate = status.download_payload_rate + status.upload_rate = status.upload_payload_rate + + #for naming the status-images + status.calc_state_str = "downloading" + if status.paused: + status.calc_state_str= "inactive" + elif status.is_seed: + status.calc_state_str = "seeding" + + #action for torrent_pause + if status.user_paused: + status.action = "start" + else: + status.action = "stop" + + if status.user_paused: + status.message = _("Paused") + elif status.paused: + status.message = _("Queued") + else: + status.message = (ws.STATE_MESSAGES[status.state]) + + #add some pre-calculated values + status.update({ + "calc_total_downloaded" : (fsize(status.total_done) + + " (" + fsize(status.total_download) + ")"), + "calc_total_uploaded": (fsize(status.uploaded_memory + + status.total_payload_upload) + " (" + + fsize(status.total_upload) + ")"), + }) + + #no non-unicode string may enter the templates. + #FIXED,l was a translation bug.. + if debug_unicode: + for k, v in status.iteritems(): + if (not isinstance(v, unicode)) and isinstance(v, str): + try: + status[k] = unicode(v) + except: + raise Exception('Non Unicode for key:%s' % (k, )) + return status + +def get_categories(torrent_list): + trackers = [(torrent['category'] or 'unknown') for torrent in torrent_list] + categories = {} + for tracker in trackers: + categories[tracker] = categories.get(tracker,0) + 1 + return categories + +def filter_torrent_state(torrent_list,filter_name): + filters = { + 'downloading': lambda t: (not t.paused and not t.is_seed) + ,'queued':lambda t: (t.paused and not t.user_paused) + ,'paused':lambda t: (t.user_paused) + ,'seeding':lambda t:(t.is_seed and not t.paused ) + ,'traffic':lambda t: (t.download_rate > 0 or t.upload_rate > 0) + } + filter_func = filters[filter_name] + return [t for t in torrent_list if filter_func(t)] + +#/utils + +#template-defs: + +def get_category_choosers(torrent_list): + """ + todo: split into 2 parts... + """ + categories = get_categories(torrent_list) + + filter_tabs = [Storage(title='All (%s)' % len(torrent_list), + filter='', category=None)] + + #static filters + for title, filter_name in [ + (_('Downloading'),'downloading') , + (_('Queued'),'queued') , + (_('Paused'),'paused') , + (_('Seeding'),'seeding'), + (_('Traffic'),'traffic') + ]: + title += ' (%s)' % ( + len(filter_torrent_state(torrent_list, filter_name)), ) + filter_tabs.append(Storage(title=title, filter=filter_name)) + + categories = [x for x in get_categories(torrent_list).iteritems()] + categories.sort() + + #trackers: + category_tabs = [] + category_tabs.append( + Storage(title=_('Trackers'),category='')) + for title,count in categories: + category = title + title += ' (%s)' % (count, ) + category_tabs.append(Storage(title=title, category=category)) + + return filter_tabs, category_tabs + +def category_tabs(torrent_list): + filter_tabs, category_tabs = get_category_choosers(torrent_list) + return ws.render.part_categories(filter_tabs, category_tabs) + + +def template_crop(text, end): + if len(text) > end: + return text[0:end - 3] + '...' + return text + +def template_sort_head(id,name): + #got tired of doing these complex things inside templetor.. + vars = web.input(sort = None, order = None) + active_up = False + active_down = False + order = 'down' + + if vars.sort == id: + if vars.order == 'down': + order = 'up' + active_down = True + else: + active_up = True + + return ws.render.sort_column_head(id, name, order, active_up, active_down) + +def template_part_stats(): + return ws.render.part_stats(get_stats()) + +def get_config(var): + return ws.config.get(var) + +irow = 0 +def altrow(reset = False): + global irow + if reset: + irow = 1 + return + irow +=1 + irow = irow % 2 + return "altrow%s" % irow + + +template.Template.globals.update({ + 'sort_head': template_sort_head, + 'part_stats':template_part_stats, + 'category_tabs':category_tabs, + 'crop': template_crop, + '_': _ , #gettext/translations + 'str': str, #because % in templetor is broken. + 'int':int, + 'sorted': sorted, + 'altrow':altrow, + 'get_config': get_config, + 'self_url': self_url, + 'fspeed': common.fspeed, + 'fsize': common.fsize, + 'render': ws.render, #for easy resuse of templates + 'rev': 'rev.%s' % (REVNO, ), + 'version': VERSION, + 'getcookie':getcookie, + 'get': lambda (var): getattr(web.input(**{var:None}), var) # unreadable :-( +}) +#/template-defs + +def create_webserver(urls, methods): + from lib.webpy022.request import webpyfunc + from lib.webpy022 import webapi + from lib.gtk_cherrypy_wsgiserver import CherryPyWSGIServer + import os + + func = webapi.wsgifunc(webpyfunc(urls, methods, False)) + server_address=("0.0.0.0", int(ws.config.get('port'))) + + server = CherryPyWSGIServer(server_address, func, server_name="localhost") + if ws.config.get('use_https'): + server.ssl_certificate = os.path.join(ws.webui_path,'ssl/deluge.pem') + server.ssl_private_key = os.path.join(ws.webui_path,'ssl/deluge.key') + + print "http://%s:%d/" % server_address + return server + +#------ +__all__ = ['deluge_page_noauth', 'deluge_page', 'remote', + 'auto_refreshed', 'check_session', + 'do_redirect', 'error_page','start_session','getcookie' + ,'setcookie','create_webserver','end_session', + 'get_torrent_status', 'check_pwd','static_handler','get_categories' + ,'template','filter_torrent_state','log']