summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCarl Suster <carl@contraflo.ws>2017-01-08 18:13:09 +1100
committerCarl Suster <carl@contraflo.ws>2017-01-08 18:13:09 +1100
commita9de2f219c8069773b7270f13d2c2278e5381dbc (patch)
treee409db37caf401609d2a5a10bc451db4afcc6f2b
import flask-login_0.4.0.orig.tar.gz
-rw-r--r--.gitignore17
-rw-r--r--.travis.yml10
-rw-r--r--CHANGES113
-rw-r--r--CONTRIBUTING.md25
-rw-r--r--ISSUE_TEMPLATE.md5
-rw-r--r--LICENSE22
-rw-r--r--MANIFEST.in1
-rw-r--r--Makefile29
-rw-r--r--README.md163
-rw-r--r--dev-py3k-requirements.txt9
-rw-r--r--dev-requirements.txt10
-rw-r--r--docs/Makefile130
-rw-r--r--docs/_themes/LICENSE37
-rw-r--r--docs/_themes/README31
-rw-r--r--docs/_themes/flask/layout.html16
-rw-r--r--docs/_themes/flask/relations.html19
-rw-r--r--docs/_themes/flask/static/flasky.css_t387
-rw-r--r--docs/_themes/flask/static/small_flask.css70
-rw-r--r--docs/_themes/flask/theme.conf7
-rw-r--r--docs/_themes/flask_small/layout.html22
-rw-r--r--docs/_themes/flask_small/static/flasky.css_t287
-rw-r--r--docs/_themes/flask_small/theme.conf10
-rw-r--r--docs/_themes/flask_theme_support.py86
-rw-r--r--docs/conf.py232
-rw-r--r--docs/index.rst546
-rw-r--r--docs/make.bat170
-rw-r--r--flask_login/__about__.py10
-rw-r--r--flask_login/__init__.py63
-rw-r--r--flask_login/_compat.py52
-rw-r--r--flask_login/config.py49
-rw-r--r--flask_login/login_manager.py425
-rw-r--r--flask_login/mixins.py76
-rw-r--r--flask_login/signals.py56
-rw-r--r--flask_login/utils.py345
-rw-r--r--install_requirements.py19
-rw-r--r--requirements.txt2
-rwxr-xr-xrun-tests.sh43
-rw-r--r--setup.cfg2
-rw-r--r--setup.py59
-rw-r--r--test_login.py1269
-rw-r--r--tox.ini22
41 files changed, 4946 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..2120435
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,17 @@
+!.gitignore
+
+tests_output/
+__pycache__/
+
+.tox
+.coverage
+
+*.py[co]
+*.egg-info
+*.swp
+
+dist/
+docs/_build/
+
+virtualenv/
+venv/
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..63bbf3a
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,10 @@
+language: python
+python:
+ - 2.6
+ - 2.7
+ - 3.3
+ - 3.4
+ - 3.5
+install:
+ - python install_requirements.py dev
+script: make check
diff --git a/CHANGES b/CHANGES
new file mode 100644
index 0000000..64198b2
--- /dev/null
+++ b/CHANGES
@@ -0,0 +1,113 @@
+Flask-Login Changelog
+=====================
+
+Here you can see the full list of changes between each Flask-Login release.
+
+Version 0.4.0
+-------------
+
+Released on TBD.
+
+- Fixes OPTIONS exemption from login. #244
+- Fixes use of MD5 by replacing with SHA512. #264
+- BREAKING: The `login_manager.token_handler` function, `get_auth_token` method
+ on the User class, and the `utils.make_secure_token` utility function have
+ been removed to prevent users from creating insecure auth implementations.
+ Use the `Alternative Tokens` example from the docs instead. #291
+
+
+Version 0.3.2
+-------------
+
+Released on October 8th, 2015
+
+- Fixes Python 2.6 compatibility.
+- Updates SESSION_KEYS to include "remember".
+
+
+Version 0.3.1
+-------------
+
+Released on September 30th, 2015
+
+- Fixes removal of non-Flask-Login keys from session object when using 'strong'
+ protection.
+
+
+Version 0.3.0
+-------------
+
+Released on September 10th, 2015
+
+- Fixes handling of X-Forward-For header.
+- Update to use SHA512 instead of MD5 for session identifier creation.
+- Fixes session creation for every view.
+- BREAKING: UTC used to set cookie duration.
+- BREAKING: Non-fresh logins now returns HTTP 401.
+- Support unicode user IDs in cookie.
+- Fixes user_logged_out signal invocation.
+- Support for per-Blueprint login views.
+- BREAKING: The `is_authenticated`, `is_active`, and `is_anonymous` members of
+ the user class are now properties, not methods. Applications should update
+ their user classes accordingly.
+- Various other improvements including documentation and code clean up.
+
+
+Version 0.2.11
+--------------
+
+Released on May 19th, 2014
+
+- Fixes missing request loader invocation when authorization header exists.
+
+
+Version 0.2.10
+--------------
+
+Released on March 9th, 2014
+
+- Generalized `request_loader` introduced; ability to log users in via
+ customized callback over request.
+- Fixes request context dependency by explicitly checking `has_request_context`.
+- Fixes remember me issues since lazy user loading changes.
+
+
+Version 0.2.9
+-------------
+
+Released on December 28th, 2013
+
+- Fixes anonymous user assignment.
+- Fixes localization in Python 3.
+
+
+Version 0.2.8
+-------------
+
+Released on December 21st 2013
+
+- Support login via authorization header. This allows login via Basic Auth, for
+ example. Useful in an API presentation context.
+- Ability to override user ID method name. This is useful if the ID getter is
+ named differently than the default.
+- Session data is now only read when the user is requested. This can be
+ beneficial for cookie and caching control when differenting between
+ requests that use user information for rendering and ones where all users
+ (including anonymous) get the same result (e.g. static pages)
+- BREAKING: User *must* always be accessed through the ``current_user``
+ local. This breaks any previous direct access to ``_request_ctx.top.user``.
+ This is because user is not loaded until current_user is accessed.
+- Fixes unnecessary access to the session when the user is anonymous
+ and session protection is active.
+ see https://github.com/maxcountryman/flask-login/issues/120
+- Fixes issue where order dependency of applying the login manager
+ before dependent applications was required.
+ see https://github.com/mattupstate/flask-principal/issues/22
+- Fixes Python 3 ``UserMixin`` hashing.
+- Fixes incorrect documentation.
+
+
+Previous Versions
+=================
+
+Prior to 0.2.8, no proper changelog was kept.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..9392ec6
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,25 @@
+# Contributor Guidelines
+
+Flask-Login is open source and will happily consider pull requests with bugfixes, documentation improvements, and ocassionally new features. Note that major changes will generally not be accepted.
+
+Before you submit an issue or pull request, please read the following guidlines.
+
+## Submitting Issues
+
+Before you submit a new issue, **please review the [CHANGES](https://github.com/maxcountryman/flask-login/blob/master/CHANGES) document**. This is where you will find all major changes, including breaking changes, which may be causing your issue.
+
+Do not open a new issue before reading through CHANGES thoroughly and reviewing other open and closed issues. Duplicate issues will be closed and locked. Please do not open issues related to release deadlines: we will get to it when we can and in the meantime you are free to issue your own releases however you like.
+
+Issues should relate to specific bugs or feature requests. If this doesn't fit the profile, then please don't open an issue.
+
+## Submitting a Pull Request
+
+If you'd like to submit PR, please make sure that all tests pass prior to submission. The README contains further instructions.
+
+## Extended Documentation
+
+Sphinx-generated documentation can be found [here](https://flask-login.readthedocs.io/en/latest/). This page is updated automatically. Documentation for prior versions of the library may be found there as well. Always review this page when a problem is first encountered.
+
+## Thanks
+
+Finally this project has seen contributions from many people and we owe them a debt of gratitude for taking time to improve the project.
diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md
new file mode 100644
index 0000000..e2ebf9c
--- /dev/null
+++ b/ISSUE_TEMPLATE.md
@@ -0,0 +1,5 @@
+Make sure these boxes are checked before submitting your issue--thank you!
+
+- [ ] Ensure you are using the latest PyPI release.
+- [ ] Read the [CHANGES](https://github.com/maxcountryman/flask-login/blob/master/CHANGES) document thoroughly.
+- [ ] Provide a clear and simple set of steps to reproduce your issue for others.
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..0446381
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,22 @@
+Copyright (c) 2011 Matthew Frazier
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..64ad321
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1 @@
+include README.md LICENSE
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..58a1aa8
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,29 @@
+.PHONY: all test clean_coverage clean pep8 pyflakes check
+
+all:
+ @echo 'test run the unit tests'
+ @echo 'coverage generate coverage statistics'
+ @echo 'pep8 check pep8 compliance'
+ @echo 'pyflakes check for unused imports (requires pyflakes)'
+ @echo 'check make sure you are ready to commit'
+ @echo 'clean cleanup the source tree'
+
+test: clean_coverage
+ @echo 'Running all tests...'
+ @VERBOSE=1 PATH=${PATH} ./run-tests.sh
+
+clean_coverage:
+ @rm -f .coverage
+
+clean:
+ @rm -f flask_login/*.pyc
+
+pep8:
+ @echo 'Checking pep8 compliance...'
+ @pep8 flask_login/* test_login.py
+
+pyflakes:
+ @echo 'Running pyflakes...'
+ @pyflakes flask_login/* test_login.py
+
+check: clean pep8 pyflakes test
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..66130a6
--- /dev/null
+++ b/README.md
@@ -0,0 +1,163 @@
+# Flask-Login
+
+[![build status](https://secure.travis-ci.org/maxcountryman/flask-login.png?branch=master)](https://travis-ci.org/#!/maxcountryman/flask-login)
+
+Flask-Login provides user session management for Flask. It handles the common
+tasks of logging in, logging out, and remembering your users' sessions over
+extended periods of time.
+
+Flask-Login is not bound to any particular database system or permissions
+model. The only requirement is that your user objects implement a few methods,
+and that you provide a callback to the extension capable of loading users from
+their ID.
+
+## Installation
+
+Install the extension with one of the following commands:
+
+```sh
+$ easy_install flask-login
+```
+
+or alternatively if you have pip installed:
+
+```sh
+$ pip install flask-login
+```
+
+## Usage
+
+Once installed, the Flask-Login is easy to use. Let's walk through setting up
+a basic application. Also please note that this is a very basic guide: we will
+be taking shortcuts here that you should never take in a real application.
+
+To begin we'll set up a Flask app:
+
+```python
+import flask
+
+app = flask.Flask(__name__)
+app.secret_key = 'super secret string' # Change this!
+```
+
+Flask-Login works via a login manager. To kick things off, we'll set up the
+login manager by instantiating it and telling it about our Flask app:
+
+```python
+import flask_login
+
+login_manager = flask_login.LoginManager()
+
+login_manager.init_app(app)
+```
+
+To keep things simple we're going to use a dictionary to represent a database
+of users. In a real application, this would be an actual persistence layer.
+However it's important to point out this is a feature of Flask-Login: it
+doesn't care how your data is stored so long as you tell it how to retrieve it!
+
+```python
+# Our mock database.
+users = {'foo@bar.tld': {'pw': 'secret'}}
+```
+
+We also need to tell Flask-Login how to load a user from a Flask request and
+from its session. To do this we need to define our user object, a
+`user_loader` callback, and a `request_loader` callback.
+
+```python
+class User(flask_login.UserMixin):
+ pass
+
+
+@login_manager.user_loader
+def user_loader(email):
+ if email not in users:
+ return
+
+ user = User()
+ user.id = email
+ return user
+
+
+@login_manager.request_loader
+def request_loader(request):
+ email = request.form.get('email')
+ if email not in users:
+ return
+
+ user = User()
+ user.id = email
+
+ # DO NOT ever store passwords in plaintext and always compare password
+ # hashes using constant-time comparison!
+ user.is_authenticated = request.form['pw'] == users[email]['pw']
+
+ return user
+```
+
+Now we're ready to define our views. We can start with a login view, which will
+populate the session with authentication bits. After that we can define a view
+that requires authentication.
+
+```python
+@app.route('/login', methods=['GET', 'POST'])
+def login():
+ if flask.request.method == 'GET':
+ return '''
+ <form action='login' method='POST'>
+ <input type='text' name='email' id='email' placeholder='email'></input>
+ <input type='password' name='pw' id='pw' placeholder='password'></input>
+ <input type='submit' name='submit'></input>
+ </form>
+ '''
+
+ email = flask.request.form['email']
+ if flask.request.form['pw'] == users[email]['pw']:
+ user = User()
+ user.id = email
+ flask_login.login_user(user)
+ return flask.redirect(flask.url_for('protected'))
+
+ return 'Bad login'
+
+
+@app.route('/protected')
+@flask_login.login_required
+def protected():
+ return 'Logged in as: ' + flask_login.current_user.id
+```
+
+Finally we can define a view to clear the session and log users out:
+
+```python
+@app.route('/logout')
+def logout():
+ flask_login.logout_user()
+ return 'Logged out'
+```
+
+We now have a basic working application that makes use of session-based
+authentication. To round things off, we should provide a callback for login
+failures:
+
+```python
+@login_manager.unauthorized_handler
+def unauthorized_handler():
+ return 'Unauthorized'
+```
+
+Complete documentation for Flask-Login is available on [ReadTheDocs](https://flask-login.readthedocs.io/en/latest/).
+
+
+## Contributing
+
+We welcome contributions! If you would like to hack on Flask-Login, please
+follow these steps:
+
+1. Fork this repository
+2. Make your changes
+3. Install the requirements in `dev-requirements.txt`
+4. Submit a pull request after running `make check` (ensure it does not error!)
+
+Please give us adequate time to review your submission. Thanks!
diff --git a/dev-py3k-requirements.txt b/dev-py3k-requirements.txt
new file mode 100644
index 0000000..61b3395
--- /dev/null
+++ b/dev-py3k-requirements.txt
@@ -0,0 +1,9 @@
+flask==0.10.1
+blinker==1.2
+coverage==3.6
+mock==1.0.1
+nose==1.2.1
+pep8==1.4.2
+pyflakes==1.1.0
+yanc==0.2.3
+werkzeug==0.9.1
diff --git a/dev-requirements.txt b/dev-requirements.txt
new file mode 100644
index 0000000..02cbc34
--- /dev/null
+++ b/dev-requirements.txt
@@ -0,0 +1,10 @@
+flask==0.9
+blinker==1.2
+coverage==3.7
+mock==1.0.1
+nose==1.3.0
+pep8==1.4.2
+pyflakes==1.1.0
+unittest2==0.5.1
+yanc==0.2.4
+werkzeug==0.8.3
diff --git a/docs/Makefile b/docs/Makefile
new file mode 100644
index 0000000..a0cd451
--- /dev/null
+++ b/docs/Makefile
@@ -0,0 +1,130 @@
+# Makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS =
+SPHINXBUILD = sphinx-build
+PAPER =
+BUILDDIR = _build
+
+# Internal variables.
+PAPEROPT_a4 = -D latex_paper_size=a4
+PAPEROPT_letter = -D latex_paper_size=letter
+ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+
+.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest
+
+help:
+ @echo "Please use \`make <target>' where <target> is one of"
+ @echo " html to make standalone HTML files"
+ @echo " dirhtml to make HTML files named index.html in directories"
+ @echo " singlehtml to make a single large HTML file"
+ @echo " pickle to make pickle files"
+ @echo " json to make JSON files"
+ @echo " htmlhelp to make HTML files and a HTML help project"
+ @echo " qthelp to make HTML files and a qthelp project"
+ @echo " devhelp to make HTML files and a Devhelp project"
+ @echo " epub to make an epub"
+ @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
+ @echo " latexpdf to make LaTeX files and run them through pdflatex"
+ @echo " text to make text files"
+ @echo " man to make manual pages"
+ @echo " changes to make an overview of all changed/added/deprecated items"
+ @echo " linkcheck to check all external links for integrity"
+ @echo " doctest to run all doctests embedded in the documentation (if enabled)"
+
+clean:
+ -rm -rf $(BUILDDIR)/*
+
+html:
+ $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
+
+dirhtml:
+ $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
+
+singlehtml:
+ $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
+ @echo
+ @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
+
+pickle:
+ $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
+ @echo
+ @echo "Build finished; now you can process the pickle files."
+
+json:
+ $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
+ @echo
+ @echo "Build finished; now you can process the JSON files."
+
+htmlhelp:
+ $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
+ @echo
+ @echo "Build finished; now you can run HTML Help Workshop with the" \
+ ".hhp project file in $(BUILDDIR)/htmlhelp."
+
+qthelp:
+ $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
+ @echo
+ @echo "Build finished; now you can run "qcollectiongenerator" with the" \
+ ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
+ @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Flask-Login.qhcp"
+ @echo "To view the help file:"
+ @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Flask-Login.qhc"
+
+devhelp:
+ $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
+ @echo
+ @echo "Build finished."
+ @echo "To view the help file:"
+ @echo "# mkdir -p $$HOME/.local/share/devhelp/Flask-Login"
+ @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Flask-Login"
+ @echo "# devhelp"
+
+epub:
+ $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
+ @echo
+ @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
+
+latex:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo
+ @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
+ @echo "Run \`make' in that directory to run these through (pdf)latex" \
+ "(use \`make latexpdf' here to do that automatically)."
+
+latexpdf:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo "Running LaTeX files through pdflatex..."
+ make -C $(BUILDDIR)/latex all-pdf
+ @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+text:
+ $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
+ @echo
+ @echo "Build finished. The text files are in $(BUILDDIR)/text."
+
+man:
+ $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
+ @echo
+ @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
+
+changes:
+ $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
+ @echo
+ @echo "The overview file is in $(BUILDDIR)/changes."
+
+linkcheck:
+ $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
+ @echo
+ @echo "Link check complete; look for any errors in the above output " \
+ "or in $(BUILDDIR)/linkcheck/output.txt."
+
+doctest:
+ $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
+ @echo "Testing of doctests in the sources finished, look at the " \
+ "results in $(BUILDDIR)/doctest/output.txt."
diff --git a/docs/_themes/LICENSE b/docs/_themes/LICENSE
new file mode 100644
index 0000000..8daab7e
--- /dev/null
+++ b/docs/_themes/LICENSE
@@ -0,0 +1,37 @@
+Copyright (c) 2010 by Armin Ronacher.
+
+Some rights reserved.
+
+Redistribution and use in source and binary forms of the theme, with or
+without modification, are permitted provided that the following conditions
+are met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials provided
+ with the distribution.
+
+* The names of the contributors may not be used to endorse or
+ promote products derived from this software without specific
+ prior written permission.
+
+We kindly ask you to only use these themes in an unmodified manner just
+for Flask and Flask-related products, not for unrelated projects. If you
+like the visual style and want to use it for your own projects, please
+consider making some larger changes to the themes (such as changing
+font faces, sizes, colors or margins).
+
+THIS THEME IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS THEME, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
diff --git a/docs/_themes/README b/docs/_themes/README
new file mode 100644
index 0000000..b3292bd
--- /dev/null
+++ b/docs/_themes/README
@@ -0,0 +1,31 @@
+Flask Sphinx Styles
+===================
+
+This repository contains sphinx styles for Flask and Flask related
+projects. To use this style in your Sphinx documentation, follow
+this guide:
+
+1. put this folder as _themes into your docs folder. Alternatively
+ you can also use git submodules to check out the contents there.
+2. add this to your conf.py:
+
+ sys.path.append(os.path.abspath('_themes'))
+ html_theme_path = ['_themes']
+ html_theme = 'flask'
+
+The following themes exist:
+
+- 'flask' - the standard flask documentation theme for large
+ projects
+- 'flask_small' - small one-page theme. Intended to be used by
+ very small addon libraries for flask.
+
+The following options exist for the flask_small theme:
+
+ [options]
+ index_logo = '' filename of a picture in _static
+ to be used as replacement for the
+ h1 in the index.rst file.
+ index_logo_height = 120px height of the index logo
+ github_fork = '' repository name on github for the
+ "fork me" badge
diff --git a/docs/_themes/flask/layout.html b/docs/_themes/flask/layout.html
new file mode 100644
index 0000000..d7c8792
--- /dev/null
+++ b/docs/_themes/flask/layout.html
@@ -0,0 +1,16 @@
+{%- extends "basic/layout.html" %}
+{%- block extrahead %}
+ {{ super() }}
+ {% if theme_touch_icon %}
+ <link rel="apple-touch-icon" href="{{ pathto('_static/' ~ theme_touch_icon, 1) }}" />
+ {% endif %}
+ <link media="only screen and (max-device-width: 480px)" href="{{
+ pathto('_static/small_flask.css', 1) }}" type= "text/css" rel="stylesheet" />
+{% endblock %}
+{%- block relbar2 %}{% endblock %}
+{%- block footer %}
+ <div class="footer">
+ &copy; Copyright {{ copyright }}.
+ Created using <a href="http://sphinx.pocoo.org/">Sphinx</a>.
+ </div>
+{%- endblock %}
diff --git a/docs/_themes/flask/relations.html b/docs/_themes/flask/relations.html
new file mode 100644
index 0000000..3bbcde8
--- /dev/null
+++ b/docs/_themes/flask/relations.html
@@ -0,0 +1,19 @@
+<h3>Related Topics</h3>
+<ul>
+ <li><a href="{{ pathto(master_doc) }}">Documentation overview</a><ul>
+ {%- for parent in parents %}
+ <li><a href="{{ parent.link|e }}">{{ parent.title }}</a><ul>
+ {%- endfor %}
+ {%- if prev %}
+ <li>Previous: <a href="{{ prev.link|e }}" title="{{ _('previous chapter')
+ }}">{{ prev.title }}</a></li>
+ {%- endif %}
+ {%- if next %}
+ <li>Next: <a href="{{ next.link|e }}" title="{{ _('next chapter')
+ }}">{{ next.title }}</a></li>
+ {%- endif %}
+ {%- for parent in parents %}
+ </ul></li>
+ {%- endfor %}
+ </ul></li>
+</ul>
diff --git a/docs/_themes/flask/static/flasky.css_t b/docs/_themes/flask/static/flasky.css_t
new file mode 100644
index 0000000..0de60ee
--- /dev/null
+++ b/docs/_themes/flask/static/flasky.css_t
@@ -0,0 +1,387 @@
+/*
+ * flasky.css_t
+ * ~~~~~~~~~~~~
+ *
+ * :copyright: Copyright 2010 by Armin Ronacher.
+ * :license: Flask Design License, see LICENSE for details.
+ */
+
+{% set page_width = '940px' %}
+{% set sidebar_width = '220px' %}
+
+@import url("basic.css");
+
+/* -- page layout ----------------------------------------------------------- */
+
+body {
+ font-family: 'Georgia', serif;
+ font-size: 17px;
+ background-color: white;
+ color: #000;
+ margin: 0;
+ padding: 0;
+}
+
+div.document {
+ width: {{ page_width }};
+ margin: 30px auto 0 auto;
+}
+
+div.documentwrapper {
+ float: left;
+ width: 100%;
+}
+
+div.bodywrapper {
+ margin: 0 0 0 {{ sidebar_width }};
+}
+
+div.sphinxsidebar {
+ width: {{ sidebar_width }};
+}
+
+hr {
+ border: 1px solid #B1B4B6;
+}
+
+div.body {
+ background-color: #ffffff;
+ color: #3E4349;
+ padding: 0 30px 0 30px;
+}
+
+img.floatingflask {
+ padding: 0 0 10px 10px;
+ float: right;
+}
+
+div.footer {
+ width: {{ page_width }};
+ margin: 20px auto 30px auto;
+ font-size: 14px;
+ color: #888;
+ text-align: right;
+}
+
+div.footer a {
+ color: #888;
+}
+
+div.related {
+ display: none;
+}
+
+div.sphinxsidebar a {
+ color: #444;
+ text-decoration: none;
+ border-bottom: 1px dotted #999;
+}
+
+div.sphinxsidebar a:hover {
+ border-bottom: 1px solid #999;
+}
+
+div.sphinxsidebar {
+ font-size: 14px;
+ line-height: 1.5;
+}
+
+div.sphinxsidebarwrapper {
+ padding: 18px 10px;
+}
+
+div.sphinxsidebarwrapper p.logo {
+ padding: 0 0 20px 0;
+ margin: 0;
+ text-align: center;
+}
+
+div.sphinxsidebar h3,
+div.sphinxsidebar h4 {
+ font-family: 'Garamond', 'Georgia', serif;
+ color: #444;
+ font-size: 24px;
+ font-weight: normal;
+ margin: 0 0 5px 0;
+ padding: 0;
+}
+
+div.sphinxsidebar h4 {
+ font-size: 20px;
+}
+
+div.sphinxsidebar h3 a {
+ color: #444;
+}
+
+div.sphinxsidebar p.logo a,
+div.sphinxsidebar h3 a,
+div.sphinxsidebar p.logo a:hover,
+div.sphinxsidebar h3 a:hover {
+ border: none;
+}
+
+div.sphinxsidebar p {
+ color: #555;
+ margin: 10px 0;
+}
+
+div.sphinxsidebar ul {
+ margin: 10px 0;
+ padding: 0;
+ color: #000;
+}
+
+div.sphinxsidebar input {
+ border: 1px solid #ccc;
+ font-family: 'Georgia', serif;
+ font-size: 1em;
+}
+
+/* -- body styles ----------------------------------------------------------- */
+
+a {
+ color: #004B6B;
+ text-decoration: underline;
+}
+
+a:hover {
+ color: #6D4100;
+ text-decoration: underline;
+}
+
+div.body h1,
+div.body h2,
+div.body h3,
+div.body h4,
+div.body h5,
+div.body h6 {
+ font-family: 'Garamond', 'Georgia', serif;
+ font-weight: normal;
+ margin: 30px 0px 10px 0px;
+ padding: 0;
+}
+
+div.body h1 { margin-top: 0; padding-top: 0; font-size: 240%; }
+div.body h2 { font-size: 180%; }
+div.body h3 { font-size: 150%; }
+div.body h4 { font-size: 130%; }
+div.body h5 { font-size: 100%; }
+div.body h6 { font-size: 100%; }
+
+a.headerlink {
+ color: #ddd;
+ padding: 0 4px;
+ text-decoration: none;
+}
+
+a.headerlink:hover {
+ color: #444;
+ background: #eaeaea;
+}
+
+div.body p, div.body dd, div.body li {
+ line-height: 1.4em;
+}
+
+div.admonition {
+ background: #fafafa;
+ margin: 20px -30px;
+ padding: 10px 30px;
+ border-top: 1px solid #ccc;
+ border-bottom: 1px solid #ccc;
+}
+
+div.admonition tt.xref, div.admonition a tt {
+ border-bottom: 1px solid #fafafa;
+}
+
+dd div.admonition {
+ margin-left: -60px;
+ padding-left: 60px;
+}
+
+div.admonition p.admonition-title {
+ font-family: 'Garamond', 'Georgia', serif;
+ font-weight: normal;
+ font-size: 24px;
+ margin: 0 0 10px 0;
+ padding: 0;
+ line-height: 1;
+}
+
+div.admonition p.last {
+ margin-bottom: 0;
+}
+
+div.highlight {
+ background-color: white;
+}
+
+dt:target, .highlight {
+ background: #FAF3E8;
+}
+
+div.note {
+ background-color: #eee;
+ border: 1px solid #ccc;
+}
+
+div.seealso {
+ background-color: #ffc;
+ border: 1px solid #ff6;
+}
+
+div.topic {
+ background-color: #eee;
+}
+
+p.admonition-title {
+ display: inline;
+}
+
+p.admonition-title:after {
+ content: ":";
+}
+
+pre, tt {
+ font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
+ font-size: 0.9em;
+}
+
+img.screenshot {
+}
+
+tt.descname, tt.descclassname {
+ font-size: 0.95em;
+}
+
+tt.descname {
+ padding-right: 0.08em;
+}
+
+img.screenshot {
+ -moz-box-shadow: 2px 2px 4px #eee;
+ -webkit-box-shadow: 2px 2px 4px #eee;
+ box-shadow: 2px 2px 4px #eee;
+}
+
+table.docutils {
+ border: 1px solid #888;
+ -moz-box-shadow: 2px 2px 4px #eee;
+ -webkit-box-shadow: 2px 2px 4px #eee;
+ box-shadow: 2px 2px 4px #eee;
+}
+
+table.docutils td, table.docutils th {
+ border: 1px solid #888;
+ padding: 0.25em 0.7em;
+}
+
+table.field-list, table.footnote {
+ border: none;
+ -moz-box-shadow: none;
+ -webkit-box-shadow: none;
+ box-shadow: none;
+}
+
+table.footnote {
+ margin: 15px 0;
+ width: 100%;
+ border: 1px solid #eee;
+ background: #fdfdfd;
+ font-size: 0.9em;
+}
+
+table.footnote + table.footnote {
+ margin-top: -15px;
+ border-top: none;
+}
+
+table.field-list th {
+ padding: 0 0.8em 0 0;
+}
+
+table.field-list td {
+ padding: 0;
+}
+
+table.footnote td.label {
+ width: 0px;
+ padding: 0.3em 0 0.3em 0.5em;
+}
+
+table.footnote td {
+ padding: 0.3em 0.5em;
+}
+
+dl {
+ margin: 0;
+ padding: 0;
+}
+
+dl dd {
+ margin-left: 30px;
+}
+
+blockquote {
+ margin: 0 0 0 30px;
+ padding: 0;
+}
+
+ul, ol {
+ margin: 10px 0 10px 30px;
+ padding: 0;
+}
+
+pre {
+ background: #eee;
+ padding: 7px 30px;
+ margin: 15px -30px;
+ line-height: 1.3em;
+}
+
+dl pre, blockquote pre, li pre {
+ margin-left: -60px;
+ padding-left: 60px;
+}
+
+dl dl pre {
+ margin-left: -90px;
+ padding-left: 90px;
+}
+
+tt {
+ background-color: #ecf0f3;
+ color: #222;
+ /* padding: 1px 2px; */
+}
+
+tt.xref, a tt {
+ background-color: #FBFBFB;
+ border-bottom: 1px solid white;
+}
+
+a.reference {
+ text-decoration: none;
+ border-bottom: 1px dotted #004B6B;
+}
+
+a.reference:hover {
+ border-bottom: 1px solid #6D4100;
+}
+
+a.footnote-reference {
+ text-decoration: none;
+ font-size: 0.7em;
+ vertical-align: top;
+ border-bottom: 1px dotted #004B6B;
+}
+
+a.footnote-reference:hover {
+ border-bottom: 1px solid #6D4100;
+}
+
+a:hover tt {
+ background: #EEE;
+}
diff --git a/docs/_themes/flask/static/small_flask.css b/docs/_themes/flask/static/small_flask.css
new file mode 100644
index 0000000..1c6df30
--- /dev/null
+++ b/docs/_themes/flask/static/small_flask.css
@@ -0,0 +1,70 @@
+/*
+ * small_flask.css_t
+ * ~~~~~~~~~~~~~~~~~
+ *
+ * :copyright: Copyright 2010 by Armin Ronacher.
+ * :license: Flask Design License, see LICENSE for details.
+ */
+
+body {
+ margin: 0;
+ padding: 20px 30px;
+}
+
+div.documentwrapper {
+ float: none;
+ background: white;
+}
+
+div.sphinxsidebar {
+ display: block;
+ float: none;
+ width: 102.5%;
+ margin: 50px -30px -20px -30px;
+ padding: 10px 20px;
+ background: #333;
+ color: white;
+}
+
+div.sphinxsidebar h3, div.sphinxsidebar h4, div.sphinxsidebar p,
+div.sphinxsidebar h3 a {
+ color: white;
+}
+
+div.sphinxsidebar a {
+ color: #aaa;
+}
+
+div.sphinxsidebar p.logo {
+ display: none;
+}
+
+div.document {
+ width: 100%;
+ margin: 0;
+}
+
+div.related {
+ display: block;
+ margin: 0;
+ padding: 10px 0 20px 0;
+}
+
+div.related ul,
+div.related ul li {
+ margin: 0;
+ padding: 0;
+}
+
+div.footer {
+ display: none;
+}
+
+div.bodywrapper {
+ margin: 0;
+}
+
+div.body {
+ min-height: 0;
+ padding: 0;
+}
diff --git a/docs/_themes/flask/theme.conf b/docs/_themes/flask/theme.conf
new file mode 100644
index 0000000..307a1f0
--- /dev/null
+++ b/docs/_themes/flask/theme.conf
@@ -0,0 +1,7 @@
+[theme]
+inherit = basic
+stylesheet = flasky.css
+pygments_style = flask_theme_support.FlaskyStyle
+
+[options]
+touch_icon =
diff --git a/docs/_themes/flask_small/layout.html b/docs/_themes/flask_small/layout.html
new file mode 100644
index 0000000..83e5213
--- /dev/null
+++ b/docs/_themes/flask_small/layout.html
@@ -0,0 +1,22 @@
+{% extends "basic/layout.html" %}
+{% block header %}
+ {{ super() }}
+ {% if pagename == 'index' %}
+ <div class=indexwrapper>
+ {% endif %}
+{% endblock %}
+{% block footer %}
+ {% if pagename == 'index' %}
+ </div>
+ {% endif %}
+{% endblock %}
+{# do not display relbars #}
+{% block relbar1 %}{% endblock %}
+{% block relbar2 %}
+ {% if theme_github_fork %}
+ <a href="https://github.com/{{ theme_github_fork }}"><img style="position: fixed; top: 0; right: 0; border: 0;"
+ src="https://s3.amazonaws.com/github/ribbons/forkme_right_darkblue_121621.png" alt="Fork me on GitHub" /></a>
+ {% endif %}
+{% endblock %}
+{% block sidebar1 %}{% endblock %}
+{% block sidebar2 %}{% endblock %}
diff --git a/docs/_themes/flask_small/static/flasky.css_t b/docs/_themes/flask_small/static/flasky.css_t
new file mode 100644
index 0000000..fe2141c
--- /dev/null
+++ b/docs/_themes/flask_small/static/flasky.css_t
@@ -0,0 +1,287 @@
+/*
+ * flasky.css_t
+ * ~~~~~~~~~~~~
+ *
+ * Sphinx stylesheet -- flasky theme based on nature theme.
+ *
+ * :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS.
+ * :license: BSD, see LICENSE for details.
+ *
+ */
+
+@import url("basic.css");
+
+/* -- page layout ----------------------------------------------------------- */
+
+body {
+ font-family: 'Georgia', serif;
+ font-size: 17px;
+ color: #000;
+ background: white;
+ margin: 0;
+ padding: 0;
+}
+
+div.documentwrapper {
+ float: left;
+ width: 100%;
+}
+
+div.bodywrapper {
+ margin: 40px auto 0 auto;
+ width: 700px;
+}
+
+hr {
+ border: 1px solid #B1B4B6;
+}
+
+div.body {
+ background-color: #ffffff;
+ color: #3E4349;
+ padding: 0 30px 30px 30px;
+}
+
+img.floatingflask {
+ padding: 0 0 10px 10px;
+ float: right;
+}
+
+div.footer {
+ text-align: right;
+ color: #888;
+ padding: 10px;
+ font-size: 14px;
+ width: 650px;
+ margin: 0 auto 40px auto;
+}
+
+div.footer a {
+ color: #888;
+ text-decoration: underline;
+}
+
+div.related {
+ line-height: 32px;
+ color: #888;
+}
+
+div.related ul {
+ padding: 0 0 0 10px;
+}
+
+div.related a {
+ color: #444;
+}
+
+/* -- body styles ----------------------------------------------------------- */
+
+a {
+ color: #004B6B;
+ text-decoration: underline;
+}
+
+a:hover {
+ color: #6D4100;
+ text-decoration: underline;
+}
+
+div.body {
+ padding-bottom: 40px; /* saved for footer */
+}
+
+div.body h1,
+div.body h2,
+div.body h3,
+div.body h4,
+div.body h5,
+div.body h6 {
+ font-family: 'Garamond', 'Georgia', serif;
+ font-weight: normal;
+ margin: 30px 0px 10px 0px;
+ padding: 0;
+}
+
+{% if theme_index_logo %}
+div.indexwrapper h1 {
+ text-indent: -999999px;
+ background: url({{ theme_index_logo }}) no-repeat center center;
+ height: {{ theme_index_logo_height }};
+}
+{% endif %}
+
+div.body h2 { font-size: 180%; }
+div.body h3 { font-size: 150%; }
+div.body h4 { font-size: 130%; }
+div.body h5 { font-size: 100%; }
+div.body h6 { font-size: 100%; }
+
+a.headerlink {
+ color: white;
+ padding: 0 4px;
+ text-decoration: none;
+}
+
+a.headerlink:hover {
+ color: #444;
+ background: #eaeaea;
+}
+
+div.body p, div.body dd, div.body li {
+ line-height: 1.4em;
+}
+
+div.admonition {
+ background: #fafafa;
+ margin: 20px -30px;
+ padding: 10px 30px;
+ border-top: 1px solid #ccc;
+ border-bottom: 1px solid #ccc;
+}
+
+div.admonition p.admonition-title {
+ font-family: 'Garamond', 'Georgia', serif;
+ font-weight: normal;
+ font-size: 24px;
+ margin: 0 0 10px 0;
+ padding: 0;
+ line-height: 1;
+}
+
+div.admonition p.last {
+ margin-bottom: 0;
+}
+
+div.highlight{
+ background-color: white;
+}
+
+dt:target, .highlight {
+ background: #FAF3E8;
+}
+
+div.note {
+ background-color: #eee;
+ border: 1px solid #ccc;
+}
+
+div.seealso {
+ background-color: #ffc;
+ border: 1px solid #ff6;
+}
+
+div.topic {
+ background-color: #eee;
+}
+
+div.warning {
+ background-color: #ffe4e4;
+ border: 1px solid #f66;
+}
+
+p.admonition-title {
+ display: inline;
+}
+
+p.admonition-title:after {
+ content: ":";
+}
+
+pre, tt {
+ font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
+ font-size: 0.85em;
+}
+
+img.screenshot {
+}
+
+tt.descname, tt.descclassname {
+ font-size: 0.95em;
+}
+
+tt.descname {
+ padding-right: 0.08em;
+}
+
+img.screenshot {
+ -moz-box-shadow: 2px 2px 4px #eee;
+ -webkit-box-shadow: 2px 2px 4px #eee;
+ box-shadow: 2px 2px 4px #eee;
+}
+
+table.docutils {
+ border: 1px solid #888;
+ -moz-box-shadow: 2px 2px 4px #eee;
+ -webkit-box-shadow: 2px 2px 4px #eee;
+ box-shadow: 2px 2px 4px #eee;
+}
+
+table.docutils td, table.docutils th {
+ border: 1px solid #888;
+ padding: 0.25em 0.7em;
+}
+
+table.field-list, table.footnote {
+ border: none;
+ -moz-box-shadow: none;
+ -webkit-box-shadow: none;
+ box-shadow: none;
+}
+
+table.footnote {
+ margin: 15px 0;
+ width: 100%;
+ border: 1px solid #eee;
+}
+
+table.field-list th {
+ padding: 0 0.8em 0 0;
+}
+
+table.field-list td {
+ padding: 0;
+}
+
+table.footnote td {
+ padding: 0.5em;
+}
+
+dl {
+ margin: 0;
+ padding: 0;
+}
+
+dl dd {
+ margin-left: 30px;
+}
+
+pre {
+ padding: 0;
+ margin: 15px -30px;
+ padding: 8px;
+ line-height: 1.3em;
+ padding: 7px 30px;
+ background: #eee;
+ border-radius: 2px;
+ -moz-border-radius: 2px;
+ -webkit-border-radius: 2px;
+}
+
+dl pre {
+ margin-left: -60px;
+ padding-left: 60px;
+}
+
+tt {
+ background-color: #ecf0f3;
+ color: #222;
+ /* padding: 1px 2px; */
+}
+
+tt.xref, a tt {
+ background-color: #FBFBFB;
+}
+
+a:hover tt {
+ background: #EEE;
+}
diff --git a/docs/_themes/flask_small/theme.conf b/docs/_themes/flask_small/theme.conf
new file mode 100644
index 0000000..542b462
--- /dev/null
+++ b/docs/_themes/flask_small/theme.conf
@@ -0,0 +1,10 @@
+[theme]
+inherit = basic
+stylesheet = flasky.css
+nosidebar = true
+pygments_style = flask_theme_support.FlaskyStyle
+
+[options]
+index_logo = ''
+index_logo_height = 120px
+github_fork = ''
diff --git a/docs/_themes/flask_theme_support.py b/docs/_themes/flask_theme_support.py
new file mode 100644
index 0000000..33f4744
--- /dev/null
+++ b/docs/_themes/flask_theme_support.py
@@ -0,0 +1,86 @@
+# flasky extensions. flasky pygments style based on tango style
+from pygments.style import Style
+from pygments.token import Keyword, Name, Comment, String, Error, \
+ Number, Operator, Generic, Whitespace, Punctuation, Other, Literal
+
+
+class FlaskyStyle(Style):
+ background_color = "#f8f8f8"
+ default_style = ""
+
+ styles = {
+ # No corresponding class for the following:
+ #Text: "", # class: ''
+ Whitespace: "underline #f8f8f8", # class: 'w'
+ Error: "#a40000 border:#ef2929", # class: 'err'
+ Other: "#000000", # class 'x'
+
+ Comment: "italic #8f5902", # class: 'c'
+ Comment.Preproc: "noitalic", # class: 'cp'
+
+ Keyword: "bold #004461", # class: 'k'
+ Keyword.Constant: "bold #004461", # class: 'kc'
+ Keyword.Declaration: "bold #004461", # class: 'kd'
+ Keyword.Namespace: "bold #004461", # class: 'kn'
+ Keyword.Pseudo: "bold #004461", # class: 'kp'
+ Keyword.Reserved: "bold #004461", # class: 'kr'
+ Keyword.Type: "bold #004461", # class: 'kt'
+
+ Operator: "#582800", # class: 'o'
+ Operator.Word: "bold #004461", # class: 'ow' - like keywords
+
+ Punctuation: "bold #000000", # class: 'p'
+
+ # because special names such as Name.Class, Name.Function, etc.
+ # are not recognized as such later in the parsing, we choose them
+ # to look the same as ordinary variables.
+ Name: "#000000", # class: 'n'
+ Name.Attribute: "#c4a000", # class: 'na' - to be revised
+ Name.Builtin: "#004461", # class: 'nb'
+ Name.Builtin.Pseudo: "#3465a4", # class: 'bp'
+ Name.Class: "#000000", # class: 'nc' - to be revised
+ Name.Constant: "#000000", # class: 'no' - to be revised
+ Name.Decorator: "#888", # class: 'nd' - to be revised
+ Name.Entity: "#ce5c00", # class: 'ni'
+ Name.Exception: "bold #cc0000", # class: 'ne'
+ Name.Function: "#000000", # class: 'nf'
+ Name.Property: "#000000", # class: 'py'
+ Name.Label: "#f57900", # class: 'nl'
+ Name.Namespace: "#000000", # class: 'nn' - to be revised
+ Name.Other: "#000000", # class: 'nx'
+ Name.Tag: "bold #004461", # class: 'nt' - like a keyword
+ Name.Variable: "#000000", # class: 'nv' - to be revised
+ Name.Variable.Class: "#000000", # class: 'vc' - to be revised
+ Name.Variable.Global: "#000000", # class: 'vg' - to be revised
+ Name.Variable.Instance: "#000000", # class: 'vi' - to be revised
+
+ Number: "#990000", # class: 'm'
+
+ Literal: "#000000", # class: 'l'
+ Literal.Date: "#000000", # class: 'ld'
+
+ String: "#4e9a06", # class: 's'
+ String.Backtick: "#4e9a06", # class: 'sb'
+ String.Char: "#4e9a06", # class: 'sc'
+ String.Doc: "italic #8f5902", # class: 'sd' - like a comment
+ String.Double: "#4e9a06", # class: 's2'
+ String.Escape: "#4e9a06", # class: 'se'
+ String.Heredoc: "#4e9a06", # class: 'sh'
+ String.Interpol: "#4e9a06", # class: 'si'
+ String.Other: "#4e9a06", # class: 'sx'
+ String.Regex: "#4e9a06", # class: 'sr'
+ String.Single: "#4e9a06", # class: 's1'
+ String.Symbol: "#4e9a06", # class: 'ss'
+
+ Generic: "#000000", # class: 'g'
+ Generic.Deleted: "#a40000", # class: 'gd'
+ Generic.Emph: "italic #000000", # class: 'ge'
+ Generic.Error: "#ef2929", # class: 'gr'
+ Generic.Heading: "bold #000080", # class: 'gh'
+ Generic.Inserted: "#00A000", # class: 'gi'
+ Generic.Output: "#888", # class: 'go'
+ Generic.Prompt: "#745334", # class: 'gp'
+ Generic.Strong: "bold #000000", # class: 'gs'
+ Generic.Subheading: "bold #800080", # class: 'gu'
+ Generic.Traceback: "bold #a40000", # class: 'gt'
+ }
diff --git a/docs/conf.py b/docs/conf.py
new file mode 100644
index 0000000..6e8f4ae
--- /dev/null
+++ b/docs/conf.py
@@ -0,0 +1,232 @@
+# -*- coding: utf-8 -*-
+#
+# Flask-Login documentation build configuration file, created by
+# sphinx-quickstart on Tue Mar 15 18:40:10 2011.
+#
+# This file is execfile()d with the current directory set to its containing dir.
+#
+# Note that not all possible configuration values are present in this
+# autogenerated file.
+#
+# All configuration values have a default; values that are commented out
+# serve to show the default.
+
+import sys, os
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#sys.path.insert(0, os.path.abspath('.'))
+
+sys.path.append(os.path.join(os.path.dirname(__file__), "_themes"))
+
+# -- General configuration -----------------------------------------------------
+
+# If your documentation needs a minimal Sphinx version, state it here.
+#needs_sphinx = '1.0'
+
+# Add any Sphinx extension module names here, as strings. They can be extensions
+# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
+extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'sphinx.ext.viewcode']
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# The suffix of source filenames.
+source_suffix = '.rst'
+
+# The encoding of source files.
+#source_encoding = 'utf-8-sig'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = u'Flask-Login'
+copyright = u'2011, Matthew Frazier'
+
+module_path = os.path.join(os.path.dirname(__file__), '..', 'flask_login')
+module_path = os.path.abspath(module_path)
+
+about = {}
+with open(os.path.join(os.path.dirname(__file__), '..', 'flask_login', '__about__.py')) as f:
+ exec(f.read(), about)
+
+# The version info for the project you're documenting, acts as replacement for
+# |version| and |release|, also used in various other places throughout the
+# built documents.
+#
+# The short X.Y version.
+version = about['__version__']
+# The full version, including alpha/beta/rc tags.
+release = about['__version__']
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#language = None
+
+# There are two options for replacing |today|: either, you set today to some
+# non-false value, then it is used:
+#today = ''
+# Else, today_fmt is used as the format for a strftime call.
+#today_fmt = '%B %d, %Y'
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+exclude_patterns = ['_build']
+
+# The reST default role (used for this markup: `text`) to use for all documents.
+default_role = 'obj'
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+#add_function_parentheses = True
+
+# If true, the current module name will be prepended to all description
+# unit titles (such as .. function::).
+#add_module_names = True
+
+# If true, sectionauthor and moduleauthor directives will be shown in the
+# output. They are ignored by default.
+#show_authors = False
+
+# The name of the Pygments (syntax highlighting) style to use.
+#pygments_style = 'sphinx'
+
+# A list of ignored prefixes for module index sorting.
+#modindex_common_prefix = []
+
+
+# -- Options for HTML output ---------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages. See the documentation for
+# a list of builtin themes.
+html_theme = 'flask_small'
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further. For a list of options available for each theme, see the
+# documentation.
+html_theme_options = dict(github_fork='maxcountryman/flask-login', index_logo=False)
+
+# Add any paths that contain custom themes here, relative to this directory.
+html_theme_path = ['_themes']
+
+# The name for this set of Sphinx documents. If None, it defaults to
+# "<project> v<release> documentation".
+#html_title = None
+
+# A shorter title for the navigation bar. Default is the same as html_title.
+#html_short_title = None
+
+# The name of an image file (relative to this directory) to place at the top
+# of the sidebar.
+#html_logo = None
+
+# The name of an image file (within the static path) to use as favicon of the
+# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
+# pixels large.
+#html_favicon = None
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ['_static']
+
+# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
+# using the given strftime format.
+#html_last_updated_fmt = '%b %d, %Y'
+
+# If true, SmartyPants will be used to convert quotes and dashes to
+# typographically correct entities.
+#html_use_smartypants = True
+
+# Custom sidebar templates, maps document names to template names.
+#html_sidebars = {}
+
+# Additional templates that should be rendered to pages, maps page names to
+# template names.
+#html_additional_pages = {}
+
+# If false, no module index is generated.
+#html_domain_indices = True
+
+# If false, no index is generated.
+#html_use_index = True
+
+# If true, the index is split into individual pages for each letter.
+#html_split_index = False
+
+# If true, links to the reST sources are added to the pages.
+#html_show_sourcelink = True
+
+# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
+#html_show_sphinx = True
+
+# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
+#html_show_copyright = True
+
+# If true, an OpenSearch description file will be output, and all pages will
+# contain a <link> tag referring to it. The value of this option must be the
+# base URL from which the finished HTML is served.
+#html_use_opensearch = ''
+
+# This is the file name suffix for HTML files (e.g. ".xhtml").
+#html_file_suffix = None
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'Flask-Logindoc'
+
+
+# -- Options for LaTeX output --------------------------------------------------
+
+# The paper size ('letter' or 'a4').
+#latex_paper_size = 'letter'
+
+# The font size ('10pt', '11pt' or '12pt').
+#latex_font_size = '10pt'
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title, author, documentclass [howto/manual]).
+latex_documents = [
+ ('index', 'Flask-Login.tex', u'Flask-Login Documentation',
+ u'Matthew Frazier', 'manual'),
+]
+
+# The name of an image file (relative to this directory) to place at the top of
+# the title page.
+#latex_logo = None
+
+# For "manual" documents, if this is true, then toplevel headings are parts,
+# not chapters.
+#latex_use_parts = False
+
+# If true, show page references after internal links.
+#latex_show_pagerefs = False
+
+# If true, show URL addresses after external links.
+#latex_show_urls = False
+
+# Additional stuff for the LaTeX preamble.
+#latex_preamble = ''
+
+# Documents to append as an appendix to all manuals.
+#latex_appendices = []
+
+# If false, no module index is generated.
+#latex_domain_indices = True
+
+
+# -- Options for manual page output --------------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [
+ ('index', 'flask-login', u'Flask-Login Documentation',
+ [u'Matthew Frazier'], 1)
+]
+
+
+# Example configuration for intersphinx: refer to the Python standard library.
+intersphinx_mapping = {'https://docs.python.org/3': None,
+ 'http://flask.pocoo.org/docs/': None}
+
+auto_content = 'both'
diff --git a/docs/index.rst b/docs/index.rst
new file mode 100644
index 0000000..b254973
--- /dev/null
+++ b/docs/index.rst
@@ -0,0 +1,546 @@
+===========
+Flask-Login
+===========
+.. currentmodule:: flask_login
+
+Flask-Login provides user session management for Flask. It handles the common
+tasks of logging in, logging out, and remembering your users' sessions over
+extended periods of time.
+
+It will:
+
+- Store the active user's ID in the session, and let you log them in and out
+ easily.
+- Let you restrict views to logged-in (or logged-out) users.
+- Handle the normally-tricky "remember me" functionality.
+- Help protect your users' sessions from being stolen by cookie thieves.
+- Possibly integrate with Flask-Principal or other authorization extensions
+ later on.
+
+However, it does not:
+
+- Impose a particular database or other storage method on you. You are
+ entirely in charge of how the user is loaded.
+- Restrict you to using usernames and passwords, OpenIDs, or any other method
+ of authenticating.
+- Handle permissions beyond "logged in or not."
+- Handle user registration or account recovery.
+
+.. contents::
+ :local:
+ :backlinks: none
+
+
+Configuring your Application
+============================
+The most important part of an application that uses Flask-Login is the
+`LoginManager` class. You should create one for your application somewhere in
+your code, like this::
+
+ login_manager = LoginManager()
+
+The login manager contains the code that lets your application and Flask-Login
+work together, such as how to load a user from an ID, where to send users when
+they need to log in, and the like.
+
+Once the actual application object has been created, you can configure it for
+login with::
+
+ login_manager.init_app(app)
+
+
+How it Works
+============
+You will need to provide a `~LoginManager.user_loader` callback. This callback
+is used to reload the user object from the user ID stored in the session. It
+should take the `unicode` ID of a user, and return the corresponding user
+object. For example::
+
+ @login_manager.user_loader
+ def load_user(user_id):
+ return User.get(user_id)
+
+It should return `None` (**not raise an exception**) if the ID is not valid.
+(In that case, the ID will manually be removed from the session and processing
+will continue.)
+
+Your User Class
+===============
+The class that you use to represent users needs to implement these properties
+and methods:
+
+`is_authenticated`
+ This property should return `True` if the user is authenticated, i.e. they
+ have provided valid credentials. (Only authenticated users will fulfill
+ the criteria of `login_required`.)
+
+`is_active`
+ This property should return `True` if this is an active user - in addition
+ to being authenticated, they also have activated their account, not been
+ suspended, or any condition your application has for rejecting an account.
+ Inactive accounts may not log in (without being forced of course).
+
+`is_anonymous`
+ This property should return `True` if this is an anonymous user. (Actual
+ users should return `False` instead.)
+
+`get_id()`
+ This method must return a `unicode` that uniquely identifies this user,
+ and can be used to load the user from the `~LoginManager.user_loader`
+ callback. Note that this **must** be a `unicode` - if the ID is natively
+ an `int` or some other type, you will need to convert it to `unicode`.
+
+To make implementing a user class easier, you can inherit from `UserMixin`,
+which provides default implementations for all of these properties and methods.
+(It's not required, though.)
+
+Login Example
+=============
+
+Once a user has authenticated, you log them in with the `login_user`
+function.
+
+ For example:
+
+.. code-block:: python
+
+ @app.route('/login', methods=['GET', 'POST'])
+ def login():
+ # Here we use a class of some kind to represent and validate our
+ # client-side form data. For example, WTForms is a library that will
+ # handle this for us, and we use a custom LoginForm to validate.
+ form = LoginForm()
+ if form.validate_on_submit():
+ # Login and validate the user.
+ # user should be an instance of your `User` class
+ login_user(user)
+
+ flask.flash('Logged in successfully.')
+
+ next = flask.request.args.get('next')
+ # next_is_valid should check if the user has valid
+ # permission to access the `next` url
+ if not next_is_valid(next):
+ return flask.abort(400)
+
+ return flask.redirect(next or flask.url_for('index'))
+ return flask.render_template('login.html', form=form)
+
+*Warning:* You MUST validate the value of the `next` parameter. If you do not,
+your application will be vulnerable to open redirects.
+
+It's that simple. You can then access the logged-in user with the
+`current_user` proxy, which is available in every template::
+
+ {% if current_user.is_authenticated %}
+ Hi {{ current_user.name }}!
+ {% endif %}
+
+Views that require your users to be logged in can be
+decorated with the `login_required` decorator::
+
+ @app.route("/settings")
+ @login_required
+ def settings():
+ pass
+
+When the user is ready to log out::
+
+ @app.route("/logout")
+ @login_required
+ def logout():
+ logout_user()
+ return redirect(somewhere)
+
+They will be logged out, and any cookies for their session will be cleaned up.
+
+
+
+Customizing the Login Process
+=============================
+By default, when a user attempts to access a `login_required` view without
+being logged in, Flask-Login will flash a message and redirect them to the
+log in view. (If the login view is not set, it will abort with a 401 error.)
+
+The name of the log in view can be set as `LoginManager.login_view`.
+For example::
+
+ login_manager.login_view = "users.login"
+
+The default message flashed is ``Please log in to access this page.`` To
+customize the message, set `LoginManager.login_message`::
+
+ login_manager.login_message = u"Bonvolu ensaluti por uzi tiun paĝon."
+
+To customize the message category, set `LoginManager.login_message_category`::
+
+ login_manager.login_message_category = "info"
+
+When the log in view is redirected to, it will have a ``next`` variable in the
+query string, which is the page that the user was trying to access.
+
+If you would like to customize the process further, decorate a function with
+`LoginManager.unauthorized_handler`::
+
+ @login_manager.unauthorized_handler
+ def unauthorized():
+ # do stuff
+ return a_response
+
+
+Login using Authorization header
+================================
+
+.. Caution::
+ This method will be deprecated; use the `~LoginManager.request_loader`
+ below instead.
+
+Sometimes you want to support Basic Auth login using the `Authorization`
+header, such as for api requests. To support login via header you will need
+to provide a `~LoginManager.header_loader` callback. This callback should behave
+the same as your `~LoginManager.user_loader` callback, except that it accepts
+a header value instead of a user id. For example::
+
+ @login_manager.header_loader
+ def load_user_from_header(header_val):
+ header_val = header_val.replace('Basic ', '', 1)
+ try:
+ header_val = base64.b64decode(header_val)
+ except TypeError:
+ pass
+ return User.query.filter_by(api_key=header_val).first()
+
+By default the `Authorization` header's value is passed to your
+`~LoginManager.header_loader` callback. You can change the header used with
+the `AUTH_HEADER_NAME` configuration.
+
+
+Custom Login using Request Loader
+=================================
+Sometimes you want to login users without using cookies, such as using header
+values or an api key passed as a query argument. In these cases, you should use
+the `~LoginManager.request_loader` callback. This callback should behave the
+same as your `~LoginManager.user_loader` callback, except that it accepts the
+Flask request instead of a user_id.
+
+For example, to support login from both a url argument and from Basic Auth
+using the `Authorization` header::
+
+ @login_manager.request_loader
+ def load_user_from_request(request):
+
+ # first, try to login using the api_key url arg
+ api_key = request.args.get('api_key')
+ if api_key:
+ user = User.query.filter_by(api_key=api_key).first()
+ if user:
+ return user
+
+ # next, try to login using Basic Auth
+ api_key = request.headers.get('Authorization')
+ if api_key:
+ api_key = api_key.replace('Basic ', '', 1)
+ try:
+ api_key = base64.b64decode(api_key)
+ except TypeError:
+ pass
+ user = User.query.filter_by(api_key=api_key).first()
+ if user:
+ return user
+
+ # finally, return None if both methods did not login the user
+ return None
+
+
+Anonymous Users
+===============
+By default, when a user is not actually logged in, `current_user` is set to
+an `AnonymousUserMixin` object. It has the following properties and methods:
+
+- `is_active` and `is_authenticated` are `False`
+- `is_anonymous` is `True`
+- `get_id()` returns `None`
+
+If you have custom requirements for anonymous users (for example, they need
+to have a permissions field), you can provide a callable (either a class or
+factory function) that creates anonymous users to the `LoginManager` with::
+
+ login_manager.anonymous_user = MyAnonymousUser
+
+
+Remember Me
+===========
+"Remember Me" functionality can be tricky to implement. However, Flask-Login
+makes it nearly transparent - just pass ``remember=True`` to the `login_user`
+call. A cookie will be saved on the user's computer, and then Flask-Login
+will automatically restore the user ID from that cookie if it is not in the
+session. The cookie is tamper-proof, so if the user tampers with it (i.e.
+inserts someone else's user ID in place of their own), the cookie will merely
+be rejected, as if it was not there.
+
+That level of functionality is handled automatically. However, you can (and
+should, if your application handles any kind of sensitive data) provide
+additional infrastructure to increase the security of your remember cookies.
+
+
+Alternative Tokens
+------------------
+Using the user ID as the value of the remember token means you must change the
+user's ID to invalidate their login sessions. One way to improve this is to use
+an alternative session token instead of the user's ID. For example::
+
+ @login_manager.user_loader
+ def load_user(session_token):
+ return User.query.filter_by(session_token=session_token).first()
+
+Then the `~UserMixin.get_id` method of your User class would return the session
+token instead of the user's ID::
+
+ def get_id(self):
+ return unicode(self.session_token)
+
+This way you are free to change the user's session token to a new randomly
+generated value when the user changes their password, which would ensure their
+old authentication sessions will cease to be valid. Note that the session
+token must still uniquely identify the user... think of it as a second user ID.
+
+
+Fresh Logins
+------------
+When a user logs in, their session is marked as "fresh," which indicates that
+they actually authenticated on that session. When their session is destroyed
+and they are logged back in with a "remember me" cookie, it is marked as
+"non-fresh." `login_required` does not differentiate between freshness, which
+is fine for most pages. However, sensitive actions like changing one's
+personal information should require a fresh login. (Actions like changing
+one's password should always require a password re-entry regardless.)
+
+`fresh_login_required`, in addition to verifying that the user is logged
+in, will also ensure that their login is fresh. If not, it will send them to
+a page where they can re-enter their credentials. You can customize its
+behavior in the same ways as you can customize `login_required`, by setting
+`LoginManager.refresh_view`, `~LoginManager.needs_refresh_message`, and
+`~LoginManager.needs_refresh_message_category`::
+
+ login_manager.refresh_view = "accounts.reauthenticate"
+ login_manager.needs_refresh_message = (
+ u"To protect your account, please reauthenticate to access this page."
+ )
+ login_manager.needs_refresh_message_category = "info"
+
+Or by providing your own callback to handle refreshing::
+
+ @login_manager.needs_refresh_handler
+ def refresh():
+ # do stuff
+ return a_response
+
+To mark a session as fresh again, call the `confirm_login` function.
+
+
+Cookie Settings
+---------------
+The details of the cookie can be customized in the application settings.
+
+=========================== =================================================
+`REMEMBER_COOKIE_NAME` The name of the cookie to store the "remember me"
+ information in. **Default:** ``remember_token``
+`REMEMBER_COOKIE_DURATION` The amount of time before the cookie expires, as
+ a `datetime.timedelta` object.
+ **Default:** 365 days (1 non-leap Gregorian year)
+`REMEMBER_COOKIE_DOMAIN` If the "Remember Me" cookie should cross domains,
+ set the domain value here (i.e. ``.example.com``
+ would allow the cookie to be used on all
+ subdomains of ``example.com``).
+ **Default:** `None`
+`REMEMBER_COOKIE_PATH` Limits the "Remember Me" cookie to a certain path.
+ **Default:** ``/``
+`REMEMBER_COOKIE_SECURE` Restricts the "Remember Me" cookie's scope to
+ secure channels (typically HTTPS).
+ **Default:** `None`
+`REMEMBER_COOKIE_HTTPONLY` Prevents the "Remember Me" cookie from being
+ accessed by client-side scripts.
+ **Default:** `False`
+=========================== =================================================
+
+
+Session Protection
+==================
+While the features above help secure your "Remember Me" token from cookie
+thieves, the session cookie is still vulnerable. Flask-Login includes session
+protection to help prevent your users' sessions from being stolen.
+
+You can configure session protection on the `LoginManager`, and in the app's
+configuration. If it is enabled, it can operate in either `basic` or `strong`
+mode. To set it on the `LoginManager`, set the
+`~LoginManager.session_protection` attribute to ``"basic"`` or ``"strong"``::
+
+ login_manager.session_protection = "strong"
+
+Or, to disable it::
+
+ login_manager.session_protection = None
+
+By default, it is activated in ``"basic"`` mode. It can be disabled in the
+app's configuration by setting the `SESSION_PROTECTION` setting to `None`,
+``"basic"``, or ``"strong"``.
+
+When session protection is active, each request, it generates an identifier
+for the user's computer (basically, a secure hash of the IP address and user
+agent). If the session does not have an associated identifier, the one
+generated will be stored. If it has an identifier, and it matches the one
+generated, then the request is OK.
+
+If the identifiers do not match in `basic` mode, or when the session is
+permanent, then the session will simply be marked as non-fresh, and anything
+requiring a fresh login will force the user to re-authenticate. (Of course,
+you must be already using fresh logins where appropriate for this to have an
+effect.)
+
+If the identifiers do not match in `strong` mode for a non-permanent session,
+then the entire session (as well as the remember token if it exists) is
+deleted.
+
+
+Localization
+============
+By default, the `LoginManager` uses ``flash`` to display messages when a user
+is required to log in. These messages are in English. If you require
+localization, set the `localize_callback` attribute of `LoginManager` to a
+function to be called with these messages before they're sent to ``flash``,
+e.g. ``gettext``. This function will be called with the message and its return
+value will be sent to ``flash`` instead.
+
+
+API Documentation
+=================
+This documentation is automatically generated from Flask-Login's source code.
+
+
+Configuring Login
+-----------------
+
+.. module:: flask_login
+
+.. autoclass:: LoginManager
+
+ .. automethod:: setup_app
+
+ .. automethod:: unauthorized
+
+ .. automethod:: needs_refresh
+
+ .. rubric:: General Configuration
+
+ .. automethod:: user_loader
+
+ .. automethod:: header_loader
+
+ .. attribute:: anonymous_user
+
+ A class or factory function that produces an anonymous user, which
+ is used when no one is logged in.
+
+ .. rubric:: `unauthorized` Configuration
+
+ .. attribute:: login_view
+
+ The name of the view to redirect to when the user needs to log in. (This
+ can be an absolute URL as well, if your authentication machinery is
+ external to your application.)
+
+ .. attribute:: login_message
+
+ The message to flash when a user is redirected to the login page.
+
+ .. automethod:: unauthorized_handler
+
+ .. rubric:: `needs_refresh` Configuration
+
+ .. attribute:: refresh_view
+
+ The name of the view to redirect to when the user needs to
+ reauthenticate.
+
+ .. attribute:: needs_refresh_message
+
+ The message to flash when a user is redirected to the reauthentication
+ page.
+
+ .. automethod:: needs_refresh_handler
+
+
+Login Mechanisms
+----------------
+.. data:: current_user
+
+ A proxy for the current user.
+
+.. autofunction:: login_fresh
+
+.. autofunction:: login_user
+
+.. autofunction:: logout_user
+
+.. autofunction:: confirm_login
+
+
+Protecting Views
+----------------
+.. autofunction:: login_required
+
+.. autofunction:: fresh_login_required
+
+
+User Object Helpers
+-------------------
+.. autoclass:: UserMixin
+ :members:
+
+.. autoclass:: AnonymousUserMixin
+ :members:
+
+
+Utilities
+---------
+.. autofunction:: login_url
+
+
+Signals
+-------
+See the `Flask documentation on signals`_ for information on how to use these
+signals in your code.
+
+.. data:: user_logged_in
+
+ Sent when a user is logged in. In addition to the app (which is the
+ sender), it is passed `user`, which is the user being logged in.
+
+.. data:: user_logged_out
+
+ Sent when a user is logged out. In addition to the app (which is the
+ sender), it is passed `user`, which is the user being logged out.
+
+.. data:: user_login_confirmed
+
+ Sent when a user's login is confirmed, marking it as fresh. (It is not
+ called for a normal login.)
+ It receives no additional arguments besides the app.
+
+.. data:: user_unauthorized
+
+ Sent when the `unauthorized` method is called on a `LoginManager`. It
+ receives no additional arguments besides the app.
+
+.. data:: user_needs_refresh
+
+ Sent when the `needs_refresh` method is called on a `LoginManager`. It
+ receives no additional arguments besides the app.
+
+.. data:: session_protected
+
+ Sent whenever session protection takes effect, and a session is either
+ marked non-fresh or deleted. It receives no additional arguments besides
+ the app.
+
+.. _Flask documentation on signals: http://flask.pocoo.org/docs/signals/
diff --git a/docs/make.bat b/docs/make.bat
new file mode 100644
index 0000000..f4a9bf2
--- /dev/null
+++ b/docs/make.bat
@@ -0,0 +1,170 @@
+@ECHO OFF
+
+REM Command file for Sphinx documentation
+
+if "%SPHINXBUILD%" == "" (
+ set SPHINXBUILD=sphinx-build
+)
+set BUILDDIR=_build
+set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
+if NOT "%PAPER%" == "" (
+ set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
+)
+
+if "%1" == "" goto help
+
+if "%1" == "help" (
+ :help
+ echo.Please use `make ^<target^>` where ^<target^> is one of
+ echo. html to make standalone HTML files
+ echo. dirhtml to make HTML files named index.html in directories
+ echo. singlehtml to make a single large HTML file
+ echo. pickle to make pickle files
+ echo. json to make JSON files
+ echo. htmlhelp to make HTML files and a HTML help project
+ echo. qthelp to make HTML files and a qthelp project
+ echo. devhelp to make HTML files and a Devhelp project
+ echo. epub to make an epub
+ echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
+ echo. text to make text files
+ echo. man to make manual pages
+ echo. changes to make an overview over all changed/added/deprecated items
+ echo. linkcheck to check all external links for integrity
+ echo. doctest to run all doctests embedded in the documentation if enabled
+ goto end
+)
+
+if "%1" == "clean" (
+ for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
+ del /q /s %BUILDDIR%\*
+ goto end
+)
+
+if "%1" == "html" (
+ %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The HTML pages are in %BUILDDIR%/html.
+ goto end
+)
+
+if "%1" == "dirhtml" (
+ %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
+ goto end
+)
+
+if "%1" == "singlehtml" (
+ %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
+ goto end
+)
+
+if "%1" == "pickle" (
+ %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can process the pickle files.
+ goto end
+)
+
+if "%1" == "json" (
+ %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can process the JSON files.
+ goto end
+)
+
+if "%1" == "htmlhelp" (
+ %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can run HTML Help Workshop with the ^
+.hhp project file in %BUILDDIR%/htmlhelp.
+ goto end
+)
+
+if "%1" == "qthelp" (
+ %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can run "qcollectiongenerator" with the ^
+.qhcp project file in %BUILDDIR%/qthelp, like this:
+ echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Flask-Login.qhcp
+ echo.To view the help file:
+ echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Flask-Login.ghc
+ goto end
+)
+
+if "%1" == "devhelp" (
+ %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished.
+ goto end
+)
+
+if "%1" == "epub" (
+ %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The epub file is in %BUILDDIR%/epub.
+ goto end
+)
+
+if "%1" == "latex" (
+ %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
+ goto end
+)
+
+if "%1" == "text" (
+ %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The text files are in %BUILDDIR%/text.
+ goto end
+)
+
+if "%1" == "man" (
+ %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The manual pages are in %BUILDDIR%/man.
+ goto end
+)
+
+if "%1" == "changes" (
+ %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.The overview file is in %BUILDDIR%/changes.
+ goto end
+)
+
+if "%1" == "linkcheck" (
+ %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Link check complete; look for any errors in the above output ^
+or in %BUILDDIR%/linkcheck/output.txt.
+ goto end
+)
+
+if "%1" == "doctest" (
+ %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Testing of doctests in the sources finished, look at the ^
+results in %BUILDDIR%/doctest/output.txt.
+ goto end
+)
+
+:end
diff --git a/flask_login/__about__.py b/flask_login/__about__.py
new file mode 100644
index 0000000..ea25e2e
--- /dev/null
+++ b/flask_login/__about__.py
@@ -0,0 +1,10 @@
+__title__ = 'Flask-Login'
+__description__ = 'User session management for Flask'
+__url__ = 'https://github.com/maxcountryman/flask-login'
+__version_info__ = ('0', '4', '0')
+__version__ = '.'.join(__version_info__)
+__author__ = 'Matthew Frazier'
+__author_email__ = 'leafstormrush@gmail.com'
+__maintainer__ = 'Max Countryman'
+__license__ = 'MIT'
+__copyright__ = '(c) 2011 by Matthew Frazier'
diff --git a/flask_login/__init__.py b/flask_login/__init__.py
new file mode 100644
index 0000000..e311e09
--- /dev/null
+++ b/flask_login/__init__.py
@@ -0,0 +1,63 @@
+# -*- coding: utf-8 -*-
+'''
+ flask_login
+ -----------
+ This module provides user session management for Flask. It lets you log
+ your users in and out in a database-independent manner.
+ :copyright: (c) 2011 by Matthew Frazier.
+ :license: MIT/X11, see LICENSE for more details.
+'''
+
+from .config import (COOKIE_NAME, COOKIE_DURATION, COOKIE_SECURE,
+ COOKIE_HTTPONLY, LOGIN_MESSAGE, LOGIN_MESSAGE_CATEGORY,
+ REFRESH_MESSAGE, REFRESH_MESSAGE_CATEGORY, ID_ATTRIBUTE,
+ AUTH_HEADER_NAME)
+from .login_manager import LoginManager
+from .mixins import UserMixin, AnonymousUserMixin
+from .signals import (user_logged_in, user_logged_out, user_loaded_from_cookie,
+ user_loaded_from_header, user_loaded_from_request,
+ user_login_confirmed, user_unauthorized,
+ user_needs_refresh, user_accessed, session_protected)
+from .utils import (current_user, login_url, login_fresh, login_user,
+ logout_user, confirm_login, login_required,
+ fresh_login_required, set_login_view, encode_cookie,
+ decode_cookie, make_next_param)
+
+
+__all__ = [
+ LoginManager.__name__,
+ UserMixin.__name__,
+ AnonymousUserMixin.__name__,
+ 'COOKIE_NAME',
+ 'COOKIE_DURATION',
+ 'COOKIE_SECURE',
+ 'COOKIE_HTTPONLY',
+ 'LOGIN_MESSAGE',
+ 'LOGIN_MESSAGE_CATEGORY',
+ 'REFRESH_MESSAGE',
+ 'REFRESH_MESSAGE_CATEGORY',
+ 'ID_ATTRIBUTE',
+ 'AUTH_HEADER_NAME',
+ 'user_logged_in',
+ 'user_logged_out',
+ 'user_loaded_from_cookie',
+ 'user_loaded_from_header',
+ 'user_loaded_from_request',
+ 'user_login_confirmed',
+ 'user_unauthorized',
+ 'user_needs_refresh',
+ 'user_accessed',
+ 'session_protected',
+ 'current_user',
+ 'login_url',
+ 'login_fresh',
+ 'login_user',
+ 'logout_user',
+ 'confirm_login',
+ 'login_required',
+ 'fresh_login_required',
+ 'set_login_view',
+ 'encode_cookie',
+ 'decode_cookie',
+ 'make_next_param',
+]
diff --git a/flask_login/_compat.py b/flask_login/_compat.py
new file mode 100644
index 0000000..2f3293a
--- /dev/null
+++ b/flask_login/_compat.py
@@ -0,0 +1,52 @@
+# -*- coding: utf-8 -*-
+'''
+ flask_login._compat
+ -------------------
+ A module providing tools for cross-version compatibility.
+'''
+
+
+import sys
+
+
+PY2 = sys.version_info[0] == 2
+
+
+if not PY2: # pragma: no cover
+ unicode = str # needed for pyflakes in py3
+
+
+if PY2: # pragma: nocover
+
+ from urlparse import urlparse, urlunparse
+
+ def iteritems(d):
+ return d.iteritems()
+
+ def itervalues(d):
+ return d.itervalues()
+
+ text_type = unicode
+
+else: # pragma: nocover
+
+ from urllib.parse import urlparse, urlunparse
+
+ def iteritems(d):
+ return iter(d.items())
+
+ def itervalues(d):
+ return iter(d.values())
+
+ text_type = str
+
+
+__all__ = [
+ 'PY2',
+ 'unicode',
+ 'urlparse',
+ 'urlunparse',
+ 'iteritems',
+ 'itervalues',
+ 'text_type',
+]
diff --git a/flask_login/config.py b/flask_login/config.py
new file mode 100644
index 0000000..836c5fb
--- /dev/null
+++ b/flask_login/config.py
@@ -0,0 +1,49 @@
+# -*- coding: utf-8 -*-
+'''
+ flask_login.config
+ ------------------
+ This module provides default configuration values.
+'''
+
+
+from datetime import timedelta
+
+
+#: The default name of the "remember me" cookie (``remember_token``)
+COOKIE_NAME = 'remember_token'
+
+#: The default time before the "remember me" cookie expires (365 days).
+COOKIE_DURATION = timedelta(days=365)
+
+#: Whether the "remember me" cookie requires Secure; defaults to ``None``
+COOKIE_SECURE = None
+
+#: Whether the "remember me" cookie uses HttpOnly or not; defaults to ``False``
+COOKIE_HTTPONLY = False
+
+#: The default flash message to display when users need to log in.
+LOGIN_MESSAGE = u'Please log in to access this page.'
+
+#: The default flash message category to display when users need to log in.
+LOGIN_MESSAGE_CATEGORY = 'message'
+
+#: The default flash message to display when users need to reauthenticate.
+REFRESH_MESSAGE = u'Please reauthenticate to access this page.'
+
+#: The default flash message category to display when users need to
+#: reauthenticate.
+REFRESH_MESSAGE_CATEGORY = 'message'
+
+#: The default attribute to retreive the unicode id of the user
+ID_ATTRIBUTE = 'get_id'
+
+#: Default name of the auth header (``Authorization``)
+AUTH_HEADER_NAME = 'Authorization'
+
+#: A set of session keys that are populated by Flask-Login. Use this set to
+#: purge keys safely and accurately.
+SESSION_KEYS = set(['user_id', 'remember', '_id', '_fresh'])
+
+#: A set of HTTP methods which are exempt from `login_required` and
+#: `fresh_login_required`. By default, this is just ``OPTIONS``.
+EXEMPT_METHODS = set(['OPTIONS'])
diff --git a/flask_login/login_manager.py b/flask_login/login_manager.py
new file mode 100644
index 0000000..2786c91
--- /dev/null
+++ b/flask_login/login_manager.py
@@ -0,0 +1,425 @@
+# -*- coding: utf-8 -*-
+'''
+ flask_login.login_manager
+ -------------------------
+ The LoginManager class.
+'''
+
+
+import warnings
+from datetime import datetime
+
+from flask import (_request_ctx_stack, abort, current_app, flash, redirect,
+ request, session)
+
+from ._compat import text_type
+from .config import (COOKIE_NAME, COOKIE_DURATION, COOKIE_SECURE,
+ COOKIE_HTTPONLY, LOGIN_MESSAGE, LOGIN_MESSAGE_CATEGORY,
+ REFRESH_MESSAGE, REFRESH_MESSAGE_CATEGORY, ID_ATTRIBUTE,
+ AUTH_HEADER_NAME, SESSION_KEYS)
+from .mixins import AnonymousUserMixin
+from .signals import (user_loaded_from_cookie, user_loaded_from_header,
+ user_loaded_from_request, user_unauthorized,
+ user_needs_refresh, user_accessed, session_protected)
+from .utils import (_get_user, login_url, _create_identifier,
+ _user_context_processor, encode_cookie, decode_cookie)
+
+
+class LoginManager(object):
+ '''
+ This object is used to hold the settings used for logging in. Instances of
+ :class:`LoginManager` are *not* bound to specific apps, so you can create
+ one in the main body of your code and then bind it to your
+ app in a factory function.
+ '''
+ def __init__(self, app=None, add_context_processor=True):
+ #: A class or factory function that produces an anonymous user, which
+ #: is used when no one is logged in.
+ self.anonymous_user = AnonymousUserMixin
+
+ #: The name of the view to redirect to when the user needs to log in.
+ #: (This can be an absolute URL as well, if your authentication
+ #: machinery is external to your application.)
+ self.login_view = None
+
+ #: Names of views to redirect to when the user needs to log in,
+ #: per blueprint. If the key value is set to None the value of
+ #: :attr:`login_view` will be used instead.
+ self.blueprint_login_views = {}
+
+ #: The message to flash when a user is redirected to the login page.
+ self.login_message = LOGIN_MESSAGE
+
+ #: The message category to flash when a user is redirected to the login
+ #: page.
+ self.login_message_category = LOGIN_MESSAGE_CATEGORY
+
+ #: The name of the view to redirect to when the user needs to
+ #: reauthenticate.
+ self.refresh_view = None
+
+ #: The message to flash when a user is redirected to the 'needs
+ #: refresh' page.
+ self.needs_refresh_message = REFRESH_MESSAGE
+
+ #: The message category to flash when a user is redirected to the
+ #: 'needs refresh' page.
+ self.needs_refresh_message_category = REFRESH_MESSAGE_CATEGORY
+
+ #: The mode to use session protection in. This can be either
+ #: ``'basic'`` (the default) or ``'strong'``, or ``None`` to disable
+ #: it.
+ self.session_protection = 'basic'
+
+ #: If present, used to translate flash messages ``self.login_message``
+ #: and ``self.needs_refresh_message``
+ self.localize_callback = None
+
+ self.user_callback = None
+
+ self.unauthorized_callback = None
+
+ self.needs_refresh_callback = None
+
+ self.id_attribute = ID_ATTRIBUTE
+
+ self.header_callback = None
+
+ self.request_callback = None
+
+ if app is not None:
+ self.init_app(app, add_context_processor)
+
+ def setup_app(self, app, add_context_processor=True): # pragma: no cover
+ '''
+ This method has been deprecated. Please use
+ :meth:`LoginManager.init_app` instead.
+ '''
+ warnings.warn('Warning setup_app is deprecated. Please use init_app.',
+ DeprecationWarning)
+ self.init_app(app, add_context_processor)
+
+ def init_app(self, app, add_context_processor=True):
+ '''
+ Configures an application. This registers an `after_request` call, and
+ attaches this `LoginManager` to it as `app.login_manager`.
+
+ :param app: The :class:`flask.Flask` object to configure.
+ :type app: :class:`flask.Flask`
+ :param add_context_processor: Whether to add a context processor to
+ the app that adds a `current_user` variable to the template.
+ Defaults to ``True``.
+ :type add_context_processor: bool
+ '''
+ app.login_manager = self
+ app.after_request(self._update_remember_cookie)
+
+ self._login_disabled = app.config.get('LOGIN_DISABLED', False)
+
+ if add_context_processor:
+ app.context_processor(_user_context_processor)
+
+ def unauthorized(self):
+ '''
+ This is called when the user is required to log in. If you register a
+ callback with :meth:`LoginManager.unauthorized_handler`, then it will
+ be called. Otherwise, it will take the following actions:
+
+ - Flash :attr:`LoginManager.login_message` to the user.
+
+ - If the app is using blueprints find the login view for
+ the current blueprint using `blueprint_login_views`. If the app
+ is not using blueprints or the login view for the current
+ blueprint is not specified use the value of `login_view`.
+ Redirect the user to the login view. (The page they were
+ attempting to access will be passed in the ``next`` query
+ string variable, so you can redirect there if present instead
+ of the homepage.)
+
+ If :attr:`LoginManager.login_view` is not defined, then it will simply
+ raise a HTTP 401 (Unauthorized) error instead.
+
+ This should be returned from a view or before/after_request function,
+ otherwise the redirect will have no effect.
+ '''
+ user_unauthorized.send(current_app._get_current_object())
+
+ if self.unauthorized_callback:
+ return self.unauthorized_callback()
+
+ if request.blueprint in self.blueprint_login_views:
+ login_view = self.blueprint_login_views[request.blueprint]
+ else:
+ login_view = self.login_view
+
+ if not login_view:
+ abort(401)
+
+ if self.login_message:
+ if self.localize_callback is not None:
+ flash(self.localize_callback(self.login_message),
+ category=self.login_message_category)
+ else:
+ flash(self.login_message, category=self.login_message_category)
+
+ return redirect(login_url(login_view, request.url))
+
+ def user_loader(self, callback):
+ '''
+ This sets the callback for reloading a user from the session. The
+ function you set should take a user ID (a ``unicode``) and return a
+ user object, or ``None`` if the user does not exist.
+
+ :param callback: The callback for retrieving a user object.
+ :type callback: callable
+ '''
+ self.user_callback = callback
+ return callback
+
+ def header_loader(self, callback):
+ '''
+ This sets the callback for loading a user from a header value.
+ The function you set should take an authentication token and
+ return a user object, or `None` if the user does not exist.
+
+ :param callback: The callback for retrieving a user object.
+ :type callback: callable
+ '''
+ self.header_callback = callback
+ return callback
+
+ def request_loader(self, callback):
+ '''
+ This sets the callback for loading a user from a Flask request.
+ The function you set should take Flask request object and
+ return a user object, or `None` if the user does not exist.
+
+ :param callback: The callback for retrieving a user object.
+ :type callback: callable
+ '''
+ self.request_callback = callback
+ return callback
+
+ def unauthorized_handler(self, callback):
+ '''
+ This will set the callback for the `unauthorized` method, which among
+ other things is used by `login_required`. It takes no arguments, and
+ should return a response to be sent to the user instead of their
+ normal view.
+
+ :param callback: The callback for unauthorized users.
+ :type callback: callable
+ '''
+ self.unauthorized_callback = callback
+ return callback
+
+ def needs_refresh_handler(self, callback):
+ '''
+ This will set the callback for the `needs_refresh` method, which among
+ other things is used by `fresh_login_required`. It takes no arguments,
+ and should return a response to be sent to the user instead of their
+ normal view.
+
+ :param callback: The callback for unauthorized users.
+ :type callback: callable
+ '''
+ self.needs_refresh_callback = callback
+ return callback
+
+ def needs_refresh(self):
+ '''
+ This is called when the user is logged in, but they need to be
+ reauthenticated because their session is stale. If you register a
+ callback with `needs_refresh_handler`, then it will be called.
+ Otherwise, it will take the following actions:
+
+ - Flash :attr:`LoginManager.needs_refresh_message` to the user.
+
+ - Redirect the user to :attr:`LoginManager.refresh_view`. (The page
+ they were attempting to access will be passed in the ``next``
+ query string variable, so you can redirect there if present
+ instead of the homepage.)
+
+ If :attr:`LoginManager.refresh_view` is not defined, then it will
+ simply raise a HTTP 401 (Unauthorized) error instead.
+
+ This should be returned from a view or before/after_request function,
+ otherwise the redirect will have no effect.
+ '''
+ user_needs_refresh.send(current_app._get_current_object())
+
+ if self.needs_refresh_callback:
+ return self.needs_refresh_callback()
+
+ if not self.refresh_view:
+ abort(401)
+
+ if self.localize_callback is not None:
+ flash(self.localize_callback(self.needs_refresh_message),
+ category=self.needs_refresh_message_category)
+ else:
+ flash(self.needs_refresh_message,
+ category=self.needs_refresh_message_category)
+
+ return redirect(login_url(self.refresh_view, request.url))
+
+ def reload_user(self, user=None):
+ ctx = _request_ctx_stack.top
+
+ if user is None:
+ user_id = session.get('user_id')
+ if user_id is None:
+ ctx.user = self.anonymous_user()
+ else:
+ if self.user_callback is None:
+ raise Exception(
+ "No user_loader has been installed for this "
+ "LoginManager. Add one with the "
+ "'LoginManager.user_loader' decorator.")
+ user = self.user_callback(user_id)
+ if user is None:
+ ctx.user = self.anonymous_user()
+ else:
+ ctx.user = user
+ else:
+ ctx.user = user
+
+ def _load_user(self):
+ '''Loads user from session or remember_me cookie as applicable'''
+ user_accessed.send(current_app._get_current_object())
+
+ # first check SESSION_PROTECTION
+ config = current_app.config
+ if config.get('SESSION_PROTECTION', self.session_protection):
+ deleted = self._session_protection()
+ if deleted:
+ return self.reload_user()
+
+ # If a remember cookie is set, and the session is not, move the
+ # cookie user ID to the session.
+ #
+ # However, the session may have been set if the user has been
+ # logged out on this request, 'remember' would be set to clear,
+ # so we should check for that and not restore the session.
+ is_missing_user_id = 'user_id' not in session
+ if is_missing_user_id:
+ cookie_name = config.get('REMEMBER_COOKIE_NAME', COOKIE_NAME)
+ header_name = config.get('AUTH_HEADER_NAME', AUTH_HEADER_NAME)
+ has_cookie = (cookie_name in request.cookies and
+ session.get('remember') != 'clear')
+ if has_cookie:
+ return self._load_from_cookie(request.cookies[cookie_name])
+ elif self.request_callback:
+ return self._load_from_request(request)
+ elif header_name in request.headers:
+ return self._load_from_header(request.headers[header_name])
+
+ return self.reload_user()
+
+ def _session_protection(self):
+ sess = session._get_current_object()
+ ident = _create_identifier()
+
+ app = current_app._get_current_object()
+ mode = app.config.get('SESSION_PROTECTION', self.session_protection)
+
+ # if the sess is empty, it's an anonymous user or just logged out
+ # so we can skip this
+
+ if sess and ident != sess.get('_id', None):
+ if mode == 'basic' or sess.permanent:
+ sess['_fresh'] = False
+ session_protected.send(app)
+ return False
+ elif mode == 'strong':
+ for k in SESSION_KEYS:
+ sess.pop(k, None)
+
+ sess['remember'] = 'clear'
+ session_protected.send(app)
+ return True
+
+ return False
+
+ def _load_from_cookie(self, cookie):
+ user_id = decode_cookie(cookie)
+ if user_id is not None:
+ session['user_id'] = user_id
+ session['_fresh'] = False
+
+ self.reload_user()
+
+ if _request_ctx_stack.top.user is not None:
+ app = current_app._get_current_object()
+ user_loaded_from_cookie.send(app, user=_get_user())
+
+ def _load_from_header(self, header):
+ user = None
+ if self.header_callback:
+ user = self.header_callback(header)
+ if user is not None:
+ self.reload_user(user=user)
+ app = current_app._get_current_object()
+ user_loaded_from_header.send(app, user=_get_user())
+ else:
+ self.reload_user()
+
+ def _load_from_request(self, request):
+ user = None
+ if self.request_callback:
+ user = self.request_callback(request)
+ if user is not None:
+ self.reload_user(user=user)
+ app = current_app._get_current_object()
+ user_loaded_from_request.send(app, user=_get_user())
+ else:
+ self.reload_user()
+
+ def _update_remember_cookie(self, response):
+ # Don't modify the session unless there's something to do.
+ if 'remember' in session:
+ operation = session.pop('remember', None)
+
+ if operation == 'set' and 'user_id' in session:
+ self._set_cookie(response)
+ elif operation == 'clear':
+ self._clear_cookie(response)
+
+ return response
+
+ def _set_cookie(self, response):
+ # cookie settings
+ config = current_app.config
+ cookie_name = config.get('REMEMBER_COOKIE_NAME', COOKIE_NAME)
+ duration = config.get('REMEMBER_COOKIE_DURATION', COOKIE_DURATION)
+ domain = config.get('REMEMBER_COOKIE_DOMAIN')
+ path = config.get('REMEMBER_COOKIE_PATH', '/')
+
+ secure = config.get('REMEMBER_COOKIE_SECURE', COOKIE_SECURE)
+ httponly = config.get('REMEMBER_COOKIE_HTTPONLY', COOKIE_HTTPONLY)
+
+ # prepare data
+ data = encode_cookie(text_type(session['user_id']))
+
+ try:
+ expires = datetime.utcnow() + duration
+ except TypeError:
+ raise Exception('REMEMBER_COOKIE_DURATION must be a ' +
+ 'datetime.timedelta, instead got: {0}'.format(
+ duration))
+
+ # actually set it
+ response.set_cookie(cookie_name,
+ value=data,
+ expires=expires,
+ domain=domain,
+ path=path,
+ secure=secure,
+ httponly=httponly)
+
+ def _clear_cookie(self, response):
+ config = current_app.config
+ cookie_name = config.get('REMEMBER_COOKIE_NAME', COOKIE_NAME)
+ domain = config.get('REMEMBER_COOKIE_DOMAIN')
+ path = config.get('REMEMBER_COOKIE_PATH', '/')
+ response.delete_cookie(cookie_name, domain=domain, path=path)
diff --git a/flask_login/mixins.py b/flask_login/mixins.py
new file mode 100644
index 0000000..02fa864
--- /dev/null
+++ b/flask_login/mixins.py
@@ -0,0 +1,76 @@
+# -*- coding: utf-8 -*-
+'''
+ flask_login.mixins
+ ------------------
+ This module provides mixin objects.
+'''
+
+
+from ._compat import PY2, text_type
+
+
+class UserMixin(object):
+ '''
+ This provides default implementations for the methods that Flask-Login
+ expects user objects to have.
+ '''
+
+ if not PY2: # pragma: no cover
+ # Python 3 implicitly set __hash__ to None if we override __eq__
+ # We set it back to its default implementation
+ __hash__ = object.__hash__
+
+ @property
+ def is_active(self):
+ return True
+
+ @property
+ def is_authenticated(self):
+ return True
+
+ @property
+ def is_anonymous(self):
+ return False
+
+ def get_id(self):
+ try:
+ return text_type(self.id)
+ except AttributeError:
+ raise NotImplementedError('No `id` attribute - override `get_id`')
+
+ def __eq__(self, other):
+ '''
+ Checks the equality of two `UserMixin` objects using `get_id`.
+ '''
+ if isinstance(other, UserMixin):
+ return self.get_id() == other.get_id()
+ return NotImplemented
+
+ def __ne__(self, other):
+ '''
+ Checks the inequality of two `UserMixin` objects using `get_id`.
+ '''
+ equal = self.__eq__(other)
+ if equal is NotImplemented:
+ return NotImplemented
+ return not equal
+
+
+class AnonymousUserMixin(object):
+ '''
+ This is the default object for representing an anonymous user.
+ '''
+ @property
+ def is_authenticated(self):
+ return False
+
+ @property
+ def is_active(self):
+ return False
+
+ @property
+ def is_anonymous(self):
+ return True
+
+ def get_id(self):
+ return
diff --git a/flask_login/signals.py b/flask_login/signals.py
new file mode 100644
index 0000000..bb15af8
--- /dev/null
+++ b/flask_login/signals.py
@@ -0,0 +1,56 @@
+# -*- coding: utf-8 -*-
+'''
+ flask_login.signals
+ -------------------
+ This module provides signals to get notified when Flask-Login performs
+ certain actions.
+'''
+
+
+from flask.signals import Namespace
+
+
+_signals = Namespace()
+
+
+#: Sent when a user is logged in. In addition to the app (which is the
+#: sender), it is passed `user`, which is the user being logged in.
+user_logged_in = _signals.signal('logged-in')
+
+#: Sent when a user is logged out. In addition to the app (which is the
+#: sender), it is passed `user`, which is the user being logged out.
+user_logged_out = _signals.signal('logged-out')
+
+#: Sent when the user is loaded from the cookie. In addition to the app (which
+#: is the sender), it is passed `user`, which is the user being reloaded.
+user_loaded_from_cookie = _signals.signal('loaded-from-cookie')
+
+#: Sent when the user is loaded from the header. In addition to the app (which
+#: is the #: sender), it is passed `user`, which is the user being reloaded.
+user_loaded_from_header = _signals.signal('loaded-from-header')
+
+#: Sent when the user is loaded from the request. In addition to the app (which
+#: is the #: sender), it is passed `user`, which is the user being reloaded.
+user_loaded_from_request = _signals.signal('loaded-from-request')
+
+#: Sent when a user's login is confirmed, marking it as fresh. (It is not
+#: called for a normal login.)
+#: It receives no additional arguments besides the app.
+user_login_confirmed = _signals.signal('login-confirmed')
+
+#: Sent when the `unauthorized` method is called on a `LoginManager`. It
+#: receives no additional arguments besides the app.
+user_unauthorized = _signals.signal('unauthorized')
+
+#: Sent when the `needs_refresh` method is called on a `LoginManager`. It
+#: receives no additional arguments besides the app.
+user_needs_refresh = _signals.signal('needs-refresh')
+
+#: Sent whenever the user is accessed/loaded
+#: receives no additional arguments besides the app.
+user_accessed = _signals.signal('accessed')
+
+#: Sent whenever session protection takes effect, and a session is either
+#: marked non-fresh or deleted. It receives no additional arguments besides
+#: the app.
+session_protected = _signals.signal('session-protected')
diff --git a/flask_login/utils.py b/flask_login/utils.py
new file mode 100644
index 0000000..c2233ea
--- /dev/null
+++ b/flask_login/utils.py
@@ -0,0 +1,345 @@
+# -*- coding: utf-8 -*-
+'''
+ flask_login.utils
+ -----------------
+ General utilities.
+'''
+
+
+import hmac
+from hashlib import sha512
+from functools import wraps
+from werkzeug.local import LocalProxy
+from werkzeug.security import safe_str_cmp
+from werkzeug.urls import url_decode, url_encode
+
+from flask import (_request_ctx_stack, current_app, request, session, url_for,
+ has_request_context)
+
+from ._compat import text_type, urlparse, urlunparse
+from .config import COOKIE_NAME, EXEMPT_METHODS
+from .signals import user_logged_in, user_logged_out, user_login_confirmed
+
+
+#: A proxy for the current user. If no user is logged in, this will be an
+#: anonymous user
+current_user = LocalProxy(lambda: _get_user())
+
+
+def encode_cookie(payload):
+ '''
+ This will encode a ``unicode`` value into a cookie, and sign that cookie
+ with the app's secret key.
+
+ :param payload: The value to encode, as `unicode`.
+ :type payload: unicode
+ '''
+ return u'{0}|{1}'.format(payload, _cookie_digest(payload))
+
+
+def decode_cookie(cookie):
+ '''
+ This decodes a cookie given by `encode_cookie`. If verification of the
+ cookie fails, ``None`` will be implicitly returned.
+
+ :param cookie: An encoded cookie.
+ :type cookie: str
+ '''
+ try:
+ payload, digest = cookie.rsplit(u'|', 1)
+ if hasattr(digest, 'decode'):
+ digest = digest.decode('ascii') # pragma: no cover
+ except ValueError:
+ return
+
+ if safe_str_cmp(_cookie_digest(payload), digest):
+ return payload
+
+
+def make_next_param(login_url, current_url):
+ '''
+ Reduces the scheme and host from a given URL so it can be passed to
+ the given `login` URL more efficiently.
+
+ :param login_url: The login URL being redirected to.
+ :type login_url: str
+ :param current_url: The URL to reduce.
+ :type current_url: str
+ '''
+ l = urlparse(login_url)
+ c = urlparse(current_url)
+
+ if (not l.scheme or l.scheme == c.scheme) and \
+ (not l.netloc or l.netloc == c.netloc):
+ return urlunparse(('', '', c.path, c.params, c.query, ''))
+ return current_url
+
+
+def login_url(login_view, next_url=None, next_field='next'):
+ '''
+ Creates a URL for redirecting to a login page. If only `login_view` is
+ provided, this will just return the URL for it. If `next_url` is provided,
+ however, this will append a ``next=URL`` parameter to the query string
+ so that the login view can redirect back to that URL.
+
+ :param login_view: The name of the login view. (Alternately, the actual
+ URL to the login view.)
+ :type login_view: str
+ :param next_url: The URL to give the login view for redirection.
+ :type next_url: str
+ :param next_field: What field to store the next URL in. (It defaults to
+ ``next``.)
+ :type next_field: str
+ '''
+ if login_view.startswith(('https://', 'http://', '/')):
+ base = login_view
+ else:
+ base = url_for(login_view)
+
+ if next_url is None:
+ return base
+
+ parts = list(urlparse(base))
+ md = url_decode(parts[4])
+ md[next_field] = make_next_param(base, next_url)
+ parts[4] = url_encode(md, sort=True)
+ return urlunparse(parts)
+
+
+def login_fresh():
+ '''
+ This returns ``True`` if the current login is fresh.
+ '''
+ return session.get('_fresh', False)
+
+
+def login_user(user, remember=False, force=False, fresh=True):
+ '''
+ Logs a user in. You should pass the actual user object to this. If the
+ user's `is_active` property is ``False``, they will not be logged in
+ unless `force` is ``True``.
+
+ This will return ``True`` if the log in attempt succeeds, and ``False`` if
+ it fails (i.e. because the user is inactive).
+
+ :param user: The user object to log in.
+ :type user: object
+ :param remember: Whether to remember the user after their session expires.
+ Defaults to ``False``.
+ :type remember: bool
+ :param force: If the user is inactive, setting this to ``True`` will log
+ them in regardless. Defaults to ``False``.
+ :type force: bool
+ :param fresh: setting this to ``False`` will log in the user with a session
+ marked as not "fresh". Defaults to ``True``.
+ :type fresh: bool
+ '''
+ if not force and not user.is_active:
+ return False
+
+ user_id = getattr(user, current_app.login_manager.id_attribute)()
+ session['user_id'] = user_id
+ session['_fresh'] = fresh
+ session['_id'] = _create_identifier()
+
+ if remember:
+ session['remember'] = 'set'
+
+ _request_ctx_stack.top.user = user
+ user_logged_in.send(current_app._get_current_object(), user=_get_user())
+ return True
+
+
+def logout_user():
+ '''
+ Logs a user out. (You do not need to pass the actual user.) This will
+ also clean up the remember me cookie if it exists.
+ '''
+
+ user = _get_user()
+
+ if 'user_id' in session:
+ session.pop('user_id')
+
+ if '_fresh' in session:
+ session.pop('_fresh')
+
+ cookie_name = current_app.config.get('REMEMBER_COOKIE_NAME', COOKIE_NAME)
+ if cookie_name in request.cookies:
+ session['remember'] = 'clear'
+
+ user_logged_out.send(current_app._get_current_object(), user=user)
+
+ current_app.login_manager.reload_user()
+ return True
+
+
+def confirm_login():
+ '''
+ This sets the current session as fresh. Sessions become stale when they
+ are reloaded from a cookie.
+ '''
+ session['_fresh'] = True
+ session['_id'] = _create_identifier()
+ user_login_confirmed.send(current_app._get_current_object())
+
+
+def login_required(func):
+ '''
+ If you decorate a view with this, it will ensure that the current user is
+ logged in and authenticated before calling the actual view. (If they are
+ not, it calls the :attr:`LoginManager.unauthorized` callback.) For
+ example::
+
+ @app.route('/post')
+ @login_required
+ def post():
+ pass
+
+ If there are only certain times you need to require that your user is
+ logged in, you can do so with::
+
+ if not current_user.is_authenticated:
+ return current_app.login_manager.unauthorized()
+
+ ...which is essentially the code that this function adds to your views.
+
+ It can be convenient to globally turn off authentication when unit testing.
+ To enable this, if the application configuration variable `LOGIN_DISABLED`
+ is set to `True`, this decorator will be ignored.
+
+ .. Note ::
+
+ Per `W3 guidelines for CORS preflight requests
+ <http://www.w3.org/TR/cors/#cross-origin-request-with-preflight-0>`_,
+ HTTP ``OPTIONS`` requests are exempt from login checks.
+
+ :param func: The view function to decorate.
+ :type func: function
+ '''
+ @wraps(func)
+ def decorated_view(*args, **kwargs):
+ if request.method in EXEMPT_METHODS:
+ return func(*args, **kwargs)
+ elif current_app.login_manager._login_disabled:
+ return func(*args, **kwargs)
+ elif not current_user.is_authenticated:
+ return current_app.login_manager.unauthorized()
+ return func(*args, **kwargs)
+ return decorated_view
+
+
+def fresh_login_required(func):
+ '''
+ If you decorate a view with this, it will ensure that the current user's
+ login is fresh - i.e. their session was not restored from a 'remember me'
+ cookie. Sensitive operations, like changing a password or e-mail, should
+ be protected with this, to impede the efforts of cookie thieves.
+
+ If the user is not authenticated, :meth:`LoginManager.unauthorized` is
+ called as normal. If they are authenticated, but their session is not
+ fresh, it will call :meth:`LoginManager.needs_refresh` instead. (In that
+ case, you will need to provide a :attr:`LoginManager.refresh_view`.)
+
+ Behaves identically to the :func:`login_required` decorator with respect
+ to configutation variables.
+
+ .. Note ::
+
+ Per `W3 guidelines for CORS preflight requests
+ <http://www.w3.org/TR/cors/#cross-origin-request-with-preflight-0>`_,
+ HTTP ``OPTIONS`` requests are exempt from login checks.
+
+ :param func: The view function to decorate.
+ :type func: function
+ '''
+ @wraps(func)
+ def decorated_view(*args, **kwargs):
+ if request.method in EXEMPT_METHODS:
+ return func(*args, **kwargs)
+ elif current_app.login_manager._login_disabled:
+ return func(*args, **kwargs)
+ elif not current_user.is_authenticated:
+ return current_app.login_manager.unauthorized()
+ elif not login_fresh():
+ return current_app.login_manager.needs_refresh()
+ return func(*args, **kwargs)
+ return decorated_view
+
+
+def set_login_view(login_view, blueprint=None):
+ '''
+ Sets the login view for the app or blueprint. If a blueprint is passed,
+ the login view is set for this blueprint on ``blueprint_login_views``.
+
+ :param login_view: The user object to log in.
+ :type login_view: str
+ :param blueprint: The blueprint which this login view should be set on.
+ Defaults to ``None``.
+ :type blueprint: object
+ '''
+
+ num_login_views = len(current_app.login_manager.blueprint_login_views)
+ if blueprint is not None or num_login_views != 0:
+
+ (current_app.login_manager
+ .blueprint_login_views[blueprint.name]) = login_view
+
+ if (current_app.login_manager.login_view is not None and
+ None not in current_app.login_manager.blueprint_login_views):
+
+ (current_app.login_manager
+ .blueprint_login_views[None]) = (current_app.login_manager
+ .login_view)
+
+ current_app.login_manager.login_view = None
+ else:
+ current_app.login_manager.login_view = login_view
+
+
+def _get_user():
+ if has_request_context() and not hasattr(_request_ctx_stack.top, 'user'):
+ current_app.login_manager._load_user()
+
+ return getattr(_request_ctx_stack.top, 'user', None)
+
+
+def _cookie_digest(payload, key=None):
+ key = _secret_key(key)
+
+ return hmac.new(key, payload.encode('utf-8'), sha512).hexdigest()
+
+
+def _get_remote_addr():
+ address = request.headers.get('X-Forwarded-For', request.remote_addr)
+ if address is not None:
+ # An 'X-Forwarded-For' header includes a comma separated list of the
+ # addresses, the first address being the actual remote address.
+ address = address.encode('utf-8').split(b',')[0].strip()
+ return address
+
+
+def _create_identifier():
+ user_agent = request.headers.get('User-Agent')
+ if user_agent is not None:
+ user_agent = user_agent.encode('utf-8')
+ base = '{0}|{1}'.format(_get_remote_addr(), user_agent)
+ if str is bytes:
+ base = text_type(base, 'utf-8', errors='replace') # pragma: no cover
+ h = sha512()
+ h.update(base.encode('utf8'))
+ return h.hexdigest()
+
+
+def _user_context_processor():
+ return dict(current_user=_get_user())
+
+
+def _secret_key(key=None):
+ if key is None:
+ key = current_app.config['SECRET_KEY']
+
+ if isinstance(key, text_type): # pragma: no cover
+ key = key.encode('latin1') # ensure bytes
+
+ return key
diff --git a/install_requirements.py b/install_requirements.py
new file mode 100644
index 0000000..981b23e
--- /dev/null
+++ b/install_requirements.py
@@ -0,0 +1,19 @@
+import sys
+import os
+
+
+if sys.version_info >= (3, 3):
+ requirements = "py3k-requirements.txt"
+elif (2, 6) <= sys.version_info < (3, 0):
+ requirements = "requirements.txt"
+else:
+ raise AssertionError("only support 2.6, 2.7, 3.3")
+
+
+is_dev = sys.argv[1] == "dev" if len(sys.argv) > 1 else False
+
+
+if __name__ == "__main__":
+ if is_dev:
+ requirements = "dev-%s" % requirements
+ os.system("pip install -r %s" % requirements)
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..c25fc45
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,2 @@
+flask==0.9
+werkzeug==0.8.3
diff --git a/run-tests.sh b/run-tests.sh
new file mode 100755
index 0000000..44c174a
--- /dev/null
+++ b/run-tests.sh
@@ -0,0 +1,43 @@
+#!/bin/bash
+
+OUTPUT_PATH=$(pwd)/tests_output
+
+function log() {
+ echo "$@" | tee -a $OUTPUT_PATH/test.log
+}
+
+rm -rf $OUTPUT_PATH
+mkdir -p $OUTPUT_PATH
+
+NOSETEST_OPTIONS="-d"
+
+if [ -n "$VERBOSE" ]; then
+ NOSETEST_OPTIONS="$NOSETEST_OPTIONS --verbose"
+fi
+
+if [ -z "$NOCOLOR" ]; then
+ NOSETEST_OPTIONS="$NOSETEST_OPTIONS --with-yanc --yanc-color=on"
+fi
+
+if [ -n "$OPTIONS" ]; then
+ NOSETEST_OPTIONS="$NOSETEST_OPTIONS $OPTIONS"
+fi
+
+if [ -n "$TESTS" ]; then
+ NOSETEST_OPTIONS="$NOSETEST_OPTIONS $TESTS"
+else
+ NOSETEST_OPTIONS="$NOSETEST_OPTIONS --with-coverage --cover-min-percentage=100 --cover-package=flask_login"
+fi
+
+log "Running tests..."
+nosetests $NOSETEST_OPTIONS 2>&1 | tee -a $OUTPUT_PATH/test.log
+ret=${PIPESTATUS[0]}
+
+echo
+
+case "$ret" in
+ 0) log -e "SUCCESS" ;;
+ *) log -e "FAILURE" ;;
+esac
+
+exit $ret
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..2a9acf1
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,2 @@
+[bdist_wheel]
+universal = 1
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..c88532b
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,59 @@
+'''
+Flask-Login
+-----------
+
+Flask-Login provides user session management for Flask. It handles the common
+tasks of logging in, logging out, and remembering your users'
+sessions over extended periods of time.
+
+Flask-Login is not bound to any particular database system or permissions
+model. The only requirement is that your user objects implement a few
+methods, and that you provide a callback to the extension capable of
+loading users from their ID.
+
+Links
+`````
+* `documentation <http://packages.python.org/Flask-Login>`_
+* `development version <https://github.com/maxcountryman/flask-login>`_
+'''
+import os
+import sys
+
+from setuptools import setup
+
+about = {}
+with open('flask_login/__about__.py') as f:
+ exec(f.read(), about)
+
+if sys.argv[-1] == 'test':
+ status = os.system('make check')
+ status >>= 8
+ sys.exit(status)
+
+setup(name=about['__title__'],
+ version=about['__version__'],
+ url=about['__url__'],
+ license=about['__license__'],
+ author=about['__author__'],
+ author_email=about['__author_email__'],
+ description=about['__description__'],
+ long_description=__doc__,
+ packages=['flask_login'],
+ zip_safe=False,
+ platforms='any',
+ install_requires=['Flask'],
+ classifiers=[
+ 'Development Status :: 4 - Beta',
+ 'Environment :: Web Environment',
+ 'Intended Audience :: Developers',
+ 'License :: OSI Approved :: MIT License',
+ 'Operating System :: OS Independent',
+ 'Programming Language :: Python',
+ 'Programming Language :: Python :: 2.6',
+ 'Programming Language :: Python :: 2.7',
+ 'Programming Language :: Python :: 3',
+ 'Programming Language :: Python :: 3.3',
+ 'Programming Language :: Python :: 3.4',
+ 'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
+ 'Topic :: Software Development :: Libraries :: Python Modules'
+ ])
diff --git a/test_login.py b/test_login.py
new file mode 100644
index 0000000..fb13aec
--- /dev/null
+++ b/test_login.py
@@ -0,0 +1,1269 @@
+# -*- coding: utf-8 -*-
+try:
+ import unittest2 as unittest
+except ImportError:
+ import unittest
+
+import base64
+import collections
+from datetime import timedelta, datetime
+from contextlib import contextmanager
+from mock import ANY
+
+
+from werkzeug import __version__ as werkzeug_version
+from flask import (
+ Flask,
+ Blueprint,
+ Response,
+ session,
+ get_flashed_messages,
+)
+from flask.views import MethodView
+
+from flask_login import (LoginManager, UserMixin, AnonymousUserMixin,
+ current_user, login_user, logout_user, user_logged_in,
+ user_logged_out, user_loaded_from_cookie,
+ user_login_confirmed, user_loaded_from_header,
+ user_loaded_from_request, user_unauthorized,
+ user_needs_refresh, make_next_param, login_url,
+ login_fresh, login_required, session_protected,
+ fresh_login_required, confirm_login, encode_cookie,
+ decode_cookie, set_login_view, user_accessed)
+from flask_login.utils import _secret_key, _user_context_processor
+
+
+# be compatible with py3k
+if str is not bytes:
+ unicode = str
+
+
+@contextmanager
+def listen_to(signal):
+ ''' Context Manager that listens to signals and records emissions
+
+ Example:
+
+ with listen_to(user_logged_in) as listener:
+ login_user(user)
+
+ # Assert that a single emittance of the specific args was seen.
+ listener.assert_heard_one(app, user=user))
+
+ # Of course, you can always just look at the list yourself
+ self.assertEqual(1, len(listener.heard))
+
+ '''
+ class _SignalsCaught(object):
+ def __init__(self):
+ self.heard = []
+
+ def add(self, *args, **kwargs):
+ ''' The actual handler of the signal. '''
+ self.heard.append((args, kwargs))
+
+ def assert_heard_one(self, *args, **kwargs):
+ ''' The signal fired once, and with the arguments given '''
+ if len(self.heard) == 0:
+ raise AssertionError('No signals were fired')
+ elif len(self.heard) > 1:
+ msg = '{0} signals were fired'.format(len(self.heard))
+ raise AssertionError(msg)
+ elif self.heard[0] != (args, kwargs):
+ msg = 'One signal was heard, but with incorrect arguments: '\
+ 'Got ({0}) expected ({1}, {2})'
+ raise AssertionError(msg.format(self.heard[0], args, kwargs))
+
+ def assert_heard_none(self, *args, **kwargs):
+ ''' The signal fired no times '''
+ if len(self.heard) >= 1:
+ msg = '{0} signals were fired'.format(len(self.heard))
+ raise AssertionError(msg)
+
+ results = _SignalsCaught()
+ signal.connect(results.add)
+
+ try:
+ yield results
+ finally:
+ signal.disconnect(results.add)
+
+
+class User(UserMixin):
+ def __init__(self, name, id, active=True):
+ self.id = id
+ self.name = name
+ self.active = active
+
+ def get_id(self):
+ return self.id
+
+ @property
+ def is_active(self):
+ return self.active
+
+
+notch = User(u'Notch', 1)
+steve = User(u'Steve', 2)
+creeper = User(u'Creeper', 3, False)
+germanjapanese = User(u'Müller', u'佐藤') # Unicode user_id
+
+USERS = {1: notch, 2: steve, 3: creeper, u'佐藤': germanjapanese}
+
+
+class StaticTestCase(unittest.TestCase):
+
+ def test_static_loads_anonymous(self):
+ app = Flask(__name__)
+ app.static_url_path = '/static'
+ app.secret_key = 'this is a temp key'
+ lm = LoginManager()
+ lm.init_app(app)
+
+ with app.test_client() as c:
+ c.get('/static/favicon.ico')
+ self.assertTrue(current_user.is_anonymous)
+
+ def test_static_loads_without_accessing_session(self):
+ app = Flask(__name__)
+ app.static_url_path = '/static'
+ app.secret_key = 'this is a temp key'
+ lm = LoginManager()
+ lm.init_app(app)
+
+ with app.test_client() as c:
+ with listen_to(user_accessed) as listener:
+ c.get('/static/favicon.ico')
+ listener.assert_heard_none(app)
+
+
+class InitializationTestCase(unittest.TestCase):
+ ''' Tests the two initialization methods '''
+
+ def setUp(self):
+ self.app = Flask(__name__)
+ self.app.config['SECRET_KEY'] = '1234'
+
+ def test_init_app(self):
+ login_manager = LoginManager()
+ login_manager.init_app(self.app, add_context_processor=True)
+
+ self.assertIsInstance(login_manager, LoginManager)
+
+ def test_class_init(self):
+ login_manager = LoginManager(self.app, add_context_processor=True)
+
+ self.assertIsInstance(login_manager, LoginManager)
+
+ def test_login_disabled_is_set(self):
+ login_manager = LoginManager(self.app, add_context_processor=True)
+ self.assertFalse(login_manager._login_disabled)
+
+ def test_no_user_loader_raises(self):
+ login_manager = LoginManager(self.app, add_context_processor=True)
+ with self.app.test_request_context():
+ session['user_id'] = '2'
+ with self.assertRaises(Exception) as cm:
+ login_manager.reload_user()
+ expected_exception_message = 'No user_loader has been installed'
+ self.assertTrue(
+ str(cm.exception).startswith(expected_exception_message))
+
+
+class MethodViewLoginTestCase(unittest.TestCase):
+ def setUp(self):
+ self.app = Flask(__name__)
+ self.login_manager = LoginManager()
+ self.login_manager.init_app(self.app)
+ self.login_manager._login_disabled = False
+
+ class SecretEndpoint(MethodView):
+ decorators = [
+ login_required,
+ fresh_login_required,
+ ]
+
+ def options(self):
+ return u''
+
+ def get(self):
+ return u''
+
+ self.app.add_url_rule('/secret',
+ view_func=SecretEndpoint.as_view('secret'))
+
+ def test_options_call_exempt(self):
+ with self.app.test_client() as c:
+ result = c.open('/secret', method='OPTIONS')
+ self.assertEqual(result.status_code, 200)
+
+
+class LoginTestCase(unittest.TestCase):
+ ''' Tests for results of the login_user function '''
+
+ def setUp(self):
+ self.app = Flask(__name__)
+ self.app.config['SECRET_KEY'] = 'deterministic'
+ self.app.config['SESSION_PROTECTION'] = None
+ self.remember_cookie_name = 'remember'
+ self.app.config['REMEMBER_COOKIE_NAME'] = self.remember_cookie_name
+ self.login_manager = LoginManager()
+ self.login_manager.init_app(self.app)
+ self.login_manager._login_disabled = False
+
+ @self.app.route('/')
+ def index():
+ return u'Welcome!'
+
+ @self.app.route('/secret')
+ def secret():
+ return self.login_manager.unauthorized()
+
+ @self.app.route('/login-notch')
+ def login_notch():
+ return unicode(login_user(notch))
+
+ @self.app.route('/login-notch-remember')
+ def login_notch_remember():
+ return unicode(login_user(notch, remember=True))
+
+ @self.app.route('/login-notch-permanent')
+ def login_notch_permanent():
+ session.permanent = True
+ return unicode(login_user(notch))
+
+ @self.app.route('/needs-refresh')
+ def needs_refresh():
+ return self.login_manager.needs_refresh()
+
+ @self.app.route('/confirm-login')
+ def _confirm_login():
+ confirm_login()
+ return u''
+
+ @self.app.route('/username')
+ def username():
+ if current_user.is_authenticated:
+ return current_user.name
+ return u'Anonymous'
+
+ @self.app.route('/is-fresh')
+ def is_fresh():
+ return unicode(login_fresh())
+
+ @self.app.route('/logout')
+ def logout():
+ return unicode(logout_user())
+
+ @self.login_manager.user_loader
+ def load_user(user_id):
+ return USERS[int(user_id)]
+
+ @self.login_manager.header_loader
+ def load_user_from_header(header_value):
+ if header_value.startswith('Basic '):
+ header_value = header_value.replace('Basic ', '', 1)
+ try:
+ user_id = base64.b64decode(header_value)
+ except TypeError:
+ pass
+ return USERS.get(int(user_id))
+
+ @self.login_manager.request_loader
+ def load_user_from_request(request):
+ user_id = request.args.get('user_id')
+ try:
+ user_id = int(float(user_id))
+ except TypeError:
+ pass
+ return USERS.get(user_id)
+
+ @self.app.route('/empty_session')
+ def empty_session():
+ return unicode(u'modified=%s' % session.modified)
+
+ # This will help us with the possibility of typoes in the tests. Now
+ # we shouldn't have to check each response to help us set up state
+ # (such as login pages) to make sure it worked: we will always
+ # get an exception raised (rather than return a 404 response)
+ @self.app.errorhandler(404)
+ def handle_404(e):
+ raise e
+
+ unittest.TestCase.setUp(self)
+
+ def _get_remember_cookie(self, test_client):
+ our_cookies = test_client.cookie_jar._cookies['localhost.local']['/']
+ return our_cookies[self.remember_cookie_name]
+
+ def _delete_session(self, c):
+ # Helper method to cause the session to be deleted
+ # as if the browser was closed. This will remove
+ # the session regardless of the permament flag
+ # on the session!
+ with c.session_transaction() as sess:
+ sess.clear()
+
+ #
+ # Login
+ #
+ def test_test_request_context_users_are_anonymous(self):
+ with self.app.test_request_context():
+ self.assertTrue(current_user.is_anonymous)
+
+ def test_defaults_anonymous(self):
+ with self.app.test_client() as c:
+ result = c.get('/username')
+ self.assertEqual(u'Anonymous', result.data.decode('utf-8'))
+
+ def test_login_user(self):
+ with self.app.test_request_context():
+ result = login_user(notch)
+ self.assertTrue(result)
+ self.assertEqual(current_user.name, u'Notch')
+
+ def test_login_user_not_fresh(self):
+ with self.app.test_request_context():
+ result = login_user(notch, fresh=False)
+ self.assertTrue(result)
+ self.assertEqual(current_user.name, u'Notch')
+ self.assertIs(login_fresh(), False)
+
+ def test_login_user_emits_signal(self):
+ with self.app.test_request_context():
+ with listen_to(user_logged_in) as listener:
+ login_user(notch)
+ listener.assert_heard_one(self.app, user=notch)
+
+ def test_login_inactive_user(self):
+ with self.app.test_request_context():
+ result = login_user(creeper)
+ self.assertTrue(current_user.is_anonymous)
+ self.assertFalse(result)
+
+ def test_login_inactive_user_forced(self):
+ with self.app.test_request_context():
+ login_user(creeper, force=True)
+ self.assertEqual(current_user.name, u'Creeper')
+
+ def test_login_user_with_header(self):
+ user_id = 2
+ user_name = USERS[user_id].name
+ self.login_manager.request_callback = None
+ with self.app.test_client() as c:
+ basic_fmt = 'Basic {0}'
+ decoded = bytes.decode(base64.b64encode(str.encode(str(user_id))))
+ headers = [('Authorization', basic_fmt.format(decoded))]
+ result = c.get('/username', headers=headers)
+ self.assertEqual(user_name, result.data.decode('utf-8'))
+
+ def test_login_invalid_user_with_header(self):
+ user_id = 9000
+ user_name = u'Anonymous'
+ self.login_manager.request_callback = None
+ with self.app.test_client() as c:
+ basic_fmt = 'Basic {0}'
+ decoded = bytes.decode(base64.b64encode(str.encode(str(user_id))))
+ headers = [('Authorization', basic_fmt.format(decoded))]
+ result = c.get('/username', headers=headers)
+ self.assertEqual(user_name, result.data.decode('utf-8'))
+
+ def test_login_user_with_request(self):
+ user_id = 2
+ user_name = USERS[user_id].name
+ with self.app.test_client() as c:
+ url = '/username?user_id={user_id}'.format(user_id=user_id)
+ result = c.get(url)
+ self.assertEqual(user_name, result.data.decode('utf-8'))
+
+ def test_login_invalid_user_with_request(self):
+ user_id = 9000
+ user_name = u'Anonymous'
+ with self.app.test_client() as c:
+ url = '/username?user_id={user_id}'.format(user_id=user_id)
+ result = c.get(url)
+ self.assertEqual(user_name, result.data.decode('utf-8'))
+
+ #
+ # Logout
+ #
+ def test_logout_logs_out_current_user(self):
+ with self.app.test_request_context():
+ login_user(notch)
+ logout_user()
+ self.assertTrue(current_user.is_anonymous)
+
+ def test_logout_emits_signal(self):
+ with self.app.test_request_context():
+ login_user(notch)
+ with listen_to(user_logged_out) as listener:
+ logout_user()
+ listener.assert_heard_one(self.app, user=notch)
+
+ def test_logout_without_current_user(self):
+ with self.app.test_request_context():
+ login_user(notch)
+ del session['user_id']
+ with listen_to(user_logged_out) as listener:
+ logout_user()
+ listener.assert_heard_one(self.app, user=ANY)
+
+ #
+ # Unauthorized
+ #
+ def test_unauthorized_fires_unauthorized_signal(self):
+ with self.app.test_client() as c:
+ with listen_to(user_unauthorized) as listener:
+ c.get('/secret')
+ listener.assert_heard_one(self.app)
+
+ def test_unauthorized_flashes_message_with_login_view(self):
+ self.login_manager.login_view = '/login'
+
+ expected_message = self.login_manager.login_message = u'Log in!'
+ expected_category = self.login_manager.login_message_category = 'login'
+
+ with self.app.test_client() as c:
+ c.get('/secret')
+ msgs = get_flashed_messages(category_filter=[expected_category])
+ self.assertEqual([expected_message], msgs)
+
+ def test_unauthorized_flash_message_localized(self):
+ def _gettext(msg):
+ if msg == u'Log in!':
+ return u'Einloggen'
+
+ self.login_manager.login_view = '/login'
+ self.login_manager.localize_callback = _gettext
+ self.login_manager.login_message = u'Log in!'
+
+ expected_message = u'Einloggen'
+ expected_category = self.login_manager.login_message_category = 'login'
+
+ with self.app.test_client() as c:
+ c.get('/secret')
+ msgs = get_flashed_messages(category_filter=[expected_category])
+ self.assertEqual([expected_message], msgs)
+ self.login_manager.localize_callback = None
+
+ def test_unauthorized_uses_authorized_handler(self):
+ @self.login_manager.unauthorized_handler
+ def _callback():
+ return Response('This is secret!', 401)
+
+ with self.app.test_client() as c:
+ result = c.get('/secret')
+ self.assertEqual(result.status_code, 401)
+ self.assertEqual(u'This is secret!', result.data.decode('utf-8'))
+
+ def test_unauthorized_aborts_with_401(self):
+ with self.app.test_client() as c:
+ result = c.get('/secret')
+ self.assertEqual(result.status_code, 401)
+
+ def test_unauthorized_redirects_to_login_view(self):
+ self.login_manager.login_view = 'login'
+
+ @self.app.route('/login')
+ def login():
+ return 'Login Form Goes Here!'
+
+ with self.app.test_client() as c:
+ result = c.get('/secret')
+ self.assertEqual(result.status_code, 302)
+ self.assertEqual(result.location,
+ 'http://localhost/login?next=%2Fsecret')
+
+ def test_unauthorized_uses_blueprint_login_view(self):
+ with self.app.app_context():
+
+ first = Blueprint('first', 'first')
+ second = Blueprint('second', 'second')
+
+ @self.app.route('/app_login')
+ def app_login():
+ return 'Login Form Goes Here!'
+
+ @self.app.route('/first_login')
+ def first_login():
+ return 'Login Form Goes Here!'
+
+ @self.app.route('/second_login')
+ def second_login():
+ return 'Login Form Goes Here!'
+
+ @self.app.route('/protected')
+ @login_required
+ def protected():
+ return u'Access Granted'
+
+ @first.route('/protected')
+ @login_required
+ def first_protected():
+ return u'Access Granted'
+
+ @second.route('/protected')
+ @login_required
+ def second_protected():
+ return u'Access Granted'
+
+ self.app.register_blueprint(first, url_prefix='/first')
+ self.app.register_blueprint(second, url_prefix='/second')
+
+ set_login_view('app_login')
+ set_login_view('first_login', blueprint=first)
+ set_login_view('second_login', blueprint=second)
+
+ with self.app.test_client() as c:
+
+ result = c.get('/protected')
+ self.assertEqual(result.status_code, 302)
+ expected = ('http://localhost/'
+ 'app_login?next=%2Fprotected')
+ self.assertEqual(result.location, expected)
+
+ result = c.get('/first/protected')
+ self.assertEqual(result.status_code, 302)
+ expected = ('http://localhost/'
+ 'first_login?next=%2Ffirst%2Fprotected')
+ self.assertEqual(result.location, expected)
+
+ result = c.get('/second/protected')
+ self.assertEqual(result.status_code, 302)
+ expected = ('http://localhost/'
+ 'second_login?next=%2Fsecond%2Fprotected')
+ self.assertEqual(result.location, expected)
+
+ def test_set_login_view_without_blueprints(self):
+ with self.app.app_context():
+
+ @self.app.route('/app_login')
+ def app_login():
+ return 'Login Form Goes Here!'
+
+ @self.app.route('/protected')
+ @login_required
+ def protected():
+ return u'Access Granted'
+
+ set_login_view('app_login')
+
+ with self.app.test_client() as c:
+
+ result = c.get('/protected')
+ self.assertEqual(result.status_code, 302)
+ expected = 'http://localhost/app_login?next=%2Fprotected'
+ self.assertEqual(result.location, expected)
+
+ #
+ # Session Persistence/Freshness
+ #
+ def test_login_persists(self):
+ with self.app.test_client() as c:
+ c.get('/login-notch')
+ result = c.get('/username')
+
+ self.assertEqual(u'Notch', result.data.decode('utf-8'))
+
+ def test_logout_persists(self):
+ with self.app.test_client() as c:
+ c.get('/login-notch')
+ c.get('/logout')
+ result = c.get('/username')
+ self.assertEqual(result.data.decode('utf-8'), u'Anonymous')
+
+ def test_incorrect_id_logs_out(self):
+ # Ensure that any attempt to reload the user by the ID
+ # will seem as if the user is no longer valid
+ @self.login_manager.user_loader
+ def new_user_loader(user_id):
+ return
+
+ with self.app.test_client() as c:
+ # Successfully logs in
+ c.get('/login-notch')
+ result = c.get('/username')
+
+ self.assertEqual(u'Anonymous', result.data.decode('utf-8'))
+
+ def test_authentication_is_fresh(self):
+ with self.app.test_client() as c:
+ c.get('/login-notch-remember')
+ result = c.get('/is-fresh')
+ self.assertEqual(u'True', result.data.decode('utf-8'))
+
+ def test_remember_me(self):
+ with self.app.test_client() as c:
+ c.get('/login-notch-remember')
+ self._delete_session(c)
+ result = c.get('/username')
+ self.assertEqual(u'Notch', result.data.decode('utf-8'))
+
+ def test_remember_me_uses_custom_cookie_parameters(self):
+ name = self.app.config['REMEMBER_COOKIE_NAME'] = 'myname'
+ duration = self.app.config['REMEMBER_COOKIE_DURATION'] = \
+ timedelta(days=2)
+ path = self.app.config['REMEMBER_COOKIE_PATH'] = '/mypath'
+ domain = self.app.config['REMEMBER_COOKIE_DOMAIN'] = '.localhost.local'
+
+ with self.app.test_client() as c:
+ c.get('/login-notch-remember')
+
+ # TODO: Is there a better way to test this?
+ self.assertIn(domain, c.cookie_jar._cookies,
+ 'Custom domain not found as cookie domain')
+ domain_cookie = c.cookie_jar._cookies[domain]
+ self.assertIn(path, domain_cookie,
+ 'Custom path not found as cookie path')
+ path_cookie = domain_cookie[path]
+ self.assertIn(name, path_cookie,
+ 'Custom name not found as cookie name')
+ cookie = path_cookie[name]
+
+ expiration_date = datetime.utcfromtimestamp(cookie.expires)
+ expected_date = datetime.utcnow() + duration
+ difference = expected_date - expiration_date
+
+ fail_msg = 'The expiration date {0} was far from the expected {1}'
+ fail_msg = fail_msg.format(expiration_date, expected_date)
+ self.assertLess(difference, timedelta(seconds=10), fail_msg)
+ self.assertGreater(difference, timedelta(seconds=-10), fail_msg)
+
+ def test_remember_me_with_invalid_duration_returns_500_response(self):
+ self.app.config['REMEMBER_COOKIE_DURATION'] = 123
+
+ with self.app.test_client() as c:
+ result = c.get('/login-notch-remember')
+ self.assertEqual(result.status_code, 500)
+
+ def test_set_cookie_with_invalid_duration_raises_exception(self):
+ self.app.config['REMEMBER_COOKIE_DURATION'] = 123
+
+ with self.assertRaises(Exception) as cm:
+ with self.app.test_request_context():
+ session['user_id'] = 2
+ self.login_manager._set_cookie(None)
+
+ expected_exception_message = 'Exception: ' \
+ 'REMEMBER_COOKIE_DURATION must be a datetime.timedelta, ' \
+ 'instead got: 123'
+ self.assertIn(expected_exception_message, str(cm.exception))
+
+ def test_remember_me_is_unfresh(self):
+ with self.app.test_client() as c:
+ c.get('/login-notch-remember')
+ self._delete_session(c)
+ self.assertEqual(u'False', c.get('/is-fresh').data.decode('utf-8'))
+
+ def test_login_persists_with_signle_x_forwarded_for(self):
+ self.app.config['SESSION_PROTECTION'] = 'strong'
+ with self.app.test_client() as c:
+ c.get('/login-notch', headers=[('X-Forwarded-For', '10.1.1.1')])
+ result = c.get('/username',
+ headers=[('X-Forwarded-For', '10.1.1.1')])
+ self.assertEqual(u'Notch', result.data.decode('utf-8'))
+ result = c.get('/username',
+ headers=[('X-Forwarded-For', '10.1.1.1')])
+ self.assertEqual(u'Notch', result.data.decode('utf-8'))
+
+ def test_login_persists_with_many_x_forwarded_for(self):
+ self.app.config['SESSION_PROTECTION'] = 'strong'
+ with self.app.test_client() as c:
+ c.get('/login-notch',
+ headers=[('X-Forwarded-For', '10.1.1.1')])
+ result = c.get('/username',
+ headers=[('X-Forwarded-For', '10.1.1.1')])
+ self.assertEqual(u'Notch', result.data.decode('utf-8'))
+ result = c.get('/username',
+ headers=[('X-Forwarded-For', '10.1.1.1, 10.1.1.2')])
+ self.assertEqual(u'Notch', result.data.decode('utf-8'))
+
+ def test_user_loaded_from_cookie_fired(self):
+ with self.app.test_client() as c:
+ c.get('/login-notch-remember')
+ self._delete_session(c)
+ with listen_to(user_loaded_from_cookie) as listener:
+ c.get('/username')
+ listener.assert_heard_one(self.app, user=notch)
+
+ def test_user_loaded_from_header_fired(self):
+ user_id = 1
+ user_name = USERS[user_id].name
+ self.login_manager.request_callback = None
+ with self.app.test_client() as c:
+ with listen_to(user_loaded_from_header) as listener:
+ headers = [
+ (
+ 'Authorization',
+ 'Basic %s' % (
+ bytes.decode(
+ base64.b64encode(str.encode(str(user_id))))
+ ),
+ )
+ ]
+ result = c.get('/username', headers=headers)
+ self.assertEqual(user_name, result.data.decode('utf-8'))
+ listener.assert_heard_one(self.app, user=USERS[user_id])
+
+ def test_user_loaded_from_request_fired(self):
+ user_id = 1
+ user_name = USERS[user_id].name
+ with self.app.test_client() as c:
+ with listen_to(user_loaded_from_request) as listener:
+ url = '/username?user_id={user_id}'.format(user_id=user_id)
+ result = c.get(url)
+ self.assertEqual(user_name, result.data.decode('utf-8'))
+ listener.assert_heard_one(self.app, user=USERS[user_id])
+
+ def test_logout_stays_logged_out_with_remember_me(self):
+ with self.app.test_client() as c:
+ c.get('/login-notch-remember')
+ c.get('/logout')
+ result = c.get('/username')
+ self.assertEqual(result.data.decode('utf-8'), u'Anonymous')
+
+ def test_needs_refresh_uses_handler(self):
+ @self.login_manager.needs_refresh_handler
+ def _on_refresh():
+ return u'Needs Refresh!'
+
+ with self.app.test_client() as c:
+ c.get('/login-notch-remember')
+ result = c.get('/needs-refresh')
+ self.assertEqual(u'Needs Refresh!', result.data.decode('utf-8'))
+
+ def test_needs_refresh_fires_needs_refresh_signal(self):
+ with self.app.test_client() as c:
+ c.get('/login-notch-remember')
+ with listen_to(user_needs_refresh) as listener:
+ c.get('/needs-refresh')
+ listener.assert_heard_one(self.app)
+
+ def test_needs_refresh_fires_flash_when_redirect_to_refresh_view(self):
+ self.login_manager.refresh_view = '/refresh_view'
+
+ self.login_manager.needs_refresh_message = u'Refresh'
+ self.login_manager.needs_refresh_message_category = 'refresh'
+ category_filter = [self.login_manager.needs_refresh_message_category]
+
+ with self.app.test_client() as c:
+ c.get('/login-notch-remember')
+ c.get('/needs-refresh')
+ msgs = get_flashed_messages(category_filter=category_filter)
+ self.assertIn(self.login_manager.needs_refresh_message, msgs)
+
+ def test_needs_refresh_flash_message_localized(self):
+ def _gettext(msg):
+ if msg == u'Refresh':
+ return u'Aktualisieren'
+
+ self.login_manager.refresh_view = '/refresh_view'
+ self.login_manager.localize_callback = _gettext
+
+ self.login_manager.needs_refresh_message = u'Refresh'
+ self.login_manager.needs_refresh_message_category = 'refresh'
+ category_filter = [self.login_manager.needs_refresh_message_category]
+
+ with self.app.test_client() as c:
+ c.get('/login-notch-remember')
+ c.get('/needs-refresh')
+ msgs = get_flashed_messages(category_filter=category_filter)
+ self.assertIn(u'Aktualisieren', msgs)
+ self.login_manager.localize_callback = None
+
+ def test_needs_refresh_aborts_401(self):
+ with self.app.test_client() as c:
+ c.get('/login-notch-remember')
+ result = c.get('/needs-refresh')
+ self.assertEqual(result.status_code, 401)
+
+ def test_redirects_to_refresh_view(self):
+ @self.app.route('/refresh-view')
+ def refresh_view():
+ return ''
+
+ self.login_manager.refresh_view = 'refresh_view'
+ with self.app.test_client() as c:
+ c.get('/login-notch-remember')
+ result = c.get('/needs-refresh')
+ self.assertEqual(result.status_code, 302)
+ expected = 'http://localhost/refresh-view?next=%2Fneeds-refresh'
+ self.assertEqual(result.location, expected)
+
+ def test_confirm_login(self):
+ with self.app.test_client() as c:
+ c.get('/login-notch-remember')
+ self._delete_session(c)
+ self.assertEqual(u'False', c.get('/is-fresh').data.decode('utf-8'))
+ c.get('/confirm-login')
+ self.assertEqual(u'True', c.get('/is-fresh').data.decode('utf-8'))
+
+ def test_user_login_confirmed_signal_fired(self):
+ with self.app.test_client() as c:
+ with listen_to(user_login_confirmed) as listener:
+ c.get('/confirm-login')
+ listener.assert_heard_one(self.app)
+
+ def test_session_not_modified(self):
+ with self.app.test_client() as c:
+ # Within the request we think we didn't modify the session.
+ self.assertEquals(
+ u'modified=False',
+ c.get('/empty_session').data.decode('utf-8'))
+ # But after the request, the session could be modified by the
+ # "after_request" handlers that call _update_remember_cookie.
+ # Ensure that if nothing changed the session is not modified.
+ self.assertFalse(session.modified)
+
+ #
+ # Session Protection
+ #
+ def test_session_protection_basic_passes_successive_requests(self):
+ self.app.config['SESSION_PROTECTION'] = 'basic'
+ with self.app.test_client() as c:
+ c.get('/login-notch-remember')
+ username_result = c.get('/username')
+ self.assertEqual(u'Notch', username_result.data.decode('utf-8'))
+ fresh_result = c.get('/is-fresh')
+ self.assertEqual(u'True', fresh_result.data.decode('utf-8'))
+
+ def test_session_protection_strong_passes_successive_requests(self):
+ self.app.config['SESSION_PROTECTION'] = 'strong'
+ with self.app.test_client() as c:
+ c.get('/login-notch-remember')
+ username_result = c.get('/username')
+ self.assertEqual(u'Notch', username_result.data.decode('utf-8'))
+ fresh_result = c.get('/is-fresh')
+ self.assertEqual(u'True', fresh_result.data.decode('utf-8'))
+
+ def test_session_protection_basic_marks_session_unfresh(self):
+ self.app.config['SESSION_PROTECTION'] = 'basic'
+ with self.app.test_client() as c:
+ c.get('/login-notch-remember')
+ username_result = c.get('/username',
+ headers=[('User-Agent', 'different')])
+ self.assertEqual(u'Notch', username_result.data.decode('utf-8'))
+ fresh_result = c.get('/is-fresh')
+ self.assertEqual(u'False', fresh_result.data.decode('utf-8'))
+
+ def test_session_protection_basic_fires_signal(self):
+ self.app.config['SESSION_PROTECTION'] = 'basic'
+
+ with self.app.test_client() as c:
+ c.get('/login-notch-remember')
+ with listen_to(session_protected) as listener:
+ c.get('/username', headers=[('User-Agent', 'different')])
+ listener.assert_heard_one(self.app)
+
+ def test_session_protection_basic_skips_when_remember_me(self):
+ self.app.config['SESSION_PROTECTION'] = 'basic'
+
+ with self.app.test_client() as c:
+ c.get('/login-notch-remember')
+ # clear session to force remember me (and remove old session id)
+ self._delete_session(c)
+ # should not trigger protection because "sess" is empty
+ with listen_to(session_protected) as listener:
+ c.get('/username')
+ listener.assert_heard_none(self.app)
+
+ def test_session_protection_strong_skips_when_remember_me(self):
+ self.app.config['SESSION_PROTECTION'] = 'strong'
+
+ with self.app.test_client() as c:
+ c.get('/login-notch-remember')
+ # clear session to force remember me (and remove old session id)
+ self._delete_session(c)
+ # should not trigger protection because "sess" is empty
+ with listen_to(session_protected) as listener:
+ c.get('/username')
+ listener.assert_heard_none(self.app)
+
+ def test_permanent_strong_session_protection_marks_session_unfresh(self):
+ self.app.config['SESSION_PROTECTION'] = 'strong'
+ with self.app.test_client() as c:
+ c.get('/login-notch-permanent')
+ username_result = c.get('/username', headers=[('User-Agent',
+ 'different')])
+ self.assertEqual(u'Notch', username_result.data.decode('utf-8'))
+ fresh_result = c.get('/is-fresh')
+ self.assertEqual(u'False', fresh_result.data.decode('utf-8'))
+
+ def test_permanent_strong_session_protection_fires_signal(self):
+ self.app.config['SESSION_PROTECTION'] = 'strong'
+
+ with self.app.test_client() as c:
+ c.get('/login-notch-permanent')
+ with listen_to(session_protected) as listener:
+ c.get('/username', headers=[('User-Agent', 'different')])
+ listener.assert_heard_one(self.app)
+
+ def test_session_protection_strong_deletes_session(self):
+ self.app.config['SESSION_PROTECTION'] = 'strong'
+ with self.app.test_client() as c:
+ # write some unrelated data in the session, to ensure it does not
+ # get destroyed
+ with c.session_transaction() as sess:
+ sess['foo'] = 'bar'
+ c.get('/login-notch-remember')
+ username_result = c.get('/username', headers=[('User-Agent',
+ 'different')])
+ self.assertEqual(u'Anonymous',
+ username_result.data.decode('utf-8'))
+ with c.session_transaction() as sess:
+ self.assertIn('foo', sess)
+ self.assertEqual('bar', sess['foo'])
+
+ def test_session_protection_strong_fires_signal_user_agent(self):
+ self.app.config['SESSION_PROTECTION'] = 'strong'
+
+ with self.app.test_client() as c:
+ c.get('/login-notch-remember')
+ with listen_to(session_protected) as listener:
+ c.get('/username', headers=[('User-Agent', 'different')])
+ listener.assert_heard_one(self.app)
+
+ def test_session_protection_strong_fires_signal_x_forwarded_for(self):
+ self.app.config['SESSION_PROTECTION'] = 'strong'
+
+ with self.app.test_client() as c:
+ c.get('/login-notch-remember',
+ headers=[('X-Forwarded-For', '10.1.1.1')])
+ with listen_to(session_protected) as listener:
+ c.get('/username', headers=[('X-Forwarded-For', '10.1.1.2')])
+ listener.assert_heard_one(self.app)
+
+ def test_session_protection_skip_when_off_and_anonymous(self):
+ with self.app.test_client() as c:
+ # no user access
+ with listen_to(user_accessed) as user_listener:
+ results = c.get('/')
+ user_listener.assert_heard_none(self.app)
+
+ # access user with no session data
+ with listen_to(session_protected) as session_listener:
+ results = c.get('/username')
+ self.assertEqual(results.data.decode('utf-8'), u'Anonymous')
+ session_listener.assert_heard_none(self.app)
+
+ # verify no session data has been set
+ self.assertFalse(session)
+
+ def test_session_protection_skip_when_basic_and_anonymous(self):
+ self.app.config['SESSION_PROTECTION'] = 'basic'
+
+ with self.app.test_client() as c:
+ # no user access
+ with listen_to(user_accessed) as user_listener:
+ results = c.get('/')
+ user_listener.assert_heard_none(self.app)
+
+ # access user with no session data
+ with listen_to(session_protected) as session_listener:
+ results = c.get('/username')
+ self.assertEqual(results.data.decode('utf-8'), u'Anonymous')
+ session_listener.assert_heard_none(self.app)
+
+ # verify no session data has been set
+ self.assertFalse(session)
+
+ #
+ # Lazy Access User
+ #
+ def test_requests_without_accessing_session(self):
+ with self.app.test_client() as c:
+ c.get('/login-notch')
+
+ # no session access
+ with listen_to(user_accessed) as listener:
+ c.get('/')
+ listener.assert_heard_none(self.app)
+
+ # should have a session access
+ with listen_to(user_accessed) as listener:
+ result = c.get('/username')
+ listener.assert_heard_one(self.app)
+ self.assertEqual(result.data.decode('utf-8'), u'Notch')
+
+ #
+ # View Decorators
+ #
+ def test_login_required_decorator(self):
+ @self.app.route('/protected')
+ @login_required
+ def protected():
+ return u'Access Granted'
+
+ with self.app.test_client() as c:
+ result = c.get('/protected')
+ self.assertEqual(result.status_code, 401)
+
+ c.get('/login-notch')
+ result2 = c.get('/protected')
+ self.assertIn(u'Access Granted', result2.data.decode('utf-8'))
+
+ def test_decorators_are_disabled(self):
+ @self.app.route('/protected')
+ @login_required
+ @fresh_login_required
+ def protected():
+ return u'Access Granted'
+
+ self.app.login_manager._login_disabled = True
+
+ with self.app.test_client() as c:
+ result = c.get('/protected')
+ self.assertIn(u'Access Granted', result.data.decode('utf-8'))
+
+ def test_fresh_login_required_decorator(self):
+ @self.app.route('/very-protected')
+ @fresh_login_required
+ def very_protected():
+ return 'Access Granted'
+
+ with self.app.test_client() as c:
+ result = c.get('/very-protected')
+ self.assertEqual(result.status_code, 401)
+
+ c.get('/login-notch-remember')
+ logged_in_result = c.get('/very-protected')
+ self.assertEqual(u'Access Granted',
+ logged_in_result.data.decode('utf-8'))
+
+ self._delete_session(c)
+ stale_result = c.get('/very-protected')
+ self.assertEqual(stale_result.status_code, 401)
+
+ c.get('/confirm-login')
+ refreshed_result = c.get('/very-protected')
+ self.assertEqual(u'Access Granted',
+ refreshed_result.data.decode('utf-8'))
+
+ #
+ # Misc
+ #
+ @unittest.skipIf(werkzeug_version.startswith("0.9"),
+ "wait for upstream implementing RFC 5987")
+ def test_chinese_user_agent(self):
+ with self.app.test_client() as c:
+ result = c.get('/', headers=[('User-Agent', u'中文')])
+ self.assertEqual(u'Welcome!', result.data.decode('utf-8'))
+
+ @unittest.skipIf(werkzeug_version.startswith("0.9"),
+ "wait for upstream implementing RFC 5987")
+ def test_russian_cp1251_user_agent(self):
+ with self.app.test_client() as c:
+ headers = [('User-Agent', u'ЯЙЮя'.encode('cp1251'))]
+ response = c.get('/', headers=headers)
+ self.assertEqual(response.data.decode('utf-8'), u'Welcome!')
+
+ def test_user_context_processor(self):
+ with self.app.test_request_context():
+ _ucp = self.app.context_processor(_user_context_processor)
+ self.assertIsInstance(_ucp()['current_user'], AnonymousUserMixin)
+
+
+class TestLoginUrlGeneration(unittest.TestCase):
+ def test_make_next_param(self):
+ self.assertEqual('/profile',
+ make_next_param('/login', 'http://localhost/profile'))
+
+ self.assertEqual('http://localhost/profile',
+ make_next_param('https://localhost/login',
+ 'http://localhost/profile'))
+
+ self.assertEqual('http://localhost/profile',
+ make_next_param('http://accounts.localhost/login',
+ 'http://localhost/profile'))
+
+ def test_login_url_generation(self):
+ PROTECTED = 'http://localhost/protected'
+
+ self.assertEqual('/login?n=%2Fprotected', login_url('/login',
+ PROTECTED, 'n'))
+
+ self.assertEqual('/login?next=%2Fprotected', login_url('/login',
+ PROTECTED))
+
+ expected = 'https://auth.localhost/login' + \
+ '?next=http%3A%2F%2Flocalhost%2Fprotected'
+ self.assertEqual(expected,
+ login_url('https://auth.localhost/login', PROTECTED))
+
+ self.assertEqual('/login?affil=cgnu&next=%2Fprotected',
+ login_url('/login?affil=cgnu', PROTECTED))
+
+ def test_login_url_generation_with_view(self):
+ app = Flask(__name__)
+ login_manager = LoginManager()
+ login_manager.init_app(app)
+
+ @app.route('/login')
+ def login():
+ return ''
+
+ with app.test_request_context():
+ self.assertEqual('/login?next=%2Fprotected',
+ login_url('login', '/protected'))
+
+ def test_login_url_no_next_url(self):
+ self.assertEqual(login_url('/foo'), '/foo')
+
+
+class CookieEncodingTestCase(unittest.TestCase):
+ def test_cookie_encoding(self):
+ app = Flask(__name__)
+ app.config['SECRET_KEY'] = 'deterministic'
+
+ # COOKIE = u'1|7d276051c1eec578ed86f6b8478f7f7d803a7970'
+
+ # Due to the restriction of 80 chars I have to break up the hash in two
+ h1 = u'0e9e6e9855fbe6df7906ec4737578a1d491b38d3fd5246c1561016e189d6516'
+ h2 = u'043286501ca43257c938e60aad77acec5ce916b94ca9d00c0bb6f9883ae4b82'
+ h3 = u'ae'
+ COOKIE = u'1|' + h1 + h2 + h3
+
+ with app.test_request_context():
+ self.assertEqual(COOKIE, encode_cookie(u'1'))
+ self.assertEqual(u'1', decode_cookie(COOKIE))
+ self.assertIsNone(decode_cookie(u'Foo|BAD_BASH'))
+ self.assertIsNone(decode_cookie(u'no bar'))
+
+
+class SecretKeyTestCase(unittest.TestCase):
+ def setUp(self):
+ self.app = Flask(__name__)
+
+ def test_bytes(self):
+ self.app.config['SECRET_KEY'] = b'\x9e\x8f\x14'
+ with self.app.test_request_context():
+ self.assertEqual(_secret_key(), b'\x9e\x8f\x14')
+
+ def test_native(self):
+ self.app.config['SECRET_KEY'] = '\x9e\x8f\x14'
+ with self.app.test_request_context():
+ self.assertEqual(_secret_key(), b'\x9e\x8f\x14')
+
+ def test_default(self):
+ self.assertEqual(_secret_key('\x9e\x8f\x14'), b'\x9e\x8f\x14')
+
+
+class ImplicitIdUser(UserMixin):
+ def __init__(self, id):
+ self.id = id
+
+
+class ExplicitIdUser(UserMixin):
+ def __init__(self, name):
+ self.name = name
+
+
+class UserMixinTestCase(unittest.TestCase):
+ def test_default_values(self):
+ user = ImplicitIdUser(1)
+ self.assertTrue(user.is_active)
+ self.assertTrue(user.is_authenticated)
+ self.assertFalse(user.is_anonymous)
+
+ def test_get_id_from_id_attribute(self):
+ user = ImplicitIdUser(1)
+ self.assertEqual(u'1', user.get_id())
+
+ def test_get_id_not_implemented(self):
+ user = ExplicitIdUser('Notch')
+ self.assertRaises(NotImplementedError, lambda: user.get_id())
+
+ def test_equality(self):
+ first = ImplicitIdUser(1)
+ same = ImplicitIdUser(1)
+ different = ImplicitIdUser(2)
+
+ # Explicitly test the equality operator
+ self.assertTrue(first == same)
+ self.assertFalse(first == different)
+ self.assertFalse(first != same)
+ self.assertTrue(first != different)
+
+ self.assertFalse(first == u'1')
+ self.assertTrue(first != u'1')
+
+ def test_hashable(self):
+ self.assertTrue(isinstance(UserMixin(), collections.Hashable))
+
+
+class AnonymousUserTestCase(unittest.TestCase):
+ def test_values(self):
+ user = AnonymousUserMixin()
+
+ self.assertFalse(user.is_active)
+ self.assertFalse(user.is_authenticated)
+ self.assertTrue(user.is_anonymous)
+ self.assertIsNone(user.get_id())
+
+
+class UnicodeCookieUserIDTestCase(unittest.TestCase):
+ def setUp(self):
+ self.app = Flask(__name__)
+ self.app.config['SECRET_KEY'] = 'deterministic'
+ self.app.config['SESSION_PROTECTION'] = None
+ self.remember_cookie_name = 'remember'
+ self.app.config['REMEMBER_COOKIE_NAME'] = self.remember_cookie_name
+ self.login_manager = LoginManager()
+ self.login_manager.init_app(self.app)
+ self.login_manager._login_disabled = False
+
+ @self.app.route('/')
+ def index():
+ return u'Welcome!'
+
+ @self.app.route('/login-germanjapanese-remember')
+ def login_germanjapanese_remember():
+ return unicode(login_user(germanjapanese, remember=True))
+
+ @self.app.route('/username')
+ def username():
+ if current_user.is_authenticated:
+ return current_user.name
+ return u'Anonymous'
+
+ @self.app.route('/userid')
+ def user_id():
+ if current_user.is_authenticated:
+ return current_user.id
+ return u'wrong_id'
+
+ @self.login_manager.user_loader
+ def load_user(user_id):
+ return USERS[unicode(user_id)]
+
+ # This will help us with the possibility of typoes in the tests. Now
+ # we shouldn't have to check each response to help us set up state
+ # (such as login pages) to make sure it worked: we will always
+ # get an exception raised (rather than return a 404 response)
+ @self.app.errorhandler(404)
+ def handle_404(e):
+ raise e
+
+ unittest.TestCase.setUp(self)
+
+ def _delete_session(self, c):
+ # Helper method to cause the session to be deleted
+ # as if the browser was closed. This will remove
+ # the session regardless of the permament flag
+ # on the session!
+ with c.session_transaction() as sess:
+ sess.clear()
+
+ def test_remember_me_username(self):
+ with self.app.test_client() as c:
+ c.get('/login-germanjapanese-remember')
+ self._delete_session(c)
+ result = c.get('/username')
+ self.assertEqual(u'Müller', result.data.decode('utf-8'))
+
+ def test_remember_me_user_id(self):
+ with self.app.test_client() as c:
+ c.get('/login-germanjapanese-remember')
+ self._delete_session(c)
+ result = c.get('/userid')
+ self.assertEqual(u'佐藤', result.data.decode('utf-8'))
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..e40074c
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,22 @@
+[tox]
+envlist = py26,py27,py33,pypy
+
+[testenv:py26]
+deps = -r{toxinidir}/dev-requirements.txt
+commands =
+ make check
+
+[testenv:py27]
+deps = -r{toxinidir}/dev-requirements.txt
+commands =
+ make check
+
+[testenv:pypy]
+deps = -r{toxinidir}/dev-requirements.txt
+commands =
+ make check
+
+[testenv:py33]
+deps = -r{toxinidir}/dev-py3k-requirements.txt
+commands =
+ make check