diff options
Diffstat (limited to 'mediagoblin')
69 files changed, 1772 insertions, 356 deletions
diff --git a/mediagoblin/auth/routing.py b/mediagoblin/auth/routing.py index 2a6abb47..7a688a49 100644 --- a/mediagoblin/auth/routing.py +++ b/mediagoblin/auth/routing.py @@ -25,9 +25,4 @@ auth_routes = [ ('mediagoblin.auth.verify_email', '/verify_email/', 'mediagoblin.auth.views:verify_email'), ('mediagoblin.auth.resend_verification', '/resend_verification/', - 'mediagoblin.auth.views:resend_activation'), - ('mediagoblin.auth.forgot_password', '/forgot_password/', - 'mediagoblin.auth.views:forgot_password'), - ('mediagoblin.auth.verify_forgot_password', - '/forgot_password/verify/', - 'mediagoblin.auth.views:verify_forgot_password')] + 'mediagoblin.auth.views:resend_activation')] diff --git a/mediagoblin/auth/tools.py b/mediagoblin/auth/tools.py index 76b37e29..88716e1c 100644 --- a/mediagoblin/auth/tools.py +++ b/mediagoblin/auth/tools.py @@ -103,38 +103,6 @@ def send_verification_email(user, request, email=None, rendered_email) -EMAIL_FP_VERIFICATION_TEMPLATE = ( - u"{uri}?" - u"token={fp_verification_key}") - - -def send_fp_verification_email(user, request): - """ - Send the verification email to users to change their password. - - Args: - - user: a user object - - request: the request - """ - fp_verification_key = get_timed_signer_url('mail_verification_token') \ - .dumps(user.id) - - rendered_email = render_template( - request, 'mediagoblin/auth/fp_verification_email.txt', - {'username': user.username, - 'verification_url': EMAIL_FP_VERIFICATION_TEMPLATE.format( - uri=request.urlgen('mediagoblin.auth.verify_forgot_password', - qualified=True), - fp_verification_key=fp_verification_key)}) - - # TODO: There is no error handling in place - send_email( - mg_globals.app_config['email_sender_address'], - [user.email], - 'GNU MediaGoblin - Change forgotten password!', - rendered_email) - - def basic_extra_validation(register_form, *args): users_with_username = User.query.filter_by( username=register_form.username.data).count() @@ -206,7 +174,10 @@ def check_auth_enabled(): def no_auth_logout(request): - """Log out the user if authentication_disabled, but don't delete the messages""" + """ + Log out the user if no authentication is enabled, but don't delete + the messages + """ if not mg_globals.app.auth and 'user_id' in request.session: del request.session['user_id'] request.session.save() diff --git a/mediagoblin/auth/views.py b/mediagoblin/auth/views.py index 6f343d0e..dc03515b 100644 --- a/mediagoblin/auth/views.py +++ b/mediagoblin/auth/views.py @@ -24,11 +24,8 @@ from mediagoblin.tools.response import render_to_response, redirect, render_404 from mediagoblin.tools.translate import pass_to_ugettext as _ from mediagoblin.tools.mail import email_debug_message from mediagoblin.tools.pluginapi import hook_handle -from mediagoblin.auth import forms as auth_forms from mediagoblin.auth.tools import (send_verification_email, register_user, - send_fp_verification_email, check_login_simple) -from mediagoblin import auth @allow_registration diff --git a/mediagoblin/config_spec.ini b/mediagoblin/config_spec.ini index 0e224503..d738074d 100644 --- a/mediagoblin/config_spec.ini +++ b/mediagoblin/config_spec.ini @@ -78,6 +78,12 @@ theme = string() plugin_web_path = string(default="/plugin_static/") plugin_linked_assets_dir = string(default="%(here)s/user_dev/plugin_static/") +# Default user upload limit (in Mb) +upload_limit = integer(default=None) + +# Max file size (in Mb) +max_file_size = integer(default=None) + [jinja2] # Jinja2 supports more directives than the minimum required by mediagoblin. # This setting allows users creating custom templates to specify a list of diff --git a/mediagoblin/db/extratypes.py b/mediagoblin/db/extratypes.py index f2304af0..8e04d58d 100644 --- a/mediagoblin/db/extratypes.py +++ b/mediagoblin/db/extratypes.py @@ -15,6 +15,7 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. +from sqlalchemy.ext.mutable import Mutable from sqlalchemy.types import TypeDecorator, Unicode, TEXT import json @@ -38,7 +39,7 @@ class PathTupleWithSlashes(TypeDecorator): return value -# The following class and only this one class is in very +# The following two classes and only these two classes is in very # large parts based on example code from sqlalchemy. # # The original copyright notice and license follows: @@ -61,3 +62,30 @@ class JSONEncoded(TypeDecorator): if value is not None: value = json.loads(value) return value + + +class MutationDict(Mutable, dict): + @classmethod + def coerce(cls, key, value): + "Convert plain dictionaries to MutationDict." + + if not isinstance(value, MutationDict): + if isinstance(value, dict): + return MutationDict(value) + + # this call will raise ValueError + return Mutable.coerce(key, value) + else: + return value + + def __setitem__(self, key, value): + "Detect dictionary set events and emit change events." + + dict.__setitem__(self, key, value) + self.changed() + + def __delitem__(self, key): + "Detect dictionary del events and emit change events." + + dict.__delitem__(self, key) + self.changed() diff --git a/mediagoblin/db/migrations.py b/mediagoblin/db/migrations.py index 6616b657..0356c672 100644 --- a/mediagoblin/db/migrations.py +++ b/mediagoblin/db/migrations.py @@ -26,7 +26,7 @@ from sqlalchemy.sql import and_ from migrate.changeset.constraint import UniqueConstraint -from mediagoblin.db.extratypes import JSONEncoded +from mediagoblin.db.extratypes import JSONEncoded, MutationDict from mediagoblin.db.migration_tools import RegisterMigration, inspect_table from mediagoblin.db.models import (MediaEntry, Collection, MediaComment, User, Privilege) @@ -474,6 +474,43 @@ def wants_notifications(db): col.create(user_table) db.commit() + + +@RegisterMigration(16, MIGRATIONS) +def upload_limits(db): + """Add user upload limit columns""" + metadata = MetaData(bind=db.bind) + + user_table = inspect_table(metadata, 'core__users') + media_entry_table = inspect_table(metadata, 'core__media_entries') + + col = Column('uploaded', Integer, default=0) + col.create(user_table) + + col = Column('upload_limit', Integer) + col.create(user_table) + + col = Column('file_size', Integer, default=0) + col.create(media_entry_table) + + db.commit() + + +@RegisterMigration(17, MIGRATIONS) +def add_file_metadata(db): + """Add file_metadata to MediaFile""" + metadata = MetaData(bind=db.bind) + media_file_table = inspect_table(metadata, "core__mediafiles") + + col = Column('file_metadata', MutationDict.as_mutable(JSONEncoded)) + col.create(media_file_table) + + db.commit() + +################### +# Moderation tables +################### + class ReportBase_v0(declarative_base()): __tablename__ = 'core__reports' id = Column(Integer, primary_key=True) @@ -487,6 +524,7 @@ class ReportBase_v0(declarative_base()): result = Column(UnicodeText) __mapper_args__ = {'polymorphic_on': discriminator} + class CommentReport_v0(ReportBase_v0): __tablename__ = 'core__reports_on_comments' __mapper_args__ = {'polymorphic_identity': 'comment_report'} @@ -496,7 +534,6 @@ class CommentReport_v0(ReportBase_v0): comment_id = Column(Integer, ForeignKey(MediaComment.id), nullable=True) - class MediaReport_v0(ReportBase_v0): __tablename__ = 'core__reports_on_media' __mapper_args__ = {'polymorphic_identity': 'media_report'} @@ -504,6 +541,7 @@ class MediaReport_v0(ReportBase_v0): id = Column('id',Integer, ForeignKey('core__reports.id'), primary_key=True) media_entry_id = Column(Integer, ForeignKey(MediaEntry.id), nullable=True) + class UserBan_v0(declarative_base()): __tablename__ = 'core__user_bans' user_id = Column(Integer, ForeignKey(User.id), nullable=False, @@ -511,11 +549,13 @@ class UserBan_v0(declarative_base()): expiration_date = Column(Date) reason = Column(UnicodeText, nullable=False) + class Privilege_v0(declarative_base()): __tablename__ = 'core__privileges' id = Column(Integer, nullable=False, primary_key=True, unique=True) privilege_name = Column(Unicode, nullable=False, unique=True) + class PrivilegeUserAssociation_v0(declarative_base()): __tablename__ = 'core__privileges_users' privilege_id = Column( @@ -529,13 +569,18 @@ class PrivilegeUserAssociation_v0(declarative_base()): ForeignKey(Privilege.id), primary_key=True) + PRIVILEGE_FOUNDATIONS_v0 = [{'privilege_name':u'admin'}, - {'privilege_name':u'moderator'}, - {'privilege_name':u'uploader'}, - {'privilege_name':u'reporter'}, - {'privilege_name':u'commenter'}, - {'privilege_name':u'active'}] + {'privilege_name':u'moderator'}, + {'privilege_name':u'uploader'}, + {'privilege_name':u'reporter'}, + {'privilege_name':u'commenter'}, + {'privilege_name':u'active'}] + +# vR1 stands for "version Rename 1". This only exists because we need +# to deal with dropping some booleans and it's otherwise impossible +# with sqlite. class User_vR1(declarative_base()): __tablename__ = 'rename__users' @@ -550,6 +595,7 @@ class User_vR1(declarative_base()): url = Column(Unicode) bio = Column(UnicodeText) # ?? + @RegisterMigration(18, MIGRATIONS) def create_moderation_tables(db): diff --git a/mediagoblin/db/models.py b/mediagoblin/db/models.py index 5173be9e..1514a3aa 100644 --- a/mediagoblin/db/models.py +++ b/mediagoblin/db/models.py @@ -30,8 +30,8 @@ from sqlalchemy.sql.expression import desc from sqlalchemy.ext.associationproxy import association_proxy from sqlalchemy.util import memoized_property - -from mediagoblin.db.extratypes import PathTupleWithSlashes, JSONEncoded +from mediagoblin.db.extratypes import (PathTupleWithSlashes, JSONEncoded, + MutationDict) from mediagoblin.db.base import Base, DictReadAttrProxy from mediagoblin.db.mixin import UserMixin, MediaEntryMixin, \ MediaCommentMixin, CollectionMixin, CollectionItemMixin @@ -48,6 +48,7 @@ from migrate import changeset _log = logging.getLogger(__name__) + class User(Base, UserMixin): """ TODO: We should consider moving some rarely used fields @@ -71,6 +72,8 @@ class User(Base, UserMixin): license_preference = Column(Unicode) url = Column(Unicode) bio = Column(UnicodeText) # ?? + uploaded = Column(Integer, default=0) + upload_limit = Column(Integer) ## TODO # plugin data would be in a separate model @@ -217,6 +220,7 @@ class MediaEntry(Base, MediaEntryMixin): # or use sqlalchemy.types.Enum? license = Column(Unicode) collected = Column(Integer, default=0) + file_size = Column(Integer, default=0) fail_error = Column(Unicode) fail_metadata = Column(JSONEncoded) @@ -291,6 +295,35 @@ class MediaEntry(Base, MediaEntryMixin): if media is not None: return media.url_for_self(urlgen) + def get_file_metadata(self, file_key, metadata_key=None): + """ + Return the file_metadata dict of a MediaFile. If metadata_key is given, + return the value of the key. + """ + media_file = MediaFile.query.filter_by(media_entry=self.id, + name=unicode(file_key)).first() + + if media_file: + if metadata_key: + return media_file.file_metadata.get(metadata_key, None) + + return media_file.file_metadata + + def set_file_metadata(self, file_key, **kwargs): + """ + Update the file_metadata of a MediaFile. + """ + media_file = MediaFile.query.filter_by(media_entry=self.id, + name=unicode(file_key)).first() + + file_metadata = media_file.file_metadata or {} + + for key, value in kwargs.iteritems(): + file_metadata[key] = value + + media_file.file_metadata = file_metadata + media_file.save() + @property def media_data(self): return getattr(self, self.media_data_ref) @@ -387,6 +420,7 @@ class MediaFile(Base): nullable=False) name_id = Column(SmallInteger, ForeignKey(FileKeynames.id), nullable=False) file_path = Column(PathTupleWithSlashes) + file_metadata = Column(MutationDict.as_mutable(JSONEncoded)) __table_args__ = ( PrimaryKeyConstraint('media_entry', 'name_id'), diff --git a/mediagoblin/edit/forms.py b/mediagoblin/edit/forms.py index 388940b4..2c9b5e99 100644 --- a/mediagoblin/edit/forms.py +++ b/mediagoblin/edit/forms.py @@ -19,7 +19,7 @@ import wtforms from mediagoblin.tools.text import tag_length_validator from mediagoblin.tools.translate import lazy_pass_to_ugettext as _ from mediagoblin.tools.licenses import licenses_as_choices -from mediagoblin.auth.forms import normalize_user_or_email_field +from mediagoblin.auth.tools import normalize_user_or_email_field class EditForm(wtforms.Form): @@ -81,6 +81,7 @@ class EditAttachmentsForm(wtforms.Form): attachment_file = wtforms.FileField( 'File') + class EditCollectionForm(wtforms.Form): title = wtforms.TextField( _('Title'), diff --git a/mediagoblin/edit/routing.py b/mediagoblin/edit/routing.py index 75f5a6d8..a2d03d26 100644 --- a/mediagoblin/edit/routing.py +++ b/mediagoblin/edit/routing.py @@ -24,8 +24,6 @@ add_route('mediagoblin.edit.account', '/edit/account/', 'mediagoblin.edit.views:edit_account') add_route('mediagoblin.edit.delete_account', '/edit/account/delete/', 'mediagoblin.edit.views:delete_account') -add_route('mediagoblin.edit.pass', '/edit/password/', - 'mediagoblin.edit.views:change_pass') add_route('mediagoblin.edit.verify_email', '/edit/verify_email/', 'mediagoblin.edit.views:verify_email') add_route('mediagoblin.edit.email', '/edit/email/', diff --git a/mediagoblin/edit/views.py b/mediagoblin/edit/views.py index 28952fa0..80590875 100644 --- a/mediagoblin/edit/views.py +++ b/mediagoblin/edit/views.py @@ -23,8 +23,8 @@ from werkzeug.utils import secure_filename from mediagoblin import messages from mediagoblin import mg_globals -from mediagoblin import auth -from mediagoblin.auth import tools as auth_tools +from mediagoblin.auth import (check_password, + tools as auth_tools) from mediagoblin.edit import forms from mediagoblin.edit.lib import may_edit_media from mediagoblin.decorators import (require_active_login, active_user_from_url, @@ -338,46 +338,6 @@ def edit_collection(request, collection): 'form': form}) -@require_active_login -def change_pass(request): - # If no password authentication, no need to change your password - if 'pass_auth' not in request.template_env.globals: - return redirect(request, 'index') - - form = forms.ChangePassForm(request.form) - user = request.user - - if request.method == 'POST' and form.validate(): - - if not auth.check_password( - form.old_password.data, user.pw_hash): - form.old_password.errors.append( - _('Wrong password')) - - return render_to_response( - request, - 'mediagoblin/edit/change_pass.html', - {'form': form, - 'user': user}) - - # Password matches - user.pw_hash = auth.gen_password_hash( - form.new_password.data) - user.save() - - messages.add_message( - request, messages.SUCCESS, - _('Your password was changed successfully')) - - return redirect(request, 'mediagoblin.edit.account') - - return render_to_response( - request, - 'mediagoblin/edit/change_pass.html', - {'form': form, - 'user': user}) - - def verify_email(request): """ Email verification view for changing email address @@ -442,7 +402,7 @@ def change_email(request): _('Sorry, a user with that email address' ' already exists.')) - if form.password and user.pw_hash and not auth.check_password( + if form.password and user.pw_hash and not check_password( form.password.data, user.pw_hash): form.password.errors.append( _('Wrong password')) diff --git a/mediagoblin/media_types/ascii/processing.py b/mediagoblin/media_types/ascii/processing.py index 3b287877..b3c2a298 100644 --- a/mediagoblin/media_types/ascii/processing.py +++ b/mediagoblin/media_types/ascii/processing.py @@ -120,6 +120,9 @@ class CommonAsciiProcessor(MediaProcessor): thumb_size = (mgg.global_config['media:thumb']['max_width'], mgg.global_config['media:thumb']['max_height']) + if self._skip_resizing(font, thumb_size): + return + tmp_thumb = os.path.join( self.conversions_subdir, self.name_builder.fill('{basename}.thumbnail.png')) @@ -144,10 +147,33 @@ class CommonAsciiProcessor(MediaProcessor): Image.ANTIALIAS) thumb.save(thumb_file) + thumb_info = {'font': font, + 'width': thumb_size[0], + 'height': thumb_size[1]} + + self.entry.set_file_metadata('thumb', **thumb_info) + _log.debug('Copying local file to public storage') store_public(self.entry, 'thumb', tmp_thumb, self.name_builder.fill('{basename}.thumbnail.jpg')) + def _skip_resizing(self, font, thumb_size): + thumb_info = self.entry.get_file_metadata('thumb') + + if not thumb_info: + return False + + skip = True + + if thumb_info.get('font') != font: + skip = False + elif thumb_info.get('width') != thumb_size[0]: + skip = False + elif thumb_info.get('height') != thumb_size[1]: + skip = False + + return skip + class InitialProcessor(CommonAsciiProcessor): """ diff --git a/mediagoblin/media_types/audio/processing.py b/mediagoblin/media_types/audio/processing.py index d0716448..883d3ffc 100644 --- a/mediagoblin/media_types/audio/processing.py +++ b/mediagoblin/media_types/audio/processing.py @@ -87,10 +87,34 @@ class CommonAudioProcessor(MediaProcessor): self.entry.media_files['best_quality'] = self.entry \ .media_files['webm_audio'] + def _skip_processing(self, keyname, **kwargs): + file_metadata = self.entry.get_file_metadata(keyname) + skip = True + + if not file_metadata: + return False + + if keyname == 'webm_audio': + if kwargs.get('quality') != file_metadata.get('quality'): + skip = False + elif keyname == 'spectrogram': + if kwargs.get('max_width') != file_metadata.get('max_width'): + skip = False + elif kwargs.get('fft_size') != file_metadata.get('fft_size'): + skip = False + elif keyname == 'thumb': + if kwargs.get('size') != file_metadata.get('size'): + skip = False + + return skip + def transcode(self, quality=None): if not quality: quality = self.audio_config['quality'] + if self._skip_processing('webm_audio', quality=quality): + return + progress_callback = ProgressCallback(self.entry) webm_audio_tmp = os.path.join(self.workbench.dir, self.name_builder.fill( @@ -110,12 +134,18 @@ class CommonAudioProcessor(MediaProcessor): store_public(self.entry, 'webm_audio', webm_audio_tmp, self.name_builder.fill('{basename}.medium.webm')) + self.entry.set_file_metadata('webm_audio', **{'quality': quality}) + def create_spectrogram(self, max_width=None, fft_size=None): if not max_width: max_width = mgg.global_config['media:medium']['max_width'] if not fft_size: fft_size = self.audio_config['spectrogram_fft_size'] + if self._skip_processing('spectrogram', max_width=max_width, + fft_size=fft_size): + return + wav_tmp = os.path.join(self.workbench.dir, self.name_builder.fill( '{basename}.ogg')) @@ -140,12 +170,19 @@ class CommonAudioProcessor(MediaProcessor): store_public(self.entry, 'spectrogram', spectrogram_tmp, self.name_builder.fill('{basename}.spectrogram.jpg')) + file_metadata = {'max_width': max_width, + 'fft_size': fft_size} + self.entry.set_file_metadata('spectrogram', **file_metadata) + def generate_thumb(self, size=None): if not size: max_width = mgg.global_config['media:thumb']['max_width'] max_height = mgg.global_config['media:thumb']['max_height'] size = (max_width, max_height) + if self._skip_processing('thumb', size=size): + return + thumb_tmp = os.path.join(self.workbench.dir, self.name_builder.fill( '{basename}-thumbnail.jpg')) @@ -166,6 +203,8 @@ class CommonAudioProcessor(MediaProcessor): store_public(self.entry, 'thumb', thumb_tmp, self.name_builder.fill('{basename}.thumbnail.jpg')) + self.entry.set_file_metadata('thumb', **{'size': size}) + class InitialProcessor(CommonAudioProcessor): """ diff --git a/mediagoblin/media_types/image/processing.py b/mediagoblin/media_types/image/processing.py index 4c7bdb15..a0ad2ce8 100644 --- a/mediagoblin/media_types/image/processing.py +++ b/mediagoblin/media_types/image/processing.py @@ -76,6 +76,14 @@ def resize_image(entry, resized, keyname, target_name, new_size, resized.save(resized_file, quality=quality) store_public(entry, keyname, tmp_resized_filename, target_name) + # store the thumb/medium info + image_info = {'width': new_size[0], + 'height': new_size[1], + 'quality': quality, + 'filter': filter} + + entry.set_file_metadata(keyname, **image_info) + def resize_tool(entry, force, keyname, orig_file, target_name, @@ -86,6 +94,13 @@ def resize_tool(entry, max_height = mgg.global_config['media:' + keyname]['max_height'] new_size = (max_width, max_height) + # If thumb or medium is already the same quality and size, then don't + # reprocess + if _skip_resizing(entry, keyname, new_size, quality, filter): + _log.info('{0} of same size and quality already in use, skipping ' + 'resizing of media {1}.'.format(keyname, entry.id)) + return + # If the size of the original file exceeds the specified size for the desized # file, a target_name file is created and later associated with the media # entry. @@ -105,6 +120,32 @@ def resize_tool(entry, quality, filter) +def _skip_resizing(entry, keyname, size, quality, filter): + """ + Determines wither the saved thumb or medium is of the same quality and size + """ + image_info = entry.get_file_metadata(keyname) + + if not image_info: + return False + + skip = True + + if image_info.get('width') != size[0]: + skip = False + + elif image_info.get('height') != size[1]: + skip = False + + elif image_info.get('filter') != filter: + skip = False + + elif image_info.get('quality') != quality: + skip = False + + return skip + + SUPPORTED_FILETYPES = ['png', 'gif', 'jpg', 'jpeg', 'tiff'] diff --git a/mediagoblin/media_types/pdf/processing.py b/mediagoblin/media_types/pdf/processing.py index 549def69..b60495eb 100644 --- a/mediagoblin/media_types/pdf/processing.py +++ b/mediagoblin/media_types/pdf/processing.py @@ -261,6 +261,22 @@ class CommonPdfProcessor(MediaProcessor): else: self.pdf_filename = self._generate_pdf() + def _skip_processing(self, keyname, **kwargs): + file_metadata = self.entry.get_file_metadata(keyname) + skip = True + + if not file_metadata: + return False + + if keyname == 'thumb': + if kwargs.get('thumb_size') != file_metadata.get('thumb_size'): + skip = False + elif keyname == 'medium': + if kwargs.get('size') != file_metadata.get('size'): + skip = False + + return skip + def copy_original(self): copy_original( self.entry, self.process_filename, @@ -271,6 +287,9 @@ class CommonPdfProcessor(MediaProcessor): thumb_size = (mgg.global_config['media:thumb']['max_width'], mgg.global_config['media:thumb']['max_height']) + if self._skip_processing('thumb', thumb_size=thumb_size): + return + # Note: pdftocairo adds '.png', so don't include an ext thumb_filename = os.path.join(self.workbench.dir, self.name_builder.fill( @@ -288,6 +307,8 @@ class CommonPdfProcessor(MediaProcessor): store_public(self.entry, 'thumb', thumb_filename + '.png', self.name_builder.fill('{basename}.thumbnail.png')) + self.entry.set_file_metadata('thumb', thumb_size=thumb_size) + def _generate_pdf(self): """ Store the pdf. If the file is not a pdf, make it a pdf @@ -317,6 +338,9 @@ class CommonPdfProcessor(MediaProcessor): size = (mgg.global_config['media:medium']['max_width'], mgg.global_config['media:medium']['max_height']) + if self._skip_processing('medium', size=size): + return + # Note: pdftocairo adds '.png', so don't include an ext filename = os.path.join(self.workbench.dir, self.name_builder.fill('{basename}.medium')) @@ -333,6 +357,8 @@ class CommonPdfProcessor(MediaProcessor): store_public(self.entry, 'medium', filename + '.png', self.name_builder.fill('{basename}.medium.png')) + self.entry.set_file_metadata('medium', size=size) + class InitialProcessor(CommonPdfProcessor): """ diff --git a/mediagoblin/media_types/stl/processing.py b/mediagoblin/media_types/stl/processing.py index 77d3d86e..39949b59 100644 --- a/mediagoblin/media_types/stl/processing.py +++ b/mediagoblin/media_types/stl/processing.py @@ -144,11 +144,30 @@ class CommonStlProcessor(MediaProcessor): # copy it up! store_public(self.entry, keyname, workbench_path, filename) + def _skip_processing(self, keyname, **kwargs): + file_metadata = self.entry.get_file_metadata(keyname) + + if not file_metadata: + return False + skip = True + + if keyname == 'thumb': + if kwargs.get('thumb_size') != file_metadata.get('thumb_size'): + skip = False + else: + if kwargs.get('size') != file_metadata.get('size'): + skip = False + + return skip + def generate_thumb(self, thumb_size=None): if not thumb_size: thumb_size = (mgg.global_config['media:thumb']['max_width'], mgg.global_config['media:thumb']['max_height']) + if self._skip_processing('thumb', thumb_size=thumb_size): + return + self._snap( "thumb", "{basename}.thumb.jpg", @@ -156,11 +175,16 @@ class CommonStlProcessor(MediaProcessor): thumb_size, project="PERSP") + self.entry.set_file_metadata('thumb', thumb_size=thumb_size) + def generate_perspective(self, size=None): if not size: size = (mgg.global_config['media:medium']['max_width'], mgg.global_config['media:medium']['max_height']) + if self._skip_processing('perspective', size=size): + return + self._snap( "perspective", "{basename}.perspective.jpg", @@ -168,11 +192,16 @@ class CommonStlProcessor(MediaProcessor): size, project="PERSP") + self.entry.set_file_metadata('perspective', size=size) + def generate_topview(self, size=None): if not size: size = (mgg.global_config['media:medium']['max_width'], mgg.global_config['media:medium']['max_height']) + if self._skip_processing('top', size=size): + return + self._snap( "top", "{basename}.top.jpg", @@ -180,11 +209,16 @@ class CommonStlProcessor(MediaProcessor): self.greatest*2], size) + self.entry.set_file_metadata('top', size=size) + def generate_frontview(self, size=None): if not size: size = (mgg.global_config['media:medium']['max_width'], mgg.global_config['media:medium']['max_height']) + if self._skip_processing('front', size=size): + return + self._snap( "front", "{basename}.front.jpg", @@ -192,11 +226,16 @@ class CommonStlProcessor(MediaProcessor): self.model.average[2]], size) + self.entry.set_file_metadata('front', size=size) + def generate_sideview(self, size=None): if not size: size = (mgg.global_config['media:medium']['max_width'], mgg.global_config['media:medium']['max_height']) + if self._skip_processing('side', size=size): + return + self._snap( "side", "{basename}.side.jpg", @@ -204,6 +243,8 @@ class CommonStlProcessor(MediaProcessor): self.model.average[2]], size) + self.entry.set_file_metadata('side', size=size) + def store_dimensions(self): """ Put model dimensions into the database diff --git a/mediagoblin/media_types/video/processing.py b/mediagoblin/media_types/video/processing.py index 4d1d5ea2..5ef9b854 100644 --- a/mediagoblin/media_types/video/processing.py +++ b/mediagoblin/media_types/video/processing.py @@ -155,6 +155,29 @@ class CommonVideoProcessor(MediaProcessor): self.entry.media_files['best_quality'] = self.entry \ .media_files['webm_video'] + def _skip_processing(self, keyname, **kwargs): + file_metadata = self.entry.get_file_metadata(keyname) + + if not file_metadata: + return False + skip = True + + if keyname == 'webm_video': + if kwargs.get('medium_size') != file_metadata.get('medium_size'): + skip = False + elif kwargs.get('vp8_quality') != file_metadata.get('vp8_quality'): + skip = False + elif kwargs.get('vp8_threads') != file_metadata.get('vp8_threads'): + skip = False + elif kwargs.get('vorbis_quality') != \ + file_metadata.get('vorbis_quality'): + skip = False + elif keyname == 'thumb': + if kwargs.get('thumb_size') != file_metadata.get('thumb_size'): + skip = False + + return skip + def transcode(self, medium_size=None, vp8_quality=None, vp8_threads=None, vorbis_quality=None): @@ -173,6 +196,14 @@ class CommonVideoProcessor(MediaProcessor): if not vorbis_quality: vorbis_quality = self.video_config['vorbis_quality'] + file_metadata = {'medium_size': medium_size, + 'vp8_threads': vp8_threads, + 'vp8_quality': vp8_quality, + 'vorbis_quality': vorbis_quality} + + if self._skip_processing('webm_video', **file_metadata): + return + # Extract metadata and keep a record of it metadata = self.transcoder.discover(self.process_filename) store_metadata(self.entry, metadata) @@ -209,6 +240,8 @@ class CommonVideoProcessor(MediaProcessor): self.name_builder.fill('{basename}.medium.webm')) _log.debug('Saved medium') + self.entry.set_file_metadata('webm_video', **file_metadata) + self.did_transcode = True # Save the width and height of the transcoded video @@ -225,6 +258,9 @@ class CommonVideoProcessor(MediaProcessor): if not thumb_size: thumb_size = (mgg.global_config['media:thumb']['max_width'],) + if self._skip_processing('thumb', thumb_size=thumb_size): + return + # We will only use the width so that the correct scale is kept transcoders.VideoThumbnailerMarkII( self.process_filename, @@ -236,6 +272,7 @@ class CommonVideoProcessor(MediaProcessor): store_public(self.entry, 'thumb', tmp_thumb, self.name_builder.fill('{basename}.thumbnail.jpg')) + self.entry.set_file_metadata('thumb', thumb_size=thumb_size) class InitialProcessor(CommonVideoProcessor): """ diff --git a/mediagoblin/oauth/views.py b/mediagoblin/oauth/views.py index 116eb023..14c8ab14 100644 --- a/mediagoblin/oauth/views.py +++ b/mediagoblin/oauth/views.py @@ -18,12 +18,12 @@ import datetime from oauthlib.oauth1 import (RequestTokenEndpoint, AuthorizationEndpoint, AccessTokenEndpoint) - + from mediagoblin.decorators import require_active_login from mediagoblin.tools.translate import pass_to_ugettext from mediagoblin.meddleware.csrf import csrf_exempt from mediagoblin.tools.request import decode_request -from mediagoblin.tools.response import (render_to_response, redirect, +from mediagoblin.tools.response import (render_to_response, redirect, json_response, render_400, form_response) from mediagoblin.tools.crypto import random_string @@ -41,7 +41,7 @@ client_types = ["web", "native"] # currently what pump supports def client_register(request): """ Endpoint for client registration """ try: - data = decode_request(request) + data = decode_request(request) except ValueError: error = "Could not decode data." return json_response({"error": error}, status=400) @@ -56,7 +56,7 @@ def client_register(request): if data.get("application_type", None) not in client_types: error = "Unknown application_type." return json_response({"error": error}, status=400) - + client_type = data["type"] if client_type == "client_update": @@ -69,7 +69,7 @@ def client_register(request): return json_response({"error": error}, status=400) client = Client.query.filter_by( - id=data["client_id"], + id=data["client_id"], secret=data["client_secret"] ).first() @@ -78,7 +78,7 @@ def client_register(request): return json_response({"error": error}, status=403) client.application_name = data.get( - "application_name", + "application_name", client.application_name ) @@ -108,12 +108,12 @@ def client_register(request): client_secret = random_string(43) # again, seems to be what pump uses expirey = 0 # for now, lets not have it expire expirey_db = None if expirey == 0 else expirey - application_type = data["application_type"] - + application_type = data["application_type"] + # save it client = Client( - id=client_id, - secret=client_secret, + id=client_id, + secret=client_secret, expirey=expirey_db, application_type=application_type, ) @@ -126,12 +126,12 @@ def client_register(request): if logo_url is not None and not validate_url(logo_url): error = "Logo URL {0} is not a valid URL.".format(logo_url) return json_response( - {"error": error}, + {"error": error}, status=400 ) else: client.logo_url = logo_url - + client.application_name = data.get("application_name", None) contacts = data.get("contacts", None) @@ -146,8 +146,8 @@ def client_register(request): # not a valid email error = "Email {0} is not a valid email.".format(contact) return json_response({"error": error}, status=400) - - + + client.contacts = contacts redirect_uris = data.get("redirect_uris", None) @@ -166,7 +166,7 @@ def client_register(request): client.redirect_uri = redirect_uris - + client.save() expirey = 0 if client.expirey is None else client.expirey @@ -182,7 +182,7 @@ def client_register(request): def request_token(request): """ Returns request token """ try: - data = decode_request(request) + data = decode_request(request) except ValueError: error = "Could not decode data." return json_response({"error": error}, status=400) @@ -193,7 +193,7 @@ def request_token(request): if not data and request.headers: data = request.headers - + data = dict(data) # mutableifying authorization = decode_authorization_header(data) @@ -226,12 +226,12 @@ def request_token(request): return form_response(tokens) -@require_active_login +@require_active_login def authorize(request): """ Displays a page for user to authorize """ if request.method == "POST": return authorize_finish(request) - + _ = pass_to_ugettext token = request.args.get("oauth_token", None) if token is None: @@ -243,10 +243,10 @@ def authorize(request): if oauth_request is None: err_msg = _("No request token found.") return render_400(request, err_msg) - + if oauth_request.used: return authorize_finish(request) - + if oauth_request.verifier is None: orequest = GMGRequest(request) request_validator = GMGRequestValidator() @@ -279,7 +279,7 @@ def authorize(request): "mediagoblin/api/authorize.html", context ) - + def authorize_finish(request): """ Finishes the authorize """ @@ -288,7 +288,7 @@ def authorize_finish(request): verifier = request.form["oauth_verifier"] oauth_request = RequestToken.query.filter_by(token=token, verifier=verifier) oauth_request = oauth_request.first() - + if oauth_request is None: # invalid token or verifier err_msg = _("No request token found.") @@ -321,10 +321,10 @@ def authorize_finish(request): @csrf_exempt def access_token(request): - """ Provides an access token based on a valid verifier and request token """ + """ Provides an access token based on a valid verifier and request token """ data = request.headers - parsed_tokens = decode_authorization_header(data) + parsed_tokens = decode_authorization_header(data) if parsed_tokens == dict() or "oauth_token" not in parsed_tokens: error = "Missing required parameter." diff --git a/mediagoblin/plugins/basic_auth/__init__.py b/mediagoblin/plugins/basic_auth/__init__.py index 33a554b0..64564c7f 100644 --- a/mediagoblin/plugins/basic_auth/__init__.py +++ b/mediagoblin/plugins/basic_auth/__init__.py @@ -13,17 +13,45 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. +from pkg_resources import resource_filename +import os + from mediagoblin.plugins.basic_auth import forms as auth_forms from mediagoblin.plugins.basic_auth import tools as auth_tools from mediagoblin.auth.tools import create_basic_user from mediagoblin.db.models import User from mediagoblin.tools import pluginapi from sqlalchemy import or_ +from mediagoblin.tools.staticdirect import PluginStatic + + +PLUGIN_DIR = os.path.dirname(__file__) def setup_plugin(): config = pluginapi.get_config('mediagoblin.plugins.basic_auth') + routes = [ + ('mediagoblin.plugins.basic_auth.edit.pass', + '/edit/password/', + 'mediagoblin.plugins.basic_auth.views:change_pass'), + ('mediagoblin.plugins.basic_auth.forgot_password', + '/auth/forgot_password/', + 'mediagoblin.plugins.basic_auth.views:forgot_password'), + ('mediagoblin.plugins.basic_auth.verify_forgot_password', + '/auth/forgot_password/verify/', + 'mediagoblin.plugins.basic_auth.views:verify_forgot_password')] + + pluginapi.register_routes(routes) + pluginapi.register_template_path(os.path.join(PLUGIN_DIR, 'templates')) + + pluginapi.register_template_hooks( + {'edit_link': 'mediagoblin/plugins/basic_auth/edit_link.html', + 'fp_link': 'mediagoblin/plugins/basic_auth/fp_link.html', + 'fp_head': 'mediagoblin/plugins/basic_auth/fp_head.html', + 'create_account': + 'mediagoblin/plugins/basic_auth/create_account_link.html'}) + def get_user(**kwargs): username = kwargs.pop('username', None) @@ -85,4 +113,7 @@ hooks = { 'auth_check_password': check_password, 'auth_fake_login_attempt': auth_tools.fake_login_attempt, 'template_global_context': append_to_global_context, + 'static_setup': lambda: PluginStatic( + 'coreplugin_basic_auth', + resource_filename('mediagoblin.plugins.basic_auth', 'static')) } diff --git a/mediagoblin/plugins/basic_auth/forms.py b/mediagoblin/plugins/basic_auth/forms.py index 6cf01b38..c10496f8 100644 --- a/mediagoblin/plugins/basic_auth/forms.py +++ b/mediagoblin/plugins/basic_auth/forms.py @@ -44,3 +44,33 @@ class LoginForm(wtforms.Form): stay_logged_in = wtforms.BooleanField( label='', description=_('Stay logged in')) + + +class ForgotPassForm(wtforms.Form): + username = wtforms.TextField( + _('Username or email'), + [wtforms.validators.Required(), + normalize_user_or_email_field()]) + + +class ChangeForgotPassForm(wtforms.Form): + password = wtforms.PasswordField( + 'Password', + [wtforms.validators.Required(), + wtforms.validators.Length(min=5, max=1024)]) + token = wtforms.HiddenField( + '', + [wtforms.validators.Required()]) + + +class ChangePassForm(wtforms.Form): + old_password = wtforms.PasswordField( + _('Old password'), + [wtforms.validators.Required()], + description=_( + "Enter your old password to prove you own this account.")) + new_password = wtforms.PasswordField( + _('New password'), + [wtforms.validators.Required(), + wtforms.validators.Length(min=6, max=30)], + id="password") diff --git a/mediagoblin/static/js/autofilledin_password.js b/mediagoblin/plugins/basic_auth/static/js/autofilledin_password.js index 45e867fe..45e867fe 100644 --- a/mediagoblin/static/js/autofilledin_password.js +++ b/mediagoblin/plugins/basic_auth/static/js/autofilledin_password.js diff --git a/mediagoblin/templates/mediagoblin/auth/change_fp.html b/mediagoblin/plugins/basic_auth/templates/mediagoblin/plugins/basic_auth/change_fp.html index a3cf9cb9..47cd591e 100644 --- a/mediagoblin/templates/mediagoblin/auth/change_fp.html +++ b/mediagoblin/plugins/basic_auth/templates/mediagoblin/plugins/basic_auth/change_fp.html @@ -29,7 +29,7 @@ {%- endblock %} {% block mediagoblin_content %} - <form action="{{ request.urlgen('mediagoblin.auth.verify_forgot_password') }}" + <form action="{{ request.urlgen('mediagoblin.plugins.basic_auth.verify_forgot_password') }}" method="POST" enctype="multipart/form-data"> {{ csrf_token }} <div class="form_box"> diff --git a/mediagoblin/templates/mediagoblin/edit/change_pass.html b/mediagoblin/plugins/basic_auth/templates/mediagoblin/plugins/basic_auth/change_pass.html index 2a1ffee0..596a4def 100644 --- a/mediagoblin/templates/mediagoblin/edit/change_pass.html +++ b/mediagoblin/plugins/basic_auth/templates/mediagoblin/plugins/basic_auth/change_pass.html @@ -31,7 +31,7 @@ {%- endblock %} {% block mediagoblin_content %} - <form action="{{ request.urlgen('mediagoblin.edit.pass') }}" + <form action="{{ request.urlgen('mediagoblin.plugins.basic_auth.edit.pass') }}" method="POST" enctype="multipart/form-data"> <div class="form_box edit_box"> <h1> diff --git a/mediagoblin/plugins/basic_auth/templates/mediagoblin/plugins/basic_auth/create_account_link.html b/mediagoblin/plugins/basic_auth/templates/mediagoblin/plugins/basic_auth/create_account_link.html new file mode 100644 index 00000000..7425630b --- /dev/null +++ b/mediagoblin/plugins/basic_auth/templates/mediagoblin/plugins/basic_auth/create_account_link.html @@ -0,0 +1,27 @@ +{# +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +#} + +{% block create_account %} + {% if allow_registration %} + <p> + {% trans %}Don't have an account yet?{% endtrans %} + <a href="{{ request.urlgen('mediagoblin.auth.register') }}"> + {%- trans %}Create one here!{% endtrans %}</a> + </p> + {% endif %} +{% endblock %} diff --git a/mediagoblin/plugins/basic_auth/templates/mediagoblin/plugins/basic_auth/edit_link.html b/mediagoblin/plugins/basic_auth/templates/mediagoblin/plugins/basic_auth/edit_link.html new file mode 100644 index 00000000..9fd09ab1 --- /dev/null +++ b/mediagoblin/plugins/basic_auth/templates/mediagoblin/plugins/basic_auth/edit_link.html @@ -0,0 +1,25 @@ +{# +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +#} + +{% block change_pass_link %} + <p> + <a href="{{ request.urlgen('mediagoblin.plugins.basic_auth.edit.pass') }}"> + {% trans %}Change your password.{% endtrans %} + </a> + </p> +{% endblock %} diff --git a/mediagoblin/templates/mediagoblin/auth/forgot_password.html b/mediagoblin/plugins/basic_auth/templates/mediagoblin/plugins/basic_auth/forgot_password.html index 6cfd2c85..b0028ab6 100644 --- a/mediagoblin/templates/mediagoblin/auth/forgot_password.html +++ b/mediagoblin/plugins/basic_auth/templates/mediagoblin/plugins/basic_auth/forgot_password.html @@ -24,7 +24,7 @@ {%- endblock %} {% block mediagoblin_content %} - <form action="{{ request.urlgen('mediagoblin.auth.forgot_password') }}" + <form action="{{ request.urlgen('mediagoblin.plugins.basic_auth.forgot_password') }}" method="POST" enctype="multipart/form-data"> {{ csrf_token }} <div class="form_box"> diff --git a/mediagoblin/plugins/basic_auth/templates/mediagoblin/plugins/basic_auth/fp_head.html b/mediagoblin/plugins/basic_auth/templates/mediagoblin/plugins/basic_auth/fp_head.html new file mode 100644 index 00000000..292dce28 --- /dev/null +++ b/mediagoblin/plugins/basic_auth/templates/mediagoblin/plugins/basic_auth/fp_head.html @@ -0,0 +1,20 @@ +{# +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 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 Affero General Public License for more details. +# + You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +#} + +<script type="text/javascript" + src="{{ request.staticdirect('/js/autofilledin_password.js', 'coreplugin_basic_auth') }}"></script> diff --git a/mediagoblin/plugins/basic_auth/templates/mediagoblin/plugins/basic_auth/fp_link.html b/mediagoblin/plugins/basic_auth/templates/mediagoblin/plugins/basic_auth/fp_link.html new file mode 100644 index 00000000..404358d8 --- /dev/null +++ b/mediagoblin/plugins/basic_auth/templates/mediagoblin/plugins/basic_auth/fp_link.html @@ -0,0 +1,25 @@ +{# +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +#} + +{% block fp_link %} + <p> + <a href="{{ request.urlgen('mediagoblin.plugins.basic_auth.forgot_password') }}" id="forgot_password"> + {% trans %}Forgot your password?{% endtrans %}</a> + </p> +{% endblock %} + diff --git a/mediagoblin/templates/mediagoblin/auth/fp_verification_email.txt b/mediagoblin/plugins/basic_auth/templates/mediagoblin/plugins/basic_auth/fp_verification_email.txt index fb5e1674..fb5e1674 100644 --- a/mediagoblin/templates/mediagoblin/auth/fp_verification_email.txt +++ b/mediagoblin/plugins/basic_auth/templates/mediagoblin/plugins/basic_auth/fp_verification_email.txt diff --git a/mediagoblin/plugins/basic_auth/tools.py b/mediagoblin/plugins/basic_auth/tools.py index 1300bb9a..f943bf39 100644 --- a/mediagoblin/plugins/basic_auth/tools.py +++ b/mediagoblin/plugins/basic_auth/tools.py @@ -16,6 +16,11 @@ import bcrypt import random +from mediagoblin import mg_globals +from mediagoblin.tools.crypto import get_timed_signer_url +from mediagoblin.tools.mail import send_email +from mediagoblin.tools.template import render_template + def bcrypt_check_password(raw_pass, stored_hash, extra_salt=None): """ @@ -82,3 +87,35 @@ def fake_login_attempt(): randplus_hashed_pass = bcrypt.hashpw(hashed_pass, rand_salt) randplus_stored_hash == randplus_hashed_pass + + +EMAIL_FP_VERIFICATION_TEMPLATE = ( + u"{uri}?" + u"token={fp_verification_key}") + + +def send_fp_verification_email(user, request): + """ + Send the verification email to users to change their password. + + Args: + - user: a user object + - request: the request + """ + fp_verification_key = get_timed_signer_url('mail_verification_token') \ + .dumps(user.id) + + rendered_email = render_template( + request, 'mediagoblin/plugins/basic_auth/fp_verification_email.txt', + {'username': user.username, + 'verification_url': EMAIL_FP_VERIFICATION_TEMPLATE.format( + uri=request.urlgen('mediagoblin.plugins.basic_auth.verify_forgot_password', + qualified=True), + fp_verification_key=fp_verification_key)}) + + # TODO: There is no error handling in place + send_email( + mg_globals.app_config['email_sender_address'], + [user.email], + 'GNU MediaGoblin - Change forgotten password!', + rendered_email) diff --git a/mediagoblin/plugins/basic_auth/views.py b/mediagoblin/plugins/basic_auth/views.py new file mode 100644 index 00000000..47f7ee9a --- /dev/null +++ b/mediagoblin/plugins/basic_auth/views.py @@ -0,0 +1,223 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +from itsdangerous import BadSignature + +from mediagoblin import messages +from mediagoblin.db.models import User +from mediagoblin.decorators import require_active_login +from mediagoblin.plugins.basic_auth import forms, tools +from mediagoblin.tools.crypto import get_timed_signer_url +from mediagoblin.tools.mail import email_debug_message +from mediagoblin.tools.response import redirect, render_to_response, render_404 +from mediagoblin.tools.translate import pass_to_ugettext as _ + + +def forgot_password(request): + """ + Forgot password view + + Sends an email with an url to renew forgotten password. + Use GET querystring parameter 'username' to pre-populate the input field + """ + fp_form = forms.ForgotPassForm(request.form, + username=request.args.get('username')) + + if not (request.method == 'POST' and fp_form.validate()): + # Either GET request, or invalid form submitted. Display the template + return render_to_response(request, + 'mediagoblin/plugins/basic_auth/forgot_password.html', + {'fp_form': fp_form}) + + # If we are here: method == POST and form is valid. username casing + # has been sanitized. Store if a user was found by email. We should + # not reveal if the operation was successful then as we don't want to + # leak if an email address exists in the system. + found_by_email = '@' in fp_form.username.data + + if found_by_email: + user = User.query.filter_by( + email=fp_form.username.data).first() + # Don't reveal success in case the lookup happened by email address. + success_message = _("If that email address (case sensitive!) is " + "registered an email has been sent with " + "instructions on how to change your password.") + + else: # found by username + user = User.query.filter_by( + username=fp_form.username.data).first() + + if user is None: + messages.add_message(request, + messages.WARNING, + _("Couldn't find someone with that username.")) + return redirect(request, 'mediagoblin.auth.forgot_password') + + success_message = _("An email has been sent with instructions " + "on how to change your password.") + + if user and user.has_privilege(u'active') is False: + # Don't send reminder because user is inactive or has no verified email + messages.add_message(request, + messages.WARNING, + _("Could not send password recovery email as your username is in" + "active or your account's email address has not been verified.")) + + return redirect(request, 'mediagoblin.user_pages.user_home', + user=user.username) + + # SUCCESS. Send reminder and return to login page + if user: + email_debug_message(request) + tools.send_fp_verification_email(user, request) + + messages.add_message(request, messages.INFO, success_message) + return redirect(request, 'mediagoblin.auth.login') + + +def verify_forgot_password(request): + """ + Check the forgot-password verification and possibly let the user + change their password because of it. + """ + # get form data variables, and specifically check for presence of token + formdata = _process_for_token(request) + if not formdata['has_token']: + return render_404(request) + + formdata_vars = formdata['vars'] + + # Catch error if token is faked or expired + try: + token = get_timed_signer_url("mail_verification_token") \ + .loads(formdata_vars['token'], max_age=10*24*3600) + except BadSignature: + messages.add_message( + request, + messages.ERROR, + _('The verification key or user id is incorrect.')) + + return redirect( + request, + 'index') + + # check if it's a valid user id + user = User.query.filter_by(id=int(token)).first() + + # no user in db + if not user: + messages.add_message( + request, messages.ERROR, + _('The user id is incorrect.')) + return redirect( + request, 'index') + + # check if user active and has email verified + if user.has_privilege(u'active'): + cp_form = forms.ChangeForgotPassForm(formdata_vars) + + if request.method == 'POST' and cp_form.validate(): + user.pw_hash = tools.bcrypt_gen_password_hash( + cp_form.password.data) + user.save() + + messages.add_message( + request, + messages.INFO, + _("You can now log in using your new password.")) + return redirect(request, 'mediagoblin.auth.login') + else: + return render_to_response( + request, + 'mediagoblin/plugins/basic_auth/change_fp.html', + {'cp_form': cp_form}) + + ## Commenting this out temporarily because I'm checking into + ## what's going on with user.email_verified. + ## + ## ... if this commit lasts long enough for anyone but me (cwebber) to + ## notice it, they should pester me to remove this or remove it + ## themselves ;) + # + # if not user.email_verified: + # messages.add_message( + # request, messages.ERROR, + # _('You need to verify your email before you can reset your' + # ' password.')) + + if not user.status == 'active': + messages.add_message( + request, messages.ERROR, + _('You are no longer an active user. Please contact the system' + ' admin to reactivate your accoutn.')) + + return redirect( + request, 'index') + + +def _process_for_token(request): + """ + Checks for tokens in formdata without prior knowledge of request method + + For now, returns whether the userid and token formdata variables exist, and + the formdata variables in a hash. Perhaps an object is warranted? + """ + # retrieve the formdata variables + if request.method == 'GET': + formdata_vars = request.GET + else: + formdata_vars = request.form + + formdata = { + 'vars': formdata_vars, + 'has_token': 'token' in formdata_vars} + + return formdata + + +@require_active_login +def change_pass(request): + form = forms.ChangePassForm(request.form) + user = request.user + + if request.method == 'POST' and form.validate(): + + if not tools.bcrypt_check_password( + form.old_password.data, user.pw_hash): + form.old_password.errors.append( + _('Wrong password')) + + return render_to_response( + request, + 'mediagoblin/plugins/basic_auth/change_pass.html', + {'form': form, + 'user': user}) + + # Password matches + user.pw_hash = tools.bcrypt_gen_password_hash( + form.new_password.data) + user.save() + + messages.add_message( + request, messages.SUCCESS, + _('Your password was changed successfully')) + + return redirect(request, 'mediagoblin.edit.account') + + return render_to_response( + request, + 'mediagoblin/plugins/basic_auth/change_pass.html', + {'form': form, + 'user': user}) diff --git a/mediagoblin/plugins/ldap/README.rst b/mediagoblin/plugins/ldap/README.rst new file mode 100644 index 00000000..ae03b31e --- /dev/null +++ b/mediagoblin/plugins/ldap/README.rst @@ -0,0 +1,49 @@ +============= + ldap plugin +============= + +.. Warning:: + This plugin is not compatible with the other authentication plugins. + +This plugin allow your GNU Mediagoblin instance to authenticate against an +LDAP server. + +Set up the ldap plugin +====================== + +1. Install the ``python-ldap`` package. + +2. Add the following to your MediaGoblin .ini file in the ``[plugins]`` section:: + + [[mediagoblin.plugins.ldap]] + +Configuring the ldap plugin +=========================== + +This plugin allows you to use multiple ldap servers for authentication. + +In order to configure a server, add the following to you MediaGoblin .ini file +under the ldap plugin:: + + [[mediagoblin.plugins.ldap]] + [[[server1]]] + LDAP_SERVER_URI = 'ldap://ldap.testathon.net:389' + LDAP_USER_DN_TEMPLATE = 'cn={username},ou=users,dc=testathon,dc=net' + [[[server2]]] + ... + +Make any necessary changes to the above to work with your sever. Make sure +``{username}`` is where the username should be in LDAP_USER_DN_TEMPLATE. + +If you would like to fetch the users email from the ldap server upon account +registration, add ``LDAP_SEARCH_BASE = 'ou=users,dc=testathon,dc=net'`` and +``EMAIL_SEARCH_FIELD = 'mail'`` under you server configuration in your +MediaGoblin .ini file. + +.. Warning:: + By default, this plugin provides no encryption when communicating with the + ldap servers. If you would like to use an SSL connection, change + LDAP_SERVER_URI to use ``ldaps://`` and whichever port you use. Default ldap + port for SSL connections is 636. If you would like to use a TLS connection, + add ``LDAP_START_TLS = 'true'`` under your server configuration in your + MediaGoblin .ini file. diff --git a/mediagoblin/plugins/ldap/__init__.py b/mediagoblin/plugins/ldap/__init__.py new file mode 100644 index 00000000..4673acee --- /dev/null +++ b/mediagoblin/plugins/ldap/__init__.py @@ -0,0 +1,59 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +import os + +from mediagoblin.auth.tools import create_basic_user +from mediagoblin.tools import pluginapi + +PLUGIN_DIR = os.path.dirname(__file__) + + +def setup_plugin(): + config = pluginapi.get_config('mediagoblin.plugins.ldap') + + routes = [ + ('mediagoblin.plugins.ldap.register', + '/auth/ldap/register/', + 'mediagoblin.plugins.ldap.views:register'), + ('mediagoblin.plugins.ldap.login', + '/auth/ldap/login/', + 'mediagoblin.plugins.ldap.views:login')] + + pluginapi.register_routes(routes) + pluginapi.register_template_path(os.path.join(PLUGIN_DIR, 'templates')) + + pluginapi.register_template_hooks( + {'create_account': 'mediagoblin/plugins/ldap/create_account_link.html'}) + + +def create_user(register_form): + if 'username' in register_form and 'password' not in register_form: + return create_basic_user(register_form) + + +def no_pass_redirect(): + return 'ldap' + + +def auth(): + return True + +hooks = { + 'setup': setup_plugin, + 'authentication': auth, + 'auth_no_pass_redirect': no_pass_redirect, + 'auth_create_user': create_user, +} diff --git a/mediagoblin/auth/forms.py b/mediagoblin/plugins/ldap/forms.py index 865502e9..7ec1479e 100644 --- a/mediagoblin/auth/forms.py +++ b/mediagoblin/plugins/ldap/forms.py @@ -13,25 +13,28 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. - import wtforms from mediagoblin.tools.translate import lazy_pass_to_ugettext as _ from mediagoblin.auth.tools import normalize_user_or_email_field -class ForgotPassForm(wtforms.Form): - username = wtforms.TextField( - _('Username or email'), +class RegisterForm(wtforms.Form): + username = wtforms.HiddenField( + '', [wtforms.validators.Required(), - normalize_user_or_email_field()]) + normalize_user_or_email_field(allow_email=False)]) + email = wtforms.TextField( + _('Email address'), + [wtforms.validators.Required(), + normalize_user_or_email_field(allow_user=False)]) -class ChangePassForm(wtforms.Form): - password = wtforms.PasswordField( - 'Password', +class LoginForm(wtforms.Form): + username = wtforms.TextField( + _('Username'), [wtforms.validators.Required(), - wtforms.validators.Length(min=5, max=1024)]) - token = wtforms.HiddenField( - '', + normalize_user_or_email_field()]) + password = wtforms.PasswordField( + _('Password'), [wtforms.validators.Required()]) diff --git a/mediagoblin/plugins/ldap/templates/mediagoblin/plugins/ldap/create_account_link.html b/mediagoblin/plugins/ldap/templates/mediagoblin/plugins/ldap/create_account_link.html new file mode 100644 index 00000000..947e4ae2 --- /dev/null +++ b/mediagoblin/plugins/ldap/templates/mediagoblin/plugins/ldap/create_account_link.html @@ -0,0 +1,25 @@ +{# +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +#} + +{% block create_account %} + {% if allow_registration %} + <p> + {% trans %}Sign in to create an account!{% endtrans %} + </p> + {% endif %} +{% endblock %} diff --git a/mediagoblin/plugins/ldap/tools.py b/mediagoblin/plugins/ldap/tools.py new file mode 100644 index 00000000..1c436792 --- /dev/null +++ b/mediagoblin/plugins/ldap/tools.py @@ -0,0 +1,65 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +import ldap +import logging + +from mediagoblin.tools import pluginapi + +_log = logging.getLogger(__name__) + + +class LDAP(object): + def __init__(self): + self.ldap_settings = pluginapi.get_config('mediagoblin.plugins.ldap') + + def _connect(self, server): + _log.info('Connecting to {0}.'.format(server['LDAP_SERVER_URI'])) + self.conn = ldap.initialize(server['LDAP_SERVER_URI']) + + if server['LDAP_START_TLS'] == 'true': + _log.info('Initiating TLS') + self.conn.start_tls_s() + + def _get_email(self, server, username): + try: + results = self.conn.search_s(server['LDAP_SEARCH_BASE'], + ldap.SCOPE_SUBTREE, 'uid={0}' + .format(username), + [server['EMAIL_SEARCH_FIELD']]) + + email = results[0][1][server['EMAIL_SEARCH_FIELD']][0] + except KeyError: + email = None + + return email + + def login(self, username, password): + for k, v in self.ldap_settings.iteritems(): + try: + self._connect(v) + user_dn = v['LDAP_USER_DN_TEMPLATE'].format(username=username) + self.conn.simple_bind_s(user_dn, password.encode('utf8')) + email = self._get_email(v, username) + return username, email + + except ldap.LDAPError, e: + _log.info(e) + + finally: + _log.info('Unbinding {0}.'.format(v['LDAP_SERVER_URI'])) + self.conn.unbind() + + return False, None diff --git a/mediagoblin/plugins/ldap/views.py b/mediagoblin/plugins/ldap/views.py new file mode 100644 index 00000000..aef1bf56 --- /dev/null +++ b/mediagoblin/plugins/ldap/views.py @@ -0,0 +1,104 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +from mediagoblin import mg_globals, messages +from mediagoblin.auth.tools import register_user +from mediagoblin.db.models import User +from mediagoblin.decorators import allow_registration, auth_enabled +from mediagoblin.plugins.ldap import forms +from mediagoblin.plugins.ldap.tools import LDAP +from mediagoblin.tools.translate import pass_to_ugettext as _ +from mediagoblin.tools.response import redirect, render_to_response + + +@auth_enabled +def login(request): + login_form = forms.LoginForm(request.form) + + login_failed = False + + if request.method == 'POST' and login_form.validate(): + l = LDAP() + username, email = l.login(login_form.username.data, + login_form.password.data) + + if username: + user = User.query.filter_by( + username=username).first() + + if user: + # set up login in session + request.session['user_id'] = unicode(user.id) + request.session.save() + + if request.form.get('next'): + return redirect(request, location=request.form['next']) + else: + return redirect(request, "index") + else: + if not mg_globals.app.auth: + messages.add_message( + request, + messages.WARNING, + _('Sorry, authentication is disabled on this ' + 'instance.')) + return redirect(request, 'index') + + register_form = forms.RegisterForm(username=username, + email=email) + + return render_to_response( + request, + 'mediagoblin/auth/register.html', + {'register_form': register_form, + 'post_url': request.urlgen('mediagoblin.plugins.ldap.register')}) + + login_failed = True + + return render_to_response( + request, + 'mediagoblin/auth/login.html', + {'login_form': login_form, + 'next': request.GET.get('next') or request.form.get('next'), + 'login_failed': login_failed, + 'post_url': request.urlgen('mediagoblin.plugins.ldap.login'), + 'allow_registration': mg_globals.app_config["allow_registration"]}) + + +@allow_registration +@auth_enabled +def register(request): + if request.method == 'GET': + return redirect( + request, + 'mediagoblin.plugins.ldap.login') + + register_form = forms.RegisterForm(request.form) + + if register_form.validate(): + user = register_user(request, register_form) + + if user: + # redirect the user to their homepage... there will be a + # message waiting for them to verify their email + return redirect( + request, 'mediagoblin.user_pages.user_home', + user=user.username) + + return render_to_response( + request, + 'mediagoblin/auth/register.html', + {'register_form': register_form, + 'post_url': request.urlgen('mediagoblin.plugins.ldap.register')}) diff --git a/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/login.html b/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/login.html index 8d74c2b9..193a3b2d 100644 --- a/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/login.html +++ b/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/login.html @@ -20,8 +20,8 @@ {% import "/mediagoblin/utils/wtforms.html" as wtforms_util %} {% block mediagoblin_head %} - <script type="text/javascript" - src="{{ request.staticdirect('/js/autofilledin_password.js') }}"></script> + {{ super() }} + {% template_hook("fp_head") %} {% endblock %} {% block title -%} diff --git a/mediagoblin/plugins/openid/views.py b/mediagoblin/plugins/openid/views.py index b639a4cb..bb2de7ab 100644 --- a/mediagoblin/plugins/openid/views.py +++ b/mediagoblin/plugins/openid/views.py @@ -195,11 +195,11 @@ def finish_login(request): return redirect(request, "index") else: # No user, need to register - if not mg_globals.app.auth: + if not mg_globals.app_config['allow_registration']: messages.add_message( request, messages.WARNING, - _('Sorry, authentication is disabled on this instance.')) + _('Sorry, registration is disabled on this instance.')) return redirect(request, 'index') # Get email and nickname from response diff --git a/mediagoblin/static/images/frontpage_image.png b/mediagoblin/static/images/frontpage_image.png Binary files differdeleted file mode 100644 index 689eb2c2..00000000 --- a/mediagoblin/static/images/frontpage_image.png +++ /dev/null diff --git a/mediagoblin/static/images/home_goblin.xcf b/mediagoblin/static/images/home_goblin.xcf Binary files differnew file mode 100644 index 00000000..e75d6b5f --- /dev/null +++ b/mediagoblin/static/images/home_goblin.xcf diff --git a/mediagoblin/static/js/file_size.js b/mediagoblin/static/js/file_size.js new file mode 100644 index 00000000..2238ef89 --- /dev/null +++ b/mediagoblin/static/js/file_size.js @@ -0,0 +1,45 @@ +/** + * GNU MediaGoblin -- federated, autonomous media hosting + * Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +$(document).ready(function(){ + var file = document.getElementById('file'); + var uploaded = parseInt(document.getElementById('uploaded').value); + var upload_limit = parseInt(document.getElementById('upload_limit').value); + var max_file_size = parseInt(document.getElementById('max_file_size').value); + + file.onchange = function() { + var file_size = file.files[0].size / (1024.0 * 1024); + + if (file_size >= max_file_size) { + $('#file').after('<p id="file_size_error" class="form_field_error">Sorry, the file size is too big.</p>'); + } + else if (document.getElementById('file_size_error')) { + $('#file_size_error').hide(); + } + + if (upload_limit) { + if ( uploaded + file_size >= upload_limit) { + $('#file').after('<p id="upload_limit_error" class="form_field_error">Sorry, uploading this file will put you over your upload limit.</p>'); + } + else if (document.getElementById('upload_limit_error')) { + $('#upload_limit_error').hide(); + console.log(file_size >= max_file_size); + } + } + }; +}); diff --git a/mediagoblin/storage/__init__.py b/mediagoblin/storage/__init__.py index bbe134a7..51b46c07 100644 --- a/mediagoblin/storage/__init__.py +++ b/mediagoblin/storage/__init__.py @@ -191,6 +191,13 @@ class StorageInterface(object): # Copy to storage system in 4M chunks shutil.copyfileobj(source_file, dest_file, length=4*1048576) + def get_file_size(self, filepath): + """ + Return the size of the file in bytes. + """ + # Subclasses should override this method. + self.__raise_not_implemented() + ########### # Utilities diff --git a/mediagoblin/storage/cloudfiles.py b/mediagoblin/storage/cloudfiles.py index 250f06d4..47c81ad6 100644 --- a/mediagoblin/storage/cloudfiles.py +++ b/mediagoblin/storage/cloudfiles.py @@ -168,6 +168,12 @@ class CloudFilesStorage(StorageInterface): # Copy to storage system in 4096 byte chunks dest_file.send(source_file) + def get_file_size(self, filepath): + """Returns the file size in bytes""" + obj = self.container.get_object( + self._resolve_filepath(filepath)) + return obj.total_bytes + class CloudFilesStorageObjectWrapper(): """ Wrapper for python-cloudfiles's cloudfiles.storage_object.Object diff --git a/mediagoblin/storage/filestorage.py b/mediagoblin/storage/filestorage.py index 3d6e0753..29b8383b 100644 --- a/mediagoblin/storage/filestorage.py +++ b/mediagoblin/storage/filestorage.py @@ -111,3 +111,6 @@ class BasicFileStorage(StorageInterface): os.makedirs(directory) # This uses chunked copying of 16kb buffers (Py2.7): shutil.copy(filename, self.get_local_path(filepath)) + + def get_file_size(self, filepath): + return os.stat(self._resolve_filepath(filepath)).st_size diff --git a/mediagoblin/submit/forms.py b/mediagoblin/submit/forms.py index e9bd93fd..e2264645 100644 --- a/mediagoblin/submit/forms.py +++ b/mediagoblin/submit/forms.py @@ -17,30 +17,44 @@ import wtforms +from mediagoblin import mg_globals from mediagoblin.tools.text import tag_length_validator from mediagoblin.tools.translate import lazy_pass_to_ugettext as _ from mediagoblin.tools.licenses import licenses_as_choices -class SubmitStartForm(wtforms.Form): - file = wtforms.FileField(_('File')) - title = wtforms.TextField( - _('Title'), - [wtforms.validators.Length(min=0, max=500)]) - description = wtforms.TextAreaField( - _('Description of this work'), - description=_("""You can use - <a href="http://daringfireball.net/projects/markdown/basics"> - Markdown</a> for formatting.""")) - tags = wtforms.TextField( - _('Tags'), - [tag_length_validator], - description=_( - "Separate tags by commas.")) - license = wtforms.SelectField( - _('License'), - [wtforms.validators.Optional(),], - choices=licenses_as_choices()) +def get_submit_start_form(form, **kwargs): + max_file_size = kwargs.get('max_file_size') + desc = None + if max_file_size: + desc = _('Max file size: {0} mb'.format(max_file_size)) + + class SubmitStartForm(wtforms.Form): + file = wtforms.FileField( + _('File'), + description=desc) + title = wtforms.TextField( + _('Title'), + [wtforms.validators.Length(min=0, max=500)]) + description = wtforms.TextAreaField( + _('Description of this work'), + description=_("""You can use + <a href="http://daringfireball.net/projects/markdown/basics"> + Markdown</a> for formatting.""")) + tags = wtforms.TextField( + _('Tags'), + [tag_length_validator], + description=_( + "Separate tags by commas.")) + license = wtforms.SelectField( + _('License'), + [wtforms.validators.Optional(),], + choices=licenses_as_choices()) + max_file_size = wtforms.HiddenField('') + upload_limit = wtforms.HiddenField('') + uploaded = wtforms.HiddenField('') + + return SubmitStartForm(form, **kwargs) class AddCollectionForm(wtforms.Form): title = wtforms.TextField( diff --git a/mediagoblin/submit/views.py b/mediagoblin/submit/views.py index 8640b8de..e0e2f1a5 100644 --- a/mediagoblin/submit/views.py +++ b/mediagoblin/submit/views.py @@ -44,8 +44,28 @@ def submit_start(request): """ First view for submitting a file. """ - submit_form = submit_forms.SubmitStartForm(request.form, - license=request.user.license_preference) + user = request.user + if user.upload_limit >= 0: + upload_limit = user.upload_limit + else: + upload_limit = mg_globals.app_config.get('upload_limit', None) + + if upload_limit and user.uploaded >= upload_limit: + messages.add_message( + request, + messages.WARNING, + _('Sorry, you have reached your upload limit.')) + return redirect(request, "mediagoblin.user_pages.user_home", + user=request.user.username) + + max_file_size = mg_globals.app_config.get('max_file_size', None) + + submit_form = submit_forms.get_submit_start_form( + request.form, + license=request.user.license_preference, + max_file_size=max_file_size, + upload_limit=upload_limit, + uploaded=user.uploaded) if request.method == 'POST' and submit_form.validate(): if not check_file_field(request, 'file'): @@ -87,24 +107,49 @@ def submit_start(request): with queue_file: queue_file.write(request.files['file'].stream.read()) - # Save now so we have this data before kicking off processing - entry.save() + # Get file size and round to 2 decimal places + file_size = request.app.queue_store.get_file_size( + entry.queued_media_file) / (1024.0 * 1024) + file_size = float('{0:.2f}'.format(file_size)) + + error = False + + # Check if file size is over the limit + if max_file_size and file_size >= max_file_size: + submit_form.file.errors.append( + _(u'Sorry, the file size is too big.')) + error = True + + # Check if user is over upload limit + if upload_limit and (user.uploaded + file_size) >= upload_limit: + submit_form.file.errors.append( + _('Sorry, uploading this file will put you over your' + ' upload limit.')) + error = True + + if not error: + user.uploaded = user.uploaded + file_size + user.save() + + entry.file_size = file_size - # Pass off to async processing - # - # (... don't change entry after this point to avoid race - # conditions with changes to the document via processing code) - feed_url = request.urlgen( - 'mediagoblin.user_pages.atom_feed', - qualified=True, user=request.user.username) - run_process_media(entry, feed_url) + # Save now so we have this data before kicking off processing + entry.save() - add_message(request, SUCCESS, _('Woohoo! Submitted!')) + # Pass off to processing + # + # (... don't change entry after this point to avoid race + # conditions with changes to the document via processing code) + feed_url = request.urlgen( + 'mediagoblin.user_pages.atom_feed', + qualified=True, user=request.user.username) + run_process_media(entry, feed_url) + add_message(request, SUCCESS, _('Woohoo! Submitted!')) - add_comment_subscription(request.user, entry) + add_comment_subscription(request.user, entry) - return redirect(request, "mediagoblin.user_pages.user_home", - user=request.user.username) + return redirect(request, "mediagoblin.user_pages.user_home", + user=user.username) except Exception as e: ''' This section is intended to catch exceptions raised in diff --git a/mediagoblin/templates/mediagoblin/auth/login.html b/mediagoblin/templates/mediagoblin/auth/login.html index 3329b5d0..93cd82d9 100644 --- a/mediagoblin/templates/mediagoblin/auth/login.html +++ b/mediagoblin/templates/mediagoblin/auth/login.html @@ -20,8 +20,8 @@ {% import "/mediagoblin/utils/wtforms.html" as wtforms_util %} {% block mediagoblin_head %} - <script type="text/javascript" - src="{{ request.staticdirect('/js/autofilledin_password.js') }}"></script> + {{ super() }} + {% template_hook("fp_head") %} {% endblock %} {% block title -%} @@ -39,21 +39,10 @@ {% trans %}Logging in failed!{% endtrans %} </div> {% endif %} - {% if allow_registration %} - <p> - {% trans %}Don't have an account yet?{% endtrans %} - <a href="{{ request.urlgen('mediagoblin.auth.register') }}"> - {%- trans %}Create one here!{% endtrans %}</a> - </p> - {% endif %} + {% template_hook("create_account") %} {% template_hook("login_link") %} {{ wtforms_util.render_divs(login_form, True) }} - {% if pass_auth %} - <p> - <a href="{{ request.urlgen('mediagoblin.auth.forgot_password') }}" id="forgot_password"> - {% trans %}Forgot your password?{% endtrans %}</a> - </p> - {% endif %} + {% template_hook("fp_link") %} <div class="form_submit_buttons"> <input type="submit" value="{% trans %}Log in{% endtrans %}" class="button_form"/> </div> diff --git a/mediagoblin/templates/mediagoblin/edit/edit_account.html b/mediagoblin/templates/mediagoblin/edit/edit_account.html index 14011daa..574fe459 100644 --- a/mediagoblin/templates/mediagoblin/edit/edit_account.html +++ b/mediagoblin/templates/mediagoblin/edit/edit_account.html @@ -41,6 +41,7 @@ Changing {{ username }}'s account settings {%- endtrans -%} </h1> + {% template_hook("edit_link") %} {{ wtforms_util.render_divs(form, True) }} <div class="form_submit_buttons"> <input type="submit" value="{% trans %}Save changes{% endtrans %}" class="button_form" /> @@ -59,7 +60,7 @@ </a> {% if pass_auth is defined %} · - <a href="{{ request.urlgen('mediagoblin.edit.pass') }}"> + <a href="{{ request.urlgen('mediagoblin.plugins.basic_auth.edit.pass') }}"> {% trans %}Password{% endtrans %} </a> {% endif %} diff --git a/mediagoblin/templates/mediagoblin/media_displays/video.html b/mediagoblin/templates/mediagoblin/media_displays/video.html index e35169bf..9386ffcf 100644 --- a/mediagoblin/templates/mediagoblin/media_displays/video.html +++ b/mediagoblin/templates/mediagoblin/media_displays/video.html @@ -21,9 +21,28 @@ {% block mediagoblin_head -%} {{ super() }} <script type="text/javascript" src="{{ - request.staticdirect('/extlib/video-js/video.min.js') }}"></script> - <link href="{{ request.staticdirect('/css/vjs-mg-skin.css') }}" + request.staticdirect('/extlib/video-js/video.js') }}"></script> + {# Sadly commented out till we can get the mediagoblin skin ported over + # to the newest video.js release ;\ #} + {# + <link href="{{ request.staticdirect('/css/vjs-mg-skin.css') }}" + rel="stylesheet"> + #} + <link href="{{ + request.staticdirect('/extlib/video-js/video-js.css') }}" rel="stylesheet"> + + <style type="text/css"> + .vjs-default-skin .vjs-big-play-button + { + top: 50%; + left: 50%; + margin: -1.5em auto auto -2em; + } + .vjs-play-progress, .vjs-volume-level { + background-color: #86D4B1 !important; + } + </style> {%- endblock %} {% block mediagoblin_media %} @@ -31,7 +50,7 @@ <video controls {% if global_config['plugins']['mediagoblin.media_types.video']['auto_play'] %}autoplay{% endif %} - preload="auto" class="video-js vjs-mg-skin" + preload="auto" class="video-js vjs-default-skin" data-setup='{"height": {{ media.media_data.height }}, "width": {{ media.media_data.width }} }'> <source src="{{ request.app.public_store.file_url(display_path) }}" diff --git a/mediagoblin/templates/mediagoblin/submit/start.html b/mediagoblin/templates/mediagoblin/submit/start.html index aa390f56..d335d742 100644 --- a/mediagoblin/templates/mediagoblin/submit/start.html +++ b/mediagoblin/templates/mediagoblin/submit/start.html @@ -19,6 +19,11 @@ {% import "/mediagoblin/utils/wtforms.html" as wtforms_util %} +{% block mediagoblin_head %} + <script type="text/javascript" + src="{{ request.staticdirect('/js/file_size.js') }}"></script> +{% endblock %} + {% block title -%} {% trans %}Add your media{% endtrans %} — {{ super() }} {%- endblock %} diff --git a/mediagoblin/templates/mediagoblin/user_pages/user.html b/mediagoblin/templates/mediagoblin/user_pages/user.html index ab616ea8..37983400 100644 --- a/mediagoblin/templates/mediagoblin/user_pages/user.html +++ b/mediagoblin/templates/mediagoblin/user_pages/user.html @@ -14,6 +14,9 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +# This is the main user homepage for active users #} {% extends "mediagoblin/base.html" %} @@ -38,134 +41,86 @@ {% block mediagoblin_content -%} - {# If no user... #} - {% if not user %} - <p>{% trans %}Sorry, no such user found.{% endtrans %}</p> - - {# User exists, but needs verification #} - {% elif not user.has_privilege('active') %} - {% if user == request.user %} - {# this should only be visible when you are this user #} - <div class="form_box"> - <h1>{% trans %}Email verification needed{% endtrans %}</h1> + <h1> + {%- trans username=user.username %}{{ username }}'s profile{% endtrans -%} + </h1> + {% if not user.url and not user.bio %} + {% if request.user and (request.user.id == user.id) %} + <div class="profile_sidebar empty_space"> <p> - {% trans -%} - Almost done! Your account still needs to be activated. - {%- endtrans %} - </p> - <p> - {% trans -%} - An email should arrive in a few moments with instructions on how to do so. - {%- endtrans %} + {% trans %}Here's a spot to tell others about yourself.{% endtrans %} </p> - <p>{% trans %}In case it doesn't:{% endtrans %}</p> - - <a href="{{ request.urlgen('mediagoblin.auth.resend_verification') }}" - class="button_action_highlight">{% trans %}Resend verification email{% endtrans %}</a> - </div> + <a href="{{ request.urlgen('mediagoblin.edit.profile', + user=user.username) }}" class="button_action"> + {%- trans %}Edit profile{% endtrans -%} + </a> {% else %} - {# if the user is not you, but still needs to verify their email #} - <div class="form_box"> - <h1>{% trans %}Email verification needed{% endtrans %}</h1> - + <div class="profile_sidebar empty_space"> <p> {% trans -%} - Someone has registered an account with this username, but it still has to be activated. - {%- endtrans %} - </p> - - <p> - {% trans login_url=request.urlgen('mediagoblin.auth.login') -%} - If you are that person but you've lost your verification email, you can <a href="{{ login_url }}">log in</a> and resend it. + This user hasn't filled in their profile (yet). {%- endtrans %} </p> - </div> {% endif %} - - {# Active(?) (or at least verified at some point) user, horray! #} {% else %} - <h1> - {%- trans username=user.username %}{{ username }}'s profile{% endtrans -%} - </h1> - - {% if not user.url and not user.bio %} - {% if request.user and (request.user.id == user.id) %} - <div class="profile_sidebar empty_space"> - <p> - {% trans %}Here's a spot to tell others about yourself.{% endtrans %} - </p> - <a href="{{ request.urlgen('mediagoblin.edit.profile', - user=user.username) }}" class="button_action"> - {%- trans %}Edit profile{% endtrans -%} - </a> - {% else %} - <div class="profile_sidebar empty_space"> - <p> - {% trans -%} - This user hasn't filled in their profile (yet). - {%- endtrans %} - </p> + <div class="profile_sidebar"> + {% include "mediagoblin/utils/profile.html" %} + {% if request.user and + (request.user.id == user.id or request.user.has_privilege('admin')) %} + <a href="{{ request.urlgen('mediagoblin.edit.profile', + user=user.username) }}"> + {%- trans %}Edit profile{% endtrans -%} + </a> {% endif %} - {% else %} - <div class="profile_sidebar"> - {% include "mediagoblin/utils/profile.html" %} - {% if request.user and - (request.user.id == user.id or request.user.has_privilege('admin')) %} - <a href="{{ request.urlgen('mediagoblin.edit.profile', - user=user.username) }}"> - {%- trans %}Edit profile{% endtrans -%} - </a> - {% endif %} - {% endif %} + {% endif %} + <p> + <a href="{{ request.urlgen('mediagoblin.user_pages.collection_list', + user=user.username) }}"> + {%- trans %}Browse collections{% endtrans -%} + </a> + </p> + </div> + + {% if media_entries.count() %} + <div class="profile_showcase"> + {{ object_gallery(request, media_entries, pagination, + pagination_base_url=user_gallery_url, col_number=3) }} + {% include "mediagoblin/utils/object_gallery.html" %} + <div class="clear"></div> <p> - <a href="{{ request.urlgen('mediagoblin.user_pages.collection_list', - user=user.username) }}"> - {%- trans %}Browse collections{% endtrans -%} + <a href="{{ user_gallery_url }}"> + {% trans username=user.username -%} + View all of {{ username }}'s media{% endtrans -%} </a> </p> + {% set feed_url = request.urlgen( + 'mediagoblin.user_pages.atom_feed', + user=user.username) %} + {% include "mediagoblin/utils/feed_link.html" %} </div> - - {% if media_entries.count() %} - <div class="profile_showcase"> - {{ object_gallery(request, media_entries, pagination, - pagination_base_url=user_gallery_url, col_number=3) }} - {% include "mediagoblin/utils/object_gallery.html" %} - <div class="clear"></div> + {% else %} + {% if request.user and (request.user.id == user.id) %} + <div class="profile_showcase empty_space"> <p> - <a href="{{ user_gallery_url }}"> - {% trans username=user.username -%} - View all of {{ username }}'s media{% endtrans -%} - </a> + {% trans -%} + This is where your media will appear, but you don't seem to have added anything yet. + {%- endtrans %} </p> - {% set feed_url = request.urlgen( - 'mediagoblin.user_pages.atom_feed', - user=user.username) %} - {% include "mediagoblin/utils/feed_link.html" %} + <a class="button_action" + href="{{ request.urlgen('mediagoblin.submit.start') }}"> + {%- trans %}Add media{% endtrans -%} + </a> </div> {% else %} - {% if request.user and (request.user.id == user.id) %} - <div class="profile_showcase empty_space"> - <p> - {% trans -%} - This is where your media will appear, but you don't seem to have added anything yet. - {%- endtrans %} - </p> - <a class="button_action" - href="{{ request.urlgen('mediagoblin.submit.start') }}"> - {%- trans %}Add media{% endtrans -%} - </a> - </div> - {% else %} - <div class="profile_showcase empty_space"> - <p> - {% trans -%} - There doesn't seem to be any media here yet... - {%- endtrans %} - </p> - </div> - {% endif %} + <div class="profile_showcase empty_space"> + <p> + {% trans -%} + There doesn't seem to be any media here yet... + {%- endtrans %} + </p> + </div> {% endif %} - <div class="clear"></div> {% endif %} + <div class="clear"></div> {% endblock %} diff --git a/mediagoblin/templates/mediagoblin/user_pages/user_nonactive.html b/mediagoblin/templates/mediagoblin/user_pages/user_nonactive.html new file mode 100644 index 00000000..d924198b --- /dev/null +++ b/mediagoblin/templates/mediagoblin/user_pages/user_nonactive.html @@ -0,0 +1,83 @@ +{# +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +# This is the main user homepage for non-active users that still need +# registration etc. +#} +{% extends "mediagoblin/base.html" %} + +{% block title %} + {%- if user -%} + {%- trans username=user.username -%} + {{ username }}'s profile + {%- endtrans %} — {{ super() }} + {%- else -%} + {{ super() }} + {%- endif -%} +{% endblock %} + + +{% block mediagoblin_content -%} + {# User exists, but needs verification #} + {% if not user.has_privilege('active') %} + {% if user == request.user %} + {# this should only be visible when you are this user #} + <div class="form_box"> + <h1>{% trans %}Email verification needed{% endtrans %}</h1> + + <p> + {% trans -%} + Almost done! Your account still needs to be activated. + {%- endtrans %} + </p> + <p> + {% trans -%} + An email should arrive in a few moments with instructions on how to do so. + {%- endtrans %} + </p> + <p>{% trans %}In case it doesn't:{% endtrans %}</p> + + <a href="{{ request.urlgen('mediagoblin.auth.resend_verification') }}" + class="button_action_highlight">{% trans %}Resend verification email{% endtrans %}</a> + </div> + {% else %} + {# if the user is not you, but still needs to verify their email #} + <div class="form_box"> + <h1>{% trans %}Email verification needed{% endtrans %}</h1> + + <p> + {% trans -%} + Someone has registered an account with this username, but it still has to be activated. + {%- endtrans %} + </p> + <p> + {% trans login_url=request.urlgen('mediagoblin.auth.login') -%} + If you are that person but you've lost your verification email, you can <a href="{{ login_url }}">log in</a> and resend it. + {%- endtrans %} + </p> + </div> + {% endif %} + + {# Active(?) (or at least verified at some point) user, horray! #} + {% else %} + <h1> + {%- trans username=user.username %}{{ username }}{% endtrans -%} + </h1> + <p>{{ username }} is not active.</p> + <div class="clear"></div> + {% endif %} +{% endblock %} diff --git a/mediagoblin/tests/auth_configs/authentication_disabled_appconfig.ini b/mediagoblin/tests/auth_configs/authentication_disabled_appconfig.ini index a64e9e40..07c69442 100644 --- a/mediagoblin/tests/auth_configs/authentication_disabled_appconfig.ini +++ b/mediagoblin/tests/auth_configs/authentication_disabled_appconfig.ini @@ -3,8 +3,8 @@ direct_remote_path = /test_static/ email_sender_address = "notice@mediagoblin.example.org" email_debug_mode = true -# TODO: Switch to using an in-memory database -sql_engine = "sqlite:///%(here)s/user_dev/mediagoblin.db" +sql_engine = "sqlite://" +run_migrations = true # Celery shouldn't be set up by the application as it's setup via # mediagoblin.init.celery.from_celery diff --git a/mediagoblin/tests/auth_configs/ldap_appconfig.ini b/mediagoblin/tests/auth_configs/ldap_appconfig.ini new file mode 100644 index 00000000..9be37e17 --- /dev/null +++ b/mediagoblin/tests/auth_configs/ldap_appconfig.ini @@ -0,0 +1,41 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +[mediagoblin] +direct_remote_path = /test_static/ +email_sender_address = "notice@mediagoblin.example.org" +email_debug_mode = true + +sql_engine = "sqlite://" +run_migrations = true + +# Celery shouldn't be set up by the application as it's setup via +# mediagoblin.init.celery.from_celery +celery_setup_elsewhere = true + +[storage:publicstore] +base_dir = %(here)s/user_dev/media/public +base_url = /mgoblin_media/ + +[storage:queuestore] +base_dir = %(here)s/user_dev/media/queue + +[celery] +CELERY_ALWAYS_EAGER = true +CELERY_RESULT_DBURI = "sqlite:///%(here)s/user_dev/celery.db" +BROKER_HOST = "sqlite:///%(here)s/user_dev/kombu.db" + +[plugins] +[[mediagoblin.plugins.ldap]] diff --git a/mediagoblin/tests/auth_configs/openid_appconfig.ini b/mediagoblin/tests/auth_configs/openid_appconfig.ini index c2bd82fd..3433e139 100644 --- a/mediagoblin/tests/auth_configs/openid_appconfig.ini +++ b/mediagoblin/tests/auth_configs/openid_appconfig.ini @@ -18,8 +18,8 @@ direct_remote_path = /test_static/ email_sender_address = "notice@mediagoblin.example.org" email_debug_mode = true -# TODO: Switch to using an in-memory database -sql_engine = "sqlite:///%(here)s/user_dev/mediagoblin.db" +sql_engine = "sqlite://" +run_migrations = true # Celery shouldn't be set up by the application as it's setup via # mediagoblin.init.celery.from_celery diff --git a/mediagoblin/tests/resources.py b/mediagoblin/tests/resources.py index f7b3037d..480f6d9a 100644 --- a/mediagoblin/tests/resources.py +++ b/mediagoblin/tests/resources.py @@ -29,6 +29,8 @@ EVIL_JPG = resource('evil.jpg') EVIL_PNG = resource('evil.png') BIG_BLUE = resource('bigblue.png') GOOD_PDF = resource('good.pdf') +MED_PNG = resource('medium.png') +BIG_PNG = resource('big.png') def resource_exif(f): diff --git a/mediagoblin/tests/test_auth.py b/mediagoblin/tests/test_auth.py index 63c12682..1bbc3d01 100644 --- a/mediagoblin/tests/test_auth.py +++ b/mediagoblin/tests/test_auth.py @@ -1,3 +1,4 @@ + # GNU MediaGoblin -- federated, autonomous media hosting # Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. # @@ -90,7 +91,7 @@ def test_register_views(test_app): ## Did we redirect to the proper page? Use the right template? assert urlparse.urlsplit(response.location)[2] == '/u/angrygirl/' - assert 'mediagoblin/user_pages/user.html' in template.TEMPLATE_TEST_CONTEXT + assert 'mediagoblin/user_pages/user_nonactive.html' in template.TEMPLATE_TEST_CONTEXT ## Make sure user is in place new_user = mg_globals.database.User.query.filter_by( @@ -105,7 +106,7 @@ def test_register_views(test_app): assert not new_user.has_privilege(u'active') ## Make sure user is logged in request = template.TEMPLATE_TEST_CONTEXT[ - 'mediagoblin/user_pages/user.html']['request'] + 'mediagoblin/user_pages/user_nonactive.html']['request'] assert request.session['user_id'] == unicode(new_user.id) ## Make sure we get email confirmation, and try verifying @@ -183,7 +184,7 @@ def test_register_views(test_app): message = mail.EMAIL_TEST_INBOX.pop() assert message['To'] == 'angrygrrl@example.org' email_context = template.TEMPLATE_TEST_CONTEXT[ - 'mediagoblin/auth/fp_verification_email.txt'] + 'mediagoblin/plugins/basic_auth/fp_verification_email.txt'] #TODO - change the name of verification_url to something forgot-password-ish assert email_context['verification_url'] in message.get_payload(decode=True) @@ -204,7 +205,8 @@ def test_register_views(test_app): ## Verify step 1 of password-change works -- can see form to change password template.clear_test_template_context() response = test_app.get("%s?%s" % (path, get_params)) - assert 'mediagoblin/auth/change_fp.html' in template.TEMPLATE_TEST_CONTEXT + assert 'mediagoblin/plugins/basic_auth/change_fp.html' in \ + template.TEMPLATE_TEST_CONTEXT ## Verify step 2.1 of password-change works -- report success to user template.clear_test_template_context() diff --git a/mediagoblin/tests/test_basic_auth.py b/mediagoblin/tests/test_basic_auth.py index cdd80fca..828f0515 100644 --- a/mediagoblin/tests/test_basic_auth.py +++ b/mediagoblin/tests/test_basic_auth.py @@ -13,7 +13,12 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. +import urlparse + +from mediagoblin.db.models import User from mediagoblin.plugins.basic_auth import tools as auth_tools +from mediagoblin.tests.tools import fixture_add_user +from mediagoblin.tools import template from mediagoblin.tools.testing import _activate_testing _activate_testing() @@ -57,3 +62,42 @@ def test_bcrypt_gen_password_hash(): pw, hashed_pw, '3><7R45417') assert not auth_tools.bcrypt_check_password( 'notthepassword', hashed_pw, '3><7R45417') + + +def test_change_password(test_app): + """Test changing password correctly and incorrectly""" + test_user = fixture_add_user( + password=u'toast', + privileges=[u'active']) + + test_app.post( + '/auth/login/', { + 'username': u'chris', + 'password': u'toast'}) + + # test that the password can be changed + res = test_app.post( + '/edit/password/', { + 'old_password': 'toast', + 'new_password': '123456', + }) + res.follow() + + # Did we redirect to the correct page? + assert urlparse.urlsplit(res.location)[2] == '/edit/account/' + + # test_user has to be fetched again in order to have the current values + test_user = User.query.filter_by(username=u'chris').first() + assert auth_tools.bcrypt_check_password('123456', test_user.pw_hash) + + # test that the password cannot be changed if the given + # old_password is wrong + template.clear_test_template_context() + test_app.post( + '/edit/password/', { + 'old_password': 'toast', + 'new_password': '098765', + }) + + test_user = User.query.filter_by(username=u'chris').first() + assert not auth_tools.bcrypt_check_password('098765', test_user.pw_hash) diff --git a/mediagoblin/tests/test_edit.py b/mediagoblin/tests/test_edit.py index 7b58cb9b..4f44e0b9 100644 --- a/mediagoblin/tests/test_edit.py +++ b/mediagoblin/tests/test_edit.py @@ -58,41 +58,6 @@ class TestUserEdit(object): self.login(test_app) - def test_change_password(self, test_app): - """Test changing password correctly and incorrectly""" - self.login(test_app) - - # test that the password can be changed - template.clear_test_template_context() - res = test_app.post( - '/edit/password/', { - 'old_password': 'toast', - 'new_password': '123456', - }) - res.follow() - - # Did we redirect to the correct page? - assert urlparse.urlsplit(res.location)[2] == '/edit/account/' - - # test_user has to be fetched again in order to have the current values - test_user = User.query.filter_by(username=u'chris').first() - assert auth.check_password('123456', test_user.pw_hash) - # Update current user passwd - self.user_password = '123456' - - # test that the password cannot be changed if the given - # old_password is wrong - template.clear_test_template_context() - test_app.post( - '/edit/password/', { - 'old_password': 'toast', - 'new_password': '098765', - }) - - test_user = User.query.filter_by(username=u'chris').first() - assert not auth.check_password('098765', test_user.pw_hash) - - def test_change_bio_url(self, test_app): """Test changing bio and URL""" self.login(test_app) diff --git a/mediagoblin/tests/test_ldap.py b/mediagoblin/tests/test_ldap.py new file mode 100644 index 00000000..48efb4b6 --- /dev/null +++ b/mediagoblin/tests/test_ldap.py @@ -0,0 +1,125 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +import urlparse +import pkg_resources +import pytest +import mock + +from mediagoblin import mg_globals +from mediagoblin.db.base import Session +from mediagoblin.tests.tools import get_app +from mediagoblin.tools import template + +pytest.importorskip("ldap") + + +@pytest.fixture() +def ldap_plugin_app(request): + return get_app( + request, + mgoblin_config=pkg_resources.resource_filename( + 'mediagoblin.tests.auth_configs', + 'ldap_appconfig.ini')) + + +def return_value(): + return u'chris', u'chris@example.com' + + +def test_ldap_plugin(ldap_plugin_app): + res = ldap_plugin_app.get('/auth/login/') + + assert urlparse.urlsplit(res.location)[2] == '/auth/ldap/login/' + + res = ldap_plugin_app.get('/auth/register/') + + assert urlparse.urlsplit(res.location)[2] == '/auth/ldap/register/' + + res = ldap_plugin_app.get('/auth/ldap/register/') + + assert urlparse.urlsplit(res.location)[2] == '/auth/ldap/login/' + + template.clear_test_template_context() + res = ldap_plugin_app.post( + '/auth/ldap/login/', {}) + + context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/auth/login.html'] + form = context['login_form'] + assert form.username.errors == [u'This field is required.'] + assert form.password.errors == [u'This field is required.'] + + @mock.patch('mediagoblin.plugins.ldap.tools.LDAP.login', mock.Mock(return_value=return_value())) + def _test_authentication(): + template.clear_test_template_context() + res = ldap_plugin_app.post( + '/auth/ldap/login/', + {'username': u'chris', + 'password': u'toast'}) + + context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/auth/register.html'] + register_form = context['register_form'] + + assert register_form.username.data == u'chris' + assert register_form.email.data == u'chris@example.com' + + template.clear_test_template_context() + res = ldap_plugin_app.post( + '/auth/ldap/register/', + {'username': u'chris', + 'email': u'chris@example.com'}) + res.follow() + + assert urlparse.urlsplit(res.location)[2] == '/u/chris/' + assert 'mediagoblin/user_pages/user_nonactive.html' in template.TEMPLATE_TEST_CONTEXT + + # Try to register with same email and username + template.clear_test_template_context() + res = ldap_plugin_app.post( + '/auth/ldap/register/', + {'username': u'chris', + 'email': u'chris@example.com'}) + + context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/auth/register.html'] + register_form = context['register_form'] + + assert register_form.email.errors == [u'Sorry, a user with that email address already exists.'] + assert register_form.username.errors == [u'Sorry, a user with that name already exists.'] + + # Log out + ldap_plugin_app.get('/auth/logout/') + + # Get user and detach from session + test_user = mg_globals.database.User.query.filter_by( + username=u'chris').first() + Session.expunge(test_user) + + # Log back in + template.clear_test_template_context() + res = ldap_plugin_app.post( + '/auth/ldap/login/', + {'username': u'chris', + 'password': u'toast'}) + res.follow() + + assert urlparse.urlsplit(res.location)[2] == '/' + assert 'mediagoblin/root.html' in template.TEMPLATE_TEST_CONTEXT + + # Make sure user is in the session + context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/root.html'] + session = context['request'].session + assert session['user_id'] == unicode(test_user.id) + + _test_authentication() diff --git a/mediagoblin/tests/test_mgoblin_app.ini b/mediagoblin/tests/test_mgoblin_app.ini index da0dffb9..4cd3d9b6 100644 --- a/mediagoblin/tests/test_mgoblin_app.ini +++ b/mediagoblin/tests/test_mgoblin_app.ini @@ -13,6 +13,10 @@ tags_max_length = 50 # So we can start to test attachments: allow_attachments = True +upload_limit = 500 + +max_file_size = 2 + [storage:publicstore] base_dir = %(here)s/user_dev/media/public base_url = /mgoblin_media/ diff --git a/mediagoblin/tests/test_openid.py b/mediagoblin/tests/test_openid.py index 3aea7982..0424fdda 100644 --- a/mediagoblin/tests/test_openid.py +++ b/mediagoblin/tests/test_openid.py @@ -29,6 +29,7 @@ from mediagoblin.plugins.openid.models import OpenIDUserURL from mediagoblin.tests.tools import get_app, fixture_add_user from mediagoblin.tools import template + # App with plugin enabled @pytest.fixture() def openid_plugin_app(request): @@ -177,7 +178,7 @@ class TestOpenIDPlugin(object): # Correct place? assert urlparse.urlsplit(res.location)[2] == '/u/chris/' - assert 'mediagoblin/user_pages/user.html' in template.TEMPLATE_TEST_CONTEXT + assert 'mediagoblin/user_pages/user_nonactive.html' in template.TEMPLATE_TEST_CONTEXT # No need to test if user is in logged in and verification email # awaits, since openid uses the register_user function which is diff --git a/mediagoblin/tests/test_persona.py b/mediagoblin/tests/test_persona.py index a696858d..a1cd30eb 100644 --- a/mediagoblin/tests/test_persona.py +++ b/mediagoblin/tests/test_persona.py @@ -91,7 +91,7 @@ class TestPersonaPlugin(object): res.follow() assert urlparse.urlsplit(res.location)[2] == '/u/chris/' - assert 'mediagoblin/user_pages/user.html' in template.TEMPLATE_TEST_CONTEXT + assert 'mediagoblin/user_pages/user_nonactive.html' in template.TEMPLATE_TEST_CONTEXT # Try to register same Persona email address template.clear_test_template_context() diff --git a/mediagoblin/tests/test_submission.py b/mediagoblin/tests/test_submission.py index 14766c50..b5b13ed3 100644 --- a/mediagoblin/tests/test_submission.py +++ b/mediagoblin/tests/test_submission.py @@ -25,12 +25,13 @@ import pytest from mediagoblin.tests.tools import fixture_add_user from mediagoblin import mg_globals from mediagoblin.db.models import MediaEntry, User +from mediagoblin.db.base import Session from mediagoblin.tools import template from mediagoblin.media_types.image import ImageMediaManager from mediagoblin.media_types.pdf.processing import check_prerequisites as pdf_check_prerequisites from .resources import GOOD_JPG, GOOD_PNG, EVIL_FILE, EVIL_JPG, EVIL_PNG, \ - BIG_BLUE, GOOD_PDF, GPS_JPG + BIG_BLUE, GOOD_PDF, GPS_JPG, MED_PNG, BIG_PNG GOOD_TAG_STRING = u'yin,yang' BAD_TAG_STRING = unicode('rage,' + 'f' * 26 + 'u' * 26) @@ -117,9 +118,30 @@ class TestSubmission: self.logout() self.test_app.get(url) + def user_upload_limits(self, uploaded=None, upload_limit=None): + our_user = self.our_user() + + if uploaded: + our_user.uploaded = uploaded + if upload_limit: + our_user.upload_limit = upload_limit + + our_user.save() + Session.expunge(our_user) + def test_normal_jpg(self): + # User uploaded should be 0 + assert self.our_user().uploaded == 0 + self.check_normal_upload(u'Normal upload 1', GOOD_JPG) + # User uploaded should be the same as GOOD_JPG size in Mb + file_size = os.stat(GOOD_JPG).st_size / (1024.0 * 1024) + file_size = float('{0:.2f}'.format(file_size)) + + # Reload user + assert self.our_user().uploaded == file_size + def test_normal_png(self): self.check_normal_upload(u'Normal upload 2', GOOD_PNG) @@ -131,6 +153,60 @@ class TestSubmission: self.check_url(response, '/u/{0}/'.format(self.our_user().username)) assert 'mediagoblin/user_pages/user.html' in context + def test_default_upload_limits(self): + self.user_upload_limits(uploaded=500) + + # User uploaded should be 500 + assert self.our_user().uploaded == 500 + + response, context = self.do_post({'title': u'Normal upload 4'}, + do_follow=True, + **self.upload_data(GOOD_JPG)) + self.check_url(response, '/u/{0}/'.format(self.our_user().username)) + assert 'mediagoblin/user_pages/user.html' in context + + # Shouldn't have uploaded + assert self.our_user().uploaded == 500 + + def test_user_upload_limit(self): + self.user_upload_limits(uploaded=25, upload_limit=25) + + # User uploaded should be 25 + assert self.our_user().uploaded == 25 + + response, context = self.do_post({'title': u'Normal upload 5'}, + do_follow=True, + **self.upload_data(GOOD_JPG)) + self.check_url(response, '/u/{0}/'.format(self.our_user().username)) + assert 'mediagoblin/user_pages/user.html' in context + + # Shouldn't have uploaded + assert self.our_user().uploaded == 25 + + def test_user_under_limit(self): + self.user_upload_limits(uploaded=499) + + # User uploaded should be 499 + assert self.our_user().uploaded == 499 + + response, context = self.do_post({'title': u'Normal upload 6'}, + do_follow=False, + **self.upload_data(MED_PNG)) + form = context['mediagoblin/submit/start.html']['submit_form'] + assert form.file.errors == [u'Sorry, uploading this file will put you' + ' over your upload limit.'] + + # Shouldn't have uploaded + assert self.our_user().uploaded == 499 + + def test_big_file(self): + response, context = self.do_post({'title': u'Normal upload 7'}, + do_follow=False, + **self.upload_data(BIG_PNG)) + + form = context['mediagoblin/submit/start.html']['submit_form'] + assert form.file.errors == [u'Sorry, the file size is too big.'] + def check_media(self, request, find_data, count=None): media = MediaEntry.query.filter_by(**find_data) if count is not None: @@ -165,6 +241,7 @@ class TestSubmission: 'ffffffffffffffffffffffffffuuuuuuuuuuuuuuuuuuuuuuuuuu'] def test_delete(self): + self.user_upload_limits(uploaded=50) response, request = self.do_post({'title': u'Balanced Goblin'}, *REQUEST_CONTEXT, do_follow=True, **self.upload_data(GOOD_JPG)) @@ -209,6 +286,9 @@ class TestSubmission: self.check_media(request, {'id': media_id}, 0) self.check_comments(request, media_id, 0) + # Check that user.uploaded is the same as before the upload + assert self.our_user().uploaded == 50 + def test_evil_file(self): # Test non-suppoerted file with non-supported extension # ----------------------------------------------------- diff --git a/mediagoblin/tests/test_submission/COPYING.txt b/mediagoblin/tests/test_submission/COPYING.txt new file mode 100644 index 00000000..3818aae4 --- /dev/null +++ b/mediagoblin/tests/test_submission/COPYING.txt @@ -0,0 +1,5 @@ +Images located in this directory tree are released under a GPLv3 license +and CC BY-SA 3.0 license. To the extent possible under law, the author(s) +have dedicated all copyright and related and neighboring rights to these +files to the public domain worldwide. These files are distributed without +any warranty. diff --git a/mediagoblin/tests/test_submission/big.png b/mediagoblin/tests/test_submission/big.png Binary files differnew file mode 100644 index 00000000..a284cfda --- /dev/null +++ b/mediagoblin/tests/test_submission/big.png diff --git a/mediagoblin/tests/test_submission/medium.png b/mediagoblin/tests/test_submission/medium.png Binary files differnew file mode 100644 index 00000000..e8b9ca00 --- /dev/null +++ b/mediagoblin/tests/test_submission/medium.png diff --git a/mediagoblin/tools/pagination.py b/mediagoblin/tools/pagination.py index d0f08c94..855878e0 100644 --- a/mediagoblin/tools/pagination.py +++ b/mediagoblin/tools/pagination.py @@ -18,7 +18,7 @@ import urllib import copy from math import ceil, floor from itertools import izip, count - +from werkzeug.datastructures import MultiDict PAGINATION_DEFAULT_PER_PAGE = 30 @@ -98,7 +98,11 @@ class Pagination(object): """ Get a page url by adding a page= parameter to the base url """ - new_get_params = dict(get_params) or {} + if isinstance(get_params, MultiDict): + new_get_params = get_params.to_dict() + else: + new_get_params = dict(get_params) or {} + new_get_params['page'] = page_no return "%s?%s" % ( base_url, urllib.urlencode(new_get_params)) diff --git a/mediagoblin/user_pages/views.py b/mediagoblin/user_pages/views.py index 020fa6a8..73823e4d 100644 --- a/mediagoblin/user_pages/views.py +++ b/mediagoblin/user_pages/views.py @@ -50,16 +50,13 @@ _log.setLevel(logging.DEBUG) @uses_pagination def user_home(request, page): """'Homepage' of a User()""" - # TODO: decide if we only want homepages for active users, we can - # then use the @get_active_user decorator and also simplify the - # template html. user = User.query.filter_by(username=request.matchdict['user']).first() if not user: return render_404(request) elif not user.has_privilege(u'active'): return render_to_response( request, - 'mediagoblin/user_pages/user.html', + 'mediagoblin/user_pages/user_nonactive.html', {'user': user}) cursor = MediaEntry.query.\ @@ -299,6 +296,11 @@ def media_confirm_delete(request, media): if request.method == 'POST' and form.validate(): if form.confirm.data is True: username = media.get_uploader.username + + media.get_uploader.uploaded = media.get_uploader.uploaded - \ + media.file_size + media.get_uploader.save() + # Delete MediaEntry and all related files, comments etc. media.delete() messages.add_message( |