def __init__(self,
                 application,
                 port,
                 content_db_query,
                 *args,
                 middlewares=None,
                 **kwargs):
        '''Initialize the service and create webserver on port.

        :content_db_query: is a function satisfying the signature
                           def content_db_query(app_id: str,
                                                query: dict,
                                                callback: GAsyncReadyCallback)
                           which can be used by a route to query a content
                           database of some sort for an app_id.

        '''
        super().__init__(*args, **kwargs)

        # Create the server now and start listening.
        #
        # We want to listen right away as we'll probably be started by
        # socket activation
        self._cache = EosCompanionAppService.ManagedCache()
        self._monitors = configure_drop_cache_on_changes(self._cache)
        self._server = create_companion_app_webserver(application,
                                                      self._cache,
                                                      content_db_query,
                                                      middlewares=middlewares)
        EosCompanionAppService.soup_server_listen_on_sd_fd_or_port(self._server,
                                                                   port,
                                                                   0)
def _render_license_css_content(content_bytes, version, query, source_path):
    '''Render the CSS license content by rewriting all the URIs.'''
    unrendered_css_string = EosCompanionAppService.bytes_to_string(
        content_bytes)
    return EosCompanionAppService.string_to_bytes(
        _RE_CSS_URL_CAPTURE.sub(
            relative_css_url_rewriter(version, query, source_path),
            unrendered_css_string))
def not_found_response(msg, path):
    '''Respond with an error message and 404.'''
    msg.set_status(Soup.Status.NOT_FOUND)
    error = serialize_error_as_json_object(
        EosCompanionAppService.error_quark(),
        EosCompanionAppService.Error.INVALID_REQUEST,
        detail={'invalid_path': path})
    EosCompanionAppService.set_soup_message_response(
        msg, 'application/json', json.dumps({
            'status': 'error',
            'error': error
        }))
        def _on_rendered_wrapper(error, rendered_page):
            '''Called when rendering the wrapper is complete.'''
            if error is not None:
                callback(error, None)
                return

            try:
                response_bytes = EosCompanionAppService.string_to_bytes(
                    pipeline(
                        rendered_page,
                        lambda content: _RE_EKN_URL_CAPTURE.sub(
                            ekn_url_rewriter(version, query),
                            content
                        ),
                        lambda content: _RE_RESOURCE_URL_CAPTURE.sub(
                            resource_url_rewriter(version, query),
                            content
                        ),
                        lambda content: _RE_LICENSE_URL_CAPTURE.sub(
                            license_url_rewriter(version, query),
                            content
                        )
                    )
                )
            except GLib.Error as render_bytes_error:
                callback(render_bytes_error, None)
                return

            callback(None, response_bytes)
Exemple #5
0
        def _internal(_load_app_info_callback):
            def _on_got_application_info(_, result):
                '''Marshal the error and result into a tuple.'''
                try:
                    info = application_listing_from_app_info(
                        EosCompanionAppService.finish_load_application_info(
                            result))
                except GLib.Error as error:
                    _load_app_info_callback(error, None)
                    return

                _load_app_info_callback(None, info)

            application_id = desktop_id_to_app_id(desktop_id)
            EosCompanionAppService.load_application_info(
                application_id, cache, cancellable, _on_got_application_info)
Exemple #6
0
def list_all_applications(cache, cancellable, callback):
    '''Convenience function to pass list of ApplicationListing to callback.'''
    def _callback(_, result):
        '''Callback function that gets called when we are done.'''
        try:
            infos = EosCompanionAppService.finish_list_application_infos(
                result)
        except GLib.Error as error:
            callback(error, None)
            return

        callback(None, [
            application_listing_from_app_info(app_info) for app_info in infos
        ])

    EosCompanionAppService.list_application_infos(cache, cancellable,
                                                  _callback)
    def _callback(_, result):
        '''Marshal the GAsyncReady callback into an (error, data) callback.'''
        try:
            bytes_data = EosCompanionAppService.finish_load_all_in_stream_to_bytes(
                result)
        except GLib.Error as error:
            callback(error, None)
            return

        callback(None, bytes_data)
Exemple #8
0
            def _on_got_application_info(_, result):
                '''Marshal the error and result into a tuple.'''
                try:
                    info = application_listing_from_app_info(
                        EosCompanionAppService.finish_load_application_info(
                            result))
                except GLib.Error as error:
                    _load_app_info_callback(error, None)
                    return

                _load_app_info_callback(None, info)
    def _read_stream_callback(_, result):
        '''Callback once we have finished loading the stream to bytes.'''
        try:
            content_bytes = EosCompanionAppService.finish_load_all_in_stream_to_bytes(
                result)
        except GLib.Error as error:
            callback(error, None)
            return

        adjuster.render_async(content_type, content_bytes, version, query,
                              cache, cancellable, _content_adjusted_callback)
Exemple #10
0
    def _callback(_, result):
        '''Callback function that gets called when we are done.'''
        try:
            infos = EosCompanionAppService.finish_list_application_infos(
                result)
        except GLib.Error as error:
            callback(error, None)
            return

        callback(None, [
            application_listing_from_app_info(app_info) for app_info in infos
        ])
def load_record_from_shards_async(shards, content_id, attr, callback):
    '''Load bytes from stream for app and content_id.

    :attr: must be one of 'data' or 'metadata'.

    Once loading is complete, callback will be invoked with a GAsyncResult,
    use EosCompanionAppService.finish_load_all_in_stream_to_bytes
    to get the result or handle the corresponding error.

    Returns LOAD_FROM_ENGINE_SUCCESS if a stream could be loaded,
    LOAD_FROM_ENGINE_NO_SUCH_CONTENT if the content wasn't found.
    '''
    def _callback(_, result):
        '''Marshal the GAsyncReady callback into an (error, data) callback.'''
        try:
            bytes_data = EosCompanionAppService.finish_load_all_in_stream_to_bytes(
                result)
        except GLib.Error as error:
            callback(error, None)
            return

        callback(None, bytes_data)

    status, blob = load_record_blob_from_shards(shards, content_id, attr)

    if status == LOAD_FROM_ENGINE_NO_SUCH_CONTENT:
        GLib.idle_add(lambda: callback(
            GLib.Error(
                'EKN ID {} not found in shards'.format(content_id),
                GLib.quark_to_string(EosCompanionAppService.error_quark()),
                EosCompanionAppService.Error.INVALID_CONTENT_ID), None))
        return

    EosCompanionAppService.load_all_in_stream_to_bytes(
        blob.get_stream(),
        chunk_size=BYTE_CHUNK_SIZE,
        cancellable=None,
        callback=_callback)
    def _on_got_application_info(_, result):
        '''Callback function that gets called when we get the app info.'''
        try:
            app_info = EosCompanionAppService.finish_load_application_info(result)
        except GLib.Error as error:
            callback(error, None)
            return

        content_db_conn.query(application_listing=application_listing_from_app_info(app_info),
                              query={
                                  'tags-match-all': ['EknSetObject']
                              },
                              cancellable=cancellable,
                              callback=_on_queried_sets)
def conditionally_wrap_stream(stream, content_size, content_type, version,
                              query, adjuster, cache, cancellable, callback):
    '''Inspect content_type to adjust stream content.'''
    def _content_adjusted_callback(error, adjusted):
        '''Callback once we have finished adjusting the content.'''
        if error is not None:
            callback(error, None)
            return

        memory_stream = Gio.MemoryInputStream.new_from_bytes(adjusted)
        callback(None, (memory_stream, adjusted.get_size()))

    def _read_stream_callback(_, result):
        '''Callback once we have finished loading the stream to bytes.'''
        try:
            content_bytes = EosCompanionAppService.finish_load_all_in_stream_to_bytes(
                result)
        except GLib.Error as error:
            callback(error, None)
            return

        adjuster.render_async(content_type, content_bytes, version, query,
                              cache, cancellable, _content_adjusted_callback)

    if adjuster.needs_adjustment(content_type):
        EosCompanionAppService.load_all_in_stream_to_bytes(
            stream,
            chunk_size=BYTE_CHUNK_SIZE,
            cancellable=cancellable,
            callback=_read_stream_callback)
        return

    # We call callback here on idle so as to ensure that both invocations
    # are asynchronous (mixing asynchronous with synchronous disguised as
    # asynchronous is a bad idea)
    GLib.idle_add(lambda: callback(None, (stream, content_size)))
Exemple #14
0
        def middleware(server, msg, path, query, *args):
            '''Middleware to check the query parameters.'''
            rectified_query = query or {}
            if not rectified_query.get(param, None):
                return json_response(
                    msg, {
                        'status':
                        'error',
                        'error':
                        serialize_error_as_json_object(
                            EosCompanionAppService.error_quark(),
                            EosCompanionAppService.Error.INVALID_REQUEST,
                            detail={'missing_querystring_param': param})
                    })

            return handler(server, msg, path, rectified_query, *args)
    def _on_finished_loading_shards(shard_init_results):
        '''Callback for when shards have finished initializing.

        Check for any errors and invoke the callback accordingly,
        otherwise, invoke the callback with the resolved shards
        and metadata now.
        '''
        try:
            callback(None,
                     list(_iterate_init_shard_results(shard_init_results)))
        except GLib.Error as error:
            callback(GLib.Error(error.message,  # pylint: disable=no-member
                                EosCompanionAppService.error_quark(),
                                EosCompanionAppService.Error.FAILED),
                     None)
            return
def translate_error(error, error_mappings=None):
    '''Translate any error type into an EosCompanionAppError error.'''
    if error.domain == GLib.quark_to_string(
            EosCompanionAppService.error_quark()):
        return (EosCompanionAppService.error_quark(),
                EosCompanionAppService.Error(error.code))

    for src_domain, src_code, target_code in generate_error_mappings(
            error_mappings):
        if error.matches(src_domain, src_code):
            return (EosCompanionAppService.error_quark(),
                    EosCompanionAppService.Error(target_code))

    return (EosCompanionAppService.error_quark(),
            EosCompanionAppService.Error.FAILED)
    def _on_received_application_info(_, result):
        '''Called when we receive requested application info.'''
        try:
            listing = application_listing_from_app_info(
                EosCompanionAppService.finish_load_application_info(result))
        except Exception as error:
            done_callback(error, None)
            return

        done_callback(None, [{
            'tags':
            _GLOBAL_SET_INDICATOR_TAG,
            'title':
            listing.display_name,
            'contentType':
            'application/x-ekncontent-set',
            'thumbnail':
            format_app_icon_uri(version, listing.icon, device_uuid),
            'id':
            '',
            'global':
            True
        }])
def configure_drop_cache_on_changes(cache):
    '''Configure :cache: to be dropped when the Flatpak installation state changes.

    This creates a Gio.FileMonitor over each of the configured Flatpak
    installations on the system and drops all caches when they change.

    Note that we cannot use the Flatpak API here directly as we are running
    from within Flatpak. We are relying on an internal implementation
    detail, namely that Flatpak itself will update ".changed" in the
    installation directory on state changes.
    '''
    def _on_installation_changed(*args):
        '''Callback for when something changes.'''
        del args

        cache.clear()

    return list(
        yield_monitors_over_changed_file_in_paths(
            EosCompanionAppService.flatpak_install_dirs(),
            _on_installation_changed
        )
    )
def main(args=None):
    '''Entry point function.

    Since we're often running from within flatpak, make sure to override
    XDG_DATA_DIRS to include the flatpak exports too, since they don't get
    included by default.

    We use GLib.setenv here, since os.environ is only visible to
    Python code, but setting a variable in os.environ does not actually
    update the 'environ' global variable on the C side.
    '''
    flatpak_export_share_dirs = [
        os.path.join(d, 'exports', 'share')
        for d in EosCompanionAppService.flatpak_install_dirs()
    ]
    GLib.setenv(
        'XDG_DATA_DIRS',
        os.pathsep.join([GLib.getenv('XDG_DATA_DIRS') or ''] +
                        flatpak_export_share_dirs), True)

    logging.basicConfig(
        format='CompanionAppService %(levelname)s: %(message)s',
        level=get_log_level())
    CompanionAppApplication().run(args or sys.argv)
def _dbus_error_to_companion_app_error(error):
    '''Translate the GDBusError from EknServices to a Companion App error.'''
    code = _EKS_ERROR_TO_COMPANION_APP_ERROR[Gio.DBusError.get_remote_error(error)]
    return GLib.Error(error.message,  # pylint: disable=no-member
                      EosCompanionAppService.error_quark(),
                      code)
def render_mobile_wrapper(renderer,
                          app_id,
                          rendered_content,
                          metadata,
                          content_db_conn,
                          shards,
                          version,
                          query,
                          cache,
                          cancellable,
                          callback):
    '''Render the page wrapper and initialize crosslinks.

    This is the final rendering step that EKN content needs to undergo before
    it becomes usable, at least before it leaves the server. This step
    initializes both a dictionary of metadata about the content and also a map
    of outgoing links to internal links. Then we inject some javascript which
    at browser-render time, rewrites the page to resolve all those links.
    '''
    def _on_queried_sets(error, result):
        '''Called when we finish querying set objects.'''
        if error is not None:
            callback(error, None)
            return

        _, set_objects = result
        content_metadata = {
            'title': metadata.get('title', ''),
            'published': metadata.get('published', ''),
            'authors': metadata.get('authors', []),
            'license': metadata.get('license', ''),
            'source': metadata.get('source', ''),
            'source_name': metadata.get('sourceName', ''),
            'originalURI': metadata.get('originalURI', ''),
            'sets': [
                {
                    'child_tags': set_object['child_tags'],
                    'id': set_object['id'],
                    'title': set_object['title'],
                    'tags': set_object['tags']
                }
                for set_object in set_objects
                if any([
                    tag in set_object['child_tags']
                    for tag in metadata.get('tags', [])
                    if not tag.startswith('Ekn')
                ])
            ]
        }

        # Now that we have everything, read the template and render it
        variables = GLib.Variant('a{sv}', {
            'css-files': GLib.Variant('as', [
                'clipboard.css',
                'share-actions.css'
            ]),
            'custom-css-files': GLib.Variant('as', []),
            'javascript-files': GLib.Variant('as', [
                'jquery-min.js',
                'collapse-infotable.js',
                'crosslink.js'
            ]),
            'content': GLib.Variant('s', rendered_content),
            'crosslink-data': GLib.Variant('s', json.dumps(link_resolution_table)),
            'content-metadata': GLib.Variant('s', json.dumps(content_metadata)),
            'title': GLib.Variant(
                's',
                metadata.get('title', 'Content from {app_id}'.format(app_id=app_id))
            )
        })

        try:
            template_file = Gio.File.new_for_uri(_MOBILE_WRAPPER_TEMPLATE_URI)
            rendered_page = renderer.render_mustache_document_from_file(template_file,
                                                                        variables)
        except GLib.Error as page_render_error:
            callback(page_render_error, None)
            return

        callback(None, rendered_page)

    def _on_got_application_info(_, result):
        '''Callback function that gets called when we get the app info.'''
        try:
            app_info = EosCompanionAppService.finish_load_application_info(result)
        except GLib.Error as error:
            callback(error, None)
            return

        content_db_conn.query(application_listing=application_listing_from_app_info(app_info),
                              query={
                                  'tags-match-all': ['EknSetObject']
                              },
                              cancellable=cancellable,
                              callback=_on_queried_sets)

    link_tables = list(link_tables_from_shards(shards))
    link_resolution_table = [
        maybe_ekn_id_to_server_uri(
            resolve_outgoing_link_to_internal_link(link_tables, l),
            version,
            query
        )
        for l in metadata.get('outgoingLinks', [])
    ]

    EosCompanionAppService.load_application_info(app_id,
                                                 cache=cache,
                                                 cancellable=cancellable,
                                                 callback=_on_got_application_info)
    def _html_content_adjuster(content_bytes,
                               version,
                               query,
                               metadata,
                               content_db_conn,
                               shards,
                               cache,
                               cancellable,
                               callback):
        '''Adjust HTML content by rewriting all the embedded URLs.

        There will be images, video and links in the document, parse it
        and rewrite it so that the links all go to somewhere that can
        be resolved by the server.

        Right now the way that this is done is a total hack (regex), in future
        we might want to depend on beautifulsoup and use that instead.
        '''
        def _on_rendered_wrapper(error, rendered_page):
            '''Called when rendering the wrapper is complete.'''
            if error is not None:
                callback(error, None)
                return

            try:
                response_bytes = EosCompanionAppService.string_to_bytes(
                    pipeline(
                        rendered_page,
                        lambda content: _RE_EKN_URL_CAPTURE.sub(
                            ekn_url_rewriter(version, query),
                            content
                        ),
                        lambda content: _RE_RESOURCE_URL_CAPTURE.sub(
                            resource_url_rewriter(version, query),
                            content
                        ),
                        lambda content: _RE_LICENSE_URL_CAPTURE.sub(
                            license_url_rewriter(version, query),
                            content
                        )
                    )
                )
            except GLib.Error as render_bytes_error:
                callback(render_bytes_error, None)
                return

            callback(None, response_bytes)

        unrendered_html_string = EosCompanionAppService.bytes_to_string(content_bytes)

        # We need the content_db_conn, shards and metadata
        # in order to render our mobile wrapper.
        if (metadata is not None and
                content_db_conn is not None and
                shards is not None):
            if not metadata.get('isServerTemplated', False):
                rendered_content = renderer.render_legacy_content(
                    unrendered_html_string,
                    metadata.get('source', ''),
                    metadata.get('sourceName', ''),
                    metadata.get('originalURI', ''),
                    metadata.get('license', ''),
                    metadata.get('title', ''),
                    show_title=True,
                    use_scroll_manager=False
                )
            else:
                rendered_content = unrendered_html_string

            render_mobile_wrapper(renderer,
                                  query['applicationId'],
                                  rendered_content,
                                  metadata,
                                  content_db_conn,
                                  shards,
                                  version,
                                  query,
                                  cache,
                                  cancellable,
                                  _on_rendered_wrapper)
        else:
            # Put this on an idle timer, mixing async and sync code
            # is a bad idea
            GLib.idle_add(lambda: _on_rendered_wrapper(None, unrendered_html_string))
def custom_response(msg, content_type, content_bytes):
    '''Respond with :content_type: using :content_bytes:.'''
    msg.set_status(Soup.Status.OK)
    EosCompanionAppService.set_soup_message_response_bytes(
        msg, content_type, content_bytes)
def jpeg_response(msg, image_bytes):
    '''Respond with image/jpeg bytes.'''
    msg.set_status(Soup.Status.OK)
    EosCompanionAppService.set_soup_message_response_bytes(
        msg, 'image/jpeg', image_bytes)
def html_response(msg, html):
    '''Respond with an HTML body.'''
    msg.set_status(Soup.Status.OK)
    EosCompanionAppService.set_soup_message_response(msg, 'text/html', html)
def json_response(msg, obj):
    '''Respond with a JSON object'''
    msg.set_status(Soup.Status.OK)
    EosCompanionAppService.set_soup_message_response(msg, 'application/json',
                                                     json.dumps(obj))
def ascertain_application_sets_from_models(models, version, device_uuid,
                                           application_id, cache, cancellable,
                                           done_callback):
    '''Pass application sets or an entry for the global set to callback.'''
    def _on_received_application_info(_, result):
        '''Called when we receive requested application info.'''
        try:
            listing = application_listing_from_app_info(
                EosCompanionAppService.finish_load_application_info(result))
        except Exception as error:
            done_callback(error, None)
            return

        done_callback(None, [{
            'tags':
            _GLOBAL_SET_INDICATOR_TAG,
            'title':
            listing.display_name,
            'contentType':
            'application/x-ekncontent-set',
            'thumbnail':
            format_app_icon_uri(version, listing.icon, device_uuid),
            'id':
            '',
            'global':
            True
        }])

    try:
        any_featured = any([model.get('featured', True) for model in models])
        application_sets_response = [
            {
                'tags':
                model['child_tags'],
                'title':
                model['title'],
                'contentType':
                'application/x-ekncontent-set',
                'thumbnail':
                optional_format_thumbnail_uri(version, application_id, model,
                                              device_uuid),
                'id':
                urllib.parse.urlparse(model['id']).path[1:],
                'global':
                False
            } for model in models
            # Filter out sets explicitly not marked featured, except
            # if nothing was featured - in that case include everything
            if model.get('featured', True) or not any_featured
        ]

        if application_sets_response:
            GLib.idle_add(done_callback, None, application_sets_response)
            return

        EosCompanionAppService.load_application_info(
            application_id,
            cache,
            cancellable=cancellable,
            callback=_on_received_application_info)
    except Exception as error:
        GLib.idle_add(done_callback, error, None)