summaryrefslogtreecommitdiff
path: root/src/pixelated/resources
diff options
context:
space:
mode:
Diffstat (limited to 'src/pixelated/resources')
-rw-r--r--src/pixelated/resources/__init__.py118
-rw-r--r--src/pixelated/resources/attachments_resource.py113
-rw-r--r--src/pixelated/resources/auth.py150
-rw-r--r--src/pixelated/resources/contacts_resource.py44
-rw-r--r--src/pixelated/resources/features_resource.py52
-rw-r--r--src/pixelated/resources/feedback_resource.py31
-rw-r--r--src/pixelated/resources/keys_resource.py30
-rw-r--r--src/pixelated/resources/login_resource.py182
-rw-r--r--src/pixelated/resources/logout_resource.py45
-rw-r--r--src/pixelated/resources/mail_resource.py79
-rw-r--r--src/pixelated/resources/mails_resource.py239
-rw-r--r--src/pixelated/resources/root_resource.py150
-rw-r--r--src/pixelated/resources/sandbox_resource.py37
-rw-r--r--src/pixelated/resources/session.py39
-rw-r--r--src/pixelated/resources/tags_resource.py39
-rw-r--r--src/pixelated/resources/user_settings_resource.py45
16 files changed, 1393 insertions, 0 deletions
diff --git a/src/pixelated/resources/__init__.py b/src/pixelated/resources/__init__.py
new file mode 100644
index 00000000..e3ba7105
--- /dev/null
+++ b/src/pixelated/resources/__init__.py
@@ -0,0 +1,118 @@
+#
+# Copyright (c) 2014 ThoughtWorks, Inc.
+#
+# Pixelated is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Pixelated is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+
+import json
+import logging
+
+from twisted.web.http import UNAUTHORIZED
+from twisted.web.resource import Resource
+
+# from pixelated.resources.login_resource import LoginResource
+from pixelated.resources.session import IPixelatedSession
+
+from twisted.web.http import INTERNAL_SERVER_ERROR
+log = logging.getLogger(__name__)
+
+
+class SetEncoder(json.JSONEncoder):
+
+ def default(self, obj):
+ if isinstance(obj, set):
+ return list(obj)
+ return super(SetEncoder, self).default(obj)
+
+
+def respond_json(entity, request, status_code=200):
+ json_response = json.dumps(entity, cls=SetEncoder)
+ request.responseHeaders.addRawHeader(b"content-type", b"application/json")
+ request.code = status_code
+ return json_response
+
+
+def respond_json_deferred(entity, request, status_code=200):
+ json_response = json.dumps(entity, cls=SetEncoder)
+ request.responseHeaders.addRawHeader(b"content-type", b"application/json")
+ request.code = status_code
+ request.write(json_response)
+ request.finish()
+
+
+class GenericDeferredErrorHandler(object):
+
+ @classmethod
+ def generic_error_handling(cls, e, request):
+ log.error(e)
+ request.setResponseCode(INTERNAL_SERVER_ERROR)
+ request.write('Something went wrong!')
+ request.finish()
+
+
+class BaseResource(Resource, GenericDeferredErrorHandler):
+
+ def __init__(self, services_factory):
+ Resource.__init__(self)
+ self._services_factory = services_factory
+
+ def _get_user_id_from_request(self, request):
+ if self._services_factory.mode.is_single_user:
+ return None # it doesn't matter
+ session = self.get_session(request)
+ if session.is_logged_in():
+ return session.user_uuid
+ raise ValueError('Not logged in')
+
+ def is_logged_in(self, request):
+ session = self.get_session(request)
+ return session.is_logged_in() and self._services_factory.is_logged_in(session.user_uuid)
+
+ def get_session(self, request):
+ return IPixelatedSession(request.getSession())
+
+ def _services(self, request):
+ user_id = self._get_user_id_from_request(request)
+ return self._services_factory.services(user_id)
+
+ def _service(self, request, attribute):
+ return getattr(self._services(request), attribute)
+
+ def keymanager(self, request):
+ return self._service(request, 'keymanager')
+
+ def mail_service(self, request):
+ return self._service(request, 'mail_service')
+
+ def search_engine(self, request):
+ return self._service(request, 'search_engine')
+
+ def draft_service(self, request):
+ return self._service(request, 'draft_service')
+
+ def feedback_service(self, request):
+ return self._service(request, 'feedback_service')
+
+
+class UnAuthorizedResource(Resource):
+
+ def __init__(self):
+ Resource.__init__(self)
+
+ def render_GET(self, request):
+ request.setResponseCode(UNAUTHORIZED)
+ return "Unauthorized!"
+
+ def render_POST(self, request):
+ request.setResponseCode(UNAUTHORIZED)
+ return "Unauthorized!"
diff --git a/src/pixelated/resources/attachments_resource.py b/src/pixelated/resources/attachments_resource.py
new file mode 100644
index 00000000..146258f9
--- /dev/null
+++ b/src/pixelated/resources/attachments_resource.py
@@ -0,0 +1,113 @@
+#
+# Copyright (c) 2014 ThoughtWorks, Inc.
+#
+# Pixelated is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Pixelated is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+import cgi
+import io
+import re
+import logging
+
+from twisted.internet import defer
+from twisted.protocols.basic import FileSender
+from twisted.python.log import msg
+from twisted.web import server
+from twisted.web.resource import Resource
+from twisted.web.server import NOT_DONE_YET
+
+from pixelated.resources import respond_json_deferred, BaseResource
+
+logger = logging.getLogger(__name__)
+
+
+class AttachmentResource(Resource):
+ isLeaf = True
+
+ def __init__(self, mail_service, attachment_id):
+ Resource.__init__(self)
+ self.attachment_id = attachment_id
+ self.mail_service = mail_service
+
+ def render_GET(self, request):
+ def error_handler(failure):
+ msg(failure, 'attachment not found')
+ request.code = 404
+ request.finish()
+
+ encoding = request.args.get('encoding', [None])[0]
+ filename = request.args.get('filename', [self.attachment_id])[0]
+ content_type = request.args.get(
+ 'content_type', ['application/octet-stream'])[0]
+ request.setHeader(b'Content-Type', content_type)
+ request.setHeader(b'Content-Disposition',
+ bytes('attachment; filename="' + filename + '"'))
+
+ d = self._send_attachment(encoding, filename, request)
+ d.addErrback(error_handler)
+
+ return server.NOT_DONE_YET
+
+ @defer.inlineCallbacks
+ def _send_attachment(self, encoding, filename, request):
+ attachment = yield self.mail_service.attachment(self.attachment_id)
+
+ bytes_io = io.BytesIO(attachment['content'])
+
+ try:
+ request.code = 200
+ yield FileSender().beginFileTransfer(bytes_io, request)
+ finally:
+ bytes_io.close()
+ request.finish()
+
+ def _extract_mimetype(self, content_type):
+ match = re.compile('([A-Za-z-]+\/[A-Za-z-]+)').search(content_type)
+ return match.group(1)
+
+
+class AttachmentsResource(BaseResource):
+ BASE_URL = 'attachment'
+
+ def __init__(self, services_factory):
+ BaseResource.__init__(self, services_factory)
+
+ def getChild(self, attachment_id, request):
+ _mail_service = self.mail_service(request)
+ return AttachmentResource(_mail_service, attachment_id)
+
+ def render_POST(self, request):
+ _mail_service = self.mail_service(request)
+ fields = cgi.FieldStorage(fp=request.content, headers=(request.getAllHeaders()),
+ environ={'REQUEST_METHOD': 'POST'})
+ _file = fields['attachment']
+ deferred = _mail_service.save_attachment(_file.value, _file.type)
+
+ def send_location(attachment_id):
+ request.headers[
+ 'Location'] = '/%s/%s' % (self.BASE_URL, attachment_id)
+ response_json = {"ident": attachment_id,
+ "content-type": _file.type,
+ "encoding": "base64", # hard coded for now -- not really used
+ "name": _file.filename,
+ "size": len(_file.value)}
+ respond_json_deferred(response_json, request, status_code=201)
+
+ def error_handler(error):
+ logger.error(error)
+ respond_json_deferred(
+ {"message": "Something went wrong. Attachment not saved."}, request, status_code=500)
+
+ deferred.addCallback(send_location)
+ deferred.addErrback(error_handler)
+
+ return NOT_DONE_YET
diff --git a/src/pixelated/resources/auth.py b/src/pixelated/resources/auth.py
new file mode 100644
index 00000000..3497f325
--- /dev/null
+++ b/src/pixelated/resources/auth.py
@@ -0,0 +1,150 @@
+#
+# Copyright (c) 2016 ThoughtWorks, Inc.
+#
+# Pixelated is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Pixelated is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+
+import logging
+
+from leap.auth import SRPAuth
+from leap.exceptions import SRPAuthenticationError
+from twisted.cred.checkers import ANONYMOUS
+from twisted.cred.credentials import ICredentials
+from twisted.cred.error import UnauthorizedLogin
+from twisted.internet import defer, threads
+from twisted.web._auth.wrapper import UnauthorizedResource
+from twisted.web.error import UnsupportedMethod
+from zope.interface import implements, implementer, Attribute
+from twisted.cred import portal, checkers, credentials
+from twisted.web import util
+from twisted.cred import error
+from twisted.web.resource import IResource, ErrorPage
+
+from pixelated.config.leap import authenticate_user
+from pixelated.resources import IPixelatedSession
+
+
+log = logging.getLogger(__name__)
+
+
+@implementer(checkers.ICredentialsChecker)
+class LeapPasswordChecker(object):
+ credentialInterfaces = (
+ credentials.IUsernamePassword,
+ )
+
+ def __init__(self, leap_provider):
+ self._leap_provider = leap_provider
+
+ def requestAvatarId(self, credentials):
+ def _validate_credentials():
+ try:
+ srp_auth = SRPAuth(self._leap_provider.api_uri,
+ self._leap_provider.local_ca_crt)
+ return srp_auth.authenticate(credentials.username, credentials.password)
+ except SRPAuthenticationError:
+ raise UnauthorizedLogin()
+
+ def _get_leap_session(srp_auth):
+ return authenticate_user(self._leap_provider, credentials.username, credentials.password, auth=srp_auth)
+
+ d = threads.deferToThread(_validate_credentials)
+ d.addCallback(_get_leap_session)
+ return d
+
+
+class ISessionCredential(ICredentials):
+
+ request = Attribute('the current request')
+
+
+@implementer(ISessionCredential)
+class SessionCredential(object):
+
+ def __init__(self, request):
+ self.request = request
+
+
+@implementer(checkers.ICredentialsChecker)
+class SessionChecker(object):
+ credentialInterfaces = (ISessionCredential,)
+
+ def __init__(self, services_factory):
+ self._services_factory = services_factory
+
+ def requestAvatarId(self, credentials):
+ session = self.get_session(credentials.request)
+ if session.is_logged_in() and self._services_factory.is_logged_in(session.user_uuid):
+ return defer.succeed(session.user_uuid)
+ return defer.succeed(ANONYMOUS)
+
+ def get_session(self, request):
+ return IPixelatedSession(request.getSession())
+
+
+class PixelatedRealm(object):
+ implements(portal.IRealm)
+
+ def __init__(self, root_resource, anonymous_resource):
+ self._root_resource = root_resource
+ self._anonymous_resource = anonymous_resource
+
+ def requestAvatar(self, avatarId, mind, *interfaces):
+ if IResource in interfaces:
+ return IResource, avatarId, lambda: None
+ raise NotImplementedError()
+
+
+@implementer(IResource)
+class PixelatedAuthSessionWrapper(object):
+
+ isLeaf = False
+
+ def __init__(self, portal, root_resource, anonymous_resource, credentialFactories):
+ self._portal = portal
+ self._credentialFactories = credentialFactories
+ self._root_resource = root_resource
+ self._anonymous_resource = anonymous_resource
+
+ def render(self, request):
+ raise UnsupportedMethod(())
+
+ def getChildWithDefault(self, path, request):
+ request.postpath.insert(0, request.prepath.pop())
+ return self._authorizedResource(request)
+
+ def _authorizedResource(self, request):
+ creds = SessionCredential(request)
+ return util.DeferredResource(self._login(creds))
+
+ def _login(self, credentials):
+ d = self._portal.login(credentials, None, IResource)
+ d.addCallbacks(self._loginSucceeded, self._loginFailed)
+ return d
+
+ def _loginSucceeded(self, args):
+ interface, avatar, logout = args
+ if avatar == checkers.ANONYMOUS:
+ return self._anonymous_resource
+ else:
+ return self._root_resource
+
+ def _loginFailed(self, result):
+ if result.check(error.Unauthorized, error.LoginFailed):
+ return UnauthorizedResource(self._credentialFactories)
+ else:
+ log.err(
+ result,
+ "HTTPAuthSessionWrapper.getChildWithDefault encountered "
+ "unexpected error")
+ return ErrorPage(500, None, None)
diff --git a/src/pixelated/resources/contacts_resource.py b/src/pixelated/resources/contacts_resource.py
new file mode 100644
index 00000000..dc17d1ac
--- /dev/null
+++ b/src/pixelated/resources/contacts_resource.py
@@ -0,0 +1,44 @@
+#
+# Copyright (c) 2014 ThoughtWorks, Inc.
+#
+# Pixelated is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Pixelated is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+
+from pixelated.resources import respond_json_deferred, BaseResource
+from twisted.internet.threads import deferToThread
+from twisted.web import server
+from twisted.web.resource import Resource
+
+
+class ContactsResource(BaseResource):
+
+ isLeaf = True
+
+ def __init__(self, services_factory):
+ BaseResource.__init__(self, services_factory)
+
+ def render_GET(self, request):
+ _search_engine = self.search_engine(request)
+ query = request.args.get('q', [''])[-1]
+ d = deferToThread(lambda: _search_engine.contacts(query))
+ d.addCallback(lambda tags: respond_json_deferred(tags, request))
+
+ def handle_error(error):
+ print 'Something went wrong'
+ import traceback
+ traceback.print_exc()
+ print error
+
+ d.addErrback(handle_error)
+
+ return server.NOT_DONE_YET
diff --git a/src/pixelated/resources/features_resource.py b/src/pixelated/resources/features_resource.py
new file mode 100644
index 00000000..f2abdb9a
--- /dev/null
+++ b/src/pixelated/resources/features_resource.py
@@ -0,0 +1,52 @@
+#
+# Copyright (c) 2014 ThoughtWorks, Inc.
+#
+# Pixelated is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Pixelated is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+
+from pixelated.resources import respond_json
+import os
+from twisted.web.resource import Resource
+
+from pixelated.resources.logout_resource import LogoutResource
+
+
+class FeaturesResource(Resource):
+ DISABLED_FEATURES = ['draftReply']
+ isLeaf = True
+
+ def __init__(self, multi_user=False):
+ Resource.__init__(self)
+ self._multi_user = multi_user
+
+ def render_GET(self, request):
+ dispatcher_features = self._dispatcher_features()
+ disabled_features = self._disabled_features()
+ return respond_json(
+ {'disabled_features': disabled_features, 'dispatcher_features': dispatcher_features}, request)
+
+ def _dispatcher_features(self):
+ dispatcher_features = {}
+ if os.environ.get('DISPATCHER_LOGOUT_URL'):
+ dispatcher_features['logout'] = os.environ.get(
+ 'DISPATCHER_LOGOUT_URL')
+ if self._multi_user:
+ dispatcher_features['logout'] = '/%s' % LogoutResource.BASE_URL
+ return dispatcher_features
+
+ def _disabled_features(self):
+ disabled_features = [
+ default_disabled_feature for default_disabled_feature in self.DISABLED_FEATURES]
+ if os.environ.get('FEEDBACK_URL') is None:
+ disabled_features.append('feedback')
+ return disabled_features
diff --git a/src/pixelated/resources/feedback_resource.py b/src/pixelated/resources/feedback_resource.py
new file mode 100644
index 00000000..aeead401
--- /dev/null
+++ b/src/pixelated/resources/feedback_resource.py
@@ -0,0 +1,31 @@
+#
+# Copyright (c) 2015 ThoughtWorks, Inc.
+#
+# Pixelated is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Pixelated is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+import json
+
+from pixelated.resources import respond_json, BaseResource
+
+
+class FeedbackResource(BaseResource):
+ isLeaf = True
+
+ def __init__(self, services_factory):
+ BaseResource.__init__(self, services_factory)
+
+ def render_POST(self, request):
+ _feedback_service = self.feedback_service(request)
+ feedback = json.loads(request.content.read()).get('feedback')
+ _feedback_service.open_ticket(feedback)
+ return respond_json({}, request)
diff --git a/src/pixelated/resources/keys_resource.py b/src/pixelated/resources/keys_resource.py
new file mode 100644
index 00000000..9075ab9e
--- /dev/null
+++ b/src/pixelated/resources/keys_resource.py
@@ -0,0 +1,30 @@
+from email.utils import parseaddr
+from pixelated.resources import respond_json_deferred, BaseResource
+from twisted.web import server
+
+
+class KeysResource(BaseResource):
+
+ isLeaf = True
+
+ def __init__(self, services_factory):
+ BaseResource.__init__(self, services_factory)
+
+ def render_GET(self, request):
+ _keymanager = self.keymanager(request)
+
+ def finish_request(key):
+ if key.private:
+ respond_json_deferred(None, request, status_code=401)
+ else:
+ respond_json_deferred(key.get_active_json(), request)
+
+ def key_not_found(_):
+ respond_json_deferred(None, request, status_code=404)
+
+ _, key_to_find = parseaddr(request.args.get('search')[0])
+ d = _keymanager.fetch_key(key_to_find)
+ d.addCallback(finish_request)
+ d.addErrback(key_not_found)
+
+ return server.NOT_DONE_YET
diff --git a/src/pixelated/resources/login_resource.py b/src/pixelated/resources/login_resource.py
new file mode 100644
index 00000000..fd09976b
--- /dev/null
+++ b/src/pixelated/resources/login_resource.py
@@ -0,0 +1,182 @@
+#
+# Copyright (c) 2016 ThoughtWorks, Inc.
+#
+# Pixelated is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Pixelated is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+
+import logging
+import os
+from xml.sax import SAXParseException
+
+from twisted.cred import credentials
+from twisted.internet import defer
+from twisted.python.filepath import FilePath
+from twisted.web import util
+from twisted.web.http import UNAUTHORIZED, OK
+from twisted.web.resource import IResource, NoResource
+from twisted.web.server import NOT_DONE_YET
+from twisted.web.static import File
+from twisted.web.template import Element, XMLFile, renderElement, renderer
+
+from pixelated.adapter.welcome_mail import add_welcome_mail
+from pixelated.resources import BaseResource, UnAuthorizedResource, IPixelatedSession
+
+log = logging.getLogger(__name__)
+
+
+def _get_startup_folder():
+ path = os.path.dirname(os.path.abspath(__file__))
+ return os.path.join(path, '..', 'assets')
+
+
+def _get_static_folder():
+ static_folder = os.path.abspath(os.path.join(
+ os.path.abspath(__file__), "..", "..", "..", "web-ui", "app"))
+ # this is a workaround for packaging
+ if not os.path.exists(static_folder):
+ static_folder = os.path.abspath(
+ os.path.join(os.path.abspath(__file__), "..", "..", "..", "..", "web-ui", "app"))
+ if not os.path.exists(static_folder):
+ static_folder = os.path.join(
+ '/', 'usr', 'share', 'pixelated-user-agent')
+ return static_folder
+
+
+class DisclaimerElement(Element):
+ loader = XMLFile(FilePath(os.path.join(
+ _get_startup_folder(), '_login_disclaimer_banner.html')))
+
+ def __init__(self, banner):
+ super(DisclaimerElement, self).__init__()
+ self._set_loader(banner)
+ self._banner_filename = banner or "_login_disclaimer_banner.html"
+
+ def _set_loader(self, banner):
+ if banner:
+ current_path = os.path.dirname(os.path.abspath(__file__))
+ banner_file_path = os.path.join(
+ current_path, "..", "..", "..", banner)
+ self.loader = XMLFile(FilePath(banner_file_path))
+
+ def render(self, request):
+ try:
+ return super(DisclaimerElement, self).render(request)
+ except SAXParseException:
+ return ["Invalid XML template format for %s." % self._banner_filename]
+ except IOError:
+ return ["Disclaimer banner file %s could not be read or does not exit." % self._banner_filename]
+
+
+class LoginWebSite(Element):
+ loader = XMLFile(FilePath(os.path.join(
+ _get_startup_folder(), 'login.html')))
+
+ def __init__(self, error_msg=None, disclaimer_banner_file=None):
+ super(LoginWebSite, self).__init__()
+ self._error_msg = error_msg
+ self.disclaimer_banner_file = disclaimer_banner_file
+
+ @renderer
+ def error_msg(self, request, tag):
+ if self._error_msg is not None:
+ return tag(self._error_msg)
+ return tag('')
+
+ @renderer
+ def disclaimer(self, request, tag):
+ return DisclaimerElement(self.disclaimer_banner_file).render(request)
+
+
+class LoginResource(BaseResource):
+ BASE_URL = 'login'
+
+ def __init__(self, services_factory, portal=None, disclaimer_banner=None):
+ BaseResource.__init__(self, services_factory)
+ self._static_folder = _get_static_folder()
+ self._startup_folder = _get_startup_folder()
+ self._portal = portal
+ self._disclaimer_banner = disclaimer_banner
+ self.putChild('startup-assets', File(self._startup_folder))
+
+ def set_portal(self, portal):
+ self._portal = portal
+
+ def getChild(self, path, request):
+ if path == '':
+ return self
+ if path == 'login':
+ return self
+ if not self.is_logged_in(request):
+ return UnAuthorizedResource()
+ return NoResource()
+
+ def render_GET(self, request):
+ request.setResponseCode(OK)
+ return self._render_template(request)
+
+ def _render_template(self, request, error_msg=None):
+ site = LoginWebSite(error_msg=error_msg,
+ disclaimer_banner_file=self._disclaimer_banner)
+ return renderElement(request, site)
+
+ def render_POST(self, request):
+ if self.is_logged_in(request):
+ return util.redirectTo("/", request)
+
+ def render_response(leap_session):
+ request.setResponseCode(OK)
+ request.write(
+ open(os.path.join(self._startup_folder, 'Interstitial.html')).read())
+ request.finish()
+ self._setup_user_services(leap_session, request)
+
+ def render_error(error):
+ log.info('Login Error for %s' % request.args['username'][0])
+ log.info('%s' % error)
+ request.setResponseCode(UNAUTHORIZED)
+ return self._render_template(request, 'Invalid credentials')
+
+ d = self._handle_login(request)
+ d.addCallbacks(render_response, render_error)
+ d.addErrback(self.generic_error_handling, request)
+
+ return NOT_DONE_YET
+
+ @defer.inlineCallbacks
+ def _handle_login(self, request):
+ self.creds = self._get_creds_from(request)
+ iface, leap_session, logout = yield self._portal.login(self.creds, None, IResource)
+ defer.returnValue(leap_session)
+
+ def _get_creds_from(self, request):
+ username = request.args['username'][0]
+ password = request.args['password'][0]
+ return credentials.UsernamePassword(username, password)
+
+ @defer.inlineCallbacks
+ def _setup_user_services(self, leap_session, request):
+ user_id = leap_session.user_auth.uuid
+ if not self._services_factory.is_logged_in(user_id):
+ yield self._services_factory.create_services_from(leap_session)
+ self._init_http_session(request, user_id)
+
+ @defer.inlineCallbacks
+ def _initialize_services(self, leap_session):
+ yield self._services_factory.create_services_from(leap_session)
+
+ if leap_session.fresh_account:
+ yield add_welcome_mail(leap_session.mail_store)
+
+ def _init_http_session(self, request, user_id):
+ session = IPixelatedSession(request.getSession())
+ session.user_uuid = user_id
diff --git a/src/pixelated/resources/logout_resource.py b/src/pixelated/resources/logout_resource.py
new file mode 100644
index 00000000..c22815ce
--- /dev/null
+++ b/src/pixelated/resources/logout_resource.py
@@ -0,0 +1,45 @@
+#
+# Copyright (c) 2014 ThoughtWorks, Inc.
+#
+# Pixelated is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Pixelated is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+
+from twisted.internet import defer
+from twisted.web import util
+from twisted.web.server import NOT_DONE_YET
+
+from pixelated.resources import BaseResource
+from pixelated.resources.login_resource import LoginResource
+
+
+class LogoutResource(BaseResource):
+ BASE_URL = "logout"
+ isLeaf = True
+
+ @defer.inlineCallbacks
+ def _execute_logout(self, request):
+ session = self.get_session(request)
+ yield self._services_factory.log_out_user(session.user_uuid)
+ session.expire()
+
+ def render_POST(self, request):
+ def _redirect_to_login(_):
+ content = util.redirectTo("/%s" % LoginResource.BASE_URL, request)
+ request.write(content)
+ request.finish()
+
+ d = self._execute_logout(request)
+ d.addCallback(_redirect_to_login)
+ d.addErrback(self.generic_error_handling, request)
+
+ return NOT_DONE_YET
diff --git a/src/pixelated/resources/mail_resource.py b/src/pixelated/resources/mail_resource.py
new file mode 100644
index 00000000..15db37cb
--- /dev/null
+++ b/src/pixelated/resources/mail_resource.py
@@ -0,0 +1,79 @@
+import json
+
+from twisted.python.log import err
+from twisted.web.resource import Resource
+from twisted.web.server import NOT_DONE_YET
+
+from pixelated.resources import respond_json_deferred, BaseResource, GenericDeferredErrorHandler
+from pixelated.support import replier
+
+
+class MailTags(Resource):
+
+ isLeaf = True
+
+ def __init__(self, mail_id, mail_service):
+ Resource.__init__(self)
+ self._mail_service = mail_service
+ self._mail_id = mail_id
+
+ def render_POST(self, request):
+ new_tags = json.loads(request.content.read()).get('newtags')
+
+ d = self._mail_service.update_tags(self._mail_id, new_tags)
+ d.addCallback(lambda mail: respond_json_deferred(
+ mail.as_dict(), request))
+
+ def handle403(failure):
+ failure.trap(ValueError)
+ return respond_json_deferred(failure.getErrorMessage(), request, 403)
+ d.addErrback(handle403)
+ return NOT_DONE_YET
+
+
+class Mail(Resource, GenericDeferredErrorHandler):
+
+ def __init__(self, mail_id, mail_service):
+ Resource.__init__(self)
+ self.putChild('tags', MailTags(mail_id, mail_service))
+ self._mail_id = mail_id
+ self._mail_service = mail_service
+
+ def render_GET(self, request):
+ def populate_reply(mail):
+ mail_dict = mail.as_dict()
+ current_user = self._mail_service.account_email
+ sender = mail.headers.get('Reply-to', mail.headers.get('From'))
+ to = mail.headers.get('To', [])
+ ccs = mail.headers.get('Cc', [])
+ mail_dict['replying'] = replier.generate_recipients(
+ sender, to, ccs, current_user)
+ return mail_dict
+
+ d = self._mail_service.mail(self._mail_id)
+ d.addCallback(lambda mail: populate_reply(mail))
+ d.addCallback(
+ lambda mail_dict: respond_json_deferred(mail_dict, request))
+ d.addErrback(self.generic_error_handling, request)
+
+ return NOT_DONE_YET
+
+ def render_DELETE(self, request):
+ def response_failed(failure):
+ err(failure, 'something failed')
+ request.finish()
+
+ d = self._mail_service.delete_mail(self._mail_id)
+ d.addCallback(lambda _: respond_json_deferred(None, request))
+ d.addErrback(response_failed)
+ return NOT_DONE_YET
+
+
+class MailResource(BaseResource):
+
+ def __init__(self, services_factory):
+ BaseResource.__init__(self, services_factory)
+
+ def getChild(self, mail_id, request):
+ _mail_service = self.mail_service(request)
+ return Mail(mail_id, _mail_service)
diff --git a/src/pixelated/resources/mails_resource.py b/src/pixelated/resources/mails_resource.py
new file mode 100644
index 00000000..6ae08eb5
--- /dev/null
+++ b/src/pixelated/resources/mails_resource.py
@@ -0,0 +1,239 @@
+import time
+import json
+import logging
+from pixelated.adapter.services.mail_sender import SMTPDownException
+from pixelated.adapter.model.mail import InputMail
+from twisted.web.server import NOT_DONE_YET
+from pixelated.resources import respond_json_deferred, BaseResource
+from twisted.web.resource import Resource
+from twisted.web import server
+from twisted.internet import defer
+from twisted.python.log import err
+from leap.common import events
+
+from pixelated.support.functional import to_unicode
+
+
+log = logging.getLogger(__name__)
+
+
+class MailsUnreadResource(Resource):
+ isLeaf = True
+
+ def __init__(self, mail_service):
+ Resource.__init__(self)
+ self._mail_service = mail_service
+
+ def render_POST(self, request):
+ idents = json.load(request.content).get('idents')
+ deferreds = []
+ for ident in idents:
+ deferreds.append(self._mail_service.mark_as_unread(ident))
+
+ d = defer.gatherResults(deferreds, consumeErrors=True)
+ d.addCallback(lambda _: respond_json_deferred(None, request))
+ d.addErrback(lambda _: respond_json_deferred(
+ None, request, status_code=500))
+
+ return NOT_DONE_YET
+
+
+class MailsReadResource(Resource):
+ isLeaf = True
+
+ def __init__(self, mail_service):
+ Resource.__init__(self)
+ self._mail_service = mail_service
+
+ def render_POST(self, request):
+ idents = json.load(request.content).get('idents')
+ deferreds = []
+ for ident in idents:
+ deferreds.append(self._mail_service.mark_as_read(ident))
+
+ d = defer.gatherResults(deferreds, consumeErrors=True)
+ d.addCallback(lambda _: respond_json_deferred(None, request))
+ d.addErrback(lambda _: respond_json_deferred(
+ None, request, status_code=500))
+
+ return NOT_DONE_YET
+
+
+class MailsDeleteResource(Resource):
+ isLeaf = True
+
+ def __init__(self, mail_service):
+ Resource.__init__(self)
+ self._mail_service = mail_service
+
+ def render_POST(self, request):
+ def response_failed(failure):
+ err(failure, 'something failed')
+ request.finish()
+
+ idents = json.loads(request.content.read())['idents']
+ deferreds = []
+ for ident in idents:
+ deferreds.append(self._mail_service.delete_mail(ident))
+
+ d = defer.gatherResults(deferreds, consumeErrors=True)
+ d.addCallback(lambda _: respond_json_deferred(None, request))
+ d.addErrback(response_failed)
+ return NOT_DONE_YET
+
+
+class MailsRecoverResource(Resource):
+ isLeaf = True
+
+ def __init__(self, mail_service):
+ Resource.__init__(self)
+ self._mail_service = mail_service
+
+ def render_POST(self, request):
+ idents = json.loads(request.content.read())['idents']
+ deferreds = []
+ for ident in idents:
+ deferreds.append(self._mail_service.recover_mail(ident))
+ d = defer.gatherResults(deferreds, consumeErrors=True)
+ d.addCallback(lambda _: respond_json_deferred(None, request))
+ d.addErrback(lambda _: respond_json_deferred(
+ None, request, status_code=500))
+ return NOT_DONE_YET
+
+
+class MailsArchiveResource(Resource):
+ isLeaf = True
+
+ def __init__(self, mail_service):
+ Resource.__init__(self)
+ self._mail_service = mail_service
+
+ def render_POST(self, request):
+ idents = json.loads(request.content.read())['idents']
+ deferreds = []
+ for ident in idents:
+ deferreds.append(self._mail_service.archive_mail(ident))
+ d = defer.gatherResults(deferreds, consumeErrors=True)
+ d.addCallback(lambda _: respond_json_deferred(
+ {'successMessage': 'Your message was archived'}, request))
+ d.addErrback(lambda _: respond_json_deferred(
+ None, request, status_code=500))
+ return NOT_DONE_YET
+
+
+class MailsResource(BaseResource):
+
+ def _register_smtp_error_handler(self):
+
+ def on_error(event, content):
+ delivery_error_mail = InputMail.delivery_error_template(
+ delivery_address=event.content)
+ self._mail_service.mailboxes.inbox.add(delivery_error_mail)
+
+ events.register(events.catalog.SMTP_SEND_MESSAGE_ERROR,
+ callback=on_error)
+
+ def __init__(self, services_factory):
+ BaseResource.__init__(self, services_factory)
+ self._register_smtp_error_handler()
+
+ def getChild(self, action, request):
+ _mail_service = self.mail_service(request)
+
+ if action == 'delete':
+ return MailsDeleteResource(_mail_service)
+ if action == 'recover':
+ return MailsRecoverResource(_mail_service)
+ if action == 'archive':
+ return MailsArchiveResource(_mail_service)
+ if action == 'read':
+ return MailsReadResource(_mail_service)
+ if action == 'unread':
+ return MailsUnreadResource(_mail_service)
+
+ def _build_mails_response(self, (mails, total)):
+ return {
+ "stats": {
+ "total": total,
+ },
+ "mails": [mail.as_dict() for mail in mails]
+ }
+
+ def render_GET(self, request):
+
+ _mail_service = self.mail_service(request)
+ query, window_size, page = request.args.get('q')[0], request.args.get('w')[
+ 0], request.args.get('p')[0]
+ unicode_query = to_unicode(query)
+ d = _mail_service.mails(unicode_query, window_size, page)
+
+ d.addCallback(self._build_mails_response)
+ d.addCallback(lambda res: respond_json_deferred(res, request))
+
+ def error_handler(error):
+ print error
+
+ d.addErrback(error_handler)
+
+ return NOT_DONE_YET
+
+ def render_POST(self, request):
+ def onError(error):
+ if isinstance(error.value, SMTPDownException):
+ respond_json_deferred(
+ {'message': str(error.value)}, request, status_code=503)
+ else:
+ err(error, 'error occurred while sending')
+ respond_json_deferred(
+ {'message': 'an error occurred while sending'}, request, status_code=422)
+
+ deferred = self._handle_post(request)
+ deferred.addErrback(onError)
+
+ return server.NOT_DONE_YET
+
+ def render_PUT(self, request):
+ def onError(error):
+ err(error, 'error saving draft')
+ respond_json_deferred("", request, status_code=422)
+
+ deferred = self._handle_put(request)
+ deferred.addErrback(onError)
+
+ return server.NOT_DONE_YET
+
+ @defer.inlineCallbacks
+ def _fetch_attachment_contents(self, content_dict, _mail_service):
+ attachments = content_dict.get(
+ 'attachments', []) if content_dict else []
+ for attachment in attachments:
+ retrieved_attachment = yield _mail_service.attachment(attachment['ident'])
+ attachment['raw'] = retrieved_attachment['content']
+ content_dict['attachments'] = attachments
+ defer.returnValue(content_dict)
+
+ @defer.inlineCallbacks
+ def _handle_post(self, request):
+ _mail_service = self.mail_service(request)
+ content_dict = json.loads(request.content.read())
+ with_attachment_content = yield self._fetch_attachment_contents(content_dict, _mail_service)
+
+ sent_mail = yield _mail_service.send_mail(with_attachment_content)
+ respond_json_deferred(sent_mail.as_dict(), request, status_code=201)
+
+ @defer.inlineCallbacks
+ def _handle_put(self, request):
+ _draft_service = self.draft_service(request)
+ _mail_service = self.mail_service(request)
+ content_dict = json.loads(request.content.read())
+ with_attachment_content = yield self._fetch_attachment_contents(content_dict, _mail_service)
+
+ _mail = InputMail.from_dict(
+ with_attachment_content, from_address=_mail_service.account_email)
+ draft_id = content_dict.get('ident')
+ pixelated_mail = yield _draft_service.process_draft(draft_id, _mail)
+
+ if not pixelated_mail:
+ respond_json_deferred("", request, status_code=422)
+ else:
+ respond_json_deferred({'ident': pixelated_mail.ident}, request)
diff --git a/src/pixelated/resources/root_resource.py b/src/pixelated/resources/root_resource.py
new file mode 100644
index 00000000..f3a41d57
--- /dev/null
+++ b/src/pixelated/resources/root_resource.py
@@ -0,0 +1,150 @@
+#
+# Copyright (c) 2016 ThoughtWorks, Inc.
+#
+# Pixelated is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Pixelated is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+import hashlib
+import json
+import os
+from string import Template
+
+from pixelated.resources import BaseResource, UnAuthorizedResource
+from pixelated.resources.attachments_resource import AttachmentsResource
+from pixelated.resources.sandbox_resource import SandboxResource
+from pixelated.resources.contacts_resource import ContactsResource
+from pixelated.resources.features_resource import FeaturesResource
+from pixelated.resources.feedback_resource import FeedbackResource
+from pixelated.resources.login_resource import LoginResource
+from pixelated.resources.logout_resource import LogoutResource
+from pixelated.resources.user_settings_resource import UserSettingsResource
+from pixelated.resources.mail_resource import MailResource
+from pixelated.resources.mails_resource import MailsResource
+from pixelated.resources.tags_resource import TagsResource
+from pixelated.resources.keys_resource import KeysResource
+from twisted.web.static import File
+
+CSRF_TOKEN_LENGTH = 32
+
+MODE_STARTUP = 1
+MODE_RUNNING = 2
+
+
+class RootResource(BaseResource):
+
+ def __init__(self, services_factory, static_folder=None):
+ BaseResource.__init__(self, services_factory)
+ self._startup_assets_folder = self._get_startup_folder()
+ self._static_folder = static_folder or self._get_static_folder()
+ self._html_template = open(os.path.join(
+ self._static_folder, 'index.html')).read()
+ self._services_factory = services_factory
+ self._child_resources = ChildResourcesMap()
+ self._startup_mode()
+
+ def _startup_mode(self):
+ self.putChild('startup-assets', File(self._startup_assets_folder))
+ self._mode = MODE_STARTUP
+
+ def getChild(self, path, request):
+ if path == '':
+ return self
+ if self._is_xsrf_valid(request):
+ return self._child_resources.get(path)
+ return UnAuthorizedResource()
+
+ def _is_xsrf_valid(self, request):
+ xsrf_token = request.getCookie('XSRF-TOKEN')
+
+ ajax_request = (request.getHeader(
+ 'x-requested-with') == 'XMLHttpRequest')
+ if ajax_request:
+ xsrf_header = request.getHeader('x-xsrf-token')
+ return xsrf_header and xsrf_header == xsrf_token
+
+ get_request = (request.method == 'GET')
+ if get_request:
+ return True
+
+ csrf_input = request.args.get('csrftoken', [None])[0] or json.loads(
+ request.content.read()).get('csrftoken', [None])[0]
+ return csrf_input and csrf_input == xsrf_token
+
+ def initialize(self, portal=None, disclaimer_banner=None):
+ self._child_resources.add(
+ 'sandbox', SandboxResource(self._static_folder))
+ self._child_resources.add('assets', File(self._static_folder))
+ self._child_resources.add('keys', KeysResource(self._services_factory))
+ self._child_resources.add(
+ AttachmentsResource.BASE_URL, AttachmentsResource(self._services_factory))
+ self._child_resources.add(
+ 'contacts', ContactsResource(self._services_factory))
+ self._child_resources.add('features', FeaturesResource(portal))
+ self._child_resources.add('tags', TagsResource(self._services_factory))
+ self._child_resources.add(
+ 'mails', MailsResource(self._services_factory))
+ self._child_resources.add('mail', MailResource(self._services_factory))
+ self._child_resources.add(
+ 'feedback', FeedbackResource(self._services_factory))
+ self._child_resources.add(
+ 'user-settings', UserSettingsResource(self._services_factory))
+ self._child_resources.add(LoginResource.BASE_URL,
+ LoginResource(self._services_factory, portal, disclaimer_banner=disclaimer_banner))
+ self._child_resources.add(
+ LogoutResource.BASE_URL, LogoutResource(self._services_factory))
+
+ self._mode = MODE_RUNNING
+
+ def _get_startup_folder(self):
+ path = os.path.dirname(os.path.abspath(__file__))
+ return os.path.join(path, '..', 'assets')
+
+ def _get_static_folder(self):
+ static_folder = os.path.abspath(os.path.join(
+ os.path.abspath(__file__), "..", "..", "..", "web-ui", "app"))
+ # this is a workaround for packaging
+ if not os.path.exists(static_folder):
+ static_folder = os.path.abspath(
+ os.path.join(os.path.abspath(__file__), "..", "..", "..", "..", "web-ui", "app"))
+ if not os.path.exists(static_folder):
+ static_folder = os.path.join(
+ '/', 'usr', 'share', 'pixelated-user-agent')
+ return static_folder
+
+ def _is_starting(self):
+ return self._mode == MODE_STARTUP
+
+ def _add_csrf_cookie(self, request):
+ csrf_token = hashlib.sha256(os.urandom(CSRF_TOKEN_LENGTH)).hexdigest()
+ request.addCookie('XSRF-TOKEN', csrf_token)
+
+ def render_GET(self, request):
+ self._add_csrf_cookie(request)
+ if self._is_starting():
+ return open(os.path.join(self._startup_assets_folder, 'Interstitial.html')).read()
+ else:
+ account_email = self.mail_service(request).account_email
+ response = Template(self._html_template).safe_substitute(
+ account_email=account_email)
+ return str(response)
+
+
+class ChildResourcesMap(object):
+
+ def __init__(self):
+ self._registry = {}
+
+ def add(self, path, resource):
+ self._registry[path] = resource
+
+ def get(self, path):
+ return self._registry.get(path)
diff --git a/src/pixelated/resources/sandbox_resource.py b/src/pixelated/resources/sandbox_resource.py
new file mode 100644
index 00000000..35f99774
--- /dev/null
+++ b/src/pixelated/resources/sandbox_resource.py
@@ -0,0 +1,37 @@
+#
+# Copyright (c) 2016 ThoughtWorks, Inc.
+#
+# Pixelated is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Pixelated is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+
+from twisted.web.static import File
+
+
+class SandboxResource(File):
+ CSP_HEADER_VALUES = "sandbox allow-popups allow-scripts;" \
+ "default-src 'self';" \
+ "style-src *;" \
+ "script-src *;" \
+ "font-src *;" \
+ "img-src *;" \
+ "object-src 'none';" \
+ "connect-src 'none';"
+
+ def render_GET(self, request):
+ request.setHeader('Content-Security-Policy', self.CSP_HEADER_VALUES)
+ request.setHeader('X-Content-Security-Policy', self.CSP_HEADER_VALUES)
+ request.setHeader('X-Webkit-CSP', self.CSP_HEADER_VALUES)
+ request.setHeader('Access-Control-Allow-Origin', '*')
+ request.setHeader('Access-Control-Allow-Methods', 'GET')
+
+ return super(SandboxResource, self).render_GET(request)
diff --git a/src/pixelated/resources/session.py b/src/pixelated/resources/session.py
new file mode 100644
index 00000000..9ade8d29
--- /dev/null
+++ b/src/pixelated/resources/session.py
@@ -0,0 +1,39 @@
+#
+# Copyright (c) 2016 ThoughtWorks, Inc.
+#
+# Pixelated is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Pixelated is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+
+from zope.interface import Interface, Attribute, implements
+from twisted.python.components import registerAdapter
+from twisted.web.server import Session
+
+
+class IPixelatedSession(Interface):
+ user_uuid = Attribute('The uuid of the currently logged in user')
+
+
+class PixelatedSession(object):
+ implements(IPixelatedSession)
+
+ def __init__(self, session):
+ self.user_uuid = None
+
+ def is_logged_in(self):
+ return self.user_uuid is not None
+
+ def expire(self):
+ self.user_uuid = None
+
+
+registerAdapter(PixelatedSession, Session, IPixelatedSession)
diff --git a/src/pixelated/resources/tags_resource.py b/src/pixelated/resources/tags_resource.py
new file mode 100644
index 00000000..fa5982cb
--- /dev/null
+++ b/src/pixelated/resources/tags_resource.py
@@ -0,0 +1,39 @@
+#
+# Copyright (c) 2014 ThoughtWorks, Inc.
+#
+# Pixelated is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Pixelated is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+
+from pixelated.resources import respond_json_deferred, BaseResource
+from twisted.internet.threads import deferToThread
+from twisted.web.server import NOT_DONE_YET
+
+
+class TagsResource(BaseResource):
+
+ isLeaf = True
+
+ def __init__(self, services_factory):
+ BaseResource.__init__(self, services_factory)
+
+ def render_GET(self, request):
+ _search_engine = self.search_engine(request)
+ query = request.args.get('q', [''])[0]
+ skip_default_tags = request.args.get('skipDefaultTags', [False])[0]
+
+ d = deferToThread(lambda: _search_engine.tags(
+ query=query, skip_default_tags=skip_default_tags))
+ d.addCallback(lambda tags: respond_json_deferred(tags, request))
+ d.addErrback(self.generic_error_handling, request)
+
+ return NOT_DONE_YET
diff --git a/src/pixelated/resources/user_settings_resource.py b/src/pixelated/resources/user_settings_resource.py
new file mode 100644
index 00000000..7420a406
--- /dev/null
+++ b/src/pixelated/resources/user_settings_resource.py
@@ -0,0 +1,45 @@
+#
+# Copyright (c) 2015 ThoughtWorks, Inc.
+#
+# Pixelated is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Pixelated is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+
+from pixelated.resources import respond_json_deferred, BaseResource
+from twisted.web import server
+
+FINGERPRINT_NOT_FOUND = 'Fingerprint not found'
+
+
+class UserSettingsResource(BaseResource):
+ isLeaf = True
+
+ def __init__(self, services_factory):
+ BaseResource.__init__(self, services_factory)
+
+ def render_GET(self, request):
+ _account_email = self.mail_service(request).account_email
+
+ def finish_request(key):
+ _fingerprint = key.fingerprint
+ respond_json_deferred(
+ {'account_email': _account_email, 'fingerprint': _fingerprint}, request)
+
+ def key_not_found(_):
+ respond_json_deferred(
+ {'account_email': _account_email, 'fingerprint': FINGERPRINT_NOT_FOUND}, request)
+
+ d = self.keymanager(request).fetch_key(_account_email)
+ d.addCallback(finish_request)
+ d.addErrback(key_not_found)
+
+ return server.NOT_DONE_YET