server.py 22.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 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
Julian Rother's avatar
Julian Rother committed
18
19

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

21
app = Flask(__name__)
22

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

32

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

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

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

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

# get git commit
import subprocess
Andreas Valder's avatar
Andreas Valder committed
69
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
70
app.jinja_env.globals['gitversion'] = { 'hash': output[1], 'longhash': output[0], 'branch': output[2], 'msg': output[3]  }
71

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

75
76
from db import query, modify, show, searchquery
from ldap import ldapauth
Julian Rother's avatar
Julian Rother committed
77

78
mod_endpoints = []
Julian Rother's avatar
Julian Rother committed
79

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

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

110
from template_helper import *
111

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

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

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

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

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

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

166
167
168
169
170
171
172
173
174
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


175
@app.route('/')
176
@register_navbar('Home', icon='home')
177
def index():
178
179
180
	# handle legacy urls...
	if 'course' in request.args:
		return redirect(url_for('course', handle=request.args['course']),code=302)
181
182
	if 'view' in request.args:
		if (request.args['view'] == 'player') and ('lectureid' in request.args) :
183
184
185
186
			courses = query('SELECT courses.handle FROM courses JOIN lectures ON courses.id = lectures.course_id WHERE lectures.id = ?', request.args['lectureid'])
			if not courses:
				return "Not found", 404
			return redirect(url_for('lecture', course=courses[0]['handle'], id=request.args['lectureid']),code=302)
187

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

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

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

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

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

343
344
345
346
347
348
349

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

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

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

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

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

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

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


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

478
479
480
481
482

@app.route('/site/')
@app.route('/site/<string:phpfile>')
def legacy(phpfile=None):
	if phpfile=='embed.php' and ('lecture' in request.args):
483
484
485
486
		courses = query('SELECT courses.handle FROM courses JOIN lectures ON courses.id = lectures.course_id WHERE lectures.id = ?', request.args['lecture'])
		if not courses:
			return render_endpoint('index', 'Diese Seite existiert nicht!'), 404
		return redirect(url_for('embed', course=courses[0]['handle'], id=request.args['lecture']),code=302)
487
488
489
490
491
492
493
494
	if phpfile=='embed.php' and ('vid' in request.args):
		lectures = query('SELECT lecture_id FROM videos WHERE id = ?', request.args['vid'])
		if not lectures:
			return render_endpoint('index', 'Dieses Videos existiert nicht!'), 404
		courses = query('SELECT courses.handle FROM courses JOIN lectures ON courses.id = lectures.course_id WHERE lectures.id = ?', lectures[0]['lecture_id'])
		if not courses:
			return render_endpoint('index', 'Diese Seite existiert nicht!'), 404
		return redirect(url_for('embed', course=courses[0]['handle'], id=lectures[0]['lecture_id']),code=302)
495
496
497
498
499
500
501
502
	if phpfile=='feed.php' and ('all' in request.args):
		return redirect(url_for('feed'),code=302)
	if phpfile=='feed.php' and ('newcourses' in request.args):
		return redirect(url_for('courses_feed'),code=302)
	if phpfile=='feed.php':
		return redirect(url_for('feed', handle=request.args.copy().popitem()[0]),code=302)
	print("Unknown legacy url:",request.url)
	return redirect(url_for('index'),code=302)
Julian Rother's avatar
Julian Rother committed
503
504
505
506
507
508
509
510
511
512
513
514

import json

@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
515
516
517
518
519
		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
520
	for host in sorted(list(hosts)):
Julian Rother's avatar
Julian Rother committed
521
522
523
524
525
526
527
528
529
530
531
532
		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
533
import edit
Julian Rother's avatar
Julian Rother committed
534
import feeds
535
import importer
536
import stats
Andreas Valder's avatar
Andreas Valder committed
537
import sorter
538
539
if 'ICAL_URL' in config:
	import meetings
540
import l2pauth
Andreas Valder's avatar
Andreas Valder committed
541
542
if 'JOBS_API_KEY' in config:
	import jobs
Andreas Valder's avatar
Andreas Valder committed
543
import timetable
Andreas Valder's avatar
Andreas Valder committed
544
import chapters
545
import icalexport
546
import livestreams