server.py 20.9 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
39
	config['JOBS_API_KEY'] = '1'
Andreas Valder's avatar
Andreas Valder committed
40
41
if config['DEBUG']:
	app.jinja_env.auto_reload = True
Andreas Valder's avatar
Andreas Valder committed
42

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

46
mod_endpoints = []
Julian Rother's avatar
Julian Rother committed
47

48
def mod_required(func):
49
	mod_endpoints.append(func.__name__)
50
51
	@wraps(func)
	def decorator(*args, **kwargs):
52
		if not ismod():
53
54
55
56
57
58
			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

59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
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'}]
77

78
app.jinja_env.globals['navbar'] = []
79
80
81
82
# iconlib can be 'bootstrap'
# ( see: http://getbootstrap.com/components/#glyphicons )
# or 'fa'
# ( see: http://fontawesome.io/icons/ )
Andreas Valder's avatar
Andreas Valder committed
83
def register_navbar(name, iconlib='bootstrap', icon=None, userendpoint=False):
84
	def wrapper(func):
85
		endpoint = func.__name__
Andreas Valder's avatar
Andreas Valder committed
86
		app.jinja_env.globals['navbar'].append((endpoint, name, iconlib, icon, not endpoint in mod_endpoints, userendpoint))
87
88
89
		return func
	return wrapper

Julian Rother's avatar
Julian Rother committed
90
91
92
93
94
95
from db import query, modify, show, searchquery
from mail import notify_mods, notify_admins
from ldap import ldapauth
from legacy import legacy_index
from scheduler import sched_func

96
97
from template_helper import *

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

105
106
107
108
109
110
111
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
112
				if endpoint:
113
					return make_response(render_endpoint(endpoint, text, **epargs), code)
Julian Rother's avatar
Julian Rother committed
114
115
				else:
					return text, code
116
117
118
		return decorator
	return wrapper

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

124
125
126
127
@app.errorhandler(500)
@app.errorhandler(Exception)
def handle_internal_error(e):
	traceback.print_exc()
128
	notify_admins('endpoint_exception', traceback=traceback.format_exc())
129
	return render_template('500.html', online=True), 500
130

131
132
133
134
135
136
137
138
139
140
@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()

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


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

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

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

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

Andreas Valder's avatar
Andreas Valder committed
272
@app.route('/faq')
273
@register_navbar('FAQ', icon='question-sign')
Andreas Valder's avatar
Andreas Valder committed
274
def faq():
275
	return render_template('faq.html')
Andreas Valder's avatar
Andreas Valder committed
276

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

329
330
331
332
333
334
335

@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'],
336
			'courses', 'WHERE (? OR (visible AND listed)) GROUP BY id ORDER BY _score DESC, semester DESC LIMIT 20', ismod())
337
	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',
338
339
			['lectures.title', 'lectures.comment', 'lectures.speaker', 'courses.short'],
			'lectures LEFT JOIN courses on (courses.id = lectures.course_id)',
340
			'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
341
342
343
344
345
	for lecture in lectures:
		lecture['course'] = {}
		for key in lecture:
			if key.startswith('courses_'):
				lecture['course'][key[8:]] = lecture[key]
346
	return render_template('search.html', searchtext=request.args['q'], courses=courses, lectures=lectures)
Andreas Valder's avatar
Andreas Valder committed
347

348
def check_mod(user, groups):
349
350
351
352
353
354
	if not user:
		return False
	for group in config['LDAP_GROUPS']:
		if group in groups:
			return True
	return False
355

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

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

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

Julian Rother's avatar
Julian Rother committed
440
441
442
443
@app.route('/files/<filename>')
def files(filename):
	return redirect(config['VIDEOPREFIX']+'/'+filename)

Andreas Valder's avatar
Andreas Valder committed
444
@app.route('/sitemap.xml')
Andreas Valder's avatar
Andreas Valder committed
445
446
447
448
449
450
451
452
453
454
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']):
455
			pages.append([url_for('lecture',course=i['handle'],id=j['id'])])
Andreas Valder's avatar
Andreas Valder committed
456
457
458
459


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

Julian Rother's avatar
Julian Rother committed
460
461
462
463
464
465
466
467
468
@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
469
470
471
472
473
		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
474
	for host in sorted(list(hosts)):
Julian Rother's avatar
Julian Rother committed
475
476
477
478
479
480
481
482
483
484
485
486
		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

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

Julian Rother's avatar
Julian Rother committed
490
from jobs import job_handler, schedule_job, cancel_job, restart_job
491
from edit import edit_handler
Julian Rother's avatar
Julian Rother committed
492
import feeds
493
import importer
494
import stats
495
496
if 'ICAL_URL' in config:
	import meetings
497
import l2pauth
Julian Rother's avatar
Julian Rother committed
498
import sorter
499
import encoding
Andreas Valder's avatar
Andreas Valder committed
500
import timetable
Andreas Valder's avatar
Andreas Valder committed
501
import chapters
502
import icalexport
503
import livestreams
504
import cutprogress