From 5861585e94a32b3032ac473804bf90c6e1363940 Mon Sep 17 00:00:00 2001 From: Sebastian Silva Date: Wed, 28 Sep 2011 00:19:33 +0000 Subject: Migrated to Flask, added JQuery sugar theme, fixed race condition --- (limited to 'websdk') diff --git a/websdk/__init__.py b/websdk/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/websdk/__init__.py diff --git a/websdk/flask/__init__.py b/websdk/flask/__init__.py new file mode 100644 index 0000000..c1076c3 --- /dev/null +++ b/websdk/flask/__init__.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +""" + flask + ~~~~~ + + A microframework based on Werkzeug. It's extensively documented + and follows best practice patterns. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" + +__version__ = '0.8-dev' + +# utilities we import from Werkzeug and Jinja2 that are unused +# in the module but are exported as public interface. +from werkzeug.exceptions import abort +from werkzeug.utils import redirect +from jinja2 import Markup, escape + +from .app import Flask, Request, Response +from .config import Config +from .helpers import url_for, jsonify, json_available, flash, \ + send_file, send_from_directory, get_flashed_messages, \ + get_template_attribute, make_response, safe_join +from .globals import current_app, g, request, session, _request_ctx_stack +from .ctx import has_request_context +from .module import Module +from .blueprints import Blueprint +from .templating import render_template, render_template_string + +# the signals +from .signals import signals_available, template_rendered, request_started, \ + request_finished, got_request_exception, request_tearing_down + +# only import json if it's available +if json_available: + from .helpers import json + +# backwards compat, goes away in 1.0 +from .sessions import SecureCookieSession as Session diff --git a/websdk/flask/app.py b/websdk/flask/app.py new file mode 100644 index 0000000..ebf4e6a --- /dev/null +++ b/websdk/flask/app.py @@ -0,0 +1,1518 @@ +# -*- coding: utf-8 -*- +""" + flask.app + ~~~~~~~~~ + + This module implements the central WSGI application object. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import with_statement + +import os +import sys +from threading import Lock +from datetime import timedelta +from itertools import chain +from functools import update_wrapper + +from werkzeug.datastructures import ImmutableDict +from werkzeug.routing import Map, Rule, RequestRedirect +from werkzeug.exceptions import HTTPException, InternalServerError, \ + MethodNotAllowed, BadRequest + +from .helpers import _PackageBoundObject, url_for, get_flashed_messages, \ + locked_cached_property, _tojson_filter, _endpoint_from_view_func, \ + find_package +from .wrappers import Request, Response +from .config import ConfigAttribute, Config +from .ctx import RequestContext +from .globals import _request_ctx_stack, request +from .sessions import SecureCookieSessionInterface +from .module import blueprint_is_module +from .templating import DispatchingJinjaLoader, Environment, \ + _default_template_ctx_processor +from .signals import request_started, request_finished, got_request_exception, \ + request_tearing_down + +# a lock used for logger initialization +_logger_lock = Lock() + + +def _make_timedelta(value): + if not isinstance(value, timedelta): + return timedelta(seconds=value) + return value + + +def setupmethod(f): + """Wraps a method so that it performs a check in debug mode if the + first request was already handled. + """ + def wrapper_func(self, *args, **kwargs): + if self.debug and self._got_first_request: + raise AssertionError('A setup function was called after the ' + 'first request was handled. This usually indicates a bug ' + 'in the application where a module was not imported ' + 'and decorators or other functionality was called too late.\n' + 'To fix this make sure to import all your view modules, ' + 'database models and everything related at a central place ' + 'before the application starts serving requests.') + return f(self, *args, **kwargs) + return update_wrapper(wrapper_func, f) + + +class Flask(_PackageBoundObject): + """The flask object implements a WSGI application and acts as the central + object. It is passed the name of the module or package of the + application. Once it is created it will act as a central registry for + the view functions, the URL rules, template configuration and much more. + + The name of the package is used to resolve resources from inside the + package or the folder the module is contained in depending on if the + package parameter resolves to an actual python package (a folder with + an `__init__.py` file inside) or a standard module (just a `.py` file). + + For more information about resource loading, see :func:`open_resource`. + + Usually you create a :class:`Flask` instance in your main module or + in the `__init__.py` file of your package like this:: + + from flask import Flask + app = Flask(__name__) + + .. admonition:: About the First Parameter + + The idea of the first parameter is to give Flask an idea what + belongs to your application. This name is used to find resources + on the file system, can be used by extensions to improve debugging + information and a lot more. + + So it's important what you provide there. If you are using a single + module, `__name__` is always the correct value. If you however are + using a package, it's usually recommended to hardcode the name of + your package there. + + For example if your application is defined in `yourapplication/app.py` + you should create it with one of the two versions below:: + + app = Flask('yourapplication') + app = Flask(__name__.split('.')[0]) + + Why is that? The application will work even with `__name__`, thanks + to how resources are looked up. However it will make debugging more + painful. Certain extensions can make assumptions based on the + import name of your application. For example the Flask-SQLAlchemy + extension will look for the code in your application that triggered + an SQL query in debug mode. If the import name is not properly set + up, that debugging information is lost. (For example it would only + pick up SQL queries in `yourapplication.app` and not + `yourapplication.views.frontend`) + + .. versionadded:: 0.7 + The `static_url_path`, `static_folder`, and `template_folder` + parameters were added. + + .. versionadded:: 0.8 + The `instance_path` and `instance_relative_config` parameters were + added. + + :param import_name: the name of the application package + :param static_url_path: can be used to specify a different path for the + static files on the web. Defaults to the name + of the `static_folder` folder. + :param static_folder: the folder with static files that should be served + at `static_url_path`. Defaults to the ``'static'`` + folder in the root path of the application. + :param template_folder: the folder that contains the templates that should + be used by the application. Defaults to + ``'templates'`` folder in the root path of the + application. + :param instance_path: An alternative instance path for the application. + By default the folder ``'instance'`` next to the + package or module is assumed to be the instance + path. + :param instance_relative_config: if set to `True` relative filenames + for loading the config are assumed to + be relative to the instance path instead + of the application root. + """ + + #: The class that is used for request objects. See :class:`~flask.Request` + #: for more information. + request_class = Request + + #: The class that is used for response objects. See + #: :class:`~flask.Response` for more information. + response_class = Response + + #: The debug flag. Set this to `True` to enable debugging of the + #: application. In debug mode the debugger will kick in when an unhandled + #: exception ocurrs and the integrated server will automatically reload + #: the application if changes in the code are detected. + #: + #: This attribute can also be configured from the config with the `DEBUG` + #: configuration key. Defaults to `False`. + debug = ConfigAttribute('DEBUG') + + #: The testing flag. Set this to `True` to enable the test mode of + #: Flask extensions (and in the future probably also Flask itself). + #: For example this might activate unittest helpers that have an + #: additional runtime cost which should not be enabled by default. + #: + #: If this is enabled and PROPAGATE_EXCEPTIONS is not changed from the + #: default it's implicitly enabled. + #: + #: This attribute can also be configured from the config with the + #: `TESTING` configuration key. Defaults to `False`. + testing = ConfigAttribute('TESTING') + + #: If a secret key is set, cryptographic components can use this to + #: sign cookies and other things. Set this to a complex random value + #: when you want to use the secure cookie for instance. + #: + #: This attribute can also be configured from the config with the + #: `SECRET_KEY` configuration key. Defaults to `None`. + secret_key = ConfigAttribute('SECRET_KEY') + + #: The secure cookie uses this for the name of the session cookie. + #: + #: This attribute can also be configured from the config with the + #: `SESSION_COOKIE_NAME` configuration key. Defaults to ``'session'`` + session_cookie_name = ConfigAttribute('SESSION_COOKIE_NAME') + + #: A :class:`~datetime.timedelta` which is used to set the expiration + #: date of a permanent session. The default is 31 days which makes a + #: permanent session survive for roughly one month. + #: + #: This attribute can also be configured from the config with the + #: `PERMANENT_SESSION_LIFETIME` configuration key. Defaults to + #: ``timedelta(days=31)`` + permanent_session_lifetime = ConfigAttribute('PERMANENT_SESSION_LIFETIME', + get_converter=_make_timedelta) + + #: Enable this if you want to use the X-Sendfile feature. Keep in + #: mind that the server has to support this. This only affects files + #: sent with the :func:`send_file` method. + #: + #: .. versionadded:: 0.2 + #: + #: This attribute can also be configured from the config with the + #: `USE_X_SENDFILE` configuration key. Defaults to `False`. + use_x_sendfile = ConfigAttribute('USE_X_SENDFILE') + + #: The name of the logger to use. By default the logger name is the + #: package name passed to the constructor. + #: + #: .. versionadded:: 0.4 + logger_name = ConfigAttribute('LOGGER_NAME') + + #: Enable the deprecated module support? This is active by default + #: in 0.7 but will be changed to False in 0.8. With Flask 1.0 modules + #: will be removed in favor of Blueprints + enable_modules = True + + #: The logging format used for the debug logger. This is only used when + #: the application is in debug mode, otherwise the attached logging + #: handler does the formatting. + #: + #: .. versionadded:: 0.3 + debug_log_format = ( + '-' * 80 + '\n' + + '%(levelname)s in %(module)s [%(pathname)s:%(lineno)d]:\n' + + '%(message)s\n' + + '-' * 80 + ) + + #: Options that are passed directly to the Jinja2 environment. + jinja_options = ImmutableDict( + extensions=['jinja2.ext.autoescape', 'jinja2.ext.with_'] + ) + + #: Default configuration parameters. + default_config = ImmutableDict({ + 'DEBUG': False, + 'TESTING': False, + 'PROPAGATE_EXCEPTIONS': None, + 'PRESERVE_CONTEXT_ON_EXCEPTION': None, + 'SECRET_KEY': None, + 'PERMANENT_SESSION_LIFETIME': timedelta(days=31), + 'USE_X_SENDFILE': False, + 'LOGGER_NAME': None, + 'SERVER_NAME': None, + 'APPLICATION_ROOT': None, + 'SESSION_COOKIE_NAME': 'session', + 'SESSION_COOKIE_DOMAIN': None, + 'SESSION_COOKIE_PATH': None, + 'SESSION_COOKIE_HTTPONLY': True, + 'SESSION_COOKIE_SECURE': False, + 'MAX_CONTENT_LENGTH': None, + 'TRAP_BAD_REQUEST_ERRORS': False, + 'TRAP_HTTP_EXCEPTIONS': False + }) + + #: The rule object to use for URL rules created. This is used by + #: :meth:`add_url_rule`. Defaults to :class:`werkzeug.routing.Rule`. + #: + #: .. versionadded:: 0.7 + url_rule_class = Rule + + #: the test client that is used with when `test_client` is used. + #: + #: .. versionadded:: 0.7 + test_client_class = None + + #: the session interface to use. By default an instance of + #: :class:`~flask.sessions.SecureCookieSessionInterface` is used here. + #: + #: .. versionadded:: 0.8 + session_interface = SecureCookieSessionInterface() + + def __init__(self, import_name, static_path=None, static_url_path=None, + static_folder='static', template_folder='templates', + instance_path=None, instance_relative_config=False): + _PackageBoundObject.__init__(self, import_name, + template_folder=template_folder) + if static_path is not None: + from warnings import warn + warn(DeprecationWarning('static_path is now called ' + 'static_url_path'), stacklevel=2) + static_url_path = static_path + + if static_url_path is not None: + self.static_url_path = static_url_path + if static_folder is not None: + self.static_folder = static_folder + if instance_path is None: + instance_path = self.auto_find_instance_path() + elif not os.path.isabs(instance_path): + raise ValueError('If an instance path is provided it must be ' + 'absolute. A relative path was given instead.') + + #: Holds the path to the instance folder. + #: + #: .. versionadded:: 0.8 + self.instance_path = instance_path + + #: The configuration dictionary as :class:`Config`. This behaves + #: exactly like a regular dictionary but supports additional methods + #: to load a config from files. + self.config = self.make_config(instance_relative_config) + + # Prepare the deferred setup of the logger. + self._logger = None + self.logger_name = self.import_name + + #: A dictionary of all view functions registered. The keys will + #: be function names which are also used to generate URLs and + #: the values are the function objects themselves. + #: To register a view function, use the :meth:`route` decorator. + self.view_functions = {} + + # support for the now deprecated `error_handlers` attribute. The + # :attr:`error_handler_spec` shall be used now. + self._error_handlers = {} + + #: A dictionary of all registered error handlers. The key is `None` + #: for error handlers active on the application, otherwise the key is + #: the name of the blueprint. Each key points to another dictionary + #: where they key is the status code of the http exception. The + #: special key `None` points to a list of tuples where the first item + #: is the class for the instance check and the second the error handler + #: function. + #: + #: To register a error handler, use the :meth:`errorhandler` + #: decorator. + self.error_handler_spec = {None: self._error_handlers} + + #: A dictionary with lists of functions that should be called at the + #: beginning of the request. The key of the dictionary is the name of + #: the blueprint this function is active for, `None` for all requests. + #: This can for example be used to open database connections or + #: getting hold of the currently logged in user. To register a + #: function here, use the :meth:`before_request` decorator. + self.before_request_funcs = {} + + #: A lists of functions that should be called at the beginning of the + #: first request to this instance. To register a function here, use + #: the :meth:`before_first_request` decorator. + #: + #: .. versionadded:: 0.8 + self.before_first_request_funcs = [] + + #: A dictionary with lists of functions that should be called after + #: each request. The key of the dictionary is the name of the blueprint + #: this function is active for, `None` for all requests. This can for + #: example be used to open database connections or getting hold of the + #: currently logged in user. To register a function here, use the + #: :meth:`after_request` decorator. + self.after_request_funcs = {} + + #: A dictionary with lists of functions that are called after + #: each request, even if an exception has occurred. The key of the + #: dictionary is the name of the blueprint this function is active for, + #: `None` for all requests. These functions are not allowed to modify + #: the request, and their return values are ignored. If an exception + #: occurred while processing the request, it gets passed to each + #: teardown_request function. To register a function here, use the + #: :meth:`teardown_request` decorator. + #: + #: .. versionadded:: 0.7 + self.teardown_request_funcs = {} + + #: A dictionary with lists of functions that can be used as URL + #: value processor functions. Whenever a URL is built these functions + #: are called to modify the dictionary of values in place. The key + #: `None` here is used for application wide + #: callbacks, otherwise the key is the name of the blueprint. + #: Each of these functions has the chance to modify the dictionary + #: + #: .. versionadded:: 0.7 + self.url_value_preprocessors = {} + + #: A dictionary with lists of functions that can be used as URL value + #: preprocessors. The key `None` here is used for application wide + #: callbacks, otherwise the key is the name of the blueprint. + #: Each of these functions has the chance to modify the dictionary + #: of URL values before they are used as the keyword arguments of the + #: view function. For each function registered this one should also + #: provide a :meth:`url_defaults` function that adds the parameters + #: automatically again that were removed that way. + #: + #: .. versionadded:: 0.7 + self.url_default_functions = {} + + #: A dictionary with list of functions that are called without argument + #: to populate the template context. The key of the dictionary is the + #: name of the blueprint this function is active for, `None` for all + #: requests. Each returns a dictionary that the template context is + #: updated with. To register a function here, use the + #: :meth:`context_processor` decorator. + self.template_context_processors = { + None: [_default_template_ctx_processor] + } + + #: all the attached blueprints in a directory by name. Blueprints + #: can be attached multiple times so this dictionary does not tell + #: you how often they got attached. + #: + #: .. versionadded:: 0.7 + self.blueprints = {} + + #: a place where extensions can store application specific state. For + #: example this is where an extension could store database engines and + #: similar things. For backwards compatibility extensions should register + #: themselves like this:: + #: + #: if not hasattr(app, 'extensions'): + #: app.extensions = {} + #: app.extensions['extensionname'] = SomeObject() + #: + #: The key must match the name of the `flaskext` module. For example in + #: case of a "Flask-Foo" extension in `flaskext.foo`, the key would be + #: ``'foo'``. + #: + #: .. versionadded:: 0.7 + self.extensions = {} + + #: The :class:`~werkzeug.routing.Map` for this instance. You can use + #: this to change the routing converters after the class was created + #: but before any routes are connected. Example:: + #: + #: from werkzeug.routing import BaseConverter + #: + #: class ListConverter(BaseConverter): + #: def to_python(self, value): + #: return value.split(',') + #: def to_url(self, values): + #: return ','.join(BaseConverter.to_url(value) + #: for value in values) + #: + #: app = Flask(__name__) + #: app.url_map.converters['list'] = ListConverter + self.url_map = Map() + + # tracks internally if the application already handled at least one + # request. + self._got_first_request = False + self._before_request_lock = Lock() + + # register the static folder for the application. Do that even + # if the folder does not exist. First of all it might be created + # while the server is running (usually happens during development) + # but also because google appengine stores static files somewhere + # else when mapped with the .yml file. + if self.has_static_folder: + self.add_url_rule(self.static_url_path + '/', + endpoint='static', + view_func=self.send_static_file) + + def _get_error_handlers(self): + from warnings import warn + warn(DeprecationWarning('error_handlers is deprecated, use the ' + 'new error_handler_spec attribute instead.'), stacklevel=1) + return self._error_handlers + def _set_error_handlers(self, value): + self._error_handlers = value + self.error_handler_spec[None] = value + error_handlers = property(_get_error_handlers, _set_error_handlers) + del _get_error_handlers, _set_error_handlers + + @locked_cached_property + def name(self): + """The name of the application. This is usually the import name + with the difference that it's guessed from the run file if the + import name is main. This name is used as a display name when + Flask needs the name of the application. It can be set and overriden + to change the value. + + .. versionadded:: 0.8 + """ + if self.import_name == '__main__': + fn = getattr(sys.modules['__main__'], '__file__', None) + if fn is None: + return '__main__' + return os.path.splitext(os.path.basename(fn))[0] + return self.import_name + + @property + def propagate_exceptions(self): + """Returns the value of the `PROPAGATE_EXCEPTIONS` configuration + value in case it's set, otherwise a sensible default is returned. + + .. versionadded:: 0.7 + """ + rv = self.config['PROPAGATE_EXCEPTIONS'] + if rv is not None: + return rv + return self.testing or self.debug + + @property + def preserve_context_on_exception(self): + """Returns the value of the `PRESERVE_CONTEXT_ON_EXCEPTION` + configuration value in case it's set, otherwise a sensible default + is returned. + + .. versionadded:: 0.7 + """ + rv = self.config['PRESERVE_CONTEXT_ON_EXCEPTION'] + if rv is not None: + return rv + return self.debug + + @property + def logger(self): + """A :class:`logging.Logger` object for this application. The + default configuration is to log to stderr if the application is + in debug mode. This logger can be used to (surprise) log messages. + Here some examples:: + + app.logger.debug('A value for debugging') + app.logger.warning('A warning ocurred (%d apples)', 42) + app.logger.error('An error occoured') + + .. versionadded:: 0.3 + """ + if self._logger and self._logger.name == self.logger_name: + return self._logger + with _logger_lock: + if self._logger and self._logger.name == self.logger_name: + return self._logger + from flask.logging import create_logger + self._logger = rv = create_logger(self) + return rv + + @locked_cached_property + def jinja_env(self): + """The Jinja2 environment used to load templates.""" + rv = self.create_jinja_environment() + + # Hack to support the init_jinja_globals method which is supported + # until 1.0 but has an API deficiency. + if getattr(self.init_jinja_globals, 'im_func', None) is not \ + Flask.init_jinja_globals.im_func: + from warnings import warn + warn(DeprecationWarning('This flask class uses a customized ' + 'init_jinja_globals() method which is deprecated. ' + 'Move the code from that method into the ' + 'create_jinja_environment() method instead.')) + self.__dict__['jinja_env'] = rv + self.init_jinja_globals() + + return rv + + @property + def got_first_request(self): + """This attribute is set to `True` if the application started + handling the first request. + + .. versionadded:: 0.8 + """ + return self._got_first_request + + def make_config(self, instance_relative=False): + """Used to create the config attribute by the Flask constructor. + The `instance_relative` parameter is passed in from the constructor + of Flask (there named `instance_relative_config`) and indicates if + the config should be relative to the instance path or the root path + of the application. + + .. versionadded:: 0.8 + """ + root_path = self.root_path + if instance_relative: + root_path = self.instance_path + return Config(root_path, self.default_config) + + def auto_find_instance_path(self): + """Tries to locate the instance path if it was not provided to the + constructor of the application class. It will basically calculate + the path to a folder named ``instance`` next to your main file or + the package. + + .. versionadded:: 0.8 + """ + prefix, package_path = find_package(self.import_name) + if prefix is None: + return os.path.join(package_path, 'instance') + return os.path.join(prefix, 'var', self.name + '-instance') + + def open_instance_resource(self, resource, mode='rb'): + """Opens a resource from the application's instance folder + (:attr:`instance_path`). Otherwise works like + :meth:`open_resource`. Instance resources can also be opened for + writing. + + :param resource: the name of the resource. To access resources within + subfolders use forward slashes as separator. + """ + return open(os.path.join(self.instance_path, resource), mode) + + def create_jinja_environment(self): + """Creates the Jinja2 environment based on :attr:`jinja_options` + and :meth:`select_jinja_autoescape`. Since 0.7 this also adds + the Jinja2 globals and filters after initialization. Override + this function to customize the behavior. + + .. versionadded:: 0.5 + """ + options = dict(self.jinja_options) + if 'autoescape' not in options: + options['autoescape'] = self.select_jinja_autoescape + rv = Environment(self, **options) + rv.globals.update( + url_for=url_for, + get_flashed_messages=get_flashed_messages + ) + rv.filters['tojson'] = _tojson_filter + return rv + + def create_global_jinja_loader(self): + """Creates the loader for the Jinja2 environment. Can be used to + override just the loader and keeping the rest unchanged. It's + discouraged to override this function. Instead one should override + the :meth:`jinja_loader` function instead. + + The global loader dispatches between the loaders of the application + and the individual blueprints. + + .. versionadded:: 0.7 + """ + return DispatchingJinjaLoader(self) + + def init_jinja_globals(self): + """Deprecated. Used to initialize the Jinja2 globals. + + .. versionadded:: 0.5 + .. versionchanged:: 0.7 + This method is deprecated with 0.7. Override + :meth:`create_jinja_environment` instead. + """ + + def select_jinja_autoescape(self, filename): + """Returns `True` if autoescaping should be active for the given + template name. + + .. versionadded:: 0.5 + """ + if filename is None: + return False + return filename.endswith(('.html', '.htm', '.xml', '.xhtml')) + + def update_template_context(self, context): + """Update the template context with some commonly used variables. + This injects request, session, config and g into the template + context as well as everything template context processors want + to inject. Note that the as of Flask 0.6, the original values + in the context will not be overriden if a context processor + decides to return a value with the same key. + + :param context: the context as a dictionary that is updated in place + to add extra variables. + """ + funcs = self.template_context_processors[None] + bp = _request_ctx_stack.top.request.blueprint + if bp is not None and bp in self.template_context_processors: + funcs = chain(funcs, self.template_context_processors[bp]) + orig_ctx = context.copy() + for func in funcs: + context.update(func()) + # make sure the original values win. This makes it possible to + # easier add new variables in context processors without breaking + # existing views. + context.update(orig_ctx) + + def run(self, host='127.0.0.1', port=5000, debug=None, **options): + """Runs the application on a local development server. If the + :attr:`debug` flag is set the server will automatically reload + for code changes and show a debugger in case an exception happened. + + If you want to run the application in debug mode, but disable the + code execution on the interactive debugger, you can pass + ``use_evalex=False`` as parameter. This will keep the debugger's + traceback screen active, but disable code execution. + + .. admonition:: Keep in Mind + + Flask will suppress any server error with a generic error page + unless it is in debug mode. As such to enable just the + interactive debugger without the code reloading, you have to + invoke :meth:`run` with ``debug=True`` and ``use_reloader=False``. + Setting ``use_debugger`` to `True` without being in debug mode + won't catch any exceptions because there won't be any to + catch. + + :param host: the hostname to listen on. set this to ``'0.0.0.0'`` + to have the server available externally as well. + :param port: the port of the webserver + :param debug: if given, enable or disable debug mode. + See :attr:`debug`. + :param options: the options to be forwarded to the underlying + Werkzeug server. See + :func:`werkzeug.serving.run_simple` for more + information. + """ + from werkzeug.serving import run_simple + if debug is not None: + self.debug = bool(debug) + options.setdefault('use_reloader', self.debug) + options.setdefault('use_debugger', self.debug) + try: + run_simple(host, port, self, **options) + finally: + # reset the first request information if the development server + # resetted normally. This makes it possible to restart the server + # without reloader and that stuff from an interactive shell. + self._got_first_request = False + + def test_client(self, use_cookies=True): + """Creates a test client for this application. For information + about unit testing head over to :ref:`testing`. + + The test client can be used in a `with` block to defer the closing down + of the context until the end of the `with` block. This is useful if + you want to access the context locals for testing:: + + with app.test_client() as c: + rv = c.get('/?vodka=42') + assert request.args['vodka'] == '42' + + See :class:`~flask.testing.FlaskClient` for more information. + + .. versionchanged:: 0.4 + added support for `with` block usage for the client. + + .. versionadded:: 0.7 + The `use_cookies` parameter was added as well as the ability + to override the client to be used by setting the + :attr:`test_client_class` attribute. + """ + cls = self.test_client_class + if cls is None: + from flask.testing import FlaskClient as cls + return cls(self, self.response_class, use_cookies=use_cookies) + + def open_session(self, request): + """Creates or opens a new session. Default implementation stores all + session data in a signed cookie. This requires that the + :attr:`secret_key` is set. Instead of overriding this method + we recommend replacing the :class:`session_interface`. + + :param request: an instance of :attr:`request_class`. + """ + return self.session_interface.open_session(self, request) + + def save_session(self, session, response): + """Saves the session if it needs updates. For the default + implementation, check :meth:`open_session`. Instead of overriding this + method we recommend replacing the :class:`session_interface`. + + :param session: the session to be saved (a + :class:`~werkzeug.contrib.securecookie.SecureCookie` + object) + :param response: an instance of :attr:`response_class` + """ + return self.session_interface.save_session(self, session, response) + + def make_null_session(self): + """Creates a new instance of a missing session. Instead of overriding + this method we recommend replacing the :class:`session_interface`. + + .. versionadded:: 0.7 + """ + return self.session_interface.make_null_session(self) + + def register_module(self, module, **options): + """Registers a module with this application. The keyword argument + of this function are the same as the ones for the constructor of the + :class:`Module` class and will override the values of the module if + provided. + + .. versionchanged:: 0.7 + The module system was deprecated in favor for the blueprint + system. + """ + assert blueprint_is_module(module), 'register_module requires ' \ + 'actual module objects. Please upgrade to blueprints though.' + if not self.enable_modules: + raise RuntimeError('Module support was disabled but code ' + 'attempted to register a module named %r' % module) + else: + from warnings import warn + warn(DeprecationWarning('Modules are deprecated. Upgrade to ' + 'using blueprints. Have a look into the documentation for ' + 'more information. If this module was registered by a ' + 'Flask-Extension upgrade the extension or contact the author ' + 'of that extension instead. (Registered %r)' % module), + stacklevel=2) + + self.register_blueprint(module, **options) + + @setupmethod + def register_blueprint(self, blueprint, **options): + """Registers a blueprint on the application. + + .. versionadded:: 0.7 + """ + first_registration = False + if blueprint.name in self.blueprints: + assert self.blueprints[blueprint.name] is blueprint, \ + 'A blueprint\'s name collision ocurred between %r and ' \ + '%r. Both share the same name "%s". Blueprints that ' \ + 'are created on the fly need unique names.' % \ + (blueprint, self.blueprints[blueprint.name], blueprint.name) + else: + self.blueprints[blueprint.name] = blueprint + first_registration = True + blueprint.register(self, options, first_registration) + + @setupmethod + def add_url_rule(self, rule, endpoint=None, view_func=None, **options): + """Connects a URL rule. Works exactly like the :meth:`route` + decorator. If a view_func is provided it will be registered with the + endpoint. + + Basically this example:: + + @app.route('/') + def index(): + pass + + Is equivalent to the following:: + + def index(): + pass + app.add_url_rule('/', 'index', index) + + If the view_func is not provided you will need to connect the endpoint + to a view function like so:: + + app.view_functions['index'] = index + + Internally :meth:`route` invokes :meth:`add_url_rule` so if you want + to customize the behavior via subclassing you only need to change + this method. + + For more information refer to :ref:`url-route-registrations`. + + .. versionchanged:: 0.2 + `view_func` parameter added. + + .. versionchanged:: 0.6 + `OPTIONS` is added automatically as method. + + :param rule: the URL rule as string + :param endpoint: the endpoint for the registered URL rule. Flask + itself assumes the name of the view function as + endpoint + :param view_func: the function to call when serving a request to the + provided endpoint + :param options: the options to be forwarded to the underlying + :class:`~werkzeug.routing.Rule` object. A change + to Werkzeug is handling of method options. methods + is a list of methods this rule should be limited + to (`GET`, `POST` etc.). By default a rule + just listens for `GET` (and implicitly `HEAD`). + Starting with Flask 0.6, `OPTIONS` is implicitly + added and handled by the standard request handling. + """ + if endpoint is None: + endpoint = _endpoint_from_view_func(view_func) + options['endpoint'] = endpoint + methods = options.pop('methods', None) + + # if the methods are not given and the view_func object knows its + # methods we can use that instead. If neither exists, we go with + # a tuple of only `GET` as default. + if methods is None: + methods = getattr(view_func, 'methods', None) or ('GET',) + + # starting with Flask 0.8 the view_func object can disable and + # force-enable the automatic options handling. + provide_automatic_options = getattr(view_func, + 'provide_automatic_options', None) + + if provide_automatic_options is None: + if 'OPTIONS' not in methods: + methods = tuple(methods) + ('OPTIONS',) + provide_automatic_options = True + else: + provide_automatic_options = False + + # due to a werkzeug bug we need to make sure that the defaults are + # None if they are an empty dictionary. This should not be necessary + # with Werkzeug 0.7 + options['defaults'] = options.get('defaults') or None + + rule = self.url_rule_class(rule, methods=methods, **options) + rule.provide_automatic_options = provide_automatic_options + self.url_map.add(rule) + if view_func is not None: + self.view_functions[endpoint] = view_func + + def route(self, rule, **options): + """A decorator that is used to register a view function for a + given URL rule. This does the same thing as :meth:`add_url_rule` + but is intended for decorator usage:: + + @app.route('/') + def index(): + return 'Hello World' + + For more information refer to :ref:`url-route-registrations`. + + :param rule: the URL rule as string + :param endpoint: the endpoint for the registered URL rule. Flask + itself assumes the name of the view function as + endpoint + :param view_func: the function to call when serving a request to the + provided endpoint + :param options: the options to be forwarded to the underlying + :class:`~werkzeug.routing.Rule` object. A change + to Werkzeug is handling of method options. methods + is a list of methods this rule should be limited + to (`GET`, `POST` etc.). By default a rule + just listens for `GET` (and implicitly `HEAD`). + Starting with Flask 0.6, `OPTIONS` is implicitly + added and handled by the standard request handling. + """ + def decorator(f): + endpoint = options.pop('endpoint', None) + self.add_url_rule(rule, endpoint, f, **options) + return f + return decorator + + @setupmethod + def endpoint(self, endpoint): + """A decorator to register a function as an endpoint. + Example:: + + @app.endpoint('example.endpoint') + def example(): + return "example" + + :param endpoint: the name of the endpoint + """ + def decorator(f): + self.view_functions[endpoint] = f + return f + return decorator + + @setupmethod + def errorhandler(self, code_or_exception): + """A decorator that is used to register a function give a given + error code. Example:: + + @app.errorhandler(404) + def page_not_found(error): + return 'This page does not exist', 404 + + You can also register handlers for arbitrary exceptions:: + + @app.errorhandler(DatabaseError) + def special_exception_handler(error): + return 'Database connection failed', 500 + + You can also register a function as error handler without using + the :meth:`errorhandler` decorator. The following example is + equivalent to the one above:: + + def page_not_found(error): + return 'This page does not exist', 404 + app.error_handler_spec[None][404] = page_not_found + + Setting error handlers via assignments to :attr:`error_handler_spec` + however is discouraged as it requires fidling with nested dictionaries + and the special case for arbitrary exception types. + + The first `None` refers to the active blueprint. If the error + handler should be application wide `None` shall be used. + + .. versionadded:: 0.7 + One can now additionally also register custom exception types + that do not necessarily have to be a subclass of the + :class:`~werkzeug.exceptions.HTTPException` class. + + :param code: the code as integer for the handler + """ + def decorator(f): + self._register_error_handler(None, code_or_exception, f) + return f + return decorator + + def register_error_handler(self, code_or_exception, f): + """Alternative error attach function to the :meth:`errorhandler` + decorator that is more straightforward to use for non decorator + usage. + + .. versionadded:: 0.7 + """ + self._register_error_handler(None, code_or_exception, f) + + @setupmethod + def _register_error_handler(self, key, code_or_exception, f): + if isinstance(code_or_exception, HTTPException): + code_or_exception = code_or_exception.code + if isinstance(code_or_exception, (int, long)): + assert code_or_exception != 500 or key is None, \ + 'It is currently not possible to register a 500 internal ' \ + 'server error on a per-blueprint level.' + self.error_handler_spec.setdefault(key, {})[code_or_exception] = f + else: + self.error_handler_spec.setdefault(key, {}).setdefault(None, []) \ + .append((code_or_exception, f)) + + @setupmethod + def template_filter(self, name=None): + """A decorator that is used to register custom template filter. + You can specify a name for the filter, otherwise the function + name will be used. Example:: + + @app.template_filter() + def reverse(s): + return s[::-1] + + :param name: the optional name of the filter, otherwise the + function name will be used. + """ + def decorator(f): + self.jinja_env.filters[name or f.__name__] = f + return f + return decorator + + @setupmethod + def before_request(self, f): + """Registers a function to run before each request.""" + self.before_request_funcs.setdefault(None, []).append(f) + return f + + @setupmethod + def before_first_request(self, f): + """Registers a function to be run before the first request to this + instance of the application. + + .. versionadded:: 0.8 + """ + self.before_first_request_funcs.append(f) + + @setupmethod + def after_request(self, f): + """Register a function to be run after each request. Your function + must take one parameter, a :attr:`response_class` object and return + a new response object or the same (see :meth:`process_response`). + + As of Flask 0.7 this function might not be executed at the end of the + request in case an unhandled exception ocurred. + """ + self.after_request_funcs.setdefault(None, []).append(f) + return f + + @setupmethod + def teardown_request(self, f): + """Register a function to be run at the end of each request, + regardless of whether there was an exception or not. These functions + are executed when the request context is popped, even if not an + actual request was performed. + + Example:: + + ctx = app.test_request_context() + ctx.push() + ... + ctx.pop() + + When ``ctx.pop()`` is executed in the above example, the teardown + functions are called just before the request context moves from the + stack of active contexts. This becomes relevant if you are using + such constructs in tests. + + Generally teardown functions must take every necesary step to avoid + that they will fail. If they do execute code that might fail they + will have to surround the execution of these code by try/except + statements and log ocurring errors. + """ + self.teardown_request_funcs.setdefault(None, []).append(f) + return f + + @setupmethod + def context_processor(self, f): + """Registers a template context processor function.""" + self.template_context_processors[None].append(f) + return f + + @setupmethod + def url_value_preprocessor(self, f): + """Registers a function as URL value preprocessor for all view + functions of the application. It's called before the view functions + are called and can modify the url values provided. + """ + self.url_value_preprocessors.setdefault(None, []).append(f) + return f + + @setupmethod + def url_defaults(self, f): + """Callback function for URL defaults for all view functions of the + application. It's called with the endpoint and values and should + update the values passed in place. + """ + self.url_default_functions.setdefault(None, []).append(f) + return f + + def handle_http_exception(self, e): + """Handles an HTTP exception. By default this will invoke the + registered error handlers and fall back to returning the + exception as response. + + .. versionadded: 0.3 + """ + handlers = self.error_handler_spec.get(request.blueprint) + if handlers and e.code in handlers: + handler = handlers[e.code] + else: + handler = self.error_handler_spec[None].get(e.code) + if handler is None: + return e + return handler(e) + + def trap_http_exception(self, e): + """Checks if an HTTP exception should be trapped or not. By default + this will return `False` for all exceptions except for a bad request + key error if ``TRAP_BAD_REQUEST_ERRORS`` is set to `True`. It + also returns `True` if ``TRAP_HTTP_EXCEPTIONS`` is set to `True`. + + This is called for all HTTP exceptions raised by a view function. + If it returns `True` for any exception the error handler for this + exception is not called and it shows up as regular exception in the + traceback. This is helpful for debugging implicitly raised HTTP + exceptions. + + .. versionadded:: 0.8 + """ + if self.config['TRAP_HTTP_EXCEPTIONS']: + return True + if self.config['TRAP_BAD_REQUEST_ERRORS']: + return isinstance(e, BadRequest) + return False + + def handle_user_exception(self, e): + """This method is called whenever an exception occurs that should be + handled. A special case are + :class:`~werkzeug.exception.HTTPException`\s which are forwarded by + this function to the :meth:`handle_http_exception` method. This + function will either return a response value or reraise the + exception with the same traceback. + + .. versionadded:: 0.7 + """ + exc_type, exc_value, tb = sys.exc_info() + assert exc_value is e + + # ensure not to trash sys.exc_info() at that point in case someone + # wants the traceback preserved in handle_http_exception. Of course + # we cannot prevent users from trashing it themselves in a custom + # trap_http_exception method so that's their fault then. + if isinstance(e, HTTPException) and not self.trap_http_exception(e): + return self.handle_http_exception(e) + + blueprint_handlers = () + handlers = self.error_handler_spec.get(request.blueprint) + if handlers is not None: + blueprint_handlers = handlers.get(None, ()) + app_handlers = self.error_handler_spec[None].get(None, ()) + for typecheck, handler in chain(blueprint_handlers, app_handlers): + if isinstance(e, typecheck): + return handler(e) + + raise exc_type, exc_value, tb + + def handle_exception(self, e): + """Default exception handling that kicks in when an exception + occours that is not caught. In debug mode the exception will + be re-raised immediately, otherwise it is logged and the handler + for a 500 internal server error is used. If no such handler + exists, a default 500 internal server error message is displayed. + + .. versionadded: 0.3 + """ + exc_type, exc_value, tb = sys.exc_info() + + got_request_exception.send(self, exception=e) + handler = self.error_handler_spec[None].get(500) + + if self.propagate_exceptions: + # if we want to repropagate the exception, we can attempt to + # raise it with the whole traceback in case we can do that + # (the function was actually called from the except part) + # otherwise, we just raise the error again + if exc_value is e: + raise exc_type, exc_value, tb + else: + raise e + + self.log_exception((exc_type, exc_value, tb)) + if handler is None: + return InternalServerError() + return handler(e) + + def log_exception(self, exc_info): + """Logs an exception. This is called by :meth:`handle_exception` + if debugging is disabled and right before the handler is called. + The default implementation logs the exception as error on the + :attr:`logger`. + + .. versionadded:: 0.8 + """ + self.logger.error('Exception on %s [%s]' % ( + request.path, + request.method + ), exc_info=exc_info) + + def raise_routing_exception(self, request): + """Exceptions that are recording during routing are reraised with + this method. During debug we are not reraising redirect requests + for non ``GET``, ``HEAD``, or ``OPTIONS`` requests and we're raising + a different error instead to help debug situations. + + :internal: + """ + if not self.debug \ + or not isinstance(request.routing_exception, RequestRedirect) \ + or request.method in ('GET', 'HEAD', 'OPTIONS'): + raise request.routing_exception + + from .debughelpers import FormDataRoutingRedirect + raise FormDataRoutingRedirect(request) + + def dispatch_request(self): + """Does the request dispatching. Matches the URL and returns the + return value of the view or error handler. This does not have to + be a response object. In order to convert the return value to a + proper response object, call :func:`make_response`. + + .. versionchanged:: 0.7 + This no longer does the exception handling, this code was + moved to the new :meth:`full_dispatch_request`. + """ + req = _request_ctx_stack.top.request + if req.routing_exception is not None: + self.raise_routing_exception(req) + rule = req.url_rule + # if we provide automatic options for this URL and the + # request came with the OPTIONS method, reply automatically + if getattr(rule, 'provide_automatic_options', False) \ + and req.method == 'OPTIONS': + return self.make_default_options_response() + # otherwise dispatch to the handler for that endpoint + return self.view_functions[rule.endpoint](**req.view_args) + + def full_dispatch_request(self): + """Dispatches the request and on top of that performs request + pre and postprocessing as well as HTTP exception catching and + error handling. + + .. versionadded:: 0.7 + """ + self.try_trigger_before_first_request_functions() + try: + request_started.send(self) + rv = self.preprocess_request() + if rv is None: + rv = self.dispatch_request() + except Exception, e: + rv = self.handle_user_exception(e) + response = self.make_response(rv) + response = self.process_response(response) + request_finished.send(self, response=response) + return response + + def try_trigger_before_first_request_functions(self): + """Called before each request and will ensure that it triggers + the :attr:`before_first_request_funcs` and only exactly once per + application instance (which means process usually). + + :internal: + """ + if self._got_first_request: + return + with self._before_request_lock: + if self._got_first_request: + return + self._got_first_request = True + for func in self.before_first_request_funcs: + func() + + def make_default_options_response(self): + """This method is called to create the default `OPTIONS` response. + This can be changed through subclassing to change the default + behaviour of `OPTIONS` responses. + + .. versionadded:: 0.7 + """ + adapter = _request_ctx_stack.top.url_adapter + if hasattr(adapter, 'allowed_methods'): + methods = adapter.allowed_methods() + else: + # fallback for Werkzeug < 0.7 + methods = [] + try: + adapter.match(method='--') + except MethodNotAllowed, e: + methods = e.valid_methods + except HTTPException, e: + pass + rv = self.response_class() + rv.allow.update(methods) + return rv + + def make_response(self, rv): + """Converts the return value from a view function to a real + response object that is an instance of :attr:`response_class`. + + The following types are allowed for `rv`: + + .. tabularcolumns:: |p{3.5cm}|p{9.5cm}| + + ======================= =========================================== + :attr:`response_class` the object is returned unchanged + :class:`str` a response object is created with the + string as body + :class:`unicode` a response object is created with the + string encoded to utf-8 as body + :class:`tuple` the response object is created with the + contents of the tuple as arguments + a WSGI function the function is called as WSGI application + and buffered as response object + ======================= =========================================== + + :param rv: the return value from the view function + """ + if rv is None: + raise ValueError('View function did not return a response') + if isinstance(rv, self.response_class): + return rv + if isinstance(rv, basestring): + return self.response_class(rv) + if isinstance(rv, tuple): + return self.response_class(*rv) + return self.response_class.force_type(rv, request.environ) + + def create_url_adapter(self, request): + """Creates a URL adapter for the given request. The URL adapter + is created at a point where the request context is not yet set up + so the request is passed explicitly. + + .. versionadded:: 0.6 + """ + return self.url_map.bind_to_environ(request.environ, + server_name=self.config['SERVER_NAME']) + + def inject_url_defaults(self, endpoint, values): + """Injects the URL defaults for the given endpoint directly into + the values dictionary passed. This is used internally and + automatically called on URL building. + + .. versionadded:: 0.7 + """ + funcs = self.url_default_functions.get(None, ()) + if '.' in endpoint: + bp = endpoint.split('.', 1)[0] + funcs = chain(funcs, self.url_default_functions.get(bp, ())) + for func in funcs: + func(endpoint, values) + + def preprocess_request(self): + """Called before the actual request dispatching and will + call every as :meth:`before_request` decorated function. + If any of these function returns a value it's handled as + if it was the return value from the view and further + request handling is stopped. + + This also triggers the :meth:`url_value_processor` functions before + the actualy :meth:`before_request` functions are called. + """ + bp = _request_ctx_stack.top.request.blueprint + + funcs = self.url_value_preprocessors.get(None, ()) + if bp is not None and bp in self.url_value_preprocessors: + funcs = chain(funcs, self.url_value_preprocessors[bp]) + for func in funcs: + func(request.endpoint, request.view_args) + + funcs = self.before_request_funcs.get(None, ()) + if bp is not None and bp in self.before_request_funcs: + funcs = chain(funcs, self.before_request_funcs[bp]) + for func in funcs: + rv = func() + if rv is not None: + return rv + + def process_response(self, response): + """Can be overridden in order to modify the response object + before it's sent to the WSGI server. By default this will + call all the :meth:`after_request` decorated functions. + + .. versionchanged:: 0.5 + As of Flask 0.5 the functions registered for after request + execution are called in reverse order of registration. + + :param response: a :attr:`response_class` object. + :return: a new response object or the same, has to be an + instance of :attr:`response_class`. + """ + ctx = _request_ctx_stack.top + bp = ctx.request.blueprint + if not self.session_interface.is_null_session(ctx.session): + self.save_session(ctx.session, response) + funcs = () + if bp is not None and bp in self.after_request_funcs: + funcs = reversed(self.after_request_funcs[bp]) + if None in self.after_request_funcs: + funcs = chain(funcs, reversed(self.after_request_funcs[None])) + for handler in funcs: + response = handler(response) + return response + + def do_teardown_request(self): + """Called after the actual request dispatching and will + call every as :meth:`teardown_request` decorated function. This is + not actually called by the :class:`Flask` object itself but is always + triggered when the request context is popped. That way we have a + tighter control over certain resources under testing environments. + """ + funcs = reversed(self.teardown_request_funcs.get(None, ())) + bp = _request_ctx_stack.top.request.blueprint + if bp is not None and bp in self.teardown_request_funcs: + funcs = chain(funcs, reversed(self.teardown_request_funcs[bp])) + exc = sys.exc_info()[1] + for func in funcs: + rv = func(exc) + if rv is not None: + return rv + request_tearing_down.send(self) + + def request_context(self, environ): + """Creates a :class:`~flask.ctx.RequestContext` from the given + environment and binds it to the current context. This must be used in + combination with the `with` statement because the request is only bound + to the current context for the duration of the `with` block. + + Example usage:: + + with app.request_context(environ): + do_something_with(request) + + The object returned can also be used without the `with` statement + which is useful for working in the shell. The example above is + doing exactly the same as this code:: + + ctx = app.request_context(environ) + ctx.push() + try: + do_something_with(request) + finally: + ctx.pop() + + .. versionchanged:: 0.3 + Added support for non-with statement usage and `with` statement + is now passed the ctx object. + + :param environ: a WSGI environment + """ + return RequestContext(self, environ) + + def test_request_context(self, *args, **kwargs): + """Creates a WSGI environment from the given values (see + :func:`werkzeug.test.EnvironBuilder` for more information, this + function accepts the same arguments). + """ + from flask.testing import make_test_environ_builder + builder = make_test_environ_builder(self, *args, **kwargs) + try: + return self.request_context(builder.get_environ()) + finally: + builder.close() + + def wsgi_app(self, environ, start_response): + """The actual WSGI application. This is not implemented in + `__call__` so that middlewares can be applied without losing a + reference to the class. So instead of doing this:: + + app = MyMiddleware(app) + + It's a better idea to do this instead:: + + app.wsgi_app = MyMiddleware(app.wsgi_app) + + Then you still have the original application object around and + can continue to call methods on it. + + .. versionchanged:: 0.7 + The behavior of the before and after request callbacks was changed + under error conditions and a new callback was added that will + always execute at the end of the request, independent on if an + error ocurred or not. See :ref:`callbacks-and-errors`. + + :param environ: a WSGI environment + :param start_response: a callable accepting a status code, + a list of headers and an optional + exception context to start the response + """ + with self.request_context(environ): + try: + response = self.full_dispatch_request() + except Exception, e: + response = self.make_response(self.handle_exception(e)) + return response(environ, start_response) + + @property + def modules(self): + from warnings import warn + warn(DeprecationWarning('Flask.modules is deprecated, use ' + 'Flask.blueprints instead'), stacklevel=2) + return self.blueprints + + def __call__(self, environ, start_response): + """Shortcut for :attr:`wsgi_app`.""" + return self.wsgi_app(environ, start_response) diff --git a/websdk/flask/blueprints.py b/websdk/flask/blueprints.py new file mode 100644 index 0000000..ccdda38 --- /dev/null +++ b/websdk/flask/blueprints.py @@ -0,0 +1,321 @@ +# -*- coding: utf-8 -*- +""" + flask.blueprints + ~~~~~~~~~~~~~~~~ + + Blueprints are the recommended way to implement larger or more + pluggable applications in Flask 0.7 and later. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +from functools import update_wrapper + +from .helpers import _PackageBoundObject, _endpoint_from_view_func + + +class BlueprintSetupState(object): + """Temporary holder object for registering a blueprint with the + application. An instance of this class is created by the + :meth:`~flask.Blueprint.make_setup_state` method and later passed + to all register callback functions. + """ + + def __init__(self, blueprint, app, options, first_registration): + #: a reference to the current application + self.app = app + + #: a reference to the blurprint that created this setup state. + self.blueprint = blueprint + + #: a dictionary with all options that were passed to the + #: :meth:`~flask.Flask.register_blueprint` method. + self.options = options + + #: as blueprints can be registered multiple times with the + #: application and not everything wants to be registered + #: multiple times on it, this attribute can be used to figure + #: out if the blueprint was registered in the past already. + self.first_registration = first_registration + + subdomain = self.options.get('subdomain') + if subdomain is None: + subdomain = self.blueprint.subdomain + + #: The subdomain that the blueprint should be active for, `None` + #: otherwise. + self.subdomain = subdomain + + url_prefix = self.options.get('url_prefix') + if url_prefix is None: + url_prefix = self.blueprint.url_prefix + + #: The prefix that should be used for all URLs defined on the + #: blueprint. + self.url_prefix = url_prefix + + #: A dictionary with URL defaults that is added to each and every + #: URL that was defined with the blueprint. + self.url_defaults = dict(self.blueprint.url_values_defaults) + self.url_defaults.update(self.options.get('url_defaults', ())) + + def add_url_rule(self, rule, endpoint=None, view_func=None, **options): + """A helper method to register a rule (and optionally a view function) + to the application. The endpoint is automatically prefixed with the + blueprint's name. + """ + if self.url_prefix: + rule = self.url_prefix + rule + options.setdefault('subdomain', self.subdomain) + if endpoint is None: + endpoint = _endpoint_from_view_func(view_func) + defaults = self.url_defaults + if 'defaults' in options: + defaults = dict(defaults, **options.pop('defaults')) + self.app.add_url_rule(rule, '%s.%s' % (self.blueprint.name, endpoint), + view_func, defaults=defaults, **options) + + +class Blueprint(_PackageBoundObject): + """Represents a blueprint. A blueprint is an object that records + functions that will be called with the + :class:`~flask.blueprint.BlueprintSetupState` later to register functions + or other things on the main application. See :ref:`blueprints` for more + information. + + .. versionadded:: 0.7 + """ + + warn_on_modifications = False + _got_registered_once = False + + def __init__(self, name, import_name, static_folder=None, + static_url_path=None, template_folder=None, + url_prefix=None, subdomain=None, url_defaults=None): + _PackageBoundObject.__init__(self, import_name, template_folder) + self.name = name + self.url_prefix = url_prefix + self.subdomain = subdomain + self.static_folder = static_folder + self.static_url_path = static_url_path + self.deferred_functions = [] + self.view_functions = {} + if url_defaults is None: + url_defaults = {} + self.url_values_defaults = url_defaults + + def record(self, func): + """Registers a function that is called when the blueprint is + registered on the application. This function is called with the + state as argument as returned by the :meth:`make_setup_state` + method. + """ + if self._got_registered_once and self.warn_on_modifications: + from warnings import warn + warn(Warning('The blueprint was already registered once ' + 'but is getting modified now. These changes ' + 'will not show up.')) + self.deferred_functions.append(func) + + def record_once(self, func): + """Works like :meth:`record` but wraps the function in another + function that will ensure the function is only called once. If the + blueprint is registered a second time on the application, the + function passed is not called. + """ + def wrapper(state): + if state.first_registration: + func(state) + return self.record(update_wrapper(wrapper, func)) + + def make_setup_state(self, app, options, first_registration=False): + """Creates an instance of :meth:`~flask.blueprints.BlueprintSetupState` + object that is later passed to the register callback functions. + Subclasses can override this to return a subclass of the setup state. + """ + return BlueprintSetupState(self, app, options, first_registration) + + def register(self, app, options, first_registration=False): + """Called by :meth:`Flask.register_blueprint` to register a blueprint + on the application. This can be overridden to customize the register + behavior. Keyword arguments from + :func:`~flask.Flask.register_blueprint` are directly forwarded to this + method in the `options` dictionary. + """ + self._got_registered_once = True + state = self.make_setup_state(app, options, first_registration) + if self.has_static_folder: + state.add_url_rule(self.static_url_path + '/', + view_func=self.send_static_file, + endpoint='static') + + for deferred in self.deferred_functions: + deferred(state) + + def route(self, rule, **options): + """Like :meth:`Flask.route` but for a blueprint. The endpoint for the + :func:`url_for` function is prefixed with the name of the blueprint. + """ + def decorator(f): + endpoint = options.pop("endpoint", f.__name__) + self.add_url_rule(rule, endpoint, f, **options) + return f + return decorator + + def add_url_rule(self, rule, endpoint=None, view_func=None, **options): + """Like :meth:`Flask.add_url_rule` but for a blueprint. The endpoint for + the :func:`url_for` function is prefixed with the name of the blueprint. + """ + if endpoint: + assert '.' not in endpoint, "Blueprint endpoint's should not contain dot's" + self.record(lambda s: + s.add_url_rule(rule, endpoint, view_func, **options)) + + def endpoint(self, endpoint): + """Like :meth:`Flask.endpoint` but for a blueprint. This does not + prefix the endpoint with the blueprint name, this has to be done + explicitly by the user of this method. If the endpoint is prefixed + with a `.` it will be registered to the current blueprint, otherwise + it's an application independent endpoint. + """ + def decorator(f): + def register_endpoint(state): + state.app.view_functions[endpoint] = f + self.record_once(register_endpoint) + return f + return decorator + + def before_request(self, f): + """Like :meth:`Flask.before_request` but for a blueprint. This function + is only executed before each request that is handled by a function of + that blueprint. + """ + self.record_once(lambda s: s.app.before_request_funcs + .setdefault(self.name, []).append(f)) + return f + + def before_app_request(self, f): + """Like :meth:`Flask.before_request`. Such a function is executed + before each request, even if outside of a blueprint. + """ + self.record_once(lambda s: s.app.before_request_funcs + .setdefault(None, []).append(f)) + return f + + def before_app_first_request(self, f): + """Like :meth:`Flask.before_first_request`. Such a function is + executed before the first request to the application. + """ + self.record_once(lambda s: s.app.before_first_request_funcs.append(f)) + return f + + def after_request(self, f): + """Like :meth:`Flask.after_request` but for a blueprint. This function + is only executed after each request that is handled by a function of + that blueprint. + """ + self.record_once(lambda s: s.app.after_request_funcs + .setdefault(self.name, []).append(f)) + return f + + def after_app_request(self, f): + """Like :meth:`Flask.after_request` but for a blueprint. Such a function + is executed after each request, even if outside of the blueprint. + """ + self.record_once(lambda s: s.app.after_request_funcs + .setdefault(None, []).append(f)) + return f + + def teardown_request(self, f): + """Like :meth:`Flask.teardown_request` but for a blueprint. This + function is only executed when tearing down requests handled by a + function of that blueprint. Teardown request functions are executed + when the request context is popped, even when no actual request was + performed. + """ + self.record_once(lambda s: s.app.teardown_request_funcs + .setdefault(self.name, []).append(f)) + return f + + def teardown_app_request(self, f): + """Like :meth:`Flask.teardown_request` but for a blueprint. Such a + function is executed when tearing down each request, even if outside of + the blueprint. + """ + self.record_once(lambda s: s.app.teardown_request_funcs + .setdefault(None, []).append(f)) + return f + + def context_processor(self, f): + """Like :meth:`Flask.context_processor` but for a blueprint. This + function is only executed for requests handled by a blueprint. + """ + self.record_once(lambda s: s.app.template_context_processors + .setdefault(self.name, []).append(f)) + return f + + def app_context_processor(self, f): + """Like :meth:`Flask.context_processor` but for a blueprint. Such a + function is executed each request, even if outside of the blueprint. + """ + self.record_once(lambda s: s.app.template_context_processors + .setdefault(None, []).append(f)) + return f + + def app_errorhandler(self, code): + """Like :meth:`Flask.errorhandler` but for a blueprint. This + handler is used for all requests, even if outside of the blueprint. + """ + def decorator(f): + self.record_once(lambda s: s.app.errorhandler(code)(f)) + return f + return decorator + + def url_value_preprocessor(self, f): + """Registers a function as URL value preprocessor for this + blueprint. It's called before the view functions are called and + can modify the url values provided. + """ + self.record_once(lambda s: s.app.url_value_preprocessors + .setdefault(self.name, []).append(f)) + return f + + def url_defaults(self, f): + """Callback function for URL defaults for this blueprint. It's called + with the endpoint and values and should update the values passed + in place. + """ + self.record_once(lambda s: s.app.url_default_functions + .setdefault(self.name, []).append(f)) + return f + + def app_url_value_preprocessor(self, f): + """Same as :meth:`url_value_preprocessor` but application wide. + """ + self.record_once(lambda s: s.app.url_value_preprocessors + .setdefault(None, []).append(f)) + return f + + def app_url_defaults(self, f): + """Same as :meth:`url_defaults` but application wide. + """ + self.record_once(lambda s: s.app.url_default_functions + .setdefault(None, []).append(f)) + return f + + def errorhandler(self, code_or_exception): + """Registers an error handler that becomes active for this blueprint + only. Please be aware that routing does not happen local to a + blueprint so an error handler for 404 usually is not handled by + a blueprint unless it is caused inside a view function. Another + special case is the 500 internal server error which is always looked + up from the application. + + Otherwise works as the :meth:`~flask.Flask.errorhandler` decorator + of the :class:`~flask.Flask` object. + """ + def decorator(f): + self.record_once(lambda s: s.app._register_error_handler( + self.name, code_or_exception, f)) + return f + return decorator diff --git a/websdk/flask/config.py b/websdk/flask/config.py new file mode 100644 index 0000000..67dbf9b --- /dev/null +++ b/websdk/flask/config.py @@ -0,0 +1,169 @@ +# -*- coding: utf-8 -*- +""" + flask.config + ~~~~~~~~~~~~ + + Implements the configuration related objects. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import with_statement + +import imp +import os +import errno + +from werkzeug.utils import import_string + + +class ConfigAttribute(object): + """Makes an attribute forward to the config""" + + def __init__(self, name, get_converter=None): + self.__name__ = name + self.get_converter = get_converter + + def __get__(self, obj, type=None): + if obj is None: + return self + rv = obj.config[self.__name__] + if self.get_converter is not None: + rv = self.get_converter(rv) + return rv + + def __set__(self, obj, value): + obj.config[self.__name__] = value + + +class Config(dict): + """Works exactly like a dict but provides ways to fill it from files + or special dictionaries. There are two common patterns to populate the + config. + + Either you can fill the config from a config file:: + + app.config.from_pyfile('yourconfig.cfg') + + Or alternatively you can define the configuration options in the + module that calls :meth:`from_object` or provide an import path to + a module that should be loaded. It is also possible to tell it to + use the same module and with that provide the configuration values + just before the call:: + + DEBUG = True + SECRET_KEY = 'development key' + app.config.from_object(__name__) + + In both cases (loading from any Python file or loading from modules), + only uppercase keys are added to the config. This makes it possible to use + lowercase values in the config file for temporary values that are not added + to the config or to define the config keys in the same file that implements + the application. + + Probably the most interesting way to load configurations is from an + environment variable pointing to a file:: + + app.config.from_envvar('YOURAPPLICATION_SETTINGS') + + In this case before launching the application you have to set this + environment variable to the file you want to use. On Linux and OS X + use the export statement:: + + export YOURAPPLICATION_SETTINGS='/path/to/config/file' + + On windows use `set` instead. + + :param root_path: path to which files are read relative from. When the + config object is created by the application, this is + the application's :attr:`~flask.Flask.root_path`. + :param defaults: an optional dictionary of default values + """ + + def __init__(self, root_path, defaults=None): + dict.__init__(self, defaults or {}) + self.root_path = root_path + + def from_envvar(self, variable_name, silent=False): + """Loads a configuration from an environment variable pointing to + a configuration file. This is basically just a shortcut with nicer + error messages for this line of code:: + + app.config.from_pyfile(os.environ['YOURAPPLICATION_SETTINGS']) + + :param variable_name: name of the environment variable + :param silent: set to `True` if you want silent failure for missing + files. + :return: bool. `True` if able to load config, `False` otherwise. + """ + rv = os.environ.get(variable_name) + if not rv: + if silent: + return False + raise RuntimeError('The environment variable %r is not set ' + 'and as such configuration could not be ' + 'loaded. Set this variable and make it ' + 'point to a configuration file' % + variable_name) + self.from_pyfile(rv) + return True + + def from_pyfile(self, filename, silent=False): + """Updates the values in the config from a Python file. This function + behaves as if the file was imported as module with the + :meth:`from_object` function. + + :param filename: the filename of the config. This can either be an + absolute filename or a filename relative to the + root path. + :param silent: set to `True` if you want silent failure for missing + files. + + .. versionadded:: 0.7 + `silent` parameter. + """ + filename = os.path.join(self.root_path, filename) + d = imp.new_module('config') + d.__file__ = filename + try: + execfile(filename, d.__dict__) + except IOError, e: + if silent and e.errno in (errno.ENOENT, errno.EISDIR): + return False + e.strerror = 'Unable to load configuration file (%s)' % e.strerror + raise + self.from_object(d) + return True + + def from_object(self, obj): + """Updates the values from the given object. An object can be of one + of the following two types: + + - a string: in this case the object with that name will be imported + - an actual object reference: that object is used directly + + Objects are usually either modules or classes. + + Just the uppercase variables in that object are stored in the config. + Example usage:: + + app.config.from_object('yourapplication.default_config') + from yourapplication import default_config + app.config.from_object(default_config) + + You should not use this function to load the actual configuration but + rather configuration defaults. The actual config should be loaded + with :meth:`from_pyfile` and ideally from a location not within the + package because the package might be installed system wide. + + :param obj: an import name or object + """ + if isinstance(obj, basestring): + obj = import_string(obj) + for key in dir(obj): + if key.isupper(): + self[key] = getattr(obj, key) + + def __repr__(self): + return '<%s %s>' % (self.__class__.__name__, dict.__repr__(self)) diff --git a/websdk/flask/ctx.py b/websdk/flask/ctx.py new file mode 100644 index 0000000..26781db --- /dev/null +++ b/websdk/flask/ctx.py @@ -0,0 +1,175 @@ +# -*- coding: utf-8 -*- +""" + flask.ctx + ~~~~~~~~~ + + Implements the objects required to keep the context. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" + +from werkzeug.exceptions import HTTPException + +from .globals import _request_ctx_stack +from .module import blueprint_is_module + + +class _RequestGlobals(object): + pass + + +def has_request_context(): + """If you have code that wants to test if a request context is there or + not this function can be used. For instance if you want to take advantage + of request information is it's available but fail silently if the request + object is unavailable. + + :: + + class User(db.Model): + + def __init__(self, username, remote_addr=None): + self.username = username + if remote_addr is None and has_request_context(): + remote_addr = request.remote_addr + self.remote_addr = remote_addr + + Alternatively you can also just test any of the context bound objects + (such as :class:`request` or :class:`g` for truthness):: + + class User(db.Model): + + def __init__(self, username, remote_addr=None): + self.username = username + if remote_addr is None and request: + remote_addr = request.remote_addr + self.remote_addr = remote_addr + + .. versionadded:: 0.7 + """ + return _request_ctx_stack.top is not None + + +class RequestContext(object): + """The request context contains all request relevant information. It is + created at the beginning of the request and pushed to the + `_request_ctx_stack` and removed at the end of it. It will create the + URL adapter and request object for the WSGI environment provided. + + Do not attempt to use this class directly, instead use + :meth:`~flask.Flask.test_request_context` and + :meth:`~flask.Flask.request_context` to create this object. + + When the request context is popped, it will evaluate all the + functions registered on the application for teardown execution + (:meth:`~flask.Flask.teardown_request`). + + The request context is automatically popped at the end of the request + for you. In debug mode the request context is kept around if + exceptions happen so that interactive debuggers have a chance to + introspect the data. With 0.4 this can also be forced for requests + that did not fail and outside of `DEBUG` mode. By setting + ``'flask._preserve_context'`` to `True` on the WSGI environment the + context will not pop itself at the end of the request. This is used by + the :meth:`~flask.Flask.test_client` for example to implement the + deferred cleanup functionality. + + You might find this helpful for unittests where you need the + information from the context local around for a little longer. Make + sure to properly :meth:`~werkzeug.LocalStack.pop` the stack yourself in + that situation, otherwise your unittests will leak memory. + """ + + def __init__(self, app, environ): + self.app = app + self.request = app.request_class(environ) + self.url_adapter = app.create_url_adapter(self.request) + self.g = _RequestGlobals() + self.flashes = None + self.session = None + + # indicator if the context was preserved. Next time another context + # is pushed the preserved context is popped. + self.preserved = False + + self.match_request() + + # XXX: Support for deprecated functionality. This is doing away with + # Flask 1.0 + blueprint = self.request.blueprint + if blueprint is not None: + # better safe than sorry, we don't want to break code that + # already worked + bp = app.blueprints.get(blueprint) + if bp is not None and blueprint_is_module(bp): + self.request._is_old_module = True + + def match_request(self): + """Can be overridden by a subclass to hook into the matching + of the request. + """ + try: + url_rule, self.request.view_args = \ + self.url_adapter.match(return_rule=True) + self.request.url_rule = url_rule + except HTTPException, e: + self.request.routing_exception = e + + def push(self): + """Binds the request context to the current context.""" + # If an exception ocurrs in debug mode or if context preservation is + # activated under exception situations exactly one context stays + # on the stack. The rationale is that you want to access that + # information under debug situations. However if someone forgets to + # pop that context again we want to make sure that on the next push + # it's invalidated otherwise we run at risk that something leaks + # memory. This is usually only a problem in testsuite since this + # functionality is not active in production environments. + top = _request_ctx_stack.top + if top is not None and top.preserved: + top.pop() + + _request_ctx_stack.push(self) + + # Open the session at the moment that the request context is + # available. This allows a custom open_session method to use the + # request context (e.g. flask-sqlalchemy). + self.session = self.app.open_session(self.request) + if self.session is None: + self.session = self.app.make_null_session() + + def pop(self): + """Pops the request context and unbinds it by doing that. This will + also trigger the execution of functions registered by the + :meth:`~flask.Flask.teardown_request` decorator. + """ + self.preserved = False + self.app.do_teardown_request() + rv = _request_ctx_stack.pop() + assert rv is self, 'Popped wrong request context. (%r instead of %r)' \ + % (rv, self) + + def __enter__(self): + self.push() + return self + + def __exit__(self, exc_type, exc_value, tb): + # do not pop the request stack if we are in debug mode and an + # exception happened. This will allow the debugger to still + # access the request object in the interactive shell. Furthermore + # the context can be force kept alive for the test client. + # See flask.testing for how this works. + if self.request.environ.get('flask._preserve_context') or \ + (tb is not None and self.app.preserve_context_on_exception): + self.preserved = True + else: + self.pop() + + def __repr__(self): + return '<%s \'%s\' [%s] of %s>' % ( + self.__class__.__name__, + self.request.url, + self.request.method, + self.app.name + ) diff --git a/websdk/flask/debughelpers.py b/websdk/flask/debughelpers.py new file mode 100644 index 0000000..edf8c11 --- /dev/null +++ b/websdk/flask/debughelpers.py @@ -0,0 +1,79 @@ +# -*- coding: utf-8 -*- +""" + flask.debughelpers + ~~~~~~~~~~~~~~~~~~ + + Various helpers to make the development experience better. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" + + +class DebugFilesKeyError(KeyError, AssertionError): + """Raised from request.files during debugging. The idea is that it can + provide a better error message than just a generic KeyError/BadRequest. + """ + + def __init__(self, request, key): + form_matches = request.form.getlist(key) + buf = ['You tried to access the file "%s" in the request.files ' + 'dictionary but it does not exist. The mimetype for the request ' + 'is "%s" instead of "multipart/form-data" which means that no ' + 'file contents were transmitted. To fix this error you should ' + 'provide enctype="multipart/form-data" in your form.' % + (key, request.mimetype)] + if form_matches: + buf.append('\n\nThe browser instead transmitted some file names. ' + 'This was submitted: %s' % ', '.join('"%s"' % x + for x in form_matches)) + self.msg = ''.join(buf).encode('utf-8') + + def __str__(self): + return self.msg + + +class FormDataRoutingRedirect(AssertionError): + """This exception is raised by Flask in debug mode if it detects a + redirect caused by the routing system when the request method is not + GET, HEAD or OPTIONS. Reasoning: form data will be dropped. + """ + + def __init__(self, request): + exc = request.routing_exception + buf = ['A request was sent to this URL (%s) but a redirect was ' + 'issued automatically by the routing system to "%s".' + % (request.url, exc.new_url)] + + # In case just a slash was appended we can be extra helpful + if request.base_url + '/' == exc.new_url.split('?')[0]: + buf.append(' The URL was defined with a trailing slash so ' + 'Flask will automatically redirect to the URL ' + 'with the trailing slash if it was accessed ' + 'without one.') + + buf.append(' Make sure to directly send your %s-request to this URL ' + 'since we can\'t make browsers or HTTP clients redirect ' + 'with form data reliably or without user interaction.' % + request.method) + buf.append('\n\nNote: this exception is only raised in debug mode') + AssertionError.__init__(self, ''.join(buf).encode('utf-8')) + + +def attach_enctype_error_multidict(request): + """Since Flask 0.8 we're monkeypatching the files object in case a + request is detected that does not use multipart form data but the files + object is accessed. + """ + oldcls = request.files.__class__ + class newcls(oldcls): + def __getitem__(self, key): + try: + return oldcls.__getitem__(self, key) + except KeyError, e: + if key not in request.form: + raise + raise DebugFilesKeyError(request, key) + newcls.__name__ = oldcls.__name__ + newcls.__module__ = oldcls.__module__ + request.files.__class__ = newcls diff --git a/websdk/flask/ext/__init__.py b/websdk/flask/ext/__init__.py new file mode 100644 index 0000000..f29958a --- /dev/null +++ b/websdk/flask/ext/__init__.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +""" + flask.ext + ~~~~~~~~~ + + Redirect imports for extensions. This module basically makes it possible + for us to transition from flaskext.foo to flask_foo without having to + force all extensions to upgrade at the same time. + + When a user does ``from flask.ext.foo import bar`` it will attempt to + import ``from flask_foo import bar`` first and when that fails it will + try to import ``from flaskext.foo import bar``. + + We're switching from namespace packages because it was just too painful for + everybody involved. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" + + +def setup(): + from ..exthook import ExtensionImporter + importer = ExtensionImporter(['flask_%s', 'flaskext.%s'], __name__) + importer.install() + + +setup() +del setup diff --git a/websdk/flask/exthook.py b/websdk/flask/exthook.py new file mode 100644 index 0000000..bb1deb2 --- /dev/null +++ b/websdk/flask/exthook.py @@ -0,0 +1,119 @@ +# -*- coding: utf-8 -*- +""" + flask.exthook + ~~~~~~~~~~~~~ + + Redirect imports for extensions. This module basically makes it possible + for us to transition from flaskext.foo to flask_foo without having to + force all extensions to upgrade at the same time. + + When a user does ``from flask.ext.foo import bar`` it will attempt to + import ``from flask_foo import bar`` first and when that fails it will + try to import ``from flaskext.foo import bar``. + + We're switching from namespace packages because it was just too painful for + everybody involved. + + This is used by `flask.ext`. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +import sys +import os + + +class ExtensionImporter(object): + """This importer redirects imports from this submodule to other locations. + This makes it possible to transition from the old flaskext.name to the + newer flask_name without people having a hard time. + """ + + def __init__(self, module_choices, wrapper_module): + self.module_choices = module_choices + self.wrapper_module = wrapper_module + self.prefix = wrapper_module + '.' + self.prefix_cutoff = wrapper_module.count('.') + 1 + + def __eq__(self, other): + return self.__class__.__module__ == other.__class__.__module__ and \ + self.__class__.__name__ == other.__class__.__name__ and \ + self.wrapper_module == other.wrapper_module and \ + self.module_choices == other.module_choices + + def __ne__(self, other): + return not self.__eq__(other) + + def install(self): + sys.meta_path[:] = [x for x in sys.meta_path if self != x] + [self] + + def find_module(self, fullname, path=None): + if fullname.startswith(self.prefix): + return self + + def load_module(self, fullname): + if fullname in sys.modules: + return sys.modules[fullname] + modname = fullname.split('.', self.prefix_cutoff)[self.prefix_cutoff] + for path in self.module_choices: + realname = path % modname + try: + __import__(realname) + except ImportError: + exc_type, exc_value, tb = sys.exc_info() + # since we only establish the entry in sys.modules at the + # very this seems to be redundant, but if recursive imports + # happen we will call into the move import a second time. + # On the second invocation we still don't have an entry for + # fullname in sys.modules, but we will end up with the same + # fake module name and that import will succeed since this + # one already has a temporary entry in the modules dict. + # Since this one "succeeded" temporarily that second + # invocation now will have created a fullname entry in + # sys.modules which we have to kill. + sys.modules.pop(fullname, None) + + # If it's an important traceback we reraise it, otherwise + # we swallow it and try the next choice. The skipped frame + # is the one from __import__ above which we don't care about + if self.is_important_traceback(realname, tb): + raise exc_type, exc_value, tb.tb_next + continue + module = sys.modules[fullname] = sys.modules[realname] + if '.' not in modname: + setattr(sys.modules[self.wrapper_module], modname, module) + return module + raise ImportError('No module named %s' % fullname) + + def is_important_traceback(self, important_module, tb): + """Walks a traceback's frames and checks if any of the frames + originated in the given important module. If that is the case then we + were able to import the module itself but apparently something went + wrong when the module was imported. (Eg: import of an import failed). + """ + while tb is not None: + if self.is_important_frame(important_module, tb): + return True + tb = tb.tb_next + return False + + def is_important_frame(self, important_module, tb): + """Checks a single frame if it's important.""" + g = tb.tb_frame.f_globals + if '__name__' not in g: + return False + + module_name = g['__name__'] + + # Python 2.7 Behavior. Modules are cleaned up late so the + # name shows up properly here. Success! + if module_name == important_module: + return True + + # Some python verisons will will clean up modules so early that the + # module name at that point is no longer set. Try guessing from + # the filename then. + filename = os.path.abspath(tb.tb_frame.f_code.co_filename) + test_string = os.path.sep + important_module.replace('.', os.path.sep) + return test_string + '.py' in filename or \ + test_string + os.path.sep + '__init__.py' in filename diff --git a/websdk/flask/globals.py b/websdk/flask/globals.py new file mode 100644 index 0000000..16580d1 --- /dev/null +++ b/websdk/flask/globals.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +""" + flask.globals + ~~~~~~~~~~~~~ + + Defines all the global objects that are proxies to the current + active context. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" + +from functools import partial +from werkzeug.local import LocalStack, LocalProxy + +def _lookup_object(name): + top = _request_ctx_stack.top + if top is None: + raise RuntimeError('working outside of request context') + return getattr(top, name) + + +# context locals +_request_ctx_stack = LocalStack() +current_app = LocalProxy(partial(_lookup_object, 'app')) +request = LocalProxy(partial(_lookup_object, 'request')) +session = LocalProxy(partial(_lookup_object, 'session')) +g = LocalProxy(partial(_lookup_object, 'g')) diff --git a/websdk/flask/helpers.py b/websdk/flask/helpers.py new file mode 100644 index 0000000..72c8f17 --- /dev/null +++ b/websdk/flask/helpers.py @@ -0,0 +1,649 @@ +# -*- coding: utf-8 -*- +""" + flask.helpers + ~~~~~~~~~~~~~ + + Implements various helpers. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import with_statement + +import os +import sys +import posixpath +import mimetypes +from time import time +from zlib import adler32 +from threading import RLock + +# try to load the best simplejson implementation available. If JSON +# is not installed, we add a failing class. +json_available = True +json = None +try: + import simplejson as json +except ImportError: + try: + import json + except ImportError: + try: + # Google Appengine offers simplejson via django + from django.utils import simplejson as json + except ImportError: + json_available = False + + +from werkzeug.datastructures import Headers +from werkzeug.exceptions import NotFound + +# this was moved in 0.7 +try: + from werkzeug.wsgi import wrap_file +except ImportError: + from werkzeug.utils import wrap_file + +from jinja2 import FileSystemLoader + +from .globals import session, _request_ctx_stack, current_app, request + + +def _assert_have_json(): + """Helper function that fails if JSON is unavailable.""" + if not json_available: + raise RuntimeError('simplejson not installed') + +# figure out if simplejson escapes slashes. This behaviour was changed +# from one version to another without reason. +if not json_available or '\\/' not in json.dumps('/'): + + def _tojson_filter(*args, **kwargs): + if __debug__: + _assert_have_json() + return json.dumps(*args, **kwargs).replace('/', '\\/') +else: + _tojson_filter = json.dumps + + +# sentinel +_missing = object() + + +# what separators does this operating system provide that are not a slash? +# this is used by the send_from_directory function to ensure that nobody is +# able to access files from outside the filesystem. +_os_alt_seps = list(sep for sep in [os.path.sep, os.path.altsep] + if sep not in (None, '/')) + + +def _endpoint_from_view_func(view_func): + """Internal helper that returns the default endpoint for a given + function. This always is the function name. + """ + assert view_func is not None, 'expected view func if endpoint ' \ + 'is not provided.' + return view_func.__name__ + + +def jsonify(*args, **kwargs): + """Creates a :class:`~flask.Response` with the JSON representation of + the given arguments with an `application/json` mimetype. The arguments + to this function are the same as to the :class:`dict` constructor. + + Example usage:: + + @app.route('/_get_current_user') + def get_current_user(): + return jsonify(username=g.user.username, + email=g.user.email, + id=g.user.id) + + This will send a JSON response like this to the browser:: + + { + "username": "admin", + "email": "admin@localhost", + "id": 42 + } + + This requires Python 2.6 or an installed version of simplejson. For + security reasons only objects are supported toplevel. For more + information about this, have a look at :ref:`json-security`. + + .. versionadded:: 0.2 + """ + if __debug__: + _assert_have_json() + return current_app.response_class(json.dumps(dict(*args, **kwargs), + indent=None if request.is_xhr else 2), mimetype='application/json') + + +def make_response(*args): + """Sometimes it is necessary to set additional headers in a view. Because + views do not have to return response objects but can return a value that + is converted into a response object by Flask itself, it becomes tricky to + add headers to it. This function can be called instead of using a return + and you will get a response object which you can use to attach headers. + + If view looked like this and you want to add a new header:: + + def index(): + return render_template('index.html', foo=42) + + You can now do something like this:: + + def index(): + response = make_response(render_template('index.html', foo=42)) + response.headers['X-Parachutes'] = 'parachutes are cool' + return response + + This function accepts the very same arguments you can return from a + view function. This for example creates a response with a 404 error + code:: + + response = make_response(render_template('not_found.html'), 404) + + The other use case of this function is to force the return value of a + view function into a response which is helpful with view + decorators:: + + response = make_response(view_function()) + response.headers['X-Parachutes'] = 'parachutes are cool' + + Internally this function does the following things: + + - if no arguments are passed, it creates a new response argument + - if one argument is passed, :meth:`flask.Flask.make_response` + is invoked with it. + - if more than one argument is passed, the arguments are passed + to the :meth:`flask.Flask.make_response` function as tuple. + + .. versionadded:: 0.6 + """ + if not args: + return current_app.response_class() + if len(args) == 1: + args = args[0] + return current_app.make_response(args) + + +def url_for(endpoint, **values): + """Generates a URL to the given endpoint with the method provided. + + Variable arguments that are unknown to the target endpoint are appended + to the generated URL as query arguments. If the value of a query argument + is `None`, the whole pair is skipped. In case blueprints are active + you can shortcut references to the same blueprint by prefixing the + local endpoint with a dot (``.``). + + This will reference the index function local to the current blueprint:: + + url_for('.index') + + For more information, head over to the :ref:`Quickstart `. + + :param endpoint: the endpoint of the URL (name of the function) + :param values: the variable arguments of the URL rule + :param _external: if set to `True`, an absolute URL is generated. + """ + ctx = _request_ctx_stack.top + blueprint_name = request.blueprint + if not ctx.request._is_old_module: + if endpoint[:1] == '.': + if blueprint_name is not None: + endpoint = blueprint_name + endpoint + else: + endpoint = endpoint[1:] + else: + # TODO: get rid of this deprecated functionality in 1.0 + if '.' not in endpoint: + if blueprint_name is not None: + endpoint = blueprint_name + '.' + endpoint + elif endpoint.startswith('.'): + endpoint = endpoint[1:] + external = values.pop('_external', False) + ctx.app.inject_url_defaults(endpoint, values) + return ctx.url_adapter.build(endpoint, values, force_external=external) + + +def get_template_attribute(template_name, attribute): + """Loads a macro (or variable) a template exports. This can be used to + invoke a macro from within Python code. If you for example have a + template named `_cider.html` with the following contents: + + .. sourcecode:: html+jinja + + {% macro hello(name) %}Hello {{ name }}!{% endmacro %} + + You can access this from Python code like this:: + + hello = get_template_attribute('_cider.html', 'hello') + return hello('World') + + .. versionadded:: 0.2 + + :param template_name: the name of the template + :param attribute: the name of the variable of macro to acccess + """ + return getattr(current_app.jinja_env.get_template(template_name).module, + attribute) + + +def flash(message, category='message'): + """Flashes a message to the next request. In order to remove the + flashed message from the session and to display it to the user, + the template has to call :func:`get_flashed_messages`. + + .. versionchanged: 0.3 + `category` parameter added. + + :param message: the message to be flashed. + :param category: the category for the message. The following values + are recommended: ``'message'`` for any kind of message, + ``'error'`` for errors, ``'info'`` for information + messages and ``'warning'`` for warnings. However any + kind of string can be used as category. + """ + session.setdefault('_flashes', []).append((category, message)) + + +def get_flashed_messages(with_categories=False): + """Pulls all flashed messages from the session and returns them. + Further calls in the same request to the function will return + the same messages. By default just the messages are returned, + but when `with_categories` is set to `True`, the return value will + be a list of tuples in the form ``(category, message)`` instead. + + Example usage: + + .. sourcecode:: html+jinja + + {% for category, msg in get_flashed_messages(with_categories=true) %} +

{{ msg }} + {% endfor %} + + .. versionchanged:: 0.3 + `with_categories` parameter added. + + :param with_categories: set to `True` to also receive categories. + """ + flashes = _request_ctx_stack.top.flashes + if flashes is None: + _request_ctx_stack.top.flashes = flashes = session.pop('_flashes') \ + if '_flashes' in session else [] + if not with_categories: + return [x[1] for x in flashes] + return flashes + + +def send_file(filename_or_fp, mimetype=None, as_attachment=False, + attachment_filename=None, add_etags=True, + cache_timeout=60 * 60 * 12, conditional=False): + """Sends the contents of a file to the client. This will use the + most efficient method available and configured. By default it will + try to use the WSGI server's file_wrapper support. Alternatively + you can set the application's :attr:`~Flask.use_x_sendfile` attribute + to ``True`` to directly emit an `X-Sendfile` header. This however + requires support of the underlying webserver for `X-Sendfile`. + + By default it will try to guess the mimetype for you, but you can + also explicitly provide one. For extra security you probably want + to send certain files as attachment (HTML for instance). The mimetype + guessing requires a `filename` or an `attachment_filename` to be + provided. + + Please never pass filenames to this function from user sources without + checking them first. Something like this is usually sufficient to + avoid security problems:: + + if '..' in filename or filename.startswith('/'): + abort(404) + + .. versionadded:: 0.2 + + .. versionadded:: 0.5 + The `add_etags`, `cache_timeout` and `conditional` parameters were + added. The default behaviour is now to attach etags. + + .. versionchanged:: 0.7 + mimetype guessing and etag support for file objects was + deprecated because it was unreliable. Pass a filename if you are + able to, otherwise attach an etag yourself. This functionality + will be removed in Flask 1.0 + + :param filename_or_fp: the filename of the file to send. This is + relative to the :attr:`~Flask.root_path` if a + relative path is specified. + Alternatively a file object might be provided + in which case `X-Sendfile` might not work and + fall back to the traditional method. Make sure + that the file pointer is positioned at the start + of data to send before calling :func:`send_file`. + :param mimetype: the mimetype of the file if provided, otherwise + auto detection happens. + :param as_attachment: set to `True` if you want to send this file with + a ``Content-Disposition: attachment`` header. + :param attachment_filename: the filename for the attachment if it + differs from the file's filename. + :param add_etags: set to `False` to disable attaching of etags. + :param conditional: set to `True` to enable conditional responses. + :param cache_timeout: the timeout in seconds for the headers. + """ + mtime = None + if isinstance(filename_or_fp, basestring): + filename = filename_or_fp + file = None + else: + from warnings import warn + file = filename_or_fp + filename = getattr(file, 'name', None) + + # XXX: this behaviour is now deprecated because it was unreliable. + # removed in Flask 1.0 + if not attachment_filename and not mimetype \ + and isinstance(filename, basestring): + warn(DeprecationWarning('The filename support for file objects ' + 'passed to send_file is now deprecated. Pass an ' + 'attach_filename if you want mimetypes to be guessed.'), + stacklevel=2) + if add_etags: + warn(DeprecationWarning('In future flask releases etags will no ' + 'longer be generated for file objects passed to the send_file ' + 'function because this behaviour was unreliable. Pass ' + 'filenames instead if possible, otherwise attach an etag ' + 'yourself based on another value'), stacklevel=2) + + if filename is not None: + if not os.path.isabs(filename): + filename = os.path.join(current_app.root_path, filename) + if mimetype is None and (filename or attachment_filename): + mimetype = mimetypes.guess_type(filename or attachment_filename)[0] + if mimetype is None: + mimetype = 'application/octet-stream' + + headers = Headers() + if as_attachment: + if attachment_filename is None: + if filename is None: + raise TypeError('filename unavailable, required for ' + 'sending as attachment') + attachment_filename = os.path.basename(filename) + headers.add('Content-Disposition', 'attachment', + filename=attachment_filename) + + if current_app.use_x_sendfile and filename: + if file is not None: + file.close() + headers['X-Sendfile'] = filename + data = None + else: + if file is None: + file = open(filename, 'rb') + mtime = os.path.getmtime(filename) + data = wrap_file(request.environ, file) + + rv = current_app.response_class(data, mimetype=mimetype, headers=headers, + direct_passthrough=True) + + # if we know the file modification date, we can store it as the + # the time of the last modification. + if mtime is not None: + rv.last_modified = int(mtime) + + rv.cache_control.public = True + if cache_timeout: + rv.cache_control.max_age = cache_timeout + rv.expires = int(time() + cache_timeout) + + if add_etags and filename is not None: + rv.set_etag('flask-%s-%s-%s' % ( + os.path.getmtime(filename), + os.path.getsize(filename), + adler32( + filename.encode('utf8') if isinstance(filename, unicode) + else filename + ) & 0xffffffff + )) + if conditional: + rv = rv.make_conditional(request) + # make sure we don't send x-sendfile for servers that + # ignore the 304 status code for x-sendfile. + if rv.status_code == 304: + rv.headers.pop('x-sendfile', None) + return rv + + +def safe_join(directory, filename): + """Safely join `directory` and `filename`. + + Example usage:: + + @app.route('/wiki/') + def wiki_page(filename): + filename = safe_join(app.config['WIKI_FOLDER'], filename) + with open(filename, 'rb') as fd: + content = fd.read() # Read and process the file content... + + :param directory: the base directory. + :param filename: the untrusted filename relative to that directory. + :raises: :class:`~werkzeug.exceptions.NotFound` if the resulting path + would fall out of `directory`. + """ + filename = posixpath.normpath(filename) + for sep in _os_alt_seps: + if sep in filename: + raise NotFound() + if os.path.isabs(filename) or filename.startswith('../'): + raise NotFound() + return os.path.join(directory, filename) + + +def send_from_directory(directory, filename, **options): + """Send a file from a given directory with :func:`send_file`. This + is a secure way to quickly expose static files from an upload folder + or something similar. + + Example usage:: + + @app.route('/uploads/') + def download_file(filename): + return send_from_directory(app.config['UPLOAD_FOLDER'], + filename, as_attachment=True) + + .. admonition:: Sending files and Performance + + It is strongly recommended to activate either `X-Sendfile` support in + your webserver or (if no authentication happens) to tell the webserver + to serve files for the given path on its own without calling into the + web application for improved performance. + + .. versionadded:: 0.5 + + :param directory: the directory where all the files are stored. + :param filename: the filename relative to that directory to + download. + :param options: optional keyword arguments that are directly + forwarded to :func:`send_file`. + """ + filename = safe_join(directory, filename) + if not os.path.isfile(filename): + raise NotFound() + return send_file(filename, conditional=True, **options) + + +def get_root_path(import_name): + """Returns the path to a package or cwd if that cannot be found. This + returns the path of a package or the folder that contains a module. + + Not to be confused with the package path returned by :func:`find_package`. + """ + __import__(import_name) + try: + directory = os.path.dirname(sys.modules[import_name].__file__) + return os.path.abspath(directory) + except AttributeError: + # this is necessary in case we are running from the interactive + # python shell. It will never be used for production code however + return os.getcwd() + + +def find_package(import_name): + """Finds a package and returns the prefix (or None if the package is + not installed) as well as the folder that contains the package or + module as a tuple. The package path returned is the module that would + have to be added to the pythonpath in order to make it possible to + import the module. The prefix is the path below which a UNIX like + folder structure exists (lib, share etc.). + """ + __import__(import_name) + root_mod = sys.modules[import_name.split('.')[0]] + package_path = getattr(root_mod, '__file__', None) + if package_path is None: + # support for the interactive python shell + package_path = os.getcwd() + else: + package_path = os.path.abspath(os.path.dirname(package_path)) + if hasattr(root_mod, '__path__'): + package_path = os.path.dirname(package_path) + + # leave the egg wrapper folder or the actual .egg on the filesystem + test_package_path = package_path + if os.path.basename(test_package_path).endswith('.egg'): + test_package_path = os.path.dirname(test_package_path) + + site_parent, site_folder = os.path.split(test_package_path) + py_prefix = os.path.abspath(sys.prefix) + if test_package_path.startswith(py_prefix): + return py_prefix, package_path + elif site_folder.lower() == 'site-packages': + parent, folder = os.path.split(site_parent) + # Windows like installations + if folder.lower() == 'lib': + base_dir = parent + # UNIX like installations + elif os.path.basename(parent).lower() == 'lib': + base_dir = os.path.dirname(parent) + else: + base_dir = site_parent + return base_dir, package_path + return None, package_path + + +class locked_cached_property(object): + """A decorator that converts a function into a lazy property. The + function wrapped is called the first time to retrieve the result + and then that calculated result is used the next time you access + the value. Works like the one in Werkzeug but has a lock for + thread safety. + """ + + def __init__(self, func, name=None, doc=None): + self.__name__ = name or func.__name__ + self.__module__ = func.__module__ + self.__doc__ = doc or func.__doc__ + self.func = func + self.lock = RLock() + + def __get__(self, obj, type=None): + if obj is None: + return self + with self.lock: + value = obj.__dict__.get(self.__name__, _missing) + if value is _missing: + value = self.func(obj) + obj.__dict__[self.__name__] = value + return value + + +class _PackageBoundObject(object): + + def __init__(self, import_name, template_folder=None): + #: The name of the package or module. Do not change this once + #: it was set by the constructor. + self.import_name = import_name + + #: location of the templates. `None` if templates should not be + #: exposed. + self.template_folder = template_folder + + #: Where is the app root located? + self.root_path = get_root_path(self.import_name) + + self._static_folder = None + self._static_url_path = None + + def _get_static_folder(self): + if self._static_folder is not None: + return os.path.join(self.root_path, self._static_folder) + def _set_static_folder(self, value): + self._static_folder = value + static_folder = property(_get_static_folder, _set_static_folder) + del _get_static_folder, _set_static_folder + + def _get_static_url_path(self): + if self._static_url_path is None: + if self.static_folder is None: + return None + return '/' + os.path.basename(self.static_folder) + return self._static_url_path + def _set_static_url_path(self, value): + self._static_url_path = value + static_url_path = property(_get_static_url_path, _set_static_url_path) + del _get_static_url_path, _set_static_url_path + + @property + def has_static_folder(self): + """This is `True` if the package bound object's container has a + folder named ``'static'``. + + .. versionadded:: 0.5 + """ + return self.static_folder is not None + + @locked_cached_property + def jinja_loader(self): + """The Jinja loader for this package bound object. + + .. versionadded:: 0.5 + """ + if self.template_folder is not None: + return FileSystemLoader(os.path.join(self.root_path, + self.template_folder)) + + def send_static_file(self, filename): + """Function used internally to send static files from the static + folder to the browser. + + .. versionadded:: 0.5 + """ + if not self.has_static_folder: + raise RuntimeError('No static folder for this object') + return send_from_directory(self.static_folder, filename) + + def open_resource(self, resource, mode='rb'): + """Opens a resource from the application's resource folder. To see + how this works, consider the following folder structure:: + + /myapplication.py + /schema.sql + /static + /style.css + /templates + /layout.html + /index.html + + If you want to open the `schema.sql` file you would do the + following:: + + with app.open_resource('schema.sql') as f: + contents = f.read() + do_something_with(contents) + + :param resource: the name of the resource. To access resources within + subfolders use forward slashes as separator. + """ + if mode not in ('r', 'rb'): + raise ValueError('Resources can only be opened for reading') + return open(os.path.join(self.root_path, resource), mode) diff --git a/websdk/flask/logging.py b/websdk/flask/logging.py new file mode 100644 index 0000000..b992aef --- /dev/null +++ b/websdk/flask/logging.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +""" + flask.logging + ~~~~~~~~~~~~~ + + Implements the logging support for Flask. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import absolute_import + +from logging import getLogger, StreamHandler, Formatter, getLoggerClass, DEBUG + + +def create_logger(app): + """Creates a logger for the given application. This logger works + similar to a regular Python logger but changes the effective logging + level based on the application's debug flag. Furthermore this + function also removes all attached handlers in case there was a + logger with the log name before. + """ + Logger = getLoggerClass() + + class DebugLogger(Logger): + def getEffectiveLevel(x): + return DEBUG if app.debug else Logger.getEffectiveLevel(x) + + class DebugHandler(StreamHandler): + def emit(x, record): + StreamHandler.emit(x, record) if app.debug else None + + handler = DebugHandler() + handler.setLevel(DEBUG) + handler.setFormatter(Formatter(app.debug_log_format)) + logger = getLogger(app.logger_name) + # just in case that was not a new logger, get rid of all the handlers + # already attached to it. + del logger.handlers[:] + logger.__class__ = DebugLogger + logger.addHandler(handler) + return logger diff --git a/websdk/flask/module.py b/websdk/flask/module.py new file mode 100644 index 0000000..1c4f466 --- /dev/null +++ b/websdk/flask/module.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +""" + flask.module + ~~~~~~~~~~~~ + + Implements a class that represents module blueprints. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" + +import os + +from .blueprints import Blueprint + + +def blueprint_is_module(bp): + """Used to figure out if something is actually a module""" + return isinstance(bp, Module) + + +class Module(Blueprint): + """Deprecated module support. Until Flask 0.6 modules were a different + name of the concept now available as blueprints in Flask. They are + essentially doing the same but have some bad semantics for templates and + static files that were fixed with blueprints. + + .. versionchanged:: 0.7 + Modules were deprecated in favor for blueprints. + """ + + def __init__(self, import_name, name=None, url_prefix=None, + static_path=None, subdomain=None): + if name is None: + assert '.' in import_name, 'name required if package name ' \ + 'does not point to a submodule' + name = import_name.rsplit('.', 1)[1] + Blueprint.__init__(self, name, import_name, url_prefix=url_prefix, + subdomain=subdomain, template_folder='templates') + + if os.path.isdir(os.path.join(self.root_path, 'static')): + self._static_folder = 'static' diff --git a/websdk/flask/session.py b/websdk/flask/session.py new file mode 100644 index 0000000..4d4d2cd --- /dev/null +++ b/websdk/flask/session.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +""" + flask.session + ~~~~~~~~~~~~~ + + This module used to flask with the session global so we moved it + over to flask.sessions + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" + +from warnings import warn +warn(DeprecationWarning('please use flask.sessions instead')) + +from .sessions import * + +Session = SecureCookieSession +_NullSession = NullSession diff --git a/websdk/flask/sessions.py b/websdk/flask/sessions.py new file mode 100644 index 0000000..2795bb1 --- /dev/null +++ b/websdk/flask/sessions.py @@ -0,0 +1,205 @@ +# -*- coding: utf-8 -*- +""" + flask.sessions + ~~~~~~~~~~~~~~ + + Implements cookie based sessions based on Werkzeug's secure cookie + system. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" + +from datetime import datetime +from werkzeug.contrib.securecookie import SecureCookie + + +class SessionMixin(object): + """Expands a basic dictionary with an accessors that are expected + by Flask extensions and users for the session. + """ + + def _get_permanent(self): + return self.get('_permanent', False) + + def _set_permanent(self, value): + self['_permanent'] = bool(value) + + #: this reflects the ``'_permanent'`` key in the dict. + permanent = property(_get_permanent, _set_permanent) + del _get_permanent, _set_permanent + + #: some session backends can tell you if a session is new, but that is + #: not necessarily guaranteed. Use with caution. The default mixin + #: implementation just hardcodes `False` in. + new = False + + #: for some backends this will always be `True`, but some backends will + #: default this to false and detect changes in the dictionary for as + #: long as changes do not happen on mutable structures in the session. + #: The default mixin implementation just hardcodes `True` in. + modified = True + + +class SecureCookieSession(SecureCookie, SessionMixin): + """Expands the session with support for switching between permanent + and non-permanent sessions. + """ + + +class NullSession(SecureCookieSession): + """Class used to generate nicer error messages if sessions are not + available. Will still allow read-only access to the empty session + but fail on setting. + """ + + def _fail(self, *args, **kwargs): + raise RuntimeError('the session is unavailable because no secret ' + 'key was set. Set the secret_key on the ' + 'application to something unique and secret.') + __setitem__ = __delitem__ = clear = pop = popitem = \ + update = setdefault = _fail + del _fail + + +class SessionInterface(object): + """The basic interface you have to implement in order to replace the + default session interface which uses werkzeug's securecookie + implementation. The only methods you have to implement are + :meth:`open_session` and :meth:`save_session`, the others have + useful defaults which you don't need to change. + + The session object returned by the :meth:`open_session` method has to + provide a dictionary like interface plus the properties and methods + from the :class:`SessionMixin`. We recommend just subclassing a dict + and adding that mixin:: + + class Session(dict, SessionMixin): + pass + + If :meth:`open_session` returns `None` Flask will call into + :meth:`make_null_session` to create a session that acts as replacement + if the session support cannot work because some requirement is not + fulfilled. The default :class:`NullSession` class that is created + will complain that the secret key was not set. + + To replace the session interface on an application all you have to do + is to assign :attr:`flask.Flask.session_interface`:: + + app = Flask(__name__) + app.session_interface = MySessionInterface() + + .. versionadded:: 0.8 + """ + + #: :meth:`make_null_session` will look here for the class that should + #: be created when a null session is requested. Likewise the + #: :meth:`is_null_session` method will perform a typecheck against + #: this type. + null_session_class = NullSession + + def make_null_session(self, app): + """Creates a null session which acts as a replacement object if the + real session support could not be loaded due to a configuration + error. This mainly aids the user experience because the job of the + null session is to still support lookup without complaining but + modifications are answered with a helpful error message of what + failed. + + This creates an instance of :attr:`null_session_class` by default. + """ + return self.null_session_class() + + def is_null_session(self, obj): + """Checks if a given object is a null session. Null sessions are + not asked to be saved. + + This checks if the object is an instance of :attr:`null_session_class` + by default. + """ + return isinstance(obj, self.null_session_class) + + def get_cookie_domain(self, app): + """Helpful helper method that returns the cookie domain that should + be used for the session cookie if session cookies are used. + """ + if app.config['SESSION_COOKIE_DOMAIN'] is not None: + return app.config['SESSION_COOKIE_DOMAIN'] + if app.config['SERVER_NAME'] is not None: + # chop of the port which is usually not supported by browsers + return '.' + app.config['SERVER_NAME'].rsplit(':', 1)[0] + + def get_cookie_path(self, app): + """Returns the path for which the cookie should be valid. The + default implementation uses the value from the SESSION_COOKIE_PATH`` + config var if it's set, and falls back to ``APPLICATION_ROOT`` or + uses ``/`` if it's `None`. + """ + return app.config['SESSION_COOKIE_PATH'] or \ + app.config['APPLICATION_ROOT'] or '/' + + def get_cookie_httponly(self, app): + """Returns True if the session cookie should be httponly. This + currently just returns the value of the ``SESSION_COOKIE_HTTPONLY`` + config var. + """ + return app.config['SESSION_COOKIE_HTTPONLY'] + + def get_cookie_secure(self, app): + """Returns True if the cookie should be secure. This currently + just returns the value of the ``SESSION_COOKIE_SECURE`` setting. + """ + return app.config['SESSION_COOKIE_SECURE'] + + def get_expiration_time(self, app, session): + """A helper method that returns an expiration date for the session + or `None` if the session is linked to the browser session. The + default implementation returns now + the permanent session + lifetime configured on the application. + """ + if session.permanent: + return datetime.utcnow() + app.permanent_session_lifetime + + def open_session(self, app, request): + """This method has to be implemented and must either return `None` + in case the loading failed because of a configuration error or an + instance of a session object which implements a dictionary like + interface + the methods and attributes on :class:`SessionMixin`. + """ + raise NotImplementedError() + + def save_session(self, app, session, response): + """This is called for actual sessions returned by :meth:`open_session` + at the end of the request. This is still called during a request + context so if you absolutely need access to the request you can do + that. + """ + raise NotImplementedError() + + +class SecureCookieSessionInterface(SessionInterface): + """The cookie session interface that uses the Werkzeug securecookie + as client side session backend. + """ + session_class = SecureCookieSession + + def open_session(self, app, request): + key = app.secret_key + if key is not None: + return self.session_class.load_cookie(request, + app.session_cookie_name, + secret_key=key) + + def save_session(self, app, session, response): + expires = self.get_expiration_time(app, session) + domain = self.get_cookie_domain(app) + path = self.get_cookie_path(app) + httponly = self.get_cookie_httponly(app) + secure = self.get_cookie_secure(app) + if session.modified and not session: + response.delete_cookie(app.session_cookie_name, path=path, + domain=domain) + else: + session.save_cookie(response, app.session_cookie_name, path=path, + expires=expires, httponly=httponly, + secure=secure, domain=domain) diff --git a/websdk/flask/signals.py b/websdk/flask/signals.py new file mode 100644 index 0000000..eeb763d --- /dev/null +++ b/websdk/flask/signals.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +""" + flask.signals + ~~~~~~~~~~~~~ + + Implements signals based on blinker if available, otherwise + falls silently back to a noop + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +signals_available = False +try: + from blinker import Namespace + signals_available = True +except ImportError: + class Namespace(object): + def signal(self, name, doc=None): + return _FakeSignal(name, doc) + + class _FakeSignal(object): + """If blinker is unavailable, create a fake class with the same + interface that allows sending of signals but will fail with an + error on anything else. Instead of doing anything on send, it + will just ignore the arguments and do nothing instead. + """ + + def __init__(self, name, doc=None): + self.name = name + self.__doc__ = doc + def _fail(self, *args, **kwargs): + raise RuntimeError('signalling support is unavailable ' + 'because the blinker library is ' + 'not installed.') + send = lambda *a, **kw: None + connect = disconnect = has_receivers_for = receivers_for = \ + temporarily_connected_to = connected_to = _fail + del _fail + +# the namespace for code signals. If you are not flask code, do +# not put signals in here. Create your own namespace instead. +_signals = Namespace() + + +# core signals. For usage examples grep the sourcecode or consult +# the API documentation in docs/api.rst as well as docs/signals.rst +template_rendered = _signals.signal('template-rendered') +request_started = _signals.signal('request-started') +request_finished = _signals.signal('request-finished') +request_tearing_down = _signals.signal('request-tearing-down') +got_request_exception = _signals.signal('got-request-exception') diff --git a/websdk/flask/templating.py b/websdk/flask/templating.py new file mode 100644 index 0000000..90e8772 --- /dev/null +++ b/websdk/flask/templating.py @@ -0,0 +1,138 @@ +# -*- coding: utf-8 -*- +""" + flask.templating + ~~~~~~~~~~~~~~~~ + + Implements the bridge to Jinja2. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +import posixpath +from jinja2 import BaseLoader, Environment as BaseEnvironment, \ + TemplateNotFound + +from .globals import _request_ctx_stack +from .signals import template_rendered +from .module import blueprint_is_module + + +def _default_template_ctx_processor(): + """Default template context processor. Injects `request`, + `session` and `g`. + """ + reqctx = _request_ctx_stack.top + return dict( + config=reqctx.app.config, + request=reqctx.request, + session=reqctx.session, + g=reqctx.g + ) + + +class Environment(BaseEnvironment): + """Works like a regular Jinja2 environment but has some additional + knowledge of how Flask's blueprint works so that it can prepend the + name of the blueprint to referenced templates if necessary. + """ + + def __init__(self, app, **options): + if 'loader' not in options: + options['loader'] = app.create_global_jinja_loader() + BaseEnvironment.__init__(self, **options) + self.app = app + + +class DispatchingJinjaLoader(BaseLoader): + """A loader that looks for templates in the application and all + the blueprint folders. + """ + + def __init__(self, app): + self.app = app + + def get_source(self, environment, template): + for loader, local_name in self._iter_loaders(template): + try: + return loader.get_source(environment, local_name) + except TemplateNotFound: + pass + + raise TemplateNotFound(template) + + def _iter_loaders(self, template): + loader = self.app.jinja_loader + if loader is not None: + yield loader, template + + # old style module based loaders in case we are dealing with a + # blueprint that is an old style module + try: + module, local_name = posixpath.normpath(template).split('/', 1) + blueprint = self.app.blueprints[module] + if blueprint_is_module(blueprint): + loader = blueprint.jinja_loader + if loader is not None: + yield loader, local_name + except (ValueError, KeyError): + pass + + for blueprint in self.app.blueprints.itervalues(): + if blueprint_is_module(blueprint): + continue + loader = blueprint.jinja_loader + if loader is not None: + yield loader, template + + def list_templates(self): + result = set() + loader = self.app.jinja_loader + if loader is not None: + result.update(loader.list_templates()) + + for name, blueprint in self.app.blueprints.iteritems(): + loader = blueprint.jinja_loader + if loader is not None: + for template in loader.list_templates(): + prefix = '' + if blueprint_is_module(blueprint): + prefix = name + '/' + result.add(prefix + template) + + return list(result) + + +def _render(template, context, app): + """Renders the template and fires the signal""" + rv = template.render(context) + template_rendered.send(app, template=template, context=context) + return rv + + +def render_template(template_name, **context): + """Renders a template from the template folder with the given + context. + + :param template_name: the name of the template to be rendered + :param context: the variables that should be available in the + context of the template. + """ + ctx = _request_ctx_stack.top + ctx.app.update_template_context(context) + return _render(ctx.app.jinja_env.get_template(template_name), + context, ctx.app) + + +def render_template_string(source, **context): + """Renders a template from the given template source string + with the given context. + + :param template_name: the sourcecode of the template to be + rendered + :param context: the variables that should be available in the + context of the template. + """ + ctx = _request_ctx_stack.top + ctx.app.update_template_context(context) + return _render(ctx.app.jinja_env.from_string(source), + context, ctx.app) diff --git a/websdk/flask/testing.py b/websdk/flask/testing.py new file mode 100644 index 0000000..782b40f --- /dev/null +++ b/websdk/flask/testing.py @@ -0,0 +1,118 @@ +# -*- coding: utf-8 -*- +""" + flask.testing + ~~~~~~~~~~~~~ + + Implements test support helpers. This module is lazily imported + and usually not used in production environments. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import with_statement + +from contextlib import contextmanager +from werkzeug.test import Client, EnvironBuilder +from flask import _request_ctx_stack + + +def make_test_environ_builder(app, path='/', base_url=None, *args, **kwargs): + """Creates a new test builder with some application defaults thrown in.""" + http_host = app.config.get('SERVER_NAME') + app_root = app.config.get('APPLICATION_ROOT') + if base_url is None: + base_url = 'http://%s/' % (http_host or 'localhost') + if app_root: + base_url += app_root.lstrip('/') + return EnvironBuilder(path, base_url, *args, **kwargs) + + +class FlaskClient(Client): + """Works like a regular Werkzeug test client but has some knowledge about + how Flask works to defer the cleanup of the request context stack to the + end of a with body when used in a with statement. For general information + about how to use this class refer to :class:`werkzeug.test.Client`. + + Basic usage is outlined in the :ref:`testing` chapter. + """ + + preserve_context = False + + @contextmanager + def session_transaction(self, *args, **kwargs): + """When used in combination with a with statement this opens a + session transaction. This can be used to modify the session that + the test client uses. Once the with block is left the session is + stored back. + + with client.session_transaction() as session: + session['value'] = 42 + + Internally this is implemented by going through a temporary test + request context and since session handling could depend on + request variables this function accepts the same arguments as + :meth:`~flask.Flask.test_request_context` which are directly + passed through. + """ + if self.cookie_jar is None: + raise RuntimeError('Session transactions only make sense ' + 'with cookies enabled.') + app = self.application + environ_overrides = kwargs.setdefault('environ_overrides', {}) + self.cookie_jar.inject_wsgi(environ_overrides) + outer_reqctx = _request_ctx_stack.top + with app.test_request_context(*args, **kwargs) as c: + sess = app.open_session(c.request) + if sess is None: + raise RuntimeError('Session backend did not open a session. ' + 'Check the configuration') + + # Since we have to open a new request context for the session + # handling we want to make sure that we hide out own context + # from the caller. By pushing the original request context + # (or None) on top of this and popping it we get exactly that + # behavior. It's important to not use the push and pop + # methods of the actual request context object since that would + # mean that cleanup handlers are called + _request_ctx_stack.push(outer_reqctx) + try: + yield sess + finally: + _request_ctx_stack.pop() + + resp = app.response_class() + if not app.session_interface.is_null_session(sess): + app.save_session(sess, resp) + headers = resp.get_wsgi_headers(c.request.environ) + self.cookie_jar.extract_wsgi(c.request.environ, headers) + + def open(self, *args, **kwargs): + kwargs.setdefault('environ_overrides', {}) \ + ['flask._preserve_context'] = self.preserve_context + + as_tuple = kwargs.pop('as_tuple', False) + buffered = kwargs.pop('buffered', False) + follow_redirects = kwargs.pop('follow_redirects', False) + builder = make_test_environ_builder(self.application, *args, **kwargs) + + return Client.open(self, builder, + as_tuple=as_tuple, + buffered=buffered, + follow_redirects=follow_redirects) + + def __enter__(self): + if self.preserve_context: + raise RuntimeError('Cannot nest client invocations') + self.preserve_context = True + return self + + def __exit__(self, exc_type, exc_value, tb): + self.preserve_context = False + + # on exit we want to clean up earlier. Normally the request context + # stays preserved until the next request in the same thread comes + # in. See RequestGlobals.push() for the general behavior. + top = _request_ctx_stack.top + if top is not None and top.preserved: + top.pop() diff --git a/websdk/flask/testsuite/__init__.py b/websdk/flask/testsuite/__init__.py new file mode 100644 index 0000000..76a4d72 --- /dev/null +++ b/websdk/flask/testsuite/__init__.py @@ -0,0 +1,221 @@ +# -*- coding: utf-8 -*- +""" + flask.testsuite + ~~~~~~~~~~~~~~~ + + Tests Flask itself. The majority of Flask is already tested + as part of Werkzeug. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import with_statement + +import os +import sys +import flask +import warnings +import unittest +from StringIO import StringIO +from functools import update_wrapper +from contextlib import contextmanager +from werkzeug.utils import import_string, find_modules + + +def add_to_path(path): + """Adds an entry to sys.path if it's not already there. This does + not append it but moves it to the front so that we can be sure it + is loaded. + """ + if not os.path.isdir(path): + raise RuntimeError('Tried to add nonexisting path') + + def _samefile(x, y): + try: + return os.path.samefile(x, y) + except (IOError, OSError): + return False + sys.path[:] = [x for x in sys.path if not _samefile(path, x)] + sys.path.insert(0, path) + + +def iter_suites(): + """Yields all testsuites.""" + for module in find_modules(__name__): + mod = import_string(module) + if hasattr(mod, 'suite'): + yield mod.suite() + + +def find_all_tests(suite): + """Yields all the tests and their names from a given suite.""" + suites = [suite] + while suites: + s = suites.pop() + try: + suites.extend(s) + except TypeError: + yield s, '%s.%s.%s' % ( + s.__class__.__module__, + s.__class__.__name__, + s._testMethodName + ) + + +@contextmanager +def catch_warnings(): + """Catch warnings in a with block in a list""" + # make sure deprecation warnings are active in tests + warnings.simplefilter('default', category=DeprecationWarning) + + filters = warnings.filters + warnings.filters = filters[:] + old_showwarning = warnings.showwarning + log = [] + def showwarning(message, category, filename, lineno, file=None, line=None): + log.append(locals()) + try: + warnings.showwarning = showwarning + yield log + finally: + warnings.filters = filters + warnings.showwarning = old_showwarning + + +@contextmanager +def catch_stderr(): + """Catch stderr in a StringIO""" + old_stderr = sys.stderr + sys.stderr = rv = StringIO() + try: + yield rv + finally: + sys.stderr = old_stderr + + +def emits_module_deprecation_warning(f): + def new_f(self, *args, **kwargs): + with catch_warnings() as log: + f(self, *args, **kwargs) + self.assert_(log, 'expected deprecation warning') + for entry in log: + self.assert_('Modules are deprecated' in str(entry['message'])) + return update_wrapper(new_f, f) + + +class FlaskTestCase(unittest.TestCase): + """Baseclass for all the tests that Flask uses. Use these methods + for testing instead of the camelcased ones in the baseclass for + consistency. + """ + + def ensure_clean_request_context(self): + # make sure we're not leaking a request context since we are + # testing flask internally in debug mode in a few cases + self.assert_equal(flask._request_ctx_stack.top, None) + + def setup(self): + pass + + def teardown(self): + pass + + def setUp(self): + self.setup() + + def tearDown(self): + unittest.TestCase.tearDown(self) + self.ensure_clean_request_context() + self.teardown() + + def assert_equal(self, x, y): + return self.assertEqual(x, y) + + def assert_raises(self, exc_type, callable=None, *args, **kwargs): + catcher = _ExceptionCatcher(self, exc_type) + if callable is None: + return catcher + with catcher: + callable(*args, **kwargs) + + +class _ExceptionCatcher(object): + + def __init__(self, test_case, exc_type): + self.test_case = test_case + self.exc_type = exc_type + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, tb): + exception_name = self.exc_type.__name__ + if exc_type is None: + self.test_case.fail('Expected exception of type %r' % + exception_name) + elif not issubclass(exc_type, self.exc_type): + raise exc_type, exc_value, tb + return True + + +class BetterLoader(unittest.TestLoader): + """A nicer loader that solves two problems. First of all we are setting + up tests from different sources and we're doing this programmatically + which breaks the default loading logic so this is required anyways. + Secondly this loader has a nicer interpolation for test names than the + default one so you can just do ``run-tests.py ViewTestCase`` and it + will work. + """ + + def getRootSuite(self): + return suite() + + def loadTestsFromName(self, name, module=None): + root = self.getRootSuite() + if name == 'suite': + return root + + all_tests = [] + for testcase, testname in find_all_tests(root): + if testname == name or \ + testname.endswith('.' + name) or \ + ('.' + name + '.') in testname or \ + testname.startswith(name + '.'): + all_tests.append(testcase) + + if not all_tests: + raise LookupError('could not find test case for "%s"' % name) + + if len(all_tests) == 1: + return all_tests[0] + rv = unittest.TestSuite() + for test in all_tests: + rv.addTest(test) + return rv + + +def setup_path(): + add_to_path(os.path.abspath(os.path.join( + os.path.dirname(__file__), 'test_apps'))) + + +def suite(): + """A testsuite that has all the Flask tests. You can use this + function to integrate the Flask tests into your own testsuite + in case you want to test that monkeypatches to Flask do not + break it. + """ + setup_path() + suite = unittest.TestSuite() + for other_suite in iter_suites(): + suite.addTest(other_suite) + return suite + + +def main(): + """Runs the testsuite as command line application.""" + try: + unittest.main(testLoader=BetterLoader(), defaultTest='suite') + except Exception, e: + print 'Error: %s' % e diff --git a/websdk/flask/testsuite/basic.py b/websdk/flask/testsuite/basic.py new file mode 100644 index 0000000..1733f0a --- /dev/null +++ b/websdk/flask/testsuite/basic.py @@ -0,0 +1,1051 @@ +# -*- coding: utf-8 -*- +""" + flask.testsuite.basic + ~~~~~~~~~~~~~~~~~~~~~ + + The basic functionality. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import with_statement + +import re +import flask +import unittest +from datetime import datetime +from threading import Thread +from flask.testsuite import FlaskTestCase, emits_module_deprecation_warning +from werkzeug.exceptions import BadRequest, NotFound +from werkzeug.http import parse_date + + +class BasicFunctionalityTestCase(FlaskTestCase): + + def test_options_work(self): + app = flask.Flask(__name__) + @app.route('/', methods=['GET', 'POST']) + def index(): + return 'Hello World' + rv = app.test_client().open('/', method='OPTIONS') + self.assert_equal(sorted(rv.allow), ['GET', 'HEAD', 'OPTIONS', 'POST']) + self.assert_equal(rv.data, '') + + def test_options_on_multiple_rules(self): + app = flask.Flask(__name__) + @app.route('/', methods=['GET', 'POST']) + def index(): + return 'Hello World' + @app.route('/', methods=['PUT']) + def index_put(): + return 'Aha!' + rv = app.test_client().open('/', method='OPTIONS') + self.assert_equal(sorted(rv.allow), ['GET', 'HEAD', 'OPTIONS', 'POST', 'PUT']) + + def test_options_handling_disabled(self): + app = flask.Flask(__name__) + def index(): + return 'Hello World!' + index.provide_automatic_options = False + app.route('/')(index) + rv = app.test_client().open('/', method='OPTIONS') + self.assert_equal(rv.status_code, 405) + + app = flask.Flask(__name__) + def index2(): + return 'Hello World!' + index2.provide_automatic_options = True + app.route('/', methods=['OPTIONS'])(index2) + rv = app.test_client().open('/', method='OPTIONS') + self.assert_equal(sorted(rv.allow), ['OPTIONS']) + + def test_request_dispatching(self): + app = flask.Flask(__name__) + @app.route('/') + def index(): + return flask.request.method + @app.route('/more', methods=['GET', 'POST']) + def more(): + return flask.request.method + + c = app.test_client() + self.assert_equal(c.get('/').data, 'GET') + rv = c.post('/') + self.assert_equal(rv.status_code, 405) + self.assert_equal(sorted(rv.allow), ['GET', 'HEAD', 'OPTIONS']) + rv = c.head('/') + self.assert_equal(rv.status_code, 200) + self.assert_(not rv.data) # head truncates + self.assert_equal(c.post('/more').data, 'POST') + self.assert_equal(c.get('/more').data, 'GET') + rv = c.delete('/more') + self.assert_equal(rv.status_code, 405) + self.assert_equal(sorted(rv.allow), ['GET', 'HEAD', 'OPTIONS', 'POST']) + + def test_url_mapping(self): + app = flask.Flask(__name__) + def index(): + return flask.request.method + def more(): + return flask.request.method + + app.add_url_rule('/', 'index', index) + app.add_url_rule('/more', 'more', more, methods=['GET', 'POST']) + + c = app.test_client() + self.assert_equal(c.get('/').data, 'GET') + rv = c.post('/') + self.assert_equal(rv.status_code, 405) + self.assert_equal(sorted(rv.allow), ['GET', 'HEAD', 'OPTIONS']) + rv = c.head('/') + self.assert_equal(rv.status_code, 200) + self.assert_(not rv.data) # head truncates + self.assert_equal(c.post('/more').data, 'POST') + self.assert_equal(c.get('/more').data, 'GET') + rv = c.delete('/more') + self.assert_equal(rv.status_code, 405) + self.assert_equal(sorted(rv.allow), ['GET', 'HEAD', 'OPTIONS', 'POST']) + + def test_werkzeug_routing(self): + from werkzeug.routing import Submount, Rule + app = flask.Flask(__name__) + app.url_map.add(Submount('/foo', [ + Rule('/bar', endpoint='bar'), + Rule('/', endpoint='index') + ])) + def bar(): + return 'bar' + def index(): + return 'index' + app.view_functions['bar'] = bar + app.view_functions['index'] = index + + c = app.test_client() + self.assert_equal(c.get('/foo/').data, 'index') + self.assert_equal(c.get('/foo/bar').data, 'bar') + + def test_endpoint_decorator(self): + from werkzeug.routing import Submount, Rule + app = flask.Flask(__name__) + app.url_map.add(Submount('/foo', [ + Rule('/bar', endpoint='bar'), + Rule('/', endpoint='index') + ])) + + @app.endpoint('bar') + def bar(): + return 'bar' + + @app.endpoint('index') + def index(): + return 'index' + + c = app.test_client() + self.assert_equal(c.get('/foo/').data, 'index') + self.assert_equal(c.get('/foo/bar').data, 'bar') + + def test_session(self): + app = flask.Flask(__name__) + app.secret_key = 'testkey' + @app.route('/set', methods=['POST']) + def set(): + flask.session['value'] = flask.request.form['value'] + return 'value set' + @app.route('/get') + def get(): + return flask.session['value'] + + c = app.test_client() + self.assert_equal(c.post('/set', data={'value': '42'}).data, 'value set') + self.assert_equal(c.get('/get').data, '42') + + def test_session_using_server_name(self): + app = flask.Flask(__name__) + app.config.update( + SECRET_KEY='foo', + SERVER_NAME='example.com' + ) + @app.route('/') + def index(): + flask.session['testing'] = 42 + return 'Hello World' + rv = app.test_client().get('/', 'http://example.com/') + self.assert_('domain=.example.com' in rv.headers['set-cookie'].lower()) + self.assert_('httponly' in rv.headers['set-cookie'].lower()) + + def test_session_using_server_name_and_port(self): + app = flask.Flask(__name__) + app.config.update( + SECRET_KEY='foo', + SERVER_NAME='example.com:8080' + ) + @app.route('/') + def index(): + flask.session['testing'] = 42 + return 'Hello World' + rv = app.test_client().get('/', 'http://example.com:8080/') + self.assert_('domain=.example.com' in rv.headers['set-cookie'].lower()) + self.assert_('httponly' in rv.headers['set-cookie'].lower()) + + def test_session_using_application_root(self): + class PrefixPathMiddleware(object): + def __init__(self, app, prefix): + self.app = app + self.prefix = prefix + def __call__(self, environ, start_response): + environ['SCRIPT_NAME'] = self.prefix + return self.app(environ, start_response) + + app = flask.Flask(__name__) + app.wsgi_app = PrefixPathMiddleware(app.wsgi_app, '/bar') + app.config.update( + SECRET_KEY='foo', + APPLICATION_ROOT='/bar' + ) + @app.route('/') + def index(): + flask.session['testing'] = 42 + return 'Hello World' + rv = app.test_client().get('/', 'http://example.com:8080/') + self.assert_('path=/bar' in rv.headers['set-cookie'].lower()) + + def test_session_using_session_settings(self): + app = flask.Flask(__name__) + app.config.update( + SECRET_KEY='foo', + SERVER_NAME='www.example.com:8080', + APPLICATION_ROOT='/test', + SESSION_COOKIE_DOMAIN='.example.com', + SESSION_COOKIE_HTTPONLY=False, + SESSION_COOKIE_SECURE=True, + SESSION_COOKIE_PATH='/' + ) + @app.route('/') + def index(): + flask.session['testing'] = 42 + return 'Hello World' + rv = app.test_client().get('/', 'http://www.example.com:8080/test/') + cookie = rv.headers['set-cookie'].lower() + self.assert_('domain=.example.com' in cookie) + self.assert_('path=/;' in cookie) + self.assert_('secure' in cookie) + self.assert_('httponly' not in cookie) + + def test_missing_session(self): + app = flask.Flask(__name__) + def expect_exception(f, *args, **kwargs): + try: + f(*args, **kwargs) + except RuntimeError, e: + self.assert_(e.args and 'session is unavailable' in e.args[0]) + else: + self.assert_(False, 'expected exception') + with app.test_request_context(): + self.assert_(flask.session.get('missing_key') is None) + expect_exception(flask.session.__setitem__, 'foo', 42) + expect_exception(flask.session.pop, 'foo') + + def test_session_expiration(self): + permanent = True + app = flask.Flask(__name__) + app.secret_key = 'testkey' + @app.route('/') + def index(): + flask.session['test'] = 42 + flask.session.permanent = permanent + return '' + + @app.route('/test') + def test(): + return unicode(flask.session.permanent) + + client = app.test_client() + rv = client.get('/') + self.assert_('set-cookie' in rv.headers) + match = re.search(r'\bexpires=([^;]+)', rv.headers['set-cookie']) + expires = parse_date(match.group()) + expected = datetime.utcnow() + app.permanent_session_lifetime + self.assert_equal(expires.year, expected.year) + self.assert_equal(expires.month, expected.month) + self.assert_equal(expires.day, expected.day) + + rv = client.get('/test') + self.assert_equal(rv.data, 'True') + + permanent = False + rv = app.test_client().get('/') + self.assert_('set-cookie' in rv.headers) + match = re.search(r'\bexpires=([^;]+)', rv.headers['set-cookie']) + self.assert_(match is None) + + def test_flashes(self): + app = flask.Flask(__name__) + app.secret_key = 'testkey' + + with app.test_request_context(): + self.assert_(not flask.session.modified) + flask.flash('Zap') + flask.session.modified = False + flask.flash('Zip') + self.assert_(flask.session.modified) + self.assert_equal(list(flask.get_flashed_messages()), ['Zap', 'Zip']) + + def test_extended_flashing(self): + app = flask.Flask(__name__) + app.secret_key = 'testkey' + + @app.route('/') + def index(): + flask.flash(u'Hello World') + flask.flash(u'Hello World', 'error') + flask.flash(flask.Markup(u'Testing'), 'warning') + return '' + + @app.route('/test') + def test(): + messages = flask.get_flashed_messages(with_categories=True) + self.assert_equal(len(messages), 3) + self.assert_equal(messages[0], ('message', u'Hello World')) + self.assert_equal(messages[1], ('error', u'Hello World')) + self.assert_equal(messages[2], ('warning', flask.Markup(u'Testing'))) + return '' + messages = flask.get_flashed_messages() + self.assert_equal(len(messages), 3) + self.assert_equal(messages[0], u'Hello World') + self.assert_equal(messages[1], u'Hello World') + self.assert_equal(messages[2], flask.Markup(u'Testing')) + + c = app.test_client() + c.get('/') + c.get('/test') + + def test_request_processing(self): + app = flask.Flask(__name__) + evts = [] + @app.before_request + def before_request(): + evts.append('before') + @app.after_request + def after_request(response): + response.data += '|after' + evts.append('after') + return response + @app.route('/') + def index(): + self.assert_('before' in evts) + self.assert_('after' not in evts) + return 'request' + self.assert_('after' not in evts) + rv = app.test_client().get('/').data + self.assert_('after' in evts) + self.assert_equal(rv, 'request|after') + + def test_teardown_request_handler(self): + called = [] + app = flask.Flask(__name__) + @app.teardown_request + def teardown_request(exc): + called.append(True) + return "Ignored" + @app.route('/') + def root(): + return "Response" + rv = app.test_client().get('/') + self.assert_equal(rv.status_code, 200) + self.assert_('Response' in rv.data) + self.assert_equal(len(called), 1) + + def test_teardown_request_handler_debug_mode(self): + called = [] + app = flask.Flask(__name__) + app.testing = True + @app.teardown_request + def teardown_request(exc): + called.append(True) + return "Ignored" + @app.route('/') + def root(): + return "Response" + rv = app.test_client().get('/') + self.assert_equal(rv.status_code, 200) + self.assert_('Response' in rv.data) + self.assert_equal(len(called), 1) + + def test_teardown_request_handler_error(self): + called = [] + app = flask.Flask(__name__) + @app.teardown_request + def teardown_request1(exc): + self.assert_equal(type(exc), ZeroDivisionError) + called.append(True) + # This raises a new error and blows away sys.exc_info(), so we can + # test that all teardown_requests get passed the same original + # exception. + try: + raise TypeError + except: + pass + @app.teardown_request + def teardown_request2(exc): + self.assert_equal(type(exc), ZeroDivisionError) + called.append(True) + # This raises a new error and blows away sys.exc_info(), so we can + # test that all teardown_requests get passed the same original + # exception. + try: + raise TypeError + except: + pass + @app.route('/') + def fails(): + 1/0 + rv = app.test_client().get('/') + self.assert_equal(rv.status_code, 500) + self.assert_('Internal Server Error' in rv.data) + self.assert_equal(len(called), 2) + + def test_before_after_request_order(self): + called = [] + app = flask.Flask(__name__) + @app.before_request + def before1(): + called.append(1) + @app.before_request + def before2(): + called.append(2) + @app.after_request + def after1(response): + called.append(4) + return response + @app.after_request + def after2(response): + called.append(3) + return response + @app.teardown_request + def finish1(exc): + called.append(6) + @app.teardown_request + def finish2(exc): + called.append(5) + @app.route('/') + def index(): + return '42' + rv = app.test_client().get('/') + self.assert_equal(rv.data, '42') + self.assert_equal(called, [1, 2, 3, 4, 5, 6]) + + def test_error_handling(self): + app = flask.Flask(__name__) + @app.errorhandler(404) + def not_found(e): + return 'not found', 404 + @app.errorhandler(500) + def internal_server_error(e): + return 'internal server error', 500 + @app.route('/') + def index(): + flask.abort(404) + @app.route('/error') + def error(): + 1 // 0 + c = app.test_client() + rv = c.get('/') + self.assert_equal(rv.status_code, 404) + self.assert_equal(rv.data, 'not found') + rv = c.get('/error') + self.assert_equal(rv.status_code, 500) + self.assert_equal('internal server error', rv.data) + + def test_before_request_and_routing_errors(self): + app = flask.Flask(__name__) + @app.before_request + def attach_something(): + flask.g.something = 'value' + @app.errorhandler(404) + def return_something(error): + return flask.g.something, 404 + rv = app.test_client().get('/') + self.assert_equal(rv.status_code, 404) + self.assert_equal(rv.data, 'value') + + def test_user_error_handling(self): + class MyException(Exception): + pass + + app = flask.Flask(__name__) + @app.errorhandler(MyException) + def handle_my_exception(e): + self.assert_(isinstance(e, MyException)) + return '42' + @app.route('/') + def index(): + raise MyException() + + c = app.test_client() + self.assert_equal(c.get('/').data, '42') + + def test_trapping_of_bad_request_key_errors(self): + app = flask.Flask(__name__) + app.testing = True + @app.route('/fail') + def fail(): + flask.request.form['missing_key'] + c = app.test_client() + self.assert_equal(c.get('/fail').status_code, 400) + + app.config['TRAP_BAD_REQUEST_ERRORS'] = True + c = app.test_client() + try: + c.get('/fail') + except KeyError, e: + self.assert_(isinstance(e, BadRequest)) + else: + self.fail('Expected exception') + + def test_trapping_of_all_http_exceptions(self): + app = flask.Flask(__name__) + app.testing = True + app.config['TRAP_HTTP_EXCEPTIONS'] = True + @app.route('/fail') + def fail(): + flask.abort(404) + + c = app.test_client() + try: + c.get('/fail') + except NotFound, e: + pass + else: + self.fail('Expected exception') + + def test_enctype_debug_helper(self): + from flask.debughelpers import DebugFilesKeyError + app = flask.Flask(__name__) + app.debug = True + @app.route('/fail', methods=['POST']) + def index(): + return flask.request.files['foo'].filename + + # with statement is important because we leave an exception on the + # stack otherwise and we want to ensure that this is not the case + # to not negatively affect other tests. + with app.test_client() as c: + try: + c.post('/fail', data={'foo': 'index.txt'}) + except DebugFilesKeyError, e: + self.assert_('no file contents were transmitted' in str(e)) + self.assert_('This was submitted: "index.txt"' in str(e)) + else: + self.fail('Expected exception') + + def test_teardown_on_pop(self): + buffer = [] + app = flask.Flask(__name__) + @app.teardown_request + def end_of_request(exception): + buffer.append(exception) + + ctx = app.test_request_context() + ctx.push() + self.assert_equal(buffer, []) + ctx.pop() + self.assert_equal(buffer, [None]) + + def test_response_creation(self): + app = flask.Flask(__name__) + @app.route('/unicode') + def from_unicode(): + return u'Hällo Wörld' + @app.route('/string') + def from_string(): + return u'Hällo Wörld'.encode('utf-8') + @app.route('/args') + def from_tuple(): + return 'Meh', 400, {'X-Foo': 'Testing'}, 'text/plain' + c = app.test_client() + self.assert_equal(c.get('/unicode').data, u'Hällo Wörld'.encode('utf-8')) + self.assert_equal(c.get('/string').data, u'Hällo Wörld'.encode('utf-8')) + rv = c.get('/args') + self.assert_equal(rv.data, 'Meh') + self.assert_equal(rv.headers['X-Foo'], 'Testing') + self.assert_equal(rv.status_code, 400) + self.assert_equal(rv.mimetype, 'text/plain') + + def test_make_response(self): + app = flask.Flask(__name__) + with app.test_request_context(): + rv = flask.make_response() + self.assert_equal(rv.status_code, 200) + self.assert_equal(rv.data, '') + self.assert_equal(rv.mimetype, 'text/html') + + rv = flask.make_response('Awesome') + self.assert_equal(rv.status_code, 200) + self.assert_equal(rv.data, 'Awesome') + self.assert_equal(rv.mimetype, 'text/html') + + rv = flask.make_response('W00t', 404) + self.assert_equal(rv.status_code, 404) + self.assert_equal(rv.data, 'W00t') + self.assert_equal(rv.mimetype, 'text/html') + + def test_url_generation(self): + app = flask.Flask(__name__) + @app.route('/hello/', methods=['POST']) + def hello(): + pass + with app.test_request_context(): + self.assert_equal(flask.url_for('hello', name='test x'), '/hello/test%20x') + self.assert_equal(flask.url_for('hello', name='test x', _external=True), + 'http://localhost/hello/test%20x') + + def test_custom_converters(self): + from werkzeug.routing import BaseConverter + class ListConverter(BaseConverter): + def to_python(self, value): + return value.split(',') + def to_url(self, value): + base_to_url = super(ListConverter, self).to_url + return ','.join(base_to_url(x) for x in value) + app = flask.Flask(__name__) + app.url_map.converters['list'] = ListConverter + @app.route('/') + def index(args): + return '|'.join(args) + c = app.test_client() + self.assert_equal(c.get('/1,2,3').data, '1|2|3') + + def test_static_files(self): + app = flask.Flask(__name__) + rv = app.test_client().get('/static/index.html') + self.assert_equal(rv.status_code, 200) + self.assert_equal(rv.data.strip(), '

Hello World!

') + with app.test_request_context(): + self.assert_equal(flask.url_for('static', filename='index.html'), + '/static/index.html') + + def test_none_response(self): + app = flask.Flask(__name__) + @app.route('/') + def test(): + return None + try: + app.test_client().get('/') + except ValueError, e: + self.assert_equal(str(e), 'View function did not return a response') + pass + else: + self.assert_("Expected ValueError") + + def test_request_locals(self): + self.assert_equal(repr(flask.g), '') + self.assertFalse(flask.g) + + def test_proper_test_request_context(self): + app = flask.Flask(__name__) + app.config.update( + SERVER_NAME='localhost.localdomain:5000' + ) + + @app.route('/') + def index(): + return None + + @app.route('/', subdomain='foo') + def sub(): + return None + + with app.test_request_context('/'): + self.assert_equal(flask.url_for('index', _external=True), 'http://localhost.localdomain:5000/') + + with app.test_request_context('/'): + self.assert_equal(flask.url_for('sub', _external=True), 'http://foo.localhost.localdomain:5000/') + + try: + with app.test_request_context('/', environ_overrides={'HTTP_HOST': 'localhost'}): + pass + except Exception, e: + self.assert_(isinstance(e, ValueError)) + self.assert_equal(str(e), "the server name provided " + + "('localhost.localdomain:5000') does not match the " + \ + "server name from the WSGI environment ('localhost')") + + try: + app.config.update(SERVER_NAME='localhost') + with app.test_request_context('/', environ_overrides={'SERVER_NAME': 'localhost'}): + pass + except ValueError, e: + raise ValueError( + "No ValueError exception should have been raised \"%s\"" % e + ) + + try: + app.config.update(SERVER_NAME='localhost:80') + with app.test_request_context('/', environ_overrides={'SERVER_NAME': 'localhost:80'}): + pass + except ValueError, e: + raise ValueError( + "No ValueError exception should have been raised \"%s\"" % e + ) + + def test_test_app_proper_environ(self): + app = flask.Flask(__name__) + app.config.update( + SERVER_NAME='localhost.localdomain:5000' + ) + @app.route('/') + def index(): + return 'Foo' + + @app.route('/', subdomain='foo') + def subdomain(): + return 'Foo SubDomain' + + rv = app.test_client().get('/') + self.assert_equal(rv.data, 'Foo') + + rv = app.test_client().get('/', 'http://localhost.localdomain:5000') + self.assert_equal(rv.data, 'Foo') + + rv = app.test_client().get('/', 'https://localhost.localdomain:5000') + self.assert_equal(rv.data, 'Foo') + + app.config.update(SERVER_NAME='localhost.localdomain') + rv = app.test_client().get('/', 'https://localhost.localdomain') + self.assert_equal(rv.data, 'Foo') + + try: + app.config.update(SERVER_NAME='localhost.localdomain:443') + rv = app.test_client().get('/', 'https://localhost.localdomain') + # Werkzeug 0.8 + self.assert_equal(rv.status_code, 404) + except ValueError, e: + # Werkzeug 0.7 + self.assert_equal(str(e), "the server name provided " + + "('localhost.localdomain:443') does not match the " + \ + "server name from the WSGI environment ('localhost.localdomain')") + + try: + app.config.update(SERVER_NAME='localhost.localdomain') + rv = app.test_client().get('/', 'http://foo.localhost') + # Werkzeug 0.8 + self.assert_equal(rv.status_code, 404) + except ValueError, e: + # Werkzeug 0.7 + self.assert_equal(str(e), "the server name provided " + \ + "('localhost.localdomain') does not match the " + \ + "server name from the WSGI environment ('foo.localhost')") + + rv = app.test_client().get('/', 'http://foo.localhost.localdomain') + self.assert_equal(rv.data, 'Foo SubDomain') + + def test_exception_propagation(self): + def apprunner(configkey): + app = flask.Flask(__name__) + @app.route('/') + def index(): + 1/0 + c = app.test_client() + if config_key is not None: + app.config[config_key] = True + try: + resp = c.get('/') + except Exception: + pass + else: + self.fail('expected exception') + else: + self.assert_equal(c.get('/').status_code, 500) + + # we have to run this test in an isolated thread because if the + # debug flag is set to true and an exception happens the context is + # not torn down. This causes other tests that run after this fail + # when they expect no exception on the stack. + for config_key in 'TESTING', 'PROPAGATE_EXCEPTIONS', 'DEBUG', None: + t = Thread(target=apprunner, args=(config_key,)) + t.start() + t.join() + + def test_max_content_length(self): + app = flask.Flask(__name__) + app.config['MAX_CONTENT_LENGTH'] = 64 + @app.before_request + def always_first(): + flask.request.form['myfile'] + self.assert_(False) + @app.route('/accept', methods=['POST']) + def accept_file(): + flask.request.form['myfile'] + self.assert_(False) + @app.errorhandler(413) + def catcher(error): + return '42' + + c = app.test_client() + rv = c.post('/accept', data={'myfile': 'foo' * 100}) + self.assert_equal(rv.data, '42') + + def test_url_processors(self): + app = flask.Flask(__name__) + + @app.url_defaults + def add_language_code(endpoint, values): + if flask.g.lang_code is not None and \ + app.url_map.is_endpoint_expecting(endpoint, 'lang_code'): + values.setdefault('lang_code', flask.g.lang_code) + + @app.url_value_preprocessor + def pull_lang_code(endpoint, values): + flask.g.lang_code = values.pop('lang_code', None) + + @app.route('//') + def index(): + return flask.url_for('about') + + @app.route('//about') + def about(): + return flask.url_for('something_else') + + @app.route('/foo') + def something_else(): + return flask.url_for('about', lang_code='en') + + c = app.test_client() + + self.assert_equal(c.get('/de/').data, '/de/about') + self.assert_equal(c.get('/de/about').data, '/foo') + self.assert_equal(c.get('/foo').data, '/en/about') + + def test_debug_mode_complains_after_first_request(self): + app = flask.Flask(__name__) + app.debug = True + @app.route('/') + def index(): + return 'Awesome' + self.assert_(not app.got_first_request) + self.assert_equal(app.test_client().get('/').data, 'Awesome') + try: + @app.route('/foo') + def broken(): + return 'Meh' + except AssertionError, e: + self.assert_('A setup function was called' in str(e)) + else: + self.fail('Expected exception') + + app.debug = False + @app.route('/foo') + def working(): + return 'Meh' + self.assert_equal(app.test_client().get('/foo').data, 'Meh') + self.assert_(app.got_first_request) + + def test_before_first_request_functions(self): + got = [] + app = flask.Flask(__name__) + @app.before_first_request + def foo(): + got.append(42) + c = app.test_client() + c.get('/') + self.assert_equal(got, [42]) + c.get('/') + self.assert_equal(got, [42]) + self.assert_(app.got_first_request) + + def test_routing_redirect_debugging(self): + app = flask.Flask(__name__) + app.debug = True + @app.route('/foo/', methods=['GET', 'POST']) + def foo(): + return 'success' + with app.test_client() as c: + try: + c.post('/foo', data={}) + except AssertionError, e: + self.assert_('http://localhost/foo/' in str(e)) + self.assert_('Make sure to directly send your POST-request ' + 'to this URL' in str(e)) + else: + self.fail('Expected exception') + + rv = c.get('/foo', data={}, follow_redirects=True) + self.assert_equal(rv.data, 'success') + + app.debug = False + with app.test_client() as c: + rv = c.post('/foo', data={}, follow_redirects=True) + self.assert_equal(rv.data, 'success') + + def test_route_decorator_custom_endpoint(self): + app = flask.Flask(__name__) + app.debug = True + + @app.route('/foo/') + def foo(): + return flask.request.endpoint + + @app.route('/bar/', endpoint='bar') + def for_bar(): + return flask.request.endpoint + + @app.route('/bar/123', endpoint='123') + def for_bar_foo(): + return flask.request.endpoint + + with app.test_request_context(): + assert flask.url_for('foo') == '/foo/' + assert flask.url_for('bar') == '/bar/' + assert flask.url_for('123') == '/bar/123' + + c = app.test_client() + self.assertEqual(c.get('/foo/').data, 'foo') + self.assertEqual(c.get('/bar/').data, 'bar') + self.assertEqual(c.get('/bar/123').data, '123') + + def test_preserve_only_once(self): + app = flask.Flask(__name__) + app.debug = True + + @app.route('/fail') + def fail_func(): + 1/0 + + c = app.test_client() + for x in xrange(3): + with self.assert_raises(ZeroDivisionError): + c.get('/fail') + + self.assert_(flask._request_ctx_stack.top is not None) + flask._request_ctx_stack.pop() + self.assert_(flask._request_ctx_stack.top is None) + + +class ContextTestCase(FlaskTestCase): + + def test_context_binding(self): + app = flask.Flask(__name__) + @app.route('/') + def index(): + return 'Hello %s!' % flask.request.args['name'] + @app.route('/meh') + def meh(): + return flask.request.url + + with app.test_request_context('/?name=World'): + self.assert_equal(index(), 'Hello World!') + with app.test_request_context('/meh'): + self.assert_equal(meh(), 'http://localhost/meh') + self.assert_(flask._request_ctx_stack.top is None) + + def test_context_test(self): + app = flask.Flask(__name__) + self.assert_(not flask.request) + self.assert_(not flask.has_request_context()) + ctx = app.test_request_context() + ctx.push() + try: + self.assert_(flask.request) + self.assert_(flask.has_request_context()) + finally: + ctx.pop() + + def test_manual_context_binding(self): + app = flask.Flask(__name__) + @app.route('/') + def index(): + return 'Hello %s!' % flask.request.args['name'] + + ctx = app.test_request_context('/?name=World') + ctx.push() + self.assert_equal(index(), 'Hello World!') + ctx.pop() + try: + index() + except RuntimeError: + pass + else: + self.assert_(0, 'expected runtime error') + + +class SubdomainTestCase(FlaskTestCase): + + def test_basic_support(self): + app = flask.Flask(__name__) + app.config['SERVER_NAME'] = 'localhost' + @app.route('/') + def normal_index(): + return 'normal index' + @app.route('/', subdomain='test') + def test_index(): + return 'test index' + + c = app.test_client() + rv = c.get('/', 'http://localhost/') + self.assert_equal(rv.data, 'normal index') + + rv = c.get('/', 'http://test.localhost/') + self.assert_equal(rv.data, 'test index') + + @emits_module_deprecation_warning + def test_module_static_path_subdomain(self): + app = flask.Flask(__name__) + app.config['SERVER_NAME'] = 'example.com' + from subdomaintestmodule import mod + app.register_module(mod) + c = app.test_client() + rv = c.get('/static/hello.txt', 'http://foo.example.com/') + self.assert_equal(rv.data.strip(), 'Hello Subdomain') + + def test_subdomain_matching(self): + app = flask.Flask(__name__) + app.config['SERVER_NAME'] = 'localhost' + @app.route('/', subdomain='') + def index(user): + return 'index for %s' % user + + c = app.test_client() + rv = c.get('/', 'http://mitsuhiko.localhost/') + self.assert_equal(rv.data, 'index for mitsuhiko') + + def test_subdomain_matching_with_ports(self): + app = flask.Flask(__name__) + app.config['SERVER_NAME'] = 'localhost:3000' + @app.route('/', subdomain='') + def index(user): + return 'index for %s' % user + + c = app.test_client() + rv = c.get('/', 'http://mitsuhiko.localhost:3000/') + self.assert_equal(rv.data, 'index for mitsuhiko') + + @emits_module_deprecation_warning + def test_module_subdomain_support(self): + app = flask.Flask(__name__) + mod = flask.Module(__name__, 'test', subdomain='testing') + app.config['SERVER_NAME'] = 'localhost' + + @mod.route('/test') + def test(): + return 'Test' + + @mod.route('/outside', subdomain='xtesting') + def bar(): + return 'Outside' + + app.register_module(mod) + + c = app.test_client() + rv = c.get('/test', 'http://testing.localhost/') + self.assert_equal(rv.data, 'Test') + rv = c.get('/outside', 'http://xtesting.localhost/') + self.assert_equal(rv.data, 'Outside') + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(BasicFunctionalityTestCase)) + suite.addTest(unittest.makeSuite(ContextTestCase)) + suite.addTest(unittest.makeSuite(SubdomainTestCase)) + return suite diff --git a/websdk/flask/testsuite/blueprints.py b/websdk/flask/testsuite/blueprints.py new file mode 100644 index 0000000..3f65dd4 --- /dev/null +++ b/websdk/flask/testsuite/blueprints.py @@ -0,0 +1,512 @@ +# -*- coding: utf-8 -*- +""" + flask.testsuite.blueprints + ~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Blueprints (and currently modules) + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import with_statement + +import flask +import unittest +import warnings +from flask.testsuite import FlaskTestCase, emits_module_deprecation_warning +from werkzeug.exceptions import NotFound +from jinja2 import TemplateNotFound + + +# import moduleapp here because it uses deprecated features and we don't +# want to see the warnings +warnings.simplefilter('ignore', DeprecationWarning) +from moduleapp import app as moduleapp +warnings.simplefilter('default', DeprecationWarning) + + +class ModuleTestCase(FlaskTestCase): + + @emits_module_deprecation_warning + def test_basic_module(self): + app = flask.Flask(__name__) + admin = flask.Module(__name__, 'admin', url_prefix='/admin') + @admin.route('/') + def admin_index(): + return 'admin index' + @admin.route('/login') + def admin_login(): + return 'admin login' + @admin.route('/logout') + def admin_logout(): + return 'admin logout' + @app.route('/') + def index(): + return 'the index' + app.register_module(admin) + c = app.test_client() + self.assert_equal(c.get('/').data, 'the index') + self.assert_equal(c.get('/admin/').data, 'admin index') + self.assert_equal(c.get('/admin/login').data, 'admin login') + self.assert_equal(c.get('/admin/logout').data, 'admin logout') + + @emits_module_deprecation_warning + def test_default_endpoint_name(self): + app = flask.Flask(__name__) + mod = flask.Module(__name__, 'frontend') + def index(): + return 'Awesome' + mod.add_url_rule('/', view_func=index) + app.register_module(mod) + rv = app.test_client().get('/') + self.assert_equal(rv.data, 'Awesome') + with app.test_request_context(): + self.assert_equal(flask.url_for('frontend.index'), '/') + + @emits_module_deprecation_warning + def test_request_processing(self): + catched = [] + app = flask.Flask(__name__) + admin = flask.Module(__name__, 'admin', url_prefix='/admin') + @admin.before_request + def before_admin_request(): + catched.append('before-admin') + @admin.after_request + def after_admin_request(response): + catched.append('after-admin') + return response + @admin.route('/') + def admin_index(): + return 'the admin' + @app.before_request + def before_request(): + catched.append('before-app') + @app.after_request + def after_request(response): + catched.append('after-app') + return response + @app.route('/') + def index(): + return 'the index' + app.register_module(admin) + c = app.test_client() + + self.assert_equal(c.get('/').data, 'the index') + self.assert_equal(catched, ['before-app', 'after-app']) + del catched[:] + + self.assert_equal(c.get('/admin/').data, 'the admin') + self.assert_equal(catched, ['before-app', 'before-admin', + 'after-admin', 'after-app']) + + @emits_module_deprecation_warning + def test_context_processors(self): + app = flask.Flask(__name__) + admin = flask.Module(__name__, 'admin', url_prefix='/admin') + @app.context_processor + def inject_all_regualr(): + return {'a': 1} + @admin.context_processor + def inject_admin(): + return {'b': 2} + @admin.app_context_processor + def inject_all_module(): + return {'c': 3} + @app.route('/') + def index(): + return flask.render_template_string('{{ a }}{{ b }}{{ c }}') + @admin.route('/') + def admin_index(): + return flask.render_template_string('{{ a }}{{ b }}{{ c }}') + app.register_module(admin) + c = app.test_client() + self.assert_equal(c.get('/').data, '13') + self.assert_equal(c.get('/admin/').data, '123') + + @emits_module_deprecation_warning + def test_late_binding(self): + app = flask.Flask(__name__) + admin = flask.Module(__name__, 'admin') + @admin.route('/') + def index(): + return '42' + app.register_module(admin, url_prefix='/admin') + self.assert_equal(app.test_client().get('/admin/').data, '42') + + @emits_module_deprecation_warning + def test_error_handling(self): + app = flask.Flask(__name__) + admin = flask.Module(__name__, 'admin') + @admin.app_errorhandler(404) + def not_found(e): + return 'not found', 404 + @admin.app_errorhandler(500) + def internal_server_error(e): + return 'internal server error', 500 + @admin.route('/') + def index(): + flask.abort(404) + @admin.route('/error') + def error(): + 1 // 0 + app.register_module(admin) + c = app.test_client() + rv = c.get('/') + self.assert_equal(rv.status_code, 404) + self.assert_equal(rv.data, 'not found') + rv = c.get('/error') + self.assert_equal(rv.status_code, 500) + self.assert_equal('internal server error', rv.data) + + def test_templates_and_static(self): + app = moduleapp + app.testing = True + c = app.test_client() + + rv = c.get('/') + self.assert_equal(rv.data, 'Hello from the Frontend') + rv = c.get('/admin/') + self.assert_equal(rv.data, 'Hello from the Admin') + rv = c.get('/admin/index2') + self.assert_equal(rv.data, 'Hello from the Admin') + rv = c.get('/admin/static/test.txt') + self.assert_equal(rv.data.strip(), 'Admin File') + rv = c.get('/admin/static/css/test.css') + self.assert_equal(rv.data.strip(), '/* nested file */') + + with app.test_request_context(): + self.assert_equal(flask.url_for('admin.static', filename='test.txt'), + '/admin/static/test.txt') + + with app.test_request_context(): + try: + flask.render_template('missing.html') + except TemplateNotFound, e: + self.assert_equal(e.name, 'missing.html') + else: + self.assert_(0, 'expected exception') + + with flask.Flask(__name__).test_request_context(): + self.assert_equal(flask.render_template('nested/nested.txt'), 'I\'m nested') + + def test_safe_access(self): + app = moduleapp + + with app.test_request_context(): + f = app.view_functions['admin.static'] + + try: + f('/etc/passwd') + except NotFound: + pass + else: + self.assert_(0, 'expected exception') + try: + f('../__init__.py') + except NotFound: + pass + else: + self.assert_(0, 'expected exception') + + # testcase for a security issue that may exist on windows systems + import os + import ntpath + old_path = os.path + os.path = ntpath + try: + try: + f('..\\__init__.py') + except NotFound: + pass + else: + self.assert_(0, 'expected exception') + finally: + os.path = old_path + + @emits_module_deprecation_warning + def test_endpoint_decorator(self): + from werkzeug.routing import Submount, Rule + from flask import Module + + app = flask.Flask(__name__) + app.testing = True + app.url_map.add(Submount('/foo', [ + Rule('/bar', endpoint='bar'), + Rule('/', endpoint='index') + ])) + module = Module(__name__, __name__) + + @module.endpoint('bar') + def bar(): + return 'bar' + + @module.endpoint('index') + def index(): + return 'index' + + app.register_module(module) + + c = app.test_client() + self.assert_equal(c.get('/foo/').data, 'index') + self.assert_equal(c.get('/foo/bar').data, 'bar') + + +class BlueprintTestCase(FlaskTestCase): + + def test_blueprint_specific_error_handling(self): + frontend = flask.Blueprint('frontend', __name__) + backend = flask.Blueprint('backend', __name__) + sideend = flask.Blueprint('sideend', __name__) + + @frontend.errorhandler(403) + def frontend_forbidden(e): + return 'frontend says no', 403 + + @frontend.route('/frontend-no') + def frontend_no(): + flask.abort(403) + + @backend.errorhandler(403) + def backend_forbidden(e): + return 'backend says no', 403 + + @backend.route('/backend-no') + def backend_no(): + flask.abort(403) + + @sideend.route('/what-is-a-sideend') + def sideend_no(): + flask.abort(403) + + app = flask.Flask(__name__) + app.register_blueprint(frontend) + app.register_blueprint(backend) + app.register_blueprint(sideend) + + @app.errorhandler(403) + def app_forbidden(e): + return 'application itself says no', 403 + + c = app.test_client() + + self.assert_equal(c.get('/frontend-no').data, 'frontend says no') + self.assert_equal(c.get('/backend-no').data, 'backend says no') + self.assert_equal(c.get('/what-is-a-sideend').data, 'application itself says no') + + def test_blueprint_url_definitions(self): + bp = flask.Blueprint('test', __name__) + + @bp.route('/foo', defaults={'baz': 42}) + def foo(bar, baz): + return '%s/%d' % (bar, baz) + + @bp.route('/bar') + def bar(bar): + return unicode(bar) + + app = flask.Flask(__name__) + app.register_blueprint(bp, url_prefix='/1', url_defaults={'bar': 23}) + app.register_blueprint(bp, url_prefix='/2', url_defaults={'bar': 19}) + + c = app.test_client() + self.assert_equal(c.get('/1/foo').data, u'23/42') + self.assert_equal(c.get('/2/foo').data, u'19/42') + self.assert_equal(c.get('/1/bar').data, u'23') + self.assert_equal(c.get('/2/bar').data, u'19') + + def test_blueprint_url_processors(self): + bp = flask.Blueprint('frontend', __name__, url_prefix='/') + + @bp.url_defaults + def add_language_code(endpoint, values): + values.setdefault('lang_code', flask.g.lang_code) + + @bp.url_value_preprocessor + def pull_lang_code(endpoint, values): + flask.g.lang_code = values.pop('lang_code') + + @bp.route('/') + def index(): + return flask.url_for('.about') + + @bp.route('/about') + def about(): + return flask.url_for('.index') + + app = flask.Flask(__name__) + app.register_blueprint(bp) + + c = app.test_client() + + self.assert_equal(c.get('/de/').data, '/de/about') + self.assert_equal(c.get('/de/about').data, '/de/') + + def test_templates_and_static(self): + from blueprintapp import app + c = app.test_client() + + rv = c.get('/') + self.assert_equal(rv.data, 'Hello from the Frontend') + rv = c.get('/admin/') + self.assert_equal(rv.data, 'Hello from the Admin') + rv = c.get('/admin/index2') + self.assert_equal(rv.data, 'Hello from the Admin') + rv = c.get('/admin/static/test.txt') + self.assert_equal(rv.data.strip(), 'Admin File') + rv = c.get('/admin/static/css/test.css') + self.assert_equal(rv.data.strip(), '/* nested file */') + + with app.test_request_context(): + self.assert_equal(flask.url_for('admin.static', filename='test.txt'), + '/admin/static/test.txt') + + with app.test_request_context(): + try: + flask.render_template('missing.html') + except TemplateNotFound, e: + self.assert_equal(e.name, 'missing.html') + else: + self.assert_(0, 'expected exception') + + with flask.Flask(__name__).test_request_context(): + self.assert_equal(flask.render_template('nested/nested.txt'), 'I\'m nested') + + def test_templates_list(self): + from blueprintapp import app + templates = sorted(app.jinja_env.list_templates()) + self.assert_equal(templates, ['admin/index.html', + 'frontend/index.html']) + + def test_dotted_names(self): + frontend = flask.Blueprint('myapp.frontend', __name__) + backend = flask.Blueprint('myapp.backend', __name__) + + @frontend.route('/fe') + def frontend_index(): + return flask.url_for('myapp.backend.backend_index') + + @frontend.route('/fe2') + def frontend_page2(): + return flask.url_for('.frontend_index') + + @backend.route('/be') + def backend_index(): + return flask.url_for('myapp.frontend.frontend_index') + + app = flask.Flask(__name__) + app.register_blueprint(frontend) + app.register_blueprint(backend) + + c = app.test_client() + self.assert_equal(c.get('/fe').data.strip(), '/be') + self.assert_equal(c.get('/fe2').data.strip(), '/fe') + self.assert_equal(c.get('/be').data.strip(), '/fe') + + def test_empty_url_defaults(self): + bp = flask.Blueprint('bp', __name__) + + @bp.route('/', defaults={'page': 1}) + @bp.route('/page/') + def something(page): + return str(page) + + app = flask.Flask(__name__) + app.register_blueprint(bp) + + c = app.test_client() + self.assert_equal(c.get('/').data, '1') + self.assert_equal(c.get('/page/2').data, '2') + + def test_route_decorator_custom_endpoint(self): + + bp = flask.Blueprint('bp', __name__) + + @bp.route('/foo') + def foo(): + return flask.request.endpoint + + @bp.route('/bar', endpoint='bar') + def foo_bar(): + return flask.request.endpoint + + @bp.route('/bar/123', endpoint='123') + def foo_bar_foo(): + return flask.request.endpoint + + @bp.route('/bar/foo') + def bar_foo(): + return flask.request.endpoint + + app = flask.Flask(__name__) + app.register_blueprint(bp, url_prefix='/py') + + @app.route('/') + def index(): + return flask.request.endpoint + + c = app.test_client() + self.assertEqual(c.get('/').data, 'index') + self.assertEqual(c.get('/py/foo').data, 'bp.foo') + self.assertEqual(c.get('/py/bar').data, 'bp.bar') + self.assertEqual(c.get('/py/bar/123').data, 'bp.123') + self.assertEqual(c.get('/py/bar/foo').data, 'bp.bar_foo') + + def test_route_decorator_custom_endpoint_with_dots(self): + bp = flask.Blueprint('bp', __name__) + + @bp.route('/foo') + def foo(): + return flask.request.endpoint + + try: + @bp.route('/bar', endpoint='bar.bar') + def foo_bar(): + return flask.request.endpoint + except AssertionError: + pass + else: + raise AssertionError('expected AssertionError not raised') + + try: + @bp.route('/bar/123', endpoint='bar.123') + def foo_bar_foo(): + return flask.request.endpoint + except AssertionError: + pass + else: + raise AssertionError('expected AssertionError not raised') + + def foo_foo_foo(): + pass + + self.assertRaises( + AssertionError, + lambda: bp.add_url_rule( + '/bar/123', endpoint='bar.123', view_func=foo_foo_foo + ) + ) + + self.assertRaises( + AssertionError, + bp.route('/bar/123', endpoint='bar.123'), + lambda: None + ) + + app = flask.Flask(__name__) + app.register_blueprint(bp, url_prefix='/py') + + c = app.test_client() + self.assertEqual(c.get('/py/foo').data, 'bp.foo') + # The rule's din't actually made it through + rv = c.get('/py/bar') + assert rv.status_code == 404 + rv = c.get('/py/bar/123') + assert rv.status_code == 404 + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(BlueprintTestCase)) + suite.addTest(unittest.makeSuite(ModuleTestCase)) + return suite diff --git a/websdk/flask/testsuite/config.py b/websdk/flask/testsuite/config.py new file mode 100644 index 0000000..ad1721f --- /dev/null +++ b/websdk/flask/testsuite/config.py @@ -0,0 +1,182 @@ +# -*- coding: utf-8 -*- +""" + flask.testsuite.config + ~~~~~~~~~~~~~~~~~~~~~~ + + Configuration and instances. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +import os +import sys +import flask +import unittest +from flask.testsuite import FlaskTestCase + + +# config keys used for the ConfigTestCase +TEST_KEY = 'foo' +SECRET_KEY = 'devkey' + + +class ConfigTestCase(FlaskTestCase): + + def common_object_test(self, app): + self.assert_equal(app.secret_key, 'devkey') + self.assert_equal(app.config['TEST_KEY'], 'foo') + self.assert_('ConfigTestCase' not in app.config) + + def test_config_from_file(self): + app = flask.Flask(__name__) + app.config.from_pyfile(__file__.rsplit('.', 1)[0] + '.py') + self.common_object_test(app) + + def test_config_from_object(self): + app = flask.Flask(__name__) + app.config.from_object(__name__) + self.common_object_test(app) + + def test_config_from_class(self): + class Base(object): + TEST_KEY = 'foo' + class Test(Base): + SECRET_KEY = 'devkey' + app = flask.Flask(__name__) + app.config.from_object(Test) + self.common_object_test(app) + + def test_config_from_envvar(self): + env = os.environ + try: + os.environ = {} + app = flask.Flask(__name__) + try: + app.config.from_envvar('FOO_SETTINGS') + except RuntimeError, e: + self.assert_("'FOO_SETTINGS' is not set" in str(e)) + else: + self.assert_(0, 'expected exception') + self.assert_(not app.config.from_envvar('FOO_SETTINGS', silent=True)) + + os.environ = {'FOO_SETTINGS': __file__.rsplit('.', 1)[0] + '.py'} + self.assert_(app.config.from_envvar('FOO_SETTINGS')) + self.common_object_test(app) + finally: + os.environ = env + + def test_config_missing(self): + app = flask.Flask(__name__) + try: + app.config.from_pyfile('missing.cfg') + except IOError, e: + msg = str(e) + self.assert_(msg.startswith('[Errno 2] Unable to load configuration ' + 'file (No such file or directory):')) + self.assert_(msg.endswith("missing.cfg'")) + else: + self.assert_(0, 'expected config') + self.assert_(not app.config.from_pyfile('missing.cfg', silent=True)) + + def test_session_lifetime(self): + app = flask.Flask(__name__) + app.config['PERMANENT_SESSION_LIFETIME'] = 42 + self.assert_equal(app.permanent_session_lifetime.seconds, 42) + + +class InstanceTestCase(FlaskTestCase): + + def test_explicit_instance_paths(self): + here = os.path.abspath(os.path.dirname(__file__)) + try: + flask.Flask(__name__, instance_path='instance') + except ValueError, e: + self.assert_('must be absolute' in str(e)) + else: + self.fail('Expected value error') + + app = flask.Flask(__name__, instance_path=here) + self.assert_equal(app.instance_path, here) + + def test_uninstalled_module_paths(self): + from config_module_app import app + here = os.path.abspath(os.path.dirname(__file__)) + self.assert_equal(app.instance_path, os.path.join(here, 'test_apps', 'instance')) + + def test_uninstalled_package_paths(self): + from config_package_app import app + here = os.path.abspath(os.path.dirname(__file__)) + self.assert_equal(app.instance_path, os.path.join(here, 'test_apps', 'instance')) + + def test_installed_module_paths(self): + import types + expected_prefix = os.path.abspath('foo') + mod = types.ModuleType('myapp') + mod.__file__ = os.path.join(expected_prefix, 'lib', 'python2.5', + 'site-packages', 'myapp.py') + sys.modules['myapp'] = mod + try: + mod.app = flask.Flask(mod.__name__) + self.assert_equal(mod.app.instance_path, + os.path.join(expected_prefix, 'var', + 'myapp-instance')) + finally: + sys.modules['myapp'] = None + + def test_installed_package_paths(self): + import types + expected_prefix = os.path.abspath('foo') + package_path = os.path.join(expected_prefix, 'lib', 'python2.5', + 'site-packages', 'myapp') + mod = types.ModuleType('myapp') + mod.__path__ = [package_path] + mod.__file__ = os.path.join(package_path, '__init__.py') + sys.modules['myapp'] = mod + try: + mod.app = flask.Flask(mod.__name__) + self.assert_equal(mod.app.instance_path, + os.path.join(expected_prefix, 'var', + 'myapp-instance')) + finally: + sys.modules['myapp'] = None + + def test_prefix_installed_paths(self): + import types + expected_prefix = os.path.abspath(sys.prefix) + package_path = os.path.join(expected_prefix, 'lib', 'python2.5', + 'site-packages', 'myapp') + mod = types.ModuleType('myapp') + mod.__path__ = [package_path] + mod.__file__ = os.path.join(package_path, '__init__.py') + sys.modules['myapp'] = mod + try: + mod.app = flask.Flask(mod.__name__) + self.assert_equal(mod.app.instance_path, + os.path.join(expected_prefix, 'var', + 'myapp-instance')) + finally: + sys.modules['myapp'] = None + + def test_egg_installed_paths(self): + import types + expected_prefix = os.path.abspath(sys.prefix) + package_path = os.path.join(expected_prefix, 'lib', 'python2.5', + 'site-packages', 'MyApp.egg', 'myapp') + mod = types.ModuleType('myapp') + mod.__path__ = [package_path] + mod.__file__ = os.path.join(package_path, '__init__.py') + sys.modules['myapp'] = mod + try: + mod.app = flask.Flask(mod.__name__) + self.assert_equal(mod.app.instance_path, + os.path.join(expected_prefix, 'var', + 'myapp-instance')) + finally: + sys.modules['myapp'] = None + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(ConfigTestCase)) + suite.addTest(unittest.makeSuite(InstanceTestCase)) + return suite diff --git a/websdk/flask/testsuite/deprecations.py b/websdk/flask/testsuite/deprecations.py new file mode 100644 index 0000000..795a5d3 --- /dev/null +++ b/websdk/flask/testsuite/deprecations.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +""" + flask.testsuite.deprecations + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Tests deprecation support. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import with_statement + +import flask +import unittest +from flask.testsuite import FlaskTestCase, catch_warnings + + +class DeprecationsTestCase(FlaskTestCase): + + def test_init_jinja_globals(self): + class MyFlask(flask.Flask): + def init_jinja_globals(self): + self.jinja_env.globals['foo'] = '42' + + with catch_warnings() as log: + app = MyFlask(__name__) + @app.route('/') + def foo(): + return app.jinja_env.globals['foo'] + + c = app.test_client() + self.assert_equal(c.get('/').data, '42') + self.assert_equal(len(log), 1) + self.assert_('init_jinja_globals' in str(log[0]['message'])) + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(DeprecationsTestCase)) + return suite diff --git a/websdk/flask/testsuite/examples.py b/websdk/flask/testsuite/examples.py new file mode 100644 index 0000000..2d30958 --- /dev/null +++ b/websdk/flask/testsuite/examples.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +""" + flask.testsuite.examples + ~~~~~~~~~~~~~~~~~~~~~~~~ + + Tests the examples. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +import os +import unittest +from flask.testsuite import add_to_path + + +def setup_path(): + example_path = os.path.join(os.path.dirname(__file__), + os.pardir, os.pardir, 'examples') + add_to_path(os.path.join(example_path, 'flaskr')) + add_to_path(os.path.join(example_path, 'minitwit')) + + +def suite(): + setup_path() + suite = unittest.TestSuite() + try: + from minitwit_tests import MiniTwitTestCase + except ImportError: + pass + else: + suite.addTest(unittest.makeSuite(MiniTwitTestCase)) + try: + from flaskr_tests import FlaskrTestCase + except ImportError: + pass + else: + suite.addTest(unittest.makeSuite(FlaskrTestCase)) + return suite diff --git a/websdk/flask/testsuite/ext.py b/websdk/flask/testsuite/ext.py new file mode 100644 index 0000000..034ab5b --- /dev/null +++ b/websdk/flask/testsuite/ext.py @@ -0,0 +1,123 @@ +# -*- coding: utf-8 -*- +""" + flask.testsuite.ext + ~~~~~~~~~~~~~~~~~~~ + + Tests the extension import thing. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +from __future__ import with_statement + +import sys +import unittest +from flask.testsuite import FlaskTestCase + + +class ExtImportHookTestCase(FlaskTestCase): + + def setup(self): + # we clear this out for various reasons. The most important one is + # that a real flaskext could be in there which would disable our + # fake package. Secondly we want to make sure that the flaskext + # import hook does not break on reloading. + for entry, value in sys.modules.items(): + if (entry.startswith('flask.ext.') or + entry.startswith('flask_') or + entry.startswith('flaskext.') or + entry == 'flaskext') and value is not None: + sys.modules.pop(entry, None) + from flask import ext + reload(ext) + + # reloading must not add more hooks + import_hooks = 0 + for item in sys.meta_path: + cls = type(item) + if cls.__module__ == 'flask.exthook' and \ + cls.__name__ == 'ExtensionImporter': + import_hooks += 1 + self.assert_equal(import_hooks, 1) + + def teardown(self): + from flask import ext + for key in ext.__dict__: + self.assert_('.' not in key) + + def test_flaskext_new_simple_import_normal(self): + from flask.ext.newext_simple import ext_id + self.assert_equal(ext_id, 'newext_simple') + + def test_flaskext_new_simple_import_module(self): + from flask.ext import newext_simple + self.assert_equal(newext_simple.ext_id, 'newext_simple') + self.assert_equal(newext_simple.__name__, 'flask_newext_simple') + + def test_flaskext_new_package_import_normal(self): + from flask.ext.newext_package import ext_id + self.assert_equal(ext_id, 'newext_package') + + def test_flaskext_new_package_import_module(self): + from flask.ext import newext_package + self.assert_equal(newext_package.ext_id, 'newext_package') + self.assert_equal(newext_package.__name__, 'flask_newext_package') + + def test_flaskext_new_package_import_submodule_function(self): + from flask.ext.newext_package.submodule import test_function + self.assert_equal(test_function(), 42) + + def test_flaskext_new_package_import_submodule(self): + from flask.ext.newext_package import submodule + self.assert_equal(submodule.__name__, 'flask_newext_package.submodule') + self.assert_equal(submodule.test_function(), 42) + + def test_flaskext_old_simple_import_normal(self): + from flask.ext.oldext_simple import ext_id + self.assert_equal(ext_id, 'oldext_simple') + + def test_flaskext_old_simple_import_module(self): + from flask.ext import oldext_simple + self.assert_equal(oldext_simple.ext_id, 'oldext_simple') + self.assert_equal(oldext_simple.__name__, 'flaskext.oldext_simple') + + def test_flaskext_old_package_import_normal(self): + from flask.ext.oldext_package import ext_id + self.assert_equal(ext_id, 'oldext_package') + + def test_flaskext_old_package_import_module(self): + from flask.ext import oldext_package + self.assert_equal(oldext_package.ext_id, 'oldext_package') + self.assert_equal(oldext_package.__name__, 'flaskext.oldext_package') + + def test_flaskext_old_package_import_submodule(self): + from flask.ext.oldext_package import submodule + self.assert_equal(submodule.__name__, 'flaskext.oldext_package.submodule') + self.assert_equal(submodule.test_function(), 42) + + def test_flaskext_old_package_import_submodule_function(self): + from flask.ext.oldext_package.submodule import test_function + self.assert_equal(test_function(), 42) + + def test_flaskext_broken_package_no_module_caching(self): + for x in xrange(2): + with self.assert_raises(ImportError): + import flask.ext.broken + + def test_no_error_swallowing(self): + try: + import flask.ext.broken + except ImportError: + exc_type, exc_value, tb = sys.exc_info() + self.assert_(exc_type is ImportError) + self.assert_equal(str(exc_value), 'No module named missing_module') + self.assert_(tb.tb_frame.f_globals is globals()) + + next = tb.tb_next + self.assert_('flask_broken/__init__.py' in next.tb_frame.f_code.co_filename) + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(ExtImportHookTestCase)) + return suite diff --git a/websdk/flask/testsuite/helpers.py b/websdk/flask/testsuite/helpers.py new file mode 100644 index 0000000..052d36e --- /dev/null +++ b/websdk/flask/testsuite/helpers.py @@ -0,0 +1,298 @@ +# -*- coding: utf-8 -*- +""" + flask.testsuite.helpers + ~~~~~~~~~~~~~~~~~~~~~~~ + + Various helpers. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import with_statement + +import os +import flask +import unittest +from logging import StreamHandler +from StringIO import StringIO +from flask.testsuite import FlaskTestCase, catch_warnings, catch_stderr +from werkzeug.http import parse_options_header + + +def has_encoding(name): + try: + import codecs + codecs.lookup(name) + return True + except LookupError: + return False + + +class JSONTestCase(FlaskTestCase): + + def test_json_bad_requests(self): + app = flask.Flask(__name__) + @app.route('/json', methods=['POST']) + def return_json(): + return unicode(flask.request.json) + c = app.test_client() + rv = c.post('/json', data='malformed', content_type='application/json') + self.assert_equal(rv.status_code, 400) + + def test_json_body_encoding(self): + app = flask.Flask(__name__) + app.testing = True + @app.route('/') + def index(): + return flask.request.json + + c = app.test_client() + resp = c.get('/', data=u'"Hällo Wörld"'.encode('iso-8859-15'), + content_type='application/json; charset=iso-8859-15') + self.assert_equal(resp.data, u'Hällo Wörld'.encode('utf-8')) + + def test_jsonify(self): + d = dict(a=23, b=42, c=[1, 2, 3]) + app = flask.Flask(__name__) + @app.route('/kw') + def return_kwargs(): + return flask.jsonify(**d) + @app.route('/dict') + def return_dict(): + return flask.jsonify(d) + c = app.test_client() + for url in '/kw', '/dict': + rv = c.get(url) + self.assert_equal(rv.mimetype, 'application/json') + self.assert_equal(flask.json.loads(rv.data), d) + + def test_json_attr(self): + app = flask.Flask(__name__) + @app.route('/add', methods=['POST']) + def add(): + return unicode(flask.request.json['a'] + flask.request.json['b']) + c = app.test_client() + rv = c.post('/add', data=flask.json.dumps({'a': 1, 'b': 2}), + content_type='application/json') + self.assert_equal(rv.data, '3') + + def test_template_escaping(self): + app = flask.Flask(__name__) + render = flask.render_template_string + with app.test_request_context(): + rv = render('{{ ""|tojson|safe }}') + self.assert_equal(rv, '"<\\/script>"') + rv = render('{{ "<\0/script>"|tojson|safe }}') + self.assert_equal(rv, '"<\\u0000\\/script>"') + + def test_modified_url_encoding(self): + class ModifiedRequest(flask.Request): + url_charset = 'euc-kr' + app = flask.Flask(__name__) + app.request_class = ModifiedRequest + app.url_map.charset = 'euc-kr' + + @app.route('/') + def index(): + return flask.request.args['foo'] + + rv = app.test_client().get(u'/?foo=정상처리'.encode('euc-kr')) + self.assert_equal(rv.status_code, 200) + self.assert_equal(rv.data, u'정상처리'.encode('utf-8')) + + if not has_encoding('euc-kr'): + test_modified_url_encoding = None + + +class SendfileTestCase(FlaskTestCase): + + def test_send_file_regular(self): + app = flask.Flask(__name__) + with app.test_request_context(): + rv = flask.send_file('static/index.html') + self.assert_(rv.direct_passthrough) + self.assert_equal(rv.mimetype, 'text/html') + with app.open_resource('static/index.html') as f: + self.assert_equal(rv.data, f.read()) + + def test_send_file_xsendfile(self): + app = flask.Flask(__name__) + app.use_x_sendfile = True + with app.test_request_context(): + rv = flask.send_file('static/index.html') + self.assert_(rv.direct_passthrough) + self.assert_('x-sendfile' in rv.headers) + self.assert_equal(rv.headers['x-sendfile'], + os.path.join(app.root_path, 'static/index.html')) + self.assert_equal(rv.mimetype, 'text/html') + + def test_send_file_object(self): + app = flask.Flask(__name__) + with catch_warnings() as captured: + with app.test_request_context(): + f = open(os.path.join(app.root_path, 'static/index.html')) + rv = flask.send_file(f) + with app.open_resource('static/index.html') as f: + self.assert_equal(rv.data, f.read()) + self.assert_equal(rv.mimetype, 'text/html') + # mimetypes + etag + self.assert_equal(len(captured), 2) + + app.use_x_sendfile = True + with catch_warnings() as captured: + with app.test_request_context(): + f = open(os.path.join(app.root_path, 'static/index.html')) + rv = flask.send_file(f) + self.assert_equal(rv.mimetype, 'text/html') + self.assert_('x-sendfile' in rv.headers) + self.assert_equal(rv.headers['x-sendfile'], + os.path.join(app.root_path, 'static/index.html')) + # mimetypes + etag + self.assert_equal(len(captured), 2) + + app.use_x_sendfile = False + with app.test_request_context(): + with catch_warnings() as captured: + f = StringIO('Test') + rv = flask.send_file(f) + self.assert_equal(rv.data, 'Test') + self.assert_equal(rv.mimetype, 'application/octet-stream') + # etags + self.assert_equal(len(captured), 1) + with catch_warnings() as captured: + f = StringIO('Test') + rv = flask.send_file(f, mimetype='text/plain') + self.assert_equal(rv.data, 'Test') + self.assert_equal(rv.mimetype, 'text/plain') + # etags + self.assert_equal(len(captured), 1) + + app.use_x_sendfile = True + with catch_warnings() as captured: + with app.test_request_context(): + f = StringIO('Test') + rv = flask.send_file(f) + self.assert_('x-sendfile' not in rv.headers) + # etags + self.assert_equal(len(captured), 1) + + def test_attachment(self): + app = flask.Flask(__name__) + with catch_warnings() as captured: + with app.test_request_context(): + f = open(os.path.join(app.root_path, 'static/index.html')) + rv = flask.send_file(f, as_attachment=True) + value, options = parse_options_header(rv.headers['Content-Disposition']) + self.assert_equal(value, 'attachment') + # mimetypes + etag + self.assert_equal(len(captured), 2) + + with app.test_request_context(): + self.assert_equal(options['filename'], 'index.html') + rv = flask.send_file('static/index.html', as_attachment=True) + value, options = parse_options_header(rv.headers['Content-Disposition']) + self.assert_equal(value, 'attachment') + self.assert_equal(options['filename'], 'index.html') + + with app.test_request_context(): + rv = flask.send_file(StringIO('Test'), as_attachment=True, + attachment_filename='index.txt', + add_etags=False) + self.assert_equal(rv.mimetype, 'text/plain') + value, options = parse_options_header(rv.headers['Content-Disposition']) + self.assert_equal(value, 'attachment') + self.assert_equal(options['filename'], 'index.txt') + + +class LoggingTestCase(FlaskTestCase): + + def test_logger_cache(self): + app = flask.Flask(__name__) + logger1 = app.logger + self.assert_(app.logger is logger1) + self.assert_equal(logger1.name, __name__) + app.logger_name = __name__ + '/test_logger_cache' + self.assert_(app.logger is not logger1) + + def test_debug_log(self): + app = flask.Flask(__name__) + app.debug = True + + @app.route('/') + def index(): + app.logger.warning('the standard library is dead') + app.logger.debug('this is a debug statement') + return '' + + @app.route('/exc') + def exc(): + 1/0 + + with app.test_client() as c: + with catch_stderr() as err: + c.get('/') + out = err.getvalue() + self.assert_('WARNING in helpers [' in out) + self.assert_(os.path.basename(__file__.rsplit('.', 1)[0] + '.py') in out) + self.assert_('the standard library is dead' in out) + self.assert_('this is a debug statement' in out) + + with catch_stderr() as err: + try: + c.get('/exc') + except ZeroDivisionError: + pass + else: + self.assert_(False, 'debug log ate the exception') + + def test_exception_logging(self): + out = StringIO() + app = flask.Flask(__name__) + app.logger_name = 'flask_tests/test_exception_logging' + app.logger.addHandler(StreamHandler(out)) + + @app.route('/') + def index(): + 1/0 + + rv = app.test_client().get('/') + self.assert_equal(rv.status_code, 500) + self.assert_('Internal Server Error' in rv.data) + + err = out.getvalue() + self.assert_('Exception on / [GET]' in err) + self.assert_('Traceback (most recent call last):' in err) + self.assert_('1/0' in err) + self.assert_('ZeroDivisionError:' in err) + + def test_processor_exceptions(self): + app = flask.Flask(__name__) + @app.before_request + def before_request(): + if trigger == 'before': + 1/0 + @app.after_request + def after_request(response): + if trigger == 'after': + 1/0 + return response + @app.route('/') + def index(): + return 'Foo' + @app.errorhandler(500) + def internal_server_error(e): + return 'Hello Server Error', 500 + for trigger in 'before', 'after': + rv = app.test_client().get('/') + self.assert_equal(rv.status_code, 500) + self.assert_equal(rv.data, 'Hello Server Error') + + +def suite(): + suite = unittest.TestSuite() + if flask.json_available: + suite.addTest(unittest.makeSuite(JSONTestCase)) + suite.addTest(unittest.makeSuite(SendfileTestCase)) + suite.addTest(unittest.makeSuite(LoggingTestCase)) + return suite diff --git a/websdk/flask/testsuite/signals.py b/websdk/flask/testsuite/signals.py new file mode 100644 index 0000000..da1a68c --- /dev/null +++ b/websdk/flask/testsuite/signals.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- +""" + flask.testsuite.signals + ~~~~~~~~~~~~~~~~~~~~~~~ + + Signalling. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +import flask +import unittest +from flask.testsuite import FlaskTestCase + + +class SignalsTestCase(FlaskTestCase): + + def test_template_rendered(self): + app = flask.Flask(__name__) + + @app.route('/') + def index(): + return flask.render_template('simple_template.html', whiskey=42) + + recorded = [] + def record(sender, template, context): + recorded.append((template, context)) + + flask.template_rendered.connect(record, app) + try: + app.test_client().get('/') + self.assert_equal(len(recorded), 1) + template, context = recorded[0] + self.assert_equal(template.name, 'simple_template.html') + self.assert_equal(context['whiskey'], 42) + finally: + flask.template_rendered.disconnect(record, app) + + def test_request_signals(self): + app = flask.Flask(__name__) + calls = [] + + def before_request_signal(sender): + calls.append('before-signal') + + def after_request_signal(sender, response): + self.assert_equal(response.data, 'stuff') + calls.append('after-signal') + + @app.before_request + def before_request_handler(): + calls.append('before-handler') + + @app.after_request + def after_request_handler(response): + calls.append('after-handler') + response.data = 'stuff' + return response + + @app.route('/') + def index(): + calls.append('handler') + return 'ignored anyway' + + flask.request_started.connect(before_request_signal, app) + flask.request_finished.connect(after_request_signal, app) + + try: + rv = app.test_client().get('/') + self.assert_equal(rv.data, 'stuff') + + self.assert_equal(calls, ['before-signal', 'before-handler', + 'handler', 'after-handler', + 'after-signal']) + finally: + flask.request_started.disconnect(before_request_signal, app) + flask.request_finished.disconnect(after_request_signal, app) + + def test_request_exception_signal(self): + app = flask.Flask(__name__) + recorded = [] + + @app.route('/') + def index(): + 1/0 + + def record(sender, exception): + recorded.append(exception) + + flask.got_request_exception.connect(record, app) + try: + self.assert_equal(app.test_client().get('/').status_code, 500) + self.assert_equal(len(recorded), 1) + self.assert_(isinstance(recorded[0], ZeroDivisionError)) + finally: + flask.got_request_exception.disconnect(record, app) + + +def suite(): + suite = unittest.TestSuite() + if flask.signals_available: + suite.addTest(unittest.makeSuite(SignalsTestCase)) + return suite diff --git a/websdk/flask/testsuite/static/index.html b/websdk/flask/testsuite/static/index.html new file mode 100644 index 0000000..de8b69b --- /dev/null +++ b/websdk/flask/testsuite/static/index.html @@ -0,0 +1 @@ +

Hello World!

diff --git a/websdk/flask/testsuite/subclassing.py b/websdk/flask/testsuite/subclassing.py new file mode 100644 index 0000000..e56ad56 --- /dev/null +++ b/websdk/flask/testsuite/subclassing.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +""" + flask.testsuite.subclassing + ~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Test that certain behavior of flask can be customized by + subclasses. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +import flask +import unittest +from StringIO import StringIO +from logging import StreamHandler +from flask.testsuite import FlaskTestCase + + +class FlaskSubclassingTestCase(FlaskTestCase): + + def test_supressed_exception_logging(self): + class SupressedFlask(flask.Flask): + def log_exception(self, exc_info): + pass + + out = StringIO() + app = SupressedFlask(__name__) + app.logger_name = 'flask_tests/test_supressed_exception_logging' + app.logger.addHandler(StreamHandler(out)) + + @app.route('/') + def index(): + 1/0 + + rv = app.test_client().get('/') + self.assert_equal(rv.status_code, 500) + self.assert_('Internal Server Error' in rv.data) + + err = out.getvalue() + self.assert_equal(err, '') + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(FlaskSubclassingTestCase)) + return suite diff --git a/websdk/flask/testsuite/templates/_macro.html b/websdk/flask/testsuite/templates/_macro.html new file mode 100644 index 0000000..3460ae2 --- /dev/null +++ b/websdk/flask/testsuite/templates/_macro.html @@ -0,0 +1 @@ +{% macro hello(name) %}Hello {{ name }}!{% endmacro %} diff --git a/websdk/flask/testsuite/templates/context_template.html b/websdk/flask/testsuite/templates/context_template.html new file mode 100644 index 0000000..fadf3e5 --- /dev/null +++ b/websdk/flask/testsuite/templates/context_template.html @@ -0,0 +1 @@ +

{{ value }}|{{ injected_value }} diff --git a/websdk/flask/testsuite/templates/escaping_template.html b/websdk/flask/testsuite/templates/escaping_template.html new file mode 100644 index 0000000..dc47644 --- /dev/null +++ b/websdk/flask/testsuite/templates/escaping_template.html @@ -0,0 +1,6 @@ +{{ text }} +{{ html }} +{% autoescape false %}{{ text }} +{{ html }}{% endautoescape %} +{% autoescape true %}{{ text }} +{{ html }}{% endautoescape %} diff --git a/websdk/flask/testsuite/templates/mail.txt b/websdk/flask/testsuite/templates/mail.txt new file mode 100644 index 0000000..d6cb92e --- /dev/null +++ b/websdk/flask/testsuite/templates/mail.txt @@ -0,0 +1 @@ +{{ foo}} Mail diff --git a/websdk/flask/testsuite/templates/nested/nested.txt b/websdk/flask/testsuite/templates/nested/nested.txt new file mode 100644 index 0000000..2c8634f --- /dev/null +++ b/websdk/flask/testsuite/templates/nested/nested.txt @@ -0,0 +1 @@ +I'm nested diff --git a/websdk/flask/testsuite/templates/simple_template.html b/websdk/flask/testsuite/templates/simple_template.html new file mode 100644 index 0000000..c24612c --- /dev/null +++ b/websdk/flask/testsuite/templates/simple_template.html @@ -0,0 +1 @@ +

{{ whiskey }}

diff --git a/websdk/flask/testsuite/templates/template_filter.html b/websdk/flask/testsuite/templates/template_filter.html new file mode 100644 index 0000000..af46cd9 --- /dev/null +++ b/websdk/flask/testsuite/templates/template_filter.html @@ -0,0 +1 @@ +{{ value|super_reverse }} \ No newline at end of file diff --git a/websdk/flask/testsuite/templating.py b/websdk/flask/testsuite/templating.py new file mode 100644 index 0000000..453bfb6 --- /dev/null +++ b/websdk/flask/testsuite/templating.py @@ -0,0 +1,144 @@ +# -*- coding: utf-8 -*- +""" + flask.testsuite.templating + ~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Template functionality + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import with_statement + +import flask +import unittest +from flask.testsuite import FlaskTestCase + + +class TemplatingTestCase(FlaskTestCase): + + def test_context_processing(self): + app = flask.Flask(__name__) + @app.context_processor + def context_processor(): + return {'injected_value': 42} + @app.route('/') + def index(): + return flask.render_template('context_template.html', value=23) + rv = app.test_client().get('/') + self.assert_equal(rv.data, '

23|42') + + def test_original_win(self): + app = flask.Flask(__name__) + @app.route('/') + def index(): + return flask.render_template_string('{{ config }}', config=42) + rv = app.test_client().get('/') + self.assert_equal(rv.data, '42') + + def test_standard_context(self): + app = flask.Flask(__name__) + app.secret_key = 'development key' + @app.route('/') + def index(): + flask.g.foo = 23 + flask.session['test'] = 'aha' + return flask.render_template_string(''' + {{ request.args.foo }} + {{ g.foo }} + {{ config.DEBUG }} + {{ session.test }} + ''') + rv = app.test_client().get('/?foo=42') + self.assert_equal(rv.data.split(), ['42', '23', 'False', 'aha']) + + def test_escaping(self): + text = '

Hello World!' + app = flask.Flask(__name__) + @app.route('/') + def index(): + return flask.render_template('escaping_template.html', text=text, + html=flask.Markup(text)) + lines = app.test_client().get('/').data.splitlines() + self.assert_equal(lines, [ + '<p>Hello World!', + '

Hello World!', + '

Hello World!', + '

Hello World!', + '<p>Hello World!', + '

Hello World!' + ]) + + def test_no_escaping(self): + app = flask.Flask(__name__) + with app.test_request_context(): + self.assert_equal(flask.render_template_string('{{ foo }}', + foo=''), '') + self.assert_equal(flask.render_template('mail.txt', foo=''), + ' Mail') + + def test_macros(self): + app = flask.Flask(__name__) + with app.test_request_context(): + macro = flask.get_template_attribute('_macro.html', 'hello') + self.assert_equal(macro('World'), 'Hello World!') + + def test_template_filter(self): + app = flask.Flask(__name__) + @app.template_filter() + def my_reverse(s): + return s[::-1] + self.assert_('my_reverse' in app.jinja_env.filters.keys()) + self.assert_equal(app.jinja_env.filters['my_reverse'], my_reverse) + self.assert_equal(app.jinja_env.filters['my_reverse']('abcd'), 'dcba') + + def test_template_filter_with_name(self): + app = flask.Flask(__name__) + @app.template_filter('strrev') + def my_reverse(s): + return s[::-1] + self.assert_('strrev' in app.jinja_env.filters.keys()) + self.assert_equal(app.jinja_env.filters['strrev'], my_reverse) + self.assert_equal(app.jinja_env.filters['strrev']('abcd'), 'dcba') + + def test_template_filter_with_template(self): + app = flask.Flask(__name__) + @app.template_filter() + def super_reverse(s): + return s[::-1] + @app.route('/') + def index(): + return flask.render_template('template_filter.html', value='abcd') + rv = app.test_client().get('/') + self.assert_equal(rv.data, 'dcba') + + def test_template_filter_with_name_and_template(self): + app = flask.Flask(__name__) + @app.template_filter('super_reverse') + def my_reverse(s): + return s[::-1] + @app.route('/') + def index(): + return flask.render_template('template_filter.html', value='abcd') + rv = app.test_client().get('/') + self.assert_equal(rv.data, 'dcba') + + def test_custom_template_loader(self): + class MyFlask(flask.Flask): + def create_global_jinja_loader(self): + from jinja2 import DictLoader + return DictLoader({'index.html': 'Hello Custom World!'}) + app = MyFlask(__name__) + @app.route('/') + def index(): + return flask.render_template('index.html') + c = app.test_client() + rv = c.get('/') + self.assert_equal(rv.data, 'Hello Custom World!') + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(TemplatingTestCase)) + return suite diff --git a/websdk/flask/testsuite/test_apps/blueprintapp/__init__.py b/websdk/flask/testsuite/test_apps/blueprintapp/__init__.py new file mode 100644 index 0000000..2b8ef75 --- /dev/null +++ b/websdk/flask/testsuite/test_apps/blueprintapp/__init__.py @@ -0,0 +1,7 @@ +from flask import Flask + +app = Flask(__name__) +from blueprintapp.apps.admin import admin +from blueprintapp.apps.frontend import frontend +app.register_blueprint(admin) +app.register_blueprint(frontend) diff --git a/websdk/flask/testsuite/test_apps/blueprintapp/apps/__init__.py b/websdk/flask/testsuite/test_apps/blueprintapp/apps/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/websdk/flask/testsuite/test_apps/blueprintapp/apps/__init__.py diff --git a/websdk/flask/testsuite/test_apps/blueprintapp/apps/admin/__init__.py b/websdk/flask/testsuite/test_apps/blueprintapp/apps/admin/__init__.py new file mode 100644 index 0000000..3f714d9 --- /dev/null +++ b/websdk/flask/testsuite/test_apps/blueprintapp/apps/admin/__init__.py @@ -0,0 +1,15 @@ +from flask import Blueprint, render_template + +admin = Blueprint('admin', __name__, url_prefix='/admin', + template_folder='templates', + static_folder='static') + + +@admin.route('/') +def index(): + return render_template('admin/index.html') + + +@admin.route('/index2') +def index2(): + return render_template('./admin/index.html') diff --git a/websdk/flask/testsuite/test_apps/blueprintapp/apps/admin/static/css/test.css b/websdk/flask/testsuite/test_apps/blueprintapp/apps/admin/static/css/test.css new file mode 100644 index 0000000..b9f564d --- /dev/null +++ b/websdk/flask/testsuite/test_apps/blueprintapp/apps/admin/static/css/test.css @@ -0,0 +1 @@ +/* nested file */ diff --git a/websdk/flask/testsuite/test_apps/blueprintapp/apps/admin/static/test.txt b/websdk/flask/testsuite/test_apps/blueprintapp/apps/admin/static/test.txt new file mode 100644 index 0000000..f220d22 --- /dev/null +++ b/websdk/flask/testsuite/test_apps/blueprintapp/apps/admin/static/test.txt @@ -0,0 +1 @@ +Admin File diff --git a/websdk/flask/testsuite/test_apps/blueprintapp/apps/admin/templates/admin/index.html b/websdk/flask/testsuite/test_apps/blueprintapp/apps/admin/templates/admin/index.html new file mode 100644 index 0000000..eeec199 --- /dev/null +++ b/websdk/flask/testsuite/test_apps/blueprintapp/apps/admin/templates/admin/index.html @@ -0,0 +1 @@ +Hello from the Admin diff --git a/websdk/flask/testsuite/test_apps/blueprintapp/apps/frontend/__init__.py b/websdk/flask/testsuite/test_apps/blueprintapp/apps/frontend/__init__.py new file mode 100644 index 0000000..69c8666 --- /dev/null +++ b/websdk/flask/testsuite/test_apps/blueprintapp/apps/frontend/__init__.py @@ -0,0 +1,8 @@ +from flask import Blueprint, render_template + +frontend = Blueprint('frontend', __name__, template_folder='templates') + + +@frontend.route('/') +def index(): + return render_template('frontend/index.html') diff --git a/websdk/flask/testsuite/test_apps/blueprintapp/apps/frontend/templates/frontend/index.html b/websdk/flask/testsuite/test_apps/blueprintapp/apps/frontend/templates/frontend/index.html new file mode 100644 index 0000000..a062d71 --- /dev/null +++ b/websdk/flask/testsuite/test_apps/blueprintapp/apps/frontend/templates/frontend/index.html @@ -0,0 +1 @@ +Hello from the Frontend diff --git a/websdk/flask/testsuite/test_apps/config_module_app.py b/websdk/flask/testsuite/test_apps/config_module_app.py new file mode 100644 index 0000000..380d46b --- /dev/null +++ b/websdk/flask/testsuite/test_apps/config_module_app.py @@ -0,0 +1,4 @@ +import os +import flask +here = os.path.abspath(os.path.dirname(__file__)) +app = flask.Flask(__name__) diff --git a/websdk/flask/testsuite/test_apps/config_package_app/__init__.py b/websdk/flask/testsuite/test_apps/config_package_app/__init__.py new file mode 100644 index 0000000..380d46b --- /dev/null +++ b/websdk/flask/testsuite/test_apps/config_package_app/__init__.py @@ -0,0 +1,4 @@ +import os +import flask +here = os.path.abspath(os.path.dirname(__file__)) +app = flask.Flask(__name__) diff --git a/websdk/flask/testsuite/test_apps/flask_broken/__init__.py b/websdk/flask/testsuite/test_apps/flask_broken/__init__.py new file mode 100644 index 0000000..c194c04 --- /dev/null +++ b/websdk/flask/testsuite/test_apps/flask_broken/__init__.py @@ -0,0 +1,2 @@ +import flask.ext.broken.b +import missing_module diff --git a/websdk/flask/testsuite/test_apps/flask_broken/b.py b/websdk/flask/testsuite/test_apps/flask_broken/b.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/websdk/flask/testsuite/test_apps/flask_broken/b.py diff --git a/websdk/flask/testsuite/test_apps/flask_newext_package/__init__.py b/websdk/flask/testsuite/test_apps/flask_newext_package/__init__.py new file mode 100644 index 0000000..3fd13e1 --- /dev/null +++ b/websdk/flask/testsuite/test_apps/flask_newext_package/__init__.py @@ -0,0 +1 @@ +ext_id = 'newext_package' diff --git a/websdk/flask/testsuite/test_apps/flask_newext_package/submodule.py b/websdk/flask/testsuite/test_apps/flask_newext_package/submodule.py new file mode 100644 index 0000000..26ad56b --- /dev/null +++ b/websdk/flask/testsuite/test_apps/flask_newext_package/submodule.py @@ -0,0 +1,2 @@ +def test_function(): + return 42 diff --git a/websdk/flask/testsuite/test_apps/flask_newext_simple.py b/websdk/flask/testsuite/test_apps/flask_newext_simple.py new file mode 100644 index 0000000..dc4a362 --- /dev/null +++ b/websdk/flask/testsuite/test_apps/flask_newext_simple.py @@ -0,0 +1 @@ +ext_id = 'newext_simple' diff --git a/websdk/flask/testsuite/test_apps/flaskext/__init__.py b/websdk/flask/testsuite/test_apps/flaskext/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/websdk/flask/testsuite/test_apps/flaskext/__init__.py diff --git a/websdk/flask/testsuite/test_apps/flaskext/oldext_package/__init__.py b/websdk/flask/testsuite/test_apps/flaskext/oldext_package/__init__.py new file mode 100644 index 0000000..7c46206 --- /dev/null +++ b/websdk/flask/testsuite/test_apps/flaskext/oldext_package/__init__.py @@ -0,0 +1 @@ +ext_id = 'oldext_package' diff --git a/websdk/flask/testsuite/test_apps/flaskext/oldext_package/submodule.py b/websdk/flask/testsuite/test_apps/flaskext/oldext_package/submodule.py new file mode 100644 index 0000000..26ad56b --- /dev/null +++ b/websdk/flask/testsuite/test_apps/flaskext/oldext_package/submodule.py @@ -0,0 +1,2 @@ +def test_function(): + return 42 diff --git a/websdk/flask/testsuite/test_apps/flaskext/oldext_simple.py b/websdk/flask/testsuite/test_apps/flaskext/oldext_simple.py new file mode 100644 index 0000000..c6664a7 --- /dev/null +++ b/websdk/flask/testsuite/test_apps/flaskext/oldext_simple.py @@ -0,0 +1 @@ +ext_id = 'oldext_simple' diff --git a/websdk/flask/testsuite/test_apps/moduleapp/__init__.py b/websdk/flask/testsuite/test_apps/moduleapp/__init__.py new file mode 100644 index 0000000..35e82d4 --- /dev/null +++ b/websdk/flask/testsuite/test_apps/moduleapp/__init__.py @@ -0,0 +1,7 @@ +from flask import Flask + +app = Flask(__name__) +from moduleapp.apps.admin import admin +from moduleapp.apps.frontend import frontend +app.register_module(admin) +app.register_module(frontend) diff --git a/websdk/flask/testsuite/test_apps/moduleapp/apps/__init__.py b/websdk/flask/testsuite/test_apps/moduleapp/apps/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/websdk/flask/testsuite/test_apps/moduleapp/apps/__init__.py diff --git a/websdk/flask/testsuite/test_apps/moduleapp/apps/admin/__init__.py b/websdk/flask/testsuite/test_apps/moduleapp/apps/admin/__init__.py new file mode 100644 index 0000000..b85b802 --- /dev/null +++ b/websdk/flask/testsuite/test_apps/moduleapp/apps/admin/__init__.py @@ -0,0 +1,14 @@ +from flask import Module, render_template + + +admin = Module(__name__, url_prefix='/admin') + + +@admin.route('/') +def index(): + return render_template('admin/index.html') + + +@admin.route('/index2') +def index2(): + return render_template('./admin/index.html') diff --git a/websdk/flask/testsuite/test_apps/moduleapp/apps/admin/static/css/test.css b/websdk/flask/testsuite/test_apps/moduleapp/apps/admin/static/css/test.css new file mode 100644 index 0000000..b9f564d --- /dev/null +++ b/websdk/flask/testsuite/test_apps/moduleapp/apps/admin/static/css/test.css @@ -0,0 +1 @@ +/* nested file */ diff --git a/websdk/flask/testsuite/test_apps/moduleapp/apps/admin/static/test.txt b/websdk/flask/testsuite/test_apps/moduleapp/apps/admin/static/test.txt new file mode 100644 index 0000000..f220d22 --- /dev/null +++ b/websdk/flask/testsuite/test_apps/moduleapp/apps/admin/static/test.txt @@ -0,0 +1 @@ +Admin File diff --git a/websdk/flask/testsuite/test_apps/moduleapp/apps/admin/templates/index.html b/websdk/flask/testsuite/test_apps/moduleapp/apps/admin/templates/index.html new file mode 100644 index 0000000..eeec199 --- /dev/null +++ b/websdk/flask/testsuite/test_apps/moduleapp/apps/admin/templates/index.html @@ -0,0 +1 @@ +Hello from the Admin diff --git a/websdk/flask/testsuite/test_apps/moduleapp/apps/frontend/__init__.py b/websdk/flask/testsuite/test_apps/moduleapp/apps/frontend/__init__.py new file mode 100644 index 0000000..f83581e --- /dev/null +++ b/websdk/flask/testsuite/test_apps/moduleapp/apps/frontend/__init__.py @@ -0,0 +1,9 @@ +from flask import Module, render_template + + +frontend = Module(__name__) + + +@frontend.route('/') +def index(): + return render_template('frontend/index.html') diff --git a/websdk/flask/testsuite/test_apps/moduleapp/apps/frontend/templates/index.html b/websdk/flask/testsuite/test_apps/moduleapp/apps/frontend/templates/index.html new file mode 100644 index 0000000..a062d71 --- /dev/null +++ b/websdk/flask/testsuite/test_apps/moduleapp/apps/frontend/templates/index.html @@ -0,0 +1 @@ +Hello from the Frontend diff --git a/websdk/flask/testsuite/test_apps/subdomaintestmodule/__init__.py b/websdk/flask/testsuite/test_apps/subdomaintestmodule/__init__.py new file mode 100644 index 0000000..3c5e358 --- /dev/null +++ b/websdk/flask/testsuite/test_apps/subdomaintestmodule/__init__.py @@ -0,0 +1,4 @@ +from flask import Module + + +mod = Module(__name__, 'foo', subdomain='foo') diff --git a/websdk/flask/testsuite/test_apps/subdomaintestmodule/static/hello.txt b/websdk/flask/testsuite/test_apps/subdomaintestmodule/static/hello.txt new file mode 100644 index 0000000..12e23c1 --- /dev/null +++ b/websdk/flask/testsuite/test_apps/subdomaintestmodule/static/hello.txt @@ -0,0 +1 @@ +Hello Subdomain diff --git a/websdk/flask/testsuite/testing.py b/websdk/flask/testsuite/testing.py new file mode 100644 index 0000000..6574e77 --- /dev/null +++ b/websdk/flask/testsuite/testing.py @@ -0,0 +1,171 @@ +# -*- coding: utf-8 -*- +""" + flask.testsuite.testing + ~~~~~~~~~~~~~~~~~~~~~~~ + + Test client and more. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import with_statement + +import flask +import unittest +from flask.testsuite import FlaskTestCase + + +class TestToolsTestCase(FlaskTestCase): + + def test_environ_defaults_from_config(self): + app = flask.Flask(__name__) + app.testing = True + app.config['SERVER_NAME'] = 'example.com:1234' + app.config['APPLICATION_ROOT'] = '/foo' + @app.route('/') + def index(): + return flask.request.url + + ctx = app.test_request_context() + self.assert_equal(ctx.request.url, 'http://example.com:1234/foo/') + with app.test_client() as c: + rv = c.get('/') + self.assert_equal(rv.data, 'http://example.com:1234/foo/') + + def test_environ_defaults(self): + app = flask.Flask(__name__) + app.testing = True + @app.route('/') + def index(): + return flask.request.url + + ctx = app.test_request_context() + self.assert_equal(ctx.request.url, 'http://localhost/') + with app.test_client() as c: + rv = c.get('/') + self.assert_equal(rv.data, 'http://localhost/') + + def test_session_transactions(self): + app = flask.Flask(__name__) + app.testing = True + app.secret_key = 'testing' + + @app.route('/') + def index(): + return unicode(flask.session['foo']) + + with app.test_client() as c: + with c.session_transaction() as sess: + self.assert_equal(len(sess), 0) + sess['foo'] = [42] + self.assert_equal(len(sess), 1) + rv = c.get('/') + self.assert_equal(rv.data, '[42]') + with c.session_transaction() as sess: + self.assert_equal(len(sess), 1) + self.assert_equal(sess['foo'], [42]) + + def test_session_transactions_no_null_sessions(self): + app = flask.Flask(__name__) + app.testing = True + + with app.test_client() as c: + try: + with c.session_transaction() as sess: + pass + except RuntimeError, e: + self.assert_('Session backend did not open a session' in str(e)) + else: + self.fail('Expected runtime error') + + def test_session_transactions_keep_context(self): + app = flask.Flask(__name__) + app.testing = True + app.secret_key = 'testing' + + with app.test_client() as c: + rv = c.get('/') + req = flask.request._get_current_object() + with c.session_transaction(): + self.assert_(req is flask.request._get_current_object()) + + def test_session_transaction_needs_cookies(self): + app = flask.Flask(__name__) + app.testing = True + c = app.test_client(use_cookies=False) + try: + with c.session_transaction() as s: + pass + except RuntimeError, e: + self.assert_('cookies' in str(e)) + else: + self.fail('Expected runtime error') + + def test_test_client_context_binding(self): + app = flask.Flask(__name__) + @app.route('/') + def index(): + flask.g.value = 42 + return 'Hello World!' + + @app.route('/other') + def other(): + 1/0 + + with app.test_client() as c: + resp = c.get('/') + self.assert_equal(flask.g.value, 42) + self.assert_equal(resp.data, 'Hello World!') + self.assert_equal(resp.status_code, 200) + + resp = c.get('/other') + self.assert_(not hasattr(flask.g, 'value')) + self.assert_('Internal Server Error' in resp.data) + self.assert_equal(resp.status_code, 500) + flask.g.value = 23 + + try: + flask.g.value + except (AttributeError, RuntimeError): + pass + else: + raise AssertionError('some kind of exception expected') + + def test_reuse_client(self): + app = flask.Flask(__name__) + c = app.test_client() + + with c: + self.assert_equal(c.get('/').status_code, 404) + + with c: + self.assert_equal(c.get('/').status_code, 404) + + def test_test_client_calls_teardown_handlers(self): + app = flask.Flask(__name__) + called = [] + @app.teardown_request + def remember(error): + called.append(error) + + with app.test_client() as c: + self.assert_equal(called, []) + c.get('/') + self.assert_equal(called, []) + self.assert_equal(called, [None]) + + del called[:] + with app.test_client() as c: + self.assert_equal(called, []) + c.get('/') + self.assert_equal(called, []) + c.get('/') + self.assert_equal(called, [None]) + self.assert_equal(called, [None, None]) + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(TestToolsTestCase)) + return suite diff --git a/websdk/flask/testsuite/views.py b/websdk/flask/testsuite/views.py new file mode 100644 index 0000000..c7cb0a8 --- /dev/null +++ b/websdk/flask/testsuite/views.py @@ -0,0 +1,152 @@ +# -*- coding: utf-8 -*- +""" + flask.testsuite.views + ~~~~~~~~~~~~~~~~~~~~~ + + Pluggable views. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +import flask +import flask.views +import unittest +from flask.testsuite import FlaskTestCase +from werkzeug.http import parse_set_header + + +class ViewTestCase(FlaskTestCase): + + def common_test(self, app): + c = app.test_client() + + self.assert_equal(c.get('/').data, 'GET') + self.assert_equal(c.post('/').data, 'POST') + self.assert_equal(c.put('/').status_code, 405) + meths = parse_set_header(c.open('/', method='OPTIONS').headers['Allow']) + self.assert_equal(sorted(meths), ['GET', 'HEAD', 'OPTIONS', 'POST']) + + def test_basic_view(self): + app = flask.Flask(__name__) + + class Index(flask.views.View): + methods = ['GET', 'POST'] + def dispatch_request(self): + return flask.request.method + + app.add_url_rule('/', view_func=Index.as_view('index')) + self.common_test(app) + + def test_method_based_view(self): + app = flask.Flask(__name__) + + class Index(flask.views.MethodView): + def get(self): + return 'GET' + def post(self): + return 'POST' + + app.add_url_rule('/', view_func=Index.as_view('index')) + + self.common_test(app) + + def test_view_patching(self): + app = flask.Flask(__name__) + + class Index(flask.views.MethodView): + def get(self): + 1/0 + def post(self): + 1/0 + + class Other(Index): + def get(self): + return 'GET' + def post(self): + return 'POST' + + view = Index.as_view('index') + view.view_class = Other + app.add_url_rule('/', view_func=view) + self.common_test(app) + + def test_view_inheritance(self): + app = flask.Flask(__name__) + + class Index(flask.views.MethodView): + def get(self): + return 'GET' + def post(self): + return 'POST' + + class BetterIndex(Index): + def delete(self): + return 'DELETE' + + app.add_url_rule('/', view_func=BetterIndex.as_view('index')) + c = app.test_client() + + meths = parse_set_header(c.open('/', method='OPTIONS').headers['Allow']) + self.assert_equal(sorted(meths), ['DELETE', 'GET', 'HEAD', 'OPTIONS', 'POST']) + + def test_view_decorators(self): + app = flask.Flask(__name__) + + def add_x_parachute(f): + def new_function(*args, **kwargs): + resp = flask.make_response(f(*args, **kwargs)) + resp.headers['X-Parachute'] = 'awesome' + return resp + return new_function + + class Index(flask.views.View): + decorators = [add_x_parachute] + def dispatch_request(self): + return 'Awesome' + + app.add_url_rule('/', view_func=Index.as_view('index')) + c = app.test_client() + rv = c.get('/') + self.assert_equal(rv.headers['X-Parachute'], 'awesome') + self.assert_equal(rv.data, 'Awesome') + + def test_implicit_head(self): + app = flask.Flask(__name__) + + class Index(flask.views.MethodView): + def get(self): + return flask.Response('Blub', headers={ + 'X-Method': flask.request.method + }) + + app.add_url_rule('/', view_func=Index.as_view('index')) + c = app.test_client() + rv = c.get('/') + self.assert_equal(rv.data, 'Blub') + self.assert_equal(rv.headers['X-Method'], 'GET') + rv = c.head('/') + self.assert_equal(rv.data, '') + self.assert_equal(rv.headers['X-Method'], 'HEAD') + + def test_explicit_head(self): + app = flask.Flask(__name__) + + class Index(flask.views.MethodView): + def get(self): + return 'GET' + def head(self): + return flask.Response('', headers={'X-Method': 'HEAD'}) + + app.add_url_rule('/', view_func=Index.as_view('index')) + c = app.test_client() + rv = c.get('/') + self.assert_equal(rv.data, 'GET') + rv = c.head('/') + self.assert_equal(rv.data, '') + self.assert_equal(rv.headers['X-Method'], 'HEAD') + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(ViewTestCase)) + return suite diff --git a/websdk/flask/views.py b/websdk/flask/views.py new file mode 100644 index 0000000..be718cc --- /dev/null +++ b/websdk/flask/views.py @@ -0,0 +1,151 @@ +# -*- coding: utf-8 -*- +""" + flask.views + ~~~~~~~~~~~ + + This module provides class based views inspired by the ones in Django. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +from .globals import request + + +http_method_funcs = frozenset(['get', 'post', 'head', 'options', + 'delete', 'put', 'trace']) + + + +class View(object): + """Alternative way to use view functions. A subclass has to implement + :meth:`dispatch_request` which is called with the view arguments from + the URL routing system. If :attr:`methods` is provided the methods + do not have to be passed to the :meth:`~flask.Flask.add_url_rule` + method explicitly:: + + class MyView(View): + methods = ['GET'] + + def dispatch_request(self, name): + return 'Hello %s!' % name + + app.add_url_rule('/hello/', view_func=MyView.as_view('myview')) + + When you want to decorate a pluggable view you will have to either do that + when the view function is created (by wrapping the return value of + :meth:`as_view`) or you can use the :attr:`decorators` attribute:: + + class SecretView(View): + methods = ['GET'] + decorators = [superuser_required] + + def dispatch_request(self): + ... + + The decorators stored in the decorators list are applied one after another + when the view function is created. Note that you can *not* use the class + based decorators since those would decorate the view class and not the + generated view function! + """ + + #: A for which methods this pluggable view can handle. + methods = None + + #: The canonical way to decorate class based views is to decorate the + #: return value of as_view(). However since this moves parts of the + #: logic from the class declaration to the place where it's hooked + #: into the routing system. + #: + #: You can place one or more decorators in this list and whenever the + #: view function is created the result is automatically decorated. + #: + #: .. versionadded:: 0.8 + decorators = [] + + def dispatch_request(self): + """Subclasses have to override this method to implement the + actual view function code. This method is called with all + the arguments from the URL rule. + """ + raise NotImplementedError() + + @classmethod + def as_view(cls, name, *class_args, **class_kwargs): + """Converts the class into an actual view function that can be + used with the routing system. What it does internally is generating + a function on the fly that will instanciate the :class:`View` + on each request and call the :meth:`dispatch_request` method on it. + + The arguments passed to :meth:`as_view` are forwarded to the + constructor of the class. + """ + def view(*args, **kwargs): + self = view.view_class(*class_args, **class_kwargs) + return self.dispatch_request(*args, **kwargs) + + if cls.decorators: + view.__name__ = name + view.__module__ = cls.__module__ + for decorator in cls.decorators: + view = decorator(view) + + # we attach the view class to the view function for two reasons: + # first of all it allows us to easily figure out what class based + # view this thing came from, secondly it's also used for instanciating + # the view class so you can actually replace it with something else + # for testing purposes and debugging. + view.view_class = cls + view.__name__ = name + view.__doc__ = cls.__doc__ + view.__module__ = cls.__module__ + view.methods = cls.methods + return view + + +class MethodViewType(type): + + def __new__(cls, name, bases, d): + rv = type.__new__(cls, name, bases, d) + if 'methods' not in d: + methods = set(rv.methods or []) + for key, value in d.iteritems(): + if key in http_method_funcs: + methods.add(key.upper()) + # if we have no method at all in there we don't want to + # add a method list. (This is for instance the case for + # the baseclass or another subclass of a base method view + # that does not introduce new methods). + if methods: + rv.methods = sorted(methods) + return rv + + +class MethodView(View): + """Like a regular class based view but that dispatches requests to + particular methods. For instance if you implement a method called + :meth:`get` it means you will response to ``'GET'`` requests and + the :meth:`dispatch_request` implementation will automatically + forward your request to that. Also :attr:`options` is set for you + automatically:: + + class CounterAPI(MethodView): + + def get(self): + return session.get('counter', 0) + + def post(self): + session['counter'] = session.get('counter', 0) + 1 + return 'OK' + + app.add_url_rule('/counter', view_func=CounterAPI.as_view('counter')) + """ + __metaclass__ = MethodViewType + + def dispatch_request(self, *args, **kwargs): + meth = getattr(self, request.method.lower(), None) + # if the request method is HEAD and we don't have a handler for it + # retry with GET + if meth is None and request.method == 'HEAD': + meth = getattr(self, 'get', None) + assert meth is not None, 'Not implemented method %r' % request.method + return meth(*args, **kwargs) diff --git a/websdk/flask/wrappers.py b/websdk/flask/wrappers.py new file mode 100644 index 0000000..f6ec278 --- /dev/null +++ b/websdk/flask/wrappers.py @@ -0,0 +1,138 @@ +# -*- coding: utf-8 -*- +""" + flask.wrappers + ~~~~~~~~~~~~~~ + + Implements the WSGI wrappers (request and response). + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" + +from werkzeug.wrappers import Request as RequestBase, Response as ResponseBase +from werkzeug.exceptions import BadRequest +from werkzeug.utils import cached_property + +from .debughelpers import attach_enctype_error_multidict +from .helpers import json, _assert_have_json +from .globals import _request_ctx_stack + + +class Request(RequestBase): + """The request object used by default in Flask. Remembers the + matched endpoint and view arguments. + + It is what ends up as :class:`~flask.request`. If you want to replace + the request object used you can subclass this and set + :attr:`~flask.Flask.request_class` to your subclass. + + The request object is a :class:`~werkzeug.wrappers.Request` subclass and + provides all of the attributes Werkzeug defines plus a few Flask + specific ones. + """ + + #: the internal URL rule that matched the request. This can be + #: useful to inspect which methods are allowed for the URL from + #: a before/after handler (``request.url_rule.methods``) etc. + #: + #: .. versionadded:: 0.6 + url_rule = None + + #: a dict of view arguments that matched the request. If an exception + #: happened when matching, this will be `None`. + view_args = None + + #: if matching the URL failed, this is the exception that will be + #: raised / was raised as part of the request handling. This is + #: usually a :exc:`~werkzeug.exceptions.NotFound` exception or + #: something similar. + routing_exception = None + + # switched by the request context until 1.0 to opt in deprecated + # module functionality + _is_old_module = False + + @property + def max_content_length(self): + """Read-only view of the `MAX_CONTENT_LENGTH` config key.""" + ctx = _request_ctx_stack.top + if ctx is not None: + return ctx.app.config['MAX_CONTENT_LENGTH'] + + @property + def endpoint(self): + """The endpoint that matched the request. This in combination with + :attr:`view_args` can be used to reconstruct the same or a + modified URL. If an exception happened when matching, this will + be `None`. + """ + if self.url_rule is not None: + return self.url_rule.endpoint + + @property + def module(self): + """The name of the current module if the request was dispatched + to an actual module. This is deprecated functionality, use blueprints + instead. + """ + from warnings import warn + warn(DeprecationWarning('modules were deprecated in favor of ' + 'blueprints. Use request.blueprint ' + 'instead.'), stacklevel=2) + if self._is_old_module: + return self.blueprint + + @property + def blueprint(self): + """The name of the current blueprint""" + if self.url_rule and '.' in self.url_rule.endpoint: + return self.url_rule.endpoint.rsplit('.', 1)[0] + + @cached_property + def json(self): + """If the mimetype is `application/json` this will contain the + parsed JSON data. Otherwise this will be `None`. + + This requires Python 2.6 or an installed version of simplejson. + """ + if __debug__: + _assert_have_json() + if self.mimetype == 'application/json': + request_charset = self.mimetype_params.get('charset') + try: + if request_charset is not None: + return json.loads(self.data, encoding=request_charset) + return json.loads(self.data) + except ValueError, e: + return self.on_json_loading_failed(e) + + def on_json_loading_failed(self, e): + """Called if decoding of the JSON data failed. The return value of + this method is used by :attr:`json` when an error ocurred. The + default implementation raises a :class:`~werkzeug.exceptions.BadRequest`. + + .. versionadded:: 0.8 + """ + raise BadRequest() + + def _load_form_data(self): + RequestBase._load_form_data(self) + + # in debug mode we're replacing the files multidict with an ad-hoc + # subclass that raises a different error for key errors. + ctx = _request_ctx_stack.top + if ctx is not None and ctx.app.debug and \ + self.mimetype != 'multipart/form-data' and not self.files: + attach_enctype_error_multidict(self) + + +class Response(ResponseBase): + """The response object that is used by default in Flask. Works like the + response object from Werkzeug but is set to have an HTML mimetype by + default. Quite often you don't have to create this object yourself because + :meth:`~flask.Flask.make_response` will take care of that for you. + + If you want to replace the response object used you can subclass this and + set :attr:`~flask.Flask.response_class` to your subclass. + """ + default_mimetype = 'text/html' diff --git a/websdk/flaskext/__init__.py b/websdk/flaskext/__init__.py new file mode 100644 index 0000000..de40ea7 --- /dev/null +++ b/websdk/flaskext/__init__.py @@ -0,0 +1 @@ +__import__('pkg_resources').declare_namespace(__name__) diff --git a/websdk/flaskext/genshi.py b/websdk/flaskext/genshi.py new file mode 100644 index 0000000..a2bbbe5 --- /dev/null +++ b/websdk/flaskext/genshi.py @@ -0,0 +1,294 @@ +# -*- coding: utf-8 -*- +""" + flaskext.genshi + ~~~~~~~~~~~~~~~ + + An extension to Flask for easy Genshi templating. + + :copyright: (c) 2010 by Dag Odenhall . + :license: BSD, see LICENSE for more details. +""" + +from __future__ import absolute_import + +from collections import defaultdict +import os.path +from warnings import warn +from inspect import getargspec + +from genshi.template import (NewTextTemplate, MarkupTemplate, + loader, TemplateLoader) +from werkzeug import cached_property +from flask import current_app + +try: + from flask import signals_available +except ImportError: + signals_available = False +else: + from flask.signals import Namespace + signals = Namespace() + template_generated = signals.signal('template-generated') + + +class Genshi(object): + """Initialize extension. + + :: + + app = Flask(__name__) + genshi = Genshi(app) + + .. versionchanged:: 0.4 + You can now initialize your application later with :meth:`init_app`. + + .. deprecated:: 0.4 + ``app.genshi_instance`` in favor of ``app.extensions['genshi']``. + + """ + + def __init__(self, app=None): + + if app is not None: + self.init_app(app) + + #: A callable for Genshi's callback interface, called when a template + #: is loaded, with the template as the only argument. + #: + #: :meth:`template_parsed` is a decorator for setting this. + #: + #: .. versionadded:: 0.5 + self.callback = None + + #: What method is used for an extension. + self.extensions = { + 'html': 'html', + 'xml': 'xml', + 'txt': 'text', + 'js': 'js', + 'css': 'css', + 'svg': 'svg' + } + + #: Render methods. + #: + #: .. versionchanged:: 0.3 Support for Javascript and CSS. + #: .. versionchanged:: 0.4 Support for SVG. + self.methods = { + 'html': { + 'serializer': 'html', + 'doctype': 'html', + }, + 'html5': { + 'serializer': 'html', + 'doctype': 'html5', + }, + 'xhtml': { + 'serializer': 'xhtml', + 'doctype': 'xhtml', + 'mimetype': 'application/xhtml+xml' + }, + 'xml': { + 'serializer': 'xml', + 'mimetype': 'application/xml' + }, + 'text': { + 'serializer': 'text', + 'mimetype': 'text/plain', + 'class': NewTextTemplate + }, + 'js': { + 'serializer': 'text', + 'mimetype': 'application/javascript', + 'class': NewTextTemplate + }, + 'css': { + 'serializer': 'text', + 'mimetype': 'text/css', + 'class': NewTextTemplate + }, + 'svg': { + 'serializer': 'xml', + 'doctype': 'svg', + 'mimetype': 'image/svg+xml' + } + } + + #: Filter functions to be applied to templates. + #: + #: .. versionadded:: 0.3 + self.filters = defaultdict(list) + + def init_app(self, app): + """Initialize a :class:`~flask.Flask` application + for use with this extension. Useful for the factory pattern but + not needed if you passed your application to the :class:`Genshi` + constructor. + + :: + + genshi = Genshi() + + app = Flask(__name__) + genshi.init_app(app) + + .. versionadded:: 0.4 + + """ + if not hasattr(app, 'extensions'): + app.extensions = {} + + app.extensions['genshi'] = self + app.genshi_instance = self + self.app = app + + def template_parsed(self, callback): + """Set up a calback to be called with a template when it is first + loaded and parsed. This is the correct way to set up the + :class:`~genshi.filters.Translator` filter. + + .. versionadded:: 0.5 + + """ + self.callback = callback + return callback + + @cached_property + def template_loader(self): + """A :class:`genshi.template.TemplateLoader` that loads templates + from the same places as Flask. + + """ + path = loader.directory(os.path.join(self.app.root_path, 'templates')) + module_paths = {} + modules = getattr(self.app, 'modules', {}) + for name, module in modules.iteritems(): + module_path = os.path.join(module.root_path, 'templates') + if os.path.isdir(module_path): + module_paths[name] = loader.directory(module_path) + return TemplateLoader([path, loader.prefixed(**module_paths)], + auto_reload=self.app.debug, + callback=self.callback) + + def filter(self, *methods): + """Decorator that adds a function to apply filters + to templates by rendering method. + + .. versionadded:: 0.3 + + .. versionchanged:: 0.5 + Filters can now optionally take a second argument for the context. + + """ + def decorator(function): + for method in methods: + self.filters[method].append(function) + return function + return decorator + + def _method_for(self, template, method=None): + """Selects a method from :attr:`Genshi.methods` + based on the file extension of ``template`` + and :attr:`Genshi.extensions`, or based on ``method``. + + """ + if method is None: + ext = os.path.splitext(template)[1][1:] + return self.extensions[ext] + return method + + +def select_method(template, method=None): + """Same as :meth:`Genshi._method_for`. + + .. deprecated:: 0.4 + + """ + warn('select_method to be dropped in future releases', + DeprecationWarning, stacklevel=2) + return current_app.extensions['genshi']._method_for(template, method) + + +def generate_template(template=None, context=None, + method=None, string=None, filter=None): + """Creates a Genshi template stream that you can + run filters and transformations on. + + """ + genshi = current_app.extensions['genshi'] + method = genshi._method_for(template, method) + class_ = genshi.methods[method].get('class', MarkupTemplate) + + context = context or {} + for key, value in current_app.jinja_env.globals.iteritems(): + context.setdefault(key, value) + context.setdefault('filters', current_app.jinja_env.filters) + context.setdefault('tests', current_app.jinja_env.tests) + for key, value in current_app.jinja_env.filters.iteritems(): + context.setdefault(key, value) + for key, value in current_app.jinja_env.tests.iteritems(): + context.setdefault('is%s' % key, value) + current_app.update_template_context(context) + + if template is not None: + template = genshi.template_loader.load(template, cls=class_) + elif string is not None: + template = class_(string) + else: + raise RuntimeError('Need a template or string') + + stream = template.generate(**context) + + if signals_available: + template_generated.send(current_app._get_current_object(), + template=template, context=context) + + for func in genshi.filters[method]: + if len(getargspec(func)[0]) == 2: # Filter takes context? + stream = func(stream, context) + else: + stream = func(stream) + + if filter: + if len(getargspec(filter)[0]) == 2: # Filter takes context? + stream = filter(stream, context) + else: + stream = filter(stream) + + return stream + + +def render_template(template=None, context=None, + method=None, string=None, filter=None): + """Renders a template to a string.""" + genshi = current_app.extensions['genshi'] + method = genshi._method_for(template, method) + template = generate_template(template, context, method, string, filter) + render_args = dict(method=genshi.methods[method]['serializer']) + if 'doctype' in genshi.methods[method]: + render_args['doctype'] = genshi.methods[method]['doctype'] + return template.render(**render_args) + + +def render_response(template=None, context=None, + method=None, string=None, filter=None): + """Renders a template and wraps it in a :attr:`~flask.Flask.response_class` + with mimetype set according to the rendering method. + + """ + genshi = current_app.extensions['genshi'] + method = genshi._method_for(template, method) + mimetype = genshi.methods[method].get('mimetype', 'text/html') + template = render_template(template, context, method, string, filter) + return current_app.response_class(template, mimetype=mimetype) + + +def render(template, **context): + """Render a template to a response object, passing the context as + keyword arguments. Shorthand for + ``render_response(template, dict(**context))``. + + .. versionadded:: 0.6 + + """ + return render_response(template, context) diff --git a/websdk/genshi/__init__.py b/websdk/genshi/__init__.py new file mode 100644 index 0000000..02f4347 --- /dev/null +++ b/websdk/genshi/__init__.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2006-2009 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://genshi.edgewall.org/wiki/License. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://genshi.edgewall.org/log/. + +"""This package provides various means for generating and processing web markup +(XML or HTML). + +The design is centered around the concept of streams of markup events (similar +in concept to SAX parsing events) which can be processed in a uniform manner +independently of where or how they are produced. +""" + +__docformat__ = 'restructuredtext en' +__version__ = '0.6' + +from genshi.core import * +from genshi.input import ParseError, XML, HTML diff --git a/websdk/genshi/builder.py b/websdk/genshi/builder.py new file mode 100644 index 0000000..724e364 --- /dev/null +++ b/websdk/genshi/builder.py @@ -0,0 +1,359 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2006-2009 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://genshi.edgewall.org/wiki/License. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://genshi.edgewall.org/log/. + +"""Support for programmatically generating markup streams from Python code using +a very simple syntax. The main entry point to this module is the `tag` object +(which is actually an instance of the ``ElementFactory`` class). You should +rarely (if ever) need to directly import and use any of the other classes in +this module. + +Elements can be created using the `tag` object using attribute access. For +example: + +>>> doc = tag.p('Some text and ', tag.a('a link', href='http://example.org/'), '.') +>>> doc + + +This produces an `Element` instance which can be further modified to add child +nodes and attributes. This is done by "calling" the element: positional +arguments are added as child nodes (alternatively, the `Element.append` method +can be used for that purpose), whereas keywords arguments are added as +attributes: + +>>> doc(tag.br) + +>>> print(doc) +

Some text and a link.

+ +If an attribute name collides with a Python keyword, simply append an underscore +to the name: + +>>> doc(class_='intro') + +>>> print(doc) +

Some text and a link.

+ +As shown above, an `Element` can easily be directly rendered to XML text by +printing it or using the Python ``str()`` function. This is basically a +shortcut for converting the `Element` to a stream and serializing that +stream: + +>>> stream = doc.generate() +>>> stream #doctest: +ELLIPSIS + +>>> print(stream) +

Some text and a link.

+ + +The `tag` object also allows creating "fragments", which are basically lists +of nodes (elements or text) that don't have a parent element. This can be useful +for creating snippets of markup that are attached to a parent element later (for +example in a template). Fragments are created by calling the `tag` object, which +returns an object of type `Fragment`: + +>>> fragment = tag('Hello, ', tag.em('world'), '!') +>>> fragment + +>>> print(fragment) +Hello, world! +""" + +from genshi.core import Attrs, Markup, Namespace, QName, Stream, \ + START, END, TEXT + +__all__ = ['Fragment', 'Element', 'ElementFactory', 'tag'] +__docformat__ = 'restructuredtext en' + + +class Fragment(object): + """Represents a markup fragment, which is basically just a list of element + or text nodes. + """ + __slots__ = ['children'] + + def __init__(self): + """Create a new fragment.""" + self.children = [] + + def __add__(self, other): + return Fragment()(self, other) + + def __call__(self, *args): + """Append any positional arguments as child nodes. + + :see: `append` + """ + for arg in args: + self.append(arg) + return self + + def __iter__(self): + return self._generate() + + def __repr__(self): + return '<%s>' % type(self).__name__ + + def __str__(self): + return str(self.generate()) + + def __unicode__(self): + return unicode(self.generate()) + + def __html__(self): + return Markup(self.generate()) + + def append(self, node): + """Append an element or string as child node. + + :param node: the node to append; can be an `Element`, `Fragment`, or a + `Stream`, or a Python string or number + """ + if isinstance(node, (Stream, Element, basestring, int, float, long)): + # For objects of a known/primitive type, we avoid the check for + # whether it is iterable for better performance + self.children.append(node) + elif isinstance(node, Fragment): + self.children.extend(node.children) + elif node is not None: + try: + for child in node: + self.append(child) + except TypeError: + self.children.append(node) + + def _generate(self): + for child in self.children: + if isinstance(child, Fragment): + for event in child._generate(): + yield event + elif isinstance(child, Stream): + for event in child: + yield event + else: + if not isinstance(child, basestring): + child = unicode(child) + yield TEXT, child, (None, -1, -1) + + def generate(self): + """Return a markup event stream for the fragment. + + :rtype: `Stream` + """ + return Stream(self._generate()) + + +def _kwargs_to_attrs(kwargs): + attrs = [] + names = set() + for name, value in kwargs.items(): + name = name.rstrip('_').replace('_', '-') + if value is not None and name not in names: + attrs.append((QName(name), unicode(value))) + names.add(name) + return Attrs(attrs) + + +class Element(Fragment): + """Simple XML output generator based on the builder pattern. + + Construct XML elements by passing the tag name to the constructor: + + >>> print(Element('strong')) + + + Attributes can be specified using keyword arguments. The values of the + arguments will be converted to strings and any special XML characters + escaped: + + >>> print(Element('textarea', rows=10, cols=60)) + " + + "", + + boxSelector: ".wym_box", + toolsSelector: ".wym_tools", + toolsListSelector: " ul", + containersSelector:".wym_containers", + classesSelector: ".wym_classes", + htmlSelector: ".wym_html", + iframeSelector: ".wym_iframe iframe", + iframeBodySelector:".wym_iframe", + statusSelector: ".wym_status", + toolSelector: ".wym_tools a", + containerSelector: ".wym_containers a", + classSelector: ".wym_classes a", + htmlValSelector: ".wym_html_val", + + hrefSelector: ".wym_href", + srcSelector: ".wym_src", + titleSelector: ".wym_title", + relSelector: ".wym_rel", + altSelector: ".wym_alt", + textSelector: ".wym_text", + + rowsSelector: ".wym_rows", + colsSelector: ".wym_cols", + captionSelector: ".wym_caption", + summarySelector: ".wym_summary", + + submitSelector: "form", + cancelSelector: ".wym_cancel", + previewSelector: "", + + dialogTypeSelector: ".wym_dialog_type", + dialogLinkSelector: ".wym_dialog_link", + dialogImageSelector: ".wym_dialog_image", + dialogTableSelector: ".wym_dialog_table", + dialogPasteSelector: ".wym_dialog_paste", + dialogPreviewSelector: ".wym_dialog_preview", + + updateSelector: ".wymupdate", + updateEvent: "click", + + dialogFeatures: "menubar=no,titlebar=no,toolbar=no,resizable=no" + + ",width=560,height=300,top=0,left=0", + dialogFeaturesPreview: "menubar=no,titlebar=no,toolbar=no,resizable=no" + + ",scrollbars=yes,width=560,height=300,top=0,left=0", + + dialogHtml: "" + + "" + + "" + + "" + + WYMeditor.DIALOG_TITLE + + "" + + "" + + "" + + "" + + WYMeditor.DIALOG_BODY + + "", + + dialogLinkHtml: "" + + "
" + + "
" + + "" + + "{Link}" + + "
" + + "" + + "" + + "
" + + "
" + + "" + + "" + + "
" + + "
" + + "" + + "" + + "
" + + "
" + + "" + + "" + + "
" + + "
" + + "
" + + "", + + dialogImageHtml: "" + + "
" + + "
" + + "" + + "{Image}" + + "
" + + "" + + "" + + "
" + + "
" + + "" + + "" + + "
" + + "
" + + "" + + "" + + "
" + + "
" + + "" + + "" + + "
" + + "
" + + "
" + + "", + + dialogTableHtml: "" + + "
" + + "
" + + "" + + "{Table}" + + "
" + + "" + + "" + + "
" + + "
" + + "" + + "" + + "
" + + "
" + + "" + + "" + + "
" + + "
" + + "" + + "" + + "
" + + "
" + + "" + + "" + + "
" + + "
" + + "
" + + "", + + dialogPasteHtml: "" + + "
" + + "" + + "
" + + "{Paste_From_Word}" + + "
" + + "" + + "
" + + "
" + + "" + + "" + + "
" + + "
" + + "
" + + "", + + dialogPreviewHtml: "", + + dialogStyles: [], + + stringDelimiterLeft: "{", + stringDelimiterRight:"}", + + preInit: null, + preBind: null, + postInit: null, + + preInitDialog: null, + postInitDialog: null + + }, options); + + return this.each(function() { + + new WYMeditor.editor(jQuery(this),options); + }); +}; + +/* @name extend + * @description Returns the WYMeditor instance based on its index + */ +jQuery.extend({ + wymeditors: function(i) { + return (WYMeditor.INSTANCES[i]); + } +}); + + +/********** WYMeditor **********/ + +/* @name Wymeditor + * @description WYMeditor class + */ + +/* @name init + * @description Initializes a WYMeditor instance + */ +WYMeditor.editor.prototype.init = function() { + + //load subclass - browser specific + //unsupported browsers: do nothing + if (jQuery.browser.msie) { + var WymClass = new WYMeditor.WymClassExplorer(this); + } + else if (jQuery.browser.mozilla) { + var WymClass = new WYMeditor.WymClassMozilla(this); + } + else if (jQuery.browser.opera) { + var WymClass = new WYMeditor.WymClassOpera(this); + } + else if (jQuery.browser.safari) { + var WymClass = new WYMeditor.WymClassSafari(this); + } + + if(WymClass) { + + if(jQuery.isFunction(this._options.preInit)) this._options.preInit(this); + + var SaxListener = new WYMeditor.XhtmlSaxListener(); + jQuery.extend(SaxListener, WymClass); + this.parser = new WYMeditor.XhtmlParser(SaxListener); + + if(this._options.styles || this._options.stylesheet){ + this.configureEditorUsingRawCss(); + } + + this.helper = new WYMeditor.XmlHelper(); + + //extend the Wymeditor object + //don't use jQuery.extend since 1.1.4 + //jQuery.extend(this, WymClass); + for (var prop in WymClass) { this[prop] = WymClass[prop]; } + + //load wymbox + this._box = jQuery(this._element).hide().after(this._options.boxHtml).next().addClass('wym_box_' + this._index); + + //store the instance index in wymbox and element replaced by editor instance + //but keep it compatible with jQuery < 1.2.3, see #122 + if( jQuery.isFunction( jQuery.fn.data ) ) { + jQuery.data(this._box.get(0), WYMeditor.WYM_INDEX, this._index); + jQuery.data(this._element.get(0), WYMeditor.WYM_INDEX, this._index); + } + + var h = WYMeditor.Helper; + + //construct the iframe + var iframeHtml = this._options.iframeHtml; + iframeHtml = h.replaceAll(iframeHtml, WYMeditor.INDEX, this._index); + iframeHtml = h.replaceAll(iframeHtml, WYMeditor.IFRAME_BASE_PATH, this._options.iframeBasePath); + + //construct wymbox + var boxHtml = jQuery(this._box).html(); + + boxHtml = h.replaceAll(boxHtml, WYMeditor.LOGO, this._options.logoHtml); + boxHtml = h.replaceAll(boxHtml, WYMeditor.TOOLS, this._options.toolsHtml); + boxHtml = h.replaceAll(boxHtml, WYMeditor.CONTAINERS,this._options.containersHtml); + boxHtml = h.replaceAll(boxHtml, WYMeditor.CLASSES, this._options.classesHtml); + boxHtml = h.replaceAll(boxHtml, WYMeditor.HTML, this._options.htmlHtml); + boxHtml = h.replaceAll(boxHtml, WYMeditor.IFRAME, iframeHtml); + boxHtml = h.replaceAll(boxHtml, WYMeditor.STATUS, this._options.statusHtml); + + //construct tools list + var aTools = eval(this._options.toolsItems); + var sTools = ""; + + for(var i = 0; i < aTools.length; i++) { + var oTool = aTools[i]; + if(oTool.name && oTool.title) + var sTool = this._options.toolsItemHtml; + var sTool = h.replaceAll(sTool, WYMeditor.TOOL_NAME, oTool.name); + sTool = h.replaceAll(sTool, WYMeditor.TOOL_TITLE, this._options.stringDelimiterLeft + + oTool.title + + this._options.stringDelimiterRight); + sTool = h.replaceAll(sTool, WYMeditor.TOOL_CLASS, oTool.css); + sTools += sTool; + } + + boxHtml = h.replaceAll(boxHtml, WYMeditor.TOOLS_ITEMS, sTools); + + //construct classes list + var aClasses = eval(this._options.classesItems); + var sClasses = ""; + + for(var i = 0; i < aClasses.length; i++) { + var oClass = aClasses[i]; + if(oClass.name && oClass.title) + var sClass = this._options.classesItemHtml; + sClass = h.replaceAll(sClass, WYMeditor.CLASS_NAME, oClass.name); + sClass = h.replaceAll(sClass, WYMeditor.CLASS_TITLE, oClass.title); + sClasses += sClass; + } + + boxHtml = h.replaceAll(boxHtml, WYMeditor.CLASSES_ITEMS, sClasses); + + //construct containers list + var aContainers = eval(this._options.containersItems); + var sContainers = ""; + + for(var i = 0; i < aContainers.length; i++) { + var oContainer = aContainers[i]; + if(oContainer.name && oContainer.title) + var sContainer = this._options.containersItemHtml; + sContainer = h.replaceAll(sContainer, WYMeditor.CONTAINER_NAME, oContainer.name); + sContainer = h.replaceAll(sContainer, WYMeditor.CONTAINER_TITLE, + this._options.stringDelimiterLeft + + oContainer.title + + this._options.stringDelimiterRight); + sContainer = h.replaceAll(sContainer, WYMeditor.CONTAINER_CLASS, oContainer.css); + sContainers += sContainer; + } + + boxHtml = h.replaceAll(boxHtml, WYMeditor.CONTAINERS_ITEMS, sContainers); + + //l10n + boxHtml = this.replaceStrings(boxHtml); + + //load html in wymbox + jQuery(this._box).html(boxHtml); + + //hide the html value + jQuery(this._box).find(this._options.htmlSelector).hide(); + + //enable the skin + this.loadSkin(); + + } +}; + +WYMeditor.editor.prototype.bindEvents = function() { + + //copy the instance + var wym = this; + + //handle click event on tools buttons + jQuery(this._box).find(this._options.toolSelector).click(function() { + wym._iframe.contentWindow.focus(); //See #154 + wym.exec(jQuery(this).attr(WYMeditor.NAME)); + return(false); + }); + + //handle click event on containers buttons + jQuery(this._box).find(this._options.containerSelector).click(function() { + wym.container(jQuery(this).attr(WYMeditor.NAME)); + return(false); + }); + + //handle keyup event on html value: set the editor value + //handle focus/blur events to check if the element has focus, see #147 + jQuery(this._box).find(this._options.htmlValSelector) + .keyup(function() { jQuery(wym._doc.body).html(jQuery(this).val());}) + .focus(function() { jQuery(this).toggleClass('hasfocus'); }) + .blur(function() { jQuery(this).toggleClass('hasfocus'); }); + + //handle click event on classes buttons + jQuery(this._box).find(this._options.classSelector).click(function() { + + var aClasses = eval(wym._options.classesItems); + var sName = jQuery(this).attr(WYMeditor.NAME); + + var oClass = WYMeditor.Helper.findByName(aClasses, sName); + + if(oClass) { + var jqexpr = oClass.expr; + wym.toggleClass(sName, jqexpr); + } + wym._iframe.contentWindow.focus(); //See #154 + return(false); + }); + + //handle event on update element + jQuery(this._options.updateSelector) + .bind(this._options.updateEvent, function() { + wym.update(); + }); +}; + +WYMeditor.editor.prototype.ready = function() { + return(this._doc != null); +}; + + +/********** METHODS **********/ + +/* @name box + * @description Returns the WYMeditor container + */ +WYMeditor.editor.prototype.box = function() { + return(this._box); +}; + +/* @name html + * @description Get/Set the html value + */ +WYMeditor.editor.prototype.html = function(html) { + + if(typeof html === 'string') jQuery(this._doc.body).html(html); + else return(jQuery(this._doc.body).html()); +}; + +/* @name xhtml + * @description Cleans up the HTML + */ +WYMeditor.editor.prototype.xhtml = function() { + return this.parser.parse(this.html()); +}; + +/* @name exec + * @description Executes a button command + */ +WYMeditor.editor.prototype.exec = function(cmd) { + + //base function for execCommand + //open a dialog or exec + switch(cmd) { + case WYMeditor.CREATE_LINK: + var container = this.container(); + if(container || this._selected_image) this.dialog(WYMeditor.DIALOG_LINK); + break; + + case WYMeditor.INSERT_IMAGE: + this.dialog(WYMeditor.DIALOG_IMAGE); + break; + + case WYMeditor.INSERT_TABLE: + this.dialog(WYMeditor.DIALOG_TABLE); + break; + + case WYMeditor.PASTE: + this.dialog(WYMeditor.DIALOG_PASTE); + break; + + case WYMeditor.TOGGLE_HTML: + this.update(); + this.toggleHtml(); + break; + + case WYMeditor.PREVIEW: + this.dialog(WYMeditor.PREVIEW, this._options.dialogFeaturesPreview); + break; + + default: + this._exec(cmd); + break; + } +}; + +/* @name container + * @description Get/Set the selected container + */ +WYMeditor.editor.prototype.container = function(sType) { + + if(sType) { + + var container = null; + + if(sType.toLowerCase() == WYMeditor.TH) { + + container = this.container(); + + //find the TD or TH container + switch(container.tagName.toLowerCase()) { + + case WYMeditor.TD: case WYMeditor.TH: + break; + default: + var aTypes = new Array(WYMeditor.TD,WYMeditor.TH); + container = this.findUp(this.container(), aTypes); + break; + } + + //if it exists, switch + if(container!=null) { + + sType = (container.tagName.toLowerCase() == WYMeditor.TD)? WYMeditor.TH: WYMeditor.TD; + this.switchTo(container,sType); + this.update(); + } + } else { + + //set the container type + var aTypes=new Array(WYMeditor.P,WYMeditor.H1,WYMeditor.H2,WYMeditor.H3,WYMeditor.H4,WYMeditor.H5, + WYMeditor.H6,WYMeditor.PRE,WYMeditor.BLOCKQUOTE); + container = this.findUp(this.container(), aTypes); + + if(container) { + + var newNode = null; + + //blockquotes must contain a block level element + if(sType.toLowerCase() == WYMeditor.BLOCKQUOTE) { + + var blockquote = this.findUp(this.container(), WYMeditor.BLOCKQUOTE); + + if(blockquote == null) { + + newNode = this._doc.createElement(sType); + container.parentNode.insertBefore(newNode,container); + newNode.appendChild(container); + this.setFocusToNode(newNode.firstChild); + + } else { + + var nodes = blockquote.childNodes; + var lgt = nodes.length; + var firstNode = null; + + if(lgt > 0) firstNode = nodes.item(0); + for(var x=0; x') ) + + '

'; + } + + // Insert where appropriate + if (container && container.tagName.toLowerCase() != WYMeditor.BODY) { + // No .last() pre jQuery 1.4 + //focusNode = jQuery(html).insertAfter(container).last()[0]; + paragraphs = jQuery(html, this._doc).insertAfter(container); + focusNode = paragraphs[paragraphs.length - 1]; + } else { + paragraphs = jQuery(html, this._doc).appendTo(this._doc.body); + focusNode = paragraphs[paragraphs.length - 1]; + } + + // Do some minor cleanup (#131) + if (jQuery(container).text() == '') { + jQuery(container).remove(); + } + // And remove br (if editor was empty) + jQuery('body > br', this._doc).remove(); + + // Restore focus + this.setFocusToNode(focusNode); +}; + +WYMeditor.editor.prototype.insert = function(html) { + // Do we have a selection? + var selection = this._iframe.contentWindow.getSelection(), + range, + node; + if (selection.focusNode != null) { + // Overwrite selection with provided html + range = selection.getRangeAt(0); + node = range.createContextualFragment(html); + range.deleteContents(); + range.insertNode(node); + } else { + // Fall back to the internal paste function if there's no selection + this.paste(html) + } +}; + +WYMeditor.editor.prototype.wrap = function(left, right) { + this.insert(left + this._iframe.contentWindow.getSelection().toString() + right); +}; + +WYMeditor.editor.prototype.unwrap = function() { + this.insert(this._iframe.contentWindow.getSelection().toString()); +}; + +WYMeditor.editor.prototype.setFocusToNode = function(node, toStart) { + var range = this._doc.createRange(), + selection = this._iframe.contentWindow.getSelection(); + toStart = toStart ? 0 : 1; + + range.selectNodeContents(node); + selection.addRange(range); + selection.collapse(node, toStart); + this._iframe.contentWindow.focus(); +}; + +WYMeditor.editor.prototype.addCssRules = function(doc, aCss) { + var styles = doc.styleSheets[0]; + if(styles) { + for(var i = 0; i < aCss.length; i++) { + var oCss = aCss[i]; + if(oCss.name && oCss.css) this.addCssRule(styles, oCss); + } + } +}; + +/********** CONFIGURATION **********/ + +WYMeditor.editor.prototype.computeBasePath = function() { + return jQuery(jQuery.grep(jQuery('script'), function(s){ + return (s.src && s.src.match(/jquery\.wymeditor(\.pack|\.min|\.packed)?\.js(\?.*)?$/ )) + })).attr('src').replace(/jquery\.wymeditor(\.pack|\.min|\.packed)?\.js(\?.*)?$/, ''); +}; + +WYMeditor.editor.prototype.computeWymPath = function() { + return jQuery(jQuery.grep(jQuery('script'), function(s){ + return (s.src && s.src.match(/jquery\.wymeditor(\.pack|\.min|\.packed)?\.js(\?.*)?$/ )) + })).attr('src'); +}; + +WYMeditor.editor.prototype.computeJqueryPath = function() { + return jQuery(jQuery.grep(jQuery('script'), function(s){ + return (s.src && s.src.match(/jquery(-(.*)){0,1}(\.pack|\.min|\.packed)?\.js(\?.*)?$/ )) + })).attr('src'); +}; + +WYMeditor.editor.prototype.computeCssPath = function() { + return jQuery(jQuery.grep(jQuery('link'), function(s){ + return (s.href && s.href.match(/wymeditor\/skins\/(.*)screen\.css(\?.*)?$/ )) + })).attr('href'); +}; + +WYMeditor.editor.prototype.configureEditorUsingRawCss = function() { + + var CssParser = new WYMeditor.WymCssParser(); + if(this._options.stylesheet){ + CssParser.parse(jQuery.ajax({url: this._options.stylesheet,async:false}).responseText); + }else{ + CssParser.parse(this._options.styles, false); + } + + if(this._options.classesItems.length == 0) { + this._options.classesItems = CssParser.css_settings.classesItems; + } + if(this._options.editorStyles.length == 0) { + this._options.editorStyles = CssParser.css_settings.editorStyles; + } + if(this._options.dialogStyles.length == 0) { + this._options.dialogStyles = CssParser.css_settings.dialogStyles; + } +}; + +/********** EVENTS **********/ + +WYMeditor.editor.prototype.listen = function() { + //don't use jQuery.find() on the iframe body + //because of MSIE + jQuery + expando issue (#JQ1143) + //jQuery(this._doc.body).find("*").bind("mouseup", this.mouseup); + + jQuery(this._doc.body).bind("mousedown", this.mousedown); +}; + +WYMeditor.editor.prototype.mousedown = function(evt) { + var wym = WYMeditor.INSTANCES[this.ownerDocument.title]; + wym._selected_image = (evt.target.tagName.toLowerCase() == WYMeditor.IMG) ? evt.target : null; +}; + +/********** SKINS **********/ + +/* + * Function: WYMeditor.loadCss + * Loads a stylesheet in the document. + * + * Parameters: + * href - The CSS path. + */ +WYMeditor.loadCss = function(href) { + + var link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = href; + + var head = jQuery('head').get(0); + head.appendChild(link); +}; + +/* + * Function: WYMeditor.editor.loadSkin + * Loads the skin CSS and initialization script (if needed). + */ +WYMeditor.editor.prototype.loadSkin = function() { + + //does the user want to automatically load the CSS (default: yes)? + //we also test if it hasn't been already loaded by another instance + //see below for a better (second) test + if(this._options.loadSkin && !WYMeditor.SKINS[this._options.skin]) { + + //check if it hasn't been already loaded + //so we don't load it more than once + //(we check the existing elements) + + var found = false; + var rExp = new RegExp(this._options.skin + + '\/' + WYMeditor.SKINS_DEFAULT_CSS + '$'); + + jQuery('link').each( function() { + if(this.href.match(rExp)) found = true; + }); + + //load it, using the skin path + if(!found) WYMeditor.loadCss( this._options.skinPath + + WYMeditor.SKINS_DEFAULT_CSS ); + } + + //put the classname (ex. wym_skin_default) on wym_box + jQuery(this._box).addClass( "wym_skin_" + this._options.skin ); + + //does the user want to use some JS to initialize the skin (default: yes)? + //also check if it hasn't already been loaded by another instance + if(this._options.initSkin && !WYMeditor.SKINS[this._options.skin]) { + + eval(jQuery.ajax({url:this._options.skinPath + + WYMeditor.SKINS_DEFAULT_JS, async:false}).responseText); + } + + //init the skin, if needed + if(WYMeditor.SKINS[this._options.skin] + && WYMeditor.SKINS[this._options.skin].init) + WYMeditor.SKINS[this._options.skin].init(this); + +}; + + +/********** DIALOGS **********/ + +WYMeditor.INIT_DIALOG = function(index) { + + var wym = window.opener.WYMeditor.INSTANCES[index]; + var doc = window.document; + var selected = wym.selected(); + var dialogType = jQuery(wym._options.dialogTypeSelector).val(); + var sStamp = wym.uniqueStamp(); + + switch(dialogType) { + + case WYMeditor.DIALOG_LINK: + //ensure that we select the link to populate the fields + if(selected && selected.tagName && selected.tagName.toLowerCase != WYMeditor.A) + selected = jQuery(selected).parentsOrSelf(WYMeditor.A); + + //fix MSIE selection if link image has been clicked + if(!selected && wym._selected_image) + selected = jQuery(wym._selected_image).parentsOrSelf(WYMeditor.A); + break; + + } + + //pre-init functions + if(jQuery.isFunction(wym._options.preInitDialog)) + wym._options.preInitDialog(wym,window); + + //add css rules from options + var styles = doc.styleSheets[0]; + var aCss = eval(wym._options.dialogStyles); + + wym.addCssRules(doc, aCss); + + //auto populate fields if selected container (e.g. A) + if(selected) { + jQuery(wym._options.hrefSelector).val(jQuery(selected).attr(WYMeditor.HREF)); + jQuery(wym._options.srcSelector).val(jQuery(selected).attr(WYMeditor.SRC)); + jQuery(wym._options.titleSelector).val(jQuery(selected).attr(WYMeditor.TITLE)); + jQuery(wym._options.relSelector).val(jQuery(selected).attr(WYMeditor.REL)); + jQuery(wym._options.altSelector).val(jQuery(selected).attr(WYMeditor.ALT)); + } + + //auto populate image fields if selected image + if(wym._selected_image) { + jQuery(wym._options.dialogImageSelector + " " + wym._options.srcSelector) + .val(jQuery(wym._selected_image).attr(WYMeditor.SRC)); + jQuery(wym._options.dialogImageSelector + " " + wym._options.titleSelector) + .val(jQuery(wym._selected_image).attr(WYMeditor.TITLE)); + jQuery(wym._options.dialogImageSelector + " " + wym._options.altSelector) + .val(jQuery(wym._selected_image).attr(WYMeditor.ALT)); + } + + jQuery(wym._options.dialogLinkSelector + " " + + wym._options.submitSelector).submit(function() { + + var sUrl = jQuery(wym._options.hrefSelector).val(); + if(sUrl.length > 0) { + var link; + + if (selected[0] && selected[0].tagName.toLowerCase() == WYMeditor.A) { + link = selected; + } else { + wym._exec(WYMeditor.CREATE_LINK, sStamp); + link = jQuery("a[href=" + sStamp + "]", wym._doc.body); + } + + link.attr(WYMeditor.HREF, sUrl) + .attr(WYMeditor.TITLE, jQuery(wym._options.titleSelector).val()) + .attr(WYMeditor.REL, jQuery(wym._options.relSelector).val()); + + } + window.close(); + }); + + jQuery(wym._options.dialogImageSelector + " " + + wym._options.submitSelector).submit(function() { + + var sUrl = jQuery(wym._options.srcSelector).val(); + if(sUrl.length > 0) { + + wym._exec(WYMeditor.INSERT_IMAGE, sStamp); + + jQuery("img[src$=" + sStamp + "]", wym._doc.body) + .attr(WYMeditor.SRC, sUrl) + .attr(WYMeditor.TITLE, jQuery(wym._options.titleSelector).val()) + .attr(WYMeditor.ALT, jQuery(wym._options.altSelector).val()); + } + window.close(); + }); + + jQuery(wym._options.dialogTableSelector + " " + + wym._options.submitSelector).submit(function() { + + var iRows = jQuery(wym._options.rowsSelector).val(); + var iCols = jQuery(wym._options.colsSelector).val(); + + if(iRows > 0 && iCols > 0) { + + var table = wym._doc.createElement(WYMeditor.TABLE); + var newRow = null; + var newCol = null; + + var sCaption = jQuery(wym._options.captionSelector).val(); + + //we create the caption + var newCaption = table.createCaption(); + newCaption.innerHTML = sCaption; + + //we create the rows and cells + for(x=0; x
+* this.tag ('br', false, true) +* # =>
+* this.tag ('input', jQuery({type:'text',disabled:true }) ) +* # => +*/ +WYMeditor.XmlHelper.prototype.tag = function(name, options, open) +{ + options = options || false; + open = open || false; + return '<'+name+(options ? this.tagOptions(options) : '')+(open ? '>' : ' />'); +}; + +/* +* @name contentTag +* @description +* Returns a XML block tag of type *name* surrounding the *content*. Add +* XML attributes by passing an attributes array to *options*. For attributes +* with no value like (disabled and readonly), give it a value of true in +* the *options* array. You can use symbols or strings for the attribute names. +* +* this.contentTag ('p', 'Hello world!' ) +* # =>

Hello world!

+* this.contentTag('div', this.contentTag('p', "Hello world!"), jQuery({class : "strong"})) +* # =>

Hello world!

+* this.contentTag("select", options, jQuery({multiple : true})) +* # => +*/ +WYMeditor.XmlHelper.prototype.contentTag = function(name, content, options) +{ + options = options || false; + return '<'+name+(options ? this.tagOptions(options) : '')+'>'+content+''; +}; + +/* +* @name cdataSection +* @description +* Returns a CDATA section for the given +content+. CDATA sections +* are used to escape blocks of text containing characters which would +* otherwise be recognized as markup. CDATA sections begin with the string +* <![CDATA[ and } with (and may not contain) the string +* ]]>. +*/ +WYMeditor.XmlHelper.prototype.cdataSection = function(content) +{ + return ''; +}; + + +/* +* @name escapeOnce +* @description +* Returns the escaped +xml+ without affecting existing escaped entities. +* +* this.escapeOnce( "1 > 2 & 3") +* # => "1 > 2 & 3" +*/ +WYMeditor.XmlHelper.prototype.escapeOnce = function(xml) +{ + return this._fixDoubleEscape(this.escapeEntities(xml)); +}; + +/* +* @name _fixDoubleEscape +* @description +* Fix double-escaped entities, such as &amp;, &#123;, etc. +*/ +WYMeditor.XmlHelper.prototype._fixDoubleEscape = function(escaped) +{ + return escaped.replace(/&([a-z]+|(#\d+));/ig, "&$1;"); +}; + +/* +* @name tagOptions +* @description +* Takes an array like the one generated by Tag.parseAttributes +* [["src", "http://www.editam.com/?a=b&c=d&f=g"], ["title", "Editam, CMS"]] +* or an object like {src:"http://www.editam.com/?a=b&c=d&f=g", title:"Editam, CMS"} +* and returns a string properly escaped like +* ' src = "http://www.editam.com/?a=b&c=d&f=g" title = "Editam, <Simplified> CMS"' +* which is valid for strict XHTML +*/ +WYMeditor.XmlHelper.prototype.tagOptions = function(options) +{ + var xml = this; + xml._formated_options = ''; + + for (var key in options) { + var formated_options = ''; + var value = options[key]; + if(typeof value != 'function' && value.length > 0) { + + if(parseInt(key) == key && typeof value == 'object'){ + key = value.shift(); + value = value.pop(); + } + if(key != '' && value != ''){ + xml._formated_options += ' '+key+'="'+xml.escapeOnce(value)+'"'; + } + } + } + return xml._formated_options; +}; + +/* +* @name escapeEntities +* @description +* Escapes XML/HTML entities <, >, & and ". If seccond parameter is set to false it +* will not escape ". If set to true it will also escape ' +*/ +WYMeditor.XmlHelper.prototype.escapeEntities = function(string, escape_quotes) +{ + this._entitiesDiv.innerHTML = string; + this._entitiesDiv.textContent = string; + var result = this._entitiesDiv.innerHTML; + if(typeof escape_quotes == 'undefined'){ + if(escape_quotes != false) result = result.replace('"', '"'); + if(escape_quotes == true) result = result.replace('"', '''); + } + return result; +}; + +/* +* Parses a string conatining tag attributes and values an returns an array formated like +* [["src", "http://www.editam.com"], ["title", "Editam, Simplified CMS"]] +*/ +WYMeditor.XmlHelper.prototype.parseAttributes = function(tag_attributes) +{ + // Use a compounded regex to match single quoted, double quoted and unquoted attribute pairs + var result = []; + var matches = tag_attributes.split(/((=\s*")(")("))|((=\s*\')(\')(\'))|((=\s*[^>\s]*))/g); + if(matches.toString() != tag_attributes){ + for (var k in matches) { + var v = matches[k]; + if(typeof v != 'function' && v.length != 0){ + var re = new RegExp('(\\w+)\\s*'+v); + if(match = tag_attributes.match(re) ){ + var value = v.replace(/^[\s=]+/, ""); + var delimiter = value.charAt(0); + delimiter = delimiter == '"' ? '"' : (delimiter=="'"?"'":''); + if(delimiter != ''){ + value = delimiter == '"' ? value.replace(/^"|"+$/g, '') : value.replace(/^'|'+$/g, ''); + } + tag_attributes = tag_attributes.replace(match[0],''); + result.push([match[1] , value]); + } + } + } + } + return result; +}; + +/** +* XhtmlValidator for validating tag attributes +* +* @author Bermi Ferrer - http://bermi.org +*/ +WYMeditor.XhtmlValidator = { + "_attributes": + { + "core": + { + "except":[ + "base", + "head", + "html", + "meta", + "param", + "script", + "style", + "title" + ], + "attributes":[ + "class", + "id", + "style", + "title", + "accesskey", + "tabindex" + ] + }, + "language": + { + "except":[ + "base", + "br", + "hr", + "iframe", + "param", + "script" + ], + "attributes": + { + "dir":[ + "ltr", + "rtl" + ], + "0":"lang", + "1":"xml:lang" + } + }, + "keyboard": + { + "attributes": + { + "accesskey":/^(\w){1}$/, + "tabindex":/^(\d)+$/ + } + } + }, + "_events": + { + "window": + { + "only":[ + "body" + ], + "attributes":[ + "onload", + "onunload" + ] + }, + "form": + { + "only":[ + "form", + "input", + "textarea", + "select", + "a", + "label", + "button" + ], + "attributes":[ + "onchange", + "onsubmit", + "onreset", + "onselect", + "onblur", + "onfocus" + ] + }, + "keyboard": + { + "except":[ + "base", + "bdo", + "br", + "frame", + "frameset", + "head", + "html", + "iframe", + "meta", + "param", + "script", + "style", + "title" + ], + "attributes":[ + "onkeydown", + "onkeypress", + "onkeyup" + ] + }, + "mouse": + { + "except":[ + "base", + "bdo", + "br", + "head", + "html", + "meta", + "param", + "script", + "style", + "title" + ], + "attributes":[ + "onclick", + "ondblclick", + "onmousedown", + "onmousemove", + "onmouseover", + "onmouseout", + "onmouseup" + ] + } + }, + "_tags": + { + "a": + { + "attributes": + { + "0":"charset", + "1":"coords", + "2":"href", + "3":"hreflang", + "4":"name", + "5":"rel", + "6":"rev", + "shape":/^(rect|rectangle|circ|circle|poly|polygon)$/, + "7":"type" + } + }, + "0":"abbr", + "1":"acronym", + "2":"address", + "area": + { + "attributes": + { + "0":"alt", + "1":"coords", + "2":"href", + "nohref":/^(true|false)$/, + "shape":/^(rect|rectangle|circ|circle|poly|polygon)$/ + }, + "required":[ + "alt" + ] + }, + "3":"b", + "base": + { + "attributes":[ + "href" + ], + "required":[ + "href" + ] + }, + "bdo": + { + "attributes": + { + "dir":/^(ltr|rtl)$/ + }, + "required":[ + "dir" + ] + }, + "4":"big", + "blockquote": + { + "attributes":[ + "cite" + ] + }, + "5":"body", + "6":"br", + "button": + { + "attributes": + { + "disabled":/^(disabled)$/, + "type":/^(button|reset|submit)$/, + "0":"value" + }, + "inside":"form" + }, + "7":"caption", + "8":"cite", + "9":"code", + "col": + { + "attributes": + { + "align":/^(right|left|center|justify)$/, + "0":"char", + "1":"charoff", + "span":/^(\d)+$/, + "valign":/^(top|middle|bottom|baseline)$/, + "2":"width" + }, + "inside":"colgroup" + }, + "colgroup": + { + "attributes": + { + "align":/^(right|left|center|justify)$/, + "0":"char", + "1":"charoff", + "span":/^(\d)+$/, + "valign":/^(top|middle|bottom|baseline)$/, + "2":"width" + } + }, + "10":"dd", + "del": + { + "attributes": + { + "0":"cite", + "datetime":/^([0-9]){8}/ + } + }, + "11":"div", + "12":"dfn", + "13":"dl", + "14":"dt", + "15":"em", + "fieldset": + { + "inside":"form" + }, + "form": + { + "attributes": + { + "0":"action", + "1":"accept", + "2":"accept-charset", + "3":"enctype", + "method":/^(get|post)$/ + }, + "required":[ + "action" + ] + }, + "head": + { + "attributes":[ + "profile" + ] + }, + "16":"h1", + "17":"h2", + "18":"h3", + "19":"h4", + "20":"h5", + "21":"h6", + "22":"hr", + "html": + { + "attributes":[ + "xmlns" + ] + }, + "23":"i", + "img": + { + "attributes":[ + "alt", + "src", + "height", + "ismap", + "longdesc", + "usemap", + "width" + ], + "required":[ + "alt", + "src" + ] + }, + "input": + { + "attributes": + { + "0":"accept", + "1":"alt", + "checked":/^(checked)$/, + "disabled":/^(disabled)$/, + "maxlength":/^(\d)+$/, + "2":"name", + "readonly":/^(readonly)$/, + "size":/^(\d)+$/, + "3":"src", + "type":/^(button|checkbox|file|hidden|image|password|radio|reset|submit|text)$/, + "4":"value" + }, + "inside":"form" + }, + "ins": + { + "attributes": + { + "0":"cite", + "datetime":/^([0-9]){8}/ + } + }, + "24":"kbd", + "label": + { + "attributes":[ + "for" + ], + "inside":"form" + }, + "25":"legend", + "26":"li", + "link": + { + "attributes": + { + "0":"charset", + "1":"href", + "2":"hreflang", + "media":/^(all|braille|print|projection|screen|speech|,|;| )+$/i, + //next comment line required by Opera! + /*"rel":/^(alternate|appendix|bookmark|chapter|contents|copyright|glossary|help|home|index|next|prev|section|start|stylesheet|subsection| |shortcut|icon)+$/i,*/ + "rel":/^(alternate|appendix|bookmark|chapter|contents|copyright|glossary|help|home|index|next|prev|section|start|stylesheet|subsection| |shortcut|icon)+$/i, + "rev":/^(alternate|appendix|bookmark|chapter|contents|copyright|glossary|help|home|index|next|prev|section|start|stylesheet|subsection| |shortcut|icon)+$/i, + "3":"type" + }, + "inside":"head" + }, + "map": + { + "attributes":[ + "id", + "name" + ], + "required":[ + "id" + ] + }, + "meta": + { + "attributes": + { + "0":"content", + "http-equiv":/^(content\-type|expires|refresh|set\-cookie)$/i, + "1":"name", + "2":"scheme" + }, + "required":[ + "content" + ] + }, + "27":"noscript", + "object": + { + "attributes":[ + "archive", + "classid", + "codebase", + "codetype", + "data", + "declare", + "height", + "name", + "standby", + "type", + "usemap", + "width" + ] + }, + "28":"ol", + "optgroup": + { + "attributes": + { + "0":"label", + "disabled": /^(disabled)$/ + }, + "required":[ + "label" + ] + }, + "option": + { + "attributes": + { + "0":"label", + "disabled":/^(disabled)$/, + "selected":/^(selected)$/, + "1":"value" + }, + "inside":"select" + }, + "29":"p", + "param": + { + "attributes": + { + "0":"type", + "valuetype":/^(data|ref|object)$/, + "1":"valuetype", + "2":"value" + }, + "required":[ + "name" + ] + }, + "30":"pre", + "q": + { + "attributes":[ + "cite" + ] + }, + "31":"samp", + "script": + { + "attributes": + { + "type":/^(text\/ecmascript|text\/javascript|text\/jscript|text\/vbscript|text\/vbs|text\/xml)$/, + "0":"charset", + "defer":/^(defer)$/, + "1":"src" + }, + "required":[ + "type" + ] + }, + "select": + { + "attributes": + { + "disabled":/^(disabled)$/, + "multiple":/^(multiple)$/, + "0":"name", + "1":"size" + }, + "inside":"form" + }, + "32":"small", + "33":"span", + "34":"strong", + "style": + { + "attributes": + { + "0":"type", + "media":/^(screen|tty|tv|projection|handheld|print|braille|aural|all)$/ + }, + "required":[ + "type" + ] + }, + "35":"sub", + "36":"sup", + "table": + { + "attributes": + { + "0":"border", + "1":"cellpadding", + "2":"cellspacing", + "frame":/^(void|above|below|hsides|lhs|rhs|vsides|box|border)$/, + "rules":/^(none|groups|rows|cols|all)$/, + "3":"summary", + "4":"width" + } + }, + "tbody": + { + "attributes": + { + "align":/^(right|left|center|justify)$/, + "0":"char", + "1":"charoff", + "valign":/^(top|middle|bottom|baseline)$/ + } + }, + "td": + { + "attributes": + { + "0":"abbr", + "align":/^(left|right|center|justify|char)$/, + "1":"axis", + "2":"char", + "3":"charoff", + "colspan":/^(\d)+$/, + "4":"headers", + "rowspan":/^(\d)+$/, + "scope":/^(col|colgroup|row|rowgroup)$/, + "valign":/^(top|middle|bottom|baseline)$/ + } + }, + "textarea": + { + "attributes":[ + "cols", + "rows", + "disabled", + "name", + "readonly" + ], + "required":[ + "cols", + "rows" + ], + "inside":"form" + }, + "tfoot": + { + "attributes": + { + "align":/^(right|left|center|justify)$/, + "0":"char", + "1":"charoff", + "valign":/^(top|middle|bottom)$/, + "2":"baseline" + } + }, + "th": + { + "attributes": + { + "0":"abbr", + "align":/^(left|right|center|justify|char)$/, + "1":"axis", + "2":"char", + "3":"charoff", + "colspan":/^(\d)+$/, + "4":"headers", + "rowspan":/^(\d)+$/, + "scope":/^(col|colgroup|row|rowgroup)$/, + "valign":/^(top|middle|bottom|baseline)$/ + } + }, + "thead": + { + "attributes": + { + "align":/^(right|left|center|justify)$/, + "0":"char", + "1":"charoff", + "valign":/^(top|middle|bottom|baseline)$/ + } + }, + "37":"title", + "tr": + { + "attributes": + { + "align":/^(right|left|center|justify|char)$/, + "0":"char", + "1":"charoff", + "valign":/^(top|middle|bottom|baseline)$/ + } + }, + "38":"tt", + "39":"ul", + "40":"var" + }, + + // Temporary skiped attributes + skiped_attributes : [], + skiped_attribute_values : [], + + getValidTagAttributes: function(tag, attributes) + { + var valid_attributes = {}; + var possible_attributes = this.getPossibleTagAttributes(tag); + for(var attribute in attributes) { + var value = attributes[attribute]; + var h = WYMeditor.Helper; + if(!h.contains(this.skiped_attributes, attribute) && !h.contains(this.skiped_attribute_values, value)){ + if (typeof value != 'function' && h.contains(possible_attributes, attribute)) { + if (this.doesAttributeNeedsValidation(tag, attribute)) { + if(this.validateAttribute(tag, attribute, value)){ + valid_attributes[attribute] = value; + } + }else{ + valid_attributes[attribute] = value; + } + } + } + } + return valid_attributes; + }, + getUniqueAttributesAndEventsForTag : function(tag) + { + var result = []; + + if (this._tags[tag] && this._tags[tag]['attributes']) { + for (k in this._tags[tag]['attributes']) { + result.push(parseInt(k) == k ? this._tags[tag]['attributes'][k] : k); + } + } + return result; + }, + getDefaultAttributesAndEventsForTags : function() + { + var result = []; + for (var key in this._events){ + result.push(this._events[key]); + } + for (var key in this._attributes){ + result.push(this._attributes[key]); + } + return result; + }, + isValidTag : function(tag) + { + if(this._tags[tag]){ + return true; + } + for(var key in this._tags){ + if(this._tags[key] == tag){ + return true; + } + } + return false; + }, + getDefaultAttributesAndEventsForTag : function(tag) + { + var default_attributes = []; + if (this.isValidTag(tag)) { + var default_attributes_and_events = this.getDefaultAttributesAndEventsForTags(); + + for(var key in default_attributes_and_events) { + var defaults = default_attributes_and_events[key]; + if(typeof defaults == 'object'){ + var h = WYMeditor.Helper; + if ((defaults['except'] && h.contains(defaults['except'], tag)) || (defaults['only'] && !h.contains(defaults['only'], tag))) { + continue; + } + + var tag_defaults = defaults['attributes'] ? defaults['attributes'] : defaults['events']; + for(var k in tag_defaults) { + default_attributes.push(typeof tag_defaults[k] != 'string' ? k : tag_defaults[k]); + } + } + } + } + return default_attributes; + }, + doesAttributeNeedsValidation: function(tag, attribute) + { + return this._tags[tag] && ((this._tags[tag]['attributes'] && this._tags[tag]['attributes'][attribute]) || (this._tags[tag]['required'] && + WYMeditor.Helper.contains(this._tags[tag]['required'], attribute))); + }, + validateAttribute : function(tag, attribute, value) + { + if ( this._tags[tag] && + (this._tags[tag]['attributes'] && this._tags[tag]['attributes'][attribute] && value.length > 0 && !value.match(this._tags[tag]['attributes'][attribute])) || // invalid format + (this._tags[tag] && this._tags[tag]['required'] && WYMeditor.Helper.contains(this._tags[tag]['required'], attribute) && value.length == 0) // required attribute + ) { + return false; + } + return typeof this._tags[tag] != 'undefined'; + }, + getPossibleTagAttributes : function(tag) + { + if (!this._possible_tag_attributes) { + this._possible_tag_attributes = {}; + } + if (!this._possible_tag_attributes[tag]) { + this._possible_tag_attributes[tag] = this.getUniqueAttributesAndEventsForTag(tag).concat(this.getDefaultAttributesAndEventsForTag(tag)); + } + return this._possible_tag_attributes[tag]; + } +}; + + +/** +* Compounded regular expression. Any of +* the contained patterns could match and +* when one does, it's label is returned. +* +* Constructor. Starts with no patterns. +* @param boolean case True for case sensitive, false +* for insensitive. +* @access public +* @author Marcus Baker (http://lastcraft.com) +* @author Bermi Ferrer (http://bermi.org) +*/ +WYMeditor.ParallelRegex = function(case_sensitive) +{ + this._case = case_sensitive; + this._patterns = []; + this._labels = []; + this._regex = null; + return this; +}; + + +/** +* Adds a pattern with an optional label. +* @param string pattern Perl style regex, but ( and ) +* lose the usual meaning. +* @param string label Label of regex to be returned +* on a match. +* @access public +*/ +WYMeditor.ParallelRegex.prototype.addPattern = function(pattern, label) +{ + label = label || true; + var count = this._patterns.length; + this._patterns[count] = pattern; + this._labels[count] = label; + this._regex = null; +}; + +/** +* Attempts to match all patterns at once against +* a string. +* @param string subject String to match against. +* +* @return boolean True on success. +* @return string match First matched portion of +* subject. +* @access public +*/ +WYMeditor.ParallelRegex.prototype.match = function(subject) +{ + if (this._patterns.length == 0) { + return [false, '']; + } + var matches = subject.match(this._getCompoundedRegex()); + + if(!matches){ + return [false, '']; + } + var match = matches[0]; + for (var i = 1; i < matches.length; i++) { + if (matches[i]) { + return [this._labels[i-1], match]; + } + } + return [true, matches[0]]; +}; + +/** +* Compounds the patterns into a single +* regular expression separated with the +* "or" operator. Caches the regex. +* Will automatically escape (, ) and / tokens. +* @param array patterns List of patterns in order. +* @access private +*/ +WYMeditor.ParallelRegex.prototype._getCompoundedRegex = function() +{ + if (this._regex == null) { + for (var i = 0, count = this._patterns.length; i < count; i++) { + this._patterns[i] = '(' + this._untokenizeRegex(this._tokenizeRegex(this._patterns[i]).replace(/([\/\(\)])/g,'\\$1')) + ')'; + } + this._regex = new RegExp(this._patterns.join("|") ,this._getPerlMatchingFlags()); + } + return this._regex; +}; + +/** +* Escape lookahead/lookbehind blocks +*/ +WYMeditor.ParallelRegex.prototype._tokenizeRegex = function(regex) +{ + return regex. + replace(/\(\?(i|m|s|x|U)\)/, '~~~~~~Tk1\$1~~~~~~'). + replace(/\(\?(\-[i|m|s|x|U])\)/, '~~~~~~Tk2\$1~~~~~~'). + replace(/\(\?\=(.*)\)/, '~~~~~~Tk3\$1~~~~~~'). + replace(/\(\?\!(.*)\)/, '~~~~~~Tk4\$1~~~~~~'). + replace(/\(\?\<\=(.*)\)/, '~~~~~~Tk5\$1~~~~~~'). + replace(/\(\?\<\!(.*)\)/, '~~~~~~Tk6\$1~~~~~~'). + replace(/\(\?\:(.*)\)/, '~~~~~~Tk7\$1~~~~~~'); +}; + +/** +* Unscape lookahead/lookbehind blocks +*/ +WYMeditor.ParallelRegex.prototype._untokenizeRegex = function(regex) +{ + return regex. + replace(/~~~~~~Tk1(.{1})~~~~~~/, "(?\$1)"). + replace(/~~~~~~Tk2(.{2})~~~~~~/, "(?\$1)"). + replace(/~~~~~~Tk3(.*)~~~~~~/, "(?=\$1)"). + replace(/~~~~~~Tk4(.*)~~~~~~/, "(?!\$1)"). + replace(/~~~~~~Tk5(.*)~~~~~~/, "(?<=\$1)"). + replace(/~~~~~~Tk6(.*)~~~~~~/, "(?", 'Comment'); +}; + +WYMeditor.XhtmlLexer.prototype.addScriptTokens = function(scope) +{ + this.addEntryPattern("", 'Script'); +}; + +WYMeditor.XhtmlLexer.prototype.addCssTokens = function(scope) +{ + this.addEntryPattern("", 'Css'); +}; + +WYMeditor.XhtmlLexer.prototype.addTagTokens = function(scope) +{ + this.addSpecialPattern("<\\s*[a-z0-9:\-]+\\s*>", scope, 'OpeningTag'); + this.addEntryPattern("<[a-z0-9:\-]+"+'[\\\/ \\\>]+', scope, 'OpeningTag'); + this.addInTagDeclarationTokens('OpeningTag'); + + this.addSpecialPattern("", scope, 'ClosingTag'); + +}; + +WYMeditor.XhtmlLexer.prototype.addInTagDeclarationTokens = function(scope) +{ + this.addSpecialPattern('\\s+', scope, 'Ignore'); + + this.addAttributeTokens(scope); + + this.addExitPattern('/>', scope); + this.addExitPattern('>', scope); + +}; + +WYMeditor.XhtmlLexer.prototype.addAttributeTokens = function(scope) +{ + this.addSpecialPattern("\\s*[a-z-_0-9]*:?[a-z-_0-9]+\\s*(?=\=)\\s*", scope, 'TagAttributes'); + + this.addEntryPattern('=\\s*"', scope, 'DoubleQuotedAttribute'); + this.addPattern("\\\\\"", 'DoubleQuotedAttribute'); + this.addExitPattern('"', 'DoubleQuotedAttribute'); + + this.addEntryPattern("=\\s*'", scope, 'SingleQuotedAttribute'); + this.addPattern("\\\\'", 'SingleQuotedAttribute'); + this.addExitPattern("'", 'SingleQuotedAttribute'); + + this.addSpecialPattern('=\\s*[^>\\s]*', scope, 'UnquotedAttribute'); +}; + + + +/** +* XHTML Parser. +* +* This XHTML parser will trigger the events available on on +* current SaxListener +* +* @author Bermi Ferrer (http://bermi.org) +*/ +WYMeditor.XhtmlParser = function(Listener, mode) +{ + var mode = mode || 'Text'; + this._Lexer = new WYMeditor.XhtmlLexer(this); + this._Listener = Listener; + this._mode = mode; + this._matches = []; + this._last_match = ''; + this._current_match = ''; + + return this; +}; + +WYMeditor.XhtmlParser.prototype.parse = function(raw) +{ + this._Lexer.parse(this.beforeParsing(raw)); + return this.afterParsing(this._Listener.getResult()); +}; + +WYMeditor.XhtmlParser.prototype.beforeParsing = function(raw) +{ + if(raw.match(/class="MsoNormal"/) || raw.match(/ns = "urn:schemas-microsoft-com/)){ + // Usefull for cleaning up content pasted from other sources (MSWord) + this._Listener.avoidStylingTagsAndAttributes(); + } + return this._Listener.beforeParsing(raw); +}; + +WYMeditor.XhtmlParser.prototype.afterParsing = function(parsed) +{ + if(this._Listener._avoiding_tags_implicitly){ + this._Listener.allowStylingTagsAndAttributes(); + } + return this._Listener.afterParsing(parsed); +}; + + +WYMeditor.XhtmlParser.prototype.Ignore = function(match, state) +{ + return true; +}; + +WYMeditor.XhtmlParser.prototype.Text = function(text) +{ + this._Listener.addContent(text); + return true; +}; + +WYMeditor.XhtmlParser.prototype.Comment = function(match, status) +{ + return this._addNonTagBlock(match, status, 'addComment'); +}; + +WYMeditor.XhtmlParser.prototype.Script = function(match, status) +{ + return this._addNonTagBlock(match, status, 'addScript'); +}; + +WYMeditor.XhtmlParser.prototype.Css = function(match, status) +{ + return this._addNonTagBlock(match, status, 'addCss'); +}; + +WYMeditor.XhtmlParser.prototype._addNonTagBlock = function(match, state, type) +{ + switch (state){ + case WYMeditor.LEXER_ENTER: + this._non_tag = match; + break; + case WYMeditor.LEXER_UNMATCHED: + this._non_tag += match; + break; + case WYMeditor.LEXER_EXIT: + switch(type) { + case 'addComment': + this._Listener.addComment(this._non_tag+match); + break; + case 'addScript': + this._Listener.addScript(this._non_tag+match); + break; + case 'addCss': + this._Listener.addCss(this._non_tag+match); + break; + } + } + return true; +}; + +WYMeditor.XhtmlParser.prototype.OpeningTag = function(match, state) +{ + switch (state){ + case WYMeditor.LEXER_ENTER: + this._tag = this.normalizeTag(match); + this._tag_attributes = {}; + break; + case WYMeditor.LEXER_SPECIAL: + this._callOpenTagListener(this.normalizeTag(match)); + break; + case WYMeditor.LEXER_EXIT: + this._callOpenTagListener(this._tag, this._tag_attributes); + } + return true; +}; + +WYMeditor.XhtmlParser.prototype.ClosingTag = function(match, state) +{ + this._callCloseTagListener(this.normalizeTag(match)); + return true; +}; + +WYMeditor.XhtmlParser.prototype._callOpenTagListener = function(tag, attributes) +{ + var attributes = attributes || {}; + this.autoCloseUnclosedBeforeNewOpening(tag); + + if(this._Listener.isBlockTag(tag)){ + this._Listener._tag_stack.push(tag); + this._Listener.fixNestingBeforeOpeningBlockTag(tag, attributes); + this._Listener.openBlockTag(tag, attributes); + this._increaseOpenTagCounter(tag); + }else if(this._Listener.isInlineTag(tag)){ + this._Listener.inlineTag(tag, attributes); + }else{ + this._Listener.openUnknownTag(tag, attributes); + this._increaseOpenTagCounter(tag); + } + this._Listener.last_tag = tag; + this._Listener.last_tag_opened = true; + this._Listener.last_tag_attributes = attributes; +}; + +WYMeditor.XhtmlParser.prototype._callCloseTagListener = function(tag) +{ + if(this._decreaseOpenTagCounter(tag)){ + this.autoCloseUnclosedBeforeTagClosing(tag); + + if(this._Listener.isBlockTag(tag)){ + var expected_tag = this._Listener._tag_stack.pop(); + if(expected_tag == false){ + return; + }else if(expected_tag != tag){ + tag = expected_tag; + } + this._Listener.closeBlockTag(tag); + }else{ + this._Listener.closeUnknownTag(tag); + } + }else{ + this._Listener.closeUnopenedTag(tag); + } + this._Listener.last_tag = tag; + this._Listener.last_tag_opened = false; +}; + +WYMeditor.XhtmlParser.prototype._increaseOpenTagCounter = function(tag) +{ + this._Listener._open_tags[tag] = this._Listener._open_tags[tag] || 0; + this._Listener._open_tags[tag]++; +}; + +WYMeditor.XhtmlParser.prototype._decreaseOpenTagCounter = function(tag) +{ + if(this._Listener._open_tags[tag]){ + this._Listener._open_tags[tag]--; + if(this._Listener._open_tags[tag] == 0){ + this._Listener._open_tags[tag] = undefined; + } + return true; + } + return false; +}; + +WYMeditor.XhtmlParser.prototype.autoCloseUnclosedBeforeNewOpening = function(new_tag) +{ + this._autoCloseUnclosed(new_tag, false); +}; + +WYMeditor.XhtmlParser.prototype.autoCloseUnclosedBeforeTagClosing = function(tag) +{ + this._autoCloseUnclosed(tag, true); +}; + +WYMeditor.XhtmlParser.prototype._autoCloseUnclosed = function(new_tag, closing) +{ + var closing = closing || false; + if(this._Listener._open_tags){ + for (var tag in this._Listener._open_tags) { + var counter = this._Listener._open_tags[tag]; + if(counter > 0 && this._Listener.shouldCloseTagAutomatically(tag, new_tag, closing)){ + this._callCloseTagListener(tag, true); + } + } + } +}; + +WYMeditor.XhtmlParser.prototype.getTagReplacements = function() +{ + return this._Listener.getTagReplacements(); +}; + +WYMeditor.XhtmlParser.prototype.normalizeTag = function(tag) +{ + tag = tag.replace(/^([\s<\/>]*)|([\s<\/>]*)$/gm,'').toLowerCase(); + var tags = this._Listener.getTagReplacements(); + if(tags[tag]){ + return tags[tag]; + } + return tag; +}; + +WYMeditor.XhtmlParser.prototype.TagAttributes = function(match, state) +{ + if(WYMeditor.LEXER_SPECIAL == state){ + this._current_attribute = match; + } + return true; +}; + +WYMeditor.XhtmlParser.prototype.DoubleQuotedAttribute = function(match, state) +{ + if(WYMeditor.LEXER_UNMATCHED == state){ + this._tag_attributes[this._current_attribute] = match; + } + return true; +}; + +WYMeditor.XhtmlParser.prototype.SingleQuotedAttribute = function(match, state) +{ + if(WYMeditor.LEXER_UNMATCHED == state){ + this._tag_attributes[this._current_attribute] = match; + } + return true; +}; + +WYMeditor.XhtmlParser.prototype.UnquotedAttribute = function(match, state) +{ + this._tag_attributes[this._current_attribute] = match.replace(/^=/,''); + return true; +}; + + + +/** +* XHTML Sax parser. +* +* @author Bermi Ferrer (http://bermi.org) +*/ +WYMeditor.XhtmlSaxListener = function() +{ + this.output = ''; + this.helper = new WYMeditor.XmlHelper(); + this._open_tags = {}; + this.validator = WYMeditor.XhtmlValidator; + this._tag_stack = []; + this.avoided_tags = []; + + this.entities = { + ' ':' ','¡':'¡','¢':'¢', + '£':'£','¤':'¤','¥':'¥', + '¦':'¦','§':'§','¨':'¨', + '©':'©','ª':'ª','«':'«', + '¬':'¬','­':'­','®':'®', + '¯':'¯','°':'°','±':'±', + '²':'²','³':'³','´':'´', + 'µ':'µ','¶':'¶','·':'·', + '¸':'¸','¹':'¹','º':'º', + '»':'»','¼':'¼','½':'½', + '¾':'¾','¿':'¿','À':'À', + 'Á':'Á','Â':'Â','Ã':'Ã', + 'Ä':'Ä','Å':'Å','Æ':'Æ', + 'Ç':'Ç','È':'È','É':'É', + 'Ê':'Ê','Ë':'Ë','Ì':'Ì', + 'Í':'Í','Î':'Î','Ï':'Ï', + 'Ð':'Ð','Ñ':'Ñ','Ò':'Ò', + 'Ó':'Ó','Ô':'Ô','Õ':'Õ', + 'Ö':'Ö','×':'×','Ø':'Ø', + 'Ù':'Ù','Ú':'Ú','Û':'Û', + 'Ü':'Ü','Ý':'Ý','Þ':'Þ', + 'ß':'ß','à':'à','á':'á', + 'â':'â','ã':'ã','ä':'ä', + 'å':'å','æ':'æ','ç':'ç', + 'è':'è','é':'é','ê':'ê', + 'ë':'ë','ì':'ì','í':'í', + 'î':'î','ï':'ï','ð':'ð', + 'ñ':'ñ','ò':'ò','ó':'ó', + 'ô':'ô','õ':'õ','ö':'ö', + '÷':'÷','ø':'ø','ù':'ù', + 'ú':'ú','û':'û','ü':'ü', + 'ý':'ý','þ':'þ','ÿ':'ÿ', + 'Œ':'Œ','œ':'œ','Š':'Š', + 'š':'š','Ÿ':'Ÿ','ƒ':'ƒ', + 'ˆ':'ˆ','˜':'˜','Α':'Α', + 'Β':'Β','Γ':'Γ','Δ':'Δ', + 'Ε':'Ε','Ζ':'Ζ','Η':'Η', + 'Θ':'Θ','Ι':'Ι','Κ':'Κ', + 'Λ':'Λ','Μ':'Μ','Ν':'Ν', + 'Ξ':'Ξ','Ο':'Ο','Π':'Π', + 'Ρ':'Ρ','Σ':'Σ','Τ':'Τ', + 'Υ':'Υ','Φ':'Φ','Χ':'Χ', + 'Ψ':'Ψ','Ω':'Ω','α':'α', + 'β':'β','γ':'γ','δ':'δ', + 'ε':'ε','ζ':'ζ','η':'η', + 'θ':'θ','ι':'ι','κ':'κ', + 'λ':'λ','μ':'μ','ν':'ν', + 'ξ':'ξ','ο':'ο','π':'π', + 'ρ':'ρ','ς':'ς','σ':'σ', + 'τ':'τ','υ':'υ','φ':'φ', + 'χ':'χ','ψ':'ψ','ω':'ω', + 'ϑ':'ϑ','ϒ':'ϒ','ϖ':'ϖ', + ' ':' ',' ':' ',' ':' ', + '‌':'‌','‍':'‍','‎':'‎', + '‏':'‏','–':'–','—':'—', + '‘':'‘','’':'’','‚':'‚', + '“':'“','”':'”','„':'„', + '†':'†','‡':'‡','•':'•', + '…':'…','‰':'‰','′':'′', + '″':'″','‹':'‹','›':'›', + '‾':'‾','⁄':'⁄','€':'€', + 'ℑ':'ℑ','℘':'℘','ℜ':'ℜ', + '™':'™','ℵ':'ℵ','←':'←', + '↑':'↑','→':'→','↓':'↓', + '↔':'↔','↵':'↵','⇐':'⇐', + '⇑':'⇑','⇒':'⇒','⇓':'⇓', + '⇔':'⇔','∀':'∀','∂':'∂', + '∃':'∃','∅':'∅','∇':'∇', + '∈':'∈','∉':'∉','∋':'∋', + '∏':'∏','∑':'∑','−':'−', + '∗':'∗','√':'√','∝':'∝', + '∞':'∞','∠':'∠','∧':'∧', + '∨':'∨','∩':'∩','∪':'∪', + '∫':'∫','∴':'∴','∼':'∼', + '≅':'≅','≈':'≈','≠':'≠', + '≡':'≡','≤':'≤','≥':'≥', + '⊂':'⊂','⊃':'⊃','⊄':'⊄', + '⊆':'⊆','⊇':'⊇','⊕':'⊕', + '⊗':'⊗','⊥':'⊥','⋅':'⋅', + '⌈':'⌈','⌉':'⌉','⌊':'⌊', + '⌋':'⌋','⟨':'〈','⟩':'〉', + '◊':'◊','♠':'♠','♣':'♣', + '♥':'♥','♦':'♦'}; + + this.block_tags = ["a", "abbr", "acronym", "address", "area", "b", + "base", "bdo", "big", "blockquote", "body", "button", + "caption", "cite", "code", "col", "colgroup", "dd", "del", "div", + "dfn", "dl", "dt", "em", "fieldset", "form", "head", "h1", "h2", + "h3", "h4", "h5", "h6", "html", "i", "ins", + "kbd", "label", "legend", "li", "map", "noscript", + "object", "ol", "optgroup", "option", "p", "param", "pre", "q", + "samp", "script", "select", "small", "span", "strong", "style", + "sub", "sup", "table", "tbody", "td", "textarea", "tfoot", "th", + "thead", "title", "tr", "tt", "ul", "var", "extends"]; + + + this.inline_tags = ["br", "hr", "img", "input"]; + + return this; +}; + +WYMeditor.XhtmlSaxListener.prototype.shouldCloseTagAutomatically = function(tag, now_on_tag, closing) +{ + var closing = closing || false; + if(tag == 'td'){ + if((closing && now_on_tag == 'tr') || (!closing && now_on_tag == 'td')){ + return true; + } + } + if(tag == 'option'){ + if((closing && now_on_tag == 'select') || (!closing && now_on_tag == 'option')){ + return true; + } + } + return false; +}; + +WYMeditor.XhtmlSaxListener.prototype.beforeParsing = function(raw) +{ + this.output = ''; + return raw; +}; + +WYMeditor.XhtmlSaxListener.prototype.afterParsing = function(xhtml) +{ + xhtml = this.replaceNamedEntities(xhtml); + xhtml = this.joinRepeatedEntities(xhtml); + xhtml = this.removeEmptyTags(xhtml); + xhtml = this.removeBrInPre(xhtml); + return xhtml; +}; + +WYMeditor.XhtmlSaxListener.prototype.replaceNamedEntities = function(xhtml) +{ + for (var entity in this.entities) { + xhtml = xhtml.replace(new RegExp(entity, 'g'), this.entities[entity]); + } + return xhtml; +}; + +WYMeditor.XhtmlSaxListener.prototype.joinRepeatedEntities = function(xhtml) +{ + var tags = 'em|strong|sub|sup|acronym|pre|del|address'; + return xhtml.replace(new RegExp('<\/('+tags+')><\\1>' ,''),''). + replace(new RegExp('(\s*<('+tags+')>\s*){2}(.*)(\s*<\/\\2>\s*){2}' ,''),'<\$2>\$3<\$2>'); +}; + +WYMeditor.XhtmlSaxListener.prototype.removeEmptyTags = function(xhtml) +{ + return xhtml.replace(new RegExp('<('+this.block_tags.join("|").replace(/\|td/,'').replace(/\|th/, '')+')>(
| | |\\s)*<\/\\1>' ,'g'),''); +}; + +WYMeditor.XhtmlSaxListener.prototype.removeBrInPre = function(xhtml) +{ + var matches = xhtml.match(new RegExp(']*>(.*?)<\/pre>','gmi')); + if(matches) { + for(var i=0; i', 'g'), String.fromCharCode(13,10))); + } + } + return xhtml; +}; + +WYMeditor.XhtmlSaxListener.prototype.getResult = function() +{ + return this.output; +}; + +WYMeditor.XhtmlSaxListener.prototype.getTagReplacements = function() +{ + return {'b':'strong', 'i':'em'}; +}; + +WYMeditor.XhtmlSaxListener.prototype.addContent = function(text) +{ + this.output += text; +}; + +WYMeditor.XhtmlSaxListener.prototype.addComment = function(text) +{ + if(this.remove_comments){ + this.output += text; + } +}; + +WYMeditor.XhtmlSaxListener.prototype.addScript = function(text) +{ + if(!this.remove_scripts){ + this.output += text; + } +}; + +WYMeditor.XhtmlSaxListener.prototype.addCss = function(text) +{ + if(!this.remove_embeded_styles){ + this.output += text; + } +}; + +WYMeditor.XhtmlSaxListener.prototype.openBlockTag = function(tag, attributes) +{ + this.output += this.helper.tag(tag, this.validator.getValidTagAttributes(tag, attributes), true); +}; + +WYMeditor.XhtmlSaxListener.prototype.inlineTag = function(tag, attributes) +{ + this.output += this.helper.tag(tag, this.validator.getValidTagAttributes(tag, attributes)); +}; + +WYMeditor.XhtmlSaxListener.prototype.openUnknownTag = function(tag, attributes) +{ + //this.output += this.helper.tag(tag, attributes, true); +}; + +WYMeditor.XhtmlSaxListener.prototype.closeBlockTag = function(tag) +{ + this.output = this.output.replace(/
$/, '')+this._getClosingTagContent('before', tag)+""+this._getClosingTagContent('after', tag); +}; + +WYMeditor.XhtmlSaxListener.prototype.closeUnknownTag = function(tag) +{ + //this.output += ""; +}; + +WYMeditor.XhtmlSaxListener.prototype.closeUnopenedTag = function(tag) +{ + this.output += ""; +}; + +WYMeditor.XhtmlSaxListener.prototype.avoidStylingTagsAndAttributes = function() +{ + this.avoided_tags = ['div','span']; + this.validator.skiped_attributes = ['style']; + this.validator.skiped_attribute_values = ['MsoNormal','main1']; // MS Word attributes for class + this._avoiding_tags_implicitly = true; +}; + +WYMeditor.XhtmlSaxListener.prototype.allowStylingTagsAndAttributes = function() +{ + this.avoided_tags = []; + this.validator.skiped_attributes = []; + this.validator.skiped_attribute_values = []; + this._avoiding_tags_implicitly = false; +}; + +WYMeditor.XhtmlSaxListener.prototype.isBlockTag = function(tag) +{ + return !WYMeditor.Helper.contains(this.avoided_tags, tag) && WYMeditor.Helper.contains(this.block_tags, tag); +}; + +WYMeditor.XhtmlSaxListener.prototype.isInlineTag = function(tag) +{ + return !WYMeditor.Helper.contains(this.avoided_tags, tag) && WYMeditor.Helper.contains(this.inline_tags, tag); +}; + +WYMeditor.XhtmlSaxListener.prototype.insertContentAfterClosingTag = function(tag, content) +{ + this._insertContentWhenClosingTag('after', tag, content); +}; + +WYMeditor.XhtmlSaxListener.prototype.insertContentBeforeClosingTag = function(tag, content) +{ + this._insertContentWhenClosingTag('before', tag, content); +}; + +WYMeditor.XhtmlSaxListener.prototype.fixNestingBeforeOpeningBlockTag = function(tag, attributes) +{ + if(tag != 'li' && (tag == 'ul' || tag == 'ol') && this.last_tag && !this.last_tag_opened && this.last_tag == 'li'){ + this.output = this.output.replace(/<\/li>$/, ''); + this.insertContentAfterClosingTag(tag, ''); + } +}; + +WYMeditor.XhtmlSaxListener.prototype._insertContentWhenClosingTag = function(position, tag, content) +{ + if(!this['_insert_'+position+'_closing']){ + this['_insert_'+position+'_closing'] = []; + } + if(!this['_insert_'+position+'_closing'][tag]){ + this['_insert_'+position+'_closing'][tag] = []; + } + this['_insert_'+position+'_closing'][tag].push(content); +}; + +WYMeditor.XhtmlSaxListener.prototype._getClosingTagContent = function(position, tag) +{ + if( this['_insert_'+position+'_closing'] && + this['_insert_'+position+'_closing'][tag] && + this['_insert_'+position+'_closing'][tag].length > 0){ + return this['_insert_'+position+'_closing'][tag].pop(); + } + return ''; +}; + + +/********** CSS PARSER **********/ + + +WYMeditor.WymCssLexer = function(parser, only_wym_blocks) +{ + var only_wym_blocks = (typeof only_wym_blocks == 'undefined' ? true : only_wym_blocks); + + jQuery.extend(this, new WYMeditor.Lexer(parser, (only_wym_blocks?'Ignore':'WymCss'))); + + this.mapHandler('WymCss', 'Ignore'); + + if(only_wym_blocks == true){ + this.addEntryPattern("/\\\x2a[<\\s]*WYMeditor[>\\s]*\\\x2a/", 'Ignore', 'WymCss'); + this.addExitPattern("/\\\x2a[<\/\\s]*WYMeditor[>\\s]*\\\x2a/", 'WymCss'); + } + + this.addSpecialPattern("[\\sa-z1-6]*\\\x2e[a-z-_0-9]+", 'WymCss', 'WymCssStyleDeclaration'); + + this.addEntryPattern("/\\\x2a", 'WymCss', 'WymCssComment'); + this.addExitPattern("\\\x2a/", 'WymCssComment'); + + this.addEntryPattern("\x7b", 'WymCss', 'WymCssStyle'); + this.addExitPattern("\x7d", 'WymCssStyle'); + + this.addEntryPattern("/\\\x2a", 'WymCssStyle', 'WymCssFeedbackStyle'); + this.addExitPattern("\\\x2a/", 'WymCssFeedbackStyle'); + + return this; +}; + +WYMeditor.WymCssParser = function() +{ + this._in_style = false; + this._has_title = false; + this.only_wym_blocks = true; + this.css_settings = {'classesItems':[], 'editorStyles':[], 'dialogStyles':[]}; + return this; +}; + +WYMeditor.WymCssParser.prototype.parse = function(raw, only_wym_blocks) +{ + var only_wym_blocks = (typeof only_wym_blocks == 'undefined' ? this.only_wym_blocks : only_wym_blocks); + this._Lexer = new WYMeditor.WymCssLexer(this, only_wym_blocks); + this._Lexer.parse(raw); +}; + +WYMeditor.WymCssParser.prototype.Ignore = function(match, state) +{ + return true; +}; + +WYMeditor.WymCssParser.prototype.WymCssComment = function(text, status) +{ + if(text.match(/end[a-z0-9\s]*wym[a-z0-9\s]*/mi)){ + return false; + } + if(status == WYMeditor.LEXER_UNMATCHED){ + if(!this._in_style){ + this._has_title = true; + this._current_item = {'title':WYMeditor.Helper.trim(text)}; + }else{ + if(this._current_item[this._current_element]){ + if(!this._current_item[this._current_element].expressions){ + this._current_item[this._current_element].expressions = [text]; + }else{ + this._current_item[this._current_element].expressions.push(text); + } + } + } + this._in_style = true; + } + return true; +}; + +WYMeditor.WymCssParser.prototype.WymCssStyle = function(match, status) +{ + if(status == WYMeditor.LEXER_UNMATCHED){ + match = WYMeditor.Helper.trim(match); + if(match != ''){ + this._current_item[this._current_element].style = match; + } + }else if (status == WYMeditor.LEXER_EXIT){ + this._in_style = false; + this._has_title = false; + this.addStyleSetting(this._current_item); + } + return true; +}; + +WYMeditor.WymCssParser.prototype.WymCssFeedbackStyle = function(match, status) +{ + if(status == WYMeditor.LEXER_UNMATCHED){ + this._current_item[this._current_element].feedback_style = match.replace(/^([\s\/\*]*)|([\s\/\*]*)$/gm,''); + } + return true; +}; + +WYMeditor.WymCssParser.prototype.WymCssStyleDeclaration = function(match) +{ + match = match.replace(/^([\s\.]*)|([\s\.*]*)$/gm, ''); + + var tag = ''; + if(match.indexOf('.') > 0){ + var parts = match.split('.'); + this._current_element = parts[1]; + var tag = parts[0]; + }else{ + this._current_element = match; + } + + if(!this._has_title){ + this._current_item = {'title':(!tag?'':tag.toUpperCase()+': ')+this._current_element}; + this._has_title = true; + } + + if(!this._current_item[this._current_element]){ + this._current_item[this._current_element] = {'name':this._current_element}; + } + if(tag){ + if(!this._current_item[this._current_element].tags){ + this._current_item[this._current_element].tags = [tag]; + }else{ + this._current_item[this._current_element].tags.push(tag); + } + } + return true; +}; + +WYMeditor.WymCssParser.prototype.addStyleSetting = function(style_details) +{ + for (var name in style_details){ + var details = style_details[name]; + if(typeof details == 'object' && name != 'title'){ + + this.css_settings.classesItems.push({ + 'name': WYMeditor.Helper.trim(details.name), + 'title': style_details.title, + 'expr' : WYMeditor.Helper.trim((details.expressions||details.tags).join(', ')) + }); + if(details.feedback_style){ + this.css_settings.editorStyles.push({ + 'name': '.'+ WYMeditor.Helper.trim(details.name), + 'css': details.feedback_style + }); + } + if(details.style){ + this.css_settings.dialogStyles.push({ + 'name': '.'+ WYMeditor.Helper.trim(details.name), + 'css': details.style + }); + } + } + } +}; + +/********** HELPERS **********/ + +// Returns true if it is a text node with whitespaces only +jQuery.fn.isPhantomNode = function() { + if (this[0].nodeType == 3) + return !(/[^\t\n\r ]/.test(this[0].data)); + + return false; +}; + +WYMeditor.isPhantomNode = function(n) { + if (n.nodeType == 3) + return !(/[^\t\n\r ]/.test(n.data)); + + return false; +}; + +WYMeditor.isPhantomString = function(str) { + return !(/[^\t\n\r ]/.test(str)); +}; + +// Returns the Parents or the node itself +// jqexpr = a jQuery expression +jQuery.fn.parentsOrSelf = function(jqexpr) { + var n = this; + + if (n[0].nodeType == 3) + n = n.parents().slice(0,1); + +// if (n.is(jqexpr)) // XXX should work, but doesn't (probably a jQuery bug) + if (n.filter(jqexpr).size() == 1) + return n; + else + return n.parents(jqexpr).slice(0,1); +}; + +// String & array helpers + +WYMeditor.Helper = { + + //replace all instances of 'old' by 'rep' in 'str' string + replaceAll: function(str, old, rep) { + var rExp = new RegExp(old, "g"); + return(str.replace(rExp, rep)); + }, + + //insert 'inserted' at position 'pos' in 'str' string + insertAt: function(str, inserted, pos) { + return(str.substr(0,pos) + inserted + str.substring(pos)); + }, + + //trim 'str' string + trim: function(str) { + return str.replace(/^(\s*)|(\s*)$/gm,''); + }, + + //return true if 'arr' array contains 'elem', or false + contains: function(arr, elem) { + for (var i = 0; i < arr.length; i++) { + if (arr[i] === elem) return true; + } + return false; + }, + + //return 'item' position in 'arr' array, or -1 + indexOf: function(arr, item) { + var ret=-1; + for(var i = 0; i < arr.length; i++) { + if (arr[i] == item) { + ret = i; + break; + } + } + return(ret); + }, + + //return 'item' object in 'arr' array, checking its 'name' property, or null + findByName: function(arr, name) { + for(var i = 0; i < arr.length; i++) { + var item = arr[i]; + if(item.name == name) return(item); + } + return(null); + } +}; + + diff --git a/websdk/static/js/wymeditor/jquery.wymeditor.min.js b/websdk/static/js/wymeditor/jquery.wymeditor.min.js new file mode 100644 index 0000000..b57a18e --- /dev/null +++ b/websdk/static/js/wymeditor/jquery.wymeditor.min.js @@ -0,0 +1 @@ +if(!WYMeditor){var WYMeditor={}}(function(){if(!window.console||!console.firebug){var b=["log","debug","info","warn","error","assert","dir","dirxml","group","groupEnd","time","timeEnd","count","trace","profile","profileEnd"];WYMeditor.console={};for(var a=0;a
"+WYMeditor.TOOLS+"
"+WYMeditor.CONTAINERS+WYMeditor.CLASSES+"
"+WYMeditor.HTML+WYMeditor.IFRAME+WYMeditor.STATUS+"
"+WYMeditor.LOGO+"
",logoHtml:"WYMeditor",iframeHtml:"
",editorStyles:[],toolsHtml:"

{Tools}

    "+WYMeditor.TOOLS_ITEMS+"
",toolsItemHtml:"
  • "+WYMeditor.TOOL_TITLE+"
  • ",toolsItems:[{name:"Bold",title:"Strong",css:"wym_tools_strong"},{name:"Italic",title:"Emphasis",css:"wym_tools_emphasis"},{name:"Superscript",title:"Superscript",css:"wym_tools_superscript"},{name:"Subscript",title:"Subscript",css:"wym_tools_subscript"},{name:"InsertOrderedList",title:"Ordered_List",css:"wym_tools_ordered_list"},{name:"InsertUnorderedList",title:"Unordered_List",css:"wym_tools_unordered_list"},{name:"Indent",title:"Indent",css:"wym_tools_indent"},{name:"Outdent",title:"Outdent",css:"wym_tools_outdent"},{name:"Undo",title:"Undo",css:"wym_tools_undo"},{name:"Redo",title:"Redo",css:"wym_tools_redo"},{name:"CreateLink",title:"Link",css:"wym_tools_link"},{name:"Unlink",title:"Unlink",css:"wym_tools_unlink"},{name:"InsertImage",title:"Image",css:"wym_tools_image"},{name:"InsertTable",title:"Table",css:"wym_tools_table"},{name:"Paste",title:"Paste_From_Word",css:"wym_tools_paste"},{name:"ToggleHtml",title:"HTML",css:"wym_tools_html"},{name:"Preview",title:"Preview",css:"wym_tools_preview"}],containersHtml:"

    {Containers}

      "+WYMeditor.CONTAINERS_ITEMS+"
    ",containersItemHtml:"
  • "+WYMeditor.CONTAINER_TITLE+"
  • ",containersItems:[{name:"P",title:"Paragraph",css:"wym_containers_p"},{name:"H1",title:"Heading_1",css:"wym_containers_h1"},{name:"H2",title:"Heading_2",css:"wym_containers_h2"},{name:"H3",title:"Heading_3",css:"wym_containers_h3"},{name:"H4",title:"Heading_4",css:"wym_containers_h4"},{name:"H5",title:"Heading_5",css:"wym_containers_h5"},{name:"H6",title:"Heading_6",css:"wym_containers_h6"},{name:"PRE",title:"Preformatted",css:"wym_containers_pre"},{name:"BLOCKQUOTE",title:"Blockquote",css:"wym_containers_blockquote"},{name:"TH",title:"Table_Header",css:"wym_containers_th"}],classesHtml:"

    {Classes}

      "+WYMeditor.CLASSES_ITEMS+"
    ",classesItemHtml:"
  • "+WYMeditor.CLASS_TITLE+"
  • ",classesItems:[],statusHtml:"

    {Status}

    ",htmlHtml:"

    {Source_Code}

    ",boxSelector:".wym_box",toolsSelector:".wym_tools",toolsListSelector:" ul",containersSelector:".wym_containers",classesSelector:".wym_classes",htmlSelector:".wym_html",iframeSelector:".wym_iframe iframe",iframeBodySelector:".wym_iframe",statusSelector:".wym_status",toolSelector:".wym_tools a",containerSelector:".wym_containers a",classSelector:".wym_classes a",htmlValSelector:".wym_html_val",hrefSelector:".wym_href",srcSelector:".wym_src",titleSelector:".wym_title",relSelector:".wym_rel",altSelector:".wym_alt",textSelector:".wym_text",rowsSelector:".wym_rows",colsSelector:".wym_cols",captionSelector:".wym_caption",summarySelector:".wym_summary",submitSelector:"form",cancelSelector:".wym_cancel",previewSelector:"",dialogTypeSelector:".wym_dialog_type",dialogLinkSelector:".wym_dialog_link",dialogImageSelector:".wym_dialog_image",dialogTableSelector:".wym_dialog_table",dialogPasteSelector:".wym_dialog_paste",dialogPreviewSelector:".wym_dialog_preview",updateSelector:".wymupdate",updateEvent:"click",dialogFeatures:"menubar=no,titlebar=no,toolbar=no,resizable=no,width=560,height=300,top=0,left=0",dialogFeaturesPreview:"menubar=no,titlebar=no,toolbar=no,resizable=no,scrollbars=yes,width=560,height=300,top=0,left=0",dialogHtml:""+WYMeditor.DIALOG_TITLE+" + +Make sure to adjust the ``src`` attribute to your needs, then initialize the +plugin in WYMeditor's ``postInit`` function:: + + wymeditor({postInit: function(wym) { + wym.hovertools(); // other plugins... + wym.resizable({handles: "s,e", + maxHeight: 600}); + } + }) + +The ``resizable`` plugin takes exactly one parameter, which is an object literal +containing the options of the plugin. The WYMeditor ``resizable`` plugin +supports all options of the jQuery UI ``resizable`` plugin. These are the +default values used by the plugin:: + + handles: "s,e,se", + minHeight: 250, + maxHeight: 600 + +See the `jQuery UI resizable plugin docs`_ for a list of all options. + +That's it! You are now able to resize the WYMeditor vertically, horizontally or +both, depending on your options. + +.. _jQuery UI resizable plugin docs: http://docs.jquery.com/UI/Resizables + +Internals +========= +The plugin takes care of loading the necessary jQuery UI files (``base`` and +``resizable``) from the same path the jQuery library was loaded. Here's how +it's done:: + + // Get the jQuery path from the editor, stripping away the jQuery file. + // see http://www.oreilly.com/catalog/regex/chapter/ch04.html + // The match result array contains the path and the filename. + var jQueryPath = wym.computeJqueryPath().match(/^(.*)\/(.*)$/)[1]; + + // Make an array of the external JavaScript files required by the plugin. + var jQueryPlugins = [jQueryPath + '/ui.base.js', + jQueryPath + '/ui.resizable.js']; + + // First get the jQuery UI base file + $.getScript(jQueryPlugins[0]); + + // Get the jQuery UI resizeable plugin and then init the wymeditor resizable + // plugin. It is import to do the initialisation after loading the + // necessary jQuery UI files has finished, otherwise the "resizable" method + // would not be available. + $.getScript(jQueryPlugins[1], function() { + jQuery(wym._box).resizable(final_options); + }); + +An alternative approach would be to use an AJAX queue when getting the script +files to ensure that all jQuery files are loaded before the initialisation code +of the plugin is executed. There is an `jQuery AJAX queue plugin`_ which does +that. + +.. _jQuery AJAX queue plugin: http://plugins.jquery.com/project/ajaxqueue + +Changelog +========= + +0.2 +--- +- Added full support for all jQuery UI resizable plugin options. +- Refactored and documented code. +- Now contains a packed version (775 bytes). + +0.1 +--- +- Initial release. + +.. _WYMeditor: http://www.wymeditor.org/ +.. _WYMeditor plugin page: http://trac.wymeditor.org/trac/wiki/0.4/Plugins diff --git a/websdk/static/js/wymeditor/plugins/resizable/jquery.wymeditor.resizable.js b/websdk/static/js/wymeditor/plugins/resizable/jquery.wymeditor.resizable.js new file mode 100644 index 0000000..1ba2d2e --- /dev/null +++ b/websdk/static/js/wymeditor/plugins/resizable/jquery.wymeditor.resizable.js @@ -0,0 +1,91 @@ +/* + * WYMeditor : what you see is What You Mean web-based editor + * Copyright (c) 2005 - 2009 Jean-Francois Hovinne, http://www.wymeditor.org/ + * Dual licensed under the MIT (MIT-license.txt) + * and GPL (GPL-license.txt) licenses. + * + * For further information visit: + * http://www.wymeditor.org/ + * + * File Name: + * jquery.wymeditor.resizable.js + * resize plugin for WYMeditor + * + * File Authors: + * Peter Eschler (peschler _at_ gmail.com) + * Jean-Francois Hovinne - http://www.hovinne.com/ + * + * Version: + * 0.4 + * + * Changelog: + * + * 0.4 + * - Removed UI and UI.resizable scripts loading - see #167 (jfh). + * + * 0.3 + * - Added 'iframeOriginalSize' and removed 'ui.instance' calls (jfh). + * + * 0.2 + * - Added full support for all jQueryUI resizable plugin options. + * - Refactored and documented code. + * 0.1 + * - Initial release. + */ + +/** + * The resizable plugin makes the wymeditor box vertically resizable. + * It it based on the ui.resizable.js plugin of the jQuery UI library. + * + * The WYMeditor resizable plugin supports all parameters of the jQueryUI + * resizable plugin. The parameters are passed like this: + * + * wym.resizable({ handles: "s,e", + * maxHeight: 600 }); + * + * DEPENDENCIES: jQuery UI, jQuery UI resizable + * + * @param options options for the plugin + */ +WYMeditor.editor.prototype.resizable = function(options) { + + var wym = this; + var iframe = jQuery(wym._box).find('iframe'); + var iframeOriginalSize = {}; + + // Define some default options + var default_options = { + start: function(e, ui) { + iframeOriginalSize = { + width: jQuery(iframe).width(), + height: jQuery(iframe).height() + } + }, + + // resize is called by the jQuery resizable plugin whenever the + // client area was resized. + resize: function(e, ui) { + var diff = ui.size.height - ui.originalSize.height; + jQuery(iframe).height( iframeOriginalSize.height + diff ); + + // If the plugin has horizontal resizing disabled we need to + // adjust the "width" attribute of the area css, because the + // resizing will set a fixed width (which breaks liquid layout + // of the wymeditor area). + if( !ui.options.handles['w'] && !ui.options.handles['e'] ) { + ui.size.width = "inherit"; + } + }, + handles: "s,e,se", + minHeight: 250, + maxHeight: 600 + }; + + // Merge given options with default options. Given options override + // default ones. + var final_options = jQuery.extend(default_options, options); + + if(jQuery.isFunction( jQuery.fn.resizable )) jQuery(wym._box).resizable(final_options); + else WYMeditor.console.error('Oops, jQuery UI.resizable unavailable.'); + +}; diff --git a/websdk/static/js/wymeditor/plugins/resizable/readme.txt b/websdk/static/js/wymeditor/plugins/resizable/readme.txt new file mode 100644 index 0000000..2a0444e --- /dev/null +++ b/websdk/static/js/wymeditor/plugins/resizable/readme.txt @@ -0,0 +1,124 @@ + + +resizable plugin for WYMeditor +############################## + +The ``resizable`` plugin for WYMeditor_ enables vertical resizing of the +editor area. The plugin is based on the jQuery UI library. + +Requirements +============ +The following packages are required for using the WYMeditor ``resizable`` +plugin: + +* jQuery (tested with jQuery ``jquery-1.2.4a.js`` from ``jquery.ui`` package) +* WYMeditor SVN trunk (Revision: 482) +* jQuery-UI (tested with ``jquery.ui-1.5b2``) + +It should be possible to use this plugin with ``WYMeditor-0.4`` but I have not +tried. + +Download +======== +You can download the WYMeditor ``resizable`` plugin here: + +* wymeditor-resizable-plugin-0.2.tgz_ +* wymeditor-resizable-plugin-0.1.tgz_ + +See the Changelog_ for more infos about the releases. + +.. _wymeditor-resizable-plugin-0.2.tgz: http://pyjax.net/download/wymeditor-resizable-plugin-0.2.tgz +.. _wymeditor-resizable-plugin-0.1.tgz: http://pyjax.net/download/wymeditor-resizable-plugin-0.1.tgz + +Installation +============ +Just extract the downloaded archive into your WYMeditor's ``plugin`` +directory. + +Usage +===== +For general instructions on WYMeditor plugins please refer to the `WYMeditor +plugin page`_. + +To use the ``resizable`` plugin simply include the plugin's JavaScript file in +your code. You **do not** need to include the jQuery UI files - this is done +automatically by the plugin (see `Internals`_):: + + + +Make sure to adjust the ``src`` attribute to your needs, then initialize the +plugin in WYMeditor's ``postInit`` function:: + + wymeditor({postInit: function(wym) { + wym.hovertools(); // other plugins... + wym.resizable({handles: "s,e", + maxHeight: 600}); + } + }) + +The ``resizable`` plugin takes exactly one parameter, which is an object literal +containing the options of the plugin. The WYMeditor ``resizable`` plugin +supports all options of the jQuery UI ``resizable`` plugin. These are the +default values used by the plugin:: + + handles: "s,e,se", + minHeight: 250, + maxHeight: 600 + +See the `jQuery UI resizable plugin docs`_ for a list of all options. + +That's it! You are now able to resize the WYMeditor vertically, horizontally or +both, depending on your options. + +.. _jQuery UI resizable plugin docs: http://docs.jquery.com/UI/Resizables + +Internals +========= +The plugin takes care of loading the necessary jQuery UI files (``base`` and +``resizable``) from the same path the jQuery library was loaded. Here's how +it's done:: + + // Get the jQuery path from the editor, stripping away the jQuery file. + // see http://www.oreilly.com/catalog/regex/chapter/ch04.html + // The match result array contains the path and the filename. + var jQueryPath = wym.computeJqueryPath().match(/^(.*)\/(.*)$/)[1]; + + // Make an array of the external JavaScript files required by the plugin. + var jQueryPlugins = [jQueryPath + '/ui.base.js', + jQueryPath + '/ui.resizable.js']; + + // First get the jQuery UI base file + $.getScript(jQueryPlugins[0]); + + // Get the jQuery UI resizeable plugin and then init the wymeditor resizable + // plugin. It is import to do the initialisation after loading the + // necessary jQuery UI files has finished, otherwise the "resizable" method + // would not be available. + $.getScript(jQueryPlugins[1], function() { + jQuery(wym._box).resizable(final_options); + }); + +An alternative approach would be to use an AJAX queue when getting the script +files to ensure that all jQuery files are loaded before the initialisation code +of the plugin is executed. There is an `jQuery AJAX queue plugin`_ which does +that. + +.. _jQuery AJAX queue plugin: http://plugins.jquery.com/project/ajaxqueue + +Changelog +========= + +0.2 +--- +- Added full support for all jQuery UI resizable plugin options. +- Refactored and documented code. +- Now contains a packed version (775 bytes). + +0.1 +--- +- Initial release. + +.. _WYMeditor: http://www.wymeditor.org/ +.. _WYMeditor plugin page: http://trac.wymeditor.org/trac/wiki/0.4/Plugins diff --git a/websdk/static/js/wymeditor/plugins/tidy/.svn/entries b/websdk/static/js/wymeditor/plugins/tidy/.svn/entries new file mode 100644 index 0000000..3acea50 --- /dev/null +++ b/websdk/static/js/wymeditor/plugins/tidy/.svn/entries @@ -0,0 +1,164 @@ +10 + +dir +677 +svn://svn.wymeditor.org/wymeditor/trunk/src/wymeditor/plugins/tidy +svn://svn.wymeditor.org/wymeditor + + + +2010-04-11T19:34:57.530630Z +658 +mr_lundis + + + + + + + + + + + + + + +89e89e35-0a13-0410-8f61-920bba073fa9 + +tidy.php +file + + + + +2011-07-13T16:45:39.000000Z +3355fbc23378052db8213dbdcd4fe31f +2007-04-26T12:45:23.848206Z +245 +jf.hovinne + + + + + + + + + + + + + + + + + + + + + +1052 + +jquery.wymeditor.tidy.js +file + + + + +2011-07-13T16:45:39.000000Z +3a2dee9a16df26d6d1b36886f77880c8 +2010-04-11T19:34:57.530630Z +658 +mr_lundis + + + + + + + + + + + + + + + + + + + + + +2157 + +wand.png +file + + + + +2011-07-13T16:45:39.000000Z +22d8038ebf5ac63b0062ebf361e8261c +2007-04-23T11:42:06.279227Z +233 +d.reszka +has-props + + + + + + + + + + + + + + + + + + + + +715 + +README +file + + + + +2011-07-13T16:45:39.000000Z +b31aaf6b3928608a98b2028c2d9f523e +2009-05-27T19:20:55.910061Z +632 +jf.hovinne + + + + + + + + + + + + + + + + + + + + + +607 + diff --git a/websdk/static/js/wymeditor/plugins/tidy/.svn/prop-base/wand.png.svn-base b/websdk/static/js/wymeditor/plugins/tidy/.svn/prop-base/wand.png.svn-base new file mode 100644 index 0000000..5e9587e --- /dev/null +++ b/websdk/static/js/wymeditor/plugins/tidy/.svn/prop-base/wand.png.svn-base @@ -0,0 +1,5 @@ +K 13 +svn:mime-type +V 24 +application/octet-stream +END diff --git a/websdk/static/js/wymeditor/plugins/tidy/.svn/text-base/README.svn-base b/websdk/static/js/wymeditor/plugins/tidy/.svn/text-base/README.svn-base new file mode 100644 index 0000000..acc7ffd --- /dev/null +++ b/websdk/static/js/wymeditor/plugins/tidy/.svn/text-base/README.svn-base @@ -0,0 +1,19 @@ +WYMeditor : what you see is What You Mean web-based editor +Copyright (c) 2005 - 2009 Jean-Francois Hovinne, http://www.wymeditor.org/ +Dual licensed under the MIT (MIT-license.txt) +and GPL (GPL-license.txt) licenses. + +For further information visit: + http://www.wymeditor.org/ + +File Name: + README - HTML Tidy plugin for WYMeditor + +File Authors: + Jean-François Hovinne (jf.hovinne a-t wymeditor dotorg) + +Credits: + 'HTML Tidy' by Dave Ragget - http://tidy.sourceforge.net/ + Icon 'wand' by Mark James - http://famfamfam.com/ + +WYMeditor documentation is available online at http://www.wymeditor.org/ diff --git a/websdk/static/js/wymeditor/plugins/tidy/.svn/text-base/jquery.wymeditor.tidy.js.svn-base b/websdk/static/js/wymeditor/plugins/tidy/.svn/text-base/jquery.wymeditor.tidy.js.svn-base new file mode 100644 index 0000000..bf30c4c --- /dev/null +++ b/websdk/static/js/wymeditor/plugins/tidy/.svn/text-base/jquery.wymeditor.tidy.js.svn-base @@ -0,0 +1,82 @@ +/* + * WYMeditor : what you see is What You Mean web-based editor + * Copyright (c) 2005 - 2009 Jean-Francois Hovinne, http://www.wymeditor.org/ + * Dual licensed under the MIT (MIT-license.txt) + * and GPL (GPL-license.txt) licenses. + * + * For further information visit: + * http://www.wymeditor.org/ + * + * File Name: + * jquery.wymeditor.tidy.js + * HTML Tidy plugin for WYMeditor + * + * File Authors: + * Jean-Francois Hovinne (jf.hovinne a-t wymeditor dotorg) + */ + +//Extend WYMeditor +WYMeditor.editor.prototype.tidy = function(options) { + var tidy = new WymTidy(options, this); + return(tidy); +}; + +//WymTidy constructor +function WymTidy(options, wym) { + + options = jQuery.extend({ + + sUrl: wym._options.basePath + "plugins/tidy/tidy.php", + sButtonHtml: "
  • " + + "" + + "Clean up HTML" + + "
  • ", + + sButtonSelector: "li.wym_tools_tidy a" + + }, options); + + this._options = options; + this._wym = wym; + +}; + +//WymTidy initialization +WymTidy.prototype.init = function() { + + var tidy = this; + + jQuery(this._wym._box).find( + this._wym._options.toolsSelector + this._wym._options.toolsListSelector) + .append(this._options.sButtonHtml); + + //handle click event + jQuery(this._wym._box).find(this._options.sButtonSelector).click(function() { + tidy.cleanup(); + return(false); + }); + +}; + +//WymTidy cleanup +WymTidy.prototype.cleanup = function() { + + var wym = this._wym; + var html = "" + wym.xhtml() + ""; + + jQuery.post(this._options.sUrl, { html: html}, function(data) { + + if(data.length > 0 && data != '0') { + if(data.indexOf(" 0) { + + // Specify configuration + $config = array( + 'bare' => true, + 'clean' => true, + 'doctype' => 'strict', + 'drop-empty-paras' => true, + 'drop-font-tags' => true, + 'drop-proprietary-attributes' => true, + 'enclose-block-text' => true, + 'indent' => false, + 'join-classes' => true, + 'join-styles' => true, + 'logical-emphasis' => true, + 'output-xhtml' => true, + 'show-body-only' => true, + 'wrap' => 0); + + // Tidy + $tidy = new tidy; + $tidy->parseString($html, $config, 'utf8'); + $tidy->cleanRepair(); + + // Output + echo $tidy; +} else { + +echo ('0'); +} +?> diff --git a/websdk/static/js/wymeditor/plugins/tidy/.svn/text-base/wand.png.svn-base b/websdk/static/js/wymeditor/plugins/tidy/.svn/text-base/wand.png.svn-base new file mode 100644 index 0000000..bb55eea --- /dev/null +++ b/websdk/static/js/wymeditor/plugins/tidy/.svn/text-base/wand.png.svn-base Binary files differ diff --git a/websdk/static/js/wymeditor/plugins/tidy/README b/websdk/static/js/wymeditor/plugins/tidy/README new file mode 100644 index 0000000..acc7ffd --- /dev/null +++ b/websdk/static/js/wymeditor/plugins/tidy/README @@ -0,0 +1,19 @@ +WYMeditor : what you see is What You Mean web-based editor +Copyright (c) 2005 - 2009 Jean-Francois Hovinne, http://www.wymeditor.org/ +Dual licensed under the MIT (MIT-license.txt) +and GPL (GPL-license.txt) licenses. + +For further information visit: + http://www.wymeditor.org/ + +File Name: + README - HTML Tidy plugin for WYMeditor + +File Authors: + Jean-François Hovinne (jf.hovinne a-t wymeditor dotorg) + +Credits: + 'HTML Tidy' by Dave Ragget - http://tidy.sourceforge.net/ + Icon 'wand' by Mark James - http://famfamfam.com/ + +WYMeditor documentation is available online at http://www.wymeditor.org/ diff --git a/websdk/static/js/wymeditor/plugins/tidy/jquery.wymeditor.tidy.js b/websdk/static/js/wymeditor/plugins/tidy/jquery.wymeditor.tidy.js new file mode 100644 index 0000000..bf30c4c --- /dev/null +++ b/websdk/static/js/wymeditor/plugins/tidy/jquery.wymeditor.tidy.js @@ -0,0 +1,82 @@ +/* + * WYMeditor : what you see is What You Mean web-based editor + * Copyright (c) 2005 - 2009 Jean-Francois Hovinne, http://www.wymeditor.org/ + * Dual licensed under the MIT (MIT-license.txt) + * and GPL (GPL-license.txt) licenses. + * + * For further information visit: + * http://www.wymeditor.org/ + * + * File Name: + * jquery.wymeditor.tidy.js + * HTML Tidy plugin for WYMeditor + * + * File Authors: + * Jean-Francois Hovinne (jf.hovinne a-t wymeditor dotorg) + */ + +//Extend WYMeditor +WYMeditor.editor.prototype.tidy = function(options) { + var tidy = new WymTidy(options, this); + return(tidy); +}; + +//WymTidy constructor +function WymTidy(options, wym) { + + options = jQuery.extend({ + + sUrl: wym._options.basePath + "plugins/tidy/tidy.php", + sButtonHtml: "
  • " + + "" + + "Clean up HTML" + + "
  • ", + + sButtonSelector: "li.wym_tools_tidy a" + + }, options); + + this._options = options; + this._wym = wym; + +}; + +//WymTidy initialization +WymTidy.prototype.init = function() { + + var tidy = this; + + jQuery(this._wym._box).find( + this._wym._options.toolsSelector + this._wym._options.toolsListSelector) + .append(this._options.sButtonHtml); + + //handle click event + jQuery(this._wym._box).find(this._options.sButtonSelector).click(function() { + tidy.cleanup(); + return(false); + }); + +}; + +//WymTidy cleanup +WymTidy.prototype.cleanup = function() { + + var wym = this._wym; + var html = "" + wym.xhtml() + ""; + + jQuery.post(this._options.sUrl, { html: html}, function(data) { + + if(data.length > 0 && data != '0') { + if(data.indexOf(" 0) { + + // Specify configuration + $config = array( + 'bare' => true, + 'clean' => true, + 'doctype' => 'strict', + 'drop-empty-paras' => true, + 'drop-font-tags' => true, + 'drop-proprietary-attributes' => true, + 'enclose-block-text' => true, + 'indent' => false, + 'join-classes' => true, + 'join-styles' => true, + 'logical-emphasis' => true, + 'output-xhtml' => true, + 'show-body-only' => true, + 'wrap' => 0); + + // Tidy + $tidy = new tidy; + $tidy->parseString($html, $config, 'utf8'); + $tidy->cleanRepair(); + + // Output + echo $tidy; +} else { + +echo ('0'); +} +?> diff --git a/websdk/static/js/wymeditor/plugins/tidy/wand.png b/websdk/static/js/wymeditor/plugins/tidy/wand.png new file mode 100644 index 0000000..bb55eea --- /dev/null +++ b/websdk/static/js/wymeditor/plugins/tidy/wand.png Binary files differ diff --git a/websdk/static/js/wymeditor/skins/.svn/entries b/websdk/static/js/wymeditor/skins/.svn/entries new file mode 100644 index 0000000..81655ae --- /dev/null +++ b/websdk/static/js/wymeditor/skins/.svn/entries @@ -0,0 +1,77 @@ +10 + +dir +677 +svn://svn.wymeditor.org/wymeditor/trunk/src/wymeditor/skins +svn://svn.wymeditor.org/wymeditor + + + +2010-06-20T14:29:05.352766Z +675 +mr_lundis + + + + + + + + + + + + + + +89e89e35-0a13-0410-8f61-920bba073fa9 + +default +dir + +silver +dir + +compact +dir + +minimal +dir + +twopanels +dir + +wymeditor_icon.png +file + + + + +2011-07-13T16:45:40.000000Z +d43650efc0228099352fc9998573dbb6 +2007-08-22T15:06:37.289602Z +413 +d.reszka +has-props + + + + + + + + + + + + + + + + + + + + +1028 + diff --git a/websdk/static/js/wymeditor/skins/.svn/prop-base/wymeditor_icon.png.svn-base b/websdk/static/js/wymeditor/skins/.svn/prop-base/wymeditor_icon.png.svn-base new file mode 100644 index 0000000..5e9587e --- /dev/null +++ b/websdk/static/js/wymeditor/skins/.svn/prop-base/wymeditor_icon.png.svn-base @@ -0,0 +1,5 @@ +K 13 +svn:mime-type +V 24 +application/octet-stream +END diff --git a/websdk/static/js/wymeditor/skins/.svn/text-base/wymeditor_icon.png.svn-base b/websdk/static/js/wymeditor/skins/.svn/text-base/wymeditor_icon.png.svn-base new file mode 100644 index 0000000..d4fc155 --- /dev/null +++ b/websdk/static/js/wymeditor/skins/.svn/text-base/wymeditor_icon.png.svn-base Binary files differ diff --git a/websdk/static/js/wymeditor/skins/compact/.svn/entries b/websdk/static/js/wymeditor/skins/compact/.svn/entries new file mode 100644 index 0000000..4218693 --- /dev/null +++ b/websdk/static/js/wymeditor/skins/compact/.svn/entries @@ -0,0 +1,130 @@ +10 + +dir +677 +svn://svn.wymeditor.org/wymeditor/trunk/src/wymeditor/skins/compact +svn://svn.wymeditor.org/wymeditor + + + +2009-05-27T19:20:55.910061Z +632 +jf.hovinne + + + + + + + + + + + + + + +89e89e35-0a13-0410-8f61-920bba073fa9 + +skin.js +file + + + + +2011-07-13T16:45:40.000000Z +eb8726f426d41a0cb87a3a6831cf2848 +2009-04-20T15:16:38.336253Z +595 +totoro + + + + + + + + + + + + + + + + + + + + + +1155 + +skin.css +file + + + + +2011-07-13T16:45:40.000000Z +85d1cb084ccc301c9306974311a86df7 +2009-05-27T19:20:55.910061Z +632 +jf.hovinne + + + + + + + + + + + + + + + + + + + + + +7937 + +icons.png +file + + + + +2011-07-13T16:45:40.000000Z +45a781288dc799f892fa517355ff80b6 +2009-04-18T11:14:41.598474Z +592 +totoro +has-props + + + + + + + + + + + + + + + + + + + + +3651 + diff --git a/websdk/static/js/wymeditor/skins/compact/.svn/prop-base/icons.png.svn-base b/websdk/static/js/wymeditor/skins/compact/.svn/prop-base/icons.png.svn-base new file mode 100644 index 0000000..5e9587e --- /dev/null +++ b/websdk/static/js/wymeditor/skins/compact/.svn/prop-base/icons.png.svn-base @@ -0,0 +1,5 @@ +K 13 +svn:mime-type +V 24 +application/octet-stream +END diff --git a/websdk/static/js/wymeditor/skins/compact/.svn/text-base/icons.png.svn-base b/websdk/static/js/wymeditor/skins/compact/.svn/text-base/icons.png.svn-base new file mode 100644 index 0000000..c6eb463 --- /dev/null +++ b/websdk/static/js/wymeditor/skins/compact/.svn/text-base/icons.png.svn-base Binary files differ diff --git a/websdk/static/js/wymeditor/skins/compact/.svn/text-base/skin.css.svn-base b/websdk/static/js/wymeditor/skins/compact/.svn/text-base/skin.css.svn-base new file mode 100644 index 0000000..4a6a0c6 --- /dev/null +++ b/websdk/static/js/wymeditor/skins/compact/.svn/text-base/skin.css.svn-base @@ -0,0 +1,134 @@ +/* + * WYMeditor : what you see is What You Mean web-based editor + * Copyright (c) 2005 - 2009 Jean-Francois Hovinne, http://www.wymeditor.org/ + * Dual licensed under the MIT (MIT-license.txt) + * and GPL (GPL-license.txt) licenses. + * + * For further information visit: + * http://www.wymeditor.org/ + * + * File Name: + * screen.css + * main stylesheet for the WYMeditor skin + * See the documentation for more info. + * + * File Authors: + * Daniel Reszka (d.reszka a-t wymeditor dotorg) + * Jean-Francois Hovinne (jf.hovinne a-t wymeditor dotorg) +*/ + +/*TRYING TO RESET STYLES THAT MAY INTERFERE WITH WYMEDITOR*/ + .wym_skin_compact p, .wym_skin_compact h2, .wym_skin_compact h3, + .wym_skin_compact ul, .wym_skin_compact li { background: transparent url(); margin: 0; padding: 0; border-width:0; list-style: none; } + + +/*HIDDEN BY DEFAULT*/ + .wym_skin_compact .wym_area_left { display: none; } + .wym_skin_compact .wym_area_right { display: none; } + + +/*TYPO*/ + .wym_skin_compact { font-size: 10px; font-family: Verdana, Arial, sans-serif; } + .wym_skin_compact h2 { font-size: 110%; /* = 11px */} + .wym_skin_compact h3 { font-size: 100%; /* = 10px */} + .wym_skin_compact li { font-size: 100%; /* = 10px */} + + +/*WYM_BOX*/ + .wym_skin_compact { border: 1px solid gray; padding: 5px} + + /*auto-clear the wym_box*/ + .wym_skin_compact:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } + * html .wym_skin_compact { height: 1%;} + + +/*WYM_HTML*/ + .wym_skin_compact .wym_html { width: 98%;} + .wym_skin_compact .wym_html textarea { font-size: 120%; width: 100%; height: 200px; border: 1px solid gray; background: white; } + + +/*WYM_IFRAME*/ + .wym_skin_compact .wym_iframe { width: 98%;} + .wym_skin_compact .wym_iframe iframe { width: 100%; height: 200px; border: 1px solid gray; background: white } + + +/*AREAS*/ + .wym_skin_compact .wym_area_left { width: 100px; float: left;} + .wym_skin_compact .wym_area_right { width: 150px; float: right;} + .wym_skin_compact .wym_area_bottom { height: 1%; clear: both;} + * html .wym_skin_compact .wym_area_main { height: 1%;} + * html .wym_skin_compact .wym_area_top { height: 1%;} + *+html .wym_skin_compact .wym_area_top { height: 1%;} + +/*SECTIONS SYSTEM*/ + + /*common defaults for all sections*/ + .wym_skin_compact .wym_section { margin-bottom: 5px; } + .wym_skin_compact .wym_section h2, + .wym_skin_compact .wym_section h3 { padding: 1px 3px; margin: 0; } + .wym_skin_compact .wym_section a { padding: 0 3px; display: block; text-decoration: none; color: black; } + .wym_skin_compact .wym_section a:hover { background-color: yellow; } + /*hide section titles by default*/ + .wym_skin_compact .wym_section h2 { display: none; } + /*disable any margin-collapse*/ + .wym_skin_compact .wym_section { padding-top: 1px; padding-bottom: 1px; } + /*auto-clear sections*/ + .wym_skin_compact .wym_section ul:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } + * html .wym_skin_compact .wym_section ul { height: 1%;} + + /*option: add this class to a section to make it render as a panel*/ + .wym_skin_compact .wym_panel { } + .wym_skin_compact .wym_panel h2 { display: block; } + + /*option: add this class to a section to make it render as a dropdown menu*/ + .wym_skin_compact .wym_dropdown h2 { display: block; } + .wym_skin_compact .wym_dropdown ul { display: none; position: absolute; background: white; } + .wym_skin_compact .wym_dropdown:hover ul, + .wym_skin_compact .wym_dropdown.hover ul { display: block; } + + /*option: add this class to a section to make its elements render buttons (icons are only available for the wym_tools section for now)*/ + .wym_skin_compact .wym_buttons li { float:left;} + .wym_skin_compact .wym_buttons a { width: 20px; height: 20px; overflow: hidden; padding: 2px } + /*image replacements*/ + .wym_skin_compact .wym_buttons li a { background: url(icons.png) no-repeat; text-indent: -9999px;} + .wym_skin_compact .wym_buttons li.wym_tools_strong a { background-position: 0 -382px;} + .wym_skin_compact .wym_buttons li.wym_tools_emphasis a { background-position: 0 -22px;} + .wym_skin_compact .wym_buttons li.wym_tools_superscript a { background-position: 0 -430px;} + .wym_skin_compact .wym_buttons li.wym_tools_subscript a { background-position: 0 -454px;} + .wym_skin_compact .wym_buttons li.wym_tools_ordered_list a { background-position: 0 -48px;} + .wym_skin_compact .wym_buttons li.wym_tools_unordered_list a{ background-position: 0 -72px;} + .wym_skin_compact .wym_buttons li.wym_tools_indent a { background-position: 0 -574px;} + .wym_skin_compact .wym_buttons li.wym_tools_outdent a { background-position: 0 -598px;} + .wym_skin_compact .wym_buttons li.wym_tools_undo a { background-position: 0 -502px;} + .wym_skin_compact .wym_buttons li.wym_tools_redo a { background-position: 0 -526px;} + .wym_skin_compact .wym_buttons li.wym_tools_link a { background-position: 0 -96px;} + .wym_skin_compact .wym_buttons li.wym_tools_unlink a { background-position: 0 -168px;} + .wym_skin_compact .wym_buttons li.wym_tools_image a { background-position: 0 -121px;} + .wym_skin_compact .wym_buttons li.wym_tools_table a { background-position: 0 -144px;} + .wym_skin_compact .wym_buttons li.wym_tools_paste a { background-position: 0 -552px;} + .wym_skin_compact .wym_buttons li.wym_tools_html a { background-position: 0 -193px;} + .wym_skin_compact .wym_buttons li.wym_tools_preview a { background-position: 0 -408px;} + +/*DECORATION*/ + .wym_skin_compact .wym_section h2 { background: #f0f0f0; border: solid gray; border-width: 0 0 1px;} + .wym_skin_compact .wym_section h2 span { color: gray;} + .wym_skin_compact .wym_panel { padding: 0; border: solid gray; border-width: 1px; background: white;} + .wym_skin_compact .wym_panel ul { margin: 2px 0 5px; } + .wym_skin_compact .wym_dropdown { padding: 0; border: solid gray; border-width: 1px 1px 0 1px; } + .wym_skin_compact .wym_dropdown ul { border: solid gray; border-width: 0 1px 1px 1px; margin-left: -1px; padding: 5px 10px 5px 3px;} + +/*DIALOGS*/ + .wym_dialog div.row { margin-bottom: 5px;} + .wym_dialog div.row input { margin-right: 5px;} + .wym_dialog div.row label { float: left; width: 150px; display: block; text-align: right; margin-right: 10px; } + .wym_dialog div.row-indent { padding-left: 160px; } + /*autoclearing*/ + .wym_dialog div.row:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } + .wym_dialog div.row { display: inline-block; } + /* Hides from IE-mac \*/ + * html .wym_dialog div.row { height: 1%; } + .wym_dialog div.row { display: block; } + /* End hide from IE-mac */ + +/*WYMEDITOR_LINK*/ + a.wym_wymeditor_link { text-indent: -9999px; float: right; display: block; width: 50px; height: 15px; background: url(../wymeditor_icon.png); overflow: hidden; text-decoration: none; } diff --git a/websdk/static/js/wymeditor/skins/compact/.svn/text-base/skin.js.svn-base b/websdk/static/js/wymeditor/skins/compact/.svn/text-base/skin.js.svn-base new file mode 100644 index 0000000..cfb7cc1 --- /dev/null +++ b/websdk/static/js/wymeditor/skins/compact/.svn/text-base/skin.js.svn-base @@ -0,0 +1,35 @@ +WYMeditor.SKINS['compact'] = { + + init: function(wym) { + + //move the containers panel to the top area + jQuery(wym._options.containersSelector + ', ' + + wym._options.classesSelector, wym._box) + .appendTo( jQuery("div.wym_area_top", wym._box) ) + .addClass("wym_dropdown") + .css({"margin-right": "10px", "width": "120px", "float": "left"}); + + //render following sections as buttons + jQuery(wym._options.toolsSelector, wym._box) + .addClass("wym_buttons") + .css({"margin-right": "10px", "float": "left"}); + + //make hover work under IE < 7 + jQuery(".wym_section", wym._box).hover(function(){ + jQuery(this).addClass("hover"); + },function(){ + jQuery(this).removeClass("hover"); + }); + + var postInit = wym._options.postInit; + wym._options.postInit = function(wym) { + + if(postInit) postInit.call(wym, wym); + var rule = { + name: 'body', + css: 'background-color: #f0f0f0;' + }; + wym.addCssRule( wym._doc.styleSheets[0], rule); + }; + } +}; diff --git a/websdk/static/js/wymeditor/skins/compact/icons.png b/websdk/static/js/wymeditor/skins/compact/icons.png new file mode 100644 index 0000000..c6eb463 --- /dev/null +++ b/websdk/static/js/wymeditor/skins/compact/icons.png Binary files differ diff --git a/websdk/static/js/wymeditor/skins/compact/skin.css b/websdk/static/js/wymeditor/skins/compact/skin.css new file mode 100644 index 0000000..4a6a0c6 --- /dev/null +++ b/websdk/static/js/wymeditor/skins/compact/skin.css @@ -0,0 +1,134 @@ +/* + * WYMeditor : what you see is What You Mean web-based editor + * Copyright (c) 2005 - 2009 Jean-Francois Hovinne, http://www.wymeditor.org/ + * Dual licensed under the MIT (MIT-license.txt) + * and GPL (GPL-license.txt) licenses. + * + * For further information visit: + * http://www.wymeditor.org/ + * + * File Name: + * screen.css + * main stylesheet for the WYMeditor skin + * See the documentation for more info. + * + * File Authors: + * Daniel Reszka (d.reszka a-t wymeditor dotorg) + * Jean-Francois Hovinne (jf.hovinne a-t wymeditor dotorg) +*/ + +/*TRYING TO RESET STYLES THAT MAY INTERFERE WITH WYMEDITOR*/ + .wym_skin_compact p, .wym_skin_compact h2, .wym_skin_compact h3, + .wym_skin_compact ul, .wym_skin_compact li { background: transparent url(); margin: 0; padding: 0; border-width:0; list-style: none; } + + +/*HIDDEN BY DEFAULT*/ + .wym_skin_compact .wym_area_left { display: none; } + .wym_skin_compact .wym_area_right { display: none; } + + +/*TYPO*/ + .wym_skin_compact { font-size: 10px; font-family: Verdana, Arial, sans-serif; } + .wym_skin_compact h2 { font-size: 110%; /* = 11px */} + .wym_skin_compact h3 { font-size: 100%; /* = 10px */} + .wym_skin_compact li { font-size: 100%; /* = 10px */} + + +/*WYM_BOX*/ + .wym_skin_compact { border: 1px solid gray; padding: 5px} + + /*auto-clear the wym_box*/ + .wym_skin_compact:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } + * html .wym_skin_compact { height: 1%;} + + +/*WYM_HTML*/ + .wym_skin_compact .wym_html { width: 98%;} + .wym_skin_compact .wym_html textarea { font-size: 120%; width: 100%; height: 200px; border: 1px solid gray; background: white; } + + +/*WYM_IFRAME*/ + .wym_skin_compact .wym_iframe { width: 98%;} + .wym_skin_compact .wym_iframe iframe { width: 100%; height: 200px; border: 1px solid gray; background: white } + + +/*AREAS*/ + .wym_skin_compact .wym_area_left { width: 100px; float: left;} + .wym_skin_compact .wym_area_right { width: 150px; float: right;} + .wym_skin_compact .wym_area_bottom { height: 1%; clear: both;} + * html .wym_skin_compact .wym_area_main { height: 1%;} + * html .wym_skin_compact .wym_area_top { height: 1%;} + *+html .wym_skin_compact .wym_area_top { height: 1%;} + +/*SECTIONS SYSTEM*/ + + /*common defaults for all sections*/ + .wym_skin_compact .wym_section { margin-bottom: 5px; } + .wym_skin_compact .wym_section h2, + .wym_skin_compact .wym_section h3 { padding: 1px 3px; margin: 0; } + .wym_skin_compact .wym_section a { padding: 0 3px; display: block; text-decoration: none; color: black; } + .wym_skin_compact .wym_section a:hover { background-color: yellow; } + /*hide section titles by default*/ + .wym_skin_compact .wym_section h2 { display: none; } + /*disable any margin-collapse*/ + .wym_skin_compact .wym_section { padding-top: 1px; padding-bottom: 1px; } + /*auto-clear sections*/ + .wym_skin_compact .wym_section ul:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } + * html .wym_skin_compact .wym_section ul { height: 1%;} + + /*option: add this class to a section to make it render as a panel*/ + .wym_skin_compact .wym_panel { } + .wym_skin_compact .wym_panel h2 { display: block; } + + /*option: add this class to a section to make it render as a dropdown menu*/ + .wym_skin_compact .wym_dropdown h2 { display: block; } + .wym_skin_compact .wym_dropdown ul { display: none; position: absolute; background: white; } + .wym_skin_compact .wym_dropdown:hover ul, + .wym_skin_compact .wym_dropdown.hover ul { display: block; } + + /*option: add this class to a section to make its elements render buttons (icons are only available for the wym_tools section for now)*/ + .wym_skin_compact .wym_buttons li { float:left;} + .wym_skin_compact .wym_buttons a { width: 20px; height: 20px; overflow: hidden; padding: 2px } + /*image replacements*/ + .wym_skin_compact .wym_buttons li a { background: url(icons.png) no-repeat; text-indent: -9999px;} + .wym_skin_compact .wym_buttons li.wym_tools_strong a { background-position: 0 -382px;} + .wym_skin_compact .wym_buttons li.wym_tools_emphasis a { background-position: 0 -22px;} + .wym_skin_compact .wym_buttons li.wym_tools_superscript a { background-position: 0 -430px;} + .wym_skin_compact .wym_buttons li.wym_tools_subscript a { background-position: 0 -454px;} + .wym_skin_compact .wym_buttons li.wym_tools_ordered_list a { background-position: 0 -48px;} + .wym_skin_compact .wym_buttons li.wym_tools_unordered_list a{ background-position: 0 -72px;} + .wym_skin_compact .wym_buttons li.wym_tools_indent a { background-position: 0 -574px;} + .wym_skin_compact .wym_buttons li.wym_tools_outdent a { background-position: 0 -598px;} + .wym_skin_compact .wym_buttons li.wym_tools_undo a { background-position: 0 -502px;} + .wym_skin_compact .wym_buttons li.wym_tools_redo a { background-position: 0 -526px;} + .wym_skin_compact .wym_buttons li.wym_tools_link a { background-position: 0 -96px;} + .wym_skin_compact .wym_buttons li.wym_tools_unlink a { background-position: 0 -168px;} + .wym_skin_compact .wym_buttons li.wym_tools_image a { background-position: 0 -121px;} + .wym_skin_compact .wym_buttons li.wym_tools_table a { background-position: 0 -144px;} + .wym_skin_compact .wym_buttons li.wym_tools_paste a { background-position: 0 -552px;} + .wym_skin_compact .wym_buttons li.wym_tools_html a { background-position: 0 -193px;} + .wym_skin_compact .wym_buttons li.wym_tools_preview a { background-position: 0 -408px;} + +/*DECORATION*/ + .wym_skin_compact .wym_section h2 { background: #f0f0f0; border: solid gray; border-width: 0 0 1px;} + .wym_skin_compact .wym_section h2 span { color: gray;} + .wym_skin_compact .wym_panel { padding: 0; border: solid gray; border-width: 1px; background: white;} + .wym_skin_compact .wym_panel ul { margin: 2px 0 5px; } + .wym_skin_compact .wym_dropdown { padding: 0; border: solid gray; border-width: 1px 1px 0 1px; } + .wym_skin_compact .wym_dropdown ul { border: solid gray; border-width: 0 1px 1px 1px; margin-left: -1px; padding: 5px 10px 5px 3px;} + +/*DIALOGS*/ + .wym_dialog div.row { margin-bottom: 5px;} + .wym_dialog div.row input { margin-right: 5px;} + .wym_dialog div.row label { float: left; width: 150px; display: block; text-align: right; margin-right: 10px; } + .wym_dialog div.row-indent { padding-left: 160px; } + /*autoclearing*/ + .wym_dialog div.row:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } + .wym_dialog div.row { display: inline-block; } + /* Hides from IE-mac \*/ + * html .wym_dialog div.row { height: 1%; } + .wym_dialog div.row { display: block; } + /* End hide from IE-mac */ + +/*WYMEDITOR_LINK*/ + a.wym_wymeditor_link { text-indent: -9999px; float: right; display: block; width: 50px; height: 15px; background: url(../wymeditor_icon.png); overflow: hidden; text-decoration: none; } diff --git a/websdk/static/js/wymeditor/skins/compact/skin.js b/websdk/static/js/wymeditor/skins/compact/skin.js new file mode 100644 index 0000000..cfb7cc1 --- /dev/null +++ b/websdk/static/js/wymeditor/skins/compact/skin.js @@ -0,0 +1,35 @@ +WYMeditor.SKINS['compact'] = { + + init: function(wym) { + + //move the containers panel to the top area + jQuery(wym._options.containersSelector + ', ' + + wym._options.classesSelector, wym._box) + .appendTo( jQuery("div.wym_area_top", wym._box) ) + .addClass("wym_dropdown") + .css({"margin-right": "10px", "width": "120px", "float": "left"}); + + //render following sections as buttons + jQuery(wym._options.toolsSelector, wym._box) + .addClass("wym_buttons") + .css({"margin-right": "10px", "float": "left"}); + + //make hover work under IE < 7 + jQuery(".wym_section", wym._box).hover(function(){ + jQuery(this).addClass("hover"); + },function(){ + jQuery(this).removeClass("hover"); + }); + + var postInit = wym._options.postInit; + wym._options.postInit = function(wym) { + + if(postInit) postInit.call(wym, wym); + var rule = { + name: 'body', + css: 'background-color: #f0f0f0;' + }; + wym.addCssRule( wym._doc.styleSheets[0], rule); + }; + } +}; diff --git a/websdk/static/js/wymeditor/skins/default/.svn/entries b/websdk/static/js/wymeditor/skins/default/.svn/entries new file mode 100644 index 0000000..3893f6d --- /dev/null +++ b/websdk/static/js/wymeditor/skins/default/.svn/entries @@ -0,0 +1,130 @@ +10 + +dir +677 +svn://svn.wymeditor.org/wymeditor/trunk/src/wymeditor/skins/default +svn://svn.wymeditor.org/wymeditor + + + +2010-06-20T14:29:05.352766Z +675 +mr_lundis + + + + + + + + + + + + + + +89e89e35-0a13-0410-8f61-920bba073fa9 + +skin.js +file + + + + +2011-07-13T16:45:39.000000Z +cbb79789e5a95dc8cbdcff294dd67ea0 +2010-06-20T14:29:05.352766Z +675 +mr_lundis + + + + + + + + + + + + + + + + + + + + + +1386 + +skin.css +file + + + + +2011-07-13T16:45:39.000000Z +c8b6d612f048cbf5c1b8a54f816dc5db +2009-05-27T19:20:55.910061Z +632 +jf.hovinne + + + + + + + + + + + + + + + + + + + + + +7890 + +icons.png +file + + + + +2011-07-13T16:45:39.000000Z +45a781288dc799f892fa517355ff80b6 +2007-03-06T13:53:36.153298Z +149 +d.reszka +has-props + + + + + + + + + + + + + + + + + + + + +3651 + diff --git a/websdk/static/js/wymeditor/skins/default/.svn/prop-base/icons.png.svn-base b/websdk/static/js/wymeditor/skins/default/.svn/prop-base/icons.png.svn-base new file mode 100644 index 0000000..5e9587e --- /dev/null +++ b/websdk/static/js/wymeditor/skins/default/.svn/prop-base/icons.png.svn-base @@ -0,0 +1,5 @@ +K 13 +svn:mime-type +V 24 +application/octet-stream +END diff --git a/websdk/static/js/wymeditor/skins/default/.svn/text-base/icons.png.svn-base b/websdk/static/js/wymeditor/skins/default/.svn/text-base/icons.png.svn-base new file mode 100644 index 0000000..c6eb463 --- /dev/null +++ b/websdk/static/js/wymeditor/skins/default/.svn/text-base/icons.png.svn-base Binary files differ diff --git a/websdk/static/js/wymeditor/skins/default/.svn/text-base/skin.css.svn-base b/websdk/static/js/wymeditor/skins/default/.svn/text-base/skin.css.svn-base new file mode 100644 index 0000000..eb4680f --- /dev/null +++ b/websdk/static/js/wymeditor/skins/default/.svn/text-base/skin.css.svn-base @@ -0,0 +1,133 @@ +/* + * WYMeditor : what you see is What You Mean web-based editor + * Copyright (c) 2005 - 2009 Jean-Francois Hovinne, http://www.wymeditor.org/ + * Dual licensed under the MIT (MIT-license.txt) + * and GPL (GPL-license.txt) licenses. + * + * For further information visit: + * http://www.wymeditor.org/ + * + * File Name: + * skin.css + * main stylesheet for the default WYMeditor skin + * See the documentation for more info. + * + * File Authors: + * Daniel Reszka (d.reszka a-t wymeditor dotorg) +*/ + +/*TRYING TO RESET STYLES THAT MAY INTERFERE WITH WYMEDITOR*/ + .wym_skin_default p, .wym_skin_default h2, .wym_skin_default h3, + .wym_skin_default ul, .wym_skin_default li { background: transparent url(); margin: 0; padding: 0; border-width:0; list-style: none; } + + +/*HIDDEN BY DEFAULT*/ + .wym_skin_default .wym_area_left { display: none; } + .wym_skin_default .wym_area_right { display: block; } + + +/*TYPO*/ + .wym_skin_default { font-size: 62.5%; font-family: Verdana, Arial, sans-serif; } + .wym_skin_default h2 { font-size: 110%; /* = 11px */} + .wym_skin_default h3 { font-size: 100%; /* = 10px */} + .wym_skin_default li { font-size: 100%; /* = 10px */} + + +/*WYM_BOX*/ + .wym_skin_default { border: 1px solid gray; background: #f2f2f2; padding: 5px} + + /*auto-clear the wym_box*/ + .wym_skin_default:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } + * html .wym_skin_default { height: 1%;} + + +/*WYM_HTML*/ + .wym_skin_default .wym_html { width: 98%;} + .wym_skin_default .wym_html textarea { width: 100%; height: 200px; border: 1px solid gray; background: white; } + + +/*WYM_IFRAME*/ + .wym_skin_default .wym_iframe { width: 98%;} + .wym_skin_default .wym_iframe iframe { width: 100%; height: 200px; border: 1px solid gray; background: white } + + +/*AREAS*/ + .wym_skin_default .wym_area_left { width: 150px; float: left;} + .wym_skin_default .wym_area_right { width: 150px; float: right;} + .wym_skin_default .wym_area_bottom { height: 1%; clear: both;} + * html .wym_skin_default .wym_area_main { height: 1%;} + * html .wym_skin_default .wym_area_top { height: 1%;} + *+html .wym_skin_default .wym_area_top { height: 1%;} + +/*SECTIONS SYSTEM*/ + + /*common defaults for all sections*/ + .wym_skin_default .wym_section { margin-bottom: 5px; } + .wym_skin_default .wym_section h2, + .wym_skin_default .wym_section h3 { padding: 1px 3px; margin: 0; } + .wym_skin_default .wym_section a { padding: 0 3px; display: block; text-decoration: none; color: black; } + .wym_skin_default .wym_section a:hover { background-color: yellow; } + /*hide section titles by default*/ + .wym_skin_default .wym_section h2 { display: none; } + /*disable any margin-collapse*/ + .wym_skin_default .wym_section { padding-top: 1px; padding-bottom: 1px; } + /*auto-clear sections*/ + .wym_skin_default .wym_section ul:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } + * html .wym_skin_default .wym_section ul { height: 1%;} + + /*option: add this class to a section to make it render as a panel*/ + .wym_skin_default .wym_panel { } + .wym_skin_default .wym_panel h2 { display: block; } + + /*option: add this class to a section to make it render as a dropdown menu*/ + .wym_skin_default .wym_dropdown h2 { display: block; } + .wym_skin_default .wym_dropdown ul { display: none; position: absolute; background: white; } + .wym_skin_default .wym_dropdown:hover ul, + .wym_skin_default .wym_dropdown.hover ul { display: block; } + + /*option: add this class to a section to make its elements render buttons (icons are only available for the wym_tools section for now)*/ + .wym_skin_default .wym_buttons li { float:left;} + .wym_skin_default .wym_buttons a { width: 20px; height: 20px; overflow: hidden; padding: 2px } + /*image replacements*/ + .wym_skin_default .wym_buttons li a { background: url(icons.png) no-repeat; text-indent: -9999px;} + .wym_skin_default .wym_buttons li.wym_tools_strong a { background-position: 0 -382px;} + .wym_skin_default .wym_buttons li.wym_tools_emphasis a { background-position: 0 -22px;} + .wym_skin_default .wym_buttons li.wym_tools_superscript a { background-position: 0 -430px;} + .wym_skin_default .wym_buttons li.wym_tools_subscript a { background-position: 0 -454px;} + .wym_skin_default .wym_buttons li.wym_tools_ordered_list a { background-position: 0 -48px;} + .wym_skin_default .wym_buttons li.wym_tools_unordered_list a{ background-position: 0 -72px;} + .wym_skin_default .wym_buttons li.wym_tools_indent a { background-position: 0 -574px;} + .wym_skin_default .wym_buttons li.wym_tools_outdent a { background-position: 0 -598px;} + .wym_skin_default .wym_buttons li.wym_tools_undo a { background-position: 0 -502px;} + .wym_skin_default .wym_buttons li.wym_tools_redo a { background-position: 0 -526px;} + .wym_skin_default .wym_buttons li.wym_tools_link a { background-position: 0 -96px;} + .wym_skin_default .wym_buttons li.wym_tools_unlink a { background-position: 0 -168px;} + .wym_skin_default .wym_buttons li.wym_tools_image a { background-position: 0 -121px;} + .wym_skin_default .wym_buttons li.wym_tools_table a { background-position: 0 -144px;} + .wym_skin_default .wym_buttons li.wym_tools_paste a { background-position: 0 -552px;} + .wym_skin_default .wym_buttons li.wym_tools_html a { background-position: 0 -193px;} + .wym_skin_default .wym_buttons li.wym_tools_preview a { background-position: 0 -408px;} + +/*DECORATION*/ + .wym_skin_default .wym_section h2 { background: #ddd; border: solid gray; border-width: 0 0 1px;} + .wym_skin_default .wym_section h2 span { color: gray;} + .wym_skin_default .wym_panel { padding: 0; border: solid gray; border-width: 1px; background: white;} + .wym_skin_default .wym_panel ul { margin: 2px 0 5px; } + .wym_skin_default .wym_dropdown { padding: 0; border: solid gray; border-width: 1px 1px 0 1px; } + .wym_skin_default .wym_dropdown ul { border: solid gray; border-width: 0 1px 1px 1px; margin-left: -1px; padding: 5px 10px 5px 3px;} + +/*DIALOGS*/ + .wym_dialog div.row { margin-bottom: 5px;} + .wym_dialog div.row input { margin-right: 5px;} + .wym_dialog div.row label { float: left; width: 150px; display: block; text-align: right; margin-right: 10px; } + .wym_dialog div.row-indent { padding-left: 160px; } + /*autoclearing*/ + .wym_dialog div.row:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } + .wym_dialog div.row { display: inline-block; } + /* Hides from IE-mac \*/ + * html .wym_dialog div.row { height: 1%; } + .wym_dialog div.row { display: block; } + /* End hide from IE-mac */ + +/*WYMEDITOR_LINK*/ + a.wym_wymeditor_link { text-indent: -9999px; float: right; display: block; width: 50px; height: 15px; background: url(../wymeditor_icon.png); overflow: hidden; text-decoration: none; } diff --git a/websdk/static/js/wymeditor/skins/default/.svn/text-base/skin.js.svn-base b/websdk/static/js/wymeditor/skins/default/.svn/text-base/skin.js.svn-base new file mode 100644 index 0000000..5f6d97e --- /dev/null +++ b/websdk/static/js/wymeditor/skins/default/.svn/text-base/skin.js.svn-base @@ -0,0 +1,40 @@ +WYMeditor.SKINS['default'] = { + + init: function(wym) { + + //render following sections as panels + jQuery(wym._box).find(wym._options.classesSelector) + .addClass("wym_panel"); + + //render following sections as buttons + jQuery(wym._box).find(wym._options.toolsSelector) + .addClass("wym_buttons"); + + //render following sections as dropdown menus + jQuery(wym._box).find(wym._options.containersSelector) + .addClass("wym_dropdown") + .find(WYMeditor.H2) + .append(" >"); + + // auto add some margin to the main area sides if left area + // or right area are not empty (if they contain sections) + jQuery(wym._box).find("div.wym_area_right ul") + .parents("div.wym_area_right").show() + .parents(wym._options.boxSelector) + .find("div.wym_area_main") + .css({"margin-right": "155px"}); + + jQuery(wym._box).find("div.wym_area_left ul") + .parents("div.wym_area_left").show() + .parents(wym._options.boxSelector) + .find("div.wym_area_main") + .css({"margin-left": "155px"}); + + //make hover work under IE < 7 + jQuery(wym._box).find(".wym_section").hover(function(){ + jQuery(this).addClass("hover"); + },function(){ + jQuery(this).removeClass("hover"); + }); + } +}; diff --git a/websdk/static/js/wymeditor/skins/default/icons.png b/websdk/static/js/wymeditor/skins/default/icons.png new file mode 100644 index 0000000..c6eb463 --- /dev/null +++ b/websdk/static/js/wymeditor/skins/default/icons.png Binary files differ diff --git a/websdk/static/js/wymeditor/skins/default/skin.css b/websdk/static/js/wymeditor/skins/default/skin.css new file mode 100644 index 0000000..eb4680f --- /dev/null +++ b/websdk/static/js/wymeditor/skins/default/skin.css @@ -0,0 +1,133 @@ +/* + * WYMeditor : what you see is What You Mean web-based editor + * Copyright (c) 2005 - 2009 Jean-Francois Hovinne, http://www.wymeditor.org/ + * Dual licensed under the MIT (MIT-license.txt) + * and GPL (GPL-license.txt) licenses. + * + * For further information visit: + * http://www.wymeditor.org/ + * + * File Name: + * skin.css + * main stylesheet for the default WYMeditor skin + * See the documentation for more info. + * + * File Authors: + * Daniel Reszka (d.reszka a-t wymeditor dotorg) +*/ + +/*TRYING TO RESET STYLES THAT MAY INTERFERE WITH WYMEDITOR*/ + .wym_skin_default p, .wym_skin_default h2, .wym_skin_default h3, + .wym_skin_default ul, .wym_skin_default li { background: transparent url(); margin: 0; padding: 0; border-width:0; list-style: none; } + + +/*HIDDEN BY DEFAULT*/ + .wym_skin_default .wym_area_left { display: none; } + .wym_skin_default .wym_area_right { display: block; } + + +/*TYPO*/ + .wym_skin_default { font-size: 62.5%; font-family: Verdana, Arial, sans-serif; } + .wym_skin_default h2 { font-size: 110%; /* = 11px */} + .wym_skin_default h3 { font-size: 100%; /* = 10px */} + .wym_skin_default li { font-size: 100%; /* = 10px */} + + +/*WYM_BOX*/ + .wym_skin_default { border: 1px solid gray; background: #f2f2f2; padding: 5px} + + /*auto-clear the wym_box*/ + .wym_skin_default:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } + * html .wym_skin_default { height: 1%;} + + +/*WYM_HTML*/ + .wym_skin_default .wym_html { width: 98%;} + .wym_skin_default .wym_html textarea { width: 100%; height: 200px; border: 1px solid gray; background: white; } + + +/*WYM_IFRAME*/ + .wym_skin_default .wym_iframe { width: 98%;} + .wym_skin_default .wym_iframe iframe { width: 100%; height: 200px; border: 1px solid gray; background: white } + + +/*AREAS*/ + .wym_skin_default .wym_area_left { width: 150px; float: left;} + .wym_skin_default .wym_area_right { width: 150px; float: right;} + .wym_skin_default .wym_area_bottom { height: 1%; clear: both;} + * html .wym_skin_default .wym_area_main { height: 1%;} + * html .wym_skin_default .wym_area_top { height: 1%;} + *+html .wym_skin_default .wym_area_top { height: 1%;} + +/*SECTIONS SYSTEM*/ + + /*common defaults for all sections*/ + .wym_skin_default .wym_section { margin-bottom: 5px; } + .wym_skin_default .wym_section h2, + .wym_skin_default .wym_section h3 { padding: 1px 3px; margin: 0; } + .wym_skin_default .wym_section a { padding: 0 3px; display: block; text-decoration: none; color: black; } + .wym_skin_default .wym_section a:hover { background-color: yellow; } + /*hide section titles by default*/ + .wym_skin_default .wym_section h2 { display: none; } + /*disable any margin-collapse*/ + .wym_skin_default .wym_section { padding-top: 1px; padding-bottom: 1px; } + /*auto-clear sections*/ + .wym_skin_default .wym_section ul:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } + * html .wym_skin_default .wym_section ul { height: 1%;} + + /*option: add this class to a section to make it render as a panel*/ + .wym_skin_default .wym_panel { } + .wym_skin_default .wym_panel h2 { display: block; } + + /*option: add this class to a section to make it render as a dropdown menu*/ + .wym_skin_default .wym_dropdown h2 { display: block; } + .wym_skin_default .wym_dropdown ul { display: none; position: absolute; background: white; } + .wym_skin_default .wym_dropdown:hover ul, + .wym_skin_default .wym_dropdown.hover ul { display: block; } + + /*option: add this class to a section to make its elements render buttons (icons are only available for the wym_tools section for now)*/ + .wym_skin_default .wym_buttons li { float:left;} + .wym_skin_default .wym_buttons a { width: 20px; height: 20px; overflow: hidden; padding: 2px } + /*image replacements*/ + .wym_skin_default .wym_buttons li a { background: url(icons.png) no-repeat; text-indent: -9999px;} + .wym_skin_default .wym_buttons li.wym_tools_strong a { background-position: 0 -382px;} + .wym_skin_default .wym_buttons li.wym_tools_emphasis a { background-position: 0 -22px;} + .wym_skin_default .wym_buttons li.wym_tools_superscript a { background-position: 0 -430px;} + .wym_skin_default .wym_buttons li.wym_tools_subscript a { background-position: 0 -454px;} + .wym_skin_default .wym_buttons li.wym_tools_ordered_list a { background-position: 0 -48px;} + .wym_skin_default .wym_buttons li.wym_tools_unordered_list a{ background-position: 0 -72px;} + .wym_skin_default .wym_buttons li.wym_tools_indent a { background-position: 0 -574px;} + .wym_skin_default .wym_buttons li.wym_tools_outdent a { background-position: 0 -598px;} + .wym_skin_default .wym_buttons li.wym_tools_undo a { background-position: 0 -502px;} + .wym_skin_default .wym_buttons li.wym_tools_redo a { background-position: 0 -526px;} + .wym_skin_default .wym_buttons li.wym_tools_link a { background-position: 0 -96px;} + .wym_skin_default .wym_buttons li.wym_tools_unlink a { background-position: 0 -168px;} + .wym_skin_default .wym_buttons li.wym_tools_image a { background-position: 0 -121px;} + .wym_skin_default .wym_buttons li.wym_tools_table a { background-position: 0 -144px;} + .wym_skin_default .wym_buttons li.wym_tools_paste a { background-position: 0 -552px;} + .wym_skin_default .wym_buttons li.wym_tools_html a { background-position: 0 -193px;} + .wym_skin_default .wym_buttons li.wym_tools_preview a { background-position: 0 -408px;} + +/*DECORATION*/ + .wym_skin_default .wym_section h2 { background: #ddd; border: solid gray; border-width: 0 0 1px;} + .wym_skin_default .wym_section h2 span { color: gray;} + .wym_skin_default .wym_panel { padding: 0; border: solid gray; border-width: 1px; background: white;} + .wym_skin_default .wym_panel ul { margin: 2px 0 5px; } + .wym_skin_default .wym_dropdown { padding: 0; border: solid gray; border-width: 1px 1px 0 1px; } + .wym_skin_default .wym_dropdown ul { border: solid gray; border-width: 0 1px 1px 1px; margin-left: -1px; padding: 5px 10px 5px 3px;} + +/*DIALOGS*/ + .wym_dialog div.row { margin-bottom: 5px;} + .wym_dialog div.row input { margin-right: 5px;} + .wym_dialog div.row label { float: left; width: 150px; display: block; text-align: right; margin-right: 10px; } + .wym_dialog div.row-indent { padding-left: 160px; } + /*autoclearing*/ + .wym_dialog div.row:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } + .wym_dialog div.row { display: inline-block; } + /* Hides from IE-mac \*/ + * html .wym_dialog div.row { height: 1%; } + .wym_dialog div.row { display: block; } + /* End hide from IE-mac */ + +/*WYMEDITOR_LINK*/ + a.wym_wymeditor_link { text-indent: -9999px; float: right; display: block; width: 50px; height: 15px; background: url(../wymeditor_icon.png); overflow: hidden; text-decoration: none; } diff --git a/websdk/static/js/wymeditor/skins/default/skin.js b/websdk/static/js/wymeditor/skins/default/skin.js new file mode 100644 index 0000000..5f6d97e --- /dev/null +++ b/websdk/static/js/wymeditor/skins/default/skin.js @@ -0,0 +1,40 @@ +WYMeditor.SKINS['default'] = { + + init: function(wym) { + + //render following sections as panels + jQuery(wym._box).find(wym._options.classesSelector) + .addClass("wym_panel"); + + //render following sections as buttons + jQuery(wym._box).find(wym._options.toolsSelector) + .addClass("wym_buttons"); + + //render following sections as dropdown menus + jQuery(wym._box).find(wym._options.containersSelector) + .addClass("wym_dropdown") + .find(WYMeditor.H2) + .append(" >"); + + // auto add some margin to the main area sides if left area + // or right area are not empty (if they contain sections) + jQuery(wym._box).find("div.wym_area_right ul") + .parents("div.wym_area_right").show() + .parents(wym._options.boxSelector) + .find("div.wym_area_main") + .css({"margin-right": "155px"}); + + jQuery(wym._box).find("div.wym_area_left ul") + .parents("div.wym_area_left").show() + .parents(wym._options.boxSelector) + .find("div.wym_area_main") + .css({"margin-left": "155px"}); + + //make hover work under IE < 7 + jQuery(wym._box).find(".wym_section").hover(function(){ + jQuery(this).addClass("hover"); + },function(){ + jQuery(this).removeClass("hover"); + }); + } +}; diff --git a/websdk/static/js/wymeditor/skins/minimal/.svn/entries b/websdk/static/js/wymeditor/skins/minimal/.svn/entries new file mode 100644 index 0000000..fb50d46 --- /dev/null +++ b/websdk/static/js/wymeditor/skins/minimal/.svn/entries @@ -0,0 +1,99 @@ +10 + +dir +677 +svn://svn.wymeditor.org/wymeditor/trunk/src/wymeditor/skins/minimal +svn://svn.wymeditor.org/wymeditor + + + +2009-05-27T19:20:55.910061Z +632 +jf.hovinne + + + + + + + + + + + + + + +89e89e35-0a13-0410-8f61-920bba073fa9 + +skin.js +file + + + + +2011-07-13T16:45:40.000000Z +4f493f73bfb815d0dfff9c39d15a14e8 +2008-06-02T20:02:11.870601Z +505 +jf.hovinne + + + + + + + + + + + + + + + + + + + + + +868 + +images +dir + +skin.css +file + + + + +2011-07-13T16:45:40.000000Z +b24eafed148e80826464943a977ba2ad +2009-05-27T19:20:55.910061Z +632 +jf.hovinne + + + + + + + + + + + + + + + + + + + + + +2743 + diff --git a/websdk/static/js/wymeditor/skins/minimal/.svn/text-base/skin.css.svn-base b/websdk/static/js/wymeditor/skins/minimal/.svn/text-base/skin.css.svn-base new file mode 100644 index 0000000..cea8d84 --- /dev/null +++ b/websdk/static/js/wymeditor/skins/minimal/.svn/text-base/skin.css.svn-base @@ -0,0 +1,131 @@ +/* + * WYMeditor : what you see is What You Mean web-based editor + * Copyright (c) 2005 - 2009 Jean-Francois Hovinne, http://www.wymeditor.org/ + * Dual licensed under the MIT (MIT-license.txt) + * and GPL (GPL-license.txt) licenses. + * + * For further information visit: + * http://www.wymeditor.org/ + * + * File Name: + * skin.css + * main stylesheet for the minimal WYMeditor skin + * See the documentation for more info. + * + * File Authors: + * Jean-Francois Hovinne + * Scott Lewis (see Silver skin) +*/ + +/* Set iframe */ +.wym_skin_minimal div.wym_iframe iframe { + width: 90%; + height: 200px; +} + +/* Hide h2 by default */ +.wym_skin_minimal h2 { + display: none; +} + +/* Show specific h2 */ +.wym_skin_minimal div.wym_tools h2, +.wym_skin_minimal div.wym_containers h2, +.wym_skin_minimal div.wym_classes h2 { + display: block; +} + +.wym_skin_minimal div.wym_section ul { + margin: 0; +} + +.wym_skin_minimal div.wym_section ul li { + float: left; + list-style-type: none; + margin-right: 5px; +} + +.wym_skin_minimal div.wym_area_top, +.wym_skin_minimal div.wym_area_right, +.wym_skin_minimal div.wym_containers, +.wym_skin_minimal div.wym_classes { + float: left; +} + +.wym_skin_minimal div.wym_area_main { + clear: both; +} + +.wym_skin_minimal div.wym_html { + width: 90%; +} + +.wym_skin_minimal textarea.wym_html_val { + width: 100%; + height: 100px; +} + +/* DROPDOWNS (see Silver skin) */ +.wym_skin_minimal div.wym_dropdown { + cursor: pointer; + margin: 0px 4px 10px 0px; + padding: 0px; + z-index: 1001; + display: block; +} + +.wym_skin_minimal div.wym_dropdown ul { + display: none; + width: 124px; + padding: 0px; + margin: 0px; + list-style-type: none; + list-style-image: none; + z-index: 1002; + position: absolute; + border-top: 1px solid #AAA; +} + +.wym_skin_minimal div.wym_dropdown ul li { + width: 146px; + height: 20px; + padding: 0px; + margin: 0px; + border: 1px solid #777; + border-top: none; + background: #EEE; + list-style-image: none; +} + +.wym_skin_minimal div.wym_dropdown h2 { + width: 138px; + height: 16px; + color: #000; + background-image: url(images/bg.selector.silver.gif); + background-position: 0px -18px; + background-repeat: no-repeat; + border: none; + font-family: "Trebuchet MS", Verdana, Arial, Helvetica, Sanserif; + font-size: 12px; + font-weight: bold; + padding: 2px 0px 0px 10px; + margin: 0px; +} + +.wym_skin_minimal div.wym_dropdown a { + text-decoration: none; + font-family: "Trebuchet MS", Verdana, Arial, Helvetica, Sanserif; + font-size: 12px; + padding: 5px 0px 0px 10px; + display: block; + width: 136px; + height: 15px; + color: #000; + text-align: left; + margin-left: 0px; +} + +.wym_skin_minimal div.wym_dropdown a:hover { + background: #BBB; + border-bottom: none; +} diff --git a/websdk/static/js/wymeditor/skins/minimal/.svn/text-base/skin.js.svn-base b/websdk/static/js/wymeditor/skins/minimal/.svn/text-base/skin.js.svn-base new file mode 100644 index 0000000..af29ed4 --- /dev/null +++ b/websdk/static/js/wymeditor/skins/minimal/.svn/text-base/skin.js.svn-base @@ -0,0 +1,30 @@ +jQuery.fn.selectify = function() { + return this.each(function() { + jQuery(this).hover( + function() { + jQuery("h2", this).css("background-position", "0px -18px"); + jQuery("ul", this).fadeIn("fast"); + }, + function() { + jQuery("h2", this).css("background-position", ""); + jQuery("ul", this).fadeOut("fast"); + } + ); + }); +}; + +WYMeditor.SKINS['minimal'] = { + //placeholder for the skin JS, if needed + + //init the skin + //wym is the WYMeditor.editor instance + init: function(wym) { + + //render following sections as dropdown menus + jQuery(wym._box).find(wym._options.toolsSelector + ', ' + wym._options.containersSelector + ', ' + wym._options.classesSelector) + .addClass("wym_dropdown") + .selectify(); + + + } +}; diff --git a/websdk/static/js/wymeditor/skins/minimal/images/.svn/entries b/websdk/static/js/wymeditor/skins/minimal/images/.svn/entries new file mode 100644 index 0000000..ff30362 --- /dev/null +++ b/websdk/static/js/wymeditor/skins/minimal/images/.svn/entries @@ -0,0 +1,164 @@ +10 + +dir +677 +svn://svn.wymeditor.org/wymeditor/trunk/src/wymeditor/skins/minimal/images +svn://svn.wymeditor.org/wymeditor + + + +2008-06-02T20:02:11.870601Z +505 +jf.hovinne + + + + + + + + + + + + + + +89e89e35-0a13-0410-8f61-920bba073fa9 + +icons.silver.gif +file + + + + +2011-07-13T16:45:40.000000Z +3d55143203f242061d02ed4387e3c498 +2008-06-02T20:02:11.870601Z +505 +jf.hovinne +has-props + + + + + + + + + + + + + + + + + + + + +15382 + +bg.header.gif +file + + + + +2011-07-13T16:45:40.000000Z +4871b677b0af34f02d3a51046dd51f20 +2008-06-02T20:02:11.870601Z +505 +jf.hovinne +has-props + + + + + + + + + + + + + + + + + + + + +781 + +bg.wymeditor.png +file + + + + +2011-07-13T16:45:40.000000Z +dae577218f4bdd6f59197e3d8c8c9ea6 +2008-06-02T20:02:11.870601Z +505 +jf.hovinne +has-props + + + + + + + + + + + + + + + + + + + + +498 + +bg.selector.silver.gif +file + + + + +2011-07-13T16:45:40.000000Z +1101554f412121db5ad6157f366515e3 +2008-06-02T20:02:11.870601Z +505 +jf.hovinne +has-props + + + + + + + + + + + + + + + + + + + + +1621 + diff --git a/websdk/static/js/wymeditor/skins/minimal/images/.svn/prop-base/bg.header.gif.svn-base b/websdk/static/js/wymeditor/skins/minimal/images/.svn/prop-base/bg.header.gif.svn-base new file mode 100644 index 0000000..5e9587e --- /dev/null +++ b/websdk/static/js/wymeditor/skins/minimal/images/.svn/prop-base/bg.header.gif.svn-base @@ -0,0 +1,5 @@ +K 13 +svn:mime-type +V 24 +application/octet-stream +END diff --git a/websdk/static/js/wymeditor/skins/minimal/images/.svn/prop-base/bg.selector.silver.gif.svn-base b/websdk/static/js/wymeditor/skins/minimal/images/.svn/prop-base/bg.selector.silver.gif.svn-base new file mode 100644 index 0000000..5e9587e --- /dev/null +++ b/websdk/static/js/wymeditor/skins/minimal/images/.svn/prop-base/bg.selector.silver.gif.svn-base @@ -0,0 +1,5 @@ +K 13 +svn:mime-type +V 24 +application/octet-stream +END diff --git a/websdk/static/js/wymeditor/skins/minimal/images/.svn/prop-base/bg.wymeditor.png.svn-base b/websdk/static/js/wymeditor/skins/minimal/images/.svn/prop-base/bg.wymeditor.png.svn-base new file mode 100644 index 0000000..5e9587e --- /dev/null +++ b/websdk/static/js/wymeditor/skins/minimal/images/.svn/prop-base/bg.wymeditor.png.svn-base @@ -0,0 +1,5 @@ +K 13 +svn:mime-type +V 24 +application/octet-stream +END diff --git a/websdk/static/js/wymeditor/skins/minimal/images/.svn/prop-base/icons.silver.gif.svn-base b/websdk/static/js/wymeditor/skins/minimal/images/.svn/prop-base/icons.silver.gif.svn-base new file mode 100644 index 0000000..5e9587e --- /dev/null +++ b/websdk/static/js/wymeditor/skins/minimal/images/.svn/prop-base/icons.silver.gif.svn-base @@ -0,0 +1,5 @@ +K 13 +svn:mime-type +V 24 +application/octet-stream +END diff --git a/websdk/static/js/wymeditor/skins/minimal/images/.svn/text-base/bg.header.gif.svn-base b/websdk/static/js/wymeditor/skins/minimal/images/.svn/text-base/bg.header.gif.svn-base new file mode 100644 index 0000000..b2d2907 --- /dev/null +++ b/websdk/static/js/wymeditor/skins/minimal/images/.svn/text-base/bg.header.gif.svn-base Binary files differ diff --git a/websdk/static/js/wymeditor/skins/minimal/images/.svn/text-base/bg.selector.silver.gif.svn-base b/websdk/static/js/wymeditor/skins/minimal/images/.svn/text-base/bg.selector.silver.gif.svn-base new file mode 100644 index 0000000..e65976b --- /dev/null +++ b/websdk/static/js/wymeditor/skins/minimal/images/.svn/text-base/bg.selector.silver.gif.svn-base Binary files differ diff --git a/websdk/static/js/wymeditor/skins/minimal/images/.svn/text-base/bg.wymeditor.png.svn-base b/websdk/static/js/wymeditor/skins/minimal/images/.svn/text-base/bg.wymeditor.png.svn-base new file mode 100644 index 0000000..1e84813 --- /dev/null +++ b/websdk/static/js/wymeditor/skins/minimal/images/.svn/text-base/bg.wymeditor.png.svn-base Binary files differ diff --git a/websdk/static/js/wymeditor/skins/minimal/images/.svn/text-base/icons.silver.gif.svn-base b/websdk/static/js/wymeditor/skins/minimal/images/.svn/text-base/icons.silver.gif.svn-base new file mode 100644 index 0000000..8c6a4fb --- /dev/null +++ b/websdk/static/js/wymeditor/skins/minimal/images/.svn/text-base/icons.silver.gif.svn-base Binary files differ diff --git a/websdk/static/js/wymeditor/skins/minimal/images/bg.header.gif b/websdk/static/js/wymeditor/skins/minimal/images/bg.header.gif new file mode 100644 index 0000000..b2d2907 --- /dev/null +++ b/websdk/static/js/wymeditor/skins/minimal/images/bg.header.gif Binary files differ diff --git a/websdk/static/js/wymeditor/skins/minimal/images/bg.selector.silver.gif b/websdk/static/js/wymeditor/skins/minimal/images/bg.selector.silver.gif new file mode 100644 index 0000000..e65976b --- /dev/null +++ b/websdk/static/js/wymeditor/skins/minimal/images/bg.selector.silver.gif Binary files differ diff --git a/websdk/static/js/wymeditor/skins/minimal/images/bg.wymeditor.png b/websdk/static/js/wymeditor/skins/minimal/images/bg.wymeditor.png new file mode 100644 index 0000000..1e84813 --- /dev/null +++ b/websdk/static/js/wymeditor/skins/minimal/images/bg.wymeditor.png Binary files differ diff --git a/websdk/static/js/wymeditor/skins/minimal/images/icons.silver.gif b/websdk/static/js/wymeditor/skins/minimal/images/icons.silver.gif new file mode 100644 index 0000000..8c6a4fb --- /dev/null +++ b/websdk/static/js/wymeditor/skins/minimal/images/icons.silver.gif Binary files differ diff --git a/websdk/static/js/wymeditor/skins/minimal/skin.css b/websdk/static/js/wymeditor/skins/minimal/skin.css new file mode 100644 index 0000000..cea8d84 --- /dev/null +++ b/websdk/static/js/wymeditor/skins/minimal/skin.css @@ -0,0 +1,131 @@ +/* + * WYMeditor : what you see is What You Mean web-based editor + * Copyright (c) 2005 - 2009 Jean-Francois Hovinne, http://www.wymeditor.org/ + * Dual licensed under the MIT (MIT-license.txt) + * and GPL (GPL-license.txt) licenses. + * + * For further information visit: + * http://www.wymeditor.org/ + * + * File Name: + * skin.css + * main stylesheet for the minimal WYMeditor skin + * See the documentation for more info. + * + * File Authors: + * Jean-Francois Hovinne + * Scott Lewis (see Silver skin) +*/ + +/* Set iframe */ +.wym_skin_minimal div.wym_iframe iframe { + width: 90%; + height: 200px; +} + +/* Hide h2 by default */ +.wym_skin_minimal h2 { + display: none; +} + +/* Show specific h2 */ +.wym_skin_minimal div.wym_tools h2, +.wym_skin_minimal div.wym_containers h2, +.wym_skin_minimal div.wym_classes h2 { + display: block; +} + +.wym_skin_minimal div.wym_section ul { + margin: 0; +} + +.wym_skin_minimal div.wym_section ul li { + float: left; + list-style-type: none; + margin-right: 5px; +} + +.wym_skin_minimal div.wym_area_top, +.wym_skin_minimal div.wym_area_right, +.wym_skin_minimal div.wym_containers, +.wym_skin_minimal div.wym_classes { + float: left; +} + +.wym_skin_minimal div.wym_area_main { + clear: both; +} + +.wym_skin_minimal div.wym_html { + width: 90%; +} + +.wym_skin_minimal textarea.wym_html_val { + width: 100%; + height: 100px; +} + +/* DROPDOWNS (see Silver skin) */ +.wym_skin_minimal div.wym_dropdown { + cursor: pointer; + margin: 0px 4px 10px 0px; + padding: 0px; + z-index: 1001; + display: block; +} + +.wym_skin_minimal div.wym_dropdown ul { + display: none; + width: 124px; + padding: 0px; + margin: 0px; + list-style-type: none; + list-style-image: none; + z-index: 1002; + position: absolute; + border-top: 1px solid #AAA; +} + +.wym_skin_minimal div.wym_dropdown ul li { + width: 146px; + height: 20px; + padding: 0px; + margin: 0px; + border: 1px solid #777; + border-top: none; + background: #EEE; + list-style-image: none; +} + +.wym_skin_minimal div.wym_dropdown h2 { + width: 138px; + height: 16px; + color: #000; + background-image: url(images/bg.selector.silver.gif); + background-position: 0px -18px; + background-repeat: no-repeat; + border: none; + font-family: "Trebuchet MS", Verdana, Arial, Helvetica, Sanserif; + font-size: 12px; + font-weight: bold; + padding: 2px 0px 0px 10px; + margin: 0px; +} + +.wym_skin_minimal div.wym_dropdown a { + text-decoration: none; + font-family: "Trebuchet MS", Verdana, Arial, Helvetica, Sanserif; + font-size: 12px; + padding: 5px 0px 0px 10px; + display: block; + width: 136px; + height: 15px; + color: #000; + text-align: left; + margin-left: 0px; +} + +.wym_skin_minimal div.wym_dropdown a:hover { + background: #BBB; + border-bottom: none; +} diff --git a/websdk/static/js/wymeditor/skins/minimal/skin.js b/websdk/static/js/wymeditor/skins/minimal/skin.js new file mode 100644 index 0000000..af29ed4 --- /dev/null +++ b/websdk/static/js/wymeditor/skins/minimal/skin.js @@ -0,0 +1,30 @@ +jQuery.fn.selectify = function() { + return this.each(function() { + jQuery(this).hover( + function() { + jQuery("h2", this).css("background-position", "0px -18px"); + jQuery("ul", this).fadeIn("fast"); + }, + function() { + jQuery("h2", this).css("background-position", ""); + jQuery("ul", this).fadeOut("fast"); + } + ); + }); +}; + +WYMeditor.SKINS['minimal'] = { + //placeholder for the skin JS, if needed + + //init the skin + //wym is the WYMeditor.editor instance + init: function(wym) { + + //render following sections as dropdown menus + jQuery(wym._box).find(wym._options.toolsSelector + ', ' + wym._options.containersSelector + ', ' + wym._options.classesSelector) + .addClass("wym_dropdown") + .selectify(); + + + } +}; diff --git a/websdk/static/js/wymeditor/skins/silver/.svn/entries b/websdk/static/js/wymeditor/skins/silver/.svn/entries new file mode 100644 index 0000000..2969c6b --- /dev/null +++ b/websdk/static/js/wymeditor/skins/silver/.svn/entries @@ -0,0 +1,167 @@ +10 + +dir +677 +svn://svn.wymeditor.org/wymeditor/trunk/src/wymeditor/skins/silver +svn://svn.wymeditor.org/wymeditor + + + +2009-05-27T19:20:55.910061Z +632 +jf.hovinne + + + + + + + + + + + + + + +89e89e35-0a13-0410-8f61-920bba073fa9 + +skin.js +file + + + + +2011-07-13T16:45:39.000000Z +7f55ea883250c072ce72cf60c2d77064 +2008-05-12T20:05:54.287558Z +494 +jf.hovinne + + + + + + + + + + + + + + + + + + + + + +2024 + +images +dir + +COPYING +file + + + + +2011-07-13T16:45:39.000000Z +d32239bcb673463ab874e80d47fae504 +2008-05-12T20:05:54.287558Z +494 +jf.hovinne +has-props + + + + + + + + + + + + + + + + + + + + +35147 + +skin.css +file + + + + +2011-07-13T16:45:39.000000Z +4af1f9844350d87d34bfeeca2702ea0c +2009-05-27T19:20:55.910061Z +632 +jf.hovinne + + + + + + + + + + + + + + + + + + + + + +13023 + +README +file + + + + +2011-07-13T16:45:39.000000Z +649465d1c305caedb34adc02b8cc0ea8 +2008-05-12T20:05:54.287558Z +494 +jf.hovinne +has-props + + + + + + + + + + + + + + + + + + + + +725 + diff --git a/websdk/static/js/wymeditor/skins/silver/.svn/prop-base/COPYING.svn-base b/websdk/static/js/wymeditor/skins/silver/.svn/prop-base/COPYING.svn-base new file mode 100644 index 0000000..869ac71 --- /dev/null +++ b/websdk/static/js/wymeditor/skins/silver/.svn/prop-base/COPYING.svn-base @@ -0,0 +1,5 @@ +K 14 +svn:executable +V 1 +* +END diff --git a/websdk/static/js/wymeditor/skins/silver/.svn/prop-base/README.svn-base b/websdk/static/js/wymeditor/skins/silver/.svn/prop-base/README.svn-base new file mode 100644 index 0000000..869ac71 --- /dev/null +++ b/websdk/static/js/wymeditor/skins/silver/.svn/prop-base/README.svn-base @@ -0,0 +1,5 @@ +K 14 +svn:executable +V 1 +* +END diff --git a/websdk/static/js/wymeditor/skins/silver/.svn/text-base/COPYING.svn-base b/websdk/static/js/wymeditor/skins/silver/.svn/text-base/COPYING.svn-base new file mode 100644 index 0000000..94a9ed0 --- /dev/null +++ b/websdk/static/js/wymeditor/skins/silver/.svn/text-base/COPYING.svn-base @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. 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 +them 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 prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. 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. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey 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; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If 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 convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU 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 that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + 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. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +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. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + 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 +state 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 3 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, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program 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, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU 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 Lesser General +Public License instead of this License. But first, please read +. diff --git a/websdk/static/js/wymeditor/skins/silver/.svn/text-base/README.svn-base b/websdk/static/js/wymeditor/skins/silver/.svn/text-base/README.svn-base new file mode 100644 index 0000000..130dc46 --- /dev/null +++ b/websdk/static/js/wymeditor/skins/silver/.svn/text-base/README.svn-base @@ -0,0 +1,27 @@ +/** +* @version Alpha 0.1 2008-05-10 23:28:43 $ +* @package Silver skin for WYMeditor +* @copyright Copyright (C) 2008 Scott Edwin Lewis. All rights reserved. +* @license GNU/GPL, see COPYING +* Silver skin for WYMeditor is free software and is licensed under the +* GNU General Public License. See COPYING for copyright notices and details. +*/ + +Adds custom buttons and color palette to the WYMeditor XHTML Editor. + +INSTALLATION: + +1. Copy the entire /silver/ directory to /wymeditor/skins/ +2. Initialize the WYMeditor 'skin' option as below: + + + +That's it. You're done. diff --git a/websdk/static/js/wymeditor/skins/silver/.svn/text-base/skin.css.svn-base b/websdk/static/js/wymeditor/skins/silver/.svn/text-base/skin.css.svn-base new file mode 100644 index 0000000..8284d81 --- /dev/null +++ b/websdk/static/js/wymeditor/skins/silver/.svn/text-base/skin.css.svn-base @@ -0,0 +1,297 @@ +/* + * WYMeditor : what you see is What You Mean web-based editor + * Copyright (c) 2005 - 2009 Jean-Francois Hovinne, http://www.wymeditor.org/ + * Dual licensed under the MIT (MIT-license.txt) + * and GPL (GPL-license.txt) licenses. + * + * For further information visit: + * http://www.wymeditor.org/ + * + * File Name: + * screen.css + * main stylesheet for the default WYMeditor skin + * See the documentation for more info. + * + * File Authors: + * Daniel Reszka (d.reszka a-t wymeditor dotorg) + * Scott Edwin Lewis +*/ + +/*TRYING TO RESET STYLES THAT MAY INTERFERE WITH WYMEDITOR*/ + .wym_skin_silver p, .wym_skin_silver h2, .wym_skin_silver h3, + .wym_skin_silver ul, .wym_skin_silver li { background: transparent url(); margin: 0; padding: 0; border-width:0; list-style: none; } + + +/*HIDDEN BY DEFAULT*/ + .wym_skin_silver .wym_area_left { display: none; } + .wym_skin_silver .wym_area_right { display: block; } + + +/*TYPO*/ + .wym_skin_silver { font-size: 62.5%; font-family: Verdana, Arial, sans-serif; } + .wym_skin_silver h2 { font-size: 110%; /* = 11px */} + .wym_skin_silver h3 { font-size: 100%; /* = 10px */} + .wym_skin_silver li { font-size: 100%; /* = 10px */} + + +/*WYM_BOX*/ + .wym_skin_silver { border: 1px solid gray; background: #f2f2f2; padding: 0px; margin: 0px;} + + /*auto-clear the wym_box*/ + .wym_skin_silver:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } + * html .wym_skin_silver { height: 1%;} + + +/*WYM_HTML*/ + .wym_skin_silver .wym_html { width: 98%;} + .wym_skin_silver .wym_html textarea { width: 100%; height: 200px; border: 1px solid gray; background: white; } + + +/*WYM_IFRAME*/ + .wym_skin_silver .wym_iframe { width: 98%;} + .wym_skin_silver .wym_iframe iframe { width: 100%; height: 200px; border: 1px solid gray; background: white } + + +/*AREAS*/ + .wym_skin_silver .wym_area_left { width: 150px; float: left;} + .wym_skin_silver .wym_area_right { width: 150px; float: right;} + .wym_skin_silver .wym_area_bottom { height: 1%; clear: both;} + * html .wym_skin_silver .wym_area_main { height: 1%;} + * html .wym_skin_silver .wym_area_top { height: 1%;} + *+html .wym_skin_silver .wym_area_top { height: 1%;} + +/*SECTIONS SYSTEM*/ + + /*common defaults for all sections*/ + .wym_skin_silver .wym_section { margin-bottom: 5px; } + .wym_skin_silver .wym_section h2, + .wym_skin_silver .wym_section h3 { padding: 1px 3px; margin: 0; cursor: pointer; } + .wym_skin_silver .wym_section a { padding: 5px 0px 0px 10px; display: block; text-decoration: none; color: black; } + .wym_skin_silver .wym_section a:hover { /*background-color: #DDD;*/} + /*hide section titles by default*/ + .wym_skin_silver .wym_section h2 { display: none; } + /*disable any margin-collapse*/ + .wym_skin_silver .wym_section { padding-top: 1px; padding-bottom: 1px; } + /*auto-clear sections*/ + .wym_skin_silver .wym_section ul:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; padding: 0px; } + * html .wym_skin_silver .wym_section ul { height: 1%;} + .wym_skin_silver .wym_section li {} + + /*option: add this class to a section to make it render as a panel*/ + .wym_skin_silver .wym_panel { } + .wym_skin_silver .wym_panel h2 { display: block; font-size: 11px; } + + /*option: add this class to a section to make it render as a dropdown menu*/ + .wym_skin_silver .wym_dropdown h2 { display: block; font-size: 11px;} + .wym_skin_silver .wym_dropdown ul { position: absolute; background: white; padding: 0px;} + .wym_skin_silver .wym_dropdown:hover ul, + .wym_skin_silver .wym_dropdown.hover ul { cursor: pointer;} + .wym_skin_silver .wym_dropdown ul li a {/*border-bottom: 1px solid #AAA;*/} + + /*option: add this class to a section to make its elements render buttons (icons are only available for the wym_tools section for now)*/ + .wym_skin_silver .wym_buttons li { float:left;} + .wym_skin_silver .wym_buttons a { width: 20px; height: 20px; overflow: hidden; padding: 2px; text-decoration: none !important; border: 1px solid #666; } + .wym_skin_silver .wym_buttons a:hover { text-decoration: none !important; border: 1px solid #000;} + /*image replacements*/ + .wym_skin_silver .wym_buttons li a { background: url(images/icons.silver.gif) no-repeat; text-indent: -9999px;} + .wym_skin_silver .wym_buttons li.wym_tools_strong a { background-position: 0 -384px;} + .wym_skin_silver .wym_buttons li.wym_tools_emphasis a { background-position: 0 -24px;} + .wym_skin_silver .wym_buttons li.wym_tools_superscript a { background-position: 0 -432px;} + .wym_skin_silver .wym_buttons li.wym_tools_subscript a { background-position: 0 -456px;} + .wym_skin_silver .wym_buttons li.wym_tools_ordered_list a { background-position: 0 -48px;} + .wym_skin_silver .wym_buttons li.wym_tools_unordered_list a{ background-position: 0 -72px;} + .wym_skin_silver .wym_buttons li.wym_tools_indent a { background-position: 0 -600px;} + .wym_skin_silver .wym_buttons li.wym_tools_outdent a { background-position: 0 -624px;} + .wym_skin_silver .wym_buttons li.wym_tools_undo a { background-position: 0 -504px;} + .wym_skin_silver .wym_buttons li.wym_tools_redo a { background-position: 0 -528px;} + .wym_skin_silver .wym_buttons li.wym_tools_link a { background-position: 0 -96px;} + .wym_skin_silver .wym_buttons li.wym_tools_unlink a { background-position: 0 -168px;} + .wym_skin_silver .wym_buttons li.wym_tools_image a { background-position: 0 -120px;} + .wym_skin_silver .wym_buttons li.wym_tools_table a { background-position: 0 -144px;} + .wym_skin_silver .wym_buttons li.wym_tools_paste a { background-position: 0 -552px;} + .wym_skin_silver .wym_buttons li.wym_tools_html a { background-position: 0 -192px;} + .wym_skin_silver .wym_buttons li.wym_tools_preview a { background-position: 0 -408px;} + .wym_skin_silver .wym_buttons li.wym_tools_gadget a { background-position: 0 -576px;} + + .wym_skin_silver .wym_buttons li.wym_tools_strong a:hover { background-position: -24px -384px;} + .wym_skin_silver .wym_buttons li.wym_tools_emphasis a:hover { background-position: -24px -24px;} + .wym_skin_silver .wym_buttons li.wym_tools_superscript a:hover { background-position: -24px -432px;} + .wym_skin_silver .wym_buttons li.wym_tools_subscript a:hover { background-position: -24px -456px;} + .wym_skin_silver .wym_buttons li.wym_tools_ordered_list a:hover { background-position: -24px -48px;} + .wym_skin_silver .wym_buttons li.wym_tools_unordered_list a:hover{ background-position: -24px -72px;} + .wym_skin_silver .wym_buttons li.wym_tools_indent a:hover { background-position: -24px -600px;} + .wym_skin_silver .wym_buttons li.wym_tools_outdent a:hover { background-position: -24px -624px;} + .wym_skin_silver .wym_buttons li.wym_tools_undo a:hover { background-position: -24px -504px;} + .wym_skin_silver .wym_buttons li.wym_tools_redo a:hover { background-position: -24px -528px;} + .wym_skin_silver .wym_buttons li.wym_tools_link a:hover { background-position: -24px -96px;} + .wym_skin_silver .wym_buttons li.wym_tools_unlink a:hover { background-position: -24px -168px;} + .wym_skin_silver .wym_buttons li.wym_tools_image a:hover { background-position: -24px -120px;} + .wym_skin_silver .wym_buttons li.wym_tools_table a:hover { background-position: -24px -144px;} + .wym_skin_silver .wym_buttons li.wym_tools_paste a:hover { background-position: -24px -552px;} + .wym_skin_silver .wym_buttons li.wym_tools_html a:hover { background-position: -24px -192px;} + .wym_skin_silver .wym_buttons li.wym_tools_preview a:hover { background-position: -24px -408px;} + .wym_skin_silver .wym_buttons li.wym_tools_gadget a:hover { background-position: -24px -576px;} + +/*DECORATION*/ + .wym_skin_silver .wym_section h2 { background: #ddd; border: none;} + .wym_skin_silver .wym_section h2 span { color: gray;} + .wym_skin_silver .wym_panel { padding: 0; border: solid gray; border-width: 0px;} + .wym_skin_silver .wym_panel ul { margin: 2px 0 5px; } + .wym_skin_silver .wym_dropdown { padding: 0; border: none; } + .wym_skin_silver .wym_dropdown ul { border: none; margin-left: -1px; padding: 0px;} + +/*DIALOGS*/ + .wym_dialog div.row { margin-bottom: 5px;} + .wym_dialog div.row input { margin-right: 5px;} + .wym_dialog div.row label { float: left; width: 150px; display: block; text-align: right; margin-right: 10px; } + .wym_dialog div.row-indent { padding-left: 160px; } + /*autoclearing*/ + .wym_dialog div.row:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } + .wym_dialog div.row { display: inline-block; } + /* Hides from IE-mac \*/ + * html .wym_dialog div.row { height: 1%; } + .wym_dialog div.row { display: block; } + /* End hide from IE-mac */ + +/*WYMEDITOR_LINK*/ + a.wym_wymeditor_link + { + text-indent: -9999px; + float: right; + display: block; + width: 50px; + height: 15px; + background: url(../wymeditor_icon.png); + background-position: 1px 1px; + background-repeat: no-repeat; + overflow: hidden; + text-decoration: none; + padding: 1px !important; + border: 1px solid #333 !important; + background-color: #FFF !important; + } + +.wym_box +{ + padding: 0px !important; + margin: 0px; +} +.wym_inner +{ + border-left: 1px solid #FFF; + border-top: 1px solid #FFF; + border-right: 1px solid #FFF; + border-bottom: 1px solid #FFF; + padding: 5px; + background-color: #B8C1C4; + height: auto; +} + +.clear {clear: both;} + +div.wym_dropdown +{ + cursor: pointer; + width: 138px !important; + margin: 0px 4px 10px 0px !important; + padding: 0px; + z-index: 1001; + display: block; + border: 1px solid red; +} + +div.wym_dropdown ul +{ + display: none; + width: 124px; + padding: 0px !important; + margin: 0px !important; + list-style-type: none; + list-style-image: none; + z-index: 1002; + position: absolute; + border-top: 1px solid #AAA; +} + +div.wym_dropdown ul li +{ + width: 146px; + height: 20px; + padding: 0px !important; + margin: 0px; + border: 1px solid #777; + border-top: none; + background: #DDD; + list-style-image: none; +} + +div.wym_dropdown h2 +{ + width: 138px; + height: 16px; + color: #000 !important; + background-image: url(images/bg.selector.silver.gif) !important; + background-position: 0px -18px; + background-repeat: no-repeat; + border: none; + font-family: "Trebuchet MS", Verdana, Arial, Helvetica, Sanserif; + font-size: 12px !important; + font-weight: bold !important; + padding: 2px 0px 0px 10px !important; + margin: 0px !important; +} + +.wym_skin_silver .wym_panel h2 +{ + width: 138px; + height: 16px; + color: #000 !important; + background-image: url(images/bg.header.gif) !important; + background-position: 0px 0px; + background-repeat: no-repeat; + border: none; + font-family: "Trebuchet MS", Verdana, Arial, Helvetica, Sanserif; + font-size: 12px !important; + font-weight: bold !important; + padding: 2px 0px 0px 10px !important; + margin: 0px !important; +} + +.wym_skin_silver .wym_panel ul +{ + margin-top: 0px !important; +} + +.wym_skin_silver .wym_panel ul li +{ + width: 146px; + height: 20px; + padding: 0px !important; + margin: 0px; + border: 1px solid #777; + border-top: none; + background: #DDD; + list-style-image: none; +} + +.wym_skin_silver .wym_panel a, +div.wym_dropdown a +{ + text-decoration: none; + font-family: "Trebuchet MS", Verdana, Arial, Helvetica, Sanserif; + font-size: 12px; + padding: 5px 0px 0px 10px !important; + display: block; + width: 136px; + height: 15px; + color: #000; + text-align: left !important; + margin-left: 0px !important; +} + +div.wym_dropdown a:hover, +.wym_skin_silver .wym_panel a:hover +{ + background: #BBB; + border-bottom: none !important; +} diff --git a/websdk/static/js/wymeditor/skins/silver/.svn/text-base/skin.js.svn-base b/websdk/static/js/wymeditor/skins/silver/.svn/text-base/skin.js.svn-base new file mode 100644 index 0000000..948ed91 --- /dev/null +++ b/websdk/static/js/wymeditor/skins/silver/.svn/text-base/skin.js.svn-base @@ -0,0 +1,61 @@ +/* This file is part of the Silver skin for WYMeditor by Scott Edwin Lewis */ + +jQuery.fn.selectify = function() { + return this.each(function() { + jQuery(this).hover( + function() { + jQuery("h2", this).css("background-position", "0px -18px"); + jQuery("ul", this).fadeIn("fast"); + }, + function() { + jQuery("h2", this).css("background-position", ""); + jQuery("ul", this).fadeOut("fast"); + } + ); + }); +}; + +WYMeditor.SKINS['silver'] = { + + init: function(wym) { + + //add some elements to improve the rendering + jQuery(wym._box) + .append('
    ') + .wrapInner('
    '); + + //render following sections as panels + jQuery(wym._box).find(wym._options.classesSelector) + .addClass("wym_panel"); + + //render following sections as buttons + jQuery(wym._box).find(wym._options.toolsSelector) + .addClass("wym_buttons"); + + //render following sections as dropdown menus + jQuery(wym._box).find(wym._options.containersSelector) + .addClass("wym_dropdown") + .selectify(); + + // auto add some margin to the main area sides if left area + // or right area are not empty (if they contain sections) + jQuery(wym._box).find("div.wym_area_right ul") + .parents("div.wym_area_right").show() + .parents(wym._options.boxSelector) + .find("div.wym_area_main") + .css({"margin-right": "155px"}); + + jQuery(wym._box).find("div.wym_area_left ul") + .parents("div.wym_area_left").show() + .parents(wym._options.boxSelector) + .find("div.wym_area_main") + .css({"margin-left": "155px"}); + + //make hover work under IE < 7 + jQuery(wym._box).find(".wym_section").hover(function(){ + jQuery(this).addClass("hover"); + },function(){ + jQuery(this).removeClass("hover"); + }); + } +}; diff --git a/websdk/static/js/wymeditor/skins/silver/COPYING b/websdk/static/js/wymeditor/skins/silver/COPYING new file mode 100755 index 0000000..94a9ed0 --- /dev/null +++ b/websdk/static/js/wymeditor/skins/silver/COPYING @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. 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 +them 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 prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. 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. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey 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; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If 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 convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU 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 that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + 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. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +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. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + 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 +state 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 3 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, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program 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, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU 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 Lesser General +Public License instead of this License. But first, please read +. diff --git a/websdk/static/js/wymeditor/skins/silver/README b/websdk/static/js/wymeditor/skins/silver/README new file mode 100755 index 0000000..130dc46 --- /dev/null +++ b/websdk/static/js/wymeditor/skins/silver/README @@ -0,0 +1,27 @@ +/** +* @version Alpha 0.1 2008-05-10 23:28:43 $ +* @package Silver skin for WYMeditor +* @copyright Copyright (C) 2008 Scott Edwin Lewis. All rights reserved. +* @license GNU/GPL, see COPYING +* Silver skin for WYMeditor is free software and is licensed under the +* GNU General Public License. See COPYING for copyright notices and details. +*/ + +Adds custom buttons and color palette to the WYMeditor XHTML Editor. + +INSTALLATION: + +1. Copy the entire /silver/ directory to /wymeditor/skins/ +2. Initialize the WYMeditor 'skin' option as below: + + + +That's it. You're done. diff --git a/websdk/static/js/wymeditor/skins/silver/images/.svn/entries b/websdk/static/js/wymeditor/skins/silver/images/.svn/entries new file mode 100644 index 0000000..d70fd13 --- /dev/null +++ b/websdk/static/js/wymeditor/skins/silver/images/.svn/entries @@ -0,0 +1,164 @@ +10 + +dir +677 +svn://svn.wymeditor.org/wymeditor/trunk/src/wymeditor/skins/silver/images +svn://svn.wymeditor.org/wymeditor + + + +2008-05-12T20:05:54.287558Z +494 +jf.hovinne + + + + + + + + + + + + + + +89e89e35-0a13-0410-8f61-920bba073fa9 + +icons.silver.gif +file + + + + +2011-07-13T16:45:39.000000Z +3d55143203f242061d02ed4387e3c498 +2008-05-12T20:05:54.287558Z +494 +jf.hovinne +has-props + + + + + + + + + + + + + + + + + + + + +15382 + +bg.header.gif +file + + + + +2011-07-13T16:45:39.000000Z +4871b677b0af34f02d3a51046dd51f20 +2008-05-12T20:05:54.287558Z +494 +jf.hovinne +has-props + + + + + + + + + + + + + + + + + + + + +781 + +bg.wymeditor.png +file + + + + +2011-07-13T16:45:39.000000Z +dae577218f4bdd6f59197e3d8c8c9ea6 +2008-05-12T20:05:54.287558Z +494 +jf.hovinne +has-props + + + + + + + + + + + + + + + + + + + + +498 + +bg.selector.silver.gif +file + + + + +2011-07-13T16:45:39.000000Z +1101554f412121db5ad6157f366515e3 +2008-05-12T20:05:54.287558Z +494 +jf.hovinne +has-props + + + + + + + + + + + + + + + + + + + + +1621 + diff --git a/websdk/static/js/wymeditor/skins/silver/images/.svn/prop-base/bg.header.gif.svn-base b/websdk/static/js/wymeditor/skins/silver/images/.svn/prop-base/bg.header.gif.svn-base new file mode 100644 index 0000000..5e9587e --- /dev/null +++ b/websdk/static/js/wymeditor/skins/silver/images/.svn/prop-base/bg.header.gif.svn-base @@ -0,0 +1,5 @@ +K 13 +svn:mime-type +V 24 +application/octet-stream +END diff --git a/websdk/static/js/wymeditor/skins/silver/images/.svn/prop-base/bg.selector.silver.gif.svn-base b/websdk/static/js/wymeditor/skins/silver/images/.svn/prop-base/bg.selector.silver.gif.svn-base new file mode 100644 index 0000000..5e9587e --- /dev/null +++ b/websdk/static/js/wymeditor/skins/silver/images/.svn/prop-base/bg.selector.silver.gif.svn-base @@ -0,0 +1,5 @@ +K 13 +svn:mime-type +V 24 +application/octet-stream +END diff --git a/websdk/static/js/wymeditor/skins/silver/images/.svn/prop-base/bg.wymeditor.png.svn-base b/websdk/static/js/wymeditor/skins/silver/images/.svn/prop-base/bg.wymeditor.png.svn-base new file mode 100644 index 0000000..5e9587e --- /dev/null +++ b/websdk/static/js/wymeditor/skins/silver/images/.svn/prop-base/bg.wymeditor.png.svn-base @@ -0,0 +1,5 @@ +K 13 +svn:mime-type +V 24 +application/octet-stream +END diff --git a/websdk/static/js/wymeditor/skins/silver/images/.svn/prop-base/icons.silver.gif.svn-base b/websdk/static/js/wymeditor/skins/silver/images/.svn/prop-base/icons.silver.gif.svn-base new file mode 100644 index 0000000..5e9587e --- /dev/null +++ b/websdk/static/js/wymeditor/skins/silver/images/.svn/prop-base/icons.silver.gif.svn-base @@ -0,0 +1,5 @@ +K 13 +svn:mime-type +V 24 +application/octet-stream +END diff --git a/websdk/static/js/wymeditor/skins/silver/images/.svn/text-base/bg.header.gif.svn-base b/websdk/static/js/wymeditor/skins/silver/images/.svn/text-base/bg.header.gif.svn-base new file mode 100644 index 0000000..b2d2907 --- /dev/null +++ b/websdk/static/js/wymeditor/skins/silver/images/.svn/text-base/bg.header.gif.svn-base Binary files differ diff --git a/websdk/static/js/wymeditor/skins/silver/images/.svn/text-base/bg.selector.silver.gif.svn-base b/websdk/static/js/wymeditor/skins/silver/images/.svn/text-base/bg.selector.silver.gif.svn-base new file mode 100644 index 0000000..e65976b --- /dev/null +++ b/websdk/static/js/wymeditor/skins/silver/images/.svn/text-base/bg.selector.silver.gif.svn-base Binary files differ diff --git a/websdk/static/js/wymeditor/skins/silver/images/.svn/text-base/bg.wymeditor.png.svn-base b/websdk/static/js/wymeditor/skins/silver/images/.svn/text-base/bg.wymeditor.png.svn-base new file mode 100644 index 0000000..1e84813 --- /dev/null +++ b/websdk/static/js/wymeditor/skins/silver/images/.svn/text-base/bg.wymeditor.png.svn-base Binary files differ diff --git a/websdk/static/js/wymeditor/skins/silver/images/.svn/text-base/icons.silver.gif.svn-base b/websdk/static/js/wymeditor/skins/silver/images/.svn/text-base/icons.silver.gif.svn-base new file mode 100644 index 0000000..8c6a4fb --- /dev/null +++ b/websdk/static/js/wymeditor/skins/silver/images/.svn/text-base/icons.silver.gif.svn-base Binary files differ diff --git a/websdk/static/js/wymeditor/skins/silver/images/bg.header.gif b/websdk/static/js/wymeditor/skins/silver/images/bg.header.gif new file mode 100644 index 0000000..b2d2907 --- /dev/null +++ b/websdk/static/js/wymeditor/skins/silver/images/bg.header.gif Binary files differ diff --git a/websdk/static/js/wymeditor/skins/silver/images/bg.selector.silver.gif b/websdk/static/js/wymeditor/skins/silver/images/bg.selector.silver.gif new file mode 100644 index 0000000..e65976b --- /dev/null +++ b/websdk/static/js/wymeditor/skins/silver/images/bg.selector.silver.gif Binary files differ diff --git a/websdk/static/js/wymeditor/skins/silver/images/bg.wymeditor.png b/websdk/static/js/wymeditor/skins/silver/images/bg.wymeditor.png new file mode 100644 index 0000000..1e84813 --- /dev/null +++ b/websdk/static/js/wymeditor/skins/silver/images/bg.wymeditor.png Binary files differ diff --git a/websdk/static/js/wymeditor/skins/silver/images/icons.silver.gif b/websdk/static/js/wymeditor/skins/silver/images/icons.silver.gif new file mode 100644 index 0000000..8c6a4fb --- /dev/null +++ b/websdk/static/js/wymeditor/skins/silver/images/icons.silver.gif Binary files differ diff --git a/websdk/static/js/wymeditor/skins/silver/skin.css b/websdk/static/js/wymeditor/skins/silver/skin.css new file mode 100644 index 0000000..56e3248 --- /dev/null +++ b/websdk/static/js/wymeditor/skins/silver/skin.css @@ -0,0 +1,297 @@ +/* + * WYMeditor : what you see is What You Mean web-based editor + * Copyright (c) 2005 - 2009 Jean-Francois Hovinne, http://www.wymeditor.org/ + * Dual licensed under the MIT (MIT-license.txt) + * and GPL (GPL-license.txt) licenses. + * + * For further information visit: + * http://www.wymeditor.org/ + * + * File Name: + * screen.css + * main stylesheet for the default WYMeditor skin + * See the documentation for more info. + * + * File Authors: + * Daniel Reszka (d.reszka a-t wymeditor dotorg) + * Scott Edwin Lewis +*/ + +/*TRYING TO RESET STYLES THAT MAY INTERFERE WITH WYMEDITOR*/ + .wym_skin_silver p, .wym_skin_silver h2, .wym_skin_silver h3, + .wym_skin_silver ul, .wym_skin_silver li { background: transparent url(); margin: 0; padding: 0; border-width:0; list-style: none; } + + +/*HIDDEN BY DEFAULT*/ + .wym_skin_silver .wym_area_left { display: none; } + .wym_skin_silver .wym_area_right { display: block; } + + +/*TYPO*/ + .wym_skin_silver { font-size: 62.5%; font-family: Verdana, Arial, sans-serif; } + .wym_skin_silver h2 { font-size: 110%; /* = 11px */} + .wym_skin_silver h3 { font-size: 100%; /* = 10px */} + .wym_skin_silver li { font-size: 100%; /* = 10px */} + + +/*WYM_BOX*/ + .wym_skin_silver { border: 1px solid gray; background: #E8E8E8; padding: 0px; margin: 0px;} + + /*auto-clear the wym_box*/ + .wym_skin_silver:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } + * html .wym_skin_silver { height: 1%;} + + +/*WYM_HTML*/ + .wym_skin_silver .wym_html { width: 98%;} + .wym_skin_silver .wym_html textarea { width: 100%; height: 200px; border: 1px solid gray; background: white; } + + +/*WYM_IFRAME*/ + .wym_skin_silver .wym_iframe { width: 98%;} + .wym_skin_silver .wym_iframe iframe { width: 100%; height: 200px; border: 1px solid gray; background: white } + + +/*AREAS*/ + .wym_skin_silver .wym_area_left { width: 150px; float: left;} + .wym_skin_silver .wym_area_right { width: 150px; float: right;} + .wym_skin_silver .wym_area_bottom { height: 1%; clear: both;} + * html .wym_skin_silver .wym_area_main { height: 1%;} + * html .wym_skin_silver .wym_area_top { height: 1%;} + *+html .wym_skin_silver .wym_area_top { height: 1%;} + +/*SECTIONS SYSTEM*/ + + /*common defaults for all sections*/ + .wym_skin_silver .wym_section { margin-bottom: 5px; } + .wym_skin_silver .wym_section h2, + .wym_skin_silver .wym_section h3 { padding: 1px 3px; margin: 0; cursor: pointer; } + .wym_skin_silver .wym_section a { padding: 5px 0px 0px 10px; display: block; text-decoration: none; color: black; } + .wym_skin_silver .wym_section a:hover { /*background-color: #DDD;*/} + /*hide section titles by default*/ + .wym_skin_silver .wym_section h2 { display: none; } + /*disable any margin-collapse*/ + .wym_skin_silver .wym_section { padding-top: 1px; padding-bottom: 1px; } + /*auto-clear sections*/ + .wym_skin_silver .wym_section ul:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; padding: 0px; } + * html .wym_skin_silver .wym_section ul { height: 1%;} + .wym_skin_silver .wym_section li {} + + /*option: add this class to a section to make it render as a panel*/ + .wym_skin_silver .wym_panel { } + .wym_skin_silver .wym_panel h2 { display: block; font-size: 11px; } + + /*option: add this class to a section to make it render as a dropdown menu*/ + .wym_skin_silver .wym_dropdown h2 { display: block; font-size: 11px;} + .wym_skin_silver .wym_dropdown ul { position: absolute; background: white; padding: 0px;} + .wym_skin_silver .wym_dropdown:hover ul, + .wym_skin_silver .wym_dropdown.hover ul { cursor: pointer;} + .wym_skin_silver .wym_dropdown ul li a {/*border-bottom: 1px solid #AAA;*/} + + /*option: add this class to a section to make its elements render buttons (icons are only available for the wym_tools section for now)*/ + .wym_skin_silver .wym_buttons li { float:left;} + .wym_skin_silver .wym_buttons a { width: 20px; height: 20px; overflow: hidden; padding: 2px; text-decoration: none !important; border: 1px solid #666; } + .wym_skin_silver .wym_buttons a:hover { text-decoration: none !important; border: 1px solid #000;} + /*image replacements*/ + .wym_skin_silver .wym_buttons li a { background: url(images/icons.silver.gif) no-repeat; text-indent: -9999px;} + .wym_skin_silver .wym_buttons li.wym_tools_strong a { background-position: 0 -384px;} + .wym_skin_silver .wym_buttons li.wym_tools_emphasis a { background-position: 0 -24px;} + .wym_skin_silver .wym_buttons li.wym_tools_superscript a { background-position: 0 -432px;} + .wym_skin_silver .wym_buttons li.wym_tools_subscript a { background-position: 0 -456px;} + .wym_skin_silver .wym_buttons li.wym_tools_ordered_list a { background-position: 0 -48px;} + .wym_skin_silver .wym_buttons li.wym_tools_unordered_list a{ background-position: 0 -72px;} + .wym_skin_silver .wym_buttons li.wym_tools_indent a { background-position: 0 -600px;} + .wym_skin_silver .wym_buttons li.wym_tools_outdent a { background-position: 0 -624px;} + .wym_skin_silver .wym_buttons li.wym_tools_undo a { background-position: 0 -504px;} + .wym_skin_silver .wym_buttons li.wym_tools_redo a { background-position: 0 -528px;} + .wym_skin_silver .wym_buttons li.wym_tools_link a { background-position: 0 -96px;} + .wym_skin_silver .wym_buttons li.wym_tools_unlink a { background-position: 0 -168px;} + .wym_skin_silver .wym_buttons li.wym_tools_image a { background-position: 0 -120px;} + .wym_skin_silver .wym_buttons li.wym_tools_table a { background-position: 0 -144px;} + .wym_skin_silver .wym_buttons li.wym_tools_paste a { background-position: 0 -552px;} + .wym_skin_silver .wym_buttons li.wym_tools_html a { background-position: 0 -192px;} + .wym_skin_silver .wym_buttons li.wym_tools_preview a { background-position: 0 -408px;} + .wym_skin_silver .wym_buttons li.wym_tools_gadget a { background-position: 0 -576px;} + + .wym_skin_silver .wym_buttons li.wym_tools_strong a:hover { background-position: -24px -384px;} + .wym_skin_silver .wym_buttons li.wym_tools_emphasis a:hover { background-position: -24px -24px;} + .wym_skin_silver .wym_buttons li.wym_tools_superscript a:hover { background-position: -24px -432px;} + .wym_skin_silver .wym_buttons li.wym_tools_subscript a:hover { background-position: -24px -456px;} + .wym_skin_silver .wym_buttons li.wym_tools_ordered_list a:hover { background-position: -24px -48px;} + .wym_skin_silver .wym_buttons li.wym_tools_unordered_list a:hover{ background-position: -24px -72px;} + .wym_skin_silver .wym_buttons li.wym_tools_indent a:hover { background-position: -24px -600px;} + .wym_skin_silver .wym_buttons li.wym_tools_outdent a:hover { background-position: -24px -624px;} + .wym_skin_silver .wym_buttons li.wym_tools_undo a:hover { background-position: -24px -504px;} + .wym_skin_silver .wym_buttons li.wym_tools_redo a:hover { background-position: -24px -528px;} + .wym_skin_silver .wym_buttons li.wym_tools_link a:hover { background-position: -24px -96px;} + .wym_skin_silver .wym_buttons li.wym_tools_unlink a:hover { background-position: -24px -168px;} + .wym_skin_silver .wym_buttons li.wym_tools_image a:hover { background-position: -24px -120px;} + .wym_skin_silver .wym_buttons li.wym_tools_table a:hover { background-position: -24px -144px;} + .wym_skin_silver .wym_buttons li.wym_tools_paste a:hover { background-position: -24px -552px;} + .wym_skin_silver .wym_buttons li.wym_tools_html a:hover { background-position: -24px -192px;} + .wym_skin_silver .wym_buttons li.wym_tools_preview a:hover { background-position: -24px -408px;} + .wym_skin_silver .wym_buttons li.wym_tools_gadget a:hover { background-position: -24px -576px;} + +/*DECORATION*/ + .wym_skin_silver .wym_section h2 { background: #ddd; border: none;} + .wym_skin_silver .wym_section h2 span { color: gray;} + .wym_skin_silver .wym_panel { padding: 0; border: solid gray; border-width: 0px;} + .wym_skin_silver .wym_panel ul { margin: 2px 0 5px; } + .wym_skin_silver .wym_dropdown { padding: 0; border: none; } + .wym_skin_silver .wym_dropdown ul { border: none; margin-left: -1px; padding: 0px;} + +/*DIALOGS*/ + .wym_dialog div.row { margin-bottom: 5px;} + .wym_dialog div.row input { margin-right: 5px;} + .wym_dialog div.row label { float: left; width: 150px; display: block; text-align: right; margin-right: 10px; } + .wym_dialog div.row-indent { padding-left: 160px; } + /*autoclearing*/ + .wym_dialog div.row:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } + .wym_dialog div.row { display: inline-block; } + /* Hides from IE-mac \*/ + * html .wym_dialog div.row { height: 1%; } + .wym_dialog div.row { display: block; } + /* End hide from IE-mac */ + +/*WYMEDITOR_LINK*/ + a.wym_wymeditor_link + { + text-indent: -9999px; + float: right; + display: block; + width: 50px; + height: 15px; + background: url(../wymeditor_icon.png); + background-position: 1px 1px; + background-repeat: no-repeat; + overflow: hidden; + text-decoration: none; + padding: 1px !important; + border: 1px solid #333 !important; + background-color: #FFF !important; + } + +.wym_box +{ + padding: 0px !important; + margin: 0px; +} +.wym_inner +{ + border-left: 1px solid #FFF; + border-top: 1px solid #FFF; + border-right: 1px solid #FFF; + border-bottom: 1px solid #FFF; + padding: 5px; + background-color: #E8E8E8; + height: auto; +} + +.clear {clear: both;} + +div.wym_dropdown +{ + cursor: pointer; + width: 138px !important; + margin: 0px 4px 10px 0px !important; + padding: 0px; + z-index: 1001; + display: block; + border: 1px solid red; +} + +div.wym_dropdown ul +{ + display: none; + width: 124px; + padding: 0px !important; + margin: 0px !important; + list-style-type: none; + list-style-image: none; + z-index: 1002; + position: absolute; + border-top: 1px solid #AAA; +} + +div.wym_dropdown ul li +{ + width: 146px; + height: 20px; + padding: 0px !important; + margin: 0px; + border: 1px solid #777; + border-top: none; + background: #DDD; + list-style-image: none; +} + +div.wym_dropdown h2 +{ + width: 138px; + height: 16px; + color: #000 !important; + background-image: url(images/bg.selector.silver.gif) !important; + background-position: 0px -18px; + background-repeat: no-repeat; + border: none; + font-family: "Trebuchet MS", Verdana, Arial, Helvetica, Sanserif; + font-size: 12px !important; + font-weight: bold !important; + padding: 2px 0px 0px 10px !important; + margin: 0px !important; +} + +.wym_skin_silver .wym_panel h2 +{ + width: 138px; + height: 16px; + color: #000 !important; + background-image: url(images/bg.header.gif) !important; + background-position: 0px 0px; + background-repeat: no-repeat; + border: none; + font-family: "Trebuchet MS", Verdana, Arial, Helvetica, Sanserif; + font-size: 12px !important; + font-weight: bold !important; + padding: 2px 0px 0px 10px !important; + margin: 0px !important; +} + +.wym_skin_silver .wym_panel ul +{ + margin-top: 0px !important; +} + +.wym_skin_silver .wym_panel ul li +{ + width: 146px; + height: 20px; + padding: 0px !important; + margin: 0px; + border: 1px solid #777; + border-top: none; + background: #DDD; + list-style-image: none; +} + +.wym_skin_silver .wym_panel a, +div.wym_dropdown a +{ + text-decoration: none; + font-family: "Trebuchet MS", Verdana, Arial, Helvetica, Sanserif; + font-size: 12px; + padding: 5px 0px 0px 10px !important; + display: block; + width: 136px; + height: 15px; + color: #000; + text-align: left !important; + margin-left: 0px !important; +} + +div.wym_dropdown a:hover, +.wym_skin_silver .wym_panel a:hover +{ + background: #BBB; + border-bottom: none !important; +} diff --git a/websdk/static/js/wymeditor/skins/silver/skin.js b/websdk/static/js/wymeditor/skins/silver/skin.js new file mode 100644 index 0000000..948ed91 --- /dev/null +++ b/websdk/static/js/wymeditor/skins/silver/skin.js @@ -0,0 +1,61 @@ +/* This file is part of the Silver skin for WYMeditor by Scott Edwin Lewis */ + +jQuery.fn.selectify = function() { + return this.each(function() { + jQuery(this).hover( + function() { + jQuery("h2", this).css("background-position", "0px -18px"); + jQuery("ul", this).fadeIn("fast"); + }, + function() { + jQuery("h2", this).css("background-position", ""); + jQuery("ul", this).fadeOut("fast"); + } + ); + }); +}; + +WYMeditor.SKINS['silver'] = { + + init: function(wym) { + + //add some elements to improve the rendering + jQuery(wym._box) + .append('
    ') + .wrapInner('
    '); + + //render following sections as panels + jQuery(wym._box).find(wym._options.classesSelector) + .addClass("wym_panel"); + + //render following sections as buttons + jQuery(wym._box).find(wym._options.toolsSelector) + .addClass("wym_buttons"); + + //render following sections as dropdown menus + jQuery(wym._box).find(wym._options.containersSelector) + .addClass("wym_dropdown") + .selectify(); + + // auto add some margin to the main area sides if left area + // or right area are not empty (if they contain sections) + jQuery(wym._box).find("div.wym_area_right ul") + .parents("div.wym_area_right").show() + .parents(wym._options.boxSelector) + .find("div.wym_area_main") + .css({"margin-right": "155px"}); + + jQuery(wym._box).find("div.wym_area_left ul") + .parents("div.wym_area_left").show() + .parents(wym._options.boxSelector) + .find("div.wym_area_main") + .css({"margin-left": "155px"}); + + //make hover work under IE < 7 + jQuery(wym._box).find(".wym_section").hover(function(){ + jQuery(this).addClass("hover"); + },function(){ + jQuery(this).removeClass("hover"); + }); + } +}; diff --git a/websdk/static/js/wymeditor/skins/twopanels/.svn/entries b/websdk/static/js/wymeditor/skins/twopanels/.svn/entries new file mode 100644 index 0000000..a477f85 --- /dev/null +++ b/websdk/static/js/wymeditor/skins/twopanels/.svn/entries @@ -0,0 +1,130 @@ +10 + +dir +677 +svn://svn.wymeditor.org/wymeditor/trunk/src/wymeditor/skins/twopanels +svn://svn.wymeditor.org/wymeditor + + + +2009-05-27T19:20:55.910061Z +632 +jf.hovinne + + + + + + + + + + + + + + +89e89e35-0a13-0410-8f61-920bba073fa9 + +skin.js +file + + + + +2011-07-13T16:45:40.000000Z +d47d82f6cda558d258263f3949d815f1 +2008-05-30T19:59:08.978756Z +502 +jf.hovinne + + + + + + + + + + + + + + + + + + + + + +1380 + +skin.css +file + + + + +2011-07-13T16:45:40.000000Z +5da884f153705d38e473d5cf4bdc7deb +2009-05-27T19:20:55.910061Z +632 +jf.hovinne + + + + + + + + + + + + + + + + + + + + + +8045 + +icons.png +file + + + + +2011-07-13T16:45:40.000000Z +45a781288dc799f892fa517355ff80b6 +2008-05-30T19:59:08.978756Z +502 +jf.hovinne +has-props + + + + + + + + + + + + + + + + + + + + +3651 + diff --git a/websdk/static/js/wymeditor/skins/twopanels/.svn/prop-base/icons.png.svn-base b/websdk/static/js/wymeditor/skins/twopanels/.svn/prop-base/icons.png.svn-base new file mode 100644 index 0000000..5e9587e --- /dev/null +++ b/websdk/static/js/wymeditor/skins/twopanels/.svn/prop-base/icons.png.svn-base @@ -0,0 +1,5 @@ +K 13 +svn:mime-type +V 24 +application/octet-stream +END diff --git a/websdk/static/js/wymeditor/skins/twopanels/.svn/text-base/icons.png.svn-base b/websdk/static/js/wymeditor/skins/twopanels/.svn/text-base/icons.png.svn-base new file mode 100644 index 0000000..c6eb463 --- /dev/null +++ b/websdk/static/js/wymeditor/skins/twopanels/.svn/text-base/icons.png.svn-base Binary files differ diff --git a/websdk/static/js/wymeditor/skins/twopanels/.svn/text-base/skin.css.svn-base b/websdk/static/js/wymeditor/skins/twopanels/.svn/text-base/skin.css.svn-base new file mode 100644 index 0000000..7e6b8fd --- /dev/null +++ b/websdk/static/js/wymeditor/skins/twopanels/.svn/text-base/skin.css.svn-base @@ -0,0 +1,134 @@ +/* + * WYMeditor : what you see is What You Mean web-based editor + * Copyright (c) 2005 - 2009 Jean-Francois Hovinne, http://www.wymeditor.org/ + * Dual licensed under the MIT (MIT-license.txt) + * and GPL (GPL-license.txt) licenses. + * + * For further information visit: + * http://www.wymeditor.org/ + * + * File Name: + * screen.css + * main stylesheet for the WYMeditor skin + * See the documentation for more info. + * + * File Authors: + * Daniel Reszka (d.reszka a-t wymeditor dotorg) + * Jean-Francois Hovinne +*/ + +/*TRYING TO RESET STYLES THAT MAY INTERFERE WITH WYMEDITOR*/ + .wym_skin_twopanels p, .wym_skin_twopanels h2, .wym_skin_twopanels h3, + .wym_skin_twopanels ul, .wym_skin_twopanels li { background: transparent url(); margin: 0; padding: 0; border-width:0; list-style: none; } + + +/*HIDDEN BY DEFAULT*/ + .wym_skin_twopanels .wym_area_left { display: block; } + .wym_skin_twopanels .wym_area_right { display: block; } + + +/*TYPO*/ + .wym_skin_twopanels { font-size: 62.5%; font-family: Verdana, Arial, sans-serif; } + .wym_skin_twopanels h2 { font-size: 110%; /* = 11px */} + .wym_skin_twopanels h3 { font-size: 100%; /* = 10px */} + .wym_skin_twopanels li { font-size: 100%; /* = 10px */} + + +/*WYM_BOX*/ + .wym_skin_twopanels { border: 1px solid gray; background: #f2f2f2; padding: 5px} + + /*auto-clear the wym_box*/ + .wym_skin_twopanels:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } + * html .wym_skin_twopanels { height: 1%;} + + +/*WYM_HTML*/ + .wym_skin_twopanels .wym_html { width: 98%;} + .wym_skin_twopanels .wym_html textarea { width: 100%; height: 200px; border: 1px solid gray; background: white; } + + +/*WYM_IFRAME*/ + .wym_skin_twopanels .wym_iframe { width: 98%;} + .wym_skin_twopanels .wym_iframe iframe { width: 100%; height: 200px; border: 1px solid gray; background: white } + + +/*AREAS*/ + .wym_skin_twopanels .wym_area_left { width: 100px; float: left;} + .wym_skin_twopanels .wym_area_right { width: 150px; float: right;} + .wym_skin_twopanels .wym_area_bottom { height: 1%; clear: both;} + * html .wym_skin_twopanels .wym_area_main { height: 1%;} + * html .wym_skin_twopanels .wym_area_top { height: 1%;} + *+html .wym_skin_twopanels .wym_area_top { height: 1%;} + +/*SECTIONS SYSTEM*/ + + /*common defaults for all sections*/ + .wym_skin_twopanels .wym_section { margin-bottom: 5px; } + .wym_skin_twopanels .wym_section h2, + .wym_skin_twopanels .wym_section h3 { padding: 1px 3px; margin: 0; } + .wym_skin_twopanels .wym_section a { padding: 0 3px; display: block; text-decoration: none; color: black; } + .wym_skin_twopanels .wym_section a:hover { background-color: yellow; } + /*hide section titles by default*/ + .wym_skin_twopanels .wym_section h2 { display: none; } + /*disable any margin-collapse*/ + .wym_skin_twopanels .wym_section { padding-top: 1px; padding-bottom: 1px; } + /*auto-clear sections*/ + .wym_skin_twopanels .wym_section ul:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } + * html .wym_skin_twopanels .wym_section ul { height: 1%;} + + /*option: add this class to a section to make it render as a panel*/ + .wym_skin_twopanels .wym_panel { } + .wym_skin_twopanels .wym_panel h2 { display: block; } + + /*option: add this class to a section to make it render as a dropdown menu*/ + .wym_skin_twopanels .wym_dropdown h2 { display: block; } + .wym_skin_twopanels .wym_dropdown ul { display: none; position: absolute; background: white; } + .wym_skin_twopanels .wym_dropdown:hover ul, + .wym_skin_twopanels .wym_dropdown.hover ul { display: block; } + + /*option: add this class to a section to make its elements render buttons (icons are only available for the wym_tools section for now)*/ + .wym_skin_twopanels .wym_buttons li { float:left;} + .wym_skin_twopanels .wym_buttons a { width: 20px; height: 20px; overflow: hidden; padding: 2px } + /*image replacements*/ + .wym_skin_twopanels .wym_buttons li a { background: url(icons.png) no-repeat; text-indent: -9999px;} + .wym_skin_twopanels .wym_buttons li.wym_tools_strong a { background-position: 0 -382px;} + .wym_skin_twopanels .wym_buttons li.wym_tools_emphasis a { background-position: 0 -22px;} + .wym_skin_twopanels .wym_buttons li.wym_tools_superscript a { background-position: 0 -430px;} + .wym_skin_twopanels .wym_buttons li.wym_tools_subscript a { background-position: 0 -454px;} + .wym_skin_twopanels .wym_buttons li.wym_tools_ordered_list a { background-position: 0 -48px;} + .wym_skin_twopanels .wym_buttons li.wym_tools_unordered_list a{ background-position: 0 -72px;} + .wym_skin_twopanels .wym_buttons li.wym_tools_indent a { background-position: 0 -574px;} + .wym_skin_twopanels .wym_buttons li.wym_tools_outdent a { background-position: 0 -598px;} + .wym_skin_twopanels .wym_buttons li.wym_tools_undo a { background-position: 0 -502px;} + .wym_skin_twopanels .wym_buttons li.wym_tools_redo a { background-position: 0 -526px;} + .wym_skin_twopanels .wym_buttons li.wym_tools_link a { background-position: 0 -96px;} + .wym_skin_twopanels .wym_buttons li.wym_tools_unlink a { background-position: 0 -168px;} + .wym_skin_twopanels .wym_buttons li.wym_tools_image a { background-position: 0 -121px;} + .wym_skin_twopanels .wym_buttons li.wym_tools_table a { background-position: 0 -144px;} + .wym_skin_twopanels .wym_buttons li.wym_tools_paste a { background-position: 0 -552px;} + .wym_skin_twopanels .wym_buttons li.wym_tools_html a { background-position: 0 -193px;} + .wym_skin_twopanels .wym_buttons li.wym_tools_preview a { background-position: 0 -408px;} + +/*DECORATION*/ + .wym_skin_twopanels .wym_section h2 { background: #ddd; border: solid gray; border-width: 0 0 1px;} + .wym_skin_twopanels .wym_section h2 span { color: gray;} + .wym_skin_twopanels .wym_panel { padding: 0; border: solid gray; border-width: 1px; background: white;} + .wym_skin_twopanels .wym_panel ul { margin: 2px 0 5px; } + .wym_skin_twopanels .wym_dropdown { padding: 0; border: solid gray; border-width: 1px 1px 0 1px; } + .wym_skin_twopanels .wym_dropdown ul { border: solid gray; border-width: 0 1px 1px 1px; margin-left: -1px; padding: 5px 10px 5px 3px;} + +/*DIALOGS*/ + .wym_dialog div.row { margin-bottom: 5px;} + .wym_dialog div.row input { margin-right: 5px;} + .wym_dialog div.row label { float: left; width: 150px; display: block; text-align: right; margin-right: 10px; } + .wym_dialog div.row-indent { padding-left: 160px; } + /*autoclearing*/ + .wym_dialog div.row:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } + .wym_dialog div.row { display: inline-block; } + /* Hides from IE-mac \*/ + * html .wym_dialog div.row { height: 1%; } + .wym_dialog div.row { display: block; } + /* End hide from IE-mac */ + +/*WYMEDITOR_LINK*/ + a.wym_wymeditor_link { text-indent: -9999px; float: right; display: block; width: 50px; height: 15px; background: url(../wymeditor_icon.png); overflow: hidden; text-decoration: none; } diff --git a/websdk/static/js/wymeditor/skins/twopanels/.svn/text-base/skin.js.svn-base b/websdk/static/js/wymeditor/skins/twopanels/.svn/text-base/skin.js.svn-base new file mode 100644 index 0000000..e82efc5 --- /dev/null +++ b/websdk/static/js/wymeditor/skins/twopanels/.svn/text-base/skin.js.svn-base @@ -0,0 +1,39 @@ +WYMeditor.SKINS['twopanels'] = { + + init: function(wym) { + + //move the containers panel to the left area + jQuery(wym._box).find(wym._options.containersSelector) + .appendTo("div.wym_area_left"); + + //render following sections as panels + jQuery(wym._box).find(wym._options.classesSelector + ', ' + + wym._options.containersSelector) + .addClass("wym_panel"); + + //render following sections as buttons + jQuery(wym._box).find(wym._options.toolsSelector) + .addClass("wym_buttons"); + + // auto add some margin to the main area sides if left area + // or right area are not empty (if they contain sections) + jQuery(wym._box).find("div.wym_area_right ul") + .parents("div.wym_area_right").show() + .parents(wym._options.boxSelector) + .find("div.wym_area_main") + .css({"margin-right": "155px"}); + + jQuery(wym._box).find("div.wym_area_left ul") + .parents("div.wym_area_left").show() + .parents(wym._options.boxSelector) + .find("div.wym_area_main") + .css({"margin-left": "115px"}); + + //make hover work under IE < 7 + jQuery(wym._box).find(".wym_section").hover(function(){ + jQuery(this).addClass("hover"); + },function(){ + jQuery(this).removeClass("hover"); + }); + } +}; diff --git a/websdk/static/js/wymeditor/skins/twopanels/icons.png b/websdk/static/js/wymeditor/skins/twopanels/icons.png new file mode 100644 index 0000000..c6eb463 --- /dev/null +++ b/websdk/static/js/wymeditor/skins/twopanels/icons.png Binary files differ diff --git a/websdk/static/js/wymeditor/skins/twopanels/skin.css b/websdk/static/js/wymeditor/skins/twopanels/skin.css new file mode 100644 index 0000000..7e6b8fd --- /dev/null +++ b/websdk/static/js/wymeditor/skins/twopanels/skin.css @@ -0,0 +1,134 @@ +/* + * WYMeditor : what you see is What You Mean web-based editor + * Copyright (c) 2005 - 2009 Jean-Francois Hovinne, http://www.wymeditor.org/ + * Dual licensed under the MIT (MIT-license.txt) + * and GPL (GPL-license.txt) licenses. + * + * For further information visit: + * http://www.wymeditor.org/ + * + * File Name: + * screen.css + * main stylesheet for the WYMeditor skin + * See the documentation for more info. + * + * File Authors: + * Daniel Reszka (d.reszka a-t wymeditor dotorg) + * Jean-Francois Hovinne +*/ + +/*TRYING TO RESET STYLES THAT MAY INTERFERE WITH WYMEDITOR*/ + .wym_skin_twopanels p, .wym_skin_twopanels h2, .wym_skin_twopanels h3, + .wym_skin_twopanels ul, .wym_skin_twopanels li { background: transparent url(); margin: 0; padding: 0; border-width:0; list-style: none; } + + +/*HIDDEN BY DEFAULT*/ + .wym_skin_twopanels .wym_area_left { display: block; } + .wym_skin_twopanels .wym_area_right { display: block; } + + +/*TYPO*/ + .wym_skin_twopanels { font-size: 62.5%; font-family: Verdana, Arial, sans-serif; } + .wym_skin_twopanels h2 { font-size: 110%; /* = 11px */} + .wym_skin_twopanels h3 { font-size: 100%; /* = 10px */} + .wym_skin_twopanels li { font-size: 100%; /* = 10px */} + + +/*WYM_BOX*/ + .wym_skin_twopanels { border: 1px solid gray; background: #f2f2f2; padding: 5px} + + /*auto-clear the wym_box*/ + .wym_skin_twopanels:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } + * html .wym_skin_twopanels { height: 1%;} + + +/*WYM_HTML*/ + .wym_skin_twopanels .wym_html { width: 98%;} + .wym_skin_twopanels .wym_html textarea { width: 100%; height: 200px; border: 1px solid gray; background: white; } + + +/*WYM_IFRAME*/ + .wym_skin_twopanels .wym_iframe { width: 98%;} + .wym_skin_twopanels .wym_iframe iframe { width: 100%; height: 200px; border: 1px solid gray; background: white } + + +/*AREAS*/ + .wym_skin_twopanels .wym_area_left { width: 100px; float: left;} + .wym_skin_twopanels .wym_area_right { width: 150px; float: right;} + .wym_skin_twopanels .wym_area_bottom { height: 1%; clear: both;} + * html .wym_skin_twopanels .wym_area_main { height: 1%;} + * html .wym_skin_twopanels .wym_area_top { height: 1%;} + *+html .wym_skin_twopanels .wym_area_top { height: 1%;} + +/*SECTIONS SYSTEM*/ + + /*common defaults for all sections*/ + .wym_skin_twopanels .wym_section { margin-bottom: 5px; } + .wym_skin_twopanels .wym_section h2, + .wym_skin_twopanels .wym_section h3 { padding: 1px 3px; margin: 0; } + .wym_skin_twopanels .wym_section a { padding: 0 3px; display: block; text-decoration: none; color: black; } + .wym_skin_twopanels .wym_section a:hover { background-color: yellow; } + /*hide section titles by default*/ + .wym_skin_twopanels .wym_section h2 { display: none; } + /*disable any margin-collapse*/ + .wym_skin_twopanels .wym_section { padding-top: 1px; padding-bottom: 1px; } + /*auto-clear sections*/ + .wym_skin_twopanels .wym_section ul:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } + * html .wym_skin_twopanels .wym_section ul { height: 1%;} + + /*option: add this class to a section to make it render as a panel*/ + .wym_skin_twopanels .wym_panel { } + .wym_skin_twopanels .wym_panel h2 { display: block; } + + /*option: add this class to a section to make it render as a dropdown menu*/ + .wym_skin_twopanels .wym_dropdown h2 { display: block; } + .wym_skin_twopanels .wym_dropdown ul { display: none; position: absolute; background: white; } + .wym_skin_twopanels .wym_dropdown:hover ul, + .wym_skin_twopanels .wym_dropdown.hover ul { display: block; } + + /*option: add this class to a section to make its elements render buttons (icons are only available for the wym_tools section for now)*/ + .wym_skin_twopanels .wym_buttons li { float:left;} + .wym_skin_twopanels .wym_buttons a { width: 20px; height: 20px; overflow: hidden; padding: 2px } + /*image replacements*/ + .wym_skin_twopanels .wym_buttons li a { background: url(icons.png) no-repeat; text-indent: -9999px;} + .wym_skin_twopanels .wym_buttons li.wym_tools_strong a { background-position: 0 -382px;} + .wym_skin_twopanels .wym_buttons li.wym_tools_emphasis a { background-position: 0 -22px;} + .wym_skin_twopanels .wym_buttons li.wym_tools_superscript a { background-position: 0 -430px;} + .wym_skin_twopanels .wym_buttons li.wym_tools_subscript a { background-position: 0 -454px;} + .wym_skin_twopanels .wym_buttons li.wym_tools_ordered_list a { background-position: 0 -48px;} + .wym_skin_twopanels .wym_buttons li.wym_tools_unordered_list a{ background-position: 0 -72px;} + .wym_skin_twopanels .wym_buttons li.wym_tools_indent a { background-position: 0 -574px;} + .wym_skin_twopanels .wym_buttons li.wym_tools_outdent a { background-position: 0 -598px;} + .wym_skin_twopanels .wym_buttons li.wym_tools_undo a { background-position: 0 -502px;} + .wym_skin_twopanels .wym_buttons li.wym_tools_redo a { background-position: 0 -526px;} + .wym_skin_twopanels .wym_buttons li.wym_tools_link a { background-position: 0 -96px;} + .wym_skin_twopanels .wym_buttons li.wym_tools_unlink a { background-position: 0 -168px;} + .wym_skin_twopanels .wym_buttons li.wym_tools_image a { background-position: 0 -121px;} + .wym_skin_twopanels .wym_buttons li.wym_tools_table a { background-position: 0 -144px;} + .wym_skin_twopanels .wym_buttons li.wym_tools_paste a { background-position: 0 -552px;} + .wym_skin_twopanels .wym_buttons li.wym_tools_html a { background-position: 0 -193px;} + .wym_skin_twopanels .wym_buttons li.wym_tools_preview a { background-position: 0 -408px;} + +/*DECORATION*/ + .wym_skin_twopanels .wym_section h2 { background: #ddd; border: solid gray; border-width: 0 0 1px;} + .wym_skin_twopanels .wym_section h2 span { color: gray;} + .wym_skin_twopanels .wym_panel { padding: 0; border: solid gray; border-width: 1px; background: white;} + .wym_skin_twopanels .wym_panel ul { margin: 2px 0 5px; } + .wym_skin_twopanels .wym_dropdown { padding: 0; border: solid gray; border-width: 1px 1px 0 1px; } + .wym_skin_twopanels .wym_dropdown ul { border: solid gray; border-width: 0 1px 1px 1px; margin-left: -1px; padding: 5px 10px 5px 3px;} + +/*DIALOGS*/ + .wym_dialog div.row { margin-bottom: 5px;} + .wym_dialog div.row input { margin-right: 5px;} + .wym_dialog div.row label { float: left; width: 150px; display: block; text-align: right; margin-right: 10px; } + .wym_dialog div.row-indent { padding-left: 160px; } + /*autoclearing*/ + .wym_dialog div.row:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } + .wym_dialog div.row { display: inline-block; } + /* Hides from IE-mac \*/ + * html .wym_dialog div.row { height: 1%; } + .wym_dialog div.row { display: block; } + /* End hide from IE-mac */ + +/*WYMEDITOR_LINK*/ + a.wym_wymeditor_link { text-indent: -9999px; float: right; display: block; width: 50px; height: 15px; background: url(../wymeditor_icon.png); overflow: hidden; text-decoration: none; } diff --git a/websdk/static/js/wymeditor/skins/twopanels/skin.js b/websdk/static/js/wymeditor/skins/twopanels/skin.js new file mode 100644 index 0000000..e82efc5 --- /dev/null +++ b/websdk/static/js/wymeditor/skins/twopanels/skin.js @@ -0,0 +1,39 @@ +WYMeditor.SKINS['twopanels'] = { + + init: function(wym) { + + //move the containers panel to the left area + jQuery(wym._box).find(wym._options.containersSelector) + .appendTo("div.wym_area_left"); + + //render following sections as panels + jQuery(wym._box).find(wym._options.classesSelector + ', ' + + wym._options.containersSelector) + .addClass("wym_panel"); + + //render following sections as buttons + jQuery(wym._box).find(wym._options.toolsSelector) + .addClass("wym_buttons"); + + // auto add some margin to the main area sides if left area + // or right area are not empty (if they contain sections) + jQuery(wym._box).find("div.wym_area_right ul") + .parents("div.wym_area_right").show() + .parents(wym._options.boxSelector) + .find("div.wym_area_main") + .css({"margin-right": "155px"}); + + jQuery(wym._box).find("div.wym_area_left ul") + .parents("div.wym_area_left").show() + .parents(wym._options.boxSelector) + .find("div.wym_area_main") + .css({"margin-left": "115px"}); + + //make hover work under IE < 7 + jQuery(wym._box).find(".wym_section").hover(function(){ + jQuery(this).addClass("hover"); + },function(){ + jQuery(this).removeClass("hover"); + }); + } +}; diff --git a/websdk/static/js/wymeditor/skins/wymeditor_icon.png b/websdk/static/js/wymeditor/skins/wymeditor_icon.png new file mode 100644 index 0000000..d4fc155 --- /dev/null +++ b/websdk/static/js/wymeditor/skins/wymeditor_icon.png Binary files differ diff --git a/websdk/static/static b/websdk/static/static new file mode 120000 index 0000000..945c9b4 --- /dev/null +++ b/websdk/static/static @@ -0,0 +1 @@ +. \ No newline at end of file diff --git a/websdk/studio.py b/websdk/studio.py new file mode 100644 index 0000000..d7b5879 --- /dev/null +++ b/websdk/studio.py @@ -0,0 +1,105 @@ +import os +import sys +from flask import Flask +from flaskext.genshi import Genshi, render_response +from werkzeug.utils import redirect +from flask import request,url_for + +app = Flask(__name__) +app.debug = True +genshi = Genshi(app) + +def shutdown_server(): + func = request.environ.get('werkzeug.server.shutdown') + if func is None: + raise RuntimeError('Not running with the Werkzeug Server') + func() + +def list_files(directory): + files=os.listdir(directory) + print "showing %s" % directory + return files + +@app.route('/') +def index(): + return render_response('index.html') + +@app.route('/edit/') +@app.route('/edit/') +def edit(filename="activity.py"): + icon = 'document-generic.svg' + mode = '' + if filename.endswith('.py'): + icon = 'text-x-python.svg' + mode = 'python' + if filename.endswith('.html'): + icon = 'text-uri-list.svg' + mode = 'html' + if filename.endswith('.css'): + icon = 'text-uri-list.svg' + mode = 'css' + if filename.endswith('.js'): + icon = 'text-uri-list.svg' + mode = 'javascript' + content = open(filename).read().decode('utf-8') + tmpl = 'editor.html' + directory=os.path.dirname(filename) + return render_response(tmpl, dict(content=content, icon=icon,basename=os.path.basename(filename), + filename=filename, absdir=os.path.normpath(directory), mode=mode, directory=directory)) + +@app.route('/save', methods=['POST']) +def save(): + filename = request.form['filename'] + f=open(filename,"wb") + content = request.form['content'] + content = content.replace('\r\n', '\n').replace('\r', '\n') # HACK - Ace seems to be confused about newlines + f.write(content.encode('utf-8')) + print "saving content: %s" % filename + f.close() + directory = os.path.dirname(filename) + return redirect(url_for('browse', directory=directory)) + +@app.route('/files/') +@app.route('/files/') +def browse(directory="."): + filelist = list_files(directory) + files = [] + if not os.path.abspath(directory)==os.path.abspath("."): + files.append( { 'name': '..', + 'icon': 'folder.svg', + 'href': '/files/%s' % os.path.join(directory,"..") }) + for filename in sorted(filelist): + fullname = os.path.join(directory,filename) + icon = 'document-generic.svg' + href = '/edit/%s/%s' % (directory,filename) + if filename.endswith('.py'): + icon = 'text-x-python.svg' + if filename.endswith('.html'): + icon = 'text-uri-list.svg' + if filename.endswith('.css'): + icon = 'text-uri-list.svg' + if filename.endswith('.js'): + icon = 'text-uri-list.svg' + if os.path.isdir(fullname): + icon = 'folder.svg' + href = '/files/%s' % fullname + if filename.endswith('.xo'): + href = '#' + if filename.startswith('.'): + continue + if filename.endswith('.pyc'): + continue + files.append( { 'name': filename, + 'icon': icon, + 'href': href } ) + return render_response('filer.html', dict(files=files, absdir=os.path.normpath(directory))) + +@app.route('/shutdown') +def shutdown(): + shutdown_server() + return 'Server shutting down...' + +if __name__=="__main__": + port=int(sys.argv[1]) + app.run(port=port) + #or app.run(host='0.0.0.0') diff --git a/websdk/templates/editor.html b/websdk/templates/editor.html new file mode 100644 index 0000000..7fb4737 --- /dev/null +++ b/websdk/templates/editor.html @@ -0,0 +1,67 @@ + + + + + + Editor + + + + + + + + + + + +
    $content
    +
    +
    +
    + ${absdir}/
    + ${basename}
    +
    +
    +
    + + + + +
    +
    + + + + +
    +
    + + + diff --git a/websdk/templates/filer.html b/websdk/templates/filer.html new file mode 100644 index 0000000..eb2a003 --- /dev/null +++ b/websdk/templates/filer.html @@ -0,0 +1,44 @@ + + + + + + Filer + + + + + + +
    + ${absdir}/ +
    + + + + diff --git a/websdk/templates/index.html b/websdk/templates/index.html new file mode 100644 index 0000000..3b655dc --- /dev/null +++ b/websdk/templates/index.html @@ -0,0 +1,52 @@ + + + + + + + + + + + + +
    +

    Sugar WebSDK beta

    +
    +

    Technology Preview

    +

    The following are examples of things that can be achieved with WebSDK.

    +
      +
    • + - +

      Featuring Ace Editor and some tricks.

      +
    • +
    • +

      "Write less, do more". Jquery is a bag of candy.

      +
    • +
    • +
    • +
    • + +

      Not yet implemented.

      +
    • +
    • +

      Begin with right click, inspect element.

      +
    • +
    +
    +
    + + + diff --git a/websdk/templates/skel.html b/websdk/templates/skel.html new file mode 100644 index 0000000..65d351e --- /dev/null +++ b/websdk/templates/skel.html @@ -0,0 +1,19 @@ + + + + + + Filer + + + + + + + + diff --git a/websdk/templates/split-view.html b/websdk/templates/split-view.html new file mode 100644 index 0000000..e23b696 --- /dev/null +++ b/websdk/templates/split-view.html @@ -0,0 +1,10 @@ + + +basic frameset + + + + + + + diff --git a/websdk/templates/wysiwyg-editor.html b/websdk/templates/wysiwyg-editor.html new file mode 100644 index 0000000..3f162c9 --- /dev/null +++ b/websdk/templates/wysiwyg-editor.html @@ -0,0 +1,54 @@ + + + + + + Editor + + + + + + + +
    +
    + +
    +
    +
    +
    + ${absdir}/
    + ${basename}
    +
    +
    + + + +
    +
    + + + +
    +
    + + + diff --git a/websdk/webkit_local/__init__.py b/websdk/webkit_local/__init__.py new file mode 100644 index 0000000..e309204 --- /dev/null +++ b/websdk/webkit_local/__init__.py @@ -0,0 +1,5 @@ +#!/usr/bin/env python + +import webkit_local + +from webkit_local import * diff --git a/websdk/webkit_local/libWebKitGdk.so.1 b/websdk/webkit_local/libWebKitGdk.so.1 new file mode 100644 index 0000000..b2c1f0b --- /dev/null +++ b/websdk/webkit_local/libWebKitGdk.so.1 Binary files differ diff --git a/websdk/webkit_local/libxslt.so.1 b/websdk/webkit_local/libxslt.so.1 new file mode 100644 index 0000000..4dc90cd --- /dev/null +++ b/websdk/webkit_local/libxslt.so.1 Binary files differ diff --git a/websdk/webkit_local/webkit.so b/websdk/webkit_local/webkit.so new file mode 100644 index 0000000..bc94300 --- /dev/null +++ b/websdk/webkit_local/webkit.so Binary files differ diff --git a/websdk/webpy.py b/websdk/webpy.py new file mode 100644 index 0000000..428df6f --- /dev/null +++ b/websdk/webpy.py @@ -0,0 +1,174 @@ +#!/bin/env python +# -*- coding: UTF-8 -*- +# this file is deprecated in favor of studio.py +# left to implement couple of details to remove + +import os +import os.path +import sys +try: + import cherrypy +except ImportError: + import cherrypy_local as cherrypy +from genshi.template import TemplateLoader + +class Root(object): + + def __init__(self, data): + self.data = data + try: + self.bundle_dir = data['bundle_dir'] + except KeyError: + self.bundle_dir = os.curdir + self.loader = TemplateLoader( + os.path.join(self.bundle_dir, 'templates'), + auto_reload=True) + + @cherrypy.expose + def index(self): + port = cherrypy.config['server.socket_port'] + return '''Server is running on port %s.
    + Try pointing a browser at + http://localhost:%s/www/index.html''' % (port, port, port) + + @cherrypy.expose + def debug(self): + port = cherrypy.config['server.socket_port'] + return '''Try right clicking on any element on the main canvas and choosing "Inspect Element". +
    Return to main''' % (port) + + @cherrypy.expose + def candy(self): + return '''Not yet implemented. +
    Return to main''' % (port) + + @cherrypy.expose + def journal(self): + port = cherrypy.config['server.socket_port'] + return '''Not yet implemented. +
    Return to main''' % (port) + + @cherrypy.expose + def collaboration(self): + port = cherrypy.config['server.socket_port'] + return '''Not yet implemented. +
    Return to main''' % (port) + + def list_files(self, directory): + files=os.listdir(directory) + print "showing %s" % directory + return files + + @cherrypy.expose + def browse(self, directory="."): + filelist = self.list_files(directory) + files = [] + if not os.path.abspath(directory)==os.path.abspath("."): + files.append( { 'name': '..', + 'icon': 'folder.svg', + 'href': 'browse?directory=%s' % os.path.join(directory,"..") }) + for filename in sorted(filelist): + fullname = os.path.join(directory,filename) + icon = 'document-generic.svg' + href = 'edit?filename=%s&directory=%s' % (fullname,directory) + if filename.endswith('.py'): + icon = 'text-x-python.svg' + if filename.endswith('.html'): + icon = 'text-uri-list.svg' + if filename.endswith('.css'): + icon = 'text-uri-list.svg' + if filename.endswith('.js'): + icon = 'text-uri-list.svg' + if os.path.isdir(fullname): + icon = 'folder.svg' + href = 'browse?directory=%s' % fullname + if filename.endswith('.xo'): + href = '#' + if filename.startswith('.'): + continue + if filename.endswith('.pyc'): + continue + files.append( { 'name': filename, + 'icon': icon, + 'href': href } ) + + tmpl = self.loader.load('filer.html') + return tmpl.generate(files=files, absdir=os.path.normpath(directory) + ).render('html', doctype='html') + @cherrypy.expose + def vsplit(self, frame1="/browse", frame2="/browse"): + tmpl = self.loader.load('split-view.html') + return tmpl.generate(frame1=frame1, frame2=frame2 + ).render('html', doctype='html') + + + @cherrypy.expose + def edit(self, directory=".", filename="activity.py", editor="ace"): + icon = 'document-generic.svg' + mode = '' + if filename.endswith('.py'): + icon = 'text-x-python.svg' + mode = 'python' + if filename.endswith('.html'): + icon = 'text-uri-list.svg' + mode = 'html' + if filename.endswith('.css'): + icon = 'text-uri-list.svg' + mode = 'css' + if filename.endswith('.js'): + icon = 'text-uri-list.svg' + mode = 'javascript' + content = open(filename).read().decode('utf-8') + if editor=="wysiwyg": + tmpl = self.loader.load('wysiwyg-editor.html') + else: + tmpl = self.loader.load('editor.html') + return tmpl.generate(content=content, icon=icon,basename=os.path.basename(filename), + filename=filename, directory=directory, absdir=os.path.normpath(directory), + mode=mode).render('html', doctype='html', encoding='utf-8') + + @cherrypy.expose + def save(self, filename, content, directory): + f=open(filename,"wb") + content = content.replace('\r\n', '\n').replace('\r', '\n') # HACK + f.write(content) + print "saving content: %s" % filename + f.close() + href = "/browse?directory=%s" % directory + cherrypy.tools.redirect.callable(url=href, internal=False) + return "content saved: %s" % content + + @cherrypy.expose + def delete(self, filename): + os.unlink(filename) + cherrypy.tools.redirect.callable(url='/browse', internal=True) + +def start(root, port): + root = os.path.abspath(root) + print "root is %s" % root + data = {} + # Some global configuration; note that this could be moved into a + # configuration file + cherrypy.config.update({ + 'server.socket_port': port, + 'tools.encode.on': True, 'tools.encode.encoding': 'utf-8', + 'tools.decode.on': True, + 'tools.trailing_slash.on': True, + 'tools.staticdir.root': root, + }) + + cherrypy.quickstart(Root(data), '/', { + '/www': { + 'tools.staticdir.on': True, + 'tools.staticdir.dir': 'www' + } + }) + #cherrypy.tree.mount(Root({}), '/', { + # '/www': { + # 'tools.staticdir.on': True, + # 'tools.staticdir.dir': 'www' + #}) + #cherrypy.engine.start() + +if __name__ == '__main__': + start(os.path.abspath(os.curdir), int(sys.argv[1])) diff --git a/websdk/werkzeug/__init__.py b/websdk/werkzeug/__init__.py new file mode 100644 index 0000000..fea2c98 --- /dev/null +++ b/websdk/werkzeug/__init__.py @@ -0,0 +1,149 @@ +# -*- coding: utf-8 -*- +""" + werkzeug + ~~~~~~~~ + + Werkzeug is the Swiss Army knife of Python web development. + + It provides useful classes and functions for any WSGI application to make + the life of a python web developer much easier. All of the provided + classes are independent from each other so you can mix it with any other + library. + + + :copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" +from types import ModuleType +import sys + + +# the version. Usually set automatically by a script. +__version__ = '0.8-dev' + + +# This import magic raises concerns quite often which is why the implementation +# and motivation is explained here in detail now. +# +# The majority of the functions and classes provided by Werkzeug work on the +# HTTP and WSGI layer. There is no useful grouping for those which is why +# they are all importable from "werkzeug" instead of the modules where they are +# implemented. The downside of that is, that now everything would be loaded at +# once, even if unused. +# +# The implementation of a lazy-loading module in this file replaces the +# werkzeug package when imported from within. Attribute access to the werkzeug +# module will then lazily import from the modules that implement the objects. + + +# import mapping to objects in other modules +all_by_module = { + 'werkzeug.debug': ['DebuggedApplication'], + 'werkzeug.local': ['Local', 'LocalManager', 'LocalProxy', + 'LocalStack', 'release_local'], + 'werkzeug.templates': ['Template'], + 'werkzeug.serving': ['run_simple'], + 'werkzeug.test': ['Client', 'EnvironBuilder', 'create_environ', + 'run_wsgi_app'], + 'werkzeug.testapp': ['test_app'], + 'werkzeug.exceptions': ['abort', 'Aborter'], + 'werkzeug.urls': ['url_decode', 'url_encode', 'url_quote', + 'url_quote_plus', 'url_unquote', + 'url_unquote_plus', 'url_fix', 'Href', + 'iri_to_uri', 'uri_to_iri'], + 'werkzeug.formparser': ['parse_form_data'], + 'werkzeug.utils': ['escape', 'environ_property', + 'append_slash_redirect', 'redirect', + 'cached_property', 'import_string', + 'dump_cookie', 'parse_cookie', 'unescape', + 'format_string', 'find_modules', 'header_property', + 'html', 'xhtml', 'HTMLBuilder', + 'validate_arguments', 'ArgumentValidationError', + 'bind_arguments', 'secure_filename'], + 'werkzeug.wsgi': ['get_current_url', 'get_host', 'pop_path_info', + 'peek_path_info', 'SharedDataMiddleware', + 'DispatcherMiddleware', 'ClosingIterator', + 'FileWrapper', 'make_line_iter', 'LimitedStream', + 'responder', 'wrap_file', 'extract_path_info'], + 'werkzeug.datastructures': ['MultiDict', 'CombinedMultiDict', 'Headers', + 'EnvironHeaders', 'ImmutableList', + 'ImmutableDict', 'ImmutableMultiDict', + 'TypeConversionDict', 'ImmutableTypeConversionDict', + 'Accept', 'MIMEAccept', 'CharsetAccept', + 'LanguageAccept', 'RequestCacheControl', + 'ResponseCacheControl', 'ETags', 'HeaderSet', + 'WWWAuthenticate', 'Authorization', + 'FileMultiDict', 'CallbackDict', 'FileStorage', + 'OrderedMultiDict', 'ImmutableOrderedMultiDict'], + 'werkzeug.useragents': ['UserAgent'], + 'werkzeug.http': ['parse_etags', 'parse_date', 'http_date', + 'cookie_date', 'parse_cache_control_header', + 'is_resource_modified', 'parse_accept_header', + 'parse_set_header', 'quote_etag', 'unquote_etag', + 'generate_etag', 'dump_header', + 'parse_list_header', 'parse_dict_header', + 'parse_authorization_header', + 'parse_www_authenticate_header', + 'remove_entity_headers', 'is_entity_header', + 'remove_hop_by_hop_headers', 'parse_options_header', + 'dump_options_header', 'is_hop_by_hop_header', + 'unquote_header_value', + 'quote_header_value', 'HTTP_STATUS_CODES'], + 'werkzeug.wrappers': ['BaseResponse', 'BaseRequest', 'Request', + 'Response', 'AcceptMixin', 'ETagRequestMixin', + 'ETagResponseMixin', 'ResponseStreamMixin', + 'CommonResponseDescriptorsMixin', + 'UserAgentMixin', 'AuthorizationMixin', + 'WWWAuthenticateMixin', + 'CommonRequestDescriptorsMixin'], + 'werkzeug.security': ['generate_password_hash', 'check_password_hash'], + # the undocumented easteregg ;-) + 'werkzeug._internal': ['_easteregg'] +} + +# modules that should be imported when accessed as attributes of werkzeug +attribute_modules = frozenset(['exceptions', 'routing', 'script']) + + +object_origins = {} +for module, items in all_by_module.iteritems(): + for item in items: + object_origins[item] = module + + +class module(ModuleType): + """Automatically import objects from the modules.""" + + def __getattr__(self, name): + if name in object_origins: + module = __import__(object_origins[name], None, None, [name]) + for extra_name in all_by_module[module.__name__]: + setattr(self, extra_name, getattr(module, extra_name)) + return getattr(module, name) + elif name in attribute_modules: + __import__('werkzeug.' + name) + return ModuleType.__getattribute__(self, name) + + def __dir__(self): + """Just show what we want to show.""" + result = list(new_module.__all__) + result.extend(('__file__', '__path__', '__doc__', '__all__', + '__docformat__', '__name__', '__path__', + '__package__', '__version__')) + return result + +# keep a reference to this module so that it's not garbage collected +old_module = sys.modules['werkzeug'] + + +# setup the new module and patch it into the dict of loaded modules +new_module = sys.modules['werkzeug'] = module('werkzeug') +new_module.__dict__.update({ + '__file__': __file__, + '__package__': 'werkzeug', + '__path__': __path__, + '__doc__': __doc__, + '__version__': __version__, + '__all__': tuple(object_origins) + tuple(attribute_modules), + '__docformat__': 'restructuredtext en' +}) diff --git a/websdk/werkzeug/_internal.py b/websdk/werkzeug/_internal.py new file mode 100644 index 0000000..f148f14 --- /dev/null +++ b/websdk/werkzeug/_internal.py @@ -0,0 +1,405 @@ +# -*- coding: utf-8 -*- +""" + werkzeug._internal + ~~~~~~~~~~~~~~~~~~ + + This module provides internally used helpers and constants. + + :copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" +import inspect +from weakref import WeakKeyDictionary +from cStringIO import StringIO +from Cookie import SimpleCookie, Morsel, CookieError +from time import gmtime +from datetime import datetime, date + + +_logger = None +_empty_stream = StringIO('') +_signature_cache = WeakKeyDictionary() +_epoch_ord = date(1970, 1, 1).toordinal() + + +HTTP_STATUS_CODES = { + 100: 'Continue', + 101: 'Switching Protocols', + 102: 'Processing', + 200: 'OK', + 201: 'Created', + 202: 'Accepted', + 203: 'Non Authoritative Information', + 204: 'No Content', + 205: 'Reset Content', + 206: 'Partial Content', + 207: 'Multi Status', + 226: 'IM Used', # see RFC 3229 + 300: 'Multiple Choices', + 301: 'Moved Permanently', + 302: 'Found', + 303: 'See Other', + 304: 'Not Modified', + 305: 'Use Proxy', + 307: 'Temporary Redirect', + 400: 'Bad Request', + 401: 'Unauthorized', + 402: 'Payment Required', # unused + 403: 'Forbidden', + 404: 'Not Found', + 405: 'Method Not Allowed', + 406: 'Not Acceptable', + 407: 'Proxy Authentication Required', + 408: 'Request Timeout', + 409: 'Conflict', + 410: 'Gone', + 411: 'Length Required', + 412: 'Precondition Failed', + 413: 'Request Entity Too Large', + 414: 'Request URI Too Long', + 415: 'Unsupported Media Type', + 416: 'Requested Range Not Satisfiable', + 417: 'Expectation Failed', + 418: 'I\'m a teapot', # see RFC 2324 + 422: 'Unprocessable Entity', + 423: 'Locked', + 424: 'Failed Dependency', + 426: 'Upgrade Required', + 449: 'Retry With', # proprietary MS extension + 500: 'Internal Server Error', + 501: 'Not Implemented', + 502: 'Bad Gateway', + 503: 'Service Unavailable', + 504: 'Gateway Timeout', + 505: 'HTTP Version Not Supported', + 507: 'Insufficient Storage', + 510: 'Not Extended' +} + + +class _Missing(object): + + def __repr__(self): + return 'no value' + + def __reduce__(self): + return '_missing' + +_missing = _Missing() + + +def _proxy_repr(cls): + def proxy_repr(self): + return '%s(%s)' % (self.__class__.__name__, cls.__repr__(self)) + return proxy_repr + + +def _get_environ(obj): + env = getattr(obj, 'environ', obj) + assert isinstance(env, dict), \ + '%r is not a WSGI environment (has to be a dict)' % type(obj).__name__ + return env + + +def _log(type, message, *args, **kwargs): + """Log into the internal werkzeug logger.""" + global _logger + if _logger is None: + import logging + _logger = logging.getLogger('werkzeug') + # Only set up a default log handler if the + # end-user application didn't set anything up. + if not logging.root.handlers and _logger.level == logging.NOTSET: + _logger.setLevel(logging.INFO) + handler = logging.StreamHandler() + _logger.addHandler(handler) + getattr(_logger, type)(message.rstrip(), *args, **kwargs) + + +def _parse_signature(func): + """Return a signature object for the function.""" + if hasattr(func, 'im_func'): + func = func.im_func + + # if we have a cached validator for this function, return it + parse = _signature_cache.get(func) + if parse is not None: + return parse + + # inspect the function signature and collect all the information + positional, vararg_var, kwarg_var, defaults = inspect.getargspec(func) + defaults = defaults or () + arg_count = len(positional) + arguments = [] + for idx, name in enumerate(positional): + if isinstance(name, list): + raise TypeError('cannot parse functions that unpack tuples ' + 'in the function signature') + try: + default = defaults[idx - arg_count] + except IndexError: + param = (name, False, None) + else: + param = (name, True, default) + arguments.append(param) + arguments = tuple(arguments) + + def parse(args, kwargs): + new_args = [] + missing = [] + extra = {} + + # consume as many arguments as positional as possible + for idx, (name, has_default, default) in enumerate(arguments): + try: + new_args.append(args[idx]) + except IndexError: + try: + new_args.append(kwargs.pop(name)) + except KeyError: + if has_default: + new_args.append(default) + else: + missing.append(name) + else: + if name in kwargs: + extra[name] = kwargs.pop(name) + + # handle extra arguments + extra_positional = args[arg_count:] + if vararg_var is not None: + new_args.extend(extra_positional) + extra_positional = () + if kwargs and not kwarg_var is not None: + extra.update(kwargs) + kwargs = {} + + return new_args, kwargs, missing, extra, extra_positional, \ + arguments, vararg_var, kwarg_var + _signature_cache[func] = parse + return parse + + +def _patch_wrapper(old, new): + """Helper function that forwards all the function details to the + decorated function.""" + try: + new.__name__ = old.__name__ + new.__module__ = old.__module__ + new.__doc__ = old.__doc__ + new.__dict__ = old.__dict__ + except Exception: + pass + return new + + +def _decode_unicode(value, charset, errors): + """Like the regular decode function but this one raises an + `HTTPUnicodeError` if errors is `strict`.""" + fallback = None + if errors.startswith('fallback:'): + fallback = errors[9:] + errors = 'strict' + try: + return value.decode(charset, errors) + except UnicodeError, e: + if fallback is not None: + return value.decode(fallback, 'replace') + from werkzeug.exceptions import HTTPUnicodeError + raise HTTPUnicodeError(str(e)) + + +def _iter_modules(path): + """Iterate over all modules in a package.""" + import os + import pkgutil + if hasattr(pkgutil, 'iter_modules'): + for importer, modname, ispkg in pkgutil.iter_modules(path): + yield modname, ispkg + return + from inspect import getmodulename + from pydoc import ispackage + found = set() + for path in path: + for filename in os.listdir(path): + p = os.path.join(path, filename) + modname = getmodulename(filename) + if modname and modname != '__init__': + if modname not in found: + found.add(modname) + yield modname, ispackage(modname) + + +def _dump_date(d, delim): + """Used for `http_date` and `cookie_date`.""" + if d is None: + d = gmtime() + elif isinstance(d, datetime): + d = d.utctimetuple() + elif isinstance(d, (int, long, float)): + d = gmtime(d) + return '%s, %02d%s%s%s%s %02d:%02d:%02d GMT' % ( + ('Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun')[d.tm_wday], + d.tm_mday, delim, + ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', + 'Oct', 'Nov', 'Dec')[d.tm_mon - 1], + delim, str(d.tm_year), d.tm_hour, d.tm_min, d.tm_sec + ) + + +def _date_to_unix(arg): + """Converts a timetuple, integer or datetime object into the seconds from + epoch in utc. + """ + if isinstance(arg, datetime): + arg = arg.utctimetuple() + elif isinstance(arg, (int, long, float)): + return int(arg) + year, month, day, hour, minute, second = arg[:6] + days = date(year, month, 1).toordinal() - _epoch_ord + day - 1 + hours = days * 24 + hour + minutes = hours * 60 + minute + seconds = minutes * 60 + second + return seconds + + +class _ExtendedMorsel(Morsel): + _reserved = {'httponly': 'HttpOnly'} + _reserved.update(Morsel._reserved) + + def __init__(self, name=None, value=None): + Morsel.__init__(self) + if name is not None: + self.set(name, value, value) + + def OutputString(self, attrs=None): + httponly = self.pop('httponly', False) + result = Morsel.OutputString(self, attrs).rstrip('\t ;') + if httponly: + result += '; HttpOnly' + return result + + +class _ExtendedCookie(SimpleCookie): + """Form of the base cookie that doesn't raise a `CookieError` for + malformed keys. This has the advantage that broken cookies submitted + by nonstandard browsers don't cause the cookie to be empty. + """ + + def _BaseCookie__set(self, key, real_value, coded_value): + morsel = self.get(key, _ExtendedMorsel()) + try: + morsel.set(key, real_value, coded_value) + except CookieError: + pass + dict.__setitem__(self, key, morsel) + + +class _DictAccessorProperty(object): + """Baseclass for `environ_property` and `header_property`.""" + read_only = False + + def __init__(self, name, default=None, load_func=None, dump_func=None, + read_only=None, doc=None): + self.name = name + self.default = default + self.load_func = load_func + self.dump_func = dump_func + if read_only is not None: + self.read_only = read_only + self.__doc__ = doc + + def __get__(self, obj, type=None): + if obj is None: + return self + storage = self.lookup(obj) + if self.name not in storage: + return self.default + rv = storage[self.name] + if self.load_func is not None: + try: + rv = self.load_func(rv) + except (ValueError, TypeError): + rv = self.default + return rv + + def __set__(self, obj, value): + if self.read_only: + raise AttributeError('read only property') + if self.dump_func is not None: + value = self.dump_func(value) + self.lookup(obj)[self.name] = value + + def __delete__(self, obj): + if self.read_only: + raise AttributeError('read only property') + self.lookup(obj).pop(self.name, None) + + def __repr__(self): + return '<%s %s>' % ( + self.__class__.__name__, + self.name + ) + + +def _easteregg(app): + """Like the name says. But who knows how it works?""" + gyver = '\n'.join([x + (77 - len(x)) * ' ' for x in ''' +eJyFlzuOJDkMRP06xRjymKgDJCDQStBYT8BCgK4gTwfQ2fcFs2a2FzvZk+hvlcRvRJD148efHt9m +9Xz94dRY5hGt1nrYcXx7us9qlcP9HHNh28rz8dZj+q4rynVFFPdlY4zH873NKCexrDM6zxxRymzz +4QIxzK4bth1PV7+uHn6WXZ5C4ka/+prFzx3zWLMHAVZb8RRUxtFXI5DTQ2n3Hi2sNI+HK43AOWSY +jmEzE4naFp58PdzhPMdslLVWHTGUVpSxImw+pS/D+JhzLfdS1j7PzUMxij+mc2U0I9zcbZ/HcZxc +q1QjvvcThMYFnp93agEx392ZdLJWXbi/Ca4Oivl4h/Y1ErEqP+lrg7Xa4qnUKu5UE9UUA4xeqLJ5 +jWlPKJvR2yhRI7xFPdzPuc6adXu6ovwXwRPXXnZHxlPtkSkqWHilsOrGrvcVWXgGP3daXomCj317 +8P2UOw/NnA0OOikZyFf3zZ76eN9QXNwYdD8f8/LdBRFg0BO3bB+Pe/+G8er8tDJv83XTkj7WeMBJ +v/rnAfdO51d6sFglfi8U7zbnr0u9tyJHhFZNXYfH8Iafv2Oa+DT6l8u9UYlajV/hcEgk1x8E8L/r +XJXl2SK+GJCxtnyhVKv6GFCEB1OO3f9YWAIEbwcRWv/6RPpsEzOkXURMN37J0PoCSYeBnJQd9Giu +LxYQJNlYPSo/iTQwgaihbART7Fcyem2tTSCcwNCs85MOOpJtXhXDe0E7zgZJkcxWTar/zEjdIVCk +iXy87FW6j5aGZhttDBoAZ3vnmlkx4q4mMmCdLtnHkBXFMCReqthSGkQ+MDXLLCpXwBs0t+sIhsDI +tjBB8MwqYQpLygZ56rRHHpw+OAVyGgaGRHWy2QfXez+ZQQTTBkmRXdV/A9LwH6XGZpEAZU8rs4pE +1R4FQ3Uwt8RKEtRc0/CrANUoes3EzM6WYcFyskGZ6UTHJWenBDS7h163Eo2bpzqxNE9aVgEM2CqI +GAJe9Yra4P5qKmta27VjzYdR04Vc7KHeY4vs61C0nbywFmcSXYjzBHdiEjraS7PGG2jHHTpJUMxN +Jlxr3pUuFvlBWLJGE3GcA1/1xxLcHmlO+LAXbhrXah1tD6Ze+uqFGdZa5FM+3eHcKNaEarutAQ0A +QMAZHV+ve6LxAwWnXbbSXEG2DmCX5ijeLCKj5lhVFBrMm+ryOttCAeFpUdZyQLAQkA06RLs56rzG +8MID55vqr/g64Qr/wqwlE0TVxgoiZhHrbY2h1iuuyUVg1nlkpDrQ7Vm1xIkI5XRKLedN9EjzVchu +jQhXcVkjVdgP2O99QShpdvXWoSwkp5uMwyjt3jiWCqWGSiaaPAzohjPanXVLbM3x0dNskJsaCEyz +DTKIs+7WKJD4ZcJGfMhLFBf6hlbnNkLEePF8Cx2o2kwmYF4+MzAxa6i+6xIQkswOqGO+3x9NaZX8 +MrZRaFZpLeVTYI9F/djY6DDVVs340nZGmwrDqTCiiqD5luj3OzwpmQCiQhdRYowUYEA3i1WWGwL4 +GCtSoO4XbIPFeKGU13XPkDf5IdimLpAvi2kVDVQbzOOa4KAXMFlpi/hV8F6IDe0Y2reg3PuNKT3i +RYhZqtkQZqSB2Qm0SGtjAw7RDwaM1roESC8HWiPxkoOy0lLTRFG39kvbLZbU9gFKFRvixDZBJmpi +Xyq3RE5lW00EJjaqwp/v3EByMSpVZYsEIJ4APaHmVtpGSieV5CALOtNUAzTBiw81GLgC0quyzf6c +NlWknzJeCsJ5fup2R4d8CYGN77mu5vnO1UqbfElZ9E6cR6zbHjgsr9ly18fXjZoPeDjPuzlWbFwS +pdvPkhntFvkc13qb9094LL5NrA3NIq3r9eNnop9DizWOqCEbyRBFJTHn6Tt3CG1o8a4HevYh0XiJ +sR0AVVHuGuMOIfbuQ/OKBkGRC6NJ4u7sbPX8bG/n5sNIOQ6/Y/BX3IwRlTSabtZpYLB85lYtkkgm +p1qXK3Du2mnr5INXmT/78KI12n11EFBkJHHp0wJyLe9MvPNUGYsf+170maayRoy2lURGHAIapSpQ +krEDuNoJCHNlZYhKpvw4mspVWxqo415n8cD62N9+EfHrAvqQnINStetek7RY2Urv8nxsnGaZfRr/ +nhXbJ6m/yl1LzYqscDZA9QHLNbdaSTTr+kFg3bC0iYbX/eQy0Bv3h4B50/SGYzKAXkCeOLI3bcAt +mj2Z/FM1vQWgDynsRwNvrWnJHlespkrp8+vO1jNaibm+PhqXPPv30YwDZ6jApe3wUjFQobghvW9p +7f2zLkGNv8b191cD/3vs9Q833z8t'''.decode('base64').decode('zlib').splitlines()]) + def easteregged(environ, start_response): + def injecting_start_response(status, headers, exc_info=None): + headers.append(('X-Powered-By', 'Werkzeug')) + return start_response(status, headers, exc_info) + if environ.get('QUERY_STRING') != 'macgybarchakku': + return app(environ, injecting_start_response) + injecting_start_response('200 OK', [('Content-Type', 'text/html')]) + return [''' + + + +About Werkzeug + + + +

    Werkzeug

    +

    the Swiss Army knife of Python web development.

    +
    %s\n\n\n
    + +''' % gyver] + return easteregged diff --git a/websdk/werkzeug/contrib/__init__.py b/websdk/werkzeug/contrib/__init__.py new file mode 100644 index 0000000..ffc48c9 --- /dev/null +++ b/websdk/werkzeug/contrib/__init__.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +""" + werkzeug.contrib + ~~~~~~~~~~~~~~~~ + + Contains user-submitted code that other users may find useful, but which + is not part of the Werkzeug core. Anyone can write code for inclusion in + the `contrib` package. All modules in this package are distributed as an + add-on library and thus are not part of Werkzeug itself. + + This file itself is mostly for informational purposes and to tell the + Python interpreter that `contrib` is a package. + + :copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" diff --git a/websdk/werkzeug/contrib/atom.py b/websdk/werkzeug/contrib/atom.py new file mode 100644 index 0000000..7aaa2fb --- /dev/null +++ b/websdk/werkzeug/contrib/atom.py @@ -0,0 +1,343 @@ +# -*- coding: utf-8 -*- +""" + werkzeug.contrib.atom + ~~~~~~~~~~~~~~~~~~~~~ + + This module provides a class called :class:`AtomFeed` which can be + used to generate feeds in the Atom syndication format (see :rfc:`4287`). + + Example:: + + def atom_feed(request): + feed = AtomFeed("My Blog", feed_url=request.url, + url=request.host_url, + subtitle="My example blog for a feed test.") + for post in Post.query.limit(10).all(): + feed.add(post.title, post.body, content_type='html', + author=post.author, url=post.url, id=post.uid, + updated=post.last_update, published=post.pub_date) + return feed.get_response() + + :copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" +from datetime import datetime +from werkzeug.utils import escape +from werkzeug.wrappers import BaseResponse + + +XHTML_NAMESPACE = 'http://www.w3.org/1999/xhtml' + + +def _make_text_block(name, content, content_type=None): + """Helper function for the builder that creates an XML text block.""" + if content_type == 'xhtml': + return u'<%s type="xhtml">
    %s
    \n' % \ + (name, XHTML_NAMESPACE, content, name) + if not content_type: + return u'<%s>%s\n' % (name, escape(content), name) + return u'<%s type="%s">%s\n' % (name, content_type, + escape(content), name) + + +def format_iso8601(obj): + """Format a datetime object for iso8601""" + return obj.strftime('%Y-%m-%dT%H:%M:%SZ') + + +class AtomFeed(object): + """A helper class that creates Atom feeds. + + :param title: the title of the feed. Required. + :param title_type: the type attribute for the title element. One of + ``'html'``, ``'text'`` or ``'xhtml'``. + :param url: the url for the feed (not the url *of* the feed) + :param id: a globally unique id for the feed. Must be an URI. If + not present the `feed_url` is used, but one of both is + required. + :param updated: the time the feed was modified the last time. Must + be a :class:`datetime.datetime` object. If not + present the latest entry's `updated` is used. + :param feed_url: the URL to the feed. Should be the URL that was + requested. + :param author: the author of the feed. Must be either a string (the + name) or a dict with name (required) and uri or + email (both optional). Can be a list of (may be + mixed, too) strings and dicts, too, if there are + multiple authors. Required if not every entry has an + author element. + :param icon: an icon for the feed. + :param logo: a logo for the feed. + :param rights: copyright information for the feed. + :param rights_type: the type attribute for the rights element. One of + ``'html'``, ``'text'`` or ``'xhtml'``. Default is + ``'text'``. + :param subtitle: a short description of the feed. + :param subtitle_type: the type attribute for the subtitle element. + One of ``'text'``, ``'html'``, ``'text'`` + or ``'xhtml'``. Default is ``'text'``. + :param links: additional links. Must be a list of dictionaries with + href (required) and rel, type, hreflang, title, length + (all optional) + :param generator: the software that generated this feed. This must be + a tuple in the form ``(name, url, version)``. If + you don't want to specify one of them, set the item + to `None`. + :param entries: a list with the entries for the feed. Entries can also + be added later with :meth:`add`. + + For more information on the elements see + http://www.atomenabled.org/developers/syndication/ + + Everywhere where a list is demanded, any iterable can be used. + """ + + default_generator = ('Werkzeug', None, None) + + def __init__(self, title=None, entries=None, **kwargs): + self.title = title + self.title_type = kwargs.get('title_type', 'text') + self.url = kwargs.get('url') + self.feed_url = kwargs.get('feed_url', self.url) + self.id = kwargs.get('id', self.feed_url) + self.updated = kwargs.get('updated') + self.author = kwargs.get('author', ()) + self.icon = kwargs.get('icon') + self.logo = kwargs.get('logo') + self.rights = kwargs.get('rights') + self.rights_type = kwargs.get('rights_type') + self.subtitle = kwargs.get('subtitle') + self.subtitle_type = kwargs.get('subtitle_type', 'text') + self.generator = kwargs.get('generator') + if self.generator is None: + self.generator = self.default_generator + self.links = kwargs.get('links', []) + self.entries = entries and list(entries) or [] + + if not hasattr(self.author, '__iter__') \ + or isinstance(self.author, (basestring, dict)): + self.author = [self.author] + for i, author in enumerate(self.author): + if not isinstance(author, dict): + self.author[i] = {'name': author} + + if not self.title: + raise ValueError('title is required') + if not self.id: + raise ValueError('id is required') + for author in self.author: + if 'name' not in author: + raise TypeError('author must contain at least a name') + + def add(self, *args, **kwargs): + """Add a new entry to the feed. This function can either be called + with a :class:`FeedEntry` or some keyword and positional arguments + that are forwarded to the :class:`FeedEntry` constructor. + """ + if len(args) == 1 and not kwargs and isinstance(args[0], FeedEntry): + self.entries.append(args[0]) + else: + kwargs['feed_url'] = self.feed_url + self.entries.append(FeedEntry(*args, **kwargs)) + + def __repr__(self): + return '<%s %r (%d entries)>' % ( + self.__class__.__name__, + self.title, + len(self.entries) + ) + + def generate(self): + """Return a generator that yields pieces of XML.""" + # atom demands either an author element in every entry or a global one + if not self.author: + if False in map(lambda e: bool(e.author), self.entries): + self.author = ({'name': 'Unknown author'},) + + if not self.updated: + dates = sorted([entry.updated for entry in self.entries]) + self.updated = dates and dates[-1] or datetime.utcnow() + + yield u'\n' + yield u'\n' + yield ' ' + _make_text_block('title', self.title, self.title_type) + yield u' %s\n' % escape(self.id) + yield u' %s\n' % format_iso8601(self.updated) + if self.url: + yield u' \n' % escape(self.url, True) + if self.feed_url: + yield u' \n' % \ + escape(self.feed_url, True) + for link in self.links: + yield u' \n' % ''.join('%s="%s" ' % \ + (k, escape(link[k], True)) for k in link) + for author in self.author: + yield u' \n' + yield u' %s\n' % escape(author['name']) + if 'uri' in author: + yield u' %s\n' % escape(author['uri']) + if 'email' in author: + yield ' %s\n' % escape(author['email']) + yield ' \n' + if self.subtitle: + yield ' ' + _make_text_block('subtitle', self.subtitle, + self.subtitle_type) + if self.icon: + yield u' %s\n' % escape(self.icon) + if self.logo: + yield u' %s\n' % escape(self.logo) + if self.rights: + yield ' ' + _make_text_block('rights', self.rights, + self.rights_type) + generator_name, generator_url, generator_version = self.generator + if generator_name or generator_url or generator_version: + tmp = [u' %s\n' % escape(generator_name)) + yield u''.join(tmp) + for entry in self.entries: + for line in entry.generate(): + yield u' ' + line + yield u'\n' + + def to_string(self): + """Convert the feed into a string.""" + return u''.join(self.generate()) + + def get_response(self): + """Return a response object for the feed.""" + return BaseResponse(self.to_string(), mimetype='application/atom+xml') + + def __call__(self, environ, start_response): + """Use the class as WSGI response object.""" + return self.get_response()(environ, start_response) + + def __unicode__(self): + return self.to_string() + + def __str__(self): + return self.to_string().encode('utf-8') + + +class FeedEntry(object): + """Represents a single entry in a feed. + + :param title: the title of the entry. Required. + :param title_type: the type attribute for the title element. One of + ``'html'``, ``'text'`` or ``'xhtml'``. + :param content: the content of the entry. + :param content_type: the type attribute for the content element. One + of ``'html'``, ``'text'`` or ``'xhtml'``. + :param summary: a summary of the entry's content. + :param summary_type: the type attribute for the summary element. One + of ``'html'``, ``'text'`` or ``'xhtml'``. + :param url: the url for the entry. + :param id: a globally unique id for the entry. Must be an URI. If + not present the URL is used, but one of both is required. + :param updated: the time the entry was modified the last time. Must + be a :class:`datetime.datetime` object. Required. + :param author: the author of the feed. Must be either a string (the + name) or a dict with name (required) and uri or + email (both optional). Can be a list of (may be + mixed, too) strings and dicts, too, if there are + multiple authors. Required if not every entry has an + author element. + :param published: the time the entry was initially published. Must + be a :class:`datetime.datetime` object. + :param rights: copyright information for the entry. + :param rights_type: the type attribute for the rights element. One of + ``'html'``, ``'text'`` or ``'xhtml'``. Default is + ``'text'``. + :param links: additional links. Must be a list of dictionaries with + href (required) and rel, type, hreflang, title, length + (all optional) + :param xml_base: The xml base (url) for this feed item. If not provided + it will default to the item url. + + For more information on the elements see + http://www.atomenabled.org/developers/syndication/ + + Everywhere where a list is demanded, any iterable can be used. + """ + + def __init__(self, title=None, content=None, feed_url=None, **kwargs): + self.title = title + self.title_type = kwargs.get('title_type', 'text') + self.content = content + self.content_type = kwargs.get('content_type', 'html') + self.url = kwargs.get('url') + self.id = kwargs.get('id', self.url) + self.updated = kwargs.get('updated') + self.summary = kwargs.get('summary') + self.summary_type = kwargs.get('summary_type', 'html') + self.author = kwargs.get('author') + self.published = kwargs.get('published') + self.rights = kwargs.get('rights') + self.links = kwargs.get('links', []) + self.xml_base = kwargs.get('xml_base', feed_url) + + if not hasattr(self.author, '__iter__') \ + or isinstance(self.author, (basestring, dict)): + self.author = [self.author] + for i, author in enumerate(self.author): + if not isinstance(author, dict): + self.author[i] = {'name': author} + + if not self.title: + raise ValueError('title is required') + if not self.id: + raise ValueError('id is required') + if not self.updated: + raise ValueError('updated is required') + + def __repr__(self): + return '<%s %r>' % ( + self.__class__.__name__, + self.title + ) + + def generate(self): + """Yields pieces of ATOM XML.""" + base = '' + if self.xml_base: + base = ' xml:base="%s"' % escape(self.xml_base, True) + yield u'\n' % base + yield u' ' + _make_text_block('title', self.title, self.title_type) + yield u' %s\n' % escape(self.id) + yield u' %s\n' % format_iso8601(self.updated) + if self.published: + yield u' %s\n' % \ + format_iso8601(self.published) + if self.url: + yield u' \n' % escape(self.url) + for author in self.author: + yield u' \n' + yield u' %s\n' % escape(author['name']) + if 'uri' in author: + yield u' %s\n' % escape(author['uri']) + if 'email' in author: + yield u' %s\n' % escape(author['email']) + yield u' \n' + for link in self.links: + yield u' \n' % ''.join('%s="%s" ' % \ + (k, escape(link[k], True)) for k in link) + if self.summary: + yield u' ' + _make_text_block('summary', self.summary, + self.summary_type) + if self.content: + yield u' ' + _make_text_block('content', self.content, + self.content_type) + yield u'\n' + + def to_string(self): + """Convert the feed item into a unicode object.""" + return u''.join(self.generate()) + + def __unicode__(self): + return self.to_string() + + def __str__(self): + return self.to_string().encode('utf-8') diff --git a/websdk/werkzeug/contrib/cache.py b/websdk/werkzeug/contrib/cache.py new file mode 100644 index 0000000..0526c48 --- /dev/null +++ b/websdk/werkzeug/contrib/cache.py @@ -0,0 +1,635 @@ +# -*- coding: utf-8 -*- +""" + werkzeug.contrib.cache + ~~~~~~~~~~~~~~~~~~~~~~ + + The main problem with dynamic Web sites is, well, they're dynamic. Each + time a user requests a page, the webserver executes a lot of code, queries + the database, renders templates until the visitor gets the page he sees. + + This is a lot more expensive than just loading a file from the file system + and sending it to the visitor. + + For most Web applications, this overhead isn't a big deal but once it + becomes, you will be glad to have a cache system in place. + + How Caching Works + ================= + + Caching is pretty simple. Basically you have a cache object lurking around + somewhere that is connected to a remote cache or the file system or + something else. When the request comes in you check if the current page + is already in the cache and if so, you're returning it from the cache. + Otherwise you generate the page and put it into the cache. (Or a fragment + of the page, you don't have to cache the full thing) + + Here is a simple example of how to cache a sidebar for a template:: + + def get_sidebar(user): + identifier = 'sidebar_for/user%d' % user.id + value = cache.get(identifier) + if value is not None: + return value + value = generate_sidebar_for(user=user) + cache.set(identifier, value, timeout=60 * 5) + return value + + Creating a Cache Object + ======================= + + To create a cache object you just import the cache system of your choice + from the cache module and instantiate it. Then you can start working + with that object: + + >>> from werkzeug.contrib.cache import SimpleCache + >>> c = SimpleCache() + >>> c.set("foo", "value") + >>> c.get("foo") + 'value' + >>> c.get("missing") is None + True + + Please keep in mind that you have to create the cache and put it somewhere + you have access to it (either as a module global you can import or you just + put it into your WSGI application). + + :copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" +import os +import re +import tempfile +try: + from hashlib import md5 +except ImportError: + from md5 import new as md5 +from itertools import izip +from time import time +from cPickle import loads, dumps, load, dump, HIGHEST_PROTOCOL +from werkzeug.posixemulation import rename + +def _items(mappingorseq): + """Wrapper for efficient iteration over mappings represented by dicts + or sequences:: + + >>> for k, v in _items((i, i*i) for i in xrange(5)): + ... assert k*k == v + + >>> for k, v in _items(dict((i, i*i) for i in xrange(5))): + ... assert k*k == v + + """ + return mappingorseq.iteritems() if hasattr(mappingorseq, 'iteritems') \ + else mappingorseq + +class BaseCache(object): + """Baseclass for the cache systems. All the cache systems implement this + API or a superset of it. + + :param default_timeout: the default timeout that is used if no timeout is + specified on :meth:`set`. + """ + + def __init__(self, default_timeout=300): + self.default_timeout = default_timeout + + def get(self, key): + """Looks up key in the cache and returns the value for it. + If the key does not exist `None` is returned instead. + + :param key: the key to be looked up. + """ + return None + + def delete(self, key): + """Deletes `key` from the cache. If it does not exist in the cache + nothing happens. + + :param key: the key to delete. + """ + pass + + def get_many(self, *keys): + """Returns a list of values for the given keys. + For each key a item in the list is created. Example:: + + foo, bar = cache.get_many("foo", "bar") + + If a key can't be looked up `None` is returned for that key + instead. + + :param keys: The function accepts multiple keys as positional + arguments. + """ + return map(self.get, keys) + + def get_dict(self, *keys): + """Works like :meth:`get_many` but returns a dict:: + + d = cache.get_dict("foo", "bar") + foo = d["foo"] + bar = d["bar"] + + :param keys: The function accepts multiple keys as positional + arguments. + """ + return dict(izip(keys, self.get_many(*keys))) + + def set(self, key, value, timeout=None): + """Adds a new key/value to the cache (overwrites value, if key already + exists in the cache). + + :param key: the key to set + :param value: the value for the key + :param timeout: the cache timeout for the key (if not specified, + it uses the default timeout). + """ + pass + + def add(self, key, value, timeout=None): + """Works like :meth:`set` but does not overwrite the values of already + existing keys. + + :param key: the key to set + :param value: the value for the key + :param timeout: the cache timeout for the key or the default + timeout if not specified. + """ + pass + + def set_many(self, mapping, timeout=None): + """Sets multiple keys and values from a mapping. + + :param mapping: a mapping with the keys/values to set. + :param timeout: the cache timeout for the key (if not specified, + it uses the default timeout). + """ + for key, value in _items(mapping): + self.set(key, value, timeout) + + def delete_many(self, *keys): + """Deletes multiple keys at once. + + :param keys: The function accepts multiple keys as positional + arguments. + """ + for key in keys: + self.delete(key) + + def clear(self): + """Clears the cache. Keep in mind that not all caches support + completely clearing the cache. + """ + pass + + def inc(self, key, delta=1): + """Increments the value of a key by `delta`. If the key does + not yet exist it is initialized with `delta`. + + For supporting caches this is an atomic operation. + + :param key: the key to increment. + :param delta: the delta to add. + """ + self.set(key, (self.get(key) or 0) + delta) + + def dec(self, key, delta=1): + """Decrements the value of a key by `delta`. If the key does + not yet exist it is initialized with `-delta`. + + For supporting caches this is an atomic operation. + + :param key: the key to increment. + :param delta: the delta to subtract. + """ + self.set(key, (self.get(key) or 0) - delta) + + +class NullCache(BaseCache): + """A cache that doesn't cache. This can be useful for unit testing. + + :param default_timeout: a dummy parameter that is ignored but exists + for API compatibility with other caches. + """ + + +class SimpleCache(BaseCache): + """Simple memory cache for single process environments. This class exists + mainly for the development server and is not 100% thread safe. It tries + to use as many atomic operations as possible and no locks for simplicity + but it could happen under heavy load that keys are added multiple times. + + :param threshold: the maximum number of items the cache stores before + it starts deleting some. + :param default_timeout: the default timeout that is used if no timeout is + specified on :meth:`~BaseCache.set`. + """ + + def __init__(self, threshold=500, default_timeout=300): + BaseCache.__init__(self, default_timeout) + self._cache = {} + self.clear = self._cache.clear + self._threshold = threshold + + def _prune(self): + if len(self._cache) > self._threshold: + now = time() + for idx, (key, (expires, _)) in enumerate(self._cache.items()): + if expires <= now or idx % 3 == 0: + self._cache.pop(key, None) + + def get(self, key): + now = time() + expires, value = self._cache.get(key, (0, None)) + if expires > time(): + return loads(value) + + def set(self, key, value, timeout=None): + if timeout is None: + timeout = self.default_timeout + self._prune() + self._cache[key] = (time() + timeout, dumps(value, HIGHEST_PROTOCOL)) + + def add(self, key, value, timeout=None): + if timeout is None: + timeout = self.default_timeout + if len(self._cache) > self._threshold: + self._prune() + item = (time() + timeout, dumps(value, HIGHEST_PROTOCOL)) + self._cache.setdefault(key, item) + + def delete(self, key): + self._cache.pop(key, None) + + +_test_memcached_key = re.compile(r'[^\x00-\x21\xff]{1,250}$').match + +class MemcachedCache(BaseCache): + """A cache that uses memcached as backend. + + The first argument can either be an object that resembles the API of a + :class:`memcache.Client` or a tuple/list of server addresses. In the + event that a tuple/list is passed, Werkzeug tries to import the best + available memcache library. + + Implementation notes: This cache backend works around some limitations in + memcached to simplify the interface. For example unicode keys are encoded + to utf-8 on the fly. Methods such as :meth:`~BaseCache.get_dict` return + the keys in the same format as passed. Furthermore all get methods + silently ignore key errors to not cause problems when untrusted user data + is passed to the get methods which is often the case in web applications. + + :param servers: a list or tuple of server addresses or alternatively + a :class:`memcache.Client` or a compatible client. + :param default_timeout: the default timeout that is used if no timeout is + specified on :meth:`~BaseCache.set`. + :param key_prefix: a prefix that is added before all keys. This makes it + possible to use the same memcached server for different + applications. Keep in mind that + :meth:`~BaseCache.clear` will also clear keys with a + different prefix. + """ + + def __init__(self, servers, default_timeout=300, key_prefix=None): + BaseCache.__init__(self, default_timeout) + if isinstance(servers, (list, tuple)): + self._client = self.import_preferred_memcache_lib(servers) + else: + # NOTE: servers is actually an already initialized memcache + # client. + self._client = servers + + self.key_prefix = key_prefix + + + def get(self, key): + if isinstance(key, unicode): + key = key.encode('utf-8') + if self.key_prefix: + key = self.key_prefix + key + # memcached doesn't support keys longer than that. Because often + # checks for so long keys can occour because it's tested from user + # submitted data etc we fail silently for getting. + if _test_memcached_key(key): + return self._client.get(key) + + def get_dict(self, *keys): + key_mapping = {} + have_encoded_keys = False + for key in keys: + if isinstance(key, unicode): + encoded_key = key.encode('utf-8') + have_encoded_keys = True + else: + encoded_key = key + if self.key_prefix: + encoded_key = self.key_prefix + encoded_key + if _test_memcached_key(key): + key_mapping[encoded_key] = key + d = rv = self._client.get_multi(key_mapping.keys()) + if have_encoded_keys or self.key_prefix: + rv = {} + for key, value in d.iteritems(): + rv[key_mapping[key]] = value + if len(rv) < len(keys): + for key in keys: + if key not in rv: + rv[key] = None + return rv + + def add(self, key, value, timeout=None): + if timeout is None: + timeout = self.default_timeout + if isinstance(key, unicode): + key = key.encode('utf-8') + if self.key_prefix: + key = self.key_prefix + key + self._client.add(key, value, timeout) + + def set(self, key, value, timeout=None): + if timeout is None: + timeout = self.default_timeout + if isinstance(key, unicode): + key = key.encode('utf-8') + if self.key_prefix: + key = self.key_prefix + key + self._client.set(key, value, timeout) + + def get_many(self, *keys): + d = self.get_dict(*keys) + return [d[key] for key in keys] + + def set_many(self, mapping, timeout=None): + if timeout is None: + timeout = self.default_timeout + new_mapping = {} + for key, value in _items(mapping): + if isinstance(key, unicode): + key = key.encode('utf-8') + if self.key_prefix: + key = self.key_prefix + key + new_mapping[key] = value + self._client.set_multi(new_mapping, timeout) + + def delete(self, key): + if isinstance(key, unicode): + key = key.encode('utf-8') + if self.key_prefix: + key = self.key_prefix + key + if _test_memcached_key(key): + self._client.delete(key) + + def delete_many(self, *keys): + new_keys = [] + for key in keys: + if isinstance(key, unicode): + key = key.encode('utf-8') + if self.key_prefix: + key = self.key_prefix + key + if _test_memcached_key(key): + new_keys.append(key) + self._client.delete_multi(new_keys) + + def clear(self): + self._client.flush_all() + + def inc(self, key, delta=1): + if isinstance(key, unicode): + key = key.encode('utf-8') + if self.key_prefix: + key = self.key_prefix + key + self._client.incr(key, delta) + + def dec(self, key, delta=1): + if isinstance(key, unicode): + key = key.encode('utf-8') + if self.key_prefix: + key = self.key_prefix + key + self._client.decr(key, delta) + + + def import_preferred_memcache_lib(self, servers): + """ Returns an initialized memcache client """ + try: + import pylibmc + return pylibmc.Client(servers) + except ImportError: + pass + + try: + from google.appengine.api import memcache + return memcache.Client() + except ImportError: + pass + + try: + import memcache + return memcache.Client(servers) + except ImportError: + pass + + # If you're seeing this, either you need to install a memcache client + # or you need to monkey patch this method to support your + # environment. + raise RuntimeError('no memcache module found') + + +# backwards compatibility +GAEMemcachedCache = MemcachedCache + + +class RedisCache(BaseCache): + """Uses the Redis key-value store as a cache backend. + + The first argument can be either a string denoting address of the Redis + server or an object resembling an instance of a redis.Redis class. + + Note: Python Redis API already takes care of encoding unicode strings on + the fly. + + .. versionadded:: 0.7 + + .. versionadded:: 0.8 + `key_prefix` was added. + + :param host: address of the Redis server or an object which API is + compatible with the official Python Redis client (redis-py). + :param port: port number on which Redis server listens for connections + :param default_timeout: the default timeout that is used if no timeout is + specified on :meth:`~BaseCache.set`. + :param key_prefix: A prefix that should be added to all keys. + """ + def __init__(self, host='localhost', port=6379, default_timeout=300, + key_prefix=None): + BaseCache.__init__(self, default_timeout) + if isinstance(host, basestring): + try: + import redis + except ImportError: + raise RuntimeError('no redis module found') + self._client = redis.Redis(host=host, port=port) + else: + self._client = host + self.key_prefix = key_prefix or '' + + def get(self, key): + return self._client.get(self.key_prefix + key) + + def get_many(self, *keys): + if self.key_prefix: + keys = [self.key_prefix + key for key in keys] + return self._client.mget(keys) + + def set(self, key, value, timeout=None): + if timeout is None: + timeout = self.default_timeout + self._client.setex(self.key_prefix + key, value, timeout) + + def add(self, key, value, timeout=None): + if timeout is None: + timeout = self.default_timeout + added = self._client.setnx(self.key_prefix + key, value) + if added: + self._client.expire(self.key_prefix + key, timeout) + + def set_many(self, mapping, timeout=None): + if timeout is None: + timeout = self.default_timeout + pipe = self._client.pipeline() + for key, value in _items(mapping): + pipe.setex(self.key_prefix + key, value, timeout) + pipe.execute() + + def delete(self, key): + self._client.delete(self.key_prefix + key) + + def delete_many(self, *keys): + if not keys: + return + if self.key_prefix: + keys = [self.key_prefix + key for key in keys] + self._client.delete(*keys) + + def clear(self): + if self.key_prefix: + keys = self._client.keys(self.key_prefix + '*') + if keys: + self._client.delete(*keys) + else: + self._client.flushdb() + + def inc(self, key, delta=1): + return self._client.incr(self.key_prefix + key, delta) + + def dec(self, key, delta=1): + return self._client.decr(self.key_prefix + key, delta) + + +class FileSystemCache(BaseCache): + """A cache that stores the items on the file system. This cache depends + on being the only user of the `cache_dir`. Make absolutely sure that + nobody but this cache stores files there or otherwise the cache will + randomly delete files therein. + + :param cache_dir: the directory where cache files are stored. + :param threshold: the maximum number of items the cache stores before + it starts deleting some. + :param default_timeout: the default timeout that is used if no timeout is + specified on :meth:`~BaseCache.set`. + :param mode: the file mode wanted for the cache files, default 0600 + """ + + #: used for temporary files by the FileSystemCache + _fs_transaction_suffix = '.__wz_cache' + + def __init__(self, cache_dir, threshold=500, default_timeout=300, mode=0600): + BaseCache.__init__(self, default_timeout) + self._path = cache_dir + self._threshold = threshold + self._mode = mode + if not os.path.exists(self._path): + os.makedirs(self._path) + + def _list_dir(self): + """return a list of (fully qualified) cache filenames + """ + return [os.path.join(self._path, fn) for fn in os.listdir(self._path) + if not fn.endswith(self._fs_transaction_suffix)] + + def _prune(self): + entries = self._list_dir() + if len(entries) > self._threshold: + now = time() + for idx, fname in enumerate(entries): + remove = False + f = None + try: + try: + f = open(fname, 'rb') + expires = load(f) + remove = expires <= now or idx % 3 == 0 + finally: + if f is not None: + f.close() + except Exception: + pass + if remove: + try: + os.remove(fname) + except (IOError, OSError): + pass + + def clear(self): + for fname in self._list_dir(): + try: + os.remove(fname) + except (IOError, OSError): + pass + + def _get_filename(self, key): + hash = md5(key).hexdigest() + return os.path.join(self._path, hash) + + def get(self, key): + filename = self._get_filename(key) + try: + f = open(filename, 'rb') + try: + if load(f) >= time(): + return load(f) + finally: + f.close() + os.remove(filename) + except Exception: + return None + + def add(self, key, value, timeout=None): + filename = self._get_filename(key) + if not os.path.exists(filename): + self.set(key, value, timeout) + + def set(self, key, value, timeout=None): + if timeout is None: + timeout = self.default_timeout + filename = self._get_filename(key) + self._prune() + try: + fd, tmp = tempfile.mkstemp(suffix=self._fs_transaction_suffix, + dir=self._path) + f = os.fdopen(fd, 'wb') + try: + dump(int(time() + timeout), f, 1) + dump(value, f, HIGHEST_PROTOCOL) + finally: + f.close() + rename(tmp, filename) + os.chmod(filename, self._mode) + except (IOError, OSError): + pass + + def delete(self, key): + try: + os.remove(self._get_filename(key)) + except (IOError, OSError): + pass + diff --git a/websdk/werkzeug/contrib/fixers.py b/websdk/werkzeug/contrib/fixers.py new file mode 100644 index 0000000..6286e6c --- /dev/null +++ b/websdk/werkzeug/contrib/fixers.py @@ -0,0 +1,224 @@ +# -*- coding: utf-8 -*- +""" + werkzeug.contrib.fixers + ~~~~~~~~~~~~~~~~~~~~~~~ + + .. versionadded:: 0.5 + + This module includes various helpers that fix bugs in web servers. They may + be necessary for some versions of a buggy web server but not others. We try + to stay updated with the status of the bugs as good as possible but you have + to make sure whether they fix the problem you encounter. + + If you notice bugs in webservers not fixed in this module consider + contributing a patch. + + :copyright: Copyright 2009 by the Werkzeug Team, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" +from urllib import unquote +from werkzeug.http import parse_options_header, parse_cache_control_header, \ + parse_set_header +from werkzeug.useragents import UserAgent +from werkzeug.datastructures import Headers, ResponseCacheControl + + +class LighttpdCGIRootFix(object): + """Wrap the application in this middleware if you are using lighttpd + with FastCGI or CGI and the application is mounted on the URL root. + + :param app: the WSGI application + """ + + def __init__(self, app): + self.app = app + + def __call__(self, environ, start_response): + # only set PATH_INFO for older versions of Lighty or if no + # server software is provided. That's because the test was + # added in newer Werkzeug versions and we don't want to break + # people's code if they are using this fixer in a test that + # does not set the SERVER_SOFTWARE key. + if 'SERVER_SOFTWARE' not in environ or \ + environ['SERVER_SOFTWARE'] < 'lighttpd/1.4.28': + environ['PATH_INFO'] = environ.get('SCRIPT_NAME', '') + \ + environ.get('PATH_INFO', '') + environ['SCRIPT_NAME'] = '' + return self.app(environ, start_response) + + +class PathInfoFromRequestUriFix(object): + """On windows environment variables are limited to the system charset + which makes it impossible to store the `PATH_INFO` variable in the + environment without loss of information on some systems. + + This is for example a problem for CGI scripts on a Windows Apache. + + This fixer works by recreating the `PATH_INFO` from `REQUEST_URI`, + `REQUEST_URL`, or `UNENCODED_URL` (whatever is available). Thus the + fix can only be applied if the webserver supports either of these + variables. + + :param app: the WSGI application + """ + + def __init__(self, app): + self.app = app + + def __call__(self, environ, start_response): + for key in 'REQUEST_URL', 'REQUEST_URI', 'UNENCODED_URL': + if key not in environ: + continue + request_uri = unquote(environ[key]) + script_name = unquote(environ.get('SCRIPT_NAME', '')) + if request_uri.startswith(script_name): + environ['PATH_INFO'] = request_uri[len(script_name):] \ + .split('?', 1)[0] + break + return self.app(environ, start_response) + + +class ProxyFix(object): + """This middleware can be applied to add HTTP proxy support to an + application that was not designed with HTTP proxies in mind. It + sets `REMOTE_ADDR`, `HTTP_HOST` from `X-Forwarded` headers. + + Do not use this middleware in non-proxy setups for security reasons. + + The original values of `REMOTE_ADDR` and `HTTP_HOST` are stored in + the WSGI environment as `werkzeug.proxy_fix.orig_remote_addr` and + `werkzeug.proxy_fix.orig_http_host`. + + :param app: the WSGI application + """ + + def __init__(self, app): + self.app = app + + def get_remote_addr(self, forwarded_for): + """Selects the new remote addr from the given list of ips in + X-Forwarded-For. By default the first one is picked. + + .. versionadded:: 0.8 + """ + if forwarded_for: + return forwarded_for[0] + + def __call__(self, environ, start_response): + getter = environ.get + forwarded_proto = getter('HTTP_X_FORWARDED_PROTO', '') + forwarded_for = getter('HTTP_X_FORWARDED_FOR', '').split(',') + forwarded_host = getter('HTTP_X_FORWARDED_HOST', '') + environ.update({ + 'werkzeug.proxy_fix.orig_wsgi_url_scheme': getter('wsgi.url_scheme'), + 'werkzeug.proxy_fix.orig_remote_addr': getter('REMOTE_ADDR'), + 'werkzeug.proxy_fix.orig_http_host': getter('HTTP_HOST') + }) + forwarded_for = [x for x in [x.strip() for x in forwarded_for] if x] + remote_addr = self.get_remote_addr(forwarded_for) + if remote_addr is not None: + environ['REMOTE_ADDR'] = remote_addr + if forwarded_host: + environ['HTTP_HOST'] = forwarded_host + if forwarded_proto: + environ['wsgi.url_scheme'] = forwarded_proto + return self.app(environ, start_response) + + +class HeaderRewriterFix(object): + """This middleware can remove response headers and add others. This + is for example useful to remove the `Date` header from responses if you + are using a server that adds that header, no matter if it's present or + not or to add `X-Powered-By` headers:: + + app = HeaderRewriterFix(app, remove_headers=['Date'], + add_headers=[('X-Powered-By', 'WSGI')]) + + :param app: the WSGI application + :param remove_headers: a sequence of header keys that should be + removed. + :param add_headers: a sequence of ``(key, value)`` tuples that should + be added. + """ + + def __init__(self, app, remove_headers=None, add_headers=None): + self.app = app + self.remove_headers = set(x.lower() for x in (remove_headers or ())) + self.add_headers = list(add_headers or ()) + + def __call__(self, environ, start_response): + def rewriting_start_response(status, headers, exc_info=None): + new_headers = [] + for key, value in headers: + if key.lower() not in self.remove_headers: + new_headers.append((key, value)) + new_headers += self.add_headers + return start_response(status, new_headers, exc_info) + return self.app(environ, rewriting_start_response) + + +class InternetExplorerFix(object): + """This middleware fixes a couple of bugs with Microsoft Internet + Explorer. Currently the following fixes are applied: + + - removing of `Vary` headers for unsupported mimetypes which + causes troubles with caching. Can be disabled by passing + ``fix_vary=False`` to the constructor. + see: http://support.microsoft.com/kb/824847/en-us + + - removes offending headers to work around caching bugs in + Internet Explorer if `Content-Disposition` is set. Can be + disabled by passing ``fix_attach=False`` to the constructor. + + If it does not detect affected Internet Explorer versions it won't touch + the request / response. + """ + + # This code was inspired by Django fixers for the same bugs. The + # fix_vary and fix_attach fixers were originally implemented in Django + # by Michael Axiak and is available as part of the Django project: + # http://code.djangoproject.com/ticket/4148 + + def __init__(self, app, fix_vary=True, fix_attach=True): + self.app = app + self.fix_vary = fix_vary + self.fix_attach = fix_attach + + def fix_headers(self, environ, headers, status=None): + if self.fix_vary: + header = headers.get('content-type', '') + mimetype, options = parse_options_header(header) + if mimetype not in ('text/html', 'text/plain', 'text/sgml'): + headers.pop('vary', None) + + if self.fix_attach and 'content-disposition' in headers: + pragma = parse_set_header(headers.get('pragma', '')) + pragma.discard('no-cache') + header = pragma.to_header() + if not header: + headers.pop('pragma', '') + else: + headers['Pragma'] = header + header = headers.get('cache-control', '') + if header: + cc = parse_cache_control_header(header, + cls=ResponseCacheControl) + cc.no_cache = None + cc.no_store = False + header = cc.to_header() + if not header: + headers.pop('cache-control', '') + else: + headers['Cache-Control'] = header + + def run_fixed(self, environ, start_response): + def fixing_start_response(status, headers, exc_info=None): + self.fix_headers(environ, Headers.linked(headers), status) + return start_response(status, headers, exc_info) + return self.app(environ, fixing_start_response) + + def __call__(self, environ, start_response): + ua = UserAgent(environ) + if ua.browser != 'msie': + return self.app(environ, start_response) + return self.run_fixed(environ, start_response) diff --git a/websdk/werkzeug/contrib/iterio.py b/websdk/werkzeug/contrib/iterio.py new file mode 100644 index 0000000..0718659 --- /dev/null +++ b/websdk/werkzeug/contrib/iterio.py @@ -0,0 +1,277 @@ +# -*- coding: utf-8 -*- +r""" + werkzeug.contrib.iterio + ~~~~~~~~~~~~~~~~~~~~~~~ + + This module implements a :class:`IterIO` that converts an iterator into + a stream object and the other way round. Converting streams into + iterators requires the `greenlet`_ module. + + To convert an iterator into a stream all you have to do is to pass it + directly to the :class:`IterIO` constructor. In this example we pass it + a newly created generator:: + + def foo(): + yield "something\n" + yield "otherthings" + stream = IterIO(foo()) + print stream.read() # read the whole iterator + + The other way round works a bit different because we have to ensure that + the code execution doesn't take place yet. An :class:`IterIO` call with a + callable as first argument does two things. The function itself is passed + an :class:`IterIO` stream it can feed. The object returned by the + :class:`IterIO` constructor on the other hand is not an stream object but + an iterator:: + + def foo(stream): + stream.write("some") + stream.write("thing") + stream.flush() + stream.write("otherthing") + iterator = IterIO(foo) + print iterator.next() # prints something + print iterator.next() # prints otherthing + iterator.next() # raises StopIteration + + .. _greenlet: http://codespeak.net/py/dist/greenlet.html + + :copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" +try: + import greenlet +except ImportError: + greenlet = None + + +class IterIO(object): + """Instances of this object implement an interface compatible with the + standard Python :class:`file` object. Streams are either read-only or + write-only depending on how the object is created. + """ + + def __new__(cls, obj): + try: + iterator = iter(obj) + except TypeError: + return IterI(obj) + return IterO(iterator) + + def __iter__(self): + return self + + def tell(self): + if self.closed: + raise ValueError('I/O operation on closed file') + return self.pos + + def isatty(self): + if self.closed: + raise ValueError('I/O operation on closed file') + return False + + def seek(self, pos, mode=0): + if self.closed: + raise ValueError('I/O operation on closed file') + raise IOError(9, 'Bad file descriptor') + + def truncate(self, size=None): + if self.closed: + raise ValueError('I/O operation on closed file') + raise IOError(9, 'Bad file descriptor') + + def write(self, s): + if self.closed: + raise ValueError('I/O operation on closed file') + raise IOError(9, 'Bad file descriptor') + + def writelines(self, list): + if self.closed: + raise ValueError('I/O operation on closed file') + raise IOError(9, 'Bad file descriptor') + + def read(self, n=-1): + if self.closed: + raise ValueError('I/O operation on closed file') + raise IOError(9, 'Bad file descriptor') + + def readlines(self, sizehint=0): + if self.closed: + raise ValueError('I/O operation on closed file') + raise IOError(9, 'Bad file descriptor') + + def readline(self, length=None): + if self.closed: + raise ValueError('I/O operation on closed file') + raise IOError(9, 'Bad file descriptor') + + def flush(self): + if self.closed: + raise ValueError('I/O operation on closed file') + raise IOError(9, 'Bad file descriptor') + + def next(self): + if self.closed: + raise StopIteration() + line = self.readline() + if not line: + raise StopIteration() + return line + + +class IterI(IterIO): + """Convert an stream into an iterator.""" + + def __new__(cls, func): + if greenlet is None: + raise RuntimeError('IterI requires greenlet support') + stream = object.__new__(cls) + stream._parent = greenlet.getcurrent() + stream._buffer = [] + stream.closed = False + stream.pos = 0 + + def run(): + func(stream) + stream.flush() + + g = greenlet.greenlet(run, stream._parent) + while 1: + rv = g.switch() + if not rv: + return + yield rv[0] + + def close(self): + if not self.closed: + self.closed = True + + def write(self, s): + if self.closed: + raise ValueError('I/O operation on closed file') + self.pos += len(s) + self._buffer.append(s) + + def writelines(self, list): + self.write(''.join(list)) + + def flush(self): + if self.closed: + raise ValueError('I/O operation on closed file') + data = ''.join(self._buffer) + self._buffer = [] + self._parent.switch((data,)) + + +class IterO(IterIO): + """Iter output. Wrap an iterator and give it a stream like interface.""" + + def __new__(cls, gen): + self = object.__new__(cls) + self._gen = gen + self._buf = '' + self.closed = False + self.pos = 0 + return self + + def __iter__(self): + return self + + def close(self): + if not self.closed: + self.closed = True + if hasattr(self._gen, 'close'): + self._gen.close() + + def seek(self, pos, mode=0): + if self.closed: + raise ValueError('I/O operation on closed file') + if mode == 1: + pos += self.pos + elif mode == 2: + self.read() + self.pos = min(self.pos, self.pos + pos) + return + elif mode != 0: + raise IOError('Invalid argument') + buf = [] + try: + tmp_end_pos = len(self._buf) + while pos > tmp_end_pos: + item = self._gen.next() + tmp_end_pos += len(item) + buf.append(item) + except StopIteration: + pass + if buf: + self._buf += ''.join(buf) + self.pos = max(0, pos) + + def read(self, n=-1): + if self.closed: + raise ValueError('I/O operation on closed file') + if n < 0: + self._buf += ''.join(self._gen) + result = self._buf[self.pos:] + self.pos += len(result) + return result + new_pos = self.pos + n + buf = [] + try: + tmp_end_pos = len(self._buf) + while new_pos > tmp_end_pos: + item = self._gen.next() + tmp_end_pos += len(item) + buf.append(item) + except StopIteration: + pass + if buf: + self._buf += ''.join(buf) + new_pos = max(0, new_pos) + try: + return self._buf[self.pos:new_pos] + finally: + self.pos = min(new_pos, len(self._buf)) + + def readline(self, length=None): + if self.closed: + raise ValueError('I/O operation on closed file') + nl_pos = self._buf.find('\n', self.pos) + buf = [] + try: + pos = self.pos + while nl_pos < 0: + item = self._gen.next() + local_pos = item.find('\n') + buf.append(item) + if local_pos >= 0: + nl_pos = pos + local_pos + break + pos += len(item) + except StopIteration: + pass + if buf: + self._buf += ''.join(buf) + if nl_pos < 0: + new_pos = len(self._buf) + else: + new_pos = nl_pos + 1 + if length is not None and self.pos + length < new_pos: + new_pos = self.pos + length + try: + return self._buf[self.pos:new_pos] + finally: + self.pos = min(new_pos, len(self._buf)) + + def readlines(self, sizehint=0): + total = 0 + lines = [] + line = self.readline() + while line: + lines.append(line) + total += len(line) + if 0 < sizehint <= total: + break + line = self.readline() + return lines diff --git a/websdk/werkzeug/contrib/jsrouting.py b/websdk/werkzeug/contrib/jsrouting.py new file mode 100644 index 0000000..9b7d0c0 --- /dev/null +++ b/websdk/werkzeug/contrib/jsrouting.py @@ -0,0 +1,259 @@ +# -*- coding: utf-8 -*- +""" + werkzeug.contrib.jsrouting + ~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Addon module that allows to create a JavaScript function from a map + that generates rules. + + :copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" +try: + from simplejson import dumps +except ImportError: + try: + from json import dumps + except ImportError: + def dumps(*args): + raise RuntimeError('simplejson required for jsrouting') + +from inspect import getmro +from werkzeug.routing import NumberConverter + + +def render_template(name_parts, rules, converters): + result = u'' + if name_parts: + for idx in xrange(0, len(name_parts) - 1): + name = u'.'.join(name_parts[:idx + 1]) + result += u"if (typeof %s === 'undefined') %s = {}\n" % (name, name) + result += '%s = ' % '.'.join(name_parts) + result += """(function (server_name, script_name, subdomain, url_scheme) { + var converters = %(converters)s; + var rules = $rules; + function in_array(array, value) { + if (array.indexOf != undefined) { + return array.indexOf(value) != -1; + } + for (var i = 0; i < array.length; i++) { + if (array[i] == value) { + return true; + } + } + return false; + } + function array_diff(array1, array2) { + array1 = array1.slice(); + for (var i = array1.length-1; i >= 0; i--) { + if (in_array(array2, array1[i])) { + array1.splice(i, 1); + } + } + return array1; + } + function split_obj(obj) { + var names = []; + var values = []; + for (var name in obj) { + if (typeof(obj[name]) != 'function') { + names.push(name); + values.push(obj[name]); + } + } + return {names: names, values: values, original: obj}; + } + function suitable(rule, args) { + var default_args = split_obj(rule.defaults || {}); + var diff_arg_names = array_diff(rule.arguments, default_args.names); + + for (var i = 0; i < diff_arg_names.length; i++) { + if (!in_array(args.names, diff_arg_names[i])) { + return false; + } + } + + if (array_diff(rule.arguments, args.names).length == 0) { + if (rule.defaults == null) { + return true; + } + for (var i = 0; i < default_args.names.length; i++) { + var key = default_args.names[i]; + var value = default_args.values[i]; + if (value != args.original[key]) { + return false; + } + } + } + + return true; + } + function build(rule, args) { + var tmp = []; + var processed = rule.arguments.slice(); + for (var i = 0; i < rule.trace.length; i++) { + var part = rule.trace[i]; + if (part.is_dynamic) { + var converter = converters[rule.converters[part.data]]; + var data = converter(args.original[part.data]); + if (data == null) { + return null; + } + tmp.push(data); + processed.push(part.name); + } else { + tmp.push(part.data); + } + } + tmp = tmp.join(''); + var pipe = tmp.indexOf('|'); + var subdomain = tmp.substring(0, pipe); + var url = tmp.substring(pipe+1); + + var unprocessed = array_diff(args.names, processed); + var first_query_var = true; + for (var i = 0; i < unprocessed.length; i++) { + if (first_query_var) { + url += '?'; + } else { + url += '&'; + } + first_query_var = false; + url += encodeURIComponent(unprocessed[i]); + url += '='; + url += encodeURIComponent(args.original[unprocessed[i]]); + } + return {subdomain: subdomain, path: url}; + } + function lstrip(s, c) { + while (s && s.substring(0, 1) == c) { + s = s.substring(1); + } + return s; + } + function rstrip(s, c) { + while (s && s.substring(s.length-1, s.length) == c) { + s = s.substring(0, s.length-1); + } + return s; + } + return function(endpoint, args, force_external) { + args = split_obj(args); + var rv = null; + for (var i = 0; i < rules.length; i++) { + var rule = rules[i]; + if (rule.endpoint != endpoint) continue; + if (suitable(rule, args)) { + rv = build(rule, args); + if (rv != null) { + break; + } + } + } + if (rv == null) { + return null; + } + if (!force_external && rv.subdomain == subdomain) { + return rstrip(script_name, '/') + '/' + lstrip(rv.path, '/'); + } else { + return url_scheme + '://' + + (rv.subdomain ? rv.subdomain + '.' : '') + + server_name + rstrip(script_name, '/') + + '/' + lstrip(rv.path, '/'); + } + }; +})""" % {'converters': u', '.join(converters)} + return result + + +def generate_map(map, name='url_map'): + """ + Generates a JavaScript function containing the rules defined in + this map, to be used with a MapAdapter's generate_javascript + method. If you don't pass a name the returned JavaScript code is + an expression that returns a function. Otherwise it's a standalone + script that assigns the function with that name. Dotted names are + resolved (so you an use a name like 'obj.url_for') + + In order to use JavaScript generation, simplejson must be installed. + + Note that using this feature will expose the rules + defined in your map to users. If your rules contain sensitive + information, don't use JavaScript generation! + """ + map.update() + rules = [] + converters = [] + for rule in map.iter_rules(): + trace = [{ + 'is_dynamic': is_dynamic, + 'data': data + } for is_dynamic, data in rule._trace] + rule_converters = {} + for key, converter in rule._converters.iteritems(): + js_func = js_to_url_function(converter) + try: + index = converters.index(js_func) + except ValueError: + converters.append(js_func) + index = len(converters) - 1 + rule_converters[key] = index + rules.append({ + u'endpoint': rule.endpoint, + u'arguments': list(rule.arguments), + u'converters': rule_converters, + u'trace': trace, + u'defaults': rule.defaults + }) + + return render_template(name_parts=name and name.split('.') or [], + rules=dumps(rules), + converters=converters) + + +def generate_adapter(adapter, name='url_for', map_name='url_map'): + """Generates the url building function for a map.""" + values = { + u'server_name': dumps(adapter.server_name), + u'script_name': dumps(adapter.script_name), + u'subdomain': dumps(adapter.subdomain), + u'url_scheme': dumps(adapter.url_scheme), + u'name': name, + u'map_name': map_name + } + return u'''\ +var %(name)s = %(map_name)s( + %(server_name)s, + %(script_name)s, + %(subdomain)s, + %(url_scheme)s +);''' % values + + +def js_to_url_function(converter): + """Get the JavaScript converter function from a rule.""" + if hasattr(converter, 'js_to_url_function'): + data = converter.js_to_url_function() + else: + for cls in getmro(type(converter)): + if cls in js_to_url_functions: + data = js_to_url_functions[cls](converter) + break + else: + return 'encodeURIComponent' + return '(function(value) { %s })' % data + + +def NumberConverter_js_to_url(conv): + if conv.fixed_digits: + return u'''\ +var result = value.toString(); +while (result.length < %s) + result = '0' + result; +return result;''' % conv.fixed_digits + return u'return value.toString();' + + +js_to_url_functions = { + NumberConverter: NumberConverter_js_to_url +} diff --git a/websdk/werkzeug/contrib/kickstart.py b/websdk/werkzeug/contrib/kickstart.py new file mode 100644 index 0000000..43c0e7c --- /dev/null +++ b/websdk/werkzeug/contrib/kickstart.py @@ -0,0 +1,288 @@ +# -*- coding: utf-8 -*- +""" + werkzeug.contrib.kickstart + ~~~~~~~~~~~~~~~~~~~~~~~~~~ + + This module provides some simple shortcuts to make using Werkzeug simpler + for small scripts. + + These improvements include predefined `Request` and `Response` objects as + well as a predefined `Application` object which can be customized in child + classes, of course. The `Request` and `Reponse` objects handle URL + generation as well as sessions via `werkzeug.contrib.sessions` and are + purely optional. + + There is also some integration of template engines. The template loaders + are, of course, not neccessary to use the template engines in Werkzeug, + but they provide a common interface. Currently supported template engines + include Werkzeug's minitmpl and Genshi_. Support for other engines can be + added in a trivial way. These loaders provide a template interface + similar to the one used by Django_. + + .. _Genshi: http://genshi.edgewall.org/ + .. _Django: http://www.djangoproject.com/ + + :copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" +from os import path +from werkzeug.wrappers import Request as RequestBase, Response as ResponseBase +from werkzeug.templates import Template +from werkzeug.exceptions import HTTPException +from werkzeug.routing import RequestRedirect + +__all__ = ['Request', 'Response', 'TemplateNotFound', 'TemplateLoader', + 'GenshiTemplateLoader', 'Application'] + +from warnings import warn +warn(DeprecationWarning('werkzeug.contrib.kickstart is deprecated and ' + 'will be removed in Werkzeug 1.0')) + + +class Request(RequestBase): + """A handy subclass of the base request that adds a URL builder. + It when supplied a session store, it is also able to handle sessions. + """ + + def __init__(self, environ, url_map, + session_store=None, cookie_name=None): + # call the parent for initialization + RequestBase.__init__(self, environ) + # create an adapter + self.url_adapter = url_map.bind_to_environ(environ) + # create all stuff for sessions + self.session_store = session_store + self.cookie_name = cookie_name + + if session_store is not None and cookie_name is not None: + if cookie_name in self.cookies: + # get the session out of the storage + self.session = session_store.get(self.cookies[cookie_name]) + else: + # create a new session + self.session = session_store.new() + + def url_for(self, callback, **values): + return self.url_adapter.build(callback, values) + + +class Response(ResponseBase): + """ + A subclass of base response which sets the default mimetype to text/html. + It the `Request` that came in is using Werkzeug sessions, this class + takes care of saving that session. + """ + default_mimetype = 'text/html' + + def __call__(self, environ, start_response): + # get the request object + request = environ['werkzeug.request'] + + if request.session_store is not None: + # save the session if neccessary + request.session_store.save_if_modified(request.session) + + # set the cookie for the browser if it is not there: + if request.cookie_name not in request.cookies: + self.set_cookie(request.cookie_name, request.session.sid) + + # go on with normal response business + return ResponseBase.__call__(self, environ, start_response) + + +class Processor(object): + """A request and response processor - it is what Django calls a + middleware, but Werkzeug also includes straight-foward support for real + WSGI middlewares, so another name was chosen. + + The code of this processor is derived from the example in the Werkzeug + trac, called `Request and Response Processor + `_ + """ + + def process_request(self, request): + return request + + def process_response(self, request, response): + return response + + def process_view(self, request, view_func, view_args, view_kwargs): + """process_view() is called just before the Application calls the + function specified by view_func. + + If this returns None, the Application processes the next Processor, + and if it returns something else (like a Response instance), that + will be returned without any further processing. + """ + return None + + def process_exception(self, request, exception): + return None + + +class Application(object): + """A generic WSGI application which can be used to start with Werkzeug in + an easy, straightforward way. + """ + + def __init__(self, name, url_map, session=False, processors=None): + # save the name and the URL-map, as it'll be needed later on + self.name = name + self.url_map = url_map + # save the list of processors if supplied + self.processors = processors or [] + # create an instance of the storage + if session: + self.store = session + else: + self.store = None + + def __call__(self, environ, start_response): + # create a request - with or without session support + if self.store is not None: + request = Request(environ, self.url_map, + session_store=self.store, cookie_name='%s_sid' % self.name) + else: + request = Request(environ, self.url_map) + + # apply the request processors + for processor in self.processors: + request = processor.process_request(request) + + try: + # find the callback to which the URL is mapped + callback, args = request.url_adapter.match(request.path) + except (HTTPException, RequestRedirect), e: + response = e + else: + # check all view processors + for processor in self.processors: + action = processor.process_view(request, callback, (), args) + if action is not None: + # it is overriding the default behaviour, this is + # short-circuiting the processing, so it returns here + return action(environ, start_response) + + try: + response = callback(request, **args) + except Exception, exception: + # the callback raised some exception, need to process that + for processor in reversed(self.processors): + # filter it through the exception processor + action = processor.process_exception(request, exception) + if action is not None: + # the exception processor returned some action + return action(environ, start_response) + # still not handled by a exception processor, so re-raise + raise + + # apply the response processors + for processor in reversed(self.processors): + response = processor.process_response(request, response) + + # return the completely processed response + return response(environ, start_response) + + + def config_session(self, store, expiration='session'): + """ + Configures the setting for cookies. You can also disable cookies by + setting store to None. + """ + self.store = store + # expiration=session is the default anyway + # TODO: add settings to define the expiration date, the domain, the + # path any maybe the secure parameter. + + +class TemplateNotFound(IOError, LookupError): + """ + A template was not found by the template loader. + """ + + def __init__(self, name): + IOError.__init__(self, name) + self.name = name + + +class TemplateLoader(object): + """ + A simple loader interface for the werkzeug minitmpl + template language. + """ + + def __init__(self, search_path, encoding='utf-8'): + self.search_path = path.abspath(search_path) + self.encoding = encoding + + def get_template(self, name): + """Get a template from a given name.""" + filename = path.join(self.search_path, *[p for p in name.split('/') + if p and p[0] != '.']) + if not path.exists(filename): + raise TemplateNotFound(name) + return Template.from_file(filename, self.encoding) + + def render_to_response(self, *args, **kwargs): + """Load and render a template into a response object.""" + return Response(self.render_to_string(*args, **kwargs)) + + def render_to_string(self, *args, **kwargs): + """Load and render a template into a unicode string.""" + try: + template_name, args = args[0], args[1:] + except IndexError: + raise TypeError('name of template required') + return self.get_template(template_name).render(*args, **kwargs) + + +class GenshiTemplateLoader(TemplateLoader): + """A unified interface for loading Genshi templates. Actually a quite thin + wrapper for Genshi's TemplateLoader. + + It sets some defaults that differ from the Genshi loader, most notably + auto_reload is active. All imporant options can be passed through to + Genshi. + The default output type is 'html', but can be adjusted easily by changing + the `output_type` attribute. + """ + def __init__(self, search_path, encoding='utf-8', **kwargs): + TemplateLoader.__init__(self, search_path, encoding) + # import Genshi here, because we don't want a general Genshi + # dependency, only a local one + from genshi.template import TemplateLoader as GenshiLoader + from genshi.template.loader import TemplateNotFound + + self.not_found_exception = TemplateNotFound + # set auto_reload to True per default + reload_template = kwargs.pop('auto_reload', True) + # get rid of default_encoding as this template loaders overwrites it + # with the value of encoding + kwargs.pop('default_encoding', None) + + # now, all arguments are clean, pass them on + self.loader = GenshiLoader(search_path, default_encoding=encoding, + auto_reload=reload_template, **kwargs) + + # the default output is HTML but can be overridden easily + self.output_type = 'html' + self.encoding = encoding + + def get_template(self, template_name): + """Get the template which is at the given name""" + try: + return self.loader.load(template_name, encoding=self.encoding) + except self.not_found_exception, e: + # catch the exception raised by Genshi, convert it into a werkzeug + # exception (for the sake of consistency) + raise TemplateNotFound(template_name) + + def render_to_string(self, template_name, context=None): + """Load and render a template into an unicode string""" + # create an empty context if no context was specified + context = context or {} + tmpl = self.get_template(template_name) + # render the template into a unicode string (None means unicode) + return tmpl. \ + generate(**context). \ + render(self.output_type, encoding=None) diff --git a/websdk/werkzeug/contrib/limiter.py b/websdk/werkzeug/contrib/limiter.py new file mode 100644 index 0000000..d0dbcc5 --- /dev/null +++ b/websdk/werkzeug/contrib/limiter.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +""" + werkzeug.contrib.limiter + ~~~~~~~~~~~~~~~~~~~~~~~~ + + A middleware that limits incoming data. This works around problems with + Trac_ or Django_ because those directly stream into the memory. + + .. _Trac: http://trac.edgewall.org/ + .. _Django: http://www.djangoproject.com/ + + :copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" +from warnings import warn + +from werkzeug.wsgi import LimitedStream + + +class StreamLimitMiddleware(object): + """Limits the input stream to a given number of bytes. This is useful if + you have a WSGI application that reads form data into memory (django for + example) and you don't want users to harm the server by uploading tons of + data. + + Default is 10MB + """ + + def __init__(self, app, maximum_size=1024 * 1024 * 10): + self.app = app + self.maximum_size = maximum_size + + def __call__(self, environ, start_response): + limit = min(self.maximum_size, int(environ.get('CONTENT_LENGTH') or 0)) + environ['wsgi.input'] = LimitedStream(environ['wsgi.input'], limit) + return self.app(environ, start_response) diff --git a/websdk/werkzeug/contrib/lint.py b/websdk/werkzeug/contrib/lint.py new file mode 100644 index 0000000..c7adff9 --- /dev/null +++ b/websdk/werkzeug/contrib/lint.py @@ -0,0 +1,333 @@ +# -*- coding: utf-8 -*- +""" + werkzeug.contrib.lint + ~~~~~~~~~~~~~~~~~~~~~ + + .. versionadded:: 0.5 + + This module provides a middleware that performs sanity checks of the WSGI + application. It checks that :pep:`333` is properly implemented and warns + on some common HTTP errors such as non-empty responses for 304 status + codes. + + This module provides a middleware, the :class:`LintMiddleware`. Wrap your + application with it and it will warn about common problems with WSGI and + HTTP while your application is running. + + It's strongly recommended to use it during development. + + :copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" +from urlparse import urlparse +from warnings import warn + +from werkzeug.datastructures import Headers +from werkzeug.http import is_entity_header +from werkzeug.wsgi import FileWrapper + + +class WSGIWarning(Warning): + """Warning class for WSGI warnings.""" + + +class HTTPWarning(Warning): + """Warning class for HTTP warnings.""" + + +def check_string(context, obj, stacklevel=3): + if type(obj) is not str: + warn(WSGIWarning('%s requires bytestrings, got %s' % + (context, obj.__class__.__name__))) + + +class InputStream(object): + + def __init__(self, stream): + self._stream = stream + + def read(self, *args): + if len(args) == 0: + warn(WSGIWarning('wsgi does not guarantee an EOF marker on the ' + 'input stream, thus making calls to ' + 'wsgi.input.read() unsafe. Conforming servers ' + 'may never return from this call.'), + stacklevel=2) + elif len(args) != 1: + warn(WSGIWarning('too many parameters passed to wsgi.input.read()'), + stacklevel=2) + return self._stream.read(*args) + + def readline(self, *args): + if len(args) == 0: + warn(WSGIWarning('Calls to wsgi.input.readline() without arguments' + ' are unsafe. Use wsgi.input.read() instead.'), + stacklevel=2) + elif len(args) == 1: + warn(WSGIWarning('wsgi.input.readline() was called with a size hint. ' + 'WSGI does not support this, although it\'s available ' + 'on all major servers.'), + stacklevel=2) + else: + raise TypeError('too many arguments passed to wsgi.input.readline()') + return self._stream.readline(*args) + + def __iter__(self): + try: + return iter(self._stream) + except TypeError: + warn(WSGIWarning('wsgi.input is not iterable.'), stacklevel=2) + return iter(()) + + def close(self): + warn(WSGIWarning('application closed the input stream!'), + stacklevel=2) + self._stream.close() + + +class ErrorStream(object): + + def __init__(self, stream): + self._stream = stream + + def write(self, s): + check_string('wsgi.error.write()', s) + self._stream.write(s) + + def flush(self): + self._stream.flush() + + def writelines(self, seq): + for line in seq: + self.write(seq) + + def close(self): + warn(WSGIWarning('application closed the error stream!'), + stacklevel=2) + self._stream.close() + + +class GuardedWrite(object): + + def __init__(self, write, chunks): + self._write = write + self._chunks = chunks + + def __call__(self, s): + check_string('write()', s) + self._write.write(s) + self._chunks.append(len(s)) + + +class GuardedIterator(object): + + def __init__(self, iterator, headers_set, chunks): + self._iterator = iterator + self._next = iter(iterator).next + self.closed = False + self.headers_set = headers_set + self.chunks = chunks + + def __iter__(self): + return self + + def next(self): + if self.closed: + warn(WSGIWarning('iterated over closed app_iter'), + stacklevel=2) + rv = self._next() + if not self.headers_set: + warn(WSGIWarning('Application returned before it ' + 'started the response'), stacklevel=2) + check_string('application iterator items', rv) + self.chunks.append(len(rv)) + return rv + + def close(self): + self.closed = True + if hasattr(self._iterator, 'close'): + self._iterator.close() + + if self.headers_set: + status_code, headers = self.headers_set + bytes_sent = sum(self.chunks) + content_length = headers.get('content-length', type=int) + + if status_code == 304: + for key, value in headers: + key = key.lower() + if key not in ('expires', 'content-location') and \ + is_entity_header(key): + warn(HTTPWarning('entity header %r found in 304 ' + 'response' % key)) + if bytes_sent: + warn(HTTPWarning('304 responses must not have a body')) + elif 100 <= status_code < 200 or status_code == 204: + if content_length != 0: + warn(HTTPWarning('%r responses must have an empty ' + 'content length') % status_code) + if bytes_sent: + warn(HTTPWarning('%r responses must not have a body' % + status_code)) + elif content_length is not None and content_length != bytes_sent: + warn(WSGIWarning('Content-Length and the number of bytes ' + 'sent to the client do not match.')) + + def __del__(self): + if not self.closed: + try: + warn(WSGIWarning('Iterator was garbage collected before ' + 'it was closed.')) + except Exception: + pass + + +class LintMiddleware(object): + """This middleware wraps an application and warns on common errors. + Among other thing it currently checks for the following problems: + + - invalid status codes + - non-bytestrings sent to the WSGI server + - strings returned from the WSGI application + - non-empty conditional responses + - unquoted etags + - relative URLs in the Location header + - unsafe calls to wsgi.input + - unclosed iterators + + Detected errors are emitted using the standard Python :mod:`warnings` + system and usually end up on :data:`stderr`. + + :: + + from werkzeug.contrib.lint import LintMiddleware + app = LintMiddleware(app) + + :param app: the application to wrap + """ + + def __init__(self, app): + self.app = app + + def check_environ(self, environ): + if type(environ) is not dict: + warn(WSGIWarning('WSGI environment is not a standard python dict.'), + stacklevel=4) + for key in ('REQUEST_METHOD', 'SERVER_NAME', 'SERVER_PORT', + 'wsgi.version', 'wsgi.input', 'wsgi.errors', + 'wsgi.multithread', 'wsgi.multiprocess', + 'wsgi.run_once'): + if key not in environ: + warn(WSGIWarning('required environment key %r not found' + % key), stacklevel=3) + if environ['wsgi.version'] != (1, 0): + warn(WSGIWarning('environ is not a WSGI 1.0 environ'), + stacklevel=3) + + script_name = environ.get('SCRIPT_NAME', '') + if script_name and script_name[:1] != '/': + warn(WSGIWarning('SCRIPT_NAME does not start with a slash: %r' + % script_name), stacklevel=3) + path_info = environ.get('PATH_INFO', '') + if path_info[:1] != '/': + warn(WSGIWarning('PATH_INFO does not start with a slash: %r' + % path_info), stacklevel=3) + + + def check_start_response(self, status, headers, exc_info): + check_string('status', status) + status_code = status.split(None, 1)[0] + if len(status_code) != 3 or not status_code.isdigit(): + warn(WSGIWarning('Status code must be three digits'), stacklevel=3) + if len(status) < 4 or status[3] != ' ': + warn(WSGIWarning('Invalid value for status %r. Valid ' + 'status strings are three digits, a space ' + 'and a status explanation'), stacklevel=3) + status_code = int(status_code) + if status_code < 100: + warn(WSGIWarning('status code < 100 detected'), stacklevel=3) + + if type(headers) is not list: + warn(WSGIWarning('header list is not a list'), stacklevel=3) + for item in headers: + if type(item) is not tuple or len(item) != 2: + warn(WSGIWarning('Headers must tuple 2-item tuples'), + stacklevel=3) + name, value = item + if type(name) is not str or type(value) is not str: + warn(WSGIWarning('header items must be strings'), + stacklevel=3) + if name.lower() == 'status': + warn(WSGIWarning('The status header is not supported due to ' + 'conflicts with the CGI spec.'), + stacklevel=3) + + if exc_info is not None and not isinstance(exc_info, tuple): + warn(WSGIWarning('invalid value for exc_info'), stacklevel=3) + + headers = Headers(headers) + self.check_headers(headers) + + return status_code, headers + + def check_headers(self, headers): + etag = headers.get('etag') + if etag is not None: + if etag.startswith('w/'): + etag = etag[2:] + if not (etag[:1] == etag[-1:] == '"'): + warn(HTTPWarning('unquoted etag emitted.'), stacklevel=4) + + location = headers.get('location') + if location is not None: + if not urlparse(location).netloc: + warn(HTTPWarning('absolute URLs required for location header'), + stacklevel=4) + + def check_iterator(self, app_iter): + if isinstance(app_iter, basestring): + warn(WSGIWarning('application returned string. Response will ' + 'send character for character to the client ' + 'which will kill the performance. Return a ' + 'list or iterable instead.'), stacklevel=3) + + def __call__(self, *args, **kwargs): + if len(args) != 2: + warn(WSGIWarning('Two arguments to WSGI app required'), stacklevel=2) + if kwargs: + warn(WSGIWarning('No keyword arguments to WSGI app allowed'), + stacklevel=2) + environ, start_response = args + + self.check_environ(environ) + environ['wsgi.input'] = InputStream(environ['wsgi.input']) + environ['wsgi.errors'] = ErrorStream(environ['wsgi.errors']) + + # hook our own file wrapper in so that applications will always + # iterate to the end and we can check the content length + environ['wsgi.file_wrapper'] = FileWrapper + + headers_set = [] + chunks = [] + + def checking_start_response(*args, **kwargs): + if len(args) not in (2, 3): + warn(WSGIWarning('Invalid number of arguments: %s, expected ' + '2 or 3' % len(args), stacklevel=2)) + if kwargs: + warn(WSGIWarning('no keyword arguments allowed.')) + + status, headers = args[:2] + if len(args) == 3: + exc_info = args[2] + else: + exc_info = None + + headers_set[:] = self.check_start_response(status, headers, + exc_info) + return GuardedWrite(start_response(status, headers, exc_info), + chunks) + + app_iter = self.app(environ, checking_start_response) + self.check_iterator(app_iter) + return GuardedIterator(app_iter, headers_set, chunks) diff --git a/websdk/werkzeug/contrib/profiler.py b/websdk/werkzeug/contrib/profiler.py new file mode 100644 index 0000000..58d1465 --- /dev/null +++ b/websdk/werkzeug/contrib/profiler.py @@ -0,0 +1,118 @@ +# -*- coding: utf-8 -*- +""" + werkzeug.contrib.profiler + ~~~~~~~~~~~~~~~~~~~~~~~~~ + + This module provides a simple WSGI profiler middleware for finding + bottlenecks in web application. It uses the :mod:`profile` or + :mod:`cProfile` module to do the profiling and writes the stats to the + stream provided (defaults to stderr). + + Example usage:: + + from werkzeug.contrib.profiler import ProfilerMiddleware + app = ProfilerMiddleware(app) + + :copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" +import sys +try: + try: + from cProfile import Profile + except ImportError: + from profile import Profile + from pstats import Stats + available = True +except ImportError: + available = False + + +class MergeStream(object): + """An object that redirects `write` calls to multiple streams. + Use this to log to both `sys.stdout` and a file:: + + f = open('profiler.log', 'w') + stream = MergeStream(sys.stdout, f) + profiler = ProfilerMiddleware(app, stream) + """ + + def __init__(self, *streams): + if not streams: + raise TypeError('at least one stream must be given') + self.streams = streams + + def write(self, data): + for stream in self.streams: + stream.write(data) + + +class ProfilerMiddleware(object): + """Simple profiler middleware. Wraps a WSGI application and profiles + a request. This intentionally buffers the response so that timings are + more exact. + + For the exact meaning of `sort_by` and `restrictions` consult the + :mod:`profile` documentation. + + :param app: the WSGI application to profile. + :param stream: the stream for the profiled stats. defaults to stderr. + :param sort_by: a tuple of columns to sort the result by. + :param restrictions: a tuple of profiling strictions. + """ + + def __init__(self, app, stream=None, + sort_by=('time', 'calls'), restrictions=()): + if not available: + raise RuntimeError('the profiler is not available because ' + 'profile or pstat is not installed.') + self._app = app + self._stream = stream or sys.stdout + self._sort_by = sort_by + self._restrictions = restrictions + + def __call__(self, environ, start_response): + response_body = [] + + def catching_start_response(status, headers, exc_info=None): + start_response(status, headers, exc_info) + return response_body.append + + def runapp(): + appiter = self._app(environ, catching_start_response) + response_body.extend(appiter) + if hasattr(appiter, 'close'): + appiter.close() + + p = Profile() + p.runcall(runapp) + body = ''.join(response_body) + stats = Stats(p, stream=self._stream) + stats.sort_stats(*self._sort_by) + + self._stream.write('-' * 80) + self._stream.write('\nPATH: %r\n' % environ.get('PATH_INFO')) + stats.print_stats(*self._restrictions) + self._stream.write('-' * 80 + '\n\n') + + return [body] + + +def make_action(app_factory, hostname='localhost', port=5000, + threaded=False, processes=1, stream=None, + sort_by=('time', 'calls'), restrictions=()): + """Return a new callback for :mod:`werkzeug.script` that starts a local + server with the profiler enabled. + + :: + + from werkzeug.contrib import profiler + action_profile = profiler.make_action(make_app) + """ + def action(hostname=('h', hostname), port=('p', port), + threaded=threaded, processes=processes): + """Start a new development server.""" + from werkzeug.serving import run_simple + app = ProfilerMiddleware(app_factory(), stream, sort_by, restrictions) + run_simple(hostname, port, app, False, None, threaded, processes) + return action diff --git a/websdk/werkzeug/contrib/securecookie.py b/websdk/werkzeug/contrib/securecookie.py new file mode 100644 index 0000000..9e6feeb --- /dev/null +++ b/websdk/werkzeug/contrib/securecookie.py @@ -0,0 +1,332 @@ +# -*- coding: utf-8 -*- +r""" + werkzeug.contrib.securecookie + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + This module implements a cookie that is not alterable from the client + because it adds a checksum the server checks for. You can use it as + session replacement if all you have is a user id or something to mark + a logged in user. + + Keep in mind that the data is still readable from the client as a + normal cookie is. However you don't have to store and flush the + sessions you have at the server. + + Example usage: + + >>> from werkzeug.contrib.securecookie import SecureCookie + >>> x = SecureCookie({"foo": 42, "baz": (1, 2, 3)}, "deadbeef") + + Dumping into a string so that one can store it in a cookie: + + >>> value = x.serialize() + + Loading from that string again: + + >>> x = SecureCookie.unserialize(value, "deadbeef") + >>> x["baz"] + (1, 2, 3) + + If someone modifies the cookie and the checksum is wrong the unserialize + method will fail silently and return a new empty `SecureCookie` object. + + Keep in mind that the values will be visible in the cookie so do not + store data in a cookie you don't want the user to see. + + Application Integration + ======================= + + If you are using the werkzeug request objects you could integrate the + secure cookie into your application like this:: + + from werkzeug.utils import cached_property + from werkzeug.wrappers import BaseRequest + from werkzeug.contrib.securecookie import SecureCookie + + # don't use this key but a different one; you could just use + # os.urandom(20) to get something random + SECRET_KEY = '\xfa\xdd\xb8z\xae\xe0}4\x8b\xea' + + class Request(BaseRequest): + + @cached_property + def client_session(self): + data = self.cookies.get('session_data') + if not data: + return SecureCookie(secret_key=SECRET_KEY) + return SecureCookie.unserialize(data, SECRET_KEY) + + def application(environ, start_response): + request = Request(environ, start_response) + + # get a response object here + response = ... + + if request.client_session.should_save: + session_data = request.client_session.serialize() + response.set_cookie('session_data', session_data, + httponly=True) + return response(environ, start_response) + + A less verbose integration can be achieved by using shorthand methods:: + + class Request(BaseRequest): + + @cached_property + def client_session(self): + return SecureCookie.load_cookie(self, secret_key=COOKIE_SECRET) + + def application(environ, start_response): + request = Request(environ, start_response) + + # get a response object here + response = ... + + request.client_session.save_cookie(response) + return response(environ, start_response) + + :copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" +import sys +import cPickle as pickle +from hmac import new as hmac +from time import time +from werkzeug.urls import url_quote_plus, url_unquote_plus +from werkzeug._internal import _date_to_unix +from werkzeug.contrib.sessions import ModificationTrackingDict +from werkzeug.security import safe_str_cmp + + +# rather ugly way to import the correct hash method. Because +# hmac either accepts modules with a new method (sha, md5 etc.) +# or a hashlib factory function we have to figure out what to +# pass to it. If we have 2.5 or higher (so not 2.4 with a +# custom hashlib) we import from hashlib and fail if it does +# not exist (have seen that in old OS X versions). +# in all other cases the now deprecated sha module is used. +_default_hash = None +if sys.version_info >= (2, 5): + try: + from hashlib import sha1 as _default_hash + except ImportError: + pass +if _default_hash is None: + import sha as _default_hash + + +class UnquoteError(Exception): + """Internal exception used to signal failures on quoting.""" + + +class SecureCookie(ModificationTrackingDict): + """Represents a secure cookie. You can subclass this class and provide + an alternative mac method. The import thing is that the mac method + is a function with a similar interface to the hashlib. Required + methods are update() and digest(). + + Example usage: + + >>> x = SecureCookie({"foo": 42, "baz": (1, 2, 3)}, "deadbeef") + >>> x["foo"] + 42 + >>> x["baz"] + (1, 2, 3) + >>> x["blafasel"] = 23 + >>> x.should_save + True + + :param data: the initial data. Either a dict, list of tuples or `None`. + :param secret_key: the secret key. If not set `None` or not specified + it has to be set before :meth:`serialize` is called. + :param new: The initial value of the `new` flag. + """ + + #: The hash method to use. This has to be a module with a new function + #: or a function that creates a hashlib object. Such as `hashlib.md5` + #: Subclasses can override this attribute. The default hash is sha1. + #: Make sure to wrap this in staticmethod() if you store an arbitrary + #: function there such as hashlib.sha1 which might be implemented + #: as a function. + hash_method = staticmethod(_default_hash) + + #: the module used for serialization. Unless overriden by subclasses + #: the standard pickle module is used. + serialization_method = pickle + + #: if the contents should be base64 quoted. This can be disabled if the + #: serialization process returns cookie safe strings only. + quote_base64 = True + + def __init__(self, data=None, secret_key=None, new=True): + ModificationTrackingDict.__init__(self, data or ()) + # explicitly convert it into a bytestring because python 2.6 + # no longer performs an implicit string conversion on hmac + if secret_key is not None: + secret_key = str(secret_key) + self.secret_key = secret_key + self.new = new + + def __repr__(self): + return '<%s %s%s>' % ( + self.__class__.__name__, + dict.__repr__(self), + self.should_save and '*' or '' + ) + + @property + def should_save(self): + """True if the session should be saved. By default this is only true + for :attr:`modified` cookies, not :attr:`new`. + """ + return self.modified + + @classmethod + def quote(cls, value): + """Quote the value for the cookie. This can be any object supported + by :attr:`serialization_method`. + + :param value: the value to quote. + """ + if cls.serialization_method is not None: + value = cls.serialization_method.dumps(value) + if cls.quote_base64: + value = ''.join(value.encode('base64').splitlines()).strip() + return value + + @classmethod + def unquote(cls, value): + """Unquote the value for the cookie. If unquoting does not work a + :exc:`UnquoteError` is raised. + + :param value: the value to unquote. + """ + try: + if cls.quote_base64: + value = value.decode('base64') + if cls.serialization_method is not None: + value = cls.serialization_method.loads(value) + return value + except Exception: + # unfortunately pickle and other serialization modules can + # cause pretty every error here. if we get one we catch it + # and convert it into an UnquoteError + raise UnquoteError() + + def serialize(self, expires=None): + """Serialize the secure cookie into a string. + + If expires is provided, the session will be automatically invalidated + after expiration when you unseralize it. This provides better + protection against session cookie theft. + + :param expires: an optional expiration date for the cookie (a + :class:`datetime.datetime` object) + """ + if self.secret_key is None: + raise RuntimeError('no secret key defined') + if expires: + self['_expires'] = _date_to_unix(expires) + result = [] + mac = hmac(self.secret_key, None, self.hash_method) + for key, value in sorted(self.items()): + result.append('%s=%s' % ( + url_quote_plus(key), + self.quote(value) + )) + mac.update('|' + result[-1]) + return '%s?%s' % ( + mac.digest().encode('base64').strip(), + '&'.join(result) + ) + + @classmethod + def unserialize(cls, string, secret_key): + """Load the secure cookie from a serialized string. + + :param string: the cookie value to unserialize. + :param secret_key: the secret key used to serialize the cookie. + :return: a new :class:`SecureCookie`. + """ + if isinstance(string, unicode): + string = string.encode('utf-8', 'replace') + try: + base64_hash, data = string.split('?', 1) + except (ValueError, IndexError): + items = () + else: + items = {} + mac = hmac(secret_key, None, cls.hash_method) + for item in data.split('&'): + mac.update('|' + item) + if not '=' in item: + items = None + break + key, value = item.split('=', 1) + # try to make the key a string + key = url_unquote_plus(key) + try: + key = str(key) + except UnicodeError: + pass + items[key] = value + + # no parsing error and the mac looks okay, we can now + # sercurely unpickle our cookie. + try: + client_hash = base64_hash.decode('base64') + except Exception: + items = client_hash = None + if items is not None and safe_str_cmp(client_hash, mac.digest()): + try: + for key, value in items.iteritems(): + items[key] = cls.unquote(value) + except UnquoteError: + items = () + else: + if '_expires' in items: + if time() > items['_expires']: + items = () + else: + del items['_expires'] + else: + items = () + return cls(items, secret_key, False) + + @classmethod + def load_cookie(cls, request, key='session', secret_key=None): + """Loads a :class:`SecureCookie` from a cookie in request. If the + cookie is not set, a new :class:`SecureCookie` instanced is + returned. + + :param request: a request object that has a `cookies` attribute + which is a dict of all cookie values. + :param key: the name of the cookie. + :param secret_key: the secret key used to unquote the cookie. + Always provide the value even though it has + no default! + """ + data = request.cookies.get(key) + if not data: + return cls(secret_key=secret_key) + return cls.unserialize(data, secret_key) + + def save_cookie(self, response, key='session', expires=None, + session_expires=None, max_age=None, path='/', domain=None, + secure=None, httponly=False, force=False): + """Saves the SecureCookie in a cookie on response object. All + parameters that are not described here are forwarded directly + to :meth:`~BaseResponse.set_cookie`. + + :param response: a response object that has a + :meth:`~BaseResponse.set_cookie` method. + :param key: the name of the cookie. + :param session_expires: the expiration date of the secure cookie + stored information. If this is not provided + the cookie `expires` date is used instead. + """ + if force or self.should_save: + data = self.serialize(session_expires or expires) + response.set_cookie(key, data, expires=expires, max_age=max_age, + path=path, domain=domain, secure=secure, + httponly=httponly) diff --git a/websdk/werkzeug/contrib/sessions.py b/websdk/werkzeug/contrib/sessions.py new file mode 100644 index 0000000..b81351a --- /dev/null +++ b/websdk/werkzeug/contrib/sessions.py @@ -0,0 +1,344 @@ +# -*- coding: utf-8 -*- +r""" + werkzeug.contrib.sessions + ~~~~~~~~~~~~~~~~~~~~~~~~~ + + This module contains some helper classes that help one to add session + support to a python WSGI application. For full client-side session + storage see :mod:`~werkzeug.contrib.securecookie` which implements a + secure, client-side session storage. + + + Application Integration + ======================= + + :: + + from werkzeug.contrib.sessions import SessionMiddleware, \ + FilesystemSessionStore + + app = SessionMiddleware(app, FilesystemSessionStore()) + + The current session will then appear in the WSGI environment as + `werkzeug.session`. However it's recommended to not use the middleware + but the stores directly in the application. However for very simple + scripts a middleware for sessions could be sufficient. + + This module does not implement methods or ways to check if a session is + expired. That should be done by a cronjob and storage specific. For + example to prune unused filesystem sessions one could check the modified + time of the files. It sessions are stored in the database the new() + method should add an expiration timestamp for the session. + + For better flexibility it's recommended to not use the middleware but the + store and session object directly in the application dispatching:: + + session_store = FilesystemSessionStore() + + def application(environ, start_response): + request = Request(environ) + sid = request.cookies.get('cookie_name') + if sid is None: + request.session = session_store.new() + else: + request.session = session_store.get(sid) + response = get_the_response_object(request) + if request.session.should_save: + session_store.save(request.session) + response.set_cookie('cookie_name', request.session.sid) + return response(environ, start_response) + + :copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" +import re +import os +import sys +import tempfile +from os import path +from time import time +from random import random +try: + from hashlib import sha1 +except ImportError: + from sha import new as sha1 +from cPickle import dump, load, HIGHEST_PROTOCOL + +from werkzeug.datastructures import CallbackDict +from werkzeug.utils import dump_cookie, parse_cookie +from werkzeug.wsgi import ClosingIterator +from werkzeug.posixemulation import rename + + +_sha1_re = re.compile(r'^[a-f0-9]{40}$') + + +def _urandom(): + if hasattr(os, 'urandom'): + return os.urandom(30) + return random() + + +def generate_key(salt=None): + return sha1('%s%s%s' % (salt, time(), _urandom())).hexdigest() + + +class ModificationTrackingDict(CallbackDict): + __slots__ = ('modified',) + + def __init__(self, *args, **kwargs): + def on_update(self): + self.modified = True + self.modified = False + CallbackDict.__init__(self, on_update=on_update) + dict.update(self, *args, **kwargs) + + def copy(self): + """Create a flat copy of the dict.""" + missing = object() + result = object.__new__(self.__class__) + for name in self.__slots__: + val = getattr(self, name, missing) + if val is not missing: + setattr(result, name, val) + return result + + def __copy__(self): + return self.copy() + + +class Session(ModificationTrackingDict): + """Subclass of a dict that keeps track of direct object changes. Changes + in mutable structures are not tracked, for those you have to set + `modified` to `True` by hand. + """ + __slots__ = ModificationTrackingDict.__slots__ + ('sid', 'new') + + def __init__(self, data, sid, new=False): + ModificationTrackingDict.__init__(self, data) + self.sid = sid + self.new = new + + def __repr__(self): + return '<%s %s%s>' % ( + self.__class__.__name__, + dict.__repr__(self), + self.should_save and '*' or '' + ) + + @property + def should_save(self): + """True if the session should be saved. + + .. versionchanged:: 0.6 + By default the session is now only saved if the session is + modified, not if it is new like it was before. + """ + return self.modified + + +class SessionStore(object): + """Baseclass for all session stores. The Werkzeug contrib module does not + implement any useful stores besides the filesystem store, application + developers are encouraged to create their own stores. + + :param session_class: The session class to use. Defaults to + :class:`Session`. + """ + + def __init__(self, session_class=None): + if session_class is None: + session_class = Session + self.session_class = session_class + + def is_valid_key(self, key): + """Check if a key has the correct format.""" + return _sha1_re.match(key) is not None + + def generate_key(self, salt=None): + """Simple function that generates a new session key.""" + return generate_key(salt) + + def new(self): + """Generate a new session.""" + return self.session_class({}, self.generate_key(), True) + + def save(self, session): + """Save a session.""" + + def save_if_modified(self, session): + """Save if a session class wants an update.""" + if session.should_save: + self.save(session) + + def delete(self, session): + """Delete a session.""" + + def get(self, sid): + """Get a session for this sid or a new session object. This method + has to check if the session key is valid and create a new session if + that wasn't the case. + """ + return self.session_class({}, sid, True) + + +#: used for temporary files by the filesystem session store +_fs_transaction_suffix = '.__wz_sess' + + +class FilesystemSessionStore(SessionStore): + """Simple example session store that saves sessions on the filesystem. + This store works best on POSIX systems and Windows Vista / Windows + Server 2008 and newer. + + .. versionchanged:: 0.6 + `renew_missing` was added. Previously this was considered `True`, + now the default changed to `False` and it can be explicitly + deactivated. + + :param path: the path to the folder used for storing the sessions. + If not provided the default temporary directory is used. + :param filename_template: a string template used to give the session + a filename. ``%s`` is replaced with the + session id. + :param session_class: The session class to use. Defaults to + :class:`Session`. + :param renew_missing: set to `True` if you want the store to + give the user a new sid if the session was + not yet saved. + """ + + def __init__(self, path=None, filename_template='werkzeug_%s.sess', + session_class=None, renew_missing=False, mode=0644): + SessionStore.__init__(self, session_class) + if path is None: + path = tempfile.gettempdir() + self.path = path + if isinstance(filename_template, unicode): + filename_template = filename_template.encode( + sys.getfilesystemencoding() or 'utf-8') + assert not filename_template.endswith(_fs_transaction_suffix), \ + 'filename templates may not end with %s' % _fs_transaction_suffix + self.filename_template = filename_template + self.renew_missing = renew_missing + self.mode = mode + + def get_session_filename(self, sid): + # out of the box, this should be a strict ASCII subset but + # you might reconfigure the session object to have a more + # arbitrary string. + if isinstance(sid, unicode): + sid = sid.encode(sys.getfilesystemencoding() or 'utf-8') + return path.join(self.path, self.filename_template % sid) + + def save(self, session): + fn = self.get_session_filename(session.sid) + fd, tmp = tempfile.mkstemp(suffix=_fs_transaction_suffix, + dir=self.path) + f = os.fdopen(fd, 'wb') + try: + dump(dict(session), f, HIGHEST_PROTOCOL) + finally: + f.close() + try: + rename(tmp, fn) + os.chmod(fn, self.mode) + except (IOError, OSError): + pass + + def delete(self, session): + fn = self.get_session_filename(session.sid) + try: + os.unlink(fn) + except OSError: + pass + + def get(self, sid): + if not self.is_valid_key(sid): + return self.new() + try: + f = open(self.get_session_filename(sid), 'rb') + except IOError: + if self.renew_missing: + return self.new() + data = {} + else: + try: + try: + data = load(f) + except Exception: + data = {} + finally: + f.close() + return self.session_class(data, sid, False) + + def list(self): + """Lists all sessions in the store. + + .. versionadded:: 0.6 + """ + before, after = self.filename_template.split('%s', 1) + filename_re = re.compile(r'%s(.{5,})%s$' % (re.escape(before), + re.escape(after))) + result = [] + for filename in os.listdir(self.path): + #: this is a session that is still being saved. + if filename.endswith(_fs_transaction_suffix): + continue + match = filename_re.match(filename) + if match is not None: + result.append(match.group(1)) + return result + + +class SessionMiddleware(object): + """A simple middleware that puts the session object of a store provided + into the WSGI environ. It automatically sets cookies and restores + sessions. + + However a middleware is not the preferred solution because it won't be as + fast as sessions managed by the application itself and will put a key into + the WSGI environment only relevant for the application which is against + the concept of WSGI. + + The cookie parameters are the same as for the :func:`~dump_cookie` + function just prefixed with ``cookie_``. Additionally `max_age` is + called `cookie_age` and not `cookie_max_age` because of backwards + compatibility. + """ + + def __init__(self, app, store, cookie_name='session_id', + cookie_age=None, cookie_expires=None, cookie_path='/', + cookie_domain=None, cookie_secure=None, + cookie_httponly=False, environ_key='werkzeug.session'): + self.app = app + self.store = store + self.cookie_name = cookie_name + self.cookie_age = cookie_age + self.cookie_expires = cookie_expires + self.cookie_path = cookie_path + self.cookie_domain = cookie_domain + self.cookie_secure = cookie_secure + self.cookie_httponly = cookie_httponly + self.environ_key = environ_key + + def __call__(self, environ, start_response): + cookie = parse_cookie(environ.get('HTTP_COOKIE', '')) + sid = cookie.get(self.cookie_name, None) + if sid is None: + session = self.store.new() + else: + session = self.store.get(sid) + environ[self.environ_key] = session + + def injecting_start_response(status, headers, exc_info=None): + if session.should_save: + self.store.save(session) + headers.append(('Set-Cookie', dump_cookie(self.cookie_name, + session.sid, self.cookie_age, + self.cookie_expires, self.cookie_path, + self.cookie_domain, self.cookie_secure, + self.cookie_httponly))) + return start_response(status, headers, exc_info) + return ClosingIterator(self.app(environ, injecting_start_response), + lambda: self.store.save_if_modified(session)) diff --git a/websdk/werkzeug/contrib/testtools.py b/websdk/werkzeug/contrib/testtools.py new file mode 100644 index 0000000..9bbf76a --- /dev/null +++ b/websdk/werkzeug/contrib/testtools.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +""" + werkzeug.contrib.testtools + ~~~~~~~~~~~~~~~~~~~~~~~~~~ + + This module implements extended wrappers for simplified testing. + + `TestResponse` + A response wrapper which adds various cached attributes for + simplified assertions on various content types. + + :copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" +from werkzeug.utils import cached_property, import_string +from werkzeug.wrappers import Response + +from warnings import warn +warn(DeprecationWarning('werkzeug.contrib.testtools is deprecated and ' + 'will be removed with Werkzeug 1.0')) + + +class ContentAccessors(object): + """ + A mixin class for response objects that provides a couple of useful + accessors for unittesting. + """ + + def xml(self): + """Get an etree if possible.""" + if 'xml' not in self.mimetype: + raise AttributeError( + 'Not a XML response (Content-Type: %s)' + % self.mimetype) + for module in ['xml.etree.ElementTree', 'ElementTree', + 'elementtree.ElementTree']: + etree = import_string(module, silent=True) + if etree is not None: + return etree.XML(self.body) + raise RuntimeError('You must have ElementTree installed ' + 'to use TestResponse.xml') + xml = cached_property(xml) + + def lxml(self): + """Get an lxml etree if possible.""" + if ('html' not in self.mimetype and 'xml' not in self.mimetype): + raise AttributeError('Not an HTML/XML response') + from lxml import etree + try: + from lxml.html import fromstring + except ImportError: + fromstring = etree.HTML + if self.mimetype=='text/html': + return fromstring(self.data) + return etree.XML(self.data) + lxml = cached_property(lxml) + + def json(self): + """Get the result of simplejson.loads if possible.""" + if 'json' not in self.mimetype: + raise AttributeError('Not a JSON response') + try: + from simplejson import loads + except ImportError: + from json import loads + return loads(self.data) + json = cached_property(json) + + +class TestResponse(Response, ContentAccessors): + """Pass this to `werkzeug.test.Client` for easier unittesting.""" diff --git a/websdk/werkzeug/contrib/wrappers.py b/websdk/werkzeug/contrib/wrappers.py new file mode 100644 index 0000000..bd6a2d4 --- /dev/null +++ b/websdk/werkzeug/contrib/wrappers.py @@ -0,0 +1,275 @@ +# -*- coding: utf-8 -*- +""" + werkzeug.contrib.wrappers + ~~~~~~~~~~~~~~~~~~~~~~~~~ + + Extra wrappers or mixins contributed by the community. These wrappers can + be mixed in into request objects to add extra functionality. + + Example:: + + from werkzeug.wrappers import Request as RequestBase + from werkzeug.contrib.wrappers import JSONRequestMixin + + class Request(RequestBase, JSONRequestMixin): + pass + + Afterwards this request object provides the extra functionality of the + :class:`JSONRequestMixin`. + + :copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" +import codecs +from werkzeug.exceptions import BadRequest +from werkzeug.utils import cached_property +from werkzeug.http import dump_options_header, parse_options_header +from werkzeug._internal import _decode_unicode +try: + from simplejson import loads +except ImportError: + from json import loads + + +def is_known_charset(charset): + """Checks if the given charset is known to Python.""" + try: + codecs.lookup(charset) + except LookupError: + return False + return True + + +class JSONRequestMixin(object): + """Add json method to a request object. This will parse the input data + through simplejson if possible. + + :exc:`~werkzeug.exceptions.BadRequest` will be raised if the content-type + is not json or if the data itself cannot be parsed as json. + """ + + @cached_property + def json(self): + """Get the result of simplejson.loads if possible.""" + if 'json' not in self.environ.get('CONTENT_TYPE', ''): + raise BadRequest('Not a JSON request') + try: + return loads(self.data) + except Exception: + raise BadRequest('Unable to read JSON request') + + +class ProtobufRequestMixin(object): + """Add protobuf parsing method to a request object. This will parse the + input data through `protobuf`_ if possible. + + :exc:`~werkzeug.exceptions.BadRequest` will be raised if the content-type + is not protobuf or if the data itself cannot be parsed property. + + .. _protobuf: http://code.google.com/p/protobuf/ + """ + + #: by default the :class:`ProtobufRequestMixin` will raise a + #: :exc:`~werkzeug.exceptions.BadRequest` if the object is not + #: initialized. You can bypass that check by setting this + #: attribute to `False`. + protobuf_check_initialization = True + + def parse_protobuf(self, proto_type): + """Parse the data into an instance of proto_type.""" + if 'protobuf' not in self.environ.get('CONTENT_TYPE', ''): + raise BadRequest('Not a Protobuf request') + + obj = proto_type() + try: + obj.ParseFromString(self.data) + except Exception: + raise BadRequest("Unable to parse Protobuf request") + + # Fail if not all required fields are set + if self.protobuf_check_initialization and not obj.IsInitialized(): + raise BadRequest("Partial Protobuf request") + + return obj + + +class RoutingArgsRequestMixin(object): + """This request mixin adds support for the wsgiorg routing args + `specification`_. + + .. _specification: http://www.wsgi.org/wsgi/Specifications/routing_args + """ + + def _get_routing_args(self): + return self.environ.get('wsgiorg.routing_args', (()))[0] + + def _set_routing_args(self, value): + if self.shallow: + raise RuntimeError('A shallow request tried to modify the WSGI ' + 'environment. If you really want to do that, ' + 'set `shallow` to False.') + self.environ['wsgiorg.routing_args'] = (value, self.routing_vars) + + routing_args = property(_get_routing_args, _set_routing_args, doc=''' + The positional URL arguments as `tuple`.''') + del _get_routing_args, _set_routing_args + + def _get_routing_vars(self): + rv = self.environ.get('wsgiorg.routing_args') + if rv is not None: + return rv[1] + rv = {} + if not self.shallow: + self.routing_vars = rv + return rv + + def _set_routing_vars(self, value): + if self.shallow: + raise RuntimeError('A shallow request tried to modify the WSGI ' + 'environment. If you really want to do that, ' + 'set `shallow` to False.') + self.environ['wsgiorg.routing_args'] = (self.routing_args, value) + + routing_vars = property(_get_routing_vars, _set_routing_vars, doc=''' + The keyword URL arguments as `dict`.''') + del _get_routing_vars, _set_routing_vars + + +class ReverseSlashBehaviorRequestMixin(object): + """This mixin reverses the trailing slash behavior of :attr:`script_root` + and :attr:`path`. This makes it possible to use :func:`~urlparse.urljoin` + directly on the paths. + + Because it changes the behavior or :class:`Request` this class has to be + mixed in *before* the actual request class:: + + class MyRequest(ReverseSlashBehaviorRequestMixin, Request): + pass + + This example shows the differences (for an application mounted on + `/application` and the request going to `/application/foo/bar`): + + +---------------+-------------------+---------------------+ + | | normal behavior | reverse behavior | + +===============+===================+=====================+ + | `script_root` | ``/application`` | ``/application/`` | + +---------------+-------------------+---------------------+ + | `path` | ``/foo/bar`` | ``foo/bar`` | + +---------------+-------------------+---------------------+ + """ + + @cached_property + def path(self): + """Requested path as unicode. This works a bit like the regular path + info in the WSGI environment but will not include a leading slash. + """ + path = (self.environ.get('PATH_INFO') or '').lstrip('/') + return _decode_unicode(path, self.charset, self.encoding_errors) + + @cached_property + def script_root(self): + """The root path of the script includling a trailing slash.""" + path = (self.environ.get('SCRIPT_NAME') or '').rstrip('/') + '/' + return _decode_unicode(path, self.charset, self.encoding_errors) + + +class DynamicCharsetRequestMixin(object): + """"If this mixin is mixed into a request class it will provide + a dynamic `charset` attribute. This means that if the charset is + transmitted in the content type headers it's used from there. + + Because it changes the behavior or :class:`Request` this class has + to be mixed in *before* the actual request class:: + + class MyRequest(DynamicCharsetRequestMixin, Request): + pass + + By default the request object assumes that the URL charset is the + same as the data charset. If the charset varies on each request + based on the transmitted data it's not a good idea to let the URLs + change based on that. Most browsers assume either utf-8 or latin1 + for the URLs if they have troubles figuring out. It's strongly + recommended to set the URL charset to utf-8:: + + class MyRequest(DynamicCharsetRequestMixin, Request): + url_charset = 'utf-8' + + .. versionadded:: 0.6 + """ + + #: the default charset that is assumed if the content type header + #: is missing or does not contain a charset parameter. The default + #: is latin1 which is what HTTP specifies as default charset. + #: You may however want to set this to utf-8 to better support + #: browsers that do not transmit a charset for incoming data. + default_charset = 'latin1' + + def unknown_charset(self, charset): + """Called if a charset was provided but is not supported by + the Python codecs module. By default latin1 is assumed then + to not lose any information, you may override this method to + change the behavior. + + :param charset: the charset that was not found. + :return: the replacement charset. + """ + return 'latin1' + + @cached_property + def charset(self): + """The charset from the content type.""" + header = self.environ.get('CONTENT_TYPE') + if header: + ct, options = parse_options_header(header) + charset = options.get('charset') + if charset: + if is_known_charset(charset): + return charset + return self.unknown_charset(charset) + return self.default_charset + + +class DynamicCharsetResponseMixin(object): + """If this mixin is mixed into a response class it will provide + a dynamic `charset` attribute. This means that if the charset is + looked up and stored in the `Content-Type` header and updates + itself automatically. This also means a small performance hit but + can be useful if you're working with different charsets on + responses. + + Because the charset attribute is no a property at class-level, the + default value is stored in `default_charset`. + + Because it changes the behavior or :class:`Response` this class has + to be mixed in *before* the actual response class:: + + class MyResponse(DynamicCharsetResponseMixin, Response): + pass + + .. versionadded:: 0.6 + """ + + #: the default charset. + default_charset = 'utf-8' + + def _get_charset(self): + header = self.headers.get('content-type') + if header: + charset = parse_options_header(header)[1].get('charset') + if charset: + return charset + return self.default_charset + + def _set_charset(self, charset): + header = self.headers.get('content-type') + ct, options = parse_options_header(header) + if not ct: + raise TypeError('Cannot set charset if Content-Type ' + 'header is missing.') + options['charset'] = charset + self.headers['Content-Type'] = dump_options_header(ct, options) + + charset = property(_get_charset, _set_charset, doc=""" + The charset for the response. It's stored inside the + Content-Type header as a parameter.""") + del _get_charset, _set_charset diff --git a/websdk/werkzeug/datastructures.py b/websdk/werkzeug/datastructures.py new file mode 100644 index 0000000..8f8498c --- /dev/null +++ b/websdk/werkzeug/datastructures.py @@ -0,0 +1,2567 @@ +# -*- coding: utf-8 -*- +""" + werkzeug.datastructures + ~~~~~~~~~~~~~~~~~~~~~~~ + + This module provides mixins and classes with an immutable interface. + + :copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" +import re +import codecs +import mimetypes +from itertools import repeat + +from werkzeug._internal import _proxy_repr, _missing, _empty_stream + + +_locale_delim_re = re.compile(r'[_-]') + + +def is_immutable(self): + raise TypeError('%r objects are immutable' % self.__class__.__name__) + + +def iter_multi_items(mapping): + """Iterates over the items of a mapping yielding keys and values + without dropping any from more complex structures. + """ + if isinstance(mapping, MultiDict): + for item in mapping.iteritems(multi=True): + yield item + elif isinstance(mapping, dict): + for key, value in mapping.iteritems(): + if isinstance(value, (tuple, list)): + for value in value: + yield key, value + else: + yield key, value + else: + for item in mapping: + yield item + + +class ImmutableListMixin(object): + """Makes a :class:`list` immutable. + + .. versionadded:: 0.5 + + :private: + """ + + _hash_cache = None + + def __hash__(self): + if self._hash_cache is not None: + return self._hash_cache + rv = self._hash_cache = hash(tuple(self)) + return rv + + def __reduce_ex__(self, protocol): + return type(self), (list(self),) + + def __delitem__(self, key): + is_immutable(self) + + def __delslice__(self, i, j): + is_immutable(self) + + def __iadd__(self, other): + is_immutable(self) + __imul__ = __iadd__ + + def __setitem__(self, key, value): + is_immutable(self) + + def __setslice__(self, i, j, value): + is_immutable(self) + + def append(self, item): + is_immutable(self) + remove = append + + def extend(self, iterable): + is_immutable(self) + + def insert(self, pos, value): + is_immutable(self) + + def pop(self, index=-1): + is_immutable(self) + + def reverse(self): + is_immutable(self) + + def sort(self, cmp=None, key=None, reverse=None): + is_immutable(self) + + +class ImmutableList(ImmutableListMixin, list): + """An immutable :class:`list`. + + .. versionadded:: 0.5 + + :private: + """ + + __repr__ = _proxy_repr(list) + + +class ImmutableDictMixin(object): + """Makes a :class:`dict` immutable. + + .. versionadded:: 0.5 + + :private: + """ + _hash_cache = None + + @classmethod + def fromkeys(cls, keys, value=None): + instance = super(cls, cls).__new__(cls) + instance.__init__(zip(keys, repeat(value))) + return instance + + def __reduce_ex__(self, protocol): + return type(self), (dict(self),) + + def _iter_hashitems(self): + return self.iteritems() + + def __hash__(self): + if self._hash_cache is not None: + return self._hash_cache + rv = self._hash_cache = hash(frozenset(self._iter_hashitems())) + return rv + + def setdefault(self, key, default=None): + is_immutable(self) + + def update(self, *args, **kwargs): + is_immutable(self) + + def pop(self, key, default=None): + is_immutable(self) + + def popitem(self): + is_immutable(self) + + def __setitem__(self, key, value): + is_immutable(self) + + def __delitem__(self, key): + is_immutable(self) + + def clear(self): + is_immutable(self) + + +class ImmutableMultiDictMixin(ImmutableDictMixin): + """Makes a :class:`MultiDict` immutable. + + .. versionadded:: 0.5 + + :private: + """ + + def __reduce_ex__(self, protocol): + return type(self), (self.items(multi=True),) + + def _iter_hashitems(self): + return enumerate(self.iteritems(multi=True)) + + def add(self, key, value): + is_immutable(self) + + def popitemlist(self): + is_immutable(self) + + def poplist(self, key): + is_immutable(self) + + def setlist(self, key, new_list): + is_immutable(self) + + def setlistdefault(self, key, default_list=None): + is_immutable(self) + + +class UpdateDictMixin(object): + """Makes dicts call `self.on_update` on modifications. + + .. versionadded:: 0.5 + + :private: + """ + + on_update = None + + def calls_update(name): + def oncall(self, *args, **kw): + rv = getattr(super(UpdateDictMixin, self), name)(*args, **kw) + if self.on_update is not None: + self.on_update(self) + return rv + oncall.__name__ = name + return oncall + + __setitem__ = calls_update('__setitem__') + __delitem__ = calls_update('__delitem__') + clear = calls_update('clear') + pop = calls_update('pop') + popitem = calls_update('popitem') + setdefault = calls_update('setdefault') + update = calls_update('update') + del calls_update + + +class TypeConversionDict(dict): + """Works like a regular dict but the :meth:`get` method can perform + type conversions. :class:`MultiDict` and :class:`CombinedMultiDict` + are subclasses of this class and provide the same feature. + + .. versionadded:: 0.5 + """ + + def get(self, key, default=None, type=None): + """Return the default value if the requested data doesn't exist. + If `type` is provided and is a callable it should convert the value, + return it or raise a :exc:`ValueError` if that is not possible. In + this case the function will return the default as if the value was not + found: + + >>> d = TypeConversionDict(foo='42', bar='blub') + >>> d.get('foo', type=int) + 42 + >>> d.get('bar', -1, type=int) + -1 + + :param key: The key to be looked up. + :param default: The default value to be returned if the key can't + be looked up. If not further specified `None` is + returned. + :param type: A callable that is used to cast the value in the + :class:`MultiDict`. If a :exc:`ValueError` is raised + by this callable the default value is returned. + """ + try: + rv = self[key] + if type is not None: + rv = type(rv) + except (KeyError, ValueError): + rv = default + return rv + + +class ImmutableTypeConversionDict(ImmutableDictMixin, TypeConversionDict): + """Works like a :class:`TypeConversionDict` but does not support + modifications. + + .. versionadded:: 0.5 + """ + + def copy(self): + """Return a shallow mutable copy of this object. Keep in mind that + the standard library's :func:`copy` function is a no-op for this class + like for any other python immutable type (eg: :class:`tuple`). + """ + return TypeConversionDict(self) + + def __copy__(self): + return self + + +class MultiDict(TypeConversionDict): + """A :class:`MultiDict` is a dictionary subclass customized to deal with + multiple values for the same key which is for example used by the parsing + functions in the wrappers. This is necessary because some HTML form + elements pass multiple values for the same key. + + :class:`MultiDict` implements all standard dictionary methods. + Internally, it saves all values for a key as a list, but the standard dict + access methods will only return the first value for a key. If you want to + gain access to the other values, too, you have to use the `list` methods as + explained below. + + Basic Usage: + + >>> d = MultiDict([('a', 'b'), ('a', 'c')]) + >>> d + MultiDict([('a', 'b'), ('a', 'c')]) + >>> d['a'] + 'b' + >>> d.getlist('a') + ['b', 'c'] + >>> 'a' in d + True + + It behaves like a normal dict thus all dict functions will only return the + first value when multiple values for one key are found. + + From Werkzeug 0.3 onwards, the `KeyError` raised by this class is also a + subclass of the :exc:`~exceptions.BadRequest` HTTP exception and will + render a page for a ``400 BAD REQUEST`` if caught in a catch-all for HTTP + exceptions. + + A :class:`MultiDict` can be constructed from an iterable of + ``(key, value)`` tuples, a dict, a :class:`MultiDict` or from Werkzeug 0.2 + onwards some keyword parameters. + + :param mapping: the initial value for the :class:`MultiDict`. Either a + regular dict, an iterable of ``(key, value)`` tuples + or `None`. + """ + + def __init__(self, mapping=None): + if isinstance(mapping, MultiDict): + dict.__init__(self, ((k, l[:]) for k, l in mapping.iterlists())) + elif isinstance(mapping, dict): + tmp = {} + for key, value in mapping.iteritems(): + if isinstance(value, (tuple, list)): + value = list(value) + else: + value = [value] + tmp[key] = value + dict.__init__(self, tmp) + else: + tmp = {} + for key, value in mapping or (): + tmp.setdefault(key, []).append(value) + dict.__init__(self, tmp) + + def __getstate__(self): + return dict(self.lists()) + + def __setstate__(self, value): + dict.clear(self) + dict.update(self, value) + + def __iter__(self): + return self.iterkeys() + + def __getitem__(self, key): + """Return the first data value for this key; + raises KeyError if not found. + + :param key: The key to be looked up. + :raise KeyError: if the key does not exist. + """ + if key in self: + return dict.__getitem__(self, key)[0] + raise BadRequestKeyError(key) + + def __setitem__(self, key, value): + """Like :meth:`add` but removes an existing key first. + + :param key: the key for the value. + :param value: the value to set. + """ + dict.__setitem__(self, key, [value]) + + def add(self, key, value): + """Adds a new value for the key. + + .. versionadded:: 0.6 + + :param key: the key for the value. + :param value: the value to add. + """ + dict.setdefault(self, key, []).append(value) + + def getlist(self, key, type=None): + """Return the list of items for a given key. If that key is not in the + `MultiDict`, the return value will be an empty list. Just as `get` + `getlist` accepts a `type` parameter. All items will be converted + with the callable defined there. + + :param key: The key to be looked up. + :param type: A callable that is used to cast the value in the + :class:`MultiDict`. If a :exc:`ValueError` is raised + by this callable the value will be removed from the list. + :return: a :class:`list` of all the values for the key. + """ + try: + rv = dict.__getitem__(self, key) + except KeyError: + return [] + if type is None: + return list(rv) + result = [] + for item in rv: + try: + result.append(type(item)) + except ValueError: + pass + return result + + def setlist(self, key, new_list): + """Remove the old values for a key and add new ones. Note that the list + you pass the values in will be shallow-copied before it is inserted in + the dictionary. + + >>> d = MultiDict() + >>> d.setlist('foo', ['1', '2']) + >>> d['foo'] + '1' + >>> d.getlist('foo') + ['1', '2'] + + :param key: The key for which the values are set. + :param new_list: An iterable with the new values for the key. Old values + are removed first. + """ + dict.__setitem__(self, key, list(new_list)) + + def setdefault(self, key, default=None): + """Returns the value for the key if it is in the dict, otherwise it + returns `default` and sets that value for `key`. + + :param key: The key to be looked up. + :param default: The default value to be returned if the key is not + in the dict. If not further specified it's `None`. + """ + if key not in self: + self[key] = default + else: + default = self[key] + return default + + def setlistdefault(self, key, default_list=None): + """Like `setdefault` but sets multiple values. The list returned + is not a copy, but the list that is actually used internally. This + means that you can put new values into the dict by appending items + to the list: + + >>> d = MultiDict({"foo": 1}) + >>> d.setlistdefault("foo").extend([2, 3]) + >>> d.getlist("foo") + [1, 2, 3] + + :param key: The key to be looked up. + :param default: An iterable of default values. It is either copied + (in case it was a list) or converted into a list + before returned. + :return: a :class:`list` + """ + if key not in self: + default_list = list(default_list or ()) + dict.__setitem__(self, key, default_list) + else: + default_list = dict.__getitem__(self, key) + return default_list + + def items(self, multi=False): + """Return a list of ``(key, value)`` pairs. + + :param multi: If set to `True` the list returned will have a + pair for each value of each key. Otherwise it + will only contain pairs for the first value of + each key. + + :return: a :class:`list` + """ + return list(self.iteritems(multi)) + + def lists(self): + """Return a list of ``(key, values)`` pairs, where values is the list of + all values associated with the key. + + :return: a :class:`list` + """ + return list(self.iterlists()) + + def values(self): + """Returns a list of the first value on every key's value list. + + :return: a :class:`list`. + """ + return [self[key] for key in self.iterkeys()] + + def listvalues(self): + """Return a list of all values associated with a key. Zipping + :meth:`keys` and this is the same as calling :meth:`lists`: + + >>> d = MultiDict({"foo": [1, 2, 3]}) + >>> zip(d.keys(), d.listvalues()) == d.lists() + True + + :return: a :class:`list` + """ + return list(self.iterlistvalues()) + + def iteritems(self, multi=False): + """Like :meth:`items` but returns an iterator.""" + for key, values in dict.iteritems(self): + if multi: + for value in values: + yield key, value + else: + yield key, values[0] + + def iterlists(self): + """Like :meth:`items` but returns an iterator.""" + for key, values in dict.iteritems(self): + yield key, list(values) + + def itervalues(self): + """Like :meth:`values` but returns an iterator.""" + for values in dict.itervalues(self): + yield values[0] + + def iterlistvalues(self): + """Like :meth:`listvalues` but returns an iterator.""" + return dict.itervalues(self) + + def copy(self): + """Return a shallow copy of this object.""" + return self.__class__(self) + + def to_dict(self, flat=True): + """Return the contents as regular dict. If `flat` is `True` the + returned dict will only have the first item present, if `flat` is + `False` all values will be returned as lists. + + :param flat: If set to `False` the dict returned will have lists + with all the values in it. Otherwise it will only + contain the first value for each key. + :return: a :class:`dict` + """ + if flat: + return dict(self.iteritems()) + return dict(self.lists()) + + def update(self, other_dict): + """update() extends rather than replaces existing key lists.""" + for key, value in iter_multi_items(other_dict): + MultiDict.add(self, key, value) + + def pop(self, key, default=_missing): + """Pop the first item for a list on the dict. Afterwards the + key is removed from the dict, so additional values are discarded: + + >>> d = MultiDict({"foo": [1, 2, 3]}) + >>> d.pop("foo") + 1 + >>> "foo" in d + False + + :param key: the key to pop. + :param default: if provided the value to return if the key was + not in the dictionary. + """ + try: + return dict.pop(self, key)[0] + except KeyError, e: + if default is not _missing: + return default + raise BadRequestKeyError(str(e)) + + def popitem(self): + """Pop an item from the dict.""" + try: + item = dict.popitem(self) + return (item[0], item[1][0]) + except KeyError, e: + raise BadRequestKeyError(str(e)) + + def poplist(self, key): + """Pop the list for a key from the dict. If the key is not in the dict + an empty list is returned. + + .. versionchanged:: 0.5 + If the key does no longer exist a list is returned instead of + raising an error. + """ + return dict.pop(self, key, []) + + def popitemlist(self): + """Pop a ``(key, list)`` tuple from the dict.""" + try: + return dict.popitem(self) + except KeyError, e: + raise BadRequestKeyError(str(e)) + + def __repr__(self): + return '%s(%r)' % (self.__class__.__name__, self.items(multi=True)) + + +class _omd_bucket(object): + """Wraps values in the :class:`OrderedMultiDict`. This makes it + possible to keep an order over multiple different keys. It requires + a lot of extra memory and slows down access a lot, but makes it + possible to access elements in O(1) and iterate in O(n). + """ + __slots__ = ('prev', 'key', 'value', 'next') + + def __init__(self, omd, key, value): + self.prev = omd._last_bucket + self.key = key + self.value = value + self.next = None + + if omd._first_bucket is None: + omd._first_bucket = self + if omd._last_bucket is not None: + omd._last_bucket.next = self + omd._last_bucket = self + + def unlink(self, omd): + if self.prev: + self.prev.next = self.next + if self.next: + self.next.prev = self.prev + if omd._first_bucket is self: + omd._first_bucket = self.next + if omd._last_bucket is self: + omd._last_bucket = self.prev + + +class OrderedMultiDict(MultiDict): + """Works like a regular :class:`MultiDict` but preserves the + order of the fields. To convert the ordered multi dict into a + list you can use the :meth:`items` method and pass it ``multi=True``. + + In general an :class:`OrderedMultiDict` is an order of magnitude + slower than a :class:`MultiDict`. + + .. admonition:: note + + Due to a limitation in Python you cannot convert an ordered + multi dict into a regular dict by using ``dict(multidict)``. + Instead you have to use the :meth:`to_dict` method, otherwise + the internal bucket objects are exposed. + """ + + def __init__(self, mapping=None): + dict.__init__(self) + self._first_bucket = self._last_bucket = None + if mapping is not None: + OrderedMultiDict.update(self, mapping) + + def __eq__(self, other): + if not isinstance(other, MultiDict): + return NotImplemented + if isinstance(other, OrderedMultiDict): + iter1 = self.iteritems(multi=True) + iter2 = other.iteritems(multi=True) + try: + for k1, v1 in iter1: + k2, v2 = iter2.next() + if k1 != k2 or v1 != v2: + return False + except StopIteration: + return False + try: + iter2.next() + except StopIteration: + return True + return False + if len(self) != len(other): + return False + for key, values in self.iterlists(): + if other.getlist(key) != values: + return False + return True + + def __ne__(self, other): + return not self.__eq__(other) + + def __reduce_ex__(self, protocol): + return type(self), (self.items(multi=True),) + + def __getstate__(self): + return self.items(multi=True) + + def __setstate__(self, values): + dict.clear(self) + for key, value in values: + self.add(key, value) + + def __getitem__(self, key): + if key in self: + return dict.__getitem__(self, key)[0].value + raise BadRequestKeyError(key) + + def __setitem__(self, key, value): + self.poplist(key) + self.add(key, value) + + def __delitem__(self, key): + self.pop(key) + + def iterkeys(self): + return (key for key, value in self.iteritems()) + + def itervalues(self): + return (value for key, value in self.iteritems()) + + def iteritems(self, multi=False): + ptr = self._first_bucket + if multi: + while ptr is not None: + yield ptr.key, ptr.value + ptr = ptr.next + else: + returned_keys = set() + while ptr is not None: + if ptr.key not in returned_keys: + returned_keys.add(ptr.key) + yield ptr.key, ptr.value + ptr = ptr.next + + def iterlists(self): + returned_keys = set() + ptr = self._first_bucket + while ptr is not None: + if ptr.key not in returned_keys: + yield ptr.key, self.getlist(ptr.key) + returned_keys.add(ptr.key) + ptr = ptr.next + + def iterlistvalues(self): + for key, values in self.iterlists(): + yield values + + def add(self, key, value): + dict.setdefault(self, key, []).append(_omd_bucket(self, key, value)) + + def getlist(self, key, type=None): + try: + rv = dict.__getitem__(self, key) + except KeyError: + return [] + if type is None: + return [x.value for x in rv] + result = [] + for item in rv: + try: + result.append(type(item.value)) + except ValueError: + pass + return result + + def setlist(self, key, new_list): + self.poplist(key) + for value in new_list: + self.add(key, value) + + def setlistdefault(self, key, default_list=None): + raise TypeError('setlistdefault is unsupported for ' + 'ordered multi dicts') + + def update(self, mapping): + for key, value in iter_multi_items(mapping): + OrderedMultiDict.add(self, key, value) + + def poplist(self, key): + buckets = dict.pop(self, key, ()) + for bucket in buckets: + bucket.unlink(self) + return [x.value for x in buckets] + + def pop(self, key, default=_missing): + try: + buckets = dict.pop(self, key) + except KeyError, e: + if default is not _missing: + return default + raise BadRequestKeyError(str(e)) + for bucket in buckets: + bucket.unlink(self) + return buckets[0].value + + def popitem(self): + try: + key, buckets = dict.popitem(self) + except KeyError, e: + raise BadRequestKeyError(str(e)) + for bucket in buckets: + bucket.unlink(self) + return key, buckets[0].value + + def popitemlist(self): + try: + key, buckets = dict.popitem(self) + except KeyError, e: + raise BadRequestKeyError(str(e)) + for bucket in buckets: + bucket.unlink(self) + return key, [x.value for x in buckets] + + +def _options_header_vkw(value, kw): + return dump_options_header(value, dict((k.replace('_', '-'), v) + for k, v in kw.items())) + + +class Headers(object): + """An object that stores some headers. It has a dict-like interface + but is ordered and can store the same keys multiple times. + + This data structure is useful if you want a nicer way to handle WSGI + headers which are stored as tuples in a list. + + From Werkzeug 0.3 onwards, the :exc:`KeyError` raised by this class is + also a subclass of the :class:`~exceptions.BadRequest` HTTP exception + and will render a page for a ``400 BAD REQUEST`` if caught in a + catch-all for HTTP exceptions. + + Headers is mostly compatible with the Python :class:`wsgiref.headers.Headers` + class, with the exception of `__getitem__`. :mod:`wsgiref` will return + `None` for ``headers['missing']``, whereas :class:`Headers` will raise + a :class:`KeyError`. + + To create a new :class:`Headers` object pass it a list or dict of headers + which are used as default values. This does not reuse the list passed + to the constructor for internal usage. To create a :class:`Headers` + object that uses as internal storage the list or list-like object you + can use the :meth:`linked` class method. + + :param defaults: The list of default values for the :class:`Headers`. + """ + + def __init__(self, defaults=None, _list=None): + if _list is None: + _list = [] + self._list = _list + if defaults is not None: + if isinstance(defaults, (list, Headers)): + self._list.extend(defaults) + else: + self.extend(defaults) + + @classmethod + def linked(cls, headerlist): + """Create a new :class:`Headers` object that uses the list of headers + passed as internal storage: + + >>> headerlist = [('Content-Length', '40')] + >>> headers = Headers.linked(headerlist) + >>> headers['Content-Type'] = 'text/html' + >>> headerlist + [('Content-Length', '40'), ('Content-Type', 'text/html')] + + :param headerlist: The list of headers the class is linked to. + :return: new linked :class:`Headers` object. + """ + return cls(_list=headerlist) + + def __getitem__(self, key, _get_mode=False): + if not _get_mode: + if isinstance(key, (int, long)): + return self._list[key] + elif isinstance(key, slice): + return self.__class__(self._list[key]) + ikey = key.lower() + for k, v in self._list: + if k.lower() == ikey: + return v + # micro optimization: if we are in get mode we will catch that + # exception one stack level down so we can raise a standard + # key error instead of our special one. + if _get_mode: + raise KeyError() + raise BadRequestKeyError(key) + + def __eq__(self, other): + return other.__class__ is self.__class__ and \ + set(other._list) == set(self._list) + + def __ne__(self, other): + return not self.__eq__(other) + + def get(self, key, default=None, type=None): + """Return the default value if the requested data doesn't exist. + If `type` is provided and is a callable it should convert the value, + return it or raise a :exc:`ValueError` if that is not possible. In + this case the function will return the default as if the value was not + found: + + >>> d = Headers([('Content-Length', '42')]) + >>> d.get('Content-Length', type=int) + 42 + + If a headers object is bound you must not add unicode strings + because no encoding takes place. + + :param key: The key to be looked up. + :param default: The default value to be returned if the key can't + be looked up. If not further specified `None` is + returned. + :param type: A callable that is used to cast the value in the + :class:`Headers`. If a :exc:`ValueError` is raised + by this callable the default value is returned. + """ + try: + rv = self.__getitem__(key, _get_mode=True) + except KeyError: + return default + if type is None: + return rv + try: + return type(rv) + except ValueError: + return default + + def getlist(self, key, type=None): + """Return the list of items for a given key. If that key is not in the + :class:`Headers`, the return value will be an empty list. Just as + :meth:`get` :meth:`getlist` accepts a `type` parameter. All items will + be converted with the callable defined there. + + :param key: The key to be looked up. + :param type: A callable that is used to cast the value in the + :class:`Headers`. If a :exc:`ValueError` is raised + by this callable the value will be removed from the list. + :return: a :class:`list` of all the values for the key. + """ + ikey = key.lower() + result = [] + for k, v in self: + if k.lower() == ikey: + if type is not None: + try: + v = type(v) + except ValueError: + continue + result.append(v) + return result + + def get_all(self, name): + """Return a list of all the values for the named field. + + This method is compatible with the :mod:`wsgiref` + :meth:`~wsgiref.headers.Headers.get_all` method. + """ + return self.getlist(name) + + def iteritems(self, lower=False): + for key, value in self: + if lower: + key = key.lower() + yield key, value + + def iterkeys(self, lower=False): + for key, _ in self.iteritems(lower): + yield key + + def itervalues(self): + for _, value in self.iteritems(): + yield value + + def keys(self, lower=False): + return list(self.iterkeys(lower)) + + def values(self): + return list(self.itervalues()) + + def items(self, lower=False): + return list(self.iteritems(lower)) + + def extend(self, iterable): + """Extend the headers with a dict or an iterable yielding keys and + values. + """ + if isinstance(iterable, dict): + for key, value in iterable.iteritems(): + if isinstance(value, (tuple, list)): + for v in value: + self.add(key, v) + else: + self.add(key, value) + else: + for key, value in iterable: + self.add(key, value) + + def __delitem__(self, key, _index_operation=True): + if _index_operation and isinstance(key, (int, long, slice)): + del self._list[key] + return + key = key.lower() + new = [] + for k, v in self._list: + if k.lower() != key: + new.append((k, v)) + self._list[:] = new + + def remove(self, key): + """Remove a key. + + :param key: The key to be removed. + """ + return self.__delitem__(key, _index_operation=False) + + def pop(self, key=None, default=_missing): + """Removes and returns a key or index. + + :param key: The key to be popped. If this is an integer the item at + that position is removed, if it's a string the value for + that key is. If the key is omitted or `None` the last + item is removed. + :return: an item. + """ + if key is None: + return self._list.pop() + if isinstance(key, (int, long)): + return self._list.pop(key) + try: + rv = self[key] + self.remove(key) + except KeyError: + if default is not _missing: + return default + raise + return rv + + def popitem(self): + """Removes a key or index and returns a (key, value) item.""" + return self.pop() + + def __contains__(self, key): + """Check if a key is present.""" + try: + self.__getitem__(key, _get_mode=True) + except KeyError: + return False + return True + + has_key = __contains__ + + def __iter__(self): + """Yield ``(key, value)`` tuples.""" + return iter(self._list) + + def __len__(self): + return len(self._list) + + def add(self, _key, _value, **kw): + """Add a new header tuple to the list. + + Keyword arguments can specify additional parameters for the header + value, with underscores converted to dashes:: + + >>> d = Headers() + >>> d.add('Content-Type', 'text/plain') + >>> d.add('Content-Disposition', 'attachment', filename='foo.png') + + The keyword argument dumping uses :func:`dump_options_header` + behind the scenes. + + .. versionadded:: 0.4.1 + keyword arguments were added for :mod:`wsgiref` compatibility. + """ + if kw: + _value = _options_header_vkw(_value, kw) + self._validate_value(_value) + self._list.append((_key, _value)) + + def _validate_value(self, value): + if isinstance(value, basestring) and ('\n' in value or '\r' in value): + raise ValueError('Detected newline in header value. This is ' + 'a potential security problem') + + def add_header(self, _key, _value, **_kw): + """Add a new header tuple to the list. + + An alias for :meth:`add` for compatibility with the :mod:`wsgiref` + :meth:`~wsgiref.headers.Headers.add_header` method. + """ + self.add(_key, _value, **_kw) + + def clear(self): + """Clears all headers.""" + del self._list[:] + + def set(self, _key, _value, **kw): + """Remove all header tuples for `key` and add a new one. The newly + added key either appears at the end of the list if there was no + entry or replaces the first one. + + Keyword arguments can specify additional parameters for the header + value, with underscores converted to dashes. See :meth:`add` for + more information. + + .. versionchanged:: 0.6.1 + :meth:`set` now accepts the same arguments as :meth:`add`. + + :param key: The key to be inserted. + :param value: The value to be inserted. + """ + if kw: + _value = _options_header_vkw(_value, kw) + self._validate_value(_value) + if not self._list: + self._list.append((_key, _value)) + return + listiter = iter(self._list) + ikey = _key.lower() + for idx, (old_key, old_value) in enumerate(listiter): + if old_key.lower() == ikey: + # replace first ocurrence + self._list[idx] = (_key, _value) + break + else: + self._list.append((_key, _value)) + return + self._list[idx + 1:] = [t for t in listiter if t[0].lower() != ikey] + + def setdefault(self, key, value): + """Returns the value for the key if it is in the dict, otherwise it + returns `default` and sets that value for `key`. + + :param key: The key to be looked up. + :param default: The default value to be returned if the key is not + in the dict. If not further specified it's `None`. + """ + if key in self: + return self[key] + self.set(key, value) + return value + + def __setitem__(self, key, value): + """Like :meth:`set` but also supports index/slice based setting.""" + if isinstance(key, (slice, int, long)): + self._validate_value(value) + self._list[key] = value + else: + self.set(key, value) + + def to_list(self, charset='iso-8859-1'): + """Convert the headers into a list and converts the unicode header + items to the specified charset. + + :return: list + """ + return [(k, isinstance(v, unicode) and v.encode(charset) or str(v)) + for k, v in self] + + def copy(self): + return self.__class__(self._list) + + def __copy__(self): + return self.copy() + + def __str__(self, charset='iso-8859-1'): + """Returns formatted headers suitable for HTTP transmission.""" + strs = [] + for key, value in self.to_list(charset): + strs.append('%s: %s' % (key, value)) + strs.append('\r\n') + return '\r\n'.join(strs) + + def __repr__(self): + return '%s(%r)' % ( + self.__class__.__name__, + list(self) + ) + + +class ImmutableHeadersMixin(object): + """Makes a :class:`Headers` immutable. We do not mark them as + hashable though since the only usecase for this datastructure + in Werkzeug is a view on a mutable structure. + + .. versionadded:: 0.5 + + :private: + """ + + def __delitem__(self, key): + is_immutable(self) + + def __setitem__(self, key, value): + is_immutable(self) + set = __setitem__ + + def add(self, item): + is_immutable(self) + remove = add_header = add + + def extend(self, iterable): + is_immutable(self) + + def insert(self, pos, value): + is_immutable(self) + + def pop(self, index=-1): + is_immutable(self) + + def popitem(self): + is_immutable(self) + + def setdefault(self, key, default): + is_immutable(self) + + +class EnvironHeaders(ImmutableHeadersMixin, Headers): + """Read only version of the headers from a WSGI environment. This + provides the same interface as `Headers` and is constructed from + a WSGI environment. + + From Werkzeug 0.3 onwards, the `KeyError` raised by this class is also a + subclass of the :exc:`~exceptions.BadRequest` HTTP exception and will + render a page for a ``400 BAD REQUEST`` if caught in a catch-all for + HTTP exceptions. + """ + + def __init__(self, environ): + self.environ = environ + + @classmethod + def linked(cls, environ): + raise TypeError('%r object is always linked to environment, ' + 'no separate initializer' % cls.__name__) + + def __eq__(self, other): + return self.environ is other.environ + + def __getitem__(self, key, _get_mode=False): + # _get_mode is a no-op for this class as there is no index but + # used because get() calls it. + key = key.upper().replace('-', '_') + if key in ('CONTENT_TYPE', 'CONTENT_LENGTH'): + return self.environ[key] + return self.environ['HTTP_' + key] + + def __len__(self): + # the iter is necessary because otherwise list calls our + # len which would call list again and so forth. + return len(list(iter(self))) + + def __iter__(self): + for key, value in self.environ.iteritems(): + if key.startswith('HTTP_') and key not in \ + ('HTTP_CONTENT_TYPE', 'HTTP_CONTENT_LENGTH'): + yield key[5:].replace('_', '-').title(), value + elif key in ('CONTENT_TYPE', 'CONTENT_LENGTH'): + yield key.replace('_', '-').title(), value + + def copy(self): + raise TypeError('cannot create %r copies' % self.__class__.__name__) + + +class CombinedMultiDict(ImmutableMultiDictMixin, MultiDict): + """A read only :class:`MultiDict` that you can pass multiple :class:`MultiDict` + instances as sequence and it will combine the return values of all wrapped + dicts: + + >>> from werkzeug.datastructures import CombinedMultiDict, MultiDict + >>> post = MultiDict([('foo', 'bar')]) + >>> get = MultiDict([('blub', 'blah')]) + >>> combined = CombinedMultiDict([get, post]) + >>> combined['foo'] + 'bar' + >>> combined['blub'] + 'blah' + + This works for all read operations and will raise a `TypeError` for + methods that usually change data which isn't possible. + + From Werkzeug 0.3 onwards, the `KeyError` raised by this class is also a + subclass of the :exc:`~exceptions.BadRequest` HTTP exception and will + render a page for a ``400 BAD REQUEST`` if caught in a catch-all for HTTP + exceptions. + """ + + def __reduce_ex__(self, protocol): + return type(self), (self.dicts,) + + def __init__(self, dicts=None): + self.dicts = dicts or [] + + @classmethod + def fromkeys(cls): + raise TypeError('cannot create %r instances by fromkeys' % + cls.__name__) + + def __getitem__(self, key): + for d in self.dicts: + if key in d: + return d[key] + raise BadRequestKeyError(key) + + def get(self, key, default=None, type=None): + for d in self.dicts: + if key in d: + if type is not None: + try: + return type(d[key]) + except ValueError: + continue + return d[key] + return default + + def getlist(self, key, type=None): + rv = [] + for d in self.dicts: + rv.extend(d.getlist(key, type)) + return rv + + def keys(self): + rv = set() + for d in self.dicts: + rv.update(d.keys()) + return list(rv) + + def iteritems(self, multi=False): + found = set() + for d in self.dicts: + for key, value in d.iteritems(multi): + if multi: + yield key, value + elif key not in found: + found.add(key) + yield key, value + + def itervalues(self): + for key, value in self.iteritems(): + yield value + + def values(self): + return list(self.itervalues()) + + def items(self, multi=False): + return list(self.iteritems(multi)) + + def iterlists(self): + rv = {} + for d in self.dicts: + for key, values in d.iterlists(): + rv.setdefault(key, []).extend(values) + return rv.iteritems() + + def lists(self): + return list(self.iterlists()) + + def iterlistvalues(self): + return (x[0] for x in self.lists()) + + def listvalues(self): + return list(self.iterlistvalues()) + + def iterkeys(self): + return iter(self.keys()) + + __iter__ = iterkeys + + def copy(self): + """Return a shallow copy of this object.""" + return self.__class__(self.dicts[:]) + + def to_dict(self, flat=True): + """Return the contents as regular dict. If `flat` is `True` the + returned dict will only have the first item present, if `flat` is + `False` all values will be returned as lists. + + :param flat: If set to `False` the dict returned will have lists + with all the values in it. Otherwise it will only + contain the first item for each key. + :return: a :class:`dict` + """ + rv = {} + for d in reversed(self.dicts): + rv.update(d.to_dict(flat)) + return rv + + def __len__(self): + return len(self.keys()) + + def __contains__(self, key): + for d in self.dicts: + if key in d: + return True + return False + + has_key = __contains__ + + def __repr__(self): + return '%s(%r)' % (self.__class__.__name__, self.dicts) + + +class FileMultiDict(MultiDict): + """A special :class:`MultiDict` that has convenience methods to add + files to it. This is used for :class:`EnvironBuilder` and generally + useful for unittesting. + + .. versionadded:: 0.5 + """ + + def add_file(self, name, file, filename=None, content_type=None): + """Adds a new file to the dict. `file` can be a file name or + a :class:`file`-like or a :class:`FileStorage` object. + + :param name: the name of the field. + :param file: a filename or :class:`file`-like object + :param filename: an optional filename + :param content_type: an optional content type + """ + if isinstance(file, FileStorage): + value = file + else: + if isinstance(file, basestring): + if filename is None: + filename = file + file = open(file, 'rb') + if filename and content_type is None: + content_type = mimetypes.guess_type(filename)[0] or \ + 'application/octet-stream' + value = FileStorage(file, filename, name, content_type) + + self.add(name, value) + + +class ImmutableDict(ImmutableDictMixin, dict): + """An immutable :class:`dict`. + + .. versionadded:: 0.5 + """ + + __repr__ = _proxy_repr(dict) + + def copy(self): + """Return a shallow mutable copy of this object. Keep in mind that + the standard library's :func:`copy` function is a no-op for this class + like for any other python immutable type (eg: :class:`tuple`). + """ + return dict(self) + + def __copy__(self): + return self + + +class ImmutableMultiDict(ImmutableMultiDictMixin, MultiDict): + """An immutable :class:`MultiDict`. + + .. versionadded:: 0.5 + """ + + def copy(self): + """Return a shallow mutable copy of this object. Keep in mind that + the standard library's :func:`copy` function is a no-op for this class + like for any other python immutable type (eg: :class:`tuple`). + """ + return MultiDict(self) + + def __copy__(self): + return self + + +class ImmutableOrderedMultiDict(ImmutableMultiDictMixin, OrderedMultiDict): + """An immutable :class:`OrderedMultiDict`. + + .. versionadded:: 0.6 + """ + + def copy(self): + """Return a shallow mutable copy of this object. Keep in mind that + the standard library's :func:`copy` function is a no-op for this class + like for any other python immutable type (eg: :class:`tuple`). + """ + return OrderedMultiDict(self) + + def __copy__(self): + return self + + +class Accept(ImmutableList): + """An :class:`Accept` object is just a list subclass for lists of + ``(value, quality)`` tuples. It is automatically sorted by quality. + + All :class:`Accept` objects work similar to a list but provide extra + functionality for working with the data. Containment checks are + normalized to the rules of that header: + + >>> a = CharsetAccept([('ISO-8859-1', 1), ('utf-8', 0.7)]) + >>> a.best + 'ISO-8859-1' + >>> 'iso-8859-1' in a + True + >>> 'UTF8' in a + True + >>> 'utf7' in a + False + + To get the quality for an item you can use normal item lookup: + + >>> print a['utf-8'] + 0.7 + >>> a['utf7'] + 0 + + .. versionchanged:: 0.5 + :class:`Accept` objects are forced immutable now. + """ + + def __init__(self, values=()): + if values is None: + list.__init__(self) + self.provided = False + elif isinstance(values, Accept): + self.provided = values.provided + list.__init__(self, values) + else: + self.provided = True + values = [(a, b) for b, a in values] + values.sort() + values.reverse() + list.__init__(self, [(a, b) for b, a in values]) + + def _value_matches(self, value, item): + """Check if a value matches a given accept item.""" + return item == '*' or item.lower() == value.lower() + + def __getitem__(self, key): + """Besides index lookup (getting item n) you can also pass it a string + to get the quality for the item. If the item is not in the list, the + returned quality is ``0``. + """ + if isinstance(key, basestring): + return self.quality(key) + return list.__getitem__(self, key) + + def quality(self, key): + """Returns the quality of the key. + + .. versionadded:: 0.6 + In previous versions you had to use the item-lookup syntax + (eg: ``obj[key]`` instead of ``obj.quality(key)``) + """ + for item, quality in self: + if self._value_matches(key, item): + return quality + return 0 + + def __contains__(self, value): + for item, quality in self: + if self._value_matches(value, item): + return True + return False + + def __repr__(self): + return '%s([%s])' % ( + self.__class__.__name__, + ', '.join('(%r, %s)' % (x, y) for x, y in self) + ) + + def index(self, key): + """Get the position of an entry or raise :exc:`ValueError`. + + :param key: The key to be looked up. + + .. versionchanged:: 0.5 + This used to raise :exc:`IndexError`, which was inconsistent + with the list API. + """ + if isinstance(key, basestring): + for idx, (item, quality) in enumerate(self): + if self._value_matches(key, item): + return idx + raise ValueError(key) + return list.index(self, key) + + def find(self, key): + """Get the position of an entry or return -1. + + :param key: The key to be looked up. + """ + try: + return self.index(key) + except ValueError: + return -1 + + def values(self): + """Return a list of the values, not the qualities.""" + return list(self.itervalues()) + + def itervalues(self): + """Iterate over all values.""" + for item in self: + yield item[0] + + def to_header(self): + """Convert the header set into an HTTP header string.""" + result = [] + for value, quality in self: + if quality != 1: + value = '%s;q=%s' % (value, quality) + result.append(value) + return ','.join(result) + + def __str__(self): + return self.to_header() + + def best_match(self, matches, default=None): + """Returns the best match from a list of possible matches based + on the quality of the client. If two items have the same quality, + the one is returned that comes first. + + :param matches: a list of matches to check for + :param default: the value that is returned if none match + """ + best_quality = -1 + result = default + for server_item in matches: + for client_item, quality in self: + if quality <= best_quality: + break + if self._value_matches(server_item, client_item): + best_quality = quality + result = server_item + return result + + @property + def best(self): + """The best match as value.""" + if self: + return self[0][0] + + +class MIMEAccept(Accept): + """Like :class:`Accept` but with special methods and behavior for + mimetypes. + """ + + def _value_matches(self, value, item): + def _normalize(x): + x = x.lower() + return x == '*' and ('*', '*') or x.split('/', 1) + + # this is from the application which is trusted. to avoid developer + # frustration we actually check these for valid values + if '/' not in value: + raise ValueError('invalid mimetype %r' % value) + value_type, value_subtype = _normalize(value) + if value_type == '*' and value_subtype != '*': + raise ValueError('invalid mimetype %r' % value) + + if '/' not in item: + return False + item_type, item_subtype = _normalize(item) + if item_type == '*' and item_subtype != '*': + return False + return ( + (item_type == item_subtype == '*' or + value_type == value_subtype == '*') or + (item_type == value_type and (item_subtype == '*' or + value_subtype == '*' or + item_subtype == value_subtype)) + ) + + @property + def accept_html(self): + """True if this object accepts HTML.""" + return ( + 'text/html' in self or + 'application/xhtml+xml' in self or + self.accept_xhtml + ) + + @property + def accept_xhtml(self): + """True if this object accepts XHTML.""" + return ( + 'application/xhtml+xml' in self or + 'application/xml' in self + ) + + @property + def accept_json(self): + """True if this object accepts JSON.""" + return 'application/json' in self + + +class LanguageAccept(Accept): + """Like :class:`Accept` but with normalization for languages.""" + + def _value_matches(self, value, item): + def _normalize(language): + return _locale_delim_re.split(language.lower()) + return item == '*' or _normalize(value) == _normalize(item) + + +class CharsetAccept(Accept): + """Like :class:`Accept` but with normalization for charsets.""" + + def _value_matches(self, value, item): + def _normalize(name): + try: + return codecs.lookup(name).name + except LookupError: + return name.lower() + return item == '*' or _normalize(value) == _normalize(item) + + +def cache_property(key, empty, type): + """Return a new property object for a cache header. Useful if you + want to add support for a cache extension in a subclass.""" + return property(lambda x: x._get_cache_value(key, empty, type), + lambda x, v: x._set_cache_value(key, v, type), + lambda x: x._del_cache_value(key), + 'accessor for %r' % key) + + +class _CacheControl(UpdateDictMixin, dict): + """Subclass of a dict that stores values for a Cache-Control header. It + has accessors for all the cache-control directives specified in RFC 2616. + The class does not differentiate between request and response directives. + + Because the cache-control directives in the HTTP header use dashes the + python descriptors use underscores for that. + + To get a header of the :class:`CacheControl` object again you can convert + the object into a string or call the :meth:`to_header` method. If you plan + to subclass it and add your own items have a look at the sourcecode for + that class. + + .. versionchanged:: 0.4 + + Setting `no_cache` or `private` to boolean `True` will set the implicit + none-value which is ``*``: + + >>> cc = ResponseCacheControl() + >>> cc.no_cache = True + >>> cc + + >>> cc.no_cache + '*' + >>> cc.no_cache = None + >>> cc + + + In versions before 0.5 the behavior documented here affected the now + no longer existing `CacheControl` class. + """ + + no_cache = cache_property('no-cache', '*', None) + no_store = cache_property('no-store', None, bool) + max_age = cache_property('max-age', -1, int) + no_transform = cache_property('no-transform', None, None) + + def __init__(self, values=(), on_update=None): + dict.__init__(self, values or ()) + self.on_update = on_update + self.provided = values is not None + + def _get_cache_value(self, key, empty, type): + """Used internally by the accessor properties.""" + if type is bool: + return key in self + if key in self: + value = self[key] + if value is None: + return empty + elif type is not None: + try: + value = type(value) + except ValueError: + pass + return value + + def _set_cache_value(self, key, value, type): + """Used internally by the accessor properties.""" + if type is bool: + if value: + self[key] = None + else: + self.pop(key, None) + else: + if value is None: + self.pop(key) + elif value is True: + self[key] = None + else: + self[key] = value + + def _del_cache_value(self, key): + """Used internally by the accessor properties.""" + if key in self: + del self[key] + + def to_header(self): + """Convert the stored values into a cache control header.""" + return dump_header(self) + + def __str__(self): + return self.to_header() + + def __repr__(self): + return '<%s %r>' % ( + self.__class__.__name__, + self.to_header() + ) + + +class RequestCacheControl(ImmutableDictMixin, _CacheControl): + """A cache control for requests. This is immutable and gives access + to all the request-relevant cache control headers. + + To get a header of the :class:`RequestCacheControl` object again you can + convert the object into a string or call the :meth:`to_header` method. If + you plan to subclass it and add your own items have a look at the sourcecode + for that class. + + .. versionadded:: 0.5 + In previous versions a `CacheControl` class existed that was used + both for request and response. + """ + + max_stale = cache_property('max-stale', '*', int) + min_fresh = cache_property('min-fresh', '*', int) + no_transform = cache_property('no-transform', None, None) + only_if_cached = cache_property('only-if-cached', None, bool) + + +class ResponseCacheControl(_CacheControl): + """A cache control for responses. Unlike :class:`RequestCacheControl` + this is mutable and gives access to response-relevant cache control + headers. + + To get a header of the :class:`ResponseCacheControl` object again you can + convert the object into a string or call the :meth:`to_header` method. If + you plan to subclass it and add your own items have a look at the sourcecode + for that class. + + .. versionadded:: 0.5 + In previous versions a `CacheControl` class existed that was used + both for request and response. + """ + + public = cache_property('public', None, bool) + private = cache_property('private', '*', None) + must_revalidate = cache_property('must-revalidate', None, bool) + proxy_revalidate = cache_property('proxy-revalidate', None, bool) + s_maxage = cache_property('s-maxage', None, None) + + +# attach cache_property to the _CacheControl as staticmethod +# so that others can reuse it. +_CacheControl.cache_property = staticmethod(cache_property) + + +class CallbackDict(UpdateDictMixin, dict): + """A dict that calls a function passed every time something is changed. + The function is passed the dict instance. + """ + + def __init__(self, initial=None, on_update=None): + dict.__init__(self, initial or ()) + self.on_update = on_update + + def __repr__(self): + return '<%s %s>' % ( + self.__class__.__name__, + dict.__repr__(self) + ) + + +class HeaderSet(object): + """Similar to the :class:`ETags` class this implements a set-like structure. + Unlike :class:`ETags` this is case insensitive and used for vary, allow, and + content-language headers. + + If not constructed using the :func:`parse_set_header` function the + instantiation works like this: + + >>> hs = HeaderSet(['foo', 'bar', 'baz']) + >>> hs + HeaderSet(['foo', 'bar', 'baz']) + """ + + def __init__(self, headers=None, on_update=None): + self._headers = list(headers or ()) + self._set = set([x.lower() for x in self._headers]) + self.on_update = on_update + + def add(self, header): + """Add a new header to the set.""" + self.update((header,)) + + def remove(self, header): + """Remove a header from the set. This raises an :exc:`KeyError` if the + header is not in the set. + + .. versionchanged:: 0.5 + In older versions a :exc:`IndexError` was raised instead of a + :exc:`KeyError` if the object was missing. + + :param header: the header to be removed. + """ + key = header.lower() + if key not in self._set: + raise KeyError(header) + self._set.remove(key) + for idx, key in enumerate(self._headers): + if key.lower() == header: + del self._headers[idx] + break + if self.on_update is not None: + self.on_update(self) + + def update(self, iterable): + """Add all the headers from the iterable to the set. + + :param iterable: updates the set with the items from the iterable. + """ + inserted_any = False + for header in iterable: + key = header.lower() + if key not in self._set: + self._headers.append(header) + self._set.add(key) + inserted_any = True + if inserted_any and self.on_update is not None: + self.on_update(self) + + def discard(self, header): + """Like :meth:`remove` but ignores errors. + + :param header: the header to be discarded. + """ + try: + return self.remove(header) + except KeyError: + pass + + def find(self, header): + """Return the index of the header in the set or return -1 if not found. + + :param header: the header to be looked up. + """ + header = header.lower() + for idx, item in enumerate(self._headers): + if item.lower() == header: + return idx + return -1 + + def index(self, header): + """Return the index of the header in the set or raise an + :exc:`IndexError`. + + :param header: the header to be looked up. + """ + rv = self.find(header) + if rv < 0: + raise IndexError(header) + return rv + + def clear(self): + """Clear the set.""" + self._set.clear() + del self._headers[:] + if self.on_update is not None: + self.on_update(self) + + def as_set(self, preserve_casing=False): + """Return the set as real python set type. When calling this, all + the items are converted to lowercase and the ordering is lost. + + :param preserve_casing: if set to `True` the items in the set returned + will have the original case like in the + :class:`HeaderSet`, otherwise they will + be lowercase. + """ + if preserve_casing: + return set(self._headers) + return set(self._set) + + def to_header(self): + """Convert the header set into an HTTP header string.""" + return ', '.join(map(quote_header_value, self._headers)) + + def __getitem__(self, idx): + return self._headers[idx] + + def __delitem__(self, idx): + rv = self._headers.pop(idx) + self._set.remove(rv.lower()) + if self.on_update is not None: + self.on_update(self) + + def __setitem__(self, idx, value): + old = self._headers[idx] + self._set.remove(old.lower()) + self._headers[idx] = value + self._set.add(value.lower()) + if self.on_update is not None: + self.on_update(self) + + def __contains__(self, header): + return header.lower() in self._set + + def __len__(self): + return len(self._set) + + def __iter__(self): + return iter(self._headers) + + def __nonzero__(self): + return bool(self._set) + + def __str__(self): + return self.to_header() + + def __repr__(self): + return '%s(%r)' % ( + self.__class__.__name__, + self._headers + ) + + +class ETags(object): + """A set that can be used to check if one etag is present in a collection + of etags. + """ + + def __init__(self, strong_etags=None, weak_etags=None, star_tag=False): + self._strong = frozenset(not star_tag and strong_etags or ()) + self._weak = frozenset(weak_etags or ()) + self.star_tag = star_tag + + def as_set(self, include_weak=False): + """Convert the `ETags` object into a python set. Per default all the + weak etags are not part of this set.""" + rv = set(self._strong) + if include_weak: + rv.update(self._weak) + return rv + + def is_weak(self, etag): + """Check if an etag is weak.""" + return etag in self._weak + + def contains_weak(self, etag): + """Check if an etag is part of the set including weak and strong tags.""" + return self.is_weak(etag) or self.contains(etag) + + def contains(self, etag): + """Check if an etag is part of the set ignoring weak tags. + It is also possible to use the ``in`` operator. + + """ + if self.star_tag: + return True + return etag in self._strong + + def contains_raw(self, etag): + """When passed a quoted tag it will check if this tag is part of the + set. If the tag is weak it is checked against weak and strong tags, + otherwise strong only.""" + etag, weak = unquote_etag(etag) + if weak: + return self.contains_weak(etag) + return self.contains(etag) + + def to_header(self): + """Convert the etags set into a HTTP header string.""" + if self.star_tag: + return '*' + return ', '.join( + ['"%s"' % x for x in self._strong] + + ['w/"%s"' % x for x in self._weak] + ) + + def __call__(self, etag=None, data=None, include_weak=False): + if [etag, data].count(None) != 1: + raise TypeError('either tag or data required, but at least one') + if etag is None: + etag = generate_etag(data) + if include_weak: + if etag in self._weak: + return True + return etag in self._strong + + def __nonzero__(self): + return bool(self.star_tag or self._strong) + + def __str__(self): + return self.to_header() + + def __iter__(self): + return iter(self._strong) + + def __contains__(self, etag): + return self.contains(etag) + + def __repr__(self): + return '<%s %r>' % (self.__class__.__name__, str(self)) + + +class IfRange(object): + """Very simple object that represents the `If-Range` header in parsed + form. It will either have neither a etag or date or one of either but + never both. + + .. versionadded:: 0.7 + """ + + def __init__(self, etag=None, date=None): + #: The etag parsed and unquoted. Ranges always operate on strong + #: etags so the weakness information is not necessary. + self.etag = etag + #: The date in parsed format or `None`. + self.date = date + + def to_header(self): + """Converts the object back into an HTTP header.""" + if self.date is not None: + return http_date(self.date) + if self.etag is not None: + return quote_etag(self.etag) + return '' + + def __str__(self): + return self.to_header() + + def __repr__(self): + return '<%s %r>' % (self.__class__.__name__, str(self)) + + +class Range(object): + """Represents a range header. All the methods are only supporting bytes + as unit. It does store multiple ranges but :meth:`range_for_length` will + only work if only one range is provided. + + .. versionadded:: 0.7 + """ + + def __init__(self, units, ranges): + #: The units of this range. Usually "bytes". + self.units = units + #: A list of ``(begin, end)`` tuples for the range header provided. + #: The ranges are non-inclusive. + self.ranges = ranges + + def range_for_length(self, length): + """If the range is for bytes, the length is not None and there is + exactly one range and it is satisfiable it returns a ``(start, stop)`` + tuple, otherwise `None`. + """ + if self.units != 'bytes' or length is None or len(self.ranges) != 1: + return None + start, end = self.ranges[0] + if end is None: + end = length + if start < 0: + start += length + if is_byte_range_valid(start, end, length): + return start, min(end, length) + + def make_content_range(self, length): + """Creates a :class:`~werkzeug.datastructures.ContentRange` object + from the current range and given content length. + """ + rng = self.range_for_length(length) + if rng is not None: + return ContentRange(self.units, rng[0], rng[1], length) + + def to_header(self): + """Converts the object back into an HTTP header.""" + ranges = [] + for begin, end in self.ranges: + if end is None: + ranges.append(begin >= 0 and '%s-' % begin or str(begin)) + else: + ranges.append('%s-%s' % (begin, end - 1)) + return '%s=%s' % (self.units, ','.join(ranges)) + + def __str__(self): + return self.to_header() + + def __repr__(self): + return '<%s %r>' % (self.__class__.__name__, str(self)) + + +class ContentRange(object): + """Represents the content range header. + + .. versionadded:: 0.7 + """ + + def __init__(self, units, start, stop, length=None, on_update=None): + assert is_byte_range_valid(start, stop, length), \ + 'Bad range provided' + self.on_update = on_update + self.set(start, stop, length, units) + + def _callback_property(name): + def fget(self): + return getattr(self, name) + def fset(self, value): + setattr(self, name, value) + if self.on_update is not None: + self.on_update(self) + return property(fget, fset) + + #: The units to use, usually "bytes" + units = _callback_property('_units') + #: The start point of the range or `None`. + start = _callback_property('_start') + #: The stop point of the range (non-inclusive) or `None`. Can only be + #: `None` if also start is `None`. + stop = _callback_property('_stop') + #: The length of the range or `None`. + length = _callback_property('_length') + + def set(self, start, stop, length=None, units='bytes'): + """Simple method to update the ranges.""" + assert is_byte_range_valid(start, stop, length), \ + 'Bad range provided' + self._units = units + self._start = start + self._stop = stop + self._length = length + if self.on_update is not None: + self.on_update(self) + + def unset(self): + """Sets the units to `None` which indicates that the header should + no longer be used. + """ + self.set(None, None, units=None) + + def to_header(self): + if self.units is None: + return '' + if self.length is None: + length = '*' + else: + length = self.length + if self.start is None: + return '%s */%s' % (self.units, length) + return '%s %s-%s/%s' % ( + self.units, + self.start, + self.stop - 1, + length + ) + + def __nonzero__(self): + return self.units is not None + + def __str__(self): + return self.to_header() + + def __repr__(self): + return '<%s %r>' % (self.__class__.__name__, str(self)) + + +class Authorization(ImmutableDictMixin, dict): + """Represents an `Authorization` header sent by the client. You should + not create this kind of object yourself but use it when it's returned by + the `parse_authorization_header` function. + + This object is a dict subclass and can be altered by setting dict items + but it should be considered immutable as it's returned by the client and + not meant for modifications. + + .. versionchanged:: 0.5 + This object became immutable. + """ + + def __init__(self, auth_type, data=None): + dict.__init__(self, data or {}) + self.type = auth_type + + username = property(lambda x: x.get('username'), doc=''' + The username transmitted. This is set for both basic and digest + auth all the time.''') + password = property(lambda x: x.get('password'), doc=''' + When the authentication type is basic this is the password + transmitted by the client, else `None`.''') + realm = property(lambda x: x.get('realm'), doc=''' + This is the server realm sent back for HTTP digest auth.''') + nonce = property(lambda x: x.get('nonce'), doc=''' + The nonce the server sent for digest auth, sent back by the client. + A nonce should be unique for every 401 response for HTTP digest + auth.''') + uri = property(lambda x: x.get('uri'), doc=''' + The URI from Request-URI of the Request-Line; duplicated because + proxies are allowed to change the Request-Line in transit. HTTP + digest auth only.''') + nc = property(lambda x: x.get('nc'), doc=''' + The nonce count value transmitted by clients if a qop-header is + also transmitted. HTTP digest auth only.''') + cnonce = property(lambda x: x.get('cnonce'), doc=''' + If the server sent a qop-header in the ``WWW-Authenticate`` + header, the client has to provide this value for HTTP digest auth. + See the RFC for more details.''') + response = property(lambda x: x.get('response'), doc=''' + A string of 32 hex digits computed as defined in RFC 2617, which + proves that the user knows a password. Digest auth only.''') + opaque = property(lambda x: x.get('opaque'), doc=''' + The opaque header from the server returned unchanged by the client. + It is recommended that this string be base64 or hexadecimal data. + Digest auth only.''') + + @property + def qop(self): + """Indicates what "quality of protection" the client has applied to + the message for HTTP digest auth.""" + def on_update(header_set): + if not header_set and 'qop' in self: + del self['qop'] + elif header_set: + self['qop'] = header_set.to_header() + return parse_set_header(self.get('qop'), on_update) + + +class WWWAuthenticate(UpdateDictMixin, dict): + """Provides simple access to `WWW-Authenticate` headers.""" + + #: list of keys that require quoting in the generated header + _require_quoting = frozenset(['domain', 'nonce', 'opaque', 'realm']) + + def __init__(self, auth_type=None, values=None, on_update=None): + dict.__init__(self, values or ()) + if auth_type: + self['__auth_type__'] = auth_type + self.on_update = on_update + + def set_basic(self, realm='authentication required'): + """Clear the auth info and enable basic auth.""" + dict.clear(self) + dict.update(self, {'__auth_type__': 'basic', 'realm': realm}) + if self.on_update: + self.on_update(self) + + def set_digest(self, realm, nonce, qop=('auth',), opaque=None, + algorithm=None, stale=False): + """Clear the auth info and enable digest auth.""" + d = { + '__auth_type__': 'digest', + 'realm': realm, + 'nonce': nonce, + 'qop': dump_header(qop) + } + if stale: + d['stale'] = 'TRUE' + if opaque is not None: + d['opaque'] = opaque + if algorithm is not None: + d['algorithm'] = algorithm + dict.clear(self) + dict.update(self, d) + if self.on_update: + self.on_update(self) + + def to_header(self): + """Convert the stored values into a WWW-Authenticate header.""" + d = dict(self) + auth_type = d.pop('__auth_type__', None) or 'basic' + return '%s %s' % (auth_type.title(), ', '.join([ + '%s=%s' % (key, quote_header_value(value, + allow_token=key not in self._require_quoting)) + for key, value in d.iteritems() + ])) + + def __str__(self): + return self.to_header() + + def __repr__(self): + return '<%s %r>' % ( + self.__class__.__name__, + self.to_header() + ) + + def auth_property(name, doc=None): + """A static helper function for subclasses to add extra authentication + system properties onto a class:: + + class FooAuthenticate(WWWAuthenticate): + special_realm = auth_property('special_realm') + + For more information have a look at the sourcecode to see how the + regular properties (:attr:`realm` etc.) are implemented. + """ + def _set_value(self, value): + if value is None: + self.pop(name, None) + else: + self[name] = str(value) + return property(lambda x: x.get(name), _set_value, doc=doc) + + def _set_property(name, doc=None): + def fget(self): + def on_update(header_set): + if not header_set and name in self: + del self[name] + elif header_set: + self[name] = header_set.to_header() + return parse_set_header(self.get(name), on_update) + return property(fget, doc=doc) + + type = auth_property('__auth_type__', doc=''' + The type of the auth mechanism. HTTP currently specifies + `Basic` and `Digest`.''') + realm = auth_property('realm', doc=''' + A string to be displayed to users so they know which username and + password to use. This string should contain at least the name of + the host performing the authentication and might additionally + indicate the collection of users who might have access.''') + domain = _set_property('domain', doc=''' + A list of URIs that define the protection space. If a URI is an + absolute path, it is relative to the canonical root URL of the + server being accessed.''') + nonce = auth_property('nonce', doc=''' + A server-specified data string which should be uniquely generated + each time a 401 response is made. It is recommended that this + string be base64 or hexadecimal data.''') + opaque = auth_property('opaque', doc=''' + A string of data, specified by the server, which should be returned + by the client unchanged in the Authorization header of subsequent + requests with URIs in the same protection space. It is recommended + that this string be base64 or hexadecimal data.''') + algorithm = auth_property('algorithm', doc=''' + A string indicating a pair of algorithms used to produce the digest + and a checksum. If this is not present it is assumed to be "MD5". + If the algorithm is not understood, the challenge should be ignored + (and a different one used, if there is more than one).''') + qop = _set_property('qop', doc=''' + A set of quality-of-privacy directives such as auth and auth-int.''') + + def _get_stale(self): + val = self.get('stale') + if val is not None: + return val.lower() == 'true' + def _set_stale(self, value): + if value is None: + self.pop('stale', None) + else: + self['stale'] = value and 'TRUE' or 'FALSE' + stale = property(_get_stale, _set_stale, doc=''' + A flag, indicating that the previous request from the client was + rejected because the nonce value was stale.''') + del _get_stale, _set_stale + + # make auth_property a staticmethod so that subclasses of + # `WWWAuthenticate` can use it for new properties. + auth_property = staticmethod(auth_property) + del _set_property + + +class FileStorage(object): + """The :class:`FileStorage` class is a thin wrapper over incoming files. + It is used by the request object to represent uploaded files. All the + attributes of the wrapper stream are proxied by the file storage so + it's possible to do ``storage.read()`` instead of the long form + ``storage.stream.read()``. + """ + + def __init__(self, stream=None, filename=None, name=None, + content_type=None, content_length=None, + headers=None): + self.name = name + self.stream = stream or _empty_stream + + # if no filename is provided we can attempt to get the filename + # from the stream object passed. There we have to be careful to + # skip things like , etc. Python marks these + # special filenames with angular brackets. + if filename is None: + filename = getattr(stream, 'name', None) + if filename and filename[0] == '<' and filename[-1] == '>': + filename = None + + self.filename = filename + if headers is None: + headers = Headers() + self.headers = headers + if content_type is not None: + headers['Content-Type'] = content_type + if content_length is not None: + headers['Content-Length'] = str(content_length) + + def _parse_content_type(self): + if not hasattr(self, '_parsed_content_type'): + self._parsed_content_type = \ + parse_options_header(self.content_type) + + @property + def content_type(self): + """The file's content type. Usually not available""" + return self.headers.get('content-type') + + @property + def content_length(self): + """The file's content length. Usually not available""" + return int(self.headers.get('content-length') or 0) + + @property + def mimetype(self): + """Like :attr:`content_type` but without parameters (eg, without + charset, type etc.). For example if the content + type is ``text/html; charset=utf-8`` the mimetype would be + ``'text/html'``. + + .. versionadded:: 0.7 + """ + self._parse_content_type() + return self._parsed_content_type[0] + + @property + def mimetype_params(self): + """The mimetype parameters as dict. For example if the content + type is ``text/html; charset=utf-8`` the params would be + ``{'charset': 'utf-8'}``. + + .. versionadded:: 0.7 + """ + self._parse_content_type() + return self._parsed_content_type[1] + + def save(self, dst, buffer_size=16384): + """Save the file to a destination path or file object. If the + destination is a file object you have to close it yourself after the + call. The buffer size is the number of bytes held in memory during + the copy process. It defaults to 16KB. + + For secure file saving also have a look at :func:`secure_filename`. + + :param dst: a filename or open file object the uploaded file + is saved to. + :param buffer_size: the size of the buffer. This works the same as + the `length` parameter of + :func:`shutil.copyfileobj`. + """ + from shutil import copyfileobj + close_dst = False + if isinstance(dst, basestring): + dst = file(dst, 'wb') + close_dst = True + try: + copyfileobj(self.stream, dst, buffer_size) + finally: + if close_dst: + dst.close() + + def close(self): + """Close the underlying file if possible.""" + try: + self.stream.close() + except Exception: + pass + + def __nonzero__(self): + return bool(self.filename) + + def __getattr__(self, name): + return getattr(self.stream, name) + + def __iter__(self): + return iter(self.readline, '') + + def __repr__(self): + return '<%s: %r (%r)>' % ( + self.__class__.__name__, + self.filename, + self.content_type + ) + + +# circular dependencies +from werkzeug.http import dump_options_header, dump_header, generate_etag, \ + quote_header_value, parse_set_header, unquote_etag, quote_etag, \ + parse_options_header, http_date, is_byte_range_valid +from werkzeug.exceptions import BadRequestKeyError diff --git a/websdk/werkzeug/debug/__init__.py b/websdk/werkzeug/debug/__init__.py new file mode 100644 index 0000000..db91b74 --- /dev/null +++ b/websdk/werkzeug/debug/__init__.py @@ -0,0 +1,185 @@ +# -*- coding: utf-8 -*- +""" + werkzeug.debug + ~~~~~~~~~~~~~~ + + WSGI application traceback debugger. + + :copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" +import mimetypes +from os.path import join, dirname, basename, isfile +from werkzeug.wrappers import BaseRequest as Request, BaseResponse as Response +from werkzeug.debug.tbtools import get_current_traceback, render_console_html +from werkzeug.debug.console import Console +from werkzeug.security import gen_salt + + +#: import this here because it once was documented as being available +#: from this module. In case there are users left ... +from werkzeug.debug.repr import debug_repr + + +class _ConsoleFrame(object): + """Helper class so that we can reuse the frame console code for the + standalone console. + """ + + def __init__(self, namespace): + self.console = Console(namespace) + self.id = 0 + + +class DebuggedApplication(object): + """Enables debugging support for a given application:: + + from werkzeug.debug import DebuggedApplication + from myapp import app + app = DebuggedApplication(app, evalex=True) + + The `evalex` keyword argument allows evaluating expressions in a + traceback's frame context. + + .. versionadded:: 0.7 + The `lodgeit_url` parameter was added. + + :param app: the WSGI application to run debugged. + :param evalex: enable exception evaluation feature (interactive + debugging). This requires a non-forking server. + :param request_key: The key that points to the request object in ths + environment. This parameter is ignored in current + versions. + :param console_path: the URL for a general purpose console. + :param console_init_func: the function that is executed before starting + the general purpose console. The return value + is used as initial namespace. + :param show_hidden_frames: by default hidden traceback frames are skipped. + You can show them by setting this parameter + to `True`. + :param lodgeit_url: the base URL of the LodgeIt instance to use for + pasting tracebacks. + """ + + # this class is public + __module__ = 'werkzeug' + + def __init__(self, app, evalex=False, request_key='werkzeug.request', + console_path='/console', console_init_func=None, + show_hidden_frames=False, + lodgeit_url='http://paste.pocoo.org/'): + if not console_init_func: + console_init_func = dict + self.app = app + self.evalex = evalex + self.frames = {} + self.tracebacks = {} + self.request_key = request_key + self.console_path = console_path + self.console_init_func = console_init_func + self.show_hidden_frames = show_hidden_frames + self.lodgeit_url = lodgeit_url + self.secret = gen_salt(20) + + def debug_application(self, environ, start_response): + """Run the application and conserve the traceback frames.""" + app_iter = None + try: + app_iter = self.app(environ, start_response) + for item in app_iter: + yield item + if hasattr(app_iter, 'close'): + app_iter.close() + except Exception: + if hasattr(app_iter, 'close'): + app_iter.close() + traceback = get_current_traceback(skip=1, show_hidden_frames= + self.show_hidden_frames, + ignore_system_exceptions=True) + for frame in traceback.frames: + self.frames[frame.id] = frame + self.tracebacks[traceback.id] = traceback + + try: + start_response('500 INTERNAL SERVER ERROR', [ + ('Content-Type', 'text/html; charset=utf-8') + ]) + except Exception: + # if we end up here there has been output but an error + # occurred. in that situation we can do nothing fancy any + # more, better log something into the error log and fall + # back gracefully. + environ['wsgi.errors'].write( + 'Debugging middleware caught exception in streamed ' + 'response at a point where response headers were already ' + 'sent.\n') + else: + yield traceback.render_full(evalex=self.evalex, + lodgeit_url=self.lodgeit_url, + secret=self.secret) \ + .encode('utf-8', 'replace') + + traceback.log(environ['wsgi.errors']) + + def execute_command(self, request, command, frame): + """Execute a command in a console.""" + return Response(frame.console.eval(command), mimetype='text/html') + + def display_console(self, request): + """Display a standalone shell.""" + if 0 not in self.frames: + self.frames[0] = _ConsoleFrame(self.console_init_func()) + return Response(render_console_html(secret=self.secret), + mimetype='text/html') + + def paste_traceback(self, request, traceback): + """Paste the traceback and return a JSON response.""" + paste_id = traceback.paste(self.lodgeit_url) + return Response('{"url": "%sshow/%s/", "id": "%s"}' + % (self.lodgeit_url, paste_id, paste_id), + mimetype='application/json') + + def get_source(self, request, frame): + """Render the source viewer.""" + return Response(frame.render_source(), mimetype='text/html') + + def get_resource(self, request, filename): + """Return a static resource from the shared folder.""" + filename = join(dirname(__file__), 'shared', basename(filename)) + if isfile(filename): + mimetype = mimetypes.guess_type(filename)[0] \ + or 'application/octet-stream' + f = file(filename, 'rb') + try: + return Response(f.read(), mimetype=mimetype) + finally: + f.close() + return Response('Not Found', status=404) + + def __call__(self, environ, start_response): + """Dispatch the requests.""" + # important: don't ever access a function here that reads the incoming + # form data! Otherwise the application won't have access to that data + # any more! + request = Request(environ) + response = self.debug_application + if request.args.get('__debugger__') == 'yes': + cmd = request.args.get('cmd') + arg = request.args.get('f') + secret = request.args.get('s') + traceback = self.tracebacks.get(request.args.get('tb', type=int)) + frame = self.frames.get(request.args.get('frm', type=int)) + if cmd == 'resource' and arg: + response = self.get_resource(request, arg) + elif cmd == 'paste' and traceback is not None and \ + secret == self.secret: + response = self.paste_traceback(request, traceback) + elif cmd == 'source' and frame and self.secret == secret: + response = self.get_source(request, frame) + elif self.evalex and cmd is not None and frame is not None and \ + self.secret == secret: + response = self.execute_command(request, cmd, frame) + elif self.evalex and self.console_path is not None and \ + request.path == self.console_path: + response = self.display_console(request) + return response(environ, start_response) diff --git a/websdk/werkzeug/debug/console.py b/websdk/werkzeug/debug/console.py new file mode 100644 index 0000000..aace8c6 --- /dev/null +++ b/websdk/werkzeug/debug/console.py @@ -0,0 +1,204 @@ +# -*- coding: utf-8 -*- +""" + werkzeug.debug.console + ~~~~~~~~~~~~~~~~~~~~~~ + + Interactive console support. + + :copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details. + :license: BSD. +""" +import sys +import code +from types import CodeType +from werkzeug.utils import escape +from werkzeug.local import Local +from werkzeug.debug.repr import debug_repr, dump, helper + + +_local = Local() + + +class HTMLStringO(object): + """A StringO version that HTML escapes on write.""" + + def __init__(self): + self._buffer = [] + + def isatty(self): + return False + + def close(self): + pass + + def flush(self): + pass + + def seek(self, n, mode=0): + pass + + def readline(self): + if len(self._buffer) == 0: + return '' + ret = self._buffer[0] + del self._buffer[0] + return ret + + def reset(self): + val = ''.join(self._buffer) + del self._buffer[:] + return val + + def _write(self, x): + if isinstance(x, str): + x = x.decode('utf-8', 'replace') + self._buffer.append(x) + + def write(self, x): + self._write(escape(x)) + + def writelines(self, x): + self._write(escape(''.join(x))) + + +class ThreadedStream(object): + """Thread-local wrapper for sys.stdout for the interactive console.""" + + def push(): + if not isinstance(sys.stdout, ThreadedStream): + sys.stdout = ThreadedStream() + _local.stream = HTMLStringO() + push = staticmethod(push) + + def fetch(): + try: + stream = _local.stream + except AttributeError: + return '' + return stream.reset() + fetch = staticmethod(fetch) + + def displayhook(obj): + try: + stream = _local.stream + except AttributeError: + return _displayhook(obj) + # stream._write bypasses escaping as debug_repr is + # already generating HTML for us. + if obj is not None: + stream._write(debug_repr(obj)) + displayhook = staticmethod(displayhook) + + def __setattr__(self, name, value): + raise AttributeError('read only attribute %s' % name) + + def __dir__(self): + return dir(sys.__stdout__) + + def __getattribute__(self, name): + if name == '__members__': + return dir(sys.__stdout__) + try: + stream = _local.stream + except AttributeError: + stream = sys.__stdout__ + return getattr(stream, name) + + def __repr__(self): + return repr(sys.__stdout__) + + +# add the threaded stream as display hook +_displayhook = sys.displayhook +sys.displayhook = ThreadedStream.displayhook + + +class _ConsoleLoader(object): + + def __init__(self): + self._storage = {} + + def register(self, code, source): + self._storage[id(code)] = source + # register code objects of wrapped functions too. + for var in code.co_consts: + if isinstance(var, CodeType): + self._storage[id(var)] = source + + def get_source_by_code(self, code): + try: + return self._storage[id(code)] + except KeyError: + pass + + +def _wrap_compiler(console): + compile = console.compile + def func(source, filename, symbol): + code = compile(source, filename, symbol) + console.loader.register(code, source) + return code + console.compile = func + + +class _InteractiveConsole(code.InteractiveInterpreter): + + def __init__(self, globals, locals): + code.InteractiveInterpreter.__init__(self, locals) + self.globals = dict(globals) + self.globals['dump'] = dump + self.globals['help'] = helper + self.globals['__loader__'] = self.loader = _ConsoleLoader() + self.more = False + self.buffer = [] + _wrap_compiler(self) + + def runsource(self, source): + source = source.rstrip() + '\n' + ThreadedStream.push() + prompt = self.more and '... ' or '>>> ' + try: + source_to_eval = ''.join(self.buffer + [source]) + if code.InteractiveInterpreter.runsource(self, + source_to_eval, '', 'single'): + self.more = True + self.buffer.append(source) + else: + self.more = False + del self.buffer[:] + finally: + output = ThreadedStream.fetch() + return prompt + source + output + + def runcode(self, code): + try: + exec code in self.globals, self.locals + except Exception: + self.showtraceback() + + def showtraceback(self): + from werkzeug.debug.tbtools import get_current_traceback + tb = get_current_traceback(skip=1) + sys.stdout._write(tb.render_summary()) + + def showsyntaxerror(self, filename=None): + from werkzeug.debug.tbtools import get_current_traceback + tb = get_current_traceback(skip=4) + sys.stdout._write(tb.render_summary()) + + def write(self, data): + sys.stdout.write(data) + + +class Console(object): + """An interactive console.""" + + def __init__(self, globals=None, locals=None): + if locals is None: + locals = {} + if globals is None: + globals = {} + self._ipy = _InteractiveConsole(globals, locals) + + def eval(self, code): + return self._ipy.runsource(code) diff --git a/websdk/werkzeug/debug/repr.py b/websdk/werkzeug/debug/repr.py new file mode 100644 index 0000000..a6a54a9 --- /dev/null +++ b/websdk/werkzeug/debug/repr.py @@ -0,0 +1,267 @@ +# -*- coding: utf-8 -*- +""" + werkzeug.debug.repr + ~~~~~~~~~~~~~~~~~~~ + + This module implements object representations for debugging purposes. + Unlike the default repr these reprs expose a lot more information and + produce HTML instead of ASCII. + + Together with the CSS and JavaScript files of the debugger this gives + a colorful and more compact output. + + :copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details. + :license: BSD. +""" +import sys +import re +from traceback import format_exception_only +try: + from collections import deque +except ImportError: # pragma: no cover + deque = None +from werkzeug.utils import escape + + +missing = object() +_paragraph_re = re.compile(r'(?:\r\n|\r|\n){2,}') +RegexType = type(_paragraph_re) + + +HELP_HTML = '''\ +
    +

    %(title)s

    +
    %(text)s
    +
    \ +''' +OBJECT_DUMP_HTML = '''\ +
    +

    %(title)s

    + %(repr)s + %(items)s
    +
    \ +''' + + +def debug_repr(obj): + """Creates a debug repr of an object as HTML unicode string.""" + return DebugReprGenerator().repr(obj) + + +def dump(obj=missing): + """Print the object details to stdout._write (for the interactive + console of the web debugger. + """ + gen = DebugReprGenerator() + if obj is missing: + rv = gen.dump_locals(sys._getframe(1).f_locals) + else: + rv = gen.dump_object(obj) + sys.stdout._write(rv) + + +class _Helper(object): + """Displays an HTML version of the normal help, for the interactive + debugger only because it requires a patched sys.stdout. + """ + + def __repr__(self): + return 'Type help(object) for help about object.' + + def __call__(self, topic=None): + if topic is None: + sys.stdout._write('%s' % repr(self)) + return + import pydoc + pydoc.help(topic) + rv = sys.stdout.reset().decode('utf-8', 'ignore') + paragraphs = _paragraph_re.split(rv) + if len(paragraphs) > 1: + title = paragraphs[0] + text = '\n\n'.join(paragraphs[1:]) + else: # pragma: no cover + title = 'Help' + text = paragraphs[0] + sys.stdout._write(HELP_HTML % {'title': title, 'text': text}) + + +helper = _Helper() + + +def _add_subclass_info(inner, obj, base): + if isinstance(base, tuple): + for base in base: + if type(obj) is base: + return inner + elif type(obj) is base: + return inner + module = '' + if obj.__class__.__module__ not in ('__builtin__', 'exceptions'): + module = '%s.' % obj.__class__.__module__ + return '%s%s(%s)' % (module, obj.__class__.__name__, inner) + + +class DebugReprGenerator(object): + + def __init__(self): + self._stack = [] + + def _sequence_repr_maker(left, right, base=object(), limit=8): + def proxy(self, obj, recursive): + if recursive: + return _add_subclass_info(left + '...' + right, obj, base) + buf = [left] + have_extended_section = False + for idx, item in enumerate(obj): + if idx: + buf.append(', ') + if idx == limit: + buf.append('') + have_extended_section = True + buf.append(self.repr(item)) + if have_extended_section: + buf.append('') + buf.append(right) + return _add_subclass_info(u''.join(buf), obj, base) + return proxy + + list_repr = _sequence_repr_maker('[', ']', list) + tuple_repr = _sequence_repr_maker('(', ')', tuple) + set_repr = _sequence_repr_maker('set([', '])', set) + frozenset_repr = _sequence_repr_maker('frozenset([', '])', frozenset) + if deque is not None: + deque_repr = _sequence_repr_maker('collections.' + 'deque([', '])', deque) + del _sequence_repr_maker + + def regex_repr(self, obj): + pattern = repr(obj.pattern).decode('string-escape', 'ignore') + if pattern[:1] == 'u': + pattern = 'ur' + pattern[1:] + else: + pattern = 'r' + pattern + return u're.compile(%s)' % pattern + + def string_repr(self, obj, limit=70): + buf = [''] + escaped = escape(obj) + a = repr(escaped[:limit]) + b = repr(escaped[limit:]) + if isinstance(obj, unicode): + buf.append('u') + a = a[1:] + b = b[1:] + if b != "''": + buf.extend((a[:-1], '', b[1:], '')) + else: + buf.append(a) + buf.append('') + return _add_subclass_info(u''.join(buf), obj, (str, unicode)) + + def dict_repr(self, d, recursive, limit=5): + if recursive: + return _add_subclass_info(u'{...}', d, dict) + buf = ['{'] + have_extended_section = False + for idx, (key, value) in enumerate(d.iteritems()): + if idx: + buf.append(', ') + if idx == limit - 1: + buf.append('') + have_extended_section = True + buf.append('%s: ' + '%s' % + (self.repr(key), self.repr(value))) + if have_extended_section: + buf.append('') + buf.append('}') + return _add_subclass_info(u''.join(buf), d, dict) + + def object_repr(self, obj): + return u'%s' % \ + escape(repr(obj).decode('utf-8', 'replace')) + + def dispatch_repr(self, obj, recursive): + if obj is helper: + return u'%r' % helper + if isinstance(obj, (int, long, float, complex)): + return u'%r' % obj + if isinstance(obj, basestring): + return self.string_repr(obj) + if isinstance(obj, RegexType): + return self.regex_repr(obj) + if isinstance(obj, list): + return self.list_repr(obj, recursive) + if isinstance(obj, tuple): + return self.tuple_repr(obj, recursive) + if isinstance(obj, set): + return self.set_repr(obj, recursive) + if isinstance(obj, frozenset): + return self.frozenset_repr(obj, recursive) + if isinstance(obj, dict): + return self.dict_repr(obj, recursive) + if deque is not None and isinstance(obj, deque): + return self.deque_repr(obj, recursive) + return self.object_repr(obj) + + def fallback_repr(self): + try: + info = ''.join(format_exception_only(*sys.exc_info()[:2])) + except Exception: # pragma: no cover + info = '?' + return u'<broken repr (%s)>' \ + u'' % escape(info.decode('utf-8', 'ignore').strip()) + + def repr(self, obj): + recursive = False + for item in self._stack: + if item is obj: + recursive = True + break + self._stack.append(obj) + try: + try: + return self.dispatch_repr(obj, recursive) + except Exception: + return self.fallback_repr() + finally: + self._stack.pop() + + def dump_object(self, obj): + repr = items = None + if isinstance(obj, dict): + title = 'Contents of' + items = [] + for key, value in obj.iteritems(): + if not isinstance(key, basestring): + items = None + break + items.append((key, self.repr(value))) + if items is None: + items = [] + repr = self.repr(obj) + for key in dir(obj): + try: + items.append((key, self.repr(getattr(obj, key)))) + except Exception: + pass + title = 'Details for' + title += ' ' + object.__repr__(obj)[1:-1] + return self.render_object_dump(items, title, repr) + + def dump_locals(self, d): + items = [(key, self.repr(value)) for key, value in d.items()] + return self.render_object_dump(items, 'Local variables in frame') + + def render_object_dump(self, items, title, repr=None): + html_items = [] + for key, value in items: + html_items.append('%s
    %s
    ' % + (escape(key), value)) + if not html_items: + html_items.append('Nothing') + return OBJECT_DUMP_HTML % { + 'title': escape(title), + 'repr': repr and '
    %s
    ' % repr or '', + 'items': '\n'.join(html_items) + } diff --git a/websdk/werkzeug/debug/shared/FONT_LICENSE b/websdk/werkzeug/debug/shared/FONT_LICENSE new file mode 100644 index 0000000..ae78a8f --- /dev/null +++ b/websdk/werkzeug/debug/shared/FONT_LICENSE @@ -0,0 +1,96 @@ +------------------------------- +UBUNTU FONT LICENCE Version 1.0 +------------------------------- + +PREAMBLE +This licence allows the licensed fonts to be used, studied, modified and +redistributed freely. The fonts, including any derivative works, can be +bundled, embedded, and redistributed provided the terms of this licence +are met. The fonts and derivatives, however, cannot be released under +any other licence. The requirement for fonts to remain under this +licence does not require any document created using the fonts or their +derivatives to be published under this licence, as long as the primary +purpose of the document is not to be a vehicle for the distribution of +the fonts. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this licence and clearly marked as such. This may +include source files, build scripts and documentation. + +"Original Version" refers to the collection of Font Software components +as received under this licence. + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to +a new environment. + +"Copyright Holder(s)" refers to all individuals and companies who have a +copyright ownership of the Font Software. + +"Substantially Changed" refers to Modified Versions which can be easily +identified as dissimilar to the Font Software by users of the Font +Software comparing the Original Version with the Modified Version. + +To "Propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification and with or without charging +a redistribution fee), making available to the public, and in some +countries other activities as well. + +PERMISSION & CONDITIONS +This licence does not grant any rights under trademark law and all such +rights are reserved. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of the Font Software, to propagate the Font Software, subject to +the below conditions: + +1) Each copy of the Font Software must contain the above copyright +notice and this licence. These can be included either as stand-alone +text files, human-readable headers or in the appropriate machine- +readable metadata fields within text or binary files as long as those +fields can be easily viewed by the user. + +2) The font name complies with the following: +(a) The Original Version must retain its name, unmodified. +(b) Modified Versions which are Substantially Changed must be renamed to +avoid use of the name of the Original Version or similar names entirely. +(c) Modified Versions which are not Substantially Changed must be +renamed to both (i) retain the name of the Original Version and (ii) add +additional naming elements to distinguish the Modified Version from the +Original Version. The name of such Modified Versions must be the name of +the Original Version, with "derivative X" where X represents the name of +the new work, appended to that name. + +3) The name(s) of the Copyright Holder(s) and any contributor to the +Font Software shall not be used to promote, endorse or advertise any +Modified Version, except (i) as required by this licence, (ii) to +acknowledge the contribution(s) of the Copyright Holder(s) or (iii) with +their explicit written permission. + +4) The Font Software, modified or unmodified, in part or in whole, must +be distributed entirely under this licence, and must not be distributed +under any other licence. The requirement for fonts to remain under this +licence does not affect any document created using the Font Software, +except any version of the Font Software extracted from a document +created using the Font Software may only be distributed under this +licence. + +TERMINATION +This licence becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF +COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER +DEALINGS IN THE FONT SOFTWARE. diff --git a/websdk/werkzeug/debug/shared/console.png b/websdk/werkzeug/debug/shared/console.png new file mode 100755 index 0000000..c28dd63 --- /dev/null +++ b/websdk/werkzeug/debug/shared/console.png Binary files differ diff --git a/websdk/werkzeug/debug/shared/debugger.js b/websdk/werkzeug/debug/shared/debugger.js new file mode 100644 index 0000000..6468cba --- /dev/null +++ b/websdk/werkzeug/debug/shared/debugger.js @@ -0,0 +1,200 @@ +$(function() { + var sourceView = null; + + /** + * if we are in console mode, show the console. + */ + if (CONSOLE_MODE && EVALEX) { + openShell(null, $('div.console div.inner').empty(), 0); + } + + $('div.traceback div.frame').each(function() { + var + target = $('pre', this) + .click(function() { + sourceButton.click(); + }), + consoleNode = null, source = null, + frameID = this.id.substring(6); + + /** + * Add an interactive console to the frames + */ + if (EVALEX) + $('') + .attr('title', 'Open an interactive python shell in this frame') + .click(function() { + consoleNode = openShell(consoleNode, target, frameID); + return false; + }) + .prependTo(target); + + /** + * Show sourcecode + */ + var sourceButton = $('') + .attr('title', 'Display the sourcecode for this frame') + .click(function() { + if (!sourceView) + $('h2', sourceView = + $('

    View Source

    ' + + '
    ') + .insertBefore('div.explanation')) + .css('cursor', 'pointer') + .click(function() { + sourceView.slideUp('fast'); + }); + $.get(document.location.pathname, {__debugger__: 'yes', cmd: + 'source', frm: frameID, s: SECRET}, function(data) { + $('table', sourceView) + .replaceWith(data); + if (!sourceView.is(':visible')) + sourceView.slideDown('fast', function() { + focusSourceBlock(); + }); + else + focusSourceBlock(); + }); + return false; + }) + .prependTo(target); + }); + + /** + * toggle traceback types on click. + */ + $('h2.traceback').click(function() { + $(this).next().slideToggle('fast'); + $('div.plain').slideToggle('fast'); + }).css('cursor', 'pointer'); + $('div.plain').hide(); + + /** + * Add extra info (this is here so that only users with JavaScript + * enabled see it.) + */ + $('span.nojavascript') + .removeClass('nojavascript') + .html('

    To switch between the interactive traceback and the plaintext ' + + 'one, you can click on the "Traceback" headline. From the text ' + + 'traceback you can also create a paste of it. ' + (!EVALEX ? '' : + 'For code execution mouse-over the frame you want to debug and ' + + 'click on the console icon on the right side.' + + '

    You can execute arbitrary Python code in the stack frames and ' + + 'there are some extra helpers available for introspection:' + + '

    • dump() shows all variables in the frame' + + '
    • dump(obj) dumps all that\'s known about the object
    ')); + + /** + * Add the pastebin feature + */ + $('div.plain form') + .submit(function() { + var label = $('input[type="submit"]', this); + var old_val = label.val(); + label.val('submitting...'); + $.ajax({ + dataType: 'json', + url: document.location.pathname, + data: {__debugger__: 'yes', tb: TRACEBACK, cmd: 'paste', + s: SECRET}, + success: function(data) { + $('div.plain span.pastemessage') + .removeClass('pastemessage') + .text('Paste created: ') + .append($('#' + data.id + '').attr('href', data.url)); + }, + error: function() { + alert('Error: Could not submit paste. No network connection?'); + label.val(old_val); + } + }); + return false; + }); + + // if we have javascript we submit by ajax anyways, so no need for the + // not scaling textarea. + var plainTraceback = $('div.plain textarea'); + plainTraceback.replaceWith($('
    ').text(plainTraceback.text()));
    +});
    +
    +
    +/**
    + * Helper function for shell initialization
    + */
    +function openShell(consoleNode, target, frameID) {
    +  if (consoleNode)
    +    return consoleNode.slideToggle('fast');
    +  consoleNode = $('
    ')
    +    .appendTo(target.parent())
    +    .hide()
    +  var historyPos = 0, history = [''];
    +  var output = $('
    [console ready]
    ') + .appendTo(consoleNode); + var form = $('
    >>>
    ') + .submit(function() { + var cmd = command.val(); + $.get(document.location.pathname, { + __debugger__: 'yes', cmd: cmd, frm: frameID, s: SECRET}, function(data) { + var tmp = $('
    ').html(data); + $('span.extended', tmp).each(function() { + var hidden = $(this).wrap('').hide(); + hidden + .parent() + .append($('  ') + .click(function() { + hidden.toggle(); + $(this).toggleClass('open') + return false; + })); + }); + output.append(tmp); + command.focus(); + consoleNode.scrollTop(command.position().top); + var old = history.pop(); + history.push(cmd); + if (typeof old != 'undefined') + history.push(old); + historyPos = history.length - 1; + }); + command.val(''); + return false; + }). + appendTo(consoleNode); + + var command = $('') + .appendTo(form) + .keydown(function(e) { + if (e.charCode == 100 && e.ctrlKey) { + output.text('--- screen cleared ---'); + return false; + } + else if (e.charCode == 0 && (e.keyCode == 38 || e.keyCode == 40)) { + if (e.keyCode == 38 && historyPos > 0) + historyPos--; + else if (e.keyCode == 40 && historyPos < history.length) + historyPos++; + command.val(history[historyPos]); + return false; + } + }); + + return consoleNode.slideDown('fast', function() { + command.focus(); + }); +} + +/** + * Focus the current block in the source view. + */ +function focusSourceBlock() { + var tmp, line = $('table.source tr.current'); + for (var i = 0; i < 7; i++) { + tmp = line.prev(); + if (!(tmp && tmp.is('.in-frame'))) + break + line = tmp; + } + var container = $('div.sourceview'); + container.scrollTop(line.offset().top); +} diff --git a/websdk/werkzeug/debug/shared/jquery.js b/websdk/werkzeug/debug/shared/jquery.js new file mode 100644 index 0000000..8f3ca2e --- /dev/null +++ b/websdk/werkzeug/debug/shared/jquery.js @@ -0,0 +1,167 @@ +/*! + * jQuery JavaScript Library v1.4.4 + * http://jquery.com/ + * + * Copyright 2010, John Resig + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * Includes Sizzle.js + * http://sizzlejs.com/ + * Copyright 2010, The Dojo Foundation + * Released under the MIT, BSD, and GPL Licenses. + * + * Date: Thu Nov 11 19:04:53 2010 -0500 + */ +(function(E,B){function ka(a,b,d){if(d===B&&a.nodeType===1){d=a.getAttribute("data-"+b);if(typeof d==="string"){try{d=d==="true"?true:d==="false"?false:d==="null"?null:!c.isNaN(d)?parseFloat(d):Ja.test(d)?c.parseJSON(d):d}catch(e){}c.data(a,b,d)}else d=B}return d}function U(){return false}function ca(){return true}function la(a,b,d){d[0].type=a;return c.event.handle.apply(b,d)}function Ka(a){var b,d,e,f,h,l,k,o,x,r,A,C=[];f=[];h=c.data(this,this.nodeType?"events":"__events__");if(typeof h==="function")h= +h.events;if(!(a.liveFired===this||!h||!h.live||a.button&&a.type==="click")){if(a.namespace)A=RegExp("(^|\\.)"+a.namespace.split(".").join("\\.(?:.*\\.)?")+"(\\.|$)");a.liveFired=this;var J=h.live.slice(0);for(k=0;kd)break;a.currentTarget=f.elem;a.data=f.handleObj.data;a.handleObj=f.handleObj;A=f.handleObj.origHandler.apply(f.elem,arguments);if(A===false||a.isPropagationStopped()){d=f.level;if(A===false)b=false;if(a.isImmediatePropagationStopped())break}}return b}}function Y(a,b){return(a&&a!=="*"?a+".":"")+b.replace(La, +"`").replace(Ma,"&")}function ma(a,b,d){if(c.isFunction(b))return c.grep(a,function(f,h){return!!b.call(f,h,f)===d});else if(b.nodeType)return c.grep(a,function(f){return f===b===d});else if(typeof b==="string"){var e=c.grep(a,function(f){return f.nodeType===1});if(Na.test(b))return c.filter(b,e,!d);else b=c.filter(b,e)}return c.grep(a,function(f){return c.inArray(f,b)>=0===d})}function na(a,b){var d=0;b.each(function(){if(this.nodeName===(a[d]&&a[d].nodeName)){var e=c.data(a[d++]),f=c.data(this, +e);if(e=e&&e.events){delete f.handle;f.events={};for(var h in e)for(var l in e[h])c.event.add(this,h,e[h][l],e[h][l].data)}}})}function Oa(a,b){b.src?c.ajax({url:b.src,async:false,dataType:"script"}):c.globalEval(b.text||b.textContent||b.innerHTML||"");b.parentNode&&b.parentNode.removeChild(b)}function oa(a,b,d){var e=b==="width"?a.offsetWidth:a.offsetHeight;if(d==="border")return e;c.each(b==="width"?Pa:Qa,function(){d||(e-=parseFloat(c.css(a,"padding"+this))||0);if(d==="margin")e+=parseFloat(c.css(a, +"margin"+this))||0;else e-=parseFloat(c.css(a,"border"+this+"Width"))||0});return e}function da(a,b,d,e){if(c.isArray(b)&&b.length)c.each(b,function(f,h){d||Ra.test(a)?e(a,h):da(a+"["+(typeof h==="object"||c.isArray(h)?f:"")+"]",h,d,e)});else if(!d&&b!=null&&typeof b==="object")c.isEmptyObject(b)?e(a,""):c.each(b,function(f,h){da(a+"["+f+"]",h,d,e)});else e(a,b)}function S(a,b){var d={};c.each(pa.concat.apply([],pa.slice(0,b)),function(){d[this]=a});return d}function qa(a){if(!ea[a]){var b=c("<"+ +a+">").appendTo("body"),d=b.css("display");b.remove();if(d==="none"||d==="")d="block";ea[a]=d}return ea[a]}function fa(a){return c.isWindow(a)?a:a.nodeType===9?a.defaultView||a.parentWindow:false}var t=E.document,c=function(){function a(){if(!b.isReady){try{t.documentElement.doScroll("left")}catch(j){setTimeout(a,1);return}b.ready()}}var b=function(j,s){return new b.fn.init(j,s)},d=E.jQuery,e=E.$,f,h=/^(?:[^<]*(<[\w\W]+>)[^>]*$|#([\w\-]+)$)/,l=/\S/,k=/^\s+/,o=/\s+$/,x=/\W/,r=/\d/,A=/^<(\w+)\s*\/?>(?:<\/\1>)?$/, +C=/^[\],:{}\s]*$/,J=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,w=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,I=/(?:^|:|,)(?:\s*\[)+/g,L=/(webkit)[ \/]([\w.]+)/,g=/(opera)(?:.*version)?[ \/]([\w.]+)/,i=/(msie) ([\w.]+)/,n=/(mozilla)(?:.*? rv:([\w.]+))?/,m=navigator.userAgent,p=false,q=[],u,y=Object.prototype.toString,F=Object.prototype.hasOwnProperty,M=Array.prototype.push,N=Array.prototype.slice,O=String.prototype.trim,D=Array.prototype.indexOf,R={};b.fn=b.prototype={init:function(j, +s){var v,z,H;if(!j)return this;if(j.nodeType){this.context=this[0]=j;this.length=1;return this}if(j==="body"&&!s&&t.body){this.context=t;this[0]=t.body;this.selector="body";this.length=1;return this}if(typeof j==="string")if((v=h.exec(j))&&(v[1]||!s))if(v[1]){H=s?s.ownerDocument||s:t;if(z=A.exec(j))if(b.isPlainObject(s)){j=[t.createElement(z[1])];b.fn.attr.call(j,s,true)}else j=[H.createElement(z[1])];else{z=b.buildFragment([v[1]],[H]);j=(z.cacheable?z.fragment.cloneNode(true):z.fragment).childNodes}return b.merge(this, +j)}else{if((z=t.getElementById(v[2]))&&z.parentNode){if(z.id!==v[2])return f.find(j);this.length=1;this[0]=z}this.context=t;this.selector=j;return this}else if(!s&&!x.test(j)){this.selector=j;this.context=t;j=t.getElementsByTagName(j);return b.merge(this,j)}else return!s||s.jquery?(s||f).find(j):b(s).find(j);else if(b.isFunction(j))return f.ready(j);if(j.selector!==B){this.selector=j.selector;this.context=j.context}return b.makeArray(j,this)},selector:"",jquery:"1.4.4",length:0,size:function(){return this.length}, +toArray:function(){return N.call(this,0)},get:function(j){return j==null?this.toArray():j<0?this.slice(j)[0]:this[j]},pushStack:function(j,s,v){var z=b();b.isArray(j)?M.apply(z,j):b.merge(z,j);z.prevObject=this;z.context=this.context;if(s==="find")z.selector=this.selector+(this.selector?" ":"")+v;else if(s)z.selector=this.selector+"."+s+"("+v+")";return z},each:function(j,s){return b.each(this,j,s)},ready:function(j){b.bindReady();if(b.isReady)j.call(t,b);else q&&q.push(j);return this},eq:function(j){return j=== +-1?this.slice(j):this.slice(j,+j+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(N.apply(this,arguments),"slice",N.call(arguments).join(","))},map:function(j){return this.pushStack(b.map(this,function(s,v){return j.call(s,v,s)}))},end:function(){return this.prevObject||b(null)},push:M,sort:[].sort,splice:[].splice};b.fn.init.prototype=b.fn;b.extend=b.fn.extend=function(){var j,s,v,z,H,G=arguments[0]||{},K=1,Q=arguments.length,ga=false; +if(typeof G==="boolean"){ga=G;G=arguments[1]||{};K=2}if(typeof G!=="object"&&!b.isFunction(G))G={};if(Q===K){G=this;--K}for(;K0))if(q){var s=0,v=q;for(q=null;j=v[s++];)j.call(t,b);b.fn.trigger&&b(t).trigger("ready").unbind("ready")}}},bindReady:function(){if(!p){p=true;if(t.readyState==="complete")return setTimeout(b.ready,1);if(t.addEventListener){t.addEventListener("DOMContentLoaded",u,false);E.addEventListener("load",b.ready,false)}else if(t.attachEvent){t.attachEvent("onreadystatechange",u);E.attachEvent("onload", +b.ready);var j=false;try{j=E.frameElement==null}catch(s){}t.documentElement.doScroll&&j&&a()}}},isFunction:function(j){return b.type(j)==="function"},isArray:Array.isArray||function(j){return b.type(j)==="array"},isWindow:function(j){return j&&typeof j==="object"&&"setInterval"in j},isNaN:function(j){return j==null||!r.test(j)||isNaN(j)},type:function(j){return j==null?String(j):R[y.call(j)]||"object"},isPlainObject:function(j){if(!j||b.type(j)!=="object"||j.nodeType||b.isWindow(j))return false;if(j.constructor&& +!F.call(j,"constructor")&&!F.call(j.constructor.prototype,"isPrototypeOf"))return false;for(var s in j);return s===B||F.call(j,s)},isEmptyObject:function(j){for(var s in j)return false;return true},error:function(j){throw j;},parseJSON:function(j){if(typeof j!=="string"||!j)return null;j=b.trim(j);if(C.test(j.replace(J,"@").replace(w,"]").replace(I,"")))return E.JSON&&E.JSON.parse?E.JSON.parse(j):(new Function("return "+j))();else b.error("Invalid JSON: "+j)},noop:function(){},globalEval:function(j){if(j&& +l.test(j)){var s=t.getElementsByTagName("head")[0]||t.documentElement,v=t.createElement("script");v.type="text/javascript";if(b.support.scriptEval)v.appendChild(t.createTextNode(j));else v.text=j;s.insertBefore(v,s.firstChild);s.removeChild(v)}},nodeName:function(j,s){return j.nodeName&&j.nodeName.toUpperCase()===s.toUpperCase()},each:function(j,s,v){var z,H=0,G=j.length,K=G===B||b.isFunction(j);if(v)if(K)for(z in j){if(s.apply(j[z],v)===false)break}else for(;H
    a";var f=d.getElementsByTagName("*"),h=d.getElementsByTagName("a")[0],l=t.createElement("select"), +k=l.appendChild(t.createElement("option"));if(!(!f||!f.length||!h)){c.support={leadingWhitespace:d.firstChild.nodeType===3,tbody:!d.getElementsByTagName("tbody").length,htmlSerialize:!!d.getElementsByTagName("link").length,style:/red/.test(h.getAttribute("style")),hrefNormalized:h.getAttribute("href")==="/a",opacity:/^0.55$/.test(h.style.opacity),cssFloat:!!h.style.cssFloat,checkOn:d.getElementsByTagName("input")[0].value==="on",optSelected:k.selected,deleteExpando:true,optDisabled:false,checkClone:false, +scriptEval:false,noCloneEvent:true,boxModel:null,inlineBlockNeedsLayout:false,shrinkWrapBlocks:false,reliableHiddenOffsets:true};l.disabled=true;c.support.optDisabled=!k.disabled;b.type="text/javascript";try{b.appendChild(t.createTextNode("window."+e+"=1;"))}catch(o){}a.insertBefore(b,a.firstChild);if(E[e]){c.support.scriptEval=true;delete E[e]}try{delete b.test}catch(x){c.support.deleteExpando=false}a.removeChild(b);if(d.attachEvent&&d.fireEvent){d.attachEvent("onclick",function r(){c.support.noCloneEvent= +false;d.detachEvent("onclick",r)});d.cloneNode(true).fireEvent("onclick")}d=t.createElement("div");d.innerHTML="";a=t.createDocumentFragment();a.appendChild(d.firstChild);c.support.checkClone=a.cloneNode(true).cloneNode(true).lastChild.checked;c(function(){var r=t.createElement("div");r.style.width=r.style.paddingLeft="1px";t.body.appendChild(r);c.boxModel=c.support.boxModel=r.offsetWidth===2;if("zoom"in r.style){r.style.display="inline";r.style.zoom= +1;c.support.inlineBlockNeedsLayout=r.offsetWidth===2;r.style.display="";r.innerHTML="
    ";c.support.shrinkWrapBlocks=r.offsetWidth!==2}r.innerHTML="
    t
    ";var A=r.getElementsByTagName("td");c.support.reliableHiddenOffsets=A[0].offsetHeight===0;A[0].style.display="";A[1].style.display="none";c.support.reliableHiddenOffsets=c.support.reliableHiddenOffsets&&A[0].offsetHeight===0;r.innerHTML="";t.body.removeChild(r).style.display= +"none"});a=function(r){var A=t.createElement("div");r="on"+r;var C=r in A;if(!C){A.setAttribute(r,"return;");C=typeof A[r]==="function"}return C};c.support.submitBubbles=a("submit");c.support.changeBubbles=a("change");a=b=d=f=h=null}})();var ra={},Ja=/^(?:\{.*\}|\[.*\])$/;c.extend({cache:{},uuid:0,expando:"jQuery"+c.now(),noData:{embed:true,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:true},data:function(a,b,d){if(c.acceptData(a)){a=a==E?ra:a;var e=a.nodeType,f=e?a[c.expando]:null,h= +c.cache;if(!(e&&!f&&typeof b==="string"&&d===B)){if(e)f||(a[c.expando]=f=++c.uuid);else h=a;if(typeof b==="object")if(e)h[f]=c.extend(h[f],b);else c.extend(h,b);else if(e&&!h[f])h[f]={};a=e?h[f]:h;if(d!==B)a[b]=d;return typeof b==="string"?a[b]:a}}},removeData:function(a,b){if(c.acceptData(a)){a=a==E?ra:a;var d=a.nodeType,e=d?a[c.expando]:a,f=c.cache,h=d?f[e]:e;if(b){if(h){delete h[b];d&&c.isEmptyObject(h)&&c.removeData(a)}}else if(d&&c.support.deleteExpando)delete a[c.expando];else if(a.removeAttribute)a.removeAttribute(c.expando); +else if(d)delete f[e];else for(var l in a)delete a[l]}},acceptData:function(a){if(a.nodeName){var b=c.noData[a.nodeName.toLowerCase()];if(b)return!(b===true||a.getAttribute("classid")!==b)}return true}});c.fn.extend({data:function(a,b){var d=null;if(typeof a==="undefined"){if(this.length){var e=this[0].attributes,f;d=c.data(this[0]);for(var h=0,l=e.length;h-1)return true;return false},val:function(a){if(!arguments.length){var b=this[0];if(b){if(c.nodeName(b,"option")){var d=b.attributes.value;return!d||d.specified?b.value:b.text}if(c.nodeName(b,"select")){var e=b.selectedIndex;d=[];var f=b.options;b=b.type==="select-one"; +if(e<0)return null;var h=b?e:0;for(e=b?e+1:f.length;h=0;else if(c.nodeName(this,"select")){var A=c.makeArray(r);c("option",this).each(function(){this.selected=c.inArray(c(this).val(),A)>=0});if(!A.length)this.selectedIndex=-1}else this.value=r}})}});c.extend({attrFn:{val:true,css:true,html:true,text:true,data:true,width:true,height:true,offset:true}, +attr:function(a,b,d,e){if(!a||a.nodeType===3||a.nodeType===8)return B;if(e&&b in c.attrFn)return c(a)[b](d);e=a.nodeType!==1||!c.isXMLDoc(a);var f=d!==B;b=e&&c.props[b]||b;var h=Ta.test(b);if((b in a||a[b]!==B)&&e&&!h){if(f){b==="type"&&Ua.test(a.nodeName)&&a.parentNode&&c.error("type property can't be changed");if(d===null)a.nodeType===1&&a.removeAttribute(b);else a[b]=d}if(c.nodeName(a,"form")&&a.getAttributeNode(b))return a.getAttributeNode(b).nodeValue;if(b==="tabIndex")return(b=a.getAttributeNode("tabIndex"))&& +b.specified?b.value:Va.test(a.nodeName)||Wa.test(a.nodeName)&&a.href?0:B;return a[b]}if(!c.support.style&&e&&b==="style"){if(f)a.style.cssText=""+d;return a.style.cssText}f&&a.setAttribute(b,""+d);if(!a.attributes[b]&&a.hasAttribute&&!a.hasAttribute(b))return B;a=!c.support.hrefNormalized&&e&&h?a.getAttribute(b,2):a.getAttribute(b);return a===null?B:a}});var X=/\.(.*)$/,ia=/^(?:textarea|input|select)$/i,La=/\./g,Ma=/ /g,Xa=/[^\w\s.|`]/g,Ya=function(a){return a.replace(Xa,"\\$&")},ua={focusin:0,focusout:0}; +c.event={add:function(a,b,d,e){if(!(a.nodeType===3||a.nodeType===8)){if(c.isWindow(a)&&a!==E&&!a.frameElement)a=E;if(d===false)d=U;else if(!d)return;var f,h;if(d.handler){f=d;d=f.handler}if(!d.guid)d.guid=c.guid++;if(h=c.data(a)){var l=a.nodeType?"events":"__events__",k=h[l],o=h.handle;if(typeof k==="function"){o=k.handle;k=k.events}else if(!k){a.nodeType||(h[l]=h=function(){});h.events=k={}}if(!o)h.handle=o=function(){return typeof c!=="undefined"&&!c.event.triggered?c.event.handle.apply(o.elem, +arguments):B};o.elem=a;b=b.split(" ");for(var x=0,r;l=b[x++];){h=f?c.extend({},f):{handler:d,data:e};if(l.indexOf(".")>-1){r=l.split(".");l=r.shift();h.namespace=r.slice(0).sort().join(".")}else{r=[];h.namespace=""}h.type=l;if(!h.guid)h.guid=d.guid;var A=k[l],C=c.event.special[l]||{};if(!A){A=k[l]=[];if(!C.setup||C.setup.call(a,e,r,o)===false)if(a.addEventListener)a.addEventListener(l,o,false);else a.attachEvent&&a.attachEvent("on"+l,o)}if(C.add){C.add.call(a,h);if(!h.handler.guid)h.handler.guid= +d.guid}A.push(h);c.event.global[l]=true}a=null}}},global:{},remove:function(a,b,d,e){if(!(a.nodeType===3||a.nodeType===8)){if(d===false)d=U;var f,h,l=0,k,o,x,r,A,C,J=a.nodeType?"events":"__events__",w=c.data(a),I=w&&w[J];if(w&&I){if(typeof I==="function"){w=I;I=I.events}if(b&&b.type){d=b.handler;b=b.type}if(!b||typeof b==="string"&&b.charAt(0)==="."){b=b||"";for(f in I)c.event.remove(a,f+b)}else{for(b=b.split(" ");f=b[l++];){r=f;k=f.indexOf(".")<0;o=[];if(!k){o=f.split(".");f=o.shift();x=RegExp("(^|\\.)"+ +c.map(o.slice(0).sort(),Ya).join("\\.(?:.*\\.)?")+"(\\.|$)")}if(A=I[f])if(d){r=c.event.special[f]||{};for(h=e||0;h=0){a.type=f=f.slice(0,-1);a.exclusive=true}if(!d){a.stopPropagation();c.event.global[f]&&c.each(c.cache,function(){this.events&&this.events[f]&&c.event.trigger(a,b,this.handle.elem)})}if(!d||d.nodeType===3||d.nodeType=== +8)return B;a.result=B;a.target=d;b=c.makeArray(b);b.unshift(a)}a.currentTarget=d;(e=d.nodeType?c.data(d,"handle"):(c.data(d,"__events__")||{}).handle)&&e.apply(d,b);e=d.parentNode||d.ownerDocument;try{if(!(d&&d.nodeName&&c.noData[d.nodeName.toLowerCase()]))if(d["on"+f]&&d["on"+f].apply(d,b)===false){a.result=false;a.preventDefault()}}catch(h){}if(!a.isPropagationStopped()&&e)c.event.trigger(a,b,e,true);else if(!a.isDefaultPrevented()){var l;e=a.target;var k=f.replace(X,""),o=c.nodeName(e,"a")&&k=== +"click",x=c.event.special[k]||{};if((!x._default||x._default.call(d,a)===false)&&!o&&!(e&&e.nodeName&&c.noData[e.nodeName.toLowerCase()])){try{if(e[k]){if(l=e["on"+k])e["on"+k]=null;c.event.triggered=true;e[k]()}}catch(r){}if(l)e["on"+k]=l;c.event.triggered=false}}},handle:function(a){var b,d,e,f;d=[];var h=c.makeArray(arguments);a=h[0]=c.event.fix(a||E.event);a.currentTarget=this;b=a.type.indexOf(".")<0&&!a.exclusive;if(!b){e=a.type.split(".");a.type=e.shift();d=e.slice(0).sort();e=RegExp("(^|\\.)"+ +d.join("\\.(?:.*\\.)?")+"(\\.|$)")}a.namespace=a.namespace||d.join(".");f=c.data(this,this.nodeType?"events":"__events__");if(typeof f==="function")f=f.events;d=(f||{})[a.type];if(f&&d){d=d.slice(0);f=0;for(var l=d.length;f-1?c.map(a.options,function(e){return e.selected}).join("-"):"";else if(a.nodeName.toLowerCase()==="select")d=a.selectedIndex;return d},Z=function(a,b){var d=a.target,e,f;if(!(!ia.test(d.nodeName)||d.readOnly)){e=c.data(d,"_change_data");f=xa(d);if(a.type!=="focusout"||d.type!=="radio")c.data(d,"_change_data",f);if(!(e===B||f===e))if(e!=null||f){a.type="change";a.liveFired= +B;return c.event.trigger(a,b,d)}}};c.event.special.change={filters:{focusout:Z,beforedeactivate:Z,click:function(a){var b=a.target,d=b.type;if(d==="radio"||d==="checkbox"||b.nodeName.toLowerCase()==="select")return Z.call(this,a)},keydown:function(a){var b=a.target,d=b.type;if(a.keyCode===13&&b.nodeName.toLowerCase()!=="textarea"||a.keyCode===32&&(d==="checkbox"||d==="radio")||d==="select-multiple")return Z.call(this,a)},beforeactivate:function(a){a=a.target;c.data(a,"_change_data",xa(a))}},setup:function(){if(this.type=== +"file")return false;for(var a in V)c.event.add(this,a+".specialChange",V[a]);return ia.test(this.nodeName)},teardown:function(){c.event.remove(this,".specialChange");return ia.test(this.nodeName)}};V=c.event.special.change.filters;V.focus=V.beforeactivate}t.addEventListener&&c.each({focus:"focusin",blur:"focusout"},function(a,b){function d(e){e=c.event.fix(e);e.type=b;return c.event.trigger(e,null,e.target)}c.event.special[b]={setup:function(){ua[b]++===0&&t.addEventListener(a,d,true)},teardown:function(){--ua[b]=== +0&&t.removeEventListener(a,d,true)}}});c.each(["bind","one"],function(a,b){c.fn[b]=function(d,e,f){if(typeof d==="object"){for(var h in d)this[b](h,e,d[h],f);return this}if(c.isFunction(e)||e===false){f=e;e=B}var l=b==="one"?c.proxy(f,function(o){c(this).unbind(o,l);return f.apply(this,arguments)}):f;if(d==="unload"&&b!=="one")this.one(d,e,f);else{h=0;for(var k=this.length;h0?this.bind(b,d,e):this.trigger(b)};if(c.attrFn)c.attrFn[b]=true});E.attachEvent&&!E.addEventListener&&c(E).bind("unload",function(){for(var a in c.cache)if(c.cache[a].handle)try{c.event.remove(c.cache[a].handle.elem)}catch(b){}}); +(function(){function a(g,i,n,m,p,q){p=0;for(var u=m.length;p0){F=y;break}}y=y[g]}m[p]=F}}}var d=/((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,e=0,f=Object.prototype.toString,h=false,l=true;[0,0].sort(function(){l=false;return 0});var k=function(g,i,n,m){n=n||[];var p=i=i||t;if(i.nodeType!==1&&i.nodeType!==9)return[];if(!g||typeof g!=="string")return n;var q,u,y,F,M,N=true,O=k.isXML(i),D=[],R=g;do{d.exec("");if(q=d.exec(R)){R=q[3];D.push(q[1]);if(q[2]){F=q[3]; +break}}}while(q);if(D.length>1&&x.exec(g))if(D.length===2&&o.relative[D[0]])u=L(D[0]+D[1],i);else for(u=o.relative[D[0]]?[i]:k(D.shift(),i);D.length;){g=D.shift();if(o.relative[g])g+=D.shift();u=L(g,u)}else{if(!m&&D.length>1&&i.nodeType===9&&!O&&o.match.ID.test(D[0])&&!o.match.ID.test(D[D.length-1])){q=k.find(D.shift(),i,O);i=q.expr?k.filter(q.expr,q.set)[0]:q.set[0]}if(i){q=m?{expr:D.pop(),set:C(m)}:k.find(D.pop(),D.length===1&&(D[0]==="~"||D[0]==="+")&&i.parentNode?i.parentNode:i,O);u=q.expr?k.filter(q.expr, +q.set):q.set;if(D.length>0)y=C(u);else N=false;for(;D.length;){q=M=D.pop();if(o.relative[M])q=D.pop();else M="";if(q==null)q=i;o.relative[M](y,q,O)}}else y=[]}y||(y=u);y||k.error(M||g);if(f.call(y)==="[object Array]")if(N)if(i&&i.nodeType===1)for(g=0;y[g]!=null;g++){if(y[g]&&(y[g]===true||y[g].nodeType===1&&k.contains(i,y[g])))n.push(u[g])}else for(g=0;y[g]!=null;g++)y[g]&&y[g].nodeType===1&&n.push(u[g]);else n.push.apply(n,y);else C(y,n);if(F){k(F,p,n,m);k.uniqueSort(n)}return n};k.uniqueSort=function(g){if(w){h= +l;g.sort(w);if(h)for(var i=1;i0};k.find=function(g,i,n){var m;if(!g)return[];for(var p=0,q=o.order.length;p":function(g,i){var n,m=typeof i==="string",p=0,q=g.length;if(m&&!/\W/.test(i))for(i=i.toLowerCase();p=0))n||m.push(u);else if(n)i[q]=false;return false},ID:function(g){return g[1].replace(/\\/g,"")},TAG:function(g){return g[1].toLowerCase()},CHILD:function(g){if(g[1]==="nth"){var i=/(-?)(\d*)n((?:\+|-)?\d*)/.exec(g[2]==="even"&&"2n"||g[2]==="odd"&&"2n+1"||!/\D/.test(g[2])&&"0n+"+g[2]||g[2]);g[2]=i[1]+(i[2]||1)-0;g[3]=i[3]-0}g[0]=e++;return g},ATTR:function(g,i,n, +m,p,q){i=g[1].replace(/\\/g,"");if(!q&&o.attrMap[i])g[1]=o.attrMap[i];if(g[2]==="~=")g[4]=" "+g[4]+" ";return g},PSEUDO:function(g,i,n,m,p){if(g[1]==="not")if((d.exec(g[3])||"").length>1||/^\w/.test(g[3]))g[3]=k(g[3],null,null,i);else{g=k.filter(g[3],i,n,true^p);n||m.push.apply(m,g);return false}else if(o.match.POS.test(g[0])||o.match.CHILD.test(g[0]))return true;return g},POS:function(g){g.unshift(true);return g}},filters:{enabled:function(g){return g.disabled===false&&g.type!=="hidden"},disabled:function(g){return g.disabled=== +true},checked:function(g){return g.checked===true},selected:function(g){return g.selected===true},parent:function(g){return!!g.firstChild},empty:function(g){return!g.firstChild},has:function(g,i,n){return!!k(n[3],g).length},header:function(g){return/h\d/i.test(g.nodeName)},text:function(g){return"text"===g.type},radio:function(g){return"radio"===g.type},checkbox:function(g){return"checkbox"===g.type},file:function(g){return"file"===g.type},password:function(g){return"password"===g.type},submit:function(g){return"submit"=== +g.type},image:function(g){return"image"===g.type},reset:function(g){return"reset"===g.type},button:function(g){return"button"===g.type||g.nodeName.toLowerCase()==="button"},input:function(g){return/input|select|textarea|button/i.test(g.nodeName)}},setFilters:{first:function(g,i){return i===0},last:function(g,i,n,m){return i===m.length-1},even:function(g,i){return i%2===0},odd:function(g,i){return i%2===1},lt:function(g,i,n){return in[3]-0},nth:function(g,i,n){return n[3]- +0===i},eq:function(g,i,n){return n[3]-0===i}},filter:{PSEUDO:function(g,i,n,m){var p=i[1],q=o.filters[p];if(q)return q(g,n,i,m);else if(p==="contains")return(g.textContent||g.innerText||k.getText([g])||"").indexOf(i[3])>=0;else if(p==="not"){i=i[3];n=0;for(m=i.length;n=0}},ID:function(g,i){return g.nodeType===1&&g.getAttribute("id")===i},TAG:function(g,i){return i==="*"&&g.nodeType===1||g.nodeName.toLowerCase()=== +i},CLASS:function(g,i){return(" "+(g.className||g.getAttribute("class"))+" ").indexOf(i)>-1},ATTR:function(g,i){var n=i[1];n=o.attrHandle[n]?o.attrHandle[n](g):g[n]!=null?g[n]:g.getAttribute(n);var m=n+"",p=i[2],q=i[4];return n==null?p==="!=":p==="="?m===q:p==="*="?m.indexOf(q)>=0:p==="~="?(" "+m+" ").indexOf(q)>=0:!q?m&&n!==false:p==="!="?m!==q:p==="^="?m.indexOf(q)===0:p==="$="?m.substr(m.length-q.length)===q:p==="|="?m===q||m.substr(0,q.length+1)===q+"-":false},POS:function(g,i,n,m){var p=o.setFilters[i[2]]; +if(p)return p(g,n,i,m)}}},x=o.match.POS,r=function(g,i){return"\\"+(i-0+1)},A;for(A in o.match){o.match[A]=RegExp(o.match[A].source+/(?![^\[]*\])(?![^\(]*\))/.source);o.leftMatch[A]=RegExp(/(^(?:.|\r|\n)*?)/.source+o.match[A].source.replace(/\\(\d+)/g,r))}var C=function(g,i){g=Array.prototype.slice.call(g,0);if(i){i.push.apply(i,g);return i}return g};try{Array.prototype.slice.call(t.documentElement.childNodes,0)}catch(J){C=function(g,i){var n=0,m=i||[];if(f.call(g)==="[object Array]")Array.prototype.push.apply(m, +g);else if(typeof g.length==="number")for(var p=g.length;n";n.insertBefore(g,n.firstChild);if(t.getElementById(i)){o.find.ID=function(m,p,q){if(typeof p.getElementById!=="undefined"&&!q)return(p=p.getElementById(m[1]))?p.id===m[1]||typeof p.getAttributeNode!=="undefined"&&p.getAttributeNode("id").nodeValue===m[1]?[p]:B:[]};o.filter.ID=function(m,p){var q=typeof m.getAttributeNode!=="undefined"&&m.getAttributeNode("id");return m.nodeType===1&&q&&q.nodeValue===p}}n.removeChild(g); +n=g=null})();(function(){var g=t.createElement("div");g.appendChild(t.createComment(""));if(g.getElementsByTagName("*").length>0)o.find.TAG=function(i,n){var m=n.getElementsByTagName(i[1]);if(i[1]==="*"){for(var p=[],q=0;m[q];q++)m[q].nodeType===1&&p.push(m[q]);m=p}return m};g.innerHTML="";if(g.firstChild&&typeof g.firstChild.getAttribute!=="undefined"&&g.firstChild.getAttribute("href")!=="#")o.attrHandle.href=function(i){return i.getAttribute("href",2)};g=null})();t.querySelectorAll&& +function(){var g=k,i=t.createElement("div");i.innerHTML="

    ";if(!(i.querySelectorAll&&i.querySelectorAll(".TEST").length===0)){k=function(m,p,q,u){p=p||t;m=m.replace(/\=\s*([^'"\]]*)\s*\]/g,"='$1']");if(!u&&!k.isXML(p))if(p.nodeType===9)try{return C(p.querySelectorAll(m),q)}catch(y){}else if(p.nodeType===1&&p.nodeName.toLowerCase()!=="object"){var F=p.getAttribute("id"),M=F||"__sizzle__";F||p.setAttribute("id",M);try{return C(p.querySelectorAll("#"+M+" "+m),q)}catch(N){}finally{F|| +p.removeAttribute("id")}}return g(m,p,q,u)};for(var n in g)k[n]=g[n];i=null}}();(function(){var g=t.documentElement,i=g.matchesSelector||g.mozMatchesSelector||g.webkitMatchesSelector||g.msMatchesSelector,n=false;try{i.call(t.documentElement,"[test!='']:sizzle")}catch(m){n=true}if(i)k.matchesSelector=function(p,q){q=q.replace(/\=\s*([^'"\]]*)\s*\]/g,"='$1']");if(!k.isXML(p))try{if(n||!o.match.PSEUDO.test(q)&&!/!=/.test(q))return i.call(p,q)}catch(u){}return k(q,null,null,[p]).length>0}})();(function(){var g= +t.createElement("div");g.innerHTML="
    ";if(!(!g.getElementsByClassName||g.getElementsByClassName("e").length===0)){g.lastChild.className="e";if(g.getElementsByClassName("e").length!==1){o.order.splice(1,0,"CLASS");o.find.CLASS=function(i,n,m){if(typeof n.getElementsByClassName!=="undefined"&&!m)return n.getElementsByClassName(i[1])};g=null}}})();k.contains=t.documentElement.contains?function(g,i){return g!==i&&(g.contains?g.contains(i):true)}:t.documentElement.compareDocumentPosition? +function(g,i){return!!(g.compareDocumentPosition(i)&16)}:function(){return false};k.isXML=function(g){return(g=(g?g.ownerDocument||g:0).documentElement)?g.nodeName!=="HTML":false};var L=function(g,i){for(var n,m=[],p="",q=i.nodeType?[i]:i;n=o.match.PSEUDO.exec(g);){p+=n[0];g=g.replace(o.match.PSEUDO,"")}g=o.relative[g]?g+"*":g;n=0;for(var u=q.length;n0)for(var h=d;h0},closest:function(a,b){var d=[],e,f,h=this[0];if(c.isArray(a)){var l,k={},o=1;if(h&&a.length){e=0;for(f=a.length;e-1:c(h).is(e))d.push({selector:l,elem:h,level:o})}h= +h.parentNode;o++}}return d}l=cb.test(a)?c(a,b||this.context):null;e=0;for(f=this.length;e-1:c.find.matchesSelector(h,a)){d.push(h);break}else{h=h.parentNode;if(!h||!h.ownerDocument||h===b)break}d=d.length>1?c.unique(d):d;return this.pushStack(d,"closest",a)},index:function(a){if(!a||typeof a==="string")return c.inArray(this[0],a?c(a):this.parent().children());return c.inArray(a.jquery?a[0]:a,this)},add:function(a,b){var d=typeof a==="string"?c(a,b||this.context): +c.makeArray(a),e=c.merge(this.get(),d);return this.pushStack(!d[0]||!d[0].parentNode||d[0].parentNode.nodeType===11||!e[0]||!e[0].parentNode||e[0].parentNode.nodeType===11?e:c.unique(e))},andSelf:function(){return this.add(this.prevObject)}});c.each({parent:function(a){return(a=a.parentNode)&&a.nodeType!==11?a:null},parents:function(a){return c.dir(a,"parentNode")},parentsUntil:function(a,b,d){return c.dir(a,"parentNode",d)},next:function(a){return c.nth(a,2,"nextSibling")},prev:function(a){return c.nth(a, +2,"previousSibling")},nextAll:function(a){return c.dir(a,"nextSibling")},prevAll:function(a){return c.dir(a,"previousSibling")},nextUntil:function(a,b,d){return c.dir(a,"nextSibling",d)},prevUntil:function(a,b,d){return c.dir(a,"previousSibling",d)},siblings:function(a){return c.sibling(a.parentNode.firstChild,a)},children:function(a){return c.sibling(a.firstChild)},contents:function(a){return c.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:c.makeArray(a.childNodes)}},function(a, +b){c.fn[a]=function(d,e){var f=c.map(this,b,d);Za.test(a)||(e=d);if(e&&typeof e==="string")f=c.filter(e,f);f=this.length>1?c.unique(f):f;if((this.length>1||ab.test(e))&&$a.test(a))f=f.reverse();return this.pushStack(f,a,bb.call(arguments).join(","))}});c.extend({filter:function(a,b,d){if(d)a=":not("+a+")";return b.length===1?c.find.matchesSelector(b[0],a)?[b[0]]:[]:c.find.matches(a,b)},dir:function(a,b,d){var e=[];for(a=a[b];a&&a.nodeType!==9&&(d===B||a.nodeType!==1||!c(a).is(d));){a.nodeType===1&& +e.push(a);a=a[b]}return e},nth:function(a,b,d){b=b||1;for(var e=0;a;a=a[d])if(a.nodeType===1&&++e===b)break;return a},sibling:function(a,b){for(var d=[];a;a=a.nextSibling)a.nodeType===1&&a!==b&&d.push(a);return d}});var za=/ jQuery\d+="(?:\d+|null)"/g,$=/^\s+/,Aa=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,Ba=/<([\w:]+)/,db=/\s]+\/)>/g,P={option:[1, +""],legend:[1,"
    ","
    "],thead:[1,"","
    "],tr:[2,"","
    "],td:[3,"","
    "],col:[2,"","
    "],area:[1,"",""],_default:[0,"",""]};P.optgroup=P.option;P.tbody=P.tfoot=P.colgroup=P.caption=P.thead;P.th=P.td;if(!c.support.htmlSerialize)P._default=[1,"div
    ","
    "];c.fn.extend({text:function(a){if(c.isFunction(a))return this.each(function(b){var d= +c(this);d.text(a.call(this,b,d.text()))});if(typeof a!=="object"&&a!==B)return this.empty().append((this[0]&&this[0].ownerDocument||t).createTextNode(a));return c.text(this)},wrapAll:function(a){if(c.isFunction(a))return this.each(function(d){c(this).wrapAll(a.call(this,d))});if(this[0]){var b=c(a,this[0].ownerDocument).eq(0).clone(true);this[0].parentNode&&b.insertBefore(this[0]);b.map(function(){for(var d=this;d.firstChild&&d.firstChild.nodeType===1;)d=d.firstChild;return d}).append(this)}return this}, +wrapInner:function(a){if(c.isFunction(a))return this.each(function(b){c(this).wrapInner(a.call(this,b))});return this.each(function(){var b=c(this),d=b.contents();d.length?d.wrapAll(a):b.append(a)})},wrap:function(a){return this.each(function(){c(this).wrapAll(a)})},unwrap:function(){return this.parent().each(function(){c.nodeName(this,"body")||c(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,true,function(a){this.nodeType===1&&this.appendChild(a)})}, +prepend:function(){return this.domManip(arguments,true,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,false,function(b){this.parentNode.insertBefore(b,this)});else if(arguments.length){var a=c(arguments[0]);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,false,function(b){this.parentNode.insertBefore(b, +this.nextSibling)});else if(arguments.length){var a=this.pushStack(this,"after",arguments);a.push.apply(a,c(arguments[0]).toArray());return a}},remove:function(a,b){for(var d=0,e;(e=this[d])!=null;d++)if(!a||c.filter(a,[e]).length){if(!b&&e.nodeType===1){c.cleanData(e.getElementsByTagName("*"));c.cleanData([e])}e.parentNode&&e.parentNode.removeChild(e)}return this},empty:function(){for(var a=0,b;(b=this[a])!=null;a++)for(b.nodeType===1&&c.cleanData(b.getElementsByTagName("*"));b.firstChild;)b.removeChild(b.firstChild); +return this},clone:function(a){var b=this.map(function(){if(!c.support.noCloneEvent&&!c.isXMLDoc(this)){var d=this.outerHTML,e=this.ownerDocument;if(!d){d=e.createElement("div");d.appendChild(this.cloneNode(true));d=d.innerHTML}return c.clean([d.replace(za,"").replace(fb,'="$1">').replace($,"")],e)[0]}else return this.cloneNode(true)});if(a===true){na(this,b);na(this.find("*"),b.find("*"))}return b},html:function(a){if(a===B)return this[0]&&this[0].nodeType===1?this[0].innerHTML.replace(za,""):null; +else if(typeof a==="string"&&!Ca.test(a)&&(c.support.leadingWhitespace||!$.test(a))&&!P[(Ba.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(Aa,"<$1>");try{for(var b=0,d=this.length;b0||e.cacheable||this.length>1?h.cloneNode(true):h)}k.length&&c.each(k,Oa)}return this}});c.buildFragment=function(a,b,d){var e,f,h;b=b&&b[0]?b[0].ownerDocument||b[0]:t;if(a.length===1&&typeof a[0]==="string"&&a[0].length<512&&b===t&&!Ca.test(a[0])&&(c.support.checkClone||!Da.test(a[0]))){f=true;if(h=c.fragments[a[0]])if(h!==1)e=h}if(!e){e=b.createDocumentFragment();c.clean(a,b,e,d)}if(f)c.fragments[a[0]]=h?e:1;return{fragment:e,cacheable:f}};c.fragments={};c.each({appendTo:"append", +prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){c.fn[a]=function(d){var e=[];d=c(d);var f=this.length===1&&this[0].parentNode;if(f&&f.nodeType===11&&f.childNodes.length===1&&d.length===1){d[b](this[0]);return this}else{f=0;for(var h=d.length;f0?this.clone(true):this).get();c(d[f])[b](l);e=e.concat(l)}return this.pushStack(e,a,d.selector)}}});c.extend({clean:function(a,b,d,e){b=b||t;if(typeof b.createElement==="undefined")b=b.ownerDocument|| +b[0]&&b[0].ownerDocument||t;for(var f=[],h=0,l;(l=a[h])!=null;h++){if(typeof l==="number")l+="";if(l){if(typeof l==="string"&&!eb.test(l))l=b.createTextNode(l);else if(typeof l==="string"){l=l.replace(Aa,"<$1>");var k=(Ba.exec(l)||["",""])[1].toLowerCase(),o=P[k]||P._default,x=o[0],r=b.createElement("div");for(r.innerHTML=o[1]+l+o[2];x--;)r=r.lastChild;if(!c.support.tbody){x=db.test(l);k=k==="table"&&!x?r.firstChild&&r.firstChild.childNodes:o[1]===""&&!x?r.childNodes:[];for(o=k.length- +1;o>=0;--o)c.nodeName(k[o],"tbody")&&!k[o].childNodes.length&&k[o].parentNode.removeChild(k[o])}!c.support.leadingWhitespace&&$.test(l)&&r.insertBefore(b.createTextNode($.exec(l)[0]),r.firstChild);l=r.childNodes}if(l.nodeType)f.push(l);else f=c.merge(f,l)}}if(d)for(h=0;f[h];h++)if(e&&c.nodeName(f[h],"script")&&(!f[h].type||f[h].type.toLowerCase()==="text/javascript"))e.push(f[h].parentNode?f[h].parentNode.removeChild(f[h]):f[h]);else{f[h].nodeType===1&&f.splice.apply(f,[h+1,0].concat(c.makeArray(f[h].getElementsByTagName("script")))); +d.appendChild(f[h])}return f},cleanData:function(a){for(var b,d,e=c.cache,f=c.event.special,h=c.support.deleteExpando,l=0,k;(k=a[l])!=null;l++)if(!(k.nodeName&&c.noData[k.nodeName.toLowerCase()]))if(d=k[c.expando]){if((b=e[d])&&b.events)for(var o in b.events)f[o]?c.event.remove(k,o):c.removeEvent(k,o,b.handle);if(h)delete k[c.expando];else k.removeAttribute&&k.removeAttribute(c.expando);delete e[d]}}});var Ea=/alpha\([^)]*\)/i,gb=/opacity=([^)]*)/,hb=/-([a-z])/ig,ib=/([A-Z])/g,Fa=/^-?\d+(?:px)?$/i, +jb=/^-?\d/,kb={position:"absolute",visibility:"hidden",display:"block"},Pa=["Left","Right"],Qa=["Top","Bottom"],W,Ga,aa,lb=function(a,b){return b.toUpperCase()};c.fn.css=function(a,b){if(arguments.length===2&&b===B)return this;return c.access(this,a,b,true,function(d,e,f){return f!==B?c.style(d,e,f):c.css(d,e)})};c.extend({cssHooks:{opacity:{get:function(a,b){if(b){var d=W(a,"opacity","opacity");return d===""?"1":d}else return a.style.opacity}}},cssNumber:{zIndex:true,fontWeight:true,opacity:true, +zoom:true,lineHeight:true},cssProps:{"float":c.support.cssFloat?"cssFloat":"styleFloat"},style:function(a,b,d,e){if(!(!a||a.nodeType===3||a.nodeType===8||!a.style)){var f,h=c.camelCase(b),l=a.style,k=c.cssHooks[h];b=c.cssProps[h]||h;if(d!==B){if(!(typeof d==="number"&&isNaN(d)||d==null)){if(typeof d==="number"&&!c.cssNumber[h])d+="px";if(!k||!("set"in k)||(d=k.set(a,d))!==B)try{l[b]=d}catch(o){}}}else{if(k&&"get"in k&&(f=k.get(a,false,e))!==B)return f;return l[b]}}},css:function(a,b,d){var e,f=c.camelCase(b), +h=c.cssHooks[f];b=c.cssProps[f]||f;if(h&&"get"in h&&(e=h.get(a,true,d))!==B)return e;else if(W)return W(a,b,f)},swap:function(a,b,d){var e={},f;for(f in b){e[f]=a.style[f];a.style[f]=b[f]}d.call(a);for(f in b)a.style[f]=e[f]},camelCase:function(a){return a.replace(hb,lb)}});c.curCSS=c.css;c.each(["height","width"],function(a,b){c.cssHooks[b]={get:function(d,e,f){var h;if(e){if(d.offsetWidth!==0)h=oa(d,b,f);else c.swap(d,kb,function(){h=oa(d,b,f)});if(h<=0){h=W(d,b,b);if(h==="0px"&&aa)h=aa(d,b,b); +if(h!=null)return h===""||h==="auto"?"0px":h}if(h<0||h==null){h=d.style[b];return h===""||h==="auto"?"0px":h}return typeof h==="string"?h:h+"px"}},set:function(d,e){if(Fa.test(e)){e=parseFloat(e);if(e>=0)return e+"px"}else return e}}});if(!c.support.opacity)c.cssHooks.opacity={get:function(a,b){return gb.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?parseFloat(RegExp.$1)/100+"":b?"1":""},set:function(a,b){var d=a.style;d.zoom=1;var e=c.isNaN(b)?"":"alpha(opacity="+b*100+")",f= +d.filter||"";d.filter=Ea.test(f)?f.replace(Ea,e):d.filter+" "+e}};if(t.defaultView&&t.defaultView.getComputedStyle)Ga=function(a,b,d){var e;d=d.replace(ib,"-$1").toLowerCase();if(!(b=a.ownerDocument.defaultView))return B;if(b=b.getComputedStyle(a,null)){e=b.getPropertyValue(d);if(e===""&&!c.contains(a.ownerDocument.documentElement,a))e=c.style(a,d)}return e};if(t.documentElement.currentStyle)aa=function(a,b){var d,e,f=a.currentStyle&&a.currentStyle[b],h=a.style;if(!Fa.test(f)&&jb.test(f)){d=h.left; +e=a.runtimeStyle.left;a.runtimeStyle.left=a.currentStyle.left;h.left=b==="fontSize"?"1em":f||0;f=h.pixelLeft+"px";h.left=d;a.runtimeStyle.left=e}return f===""?"auto":f};W=Ga||aa;if(c.expr&&c.expr.filters){c.expr.filters.hidden=function(a){var b=a.offsetHeight;return a.offsetWidth===0&&b===0||!c.support.reliableHiddenOffsets&&(a.style.display||c.css(a,"display"))==="none"};c.expr.filters.visible=function(a){return!c.expr.filters.hidden(a)}}var mb=c.now(),nb=/)<[^<]*)*<\/script>/gi, +ob=/^(?:select|textarea)/i,pb=/^(?:color|date|datetime|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,qb=/^(?:GET|HEAD)$/,Ra=/\[\]$/,T=/\=\?(&|$)/,ja=/\?/,rb=/([?&])_=[^&]*/,sb=/^(\w+:)?\/\/([^\/?#]+)/,tb=/%20/g,ub=/#.*$/,Ha=c.fn.load;c.fn.extend({load:function(a,b,d){if(typeof a!=="string"&&Ha)return Ha.apply(this,arguments);else if(!this.length)return this;var e=a.indexOf(" ");if(e>=0){var f=a.slice(e,a.length);a=a.slice(0,e)}e="GET";if(b)if(c.isFunction(b)){d=b;b=null}else if(typeof b=== +"object"){b=c.param(b,c.ajaxSettings.traditional);e="POST"}var h=this;c.ajax({url:a,type:e,dataType:"html",data:b,complete:function(l,k){if(k==="success"||k==="notmodified")h.html(f?c("
    ").append(l.responseText.replace(nb,"")).find(f):l.responseText);d&&h.each(d,[l.responseText,k,l])}});return this},serialize:function(){return c.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?c.makeArray(this.elements):this}).filter(function(){return this.name&& +!this.disabled&&(this.checked||ob.test(this.nodeName)||pb.test(this.type))}).map(function(a,b){var d=c(this).val();return d==null?null:c.isArray(d)?c.map(d,function(e){return{name:b.name,value:e}}):{name:b.name,value:d}}).get()}});c.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(a,b){c.fn[b]=function(d){return this.bind(b,d)}});c.extend({get:function(a,b,d,e){if(c.isFunction(b)){e=e||d;d=b;b=null}return c.ajax({type:"GET",url:a,data:b,success:d,dataType:e})}, +getScript:function(a,b){return c.get(a,null,b,"script")},getJSON:function(a,b,d){return c.get(a,b,d,"json")},post:function(a,b,d,e){if(c.isFunction(b)){e=e||d;d=b;b={}}return c.ajax({type:"POST",url:a,data:b,success:d,dataType:e})},ajaxSetup:function(a){c.extend(c.ajaxSettings,a)},ajaxSettings:{url:location.href,global:true,type:"GET",contentType:"application/x-www-form-urlencoded",processData:true,async:true,xhr:function(){return new E.XMLHttpRequest},accepts:{xml:"application/xml, text/xml",html:"text/html", +script:"text/javascript, application/javascript",json:"application/json, text/javascript",text:"text/plain",_default:"*/*"}},ajax:function(a){var b=c.extend(true,{},c.ajaxSettings,a),d,e,f,h=b.type.toUpperCase(),l=qb.test(h);b.url=b.url.replace(ub,"");b.context=a&&a.context!=null?a.context:b;if(b.data&&b.processData&&typeof b.data!=="string")b.data=c.param(b.data,b.traditional);if(b.dataType==="jsonp"){if(h==="GET")T.test(b.url)||(b.url+=(ja.test(b.url)?"&":"?")+(b.jsonp||"callback")+"=?");else if(!b.data|| +!T.test(b.data))b.data=(b.data?b.data+"&":"")+(b.jsonp||"callback")+"=?";b.dataType="json"}if(b.dataType==="json"&&(b.data&&T.test(b.data)||T.test(b.url))){d=b.jsonpCallback||"jsonp"+mb++;if(b.data)b.data=(b.data+"").replace(T,"="+d+"$1");b.url=b.url.replace(T,"="+d+"$1");b.dataType="script";var k=E[d];E[d]=function(m){if(c.isFunction(k))k(m);else{E[d]=B;try{delete E[d]}catch(p){}}f=m;c.handleSuccess(b,w,e,f);c.handleComplete(b,w,e,f);r&&r.removeChild(A)}}if(b.dataType==="script"&&b.cache===null)b.cache= +false;if(b.cache===false&&l){var o=c.now(),x=b.url.replace(rb,"$1_="+o);b.url=x+(x===b.url?(ja.test(b.url)?"&":"?")+"_="+o:"")}if(b.data&&l)b.url+=(ja.test(b.url)?"&":"?")+b.data;b.global&&c.active++===0&&c.event.trigger("ajaxStart");o=(o=sb.exec(b.url))&&(o[1]&&o[1].toLowerCase()!==location.protocol||o[2].toLowerCase()!==location.host);if(b.dataType==="script"&&h==="GET"&&o){var r=t.getElementsByTagName("head")[0]||t.documentElement,A=t.createElement("script");if(b.scriptCharset)A.charset=b.scriptCharset; +A.src=b.url;if(!d){var C=false;A.onload=A.onreadystatechange=function(){if(!C&&(!this.readyState||this.readyState==="loaded"||this.readyState==="complete")){C=true;c.handleSuccess(b,w,e,f);c.handleComplete(b,w,e,f);A.onload=A.onreadystatechange=null;r&&A.parentNode&&r.removeChild(A)}}}r.insertBefore(A,r.firstChild);return B}var J=false,w=b.xhr();if(w){b.username?w.open(h,b.url,b.async,b.username,b.password):w.open(h,b.url,b.async);try{if(b.data!=null&&!l||a&&a.contentType)w.setRequestHeader("Content-Type", +b.contentType);if(b.ifModified){c.lastModified[b.url]&&w.setRequestHeader("If-Modified-Since",c.lastModified[b.url]);c.etag[b.url]&&w.setRequestHeader("If-None-Match",c.etag[b.url])}o||w.setRequestHeader("X-Requested-With","XMLHttpRequest");w.setRequestHeader("Accept",b.dataType&&b.accepts[b.dataType]?b.accepts[b.dataType]+", */*; q=0.01":b.accepts._default)}catch(I){}if(b.beforeSend&&b.beforeSend.call(b.context,w,b)===false){b.global&&c.active--===1&&c.event.trigger("ajaxStop");w.abort();return false}b.global&& +c.triggerGlobal(b,"ajaxSend",[w,b]);var L=w.onreadystatechange=function(m){if(!w||w.readyState===0||m==="abort"){J||c.handleComplete(b,w,e,f);J=true;if(w)w.onreadystatechange=c.noop}else if(!J&&w&&(w.readyState===4||m==="timeout")){J=true;w.onreadystatechange=c.noop;e=m==="timeout"?"timeout":!c.httpSuccess(w)?"error":b.ifModified&&c.httpNotModified(w,b.url)?"notmodified":"success";var p;if(e==="success")try{f=c.httpData(w,b.dataType,b)}catch(q){e="parsererror";p=q}if(e==="success"||e==="notmodified")d|| +c.handleSuccess(b,w,e,f);else c.handleError(b,w,e,p);d||c.handleComplete(b,w,e,f);m==="timeout"&&w.abort();if(b.async)w=null}};try{var g=w.abort;w.abort=function(){w&&Function.prototype.call.call(g,w);L("abort")}}catch(i){}b.async&&b.timeout>0&&setTimeout(function(){w&&!J&&L("timeout")},b.timeout);try{w.send(l||b.data==null?null:b.data)}catch(n){c.handleError(b,w,null,n);c.handleComplete(b,w,e,f)}b.async||L();return w}},param:function(a,b){var d=[],e=function(h,l){l=c.isFunction(l)?l():l;d[d.length]= +encodeURIComponent(h)+"="+encodeURIComponent(l)};if(b===B)b=c.ajaxSettings.traditional;if(c.isArray(a)||a.jquery)c.each(a,function(){e(this.name,this.value)});else for(var f in a)da(f,a[f],b,e);return d.join("&").replace(tb,"+")}});c.extend({active:0,lastModified:{},etag:{},handleError:function(a,b,d,e){a.error&&a.error.call(a.context,b,d,e);a.global&&c.triggerGlobal(a,"ajaxError",[b,a,e])},handleSuccess:function(a,b,d,e){a.success&&a.success.call(a.context,e,d,b);a.global&&c.triggerGlobal(a,"ajaxSuccess", +[b,a])},handleComplete:function(a,b,d){a.complete&&a.complete.call(a.context,b,d);a.global&&c.triggerGlobal(a,"ajaxComplete",[b,a]);a.global&&c.active--===1&&c.event.trigger("ajaxStop")},triggerGlobal:function(a,b,d){(a.context&&a.context.url==null?c(a.context):c.event).trigger(b,d)},httpSuccess:function(a){try{return!a.status&&location.protocol==="file:"||a.status>=200&&a.status<300||a.status===304||a.status===1223}catch(b){}return false},httpNotModified:function(a,b){var d=a.getResponseHeader("Last-Modified"), +e=a.getResponseHeader("Etag");if(d)c.lastModified[b]=d;if(e)c.etag[b]=e;return a.status===304},httpData:function(a,b,d){var e=a.getResponseHeader("content-type")||"",f=b==="xml"||!b&&e.indexOf("xml")>=0;a=f?a.responseXML:a.responseText;f&&a.documentElement.nodeName==="parsererror"&&c.error("parsererror");if(d&&d.dataFilter)a=d.dataFilter(a,b);if(typeof a==="string")if(b==="json"||!b&&e.indexOf("json")>=0)a=c.parseJSON(a);else if(b==="script"||!b&&e.indexOf("javascript")>=0)c.globalEval(a);return a}}); +if(E.ActiveXObject)c.ajaxSettings.xhr=function(){if(E.location.protocol!=="file:")try{return new E.XMLHttpRequest}catch(a){}try{return new E.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}};c.support.ajax=!!c.ajaxSettings.xhr();var ea={},vb=/^(?:toggle|show|hide)$/,wb=/^([+\-]=)?([\d+.\-]+)(.*)$/,ba,pa=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]];c.fn.extend({show:function(a,b,d){if(a||a===0)return this.animate(S("show", +3),a,b,d);else{d=0;for(var e=this.length;d=0;e--)if(d[e].elem===this){b&&d[e](true);d.splice(e,1)}});b||this.dequeue();return this}});c.each({slideDown:S("show",1),slideUp:S("hide",1),slideToggle:S("toggle",1),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(a,b){c.fn[a]=function(d,e,f){return this.animate(b, +d,e,f)}});c.extend({speed:function(a,b,d){var e=a&&typeof a==="object"?c.extend({},a):{complete:d||!d&&b||c.isFunction(a)&&a,duration:a,easing:d&&b||b&&!c.isFunction(b)&&b};e.duration=c.fx.off?0:typeof e.duration==="number"?e.duration:e.duration in c.fx.speeds?c.fx.speeds[e.duration]:c.fx.speeds._default;e.old=e.complete;e.complete=function(){e.queue!==false&&c(this).dequeue();c.isFunction(e.old)&&e.old.call(this)};return e},easing:{linear:function(a,b,d,e){return d+e*a},swing:function(a,b,d,e){return(-Math.cos(a* +Math.PI)/2+0.5)*e+d}},timers:[],fx:function(a,b,d){this.options=b;this.elem=a;this.prop=d;if(!b.orig)b.orig={}}});c.fx.prototype={update:function(){this.options.step&&this.options.step.call(this.elem,this.now,this);(c.fx.step[this.prop]||c.fx.step._default)(this)},cur:function(){if(this.elem[this.prop]!=null&&(!this.elem.style||this.elem.style[this.prop]==null))return this.elem[this.prop];var a=parseFloat(c.css(this.elem,this.prop));return a&&a>-1E4?a:0},custom:function(a,b,d){function e(l){return f.step(l)} +var f=this,h=c.fx;this.startTime=c.now();this.start=a;this.end=b;this.unit=d||this.unit||"px";this.now=this.start;this.pos=this.state=0;e.elem=this.elem;if(e()&&c.timers.push(e)&&!ba)ba=setInterval(h.tick,h.interval)},show:function(){this.options.orig[this.prop]=c.style(this.elem,this.prop);this.options.show=true;this.custom(this.prop==="width"||this.prop==="height"?1:0,this.cur());c(this.elem).show()},hide:function(){this.options.orig[this.prop]=c.style(this.elem,this.prop);this.options.hide=true; +this.custom(this.cur(),0)},step:function(a){var b=c.now(),d=true;if(a||b>=this.options.duration+this.startTime){this.now=this.end;this.pos=this.state=1;this.update();this.options.curAnim[this.prop]=true;for(var e in this.options.curAnim)if(this.options.curAnim[e]!==true)d=false;if(d){if(this.options.overflow!=null&&!c.support.shrinkWrapBlocks){var f=this.elem,h=this.options;c.each(["","X","Y"],function(k,o){f.style["overflow"+o]=h.overflow[k]})}this.options.hide&&c(this.elem).hide();if(this.options.hide|| +this.options.show)for(var l in this.options.curAnim)c.style(this.elem,l,this.options.orig[l]);this.options.complete.call(this.elem)}return false}else{a=b-this.startTime;this.state=a/this.options.duration;b=this.options.easing||(c.easing.swing?"swing":"linear");this.pos=c.easing[this.options.specialEasing&&this.options.specialEasing[this.prop]||b](this.state,a,0,1,this.options.duration);this.now=this.start+(this.end-this.start)*this.pos;this.update()}return true}};c.extend(c.fx,{tick:function(){for(var a= +c.timers,b=0;b-1;e={};var x={};if(o)x=f.position();l=o?x.top:parseInt(l,10)||0;k=o?x.left:parseInt(k,10)||0;if(c.isFunction(b))b=b.call(a,d,h);if(b.top!=null)e.top=b.top-h.top+l;if(b.left!=null)e.left=b.left-h.left+k;"using"in b?b.using.call(a, +e):f.css(e)}};c.fn.extend({position:function(){if(!this[0])return null;var a=this[0],b=this.offsetParent(),d=this.offset(),e=Ia.test(b[0].nodeName)?{top:0,left:0}:b.offset();d.top-=parseFloat(c.css(a,"marginTop"))||0;d.left-=parseFloat(c.css(a,"marginLeft"))||0;e.top+=parseFloat(c.css(b[0],"borderTopWidth"))||0;e.left+=parseFloat(c.css(b[0],"borderLeftWidth"))||0;return{top:d.top-e.top,left:d.left-e.left}},offsetParent:function(){return this.map(function(){for(var a=this.offsetParent||t.body;a&&!Ia.test(a.nodeName)&& +c.css(a,"position")==="static";)a=a.offsetParent;return a})}});c.each(["Left","Top"],function(a,b){var d="scroll"+b;c.fn[d]=function(e){var f=this[0],h;if(!f)return null;if(e!==B)return this.each(function(){if(h=fa(this))h.scrollTo(!a?e:c(h).scrollLeft(),a?e:c(h).scrollTop());else this[d]=e});else return(h=fa(f))?"pageXOffset"in h?h[a?"pageYOffset":"pageXOffset"]:c.support.boxModel&&h.document.documentElement[d]||h.document.body[d]:f[d]}});c.each(["Height","Width"],function(a,b){var d=b.toLowerCase(); +c.fn["inner"+b]=function(){return this[0]?parseFloat(c.css(this[0],d,"padding")):null};c.fn["outer"+b]=function(e){return this[0]?parseFloat(c.css(this[0],d,e?"margin":"border")):null};c.fn[d]=function(e){var f=this[0];if(!f)return e==null?null:this;if(c.isFunction(e))return this.each(function(l){var k=c(this);k[d](e.call(this,l,k[d]()))});if(c.isWindow(f))return f.document.compatMode==="CSS1Compat"&&f.document.documentElement["client"+b]||f.document.body["client"+b];else if(f.nodeType===9)return Math.max(f.documentElement["client"+ +b],f.body["scroll"+b],f.documentElement["scroll"+b],f.body["offset"+b],f.documentElement["offset"+b]);else if(e===B){f=c.css(f,d);var h=parseFloat(f);return c.isNaN(h)?f:h}else return this.css(d,typeof e==="string"?e:e+"px")}})})(window); diff --git a/websdk/werkzeug/debug/shared/less.png b/websdk/werkzeug/debug/shared/less.png new file mode 100755 index 0000000..5efefd6 --- /dev/null +++ b/websdk/werkzeug/debug/shared/less.png Binary files differ diff --git a/websdk/werkzeug/debug/shared/more.png b/websdk/werkzeug/debug/shared/more.png new file mode 100755 index 0000000..804fa22 --- /dev/null +++ b/websdk/werkzeug/debug/shared/more.png Binary files differ diff --git a/websdk/werkzeug/debug/shared/source.png b/websdk/werkzeug/debug/shared/source.png new file mode 100755 index 0000000..f7ea904 --- /dev/null +++ b/websdk/werkzeug/debug/shared/source.png Binary files differ diff --git a/websdk/werkzeug/debug/shared/style.css b/websdk/werkzeug/debug/shared/style.css new file mode 100644 index 0000000..53a107e --- /dev/null +++ b/websdk/werkzeug/debug/shared/style.css @@ -0,0 +1,113 @@ +@font-face { + font-family: 'Ubuntu'; + font-style: normal; + font-weight: normal; + src: local('Ubuntu'), local('Ubuntu-Regular'), + url('?__debugger__=yes&cmd=resource&f=ubuntu.ttf') format('truetype'); +} + +body, input { font-family: 'Lucida Grande', 'Lucida Sans Unicode', 'Geneva', + 'Verdana', sans-serif; color: #000; text-align: center; + margin: 1em; padding: 0; font-size: 15px; } +h1, h2, h3 { font-family: 'Ubuntu', 'Lucida Grande', 'Lucida Sans Unicode', + 'Geneva', 'Verdana', sans-serif; font-weight: normal; } + +input { background-color: #fff; margin: 0; text-align: left; + outline: none !important; } +a { color: #11557C; } +a:hover { color: #177199; } +pre, code, table.source, +textarea { font-family: 'Consolas', 'Monaco', 'Bitstream Vera Sans Mono', + monospace; font-size: 14px; } + +div.debugger { text-align: left; padding: 12px; margin: auto; + background-color: white; } +h1 { font-size: 36px; margin: 0 0 0.3em 0; } +div.detail p { margin: 0 0 8px 13px; font-size: 14px; white-space: pre-wrap; } +div.explanation { margin: 20px 13px; font-size: 15px; color: #555; } +div.footer { font-size: 13px; text-align: right; margin: 30px 0; + color: #86989B; } + +h2 { font-size: 16px; margin: 1.3em 0 0.0 0; padding: 9px; + background-color: #11557C; color: white; } +h2 em, h3 em { font-style: normal; color: #A5D6D9; font-weight: normal; } + +div.traceback, div.plain { border: 1px solid #ddd; margin: 0 0 1em 0; padding: 10px; } +div.plain p { margin: 0; } +div.plain textarea, +div.plain pre { margin: 10px 0 0 0; padding: 4px; + background-color: #E8EFF0; border: 1px solid #D3E7E9; } +div.plain textarea { width: 99%; height: 300px; } +div.traceback h3 { font-size: 1em; margin: 0 0 0.8em 0; } +div.traceback ul { list-style: none; margin: 0; padding: 0 0 0 1em; } +div.traceback h4 { font-size: 13px; font-weight: normal; margin: 0.7em 0 0.1em 0; } +div.traceback pre { margin: 0; padding: 5px 0 3px 15px; + background-color: #E8EFF0; border: 1px solid #D3E7E9; } +div.traceback pre, +div.box table.source { white-space: pre-wrap; /* css-3 should we be so lucky... */ + white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ + white-space: -pre-wrap; /* Opera 4-6 ?? */ + white-space: -o-pre-wrap; /* Opera 7 ?? */ + word-wrap: break-word; /* Internet Explorer 5.5+ */ + _white-space: pre; /* IE only hack to re-specify in + addition to word-wrap */ } +div.traceback pre:hover { background-color: #DDECEE; color: black; cursor: pointer; } +div.traceback blockquote { margin: 1em 0 0 0; padding: 0; } +div.traceback img { float: right; padding: 2px; margin: -3px 2px 0 0; display: none; } +div.traceback img:hover { background-color: #ddd; cursor: pointer; + border-color: #BFDDE0; } +div.traceback pre:hover img { display: block; } +div.traceback cite.filename { font-style: normal; color: #3B666B; } + +pre.console { border: 1px solid #ccc; background: white!important; + color: black; padding: 5px!important; + margin: 3px 0 0 0!important; cursor: default!important; + max-height: 400px; overflow: auto; } +pre.console form { color: #555; } +pre.console input { background-color: transparent; color: #555; + width: 90%; font-family: 'Consolas', 'Deja Vu Sans Mono', + 'Bitstream Vera Sans Mono', monospace; font-size: 14px; + border: none!important; } + +span.string { color: #30799B; } +span.number { color: #9C1A1C; } +span.help { color: #3A7734; } +span.object { color: #485F6E; } +span.extended { opacity: 0.5; } +span.extended:hover { opacity: 1; } +a.toggle { text-decoration: none; background-repeat: no-repeat; + background-position: center center; + background-image: url(?__debugger__=yes&cmd=resource&f=more.png); } +a.toggle:hover { background-color: #444; } +a.open { background-image: url(?__debugger__=yes&cmd=resource&f=less.png); } + +pre.console div.traceback, +pre.console div.box { margin: 5px 10px; white-space: normal; + border: 1px solid #11557C; padding: 10px; + font-family: 'Lucida Grande', 'Lucida Sans Unicode', 'Geneva', + 'Verdana', sans-serif; } +pre.console div.box h3, +pre.console div.traceback h3 { margin: -10px -10px 10px -10px; padding: 5px; + background: #11557C; color: white; } + +pre.console div.traceback pre:hover { cursor: default; background: #E8EFF0; } +pre.console div.traceback pre.syntaxerror { background: inherit; border: none; + margin: 20px -10px -10px -10px; + padding: 10px; border-top: 1px solid #BFDDE0; + background: #E8EFF0; } +pre.console div.noframe-traceback pre.syntaxerror { margin-top: -10px; border: none; } + +pre.console div.box pre.repr { padding: 0; margin: 0; background-color: white; border: none; } +pre.console div.box table { margin-top: 6px; } +pre.console div.box pre { border: none; } +pre.console div.box pre.help { background-color: white; } +pre.console div.box pre.help:hover { cursor: default; } +pre.console table tr { vertical-align: top; } +div.console { border: 1px solid #ccc; padding: 4px; background-color: #fafafa; } + +div.box table.source { border-collapse: collapse; width: 100%; background: #E8EFF0 } +div.box table.source td { border-top: 1px solid #E8EFF0; padding: 4px 0 4px 10px; } +div.box table.source td.lineno { color: #999; padding-right: 10px; width: 1px; } +div.box table.source tr.in-frame { background-color: white; } +div.box table.source tr.current { background-color: #EEF7F8; color: #23707E; } +div.sourceview { max-height: 400px; overflow: auto; border: 1px solid #ccc; } diff --git a/websdk/werkzeug/debug/shared/ubuntu.ttf b/websdk/werkzeug/debug/shared/ubuntu.ttf new file mode 100644 index 0000000..8079f93 --- /dev/null +++ b/websdk/werkzeug/debug/shared/ubuntu.ttf Binary files differ diff --git a/websdk/werkzeug/debug/tbtools.py b/websdk/werkzeug/debug/tbtools.py new file mode 100644 index 0000000..4bcba21 --- /dev/null +++ b/websdk/werkzeug/debug/tbtools.py @@ -0,0 +1,483 @@ +# -*- coding: utf-8 -*- +""" + werkzeug.debug.tbtools + ~~~~~~~~~~~~~~~~~~~~~~ + + This module provides various traceback related utility functions. + + :copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details. + :license: BSD. +""" +import re +import os +import sys +import inspect +import traceback +import codecs +from tokenize import TokenError +from werkzeug.utils import cached_property, escape +from werkzeug.debug.console import Console + +_coding_re = re.compile(r'coding[:=]\s*([-\w.]+)') +_line_re = re.compile(r'^(.*?)$(?m)') +_funcdef_re = re.compile(r'^(\s*def\s)|(.*(? + + + %(title)s // Werkzeug Debugger + + + + + + +
    +''' +FOOTER = u'''\ + +
    + + +''' + +PAGE_HTML = HEADER + u'''\ +

    %(exception_type)s

    +
    +

    %(exception)s

    +
    +

    Traceback (most recent call last)

    +%(summary)s +
    +
    +

    + + This is the Copy/Paste friendly version of the traceback. You can also paste this traceback into LodgeIt: + +

    + + +
    +
    + The debugger caught an exception in your WSGI application. You can now + look at the traceback which led to the error. + If you enable JavaScript you can also use additional features such as code + execution (if the evalex feature is enabled), automatic pasting of the + exceptions and much more. +
    +''' + FOOTER + ''' + +''' + +CONSOLE_HTML = HEADER + u'''\ +

    Interactive Console

    +
    +In this console you can execute Python expressions in the context of the +application. The initial namespace was created by the debugger automatically. +
    +
    The Console requires JavaScript.
    +''' + FOOTER + +SUMMARY_HTML = u'''\ +
    + %(title)s +
      %(frames)s
    + %(description)s +
    +''' + +FRAME_HTML = u'''\ +
    +

    File "%(filename)s", + line %(lineno)s, + in %(function_name)s

    +
    %(current_line)s
    +
    +''' + +SOURCE_TABLE_HTML = u'
    %s
    ' + +SOURCE_LINE_HTML = u'''\ + + %(lineno)s + %(code)s + +''' + + +def render_console_html(secret): + return CONSOLE_HTML % { + 'evalex': 'true', + 'console': 'true', + 'title': 'Console', + 'secret': secret, + 'traceback_id': -1 + } + + +def get_current_traceback(ignore_system_exceptions=False, + show_hidden_frames=False, skip=0): + """Get the current exception info as `Traceback` object. Per default + calling this method will reraise system exceptions such as generator exit, + system exit or others. This behavior can be disabled by passing `False` + to the function as first parameter. + """ + exc_type, exc_value, tb = sys.exc_info() + if ignore_system_exceptions and exc_type in system_exceptions: + raise + for x in xrange(skip): + if tb.tb_next is None: + break + tb = tb.tb_next + tb = Traceback(exc_type, exc_value, tb) + if not show_hidden_frames: + tb.filter_hidden_frames() + return tb + + +class Line(object): + """Helper for the source renderer.""" + __slots__ = ('lineno', 'code', 'in_frame', 'current') + + def __init__(self, lineno, code): + self.lineno = lineno + self.code = code + self.in_frame = False + self.current = False + + def classes(self): + rv = ['line'] + if self.in_frame: + rv.append('in-frame') + if self.current: + rv.append('current') + return rv + classes = property(classes) + + def render(self): + return SOURCE_LINE_HTML % { + 'classes': u' '.join(self.classes), + 'lineno': self.lineno, + 'code': escape(self.code) + } + + +class Traceback(object): + """Wraps a traceback.""" + + def __init__(self, exc_type, exc_value, tb): + self.exc_type = exc_type + self.exc_value = exc_value + if not isinstance(exc_type, str): + exception_type = exc_type.__name__ + if exc_type.__module__ not in ('__builtin__', 'exceptions'): + exception_type = exc_type.__module__ + '.' + exception_type + else: + exception_type = exc_type + self.exception_type = exception_type + + # we only add frames to the list that are not hidden. This follows + # the the magic variables as defined by paste.exceptions.collector + self.frames = [] + while tb: + self.frames.append(Frame(exc_type, exc_value, tb)) + tb = tb.tb_next + + def filter_hidden_frames(self): + """Remove the frames according to the paste spec.""" + if not self.frames: + return + + new_frames = [] + hidden = False + for frame in self.frames: + hide = frame.hide + if hide in ('before', 'before_and_this'): + new_frames = [] + hidden = False + if hide == 'before_and_this': + continue + elif hide in ('reset', 'reset_and_this'): + hidden = False + if hide == 'reset_and_this': + continue + elif hide in ('after', 'after_and_this'): + hidden = True + if hide == 'after_and_this': + continue + elif hide or hidden: + continue + new_frames.append(frame) + + # if we only have one frame and that frame is from the codeop + # module, remove it. + if len(new_frames) == 1 and self.frames[0].module == 'codeop': + del self.frames[:] + + # if the last frame is missing something went terrible wrong :( + elif self.frames[-1] in new_frames: + self.frames[:] = new_frames + + def is_syntax_error(self): + """Is it a syntax error?""" + return isinstance(self.exc_value, SyntaxError) + is_syntax_error = property(is_syntax_error) + + def exception(self): + """String representation of the exception.""" + buf = traceback.format_exception_only(self.exc_type, self.exc_value) + return ''.join(buf).strip().decode('utf-8', 'replace') + exception = property(exception) + + def log(self, logfile=None): + """Log the ASCII traceback into a file object.""" + if logfile is None: + logfile = sys.stderr + tb = self.plaintext.encode('utf-8', 'replace').rstrip() + '\n' + logfile.write(tb) + + def paste(self, lodgeit_url): + """Create a paste and return the paste id.""" + from xmlrpclib import ServerProxy + srv = ServerProxy('%sxmlrpc/' % lodgeit_url) + return srv.pastes.newPaste('pytb', self.plaintext, '', '', '', True) + + def render_summary(self, include_title=True): + """Render the traceback for the interactive console.""" + title = '' + description = '' + frames = [] + classes = ['traceback'] + if not self.frames: + classes.append('noframe-traceback') + + if include_title: + if self.is_syntax_error: + title = u'Syntax Error' + else: + title = u'Traceback (most recent call last):' + + for frame in self.frames: + frames.append(u'%s' % ( + frame.info and u' title="%s"' % escape(frame.info) or u'', + frame.render() + )) + + if self.is_syntax_error: + description_wrapper = u'
    %s
    ' + else: + description_wrapper = u'
    %s
    ' + + return SUMMARY_HTML % { + 'classes': u' '.join(classes), + 'title': title and u'

    %s

    ' % title or u'', + 'frames': u'\n'.join(frames), + 'description': description_wrapper % escape(self.exception) + } + + def render_full(self, evalex=False, lodgeit_url=None, + secret=None): + """Render the Full HTML page with the traceback info.""" + exc = escape(self.exception) + return PAGE_HTML % { + 'evalex': evalex and 'true' or 'false', + 'console': 'false', + 'lodgeit_url': escape(lodgeit_url), + 'title': exc, + 'exception': exc, + 'exception_type': escape(self.exception_type), + 'summary': self.render_summary(include_title=False), + 'plaintext': self.plaintext, + 'plaintext_cs': re.sub('-{2,}', '-', self.plaintext), + 'traceback_id': self.id, + 'secret': secret + } + + def generate_plaintext_traceback(self): + """Like the plaintext attribute but returns a generator""" + yield u'Traceback (most recent call last):' + for frame in self.frames: + yield u' File "%s", line %s, in %s' % ( + frame.filename, + frame.lineno, + frame.function_name + ) + yield u' ' + frame.current_line.strip() + yield self.exception + + def plaintext(self): + return u'\n'.join(self.generate_plaintext_traceback()) + plaintext = cached_property(plaintext) + + id = property(lambda x: id(x)) + + +class Frame(object): + """A single frame in a traceback.""" + + def __init__(self, exc_type, exc_value, tb): + self.lineno = tb.tb_lineno + self.function_name = tb.tb_frame.f_code.co_name + self.locals = tb.tb_frame.f_locals + self.globals = tb.tb_frame.f_globals + + fn = inspect.getsourcefile(tb) or inspect.getfile(tb) + if fn[-4:] in ('.pyo', '.pyc'): + fn = fn[:-1] + # if it's a file on the file system resolve the real filename. + if os.path.isfile(fn): + fn = os.path.realpath(fn) + self.filename = fn + self.module = self.globals.get('__name__') + self.loader = self.globals.get('__loader__') + self.code = tb.tb_frame.f_code + + # support for paste's traceback extensions + self.hide = self.locals.get('__traceback_hide__', False) + info = self.locals.get('__traceback_info__') + if info is not None: + try: + info = unicode(info) + except UnicodeError: + info = str(info).decode('utf-8', 'replace') + self.info = info + + def render(self): + """Render a single frame in a traceback.""" + return FRAME_HTML % { + 'id': self.id, + 'filename': escape(self.filename), + 'lineno': self.lineno, + 'function_name': escape(self.function_name), + 'current_line': escape(self.current_line.strip()) + } + + def get_annotated_lines(self): + """Helper function that returns lines with extra information.""" + lines = [Line(idx + 1, x) for idx, x in enumerate(self.sourcelines)] + + # find function definition and mark lines + if hasattr(self.code, 'co_firstlineno'): + lineno = self.code.co_firstlineno - 1 + while lineno > 0: + if _funcdef_re.match(lines[lineno].code): + break + lineno -= 1 + try: + offset = len(inspect.getblock([x.code + '\n' for x + in lines[lineno:]])) + except TokenError: + offset = 0 + for line in lines[lineno:lineno + offset]: + line.in_frame = True + + # mark current line + try: + lines[self.lineno - 1].current = True + except IndexError: + pass + + return lines + + def render_source(self): + """Render the sourcecode.""" + return SOURCE_TABLE_HTML % u'\n'.join(line.render() for line in + self.get_annotated_lines()) + + def eval(self, code, mode='single'): + """Evaluate code in the context of the frame.""" + if isinstance(code, basestring): + if isinstance(code, unicode): + code = UTF8_COOKIE + code.encode('utf-8') + code = compile(code, '', mode) + if mode != 'exec': + return eval(code, self.globals, self.locals) + exec code in self.globals, self.locals + + @cached_property + def sourcelines(self): + """The sourcecode of the file as list of unicode strings.""" + # get sourcecode from loader or file + source = None + if self.loader is not None: + try: + if hasattr(self.loader, 'get_source'): + source = self.loader.get_source(self.module) + elif hasattr(self.loader, 'get_source_by_code'): + source = self.loader.get_source_by_code(self.code) + except Exception: + # we munch the exception so that we don't cause troubles + # if the loader is broken. + pass + + if source is None: + try: + f = file(self.filename) + except IOError: + return [] + try: + source = f.read() + finally: + f.close() + + # already unicode? return right away + if isinstance(source, unicode): + return source.splitlines() + + # yes. it should be ascii, but we don't want to reject too many + # characters in the debugger if something breaks + charset = 'utf-8' + if source.startswith(UTF8_COOKIE): + source = source[3:] + else: + for idx, match in enumerate(_line_re.finditer(source)): + match = _line_re.search(match.group()) + if match is not None: + charset = match.group(1) + break + if idx > 1: + break + + # on broken cookies we fall back to utf-8 too + try: + codecs.lookup(charset) + except LookupError: + charset = 'utf-8' + + return source.decode(charset, 'replace').splitlines() + + @property + def current_line(self): + try: + return self.sourcelines[self.lineno - 1] + except IndexError: + return u'' + + @cached_property + def console(self): + return Console(self.globals, self.locals) + + id = property(lambda x: id(x)) diff --git a/websdk/werkzeug/exceptions.py b/websdk/werkzeug/exceptions.py new file mode 100644 index 0000000..b9b43c9 --- /dev/null +++ b/websdk/werkzeug/exceptions.py @@ -0,0 +1,536 @@ +# -*- coding: utf-8 -*- +""" + werkzeug.exceptions + ~~~~~~~~~~~~~~~~~~~ + + This module implements a number of Python exceptions you can raise from + within your views to trigger a standard non-200 response. + + + Usage Example + ------------- + + :: + + from werkzeug.wrappers import BaseRequest + from werkzeug.wsgi import responder + from werkzeug.exceptions import HTTPException, NotFound + + def view(request): + raise NotFound() + + @responder + def application(environ, start_response): + request = BaseRequest(environ) + try: + return view(request) + except HTTPException, e: + return e + + + As you can see from this example those exceptions are callable WSGI + applications. Because of Python 2.4 compatibility those do not extend + from the response objects but only from the python exception class. + + As a matter of fact they are not Werkzeug response objects. However you + can get a response object by calling ``get_response()`` on a HTTP + exception. + + Keep in mind that you have to pass an environment to ``get_response()`` + because some errors fetch additional information from the WSGI + environment. + + If you want to hook in a different exception page to say, a 404 status + code, you can add a second except for a specific subclass of an error:: + + @responder + def application(environ, start_response): + request = BaseRequest(environ) + try: + return view(request) + except NotFound, e: + return not_found(request) + except HTTPException, e: + return e + + + :copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" +import sys +from werkzeug._internal import HTTP_STATUS_CODES, _get_environ + + +class HTTPException(Exception): + """ + Baseclass for all HTTP exceptions. This exception can be called as WSGI + application to render a default error page or you can catch the subclasses + of it independently and render nicer error messages. + """ + + code = None + description = None + + def __init__(self, description=None): + Exception.__init__(self, '%d %s' % (self.code, self.name)) + if description is not None: + self.description = description + + @classmethod + def wrap(cls, exception, name=None): + """This method returns a new subclass of the exception provided that + also is a subclass of `BadRequest`. + """ + class newcls(cls, exception): + def __init__(self, arg=None, description=None): + cls.__init__(self, description) + exception.__init__(self, arg) + newcls.__module__ = sys._getframe(1).f_globals.get('__name__') + newcls.__name__ = name or cls.__name__ + exception.__name__ + return newcls + + @property + def name(self): + """The status name.""" + return HTTP_STATUS_CODES[self.code] + + def get_description(self, environ): + """Get the description.""" + environ = _get_environ(environ) + return self.description + + def get_body(self, environ): + """Get the HTML body.""" + return ( + '\n' + '%(code)s %(name)s\n' + '

    %(name)s

    \n' + '%(description)s\n' + ) % { + 'code': self.code, + 'name': escape(self.name), + 'description': self.get_description(environ) + } + + def get_headers(self, environ): + """Get a list of headers.""" + return [('Content-Type', 'text/html')] + + def get_response(self, environ): + """Get a response object. + + :param environ: the environ for the request. + :return: a :class:`BaseResponse` object or a subclass thereof. + """ + # lazily imported for various reasons. For one, we can use the exceptions + # with custom responses (testing exception instances against types) and + # so we don't ever have to import the wrappers, but also because there + # are circular dependencies when bootstrapping the module. + environ = _get_environ(environ) + from werkzeug.wrappers import BaseResponse + headers = self.get_headers(environ) + return BaseResponse(self.get_body(environ), self.code, headers) + + def __call__(self, environ, start_response): + """Call the exception as WSGI application. + + :param environ: the WSGI environment. + :param start_response: the response callable provided by the WSGI + server. + """ + response = self.get_response(environ) + return response(environ, start_response) + + def __str__(self): + return unicode(self).encode('utf-8') + + def __unicode__(self): + if 'description' in self.__dict__: + txt = self.description + else: + txt = self.name + return '%d: %s' % (self.code, txt) + + def __repr__(self): + return '<%s \'%s\'>' % (self.__class__.__name__, self) + + +class _ProxyException(HTTPException): + """An HTTP exception that expands renders a WSGI application on error.""" + + def __init__(self, response): + Exception.__init__(self, 'proxy exception for %r' % response) + self.response = response + + def get_response(self, environ): + return self.response + + +class BadRequest(HTTPException): + """*400* `Bad Request` + + Raise if the browser sends something to the application the application + or server cannot handle. + """ + code = 400 + description = ( + '

    The browser (or proxy) sent a request that this server could ' + 'not understand.

    ' + ) + + +class ClientDisconnected(BadRequest): + """Internal exception that is raised if Werkzeug detects a disconnected + client. Since the client is already gone at that point attempting to + send the error message to the client might not work and might ultimately + result in another exception in the server. Mainly this is here so that + it is silenced by default as far as Werkzeug is concerned. + + Since disconnections cannot be reliably detected and are unspecified + by WSGI to a large extend this might or might not be raised if a client + is gone. + + .. versionadded:: 0.8 + """ + + +class Unauthorized(HTTPException): + """*401* `Unauthorized` + + Raise if the user is not authorized. Also used if you want to use HTTP + basic auth. + """ + code = 401 + description = ( + '

    The server could not verify that you are authorized to access ' + 'the URL requested. You either supplied the wrong credentials (e.g. ' + 'a bad password), or your browser doesn\'t understand how to supply ' + 'the credentials required.

    In case you are allowed to request ' + 'the document, please check your user-id and password and try ' + 'again.

    ' + ) + + +class Forbidden(HTTPException): + """*403* `Forbidden` + + Raise if the user doesn't have the permission for the requested resource + but was authenticated. + """ + code = 403 + description = ( + '

    You don\'t have the permission to access the requested resource. ' + 'It is either read-protected or not readable by the server.

    ' + ) + + +class NotFound(HTTPException): + """*404* `Not Found` + + Raise if a resource does not exist and never existed. + """ + code = 404 + description = ( + '

    The requested URL was not found on the server.

    ' + '

    If you entered the URL manually please check your spelling and ' + 'try again.

    ' + ) + + +class MethodNotAllowed(HTTPException): + """*405* `Method Not Allowed` + + Raise if the server used a method the resource does not handle. For + example `POST` if the resource is view only. Especially useful for REST. + + The first argument for this exception should be a list of allowed methods. + Strictly speaking the response would be invalid if you don't provide valid + methods in the header which you can do with that list. + """ + code = 405 + + def __init__(self, valid_methods=None, description=None): + """Takes an optional list of valid http methods + starting with werkzeug 0.3 the list will be mandatory.""" + HTTPException.__init__(self, description) + self.valid_methods = valid_methods + + def get_headers(self, environ): + headers = HTTPException.get_headers(self, environ) + if self.valid_methods: + headers.append(('Allow', ', '.join(self.valid_methods))) + return headers + + def get_description(self, environ): + m = escape(environ.get('REQUEST_METHOD', 'GET')) + return '

    The method %s is not allowed for the requested URL.

    ' % m + + +class NotAcceptable(HTTPException): + """*406* `Not Acceptable` + + Raise if the server can't return any content conforming to the + `Accept` headers of the client. + """ + code = 406 + + description = ( + '

    The resource identified by the request is only capable of ' + 'generating response entities which have content characteristics ' + 'not acceptable according to the accept headers sent in the ' + 'request.

    ' + ) + + +class RequestTimeout(HTTPException): + """*408* `Request Timeout` + + Raise to signalize a timeout. + """ + code = 408 + description = ( + '

    The server closed the network connection because the browser ' + 'didn\'t finish the request within the specified time.

    ' + ) + + +class Conflict(HTTPException): + """*409* `Conflict` + + Raise to signal that a request cannot be completed because it conflicts + with the current state on the server. + + .. versionadded:: 0.7 + """ + code = 409 + description = ( + '

    A conflict happened while processing the request. The resource ' + 'might have been modified while the request was being processed.' + ) + + +class Gone(HTTPException): + """*410* `Gone` + + Raise if a resource existed previously and went away without new location. + """ + code = 410 + description = ( + '

    The requested URL is no longer available on this server and ' + 'there is no forwarding address.

    If you followed a link ' + 'from a foreign page, please contact the author of this page.' + ) + + +class LengthRequired(HTTPException): + """*411* `Length Required` + + Raise if the browser submitted data but no ``Content-Length`` header which + is required for the kind of processing the server does. + """ + code = 411 + description = ( + '

    A request with this method requires a valid Content-' + 'Length header.

    ' + ) + + +class PreconditionFailed(HTTPException): + """*412* `Precondition Failed` + + Status code used in combination with ``If-Match``, ``If-None-Match``, or + ``If-Unmodified-Since``. + """ + code = 412 + description = ( + '

    The precondition on the request for the URL failed positive ' + 'evaluation.

    ' + ) + + +class RequestEntityTooLarge(HTTPException): + """*413* `Request Entity Too Large` + + The status code one should return if the data submitted exceeded a given + limit. + """ + code = 413 + description = ( + '

    The data value transmitted exceeds the capacity limit.

    ' + ) + + +class RequestURITooLarge(HTTPException): + """*414* `Request URI Too Large` + + Like *413* but for too long URLs. + """ + code = 414 + description = ( + '

    The length of the requested URL exceeds the capacity limit ' + 'for this server. The request cannot be processed.

    ' + ) + + +class UnsupportedMediaType(HTTPException): + """*415* `Unsupported Media Type` + + The status code returned if the server is unable to handle the media type + the client transmitted. + """ + code = 415 + description = ( + '

    The server does not support the media type transmitted in ' + 'the request.

    ' + ) + + +class RequestedRangeNotSatisfiable(HTTPException): + """*416* `Requested Range Not Satisfiable` + + The client asked for a part of the file that lies beyond the end + of the file. + + .. versionadded:: 0.7 + """ + code = 416 + description = ( + '

    The server cannot provide the requested range.' + ) + + +class ExpectationFailed(HTTPException): + """*417* `Expectation Failed` + + The server cannot meet the requirements of the Expect request-header. + + .. versionadded:: 0.7 + """ + code = 417 + description = ( + '

    The server could not meet the requirements of the Expect header' + ) + + +class ImATeapot(HTTPException): + """*418* `I'm a teapot` + + The server should return this if it is a teapot and someone attempted + to brew coffee with it. + + .. versionadded:: 0.7 + """ + code = 418 + description = ( + '

    This server is a teapot, not a coffee machine' + ) + + +class InternalServerError(HTTPException): + """*500* `Internal Server Error` + + Raise if an internal server error occurred. This is a good fallback if an + unknown error occurred in the dispatcher. + """ + code = 500 + description = ( + '

    The server encountered an internal error and was unable to ' + 'complete your request. Either the server is overloaded or there ' + 'is an error in the application.

    ' + ) + + +class NotImplemented(HTTPException): + """*501* `Not Implemented` + + Raise if the application does not support the action requested by the + browser. + """ + code = 501 + description = ( + '

    The server does not support the action requested by the ' + 'browser.

    ' + ) + + +class BadGateway(HTTPException): + """*502* `Bad Gateway` + + If you do proxying in your application you should return this status code + if you received an invalid response from the upstream server it accessed + in attempting to fulfill the request. + """ + code = 502 + description = ( + '

    The proxy server received an invalid response from an upstream ' + 'server.

    ' + ) + + +class ServiceUnavailable(HTTPException): + """*503* `Service Unavailable` + + Status code you should return if a service is temporarily unavailable. + """ + code = 503 + description = ( + '

    The server is temporarily unable to service your request due to ' + 'maintenance downtime or capacity problems. Please try again ' + 'later.

    ' + ) + + +default_exceptions = {} +__all__ = ['HTTPException'] + +def _find_exceptions(): + for name, obj in globals().iteritems(): + try: + if getattr(obj, 'code', None) is not None: + default_exceptions[obj.code] = obj + __all__.append(obj.__name__) + except TypeError: # pragma: no cover + continue +_find_exceptions() +del _find_exceptions + + +#: raised by the request functions if they were unable to decode the +#: incoming data properly. +HTTPUnicodeError = BadRequest.wrap(UnicodeError, 'HTTPUnicodeError') + + +class Aborter(object): + """ + When passed a dict of code -> exception items it can be used as + callable that raises exceptions. If the first argument to the + callable is an integer it will be looked up in the mapping, if it's + a WSGI application it will be raised in a proxy exception. + + The rest of the arguments are forwarded to the exception constructor. + """ + + def __init__(self, mapping=None, extra=None): + if mapping is None: + mapping = default_exceptions + self.mapping = dict(mapping) + if extra is not None: + self.mapping.update(extra) + + def __call__(self, code, *args, **kwargs): + if not args and not kwargs and not isinstance(code, (int, long)): + raise _ProxyException(code) + if code not in self.mapping: + raise LookupError('no exception for %r' % code) + raise self.mapping[code](*args, **kwargs) + +abort = Aborter() + + +#: an exception that is used internally to signal both a key error and a +#: bad request. Used by a lot of the datastructures. +BadRequestKeyError = BadRequest.wrap(KeyError) + + +# imported here because of circular dependencies of werkzeug.utils +from werkzeug.utils import escape diff --git a/websdk/werkzeug/formparser.py b/websdk/werkzeug/formparser.py new file mode 100644 index 0000000..cd57620 --- /dev/null +++ b/websdk/werkzeug/formparser.py @@ -0,0 +1,463 @@ +# -*- coding: utf-8 -*- +""" + werkzeug.formparser + ~~~~~~~~~~~~~~~~~~~ + + This module implements the form parsing. It supports url-encoded forms + as well as non-nested multipart uploads. + + :copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" +import re +from cStringIO import StringIO +from tempfile import TemporaryFile +from itertools import chain, repeat +from functools import update_wrapper + +from werkzeug._internal import _decode_unicode, _empty_stream +from werkzeug.urls import url_decode_stream +from werkzeug.wsgi import LimitedStream, make_line_iter +from werkzeug.exceptions import RequestEntityTooLarge +from werkzeug.datastructures import Headers, FileStorage, MultiDict +from werkzeug.http import parse_options_header + + +#: an iterator that yields empty strings +_empty_string_iter = repeat('') + +#: a regular expression for multipart boundaries +_multipart_boundary_re = re.compile('^[ -~]{0,200}[!-~]$') + +#: supported http encodings that are also available in python we support +#: for multipart messages. +_supported_multipart_encodings = frozenset(['base64', 'quoted-printable']) + + +def default_stream_factory(total_content_length, filename, content_type, + content_length=None): + """The stream factory that is used per default.""" + if total_content_length > 1024 * 500: + return TemporaryFile('wb+') + return StringIO() + + +def parse_form_data(environ, stream_factory=None, charset='utf-8', + errors='replace', max_form_memory_size=None, + max_content_length=None, cls=None, + silent=True): + """Parse the form data in the environ and return it as tuple in the form + ``(stream, form, files)``. You should only call this method if the + transport method is `POST`, `PUT`, or `PATCH`. + + If the mimetype of the data transmitted is `multipart/form-data` the + files multidict will be filled with `FileStorage` objects. If the + mimetype is unknown the input stream is wrapped and returned as first + argument, else the stream is empty. + + This is a shortcut for the common usage of :class:`FormDataParser`. + + Have a look at :ref:`dealing-with-request-data` for more details. + + .. versionadded:: 0.5 + The `max_form_memory_size`, `max_content_length` and + `cls` parameters were added. + + .. versionadded:: 0.5.1 + The optional `silent` flag was added. + + :param environ: the WSGI environment to be used for parsing. + :param stream_factory: An optional callable that returns a new read and + writeable file descriptor. This callable works + the same as :meth:`~BaseResponse._get_file_stream`. + :param charset: The character set for URL and url encoded form data. + :param errors: The encoding error behavior. + :param max_form_memory_size: the maximum number of bytes to be accepted for + in-memory stored form data. If the data + exceeds the value specified an + :exc:`~exceptions.RequestURITooLarge` + exception is raised. + :param max_content_length: If this is provided and the transmitted data + is longer than this value an + :exc:`~exceptions.RequestEntityTooLarge` + exception is raised. + :param cls: an optional dict class to use. If this is not specified + or `None` the default :class:`MultiDict` is used. + :param silent: If set to False parsing errors will not be caught. + :return: A tuple in the form ``(stream, form, files)``. + """ + return FormDataParser(stream_factory, charset, errors, + max_form_memory_size, max_content_length, + cls, silent).parse_from_environ(environ) + + +def exhaust_stream(f): + """Helper decorator for methods that exhausts the stream on return.""" + def wrapper(self, stream, *args, **kwargs): + try: + return f(self, stream, *args, **kwargs) + finally: + stream.exhaust() + return update_wrapper(wrapper, f) + + +class FormDataParser(object): + """This class implements parsing of form data for Werkzeug. By itself + it can parse multipart and url encoded form data. It can be subclasses + and extended but for most mimetypes it is a better idea to use the + untouched stream and expose it as separate attributes on a request + object. + + .. versionadded:: 0.8 + + :param stream_factory: An optional callable that returns a new read and + writeable file descriptor. This callable works + the same as :meth:`~BaseResponse._get_file_stream`. + :param charset: The character set for URL and url encoded form data. + :param errors: The encoding error behavior. + :param max_form_memory_size: the maximum number of bytes to be accepted for + in-memory stored form data. If the data + exceeds the value specified an + :exc:`~exceptions.RequestURITooLarge` + exception is raised. + :param max_content_length: If this is provided and the transmitted data + is longer than this value an + :exc:`~exceptions.RequestEntityTooLarge` + exception is raised. + :param cls: an optional dict class to use. If this is not specified + or `None` the default :class:`MultiDict` is used. + :param silent: If set to False parsing errors will not be caught. + """ + + def __init__(self, stream_factory=None, charset='utf-8', + errors='replace', max_form_memory_size=None, + max_content_length=None, cls=None, + silent=True): + if stream_factory is None: + stream_factory = default_stream_factory + self.stream_factory = stream_factory + self.charset = charset + self.errors = errors + self.max_form_memory_size = max_form_memory_size + self.max_content_length = max_content_length + if cls is None: + cls = MultiDict + self.cls = cls + self.silent = silent + + def get_parse_func(self, mimetype, options): + return self.parse_functions.get(mimetype) + + def parse_from_environ(self, environ): + """Parses the information from the environment as form data. + + :param environ: the WSGI environment to be used for parsing. + :return: A tuple in the form ``(stream, form, files)``. + """ + content_type = environ.get('CONTENT_TYPE', '') + mimetype, options = parse_options_header(content_type) + try: + content_length = int(environ['CONTENT_LENGTH']) + except (KeyError, ValueError): + content_length = 0 + stream = environ['wsgi.input'] + return self.parse(stream, mimetype, content_length, options) + + def parse(self, stream, mimetype, content_length, options=None): + """Parses the information from the given stream, mimetype, + content length and mimetype parameters. + + :param stream: an input stream + :param mimetype: the mimetype of the data + :param content_length: the content length of the incoming data + :param options: optional mimetype parameters (used for + the multipart boundary for instance) + :return: A tuple in the form ``(stream, form, files)``. + """ + if self.max_content_length is not None and \ + content_length > self.max_content_length: + raise RequestEntityTooLarge() + if options is None: + options = {} + input_stream = LimitedStream(stream, content_length) + + parse_func = self.get_parse_func(mimetype, options) + if parse_func is not None: + try: + return parse_func(self, input_stream, mimetype, + content_length, options) + except ValueError: + if not self.silent: + raise + return input_stream, self.cls(), self.cls() + + @exhaust_stream + def _parse_multipart(self, stream, mimetype, content_length, options): + parser = MultiPartParser(self.stream_factory, self.charset, self.errors, + max_form_memory_size=self.max_form_memory_size, + cls=self.cls) + form, files = parser.parse(stream, options.get('boundary'), + content_length) + return _empty_stream, form, files + + @exhaust_stream + def _parse_urlencoded(self, stream, mimetype, content_length, options): + if self.max_form_memory_size is not None and \ + content_length > self.max_form_memory_size: + raise RequestEntityTooLarge() + form = url_decode_stream(stream, self.charset, + errors=self.errors, cls=self.cls) + return _empty_stream, form, self.cls() + + #: mapping of mimetypes to parsing functions + parse_functions = { + 'multipart/form-data': _parse_multipart, + 'application/x-www-form-urlencoded': _parse_urlencoded, + 'application/x-url-encoded': _parse_urlencoded + } + + +def is_valid_multipart_boundary(boundary): + """Checks if the string given is a valid multipart boundary.""" + return _multipart_boundary_re.match(boundary) is not None + + +def _line_parse(line): + """Removes line ending characters and returns a tuple (`stripped_line`, + `is_terminated`). + """ + if line[-2:] == '\r\n': + return line[:-2], True + elif line[-1:] in '\r\n': + return line[:-1], True + return line, False + + +def parse_multipart_headers(iterable): + """Parses multipart headers from an iterable that yields lines (including + the trailing newline symbol. The iterable has to be newline terminated: + + >>> parse_multipart_headers(['Foo: Bar\r\n', 'Test: Blub\r\n', + ... '\r\n', 'More data']) + Headers([('Foo', 'Bar'), ('Test', 'Blub')]) + + :param iterable: iterable of strings that are newline terminated + """ + result = [] + for line in iterable: + line, line_terminated = _line_parse(line) + if not line_terminated: + raise ValueError('unexpected end of line in multipart header') + if not line: + break + elif line[0] in ' \t' and result: + key, value = result[-1] + result[-1] = (key, value + '\n ' + line[1:]) + else: + parts = line.split(':', 1) + if len(parts) == 2: + result.append((parts[0].strip(), parts[1].strip())) + + # we link the list to the headers, no need to create a copy, the + # list was not shared anyways. + return Headers.linked(result) + + +class MultiPartParser(object): + + def __init__(self, stream_factory=None, charset='utf-8', errors='replace', + max_form_memory_size=None, cls=None, buffer_size=10 * 1024): + self.stream_factory = stream_factory + self.charset = charset + self.errors = errors + self.max_form_memory_size = max_form_memory_size + if stream_factory is None: + stream_factory = default_stream_factory + if cls is None: + cls = MultiDict + self.cls = cls + + # make sure the buffer size is divisible by four so that we can base64 + # decode chunk by chunk + assert buffer_size % 4 == 0, 'buffer size has to be divisible by 4' + # also the buffer size has to be at least 1024 bytes long or long headers + # will freak out the system + assert buffer_size >= 1024, 'buffer size has to be at least 1KB' + + self.buffer_size = buffer_size + + def _fix_ie_filename(self, filename): + """Internet Explorer 6 transmits the full file name if a file is + uploaded. This function strips the full path if it thinks the + filename is Windows-like absolute. + """ + if filename[1:3] == ':\\' or filename[:2] == '\\\\': + return filename.split('\\')[-1] + return filename + + def _find_terminator(self, iterator): + """The terminator might have some additional newlines before it. + There is at least one application that sends additional newlines + before headers (the python setuptools package). + """ + for line in iterator: + if not line: + break + line = line.strip() + if line: + return line + return '' + + def fail(self, message): + raise ValueError(message) + + def get_part_encoding(self, headers): + transfer_encoding = headers.get('content-transfer-encoding') + if transfer_encoding is not None and \ + transfer_encoding in _supported_multipart_encodings: + return transfer_encoding + + def get_part_charset(self, headers): + # Figure out input charset for current part + content_type = headers.get('content-type') + if content_type: + mimetype, ct_params = parse_options_header(content_type) + return ct_params.get('charset', self.charset) + return self.charset + + def start_file_streaming(self, filename, headers, total_content_length): + filename = _decode_unicode(filename, self.charset, self.errors) + filename = self._fix_ie_filename(filename) + content_type = headers.get('content_type') + try: + content_length = int(headers['content-length']) + except (KeyError, ValueError): + content_length = 0 + container = self.stream_factory(total_content_length, content_type, + filename, content_length) + return filename, container + + def in_memory_threshold_reached(self, bytes): + raise RequestEntityTooLarge() + + def validate_boundary(self, boundary): + if not boundary: + self.fail('Missing boundary') + if not is_valid_multipart_boundary(boundary): + self.fail('Invalid boundary: %s' % boundary) + if len(boundary) > self.buffer_size: # pragma: no cover + # this should never happen because we check for a minimum size + # of 1024 and boundaries may not be longer than 200. The only + # situation when this happen is for non debug builds where + # the assert i skipped. + self.fail('Boundary longer than buffer size') + + def parse(self, file, boundary, content_length): + next_part = '--' + boundary + last_part = next_part + '--' + + form = [] + files = [] + in_memory = 0 + + iterator = chain(make_line_iter(file, limit=content_length, + buffer_size=self.buffer_size), + _empty_string_iter) + + terminator = self._find_terminator(iterator) + if terminator != next_part: + self.fail('Expected boundary at start of multipart data') + + while terminator != last_part: + headers = parse_multipart_headers(iterator) + + disposition = headers.get('content-disposition') + if disposition is None: + self.fail('Missing Content-Disposition header') + disposition, extra = parse_options_header(disposition) + transfer_encoding = self.get_part_encoding(headers) + name = extra.get('name') + filename = extra.get('filename') + part_charset = self.get_part_charset(headers) + + # if no content type is given we stream into memory. A list is + # used as a temporary container. + if filename is None: + is_file = False + container = [] + _write = container.append + guard_memory = self.max_form_memory_size is not None + + # otherwise we parse the rest of the headers and ask the stream + # factory for something we can write in. + else: + is_file = True + guard_memory = False + filename, container = self.start_file_streaming( + filename, headers, content_length) + _write = container.write + + buf = '' + for line in iterator: + if not line: + self.fail('unexpected end of stream') + + if line[:2] == '--': + terminator = line.rstrip() + if terminator in (next_part, last_part): + break + + if transfer_encoding is not None: + try: + line = line.decode(transfer_encoding) + except Exception: + self.fail('could not decode transfer encoded chunk') + + # we have something in the buffer from the last iteration. + # this is usually a newline delimiter. + if buf: + _write(buf) + buf = '' + + # If the line ends with windows CRLF we write everything except + # the last two bytes. In all other cases however we write + # everything except the last byte. If it was a newline, that's + # fine, otherwise it does not matter because we will write it + # the next iteration. this ensures we do not write the + # final newline into the stream. That way we do not have to + # truncate the stream. However we do have to make sure that + # if something else than a newline is in there we write it + # out. + if line[-2:] == '\r\n': + buf = '\r\n' + cutoff = -2 + else: + buf = line[-1] + cutoff = -1 + _write(line[:cutoff]) + + # if we write into memory and there is a memory size limit we + # count the number of bytes in memory and raise an exception if + # there is too much data in memory. + if guard_memory: + in_memory += len(line) + if in_memory > self.max_form_memory_size: + self.in_memory_threshold_reached(in_memory) + else: # pragma: no cover + raise ValueError('unexpected end of part') + + # if we have a leftover in the buffer that is not a newline + # character we have to flush it, otherwise we will chop of + # certain values. + if buf not in ('', '\r', '\n', '\r\n'): + _write(buf) + + if is_file: + container.seek(0) + files.append((name, FileStorage(container, filename, name, + headers=headers))) + else: + form.append((name, _decode_unicode(''.join(container), + part_charset, self.errors))) + + return self.cls(form), self.cls(files) diff --git a/websdk/werkzeug/http.py b/websdk/werkzeug/http.py new file mode 100644 index 0000000..23e1328 --- /dev/null +++ b/websdk/werkzeug/http.py @@ -0,0 +1,841 @@ +# -*- coding: utf-8 -*- +""" + werkzeug.http + ~~~~~~~~~~~~~ + + Werkzeug comes with a bunch of utilities that help Werkzeug to deal with + HTTP data. Most of the classes and functions provided by this module are + used by the wrappers, but they are useful on their own, too, especially if + the response and request objects are not used. + + This covers some of the more HTTP centric features of WSGI, some other + utilities such as cookie handling are documented in the `werkzeug.utils` + module. + + + :copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" +import re +from time import time +try: + from email.utils import parsedate_tz +except ImportError: # pragma: no cover + from email.Utils import parsedate_tz +from urllib2 import parse_http_list as _parse_list_header +from datetime import datetime, timedelta +try: + from hashlib import md5 +except ImportError: # pragma: no cover + from md5 import new as md5 + + +#: HTTP_STATUS_CODES is "exported" from this module. +#: XXX: move to werkzeug.consts or something +from werkzeug._internal import HTTP_STATUS_CODES, _dump_date, \ + _ExtendedCookie, _ExtendedMorsel, _decode_unicode + + +_accept_re = re.compile(r'([^\s;,]+)(?:[^,]*?;\s*q=(\d*(?:\.\d+)?))?') +_token_chars = frozenset("!#$%&'*+-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" + '^_`abcdefghijklmnopqrstuvwxyz|~') +_etag_re = re.compile(r'([Ww]/)?(?:"(.*?)"|(.*?))(?:\s*,\s*|$)') +_unsafe_header_chars = set('()<>@,;:\"/[]?={} \t') +_quoted_string_re = r'"[^"\\]*(?:\\.[^"\\]*)*"' +_option_header_piece_re = re.compile(r';\s*([^\s;=]+|%s)\s*(?:=\s*([^;]+|%s))?\s*' % + (_quoted_string_re, _quoted_string_re)) + +_entity_headers = frozenset([ + 'allow', 'content-encoding', 'content-language', 'content-length', + 'content-location', 'content-md5', 'content-range', 'content-type', + 'expires', 'last-modified' +]) +_hop_by_hop_headers = frozenset([ + 'connection', 'keep-alive', 'proxy-authenticate', + 'proxy-authorization', 'te', 'trailers', 'transfer-encoding', + 'upgrade' +]) + + +def quote_header_value(value, extra_chars='', allow_token=True): + """Quote a header value if necessary. + + .. versionadded:: 0.5 + + :param value: the value to quote. + :param extra_chars: a list of extra characters to skip quoting. + :param allow_token: if this is enabled token values are returned + unchanged. + """ + value = str(value) + if allow_token: + token_chars = _token_chars | set(extra_chars) + if set(value).issubset(token_chars): + return value + return '"%s"' % value.replace('\\', '\\\\').replace('"', '\\"') + + +def unquote_header_value(value, is_filename=False): + r"""Unquotes a header value. (Reversal of :func:`quote_header_value`). + This does not use the real unquoting but what browsers are actually + using for quoting. + + .. versionadded:: 0.5 + + :param value: the header value to unquote. + """ + if value and value[0] == value[-1] == '"': + # this is not the real unquoting, but fixing this so that the + # RFC is met will result in bugs with internet explorer and + # probably some other browsers as well. IE for example is + # uploading files with "C:\foo\bar.txt" as filename + value = value[1:-1] + + # if this is a filename and the starting characters look like + # a UNC path, then just return the value without quotes. Using the + # replace sequence below on a UNC path has the effect of turning + # the leading double slash into a single slash and then + # _fix_ie_filename() doesn't work correctly. See #458. + if not is_filename or value[:2] != '\\\\': + return value.replace('\\\\', '\\').replace('\\"', '"') + return value + + +def dump_options_header(header, options): + """The reverse function to :func:`parse_options_header`. + + :param header: the header to dump + :param options: a dict of options to append. + """ + segments = [] + if header is not None: + segments.append(header) + for key, value in options.iteritems(): + if value is None: + segments.append(key) + else: + segments.append('%s=%s' % (key, quote_header_value(value))) + return '; '.join(segments) + + +def dump_header(iterable, allow_token=True): + """Dump an HTTP header again. This is the reversal of + :func:`parse_list_header`, :func:`parse_set_header` and + :func:`parse_dict_header`. This also quotes strings that include an + equals sign unless you pass it as dict of key, value pairs. + + >>> dump_header({'foo': 'bar baz'}) + 'foo="bar baz"' + >>> dump_header(('foo', 'bar baz')) + 'foo, "bar baz"' + + :param iterable: the iterable or dict of values to quote. + :param allow_token: if set to `False` tokens as values are disallowed. + See :func:`quote_header_value` for more details. + """ + if isinstance(iterable, dict): + items = [] + for key, value in iterable.iteritems(): + if value is None: + items.append(key) + else: + items.append('%s=%s' % ( + key, + quote_header_value(value, allow_token=allow_token) + )) + else: + items = [quote_header_value(x, allow_token=allow_token) + for x in iterable] + return ', '.join(items) + + +def parse_list_header(value): + """Parse lists as described by RFC 2068 Section 2. + + In particular, parse comma-separated lists where the elements of + the list may include quoted-strings. A quoted-string could + contain a comma. A non-quoted string could have quotes in the + middle. Quotes are removed automatically after parsing. + + It basically works like :func:`parse_set_header` just that items + may appear multiple times and case sensitivity is preserved. + + The return value is a standard :class:`list`: + + >>> parse_list_header('token, "quoted value"') + ['token', 'quoted value'] + + To create a header from the :class:`list` again, use the + :func:`dump_header` function. + + :param value: a string with a list header. + :return: :class:`list` + """ + result = [] + for item in _parse_list_header(value): + if item[:1] == item[-1:] == '"': + item = unquote_header_value(item[1:-1]) + result.append(item) + return result + + +def parse_dict_header(value): + """Parse lists of key, value pairs as described by RFC 2068 Section 2 and + convert them into a python dict: + + >>> d = parse_dict_header('foo="is a fish", bar="as well"') + >>> type(d) is dict + True + >>> sorted(d.items()) + [('bar', 'as well'), ('foo', 'is a fish')] + + If there is no value for a key it will be `None`: + + >>> parse_dict_header('key_without_value') + {'key_without_value': None} + + To create a header from the :class:`dict` again, use the + :func:`dump_header` function. + + :param value: a string with a dict header. + :return: :class:`dict` + """ + result = {} + for item in _parse_list_header(value): + if '=' not in item: + result[item] = None + continue + name, value = item.split('=', 1) + if value[:1] == value[-1:] == '"': + value = unquote_header_value(value[1:-1]) + result[name] = value + return result + + +def parse_options_header(value): + """Parse a ``Content-Type`` like header into a tuple with the content + type and the options: + + >>> parse_options_header('Content-Type: text/html; mimetype=text/html') + ('Content-Type:', {'mimetype': 'text/html'}) + + This should not be used to parse ``Cache-Control`` like headers that use + a slightly different format. For these headers use the + :func:`parse_dict_header` function. + + .. versionadded:: 0.5 + + :param value: the header to parse. + :return: (str, options) + """ + def _tokenize(string): + for match in _option_header_piece_re.finditer(string): + key, value = match.groups() + key = unquote_header_value(key) + if value is not None: + value = unquote_header_value(value, key == 'filename') + yield key, value + + if not value: + return '', {} + + parts = _tokenize(';' + value) + name = parts.next()[0] + extra = dict(parts) + return name, extra + + +def parse_accept_header(value, cls=None): + """Parses an HTTP Accept-* header. This does not implement a complete + valid algorithm but one that supports at least value and quality + extraction. + + Returns a new :class:`Accept` object (basically a list of ``(value, quality)`` + tuples sorted by the quality with some additional accessor methods). + + The second parameter can be a subclass of :class:`Accept` that is created + with the parsed values and returned. + + :param value: the accept header string to be parsed. + :param cls: the wrapper class for the return value (can be + :class:`Accept` or a subclass thereof) + :return: an instance of `cls`. + """ + if cls is None: + cls = Accept + + if not value: + return cls(None) + + result = [] + for match in _accept_re.finditer(value): + quality = match.group(2) + if not quality: + quality = 1 + else: + quality = max(min(float(quality), 1), 0) + result.append((match.group(1), quality)) + return cls(result) + + +def parse_cache_control_header(value, on_update=None, cls=None): + """Parse a cache control header. The RFC differs between response and + request cache control, this method does not. It's your responsibility + to not use the wrong control statements. + + .. versionadded:: 0.5 + The `cls` was added. If not specified an immutable + :class:`~werkzeug.datastructures.RequestCacheControl` is returned. + + :param value: a cache control header to be parsed. + :param on_update: an optional callable that is called every time a value + on the :class:`~werkzeug.datastructures.CacheControl` + object is changed. + :param cls: the class for the returned object. By default + :class:`~werkzeug.datastructures.RequestCacheControl` is used. + :return: a `cls` object. + """ + if cls is None: + cls = RequestCacheControl + if not value: + return cls(None, on_update) + return cls(parse_dict_header(value), on_update) + + +def parse_set_header(value, on_update=None): + """Parse a set-like header and return a + :class:`~werkzeug.datastructures.HeaderSet` object: + + >>> hs = parse_set_header('token, "quoted value"') + + The return value is an object that treats the items case-insensitively + and keeps the order of the items: + + >>> 'TOKEN' in hs + True + >>> hs.index('quoted value') + 1 + >>> hs + HeaderSet(['token', 'quoted value']) + + To create a header from the :class:`HeaderSet` again, use the + :func:`dump_header` function. + + :param value: a set header to be parsed. + :param on_update: an optional callable that is called every time a + value on the :class:`~werkzeug.datastructures.HeaderSet` + object is changed. + :return: a :class:`~werkzeug.datastructures.HeaderSet` + """ + if not value: + return HeaderSet(None, on_update) + return HeaderSet(parse_list_header(value), on_update) + + +def parse_authorization_header(value): + """Parse an HTTP basic/digest authorization header transmitted by the web + browser. The return value is either `None` if the header was invalid or + not given, otherwise an :class:`~werkzeug.datastructures.Authorization` + object. + + :param value: the authorization header to parse. + :return: a :class:`~werkzeug.datastructures.Authorization` object or `None`. + """ + if not value: + return + try: + auth_type, auth_info = value.split(None, 1) + auth_type = auth_type.lower() + except ValueError: + return + if auth_type == 'basic': + try: + username, password = auth_info.decode('base64').split(':', 1) + except Exception, e: + return + return Authorization('basic', {'username': username, + 'password': password}) + elif auth_type == 'digest': + auth_map = parse_dict_header(auth_info) + for key in 'username', 'realm', 'nonce', 'uri', 'response': + if not key in auth_map: + return + if 'qop' in auth_map: + if not auth_map.get('nc') or not auth_map.get('cnonce'): + return + return Authorization('digest', auth_map) + + +def parse_www_authenticate_header(value, on_update=None): + """Parse an HTTP WWW-Authenticate header into a + :class:`~werkzeug.datastructures.WWWAuthenticate` object. + + :param value: a WWW-Authenticate header to parse. + :param on_update: an optional callable that is called every time a value + on the :class:`~werkzeug.datastructures.WWWAuthenticate` + object is changed. + :return: a :class:`~werkzeug.datastructures.WWWAuthenticate` object. + """ + if not value: + return WWWAuthenticate(on_update=on_update) + try: + auth_type, auth_info = value.split(None, 1) + auth_type = auth_type.lower() + except (ValueError, AttributeError): + return WWWAuthenticate(value.strip().lower(), on_update=on_update) + return WWWAuthenticate(auth_type, parse_dict_header(auth_info), + on_update) + + +def parse_if_range_header(value): + """Parses an if-range header which can be an etag or a date. Returns + a :class:`~werkzeug.datastructures.IfRange` object. + + .. versionadded:: 0.7 + """ + if not value: + return IfRange() + date = parse_date(value) + if date is not None: + return IfRange(date=date) + # drop weakness information + return IfRange(unquote_etag(value)[0]) + + +def parse_range_header(value, make_inclusive=True): + """Parses a range header into a :class:`~werkzeug.datastructures.Range` + object. If the header is missing or malformed `None` is returned. + `ranges` is a list of ``(start, stop)`` tuples where the ranges are + non-inclusive. + + .. versionadded:: 0.7 + """ + if not value or '=' not in value: + return None + + ranges = [] + last_end = 0 + units, rng = value.split('=', 1) + units = units.strip().lower() + + for item in rng.split(','): + item = item.strip() + if '-' not in item: + return None + if item.startswith('-'): + if last_end < 0: + return None + begin = int(item) + end = None + last_end = -1 + elif '-' in item: + begin, end = item.split('-', 1) + begin = int(begin) + if begin < last_end or last_end < 0: + return None + if end: + end = int(end) + 1 + if begin >= end: + return None + else: + end = None + last_end = end + ranges.append((begin, end)) + + return Range(units, ranges) + + +def parse_content_range_header(value, on_update=None): + """Parses a range header into a + :class:`~werkzeug.datastructures.ContentRange` object or `None` if + parsing is not possible. + + .. versionadded:: 0.7 + + :param value: a content range header to be parsed. + :param on_update: an optional callable that is called every time a value + on the :class:`~werkzeug.datastructures.ContentRange` + object is changed. + """ + if value is None: + return None + try: + units, rangedef = (value or '').strip().split(None, 1) + except ValueError: + return None + + if '/' not in rangedef: + return None + rng, length = rangedef.split('/', 1) + if length == '*': + length = None + elif length.isdigit(): + length = int(length) + else: + return None + + if rng == '*': + return ContentRange(units, None, None, length, on_update=on_update) + elif '-' not in rng: + return None + + start, stop = rng.split('-', 1) + try: + start = int(start) + stop = int(stop) + 1 + except ValueError: + return None + + if is_byte_range_valid(start, stop, length): + return ContentRange(units, start, stop, length, on_update=on_update) + + +def quote_etag(etag, weak=False): + """Quote an etag. + + :param etag: the etag to quote. + :param weak: set to `True` to tag it "weak". + """ + if '"' in etag: + raise ValueError('invalid etag') + etag = '"%s"' % etag + if weak: + etag = 'w/' + etag + return etag + + +def unquote_etag(etag): + """Unquote a single etag: + + >>> unquote_etag('w/"bar"') + ('bar', True) + >>> unquote_etag('"bar"') + ('bar', False) + + :param etag: the etag identifier to unquote. + :return: a ``(etag, weak)`` tuple. + """ + if not etag: + return None, None + etag = etag.strip() + weak = False + if etag[:2] in ('w/', 'W/'): + weak = True + etag = etag[2:] + if etag[:1] == etag[-1:] == '"': + etag = etag[1:-1] + return etag, weak + + +def parse_etags(value): + """Parse an etag header. + + :param value: the tag header to parse + :return: an :class:`~werkzeug.datastructures.ETags` object. + """ + if not value: + return ETags() + strong = [] + weak = [] + end = len(value) + pos = 0 + while pos < end: + match = _etag_re.match(value, pos) + if match is None: + break + is_weak, quoted, raw = match.groups() + if raw == '*': + return ETags(star_tag=True) + elif quoted: + raw = quoted + if is_weak: + weak.append(raw) + else: + strong.append(raw) + pos = match.end() + return ETags(strong, weak) + + +def generate_etag(data): + """Generate an etag for some data.""" + return md5(data).hexdigest() + + +def parse_date(value): + """Parse one of the following date formats into a datetime object: + + .. sourcecode:: text + + Sun, 06 Nov 1994 08:49:37 GMT ; RFC 822, updated by RFC 1123 + Sunday, 06-Nov-94 08:49:37 GMT ; RFC 850, obsoleted by RFC 1036 + Sun Nov 6 08:49:37 1994 ; ANSI C's asctime() format + + If parsing fails the return value is `None`. + + :param value: a string with a supported date format. + :return: a :class:`datetime.datetime` object. + """ + if value: + t = parsedate_tz(value.strip()) + if t is not None: + try: + year = t[0] + # unfortunately that function does not tell us if two digit + # years were part of the string, or if they were prefixed + # with two zeroes. So what we do is to assume that 69-99 + # refer to 1900, and everything below to 2000 + if year >= 0 and year <= 68: + year += 2000 + elif year >= 69 and year <= 99: + year += 1900 + return datetime(*((year,) + t[1:7])) - \ + timedelta(seconds=t[-1] or 0) + except (ValueError, OverflowError): + return None + + +def cookie_date(expires=None): + """Formats the time to ensure compatibility with Netscape's cookie + standard. + + Accepts a floating point number expressed in seconds since the epoch in, a + datetime object or a timetuple. All times in UTC. The :func:`parse_date` + function can be used to parse such a date. + + Outputs a string in the format ``Wdy, DD-Mon-YYYY HH:MM:SS GMT``. + + :param expires: If provided that date is used, otherwise the current. + """ + return _dump_date(expires, '-') + + +def http_date(timestamp=None): + """Formats the time to match the RFC1123 date format. + + Accepts a floating point number expressed in seconds since the epoch in, a + datetime object or a timetuple. All times in UTC. The :func:`parse_date` + function can be used to parse such a date. + + Outputs a string in the format ``Wdy, DD Mon YYYY HH:MM:SS GMT``. + + :param timestamp: If provided that date is used, otherwise the current. + """ + return _dump_date(timestamp, ' ') + + +def is_resource_modified(environ, etag=None, data=None, last_modified=None): + """Convenience method for conditional requests. + + :param environ: the WSGI environment of the request to be checked. + :param etag: the etag for the response for comparison. + :param data: or alternatively the data of the response to automatically + generate an etag using :func:`generate_etag`. + :param last_modified: an optional date of the last modification. + :return: `True` if the resource was modified, otherwise `False`. + """ + if etag is None and data is not None: + etag = generate_etag(data) + elif data is not None: + raise TypeError('both data and etag given') + if environ['REQUEST_METHOD'] not in ('GET', 'HEAD'): + return False + + unmodified = False + if isinstance(last_modified, basestring): + last_modified = parse_date(last_modified) + + # ensure that microsecond is zero because the HTTP spec does not transmit + # that either and we might have some false positives. See issue #39 + if last_modified is not None: + last_modified = last_modified.replace(microsecond=0) + + modified_since = parse_date(environ.get('HTTP_IF_MODIFIED_SINCE')) + + if modified_since and last_modified and last_modified <= modified_since: + unmodified = True + if etag: + if_none_match = parse_etags(environ.get('HTTP_IF_NONE_MATCH')) + if if_none_match: + unmodified = if_none_match.contains_raw(etag) + + return not unmodified + + +def remove_entity_headers(headers, allowed=('expires', 'content-location')): + """Remove all entity headers from a list or :class:`Headers` object. This + operation works in-place. `Expires` and `Content-Location` headers are + by default not removed. The reason for this is :rfc:`2616` section + 10.3.5 which specifies some entity headers that should be sent. + + .. versionchanged:: 0.5 + added `allowed` parameter. + + :param headers: a list or :class:`Headers` object. + :param allowed: a list of headers that should still be allowed even though + they are entity headers. + """ + allowed = set(x.lower() for x in allowed) + headers[:] = [(key, value) for key, value in headers if + not is_entity_header(key) or key.lower() in allowed] + + +def remove_hop_by_hop_headers(headers): + """Remove all HTTP/1.1 "Hop-by-Hop" headers from a list or + :class:`Headers` object. This operation works in-place. + + .. versionadded:: 0.5 + + :param headers: a list or :class:`Headers` object. + """ + headers[:] = [(key, value) for key, value in headers if + not is_hop_by_hop_header(key)] + + +def is_entity_header(header): + """Check if a header is an entity header. + + .. versionadded:: 0.5 + + :param header: the header to test. + :return: `True` if it's an entity header, `False` otherwise. + """ + return header.lower() in _entity_headers + + +def is_hop_by_hop_header(header): + """Check if a header is an HTTP/1.1 "Hop-by-Hop" header. + + .. versionadded:: 0.5 + + :param header: the header to test. + :return: `True` if it's an entity header, `False` otherwise. + """ + return header.lower() in _hop_by_hop_headers + + +def parse_cookie(header, charset='utf-8', errors='replace', + cls=None): + """Parse a cookie. Either from a string or WSGI environ. + + Per default encoding errors are ignored. If you want a different behavior + you can set `errors` to ``'replace'`` or ``'strict'``. In strict mode a + :exc:`HTTPUnicodeError` is raised. + + .. versionchanged:: 0.5 + This function now returns a :class:`TypeConversionDict` instead of a + regular dict. The `cls` parameter was added. + + :param header: the header to be used to parse the cookie. Alternatively + this can be a WSGI environment. + :param charset: the charset for the cookie values. + :param errors: the error behavior for the charset decoding. + :param cls: an optional dict class to use. If this is not specified + or `None` the default :class:`TypeConversionDict` is + used. + """ + if isinstance(header, dict): + header = header.get('HTTP_COOKIE', '') + if cls is None: + cls = TypeConversionDict + cookie = _ExtendedCookie() + cookie.load(header) + result = {} + + # decode to unicode and skip broken items. Our extended morsel + # and extended cookie will catch CookieErrors and convert them to + # `None` items which we have to skip here. + for key, value in cookie.iteritems(): + if value.value is not None: + result[key] = _decode_unicode(unquote_header_value(value.value), + charset, errors) + + return cls(result) + + +def dump_cookie(key, value='', max_age=None, expires=None, path='/', + domain=None, secure=None, httponly=False, charset='utf-8', + sync_expires=True): + """Creates a new Set-Cookie header without the ``Set-Cookie`` prefix + The parameters are the same as in the cookie Morsel object in the + Python standard library but it accepts unicode data, too. + + :param max_age: should be a number of seconds, or `None` (default) if + the cookie should last only as long as the client's + browser session. Additionally `timedelta` objects + are accepted, too. + :param expires: should be a `datetime` object or unix timestamp. + :param path: limits the cookie to a given path, per default it will + span the whole domain. + :param domain: Use this if you want to set a cross-domain cookie. For + example, ``domain=".example.com"`` will set a cookie + that is readable by the domain ``www.example.com``, + ``foo.example.com`` etc. Otherwise, a cookie will only + be readable by the domain that set it. + :param secure: The cookie will only be available via HTTPS + :param httponly: disallow JavaScript to access the cookie. This is an + extension to the cookie standard and probably not + supported by all browsers. + :param charset: the encoding for unicode values. + :param sync_expires: automatically set expires if max_age is defined + but expires not. + """ + try: + key = str(key) + except UnicodeError: + raise TypeError('invalid key %r' % key) + if isinstance(value, unicode): + value = value.encode(charset) + value = quote_header_value(value) + morsel = _ExtendedMorsel(key, value) + if isinstance(max_age, timedelta): + max_age = (max_age.days * 60 * 60 * 24) + max_age.seconds + if expires is not None: + if not isinstance(expires, basestring): + expires = cookie_date(expires) + morsel['expires'] = expires + elif max_age is not None and sync_expires: + morsel['expires'] = cookie_date(time() + max_age) + if domain and ':' in domain: + # The port part of the domain should NOT be used. Strip it + domain = domain.split(':', 1)[0] + if domain: + assert '.' in domain, ( + "Setting \"domain\" for a cookie on a server running localy (ex: " + "localhost) is not supportted by complying browsers. You should " + "have something like: \"127.0.0.1 localhost dev.localhost\" on " + "your hosts file and then point your server to run on " + "\"dev.localhost\" and also set \"domain\" for \"dev.localhost\"" + ) + for k, v in (('path', path), ('domain', domain), ('secure', secure), + ('max-age', max_age), ('httponly', httponly)): + if v is not None and v is not False: + morsel[k] = str(v) + return morsel.output(header='').lstrip() + + +def is_byte_range_valid(start, stop, length): + """Checks if a given byte content range is valid for the given length. + + .. versionadded:: 0.7 + """ + if (start is None) != (stop is None): + return False + elif start is None: + return length is None or length >= 0 + elif length is None: + return 0 <= start < stop + elif start >= stop: + return False + return 0 <= start < length + + +# circular dependency fun +from werkzeug.datastructures import Accept, HeaderSet, ETags, Authorization, \ + WWWAuthenticate, TypeConversionDict, IfRange, Range, ContentRange, \ + RequestCacheControl + + +# DEPRECATED +# backwards compatible imports +from werkzeug.datastructures import MIMEAccept, CharsetAccept, \ + LanguageAccept, Headers diff --git a/websdk/werkzeug/local.py b/websdk/werkzeug/local.py new file mode 100644 index 0000000..c5507bb --- /dev/null +++ b/websdk/werkzeug/local.py @@ -0,0 +1,394 @@ +# -*- coding: utf-8 -*- +""" + werkzeug.local + ~~~~~~~~~~~~~~ + + This module implements context-local objects. + + :copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" +from werkzeug.wsgi import ClosingIterator +from werkzeug._internal import _patch_wrapper + +# since each thread has its own greenlet we can just use those as identifiers +# for the context. If greenlets are not available we fall back to the +# current thread ident. +try: + from greenlet import getcurrent as get_ident +except ImportError: # pragma: no cover + try: + from thread import get_ident + except ImportError: # pragma: no cover + from dummy_thread import get_ident + + +def release_local(local): + """Releases the contents of the local for the current context. + This makes it possible to use locals without a manager. + + Example:: + + >>> loc = Local() + >>> loc.foo = 42 + >>> release_local(loc) + >>> hasattr(loc, 'foo') + False + + With this function one can release :class:`Local` objects as well + as :class:`StackLocal` objects. However it is not possible to + release data held by proxies that way, one always has to retain + a reference to the underlying local object in order to be able + to release it. + + .. versionadded:: 0.6.1 + """ + local.__release_local__() + + +class Local(object): + __slots__ = ('__storage__', '__ident_func__') + + def __init__(self): + object.__setattr__(self, '__storage__', {}) + object.__setattr__(self, '__ident_func__', get_ident) + + def __iter__(self): + return iter(self.__storage__.items()) + + def __call__(self, proxy): + """Create a proxy for a name.""" + return LocalProxy(self, proxy) + + def __release_local__(self): + self.__storage__.pop(self.__ident_func__(), None) + + def __getattr__(self, name): + try: + return self.__storage__[self.__ident_func__()][name] + except KeyError: + raise AttributeError(name) + + def __setattr__(self, name, value): + ident = self.__ident_func__() + storage = self.__storage__ + try: + storage[ident][name] = value + except KeyError: + storage[ident] = {name: value} + + def __delattr__(self, name): + try: + del self.__storage__[self.__ident_func__()][name] + except KeyError: + raise AttributeError(name) + + +class LocalStack(object): + """This class works similar to a :class:`Local` but keeps a stack + of objects instead. This is best explained with an example:: + + >>> ls = LocalStack() + >>> ls.push(42) + >>> ls.top + 42 + >>> ls.push(23) + >>> ls.top + 23 + >>> ls.pop() + 23 + >>> ls.top + 42 + + They can be force released by using a :class:`LocalManager` or with + the :func:`release_local` function but the correct way is to pop the + item from the stack after using. When the stack is empty it will + no longer be bound to the current context (and as such released). + + By calling the stack without arguments it returns a proxy that resolves to + the topmost item on the stack. + + .. versionadded:: 0.6.1 + """ + + def __init__(self): + self._local = Local() + + def __release_local__(self): + self._local.__release_local__() + + def _get__ident_func__(self): + return self._local.__ident_func__ + def _set__ident_func__(self, value): + object.__setattr__(self._local, '__ident_func__', value) + __ident_func__ = property(_get__ident_func__, _set__ident_func__) + del _get__ident_func__, _set__ident_func__ + + def __call__(self): + def _lookup(): + rv = self.top + if rv is None: + raise RuntimeError('object unbound') + return rv + return LocalProxy(_lookup) + + def push(self, obj): + """Pushes a new item to the stack""" + rv = getattr(self._local, 'stack', None) + if rv is None: + self._local.stack = rv = [] + rv.append(obj) + return rv + + def pop(self): + """Removes the topmost item from the stack, will return the + old value or `None` if the stack was already empty. + """ + stack = getattr(self._local, 'stack', None) + if stack is None: + return None + elif len(stack) == 1: + release_local(self._local) + return stack[-1] + else: + return stack.pop() + + @property + def top(self): + """The topmost item on the stack. If the stack is empty, + `None` is returned. + """ + try: + return self._local.stack[-1] + except (AttributeError, IndexError): + return None + + +class LocalManager(object): + """Local objects cannot manage themselves. For that you need a local + manager. You can pass a local manager multiple locals or add them later + by appending them to `manager.locals`. Everytime the manager cleans up + it, will clean up all the data left in the locals for this context. + + The `ident_func` parameter can be added to override the default ident + function for the wrapped locals. + + .. versionchanged:: 0.6.1 + Instead of a manager the :func:`release_local` function can be used + as well. + + .. versionchanged:: 0.7 + `ident_func` was added. + """ + + def __init__(self, locals=None, ident_func=None): + if locals is None: + self.locals = [] + elif isinstance(locals, Local): + self.locals = [locals] + else: + self.locals = list(locals) + if ident_func is not None: + self.ident_func = ident_func + for local in self.locals: + object.__setattr__(local, '__ident_func__', ident_func) + else: + self.ident_func = get_ident + + def get_ident(self): + """Return the context identifier the local objects use internally for + this context. You cannot override this method to change the behavior + but use it to link other context local objects (such as SQLAlchemy's + scoped sessions) to the Werkzeug locals. + + .. versionchanged:: 0.7 + Yu can pass a different ident function to the local manager that + will then be propagated to all the locals passed to the + constructor. + """ + return self.ident_func() + + def cleanup(self): + """Manually clean up the data in the locals for this context. Call + this at the end of the request or use `make_middleware()`. + """ + for local in self.locals: + release_local(local) + + def make_middleware(self, app): + """Wrap a WSGI application so that cleaning up happens after + request end. + """ + def application(environ, start_response): + return ClosingIterator(app(environ, start_response), self.cleanup) + return application + + def middleware(self, func): + """Like `make_middleware` but for decorating functions. + + Example usage:: + + @manager.middleware + def application(environ, start_response): + ... + + The difference to `make_middleware` is that the function passed + will have all the arguments copied from the inner application + (name, docstring, module). + """ + return _patch_wrapper(func, self.make_middleware(func)) + + def __repr__(self): + return '<%s storages: %d>' % ( + self.__class__.__name__, + len(self.locals) + ) + + +class LocalProxy(object): + """Acts as a proxy for a werkzeug local. Forwards all operations to + a proxied object. The only operations not supported for forwarding + are right handed operands and any kind of assignment. + + Example usage:: + + from werkzeug.local import Local + l = Local() + + # these are proxies + request = l('request') + user = l('user') + + + from werkzeug.local import LocalStack + _response_local = LocalStack() + + # this is a proxy + response = _response_local() + + Whenever something is bound to l.user / l.request the proxy objects + will forward all operations. If no object is bound a :exc:`RuntimeError` + will be raised. + + To create proxies to :class:`Local` or :class:`LocalStack` objects, + call the object as shown above. If you want to have a proxy to an + object looked up by a function, you can (as of Werkzeug 0.6.1) pass + a function to the :class:`LocalProxy` constructor:: + + session = LocalProxy(lambda: get_current_request().session) + + .. versionchanged:: 0.6.1 + The class can be instanciated with a callable as well now. + """ + __slots__ = ('__local', '__dict__', '__name__') + + def __init__(self, local, name=None): + object.__setattr__(self, '_LocalProxy__local', local) + object.__setattr__(self, '__name__', name) + + def _get_current_object(self): + """Return the current object. This is useful if you want the real + object behind the proxy at a time for performance reasons or because + you want to pass the object into a different context. + """ + if not hasattr(self.__local, '__release_local__'): + return self.__local() + try: + return getattr(self.__local, self.__name__) + except AttributeError: + raise RuntimeError('no object bound to %s' % self.__name__) + + @property + def __dict__(self): + try: + return self._get_current_object().__dict__ + except RuntimeError: + raise AttributeError('__dict__') + + def __repr__(self): + try: + obj = self._get_current_object() + except RuntimeError: + return '<%s unbound>' % self.__class__.__name__ + return repr(obj) + + def __nonzero__(self): + try: + return bool(self._get_current_object()) + except RuntimeError: + return False + + def __unicode__(self): + try: + return unicode(self._get_current_object()) + except RuntimeError: + return repr(self) + + def __dir__(self): + try: + return dir(self._get_current_object()) + except RuntimeError: + return [] + + def __getattr__(self, name): + if name == '__members__': + return dir(self._get_current_object()) + return getattr(self._get_current_object(), name) + + def __setitem__(self, key, value): + self._get_current_object()[key] = value + + def __delitem__(self, key): + del self._get_current_object()[key] + + def __setslice__(self, i, j, seq): + self._get_current_object()[i:j] = seq + + def __delslice__(self, i, j): + del self._get_current_object()[i:j] + + __setattr__ = lambda x, n, v: setattr(x._get_current_object(), n, v) + __delattr__ = lambda x, n: delattr(x._get_current_object(), n) + __str__ = lambda x: str(x._get_current_object()) + __lt__ = lambda x, o: x._get_current_object() < o + __le__ = lambda x, o: x._get_current_object() <= o + __eq__ = lambda x, o: x._get_current_object() == o + __ne__ = lambda x, o: x._get_current_object() != o + __gt__ = lambda x, o: x._get_current_object() > o + __ge__ = lambda x, o: x._get_current_object() >= o + __cmp__ = lambda x, o: cmp(x._get_current_object(), o) + __hash__ = lambda x: hash(x._get_current_object()) + __call__ = lambda x, *a, **kw: x._get_current_object()(*a, **kw) + __len__ = lambda x: len(x._get_current_object()) + __getitem__ = lambda x, i: x._get_current_object()[i] + __iter__ = lambda x: iter(x._get_current_object()) + __contains__ = lambda x, i: i in x._get_current_object() + __getslice__ = lambda x, i, j: x._get_current_object()[i:j] + __add__ = lambda x, o: x._get_current_object() + o + __sub__ = lambda x, o: x._get_current_object() - o + __mul__ = lambda x, o: x._get_current_object() * o + __floordiv__ = lambda x, o: x._get_current_object() // o + __mod__ = lambda x, o: x._get_current_object() % o + __divmod__ = lambda x, o: x._get_current_object().__divmod__(o) + __pow__ = lambda x, o: x._get_current_object() ** o + __lshift__ = lambda x, o: x._get_current_object() << o + __rshift__ = lambda x, o: x._get_current_object() >> o + __and__ = lambda x, o: x._get_current_object() & o + __xor__ = lambda x, o: x._get_current_object() ^ o + __or__ = lambda x, o: x._get_current_object() | o + __div__ = lambda x, o: x._get_current_object().__div__(o) + __truediv__ = lambda x, o: x._get_current_object().__truediv__(o) + __neg__ = lambda x: -(x._get_current_object()) + __pos__ = lambda x: +(x._get_current_object()) + __abs__ = lambda x: abs(x._get_current_object()) + __invert__ = lambda x: ~(x._get_current_object()) + __complex__ = lambda x: complex(x._get_current_object()) + __int__ = lambda x: int(x._get_current_object()) + __long__ = lambda x: long(x._get_current_object()) + __float__ = lambda x: float(x._get_current_object()) + __oct__ = lambda x: oct(x._get_current_object()) + __hex__ = lambda x: hex(x._get_current_object()) + __index__ = lambda x: x._get_current_object().__index__() + __coerce__ = lambda x, o: x.__coerce__(x, o) + __enter__ = lambda x: x.__enter__() + __exit__ = lambda x, *a, **kw: x.__exit__(*a, **kw) diff --git a/websdk/werkzeug/posixemulation.py b/websdk/werkzeug/posixemulation.py new file mode 100644 index 0000000..d131d23 --- /dev/null +++ b/websdk/werkzeug/posixemulation.py @@ -0,0 +1,105 @@ +# -*- coding: utf-8 -*- +r""" + werkzeug.posixemulation + ~~~~~~~~~~~~~~~~~~~~~~~ + + Provides a POSIX emulation for some features that are relevant to + web applications. The main purpose is to simplify support for + systems such as Windows NT that are not 100% POSIX compatible. + + Currently this only implements a :func:`rename` function that + follows POSIX semantics. Eg: if the target file already exists it + will be replaced without asking. + + This module was introduced in 0.6.1 and is not a public interface. + It might become one in later versions of Werkzeug. + + :copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" +import sys +import os +import errno +import time +import random + + +can_rename_open_file = False +if os.name == 'nt': # pragma: no cover + _rename = lambda src, dst: False + _rename_atomic = lambda src, dst: False + + try: + import ctypes + + _MOVEFILE_REPLACE_EXISTING = 0x1 + _MOVEFILE_WRITE_THROUGH = 0x8 + _MoveFileEx = ctypes.windll.kernel32.MoveFileExW + + def _rename(src, dst): + if not isinstance(src, unicode): + src = unicode(src, sys.getfilesystemencoding()) + if not isinstance(dst, unicode): + dst = unicode(dst, sys.getfilesystemencoding()) + if _rename_atomic(src, dst): + return True + retry = 0 + rv = False + while not rv and retry < 100: + rv = _MoveFileEx(src, dst, _MOVEFILE_REPLACE_EXISTING | + _MOVEFILE_WRITE_THROUGH) + if not rv: + time.sleep(0.001) + retry += 1 + return rv + + # new in Vista and Windows Server 2008 + _CreateTransaction = ctypes.windll.ktmw32.CreateTransaction + _CommitTransaction = ctypes.windll.ktmw32.CommitTransaction + _MoveFileTransacted = ctypes.windll.kernel32.MoveFileTransactedW + _CloseHandle = ctypes.windll.kernel32.CloseHandle + can_rename_open_file = True + + def _rename_atomic(src, dst): + ta = _CreateTransaction(None, 0, 0, 0, 0, 1000, 'Werkzeug rename') + if ta == -1: + return False + try: + retry = 0 + rv = False + while not rv and retry < 100: + rv = _MoveFileTransacted(src, dst, None, None, + _MOVEFILE_REPLACE_EXISTING | + _MOVEFILE_WRITE_THROUGH, ta) + if rv: + rv = _CommitTransaction(ta) + break + else: + time.sleep(0.001) + retry += 1 + return rv + finally: + _CloseHandle(ta) + except Exception: + pass + + def rename(src, dst): + # Try atomic or pseudo-atomic rename + if _rename(src, dst): + return + # Fall back to "move away and replace" + try: + os.rename(src, dst) + except OSError, e: + if e.errno != errno.EEXIST: + raise + old = "%s-%08x" % (dst, random.randint(0, sys.maxint)) + os.rename(dst, old) + os.rename(src, dst) + try: + os.unlink(old) + except Exception: + pass +else: + rename = os.rename + can_rename_open_file = True diff --git a/websdk/werkzeug/routing.py b/websdk/werkzeug/routing.py new file mode 100644 index 0000000..9f8ca01 --- /dev/null +++ b/websdk/werkzeug/routing.py @@ -0,0 +1,1620 @@ +# -*- coding: utf-8 -*- +""" + werkzeug.routing + ~~~~~~~~~~~~~~~~ + + When it comes to combining multiple controller or view functions (however + you want to call them) you need a dispatcher. A simple way would be + applying regular expression tests on the ``PATH_INFO`` and calling + registered callback functions that return the value then. + + This module implements a much more powerful system than simple regular + expression matching because it can also convert values in the URLs and + build URLs. + + Here a simple example that creates an URL map for an application with + two subdomains (www and kb) and some URL rules: + + >>> m = Map([ + ... # Static URLs + ... Rule('/', endpoint='static/index'), + ... Rule('/about', endpoint='static/about'), + ... Rule('/help', endpoint='static/help'), + ... # Knowledge Base + ... Subdomain('kb', [ + ... Rule('/', endpoint='kb/index'), + ... Rule('/browse/', endpoint='kb/browse'), + ... Rule('/browse//', endpoint='kb/browse'), + ... Rule('/browse//', endpoint='kb/browse') + ... ]) + ... ], default_subdomain='www') + + If the application doesn't use subdomains it's perfectly fine to not set + the default subdomain and not use the `Subdomain` rule factory. The endpoint + in the rules can be anything, for example import paths or unique + identifiers. The WSGI application can use those endpoints to get the + handler for that URL. It doesn't have to be a string at all but it's + recommended. + + Now it's possible to create a URL adapter for one of the subdomains and + build URLs: + + >>> c = m.bind('example.com') + >>> c.build("kb/browse", dict(id=42)) + 'http://kb.example.com/browse/42/' + >>> c.build("kb/browse", dict()) + 'http://kb.example.com/browse/' + >>> c.build("kb/browse", dict(id=42, page=3)) + 'http://kb.example.com/browse/42/3' + >>> c.build("static/about") + '/about' + >>> c.build("static/index", force_external=True) + 'http://www.example.com/' + + >>> c = m.bind('example.com', subdomain='kb') + >>> c.build("static/about") + 'http://www.example.com/about' + + The first argument to bind is the server name *without* the subdomain. + Per default it will assume that the script is mounted on the root, but + often that's not the case so you can provide the real mount point as + second argument: + + >>> c = m.bind('example.com', '/applications/example') + + The third argument can be the subdomain, if not given the default + subdomain is used. For more details about binding have a look at the + documentation of the `MapAdapter`. + + And here is how you can match URLs: + + >>> c = m.bind('example.com') + >>> c.match("/") + ('static/index', {}) + >>> c.match("/about") + ('static/about', {}) + >>> c = m.bind('example.com', '/', 'kb') + >>> c.match("/") + ('kb/index', {}) + >>> c.match("/browse/42/23") + ('kb/browse', {'id': 42, 'page': 23}) + + If matching fails you get a `NotFound` exception, if the rule thinks + it's a good idea to redirect (for example because the URL was defined + to have a slash at the end but the request was missing that slash) it + will raise a `RequestRedirect` exception. Both are subclasses of the + `HTTPException` so you can use those errors as responses in the + application. + + If matching succeeded but the URL rule was incompatible to the given + method (for example there were only rules for `GET` and `HEAD` and + routing system tried to match a `POST` request) a `MethodNotAllowed` + method is raised. + + + :copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" +import re +import posixpath +from pprint import pformat +from urlparse import urljoin + +from werkzeug.urls import url_encode, url_decode, url_quote +from werkzeug.utils import redirect, format_string +from werkzeug.exceptions import HTTPException, NotFound, MethodNotAllowed +from werkzeug._internal import _get_environ +from werkzeug.datastructures import ImmutableDict, MultiDict + + +_rule_re = re.compile(r''' + (?P[^<]*) # static rule data + < + (?: + (?P[a-zA-Z_][a-zA-Z0-9_]*) # converter name + (?:\((?P.*?)\))? # converter arguments + \: # variable delimiter + )? + (?P[a-zA-Z_][a-zA-Z0-9_]*) # variable name + > +''', re.VERBOSE) +_simple_rule_re = re.compile(r'<([^>]+)>') +_converter_args_re = re.compile(r''' + ((?P\w+)\s*=\s*)? + (?P + True|False| + \d+.\d+| + \d+.| + \d+| + \w+| + [urUR]?(?P"[^"]*?"|'[^']*') + )\s*, +''', re.VERBOSE|re.UNICODE) + + +_PYTHON_CONSTANTS = { + 'None': None, + 'True': True, + 'False': False +} + + +def _pythonize(value): + if value in _PYTHON_CONSTANTS: + return _PYTHON_CONSTANTS[value] + for convert in int, float: + try: + return convert(value) + except ValueError: + pass + if value[:1] == value[-1:] and value[0] in '"\'': + value = value[1:-1] + return unicode(value) + + +def parse_converter_args(argstr): + argstr += ',' + args = [] + kwargs = {} + + for item in _converter_args_re.finditer(argstr): + value = item.group('stringval') + if value is None: + value = item.group('value') + value = _pythonize(value) + if not item.group('name'): + args.append(value) + else: + name = item.group('name') + kwargs[name] = value + + return tuple(args), kwargs + + +def parse_rule(rule): + """Parse a rule and return it as generator. Each iteration yields tuples + in the form ``(converter, arguments, variable)``. If the converter is + `None` it's a static url part, otherwise it's a dynamic one. + + :internal: + """ + pos = 0 + end = len(rule) + do_match = _rule_re.match + used_names = set() + while pos < end: + m = do_match(rule, pos) + if m is None: + break + data = m.groupdict() + if data['static']: + yield None, None, data['static'] + variable = data['variable'] + converter = data['converter'] or 'default' + if variable in used_names: + raise ValueError('variable name %r used twice.' % variable) + used_names.add(variable) + yield converter, data['args'] or None, variable + pos = m.end() + if pos < end: + remaining = rule[pos:] + if '>' in remaining or '<' in remaining: + raise ValueError('malformed url rule: %r' % rule) + yield None, None, remaining + + +def get_converter(map, name, args): + """Create a new converter for the given arguments or raise + exception if the converter does not exist. + + :internal: + """ + if not name in map.converters: + raise LookupError('the converter %r does not exist' % name) + if args: + args, kwargs = parse_converter_args(args) + else: + args = () + kwargs = {} + return map.converters[name](map, *args, **kwargs) + + +class RoutingException(Exception): + """Special exceptions that require the application to redirect, notifying + about missing urls, etc. + + :internal: + """ + + +class RequestRedirect(HTTPException, RoutingException): + """Raise if the map requests a redirect. This is for example the case if + `strict_slashes` are activated and an url that requires a trailing slash. + + The attribute `new_url` contains the absolute destination url. + """ + code = 301 + + def __init__(self, new_url): + RoutingException.__init__(self, new_url) + self.new_url = new_url + + def get_response(self, environ): + return redirect(self.new_url, 301) + + +class RequestSlash(RoutingException): + """Internal exception.""" + + +class RequestAliasRedirect(RoutingException): + """This rule is an alias and wants to redirect to the canonical URL.""" + + def __init__(self, matched_values): + self.matched_values = matched_values + + +class BuildError(RoutingException, LookupError): + """Raised if the build system cannot find a URL for an endpoint with the + values provided. + """ + + def __init__(self, endpoint, values, method): + LookupError.__init__(self, endpoint, values, method) + self.endpoint = endpoint + self.values = values + self.method = method + + +class ValidationError(ValueError): + """Validation error. If a rule converter raises this exception the rule + does not match the current URL and the next URL is tried. + """ + + +class RuleFactory(object): + """As soon as you have more complex URL setups it's a good idea to use rule + factories to avoid repetitive tasks. Some of them are builtin, others can + be added by subclassing `RuleFactory` and overriding `get_rules`. + """ + + def get_rules(self, map): + """Subclasses of `RuleFactory` have to override this method and return + an iterable of rules.""" + raise NotImplementedError() + + +class Subdomain(RuleFactory): + """All URLs provided by this factory have the subdomain set to a + specific domain. For example if you want to use the subdomain for + the current language this can be a good setup:: + + url_map = Map([ + Rule('/', endpoint='#select_language'), + Subdomain('', [ + Rule('/', endpoint='index'), + Rule('/about', endpoint='about'), + Rule('/help', endpoint='help') + ]) + ]) + + All the rules except for the ``'#select_language'`` endpoint will now + listen on a two letter long subdomain that holds the language code + for the current request. + """ + + def __init__(self, subdomain, rules): + self.subdomain = subdomain + self.rules = rules + + def get_rules(self, map): + for rulefactory in self.rules: + for rule in rulefactory.get_rules(map): + rule = rule.empty() + rule.subdomain = self.subdomain + yield rule + + +class Submount(RuleFactory): + """Like `Subdomain` but prefixes the URL rule with a given string:: + + url_map = Map([ + Rule('/', endpoint='index'), + Submount('/blog', [ + Rule('/', endpoint='blog/index'), + Rule('/entry/', endpoint='blog/show') + ]) + ]) + + Now the rule ``'blog/show'`` matches ``/blog/entry/``. + """ + + def __init__(self, path, rules): + self.path = path.rstrip('/') + self.rules = rules + + def get_rules(self, map): + for rulefactory in self.rules: + for rule in rulefactory.get_rules(map): + rule = rule.empty() + rule.rule = self.path + rule.rule + yield rule + + +class EndpointPrefix(RuleFactory): + """Prefixes all endpoints (which must be strings for this factory) with + another string. This can be useful for sub applications:: + + url_map = Map([ + Rule('/', endpoint='index'), + EndpointPrefix('blog/', [Submount('/blog', [ + Rule('/', endpoint='index'), + Rule('/entry/', endpoint='show') + ])]) + ]) + """ + + def __init__(self, prefix, rules): + self.prefix = prefix + self.rules = rules + + def get_rules(self, map): + for rulefactory in self.rules: + for rule in rulefactory.get_rules(map): + rule = rule.empty() + rule.endpoint = self.prefix + rule.endpoint + yield rule + + +class RuleTemplate(object): + """Returns copies of the rules wrapped and expands string templates in + the endpoint, rule, defaults or subdomain sections. + + Here a small example for such a rule template:: + + from werkzeug.routing import Map, Rule, RuleTemplate + + resource = RuleTemplate([ + Rule('/$name/', endpoint='$name.list'), + Rule('/$name/', endpoint='$name.show') + ]) + + url_map = Map([resource(name='user'), resource(name='page')]) + + When a rule template is called the keyword arguments are used to + replace the placeholders in all the string parameters. + """ + + def __init__(self, rules): + self.rules = list(rules) + + def __call__(self, *args, **kwargs): + return RuleTemplateFactory(self.rules, dict(*args, **kwargs)) + + +class RuleTemplateFactory(RuleFactory): + """A factory that fills in template variables into rules. Used by + `RuleTemplate` internally. + + :internal: + """ + + def __init__(self, rules, context): + self.rules = rules + self.context = context + + def get_rules(self, map): + for rulefactory in self.rules: + for rule in rulefactory.get_rules(map): + new_defaults = subdomain = None + if rule.defaults: + new_defaults = {} + for key, value in rule.defaults.iteritems(): + if isinstance(value, basestring): + value = format_string(value, self.context) + new_defaults[key] = value + if rule.subdomain is not None: + subdomain = format_string(rule.subdomain, self.context) + new_endpoint = rule.endpoint + if isinstance(new_endpoint, basestring): + new_endpoint = format_string(new_endpoint, self.context) + yield Rule( + format_string(rule.rule, self.context), + new_defaults, + subdomain, + rule.methods, + rule.build_only, + new_endpoint, + rule.strict_slashes + ) + + +class Rule(RuleFactory): + """A Rule represents one URL pattern. There are some options for `Rule` + that change the way it behaves and are passed to the `Rule` constructor. + Note that besides the rule-string all arguments *must* be keyword arguments + in order to not break the application on Werkzeug upgrades. + + `string` + Rule strings basically are just normal URL paths with placeholders in + the format ```` where the converter and the + arguments are optional. If no converter is defined the `default` + converter is used which means `string` in the normal configuration. + + URL rules that end with a slash are branch URLs, others are leaves. + If you have `strict_slashes` enabled (which is the default), all + branch URLs that are matched without a trailing slash will trigger a + redirect to the same URL with the missing slash appended. + + The converters are defined on the `Map`. + + `endpoint` + The endpoint for this rule. This can be anything. A reference to a + function, a string, a number etc. The preferred way is using a string + because the endpoint is used for URL generation. + + `defaults` + An optional dict with defaults for other rules with the same endpoint. + This is a bit tricky but useful if you want to have unique URLs:: + + url_map = Map([ + Rule('/all/', defaults={'page': 1}, endpoint='all_entries'), + Rule('/all/page/', endpoint='all_entries') + ]) + + If a user now visits ``http://example.com/all/page/1`` he will be + redirected to ``http://example.com/all/``. If `redirect_defaults` is + disabled on the `Map` instance this will only affect the URL + generation. + + `subdomain` + The subdomain rule string for this rule. If not specified the rule + only matches for the `default_subdomain` of the map. If the map is + not bound to a subdomain this feature is disabled. + + Can be useful if you want to have user profiles on different subdomains + and all subdomains are forwarded to your application:: + + url_map = Map([ + Rule('/', subdomain='', endpoint='user/homepage'), + Rule('/stats', subdomain='', endpoint='user/stats') + ]) + + `methods` + A sequence of http methods this rule applies to. If not specified, all + methods are allowed. For example this can be useful if you want different + endpoints for `POST` and `GET`. If methods are defined and the path + matches but the method matched against is not in this list or in the + list of another rule for that path the error raised is of the type + `MethodNotAllowed` rather than `NotFound`. If `GET` is present in the + list of methods and `HEAD` is not, `HEAD` is added automatically. + + .. versionchanged:: 0.6.1 + `HEAD` is now automatically added to the methods if `GET` is + present. The reason for this is that existing code often did not + work properly in servers not rewriting `HEAD` to `GET` + automatically and it was not documented how `HEAD` should be + treated. This was considered a bug in Werkzeug because of that. + + `strict_slashes` + Override the `Map` setting for `strict_slashes` only for this rule. If + not specified the `Map` setting is used. + + `build_only` + Set this to True and the rule will never match but will create a URL + that can be build. This is useful if you have resources on a subdomain + or folder that are not handled by the WSGI application (like static data) + + `redirect_to` + If given this must be either a string or callable. In case of a + callable it's called with the url adapter that triggered the match and + the values of the URL as keyword arguments and has to return the target + for the redirect, otherwise it has to be a string with placeholders in + rule syntax:: + + def foo_with_slug(adapter, id): + # ask the database for the slug for the old id. this of + # course has nothing to do with werkzeug. + return 'foo/' + Foo.get_slug_for_id(id) + + url_map = Map([ + Rule('/foo/', endpoint='foo'), + Rule('/some/old/url/', redirect_to='foo/'), + Rule('/other/old/url/', redirect_to=foo_with_slug) + ]) + + When the rule is matched the routing system will raise a + `RequestRedirect` exception with the target for the redirect. + + Keep in mind that the URL will be joined against the URL root of the + script so don't use a leading slash on the target URL unless you + really mean root of that domain. + + `alias` + If enabled this rule serves as an alias for another rule with the same + endpoint and arguments. + + `host` + If provided and the URL map has host matching enabled this can be + used to provide a match rule for the whole host. This also means + that the subdomain feature is disabled. + + .. versionadded:: 0.7 + The `alias` and `host` parameters were added. + """ + + def __init__(self, string, defaults=None, subdomain=None, methods=None, + build_only=False, endpoint=None, strict_slashes=None, + redirect_to=None, alias=False, host=None): + if not string.startswith('/'): + raise ValueError('urls must start with a leading slash') + self.rule = string + self.is_leaf = not string.endswith('/') + + self.map = None + self.strict_slashes = strict_slashes + self.subdomain = subdomain + self.host = host + self.defaults = defaults + self.build_only = build_only + self.alias = alias + if methods is None: + self.methods = None + else: + self.methods = set([x.upper() for x in methods]) + if 'HEAD' not in self.methods and 'GET' in self.methods: + self.methods.add('HEAD') + self.endpoint = endpoint + self.redirect_to = redirect_to + + if defaults: + self.arguments = set(map(str, defaults)) + else: + self.arguments = set() + self._trace = self._converters = self._regex = self._weights = None + + def empty(self): + """Return an unbound copy of this rule. This can be useful if you + want to reuse an already bound URL for another map.""" + defaults = None + if self.defaults: + defaults = dict(self.defaults) + return Rule(self.rule, defaults, self.subdomain, self.methods, + self.build_only, self.endpoint, self.strict_slashes, + self.redirect_to, self.alias, self.host) + + def get_rules(self, map): + yield self + + def refresh(self): + """Rebinds and refreshes the URL. Call this if you modified the + rule in place. + + :internal: + """ + self.bind(self.map, rebind=True) + + def bind(self, map, rebind=False): + """Bind the url to a map and create a regular expression based on + the information from the rule itself and the defaults from the map. + + :internal: + """ + if self.map is not None and not rebind: + raise RuntimeError('url rule %r already bound to map %r' % + (self, self.map)) + self.map = map + if self.strict_slashes is None: + self.strict_slashes = map.strict_slashes + if self.subdomain is None: + self.subdomain = map.default_subdomain + self.compile() + + def compile(self): + """Compiles the regular expression and stores it.""" + assert self.map is not None, 'rule not bound' + + if self.map.host_matching: + domain_rule = self.host or '' + else: + domain_rule = self.subdomain or '' + + self._trace = [] + self._converters = {} + self._weights = [] + regex_parts = [] + + def _build_regex(rule): + for converter, arguments, variable in parse_rule(rule): + if converter is None: + regex_parts.append(re.escape(variable)) + self._trace.append((False, variable)) + for part in variable.split('/'): + if part: + self._weights.append((0, -len(part))) + else: + convobj = get_converter(self.map, converter, arguments) + regex_parts.append('(?P<%s>%s)' % (variable, convobj.regex)) + self._converters[variable] = convobj + self._trace.append((True, variable)) + self._weights.append((1, convobj.weight)) + self.arguments.add(str(variable)) + + _build_regex(domain_rule) + regex_parts.append('\\|') + self._trace.append((False, '|')) + _build_regex(self.is_leaf and self.rule or self.rule.rstrip('/')) + if not self.is_leaf: + self._trace.append((False, '/')) + + if self.build_only: + return + regex = r'^%s%s$' % ( + u''.join(regex_parts), + (not self.is_leaf or not self.strict_slashes) and \ + '(?/?)' or '' + ) + self._regex = re.compile(regex, re.UNICODE) + + def match(self, path): + """Check if the rule matches a given path. Path is a string in the + form ``"subdomain|/path(method)"`` and is assembled by the map. If + the map is doing host matching the subdomain part will be the host + instead. + + If the rule matches a dict with the converted values is returned, + otherwise the return value is `None`. + + :internal: + """ + if not self.build_only: + m = self._regex.search(path) + if m is not None: + groups = m.groupdict() + # we have a folder like part of the url without a trailing + # slash and strict slashes enabled. raise an exception that + # tells the map to redirect to the same url but with a + # trailing slash + if self.strict_slashes and not self.is_leaf and \ + not groups.pop('__suffix__'): + raise RequestSlash() + # if we are not in strict slashes mode we have to remove + # a __suffix__ + elif not self.strict_slashes: + del groups['__suffix__'] + + result = {} + for name, value in groups.iteritems(): + try: + value = self._converters[name].to_python(value) + except ValidationError: + return + result[str(name)] = value + if self.defaults: + result.update(self.defaults) + + if self.alias and self.map.redirect_defaults: + raise RequestAliasRedirect(result) + + return result + + def build(self, values, append_unknown=True): + """Assembles the relative url for that rule and the subdomain. + If building doesn't work for some reasons `None` is returned. + + :internal: + """ + tmp = [] + add = tmp.append + processed = set(self.arguments) + for is_dynamic, data in self._trace: + if is_dynamic: + try: + add(self._converters[data].to_url(values[data])) + except ValidationError: + return + processed.add(data) + else: + add(data) + domain_part, url = (u''.join(tmp)).split('|', 1) + + if append_unknown: + query_vars = MultiDict(values) + for key in processed: + if key in query_vars: + del query_vars[key] + + if query_vars: + url += '?' + url_encode(query_vars, self.map.charset, + sort=self.map.sort_parameters, + key=self.map.sort_key) + + return domain_part, url + + def provides_defaults_for(self, rule): + """Check if this rule has defaults for a given rule. + + :internal: + """ + return not self.build_only and self.defaults and \ + self.endpoint == rule.endpoint and self != rule and \ + self.arguments == rule.arguments + + def suitable_for(self, values, method=None): + """Check if the dict of values has enough data for url generation. + + :internal: + """ + # if a method was given explicitly and that method is not supported + # by this rule, this rule is not suitable. + if method is not None and self.methods is not None \ + and method not in self.methods: + return False + + defaults = self.defaults or () + + # all arguments required must be either in the defaults dict or + # the value dictionary otherwise it's not suitable + for key in self.arguments: + if key not in defaults and key not in values: + return False + + # in case defaults are given we ensure taht either the value was + # skipped or the value is the same as the default value. + if defaults: + for key, value in defaults.iteritems(): + if key in values and value != values[key]: + return False + + return True + + def match_compare_key(self): + """The match compare key for sorting. + + Current implementation: + + 1. rules without any arguments come first for performance + reasons only as we expect them to match faster and some + common ones usually don't have any arguments (index pages etc.) + 2. The more complex rules come first so the second argument is the + negative length of the number of weights. + 3. lastly we order by the actual weights. + + :internal: + """ + return bool(self.arguments), -len(self._weights), self._weights + + def build_compare_key(self): + """The build compare key for sorting. + + :internal: + """ + return self.alias and 1 or 0, -len(self.arguments), \ + -len(self.defaults or ()) + + def __eq__(self, other): + return self.__class__ is other.__class__ and \ + self._trace == other._trace + + def __ne__(self, other): + return not self.__eq__(other) + + def __unicode__(self): + return self.rule + + def __str__(self): + charset = self.map is not None and self.map.charset or 'utf-8' + return unicode(self).encode(charset) + + def __repr__(self): + if self.map is None: + return '<%s (unbound)>' % self.__class__.__name__ + charset = self.map is not None and self.map.charset or 'utf-8' + tmp = [] + for is_dynamic, data in self._trace: + if is_dynamic: + tmp.append('<%s>' % data) + else: + tmp.append(data) + return '<%s %r%s -> %s>' % ( + self.__class__.__name__, + (u''.join(tmp).encode(charset)).lstrip('|'), + self.methods is not None and ' (%s)' % \ + ', '.join(self.methods) or '', + self.endpoint + ) + + +class BaseConverter(object): + """Base class for all converters.""" + regex = '[^/]+' + weight = 100 + + def __init__(self, map): + self.map = map + + def to_python(self, value): + return value + + def to_url(self, value): + return url_quote(value, self.map.charset) + + +class UnicodeConverter(BaseConverter): + """This converter is the default converter and accepts any string but + only one path segment. Thus the string can not include a slash. + + This is the default validator. + + Example:: + + Rule('/pages/'), + Rule('/') + + :param map: the :class:`Map`. + :param minlength: the minimum length of the string. Must be greater + or equal 1. + :param maxlength: the maximum length of the string. + :param length: the exact length of the string. + """ + + def __init__(self, map, minlength=1, maxlength=None, length=None): + BaseConverter.__init__(self, map) + if length is not None: + length = '{%d}' % int(length) + else: + if maxlength is None: + maxlength = '' + else: + maxlength = int(maxlength) + length = '{%s,%s}' % ( + int(minlength), + maxlength + ) + self.regex = '[^/]' + length + + +class AnyConverter(BaseConverter): + """Matches one of the items provided. Items can either be Python + identifiers or strings:: + + Rule('/') + + :param map: the :class:`Map`. + :param items: this function accepts the possible items as positional + arguments. + """ + + def __init__(self, map, *items): + BaseConverter.__init__(self, map) + self.regex = '(?:%s)' % '|'.join([re.escape(x) for x in items]) + + +class PathConverter(BaseConverter): + """Like the default :class:`UnicodeConverter`, but it also matches + slashes. This is useful for wikis and similar applications:: + + Rule('/') + Rule('//edit') + + :param map: the :class:`Map`. + """ + regex = '[^/].*?' + weight = 200 + + +class NumberConverter(BaseConverter): + """Baseclass for `IntegerConverter` and `FloatConverter`. + + :internal: + """ + weight = 50 + + def __init__(self, map, fixed_digits=0, min=None, max=None): + BaseConverter.__init__(self, map) + self.fixed_digits = fixed_digits + self.min = min + self.max = max + + def to_python(self, value): + if (self.fixed_digits and len(value) != self.fixed_digits): + raise ValidationError() + value = self.num_convert(value) + if (self.min is not None and value < self.min) or \ + (self.max is not None and value > self.max): + raise ValidationError() + return value + + def to_url(self, value): + value = self.num_convert(value) + if self.fixed_digits: + value = ('%%0%sd' % self.fixed_digits) % value + return str(value) + + +class IntegerConverter(NumberConverter): + """This converter only accepts integer values:: + + Rule('/page/') + + This converter does not support negative values. + + :param map: the :class:`Map`. + :param fixed_digits: the number of fixed digits in the URL. If you set + this to ``4`` for example, the application will + only match if the url looks like ``/0001/``. The + default is variable length. + :param min: the minimal value. + :param max: the maximal value. + """ + regex = r'\d+' + num_convert = int + + +class FloatConverter(NumberConverter): + """This converter only accepts floating point values:: + + Rule('/probability/') + + This converter does not support negative values. + + :param map: the :class:`Map`. + :param min: the minimal value. + :param max: the maximal value. + """ + regex = r'\d+\.\d+' + num_convert = float + + def __init__(self, map, min=None, max=None): + NumberConverter.__init__(self, map, 0, min, max) + + +#: the default converter mapping for the map. +DEFAULT_CONVERTERS = { + 'default': UnicodeConverter, + 'string': UnicodeConverter, + 'any': AnyConverter, + 'path': PathConverter, + 'int': IntegerConverter, + 'float': FloatConverter +} + + +class Map(object): + """The map class stores all the URL rules and some configuration + parameters. Some of the configuration values are only stored on the + `Map` instance since those affect all rules, others are just defaults + and can be overridden for each rule. Note that you have to specify all + arguments besides the `rules` as keyword arguments! + + :param rules: sequence of url rules for this map. + :param default_subdomain: The default subdomain for rules without a + subdomain defined. + :param charset: charset of the url. defaults to ``"utf-8"`` + :param strict_slashes: Take care of trailing slashes. + :param redirect_defaults: This will redirect to the default rule if it + wasn't visited that way. This helps creating + unique URLs. + :param converters: A dict of converters that adds additional converters + to the list of converters. If you redefine one + converter this will override the original one. + :param sort_parameters: If set to `True` the url parameters are sorted. + See `url_encode` for more details. + :param sort_key: The sort key function for `url_encode`. + :param encoding_errors: the error method to use for decoding + :param host_matching: if set to `True` it enables the host matching + feature and disables the subdomain one. If + enabled the `host` parameter to rules is used + instead of the `subdomain` one. + + .. versionadded:: 0.5 + `sort_parameters` and `sort_key` was added. + + .. versionadded:: 0.7 + `encoding_errors` and `host_matching` was added. + """ + + #: .. versionadded:: 0.6 + #: a dict of default converters to be used. + default_converters = ImmutableDict(DEFAULT_CONVERTERS) + + def __init__(self, rules=None, default_subdomain='', charset='utf-8', + strict_slashes=True, redirect_defaults=True, + converters=None, sort_parameters=False, sort_key=None, + encoding_errors='replace', host_matching=False): + self._rules = [] + self._rules_by_endpoint = {} + self._remap = True + + self.default_subdomain = default_subdomain + self.charset = charset + self.encoding_errors = encoding_errors + self.strict_slashes = strict_slashes + self.redirect_defaults = redirect_defaults + self.host_matching = host_matching + + self.converters = self.default_converters.copy() + if converters: + self.converters.update(converters) + + self.sort_parameters = sort_parameters + self.sort_key = sort_key + + for rulefactory in rules or (): + self.add(rulefactory) + + def is_endpoint_expecting(self, endpoint, *arguments): + """Iterate over all rules and check if the endpoint expects + the arguments provided. This is for example useful if you have + some URLs that expect a language code and others that do not and + you want to wrap the builder a bit so that the current language + code is automatically added if not provided but endpoints expect + it. + + :param endpoint: the endpoint to check. + :param arguments: this function accepts one or more arguments + as positional arguments. Each one of them is + checked. + """ + self.update() + arguments = set(arguments) + for rule in self._rules_by_endpoint[endpoint]: + if arguments.issubset(rule.arguments): + return True + return False + + def iter_rules(self, endpoint=None): + """Iterate over all rules or the rules of an endpoint. + + :param endpoint: if provided only the rules for that endpoint + are returned. + :return: an iterator + """ + self.update() + if endpoint is not None: + return iter(self._rules_by_endpoint[endpoint]) + return iter(self._rules) + + def add(self, rulefactory): + """Add a new rule or factory to the map and bind it. Requires that the + rule is not bound to another map. + + :param rulefactory: a :class:`Rule` or :class:`RuleFactory` + """ + for rule in rulefactory.get_rules(self): + rule.bind(self) + self._rules.append(rule) + self._rules_by_endpoint.setdefault(rule.endpoint, []).append(rule) + self._remap = True + + def bind(self, server_name, script_name=None, subdomain=None, + url_scheme='http', default_method='GET', path_info=None, + query_args=None): + """Return a new :class:`MapAdapter` with the details specified to the + call. Note that `script_name` will default to ``'/'`` if not further + specified or `None`. The `server_name` at least is a requirement + because the HTTP RFC requires absolute URLs for redirects and so all + redirect exceptions raised by Werkzeug will contain the full canonical + URL. + + If no path_info is passed to :meth:`match` it will use the default path + info passed to bind. While this doesn't really make sense for + manual bind calls, it's useful if you bind a map to a WSGI + environment which already contains the path info. + + `subdomain` will default to the `default_subdomain` for this map if + no defined. If there is no `default_subdomain` you cannot use the + subdomain feature. + + .. versionadded:: 0.7 + `query_args` added + + .. versionadded:: 0.8 + `query_args` can now also be a string. + """ + server_name = server_name.lower() + if self.host_matching: + if subdomain is not None: + raise RuntimeError('host matching enabled and a ' + 'subdomain was provided') + elif subdomain is None: + subdomain = self.default_subdomain + if script_name is None: + script_name = '/' + return MapAdapter(self, server_name, script_name, subdomain, + url_scheme, path_info, default_method, query_args) + + def bind_to_environ(self, environ, server_name=None, subdomain=None): + """Like :meth:`bind` but you can pass it an WSGI environment and it + will fetch the information from that dictionary. Note that because of + limitations in the protocol there is no way to get the current + subdomain and real `server_name` from the environment. If you don't + provide it, Werkzeug will use `SERVER_NAME` and `SERVER_PORT` (or + `HTTP_HOST` if provided) as used `server_name` with disabled subdomain + feature. + + If `subdomain` is `None` but an environment and a server name is + provided it will calculate the current subdomain automatically. + Example: `server_name` is ``'example.com'`` and the `SERVER_NAME` + in the wsgi `environ` is ``'staging.dev.example.com'`` the calculated + subdomain will be ``'staging.dev'``. + + If the object passed as environ has an environ attribute, the value of + this attribute is used instead. This allows you to pass request + objects. Additionally `PATH_INFO` added as a default of the + :class:`MapAdapter` so that you don't have to pass the path info to + the match method. + + .. versionchanged:: 0.5 + previously this method accepted a bogus `calculate_subdomain` + parameter that did not have any effect. It was removed because + of that. + + .. versionchanged:: 0.8 + This will no longer raise a ValueError when an unexpected server + name was passed. + + :param environ: a WSGI environment. + :param server_name: an optional server name hint (see above). + :param subdomain: optionally the current subdomain (see above). + """ + environ = _get_environ(environ) + if server_name is None: + if 'HTTP_HOST' in environ: + server_name = environ['HTTP_HOST'] + else: + server_name = environ['SERVER_NAME'] + if (environ['wsgi.url_scheme'], environ['SERVER_PORT']) not \ + in (('https', '443'), ('http', '80')): + server_name += ':' + environ['SERVER_PORT'] + elif subdomain is None and not self.host_matching: + server_name = server_name.lower() + if 'HTTP_HOST' in environ: + wsgi_server_name = environ.get('HTTP_HOST') + else: + wsgi_server_name = environ.get('SERVER_NAME') + if (environ['wsgi.url_scheme'], environ['SERVER_PORT']) not \ + in (('https', '443'), ('http', '80')): + wsgi_server_name += ':' + environ['SERVER_PORT'] + wsgi_server_name = wsgi_server_name.lower() + cur_server_name = wsgi_server_name.split('.') + real_server_name = server_name.split('.') + offset = -len(real_server_name) + if cur_server_name[offset:] != real_server_name: + # This can happen even with valid configs if the server was + # accesssed directly by IP address under some situations. + # Instead of raising an exception like in Werkzeug 0.7 or + # earlier we go by an invalid subdomain which will result + # in a 404 error on matching. + subdomain = '' + else: + subdomain = '.'.join(filter(None, cur_server_name[:offset])) + return Map.bind(self, server_name, environ.get('SCRIPT_NAME'), + subdomain, environ['wsgi.url_scheme'], + environ['REQUEST_METHOD'], environ.get('PATH_INFO'), + query_args=environ.get('QUERY_STRING', '')) + + def update(self): + """Called before matching and building to keep the compiled rules + in the correct order after things changed. + """ + if self._remap: + self._rules.sort(key=lambda x: x.match_compare_key()) + for rules in self._rules_by_endpoint.itervalues(): + rules.sort(key=lambda x: x.build_compare_key()) + self._remap = False + + def __repr__(self): + rules = self.iter_rules() + return '%s(%s)' % (self.__class__.__name__, pformat(list(rules))) + + +class MapAdapter(object): + """Returned by :meth:`Map.bind` or :meth:`Map.bind_to_environ` and does + the URL matching and building based on runtime information. + """ + + def __init__(self, map, server_name, script_name, subdomain, + url_scheme, path_info, default_method, query_args=None): + self.map = map + self.server_name = server_name + if not script_name.endswith('/'): + script_name += '/' + self.script_name = script_name + self.subdomain = subdomain + self.url_scheme = url_scheme + self.path_info = path_info or u'' + self.default_method = default_method + self.query_args = query_args + + def dispatch(self, view_func, path_info=None, method=None, + catch_http_exceptions=False): + """Does the complete dispatching process. `view_func` is called with + the endpoint and a dict with the values for the view. It should + look up the view function, call it, and return a response object + or WSGI application. http exceptions are not caught by default + so that applications can display nicer error messages by just + catching them by hand. If you want to stick with the default + error messages you can pass it ``catch_http_exceptions=True`` and + it will catch the http exceptions. + + Here a small example for the dispatch usage:: + + from werkzeug.wrappers import Request, Response + from werkzeug.wsgi import responder + from werkzeug.routing import Map, Rule + + def on_index(request): + return Response('Hello from the index') + + url_map = Map([Rule('/', endpoint='index')]) + views = {'index': on_index} + + @responder + def application(environ, start_response): + request = Request(environ) + urls = url_map.bind_to_environ(environ) + return urls.dispatch(lambda e, v: views[e](request, **v), + catch_http_exceptions=True) + + Keep in mind that this method might return exception objects, too, so + use :class:`Response.force_type` to get a response object. + + :param view_func: a function that is called with the endpoint as + first argument and the value dict as second. Has + to dispatch to the actual view function with this + information. (see above) + :param path_info: the path info to use for matching. Overrides the + path info specified on binding. + :param method: the HTTP method used for matching. Overrides the + method specified on binding. + :param catch_http_exceptions: set to `True` to catch any of the + werkzeug :class:`HTTPException`\s. + """ + try: + try: + endpoint, args = self.match(path_info, method) + except RequestRedirect, e: + return e + return view_func(endpoint, args) + except HTTPException, e: + if catch_http_exceptions: + return e + raise + + def match(self, path_info=None, method=None, return_rule=False, + query_args=None): + """The usage is simple: you just pass the match method the current + path info as well as the method (which defaults to `GET`). The + following things can then happen: + + - you receive a `NotFound` exception that indicates that no URL is + matching. A `NotFound` exception is also a WSGI application you + can call to get a default page not found page (happens to be the + same object as `werkzeug.exceptions.NotFound`) + + - you receive a `MethodNotAllowed` exception that indicates that there + is a match for this URL but not for the current request method. + This is useful for RESTful applications. + + - you receive a `RequestRedirect` exception with a `new_url` + attribute. This exception is used to notify you about a request + Werkzeug requests from your WSGI application. This is for example the + case if you request ``/foo`` although the correct URL is ``/foo/`` + You can use the `RequestRedirect` instance as response-like object + similar to all other subclasses of `HTTPException`. + + - you get a tuple in the form ``(endpoint, arguments)`` if there is + a match (unless `return_rule` is True, in which case you get a tuple + in the form ``(rule, arguments)``) + + If the path info is not passed to the match method the default path + info of the map is used (defaults to the root URL if not defined + explicitly). + + All of the exceptions raised are subclasses of `HTTPException` so they + can be used as WSGI responses. The will all render generic error or + redirect pages. + + Here is a small example for matching: + + >>> m = Map([ + ... Rule('/', endpoint='index'), + ... Rule('/downloads/', endpoint='downloads/index'), + ... Rule('/downloads/', endpoint='downloads/show') + ... ]) + >>> urls = m.bind("example.com", "/") + >>> urls.match("/", "GET") + ('index', {}) + >>> urls.match("/downloads/42") + ('downloads/show', {'id': 42}) + + And here is what happens on redirect and missing URLs: + + >>> urls.match("/downloads") + Traceback (most recent call last): + ... + RequestRedirect: http://example.com/downloads/ + >>> urls.match("/missing") + Traceback (most recent call last): + ... + NotFound: 404 Not Found + + :param path_info: the path info to use for matching. Overrides the + path info specified on binding. + :param method: the HTTP method used for matching. Overrides the + method specified on binding. + :param return_rule: return the rule that matched instead of just the + endpoint (defaults to `False`). + :param query_args: optional query arguments that are used for + automatic redirects as string or dictionary. It's + currently not possible to use the query arguments + for URL matching. + + .. versionadded:: 0.6 + `return_rule` was added. + + .. versionadded:: 0.7 + `query_args` was added. + + .. versionchanged:: 0.8 + `query_args` can now also be a string. + """ + self.map.update() + if path_info is None: + path_info = self.path_info + if not isinstance(path_info, unicode): + path_info = path_info.decode(self.map.charset, + self.map.encoding_errors) + if query_args is None: + query_args = self.query_args + method = (method or self.default_method).upper() + + path = u'%s|/%s' % (self.map.host_matching and self.server_name or + self.subdomain, path_info.lstrip('/')) + + have_match_for = set() + for rule in self.map._rules: + try: + rv = rule.match(path) + except RequestSlash: + raise RequestRedirect(self.make_redirect_url( + path_info + '/', query_args)) + except RequestAliasRedirect, e: + raise RequestRedirect(self.make_alias_redirect_url( + path, rule.endpoint, e.matched_values, method, query_args)) + if rv is None: + continue + if rule.methods is not None and method not in rule.methods: + have_match_for.update(rule.methods) + continue + + if self.map.redirect_defaults: + redirect_url = self.get_default_redirect(rule, method, rv, + query_args) + if redirect_url is not None: + raise RequestRedirect(redirect_url) + + if rule.redirect_to is not None: + if isinstance(rule.redirect_to, basestring): + def _handle_match(match): + value = rv[match.group(1)] + return rule._converters[match.group(1)].to_url(value) + redirect_url = _simple_rule_re.sub(_handle_match, + rule.redirect_to) + else: + redirect_url = rule.redirect_to(self, **rv) + raise RequestRedirect(str(urljoin('%s://%s%s%s' % ( + self.url_scheme, + self.subdomain and self.subdomain + '.' or '', + self.server_name, + self.script_name + ), redirect_url))) + + if return_rule: + return rule, rv + else: + return rule.endpoint, rv + + if have_match_for: + raise MethodNotAllowed(valid_methods=list(have_match_for)) + raise NotFound() + + def test(self, path_info=None, method=None): + """Test if a rule would match. Works like `match` but returns `True` + if the URL matches, or `False` if it does not exist. + + :param path_info: the path info to use for matching. Overrides the + path info specified on binding. + :param method: the HTTP method used for matching. Overrides the + method specified on binding. + """ + try: + self.match(path_info, method) + except RequestRedirect: + pass + except HTTPException: + return False + return True + + def allowed_methods(self, path_info=None): + """Returns the valid methods that match for a given path. + + .. versionadded:: 0.7 + """ + try: + self.match(path_info, method='--') + except MethodNotAllowed, e: + return e.valid_methods + except HTTPException, e: + pass + return [] + + def get_host(self, domain_part): + """Figures out the full host name for the given domain part. The + domain part is a subdomain in case host matching is disabled or + a full host name. + """ + if self.map.host_matching: + if domain_part is None: + return self.server_name + return domain_part + subdomain = domain_part + if subdomain is None: + subdomain = self.subdomain + return (subdomain and subdomain + '.' or '') + self.server_name + + def get_default_redirect(self, rule, method, values, query_args): + """A helper that returns the URL to redirect to if it finds one. + This is used for default redirecting only. + + :internal: + """ + assert self.map.redirect_defaults + for r in self.map._rules_by_endpoint[rule.endpoint]: + # every rule that comes after this one, including ourself + # has a lower priority for the defaults. We order the ones + # with the highest priority up for building. + if r is rule: + break + if r.provides_defaults_for(rule) and \ + r.suitable_for(values, method): + values.update(r.defaults) + domain_part, path = r.build(values) + return self.make_redirect_url( + path, query_args, domain_part=domain_part) + + def encode_query_args(self, query_args): + if not isinstance(query_args, basestring): + query_args = url_encode(query_args, self.map.charset) + return query_args + + def make_redirect_url(self, path_info, query_args=None, domain_part=None): + """Creates a redirect URL. + + :internal: + """ + suffix = '' + if query_args: + suffix = '?' + self.encode_query_args(query_args) + return str('%s://%s/%s%s' % ( + self.url_scheme, + self.get_host(domain_part), + posixpath.join(self.script_name[:-1].lstrip('/'), + url_quote(path_info.lstrip('/'), self.map.charset)), + suffix + )) + + def make_alias_redirect_url(self, path, endpoint, values, method, query_args): + """Internally called to make an alias redirect URL.""" + url = self.build(endpoint, values, method, append_unknown=False, + force_external=True) + if query_args: + url += '?' + self.encode_query_args(query_args) + assert url != path, 'detected invalid alias setting. No canonical ' \ + 'URL found' + return url + + def _partial_build(self, endpoint, values, method, append_unknown): + """Helper for :meth:`build`. Returns subdomain and path for the + rule that accepts this endpoint, values and method. + + :internal: + """ + # in case the method is none, try with the default method first + if method is None: + rv = self._partial_build(endpoint, values, self.default_method, + append_unknown) + if rv is not None: + return rv + + # default method did not match or a specific method is passed, + # check all and go with first result. + for rule in self.map._rules_by_endpoint.get(endpoint, ()): + if rule.suitable_for(values, method): + rv = rule.build(values, append_unknown) + if rv is not None: + return rv + + def build(self, endpoint, values=None, method=None, force_external=False, + append_unknown=True): + """Building URLs works pretty much the other way round. Instead of + `match` you call `build` and pass it the endpoint and a dict of + arguments for the placeholders. + + The `build` function also accepts an argument called `force_external` + which, if you set it to `True` will force external URLs. Per default + external URLs (include the server name) will only be used if the + target URL is on a different subdomain. + + >>> m = Map([ + ... Rule('/', endpoint='index'), + ... Rule('/downloads/', endpoint='downloads/index'), + ... Rule('/downloads/', endpoint='downloads/show') + ... ]) + >>> urls = m.bind("example.com", "/") + >>> urls.build("index", {}) + '/' + >>> urls.build("downloads/show", {'id': 42}) + '/downloads/42' + >>> urls.build("downloads/show", {'id': 42}, force_external=True) + 'http://example.com/downloads/42' + + Because URLs cannot contain non ASCII data you will always get + bytestrings back. Non ASCII characters are urlencoded with the + charset defined on the map instance. + + Additional values are converted to unicode and appended to the URL as + URL querystring parameters: + + >>> urls.build("index", {'q': 'My Searchstring'}) + '/?q=My+Searchstring' + + If a rule does not exist when building a `BuildError` exception is + raised. + + The build method accepts an argument called `method` which allows you + to specify the method you want to have an URL built for if you have + different methods for the same endpoint specified. + + .. versionadded:: 0.6 + the `append_unknown` parameter was added. + + :param endpoint: the endpoint of the URL to build. + :param values: the values for the URL to build. Unhandled values are + appended to the URL as query parameters. + :param method: the HTTP method for the rule if there are different + URLs for different methods on the same endpoint. + :param force_external: enforce full canonical external URLs. + :param append_unknown: unknown parameters are appended to the generated + URL as query string argument. Disable this + if you want the builder to ignore those. + """ + self.map.update() + if values: + if isinstance(values, MultiDict): + valueiter = values.iteritems(multi=True) + else: + valueiter = values.iteritems() + values = dict((k, v) for k, v in valueiter if v is not None) + else: + values = {} + + rv = self._partial_build(endpoint, values, method, append_unknown) + if rv is None: + raise BuildError(endpoint, values, method) + domain_part, path = rv + + host = self.get_host(domain_part) + + # shortcut this. + if not force_external and ( + (self.map.host_matching and host == self.server_name) or + (not self.map.host_matching and domain_part == self.subdomain)): + return str(urljoin(self.script_name, './' + path.lstrip('/'))) + return str('%s://%s%s/%s' % ( + self.url_scheme, + host, + self.script_name[:-1], + path.lstrip('/') + )) diff --git a/websdk/werkzeug/script.py b/websdk/werkzeug/script.py new file mode 100644 index 0000000..2411cbd --- /dev/null +++ b/websdk/werkzeug/script.py @@ -0,0 +1,313 @@ +# -*- coding: utf-8 -*- +r''' + werkzeug.script + ~~~~~~~~~~~~~~~ + + .. admonition:: Deprecated Functionality + + ``werkzeug.script`` is deprecated without replacement functionality. + Python's command line support improved greatly with :mod:`argparse` + and a bunch of alternative modules. + + Most of the time you have recurring tasks while writing an application + such as starting up an interactive python interpreter with some prefilled + imports, starting the development server, initializing the database or + something similar. + + For that purpose werkzeug provides the `werkzeug.script` module which + helps you writing such scripts. + + + Basic Usage + ----------- + + The following snippet is roughly the same in every werkzeug script:: + + #!/usr/bin/env python + # -*- coding: utf-8 -*- + from werkzeug import script + + # actions go here + + if __name__ == '__main__': + script.run() + + Starting this script now does nothing because no actions are defined. + An action is a function in the same module starting with ``"action_"`` + which takes a number of arguments where every argument has a default. The + type of the default value specifies the type of the argument. + + Arguments can then be passed by position or using ``--name=value`` from + the shell. + + Because a runserver and shell command is pretty common there are two + factory functions that create such commands:: + + def make_app(): + from yourapplication import YourApplication + return YourApplication(...) + + action_runserver = script.make_runserver(make_app, use_reloader=True) + action_shell = script.make_shell(lambda: {'app': make_app()}) + + + Using The Scripts + ----------------- + + The script from above can be used like this from the shell now: + + .. sourcecode:: text + + $ ./manage.py --help + $ ./manage.py runserver localhost 8080 --debugger --no-reloader + $ ./manage.py runserver -p 4000 + $ ./manage.py shell + + As you can see it's possible to pass parameters as positional arguments + or as named parameters, pretty much like Python function calls. + + + :copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +''' +import sys +import inspect +import getopt +from os.path import basename + + +argument_types = { + bool: 'boolean', + str: 'string', + int: 'integer', + float: 'float' +} + + +converters = { + 'boolean': lambda x: x.lower() in ('1', 'true', 'yes', 'on'), + 'string': str, + 'integer': int, + 'float': float +} + + +def run(namespace=None, action_prefix='action_', args=None): + """Run the script. Participating actions are looked up in the caller's + namespace if no namespace is given, otherwise in the dict provided. + Only items that start with action_prefix are processed as actions. If + you want to use all items in the namespace provided as actions set + action_prefix to an empty string. + + :param namespace: An optional dict where the functions are looked up in. + By default the local namespace of the caller is used. + :param action_prefix: The prefix for the functions. Everything else + is ignored. + :param args: the arguments for the function. If not specified + :data:`sys.argv` without the first argument is used. + """ + if namespace is None: + namespace = sys._getframe(1).f_locals + actions = find_actions(namespace, action_prefix) + + if args is None: + args = sys.argv[1:] + if not args or args[0] in ('-h', '--help'): + return print_usage(actions) + elif args[0] not in actions: + fail('Unknown action \'%s\'' % args[0]) + + arguments = {} + types = {} + key_to_arg = {} + long_options = [] + formatstring = '' + func, doc, arg_def = actions[args.pop(0)] + for idx, (arg, shortcut, default, option_type) in enumerate(arg_def): + real_arg = arg.replace('-', '_') + if shortcut: + formatstring += shortcut + if not isinstance(default, bool): + formatstring += ':' + key_to_arg['-' + shortcut] = real_arg + long_options.append(isinstance(default, bool) and arg or arg + '=') + key_to_arg['--' + arg] = real_arg + key_to_arg[idx] = real_arg + types[real_arg] = option_type + arguments[real_arg] = default + + try: + optlist, posargs = getopt.gnu_getopt(args, formatstring, long_options) + except getopt.GetoptError, e: + fail(str(e)) + + specified_arguments = set() + for key, value in enumerate(posargs): + try: + arg = key_to_arg[key] + except IndexError: + fail('Too many parameters') + specified_arguments.add(arg) + try: + arguments[arg] = converters[types[arg]](value) + except ValueError: + fail('Invalid value for argument %s (%s): %s' % (key, arg, value)) + + for key, value in optlist: + arg = key_to_arg[key] + if arg in specified_arguments: + fail('Argument \'%s\' is specified twice' % arg) + if types[arg] == 'boolean': + if arg.startswith('no_'): + value = 'no' + else: + value = 'yes' + try: + arguments[arg] = converters[types[arg]](value) + except ValueError: + fail('Invalid value for \'%s\': %s' % (key, value)) + + newargs = {} + for k, v in arguments.iteritems(): + newargs[k.startswith('no_') and k[3:] or k] = v + arguments = newargs + return func(**arguments) + + +def fail(message, code=-1): + """Fail with an error.""" + print >> sys.stderr, 'Error:', message + sys.exit(code) + + +def find_actions(namespace, action_prefix): + """Find all the actions in the namespace.""" + actions = {} + for key, value in namespace.iteritems(): + if key.startswith(action_prefix): + actions[key[len(action_prefix):]] = analyse_action(value) + return actions + + +def print_usage(actions): + """Print the usage information. (Help screen)""" + actions = actions.items() + actions.sort() + print 'usage: %s []' % basename(sys.argv[0]) + print ' %s --help' % basename(sys.argv[0]) + print + print 'actions:' + for name, (func, doc, arguments) in actions: + print ' %s:' % name + for line in doc.splitlines(): + print ' %s' % line + if arguments: + print + for arg, shortcut, default, argtype in arguments: + if isinstance(default, bool): + print ' %s' % ( + (shortcut and '-%s, ' % shortcut or '') + '--' + arg + ) + else: + print ' %-30s%-10s%s' % ( + (shortcut and '-%s, ' % shortcut or '') + '--' + arg, + argtype, default + ) + print + + +def analyse_action(func): + """Analyse a function.""" + description = inspect.getdoc(func) or 'undocumented action' + arguments = [] + args, varargs, kwargs, defaults = inspect.getargspec(func) + if varargs or kwargs: + raise TypeError('variable length arguments for action not allowed.') + if len(args) != len(defaults or ()): + raise TypeError('not all arguments have proper definitions') + + for idx, (arg, definition) in enumerate(zip(args, defaults or ())): + if arg.startswith('_'): + raise TypeError('arguments may not start with an underscore') + if not isinstance(definition, tuple): + shortcut = None + default = definition + else: + shortcut, default = definition + argument_type = argument_types[type(default)] + if isinstance(default, bool) and default is True: + arg = 'no-' + arg + arguments.append((arg.replace('_', '-'), shortcut, + default, argument_type)) + return func, description, arguments + + +def make_shell(init_func=None, banner=None, use_ipython=True): + """Returns an action callback that spawns a new interactive + python shell. + + :param init_func: an optional initialization function that is + called before the shell is started. The return + value of this function is the initial namespace. + :param banner: the banner that is displayed before the shell. If + not specified a generic banner is used instead. + :param use_ipython: if set to `True` ipython is used if available. + """ + if banner is None: + banner = 'Interactive Werkzeug Shell' + if init_func is None: + init_func = dict + def action(ipython=use_ipython): + """Start a new interactive python session.""" + namespace = init_func() + if ipython: + try: + try: + from IPython.frontend.terminal.embed import InteractiveShellEmbed + sh = InteractiveShellEmbed(banner1=banner) + except ImportError: + from IPython.Shell import IPShellEmbed + sh = IPShellEmbed(banner=banner) + except ImportError: + pass + else: + sh(global_ns={}, local_ns=namespace) + return + from code import interact + interact(banner, local=namespace) + return action + + +def make_runserver(app_factory, hostname='localhost', port=5000, + use_reloader=False, use_debugger=False, use_evalex=True, + threaded=False, processes=1, static_files=None, + extra_files=None, ssl_context=None): + """Returns an action callback that spawns a new development server. + + .. versionadded:: 0.5 + `static_files` and `extra_files` was added. + + ..versionadded:: 0.6.1 + `ssl_context` was added. + + :param app_factory: a function that returns a new WSGI application. + :param hostname: the default hostname the server should listen on. + :param port: the default port of the server. + :param use_reloader: the default setting for the reloader. + :param use_evalex: the default setting for the evalex flag of the debugger. + :param threaded: the default threading setting. + :param processes: the default number of processes to start. + :param static_files: optional dict of static files. + :param extra_files: optional list of extra files to track for reloading. + :param ssl_context: optional SSL context for running server in HTTPS mode. + """ + def action(hostname=('h', hostname), port=('p', port), + reloader=use_reloader, debugger=use_debugger, + evalex=use_evalex, threaded=threaded, processes=processes): + """Start a new development server.""" + from werkzeug.serving import run_simple + app = app_factory() + run_simple(hostname, port, app, reloader, debugger, evalex, + extra_files, 1, threaded, processes, + static_files=static_files, ssl_context=ssl_context) + return action diff --git a/websdk/werkzeug/security.py b/websdk/werkzeug/security.py new file mode 100644 index 0000000..5f1d7d4 --- /dev/null +++ b/websdk/werkzeug/security.py @@ -0,0 +1,140 @@ +# -*- coding: utf-8 -*- +""" + werkzeug.security + ~~~~~~~~~~~~~~~~~ + + Security related helpers such as secure password hashing tools. + + :copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" +import os +import hmac +import posixpath +from itertools import izip +from random import SystemRandom + +# because the API of hmac changed with the introduction of the +# new hashlib module, we have to support both. This sets up a +# mapping to the digest factory functions and the digest modules +# (or factory functions with changed API) +try: + from hashlib import sha1, md5 + _hash_funcs = _hash_mods = {'sha1': sha1, 'md5': md5} + _sha1_mod = sha1 + _md5_mod = md5 +except ImportError: + import sha as _sha1_mod, md5 as _md5_mod + _hash_mods = {'sha1': _sha1_mod, 'md5': _md5_mod} + _hash_funcs = {'sha1': _sha1_mod.new, 'md5': _md5_mod.new} + + +SALT_CHARS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' + + +_sys_rng = SystemRandom() +_os_alt_seps = list(sep for sep in [os.path.sep, os.path.altsep] + if sep not in (None, '/')) + + +def safe_str_cmp(a, b): + """This function compares strings in somewhat constant time. This + requires that the length of at least one string is known in advance. + + Returns `True` if the two strings are equal or `False` if they are not. + + .. versionadded:: 0.7 + """ + if len(a) != len(b): + return False + rv = 0 + for x, y in izip(a, b): + rv |= ord(x) ^ ord(y) + return rv == 0 + + +def gen_salt(length): + """Generate a random string of SALT_CHARS with specified ``length``.""" + if length <= 0: + raise ValueError('requested salt of length <= 0') + return ''.join(_sys_rng.choice(SALT_CHARS) for _ in xrange(length)) + + +def _hash_internal(method, salt, password): + """Internal password hash helper. Supports plaintext without salt, + unsalted and salted passwords. In case salted passwords are used + hmac is used. + """ + if method == 'plain': + return password + if salt: + if method not in _hash_mods: + return None + if isinstance(salt, unicode): + salt = salt.encode('utf-8') + h = hmac.new(salt, None, _hash_mods[method]) + else: + if method not in _hash_funcs: + return None + h = _hash_funcs[method]() + if isinstance(password, unicode): + password = password.encode('utf-8') + h.update(password) + return h.hexdigest() + + +def generate_password_hash(password, method='sha1', salt_length=8): + """Hash a password with the given method and salt with with a string of + the given length. The format of the string returned includes the method + that was used so that :func:`check_password_hash` can check the hash. + + The format for the hashed string looks like this:: + + method$salt$hash + + This method can **not** generate unsalted passwords but it is possible + to set the method to plain to enforce plaintext passwords. If a salt + is used, hmac is used internally to salt the password. + + :param password: the password to hash + :param method: the hash method to use (``'md5'`` or ``'sha1'``) + :param salt_length: the lengt of the salt in letters + """ + salt = method != 'plain' and gen_salt(salt_length) or '' + h = _hash_internal(method, salt, password) + if h is None: + raise TypeError('invalid method %r' % method) + return '%s$%s$%s' % (method, salt, h) + + +def check_password_hash(pwhash, password): + """check a password against a given salted and hashed password value. + In order to support unsalted legacy passwords this method supports + plain text passwords, md5 and sha1 hashes (both salted and unsalted). + + Returns `True` if the password matched, `False` otherwise. + + :param pwhash: a hashed string like returned by + :func:`generate_password_hash` + :param password: the plaintext password to compare against the hash + """ + if pwhash.count('$') < 2: + return False + method, salt, hashval = pwhash.split('$', 2) + return safe_str_cmp(_hash_internal(method, salt, password), hashval) + + +def safe_join(directory, filename): + """Safely join `directory` and `filename`. If this cannot be done, + this function returns ``None``. + + :param directory: the base directory. + :param filename: the untrusted filename relative to that directory. + """ + filename = posixpath.normpath(filename) + for sep in _os_alt_seps: + if sep in filename: + return None + if os.path.isabs(filename) or filename.startswith('../'): + return None + return os.path.join(directory, filename) diff --git a/websdk/werkzeug/serving.py b/websdk/werkzeug/serving.py new file mode 100644 index 0000000..48629f5 --- /dev/null +++ b/websdk/werkzeug/serving.py @@ -0,0 +1,614 @@ +# -*- coding: utf-8 -*- +""" + werkzeug.serving + ~~~~~~~~~~~~~~~~ + + There are many ways to serve a WSGI application. While you're developing + it you usually don't want a full blown webserver like Apache but a simple + standalone one. From Python 2.5 onwards there is the `wsgiref`_ server in + the standard library. If you're using older versions of Python you can + download the package from the cheeseshop. + + However there are some caveats. Sourcecode won't reload itself when + changed and each time you kill the server using ``^C`` you get an + `KeyboardInterrupt` error. While the latter is easy to solve the first + one can be a pain in the ass in some situations. + + The easiest way is creating a small ``start-myproject.py`` that runs the + application:: + + #!/usr/bin/env python + # -*- coding: utf-8 -*- + from myproject import make_app + from werkzeug.serving import run_simple + + app = make_app(...) + run_simple('localhost', 8080, app, use_reloader=True) + + You can also pass it a `extra_files` keyword argument with a list of + additional files (like configuration files) you want to observe. + + For bigger applications you should consider using `werkzeug.script` + instead of a simple start file. + + + :copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" +import os +import socket +import sys +import time +import thread +import signal +import subprocess +from urllib import unquote +from SocketServer import ThreadingMixIn, ForkingMixIn +from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler + +import werkzeug +from werkzeug._internal import _log +from werkzeug.exceptions import InternalServerError + + +class WSGIRequestHandler(BaseHTTPRequestHandler, object): + """A request handler that implements WSGI dispatching.""" + + @property + def server_version(self): + return 'Werkzeug/' + werkzeug.__version__ + + def make_environ(self): + if '?' in self.path: + path_info, query = self.path.split('?', 1) + else: + path_info = self.path + query = '' + + def shutdown_server(): + self.server.shutdown_signal = True + + url_scheme = self.server.ssl_context is None and 'http' or 'https' + environ = { + 'wsgi.version': (1, 0), + 'wsgi.url_scheme': url_scheme, + 'wsgi.input': self.rfile, + 'wsgi.errors': sys.stderr, + 'wsgi.multithread': self.server.multithread, + 'wsgi.multiprocess': self.server.multiprocess, + 'wsgi.run_once': False, + 'werkzeug.server.shutdown': + shutdown_server, + 'SERVER_SOFTWARE': self.server_version, + 'REQUEST_METHOD': self.command, + 'SCRIPT_NAME': '', + 'PATH_INFO': unquote(path_info), + 'QUERY_STRING': query, + 'CONTENT_TYPE': self.headers.get('Content-Type', ''), + 'CONTENT_LENGTH': self.headers.get('Content-Length', ''), + 'REMOTE_ADDR': self.client_address[0], + 'REMOTE_PORT': self.client_address[1], + 'SERVER_NAME': self.server.server_address[0], + 'SERVER_PORT': str(self.server.server_address[1]), + 'SERVER_PROTOCOL': self.request_version + } + + for key, value in self.headers.items(): + key = 'HTTP_' + key.upper().replace('-', '_') + if key not in ('HTTP_CONTENT_TYPE', 'HTTP_CONTENT_LENGTH'): + environ[key] = value + + return environ + + def run_wsgi(self): + app = self.server.app + environ = self.make_environ() + headers_set = [] + headers_sent = [] + + def write(data): + assert headers_set, 'write() before start_response' + if not headers_sent: + status, response_headers = headers_sent[:] = headers_set + code, msg = status.split(None, 1) + self.send_response(int(code), msg) + header_keys = set() + for key, value in response_headers: + self.send_header(key, value) + key = key.lower() + header_keys.add(key) + if 'content-length' not in header_keys: + self.close_connection = True + self.send_header('Connection', 'close') + if 'server' not in header_keys: + self.send_header('Server', self.version_string()) + if 'date' not in header_keys: + self.send_header('Date', self.date_time_string()) + self.end_headers() + + assert type(data) is str, 'applications must write bytes' + self.wfile.write(data) + self.wfile.flush() + + def start_response(status, response_headers, exc_info=None): + if exc_info: + try: + if headers_sent: + raise exc_info[0], exc_info[1], exc_info[2] + finally: + exc_info = None + elif headers_set: + raise AssertionError('Headers already set') + headers_set[:] = [status, response_headers] + return write + + def execute(app): + application_iter = app(environ, start_response) + try: + for data in application_iter: + write(data) + # make sure the headers are sent + if not headers_sent: + write('') + finally: + if hasattr(application_iter, 'close'): + application_iter.close() + application_iter = None + + try: + execute(app) + except (socket.error, socket.timeout), e: + self.connection_dropped(e, environ) + except Exception: + if self.server.passthrough_errors: + raise + from werkzeug.debug.tbtools import get_current_traceback + traceback = get_current_traceback(ignore_system_exceptions=True) + try: + # if we haven't yet sent the headers but they are set + # we roll back to be able to set them again. + if not headers_sent: + del headers_set[:] + execute(InternalServerError()) + except Exception: + pass + self.server.log('error', 'Error on request:\n%s', + traceback.plaintext) + + def handle(self): + """Handles a request ignoring dropped connections.""" + try: + rv = BaseHTTPRequestHandler.handle(self) + except (socket.error, socket.timeout), e: + self.connection_dropped(e) + except Exception: + if self.server.ssl_context is None or not is_ssl_error(): + raise + if self.server.shutdown_signal: + self.initiate_shutdown() + return rv + + def initiate_shutdown(self): + """A horrible, horrible way to kill the server for Python 2.6 and + later. It's the best we can do. + """ + # reloader active + if os.environ.get('WERKZEUG_RUN_MAIN') == 'true': + os.kill(os.getpid(), signal.SIGKILL) + # python 2.7 + self.server._BaseServer__shutdown_request = True + # python 2.6 + self.server._BaseServer__serving = False + + def connection_dropped(self, error, environ=None): + """Called if the connection was closed by the client. By default + nothing happens. + """ + + def handle_one_request(self): + """Handle a single HTTP request.""" + self.raw_requestline = self.rfile.readline() + if not self.raw_requestline: + self.close_connection = 1 + elif self.parse_request(): + return self.run_wsgi() + + def send_response(self, code, message=None): + """Send the response header and log the response code.""" + self.log_request(code) + if message is None: + message = code in self.responses and self.responses[code][0] or '' + if self.request_version != 'HTTP/0.9': + self.wfile.write("%s %d %s\r\n" % + (self.protocol_version, code, message)) + + def version_string(self): + return BaseHTTPRequestHandler.version_string(self).strip() + + def address_string(self): + return self.client_address[0] + + def log_request(self, code='-', size='-'): + self.log('info', '"%s" %s %s', self.requestline, code, size) + + def log_error(self, *args): + self.log('error', *args) + + def log_message(self, format, *args): + self.log('info', format, *args) + + def log(self, type, message, *args): + _log(type, '%s - - [%s] %s\n' % (self.address_string(), + self.log_date_time_string(), + message % args)) + + +#: backwards compatible name if someone is subclassing it +BaseRequestHandler = WSGIRequestHandler + + +def generate_adhoc_ssl_context(): + """Generates an adhoc SSL context for the development server.""" + from random import random + from OpenSSL import crypto, SSL + + cert = crypto.X509() + cert.set_serial_number(int(random() * sys.maxint)) + cert.gmtime_adj_notBefore(0) + cert.gmtime_adj_notAfter(60 * 60 * 24 * 365) + + subject = cert.get_subject() + subject.CN = '*' + subject.O = 'Dummy Certificate' + + issuer = cert.get_issuer() + issuer.CN = 'Untrusted Authority' + issuer.O = 'Self-Signed' + + pkey = crypto.PKey() + pkey.generate_key(crypto.TYPE_RSA, 768) + cert.set_pubkey(pkey) + cert.sign(pkey, 'md5') + + ctx = SSL.Context(SSL.SSLv23_METHOD) + ctx.use_privatekey(pkey) + ctx.use_certificate(cert) + + return ctx + + +def is_ssl_error(error=None): + """Checks if the given error (or the current one) is an SSL error.""" + if error is None: + error = sys.exc_info()[1] + from OpenSSL import SSL + return isinstance(error, SSL.Error) + + +class _SSLConnectionFix(object): + """Wrapper around SSL connection to provide a working makefile().""" + + def __init__(self, con): + self._con = con + + def makefile(self, mode, bufsize): + return socket._fileobject(self._con, mode, bufsize) + + def __getattr__(self, attrib): + return getattr(self._con, attrib) + + +def select_ip_version(host, port): + """Returns AF_INET4 or AF_INET6 depending on where to connect to.""" + # disabled due to problems with current ipv6 implementations + # and various operating systems. Probably this code also is + # not supposed to work, but I can't come up with any other + # ways to implement this. + ##try: + ## info = socket.getaddrinfo(host, port, socket.AF_UNSPEC, + ## socket.SOCK_STREAM, 0, + ## socket.AI_PASSIVE) + ## if info: + ## return info[0][0] + ##except socket.gaierror: + ## pass + if ':' in host and hasattr(socket, 'AF_INET6'): + return socket.AF_INET6 + return socket.AF_INET + + +class BaseWSGIServer(HTTPServer, object): + """Simple single-threaded, single-process WSGI server.""" + multithread = False + multiprocess = False + request_queue_size = 128 + + def __init__(self, host, port, app, handler=None, + passthrough_errors=False, ssl_context=None): + if handler is None: + handler = WSGIRequestHandler + self.address_family = select_ip_version(host, port) + HTTPServer.__init__(self, (host, int(port)), handler) + self.app = app + self.passthrough_errors = passthrough_errors + self.shutdown_signal = False + + if ssl_context is not None: + try: + from OpenSSL import tsafe + except ImportError: + raise TypeError('SSL is not available if the OpenSSL ' + 'library is not installed.') + if ssl_context == 'adhoc': + ssl_context = generate_adhoc_ssl_context() + self.socket = tsafe.Connection(ssl_context, self.socket) + self.ssl_context = ssl_context + else: + self.ssl_context = None + + def log(self, type, message, *args): + _log(type, message, *args) + + def serve_forever(self): + self.shutdown_signal = False + try: + HTTPServer.serve_forever(self) + except KeyboardInterrupt: + pass + + def handle_error(self, request, client_address): + if self.passthrough_errors: + raise + else: + return HTTPServer.handle_error(self, request, client_address) + + def get_request(self): + con, info = self.socket.accept() + if self.ssl_context is not None: + con = _SSLConnectionFix(con) + return con, info + + +class ThreadedWSGIServer(ThreadingMixIn, BaseWSGIServer): + """A WSGI server that does threading.""" + multithread = True + + +class ForkingWSGIServer(ForkingMixIn, BaseWSGIServer): + """A WSGI server that does forking.""" + multiprocess = True + + def __init__(self, host, port, app, processes=40, handler=None, + passthrough_errors=False, ssl_context=None): + BaseWSGIServer.__init__(self, host, port, app, handler, + passthrough_errors, ssl_context) + self.max_children = processes + + +def make_server(host, port, app=None, threaded=False, processes=1, + request_handler=None, passthrough_errors=False, + ssl_context=None): + """Create a new server instance that is either threaded, or forks + or just processes one request after another. + """ + if threaded and processes > 1: + raise ValueError("cannot have a multithreaded and " + "multi process server.") + elif threaded: + return ThreadedWSGIServer(host, port, app, request_handler, + passthrough_errors, ssl_context) + elif processes > 1: + return ForkingWSGIServer(host, port, app, processes, request_handler, + passthrough_errors, ssl_context) + else: + return BaseWSGIServer(host, port, app, request_handler, + passthrough_errors, ssl_context) + + +def _iter_module_files(): + for module in sys.modules.values(): + filename = getattr(module, '__file__', None) + if filename: + old = None + while not os.path.isfile(filename): + old = filename + filename = os.path.dirname(filename) + if filename == old: + break + else: + if filename[-4:] in ('.pyc', '.pyo'): + filename = filename[:-1] + yield filename + + +def _reloader_stat_loop(extra_files=None, interval=1): + """When this function is run from the main thread, it will force other + threads to exit when any modules currently loaded change. + + Copyright notice. This function is based on the autoreload.py from + the CherryPy trac which originated from WSGIKit which is now dead. + + :param extra_files: a list of additional files it should watch. + """ + from itertools import chain + mtimes = {} + while 1: + for filename in chain(_iter_module_files(), extra_files or ()): + try: + mtime = os.stat(filename).st_mtime + except OSError: + continue + + old_time = mtimes.get(filename) + if old_time is None: + mtimes[filename] = mtime + continue + elif mtime > old_time: + _log('info', ' * Detected change in %r, reloading' % filename) + sys.exit(3) + time.sleep(interval) + + +def _reloader_inotify(extra_files=None, interval=None): + # Mutated by inotify loop when changes occur. + changed = [False] + + # Setup inotify watches + from pyinotify import WatchManager, Notifier + + # this API changed at one point, support both + try: + from pyinotify import EventsCodes as ec + ec.IN_ATTRIB + except (ImportError, AttributeError): + import pyinotify as ec + + wm = WatchManager() + mask = ec.IN_DELETE_SELF | ec.IN_MOVE_SELF | ec.IN_MODIFY | ec.IN_ATTRIB + + def signal_changed(event): + if changed[0]: + return + _log('info', ' * Detected change in %r, reloading' % event.path) + changed[:] = [True] + + for fname in extra_files or (): + wm.add_watch(fname, mask, signal_changed) + + # ... And now we wait... + notif = Notifier(wm) + try: + while not changed[0]: + # always reiterate through sys.modules, adding them + for fname in _iter_module_files(): + wm.add_watch(fname, mask, signal_changed) + notif.process_events() + if notif.check_events(timeout=interval): + notif.read_events() + # TODO Set timeout to something small and check parent liveliness + finally: + notif.stop() + sys.exit(3) + + +# currently we always use the stat loop reloader for the simple reason +# that the inotify one does not respond to added files properly. Also +# it's quite buggy and the API is a mess. +reloader_loop = _reloader_stat_loop + + +def restart_with_reloader(): + """Spawn a new Python interpreter with the same arguments as this one, + but running the reloader thread. + """ + while 1: + _log('info', ' * Restarting with reloader') + args = [sys.executable] + sys.argv + new_environ = os.environ.copy() + new_environ['WERKZEUG_RUN_MAIN'] = 'true' + + # a weird bug on windows. sometimes unicode strings end up in the + # environment and subprocess.call does not like this, encode them + # to latin1 and continue. + if os.name == 'nt': + for key, value in new_environ.iteritems(): + if isinstance(value, unicode): + new_environ[key] = value.encode('iso-8859-1') + + exit_code = subprocess.call(args, env=new_environ) + if exit_code != 3: + return exit_code + + +def run_with_reloader(main_func, extra_files=None, interval=1): + """Run the given function in an independent python interpreter.""" + import signal + signal.signal(signal.SIGTERM, lambda *args: sys.exit(0)) + if os.environ.get('WERKZEUG_RUN_MAIN') == 'true': + thread.start_new_thread(main_func, ()) + try: + reloader_loop(extra_files, interval) + except KeyboardInterrupt: + return + try: + sys.exit(restart_with_reloader()) + except KeyboardInterrupt: + pass + + +def run_simple(hostname, port, application, use_reloader=False, + use_debugger=False, use_evalex=True, + extra_files=None, reloader_interval=1, threaded=False, + processes=1, request_handler=None, static_files=None, + passthrough_errors=False, ssl_context=None): + """Start an application using wsgiref and with an optional reloader. This + wraps `wsgiref` to fix the wrong default reporting of the multithreaded + WSGI variable and adds optional multithreading and fork support. + + .. versionadded:: 0.5 + `static_files` was added to simplify serving of static files as well + as `passthrough_errors`. + + .. versionadded:: 0.6 + support for SSL was added. + + :param hostname: The host for the application. eg: ``'localhost'`` + :param port: The port for the server. eg: ``8080`` + :param application: the WSGI application to execute + :param use_reloader: should the server automatically restart the python + process if modules were changed? + :param use_debugger: should the werkzeug debugging system be used? + :param use_evalex: should the exception evaluation feature be enabled? + :param extra_files: a list of files the reloader should watch + additionally to the modules. For example configuration + files. + :param reloader_interval: the interval for the reloader in seconds. + :param threaded: should the process handle each request in a separate + thread? + :param processes: number of processes to spawn. + :param request_handler: optional parameter that can be used to replace + the default one. You can use this to replace it + with a different + :class:`~BaseHTTPServer.BaseHTTPRequestHandler` + subclass. + :param static_files: a dict of paths for static files. This works exactly + like :class:`SharedDataMiddleware`, it's actually + just wrapping the application in that middleware before + serving. + :param passthrough_errors: set this to `True` to disable the error catching. + This means that the server will die on errors but + it can be useful to hook debuggers in (pdb etc.) + :param ssl_context: an SSL context for the connection. Either an OpenSSL + context, the string ``'adhoc'`` if the server should + automatically create one, or `None` to disable SSL + (which is the default). + """ + if use_debugger: + from werkzeug.debug import DebuggedApplication + application = DebuggedApplication(application, use_evalex) + if static_files: + from werkzeug.wsgi import SharedDataMiddleware + application = SharedDataMiddleware(application, static_files) + + def inner(): + make_server(hostname, port, application, threaded, + processes, request_handler, + passthrough_errors, ssl_context).serve_forever() + + if os.environ.get('WERKZEUG_RUN_MAIN') != 'true': + display_hostname = hostname != '*' and hostname or 'localhost' + if ':' in display_hostname: + display_hostname = '[%s]' % display_hostname + _log('info', ' * Running on %s://%s:%d/', ssl_context is None + and 'http' or 'https', display_hostname, port) + if use_reloader: + # Create and destroy a socket so that any exceptions are raised before + # we spawn a separate Python interpreter and lose this ability. + address_family = select_ip_version(hostname, port) + test_socket = socket.socket(address_family, socket.SOCK_STREAM) + test_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + test_socket.bind((hostname, port)) + test_socket.close() + run_with_reloader(inner, extra_files, reloader_interval) + else: + inner() diff --git a/websdk/werkzeug/templates.py b/websdk/werkzeug/templates.py new file mode 100644 index 0000000..a9d60e9 --- /dev/null +++ b/websdk/werkzeug/templates.py @@ -0,0 +1,399 @@ +# -*- coding: utf-8 -*- +r""" + werkzeug.templates + ~~~~~~~~~~~~~~~~~~ + + A minimal template engine. + + :copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details. + :license: BSD License. +""" +import sys +import re +import __builtin__ as builtins +from compiler import ast, parse +from compiler.pycodegen import ModuleCodeGenerator +from tokenize import PseudoToken +from werkzeug import urls, utils +from werkzeug._internal import _decode_unicode +from werkzeug.datastructures import MultiDict + + +from warnings import warn +warn(DeprecationWarning('werkzeug.templates is deprecated and ' + 'will be removed in Werkzeug 1.0')) + + +# Copyright notice: The `parse_data` method uses the string interpolation +# algorithm by Ka-Ping Yee which originally was part of `Itpl20.py`_. +# +# .. _Itpl20.py: http://lfw.org/python/Itpl20.py + + +token_re = re.compile('%s|%s(?s)' % ( + r'[uU]?[rR]?("""|\'\'\')((?\n?(?s)') +escape_re = re.compile(r'\\\n|\\(\\|<%)') +namestart_chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_' +undefined = type('UndefinedType', (object,), { + '__iter__': lambda x: iter(()), + '__repr__': lambda x: 'Undefined', + '__str__': lambda x: '' +})() +runtime_vars = frozenset(['Undefined', '__to_unicode', '__context', + '__write', '__write_many']) + + +def call_stmt(func, args, lineno): + return ast.CallFunc(ast.Name(func, lineno=lineno), + args, lineno=lineno) + + +def tokenize(source, filename): + escape = escape_re.sub + escape_repl = lambda m: m.group(1) or '' + lineno = 1 + pos = 0 + + for match in directive_re.finditer(source): + start, end = match.span() + if start > pos: + data = source[pos:start] + yield lineno, 'data', escape(escape_repl, data) + lineno += data.count('\n') + is_comment, is_code, cmd, args = match.groups() + if is_code: + yield lineno, 'code', args + elif not is_comment: + yield lineno, 'cmd', (cmd, args) + lineno += source[start:end].count('\n') + pos = end + + if pos < len(source): + yield lineno, 'data', escape(escape_repl, source[pos:]) + + +def transform(node, filename): + root = ast.Module(None, node, lineno=1) + nodes = [root] + while nodes: + node = nodes.pop() + node.filename = filename + if node.__class__ in (ast.Printnl, ast.Print): + node.dest = ast.Name('__context') + elif node.__class__ is ast.Const and isinstance(node.value, str): + try: + node.value.decode('ascii') + except UnicodeError: + node.value = node.value.decode('utf-8') + nodes.extend(node.getChildNodes()) + return root + + +class TemplateSyntaxError(SyntaxError): + + def __init__(self, msg, filename, lineno): + from linecache import getline + l = getline(filename, lineno) + SyntaxError.__init__(self, msg, (filename, lineno, len(l) or 1, l)) + + +class Parser(object): + + def __init__(self, gen, filename): + self.gen = gen + self.filename = filename + self.lineno = 1 + + def fail(self, msg): + raise TemplateSyntaxError(msg, self.filename, self.lineno) + + def parse_python(self, expr, type='exec'): + if isinstance(expr, unicode): + expr = '\xef\xbb\xbf' + expr.encode('utf-8') + try: + node = parse(expr, type) + except SyntaxError, e: + raise TemplateSyntaxError(str(e), self.filename, + self.lineno + e.lineno - 1) + nodes = [node] + while nodes: + n = nodes.pop() + if hasattr(n, 'lineno'): + n.lineno = (n.lineno or 1) + self.lineno - 1 + nodes.extend(n.getChildNodes()) + return node.node + + def parse(self, needle=()): + start_lineno = self.lineno + result = [] + add = result.append + for self.lineno, token, value in self.gen: + if token == 'data': + add(self.parse_data(value)) + elif token == 'code': + add(self.parse_code(value.splitlines())) + elif token == 'cmd': + name, args = value + if name in needle: + return name, args, ast.Stmt(result, lineno=start_lineno) + if name in ('for', 'while'): + add(self.parse_loop(args, name)) + elif name == 'if': + add(self.parse_if(args)) + else: + self.fail('unknown directive %s' % name) + if needle: + self.fail('unexpected end of template') + return ast.Stmt(result, lineno=start_lineno) + + def parse_loop(self, args, type): + rv = self.parse_python('%s %s: pass' % (type, args), 'exec').nodes[0] + tag, value, rv.body = self.parse(('end' + type, 'else')) + if value: + self.fail('unexpected data after ' + tag) + if tag == 'else': + tag, value, rv.else_ = self.parse(('end' + type,)) + if value: + self.fail('unexpected data after else') + return rv + + def parse_if(self, args): + cond = self.parse_python('if %s: pass' % args).nodes[0] + tag, value, body = self.parse(('else', 'elif', 'endif')) + cond.tests[0] = (cond.tests[0][0], body) + while 1: + if tag == 'else': + if value: + self.fail('unexpected data after else') + tag, value, cond.else_ = self.parse(('endif',)) + elif tag == 'elif': + expr = self.parse_python(value, 'eval') + tag, value, body = self.parse(('else', 'elif', 'endif')) + cond.tests.append((expr, body)) + continue + break + if value: + self.fail('unexpected data after endif') + return cond + + def parse_code(self, lines): + margin = sys.maxint + for line in lines[1:]: + content = len(line.lstrip()) + if content: + indent = len(line) - content + margin = min(margin, indent) + if lines: + lines[0] = lines[0].lstrip() + if margin < sys.maxint: + for i in xrange(1, len(lines)): + lines[i] = lines[i][margin:] + while lines and not lines[-1]: + lines.pop() + while lines and not lines[0]: + lines.pop(0) + return self.parse_python('\n'.join(lines)) + + def parse_data(self, text): + start_lineno = lineno = self.lineno + pos = 0 + end = len(text) + nodes = [] + + def match_or_fail(pos): + match = token_re.match(text, pos) + if match is None: + self.fail('invalid syntax') + return match.group().strip(), match.end() + + def write_expr(code): + node = self.parse_python(code, 'eval') + nodes.append(call_stmt('__to_unicode', [node], lineno)) + return code.count('\n') + + def write_data(value): + if value: + nodes.append(ast.Const(value, lineno=lineno)) + return value.count('\n') + return 0 + + while 1: + offset = text.find('$', pos) + if offset < 0: + break + next = text[offset + 1] + + if next == '{': + lineno += write_data(text[pos:offset]) + pos = offset + 2 + level = 1 + while level: + token, pos = match_or_fail(pos) + if token in ('{', '}'): + level += token == '{' and 1 or -1 + lineno += write_expr(text[offset + 2:pos - 1]) + elif next in namestart_chars: + lineno += write_data(text[pos:offset]) + token, pos = match_or_fail(offset + 1) + while pos < end: + if text[pos] == '.' and pos + 1 < end and \ + text[pos + 1] in namestart_chars: + token, pos = match_or_fail(pos + 1) + elif text[pos] in '([': + pos += 1 + level = 1 + while level: + token, pos = match_or_fail(pos) + if token in ('(', ')', '[', ']'): + level += token in '([' and 1 or -1 + else: + break + lineno += write_expr(text[offset + 1:pos]) + else: + lineno += write_data(text[pos:offset + 1]) + pos = offset + 1 + (next == '$') + write_data(text[pos:]) + + return ast.Discard(call_stmt(len(nodes) == 1 and '__write' or + '__write_many', nodes, start_lineno), + lineno=start_lineno) + + +class Context(object): + + def __init__(self, namespace, charset, errors): + self.charset = charset + self.errors = errors + self._namespace = namespace + self._buffer = [] + self._write = self._buffer.append + _extend = self._buffer.extend + self.runtime = dict( + Undefined=undefined, + __to_unicode=self.to_unicode, + __context=self, + __write=self._write, + __write_many=lambda *a: _extend(a) + ) + + def write(self, value): + self._write(self.to_unicode(value)) + + def to_unicode(self, value): + if isinstance(value, str): + return _decode_unicode(value, self.charset, self.errors) + return unicode(value) + + def get_value(self, as_unicode=True): + rv = u''.join(self._buffer) + if not as_unicode: + return rv.encode(self.charset, self.errors) + return rv + + def __getitem__(self, key, default=undefined): + try: + return self._namespace[key] + except KeyError: + return getattr(builtins, key, default) + + def get(self, key, default=None): + return self.__getitem__(key, default) + + def __setitem__(self, key, value): + self._namespace[key] = value + + def __delitem__(self, key): + del self._namespace[key] + + +class TemplateCodeGenerator(ModuleCodeGenerator): + + def __init__(self, node, filename): + ModuleCodeGenerator.__init__(self, transform(node, filename)) + + def _nameOp(self, prefix, name): + if name in runtime_vars: + return self.emit(prefix + '_GLOBAL', name) + return ModuleCodeGenerator._nameOp(self, prefix, name) + + +class Template(object): + """Represents a simple text based template. It's a good idea to load such + templates from files on the file system to get better debug output. + """ + + default_context = { + 'escape': utils.escape, + 'url_quote': urls.url_quote, + 'url_quote_plus': urls.url_quote_plus, + 'url_encode': urls.url_encode + } + + def __init__(self, source, filename='