1 """
2 Infrastructure pages.
3 """
4
5
6
7
8
9
10
11 import datetime
12 import io
13 import os
14 import time
15
16 import pkg_resources
17 from nevow import inevow
18 from nevow import rend
19 from nevow import static
20 from nevow import url
21 from twisted.internet import reactor
22 from twisted.web import http
23
24 from gavo import base
25 from gavo import registry
26 from gavo import svcs
27 from gavo import utils
28 from gavo.base import meta
29 from gavo.votable import V
30 from gavo.web import caching
31 from gavo.web import grend
32
33
34 try:
35 import rjsmin
36 except ImportError:
37 from gavo.imp import rjsmin
38
39 -class LoginPage(rend.Page, grend.GavoRenderMixin):
40 """a page that logs people in or out.
41
42 You should usually give a nextURL parameter in the context, the page
43 the user is returned to afte login.
44
45 If the user is already authenticated, this will do a logout (by
46 sending a 403).
47 """
48 - def __init__(self, ctx):
49 rend.Page.__init__(self)
50 self.request = inevow.IRequest(ctx)
51 self.nextURL = self.request.args.get("nextURL", ["/"])[0]
52
53 - def render_nextURL(self, ctx, data):
54 return ctx.tag(href=self.nextURL)
55
56 - def render_iflogged(self, ctx, data):
57 if self.request.getUser():
58 return ctx.tag
59 return ""
60
61 - def render_ifnotlogged(self, ctx, data):
62 if not self.request.getUser():
63 return ctx.tag
64 return ""
65
66 - def data_loggedUser(self, ctx, data):
67 return self.request.getUser()
68
69 - def renderHTTP(self, ctx):
70 relogging = base.parseBooleanLiteral(utils.getfirst(
71 self.request.args, "relog", default="False"))
72 if self.request.getUser():
73 if relogging:
74 raise svcs.Authenticate()
75 else:
76 return url.URL.fromContext(ctx).click(self.nextURL)
77 else:
78 if relogging:
79 return url.URL.fromContext(ctx).click(self.nextURL)
80 else:
81 raise svcs.Authenticate()
82
83 docFactory = svcs.loadSystemTemplate("loginout.html")
84
85
91
92
93 -class TemplatedPage(grend.CustomTemplateMixin, grend.ServiceBasedPage):
94 """a "server-wide" template.
95
96 For now, they all are based on the dc root service.
97 """
98 checkedRenderer = False
99 - def __init__(self, ctx, fName):
100 self.customTemplate = fName
101 grend.ServiceBasedPage.__init__(self, ctx,
102 base.caches.getRD(registry.SERVICELIST_ID
103 ).getById("root"))
104 self.metaCarrier = meta.MetaMixin()
105 self.metaCarrier.setMetaParent(self.service)
106 self.metaCarrier.setMeta("dateUpdated",
107 utils.formatISODT(
108 datetime.datetime.fromtimestamp(os.path.getmtime(fName))))
109
110
112 """returns javascript in path minified.
113
114 You can turn off auto-minification by setting [web] jsSource to True;
115 that's sometimes convenient while debugging the javascript.
116
117 If jsSource is false (the default), changes to javascript are only picked
118 up on a server reload.
119 """
120 with open(path) as f:
121 if base.getConfig("web", "jsSource"):
122 return f.read()
123 else:
124 return rjsmin.jsmin(f.read())
125
126
128 """renders fName as a template on the root service.
129 """
130 return TemplatedPage(ctx, fName)
131
132
134 """a file from the file system, served pretty directly.
135
136 Some of these static files can be javascript (that's usually minified
137 on the fly) or nevow templates. These we want to cache. For everything
138 else, we don't win anything by caching, but, when we serve large data,
139 we can DoS ourselves. So, we decide manually whether to cache.
140
141 The caches are bound to an RD passed in as cacheRD at construction time.
142 For system resources, that should be getRD(registry.SERVICELIST_ID).
143
144 On-the-fly processing is based on certain magic mime types:
145 text/nevow-template is rendered and application/javascript is minified.
146 """
147 defaultType = "application/octet-stream"
148
149 magicMimes = {
150 "text/nevow-template": expandTemplate,
151 "application/javascript": minifyJS,
152 }
153
156
158 ext = os.path.splitext(self.fName)[-1]
159 return static.File.contentTypes.get(ext, self.defaultType)
160
164
189
190
191
192
193
194
195
196
198 """synthesises a query-gavo.js from Debian packages.
199 """
200 INSTALL_ROOT = "/usr/share/javascript/"
201 PARTS = ["jquery/jquery.js",
202 io.BytesIO("""
203 (function( factory ) {
204 if ( typeof define === "function" && define.amd ) {
205
206 // AMD. Register as an anonymous module.
207 define([ "jquery" ], factory );
208 } else {
209
210 // Browser globals
211 factory( jQuery );
212 }
213 }(function( $ ) {
214
215 $.ui = $.ui || {};
216
217 var version = $.ui.version = "1.12.1";
218 """),
219 "jquery-ui/ui/widget.js",
220 "jquery-ui/ui/position.js",
221 "jquery-ui/ui/data.js",
222 "jquery-ui/ui/disable-selection.js",
223 "jquery-ui/ui/focusable.js",
224 "jquery-ui/ui/form-reset-mixin.js",
225 "jquery-ui/ui/jquery-1-7.js",
226 "jquery-ui/ui/keycode.js",
227 "jquery-ui/ui/labels.js",
228 "jquery-ui/ui/scroll-parent.js",
229 "jquery-ui/ui/tabbable.js",
230 "jquery-ui/ui/unique-id.js",
231 "jquery-ui/ui/widgets/mouse.js",
232 io.BytesIO("""
233 var plugin = $.ui.plugin = {
234 add: function( module, option, set ) {
235 var i,
236 proto = $.ui[ module ].prototype;
237 for ( i in set ) {
238 proto.plugins[ i ] = proto.plugins[ i ] || [];
239 proto.plugins[ i ].push( [ option, set[ i ] ] );
240 }
241 },
242 call: function( instance, name, args, allowDisconnected ) {
243 var i,
244 set = instance.plugins[ name ];
245
246 if ( !set ) {
247 return;
248 }
249
250 if ( !allowDisconnected && ( !instance.element[ 0 ].parentNode ||
251 instance.element[ 0 ].parentNode.nodeType === 11 ) ) {
252 return;
253 }
254
255 for ( i = 0; i < set.length; i++ ) {
256 if ( instance.options[ set[ i ][ 0 ] ] ) {
257 set[ i ][ 1 ].apply( instance.element, args );
258 }
259 }
260 }
261 };
262 """),
263 "jquery-ui/ui/widgets/draggable.js",
264 "jquery-ui/ui/widgets/resizable.js",
265
266 io.BytesIO("}));"),
267 ]
268
272
273 - def _getContent(self):
274 parts = []
275 for part in self.PARTS:
276 if hasattr(part, "read"):
277 parts.append(part.read())
278 else:
279 with open(os.path.join(self.INSTALL_ROOT, part)) as f:
280 parts.append(f.read())
281 stuff = "\n".join(parts)
282
283 self.modStamp = time.time()
284 if base.getConfig("web", "jsSource"):
285 return stuff
286 else:
287 return rjsmin.jsmin(stuff)
288
290 if self.content is None:
291 self.content = self._getContent()
292
293 request = inevow.IRequest(ctx)
294 if request.setLastModified(self.modStamp) is http.CACHED:
295 return ''
296
297 return self.content
298
299
300 DEBIAN_FALLBACKS = {
301 'js/jquery-gavo.js': GavoJquery(),
302 'js/jquery.flot.js':
303 "/usr/share/javascript/jquery-flot/jquery.flot.js",
304 }
305
306
307
309 """is a server for various static files.
310
311 This is basically like static.File, except
312
313 - we don't do directory listings
314 - we don't bother with ranges
315 - we look for each file in a user area and then in the system area.
316 """
318 rend.Page.__init__(self)
319 self.userPath = utils.ensureOneSlash(
320 os.path.abspath(os.path.join(base.getConfig("webDir"), "nv_static")))
321 self.systemPath = utils.ensureOneSlash(
322 os.path.abspath(pkg_resources.resource_filename('gavo', "resources/web")))
323
326
328 relPath = "/".join(segments)
329 associatedRD = base.caches.getRD(registry.SERVICELIST_ID)
330
331 path = os.path.abspath(self.userPath+relPath)
332 if os.path.exists(path):
333 if not path.startswith(self.userPath):
334 raise svcs.ForbiddenURI(
335 "%s is not located in the static user directory"%path)
336 return StaticFile(path, associatedRD), ()
337
338 path = os.path.abspath(self.systemPath+relPath)
339 if os.path.exists(path):
340 if not path.startswith(self.systemPath):
341 raise svcs.ForbiddenURI(
342 "%s is not located in the static system directory"%path)
343 return StaticFile(path, associatedRD), ()
344
345 if relPath in DEBIAN_FALLBACKS:
346 res = DEBIAN_FALLBACKS[relPath]
347 if isinstance(res, basestring):
348 return StaticFile(res, associatedRD), ()
349 else:
350 return res, ()
351
352 raise svcs.UnknownURI("No matching file,"
353 " neither built-in nor user-provided")
354
355 processors = {
356 ".shtml": _replaceConfigStrings,
357 }
358
359
361 """A page combining some built-in robots.txt material with etc/robots.txt
362 if it exists.
363 """
364 builtin = utils.fixIndentation("""
365 Disallow: /login
366 Disallow: /seffe
367 """, "")
368
369 - def _getContent(self):
370 content = self.builtin
371 try:
372 with open(os.path.join(base.getConfig("webDir"), "robots.txt")) as f:
373 content = content+"\n"+f.read()
374 except IOError:
375 pass
376 return content
377
379 request = inevow.IRequest(ctx)
380 request.setHeader("content-type", "text/plain")
381 return self._getContent()
382
385
386
388 """A page to be rendered in emergencies.
389
390 Essentially, this is a 503 with a text taken from stateDir/MAINT.
391
392 Root checks for the presence of that file before returning this
393 page, so (ignoring race conditions) this page assumes it's there.
394 """
396 request = inevow.IRequest(ctx)
397 request.setResponseCode(503)
398 request.setHeader("retry-after", "3600")
399
400 with open(os.path.join(base.getConfig("stateDir"), "MAINT")) as f:
401 try:
402 maintText = f.read().decode("utf-8")
403 except IOError:
404 maintText = "Unspecified problem"
405
406 vot = V.VOTABLE[
407 V.RESOURCE(type="results")[
408 V.INFO(name="QUERY_STATUS", value="ERROR")[
409 maintText]
410 ]]
411
412 request.setHeader("content-type", "text/xml")
413 return ("<?xml-stylesheet href='/static"
414 "/xsl/dalierror-to-html.xsl' type='text/xsl'?>"
415 +vot.render())
416
417
419 """A handler for the .well-known hierarchy.
420
421 We only do something for ACME at this point.
422 """
423 acmeChallengeDir = os.path.join(base.getConfig("statedir"), "acme-challenge")
424
432
433
435 """A page returning an approximate number of clients served currently.
436 """
438 request = inevow.IRequest(ctx)
439 request.setHeader("content-type", "text/plain")
440
441 hostsActive = len([1
442 for r in reactor.getReaders()
443 if hasattr(r, "getHost") and r.getHost().host!='0.0.0.0'])
444 return str(hostsActive)+"\n"
445