1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
|
# IVLE - Informatics Virtual Learning Environment
# Copyright (C) 2009 The University of Melbourne
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
# Author: William Grant
'''Media file support for the framework.'''
import os
import time
import inspect
import mimetypes
import email.utils
from ivle.webapp.base.views import BaseView
from ivle.webapp.base.plugins import PublicViewPlugin, ViewPlugin, MediaPlugin
from ivle.webapp.errors import NotFound, Forbidden
from ivle.webapp.publisher import INF
from ivle.webapp import ApplicationRoot
# This maps a media namespace to an external dependency directory (in this
# case specified by the configuration option media/externals/jquery) and a
# list of permitted subpaths.
EXTERNAL_MEDIA_MAP = {'jquery': ('jquery', ['jquery.js'])}
def media_url(req, plugin, path):
'''Generates a URL to a media file.
Plugin can be a string, in which case it is put into the path literally,
or a plugin object, in which case its name is looked up.
If a version is specified in the IVLE configuration, a versioned URL will
be generated.
'''
if not isinstance(plugin, basestring):
plugin = req.config.reverse_plugins[plugin]
media_path = os.path.join('+media', '+' + req.config['media']['version']) \
if req.config['media']['version'] else '+media'
return req.make_path(os.path.join(media_path, plugin, path))
class MediaFile(object):
def __init__(self, root, external, version, ns, path):
self.root = root
self.external = external
self.version = version
self.ns = ns
self.path = path
@property
def filename(self):
if self.external:
try:
extern = EXTERNAL_MEDIA_MAP[self.ns]
except KeyError:
return None
# Unless it's a whitelisted path, we don't want to hear about it.
# If the whitelist is None (not []), we allow all.
if extern[1] is not None and self.path not in extern[1]:
return None
# Grab the admin-configured path for this particular external dep.
externdir = self.root.config['media']['externals'][extern[0]]
assert isinstance(externdir, basestring)
return os.path.join(externdir, self.path)
else:
try:
plugin = self.root.config.plugins[self.ns]
except KeyError:
return None
if not issubclass(plugin, MediaPlugin):
return None
mediadir = plugin.media
plugindir = os.path.dirname(inspect.getmodule(plugin).__file__)
return os.path.join(plugindir, mediadir, self.path)
class MediaFileView(BaseView):
def authorize(self, req):
return True
def render(self, req):
# If it begins with ".." or separator, it's illegal. Die.
if self.context.path.startswith("..") or \
self.context.path.startswith('/'):
raise Forbidden()
filename = self.get_filename(req)
if filename is None:
raise NotFound()
# Find an appropriate MIME type.
(type, _) = mimetypes.guess_type(filename)
if type is None:
type = 'application/octet-stream'
# Get out if it is unreadable or a directory.
if not os.access(filename, os.F_OK):
raise NotFound()
if not os.access(filename, os.R_OK) or os.path.isdir(filename):
raise Forbidden()
if self.context.version is not None:
req.headers_out['Expires'] = email.utils.formatdate(
timeval=time.time() + (60*60*24*365),
localtime=False,
usegmt=True)
req.content_type = type
req.sendfile(filename)
def get_filename(self, req):
return self.context.filename
def root_to_media(root, *segments):
if segments[0].startswith('+'):
if segments[0] == '+external':
external = True
version = None
path = segments[1:]
else:
version = segments[0][1:]
if segments[1] == '+external':
external = True
path = segments[2:]
else:
external = False
path = segments[1:]
else:
external = False
version = None
path = segments
if version is not None and version != root.config['media']['version']:
return None
ns = path[0]
path = os.path.normpath(os.path.join(*path[1:]))
return MediaFile(root, external, version, ns, path)
class Plugin(ViewPlugin, PublicViewPlugin):
forward_routes = [(ApplicationRoot, '+media', root_to_media, INF)]
views = [(MediaFile, '+index', MediaFileView)]
public_forward_routes = forward_routes
public_views = views
|