views.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514
  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, LocalUser, 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.actor, 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.actor != 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(actor=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. user = User.query.filter(User.id==user.id).first()
  265. user.delete()
  266. # We should send a message that the user has been deleted
  267. # successfully. But we just deleted the session, so we
  268. # can't...
  269. return redirect(request, 'index')
  270. else: # Did not check the confirmation box...
  271. messages.add_message(
  272. request, messages.WARNING,
  273. _('You need to confirm the deletion of your account.'))
  274. # No POST submission or not confirmed, just show page
  275. return render_to_response(
  276. request,
  277. 'mediagoblin/edit/delete_account.html',
  278. {'user': user})
  279. @require_active_login
  280. @user_may_alter_collection
  281. @get_user_collection
  282. def edit_collection(request, collection):
  283. defaults = dict(
  284. title=collection.title,
  285. slug=collection.slug,
  286. description=collection.description)
  287. form = forms.EditCollectionForm(
  288. request.form,
  289. **defaults)
  290. if request.method == 'POST' and form.validate():
  291. # Make sure there isn't already a Collection with such a slug
  292. # and userid.
  293. slug_used = check_collection_slug_used(collection.actor,
  294. form.slug.data, collection.id)
  295. # Make sure there isn't already a Collection with this title
  296. existing_collection = request.db.Collection.query.filter_by(
  297. actor=request.user.id,
  298. title=form.title.data).first()
  299. if existing_collection and existing_collection.id != collection.id:
  300. messages.add_message(
  301. request, messages.ERROR,
  302. _('You already have a collection called "%s"!') % \
  303. form.title.data)
  304. elif slug_used:
  305. form.slug.errors.append(
  306. _(u'A collection with that slug already exists for this user.'))
  307. else:
  308. collection.title = six.text_type(form.title.data)
  309. collection.description = six.text_type(form.description.data)
  310. collection.slug = six.text_type(form.slug.data)
  311. collection.save()
  312. return redirect_obj(request, collection)
  313. if request.user.has_privilege(u'admin') \
  314. and collection.actor != request.user.id \
  315. and request.method != 'POST':
  316. messages.add_message(
  317. request, messages.WARNING,
  318. _("You are editing another user's collection. Proceed with caution."))
  319. return render_to_response(
  320. request,
  321. 'mediagoblin/edit/edit_collection.html',
  322. {'collection': collection,
  323. 'form': form})
  324. def verify_email(request):
  325. """
  326. Email verification view for changing email address
  327. """
  328. # If no token, we can't do anything
  329. if not 'token' in request.GET:
  330. return render_404(request)
  331. # Catch error if token is faked or expired
  332. token = None
  333. try:
  334. token = get_timed_signer_url("mail_verification_token") \
  335. .loads(request.GET['token'], max_age=10*24*3600)
  336. except BadSignature:
  337. messages.add_message(
  338. request,
  339. messages.ERROR,
  340. _('The verification key or user id is incorrect.'))
  341. return redirect(
  342. request,
  343. 'index')
  344. user = User.query.filter_by(id=int(token['user'])).first()
  345. if user:
  346. user.email = token['email']
  347. user.save()
  348. messages.add_message(
  349. request,
  350. messages.SUCCESS,
  351. _('Your email address has been verified.'))
  352. else:
  353. messages.add_message(
  354. request,
  355. messages.ERROR,
  356. _('The verification key or user id is incorrect.'))
  357. return redirect(
  358. request, 'mediagoblin.user_pages.user_home',
  359. user=user.username)
  360. def change_email(request):
  361. """ View to change the user's email """
  362. form = forms.ChangeEmailForm(request.form)
  363. user = request.user
  364. # If no password authentication, no need to enter a password
  365. if 'pass_auth' not in request.template_env.globals or not user.pw_hash:
  366. form.__delitem__('password')
  367. if request.method == 'POST' and form.validate():
  368. new_email = form.new_email.data
  369. users_with_email = User.query.filter(
  370. LocalUser.email==new_email
  371. ).count()
  372. if users_with_email:
  373. form.new_email.errors.append(
  374. _('Sorry, a user with that email address'
  375. ' already exists.'))
  376. if form.password and user.pw_hash and not check_password(
  377. form.password.data, user.pw_hash):
  378. form.password.errors.append(
  379. _('Wrong password'))
  380. if not form.errors:
  381. verification_key = get_timed_signer_url(
  382. 'mail_verification_token').dumps({
  383. 'user': user.id,
  384. 'email': new_email})
  385. rendered_email = render_template(
  386. request, 'mediagoblin/edit/verification.txt',
  387. {'username': user.username,
  388. 'verification_url': EMAIL_VERIFICATION_TEMPLATE.format(
  389. uri=request.urlgen('mediagoblin.edit.verify_email',
  390. qualified=True),
  391. verification_key=verification_key)})
  392. email_debug_message(request)
  393. auth_tools.send_verification_email(user, request, new_email,
  394. rendered_email)
  395. return redirect(request, 'mediagoblin.edit.account')
  396. return render_to_response(
  397. request,
  398. 'mediagoblin/edit/change_email.html',
  399. {'form': form,
  400. 'user': user})
  401. @user_has_privilege(u'admin')
  402. @require_active_login
  403. @get_media_entry_by_id
  404. def edit_metadata(request, media):
  405. form = forms.EditMetaDataForm(request.form)
  406. if request.method == "POST" and form.validate():
  407. metadata_dict = dict([(row['identifier'],row['value'])
  408. for row in form.media_metadata.data])
  409. json_ld_metadata = None
  410. json_ld_metadata = compact_and_validate(metadata_dict)
  411. media.media_metadata = json_ld_metadata
  412. media.save()
  413. return redirect_obj(request, media)
  414. if len(form.media_metadata) == 0:
  415. for identifier, value in six.iteritems(media.media_metadata):
  416. if identifier == "@context": continue
  417. form.media_metadata.append_entry({
  418. 'identifier':identifier,
  419. 'value':value})
  420. return render_to_response(
  421. request,
  422. 'mediagoblin/edit/metadata.html',
  423. {'form':form,
  424. 'media':media})