server.py 21.5 KB
Newer Older
1
from flask import Flask, g, request, url_for, redirect, session, render_template, flash, Response, make_response
2
from werkzeug.routing import Rule
3
from functools import wraps
Julian Rother's avatar
Julian Rother committed
4
from datetime import date, timedelta, datetime, time, MINYEAR
5
import os
6
import sys
Julian Rother's avatar
Julian Rother committed
7
import hashlib
8
import random
9
import traceback
10
import string
11
from socket import gethostname
Julian Rother's avatar
Julian Rother committed
12
from ipaddress import ip_address, ip_network
13
import math
Julian Rother's avatar
Julian Rother committed
14
import locale
15
import base64
16
import json
Julian Rother's avatar
Julian Rother committed
17
18

locale.setlocale(locale.LC_ALL, 'de_DE.utf8')
19

20
app = Flask(__name__)
21

22
config = app.config
23
config.from_pyfile('config.py.example', silent=True)
24
25
26
if sys.argv[0].endswith('run.py'): 
	config['SQLITE_INIT_DATA'] = True
	config['DEBUG'] = True
27
config.from_pyfile('config.py', silent=True)
Andreas Valder's avatar
Andreas Valder committed
28
29
30
31
32
33
34
35
36
37
if sys.argv[0].endswith('tests.py'):
	print('running in test mode')
	import tempfile
	# ensure we always use a clean sqlite db for tests
	config['DB_ENGINE'] = 'sqlite'
	config['SQLITE_DB'] = tempfile.mktemp(prefix='flasktestingtmp')
	print('DB File: {}'.format(config['SQLITE_DB']))
	config['SQLITE_INIT_DATA'] = True
	config['SQLITE_INIT_SCHEMA'] = True
	config['DEBUG'] = True
38
	config['DISABLE_SCHEDULER'] = True
Andreas Valder's avatar
Andreas Valder committed
39
40
if config['DEBUG']:
	app.jinja_env.auto_reload = True
Andreas Valder's avatar
Andreas Valder committed
41

42
43
if not config.get('SECRET_KEY', None):
	config['SECRET_KEY'] = os.urandom(24)
Julian Rother's avatar
Julian Rother committed
44

45
46
from db import query, modify, show, searchquery
from ldap import ldapauth
47
from legacy import legacy_index
48
from scheduler import sched_func
Julian Rother's avatar
Julian Rother committed
49

50
mod_endpoints = []
Julian Rother's avatar
Julian Rother committed
51

52
def mod_required(func):
53
	mod_endpoints.append(func.__name__)
54
55
	@wraps(func)
	def decorator(*args, **kwargs):
56
		if not ismod():
57
58
59
60
61
62
			flash('Diese Funktion ist nur für Moderatoren verfügbar!')
			return redirect(url_for('login', ref=request.url))
		else:
			return func(*args, **kwargs)
	return decorator

63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
def evalperm(perms):
	cperms = []
	lperms = []
	vperms = []
	for perm in perms:
		if perm['course_id']:
			cperms.append(perm)
		elif perm['lecture_id']:
			lperms.append(perm)
		elif perm['video_id']:
			vperms.append(perm)
	if vperms:
		return vperms
	elif lperms:
	 	return lperms
	elif cperms:
		return cperms
	return [{'type': 'public'}]
81

82
from template_helper import *
83

84
app.jinja_env.globals['navbar'] = []
85
86
87
88
89
# iconlib can be 'bootstrap'
# ( see: http://getbootstrap.com/components/#glyphicons )
# or 'fa'
# ( see: http://fontawesome.io/icons/ )
def register_navbar(name, iconlib='bootstrap', icon=None):
90
	def wrapper(func):
91
		endpoint = func.__name__
92
		app.jinja_env.globals['navbar'].append((endpoint, name, iconlib, icon, not endpoint in mod_endpoints))
93
94
95
		return func
	return wrapper

Julian Rother's avatar
Cleanup    
Julian Rother committed
96
97
98
99
def render_endpoint(endpoint, flashtext=None, **kargs):
	if flashtext:
		flash(flashtext)
	# request.endpoint is used for navbar highlighting
100
	request.url_rule = Rule(request.path, endpoint=endpoint)
Julian Rother's avatar
Cleanup    
Julian Rother committed
101
102
	return app.view_functions[endpoint](**kargs)

103
104
105
106
107
108
109
def handle_errors(endpoint, text, code, *errors, **epargs):
	def wrapper(func):
		@wraps(func)
		def decorator(*args, **kwargs):
			try:
				return func(*args, **kwargs)
			except errors:
Julian Rother's avatar
Julian Rother committed
110
				if endpoint:
111
					return make_response(render_endpoint(endpoint, text, **epargs), code)
Julian Rother's avatar
Julian Rother committed
112
113
				else:
					return text, code
114
115
116
		return decorator
	return wrapper

Julian Rother's avatar
Cleanup    
Julian Rother committed
117
@app.errorhandler(404)
118
@app.route('/invalidpath')
Julian Rother's avatar
Julian Rother committed
119
def handle_not_found(e=None):
120
	return render_endpoint('index', 'Diese Seite existiert nicht!'), 404
Julian Rother's avatar
Cleanup    
Julian Rother committed
121

122
123
124
125
@app.errorhandler(500)
@app.errorhandler(Exception)
def handle_internal_error(e):
	traceback.print_exc()
126
	return render_template('500.html'), 500
127

128
129
130
131
132
133
134
135
136
137
@sched_func(5*60, firstdelay=0)
def dump_error_page():
	if 'ERROR_PAGE' not in config:
		return
	request.url_rule = Rule(request.path, endpoint='handle_internal_error')
	text = render_template('500.html')
	f = open(config['ERROR_PAGE'], 'w')
	f.write(text)
	f.close()

138
def genlive(streams):
139
140
141
142
143
144
	for stream in streams:
		stream['visible'] = True
		stream['downloadable'] = False
		stream['path'] = 'pub/hls/%s.m3u8'%stream['livehandle']
		stream['file_size'] = 0
	return streams
145
146


147
@app.route('/')
148
@register_navbar('Home', icon='home')
149
def index():
150
	# handle legacy urls...
151
152
153
	result = legacy_index()
	if result:
		return result
154

155
	start = date.today()
156
	end = start + timedelta(days=7)
157
	upcomming = query('''
158
		SELECT lectures.*, streams.active AS nowlive, "course" AS sep, courses.*
Andreas Valder's avatar
Andreas Valder committed
159
160
		FROM lectures
		JOIN courses ON (lectures.course_id = courses.id)
161
		LEFT JOIN streams ON lectures.id = streams.lecture_id
162
163
		WHERE (time > ?) AND (time < ?) AND (? OR (lectures.visible AND courses.visible AND courses.listed)) AND NOT lectures.norecording
		ORDER BY time ASC LIMIT 30''', start, end, ismod())
Andreas Valder's avatar
Andreas Valder committed
164
165
166
	for i in upcomming:
		i['date'] = i['time'].date()
	latestvideos=query('''
167
		SELECT lectures.*, "course" AS sep, courses.*
Andreas Valder's avatar
Andreas Valder committed
168
169
170
171
172
		FROM lectures
		LEFT JOIN videos ON (videos.lecture_id = lectures.id)
		LEFT JOIN courses on (courses.id = lectures.course_id)
		WHERE (? OR (courses.visible AND courses.listed AND lectures.visible AND videos.visible))
		GROUP BY videos.lecture_id
173
		ORDER BY MAX(videos.time_created) DESC
Andreas Valder's avatar
Andreas Valder committed
174
		LIMIT 6	''',ismod())
175
	livestreams = query('''SELECT streams.handle AS livehandle, lectures.*, "course" AS sep, courses.*
176
177
178
179
180
		FROM streams
		JOIN lectures ON lectures.id = streams.lecture_id
		JOIN courses ON courses.id = lectures.course_id
		WHERE streams.active AND (? OR (streams.visible AND courses.visible AND courses.listed AND lectures.visible))
		''', ismod())
Julian Rother's avatar
Julian Rother committed
181
	featured = query('SELECT * FROM featured WHERE (? OR visible) ORDER BY `order`', ismod())
182
	featured = list(filter(lambda x: not x['deleted'], featured))
Julian Rother's avatar
Julian Rother committed
183
184
185
186
	for item in featured:
		if item['type'] == 'courses':
			if item['param'] not in ['title', 'semester', 'organizer', 'subject']:
				continue
187
			item['courses'] = query('SELECT * FROM courses WHERE (visible AND listed) AND `%s` = ? ORDER BY `%s`'%(item['param'], item['param']), item['param2'])
Julian Rother's avatar
Julian Rother committed
188
189
		elif item['type'] == 'video':
			item['lecture'] = {'id': item['param']}
190
			streams = query('''SELECT streams.handle AS livehandle, streams.lecture_id, "formats" AS sep, formats.*
Julian Rother's avatar
Julian Rother committed
191
192
193
194
195
196
197
198
199
200
201
					FROM streams
					JOIN lectures ON lectures.id = streams.lecture_id
					JOIN formats ON formats.keywords = "hls"
					WHERE streams.active AND streams.visible AND lectures.id = ?
					''', item['param'])
			item['videos'] = query('''
					SELECT videos.*, "formats" AS sep, formats.*
					FROM videos
					JOIN formats ON (videos.video_format = formats.id)
					WHERE videos.lecture_id = ? AND videos.visible
					ORDER BY formats.prio DESC
202
					''', item['param'])+genlive(streams)
203
	return render_template('index.html', latestvideos=livestreams+latestvideos, upcomming=upcomming, featured=featured)
204

205
@app.route('/courses')
206
@register_navbar('Videos', icon='film')
207
def courses():
208
	courses = query('SELECT * FROM courses WHERE (? OR (visible AND listed)) ORDER BY lower(semester), lower(title)', ismod())
209
210
211
	chapters = {}
	for i in query('SELECT lectures.course_id AS id, COUNT(chapters.id) AS c FROM chapters JOIN lectures ON chapters.lecture_id = lectures.id WHERE NOT chapters.visible GROUP BY lectures.course_id'):
		chapters[i['id']] = i['c']
212
	for course in courses:
213
		course['chapter_count'] = chapters.get(course['id'], 0)
214
215
		if course['semester'] == '':
			course['semester'] = 'zeitlos'
Andreas Valder's avatar
Andreas Valder committed
216
	groupedby = request.args.get('groupedby')
Julian Rother's avatar
Julian Rother committed
217
	if groupedby not in ['title', 'semester', 'organizer', 'subject']:
Andreas Valder's avatar
Andreas Valder committed
218
		groupedby = 'semester'
219
	return render_template('courses.html', courses=courses, groupedby=groupedby)
Andreas Valder's avatar
Andreas Valder committed
220

221
222
@app.route('/<handle>')
@app.route('/<int:id>')
223
@handle_errors('courses', 'Diese Veranstaltung existiert nicht!', 404, IndexError)
224
225
def course(id=None, handle=None):
	if id:
226
		course = query('SELECT * FROM courses WHERE id = ? AND (? OR visible)', id, ismod())[0]
227
	else:
228
		course = query('SELECT * FROM courses WHERE handle = ? AND (? OR visible)', handle, ismod())[0]
229
230
	course['perm'] = query('SELECT * FROM perm WHERE (NOT perm.deleted) AND course_id = ? ORDER BY type', course['id'])
	perms = query('SELECT perm.* FROM perm JOIN lectures ON (perm.lecture_id = lectures.id) WHERE (NOT perm.deleted) AND lectures.course_id = ? ORDER BY perm.type', course['id'])
231
232
233
	chapters = {}
	for i in query('SELECT lectures.id AS id, COUNT(chapters.id) AS c FROM chapters JOIN lectures ON chapters.lecture_id = lectures.id WHERE lectures.course_id = ? GROUP BY chapters.lecture_id;', course['id']):
		chapters[i['id']] = i['c']
234
	lectures = query('SELECT * FROM lectures WHERE course_id = ? AND (? OR visible) ORDER BY time, duration DESC', course['id'], ismod())
235
	for lecture in lectures:
236
		lecture['perm'] = []
237
		lecture['perm'] += course['perm']
238
		lecture['course'] = course
239
		lecture['chapter_count'] = chapters.get(lecture['id'], 0)
240
241
242
		for perm in perms:
			if perm['lecture_id'] == lecture['id']:
				lecture['perm'].append(perm)
Andreas Valder's avatar
Andreas Valder committed
243
	videos = query('''
244
			SELECT videos.*, (videos.downloadable AND courses.downloadable) as downloadable, "formats" AS sep, formats.*
Andreas Valder's avatar
Andreas Valder committed
245
246
247
248
249
250
			FROM videos
			JOIN lectures ON (videos.lecture_id = lectures.id)
			JOIN formats ON (videos.video_format = formats.id)
			JOIN courses ON (lectures.course_id = courses.id)
			WHERE lectures.course_id= ? AND (? OR videos.visible)
			ORDER BY lectures.time, formats.prio DESC
251
			''', course['id'], ismod())
252
	livestreams = query('''SELECT streams.handle AS livehandle, streams.lecture_id, "formats" AS sep, formats.*
253
254
255
256
257
			FROM streams
			JOIN lectures ON lectures.id = streams.lecture_id
			JOIN formats ON formats.keywords = "hls"
			WHERE streams.active AND (? OR streams.visible) AND lectures.course_id = ?
			''', ismod(), course['id'])
258
259
260
	chapters = []
	if course['coursechapters']:
		chapters = query('SELECT chapters.* FROM chapters JOIN lectures ON lectures.id = chapters.lecture_id WHERE lectures.course_id = ? AND NOT chapters.deleted AND chapters.visible ORDER BY time ASC', course['id'])
261
	videos += genlive(livestreams)
262
	return render_template('course.html', course=course, lectures=lectures, videos=videos, chapters=chapters)
Andreas Valder's avatar
Andreas Valder committed
263

Andreas Valder's avatar
Andreas Valder committed
264
@app.route('/faq')
265
@register_navbar('FAQ', icon='question-sign')
Andreas Valder's avatar
Andreas Valder committed
266
def faq():
267
	return render_template('faq.html')
Andreas Valder's avatar
Andreas Valder committed
268

269
270
271
272
@app.route('/<course>/<int:id>')
@app.route('/<int:courseid>/<int:id>')
@app.route('/<course>/<int:id>/embed', endpoint='embed')
@app.route('/<int:courseid>/<int:id>/embed', endpoint='embed')
273
@handle_errors('course', 'Diese Vorlesung existiert nicht!', 404, IndexError)
274
def lecture(id, course=None, courseid=None):
Andreas Valder's avatar
Andreas Valder committed
275
276
	lecture = query('SELECT * FROM lectures WHERE id = ? AND (? OR visible)', id, ismod())[0]
	videos = query('''
277
			SELECT videos.*, (videos.downloadable AND courses.downloadable) as downloadable, "formats" AS sep, formats.*
Andreas Valder's avatar
Andreas Valder committed
278
279
			FROM videos
			JOIN formats ON (videos.video_format = formats.id)
280
281
282
283
			JOIN courses ON (courses.id = ?)
			WHERE videos.lecture_id = ? AND (? OR videos.visible)
			ORDER BY formats.prio DESC
			''', lecture['course_id'], lecture['id'], ismod())
284
	livestreams = query('''SELECT streams.handle AS livehandle, streams.lecture_id, "formats" AS sep, formats.*
285
286
287
288
289
290
			FROM streams
			JOIN lectures ON lectures.id = streams.lecture_id
			JOIN formats ON formats.keywords = "hls"
			WHERE streams.active AND (? OR streams.visible) AND lectures.id = ?
			''', ismod(), id)
	videos += genlive(livestreams)
291
	perms = query('SELECT perm.* FROM perm WHERE ((NOT perm.deleted) AND (perm.lecture_id = ? OR perm.course_id = ?))',
292
			lecture['id'], lecture['course_id'])
293
	if not videos:
294
295
296
297
298
299
		if lecture['live'] and lecture['time'] > datetime.now()-timedelta(minutes=30) and lecture['time']-timedelta(hours=20) < datetime.now():
			flash('Der Livestream beginnt um '+human_time(lecture['time'])+' Uhr.')
		elif lecture['time'] > datetime.now():
			flash('Diese Vorlesung hat noch nicht stattgefunden!')
		else:
			flash('Zu dieser Vorlesung wurden noch keine Videos veröffentlicht!')
300
	courses = query('SELECT * FROM courses WHERE id = ? AND (? OR visible)', lecture['course_id'], ismod())
301
302
	if not courses:
		return render_endpoint('courses', 'Diese Veranstaltung existiert nicht!'), 404
303
	chapters = query('SELECT * FROM chapters WHERE lecture_id = ? AND NOT deleted AND (? OR visible) ORDER BY time ASC', id, ismod())
304
305
306
307
308
	username = password = None
	if request.authorization:
		username = request.authorization.username
		password = request.authorization.password
	if not checkperm(perms, username=username, password=password):
309
		mode, text = permdescr(perms)
310
		if mode == 'rwth':
311
			flash(text+'. <a target="_blank" class="reloadonclose" href="'+url_for('start_rwthauth')+'">Hier authorisieren</a>.', category='player')
312
		elif mode == 'l2p':
313
			if 'l2p_courses' in session:
314
				flash(text+'. Du bist kein Teilnehmer des L2P-Kurses! <a target="_blank" class="reloadonclose" href="'+url_for('start_l2pauth')+'">Kurse aktualisieren</a>.', category='player')
315
			else:
316
				flash(text+'. <a target="_blank" class="reloadonclose" href="'+url_for('start_l2pauth')+'">Hier authorisieren</a>.', category='player')
317
		else:
318
			flash(text+'.', category='player')
319
	return render_template('embed.html' if request.endpoint == 'embed' else 'lecture.html', course=courses[0], lecture=lecture, videos=videos, chapters=chapters, seek=request.args.get('t'))
Andreas Valder's avatar
Andreas Valder committed
320

321
322
323
324
325
326
327

@app.route('/search')
def search():
	if 'q' not in request.args:
		return redirect(url_for('index'))
	q = request.args['q']
	courses = searchquery(q, '*', ['title', 'short', 'organizer', 'subject', 'description'],
328
			'courses', 'WHERE (? OR (visible AND listed)) GROUP BY id ORDER BY _score DESC, semester DESC LIMIT 20', ismod())
Julian Rother's avatar
Julian Rother committed
329
330
331
332
333
	#lectures = searchquery(q, 'lectures.*, courses.visible AS coursevisible, courses.listed, "course" AS sep, courses.*',
	#			['lectures.title', 'lectures.comment', 'lectures.speaker', 'courses.short'],
	#			'lectures LEFT JOIN courses on (courses.id = lectures.course_id)',
	#			'WHERE (? OR (coursevisible AND listed AND visible)) GROUP BY id ORDER BY _score DESC, time DESC LIMIT 30', ismod())
	lectures = searchquery(q, 'lectures.*, courses.visible AS coursevisible, courses.listed, courses.id AS courses_id, courses.visible AS courses_visible, courses.listed AS courses_listed, courses.title AS courses_title, courses.short AS courses_short, courses.handle AS courses_handle, courses.organizer AS courses_organizer, courses.subject AS courses_subject, courses.credits AS courses_credits, courses.created_by AS courses_created_by, courses.time_created AS courses_time_created, courses.time_updated AS courses_time_updated, courses.semester AS courses_semester, courses.downloadable AS courses_downloadable, courses.embedinvisible AS courses_embedinvisible, courses.description AS courses_description, courses.internal AS courses_internal, courses.responsible AS courses_responsible',
334
335
			['lectures.title', 'lectures.comment', 'lectures.speaker', 'courses.short'],
			'lectures LEFT JOIN courses on (courses.id = lectures.course_id)',
336
			'WHERE (? OR (coursevisible AND listed AND visible)) GROUP BY id ORDER BY _score DESC, time DESC LIMIT 30', ismod())
Julian Rother's avatar
Julian Rother committed
337
338
339
340
341
	for lecture in lectures:
		lecture['course'] = {}
		for key in lecture:
			if key.startswith('courses_'):
				lecture['course'][key[8:]] = lecture[key]
342
	return render_template('search.html', searchtext=request.args['q'], courses=courses, lectures=lectures)
Andreas Valder's avatar
Andreas Valder committed
343

344
def check_mod(user, groups):
345
346
347
348
349
350
	if not user:
		return False
	for group in config['LDAP_GROUPS']:
		if group in groups:
			return True
	return False
351

352
@app.route('/internal/login', methods=['GET', 'POST'])
Julian Rother's avatar
Julian Rother committed
353
def login():
354
355
	if request.method == 'GET':
		return render_template('login.html')
356
357
	userinfo, groups = ldapauth(request.form.get('user'), request.form.get('password'))
	user = userinfo.get('uid')
358
	if not check_mod(user, groups):
359
		flash('Login fehlgeschlagen!')
Andreas Valder's avatar
Andreas Valder committed
360
		return make_response(render_template('login.html'), 403)
361
	session['user'] = userinfo
362
363
	dbuser = query('SELECT * FROM users WHERE name = ?', user)
	if not dbuser:
Julian Rother's avatar
Julian Rother committed
364
		modify('INSERT INTO users (name, realname, fsacc, level, calendar_key, rfc6238) VALUES (?, ?, ?, 1, "", "")', user, session['user']['givenName'], user)
365
366
		dbuser = query('SELECT * FROM users WHERE name = ?', user)
	session['user']['dbid'] = dbuser[0]['id']
367
	session['_csrf_token'] = ''.join(random.SystemRandom().choice(string.ascii_letters + string.digits) for _ in range(64))
Andreas Valder's avatar
Andreas Valder committed
368
	session.permanent = True
Julian Rother's avatar
Julian Rother committed
369
	return redirect(request.values.get('ref', url_for('index')))
Julian Rother's avatar
Julian Rother committed
370

371
@app.route('/internal/logout', methods=['GET', 'POST'])
Julian Rother's avatar
Julian Rother committed
372
def logout():
373
	session.pop('user', None)
Julian Rother's avatar
Julian Rother committed
374
	return redirect(request.values.get('ref', url_for('index')))
Julian Rother's avatar
Julian Rother committed
375

376
@app.route('/internal/auth')
377
378
379
380
def auth(): # For use with nginx auth_request
	if 'X-Original-Uri' not in request.headers:
		return 'Internal Server Error', 500
	url = request.headers['X-Original-Uri'].lstrip(config['VIDEOPREFIX'])
Julian Rother's avatar
Julian Rother committed
381
382
	if request.cookies.get('tracking', '') and request.cookies['tracking'].isdigit():
		cookie = int(request.cookies['tracking'])
383
	else:
Julian Rother's avatar
Julian Rother committed
384
		cookie = random.getrandbits(8*8-1)
385
	if url.endswith('jpg') or ismod():
386
		return "OK", 200
387
388
	if url.startswith('pub/hls/'):
		handle = url[len('pub/hls/'):].split('_')[0].split('.')[0]
389
		perms = query('''SELECT lectures.id AS lecture, perm.*
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
				FROM streams
				JOIN lectures ON (streams.lecture_id = lectures.id)
				JOIN courses ON (lectures.course_id = courses.id)
				LEFT JOIN perm ON ((lectures.id = perm.lecture_id OR courses.id = perm.course_id) AND NOT perm.deleted)
				WHERE streams.handle = ?
				AND (courses.visible AND lectures.visible AND streams.visible)
				ORDER BY perm.video_id DESC, perm.lecture_id DESC, perm.course_id DESC''', handle)
	else:
		perms = query('''SELECT videos.path, videos.id AS vid, perm.*
				FROM videos
				JOIN lectures ON (videos.lecture_id = lectures.id)
				JOIN courses ON (lectures.course_id = courses.id)
				LEFT JOIN perm ON ((videos.id = perm.video_id OR lectures.id = perm.lecture_id OR courses.id = perm.course_id) AND NOT perm.deleted)
				WHERE videos.path = ?
				AND (courses.visible AND lectures.visible AND videos.visible)
				ORDER BY perm.video_id DESC, perm.lecture_id DESC, perm.course_id DESC''',
				url)
407
	if not perms:
408
		return "Not found", 404
409
	auth = request.authorization
410
411
412
413
	username = password = None
	if auth:
		username = auth.username
		password = auth.password
414
	if checkperm(perms, username=username, password=password):
415
		try:
416
417
			if not url.startswith('pub/hls/'):
				modify('INSERT INTO log (id, `time`, `date`, video, source) VALUES (?, ?, ?, ?, 1)', cookie, datetime.now(), datetime.combine(date.today(), time()), perms[0]['vid'])
418
419
420
421
			elif url.endswith('.ts'):
				fmt = url.split('_')[-1].split('-')[0]
				seg = url.split('.')[0].split('-')[-1]
				modify('INSERT INTO hlslog (id, `time`, segment, lecture, handle, format) VALUES (?, ?, ?, ?, ?, ?)', cookie, datetime.now(), seg, perms[0]['lecture'], handle, fmt)
422
423
424
		except:
			pass
		r = make_response('OK', 200)
Julian Rother's avatar
Julian Rother committed
425
		r.set_cookie('tracking', str(cookie), max_age=2147483647) # Many many years
426
		return r
427
	password_auth = False
428
429
	for perm in perms:
		if perm['type'] == 'password':
430
431
432
			password_auth = True
			break
	if password_auth:
433
434
		return Response("Login required", 401, {'WWW-Authenticate': 'Basic realm="Login Required"'})
	return "Not allowed", 403
Andreas Valder's avatar
Andreas Valder committed
435

Julian Rother's avatar
Julian Rother committed
436
437
438
439
@app.route('/files/<filename>')
def files(filename):
	return redirect(config['VIDEOPREFIX']+'/'+filename)

Andreas Valder's avatar
Andreas Valder committed
440
@app.route('/sitemap.xml')
Andreas Valder's avatar
Andreas Valder committed
441
442
443
444
445
446
447
448
449
450
def sitemap():
	pages=[]
	# static pages
	for rule in app.url_map.iter_rules():
		if 'GET' in rule.methods and len(rule.arguments)==0:
			if rule.endpoint not in mod_endpoints:
				pages.append([rule.rule])
	for i in query('select * from courses where visible and listed'):
		pages.append([url_for('course',handle=i['handle'])])
		for j in query('select * from lectures where (course_id = ? and visible)',i['id']):
451
			pages.append([url_for('lecture',course=i['handle'],id=j['id'])])
Andreas Valder's avatar
Andreas Valder committed
452
453
454
455


	return Response(render_template('sitemap.xml', pages=pages), 200, {'Content-Type': 'application/atom+xml'} )

Julian Rother's avatar
Julian Rother committed
456
457
458
459
460
461
462
463
464
@app.route('/internal/dbstatus')
@register_navbar('DB-Status', icon='ok')
@mod_required
def dbstatus():
	hosts = set()
	clusters = {}
	status = {}
	variables = {}
	for host in config.get('MYSQL_DBSTATUS_HOSTS', [])+[config.get('MYSQL_HOST', None)]:
Andreas Valder's avatar
Andreas Valder committed
465
466
467
468
469
		try:
			for _host in show('SHOW VARIABLES LIKE "wsrep_cluster_address"', host=host)['wsrep_cluster_address'][len('gcomm://'):].split(','):
				hosts.add(_host)
		except:
			pass
Julian Rother's avatar
Julian Rother committed
470
	for host in sorted(list(hosts)):
Julian Rother's avatar
Julian Rother committed
471
472
473
474
475
476
477
478
479
480
481
482
		try:
			status[host] = show('SHOW GLOBAL STATUS LIKE "wsrep%"', host=host)
			variables[host] = show('SHOW GLOBAL VARIABLES LIKE "wsrep%"', host=host)
		except:
			status[host] = {'wsrep_cluster_state_uuid': '', 'wsrep_local_state_comment': 'Not reachable', 'wsrep_cluster_conf_id': '0', 'wsrep_cluster_status': 'Unknown'}
			variables[host] = {'wsrep_node_name': host, 'wsrep_cluster_name': 'unknown'}
		cluster = variables[host]['wsrep_cluster_name']+'-'+status[host]['wsrep_cluster_conf_id']
		if cluster not in clusters:
			clusters[cluster] = []
		clusters[cluster].append(host)
	return render_template('dbstatus.html', clusters=clusters, statuses=status, vars=variables), 200

483
484
485
486
487
488
489

job_handlers = {}
def job_handler(*types, state='finished'):
	def wrapper(func):
		for jobtype in types:
			if jobtype not in job_handlers:
				job_handlers[jobtype] = {}
Julian Rother's avatar
Julian Rother committed
490
			if state not in job_handlers[jobtype]:
491
492
493
494
495
				job_handlers[jobtype][state] = []
			job_handlers[jobtype][state].append(func)
			return func
	return wrapper

496
497
498
499

def date_json_handler(obj):
	return obj.isoformat() if hasattr(obj, 'isoformat') else obj

500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
def schedule_job(jobtype, data=None, priority=0):
	if not data:
		data = {}
	modify('INSERT INTO jobs (type, priority, data, time_created) VALUES (?, ?, ?, ?)',
			jobtype, priority, json.dumps(data, default=date_json_handler), datetime.now())

edit_handlers = {}
def edit_handler(*tables, field=None):
	def wrapper(func):
		for table in tables:
			if table not in edit_handlers:
				edit_handlers[table] = {}
			if field not in edit_handlers[table]:
				edit_handlers[table][field] = []
			edit_handlers[table][field].append(func)
		return func
	return wrapper

Andreas Valder's avatar
Andreas Valder committed
518
import edit
Julian Rother's avatar
Julian Rother committed
519
import feeds
520
import importer
521
import stats
522
523
if 'ICAL_URL' in config:
	import meetings
524
import l2pauth
525
import jobs
Julian Rother's avatar
Julian Rother committed
526
import sorter
527
import encoding
Andreas Valder's avatar
Andreas Valder committed
528
import timetable
Andreas Valder's avatar
Andreas Valder committed
529
import chapters
530
import icalexport
531
import livestreams