aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--docs/source/devel/migrations.rst62
-rw-r--r--docs/source/index.rst1
-rw-r--r--docs/source/pluginwriter/api.rst26
-rw-r--r--docs/source/siteadmin/relnotes.rst4
-rw-r--r--mediagoblin.ini1
-rw-r--r--mediagoblin/_version.py2
-rw-r--r--mediagoblin/app.py14
-rw-r--r--mediagoblin/auth/__init__.py29
-rw-r--r--mediagoblin/auth/forms.py29
-rw-r--r--mediagoblin/auth/tools.py117
-rw-r--r--mediagoblin/auth/views.py139
-rw-r--r--mediagoblin/config_spec.ini7
-rw-r--r--mediagoblin/db/migrations.py94
-rw-r--r--mediagoblin/db/mixin.py9
-rw-r--r--mediagoblin/db/models.py115
-rw-r--r--mediagoblin/db/models_v0.py23
-rw-r--r--mediagoblin/decorators.py35
-rw-r--r--mediagoblin/edit/forms.py13
-rw-r--r--mediagoblin/edit/routing.py2
-rw-r--r--mediagoblin/edit/views.py127
-rw-r--r--mediagoblin/gmg_commands/dbupdate.py18
-rw-r--r--mediagoblin/gmg_commands/users.py6
-rw-r--r--mediagoblin/init/__init__.py14
-rw-r--r--mediagoblin/init/celery/__init__.py18
-rw-r--r--mediagoblin/meddleware/csrf.py2
-rw-r--r--mediagoblin/media_types/stl/processing.py2
-rw-r--r--mediagoblin/media_types/video/transcoders.py6
-rw-r--r--mediagoblin/notifications/__init__.py141
-rw-r--r--mediagoblin/notifications/routing.py25
-rw-r--r--mediagoblin/notifications/task.py46
-rw-r--r--mediagoblin/notifications/tools.py55
-rw-r--r--mediagoblin/notifications/views.py54
-rw-r--r--mediagoblin/plugins/basic_auth/__init__.py85
-rw-r--r--mediagoblin/plugins/basic_auth/forms.py43
-rw-r--r--mediagoblin/plugins/basic_auth/tools.py (renamed from mediagoblin/auth/lib.py)38
-rw-r--r--mediagoblin/plugins/openid/__init__.py123
-rw-r--r--mediagoblin/plugins/openid/forms.py41
-rw-r--r--mediagoblin/plugins/openid/models.py65
-rw-r--r--mediagoblin/plugins/openid/store.py127
-rw-r--r--mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/add.html44
-rw-r--r--mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/delete.html43
-rw-r--r--mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/edit_link.html25
-rw-r--r--mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/login.html65
-rw-r--r--mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/login_link.html25
-rw-r--r--mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/register_link.html27
-rw-r--r--mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/request_form.html24
-rw-r--r--mediagoblin/plugins/openid/views.py404
-rw-r--r--mediagoblin/routing.py1
-rw-r--r--mediagoblin/static/css/base.css7
-rw-r--r--mediagoblin/static/css/pdf_viewer.css1448
-rw-r--r--mediagoblin/static/js/notifications.js36
-rw-r--r--mediagoblin/static/js/pdf_viewer.js3615
-rw-r--r--mediagoblin/submit/views.py4
-rw-r--r--mediagoblin/templates/mediagoblin/auth/change_fp.html3
-rw-r--r--mediagoblin/templates/mediagoblin/auth/forgot_password.html2
-rw-r--r--mediagoblin/templates/mediagoblin/auth/login.html18
-rw-r--r--mediagoblin/templates/mediagoblin/auth/register.html7
-rw-r--r--mediagoblin/templates/mediagoblin/base.html11
-rw-r--r--mediagoblin/templates/mediagoblin/bits/frontpage_welcome.html36
-rw-r--r--mediagoblin/templates/mediagoblin/edit/edit_account.html11
-rw-r--r--mediagoblin/templates/mediagoblin/edit/verification.txt29
-rw-r--r--mediagoblin/templates/mediagoblin/fragments/header_notifications.html40
-rw-r--r--mediagoblin/templates/mediagoblin/media_displays/pdf.html28
-rw-r--r--mediagoblin/templates/mediagoblin/user_pages/media.html3
-rw-r--r--mediagoblin/templates/mediagoblin/utils/comment-subscription.html34
-rw-r--r--mediagoblin/templates/mediagoblin/utils/wtforms.html22
-rw-r--r--mediagoblin/tests/appconfig_context_modified.ini5
-rw-r--r--mediagoblin/tests/appconfig_static_plugin.ini5
-rw-r--r--mediagoblin/tests/auth_configs/__init__.py0
-rw-r--r--mediagoblin/tests/auth_configs/authentication_disabled_appconfig.ini25
-rw-r--r--mediagoblin/tests/auth_configs/openid_appconfig.ini41
-rw-r--r--mediagoblin/tests/test_auth.py141
-rw-r--r--mediagoblin/tests/test_basic_auth.py59
-rw-r--r--mediagoblin/tests/test_celery_setup.py2
-rw-r--r--mediagoblin/tests/test_edit.py74
-rw-r--r--mediagoblin/tests/test_mgoblin_app.ini7
-rw-r--r--mediagoblin/tests/test_misc.py8
-rw-r--r--mediagoblin/tests/test_notifications.py151
-rw-r--r--mediagoblin/tests/test_openid.py372
-rw-r--r--mediagoblin/tests/tools.py99
-rw-r--r--mediagoblin/tools/mail.py7
-rw-r--r--mediagoblin/tools/template.py1
-rw-r--r--mediagoblin/user_pages/views.py21
-rw-r--r--setup.py6
84 files changed, 3357 insertions, 5437 deletions
diff --git a/docs/source/devel/migrations.rst b/docs/source/devel/migrations.rst
new file mode 100644
index 00000000..16c02b04
--- /dev/null
+++ b/docs/source/devel/migrations.rst
@@ -0,0 +1,62 @@
+.. MediaGoblin Documentation
+
+ Written in 2011, 2012 by MediaGoblin contributors
+
+ To the extent possible under law, the author(s) have dedicated all
+ copyright and related and neighboring rights to this software to
+ the public domain worldwide. This software is distributed without
+ any warranty.
+
+ You should have received a copy of the CC0 Public Domain
+ Dedication along with this software. If not, see
+ <http://creativecommons.org/publicdomain/zero/1.0/>.
+
+==========
+Migrations
+==========
+
+So, about migrations. Every time we change the way the database
+structure works, we need to add a migration so that people running
+older codebases can have their databases updated to the new structure
+when they run `./bin/gmg dbupdate`.
+
+The first time `./bin/gmg dbupdate` is run by a user, it creates the
+tables at the current state that they're defined in models.py and sets
+the migration number to the current migration... after all, migrations
+only exist to get things to the current state of the db. After that,
+every migration is run with dbupdate.
+
+There's a few things you need to know:
+
+- We use `sqlalchemy-migrate
+ <http://code.google.com/p/sqlalchemy-migrate/>`_.
+ See `their docs <https://sqlalchemy-migrate.readthedocs.org/>`_.
+- `Alembic <https://bitbucket.org/zzzeek/alembic>`_ might be a better
+ choice than sqlalchemy-migrate now or in the future, but we
+ originally decided not to use it because it didn't have sqlite
+ support. It's not clear if that's changed.
+- SQLAlchemy has two parts to it, the ORM and the "core" interface.
+ We DO NOT use the ORM when running migrations. Think about it: the
+ ORM is set up with an expectation that the models already reflect a
+ certain pattern. But if a person is moving from their old patern
+ and are running tools to *get to* the current pattern, of course
+ their current database structure doesn't match the state of the ORM!
+- How to write migrations? Maybe there will be a tutorial here in the
+ future... in the meanwhile, look at existing migrations in
+ `mediagoblin/db/migrations.py` and look in
+ `mediagoblin/tests/test_sql_migrations.py` for examples.
+- Common pattern: use `inspect_table` to get the current state
+ of the table before we run alterations on it.
+- Make sure you set the RegisterMigration to be the next migration in
+ order.
+- What happens if you're adding a *totally new* table? In this case,
+ you should copy the table in entirety as it exists into
+ migrations.py then create the tables based off of that... see
+ add_collection_tables. This is easier than reproducing the SQL by
+ hand.
+- If you're writing a feature branch, you don't need to keep adding
+ migrations every time you change things around if your database
+ structure is in flux. Just alter your migrations so that they're
+ correct for the merge into master.
+
+That's it for now! Good luck!
diff --git a/docs/source/index.rst b/docs/source/index.rst
index d71f39f8..c8a3f040 100644
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -87,6 +87,7 @@ This chapter contains various information for developers.
devel/codebase
devel/storage
devel/originaldesigndecisions
+ devel/migrations
Indices and tables
diff --git a/docs/source/pluginwriter/api.rst b/docs/source/pluginwriter/api.rst
index 66def173..29adb691 100644
--- a/docs/source/pluginwriter/api.rst
+++ b/docs/source/pluginwriter/api.rst
@@ -69,6 +69,32 @@ example might look like::
This means that when people enable your plugin in their config you'll
be able to provide defaults as well as type validation.
+You can access this via the app_config variables in mg_globals, or you
+can use a shortcut to get your plugin's config section::
+
+ >>> from mediagoblin.tools import pluginapi
+ # Replace with the path to your plugin.
+ # (If an external package, it won't be part of mediagoblin.plugins)
+ >>> floobie_config = pluginapi.get_config('mediagoblin.plugins.floobifier')
+ >>> floobie_dir = floobie_config['floobie_dir']
+ # This is the same as the above
+ >>> from mediagoblin import mg_globals
+ >>> config = mg_globals.global_config['plugins']['mediagoblin.plugins.floobifier']
+ >>> floobie_dir = floobie_config['floobie_dir']
+
+A tip: you have access to the `%(here)s` variable in your config,
+which is the directory that the user's mediagoblin config is running
+out of. So for example, your plugin may need a "floobie" directory to
+store floobs in. You could give them a reasonable default that makes
+use of the default `user_dev` location, but allow users to override
+it, like so::
+
+ [plugin_spec]
+ floobie_dir = string(default="%(here)s/user_dev/floobs/")
+
+Note, this is relative to the user's mediagoblin config directory,
+*not* your plugin directory!
+
Context Hooks
-------------
diff --git a/docs/source/siteadmin/relnotes.rst b/docs/source/siteadmin/relnotes.rst
index 2666b0a8..b49d1654 100644
--- a/docs/source/siteadmin/relnotes.rst
+++ b/docs/source/siteadmin/relnotes.rst
@@ -38,7 +38,9 @@ Otherwise, follow 0.4.0 instructions.
=====
**Do this to upgrade**
-1. Make sure to run ``bin/gmg dbupdate`` after upgrading.
+1. Make sure to run
+ ``./bin/python setup.py develop --upgrade && ./bin/gmg dbupdate``
+ after upgrading.
2. See "For Theme authors" if you have a custom theme.
3. Note that ``./bin/gmg theme assetlink`` is now just
``./bin/gmg assetlink`` and covers both plugins and assets.
diff --git a/mediagoblin.ini b/mediagoblin.ini
index cc45c08d..e878a478 100644
--- a/mediagoblin.ini
+++ b/mediagoblin.ini
@@ -47,3 +47,4 @@ base_url = /mgoblin_media/
# documentation for details.
[plugins]
[[mediagoblin.plugins.geolocation]]
+[[mediagoblin.plugins.basic_auth]]
diff --git a/mediagoblin/_version.py b/mediagoblin/_version.py
index 1aa0e2c4..94629775 100644
--- a/mediagoblin/_version.py
+++ b/mediagoblin/_version.py
@@ -23,4 +23,4 @@
# see http://www.python.org/dev/peps/pep-0386/
-__version__ = "0.4.1"
+__version__ = "0.5.0.dev"
diff --git a/mediagoblin/app.py b/mediagoblin/app.py
index 1984ce77..ada0c8ba 100644
--- a/mediagoblin/app.py
+++ b/mediagoblin/app.py
@@ -37,6 +37,8 @@ from mediagoblin.init import (get_jinja_loader, get_staticdirector,
setup_storage)
from mediagoblin.tools.pluginapi import PluginManager, hook_transform
from mediagoblin.tools.crypto import setup_crypto
+from mediagoblin.auth.tools import check_auth_enabled, no_auth_logout
+from mediagoblin import notifications
_log = logging.getLogger(__name__)
@@ -85,7 +87,7 @@ class MediaGoblinApp(object):
setup_plugins()
# Set up the database
- self.db = setup_database()
+ self.db = setup_database(app_config['run_migrations'])
# Register themes
self.theme_registry, self.current_theme = register_themes(app_config)
@@ -97,6 +99,11 @@ class MediaGoblinApp(object):
PluginManager().get_template_paths()
)
+ # Check if authentication plugin is enabled and respond accordingly.
+ self.auth = check_auth_enabled()
+ if not self.auth:
+ app_config['allow_comments'] = False
+
# Set up storage systems
self.public_store, self.queue_store = setup_storage()
@@ -186,6 +193,11 @@ class MediaGoblinApp(object):
request.urlgen = build_proxy
+ # Log user out if authentication_disabled
+ no_auth_logout(request)
+
+ request.notifications = notifications
+
mg_request.setup_user_in_request(request)
request.controller_name = None
diff --git a/mediagoblin/auth/__init__.py b/mediagoblin/auth/__init__.py
index 621845ba..be5d0eed 100644
--- a/mediagoblin/auth/__init__.py
+++ b/mediagoblin/auth/__init__.py
@@ -13,3 +13,32 @@
#
# 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.tools.pluginapi import hook_handle, hook_runall
+
+
+def get_user(**kwargs):
+ """ Takes a kwarg such as username and returns a user object """
+ return hook_handle("auth_get_user", **kwargs)
+
+
+def create_user(register_form):
+ results = hook_runall("auth_create_user", register_form)
+ return results[0]
+
+
+def extra_validation(register_form):
+ from mediagoblin.auth.tools import basic_extra_validation
+
+ extra_validation_passes = basic_extra_validation(register_form)
+ if False in hook_runall("auth_extra_validation", register_form):
+ extra_validation_passes = False
+ return extra_validation_passes
+
+
+def gen_password_hash(raw_pass, extra_salt=None):
+ return hook_handle("auth_gen_password_hash", raw_pass, extra_salt)
+
+
+def check_password(raw_pass, stored_hash, extra_salt=None):
+ return hook_handle("auth_check_password",
+ raw_pass, stored_hash, extra_salt)
diff --git a/mediagoblin/auth/forms.py b/mediagoblin/auth/forms.py
index 0a391d67..865502e9 100644
--- a/mediagoblin/auth/forms.py
+++ b/mediagoblin/auth/forms.py
@@ -20,32 +20,6 @@ from mediagoblin.tools.translate import lazy_pass_to_ugettext as _
from mediagoblin.auth.tools import normalize_user_or_email_field
-class RegistrationForm(wtforms.Form):
- username = wtforms.TextField(
- _('Username'),
- [wtforms.validators.Required(),
- normalize_user_or_email_field(allow_email=False)])
- password = wtforms.PasswordField(
- _('Password'),
- [wtforms.validators.Required(),
- wtforms.validators.Length(min=5, max=1024)])
- email = wtforms.TextField(
- _('Email address'),
- [wtforms.validators.Required(),
- normalize_user_or_email_field(allow_user=False)])
-
-
-class LoginForm(wtforms.Form):
- username = wtforms.TextField(
- _('Username or Email'),
- [wtforms.validators.Required(),
- normalize_user_or_email_field()])
- password = wtforms.PasswordField(
- _('Password'),
- [wtforms.validators.Required(),
- wtforms.validators.Length(min=5, max=1024)])
-
-
class ForgotPassForm(wtforms.Form):
username = wtforms.TextField(
_('Username or email'),
@@ -58,9 +32,6 @@ class ChangePassForm(wtforms.Form):
'Password',
[wtforms.validators.Required(),
wtforms.validators.Length(min=5, max=1024)])
- userid = wtforms.HiddenField(
- '',
- [wtforms.validators.Required()])
token = wtforms.HiddenField(
'',
[wtforms.validators.Required()])
diff --git a/mediagoblin/auth/tools.py b/mediagoblin/auth/tools.py
index db6b6e37..579775ff 100644
--- a/mediagoblin/auth/tools.py
+++ b/mediagoblin/auth/tools.py
@@ -14,19 +14,18 @@
# 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 uuid
import logging
-
import wtforms
-from sqlalchemy import or_
from mediagoblin import mg_globals
-from mediagoblin.auth import lib as auth_lib
+from mediagoblin.tools.crypto import get_timed_signer_url
from mediagoblin.db.models import User
from mediagoblin.tools.mail import (normalize_email, send_email,
email_debug_message)
from mediagoblin.tools.template import render_template
from mediagoblin.tools.translate import lazy_pass_to_ugettext as _
+from mediagoblin.tools.pluginapi import hook_handle
+from mediagoblin import auth
_log = logging.getLogger(__name__)
@@ -62,11 +61,12 @@ def normalize_user_or_email_field(allow_email=True, allow_user=True):
EMAIL_VERIFICATION_TEMPLATE = (
- u"http://{host}{uri}?"
- u"userid={userid}&token={verification_key}")
+ u"{uri}?"
+ u"token={verification_key}")
-def send_verification_email(user, request):
+def send_verification_email(user, request, email=None,
+ rendered_email=None):
"""
Send the verification email to users to activate their accounts.
@@ -74,19 +74,24 @@ def send_verification_email(user, request):
- user: a user object
- request: the request
"""
- rendered_email = render_template(
- request, 'mediagoblin/auth/verification_email.txt',
- {'username': user.username,
- 'verification_url': EMAIL_VERIFICATION_TEMPLATE.format(
- host=request.host,
- uri=request.urlgen('mediagoblin.auth.verify_email'),
- userid=unicode(user.id),
- verification_key=user.verification_key)})
+ if not email:
+ email = user.email
+
+ if not rendered_email:
+ verification_key = get_timed_signer_url('mail_verification_token') \
+ .dumps(user.id)
+ rendered_email = render_template(
+ request, 'mediagoblin/auth/verification_email.txt',
+ {'username': user.username,
+ 'verification_url': EMAIL_VERIFICATION_TEMPLATE.format(
+ uri=request.urlgen('mediagoblin.auth.verify_email',
+ qualified=True),
+ verification_key=verification_key)})
# TODO: There is no error handling in place
send_email(
mg_globals.app_config['email_sender_address'],
- [user.email],
+ [email],
# TODO
# Due to the distributed nature of GNU MediaGoblin, we should
# find a way to send some additional information about the
@@ -96,11 +101,43 @@ def send_verification_email(user, request):
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.data['username']).count()
+ username=register_form.username.data).count()
users_with_email = User.query.filter_by(
- email=register_form.data['email']).count()
+ email=register_form.email.data).count()
extra_validation_passes = True
@@ -118,17 +155,11 @@ def basic_extra_validation(register_form, *args):
def register_user(request, register_form):
""" Handle user registration """
- extra_validation_passes = basic_extra_validation(register_form)
+ extra_validation_passes = auth.extra_validation(register_form)
if extra_validation_passes:
# Create the user
- user = User()
- user.username = register_form.data['username']
- user.email = register_form.data['email']
- user.pw_hash = auth_lib.bcrypt_gen_password_hash(
- register_form.password.data)
- user.verification_key = unicode(uuid.uuid4())
- user.save()
+ user = auth.create_user(register_form)
# log the user in
request.session['user_id'] = unicode(user.id)
@@ -143,17 +174,37 @@ def register_user(request, register_form):
return None
-def check_login_simple(username, password, username_might_be_email=False):
- search = (User.username == username)
- if username_might_be_email and ('@' in username):
- search = or_(search, User.email == username)
- user = User.query.filter(search).first()
+def check_login_simple(username, password):
+ user = auth.get_user(username=username)
if not user:
_log.info("User %r not found", username)
- auth_lib.fake_login_attempt()
+ hook_handle("auth_fake_login_attempt")
return None
- if not auth_lib.bcrypt_check_password(password, user.pw_hash):
+ if not auth.check_password(password, user.pw_hash):
_log.warn("Wrong password for %r", username)
return None
_log.info("Logging %r in", username)
return user
+
+
+def check_auth_enabled():
+ if not hook_handle('authentication'):
+ _log.warning('No authentication is enabled')
+ return False
+ else:
+ return True
+
+
+def no_auth_logout(request):
+ """Log out the user if authentication_disabled, 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()
+
+
+def create_basic_user(form):
+ user = User()
+ user.username = form.username.data
+ user.email = form.email.data
+ user.save()
+ return user
diff --git a/mediagoblin/auth/views.py b/mediagoblin/auth/views.py
index bb7bda77..1cff8dcc 100644
--- a/mediagoblin/auth/views.py
+++ b/mediagoblin/auth/views.py
@@ -14,36 +14,37 @@
# 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 uuid
-import datetime
+from itsdangerous import BadSignature
from mediagoblin import messages, mg_globals
from mediagoblin.db.models import User
+from mediagoblin.tools.crypto import get_timed_signer_url
+from mediagoblin.decorators import auth_enabled, allow_registration
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.auth import lib as auth_lib
+from mediagoblin.tools.pluginapi import hook_handle
from mediagoblin.auth import forms as auth_forms
-from mediagoblin.auth.lib import send_fp_verification_email
from mediagoblin.auth.tools import (send_verification_email, register_user,
+ send_fp_verification_email,
check_login_simple)
+from mediagoblin import auth
+@allow_registration
+@auth_enabled
def register(request):
"""The registration view.
Note that usernames will always be lowercased. Email domains are lowercased while
the first part remains case-sensitive.
"""
- # Redirects to indexpage if registrations are disabled
- if not mg_globals.app_config["allow_registration"]:
- messages.add_message(
- request,
- messages.WARNING,
- _('Sorry, registration is disabled on this instance.'))
- return redirect(request, "index")
+ if 'pass_auth' not in request.template_env.globals:
+ redirect_name = hook_handle('auth_no_pass_redirect')
+ return redirect(request, 'mediagoblin.plugins.{0}.register'.format(
+ redirect_name))
- register_form = auth_forms.RegistrationForm(request.form)
+ register_form = hook_handle("auth_get_registration_form", request)
if request.method == 'POST' and register_form.validate():
# TODO: Make sure the user doesn't exist already
@@ -59,25 +60,31 @@ def register(request):
return render_to_response(
request,
'mediagoblin/auth/register.html',
- {'register_form': register_form})
+ {'register_form': register_form,
+ 'post_url': request.urlgen('mediagoblin.auth.register')})
+@auth_enabled
def login(request):
"""
MediaGoblin login view.
If you provide the POST with 'next', it'll redirect to that view.
"""
- login_form = auth_forms.LoginForm(request.form)
+ if 'pass_auth' not in request.template_env.globals:
+ redirect_name = hook_handle('auth_no_pass_redirect')
+ return redirect(request, 'mediagoblin.plugins.{0}.login'.format(
+ redirect_name))
+
+ login_form = hook_handle("auth_get_login_form", request)
login_failed = False
if request.method == 'POST':
-
- username = login_form.data['username']
+ username = login_form.username.data
if login_form.validate():
- user = check_login_simple(username, login_form.password.data, True)
+ user = check_login_simple(username, login_form.password.data)
if user:
# set up login in session
@@ -97,6 +104,7 @@ def login(request):
{'login_form': login_form,
'next': request.GET.get('next') or request.form.get('next'),
'login_failed': login_failed,
+ 'post_url': request.urlgen('mediagoblin.auth.login'),
'allow_registration': mg_globals.app_config["allow_registration"]})
@@ -115,16 +123,28 @@ def verify_email(request):
you are lucky :)
"""
# If we don't have userid and token parameters, we can't do anything; 404
- if not 'userid' in request.GET or not 'token' in request.GET:
+ if not 'token' in request.GET:
return render_404(request)
- user = User.query.filter_by(id=request.args['userid']).first()
+ # Catch error if token is faked or expired
+ try:
+ token = get_timed_signer_url("mail_verification_token") \
+ .loads(request.GET['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')
+
+ user = User.query.filter_by(id=int(token)).first()
- if user and user.verification_key == unicode(request.GET['token']):
+ if user and user.email_verified is False:
user.status = u'active'
user.email_verified = True
- user.verification_key = None
-
user.save()
messages.add_message(
@@ -166,9 +186,6 @@ def resend_activation(request):
return redirect(request, "mediagoblin.user_pages.user_home", user=request.user['username'])
- request.user.verification_key = unicode(uuid.uuid4())
- request.user.save()
-
email_debug_message(request)
send_verification_email(request.user, request)
@@ -188,13 +205,16 @@ def forgot_password(request):
Sends an email with an url to renew forgotten password.
Use GET querystring parameter 'username' to pre-populate the input field
"""
+ if not 'pass_auth' in request.template_env.globals:
+ return redirect(request, 'index')
+
fp_form = auth_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/auth/forgot_password.html', {'fp_form': fp_form})
+ 'mediagoblin/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
@@ -235,11 +255,6 @@ def forgot_password(request):
# SUCCESS. Send reminder and return to login page
if user:
- user.fp_verification_key = unicode(uuid.uuid4())
- user.fp_token_expire = datetime.datetime.now() + \
- datetime.timedelta(days=10)
- user.save()
-
email_debug_message(request)
send_fp_verification_email(user, request)
@@ -254,31 +269,44 @@ def verify_forgot_password(request):
"""
# get form data variables, and specifically check for presence of token
formdata = _process_for_token(request)
- if not formdata['has_userid_and_token']:
+ if not formdata['has_token']:
return render_404(request)
- formdata_token = formdata['vars']['token']
- formdata_userid = formdata['vars']['userid']
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=formdata_userid).first()
+ user = User.query.filter_by(id=int(token)).first()
+
+ # no user in db
if not user:
- return render_404(request)
+ messages.add_message(
+ request, messages.ERROR,
+ _('The user id is incorrect.'))
+ return redirect(
+ request, 'index')
- # check if we have a real user and correct token
- if ((user and user.fp_verification_key and
- user.fp_verification_key == unicode(formdata_token) and
- datetime.datetime.now() < user.fp_token_expire
- and user.email_verified and user.status == 'active')):
+ # check if user active and has email verified
+ if user.email_verified and user.status == 'active':
cp_form = auth_forms.ChangePassForm(formdata_vars)
if request.method == 'POST' and cp_form.validate():
- user.pw_hash = auth_lib.bcrypt_gen_password_hash(
+ user.pw_hash = auth.gen_password_hash(
cp_form.password.data)
- user.fp_verification_key = None
- user.fp_token_expire = None
user.save()
messages.add_message(
@@ -290,12 +318,22 @@ def verify_forgot_password(request):
return render_to_response(
request,
'mediagoblin/auth/change_fp.html',
- {'cp_form': cp_form})
+ {'cp_form': cp_form,})
- # in case there is a valid id but no user with that id in the db
- # or the token expired
- else:
- return render_404(request)
+ 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):
@@ -313,7 +351,6 @@ def _process_for_token(request):
formdata = {
'vars': formdata_vars,
- 'has_userid_and_token':
- 'userid' in formdata_vars and 'token' in formdata_vars}
+ 'has_token': 'token' in formdata_vars}
return formdata
diff --git a/mediagoblin/config_spec.ini b/mediagoblin/config_spec.ini
index b213970d..12af2f57 100644
--- a/mediagoblin/config_spec.ini
+++ b/mediagoblin/config_spec.ini
@@ -11,6 +11,10 @@ media_types = string_list(default=list("mediagoblin.media_types.image"))
# database stuff
sql_engine = string(default="sqlite:///%(here)s/mediagoblin.db")
+# This flag is used during testing to allow use of in-memory SQLite
+# databases. It is not recommended to be used on a running instance.
+run_migrations = boolean(default=False)
+
# Where temporary files used in processing and etc are kept
workbench_path = string(default="%(here)s/user_dev/media/workbench")
@@ -22,9 +26,10 @@ direct_remote_path = string(default="/mgoblin_static/")
# set to false to enable sending notices
email_debug_mode = boolean(default=True)
+email_smtp_use_ssl = boolean(default=False)
email_sender_address = string(default="notice@mediagoblin.example.org")
email_smtp_host = string(default='')
-email_smtp_port = integer(default=25)
+email_smtp_port = integer(default=0)
email_smtp_user = string(default=None)
email_smtp_pass = string(default=None)
diff --git a/mediagoblin/db/migrations.py b/mediagoblin/db/migrations.py
index 2c553396..fe4ffb3e 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.migration_tools import RegisterMigration, inspect_table
-from mediagoblin.db.models import MediaEntry, Collection, User
+from mediagoblin.db.models import MediaEntry, Collection, User, MediaComment
MIGRATIONS = {}
@@ -287,3 +287,95 @@ def unique_collections_slug(db):
constraint.create()
db.commit()
+
+@RegisterMigration(11, MIGRATIONS)
+def drop_token_related_User_columns(db):
+ """
+ Drop unneeded columns from the User table after switching to using
+ itsdangerous tokens for email and forgot password verification.
+ """
+ metadata = MetaData(bind=db.bind)
+ user_table = inspect_table(metadata, 'core__users')
+
+ verification_key = user_table.columns['verification_key']
+ fp_verification_key = user_table.columns['fp_verification_key']
+ fp_token_expire = user_table.columns['fp_token_expire']
+
+ verification_key.drop()
+ fp_verification_key.drop()
+ fp_token_expire.drop()
+
+ db.commit()
+
+
+class CommentSubscription_v0(declarative_base()):
+ __tablename__ = 'core__comment_subscriptions'
+ id = Column(Integer, primary_key=True)
+
+ created = Column(DateTime, nullable=False, default=datetime.datetime.now)
+
+ media_entry_id = Column(Integer, ForeignKey(MediaEntry.id), nullable=False)
+
+ user_id = Column(Integer, ForeignKey(User.id), nullable=False)
+
+ notify = Column(Boolean, nullable=False, default=True)
+ send_email = Column(Boolean, nullable=False, default=True)
+
+
+class Notification_v0(declarative_base()):
+ __tablename__ = 'core__notifications'
+ id = Column(Integer, primary_key=True)
+ type = Column(Unicode)
+
+ created = Column(DateTime, nullable=False, default=datetime.datetime.now)
+
+ user_id = Column(Integer, ForeignKey(User.id), nullable=False,
+ index=True)
+ seen = Column(Boolean, default=lambda: False, index=True)
+
+
+class CommentNotification_v0(Notification_v0):
+ __tablename__ = 'core__comment_notifications'
+ id = Column(Integer, ForeignKey(Notification_v0.id), primary_key=True)
+
+ subject_id = Column(Integer, ForeignKey(MediaComment.id))
+
+
+class ProcessingNotification_v0(Notification_v0):
+ __tablename__ = 'core__processing_notifications'
+
+ id = Column(Integer, ForeignKey(Notification_v0.id), primary_key=True)
+
+ subject_id = Column(Integer, ForeignKey(MediaEntry.id))
+
+
+@RegisterMigration(12, MIGRATIONS)
+def add_new_notification_tables(db):
+ metadata = MetaData(bind=db.bind)
+
+ user_table = inspect_table(metadata, 'core__users')
+ mediaentry_table = inspect_table(metadata, 'core__media_entries')
+ mediacomment_table = inspect_table(metadata, 'core__media_comments')
+
+ CommentSubscription_v0.__table__.create(db.bind)
+
+ Notification_v0.__table__.create(db.bind)
+ CommentNotification_v0.__table__.create(db.bind)
+ ProcessingNotification_v0.__table__.create(db.bind)
+
+
+@RegisterMigration(13, MIGRATIONS)
+def pw_hash_nullable(db):
+ """Make pw_hash column nullable"""
+ metadata = MetaData(bind=db.bind)
+ user_table = inspect_table(metadata, "core__users")
+
+ user_table.c.pw_hash.alter(nullable=True)
+
+ # sqlite+sqlalchemy seems to drop this constraint during the
+ # migration, so we add it back here for now a bit manually.
+ if db.bind.url.drivername == 'sqlite':
+ constraint = UniqueConstraint('username', table=user_table)
+ constraint.create()
+
+ db.commit()
diff --git a/mediagoblin/db/mixin.py b/mediagoblin/db/mixin.py
index 9f566e36..1b32d838 100644
--- a/mediagoblin/db/mixin.py
+++ b/mediagoblin/db/mixin.py
@@ -31,6 +31,8 @@ import uuid
import re
import datetime
+from datetime import datetime
+
from werkzeug.utils import cached_property
from mediagoblin import mg_globals
@@ -288,6 +290,13 @@ class MediaCommentMixin(object):
"""
return cleaned_markdown_conversion(self.content)
+ def __repr__(self):
+ return '<{klass} #{id} {author} "{comment}">'.format(
+ klass=self.__class__.__name__,
+ id=self.id,
+ author=self.get_author,
+ comment=self.content)
+
class CollectionMixin(GenerateSlugMixin):
def check_slug_used(self, slug):
diff --git a/mediagoblin/db/models.py b/mediagoblin/db/models.py
index 2b925983..826d47ba 100644
--- a/mediagoblin/db/models.py
+++ b/mediagoblin/db/models.py
@@ -24,15 +24,17 @@ import datetime
from sqlalchemy import Column, Integer, Unicode, UnicodeText, DateTime, \
Boolean, ForeignKey, UniqueConstraint, PrimaryKeyConstraint, \
SmallInteger
-from sqlalchemy.orm import relationship, backref
+from sqlalchemy.orm import relationship, backref, with_polymorphic
from sqlalchemy.orm.collections import attribute_mapped_collection
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.base import Base, DictReadAttrProxy
-from mediagoblin.db.mixin import UserMixin, MediaEntryMixin, MediaCommentMixin, CollectionMixin, CollectionItemMixin
+from mediagoblin.db.mixin import UserMixin, MediaEntryMixin, \
+ MediaCommentMixin, CollectionMixin, CollectionItemMixin
from mediagoblin.tools.files import delete_media_files
from mediagoblin.tools.common import import_component
@@ -60,20 +62,17 @@ class User(Base, UserMixin):
# the RFC) and because it would be a mess to implement at this
# point.
email = Column(Unicode, nullable=False)
- created = Column(DateTime, nullable=False, default=datetime.datetime.now)
- pw_hash = Column(Unicode, nullable=False)
+ pw_hash = Column(Unicode)
email_verified = Column(Boolean, default=False)
+ created = Column(DateTime, nullable=False, default=datetime.datetime.now)
status = Column(Unicode, default=u"needs_email_verification", nullable=False)
# Intented to be nullable=False, but migrations would not work for it
# set to nullable=True implicitly.
wants_comment_notification = Column(Boolean, default=True)
license_preference = Column(Unicode)
- verification_key = Column(Unicode)
is_admin = Column(Boolean, default=False, nullable=False)
url = Column(Unicode)
bio = Column(UnicodeText) # ??
- fp_verification_key = Column(Unicode)
- fp_token_expire = Column(DateTime)
## TODO
# plugin data would be in a separate model
@@ -392,6 +391,10 @@ class MediaComment(Base, MediaCommentMixin):
backref=backref("posted_comments",
lazy="dynamic",
cascade="all, delete-orphan"))
+ get_entry = relationship(MediaEntry,
+ backref=backref("comments",
+ lazy="dynamic",
+ cascade="all, delete-orphan"))
# Cascade: Comments are somewhat owned by their MediaEntry.
# So do the full thing.
@@ -484,9 +487,103 @@ class ProcessingMetaData(Base):
return DictReadAttrProxy(self)
+class CommentSubscription(Base):
+ __tablename__ = 'core__comment_subscriptions'
+ id = Column(Integer, primary_key=True)
+
+ created = Column(DateTime, nullable=False, default=datetime.datetime.now)
+
+ media_entry_id = Column(Integer, ForeignKey(MediaEntry.id), nullable=False)
+ media_entry = relationship(MediaEntry,
+ backref=backref('comment_subscriptions',
+ cascade='all, delete-orphan'))
+
+ user_id = Column(Integer, ForeignKey(User.id), nullable=False)
+ user = relationship(User,
+ backref=backref('comment_subscriptions',
+ cascade='all, delete-orphan'))
+
+ notify = Column(Boolean, nullable=False, default=True)
+ send_email = Column(Boolean, nullable=False, default=True)
+
+ def __repr__(self):
+ return ('<{classname} #{id}: {user} {media} notify: '
+ '{notify} email: {email}>').format(
+ id=self.id,
+ classname=self.__class__.__name__,
+ user=self.user,
+ media=self.media_entry,
+ notify=self.notify,
+ email=self.send_email)
+
+
+class Notification(Base):
+ __tablename__ = 'core__notifications'
+ id = Column(Integer, primary_key=True)
+ type = Column(Unicode)
+
+ created = Column(DateTime, nullable=False, default=datetime.datetime.now)
+
+ user_id = Column(Integer, ForeignKey('core__users.id'), nullable=False,
+ index=True)
+ seen = Column(Boolean, default=lambda: False, index=True)
+ user = relationship(
+ User,
+ backref=backref('notifications', cascade='all, delete-orphan'))
+
+ __mapper_args__ = {
+ 'polymorphic_identity': 'notification',
+ 'polymorphic_on': type
+ }
+
+ def __repr__(self):
+ return '<{klass} #{id}: {user}: {subject} ({seen})>'.format(
+ id=self.id,
+ klass=self.__class__.__name__,
+ user=self.user,
+ subject=getattr(self, 'subject', None),
+ seen='unseen' if not self.seen else 'seen')
+
+
+class CommentNotification(Notification):
+ __tablename__ = 'core__comment_notifications'
+ id = Column(Integer, ForeignKey(Notification.id), primary_key=True)
+
+ subject_id = Column(Integer, ForeignKey(MediaComment.id))
+ subject = relationship(
+ MediaComment,
+ backref=backref('comment_notifications', cascade='all, delete-orphan'))
+
+ __mapper_args__ = {
+ 'polymorphic_identity': 'comment_notification'
+ }
+
+
+class ProcessingNotification(Notification):
+ __tablename__ = 'core__processing_notifications'
+
+ id = Column(Integer, ForeignKey(Notification.id), primary_key=True)
+
+ subject_id = Column(Integer, ForeignKey(MediaEntry.id))
+ subject = relationship(
+ MediaEntry,
+ backref=backref('processing_notifications',
+ cascade='all, delete-orphan'))
+
+ __mapper_args__ = {
+ 'polymorphic_identity': 'processing_notification'
+ }
+
+
+with_polymorphic(
+ Notification,
+ [ProcessingNotification, CommentNotification])
+
MODELS = [
- User, MediaEntry, Tag, MediaTag, MediaComment, Collection, CollectionItem, MediaFile, FileKeynames,
- MediaAttachmentFile, ProcessingMetaData]
+ User, MediaEntry, Tag, MediaTag, MediaComment, Collection, CollectionItem,
+ MediaFile, FileKeynames, MediaAttachmentFile, ProcessingMetaData,
+ Notification, CommentNotification, ProcessingNotification,
+ CommentSubscription]
######################################################
diff --git a/mediagoblin/db/models_v0.py b/mediagoblin/db/models_v0.py
index ec51a1f5..bdedec2e 100644
--- a/mediagoblin/db/models_v0.py
+++ b/mediagoblin/db/models_v0.py
@@ -18,6 +18,29 @@
TODO: indexes on foreignkeys, where useful.
"""
+###########################################################################
+# WHAT IS THIS FILE?
+# ------------------
+#
+# Upon occasion, someone runs into this file and wonders why we have
+# both a models.py and a models_v0.py.
+#
+# The short of it is: you can ignore this file.
+#
+# The long version is, in two parts:
+#
+# - We used to use MongoDB, then we switched to SQL and SQLAlchemy.
+# We needed to convert peoples' databases; the script we had would
+# switch them to the first version right after Mongo, convert over
+# all their tables, then run any migrations that were added after.
+#
+# - That script is now removed, but there is some discussion of
+# writing a test that would set us at the first SQL migration and
+# run everything after. If we wrote that, this file would still be
+# useful. But for now, it's legacy!
+#
+###########################################################################
+
import datetime
import sys
diff --git a/mediagoblin/decorators.py b/mediagoblin/decorators.py
index f3535fcf..ece222f5 100644
--- a/mediagoblin/decorators.py
+++ b/mediagoblin/decorators.py
@@ -18,11 +18,12 @@ from functools import wraps
from urlparse import urljoin
from werkzeug.exceptions import Forbidden, NotFound
-from werkzeug.urls import url_quote
from mediagoblin import mg_globals as mgg
+from mediagoblin import messages
from mediagoblin.db.models import MediaEntry, User
from mediagoblin.tools.response import redirect, render_404
+from mediagoblin.tools.translate import pass_to_ugettext as _
def require_active_login(controller):
@@ -235,3 +236,35 @@ def get_workbench(func):
return func(*args, workbench=workbench, **kwargs)
return new_func
+
+
+def allow_registration(controller):
+ """ Decorator for if registration is enabled"""
+ @wraps(controller)
+ def wrapper(request, *args, **kwargs):
+ if not mgg.app_config["allow_registration"]:
+ messages.add_message(
+ request,
+ messages.WARNING,
+ _('Sorry, registration is disabled on this instance.'))
+ return redirect(request, "index")
+
+ return controller(request, *args, **kwargs)
+
+ return wrapper
+
+
+def auth_enabled(controller):
+ """Decorator for if an auth plugin is enabled"""
+ @wraps(controller)
+ def wrapper(request, *args, **kwargs):
+ if not mgg.app.auth:
+ messages.add_message(
+ request,
+ messages.WARNING,
+ _('Sorry, authentication is disabled on this instance.'))
+ return redirect(request, 'index')
+
+ return controller(request, *args, **kwargs)
+
+ return wrapper
diff --git a/mediagoblin/edit/forms.py b/mediagoblin/edit/forms.py
index 3b2486de..e0147a0c 100644
--- a/mediagoblin/edit/forms.py
+++ b/mediagoblin/edit/forms.py
@@ -16,9 +16,11 @@
import wtforms
-from mediagoblin.tools.text import tag_length_validator, TOO_LONG_TAG_WARNING
+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
+
class EditForm(wtforms.Form):
title = wtforms.TextField(
@@ -59,6 +61,13 @@ class EditProfileForm(wtforms.Form):
class EditAccountForm(wtforms.Form):
+ new_email = wtforms.TextField(
+ _('New email address'),
+ [wtforms.validators.Optional(),
+ normalize_user_or_email_field(allow_user=False)])
+ wants_comment_notification = wtforms.BooleanField(
+ label='',
+ description=_("Email me when others comment on my media"))
license_preference = wtforms.SelectField(
_('License preference'),
[
@@ -67,8 +76,6 @@ class EditAccountForm(wtforms.Form):
],
choices=licenses_as_choices(),
description=_('This will be your default license on upload forms.'))
- wants_comment_notification = wtforms.BooleanField(
- label=_("Email me when others comment on my media"))
class EditAttachmentsForm(wtforms.Form):
diff --git a/mediagoblin/edit/routing.py b/mediagoblin/edit/routing.py
index 622729ac..3592f708 100644
--- a/mediagoblin/edit/routing.py
+++ b/mediagoblin/edit/routing.py
@@ -26,3 +26,5 @@ 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')
diff --git a/mediagoblin/edit/views.py b/mediagoblin/edit/views.py
index 508c380d..7a8d6185 100644
--- a/mediagoblin/edit/views.py
+++ b/mediagoblin/edit/views.py
@@ -16,25 +16,31 @@
from datetime import datetime
+from itsdangerous import BadSignature
from werkzeug.exceptions import Forbidden
from werkzeug.utils import secure_filename
from mediagoblin import messages
from mediagoblin import mg_globals
-from mediagoblin.auth import lib as auth_lib
+from mediagoblin import auth
+from mediagoblin.auth import 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,
- get_media_entry_by_id,
- user_may_alter_collection, get_user_collection)
-from mediagoblin.tools.response import render_to_response, \
- redirect, redirect_obj
+ get_media_entry_by_id, user_may_alter_collection,
+ get_user_collection)
+from mediagoblin.tools.crypto import get_timed_signer_url
+from mediagoblin.tools.mail import email_debug_message
+from mediagoblin.tools.response import (render_to_response,
+ redirect, redirect_obj, render_404)
from mediagoblin.tools.translate import pass_to_ugettext as _
+from mediagoblin.tools.template import render_template
from mediagoblin.tools.text import (
convert_to_tag_list_of_dicts, media_tags_as_string)
from mediagoblin.tools.url import slugify
from mediagoblin.db.util import check_media_slug_used, check_collection_slug_used
+from mediagoblin.db.models import User
import mimetypes
@@ -212,6 +218,10 @@ def edit_profile(request, url_user=None):
{'user': user,
'form': form})
+EMAIL_VERIFICATION_TEMPLATE = (
+ u'{uri}?'
+ u'token={verification_key}')
+
@require_active_login
def edit_account(request):
@@ -220,27 +230,22 @@ def edit_account(request):
wants_comment_notification=user.wants_comment_notification,
license_preference=user.license_preference)
- if request.method == 'POST':
- form_validated = form.validate()
+ if request.method == 'POST' and form.validate():
+ user.wants_comment_notification = form.wants_comment_notification.data
- if form_validated and \
- form.wants_comment_notification.validate(form):
- user.wants_comment_notification = \
- form.wants_comment_notification.data
+ user.license_preference = form.license_preference.data
- if form_validated and \
- form.license_preference.validate(form):
- user.license_preference = \
- form.license_preference.data
+ if form.new_email.data:
+ _update_email(request, form, user)
- if form_validated and not form.errors:
+ if not form.errors:
user.save()
messages.add_message(request,
- messages.SUCCESS,
- _("Account settings saved"))
+ messages.SUCCESS,
+ _("Account settings saved"))
return redirect(request,
- 'mediagoblin.user_pages.user_home',
- user=user.username)
+ 'mediagoblin.user_pages.user_home',
+ user=user.username)
return render_to_response(
request,
@@ -337,12 +342,16 @@ def edit_collection(request, collection):
@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_lib.bcrypt_check_password(
+ if not auth.check_password(
form.old_password.data, user.pw_hash):
form.old_password.errors.append(
_('Wrong password'))
@@ -354,7 +363,7 @@ def change_pass(request):
'user': user})
# Password matches
- user.pw_hash = auth_lib.bcrypt_gen_password_hash(
+ user.pw_hash = auth.gen_password_hash(
form.new_password.data)
user.save()
@@ -369,3 +378,77 @@ def change_pass(request):
'mediagoblin/edit/change_pass.html',
{'form': form,
'user': user})
+
+
+def verify_email(request):
+ """
+ Email verification view for changing email address
+ """
+ # If no token, we can't do anything
+ if not 'token' in request.GET:
+ return render_404(request)
+
+ # Catch error if token is faked or expired
+ token = None
+ try:
+ token = get_timed_signer_url("mail_verification_token") \
+ .loads(request.GET['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')
+
+ user = User.query.filter_by(id=int(token['user'])).first()
+
+ if user:
+ user.email = token['email']
+ user.save()
+
+ messages.add_message(
+ request,
+ messages.SUCCESS,
+ _('Your email address has been verified.'))
+
+ else:
+ messages.add_message(
+ request,
+ messages.ERROR,
+ _('The verification key or user id is incorrect.'))
+
+ return redirect(
+ request, 'mediagoblin.user_pages.user_home',
+ user=user.username)
+
+
+def _update_email(request, form, user):
+ new_email = form.new_email.data
+ users_with_email = User.query.filter_by(
+ email=new_email).count()
+
+ if users_with_email:
+ form.new_email.errors.append(
+ _('Sorry, a user with that email address'
+ ' already exists.'))
+
+ elif not users_with_email:
+ verification_key = get_timed_signer_url(
+ 'mail_verification_token').dumps({
+ 'user': user.id,
+ 'email': new_email})
+
+ rendered_email = render_template(
+ request, 'mediagoblin/edit/verification.txt',
+ {'username': user.username,
+ 'verification_url': EMAIL_VERIFICATION_TEMPLATE.format(
+ uri=request.urlgen('mediagoblin.edit.verify_email',
+ qualified=True),
+ verification_key=verification_key)})
+
+ email_debug_message(request)
+ auth_tools.send_verification_email(user, request, new_email,
+ rendered_email)
diff --git a/mediagoblin/gmg_commands/dbupdate.py b/mediagoblin/gmg_commands/dbupdate.py
index fa25ecb2..22ad426c 100644
--- a/mediagoblin/gmg_commands/dbupdate.py
+++ b/mediagoblin/gmg_commands/dbupdate.py
@@ -110,14 +110,26 @@ def run_dbupdate(app_config, global_config):
in the future, plugins)
"""
+ # Set up the database
+ db = setup_connection_and_db_from_config(app_config, migrations=True)
+ #Run the migrations
+ run_all_migrations(db, app_config, global_config)
+
+
+def run_all_migrations(db, app_config, global_config):
+ """
+ Initializes or migrates a database that already has a
+ connection setup and also initializes or migrates all
+ extensions based on the config files.
+
+ It can be used to initialize an in-memory database for
+ testing.
+ """
# Gather information from all media managers / projects
dbdatas = gather_database_data(
app_config['media_types'],
global_config.get('plugins', {}).keys())
- # Set up the database
- db = setup_connection_and_db_from_config(app_config, migrations=True)
-
Session = sessionmaker(bind=db.engine)
# Setup media managers for all dbdata, run init/migrate and print info
diff --git a/mediagoblin/gmg_commands/users.py b/mediagoblin/gmg_commands/users.py
index 024c8498..1f329459 100644
--- a/mediagoblin/gmg_commands/users.py
+++ b/mediagoblin/gmg_commands/users.py
@@ -15,7 +15,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from mediagoblin.gmg_commands import util as commands_util
-from mediagoblin.auth import lib as auth_lib
+from mediagoblin import auth
from mediagoblin import mg_globals
def adduser_parser_setup(subparser):
@@ -52,7 +52,7 @@ def adduser(args):
entry = db.User()
entry.username = unicode(args.username.lower())
entry.email = unicode(args.email)
- entry.pw_hash = auth_lib.bcrypt_gen_password_hash(args.password)
+ entry.pw_hash = auth.gen_password_hash(args.password)
entry.status = u'active'
entry.email_verified = True
entry.save()
@@ -96,7 +96,7 @@ def changepw(args):
user = db.User.one({'username': unicode(args.username.lower())})
if user:
- user.pw_hash = auth_lib.bcrypt_gen_password_hash(args.password)
+ user.pw_hash = auth.gen_password_hash(args.password)
user.save()
print 'Password successfully changed'
else:
diff --git a/mediagoblin/init/__init__.py b/mediagoblin/init/__init__.py
index 444c624f..e0711416 100644
--- a/mediagoblin/init/__init__.py
+++ b/mediagoblin/init/__init__.py
@@ -58,16 +58,20 @@ def setup_global_and_app_config(config_path):
return global_config, app_config
-def setup_database():
+def setup_database(run_migrations=False):
app_config = mg_globals.app_config
+ global_config = mg_globals.global_config
# Load all models for media types (plugins, ...)
load_models(app_config)
-
# Set up the database
- db = setup_connection_and_db_from_config(app_config)
-
- check_db_migrations_current(db)
+ db = setup_connection_and_db_from_config(app_config, run_migrations)
+ if run_migrations:
+ #Run the migrations to initialize/update the database.
+ from mediagoblin.gmg_commands.dbupdate import run_all_migrations
+ run_all_migrations(db, app_config, global_config)
+ else:
+ check_db_migrations_current(db)
setup_globals(database=db)
diff --git a/mediagoblin/init/celery/__init__.py b/mediagoblin/init/celery/__init__.py
index 169cc935..57242bf6 100644
--- a/mediagoblin/init/celery/__init__.py
+++ b/mediagoblin/init/celery/__init__.py
@@ -16,12 +16,18 @@
import os
import sys
+import logging
from celery import Celery
from mediagoblin.tools.pluginapi import hook_runall
-MANDATORY_CELERY_IMPORTS = ['mediagoblin.processing.task']
+_log = logging.getLogger(__name__)
+
+
+MANDATORY_CELERY_IMPORTS = [
+ 'mediagoblin.processing.task',
+ 'mediagoblin.notifications.task']
DEFAULT_SETTINGS_MODULE = 'mediagoblin.init.celery.dummy_settings_module'
@@ -97,3 +103,13 @@ def setup_celery_from_config(app_config, global_config,
if set_environ:
os.environ['CELERY_CONFIG_MODULE'] = settings_module
+
+ # Replace the default celery.current_app.conf if celery has already been
+ # initiated
+ from celery import current_app
+
+ _log.info('Setting celery configuration from object "{0}"'.format(
+ settings_module))
+ current_app.config_from_object(this_module)
+
+ _log.debug('Celery broker host: {0}'.format(current_app.conf['BROKER_HOST']))
diff --git a/mediagoblin/meddleware/csrf.py b/mediagoblin/meddleware/csrf.py
index 661f0ba2..44d42d75 100644
--- a/mediagoblin/meddleware/csrf.py
+++ b/mediagoblin/meddleware/csrf.py
@@ -111,7 +111,7 @@ class CsrfMeddleware(BaseMeddleware):
httponly=True)
# update the Vary header
- response.vary = (getattr(response, 'vary', None) or []) + ['Cookie']
+ response.vary = list(getattr(response, 'vary', None) or []) + ['Cookie']
def _make_token(self, request):
"""Generate a new token to use for CSRF protection."""
diff --git a/mediagoblin/media_types/stl/processing.py b/mediagoblin/media_types/stl/processing.py
index 49382495..ce7a5d37 100644
--- a/mediagoblin/media_types/stl/processing.py
+++ b/mediagoblin/media_types/stl/processing.py
@@ -46,7 +46,7 @@ def sniff_handler(media_file, **kw):
if kw.get('media') is not None:
name, ext = os.path.splitext(kw['media'].filename)
clean_ext = ext[1:].lower()
-
+
if clean_ext in SUPPORTED_FILETYPES:
_log.info('Found file extension in supported filetypes')
return True
diff --git a/mediagoblin/media_types/video/transcoders.py b/mediagoblin/media_types/video/transcoders.py
index 90a767dd..9d6b7655 100644
--- a/mediagoblin/media_types/video/transcoders.py
+++ b/mediagoblin/media_types/video/transcoders.py
@@ -22,9 +22,15 @@ import logging
import urllib
import multiprocessing
import gobject
+
+old_argv = sys.argv
+sys.argv = []
+
import pygst
pygst.require('0.10')
import gst
+
+sys.argv = old_argv
import struct
try:
from PIL import Image
diff --git a/mediagoblin/notifications/__init__.py b/mediagoblin/notifications/__init__.py
new file mode 100644
index 00000000..4b7fbb8c
--- /dev/null
+++ b/mediagoblin/notifications/__init__.py
@@ -0,0 +1,141 @@
+# 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 logging
+
+from mediagoblin.db.models import Notification, \
+ CommentNotification, CommentSubscription
+from mediagoblin.notifications.task import email_notification_task
+from mediagoblin.notifications.tools import generate_comment_message
+
+_log = logging.getLogger(__name__)
+
+def trigger_notification(comment, media_entry, request):
+ '''
+ Send out notifications about a new comment.
+ '''
+ subscriptions = CommentSubscription.query.filter_by(
+ media_entry_id=media_entry.id).all()
+
+ for subscription in subscriptions:
+ if not subscription.notify:
+ continue
+
+ if comment.get_author == subscription.user:
+ continue
+
+ cn = CommentNotification(
+ user_id=subscription.user_id,
+ subject_id=comment.id)
+
+ cn.save()
+
+ if subscription.send_email:
+ message = generate_comment_message(
+ subscription.user,
+ comment,
+ media_entry,
+ request)
+
+ email_notification_task.apply_async([cn.id, message])
+
+
+def mark_notification_seen(notification):
+ if notification:
+ notification.seen = True
+ notification.save()
+
+
+def mark_comment_notification_seen(comment_id, user):
+ notification = CommentNotification.query.filter_by(
+ user_id=user.id,
+ subject_id=comment_id).first()
+
+ _log.debug('Marking {0} as seen.'.format(notification))
+
+ mark_notification_seen(notification)
+
+
+def get_comment_subscription(user_id, media_entry_id):
+ return CommentSubscription.query.filter_by(
+ user_id=user_id,
+ media_entry_id=media_entry_id).first()
+
+def add_comment_subscription(user, media_entry):
+ '''
+ Create a comment subscription for a User on a MediaEntry.
+
+ Uses the User's wants_comment_notification to set email notifications for
+ the subscription to enabled/disabled.
+ '''
+ cn = get_comment_subscription(user.id, media_entry.id)
+
+ if not cn:
+ cn = CommentSubscription(
+ user_id=user.id,
+ media_entry_id=media_entry.id)
+
+ cn.notify = True
+
+ if not user.wants_comment_notification:
+ cn.send_email = False
+
+ cn.save()
+
+
+def silence_comment_subscription(user, media_entry):
+ '''
+ Silence a subscription so that the user is never notified in any way about
+ new comments on an entry
+ '''
+ cn = get_comment_subscription(user.id, media_entry.id)
+
+ if cn:
+ cn.notify = False
+ cn.send_email = False
+ cn.save()
+
+
+def remove_comment_subscription(user, media_entry):
+ cn = get_comment_subscription(user.id, media_entry.id)
+
+ if cn:
+ cn.delete()
+
+
+NOTIFICATION_FETCH_LIMIT = 100
+
+
+def get_notifications(user_id, only_unseen=True):
+ query = Notification.query.filter_by(user_id=user_id)
+
+ if only_unseen:
+ query = query.filter_by(seen=False)
+
+ notifications = query.limit(
+ NOTIFICATION_FETCH_LIMIT).all()
+
+ return notifications
+
+def get_notification_count(user_id, only_unseen=True):
+ query = Notification.query.filter_by(user_id=user_id)
+
+ if only_unseen:
+ query = query.filter_by(seen=False)
+
+ count = query.count()
+
+ return count
diff --git a/mediagoblin/notifications/routing.py b/mediagoblin/notifications/routing.py
new file mode 100644
index 00000000..e57956d3
--- /dev/null
+++ b/mediagoblin/notifications/routing.py
@@ -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/>.
+
+from mediagoblin.tools.routing import add_route
+
+add_route('mediagoblin.notifications.subscribe_comments',
+ '/u/<string:user>/m/<string:media>/notifications/subscribe/comments/',
+ 'mediagoblin.notifications.views:subscribe_comments')
+
+add_route('mediagoblin.notifications.silence_comments',
+ '/u/<string:user>/m/<string:media>/notifications/silence/',
+ 'mediagoblin.notifications.views:silence_comments')
diff --git a/mediagoblin/notifications/task.py b/mediagoblin/notifications/task.py
new file mode 100644
index 00000000..52573b57
--- /dev/null
+++ b/mediagoblin/notifications/task.py
@@ -0,0 +1,46 @@
+# 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 logging
+
+from celery import registry
+from celery.task import Task
+
+from mediagoblin.tools.mail import send_email
+from mediagoblin.db.models import CommentNotification
+
+
+_log = logging.getLogger(__name__)
+
+
+class EmailNotificationTask(Task):
+ '''
+ Celery notification task.
+
+ This task is executed by celeryd to offload long-running operations from
+ the web server.
+ '''
+ def run(self, notification_id, message):
+ cn = CommentNotification.query.filter_by(id=notification_id).first()
+ _log.info('Sending notification email about {0}'.format(cn))
+
+ return send_email(
+ message['from'],
+ [message['to']],
+ message['subject'],
+ message['body'])
+
+email_notification_task = registry.tasks[EmailNotificationTask.name]
diff --git a/mediagoblin/notifications/tools.py b/mediagoblin/notifications/tools.py
new file mode 100644
index 00000000..25432780
--- /dev/null
+++ b/mediagoblin/notifications/tools.py
@@ -0,0 +1,55 @@
+# 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.tools.template import render_template
+from mediagoblin.tools.translate import pass_to_ugettext as _
+from mediagoblin import mg_globals
+
+def generate_comment_message(user, comment, media, request):
+ """
+ Sends comment email to user when a comment is made on their media.
+
+ Args:
+ - user: the user object to whom the email is sent
+ - comment: the comment object referencing user's media
+ - media: the media object the comment is about
+ - request: the request
+ """
+
+ comment_url = request.urlgen(
+ 'mediagoblin.user_pages.media_home.view_comment',
+ comment=comment.id,
+ user=media.get_uploader.username,
+ media=media.slug_or_id,
+ qualified=True) + '#comment'
+
+ comment_author = comment.get_author.username
+
+ rendered_email = render_template(
+ request, 'mediagoblin/user_pages/comment_email.txt',
+ {'username': user.username,
+ 'comment_author': comment_author,
+ 'comment_content': comment.content,
+ 'comment_url': comment_url})
+
+ return {
+ 'from': mg_globals.app_config['email_sender_address'],
+ 'to': user.email,
+ 'subject': '{instance_title} - {comment_author} '.format(
+ comment_author=comment_author,
+ instance_title=mg_globals.app_config['html_title']) \
+ + _('commented on your post'),
+ 'body': rendered_email}
diff --git a/mediagoblin/notifications/views.py b/mediagoblin/notifications/views.py
new file mode 100644
index 00000000..d275bc92
--- /dev/null
+++ b/mediagoblin/notifications/views.py
@@ -0,0 +1,54 @@
+# 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.tools.response import render_to_response, render_404, redirect
+from mediagoblin.tools.translate import pass_to_ugettext as _
+from mediagoblin.decorators import (uses_pagination, get_user_media_entry,
+ get_media_entry_by_id,
+ require_active_login, user_may_delete_media, user_may_alter_collection,
+ get_user_collection, get_user_collection_item, active_user_from_url)
+
+from mediagoblin import messages
+
+from mediagoblin.notifications import add_comment_subscription, \
+ silence_comment_subscription
+
+from werkzeug.exceptions import BadRequest
+
+@get_user_media_entry
+@require_active_login
+def subscribe_comments(request, media):
+
+ add_comment_subscription(request.user, media)
+
+ messages.add_message(request,
+ messages.SUCCESS,
+ _('Subscribed to comments on %s!')
+ % media.title)
+
+ return redirect(request, location=media.url_for_self(request.urlgen))
+
+@get_user_media_entry
+@require_active_login
+def silence_comments(request, media):
+ silence_comment_subscription(request.user, media)
+
+ messages.add_message(request,
+ messages.SUCCESS,
+ _('You will not receive notifications for comments on'
+ ' %s.') % media.title)
+
+ return redirect(request, location=media.url_for_self(request.urlgen))
diff --git a/mediagoblin/plugins/basic_auth/__init__.py b/mediagoblin/plugins/basic_auth/__init__.py
new file mode 100644
index 00000000..c16d8855
--- /dev/null
+++ b/mediagoblin/plugins/basic_auth/__init__.py
@@ -0,0 +1,85 @@
+# 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.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_
+
+
+def setup_plugin():
+ config = pluginapi.get_config('mediagoblin.plugins.basic_auth')
+
+
+def get_user(**kwargs):
+ username = kwargs.pop('username', None)
+ if username:
+ user = User.query.filter(
+ or_(
+ User.username == username,
+ User.email == username,
+ )).first()
+ return user
+
+
+def create_user(registration_form):
+ user = get_user(username=registration_form.username.data)
+ if not user and 'password' in registration_form:
+ user = create_basic_user(registration_form)
+ user.pw_hash = gen_password_hash(
+ registration_form.password.data)
+ user.save()
+ return user
+
+
+def get_login_form(request):
+ return auth_forms.LoginForm(request.form)
+
+
+def get_registration_form(request):
+ return auth_forms.RegistrationForm(request.form)
+
+
+def gen_password_hash(raw_pass, extra_salt=None):
+ return auth_tools.bcrypt_gen_password_hash(raw_pass, extra_salt)
+
+
+def check_password(raw_pass, stored_hash, extra_salt=None):
+ return auth_tools.bcrypt_check_password(raw_pass, stored_hash, extra_salt)
+
+
+def auth():
+ return True
+
+
+def append_to_global_context(context):
+ context['pass_auth'] = True
+ return context
+
+
+hooks = {
+ 'setup': setup_plugin,
+ 'authentication': auth,
+ 'auth_get_user': get_user,
+ 'auth_create_user': create_user,
+ 'auth_get_login_form': get_login_form,
+ 'auth_get_registration_form': get_registration_form,
+ 'auth_gen_password_hash': gen_password_hash,
+ 'auth_check_password': check_password,
+ 'auth_fake_login_attempt': auth_tools.fake_login_attempt,
+ 'template_global_context': append_to_global_context,
+}
diff --git a/mediagoblin/plugins/basic_auth/forms.py b/mediagoblin/plugins/basic_auth/forms.py
new file mode 100644
index 00000000..72d99dff
--- /dev/null
+++ b/mediagoblin/plugins/basic_auth/forms.py
@@ -0,0 +1,43 @@
+# 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 wtforms
+
+from mediagoblin.tools.translate import lazy_pass_to_ugettext as _
+from mediagoblin.auth.tools import normalize_user_or_email_field
+
+
+class RegistrationForm(wtforms.Form):
+ username = wtforms.TextField(
+ _('Username'),
+ [wtforms.validators.Required(),
+ normalize_user_or_email_field(allow_email=False)])
+ password = wtforms.PasswordField(
+ _('Password'),
+ [wtforms.validators.Required(),
+ wtforms.validators.Length(min=5, max=1024)])
+ email = wtforms.TextField(
+ _('Email address'),
+ [wtforms.validators.Required(),
+ normalize_user_or_email_field(allow_user=False)])
+
+
+class LoginForm(wtforms.Form):
+ username = wtforms.TextField(
+ _('Username or Email'),
+ [wtforms.validators.Required(),
+ normalize_user_or_email_field()])
+ password = wtforms.PasswordField(
+ _('Password'))
diff --git a/mediagoblin/auth/lib.py b/mediagoblin/plugins/basic_auth/tools.py
index bfc36b28..1300bb9a 100644
--- a/mediagoblin/auth/lib.py
+++ b/mediagoblin/plugins/basic_auth/tools.py
@@ -13,14 +13,8 @@
#
# 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 random
-
import bcrypt
-
-from mediagoblin.tools.mail import send_email
-from mediagoblin.tools.template import render_template
-from mediagoblin import mg_globals
+import random
def bcrypt_check_password(raw_pass, stored_hash, extra_salt=None):
@@ -88,33 +82,3 @@ def fake_login_attempt():
randplus_hashed_pass = bcrypt.hashpw(hashed_pass, rand_salt)
randplus_stored_hash == randplus_hashed_pass
-
-
-EMAIL_FP_VERIFICATION_TEMPLATE = (
- u"http://{host}{uri}?"
- u"userid={userid}&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
- """
- rendered_email = render_template(
- request, 'mediagoblin/auth/fp_verification_email.txt',
- {'username': user.username,
- 'verification_url': EMAIL_FP_VERIFICATION_TEMPLATE.format(
- host=request.host,
- uri=request.urlgen('mediagoblin.auth.verify_forgot_password'),
- userid=unicode(user.id),
- fp_verification_key=user.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/openid/__init__.py b/mediagoblin/plugins/openid/__init__.py
new file mode 100644
index 00000000..ee88808c
--- /dev/null
+++ b/mediagoblin/plugins/openid/__init__.py
@@ -0,0 +1,123 @@
+# 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
+import uuid
+
+from sqlalchemy import or_
+
+from mediagoblin.auth.tools import create_basic_user
+from mediagoblin.db.models import User
+from mediagoblin.plugins.openid.models import OpenIDUserURL
+from mediagoblin.tools import pluginapi
+from mediagoblin.tools.translate import lazy_pass_to_ugettext as _
+
+PLUGIN_DIR = os.path.dirname(__file__)
+
+
+def setup_plugin():
+ config = pluginapi.get_config('mediagoblin.plugins.openid')
+
+ routes = [
+ ('mediagoblin.plugins.openid.register',
+ '/auth/openid/register/',
+ 'mediagoblin.plugins.openid.views:register'),
+ ('mediagoblin.plugins.openid.login',
+ '/auth/openid/login/',
+ 'mediagoblin.plugins.openid.views:login'),
+ ('mediagoblin.plugins.openid.finish_login',
+ '/auth/openid/login/finish/',
+ 'mediagoblin.plugins.openid.views:finish_login'),
+ ('mediagoblin.plugins.openid.edit',
+ '/edit/openid/',
+ 'mediagoblin.plugins.openid.views:start_edit'),
+ ('mediagoblin.plugins.openid.finish_edit',
+ '/edit/openid/finish/',
+ 'mediagoblin.plugins.openid.views:finish_edit'),
+ ('mediagoblin.plugins.openid.delete',
+ '/edit/openid/delete/',
+ 'mediagoblin.plugins.openid.views:delete_openid'),
+ ('mediagoblin.plugins.openid.finish_delete',
+ '/edit/openid/delete/finish/',
+ 'mediagoblin.plugins.openid.views:finish_delete')]
+
+ pluginapi.register_routes(routes)
+ pluginapi.register_template_path(os.path.join(PLUGIN_DIR, 'templates'))
+
+ pluginapi.register_template_hooks(
+ {'register_link': 'mediagoblin/plugins/openid/register_link.html',
+ 'login_link': 'mediagoblin/plugins/openid/login_link.html',
+ 'edit_link': 'mediagoblin/plugins/openid/edit_link.html'})
+
+
+def create_user(register_form):
+ if 'openid' in register_form:
+ username = register_form.username.data
+ user = User.query.filter(
+ or_(
+ User.username == username,
+ User.email == username,
+ )).first()
+
+ if not user:
+ user = create_basic_user(register_form)
+
+ new_entry = OpenIDUserURL()
+ new_entry.openid_url = register_form.openid.data
+ new_entry.user_id = user.id
+ new_entry.save()
+
+ return user
+
+
+def extra_validation(register_form):
+ openid = register_form.openid.data if 'openid' in \
+ register_form else None
+ if openid:
+ openid_url_exists = OpenIDUserURL.query.filter_by(
+ openid_url=openid
+ ).count()
+
+ extra_validation_passes = True
+
+ if openid_url_exists:
+ register_form.openid.errors.append(
+ _('Sorry, an account is already registered to that OpenID.'))
+ extra_validation_passes = False
+
+ return extra_validation_passes
+
+
+def no_pass_redirect():
+ return 'openid'
+
+
+def add_to_form_context(context):
+ context['openid_link'] = True
+ return context
+
+
+def Auth():
+ return True
+
+hooks = {
+ 'setup': setup_plugin,
+ 'authentication': Auth,
+ 'auth_extra_validation': extra_validation,
+ 'auth_create_user': create_user,
+ 'auth_no_pass_redirect': no_pass_redirect,
+ ('mediagoblin.auth.register',
+ 'mediagoblin/auth/register.html'): add_to_form_context,
+}
diff --git a/mediagoblin/plugins/openid/forms.py b/mediagoblin/plugins/openid/forms.py
new file mode 100644
index 00000000..f26024bd
--- /dev/null
+++ b/mediagoblin/plugins/openid/forms.py
@@ -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/>.
+import wtforms
+
+from mediagoblin.tools.translate import lazy_pass_to_ugettext as _
+from mediagoblin.auth.tools import normalize_user_or_email_field
+
+
+class RegistrationForm(wtforms.Form):
+ openid = wtforms.HiddenField(
+ '',
+ [wtforms.validators.Required()])
+ username = wtforms.TextField(
+ _('Username'),
+ [wtforms.validators.Required(),
+ 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 LoginForm(wtforms.Form):
+ openid = wtforms.TextField(
+ _('OpenID'),
+ [wtforms.validators.Required(),
+ # Can openid's only be urls?
+ wtforms.validators.URL(message='Please enter a valid url.')])
diff --git a/mediagoblin/plugins/openid/models.py b/mediagoblin/plugins/openid/models.py
new file mode 100644
index 00000000..6773f0ad
--- /dev/null
+++ b/mediagoblin/plugins/openid/models.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/>.
+from sqlalchemy import Column, Integer, Unicode, ForeignKey
+from sqlalchemy.orm import relationship, backref
+
+from mediagoblin.db.models import User
+from mediagoblin.db.base import Base
+
+
+class OpenIDUserURL(Base):
+ __tablename__ = "openid__user_urls"
+
+ id = Column(Integer, primary_key=True)
+ openid_url = Column(Unicode, nullable=False)
+ user_id = Column(Integer, ForeignKey(User.id), nullable=False)
+
+ # OpenID's are owned by their user, so do the full thing.
+ user = relationship(User, backref=backref('openid_urls',
+ cascade='all, delete-orphan'))
+
+
+# OpenID Store Models
+class Nonce(Base):
+ __tablename__ = "openid__nonce"
+
+ server_url = Column(Unicode, primary_key=True)
+ timestamp = Column(Integer, primary_key=True)
+ salt = Column(Unicode, primary_key=True)
+
+ def __unicode__(self):
+ return u'Nonce: %r, %r' % (self.server_url, self.salt)
+
+
+class Association(Base):
+ __tablename__ = "openid__association"
+
+ server_url = Column(Unicode, primary_key=True)
+ handle = Column(Unicode, primary_key=True)
+ secret = Column(Unicode)
+ issued = Column(Integer)
+ lifetime = Column(Integer)
+ assoc_type = Column(Unicode)
+
+ def __unicode__(self):
+ return u'Association: %r, %r' % (self.server_url, self.handle)
+
+
+MODELS = [
+ OpenIDUserURL,
+ Nonce,
+ Association
+]
diff --git a/mediagoblin/plugins/openid/store.py b/mediagoblin/plugins/openid/store.py
new file mode 100644
index 00000000..8f9a7012
--- /dev/null
+++ b/mediagoblin/plugins/openid/store.py
@@ -0,0 +1,127 @@
+# 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 base64
+import time
+
+from openid.association import Association as OIDAssociation
+from openid.store.interface import OpenIDStore
+from openid.store import nonce
+
+from mediagoblin.plugins.openid.models import Association, Nonce
+
+
+class SQLAlchemyOpenIDStore(OpenIDStore):
+ def __init__(self):
+ self.max_nonce_age = 6 * 60 * 60
+
+ def storeAssociation(self, server_url, association):
+ assoc = Association.query.filter_by(
+ server_url=server_url, handle=association.handle
+ ).first()
+
+ if not assoc:
+ assoc = Association()
+ assoc.server_url = unicode(server_url)
+ assoc.handle = association.handle
+
+ # django uses base64 encoding, python-openid uses a blob field for
+ # secret
+ assoc.secret = unicode(base64.encodestring(association.secret))
+ assoc.issued = association.issued
+ assoc.lifetime = association.lifetime
+ assoc.assoc_type = association.assoc_type
+ assoc.save()
+
+ def getAssociation(self, server_url, handle=None):
+ assocs = []
+ if handle is not None:
+ assocs = Association.query.filter_by(
+ server_url=server_url, handle=handle
+ )
+ else:
+ assocs = Association.query.filter_by(
+ server_url=server_url
+ )
+
+ if assocs.count() == 0:
+ return None
+ else:
+ associations = []
+ for assoc in assocs:
+ association = OIDAssociation(
+ assoc.handle, base64.decodestring(assoc.secret),
+ assoc.issued, assoc.lifetime, assoc.assoc_type
+ )
+ if association.getExpiresIn() == 0:
+ assoc.delete()
+ else:
+ associations.append((association.issued, association))
+
+ if not associations:
+ return None
+ associations.sort()
+ return associations[-1][1]
+
+ def removeAssociation(self, server_url, handle):
+ assocs = Association.query.filter_by(
+ server_url=server_url, handle=handle
+ ).first()
+
+ assoc_exists = True if assocs else False
+ for assoc in assocs:
+ assoc.delete()
+ return assoc_exists
+
+ def useNonce(self, server_url, timestamp, salt):
+ if abs(timestamp - time.time()) > nonce.SKEW:
+ return False
+
+ ononce = Nonce.query.filter_by(
+ server_url=server_url,
+ timestamp=timestamp,
+ salt=salt
+ ).first()
+
+ if ononce:
+ return False
+ else:
+ ononce = Nonce()
+ ononce.server_url = server_url
+ ononce.timestamp = timestamp
+ ononce.salt = salt
+ ononce.save()
+ return True
+
+ def cleanupNonces(self, _now=None):
+ if _now is None:
+ _now = int(time.time())
+ expired = Nonce.query.filter(
+ Nonce.timestamp < (_now - nonce.SKEW)
+ )
+ count = expired.count()
+ for each in expired:
+ each.delete()
+ return count
+
+ def cleanupAssociations(self):
+ now = int(time.time())
+ assoc = Association.query.all()
+ count = 0
+ for each in assoc:
+ if (each.lifetime + each.issued) <= now:
+ each.delete()
+ count = count + 1
+ return count
diff --git a/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/add.html b/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/add.html
new file mode 100644
index 00000000..8d308c81
--- /dev/null
+++ b/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/add.html
@@ -0,0 +1,44 @@
+{#
+# 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/>.
+#}
+{% extends "mediagoblin/base.html" %}
+
+{% import "/mediagoblin/utils/wtforms.html" as wtforms_util %}
+
+{% block title -%}
+ {% trans %}Add an OpenID{% endtrans %} &mdash; {{ super() }}
+{%- endblock %}
+
+{% block mediagoblin_content %}
+ <form action="{{ request.urlgen('mediagoblin.plugins.openid.edit') }}"
+ method="POST" enctype="multipart/form-data">
+ {{ csrf_token }}
+ <div class="form_box">
+ <h1>{% trans %}Add an OpenID{% endtrans %}</h1>
+ <p>
+ <a href="{{ request.urlgen('mediagoblin.plugins.openid.delete') }}">
+ {% trans %}Delete an OpenID{% endtrans %}
+ </a>
+ </p>
+ {{ wtforms_util.render_divs(form, True) }}
+ <div class="form_submit_buttons">
+ <input type="submit" value="{% trans %}Add{% endtrans %}" class="button_form"/>
+ </div>
+ </div>
+ </form>
+{% endblock %}
+
diff --git a/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/delete.html b/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/delete.html
new file mode 100644
index 00000000..84301b9e
--- /dev/null
+++ b/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/delete.html
@@ -0,0 +1,43 @@
+{#
+# 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/>.
+#}
+{% extends "mediagoblin/base.html" %}
+
+{% import "/mediagoblin/utils/wtforms.html" as wtforms_util %}
+
+{% block title -%}
+ {% trans %}Delete an OpenID{% endtrans %} &mdash; {{ super() }}
+{%- endblock %}
+
+{% block mediagoblin_content %}
+ <form action="{{ request.urlgen('mediagoblin.plugins.openid.delete') }}"
+ method="POST" enctype="multipart/form-data">
+ {{ csrf_token }}
+ <div class="form_box">
+ <h1>{% trans %}Delete an OpenID{% endtrans %}</h1>
+ <p>
+ <a href="{{ request.urlgen('mediagoblin.plugins.openid.edit') }}">
+ {% trans %}Add an OpenID{% endtrans %}
+ </a>
+ </p>
+ {{ wtforms_util.render_divs(form, True) }}
+ <div class="form_submit_buttons">
+ <input type="submit" value="{% trans %}Delete{% endtrans %}" class="button_form"/>
+ </div>
+ </div>
+ </form>
+{% endblock %}
diff --git a/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/edit_link.html b/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/edit_link.html
new file mode 100644
index 00000000..2e63e1f8
--- /dev/null
+++ b/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/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 openid_edit_link %}
+ <p>
+ <a href="{{ request.urlgen('mediagoblin.plugins.openid.edit') }}">
+ {% trans %}Edit your OpenID's{% endtrans %}
+ </a>
+ </p>
+{% endblock %}
diff --git a/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/login.html b/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/login.html
new file mode 100644
index 00000000..33df7200
--- /dev/null
+++ b/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/login.html
@@ -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/>.
+#}
+{% extends "mediagoblin/base.html" %}
+
+{% import "/mediagoblin/utils/wtforms.html" as wtforms_util %}
+
+{% block mediagoblin_head %}
+ <script type="text/javascript"
+ src="{{ request.staticdirect('/js/autofilledin_password.js') }}"></script>
+{% endblock %}
+
+{% block title -%}
+ {% trans %}Log in{% endtrans %} &mdash; {{ super() }}
+{%- endblock %}
+
+{% block mediagoblin_content %}
+ <form action="{{ post_url }}"
+ method="POST" enctype="multipart/form-data">
+ {{ csrf_token }}
+ <div class="form_box">
+ <h1>{% trans %}Log in{% endtrans %}</h1>
+ {% if login_failed %}
+ <div class="form_field_error">
+ {% trans %}Logging in failed!{% endtrans %}
+ </div>
+ {% endif %}
+ {% if allow_registration %}
+ <p>
+ {% trans %}Log in to create an account!{% endtrans %}
+ </p>
+ {% endif %}
+ {% if pass_auth is defined %}
+ <p>
+ <a href="{{ request.urlgen('mediagoblin.auth.login') }}?{{ request.query_string }}">
+ {%- trans %}Or login with a password!{% endtrans %}
+ </a>
+ </p>
+ {% endif %}
+ {{ wtforms_util.render_divs(login_form, True) }}
+ <div class="form_submit_buttons">
+ <input type="submit" value="{% trans %}Log in{% endtrans %}" class="button_form"/>
+ </div>
+ {% if next %}
+ <input type="hidden" name="next" value="{{ next }}" class="button_form"
+ style="display: none;"/>
+ {% endif %}
+ </div>
+ </form>
+{% endblock %}
+
diff --git a/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/login_link.html b/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/login_link.html
new file mode 100644
index 00000000..e5e77d01
--- /dev/null
+++ b/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/login_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 openid_login_link %}
+ <p>
+ <a href="{{ request.urlgen('mediagoblin.plugins.openid.login') }}?{{ request.query_string }}">
+ {%- trans %}Or login with OpenID!{% endtrans %}
+ </a>
+ </p>
+{% endblock %}
diff --git a/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/register_link.html b/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/register_link.html
new file mode 100644
index 00000000..9bccb4d8
--- /dev/null
+++ b/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/register_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 openid_register_link %}
+ {% if openid_link is defined %}
+ <p>
+ <a href="{{ request.urlgen('mediagoblin.plugins.openid.login') }}">
+ {%- trans %}Or register with OpenID!{% endtrans %}
+ </a>
+ </p>
+ {% endif %}
+{% endblock %}
diff --git a/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/request_form.html b/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/request_form.html
new file mode 100644
index 00000000..68d028d0
--- /dev/null
+++ b/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/request_form.html
@@ -0,0 +1,24 @@
+{#
+# 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/>.
+#}
+{% extends "mediagoblin/base.html" %}
+
+{% block mediagoblin_content %}
+ <div onload="document.getElementById('openid_message').submit()">
+ {{ html|safe }}
+ </div>
+{% endblock %}
diff --git a/mediagoblin/plugins/openid/views.py b/mediagoblin/plugins/openid/views.py
new file mode 100644
index 00000000..9566e38e
--- /dev/null
+++ b/mediagoblin/plugins/openid/views.py
@@ -0,0 +1,404 @@
+# 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 openid.consumer import consumer
+from openid.consumer.discover import DiscoveryFailure
+from openid.extensions.sreg import SRegRequest, SRegResponse
+
+from mediagoblin import mg_globals, messages
+from mediagoblin.db.models import User
+from mediagoblin.decorators import (auth_enabled, allow_registration,
+ require_active_login)
+from mediagoblin.tools.response import redirect, render_to_response
+from mediagoblin.tools.translate import pass_to_ugettext as _
+from mediagoblin.plugins.openid import forms as auth_forms
+from mediagoblin.plugins.openid.models import OpenIDUserURL
+from mediagoblin.plugins.openid.store import SQLAlchemyOpenIDStore
+from mediagoblin.auth.tools import register_user
+
+
+def _start_verification(request, form, return_to, sreg=True):
+ """
+ Start OpenID Verification.
+
+ Returns False if verification fails, otherwise, will return either a
+ redirect or render_to_response object
+ """
+ openid_url = form.openid.data
+ c = consumer.Consumer(request.session, SQLAlchemyOpenIDStore())
+
+ # Try to discover provider
+ try:
+ auth_request = c.begin(openid_url)
+ except DiscoveryFailure:
+ # Discovery failed, return to login page
+ form.openid.errors.append(
+ _('Sorry, the OpenID server could not be found'))
+
+ return False
+
+ host = 'http://' + request.host
+
+ if sreg:
+ # Ask provider for email and nickname
+ auth_request.addExtension(SRegRequest(required=['email', 'nickname']))
+
+ # Do we even need this?
+ if auth_request is None:
+ form.openid.errors.append(
+ _('No OpenID service was found for %s' % openid_url))
+
+ elif auth_request.shouldSendRedirect():
+ # Begin the authentication process as a HTTP redirect
+ redirect_url = auth_request.redirectURL(
+ host, return_to)
+
+ return redirect(
+ request, location=redirect_url)
+
+ else:
+ # Send request as POST
+ form_html = auth_request.htmlMarkup(
+ host, host + return_to,
+ # Is this necessary?
+ form_tag_attrs={'id': 'openid_message'})
+
+ # Beware: this renders a template whose content is a form
+ # and some javascript to submit it upon page load. Non-JS
+ # users will have to click the form submit button to
+ # initiate OpenID authentication.
+ return render_to_response(
+ request,
+ 'mediagoblin/plugins/openid/request_form.html',
+ {'html': form_html})
+
+ return False
+
+
+def _finish_verification(request):
+ """
+ Complete OpenID Verification Process.
+
+ If the verification failed, will return false, otherwise, will return
+ the response
+ """
+ c = consumer.Consumer(request.session, SQLAlchemyOpenIDStore())
+
+ # Check the response from the provider
+ response = c.complete(request.args, request.base_url)
+ if response.status == consumer.FAILURE:
+ messages.add_message(
+ request,
+ messages.WARNING,
+ _('Verification of %s failed: %s' %
+ (response.getDisplayIdentifier(), response.message)))
+
+ elif response.status == consumer.SUCCESS:
+ # Verification was successfull
+ return response
+
+ elif response.status == consumer.CANCEL:
+ # Verification canceled
+ messages.add_message(
+ request,
+ messages.WARNING,
+ _('Verification cancelled'))
+
+ return False
+
+
+def _response_email(response):
+ """ Gets the email from the OpenID providers response"""
+ sreg_response = SRegResponse.fromSuccessResponse(response)
+ if sreg_response and 'email' in sreg_response:
+ return sreg_response.data['email']
+ return None
+
+
+def _response_nickname(response):
+ """ Gets the nickname from the OpenID providers response"""
+ sreg_response = SRegResponse.fromSuccessResponse(response)
+ if sreg_response and 'nickname' in sreg_response:
+ return sreg_response.data['nickname']
+ return None
+
+
+@auth_enabled
+def login(request):
+ """OpenID Login View"""
+ login_form = auth_forms.LoginForm(request.form)
+ allow_registration = mg_globals.app_config["allow_registration"]
+
+ # Can't store next in request.GET because of redirects to OpenID provider
+ # Store it in the session
+ next = request.GET.get('next')
+ request.session['next'] = next
+
+ login_failed = False
+
+ if request.method == 'POST' and login_form.validate():
+ return_to = request.urlgen(
+ 'mediagoblin.plugins.openid.finish_login')
+
+ success = _start_verification(request, login_form, return_to)
+
+ if success:
+ return success
+
+ login_failed = True
+
+ return render_to_response(
+ request,
+ 'mediagoblin/plugins/openid/login.html',
+ {'login_form': login_form,
+ 'next': request.session.get('next'),
+ 'login_failed': login_failed,
+ 'post_url': request.urlgen('mediagoblin.plugins.openid.login'),
+ 'allow_registration': allow_registration})
+
+
+@auth_enabled
+def finish_login(request):
+ """Complete OpenID Login Process"""
+ response = _finish_verification(request)
+
+ if not response:
+ # Verification failed, redirect to login page.
+ return redirect(request, 'mediagoblin.plugins.openid.login')
+
+ # Verification was successfull
+ query = OpenIDUserURL.query.filter_by(
+ openid_url=response.identity_url,
+ ).first()
+ user = query.user if query else None
+
+ if user:
+ # Set up login in session
+ request.session['user_id'] = unicode(user.id)
+ request.session.save()
+
+ if request.session.get('next'):
+ return redirect(request, location=request.session.pop('next'))
+ else:
+ return redirect(request, "index")
+ else:
+ # No user, need to register
+ if not mg_globals.app.auth:
+ messages.add_message(
+ request,
+ messages.WARNING,
+ _('Sorry, authentication is disabled on this instance.'))
+ return redirect(request, 'index')
+
+ # Get email and nickname from response
+ email = _response_email(response)
+ username = _response_nickname(response)
+
+ register_form = auth_forms.RegistrationForm(request.form,
+ openid=response.identity_url,
+ email=email,
+ username=username)
+ return render_to_response(
+ request,
+ 'mediagoblin/auth/register.html',
+ {'register_form': register_form,
+ 'post_url': request.urlgen('mediagoblin.plugins.openid.register')})
+
+
+@allow_registration
+@auth_enabled
+def register(request):
+ """OpenID Registration View"""
+ if request.method == 'GET':
+ # Need to connect to openid provider before registering a user to
+ # get the users openid url. If method is 'GET', then this page was
+ # acessed without logging in first.
+ return redirect(request, 'mediagoblin.plugins.openid.login')
+
+ register_form = auth_forms.RegistrationForm(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.openid.register')})
+
+
+@require_active_login
+def start_edit(request):
+ """Starts the process of adding an openid url to a users account"""
+ form = auth_forms.LoginForm(request.form)
+
+ if request.method == 'POST' and form.validate():
+ query = OpenIDUserURL.query.filter_by(
+ openid_url=form.openid.data
+ ).first()
+ user = query.user if query else None
+
+ if not user:
+ return_to = request.urlgen('mediagoblin.plugins.openid.finish_edit')
+ success = _start_verification(request, form, return_to, False)
+
+ if success:
+ return success
+ else:
+ form.openid.errors.append(
+ _('Sorry, an account is already registered to that OpenID.'))
+
+ return render_to_response(
+ request,
+ 'mediagoblin/plugins/openid/add.html',
+ {'form': form,
+ 'post_url': request.urlgen('mediagoblin.plugins.openid.edit')})
+
+
+@require_active_login
+def finish_edit(request):
+ """Finishes the process of adding an openid url to a user"""
+ response = _finish_verification(request)
+
+ if not response:
+ # Verification failed, redirect to add openid page.
+ return redirect(request, 'mediagoblin.plugins.openid.edit')
+
+ # Verification was successfull
+ query = OpenIDUserURL.query.filter_by(
+ openid_url=response.identity_url,
+ ).first()
+ user_exists = query.user if query else None
+
+ if user_exists:
+ # user exists with that openid url, redirect back to edit page
+ messages.add_message(
+ request,
+ messages.WARNING,
+ _('Sorry, an account is already registered to that OpenID.'))
+ return redirect(request, 'mediagoblin.plugins.openid.edit')
+
+ else:
+ # Save openid to user
+ user = User.query.filter_by(
+ id=request.session['user_id']
+ ).first()
+
+ new_entry = OpenIDUserURL()
+ new_entry.openid_url = response.identity_url
+ new_entry.user_id = user.id
+ new_entry.save()
+
+ messages.add_message(
+ request,
+ messages.SUCCESS,
+ _('Your OpenID url was saved successfully.'))
+
+ return redirect(request, 'mediagoblin.edit.account')
+
+
+@require_active_login
+def delete_openid(request):
+ """View to remove an openid from a users account"""
+ form = auth_forms.LoginForm(request.form)
+
+ if request.method == 'POST' and form.validate():
+ # Check if a user has this openid
+ query = OpenIDUserURL.query.filter_by(
+ openid_url=form.openid.data
+ )
+ user = query.first().user if query.first() else None
+
+ if user and user.id == int(request.session['user_id']):
+ count = len(user.openid_urls)
+ if not count > 1 and not user.pw_hash:
+ # Make sure the user has a pw or another OpenID
+ messages.add_message(
+ request,
+ messages.WARNING,
+ _("You can't delete your only OpenID URL unless you"
+ " have a password set"))
+ elif user:
+ # There is a user, but not the same user who is logged in
+ form.openid.errors.append(
+ _('That OpenID is not registered to this account.'))
+
+ if not form.errors and not request.session['messages']:
+ # Okay to continue with deleting openid
+ return_to = request.urlgen(
+ 'mediagoblin.plugins.openid.finish_delete')
+ success = _start_verification(request, form, return_to, False)
+
+ if success:
+ return success
+
+ return render_to_response(
+ request,
+ 'mediagoblin/plugins/openid/delete.html',
+ {'form': form,
+ 'post_url': request.urlgen('mediagoblin.plugins.openid.delete')})
+
+
+@require_active_login
+def finish_delete(request):
+ """Finishes the deletion of an OpenID from an user's account"""
+ response = _finish_verification(request)
+
+ if not response:
+ # Verification failed, redirect to delete openid page.
+ return redirect(request, 'mediagoblin.plugins.openid.delete')
+
+ query = OpenIDUserURL.query.filter_by(
+ openid_url=response.identity_url
+ )
+ user = query.first().user if query.first() else None
+
+ # Need to check this again because of generic openid urls such as google's
+ if user and user.id == int(request.session['user_id']):
+ count = len(user.openid_urls)
+ if count > 1 or user.pw_hash:
+ # User has more then one openid or also has a password.
+ query.first().delete()
+
+ messages.add_message(
+ request,
+ messages.SUCCESS,
+ _('OpenID was successfully removed.'))
+
+ return redirect(request, 'mediagoblin.edit.account')
+
+ elif not count > 1:
+ messages.add_message(
+ request,
+ messages.WARNING,
+ _("You can't delete your only OpenID URL unless you have a "
+ "password set"))
+
+ return redirect(request, 'mediagoblin.plugins.openid.delete')
+
+ else:
+ messages.add_message(
+ request,
+ messages.WARNING,
+ _('That OpenID is not registered to this account.'))
+
+ return redirect(request, 'mediagoblin.plugins.openid.delete')
diff --git a/mediagoblin/routing.py b/mediagoblin/routing.py
index a650f22f..986eb2ed 100644
--- a/mediagoblin/routing.py
+++ b/mediagoblin/routing.py
@@ -35,6 +35,7 @@ def get_url_map():
import mediagoblin.edit.routing
import mediagoblin.webfinger.routing
import mediagoblin.listings.routing
+ import mediagoblin.notifications.routing
for route in PluginManager().get_routes():
add_route(*route)
diff --git a/mediagoblin/static/css/base.css b/mediagoblin/static/css/base.css
index 5b8226e6..8b57584d 100644
--- a/mediagoblin/static/css/base.css
+++ b/mediagoblin/static/css/base.css
@@ -129,6 +129,7 @@ header {
.header_dropdown {
margin-bottom: 20px;
+ padding: 0px 10px 0px 10px;
}
.header_dropdown li {
@@ -384,6 +385,12 @@ a.comment_whenlink:hover {
margin-top: 8px;
}
+.comment_active {
+ box-shadow: 0px 0px 15px 15px #378566;
+ background: #378566;
+ color: #f7f7f7;
+}
+
textarea#comment_content {
resize: vertical;
width: 100%;
diff --git a/mediagoblin/static/css/pdf_viewer.css b/mediagoblin/static/css/pdf_viewer.css
deleted file mode 100644
index c04c8981..00000000
--- a/mediagoblin/static/css/pdf_viewer.css
+++ /dev/null
@@ -1,1448 +0,0 @@
-/* Copyright 2012 Mozilla Foundation
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-* {
- padding: 0;
- margin: 0;
-}
-
-html {
- height: 100%;
-}
-
-body {
- height: 100%;
- background-color: #404040;
- background-image: url(../extlib/pdf.js/web/images/texture.png);
-}
-
-body,
-input,
-button,
-select {
- font: message-box;
-}
-
-.hidden {
- display: none;
-}
-[hidden] {
- display: none !important;
-}
-
-#viewerContainer:-webkit-full-screen {
- top: 0px;
- border-top: 2px solid transparent;
- background-color: #404040;
- background-image: url(../extlib/pdf.js/web/images/texture.png);
- width: 100%;
- height: 100%;
- overflow: hidden;
- cursor: none;
-}
-
-#viewerContainer:-moz-full-screen {
- top: 0px;
- border-top: 2px solid transparent;
- background-color: #404040;
- background-image: url(../extlib/pdf.js/web/images/texture.png);
- width: 100%;
- height: 100%;
- overflow: hidden;
- cursor: none;
-}
-
-#viewerContainer:fullscreen {
- top: 0px;
- border-top: 2px solid transparent;
- background-color: #404040;
- background-image: url(../extlib/pdf.js/web/images/texture.png);
- width: 100%;
- height: 100%;
- overflow: hidden;
- cursor: none;
-}
-
-
-:-webkit-full-screen .page {
- margin-bottom: 100%;
-}
-
-:-moz-full-screen .page {
- margin-bottom: 100%;
-}
-
-:fullscreen .page {
- margin-bottom: 100%;
-}
-
-#viewerContainer.presentationControls {
- cursor: default;
-}
-
-/* outer/inner center provides horizontal center */
-html[dir='ltr'] .outerCenter {
- float: right;
- position: relative;
- right: 50%;
-}
-html[dir='rtl'] .outerCenter {
- float: left;
- position: relative;
- left: 50%;
-}
-html[dir='ltr'] .innerCenter {
- float: right;
- position: relative;
- right: -50%;
-}
-html[dir='rtl'] .innerCenter {
- float: left;
- position: relative;
- left: -50%;
-}
-
-#outerContainer {
- width: 100%;
- height: 100%;
-}
-
-#sidebarContainer {
- left: 0;
- right: 0;
- height: 200px;
- visibility: hidden;
- -webkit-transition-duration: 200ms;
- -webkit-transition-timing-function: ease;
- -moz-transition-duration: 200ms;
- -moz-transition-timing-function: ease;
- -ms-transition-duration: 200ms;
- -ms-transition-timing-function: ease;
- -o-transition-duration: 200ms;
- -o-transition-timing-function: ease;
- transition-duration: 200ms;
- transition-timing-function: ease;
-
-}
-html[dir='ltr'] #sidebarContainer {
- -webkit-transition-property: top;
- -moz-transition-property: top;
- -ms-transition-property: top;
- -o-transition-property: top;
- transition-property: top;
- top: -200px;
-}
-html[dir='rtl'] #sidebarContainer {
- -webkit-transition-property: top;
- -ms-transition-property: top;
- -o-transition-property: top;
- transition-property: top;
- top: -200px;
-}
-
-#outerContainer.sidebarMoving > #sidebarContainer,
-#outerContainer.sidebarOpen > #sidebarContainer {
- visibility: visible;
-}
-html[dir='ltr'] #outerContainer.sidebarOpen > #sidebarContainer {
- left: 0px;
-}
-html[dir='rtl'] #outerContainer.sidebarOpen > #sidebarContainer {
- right: 0px;
-}
-
-#mainContainer {
- top: 0;
- right: 0;
- bottom: 0;
- left: 0;
- min-width: 320px;
- -webkit-transition-duration: 200ms;
- -webkit-transition-timing-function: ease;
- -moz-transition-duration: 200ms;
- -moz-transition-timing-function: ease;
- -ms-transition-duration: 200ms;
- -ms-transition-timing-function: ease;
- -o-transition-duration: 200ms;
- -o-transition-timing-function: ease;
- transition-duration: 200ms;
- transition-timing-function: ease;
-}
-html[dir='ltr'] #outerContainer.sidebarOpen > #mainContainer {
- -webkit-transition-property: left;
- -moz-transition-property: left;
- -ms-transition-property: left;
- -o-transition-property: left;
- transition-property: left;
- left: 200px;
-}
-html[dir='rtl'] #outerContainer.sidebarOpen > #mainContainer {
- -webkit-transition-property: right;
- -moz-transition-property: right;
- -ms-transition-property: right;
- -o-transition-property: right;
- transition-property: right;
- right: 200px;
-}
-
-#sidebarContent {
- top: 32px;
- bottom: 0;
- overflow: auto;
- height: 200px;
-
- background-color: hsla(0,0%,0%,.1);
- box-shadow: inset -1px 0 0 hsla(0,0%,0%,.25);
-}
-html[dir='ltr'] #sidebarContent {
- left: 0;
-}
-html[dir='rtl'] #sidebarContent {
- right: 0;
-}
-
-#viewerContainer {
- overflow: auto;
- box-shadow: inset 1px 0 0 hsla(0,0%,100%,.05);
- top: 32px;
- right: 0;
- bottom: 0;
- left: 0;
- height: 480px;
- width: 640px;
-}
-
-.toolbar {
- left: 0;
- right: 0;
- height: 32px;
- z-index: 9999;
- cursor: default;
-}
-
-#toolbarContainer {
- width: 100%;
-}
-
-#toolbarSidebar {
- width: 200px;
- height: 32px;
- background-image: url(../extlib/pdf.js/web/images/texture.png),
- -webkit-linear-gradient(hsla(0,0%,30%,.99), hsla(0,0%,25%,.95));
- background-image: url(../extlib/pdf.js/web/images/texture.png),
- -moz-linear-gradient(hsla(0,0%,30%,.99), hsla(0,0%,25%,.95));
- background-image: url(../extlib/pdf.js/web/images/texture.png),
- -ms-linear-gradient(hsla(0,0%,30%,.99), hsla(0,0%,25%,.95));
- background-image: url(../extlib/pdf.js/web/images/texture.png),
- -o-linear-gradient(hsla(0,0%,30%,.99), hsla(0,0%,25%,.95));
- background-image: url(../extlib/pdf.js/web/images/texture.png),
- linear-gradient(hsla(0,0%,30%,.99), hsla(0,0%,25%,.95));
- box-shadow: inset -1px 0 0 rgba(0, 0, 0, 0.25),
-
- inset 0 -1px 0 hsla(0,0%,100%,.05),
- 0 1px 0 hsla(0,0%,0%,.15),
- 0 0 1px hsla(0,0%,0%,.1);
-}
-
-#toolbarViewer, .findbar {
- position: relative;
- height: 32px;
- background-color: #474747; /* IE9 */
- background-image: url(../extlib/pdf.js/web/images/texture.png),
- -webkit-linear-gradient(hsla(0,0%,32%,.99), hsla(0,0%,27%,.95));
- background-image: url(../extlib/pdf.js/web/images/texture.png),
- -moz-linear-gradient(hsla(0,0%,32%,.99), hsla(0,0%,27%,.95));
- background-image: url(../extlib/pdf.js/web/images/texture.png),
- -ms-linear-gradient(hsla(0,0%,32%,.99), hsla(0,0%,27%,.95));
- background-image: url(../extlib/pdf.js/web/images/texture.png),
- -o-linear-gradient(hsla(0,0%,32%,.99), hsla(0,0%,27%,.95));
- background-image: url(../extlib/pdf.js/web/images/texture.png),
- linear-gradient(hsla(0,0%,32%,.99), hsla(0,0%,27%,.95));
- box-shadow: inset 1px 0 0 hsla(0,0%,100%,.08),
- inset 0 1px 1px hsla(0,0%,0%,.15),
- inset 0 -1px 0 hsla(0,0%,100%,.05),
- 0 1px 0 hsla(0,0%,0%,.15),
- 0 1px 1px hsla(0,0%,0%,.1);
-}
-
-.findbar {
- top: 64px;
- z-index: 10000;
- height: 32px;
-
- min-width: 16px;
- padding: 0px 6px 0px 6px;
- margin: 4px 2px 4px 2px;
- color: hsl(0,0%,85%);
- font-size: 12px;
- line-height: 14px;
- text-align: left;
- cursor: default;
-}
-
-html[dir='ltr'] .findbar {
- left: 68px;
-}
-
-html[dir='rtl'] .findbar {
- right: 68px;
-}
-
-.findbar label {
- -webkit-user-select: none;
- -moz-user-select: none;
-}
-
-#findInput[data-status="pending"] {
- background-image: url(../extlib/pdf.js/web/images/loading-small.png);
- background-repeat: no-repeat;
- background-position: right;
-}
-
-.doorHanger {
- border: 1px solid hsla(0,0%,0%,.5);
- border-radius: 2px;
- box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
-}
-.doorHanger:after, .doorHanger:before {
- bottom: 100%;
- border: solid transparent;
- content: " ";
- height: 0;
- width: 0;
- pointer-events: none;
-}
-.doorHanger:after {
- border-bottom-color: hsla(0,0%,32%,.99);
- border-width: 8px;
-}
-.doorHanger:before {
- border-bottom-color: hsla(0,0%,0%,.5);
- border-width: 9px;
-}
-
-html[dir='ltr'] .doorHanger:after {
- left: 13px;
- margin-left: -8px;
-}
-
-html[dir='ltr'] .doorHanger:before {
- left: 13px;
- margin-left: -9px;
-}
-
-html[dir='rtl'] .doorHanger:after {
- right: 13px;
- margin-right: -8px;
-}
-
-html[dir='rtl'] .doorHanger:before {
- right: 13px;
- margin-right: -9px;
-}
-
-#findMsg {
- font-style: italic;
- color: #A6B7D0;
-}
-
-.notFound {
- background-color: rgb(255, 137, 153);
-}
-
-html[dir='ltr'] #toolbarViewerLeft {
- margin-left: -1px;
-}
-html[dir='rtl'] #toolbarViewerRight {
- margin-left: -1px;
-}
-
-
-html[dir='ltr'] #toolbarViewerLeft,
-html[dir='rtl'] #toolbarViewerRight {
- position: absolute;
- top: 0;
- left: 0;
-}
-html[dir='ltr'] #toolbarViewerRight,
-html[dir='rtl'] #toolbarViewerLeft {
- position: absolute;
- top: 0;
- right: 0;
-}
-html[dir='ltr'] #toolbarViewerLeft > *,
-html[dir='ltr'] #toolbarViewerMiddle > *,
-html[dir='ltr'] #toolbarViewerRight > *,
-html[dir='ltr'] .findbar > * {
- float: left;
-}
-html[dir='rtl'] #toolbarViewerLeft > *,
-html[dir='rtl'] #toolbarViewerMiddle > *,
-html[dir='rtl'] #toolbarViewerRight > *,
-html[dir='rtl'] .findbar > * {
- float: right;
-}
-
-html[dir='ltr'] .splitToolbarButton {
- margin: 3px 2px 4px 0;
- display: inline-block;
-}
-html[dir='rtl'] .splitToolbarButton {
- margin: 3px 0 4px 2px;
- display: inline-block;
-}
-html[dir='ltr'] .splitToolbarButton > .toolbarButton {
- border-radius: 0;
- float: left;
-}
-html[dir='rtl'] .splitToolbarButton > .toolbarButton {
- border-radius: 0;
- float: right;
-}
-
-.toolbarButton {
- border: 0 none;
- background-color: rgba(0, 0, 0, 0);
- width: 32px;
- height: 25px;
-}
-
-.toolbarButton > span {
- display: inline-block;
- width: 0;
- height: 0;
- overflow: hidden;
-}
-
-.toolbarButton[disabled] {
- opacity: .5;
-}
-
-.toolbarButton.group {
- margin-right: 0;
-}
-
-.splitToolbarButton.toggled .toolbarButton {
- margin: 0;
-}
-
-.splitToolbarButton:hover > .toolbarButton,
-.splitToolbarButton:focus > .toolbarButton,
-.splitToolbarButton.toggled > .toolbarButton,
-.toolbarButton.textButton {
- background-color: hsla(0,0%,0%,.12);
- background-image: -webkit-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
- background-image: -moz-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
- background-image: -ms-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
- background-image: -o-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
- background-image: linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
- background-clip: padding-box;
- border: 1px solid hsla(0,0%,0%,.35);
- border-color: hsla(0,0%,0%,.32) hsla(0,0%,0%,.38) hsla(0,0%,0%,.42);
- box-shadow: 0 1px 0 hsla(0,0%,100%,.05) inset,
- 0 0 1px hsla(0,0%,100%,.15) inset,
- 0 1px 0 hsla(0,0%,100%,.05);
- -webkit-transition-property: background-color, border-color, box-shadow;
- -webkit-transition-duration: 150ms;
- -webkit-transition-timing-function: ease;
- -moz-transition-property: background-color, border-color, box-shadow;
- -moz-transition-duration: 150ms;
- -moz-transition-timing-function: ease;
- -ms-transition-property: background-color, border-color, box-shadow;
- -ms-transition-duration: 150ms;
- -ms-transition-timing-function: ease;
- -o-transition-property: background-color, border-color, box-shadow;
- -o-transition-duration: 150ms;
- -o-transition-timing-function: ease;
- transition-property: background-color, border-color, box-shadow;
- transition-duration: 150ms;
- transition-timing-function: ease;
-
-}
-.splitToolbarButton > .toolbarButton:hover,
-.splitToolbarButton > .toolbarButton:focus,
-.dropdownToolbarButton:hover,
-.toolbarButton.textButton:hover,
-.toolbarButton.textButton:focus {
- background-color: hsla(0,0%,0%,.2);
- box-shadow: 0 1px 0 hsla(0,0%,100%,.05) inset,
- 0 0 1px hsla(0,0%,100%,.15) inset,
- 0 0 1px hsla(0,0%,0%,.05);
- z-index: 199;
-}
-html[dir='ltr'] .splitToolbarButton > .toolbarButton:first-child,
-html[dir='rtl'] .splitToolbarButton > .toolbarButton:last-child {
- position: relative;
- margin: 0;
- margin-right: -1px;
- border-top-left-radius: 2px;
- border-bottom-left-radius: 2px;
- border-right-color: transparent;
-}
-html[dir='ltr'] .splitToolbarButton > .toolbarButton:last-child,
-html[dir='rtl'] .splitToolbarButton > .toolbarButton:first-child {
- position: relative;
- margin: 0;
- margin-left: -1px;
- border-top-right-radius: 2px;
- border-bottom-right-radius: 2px;
- border-left-color: transparent;
-}
-.splitToolbarButtonSeparator {
- padding: 8px 0;
- width: 1px;
- background-color: hsla(0,0%,00%,.5);
- z-index: 99;
- box-shadow: 0 0 0 1px hsla(0,0%,100%,.08);
- display: inline-block;
- margin: 5px 0;
-}
-html[dir='ltr'] .splitToolbarButtonSeparator {
- float: left;
-}
-html[dir='rtl'] .splitToolbarButtonSeparator {
- float: right;
-}
-.splitToolbarButton:hover > .splitToolbarButtonSeparator,
-.splitToolbarButton.toggled > .splitToolbarButtonSeparator {
- padding: 12px 0;
- margin: 1px 0;
- box-shadow: 0 0 0 1px hsla(0,0%,100%,.03);
- -webkit-transition-property: padding;
- -webkit-transition-duration: 10ms;
- -webkit-transition-timing-function: ease;
- -moz-transition-property: padding;
- -moz-transition-duration: 10ms;
- -moz-transition-timing-function: ease;
- -ms-transition-property: padding;
- -ms-transition-duration: 10ms;
- -ms-transition-timing-function: ease;
- -o-transition-property: padding;
- -o-transition-duration: 10ms;
- -o-transition-timing-function: ease;
- transition-property: padding;
- transition-duration: 10ms;
- transition-timing-function: ease;
-}
-
-.toolbarButton,
-.dropdownToolbarButton {
- min-width: 16px;
- padding: 2px 6px 0;
- border: 1px solid transparent;
- border-radius: 2px;
- color: hsl(0,0%,95%);
- font-size: 12px;
- line-height: 14px;
- -webkit-user-select: none;
- -moz-user-select: none;
- -ms-user-select: none;
- /* Opera does not support user-select, use <... unselectable="on"> instead */
- cursor: default;
- -webkit-transition-property: background-color, border-color, box-shadow;
- -webkit-transition-duration: 150ms;
- -webkit-transition-timing-function: ease;
- -moz-transition-property: background-color, border-color, box-shadow;
- -moz-transition-duration: 150ms;
- -moz-transition-timing-function: ease;
- -ms-transition-property: background-color, border-color, box-shadow;
- -ms-transition-duration: 150ms;
- -ms-transition-timing-function: ease;
- -o-transition-property: background-color, border-color, box-shadow;
- -o-transition-duration: 150ms;
- -o-transition-timing-function: ease;
- transition-property: background-color, border-color, box-shadow;
- transition-duration: 150ms;
- transition-timing-function: ease;
-}
-
-html[dir='ltr'] .toolbarButton,
-html[dir='ltr'] .dropdownToolbarButton {
- margin: 3px 2px 4px 0;
-}
-html[dir='rtl'] .toolbarButton,
-html[dir='rtl'] .dropdownToolbarButton {
- margin: 3px 0 4px 2px;
-}
-
-.toolbarButton:hover,
-.toolbarButton:focus,
-.dropdownToolbarButton {
- background-color: hsla(0,0%,0%,.12);
- background-image: -webkit-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
- background-image: -moz-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
- background-image: -ms-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
- background-image: -o-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
- background-image: linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
- background-clip: padding-box;
- border: 1px solid hsla(0,0%,0%,.35);
- border-color: hsla(0,0%,0%,.32) hsla(0,0%,0%,.38) hsla(0,0%,0%,.42);
- box-shadow: 0 1px 0 hsla(0,0%,100%,.05) inset,
- 0 0 1px hsla(0,0%,100%,.15) inset,
- 0 1px 0 hsla(0,0%,100%,.05);
-}
-
-.toolbarButton:hover:active,
-.dropdownToolbarButton:hover:active {
- background-color: hsla(0,0%,0%,.2);
- background-image: -webkit-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
- background-image: -moz-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
- background-image: -ms-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
- background-image: -o-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
- background-image: linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
- border-color: hsla(0,0%,0%,.35) hsla(0,0%,0%,.4) hsla(0,0%,0%,.45);
- box-shadow: 0 1px 1px hsla(0,0%,0%,.1) inset,
- 0 0 1px hsla(0,0%,0%,.2) inset,
- 0 1px 0 hsla(0,0%,100%,.05);
- -webkit-transition-property: background-color, border-color, box-shadow;
- -webkit-transition-duration: 10ms;
- -webkit-transition-timing-function: linear;
- -moz-transition-property: background-color, border-color, box-shadow;
- -moz-transition-duration: 10ms;
- -moz-transition-timing-function: linear;
- -ms-transition-property: background-color, border-color, box-shadow;
- -ms-transition-duration: 10ms;
- -ms-transition-timing-function: linear;
- -o-transition-property: background-color, border-color, box-shadow;
- -o-transition-duration: 10ms;
- -o-transition-timing-function: linear;
- transition-property: background-color, border-color, box-shadow;
- transition-duration: 10ms;
- transition-timing-function: linear;
-}
-
-.toolbarButton.toggled,
-.splitToolbarButton.toggled > .toolbarButton.toggled {
- background-color: hsla(0,0%,0%,.3);
- background-image: -webkit-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
- background-image: -moz-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
- background-image: -ms-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
- background-image: -o-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
- background-image: linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
- border-color: hsla(0,0%,0%,.4) hsla(0,0%,0%,.45) hsla(0,0%,0%,.5);
- box-shadow: 0 1px 1px hsla(0,0%,0%,.1) inset,
- 0 0 1px hsla(0,0%,0%,.2) inset,
- 0 1px 0 hsla(0,0%,100%,.05);
- -webkit-transition-property: background-color, border-color, box-shadow;
- -webkit-transition-duration: 10ms;
- -webkit-transition-timing-function: linear;
- -moz-transition-property: background-color, border-color, box-shadow;
- -moz-transition-duration: 10ms;
- -moz-transition-timing-function: linear;
- -ms-transition-property: background-color, border-color, box-shadow;
- -ms-transition-duration: 10ms;
- -ms-transition-timing-function: linear;
- -o-transition-property: background-color, border-color, box-shadow;
- -o-transition-duration: 10ms;
- -o-transition-timing-function: linear;
- transition-property: background-color, border-color, box-shadow;
- transition-duration: 10ms;
- transition-timing-function: linear;
-}
-
-.toolbarButton.toggled:hover:active,
-.splitToolbarButton.toggled > .toolbarButton.toggled:hover:active {
- background-color: hsla(0,0%,0%,.4);
- border-color: hsla(0,0%,0%,.4) hsla(0,0%,0%,.5) hsla(0,0%,0%,.55);
- box-shadow: 0 1px 1px hsla(0,0%,0%,.2) inset,
- 0 0 1px hsla(0,0%,0%,.3) inset,
- 0 1px 0 hsla(0,0%,100%,.05);
-}
-
-.dropdownToolbarButton {
- width: 120px;
- max-width: 120px;
- padding: 3px 2px 2px;
- overflow: hidden;
- background: url(../extlib/pdf.js/web/images/toolbarButton-menuArrows.png) no-repeat;
-}
-html[dir='ltr'] .dropdownToolbarButton {
- background-position: 95%;
-}
-html[dir='rtl'] .dropdownToolbarButton {
- background-position: 5%;
-}
-
-.dropdownToolbarButton > select {
- -webkit-appearance: none;
- -moz-appearance: none; /* in the future this might matter, see bugzilla bug #649849 */
- min-width: 140px;
- font-size: 12px;
- color: hsl(0,0%,95%);
- margin: 0;
- padding: 0;
- border: none;
- background: rgba(0,0,0,0); /* Opera does not support 'transparent' <select> background */
-}
-
-.dropdownToolbarButton > select > option {
- background: hsl(0,0%,24%);
-}
-
-#customScaleOption {
- display: none;
-}
-
-#pageWidthOption {
- border-bottom: 1px rgba(255, 255, 255, .5) solid;
-}
-
-html[dir='ltr'] .splitToolbarButton:first-child,
-html[dir='ltr'] .toolbarButton:first-child,
-html[dir='rtl'] .splitToolbarButton:last-child,
-html[dir='rtl'] .toolbarButton:last-child {
- margin-left: 4px;
-}
-html[dir='ltr'] .splitToolbarButton:last-child,
-html[dir='ltr'] .toolbarButton:last-child,
-html[dir='rtl'] .splitToolbarButton:first-child,
-html[dir='rtl'] .toolbarButton:first-child {
- margin-right: 4px;
-}
-
-.toolbarButtonSpacer {
- width: 30px;
- display: inline-block;
- height: 1px;
-}
-
-.toolbarButtonFlexibleSpacer {
- -webkit-box-flex: 1;
- -moz-box-flex: 1;
- min-width: 30px;
-}
-
-.toolbarButton#sidebarToggle::before {
- display: inline-block;
- content: url(../extlib/pdf.js/web/images/toolbarButton-sidebarToggle.png);
-}
-
-html[dir='ltr'] #findPrevious {
- margin-left: 3px;
-}
-html[dir='ltr'] #findNext {
- margin-right: 3px;
-}
-
-html[dir='rtl'] #findPrevious {
- margin-right: 3px;
-}
-html[dir='rtl'] #findNext {
- margin-left: 3px;
-}
-
-html[dir='ltr'] .toolbarButton.findPrevious::before {
- display: inline-block;
- content: url(../extlib/pdf.js/web/images/findbarButton-previous.png);
-}
-
-html[dir='rtl'] .toolbarButton.findPrevious::before {
- display: inline-block;
- content: url(../extlib/pdf.js/web/images/findbarButton-previous-rtl.png);
-}
-
-html[dir='ltr'] .toolbarButton.findNext::before {
- display: inline-block;
- content: url(../extlib/pdf.js/web/images/findbarButton-next.png);
-}
-
-html[dir='rtl'] .toolbarButton.findNext::before {
- display: inline-block;
- content: url(../extlib/pdf.js/web/images/findbarButton-next-rtl.png);
-}
-
-html[dir='ltr'] .toolbarButton.pageUp::before {
- display: inline-block;
- content: url(../extlib/pdf.js/web/images/toolbarButton-pageUp.png);
-}
-
-html[dir='rtl'] .toolbarButton.pageUp::before {
- display: inline-block;
- content: url(../extlib/pdf.js/web/images/toolbarButton-pageUp-rtl.png);
-}
-
-html[dir='ltr'] .toolbarButton.pageDown::before {
- display: inline-block;
- content: url(../extlib/pdf.js/web/images/toolbarButton-pageDown.png);
-}
-
-html[dir='rtl'] .toolbarButton.pageDown::before {
- display: inline-block;
- content: url(../extlib/pdf.js/web/images/toolbarButton-pageDown-rtl.png);
-}
-
-.toolbarButton.zoomOut::before {
- display: inline-block;
- content: url(../extlib/pdf.js/web/images/toolbarButton-zoomOut.png);
-}
-
-.toolbarButton.zoomIn::before {
- display: inline-block;
- content: url(../extlib/pdf.js/web/images/toolbarButton-zoomIn.png);
-}
-
-.toolbarButton.fullscreen::before {
- display: inline-block;
- content: url(../extlib/pdf.js/web/images/toolbarButton-fullscreen.png);
-}
-
-.toolbarButton.print::before {
- display: inline-block;
- content: url(../extlib/pdf.js/web/images/toolbarButton-print.png);
-}
-
-.toolbarButton.openFile::before {
- display: inline-block;
- content: url(../extlib/pdf.js/web/images/toolbarButton-openFile.png);
-}
-
-.toolbarButton.download::before {
- display: inline-block;
- content: url(../extlib/pdf.js/web/images/toolbarButton-download.png);
-}
-
-.toolbarButton.bookmark {
- -webkit-box-sizing: border-box;
- -moz-box-sizing: border-box;
- box-sizing: border-box;
- margin-top: 3px;
- padding-top: 4px;
-}
-
-#viewBookmark[href='#'] {
- opacity: .5;
- pointer-events: none;
-}
-
-.toolbarButton.bookmark::before {
- content: url(../extlib/pdf.js/web/images/toolbarButton-bookmark.png);
-}
-
-#viewThumbnail.toolbarButton::before {
- display: inline-block;
- content: url(../extlib/pdf.js/web/images/toolbarButton-viewThumbnail.png);
-}
-
-#viewOutline.toolbarButton::before {
- display: inline-block;
- content: url(../extlib/pdf.js/web/images/toolbarButton-viewOutline.png);
-}
-
-#viewFind.toolbarButton::before {
- display: inline-block;
- content: url(../extlib/pdf.js/web/images/toolbarButton-search.png);
-}
-
-
-.toolbarField {
- padding: 3px 6px;
- margin: 4px 0 4px 0;
- border: 1px solid transparent;
- border-radius: 2px;
- background-color: hsla(0,0%,100%,.09);
- background-image: -moz-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
- background-clip: padding-box;
- border: 1px solid hsla(0,0%,0%,.35);
- border-color: hsla(0,0%,0%,.32) hsla(0,0%,0%,.38) hsla(0,0%,0%,.42);
- box-shadow: 0 1px 0 hsla(0,0%,0%,.05) inset,
- 0 1px 0 hsla(0,0%,100%,.05);
- color: hsl(0,0%,95%);
- font-size: 12px;
- line-height: 14px;
- outline-style: none;
- -moz-transition-property: background-color, border-color, box-shadow;
- -moz-transition-duration: 150ms;
- -moz-transition-timing-function: ease;
-}
-
-.toolbarField[type=checkbox] {
- display: inline-block;
- margin: 8px 0px;
-}
-
-.toolbarField.pageNumber {
- min-width: 16px;
- text-align: right;
- width: 40px;
-}
-
-.toolbarField.pageNumber::-webkit-inner-spin-button,
-.toolbarField.pageNumber::-webkit-outer-spin-button {
- -webkit-appearance: none;
- margin: 0;
-}
-
-.toolbarField:hover {
- background-color: hsla(0,0%,100%,.11);
- border-color: hsla(0,0%,0%,.4) hsla(0,0%,0%,.43) hsla(0,0%,0%,.45);
-}
-
-.toolbarField:focus {
- background-color: hsla(0,0%,100%,.15);
- border-color: hsla(204,100%,65%,.8) hsla(204,100%,65%,.85) hsla(204,100%,65%,.9);
-}
-
-.toolbarLabel {
- min-width: 16px;
- padding: 3px 6px 3px 2px;
- margin: 4px 2px 4px 0;
- border: 1px solid transparent;
- border-radius: 2px;
- color: hsl(0,0%,85%);
- font-size: 12px;
- line-height: 14px;
- text-align: left;
- -webkit-user-select: none;
- -moz-user-select: none;
- cursor: default;
-}
-
-#thumbnailView {
- top: 0;
- bottom: 0;
- padding: 10px 10px 0;
- overflow: auto;
-}
-
-.thumbnail {
- float: left;
-}
-
-.thumbnail:not([data-loaded]) {
- border: 1px dashed rgba(255, 255, 255, 0.5);
- margin-bottom: 10px;
-}
-
-.thumbnailImage {
- -moz-transition-duration: 150ms;
- border: 1px solid transparent;
- box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.5), 0 2px 8px rgba(0, 0, 0, 0.3);
- opacity: 0.8;
- z-index: 99;
-}
-
-.thumbnailSelectionRing {
- border-radius: 2px;
- padding: 7px;
- -moz-transition-duration: 150ms;
-}
-
-a:focus > .thumbnail > .thumbnailSelectionRing > .thumbnailImage,
-.thumbnail:hover > .thumbnailSelectionRing > .thumbnailImage {
- opacity: .9;
-}
-
-a:focus > .thumbnail > .thumbnailSelectionRing,
-.thumbnail:hover > .thumbnailSelectionRing {
- background-color: hsla(0,0%,100%,.15);
- background-image: -moz-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
- background-clip: padding-box;
- box-shadow: 0 1px 0 hsla(0,0%,100%,.05) inset,
- 0 0 1px hsla(0,0%,100%,.2) inset,
- 0 0 1px hsla(0,0%,0%,.2);
- color: hsla(0,0%,100%,.9);
-}
-
-.thumbnail.selected > .thumbnailSelectionRing > .thumbnailImage {
- box-shadow: 0 0 0 1px hsla(0,0%,0%,.5);
- opacity: 1;
-}
-
-.thumbnail.selected > .thumbnailSelectionRing {
- background-color: hsla(0,0%,100%,.3);
- background-image: -moz-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
- background-clip: padding-box;
- box-shadow: 0 1px 0 hsla(0,0%,100%,.05) inset,
- 0 0 1px hsla(0,0%,100%,.1) inset,
- 0 0 1px hsla(0,0%,0%,.2);
- color: hsla(0,0%,100%,1);
-}
-
-#outlineView {
- width: 192px;
- top: 0;
- bottom: 0;
- padding: 4px 4px 0;
- overflow: auto;
- -webkit-user-select: none;
- -moz-user-select: none;
-}
-
-html[dir='ltr'] .outlineItem > .outlineItems {
- margin-left: 20px;
-}
-
-html[dir='rtl'] .outlineItem > .outlineItems {
- margin-right: 20px;
-}
-
-.outlineItem > a {
- text-decoration: none;
- display: inline-block;
- min-width: 95%;
- height: auto;
- margin-bottom: 1px;
- border-radius: 2px;
- color: hsla(0,0%,100%,.8);
- font-size: 13px;
- line-height: 15px;
- -moz-user-select: none;
- white-space: normal;
-}
-
-html[dir='ltr'] .outlineItem > a {
- padding: 2px 0 5px 10px;
-}
-
-html[dir='rtl'] .outlineItem > a {
- padding: 2px 10px 5px 0;
-}
-
-.outlineItem > a:hover {
- background-color: hsla(0,0%,100%,.02);
- background-image: -moz-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
- background-clip: padding-box;
- box-shadow: 0 1px 0 hsla(0,0%,100%,.05) inset,
- 0 0 1px hsla(0,0%,100%,.2) inset,
- 0 0 1px hsla(0,0%,0%,.2);
- color: hsla(0,0%,100%,.9);
-}
-
-.outlineItem.selected {
- background-color: hsla(0,0%,100%,.08);
- background-image: -moz-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
- background-clip: padding-box;
- box-shadow: 0 1px 0 hsla(0,0%,100%,.05) inset,
- 0 0 1px hsla(0,0%,100%,.1) inset,
- 0 0 1px hsla(0,0%,0%,.2);
- color: hsla(0,0%,100%,1);
-}
-
-.noOutline,
-.noResults {
- font-size: 12px;
- color: hsla(0,0%,100%,.8);
- font-style: italic;
- cursor: default;
-}
-
-#findScrollView {
- position: absolute;
- top: 10px;
- bottom: 10px;
- left: 10px;
- width: 280px;
-}
-
-#sidebarControls {
- position:absolute;
- width: 180px;
- height: 32px;
- left: 15px;
- bottom: 35px;
-}
-
-canvas {
- margin: auto;
- display: block;
-}
-
-.page {
- direction: ltr;
- width: 816px;
- height: 1056px;
- margin: 1px auto -8px auto;
- position: relative;
- overflow: visible;
- border: 9px solid transparent;
- background-clip: content-box;
- border-image: url(../extlib/pdf.js/web/images/shadow.png) 9 9 repeat;
- background-color: white;
-}
-
-.page > a {
- display: block;
- position: absolute;
-}
-
-.page > a:hover {
- opacity: 0.2;
- background: #ff0;
- box-shadow: 0px 2px 10px #ff0;
-}
-
-.loadingIcon {
- position: absolute;
- display: block;
- left: 0;
- top: 0;
- right: 0;
- bottom: 0;
- background: url('../extlib/pdf.js/web/images/loading-icon.gif') center no-repeat;
-}
-
-#loadingBox {
- position: absolute;
- top: 50%;
- margin-top: -25px;
- left: 0;
- right: 0;
- text-align: center;
- color: #ddd;
- font-size: 14px;
-}
-
-#loadingBar {
- display: inline-block;
- clear: both;
- margin: 0px;
- margin-top: 5px;
- line-height: 0;
- border-radius: 2px;
- width: 200px;
- height: 25px;
-
- background-color: hsla(0,0%,0%,.3);
- background-image: -moz-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
- background-image: -webkit-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
- border: 1px solid #000;
- box-shadow: 0 1px 1px hsla(0,0%,0%,.1) inset,
- 0 0 1px hsla(0,0%,0%,.2) inset,
- 0 0 1px 1px rgba(255, 255, 255, 0.1);
-}
-
-#loadingBar .progress {
- display: inline-block;
- float: left;
-
- background: #666;
- background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#b2b2b2), color-stop(100%,#898989));
- background: -webkit-linear-gradient(top, #b2b2b2 0%,#898989 100%);
- background: -moz-linear-gradient(top, #b2b2b2 0%,#898989 100%);
- background: -ms-linear-gradient(top, #b2b2b2 0%,#898989 100%);
- background: -o-linear-gradient(top, #b2b2b2 0%,#898989 100%);
- background: linear-gradient(top, #b2b2b2 0%,#898989 100%);
-
- border-top-left-radius: 2px;
- border-bottom-left-radius: 2px;
-
- width: 0%;
- height: 100%;
-}
-
-#loadingBar .progress.full {
- border-top-right-radius: 2px;
- border-bottom-right-radius: 2px;
-}
-
-#loadingBar .progress.indeterminate {
- width: 100%;
- height: 25px;
- background-image: -moz-linear-gradient( 30deg, #404040, #404040 15%, #898989, #404040 85%, #404040);
- background-image: -webkit-linear-gradient( 30deg, #404040, #404040 15%, #898989, #404040 85%, #404040);
- background-image: -ms-linear-gradient( 30deg, #404040, #404040 15%, #898989, #404040 85%, #404040);
- background-image: -o-linear-gradient( 30deg, #404040, #404040 15%, #898989, #404040 85%, #404040);
- background-size: 75px 25px;
- -moz-animation: progressIndeterminate 1s linear infinite;
- -webkit-animation: progressIndeterminate 1s linear infinite;
-}
-
-@-moz-keyframes progressIndeterminate {
- from { background-position: 0px 0px; }
- to { background-position: 75px 0px; }
-}
-
-@-webkit-keyframes progressIndeterminate {
- from { background-position: 0px 0px; }
- to { background-position: 75px 0px; }
-}
-
-.textLayer {
- position: absolute;
- left: 0;
- top: 0;
- right: 0;
- bottom: 0;
- color: #000;
- font-family: sans-serif;
- overflow: hidden;
-}
-
-.textLayer > div {
- color: transparent;
- position: absolute;
- line-height: 1;
- white-space: pre;
- cursor: text;
-}
-
-.textLayer .highlight {
- margin: -1px;
- padding: 1px;
-
- background-color: rgba(180, 0, 170, 0.2);
- border-radius: 4px;
-}
-
-.textLayer .highlight.begin {
- border-radius: 4px 0px 0px 4px;
-}
-
-.textLayer .highlight.end {
- border-radius: 0px 4px 4px 0px;
-}
-
-.textLayer .highlight.middle {
- border-radius: 0px;
-}
-
-.textLayer .highlight.selected {
- background-color: rgba(0, 100, 0, 0.2);
-}
-
-/* TODO: file FF bug to support ::-moz-selection:window-inactive
- so we can override the opaque grey background when the window is inactive;
- see https://bugzilla.mozilla.org/show_bug.cgi?id=706209 */
-::selection { background:rgba(0,0,255,0.3); }
-::-moz-selection { background:rgba(0,0,255,0.3); }
-
-.annotText > div {
- z-index: 200;
- position: absolute;
- padding: 0.6em;
- max-width: 20em;
- background-color: #FFFF99;
- box-shadow: 0px 2px 10px #333;
- border-radius: 7px;
-}
-
-.annotText > img {
- position: absolute;
- opacity: 0.6;
-}
-
-.annotText > img:hover {
- cursor: pointer;
- opacity: 1;
-}
-
-.annotText > div > h1 {
- font-size: 1.2em;
- border-bottom: 1px solid #000000;
- margin: 0px;
-}
-
-#errorWrapper {
- background: none repeat scroll 0 0 #FF5555;
- color: white;
- left: 0;
- position: absolute;
- right: 0;
- top: 32px;
- z-index: 1000;
- padding: 3px;
- font-size: 0.8em;
-}
-
-#errorMessageLeft {
- float: left;
-}
-
-#errorMessageRight {
- float: right;
-}
-
-#errorMoreInfo {
- background-color: #FFFFFF;
- color: black;
- padding: 3px;
- margin: 3px;
- width: 98%;
-}
-
-.clearBoth {
- clear: both;
-}
-
-.fileInput {
- background: white;
- color: black;
- margin-top: 5px;
-}
-
-#PDFBug {
- background: none repeat scroll 0 0 white;
- border: 1px solid #666666;
- position: fixed;
- top: 32px;
- right: 0;
- bottom: 0;
- font-size: 10px;
- padding: 0;
- width: 300px;
-}
-#PDFBug .controls {
- background:#EEEEEE;
- border-bottom: 1px solid #666666;
- padding: 3px;
-}
-#PDFBug .panels {
- bottom: 0;
- left: 0;
- overflow: auto;
- position: absolute;
- right: 0;
- top: 27px;
-}
-#PDFBug button.active {
- font-weight: bold;
-}
-.debuggerShowText {
- background: none repeat scroll 0 0 yellow;
- color: blue;
- opacity: 0.3;
-}
-.debuggerHideText:hover {
- background: none repeat scroll 0 0 yellow;
- opacity: 0.3;
-}
-#PDFBug .stats {
- font-family: courier;
- font-size: 10px;
- white-space: pre;
-}
-#PDFBug .stats .title {
- font-weight: bold;
-}
-#PDFBug table {
- font-size: 10px;
-}
-
-#viewer.textLayer-visible .textLayer > div,
-#viewer.textLayer-hover .textLayer > div:hover {
- background-color: white;
- color: black;
-}
-
-#viewer.textLayer-shadow .textLayer > div {
- background-color: rgba(255,255,255, .6);
- color: black;
-}
-
-@page {
- margin: 0;
-}
-
-#printContainer {
- display: none;
-}
-
-@media print {
- /* Rules for browsers that don't support mozPrintCallback. */
- #sidebarContainer, .toolbar, #loadingBox, #errorWrapper, .textLayer {
- display: none;
- }
-
- #mainContainer, #viewerContainer, .page, .page canvas {
- position: static;
- padding: 0;
- margin: 0;
- }
-
- .page {
- float: left;
- display: none;
- box-shadow: none;
- }
-
- .page[data-loaded] {
- display: block;
- }
-
- /* Rules for browsers that support mozPrintCallback */
- body[data-mozPrintCallback] #outerContainer {
- display: none;
- }
- body[data-mozPrintCallback] #printContainer {
- display: block;
- }
- #printContainer canvas {
- position: relative;
- top: 0;
- left: 0;
- }
-}
-
-@media all and (max-width: 950px) {
- html[dir='ltr'] #outerContainer.sidebarMoving .outerCenter,
- html[dir='ltr'] #outerContainer.sidebarOpen .outerCenter {
- float: left;
- left: 180px;
- }
- html[dir='rtl'] #outerContainer.sidebarMoving .outerCenter,
- html[dir='rtl'] #outerContainer.sidebarOpen .outerCenter {
- float: right;
- right: 180px;
- }
-}
-
-@media all and (max-width: 770px) {
- #sidebarContainer {
- top: 33px;
- z-index: 100;
- }
- #sidebarContent {
- top: 32px;
- background-color: hsla(0,0%,0%,.7);
- }
-
- html[dir='ltr'] #outerContainer.sidebarOpen > #mainContainer {
- left: 0px;
- }
- html[dir='rtl'] #outerContainer.sidebarOpen > #mainContainer {
- right: 0px;
- }
-
- html[dir='ltr'] .outerCenter {
- float: left;
- left: 180px;
- }
- html[dir='rtl'] .outerCenter {
- float: right;
- right: 180px;
- }
-}
-
-@media all and (max-width: 600px) {
- .hiddenSmallView {
- display: none;
- }
- html[dir='ltr'] .outerCenter {
- left: 156px;
- }
- html[dir='rtr'] .outerCenter {
- right: 156px;
- }
- .toolbarButtonSpacer {
- width: 0;
- }
-}
-
-@media all and (max-width: 500px) {
- #scaleSelectContainer, #pageNumberLabel {
- display: none;
- }
-}
-
diff --git a/mediagoblin/static/js/notifications.js b/mediagoblin/static/js/notifications.js
new file mode 100644
index 00000000..0153463a
--- /dev/null
+++ b/mediagoblin/static/js/notifications.js
@@ -0,0 +1,36 @@
+'use strict';
+/**
+ * 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/>.
+ */
+
+var notifications = {};
+
+(function (n) {
+ n._base = '/';
+ n._endpoint = 'notifications/json';
+
+ n.init = function () {
+ $('.notification-gem').on('click', function () {
+ $('.header_dropdown_down:visible').click();
+ });
+ }
+
+})(notifications)
+
+$(document).ready(function () {
+ notifications.init();
+});
diff --git a/mediagoblin/static/js/pdf_viewer.js b/mediagoblin/static/js/pdf_viewer.js
deleted file mode 100644
index 79c1e708..00000000
--- a/mediagoblin/static/js/pdf_viewer.js
+++ /dev/null
@@ -1,3615 +0,0 @@
-/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
-/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
-/* Copyright 2012 Mozilla Foundation
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-/* globals PDFJS, PDFBug, FirefoxCom, Stats */
-
-'use strict';
-
-var DEFAULT_SCALE = 'auto';
-var DEFAULT_SCALE_DELTA = 1.1;
-var UNKNOWN_SCALE = 0;
-var CACHE_SIZE = 20;
-var CSS_UNITS = 96.0 / 72.0;
-var SCROLLBAR_PADDING = 40;
-var VERTICAL_PADDING = 5;
-var MIN_SCALE = 0.25;
-var MAX_SCALE = 4.0;
-var IMAGE_DIR = './images/';
-var SETTINGS_MEMORY = 20;
-var ANNOT_MIN_SIZE = 10;
-var RenderingStates = {
- INITIAL: 0,
- RUNNING: 1,
- PAUSED: 2,
- FINISHED: 3
-};
-var FindStates = {
- FIND_FOUND: 0,
- FIND_NOTFOUND: 1,
- FIND_WRAPPED: 2,
- FIND_PENDING: 3
-};
-
-//#if (FIREFOX || MOZCENTRAL || B2G || GENERIC || CHROME)
-//PDFJS.workerSrc = '../build/pdf.js';
-//#endif
-
-var mozL10n = document.mozL10n || document.webL10n;
-
-function getFileName(url) {
- var anchor = url.indexOf('#');
- var query = url.indexOf('?');
- var end = Math.min(
- anchor > 0 ? anchor : url.length,
- query > 0 ? query : url.length);
- return url.substring(url.lastIndexOf('/', end) + 1, end);
-}
-
-function scrollIntoView(element, spot) {
- // Assuming offsetParent is available (it's not available when viewer is in
- // hidden iframe or object). We have to scroll: if the offsetParent is not set
- // producing the error. See also animationStartedClosure.
- var parent = element.offsetParent;
- var offsetY = element.offsetTop + element.clientTop;
- if (!parent) {
- console.error('offsetParent is not set -- cannot scroll');
- return;
- }
- while (parent.clientHeight == parent.scrollHeight) {
- offsetY += parent.offsetTop;
- parent = parent.offsetParent;
- if (!parent)
- return; // no need to scroll
- }
- if (spot)
- offsetY += spot.top;
- parent.scrollTop = offsetY;
-}
-
-var Cache = function cacheCache(size) {
- var data = [];
- this.push = function cachePush(view) {
- var i = data.indexOf(view);
- if (i >= 0)
- data.splice(i);
- data.push(view);
- if (data.length > size)
- data.shift().destroy();
- };
-};
-
-var ProgressBar = (function ProgressBarClosure() {
-
- function clamp(v, min, max) {
- return Math.min(Math.max(v, min), max);
- }
-
- function ProgressBar(id, opts) {
-
- // Fetch the sub-elements for later
- this.div = document.querySelector(id + ' .progress');
-
- // Get options, with sensible defaults
- this.height = opts.height || 100;
- this.width = opts.width || 100;
- this.units = opts.units || '%';
-
- // Initialize heights
- this.div.style.height = this.height + this.units;
- }
-
- ProgressBar.prototype = {
-
- updateBar: function ProgressBar_updateBar() {
- if (this._indeterminate) {
- this.div.classList.add('indeterminate');
- return;
- }
-
- var progressSize = this.width * this._percent / 100;
-
- if (this._percent > 95)
- this.div.classList.add('full');
- else
- this.div.classList.remove('full');
- this.div.classList.remove('indeterminate');
-
- this.div.style.width = progressSize + this.units;
- },
-
- get percent() {
- return this._percent;
- },
-
- set percent(val) {
- this._indeterminate = isNaN(val);
- this._percent = clamp(val, 0, 100);
- this.updateBar();
- }
- };
-
- return ProgressBar;
-})();
-
-//#if FIREFOX || MOZCENTRAL
-//#include firefoxcom.js
-//#endif
-
-// Settings Manager - This is a utility for saving settings
-// First we see if localStorage is available
-// If not, we use FUEL in FF
-// Use asyncStorage for B2G
-var Settings = (function SettingsClosure() {
-//#if !(FIREFOX || MOZCENTRAL || B2G)
- var isLocalStorageEnabled = (function localStorageEnabledTest() {
- // Feature test as per http://diveintohtml5.info/storage.html
- // The additional localStorage call is to get around a FF quirk, see
- // bug #495747 in bugzilla
- try {
- return 'localStorage' in window && window['localStorage'] !== null &&
- localStorage;
- } catch (e) {
- return false;
- }
- })();
-//#endif
-
- function Settings(fingerprint) {
- this.fingerprint = fingerprint;
- this.initializedPromise = new PDFJS.Promise();
-
- var resolvePromise = (function settingsResolvePromise(db) {
- this.initialize(db || '{}');
- this.initializedPromise.resolve();
- }).bind(this);
-
-//#if B2G
-// asyncStorage.getItem('database', resolvePromise);
-//#endif
-
-//#if FIREFOX || MOZCENTRAL
-// resolvePromise(FirefoxCom.requestSync('getDatabase', null));
-//#endif
-
-//#if !(FIREFOX || MOZCENTRAL || B2G)
- if (isLocalStorageEnabled)
- resolvePromise(localStorage.getItem('database'));
-//#endif
- }
-
- Settings.prototype = {
- initialize: function settingsInitialize(database) {
- database = JSON.parse(database);
- if (!('files' in database))
- database.files = [];
- if (database.files.length >= SETTINGS_MEMORY)
- database.files.shift();
- var index;
- for (var i = 0, length = database.files.length; i < length; i++) {
- var branch = database.files[i];
- if (branch.fingerprint == this.fingerprint) {
- index = i;
- break;
- }
- }
- if (typeof index != 'number')
- index = database.files.push({fingerprint: this.fingerprint}) - 1;
- this.file = database.files[index];
- this.database = database;
- },
-
- set: function settingsSet(name, val) {
- if (!this.initializedPromise.isResolved)
- return;
-
- var file = this.file;
- file[name] = val;
- var database = JSON.stringify(this.database);
-
-//#if B2G
-// asyncStorage.setItem('database', database);
-//#endif
-
-//#if FIREFOX || MOZCENTRAL
-// FirefoxCom.requestSync('setDatabase', database);
-//#endif
-
-//#if !(FIREFOX || MOZCENTRAL || B2G)
- if (isLocalStorageEnabled)
- localStorage.setItem('database', database);
-//#endif
- },
-
- get: function settingsGet(name, defaultValue) {
- if (!this.initializedPromise.isResolved)
- return defaultValue;
-
- return this.file[name] || defaultValue;
- }
- };
-
- return Settings;
-})();
-
-var cache = new Cache(CACHE_SIZE);
-var currentPageNumber = 1;
-
-var PDFFindController = {
- startedTextExtraction: false,
-
- extractTextPromises: [],
-
- // If active, find results will be highlighted.
- active: false,
-
- // Stores the text for each page.
- pageContents: [],
-
- pageMatches: [],
-
- // Currently selected match.
- selected: {
- pageIdx: -1,
- matchIdx: -1
- },
-
- // Where find algorithm currently is in the document.
- offset: {
- pageIdx: null,
- matchIdx: null
- },
-
- resumePageIdx: null,
-
- resumeCallback: null,
-
- state: null,
-
- dirtyMatch: false,
-
- findTimeout: null,
-
- initialize: function() {
- var events = [
- 'find',
- 'findagain',
- 'findhighlightallchange',
- 'findcasesensitivitychange'
- ];
-
- this.handleEvent = this.handleEvent.bind(this);
-
- for (var i = 0; i < events.length; i++) {
- window.addEventListener(events[i], this.handleEvent);
- }
- },
-
- calcFindMatch: function(pageIndex) {
- var pageContent = this.pageContents[pageIndex];
- var query = this.state.query;
- var caseSensitive = this.state.caseSensitive;
- var queryLen = query.length;
-
- if (queryLen === 0) {
- // Do nothing the matches should be wiped out already.
- return;
- }
-
- if (!caseSensitive) {
- pageContent = pageContent.toLowerCase();
- query = query.toLowerCase();
- }
-
- var matches = [];
-
- var matchIdx = -queryLen;
- while (true) {
- matchIdx = pageContent.indexOf(query, matchIdx + queryLen);
- if (matchIdx === -1) {
- break;
- }
-
- matches.push(matchIdx);
- }
- this.pageMatches[pageIndex] = matches;
- this.updatePage(pageIndex);
- if (this.resumePageIdx === pageIndex) {
- var callback = this.resumeCallback;
- this.resumePageIdx = null;
- this.resumeCallback = null;
- callback();
- }
- },
-
- extractText: function() {
- if (this.startedTextExtraction) {
- return;
- }
- this.startedTextExtraction = true;
-
- this.pageContents = [];
- for (var i = 0, ii = PDFView.pdfDocument.numPages; i < ii; i++) {
- this.extractTextPromises.push(new PDFJS.Promise());
- }
-
- var self = this;
- function extractPageText(pageIndex) {
- PDFView.pages[pageIndex].getTextContent().then(
- function textContentResolved(data) {
- // Build the find string.
- var bidiTexts = data.bidiTexts;
- var str = '';
-
- for (var i = 0; i < bidiTexts.length; i++) {
- str += bidiTexts[i].str;
- }
-
- // Store the pageContent as a string.
- self.pageContents.push(str);
-
- self.extractTextPromises[pageIndex].resolve(pageIndex);
- if ((pageIndex + 1) < PDFView.pages.length)
- extractPageText(pageIndex + 1);
- }
- );
- }
- extractPageText(0);
- return this.extractTextPromise;
- },
-
- handleEvent: function(e) {
- if (this.state === null || e.type !== 'findagain') {
- this.dirtyMatch = true;
- }
- this.state = e.detail;
- this.updateUIState(FindStates.FIND_PENDING);
-
- this.extractText();
-
- clearTimeout(this.findTimeout);
- if (e.type === 'find') {
- // Only trigger the find action after 250ms of silence.
- this.findTimeout = setTimeout(this.nextMatch.bind(this), 250);
- } else {
- this.nextMatch();
- }
- },
-
- updatePage: function(idx) {
- var page = PDFView.pages[idx];
-
- if (this.selected.pageIdx === idx) {
- // If the page is selected, scroll the page into view, which triggers
- // rendering the page, which adds the textLayer. Once the textLayer is
- // build, it will scroll onto the selected match.
- page.scrollIntoView();
- }
-
- if (page.textLayer) {
- page.textLayer.updateMatches();
- }
- },
-
- nextMatch: function() {
- var pages = PDFView.pages;
- var previous = this.state.findPrevious;
- var numPages = PDFView.pages.length;
-
- this.active = true;
-
- if (this.dirtyMatch) {
- // Need to recalculate the matches, reset everything.
- this.dirtyMatch = false;
- this.selected.pageIdx = this.selected.matchIdx = -1;
- this.offset.pageIdx = previous ? numPages - 1 : 0;
- this.offset.matchIdx = null;
- this.hadMatch = false;
- this.resumeCallback = null;
- this.resumePageIdx = null;
- this.pageMatches = [];
- var self = this;
-
- for (var i = 0; i < numPages; i++) {
- // Wipe out any previous highlighted matches.
- this.updatePage(i);
-
- // As soon as the text is extracted start finding the matches.
- this.extractTextPromises[i].onData(function(pageIdx) {
- // Use a timeout since all the pages may already be extracted and we
- // want to start highlighting before finding all the matches.
- setTimeout(function() {
- self.calcFindMatch(pageIdx);
- });
- });
- }
- }
-
- // If there's no query there's no point in searching.
- if (this.state.query === '') {
- this.updateUIState(FindStates.FIND_FOUND);
- return;
- }
-
- // If we're waiting on a page, we return since we can't do anything else.
- if (this.resumeCallback) {
- return;
- }
-
- var offset = this.offset;
- // If there's already a matchIdx that means we are iterating through a
- // page's matches.
- if (offset.matchIdx !== null) {
- var numPageMatches = this.pageMatches[offset.pageIdx].length;
- if ((!previous && offset.matchIdx + 1 < numPageMatches) ||
- (previous && offset.matchIdx > 0)) {
- // The simple case, we just have advance the matchIdx to select the next
- // match on the page.
- this.hadMatch = true;
- offset.matchIdx = previous ? offset.matchIdx - 1 : offset.matchIdx + 1;
- this.updateMatch(true);
- return;
- }
- // We went beyond the current page's matches, so we advance to the next
- // page.
- this.advanceOffsetPage(previous);
- }
- // Start searching through the page.
- this.nextPageMatch();
- },
-
- nextPageMatch: function() {
- if (this.resumePageIdx !== null)
- console.error('There can only be one pending page.');
-
- var matchesReady = function(matches) {
- var offset = this.offset;
- var numMatches = matches.length;
- var previous = this.state.findPrevious;
- if (numMatches) {
- // There were matches for the page, so initialize the matchIdx.
- this.hadMatch = true;
- offset.matchIdx = previous ? numMatches - 1 : 0;
- this.updateMatch(true);
- } else {
- // No matches attempt to search the next page.
- this.advanceOffsetPage(previous);
- if (offset.wrapped) {
- offset.matchIdx = null;
- if (!this.hadMatch) {
- // No point in wrapping there were no matches.
- this.updateMatch(false);
- return;
- }
- }
- // Search the next page.
- this.nextPageMatch();
- }
- }.bind(this);
-
- var pageIdx = this.offset.pageIdx;
- var pageMatches = this.pageMatches;
- if (!pageMatches[pageIdx]) {
- // The matches aren't ready setup a callback so we can be notified,
- // when they are ready.
- this.resumeCallback = function() {
- matchesReady(pageMatches[pageIdx]);
- };
- this.resumePageIdx = pageIdx;
- return;
- }
- // The matches are finished already.
- matchesReady(pageMatches[pageIdx]);
- },
-
- advanceOffsetPage: function(previous) {
- var offset = this.offset;
- var numPages = this.extractTextPromises.length;
- offset.pageIdx = previous ? offset.pageIdx - 1 : offset.pageIdx + 1;
- offset.matchIdx = null;
- if (offset.pageIdx >= numPages || offset.pageIdx < 0) {
- offset.pageIdx = previous ? numPages - 1 : 0;
- offset.wrapped = true;
- return;
- }
- },
-
- updateMatch: function(found) {
- var state = FindStates.FIND_NOTFOUND;
- var wrapped = this.offset.wrapped;
- this.offset.wrapped = false;
- if (found) {
- var previousPage = this.selected.pageIdx;
- this.selected.pageIdx = this.offset.pageIdx;
- this.selected.matchIdx = this.offset.matchIdx;
- state = wrapped ? FindStates.FIND_WRAPPED : FindStates.FIND_FOUND;
- // Update the currently selected page to wipe out any selected matches.
- if (previousPage !== -1 && previousPage !== this.selected.pageIdx) {
- this.updatePage(previousPage);
- }
- }
- this.updateUIState(state, this.state.findPrevious);
- if (this.selected.pageIdx !== -1) {
- this.updatePage(this.selected.pageIdx, true);
- }
- },
-
- updateUIState: function(state, previous) {
- if (PDFView.supportsIntegratedFind) {
- FirefoxCom.request('updateFindControlState',
- {result: state, findPrevious: previous});
- return;
- }
- PDFFindBar.updateUIState(state, previous);
- }
-};
-
-var PDFFindBar = {
- // TODO: Enable the FindBar *AFTER* the pagesPromise in the load function
- // got resolved
-
- opened: false,
-
- initialize: function() {
- this.bar = document.getElementById('findbar');
- this.toggleButton = document.getElementById('viewFind');
- this.findField = document.getElementById('findInput');
- this.highlightAll = document.getElementById('findHighlightAll');
- this.caseSensitive = document.getElementById('findMatchCase');
- this.findMsg = document.getElementById('findMsg');
- this.findStatusIcon = document.getElementById('findStatusIcon');
-
- var self = this;
- this.toggleButton.addEventListener('click', function() {
- self.toggle();
- });
-
- this.findField.addEventListener('input', function() {
- self.dispatchEvent('');
- });
-
- this.bar.addEventListener('keydown', function(evt) {
- switch (evt.keyCode) {
- case 13: // Enter
- if (evt.target === self.findField) {
- self.dispatchEvent('again', evt.shiftKey);
- }
- break;
- case 27: // Escape
- self.close();
- break;
- }
- });
-
- document.getElementById('findPrevious').addEventListener('click',
- function() { self.dispatchEvent('again', true); }
- );
-
- document.getElementById('findNext').addEventListener('click', function() {
- self.dispatchEvent('again', false);
- });
-
- this.highlightAll.addEventListener('click', function() {
- self.dispatchEvent('highlightallchange');
- });
-
- this.caseSensitive.addEventListener('click', function() {
- self.dispatchEvent('casesensitivitychange');
- });
- },
-
- dispatchEvent: function(aType, aFindPrevious) {
- var event = document.createEvent('CustomEvent');
- event.initCustomEvent('find' + aType, true, true, {
- query: this.findField.value,
- caseSensitive: this.caseSensitive.checked,
- highlightAll: this.highlightAll.checked,
- findPrevious: aFindPrevious
- });
- return window.dispatchEvent(event);
- },
-
- updateUIState: function(state, previous) {
- var notFound = false;
- var findMsg = '';
- var status = '';
-
- switch (state) {
- case FindStates.FIND_FOUND:
- break;
-
- case FindStates.FIND_PENDING:
- status = 'pending';
- break;
-
- case FindStates.FIND_NOTFOUND:
- findMsg = mozL10n.get('find_not_found', null, 'Phrase not found');
- notFound = true;
- break;
-
- case FindStates.FIND_WRAPPED:
- if (previous) {
- findMsg = mozL10n.get('find_reached_top', null,
- 'Reached top of document, continued from bottom');
- } else {
- findMsg = mozL10n.get('find_reached_bottom', null,
- 'Reached end of document, continued from top');
- }
- break;
- }
-
- if (notFound) {
- this.findField.classList.add('notFound');
- } else {
- this.findField.classList.remove('notFound');
- }
-
- this.findField.setAttribute('data-status', status);
- this.findMsg.textContent = findMsg;
- },
-
- open: function() {
- if (this.opened) return;
-
- this.opened = true;
- this.toggleButton.classList.add('toggled');
- this.bar.classList.remove('hidden');
- this.findField.select();
- this.findField.focus();
- },
-
- close: function() {
- if (!this.opened) return;
-
- this.opened = false;
- this.toggleButton.classList.remove('toggled');
- this.bar.classList.add('hidden');
-
- PDFFindController.active = false;
- },
-
- toggle: function() {
- if (this.opened) {
- this.close();
- } else {
- this.open();
- }
- }
-};
-
-var PDFView = {
- pages: [],
- thumbnails: [],
- currentScale: UNKNOWN_SCALE,
- currentScaleValue: null,
- initialBookmark: document.location.hash.substring(1),
- startedTextExtraction: false,
- pageText: [],
- container: null,
- thumbnailContainer: null,
- initialized: false,
- fellback: false,
- pdfDocument: null,
- sidebarOpen: false,
- pageViewScroll: null,
- thumbnailViewScroll: null,
- isFullscreen: false,
- previousScale: null,
- pageRotation: 0,
- mouseScrollTimeStamp: 0,
- mouseScrollDelta: 0,
- lastScroll: 0,
- previousPageNumber: 1,
-
- // called once when the document is loaded
- initialize: function pdfViewInitialize() {
- var self = this;
- var container = this.container = document.getElementById('viewerContainer');
- this.pageViewScroll = {};
- this.watchScroll(container, this.pageViewScroll, updateViewarea);
-
- var thumbnailContainer = this.thumbnailContainer =
- document.getElementById('thumbnailView');
- this.thumbnailViewScroll = {};
- this.watchScroll(thumbnailContainer, this.thumbnailViewScroll,
- this.renderHighestPriority.bind(this));
-
- PDFFindBar.initialize();
- PDFFindController.initialize();
-
- this.initialized = true;
- container.addEventListener('scroll', function() {
- self.lastScroll = Date.now();
- }, false);
- },
-
- getPage: function pdfViewGetPage(n) {
- return this.pdfDocument.getPage(n);
- },
-
- // Helper function to keep track whether a div was scrolled up or down and
- // then call a callback.
- watchScroll: function pdfViewWatchScroll(viewAreaElement, state, callback) {
- state.down = true;
- state.lastY = viewAreaElement.scrollTop;
- viewAreaElement.addEventListener('scroll', function webViewerScroll(evt) {
- var currentY = viewAreaElement.scrollTop;
- var lastY = state.lastY;
- if (currentY > lastY)
- state.down = true;
- else if (currentY < lastY)
- state.down = false;
- // else do nothing and use previous value
- state.lastY = currentY;
- callback();
- }, true);
- },
-
- setScale: function pdfViewSetScale(val, resetAutoSettings, noScroll) {
- if (val == this.currentScale)
- return;
-
- var pages = this.pages;
- for (var i = 0; i < pages.length; i++)
- pages[i].update(val * CSS_UNITS);
-
- if (!noScroll && this.currentScale != val)
- this.pages[this.page - 1].scrollIntoView();
- this.currentScale = val;
-
- var event = document.createEvent('UIEvents');
- event.initUIEvent('scalechange', false, false, window, 0);
- event.scale = val;
- event.resetAutoSettings = resetAutoSettings;
- window.dispatchEvent(event);
- },
-
- parseScale: function pdfViewParseScale(value, resetAutoSettings, noScroll) {
- if ('custom' == value)
- return;
-
- var scale = parseFloat(value);
- this.currentScaleValue = value;
- if (scale) {
- this.setScale(scale, true, noScroll);
- return;
- }
-
- var container = this.container;
- var currentPage = this.pages[this.page - 1];
- if (!currentPage) {
- return;
- }
-
- var pageWidthScale = (container.clientWidth - SCROLLBAR_PADDING) /
- currentPage.width * currentPage.scale / CSS_UNITS;
- var pageHeightScale = (container.clientHeight - VERTICAL_PADDING) /
- currentPage.height * currentPage.scale / CSS_UNITS;
- switch (value) {
- case 'page-actual':
- scale = 1;
- break;
- case 'page-width':
- scale = pageWidthScale;
- break;
- case 'page-height':
- scale = pageHeightScale;
- break;
- case 'page-fit':
- scale = Math.min(pageWidthScale, pageHeightScale);
- break;
- case 'auto':
- scale = Math.min(1.0, pageWidthScale);
- break;
- }
- this.setScale(scale, resetAutoSettings, noScroll);
-
- selectScaleOption(value);
- },
-
- zoomIn: function pdfViewZoomIn() {
- var newScale = (this.currentScale * DEFAULT_SCALE_DELTA).toFixed(2);
- newScale = Math.ceil(newScale * 10) / 10;
- newScale = Math.min(MAX_SCALE, newScale);
- this.parseScale(newScale, true);
- },
-
- zoomOut: function pdfViewZoomOut() {
- var newScale = (this.currentScale / DEFAULT_SCALE_DELTA).toFixed(2);
- newScale = Math.floor(newScale * 10) / 10;
- newScale = Math.max(MIN_SCALE, newScale);
- this.parseScale(newScale, true);
- },
-
- set page(val) {
- var pages = this.pages;
- var input = document.getElementById('pageNumber');
- var event = document.createEvent('UIEvents');
- event.initUIEvent('pagechange', false, false, window, 0);
-
- if (!(0 < val && val <= pages.length)) {
- this.previousPageNumber = val;
- event.pageNumber = this.page;
- window.dispatchEvent(event);
- return;
- }
-
- pages[val - 1].updateStats();
- this.previousPageNumber = currentPageNumber;
- currentPageNumber = val;
- event.pageNumber = val;
- window.dispatchEvent(event);
-
- // checking if the this.page was called from the updateViewarea function:
- // avoiding the creation of two "set page" method (internal and public)
- if (updateViewarea.inProgress)
- return;
-
- // Avoid scrolling the first page during loading
- if (this.loading && val == 1)
- return;
-
- pages[val - 1].scrollIntoView();
- },
-
- get page() {
- return currentPageNumber;
- },
-
- get supportsPrinting() {
- var canvas = document.createElement('canvas');
- var value = 'mozPrintCallback' in canvas;
- // shadow
- Object.defineProperty(this, 'supportsPrinting', { value: value,
- enumerable: true,
- configurable: true,
- writable: false });
- return value;
- },
-
- get supportsFullscreen() {
- var doc = document.documentElement;
- var support = doc.requestFullscreen || doc.mozRequestFullScreen ||
- doc.webkitRequestFullScreen;
-
- // Disable fullscreen button if we're in an iframe
- if (!!window.frameElement)
- support = false;
-
- Object.defineProperty(this, 'supportsFullScreen', { value: support,
- enumerable: true,
- configurable: true,
- writable: false });
- return support;
- },
-
- get supportsIntegratedFind() {
- var support = false;
-//#if !(FIREFOX || MOZCENTRAL)
-//#else
-// support = FirefoxCom.requestSync('supportsIntegratedFind');
-//#endif
- Object.defineProperty(this, 'supportsIntegratedFind', { value: support,
- enumerable: true,
- configurable: true,
- writable: false });
- return support;
- },
-
- get supportsDocumentFonts() {
- var support = true;
-//#if !(FIREFOX || MOZCENTRAL)
-//#else
-// support = FirefoxCom.requestSync('supportsDocumentFonts');
-//#endif
- Object.defineProperty(this, 'supportsDocumentFonts', { value: support,
- enumerable: true,
- configurable: true,
- writable: false });
- return support;
- },
-
- get isHorizontalScrollbarEnabled() {
- var div = document.getElementById('viewerContainer');
- return div.scrollWidth > div.clientWidth;
- },
-
- initPassiveLoading: function pdfViewInitPassiveLoading() {
- if (!PDFView.loadingBar) {
- PDFView.loadingBar = new ProgressBar('#loadingBar', {});
- }
-
- window.addEventListener('message', function window_message(e) {
- var args = e.data;
-
- if (typeof args !== 'object' || !('pdfjsLoadAction' in args))
- return;
- switch (args.pdfjsLoadAction) {
- case 'progress':
- PDFView.progress(args.loaded / args.total);
- break;
- case 'complete':
- if (!args.data) {
- PDFView.error(mozL10n.get('loading_error', null,
- 'An error occurred while loading the PDF.'), e);
- break;
- }
- PDFView.open(args.data, 0);
- break;
- }
- });
- FirefoxCom.requestSync('initPassiveLoading', null);
- },
-
- setTitleUsingUrl: function pdfViewSetTitleUsingUrl(url) {
- this.url = url;
- try {
- this.setTitle(decodeURIComponent(getFileName(url)) || url);
- } catch (e) {
- // decodeURIComponent may throw URIError,
- // fall back to using the unprocessed url in that case
- this.setTitle(url);
- }
- },
-
- setTitle: function pdfViewSetTitle(title) {
- document.title = title;
-//#if B2G
-// document.getElementById('activityTitle').textContent = title;
-//#endif
- },
-
- open: function pdfViewOpen(url, scale, password) {
- var parameters = {password: password};
- if (typeof url === 'string') { // URL
- this.setTitleUsingUrl(url);
- parameters.url = url;
- } else if (url && 'byteLength' in url) { // ArrayBuffer
- parameters.data = url;
- }
-
- if (!PDFView.loadingBar) {
- PDFView.loadingBar = new ProgressBar('#loadingBar', {});
- }
-
- this.pdfDocument = null;
- var self = this;
- self.loading = true;
- PDFJS.getDocument(parameters).then(
- function getDocumentCallback(pdfDocument) {
- self.load(pdfDocument, scale);
- self.loading = false;
- },
- function getDocumentError(message, exception) {
- if (exception && exception.name === 'PasswordException') {
- if (exception.code === 'needpassword') {
- var promptString = mozL10n.get('request_password', null,
- 'PDF is protected by a password:');
- password = prompt(promptString);
- if (password && password.length > 0) {
- return PDFView.open(url, scale, password);
- }
- }
- }
-
- var loadingErrorMessage = mozL10n.get('loading_error', null,
- 'An error occurred while loading the PDF.');
-
- if (exception && exception.name === 'InvalidPDFException') {
- // change error message also for other builds
- var loadingErrorMessage = mozL10n.get('invalid_file_error', null,
- 'Invalid or corrupted PDF file.');
-//#if B2G
-// window.alert(loadingErrorMessage);
-// return window.close();
-//#endif
- }
-
- if (exception && exception.name === 'MissingPDFException') {
- // special message for missing PDF's
- var loadingErrorMessage = mozL10n.get('missing_file_error', null,
- 'Missing PDF file.');
-
-//#if B2G
-// window.alert(loadingErrorMessage);
-// return window.close();
-//#endif
- }
-
- var loadingIndicator = document.getElementById('loading');
- loadingIndicator.textContent = mozL10n.get('loading_error_indicator',
- null, 'Error');
- var moreInfo = {
- message: message
- };
- self.error(loadingErrorMessage, moreInfo);
- self.loading = false;
- },
- function getDocumentProgress(progressData) {
- self.progress(progressData.loaded / progressData.total);
- }
- );
- },
-
- download: function pdfViewDownload() {
- function noData() {
- FirefoxCom.request('download', { originalUrl: url });
- }
- var url = this.url.split('#')[0];
-//#if !(FIREFOX || MOZCENTRAL)
- url += '#pdfjs.action=download';
- window.open(url, '_parent');
-//#else
-// // Document isn't ready just try to download with the url.
-// if (!this.pdfDocument) {
-// noData();
-// return;
-// }
-// this.pdfDocument.getData().then(
-// function getDataSuccess(data) {
-// var blob = PDFJS.createBlob(data.buffer, 'application/pdf');
-// var blobUrl = window.URL.createObjectURL(blob);
-//
-// FirefoxCom.request('download', { blobUrl: blobUrl, originalUrl: url },
-// function response(err) {
-// if (err) {
-// // This error won't really be helpful because it's likely the
-// // fallback won't work either (or is already open).
-// PDFView.error('PDF failed to download.');
-// }
-// window.URL.revokeObjectURL(blobUrl);
-// }
-// );
-// },
-// noData // Error occurred try downloading with just the url.
-// );
-//#endif
- },
-
- fallback: function pdfViewFallback() {
-//#if !(FIREFOX || MOZCENTRAL)
-// return;
-//#else
-// // Only trigger the fallback once so we don't spam the user with messages
-// // for one PDF.
-// if (this.fellback)
-// return;
-// this.fellback = true;
-// var url = this.url.split('#')[0];
-// FirefoxCom.request('fallback', url, function response(download) {
-// if (!download)
-// return;
-// PDFView.download();
-// });
-//#endif
- },
-
- navigateTo: function pdfViewNavigateTo(dest) {
- if (typeof dest === 'string')
- dest = this.destinations[dest];
- if (!(dest instanceof Array))
- return; // invalid destination
- // dest array looks like that: <page-ref> </XYZ|FitXXX> <args..>
- var destRef = dest[0];
- var pageNumber = destRef instanceof Object ?
- this.pagesRefMap[destRef.num + ' ' + destRef.gen + ' R'] : (destRef + 1);
- if (pageNumber > this.pages.length)
- pageNumber = this.pages.length;
- if (pageNumber) {
- this.page = pageNumber;
- var currentPage = this.pages[pageNumber - 1];
- if (!this.isFullscreen) { // Avoid breaking fullscreen mode.
- currentPage.scrollIntoView(dest);
- }
- }
- },
-
- getDestinationHash: function pdfViewGetDestinationHash(dest) {
- if (typeof dest === 'string')
- return PDFView.getAnchorUrl('#' + escape(dest));
- if (dest instanceof Array) {
- var destRef = dest[0]; // see navigateTo method for dest format
- var pageNumber = destRef instanceof Object ?
- this.pagesRefMap[destRef.num + ' ' + destRef.gen + ' R'] :
- (destRef + 1);
- if (pageNumber) {
- var pdfOpenParams = PDFView.getAnchorUrl('#page=' + pageNumber);
- var destKind = dest[1];
- if (typeof destKind === 'object' && 'name' in destKind &&
- destKind.name == 'XYZ') {
- var scale = (dest[4] || this.currentScale);
- pdfOpenParams += '&zoom=' + (scale * 100);
- if (dest[2] || dest[3]) {
- pdfOpenParams += ',' + (dest[2] || 0) + ',' + (dest[3] || 0);
- }
- }
- return pdfOpenParams;
- }
- }
- return '';
- },
-
- /**
- * For the firefox extension we prefix the full url on anchor links so they
- * don't come up as resource:// urls and so open in new tab/window works.
- * @param {String} anchor The anchor hash include the #.
- */
- getAnchorUrl: function getAnchorUrl(anchor) {
-//#if !(FIREFOX || MOZCENTRAL)
- return anchor;
-//#else
-// return this.url.split('#')[0] + anchor;
-//#endif
- },
-
- /**
- * Returns scale factor for the canvas. It makes sense for the HiDPI displays.
- * @return {Object} The object with horizontal (sx) and vertical (sy)
- scales. The scaled property is set to false if scaling is
- not required, true otherwise.
- */
- getOutputScale: function pdfViewGetOutputDPI() {
- var pixelRatio = 'devicePixelRatio' in window ? window.devicePixelRatio : 1;
- return {
- sx: pixelRatio,
- sy: pixelRatio,
- scaled: pixelRatio != 1
- };
- },
-
- /**
- * Show the error box.
- * @param {String} message A message that is human readable.
- * @param {Object} moreInfo (optional) Further information about the error
- * that is more technical. Should have a 'message'
- * and optionally a 'stack' property.
- */
- error: function pdfViewError(message, moreInfo) {
- var moreInfoText = mozL10n.get('error_version_info',
- {version: PDFJS.version || '?', build: PDFJS.build || '?'},
- 'PDF.js v{{version}} (build: {{build}})') + '\n';
- if (moreInfo) {
- moreInfoText +=
- mozL10n.get('error_message', {message: moreInfo.message},
- 'Message: {{message}}');
- if (moreInfo.stack) {
- moreInfoText += '\n' +
- mozL10n.get('error_stack', {stack: moreInfo.stack},
- 'Stack: {{stack}}');
- } else {
- if (moreInfo.filename) {
- moreInfoText += '\n' +
- mozL10n.get('error_file', {file: moreInfo.filename},
- 'File: {{file}}');
- }
- if (moreInfo.lineNumber) {
- moreInfoText += '\n' +
- mozL10n.get('error_line', {line: moreInfo.lineNumber},
- 'Line: {{line}}');
- }
- }
- }
-
- var loadingBox = document.getElementById('loadingBox');
- loadingBox.setAttribute('hidden', 'true');
-
-//#if !(FIREFOX || MOZCENTRAL)
- var errorWrapper = document.getElementById('errorWrapper');
- errorWrapper.removeAttribute('hidden');
-
- var errorMessage = document.getElementById('errorMessage');
- errorMessage.textContent = message;
-
- var closeButton = document.getElementById('errorClose');
- closeButton.onclick = function() {
- errorWrapper.setAttribute('hidden', 'true');
- };
-
- var errorMoreInfo = document.getElementById('errorMoreInfo');
- var moreInfoButton = document.getElementById('errorShowMore');
- var lessInfoButton = document.getElementById('errorShowLess');
- moreInfoButton.onclick = function() {
- errorMoreInfo.removeAttribute('hidden');
- moreInfoButton.setAttribute('hidden', 'true');
- lessInfoButton.removeAttribute('hidden');
- };
- lessInfoButton.onclick = function() {
- errorMoreInfo.setAttribute('hidden', 'true');
- moreInfoButton.removeAttribute('hidden');
- lessInfoButton.setAttribute('hidden', 'true');
- };
- moreInfoButton.removeAttribute('hidden');
- lessInfoButton.setAttribute('hidden', 'true');
- errorMoreInfo.value = moreInfoText;
-
- errorMoreInfo.rows = moreInfoText.split('\n').length - 1;
-//#else
-// console.error(message + '\n' + moreInfoText);
-// this.fallback();
-//#endif
- },
-
- progress: function pdfViewProgress(level) {
- var percent = Math.round(level * 100);
- PDFView.loadingBar.percent = percent;
- },
-
- load: function pdfViewLoad(pdfDocument, scale) {
- function bindOnAfterDraw(pageView, thumbnailView) {
- // when page is painted, using the image as thumbnail base
- pageView.onAfterDraw = function pdfViewLoadOnAfterDraw() {
- thumbnailView.setImage(pageView.canvas);
- };
- }
-
- this.pdfDocument = pdfDocument;
-
- var errorWrapper = document.getElementById('errorWrapper');
- errorWrapper.setAttribute('hidden', 'true');
-
- var loadingBox = document.getElementById('loadingBox');
- loadingBox.setAttribute('hidden', 'true');
- var loadingIndicator = document.getElementById('loading');
- loadingIndicator.textContent = '';
-
- var thumbsView = document.getElementById('thumbnailView');
- thumbsView.parentNode.scrollTop = 0;
-
- while (thumbsView.hasChildNodes())
- thumbsView.removeChild(thumbsView.lastChild);
-
- if ('_loadingInterval' in thumbsView)
- clearInterval(thumbsView._loadingInterval);
-
- var container = document.getElementById('viewer');
- while (container.hasChildNodes())
- container.removeChild(container.lastChild);
-
- var pagesCount = pdfDocument.numPages;
- var id = pdfDocument.fingerprint;
- document.getElementById('numPages').textContent =
- mozL10n.get('page_of', {pageCount: pagesCount}, 'of {{pageCount}}');
- document.getElementById('pageNumber').max = pagesCount;
-
- PDFView.documentFingerprint = id;
- var store = PDFView.store = new Settings(id);
-
- this.pageRotation = 0;
-
- var pages = this.pages = [];
- this.pageText = [];
- this.startedTextExtraction = false;
- var pagesRefMap = this.pagesRefMap = {};
- var thumbnails = this.thumbnails = [];
-
- var pagesPromise = new PDFJS.Promise();
- var self = this;
-
- var firstPagePromise = pdfDocument.getPage(1);
-
- // Fetch a single page so we can get a viewport that will be the default
- // viewport for all pages
- firstPagePromise.then(function(pdfPage) {
- var viewport = pdfPage.getViewport(scale || 1.0);
- var pagePromises = [];
- for (var pageNum = 1; pageNum <= pagesCount; ++pageNum) {
- var viewportClone = viewport.clone();
- var pageView = new PageView(container, pageNum, scale,
- self.navigateTo.bind(self),
- viewportClone);
- var thumbnailView = new ThumbnailView(thumbsView, pageNum,
- viewportClone);
- bindOnAfterDraw(pageView, thumbnailView);
- pages.push(pageView);
- thumbnails.push(thumbnailView);
- }
-
- var event = document.createEvent('CustomEvent');
- event.initCustomEvent('documentload', true, true, {});
- window.dispatchEvent(event);
-
- for (var pageNum = 1; pageNum <= pagesCount; ++pageNum) {
- var pagePromise = pdfDocument.getPage(pageNum);
- pagePromise.then(function(pdfPage) {
- var pageNum = pdfPage.pageNumber;
- var pageView = pages[pageNum - 1];
- if (!pageView.pdfPage) {
- // The pdfPage might already be set if we've already entered
- // pageView.draw()
- pageView.setPdfPage(pdfPage);
- }
- var thumbnailView = thumbnails[pageNum - 1];
- if (!thumbnailView.pdfPage) {
- thumbnailView.setPdfPage(pdfPage);
- }
-
- var pageRef = pdfPage.ref;
- var refStr = pageRef.num + ' ' + pageRef.gen + ' R';
- pagesRefMap[refStr] = pdfPage.pageNumber;
- });
- pagePromises.push(pagePromise);
- }
-
- PDFJS.Promise.all(pagePromises).then(function(pages) {
- pagesPromise.resolve(pages);
- });
- });
-
- var storePromise = store.initializedPromise;
- PDFJS.Promise.all([firstPagePromise, storePromise]).then(function() {
- var storedHash = null;
- if (store.get('exists', false)) {
- var pageNum = store.get('page', '1');
- var zoom = store.get('zoom', PDFView.currentScale);
- var left = store.get('scrollLeft', '0');
- var top = store.get('scrollTop', '0');
-
- storedHash = 'page=' + pageNum + '&zoom=' + zoom + ',' +
- left + ',' + top;
- }
- self.setInitialView(storedHash, scale);
- });
-
- pagesPromise.then(function() {
- if (PDFView.supportsPrinting) {
- pdfDocument.getJavaScript().then(function(javaScript) {
- if (javaScript.length) {
- console.warn('Warning: JavaScript is not supported');
- PDFView.fallback();
- }
- // Hack to support auto printing.
- var regex = /\bprint\s*\(/g;
- for (var i = 0, ii = javaScript.length; i < ii; i++) {
- var js = javaScript[i];
- if (js && regex.test(js)) {
- setTimeout(function() {
- window.print();
- });
- return;
- }
- }
- });
- }
- });
-
- var destinationsPromise = pdfDocument.getDestinations();
- destinationsPromise.then(function(destinations) {
- self.destinations = destinations;
- });
-
- // outline depends on destinations and pagesRefMap
- var promises = [pagesPromise, destinationsPromise,
- PDFView.animationStartedPromise];
- PDFJS.Promise.all(promises).then(function() {
- pdfDocument.getOutline().then(function(outline) {
- self.outline = new DocumentOutlineView(outline);
- });
-
- // Make all navigation keys work on document load,
- // unless the viewer is embedded in another page.
- if (window.parent.location === window.location) {
- PDFView.container.focus();
- }
- });
-
- pdfDocument.getMetadata().then(function(data) {
- var info = data.info, metadata = data.metadata;
- self.documentInfo = info;
- self.metadata = metadata;
-
- // Provides some basic debug information
- console.log('PDF ' + pdfDocument.fingerprint + ' [' +
- info.PDFFormatVersion + ' ' + (info.Producer || '-') +
- ' / ' + (info.Creator || '-') + ']' +
- (PDFJS.version ? ' (PDF.js: ' + PDFJS.version + ')' : ''));
-
- var pdfTitle;
- if (metadata) {
- if (metadata.has('dc:title'))
- pdfTitle = metadata.get('dc:title');
- }
-
- if (!pdfTitle && info && info['Title'])
- pdfTitle = info['Title'];
-
- if (pdfTitle)
- self.setTitle(pdfTitle + ' - ' + document.title);
-
- if (info.IsAcroFormPresent) {
- console.warn('Warning: AcroForm/XFA is not supported');
- PDFView.fallback();
- }
- });
- },
-
- setInitialView: function pdfViewSetInitialView(storedHash, scale) {
- // Reset the current scale, as otherwise the page's scale might not get
- // updated if the zoom level stayed the same.
- this.currentScale = 0;
- this.currentScaleValue = null;
- if (this.initialBookmark) {
- this.setHash(this.initialBookmark);
- this.initialBookmark = null;
- }
- else if (storedHash)
- this.setHash(storedHash);
- else if (scale) {
- this.parseScale(scale, true);
- this.page = 1;
- }
-
- if (PDFView.currentScale === UNKNOWN_SCALE) {
- // Scale was not initialized: invalid bookmark or scale was not specified.
- // Setting the default one.
- this.parseScale(DEFAULT_SCALE, true);
- }
- },
-
- renderHighestPriority: function pdfViewRenderHighestPriority() {
- // Pages have a higher priority than thumbnails, so check them first.
- var visiblePages = this.getVisiblePages();
- var pageView = this.getHighestPriority(visiblePages, this.pages,
- this.pageViewScroll.down);
- if (pageView) {
- this.renderView(pageView, 'page');
- return;
- }
- // No pages needed rendering so check thumbnails.
- if (this.sidebarOpen) {
- var visibleThumbs = this.getVisibleThumbs();
- var thumbView = this.getHighestPriority(visibleThumbs,
- this.thumbnails,
- this.thumbnailViewScroll.down);
- if (thumbView)
- this.renderView(thumbView, 'thumbnail');
- }
- },
-
- getHighestPriority: function pdfViewGetHighestPriority(visible, views,
- scrolledDown) {
- // The state has changed figure out which page has the highest priority to
- // render next (if any).
- // Priority:
- // 1 visible pages
- // 2 if last scrolled down page after the visible pages
- // 2 if last scrolled up page before the visible pages
- var visibleViews = visible.views;
-
- var numVisible = visibleViews.length;
- if (numVisible === 0) {
- return false;
- }
- for (var i = 0; i < numVisible; ++i) {
- var view = visibleViews[i].view;
- if (!this.isViewFinished(view))
- return view;
- }
-
- // All the visible views have rendered, try to render next/previous pages.
- if (scrolledDown) {
- var nextPageIndex = visible.last.id;
- // ID's start at 1 so no need to add 1.
- if (views[nextPageIndex] && !this.isViewFinished(views[nextPageIndex]))
- return views[nextPageIndex];
- } else {
- var previousPageIndex = visible.first.id - 2;
- if (views[previousPageIndex] &&
- !this.isViewFinished(views[previousPageIndex]))
- return views[previousPageIndex];
- }
- // Everything that needs to be rendered has been.
- return false;
- },
-
- isViewFinished: function pdfViewNeedsRendering(view) {
- return view.renderingState === RenderingStates.FINISHED;
- },
-
- // Render a page or thumbnail view. This calls the appropriate function based
- // on the views state. If the view is already rendered it will return false.
- renderView: function pdfViewRender(view, type) {
- var state = view.renderingState;
- switch (state) {
- case RenderingStates.FINISHED:
- return false;
- case RenderingStates.PAUSED:
- PDFView.highestPriorityPage = type + view.id;
- view.resume();
- break;
- case RenderingStates.RUNNING:
- PDFView.highestPriorityPage = type + view.id;
- break;
- case RenderingStates.INITIAL:
- PDFView.highestPriorityPage = type + view.id;
- view.draw(this.renderHighestPriority.bind(this));
- break;
- }
- return true;
- },
-
- setHash: function pdfViewSetHash(hash) {
- if (!hash)
- return;
-
- if (hash.indexOf('=') >= 0) {
- var params = PDFView.parseQueryString(hash);
- // borrowing syntax from "Parameters for Opening PDF Files"
- if ('nameddest' in params) {
- PDFView.navigateTo(params.nameddest);
- return;
- }
- if ('page' in params) {
- var pageNumber = (params.page | 0) || 1;
- if ('zoom' in params) {
- var zoomArgs = params.zoom.split(','); // scale,left,top
- // building destination array
-
- // If the zoom value, it has to get divided by 100. If it is a string,
- // it should stay as it is.
- var zoomArg = zoomArgs[0];
- var zoomArgNumber = parseFloat(zoomArg);
- if (zoomArgNumber)
- zoomArg = zoomArgNumber / 100;
-
- var dest = [null, {name: 'XYZ'},
- zoomArgs.length > 1 ? (zoomArgs[1] | 0) : null,
- zoomArgs.length > 2 ? (zoomArgs[2] | 0) : null,
- zoomArg];
- var currentPage = this.pages[pageNumber - 1];
- currentPage.scrollIntoView(dest);
- } else {
- this.page = pageNumber; // simple page
- }
- }
- if ('pagemode' in params) {
- var toggle = document.getElementById('sidebarToggle');
- if (params.pagemode === 'thumbs' || params.pagemode === 'bookmarks') {
- if (!this.sidebarOpen) {
- toggle.click();
- }
- this.switchSidebarView(params.pagemode === 'thumbs' ?
- 'thumbs' : 'outline');
- } else if (params.pagemode === 'none' && this.sidebarOpen) {
- toggle.click();
- }
- }
- } else if (/^\d+$/.test(hash)) // page number
- this.page = hash;
- else // named destination
- PDFView.navigateTo(unescape(hash));
- },
-
- switchSidebarView: function pdfViewSwitchSidebarView(view) {
- var thumbsView = document.getElementById('thumbnailView');
- var outlineView = document.getElementById('outlineView');
-
- var thumbsButton = document.getElementById('viewThumbnail');
- var outlineButton = document.getElementById('viewOutline');
-
- switch (view) {
- case 'thumbs':
- var wasOutlineViewVisible = thumbsView.classList.contains('hidden');
-
- thumbsButton.classList.add('toggled');
- outlineButton.classList.remove('toggled');
- thumbsView.classList.remove('hidden');
- outlineView.classList.add('hidden');
-
- PDFView.renderHighestPriority();
-
- if (wasOutlineViewVisible) {
- // Ensure that the thumbnail of the current page is visible
- // when switching from the outline view.
- scrollIntoView(document.getElementById('thumbnailContainer' +
- this.page));
- }
- break;
-
- case 'outline':
- thumbsButton.classList.remove('toggled');
- outlineButton.classList.add('toggled');
- thumbsView.classList.add('hidden');
- outlineView.classList.remove('hidden');
-
- if (outlineButton.getAttribute('disabled'))
- return;
- break;
- }
- },
-
- getVisiblePages: function pdfViewGetVisiblePages() {
- if (!this.isFullscreen) {
- return this.getVisibleElements(this.container, this.pages, true);
- } else {
- // The algorithm in getVisibleElements is broken in fullscreen mode.
- var visible = [], page = this.page;
- var currentPage = this.pages[page - 1];
- visible.push({ id: currentPage.id, view: currentPage });
-
- return { first: currentPage, last: currentPage, views: visible};
- }
- },
-
- getVisibleThumbs: function pdfViewGetVisibleThumbs() {
- return this.getVisibleElements(this.thumbnailContainer, this.thumbnails);
- },
-
- // Generic helper to find out what elements are visible within a scroll pane.
- getVisibleElements: function pdfViewGetVisibleElements(
- scrollEl, views, sortByVisibility) {
- var top = scrollEl.scrollTop, bottom = top + scrollEl.clientHeight;
- var left = scrollEl.scrollLeft, right = left + scrollEl.clientWidth;
-
- var visible = [], view;
- var currentHeight, viewHeight, hiddenHeight, percentHeight;
- var currentWidth, viewWidth;
- for (var i = 0, ii = views.length; i < ii; ++i) {
- view = views[i];
- currentHeight = view.el.offsetTop + view.el.clientTop;
- viewHeight = view.el.clientHeight;
- if ((currentHeight + viewHeight) < top) {
- continue;
- }
- if (currentHeight > bottom) {
- break;
- }
- currentWidth = view.el.offsetLeft + view.el.clientLeft;
- viewWidth = view.el.clientWidth;
- if ((currentWidth + viewWidth) < left || currentWidth > right) {
- continue;
- }
- hiddenHeight = Math.max(0, top - currentHeight) +
- Math.max(0, currentHeight + viewHeight - bottom);
- percentHeight = ((viewHeight - hiddenHeight) * 100 / viewHeight) | 0;
-
- visible.push({ id: view.id, y: currentHeight,
- view: view, percent: percentHeight });
- }
-
- var first = visible[0];
- var last = visible[visible.length - 1];
-
- if (sortByVisibility) {
- visible.sort(function(a, b) {
- var pc = a.percent - b.percent;
- if (Math.abs(pc) > 0.001) {
- return -pc;
- }
- return a.id - b.id; // ensure stability
- });
- }
- return {first: first, last: last, views: visible};
- },
-
- // Helper function to parse query string (e.g. ?param1=value&parm2=...).
- parseQueryString: function pdfViewParseQueryString(query) {
- var parts = query.split('&');
- var params = {};
- for (var i = 0, ii = parts.length; i < parts.length; ++i) {
- var param = parts[i].split('=');
- var key = param[0];
- var value = param.length > 1 ? param[1] : null;
- params[unescape(key)] = unescape(value);
- }
- return params;
- },
-
- beforePrint: function pdfViewSetupBeforePrint() {
- if (!this.supportsPrinting) {
- var printMessage = mozL10n.get('printing_not_supported', null,
- 'Warning: Printing is not fully supported by this browser.');
- this.error(printMessage);
- return;
- }
-
- var alertNotReady = false;
- if (!this.pages.length) {
- alertNotReady = true;
- } else {
- for (var i = 0, ii = this.pages.length; i < ii; ++i) {
- if (!this.pages[i].pdfPage) {
- alertNotReady = true;
- break;
- }
- }
- }
- if (alertNotReady) {
- var notReadyMessage = mozL10n.get('printing_not_ready', null,
- 'Warning: The PDF is not fully loaded for printing.');
- window.alert(notReadyMessage);
- return;
- }
-
- var body = document.querySelector('body');
- body.setAttribute('data-mozPrintCallback', true);
- for (var i = 0, ii = this.pages.length; i < ii; ++i) {
- this.pages[i].beforePrint();
- }
- },
-
- afterPrint: function pdfViewSetupAfterPrint() {
- var div = document.getElementById('printContainer');
- while (div.hasChildNodes())
- div.removeChild(div.lastChild);
- },
-
- fullscreen: function pdfViewFullscreen() {
- var isFullscreen = document.fullscreenElement || document.mozFullScreen ||
- document.webkitIsFullScreen;
-
- if (isFullscreen) {
- return false;
- }
-
- var wrapper = document.getElementById('viewerContainer');
- if (document.documentElement.requestFullscreen) {
- wrapper.requestFullscreen();
- } else if (document.documentElement.mozRequestFullScreen) {
- wrapper.mozRequestFullScreen();
- } else if (document.documentElement.webkitRequestFullScreen) {
- wrapper.webkitRequestFullScreen(Element.ALLOW_KEYBOARD_INPUT);
- } else {
- return false;
- }
-
- this.isFullscreen = true;
- var currentPage = this.pages[this.page - 1];
- this.previousScale = this.currentScaleValue;
- this.parseScale('page-fit', true);
-
- // Wait for fullscreen to take effect
- setTimeout(function() {
- currentPage.scrollIntoView();
- }, 0);
-
- this.showPresentationControls();
- return true;
- },
-
- exitFullscreen: function pdfViewExitFullscreen() {
- this.isFullscreen = false;
- this.parseScale(this.previousScale);
- this.page = this.page;
- this.clearMouseScrollState();
- this.hidePresentationControls();
-
- // Ensure that the thumbnail of the current page is visible
- // when exiting fullscreen mode.
- scrollIntoView(document.getElementById('thumbnailContainer' + this.page));
- },
-
- showPresentationControls: function pdfViewShowPresentationControls() {
- var DELAY_BEFORE_HIDING_CONTROLS = 3000;
- var wrapper = document.getElementById('viewerContainer');
- if (this.presentationControlsTimeout) {
- clearTimeout(this.presentationControlsTimeout);
- } else {
- wrapper.classList.add('presentationControls');
- }
- this.presentationControlsTimeout = setTimeout(function hideControls() {
- wrapper.classList.remove('presentationControls');
- delete PDFView.presentationControlsTimeout;
- }, DELAY_BEFORE_HIDING_CONTROLS);
- },
-
- hidePresentationControls: function pdfViewShowPresentationControls() {
- if (!this.presentationControlsTimeout) {
- return;
- }
- clearTimeout(this.presentationControlsTimeout);
- delete this.presentationControlsTimeout;
-
- var wrapper = document.getElementById('viewerContainer');
- wrapper.classList.remove('presentationControls');
- },
-
- rotatePages: function pdfViewPageRotation(delta) {
-
- this.pageRotation = (this.pageRotation + 360 + delta) % 360;
-
- for (var i = 0, l = this.pages.length; i < l; i++) {
- var page = this.pages[i];
- page.update(page.scale, this.pageRotation);
- }
-
- for (var i = 0, l = this.thumbnails.length; i < l; i++) {
- var thumb = this.thumbnails[i];
- thumb.update(this.pageRotation);
- }
-
- this.parseScale(this.currentScaleValue, true);
-
- this.renderHighestPriority();
-
- var currentPage = this.pages[this.page - 1];
- if (!currentPage) {
- return;
- }
-
- // Wait for fullscreen to take effect
- setTimeout(function() {
- currentPage.scrollIntoView();
- }, 0);
- },
-
- /**
- * This function flips the page in presentation mode if the user scrolls up
- * or down with large enough motion and prevents page flipping too often.
- *
- * @this {PDFView}
- * @param {number} mouseScrollDelta The delta value from the mouse event.
- */
- mouseScroll: function pdfViewMouseScroll(mouseScrollDelta) {
- var MOUSE_SCROLL_COOLDOWN_TIME = 50;
-
- var currentTime = (new Date()).getTime();
- var storedTime = this.mouseScrollTimeStamp;
-
- // In case one page has already been flipped there is a cooldown time
- // which has to expire before next page can be scrolled on to.
- if (currentTime > storedTime &&
- currentTime - storedTime < MOUSE_SCROLL_COOLDOWN_TIME)
- return;
-
- // In case the user decides to scroll to the opposite direction than before
- // clear the accumulated delta.
- if ((this.mouseScrollDelta > 0 && mouseScrollDelta < 0) ||
- (this.mouseScrollDelta < 0 && mouseScrollDelta > 0))
- this.clearMouseScrollState();
-
- this.mouseScrollDelta += mouseScrollDelta;
-
- var PAGE_FLIP_THRESHOLD = 120;
- if (Math.abs(this.mouseScrollDelta) >= PAGE_FLIP_THRESHOLD) {
-
- var PageFlipDirection = {
- UP: -1,
- DOWN: 1
- };
-
- // In fullscreen mode scroll one page at a time.
- var pageFlipDirection = (this.mouseScrollDelta > 0) ?
- PageFlipDirection.UP :
- PageFlipDirection.DOWN;
- this.clearMouseScrollState();
- var currentPage = this.page;
-
- // In case we are already on the first or the last page there is no need
- // to do anything.
- if ((currentPage == 1 && pageFlipDirection == PageFlipDirection.UP) ||
- (currentPage == this.pages.length &&
- pageFlipDirection == PageFlipDirection.DOWN))
- return;
-
- this.page += pageFlipDirection;
- this.mouseScrollTimeStamp = currentTime;
- }
- },
-
- /**
- * This function clears the member attributes used with mouse scrolling in
- * presentation mode.
- *
- * @this {PDFView}
- */
- clearMouseScrollState: function pdfViewClearMouseScrollState() {
- this.mouseScrollTimeStamp = 0;
- this.mouseScrollDelta = 0;
- }
-};
-
-var PageView = function pageView(container, id, scale,
- navigateTo, defaultViewport) {
- this.id = id;
-
- this.rotation = 0;
- this.scale = scale || 1.0;
- this.viewport = defaultViewport;
- this.pdfPageRotate = defaultViewport.rotate;
-
- this.renderingState = RenderingStates.INITIAL;
- this.resume = null;
-
- this.textContent = null;
- this.textLayer = null;
-
- var anchor = document.createElement('a');
- anchor.name = '' + this.id;
-
- var div = this.el = document.createElement('div');
- div.id = 'pageContainer' + this.id;
- div.className = 'page';
- div.style.width = Math.floor(this.viewport.width) + 'px';
- div.style.height = Math.floor(this.viewport.height) + 'px';
-
- container.appendChild(anchor);
- container.appendChild(div);
-
- this.setPdfPage = function pageViewSetPdfPage(pdfPage) {
- this.pdfPage = pdfPage;
- this.pdfPageRotate = pdfPage.rotate;
- this.viewport = pdfPage.getViewport(this.scale);
- this.stats = pdfPage.stats;
- this.update();
- };
-
- this.destroy = function pageViewDestroy() {
- this.update();
- if (this.pdfPage) {
- this.pdfPage.destroy();
- }
- };
-
- this.update = function pageViewUpdate(scale, rotation) {
- this.renderingState = RenderingStates.INITIAL;
- this.resume = null;
-
- if (typeof rotation !== 'undefined') {
- this.rotation = rotation;
- }
-
- this.scale = scale || this.scale;
-
- var totalRotation = (this.rotation + this.pdfPageRotate) % 360;
- this.viewport = this.viewport.clone({
- scale: this.scale,
- rotation: totalRotation
- });
-
- div.style.width = Math.floor(this.viewport.width) + 'px';
- div.style.height = Math.floor(this.viewport.height) + 'px';
-
- while (div.hasChildNodes())
- div.removeChild(div.lastChild);
- div.removeAttribute('data-loaded');
-
- delete this.canvas;
-
- this.loadingIconDiv = document.createElement('div');
- this.loadingIconDiv.className = 'loadingIcon';
- div.appendChild(this.loadingIconDiv);
- };
-
- Object.defineProperty(this, 'width', {
- get: function PageView_getWidth() {
- return this.viewport.width;
- },
- enumerable: true
- });
-
- Object.defineProperty(this, 'height', {
- get: function PageView_getHeight() {
- return this.viewport.height;
- },
- enumerable: true
- });
-
- function setupAnnotations(pdfPage, viewport) {
- function bindLink(link, dest) {
- link.href = PDFView.getDestinationHash(dest);
- link.onclick = function pageViewSetupLinksOnclick() {
- if (dest)
- PDFView.navigateTo(dest);
- return false;
- };
- }
- function createElementWithStyle(tagName, item, rect) {
- if (!rect) {
- rect = viewport.convertToViewportRectangle(item.rect);
- rect = PDFJS.Util.normalizeRect(rect);
- }
- var element = document.createElement(tagName);
- element.style.left = Math.floor(rect[0]) + 'px';
- element.style.top = Math.floor(rect[1]) + 'px';
- element.style.width = Math.ceil(rect[2] - rect[0]) + 'px';
- element.style.height = Math.ceil(rect[3] - rect[1]) + 'px';
- return element;
- }
- function createTextAnnotation(item) {
- var container = document.createElement('section');
- container.className = 'annotText';
-
- var rect = viewport.convertToViewportRectangle(item.rect);
- rect = PDFJS.Util.normalizeRect(rect);
- // sanity check because of OOo-generated PDFs
- if ((rect[3] - rect[1]) < ANNOT_MIN_SIZE) {
- rect[3] = rect[1] + ANNOT_MIN_SIZE;
- }
- if ((rect[2] - rect[0]) < ANNOT_MIN_SIZE) {
- rect[2] = rect[0] + (rect[3] - rect[1]); // make it square
- }
- var image = createElementWithStyle('img', item, rect);
- var iconName = item.name;
- image.src = IMAGE_DIR + 'annotation-' +
- iconName.toLowerCase() + '.svg';
- image.alt = mozL10n.get('text_annotation_type', {type: iconName},
- '[{{type}} Annotation]');
- var content = document.createElement('div');
- content.setAttribute('hidden', true);
- var title = document.createElement('h1');
- var text = document.createElement('p');
- content.style.left = Math.floor(rect[2]) + 'px';
- content.style.top = Math.floor(rect[1]) + 'px';
- title.textContent = item.title;
-
- if (!item.content && !item.title) {
- content.setAttribute('hidden', true);
- } else {
- var e = document.createElement('span');
- var lines = item.content.split(/(?:\r\n?|\n)/);
- for (var i = 0, ii = lines.length; i < ii; ++i) {
- var line = lines[i];
- e.appendChild(document.createTextNode(line));
- if (i < (ii - 1))
- e.appendChild(document.createElement('br'));
- }
- text.appendChild(e);
- image.addEventListener('mouseover', function annotationImageOver() {
- content.removeAttribute('hidden');
- }, false);
-
- image.addEventListener('mouseout', function annotationImageOut() {
- content.setAttribute('hidden', true);
- }, false);
- }
-
- content.appendChild(title);
- content.appendChild(text);
- container.appendChild(image);
- container.appendChild(content);
-
- return container;
- }
-
- pdfPage.getAnnotations().then(function(items) {
- for (var i = 0; i < items.length; i++) {
- var item = items[i];
- switch (item.type) {
- case 'Link':
- var link = createElementWithStyle('a', item);
- link.href = item.url || '';
- if (!item.url)
- bindLink(link, ('dest' in item) ? item.dest : null);
- div.appendChild(link);
- break;
- case 'Text':
- var textAnnotation = createTextAnnotation(item);
- if (textAnnotation)
- div.appendChild(textAnnotation);
- break;
- }
- }
- });
- }
-
- this.getPagePoint = function pageViewGetPagePoint(x, y) {
- return this.viewport.convertToPdfPoint(x, y);
- };
-
- this.scrollIntoView = function pageViewScrollIntoView(dest) {
- if (!dest) {
- scrollIntoView(div);
- return;
- }
-
- var x = 0, y = 0;
- var width = 0, height = 0, widthScale, heightScale;
- var scale = 0;
- switch (dest[1].name) {
- case 'XYZ':
- x = dest[2];
- y = dest[3];
- scale = dest[4];
- // If x and/or y coordinates are not supplied, default to
- // _top_ left of the page (not the obvious bottom left,
- // since aligning the bottom of the intended page with the
- // top of the window is rarely helpful).
- x = x !== null ? x : 0;
- y = y !== null ? y : this.height / this.scale;
- break;
- case 'Fit':
- case 'FitB':
- scale = 'page-fit';
- break;
- case 'FitH':
- case 'FitBH':
- y = dest[2];
- scale = 'page-width';
- break;
- case 'FitV':
- case 'FitBV':
- x = dest[2];
- scale = 'page-height';
- break;
- case 'FitR':
- x = dest[2];
- y = dest[3];
- width = dest[4] - x;
- height = dest[5] - y;
- widthScale = (this.container.clientWidth - SCROLLBAR_PADDING) /
- width / CSS_UNITS;
- heightScale = (this.container.clientHeight - SCROLLBAR_PADDING) /
- height / CSS_UNITS;
- scale = Math.min(widthScale, heightScale);
- break;
- default:
- return;
- }
-
- if (scale && scale !== PDFView.currentScale)
- PDFView.parseScale(scale, true, true);
- else if (PDFView.currentScale === UNKNOWN_SCALE)
- PDFView.parseScale(DEFAULT_SCALE, true, true);
-
- var boundingRect = [
- this.viewport.convertToViewportPoint(x, y),
- this.viewport.convertToViewportPoint(x + width, y + height)
- ];
- setTimeout(function pageViewScrollIntoViewRelayout() {
- // letting page to re-layout before scrolling
- var scale = PDFView.currentScale;
- var x = Math.min(boundingRect[0][0], boundingRect[1][0]);
- var y = Math.min(boundingRect[0][1], boundingRect[1][1]);
- var width = Math.abs(boundingRect[0][0] - boundingRect[1][0]);
- var height = Math.abs(boundingRect[0][1] - boundingRect[1][1]);
-
- scrollIntoView(div, {left: x, top: y, width: width, height: height});
- }, 0);
- };
-
- this.getTextContent = function pageviewGetTextContent() {
- if (!this.textContent) {
- this.textContent = this.pdfPage.getTextContent();
- }
- return this.textContent;
- };
-
- this.draw = function pageviewDraw(callback) {
- var pdfPage = this.pdfPage;
-
- if (!pdfPage) {
- var promise = PDFView.getPage(this.id);
- promise.then(function(pdfPage) {
- this.setPdfPage(pdfPage);
- this.draw(callback);
- }.bind(this));
- return;
- }
-
- if (this.renderingState !== RenderingStates.INITIAL) {
- console.error('Must be in new state before drawing');
- }
-
- this.renderingState = RenderingStates.RUNNING;
-
- var canvas = document.createElement('canvas');
- canvas.id = 'page' + this.id;
- div.appendChild(canvas);
- this.canvas = canvas;
-
- var scale = this.scale, viewport = this.viewport;
- var outputScale = PDFView.getOutputScale();
- canvas.width = Math.floor(viewport.width) * outputScale.sx;
- canvas.height = Math.floor(viewport.height) * outputScale.sy;
-
- var textLayerDiv = null;
- if (!PDFJS.disableTextLayer) {
- textLayerDiv = document.createElement('div');
- textLayerDiv.className = 'textLayer';
- textLayerDiv.style.width = canvas.width + 'px';
- textLayerDiv.style.height = canvas.height + 'px';
- div.appendChild(textLayerDiv);
- }
- var textLayer = this.textLayer =
- textLayerDiv ? new TextLayerBuilder(textLayerDiv, this.id - 1) : null;
-
- if (outputScale.scaled) {
- var cssScale = 'scale(' + (1 / outputScale.sx) + ', ' +
- (1 / outputScale.sy) + ')';
- CustomStyle.setProp('transform' , canvas, cssScale);
- CustomStyle.setProp('transformOrigin' , canvas, '0% 0%');
- if (textLayerDiv) {
- CustomStyle.setProp('transform' , textLayerDiv, cssScale);
- CustomStyle.setProp('transformOrigin' , textLayerDiv, '0% 0%');
- }
- }
-
- var ctx = canvas.getContext('2d');
- ctx.clearRect(0, 0, canvas.width, canvas.height);
- // TODO(mack): use data attributes to store these
- ctx._scaleX = outputScale.sx;
- ctx._scaleY = outputScale.sy;
- if (outputScale.scaled) {
- ctx.scale(outputScale.sx, outputScale.sy);
- }
-//#if (FIREFOX || MOZCENTRAL)
-// // Checking if document fonts are used only once
-// var checkIfDocumentFontsUsed = !PDFView.pdfDocument.embeddedFontsUsed;
-//#endif
-
- // Rendering area
-
- var self = this;
- var renderingWasReset = false;
- function pageViewDrawCallback(error) {
- if (renderingWasReset) {
- return;
- }
-
- self.renderingState = RenderingStates.FINISHED;
-
- if (self.loadingIconDiv) {
- div.removeChild(self.loadingIconDiv);
- delete self.loadingIconDiv;
- }
-
-//#if (FIREFOX || MOZCENTRAL)
-// if (checkIfDocumentFontsUsed && PDFView.pdfDocument.embeddedFontsUsed &&
-// !PDFView.supportsDocumentFonts) {
-// console.error(mozL10n.get('web_fonts_disabled', null,
-// 'Web fonts are disabled: unable to use embedded PDF fonts.'));
-// PDFView.fallback();
-// }
-//#endif
- if (error) {
- PDFView.error(mozL10n.get('rendering_error', null,
- 'An error occurred while rendering the page.'), error);
- }
-
- self.stats = pdfPage.stats;
- self.updateStats();
- if (self.onAfterDraw)
- self.onAfterDraw();
-
- cache.push(self);
-
- var event = document.createEvent('CustomEvent');
- event.initCustomEvent('pagerender', true, true, {
- pageNumber: pdfPage.pageNumber
- });
- div.dispatchEvent(event);
-
- callback();
- }
-
- var renderContext = {
- canvasContext: ctx,
- viewport: this.viewport,
- textLayer: textLayer,
- continueCallback: function pdfViewcContinueCallback(cont) {
- if (self.renderingState === RenderingStates.INITIAL) {
- // The page update() was called, we just need to abort any rendering.
- renderingWasReset = true;
- return;
- }
-
- if (PDFView.highestPriorityPage !== 'page' + self.id) {
- self.renderingState = RenderingStates.PAUSED;
- self.resume = function resumeCallback() {
- self.renderingState = RenderingStates.RUNNING;
- cont();
- };
- return;
- }
- cont();
- }
- };
- this.pdfPage.render(renderContext).then(
- function pdfPageRenderCallback() {
- pageViewDrawCallback(null);
- },
- function pdfPageRenderError(error) {
- pageViewDrawCallback(error);
- }
- );
-
- if (textLayer) {
- this.getTextContent().then(
- function textContentResolved(textContent) {
- textLayer.setTextContent(textContent);
- }
- );
- }
-
- setupAnnotations(this.pdfPage, this.viewport);
- div.setAttribute('data-loaded', true);
- };
-
- this.beforePrint = function pageViewBeforePrint() {
- var pdfPage = this.pdfPage;
-
- var viewport = pdfPage.getViewport(1);
- // Use the same hack we use for high dpi displays for printing to get better
- // output until bug 811002 is fixed in FF.
- var PRINT_OUTPUT_SCALE = 2;
- var canvas = this.canvas = document.createElement('canvas');
- canvas.width = Math.floor(viewport.width) * PRINT_OUTPUT_SCALE;
- canvas.height = Math.floor(viewport.height) * PRINT_OUTPUT_SCALE;
- canvas.style.width = (PRINT_OUTPUT_SCALE * viewport.width) + 'pt';
- canvas.style.height = (PRINT_OUTPUT_SCALE * viewport.height) + 'pt';
- var cssScale = 'scale(' + (1 / PRINT_OUTPUT_SCALE) + ', ' +
- (1 / PRINT_OUTPUT_SCALE) + ')';
- CustomStyle.setProp('transform' , canvas, cssScale);
- CustomStyle.setProp('transformOrigin' , canvas, '0% 0%');
-
- var printContainer = document.getElementById('printContainer');
- printContainer.appendChild(canvas);
-
- var self = this;
- canvas.mozPrintCallback = function(obj) {
- var ctx = obj.context;
-
- ctx.save();
- ctx.fillStyle = 'rgb(255, 255, 255)';
- ctx.fillRect(0, 0, canvas.width, canvas.height);
- ctx.restore();
- ctx.scale(PRINT_OUTPUT_SCALE, PRINT_OUTPUT_SCALE);
-
- var renderContext = {
- canvasContext: ctx,
- viewport: viewport
- };
-
- pdfPage.render(renderContext).then(function() {
- // Tell the printEngine that rendering this canvas/page has finished.
- obj.done();
- self.pdfPage.destroy();
- }, function(error) {
- console.error(error);
- // Tell the printEngine that rendering this canvas/page has failed.
- // This will make the print proces stop.
- if ('abort' in obj)
- obj.abort();
- else
- obj.done();
- self.pdfPage.destroy();
- });
- };
- };
-
- this.updateStats = function pageViewUpdateStats() {
- if (!this.stats) {
- return;
- }
-
- if (PDFJS.pdfBug && Stats.enabled) {
- var stats = this.stats;
- Stats.add(this.id, stats);
- }
- };
-};
-
-var ThumbnailView = function thumbnailView(container, id, defaultViewport) {
- var anchor = document.createElement('a');
- anchor.href = PDFView.getAnchorUrl('#page=' + id);
- anchor.title = mozL10n.get('thumb_page_title', {page: id}, 'Page {{page}}');
- anchor.onclick = function stopNavigation() {
- PDFView.page = id;
- return false;
- };
-
-
- this.pdfPage = undefined;
- this.viewport = defaultViewport;
- this.pdfPageRotate = defaultViewport.rotate;
-
- this.rotation = 0;
- this.pageWidth = this.viewport.width;
- this.pageHeight = this.viewport.height;
- this.pageRatio = this.pageWidth / this.pageHeight;
- this.id = id;
-
- this.canvasWidth = 98;
- this.canvasHeight = this.canvasWidth / this.pageWidth * this.pageHeight;
- this.scale = (this.canvasWidth / this.pageWidth);
-
- var div = this.el = document.createElement('div');
- div.id = 'thumbnailContainer' + id;
- div.className = 'thumbnail';
-
- if (id === 1) {
- // Highlight the thumbnail of the first page when no page number is
- // specified (or exists in cache) when the document is loaded.
- div.classList.add('selected');
- }
-
- var ring = document.createElement('div');
- ring.className = 'thumbnailSelectionRing';
- ring.style.width = this.canvasWidth + 'px';
- ring.style.height = this.canvasHeight + 'px';
-
- div.appendChild(ring);
- anchor.appendChild(div);
- container.appendChild(anchor);
-
- this.hasImage = false;
- this.renderingState = RenderingStates.INITIAL;
-
- this.setPdfPage = function thumbnailViewSetPdfPage(pdfPage) {
- this.pdfPage = pdfPage;
- this.pdfPageRotate = pdfPage.rotate;
- this.viewport = pdfPage.getViewport(1);
- this.update();
- };
-
- this.update = function thumbnailViewUpdate(rot) {
- if (!this.pdfPage) {
- return;
- }
-
- if (rot !== undefined) {
- this.rotation = rot;
- }
-
- var totalRotation = (this.rotation + this.pdfPage.rotate) % 360;
- this.viewport = this.viewport.clone({
- scale: 1,
- rotation: totalRotation
- });
- this.pageWidth = this.viewport.width;
- this.pageHeight = this.viewport.height;
- this.pageRatio = this.pageWidth / this.pageHeight;
-
- this.canvasHeight = this.canvasWidth / this.pageWidth * this.pageHeight;
- this.scale = (this.canvasWidth / this.pageWidth);
-
- div.removeAttribute('data-loaded');
- ring.textContent = '';
- ring.style.width = this.canvasWidth + 'px';
- ring.style.height = this.canvasHeight + 'px';
-
- this.hasImage = false;
- this.renderingState = RenderingStates.INITIAL;
- this.resume = null;
- };
-
- this.getPageDrawContext = function thumbnailViewGetPageDrawContext() {
- var canvas = document.createElement('canvas');
- canvas.id = 'thumbnail' + id;
-
- canvas.width = this.canvasWidth;
- canvas.height = this.canvasHeight;
- canvas.className = 'thumbnailImage';
- canvas.setAttribute('aria-label', mozL10n.get('thumb_page_canvas',
- {page: id}, 'Thumbnail of Page {{page}}'));
-
- div.setAttribute('data-loaded', true);
-
- ring.appendChild(canvas);
-
- var ctx = canvas.getContext('2d');
- ctx.save();
- ctx.fillStyle = 'rgb(255, 255, 255)';
- ctx.fillRect(0, 0, this.canvasWidth, this.canvasHeight);
- ctx.restore();
- return ctx;
- };
-
- this.drawingRequired = function thumbnailViewDrawingRequired() {
- return !this.hasImage;
- };
-
- this.draw = function thumbnailViewDraw(callback) {
- if (!this.pdfPage) {
- var promise = PDFView.getPage(this.id);
- promise.then(function(pdfPage) {
- this.setPdfPage(pdfPage);
- this.draw(callback);
- }.bind(this));
- return;
- }
-
- if (this.renderingState !== RenderingStates.INITIAL) {
- console.error('Must be in new state before drawing');
- }
-
- this.renderingState = RenderingStates.RUNNING;
- if (this.hasImage) {
- callback();
- return;
- }
-
- var self = this;
- var ctx = this.getPageDrawContext();
- var drawViewport = this.viewport.clone({ scale: this.scale });
- var renderContext = {
- canvasContext: ctx,
- viewport: drawViewport,
- continueCallback: function(cont) {
- if (PDFView.highestPriorityPage !== 'thumbnail' + self.id) {
- self.renderingState = RenderingStates.PAUSED;
- self.resume = function() {
- self.renderingState = RenderingStates.RUNNING;
- cont();
- };
- return;
- }
- cont();
- }
- };
- this.pdfPage.render(renderContext).then(
- function pdfPageRenderCallback() {
- self.renderingState = RenderingStates.FINISHED;
- callback();
- },
- function pdfPageRenderError(error) {
- self.renderingState = RenderingStates.FINISHED;
- callback();
- }
- );
- this.hasImage = true;
- };
-
- this.setImage = function thumbnailViewSetImage(img) {
- if (this.hasImage || !img)
- return;
- this.renderingState = RenderingStates.FINISHED;
- var ctx = this.getPageDrawContext();
- ctx.drawImage(img, 0, 0, img.width, img.height,
- 0, 0, ctx.canvas.width, ctx.canvas.height);
-
- this.hasImage = true;
- };
-};
-
-var DocumentOutlineView = function documentOutlineView(outline) {
- var outlineView = document.getElementById('outlineView');
- while (outlineView.firstChild)
- outlineView.removeChild(outlineView.firstChild);
-
- function bindItemLink(domObj, item) {
- domObj.href = PDFView.getDestinationHash(item.dest);
- domObj.onclick = function documentOutlineViewOnclick(e) {
- PDFView.navigateTo(item.dest);
- return false;
- };
- }
-
- if (!outline) {
- var noOutline = document.createElement('div');
- noOutline.classList.add('noOutline');
- noOutline.textContent = mozL10n.get('no_outline', null,
- 'No Outline Available');
- outlineView.appendChild(noOutline);
- return;
- }
-
- var queue = [{parent: outlineView, items: outline}];
- while (queue.length > 0) {
- var levelData = queue.shift();
- var i, n = levelData.items.length;
- for (i = 0; i < n; i++) {
- var item = levelData.items[i];
- var div = document.createElement('div');
- div.className = 'outlineItem';
- var a = document.createElement('a');
- bindItemLink(a, item);
- a.textContent = item.title;
- div.appendChild(a);
-
- if (item.items.length > 0) {
- var itemsDiv = document.createElement('div');
- itemsDiv.className = 'outlineItems';
- div.appendChild(itemsDiv);
- queue.push({parent: itemsDiv, items: item.items});
- }
-
- levelData.parent.appendChild(div);
- }
- }
-};
-
-// optimised CSS custom property getter/setter
-var CustomStyle = (function CustomStyleClosure() {
-
- // As noted on: http://www.zachstronaut.com/posts/2009/02/17/
- // animate-css-transforms-firefox-webkit.html
- // in some versions of IE9 it is critical that ms appear in this list
- // before Moz
- var prefixes = ['ms', 'Moz', 'Webkit', 'O'];
- var _cache = { };
-
- function CustomStyle() {
- }
-
- CustomStyle.getProp = function get(propName, element) {
- // check cache only when no element is given
- if (arguments.length == 1 && typeof _cache[propName] == 'string') {
- return _cache[propName];
- }
-
- element = element || document.documentElement;
- var style = element.style, prefixed, uPropName;
-
- // test standard property first
- if (typeof style[propName] == 'string') {
- return (_cache[propName] = propName);
- }
-
- // capitalize
- uPropName = propName.charAt(0).toUpperCase() + propName.slice(1);
-
- // test vendor specific properties
- for (var i = 0, l = prefixes.length; i < l; i++) {
- prefixed = prefixes[i] + uPropName;
- if (typeof style[prefixed] == 'string') {
- return (_cache[propName] = prefixed);
- }
- }
-
- //if all fails then set to undefined
- return (_cache[propName] = 'undefined');
- };
-
- CustomStyle.setProp = function set(propName, element, str) {
- var prop = this.getProp(propName);
- if (prop != 'undefined')
- element.style[prop] = str;
- };
-
- return CustomStyle;
-})();
-
-var TextLayerBuilder = function textLayerBuilder(textLayerDiv, pageIdx) {
- var textLayerFrag = document.createDocumentFragment();
-
- this.textLayerDiv = textLayerDiv;
- this.layoutDone = false;
- this.divContentDone = false;
- this.pageIdx = pageIdx;
- this.matches = [];
-
- this.beginLayout = function textLayerBuilderBeginLayout() {
- this.textDivs = [];
- this.textLayerQueue = [];
- this.renderingDone = false;
- };
-
- this.endLayout = function textLayerBuilderEndLayout() {
- this.layoutDone = true;
- this.insertDivContent();
- };
-
- this.renderLayer = function textLayerBuilderRenderLayer() {
- var self = this;
- var textDivs = this.textDivs;
- var bidiTexts = this.textContent.bidiTexts;
- var textLayerDiv = this.textLayerDiv;
- var canvas = document.createElement('canvas');
- var ctx = canvas.getContext('2d');
-
- // No point in rendering so many divs as it'd make the browser unusable
- // even after the divs are rendered
- var MAX_TEXT_DIVS_TO_RENDER = 100000;
- if (textDivs.length > MAX_TEXT_DIVS_TO_RENDER)
- return;
-
- for (var i = 0, ii = textDivs.length; i < ii; i++) {
- var textDiv = textDivs[i];
- if ('isWhitespace' in textDiv.dataset) {
- continue;
- }
- textLayerFrag.appendChild(textDiv);
-
- ctx.font = textDiv.style.fontSize + ' ' + textDiv.style.fontFamily;
- var width = ctx.measureText(textDiv.textContent).width;
-
- if (width > 0) {
- var textScale = textDiv.dataset.canvasWidth / width;
-
- var transform = 'scale(' + textScale + ', 1)';
- if (bidiTexts[i].dir === 'ttb') {
- transform = 'rotate(90deg) ' + transform;
- }
- CustomStyle.setProp('transform' , textDiv, transform);
- CustomStyle.setProp('transformOrigin' , textDiv, '0% 0%');
-
- textLayerDiv.appendChild(textDiv);
- }
- }
-
- this.renderingDone = true;
- this.updateMatches();
-
- textLayerDiv.appendChild(textLayerFrag);
- };
-
- this.setupRenderLayoutTimer = function textLayerSetupRenderLayoutTimer() {
- // Schedule renderLayout() if user has been scrolling, otherwise
- // run it right away
- var RENDER_DELAY = 200; // in ms
- var self = this;
- if (Date.now() - PDFView.lastScroll > RENDER_DELAY) {
- // Render right away
- this.renderLayer();
- } else {
- // Schedule
- if (this.renderTimer)
- clearTimeout(this.renderTimer);
- this.renderTimer = setTimeout(function() {
- self.setupRenderLayoutTimer();
- }, RENDER_DELAY);
- }
- };
-
- this.appendText = function textLayerBuilderAppendText(geom) {
- var textDiv = document.createElement('div');
-
- // vScale and hScale already contain the scaling to pixel units
- var fontHeight = geom.fontSize * Math.abs(geom.vScale);
- textDiv.dataset.canvasWidth = geom.canvasWidth * geom.hScale;
- textDiv.dataset.fontName = geom.fontName;
-
- textDiv.style.fontSize = fontHeight + 'px';
- textDiv.style.fontFamily = geom.fontFamily;
- textDiv.style.left = geom.x + 'px';
- textDiv.style.top = (geom.y - fontHeight) + 'px';
-
- // The content of the div is set in the `setTextContent` function.
-
- this.textDivs.push(textDiv);
- };
-
- this.insertDivContent = function textLayerUpdateTextContent() {
- // Only set the content of the divs once layout has finished, the content
- // for the divs is available and content is not yet set on the divs.
- if (!this.layoutDone || this.divContentDone || !this.textContent)
- return;
-
- this.divContentDone = true;
-
- var textDivs = this.textDivs;
- var bidiTexts = this.textContent.bidiTexts;
-
- for (var i = 0; i < bidiTexts.length; i++) {
- var bidiText = bidiTexts[i];
- var textDiv = textDivs[i];
- if (!/\S/.test(bidiText.str)) {
- textDiv.dataset.isWhitespace = true;
- continue;
- }
-
- textDiv.textContent = bidiText.str;
- // bidiText.dir may be 'ttb' for vertical texts.
- textDiv.dir = bidiText.dir === 'rtl' ? 'rtl' : 'ltr';
- }
-
- this.setupRenderLayoutTimer();
- };
-
- this.setTextContent = function textLayerBuilderSetTextContent(textContent) {
- this.textContent = textContent;
- this.insertDivContent();
- };
-
- this.convertMatches = function textLayerBuilderConvertMatches(matches) {
- var i = 0;
- var iIndex = 0;
- var bidiTexts = this.textContent.bidiTexts;
- var end = bidiTexts.length - 1;
- var queryLen = PDFFindController.state.query.length;
-
- var lastDivIdx = -1;
- var pos;
-
- var ret = [];
-
- // Loop over all the matches.
- for (var m = 0; m < matches.length; m++) {
- var matchIdx = matches[m];
- // # Calculate the begin position.
-
- // Loop over the divIdxs.
- while (i !== end && matchIdx >= (iIndex + bidiTexts[i].str.length)) {
- iIndex += bidiTexts[i].str.length;
- i++;
- }
-
- // TODO: Do proper handling here if something goes wrong.
- if (i == bidiTexts.length) {
- console.error('Could not find matching mapping');
- }
-
- var match = {
- begin: {
- divIdx: i,
- offset: matchIdx - iIndex
- }
- };
-
- // # Calculate the end position.
- matchIdx += queryLen;
-
- // Somewhat same array as above, but use a > instead of >= to get the end
- // position right.
- while (i !== end && matchIdx > (iIndex + bidiTexts[i].str.length)) {
- iIndex += bidiTexts[i].str.length;
- i++;
- }
-
- match.end = {
- divIdx: i,
- offset: matchIdx - iIndex
- };
- ret.push(match);
- }
-
- return ret;
- };
-
- this.renderMatches = function textLayerBuilder_renderMatches(matches) {
- // Early exit if there is nothing to render.
- if (matches.length === 0) {
- return;
- }
-
- var bidiTexts = this.textContent.bidiTexts;
- var textDivs = this.textDivs;
- var prevEnd = null;
- var isSelectedPage = this.pageIdx === PDFFindController.selected.pageIdx;
- var selectedMatchIdx = PDFFindController.selected.matchIdx;
- var highlightAll = PDFFindController.state.highlightAll;
-
- var infty = {
- divIdx: -1,
- offset: undefined
- };
-
- function beginText(begin, className) {
- var divIdx = begin.divIdx;
- var div = textDivs[divIdx];
- div.textContent = '';
-
- var content = bidiTexts[divIdx].str.substring(0, begin.offset);
- var node = document.createTextNode(content);
- if (className) {
- var isSelected = isSelectedPage &&
- divIdx === selectedMatchIdx;
- var span = document.createElement('span');
- span.className = className + (isSelected ? ' selected' : '');
- span.appendChild(node);
- div.appendChild(span);
- return;
- }
- div.appendChild(node);
- }
-
- function appendText(from, to, className) {
- var divIdx = from.divIdx;
- var div = textDivs[divIdx];
-
- var content = bidiTexts[divIdx].str.substring(from.offset, to.offset);
- var node = document.createTextNode(content);
- if (className) {
- var span = document.createElement('span');
- span.className = className;
- span.appendChild(node);
- div.appendChild(span);
- return;
- }
- div.appendChild(node);
- }
-
- function highlightDiv(divIdx, className) {
- textDivs[divIdx].className = className;
- }
-
- var i0 = selectedMatchIdx, i1 = i0 + 1, i;
-
- if (highlightAll) {
- i0 = 0;
- i1 = matches.length;
- } else if (!isSelectedPage) {
- // Not highlighting all and this isn't the selected page, so do nothing.
- return;
- }
-
- for (i = i0; i < i1; i++) {
- var match = matches[i];
- var begin = match.begin;
- var end = match.end;
-
- var isSelected = isSelectedPage && i === selectedMatchIdx;
- var highlightSuffix = (isSelected ? ' selected' : '');
- if (isSelected)
- scrollIntoView(textDivs[begin.divIdx], {top: -50});
-
- // Match inside new div.
- if (!prevEnd || begin.divIdx !== prevEnd.divIdx) {
- // If there was a previous div, then add the text at the end
- if (prevEnd !== null) {
- appendText(prevEnd, infty);
- }
- // clears the divs and set the content until the begin point.
- beginText(begin);
- } else {
- appendText(prevEnd, begin);
- }
-
- if (begin.divIdx === end.divIdx) {
- appendText(begin, end, 'highlight' + highlightSuffix);
- } else {
- appendText(begin, infty, 'highlight begin' + highlightSuffix);
- for (var n = begin.divIdx + 1; n < end.divIdx; n++) {
- highlightDiv(n, 'highlight middle' + highlightSuffix);
- }
- beginText(end, 'highlight end' + highlightSuffix);
- }
- prevEnd = end;
- }
-
- if (prevEnd) {
- appendText(prevEnd, infty);
- }
- };
-
- this.updateMatches = function textLayerUpdateMatches() {
- // Only show matches, once all rendering is done.
- if (!this.renderingDone)
- return;
-
- // Clear out all matches.
- var matches = this.matches;
- var textDivs = this.textDivs;
- var bidiTexts = this.textContent.bidiTexts;
- var clearedUntilDivIdx = -1;
-
- // Clear out all current matches.
- for (var i = 0; i < matches.length; i++) {
- var match = matches[i];
- var begin = Math.max(clearedUntilDivIdx, match.begin.divIdx);
- for (var n = begin; n <= match.end.divIdx; n++) {
- var div = textDivs[n];
- div.textContent = bidiTexts[n].str;
- div.className = '';
- }
- clearedUntilDivIdx = match.end.divIdx + 1;
- }
-
- if (!PDFFindController.active)
- return;
-
- // Convert the matches on the page controller into the match format used
- // for the textLayer.
- this.matches = matches =
- this.convertMatches(PDFFindController.pageMatches[this.pageIdx] || []);
-
- this.renderMatches(this.matches);
- };
-};
-
-document.addEventListener('DOMContentLoaded', function webViewerLoad(evt) {
- PDFView.initialize();
- var params = PDFView.parseQueryString(document.location.search.substring(1));
-
-//#if !(FIREFOX || MOZCENTRAL)
- var file = params.file || DEFAULT_URL;
-//#else
-//var file = window.location.toString()
-//#endif
-
-//#if !(FIREFOX || MOZCENTRAL)
- if (!window.File || !window.FileReader || !window.FileList || !window.Blob) {
- document.getElementById('openFile').setAttribute('hidden', 'true');
- } else {
- document.getElementById('fileInput').value = null;
- }
-//#else
-//document.getElementById('openFile').setAttribute('hidden', 'true');
-//#endif
-
- // Special debugging flags in the hash section of the URL.
- var hash = document.location.hash.substring(1);
- var hashParams = PDFView.parseQueryString(hash);
-
- if ('disableWorker' in hashParams)
- PDFJS.disableWorker = (hashParams['disableWorker'] === 'true');
-
-//#if !(FIREFOX || MOZCENTRAL)
- var locale = navigator.language;
- if ('locale' in hashParams)
- locale = hashParams['locale'];
- mozL10n.setLanguage(locale);
-//#endif
-
- if ('textLayer' in hashParams) {
- switch (hashParams['textLayer']) {
- case 'off':
- PDFJS.disableTextLayer = true;
- break;
- case 'visible':
- case 'shadow':
- case 'hover':
- var viewer = document.getElementById('viewer');
- viewer.classList.add('textLayer-' + hashParams['textLayer']);
- break;
- }
- }
-
-//#if !(FIREFOX || MOZCENTRAL)
- if ('pdfBug' in hashParams) {
-//#else
-//if ('pdfBug' in hashParams && FirefoxCom.requestSync('pdfBugEnabled')) {
-//#endif
- PDFJS.pdfBug = true;
- var pdfBug = hashParams['pdfBug'];
- var enabled = pdfBug.split(',');
- PDFBug.enable(enabled);
- PDFBug.init();
- }
-
- if (!PDFView.supportsPrinting) {
- document.getElementById('print').classList.add('hidden');
- }
-
- if (!PDFView.supportsFullscreen) {
- document.getElementById('fullscreen').classList.add('hidden');
- }
-
- if (PDFView.supportsIntegratedFind) {
- document.querySelector('#viewFind').classList.add('hidden');
- }
-
- // Listen for warnings to trigger the fallback UI. Errors should be caught
- // and call PDFView.error() so we don't need to listen for those.
- PDFJS.LogManager.addLogger({
- warn: function() {
- PDFView.fallback();
- }
- });
-
- var mainContainer = document.getElementById('mainContainer');
- var outerContainer = document.getElementById('outerContainer');
- mainContainer.addEventListener('transitionend', function(e) {
- if (e.target == mainContainer) {
- var event = document.createEvent('UIEvents');
- event.initUIEvent('resize', false, false, window, 0);
- window.dispatchEvent(event);
- outerContainer.classList.remove('sidebarMoving');
- }
- }, true);
-
- document.getElementById('sidebarToggle').addEventListener('click',
- function() {
- this.classList.toggle('toggled');
- outerContainer.classList.add('sidebarMoving');
- outerContainer.classList.toggle('sidebarOpen');
- PDFView.sidebarOpen = outerContainer.classList.contains('sidebarOpen');
- PDFView.renderHighestPriority();
- });
-
- document.getElementById('viewThumbnail').addEventListener('click',
- function() {
- PDFView.switchSidebarView('thumbs');
- });
-
- document.getElementById('viewOutline').addEventListener('click',
- function() {
- PDFView.switchSidebarView('outline');
- });
-
- document.getElementById('previous').addEventListener('click',
- function() {
- PDFView.page--;
- });
-
- document.getElementById('next').addEventListener('click',
- function() {
- PDFView.page++;
- });
-
- document.querySelector('.zoomIn').addEventListener('click',
- function() {
- PDFView.zoomIn();
- });
-
- document.querySelector('.zoomOut').addEventListener('click',
- function() {
- PDFView.zoomOut();
- });
-
- document.getElementById('fullscreen').addEventListener('click',
- function() {
- PDFView.fullscreen();
- });
-
- document.getElementById('openFile').addEventListener('click',
- function() {
- document.getElementById('fileInput').click();
- });
-
- document.getElementById('print').addEventListener('click',
- function() {
- window.print();
- });
-
- document.getElementById('download').addEventListener('click',
- function() {
- PDFView.download();
- });
-
- document.getElementById('pageNumber').addEventListener('click',
- function() {
- this.select();
- });
-
- document.getElementById('pageNumber').addEventListener('change',
- function() {
- // Handle the user inputting a floating point number.
- PDFView.page = (this.value | 0);
-
- if (this.value !== (this.value | 0).toString()) {
- this.value = PDFView.page;
- }
- });
-
- document.getElementById('scaleSelect').addEventListener('change',
- function() {
- PDFView.parseScale(this.value);
- });
-
- document.getElementById('first_page').addEventListener('click',
- function() {
- PDFView.page = 1;
- });
-
- document.getElementById('last_page').addEventListener('click',
- function() {
- PDFView.page = PDFView.pdfDocument.numPages;
- });
-
- document.getElementById('page_rotate_ccw').addEventListener('click',
- function() {
- PDFView.rotatePages(-90);
- });
-
- document.getElementById('page_rotate_cw').addEventListener('click',
- function() {
- PDFView.rotatePages(90);
- });
-
-//#if (FIREFOX || MOZCENTRAL)
-//if (FirefoxCom.requestSync('getLoadingType') == 'passive') {
-// PDFView.setTitleUsingUrl(file);
-// PDFView.initPassiveLoading();
-// return;
-//}
-//#endif
-
-//#if !B2G
- PDFView.open(file, 0);
-//#endif
-}, true);
-
-function updateViewarea() {
-
- if (!PDFView.initialized)
- return;
- var visible = PDFView.getVisiblePages();
- var visiblePages = visible.views;
- if (visiblePages.length === 0) {
- return;
- }
-
- PDFView.renderHighestPriority();
-
- var currentId = PDFView.page;
- var firstPage = visible.first;
-
- for (var i = 0, ii = visiblePages.length, stillFullyVisible = false;
- i < ii; ++i) {
- var page = visiblePages[i];
-
- if (page.percent < 100)
- break;
-
- if (page.id === PDFView.page) {
- stillFullyVisible = true;
- break;
- }
- }
-
- if (!stillFullyVisible) {
- currentId = visiblePages[0].id;
- }
-
- if (!PDFView.isFullscreen) {
- updateViewarea.inProgress = true; // used in "set page"
- PDFView.page = currentId;
- updateViewarea.inProgress = false;
- }
-
- var currentScale = PDFView.currentScale;
- var currentScaleValue = PDFView.currentScaleValue;
- var normalizedScaleValue = currentScaleValue == currentScale ?
- currentScale * 100 : currentScaleValue;
-
- var pageNumber = firstPage.id;
- var pdfOpenParams = '#page=' + pageNumber;
- pdfOpenParams += '&zoom=' + normalizedScaleValue;
- var currentPage = PDFView.pages[pageNumber - 1];
- var topLeft = currentPage.getPagePoint(PDFView.container.scrollLeft,
- (PDFView.container.scrollTop - firstPage.y));
- pdfOpenParams += ',' + Math.round(topLeft[0]) + ',' + Math.round(topLeft[1]);
-
- var store = PDFView.store;
- store.initializedPromise.then(function() {
- store.set('exists', true);
- store.set('page', pageNumber);
- store.set('zoom', normalizedScaleValue);
- store.set('scrollLeft', Math.round(topLeft[0]));
- store.set('scrollTop', Math.round(topLeft[1]));
- });
- var href = PDFView.getAnchorUrl(pdfOpenParams);
- document.getElementById('viewBookmark').href = href;
-}
-
-window.addEventListener('resize', function webViewerResize(evt) {
- if (PDFView.initialized &&
- (document.getElementById('pageWidthOption').selected ||
- document.getElementById('pageFitOption').selected ||
- document.getElementById('pageAutoOption').selected))
- PDFView.parseScale(document.getElementById('scaleSelect').value);
- updateViewarea();
-});
-
-window.addEventListener('hashchange', function webViewerHashchange(evt) {
- PDFView.setHash(document.location.hash.substring(1));
-});
-
-window.addEventListener('change', function webViewerChange(evt) {
- var files = evt.target.files;
- if (!files || files.length === 0)
- return;
-
- // Read the local file into a Uint8Array.
- var fileReader = new FileReader();
- fileReader.onload = function webViewerChangeFileReaderOnload(evt) {
- var buffer = evt.target.result;
- var uint8Array = new Uint8Array(buffer);
- PDFView.open(uint8Array, 0);
- };
-
- var file = files[0];
- fileReader.readAsArrayBuffer(file);
- PDFView.setTitleUsingUrl(file.name);
-
- // URL does not reflect proper document location - hiding some icons.
- document.getElementById('viewBookmark').setAttribute('hidden', 'true');
- document.getElementById('download').setAttribute('hidden', 'true');
-}, true);
-
-function selectScaleOption(value) {
- var options = document.getElementById('scaleSelect').options;
- var predefinedValueFound = false;
- for (var i = 0; i < options.length; i++) {
- var option = options[i];
- if (option.value != value) {
- option.selected = false;
- continue;
- }
- option.selected = true;
- predefinedValueFound = true;
- }
- return predefinedValueFound;
-}
-
-window.addEventListener('localized', function localized(evt) {
- document.getElementsByTagName('html')[0].dir = mozL10n.getDirection();
-
- // Adjust the width of the zoom box to fit the content.
- PDFView.animationStartedPromise.then(
- function() {
- var container = document.getElementById('scaleSelectContainer');
- var select = document.getElementById('scaleSelect');
- select.setAttribute('style', 'min-width: inherit;');
- var width = select.clientWidth + 8;
- select.setAttribute('style', 'min-width: ' + (width + 20) + 'px;');
- container.setAttribute('style', 'min-width: ' + width + 'px; ' +
- 'max-width: ' + width + 'px;');
- });
-}, true);
-
-window.addEventListener('scalechange', function scalechange(evt) {
- var customScaleOption = document.getElementById('customScaleOption');
- customScaleOption.selected = false;
-
- if (!evt.resetAutoSettings &&
- (document.getElementById('pageWidthOption').selected ||
- document.getElementById('pageFitOption').selected ||
- document.getElementById('pageAutoOption').selected)) {
- updateViewarea();
- return;
- }
-
- var predefinedValueFound = selectScaleOption('' + evt.scale);
- if (!predefinedValueFound) {
- customScaleOption.textContent = Math.round(evt.scale * 10000) / 100 + '%';
- customScaleOption.selected = true;
- }
-
- document.getElementById('zoom_out').disabled = (evt.scale === MIN_SCALE);
- document.getElementById('zoom_in').disabled = (evt.scale === MAX_SCALE);
-
- updateViewarea();
-}, true);
-
-window.addEventListener('pagechange', function pagechange(evt) {
- var page = evt.pageNumber;
- if (PDFView.previousPageNumber !== page) {
- document.getElementById('pageNumber').value = page;
- var selected = document.querySelector('.thumbnail.selected');
- if (selected)
- selected.classList.remove('selected');
- var thumbnail = document.getElementById('thumbnailContainer' + page);
- thumbnail.classList.add('selected');
- var visibleThumbs = PDFView.getVisibleThumbs();
- var numVisibleThumbs = visibleThumbs.views.length;
- // If the thumbnail isn't currently visible scroll it into view.
- if (numVisibleThumbs > 0) {
- var first = visibleThumbs.first.id;
- // Account for only one thumbnail being visible.
- var last = numVisibleThumbs > 1 ?
- visibleThumbs.last.id : first;
- if (page <= first || page >= last)
- scrollIntoView(thumbnail);
- }
-
- }
- document.getElementById('previous').disabled = (page <= 1);
- document.getElementById('next').disabled = (page >= PDFView.pages.length);
-}, true);
-
-// Firefox specific event, so that we can prevent browser from zooming
-window.addEventListener('DOMMouseScroll', function(evt) {
- if (evt.ctrlKey) {
- evt.preventDefault();
-
- var ticks = evt.detail;
- var direction = (ticks > 0) ? 'zoomOut' : 'zoomIn';
- for (var i = 0, length = Math.abs(ticks); i < length; i++)
- PDFView[direction]();
- } else if (PDFView.isFullscreen) {
- var FIREFOX_DELTA_FACTOR = -40;
- PDFView.mouseScroll(evt.detail * FIREFOX_DELTA_FACTOR);
- }
-}, false);
-
-window.addEventListener('mousemove', function mousemove(evt) {
- if (PDFView.isFullscreen) {
- PDFView.showPresentationControls();
- }
-}, false);
-
-window.addEventListener('mousedown', function mousedown(evt) {
- if (PDFView.isFullscreen && evt.button === 0) {
- // Enable clicking of links in fullscreen mode.
- // Note: Only links that point to the currently loaded PDF document works.
- var targetHref = evt.target.href;
- var internalLink = targetHref && (targetHref.replace(/#.*$/, '') ===
- window.location.href.replace(/#.*$/, ''));
- if (!internalLink) {
- // Unless an internal link was clicked, advance a page in fullscreen mode.
- evt.preventDefault();
- PDFView.page++;
- }
- }
-}, false);
-
-window.addEventListener('click', function click(evt) {
- if (PDFView.isFullscreen && evt.button === 0) {
- // Necessary since preventDefault() in 'mousedown' won't stop
- // the event propagation in all circumstances.
- evt.preventDefault();
- }
-}, false);
-
-window.addEventListener('keydown', function keydown(evt) {
- var handled = false;
- var cmd = (evt.ctrlKey ? 1 : 0) |
- (evt.altKey ? 2 : 0) |
- (evt.shiftKey ? 4 : 0) |
- (evt.metaKey ? 8 : 0);
-
- // First, handle the key bindings that are independent whether an input
- // control is selected or not.
- if (cmd === 1 || cmd === 8 || cmd === 5 || cmd === 12) {
- // either CTRL or META key with optional SHIFT.
- switch (evt.keyCode) {
- case 70:
- if (!PDFView.supportsIntegratedFind) {
- PDFFindBar.toggle();
- handled = true;
- }
- break;
- case 61: // FF/Mac '='
- case 107: // FF '+' and '='
- case 187: // Chrome '+'
- case 171: // FF with German keyboard
- PDFView.zoomIn();
- handled = true;
- break;
- case 173: // FF/Mac '-'
- case 109: // FF '-'
- case 189: // Chrome '-'
- PDFView.zoomOut();
- handled = true;
- break;
- case 48: // '0'
- case 96: // '0' on Numpad of Swedish keyboard
- PDFView.parseScale(DEFAULT_SCALE, true);
- handled = false; // keeping it unhandled (to restore page zoom to 100%)
- break;
- }
- }
-
- // CTRL or META with or without SHIFT.
- if (cmd == 1 || cmd == 8 || cmd == 5 || cmd == 12) {
- switch (evt.keyCode) {
- case 71: // g
- if (!PDFView.supportsIntegratedFind) {
- PDFFindBar.dispatchEvent('again', cmd == 5 || cmd == 12);
- handled = true;
- }
- break;
- }
- }
-
- if (handled) {
- evt.preventDefault();
- return;
- }
-
- // Some shortcuts should not get handled if a control/input element
- // is selected.
- var curElement = document.activeElement;
- if (curElement && (curElement.tagName == 'INPUT' ||
- curElement.tagName == 'SELECT')) {
- return;
- }
- var controlsElement = document.getElementById('toolbar');
- while (curElement) {
- if (curElement === controlsElement && !PDFView.isFullscreen)
- return; // ignoring if the 'toolbar' element is focused
- curElement = curElement.parentNode;
- }
-
- if (cmd === 0) { // no control key pressed at all.
- switch (evt.keyCode) {
- case 38: // up arrow
- case 33: // pg up
- case 8: // backspace
- if (!PDFView.isFullscreen && PDFView.currentScaleValue !== 'page-fit') {
- break;
- }
- /* in fullscreen mode */
- /* falls through */
- case 37: // left arrow
- // horizontal scrolling using arrow keys
- if (PDFView.isHorizontalScrollbarEnabled) {
- break;
- }
- /* falls through */
- case 75: // 'k'
- case 80: // 'p'
- PDFView.page--;
- handled = true;
- break;
- case 27: // esc key
- if (!PDFView.supportsIntegratedFind && PDFFindBar.opened) {
- PDFFindBar.close();
- handled = true;
- }
- break;
- case 40: // down arrow
- case 34: // pg down
- case 32: // spacebar
- if (!PDFView.isFullscreen && PDFView.currentScaleValue !== 'page-fit') {
- break;
- }
- /* falls through */
- case 39: // right arrow
- // horizontal scrolling using arrow keys
- if (PDFView.isHorizontalScrollbarEnabled) {
- break;
- }
- /* falls through */
- case 74: // 'j'
- case 78: // 'n'
- PDFView.page++;
- handled = true;
- break;
-
- case 36: // home
- if (PDFView.isFullscreen) {
- PDFView.page = 1;
- handled = true;
- }
- break;
- case 35: // end
- if (PDFView.isFullscreen) {
- PDFView.page = PDFView.pdfDocument.numPages;
- handled = true;
- }
- break;
-
- case 82: // 'r'
- PDFView.rotatePages(90);
- break;
- }
- }
-
- if (cmd == 4) { // shift-key
- switch (evt.keyCode) {
- case 82: // 'r'
- PDFView.rotatePages(-90);
- break;
- }
- }
-
- if (handled) {
- evt.preventDefault();
- PDFView.clearMouseScrollState();
- }
-});
-
-window.addEventListener('beforeprint', function beforePrint(evt) {
- PDFView.beforePrint();
-});
-
-window.addEventListener('afterprint', function afterPrint(evt) {
- PDFView.afterPrint();
-});
-
-(function fullscreenClosure() {
- function fullscreenChange(e) {
- var isFullscreen = document.fullscreenElement || document.mozFullScreen ||
- document.webkitIsFullScreen;
-
- if (!isFullscreen) {
- PDFView.exitFullscreen();
- }
- }
-
- window.addEventListener('fullscreenchange', fullscreenChange, false);
- window.addEventListener('mozfullscreenchange', fullscreenChange, false);
- window.addEventListener('webkitfullscreenchange', fullscreenChange, false);
-})();
-
-(function animationStartedClosure() {
- // The offsetParent is not set until the pdf.js iframe or object is visible.
- // Waiting for first animation.
- var requestAnimationFrame = window.requestAnimationFrame ||
- window.mozRequestAnimationFrame ||
- window.webkitRequestAnimationFrame ||
- window.oRequestAnimationFrame ||
- window.msRequestAnimationFrame ||
- function startAtOnce(callback) { callback(); };
- PDFView.animationStartedPromise = new PDFJS.Promise();
- requestAnimationFrame(function onAnimationFrame() {
- PDFView.animationStartedPromise.resolve();
- });
-})();
-
-//#if B2G
-//window.navigator.mozSetMessageHandler('activity', function(activity) {
-// var url = activity.source.data.url;
-// PDFView.open(url);
-// var cancelButton = document.getElementById('activityClose');
-// cancelButton.addEventListener('click', function() {
-// activity.postResult('close');
-// });
-//});
-//#endif
diff --git a/mediagoblin/submit/views.py b/mediagoblin/submit/views.py
index a70c89b4..64e6791b 100644
--- a/mediagoblin/submit/views.py
+++ b/mediagoblin/submit/views.py
@@ -34,6 +34,8 @@ from mediagoblin.media_types import sniff_media, \
from mediagoblin.submit.lib import check_file_field, prepare_queue_task, \
run_process_media, new_upload_entry
+from mediagoblin.notifications import add_comment_subscription
+
@require_active_login
def submit_start(request):
@@ -92,6 +94,8 @@ def submit_start(request):
run_process_media(entry, feed_url)
add_message(request, SUCCESS, _('Woohoo! Submitted!'))
+ add_comment_subscription(request.user, entry)
+
return redirect(request, "mediagoblin.user_pages.user_home",
user=request.user.username)
except Exception as e:
diff --git a/mediagoblin/templates/mediagoblin/auth/change_fp.html b/mediagoblin/templates/mediagoblin/auth/change_fp.html
index 1f7d9aca..a3cf9cb9 100644
--- a/mediagoblin/templates/mediagoblin/auth/change_fp.html
+++ b/mediagoblin/templates/mediagoblin/auth/change_fp.html
@@ -34,11 +34,10 @@
{{ csrf_token }}
<div class="form_box">
<h1>{% trans %}Set your new password{% endtrans %}</h1>
- {{ wtforms_util.render_divs(cp_form) }}
+ {{ wtforms_util.render_divs(cp_form, True) }}
<div class="form_submit_buttons">
<input type="submit" value="{% trans %}Set password{% endtrans %}" class="button_form"/>
</div>
</div>
- </form>
{% endblock %}
diff --git a/mediagoblin/templates/mediagoblin/auth/forgot_password.html b/mediagoblin/templates/mediagoblin/auth/forgot_password.html
index 46aeddef..6cfd2c85 100644
--- a/mediagoblin/templates/mediagoblin/auth/forgot_password.html
+++ b/mediagoblin/templates/mediagoblin/auth/forgot_password.html
@@ -29,7 +29,7 @@
{{ csrf_token }}
<div class="form_box">
<h1>{% trans %}Recover password{% endtrans %}</h1>
- {{ wtforms_util.render_divs(fp_form) }}
+ {{ wtforms_util.render_divs(fp_form, True) }}
<div class="form_submit_buttons">
<input type="submit" value="{% trans %}Send instructions{% endtrans %}" class="button_form"/>
</div>
diff --git a/mediagoblin/templates/mediagoblin/auth/login.html b/mediagoblin/templates/mediagoblin/auth/login.html
index 4a39059d..3329b5d0 100644
--- a/mediagoblin/templates/mediagoblin/auth/login.html
+++ b/mediagoblin/templates/mediagoblin/auth/login.html
@@ -29,7 +29,7 @@
{%- endblock %}
{% block mediagoblin_content %}
- <form action="{{ request.urlgen('mediagoblin.auth.login') }}"
+ <form action="{{ post_url }}"
method="POST" enctype="multipart/form-data">
{{ csrf_token }}
<div class="form_box">
@@ -41,15 +41,19 @@
{% endif %}
{% if allow_registration %}
<p>
- {% trans %}Don't have an account yet?{% endtrans %} <a href="{{ request.urlgen('mediagoblin.auth.register') }}">
+ {% trans %}Don't have an account yet?{% endtrans %}
+ <a href="{{ request.urlgen('mediagoblin.auth.register') }}">
{%- trans %}Create one here!{% endtrans %}</a>
</p>
{% endif %}
- {{ wtforms_util.render_divs(login_form) }}
- <p>
- <a href="{{ request.urlgen('mediagoblin.auth.forgot_password') }}" id="forgot_password">
- {% trans %}Forgot your password?{% endtrans %}</a>
- </p>
+ {% 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 %}
<div class="form_submit_buttons">
<input type="submit" value="{% trans %}Log in{% endtrans %}" class="button_form"/>
</div>
diff --git a/mediagoblin/templates/mediagoblin/auth/register.html b/mediagoblin/templates/mediagoblin/auth/register.html
index 6dff0207..a7b8033f 100644
--- a/mediagoblin/templates/mediagoblin/auth/register.html
+++ b/mediagoblin/templates/mediagoblin/auth/register.html
@@ -30,11 +30,12 @@
{% block mediagoblin_content %}
- <form action="{{ request.urlgen('mediagoblin.auth.register') }}"
+ <form action="{{ post_url }}"
method="POST" enctype="multipart/form-data">
<div class="form_box">
<h1>{% trans %}Create an account!{% endtrans %}</h1>
- {{ wtforms_util.render_divs(register_form) }}
+ {% template_hook("register_link") %}
+ {{ wtforms_util.render_divs(register_form, True) }}
{{ csrf_token }}
<div class="form_submit_buttons">
<input type="submit" value="{% trans %}Create{% endtrans %}"
@@ -42,6 +43,4 @@
</div>
</div>
</form>
-<!-- Focus the username field by default -->
-<script>$(document).ready(function(){$("#username").focus();});</script>
{% endblock %}
diff --git a/mediagoblin/templates/mediagoblin/base.html b/mediagoblin/templates/mediagoblin/base.html
index 6c7c07d0..1fc4467c 100644
--- a/mediagoblin/templates/mediagoblin/base.html
+++ b/mediagoblin/templates/mediagoblin/base.html
@@ -34,6 +34,8 @@
src="{{ request.staticdirect('/js/extlib/jquery.js') }}"></script>
<script type="text/javascript"
src="{{ request.staticdirect('/js/header_dropdown.js') }}"></script>
+ <script type="text/javascript"
+ src="{{ request.staticdirect('/js/notifications.js') }}"></script>
{# For clarification, the difference between the extra_head.html template
# and the head template hook is that the former should be used by
@@ -57,6 +59,12 @@
<div class="header_right">
{%- if request.user %}
{% if request.user and request.user.status == 'active' %}
+
+ {% set notification_count = request.notifications.get_notification_count(request.user.id) %}
+ {% if notification_count %}
+ <a href="#notifications" class="notification-gem button_action" title="Notifications">
+ {{ notification_count }}</a>
+ {% endif %}
<div class="button_action header_dropdown_down">&#9660;</div>
<div class="button_action header_dropdown_up">&#9650;</div>
{% elif request.user and request.user.status == "needs_email_verification" %}
@@ -67,7 +75,7 @@
{% trans %}Verify your email!{% endtrans %}</a>
or <a href="{{ request.urlgen('mediagoblin.auth.logout') }}">{% trans %}log out{% endtrans %}</a>
{% endif %}
- {%- else %}
+ {%- elif auth %}
<a href="{{ request.urlgen('mediagoblin.auth.login') }}?next={{
request.base_url|urlencode }}">
{%- trans %}Log in{% endtrans -%}
@@ -109,6 +117,7 @@
</a>
</p>
{% endif %}
+ {% include 'mediagoblin/fragments/header_notifications.html' %}
</div>
{% endif %}
</header>
diff --git a/mediagoblin/templates/mediagoblin/bits/frontpage_welcome.html b/mediagoblin/templates/mediagoblin/bits/frontpage_welcome.html
index 544ee146..9ef28a4d 100644
--- a/mediagoblin/templates/mediagoblin/bits/frontpage_welcome.html
+++ b/mediagoblin/templates/mediagoblin/bits/frontpage_welcome.html
@@ -17,19 +17,25 @@
#}
{% if request.user %}
- <h1>{% trans %}Explore{% endtrans %}</h1>
-{% else %}
- <h1>{% trans %}Hi there, welcome to this MediaGoblin site!{% endtrans %}</h1>
- <img class="right_align" src="{{ request.staticdirect('/images/frontpage_image.png') }}" />
- <p>{% trans %}This site is running <a href="http://mediagoblin.org">MediaGoblin</a>, an extraordinarily great piece of media hosting software.{% endtrans %}</p>
- <p>{% trans %}To add your own media, place comments, and more, you can log in with your MediaGoblin account.{% endtrans %}</p>
- {% if allow_registration %}
- <p>{% trans %}Don't have one yet? It's easy!{% endtrans %}</p>
- {% trans register_url=request.urlgen('mediagoblin.auth.register') -%}
- <a class="button_action_highlight" href="{{ register_url }}">Create an account at this site</a>
- or
- <a class="button_action" href="http://wiki.mediagoblin.org/HackingHowto">Set up MediaGoblin on your own server</a>
- {%- endtrans %}
+ <h1>{% trans %}Explore{% endtrans %}</h1>
+ {% else %}
+ <h1>{% trans %}Hi there, welcome to this MediaGoblin site!{% endtrans %}</h1>
+ <img class="right_align" src="{{ request.staticdirect('/images/frontpage_image.png') }}" />
+ <p>{% trans %}This site is running <a href="http://mediagoblin.org">MediaGoblin</a>, an extraordinarily great piece of media hosting software.{% endtrans %}</p>
+ {% if auth %}
+ <p>{% trans %}To add your own media, place comments, and more, you can log in with your MediaGoblin account.{% endtrans %}</p>
+ {% if allow_registration %}
+ <p>{% trans %}Don't have one yet? It's easy!{% endtrans %}</p>
+ {% trans register_url=request.urlgen('mediagoblin.auth.register') -%}
+ <a class="button_action_highlight" href="{{ register_url }}">Create an account at this site</a>
+ or
+ {%- endtrans %}
+ {% endif %}
+ {% endif %}
+ {% trans %}
+ <a class="button_action" href="http://wiki.mediagoblin.org/HackingHowto">Set up MediaGoblin on your own server</a>
+ {%- endtrans %}
+
+ <div class="clear"></div>
{% endif %}
- <div class="clear"></div>
-{% endif %}
+
diff --git a/mediagoblin/templates/mediagoblin/edit/edit_account.html b/mediagoblin/templates/mediagoblin/edit/edit_account.html
index 4c4aaf95..51293acb 100644
--- a/mediagoblin/templates/mediagoblin/edit/edit_account.html
+++ b/mediagoblin/templates/mediagoblin/edit/edit_account.html
@@ -41,17 +41,16 @@
Changing {{ username }}'s account settings
{%- endtrans -%}
</h1>
+ {% if pass_auth is defined %}
<p>
<a href="{{ request.urlgen('mediagoblin.edit.pass') }}">
{% trans %}Change your password.{% endtrans %}
</a>
</p>
- <div class="form_field_input">
- <p>{{ form.wants_comment_notification }}
- {{ wtforms_util.render_label(form.wants_comment_notification) }}</p>
- </div>
- {{- wtforms_util.render_field_div(form.license_preference) }}
- <div class="form_submit_buttons">
+ {% endif %}
+ {% 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" />
{{ csrf_token }}
</div>
diff --git a/mediagoblin/templates/mediagoblin/edit/verification.txt b/mediagoblin/templates/mediagoblin/edit/verification.txt
new file mode 100644
index 00000000..d53cd5e8
--- /dev/null
+++ b/mediagoblin/templates/mediagoblin/edit/verification.txt
@@ -0,0 +1,29 @@
+{#
+# 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/>.
+-#}
+
+{% trans username=username, verification_url=verification_url|safe -%}
+Hi,
+
+We wanted to verify that you are {{ username }}. If this is the case, then
+please follow the link below to verify your new email address.
+
+{{ verification_url }}
+
+If you are not {{ username }} or didn't request an email change, you can ignore
+this email.
+{%- endtrans %}
diff --git a/mediagoblin/templates/mediagoblin/fragments/header_notifications.html b/mediagoblin/templates/mediagoblin/fragments/header_notifications.html
new file mode 100644
index 00000000..613100aa
--- /dev/null
+++ b/mediagoblin/templates/mediagoblin/fragments/header_notifications.html
@@ -0,0 +1,40 @@
+{% set notifications = request.notifications.get_notifications(request.user.id) %}
+{% if notifications %}
+ <div class="header_notifications">
+ <h3>{% trans %}New comments{% endtrans %}</h3>
+ <ul>
+ {% for notification in notifications %}
+ {% set comment = notification.subject %}
+ {% set comment_author = comment.get_author %}
+ {% set media = comment.get_entry %}
+ <li class="comment_wrapper">
+ <div class="comment_author">
+ <img src="{{ request.staticdirect('/images/icon_comment.png') }}" />
+ <a href="{{ request.urlgen('mediagoblin.user_pages.user_home',
+ user=comment_author.username) }}"
+ class="comment_authorlink">
+ {{- comment_author.username -}}
+ </a>
+ <a href="{{ request.urlgen('mediagoblin.user_pages.media_home.view_comment',
+ comment=comment.id,
+ user=media.get_uploader.username,
+ media=media.slug_or_id) }}#comment"
+ class="comment_whenlink">
+ <span title='{{- comment.created.strftime("%I:%M%p %Y-%m-%d") -}}'>
+ {%- trans formatted_time=timesince(comment.created) -%}
+ {{ formatted_time }} ago
+ {%- endtrans -%}
+ </span>
+ </a>:
+ </div>
+ <div class="comment_content">
+ {% autoescape False -%}
+ {{ comment.content_html }}
+ {%- endautoescape %}
+ </div>
+
+ </li>
+ {% endfor %}
+ </ul>
+ </div>
+{% endif %}
diff --git a/mediagoblin/templates/mediagoblin/media_displays/pdf.html b/mediagoblin/templates/mediagoblin/media_displays/pdf.html
index e946f3ab..9319e87c 100644
--- a/mediagoblin/templates/mediagoblin/media_displays/pdf.html
+++ b/mediagoblin/templates/mediagoblin/media_displays/pdf.html
@@ -46,19 +46,21 @@
{%- endblock %}
{% block mediagoblin_media %}
-{% if pdf_js %}
-<iframe width=640px height=480px
- src="{{ request.staticdirect('/extlib/pdf.js/web/viewer.html') }}?file={{ pdf_view }} ">
-</iframe>
-
-{% else %}
- <a href="{{ pdf_view }}">
- <img id="medium"
- class="media_image"
- src="{{ medium_view }}"
- alt="{% trans media_title=media.title -%} Image for {{ media_title}}{% endtrans %}"/>
- </a>
-{% endif %}
+ {% if pdf_js %}
+ <iframe width="640px" height="480px"
+ src="{{ request.staticdirect('/extlib/pdf.js/web/viewer.html') }}?file={{ pdf_view }} ">
+ </iframe>
+ {% else %}
+ <a href="{{ pdf_view }}">
+ <img id="medium"
+ class="media_image"
+ src="{{ medium_view }}"
+ alt="
+ {%- trans media_title=media.title -%}
+ Image for {{ media_title}}
+ {%- endtrans %}"/>
+ </a>
+ {% endif %}
{% endblock %}
{% block mediagoblin_sidebar %}
diff --git a/mediagoblin/templates/mediagoblin/user_pages/media.html b/mediagoblin/templates/mediagoblin/user_pages/media.html
index fb892fd7..c16e4c78 100644
--- a/mediagoblin/templates/mediagoblin/user_pages/media.html
+++ b/mediagoblin/templates/mediagoblin/user_pages/media.html
@@ -81,6 +81,7 @@
user= media.get_uploader.username,
media_id=media.id) %}
<a class="button_action" href="{{ delete_url }}">{% trans %}Delete{% endtrans %}</a>
+
{% endif %}
{% autoescape False %}
<p>{{ media.description_html }}</p>
@@ -94,6 +95,8 @@
class="button_action" id="button_addcomment" title="Add a comment">
{% trans %}Add a comment{% endtrans %}
</a>
+ {% include "mediagoblin/utils/comment-subscription.html" %}
+
{% endif %}
{% if request.user %}
<form action="{{ request.urlgen('mediagoblin.user_pages.media_post_comment',
diff --git a/mediagoblin/templates/mediagoblin/utils/comment-subscription.html b/mediagoblin/templates/mediagoblin/utils/comment-subscription.html
new file mode 100644
index 00000000..8ee8c883
--- /dev/null
+++ b/mediagoblin/templates/mediagoblin/utils/comment-subscription.html
@@ -0,0 +1,34 @@
+{#
+# 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/>.
+#}
+{%- if request.user %}
+ {% set subscription = request.notifications.get_comment_subscription(
+ request.user.id, media.id) %}
+ {% if not subscription or not subscription.notify %}
+ <a type="submit" href="{{ request.urlgen('mediagoblin.notifications.subscribe_comments',
+ user=media.get_uploader.username,
+ media=media.slug)}}"
+ class="button_action">Subscribe to comments
+ </a>
+ {% else %}
+ <a type="submit" href="{{ request.urlgen('mediagoblin.notifications.silence_comments',
+ user=media.get_uploader.username,
+ media=media.slug)}}"
+ class="button_action">Silence comments
+ </a>
+ {% endif %}
+{%- endif %}
diff --git a/mediagoblin/templates/mediagoblin/utils/wtforms.html b/mediagoblin/templates/mediagoblin/utils/wtforms.html
index be6976c2..a4c33f1a 100644
--- a/mediagoblin/templates/mediagoblin/utils/wtforms.html
+++ b/mediagoblin/templates/mediagoblin/utils/wtforms.html
@@ -33,25 +33,37 @@
{%- endmacro %}
{# Generically render a field #}
-{% macro render_field_div(field) %}
+{% macro render_field_div(field, autofocus_first=False) %}
{{- render_label_p(field) }}
<div class="form_field_input">
- {{ field }}
+ {% if autofocus_first %}
+ {{ field(autofocus=True) }}
+ {% else %}
+ {{ field }}
+ {% endif %}
{%- if field.errors -%}
{% for error in field.errors %}
<p class="form_field_error">{{ error }}</p>
{% endfor %}
{%- endif %}
{%- if field.description %}
- <p class="form_field_description">{{ field.description|safe }}</p>
+ {% if field.type == 'BooleanField' %}
+ <label for="{{ field.label.field_id }}">{{ field.description|safe }}</label>
+ {% else %}
+ <p class="form_field_description">{{ field.description|safe }}</p>
+ {% endif %}
{%- endif %}
</div>
{%- endmacro %}
{# Auto-render a form as a series of divs #}
-{% macro render_divs(form) -%}
+{% macro render_divs(form, autofocus_first=False) -%}
{% for field in form %}
- {{ render_field_div(field) }}
+ {% if autofocus_first and loop.first %}
+ {{ render_field_div(field, True) }}
+ {% else %}
+ {{ render_field_div(field) }}
+ {% endif %}
{% endfor %}
{%- endmacro %}
diff --git a/mediagoblin/tests/appconfig_context_modified.ini b/mediagoblin/tests/appconfig_context_modified.ini
index 80ca69b1..cc6721f5 100644
--- a/mediagoblin/tests/appconfig_context_modified.ini
+++ b/mediagoblin/tests/appconfig_context_modified.ini
@@ -3,8 +3,9 @@ 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"
+#Runs with an in-memory sqlite db for speed.
+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/appconfig_static_plugin.ini b/mediagoblin/tests/appconfig_static_plugin.ini
index dc251171..5ce5c5bd 100644
--- a/mediagoblin/tests/appconfig_static_plugin.ini
+++ b/mediagoblin/tests/appconfig_static_plugin.ini
@@ -3,8 +3,9 @@ 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"
+#Runs with an in-memory sqlite db for speed.
+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/__init__.py b/mediagoblin/tests/auth_configs/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/mediagoblin/tests/auth_configs/__init__.py
diff --git a/mediagoblin/tests/auth_configs/authentication_disabled_appconfig.ini b/mediagoblin/tests/auth_configs/authentication_disabled_appconfig.ini
new file mode 100644
index 00000000..a64e9e40
--- /dev/null
+++ b/mediagoblin/tests/auth_configs/authentication_disabled_appconfig.ini
@@ -0,0 +1,25 @@
+[mediagoblin]
+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"
+
+# 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]
diff --git a/mediagoblin/tests/auth_configs/openid_appconfig.ini b/mediagoblin/tests/auth_configs/openid_appconfig.ini
new file mode 100644
index 00000000..c2bd82fd
--- /dev/null
+++ b/mediagoblin/tests/auth_configs/openid_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
+
+# TODO: Switch to using an in-memory database
+sql_engine = "sqlite:///%(here)s/user_dev/mediagoblin.db"
+
+# 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.openid]]
diff --git a/mediagoblin/tests/test_auth.py b/mediagoblin/tests/test_auth.py
index 755727f9..5bd8bf2c 100644
--- a/mediagoblin/tests/test_auth.py
+++ b/mediagoblin/tests/test_auth.py
@@ -13,54 +13,15 @@
#
# 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 datetime
+import pkg_resources
+import pytest
from mediagoblin import mg_globals
-from mediagoblin.auth import lib as auth_lib
from mediagoblin.db.models import User
-from mediagoblin.tests.tools import fixture_add_user
+from mediagoblin.tests.tools import get_app, fixture_add_user
from mediagoblin.tools import template, mail
-
-
-########################
-# Test bcrypt auth funcs
-########################
-
-def test_bcrypt_check_password():
- # Check known 'lollerskates' password against check function
- assert auth_lib.bcrypt_check_password(
- 'lollerskates',
- '$2a$12$PXU03zfrVCujBhVeICTwtOaHTUs5FFwsscvSSTJkqx/2RQ0Lhy/nO')
-
- assert not auth_lib.bcrypt_check_password(
- 'notthepassword',
- '$2a$12$PXU03zfrVCujBhVeICTwtOaHTUs5FFwsscvSSTJkqx/2RQ0Lhy/nO')
-
- # Same thing, but with extra fake salt.
- assert not auth_lib.bcrypt_check_password(
- 'notthepassword',
- '$2a$12$ELVlnw3z1FMu6CEGs/L8XO8vl0BuWSlUHgh0rUrry9DUXGMUNWwl6',
- '3><7R45417')
-
-
-def test_bcrypt_gen_password_hash():
- pw = 'youwillneverguessthis'
-
- # Normal password hash generation, and check on that hash
- hashed_pw = auth_lib.bcrypt_gen_password_hash(pw)
- assert auth_lib.bcrypt_check_password(
- pw, hashed_pw)
- assert not auth_lib.bcrypt_check_password(
- 'notthepassword', hashed_pw)
-
- # Same thing, extra salt.
- hashed_pw = auth_lib.bcrypt_gen_password_hash(pw, '3><7R45417')
- assert auth_lib.bcrypt_check_password(
- pw, hashed_pw, '3><7R45417')
- assert not auth_lib.bcrypt_check_password(
- 'notthepassword', hashed_pw, '3><7R45417')
+from mediagoblin.auth import tools as auth_tools
def test_register_views(test_app):
@@ -156,20 +117,15 @@ def test_register_views(test_app):
assert path == u'/auth/verify_email/'
parsed_get_params = urlparse.parse_qs(get_params)
- ### user should have these same parameters
- assert parsed_get_params['userid'] == [
- unicode(new_user.id)]
- assert parsed_get_params['token'] == [
- new_user.verification_key]
-
## Try verifying with bs verification key, shouldn't work
template.clear_test_template_context()
response = test_app.get(
- "/auth/verify_email/?userid=%s&token=total_bs" % unicode(
- new_user.id))
+ "/auth/verify_email/?token=total_bs")
response.follow()
- context = template.TEMPLATE_TEST_CONTEXT[
- 'mediagoblin/user_pages/user.html']
+
+ # Correct redirect?
+ assert urlparse.urlsplit(response.location)[2] == '/'
+
# assert context['verification_successful'] == True
# TODO: Would be good to test messages here when we can do so...
new_user = mg_globals.database.User.find_one(
@@ -233,35 +189,17 @@ def test_register_views(test_app):
path = urlparse.urlsplit(email_context['verification_url'])[2]
get_params = urlparse.urlsplit(email_context['verification_url'])[3]
- assert path == u'/auth/forgot_password/verify/'
parsed_get_params = urlparse.parse_qs(get_params)
-
- # user should have matching parameters
- new_user = mg_globals.database.User.find_one({'username': u'happygirl'})
- assert parsed_get_params['userid'] == [unicode(new_user.id)]
- assert parsed_get_params['token'] == [new_user.fp_verification_key]
-
- ### The forgotten password token should be set to expire in ~ 10 days
- # A few ticks have expired so there are only 9 full days left...
- assert (new_user.fp_token_expire - datetime.datetime.now()).days == 9
+ assert path == u'/auth/forgot_password/verify/'
## Try using a bs password-changing verification key, shouldn't work
template.clear_test_template_context()
response = test_app.get(
- "/auth/forgot_password/verify/?userid=%s&token=total_bs" % unicode(
- new_user.id), status=404)
- assert response.status.split()[0] == u'404' # status="404 NOT FOUND"
+ "/auth/forgot_password/verify/?token=total_bs")
+ response.follow()
- ## Try using an expired token to change password, shouldn't work
- template.clear_test_template_context()
- new_user = mg_globals.database.User.find_one({'username': u'happygirl'})
- real_token_expiration = new_user.fp_token_expire
- new_user.fp_token_expire = datetime.datetime.now()
- new_user.save()
- response = test_app.get("%s?%s" % (path, get_params), status=404)
- assert response.status.split()[0] == u'404' # status="404 NOT FOUND"
- new_user.fp_token_expire = real_token_expiration
- new_user.save()
+ # Correct redirect?
+ assert urlparse.urlsplit(response.location)[2] == '/'
## Verify step 1 of password-change works -- can see form to change password
template.clear_test_template_context()
@@ -272,7 +210,6 @@ def test_register_views(test_app):
template.clear_test_template_context()
response = test_app.post(
'/auth/forgot_password/verify/', {
- 'userid': parsed_get_params['userid'],
'password': 'iamveryveryhappy',
'token': parsed_get_params['token']})
response.follow()
@@ -298,6 +235,7 @@ def test_authentication_views(test_app):
# Make a new user
test_user = fixture_add_user(active_user=False)
+
# Get login
# ---------
test_app.get('/auth/login/')
@@ -310,7 +248,6 @@ def test_authentication_views(test_app):
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.']
# Failed login - blank user
# -------------------------
@@ -328,9 +265,7 @@ def test_authentication_views(test_app):
response = test_app.post(
'/auth/login/', {
'username': u'chris'})
- context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/auth/login.html']
- form = context['login_form']
- assert form.password.errors == [u'This field is required.']
+ assert 'mediagoblin/auth/login.html' in template.TEMPLATE_TEST_CONTEXT
# Failed login - bad user
# -----------------------
@@ -394,3 +329,47 @@ def test_authentication_views(test_app):
'password': 'toast',
'next' : '/u/chris/'})
assert urlparse.urlsplit(response.location)[2] == '/u/chris/'
+
+
+@pytest.fixture()
+def authentication_disabled_app(request):
+ return get_app(
+ request,
+ mgoblin_config=pkg_resources.resource_filename(
+ 'mediagoblin.tests.auth_configs',
+ 'authentication_disabled_appconfig.ini'))
+
+
+def test_authentication_disabled_app(authentication_disabled_app):
+ # app.auth should = false
+ assert mg_globals.app.auth is False
+
+ # Try to visit register page
+ template.clear_test_template_context()
+ response = authentication_disabled_app.get('/auth/register/')
+ response.follow()
+
+ # Correct redirect?
+ assert urlparse.urlsplit(response.location)[2] == '/'
+ assert 'mediagoblin/root.html' in template.TEMPLATE_TEST_CONTEXT
+
+ # Try to vist login page
+ template.clear_test_template_context()
+ response = authentication_disabled_app.get('/auth/login/')
+ response.follow()
+
+ # Correct redirect?
+ assert urlparse.urlsplit(response.location)[2] == '/'
+ assert 'mediagoblin/root.html' in template.TEMPLATE_TEST_CONTEXT
+
+ ## Test check_login_simple should return None
+ assert auth_tools.check_login_simple('test', 'simple') is None
+
+ # Try to visit the forgot password page
+ template.clear_test_template_context()
+ response = authentication_disabled_app.get('/auth/register/')
+ response.follow()
+
+ # Correct redirect?
+ assert urlparse.urlsplit(response.location)[2] == '/'
+ assert 'mediagoblin/root.html' in template.TEMPLATE_TEST_CONTEXT
diff --git a/mediagoblin/tests/test_basic_auth.py b/mediagoblin/tests/test_basic_auth.py
new file mode 100644
index 00000000..cdd80fca
--- /dev/null
+++ b/mediagoblin/tests/test_basic_auth.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/>.
+from mediagoblin.plugins.basic_auth import tools as auth_tools
+from mediagoblin.tools.testing import _activate_testing
+
+_activate_testing()
+
+
+########################
+# Test bcrypt auth funcs
+########################
+
+
+def test_bcrypt_check_password():
+ # Check known 'lollerskates' password against check function
+ assert auth_tools.bcrypt_check_password(
+ 'lollerskates',
+ '$2a$12$PXU03zfrVCujBhVeICTwtOaHTUs5FFwsscvSSTJkqx/2RQ0Lhy/nO')
+
+ assert not auth_tools.bcrypt_check_password(
+ 'notthepassword',
+ '$2a$12$PXU03zfrVCujBhVeICTwtOaHTUs5FFwsscvSSTJkqx/2RQ0Lhy/nO')
+
+ # Same thing, but with extra fake salt.
+ assert not auth_tools.bcrypt_check_password(
+ 'notthepassword',
+ '$2a$12$ELVlnw3z1FMu6CEGs/L8XO8vl0BuWSlUHgh0rUrry9DUXGMUNWwl6',
+ '3><7R45417')
+
+
+def test_bcrypt_gen_password_hash():
+ pw = 'youwillneverguessthis'
+
+ # Normal password hash generation, and check on that hash
+ hashed_pw = auth_tools.bcrypt_gen_password_hash(pw)
+ assert auth_tools.bcrypt_check_password(
+ pw, hashed_pw)
+ assert not auth_tools.bcrypt_check_password(
+ 'notthepassword', hashed_pw)
+
+ # Same thing, extra salt.
+ hashed_pw = auth_tools.bcrypt_gen_password_hash(pw, '3><7R45417')
+ assert auth_tools.bcrypt_check_password(
+ pw, hashed_pw, '3><7R45417')
+ assert not auth_tools.bcrypt_check_password(
+ 'notthepassword', hashed_pw, '3><7R45417')
diff --git a/mediagoblin/tests/test_celery_setup.py b/mediagoblin/tests/test_celery_setup.py
index 5530c6f2..0184436a 100644
--- a/mediagoblin/tests/test_celery_setup.py
+++ b/mediagoblin/tests/test_celery_setup.py
@@ -48,7 +48,7 @@ def test_setup_celery_from_config():
assert isinstance(fake_celery_module.CELERYD_ETA_SCHEDULER_PRECISION, float)
assert fake_celery_module.CELERY_RESULT_PERSISTENT is True
assert fake_celery_module.CELERY_IMPORTS == [
- 'foo.bar.baz', 'this.is.an.import', 'mediagoblin.processing.task']
+ 'foo.bar.baz', 'this.is.an.import', 'mediagoblin.processing.task', 'mediagoblin.notifications.task']
assert fake_celery_module.CELERY_RESULT_BACKEND == 'database'
assert fake_celery_module.CELERY_RESULT_DBURI == (
'sqlite:///' +
diff --git a/mediagoblin/tests/test_edit.py b/mediagoblin/tests/test_edit.py
index 08b4f8cf..acc638d9 100644
--- a/mediagoblin/tests/test_edit.py
+++ b/mediagoblin/tests/test_edit.py
@@ -15,13 +15,13 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import urlparse
-import pytest
from mediagoblin import mg_globals
from mediagoblin.db.models import User
from mediagoblin.tests.tools import fixture_add_user
-from mediagoblin.tools import template
-from mediagoblin.auth.lib import bcrypt_check_password
+from mediagoblin import auth
+from mediagoblin.tools import template, mail
+
class TestUserEdit(object):
def setup(self):
@@ -74,7 +74,7 @@ class TestUserEdit(object):
# 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 bcrypt_check_password('123456', test_user.pw_hash)
+ assert auth.check_password('123456', test_user.pw_hash)
# Update current user passwd
self.user_password = '123456'
@@ -88,7 +88,7 @@ class TestUserEdit(object):
})
test_user = User.query.filter_by(username=u'chris').first()
- assert not bcrypt_check_password('098765', test_user.pw_hash)
+ assert not auth.check_password('098765', test_user.pw_hash)
def test_change_bio_url(self, test_app):
@@ -141,4 +141,68 @@ class TestUserEdit(object):
assert form.url.errors == [
u'This address contains errors']
+ def test_email_change(self, test_app):
+ self.login(test_app)
+
+ # Test email already in db
+ template.clear_test_template_context()
+ test_app.post(
+ '/edit/account/', {
+ 'new_email': 'chris@example.com',
+ 'password': 'toast'})
+
+ # Check form errors
+ context = template.TEMPLATE_TEST_CONTEXT[
+ 'mediagoblin/edit/edit_account.html']
+ assert context['form'].new_email.errors == [
+ u'Sorry, a user with that email address already exists.']
+
+ # Test successful email change
+ template.clear_test_template_context()
+ res = test_app.post(
+ '/edit/account/', {
+ 'new_email': 'new@example.com',
+ 'password': 'toast'})
+ res.follow()
+
+ # Correct redirect?
+ assert urlparse.urlsplit(res.location)[2] == '/u/chris/'
+
+ # Make sure we get email verification and try verifying
+ assert len(mail.EMAIL_TEST_INBOX) == 1
+ message = mail.EMAIL_TEST_INBOX.pop()
+ assert message['To'] == 'new@example.com'
+ email_context = template.TEMPLATE_TEST_CONTEXT[
+ 'mediagoblin/edit/verification.txt']
+ assert email_context['verification_url'] in \
+ message.get_payload(decode=True)
+
+ path = urlparse.urlsplit(email_context['verification_url'])[2]
+ assert path == u'/edit/verify_email/'
+
+ ## Try verifying with bs verification key, shouldn't work
+ template.clear_test_template_context()
+ res = test_app.get(
+ "/edit/verify_email/?token=total_bs")
+ res.follow()
+
+ # Correct redirect?
+ assert urlparse.urlsplit(res.location)[2] == '/'
+
+ # Email shouldn't be saved
+ email_in_db = mg_globals.database.User.find_one(
+ {'email': 'new@example.com'})
+ email = User.query.filter_by(username='chris').first().email
+ assert email_in_db is None
+ assert email == 'chris@example.com'
+
+ # Verify email activation works
+ template.clear_test_template_context()
+ get_params = urlparse.urlsplit(email_context['verification_url'])[3]
+ res = test_app.get('%s?%s' % (path, get_params))
+ res.follow()
+
+ # New email saved?
+ email = User.query.filter_by(username='chris').first().email
+ assert email == 'new@example.com'
# test changing the url inproperly
diff --git a/mediagoblin/tests/test_mgoblin_app.ini b/mediagoblin/tests/test_mgoblin_app.ini
index 0466b53b..5c3c46e7 100644
--- a/mediagoblin/tests/test_mgoblin_app.ini
+++ b/mediagoblin/tests/test_mgoblin_app.ini
@@ -3,8 +3,9 @@ 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"
+#Runs with an in-memory sqlite db for speed.
+sql_engine = "sqlite://"
+run_migrations = true
# tag parsing
tags_max_length = 50
@@ -31,3 +32,5 @@ BROKER_HOST = "sqlite:///%(here)s/user_dev/kombu.db"
[[mediagoblin.plugins.oauth]]
[[mediagoblin.plugins.httpapiauth]]
[[mediagoblin.plugins.piwigo]]
+[[mediagoblin.plugins.basic_auth]]
+[[mediagoblin.plugins.openid]]
diff --git a/mediagoblin/tests/test_misc.py b/mediagoblin/tests/test_misc.py
index 755d863f..43ad0b6d 100644
--- a/mediagoblin/tests/test_misc.py
+++ b/mediagoblin/tests/test_misc.py
@@ -28,8 +28,10 @@ def test_user_deletes_other_comments(test_app):
user_a = fixture_add_user(u"chris_a")
user_b = fixture_add_user(u"chris_b")
- media_a = fixture_media_entry(uploader=user_a.id, save=False)
- media_b = fixture_media_entry(uploader=user_b.id, save=False)
+ media_a = fixture_media_entry(uploader=user_a.id, save=False,
+ expunge=False, fake_upload=False)
+ media_b = fixture_media_entry(uploader=user_b.id, save=False,
+ expunge=False, fake_upload=False)
Session.add(media_a)
Session.add(media_b)
Session.flush()
@@ -79,7 +81,7 @@ def test_user_deletes_other_comments(test_app):
def test_media_deletes_broken_attachment(test_app):
user_a = fixture_add_user(u"chris_a")
- media = fixture_media_entry(uploader=user_a.id, save=False)
+ media = fixture_media_entry(uploader=user_a.id, save=False, expunge=False)
media.attachment_files.append(dict(
name=u"some name",
filepath=[u"does", u"not", u"exist"],
diff --git a/mediagoblin/tests/test_notifications.py b/mediagoblin/tests/test_notifications.py
new file mode 100644
index 00000000..d52b8d5a
--- /dev/null
+++ b/mediagoblin/tests/test_notifications.py
@@ -0,0 +1,151 @@
+# 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 pytest
+
+import urlparse
+
+from mediagoblin.tools import template, mail
+
+from mediagoblin.db.models import Notification, CommentNotification, \
+ CommentSubscription
+from mediagoblin.db.base import Session
+
+from mediagoblin.notifications import mark_comment_notification_seen
+
+from mediagoblin.tests.tools import fixture_add_comment, \
+ fixture_media_entry, fixture_add_user, \
+ fixture_comment_subscription
+
+
+class TestNotifications:
+ @pytest.fixture(autouse=True)
+ def setup(self, test_app):
+ self.test_app = test_app
+
+ # TODO: Possibly abstract into a decorator like:
+ # @as_authenticated_user('chris')
+ self.test_user = fixture_add_user()
+
+ self.current_user = None
+
+ self.login()
+
+ def login(self, username=u'chris', password=u'toast'):
+ response = self.test_app.post(
+ '/auth/login/', {
+ 'username': username,
+ 'password': password})
+
+ response.follow()
+
+ assert urlparse.urlsplit(response.location)[2] == '/'
+ assert 'mediagoblin/root.html' in template.TEMPLATE_TEST_CONTEXT
+
+ ctx = template.TEMPLATE_TEST_CONTEXT['mediagoblin/root.html']
+
+ assert Session.merge(ctx['request'].user).username == username
+
+ self.current_user = ctx['request'].user
+
+ def logout(self):
+ self.test_app.get('/auth/logout/')
+ self.current_user = None
+
+ @pytest.mark.parametrize('wants_email', [True, False])
+ def test_comment_notification(self, wants_email):
+ '''
+ Test
+ - if a notification is created when posting a comment on
+ another users media entry.
+ - that the comment data is consistent and exists.
+
+ '''
+ user = fixture_add_user('otherperson', password='nosreprehto',
+ wants_comment_notification=wants_email)
+
+ user_id = user.id
+
+ media_entry = fixture_media_entry(uploader=user.id, state=u'processed')
+
+ media_entry_id = media_entry.id
+
+ subscription = fixture_comment_subscription(media_entry)
+
+ subscription_id = subscription.id
+
+ media_uri_id = '/u/{0}/m/{1}/'.format(user.username,
+ media_entry.id)
+ media_uri_slug = '/u/{0}/m/{1}/'.format(user.username,
+ media_entry.slug)
+
+ self.test_app.post(
+ media_uri_id + 'comment/add/',
+ {
+ 'comment_content': u'Test comment #42'
+ }
+ )
+
+ notifications = Notification.query.filter_by(
+ user_id=user.id).all()
+
+ assert len(notifications) == 1
+
+ notification = notifications[0]
+
+ assert type(notification) == CommentNotification
+ assert notification.seen == False
+ assert notification.user_id == user.id
+ assert notification.subject.get_author.id == self.test_user.id
+ assert notification.subject.content == u'Test comment #42'
+
+ if wants_email == True:
+ assert mail.EMAIL_TEST_MBOX_INBOX == [
+ {'from': 'notice@mediagoblin.example.org',
+ 'message': 'Content-Type: text/plain; \
+charset="utf-8"\nMIME-Version: 1.0\nContent-Transfer-Encoding: \
+base64\nSubject: GNU MediaGoblin - chris commented on your \
+post\nFrom: notice@mediagoblin.example.org\nTo: \
+otherperson@example.com\n\nSGkgb3RoZXJwZXJzb24sCmNocmlzIGNvbW1lbnRlZCBvbiB5b3VyIHBvc3QgKGh0dHA6Ly9sb2Nh\nbGhvc3Q6ODAvdS9vdGhlcnBlcnNvbi9tL3NvbWUtdGl0bGUvYy8xLyNjb21tZW50KSBhdCBHTlUg\nTWVkaWFHb2JsaW4KClRlc3QgY29tbWVudCAjNDIKCkdOVSBNZWRpYUdvYmxpbg==\n',
+ 'to': [u'otherperson@example.com']}]
+ else:
+ assert mail.EMAIL_TEST_MBOX_INBOX == []
+
+ # Save the ids temporarily because of DetachedInstanceError
+ notification_id = notification.id
+ comment_id = notification.subject.id
+
+ self.logout()
+ self.login('otherperson', 'nosreprehto')
+
+ self.test_app.get(media_uri_slug + '/c/{0}/'.format(comment_id))
+
+ notification = Notification.query.filter_by(id=notification_id).first()
+
+ assert notification.seen == True
+
+ self.test_app.get(media_uri_slug + '/notifications/silence/')
+
+ subscription = CommentSubscription.query.filter_by(id=subscription_id)\
+ .first()
+
+ assert subscription.notify == False
+
+ notifications = Notification.query.filter_by(
+ user_id=user_id).all()
+
+ # User should not have been notified
+ assert len(notifications) == 1
diff --git a/mediagoblin/tests/test_openid.py b/mediagoblin/tests/test_openid.py
new file mode 100644
index 00000000..c85f6318
--- /dev/null
+++ b/mediagoblin/tests/test_openid.py
@@ -0,0 +1,372 @@
+# 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 openid.consumer.consumer import SuccessResponse
+
+from mediagoblin import mg_globals
+from mediagoblin.db.base import Session
+from mediagoblin.db.models import User
+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):
+ return get_app(
+ request,
+ mgoblin_config=pkg_resources.resource_filename(
+ 'mediagoblin.tests.auth_configs',
+ 'openid_appconfig.ini'))
+
+
+class TestOpenIDPlugin(object):
+ def _setup(self, openid_plugin_app, value=True, edit=False, delete=False):
+ if value:
+ response = SuccessResponse(mock.Mock(), mock.Mock())
+ if edit or delete:
+ response.identity_url = u'http://add.myopenid.com'
+ else:
+ response.identity_url = u'http://real.myopenid.com'
+ self._finish_verification = mock.Mock(return_value=response)
+ else:
+ self._finish_verification = mock.Mock(return_value=False)
+
+ @mock.patch('mediagoblin.plugins.openid.views._response_email', mock.Mock(return_value=None))
+ @mock.patch('mediagoblin.plugins.openid.views._response_nickname', mock.Mock(return_value=None))
+ @mock.patch('mediagoblin.plugins.openid.views._finish_verification', self._finish_verification)
+ def _setup_start(self, openid_plugin_app, edit, delete):
+ if edit:
+ self._start_verification = mock.Mock(return_value=openid_plugin_app.post(
+ '/edit/openid/finish/'))
+ elif delete:
+ self._start_verification = mock.Mock(return_value=openid_plugin_app.post(
+ '/edit/openid/delete/finish/'))
+ else:
+ self._start_verification = mock.Mock(return_value=openid_plugin_app.post(
+ '/auth/openid/login/finish/'))
+ _setup_start(self, openid_plugin_app, edit, delete)
+
+ def test_bad_login(self, openid_plugin_app):
+ """ Test that attempts to login with invalid paramaters"""
+
+ # Test GET request for auth/register page
+ res = openid_plugin_app.get('/auth/register/').follow()
+
+ # Make sure it redirected to the correct place
+ assert urlparse.urlsplit(res.location)[2] == '/auth/openid/login/'
+
+ # Test GET request for auth/login page
+ res = openid_plugin_app.get('/auth/login/')
+ res.follow()
+
+ # Correct redirect?
+ assert urlparse.urlsplit(res.location)[2] == '/auth/openid/login/'
+
+ # Test GET request for auth/openid/register page
+ res = openid_plugin_app.get('/auth/openid/register/')
+ res.follow()
+
+ # Correct redirect?
+ assert urlparse.urlsplit(res.location)[2] == '/auth/openid/login/'
+
+ # Test GET request for auth/openid/login/finish page
+ res = openid_plugin_app.get('/auth/openid/login/finish/')
+ res.follow()
+
+ # Correct redirect?
+ assert urlparse.urlsplit(res.location)[2] == '/auth/openid/login/'
+
+ # Test GET request for auth/openid/login page
+ res = openid_plugin_app.get('/auth/openid/login/')
+
+ # Correct place?
+ assert 'mediagoblin/plugins/openid/login.html' in template.TEMPLATE_TEST_CONTEXT
+
+ # Try to login with an empty form
+ template.clear_test_template_context()
+ openid_plugin_app.post(
+ '/auth/openid/login/', {})
+ context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/plugins/openid/login.html']
+ form = context['login_form']
+ assert form.openid.errors == [u'This field is required.']
+
+ # Try to login with wrong form values
+ template.clear_test_template_context()
+ openid_plugin_app.post(
+ '/auth/openid/login/', {
+ 'openid': 'not_a_url.com'})
+ context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/plugins/openid/login.html']
+ form = context['login_form']
+ assert form.openid.errors == [u'Please enter a valid url.']
+
+ # Should be no users in the db
+ assert User.query.count() == 0
+
+ # Phony OpenID URl
+ template.clear_test_template_context()
+ openid_plugin_app.post(
+ '/auth/openid/login/', {
+ 'openid': 'http://phoney.myopenid.com/'})
+ context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/plugins/openid/login.html']
+ form = context['login_form']
+ assert form.openid.errors == [u'Sorry, the OpenID server could not be found']
+
+ def test_login(self, openid_plugin_app):
+ """Tests that test login and registion with openid"""
+ # Test finish_login redirects correctly when response = False
+ self._setup(openid_plugin_app, False)
+
+ @mock.patch('mediagoblin.plugins.openid.views._finish_verification', self._finish_verification)
+ @mock.patch('mediagoblin.plugins.openid.views._start_verification', self._start_verification)
+ def _test_non_response():
+ template.clear_test_template_context()
+ res = openid_plugin_app.post(
+ '/auth/openid/login/', {
+ 'openid': 'http://phoney.myopenid.com/'})
+ res.follow()
+
+ # Correct Place?
+ assert urlparse.urlsplit(res.location)[2] == '/auth/openid/login/'
+ assert 'mediagoblin/plugins/openid/login.html' in template.TEMPLATE_TEST_CONTEXT
+ _test_non_response()
+
+ # Test login with new openid
+ # Need to clear_test_template_context before calling _setup
+ template.clear_test_template_context()
+ self._setup(openid_plugin_app)
+
+ @mock.patch('mediagoblin.plugins.openid.views._finish_verification', self._finish_verification)
+ @mock.patch('mediagoblin.plugins.openid.views._start_verification', self._start_verification)
+ def _test_new_user():
+ openid_plugin_app.post(
+ '/auth/openid/login/', {
+ 'openid': u'http://real.myopenid.com'})
+
+ # Right place?
+ assert 'mediagoblin/auth/register.html' in template.TEMPLATE_TEST_CONTEXT
+ context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/auth/register.html']
+ register_form = context['register_form']
+
+ # Register User
+ res = openid_plugin_app.post(
+ '/auth/openid/register/', {
+ 'openid': register_form.openid.data,
+ 'username': u'chris',
+ 'email': u'chris@example.com'})
+ res.follow()
+
+ # Correct place?
+ assert urlparse.urlsplit(res.location)[2] == '/u/chris/'
+ assert 'mediagoblin/user_pages/user.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
+ # tested in test_auth
+
+ # Logout User
+ openid_plugin_app.get('/auth/logout')
+
+ # Get user and detach from session
+ test_user = mg_globals.database.User.find_one({
+ 'username': u'chris'})
+ Session.expunge(test_user)
+
+ # Log back in
+ # Could not get it to work by 'POST'ing to /auth/openid/login/
+ template.clear_test_template_context()
+ res = openid_plugin_app.post(
+ '/auth/openid/login/finish/', {
+ 'openid': u'http://real.myopenid.com'})
+ 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_new_user()
+
+ # Test register with empty form
+ template.clear_test_template_context()
+ openid_plugin_app.post(
+ '/auth/openid/register/', {})
+ context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/auth/register.html']
+ register_form = context['register_form']
+
+ assert register_form.openid.errors == [u'This field is required.']
+ assert register_form.email.errors == [u'This field is required.']
+ assert register_form.username.errors == [u'This field is required.']
+
+ # Try to register with existing username and email
+ template.clear_test_template_context()
+ openid_plugin_app.post(
+ '/auth/openid/register/', {
+ 'openid': 'http://real.myopenid.com',
+ 'email': 'chris@example.com',
+ 'username': 'chris'})
+ context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/auth/register.html']
+ register_form = context['register_form']
+
+ assert register_form.username.errors == [u'Sorry, a user with that name already exists.']
+ assert register_form.email.errors == [u'Sorry, a user with that email address already exists.']
+ assert register_form.openid.errors == [u'Sorry, an account is already registered to that OpenID.']
+
+ def test_add_delete(self, openid_plugin_app):
+ """Test adding and deleting openids"""
+ # Add user
+ test_user = fixture_add_user(password='')
+ openid = OpenIDUserURL()
+ openid.openid_url = 'http://real.myopenid.com'
+ openid.user_id = test_user.id
+ openid.save()
+
+ # Log user in
+ template.clear_test_template_context()
+ self._setup(openid_plugin_app)
+
+ @mock.patch('mediagoblin.plugins.openid.views._finish_verification', self._finish_verification)
+ @mock.patch('mediagoblin.plugins.openid.views._start_verification', self._start_verification)
+ def _login_user():
+ openid_plugin_app.post(
+ '/auth/openid/login/finish/', {
+ 'openid': u'http://real.myopenid.com'})
+
+ _login_user()
+
+ # Try and delete only OpenID url
+ template.clear_test_template_context()
+ res = openid_plugin_app.post(
+ '/edit/openid/delete/', {
+ 'openid': 'http://real.myopenid.com'})
+ assert 'mediagoblin/plugins/openid/delete.html' in template.TEMPLATE_TEST_CONTEXT
+
+ # Add OpenID to user
+ # Empty form
+ template.clear_test_template_context()
+ res = openid_plugin_app.post(
+ '/edit/openid/', {})
+ context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/plugins/openid/add.html']
+ form = context['form']
+ assert form.openid.errors == [u'This field is required.']
+
+ # Try with a bad url
+ template.clear_test_template_context()
+ openid_plugin_app.post(
+ '/edit/openid/', {
+ 'openid': u'not_a_url.com'})
+ context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/plugins/openid/add.html']
+ form = context['form']
+ assert form.openid.errors == [u'Please enter a valid url.']
+
+ # Try with a url that's already registered
+ template.clear_test_template_context()
+ openid_plugin_app.post(
+ '/edit/openid/', {
+ 'openid': 'http://real.myopenid.com'})
+ context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/plugins/openid/add.html']
+ form = context['form']
+ assert form.openid.errors == [u'Sorry, an account is already registered to that OpenID.']
+
+ # Test adding openid to account
+ # Need to clear_test_template_context before calling _setup
+ template.clear_test_template_context()
+ self._setup(openid_plugin_app, edit=True)
+
+ # Need to remove openid_url from db because it was added at setup
+ openid = OpenIDUserURL.query.filter_by(
+ openid_url=u'http://add.myopenid.com')
+ openid.delete()
+
+ @mock.patch('mediagoblin.plugins.openid.views._finish_verification', self._finish_verification)
+ @mock.patch('mediagoblin.plugins.openid.views._start_verification', self._start_verification)
+ def _test_add():
+ # Successful add
+ template.clear_test_template_context()
+ res = openid_plugin_app.post(
+ '/edit/openid/', {
+ 'openid': u'http://add.myopenid.com'})
+ res.follow()
+
+ # Correct place?
+ assert urlparse.urlsplit(res.location)[2] == '/edit/account/'
+ assert 'mediagoblin/edit/edit_account.html' in template.TEMPLATE_TEST_CONTEXT
+
+ # OpenID Added?
+ new_openid = mg_globals.database.OpenIDUserURL.find_one(
+ {'openid_url': u'http://add.myopenid.com'})
+ assert new_openid
+
+ _test_add()
+
+ # Test deleting openid from account
+ # Need to clear_test_template_context before calling _setup
+ template.clear_test_template_context()
+ self._setup(openid_plugin_app, delete=True)
+
+ # Need to add OpenID back to user because it was deleted during
+ # patch
+ openid = OpenIDUserURL()
+ openid.openid_url = 'http://add.myopenid.com'
+ openid.user_id = test_user.id
+ openid.save()
+
+ @mock.patch('mediagoblin.plugins.openid.views._finish_verification', self._finish_verification)
+ @mock.patch('mediagoblin.plugins.openid.views._start_verification', self._start_verification)
+ def _test_delete(self, test_user):
+ # Delete openid from user
+ # Create another user to test deleting OpenID that doesn't belong to them
+ new_user = fixture_add_user(username='newman')
+ openid = OpenIDUserURL()
+ openid.openid_url = 'http://realfake.myopenid.com/'
+ openid.user_id = new_user.id
+ openid.save()
+
+ # Try and delete OpenID url that isn't the users
+ template.clear_test_template_context()
+ res = openid_plugin_app.post(
+ '/edit/openid/delete/', {
+ 'openid': 'http://realfake.myopenid.com/'})
+ context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/plugins/openid/delete.html']
+ form = context['form']
+ assert form.openid.errors == [u'That OpenID is not registered to this account.']
+
+ # Delete OpenID
+ # Kind of weird to POST to delete/finish
+ template.clear_test_template_context()
+ res = openid_plugin_app.post(
+ '/edit/openid/delete/finish/', {
+ 'openid': u'http://add.myopenid.com'})
+ res.follow()
+
+ # Correct place?
+ assert urlparse.urlsplit(res.location)[2] == '/edit/account/'
+ assert 'mediagoblin/edit/edit_account.html' in template.TEMPLATE_TEST_CONTEXT
+
+ # OpenID deleted?
+ new_openid = mg_globals.database.OpenIDUserURL.find_one(
+ {'openid_url': u'http://add.myopenid.com'})
+ assert not new_openid
+
+ _test_delete(self, test_user)
diff --git a/mediagoblin/tests/tools.py b/mediagoblin/tests/tools.py
index 2ee39e89..2584c62f 100644
--- a/mediagoblin/tests/tools.py
+++ b/mediagoblin/tests/tools.py
@@ -15,23 +15,22 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-import sys
import os
import pkg_resources
import shutil
-from functools import wraps
from paste.deploy import loadapp
from webtest import TestApp
from mediagoblin import mg_globals
-from mediagoblin.db.models import User, MediaEntry, Collection
+from mediagoblin.db.models import User, MediaEntry, Collection, MediaComment, \
+ CommentSubscription, CommentNotification
from mediagoblin.tools import testing
from mediagoblin.init.config import read_mediagoblin_config
from mediagoblin.db.base import Session
from mediagoblin.meddleware import BaseMeddleware
-from mediagoblin.auth.lib import bcrypt_gen_password_hash
+from mediagoblin.auth import gen_password_hash
from mediagoblin.gmg_commands.dbupdate import run_dbupdate
@@ -171,7 +170,7 @@ def assert_db_meets_expected(db, expected):
def fixture_add_user(username=u'chris', password=u'toast',
- active_user=True):
+ active_user=True, wants_comment_notification=True):
# Reuse existing user or create a new one
test_user = User.query.filter_by(username=username).first()
if test_user is None:
@@ -179,11 +178,13 @@ def fixture_add_user(username=u'chris', password=u'toast',
test_user.username = username
test_user.email = username + u'@example.com'
if password is not None:
- test_user.pw_hash = bcrypt_gen_password_hash(password)
+ test_user.pw_hash = gen_password_hash(password)
if active_user:
test_user.email_verified = True
test_user.status = u'active'
+ test_user.wants_comment_notification = wants_comment_notification
+
test_user.save()
# Reload
@@ -195,19 +196,79 @@ def fixture_add_user(username=u'chris', password=u'toast',
return test_user
+def fixture_comment_subscription(entry, notify=True, send_email=None):
+ if send_email is None:
+ uploader = User.query.filter_by(id=entry.uploader).first()
+ send_email = uploader.wants_comment_notification
+
+ cs = CommentSubscription(
+ media_entry_id=entry.id,
+ user_id=entry.uploader,
+ notify=notify,
+ send_email=send_email)
+
+ cs.save()
+
+ cs = CommentSubscription.query.filter_by(id=cs.id).first()
+
+ Session.expunge(cs)
+
+ return cs
+
+
+def fixture_add_comment_notification(entry_id, subject_id, user_id,
+ seen=False):
+ cn = CommentNotification(user_id=user_id,
+ seen=seen,
+ subject_id=subject_id)
+ cn.save()
+
+ cn = CommentNotification.query.filter_by(id=cn.id).first()
+
+ Session.expunge(cn)
+
+ return cn
+
+
def fixture_media_entry(title=u"Some title", slug=None,
- uploader=None, save=True, gen_slug=True):
+ uploader=None, save=True, gen_slug=True,
+ state=u'unprocessed', fake_upload=True,
+ expunge=True):
+ """
+ Add a media entry for testing purposes.
+
+ Caution: if you're adding multiple entries with fake_upload=True,
+ make sure you save between them... otherwise you'll hit an
+ IntegrityError from multiple newly-added-MediaEntries adding
+ FileKeynames at once. :)
+ """
+ if uploader is None:
+ uploader = fixture_add_user().id
+
entry = MediaEntry()
entry.title = title
entry.slug = slug
- entry.uploader = uploader or fixture_add_user().id
+ entry.uploader = uploader
entry.media_type = u'image'
+ entry.state = state
+
+ if fake_upload:
+ entry.media_files = {'thumb': ['a', 'b', 'c.jpg'],
+ 'medium': ['d', 'e', 'f.png'],
+ 'original': ['g', 'h', 'i.png']}
+ entry.media_type = u'mediagoblin.media_types.image'
if gen_slug:
entry.generate_slug()
+
if save:
entry.save()
+ if expunge:
+ entry = MediaEntry.query.filter_by(id=entry.id).first()
+
+ Session.expunge(entry)
+
return entry
@@ -231,3 +292,25 @@ def fixture_add_collection(name=u"My first Collection", user=None):
return coll
+def fixture_add_comment(author=None, media_entry=None, comment=None):
+ if author is None:
+ author = fixture_add_user().id
+
+ if media_entry is None:
+ media_entry = fixture_media_entry().id
+
+ if comment is None:
+ comment = \
+ 'Auto-generated test comment by user #{0} on media #{0}'.format(
+ author, media_entry)
+
+ comment = MediaComment(author=author,
+ media_entry=media_entry,
+ content=comment)
+
+ comment.save()
+
+ Session.expunge(comment)
+
+ return comment
+
diff --git a/mediagoblin/tools/mail.py b/mediagoblin/tools/mail.py
index 6886c859..0fabc5a9 100644
--- a/mediagoblin/tools/mail.py
+++ b/mediagoblin/tools/mail.py
@@ -90,7 +90,12 @@ def send_email(from_addr, to_addrs, subject, message_body):
if common.TESTS_ENABLED or mg_globals.app_config['email_debug_mode']:
mhost = FakeMhost()
elif not mg_globals.app_config['email_debug_mode']:
- mhost = smtplib.SMTP(
+ if mg_globals.app_config['email_smtp_use_ssl']:
+ smtp_init = smtplib.SMTP_SSL
+ else:
+ smtp_init = smtplib.SMTP
+
+ mhost = smtp_init(
mg_globals.app_config['email_smtp_host'],
mg_globals.app_config['email_smtp_port'])
diff --git a/mediagoblin/tools/template.py b/mediagoblin/tools/template.py
index 3d651a6e..615ce129 100644
--- a/mediagoblin/tools/template.py
+++ b/mediagoblin/tools/template.py
@@ -71,6 +71,7 @@ def get_jinja_env(template_loader, locale):
template_env.globals['app_config'] = mg_globals.app_config
template_env.globals['global_config'] = mg_globals.global_config
template_env.globals['version'] = _version.__version__
+ template_env.globals['auth'] = mg_globals.app.auth
template_env.filters['urlencode'] = url_quote_plus
diff --git a/mediagoblin/user_pages/views.py b/mediagoblin/user_pages/views.py
index 738cc054..83a524ec 100644
--- a/mediagoblin/user_pages/views.py
+++ b/mediagoblin/user_pages/views.py
@@ -25,8 +25,9 @@ from mediagoblin.tools.response import render_to_response, render_404, \
from mediagoblin.tools.translate import pass_to_ugettext as _
from mediagoblin.tools.pagination import Pagination
from mediagoblin.user_pages import forms as user_forms
-from mediagoblin.user_pages.lib import (send_comment_email,
- add_media_to_collection)
+from mediagoblin.user_pages.lib import add_media_to_collection
+from mediagoblin.notifications import trigger_notification, \
+ add_comment_subscription, mark_comment_notification_seen
from mediagoblin.decorators import (uses_pagination, get_user_media_entry,
get_media_entry_by_id,
@@ -34,6 +35,7 @@ from mediagoblin.decorators import (uses_pagination, get_user_media_entry,
get_user_collection, get_user_collection_item, active_user_from_url)
from werkzeug.contrib.atom import AtomFeed
+from werkzeug.exceptions import MethodNotAllowed
_log = logging.getLogger(__name__)
@@ -110,6 +112,7 @@ def user_gallery(request, page, url_user=None):
'media_entries': media_entries,
'pagination': pagination})
+
MEDIA_COMMENTS_PER_PAGE = 50
@@ -121,6 +124,9 @@ def media_home(request, media, page, **kwargs):
"""
comment_id = request.matchdict.get('comment', None)
if comment_id:
+ if request.user:
+ mark_comment_notification_seen(comment_id, request.user)
+
pagination = Pagination(
page, media.get_comments(
mg_globals.app_config['comments_ascending']),
@@ -154,7 +160,8 @@ def media_post_comment(request, media):
"""
recieves POST from a MediaEntry() comment form, saves the comment.
"""
- assert request.method == 'POST'
+ if not request.method == 'POST':
+ raise MethodNotAllowed()
comment = request.db.MediaComment()
comment.media_entry = media.id
@@ -179,11 +186,9 @@ def media_post_comment(request, media):
request, messages.SUCCESS,
_('Your comment has been posted!'))
- media_uploader = media.get_uploader
- #don't send email if you comment on your own post
- if (comment.author != media_uploader and
- media_uploader.wants_comment_notification):
- send_comment_email(media_uploader, comment, media, request)
+ trigger_notification(comment, media, request)
+
+ add_comment_subscription(request.user, media)
return redirect_obj(request, media)
diff --git a/setup.py b/setup.py
index f28a2bb0..6e026f30 100644
--- a/setup.py
+++ b/setup.py
@@ -14,7 +14,7 @@
# 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 setuptools import setup
+from setuptools import setup, find_packages
import os
import re
@@ -36,7 +36,7 @@ def get_version():
setup(
name="mediagoblin",
version=get_version(),
- packages=['mediagoblin'],
+ packages=find_packages(exclude=['ez_setup', 'examples', 'tests']),
zip_safe=False,
include_package_data = True,
# scripts and dependencies
@@ -57,7 +57,7 @@ setup(
'webtest<2',
'ConfigObj',
'Markdown',
- 'sqlalchemy>=0.7.0',
+ 'sqlalchemy>=0.8.0',
'sqlalchemy-migrate',
'mock',
'itsdangerous',