view libervia/pages/blog/view/page_meta.py @ 1196:a15ac511390c

server: changed defaultType for File resources to "application/octet-stream" instead of "text/html"
author Goffi <goffi@goffi.org>
date Wed, 26 Jun 2019 20:36:50 +0200
parents 0f37b65fe7c2
children 67ec22356457
line wrap: on
line source

#!/usr/bin/env python2.7
# -*- coding: utf-8 -*-
import unicodedata
import re
import cgi
from libervia.server.constants import Const as C
from twisted.words.protocols.jabber import jid
from twisted.internet import defer
from sat.tools.common import data_objects
from libervia.server import session_iface
from sat.core.i18n import _
from sat.tools.common.template import safe
from sat.tools.common import uri
from sat.tools.common import data_format
from libervia.server import utils
from libervia.server.utils import SubPage
from sat.core.log import getLogger

log = getLogger(__name__)

"""generic blog (with service/node provided)"""
name = u'blog_view'
template = u"blog/articles.html"
uri_handlers = {(u'pubsub', u'microblog'): 'microblog_uri'}

RE_TEXT_URL = re.compile(ur'[^a-zA-Z,_]+')
TEXT_MAX_LEN = 60
TEXT_WORD_MIN_LENGHT = 4
URL_LIMIT_MARK = 90  # if canonical URL is longer than that, text will not be appended


def microblog_uri(self, uri_data):
    args = [uri_data[u'path'], uri_data[u'node']]
    if u'item' in uri_data:
        args.extend([u'id', uri_data[u'item']])
    return self.getURL(*args)

def parse_url(self, request):
    """URL is /[service]/[node]/[filter_keyword]/[item]|[other]

    if [node] is '@', default namespace is used
    if a value is unset, default one will be used
    keyword can be one of:
        id: next value is a item id
        tag: next value is a blog tag
    """
    data = self.getRData(request)

    try:
        service = self.nextPath(request)
    except IndexError:
        data['service'] = u''
    else:
        try:
            data[u"service"] = jid.JID(service)
        except Exception:
            log.warning(_(u"bad service entered: {}").format(service))
            self.pageError(request, C.HTTP_BAD_REQUEST)

    try:
        node = self.nextPath(request)
    except IndexError:
        node = u'@'
    data['node'] = u'' if node == u'@' else node

    try:
        filter_kw = data['filter_keyword'] = self.nextPath(request)
    except IndexError:
        filter_kw = u'@'
    else:
        if filter_kw == u'@':
            # No filter, this is used when a subpage is needed, notably Atom feed
            pass
        elif filter_kw == u'id':
            try:
                data[u'item'] = self.nextPath(request)
            except IndexError:
                self.pageError(request, C.HTTP_BAD_REQUEST)
            # we get one more argument in case text has been added to have a nice URL
            try:
                self.nextPath(request)
            except IndexError:
                pass
        elif filter_kw == u'tag':
            try:
                data[u'tag'] = self.nextPath(request)
            except IndexError:
                self.pageError(request, C.HTTP_BAD_REQUEST)
        else:
            # invalid filter keyword
            log.warning(_(u"invalid filter keyword: {filter_kw}").format(
                filter_kw=filter_kw))
            self.pageError(request, C.HTTP_BAD_REQUEST)

    # if URL is parsed here, we'll have atom.xml available and we need to
    # add the link to the page
    atom_url = self.getURLByPath(
        SubPage(u'blog_view'),
        service,
        node,
        filter_kw,
        SubPage(u'blog_feed_atom'),
    )
    request.template_data.setdefault(u'links', []).append({
        u"href": atom_url,
        u"type": "application/atom+xml",
        u"rel": "alternate",
        u"title": "{service}'s blog".format(service=service)})


@defer.inlineCallbacks
def appendComments(self, blog_items, identities, profile):
    for blog_item in blog_items:
        if identities is not None:
            author = blog_item.author_jid
            if not author:
                log.warning(_(u"no author found for item {item_id}").format(item_id=blog_item.id))
            else:
                if author not in identities:
                    identities[author] = yield self.host.bridgeCall(u'identityGet', author, profile)
        for comment_data in blog_item.comments:
            service = comment_data[u'service']
            node = comment_data[u'node']
            try:
                comments_data = yield self.host.bridgeCall(u'mbGet',
                                      service,
                                      node,
                                      C.NO_LIMIT,
                                      [],
                                      {C.KEY_ORDER_BY: C.ORDER_BY_CREATION},
                                      profile)
            except Exception as e:
                log.warning(_(u"Can't get comments at {node} (service: {service}): {msg}").format(
                    service=service,
                    node=node,
                    msg=e))
                continue

            comments = data_objects.BlogItems(comments_data)
            blog_item.appendCommentsItems(comments)
            yield appendComments(self, comments, identities, profile)

@defer.inlineCallbacks
def getBlogItems(self, request, service, node, item_id, extra, profile):
    try:
        if item_id:
            items_id = [item_id]
        else:
            items_id = []
        blog_data = yield self.host.bridgeCall(u'mbGet',
                              service.userhost(),
                              node,
                              C.NO_LIMIT,
                              items_id,
                              extra,
                              profile)
    except Exception as e:
        # FIXME: need a better way to test errors in bridge errback
        if u"forbidden" in unicode(e):
            self.pageError(request, 401)
        else:
            log.warning(_(u"can't retrieve blog for [{service}]: {msg}".format(
                service = service.userhost(), msg=e)))
            blog_data = ([], {})

    defer.returnValue(data_objects.BlogItems(blog_data))

@defer.inlineCallbacks
def prepare_render(self, request):
    data = self.getRData(request)
    page_max = data.get(u"page_max", 10)
    # if the comments are not explicitly hidden, we show them
    service, node, item_id, show_comments = data.get(u'service', u''), data.get(u'node', u''), data.get(u'item'), data.get(u'show_comments', True)
    profile = self.getProfile(request)
    if profile is None:
        profile = C.SERVICE_PROFILE
        profile_connected = False
    else:
        profile_connected = True

    ## pagination/filtering parameters
    if item_id:
        extra = {}
    else:
        extra = self.getPubsubExtra(request, page_max=page_max)
        tag = data.get('tag')
        if tag:
            extra[u'mam_filter_{}'.format(C.MAM_FILTER_CATEGORY)] = tag

    ## main data ##
    # we get data from backend/XMPP here
    items = yield getBlogItems(self, request, service, node, item_id, extra, profile)

    ## navigation ##
    # no let's fill service, node and pagination URLs
    template_data = request.template_data
    if u'service' not in template_data:
        template_data[u'service'] = service
    if u'node' not in template_data:
        template_data[u'node'] = node
    target_profile = template_data.get(u'target_profile')

    if items:
        if not item_id:
            self.setPagination(request, items.metadata)
    else:
        if item_id:
            # if item id has been specified in URL and it's not found,
            # we must return an error
            self.pageError(request, C.HTTP_NOT_FOUND)

    ## identities ##
    # identities are used to show nice nickname or avatars
    identities = template_data[u'identities'] = self.host.getSessionData(request, session_iface.ISATSession).identities

    ## Comments ##
    # if comments are requested, we need to take them
    if show_comments:
        yield appendComments(self, items, identities, profile)

    ## URLs ##
    # We will fill items_http_uri and tags_http_uri in template_data with suitable urls
    # if we know the profile, we use it instead of service + blog (nicer url)
    if target_profile is None:
        blog_base_url_item = self.getPageByName(u'blog_view').getURL(service.full(), node or u'@', u'id')
        blog_base_url_tag = self.getPageByName(u'blog_view').getURL(service.full(), node or u'@', u'tag')
    else:
        blog_base_url_item = self.getURLByNames([(u'user', [target_profile]), (u'user_blog', [u'id'])])
        blog_base_url_tag = self.getURLByNames([(u'user', [target_profile]), (u'user_blog', [u'tag'])])
        # we also set the background image if specified by user
        bg_img = yield self.host.bridgeCall(u'asyncGetParamA', u'Background', u'Blog page', u'value', -1, template_data[u'target_profile'])
        if bg_img:
            template_data['dynamic_style'] = safe(u"""
                :root {
                    --bg-img: url("%s");
                }
                """ % cgi.escape(bg_img, True))

    template_data[u'items'] = data[u'items'] = items
    if request.args.get('reverse') == ['1']:
        template_data[u'items'].items.reverse()
    template_data[u'items_http_uri'] = items_http_uri = {}
    template_data[u'tags_http_uri'] = tags_http_uri = {}


    for item in items:
        blog_canonical_url = u'/'.join([blog_base_url_item, utils.quote(item.id)])
        if len(blog_canonical_url) > URL_LIMIT_MARK:
            blog_url = blog_canonical_url
        else:
            # we add text from title or body at the end of URL
            # to make it more human readable
            text = item.title or item.content
            # we change special chars to ascii one, trick found at https://stackoverflow.com/a/3194567
            text = unicodedata.normalize('NFD', text).encode('ascii', 'ignore')
            text = RE_TEXT_URL.sub(u' ', text).lower()
            text = u'-'.join([t for t in text.split() if t and len(t)>=TEXT_WORD_MIN_LENGHT])
            while len(text) > TEXT_MAX_LEN:
                if u'-' in text:
                    text = text.rsplit(u'-', 1)[0]
                else:
                    text = text[:TEXT_MAX_LEN]
            if text:
                blog_url = blog_canonical_url + u'/' + text
            else:
                blog_url = blog_canonical_url

        items_http_uri[item.id] = self.host.getExtBaseURL(request, blog_url)
        for tag in item.tags:
            if tag not in tags_http_uri:
                tag_url = u'/'.join([blog_base_url_tag, utils.quote(tag)])
                tags_http_uri[tag] = self.host.getExtBaseURL(request, tag_url)

    # if True, page should display a comment box
    template_data[u'allow_commenting'] = data.get(u'allow_commenting', profile_connected)

    # last but not least, we add a xmpp: link to the node
    uri_args = {u'path': service.full()}
    if node:
        uri_args[u'node'] = node
    if item_id:
        uri_args[u'item'] = item_id
    template_data[u'xmpp_uri'] = uri.buildXMPPUri(u'pubsub', subtype='microblog', **uri_args)


@defer.inlineCallbacks
def on_data_post(self, request):
    profile = self.getProfile(request)
    if profile is None:
        self.pageError(request, C.HTTP_FORBIDDEN)
    type_ = self.getPostedData(request, u'type')
    if type_ == u'comment':
        service, node, body = self.getPostedData(request, (u'service', u'node', u'body'))

        if not body:
            self.pageError(request, C.HTTP_BAD_REQUEST)
        comment_data = {u"content": body}
        try:
            yield self.host.bridgeCall(u'mbSend',
                                       service,
                                       node,
                                       data_format.serialise(comment_data),
                                       profile)
        except Exception as e:
            if u"forbidden" in unicode(e):
                self.pageError(request, 401)
            else:
                raise e
    else:
        log.warning(_(u"Unhandled data type: {}").format(type_))