| Left: | ||
| Right: |
| LEFT | RIGHT |
|---|---|
| 1 # This file is part of the Adblock Plus web scripts, | 1 # This file is part of the Adblock Plus web scripts, |
| 2 # Copyright (C) 2006-present eyeo GmbH | 2 # Copyright (C) 2006-present eyeo GmbH |
| 3 # | 3 # |
| 4 # Adblock Plus is free software: you can redistribute it and/or modify | 4 # Adblock Plus is free software: you can redistribute it and/or modify |
| 5 # it under the terms of the GNU General Public License version 3 as | 5 # it under the terms of the GNU General Public License version 3 as |
| 6 # published by the Free Software Foundation. | 6 # published by the Free Software Foundation. |
| 7 # | 7 # |
| 8 # Adblock Plus is distributed in the hope that it will be useful, | 8 # Adblock Plus is distributed in the hope that it will be useful, |
| 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of | 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 11 # GNU General Public License for more details. | 11 # GNU General Public License for more details. |
| 12 # | 12 # |
| 13 # You should have received a copy of the GNU General Public License | 13 # You should have received a copy of the GNU General Public License |
| 14 # along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>. | 14 # along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>. |
| 15 | 15 |
| 16 from __future__ import print_function | 16 from __future__ import print_function |
| 17 | 17 |
| 18 import os | 18 import os |
| 19 import mimetypes | 19 import mimetypes |
| 20 from argparse import ArgumentParser | 20 import argparse |
| 21 | 21 |
| 22 import jinja2 | 22 import jinja2 |
| 23 | 23 |
| 24 from cms.converters import converters | 24 from cms.converters import converters |
| 25 from cms.utils import process_page | 25 from cms.utils import process_page |
| 26 from cms.sources import create_source | 26 from cms.sources import create_source |
| 27 | |
| 28 | |
| 29 class Parameters: | |
|
Vasily Kuznetsov
2018/10/16 13:18:23
As we discussed via IRC, let's make this whole thi
Tudor Avram
2018/10/18 13:44:05
Done.
| |
| 30 source = None | |
| 31 host = None | |
| 32 port = None | |
| 33 base_url = None | |
| 34 | |
| 35 | |
| 36 MIME_FORMAT = '{}' | |
| 37 | 27 |
| 38 UNICODE_ENCODING = 'utf-8' | 28 UNICODE_ENCODING = 'utf-8' |
| 39 | 29 |
| 40 ERROR_TEMPLATE = ''' | 30 ERROR_TEMPLATE = ''' |
| 41 <html> | 31 <html> |
| 42 <head> | 32 <head> |
| 43 <title>{{status}}</title> | 33 <title>{{status}}</title> |
| 44 </head> | 34 </head> |
| 45 <body> | 35 <body> |
| 46 <h1>{{status}}</h1> | 36 <h1>{{status}}</h1> |
| 47 {% set code = status.split()|first|int %} | 37 {% set code = status.split()|first|int %} |
| 48 {% if code == 404 %} | 38 {% if code == 404 %} |
| 49 <p>No page found for the address {{uri}}.</p> | 39 <p>No page found for the address {{uri}}.</p> |
| 50 {% elif code == 500 %} | 40 {% elif code == 500 %} |
| 51 <p>An error occurred while processing the request for {{uri}}:</p> | 41 <p>An error occurred while processing the request for {{uri}}:</p> |
| 52 <pre>{{error}}</pre> | 42 <pre>{{error}}</pre> |
| 53 {% endif %} | 43 {% endif %} |
| 54 </body> | 44 </body> |
| 55 </html>''' | 45 </html>''' |
| 56 | 46 |
| 57 # Initialize the mimetypes modules manually for consistent behavior, | 47 # Initialize the mimetypes modules manually for consistent behavior, |
| 58 # ignoring local files and Windows Registry. | 48 # ignoring local files and Windows Registry. |
| 59 mimetypes.init([]) | 49 mimetypes.init([]) |
| 60 | 50 |
| 61 | 51 |
| 62 def get_data(path): | 52 class DynamicServerHandler: |
| 63 """Read the data corresponding to a page. | 53 """General-purpose WSGI server handler that generates pages on request. |
| 64 | 54 |
| 65 Parameters | 55 Parameters |
| 66 ---------- | 56 ---------- |
| 67 path: str | 57 host: str |
| 68 The path to the page to get the data for. | 58 The host where the server will run. |
| 59 port: int | |
| 60 The TCP port the server will run on. | |
| 61 source_dir: str | |
| 62 The path to the website contents. | |
| 63 | |
| 64 """ | |
| 65 | |
| 66 def __init__(self, host, port, source_dir): | |
| 67 self.host = host | |
| 68 self.port = port | |
| 69 self.source = create_source(source_dir) | |
| 70 self.full_url = 'http://{0}:{1}'.format(host, port) | |
| 71 | |
| 72 def _get_data(self, path): | |
| 73 """Read the data corresponding to a website path. | |
| 74 | |
| 75 Parameters | |
| 76 ---------- | |
| 77 path: str | |
| 78 The path to the page to get the data for. | |
| 79 | |
| 80 Returns | |
| 81 ------- | |
| 82 str or bytes | |
| 83 The data corresponding to the path we're trying to access. | |
| 84 | |
| 85 """ | |
| 86 if self.source.has_static(path): | |
| 87 return self.source.read_static(path) | |
| 88 | |
| 89 page, data = self._get_page(path) | |
| 90 | |
| 91 if page and self._has_conflicts(page): | |
| 92 raise Exception('The requested page conflicts with another page.') | |
| 93 | |
| 94 return data | |
| 95 | |
| 96 def _get_page(self, path): | |
| 97 """Construct a page and return its contents. | |
| 98 | |
| 99 Parameters | |
| 100 ---------- | |
| 101 path: str | |
| 102 The path of the page we want to construct. | |
| 103 | |
| 104 Returns | |
| 105 ------- | |
| 106 (page_name, page_contents): (str, str) | |
| 107 | |
| 108 """ | |
| 109 path = path.strip('/') | |
| 110 if path == '': | |
| 111 locale, page = self.source.read_config().get('general', | |
| 112 'defaultlocale'), '' | |
| 113 elif '/' in path: | |
| 114 locale, page = path.split('/', 1) | |
| 115 else: | |
| 116 locale, page = path, '' | |
| 117 | |
| 118 default_page = self.source.read_config().get('general', 'defaultpage') | |
| 119 possible_pages = [page, '/'.join([page, default_page]).lstrip('/')] | |
| 120 | |
| 121 for page_format in converters.iterkeys(): | |
| 122 for p in possible_pages: | |
| 123 if self.source.has_page(p, page_format): | |
| 124 return p, process_page(self.source, locale, p, page_format, | |
| 125 self.full_url) | |
| 126 | |
| 127 if self.source.has_localizable_file(locale, page): | |
| 128 return page, self.source.read_localizable_file(locale, page) | |
| 129 | |
| 130 return None, None | |
| 131 | |
| 132 def _has_conflicts(self, page): | |
| 133 """Check if a page has conflicts. | |
| 134 | |
| 135 A page has conflicts if there are other pages with the same name. | |
| 136 | |
| 137 Parameters | |
| 138 ---------- | |
| 139 page: str | |
| 140 The path of the page we're checking for conflicts. | |
| 141 | |
| 142 Returns | |
| 143 ------- | |
| 144 bool | |
| 145 True - if the page has conflicts | |
| 146 False - otherwise | |
| 147 | |
| 148 """ | |
| 149 pages = [p for p, _ in self.source.list_pages()] | |
| 150 pages.extend(self.source.list_localizable_files()) | |
| 151 | |
| 152 if pages.count(page) > 1: | |
| 153 return True | |
| 154 if any(p.startswith(page + '/') or page.startswith(p + '/') for p in | |
| 155 pages): | |
| 156 return True | |
| 157 return False | |
| 158 | |
| 159 def get_error_page(self, start_response, status, **kw): | |
| 160 """Create and display an error page. | |
| 161 | |
| 162 Parameters | |
| 163 ---------- | |
| 164 start_response: function | |
| 165 It will be called before constructing the error page, to setup | |
| 166 things like the status of the response and the headers. | |
| 167 status: str | |
| 168 The status of the response we're sending the error page with. | |
| 169 Needs to have the following format: | |
| 170 "<status_code> <status_message>" | |
| 171 kw: dict | |
| 172 Any additional arguments that will be passed onto the `stream` | |
| 173 method of a `jinja2 Template`. | |
| 174 | |
| 175 Returns | |
| 176 ------- | |
| 177 generator of utf8 strings | |
| 178 Fragments of the corresponding error HTML template. | |
| 179 | |
| 180 """ | |
| 181 env = jinja2.Environment(autoescape=True) | |
| 182 page_template = env.from_string(ERROR_TEMPLATE) | |
| 183 mime = 'text/html; encoding={}'.format(UNICODE_ENCODING) | |
| 184 | |
| 185 start_response(status, [('Content-Type', mime)]) | |
| 186 | |
| 187 for fragment in page_template.stream(status=status, **kw): | |
| 188 yield fragment.encode(UNICODE_ENCODING) | |
| 189 | |
| 190 def __call__(self, environ, start_response): | |
| 191 """Execute the handler, according to the WSGI standards. | |
| 192 | |
| 193 Parameters | |
| 194 --------- | |
| 195 environ: dict | |
| 196 The environment under which the page is requested. | |
| 197 The requested page must be under the `PATH_INFO` key. | |
| 198 start_response: function | |
| 199 Used to initiate a response. Must take two arguments, in this | |
| 200 order: | |
| 201 - Response status, in the format "<code> <message>". | |
| 202 - Response headers, as a list of tuples. | |
| 203 | |
| 204 Returns | |
| 205 ------- | |
| 206 list of str | |
| 207 With the data for a specific page. | |
| 208 | |
| 209 """ | |
| 210 path = environ.get('PATH_INFO') | |
| 211 | |
| 212 data = self._get_data(path) | |
| 213 | |
| 214 if data is None: | |
| 215 return self.get_error_page(start_response, '404 Not Found', | |
| 216 uri=path) | |
| 217 | |
| 218 mime = mimetypes.guess_type(path)[0] or 'text/html' | |
| 219 | |
| 220 if isinstance(data, unicode): | |
| 221 data = data.encode(UNICODE_ENCODING) | |
| 222 mime = '{0}; charset={1}'.format(mime, UNICODE_ENCODING) | |
| 223 | |
| 224 start_response('200 OK', [('Content-Type', mime)]) | |
| 225 return [data] | |
| 226 | |
| 227 | |
| 228 def parse_arguments(): | |
| 229 """Set up and parse the arguments required by the script. | |
| 69 | 230 |
| 70 Returns | 231 Returns |
| 71 ------- | 232 ------- |
| 72 str/ bytes | 233 argparse.Namespace |
| 73 The data corresponding to the page we're trying to open. | 234 With the script arguments, as parsed. |
| 74 | 235 |
| 75 """ | 236 """ |
| 76 if Parameters.source.has_static(path): | 237 parser = argparse.ArgumentParser( |
| 77 return Parameters.source.read_static(path) | 238 description='CMS development server created to test pages locally and ' |
| 78 | 239 'on-the-fly.', |
| 79 page, data = get_page(path) | 240 ) |
| 80 if page and has_conflicts(page): | |
| 81 raise Exception('The requested page conflicts with another page') | |
| 82 return data | |
| 83 | |
| 84 | |
| 85 def get_page(path): | |
| 86 """Construct a page and return its contents. | |
| 87 | |
| 88 Parameters | |
| 89 ---------- | |
| 90 path: str | |
| 91 The path of the page we want to construct. | |
| 92 | |
| 93 Returns | |
| 94 ------- | |
| 95 (str, str) | |
| 96 With the following format: | |
| 97 <page_name, page_contents> | |
| 98 | |
| 99 """ | |
| 100 path = path.strip('/') | |
| 101 if path == '': | |
| 102 locale, page = Parameters.source.read_config().get( | |
| 103 'general', 'defaultlocale'), '' | |
| 104 elif '/' in path: | |
| 105 locale, page = path.split('/', 1) | |
| 106 else: | |
| 107 locale, page = path, '' | |
| 108 | |
| 109 default_page = Parameters.source.read_config().get('general', | |
| 110 'defaultpage') | |
| 111 possible_pages = [page, '/'.join([page, default_page]).lstrip('/')] | |
| 112 | |
| 113 for page_format in converters.iterkeys(): | |
| 114 for p in possible_pages: | |
| 115 if Parameters.source.has_page(p, page_format): | |
| 116 return p, process_page(Parameters.source, locale, p, | |
| 117 page_format, Parameters.base_url) | |
| 118 | |
| 119 if Parameters.source.has_localizable_file(locale, page): | |
| 120 return page, Parameters.source.read_localizable_file(locale, page) | |
| 121 | |
| 122 return None, None | |
| 123 | |
| 124 | |
| 125 def has_conflicts(page): | |
| 126 """Check if a page has conflicts. | |
| 127 | |
| 128 A page has conflicts if there are other pages with the same name. | |
| 129 Parameters | |
| 130 ---------- | |
| 131 page: str | |
| 132 The path of the page we're checking for conflicts. | |
| 133 | |
| 134 Returns | |
| 135 ------- | |
| 136 bool | |
| 137 True - if the page has conflicts | |
| 138 False - otherwise | |
| 139 | |
| 140 """ | |
| 141 pages = [p for p, _ in Parameters.source.list_pages()] | |
| 142 pages.extend(Parameters.source.list_localizable_files()) | |
| 143 | |
| 144 if pages.count(page) > 1: | |
| 145 return True | |
| 146 if any(p.startswith(page + '/') or page.startswith(p + '/') | |
| 147 for p in pages): | |
| 148 return True | |
| 149 return False | |
| 150 | |
| 151 | |
| 152 def get_error_page(start_response, status, **kw): | |
| 153 """Create and display an error page. | |
| 154 | |
| 155 Parameters | |
| 156 ---------- | |
| 157 start_response: function | |
| 158 It will be called before constructing the error page, to setup | |
| 159 things like the status of the response and the headers. | |
| 160 status: str | |
| 161 The status of the response we're sending the error page with. | |
| 162 Needs to have the following format: "<status_code> <status_message>" | |
| 163 kw: dict | |
| 164 Any additional arguments that will be passed onto the `stream` method | |
| 165 of a `jinja2 Template`. | |
| 166 | |
| 167 Returns | |
| 168 ------- | |
| 169 generator | |
| 170 of utf8 strings - fragments of the corresponding error HTML template. | |
| 171 | |
| 172 """ | |
| 173 env = jinja2.Environment(autoescape=True) | |
| 174 page_template = env.from_string(ERROR_TEMPLATE) | |
| 175 mime = 'text/html; encoding={}'.format(UNICODE_ENCODING) | |
| 176 | |
| 177 start_response(status, [('Content-Type', mime)]) | |
| 178 | |
| 179 for fragment in page_template.stream(status=status, **kw): | |
| 180 yield fragment.encode(UNICODE_ENCODING) | |
| 181 | |
| 182 | |
| 183 def set_parameters(): | |
| 184 """Set the arguments required to run the script. | |
| 185 | |
| 186 Performs the following actions: | |
| 187 1. Setup the script's argument parser | |
| 188 2. Read the arguments provided when running the script | |
| 189 3. Set the fields of the Parameters namespace | |
| 190 | |
| 191 """ | |
| 192 parser = ArgumentParser(description='CMS development server created to ' | |
| 193 'test pages locally and on-the-fly') | |
| 194 | 241 |
| 195 parser.add_argument('path', default=os.curdir, nargs='?', | 242 parser.add_argument('path', default=os.curdir, nargs='?', |
| 196 help='Path to the website we intend to run. If not ' | 243 help='Path to the website we intend to run. If not ' |
| 197 'provided, defaults, to the current directory.') | 244 'provided, defaults, to the current directory.') |
| 198 parser.add_argument('--host', default='localhost', | 245 parser.add_argument('--host', default='localhost', |
| 199 help='Address of the host the server will listen on. ' | 246 help='Address of the host the server will listen on. ' |
| 200 'Defaults to "localhost".') | 247 'Defaults to "localhost".') |
| 201 parser.add_argument('--port', default=5000, type=int, | 248 parser.add_argument('--port', default=5000, type=int, |
| 202 help='TCP port the server will listen on. Default ' | 249 help='TCP port the server will listen on. Default ' |
| 203 '5000.') | 250 '5000.') |
| 204 | 251 |
| 205 args = parser.parse_args() | 252 return parser.parse_args() |
| 206 | 253 |
| 207 Parameters.source = create_source(args.path) | 254 |
| 208 Parameters.host = args.host | 255 def run_werkzeug_server(handler, **kw): |
| 209 Parameters.port = args.port | 256 """Set up a server that uses `werkzeug`. |
| 210 Parameters.base_url = 'http://{0}:{1}'.format(args.host, args.port) | |
| 211 | |
| 212 | |
| 213 def handler(environ, start_response): | |
| 214 """Handle a request for a page. | |
| 215 | 257 |
| 216 Parameters | 258 Parameters |
| 217 ---------- | 259 ---------- |
| 218 environ: dict | 260 handler: DynamicServerHandler |
| 219 The environment under which the request si made. | 261 Defines the parameters and methods required to handle requests. |
| 220 start_response: function | |
| 221 Used to initiate a request response. | |
| 222 | |
| 223 Returns | |
| 224 ------- | |
| 225 [str] | |
| 226 With the response body. | |
| 227 | |
| 228 """ | |
| 229 path = environ.get('PATH_INFO') | |
| 230 | |
| 231 data = get_data(path) | |
| 232 | |
| 233 if data is None: | |
| 234 return get_error_page(start_response, '404 Not Found', uri=path) | |
| 235 | |
| 236 mime = mimetypes.guess_type(path)[0] or 'text/html' | |
| 237 | |
| 238 if isinstance(data, unicode): | |
| 239 data = data.encode(UNICODE_ENCODING) | |
| 240 mime = '{0}; charset={1}'.format(mime, UNICODE_ENCODING) | |
| 241 | |
| 242 start_response('200 OK', [('Content-Type', mime)]) | |
| 243 return [data] | |
| 244 | |
| 245 | |
| 246 def make_werkzeug_server(): | |
| 247 """Set up a server that uses `werkzeug`. | |
| 248 | |
| 249 Returns | |
| 250 ------- | |
| 251 function | |
| 252 Used to run the server | |
| 253 | 262 |
| 254 Raises | 263 Raises |
| 255 ------ | 264 ------ |
| 256 ImportError | 265 ImportError |
| 257 If the package `werkzeug` is not installed | 266 If the package `werkzeug` is not installed |
| 258 | 267 |
| 259 """ | 268 """ |
| 260 from werkzeug.serving import run_simple | 269 from werkzeug.serving import run_simple |
| 261 | 270 import logging |
| 262 def run_func(*args, **kwargs): | 271 |
| 272 def run(*args, **kwargs): | |
| 263 # The werkzeug logger must be configured before the | 273 # The werkzeug logger must be configured before the |
| 264 # root logger. Also we must prevent it from propagating | 274 # root logger. Also we must prevent it from propagating |
| 265 # messages, otherwise messages are logged twice. | 275 # messages, otherwise messages are logged twice. |
| 266 import logging | |
| 267 logger = logging.getLogger('werkzeug') | 276 logger = logging.getLogger('werkzeug') |
| 268 logger.propagate = False | 277 logger.propagate = False |
| 269 logger.setLevel(logging.INFO) | 278 logger.setLevel(logging.INFO) |
| 270 logger.addHandler(logging.StreamHandler()) | 279 logger.addHandler(logging.StreamHandler()) |
| 271 | 280 |
| 272 run_simple(threaded=True, *args, **kwargs) | 281 run_simple(threaded=True, *args, **kwargs) |
| 273 | 282 |
| 274 return run_func | 283 run(handler.host, handler.port, handler, **kw) |
| 275 | 284 |
| 276 | 285 |
| 277 def make_builtins_server(): | 286 def run_builtins_server(handler, **kw): |
| 278 """Configure a server that only uses builtin packages. | 287 """Configure a server that only uses builtin packages. |
| 279 | 288 |
| 280 Returns | 289 Parameters |
| 281 ------- | 290 ---------- |
| 282 function | 291 handler: DynamicServerHandler |
| 283 Used to run the server. | 292 Defines the parameters and methods required to handle requests. |
| 284 | 293 |
| 285 """ | 294 """ |
| 286 from SocketServer import ThreadingMixIn | 295 from SocketServer import ThreadingMixIn |
| 287 from wsgiref.simple_server import WSGIServer, make_server | 296 from wsgiref.simple_server import WSGIServer, make_server |
| 288 | 297 |
| 289 class ThreadedWSGIServer(ThreadingMixIn, WSGIServer): | 298 class ThreadedWSGIServer(ThreadingMixIn, WSGIServer): |
| 290 daemon_threads = True | 299 daemon_threads = True |
| 291 | 300 |
| 292 def run(host, port, app, **kwargs): | 301 def run(host, port, app, **kwargs): |
| 293 def wrapper(environ, start_response): | 302 def wrapper(environ, start_response): |
| 294 try: | 303 try: |
| 295 return app(environ, start_response) | 304 return app(environ, start_response) |
| 296 except Exception as e: | 305 except Exception as e: |
| 297 return get_error_page(start_response, | 306 return handler.get_error_page( |
| 298 '500 Internal Server Error', | 307 start_response, '500 Internal Server Error', |
| 299 uri=environ.get('PATH_INFO'), error=e) | 308 uri=environ.get('PATH_INFO'), error=e, |
| 300 | 309 ) |
| 301 server = make_server(host, port, wrapper, | 310 |
| 302 ThreadedWSGIServer) | 311 server = make_server(host, port, wrapper, ThreadedWSGIServer) |
| 303 print(' * Running on {0}:{1}'.format(*server.server_address)) | 312 print(' * Running on {0}:{1}'.format(*server.server_address)) |
| 304 server.serve_forever() | 313 server.serve_forever() |
| 305 | 314 |
| 306 return run | 315 run(handler.host, handler.port, handler, **kw) |
| 307 | 316 |
| 308 | 317 |
| 309 def main(): | 318 def main(): |
| 310 set_parameters() | 319 args = parse_arguments() |
| 320 handler = DynamicServerHandler(args.host, args.port, args.path) | |
| 321 | |
| 311 try: | 322 try: |
| 312 run = make_werkzeug_server() | 323 run_werkzeug_server(handler, use_reloader=True, use_debugger=True) |
| 313 except ImportError: | 324 except ImportError: |
| 314 run = make_builtins_server() | 325 run_builtins_server(handler) |
| 315 run(Parameters.host, Parameters.port, handler, use_reloader=True, | |
| 316 use_debugger=True) | |
| 317 | 326 |
| 318 | 327 |
| 319 if __name__ == '__main__': | 328 if __name__ == '__main__': |
| 320 main() | 329 main() |
| LEFT | RIGHT |