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 |