server.py 20.6 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 threading
6
import os
7
import sys
Julian Rother's avatar
Julian Rother committed
8
import hashlib
9
import random
10
import sched
11
import traceback
12
import string
13
from socket import gethostname
Julian Rother's avatar
Julian Rother committed
14
from ipaddress import ip_address, ip_network
15
import math
Julian Rother's avatar
Julian Rother committed
16
import locale
17
import base64
18
import json
Julian Rother's avatar
Julian Rother committed
19
20

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

22
app = Flask(__name__)
23

Andreas Valder's avatar
Andreas Valder committed
24
25
app.jinja_env.trim_blocks = True
app.jinja_env.lstrip_blocks = True
Julian Rother's avatar
Julian Rother committed
26
app.add_template_global(random.randint, name='randint')
27
28
app.add_template_global(datetime, name='datetime')
app.add_template_global(timedelta, name='timedelta')
29
app.add_template_global(gethostname, name='gethostname')
30
31
app.add_template_global(min, name='min')
app.add_template_global(max, name='max')
Andreas Valder's avatar
Andreas Valder committed
32

33

34
35
scheduler = sched.scheduler()
def run_scheduler():
Andreas Valder's avatar
Andreas Valder committed
36
	import time
37
	time.sleep(1) # UWSGI does weird things on startup
38
39
	while True:
		scheduler.run()
40
		time.sleep(10)
41

42
43
44
def sched_func(delay, priority=0, firstdelay=None, args=[], kargs={}):
	if firstdelay == None:
		firstdelay = random.randint(1, 120)
45
46
47
	def wrapper(func):
		def sched_wrapper():
			with app.test_request_context():
48
49
50
51
				try:
					func(*args, **kargs)
				except Exception:
					traceback.print_exc()
52
			scheduler.enter(delay, priority, sched_wrapper)
53
		scheduler.enter(firstdelay, priority, sched_wrapper)
54
55
56
57
		return func
	return wrapper

threading.Thread(target=run_scheduler, daemon=True).start()
58

59
config = app.config
60
config.from_pyfile('config.py.example', silent=True)
61
62
63
if sys.argv[0].endswith('run.py'): 
	config['SQLITE_INIT_DATA'] = True
	config['DEBUG'] = True
64
config.from_pyfile('config.py', silent=True)
Andreas Valder's avatar
Andreas Valder committed
65
66
if config['DEBUG']:
	app.jinja_env.auto_reload = True
Andreas Valder's avatar
Andreas Valder committed
67
68
69

# get git commit
import subprocess
Andreas Valder's avatar
Andreas Valder committed
70
output = subprocess.check_output(['git', "log", "-g", "-1", "--pretty=%H # %h # %d # %s"]).decode('UTF-8').split('#', 3)
Andreas Valder's avatar
Andreas Valder committed
71
app.jinja_env.globals['gitversion'] = { 'hash': output[1], 'longhash': output[0], 'branch': output[2], 'msg': output[3]  }
72

73
74
if not config.get('SECRET_KEY', None):
	config['SECRET_KEY'] = os.urandom(24)
Julian Rother's avatar
Julian Rother committed
75

76
77
from db import query, modify, show, searchquery
from ldap import ldapauth
78
from legacy import legacy_index
Julian Rother's avatar
Julian Rother committed
79

80
mod_endpoints = []
Julian Rother's avatar
Julian Rother committed
81

82
def mod_required(func):
83
	mod_endpoints.append(func.__name__)
84
85
	@wraps(func)
	def decorator(*args, **kwargs):
86
		if not ismod():
87
88
89
90
91
92
			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

93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
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'}]
111

112
from template_helper import *
113

114
app.jinja_env.globals['navbar'] = []
115
116
117
118
119
# 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):
120
	def wrapper(func):
121
		endpoint = func.__name__
122
		app.jinja_env.globals['navbar'].append((endpoint, name, iconlib, icon, not endpoint in mod_endpoints))
123
124
125
		return func
	return wrapper

Julian Rother's avatar
Cleanup    
Julian Rother committed
126
127
128
129
def render_endpoint(endpoint, flashtext=None, **kargs):
	if flashtext:
		flash(flashtext)
	# request.endpoint is used for navbar highlighting
130
	request.url_rule = Rule(request.path, endpoint=endpoint)
Julian Rother's avatar
Cleanup    
Julian Rother committed
131
132
	return app.view_functions[endpoint](**kargs)

133
134
135
136
137
138
139
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
140
				if endpoint:
141
					return make_response(render_endpoint(endpoint, text, **epargs), code)
Julian Rother's avatar
Julian Rother committed
142
143
				else:
					return text, code
144
145
146
		return decorator
	return wrapper

Julian Rother's avatar
Cleanup    
Julian Rother committed
147
@app.errorhandler(404)
148
@app.route('/invalidpath')
Julian Rother's avatar
Julian Rother committed
149
def handle_not_found(e=None):
150
	return render_endpoint('index', 'Diese Seite existiert nicht!'), 404
Julian Rother's avatar
Cleanup    
Julian Rother committed
151

152
153
154
155
@app.errorhandler(500)
@app.errorhandler(Exception)
def handle_internal_error(e):
	traceback.print_exc()
156
	return render_template('500.html'), 500
157

158
159
160
161
162
163
164
165
166
167
@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()

168
169
170
171
172
173
174
175
176
def genlive(streams):
        for stream in streams:
                stream['visible'] = True
                stream['downloadable'] = False
                stream['path'] = 'pub/hls/%s.m3u8'%stream['livehandle']
                stream['file_size'] = 0
        return streams


177
@app.route('/')
178
@register_navbar('Home', icon='home')
179
def index():
180
	# handle legacy urls...
181
182
183
	result = legacy_index()
	if result:
		return result
184

185
	start = date.today()
186
	end = start + timedelta(days=7)
187
	upcomming = query('''
188
		SELECT lectures.*, streams.active AS nowlive, "course" AS sep, courses.*
Andreas Valder's avatar
Andreas Valder committed
189
190
		FROM lectures
		JOIN courses ON (lectures.course_id = courses.id)
191
		LEFT JOIN streams ON lectures.id = streams.lecture_id
192
193
		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
194
195
196
	for i in upcomming:
		i['date'] = i['time'].date()
	latestvideos=query('''
197
		SELECT lectures.*, "course" AS sep, courses.*
Andreas Valder's avatar
Andreas Valder committed
198
199
200
201
202
		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
203
		ORDER BY MAX(videos.time_created) DESC
Andreas Valder's avatar
Andreas Valder committed
204
		LIMIT 6	''',ismod())
205
	livestreams = query('''SELECT streams.handle AS livehandle, lectures.*, "course" AS sep, courses.*
206
207
208
209
210
		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
211
	featured = query('SELECT * FROM featured WHERE (? OR visible) ORDER BY `order`', ismod())
212
	featured = list(filter(lambda x: not x['deleted'], featured))
Julian Rother's avatar
Julian Rother committed
213
214
215
216
	for item in featured:
		if item['type'] == 'courses':
			if item['param'] not in ['title', 'semester', 'organizer', 'subject']:
				continue
217
			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
218
219
		elif item['type'] == 'video':
			item['lecture'] = {'id': item['param']}
220
			streams = query('''SELECT streams.handle AS livehandle, streams.lecture_id, "formats" AS sep, formats.*
Julian Rother's avatar
Julian Rother committed
221
222
223
224
225
226
227
228
229
230
231
					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
232
					''', item['param'])+genlive(streams)
233
	return render_template('index.html', latestvideos=livestreams+latestvideos, upcomming=upcomming, featured=featured)
234

235
@app.route('/courses')
236
@register_navbar('Videos', icon='film')
237
def courses():
238
	courses = query('SELECT * FROM courses WHERE (? OR (visible AND listed)) ORDER BY lower(semester), lower(title)', ismod())
239
240
241
	for course in courses:
		if course['semester'] == '':
			course['semester'] = 'zeitlos'
Andreas Valder's avatar
Andreas Valder committed
242
	groupedby = request.args.get('groupedby')
Julian Rother's avatar
Julian Rother committed
243
	if groupedby not in ['title', 'semester', 'organizer', 'subject']:
Andreas Valder's avatar
Andreas Valder committed
244
		groupedby = 'semester'
245
	return render_template('courses.html', courses=courses, groupedby=groupedby)
Andreas Valder's avatar
Andreas Valder committed
246

247
248
@app.route('/<handle>')
@app.route('/<int:id>')
249
@handle_errors('courses', 'Diese Veranstaltung existiert nicht!', 404, IndexError)
250
251
def course(id=None, handle=None):
	if id:
252
		course = query('SELECT * FROM courses WHERE id = ? AND (? OR visible)', id, ismod())[0]
253
	else:
254
		course = query('SELECT * FROM courses WHERE handle = ? AND (? OR visible)', handle, ismod())[0]
255
256
	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'])
257
	lectures = query('SELECT * FROM lectures WHERE course_id = ? AND (? OR visible) ORDER BY time, duration DESC', course['id'], ismod())
258
	for lecture in lectures:
259
		lecture['perm'] = []
260
		lecture['perm'] += course['perm']
261
		lecture['course'] = course
262
263
264
		for perm in perms:
			if perm['lecture_id'] == lecture['id']:
				lecture['perm'].append(perm)
Andreas Valder's avatar
Andreas Valder committed
265
	videos = query('''
266
			SELECT videos.*, (videos.downloadable AND courses.downloadable) as downloadable, "formats" AS sep, formats.*
Andreas Valder's avatar
Andreas Valder committed
267
268
269
270
271
272
			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
273
			''', course['id'], ismod())
274
	livestreams = query('''SELECT streams.handle AS livehandle, streams.lecture_id, "formats" AS sep, formats.*
275
276
277
278
279
280
			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'])
	videos += genlive(livestreams)
281
	return render_template('course.html', course=course, lectures=lectures, videos=videos)
Andreas Valder's avatar
Andreas Valder committed
282

Andreas Valder's avatar
Andreas Valder committed
283
@app.route('/faq')
284
@register_navbar('FAQ', icon='question-sign')
Andreas Valder's avatar
Andreas Valder committed
285
def faq():
286
	return render_template('faq.html')
Andreas Valder's avatar
Andreas Valder committed
287

288
289
290
291
@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')
292
@handle_errors('course', 'Diese Vorlesung existiert nicht!', 404, IndexError)
293
def lecture(id, course=None, courseid=None):
Andreas Valder's avatar
Andreas Valder committed
294
295
	lecture = query('SELECT * FROM lectures WHERE id = ? AND (? OR visible)', id, ismod())[0]
	videos = query('''
296
			SELECT videos.*, (videos.downloadable AND courses.downloadable) as downloadable, "formats" AS sep, formats.*
Andreas Valder's avatar
Andreas Valder committed
297
298
			FROM videos
			JOIN formats ON (videos.video_format = formats.id)
299
300
301
302
			JOIN courses ON (courses.id = ?)
			WHERE videos.lecture_id = ? AND (? OR videos.visible)
			ORDER BY formats.prio DESC
			''', lecture['course_id'], lecture['id'], ismod())
303
	livestreams = query('''SELECT streams.handle AS livehandle, streams.lecture_id, "formats" AS sep, formats.*
304
305
306
307
308
309
			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)
310
	perms = query('SELECT perm.* FROM perm WHERE ((NOT perm.deleted) AND (perm.lecture_id = ? OR perm.course_id = ?))',
311
			lecture['id'], lecture['course_id'])
312
	if not videos:
313
314
315
316
317
318
		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!')
319
	courses = query('SELECT * FROM courses WHERE id = ? AND (? OR visible)', lecture['course_id'], ismod())
320
321
	if not courses:
		return render_endpoint('courses', 'Diese Veranstaltung existiert nicht!'), 404
322
	chapters = query('SELECT * FROM chapters WHERE lecture_id = ? AND NOT deleted AND (? OR visible) ORDER BY time ASC', id, ismod())
323
324
325
326
327
	username = password = None
	if request.authorization:
		username = request.authorization.username
		password = request.authorization.password
	if not checkperm(perms, username=username, password=password):
328
		mode, text = permdescr(perms)
329
		if mode == 'rwth':
330
			flash(text+'. <a target="_blank" class="reloadonclose" href="'+url_for('start_rwthauth')+'">Hier authorisieren</a>.', category='player')
331
		elif mode == 'l2p':
332
			if 'l2p_courses' in session:
333
				flash(text+'. Du bist kein Teilnehmer des L2P-Kurses! <a target="_blank" class="reloadonclose" href="'+url_for('start_l2pauth')+'">Kurse aktualisieren</a>.', category='player')
334
			else:
335
				flash(text+'. <a target="_blank" class="reloadonclose" href="'+url_for('start_l2pauth')+'">Hier authorisieren</a>.', category='player')
336
		else:
337
			flash(text+'.', category='player')
338
	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
339

340
341
342
343
344
345
346

@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'],
347
			'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
348
349
350
351
352
	#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',
353
354
			['lectures.title', 'lectures.comment', 'lectures.speaker', 'courses.short'],
			'lectures LEFT JOIN courses on (courses.id = lectures.course_id)',
355
			'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
356
357
358
359
360
	for lecture in lectures:
		lecture['course'] = {}
		for key in lecture:
			if key.startswith('courses_'):
				lecture['course'][key[8:]] = lecture[key]
361
	return render_template('search.html', searchtext=request.args['q'], courses=courses, lectures=lectures)
Andreas Valder's avatar
Andreas Valder committed
362

363
def check_mod(user, groups):
364
365
366
367
368
369
	if not user:
		return False
	for group in config['LDAP_GROUPS']:
		if group in groups:
			return True
	return False
370

371
@app.route('/internal/login', methods=['GET', 'POST'])
Julian Rother's avatar
Julian Rother committed
372
def login():
373
374
	if request.method == 'GET':
		return render_template('login.html')
375
376
	userinfo, groups = ldapauth(request.form.get('user'), request.form.get('password'))
	user = userinfo.get('uid')
377
	if not check_mod(user, groups):
378
		flash('Login fehlgeschlagen!')
379
		return render_template('login.html')
380
	session['user'] = userinfo
381
382
	dbuser = query('SELECT * FROM users WHERE name = ?', user)
	if not dbuser:
Julian Rother's avatar
Julian Rother committed
383
		modify('INSERT INTO users (name, realname, fsacc, level, calendar_key, rfc6238) VALUES (?, ?, ?, 1, "", "")', user, session['user']['givenName'], user)
384
385
		dbuser = query('SELECT * FROM users WHERE name = ?', user)
	session['user']['dbid'] = dbuser[0]['id']
386
	session['_csrf_token'] = ''.join(random.SystemRandom().choice(string.ascii_letters + string.digits) for _ in range(64))
Andreas Valder's avatar
Andreas Valder committed
387
	session.permanent = True
Julian Rother's avatar
Julian Rother committed
388
	return redirect(request.values.get('ref', url_for('index')))
Julian Rother's avatar
Julian Rother committed
389

390
@app.route('/internal/logout', methods=['GET', 'POST'])
Julian Rother's avatar
Julian Rother committed
391
def logout():
392
	session.pop('user', None)
Julian Rother's avatar
Julian Rother committed
393
	return redirect(request.values.get('ref', url_for('index')))
Julian Rother's avatar
Julian Rother committed
394

395
@app.route('/internal/auth')
396
397
398
399
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
400
401
	if request.cookies.get('tracking', '') and request.cookies['tracking'].isdigit():
		cookie = int(request.cookies['tracking'])
402
	else:
Julian Rother's avatar
Julian Rother committed
403
		cookie = random.getrandbits(8*8-1)
404
	if url.endswith('jpg') or ismod():
405
		return "OK", 200
406
407
	if url.startswith('pub/hls/'):
		handle = url[len('pub/hls/'):].split('_')[0].split('.')[0]
408
		perms = query('''SELECT lectures.id AS lecture, perm.*
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
				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)
426
	if not perms:
427
		return "Not found", 404
428
	auth = request.authorization
429
430
431
432
	username = password = None
	if auth:
		username = auth.username
		password = auth.password
433
	if checkperm(perms, username=username, password=password):
434
		try:
435
436
			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'])
437
438
439
440
			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)
441
442
443
		except:
			pass
		r = make_response('OK', 200)
Julian Rother's avatar
Julian Rother committed
444
		r.set_cookie('tracking', str(cookie), max_age=2147483647) # Many many years
445
		return r
446
	password_auth = False
447
448
	for perm in perms:
		if perm['type'] == 'password':
449
450
451
			password_auth = True
			break
	if password_auth:
452
453
		return Response("Login required", 401, {'WWW-Authenticate': 'Basic realm="Login Required"'})
	return "Not allowed", 403
Andreas Valder's avatar
Andreas Valder committed
454

Julian Rother's avatar
Julian Rother committed
455
456
457
458
@app.route('/files/<filename>')
def files(filename):
	return redirect(config['VIDEOPREFIX']+'/'+filename)

Andreas Valder's avatar
Andreas Valder committed
459
@app.route('/sitemap.xml')
Andreas Valder's avatar
Andreas Valder committed
460
461
462
463
464
465
466
467
468
469
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']):
470
			pages.append([url_for('lecture',course=i['handle'],id=j['id'])])
Andreas Valder's avatar
Andreas Valder committed
471
472
473
474


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

Julian Rother's avatar
Julian Rother committed
475
476
477
478
479
480
481
482
483
@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
484
485
486
487
488
		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
489
	for host in sorted(list(hosts)):
Julian Rother's avatar
Julian Rother committed
490
491
492
493
494
495
496
497
498
499
500
501
		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

Andreas Valder's avatar
Andreas Valder committed
502
import edit
Julian Rother's avatar
Julian Rother committed
503
import feeds
504
import importer
505
import stats
Andreas Valder's avatar
Andreas Valder committed
506
import sorter
507
508
if 'ICAL_URL' in config:
	import meetings
509
import l2pauth
Andreas Valder's avatar
Andreas Valder committed
510
511
if 'JOBS_API_KEY' in config:
	import jobs
Andreas Valder's avatar
Andreas Valder committed
512
import timetable
Andreas Valder's avatar
Andreas Valder committed
513
import chapters
514
import icalexport
515
import livestreams