views.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512
  1. # GNU MediaGoblin -- federated, autonomous media hosting
  2. # Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
  3. #
  4. # This program is free software: you can redistribute it and/or modify
  5. # it under the terms of the GNU Affero General Public License as published by
  6. # the Free Software Foundation, either version 3 of the License, or
  7. # (at your option) any later version.
  8. #
  9. # This program is distributed in the hope that it will be useful,
  10. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. # GNU Affero General Public License for more details.
  13. #
  14. # You should have received a copy of the GNU Affero General Public License
  15. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  16. import six
  17. from datetime import datetime
  18. from itsdangerous import BadSignature
  19. from pyld import jsonld
  20. from werkzeug.exceptions import Forbidden
  21. from werkzeug.utils import secure_filename
  22. from jsonschema import ValidationError, Draft4Validator
  23. from mediagoblin import messages
  24. from mediagoblin import mg_globals
  25. from mediagoblin.auth import (check_password,
  26. tools as auth_tools)
  27. from mediagoblin.edit import forms
  28. from mediagoblin.edit.lib import may_edit_media
  29. from mediagoblin.decorators import (require_active_login, active_user_from_url,
  30. get_media_entry_by_id, user_may_alter_collection,
  31. get_user_collection, user_has_privilege,
  32. user_not_banned)
  33. from mediagoblin.tools.crypto import get_timed_signer_url
  34. from mediagoblin.tools.metadata import (compact_and_validate, DEFAULT_CHECKER,
  35. DEFAULT_SCHEMA)
  36. from mediagoblin.tools.mail import email_debug_message
  37. from mediagoblin.tools.response import (render_to_response,
  38. redirect, redirect_obj, render_404)
  39. from mediagoblin.tools.translate import pass_to_ugettext as _
  40. from mediagoblin.tools.template import render_template
  41. from mediagoblin.tools.text import (
  42. convert_to_tag_list_of_dicts, media_tags_as_string)
  43. from mediagoblin.tools.url import slugify
  44. from mediagoblin.db.util import check_media_slug_used, check_collection_slug_used
  45. from mediagoblin.db.models import User, Client, AccessToken, Location
  46. import mimetypes
  47. @get_media_entry_by_id
  48. @require_active_login
  49. def edit_media(request, media):
  50. if not may_edit_media(request, media):
  51. raise Forbidden("User may not edit this media")
  52. defaults = dict(
  53. title=media.title,
  54. slug=media.slug,
  55. description=media.description,
  56. tags=media_tags_as_string(media.tags),
  57. license=media.license)
  58. form = forms.EditForm(
  59. request.form,
  60. **defaults)
  61. if request.method == 'POST' and form.validate():
  62. # Make sure there isn't already a MediaEntry with such a slug
  63. # and userid.
  64. slug = slugify(form.slug.data)
  65. slug_used = check_media_slug_used(media.uploader, slug, media.id)
  66. if slug_used:
  67. form.slug.errors.append(
  68. _(u'An entry with that slug already exists for this user.'))
  69. else:
  70. media.title = form.title.data
  71. media.description = form.description.data
  72. media.tags = convert_to_tag_list_of_dicts(
  73. form.tags.data)
  74. media.license = six.text_type(form.license.data) or None
  75. media.slug = slug
  76. media.save()
  77. return redirect_obj(request, media)
  78. if request.user.has_privilege(u'admin') \
  79. and media.uploader != request.user.id \
  80. and request.method != 'POST':
  81. messages.add_message(
  82. request, messages.WARNING,
  83. _("You are editing another user's media. Proceed with caution."))
  84. return render_to_response(
  85. request,
  86. 'mediagoblin/edit/edit.html',
  87. {'media': media,
  88. 'form': form})
  89. # Mimetypes that browsers parse scripts in.
  90. # Content-sniffing isn't taken into consideration.
  91. UNSAFE_MIMETYPES = [
  92. 'text/html',
  93. 'text/svg+xml']
  94. @get_media_entry_by_id
  95. @require_active_login
  96. def edit_attachments(request, media):
  97. if mg_globals.app_config['allow_attachments']:
  98. form = forms.EditAttachmentsForm()
  99. # Add any attachements
  100. if 'attachment_file' in request.files \
  101. and request.files['attachment_file']:
  102. # Security measure to prevent attachments from being served as
  103. # text/html, which will be parsed by web clients and pose an XSS
  104. # threat.
  105. #
  106. # TODO
  107. # This method isn't flawless as some browsers may perform
  108. # content-sniffing.
  109. # This method isn't flawless as we do the mimetype lookup on the
  110. # machine parsing the upload form, and not necessarily the machine
  111. # serving the attachments.
  112. if mimetypes.guess_type(
  113. request.files['attachment_file'].filename)[0] in \
  114. UNSAFE_MIMETYPES:
  115. public_filename = secure_filename('{0}.notsafe'.format(
  116. request.files['attachment_file'].filename))
  117. else:
  118. public_filename = secure_filename(
  119. request.files['attachment_file'].filename)
  120. attachment_public_filepath \
  121. = mg_globals.public_store.get_unique_filepath(
  122. ['media_entries', six.text_type(media.id), 'attachment',
  123. public_filename])
  124. attachment_public_file = mg_globals.public_store.get_file(
  125. attachment_public_filepath, 'wb')
  126. try:
  127. attachment_public_file.write(
  128. request.files['attachment_file'].stream.read())
  129. finally:
  130. request.files['attachment_file'].stream.close()
  131. media.attachment_files.append(dict(
  132. name=form.attachment_name.data \
  133. or request.files['attachment_file'].filename,
  134. filepath=attachment_public_filepath,
  135. created=datetime.utcnow(),
  136. ))
  137. media.save()
  138. messages.add_message(
  139. request, messages.SUCCESS,
  140. _("You added the attachment %s!") \
  141. % (form.attachment_name.data
  142. or request.files['attachment_file'].filename))
  143. return redirect(request,
  144. location=media.url_for_self(request.urlgen))
  145. return render_to_response(
  146. request,
  147. 'mediagoblin/edit/attachments.html',
  148. {'media': media,
  149. 'form': form})
  150. else:
  151. raise Forbidden("Attachments are disabled")
  152. @require_active_login
  153. def legacy_edit_profile(request):
  154. """redirect the old /edit/profile/?username=USER to /u/USER/edit/"""
  155. username = request.GET.get('username') or request.user.username
  156. return redirect(request, 'mediagoblin.edit.profile', user=username)
  157. @require_active_login
  158. @active_user_from_url
  159. def edit_profile(request, url_user=None):
  160. # admins may edit any user profile
  161. if request.user.username != url_user.username:
  162. if not request.user.has_privilege(u'admin'):
  163. raise Forbidden(_("You can only edit your own profile."))
  164. # No need to warn again if admin just submitted an edited profile
  165. if request.method != 'POST':
  166. messages.add_message(
  167. request, messages.WARNING,
  168. _("You are editing a user's profile. Proceed with caution."))
  169. user = url_user
  170. # Get the location name
  171. if user.location is None:
  172. location = ""
  173. else:
  174. location = user.get_location.name
  175. form = forms.EditProfileForm(request.form,
  176. url=user.url,
  177. bio=user.bio,
  178. location=location)
  179. if request.method == 'POST' and form.validate():
  180. user.url = six.text_type(form.url.data)
  181. user.bio = six.text_type(form.bio.data)
  182. # Save location
  183. if form.location.data and user.location is None:
  184. user.get_location = Location(name=six.text_type(form.location.data))
  185. elif form.location.data:
  186. location = user.get_location
  187. location.name = six.text_type(form.location.data)
  188. location.save()
  189. user.save()
  190. messages.add_message(request,
  191. messages.SUCCESS,
  192. _("Profile changes saved"))
  193. return redirect(request,
  194. 'mediagoblin.user_pages.user_home',
  195. user=user.username)
  196. return render_to_response(
  197. request,
  198. 'mediagoblin/edit/edit_profile.html',
  199. {'user': user,
  200. 'form': form})
  201. EMAIL_VERIFICATION_TEMPLATE = (
  202. u'{uri}?'
  203. u'token={verification_key}')
  204. @require_active_login
  205. def edit_account(request):
  206. user = request.user
  207. form = forms.EditAccountForm(request.form,
  208. wants_comment_notification=user.wants_comment_notification,
  209. license_preference=user.license_preference,
  210. wants_notifications=user.wants_notifications)
  211. if request.method == 'POST' and form.validate():
  212. user.wants_comment_notification = form.wants_comment_notification.data
  213. user.wants_notifications = form.wants_notifications.data
  214. user.license_preference = form.license_preference.data
  215. user.save()
  216. messages.add_message(request,
  217. messages.SUCCESS,
  218. _("Account settings saved"))
  219. return redirect(request,
  220. 'mediagoblin.user_pages.user_home',
  221. user=user.username)
  222. return render_to_response(
  223. request,
  224. 'mediagoblin/edit/edit_account.html',
  225. {'user': user,
  226. 'form': form})
  227. @require_active_login
  228. def deauthorize_applications(request):
  229. """ Deauthroize OAuth applications """
  230. if request.method == 'POST' and "application" in request.form:
  231. token = request.form["application"]
  232. access_token = AccessToken.query.filter_by(token=token).first()
  233. if access_token is None:
  234. messages.add_message(
  235. request,
  236. messages.ERROR,
  237. _("Unknown application, not able to deauthorize")
  238. )
  239. else:
  240. access_token.delete()
  241. messages.add_message(
  242. request,
  243. messages.SUCCESS,
  244. _("Application has been deauthorized")
  245. )
  246. access_tokens = AccessToken.query.filter_by(user=request.user.id)
  247. applications = [(a.get_requesttoken, a) for a in access_tokens]
  248. return render_to_response(
  249. request,
  250. 'mediagoblin/edit/deauthorize_applications.html',
  251. {'applications': applications}
  252. )
  253. @require_active_login
  254. def delete_account(request):
  255. """Delete a user completely"""
  256. user = request.user
  257. if request.method == 'POST':
  258. if request.form.get(u'confirmed'):
  259. # Form submitted and confirmed. Actually delete the user account
  260. # Log out user and delete cookies etc.
  261. # TODO: Should we be using MG.auth.views.py:logout for this?
  262. request.session.delete()
  263. # Delete user account and all related media files etc....
  264. request.user.delete()
  265. # We should send a message that the user has been deleted
  266. # successfully. But we just deleted the session, so we
  267. # can't...
  268. return redirect(request, 'index')
  269. else: # Did not check the confirmation box...
  270. messages.add_message(
  271. request, messages.WARNING,
  272. _('You need to confirm the deletion of your account.'))
  273. # No POST submission or not confirmed, just show page
  274. return render_to_response(
  275. request,
  276. 'mediagoblin/edit/delete_account.html',
  277. {'user': user})
  278. @require_active_login
  279. @user_may_alter_collection
  280. @get_user_collection
  281. def edit_collection(request, collection):
  282. defaults = dict(
  283. title=collection.title,
  284. slug=collection.slug,
  285. description=collection.description)
  286. form = forms.EditCollectionForm(
  287. request.form,
  288. **defaults)
  289. if request.method == 'POST' and form.validate():
  290. # Make sure there isn't already a Collection with such a slug
  291. # and userid.
  292. slug_used = check_collection_slug_used(collection.creator,
  293. form.slug.data, collection.id)
  294. # Make sure there isn't already a Collection with this title
  295. existing_collection = request.db.Collection.query.filter_by(
  296. creator=request.user.id,
  297. title=form.title.data).first()
  298. if existing_collection and existing_collection.id != collection.id:
  299. messages.add_message(
  300. request, messages.ERROR,
  301. _('You already have a collection called "%s"!') % \
  302. form.title.data)
  303. elif slug_used:
  304. form.slug.errors.append(
  305. _(u'A collection with that slug already exists for this user.'))
  306. else:
  307. collection.title = six.text_type(form.title.data)
  308. collection.description = six.text_type(form.description.data)
  309. collection.slug = six.text_type(form.slug.data)
  310. collection.save()
  311. return redirect_obj(request, collection)
  312. if request.user.has_privilege(u'admin') \
  313. and collection.creator != request.user.id \
  314. and request.method != 'POST':
  315. messages.add_message(
  316. request, messages.WARNING,
  317. _("You are editing another user's collection. Proceed with caution."))
  318. return render_to_response(
  319. request,
  320. 'mediagoblin/edit/edit_collection.html',
  321. {'collection': collection,
  322. 'form': form})
  323. def verify_email(request):
  324. """
  325. Email verification view for changing email address
  326. """
  327. # If no token, we can't do anything
  328. if not 'token' in request.GET:
  329. return render_404(request)
  330. # Catch error if token is faked or expired
  331. token = None
  332. try:
  333. token = get_timed_signer_url("mail_verification_token") \
  334. .loads(request.GET['token'], max_age=10*24*3600)
  335. except BadSignature:
  336. messages.add_message(
  337. request,
  338. messages.ERROR,
  339. _('The verification key or user id is incorrect.'))
  340. return redirect(
  341. request,
  342. 'index')
  343. user = User.query.filter_by(id=int(token['user'])).first()
  344. if user:
  345. user.email = token['email']
  346. user.save()
  347. messages.add_message(
  348. request,
  349. messages.SUCCESS,
  350. _('Your email address has been verified.'))
  351. else:
  352. messages.add_message(
  353. request,
  354. messages.ERROR,
  355. _('The verification key or user id is incorrect.'))
  356. return redirect(
  357. request, 'mediagoblin.user_pages.user_home',
  358. user=user.username)
  359. def change_email(request):
  360. """ View to change the user's email """
  361. form = forms.ChangeEmailForm(request.form)
  362. user = request.user
  363. # If no password authentication, no need to enter a password
  364. if 'pass_auth' not in request.template_env.globals or not user.pw_hash:
  365. form.__delitem__('password')
  366. if request.method == 'POST' and form.validate():
  367. new_email = form.new_email.data
  368. users_with_email = User.query.filter_by(
  369. email=new_email).count()
  370. if users_with_email:
  371. form.new_email.errors.append(
  372. _('Sorry, a user with that email address'
  373. ' already exists.'))
  374. if form.password and user.pw_hash and not check_password(
  375. form.password.data, user.pw_hash):
  376. form.password.errors.append(
  377. _('Wrong password'))
  378. if not form.errors:
  379. verification_key = get_timed_signer_url(
  380. 'mail_verification_token').dumps({
  381. 'user': user.id,
  382. 'email': new_email})
  383. rendered_email = render_template(
  384. request, 'mediagoblin/edit/verification.txt',
  385. {'username': user.username,
  386. 'verification_url': EMAIL_VERIFICATION_TEMPLATE.format(
  387. uri=request.urlgen('mediagoblin.edit.verify_email',
  388. qualified=True),
  389. verification_key=verification_key)})
  390. email_debug_message(request)
  391. auth_tools.send_verification_email(user, request, new_email,
  392. rendered_email)
  393. return redirect(request, 'mediagoblin.edit.account')
  394. return render_to_response(
  395. request,
  396. 'mediagoblin/edit/change_email.html',
  397. {'form': form,
  398. 'user': user})
  399. @user_has_privilege(u'admin')
  400. @require_active_login
  401. @get_media_entry_by_id
  402. def edit_metadata(request, media):
  403. form = forms.EditMetaDataForm(request.form)
  404. if request.method == "POST" and form.validate():
  405. metadata_dict = dict([(row['identifier'],row['value'])
  406. for row in form.media_metadata.data])
  407. json_ld_metadata = None
  408. json_ld_metadata = compact_and_validate(metadata_dict)
  409. media.media_metadata = json_ld_metadata
  410. media.save()
  411. return redirect_obj(request, media)
  412. if len(form.media_metadata) == 0:
  413. for identifier, value in six.iteritems(media.media_metadata):
  414. if identifier == "@context": continue
  415. form.media_metadata.append_entry({
  416. 'identifier':identifier,
  417. 'value':value})
  418. return render_to_response(
  419. request,
  420. 'mediagoblin/edit/metadata.html',
  421. {'form':form,
  422. 'media':media})