Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

global: backports Flask-IIIF extension from 2.1 to 2.0 #3338

Merged
merged 1 commit into from
Jul 13, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions invenio/ext/iiif/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# -*- coding: utf-8 -*-
#
# This file is part of Invenio.
# Copyright (C) 2015 CERN.
#
# Invenio is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; either version 2 of the
# License, or (at your option) any later version.
#
# Invenio is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Invenio; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.

"""Flask-IIIF extension.

.. py:data:: IIIF_IMAGE_OPENER

There are two ways to initialize an IIIF image object, by ``fullpath`` and
``bytestream``. Can be an ``import string`` or a ``callback function``.
By default ``identifier_to_path`` return the ``fullpath`` of
:class:`~invenio.modules.documents.api:Document`.

default: :func:`~invenio.modules.documents.utis.identifier_to_path`
"""

from flask_iiif import IIIF
from flask_iiif.errors import MultimediaError

from six import string_types

from werkzeug.utils import import_string

from .utils import api_file_permission_check

__all__ = ('setup_app', )

iiif = IIIF()


def setup_app(app):
"""Setup Flask-IIIF extension."""
if 'invenio.modules.documents' in app.config.get('PACKAGES_EXCLUDE'):
raise MultimediaError(
"Could not initialize the Flask-IIIF extension because "
":class:`~invenio.modules.documents.api:Document` is missing"
)

iiif.init_app(app)
iiif.init_restful(app.extensions['restful'])
app.config.setdefault(
'IIIF_IMAGE_OPENER',
'invenio.modules.documents.utils:identifier_to_path'
)

uuid_to_source_handler = app.config['IIIF_IMAGE_OPENER']

uuid_to_source = (
import_string(uuid_to_source_handler) if
isinstance(uuid_to_source_handler, string_types) else
uuid_to_source_handler
)
iiif.uuid_to_image_opener_handler(uuid_to_source)
app.config['IIIF_CACHE_HANDLER'] = 'invenio.ext.cache:cache'

# protect the api
iiif.api_decorator_handler(api_file_permission_check)
return app
31 changes: 31 additions & 0 deletions invenio/ext/iiif/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
#
# This file is part of Invenio.
# Copyright (C) 2015 CERN.
#
# Invenio is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; either version 2 of the
# License, or (at your option) any later version.
#
# Invenio is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Invenio; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.

"""Flask-IIIF extension utils."""

from invenio.modules.documents.utils import (
identifier_to_path_and_permissions
)

__all__ = ('api_file_permission_check', )


def api_file_permission_check(*args, **kwargs):
"""IIIF API file permission check."""
identifier_to_path_and_permissions(kwargs.get('uuid', ''))
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# -*- coding: utf-8 -*-
#
# This file is part of Invenio.
# Copyright (C) 2015 CERN.
#
# Invenio is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; either version 2 of the
# License, or (at your option) any later version.
#
# Invenio is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Invenio; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.

"""Test for document and legacy bibdocs access restrictions."""

import os
import shutil
import tempfile

from invenio.testsuite import InvenioTestCase, make_test_suite, run_test_suite


class DocumentAndLegacyRestrictionsTest(InvenioTestCase):

"""Test document access restrictions."""

def setUp(self):
"""Run before the test."""
from invenio.modules.documents.api import Document
self.document = Document.create({'title': 'J.A.R.V.I.S'})
self.path = tempfile.mkdtemp()

def tearDown(self):
"""Run after the tests."""
self.document = None
shutil.rmtree(self.path)

def test_legacy_syntax(self):
"""Test legacy syntax."""
from invenio.modules.documents.utils import _parse_legacy_syntax
uuid_1 = 'recid:22'
uuid_2 = 'recid:22-filename.jpg'

check_uuid_1 = _parse_legacy_syntax(uuid_1)
check_uuid_2 = _parse_legacy_syntax(uuid_2)
answer_uuid_1 = '22', None
answer_uuid_2 = '22', 'filename.jpg'

self.assertEqual(
check_uuid_1,
answer_uuid_1
)
self.assertEqual(
check_uuid_2,
answer_uuid_2
)

def test_not_found_error(self):
"""Test when the file doesn't exists."""
from werkzeug.exceptions import NotFound
from invenio.modules.documents.utils import identifier_to_path
self.assertRaises(
NotFound,
identifier_to_path,
'this_is_not_a_uuid'
)
self.assertRaises(
NotFound,
identifier_to_path,
self.document.get('uuid')
)

def test_forbidden_error(self):
"""Test when the file is restricted."""
from werkzeug.exceptions import Forbidden
from invenio.modules.documents.utils import (
identifier_to_path_and_permissions
)
content = 'S.H.I.E.L.D.'
source, sourcepath = tempfile.mkstemp()

with open(sourcepath, 'w+') as f:
f.write(content)

uri = os.path.join(self.path, 'classified.txt')
self.document.setcontents(sourcepath, uri)
self.document['restriction']['email'] = '[email protected]'
test_document = self.document.update()
self.assertRaises(
Forbidden,
identifier_to_path_and_permissions,
test_document.get('uuid')
)
shutil.rmtree(sourcepath, ignore_errors=True)

TEST_SUITE = make_test_suite(DocumentAndLegacyRestrictionsTest,)

if __name__ == "__main__":
run_test_suite(TEST_SUITE)
128 changes: 128 additions & 0 deletions invenio/modules/documents/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# -*- coding: utf-8 -*-
#
# This file is part of Invenio.
# Copyright (C) 2015 CERN.
#
# Invenio is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; either version 2 of the
# License, or (at your option) any later version.
#
# Invenio is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Invenio; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.

"""Document utils."""

from flask import abort

__all__ = ('identifier_to_path', 'identifier_to_path_and_permissions', )


def identifier_to_path(identifier):
"""Convert the identifier to path.

:param str identifier: A unique file identifier
:raises NotFound: if the file fullpath is empty
"""
fullpath = identifier_to_path_and_permissions(identifier, path_only=True)
return fullpath or abort(404)


def identifier_to_path_and_permissions(identifier, path_only=False):
"""Convert the identifier to path.

:param str identifier: A unique file identifier
:raises Forbidden: if the user doesn't have permissions
"""
if identifier.startswith('recid:'):
record_id, filename = _parse_legacy_syntax(identifier)

fullpath, permissions = _get_legacy_bibdoc(
record_id, filename=filename
)
else:
fullpath, permissions = _get_document(identifier)

if path_only:
return fullpath

try:
assert (permissions[0] == 0)
except AssertionError:
return abort(403)


def _get_document(uuid):
"""Get the document fullpath.

:param str uuid: The document's uuid
"""
from invenio.modules.documents.api import Document
from invenio.modules.documents.errors import (
DocumentNotFound, DeletedDocument
)

try:
document = Document.get_document(uuid)
except (DocumentNotFound, DeletedDocument):
path = _simulate_file_not_found()
else:
path = document.get('uri', ''), document.is_authorized()
finally:
return path


def _get_legacy_bibdoc(recid, filename=None):
"""Get the the fullpath of legacy bibdocfile.

:param int recid: The record id
:param str filename: A specific filename
:returns: bibdocfile full path
:rtype: str
"""
from invenio.ext.login import current_user
from invenio.legacy.bibdocfile.api import BibRecDocs
paths = [
(bibdoc.fullpath, bibdoc.is_restricted(current_user))
for bibdoc in BibRecDocs(recid).list_latest_files(list_hidden=False)
if not bibdoc.subformat and not filename or
bibdoc.name + bibdoc.superformat == filename
]
try:
path = paths[0]
except IndexError:
path = _simulate_file_not_found()
finally:
return path


def _parse_legacy_syntax(identifier):
"""Parse legacy syntax.

.. note::

It can handle requests such as `recid:{recid}` or
`recid:{recid}-{filename}`.
"""
if '-' in identifier:
record_id, filename = identifier.split('recid:')[1].split('-')
else:
record_id, filename = identifier.split('recid:')[1], None
return record_id, filename


def _simulate_file_not_found():
"""Simulate file not found situation.

..note ::

It simulates an file not found situation, this will always raise `404`
error.
"""
return '', (0, '')
3 changes: 3 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,9 @@ def run(self):
"oauth2client>=1.4.0",
"urllib3>=1.8.3"
],
"iiif": [
"Flask-IIIF>=0.2.0",
],
"img": [
"qrcode>=5.1",
"Pillow>=2.7.0"
Expand Down