Escape HTML in session/contribution titles
[cds-indico.git] / indico / modules / attachments / controllers / event_package.py
blob9754702cd681f6b5adbe6a55d89fb944fd2fe610
1 # This file is part of Indico.
2 # Copyright (C) 2002 - 2015 European Organization for Nuclear Research (CERN).
4 # Indico is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License as
6 # published by the Free Software Foundation; either version 3 of the
7 # License, or (at your option) any later version.
9 # Indico is distributed in the hope that it will be useful, but
10 # WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 # General Public License for more details.
14 # You should have received a copy of the GNU General Public License
15 # along with Indico; if not, see <http://www.gnu.org/licenses/>.
17 from __future__ import unicode_literals
19 import os
20 from collections import OrderedDict
21 from datetime import timedelta
22 from tempfile import NamedTemporaryFile
23 from zipfile import ZipFile
25 from flask import session, flash
26 from markupsafe import escape
27 from sqlalchemy import cast, Date
29 from indico.core.config import Config
30 from indico.core.db.sqlalchemy.links import LinkType
31 from indico.util.date_time import format_date
32 from indico.util.i18n import _
33 from indico.util.fs import secure_filename
34 from indico.util.string import to_unicode, natural_sort_key
35 from indico.util.tasks import delete_file
36 from indico.web.flask.util import send_file
37 from indico.web.forms.base import FormDefaults
38 from indico.modules.attachments.forms import AttachmentPackageForm
39 from indico.modules.attachments.models.attachments import Attachment, AttachmentFile, AttachmentType
40 from indico.modules.attachments.models.folders import AttachmentFolder
41 from MaKaC.conference import SubContribution
44 def _get_start_dt(obj):
45 if isinstance(obj, SubContribution):
46 return obj.getContribution().getAdjustedStartDate()
47 else:
48 return obj.getAdjustedStartDate()
51 class AttachmentPackageGeneratorMixin:
53 def _filter_attachments(self, filter_data):
54 added_since = filter_data.get('added_since', None)
55 filter_type = filter_data['filter_type']
56 attachments = []
58 if filter_type == 'all':
59 attachments = self._get_all_attachments(added_since)
60 elif filter_type == 'sessions':
61 attachments = self._filter_by_sessions(filter_data.get('sessions', []), added_since)
62 elif filter_type == 'contributions':
63 attachments = self._filter_by_contributions(filter_data.get('contributions', []), added_since)
64 elif filter_type == 'dates':
65 attachments = self._filter_by_dates(filter_data.get('dates', []))
67 return self._filter_protected(attachments)
69 def _filter_protected(self, attachments):
70 return [attachment for attachment in attachments if attachment.can_access(session.user)]
72 def _get_all_attachments(self, added_since):
73 query = self._build_base_query(added_since)
75 def _check_scheduled(attachment):
76 obj = attachment.folder.linked_object
77 return obj is not None and _get_start_dt(obj) is not None
79 return filter(_check_scheduled, query)
81 def _build_base_query(self, added_since=None):
82 query = Attachment.find(Attachment.type == AttachmentType.file, ~AttachmentFolder.is_deleted,
83 ~Attachment.is_deleted, AttachmentFolder.event_id == int(self._conf.getId()),
84 _join=AttachmentFolder)
85 if added_since is not None:
86 query = query.join(Attachment.file).filter(cast(AttachmentFile.created_dt, Date) >= added_since)
87 return query
89 def _filter_by_sessions(self, session_ids, added_since):
90 session_ids = set(session_ids)
91 query = self._build_base_query(added_since).filter(AttachmentFolder.link_type.in_([LinkType.session,
92 LinkType.contribution,
93 LinkType.subcontribution]))
95 def _check_session(attachment):
96 obj = attachment.folder.linked_object
97 return obj is not None and obj.getSession() and obj.getSession().getId() in session_ids
99 return filter(_check_session, query)
101 def _filter_by_contributions(self, contribution_ids, added_since):
102 query = self._build_base_query(added_since).filter(AttachmentFolder.contribution_id.in_(contribution_ids),
103 AttachmentFolder.link_type.in_([LinkType.contribution,
104 LinkType.subcontribution]))
106 def _check_scheduled(attachment):
107 obj = attachment.folder.linked_object
108 return obj is not None and _get_start_dt(obj) is not None
110 return filter(_check_scheduled, query)
112 def _filter_by_dates(self, dates):
113 dates = set(dates)
115 def _check_date(attachment):
116 obj = attachment.folder.linked_object
117 if obj is None:
118 return False
119 start_dt = _get_start_dt(obj)
120 if start_dt is None:
121 return None
122 return unicode(start_dt.date()) in dates
124 return filter(_check_date, self._build_base_query())
126 def _generate_zip_file(self, attachments):
127 temp_file = NamedTemporaryFile(suffix='indico.tmp', dir=Config.getInstance().getTempDir())
128 with ZipFile(temp_file.name, 'w', allowZip64=True) as zip_handler:
129 self.used = set()
130 for attachment in attachments:
131 name = self._prepare_folder_structure(attachment)
132 self.used.add(name)
133 with attachment.file.storage.get_local_path(attachment.file.storage_file_id) as filepath:
134 zip_handler.write(filepath, name)
136 # Delete the temporary file after some time. Even for a large file we don't
137 # need a higher delay since the webserver will keep it open anyway until it's
138 # done sending it to the client.
139 delete_file.apply_async(args=[temp_file.name], countdown=3600)
140 temp_file.delete = False
141 return send_file('material-{}.zip'.format(self._conf.id), temp_file.name, 'application/zip', inline=False)
143 def _prepare_folder_structure(self, attachment):
144 event_dir = secure_filename(self._conf.getTitle(), None)
145 segments = [event_dir] if event_dir else []
146 segments.extend(self._get_base_path(attachment))
147 if not attachment.folder.is_default:
148 segments.append(secure_filename(attachment.folder.title, unicode(attachment.folder.id)))
149 segments.append(attachment.file.filename)
150 path = os.path.join(*filter(None, segments))
151 while path in self.used:
152 # prepend the id if there's a path collision
153 segments[-1] = '{}-{}'.format(attachment.id, segments[-1])
154 path = os.path.join(*filter(None, segments))
155 return path
157 def _get_base_path(self, attachment):
158 obj = linked_object = attachment.folder.linked_object
159 paths = []
160 while obj != self._conf:
161 owner = obj.getOwner()
162 if isinstance(obj, SubContribution):
163 start_date = owner.getAdjustedStartDate()
164 else:
165 start_date = obj.getAdjustedStartDate()
167 if start_date is not None:
168 paths.append(secure_filename(start_date.strftime('%H%M_{}').format(obj.getTitle()), ''))
169 else:
170 paths.append(secure_filename(obj.getTitle(), unicode(obj.getId())))
171 obj = owner
173 if isinstance(linked_object, SubContribution):
174 linked_obj_start_date = linked_object.getOwner().getAdjustedStartDate()
175 else:
176 linked_obj_start_date = linked_object.getAdjustedStartDate()
178 if attachment.folder.linked_object != self._conf and linked_obj_start_date is not None:
179 paths.append(secure_filename(linked_obj_start_date.strftime('%Y%m%d_%A'), ''))
181 return reversed(paths)
184 class AttachmentPackageMixin(AttachmentPackageGeneratorMixin):
185 wp = None
187 def _process(self):
188 form = self._prepare_form()
189 if form.validate_on_submit():
190 attachments = self._filter_attachments(form.data)
191 if attachments:
192 return self._generate_zip_file(attachments)
193 else:
194 flash(_('There are no materials matching your criteria.'), 'warning')
196 return self.wp.render_template('generate_package.html', self._conf, form=form)
198 def _prepare_form(self):
199 form = AttachmentPackageForm(obj=FormDefaults(filter_type='all'))
200 form.dates.choices = list(self._iter_event_days())
201 filter_types = OrderedDict()
202 filter_types['all'] = _('Everything')
203 filter_types['sessions'] = _('Specific sessions')
204 filter_types['contributions'] = _('Specific contributions')
205 filter_types['dates'] = _('Specific days')
207 form.sessions.choices = self._load_session_data()
208 if not form.sessions.choices:
209 del filter_types['sessions']
210 del form.sessions
212 form.contributions.choices = self._load_contribution_data()
213 if not form.contributions.choices:
214 del filter_types['contributions']
215 del form.contributions
217 form.filter_type.choices = filter_types.items()
218 return form
220 def _load_session_data(self):
221 return [(session.getId(), escape(to_unicode(session.getTitle()))) for session in self._conf.getSessionList()]
223 def _load_contribution_data(self):
224 def _format_contrib(contrib):
225 if contrib.getSession() is None:
226 return to_unicode(contrib.getTitle())
227 else:
228 return _('{contrib} (in session "{session}")').format(
229 session=to_unicode(contrib.getSession().getTitle()),
230 contrib=to_unicode(contrib.getTitle())
233 contribs = sorted([contrib for contrib in self._conf.getContributionList() if contrib.getStartDate()],
234 key=lambda c: natural_sort_key(c.getTitle()))
235 return [(contrib.getId(), escape(_format_contrib(contrib))) for contrib in contribs]
237 def _iter_event_days(self):
238 duration = (self._conf.getAdjustedEndDate() - self._conf.getAdjustedStartDate()).days
239 for offset in xrange(duration + 1):
240 day = (self._conf.getAdjustedStartDate() + timedelta(days=offset)).date()
241 yield day.isoformat(), format_date(day, 'short')