Mercurial > libervia-web
changeset 1124:28e3eb3bb217
files reorganisation and installation rework:
- files have been reorganised to follow other SàT projects and usual Python organisation (no more "/src" directory)
- VERSION file is now used, as for other SàT projects
- replace the overcomplicated setup.py be a more sane one. Pyjamas part is not compiled anymore by setup.py, it must be done separatly
- removed check for data_dir if it's empty
- installation tested working in virtual env
- libervia launching script is now in bin/libervia
author | Goffi <goffi@goffi.org> |
---|---|
date | Sat, 25 Aug 2018 17:59:48 +0200 |
parents | 63a4b8fe9782 |
children | 56ace2d45783 |
files | bin/libervia browser/collections.py browser/libervia_main.py browser/libervia_test.py browser/otr.min.js browser/otr.min.js_README browser/public/contrat_social.html browser/public/favico.min.js browser/public/libervia.css browser/public/libervia.html browser/public/robots.txt browser/public/sat_logo_16.png browser/sat_browser/__init__.py browser/sat_browser/base_menu.py browser/sat_browser/base_panel.py browser/sat_browser/base_widget.py browser/sat_browser/blog.py browser/sat_browser/chat.py browser/sat_browser/constants.py browser/sat_browser/contact_group.py browser/sat_browser/contact_list.py browser/sat_browser/contact_panel.py browser/sat_browser/contact_widget.py browser/sat_browser/dialog.py browser/sat_browser/editor_widget.py browser/sat_browser/file_tools.py browser/sat_browser/game_radiocol.py browser/sat_browser/game_tarot.py browser/sat_browser/html_tools.py browser/sat_browser/json.py browser/sat_browser/libervia_widget.py browser/sat_browser/list_manager.py browser/sat_browser/logging.py browser/sat_browser/main_panel.py browser/sat_browser/menu.py browser/sat_browser/nativedom.py browser/sat_browser/notification.py browser/sat_browser/otrjs_wrapper.py browser/sat_browser/plugin_sec_otr.py browser/sat_browser/plugin_xep_0085.py browser/sat_browser/register.py browser/sat_browser/richtext.py browser/sat_browser/strings.py browser/sat_browser/web_widget.py browser/sat_browser/xmlui.py ez_setup.py libervia/VERSION libervia/__init__.py libervia/common/__init__.py libervia/common/constants.py libervia/pages/app/page_meta.py libervia/pages/blog/page_meta.py libervia/pages/blog/view/atom.xml/page_meta.py libervia/pages/blog/view/page_meta.py libervia/pages/chat/page_meta.py libervia/pages/chat/select/page_meta.py libervia/pages/events/admin/page_meta.py libervia/pages/events/new/page_meta.py libervia/pages/events/page_meta.py libervia/pages/events/rsvp/page_meta.py libervia/pages/events/view/page_meta.py libervia/pages/files/list/page_meta.py libervia/pages/files/page_meta.py libervia/pages/files/view/page_meta.py libervia/pages/forums/list/page_meta.py libervia/pages/forums/page_meta.py libervia/pages/forums/topics/page_meta.py libervia/pages/forums/view/page_meta.py libervia/pages/g/e/page_meta.py libervia/pages/g/page_meta.py libervia/pages/login/logged/page_meta.py libervia/pages/login/page_meta.py libervia/pages/merge-requests/disco/page_meta.py libervia/pages/merge-requests/edit/page_meta.py libervia/pages/merge-requests/new/page_meta.py libervia/pages/merge-requests/page_meta.py libervia/pages/merge-requests/view/page_meta.py libervia/pages/photos/album/page_meta.py libervia/pages/photos/page_meta.py libervia/pages/register/page_meta.py libervia/pages/tickets/disco/page_meta.py libervia/pages/tickets/edit/page_meta.py libervia/pages/tickets/new/page_meta.py libervia/pages/tickets/page_meta.py libervia/pages/tickets/view/page_meta.py libervia/pages/u/atom.xml/page_meta.py libervia/pages/u/blog/page_meta.py libervia/pages/u/page_meta.py libervia/server/__init__.py libervia/server/blog.py libervia/server/constants.py libervia/server/html_tools.py libervia/server/pages.py libervia/server/pages_tools.py libervia/server/server.py libervia/server/session_iface.py libervia/server/utils.py libervia/server/websockets.py setup.py src/__init__.py src/browser/collections.py src/browser/libervia_main.py src/browser/libervia_test.py src/browser/otr.min.js src/browser/otr.min.js_README src/browser/public/contrat_social.html src/browser/public/favico.min.js src/browser/public/libervia.css src/browser/public/libervia.html src/browser/public/robots.txt src/browser/public/sat_logo_16.png src/browser/sat_browser/__init__.py src/browser/sat_browser/base_menu.py src/browser/sat_browser/base_panel.py src/browser/sat_browser/base_widget.py src/browser/sat_browser/blog.py src/browser/sat_browser/chat.py src/browser/sat_browser/constants.py src/browser/sat_browser/contact_group.py src/browser/sat_browser/contact_list.py src/browser/sat_browser/contact_panel.py src/browser/sat_browser/contact_widget.py src/browser/sat_browser/dialog.py src/browser/sat_browser/editor_widget.py src/browser/sat_browser/file_tools.py src/browser/sat_browser/game_radiocol.py src/browser/sat_browser/game_tarot.py src/browser/sat_browser/html_tools.py src/browser/sat_browser/json.py src/browser/sat_browser/libervia_widget.py src/browser/sat_browser/list_manager.py src/browser/sat_browser/logging.py src/browser/sat_browser/main_panel.py src/browser/sat_browser/menu.py src/browser/sat_browser/nativedom.py src/browser/sat_browser/notification.py src/browser/sat_browser/otrjs_wrapper.py src/browser/sat_browser/plugin_sec_otr.py src/browser/sat_browser/plugin_xep_0085.py src/browser/sat_browser/register.py src/browser/sat_browser/richtext.py src/browser/sat_browser/strings.py src/browser/sat_browser/web_widget.py src/browser/sat_browser/xmlui.py src/common/__init__.py src/common/constants.py src/libervia.sh src/pages/app/page_meta.py src/pages/blog/page_meta.py src/pages/blog/view/atom.xml/page_meta.py src/pages/blog/view/page_meta.py src/pages/chat/page_meta.py src/pages/chat/select/page_meta.py src/pages/events/admin/page_meta.py src/pages/events/new/page_meta.py src/pages/events/page_meta.py src/pages/events/rsvp/page_meta.py src/pages/events/view/page_meta.py src/pages/files/list/page_meta.py src/pages/files/page_meta.py src/pages/files/view/page_meta.py src/pages/forums/list/page_meta.py src/pages/forums/page_meta.py src/pages/forums/topics/page_meta.py src/pages/forums/view/page_meta.py src/pages/g/e/page_meta.py src/pages/g/page_meta.py src/pages/login/logged/page_meta.py src/pages/login/page_meta.py src/pages/merge-requests/disco/page_meta.py src/pages/merge-requests/edit/page_meta.py src/pages/merge-requests/new/page_meta.py src/pages/merge-requests/page_meta.py src/pages/merge-requests/view/page_meta.py src/pages/photos/album/page_meta.py src/pages/photos/page_meta.py src/pages/register/page_meta.py src/pages/tickets/disco/page_meta.py src/pages/tickets/edit/page_meta.py src/pages/tickets/new/page_meta.py src/pages/tickets/page_meta.py src/pages/tickets/view/page_meta.py src/pages/u/atom.xml/page_meta.py src/pages/u/blog/page_meta.py src/pages/u/page_meta.py src/server/__init__.py src/server/blog.py src/server/constants.py src/server/html_tools.py src/server/pages.py src/server/pages_tools.py src/server/server.py src/server/session_iface.py src/server/utils.py src/server/websockets.py src/twisted/plugins/libervia_server.py twisted/plugins/libervia_server.py |
diffstat | 189 files changed, 20449 insertions(+), 20993 deletions(-) [+] |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bin/libervia Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,107 @@ +#!/bin/sh + +DEBUG="" +DAEMON="" +PYTHON="python2" +TWISTD="$(which twistd)" + +kill_process() { + # $1 is the file containing the PID to kill, $2 is the process name + if [ -f $1 ]; then + PID=`cat $1` + if ps -p $PID > /dev/null; then + echo "Terminating $2... " + kill -INT $PID + else + echo "No running process of ID $PID... removing PID file" + rm -f $1 + fi + else + echo "$2 is probably not running (PID file doesn't exist)" + fi +} + +#We use python to parse config files +eval `"$PYTHON" << PYTHONEND +from libervia.server.constants import Const as C +from sat.memory.memory import fixLocalDir +from ConfigParser import SafeConfigParser +from os.path import expanduser, join +import sys +import codecs +import locale + +sys.stdout = codecs.getwriter(locale.getpreferredencoding())(sys.stdout) + +fixLocalDir() # XXX: tmp update code, will be removed in the future + +config = SafeConfigParser(defaults=C.DEFAULT_CONFIG) +try: + config.read(C.CONFIG_FILES) +except: + print ("echo \"/!\\ Can't read main config ! Please check the syntax\";") + print ("exit 1") + sys.exit() + +env=[] +env.append("PID_DIR='%s'" % join(expanduser(config.get('DEFAULT', 'pid_dir')),'')) +env.append("LOG_DIR='%s'" % join(expanduser(config.get('DEFAULT', 'log_dir')),'')) +env.append("APP_NAME='%s'" % C.APP_NAME) +env.append("APP_NAME_FILE='%s'" % C.APP_NAME_FILE) +print ";".join(env) +PYTHONEND +` +APP_NAME="$APP_NAME" +PID_FILE="$PID_DIR$APP_NAME_FILE.pid" +LOG_FILE="$LOG_DIR$APP_NAME_FILE.log" +RUNNING_MSG="$APP_NAME is running" +NOT_RUNNING_MSG="$APP_NAME is *NOT* running" + +# if there is one argument which is "stop", then we kill Libervia +if [ $# -ge 1 ];then + if [ $1 = "stop" ];then + kill_process $PID_FILE "$APP_NAME" + exit 0 + elif [ $1 = "debug" ];then + echo "Launching $APP_NAME in debug mode" + DEBUG="--debug" + elif [ $1 = "fg" ];then + echo "Launching $APP_NAME in foreground mode" + DAEMON="n" + elif [ $1 = "status" ];then + if [ -f $PID_FILE ]; then + PID=`cat $PID_FILE` + ps -p$PID 2>&1 > /dev/null + if [ $? = 0 ];then + echo "$RUNNING_MSG (pid: $PID)" + exit 0 + else + echo "$NOT_RUNNING_MSG, but a pid file is present (bad exit ?): $PID_FILE" + exit 2 + fi + else + echo "$NOT_RUNNING_MSG" + exit 1 + fi + else + echo "bad argument, please use one of (stop, debug, fg, status) or no argument" + exit 1 + fi + shift +fi + + +#Don't change the next lines +PLUGIN_OPTIONS="" +AUTO_OPTIONS="" +ADDITIONAL_OPTIONS="--pidfile $PID_FILE --logfile $LOG_FILE $AUTO_OPTIONS $DEBUG" + + +MAIN_OPTIONS="-${DAEMON}o" + +log_dir=`dirname "$LOG_FILE"` +if [ ! -d $log_dir ] ; then + mkdir $log_dir +fi + +exec $PYTHON $TWISTD $MAIN_OPTIONS $ADDITIONAL_OPTIONS $APP_NAME_FILE $PLUGIN_OPTIONS $@
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/browser/collections.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,149 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2014 Jérôme Poisson <goffi@goffi.org> + +# This program 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. + +# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + +class OrderedDict(object): + """Naive implementation of OrderedDict which is compatible with pyjamas""" + + def __init__(self, *args, **kwargs): + self.__internal_dict = {} + self.__keys = [] # this list keep the keys in order + if args: + if len(args)>1: + raise TypeError("OrderedDict expected at most 1 arguments, got {}".format(len(args))) + if isinstance(args[0], (dict, OrderedDict)): + for key, value in args[0].iteritems(): + self[key] = value + for key, value in args[0]: + self[key] = value + + def __len__(self): + return len(self.__keys) + + def __setitem__(self, key, value): + if key not in self.__keys: + self.__keys.append(key) + self.__internal_dict[key] = value + + def __getitem__(self, key): + return self.__internal_dict[key] + + def __delitem__(self, key): + del self.__internal_dict[key] + self.__keys.remove(key) + + def __contains__(self, key): + return key in self.__keys + + def clear(self): + self.__internal_dict.clear() + del self.__keys[:] + + def copy(self): + return OrderedDict(self) + + @classmethod + def fromkeys(cls, seq, value=None): + ret = OrderedDict() + for key in seq: + ret[key] = value + return ret + + def get(self, key, default=None): + try: + return self.__internal_dict[key] + except KeyError: + return default + + def has_key(self, key): + return key in self.__keys + + def keys(self): + return self.__keys[:] + + def iterkeys(self): + for key in self.__keys: + yield key + + def items(self): + ret = [] + for key in self.__keys: + ret.append((key, self.__internal_dict[key])) + return ret + + def iteritems(self): + for key in self.__keys: + yield (key, self.__internal_dict[key]) + + def values(self): + ret = [] + for key in self.__keys: + ret.append(self.__internal_dict[key]) + return ret + + def itervalues(self): + for key in self.__keys: + yield (self.__internal_dict[key]) + + def popitem(self, last=True): + try: + key = self.__keys.pop(-1 if last else 0) + except IndexError: + raise KeyError('dictionnary is empty') + value = self.__internal_dict.pop(key) + return((key, value)) + + def setdefault(self, key, default=None): + try: + return self.__internal_dict[key] + except KeyError: + self[key] = default + return default + + def update(self, *args, **kwargs): + if len(args) > 1: + raise TypeError('udpate expected at most 1 argument, got {}'.format(len(args))) + if args: + if hasattr(args[0], 'keys'): + for k in args[0]: + self[k] = args[0][k] + else: + for (k, v) in args[0]: + self[k] = v + for k, v in kwargs.items(): + self[k] = v + + def pop(self, *args): + if not args: + raise TypeError('pop expected at least 1 argument, got 0') + try: + self.__internal_dict.pop(args[0]) + except KeyError: + if len(args) == 2: + return args[1] + raise KeyError(args[0]) + self.__keys.remove(args[0]) + + def viewitems(self): + raise NotImplementedError + + def viewkeys(self): + raise NotImplementedError + + def viewvalues(self): + raise NotImplementedError
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/browser/libervia_main.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,709 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2011-2018 Jérôme Poisson <goffi@goffi.org> + +# This program 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. + +# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + + +### logging configuration ### +from sat_browser import logging +logging.configure() +from sat.core.log import getLogger +log = getLogger(__name__) +### + +from sat.core.i18n import D_ + +from sat_frontends.quick_frontend.quick_app import QuickApp +from sat_frontends.quick_frontend import quick_widgets +from sat_frontends.quick_frontend import quick_menus + +from sat_frontends.tools.misc import InputHistory +from sat_browser import strings +from sat_frontends.tools import jid +from sat_frontends.tools import host_listener +from sat.core.i18n import _ + +from pyjamas.ui.RootPanel import RootPanel +# from pyjamas.ui.HTML import HTML +from pyjamas.ui.KeyboardListener import KEY_ESCAPE +from pyjamas.Timer import Timer +from pyjamas import Window, DOM + +from sat_browser import json +from sat_browser import register +from sat_browser.contact_list import ContactList +from sat_browser import main_panel +# from sat_browser import chat +from sat_browser import blog +from sat_browser import xmlui +from sat_browser import dialog +from sat_browser import html_tools +from sat_browser import notification +from sat_browser import libervia_widget +from sat_browser import web_widget +assert web_widget # XXX: just here to avoid pyflakes warning + +from sat_browser.constants import Const as C + + +try: + # FIXME: import plugin dynamically + from sat_browser import plugin_sec_otr +except ImportError: + pass + + +unicode = str # FIXME: pyjamas workaround + + +# MAX_MBLOG_CACHE = 500 # Max microblog entries kept in memories # FIXME + + +class SatWebFrontend(InputHistory, QuickApp): + ENCRYPTION_HANDLERS = False # e2e encryption is handled directly by Libervia, + # not backend + + def onModuleLoad(self): + log.info("============ onModuleLoad ==============") + self.bridge_signals = json.BridgeSignals(self) + QuickApp.__init__(self, json.BridgeCall, xmlui=xmlui, connect_bridge=False) + self.connectBridge() + self._profile_plugged = False + self.signals_cache[C.PROF_KEY_NONE] = [] + self.panel = main_panel.MainPanel(self) + self.tab_panel = self.panel.tab_panel + self.tab_panel.addTabListener(self) + self._register_box = None + RootPanel().add(self.panel) + + self.alerts_counter = notification.FaviconCounter() + self.notification = notification.Notification(self.alerts_counter) + DOM.addEventPreview(self) + self.importPlugins() + self._register = json.RegisterCall() + self._register.call('menusGet', self.gotMenus) + self._register.call('registerParams', None) + self._register.call('getSessionMetadata', self._getSessionMetadataCB) + self.initialised = False + self.init_cache = [] # used to cache events until initialisation is done + self.cached_params = {} + self.next_rsm_index = 0 + + #FIXME: microblog cache should be managed directly in blog module + self.mblog_cache = [] # used to keep our own blog entries in memory, to show them in new mblog panel + + self._versions={} # SàT and Libervia versions cache + + @property + def whoami(self): + # XXX: works because Libervia is mono-profile + # if one day Libervia manage several profiles at once, this must be deleted + return self.profiles[C.PROF_KEY_NONE].whoami + + @property + def contact_list(self): + return self.contact_lists[C.PROF_KEY_NONE] + + @property + def visible_widgets(self): + widgets_panel = self.tab_panel.getCurrentPanel() + return [wid for wid in widgets_panel.widgets if isinstance(wid, quick_widgets.QuickWidget)] + + @property + def base_location(self): + """Return absolute base url of this Libervia instance""" + url = Window.getLocation().getHref() + if url.endswith(C.LIBERVIA_MAIN_PAGE): + url = url[:-len(C.LIBERVIA_MAIN_PAGE)] + if url.endswith("/"): + url = url[:-1] + return url + + @property + def sat_version(self): + return self._versions["sat"] + + @property + def libervia_version(self): + return self._versions["libervia"] + + def getVersions(self, callback=None): + """Ask libervia server for SàT and Libervia version and fill local cache + + @param callback: method to call when both versions have been received + """ + def gotVersion(): + if len(self._versions) == 2 and callback is not None: + callback() + + if len(self._versions) == 2: + # we already have versions in cache + gotVersion() + return + + def gotSat(version): + self._versions["sat"] = version + gotVersion() + + def gotLibervia(version): + self._versions["libervia"] = version + gotVersion() + + self.bridge.getVersion(callback=gotSat, profile=None) + self.bridge.getLiberviaVersion(callback=gotLibervia, profile=None) # XXX: bridge direct call expect a profile, even for method with no profile needed + + def registerSignal(self, functionName, handler=None, iface="core", with_profile=True): + if handler is None: + callback = getattr(self, "{}{}".format(functionName, "Handler")) + else: + callback = handler + + self.bridge_signals.register_signal(functionName, callback, with_profile=with_profile) + + def importPlugins(self): + self.plugins = {} + try: + self.plugins['otr'] = plugin_sec_otr.OTR(self) + except TypeError: # plugin_sec_otr has not been imported + pass + + def getSelected(self): + wid = self.tab_panel.getCurrentPanel() + if not isinstance(wid, libervia_widget.WidgetsPanel): + log.error("Tab widget is not a WidgetsPanel, can't get selected widget") + return None + return wid.selected + + def setSelected(self, widget): + """Define the selected widget""" + widgets_panel = self.tab_panel.getCurrentPanel() + if not isinstance(widgets_panel, libervia_widget.WidgetsPanel): + return + + selected = widgets_panel.selected + + if selected == widget: + return + + if selected: + selected.removeStyleName('selected_widget') + + # FIXME: check that widget is in the current WidgetsPanel + widgets_panel.selected = widget + self.selected_widget = widget + + if widget: + widgets_panel.selected.addStyleName('selected_widget') + + def resize(self): + """Resize elements""" + Window.onResize() + + def onBeforeTabSelected(self, sender, tab_index): + return True + + def onTabSelected(self, sender, tab_index): + pass + # def onTabSelected(self, sender, tab_index): + # for widget in self.tab_panel.getCurrentPanel().widgets: + # if isinstance(widget, chat.Chat): + # clist = self.contact_list + # clist.removeAlerts(widget.current_target, True) + + def onEventPreview(self, event): + if event.type in ["keydown", "keypress", "keyup"] and event.keyCode == KEY_ESCAPE: + #needed to prevent request cancellation in Firefox + event.preventDefault() + return True + + def getAvatarURL(self, jid_): + """Return avatar of a jid if in cache, else ask for it. + + @param jid_ (jid.JID): JID of the contact + @return: the URL to the avatar (unicode) + """ + return self.getAvatar(jid_) or self.getDefaultAvatar() + + def getDefaultAvatar(self): + return C.DEFAULT_AVATAR_URL + + def registerWidget(self, wid): + log.debug(u"Registering %s" % wid.getDebugName()) + self.libervia_widgets.add(wid) + + def unregisterWidget(self, wid): + try: + self.libervia_widgets.remove(wid) + except KeyError: + log.warning(u'trying to remove a non registered Widget: %s' % wid.getDebugName()) + + def refresh(self): + """Refresh the general display.""" + self.contact_list.refresh() + for lib_wid in self.libervia_widgets: + lib_wid.refresh() + self.resize() + + def addWidget(self, wid, tab_index=None): + """ Add a widget at the bottom of the current or specified tab + + @param wid: LiberviaWidget to add + @param tab_index: index of the tab to add the widget to + """ + if tab_index is None or tab_index < 0 or tab_index >= self.tab_panel.getWidgetCount(): + panel = self.tab_panel.getCurrentPanel() + else: + panel = self.tab_panel.deck.getWidget(tab_index) + panel.addWidget(wid) + + def gotMenus(self, backend_menus): + """Put the menus data in cache and build the main menu bar + + @param backend_menus (list[tuple]): menu data from backend + """ + main_menu = self.panel.menu # most of global menu callbacks are in main_menu + + # Categories (with icons) + self.menus.addCategory(C.MENU_GLOBAL, [D_(u"General")], extra={'icon': 'home'}) + self.menus.addCategory(C.MENU_GLOBAL, [D_(u"Contacts")], extra={'icon': 'social'}) + self.menus.addCategory(C.MENU_GLOBAL, [D_(u"Groups")], extra={'icon': 'social'}) + #self.menus.addCategory(C.MENU_GLOBAL, [D_(u"Games")], extra={'icon': 'games'}) + + # menus to have before backend menus + self.menus.addMenu(C.MENU_GLOBAL, (D_(u"Groups"), D_(u"Discussion")), callback=main_menu.onJoinRoom) + + # menus added by the backend/plugins (include other types than C.MENU_GLOBAL) + self.menus.addMenus(backend_menus, top_extra={'icon': 'plugins'}) + + # menus to have under backend menus + self.menus.addMenu(C.MENU_GLOBAL, (D_(u"Contacts"), D_(u"Manage contact groups")), callback=main_menu.onManageContactGroups) + + # separator and right hand menus + self.menus.addMenuItem(C.MENU_GLOBAL, [], quick_menus.MenuSeparator()) + + self.menus.addMenu(C.MENU_GLOBAL, (D_(u"Help"), D_("Official chat room")), top_extra={'icon': 'help'}, callback=main_menu.onOfficialChatRoom) + self.menus.addMenu(C.MENU_GLOBAL, (D_(u"Help"), D_("Social contract")), top_extra={'icon': 'help'}, callback=main_menu.onSocialContract) + self.menus.addMenu(C.MENU_GLOBAL, (D_(u"Help"), D_("About")), callback=main_menu.onAbout) + self.menus.addMenu(C.MENU_GLOBAL, (D_(u"Settings"), D_("Account")), top_extra={'icon': 'settings'}, callback=main_menu.onAccount) + self.menus.addMenu(C.MENU_GLOBAL, (D_(u"Settings"), D_("Parameters")), callback=main_menu.onParameters) + # XXX: temporary, will change when a full profile will be managed in SàT + self.menus.addMenu(C.MENU_GLOBAL, (D_(u"Settings"), D_("Upload avatar")), callback=main_menu.onAvatarUpload) + + # we call listener to have menu added by local classes/plugins + self.callListeners('gotMenus') # FIXME: to be done another way or moved to quick_app + + # and finally the menus which must appear at the bottom + self.menus.addMenu(C.MENU_GLOBAL, (D_(u"General"), D_(u"Disconnect")), callback=main_menu.onDisconnect) + + # we can now display all the menus + main_menu.update(C.MENU_GLOBAL) + + # XXX: temp, will be reworked in the backed static blog plugin + self.menus.addMenu(C.MENU_JID_CONTEXT, (D_(u"User"), D_("Public blog")), callback=main_menu.onPublicBlog) + + def removeListener(self, type_, callback): + """Remove a callback from listeners + + @param type_: same as for [addListener] + @param callback: callback to remove + """ + # FIXME: workaround for pyjamas + # check KeyError issue + assert type_ in C.LISTENERS + try: + self._listeners[type_].pop(callback) + except KeyError: + pass + + def _getSessionMetadataCB(self, metadata): + if not metadata['plugged']: + warning = metadata.get("warning") + self.panel.setStyleAttribute("opacity", "0.25") # set background transparency + self._register_box = register.RegisterBox(self.logged, metadata) + self._register_box.centerBox() + self._register_box.show() + if warning: + dialog.InfoDialog(_('Security warning'), warning).show() + self._tryAutoConnect(skip_validation=not not warning) + else: + self._register.call('isConnected', self._isConnectedCB) + + def _isConnectedCB(self, connected): + if not connected: + self._register.call('connect', lambda x: self.logged()) + else: + self.logged() + + def logged(self): + self.panel.setStyleAttribute("opacity", "1") # background becomes foreground + if self._register_box: + self._register_box.hide() + del self._register_box # don't work if self._register_box is None + + # display the presence status panel and tab bar + self.presence_status_panel = main_panel.PresenceStatusPanel(self) + self.panel.addPresenceStatusPanel(self.presence_status_panel) + self.panel.tab_panel.getTabBar().setVisible(True) + + self.bridge_signals.getSignals(callback=self.bridge_signals.signalHandler, profile=None) + + def domain_cb(value): + self._defaultDomain = value + log.info(u"new account domain: %s" % value) + + def domain_eb(value): + self._defaultDomain = "libervia.org" + + self.bridge.getNewAccountDomain(callback=domain_cb, errback=domain_eb) + self.plug_profiles([C.PROF_KEY_NONE]) # XXX: None was used intitially, but pyjamas bug when using variable arguments and None is the only arg. + + def profilePlugged(self, dummy): + self._profile_plugged = True + QuickApp.profilePlugged(self, C.PROF_KEY_NONE) + contact_list = self.widgets.getOrCreateWidget(ContactList, None, on_new_widget=None, profile=C.PROF_KEY_NONE) + self.contact_list_widget = contact_list + self.panel.addContactList(contact_list) + + # FIXME: the contact list height has to be set manually the first time + self.resize() + + # XXX: as contact_list.update() is slow and it's called a lot of time + # during profile plugging, we prevent it before it's plugged + # and do all at once now + contact_list.update() + + try: + self.mblog_available = C.bool(self.features['XEP-0277']['available']) + except KeyError: + self.mblog_available = False + + try: + self.groupblog_available = C.bool(self.features['GROUPBLOG']['available']) + except KeyError: + self.groupblog_available = False + + blog_widget = self.displayWidget(blog.Blog, ()) + self.setSelected(blog_widget) + + if self.mblog_available: + if not self.groupblog_available: + dialog.InfoDialog(_(u"Group blogging not available"), _(u"Your server can manage (micro)blogging, but not fine permissions.<br />You'll only be able to blog publicly.")).show() + + else: + dialog.InfoDialog(_(u"Blogging not available"), _(u"Your server can't handle (micro)blogging.<br />You'll be able to see your contacts (micro)blogs, but not to post yourself.")).show() + + # we fill the panels already here + # for wid in self.widgets.getWidgets(blog.MicroblogPanel): + # if wid.accept_all(): + # self.bridge.getMassiveMblogs('ALL', (), None, profile=C.PROF_KEY_NONE, callback=wid.massiveInsert) + # else: + # self.bridge.getMassiveMblogs('GROUP', list(wid.accepted_groups), None, profile=C.PROF_KEY_NONE, callback=wid.massiveInsert) + + #we ask for our own microblogs: + # self.loadOurMainEntries() + + def gotDefaultMUC(default_muc): + self.default_muc = default_muc + self.bridge.mucGetDefaultService(profile=None, callback=gotDefaultMUC) + + def newWidget(self, wid): + log.debug(u"newWidget: {}".format(wid)) + self.addWidget(wid) + + def newMessageHandler(self, from_jid_s, msg, type_, to_jid_s, extra, profile=C.PROF_KEY_NONE): + if type_ == C.MESS_TYPE_HEADLINE: + from_jid = jid.JID(from_jid_s) + if from_jid.domain == self._defaultDomain: + # we display announcement from the server in a dialog for better visibility + try: + title = extra['subject'] + except KeyError: + title = _('Announcement from %s') % from_jid + msg = strings.addURLToText(html_tools.XHTML2Text(msg)) + dialog.InfoDialog(title, msg).show() + return + QuickApp.newMessageHandler(self, from_jid_s, msg, type_, to_jid_s, extra, profile) + + def disconnectedHandler(self, profile): + QuickApp.disconnectedHandler(self, profile) + Window.getLocation().reload() + + def setPresenceStatus(self, show='', status=None, profile=C.PROF_KEY_NONE): + self.presence_status_panel.setPresence(show) + if status is not None: + self.presence_status_panel.setStatus(status) + + def _tryAutoConnect(self, skip_validation=False): + """This method retrieve the eventual URL parameters to auto-connect the user. + @param skip_validation: if True, set the form values but do not validate it + """ + params = strings.getURLParams(Window.getLocation().getSearch()) + if "login" in params: + self._register_box._form.right_side.showStack(0) + self._register_box._form.login_box.setText(params["login"]) + self._register_box._form.login_pass_box.setFocus(True) + if "passwd" in params: + # try to connect + self._register_box._form.login_pass_box.setText(params["passwd"]) + if not skip_validation: + self._register_box._form.onLogin(None) + return True + else: + # this would eventually set the browser saved password + Timer(5, lambda: self._register_box._form.login_pass_box.setFocus(True)) + + def _actionManagerUnknownError(self): + dialog.InfoDialog("Error", + "Unmanaged action result", Width="400px").center() + + # def _ownBlogsFills(self, mblogs, mblog_panel=None): + # """Put our own microblogs in cache, then fill the panels with them. + + # @param mblogs (dict): dictionary mapping a publisher JID to blogs data. + # @param mblog_panel (MicroblogPanel): the panel to fill, or all if None. + # """ + # cache = [] + # for publisher in mblogs: + # for mblog in mblogs[publisher][0]: + # if 'content' not in mblog: + # log.warning(u"No content found in microblog [%s]" % mblog) + # continue + # if 'groups' in mblog: + # _groups = set(mblog['groups'].split() if mblog['groups'] else []) + # else: + # _groups = None + # mblog_entry = blog.MicroblogItem(mblog) + # cache.append((_groups, mblog_entry)) + + # self.mblog_cache.extend(cache) + # if len(self.mblog_cache) > MAX_MBLOG_CACHE: + # del self.mblog_cache[0:len(self.mblog_cache - MAX_MBLOG_CACHE)] + + # widget_list = [mblog_panel] if mblog_panel else self.widgets.getWidgets(blog.MicroblogPanel) + + # for wid in widget_list: + # self.fillMicroblogPanel(wid, cache) + + # # FIXME + + # if self.initialised: + # return + # self.initialised = True # initialisation phase is finished here + # for event_data in self.init_cache: # so we have to send all the cached events + # self.personalEventHandler(*event_data) + # del self.init_cache + + # def loadOurMainEntries(self, index=0, mblog_panel=None): + # """Load a page of our own blogs from the cache or ask them to the + # backend. Then fill the panels with them. + + # @param index (int): starting index of the blog page to retrieve. + # @param mblog_panel (MicroblogPanel): the panel to fill, or all if None. + # """ + # delta = index - self.next_rsm_index + # if delta < 0: + # assert mblog_panel is not None + # self.fillMicroblogPanel(mblog_panel, self.mblog_cache[index:index + C.RSM_MAX_ITEMS]) + # return + + # def cb(result): + # self._ownBlogsFills(result, mblog_panel) + + # rsm = {'max_': str(delta + C.RSM_MAX_ITEMS), 'index': str(self.next_rsm_index)} + # self.bridge.getMassiveMblogs('JID', [unicode(self.whoami.bare)], rsm, callback=cb, profile=C.PROF_KEY_NONE) + # self.next_rsm_index = index + C.RSM_MAX_ITEMS + + ## Signals callbacks ## + + # def personalEventHandler(self, sender, event_type, data): + # elif event_type == 'MICROBLOG_DELETE': + # for wid in self.widgets.getWidgets(blog.MicroblogPanel): + # wid.removeEntry(data['type'], data['id']) + + # if sender == self.whoami.bare and data['type'] == 'main_item': + # for index in xrange(0, len(self.mblog_cache)): + # entry = self.mblog_cache[index] + # if entry[1].id == data['id']: + # self.mblog_cache.remove(entry) + # break + + # def fillMicroblogPanel(self, mblog_panel, mblogs): + # """Fill a microblog panel with entries in cache + + # @param mblog_panel: MicroblogPanel instance + # """ + # #XXX: only our own entries are cached + # for cache_entry in mblogs: + # _groups, mblog_entry = cache_entry + # mblog_panel.addEntryIfAccepted(self.whoami.bare, *cache_entry) + + # def getEntityMBlog(self, entity): + # # FIXME: call this after a contact has been added to roster + # log.info(u"geting mblog for entity [%s]" % (entity,)) + # for lib_wid in self.libervia_widgets: + # if isinstance(lib_wid, blog.MicroblogPanel): + # if lib_wid.isJidAccepted(entity): + # self.bridge.call('getMassiveMblogs', lib_wid.massiveInsert, 'JID', [unicode(entity)]) + + def displayWidget(self, class_, target, dropped=False, new_tab=None, *args, **kwargs): + """Get or create a LiberviaWidget and select it. When the user dropped + something, a new widget is always created, otherwise we look for an + existing widget and re-use it if it's in the current tab. + + @arg class_(class): see quick_widgets.getOrCreateWidget + @arg target: see quick_widgets.getOrCreateWidget + @arg dropped(bool): if True, assume the widget has been dropped + @arg new_tab(unicode): if not None, it holds the name of a new tab to + open for the widget. If None, use the default behavior. + @param args(list): optional args to create a new instance of class_ + @param kwargs(list): optional kwargs to create a new instance of class_ + @return: the widget + """ + kwargs['profile'] = C.PROF_KEY_NONE + + if dropped: + kwargs['on_new_widget'] = None + kwargs['on_existing_widget'] = C.WIDGET_RECREATE + wid = self.widgets.getOrCreateWidget(class_, target, *args, **kwargs) + self.setSelected(wid) + return wid + + if new_tab: + kwargs['on_new_widget'] = None + kwargs['on_existing_widget'] = C.WIDGET_RECREATE + wid = self.widgets.getOrCreateWidget(class_, target, *args, **kwargs) + self.tab_panel.addWidgetsTab(new_tab) + self.addWidget(wid, tab_index=self.tab_panel.getWidgetCount() - 1) + return wid + + kwargs['on_existing_widget'] = C.WIDGET_RAISE + try: + wid = self.widgets.getOrCreateWidget(class_, target, *args, **kwargs) + except quick_widgets.WidgetAlreadyExistsError: + kwargs['on_existing_widget'] = C.WIDGET_KEEP + wid = self.widgets.getOrCreateWidget(class_, target, *args, **kwargs) + widgets_panel = wid.getParent(libervia_widget.WidgetsPanel, expect=False) + if widgets_panel is None: + # The widget exists but is hidden + self.addWidget(wid) + elif widgets_panel != self.tab_panel.getCurrentPanel(): + # the widget is on an other tab, so we add a new one here + kwargs['on_existing_widget'] = C.WIDGET_RECREATE + wid = self.widgets.getOrCreateWidget(class_, target, *args, **kwargs) + self.addWidget(wid) + self.setSelected(wid) + return wid + + def isHidden(self): + """Tells if the frontend window is hidden. + + @return bool + """ + return self.notification.isHidden() + + def updateAlertsCounter(self, extra_inc=0): + """Update the over whole alerts counter + + @param extra_inc (int): extra counter + """ + extra = self.alerts_counter.extra + extra_inc + self.alerts_counter.update(self.alerts_count, extra=extra) + + def _paramUpdate(self, name, value, category, refresh=True): + """This is called when the paramUpdate signal is received, but also + during initialization when the UI parameters values are retrieved. + @param refresh: set to True to refresh the general UI + """ + for param_cat, param_name in C.CACHED_PARAMS: + if name == param_name and category == param_cat: + self.cached_params[(category, name)] = value + if refresh: + self.refresh() + break + + def getCachedParam(self, category, name): + """Return a parameter cached value (e.g for refreshing the UI) + + @param category (unicode): the parameter category + @pram name (unicode): the parameter name + """ + return self.cached_params[(category, name)] if (category, name) in self.cached_params else None + + def sendError(self, errorData): + dialog.InfoDialog("Error while sending message", + "Your message can't be sent", Width="400px").center() + log.error("sendError: %s" % unicode(errorData)) + + def showWarning(self, type_=None, msg=None): + """Display a popup information message, e.g. to notify the recipient of a message being composed. + If type_ is None, a popup being currently displayed will be hidden. + @type_: a type determining the CSS style to be applied (see WarningPopup.showWarning) + @msg: message to be displayed + """ + if not hasattr(self, "warning_popup"): + self.warning_popup = main_panel.WarningPopup() + self.warning_popup.showWarning(type_, msg) + + def showDialog(self, message, title="", type_="info", answer_cb=None, answer_data=None): + if type_ == 'info': + popup = dialog.InfoDialog(unicode(title), unicode(message), callback=answer_cb) + elif type_ == 'error': + popup = dialog.InfoDialog(unicode(title), unicode(message), callback=answer_cb) + elif type_ == 'yes/no': + popup = dialog.ConfirmDialog(lambda answer: answer_cb(answer, answer_data), + text=unicode(message), title=unicode(title)) + popup.cancel_button.setText(_("No")) + else: + popup = dialog.InfoDialog(unicode(title), unicode(message), callback=answer_cb) + log.error(_('unmanaged dialog type: %s'), type_) + popup.show() + + def dialogFailure(self, failure): + dialog.InfoDialog("Error", + unicode(failure), Width="400px").center() + + def showFailure(self, err_data, msg=''): + """Show a failure that has been returned by an asynchronous bridge method. + + @param failure (defer.Failure): Failure instance + @param msg (unicode): message to display + """ + # FIXME: message is lost by JSON, we hardcode it for now... remove msg argument when possible + err_code, err_obj = err_data + title = err_obj['message']['faultString'] if isinstance(err_obj['message'], dict) else err_obj['message'] + self.showDialog(msg, title, 'error') + + def onJoinMUCFailure(self, err_data): + """Show a failure that has been returned when trying to join a room. + + @param failure (defer.Failure): Failure instance + """ + # FIXME: remove asap, see self.showFailure + err_code, err_obj = err_data + if err_obj["data"] == "AlreadyJoinedRoom": + msg = _(u"The room has already been joined.") + err_obj["message"] = _(u"Information") + else: + msg = _(u"Invalid room identifier. Please give a room short or full identifier like 'room' or '%s'.") % self.default_muc + err_obj["message"] = _(u"Error") + self.showFailure(err_data, msg) + + +if __name__ == '__main__': + app = SatWebFrontend() + app.onModuleLoad() + host_listener.callListeners(app)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/browser/libervia_test.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,78 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2011-2018 Jérôme Poisson <goffi@goffi.org> + +# This program 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. + +# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + + +# Just visit <root_url>/test. If you don't get any AssertError pop-up, +# everything is fine. #TODO: nicely display the results in HTML output. + + +### logging configuration ### +from sat_browser import logging +logging.configure() +from sat.core.log import getLogger +log = getLogger(__name__) +### + +from sat_frontends.tools import jid +from sat_browser import contact_list + + +def test_JID(): + """Check that the JID class reproduces the Twisted behavior""" + j1 = jid.JID("t1@test.org") + j1b = jid.JID("t1@test.org") + t1 = "t1@test.org" + + assert j1 == j1b + assert j1 != t1 + assert t1 != j1 + assert hash(j1) == hash(j1b) + assert hash(j1) != hash(t1) + + +def test_JIDIterable(): + """Check that our iterables reproduce the Twisted behavior""" + + j1 = jid.JID("t1@test.org") + j1b = jid.JID("t1@test.org") + j2 = jid.JID("t2@test.org") + t1 = "t1@test.org" + t2 = "t2@test.org" + jid_set = set([j1, t2]) + jid_list = contact_list.JIDList([j1, t2]) + jid_dict = {j1: "dummy 1", t2: "dummy 2"} + for iterable in (jid_set, jid_list, jid_dict): + log.info("Testing %s" % type(iterable)) + assert j1 in iterable + assert j1b in iterable + assert j2 not in iterable + assert t1 not in iterable + assert t2 in iterable + + # Check that the extra JIDList class is still needed + log.info("Testing Pyjamas native list") + jid_native_list = ([j1, t2]) + assert j1 in jid_native_list + assert j1b not in jid_native_list # this is NOT Twisted's behavior + assert j2 in jid_native_list # this is NOT Twisted's behavior + assert t1 in jid_native_list # this is NOT Twisted's behavior + assert t2 in jid_native_list + +test_JID() +test_JIDIterable()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/browser/otr.min.js Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,42 @@ +/* + + This file contains minified versions of otr.js, Big Integer Library, CryptoJS, EventEmitter. + It is sublicensed under AGPL v3 (or any later version) as allowed by the original licenses. + + Below some information about the authors and the licences of the included libraries: + + Big Integer Library v. 5.5 + Created 2000, last modified 2013 + Leemon Baird + www.leemon.com + Original licence information: + // This file is public domain. You can use it for any purpose without restriction. + // I do not guarantee that it is correct, so use it at your own risk. If you use + // it for something interesting, I'd appreciate hearing about it. If you find + // any bugs or make any improvements, I'd appreciate hearing about those too. + // It would also be nice if my name and URL were left in the comments. But none + // of that is required. + + CryptoJS v3.1.2 + code.google.com/p/crypto-js + (c) 2009-2013 by Jeff Mott. All rights reserved. + code.google.com/p/crypto-js/wiki/License (MIT licence) + + EventEmitter v4.2.3 - git.io/ee + Oliver Caldwell + MIT license + + otr.js v0.2.12 - 2014-04-15 + (c) 2014 - Arlo Breault <arlolra@gmail.com> + Freely distributed under the MPL v2.0 license. + +*/ + +if (typeof crypto !== 'undefined' && (typeof crypto.randomBytes === 'function' || typeof crypto.getRandomValues === 'function')) { + +(function(a,b){if(typeof define==="function"&&define.amd){define(b.bind(a,a.crypto||a.msCrypto))}else{if(typeof module!=="undefined"&&module.exports){module.exports=b(require("crypto"))}else{a.BigInt=b(a.crypto||a.msCrypto)}}}(this,function(v){var x=26;var E=1<<x;var aw=E-1;var bd="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_=!@#$%^&*()[]{}|;:,.<>/?`~ \\'\"+-";var o=a0(1,1,1);var W=new Array(0);var aT=W;var n=W;var k=W;var j=W;var i=W;var h=W,g=W;var f=W;var e=W;var ar=W;var a2=W;var aR=W,F=W,R=W;var S=W,V=W,aG=W,aE=W,aC=W,aA=W;var ah=W,ag=W,af=W,aL=W,P=W,O=W,aH=W;var N=W,al=W,aa=W,aQ=W,ao=W,bf=W,U=W,a8=W;var ae=W,H=W,X=W,ad=W,ab=W,M=W,L=W,aW=W;var a7=W;function ba(bl){var T,bj,bk,t;bj=new Array(bl);for(T=0;T<bl;T++){bj[T]=0}bj[0]=2;bk=0;for(;bj[bk]<bl;){for(T=bj[bk]*bj[bk];T<bl;T+=bj[bk]){bj[T]=1}bk++;bj[bk]=bj[bk-1]+1;for(;bj[bk]<bl&&bj[bj[bk]];bj[bk]++){}}t=new Array(bk);for(T=0;T<bk;T++){t[T]=bj[T]}return t}function l(T,t){if(aR.length!=T.length){aR=K(T);F=K(T);R=K(T)}y(R,t);return aN(T,R)}function aN(T,t){var bl,bk,bj,bm;if(aR.length!=T.length){aR=K(T);F=K(T);R=K(T)}ax(R,t);ax(F,T);ax(aR,T);at(F,-1);at(aR,-1);if(aZ(F)){return 0}for(bj=0;F[bj]==0;bj++){}for(bl=1,bk=2;F[bj]%bk==0;bk*=2,bl++){}bm=bj*x+bl-1;if(bm){G(F,bm)}aS(R,F,T);if(!z(R,1)&&!Q(R,aR)){bk=1;while(bk<=bm-1&&!Q(R,aR)){aJ(R,T);if(z(R,1)){return 0}bk++}if(!Q(R,aR)){return 0}}return 1}function ay(t){var bj,bk,T;for(bj=t.length-1;(t[bj]==0)&&(bj>0);bj--){}for(bk=0,T=t[bj];T;(T>>=1),bk++){}bk+=x*bj;return bk}function aD(t,bj){var T=a0(0,(t.length>bj?t.length:bj)*x,0);ax(T,t);return T}function ak(T){var t=a0(0,T,0);aq(t,T);return a4(t,1)}function D(t){if(t>=600){return q(t,2)}if(t>=550){return q(t,4)}if(t>=500){return q(t,5)}if(t>=400){return q(t,6)}if(t>=350){return q(t,7)}if(t>=300){return q(t,9)}if(t>=250){return q(t,12)}if(t>=200){return q(t,15)}if(t>=150){return q(t,18)}if(t>=100){return q(t,27)}return q(t,40)}function q(bj,bm){var T,bk,t,bl;bl=30000;T=a0(0,bj,0);if(N.length==0){N=ba(30000)}if(a7.length!=T.length){a7=K(T)}for(;;){a1(T,bj,0);T[0]|=1;t=0;for(bk=0;(bk<N.length)&&(N[bk]<=bl);bk++){if(be(T,N[bk])==0&&!z(T,N[bk])){t=1;break}}for(bk=0;bk<bm&&!t;bk++){a1(a7,bj,0);while(!B(T,a7)){a1(a7,bj,0)}if(!aN(T,a7)){t=1}}if(!t){return T}}}function b(t,bj){var T=K(t);aB(T,bj);return a4(T,1)}function p(t,bj){var T=aD(t,t.length+1);at(T,bj);return a4(T,1)}function d(t,bj){var T=aD(t,t.length+bj.length);aY(T,bj);return a4(T,1)}function aP(t,bk,bj){var T=aD(t,bj.length);aS(T,a4(bk,2),a4(bj,2),0);return a4(T,1)}function a5(t,bj){var T=aD(t,(t.length>bj.length?t.length+1:bj.length+1));bc(T,bj);return a4(T,1)}function w(t,bj){var T=aD(t,(t.length>bj.length?t.length+1:bj.length+1));aI(T,bj);return a4(T,1)}function bg(t,bk){var T=aD(t,bk.length);var bj;bj=Z(T,bk);return bj?a4(T,1):null}function an(t,bk,bj){var T=aD(t,bj.length);aU(T,bk,bj);return a4(T,1)}function aq(bt,bm){var br,bu,bk,bl,bv,bn,t,T,bp,bq,bj,bo,bs;if(N.length==0){N=ba(30000)}if(al.length==0){al=new Array(512);for(bn=0;bn<512;bn++){al[bn]=Math.pow(2,bn/511-1)}}br=0.1;bk=20;bs=20;if(aQ.length!=bt.length){aQ=K(bt);ao=K(bt);a8=K(bt);H=K(bt);ab=K(bt);M=K(bt);L=K(bt);ad=K(bt);X=K(bt);aa=K(bt);bf=K(bt);U=K(bt);ae=K(bt);aW=K(bt)}if(bm<=bs){bl=(1<<((bm+2)>>1))-1;y(bt,0);for(bv=1;bv;){bv=0;bt[0]=1|(1<<(bm-1))|aV(bm);for(bn=1;(bn<N.length)&&((N[bn]&bl)==N[bn]);bn++){if(0==(bt[0]%N[bn])){bv=1;break}}}Y(bt);return}T=br*bm*bm;if(bm>2*bk){for(t=1;bm-bm*t<=bk;){t=al[aV(9)]}}else{t=0.5}bo=Math.floor(t*bm)+1;aq(U,bo);y(aQ,0);aQ[Math.floor((bm-2)/x)]|=(1<<((bm-2)%x));c(aQ,U,aa,bf);bq=ay(aa);for(;;){for(;;){a1(ao,bq,0);if(B(aa,ao)){break}}at(ao,1);aI(ao,aa);ax(X,U);aY(X,ao);m(X,2);at(X,1);ax(H,ao);m(H,2);for(bp=0,bn=0;(bn<N.length)&&(N[bn]<T);bn++){if(be(X,N[bn])==0&&!z(X,N[bn])){bp=1;break}}if(!bp){if(!l(X,2)){bp=1}}if(!bp){at(X,-3);for(bn=X.length-1;(X[bn]==0)&&(bn>0);bn--){}for(bj=0,bu=X[bn];bu;(bu>>=1),bj++){}bj+=x*bn;for(;;){a1(ae,bj,0);if(B(X,ae)){break}}at(X,3);at(ae,2);ax(ad,ae);ax(a8,X);at(a8,-1);aS(ad,a8,X);at(ad,-1);if(aZ(ad)){ax(ad,ae);aS(ad,H,X);at(ad,-1);ax(aW,X);ax(ab,ad);A(ab,X);if(z(ab,1)){ax(bt,aW);return}}}}}function aO(bk,bj){var T,t;T=Math.floor((bk-1)/x)+2;t=a0(0,0,T);a1(t,bk,bj);return t}function a1(t,bl,bk){var bj,T;for(bj=0;bj<t.length;bj++){t[bj]=0}T=Math.floor((bl-1)/x)+1;for(bj=0;bj<T;bj++){t[bj]=aV(x)}t[T-1]&=(2<<((bl-1)%x))-1;if(bk==1){t[T-1]|=(1<<((bl-1)%x))}}function ap(t,bk){var T,bj;T=K(t);bj=K(bk);A(T,bj);return T}function A(br,bq){var bp,bo,T,bn,bm,bl,t,bk,bj,bs;if(ar.length!=br.length){ar=K(br)}bj=1;while(bj){bj=0;for(bp=1;bp<bq.length;bp++){if(bq[bp]){bj=1;break}}if(!bj){break}for(bp=br.length;!br[bp]&&bp>=0;bp--){}bo=br[bp];T=bq[bp];bn=1;bm=0;bl=0;t=1;while((T+bl)&&(T+t)){bk=Math.floor((bo+bn)/(T+bl));bs=Math.floor((bo+bm)/(T+t));if(bk!=bs){break}W=bn-bk*bl;bn=bl;bl=W;W=bm-bk*t;bm=t;t=W;W=bo-bk*T;bo=T;T=W}if(bm){ax(ar,br);J(br,bq,bn,bm);J(bq,ar,t,bl)}else{aB(br,bq);ax(ar,br);ax(br,bq);ax(bq,ar)}}if(bq[0]==0){return}W=be(br,bq[0]);y(br,bq[0]);bq[0]=W;while(bq[0]){br[0]%=bq[0];W=br[0];br[0]=bq[0];bq[0]=W}}function Z(t,bj){var T=1+2*Math.max(t.length,bj.length);if(!(t[0]&1)&&!(bj[0]&1)){y(t,0);return 0}if(V.length!=T){V=new Array(T);S=new Array(T);aG=new Array(T);aE=new Array(T);aC=new Array(T);aA=new Array(T)}ax(V,t);ax(S,bj);y(aG,1);y(aE,0);y(aC,0);y(aA,1);for(;;){while(!(V[0]&1)){s(V);if(!(aG[0]&1)&&!(aE[0]&1)){s(aG);s(aE)}else{aI(aG,bj);s(aG);bc(aE,t);s(aE)}}while(!(S[0]&1)){s(S);if(!(aC[0]&1)&&!(aA[0]&1)){s(aC);s(aA)}else{aI(aC,bj);s(aC);bc(aA,t);s(aA)}}if(!B(S,V)){bc(V,S);bc(aG,aC);bc(aE,aA)}else{bc(S,V);bc(aC,aG);bc(aA,aE)}if(z(V,0)){while(r(aC)){aI(aC,bj)}ax(t,aC);if(!z(S,1)){y(t,0);return 0}return 1}}}function aX(bj,bm){var bk=1,T=0,bl;for(;;){if(bj==1){return bk}if(bj==0){return 0}T-=bk*Math.floor(bm/bj);bm%=bj;if(bm==1){return T}if(bm==0){return 0}bk-=T*Math.floor(bj/bm);bj%=bm}}function C(t,T){return aX(t,T)}function u(T,bn,bl,bj,t){var bm=0;var bk=Math.max(T.length,bn.length);if(V.length!=bk){V=new Array(bk);aG=new Array(bk);aE=new Array(bk);aC=new Array(bk);aA=new Array(bk)}while(!(T[0]&1)&&!(bn[0]&1)){s(T);s(bn);bm++}ax(V,T);ax(bl,bn);y(aG,1);y(aE,0);y(aC,0);y(aA,1);for(;;){while(!(V[0]&1)){s(V);if(!(aG[0]&1)&&!(aE[0]&1)){s(aG);s(aE)}else{aI(aG,bn);s(aG);bc(aE,T);s(aE)}}while(!(bl[0]&1)){s(bl);if(!(aC[0]&1)&&!(aA[0]&1)){s(aC);s(aA)}else{aI(aC,bn);s(aC);bc(aA,T);s(aA)}}if(!B(bl,V)){bc(V,bl);bc(aG,aC);bc(aE,aA)}else{bc(bl,V);bc(aC,aG);bc(aA,aE)}if(z(V,0)){while(r(aC)){aI(aC,bn);bc(aA,T)}m(aA,-1);ax(bj,aC);ax(t,aA);bi(bl,bm);return}}}function r(t){return((t[t.length-1]>>(x-1))&1)}function a(t,bn,T){var bl,bm=t.length,bk=bn.length;var bj=((bm+T)<bk)?(bm+T):bk;for(bl=bk-1-T;bl<bm&&bl>=0;bl++){if(t[bl]>0){return 1}}for(bl=bm-1+T;bl<bk;bl++){if(bn[bl]>0){return 0}}for(bl=bj-1;bl>=T;bl--){if(t[bl-T]>bn[bl]){return 1}else{if(t[bl-T]<bn[bl]){return 0}}}return 0}function B(t,bk){var bj;var T=(t.length<bk.length)?t.length:bk.length;for(bj=t.length;bj<bk.length;bj++){if(bk[bj]){return 0}}for(bj=bk.length;bj<t.length;bj++){if(t[bj]){return 1}}for(bj=T-1;bj>=0;bj--){if(t[bj]>bk[bj]){return 1}else{if(t[bj]<bk[bj]){return 0}}}return 0}function c(bt,bq,T,t){var bm,bl;var bk,bj,bs,bp,bn,br,bo;ax(t,bt);for(bl=bq.length;bq[bl-1]==0;bl--){}bo=bq[bl-1];for(br=0;bo;br++){bo>>=1}br=x-br;bi(bq,br);bi(t,br);for(bm=t.length;t[bm-1]==0&&bm>bl;bm--){}y(T,0);while(!a(bq,t,bm-bl)){az(t,bq,bm-bl);T[bm-bl]++}for(bk=bm-1;bk>=bl;bk--){if(t[bk]==bq[bl-1]){T[bk-bl]=aw}else{T[bk-bl]=Math.floor((t[bk]*E+t[bk-1])/bq[bl-1])}for(;;){bp=(bl>1?bq[bl-2]:0)*T[bk-bl];bn=bp;bp=bp&aw;bn=(bn-bp)/E;bs=bn+T[bk-bl]*bq[bl-1];bn=bs;bs=bs&aw;bn=(bn-bs)/E;if(bn==t[bk]?bs==t[bk-1]?bp>(bk>1?t[bk-2]:0):bs>t[bk-1]:bn>t[bk]){T[bk-bl]--}else{break}}av(t,bq,-T[bk-bl],bk-bl);if(r(t)){a9(t,bq,bk-bl);T[bk-bl]--}}G(bq,br);G(t,br)}function Y(T){var bk,bj,bl,t;bj=T.length;bl=0;for(bk=0;bk<bj;bk++){bl+=T[bk];t=0;if(bl<0){t=bl&aw;t=-((bl-t)/E);bl+=t*E}T[bk]=bl&aw;bl=((bl-T[bk])/E)-t}}function be(t,bk){var T,bj=0;for(T=t.length-1;T>=0;T--){bj=(bj*E+t[T])%bk}return bj}function a0(bk,bl,bm){var bj,T,bn;T=Math.ceil(bl/x)+1;T=bm>T?bm:T;bn=new Array(T);y(bn,bk);return bn}function a6(bt,bk,bj){var bp,bn,bm,bs,br,T;var bl=bt.length;if(bk==-1){bs=new Array(0);for(;;){br=new Array(bs.length+1);for(bn=0;bn<bs.length;bn++){br[bn+1]=bs[bn]}br[0]=parseInt(bt,10);bs=br;bp=bt.indexOf(",",0);if(bp<1){break}bt=bt.substring(bp+1);if(bt.length==0){break}}if(bs.length<bj){br=new Array(bj);ax(br,bs);return br}return bs}var bo=bk,t=0;var bq=bk==1?bl:0;while(bo>1){if(bo&1){t=1}bq+=bl;bo>>=1}bq+=t*bl;bs=a0(0,bq,0);for(bn=0;bn<bl;bn++){bp=bd.indexOf(bt.substring(bn,bn+1),0);if(bk<=36&&bp>=36){bp-=26}if(bp>=bk||bp<0){break}m(bs,bk);at(bs,bp)}for(bl=bs.length;bl>0&&!bs[bl-1];bl--){}bl=bj>bl+1?bj:bl+1;br=new Array(bl);T=bl<bs.length?bl:bs.length;for(bn=0;bn<T;bn++){br[bn]=bs[bn]}for(;bn<bl;bn++){br[bn]=0}return br}function z(t,bj){var T;if(t[0]!=bj){return 0}for(T=1;T<t.length;T++){if(t[T]){return 0}}return 1}function Q(t,bk){var bj;var T=t.length<bk.length?t.length:bk.length;for(bj=0;bj<T;bj++){if(t[bj]!=bk[bj]){return 0}}if(t.length>bk.length){for(;bj<t.length;bj++){if(t[bj]){return 0}}}else{for(;bj<bk.length;bj++){if(bk[bj]){return 0}}}return 1}function aZ(t){var T;for(T=0;T<t.length;T++){if(t[T]){return 0}}return 1}function aM(T,bm){var bk,bj,bl="";if(f.length!=T.length){f=K(T)}else{ax(f,T)}if(bm==-1){for(bk=T.length-1;bk>0;bk--){bl+=T[bk]+","}bl+=T[0]}else{while(!aZ(f)){bj=ac(f,bm);bl=bd.substring(bj,bj+1)+bl}}if(bl.length==0){bl="0"}return bl}function K(t){var T,bj;bj=new Array(t.length);ax(bj,t);return bj}function ax(t,bk){var bj;var T=t.length<bk.length?t.length:bk.length;for(bj=0;bj<T;bj++){t[bj]=bk[bj]}for(bj=T;bj<t.length;bj++){t[bj]=0}}function y(t,bk){var T,bj;for(bj=bk,T=0;T<t.length;T++){t[T]=bj&aw;bj>>=x}}function at(T,bm){var bk,bj,bl,t;T[0]+=bm;bj=T.length;bl=0;for(bk=0;bk<bj;bk++){bl+=T[bk];t=0;if(bl<0){t=bl&aw;t=-((bl-t)/E);bl+=t*E}T[bk]=bl&aw;bl=((bl-T[bk])/E)-t;if(!bl){return}}}function G(t,bk){var bj;var T=Math.floor(bk/x);if(T){for(bj=0;bj<t.length-T;bj++){t[bj]=t[bj+T]}for(;bj<t.length;bj++){t[bj]=0}bk%=x}for(bj=0;bj<t.length-1;bj++){t[bj]=aw&((t[bj+1]<<(x-bk))|(t[bj]>>bk))}t[bj]>>=bk}function s(t){var T;for(T=0;T<t.length-1;T++){t[T]=aw&((t[T+1]<<(x-1))|(t[T]>>1))}t[T]=(t[T]>>1)|(t[T]&(E>>1))}function bi(t,bk){var bj;var T=Math.floor(bk/x);if(T){for(bj=t.length;bj>=T;bj--){t[bj]=t[bj-T]}for(;bj>=0;bj--){t[bj]=0}bk%=x}if(!bk){return}for(bj=t.length-1;bj>0;bj--){t[bj]=aw&((t[bj]<<bk)|(t[bj-1]>>(x-bk)))}t[bj]=aw&(t[bj]<<bk)}function m(T,bm){var bk,bj,bl,t;if(!bm){return}bj=T.length;bl=0;for(bk=0;bk<bj;bk++){bl+=T[bk]*bm;t=0;if(bl<0){t=bl&aw;t=-((bl-t)/E);bl+=t*E}T[bk]=bl&aw;bl=((bl-T[bk])/E)-t}}function ac(t,bl){var T,bk=0,bj;for(T=t.length-1;T>=0;T--){bj=bk*E+t[T];t[T]=Math.floor(bj/bl);bk=bj%bl}return bk}function J(T,bo,bj,t){var bl,bn,bk,bm;bk=T.length<bo.length?T.length:bo.length;bm=T.length;for(bn=0,bl=0;bl<bk;bl++){bn+=bj*T[bl]+t*bo[bl];T[bl]=bn&aw;bn=(bn-T[bl])/E}for(bl=bk;bl<bm;bl++){bn+=bj*T[bl];T[bl]=bn&aw;bn=(bn-T[bl])/E}}function av(T,bo,t,bl){var bk,bn,bj,bm;bj=T.length<bl+bo.length?T.length:bl+bo.length;bm=T.length;for(bn=0,bk=bl;bk<bj;bk++){bn+=T[bk]+t*bo[bk-bl];T[bk]=bn&aw;bn=(bn-T[bk])/E}for(bk=bj;bn&&bk<bm;bk++){bn+=T[bk];T[bk]=bn&aw;bn=(bn-T[bk])/E}}function a9(t,bn,bk){var bj,bm,T,bl;T=t.length<bk+bn.length?t.length:bk+bn.length;bl=t.length;for(bm=0,bj=bk;bj<T;bj++){bm+=t[bj]+bn[bj-bk];t[bj]=bm&aw;bm=(bm-t[bj])/E}for(bj=T;bm&&bj<bl;bj++){bm+=t[bj];t[bj]=bm&aw;bm=(bm-t[bj])/E}}function az(t,bn,bk){var bj,bm,T,bl;T=t.length<bk+bn.length?t.length:bk+bn.length;bl=t.length;for(bm=0,bj=bk;bj<T;bj++){bm+=t[bj]-bn[bj-bk];t[bj]=bm&aw;bm=(bm-t[bj])/E}for(bj=T;bm&&bj<bl;bj++){bm+=t[bj];t[bj]=bm&aw;bm=(bm-t[bj])/E}}function bc(t,bm){var bj,bl,T,bk;T=t.length<bm.length?t.length:bm.length;for(bl=0,bj=0;bj<T;bj++){bl+=t[bj]-bm[bj];t[bj]=bl&aw;bl=(bl-t[bj])/E}for(bj=T;bl&&bj<t.length;bj++){bl+=t[bj];t[bj]=bl&aw;bl=(bl-t[bj])/E}}function aI(t,bm){var bj,bl,T,bk;T=t.length<bm.length?t.length:bm.length;for(bl=0,bj=0;bj<T;bj++){bl+=t[bj]+bm[bj];t[bj]=bl&aw;bl=(bl-t[bj])/E}for(bj=T;bl&&bj<t.length;bj++){bl+=t[bj];t[bj]=bl&aw;bl=(bl-t[bj])/E}}function aY(t,bj){var T;if(aT.length!=2*t.length){aT=new Array(2*t.length)}y(aT,0);for(T=0;T<bj.length;T++){if(bj[T]){av(aT,t,bj[T],T)}}ax(t,aT)}function aB(t,T){if(h.length!=t.length){h=K(t)}else{ax(h,t)}if(g.length!=t.length){g=K(t)}c(h,T,g,t)}function aU(t,bk,bj){var T;if(n.length!=2*t.length){n=new Array(2*t.length)}y(n,0);for(T=0;T<bk.length;T++){if(bk[T]){av(n,t,bk[T],T)}}aB(n,bj);ax(t,n)}function aJ(bo,t){var bk,bj,bm,bn,bl,bp,T;for(bl=bo.length;bl>0&&!bo[bl-1];bl--){}T=bl>t.length?2*bl:2*t.length;if(n.length!=T){n=new Array(T)}y(n,0);for(bk=0;bk<bl;bk++){bn=n[2*bk]+bo[bk]*bo[bk];n[2*bk]=bn&aw;bn=(bn-n[2*bk])/E;for(bj=bk+1;bj<bl;bj++){bn=n[bk+bj]+2*bo[bk]*bo[bj]+bn;n[bk+bj]=(bn&aw);bn=(bn-n[bk+bj])/E}n[bk+bl]=bn}aB(n,t);ax(bo,n)}function a4(t,T){var bj,bk;for(bj=t.length;bj>0&&!t[bj-1];bj--){}bk=new Array(bj+T);ax(bk,t);return bk}function aS(t,bn,bm){var bl,bk,T,bj;if(e.length!=bm.length){e=K(bm)}if((bm[0]&1)==0){ax(e,t);y(t,1);while(!z(bn,0)){if(bn[0]&1){aU(t,e,bm)}ac(bn,2);aJ(e,bm)}return}y(e,0);for(T=bm.length;T>0&&!bm[T-1];T--){}bj=E-aX(be(bm,E),E);e[T]=1;aU(t,e,bm);if(i.length!=t.length){i=K(t)}else{ax(i,t)}for(bl=bn.length-1;bl>0&!bn[bl];bl--){}if(bn[bl]==0){y(t,1);return}for(bk=1<<(x-1);bk&&!(bn[bl]&bk);bk>>=1){}for(;;){if(!(bk>>=1)){bl--;if(bl<0){au(t,o,bm,bj);return}bk=1<<(x-1)}au(t,t,bm,bj);if(bk&bn[bl]){au(t,i,bm,bj)}}}function au(br,bo,T,bs){var bk,bj,bn,bp,bt,bl,bq;var bu=T.length;var bm=bo.length;if(a2.length!=bu){a2=new Array(bu)}y(a2,0);for(;bu>0&&T[bu-1]==0;bu--){}for(;bm>0&&bo[bm-1]==0;bm--){}bq=a2.length-1;for(bk=0;bk<bu;bk++){bt=a2[0]+br[bk]*bo[0];bp=((bt&aw)*bs)&aw;bn=(bt+bp*T[0]);bn=(bn-(bn&aw))/E;bt=br[bk];bj=1;for(;bj<bm-4;){bn+=a2[bj]+bp*T[bj]+bt*bo[bj];bl=a2[bj-1]=bn&aw;bn=(bn-bl)/E;bj++;bn+=a2[bj]+bp*T[bj]+bt*bo[bj];bl=a2[bj-1]=bn&aw;bn=(bn-bl)/E;bj++;bn+=a2[bj]+bp*T[bj]+bt*bo[bj];bl=a2[bj-1]=bn&aw;bn=(bn-bl)/E;bj++;bn+=a2[bj]+bp*T[bj]+bt*bo[bj];bl=a2[bj-1]=bn&aw;bn=(bn-bl)/E;bj++;bn+=a2[bj]+bp*T[bj]+bt*bo[bj];bl=a2[bj-1]=bn&aw;bn=(bn-bl)/E;bj++}for(;bj<bm;){bn+=a2[bj]+bp*T[bj]+bt*bo[bj];bl=a2[bj-1]=bn&aw;bn=(bn-bl)/E;bj++}for(;bj<bu-4;){bn+=a2[bj]+bp*T[bj];bl=a2[bj-1]=bn&aw;bn=(bn-bl)/E;bj++;bn+=a2[bj]+bp*T[bj];bl=a2[bj-1]=bn&aw;bn=(bn-bl)/E;bj++;bn+=a2[bj]+bp*T[bj];bl=a2[bj-1]=bn&aw;bn=(bn-bl)/E;bj++;bn+=a2[bj]+bp*T[bj];bl=a2[bj-1]=bn&aw;bn=(bn-bl)/E;bj++;bn+=a2[bj]+bp*T[bj];bl=a2[bj-1]=bn&aw;bn=(bn-bl)/E;bj++}for(;bj<bu;){bn+=a2[bj]+bp*T[bj];bl=a2[bj-1]=bn&aw;bn=(bn-bl)/E;bj++}for(;bj<bq;){bn+=a2[bj];bl=a2[bj-1]=bn&aw;bn=(bn-bl)/E;bj++}a2[bj-1]=bn&aw}if(!B(T,a2)){bc(a2,T)}ax(br,a2)}function bb(t,bj,T){return an(t,bg(bj,T),T)}function aj(T,t,bj){T=b(T,bj);t=b(t,bj);if(B(t,T)){T=w(T,bj)}return a5(T,t)}function I(bj){var T=Math.floor(bj/x)+2;var bl=new Array(T);for(var bk=0;bk<T;bk++){bl[bk]=0}bl[T-2]=1<<(bj%x);return bl}var bh=(function(){var t=0,T={};for(;t<256;++t){T[t]=String.fromCharCode(t)}return T}());function ai(t,T){T||(T=0);t=K(t);var bj="";while(!aZ(t)){bj=bh[t[0]&255]+bj;G(t,8)}while(bj.length<T){bj="\x00"+bj}return bj}function aK(T){var t=a6("0",10,T.length);T.forEach(function(bk,bj){if(bj){bi(t,8)}t[0]|=bk});return t}var a3=(function(){if(typeof v!=="undefined"&&typeof v.randomBytes==="function"){return function(bj){try{var t=v.randomBytes(bj)}catch(T){throw T}return Array.prototype.slice.call(t,0)}}else{if(typeof v!=="undefined"&&typeof v.getRandomValues==="function"){return function(T){var t=new Uint8Array(T);v.getRandomValues(t);return Array.prototype.slice.call(t,0)}}else{throw new Error("Keys should not be generated without CSPRNG.")}}}());function aF(){return a3(40)}function am(){return a3(1)[0]}function aV(bj){if(bj>31){throw new Error("Too many bits.")}var bk=0,bl=0;var t=Math.floor(bj/8);var T=(1<<(bj%8))-1;if(T){bl=am()&T}for(;bk<t;bk++){bl=(256*bl)+am()}return bl}return{str2bigInt:a6,bigInt2str:aM,int2bigInt:a0,multMod:an,powMod:aP,inverseMod:bg,randBigInt:aO,randBigInt_:a1,equals:Q,equalsInt:z,sub:a5,mod:b,modInt:be,mult:d,divInt_:ac,rightShift_:G,dup:K,greater:B,add:w,isZero:aZ,bitSize:ay,millerRabin:aN,divide_:c,trim:a4,primes:N,findPrimes:ba,getSeed:aF,divMod:bb,subMod:aj,twoToThe:I,bigInt2bits:ai,ba2bigInt:aK}}));(function(a,b){if(typeof define==="function"&&define.amd){define(b)}else{if(typeof module!=="undefined"&&module.exports){module.exports=b()}else{a.CryptoJS=b()}}}(this,function(){var a=a||(function(f,h){var b={};var c=b.lib={};var k=c.Base=(function(){function o(){}return{extend:function(q){o.prototype=this;var p=new o();if(q){p.mixIn(q)}if(!p.hasOwnProperty("init")){p.init=function(){p.$super.init.apply(this,arguments)}}p.init.prototype=p;p.$super=this;return p},create:function(){var p=this.extend();p.init.apply(p,arguments);return p},init:function(){},mixIn:function(q){for(var p in q){if(q.hasOwnProperty(p)){this[p]=q[p]}}if(q.hasOwnProperty("toString")){this.toString=q.toString}},clone:function(){return this.init.prototype.extend(this)}}}());var m=c.WordArray=k.extend({init:function(p,o){p=this.words=p||[];if(o!=h){this.sigBytes=o}else{this.sigBytes=p.length*4}},toString:function(o){return(o||i).stringify(this)},concat:function(u){var r=this.words;var q=u.words;var o=this.sigBytes;var t=u.sigBytes;this.clamp();if(o%4){for(var s=0;s<t;s++){var p=(q[s>>>2]>>>(24-(s%4)*8))&255;r[(o+s)>>>2]|=p<<(24-((o+s)%4)*8)}}else{if(q.length>65535){for(var s=0;s<t;s+=4){r[(o+s)>>>2]=q[s>>>2]}}else{r.push.apply(r,q)}}this.sigBytes+=t;return this},clamp:function(){var p=this.words;var o=this.sigBytes;p[o>>>2]&=4294967295<<(32-(o%4)*8);p.length=f.ceil(o/4)},clone:function(){var o=k.clone.call(this);o.words=this.words.slice(0);return o},random:function(q){var p=[];for(var o=0;o<q;o+=4){p.push((f.random()*4294967296)|0)}return new m.init(p,q)}});var n=b.enc={};var i=n.Hex={stringify:function(q){var s=q.words;var p=q.sigBytes;var r=[];for(var o=0;o<p;o++){var t=(s[o>>>2]>>>(24-(o%4)*8))&255;r.push((t>>>4).toString(16));r.push((t&15).toString(16))}return r.join("")},parse:function(q){var o=q.length;var r=[];for(var p=0;p<o;p+=2){r[p>>>3]|=parseInt(q.substr(p,2),16)<<(24-(p%8)*4)}return new m.init(r,o/2)}};var e=n.Latin1={stringify:function(r){var s=r.words;var q=r.sigBytes;var o=[];for(var p=0;p<q;p++){var t=(s[p>>>2]>>>(24-(p%4)*8))&255;o.push(String.fromCharCode(t))}return o.join("")},parse:function(q){var o=q.length;var r=[];for(var p=0;p<o;p++){r[p>>>2]|=(q.charCodeAt(p)&255)<<(24-(p%4)*8)}return new m.init(r,o)}};var d=n.Utf8={stringify:function(o){try{return decodeURIComponent(escape(e.stringify(o)))}catch(p){throw new Error("Malformed UTF-8 data")}},parse:function(o){return e.parse(unescape(encodeURIComponent(o)))}};var j=c.BufferedBlockAlgorithm=k.extend({reset:function(){this._data=new m.init();this._nDataBytes=0},_append:function(o){if(typeof o=="string"){o=d.parse(o)}this._data.concat(o);this._nDataBytes+=o.sigBytes},_process:function(x){var r=this._data;var y=r.words;var o=r.sigBytes;var u=this.blockSize;var w=u*4;var v=o/w;if(x){v=f.ceil(v)}else{v=f.max((v|0)-this._minBufferSize,0)}var t=v*u;var s=f.min(t*4,o);if(t){for(var q=0;q<t;q+=u){this._doProcessBlock(y,q)}var p=y.splice(0,t);r.sigBytes-=s}return new m.init(p,s)},clone:function(){var o=k.clone.call(this);o._data=this._data.clone();return o},_minBufferSize:0});var g=c.Hasher=j.extend({cfg:k.extend(),init:function(o){this.cfg=this.cfg.extend(o);this.reset()},reset:function(){j.reset.call(this);this._doReset()},update:function(o){this._append(o);this._process();return this},finalize:function(o){if(o){this._append(o)}var p=this._doFinalize();return p},blockSize:512/32,_createHelper:function(o){return function(q,p){return new o.init(p).finalize(q)}},_createHmacHelper:function(o){return function(q,p){return new l.HMAC.init(o,p).finalize(q)}}});var l=b.algo={};return b}(Math));(function(){var f=a;var b=f.lib;var c=b.WordArray;var e=f.enc;var d=e.Base64={stringify:function(m){var o=m.words;var q=m.sigBytes;var h=this._map;m.clamp();var n=[];for(var l=0;l<q;l+=3){var t=(o[l>>>2]>>>(24-(l%4)*8))&255;var r=(o[(l+1)>>>2]>>>(24-((l+1)%4)*8))&255;var p=(o[(l+2)>>>2]>>>(24-((l+2)%4)*8))&255;var s=(t<<16)|(r<<8)|p;for(var k=0;(k<4)&&(l+k*0.75<q);k++){n.push(h.charAt((s>>>(6*(3-k)))&63))}}var g=h.charAt(64);if(g){while(n.length%4){n.push(g)}}return n.join("")},parse:function(p){var n=p.length;var h=this._map;var g=h.charAt(64);if(g){var q=p.indexOf(g);if(q!=-1){n=q}}var o=[];var m=0;for(var l=0;l<n;l++){if(l%4){var k=h.indexOf(p.charAt(l-1))<<((l%4)*2);var j=h.indexOf(p.charAt(l))>>>(6-(l%4)*2);o[m>>>2]|=(k|j)<<(24-(m%4)*8);m++}}return c.create(o,m)},_map:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="}}());a.lib.Cipher||(function(e){var n=a;var x=n.lib;var j=x.Base;var u=x.WordArray;var w=x.BufferedBlockAlgorithm;var s=n.enc;var g=s.Utf8;var m=s.Base64;var c=n.algo;var i=c.EvpKDF;var k=x.Cipher=w.extend({cfg:j.extend(),createEncryptor:function(C,B){return this.create(this._ENC_XFORM_MODE,C,B)},createDecryptor:function(C,B){return this.create(this._DEC_XFORM_MODE,C,B)},init:function(D,C,B){this.cfg=this.cfg.extend(B);this._xformMode=D;this._key=C;this.reset()},reset:function(){w.reset.call(this);this._doReset()},process:function(B){this._append(B);return this._process()},finalize:function(C){if(C){this._append(C)}var B=this._doFinalize();return B},keySize:128/32,ivSize:128/32,_ENC_XFORM_MODE:1,_DEC_XFORM_MODE:2,_createHelper:(function(){function B(C){if(typeof C=="string"){return h}else{return A}}return function(C){return{encrypt:function(F,E,D){return B(E).encrypt(C,F,E,D)},decrypt:function(F,E,D){return B(E).decrypt(C,F,E,D)}}}}())});var q=x.StreamCipher=k.extend({_doFinalize:function(){var B=this._process(!!"flush");return B},blockSize:1});var t=n.mode={};var z=x.BlockCipherMode=j.extend({createEncryptor:function(B,C){return this.Encryptor.create(B,C)},createDecryptor:function(B,C){return this.Decryptor.create(B,C)},init:function(B,C){this._cipher=B;this._iv=C}});var d=t.CBC=(function(){var B=z.extend();B.Encryptor=B.extend({processBlock:function(G,F){var D=this._cipher;var E=D.blockSize;C.call(this,G,F,E);D.encryptBlock(G,F);this._prevBlock=G.slice(F,F+E)}});B.Decryptor=B.extend({processBlock:function(H,G){var D=this._cipher;var F=D.blockSize;var E=H.slice(G,G+F);D.decryptBlock(H,G);C.call(this,H,G,F);this._prevBlock=E}});function C(I,H,F){var D=this._iv;if(D){var G=D;this._iv=e}else{var G=this._prevBlock}for(var E=0;E<F;E++){I[H+E]^=G[E]}}return B}());var f=n.pad={};var b=f.Pkcs7={pad:function(G,E){var F=E*4;var I=F-G.sigBytes%F;var B=(I<<24)|(I<<16)|(I<<8)|I;var D=[];for(var C=0;C<I;C+=4){D.push(B)}var H=u.create(D,I);G.concat(H)},unpad:function(B){var C=B.words[(B.sigBytes-1)>>>2]&255;B.sigBytes-=C}};var r=x.BlockCipher=k.extend({cfg:k.cfg.extend({mode:d,padding:b}),reset:function(){k.reset.call(this);var B=this.cfg;var C=B.iv;var E=B.mode;if(this._xformMode==this._ENC_XFORM_MODE){var D=E.createEncryptor}else{var D=E.createDecryptor;this._minBufferSize=1}this._mode=D.call(E,this,C&&C.words)},_doProcessBlock:function(C,B){this._mode.processBlock(C,B)},_doFinalize:function(){var C=this.cfg.padding;if(this._xformMode==this._ENC_XFORM_MODE){C.pad(this._data,this.blockSize);var B=this._process(!!"flush")}else{var B=this._process(!!"flush");C.unpad(B)}return B},blockSize:128/32});var p=x.CipherParams=j.extend({init:function(B){this.mixIn(B)},toString:function(B){return(B||this.formatter).stringify(this)}});var o=n.format={};var v=o.OpenSSL={stringify:function(B){var E=B.ciphertext;var C=B.salt;if(C){var D=u.create([1398893684,1701076831]).concat(C).concat(E)}else{var D=E}return D.toString(m)},parse:function(D){var C=m.parse(D);var E=C.words;if(E[0]==1398893684&&E[1]==1701076831){var B=u.create(E.slice(2,4));E.splice(0,4);C.sigBytes-=16}return p.create({ciphertext:C,salt:B})}};var A=x.SerializableCipher=j.extend({cfg:j.extend({format:v}),encrypt:function(B,G,E,C){C=this.cfg.extend(C);var D=B.createEncryptor(E,C);var H=D.finalize(G);var F=D.cfg;return p.create({ciphertext:H,key:E,iv:F.iv,algorithm:B,mode:F.mode,padding:F.padding,blockSize:B.blockSize,formatter:C.format})},decrypt:function(B,F,D,C){C=this.cfg.extend(C);F=this._parse(F,C.format);var E=B.createDecryptor(D,C).finalize(F.ciphertext);return E},_parse:function(B,C){if(typeof B=="string"){return C.parse(B,this)}else{return B}}});var l=n.kdf={};var y=l.OpenSSL={execute:function(D,G,B,F){if(!F){F=u.random(64/8)}var E=i.create({keySize:G+B}).compute(D,F);var C=u.create(E.words.slice(G),B*4);E.sigBytes=G*4;return p.create({key:E,iv:C,salt:F})}};var h=x.PasswordBasedCipher=A.extend({cfg:A.cfg.extend({kdf:y}),encrypt:function(B,E,D,C){C=this.cfg.extend(C);var G=C.kdf.execute(D,B.keySize,B.ivSize);C.iv=G.iv;var F=A.encrypt.call(this,B,E,G.key,C);F.mixIn(G);return F},decrypt:function(B,F,D,C){C=this.cfg.extend(C);F=this._parse(F,C.format);var G=C.kdf.execute(D,B.keySize,B.ivSize,F.salt);C.iv=G.iv;var E=A.decrypt.call(this,B,F,G.key,C);return E}})}());(function(){var b=a;var c=b.lib;var q=c.BlockCipher;var l=b.algo;var e=[];var m=[];var p=[];var o=[];var n=[];var k=[];var j=[];var i=[];var h=[];var g=[];(function(){var u=[];for(var s=0;s<256;s++){if(s<128){u[s]=s<<1}else{u[s]=(s<<1)^283}}var y=0;var v=0;for(var s=0;s<256;s++){var w=v^(v<<1)^(v<<2)^(v<<3)^(v<<4);w=(w>>>8)^(w&255)^99;e[y]=w;m[w]=y;var r=u[y];var B=u[r];var z=u[B];var A=(u[w]*257)^(w*16843008);p[y]=(A<<24)|(A>>>8);o[y]=(A<<16)|(A>>>16);n[y]=(A<<8)|(A>>>24);k[y]=A;var A=(z*16843009)^(B*65537)^(r*257)^(y*16843008);j[w]=(A<<24)|(A>>>8);i[w]=(A<<16)|(A>>>16);h[w]=(A<<8)|(A>>>24);g[w]=A;if(!y){y=v=1}else{y=r^u[u[u[z^r]]];v^=u[u[v]]}}}());var d=[0,1,2,4,8,16,32,64,128,27,54];var f=l.AES=q.extend({_doReset:function(){var A=this._key;var s=A.words;var z=A.sigBytes/4;var y=this._nRounds=z+6;var r=(y+1)*4;var u=this._keySchedule=[];for(var x=0;x<r;x++){if(x<z){u[x]=s[x]}else{var B=u[x-1];if(!(x%z)){B=(B<<8)|(B>>>24);B=(e[B>>>24]<<24)|(e[(B>>>16)&255]<<16)|(e[(B>>>8)&255]<<8)|e[B&255];B^=d[(x/z)|0]<<24}else{if(z>6&&x%z==4){B=(e[B>>>24]<<24)|(e[(B>>>16)&255]<<16)|(e[(B>>>8)&255]<<8)|e[B&255]}}u[x]=u[x-z]^B}}var v=this._invKeySchedule=[];for(var w=0;w<r;w++){var x=r-w;if(w%4){var B=u[x]}else{var B=u[x-4]}if(w<4||x<=4){v[w]=B}else{v[w]=j[e[B>>>24]]^i[e[(B>>>16)&255]]^h[e[(B>>>8)&255]]^g[e[B&255]]}}},encryptBlock:function(s,r){this._doCryptBlock(s,r,this._keySchedule,p,o,n,k,e)},decryptBlock:function(u,s){var r=u[s+1];u[s+1]=u[s+3];u[s+3]=r;this._doCryptBlock(u,s,this._invKeySchedule,j,i,h,g,m);var r=u[s+1];u[s+1]=u[s+3];u[s+3]=r},_doCryptBlock:function(A,z,I,w,u,s,r,H){var F=this._nRounds;var y=A[z]^I[0];var x=A[z+1]^I[1];var v=A[z+2]^I[2];var t=A[z+3]^I[3];var G=4;for(var J=1;J<F;J++){var E=w[y>>>24]^u[(x>>>16)&255]^s[(v>>>8)&255]^r[t&255]^I[G++];var D=w[x>>>24]^u[(v>>>16)&255]^s[(t>>>8)&255]^r[y&255]^I[G++];var C=w[v>>>24]^u[(t>>>16)&255]^s[(y>>>8)&255]^r[x&255]^I[G++];var B=w[t>>>24]^u[(y>>>16)&255]^s[(x>>>8)&255]^r[v&255]^I[G++];y=E;x=D;v=C;t=B}var E=((H[y>>>24]<<24)|(H[(x>>>16)&255]<<16)|(H[(v>>>8)&255]<<8)|H[t&255])^I[G++];var D=((H[x>>>24]<<24)|(H[(v>>>16)&255]<<16)|(H[(t>>>8)&255]<<8)|H[y&255])^I[G++];var C=((H[v>>>24]<<24)|(H[(t>>>16)&255]<<16)|(H[(y>>>8)&255]<<8)|H[x&255])^I[G++];var B=((H[t>>>24]<<24)|(H[(y>>>16)&255]<<16)|(H[(x>>>8)&255]<<8)|H[v&255])^I[G++];A[z]=E;A[z+1]=D;A[z+2]=C;A[z+3]=B},keySize:256/32});b.AES=q._createHelper(f)}());(function(){var h=a;var e=h.lib;var g=e.WordArray;var c=e.Hasher;var f=h.algo;var b=[];var d=f.SHA1=c.extend({_doReset:function(){this._hash=new g.init([1732584193,4023233417,2562383102,271733878,3285377520])},_doProcessBlock:function(o,k){var u=this._hash.words;var s=u[0];var r=u[1];var q=u[2];var p=u[3];var m=u[4];for(var l=0;l<80;l++){if(l<16){b[l]=o[k+l]|0}else{var j=b[l-3]^b[l-8]^b[l-14]^b[l-16];b[l]=(j<<1)|(j>>>31)}var v=((s<<5)|(s>>>27))+m+b[l];if(l<20){v+=((r&q)|(~r&p))+1518500249}else{if(l<40){v+=(r^q^p)+1859775393}else{if(l<60){v+=((r&q)|(r&p)|(q&p))-1894007588}else{v+=(r^q^p)-899497514}}}m=p;p=q;q=(r<<30)|(r>>>2);r=s;s=v}u[0]=(u[0]+s)|0;u[1]=(u[1]+r)|0;u[2]=(u[2]+q)|0;u[3]=(u[3]+p)|0;u[4]=(u[4]+m)|0},_doFinalize:function(){var k=this._data;var l=k.words;var i=this._nDataBytes*8;var j=k.sigBytes*8;l[j>>>5]|=128<<(24-j%32);l[(((j+64)>>>9)<<4)+14]=Math.floor(i/4294967296);l[(((j+64)>>>9)<<4)+15]=i;k.sigBytes=l.length*4;this._process();return this._hash},clone:function(){var i=c.clone.call(this);i._hash=this._hash.clone();return i}});h.SHA1=c._createHelper(d);h.HmacSHA1=c._createHmacHelper(d)}());(function(d){var b=a;var c=b.lib;var h=c.WordArray;var f=c.Hasher;var i=b.algo;var k=[];var j=[];(function(){function o(s){var r=d.sqrt(s);for(var q=2;q<=r;q++){if(!(s%q)){return false}}return true}function m(q){return((q-(q|0))*4294967296)|0}var p=2;var l=0;while(l<64){if(o(p)){if(l<8){k[l]=m(d.pow(p,1/2))}j[l]=m(d.pow(p,1/3));l++}p++}}());var e=[];var g=i.SHA256=f.extend({_doReset:function(){this._hash=new h.init(k.slice(0))},_doProcessBlock:function(o,n){var r=this._hash.words;var E=r[0];var D=r[1];var C=r[2];var B=r[3];var A=r[4];var z=r[5];var y=r[6];var x=r[7];for(var w=0;w<64;w++){if(w<16){e[w]=o[n+w]|0}else{var m=e[w-15];var G=((m<<25)|(m>>>7))^((m<<14)|(m>>>18))^(m>>>3);var s=e[w-2];var F=((s<<15)|(s>>>17))^((s<<13)|(s>>>19))^(s>>>10);e[w]=G+e[w-7]+F+e[w-16]}var t=(A&z)^(~A&y);var l=(E&D)^(E&C)^(D&C);var v=((E<<30)|(E>>>2))^((E<<19)|(E>>>13))^((E<<10)|(E>>>22));var u=((A<<26)|(A>>>6))^((A<<21)|(A>>>11))^((A<<7)|(A>>>25));var q=x+u+t+j[w]+e[w];var p=v+l;x=y;y=z;z=A;A=(B+q)|0;B=C;C=D;D=E;E=(q+p)|0}r[0]=(r[0]+E)|0;r[1]=(r[1]+D)|0;r[2]=(r[2]+C)|0;r[3]=(r[3]+B)|0;r[4]=(r[4]+A)|0;r[5]=(r[5]+z)|0;r[6]=(r[6]+y)|0;r[7]=(r[7]+x)|0},_doFinalize:function(){var n=this._data;var o=n.words;var l=this._nDataBytes*8;var m=n.sigBytes*8;o[m>>>5]|=128<<(24-m%32);o[(((m+64)>>>9)<<4)+14]=d.floor(l/4294967296);o[(((m+64)>>>9)<<4)+15]=l;n.sigBytes=o.length*4;this._process();return this._hash},clone:function(){var l=f.clone.call(this);l._hash=this._hash.clone();return l}});b.SHA256=f._createHelper(g);b.HmacSHA256=f._createHmacHelper(g)}(Math));(function(){var h=a;var e=h.lib;var d=e.Base;var g=h.enc;var c=g.Utf8;var f=h.algo;var b=f.HMAC=d.extend({init:function(r,o){r=this._hasher=new r.init();if(typeof o=="string"){o=c.parse(o)}var l=r.blockSize;var j=l*4;if(o.sigBytes>j){o=r.finalize(o)}o.clamp();var q=this._oKey=o.clone();var n=this._iKey=o.clone();var p=q.words;var k=n.words;for(var m=0;m<l;m++){p[m]^=1549556828;k[m]^=909522486}q.sigBytes=n.sigBytes=j;this.reset()},reset:function(){var i=this._hasher;i.reset();i.update(this._iKey)},update:function(i){this._hasher.update(i);return this},finalize:function(i){var j=this._hasher;var l=j.finalize(i);j.reset();var k=j.finalize(this._oKey.clone().concat(l));return k}})}());a.pad.NoPadding={pad:function(){},unpad:function(){}};a.mode.CTR=(function(){var c=a.lib.BlockCipherMode.extend();var b=c.Encryptor=c.extend({processBlock:function(l,k){var d=this._cipher;var h=d.blockSize;var f=this._iv;var e=this._counter;if(f){e=this._counter=f.slice(0);this._iv=undefined}var j=e.slice(0);d.encryptBlock(j,0);e[h-1]=(e[h-1]+1)|0;for(var g=0;g<h;g++){l[k+g]^=j[g]}}});c.Decryptor=b;return c}());return a})); +(function(){function c(){}var l=c.prototype;function h(w,x){var v=w.length;while(v--){if(w[v].listener===x){return v}}return -1}function j(v){return function w(){return this[v].apply(this,arguments)}}l.getListeners=function t(v){var y=this._getEvents();var w;var x;if(typeof v==="object"){w={};for(x in y){if(y.hasOwnProperty(x)&&v.test(x)){w[x]=y[x]}}}else{w=y[v]||(y[v]=[])}return w};l.flattenListeners=function r(x){var v=[];var w;for(w=0;w<x.length;w+=1){v.push(x[w].listener)}return v};l.getListenersAsObject=function e(v){var x=this.getListeners(v);var w;if(x instanceof Array){w={};w[v]=x}return w||x};l.addListener=function f(v,y){var x=this.getListenersAsObject(v);var z=typeof y==="object";var w;for(w in x){if(x.hasOwnProperty(w)&&h(x[w],y)===-1){x[w].push(z?y:{listener:y,once:false})}}return this};l.on=j("addListener");l.addOnceListener=function a(v,w){return this.addListener(v,{listener:w,once:true})};l.once=j("addOnceListener");l.defineEvent=function p(v){this.getListeners(v);return this};l.defineEvents=function q(v){for(var w=0;w<v.length;w+=1){this.defineEvent(v[w])}return this};l.removeListener=function b(v,z){var y=this.getListenersAsObject(v);var w;var x;for(x in y){if(y.hasOwnProperty(x)){w=h(y[x],z);if(w!==-1){y[x].splice(w,1)}}}return this};l.off=j("removeListener");l.addListeners=function m(v,w){return this.manipulateListeners(false,v,w)};l.removeListeners=function s(v,w){return this.manipulateListeners(true,v,w)};l.manipulateListeners=function g(w,x,z){var y;var A;var B=w?this.removeListener:this.addListener;var v=w?this.removeListeners:this.addListeners;if(typeof x==="object"&&!(x instanceof RegExp)){for(y in x){if(x.hasOwnProperty(y)&&(A=x[y])){if(typeof A==="function"){B.call(this,y,A)}else{v.call(this,y,A)}}}}else{y=z.length;while(y--){B.call(this,x,z[y])}}return this};l.removeEvent=function o(v){var y=typeof v;var x=this._getEvents();var w;if(y==="string"){delete x[v]}else{if(y==="object"){for(w in x){if(x.hasOwnProperty(w)&&v.test(w)){delete x[w]}}}else{delete this._events}}return this};l.emitEvent=function u(v,x){var A=this.getListenersAsObject(v);var B;var z;var y;var w;for(y in A){if(A.hasOwnProperty(y)){z=A[y].length;while(z--){B=A[y][z];if(B.once===true){this.removeListener(v,B.listener)}w=B.listener.apply(this,x||[]);if(w===this._getOnceReturnValue()){this.removeListener(v,B.listener)}}}}return this};l.trigger=j("emitEvent");l.emit=function k(v){var w=Array.prototype.slice.call(arguments,1);return this.emitEvent(v,w)};l.setOnceReturnValue=function i(v){this._onceReturnValue=v;return this};l._getOnceReturnValue=function n(){if(this.hasOwnProperty("_onceReturnValue")){return this._onceReturnValue}else{return true}};l._getEvents=function d(){return this._events||(this._events={})};if(typeof define==="function"&&define.amd){define(function(){return c})}else{if(typeof module==="object"&&module.exports){module.exports=c}else{this.EventEmitter=c}}}.call(this)); +!function(root,factory){"function"==typeof define&&define.amd?define(["bigint","crypto","eventemitter"],function(BigInt,CryptoJS,EventEmitter){var root={BigInt:BigInt,CryptoJS:CryptoJS,EventEmitter:EventEmitter,OTR:{},DSA:{}};return factory.call(root)}):(root.OTR={},root.DSA={},factory.call(root))}(this,function(){return function(){"use strict";var root=this,CONST={N:"FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA237327FFFFFFFFFFFFFFFF",G:"2",MSGSTATE_PLAINTEXT:0,MSGSTATE_ENCRYPTED:1,MSGSTATE_FINISHED:2,AUTHSTATE_NONE:0,AUTHSTATE_AWAITING_DHKEY:1,AUTHSTATE_AWAITING_REVEALSIG:2,AUTHSTATE_AWAITING_SIG:3,WHITESPACE_TAG:" ",WHITESPACE_TAG_V2:" ",WHITESPACE_TAG_V3:" ",OTR_TAG:"?OTR",OTR_VERSION_1:"\x00",OTR_VERSION_2:"\x00",OTR_VERSION_3:"\x00",SMPSTATE_EXPECT0:0,SMPSTATE_EXPECT1:1,SMPSTATE_EXPECT2:2,SMPSTATE_EXPECT3:3,SMPSTATE_EXPECT4:4,STATUS_SEND_QUERY:0,STATUS_AKE_INIT:1,STATUS_AKE_SUCCESS:2,STATUS_END_OTR:3};"undefined"!=typeof module&&module.exports?module.exports=CONST:root.OTR.CONST=CONST}.call(this),function(){"use strict";var CryptoJS,BigInt,root=this,HLP={};"undefined"!=typeof module&&module.exports?(module.exports=HLP={},CryptoJS=require("../vendor/crypto.js"),BigInt=require("../vendor/bigint.js")):(root.OTR&&(root.OTR.HLP=HLP),root.DSA&&(root.DSA.HLP=HLP),CryptoJS=root.CryptoJS,BigInt=root.BigInt);var DTS={BYTE:1,SHORT:2,INT:4,CTR:8,MAC:20,SIG:40},WRAPPER_BEGIN="?OTR",WRAPPER_END=".",TWO=BigInt.str2bigInt("2",10);HLP.debug=function(msg){this.debug&&"function"!=typeof this.debug&&"undefined"!=typeof console&&console.log(msg)},HLP.extend=function(child,parent){function Ctor(){this.constructor=child}for(var key in parent)Object.hasOwnProperty.call(parent,key)&&(child[key]=parent[key]);Ctor.prototype=parent.prototype,child.prototype=new Ctor,child.__super__=parent.prototype},HLP.compare=function(str1,str2){if(str1.length!==str2.length)return!1;for(var i=0,result=0;i<str1.length;i++)result|=str1[i].charCodeAt(0)^str2[i].charCodeAt(0);return 0===result},HLP.randomExponent=function(){return BigInt.randBigInt(1536)},HLP.smpHash=function(version,fmpi,smpi){var sha256=CryptoJS.algo.SHA256.create();sha256.update(CryptoJS.enc.Latin1.parse(HLP.packBytes(version,DTS.BYTE))),sha256.update(CryptoJS.enc.Latin1.parse(HLP.packMPI(fmpi))),smpi&&sha256.update(CryptoJS.enc.Latin1.parse(HLP.packMPI(smpi)));var hash=sha256.finalize();return HLP.bits2bigInt(hash.toString(CryptoJS.enc.Latin1))},HLP.makeMac=function(aesctr,m){var pass=CryptoJS.enc.Latin1.parse(m),mac=CryptoJS.HmacSHA256(CryptoJS.enc.Latin1.parse(aesctr),pass);return HLP.mask(mac.toString(CryptoJS.enc.Latin1),0,160)},HLP.make1Mac=function(aesctr,m){var pass=CryptoJS.enc.Latin1.parse(m),mac=CryptoJS.HmacSHA1(CryptoJS.enc.Latin1.parse(aesctr),pass);return mac.toString(CryptoJS.enc.Latin1)},HLP.encryptAes=function(msg,c,iv){var opts={mode:CryptoJS.mode.CTR,iv:CryptoJS.enc.Latin1.parse(iv),padding:CryptoJS.pad.NoPadding},aesctr=CryptoJS.AES.encrypt(msg,CryptoJS.enc.Latin1.parse(c),opts),aesctr_decoded=CryptoJS.enc.Base64.parse(aesctr.toString());return CryptoJS.enc.Latin1.stringify(aesctr_decoded)},HLP.decryptAes=function(msg,c,iv){msg=CryptoJS.enc.Latin1.parse(msg);var opts={mode:CryptoJS.mode.CTR,iv:CryptoJS.enc.Latin1.parse(iv),padding:CryptoJS.pad.NoPadding};return CryptoJS.AES.decrypt(CryptoJS.enc.Base64.stringify(msg),CryptoJS.enc.Latin1.parse(c),opts)},HLP.multPowMod=function(a,b,c,d,e){return BigInt.multMod(BigInt.powMod(a,b,e),BigInt.powMod(c,d,e),e)},HLP.ZKP=function(v,c,d,e){return BigInt.equals(c,HLP.smpHash(v,d,e))},HLP.GTOE=function(a,b){return BigInt.equals(a,b)||BigInt.greater(a,b)},HLP.between=function(x,a,b){return BigInt.greater(x,a)&&BigInt.greater(b,x)},HLP.checkGroup=function(g,N_MINUS_2){return HLP.GTOE(g,TWO)&&HLP.GTOE(N_MINUS_2,g)},HLP.h1=function(b,secbytes){var sha1=CryptoJS.algo.SHA1.create();return sha1.update(CryptoJS.enc.Latin1.parse(b)),sha1.update(CryptoJS.enc.Latin1.parse(secbytes)),sha1.finalize().toString(CryptoJS.enc.Latin1)},HLP.h2=function(b,secbytes){var sha256=CryptoJS.algo.SHA256.create();return sha256.update(CryptoJS.enc.Latin1.parse(b)),sha256.update(CryptoJS.enc.Latin1.parse(secbytes)),sha256.finalize().toString(CryptoJS.enc.Latin1)},HLP.mask=function(bytes,start,n){return bytes.substr(start/8,n/8)};var _toString=String.fromCharCode;HLP.packBytes=function(val,bytes){val=val.toString(16);for(var nex,res="";bytes>0;bytes--)nex=val.length?val.substr(-2,2):"0",val=val.substr(0,val.length-2),res=_toString(parseInt(nex,16))+res;return res},HLP.packINT=function(d){return HLP.packBytes(d,DTS.INT)},HLP.packCtr=function(d){return HLP.padCtr(HLP.packBytes(d,DTS.CTR))},HLP.padCtr=function(ctr){return ctr+"\x00\x00\x00\x00\x00\x00\x00\x00"},HLP.unpackCtr=function(d){return d=HLP.toByteArray(d.substring(0,8)),HLP.unpack(d)},HLP.unpack=function(arr){for(var val=0,i=0,len=arr.length;len>i;i++)val=256*val+arr[i];return val},HLP.packData=function(d){return HLP.packINT(d.length)+d},HLP.bits2bigInt=function(bits){return bits=HLP.toByteArray(bits),BigInt.ba2bigInt(bits)},HLP.packMPI=function(mpi){return HLP.packData(BigInt.bigInt2bits(BigInt.trim(mpi,0)))},HLP.packSHORT=function(short){return HLP.packBytes(short,DTS.SHORT)},HLP.unpackSHORT=function(short){return short=HLP.toByteArray(short),HLP.unpack(short)},HLP.packTLV=function(type,value){return HLP.packSHORT(type)+HLP.packSHORT(value.length)+value},HLP.readLen=function(msg){return msg=HLP.toByteArray(msg.substring(0,4)),HLP.unpack(msg)},HLP.readData=function(data){var n=HLP.unpack(data.splice(0,4));return[n,data]},HLP.readMPI=function(data){return data=HLP.toByteArray(data),data=HLP.readData(data),BigInt.ba2bigInt(data[1])},HLP.packMPIs=function(arr){return arr.reduce(function(prv,cur){return prv+HLP.packMPI(cur)},"")},HLP.unpackMPIs=function(num,mpis){for(var i=0,arr=[];num>i;i++)arr.push("MPI");return HLP.splitype(arr,mpis).map(function(m){return HLP.readMPI(m)})},HLP.wrapMsg=function(msg,fs,v3,our_it,their_it){msg=CryptoJS.enc.Base64.stringify(CryptoJS.enc.Latin1.parse(msg)),msg=WRAPPER_BEGIN+":"+msg+WRAPPER_END;var its;if(v3&&(its="|",its+=HLP.readLen(our_it).toString(16),its+="|",its+=HLP.readLen(their_it).toString(16)),!fs)return[null,msg];var n=Math.ceil(msg.length/fs);if(n>65535)return["Too many fragments"];if(1==n)return[null,msg];var k,bi,ei,frag,mf,mfs=[];for(k=1;n>=k;k++)bi=(k-1)*fs,ei=k*fs,frag=msg.slice(bi,ei),mf=WRAPPER_BEGIN,v3&&(mf+=its),mf+=","+k+",",mf+=n+",",mf+=frag+",",mfs.push(mf);return[null,mfs]},HLP.splitype=function splitype(arr,msg){var data=[];return arr.forEach(function(a){var str;switch(a){case"PUBKEY":str=splitype(["SHORT","MPI","MPI","MPI","MPI"],msg).join("");break;case"DATA":case"MPI":str=msg.substring(0,HLP.readLen(msg)+4);break;default:str=msg.substring(0,DTS[a])}data.push(str),msg=msg.substring(str.length)}),data};var _bin2num=function(){for(var i=0,_bin2num={};256>i;++i)_bin2num[String.fromCharCode(i)]=i;for(i=128;256>i;++i)_bin2num[String.fromCharCode(63232+i)]=i;return _bin2num}();HLP.toByteArray=function(data){for(var rv=[],ary=data.split(""),i=-1,iz=ary.length,remain=iz%8;remain--;)++i,rv[i]=_bin2num[ary[i]];for(remain=iz>>3;remain--;)rv.push(_bin2num[ary[++i]],_bin2num[ary[++i]],_bin2num[ary[++i]],_bin2num[ary[++i]],_bin2num[ary[++i]],_bin2num[ary[++i]],_bin2num[ary[++i]],_bin2num[ary[++i]]);return rv}}.call(this),function(){"use strict";function timer(){var start=(new Date).getTime();return function(s){if(DEBUG&&"undefined"!=typeof console){var t=(new Date).getTime();console.log(s+": "+(t-start)),start=t}}}function makeRandom(min,max){var c=BigInt.randBigInt(BigInt.bitSize(max));return HLP.between(c,min,max)?c:makeRandom(min,max)}function isProbPrime(k,n){var i,B=3e4,l=BigInt.bitSize(k),primes=BigInt.primes;for(0===primes.length&&(primes=BigInt.findPrimes(B)),rpprb.length!=k.length&&(rpprb=BigInt.dup(k)),i=0;i<primes.length&&primes[i]<=B;i++)if(0===BigInt.modInt(k,primes[i])&&!BigInt.equalsInt(k,primes[i]))return 0;for(i=0;n>i;i++){for(BigInt.randBigInt_(rpprb,l,0);!BigInt.greater(k,rpprb);)BigInt.randBigInt_(rpprb,l,0);if(!BigInt.millerRabin(k,rpprb))return 0}return 1}function generatePrimes(bit_length){for(var q,p,rem,counter,t=timer(),repeat=bit_lengths[bit_length].repeat,N=bit_lengths[bit_length].N,LM1=BigInt.twoToThe(bit_length-1),bl4=4*bit_length,brk=!1;;)if(q=BigInt.randBigInt(N,1),q[0]|=1,isProbPrime(q,repeat)){for(t("q"),counter=0;bl4>counter;counter++)if(p=BigInt.randBigInt(bit_length,1),p[0]|=1,rem=BigInt.mod(p,q),rem=BigInt.sub(rem,ONE),p=BigInt.sub(p,rem),!BigInt.greater(LM1,p)&&isProbPrime(p,repeat)){t("p"),primes[bit_length]={p:p,q:q},brk=!0;break}if(brk)break}for(var g,h=BigInt.dup(TWO),pm1=BigInt.sub(p,ONE),e=BigInt.multMod(pm1,BigInt.inverseMod(q,p),p);;){g=BigInt.powMod(h,e,p);{if(!BigInt.equals(g,ONE))return primes[bit_length].g=g,void t("g");h=BigInt.add(h,ONE)}}throw new Error("Unreachable!")}function DSA(obj,opts){if(!(this instanceof DSA))return new DSA(obj,opts);if(opts=opts||{},obj){var self=this;return["p","q","g","y","x"].forEach(function(prop){self[prop]=obj[prop]}),void(this.type=obj.type||KEY_TYPE)}var bit_length=parseInt(opts.bit_length?opts.bit_length:1024,10);if(!bit_lengths[bit_length])throw new Error("Unsupported bit length.");primes[bit_length]||generatePrimes(bit_length),this.p=primes[bit_length].p,this.q=primes[bit_length].q,this.g=primes[bit_length].g,this.type=KEY_TYPE,this.x=makeRandom(ZERO,this.q),this.y=BigInt.powMod(this.g,this.x,this.p),opts.nocache&&(primes[bit_length]=null)}function tokenizeStr(str){var start,end;if(start=str.indexOf("("),end=str.lastIndexOf(")"),0>start||0>end)throw new Error("Malformed S-Expression");str=str.substring(start+1,end);var splt=str.search(/\s/),obj={type:str.substring(0,splt),val:[]};if(str=str.substring(splt+1,end),start=str.indexOf("("),0>start)obj.val.push(str);else for(var i,len,ss,es;start>-1;){for(i=start+1,len=str.length,ss=1,es=0;len>i&&ss>es;i++)"("===str[i]&&ss++,")"===str[i]&&es++;obj.val.push(tokenizeStr(str.substring(start,++i))),str=str.substring(++i),start=str.indexOf("(")}return obj}function parseLibotr(obj){if(!obj.type)throw new Error("Parse error.");var o,val;return"privkeys"===obj.type?(o=[],obj.val.forEach(function(i){o.push(parseLibotr(i))}),o):(o={},obj.val.forEach(function(i){val=i.val[0],"string"==typeof val?0===val.indexOf("#")&&(val=val.substring(1,val.lastIndexOf("#")),val=BigInt.str2bigInt(val,16)):val=parseLibotr(i),o[i.type]=val}),o)}var CryptoJS,BigInt,Worker,WWPath,HLP,root=this;"undefined"!=typeof module&&module.exports?(module.exports=DSA,CryptoJS=require("../vendor/crypto.js"),BigInt=require("../vendor/bigint.js"),WWPath=require("path").join(__dirname,"/dsa-webworker.js"),HLP=require("./helpers.js")):(Object.keys(root.DSA).forEach(function(k){DSA[k]=root.DSA[k]}),root.DSA=DSA,CryptoJS=root.CryptoJS,BigInt=root.BigInt,Worker=root.Worker,WWPath="dsa-webworker.js",HLP=DSA.HLP);var ZERO=BigInt.str2bigInt("0",10),ONE=BigInt.str2bigInt("1",10),TWO=BigInt.str2bigInt("2",10),KEY_TYPE="\x00\x00",DEBUG=!1,rpprb=[],bit_lengths={1024:{N:160,repeat:40},2048:{N:224,repeat:56}},primes={};DSA.prototype={constructor:DSA,packPublic:function(){var str=this.type;return str+=HLP.packMPI(this.p),str+=HLP.packMPI(this.q),str+=HLP.packMPI(this.g),str+=HLP.packMPI(this.y)},packPrivate:function(){var str=this.packPublic()+HLP.packMPI(this.x);return str=CryptoJS.enc.Latin1.parse(str),str.toString(CryptoJS.enc.Base64)},generateNonce:function(m){var priv=BigInt.bigInt2bits(BigInt.trim(this.x,0)),rand=BigInt.bigInt2bits(BigInt.randBigInt(256)),sha256=CryptoJS.algo.SHA256.create();sha256.update(CryptoJS.enc.Latin1.parse(priv)),sha256.update(m),sha256.update(CryptoJS.enc.Latin1.parse(rand));var hash=sha256.finalize();return hash=HLP.bits2bigInt(hash.toString(CryptoJS.enc.Latin1)),BigInt.rightShift_(hash,256-BigInt.bitSize(this.q)),HLP.between(hash,ZERO,this.q)?hash:this.generateNonce(m)},sign:function(m){m=CryptoJS.enc.Latin1.parse(m);for(var k,b=BigInt.str2bigInt(m.toString(CryptoJS.enc.Hex),16),r=ZERO,s=ZERO;BigInt.isZero(s)||BigInt.isZero(r);)k=this.generateNonce(m),r=BigInt.mod(BigInt.powMod(this.g,k,this.p),this.q),BigInt.isZero(r)||(s=BigInt.inverseMod(k,this.q),s=BigInt.mult(s,BigInt.add(b,BigInt.mult(this.x,r))),s=BigInt.mod(s,this.q));return[r,s]},fingerprint:function(){var pk=this.packPublic();return this.type===KEY_TYPE&&(pk=pk.substring(2)),pk=CryptoJS.enc.Latin1.parse(pk),CryptoJS.SHA1(pk).toString(CryptoJS.enc.Hex)}},DSA.parsePublic=function(str,priv){var fields=["SHORT","MPI","MPI","MPI","MPI"];priv&&fields.push("MPI"),str=HLP.splitype(fields,str);var obj={type:str[0],p:HLP.readMPI(str[1]),q:HLP.readMPI(str[2]),g:HLP.readMPI(str[3]),y:HLP.readMPI(str[4])};return priv&&(obj.x=HLP.readMPI(str[5])),new DSA(obj)},DSA.parsePrivate=function(str,libotr){return libotr?parseLibotr(tokenizeStr(str))[0]["private-key"].dsa:(str=CryptoJS.enc.Base64.parse(str),str=str.toString(CryptoJS.enc.Latin1),DSA.parsePublic(str,!0))},DSA.verify=function(key,m,r,s){if(!HLP.between(r,ZERO,key.q)||!HLP.between(s,ZERO,key.q))return!1;var hm=CryptoJS.enc.Latin1.parse(m);hm=BigInt.str2bigInt(hm.toString(CryptoJS.enc.Hex),16);var w=BigInt.inverseMod(s,key.q),u1=BigInt.multMod(hm,w,key.q),u2=BigInt.multMod(r,w,key.q);u1=BigInt.powMod(key.g,u1,key.p),u2=BigInt.powMod(key.y,u2,key.p);var v=BigInt.mod(BigInt.multMod(u1,u2,key.p),key.q);return BigInt.equals(v,r)},DSA.createInWebWorker=function(options,cb){var opts={path:WWPath,seed:BigInt.getSeed};options&&"object"==typeof options&&Object.keys(options).forEach(function(k){opts[k]=options[k]}),"undefined"!=typeof module&&module.exports&&(Worker=require("webworker-threads").Worker);var worker=new Worker(opts.path);worker.onmessage=function(e){var data=e.data;switch(data.type){case"debug":if(!DEBUG||"undefined"==typeof console)return;console.log(data.val);break;case"data":worker.terminate(),cb(DSA.parsePrivate(data.val));break;default:throw new Error("Unrecognized type.")}},worker.postMessage({seed:opts.seed(),imports:opts.imports,debug:DEBUG})}}.call(this),function(){"use strict";var CryptoJS,CONST,HLP,root=this,Parse={};"undefined"!=typeof module&&module.exports?(module.exports=Parse,CryptoJS=require("../vendor/crypto.js"),CONST=require("./const.js"),HLP=require("./helpers.js")):(root.OTR.Parse=Parse,CryptoJS=root.CryptoJS,CONST=root.OTR.CONST,HLP=root.OTR.HLP);var tags={};tags[CONST.WHITESPACE_TAG_V2]=CONST.OTR_VERSION_2,tags[CONST.WHITESPACE_TAG_V3]=CONST.OTR_VERSION_3,Parse.parseMsg=function(otr,msg){var ver=[],start=msg.indexOf(CONST.OTR_TAG);if(!~start){if(this.initFragment(otr),ind=msg.indexOf(CONST.WHITESPACE_TAG),~ind){msg=msg.split(""),msg.splice(ind,16);for(var tag,len=msg.length;len>ind;)tag=msg.slice(ind,ind+8).join(""),Object.hasOwnProperty.call(tags,tag)?(msg.splice(ind,8),ver.push(tags[tag])):ind+=8;msg=msg.join("")}return{msg:msg,ver:ver}}var ind=start+CONST.OTR_TAG.length,com=msg[ind];if(","===com||"|"===com)return this.msgFragment(otr,msg.substring(ind+1),"|"===com);if(this.initFragment(otr),~["?","v"].indexOf(com)){"?"===msg[ind]&&(ver.push(CONST.OTR_VERSION_1),ind+=1);var vers={2:CONST.OTR_VERSION_2,3:CONST.OTR_VERSION_3},qs=msg.substring(ind+1),qi=qs.indexOf("?");return qi>=1&&(qs=qs.substring(0,qi).split(""),"v"===msg[ind]&&qs.forEach(function(q){Object.hasOwnProperty.call(vers,q)&&ver.push(vers[q])})),{cls:"query",ver:ver}}if(":"===com){ind+=1;var info=msg.substring(ind,ind+4);if(info.length<4)return{msg:msg};info=CryptoJS.enc.Base64.parse(info).toString(CryptoJS.enc.Latin1);var version=info.substring(0,2),type=info.substring(2);if(!otr["ALLOW_V"+HLP.unpackSHORT(version)])return{msg:msg};ind+=4;var end=msg.substring(ind).indexOf(".");if(!~end)return{msg:msg};msg=CryptoJS.enc.Base64.parse(msg.substring(ind,ind+end)),msg=CryptoJS.enc.Latin1.stringify(msg);var instance_tags;version===CONST.OTR_VERSION_3&&(instance_tags=msg.substring(0,8),msg=msg.substring(8));var cls;return~["","\n","",""].indexOf(type)?cls="ake":""===type&&(cls="data"),{version:version,type:type,msg:msg,cls:cls,instance_tags:instance_tags}}return" Error:"===msg.substring(ind,ind+7)?(otr.ERROR_START_AKE&&otr.sendQueryMsg(),{msg:msg.substring(ind+7),cls:"error"}):{msg:msg}},Parse.initFragment=function(otr){otr.fragment={s:"",j:0,k:0}},Parse.msgFragment=function(otr,msg,v3){if(msg=msg.split(","),v3){var its=msg.shift().split("|"),their_it=HLP.packINT(parseInt(its[0],16)),our_it=HLP.packINT(parseInt(its[1],16));if(otr.checkInstanceTags(their_it+our_it))return}if(!(msg.length<4||isNaN(parseInt(msg[0],10))||isNaN(parseInt(msg[1],10)))){var k=parseInt(msg[0],10),n=parseInt(msg[1],10);return msg=msg[2],k>n||0===n||0===k?void this.initFragment(otr):(1===k?(this.initFragment(otr),otr.fragment={k:1,n:n,s:msg}):n===otr.fragment.n&&k===otr.fragment.k+1?(otr.fragment.s+=msg,otr.fragment.k+=1):this.initFragment(otr),n===k?(msg=otr.fragment.s,this.initFragment(otr),this.parseMsg(otr,msg)):void 0)}}}.call(this),function(){"use strict";function hMac(gx,gy,pk,kid,m){var pass=CryptoJS.enc.Latin1.parse(m),hmac=CryptoJS.algo.HMAC.create(CryptoJS.algo.SHA256,pass);return hmac.update(CryptoJS.enc.Latin1.parse(HLP.packMPI(gx))),hmac.update(CryptoJS.enc.Latin1.parse(HLP.packMPI(gy))),hmac.update(CryptoJS.enc.Latin1.parse(pk)),hmac.update(CryptoJS.enc.Latin1.parse(kid)),hmac.finalize().toString(CryptoJS.enc.Latin1)}function AKE(otr){if(!(this instanceof AKE))return new AKE(otr);this.otr=otr,this.our_dh=otr.our_old_dh,this.our_keyid=otr.our_keyid-1,this.their_y=null,this.their_keyid=null,this.their_priv_pk=null,this.ssid=null,this.transmittedRS=!1,this.r=null;var self=this;["sendMsg"].forEach(function(meth){self[meth]=self[meth].bind(self)})}var CryptoJS,BigInt,CONST,HLP,DSA,root=this;"undefined"!=typeof module&&module.exports?(module.exports=AKE,CryptoJS=require("../vendor/crypto.js"),BigInt=require("../vendor/bigint.js"),CONST=require("./const.js"),HLP=require("./helpers.js"),DSA=require("./dsa.js")):(root.OTR.AKE=AKE,CryptoJS=root.CryptoJS,BigInt=root.BigInt,CONST=root.OTR.CONST,HLP=root.OTR.HLP,DSA=root.DSA);var N=BigInt.str2bigInt(CONST.N,16),N_MINUS_2=BigInt.sub(N,BigInt.str2bigInt("2",10));AKE.prototype={constructor:AKE,createKeys:function(g){var s=BigInt.powMod(g,this.our_dh.privateKey,N),secbytes=HLP.packMPI(s);this.ssid=HLP.mask(HLP.h2("\x00",secbytes),0,64);var tmp=HLP.h2("",secbytes);this.c=HLP.mask(tmp,0,128),this.c_prime=HLP.mask(tmp,128,128),this.m1=HLP.h2("",secbytes),this.m2=HLP.h2("",secbytes),this.m1_prime=HLP.h2("",secbytes),this.m2_prime=HLP.h2("",secbytes)},verifySignMac:function(mac,aesctr,m2,c,their_y,our_dh_pk,m1,ctr){var vmac=HLP.makeMac(aesctr,m2);if(!HLP.compare(mac,vmac))return["MACs do not match."];var x=HLP.decryptAes(aesctr.substring(4),c,ctr);x=HLP.splitype(["PUBKEY","INT","SIG"],x.toString(CryptoJS.enc.Latin1));var m=hMac(their_y,our_dh_pk,x[0],x[1],m1),pub=DSA.parsePublic(x[0]),r=HLP.bits2bigInt(x[2].substring(0,20)),s=HLP.bits2bigInt(x[2].substring(20));return DSA.verify(pub,m,r,s)?[null,HLP.readLen(x[1]),pub]:["Cannot verify signature of m."]},makeM:function(their_y,m1,c,m2){var pk=this.otr.priv.packPublic(),kid=HLP.packINT(this.our_keyid),m=hMac(this.our_dh.publicKey,their_y,pk,kid,m1);m=this.otr.priv.sign(m);var msg=pk+kid;msg+=BigInt.bigInt2bits(m[0],20),msg+=BigInt.bigInt2bits(m[1],20),msg=CryptoJS.enc.Latin1.parse(msg);var aesctr=HLP.packData(HLP.encryptAes(msg,c,HLP.packCtr(0))),mac=HLP.makeMac(aesctr,m2);return aesctr+mac},akeSuccess:function(version){return HLP.debug.call(this.otr,"success"),BigInt.equals(this.their_y,this.our_dh.publicKey)?this.otr.error("equal keys - we have a problem.",!0):(this.otr.our_old_dh=this.our_dh,this.otr.their_priv_pk=this.their_priv_pk,this.their_keyid===this.otr.their_keyid&&BigInt.equals(this.their_y,this.otr.their_y)||this.their_keyid===this.otr.their_keyid-1&&BigInt.equals(this.their_y,this.otr.their_old_y)||(this.otr.their_y=this.their_y,this.otr.their_old_y=null,this.otr.their_keyid=this.their_keyid,this.otr.sessKeys[0]=[new this.otr.DHSession(this.otr.our_dh,this.otr.their_y),null],this.otr.sessKeys[1]=[new this.otr.DHSession(this.otr.our_old_dh,this.otr.their_y),null]),this.otr.ssid=this.ssid,this.otr.transmittedRS=this.transmittedRS,this.otr_version=version,this.otr.authstate=CONST.AUTHSTATE_NONE,this.otr.msgstate=CONST.MSGSTATE_ENCRYPTED,this.r=null,this.myhashed=null,this.dhcommit=null,this.encrypted=null,this.hashed=null,this.otr.trigger("status",[CONST.STATUS_AKE_SUCCESS]),void this.otr.sendStored())},handleAKE:function(msg){var send,vsm,type,version=msg.version;switch(msg.type){case"":if(HLP.debug.call(this.otr,"d-h key message"),msg=HLP.splitype(["DATA","DATA"],msg.msg),this.otr.authstate===CONST.AUTHSTATE_AWAITING_DHKEY){var ourHash=HLP.readMPI(this.myhashed),theirHash=HLP.readMPI(msg[1]);if(BigInt.greater(ourHash,theirHash)){type="",send=this.dhcommit;break}this.our_dh=this.otr.dh(),this.otr.authstate=CONST.AUTHSTATE_NONE,this.r=null,this.myhashed=null}else this.otr.authstate===CONST.AUTHSTATE_AWAITING_SIG&&(this.our_dh=this.otr.dh());this.otr.authstate=CONST.AUTHSTATE_AWAITING_REVEALSIG,this.encrypted=msg[0].substring(4),this.hashed=msg[1].substring(4),type="\n",send=HLP.packMPI(this.our_dh.publicKey);break;case"\n":if(HLP.debug.call(this.otr,"reveal signature message"),msg=HLP.splitype(["MPI"],msg.msg),this.otr.authstate!==CONST.AUTHSTATE_AWAITING_DHKEY){if(this.otr.authstate!==CONST.AUTHSTATE_AWAITING_SIG)return;if(!BigInt.equals(this.their_y,HLP.readMPI(msg[0])))return}if(this.otr.authstate=CONST.AUTHSTATE_AWAITING_SIG,this.their_y=HLP.readMPI(msg[0]),!HLP.checkGroup(this.their_y,N_MINUS_2))return this.otr.error("Illegal g^y.",!0);this.createKeys(this.their_y),type="",send=HLP.packMPI(this.r),send+=this.makeM(this.their_y,this.m1,this.c,this.m2),this.m1=null,this.m2=null,this.c=null;break;case"":if(HLP.debug.call(this.otr,"signature message"),this.otr.authstate!==CONST.AUTHSTATE_AWAITING_REVEALSIG)return;msg=HLP.splitype(["DATA","DATA","MAC"],msg.msg),this.r=HLP.readMPI(msg[0]);var key=CryptoJS.enc.Hex.parse(BigInt.bigInt2str(this.r,16));key=CryptoJS.enc.Latin1.stringify(key);var gxmpi=HLP.decryptAes(this.encrypted,key,HLP.packCtr(0));gxmpi=gxmpi.toString(CryptoJS.enc.Latin1),this.their_y=HLP.readMPI(gxmpi);var hash=CryptoJS.SHA256(CryptoJS.enc.Latin1.parse(gxmpi));return HLP.compare(this.hashed,hash.toString(CryptoJS.enc.Latin1))?HLP.checkGroup(this.their_y,N_MINUS_2)?(this.createKeys(this.their_y),vsm=this.verifySignMac(msg[2],msg[1],this.m2,this.c,this.their_y,this.our_dh.publicKey,this.m1,HLP.packCtr(0)),vsm[0]?this.otr.error(vsm[0],!0):(this.their_keyid=vsm[1],this.their_priv_pk=vsm[2],send=this.makeM(this.their_y,this.m1_prime,this.c_prime,this.m2_prime),this.m1=null,this.m2=null,this.m1_prime=null,this.m2_prime=null,this.c=null,this.c_prime=null,this.sendMsg(version,"",send),void this.akeSuccess(version))):this.otr.error("Illegal g^x.",!0):this.otr.error("Hashed g^x does not match.",!0);case"":if(HLP.debug.call(this.otr,"data message"),this.otr.authstate!==CONST.AUTHSTATE_AWAITING_SIG)return;return msg=HLP.splitype(["DATA","MAC"],msg.msg),vsm=this.verifySignMac(msg[1],msg[0],this.m2_prime,this.c_prime,this.their_y,this.our_dh.publicKey,this.m1_prime,HLP.packCtr(0)),vsm[0]?this.otr.error(vsm[0],!0):(this.their_keyid=vsm[1],this.their_priv_pk=vsm[2],this.m1_prime=null,this.m2_prime=null,this.c_prime=null,this.transmittedRS=!0,void this.akeSuccess(version));default:return}this.sendMsg(version,type,send)},sendMsg:function(version,type,msg){var send=version+type,v3=version===CONST.OTR_VERSION_3;return v3&&(HLP.debug.call(this.otr,"instance tags"),send+=this.otr.our_instance_tag,send+=this.otr.their_instance_tag),send+=msg,send=HLP.wrapMsg(send,this.otr.fragment_size,v3,this.otr.our_instance_tag,this.otr.their_instance_tag),send[0]?this.otr.error(send[0]):void this.otr.io(send[1])},initiateAKE:function(version){HLP.debug.call(this.otr,"d-h commit message"),this.otr.trigger("status",[CONST.STATUS_AKE_INIT]),this.otr.authstate=CONST.AUTHSTATE_AWAITING_DHKEY;var gxmpi=HLP.packMPI(this.our_dh.publicKey);gxmpi=CryptoJS.enc.Latin1.parse(gxmpi),this.r=BigInt.randBigInt(128);var key=CryptoJS.enc.Hex.parse(BigInt.bigInt2str(this.r,16));key=CryptoJS.enc.Latin1.stringify(key),this.myhashed=CryptoJS.SHA256(gxmpi),this.myhashed=HLP.packData(this.myhashed.toString(CryptoJS.enc.Latin1)),this.dhcommit=HLP.packData(HLP.encryptAes(gxmpi,key,HLP.packCtr(0))),this.dhcommit+=this.myhashed,this.sendMsg(version,"",this.dhcommit)}}}.call(this),function(){"use strict";function SM(reqs){return this instanceof SM?(this.version=1,this.our_fp=reqs.our_fp,this.their_fp=reqs.their_fp,this.ssid=reqs.ssid,this.debug=!!reqs.debug,void this.init()):new SM(reqs)}var CryptoJS,BigInt,EventEmitter,CONST,HLP,root=this;"undefined"!=typeof module&&module.exports?(module.exports=SM,CryptoJS=require("../vendor/crypto.js"),BigInt=require("../vendor/bigint.js"),EventEmitter=require("../vendor/eventemitter.js"),CONST=require("./const.js"),HLP=require("./helpers.js")):(root.OTR.SM=SM,CryptoJS=root.CryptoJS,BigInt=root.BigInt,EventEmitter=root.EventEmitter,CONST=root.OTR.CONST,HLP=root.OTR.HLP);var G=BigInt.str2bigInt(CONST.G,10),N=BigInt.str2bigInt(CONST.N,16),N_MINUS_2=BigInt.sub(N,BigInt.str2bigInt("2",10)),Q=BigInt.sub(N,BigInt.str2bigInt("1",10));BigInt.divInt_(Q,2),HLP.extend(SM,EventEmitter),SM.prototype.init=function(){this.smpstate=CONST.SMPSTATE_EXPECT1,this.secret=null},SM.prototype.makeSecret=function(our,secret){var sha256=CryptoJS.algo.SHA256.create();sha256.update(CryptoJS.enc.Latin1.parse(HLP.packBytes(this.version,1))),sha256.update(CryptoJS.enc.Hex.parse(our?this.our_fp:this.their_fp)),sha256.update(CryptoJS.enc.Hex.parse(our?this.their_fp:this.our_fp)),sha256.update(CryptoJS.enc.Latin1.parse(this.ssid)),sha256.update(CryptoJS.enc.Latin1.parse(secret));var hash=sha256.finalize();this.secret=HLP.bits2bigInt(hash.toString(CryptoJS.enc.Latin1))},SM.prototype.makeG2s=function(){this.a2=HLP.randomExponent(),this.a3=HLP.randomExponent(),this.g2a=BigInt.powMod(G,this.a2,N),this.g3a=BigInt.powMod(G,this.a3,N),HLP.checkGroup(this.g2a,N_MINUS_2)&&HLP.checkGroup(this.g3a,N_MINUS_2)||this.makeG2s()},SM.prototype.computeGs=function(g2a,g3a){this.g2=BigInt.powMod(g2a,this.a2,N),this.g3=BigInt.powMod(g3a,this.a3,N)},SM.prototype.computePQ=function(r){this.p=BigInt.powMod(this.g3,r,N),this.q=HLP.multPowMod(G,r,this.g2,this.secret,N)},SM.prototype.computeR=function(){this.r=BigInt.powMod(this.QoQ,this.a3,N)},SM.prototype.computeRab=function(r){return BigInt.powMod(r,this.a3,N)},SM.prototype.computeC=function(v,r){return HLP.smpHash(v,BigInt.powMod(G,r,N))},SM.prototype.computeD=function(r,a,c){return BigInt.subMod(r,BigInt.multMod(a,c,Q),Q)},SM.prototype.handleSM=function(msg){var send,r2,r3,r7,t1,t2,t3,t4,rab,tmp2,cR,d7,ms,trust,expectStates={2:CONST.SMPSTATE_EXPECT1,3:CONST.SMPSTATE_EXPECT2,4:CONST.SMPSTATE_EXPECT3,5:CONST.SMPSTATE_EXPECT4,7:CONST.SMPSTATE_EXPECT1};if(6===msg.type)return this.init(),void this.trigger("abort");if(this.smpstate!==expectStates[msg.type])return this.abort();switch(this.smpstate){case CONST.SMPSTATE_EXPECT1:HLP.debug.call(this,"smp tlv 2");var ind,question;return 7===msg.type&&(ind=msg.msg.indexOf("\x00"),question=msg.msg.substring(0,ind),msg.msg=msg.msg.substring(ind+1)),ms=HLP.readLen(msg.msg.substr(0,4)),6!==ms?this.abort():(msg=HLP.unpackMPIs(6,msg.msg.substring(4)),HLP.checkGroup(msg[0],N_MINUS_2)&&HLP.checkGroup(msg[3],N_MINUS_2)&&HLP.ZKP(1,msg[1],HLP.multPowMod(G,msg[2],msg[0],msg[1],N))&&HLP.ZKP(2,msg[4],HLP.multPowMod(G,msg[5],msg[3],msg[4],N))?(this.g3ao=msg[3],this.makeG2s(),r2=HLP.randomExponent(),r3=HLP.randomExponent(),this.c2=this.computeC(3,r2),this.c3=this.computeC(4,r3),this.d2=this.computeD(r2,this.a2,this.c2),this.d3=this.computeD(r3,this.a3,this.c3),this.computeGs(msg[0],msg[3]),this.smpstate=CONST.SMPSTATE_EXPECT0,question=CryptoJS.enc.Latin1.parse(question).toString(CryptoJS.enc.Utf8),void this.trigger("question",[question])):this.abort());case CONST.SMPSTATE_EXPECT2:if(HLP.debug.call(this,"smp tlv 3"),ms=HLP.readLen(msg.msg.substr(0,4)),11!==ms)return this.abort();if(msg=HLP.unpackMPIs(11,msg.msg.substring(4)),!(HLP.checkGroup(msg[0],N_MINUS_2)&&HLP.checkGroup(msg[3],N_MINUS_2)&&HLP.checkGroup(msg[6],N_MINUS_2)&&HLP.checkGroup(msg[7],N_MINUS_2)))return this.abort();if(!HLP.ZKP(3,msg[1],HLP.multPowMod(G,msg[2],msg[0],msg[1],N)))return this.abort();if(!HLP.ZKP(4,msg[4],HLP.multPowMod(G,msg[5],msg[3],msg[4],N)))return this.abort();if(this.g3ao=msg[3],this.computeGs(msg[0],msg[3]),t1=HLP.multPowMod(this.g3,msg[9],msg[6],msg[8],N),t2=HLP.multPowMod(G,msg[9],this.g2,msg[10],N),t2=BigInt.multMod(t2,BigInt.powMod(msg[7],msg[8],N),N),!HLP.ZKP(5,msg[8],t1,t2))return this.abort();var r4=HLP.randomExponent();this.computePQ(r4);var r5=HLP.randomExponent(),r6=HLP.randomExponent(),tmp=HLP.multPowMod(G,r5,this.g2,r6,N),cP=HLP.smpHash(6,BigInt.powMod(this.g3,r5,N),tmp),d5=this.computeD(r5,r4,cP),d6=this.computeD(r6,this.secret,cP);this.QoQ=BigInt.divMod(this.q,msg[7],N),this.PoP=BigInt.divMod(this.p,msg[6],N),this.computeR(),r7=HLP.randomExponent(),tmp2=BigInt.powMod(this.QoQ,r7,N),cR=HLP.smpHash(7,BigInt.powMod(G,r7,N),tmp2),d7=this.computeD(r7,this.a3,cR),this.smpstate=CONST.SMPSTATE_EXPECT4,send=HLP.packINT(8)+HLP.packMPIs([this.p,this.q,cP,d5,d6,this.r,cR,d7]),send=HLP.packTLV(4,send);break;case CONST.SMPSTATE_EXPECT3:if(HLP.debug.call(this,"smp tlv 4"),ms=HLP.readLen(msg.msg.substr(0,4)),8!==ms)return this.abort();if(msg=HLP.unpackMPIs(8,msg.msg.substring(4)),!HLP.checkGroup(msg[0],N_MINUS_2)||!HLP.checkGroup(msg[1],N_MINUS_2)||!HLP.checkGroup(msg[5],N_MINUS_2))return this.abort();if(t1=HLP.multPowMod(this.g3,msg[3],msg[0],msg[2],N),t2=HLP.multPowMod(G,msg[3],this.g2,msg[4],N),t2=BigInt.multMod(t2,BigInt.powMod(msg[1],msg[2],N),N),!HLP.ZKP(6,msg[2],t1,t2))return this.abort();if(t3=HLP.multPowMod(G,msg[7],this.g3ao,msg[6],N),this.QoQ=BigInt.divMod(msg[1],this.q,N),t4=HLP.multPowMod(this.QoQ,msg[7],msg[5],msg[6],N),!HLP.ZKP(7,msg[6],t3,t4))return this.abort();this.computeR(),r7=HLP.randomExponent(),tmp2=BigInt.powMod(this.QoQ,r7,N),cR=HLP.smpHash(8,BigInt.powMod(G,r7,N),tmp2),d7=this.computeD(r7,this.a3,cR),send=HLP.packINT(3)+HLP.packMPIs([this.r,cR,d7]),send=HLP.packTLV(5,send),rab=this.computeRab(msg[5]),trust=!!BigInt.equals(rab,BigInt.divMod(msg[0],this.p,N)),this.trigger("trust",[trust,"answered"]),this.init();break;case CONST.SMPSTATE_EXPECT4:return HLP.debug.call(this,"smp tlv 5"),ms=HLP.readLen(msg.msg.substr(0,4)),3!==ms?this.abort():(msg=HLP.unpackMPIs(3,msg.msg.substring(4)),HLP.checkGroup(msg[0],N_MINUS_2)?(t3=HLP.multPowMod(G,msg[2],this.g3ao,msg[1],N),t4=HLP.multPowMod(this.QoQ,msg[2],msg[0],msg[1],N),HLP.ZKP(8,msg[1],t3,t4)?(rab=this.computeRab(msg[0]),trust=!!BigInt.equals(rab,this.PoP),this.trigger("trust",[trust,"asked"]),void this.init()):this.abort()):this.abort())}this.sendMsg(send)},SM.prototype.sendMsg=function(send){this.trigger("send",[this.ssid,"\x00"+send])},SM.prototype.rcvSecret=function(secret,question){HLP.debug.call(this,"receive secret");var fn,our=!1;this.smpstate===CONST.SMPSTATE_EXPECT0?fn=this.answer:(fn=this.initiate,our=!0),this.makeSecret(our,secret),fn.call(this,question)},SM.prototype.answer=function(){HLP.debug.call(this,"smp answer");var r4=HLP.randomExponent();this.computePQ(r4);var r5=HLP.randomExponent(),r6=HLP.randomExponent(),tmp=HLP.multPowMod(G,r5,this.g2,r6,N),cP=HLP.smpHash(5,BigInt.powMod(this.g3,r5,N),tmp),d5=this.computeD(r5,r4,cP),d6=this.computeD(r6,this.secret,cP);this.smpstate=CONST.SMPSTATE_EXPECT3;var send=HLP.packINT(11)+HLP.packMPIs([this.g2a,this.c2,this.d2,this.g3a,this.c3,this.d3,this.p,this.q,cP,d5,d6]);this.sendMsg(HLP.packTLV(3,send)) +},SM.prototype.initiate=function(question){HLP.debug.call(this,"smp initiate"),this.smpstate!==CONST.SMPSTATE_EXPECT1&&this.abort(),this.makeG2s();var r2=HLP.randomExponent(),r3=HLP.randomExponent();this.c2=this.computeC(1,r2),this.c3=this.computeC(2,r3),this.d2=this.computeD(r2,this.a2,this.c2),this.d3=this.computeD(r3,this.a3,this.c3),this.smpstate=CONST.SMPSTATE_EXPECT2;var send="",type=2;question&&(send+=question,send+="\x00",type=7),send+=HLP.packINT(6)+HLP.packMPIs([this.g2a,this.c2,this.d2,this.g3a,this.c3,this.d3]),this.sendMsg(HLP.packTLV(type,send))},SM.prototype.abort=function(){this.init(),this.sendMsg(HLP.packTLV(6,"")),this.trigger("abort")}}.call(this),function(){"use strict";function OTR(options){if(!(this instanceof OTR))return new OTR(options);if(options=options||{},options.priv&&!(options.priv instanceof DSA))throw new Error("Requires long-lived DSA key.");if(this.priv=options.priv?options.priv:new DSA,this.fragment_size=options.fragment_size||0,this.fragment_size<0)throw new Error("Fragment size must be a positive integer.");if(this.send_interval=options.send_interval||0,this.send_interval<0)throw new Error("Send interval must be a positive integer.");this.outgoing=[],this.our_instance_tag=options.instance_tag||OTR.makeInstanceTag(),this.debug=!!options.debug,this.smw=options.smw,this.init();var self=this;["sendMsg","receiveMsg"].forEach(function(meth){self[meth]=self[meth].bind(self)}),EventEmitter.call(this)}var CryptoJS,BigInt,EventEmitter,Worker,SMWPath,CONST,HLP,Parse,AKE,SM,DSA,root=this;"undefined"!=typeof module&&module.exports?(module.exports=OTR,CryptoJS=require("../vendor/crypto.js"),BigInt=require("../vendor/bigint.js"),EventEmitter=require("../vendor/eventemitter.js"),SMWPath=require("path").join(__dirname,"/sm-webworker.js"),CONST=require("./const.js"),HLP=require("./helpers.js"),Parse=require("./parse.js"),AKE=require("./ake.js"),SM=require("./sm.js"),DSA=require("./dsa.js"),OTR.CONST=CONST):(Object.keys(root.OTR).forEach(function(k){OTR[k]=root.OTR[k]}),root.OTR=OTR,CryptoJS=root.CryptoJS,BigInt=root.BigInt,EventEmitter=root.EventEmitter,Worker=root.Worker,SMWPath="sm-webworker.js",CONST=OTR.CONST,HLP=OTR.HLP,Parse=OTR.Parse,AKE=OTR.AKE,SM=OTR.SM,DSA=root.DSA);var G=BigInt.str2bigInt(CONST.G,10),N=BigInt.str2bigInt(CONST.N,16),MAX_INT=Math.pow(2,53)-1,MAX_UINT=Math.pow(2,31)-1;HLP.extend(OTR,EventEmitter),OTR.prototype.init=function(){this.msgstate=CONST.MSGSTATE_PLAINTEXT,this.authstate=CONST.AUTHSTATE_NONE,this.ALLOW_V2=!0,this.ALLOW_V3=!0,this.REQUIRE_ENCRYPTION=!1,this.SEND_WHITESPACE_TAG=!1,this.WHITESPACE_START_AKE=!1,this.ERROR_START_AKE=!1,Parse.initFragment(this),this.their_y=null,this.their_old_y=null,this.their_keyid=0,this.their_priv_pk=null,this.their_instance_tag="\x00\x00\x00\x00",this.our_dh=this.dh(),this.our_old_dh=this.dh(),this.our_keyid=2,this.sessKeys=[new Array(2),new Array(2)],this.storedMgs=[],this.oldMacKeys=[],this.sm=null,this._akeInit(),this.receivedPlaintext=!1},OTR.prototype._akeInit=function(){this.ake=new AKE(this),this.transmittedRS=!1,this.ssid=null},OTR.prototype._SMW=function(otr,reqs){this.otr=otr;var opts={path:SMWPath,seed:BigInt.getSeed};"object"==typeof otr.smw&&Object.keys(otr.smw).forEach(function(k){opts[k]=otr.smw[k]}),"undefined"!=typeof module&&module.exports&&(Worker=require("webworker-threads").Worker),this.worker=new Worker(opts.path);var self=this;this.worker.onmessage=function(e){var d=e.data;d&&self.trigger(d.method,d.args)},this.worker.postMessage({type:"seed",seed:opts.seed(),imports:opts.imports}),this.worker.postMessage({type:"init",reqs:reqs})},HLP.extend(OTR.prototype._SMW,EventEmitter),["handleSM","rcvSecret","abort"].forEach(function(m){OTR.prototype._SMW.prototype[m]=function(){this.worker.postMessage({type:"method",method:m,args:Array.prototype.slice.call(arguments,0)})}}),OTR.prototype._smInit=function(){var reqs={ssid:this.ssid,our_fp:this.priv.fingerprint(),their_fp:this.their_priv_pk.fingerprint(),debug:this.debug};this.smw?(this.sm&&this.sm.worker.terminate(),this.sm=new this._SMW(this,reqs)):this.sm=new SM(reqs);var self=this;["trust","abort","question"].forEach(function(e){self.sm.on(e,function(){self.trigger("smp",[e].concat(Array.prototype.slice.call(arguments)))})}),this.sm.on("send",function(ssid,send){self.ssid===ssid&&(send=self.prepareMsg(send),self.io(send))})},OTR.prototype.io=function(msg,meta){msg=[].concat(msg).map(function(m){return{msg:m,meta:meta}}),this.outgoing=this.outgoing.concat(msg);var self=this;!function send(first){if(!first){if(!self.outgoing.length)return;var elem=self.outgoing.shift();self.trigger("io",[elem.msg,elem.meta])}setTimeout(send,first?0:self.send_interval)}(!0)},OTR.prototype.dh=function(){var keys={privateKey:BigInt.randBigInt(320)};return keys.publicKey=BigInt.powMod(G,keys.privateKey,N),keys},OTR.prototype.DHSession=function DHSession(our_dh,their_y){if(!(this instanceof DHSession))return new DHSession(our_dh,their_y);var s=BigInt.powMod(their_y,our_dh.privateKey,N),secbytes=HLP.packMPI(s);this.id=HLP.mask(HLP.h2("\x00",secbytes),0,64);var sq=BigInt.greater(our_dh.publicKey,their_y),sendbyte=sq?"":"",rcvbyte=sq?"":"";this.sendenc=HLP.mask(HLP.h1(sendbyte,secbytes),0,128),this.sendmac=CryptoJS.SHA1(CryptoJS.enc.Latin1.parse(this.sendenc)),this.sendmac=this.sendmac.toString(CryptoJS.enc.Latin1),this.rcvenc=HLP.mask(HLP.h1(rcvbyte,secbytes),0,128),this.rcvmac=CryptoJS.SHA1(CryptoJS.enc.Latin1.parse(this.rcvenc)),this.rcvmac=this.rcvmac.toString(CryptoJS.enc.Latin1),this.rcvmacused=!1,this.extra_symkey=HLP.h2("ÿ",secbytes),this.send_counter=0,this.rcv_counter=0},OTR.prototype.rotateOurKeys=function(){var self=this;this.sessKeys[1].forEach(function(sk){sk&&sk.rcvmacused&&self.oldMacKeys.push(sk.rcvmac)}),this.our_old_dh=this.our_dh,this.our_dh=this.dh(),this.our_keyid+=1,this.sessKeys[1][0]=this.sessKeys[0][0],this.sessKeys[1][1]=this.sessKeys[0][1],this.sessKeys[0]=[this.their_y?new this.DHSession(this.our_dh,this.their_y):null,this.their_old_y?new this.DHSession(this.our_dh,this.their_old_y):null]},OTR.prototype.rotateTheirKeys=function(their_y){this.their_keyid+=1;var self=this;this.sessKeys.forEach(function(sk){sk[1]&&sk[1].rcvmacused&&self.oldMacKeys.push(sk[1].rcvmac)}),this.their_old_y=this.their_y,this.sessKeys[0][1]=this.sessKeys[0][0],this.sessKeys[1][1]=this.sessKeys[1][0],this.their_y=their_y,this.sessKeys[0][0]=new this.DHSession(this.our_dh,this.their_y),this.sessKeys[1][0]=new this.DHSession(this.our_old_dh,this.their_y)},OTR.prototype.prepareMsg=function(msg,esk){if(this.msgstate!==CONST.MSGSTATE_ENCRYPTED||0===this.their_keyid)return this.error("Not ready to encrypt.");var sessKeys=this.sessKeys[1][0];if(sessKeys.send_counter>=MAX_INT)return this.error("Should have rekeyed by now.");sessKeys.send_counter+=1;var ctr=HLP.packCtr(sessKeys.send_counter),send=this.ake.otr_version+"",v3=this.ake.otr_version===CONST.OTR_VERSION_3;if(v3&&(send+=this.our_instance_tag,send+=this.their_instance_tag),send+="\x00",send+=HLP.packINT(this.our_keyid-1),send+=HLP.packINT(this.their_keyid),send+=HLP.packMPI(this.our_dh.publicKey),send+=ctr.substring(0,8),Math.ceil(msg.length/8)>=MAX_UINT)return this.error("Message is too long.");var aes=HLP.encryptAes(CryptoJS.enc.Latin1.parse(msg),sessKeys.sendenc,ctr);return send+=HLP.packData(aes),send+=HLP.make1Mac(send,sessKeys.sendmac),send+=HLP.packData(this.oldMacKeys.splice(0).join("")),send=HLP.wrapMsg(send,this.fragment_size,v3,this.our_instance_tag,this.their_instance_tag),send[0]?this.error(send[0]):(esk&&this.trigger("file",["send",sessKeys.extra_symkey,esk]),send[1])},OTR.prototype.handleDataMsg=function(msg){var vt=msg.version+msg.type;this.ake.otr_version===CONST.OTR_VERSION_3&&(vt+=msg.instance_tags);var types=["BYTE","INT","INT","MPI","CTR","DATA","MAC","DATA"];msg=HLP.splitype(types,msg.msg);var ign=""===msg[0];if(this.msgstate!==CONST.MSGSTATE_ENCRYPTED||8!==msg.length)return void(ign||this.error("Received an unreadable encrypted message.",!0));var our_keyid=this.our_keyid-HLP.readLen(msg[2]),their_keyid=this.their_keyid-HLP.readLen(msg[1]);if(0>our_keyid||our_keyid>1)return void(ign||this.error("Not of our latest keys.",!0));if(0>their_keyid||their_keyid>1)return void(ign||this.error("Not of your latest keys.",!0));var their_y=their_keyid?this.their_old_y:this.their_y;if(1===their_keyid&&!their_y)return void(ign||this.error("Do not have that key."));var sessKeys=this.sessKeys[our_keyid][their_keyid],ctr=HLP.unpackCtr(msg[4]);if(ctr<=sessKeys.rcv_counter)return void(ign||this.error("Counter in message is not larger."));sessKeys.rcv_counter=ctr,vt+=msg.slice(0,6).join("");var vmac=HLP.make1Mac(vt,sessKeys.rcvmac);if(!HLP.compare(msg[6],vmac))return void(ign||this.error("MACs do not match."));sessKeys.rcvmacused=!0;var out=HLP.decryptAes(msg[5].substring(4),sessKeys.rcvenc,HLP.padCtr(msg[4]));out=out.toString(CryptoJS.enc.Latin1),our_keyid||this.rotateOurKeys(),their_keyid||this.rotateTheirKeys(HLP.readMPI(msg[3]));var ind=out.indexOf("\x00");return~ind&&(this.handleTLVs(out.substring(ind+1),sessKeys),out=out.substring(0,ind)),out=CryptoJS.enc.Latin1.parse(out),out.toString(CryptoJS.enc.Utf8)},OTR.prototype.handleTLVs=function(tlvs,sessKeys){for(var type,len,msg;tlvs.length&&(type=HLP.unpackSHORT(tlvs.substr(0,2)),len=HLP.unpackSHORT(tlvs.substr(2,2)),msg=tlvs.substr(4,len),!(msg.length<len));){switch(type){case 1:this.msgstate=CONST.MSGSTATE_FINISHED,this.trigger("status",[CONST.STATUS_END_OTR]);break;case 2:case 3:case 4:case 5:case 6:case 7:if(this.msgstate!==CONST.MSGSTATE_ENCRYPTED)return void(this.sm&&this.sm.abort());this.sm||this._smInit(),this.sm.handleSM({msg:msg,type:type});break;case 8:msg=msg.substring(4),msg=CryptoJS.enc.Latin1.parse(msg),msg=msg.toString(CryptoJS.enc.Utf8),this.trigger("file",["receive",sessKeys.extra_symkey,msg])}tlvs=tlvs.substring(4+len)}},OTR.prototype.smpSecret=function(secret,question){return this.msgstate!==CONST.MSGSTATE_ENCRYPTED?this.error("Must be encrypted for SMP."):"string"!=typeof secret||secret.length<1?this.error("Secret is required."):(this.sm||this._smInit(),secret=CryptoJS.enc.Utf8.parse(secret).toString(CryptoJS.enc.Latin1),question=CryptoJS.enc.Utf8.parse(question).toString(CryptoJS.enc.Latin1),void this.sm.rcvSecret(secret,question))},OTR.prototype.sendQueryMsg=function(){var versions={},msg=CONST.OTR_TAG;this.ALLOW_V2&&(versions[2]=!0),this.ALLOW_V3&&(versions[3]=!0);var vs=Object.keys(versions);vs.length&&(msg+="v",vs.forEach(function(v){"1"!==v&&(msg+=v)}),msg+="?"),this.io(msg),this.trigger("status",[CONST.STATUS_SEND_QUERY])},OTR.prototype.sendMsg=function(msg,meta){switch((this.REQUIRE_ENCRYPTION||this.msgstate!==CONST.MSGSTATE_PLAINTEXT)&&(msg=CryptoJS.enc.Utf8.parse(msg),msg=msg.toString(CryptoJS.enc.Latin1)),this.msgstate){case CONST.MSGSTATE_PLAINTEXT:if(this.REQUIRE_ENCRYPTION)return this.storedMgs.push({msg:msg,meta:meta}),void this.sendQueryMsg();this.SEND_WHITESPACE_TAG&&!this.receivedPlaintext&&(msg+=CONST.WHITESPACE_TAG,this.ALLOW_V3&&(msg+=CONST.WHITESPACE_TAG_V3),this.ALLOW_V2&&(msg+=CONST.WHITESPACE_TAG_V2));break;case CONST.MSGSTATE_FINISHED:return this.storedMgs.push({msg:msg,meta:meta}),void this.error("Message cannot be sent at this time.");case CONST.MSGSTATE_ENCRYPTED:msg=this.prepareMsg(msg);break;default:throw new Error("Unknown message state.")}msg&&this.io(msg,meta)},OTR.prototype.receiveMsg=function(msg){if(msg=Parse.parseMsg(this,msg)){switch(msg.cls){case"error":return void this.error(msg.msg);case"ake":if(msg.version===CONST.OTR_VERSION_3&&this.checkInstanceTags(msg.instance_tags))return;return void this.ake.handleAKE(msg);case"data":if(msg.version===CONST.OTR_VERSION_3&&this.checkInstanceTags(msg.instance_tags))return;msg.msg=this.handleDataMsg(msg),msg.encrypted=!0;break;case"query":this.msgstate===CONST.MSGSTATE_ENCRYPTED&&this._akeInit(),this.doAKE(msg);break;default:(this.REQUIRE_ENCRYPTION||this.msgstate!==CONST.MSGSTATE_PLAINTEXT)&&this.error("Received an unencrypted message."),this.receivedPlaintext=!0,this.WHITESPACE_START_AKE&&msg.ver.length>0&&this.doAKE(msg)}msg.msg&&this.trigger("ui",[msg.msg,!!msg.encrypted])}},OTR.prototype.checkInstanceTags=function(it){var their_it=HLP.readLen(it.substr(0,4)),our_it=HLP.readLen(it.substr(4,4));if(our_it&&our_it!==HLP.readLen(this.our_instance_tag))return!0;if(HLP.readLen(this.their_instance_tag)){if(HLP.readLen(this.their_instance_tag)!==their_it)return!0}else{if(100>their_it)return!0;this.their_instance_tag=HLP.packINT(their_it)}},OTR.prototype.doAKE=function(msg){this.ALLOW_V3&&~msg.ver.indexOf(CONST.OTR_VERSION_3)?this.ake.initiateAKE(CONST.OTR_VERSION_3):this.ALLOW_V2&&~msg.ver.indexOf(CONST.OTR_VERSION_2)?this.ake.initiateAKE(CONST.OTR_VERSION_2):this.error("OTR conversation requested, but no compatible protocol version found.")},OTR.prototype.error=function(err,send){return send?(this.debug||(err="An OTR error has occurred."),err="?OTR Error:"+err,void this.io(err)):void this.trigger("error",[err])},OTR.prototype.sendStored=function(){var self=this;this.storedMgs.splice(0).forEach(function(elem){var msg=self.prepareMsg(elem.msg);self.io(msg,elem.meta)})},OTR.prototype.sendFile=function(filename){if(this.msgstate!==CONST.MSGSTATE_ENCRYPTED)return this.error("Not ready to encrypt.");if(this.ake.otr_version!==CONST.OTR_VERSION_3)return this.error("Protocol v3 required.");if(!filename)return this.error("Please specify a filename.");var l1name=CryptoJS.enc.Utf8.parse(filename);if(l1name=l1name.toString(CryptoJS.enc.Latin1),l1name.length>=65532)return this.error("filename is too long.");var msg="\x00";msg+="\x00\b",msg+=HLP.packSHORT(4+l1name.length),msg+="\x00\x00\x00",msg+=l1name,msg=this.prepareMsg(msg,filename),this.io(msg)},OTR.prototype.endOtr=function(){this.msgstate===CONST.MSGSTATE_ENCRYPTED&&(this.sendMsg("\x00\x00\x00\x00"),this.sm&&(this.smw&&this.sm.worker.terminate(),this.sm=null)),this.msgstate=CONST.MSGSTATE_PLAINTEXT,this.receivedPlaintext=!1,this.trigger("status",[CONST.STATUS_END_OTR])},OTR.makeInstanceTag=function(){var num=BigInt.randBigInt(32);return BigInt.greater(BigInt.str2bigInt("100",16),num)?OTR.makeInstanceTag():HLP.packINT(parseInt(BigInt.bigInt2str(num,10),10))}}.call(this),{OTR:this.OTR,DSA:this.DSA}}); + +};
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/browser/otr.min.js_README Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,10 @@ +The file otr.min.js includes: + - minified versions of the following otr.js dependencies: + otr/dep/bigint.js otr/dep/crypto.js otr/dep/eventemitter.js + The following minification tool has been used: yui compressor + - minified version of otr.js taken from the project homepage + +All original files can be retrieved from otr.js repository: + https://github.com/arlolra/otr/tree/master/build + +See the README file of Libervia, or inside otr.min.js itself for licence information.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/browser/public/contrat_social.html Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,110 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> +<html><head> + + <meta content="text/html; charset=ISO-8859-1" http-equiv="content-type"> + <title>Salut Toi: Contrat Social</title> + + +</head><body> +Le projet Salut Toi est n d'un besoin de protection de nos +liberts, de notre vie prive et de notre indpendance. Il se veut +garant des droits et liberts qu'un utilisateur a vis vis de ses +propres informations, des informations numriques sur sa vie ou celles +de ses connaissances, des donnes qu'il manipule; et se veut galement +un point de contact humain, ne se substituant pas aux rapports rels, +mais au contraire les facilitant.<br> + +Salut Toi lutte et luttera toujours contre toute forme de main mise +sur les technologies par des intrts privs. Le rseau global doit +appartenir tous, et tre un point d'expression et de libert pour +l'Humanit.<br> + +<br> + + ce titre, Salut Toi et ceux qui y participent se basent sur un +contrat social, un engagement vis vis de ceux qui l'utilisent. Ce +contrat consiste en les points suivants:<br> + +<ul> + + <li>nous plaons la <span style="font-style: italic;">Libert</span> en tte de nos priorits: libert de +l'utilisateur, libert vis vis de ses donnes. Pour cela, Salut +Toi est un logiciel Libre - condition essentielle -, et son +infrastructure se base galement sur des logiciels Libres, c'est dire +des logiciels qui respectent ces 4 liberts fondamentales + <ul> + + <li>la libert d'excuter le programme, pour tous les usages,</li> + + </ul> + <ul> + + <li>la libert d'tudier le fonctionnement du programme et de +l'adapter ses besoins,</li> + + </ul> + <ul> + + <li>la libert de redistribuer des copies du programme,</li> + + </ul> + <ul> + + <li>la libert d'amliorer le programme et de distribuer ces +amliorations au public.<br> +</li> + + </ul> +</li> + + + + + +Vous avez ainsi la possibilit d'installer votre propre version de +Salut Toi sur votre propre machine, d'en vrifier - et de +comprendre - ainsi son fonctionnement, de l'adapter vos besoins, d'en +faire profiter vos amis. + + <li>Les informations vous concernant vous appartiennent, et nous +n'aurons pas la prtention - et l'indcence ! - de considrer le +contenu que vous produisez ou faites circuler via Salut Toi comme +nous appartenant. De mme, nous nous engageons ne jamais faire de +profit en revendant vos informations personnelles.</li> + <li>Nous incitons fortement la <span style="text-decoration: underline;">dcentralisation gnralise</span>. +Salut Toi tant bas sur un protocole dcentralis (XMPP), il l'est +lui-mme par nature. La dcentralisation est essentielle pour une +meilleure protection de vos informations, une meilleure rsistance la +censure ou aux pannes, et pour viter les drives autoritaires.</li> + <li>Luttant contre les tentatives de contrle priv et les abus +commerciaux du rseau global, et afin de garder notre indpendance, +nous nous refusons toute forme de publicit: vous ne verrez <span style="font-weight: bold;">jamais</span> +de forme de rclame commerciale de notre fait.</li> + <li>L'<span style="font-style: italic;">galit</span> des utilisateurs est essentielle pour nous, nous +refusons toute forme de discrimination, que ce soit pour une zone +gographique, une catgorie de la population, ou tout autre raison.</li> + <li>Nous ferons tout notre possible pour lutter contre toute +tentative de censure. Le rseau global doit tre un moyen d'expression +pour tous.</li> + <li>Nous refusons toute ide d'autorit absolue en ce qui concerne +les dcisions prises pour Salut Toi et son fonctionnement, et le +choix de la dcentralisation et l'utilisation de logiciel Libre permet +de lutter contre toute forme de hirarchie.</li> + + <li>L'ide de <span style="font-style: italic;">Fraternit</span> est essentielle, aussi: + <ul> + <li>nous ferons notre +possible pour aider les utilisateurs, quel que soit leur niveau</li> + <li>de mme, des efforts seront fait quant +l'accessibilit pour tous</li> + <li> Salut Toi , +XMPP, et les technologies utilises facilitent les changes +lectroniques, mais nous dsirons mettre l'accent sur les rencontres +relles et humaines: nous favoriserons toujours le rel sur le virtuel.</li> + </ul> +</li> + + +</ul> + +</body></html>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/browser/public/favico.min.js Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,9 @@ +/** + * This file is distributed with Libervia and is sublicensed under AGPL v3 (or any later version) as allowed by its original license. + * + * @license MIT + * @fileOverview Favico animations + * @author Miroslav Magda, http://blog.ejci.net + * @version 0.3.9 + */ +!function(){var e=function(e){"use strict";function t(e){if(e.paused||e.ended||g)return!1;try{f.clearRect(0,0,s,l),f.drawImage(e,0,0,s,l)}catch(o){}p=setTimeout(t,S.duration,e),O.setIcon(h)}function o(e){var t=/^#?([a-f\d])([a-f\d])([a-f\d])$/i;e=e.replace(t,function(e,t,o,n){return t+t+o+o+n+n});var o=/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(e);return o?{r:parseInt(o[1],16),g:parseInt(o[2],16),b:parseInt(o[3],16)}:!1}function n(e,t){var o,n={};for(o in e)n[o]=e[o];for(o in t)n[o]=t[o];return n}function r(){return b.hidden||b.msHidden||b.webkitHidden||b.mozHidden}e=e?e:{};var i,a,l,s,h,f,c,d,u,y,w,g,x,m,p,b,v={bgColor:"#d00",textColor:"#fff",fontFamily:"sans-serif",fontStyle:"bold",type:"circle",position:"down",animation:"slide",elementId:!1,dataUrl:!1,win:window};x={},x.ff="undefined"!=typeof InstallTrigger,x.chrome=!!window.chrome,x.opera=!!window.opera||navigator.userAgent.indexOf("Opera")>=0,x.ie=/*@cc_on!@*/!1,x.safari=Object.prototype.toString.call(window.HTMLElement).indexOf("Constructor")>0,x.supported=x.chrome||x.ff||x.opera;var C=[];w=function(){},d=g=!1;var E=function(){i=n(v,e),i.bgColor=o(i.bgColor),i.textColor=o(i.textColor),i.position=i.position.toLowerCase(),i.animation=S.types[""+i.animation]?i.animation:v.animation,b=i.win.document;var t=i.position.indexOf("up")>-1,r=i.position.indexOf("left")>-1;if(t||r)for(var d=0;d<S.types[""+i.animation].length;d++){var u=S.types[""+i.animation][d];t&&(u.y=u.y<.6?u.y-.4:u.y-2*u.y+(1-u.w)),r&&(u.x=u.x<.6?u.x-.4:u.x-2*u.x+(1-u.h)),S.types[""+i.animation][d]=u}i.type=A[""+i.type]?i.type:v.type,a=O.getIcon(),h=document.createElement("canvas"),c=document.createElement("img"),a.hasAttribute("href")?(c.setAttribute("crossOrigin","anonymous"),c.setAttribute("src",a.getAttribute("href")),c.onload=function(){l=c.height>0?c.height:32,s=c.width>0?c.width:32,h.height=l,h.width=s,f=h.getContext("2d"),M.ready()}):(c.setAttribute("src",""),l=32,s=32,c.height=l,c.width=s,h.height=l,h.width=s,f=h.getContext("2d"),M.ready())},M={};M.ready=function(){d=!0,M.reset(),w()},M.reset=function(){d&&(C=[],u=!1,y=!1,f.clearRect(0,0,s,l),f.drawImage(c,0,0,s,l),O.setIcon(h),window.clearTimeout(m),window.clearTimeout(p))},M.start=function(){if(d&&!y){var e=function(){u=C[0],y=!1,C.length>0&&(C.shift(),M.start())};if(C.length>0){y=!0;var t=function(){["type","animation","bgColor","textColor","fontFamily","fontStyle"].forEach(function(e){e in C[0].options&&(i[e]=C[0].options[e])}),S.run(C[0].options,function(){e()},!1)};u?S.run(u.options,function(){t()},!0):t()}}};var A={},I=function(e){return e.n="number"==typeof e.n?Math.abs(0|e.n):e.n,e.x=s*e.x,e.y=l*e.y,e.w=s*e.w,e.h=l*e.h,e.len=(""+e.n).length,e};A.circle=function(e){e=I(e);var t=!1;2===e.len?(e.x=e.x-.4*e.w,e.w=1.4*e.w,t=!0):e.len>=3&&(e.x=e.x-.65*e.w,e.w=1.65*e.w,t=!0),f.clearRect(0,0,s,l),f.drawImage(c,0,0,s,l),f.beginPath(),f.font=i.fontStyle+" "+Math.floor(e.h*(e.n>99?.85:1))+"px "+i.fontFamily,f.textAlign="center",t?(f.moveTo(e.x+e.w/2,e.y),f.lineTo(e.x+e.w-e.h/2,e.y),f.quadraticCurveTo(e.x+e.w,e.y,e.x+e.w,e.y+e.h/2),f.lineTo(e.x+e.w,e.y+e.h-e.h/2),f.quadraticCurveTo(e.x+e.w,e.y+e.h,e.x+e.w-e.h/2,e.y+e.h),f.lineTo(e.x+e.h/2,e.y+e.h),f.quadraticCurveTo(e.x,e.y+e.h,e.x,e.y+e.h-e.h/2),f.lineTo(e.x,e.y+e.h/2),f.quadraticCurveTo(e.x,e.y,e.x+e.h/2,e.y)):f.arc(e.x+e.w/2,e.y+e.h/2,e.h/2,0,2*Math.PI),f.fillStyle="rgba("+i.bgColor.r+","+i.bgColor.g+","+i.bgColor.b+","+e.o+")",f.fill(),f.closePath(),f.beginPath(),f.stroke(),f.fillStyle="rgba("+i.textColor.r+","+i.textColor.g+","+i.textColor.b+","+e.o+")","number"==typeof e.n&&e.n>999?f.fillText((e.n>9999?9:Math.floor(e.n/1e3))+"k+",Math.floor(e.x+e.w/2),Math.floor(e.y+e.h-.2*e.h)):f.fillText(e.n,Math.floor(e.x+e.w/2),Math.floor(e.y+e.h-.15*e.h)),f.closePath()},A.rectangle=function(e){e=I(e);var t=!1;2===e.len?(e.x=e.x-.4*e.w,e.w=1.4*e.w,t=!0):e.len>=3&&(e.x=e.x-.65*e.w,e.w=1.65*e.w,t=!0),f.clearRect(0,0,s,l),f.drawImage(c,0,0,s,l),f.beginPath(),f.font=i.fontStyle+" "+Math.floor(e.h*(e.n>99?.9:1))+"px "+i.fontFamily,f.textAlign="center",f.fillStyle="rgba("+i.bgColor.r+","+i.bgColor.g+","+i.bgColor.b+","+e.o+")",f.fillRect(e.x,e.y,e.w,e.h),f.fillStyle="rgba("+i.textColor.r+","+i.textColor.g+","+i.textColor.b+","+e.o+")","number"==typeof e.n&&e.n>999?f.fillText((e.n>9999?9:Math.floor(e.n/1e3))+"k+",Math.floor(e.x+e.w/2),Math.floor(e.y+e.h-.2*e.h)):f.fillText(e.n,Math.floor(e.x+e.w/2),Math.floor(e.y+e.h-.15*e.h)),f.closePath()};var T=function(e,t){t=("string"==typeof t?{animation:t}:t)||{},w=function(){try{if("number"==typeof e?e>0:""!==e){var n={type:"badge",options:{n:e}};if("animation"in t&&S.types[""+t.animation]&&(n.options.animation=""+t.animation),"type"in t&&A[""+t.type]&&(n.options.type=""+t.type),["bgColor","textColor"].forEach(function(e){e in t&&(n.options[e]=o(t[e]))}),["fontStyle","fontFamily"].forEach(function(e){e in t&&(n.options[e]=t[e])}),C.push(n),C.length>100)throw new Error("Too many badges requests in queue.");M.start()}else M.reset()}catch(r){throw new Error("Error setting badge. Message: "+r.message)}},d&&w()},U=function(e){w=function(){try{var t=e.width,o=e.height,n=document.createElement("img"),r=o/l>t/s?t/s:o/l;n.setAttribute("crossOrigin","anonymous"),n.setAttribute("src",e.getAttribute("src")),n.height=o/r,n.width=t/r,f.clearRect(0,0,s,l),f.drawImage(n,0,0,s,l),O.setIcon(h)}catch(i){throw new Error("Error setting image. Message: "+i.message)}},d&&w()},R=function(e){w=function(){try{if("stop"===e)return g=!0,M.reset(),void(g=!1);e.addEventListener("play",function(){t(this)},!1)}catch(o){throw new Error("Error setting video. Message: "+o.message)}},d&&w()},L=function(e){if(window.URL&&window.URL.createObjectURL||(window.URL=window.URL||{},window.URL.createObjectURL=function(e){return e}),x.supported){var o=!1;navigator.getUserMedia=navigator.getUserMedia||navigator.oGetUserMedia||navigator.msGetUserMedia||navigator.mozGetUserMedia||navigator.webkitGetUserMedia,w=function(){try{if("stop"===e)return g=!0,M.reset(),void(g=!1);o=document.createElement("video"),o.width=s,o.height=l,navigator.getUserMedia({video:!0,audio:!1},function(e){o.src=URL.createObjectURL(e),o.play(),t(o)},function(){})}catch(n){throw new Error("Error setting webcam. Message: "+n.message)}},d&&w()}},O={};O.getIcon=function(){var e=!1,t=function(){for(var e=b.getElementsByTagName("head")[0].getElementsByTagName("link"),t=e.length,o=t-1;o>=0;o--)if(/(^|\s)icon(\s|$)/i.test(e[o].getAttribute("rel")))return e[o];return!1};return i.element?e=i.element:i.elementId?(e=b.getElementById(i.elementId),e.setAttribute("href",e.getAttribute("src"))):(e=t(),e===!1&&(e=b.createElement("link"),e.setAttribute("rel","icon"),b.getElementsByTagName("head")[0].appendChild(e))),e.setAttribute("type","image/png"),e},O.setIcon=function(e){var t=e.toDataURL("image/png");if(i.dataUrl&&i.dataUrl(t),i.element)i.element.setAttribute("href",t),i.element.setAttribute("src",t);else if(i.elementId){var o=b.getElementById(i.elementId);o.setAttribute("href",t),o.setAttribute("src",t)}else if(x.ff||x.opera){var n=a;a=b.createElement("link"),x.opera&&a.setAttribute("rel","icon"),a.setAttribute("rel","icon"),a.setAttribute("type","image/png"),b.getElementsByTagName("head")[0].appendChild(a),a.setAttribute("href",t),n.parentNode&&n.parentNode.removeChild(n)}else a.setAttribute("href",t)};var S={};return S.duration=40,S.types={},S.types.fade=[{x:.4,y:.4,w:.6,h:.6,o:0},{x:.4,y:.4,w:.6,h:.6,o:.1},{x:.4,y:.4,w:.6,h:.6,o:.2},{x:.4,y:.4,w:.6,h:.6,o:.3},{x:.4,y:.4,w:.6,h:.6,o:.4},{x:.4,y:.4,w:.6,h:.6,o:.5},{x:.4,y:.4,w:.6,h:.6,o:.6},{x:.4,y:.4,w:.6,h:.6,o:.7},{x:.4,y:.4,w:.6,h:.6,o:.8},{x:.4,y:.4,w:.6,h:.6,o:.9},{x:.4,y:.4,w:.6,h:.6,o:1}],S.types.none=[{x:.4,y:.4,w:.6,h:.6,o:1}],S.types.pop=[{x:1,y:1,w:0,h:0,o:1},{x:.9,y:.9,w:.1,h:.1,o:1},{x:.8,y:.8,w:.2,h:.2,o:1},{x:.7,y:.7,w:.3,h:.3,o:1},{x:.6,y:.6,w:.4,h:.4,o:1},{x:.5,y:.5,w:.5,h:.5,o:1},{x:.4,y:.4,w:.6,h:.6,o:1}],S.types.popFade=[{x:.75,y:.75,w:0,h:0,o:0},{x:.65,y:.65,w:.1,h:.1,o:.2},{x:.6,y:.6,w:.2,h:.2,o:.4},{x:.55,y:.55,w:.3,h:.3,o:.6},{x:.5,y:.5,w:.4,h:.4,o:.8},{x:.45,y:.45,w:.5,h:.5,o:.9},{x:.4,y:.4,w:.6,h:.6,o:1}],S.types.slide=[{x:.4,y:1,w:.6,h:.6,o:1},{x:.4,y:.9,w:.6,h:.6,o:1},{x:.4,y:.9,w:.6,h:.6,o:1},{x:.4,y:.8,w:.6,h:.6,o:1},{x:.4,y:.7,w:.6,h:.6,o:1},{x:.4,y:.6,w:.6,h:.6,o:1},{x:.4,y:.5,w:.6,h:.6,o:1},{x:.4,y:.4,w:.6,h:.6,o:1}],S.run=function(e,t,o,a){var l=S.types[r()?"none":i.animation];return a=o===!0?"undefined"!=typeof a?a:l.length-1:"undefined"!=typeof a?a:0,t=t?t:function(){},a<l.length&&a>=0?(A[i.type](n(e,l[a])),m=setTimeout(function(){o?a-=1:a+=1,S.run(e,t,o,a)},S.duration),O.setIcon(h),void 0):void t()},E(),{badge:T,video:R,image:U,webcam:L,reset:M.reset,browser:{supported:x.supported}}};"undefined"!=typeof define&&define.amd?define([],function(){return e}):"undefined"!=typeof module&&module.exports?module.exports=e:this.Favico=e}();
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/browser/public/libervia.css Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,1687 @@ +/* +Libervia: a Salut à Toi frontend +Copyright (C) 2011-2016 Jérôme Poisson <goffi@goffi.org> +Copyright (C) 2011 Adrien Vigneron <adrienvigneron@mailoo.org> +Copyright (C) 2013-2016 Adrien Cossa <souliane@mailoo.org> + +This program 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. + +This program 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 this program. If not, see <http://www.gnu.org/licenses/>. +*/ + + +/* + * CSS Reset: see http://pyjs.org/wiki/csshellandhowtodealwithit/ + */ + +/* reset/default styles */ + +html, body, div, span, applet, object, iframe, +p, blockquote, pre, +a, abbr, acronym, address, big, cite, code, +del, dfn, em, font, img, ins, kbd, q, s, samp, +small, strike, strong, sub, sup, tt, var, +b, u, i, center, dl, dt, dd, li, +fieldset, form, label, legend, table, caption, +tbody, tfoot, thead, tr, th, td { + margin: 0; + padding: 0; + border: 0; + outline: 0; + font-size: 100%; + vertical-align: baseline; + background: transparent; + color: #444; +} + +/* styles for displaying rich text - START */ +h1, h2, h3, h4, h5, h6 { + margin: 0; + padding: 0; + border: 0; + outline: 0; + vertical-align: baseline; + background: transparent; + color: #444; + border-bottom: 1px solid rgb(170, 170, 170); + margin-bottom: 0.6em; +} +ol, ul { + margin: 0; + border: 0; + outline: 0; + font-size: 100%; + vertical-align: baseline; + background: transparent; + color: #444; +} +a:link { + color: blue; +} +.bubble p { + margin: 0.4em 0em; +} +.bubble img { + /* /!\ setting a max-width percentage value affects the toolbar icons */ + max-width: 600px; +} + +/* styles for displaying rich text - END */ + +blockquote, q { quotes: none; } + +blockquote:before, blockquote:after, +q:before, q:after { + content: ''; + content: none; +} + +:focus { outline: 0; } +ins { text-decoration: none; } +del { text-decoration: line-through; } + +table { + border-collapse: collapse; + border-spacing: 0; +} + +/* pyjamas iframe hide */ +iframe { position: absolute; } + + +html, body { + width: 100%; + height: 100%; + min-height: 100%; + +} + +body { + line-height: 1em; + font-size: 1em; + overflow: auto; + +} + +.scrollpanel { + margin-bottom: -10000px; + +} + +.iescrollpanelfix { + position: relative; + top: 100%; + margin-bottom: -10000px; + +} + +/* undo part of the above (non-IE) */ +html>body .iescrollpanelfix { position: static; } + +/* CSS Reset END */ + +body { + background-color: #fff; + font: normal 0.8em/1.5em Arial, Helvetica, sans-serif; +} + +.header { + background-color: #eee; + border-bottom: 1px solid #ddd; + width: 100%; + height: 64px; +} + +.mainPanel { + width: 100%; + height: 100%; +} + +.mainMenuBar { + background-color: #222; + background: -webkit-gradient(linear, left top, left bottom, from(#444444), to(#222222)); + background: -webkit-linear-gradient(top, #444444, #222222); + background: linear-gradient(to bottom, #444444, #222222); + height: 28px; + padding: 5px 5px 0 5px; + border: 1px solid #ddd; + border-radius: 0 0 1em 1em; + line-height: 100%; + -webkit-box-shadow: 0px 1px 4px #000; + box-shadow: 0px 1px 4px #000; + display: inline-block; + position: absolute; + left: 20px; + right: 20px; + width: auto; +} + +.mainMenuBar .gwt-MenuItem { + padding: 3px 15px; + text-decoration: none; + font-weight: bold; + height: 100%; + color: #e7e5e5; + border-radius: 1em 1em 1em 1em; + text-shadow: 0 1px 1px rgba(0, 0, 0, 0.4); + -webkit-transition: color 0.2s linear; + transition: color 0.2s linear; +} + +.mainMenuBar .gwt-MenuItem-selected { + background-color: #eee; + background: -webkit-gradient(linear, left top, left bottom, from(#eee), to(#aaa)); + background: -webkit-linear-gradient(top, #eee, #aaa); + background: linear-gradient(to bottom, #eee, #aaa); + color: #444; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.6); +} + +/* Menu bars and items */ + +.gwt-MenuBar { + /* Common to all menu bars */ + margin: 0; +} + +.gwt-MenuBar table { + /* Common to all tables within a menu bar */ + width: 100%; + display: inline-table; +} + +.gwt-MenuBar-horizontal { + /* Specific to horizontal menu bars*/ +} + +.gwt-MenuBar-vertical { + /* Specific to vertical menu bars*/ + background-color: #fff; + background: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#ccc)); + background: -webkit-linear-gradient(top, #fff, #ccc); + background: linear-gradient(to bottom, #fff, #ccc); + height: 100%; + min-width: 148px; + padding: 0; + border: solid 1px #aaa; + border-radius: 0 0 10px 10px; + -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, .3); + box-shadow: 0 1px 3px rgba(0, 0, 0, .3); +} + +.gwt-MenuItem img { + /* Common to all images within a menu item */ + padding-right: 2px; +} + +.gwt-MenuBar .gwt-MenuItem { + /* Common to items of all menu bars */ +} + +.gwt-MenuBar-horizontal .gwt-MenuItem { + /* Specific to items of horizontal menu bars*/ +} + +.gwt-MenuBar-vertical .gwt-MenuItem { + /* Specific to items of vertical menu bars*/ + padding: 8px 15px; +} + +.gwt-MenuBar .gwt-MenuItem-selected { + /* Common to all selected items */ + cursor: pointer; +} + +.gwt-MenuBar-horizontal .gwt-MenuItem-selected { + /* Specific to selected items of horizontal menu bars */ +} + +.gwt-MenuBar-vertical .gwt-MenuItem-selected { + /* Specific to selected items of vertical menu bars */ + background: #cf2828 !important; + background: -webkit-gradient(linear, left top, left bottom, from(#cf2828), to(#981a1a)) !important; + background: -webkit-linear-gradient(top, #cf2828, #981a1a) !important; + background: linear-gradient(to bottom, #cf2828, #981a1a) !important; + color: #fff !important; + border-radius: 0 0 0 0; + text-shadow: 0 1px 1px rgba(0, 0, 0, .1); + -webkit-transition: color 0.2s linear; + transition: color 0.2s linear; +} + +.gwt-MenuBar-vertical tr:last-child td { + /* Specific to last items of vertical menus */ + border-radius: 0 0 9px 9px !important; +} + +.menuLastPopup .gwt-MenuBar-vertical { + /* Specific to the last popup menu of the main menu bar */ + border-top-right-radius: 9px 9px; +} + +.menuLastPopup .gwt-MenuBar-vertical tr:first-child td { + /* Specific to the first item of the last popup menu of the main menu bar */ + border-radius: 0px 9px 0px 0px !important; +} + +.menuSeparator { + width: 100%; +} + +.menuSeparator.gwt-MenuItem-selected { + border: 0; + background: inherit; + cursor: default; +} + +.menuFlattenedCategory { + font-weight: bold; + font-style: italic; + padding: 8px 5px; + cursor: default; +} + +.menuFlattenedCategory.gwt-MenuItem-selected { + /* !important are needed for the style to not be overwritten when the item is selected */ + background-color: inherit !important; + background: inherit !important; + color: #444 !important; + cursor: default !important; +} + +/* Misc Pyjamas stuff */ + +.gwt-DialogBox { + padding: 10px; + border: 1px solid #aaa; + background-color: #fff; + background: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#ccc)); + background: -webkit-linear-gradient(top, #fff, #ccc); + background: linear-gradient(to bottom, #fff, #ccc); + border-radius: 9px 9px 9px 9px; + -webkit-box-shadow: 0px 1px 4px #000; + box-shadow: 0px 1px 4px #000; +} + +.gwt-DialogBox .Caption { + height: 20px; + font-size: 1.3em !important; + background-color: #cf2828; + background: #cf2828 !important; + background: -webkit-gradient(linear, left top, left bottom, from(#cf2828), to(#981a1a)) !important; + background: -webkit-linear-gradient(top, #cf2828, #981a1a) !important; + background: linear-gradient(to bottom, #cf2828, #981a1a) !important; + color: #fff; + padding: 3px 3px 4px 3px; + margin: -10px; + margin-bottom: 5px; + font-weight: bold; + cursor: default; + text-align: center; + border-radius: 7px 7px 0 0; +} + +/*DIALOG: button, listbox, textbox, label */ + +.gwt-DialogBox .gwt-button { + background-color: #ccc; + border-radius: 5px 5px 5px 5px; + -webkit-box-shadow: 0px 1px 4px #000; + box-shadow: 0px 1px 4px #000; + background: -webkit-gradient(linear, left top, left bottom, from(#444), to(#222)); + background: -webkit-linear-gradient(top, #444, #222); + background: linear-gradient(to bottom, #444, #222); + text-shadow: 1px 1px 1px rgba(0,0,0,0.2); + padding: 3px 5px 3px 5px; + margin: 10px 5px 10px 5px; + font-weight: bold; + font-size: 1em; + border: none; + -webkit-transition: color 0.2s linear; + transition: color 0.2s linear; +} + +.gwt-DialogBox .gwt-button:enabled { + cursor: pointer; + color: #fff; +} + +.gwt-DialogBox .gwt-button:enabled:hover { + background-color: #cf2828; + background: -webkit-gradient(linear, left top, left bottom, from(#cf2828), to(#981a1a)); + background: -webkit-linear-gradient(top, #cf2828, #981a1a); + background: linear-gradient(to bottom, #cf2828, #981a1a); + color: #fff; + text-shadow: 1px 1px 1px rgba(0,0,0,0.25); +} + +.gwt-DialogBox .gwt-TextBox, .gwt-DialogBox .gwt-PasswordTextBox { + background-color: #fff; + border-radius: 5px 5px 5px 5px; + -webkit-box-shadow:inset 0px 1px 4px #000; + box-shadow:inset 0px 1px 4px #000; + padding: 3px 5px 3px 5px; + margin: 10px 5px 10px 5px; + color: #444; + font-size: 1em; + border: none; +} + +.gwt-DialogBox .gwt-TextArea { + background-color: #fff; + border-radius: 5px 5px 5px 5px; + -webkit-box-shadow:inset 0px 1px 4px #000; + box-shadow:inset 0px 1px 4px #000; + padding: 3px 5px 3px 5px; + margin: 0px 5px 10px 5px; + color: #444; + border: none; + vertical-align: text-top; +} + +.gwt-DialogBox .gwt-ListBox { + overflow: auto; + width: 100%; + background-color: #fff; + border-radius: 5px 5px 5px 5px; + -webkit-box-shadow:inset 0px 1px 4px #000; + box-shadow:inset 0px 1px 4px #000; + padding: 3px 5px 3px 5px; + margin: 9px 5px 9px 5px; + color: #444; + font-size: 1em; + border: none; +} + +.gwt-DialogBox .gwt-Label { + margin-top: 13px; +} + +.gwt-DialogBox .gwt-CheckBox { + margin-top: 12px; + display: block; +} + +.gwt-DialogBox .gwt-RadioButton { + margin-top: 13px; + display: block; +} + +.gwt-DialogBox .gwt-RadioButton label { + vertical-align: bottom; +} + +.gwt-DialogBox tr td:first-child { + vertical-align: top !important; +} + +/* Custom Dialogs */ + +.formWarning { /* used when a form is not valid and must be corrected before submission */ + font-weight: bold; + color: lightcoral !important; + height: 34px; /* a higher value will screw up the display of registration tab, check before you modify */ + text-align: center; +} + +.formInfo { /* used when a form is being edited and we want to tell something to the user */ + color: lightcyan !important; +} + +.contactsChooser { + text-align: center; + margin:auto; + cursor: pointer; +} + +.infoDialogBody { + width: 100%; + height: 100% +} +/* Contact List */ + +div.contactList { + width: 100%; + margin-top: 9px; +} + +.contactTitle { + color: #cf2828; + font-size: 1.7em; + text-indent: 5px; + text-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); + width: 200px; + height: 30px; +} + +.contactsSwitch { + /* Button used to switch contacts panel */ + background: none; + border: 0; + padding: 0; + font-size: large; + margin-top: 9px; +} + +.groupPanel { + width: 100%; +} + +.groupPanel tr:first-child td { + padding-top: 10px; +} + +.group { + curser: pointer; + padding: 2px 15px; + margin: 5px; + display: inline-block; + text-decoration: none; + font-weight: bold; + color: #e7e5e5; + text-shadow: 0 1px 1px rgba(0, 0, 0, 0.4); + border-radius: 1em 1em 1em 1em; + background-color: #eee; + background: -webkit-gradient(linear, left top, left bottom, from(#eee), to(#aaa)); + background: -webkit-linear-gradient(top, #eee, #aaa); + background: linear-gradient(to bottom, #eee, #aaa); + color: #444; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.6); + -webkit-box-shadow: 0px 1px 1px #000; + box-shadow: 0px 1px 1px #000; +} + +div.group:hover { + color: #fff; + text-shadow: 0 1px 0 rgba(0, 0, 0, 0.6); + background-color: #cf2828; + background: -webkit-gradient(linear, left top, left bottom, from(#cf2828), to(#981a1a)); + background: -webkit-linear-gradient(top, #cf2828, #981a1a); + background: linear-gradient(to bottom, #cf2828, #981a1a); + -webkit-transition: color 0.1s linear; + transition: color 0.1s linear; +} + +.contactBox { + cursor: pointer; + width: 100%; + margin: 5px; + border-radius: 5px; + background: #EDEDED; +} + +.contactBox img, .muc_contact img { + width: 32px; + height: 32px; + border-radius: 5px; + margin: 5px 5px 0px 10px; +} + +.contactBox .widgetHeader_buttonGroup { + float: left; +} + +.contactBox .widgetHeader_buttonGroup img { + width: 32px; + height: 32px; + border-radius: 5px; + border: 1px solid #ededed; + padding: 0px 0px 0px 0px; + background: -webkit-gradient(linear, left top, left bottom, from(#eee), to(#aaa)); + background: -webkit-linear-gradient(top, #eee, #aaa); + background: linear-gradient(to bottom, #eee, #aaa); +} + +.contactBox .widgetHeader_buttonGroup img:hover { + border: 1px solid #cf2828; +} + +.contactBox table { + width: 100%; +} + +.contactLabel { + font-size: 1em; + margin-top: 3px; + padding: 3px 10px 3px 10px; +} + +.contact-menu-selected { + font-size: 1em; + margin-top: 3px; + padding: 3px 10px 3px 10px; + border-radius: 5px; + background-color: rgb(175, 175, 175); +} + +.gwt-ScrollPanel { + padding-right: 15px; /* avoid systematic horizontal scroll when only the vertical one is needed */ +} + +.xmlui-JidsListWidget { + padding-right: 20px; /* avoid systematic horizontal scroll when only the vertical one is needed */ + height: 300px; +} + +/* Contacts in MUC */ + +.muc_contact { + border-radius: 5px; + background: #EDEDED; + margin: 2px; + width: 100%; +} + +/* START - contact presence status */ +.contactLabel-connected { + color: #3c7e0c; + font-weight: bold; +} +.contactLabel-unavailable { +} +.contactLabel-chat { + color: #3c7e0c; + font-weight: bold; +} +.contactLabel-away { + color: brown; + font-weight: bold; +} +.contactLabel-dnd { + color: red; + font-weight: bold; +} +.contactLabel-xa { + color: red; + font-weight: bold; +} +/* END - contact presence status */ + +.selected { + color: #fff; + background-color: #cf2828; + background: -webkit-gradient(linear, left top, left bottom, from(#cf2828), to(#981a1a)); + background: -webkit-linear-gradient(top, #cf2828, #981a1a); + background: linear-gradient(to bottom, #cf2828, #981a1a); + border-radius: 1em 1em 1em 1em; + -webkit-transition: color 0.2s linear; + transition: color 0.2s linear; +} + +.messageBox { + width: 100%; + padding: 5px; + border: 1px solid #bbb; + color: #444; + background: #fff url('media/libervia/unibox_2.png') right bottom no-repeat; + -webkit-box-shadow:inset 0 0 10px #ddd; + box-shadow:inset 0 0 10px #ddd; + border-radius: 0px 0px 10px 10px; + height: 28px; + margin: 0px; +} + +.presenceStatusPanel { + margin: auto; + text-align: center; + padding: 5px 0px; + text-shadow: 0 -1px 1px rgba(255,255,255,0.25); + font-size: 1.2em; + background-color: #eee; + font-style: italic; + font-weight: bold; + color: #666; + cursor: pointer; +} + +.presence-button { + font-size: x-large; + padding-right: 5px; + cursor: pointer; +} + +/* RegisterBox */ + +.registerPanel_main button { + margin: 0; + padding: 0; + border: 0; +} + +.registerPanel_main div, .registerPanel_main button { + color: #fff; + text-decoration: none; +} + +.registerPanel_main{ + height: 100%; + border: 5px solid #222; + -webkit-box-shadow: 0px 1px 4px #000; + box-shadow: 0px 1px 4px #000; +} + +.registerPanel_right_side { + background: #111 url('media/libervia/register_right.png'); + height: 100%; + width: 100%; +} + +.registerPanel_right_side .gwt-StackPanelItem { + margin: 15px; + height: auto; + text-align: center; + cursor: pointer; + color: #fff; + display: block; + text-shadow: 1px 1px 0px rgba(255, 255, 255, 0.2); +} + +.registerPanel_right_side .gwt-StackPanelItem-selected { + display: none; +} + +.registerPanel_content { + margin: auto 50px; +} + +.registerPanel_content div { + font-size: 1em; + margin-left: 10px; + margin-top: 15px; + font-weight: bold; + color: #aaa; +} + +.registerPanel_content input { + height: 25px; + line-height: 25px; + width: 200px; + text-indent: 11px; + background: #000; + color: #aaa; + border: 1px solid #222; + border-radius: 15px 15px 15px 15px; +} + +.registerPanel_content input:focus { + border: 1px solid #444; +} + + +.registerPanel_content .button, .registerPanel_content .button:visited { + background: #222 url('media/libervia/gradient.png') repeat-x; + display: block; + text-decoration: none; + border-radius: 6px 6px 6px 6px; + border-bottom: 1px solid rgba(0,0,0,0.25); + cursor: pointer; + margin: 30px auto; +} + +/* Fix for Opera */ +.button, .button:visited { + border-radius: 6px 6px 6px 6px !important; +} + +.registerPanel_content .button:hover { background-color: #111; color: #fff; } +.registerPanel_content .button:active { top: 1px; } +.registerPanel_content .button, .registerPanel_content .button:visited { font-size: 1em; font-weight: bold; line-height: 1; text-shadow: 0 -1px 1px rgba(0,0,0,0.25); padding: 7px 10px 8px; } +.registerPanel_content .red.button, .registerPanel_content .red.button:visited { background-color: #000; } +.registerPanel_content .red.button:hover { background-color: #bc0000; } + +/* Widgets */ + +.widgetsPanel td { + vertical-align: top; +} + +.widgetsPanel > div > table { + border-collapse: separate !important; + border-spacing: 7px; +} + +.widgetHeader { + margin: auto; + height: 25px; + border-radius: 10px 10px 0 0; + background-color: #222; + background: -webkit-gradient(linear, left top, left bottom, from(#444), to(#222)); + background: -webkit-linear-gradient(top, #444, #222); + background: linear-gradient(to bottom, #444, #222); +} + +.widgetHeader_title { + color: #fff; + font-weight: bold; + text-align: left; + text-indent: 15px; + margin-top: 4px; +} + +.widgetHeader_info { + position: absolute; + right: 90px; # FIXME: temporary dirty setting to fit a header menu with 3 icon buttons + color: white; + background-color: white; + border-radius: 5px; + padding: 0px 4px; + top: 2px !important; +} + +.widgetHeader_info img { + padding: 2px; + height: 16px; +} + +.widgetHeader_buttonsWrapper { + position: absolute; + top: 0; + height: 100%; + width: 100%; +} + +.widgetHeader_buttonGroup { + float: right; +} + +.widgetHeader_buttonGroup img { + background-color: transparent; + width: 25px; + height: 20px; + padding: 2px 0px 3px 0px; + border-left: 1px solid #666; + border-top: 0; + border-radius: 0 0 0 0; + background: -webkit-gradient(linear, left top, left bottom, from(#555), to(#333)); + background: -webkit-linear-gradient(top, #555, #333); + background: linear-gradient(to bottom, #555, #333); + cursor: pointer; +} + +.widgetHeader_buttonGroup img:hover { + background-color: #cf2828; + background: -webkit-gradient(linear, left top, left bottom, from(#cf2828), to(#981a1a)); + background: -webkit-linear-gradient(top, #cf2828, #981a1a); + background: linear-gradient(to bottom, #cf2828, #981a1a); +} + +.widgetBody { + border-radius: 0 0 10px 10px; + background-color: #fff; + min-width: 200px; + min-height: 150px; + -webkit-box-shadow:inset 0px 0 1px #444; + box-shadow:inset 0px 0 1px #444; +} + +/* BorderWidgets */ + +.borderWidgetOnDrag { + background-color: lightgray; + border: 1px dashed #000; + border-radius: 1em; +} + +.bottomBorderWidget { + height: 10px !important; +} + +.leftBorderWidget, .rightBorderWidget { + width: 10px !important; +} + +.leftBorderWidget { + float: right; +} + +.rightBorderWidget { + float: left; +} + +/* Microblog */ + +.microblogPanel { + width: 100%; +} + +.microblogPanel_footer { + cursor: pointer; + text-align: center; + background-color: #ededed; + border-radius: 5px; + width: 85%; + margin: auto; + margin-top: 5px; + margin-bottom: 5px; +} + +.microblogPanel_footer a { + color: blue; +} + +.microblogNewButton { + width: 100%; + height: 35px; +} + +.subPanel { +} + +.subpanel .mb_entry { + padding-left: 65px; +} + +.mb_entry { + min-height: 64px; +} + +.mb_entry_header +{ + width: 100%; +} + +.mb_entry_header_info { + cursor: pointer; + padding: 0px 5px 0px 5px; +} + +.selected_widget .selected_entry .mb_entry_header_info +{ + background: #cf2828; + border-radius: 5px 5px 0px 0px; +} + +.mb_entry_comments { + float: right; + padding-right: 5px; +} + +.mb_entry_comments a { + color: blue; + cursor: pointer; +} + +.mb_entry_author { + font-weight: bold; +} + +.mb_entry_avatar { + float: left; +} + +.mb_entry_avatar img { + width: 48px; + height: 48px; + padding: 8px; + border-radius: 13px; /* padding value + 5px */ +} + +.mb_entry_dialog { + float: left; + min-height: 54px; + padding: 5px 20px 5px 20px; + border-collapse: separate; /* for the bubble queue since the entry dialog is now a HorizontalPanel */ +} + +.bubble { + position: relative; + padding: 15px; + margin: 2px; + border-radius:10px; + background: #EDEDED; + border-color: #C1C1C1; + border-width: 1px; + border-style: solid; + display: block; + border-collapse: separate; + min-height: 15px; /* for the bubble queue to be aligned when the bubble is empty */ +} + +.bubble:after { + background: transparent url('media/libervia/bubble_after.png') top right no-repeat; + border: none; + content: ""; + position: absolute; + bottom: auto; + left: -20px; + top: 16px; + display: block; + height: 20px; + width: 20px; +} + +.bubble textarea{ + width: 100%; + min-width: 350px; +} + +.mb_entry_timestamp { + font-style: italic; +} + +.mb_entry_actions { + float: right; + margin: 5px; + cursor: pointer; + font-size: large; +} + +.mb_entry_action_larger { + font-size: x-large; +} + +.mb_entry_toggle_syntax { + cursor: pointer; + float: right; + display: block; + position: relative; + top: -20px; + left: -20px; +} + +.mb_entry_publish_button { + cursor: pointer; + float: left; + display: block; + position: relative; + top: -20px; + left: 20px; +} + +/* START TAGS: styles are adapted from Dotclear */ +.mblog_tags { + background: #fbfbfb none repeat scroll 0% 0%; + padding: 5px; + margin: 8px 0px 5px 0px; + overflow: hidden; + border-radius: 5px; +} + +.mblog_tags li { + display: inline; + font-size: small; +} + +.mblog_tags li a { + float: left; + padding: 2px 8px 2px 18px; + white-space: nowrap; + color: #005D99; + text-decoration: none; + background: transparent url("../themes/default/images/flaticon/tag67.png") no-repeat scroll 0px 0px; +} +/* END TAGS */ + + +/* Chat & MUC Room */ + +.chatPanel { + height: 100%; + width: 100%; +} + +.chatPanel_body { + height: 100%; + width: 100%; +} + +.chatContent { + overflow: auto; + padding: 5px 15px 5px 15px; +} + +.chatText { + margin-top: 7px; +} + +.chatTextMe { + margin-top: 7px; + font-style: italic; +} + +.chatTextInfo { + margin-top: 7px; + font-weight: bold; + font-style: italic; +} + +.chatTextInfo-link { + font-weight: bold; + font-style: italic; + cursor: pointer; + display: inline; +} + +.chatArea { + height:100%; + width:100%; +} + +.chat_text_timestamp { + font-style: italic; + margin-right: -4px; + padding: 1px 3px 1px 3px; + border-radius: 15px 0 0 15px; + background-color: #eee; + color: #888; + border: 1px solid #ddd; + border-right: none; +} + +.chat_text_nick { + font-weight: bold; + padding: 1px 3px 1px 3px; + border-radius: 0 15px 15px 0; + background-color: #eee; + color: #b01e1e; + border: 1px solid #ddd; + border-left: none; +} + +.chat_text_msg { + white-space: pre-wrap; +} + +.chat_text_mymess { + color: #006600; +} + +.occupantsPanelCell { + border-right: 2px dotted #ddd; + padding-left: 5px; + height: 100%; +} + +/* Games */ + +.cardPanel { + background: #02FE03; + margin: 0 auto; +} + +.cardGamePlayerNick { + font-weight: bold; +} + +/* Radiocol */ + +.radiocolPanel { + +} + +.radiocol_metadata_lbl { + font-weight: bold; + padding-right: 5px; +} + +.radiocol_next_song { + margin-right: 5px; + font-style:italic; +} + +.radiocol_status { + margin-left: 10px; + margin-right: 10px; + font-weight: bold; + color: black; +} + +.radiocol_upload_status_ok { + margin-left: 10px; + margin-right: 10px; + font-weight: bold; + color: #28F215; +} + +.radiocol_upload_status_ko { + margin-left: 10px; + margin-right: 10px; + font-weight: bold; + color: #B80000; +} + +/* Drag and drop */ + +.dragover { + background: #cf2828 !important; + border-radius: 1em 1em 1em 1em !important; +} + +.dragover .widgetHeader, .dragover .widgetBody, .dragover .widgetBody span, .dragover .widgetHeader img { + background: #cf2828 !important; +} + +.dragover.widgetHeader { + border-radius: 1em 1em 0 0 !important; +} + +.dragover.widgetBody { + border-radius: 0 0 1em 1em !important; +} + +/* Warning message */ + +.warningPopup { + font-size: 1em; + width: 100%; + height: 26px; + text-align: center; + padding: 5px 0; + border-bottom: 1px solid #444; +} + +.warningTarget { + font-weight: bold; + +} + +.targetPublic { + background-color: red; +} + +.targetGroup { + background-color: #00FFFB; +} + +.targetOne2One { + background-color: #66FF00; +} + +.targetStatus { + background-color: #fff; +} + +.notifInfo { + background-color: #66FF00; +} + +.notifWarning { + background-color: #DB1616; +} + +/* Tab panel */ + +.gwt-TabPanel { +} + +.gwt-TabPanelBottom { + height: 100%; +} + +.gwt-TabBar { + font-weight: bold; + text-decoration: none; + border-bottom: 3px solid #a01c1c; +} + +.gwt-TabBar .gwt-TabBarFirst { + height: 100%; +} + +.gwt-TabBar .gwt-TabBarRest { +} + +.mainPanel .gwt-TabBar { + z-index: 10; +} + +.mainPanel .gwt-TabBar-oneTab { + position: fixed; + left: 0px; + bottom: 0px; + border: none; +} + +.mainPanel .gwt-TabBar-oneTab .gwt-TabBarItem-wrapper { + display: none; +} + +.mainPanel .gwt-TabBar-oneTab .gwt-TabBarItem-wrapper:nth-child(3) { + display: block; +} + +.liberviaTabPanel { + width: 100%; + height: 100%; +} + +.liberviaTabPanel .gwt-TabBarItem div { + color: #fff; +} + +.liberviaTabPanel .gwt-TabBarItem { + color: #444 !important; + background-color: #222; + background: -webkit-gradient(linear, left top, left bottom, from(#444), to(#222)); + background: -webkit-linear-gradient(top, #444, #222); + background: linear-gradient(to bottom, #444, #222); + -webkit-box-shadow: 0px 1px 4px #000; + box-shadow: 0px 1px 4px #000; + padding: 4px 15px 4px 15px; + border-radius: 1em 1em 0 0; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.2); + cursor: pointer; + margin-right: 5px; +} + +.liberviaTabPanel .gwt-TabBarItem-selected { + color: #fff; + background-color: #cf2828; + background: -webkit-gradient(linear, left top, left bottom, from(#cf2828), to(#981a1a)); + background: -webkit-linear-gradient(top, #cf2828, #981a1a); + background: linear-gradient(to bottom, #cf2828, #981a1a); + -webkit-box-shadow: 0px 1px 4px #000; + box-shadow: 0px 1px 4px #000; + padding: 4px 15px 4px 15px; + border-radius: 1em 1em 0 0; + text-shadow: 0 1px 0 rgba(0, 0, 0, 0.2); + cursor: default; +} + +.liberviaTabPanel div.gwt-TabBarItem:hover { + color: #fff; + background-color: #cf2828; + background: -webkit-gradient(linear, left top, left bottom, from(#cf2828), to(#981a1a)); + background: -webkit-linear-gradient(top, #cf2828, #981a1a); + background: linear-gradient(to bottom, #cf2828, #981a1a); + -webkit-box-shadow: 0px 1px 4px #000; + box-shadow: 0px 1px 4px #000; + padding: 4px 15px 4px 15px; + border-radius: 1em 1em 0 0; + text-shadow: 0 1px 0 rgba(0, 0, 0, 0.2); +} + +.globalLeftArea { + margin-top: 9px; +} + + +/* Misc */ + +.selected_widget .widgetHeader { + background-color: #cf2828; + background: -webkit-gradient(linear, left top, left bottom, from(#cf2828), to(#981a1a)); + background: -webkit-linear-gradient(top, #cf2828, #981a1a); + background: linear-gradient(to bottom, #cf2828, #981a1a); +} + +.infoFrame { + position: relative; + width: 100%; + height: 100%; +} + +.marginAuto { + margin: auto; +} + +.maxWidthLimit { + max-width: 500px; +} + +.transparent { + opacity: 0; +} + +/* URLs */ + +a.url { + color: blue; + text-decoration: none +} + +a:hover.url { + text-decoration: underline +} + +/* Rich Text/Message Editor */ + +.richTextEditor { +} + +.richTextEditor tbody { + width: 100%; + display: table; +} + +.richTextTitle { + margin-bottom: 5px; +} + +.richTextTitle textarea { + height: 22px; + width: 99%; + margin: auto; + padding: 4px; + display: block; + border: 0px; + border-radius: 5px; +} + +.richTextToolbar { + white-space: nowrap; + width: 100%; +} + +.richTextArea { + width: 100%; + height: 250px; +} + +.richTextWysiwyg { + min-height: 50px; + background-color: white; + border: 1px solid #a0a0a0; + border-radius: 5px; + display: block; + font-size: larger; + white-space: pre; +} + +.richTextSyntaxLabel { + text-align: right; + margin: 14px 0px 0px 14px; + font-size: 12px; +} + +.richTextToolButton { + cursor: pointer; + width:26px; + height:26px; + vertical-align: middle; + margin: 2px 1px; + border-radius: 5px 5px 5px 5px; + -webkit-box-shadow: 0px 1px 4px #000; + box-shadow: 0px 1px 4px #000; + border: none; + -webkit-transition: color 0.2s linear; + transition: color 0.2s linear; +} + +.richTextIcon { + width:16px; + height:16px; + vertical-align: middle; +} + +/* List panel */ + +.itemButtonCell { + width:55px; +} + +.itemKeyMenu { +} + +.itemKey { + cursor: pointer; + border-radius: 5px; + width: 50px; +} + +.listItem { +} + +.listItem-box { + cursor: pointer; + width: auto; + border: 1px solid #87B3FF; + border-radius: 5px 5px 5px 5px; + -webkit-box-shadow: inset 0px 1px 0px rgba(135, 179, 255, 0.6); + box-shadow: inset 0px 1px 2px rgba(135, 179, 255, 0.6); + padding: 2px 1px; +} + +.listItem-box-invalid { + border: 1px solid rgb(255, 0, 0); + -webkit-box-shadow: inset 0px 1px 0px rgba(255, 0, 0, 0.6); + box-shadow: inset 0px 1px 0px rgba(255, 0, 0, 0.6); +} + +.listItem-button { + cursor: pointer; + margin: 0px; + padding: 0px; + border: none; + background: transparent; +} + +.listItem-button span { + color: red; +} + +/* Popup (context) menu */ + +.popupMenuItem { + cursor: pointer; + border-radius: 5px; + width: 100%; +} + +/* Contact group manager */ + +.contactGroupEditor { + width: 680px !important; +} + +.contactGroupManager { + width: 400px !important; + height: 300px !important; + margin: 20px 0px; +} + +.contactGroupRoster { + width: 280px !important; + height: 300px !important; + margin: 20px 0px; +} + +.addContactGroupPanel { + +} + +.listPanel { + vertical-align:top; + padding: 10px 0px; +} + +.listPanel.dragover { + border-radius: 5px !important; + background: none repeat scroll 0% 0% rgb(135, 179, 255) !important; + border: 1px dashed rgb(35,79,255) !important; +} + +.toggleAssignedContacts { + white-space: nowrap; +} + +.listManager-button-cell { + vertical-align: top; + padding: 10px 0px; + width: 55px; + white-space: top; +} + +.listManager-button-cell .group { + border: 0px; + margin: 0px 5px; +} + +.tagsPanel-main { + margin-bottom: 10px; +} + +.tagsPanel-tags { + padding: 0px; + display: flex; + flex-wrap: wrap; +} + +/* Room and contacts chooser */ + +.room-contact-chooser { + width:380px; +} + +/* StackPanel */ + +.gwt-StackPanel { +} + +.gwt-StackPanel .gwt-StackPanelItem { + background-color: #222; + background: -webkit-gradient(linear, left top, left bottom, from(#888888), to(#666666)); + background: -webkit-linear-gradient(top, #888888, #666666); + background: linear-gradient(to bottom, #888888, #666666); + text-decoration: none; + font-weight: bold; + height: 100%; + color: #e7e5e5; + padding: 3px 15px; + border-radius: 1em 1em 1em 1em; + text-shadow: 0 1px 1px rgba(0, 0, 0, 0.4); + -webkit-transition: color 0.2s linear; + transition: color 0.2s linear; +} + +.gwt-StackPanel .gwt-StackPanelItem:hover { + background-color: #eee; + background: -webkit-gradient(linear, left top, left bottom, from(#eee), to(#aaa)); + background: -webkit-linear-gradient(top, #eee, #aaa); + background: linear-gradient(to bottom, #eee, #aaa); + color: #444; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.6); + cursor: pointer; +} + +.gwt-StackPanel .gwt-StackPanelItem-selected { + background-color: #eee; + background: -webkit-gradient(linear, left top, left bottom, from(#eee), to(#aaa)); + background: -webkit-linear-gradient(top, #eee, #aaa); + background: linear-gradient(to bottom, #eee,#aaa); + color: #444; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.6); + cursor: pointer; +} + +/* Caption Panel */ + +.gwt-CaptionPanel { + overflow: auto; + background-color: #fff; + border-radius: 5px 5px 5px 5px; + padding: 3px 5px 3px 5px; + margin: 10px 5px 10px 5px; + color: #444; + font-size: 1em; + border: solid 1px gray; +} + +/* Radio buttons */ + +.gwt-RadioButton { + white-space: nowrap; +} + +[contenteditable="true"] { +} + +/* XMLUI styles */ + +.AdvancedListSelectable tr{ + cursor: pointer; +} + +.AdvancedListSelectable tr:hover{ + background: none repeat scroll 0 0 #EE0000; +} + +.line hr { + +} + +.dot hr { + height: 0px; + border-top: 1px dotted; + border-bottom: 0px; +} + +.dash hr { + height: 0px; + border-top: 1px dashed; + border-bottom: 0px; +} + +.plain hr { + height: 10px; + color: black; + background-color: black; +} + +.blank hr { + border: 0px; +} + + +/* Some CSS to style the quote XHTML generated by Movim */ + +.mb_entry_dialog .bubble div.quote { + display: block; + border-radius: 2px; + border: 1px solid rgba(0, 0, 0, 0.12); + padding: 2rem; + box-sizing: border-box; +} + +.mb_entry_dialog .bubble div.quote:before, +.mb_entry_dialog .bubble div.quote:after { + content: ''; + display: none; +} + +.mb_entry_dialog .bubble div.quote ul { + display: flex; + flex-flow: row wrap; +} + +.mb_entry_dialog .bubble div.quote li { + flex: 1 25%; + list-style-type: none; + padding-left: 0; +} + +.mb_entry_dialog .bubble div.quote ul li > * { + margin-right: 1rem; +} + +.mb_entry_dialog .bubble div.quote li:first-child { + flex: 1 75%; +} + +@media screen and (max-width: 1024px) { + .mb_entry_dialog .bubble div.quote li { + flex: 1 100%; + } +} + +.mb_entry_dialog .bubble div.quote li img { + max-height: 10rem; + max-width: 100%; + float: right; +} + +.parameters { +} + +.parameters .xmlui-JidsListWidget { + height: auto; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/browser/public/libervia.html Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,33 @@ +<!-- +Libervia: a Salut à Toi frontend +Copyright (C) 2011 Jérôme Poisson (goffi@goffi.org) + +This program 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. + +This program 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 this program. If not, see <http://www.gnu.org/licenses/>. +--> + +<html> +<head profile="http://www.w3.org/2005/10/profile"> +<meta http-equiv="Content-Type" content="text/html; charset=utf-8"> +<meta name="pygwt:module" content="libervia_main"> +<link rel='stylesheet' href='libervia.css'> +<link rel="icon" type="image/png" href="sat_logo_16.png"> + +<title>Libervia</title> +</head> +<body bgcolor="white"> +<script language="javascript" src="bootstrap.js"></script> +<script language="javascript" src="favico.min.js"></script> +<iframe id='__pygwt_historyFrame' style='display:none;width:0;height:0;border:0'></iframe> +</body> +</html>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/browser/public/robots.txt Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,10 @@ +User-agent: * +Allow: /blog/ +Allow: /u/ +Allow: /b/ +Disallow: /libervia.html +Disallow: /mr +Disallow: /login + +User-agent: Mediapartners-* +Disallow: /
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/browser/sat_browser/base_menu.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,183 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2011-2018 Jérôme Poisson <goffi@goffi.org> + +# This program 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. + +# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + + +"""Base classes for building a menu. + +These classes have been moved here from menu.py because they are also used +by base_widget.py, and the import sequence caused a JS runtime error.""" + + +from sat.core.log import getLogger +log = getLogger(__name__) + +from pyjamas.ui.MenuBar import MenuBar +from pyjamas.ui.MenuItem import MenuItem +from pyjamas import Window +from sat_frontends.quick_frontend import quick_menus +from sat_browser import html_tools + + +unicode = str # FIXME: pyjamas workaround + + +class MenuCmd(object): + """Return an object with an "execute" method that can be set to a menu item callback""" + + def __init__(self, menu_item, caller=None): + """ + @param menu_item(quick_menu.MenuItem): instance of a callbable MenuItem + @param caller: menu caller + """ + self.item = menu_item + self._caller = caller + + def execute(self): + self.item.call(self._caller) + + +class SimpleCmd(object): + """Return an object with an "executre" method that launch a callback""" + + def __init__(self, callback): + """ + @param callback: method to call when menu is selected + """ + self.callback = callback + + def execute(self): + self.callback() + + +class GenericMenuBar(MenuBar): + """A menu bar with sub-categories and items""" + + def __init__(self, host, vertical=False, styles=None, flat_level=0, **kwargs): + """ + @param host (SatWebFrontend): host instance + @param vertical (bool): True to display the popup menu vertically + @param styles (dict): specific styles to be applied: + - key: a value in ('moved_popup', 'menu_bar') + - value: a CSS class name + @param flat_level (int): sub-menus until that level see their items + displayed in the parent menu bar instead of in a callback popup. + """ + MenuBar.__init__(self, vertical, **kwargs) + self.host = host + self.styles = {} + if styles: + self.styles.update(styles) + try: + self.setStyleName(self.styles['menu_bar']) + except KeyError: + pass + self.menus_container = None + self.flat_level = flat_level + + def update(self, type_, caller=None): + """Method to call when menus have changed + + @param type_: menu type like in sat.core.sat_main.importMenu + @param caller: instance linked to the menus + """ + self.menus_container = self.host.menus.getMainContainer(type_) + self._caller=caller + self.createMenus() + + @classmethod + def getCategoryHTML(cls, category): + """Build the html to be used for displaying a category item. + + Inheriting classes may overwrite this method. + @param category (quick_menus.MenuCategory): category to add + @return unicode: HTML to display + """ + return html_tools.html_sanitize(category.name) + + def _buildMenus(self, container, flat_level, caller=None): + """Recursively build menus of the container + + @param container: a quick_menus.MenuContainer instance + @param caller: instance linked to the menus + """ + for child in container.getActiveMenus(): + if isinstance(child, quick_menus.MenuContainer): + item = self.addCategory(child, flat=bool(flat_level)) + submenu = item.getSubMenu() + if submenu is None: + submenu = self + submenu._buildMenus(child, flat_level-1 if flat_level else 0, caller) + elif isinstance(child, quick_menus.MenuSeparator): + item = MenuItem(text='', asHTML=None, StyleName="menuSeparator") + self.addItem(item) + elif isinstance(child, quick_menus.MenuItem): + self.addItem(child.name, False, MenuCmd(child, caller) if child.CALLABLE else None) + else: + log.error(u"Unknown child type: {}".format(child)) + + def createMenus(self): + self.clearItems() + if self.menus_container is None: + log.debug("Menu is empty") + return + self._buildMenus(self.menus_container, self.flat_level, self._caller) + + def doItemAction(self, item, fireCommand): + """Overwrites the default behavior for the popup menu to fit in the screen""" + MenuBar.doItemAction(self, item, fireCommand) + if not self.popup: + return + if self.vertical: + # move the popup if it would go over the screen's viewport + max_left = Window.getClientWidth() - self.getOffsetWidth() + 1 - self.popup.getOffsetWidth() + new_left = self.getAbsoluteLeft() - self.popup.getOffsetWidth() + 1 + top = item.getAbsoluteTop() + else: + # move the popup if it would go over the menu bar right extremity + max_left = self.getAbsoluteLeft() + self.getOffsetWidth() - self.popup.getOffsetWidth() + new_left = max_left + top = self.getAbsoluteTop() + self.getOffsetHeight() - 1 + if item.getAbsoluteLeft() > max_left: + self.popup.setPopupPosition(new_left, top) + # eventually smooth the popup edges to fit the menu own style + try: + self.popup.addStyleName(self.styles['moved_popup']) + except KeyError: + pass + + def addCategory(self, category, menu_bar=None, flat=False): + """Add a new category. + + @param category (quick_menus.MenuCategory): category to add + @param menu_bar (GenericMenuBar): instance to popup as the category sub-menu. + """ + html = self.getCategoryHTML(category) + + if menu_bar is not None: + assert not flat # can't have a menu_bar and be flat at the same time + sub_menu = menu_bar + elif not flat: + sub_menu = GenericMenuBar(self.host, vertical=True) + else: + sub_menu = None + + item = self.addItem(html, True, sub_menu) + if flat: + item.setStyleName("menuFlattenedCategory") + return item
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/browser/sat_browser/base_panel.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,236 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2011-2018 Jérôme Poisson <goffi@goffi.org> + +# This program 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. + +# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + +from sat.core.log import getLogger +log = getLogger(__name__) +from sat.core.i18n import _ + +from pyjamas.ui.VerticalPanel import VerticalPanel +from pyjamas.ui.HorizontalPanel import HorizontalPanel +from pyjamas.ui.ScrollPanel import ScrollPanel +from pyjamas.ui.Button import Button +from pyjamas.ui.SimplePanel import SimplePanel +from pyjamas.ui.PopupPanel import PopupPanel +from pyjamas.ui.StackPanel import StackPanel +from pyjamas.ui.TextArea import TextArea +from pyjamas.ui.Event import BUTTON_LEFT, BUTTON_MIDDLE, BUTTON_RIGHT +from pyjamas import DOM + + +### Menus ### + + +class PopupMenuPanel(PopupPanel): + """Popup menu (contextual menu) with common callbacks for all the items. + + This implementation of a popup menu allow you to assign two special methods which + are common to all the items, in order to hide certain items and define their callbacks. + callbacks. The menu can be bound to any button of the mouse (left, middle, right). + """ + + def __init__(self, entries, hide=None, callback=None, vertical=True, style=None, **kwargs): + """ + @param entries (dict{unicode: dict{unicode: unicode}: + - menu item keys + - values: dict{unicode: unicode}: + - item data lile "title", "desc"... + - value + @param hide (callable): function of signature Widget, unicode: bool + which takes the sender and the item key, and returns True if that + item has to be hidden from the context menu. + @param callback (callbable): function of signature Widget, unicode: None + which takes the sender and the item key. + @param vertical (bool): set the direction vertical or horizontal + @param item_style (unicode): alternative CSS class for the menu items + @param menu_style (unicode): supplementary CSS class for the sender widget + """ + PopupPanel.__init__(self, autoHide=True, **kwargs) + self.entries = entries + self.hideMenu = hide + self.callback = callback + self.vertical = vertical + self.style = {"selected": None, "menu": "itemKeyMenu", "item": "popupMenuItem"} + if isinstance(style, dict): + self.style.update(style) + self.senders = {} + + def showMenu(self, sender): + """Popup the menu on the screen, where it fits to the sender's position. + + @param sender (Widget): the widget that has been clicked + """ + menu = VerticalPanel() if self.vertical is True else HorizontalPanel() + menu.setStyleName(self.style["menu"]) + + def button_cb(item): + # XXX: you can not put that method in the loop and rely on key + if self.callback is not None: + self.callback(sender=sender, key=item.key) + self.hide(autoClosed=True) + + for key, entry in self.entries.iteritems(): + if self.hideMenu is not None and self.hideMenu(sender=sender, key=key) is True: + continue + title = entry.get("title", key) + item = Button(title, button_cb, StyleName=self.style["item"]) + item.key = key # XXX: copy the key because we loop on it and it will change + item.setTitle(entry.get("desc", title)) + menu.add(item) + + if menu.getWidgetCount() == 0: + return # no item to display means no menu at all + + self.add(menu) + + if self.vertical is True: + x = sender.getAbsoluteLeft() + sender.getOffsetWidth() + y = sender.getAbsoluteTop() + else: + x = sender.getAbsoluteLeft() + y = sender.getAbsoluteTop() + sender.getOffsetHeight() + + self.setPopupPosition(x, y) + self.show() + + if self.style["selected"]: + sender.addStyleDependentName(self.style["selected"]) + + def onHide(popup): + if self.style["selected"]: + sender.removeStyleDependentName(self.style["selected"]) + return PopupPanel.onHideImpl(self, popup) + + self.onHideImpl = onHide + + def registerClickSender(self, sender, button=BUTTON_LEFT): + """Bind the menu to the specified sender. + + @param sender (Widget): bind the menu to this widget + @param (int): BUTTON_LEFT, BUTTON_MIDDLE or BUTTON_RIGHT + """ + self.senders.setdefault(sender, []) + self.senders[sender].append(button) + + if button == BUTTON_RIGHT: + # WARNING: to disable the context menu is a bit tricky... + # The following seems to work on Firefox 24.0, but: + # TODO: find a cleaner way to disable the context menu + sender.getElement().setAttribute("oncontextmenu", "return false") + + def onBrowserEvent(event): + button = DOM.eventGetButton(event) + if DOM.eventGetType(event) == "mousedown" and button in self.senders[sender]: + self.showMenu(sender) + return sender.__class__.onBrowserEvent(sender, event) + + sender.onBrowserEvent = onBrowserEvent + + def registerMiddleClickSender(self, sender): + self.registerClickSender(sender, BUTTON_MIDDLE) + + def registerRightClickSender(self, sender): + self.registerClickSender(sender, BUTTON_RIGHT) + + +### Generic panels ### + + +class ToggleStackPanel(StackPanel): + """This is a pyjamas.ui.StackPanel with modified behavior. All sub-panels ca be + visible at the same time, clicking a sub-panel header will not display it and hide + the others but only toggle its own visibility. The argument 'visibleStack' is ignored. + Note that the argument 'visible' has been added to listener's 'onStackChanged' method. + """ + + def __init__(self, **kwargs): + StackPanel.__init__(self, **kwargs) + + def onBrowserEvent(self, event): + if DOM.eventGetType(event) == "click": + index = self.getDividerIndex(DOM.eventGetTarget(event)) + if index != -1: + self.toggleStack(index) + + def add(self, widget, stackText="", asHTML=False, visible=False): + StackPanel.add(self, widget, stackText, asHTML) + self.setStackVisible(self.getWidgetCount() - 1, visible) + + def toggleStack(self, index): + if index >= self.getWidgetCount(): + return + visible = not self.getWidget(index).getVisible() + self.setStackVisible(index, visible) + for listener in self.stackListeners: + listener.onStackChanged(self, index, visible) + + +class TitlePanel(ToggleStackPanel): + """A toggle panel to set the message title""" + + TITLE = _("Title") + + def __init__(self, text=None): + ToggleStackPanel.__init__(self, Width="100%") + self.text_area = TextArea() + self.add(self.text_area, self.TITLE) + self.addStackChangeListener(self) + if text: + self.setText(text) + + def onStackChanged(self, sender, index, visible=None): + if visible is None: + visible = sender.getWidget(index).getVisible() + text = self.getText() + suffix = "" if (visible or not text) else (": %s" % text) + sender.setStackText(index, self.TITLE + suffix) + + def getText(self): + return self.text_area.getText() + + def setText(self, text): + self.text_area.setText(text) + + +class ScrollPanelWrapper(SimplePanel): + """Scroll Panel like component, wich use the full available space + to work around percent size issue, it use some of the ideas found + here: http://code.google.com/p/google-web-toolkit/issues/detail?id=316 + specially in code given at comment #46, thanks to Stefan Bachert""" + + def __init__(self, *args, **kwargs): + SimplePanel.__init__(self) + self.spanel = ScrollPanel(*args, **kwargs) + SimplePanel.setWidget(self, self.spanel) + DOM.setStyleAttribute(self.getElement(), "position", "relative") + DOM.setStyleAttribute(self.getElement(), "top", "0px") + DOM.setStyleAttribute(self.getElement(), "left", "0px") + DOM.setStyleAttribute(self.getElement(), "width", "100%") + DOM.setStyleAttribute(self.getElement(), "height", "100%") + DOM.setStyleAttribute(self.spanel.getElement(), "position", "absolute") + DOM.setStyleAttribute(self.spanel.getElement(), "width", "100%") + DOM.setStyleAttribute(self.spanel.getElement(), "height", "100%") + + def setWidget(self, widget): + self.spanel.setWidget(widget) + + def setScrollPosition(self, position): + self.spanel.setScrollPosition(position) + + def scrollToBottom(self): + self.setScrollPosition(self.spanel.getElement().scrollHeight)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/browser/sat_browser/base_widget.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,71 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2011-2018 Jérôme Poisson <goffi@goffi.org> + +# This program 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. + +# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + +from sat.core.log import getLogger +log = getLogger(__name__) +import base_menu +from sat_frontends.quick_frontend import quick_menus + + +### Exceptions ### + + +class NoLiberviaWidgetException(Exception): + """A Libervia widget was expected""" + pass + + +### Menus ### + + +class WidgetMenuBar(base_menu.GenericMenuBar): + + ITEM_TPL = "<img src='media/icons/misc/%s.png' />" + + def __init__(self, parent, host, vertical=False, styles=None): + """ + + @param parent (Widget): LiberviaWidget, or instance of another class + implementing the method addMenus + @param host (SatWebFrontend) + @param vertical (bool): if True, set the menu vertically + @param styles (dict): optional styles dict + """ + menu_styles = {'menu_bar': 'widgetHeader_buttonGroup'} + if styles: + menu_styles.update(styles) + base_menu.GenericMenuBar.__init__(self, host, vertical=vertical, styles=menu_styles) + + # regroup all the dynamic menu categories in a sub-menu + for menu_context in parent.plugin_menu_context: + main_cont = host.menus.getMainContainer(menu_context) + if len(main_cont)>0: # we don't add the icon if the menu is empty + sub_menu = base_menu.GenericMenuBar(host, vertical=True, flat_level=1) + sub_menu.update(menu_context, parent) + menu_category = quick_menus.MenuCategory("plugins", extra={'icon':'plugins'}) + self.addCategory(menu_category, sub_menu) + + @classmethod + def getCategoryHTML(cls, category): + """Build the html to be used for displaying a category item. + + @param category (quick_menus.MenuCategory): category to add + @return unicode: HTML to display + """ + return cls.ITEM_TPL % category.icon
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/browser/sat_browser/blog.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,560 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2011-2018 Jérôme Poisson <goffi@goffi.org> + +# This program 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. + +# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + +import pyjd # this is dummy in pyjs +from sat.core.log import getLogger +log = getLogger(__name__) +from sat.tools.common import data_format +from sat.core.i18n import _ #, D_ + +from pyjamas.ui.SimplePanel import SimplePanel +from pyjamas.ui.VerticalPanel import VerticalPanel +from pyjamas.ui.ScrollPanel import ScrollPanel +from pyjamas.ui.HorizontalPanel import HorizontalPanel +from pyjamas.ui.Label import Label +from pyjamas.ui.HTML import HTML +from pyjamas.ui.Image import Image +from pyjamas.ui.ClickListener import ClickHandler +from pyjamas.ui.FlowPanel import FlowPanel +from pyjamas.ui import KeyboardListener as keyb +from pyjamas.ui.KeyboardListener import KeyboardHandler +from pyjamas.ui.FocusListener import FocusHandler +from pyjamas.ui.MouseListener import MouseHandler +from pyjamas.Timer import Timer + +from datetime import datetime + +import html_tools +import dialog +import richtext +import editor_widget +import libervia_widget +from constants import Const as C +from sat_frontends.quick_frontend import quick_widgets +from sat_frontends.quick_frontend import quick_blog + +unicode = str # XXX: pyjamas doesn't manage unicode +ENTRY_RICH = (C.ENTRY_MODE_RICH, C.ENTRY_MODE_XHTML) + + +class Entry(quick_blog.Entry, VerticalPanel, ClickHandler, FocusHandler, KeyboardHandler): + """Graphical representation of a quick_blog.Item""" + + def __init__(self, manager, item_data=None, comments_data=None, service=None, node=None): + quick_blog.Entry.__init__(self, manager, item_data, comments_data, service, node) + + VerticalPanel.__init__(self) + + self.panel = FlowPanel() + self.panel.setStyleName('mb_entry') + + self.header = HorizontalPanel(StyleName='mb_entry_header') + self.panel.add(self.header) + + self.entry_actions = VerticalPanel() + self.entry_actions.setStyleName('mb_entry_actions') + self.panel.add(self.entry_actions) + + entry_avatar = SimplePanel() + entry_avatar.setStyleName('mb_entry_avatar') + author_jid = self.author_jid + self.avatar = Image(self.blog.host.getAvatarURL(author_jid) if author_jid is not None else C.DEFAULT_AVATAR_URL) + # TODO: show a warning icon if author is not validated + entry_avatar.add(self.avatar) + self.panel.add(entry_avatar) + + self.entry_dialog = VerticalPanel() + self.entry_dialog.setStyleName('mb_entry_dialog') + self.panel.add(self.entry_dialog) + + self.comments_panel = None + self._current_comment = None + + self.add(self.panel) + ClickHandler.__init__(self) + self.addClickListener(self) + + self.refresh() + self.displayed = False # True when entry is added to parent + if comments_data: + self.addComments(comments_data) + + def refresh(self): + self.comment_label = None + self.update_label = None + self.delete_label = None + self.header.clear() + self.entry_dialog.clear() + self.entry_actions.clear() + self._setHeader() + self._setBubble() + self._setIcons() + + def _setHeader(self): + """Set the entry header.""" + if not self.new: + author = html_tools.html_sanitize(unicode(self.item.author)) + author_jid = html_tools.html_sanitize(unicode(self.item.author_jid)) + if author_jid and not self.item.author_verified: + author_jid += u' <span style="color:red; font-weight: bold;">⚠</span>' + if author: + author += " <%s>" % author_jid + elif author_jid: + author = author_jid + else: + author = _("<unknown author>") + + update_text = u" — ✍ " + "<span class='mb_entry_timestamp'>%s</span>" % datetime.fromtimestamp(self.item.updated) + self.header.add(HTML("""<span class='mb_entry_header_info'> + <span class='mb_entry_author'>%(author)s</span> on + <span class='mb_entry_timestamp'>%(published)s</span>%(updated)s + </span>""" % {'author': author, + 'published': datetime.fromtimestamp(self.item.published) if self.item.published is not None else '', + 'updated': update_text if self.item.published != self.item.updated else '' + })) + if self.item.comments: + self.show_comments_link = HTML('') + self.header.add(self.show_comments_link) + + def _setBubble(self): + """Set the bubble displaying the initial content.""" + content = {'text': self.item.content_xhtml if self.item.content_xhtml else self.item.content or '', + 'title': self.item.title_xhtml if self.item.title_xhtml else self.item.title or ''} + data_format.iter2dict('tag', self.item.tags, content) + + if self.mode == C.ENTRY_MODE_TEXT: + # assume raw text message have no title + self.bubble = editor_widget.LightTextEditor(content, modifiedCb=self._modifiedCb, afterEditCb=self._afterEditCb, options={'no_xhtml': True}) + elif self.mode in ENTRY_RICH: + content['syntax'] = C.SYNTAX_XHTML + if self.new: + options = [] + elif self.item.author_jid == self.blog.host.whoami.bare: + options = ['update_msg'] + else: + options = ['read_only'] + self.bubble = richtext.RichTextEditor(self.blog.host, content, modifiedCb=self._modifiedCb, afterEditCb=self._afterEditCb, options=options) + else: + log.error("Bad entry mode: %s" % self.mode) + self.bubble.addStyleName("bubble") + self.entry_dialog.add(self.bubble) + self.bubble.addEditListener(self._showWarning) # FIXME: remove edit listeners + self.setEditable(self.editable) + + def _setIcons(self): + """Set the entry icons (delete, update, comment)""" + if self.new: + return + + def addIcon(label, title): + label = Label(label) + label.setTitle(title) + label.addClickListener(self) + self.entry_actions.add(label) + return label + + if self.item.comments: + self.comment_label = addIcon(u"↶", "Comment this message") + self.comment_label.setStyleName('mb_entry_action_larger') + else: + self.comment_label = None + is_publisher = self.item.author_jid == self.blog.host.whoami.bare + if is_publisher: + self.update_label = addIcon(u"✍", "Edit this message") + # TODO: add delete button if we are the owner of the node + self.delete_label = addIcon(u"✗", "Delete this message") + else: + self.update_label = self.delete_label = None + + def _createCommentsPanel(self): + """Create the panel if it doesn't exists""" + if self.comments_panel is None: + self.comments_panel = VerticalPanel() + self.comments_panel.setStyleName('microblogPanel') + self.comments_panel.addStyleName('subPanel') + self.add(self.comments_panel) + + def setEditable(self, editable=True): + """Toggle the bubble between display and edit mode. + + @param editable (bool) + """ + self.editable = editable + self.bubble.edit(self.editable) + self.updateIconsAndButtons() + + def updateIconsAndButtons(self): + """Set the visibility of the icons and the button to switch between blog and microblog.""" + try: + self.bubble_commands.removeFromParent() + except (AttributeError, TypeError): + pass + if self.editable: + if self.mode == C.ENTRY_MODE_TEXT: + html = _(u'<a style="color: blue;">switch to blog</a>') + title = _(u'compose a rich text message with a title - suitable for writing articles') + else: + html = _(u'<a style="color: blue;">switch to microblog</a>') + title = _(u'compose a short message without title - suitable for sharing news') + toggle_syntax_button = HTML(html, Title=title) + toggle_syntax_button.addClickListener(self.toggleContentSyntax) + toggle_syntax_button.addStyleName('mb_entry_toggle_syntax') + toggle_syntax_button.setStyleAttribute('top', '-20px') # XXX: need to force CSS + toggle_syntax_button.setStyleAttribute('left', '-20px') + + self.bubble_commands = HorizontalPanel(Width="100%") + + if self.mode == C.ENTRY_MODE_TEXT: + publish_button = HTML(_(u'<a style="color: blue;">shift + enter to publish</a>'), Title=_(u"... or click here")) + publish_button.addStyleName('mb_entry_publish_button') + publish_button.addClickListener(lambda dummy: self.bubble.edit(False)) + publish_button.setStyleAttribute('top', '-20px') # XXX: need to force CSS + publish_button.setStyleAttribute('left', '20px') + self.bubble_commands.add(publish_button) + + self.bubble_commands.add(toggle_syntax_button) + self.entry_dialog.add(self.bubble_commands) + + # hide these icons while editing + try: + self.delete_label.setVisible(not self.editable) + except (TypeError, AttributeError): + pass + try: + self.update_label.setVisible(not self.editable) + except (TypeError, AttributeError): + pass + try: + self.comment_label.setVisible(not self.editable) + except (TypeError, AttributeError): + pass + + def onClick(self, sender): + + if sender == self: + self.blog.setSelectedEntry(self) + elif sender == self.delete_label: + self._onRetractClick() + elif sender == self.update_label: + self.setEditable(True) + elif sender == self.comment_label: + self._onCommentClick() + # elif sender == self.show_comments_link: + # self._blog_panel.loadAllCommentsForEntry(self) + + def _modifiedCb(self, content): + """Send the new content to the backend + + @return: False to restore the original content if a deletion has been cancelled + """ + if not content['text']: # previous content has been emptied + if not self.new: + self._onRetractClick() + return False + + self.item.content = self.item.content_rich = self.item.content_xhtml = None + self.item.title = self.item.title_rich = self.item.title_xhtml = None + + if self.mode in ENTRY_RICH: + # TODO: if the user change his parameters after the message edition started, + # the message syntax could be different then the current syntax: pass the + # message syntax in mb_data for the frontend to use it instead of current syntax. + self.item.content_rich = content['text'] # XXX: this also works if the syntax is XHTML + self.item.title = content['title'] + self.item.tags = list(data_format.dict2iter('tag', content)) + else: + self.item.content = content['text'] + + self.send() + + return True + + def _afterEditCb(self, content): + """Post edition treatments + + Remove the entry if it was an empty one (used for creating a new blog post). + Data for the actual new blog post will be received from the bridge + @param content(dict): edited content + """ + if self.new: + if self.level == 0: + # we have a main item, we keep the edit entry + self.reset(None) + # FIXME: would be better to reset bubble + # but bubble.setContent() doesn't seem to work + self.bubble.removeFromParent() + self._setBubble() + else: + # we don't keep edit entries for comments + self.delete() + else: + self.editable = False + self.updateIconsAndButtons() + + def _showWarning(self, sender, keycode, modifiers): + if keycode == keyb.KEY_ENTER & keyb.MODIFIER_SHIFT: # FIXME: fix edit_listeners, it's dirty (we have to check keycode/modifiers twice !) + self.blog.host.showWarning(None, None) + else: + # self.blog.host.showWarning(*self.blog.getWarningData(self.type == 'comment')) + self.blog.host.showWarning(*self.blog.getWarningData(False)) # FIXME: comments are not yet reimplemented + + def _onRetractClick(self): + """Ask confirmation then retract current entry.""" + assert not self.new + + def confirm_cb(answer): + if answer: + self.retract() + + entry_type = _("message") if self.level == 0 else _("comment") + and_comments = _(" All comments will be also deleted!") if self.item.comments else "" + text = _("Do you really want to delete this {entry_type}?{and_comments}").format( + entry_type=entry_type, and_comments=and_comments) + dialog.ConfirmDialog(confirm_cb, text=text).show() + + def _onCommentClick(self): + """Add an empty entry for a new comment""" + if self._current_comment is None: + if not self.item.comments_service or not self.item.comments_node: + log.warning("Invalid service and node for comments, can't create a comment") + self._current_comment = self.addEntry(editable=True, service=self.item.comments_service, node=self.item.comments_node, edit_entry=True) + self.blog.setSelectedEntry(self._current_comment, True) + self._current_comment.bubble.setFocus(True) # FIXME: should be done elsewhere (automatically)? + + def _changeMode(self, original_content, text): + self.mode = C.ENTRY_MODE_RICH if self.mode == C.ENTRY_MODE_TEXT else C.ENTRY_MODE_TEXT + if self.mode in ENTRY_RICH and not text: + text = ' ' # something different than empty string is needed to initialize the rich text editor + self.item.content = text + if self.mode in ENTRY_RICH: + self.item.content_rich = text # XXX: this also works if the syntax is XHTML + self.bubble.setDisplayContent() # needed in case the edition is aborted, to not end with an empty bubble + else: + self.item.content_xhtml = '' + self.bubble.removeFromParent() + self._setBubble() + self.bubble.setOriginalContent(original_content) + + def toggleContentSyntax(self): + """Toggle the editor between raw and rich text""" + original_content = self.bubble.getOriginalContent() + rich = self.mode in ENTRY_RICH + if rich: + original_content['syntax'] = C.SYNTAX_XHTML + + text = self.bubble.getContent()['text'] + + if not text.strip(): + self._changeMode(original_content,'') + else: + if rich: + def confirm_cb(answer): + if answer: + self.blog.host.bridge.syntaxConvert(text, C.SYNTAX_CURRENT, C.SYNTAX_TEXT, profile=None, + callback=lambda converted: self._changeMode(original_content, converted)) + dialog.ConfirmDialog(confirm_cb, text=_("Do you really want to lose the title and text formatting?")).show() + else: + self.blog.host.bridge.syntaxConvert(text, C.SYNTAX_TEXT, C.SYNTAX_XHTML, profile=None, + callback=lambda converted: self._changeMode(original_content, converted)) + + def update(self, entry=None): + """Update comments""" + self._createCommentsPanel() + self.entries.sort(key=lambda entry: entry.item.published) + # we put edit_entry at the end + edit_entry = [] if self.edit_entry is None else [self.edit_entry] + for idx, entry in enumerate(self.entries + edit_entry): + if not entry.displayed: + self.comments_panel.insert(entry, idx) + entry.displayed = True + + def delete(self): + quick_blog.Entry.delete(self) + + # _current comment is specific to libervia, we remove it + if isinstance(self.manager, Entry): + self.manager._current_comment = None + + # now we remove the pyjamas widgets + parent = self.parent + assert isinstance(parent, VerticalPanel) + self.removeFromParent() + if not parent.children: + # the vpanel is empty, we remove it + parent.removeFromParent() + try: + if self.manager.comments_panel == parent: + self.manager.comments_panel = None + except AttributeError: + assert isinstance(self.manager, quick_blog.QuickBlog) + + +class Blog(quick_blog.QuickBlog, libervia_widget.LiberviaWidget, MouseHandler): + """Panel used to show microblog""" + warning_msg_public = "This message will be <b>PUBLIC</b> and everybody will be able to see it, even people you don't know" + warning_msg_group = "This message will be published for all the people of the following groups: <span class='warningTarget'>%s</span>" + + def __init__(self, host, targets, profiles=None): + quick_blog.QuickBlog.__init__(self, host, targets, C.PROF_KEY_NONE) + title = ", ".join(targets) if targets else "Blog" + libervia_widget.LiberviaWidget.__init__(self, host, title, selectable=True) + MouseHandler.__init__(self) + self.vpanel = VerticalPanel() + self.vpanel.setStyleName('microblogPanel') + self.setWidget(self.vpanel) + if ((self._targets_type == C.ALL and self.host.mblog_available) or + (self._targets_type == C.GROUP and self.host.groupblog_available)): + self.addEntry(editable=True, edit_entry=True) + + self.getAll() + + # self.footer = HTML('', StyleName='microblogPanel_footer') + # self.footer.waiting = False + # self.footer.addClickListener(self) + # self.footer.addMouseListener(self) + # self.vpanel.add(self.footer) + # self.next_rsm_index = 0 + + def __str__(self): + return u"Blog Widget [targets: {}, profile: {}]".format(", ".join(self.targets) if self.targets else "meta blog", self.profile) + + def update(self): + self.entries.sort(key=lambda entry: entry.item.published, reverse=True) + + start_idx = 0 + if self.edit_entry is not None: + start_idx = 1 + if not self.edit_entry.displayed: + self.vpanel.insert(self.edit_entry, 0) + self.edit_entry.displayed = True + + # XXX: enumerate is buggued in pyjamas (start is not used) + # we have to use idx + idx = start_idx + for entry in self.entries: + if not entry.displayed: + self.vpanel.insert(entry, idx) + entry.displayed = True + idx += 1 + + # def onDelete(self): + # quick_widgets.QuickWidget.onDelete(self) + # self.host.removeListener('avatar', self.avatarListener) + + # def onAvatarUpdate(self, jid_, hash_, profile): + # """Called on avatar update events + + # @param jid_: jid of the entity with updated avatar + # @param hash_: hash of the avatar + # @param profile: %(doc_profile)s + # """ + # whoami = self.host.profiles[self.profile].whoami + # if self.isJidAccepted(jid_) or jid_.bare == whoami.bare: + # self.updateValue('avatar', jid_, hash_) + + @staticmethod + def onGroupDrop(host, targets): + """Create a microblog panel for one, several or all contact groups. + + @param host (SatWebFrontend): the SatWebFrontend instance + @param targets (tuple(unicode)): tuple of groups (empty for "all groups") + @return: the created MicroblogPanel + """ + # XXX: pyjamas doesn't support use of cls directly + widget = host.displayWidget(Blog, targets, dropped=True) + return widget + + # @property + # def accepted_groups(self): + # """Return a set of the accepted groups""" + # return set().union(*self.targets) + + def getWarningData(self, comment): + """ + @param comment: set to True if the composed message is a comment + @return: a couple (type, msg) for calling self.host.showWarning""" + if comment: + return ("PUBLIC", "This is a <span class='warningTarget'>comment</span> and keep the initial post visibility, so it is potentialy public") + elif self._targets_type == C.ALL: + # we have a meta MicroblogPanel, we publish publicly + return ("PUBLIC", self.warning_msg_public) + else: + # FIXME: manage several groups + return (self._targets_type, self.warning_msg_group % ' '.join(self.targets)) + + def ensureVisible(self, entry): + """Scroll to an entry to ensure its visibility + + @param entry (MicroblogEntry): the entry + """ + current = entry + while True: + parent = current.getParent() + if parent is None: + log.warning("Can't find any parent ScrollPanel") + return + elif isinstance(parent, ScrollPanel): + parent.ensureVisible(entry) + return + else: + current = parent + + def setSelectedEntry(self, entry, ensure_visible=False): + """Select an entry. + + @param entry (MicroblogEntry): the entry to select + @param ensure_visible (boolean): if True, also scroll to the entry + """ + if ensure_visible: + self.ensureVisible(entry) + + entry.addStyleName('selected_entry') # blink the clicked entry + clicked_entry = entry # entry may be None when the timer is done + Timer(500, lambda timer: clicked_entry.removeStyleName('selected_entry')) + + # def updateValue(self, type_, jid_, value): + # """Update a jid value in entries + + # @param type_: one of 'avatar', 'nick' + # @param jid_(jid.JID): jid concerned + # @param value: new value""" + # assert isinstance(jid_, jid.JID) # FIXME: temporary + # def updateVPanel(vpanel): + # avatar_url = self.host.getAvatarURL(jid_) + # for child in vpanel.children: + # if isinstance(child, MicroblogEntry) and child.author == jid_: + # child.updateAvatar(avatar_url) + # elif isinstance(child, VerticalPanel): + # updateVPanel(child) + # if type_ == 'avatar': + # updateVPanel(self.vpanel) + + # def onClick(self, sender): + # if sender == self.footer: + # self.loadMoreMainEntries() + + # def onMouseEnter(self, sender): + # if sender == self.footer: + # self.loadMoreMainEntries() + + +libervia_widget.LiberviaWidget.addDropKey("GROUP", lambda host, item: Blog.onGroupDrop(host, (item,))) +libervia_widget.LiberviaWidget.addDropKey("CONTACT_TITLE", lambda host, item: Blog.onGroupDrop(host, ())) +quick_blog.registerClass("ENTRY", Entry) +quick_widgets.register(quick_blog.QuickBlog, Blog)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/browser/sat_browser/chat.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,345 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2011-2018 Jérôme Poisson <goffi@goffi.org> + +# This program 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. + +# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + +from sat.core.log import getLogger +log = getLogger(__name__) + +# from sat_frontends.tools.games import SYMBOLS +from sat_browser import strings +from sat_frontends.tools import jid +from sat_frontends.quick_frontend import quick_widgets, quick_games, quick_menus +from sat_frontends.quick_frontend.quick_chat import QuickChat + +from pyjamas.ui.AbsolutePanel import AbsolutePanel +from pyjamas.ui.VerticalPanel import VerticalPanel +from pyjamas.ui.HorizontalPanel import HorizontalPanel +from pyjamas.ui.KeyboardListener import KEY_ENTER, KeyboardHandler +from pyjamas.ui.HTMLPanel import HTMLPanel +from pyjamas import DOM +from pyjamas import Window + +from datetime import datetime + +import html_tools +import libervia_widget +import base_panel +import contact_panel +import editor_widget +from constants import Const as C +import plugin_xep_0085 +import game_tarot +import game_radiocol + + +unicode = str # FIXME: pyjamas workaround + + +class MessageWidget(HTMLPanel): + + def __init__(self, mess_data): + """ + @param mess_data(quick_chat.Message, None): message data + None: used only for non text widgets (e.g.: focus separator) + """ + self.mess_data = mess_data + mess_data.widgets.add(self) + _msg_class = [] + if mess_data.type == C.MESS_TYPE_INFO: + markup = "<span class='{msg_class}'>{msg}</span>" + + if mess_data.extra.get('info_type') == 'me': + _msg_class.append('chatTextMe') + else: + _msg_class.append('chatTextInfo') + # FIXME: following code was in printInfo before refactoring + # seems to be used only in radiocol + # elif type_ == 'link': + # _wid = HTML(msg) + # _wid.setStyleName('chatTextInfo-link') + # if link_cb: + # _wid.addClickListener(link_cb) + else: + markup = "<span class='chat_text_timestamp'>{timestamp}</span> <span class='chat_text_nick'>{nick}</span> <span class='{msg_class}'>{msg}</span>" + _msg_class.append("chat_text_msg") + if mess_data.own_mess: + _msg_class.append("chat_text_mymess") + + xhtml = mess_data.main_message_xhtml + _date = datetime.fromtimestamp(float(mess_data.timestamp)) + HTMLPanel.__init__(self, markup.format( + timestamp = _date.strftime("%H:%M"), + nick = "[{}]".format(html_tools.html_sanitize(mess_data.nick)), + msg_class = ' '.join(_msg_class), + msg = strings.addURLToText(html_tools.html_sanitize(mess_data.main_message)) if not xhtml else html_tools.inlineRoot(xhtml) # FIXME: images and external links must be removed according to preferences + )) + if mess_data.type != C.MESS_TYPE_INFO: + self.setStyleName('chatText') + + +class Chat(QuickChat, libervia_widget.LiberviaWidget, KeyboardHandler): + + def __init__(self, host, target, type_=C.CHAT_ONE2ONE, nick=None, occupants=None, subject=None, profiles=None): + """Panel used for conversation (one 2 one or group chat) + + @param host: SatWebFrontend instance + @param target: entity (jid.JID) with who we have a conversation (contact's jid for one 2 one chat, or MUC room) + @param type: one2one for simple conversation, group for MUC + """ + QuickChat.__init__(self, host, target, type_, nick, occupants, subject, profiles=profiles) + self.vpanel = VerticalPanel() + self.vpanel.setSize('100%', '100%') + + # FIXME: temporary dirty initialization to display the OTR state + header_info = host.plugins['otr'].getInfoTextForUser(target) if (type_ == C.CHAT_ONE2ONE and 'otr' in host.plugins) else None + + libervia_widget.LiberviaWidget.__init__(self, host, title=unicode(target.bare), info=header_info, selectable=True) + self._body = AbsolutePanel() + self._body.setStyleName('chatPanel_body') + chat_area = HorizontalPanel() + chat_area.setStyleName('chatArea') + if type_ == C.CHAT_GROUP: + self.occupants_panel = contact_panel.ContactsPanel(host, merge_resources=False, + contacts_style="muc_contact", + contacts_menus=(C.MENU_JID_CONTEXT), + contacts_display=('resource',)) + chat_area.add(self.occupants_panel) + DOM.setAttribute(chat_area.getWidgetTd(self.occupants_panel), "className", "occupantsPanelCell") + # FIXME: workaround for a pyjamas issue: calling hash on a class method always return a different value if that method is defined directly within the class (with the "def" keyword) + self.presenceListener = self.onPresenceUpdate + self.host.addListener('presence', self.presenceListener, [C.PROF_KEY_NONE]) + self.avatarListener = self.onAvatarUpdate + host.addListener('avatar', self.avatarListener, [C.PROF_KEY_NONE]) + Window.addWindowResizeListener(self) + + else: + self.chat_state = None + + self._body.add(chat_area) + self.content = AbsolutePanel() + self.content.setStyleName('chatContent') + self.content_scroll = base_panel.ScrollPanelWrapper(self.content) + chat_area.add(self.content_scroll) + chat_area.setCellWidth(self.content_scroll, '100%') + self.vpanel.add(self._body) + self.vpanel.setCellHeight(self._body, '100%') + self.addStyleName('chatPanel') + self.setWidget(self.vpanel) + self.chat_state_machine = plugin_xep_0085.ChatStateMachine(self.host, unicode(self.target)) + + self.message_box = editor_widget.MessageBox(self.host) + self.message_box.onSelectedChange(self) + self.message_box.addKeyboardListener(self) + self.vpanel.add(self.message_box) + self.postInit() + + def onWindowResized(self, width=None, height=None): + if self.type == C.CHAT_GROUP: + ideal_height = self.content_scroll.getOffsetHeight() + self.occupants_panel.setHeight("%s%s" % (ideal_height, "px")) + + @property + def target(self): + # FIXME: for unknow reason, pyjamas doesn't use the method inherited from QuickChat + # FIXME: must remove this when either pyjamas is fixed, or we use an alternative + if self.type == C.CHAT_GROUP: + return self.current_target.bare + return self.current_target + + @property + def profile(self): + # FIXME: for unknow reason, pyjamas doesn't use the method inherited from QuickWidget + # FIXME: must remove this when either pyjamas is fixed, or we use an alternative + assert len(self.profiles) == 1 and not self.PROFILES_MULTIPLE and not self.PROFILES_ALLOW_NONE + return list(self.profiles)[0] + + @property + def plugin_menu_context(self): + return (C.MENU_ROOM,) if self.type == C.CHAT_GROUP else (C.MENU_SINGLE,) + + def onKeyDown(self, sender, keycode, modifiers): + if keycode == KEY_ENTER: + self.host.showWarning(None, None) + else: + self.host.showWarning(*self.getWarningData()) + + def getWarningData(self): + if self.type not in [C.CHAT_ONE2ONE, C.CHAT_GROUP]: + raise Exception("Unmanaged type !") + if self.type == C.CHAT_ONE2ONE: + msg = "This message will be sent to your contact <span class='warningTarget'>%s</span>" % self.target + elif self.type == C.CHAT_GROUP: + msg = "This message will be sent to all the participants of the multi-user room <span class='warningTarget'>%s</span>" % self.target + return ("ONE2ONE" if self.type == C.CHAT_ONE2ONE else "GROUP", msg) + + def onTextEntered(self, text): + self.host.messageSend(self.target, + {'': text}, + {}, + C.MESS_TYPE_GROUPCHAT if self.type == C.CHAT_GROUP else C.MESS_TYPE_CHAT, + {}, + errback=self.host.sendError, + profile_key=C.PROF_KEY_NONE + ) + self.chat_state_machine._onEvent("active") + + def onPresenceUpdate(self, entity, show, priority, statuses, profile): + """Update entity's presence status + + @param entity(jid.JID): entity updated + @param show: availability + @parap priority: resource's priority + @param statuses: dict of statuses + @param profile: %(doc_profile)s + """ + assert self.type == C.CHAT_GROUP + if entity.bare != self.target: + return + self.update(entity) + + def onAvatarUpdate(self, entity, hash_, profile): + """Called on avatar update events + + @param jid_: jid of the entity with updated avatar + @param hash_: hash of the avatar + @param profile: %(doc_profile)s + """ + assert self.type == C.CHAT_GROUP + if entity.bare != self.target: + return + self.update(entity) + + def onQuit(self): + libervia_widget.LiberviaWidget.onQuit(self) + if self.type == C.CHAT_GROUP: + self.host.removeListener('presence', self.presenceListener) + self.host.bridge.mucLeave(self.target.bare, profile=C.PROF_KEY_NONE) + + def newMessage(self, from_jid, target, msg, type_, extra, profile): + header_info = extra.pop('header_info', None) + if header_info: + self.setHeaderInfo(header_info) + QuickChat.newMessage(self, from_jid, target, msg, type_, extra, profile) + + def _onHistoryPrinted(self): + """Refresh or scroll down the focus after the history is printed""" + self.printMessages(clear=False) + super(Chat, self)._onHistoryPrinted() + + def printMessages(self, clear=True): + """generate message widgets + + @param clear(bool): clear message before printing if true + """ + if clear: + # FIXME: clear is not handler + pass + for message in self.messages.itervalues(): + self.appendMessage(message) + + def createMessage(self, message): + self.appendMessage(message) + + def appendMessage(self, message): + self.content.add(MessageWidget(message)) + self.content_scroll.scrollToBottom() + + def notify(self, contact="somebody", msg=""): + """Notify the user of a new message if primitivus doesn't have the focus. + + @param contact (unicode): contact who wrote to the users + @param msg (unicode): the message that has been received + """ + self.host.notification.notify(contact, msg) + + # def printDayChange(self, day): + # """Display the day on a new line. + + # @param day(unicode): day to display (or not if this method is not overwritten) + # """ + # self.printInfo("* " + day) + + def setTitle(self, title=None, extra=None): + """Refresh the title of this Chat dialog + + @param title (unicode): main title or None to use default + @param suffix (unicode): extra title (e.g. for chat states) or None + """ + if title is None: + title = unicode(self.target.bare) + if extra: + title += ' %s' % extra + libervia_widget.LiberviaWidget.setTitle(self, title) + + def onChatState(self, from_jid, state, profile): + super(Chat, self).onChatState(from_jid, state, profile) + if self.type == C.CHAT_ONE2ONE: + self.title_dynamic = C.CHAT_STATE_ICON[state] + + def update(self, entity=None): + """Update one or all entities. + + @param entity (jid.JID): entity to update + """ + if self.type == C.CHAT_ONE2ONE: # only update the chat title + if self.chat_state: + self.setTitle(extra='({})'.format(self.chat_state)) + else: + if entity is None: # rebuild all the occupants list + nicks = list(self.occupants) + nicks.sort() + self.occupants_panel.setList([jid.newResource(self.target, nick) for nick in nicks]) + else: # add, remove or update only one occupant + contact_list = self.host.contact_lists[self.profile] + show = contact_list.getCache(entity, C.PRESENCE_SHOW) + if show == C.PRESENCE_UNAVAILABLE or show is None: + self.occupants_panel.removeContactBox(entity) + else: + pass + # FIXME: legacy code, chat state must be checked + # box = self.occupants_panel.updateContactBox(entity) + # box.states.setHTML(u''.join(states.values())) + + # FIXME: legacy code, chat state must be checked + # if 'chat_state' in states.keys(): # start/stop sending "composing" state from now + # self.chat_state_machine.started = not not states['chat_state'] + + self.onWindowResized() # be sure to set the good height + + def addGamePanel(self, widget): + """Insert a game panel to this Chat dialog. + + @param widget (Widget): the game panel + """ + self.vpanel.insert(widget, 0) + self.vpanel.setCellHeight(widget, widget.getHeight()) + + def removeGamePanel(self, widget): + """Remove the game panel from this Chat dialog. + + @param widget (Widget): the game panel + """ + self.vpanel.remove(widget) + + +quick_widgets.register(QuickChat, Chat) +quick_widgets.register(quick_games.Tarot, game_tarot.TarotPanel) +quick_widgets.register(quick_games.Radiocol, game_radiocol.RadioColPanel) +libervia_widget.LiberviaWidget.addDropKey("CONTACT", lambda host, item: host.displayWidget(Chat, jid.JID(item), dropped=True)) +quick_menus.QuickMenusManager.addDataCollector(C.MENU_ROOM, {'room_jid': 'target'}) +quick_menus.QuickMenusManager.addDataCollector(C.MENU_SINGLE, {'jid': 'target'})
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/browser/sat_browser/constants.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,40 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a SAT frontend +# Copyright (C) 2009-2018 Jérôme Poisson (goffi@goffi.org) + +# This program 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. + +# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + +from libervia.common.constants import Const as C + + +# Auxiliary functions +param_to_bool = lambda value: value == 'true' + + +class Const(C): + """Add here the constants that are only used by the browser side.""" + + # Cached parameters, e.g those that have an incidence on UI display/refresh: + # - they can be any parameter (not necessarily specific to Libervia) + # - list them as a couple (category, name) + CACHED_PARAMS = [('General', C.SHOW_OFFLINE_CONTACTS), + ('General', C.SHOW_EMPTY_GROUPS), + ] + + WEB_PANEL_DEFAULT_URL = "http://salut-a-toi.org" + WEB_PANEL_SCHEMES = {'http', 'https', 'ftp', 'file'} + + CONTACT_DEFAULT_DISPLAY=('bare', 'nick')
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/browser/sat_browser/contact_group.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,245 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2013-2016 Adrien Cossa <souliane@mailoo.org> + +# This program 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. + +# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + +from pyjamas.ui.Button import Button +from pyjamas.ui.CheckBox import CheckBox +from pyjamas.ui.Label import Label +from pyjamas.ui.HorizontalPanel import HorizontalPanel +from pyjamas.ui.VerticalPanel import VerticalPanel +from pyjamas.ui.DialogBox import DialogBox +from pyjamas.ui.ScrollPanel import ScrollPanel +from pyjamas.ui import HasAlignment + +import dialog +import list_manager +import contact_panel +import contact_list +from sat_frontends.tools import jid + + +unicode = str # FIXME: pyjamas workaround + + +class ContactGroupManager(list_manager.ListManager): + + def __init__(self, editor, data, contacts, offsets): + """ + @param container (FlexTable): FlexTable parent widget + @param keys (dict{unicode: dict{unicode: unicode}}): dict binding items + keys to their display config data. + @param contacts (list): list of contacts + """ + self.editor = editor + list_manager.ListManager.__init__(self, data, contacts) + self.registerPopupMenuPanel(entries={"Remove group": {}}, + callback=lambda sender, key: self.removeGroup(sender)) + + def removeGroup(self, sender): + group = sender.getHTML() + + def confirm_cb(answer): + if answer: + list_manager.ListManager.removeList(self, group) + self.editor.add_group_panel.groups.remove(group) + + _dialog = dialog.ConfirmDialog(confirm_cb, text="Do you really want to delete the group '%s'?" % group) + _dialog.show() + + def tag(self, contacts): + list_manager.ListManager.tag(self, contacts) + self.editor.updateContactList(contacts) + + def untag(self, contacts, ignore_key=None): + list_manager.ListManager.untag(self, contacts, ignore_key) + self.editor.updateContactList(contacts) + + +class ContactGroupEditor(VerticalPanel): + """A big panel including a ContactGroupManager and other UI stuff.""" + + def __init__(self, host, container=None, onCloseCallback=None): + """ + + @param host (SatWebFrontend) + @param container (PanelBase): parent panel or None to display in a popup + @param onCloseCallback (callable) + """ + VerticalPanel.__init__(self, StyleName="contactGroupEditor") + self.host = host + + # eventually display in a popup + if container is None: + container = DialogBox(autoHide=False, centered=True) + container.setHTML("Manage contact groups") + self.container = container + self._on_close_callback = onCloseCallback + + self.all_contacts = contact_list.JIDList(self.host.contact_list.roster) + roster_entities_by_group = self.host.contact_list.roster_entities_by_group + del roster_entities_by_group[None] # remove the empty group + roster_groups = roster_entities_by_group.keys() + roster_groups.sort() + + # groups on the left + manager = self.initContactGroupManager(roster_entities_by_group) + self.add_group_panel = self.initAddGroupPanel(roster_groups) + left_container = VerticalPanel(Width="100%") + left_container.add(manager) + left_container.add(self.add_group_panel) + left_container.setCellHorizontalAlignment(self.add_group_panel, HasAlignment.ALIGN_CENTER) + left_panel = ScrollPanel(left_container, StyleName="contactGroupManager") + left_panel.setAlwaysShowScrollBars(True) + + # contact list on the right + east_panel = ScrollPanel(self.initContactList(), StyleName="contactGroupRoster") + east_panel.setAlwaysShowScrollBars(True) + + south_panel = self.initCloseSaveButtons() + + main_panel = HorizontalPanel() + main_panel.add(left_panel) + main_panel.add(east_panel) + self.add(Label("You get here an over whole view of your contact groups. There are two ways to assign your contacts to an existing group: write them into auto-completed textboxes or use the right panel to drag and drop them into the group.")) + self.add(main_panel) + self.add(south_panel) + + self.setCellHorizontalAlignment(south_panel, HasAlignment.ALIGN_CENTER) + + # need to be done after the contact list has been initialized + self.updateContactList() + + # Hide the contacts list from the main panel to not confuse the user + self.restore_contact_panel = False + clist = self.host.contact_list_widget + if clist.getVisible(): + self.restore_contact_panel = True + self.host.panel._contactsSwitch() + + container.add(self) + container.setVisible(True) + if isinstance(container, DialogBox): + container.center() + + def initContactGroupManager(self, data): + """Initialise the contact group manager. + + @param groups (list[unicode]): contact groups + """ + self.groups = ContactGroupManager(self, data, self.all_contacts) + return self.groups + + def initAddGroupPanel(self, groups): + """Initialise the 'Add group' panel. + + @param groups (list[unicode]): contact groups + """ + + def add_group_cb(key): + self.groups.addList(key) + self.add_group_panel.textbox.setFocus(True) + + add_group_panel = dialog.AddGroupPanel(groups, add_group_cb) + add_group_panel.addStyleName("addContactGroupPanel") + return add_group_panel + + def initCloseSaveButtons(self): + """Add the buttons to close the dialog and save the groups.""" + buttons = HorizontalPanel() + buttons.addStyleName("marginAuto") + buttons.add(Button("Cancel", listener=self.cancelWithoutSaving)) + buttons.add(Button("Save", listener=self.closeAndSave)) + return buttons + + def initContactList(self): + """Add the contact list to the DockPanel.""" + + self.toggle = CheckBox("Hide assigned contacts") + self.toggle.addClickListener(lambda dummy: self.updateContactList()) + self.toggle.addStyleName("toggleAssignedContacts") + self.contacts = contact_panel.ContactsPanel(self.host) + for contact in self.all_contacts: + self.contacts.updateContactBox(contact) + panel = VerticalPanel() + panel.add(self.toggle) + panel.add(self.contacts) + return panel + + def updateContactList(self, contacts=None): + """Update the contact list's items visibility, depending of the toggle + checkbox and the "contacts" attribute. + + @param contacts (list): contacts to be updated, or None to update all. + """ + if not hasattr(self, "toggle"): + return + if contacts is not None: + contacts = [jid.JID(contact) for contact in contacts] + contacts = set(contacts).intersection(self.all_contacts) + else: + contacts = self.all_contacts + + for contact in contacts: + if not self.toggle.getChecked(): # show all contacts + self.contacts.updateContactBox(contact).setVisible(True) + else: # show only non-assigned contacts + if contact in self.groups.untagged: + self.contacts.updateContactBox(contact).setVisible(True) + else: + self.contacts.updateContactBox(contact).setVisible(False) + + def __close(self): + """Remove the widget from parent or close the popup.""" + if isinstance(self.container, DialogBox): + self.container.hide() + self.container.remove(self) + if self._on_close_callback is not None: + self._on_close_callback() + if self.restore_contact_panel: + self.host.panel._contactsSwitch() + + def cancelWithoutSaving(self): + """Ask for confirmation before closing the dialog.""" + def confirm_cb(answer): + if answer: + self.__close() + + _dialog = dialog.ConfirmDialog(confirm_cb, text="Do you really want to cancel without saving?") + _dialog.show() + + def closeAndSave(self): + """Call bridge methods to save the changes and close the dialog""" + old_groups_by_entity = contact_list.JIDDict(self.host.contact_list.roster_groups_by_entity) + old_entities = old_groups_by_entity.keys() + result = {jid.JID(item): keys for item, keys in self.groups.getKeysByItem().iteritems()} + groups_by_entity = contact_list.JIDDict(result) + entities = groups_by_entity.keys() + + for invalid in entities.difference(self.all_contacts): + dialog.InfoDialog("Invalid contact(s)", + "The contact '%s' is not in your contact list but has been assigned to: '%s'." % (invalid, "', '".join(groups_by_entity[invalid])) + + "Your changes could not be saved: please check your assignments and save again.", Width="400px").center() + return + + for entity in old_entities.difference(entities): + self.host.bridge.call('updateContact', None, unicode(entity), '', []) + + for entity, groups in groups_by_entity.iteritems(): + if entity not in old_groups_by_entity or groups != old_groups_by_entity[entity]: + self.host.bridge.call('updateContact', None, unicode(entity), '', list(groups)) + self.__close()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/browser/sat_browser/contact_list.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,319 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2011-2018 Jérôme Poisson <goffi@goffi.org> + +# This program 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. + +# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + +import pyjd # this is dummy in pyjs +from sat.core.log import getLogger +log = getLogger(__name__) +from sat_frontends.quick_frontend.quick_contact_list import QuickContactList +from pyjamas.ui.SimplePanel import SimplePanel +from pyjamas.ui.ScrollPanel import ScrollPanel +from pyjamas.ui.VerticalPanel import VerticalPanel +from pyjamas.ui.ClickListener import ClickHandler +from pyjamas.ui.Label import Label +from pyjamas import Window +from pyjamas import DOM + +from constants import Const as C +from sat_frontends.tools import jid +import libervia_widget +import contact_panel +import blog +import chat + +unicode = str # XXX: pyjama doesn't manage unicode + + +class GroupLabel(libervia_widget.DragLabel, Label, ClickHandler): + def __init__(self, host, group): + """ + + @param host (SatWebFrontend) + @param group (unicode): group name + """ + self.group = group + Label.__init__(self, group) # , Element=DOM.createElement('div') + self.setStyleName('group') + libervia_widget.DragLabel.__init__(self, group, "GROUP", host) + ClickHandler.__init__(self) + self.addClickListener(self) + + def onClick(self, sender): + self.host.displayWidget(blog.Blog, (self.group,)) + + +class GroupPanel(VerticalPanel): + + def __init__(self, parent): + VerticalPanel.__init__(self) + self.setStyleName('groupPanel') + self._parent = parent + self._groups = set() + + def add(self, group): + if group in self._groups: + log.warning("trying to add an already existing group") + return + _item = GroupLabel(self._parent.host, group) + _item.addMouseListener(self._parent) + DOM.setStyleAttribute(_item.getElement(), "cursor", "pointer") + index = 0 + for group_ in [child.group for child in self.getChildren()]: + if group_ > group: + break + index += 1 + VerticalPanel.insert(self, _item, index) + self._groups.add(group) + + def remove(self, group): + for wid in self: + if isinstance(wid, GroupLabel) and wid.group == group: + VerticalPanel.remove(self, wid) + self._groups.remove(group) + return + log.warning("Trying to remove a non existent group") + + def getGroupBox(self, group): + """get the widget of a group + + @param group (unicode): the group + @return: GroupLabel instance if present, else None""" + for wid in self: + if isinstance(wid, GroupLabel) and wid.group == group: + return wid + return None + + def getGroups(self): + return self._groups + + +class ContactTitleLabel(libervia_widget.DragLabel, Label, ClickHandler): + + def __init__(self, host, text): + Label.__init__(self, text) # , Element=DOM.createElement('div') + self.setStyleName('contactTitle') + libervia_widget.DragLabel.__init__(self, text, "CONTACT_TITLE", host) + ClickHandler.__init__(self) + self.addClickListener(self) + + def onClick(self, sender): + self.host.displayWidget(blog.Blog, ()) + + +class ContactList(SimplePanel, QuickContactList): + """Manage the contacts and groups""" + + def __init__(self, host, target, on_click=None, on_change=None, user_data=None, profiles=None): + QuickContactList.__init__(self, host, C.PROF_KEY_NONE) + self.contact_list = self.host.contact_list + SimplePanel.__init__(self) + self.host = host + self.scroll_panel = ScrollPanel() + self.scroll_panel.addStyleName("gwt-ScrollPanel") # XXX: no class is set by Pyjamas + self.vPanel = VerticalPanel() + _title = ContactTitleLabel(host, 'Contacts') + DOM.setStyleAttribute(_title.getElement(), "cursor", "pointer") + + def on_click(contact_jid): + self.host.displayWidget(chat.Chat, contact_jid, type_=C.CHAT_ONE2ONE) + self.removeAlerts(contact_jid, True) + + self._contacts_panel = contact_panel.ContactsPanel(host, contacts_click=on_click, contacts_menus=(C.MENU_JID_CONTEXT, C.MENU_ROSTER_JID_CONTEXT)) + self._group_panel = GroupPanel(self) + + self.vPanel.add(_title) + self.vPanel.add(self._group_panel) + self.vPanel.add(self._contacts_panel) + self.scroll_panel.add(self.vPanel) + self.add(self.scroll_panel) + self.setStyleName('contactList') + Window.addWindowResizeListener(self) + + # FIXME: workaround for a pyjamas issue: calling hash on a class method always return a different value if that method is defined directly within the class (with the "def" keyword) + self.avatarListener = self.onAvatarUpdate + host.addListener('avatar', self.avatarListener, [C.PROF_KEY_NONE]) + self.postInit() + + @property + def profile(self): + return C.PROF_KEY_NONE + + def onDelete(self): + QuickContactList.onDelete(self) + self.host.removeListener('avatar', self.avatarListener) + + def update(self, entities=None, type_=None, profile=None): + # XXX: as update is slow, we avoid many updates on profile plugs + # and do them all at once at the end + if not self.host._profile_plugged: # FIXME: should not be necessary anymore (done in QuickFrontend) + return + ### GROUPS ### + _keys = self.contact_list._groups.keys() + try: + # XXX: Pyjamas doesn't do the set casting if None is present + _keys.remove(None) + except (KeyError, ValueError): # XXX: error raised depend on pyjama's compilation options + pass + current_groups = set(_keys) + shown_groups = self._group_panel.getGroups() + new_groups = current_groups.difference(shown_groups) + removed_groups = shown_groups.difference(current_groups) + for group in new_groups: + self._group_panel.add(group) + for group in removed_groups: + self._group_panel.remove(group) + + ### JIDS ### + to_show = [jid_ for jid_ in self.contact_list.roster if self.contact_list.entityVisible(jid_) and jid_ != self.contact_list.whoami.bare] + to_show.sort() + + self._contacts_panel.setList(to_show) + + def onWindowResized(self, width, height): + ideal_height = height - DOM.getAbsoluteTop(self.getElement()) - 5 + tab_panel = self.host.panel.tab_panel + if tab_panel.getWidgetCount() > 1: + ideal_height -= tab_panel.getTabBar().getOffsetHeight() + self.scroll_panel.setHeight("%s%s" % (ideal_height, "px")) + + def isContactInRoster(self, contact_jid): + """Test if the contact is in our roster list""" + for contact_box in self._contacts_panel: + if contact_jid == contact_box.jid: + return True + return False + + def getGroups(self): + return set([g for g in self._groups if g is not None]) + + def onMouseMove(self, sender, x, y): + pass + + def onMouseDown(self, sender, x, y): + pass + + def onMouseUp(self, sender, x, y): + pass + + def onMouseEnter(self, sender): + if isinstance(sender, GroupLabel): + jids = self.contact_list.getGroupData(sender.group, "jids") + for contact in self._contacts_panel.getBoxes(): + if contact.jid in jids: + contact.label.addStyleName("selected") + + def onMouseLeave(self, sender): + if isinstance(sender, GroupLabel): + jids = self.contact_list.getGroupData(sender.group, "jids") + for contact in self._contacts_panel.getBoxes(): + if contact.jid in jids: + contact.label.removeStyleName("selected") + + def onAvatarUpdate(self, jid_, hash_, profile): + """Called on avatar update events + + @param jid_: jid of the entity with updated avatar + @param hash_: hash of the avatar + @param profile: %(doc_profile)s + """ + box = self._contacts_panel.getContactBox(jid_) + if box: + box.update() + + def onNickUpdate(self, jid_, new_nick, profile): + box = self._contacts_panel.getContactBox(jid_) + if box: + box.update() + + def offlineContactsToShow(self): + """Tell if offline contacts should be visible according to the user settings + + @return: boolean + """ + return C.bool(self.host.getCachedParam('General', C.SHOW_OFFLINE_CONTACTS)) + + def emtyGroupsToShow(self): + """Tell if empty groups should be visible according to the user settings + + @return: boolean + """ + return C.bool(self.host.getCachedParam('General', C.SHOW_EMPTY_GROUPS)) + + def onPresenceUpdate(self, entity, show, priority, statuses, profile): + QuickContactList.onPresenceUpdate(self, entity, show, priority, statuses, profile) + box = self._contacts_panel.getContactBox(entity) + if box: # box doesn't exist for MUC bare entity, don't create it + box.update() + + +class JIDList(list): + """JID-friendly list implementation for Pyjamas""" + + def __contains__(self, item): + """Tells if the list contains the given item. + + @param item (object): element to check + @return: bool + """ + # Since our JID doesn't inherit from str/unicode, without this method + # the test would return True only when the objects references are the + # same. Tests have shown that the other iterable "set" and "dict" don't + # need this hack to reproduce the Twisted's behavior. + for other in self: + if other == item: + return True + return False + + def index(self, item): + i = 0 + for other in self: + if other == item: + return i + i += 1 + raise ValueError("JIDList.index(%(item)s): %(item)s not in list" % {"item": item}) + +class JIDSet(set): + """JID set implementation for Pyjamas""" + + def __contains__(self, item): + return __containsJID(self, item) + + +class JIDDict(dict): + """JID dict implementation for Pyjamas (a dict with JID keys)""" + + def __contains__(self, item): + return __containsJID(self, item) + + def keys(self): + return JIDSet(dict.keys(self)) + + +def __containsJID(iterable, item): + """Tells if the given item is in the iterable, works with JID. + + @param iterable(object): list, set or another iterable object + @param item (object): element + @return: bool + """ + # Pyjamas JID-friendly implementation of the "in" operator. Since our JID + # doesn't inherit from str, without this method the test would return True + # only when the objects references are the same. + if isinstance(item, jid.JID): + return hash(item) in [hash(other) for other in iterable if isinstance(other, jid.JID)] + return super(type(iterable), iterable).__contains__(iterable, item)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/browser/sat_browser/contact_panel.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,157 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2011-2018 Jérôme Poisson <goffi@goffi.org> + +# This program 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. + +# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + +""" Contacts / jids related panels """ + +import pyjd # this is dummy in pyjs +from sat.core.log import getLogger +log = getLogger(__name__) +from sat_frontends.tools import jid + +from pyjamas.ui.ScrollPanel import ScrollPanel +from pyjamas.ui.VerticalPanel import VerticalPanel + +import contact_widget +from constants import Const as C + + +class ContactsPanel(ScrollPanel): + """ContactList graphic representation + + Special features like popup menu panel or changing the contact states must be done in a sub-class. + """ + + def __init__(self, host, merge_resources=True, contacts_click=None, + contacts_style=None, contacts_menus=None, + contacts_display=C.CONTACT_DEFAULT_DISPLAY): + """ + + @param host (SatWebFrontend): host instance + @param merge_resources (bool): if True, the entities sharing the same + bare JID will also share the same contact box. + @param contacts_click (callable): click callback for the contact boxes + @param contacts_style (unicode): CSS style name for the contact boxes + @param contacts_menus (tuple): define the menu types that fit this + contact panel, with values from the menus type constants. + @param contacts_display (tuple): prioritize the display methods of the + contact's label with values in ("jid", "nick", "bare", "resource") + """ + self.panel = VerticalPanel() + ScrollPanel.__init__(self, self.panel) + self.addStyleName("gwt-ScrollPanel") # XXX: no class is set by Pyjamas + + self.host = host + self.merge_resources = merge_resources + self._contacts = {} # entity jid to ContactBox map + self.panel.click_listener = None + + if contacts_click is not None: + self.panel.onClick = contacts_click + + self.contacts_style = contacts_style + self.contacts_menus = contacts_menus + self.contacts_display = contacts_display + + def _key(self, contact_jid): + """Return internal key for this contact. + + @param contact_jid (jid.JID): contact JID + @return: jid.JID + """ + return contact_jid.bare if self.merge_resources else contact_jid + + def getJids(self): + """Return jids present in the panel + + @return (list[jid.JID]): full jids or bare jids if self.merge_resources is True + """ + return self._contacts.keys() + + def getBoxes(self): + """Return ContactBox present in the panel + + @return (list[ContactBox]): boxes of the contacts + """ + return self._contacts.itervalues() + + def clear(self): + """Clear all contacts.""" + self._contacts.clear() + VerticalPanel.clear(self.panel) + + def setList(self, jids): + """set all contacts in the list in one shot. + + @param jids (list[jid.JID]): jids to display (the order is kept) + @param name (unicode): optional name of the contact + """ + current_jids = [box.jid for box in self.panel.children if isinstance(box, contact_widget.ContactBox)] + if current_jids == jids: + # the display doesn't change + return + for contact_jid in set(current_jids).difference(jids): + self.removeContactBox(contact_jid) + count = 0 + for contact_jid in jids: + assert isinstance(contact_jid, jid.JID) + self.updateContactBox(contact_jid, count) + count += 1 + + def getContactBox(self, contact_jid): + """Get the contact box for the given contact. + + @param contact_jid (jid.JID): contact JID + @return: ContactBox or None + """ + try: + return self._contacts[self._key(contact_jid)] + except KeyError: + return None + + def updateContactBox(self, contact_jid, index=None): + """Add a contact or update it if it already exists. + + @param contact_jid (jid.JID): contact JID + @param index (int): insertion index if the contact is added + @return: ContactBox + """ + box = self.getContactBox(contact_jid) + if box: + box.update() + else: + entity = contact_jid.bare if self.merge_resources else contact_jid + box = contact_widget.ContactBox(self.host, entity, + style_name=self.contacts_style, + display=self.contacts_display, + plugin_menu_context=self.contacts_menus) + self._contacts[self._key(contact_jid)] = box + if index: + VerticalPanel.insert(self.panel, box, index) + else: + VerticalPanel.append(self.panel, box) + return box + + def removeContactBox(self, contact_jid): + """Remove a contact. + + @param contact_jid (jid.JID): contact JID + """ + box = self._contacts.pop(self._key(contact_jid), None) + if box: + VerticalPanel.remove(self.panel, box)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/browser/sat_browser/contact_widget.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,160 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2011-2018 Jérôme Poisson <goffi@goffi.org> + +# This program 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. + +# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + +import pyjd # this is dummy in pyjs +from sat.core.log import getLogger +log = getLogger(__name__) + +from sat.core import exceptions +from sat_frontends.quick_frontend import quick_menus +from pyjamas.ui.VerticalPanel import VerticalPanel +from pyjamas.ui.HTML import HTML +from pyjamas.ui.Image import Image +from pyjamas.ui.ClickListener import ClickHandler +from constants import Const as C +import html_tools +import base_widget +import libervia_widget + +unicode = str # XXX: pyjama doesn't manage unicode + + +class ContactLabel(HTML): + """Display a contact in HTML, selecting best display (jid/nick/etc)""" + + def __init__(self, host, jid_, display=C.CONTACT_DEFAULT_DISPLAY): + """ + + @param host (SatWebFrontend): host instance + @param jid_ (jid.JID): contact JID + @param display (tuple): prioritize the display methods of the contact's + label with values in ("jid", "nick", "bare", "resource"). + """ + # TODO: add a listener for nick changes + HTML.__init__(self) + self.host = host + self.jid = jid_ + self.display = display + self.alert = False + self.setStyleName('contactLabel') + + def update(self): + clist = self.host.contact_list + notifs = list(self.host.getNotifs(self.jid, exact_jid=False, profile=C.PROF_KEY_NONE)) + alerts_count = len(notifs) + alert_html = ("<strong>(%i)</strong> " % alerts_count) if alerts_count else "" + + contact_raw = None + for disp in self.display: + if disp == "jid": + contact_raw = unicode(self.jid) + elif disp == "nick": + clist = self.host.contact_list + contact_raw = html_tools.html_sanitize(clist.getCache(self.jid, "nick")) + elif disp == "bare": + contact_raw = unicode(self.jid.bare) + elif disp == "resource": + contact_raw = self.jid.resource + else: + raise exceptions.InternalError(u"Unknown display argument [{}]".format(disp)) + if contact_raw: + break + if not contact_raw: + log.error(u"Could not find a contact display for jid {jid} (display: {display})".format(jid=self.jid, display=self.display)) + contact_raw = "UNNAMED" + contact_html = html_tools.html_sanitize(contact_raw) + + html = "%(alert)s%(contact)s" % {'alert': alert_html, + 'contact': contact_html} + self.setHTML(html) + + +class ContactMenuBar(base_widget.WidgetMenuBar): + """WidgetMenuBar displaying the contact's avatar as category.""" + + def onBrowserEvent(self, event): + base_widget.WidgetMenuBar.onBrowserEvent(self, event) + event.stopPropagation() # prevent opening the chat dialog + + @classmethod + def getCategoryHTML(cls, category): + """Return the HTML code for displaying contact's avatar. + + @param category (quick_menus.MenuCategory): ignored + @return(unicode): HTML to display + """ + return '<img src="%s"/>' % C.DEFAULT_AVATAR_URL + + def setUrl(self, url): + """Set the URL of the contact avatar. + + @param url (unicode): avatar URL + """ + if not self.items: # the menu is empty but we've been asked to set an avatar + self.addCategory("dummy") + self.items[0].setHTML('<img src="%s" />' % url) + + +class ContactBox(VerticalPanel, ClickHandler, libervia_widget.DragLabel): + + def __init__(self, host, jid_, style_name=None, display=C.CONTACT_DEFAULT_DISPLAY, plugin_menu_context=None): + """ + @param host (SatWebFrontend): host instance + @param jid_ (jid.JID): contact JID + @param style_name (unicode): CSS style name + @param contacts_display (tuple): prioritize the display methods of the + contact's label with values in ("jid", "nick", "bare", "resource"). + @param plugin_menu_context (iterable): contexts of menus to have (list of C.MENU_* constant) + + """ + self.plugin_menu_context = [] if plugin_menu_context is None else plugin_menu_context + VerticalPanel.__init__(self, StyleName=style_name or 'contactBox', VerticalAlignment='middle') + ClickHandler.__init__(self) + libervia_widget.DragLabel.__init__(self, jid_, "CONTACT", host) + self.jid = jid_ + self.label = ContactLabel(host, self.jid, display=display) + self.avatar = ContactMenuBar(self, host) if plugin_menu_context else Image() + self.states = HTML() + self.add(self.avatar) + self.add(self.label) + self.add(self.states) + self.update() + self.addClickListener(self) + + def update(self): + """Update the display. + + @param with_bare (bool): if True, ignore the resource and update with bare information. + """ + self.avatar.setUrl(self.host.getAvatarURL(self.jid)) + + self.label.update() + clist = self.host.contact_list + show = clist.getCache(self.jid, C.PRESENCE_SHOW) + if show is None: + show = C.PRESENCE_UNAVAILABLE + html_tools.setPresenceStyle(self.label, show) + + def onClick(self, sender): + try: + self.parent.onClick(self.jid) + except (AttributeError, TypeError): + pass + +quick_menus.QuickMenusManager.addDataCollector(C.MENU_JID_CONTEXT, lambda caller, dummy: {'jid': unicode(caller.jid.bare)})
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/browser/sat_browser/dialog.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,616 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2011-2018 Jérôme Poisson <goffi@goffi.org> + +# This program 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. + +# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + +from sat.core.log import getLogger +log = getLogger(__name__) + +from constants import Const as C +from sat_frontends.tools import jid + +from pyjamas.ui.VerticalPanel import VerticalPanel +from pyjamas.ui.Grid import Grid +from pyjamas.ui.HorizontalPanel import HorizontalPanel +from pyjamas.ui.PopupPanel import PopupPanel +from pyjamas.ui.DialogBox import DialogBox +from pyjamas.ui.ListBox import ListBox +from pyjamas.ui.Button import Button +from pyjamas.ui.TextBox import TextBox +from pyjamas.ui.Label import Label +from pyjamas.ui.HTML import HTML +from pyjamas.ui.RadioButton import RadioButton +from pyjamas.ui import HasAlignment +from pyjamas.ui.KeyboardListener import KEY_ESCAPE, KEY_ENTER +from pyjamas.ui.MouseListener import MouseWheelHandler +from pyjamas import Window + +import base_panel + + +# List here the patterns that are not allowed in contact group names +FORBIDDEN_PATTERNS_IN_GROUP = () + + +unicode = str # XXX: pyjama doesn't manage unicode + + +class RoomChooser(Grid): + """Select a room from the rooms you already joined, or create a new one""" + + GENERATE_MUC = "<use random name>" + + def __init__(self, host, room_jid_s=None): + """ + + @param host (SatWebFrontend) + @param room_jid_s (unicode): room JID + """ + Grid.__init__(self, 2, 2, Width='100%') + self.host = host + + self.new_radio = RadioButton("room", "Discussion room:") + self.new_radio.setChecked(True) + self.box = TextBox(Width='95%') + self.box.setText(room_jid_s if room_jid_s else self.GENERATE_MUC) + self.exist_radio = RadioButton("room", "Already joined:") + self.rooms_list = ListBox(Width='95%') + + self.add(self.new_radio, 0, 0) + self.add(self.box, 0, 1) + self.add(self.exist_radio, 1, 0) + self.add(self.rooms_list, 1, 1) + + self.box.addFocusListener(self) + self.rooms_list.addFocusListener(self) + + self.exist_radio.setVisible(False) + self.rooms_list.setVisible(False) + self.refreshOptions() + + @property + def room(self): + """Get the room that has been selected or entered by the user + + @return: jid.JID or None to let the backend generate a new name + """ + if self.exist_radio.getChecked(): + values = self.rooms_list.getSelectedValues() + return jid.JID(values[0]) if values else None + value = self.box.getText() + return None if value in ('', self.GENERATE_MUC) else jid.JID(value) + + def onFocus(self, sender): + if sender == self.rooms_list: + self.exist_radio.setChecked(True) + elif sender == self.box: + if self.box.getText() == self.GENERATE_MUC: + self.box.setText("") + self.new_radio.setChecked(True) + + def onLostFocus(self, sender): + if sender == self.box: + if self.box.getText() == "": + self.box.setText(self.GENERATE_MUC) + + def refreshOptions(self): + """Refresh the already joined room list""" + contact_list = self.host.contact_list + muc_rooms = contact_list.getSpecials(C.CONTACT_SPECIAL_GROUP) + for room in muc_rooms: + self.rooms_list.addItem(room.bare) + if len(muc_rooms) > 0: + self.exist_radio.setVisible(True) + self.rooms_list.setVisible(True) + self.exist_radio.setChecked(True) + + +class ContactsChooser(VerticalPanel): + """Select one or several connected contacts""" + + def __init__(self, host, nb_contact=None, ok_button=None): + """ + @param host: SatWebFrontend instance + @param nb_contact: number of contacts that have to be selected, None for no limit + If a tuple is given instead of an integer, nb_contact[0] is the minimal and + nb_contact[1] is the maximal number of contacts to be chosen. + """ + self.host = host + if isinstance(nb_contact, tuple): + if len(nb_contact) == 0: + nb_contact = None + elif len(nb_contact) == 1: + nb_contact = (nb_contact[0], nb_contact[0]) + elif nb_contact is not None: + nb_contact = (nb_contact, nb_contact) + if nb_contact is None: + log.debug("Need to select as many contacts as you want") + else: + log.debug("Need to select between %d and %d contacts" % nb_contact) + self.nb_contact = nb_contact + self.ok_button = ok_button + VerticalPanel.__init__(self, Width='100%') + self.contacts_list = ListBox() + self.contacts_list.setMultipleSelect(True) + self.contacts_list.setWidth("95%") + self.contacts_list.addStyleName('contactsChooser') + self.contacts_list.addChangeListener(self.onChange) + self.add(self.contacts_list) + self.refreshOptions() + self.onChange() + + @property + def contacts(self): + """Return the selected contacts. + + @return: list[jid.JID] + """ + return [jid.JID(contact) for contact in self.contacts_list.getSelectedValues(True)] + + def onChange(self, sender=None): + if self.ok_button is None: + return + if self.nb_contact: + selected = len(self.contacts_list.getSelectedValues(True)) + if selected >= self.nb_contact[0] and selected <= self.nb_contact[1]: + self.ok_button.setEnabled(True) + else: + self.ok_button.setEnabled(False) + + def refreshOptions(self, keep_selected=False): + """Fill the list with the connected contacts. + + @param keep_selected (boolean): if True, keep the current selection + """ + selection = self.contacts if keep_selected else [] + self.contacts_list.clear() + contacts = self.host.contact_list.roster_connected + self.contacts_list.setVisibleItemCount(10 if len(contacts) > 5 else 5) + self.contacts_list.addItem("") + for contact in contacts: + self.contacts_list.addItem(contact) + if selection: + self.contacts_list.setItemTextSelection([unicode(contact) for contact in selection]) + + +class RoomAndContactsChooser(DialogBox): + """Select a room and some users to invite in""" + + def __init__(self, host, callback, nb_contact=None, ok_button="OK", title="Discussion groups", + title_room="Join room", title_invite="Invite contacts", visible=(True, True)): + DialogBox.__init__(self, centered=True) + self.host = host + self.callback = callback + self.title_room = title_room + self.title_invite = title_invite + + button_panel = HorizontalPanel() + button_panel.addStyleName("marginAuto") + ok_button = Button("OK", self.onOK) + button_panel.add(ok_button) + button_panel.add(Button("Cancel", self.onCancel)) + + self.room_panel = RoomChooser(host, None if visible == (False, True) else host.default_muc) + self.contact_panel = ContactsChooser(host, nb_contact, ok_button) + + self.stack_panel = base_panel.ToggleStackPanel(Width="100%") + self.stack_panel.add(self.room_panel, visible=visible[0]) + self.stack_panel.add(self.contact_panel, visible=visible[1]) + self.stack_panel.addStackChangeListener(self) + self.onStackChanged(self.stack_panel, 0, visible[0]) + self.onStackChanged(self.stack_panel, 1, visible[1]) + + main_panel = VerticalPanel() + main_panel.setStyleName("room-contact-chooser") + main_panel.add(self.stack_panel) + main_panel.add(button_panel) + + self.setWidget(main_panel) + self.setHTML(title) + self.show() + + # FIXME: workaround for a pyjamas issue: calling hash on a class method always return a different value if that method is defined directly within the class (with the "def" keyword) + self.presenceListener = self.refreshContactList + # update the contacts list when someone logged in/out + self.host.addListener('presence', self.presenceListener, [C.PROF_KEY_NONE]) + + @property + def room(self): + """Get the room that has been selected or entered by the user + + @return: jid.JID or None + """ + return self.room_panel.room + + @property + def contacts(self): + """Return the selected contacts. + + @return: list[jid.JID] + """ + return self.contact_panel.contacts + + def onStackChanged(self, sender, index, visible=None): + if visible is None: + visible = sender.getWidget(index).getVisible() + if index == 0: + suffix = "" if (visible or not self.room) else ": %s" % self.room + sender.setStackText(0, self.title_room + suffix) + elif index == 1: + suffix = "" if (visible or not self.contacts) else ": %s" % ", ".join([unicode(contact) for contact in self.contacts]) + sender.setStackText(1, self.title_invite + suffix) + + def refreshContactList(self, *args, **kwargs): + """Called when someone log in/out to update the list. + + @param args: set by the event call but not used here + """ + self.contact_panel.refreshOptions(keep_selected=True) + + def onOK(self, sender): + room = self.room # pyjamas issue: you need to use an intermediate variable to access a property's method + self.hide() + self.callback(room, self.contacts) + + def onCancel(self, sender): + self.hide() + + def hide(self): + self.host.removeListener('presence', self.presenceListener) + DialogBox.hide(self, autoClosed=True) + + +class GenericConfirmDialog(DialogBox): + + def __init__(self, widgets, callback, title='Confirmation', prompt_widgets=None, **kwargs): + """ + Dialog to confirm an action + @param widgets (list[Widget]): widgets to attach + @param callback (callable): method to call when a button is pressed, + with the following arguments: + - result (bool): set to True if the dialog has been confirmed + - *args: a list of unicode (the values for the prompt_widgets) + @param title: title of the dialog + @param prompt_widgets (list[TextBox]): input widgets from which to retrieve + the string value(s) to be passed to the callback when OK button is pressed. + If None, OK button will return "True". Cancel button always returns "False". + """ + self.callback = callback + added_style = kwargs.pop('AddStyleName', None) + DialogBox.__init__(self, centered=True, **kwargs) + if added_style: + self.addStyleName(added_style) + + if prompt_widgets is None: + prompt_widgets = [] + + content = VerticalPanel() + content.setWidth('100%') + for wid in widgets: + content.add(wid) + if wid in prompt_widgets: + wid.setWidth('100%') + button_panel = HorizontalPanel() + button_panel.addStyleName("marginAuto") + self.confirm_button = Button("OK", self.onConfirm) + button_panel.add(self.confirm_button) + self.cancel_button = Button("Cancel", self.onCancel) + button_panel.add(self.cancel_button) + content.add(button_panel) + self.setHTML(title) + self.setWidget(content) + self.prompt_widgets = prompt_widgets + + def onConfirm(self, sender): + self.hide() + result = [True] + result.extend([box.getText() for box in self.prompt_widgets]) + self.callback(*result) + + def onCancel(self, sender): + self.hide() + self.callback(False) + + def show(self): + DialogBox.show(self) + if self.prompt_widgets: + self.prompt_widgets[0].setFocus(True) + + +class ConfirmDialog(GenericConfirmDialog): + + def __init__(self, callback, text='Are you sure ?', title='Confirmation', **kwargs): + GenericConfirmDialog.__init__(self, [HTML(text)], callback, title, **kwargs) + + +class GenericDialog(DialogBox): + """Dialog which just show a widget and a close button""" + + def __init__(self, title, main_widget, callback=None, options=None, **kwargs): + """Simple notice dialog box + @param title: HTML put in the header + @param main_widget: widget put in the body + @param callback: method to call on closing + @param options: one or more of the following options: + - NO_CLOSE: don't add a close button""" + added_style = kwargs.pop('AddStyleName', None) + DialogBox.__init__(self, centered=True, **kwargs) + if added_style: + self.addStyleName(added_style) + + self.callback = callback + if not options: + options = [] + _body = VerticalPanel() + _body.setSize('100%', '100%') + _body.setSpacing(4) + _body.add(main_widget) + _body.setCellWidth(main_widget, '100%') + _body.setCellHeight(main_widget, '100%') + if 'NO_CLOSE' not in options: + _close_button = Button("Close", self.onClose) + _body.add(_close_button) + _body.setCellHorizontalAlignment(_close_button, HasAlignment.ALIGN_CENTER) + self.setHTML(title) + self.setWidget(_body) + self.panel.setSize('100%', '100%') # Need this hack to have correct size in Gecko & Webkit + + def close(self): + """Same effect as clicking the close button""" + self.onClose(None) + + def onClose(self, sender): + self.hide() + if self.callback: + self.callback() + + +class InfoDialog(GenericDialog): + + def __init__(self, title, body, callback=None, options=None, **kwargs): + GenericDialog.__init__(self, title, HTML(body), callback, options, **kwargs) + + +class PromptDialog(GenericConfirmDialog): + + def __init__(self, callback, textes=None, values=None, title='User input', **kwargs): + """Prompt the user for one or more input(s). + + @param callback (callable): method to call when a button is pressed, + with the following arguments: + - result (bool): set to True if the dialog has been confirmed + - *args: a list of unicode (the values entered by the user) + @param textes (list[unicode]): HTML textes to display before the inputs + @param values (list[unicode]): default values for each input + @param title (unicode): dialog title + """ + if textes is None: + textes = [''] # display a single input without any description + if values is None: + values = [] + all_widgets = [] + prompt_widgets = [] + for count in xrange(len(textes)): + all_widgets.append(HTML(textes[count])) + prompt = TextBox() + if len(values) > count: + prompt.setText(values[count]) + all_widgets.append(prompt) + prompt_widgets.append(prompt) + + GenericConfirmDialog.__init__(self, all_widgets, callback, title, prompt_widgets, **kwargs) + + +class PopupPanelWrapper(PopupPanel): + """This wrapper catch Escape event to avoid request cancellation by Firefox""" + + def onEventPreview(self, event): + if event.type in ["keydown", "keypress", "keyup"] and event.keyCode == KEY_ESCAPE: + # needed to prevent request cancellation in Firefox + event.preventDefault() + return PopupPanel.onEventPreview(self, event) + + +class ExtTextBox(TextBox): + """Extended TextBox""" + + def __init__(self, *args, **kwargs): + if 'enter_cb' in kwargs: + self.enter_cb = kwargs['enter_cb'] + del kwargs['enter_cb'] + TextBox.__init__(self, *args, **kwargs) + self.addKeyboardListener(self) + + def onKeyUp(self, sender, keycode, modifiers): + pass + + def onKeyDown(self, sender, keycode, modifiers): + pass + + def onKeyPress(self, sender, keycode, modifiers): + if self.enter_cb and keycode == KEY_ENTER: + self.enter_cb(self) + + +class GroupSelector(DialogBox): + + def __init__(self, top_widgets, initial_groups, selected_groups, + ok_title="OK", ok_cb=None, cancel_cb=None): + DialogBox.__init__(self, centered=True) + main_panel = VerticalPanel() + self.ok_cb = ok_cb + self.cancel_cb = cancel_cb + + for wid in top_widgets: + main_panel.add(wid) + + main_panel.add(Label('Select in which groups your contact is:')) + self.list_box = ListBox() + self.list_box.setMultipleSelect(True) + self.list_box.setVisibleItemCount(5) + self.setAvailableGroups(initial_groups) + self.setGroupsSelected(selected_groups) + main_panel.add(self.list_box) + + def cb(text): + self.list_box.addItem(text) + self.list_box.setItemSelected(self.list_box.getItemCount() - 1, "selected") + + main_panel.add(AddGroupPanel(initial_groups, cb)) + + button_panel = HorizontalPanel() + button_panel.addStyleName("marginAuto") + button_panel.add(Button(ok_title, self.onOK)) + button_panel.add(Button("Cancel", self.onCancel)) + main_panel.add(button_panel) + + self.setWidget(main_panel) + + def getSelectedGroups(self): + """Return a list of selected groups""" + return self.list_box.getSelectedValues() + + def setAvailableGroups(self, groups): + _groups = list(set(groups)) + _groups.sort() + self.list_box.clear() + for group in _groups: + self.list_box.addItem(group) + + def setGroupsSelected(self, selected_groups): + self.list_box.setItemTextSelection(selected_groups) + + def onOK(self, sender): + self.hide() + if self.ok_cb: + self.ok_cb(self) + + def onCancel(self, sender): + self.hide() + if self.cancel_cb: + self.cancel_cb(self) + + +class AddGroupPanel(HorizontalPanel): + def __init__(self, groups, cb=None): + """ + @param groups: list of the already existing groups + """ + HorizontalPanel.__init__(self) + self.groups = groups + self.add(Label('New group:')) + self.textbox = ExtTextBox(enter_cb=self.onGroupInput) + self.add(self.textbox) + self.add(Button("Add", lambda sender: self.onGroupInput(self.textbox))) + self.cb = cb + + def onGroupInput(self, sender): + text = sender.getText() + if text == "": + return + for group in self.groups: + if text == group: + Window.alert("The group '%s' already exists." % text) + return + for pattern in FORBIDDEN_PATTERNS_IN_GROUP: + if pattern in text: + Window.alert("The pattern '%s' is not allowed in group names." % pattern) + return + sender.setText('') + self.groups.append(text) + if self.cb is not None: + self.cb(text) + + +class WheelTextBox(TextBox, MouseWheelHandler): + + def __init__(self, *args, **kwargs): + TextBox.__init__(self, *args, **kwargs) + MouseWheelHandler.__init__(self) + + +class IntSetter(HorizontalPanel): + """This class show a bar with button to set an int value""" + + def __init__(self, label, value=0, value_max=None, visible_len=3): + """initialize the intSetter + @param label: text shown in front of the setter + @param value: initial value + @param value_max: limit value, None or 0 for unlimited""" + HorizontalPanel.__init__(self) + self.value = value + self.value_max = value_max + _label = Label(label) + self.add(_label) + self.setCellWidth(_label, "100%") + minus_button = Button("-", self.onMinus) + self.box = WheelTextBox() + self.box.setVisibleLength(visible_len) + self.box.setText(unicode(value)) + self.box.addInputListener(self) + self.box.addMouseWheelListener(self) + plus_button = Button("+", self.onPlus) + self.add(minus_button) + self.add(self.box) + self.add(plus_button) + self.valueChangedListener = [] + + def addValueChangeListener(self, listener): + self.valueChangedListener.append(listener) + + def removeValueChangeListener(self, listener): + if listener in self.valueChangedListener: + self.valueChangedListener.remove(listener) + + def _callListeners(self): + for listener in self.valueChangedListener: + listener(self.value) + + def setValue(self, value): + """Change the value and fire valueChange listeners""" + self.value = value + self.box.setText(unicode(value)) + self._callListeners() + + def onMinus(self, sender, step=1): + self.value = max(0, self.value - step) + self.box.setText(unicode(self.value)) + self._callListeners() + + def onPlus(self, sender, step=1): + self.value += step + if self.value_max: + self.value = min(self.value, self.value_max) + self.box.setText(unicode(self.value)) + self._callListeners() + + def onInput(self, sender): + """Accept only valid integer && normalize print (no leading 0)""" + try: + self.value = int(self.box.getText()) if self.box.getText() else 0 + except ValueError: + pass + if self.value_max: + self.value = min(self.value, self.value_max) + self.box.setText(unicode(self.value)) + self._callListeners() + + def onMouseWheel(self, sender, velocity): + if velocity > 0: + self.onMinus(sender, 10) + else: + self.onPlus(sender, 10)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/browser/sat_browser/editor_widget.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,380 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2011-2018 Jérôme Poisson <goffi@goffi.org> + +# This program 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. + +# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + +from sat.core.log import getLogger +log = getLogger(__name__) +from sat_browser import strings + +from pyjamas.ui.HTML import HTML +from pyjamas.ui.SimplePanel import SimplePanel +from pyjamas.ui.TextArea import TextArea +from pyjamas.ui import KeyboardListener as keyb +from pyjamas.ui.FocusListener import FocusHandler +from pyjamas.ui.ClickListener import ClickHandler +from pyjamas.ui.MouseListener import MouseHandler +from pyjamas.Timer import Timer +from pyjamas import DOM + +import html_tools + + +class MessageBox(TextArea): + """A basic text area for entering messages""" + + def __init__(self, host): + TextArea.__init__(self) + self.host = host + self.size = (0, 0) + self.setStyleName('messageBox') + self.addKeyboardListener(self) + MouseHandler.__init__(self) + self.addMouseListener(self) + + def onBrowserEvent(self, event): + # XXX: woraroung a pyjamas bug: self.currentEvent is not set + # so the TextBox's cancelKey doens't work. This is a workaround + # FIXME: fix the bug upstream + self.currentEvent = event + TextArea.onBrowserEvent(self, event) + + def onKeyPress(self, sender, keycode, modifiers): + _txt = self.getText() + + def history_cb(text): + self.setText(text) + Timer(5, lambda timer: self.setCursorPos(len(text))) + + if keycode == keyb.KEY_ENTER: + if _txt: + self.host.selected_widget.onTextEntered(_txt) + self.host._updateInputHistory(_txt) # FIXME: why using a global variable ? + self.setText('') + sender.cancelKey() + elif keycode == keyb.KEY_UP: + self.host._updateInputHistory(_txt, -1, history_cb) + elif keycode == keyb.KEY_DOWN: + self.host._updateInputHistory(_txt, +1, history_cb) + else: + self._onComposing() + + def _onComposing(self): + """Callback when the user is composing a text.""" + self.host.selected_widget.chat_state_machine._onEvent("composing") + + def onMouseUp(self, sender, x, y): + size = (self.getOffsetWidth(), self.getOffsetHeight()) + if size != self.size: + self.size = size + self.host.resize() + + def onSelectedChange(self, selected): + self._selected_cache = selected + + +class BaseTextEditor(object): + """Basic definition of a text editor. The method edit gets a boolean parameter which + should be set to True when you want to edit the text and False to only display it.""" + + def __init__(self, content=None, strproc=None, modifiedCb=None, afterEditCb=None): + """ + Remark when inheriting this class: since the setContent method could be + overwritten by the child class, you should consider calling this __init__ + after all the parameters affecting this setContent method have been set. + @param content: dict with at least a 'text' key + @param strproc: method to be applied on strings to clean the content + @param modifiedCb: method to be called when the text has been modified. + This method can return: + - True: the modification will be saved and afterEditCb called; + - False: the modification won't be saved and afterEditCb called; + - None: the modification won't be saved and afterEditCb not called. + @param afterEditCb: method to be called when the edition is done + """ + if content is None: + content = {'text': ''} + assert 'text' in content + if strproc is None: + def strproc(text): + try: + return text.strip() + except (TypeError, AttributeError): + return text + self.strproc = strproc + self._modifiedCb = modifiedCb + self._afterEditCb = afterEditCb + self.initialized = False + self.edit_listeners = [] + self.setContent(content) + + def setContent(self, content=None): + """Set the editable content. + The displayed content, which is set from the child class, could differ. + + @param content (dict): content data, need at least a 'text' key + """ + if content is None: + content = {'text': ''} + elif not isinstance(content, dict): + content = {'text': content} + assert 'text' in content + self._original_content = {} + for key in content: + self._original_content[key] = self.strproc(content[key]) + + def getContent(self): + """Get the current edited or editable content. + @return: dict with at least a 'text' key + """ + raise NotImplementedError + + def setOriginalContent(self, content): + """Use this method with care! Content initialization should normally be + done with self.setContent. This method exists to let you trick the editor, + e.g. for self.modified to return True also when nothing has been modified. + @param content: dict + """ + self._original_content = content + + def getOriginalContent(self): + """ + @return (dict): the original content before modification (i.e. content given in __init__) + """ + return self._original_content + + def modified(self, content=None): + """Check if the content has been modified. + Remark: we don't use the direct comparison because we want to ignore empty elements + @content: content to be check against the original content or None to use the current content + @return: True if the content has been modified. + """ + if content is None: + content = self.getContent() + # the following method returns True if one non empty element exists in a but not in b + diff1 = lambda a, b: [a[key] for key in set(a.keys()).difference(b.keys()) if a[key]] != [] + # the following method returns True if the values for the common keys are not equals + diff2 = lambda a, b: [1 for key in set(a.keys()).intersection(b.keys()) if a[key] != b[key]] != [] + # finally the combination of both to return True if a difference is found + diff = lambda a, b: diff1(a, b) or diff1(b, a) or diff2(a, b) + + return diff(content, self._original_content) + + def edit(self, edit, abort=False): + """ + Remark: the editor must be visible before you call this method. + @param edit: set to True to edit the content or False to only display it + @param abort: set to True to cancel the edition and loose the changes. + If edit and abort are both True, self.abortEdition can be used to ask for a + confirmation. When edit is False and abort is True, abortion is actually done. + """ + if edit: + self.setFocus(True) + if abort: + content = self.getContent() + if not self.modified(content) or self.abortEdition(content): # e.g: ask for confirmation + self.edit(False, True) + return + else: + if not self.initialized: + return + content = self.getContent() + if abort: + self._afterEditCb(content) + return + if self._modifiedCb and self.modified(content): + result = self._modifiedCb(content) # e.g.: send a message or update something + if result is not None: + if self._afterEditCb: + self._afterEditCb(content) # e.g.: restore the display mode + if result is True: + self.setContent(content) + elif self._afterEditCb: + self._afterEditCb(content) + + self.initialized = True + + def setFocus(self, focus): + """ + @param focus: set to True to focus the editor + """ + raise NotImplementedError + + def abortEdition(self, content): + return True + + def addEditListener(self, listener): + """Add a method to be called whenever the text is edited. + @param listener: method taking two arguments: sender, keycode""" + self.edit_listeners.append(listener) + + +class SimpleTextEditor(BaseTextEditor, FocusHandler, keyb.KeyboardHandler, ClickHandler): + """Base class for manage a simple text editor.""" + + CONVERT_NEW_LINES = True + VALIDATE_WITH_SHIFT_ENTER = True + + def __init__(self, content=None, modifiedCb=None, afterEditCb=None, options=None): + """ + @param content + @param modifiedCb + @param afterEditCb + @param options (dict): can have the following value: + - no_xhtml: set to True to clean any xhtml content. + - enhance_display: if True, the display text will be enhanced with strings.addURLToText + - listen_keyboard: set to True to terminate the edition with <enter> or <escape>. + - listen_focus: set to True to terminate the edition when the focus is lost. + - listen_click: set to True to start the edition when you click on the widget. + """ + self.options = {'no_xhtml': False, + 'enhance_display': True, + 'listen_keyboard': True, + 'listen_focus': False, + 'listen_click': False + } + if options: + self.options.update(options) + if self.options['listen_focus']: + FocusHandler.__init__(self) + if self.options['listen_click']: + ClickHandler.__init__(self) + keyb.KeyboardHandler.__init__(self) + strproc = lambda text: html_tools.html_sanitize(html_tools.html_strip(text)) if self.options['no_xhtml'] else html_tools.html_strip(text) + BaseTextEditor.__init__(self, content, strproc, modifiedCb, afterEditCb) + self.textarea = self.display = None + + def setContent(self, content=None): + BaseTextEditor.setContent(self, content) + + def getContent(self): + raise NotImplementedError + + def edit(self, edit, abort=False): + BaseTextEditor.edit(self, edit) + if edit: + if self.options['listen_focus'] and self not in self.textarea._focusListeners: + self.textarea.addFocusListener(self) + if self.options['listen_click']: + self.display.clearClickListener() + if self not in self.textarea._keyboardListeners: + self.textarea.addKeyboardListener(self) + else: + self.setDisplayContent() + if self.options['listen_focus']: + try: + self.textarea.removeFocusListener(self) + except ValueError: + pass + if self.options['listen_click'] and self not in self.display._clickListeners: + self.display.addClickListener(self) + try: + self.textarea.removeKeyboardListener(self) + except ValueError: + pass + + def setDisplayContent(self): + text = self._original_content['text'] + if not self.options['no_xhtml']: + text = strings.addURLToImage(text) + if self.options['enhance_display']: + text = strings.addURLToText(text) + if self.CONVERT_NEW_LINES: + text = html_tools.convertNewLinesToXHTML(text) + text = strings.fixXHTMLLinks(text) + self.display.setHTML(text) + + def setFocus(self, focus): + raise NotImplementedError + + def onKeyDown(self, sender, keycode, modifiers): + for listener in self.edit_listeners: + listener(self.textarea, keycode, modifiers) # FIXME: edit_listeners must either be removed, or send an action instead of keycode/modifiers + if not self.options['listen_keyboard']: + return + if keycode == keyb.KEY_ENTER and (not self.VALIDATE_WITH_SHIFT_ENTER or modifiers & keyb.MODIFIER_SHIFT): + self.textarea.setFocus(False) + if not self.options['listen_focus']: + self.edit(False) + + def onLostFocus(self, sender): + """Finish the edition when focus is lost""" + if self.options['listen_focus']: + self.edit(False) + + def onClick(self, sender=None): + """Start the edition when the widget is clicked""" + if self.options['listen_click']: + self.edit(True) + + def onBrowserEvent(self, event): + if self.options['listen_focus']: + FocusHandler.onBrowserEvent(self, event) + if self.options['listen_click']: + ClickHandler.onBrowserEvent(self, event) + keyb.KeyboardHandler.onBrowserEvent(self, event) + + +class HTMLTextEditor(SimpleTextEditor, HTML, FocusHandler, keyb.KeyboardHandler): + """Manage a simple text editor with the HTML 5 "contenteditable" property.""" + + CONVERT_NEW_LINES = False # overwrite definition in SimpleTextEditor + + def __init__(self, content=None, modifiedCb=None, afterEditCb=None, options=None): + HTML.__init__(self) + SimpleTextEditor.__init__(self, content, modifiedCb, afterEditCb, options) + self.textarea = self.display = self + + def getContent(self): + text = DOM.getInnerHTML(self.getElement()) + return {'text': self.strproc(text) if text else ''} + + def edit(self, edit, abort=False): + if edit: + self.textarea.setHTML(self._original_content['text']) + self.getElement().setAttribute('contenteditable', 'true' if edit else 'false') + SimpleTextEditor.edit(self, edit, abort) + + def setFocus(self, focus): + if focus: + self.getElement().focus() + else: + self.getElement().blur() + + +class LightTextEditor(SimpleTextEditor, SimplePanel, FocusHandler, keyb.KeyboardHandler): + """Manage a simple text editor with a TextArea for editing, HTML for display.""" + + def __init__(self, content=None, modifiedCb=None, afterEditCb=None, options=None): + SimplePanel.__init__(self) + SimpleTextEditor.__init__(self, content, modifiedCb, afterEditCb, options) + self.textarea = TextArea() + self.display = HTML() + + def getContent(self): + text = self.textarea.getText() + return {'text': self.strproc(text) if text else ''} + + def edit(self, edit, abort=False): + if edit: + self.textarea.setText(self._original_content['text']) + self.setWidget(self.textarea if edit else self.display) + SimpleTextEditor.edit(self, edit, abort) + + def setFocus(self, focus): + if focus and self.isAttached(): + self.textarea.setCursorPos(len(self.textarea.getText())) + self.textarea.setFocus(focus)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/browser/sat_browser/file_tools.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,163 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2011-2018 Jérôme Poisson <goffi@goffi.org> + +# This program 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. + +# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + +from sat.core.log import getLogger +log = getLogger(__name__) +from constants import Const as C +from sat.core.i18n import _, D_ +from pyjamas.ui.FileUpload import FileUpload +from pyjamas.ui.FormPanel import FormPanel +from pyjamas import Window +from pyjamas import DOM +from pyjamas.ui.VerticalPanel import VerticalPanel +from pyjamas.ui.HTML import HTML +from pyjamas.ui.HorizontalPanel import HorizontalPanel +from pyjamas.ui.Button import Button +from pyjamas.ui.Label import Label + + +class FilterFileUpload(FileUpload): + + def __init__(self, name, max_size, types=None): + """ + @param name: the input element name and id + @param max_size: maximum file size in MB + @param types: allowed types as a list of couples (x, y, z): + - x: MIME content type e.g. "audio/ogg" + - y: file extension e.g. "*.ogg" + - z: description for the user e.g. "Ogg Vorbis Audio" + If types is None, all file format are accepted + """ + FileUpload.__init__(self) + self.setName(name) + while DOM.getElementById(name): + name = "%s_" % name + self.setID(name) + self._id = name + self.max_size = max_size + self.types = types + + def getFileInfo(self): + from __pyjamas__ import JS + JS("var file = top.document.getElementById(this._id).files[0]; return [file.size, file.type]") + + def check(self): + if self.getFilename() == "": + return False + (size, filetype) = self.getFileInfo() + if self.types and filetype not in [x for (x, y, z) in self.types]: + types = [] + for type_ in ["- %s (%s)" % (z, y) for (x, y, z) in self.types]: + if type_ not in types: + types.append(type_) + Window.alert('This file type is not accepted.\nAccepted file types are:\n\n%s' % "\n".join(types)) + return False + if size > self.max_size * pow(2, 20): + Window.alert('This file is too big!\nMaximum file size: %d MB.' % self.max_size) + return False + return True + + +class FileUploadPanel(FormPanel): + + def __init__(self, action_url, input_id, max_size, texts=None, close_cb=None): + """Build a form panel to upload a file. + @param action_url: the form action URL + @param input_id: the input element name and id + @param max_size: maximum file size in MB + @param texts: a dict to ovewrite the default textual values + @param close_cb: the close button callback method + """ + FormPanel.__init__(self) + self.texts = {'ok_button': D_('Upload file'), + 'cancel_button': D_('Cancel'), + 'body': D_('Please select a file.'), + 'submitting': D_('<strong>Submitting, please wait...</strong>'), + 'errback': D_("Your file has been rejected..."), + 'body_errback': D_('Please select another file.'), + 'callback': D_("Your file has been accepted!")} + if isinstance(texts, dict): + self.texts.update(texts) + self.close_cb = close_cb + self.setEncoding(FormPanel.ENCODING_MULTIPART) + self.setMethod(FormPanel.METHOD_POST) + self.setAction(action_url) + self.vPanel = VerticalPanel() + self.message = HTML(self.texts['body']) + self.vPanel.add(self.message) + + hPanel = HorizontalPanel() + hPanel.setSpacing(5) + hPanel.setStyleName('marginAuto') + self.file_upload = FilterFileUpload(input_id, max_size) + self.vPanel.add(self.file_upload) + + self.upload_btn = Button(self.texts['ok_button'], getattr(self, "onSubmitBtnClick")) + hPanel.add(self.upload_btn) + hPanel.add(Button(self.texts['cancel_button'], getattr(self, "onCloseBtnClick"))) + + self.status = Label() + hPanel.add(self.status) + + self.vPanel.add(hPanel) + + self.add(self.vPanel) + self.addFormHandler(self) + + def setCloseCb(self, close_cb): + self.close_cb = close_cb + + def onCloseBtnClick(self): + if self.close_cb: + self.close_cb() + else: + log.warning("no close method defined") + + def onSubmitBtnClick(self): + if not self.file_upload.check(): + return + self.message.setHTML(self.texts['submitting']) + self.upload_btn.setEnabled(False) + self.submit() + + def onSubmit(self, event): + pass + + def onSubmitComplete(self, event): + result = event.getResults() + if result == C.UPLOAD_KO: + Window.alert(self.texts['errback']) + self.message.setHTML(self.texts['body_errback']) + self.upload_btn.setEnabled(True) + elif result == C.UPLOAD_OK: + Window.alert(self.texts['callback']) + self.close_cb() + else: + Window.alert(_('Submit error: %s' % result)) + self.upload_btn.setEnabled(True) + + +class AvatarUpload(FileUploadPanel): + def __init__(self): + texts = {'ok_button': 'Upload avatar', + 'body': 'Please select an image to show as your avatar...<br>Your picture must be a square and will be resized to 64x64 pixels if necessary.', + 'errback': "Can't open image... did you actually submit an image?", + 'body_errback': 'Please select another image file.', + 'callback': "Your new profile picture has been set!"} + FileUploadPanel.__init__(self, 'upload_avatar', 'avatar_path', 2, texts)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/browser/sat_browser/game_radiocol.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,347 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2011-2018 Jérôme Poisson <goffi@goffi.org> + +# This program 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. + +# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + +import pyjd # this is dummy in pyjs +from sat.core.log import getLogger +log = getLogger(__name__) + +from sat.core.i18n import _, D_ +from sat_frontends.tools import host_listener +from constants import Const as C + +from pyjamas.ui.VerticalPanel import VerticalPanel +from pyjamas.ui.HorizontalPanel import HorizontalPanel +from pyjamas.ui.FlexTable import FlexTable +from pyjamas.ui.FormPanel import FormPanel +from pyjamas.ui.Label import Label +from pyjamas.ui.Button import Button +from pyjamas.ui.ClickListener import ClickHandler +from pyjamas.ui.Hidden import Hidden +from pyjamas.ui.CaptionPanel import CaptionPanel +from pyjamas.media.Audio import Audio +from pyjamas import Window +from pyjamas.Timer import Timer + +import html_tools +import file_tools +import dialog + + +unicode = str # XXX: pyjama doesn't manage unicode + + +class MetadataPanel(FlexTable): + + def __init__(self): + FlexTable.__init__(self) + title_lbl = Label("title:") + title_lbl.setStyleName('radiocol_metadata_lbl') + artist_lbl = Label("artist:") + artist_lbl.setStyleName('radiocol_metadata_lbl') + album_lbl = Label("album:") + album_lbl.setStyleName('radiocol_metadata_lbl') + self.title = Label("") + self.title.setStyleName('radiocol_metadata') + self.artist = Label("") + self.artist.setStyleName('radiocol_metadata') + self.album = Label("") + self.album.setStyleName('radiocol_metadata') + self.setWidget(0, 0, title_lbl) + self.setWidget(1, 0, artist_lbl) + self.setWidget(2, 0, album_lbl) + self.setWidget(0, 1, self.title) + self.setWidget(1, 1, self.artist) + self.setWidget(2, 1, self.album) + self.setStyleName("radiocol_metadata_pnl") + + def setTitle(self, title): + self.title.setText(title) + + def setArtist(self, artist): + self.artist.setText(artist) + + def setAlbum(self, album): + self.album.setText(album) + + +class ControlPanel(FormPanel): + """Panel used to show controls to add a song, or vote for the current one""" + + def __init__(self, parent): + FormPanel.__init__(self) + self.setEncoding(FormPanel.ENCODING_MULTIPART) + self.setMethod(FormPanel.METHOD_POST) + self.setAction("upload_radiocol") + self.timer_on = False + self._parent = parent + vPanel = VerticalPanel() + + types = [('audio/ogg', '*.ogg', 'Ogg Vorbis Audio'), + ('video/ogg', '*.ogv', 'Ogg Vorbis Video'), + ('application/ogg', '*.ogx', 'Ogg Vorbis Multiplex'), + ('audio/mpeg', '*.mp3', 'MPEG-Layer 3'), + ('audio/mp3', '*.mp3', 'MPEG-Layer 3'), + ] + self.file_upload = file_tools.FilterFileUpload("song", 10, types) + vPanel.add(self.file_upload) + + hPanel = HorizontalPanel() + self.upload_btn = Button("Upload song", getattr(self, "onBtnClick")) + hPanel.add(self.upload_btn) + self.status = Label() + self.updateStatus() + hPanel.add(self.status) + #We need to know the filename and the referee + self.filename_field = Hidden('filename', '') + hPanel.add(self.filename_field) + referee_field = Hidden('referee', self._parent.referee) + hPanel.add(self.filename_field) + hPanel.add(referee_field) + vPanel.add(hPanel) + + self.add(vPanel) + self.addFormHandler(self) + + def updateStatus(self): + if self.timer_on: + return + # TODO: the status should be different if a song is being played or not + queue = self._parent.getQueueSize() + queue_data = self._parent.queue_data + if queue < queue_data[0]: + left = queue_data[0] - queue + self.status.setText("[we need %d more song%s]" % (left, "s" if left > 1 else "")) + elif queue < queue_data[1]: + left = queue_data[1] - queue + self.status.setText("[%d available spot%s]" % (left, "s" if left > 1 else "")) + elif queue >= queue_data[1]: + self.status.setText("[The queue is currently full]") + self.status.setStyleName('radiocol_status') + + def onBtnClick(self): + if self.file_upload.check(): + self.status.setText('[Submitting, please wait...]') + self.filename_field.setValue(self.file_upload.getFilename()) + if self.file_upload.getFilename().lower().endswith('.mp3'): + self._parent._parent.host.showWarning('STATUS', 'For a better support, it is recommended to submit Ogg Vorbis file instead of MP3. You can convert your files easily, ask for help if needed!', 5000) + self.submit() + self.file_upload.setFilename("") + + def onSubmit(self, event): + pass + + def blockUpload(self): + self.file_upload.setVisible(False) + self.upload_btn.setEnabled(False) + + def unblockUpload(self): + self.file_upload.setVisible(True) + self.upload_btn.setEnabled(True) + + def setTemporaryStatus(self, text, style): + self.status.setText(text) + self.status.setStyleName('radiocol_upload_status_%s' % style) + self.timer_on = True + + def cb(timer): + self.timer_on = False + self.updateStatus() + + Timer(5000, cb) + + def onSubmitComplete(self, event): + result = event.getResults() + if result == C.UPLOAD_OK: + # the song can still be rejected (not readable, full queue...) + self.setTemporaryStatus('[Your song has been submitted to the radio]', "ok") + elif result == C.UPLOAD_KO: + self.setTemporaryStatus('[Something went wrong during your song upload]', "ko") + self._parent.radiocolSongRejectedHandler(_("The uploaded file has been rejected, only Ogg Vorbis and MP3 songs are accepted.")) + # TODO: would be great to re-use the original Exception class and message + # but it is lost in the middle of the traceback and encapsulated within + # a DBusException instance --> extract the data from the traceback? + else: + Window.alert(_('Submit error: %s' % result)) + self.status.setText('') + + +class Player(Audio): + + def __init__(self, player_id, metadata_panel): + Audio.__init__(self) + self._id = player_id + self.metadata = metadata_panel + self.timestamp = "" + self.title = "" + self.artist = "" + self.album = "" + self.filename = None + self.played = False # True when the song is playing/has played, becomes False on preload + self.setAutobuffer(True) + self.setAutoplay(False) + self.setVisible(False) + + def preload(self, timestamp, filename, title, artist, album): + """preload the song but doesn't play it""" + self.timestamp = timestamp + self.filename = filename + self.title = title + self.artist = artist + self.album = album + self.played = False + self.setSrc(u"radiocol/%s" % html_tools.html_sanitize(filename)) + log.debug(u"preloading %s in %s" % (title, self._id)) + + def play(self, play=True): + """Play or pause the song + @param play: set to True to play or to False to pause + """ + if play: + self.played = True + self.metadata.setTitle(self.title) + self.metadata.setArtist(self.artist) + self.metadata.setAlbum(self.album) + Audio.play(self) + else: + self.pause() + + +class RadioColPanel(HorizontalPanel, ClickHandler): + + def __init__(self, parent, referee, players, queue_data): + """ + @param parent + @param referee + @param players + @param queue_data: list of integers (queue to start, queue limit) + """ + # We need to set it here and not in the CSS :( + HorizontalPanel.__init__(self, Height="90px") + ClickHandler.__init__(self) + self._parent = parent + self.referee = referee + self.queue_data = queue_data + self.setStyleName("radiocolPanel") + + # Now we set up the layout + self.metadata_panel = MetadataPanel() + self.add(CaptionPanel("Now playing", self.metadata_panel)) + self.playlist_panel = VerticalPanel() + self.add(CaptionPanel("Songs queue", self.playlist_panel)) + self.control_panel = ControlPanel(self) + self.add(CaptionPanel("Controls", self.control_panel)) + + self.next_songs = [] + self.players = [Player("player_%d" % i, self.metadata_panel) for i in xrange(queue_data[1] + 1)] + self.current_player = None + for player in self.players: + self.add(player) + self.addClickListener(self) + + help_msg = """Accepted file formats: Ogg Vorbis (recommended), MP3.<br /> + Please do not submit files that are protected by copyright.<br /> + Click <a style="color: red;">here</a> if you need some support :)""" + link_cb = lambda: self._parent.host.bridge.joinMUC(self._parent.host.default_muc, self._parent.nick, profile=C.PROF_KEY_NONE, callback=lambda dummy: None, errback=self._parent.host.onJoinMUCFailure) + # FIXME: printInfo disabled after refactoring + # self._parent.printInfo(help_msg, type_='link', link_cb=link_cb) + + def pushNextSong(self, title): + """Add a song to the left panel's next songs queue""" + next_song = Label(title) + next_song.setStyleName("radiocol_next_song") + self.next_songs.append(next_song) + self.playlist_panel.append(next_song) + self.control_panel.updateStatus() + + def popNextSong(self): + """Remove the first song of next songs list + should be called when the song is played""" + #FIXME: should check that the song we remove is the one we play + next_song = self.next_songs.pop(0) + self.playlist_panel.remove(next_song) + self.control_panel.updateStatus() + + def getQueueSize(self): + return len(self.playlist_panel.getChildren()) + + def radiocolCheckPreload(self, timestamp): + for player in self.players: + if player.timestamp == timestamp: + return False + return True + + def radiocolPreloadHandler(self, timestamp, filename, title, artist, album, sender): + if not self.radiocolCheckPreload(timestamp): + return # song already preloaded + preloaded = False + for player in self.players: + if not player.filename or \ + (player.played and player != self.current_player): + #if player has no file loaded, or it has already played its song + #we use it to preload the next one + player.preload(timestamp, filename, title, artist, album) + preloaded = True + break + if not preloaded: + log.warning("Can't preload song, we are getting too many songs to preload, we shouldn't have more than %d at once" % self.queue_data[1]) + else: + self.pushNextSong(title) + # FIXME: printInfo disabled after refactoring + # self._parent.printInfo(_('%(user)s uploaded %(artist)s - %(title)s') % {'user': sender, 'artist': artist, 'title': title}) + + def radiocolPlayHandler(self, filename): + found = False + for player in self.players: + if not found and player.filename == filename: + player.play() + self.popNextSong() + self.current_player = player + found = True + else: + player.play(False) # in case the previous player was not sync + if not found: + log.error("Song not found in queue, can't play it. This should not happen") + + def radiocolNoUploadHandler(self): + self.control_panel.blockUpload() + + def radiocolUploadOkHandler(self): + self.control_panel.unblockUpload() + + def radiocolSongRejectedHandler(self, reason): + Window.alert("Song rejected: %s" % reason) + + +## Menu + +def hostReady(host): + def onCollectiveRadio(self): + def callback(room_jid, contacts): + contacts = [unicode(contact) for contact in contacts] + room_jid_s = unicode(room_jid) if room_jid else '' + host.bridge.launchRadioCollective(contacts, room_jid_s, profile=C.PROF_KEY_NONE, callback=lambda dummy: None, errback=host.onJoinMUCFailure) + dialog.RoomAndContactsChooser(host, callback, ok_button="Choose", title="Collective Radio", visible=(False, True)) + + + def gotMenus(): + host.menus.addMenu(C.MENU_GLOBAL, (D_(u"Groups"), D_(u"Collective radio")), callback=onCollectiveRadio) + + host.addListener('gotMenus', gotMenus) + +host_listener.addListener(hostReady)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/browser/sat_browser/game_tarot.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,410 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2011-2018 Jérôme Poisson <goffi@goffi.org> + +# This program 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. + +# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + +import pyjd # this is dummy in pyjs +from sat.core.log import getLogger +log = getLogger(__name__) + +from sat.core.i18n import _, D_ +from sat_frontends.tools.games import TarotCard +from sat_frontends.tools import host_listener + +from pyjamas.ui.AbsolutePanel import AbsolutePanel +from pyjamas.ui.DockPanel import DockPanel +from pyjamas.ui.SimplePanel import SimplePanel +from pyjamas.ui.Image import Image +from pyjamas.ui.Label import Label +from pyjamas.ui.ClickListener import ClickHandler +from pyjamas.ui.MouseListener import MouseHandler +from pyjamas.ui import HasAlignment +from pyjamas import Window +from pyjamas import DOM +from constants import Const as C + +import dialog +import xmlui + + +CARD_WIDTH = 74 +CARD_HEIGHT = 136 +CARD_DELTA_Y = 30 +MIN_WIDTH = 950 # Minimum size of the panel +MIN_HEIGHT = 500 + + +unicode = str # XXX: pyjama doesn't manage unicode + + +class CardWidget(TarotCard, Image, MouseHandler): + """This class is used to represent a card, graphically and logically""" + + def __init__(self, parent, file_): + """@param file: path of the PNG file""" + self._parent = parent + Image.__init__(self, file_) + root_name = file_[file_.rfind("/") + 1:-4] + suit, value = root_name.split('_') + TarotCard.__init__(self, (suit, value)) + MouseHandler.__init__(self) + self.addMouseListener(self) + + def onMouseEnter(self, sender): + if self._parent.state == "ecart" or self._parent.state == "play": + DOM.setStyleAttribute(self.getElement(), "top", "0px") + + def onMouseLeave(self, sender): + if not self in self._parent.hand: + return + if not self in list(self._parent.selected): # FIXME: Workaround pyjs bug, must report it + DOM.setStyleAttribute(self.getElement(), "top", "%dpx" % CARD_DELTA_Y) + + def onMouseUp(self, sender, x, y): + if self._parent.state == "ecart": + if self not in list(self._parent.selected): + self._parent.addToSelection(self) + else: + self._parent.removeFromSelection(self) + elif self._parent.state == "play": + self._parent.playCard(self) + + +class TarotPanel(DockPanel, ClickHandler): + + def __init__(self, parent, referee, players): + DockPanel.__init__(self) + ClickHandler.__init__(self) + self._parent = parent + self._autoplay = None # XXX: use 0 to activate fake play, None else + self.referee = referee + self.players = players + self.player_nick = parent.nick + self.bottom_nick = self.player_nick + idx = self.players.index(self.player_nick) + idx = (idx + 1) % len(self.players) + self.right_nick = self.players[idx] + idx = (idx + 1) % len(self.players) + self.top_nick = self.players[idx] + idx = (idx + 1) % len(self.players) + self.left_nick = self.players[idx] + self.bottom_nick = self.player_nick + self.selected = set() # Card choosed by the player (e.g. during ecart) + self.hand_size = 13 # number of cards in a hand + self.hand = [] + self.to_show = [] + self.state = None + self.setSize("%dpx" % MIN_WIDTH, "%dpx" % MIN_HEIGHT) + self.setStyleName("cardPanel") + + # Now we set up the layout + _label = Label(self.top_nick) + _label.setStyleName('cardGamePlayerNick') + self.add(_label, DockPanel.NORTH) + self.setCellWidth(_label, '100%') + self.setCellHorizontalAlignment(_label, HasAlignment.ALIGN_CENTER) + + self.hand_panel = AbsolutePanel() + self.add(self.hand_panel, DockPanel.SOUTH) + self.setCellWidth(self.hand_panel, '100%') + self.setCellHorizontalAlignment(self.hand_panel, HasAlignment.ALIGN_CENTER) + + _label = Label(self.left_nick) + _label.setStyleName('cardGamePlayerNick') + self.add(_label, DockPanel.WEST) + self.setCellHeight(_label, '100%') + self.setCellVerticalAlignment(_label, HasAlignment.ALIGN_MIDDLE) + + _label = Label(self.right_nick) + _label.setStyleName('cardGamePlayerNick') + self.add(_label, DockPanel.EAST) + self.setCellHeight(_label, '100%') + self.setCellHorizontalAlignment(_label, HasAlignment.ALIGN_RIGHT) + self.setCellVerticalAlignment(_label, HasAlignment.ALIGN_MIDDLE) + + self.center_panel = DockPanel() + self.inner_left = SimplePanel() + self.inner_left.setSize("%dpx" % CARD_WIDTH, "%dpx" % CARD_HEIGHT) + self.center_panel.add(self.inner_left, DockPanel.WEST) + self.center_panel.setCellHeight(self.inner_left, '100%') + self.center_panel.setCellHorizontalAlignment(self.inner_left, HasAlignment.ALIGN_RIGHT) + self.center_panel.setCellVerticalAlignment(self.inner_left, HasAlignment.ALIGN_MIDDLE) + + self.inner_right = SimplePanel() + self.inner_right.setSize("%dpx" % CARD_WIDTH, "%dpx" % CARD_HEIGHT) + self.center_panel.add(self.inner_right, DockPanel.EAST) + self.center_panel.setCellHeight(self.inner_right, '100%') + self.center_panel.setCellVerticalAlignment(self.inner_right, HasAlignment.ALIGN_MIDDLE) + + self.inner_top = SimplePanel() + self.inner_top.setSize("%dpx" % CARD_WIDTH, "%dpx" % CARD_HEIGHT) + self.center_panel.add(self.inner_top, DockPanel.NORTH) + self.center_panel.setCellHorizontalAlignment(self.inner_top, HasAlignment.ALIGN_CENTER) + self.center_panel.setCellVerticalAlignment(self.inner_top, HasAlignment.ALIGN_BOTTOM) + + self.inner_bottom = SimplePanel() + self.inner_bottom.setSize("%dpx" % CARD_WIDTH, "%dpx" % CARD_HEIGHT) + self.center_panel.add(self.inner_bottom, DockPanel.SOUTH) + self.center_panel.setCellHorizontalAlignment(self.inner_bottom, HasAlignment.ALIGN_CENTER) + self.center_panel.setCellVerticalAlignment(self.inner_bottom, HasAlignment.ALIGN_TOP) + + self.inner_center = SimplePanel() + self.center_panel.add(self.inner_center, DockPanel.CENTER) + self.center_panel.setCellHorizontalAlignment(self.inner_center, HasAlignment.ALIGN_CENTER) + self.center_panel.setCellVerticalAlignment(self.inner_center, HasAlignment.ALIGN_MIDDLE) + + self.add(self.center_panel, DockPanel.CENTER) + self.setCellWidth(self.center_panel, '100%') + self.setCellHeight(self.center_panel, '100%') + self.setCellVerticalAlignment(self.center_panel, HasAlignment.ALIGN_MIDDLE) + self.setCellHorizontalAlignment(self.center_panel, HasAlignment.ALIGN_CENTER) + + self.loadCards() + self.mouse_over_card = None # contain the card to highlight + self.visible_size = CARD_WIDTH / 2 # number of pixels visible for cards + self.addClickListener(self) + + def loadCards(self): + """Load all the cards in memory""" + def _getTarotCardsPathsCb(paths): + log.debug("_getTarotCardsPathsCb") + for file_ in paths: + log.debug(u"path: %s" % file_) + card = CardWidget(self, file_) + log.debug(u"card: %s" % card) + self.cards[(card.suit, card.value)] = card + self.deck.append(card) + self._parent.host.bridge.call('tarotGameReady', None, self.player_nick, self.referee) + self.cards = {} + self.deck = [] + self.cards["atout"] = {} # As Tarot is a french game, it's more handy & logical to keep french names + self.cards["pique"] = {} # spade + self.cards["coeur"] = {} # heart + self.cards["carreau"] = {} # diamond + self.cards["trefle"] = {} # club + self._parent.host.bridge.call('getTarotCardsPaths', _getTarotCardsPathsCb) + + def onClick(self, sender): + if self.state == "chien": + self.to_show = [] + self.state = "wait" + self.updateToShow() + elif self.state == "wait_for_ecart": + self.state = "ecart" + self.hand.extend(self.to_show) + self.hand.sort() + self.to_show = [] + self.updateToShow() + self.updateHand() + + def tarotGameNewHandler(self, hand): + """Start a new game, with given hand""" + if hand is []: # reset the display after the scores have been showed + self.selected.clear() + del self.hand[:] + del self.to_show[:] + self.state = None + #empty hand + self.updateHand() + #nothing on the table + self.updateToShow() + for pos in ['top', 'left', 'bottom', 'right']: + getattr(self, "inner_%s" % pos).setWidget(None) + self._parent.host.bridge.call('tarotGameReady', None, self.player_nick, self.referee) + return + for suit, value in hand: + self.hand.append(self.cards[(suit, value)]) + self.hand.sort() + self.state = "init" + self.updateHand() + + def updateHand(self): + """Show the cards in the hand in the hand_panel (SOUTH panel)""" + self.hand_panel.clear() + self.hand_panel.setSize("%dpx" % (self.visible_size * (len(self.hand) + 1)), "%dpx" % (CARD_HEIGHT + CARD_DELTA_Y + 10)) + x_pos = 0 + y_pos = CARD_DELTA_Y + for card in self.hand: + self.hand_panel.add(card, x_pos, y_pos) + x_pos += self.visible_size + + def updateToShow(self): + """Show cards in the center panel""" + if not self.to_show: + _widget = self.inner_center.getWidget() + if _widget: + self.inner_center.remove(_widget) + return + panel = AbsolutePanel() + panel.setSize("%dpx" % ((CARD_WIDTH + 5) * len(self.to_show) - 5), "%dpx" % (CARD_HEIGHT)) + x_pos = 0 + y_pos = 0 + for card in self.to_show: + panel.add(card, x_pos, y_pos) + x_pos += CARD_WIDTH + 5 + self.inner_center.setWidget(panel) + + def _ecartConfirm(self, confirm): + if not confirm: + return + ecart = [] + for card in self.selected: + ecart.append((card.suit, card.value)) + self.hand.remove(card) + self.selected.clear() + self._parent.host.bridge.call('tarotGamePlayCards', None, self.player_nick, self.referee, ecart) + self.state = "wait" + self.updateHand() + + def addToSelection(self, card): + self.selected.add(card) + if len(self.selected) == 6: + dialog.ConfirmDialog(self._ecartConfirm, "Put these cards into chien ?").show() + + def tarotGameInvalidCardsHandler(self, phase, played_cards, invalid_cards): + """Invalid cards have been played + @param phase: phase of the game + @param played_cards: all the cards played + @param invalid_cards: cards which are invalid""" + + if phase == "play": + self.state = "play" + elif phase == "ecart": + self.state = "ecart" + else: + log.error("INTERNAL ERROR: unmanaged game phase") # FIXME: raise an exception here + + for suit, value in played_cards: + self.hand.append(self.cards[(suit, value)]) + + self.hand.sort() + self.updateHand() + if self._autoplay == None: # No dialog if there is autoplay + Window.alert('Cards played are invalid !') + self.__fakePlay() + + def removeFromSelection(self, card): + self.selected.remove(card) + if len(self.selected) == 6: + dialog.ConfirmDialog(self._ecartConfirm, "Put these cards into chien ?").show() + + def tarotGameChooseContratHandler(self, xml_data): + """Called when the player has to select his contrat + @param xml_data: SàT xml representation of the form""" + body = xmlui.create(self._parent.host, xml_data, flags=['NO_CANCEL']) + _dialog = dialog.GenericDialog(_('Please choose your contrat'), body, options=['NO_CLOSE']) + body.setCloseCb(_dialog.close) + _dialog.show() + + def tarotGameShowCardsHandler(self, game_stage, cards, data): + """Display cards in the middle of the game (to show for e.g. chien ou poignée)""" + self.to_show = [] + for suit, value in cards: + self.to_show.append(self.cards[(suit, value)]) + self.updateToShow() + if game_stage == "chien" and data['attaquant'] == self.player_nick: + self.state = "wait_for_ecart" + else: + self.state = "chien" + + def getPlayerLocation(self, nick): + """return player location (top,bottom,left or right)""" + for location in ['top', 'left', 'bottom', 'right']: + if getattr(self, '%s_nick' % location) == nick: + return location + log.error("This line should not be reached") + + def tarotGameCardsPlayedHandler(self, player, cards): + """A card has been played by player""" + if not len(cards): + log.warning("cards should not be empty") + return + if len(cards) > 1: + log.error("can't manage several cards played") + if self.to_show: + self.to_show = [] + self.updateToShow() + suit, value = cards[0] + player_pos = self.getPlayerLocation(player) + player_panel = getattr(self, "inner_%s" % player_pos) + + if player_panel.getWidget() != None: + #We have already cards on the table, we remove them + for pos in ['top', 'left', 'bottom', 'right']: + getattr(self, "inner_%s" % pos).setWidget(None) + + card = self.cards[(suit, value)] + DOM.setElemAttribute(card.getElement(), "style", "") + player_panel.setWidget(card) + + def tarotGameYourTurnHandler(self): + """Called when we have to play :)""" + if self.state == "chien": + self.to_show = [] + self.updateToShow() + self.state = "play" + self.__fakePlay() + + def __fakePlay(self): + """Convenience method for stupid autoplay + /!\ don't forgot to comment any interactive dialog for invalid card""" + if self._autoplay == None: + return + if self._autoplay >= len(self.hand): + self._autoplay = 0 + card = self.hand[self._autoplay] + self._parent.host.bridge.call('tarotGamePlayCards', None, self.player_nick, self.referee, [(card.suit, card.value)]) + del self.hand[self._autoplay] + self.state = "wait" + self._autoplay += 1 + + def playCard(self, card): + self.hand.remove(card) + self._parent.host.bridge.call('tarotGamePlayCards', None, self.player_nick, self.referee, [(card.suit, card.value)]) + self.state = "wait" + self.updateHand() + + def tarotGameScoreHandler(self, xml_data, winners, loosers): + """Show score at the end of a round""" + if not winners and not loosers: + title = "Draw game" + else: + if self.player_nick in winners: + title = "You <b>win</b> !" + else: + title = "You <b>loose</b> :(" + body = xmlui.create(self._parent.host, xml_data, title=title, flags=['NO_CANCEL']) + _dialog = dialog.GenericDialog(title, body, options=['NO_CLOSE']) + body.setCloseCb(_dialog.close) + _dialog.show() + + +## Menu + +def hostReady(host): + def onTarotGame(): + def onPlayersSelected(room_jid, other_players): + other_players = [unicode(contact) for contact in other_players] + room_jid_s = unicode(room_jid) if room_jid else '' + host.bridge.launchTarotGame(other_players, room_jid_s, profile=C.PROF_KEY_NONE, callback=lambda dummy: None, errback=host.onJoinMUCFailure) + dialog.RoomAndContactsChooser(host, onPlayersSelected, 3, title="Tarot", title_invite=_(u"Please select 3 other players"), visible=(False, True)) + + def gotMenus(): + host.menus.addMenu(C.MENU_GLOBAL, (D_(u"Groups"), D_(u"Tarot")), callback=onTarotGame) + host.addListener('gotMenus', gotMenus) + +host_listener.addListener(hostReady)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/browser/sat_browser/html_tools.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,81 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2011-2018 Jérôme Poisson <goffi@goffi.org> + +# This program 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. + +# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + +from sat_frontends.tools import xmltools + +import nativedom +from __pyjamas__ import JS + +dom = nativedom.NativeDOM() + + +def html_sanitize(html): + """Naive sanitization of HTML""" + return html.replace('<', '<').replace('>', '>') + + +def html_strip(html): + """Strip leading/trailing white spaces, HTML line breaks and sequences.""" + JS("""return html.replace(/(^(<br\/?>| |\s)+)|((<br\/?>| |\s)+$)/g, "");""") + +def inlineRoot(xhtml): + """ make root element inline """ + doc = dom.parseString(xhtml) + return xmltools.inlineRoot(doc) + + +def convertNewLinesToXHTML(text): + """Replace all the \n with <br/>""" + return text.replace('\n', '<br/>') + + +def XHTML2Text(xhtml): + """Helper method to apply both html_sanitize and convertNewLinesToXHTML""" + return convertNewLinesToXHTML(html_sanitize(xhtml)) + + +def buildPresenceStyle(presence, base_style=None): + """Return the CSS classname to be used for displaying the given presence information. + + @param presence (unicode): presence is a value in ('', 'chat', 'away', 'dnd', 'xa') + @param base_style (unicode): base classname + @return: unicode + """ + if not base_style: + base_style = "contactLabel" + return '%s-%s' % (base_style, presence or 'connected') + + +def setPresenceStyle(widget, presence, base_style=None): + """ + Set the CSS style of a contact's element according to its presence. + + @param widget (Widget): the UI element of the contact + @param presence (unicode): a value in ("", "chat", "away", "dnd", "xa"). + @param base_style (unicode): the base name of the style to apply + """ + if not hasattr(widget, 'presence_style'): + widget.presence_style = None + style = buildPresenceStyle(presence, base_style) + if style == widget.presence_style: + return + if widget.presence_style is not None: + widget.removeStyleName(widget.presence_style) + widget.addStyleName(style) + widget.presence_style = style
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/browser/sat_browser/json.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,298 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2011-2018 Jérôme Poisson <goffi@goffi.org> + +# This program 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. + +# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + + +### logging configuration ### +from sat.core.log import getLogger +log = getLogger(__name__) +### + +from pyjamas.Timer import Timer +from pyjamas import Window +from pyjamas import JSONService +import time +from sat_browser import main_panel + +from sat_browser.constants import Const as C +import random + + +class LiberviaMethodProxy(object): + """This class manage calling for one method""" + + def __init__(self, parent, method): + self._parent = parent + self._method = method + + def call(self, *args, **kwargs): + """Method called when self._method attribue is used in JSON_PROXY_PARENT + + This method manage callback/errback in kwargs, and profile(_key) removing + @param *args: positional arguments of self._method + @param **kwargs: keyword arguments of self._method + """ + callback=kwargs.pop('callback', None) + errback=kwargs.pop('errback', None) + + # as profile is linked to browser session and managed server side, we remove them + profile_removed = False + try: + kwargs['profile'] # FIXME: workaround for pyjamas bug: KeyError is not raised with del + del kwargs['profile'] + profile_removed = True + except KeyError: + pass + + try: + kwargs['profile_key'] # FIXME: workaround for pyjamas bug: KeyError is not raised iwith del + del kwargs['profile_key'] + profile_removed = True + except KeyError: + pass + + if not profile_removed and args: + # if profile was not in kwargs, there is most probably one in args + args = list(args) + assert isinstance(args[-1], basestring) # Detect when we want to remove a callback (or something else) instead of the profile + del args[-1] + + if kwargs: + # kwargs should be empty here, we don't manage keyword arguments on bridge calls + log.error(u"kwargs is not empty after treatment on method call: kwargs={}".format(kwargs)) + + id_ = self._parent.callMethod(self._method, args) + + # callback or errback are managed in parent LiberviaJsonProxy with call id + if callback is not None: + self._parent.cb[id_] = callback + if errback is not None: + self._parent.eb[id_] = errback + + +class LiberviaJsonProxy(JSONService.JSONService): + + def __init__(self, url, methods): + self._serviceURL = url + self.methods = methods + JSONService.JSONService.__init__(self, url, self) + self.cb = {} + self.eb = {} + self._registerMethods(methods) + + def _registerMethods(self, methods): + if methods: + for method in methods: + log.debug(u"Registering JSON method call [{}]".format(method)) + setattr(self, + method, + getattr(LiberviaMethodProxy(self, method), 'call') + ) + + def callMethod(self, method, params, handler = None): + ret = super(LiberviaJsonProxy, self).callMethod(method, params, handler) + return ret + + def call(self, method, cb, *args): + # FIXME: deprecated call method, must be removed once it's not used anymore + id_ = self.callMethod(method, args) + log.debug(u"call: method={} [id={}], args={}".format(method, id_, args)) + if cb: + if isinstance(cb, tuple): + if len(cb) != 2: + log.error("tuple syntax for bridge.call is (callback, errback), aborting") + return + if cb[0] is not None: + self.cb[id_] = cb[0] + self.eb[id_] = cb[1] + else: + self.cb[id_] = cb + + def onRemoteResponse(self, response, request_info): + try: + _cb = self.cb[request_info.id] + except KeyError: + pass + else: + _cb(response) + del self.cb[request_info.id] + + try: + del self.eb[request_info.id] + except KeyError: + pass + + def onRemoteError(self, code, errobj, request_info): + """def dump(obj): + print "\n\nDUMPING %s\n\n" % obj + for i in dir(obj): + print "%s: %s" % (i, getattr(obj,i))""" + try: + _eb = self.eb[request_info.id] + except KeyError: + if code != 0: + log.error("Internal server error") + """for o in code, error, request_info: + dump(o)""" + else: + if isinstance(errobj['message'], dict): + log.error(u"Error %s: %s" % (errobj['message']['faultCode'], errobj['message']['faultString'])) + else: + log.error(u"%s" % errobj['message']) + else: + _eb((code, errobj)) + del self.eb[request_info.id] + + try: + del self.cb[request_info.id] + except KeyError: + pass + + +class RegisterCall(LiberviaJsonProxy): + def __init__(self): + LiberviaJsonProxy.__init__(self, "/register_api", + ["getSessionMetadata", "isConnected", "connect", "registerParams", "menusGet"]) + + +class BridgeCall(LiberviaJsonProxy): + def __init__(self): + LiberviaJsonProxy.__init__(self, "/json_api", + ["getContacts", "addContact", "messageSend", + "psNodeDelete", "psRetractItem", "psRetractItems", + "mbSend", "mbRetract", "mbGet", "mbGetFromMany", "mbGetFromManyRTResult", + "mbGetFromManyWithComments", "mbGetFromManyWithCommentsRTResult", + "historyGet", "getPresenceStatuses", "joinMUC", "mucLeave", "mucGetRoomsJoined", + "inviteMUC", "launchTarotGame", "getTarotCardsPaths", "tarotGameReady", + "tarotGamePlayCards", "launchRadioCollective", + "getWaitingSub", "subscription", "delContact", "updateContact", "avatarGet", + "getEntityData", "getParamsUI", "asyncGetParamA", "setParam", "launchAction", + "disconnect", "chatStateComposing", "getNewAccountDomain", + "syntaxConvert", "getAccountDialogUI", "getMainResource", "getEntitiesData", + "getVersion", "getLiberviaVersion", "mucGetDefaultService", "getFeatures", + "namespacesGet", + ]) + + def __call__(self, *args, **kwargs): + return LiberviaJsonProxy.__call__(self, *args, **kwargs) + + def getConfig(self, dummy1, dummy2): # FIXME + log.warning("getConfig is not implemeted in Libervia yet") + return '' + + def isConnected(self, dummy, callback): # FIXME + log.warning("isConnected is not implemeted in Libervia as for now profile is connected if session is opened") + callback(True) + + def encryptionPluginsGet(self, callback, errback): + """e2e encryption have no sense if made on backend, so we ignore this call""" + callback([]) + + def bridgeConnect(self, callback, errback): + callback() + + +class BridgeSignals(LiberviaJsonProxy): + + def __init__(self, host): + self.host = host + self.retry_time = None + self.retry_nb = 0 + self.retry_warning = None + self.retry_timer = None + LiberviaJsonProxy.__init__(self, "/json_signal_api", + ["getSignals"]) + self._signals = {} # key: signal name, value: callback + + def onRemoteResponse(self, response, request_info): + if self.retry_time: + log.info("Connection with server restablished") + self.retry_nb = 0 + self.retry_time = None + LiberviaJsonProxy.onRemoteResponse(self, response, request_info) + + def onRemoteError(self, code, errobj, request_info): + if errobj['message'] == 'Empty Response': + log.warning(u"Empty reponse bridgeSignal\ncode={}\nrequest_info: id={} method={} handler={}".format(code, request_info.id, request_info.method, request_info.handler)) + # FIXME: to check/replace by a proper session end on disconnected signal + # Window.getLocation().reload() # XXX: reset page in case of session ended. + # FIXME: Should be done more properly without hard reload + LiberviaJsonProxy.onRemoteError(self, code, errobj, request_info) + #we now try to reconnect + if isinstance(errobj['message'], dict) and errobj['message']['faultCode'] == 0: + Window.alert('You are not allowed to connect to server') + else: + def _timerCb(dummy): + current = time.time() + if current > self.retry_time: + msg = "Trying to reconnect to server..." + log.info(msg) + self.retry_warning.showWarning("INFO", msg) + self.retry_timer.cancel() + self.retry_warning = self.retry_timer = None + self.getSignals(callback=self.signalHandler, profile=None) + else: + remaining = int(self.retry_time - current) + msg_html = u"Connection with server lost. Retrying in <strong>{}</strong> s".format(remaining) + self.retry_warning.showWarning("WARNING", msg_html, None) + + if self.retry_nb < 3: + retry_delay = 1 + elif self.retry_nb < 10: + retry_delay = random.randint(1,10) + else: + retry_delay = random.randint(1,60) + self.retry_nb += 1 + log.warning(u"Lost connection, trying to reconnect in {} s (try #{})".format(retry_delay, self.retry_nb)) + self.retry_time = time.time() + retry_delay + self.retry_warning = main_panel.WarningPopup() + self.retry_timer = Timer(notify=_timerCb) + self.retry_timer.scheduleRepeating(1000) + _timerCb(None) + + def register_signal(self, name, callback, with_profile=True): + """Register a signal + + @param: name of the signal to register + @param callback: method to call + @param with_profile: True if the original bridge method need a profile + """ + log.debug(u"Registering signal {}".format(name)) + if name in self._signals: + log.error(u"Trying to register and already registered signal ({})".format(name)) + else: + self._signals[name] = (callback, with_profile) + + def signalHandler(self, signal_data): + self.getSignals(callback=self.signalHandler, profile=None) + if len(signal_data) == 1: + signal_data.append([]) + log.debug(u"Got signal ==> name: %s, params: %s" % (signal_data[0], signal_data[1])) + name, args = signal_data + try: + callback, with_profile = self._signals[name] + except KeyError: + log.warning(u"Ignoring {} signal: no handler registered !".format(name)) + return + if with_profile: + args.append(C.PROF_KEY_NONE) + if not self.host._profile_plugged: + log.debug("Profile is not plugged, we cache the signal") + self.host.signals_cache[C.PROF_KEY_NONE].append((name, callback, args, {})) + else: + callback(*args)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/browser/sat_browser/libervia_widget.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,811 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2011-2018 Jérôme Poisson <goffi@goffi.org> + +# This program 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. + +# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. +"""Libervia base widget""" + +import pyjd # this is dummy in pyjs +from sat.core.log import getLogger +log = getLogger(__name__) + +from sat.core.i18n import _ +from sat.core import exceptions +from sat_frontends.quick_frontend import quick_widgets + +from pyjamas.ui.FlexTable import FlexTable +from pyjamas.ui.TabPanel import TabPanel +from pyjamas.ui.SimplePanel import SimplePanel +from pyjamas.ui.AbsolutePanel import AbsolutePanel +from pyjamas.ui.VerticalPanel import VerticalPanel +from pyjamas.ui.HorizontalPanel import HorizontalPanel +from pyjamas.ui.HTMLPanel import HTMLPanel +from pyjamas.ui.Label import Label +from pyjamas.ui.HTML import HTML +from pyjamas.ui.Button import Button +from pyjamas.ui.Widget import Widget +from pyjamas.ui.ClickListener import ClickHandler +from pyjamas.ui import HasAlignment +from pyjamas.ui.DragWidget import DragWidget +from pyjamas.ui.DropWidget import DropWidget +from pyjamas import DOM +from pyjamas import Window + +import dialog +import base_menu +import base_widget +import base_panel + + +unicode = str # FIXME: pyjamas workaround + + +# FIXME: we need to group several unrelated panels/widgets in this module because of isinstance tests and other references to classes (e.g. if we separate Drag n Drop classes in a separate module, we'll have cyclic import because of the references to LiberviaWidget in DropCell). +# TODO: use a more generic method (either use duck typing, or register classes in a generic way, without hard references), then split classes in separate modules + + +### Drag n Drop ### + + +class DragLabel(DragWidget): + + def __init__(self, text, type_, host=None): + """Base of Drag n Drop mecanism in Libervia + + @param text: data embedded with in drag n drop operation + @param type_: type of data that we are dragging + @param host: if not None, the host will be use to highlight BorderWidgets + """ + DragWidget.__init__(self) + self.host = host + self._text = text + self.type_ = type_ + + def onDragStart(self, event): + dt = event.dataTransfer + dt.setData('text/plain', "%s\n%s" % (self._text, self.type_)) + dt.setDragImage(self.getElement(), 15, 15) + if self.host is not None: + current_panel = self.host.tab_panel.getCurrentPanel() + for widget in current_panel.widgets: + if isinstance(widget, BorderWidget): + widget.addStyleName('borderWidgetOnDrag') + + def onDragEnd(self, event): + if self.host is not None: + current_panel = self.host.tab_panel.getCurrentPanel() + for widget in current_panel.widgets: + if isinstance(widget, BorderWidget): + widget.removeStyleName('borderWidgetOnDrag') + + +class LiberviaDragWidget(DragLabel): + """ A DragLabel which keep the widget being dragged as class value """ + current = None # widget currently dragged + + def __init__(self, text, type_, widget): + DragLabel.__init__(self, text, type_, widget.host) + self.widget = widget + + def onDragStart(self, event): + LiberviaDragWidget.current = self.widget + DragLabel.onDragStart(self, event) + + def onDragEnd(self, event): + DragLabel.onDragEnd(self, event) + LiberviaDragWidget.current = None + + +class DropCell(DropWidget): + """Cell in the middle grid which replace itself with the dropped widget on DnD""" + drop_keys = {} + + def __init__(self, host): + DropWidget.__init__(self) + self.host = host + self.setStyleName('dropCell') + + @classmethod + def addDropKey(cls, key, cb): + """Add a association between a key and a class to create on drop. + + @param key: key to be associated (e.g. "CONTACT", "CHAT") + @param cb: a callable (either a class or method) returning a + LiberviaWidget instance + """ + DropCell.drop_keys[key] = cb + + def onDragEnter(self, event): + if self == LiberviaDragWidget.current: + return + self.addStyleName('dragover') + DOM.eventPreventDefault(event) + + def onDragLeave(self, event): + if event.clientX <= self.getAbsoluteLeft() or event.clientY <= self.getAbsoluteTop() or\ + event.clientX >= self.getAbsoluteLeft() + self.getOffsetWidth() - 1 or event.clientY >= self.getAbsoluteTop() + self.getOffsetHeight() - 1: + # We check that we are inside widget's box, and we don't remove the style in this case because + # if the mouse is over a widget inside the DropWidget, if will leave the DropWidget, and we + # don't want that + self.removeStyleName('dragover') + + def onDragOver(self, event): + DOM.eventPreventDefault(event) + + def _getCellAndRow(self, grid, event): + """Return cell and row index where the event is occuring""" + cell = grid.getEventTargetCell(event) + row = DOM.getParent(cell) + return (row.rowIndex, cell.cellIndex) + + def onDrop(self, event): + """ + @raise NoLiberviaWidgetException: something else than a LiberviaWidget + has been returned by the callback. + """ + self.removeStyleName('dragover') + DOM.eventPreventDefault(event) + item, item_type = eventGetData(event) + if item_type == "WIDGET": + if not LiberviaDragWidget.current: + log.error("No widget registered in LiberviaDragWidget !") + return + _new_panel = LiberviaDragWidget.current + if self == _new_panel: # We can't drop on ourself + return + # we need to remove the widget from the panel as it will be inserted elsewhere + widgets_panel = _new_panel.getParent(WidgetsPanel, expect=True) + wid_row = widgets_panel.getWidgetCoords(_new_panel)[0] + row_wids = widgets_panel.getLiberviaRowWidgets(wid_row) + if len(row_wids) == 1 and wid_row == widgets_panel.getWidgetCoords(self)[0]: + # the dropped widget is the only one in the same row + # as the target widget (self), we don't do anything + return + widgets_panel.removeWidget(_new_panel) + elif item_type in self.drop_keys: + _new_panel = self.drop_keys[item_type](self.host, item) + if not isinstance(_new_panel, LiberviaWidget): + raise base_widget.NoLiberviaWidgetException + else: + log.warning("unmanaged item type") + return + if isinstance(self, LiberviaWidget): + # self.host.unregisterWidget(self) # FIXME + self.onQuit() + if not isinstance(_new_panel, LiberviaWidget): + log.warning("droping an object which is not a class of LiberviaWidget") + _flextable = self.getParent() + _widgetspanel = _flextable.getParent().getParent() + row_idx, cell_idx = self._getCellAndRow(_flextable, event) + if self.host.getSelected() == self: + self.host.setSelected(None) + _widgetspanel.changeWidget(row_idx, cell_idx, _new_panel) + """_unempty_panels = filter(lambda wid:not isinstance(wid,EmptyWidget),list(_flextable)) + _width = 90/float(len(_unempty_panels) or 1) + #now we resize all the cell of the column + for panel in _unempty_panels: + td_elt = panel.getElement().parentNode + DOM.setStyleAttribute(td_elt, "width", "%s%%" % _width)""" + if isinstance(self, quick_widgets.QuickWidget): + self.host.widgets.deleteWidget(self) + + +class EmptyWidget(DropCell, SimplePanel): + """Empty dropable panel""" + + def __init__(self, host): + SimplePanel.__init__(self) + DropCell.__init__(self, host) + #self.setWidget(HTML('')) + self.setSize('100%', '100%') + + +class BorderWidget(EmptyWidget): + def __init__(self, host): + EmptyWidget.__init__(self, host) + self.addStyleName('borderPanel') + + +class LeftBorderWidget(BorderWidget): + def __init__(self, host): + BorderWidget.__init__(self, host) + self.addStyleName('leftBorderWidget') + + +class RightBorderWidget(BorderWidget): + def __init__(self, host): + BorderWidget.__init__(self, host) + self.addStyleName('rightBorderWidget') + + +class BottomBorderWidget(BorderWidget): + def __init__(self, host): + BorderWidget.__init__(self, host) + self.addStyleName('bottomBorderWidget') + + +class DropTab(Label, DropWidget): + + def __init__(self, tab_panel, text): + Label.__init__(self, text) + DropWidget.__init__(self, tab_panel) + self.tab_panel = tab_panel + self.setStyleName('dropCell') + self.setWordWrap(False) + + def _getIndex(self): + """ get current index of the DropTab """ + # XXX: awful hack, but seems the only way to get index + return self.tab_panel.tabBar.panel.getWidgetIndex(self.getParent().getParent()) - 1 + + def onDragEnter(self, event): + #if self == LiberviaDragWidget.current: + # return + self.parent.addStyleName('dragover') + DOM.eventPreventDefault(event) + + def onDragLeave(self, event): + self.parent.removeStyleName('dragover') + + def onDragOver(self, event): + DOM.eventPreventDefault(event) + + def onDrop(self, event): + DOM.eventPreventDefault(event) + self.parent.removeStyleName('dragover') + if self._getIndex() == self.tab_panel.tabBar.getSelectedTab(): + # the widget comes from the same tab, so nothing to do, we let it there + return + + item, item_type = eventGetData(event) + if item_type == "WIDGET": + if not LiberviaDragWidget.current: + log.error("No widget registered in LiberviaDragWidget !") + return + _new_panel = LiberviaDragWidget.current + elif item_type in DropCell.drop_keys: + pass # create the widget when we are sure there's a tab for it + else: + log.warning("unmanaged item type") + return + + # XXX: when needed, new tab creation must be done exactly here to not mess up with LiberviaDragWidget.onDragEnd + try: + widgets_panel = self.tab_panel.getWidget(self._getIndex()) + except IndexError: # widgets panel doesn't exist, e.g. user dropped in "+" tab + widgets_panel = self.tab_panel.addWidgetsTab(None) + if widgets_panel is None: # user cancelled + return + + if item_type == "WIDGET": + _new_panel.getParent(WidgetsPanel, expect=True).removeWidget(_new_panel) + else: + _new_panel = DropCell.drop_keys[item_type](self.tab_panel.host, item) + + widgets_panel.addWidget(_new_panel) + + +### Libervia Widget ### + + +class WidgetHeader(AbsolutePanel, LiberviaDragWidget): + + def __init__(self, parent, host, title, info=None): + """ + @param parent (LiberviaWidget): LiberWidget instance + @param host (SatWebFrontend): SatWebFrontend instance + @param title (Label, HTML): text widget instance + @param info (Widget): text widget instance + """ + AbsolutePanel.__init__(self) + self.add(title) + if info: + # FIXME: temporary design to display the info near the menu + button_group_wrapper = HorizontalPanel() + button_group_wrapper.add(info) + else: + button_group_wrapper = SimplePanel() + button_group_wrapper.setStyleName('widgetHeader_buttonsWrapper') + button_group = base_widget.WidgetMenuBar(parent, host) + button_group.addItem('<img src="media/icons/misc/settings.png"/>', True, base_menu.SimpleCmd(parent.onSetting)) + button_group.addItem('<img src="media/icons/misc/close.png"/>', True, base_menu.SimpleCmd(parent.onClose)) + button_group_wrapper.add(button_group) + self.add(button_group_wrapper) + self.addStyleName('widgetHeader') + LiberviaDragWidget.__init__(self, "", "WIDGET", parent) + + +class LiberviaWidget(DropCell, VerticalPanel, ClickHandler): + """Libervia's widget which can replace itself with a dropped widget on DnD""" + + def __init__(self, host, title='', info=None, selectable=False, plugin_menu_context=None): + """Init the widget + + @param host (SatWebFrontend): SatWebFrontend instance + @param title (unicode): title shown in the header of the widget + @param info (unicode): info shown in the header of the widget + @param selectable (bool): True is widget can be selected by user + @param plugin_menu_context (iterable): contexts of menus to have (list of C.MENU_* constant) + """ + VerticalPanel.__init__(self) + DropCell.__init__(self, host) + ClickHandler.__init__(self) + self._selectable = selectable + self._plugin_menu_context = [] if plugin_menu_context is None else plugin_menu_context + self._title_id = HTMLPanel.createUniqueId() + self._setting_button_id = HTMLPanel.createUniqueId() + self._close_button_id = HTMLPanel.createUniqueId() + self._title = Label(title) + self._title.setStyleName('widgetHeader_title') + if info is not None: + self._info = HTML(info) + self._info.setStyleName('widgetHeader_info') + else: + self._info = None + header = WidgetHeader(self, host, self._title, self._info) + self.add(header) + self.setSize('100%', '100%') + self.addStyleName('widget') + if self._selectable: + self.addClickListener(self) + + @property + def plugin_menu_context(self): + return self._plugin_menu_context + + def getDebugName(self): + return "%s (%s)" % (self, self._title.getText()) + + def getParent(self, class_=None, expect=True): + """Return the closest ancestor of the specified class. + + Note: this method overrides pyjamas.ui.Widget.getParent + + @param class_: class of the ancestor to look for or None to return the first parent + @param expect: set to True if the parent is expected (raise an error if not found) + @return: the parent/ancestor or None if it has not been found + @raise exceptions.InternalError: expect is True and no parent is found + """ + current = Widget.getParent(self) + if class_ is None: + return current # this is the default behavior + while current is not None and not isinstance(current, class_): + current = Widget.getParent(current) + if current is None and expect: + raise exceptions.InternalError("Can't find parent %s for %s" % (class_, self)) + return current + + def onClick(self, sender): + self.host.setSelected(self) + + def onClose(self, sender): + """ Called when the close button is pushed """ + widgets_panel = self.getParent(WidgetsPanel, expect=True) + widgets_panel.removeWidget(self) + self.onQuit() + self.host.widgets.deleteWidget(self) + + def onQuit(self): + """ Called when the widget is actually ending """ + pass + + def refresh(self): + """This can be overwritten by a child class to refresh the display when, + instead of creating a new one, an existing widget is found and reused. + """ + pass + + def onSetting(self, sender): + widpanel = self.getParent(WidgetsPanel, expect=True) + row, col = widpanel.getIndex(self) + body = VerticalPanel() + + # colspan & rowspan + colspan = widpanel.getColSpan(row, col) + rowspan = widpanel.getRowSpan(row, col) + + def onColSpanChange(value): + widpanel.setColSpan(row, col, value) + + def onRowSpanChange(value): + widpanel.setRowSpan(row, col, value) + colspan_setter = dialog.IntSetter("Columns span", colspan) + colspan_setter.addValueChangeListener(onColSpanChange) + colspan_setter.setWidth('100%') + rowspan_setter = dialog.IntSetter("Rows span", rowspan) + rowspan_setter.addValueChangeListener(onRowSpanChange) + rowspan_setter.setWidth('100%') + body.add(colspan_setter) + body.add(rowspan_setter) + + # size + width_str = self.getWidth() + if width_str.endswith('px'): + width = int(width_str[:-2]) + else: + width = 0 + height_str = self.getHeight() + if height_str.endswith('px'): + height = int(height_str[:-2]) + else: + height = 0 + + def onWidthChange(value): + if not value: + self.setWidth('100%') + else: + self.setWidth('%dpx' % value) + + def onHeightChange(value): + if not value: + self.setHeight('100%') + else: + self.setHeight('%dpx' % value) + width_setter = dialog.IntSetter("width (0=auto)", width) + width_setter.addValueChangeListener(onWidthChange) + width_setter.setWidth('100%') + height_setter = dialog.IntSetter("height (0=auto)", height) + height_setter.addValueChangeListener(onHeightChange) + height_setter.setHeight('100%') + body.add(width_setter) + body.add(height_setter) + + # reset + def onReset(sender): + colspan_setter.setValue(1) + rowspan_setter.setValue(1) + width_setter.setValue(0) + height_setter.setValue(0) + + reset_bt = Button("Reset", onReset) + body.add(reset_bt) + body.setCellHorizontalAlignment(reset_bt, HasAlignment.ALIGN_CENTER) + + _dialog = dialog.GenericDialog("Widget setting", body) + _dialog.show() + + def setTitle(self, text): + """change the title in the header of the widget + @param text: text of the new title""" + self._title.setText(text) + + def setHeaderInfo(self, text): + """change the info in the header of the widget + @param text: text of the new title""" + try: + self._info.setHTML(text) + except TypeError: + log.error("LiberviaWidget.setInfo: info widget has not been initialized!") + + def isSelectable(self): + return self._selectable + + def setSelectable(self, selectable): + if not self._selectable: + try: + self.removeClickListener(self) + except ValueError: + pass + if self.selectable and not self in self._clickListeners: + self.addClickListener(self) + self._selectable = selectable + + def getWarningData(self): + """ Return exposition warning level when this widget is selected and something is sent to it + This method should be overriden by children + @return: tuple (warning level type/HTML msg). Type can be one of: + - PUBLIC + - GROUP + - ONE2ONE + - MISC + - NONE + """ + if not self._selectable: + log.error("getWarningLevel must not be called for an unselectable widget") + raise Exception + # TODO: cleaner warning types (more general constants) + return ("NONE", None) + + def setWidget(self, widget, scrollable=True): + """Set the widget that will be in the body of the LiberviaWidget + @param widget: widget to put in the body + @param scrollable: if true, the widget will be in a ScrollPanelWrapper""" + if scrollable: + _scrollpanelwrapper = base_panel.ScrollPanelWrapper() + _scrollpanelwrapper.setStyleName('widgetBody') + _scrollpanelwrapper.setWidget(widget) + body_wid = _scrollpanelwrapper + else: + body_wid = widget + self.add(body_wid) + self.setCellHeight(body_wid, '100%') + + def doDetachChildren(self): + # We need to force the use of a panel subclass method here, + # for the same reason as doAttachChildren + VerticalPanel.doDetachChildren(self) + + def doAttachChildren(self): + # We need to force the use of a panel subclass method here, else + # the event will not propagate to children + VerticalPanel.doAttachChildren(self) + + +# XXX: WidgetsPanel and MainTabPanel are both here to avoir cyclic import + + +class WidgetsPanel(base_panel.ScrollPanelWrapper): + """The panel wanaging the widgets indide a tab""" + + def __init__(self, host, locked=False): + """ + + @param host (SatWebFrontend): host instance + @param locked (bool): If True, the tab containing self will not be + removed when there are no more widget inside self. If False, the + tab will be removed with self's last widget. + """ + base_panel.ScrollPanelWrapper.__init__(self) + self.setSize('100%', '100%') + self.host = host + self.locked = locked + self.selected = None + self.flextable = FlexTable() + self.flextable.setSize('100%', '100%') + self.setWidget(self.flextable) + self.setStyleName('widgetsPanel') + _bottom = BottomBorderWidget(self.host) + self.flextable.setWidget(0, 0, _bottom) # There will be always an Empty widget on the last row, + # dropping a widget there will add a new row + td_elt = _bottom.getElement().parentNode + DOM.setStyleAttribute(td_elt, "height", "1px") # needed so the cell adapt to the size of the border (specially in webkit) + self._max_cols = 1 # give the maximum number of columns in a raw + + @property + def widgets(self): + return iter(self.flextable) + + def isLocked(self): + return self.locked + + def changeWidget(self, row, col, wid): + """Change the widget in the given location, add row or columns when necessary""" + log.debug(u"changing widget: %s %s %s" % (wid.getDebugName(), row, col)) + last_row = max(0, self.flextable.getRowCount() - 1) + # try: # FIXME: except without exception specified ! + prev_wid = self.flextable.getWidget(row, col) + # except: + # log.error("Trying to change an unexisting widget !") + # return + + cellFormatter = self.flextable.getFlexCellFormatter() + + if isinstance(prev_wid, BorderWidget): + # We are on a border, we must create a row and/or columns + prev_wid.removeStyleName('dragover') + + if isinstance(prev_wid, BottomBorderWidget): + # We are on the bottom border, we create a new row + self.flextable.insertRow(last_row) + self.flextable.setWidget(last_row, 0, LeftBorderWidget(self.host)) + self.flextable.setWidget(last_row, 1, wid) + self.flextable.setWidget(last_row, 2, RightBorderWidget(self.host)) + cellFormatter.setHorizontalAlignment(last_row, 2, HasAlignment.ALIGN_RIGHT) + row = last_row + + elif isinstance(prev_wid, LeftBorderWidget): + if col != 0: + log.error("LeftBorderWidget must be on the first column !") + return + self.flextable.insertCell(row, col + 1) + self.flextable.setWidget(row, 1, wid) + + elif isinstance(prev_wid, RightBorderWidget): + if col != self.flextable.getCellCount(row) - 1: + log.error("RightBorderWidget must be on the last column !") + return + self.flextable.insertCell(row, col) + self.flextable.setWidget(row, col, wid) + + else: + prev_wid.removeFromParent() + self.flextable.setWidget(row, col, wid) + + _max_cols = max(self._max_cols, self.flextable.getCellCount(row)) + if _max_cols != self._max_cols: + self._max_cols = _max_cols + self._sizesAdjust() + + def _sizesAdjust(self): + cellFormatter = self.flextable.getFlexCellFormatter() + width = 100.0 / max(1, self._max_cols - 2) # we don't count the borders + + for row_idx in xrange(self.flextable.getRowCount()): + for col_idx in xrange(self.flextable.getCellCount(row_idx)): + _widget = self.flextable.getWidget(row_idx, col_idx) + if _widget and not isinstance(_widget, BorderWidget): + td_elt = _widget.getElement().parentNode + DOM.setStyleAttribute(td_elt, "width", "%.2f%%" % width) + + last_row = max(0, self.flextable.getRowCount() - 1) + cellFormatter.setColSpan(last_row, 0, self._max_cols) + + def addWidget(self, wid): + """Add a widget to a new cell on the next to last row""" + last_row = max(0, self.flextable.getRowCount() - 1) + log.debug(u"putting widget %s at %d, %d" % (wid.getDebugName(), last_row, 0)) + self.changeWidget(last_row, 0, wid) + + def removeWidget(self, wid): + """Remove a widget and the cell where it is""" + _row, _col = self.flextable.getIndex(wid) + self.flextable.remove(wid) + self.flextable.removeCell(_row, _col) + if not self.getLiberviaRowWidgets(_row): # we have no more widgets, we remove the row + self.flextable.removeRow(_row) + _max_cols = 1 + for row_idx in xrange(self.flextable.getRowCount()): + _max_cols = max(_max_cols, self.flextable.getCellCount(row_idx)) + if _max_cols != self._max_cols: + self._max_cols = _max_cols + self._sizesAdjust() + current = self + + blank_page = self.getLiberviaWidgetsCount() == 0 # do we still have widgets on the page ? + + if blank_page and not self.isLocked(): + # we now notice the MainTabPanel that the WidgetsPanel is empty and need to be removed + while current is not None: + if isinstance(current, MainTabPanel): + current.onWidgetPanelRemove(self) + return + current = current.getParent() + log.error("no MainTabPanel found !") + + def getWidgetCoords(self, wid): + return self.flextable.getIndex(wid) + + def getLiberviaRowWidgets(self, row): + """ Return all the LiberviaWidget in the row """ + return [wid for wid in self.getRowWidgets(row) if isinstance(wid, LiberviaWidget)] + + def getRowWidgets(self, row): + """ Return all the widgets in the row """ + widgets = [] + cols = self.flextable.getCellCount(row) + for col in xrange(cols): + widgets.append(self.flextable.getWidget(row, col)) + return widgets + + def getLiberviaWidgetsCount(self): + """ Get count of contained widgets """ + return len([wid for wid in self.flextable if isinstance(wid, LiberviaWidget)]) + + def getIndex(self, wid): + return self.flextable.getIndex(wid) + + def getColSpan(self, row, col): + cellFormatter = self.flextable.getFlexCellFormatter() + return cellFormatter.getColSpan(row, col) + + def setColSpan(self, row, col, value): + cellFormatter = self.flextable.getFlexCellFormatter() + return cellFormatter.setColSpan(row, col, value) + + def getRowSpan(self, row, col): + cellFormatter = self.flextable.getFlexCellFormatter() + return cellFormatter.getRowSpan(row, col) + + def setRowSpan(self, row, col, value): + cellFormatter = self.flextable.getFlexCellFormatter() + return cellFormatter.setRowSpan(row, col, value) + + +class MainTabPanel(TabPanel, ClickHandler): + """The panel managing the tabs""" + + def __init__(self, host): + TabPanel.__init__(self, FloatingTab=True) + ClickHandler.__init__(self) + self.host = host + self.setStyleName('liberviaTabPanel') + self.tabBar.addTab(DropTab(self, u'✚'), asHTML=False) + self.tabBar.setVisible(False) # set to True when profile is logged + self.tabBar.addStyleDependentName('oneTab') + + def onTabSelected(self, sender, tabIndex): + if tabIndex < self.getWidgetCount(): + TabPanel.onTabSelected(self, sender, tabIndex) + self.host.selected_widget = self.getCurrentPanel().selected + return + # user clicked the "+" tab + self.addWidgetsTab(None, select=True) + + def getCurrentPanel(self): + """ Get the panel of the currently selected tab + + @return: WidgetsPanel + """ + return self.deck.visibleWidget + + def addTab(self, widget, label, select=False): + """Create a new tab for the given widget. + + @param widget (Widget): widget to associate to the tab + @param label (unicode): label of the tab + @param select (bool): True to select the added tab + """ + TabPanel.add(self, widget, DropTab(self, label), False) + if self.getWidgetCount() > 1: + self.tabBar.removeStyleDependentName('oneTab') + self.host.resize() + if select: + self.selectTab(self.getWidgetCount() - 1) + + def addWidgetsTab(self, label, select=False, locked=False): + """Create a new tab for containing LiberviaWidgets. + + @param label (unicode): label of the tab (None or '' for user prompt) + @param select (bool): True to select the added tab + @param locked (bool): If True, the tab will not be removed when there + are no more widget inside. If False, the tab will be removed with + the last widget. + @return: WidgetsPanel + """ + widgets_panel = WidgetsPanel(self.host, locked=locked) + + if not label: + default_label = _(u'new tab') + try: + label = Window.prompt(_(u'Name of the new tab'), default_label) + if not label: # empty label or user pressed "cancel" + return None + except: # this happens when the user prevents the page to open the prompt dialog + label = default_label + + self.addTab(widgets_panel, label, select) + return widgets_panel + + def onWidgetPanelRemove(self, panel): + """ Called when a child WidgetsPanel is empty and need to be removed """ + widget_index = self.getWidgetIndex(panel) + self.remove(panel) + widgets_count = self.getWidgetCount() + if widgets_count == 1: + self.tabBar.addStyleDependentName('oneTab') + self.host.resize() + self.selectTab(widget_index if widget_index < widgets_count else widgets_count - 1) + + +def eventGetData(event): + """Retrieve the event data. + + @param event(EventObject) + @return tuple: (event_text, event_type) + """ + dt = event.dataTransfer + # 'text', 'text/plain', and 'Text' are equivalent. + try: + item, item_type = dt.getData("text/plain").split('\n') # Workaround for webkit, only text/plain seems to be managed + if item_type and item_type[-1] == '\0': # Workaround for what looks like a pyjamas bug: the \0 should not be there, and + item_type = item_type[:-1] # .strip('\0') and .replace('\0','') don't work. TODO: check this and fill a bug report + # item_type = dt.getData("type") + log.debug(u"event data: %s (type %s)" % (item, item_type)) + except: + log.debug("event data not found") + item = ' ' + item_type = None + return item, item_type
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/browser/sat_browser/list_manager.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,516 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2013-2016 Adrien Cossa <souliane@mailoo.org> + +# This program 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. + +# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + +from sat.core.log import getLogger +log = getLogger(__name__) +from sat.core.i18n import _ + +from pyjamas.ui.ClickListener import ClickHandler +from pyjamas.ui.FocusListener import FocusHandler +from pyjamas.ui.ChangeListener import ChangeHandler +from pyjamas.ui.DragHandler import DragHandler +from pyjamas.ui.KeyboardListener import KeyboardHandler, KEY_ENTER +from pyjamas.ui.DragWidget import DragWidget +from pyjamas.ui.ListBox import ListBox +from pyjamas.ui.Button import Button +from pyjamas.ui.FlowPanel import FlowPanel +from pyjamas.ui.HorizontalPanel import HorizontalPanel +from pyjamas.ui.FlexTable import FlexTable +from pyjamas.ui.AutoComplete import AutoCompleteTextBox + +import base_panel +import base_widget +import libervia_widget + +from sat_frontends.quick_frontend import quick_list_manager + + +unicode = str # FIXME: pyjamas workaround + + +class ListItem(HorizontalPanel): + """This class implements a list item with auto-completion and a delete button.""" + + STYLE = {"listItem": "listItem", + "listItem-box": "listItem-box", + "listItem-box-invalid": "listItem-box-invalid", + "listItem-button": "listItem-button", + } + + VALID = 1 + INVALID = 2 + DUPLICATE = 3 + + def __init__(self, listener=None, taglist=None, validate=None): + """ + + @param listener (ListItemHandler): handler for the UI events + @param taglist (quick_list_manager.QuickTagList): list manager + @param validate (callable): method returning a bool to validate the entry + """ + HorizontalPanel.__init__(self) + self.addStyleName(self.STYLE["listItem"]) + + self.box = AutoCompleteTextBox(StyleName=self.STYLE["listItem-box"]) + self.remove_btn = Button('<span>x</span>', Visible=False) + self.remove_btn.setStyleName(self.STYLE["listItem-button"]) + self.add(self.box) + self.add(self.remove_btn) + + if listener: + self.box.addFocusListener(listener) + self.box.addChangeListener(listener) + self.box.addKeyboardListener(listener) + self.box.choices.addClickListener(listener) + self.remove_btn.addClickListener(listener) + + self.taglist = taglist + self.validate = validate + self.last_checked_value = "" + self.last_validity = self.VALID + + @property + def text(self): + return self.box.getText() + + def setText(self, text): + """ + Set the text and refresh the Widget. + + @param text (unicode): text to set + """ + self.box.setText(text) + self.refresh() + + def refresh(self): + if self.last_checked_value == self.text: + return + + if self.taglist and self.last_checked_value: + self.taglist.untag([self.last_checked_value]) + + if self.validate: # if None, the state is always valid + self.last_validity = self.validate(self.text) + + if self.last_validity == self.VALID: + self.box.removeStyleName(self.STYLE["listItem-box-invalid"]) + self.box.setVisibleLength(max(len(self.text), 10)) + elif self.last_validity == self.INVALID: + self.box.addStyleName(self.STYLE["listItem-box-invalid"]) + elif self.last_validity == self.DUPLICATE: + self.remove_btn.click() # this may do more stuff then self.remove() + return + + if self.taglist and self.text: + self.taglist.tag([self.text]) + self.last_checked_value = self.text + self.box.setSelectionRange(len(self.text), 0) + self.remove_btn.setVisible(len(self.text) > 0) + + def setFocus(self, focused): + self.box.setFocus(focused) + + def remove(self): + """Remove the list item from its parent.""" + self.removeFromParent() + + if self.taglist and self.text: # this must be done after the widget has been removed + self.taglist.untag([self.text]) + + +class DraggableListItem(ListItem, DragWidget): + """This class is like ListItem, but in addition it can be dragged.""" + + def __init__(self, listener=None, taglist=None, validate=None): + """ + + @param listener (ListItemHandler): handler for the UI events + @param taglist (quick_list_manager.QuickTagList): list manager + @param validate (callable): method returning a bool to validate the entry + """ + ListItem.__init__(self, listener, taglist, validate) + DragWidget.__init__(self) + self.addDragListener(listener) + + + def onDragStart(self, event): + """The user starts dragging the item.""" + dt = event.dataTransfer + dt.setData('text/plain', "%s\n%s" % (self.text, "CONTACT_TEXTBOX")) + dt.setDragImage(self.box.getElement(), 15, 15) + + +class ListItemHandler(ClickHandler, FocusHandler, KeyboardHandler, ChangeHandler): + """Implements basic handlers for the ListItem events.""" + + last_item = None # the last item is an empty text box for user input + + def __init__(self, taglist): + """ + + @param taglist (quick_list_manager.QuickTagList): list manager + """ + ClickHandler.__init__(self) + FocusHandler.__init__(self) + ChangeHandler.__init__(self) + KeyboardHandler.__init__(self) + self.taglist = taglist + + def addItem(self, item): + raise NotImplementedError + + def removeItem(self, item): + raise NotImplementedError + + def onClick(self, sender): + """The remove button or a suggested completion item has been clicked.""" + #log.debug("onClick sender type: %s" % type(sender)) + if isinstance(sender, Button): + item = sender.getParent() + self.removeItem(item) + elif isinstance(sender, ListBox): + # this is called after onChange when you click a suggested item, and now we get the final value + textbox = sender._clickListeners[0] + self.checkValue(textbox) + else: + raise AssertionError + + def onFocus(self, sender): + """The text box has the focus.""" + #log.debug("onFocus sender type: %s" % type(sender)) + assert isinstance(sender, AutoCompleteTextBox) + sender.setCompletionItems(self.taglist.untagged) + + def onKeyUp(self, sender, keycode, modifiers): + """The text box is being modified - or ENTER key has been pressed.""" + # this is called after onChange when you press ENTER, and now we get the final value + #log.debug("onKeyUp sender type: %s" % type(sender)) + assert isinstance(sender, AutoCompleteTextBox) + if keycode == KEY_ENTER: + self.checkValue(sender) + + def onChange(self, sender): + """The text box has been changed by the user.""" + # this is called before the completion when you press ENTER or click a suggest item + #log.debug("onChange sender type: %s" % type(sender)) + assert isinstance(sender, AutoCompleteTextBox) + self.checkValue(sender) + + def checkValue(self, textbox): + """Internal handler to call when a new value is submitted by the user.""" + item = textbox.getParent() + if item.text == item.last_checked_value: + # this method has already been called (by self.onChange) and there's nothing new + return + item.refresh() + if item == self.last_item and item.last_validity == ListItem.VALID and item.text: + self.addItem() + +class DraggableListItemHandler(ListItemHandler, DragHandler): + """Implements basic handlers for the DraggableListItem events.""" + + def __init__(self, manager): + """ + + @param manager (ListManager): list manager + """ + ListItemHandler.__init__(self, manager) + DragHandler.__init__(self) + + @property + def manager(self): + return self.taglist + + def onDragStart(self, event): + """The user starts dragging the item.""" + self.manager.drop_target = None + + def onDragEnd(self, event): + """The user dropped the list item.""" + text, dummy = libervia_widget.eventGetData(event) + target = self.manager.drop_target # self or another ListPanel + if text == "" or target is None: + return + if target != self: # move the item from self to target + target.addItem(text) + self.removeItem(self.getItem(text)) + + +class ListPanel(FlowPanel, DraggableListItemHandler, libervia_widget.DropCell): + """Implements a list of items.""" + # XXX: beware that pyjamas.ui.FlowPanel is not fully implemented: + # - it can not be used with pyjamas.ui.Label + # - FlowPanel.insert doesn't work + + STYLE = {"listPanel": "listPanel"} + ACCEPT_NEW_ENTRY = False + + def __init__(self, manager, items=None): + """Initialization with a button for the list name (key) and a DraggableListItem. + + @param manager (ListManager): list manager + @param items (list): items to be set + """ + FlowPanel.__init__(self) + DraggableListItemHandler.__init__(self, manager) + libervia_widget.DropCell.__init__(self, None) + self.addStyleName(self.STYLE["listPanel"]) + self.manager = manager + self.resetItems(items) + + # FIXME: dirty magic strings '@' and '@@' + self.drop_keys = {"GROUP": lambda host, item_s: self.addItem("@%s" % item_s), + "CONTACT": lambda host, item_s: self.addItem(item_s), + "CONTACT_TITLE": lambda host, item_s: self.addItem('@@'), + "CONTACT_TEXTBOX": lambda host, item_s: setattr(self.manager, "drop_target", self), + } + + def onDrop(self, event): + """Something has been dropped in this ListPanel""" + try: + libervia_widget.DropCell.onDrop(self, event) + except base_widget.NoLiberviaWidgetException: + pass + + def getItem(self, text): + """Get an item from its text. + + @param text(unicode): item text + """ + for child in self.getChildren(): + if child.text == text: + return child + return None + + def getItems(self): + """Get the non empty items. + + @return list(unicode) + """ + return [widget.text for widget in self.getChildren() if isinstance(widget, ListItem) and widget.text] + + def validateItem(self, text): + """Return validation code after the item has been changed. + + @param text (unicode): item text to check + @return: int value defined by one of these constants: + - VALID if the item is valid + - INVALID if the item is not valid but can be displayed + - DUPLICATE if the item is a duplicate + """ + def count(list_, item): # XXX: list.count in not implemented by pyjamas + return len([elt for elt in list_ if elt == item]) + + if count(self.getItems(), text) > 1: + return ListItem.DUPLICATE # item already exists in this list so we suggest its deletion + if self.ACCEPT_NEW_ENTRY: + return ListItem.VALID + return ListItem.VALID if text in self.manager.items or not text else ListItem.INVALID + + def addItem(self, text=""): + """Add an item. + + @param text (unicode): text to be set. + @return: True if the item has been really added or merged. + """ + if text in self.getItems(): # avoid duplicate in the same list + return + + item = DraggableListItem(self, self.manager, self.validateItem) + self.add(item) + + if self.last_item: + if self.last_item.last_validity == ListItem.INVALID: + # switch the two values so that the invalid one stays in last position + item.setText(self.last_item.text) + self.last_item.setText(text) + elif not self.last_item.text: + # copy the new value to previous empty item + self.last_item.setText(text) + else: # first item of the list, or previous last item has been deleted + item.setText(text) + + self.last_item = item + self.last_item.setFocus(True) + + def removeItem(self, item): + """Remove an item. + + @param item(DraggableListItem): item to remove + """ + if item == self.last_item: + self.addItem("") + item.remove() # this also updates the taglist + + def resetItems(self, items): + """Reset the items. + + @param items (list): items to be set + """ + for child in self.getChildren(): + child.remove() + + self.addItem() + if not items: + return + + items.sort() + for item in items: + self.addItem(unicode(item)) + + +class ListManager(FlexTable, quick_list_manager.QuickTagList): + """Implements a table to manage one or several lists of items.""" + + STYLE = {"listManager-button": "group", + "listManager-button-cell": "listManager-button-cell", + } + + def __init__(self, data=None, items=None): + """ + @param data (dict{unicode: list}): dict binding keys to tagged items. + @param items (list): full list of items (tagged and untagged) + """ + FlexTable.__init__(self, Width="100%") + quick_list_manager.QuickTagList.__init__(self, [unicode(item) for item in items]) + self.lists = {} + + if data: + for key, items in data.iteritems(): + self.addList(key, [unicode(item) for item in items]) + + def addList(self, key, items=None): + """Add a Button and ListPanel for a new list. + + @param key (unicode): list name + @param items (list): items to append to the new list + """ + if key in self.lists: + return + + if items is None: + items = [] + + self.lists[key] = {"button": Button(key, Title=key, StyleName=self.STYLE["listManager-button"]), + "panel": ListPanel(self, items)} + + y, x = len(self.lists), 0 + self.insertRow(y) + self.setWidget(y, x, self.lists[key]["button"]) + self.setWidget(y, x + 1, self.lists[key]["panel"]) + self.getCellFormatter().setStyleName(y, x, self.STYLE["listManager-button-cell"]) + + try: + self.popup_menu.registerClickSender(self.lists[key]["button"]) + except (AttributeError, TypeError): # self.registerPopupMenuPanel hasn't been called yet + pass + + def removeList(self, key): + """Remove a ListPanel from this manager. + + @param key (unicode): list name + """ + items = self.lists[key]["panel"].getItems() + (y, x) = self.getIndex(self.lists[key]["button"]) + self.removeRow(y) + del self.lists[key] + self.untag(items) + + def untag(self, items): + """Untag some items. + + Check first if the items are not used in any panel. + + @param items (list): items to be removed + """ + items_assigned = set() + for values in self.getItemsByKey().itervalues(): + items_assigned.update(values) + quick_list_manager.QuickTagList.untag(self, [item for item in items if item not in items_assigned]) + + def getItemsByKey(self): + """Get the items grouped by list name. + + @return dict{unicode: list} + """ + return {key: self.lists[key]["panel"].getItems() for key in self.lists} + + def getKeysByItem(self): + """Get the keys groups by item. + + @return dict{object: set(unicode)} + """ + result = {} + for key in self.lists: + for item in self.lists[key]["panel"].getItems(): + result.setdefault(item, set()).add(key) + return result + + def registerPopupMenuPanel(self, entries, callback): + """Register a popup menu panel for the list names' buttons. + + @param entries (dict{unicode: dict{unicode: unicode}}): menu entries + @param callback (callable): common callback for all menu items, arguments are: + - button widget + - list name (item key) + """ + self.popup_menu = base_panel.PopupMenuPanel(entries, callback=callback) + for key in self.lists: # register click sender for already existing lists + self.popup_menu.registerClickSender(self.lists[key]["button"]) + + +class TagsPanel(base_panel.ToggleStackPanel): + """A toggle panel to set the tags""" + + TAGS = _("Tags") + + STYLE = {"main": "tagsPanel-main", + "tags": "tagsPanel-tags"} + + def __init__(self, suggested_tags, tags=None): + """ + + @param suggested_tags (list[unicode]): list of all suggested tags + @param tags (list[unicode]): already assigned tags + """ + base_panel.ToggleStackPanel.__init__(self, Width="100%") + self.addStyleName(self.STYLE["main"]) + + if tags is None: + tags = [] + + self.tags = ListPanel(quick_list_manager.QuickTagList(suggested_tags), tags) + self.tags.addStyleName(self.STYLE["tags"]) + self.tags.ACCEPT_NEW_ENTRY = True + self.add(self.tags, self.TAGS) + self.addStackChangeListener(self) + + def onStackChanged(self, sender, index, visible=None): + if visible is None: + visible = sender.getWidget(index).getVisible() + text = ", ".join(self.getTags()) + suffix = "" if (visible or not text) else (": %s" % text) + sender.setStackText(index, self.TAGS + suffix) + + def getTags(self): + return self.tags.getItems() + + def setTags(self, items): + self.tags.resetItems(items) +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/browser/sat_browser/logging.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,58 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2011-2018 Jérôme Poisson <goffi@goffi.org> + +# This program 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. + +# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. +"""This module configure logs for Libervia browser side""" + +from __pyjamas__ import console +from constants import Const as C +from sat.core import log # XXX: we don't use core.log_config here to avoid the impossible imports in pyjamas + + +class LiberviaLogger(log.Logger): + + def out(self, message, level=None): + try: + console + except: + # XXX: for older Firefox version, the displayed error is "libervia_main ReferenceError: console is not defined" + # but none of the following exception class is working: ReferenceError, TypeError, NameError, Exception... + # it works when you don't explicit a class, tested with Firefox 3.0.4 + print message + return + if level == C.LOG_LVL_DEBUG: + console.debug(message) + elif level == C.LOG_LVL_INFO: + console.info(message) + elif level == C.LOG_LVL_WARNING: + console.warn(message) + else: + console.error(message) + + +def configure(): + fmt = '[%(name)s] %(message)s' + log.configure(C.LOG_BACKEND_CUSTOM, + logger_class = LiberviaLogger, + level = C.LOG_LVL_DEBUG, + fmt = fmt, + output = None, + logger = None, + colors = False, + force_colors = False) + # FIXME: workaround for Pyjamas, need to be removed when Pyjamas is fixed + LiberviaLogger.fmt = fmt
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/browser/sat_browser/main_panel.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,314 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2011-2018 Jérôme Poisson <goffi@goffi.org> + +# This program 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. + +# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + +"""Panels used as main basis""" + +import pyjd # this is dummy in pyjs +from sat.core.log import getLogger +log = getLogger(__name__) + +from sat.core.i18n import _ +from sat_browser import strings + +from pyjamas.ui.DockPanel import DockPanel +from pyjamas.ui.HorizontalPanel import HorizontalPanel +from pyjamas.ui.VerticalPanel import VerticalPanel +from pyjamas.ui.Button import Button +from pyjamas.ui.HTML import HTML +from pyjamas.ui.ClickListener import ClickHandler +from pyjamas.Timer import Timer +from pyjamas.ui import HasVerticalAlignment + + +import menu +import dialog +import base_widget +import base_menu +import libervia_widget +import editor_widget +import html_tools +from constants import Const as C + + +### Warning notification (visibility of message, and other warning data) ### + + +class WarningPopup(): + + def __init__(self): + self._popup = None + self._timer = Timer(notify=self._timeCb) + self.timeout = None + self._html = None + self._last_type = None + self._last_html = None + + def showWarning(self, type_=None, msg=None, duration=2000): + """Display a popup information message, e.g. to notify the recipient of a message being composed. + + If type_ is None, a popup being currently displayed will be hidden. + @type_: a type determining the CSS style to be applied (see _showWarning) + @msg: message to be displayed + @duration(int, None): time (in ms) to display the message + """ + if type_ is None: + self._removeWarning() + return + + self.timeout = duration + + if not self._popup or self._last_type != type_ or self._last_html != msg: + self._showWarning(type_, msg) + + def _showWarning(self, type_, msg): + """Display a popup information message, e.g. to notify the recipient of a message being composed. + + @type_: a type determining the CSS style to be applied. For now the defined styles are + "NONE" (will do nothing), "PUBLIC", "GROUP", "STATUS" and "ONE2ONE". + @msg: message to be displayed + """ + if type_ == "NONE": + return + if not msg: + log.warning("no msg set") + return + if type_ == "PUBLIC": + style = "targetPublic" + elif type_ == "GROUP": + style = "targetGroup" + elif type_ == "STATUS": + style = "targetStatus" + elif type_ == "ONE2ONE": + style = "targetOne2One" + elif type_ == "INFO": + style = "notifInfo" + elif type_ == "WARNING": + style = "notifWarning" + else: + log.error("unknown message type") + return + + self._last_html = msg + + if self._popup is None: + self._popup = dialog.PopupPanelWrapper(autoHide=False, modal=False) + self._html = HTML(msg) + self._popup.add(self._html) + + left = 0 + top = 0 # max(0, self.getAbsoluteTop() - contents.getOffsetHeight() - 2) + self._popup.setPopupPosition(left, top) + self._popup.show() + else: + self._html.setHTML(msg) + + if type_ != self._last_type: + self._last_type = type_ + self._popup.setStyleName("warningPopup") + self._popup.addStyleName(style) + + if self.timeout is not None: + self._timer.schedule(self.timeout) + + def _timeCb(self, timer): + if self._popup: + self._popup.hide() + self._popup = None + + def _removeWarning(self): + """Remove the popup""" + self._timer.cancel() + self._timeCb(None) + + +### Status ### + + +class StatusPanel(editor_widget.HTMLTextEditor): + + EMPTY_STATUS = '<click to set a status>' + VALIDATE_WITH_SHIFT_ENTER = False + + def __init__(self, host, status=''): + self.host = host + modifiedCb = lambda content: self.host.bridge.call('setStatus', None, self.host.presence_status_panel.presence, content['text']) or True + editor_widget.HTMLTextEditor.__init__(self, {'text': status}, modifiedCb, options={'no_xhtml': True, 'listen_focus': True, 'listen_click': True}) + self.edit(False) + self.setStyleName('marginAuto') + + @property + def status(self): + return self._original_content['text'] + + def __cleanContent(self, content): + status = content['text'] + if status == self.EMPTY_STATUS or status in C.PRESENCE.values(): + content['text'] = '' + return content + + def getContent(self): + return self.__cleanContent(editor_widget.HTMLTextEditor.getContent(self)) + + def setContent(self, content): + content = self.__cleanContent(content) + editor_widget.BaseTextEditor.setContent(self, content) + + def setDisplayContent(self): + status = self._original_content['text'] + try: + presence = self.host.presence_status_panel.presence + except AttributeError: # during initialization + presence = None + if not status: + if presence and presence in C.PRESENCE: + status = C.PRESENCE[presence] + else: + status = self.EMPTY_STATUS + self.display.setHTML(strings.addURLToText(status)) + + +class PresenceStatusMenuBar(base_widget.WidgetMenuBar): + def __init__(self, parent): + styles = {'menu_bar': 'presence-button'} + base_widget.WidgetMenuBar.__init__(self, parent, parent.host, styles=styles) + self.button = self.addCategory(u"◉") + presence_menu = self.button.getSubMenu() + for presence, presence_i18n in C.PRESENCE.items(): + html = u'<span class="%s">◉</span> %s' % (html_tools.buildPresenceStyle(presence), presence_i18n) + presence_menu.addItem(html, True, base_menu.SimpleCmd(lambda presence=presence: self.changePresenceCb(presence))) + self.parent_panel = parent + + def changePresenceCb(self, presence=''): + """Callback to notice the backend of a new presence set by the user. + @param presence (unicode): the new presence is a value in ('', 'chat', 'away', 'dnd', 'xa') + """ + self.host.bridge.call('setStatus', None, presence, self.parent_panel.status_panel.status) + + @classmethod + def getCategoryHTML(cls, category): + """Build the html to be used for displaying a category item. + + @param category (quick_menus.MenuCategory): category to add + @return unicode: HTML to display + """ + return category + + +class PresenceStatusPanel(HorizontalPanel, ClickHandler): + + def __init__(self, host, presence="", status=""): + self.host = host + self.plugin_menu_context = [] + HorizontalPanel.__init__(self, Width='100%') + self.presence_bar = PresenceStatusMenuBar(self) + self.status_panel = StatusPanel(host, status=status) + self.setPresence(presence) + + panel = HorizontalPanel() + panel.add(self.presence_bar) + panel.add(self.status_panel) + panel.setCellVerticalAlignment(self.presence_bar, 'baseline') + panel.setCellVerticalAlignment(self.status_panel, 'baseline') + panel.setStyleName("presenceStatusPanel") + self.add(panel) + + self.status_panel.edit(False) + + ClickHandler.__init__(self) + self.addClickListener(self) + + @property + def presence(self): + return self._presence + + @property + def status(self): + return self.status_panel._original_content['text'] + + def setPresence(self, presence): + self._presence = presence + html_tools.setPresenceStyle(self.presence_bar.button, self._presence) + + def setStatus(self, status): + self.status_panel.setContent({'text': status}) + self.status_panel.setDisplayContent() + + def onClick(self, sender): + # As status is the default target of uniBar, we don't want to select anything if click on it + self.host.setSelected(None) + + +### Panels managing the main area ### + + +class MainPanel(DockPanel): + """The panel which take the whole screen""" + + def __init__(self, host): + self.host = host + DockPanel.__init__(self, StyleName="mainPanel liberviaTabPanel") + + # menu and status panel + self.header = VerticalPanel(StyleName="header") + self.menu = menu.MainMenuBar(host) + self.header.add(self.menu) + + # contacts + self.contacts_switch = Button(u'«', self._contactsSwitch) + self.contacts_switch.addStyleName('contactsSwitch') + + # tab panel + self.tab_panel = libervia_widget.MainTabPanel(host) + self.tab_panel.addWidgetsTab(_(u"Discussions"), select=True, locked=True) + + # XXX: widget's addition order is important! + self.add(self.header, DockPanel.NORTH) + self.add(self.tab_panel, DockPanel.CENTER) + self.setCellWidth(self.tab_panel, '100%') + self.setCellHeight(self.tab_panel, '100%') + self.add(self.tab_panel.getTabBar(), DockPanel.SOUTH) + + def addContactList(self, contact_list): + self.add(self.contacts_switch, DockPanel.WEST) + self.add(contact_list, DockPanel.WEST) + + def addPresenceStatusPanel(self, panel): + self.header.add(panel) + self.header.setCellHeight(panel, '100%') + self.header.setCellVerticalAlignment(panel, HasVerticalAlignment.ALIGN_BOTTOM) + + def _contactsSwitch(self, btn=None): + """ (Un)hide contacts panel """ + if btn is None: + btn = self.contacts_switch + clist = self.host.contact_list_widget + clist.setVisible(not clist.getVisible()) + btn.setText(u"«" if clist.getVisible() else u"»") + self.host.resize() + + def _contactsMove(self, parent): + """Move the contacts container (containing the contact list and + the "hide/show" button) to another parent, but always as the + first child position (insert at index 0). + """ + if self._contacts.getParent(): + if self._contacts.getParent() == parent: + return + self._contacts.removeFromParent() + parent.insert(self._contacts, 0)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/browser/sat_browser/menu.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,177 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2011-2018 Jérôme Poisson <goffi@goffi.org> + +# This program 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. + +# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + +import pyjd # this is dummy in pyjs +from sat.core.log import getLogger +log = getLogger(__name__) + +from pyjamas.ui.HTML import HTML +from pyjamas.ui.Frame import Frame + +from constants import Const as C +import file_tools +import xmlui +import chat +import dialog +import contact_group +import base_menu +from sat_browser import html_tools +from sat_browser import web_widget + + +unicode = str # FIXME: pyjamas workaround + + +class MainMenuBar(base_menu.GenericMenuBar): + """The main menu bar which is displayed on top of the document""" + + ITEM_TPL = "<img src='media/icons/menu/%s_menu_red.png' />%s" + + def __init__(self, host): + styles = {'moved_popup': 'menuLastPopup', 'menu_bar': 'mainMenuBar'} + base_menu.GenericMenuBar.__init__(self, host, vertical=False, styles=styles) + + @classmethod + def getCategoryHTML(cls, category): + """Build the html to be used for displaying a category item. + + @param category (quick_menus.MenuCategory): category to add + @return unicode: HTML to display + """ + name = html_tools.html_sanitize(category.name) + return cls.ITEM_TPL % (category.icon, name) if category.icon is not None else name + + ## callbacks + + # General menu + + def onDisconnect(self): + def confirm_cb(answer): + if answer: + self.host.disconnect(C.PROF_KEY_NONE) + _dialog = dialog.ConfirmDialog(confirm_cb, text="Do you really want to disconnect ?") + _dialog.show() + + #Contact menu + + def onManageContactGroups(self): + """Open the contact groups manager.""" + + def onCloseCallback(): + pass + + contact_group.ContactGroupEditor(self.host, None, onCloseCallback) + + #Group menu + def onJoinRoom(self): + + def invite(room_jid, contacts): + for contact in contacts: + self.host.bridge.call('inviteMUC', None, unicode(contact), unicode(room_jid)) + + def join(room_jid, contacts): + if self.host.whoami: + nick = self.host.whoami.node + contact_list = self.host.contact_list + if room_jid is None or room_jid not in contact_list.getSpecials(C.CONTACT_SPECIAL_GROUP): + room_jid_s = unicode(room_jid) if room_jid else '' + self.host.bridge.joinMUC(room_jid_s, nick, profile=C.PROF_KEY_NONE, callback=lambda room_jid: invite(room_jid, contacts), errback=self.host.onJoinMUCFailure) + else: + self.host.displayWidget(chat.Chat, room_jid, type_="group") + invite(room_jid, contacts) + + dialog.RoomAndContactsChooser(self.host, join, ok_button="Join", visible=(True, False)) + + + # Help menu + + def onOfficialChatRoom(self): + nick = self.host.whoami.node + self.host.bridge.joinMUC(self.host.default_muc, nick, profile=C.PROF_KEY_NONE, callback=lambda dummy: None, errback=self.host.onJoinMUCFailure) + + def onSocialContract(self): + _frame = Frame('contrat_social.html') + _frame.setStyleName('infoFrame') + _dialog = dialog.GenericDialog("Contrat Social", _frame) + _dialog.setSize('80%', '80%') + _dialog.show() + + def onAbout(self): + def gotVersions(): + _about = HTML("""<b>Libervia</b>, a Salut à Toi project<br /> + <br /> + Libervia is a web frontend for Salut à Toi<br /> + <span style='font-style: italic;'>SàT version:</span> {sat_version}<br/> + <span style='font-style: italic;'>Libervia version:</span> {libervia_version}<br/> + <br /> + You can contact the authors at <a href="mailto:contact@salut-a-toi.org">contact@salut-a-toi.org</a><br /> + Blog available (mainly in french) at <a href="http://www.goffi.org" target="_blank">http://www.goffi.org</a><br /> + Project page: <a href="http://salut-a-toi.org"target="_blank">http://salut-a-toi.org</a><br /> + <br /> + Any help welcome :) + <p style='font-size:small;text-align:center'>This project is dedicated to Roger Poisson</p> + """.format(sat_version=self.host.sat_version, libervia_version=self.host.libervia_version)) + _dialog = dialog.GenericDialog("About", _about) + _dialog.show() + self.host.getVersions(gotVersions) + + #Settings menu + + def onAccount(self): + def gotUI(xml_ui): + if not xml_ui: + return + body = xmlui.create(self.host, xml_ui) + _dialog = dialog.GenericDialog("Manage your account", body, options=['NO_CLOSE']) + body.setCloseCb(_dialog.close) + _dialog.show() + self.host.bridge.call('getAccountDialogUI', gotUI) + + def onParameters(self): + def gotParams(xml_ui): + if not xml_ui: + return + body = xmlui.create(self.host, xml_ui) + _dialog = dialog.GenericDialog("Parameters", body, options=['NO_CLOSE']) + _dialog.addStyleName("parameters") + body.setCloseCb(_dialog.close) + _dialog.setSize('80%', '80%') + _dialog.show() + self.host.bridge.getParamsUI(profile=C.PROF_KEY_NONE, callback=gotParams) + + def removeItemParams(self): + """Remove the Parameters item from the Settings menu bar.""" + self.menu_settings.removeItem(self.item_params) + + def onAvatarUpload(self): + body = file_tools.AvatarUpload() + _dialog = dialog.GenericDialog("Avatar upload", body, options=['NO_CLOSE']) + body.setCloseCb(_dialog.close) + _dialog.setWidth('40%') + _dialog.show() + + def onPublicBlog(self, contact_box, data, profile): + # FIXME: Q&D way to check domain, need to be done in a cleaner way + if contact_box.jid.domain != self.host._defaultDomain: + self.host.showDialog(u"Public blogs from other domains are not managed yet", "Can't show public blog", "error") + return + + url = '{}/blog/{}'.format(self.host.base_location, contact_box.jid.node) + widget = self.host.displayWidget(web_widget.WebWidget, url, show_url=False) + self.host.setSelected(widget)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/browser/sat_browser/nativedom.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,104 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2011-2018 Jérôme Poisson <goffi@goffi.org> + +# This program 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. + +# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + +""" +This class provide basic DOM parsing based on native javascript parser +__init__ code comes from Tim Down at http://stackoverflow.com/a/8412989 +""" + +from __pyjamas__ import JS + + +class Node(object): + + def __init__(self, js_node): + self._node = js_node + + def _jsNodesList2List(self, js_nodes_list): + ret = [] + for i in range(len(js_nodes_list)): + #ret.append(Element(js_nodes_list.item(i))) + ret.append(self.__class__(js_nodes_list.item(i))) # XXX: Ugly, but used to work around a Pyjamas's bug + return ret + + def __getattr__(self, name): + if name in ('TEXT_NODE', 'ELEMENT_NODE', 'ATTRIBUTE_NODE', 'COMMENT_NODE', 'nodeName', 'nodeType', 'wholeText'): + return getattr(self._node, name) + return object.__getattribute__(self, name) + + @property # XXX: doesn't work in --strict mode in pyjs + def childNodes(self): + return self._jsNodesList2List(self._node.childNodes) + + def getAttribute(self, attr): + return self._node.getAttribute(attr) + + def setAttribute(self, attr, value): + return self._node.setAttribute(attr, value) + + def hasAttribute(self, attr): + return self._node.hasAttribute(attr) + + def toxml(self): + return JS("""this._node.outerHTML || new XMLSerializer().serializeToString(this._node);""") + + +class Element(Node): + + def __init__(self, js_node): + Node.__init__(self, js_node) + + def getElementsByTagName(self, tagName): + return self._jsNodesList2List(self._node.getElementsByTagName(tagName)) + + +class Document(Node): + + def __init__(self, js_document): + Node.__init__(self, js_document) + + @property + def documentElement(self): + return Element(self._node.documentElement) + + +class NativeDOM: + + def __init__(self): + JS(""" + + if (typeof window.DOMParser != "undefined") { + this.parseXml = function(xmlStr) { + return ( new window.DOMParser() ).parseFromString(xmlStr, "text/xml"); + }; + } else if (typeof window.ActiveXObject != "undefined" && + new window.ActiveXObject("Microsoft.XMLDOM")) { + this.parseXml = function(xmlStr) { + var xmlDoc = new window.ActiveXObject("Microsoft.XMLDOM"); + xmlDoc.async = "false"; + xmlDoc.loadXML(xmlStr); + return xmlDoc; + }; + } else { + throw new Error("No XML parser found"); + } + """) + + def parseString(self, xml): + return Document(self.parseXml(xml))
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/browser/sat_browser/notification.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,150 @@ +from __pyjamas__ import JS, wnd +from sat.core.log import getLogger +log = getLogger(__name__) +from sat.core.i18n import _ + +from pyjamas import Window +from pyjamas.Timer import Timer +import favico.min.js + +import dialog + +TIMER_DELAY = 5000 + + +class Notification(object): + """ + If the browser supports it, the user allowed it to and the tab is in the + background, send desktop notifications on messages. + + Requires both Web Notifications and Page Visibility API. + """ + + def __init__(self, alerts_counter): + """ + + @param alerts_counter (FaviconCounter): counter instance + """ + self.alerts_counter = alerts_counter + self.enabled = False + user_agent = None + notif_permission = None + JS(""" + if (!('hidden' in document)) + document.hidden = false; + + user_agent = navigator.userAgent + + if (!('Notification' in window)) + return; + + notif_permission = Notification.permission + + if (Notification.permission === 'granted') + this.enabled = true; + + else if (Notification.permission === 'default') { + Notification.requestPermission(function(permission){ + if (permission !== 'granted') + return; + + self.enabled = true; //need to use self instead of this + }); + } + """) + + if "Chrome" in user_agent and notif_permission not in ['granted', 'denied']: + self.user_agent = user_agent + self._installChromiumWorkaround() + + wnd().onfocus = self.onFocus + # wnd().onblur = self.onBlur + + def _installChromiumWorkaround(self): + # XXX: Workaround for Chromium behaviour, it's doens't manage requestPermission on onLoad event + # see https://code.google.com/p/chromium/issues/detail?id=274284 + # FIXME: need to be removed if Chromium behaviour changes + try: + version_full = [s for s in self.user_agent.split() if "Chrome" in s][0].split('/')[1] + version = int(version_full.split('.')[0]) + except (IndexError, ValueError): + log.warning("Can't find Chromium version") + version = 0 + log.info("Chromium version: %d" % (version,)) + if version < 22: + log.info("Notification use the old prefixed version or are unmanaged") + return + if version < 32: + dialog.InfoDialog(_("Notifications activation for Chromium"), _('You need to activate notifications manually for your Chromium version.<br/>To activate notifications, click on the favicon on the left of the address bar')).show() + return + + log.info("==> Installing Chromium notifications request workaround <==") + self._old_click = wnd().onclick + wnd().onclick = self._chromiumWorkaround + + def _chromiumWorkaround(self): + log.info("Activating workaround") + JS(""" + Notification.requestPermission(function(permission){ + if (permission !== 'granted') + return; + self.enabled = true; //need to use self instead of this + }); + """) + wnd().onclick = self._old_click + + def onFocus(self, event=None): + self.alerts_counter.update(extra=0) + + # def onBlur(self, event=None): + # pass + + def isHidden(self): + JS("""return document.hidden;""") + + def _notify(self, title, body, icon): + if not self.enabled: + return + notification = None + # FIXME: icon has been removed because the notification can't display a HTTPS file + JS(""" + notification = new Notification(title, {body: body}); + // Probably won’t work, but it doesn’t hurt to try. + notification.addEventListener('click', function() { + window.focus(); + }); + """) + notification.onshow = lambda: Timer(TIMER_DELAY, lambda timer: notification.close()) + + def notify(self, title, body, icon='/media/icons/apps/48/sat.png'): + if self.isHidden(): + self._notify(title, body, icon) + + +class FaviconCounter(object): + """Display numbers over the favicon to signal e.g. waiting messages""" + + def __init__(self): + # XXX: the file favico.min.js is loaded from public/libervia.html because I get NS_ERROR_FAILURE when it's loaded with Pyjamas. It sounds like a context issue, with the favicon not being found. + + JS(""" + self.counter = new top.Favico({ + animation : 'slide', + bgColor: '#5CB85C', + }); + """) + + self.count = 0 # messages that are not displayed + self.extra = 0 # messages that are displayed but the window is hidden + + def update(self, count=None, extra=None): + """Update the favicon counter. + + @param count (int): primary counter + @param extra (int): extra counter + """ + if count is not None: + self.count = count + if extra is not None: + self.extra = extra + self.counter.badge(self.count + self.extra)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/browser/sat_browser/otrjs_wrapper.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,273 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia wrapper for otr.js +# Copyright (C) 2009-2018 Jérôme Poisson (goffi@goffi.org) +# Copyright (C) 2013-2016 Adrien Cossa (souliane@mailoo.org) + +# This program 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. + +# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + +"""This file is a wrapper for otr.js. It partially reproduces the usage +(modules, classes and attributes names) and behavior of potr, so you +can easily adapt some code based on potr to Pyjamas applications. + +potr is released under the GNU LESSER GENERAL PUBLIC LICENSE Version 3 + - https://github.com/python-otr/pure-python-otr/blob/master/LICENSE + +otr.js is released under the Mozilla Public Licence Version 2.0 + - https://github.com/arlolra/otr/blob/master/license +""" + +from __pyjamas__ import JS + +# should you re-use this class outside SàT, you can import __pyjamas__.console as log instead +from sat.core.log import getLogger +log = getLogger(__name__) + + +# XXX: pyjamas can't probably import more than one JS file, it messes the order. +# XXX: pyjamas needs the file to be in the compilation directory - no submodule. +# XXX: pyjamas needs the imported file to end with a empty line or semi-column. +# FIXME: fix these bugs upstream in Pyjamas +import otr.min.js + + +def isSupported(): + JS("""return (typeof OTR !== 'undefined');""") + + +if not isSupported(): + # see https://developer.mozilla.org/en-US/docs/Web/API/window.crypto.getRandomValues#Browser_Compatibility + log.error('Your browser is not implementing CSPRNG: OTR has been disabled.') + raise ImportError('CSPRNG is not supported by your browser') + + +class context(object): + + # Pre-declare these attributes to avoid the pylint "undefined variable" errors + STATUS_SEND_QUERY = None + STATUS_AKE_INIT = None + STATUS_AKE_SUCCESS = None + STATUS_END_OTR = None + STATE_PLAINTEXT = None + STATE_ENCRYPTED = None + STATE_FINISHED = None + OTR_TAG = None + OTR_VERSION_2 = None + OTR_VERSION_3 = None + WHITESPACE_TAG = None + WHITESPACE_TAG_V2 = None + WHITESPACE_TAG_V3 = None + + JS(""" + $cls_definition['STATUS_SEND_QUERY'] = OTR.CONST.STATUS_SEND_QUERY; + $cls_definition['STATUS_AKE_INIT'] = OTR.CONST.STATUS_AKE_INIT; + $cls_definition['STATUS_AKE_SUCCESS'] = OTR.CONST.STATUS_AKE_SUCCESS; + $cls_definition['STATUS_END_OTR'] = OTR.CONST.STATUS_END_OTR; + $cls_definition['STATE_PLAINTEXT'] = OTR.CONST.MSGSTATE_PLAINTEXT; + $cls_definition['STATE_ENCRYPTED'] = OTR.CONST.MSGSTATE_ENCRYPTED; + $cls_definition['STATE_FINISHED'] = OTR.CONST.MSGSTATE_FINISHED; + $cls_definition['OTR_TAG'] = OTR.CONST.OTR_TAG; + $cls_definition['OTR_VERSION_2'] = OTR.CONST.OTR_VERSION_2; + $cls_definition['OTR_VERSION_3'] = OTR.CONST.OTR_VERSION_3; + $cls_definition['WHITESPACE_TAG'] = OTR.CONST.WHITESPACE_TAG; + $cls_definition['WHITESPACE_TAG_V2'] = OTR.CONST.WHITESPACE_TAG_V2; + $cls_definition['WHITESPACE_TAG_V3'] = OTR.CONST.WHITESPACE_TAG_V3; + """) + + class UnencryptedMessage(Exception): + pass + + class Context(object): + + def __init__(self, account, peername): + self.user = account + self.peer = peername + self.trustName = self.peer + options = {'fragment_size': 140, + 'send_interval': 200, + 'priv': account.getPrivkey(), # this would generate the account key if it hasn't been done yet + 'debug': False, + } + JS("""self.otr = new OTR(options);""") + + for policy in ('ALLOW_V2', 'ALLOW_V3', 'REQUIRE_ENCRYPTION'): + setattr(self.otr, policy, self.getPolicy(policy)) + + self.otr.on('ui', self.receiveMessageCb) + self.otr.on('io', self.sendMessageCb) + self.otr.on('error', self.messageErrorCb) + self.otr.on('status', lambda status: self.setStateCb(self.otr.msgstate, status)) + self.otr.on('smp', self.smpAuthCb) + + @property + def state(self): + return self.otr.msgstate + + @state.setter + def state(self, state): + self.otr.msgstate = state + + def getCurrentKey(self): + return self.otr.their_priv_pk + + def setTrust(self, fingerprint, trustLevel): + self.user.setTrust(self.trustName, fingerprint, trustLevel) + + def setCurrentTrust(self, trustLevel): + self.setTrust(self.otr.their_priv_pk.fingerprint(), trustLevel) + + def getTrust(self, fingerprint, default=None): + return self.user.getTrust(self.trustName, fingerprint, default) + + def getCurrentTrust(self): + # XXX: the docstring of potr for the return value of this method is incorrect + if self.otr.their_priv_pk is None: + return None + return self.getTrust(self.otr.their_priv_pk.fingerprint(), None) + + def getUsedVersion(self): + """Return the otr version that is beeing used""" + # this method doesn't exist in potr, it has been added for convenience + try: + return self.otr.ake.otr_version + except AttributeError: + return None + + def disconnect(self): + self.otr.endOtr() + + def finish(self): + """Finish the session - avoid to send any message and the user has to manually disconnect""" + # it means TLV of type 1 (two first bytes), message length 0 (2 last bytes) + self.otr.handleTLVs('\x00\x01\x00\x00') + + def receiveMessage(self, msg): + """Received a message, ask otr.js to (try to) decrypt it""" + self.otr.receiveMsg(msg) + + def sendMessage(self, msg): + """Ask otr.js to encrypt a message for sending""" + self.otr.sendMsg(msg) + + def sendQueryMessage(self): + """Start or refresh an encryption communication""" + # otr.js offers this method, with potr you have to build the query message yourself + self.otr.sendQueryMsg() + + def inject(self, msg, appdata=None): + return self.sendMessageCb(msg, appdata) + + def getPolicy(self, key): + raise NotImplementedError + + def smpAuthSecret(self, secret, question=None): + return self.otr.smpSecret(secret, question) + + def smpAuthAbort(self, act=None): + # XXX: dirty hack to let the triggered method know who aborted the + # authentication. We need it to display the proper feedback and, + # if the correspondent aborted, set the conversation 'unverified'. + self.otr.sm.init() + JS("""self.otr.sm.sendMsg(OTR.HLP.packTLV(6, ''))""") + self.smpAuthCb('abort', '', act) + + def sendMessageCb(self, msg, meta): + """Actually send the message after it's been encrypted""" + raise NotImplementedError + + def receiveMessageCb(self, msg, encrypted): + """Display the message after it's been eventually decrypted""" + raise NotImplementedError + + def messageErrorCb(self, error): + """Message error callback""" + raise NotImplementedError + + def setStateCb(self, newstate): + raise NotImplementedError + + def smpAuthCb(self, newstate): + raise NotImplementedError + + class Account(object): + + def __init__(self, host): + self.host = host + self.privkey = None + self.trusts = {} + + def getPrivkey(self): + # the return value must have a method serializePrivateKey() + # if the key is not saved yet, call savePrivkey to generate it + if self.privkey is None: + self.privkey = self.loadPrivkey() + if self.privkey is None: + JS("""self.privkey = new DSA();""") + self.savePrivkey() + return self.privkey + + def setTrust(self, key, fingerprint, trustLevel): + if key not in self.trusts: + self.trusts[key] = {} + self.trusts[key][fingerprint] = trustLevel + self.saveTrusts() + + def getTrust(self, key, fingerprint, default=None): + if key not in self.trusts: + return default + return self.trusts[key].get(fingerprint, default) + + def loadPrivkey(self): + raise NotImplementedError + + def savePrivkey(self): + raise NotImplementedError + + def saveTrusts(self): + raise NotImplementedError + + +class crypt(object): + + class PK(object): + + def parsePrivateKey(self, key): + JS("""return DSA.parsePrivate(key);""") + + +class proto(object): + + @classmethod + def checkForOTR(cls, body): + """Helper method to check if the message contains OTR starting tag or whitespace + + @return: + - context.OTR_TAG if the message starts with it + - context.WHITESPACE_TAG if the message contains OTR whitespaces + - None otherwise + """ + if body.startswith(context.OTR_TAG): + return context.OTR_TAG + index = body.find(context.WHITESPACE_TAG) + if index < 0: + return False + tags = [body[i:i + 8] for i in range(index, len(body), 8)] + if [True for tag in tags if tag in (context.WHITESPACE_TAG_V2, context.WHITESPACE_TAG_V3)]: + return context.WHITESPACE_TAG + return None + + +# serialazePrivateKey is the method name in potr +JS("""DSA.serializePrivateKey = DSA.packPrivate;""")
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/browser/sat_browser/plugin_sec_otr.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,612 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia plugin for OTR encryption +# Copyright (C) 2009-2018 Jérôme Poisson (goffi@goffi.org) +# Copyright (C) 2013-2016 Adrien Cossa (souliane@mailoo.org) + +# This program 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. + +# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + +""" +This file is adapted from sat.plugins.plugin.sec_otr. It offers browser-side OTR encryption using otr.js. +The text messages to display are mostly taken from the Pidgin OTR plugin (GPL 2.0, see http://otr.cypherpunks.ca). +""" + +from sat.core.log import getLogger +log = getLogger(__name__) + +from sat.core.i18n import _, D_ +from sat.core import exceptions +from sat.tools import trigger + +from constants import Const as C +from sat_frontends.tools import jid +import otrjs_wrapper as otr +import dialog +import chat +import uuid +import time + + +NS_OTR = "otr_plugin" +PRIVATE_KEY = "PRIVATE KEY" +MAIN_MENU = D_('OTR') # TODO: get this constant directly from backend's plugin +DIALOG_EOL = "<br />" + +AUTH_TRUSTED = D_("Verified") +AUTH_UNTRUSTED = D_("Unverified") +AUTH_OTHER_TITLE = D_("Authentication of {jid}") +AUTH_US_TITLE = D_("Authentication to {jid}") +AUTH_TRUST_NA_TITLE = D_("Authentication requirement") +AUTH_TRUST_NA_TXT = D_("You must start an OTR conversation before authenticating your correspondent.") +AUTH_INFO_TXT = D_("Authenticating a correspondent helps ensure that the person you are talking to is who he or she claims to be.{eol}{eol}") +AUTH_FINGERPRINT_YOURS = D_("Your fingerprint is:{eol}{fingerprint}{eol}{eol}Start an OTR conversation to have your correspondent one.") +AUTH_FINGERPRINT_TXT = D_("To verify the fingerprint, contact your correspondent via some other authenticated channel (i.e. not in this chat), such as the telephone or GPG-signed email. Each of you should tell your fingerprint to the other.{eol}{eol}") +AUTH_FINGERPRINT_VERIFY = D_("Fingerprint for you, {you}:{eol}{your_fp}{eol}{eol}Purported fingerprint for {other}:{eol}{other_fp}{eol}{eol}Did you verify that this is in fact the correct fingerprint for {other}?") +AUTH_QUEST_DEFINE_TXT = D_("To authenticate using a question, pick a question whose answer is known only to you and your correspondent. Enter this question and this answer, then wait for your correspondent to enter the answer too. If the answers don't match, then you may be talking to an imposter.{eol}{eol}") +AUTH_QUEST_DEFINE = D_("Enter question here:{eol}") +AUTH_QUEST_ANSWER_TXT = D_("Your correspondent is attempting to determine if he or she is really talking to you, or if it's someone pretending to be you. Your correspondent has asked a question, indicated below. To authenticate to your correspondent, enter the answer and click OK.{eol}{eol}") +AUTH_QUEST_ANSWER = D_("This is the question asked by your correspondent:{eol}{question}") +AUTH_SECRET_INPUT = D_("{eol}{eol}Enter secret answer here: (case sensitive){eol}") +AUTH_ABORTED_TXT = D_("Authentication aborted.") +AUTH_FAILED_TXT = D_("Authentication failed.") +AUTH_OTHER_OK = D_("Authentication successful.") +AUTH_US_OK = D_("Your correspondent has successfully authenticated you.") +AUTH_OTHER_TOO = D_("You may want to authenticate your correspondent as well by asking your own question.") +AUTH_STATUS = D_("The current conversation is now {state}.") + +FINISHED_CONTEXT_TITLE = D_('Finished OTR conversation with {jid}') +SEND_PLAIN_IN_FINISHED_CONTEXT = D_("Your message was not sent because your correspondent closed the OTR conversation on his/her side. Either close your own side, or refresh the session.") +RECEIVE_PLAIN_IN_ENCRYPTED_CONTEXT = D_("WARNING: received unencrypted data in a supposedly encrypted context!") + +QUERY_ENCRYPTED = D_('Attempting to refresh the OTR conversation with {jid}...') +QUERY_NOT_ENCRYPTED = D_('Attempting to start an OTR conversation with {jid}...') +AKE_ENCRYPTED = D_(" conversation with {jid} started. Your client is not logging this conversation.") +AKE_NOT_ENCRYPTED = D_("ERROR: successfully ake'd with {jid} but the conversation is not encrypted!") +END_ENCRYPTED = D_("ERROR: the OTR session ended but the context is still supposedly encrypted!") +END_PLAIN_NO_MORE = D_("Your conversation with {jid} is no more encrypted.") +END_PLAIN_HAS_NOT = D_("Your conversation with {jid} hasn't been encrypted.") +END_FINISHED = D_("{jid} has ended his or her private conversation with you; you should do the same.") + +KEY_TITLE = D_('Private key') +KEY_NA_TITLE = D_("No private key") +KEY_NA_TXT = D_("You don't have any private key yet.") +KEY_DROP_TITLE = D_('Drop your private key') +KEY_DROP_TXT = D_("You private key is used to encrypt messages for your correspondent, nobody except you must know it, if you are in doubt, you should drop it!{eol}{eol}Are you sure you want to drop your private key?") +KEY_DROPPED_TXT = D_("Your private key has been dropped.") + +QUERY_TITLE = D_("Going encrypted") +QUERY_RECEIVED = D_("{jid} is willing to start with you an OTR encrypted conversation.{eol}{eol}") +QUERY_SEND = D_("You are about to start an OTR encrypted conversation with {jid}.{eol}{eol}") +QUERY_SLOWDOWN = D_("This end-to-end encryption is computed by your web browser and you may experience slowdowns.{eol}{eol}") +QUERY_NO_KEY = D_("This will take up to 10 seconds to generate your single use private key and start the conversation. In a future version of Libervia, your private key will be safely and persistently stored, so you will have to generate it only once.{eol}{eol}") +QUERY_KEY = D_("You already have a private key, but to start the conversation will still require a couple of seconds.{eol}{eol}") +QUERY_CONFIRM = D_("Press OK to start now the encryption.") + +ACTION_NA_TITLE = D_("Impossible action") +ACTION_NA = D_("Your correspondent must be connected to start an OTR conversation with him.") + +DEFAULT_POLICY_FLAGS = { + 'ALLOW_V2': True, + 'ALLOW_V3': True, + 'REQUIRE_ENCRYPTION': False, + 'SEND_WHITESPACE_TAG': False, # FIXME: we need to complete sendMessageTg before turning this to True + 'WHITESPACE_START_AKE': False, # FIXME: we need to complete newMessageTg before turning this to True +} + +# list a couple of texts or htmls (untrusted, trusted) for each state +OTR_MSG_STATES = { + otr.context.STATE_PLAINTEXT: [ + '<img src="media/icons/silk/lock_open.png" /><img src="media/icons/silk/key_delete.png" />', + '<img src="media/icons/silk/lock_open.png" /><img src="media/icons/silk/key.png" />' + ], + otr.context.STATE_ENCRYPTED: [ + '<img src="media/icons/silk/lock.png" /><img src="media/icons/silk/key_delete.png" />', + '<img src="media/icons/silk/lock.png" /><img src="media/icons/silk/key.png" />' + ], + otr.context.STATE_FINISHED: [ + '<img src="media/icons/silk/lock_break.png" /><img src="media/icons/silk/key_delete.png" />', + '<img src="media/icons/silk/lock_break.png" /><img src="media/icons/silk/key.png" />' + ] +} + + +unicode = str # FIXME: pyjamas workaround + + +class NotConnectedEntity(Exception): + pass + + +class Context(otr.context.Context): + + def __init__(self, host, account, other_jid): + """ + + @param host (satWebFrontend) + @param account (Account) + @param other_jid (jid.JID): JID of the person your chat correspondent + """ + super(Context, self).__init__(account, other_jid) + self.host = host + + def getPolicy(self, key): + """Get the value of the specified policy + + @param key (unicode): a value in: + - ALLOW_V1 (apriori removed from otr.js) + - ALLOW_V2 + - ALLOW_V3 + - REQUIRE_ENCRYPTION + - SEND_WHITESPACE_TAG + - WHITESPACE_START_AKE + - ERROR_START_AKE + @return: unicode + """ + if key in DEFAULT_POLICY_FLAGS: + return DEFAULT_POLICY_FLAGS[key] + else: + return False + + def receiveMessageCb(self, msg, encrypted): + assert isinstance(self.peer, jid.JID) + if not encrypted: + log.warning(u"A plain-text message has been handled by otr.js") + log.debug(u"message received (was %s): %s" % ('encrypted' if encrypted else 'plain', msg)) + uuid_ = str(uuid.uuid4()) # FIXME + if not encrypted: + if self.state == otr.context.STATE_ENCRYPTED: + log.warning(u"Received unencrypted message in an encrypted context (from %(jid)s)" % {'jid': self.peer}) + self.host.newMessageHandler(uuid_, time.time(), unicode(self.peer), unicode(self.host.whoami), {'': RECEIVE_PLAIN_IN_ENCRYPTED_CONTEXT}, {}, C.MESS_TYPE_INFO, {}) + self.host.newMessageHandler(uuid_, time.time(), unicode(self.peer), unicode(self.host.whoami), {'': msg}, {}, C.MESS_TYPE_CHAT, {}) + + def sendMessageCb(self, msg, meta=None): + assert isinstance(self.peer, jid.JID) + log.debug(u"message to send%s: %s" % ((' (attached meta data: %s)' % meta) if meta else '', msg)) + self.host.bridge.call('sendMessage', (None, self.host.sendError), unicode(self.peer), msg, '', C.MESS_TYPE_CHAT, {'send_only': 'true'}) + + def messageErrorCb(self, error): + log.error(u'error occured: %s' % error) + + def setStateCb(self, msg_state, status): + if status == otr.context.STATUS_AKE_INIT: + return + + other_jid_s = self.peer + feedback = _(u"Error: the state of the conversation with %s is unknown!") + trust = self.getCurrentTrust() + + if status == otr.context.STATUS_SEND_QUERY: + feedback = QUERY_ENCRYPTED if msg_state == otr.context.STATE_ENCRYPTED else QUERY_NOT_ENCRYPTED + + elif status == otr.context.STATUS_AKE_SUCCESS: + trusted_str = AUTH_TRUSTED if trust else AUTH_UNTRUSTED + feedback = (trusted_str + AKE_ENCRYPTED) if msg_state == otr.context.STATE_ENCRYPTED else AKE_NOT_ENCRYPTED + + elif status == otr.context.STATUS_END_OTR: + if msg_state == otr.context.STATE_PLAINTEXT: + feedback = END_PLAIN_NO_MORE + elif msg_state == otr.context.STATE_ENCRYPTED: + log.error(END_ENCRYPTED) + elif msg_state == otr.context.STATE_FINISHED: + feedback = END_FINISHED + + uuid_ = str(uuid.uuid4()) # FIXME + self.host.newMessageHandler(uuid_, time.time(), unicode(self.peer), unicode(self.host.whoami), {'': feedback.format(jid=other_jid_s)}, {}, C.MESS_TYPE_INFO, {'header_info': OTR.getInfoText(msg_state, trust)}) + + def setCurrentTrust(self, new_trust='', act='asked', type_='trust'): + log.debug(u"setCurrentTrust: trust={trust}, act={act}, type={type}".format(type=type_, trust=new_trust, act=act)) + title = (AUTH_OTHER_TITLE if act == "asked" else AUTH_US_TITLE).format(jid=self.peer) + old_trust = self.getCurrentTrust() + if type_ == 'abort': + msg = AUTH_ABORTED_TXT + elif new_trust: + if act == "asked": + msg = AUTH_OTHER_OK + else: + msg = AUTH_US_OK + if not old_trust: + msg += " " + AUTH_OTHER_TOO + else: + msg = AUTH_FAILED_TXT + dialog.InfoDialog(title, msg, AddStyleName="maxWidthLimit").show() + if act != "asked": + return + otr.context.Context.setCurrentTrust(self, new_trust) + if old_trust != new_trust: + feedback = AUTH_STATUS.format(state=(AUTH_TRUSTED if new_trust else AUTH_UNTRUSTED).lower()) + uuid_ = str(uuid.uuid4()) # FIXME + self.host.newMessageHandler(uuid_, time.time(), unicode(self.peer), unicode(self.host.whoami), {'': feedback}, {}, C.MESS_TYPE_INFO, {'header_info': OTR.getInfoText(self.state, new_trust)}) + + def fingerprintAuthCb(self): + """OTR v2 authentication using manual fingerprint comparison""" + priv_key = self.user.privkey + + if priv_key is None: # OTR._authenticate should not let us arrive here + raise exceptions.InternalError + return + + other_key = self.getCurrentKey() + if other_key is None: + # we have a private key, but not the fingerprint of our correspondent + msg = (AUTH_INFO_TXT + AUTH_FINGERPRINT_YOURS).format(fingerprint=priv_key.fingerprint(), eol=DIALOG_EOL) + dialog.InfoDialog(_("Fingerprint"), msg, AddStyleName="maxWidthLimit").show() + return + + def setTrust(confirm): + self.setCurrentTrust('fingerprint' if confirm else '') + + text = (AUTH_INFO_TXT + "<i>" + AUTH_FINGERPRINT_TXT + "</i>" + AUTH_FINGERPRINT_VERIFY).format(you=self.host.whoami, your_fp=priv_key.fingerprint(), other=self.peer, other_fp=other_key.fingerprint(), eol=DIALOG_EOL) + title = AUTH_OTHER_TITLE.format(jid=self.peer) + dialog.ConfirmDialog(setTrust, text, title, AddStyleName="maxWidthLimit").show() + + def smpAuthCb(self, type_, data, act=None): + """OTR v3 authentication using the socialist millionaire protocol. + + @param type_ (unicode): a value in ('question', 'trust', 'abort') + @param data (unicode, bool): this could be: + - a string containing the question if type_ is 'question' + - a boolean value telling if the authentication succeed when type_ is 'trust' + @param act (unicode): a value in ('asked', 'answered') + """ + log.debug(u"smpAuthCb: type={type}, data={data}, act={act}".format(type=type_, data=data, act=act)) + if act is None: + if type_ == 'question': + act = 'answered' # OTR._authenticate calls this method with act="asked" + elif type_ == 'abort': + act = 'asked' # smpAuthAbort triggers this method with act='answered' when needed + + # FIXME upstream: if the correspondent uses Pidgin and authenticate us via + # fingerprints, we will reach this code... that's wrong, this method is for SMP! + # There's probably a bug to fix in otr.js. Do it together with the issue that + # make us need the dirty self.smpAuthAbort. + else: + log.error("FIXME: unmanaged ambiguous 'act' value in Context.smpAuthCb!") + title = (AUTH_OTHER_TITLE if act == "asked" else AUTH_US_TITLE).format(jid=self.peer) + if type_ == 'question': + if act == 'asked': + def cb(result, question, answer=None): + if not result or not answer: # dialog cancelled or the answer is empty + return + self.smpAuthSecret(answer, question) + text = (AUTH_INFO_TXT + "<i>" + AUTH_QUEST_DEFINE_TXT + "</i>" + AUTH_QUEST_DEFINE).format(eol=DIALOG_EOL) + dialog.PromptDialog(cb, [text, AUTH_SECRET_INPUT.format(eol=DIALOG_EOL)], title=title, AddStyleName="maxWidthLimit").show() + else: + def cb(result, answer): + if not result or not answer: # dialog cancelled or the answer is empty + self.smpAuthAbort('answered') + return + self.smpAuthSecret(answer) + text = (AUTH_INFO_TXT + "<i>" + AUTH_QUEST_ANSWER_TXT + "</i>" + AUTH_QUEST_ANSWER).format(eol=DIALOG_EOL, question=data) + dialog.PromptDialog(cb, [text + AUTH_SECRET_INPUT.format(eol=DIALOG_EOL)], title=title, AddStyleName="maxWidthLimit").show() + elif type_ == 'trust': + self.setCurrentTrust('smp' if data else '', act) + elif type_ == 'abort': + self.setCurrentTrust('', act, 'abort') + + def disconnect(self): + """Disconnect the session.""" + if self.state != otr.context.STATE_PLAINTEXT: + super(Context, self).disconnect() + + def finish(self): + """Finish the session - avoid to send any message but the user still has to end the session himself.""" + if self.state == otr.context.STATE_ENCRYPTED: + super(Context, self).finish() + + +class Account(otr.context.Account): + + def __init__(self, host): + log.debug(u"new account: %s" % host.whoami) + if not host.whoami.resource: + log.warning("Account created without resource") + super(Account, self).__init__(host.whoami) + self.host = host + + def loadPrivkey(self): + return self.privkey + + def savePrivkey(self): + # TODO: serialize and encrypt the private key and save it to a HTML5 persistent storage + # We need to ask the user before saving the key (e.g. if he's not on his private machine) + # self.privkey.serializePrivateKey() --> encrypt --> store + if self.privkey is None: + raise exceptions.InternalError(_("Save is called but privkey is None !")) + pass + + def saveTrusts(self): + # TODO save the trusts as it would be done for the private key + pass + + +class ContextManager(object): + + def __init__(self, host): + self.host = host + self.account = Account(host) + self.contexts = {} + + def startContext(self, other_jid): + assert isinstance(other_jid, jid.JID) # never start an OTR session with a bare JID + # FIXME upstream: apparently pyjamas doesn't implement setdefault well, it ignores JID.__hash__ redefinition + #context = self.contexts.setdefault(other_jid, Context(self.host, self.account, other_jid)) + if other_jid not in self.contexts: + self.contexts[other_jid] = Context(self.host, self.account, other_jid) + return self.contexts[other_jid] + + def getContextForUser(self, other_jid, start=True): + """Get the context for the given JID + + @param other_jid (jid.JID): your correspondent + @param start (bool): start non-existing context if True + @return: Context + """ + try: + other_jid = self.fixResource(other_jid) + except NotConnectedEntity: + log.debug(u"getContextForUser [%s]: not connected!" % other_jid) + return None + log.debug(u"getContextForUser [%s]" % other_jid) + if start: + return self.startContext(other_jid) + else: + return self.contexts.get(other_jid, None) + + def getContextsForBareUser(self, bare_jid): + """Get all the contexts for the users sharing the given bare JID. + + @param bare_jid (jid.JID): bare JID + @return: list[Context] + """ + return [context for other_jid, context in self.contexts.iteritems() if other_jid.bare == bare_jid] + + def fixResource(self, other_jid): + """Return the full JID in case the resource of the given JID is missing. + + @param other_jid (jid.JID): JID to check + @return jid.JID + """ + if other_jid.resource: + return other_jid + clist = self.host.contact_list + if clist.getCache(other_jid.bare, C.PRESENCE_SHOW) is None: + raise NotConnectedEntity + return clist.getFullJid(other_jid) + + +class OTR(object): + + def __init__(self, host): + log.info(_(u"OTR plugin initialization")) + self.host = host + self.context_manager = None + self.host.bridge._registerMethods(["skipOTR"]) + self.host.trigger.add("messageNewTrigger", self.newMessageTg, priority=trigger.TriggerManager.MAX_PRIORITY) + self.host.trigger.add("messageSendTrigger", self.sendMessageTg, priority=trigger.TriggerManager.MAX_PRIORITY) + + # FIXME: workaround for a pyjamas issue: calling hash on a class method always return a different value if that method is defined directly within the class (with the "def" keyword) + self._profilePluggedListener = self.profilePluggedListener + self._gotMenusListener = self.gotMenusListener + # FIXME: these listeners are never removed, can't be removed by themselves (it modifies the list while looping), maybe need a 'one_shot' argument + self.host.addListener('profilePlugged', self._profilePluggedListener) + self.host.addListener('gotMenus', self._gotMenusListener) + + @classmethod + def getInfoText(self, state=otr.context.STATE_PLAINTEXT, trust=''): + """Get the widget info text for a certain message state and trust. + + @param state (unicode): message state + @param trust (unicode): trust + @return: unicode + """ + if not state: + state = OTR_MSG_STATES.keys()[0] + return OTR_MSG_STATES[state][1 if trust else 0] + + def getInfoTextForUser(self, other_jid): + """Get the current info text for a conversation. + + @param other_jid (jid.JID): JID of the correspondant + """ + otrctx = self.context_manager.getContextForUser(other_jid, start=False) + if otrctx is None: + return OTR.getInfoText() + else: + return OTR.getInfoText(otrctx.state, otrctx.getCurrentTrust()) + + def gotMenusListener(self,): + # TODO: get menus paths to hook directly from backend's OTR plugin + self.host.menus.addMenuHook(C.MENU_SINGLE, (MAIN_MENU, D_(u"Start/Refresh")), callback=self._startRefresh) + self.host.menus.addMenuHook(C.MENU_SINGLE, (MAIN_MENU, D_(u"End session")), callback=self._endSession) + self.host.menus.addMenuHook(C.MENU_SINGLE, (MAIN_MENU, D_(u"Authenticate")), callback=self._authenticate) + self.host.menus.addMenuHook(C.MENU_SINGLE, (MAIN_MENU, D_(u"Drop private key")), callback=self._dropPrivkey) + + def profilePluggedListener(self, profile): + # FIXME: workaround for a pyjamas issue: calling hash on a class method always return a different value if that method is defined directly within the class (with the "def" keyword) + self._presenceListener = self.presenceListener + self._disconnectListener = self.disconnectListener + self.host.addListener('presence', self._presenceListener, [C.PROF_KEY_NONE]) + # FIXME: this listener is never removed, can't be removed by itself (it modifies the list while looping), maybe need a 'one_shot' argument + self.host.addListener('disconnect', self._disconnectListener, [C.PROF_KEY_NONE]) + + self.host.bridge.call('skipOTR', None) + self.context_manager = ContextManager(self.host) + # TODO: retrieve the encrypted private key from a HTML5 persistent storage, + # decrypt it, parse it with otr.crypt.PK.parsePrivateKey(privkey) and + # assign it to self.context_manager.account.privkey + + def disconnectListener(self, profile): + """Things to do just before the profile disconnection""" + self.host.removeListener('presence', self._presenceListener) + + for context in self.context_manager.contexts.values(): + context.disconnect() # FIXME: no time to send the message before the profile has been disconnected + + def presenceListener(self, entity, show, priority, statuses, profile): + if show == C.PRESENCE_UNAVAILABLE: + self.endSession(entity, disconnect=False) + + def newMessageTg(self, uid, timestamp, from_jid, to_jid, msg, subject, msg_type, extra, profile): + if msg_type != C.MESS_TYPE_CHAT: + return True + + try: + msg = msg.values()[0] # FIXME: Q&D fix for message refactoring, message is now a dict + except IndexError: + return True + tag = otr.proto.checkForOTR(msg) + if tag is None or (tag == otr.context.WHITESPACE_TAG and not DEFAULT_POLICY_FLAGS['WHITESPACE_START_AKE']): + return True + + other_jid = to_jid if from_jid.bare == self.host.whoami.bare else from_jid + otrctx = self.context_manager.getContextForUser(other_jid, start=False) + if otrctx is None: + def confirm(confirm): + if confirm: + self.host.displayWidget(chat.Chat, other_jid) + self.context_manager.startContext(other_jid).receiveMessage(msg) + else: + # FIXME: plain text messages with whitespaces would be lost here when WHITESPACE_START_AKE is True + pass + key = self.context_manager.account.privkey + question = QUERY_RECEIVED + QUERY_SLOWDOWN + (QUERY_KEY if key else QUERY_NO_KEY) + QUERY_CONFIRM + dialog.ConfirmDialog(confirm, question.format(jid=other_jid, eol=DIALOG_EOL), QUERY_TITLE, AddStyleName="maxWidthLimit").show() + else: # do not ask for user confirmation if the context exist + otrctx.receiveMessage(msg) + + return False # interrupt the main process + + def sendMessageTg(self, to_jid, message, subject, mess_type, extra, callback, errback, profile_key): + if mess_type != C.MESS_TYPE_CHAT: + return True + + otrctx = self.context_manager.getContextForUser(to_jid, start=False) + if otrctx is not None and otrctx.state != otr.context.STATE_PLAINTEXT: + if otrctx.state == otr.context.STATE_ENCRYPTED: + log.debug(u"encrypting message") + otrctx.sendMessage(message) + uuid_ = str(uuid.uuid4()) # FIXME + self.host.newMessageHandler(uuid_, time.time(), unicode(self.host.whoami), unicode(to_jid), {'': message}, {}, mess_type, extra) + else: + feedback = SEND_PLAIN_IN_FINISHED_CONTEXT + dialog.InfoDialog(FINISHED_CONTEXT_TITLE.format(jid=to_jid), feedback, AddStyleName="maxWidthLimit").show() + return False # interrupt the main process + + log.debug(u"sending message unencrypted") + return True + + def endSession(self, other_jid, disconnect=True): + """Finish or disconnect an OTR session + + @param other_jid (jid.JID): other JID + @param disconnect (bool): if False, finish the session but do not disconnect it + """ + # checking for private key existence is not needed, context checking is enough + if other_jid.resource: + contexts = [self.context_manager.getContextForUser(other_jid, start=False)] + else: # contact disconnected itself so we need to terminate the OTR session but the Chat panel lost its resource + contexts = self.context_manager.getContextsForBareUser(other_jid) + for otrctx in contexts: + if otrctx is None or otrctx.state == otr.context.STATE_PLAINTEXT: + if disconnect: + uuid_ = str(uuid.uuid4()) # FIXME + self.host.newMessageHandler(uuid_, time.time(), unicode(other_jid), unicode(self.host.whoami), {'': END_PLAIN_HAS_NOT.format(jid=other_jid)}, {}, C.MESS_TYPE_INFO, {}) + return + if disconnect: + otrctx.disconnect() + else: + otrctx.finish() + + # Menu callbacks + + def _startRefresh(self, caller, menu_data, profile): + """Start or refresh an OTR session + + @param menu_data: %(menu_data)s + """ + def query(other_jid): + otrctx = self.context_manager.getContextForUser(other_jid) + if otrctx: + otrctx.sendQueryMessage() + + other_jid = jid.JID(menu_data['jid']) + clist = self.host.contact_list + if clist.getCache(other_jid.bare, C.PRESENCE_SHOW) is None: + dialog.InfoDialog(ACTION_NA_TITLE, ACTION_NA, AddStyleName="maxWidthLimit").show() + return + + key = self.context_manager.account.privkey + if key is None: + def confirm(confirm): + if confirm: + query(other_jid) + msg = QUERY_SEND + QUERY_SLOWDOWN + QUERY_NO_KEY + QUERY_CONFIRM + dialog.ConfirmDialog(confirm, msg.format(jid=other_jid, eol=DIALOG_EOL), QUERY_TITLE, AddStyleName="maxWidthLimit").show() + else: # on query reception we ask always, if we initiate we just ask the first time + query(other_jid) + + def _endSession(self, caller, menu_data, profile): + """End an OTR session + + @param menu_data: %(menu_data)s + """ + self.endSession(jid.JID(menu_data['jid'])) + + def _authenticate(self, caller, menu_data, profile): + """Authenticate other user and see our own fingerprint + + @param menu_data: %(menu_data)s + @param profile: %(doc_profile)s + """ + def not_available(): + dialog.InfoDialog(AUTH_TRUST_NA_TITLE, AUTH_TRUST_NA_TXT, AddStyleName="maxWidthLimit").show() + + to_jid = jid.JID(menu_data['jid']) + + # checking for private key existence is not needed, context checking is enough + otrctx = self.context_manager.getContextForUser(to_jid, start=False) + if otrctx is None or otrctx.state != otr.context.STATE_ENCRYPTED: + not_available() + return + otr_version = otrctx.getUsedVersion() + if otr_version == otr.context.OTR_VERSION_2: + otrctx.fingerprintAuthCb() + elif otr_version == otr.context.OTR_VERSION_3: + otrctx.smpAuthCb('question', None, 'asked') + else: + not_available() + + def _dropPrivkey(self, caller, menu_data, profile): + """Drop our private Key + + @param menu_data: %(menu_data)s + @param profile: %(doc_profile)s + """ + priv_key = self.context_manager.account.privkey + if priv_key is None: + # we have no private key yet + dialog.InfoDialog(KEY_NA_TITLE, KEY_NA_TXT, AddStyleName="maxWidthLimit").show() + return + + def dropKey(confirm): + if confirm: + # we end all sessions + for context in self.context_manager.contexts.values(): + context.disconnect() + self.context_manager.contexts.clear() + self.context_manager.account.privkey = None + dialog.InfoDialog(KEY_TITLE, KEY_DROPPED_TXT, AddStyleName="maxWidthLimit").show() + + dialog.ConfirmDialog(dropKey, KEY_DROP_TXT.format(eol=DIALOG_EOL), KEY_DROP_TITLE, AddStyleName="maxWidthLimit").show()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/browser/sat_browser/plugin_xep_0085.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,77 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# SAT plugin for Chat State Notifications Protocol (xep-0085) +# Copyright (C) 2013-2016 Adrien Cossa (souliane@mailoo.org) + +# This program 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. + +# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + +from pyjamas.Timer import Timer + + +# Copy of the map from sat/src/plugins/plugin_xep_0085 +TRANSITIONS = {"active": {"next_state": "inactive", "delay": 120}, + "inactive": {"next_state": "gone", "delay": 480}, + "gone": {"next_state": "", "delay": 0}, + "composing": {"next_state": "paused", "delay": 30}, + "paused": {"next_state": "inactive", "delay": 450} + } + + +class ChatStateMachine: + """This is an adapted version of the ChatStateMachine from sat/src/plugins/plugin_xep_0085 + which manage a timer on the web browser and keep it synchronized with the timer that runs + on the backend. This is only needed to avoid calling the bridge method chatStateComposing + too often ; accuracy is not needed so we can ignore the delay of the communication between + the web browser and the backend (the timer on the web browser always starts a bit before). + /!\ Keep this file up to date if you modify the one in the sat plugins directory. + """ + def __init__(self, host, target_s): + + self.host = host + self.target_s = target_s + self.started = False + self.state = None + self.timer = None + + def _onEvent(self, state): + # Pyjamas callback takes no extra argument so we need this trick + + # Here we should check the value of the parameter "Send chat state notifications" + # but this costs two messages. It's even better to call chatStateComposing + # with a doubt, it will be checked by the back-end anyway before sending + # the actual notifications to the other client. + if state == "composing" and not self.started: + return + self.started = True + self.next_state = state + self.__onEvent(None) + + def __onEvent(self, timer): + state = self.next_state + + assert(state in TRANSITIONS) + transition = TRANSITIONS[state] + assert("next_state" in transition and "delay" in transition) + + if state != self.state and state == "composing": + self.host.bridge.call('chatStateComposing', None, self.target_s) + + self.state = state + if self.timer is not None: + self.timer.cancel() + + if transition["next_state"] and transition["delay"] > 0: + self.next_state = transition["next_state"] + self.timer = Timer(transition["delay"] * 1000, self.__onEvent) # pyjamas timer is in milliseconds
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/browser/sat_browser/register.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,285 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2011-2018 Jérôme Poisson <goffi@goffi.org> +# Copyright (C) 2011, 2012 Adrien Vigneron <adrienvigneron@mailoo.org> + +# This program 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. + +# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + +#This page manage subscription and new account creation + +import pyjd # this is dummy in pyjs +from sat.core.i18n import _ + +from pyjamas.ui.SimplePanel import SimplePanel +from pyjamas.ui.VerticalPanel import VerticalPanel +from pyjamas.ui.HorizontalPanel import HorizontalPanel +from pyjamas.ui.StackPanel import StackPanel +from pyjamas.ui.PasswordTextBox import PasswordTextBox +from pyjamas.ui.TextBox import TextBox +from pyjamas.ui.FormPanel import FormPanel +from pyjamas.ui.Button import Button +from pyjamas.ui.Label import Label +from pyjamas.ui.HTML import HTML +from pyjamas.ui.PopupPanel import PopupPanel +from pyjamas.ui.Image import Image +from pyjamas.ui.Hidden import Hidden +from pyjamas import Window +from pyjamas.ui.KeyboardListener import KEY_ENTER +from pyjamas.Timer import Timer + +from __pyjamas__ import JS + +from constants import Const as C + + + +class RegisterPanel(FormPanel): + + def __init__(self, callback, session_data): + """ + @param callback(callable): method to call if login successful + @param session_data(dict): session metadata + """ + FormPanel.__init__(self) + self.setSize('600px', '350px') + self.callback = callback + self.setMethod(FormPanel.METHOD_POST) + main_panel = HorizontalPanel() + main_panel.setStyleName('registerPanel_main') + left_side = Image("media/libervia/register_left.png") + main_panel.add(left_side) + + ##StackPanel## + self.right_side = StackPanel(StyleName='registerPanel_right_side') + main_panel.add(self.right_side) + main_panel.setCellWidth(self.right_side, '100%') + + ##Login stack## + login_stack = SimplePanel() + login_stack.setStyleName('registerPanel_content') + login_vpanel = VerticalPanel() + login_stack.setWidget(login_vpanel) + + self.login_warning_msg = HTML('') + self.login_warning_msg.setStyleName('formWarning') + login_vpanel.add(self.login_warning_msg) + + login_label = Label('Login:') + self.login_box = TextBox() + self.login_box.setName("login") + self.login_box.addKeyboardListener(self) + login_pass_label = Label('Password:') + self.login_pass_box = PasswordTextBox() + self.login_pass_box.setName("login_password") + self.login_pass_box.addKeyboardListener(self) + + login_vpanel.add(login_label) + login_vpanel.add(self.login_box) + login_vpanel.add(login_pass_label) + login_vpanel.add(self.login_pass_box) + login_but = Button("Log in", getattr(self, "onLogin")) + login_but.setStyleName('button') + login_but.addStyleName('red') + login_vpanel.add(login_but) + self.right_side.add(login_stack, 'Return to the login screen') + + #The hidden submit_type field + self.submit_type = Hidden('submit_type') + login_vpanel.add(self.submit_type) + + ##Register stack## + if session_data["allow_registration"]: + register_stack = SimplePanel() + register_stack.setStyleName('registerPanel_content') + register_vpanel = VerticalPanel() + register_stack.setWidget(register_vpanel) + + self.register_warning_msg = HTML('') + self.register_warning_msg.setStyleName('formWarning') + register_vpanel.add(self.register_warning_msg) + + register_login_label = Label('Login:') + self.register_login_box = TextBox() + self.register_login_box.setName("register_login") + self.register_login_box.addKeyboardListener(self) + email_label = Label('E-mail:') + self.email_box = TextBox() + self.email_box.setName("email") + self.email_box.addKeyboardListener(self) + register_pass_label = Label('Password:') + self.register_pass_box = PasswordTextBox() + self.register_pass_box.setName("register_password") + self.register_pass_box.addKeyboardListener(self) + register_vpanel.add(register_login_label) + register_vpanel.add(self.register_login_box) + register_vpanel.add(email_label) + register_vpanel.add(self.email_box) + register_vpanel.add(register_pass_label) + register_vpanel.add(self.register_pass_box) + + register_but = Button("Register a new account", getattr(self, "onRegister")) + register_but.setStyleName('button') + register_but.addStyleName('red') + register_vpanel.add(register_but) + + self.right_side.add(register_stack, 'No account yet? Create a new one!') + self.right_side.addStackChangeListener(self) + register_stack.setWidth(None) + login_stack.setWidth(None) + + self.add(main_panel) + self.addFormHandler(self) + self.setAction('register_api/login') + + def onStackChanged(self, sender, index): + if index == 0: + self.login_box.setFocus(True) + elif index == 1: + self.register_login_box.setFocus(True) + + def onKeyPress(self, sender, keycode, modifiers): + # XXX: this is triggered before the textbox value has changed + if keycode == KEY_ENTER: + # Browsers offer an auto-completion feature to any + # text box, but the selected value is not set when + # the widget looses the focus. Using a timer with + # any delay value > 0 would do the trick. + if sender == self.login_box: + Timer(5, lambda timer: self.login_pass_box.setFocus(True)) + elif sender == self.login_pass_box: + self.onLogin(None) + elif sender == self.register_login_box: + Timer(5, lambda timer: self.email_box.setFocus(True)) + elif sender == self.email_box: + Timer(5, lambda timer: self.register_pass_box.setFocus(True)) + elif sender == self.register_pass_box: + self.onRegister(None) + + def onKeyUp(self, sender, keycode, modifiers): + # XXX: this is triggered after the textbox value has changed + if sender == self.login_box: + if "@" in self.login_box.getText(): + self.login_warning_msg.setHTML(_('<span class="formInfo">Entering a full JID is only needed to connect with an external XMPP account.</span>')) + else: + self.login_warning_msg.setHTML("") + + def onKeyDown(self, sender, keycode, modifiers): + pass + + def onLogin(self, button): + if not self.checkJID(self.login_box.getText()): + self.login_warning_msg.setHTML('Invalid login, valid characters<br>are a-z A-Z 0-9 _ - or a bare JID') + else: + self.submit_type.setValue('login') + self.submit(None) + + def onRegister(self, button): + # XXX: for now libervia forces the creation to lower case + self.register_login_box.setText(self.register_login_box.getText().lower()) + if not self.checkLogin(self.register_login_box.getText()): + self.register_warning_msg.setHTML(_('Invalid login, valid characters<br>are a-z A-Z 0-9 _ -')) + elif not self.checkEmail(self.email_box.getText()): + self.register_warning_msg.setHTML(_('Invalid email address<br>(or not accepted yet)')) + elif len(self.register_pass_box.getText()) < C.PASSWORD_MIN_LENGTH: + self.register_warning_msg.setHTML(_('Your password must contain<br>at least %d characters.') % C.PASSWORD_MIN_LENGTH) + else: + self.register_warning_msg.setHTML("") + self.submit_type.setValue('register') + self.submit(None) + + def onSubmit(self, event): + pass + + def onSubmitComplete(self, event): + result = event.getResults() + if result == C.PROFILE_AUTH_ERROR: + self.login_warning_msg.setHTML(_('Your login and/or password is incorrect. Please try again.')) + elif result == C.XMPP_AUTH_ERROR: + # TODO: call stdui action CHANGE_XMPP_PASSWD_ID as it's done in primitivus + Window.alert(_(u'Your XMPP account failed to connect. Did you enter the good password? If you have changed your XMPP password since your last connection on Libervia, please use another SàT frontend to update your profile.')) + elif result == C.PROFILE_LOGGED_EXT_JID: + self.callback() + Window.alert(_('A profile has been created on this Libervia service using your existing XMPP account. Since you are not using our XMPP server, we can not guaranty that all the extra features (blog, directory...) will fully work.')) + elif result == C.PROFILE_LOGGED: + self.callback() + elif result == C.SESSION_ACTIVE: + Window.alert(_('Session already active, this should not happen, please contact the author to fix it.')) + elif result == C.NO_REPLY: + Window.alert(_("Did not receive a reply (the timeout expired or the connection is broken).")) + elif result == C.ALREADY_EXISTS: + self.register_warning_msg.setHTML(_('This login already exists,<br>please choose another one.')) + elif result == C.INTERNAL_ERROR: + self.register_warning_msg.setHTML(_('An registration error occurred, please contact the server administrator.')) + elif result == C.REGISTRATION_SUCCEED: + self.login_warning_msg.setHTML("") + self.register_warning_msg.setHTML("") + self.login_box.setText(self.register_login_box.getText()) + self.login_pass_box.setText('') + self.register_login_box.setText('') + self.register_pass_box.setText('') + self.email_box.setText('') + self.right_side.showStack(0) + self.login_pass_box.setFocus(True) + Window.alert(_('An email has been sent to you with your login informations\nPlease remember that this is ONLY A TECHNICAL DEMO.')) + else: + Window.alert(_("An error occurred and we couldn't process your request. Please report the following error name to the administrators of your network: '%s'" % result)) + + def checkLogin(self, text): + """Check if the given text is a valid login + + @param text (unicode) + @return bool + """ + # FIXME: Pyjamas re module is not stable so we use pure JS instead + # FIXME: login is restricted to this regex until we fix the account creation + JS("""return /^(\w|-)+$/.test(text);""") + + def checkEmail(self, text): + """Check if the given text is a valid email address. + + @param text (unicode) + @return bool + """ + # FIXME: Pyjamas re module is not stable so we use pure JS instead + # FIXME: send a message to validate the email instead of using a bad regex + JS("""return /^(\w|-|\.|\+)+@(\w|-)+\.(\w|-)+$/.test(text);""") + + def checkJID(self, text): + """Check if the given text is a valid JID. + + @param text (unicode) + @return bool + """ + # FIXME: Pyjamas re module is not stable so we use pure JS instead + # FIXME: this regex is too restrictive for people using external XMPP account + JS("""return /^(\w|-|\.|\+)+(@(\w|-)+\.(\w|-)+)?$/.test(text);""") + + +class RegisterBox(PopupPanel): + + def __init__(self, callback, session_data, *args, **kwargs): + PopupPanel.__init__(self, *args, **kwargs) + self._form = RegisterPanel(callback, session_data) + self.setWidget(self._form) + + def onWindowResized(self, width, height): + super(RegisterBox, self).onWindowResized(width, height) + self.centerBox() + + def show(self): + super(RegisterBox, self).show() + self.centerBox() + self._form.login_box.setFocus(True)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/browser/sat_browser/richtext.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,360 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2013-2016 Adrien Cossa <souliane@mailoo.org> + +# This program 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. + +# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + +from sat_frontends.tools import composition +from sat.core.i18n import _ +from sat.core.log import getLogger +log = getLogger(__name__) +from sat.tools.common import data_format + +from pyjamas.ui.TextArea import TextArea +from pyjamas.ui.Button import Button +from pyjamas.ui.CheckBox import CheckBox +from pyjamas.ui.Label import Label +from pyjamas.ui.FlexTable import FlexTable +from pyjamas.ui.HorizontalPanel import HorizontalPanel +from pyjamas.ui.KeyboardListener import KeyboardHandler +from pyjamas import Window +from __pyjamas__ import doc + +from constants import Const as C +import dialog +import base_panel +import editor_widget +import html_tools +import list_manager + + +class RichTextEditor(editor_widget.BaseTextEditor, FlexTable): + """Panel for the rich text editor.""" + + STYLE = {'main': 'richTextEditor', + 'title': 'richTextTitle', + 'toolbar': 'richTextToolbar', + 'textarea': 'richTextArea' + } + + def __init__(self, host, content=None, modifiedCb=None, afterEditCb=None, options=None): + """ + + @param host (SatWebFrontend): host instance + @param content (dict): dict with at least a 'text' key + @param modifiedCb (callable): to be called when the text has been modified + @param afterEditCb (callable): to be called when the edition is done + @param options (list[unicode]): UI options ("read_only", "update_msg") + """ + FlexTable.__init__(self) # FIXME + self.host = host + self.wysiwyg = False + self.read_only = 'read_only' in options + self.update_msg = 'update_msg' in options + + indices = (-1, -1, 0, -1, -1) if self.read_only else (0, 1, 2, 3, 4) + self.title_offset, self.toolbar_offset, self.content_offset, self.tags_offset, self.command_offset = indices + self.addStyleName(self.STYLE['main']) + + editor_widget.BaseTextEditor.__init__(self, content, None, modifiedCb, afterEditCb) + + def addEditListener(self, listener): + """Add a method to be called whenever the text is edited. + + @param listener: method taking two arguments: sender, keycode + """ + editor_widget.BaseTextEditor.addEditListener(self, listener) + if hasattr(self, 'display'): + self.display.addEditListener(listener) + + def refresh(self, edit=None): + """Refresh the UI for edition/display mode. + + @param edit: set to True to display the edition mode + """ + if edit is None: + edit = hasattr(self, 'textarea') and self.textarea.getVisible() + + for widget in ['title_panel', 'tags_panel', 'command']: + if hasattr(self, widget): + getattr(self, widget).setVisible(edit) + + if hasattr(self, 'toolbar'): + self.toolbar.setVisible(False) + + if not hasattr(self, 'display'): + self.display = editor_widget.HTMLTextEditor(options={'enhance_display': False, 'listen_keyboard': False}) # for display mode + for listener in self.edit_listeners: + self.display.addEditListener(listener) + + if not self.read_only and not hasattr(self, 'textarea'): + self.textarea = EditTextArea(self) # for edition mode + self.textarea.addStyleName(self.STYLE['textarea']) + + self.getFlexCellFormatter().setColSpan(self.content_offset, 0, 2) + if edit and not self.wysiwyg: + self.textarea.setWidth('100%') # CSS width doesn't do it, don't know why + self.setWidget(self.content_offset, 0, self.textarea) + else: + self.setWidget(self.content_offset, 0, self.display) + if not edit: + return + + if not self.read_only and not hasattr(self, 'title_panel'): + self.title_panel = base_panel.TitlePanel() + self.title_panel.addStyleName(self.STYLE['title']) + self.getFlexCellFormatter().setColSpan(self.title_offset, 0, 2) + self.setWidget(self.title_offset, 0, self.title_panel) + + if not self.read_only and not hasattr(self, 'tags_panel'): + suggested_tags = [] # TODO: feed this list with tags suggestion + self.tags_panel = list_manager.TagsPanel(suggested_tags) + self.getFlexCellFormatter().setColSpan(self.tags_offset, 0, 2) + self.setWidget(self.tags_offset, 0, self.tags_panel) + + if not self.read_only and not hasattr(self, 'command'): + self.command = HorizontalPanel() + self.command.addStyleName("marginAuto") + self.command.add(Button("Cancel", lambda: self.edit(True, True))) + self.command.add(Button("Update" if self.update_msg else "Send message", lambda: self.edit(False))) + self.getFlexCellFormatter().setColSpan(self.command_offset, 0, 2) + self.setWidget(self.command_offset, 0, self.command) + + def setToolBar(self, syntax): + """This method is called asynchronously after the parameter + holding the rich text syntax is retrieved. It is called at + each call of self.edit(True) because the user may + have change his setting since the last time.""" + if syntax is None or syntax not in composition.RICH_SYNTAXES.keys(): + syntax = composition.RICH_SYNTAXES.keys()[0] + if hasattr(self, "toolbar") and self.toolbar.syntax == syntax: + self.toolbar.setVisible(True) + return + self.toolbar = HorizontalPanel() + self.toolbar.syntax = syntax + self.toolbar.addStyleName(self.STYLE['toolbar']) + for key in composition.RICH_SYNTAXES[syntax].keys(): + self.addToolbarButton(syntax, key) + self.wysiwyg_button = CheckBox(_('preview')) + wysiywgCb = lambda sender: self.setWysiwyg(sender.getChecked()) + self.wysiwyg_button.addClickListener(wysiywgCb) + self.toolbar.add(self.wysiwyg_button) + self.syntax_label = Label(_("Syntax: %s") % syntax) + self.syntax_label.addStyleName("richTextSyntaxLabel") + self.toolbar.add(self.syntax_label) + self.toolbar.setCellWidth(self.syntax_label, "100%") + self.getFlexCellFormatter().setColSpan(self.toolbar_offset, 0, 2) + self.setWidget(self.toolbar_offset, 0, self.toolbar) + + def setWysiwyg(self, wysiwyg, init=False): + """Toggle the edition mode between rich content syntax and wysiwyg. + @param wysiwyg: boolean value + @param init: set to True to re-init without switching the widgets.""" + def setWysiwyg(): + self.wysiwyg = wysiwyg + try: + self.wysiwyg_button.setChecked(wysiwyg) + except (AttributeError, TypeError): + pass + try: + if wysiwyg: + self.syntax_label.addStyleName('transparent') + else: + self.syntax_label.removeStyleName('transparent') + except (AttributeError, TypeError): + pass + if not wysiwyg: + self.display.removeStyleName('richTextWysiwyg') + + if init: + setWysiwyg() + return + + self.getFlexCellFormatter().setColSpan(self.content_offset, 0, 2) + if wysiwyg: + def syntaxConvertCb(text): + self.display.setContent({'text': text}) + self.textarea.removeFromParent() # XXX: force as it is not always done... + self.setWidget(self.content_offset, 0, self.display) + self.display.addStyleName('richTextWysiwyg') + self.display.edit(True) + content = self.getContent() + if content['text'] and content['syntax'] != C.SYNTAX_XHTML: + self.host.bridge.call('syntaxConvert', syntaxConvertCb, content['text'], content['syntax'], C.SYNTAX_XHTML) + else: + syntaxConvertCb(content['text']) + else: + syntaxConvertCb = lambda text: self.textarea.setText(text) + text = self.display.getContent()['text'] + if text and self.toolbar.syntax != C.SYNTAX_XHTML: + self.host.bridge.call('syntaxConvert', syntaxConvertCb, text) + else: + syntaxConvertCb(text) + self.setWidget(self.content_offset, 0, self.textarea) + self.textarea.setWidth('100%') # CSS width doesn't do it, don't know why + + setWysiwyg() # do it in the end because it affects self.getContent + + def addToolbarButton(self, syntax, key): + """Add a button with the defined parameters.""" + button = Button('<img src="%s" class="richTextIcon" />' % + composition.RICH_BUTTONS[key]["icon"]) + button.setTitle(composition.RICH_BUTTONS[key]["tip"]) + button.addStyleName('richTextToolButton') + self.toolbar.add(button) + + def buttonCb(): + """Generic callback for a toolbar button.""" + text = self.textarea.getText() + cursor_pos = self.textarea.getCursorPos() + selection_length = self.textarea.getSelectionLength() + data = composition.RICH_SYNTAXES[syntax][key] + if selection_length == 0: + middle_text = data[1] + else: + middle_text = text[cursor_pos:cursor_pos + selection_length] + self.textarea.setText(text[:cursor_pos] + + data[0] + + middle_text + + data[2] + + text[cursor_pos + selection_length:]) + self.textarea.setCursorPos(cursor_pos + len(data[0]) + len(middle_text)) + self.textarea.setFocus(True) + self.textarea.onKeyDown() + + def wysiwygCb(): + """Callback for a toolbar button while wysiwyg mode is enabled.""" + data = composition.COMMANDS[key] + + def execCommand(command, arg): + self.display.setFocus(True) + doc().execCommand(command, False, arg.strip() if arg else '') + # use Window.prompt instead of dialog.PromptDialog to not loose the focus + prompt = lambda command, text: execCommand(command, Window.prompt(text)) + if isinstance(data, tuple) or isinstance(data, list): + if data[1]: + prompt(data[0], data[1]) + else: + execCommand(data[0], data[2]) + else: + execCommand(data, False, '') + self.textarea.onKeyDown() + + button.addClickListener(lambda: wysiwygCb() if self.wysiwyg else buttonCb()) + + def getContent(self): + assert(hasattr(self, 'textarea')) + assert(hasattr(self, 'toolbar')) + if self.wysiwyg: + content = {'text': self.display.getContent()['text'], 'syntax': C.SYNTAX_XHTML} + else: + content = {'text': self.strproc(self.textarea.getText()), 'syntax': self.toolbar.syntax} + if hasattr(self, 'title_panel'): + content.update({'title': self.strproc(self.title_panel.getText())}) + if hasattr(self, 'tags_panel'): + data_format.iter2dict('tag', self.tags_panel.getTags(), content) + return content + + def edit(self, edit=False, abort=False, sync=False): + """ + Remark: the editor must be visible before you call this method. + @param edit: set to True to edit the content or False to only display it + @param abort: set to True to cancel the edition and loose the changes. + If edit and abort are both True, self.abortEdition can be used to ask for a + confirmation. When edit is False and abort is True, abortion is actually done. + @param sync: set to True to cancel the edition after the content has been saved somewhere else + """ + if not (edit and abort): + self.refresh(edit) # not when we are asking for a confirmation + editor_widget.BaseTextEditor.edit(self, edit, abort, sync) # after the UI has been refreshed + if (edit and abort): + return # self.abortEdition is called by editor_widget.BaseTextEditor.edit + self.setWysiwyg(False, init=True) # after editor_widget.BaseTextEditor (it affects self.getContent) + if sync: + return + # the following must NOT be done at each UI refresh! + content = self._original_content + if edit: + def getParamCb(syntax): + # set the editable text in the current user-selected syntax + def syntaxConvertCb(text=None): + if text is not None: + # Important: this also update self._original_content + content.update({'text': text}) + content.update({'syntax': syntax}) + self.textarea.setText(content['text']) + + if hasattr(self, 'title_panel') and 'title' in content: + self.title_panel.setText(content['title']) + self.title_panel.setStackVisible(0, content['title'] != '') + + if hasattr(self, 'tags_panel'): + tags = list(data_format.dict2iter('tag', content)) + self.tags_panel.setTags(tags) + self.tags_panel.setStackVisible(0, len(tags) > 0) + + self.setToolBar(syntax) + if content['text'] and content['syntax'] != syntax: + self.host.bridge.call('syntaxConvert', syntaxConvertCb, content['text'], content['syntax']) + else: + syntaxConvertCb() + self.host.bridge.call('asyncGetParamA', getParamCb, composition.PARAM_NAME_SYNTAX, composition.PARAM_KEY_COMPOSITION) + else: + if not self.initialized: + # set the display text in XHTML only during init because a new MicroblogEntry instance is created after each modification + self.setDisplayContent() + self.display.edit(False) + + def setDisplayContent(self): + """Set the content of the editor_widget.HTMLTextEditor which is used for display/wysiwyg""" + content = self._original_content + text = content['text'] + if 'title' in content and content['title']: + title = '<h2>%s</h2>' % html_tools.html_sanitize(content['title']) + else: + title = "" + + tags = "" + for tag in data_format.dict2iter('tag', content): + tags += "<li><a>%s</a></li>" % html_tools.html_sanitize(tag) + if tags: + tags = '<ul class="mblog_tags">%s</ul>' % tags + + self.display.setContent({'text': "%s%s%s" % (title, tags, text)}) + + def setFocus(self, focus): + self.textarea.setFocus(focus) + + def abortEdition(self, content): + """Ask for confirmation before closing the dialog.""" + def confirm_cb(answer): + if answer: + self.edit(False, True) + _dialog = dialog.ConfirmDialog(confirm_cb, text="Do you really want to %s?" % ("cancel your changes" if self.update_msg else "cancel this message")) + _dialog.cancel_button.setText(_("No")) + _dialog.show() + + +class EditTextArea(TextArea, KeyboardHandler): + def __init__(self, _parent): + TextArea.__init__(self) + self._parent = _parent + KeyboardHandler.__init__(self) + self.addKeyboardListener(self) + + def onKeyDown(self, sender=None, keycode=None, modifiers=None): + for listener in self._parent.edit_listeners: + listener(self, keycode, modifiers) # FIXME: edit_listeners must either be removed, or send an action instead of keycode/modifiers
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/browser/sat_browser/strings.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,101 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# SAT helpers methods for plugins +# Copyright (C) 2013-2016 Adrien Cossa (souliane@mailoo.org) + +# This program 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. + +# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + +from __pyjamas__ import JS + + +def getURLParams(url): + """This comes from pyjamas.Location.makeUrlDict with a small change + to also parse full URLs, and parameters with no value specified + (in that case the default value "" is used). + @param url: any URL with or without parameters + @return: a dictionary of the parameters, if any was given, or {} + """ + dict_ = {} + if "/" in url: + # keep the part after the last "/" + url = url[url.rindex("/") + 1:] + if url.startswith("?"): + # remove the first "?" + url = url[1:] + pairs = url.split("&") + for pair in pairs: + if len(pair) < 3: + continue + kv = pair.split("=", 1) + dict_[kv[0]] = kv[1] if len(kv) > 1 else "" + return dict_ + + +def addURLToText(text, new_target=True): + """Check a text for what looks like an URL and make it clickable. + + @param string (unicode): text to process + @param new_target (bool): if True, make the link open in a new window + """ + # FIXME: Workaround for a pyjamas bug with regex, base method in sat.frontends.tools.strings + # In some case, Pyjamas' re module get crazy and freeze browsers (tested with Iceweasel and Chromium). + # we use javascript as a workaround + # This method is inspired from https://stackoverflow.com/questions/1500260/detect-urls-in-text-with-javascript + JS("""var urlRegex = /(https?:\/\/[^\s]+)/g; + var target = new_target ? ' target="_blank"' : ''; + return text.replace(urlRegex, function(url) { + return '<a href="' + url + '"' + target + ' class="url">' + url + '</a>'; + })""") + + +def addURLToImage(text): + """Check a XHTML text for what looks like an imageURL and make it clickable. + + @param text (unicode): text to process + """ + # FIXME: Pyjamas re module is not stable so we use pure JS instead, base method in sat.frontends.tools.strings + JS("""var imgRegex = /<img[^>]* src="([^"]+)"[^>]*>/g; + return text.replace(imgRegex, function(img, src) { + return '<a href="' + src + '" target="_blank">' + img + '</a>'; + })""") + +def fixXHTMLLinks(xhtml): + """Add http:// if the scheme is missing and force opening in a new window. + + @param string (unicode): XHTML Content + """ + # FIXME: Pyjamas re module is not stable so we use pure JS instead, base method in sat.frontends.tools.strings + JS("""var subs = []; + var tag_re = /<a(?: \w+="[^"]*")* ?\/?>/g; + var result; + while ((result = tag_re.exec(xhtml)) !== null) { + tag = result[0]; + var link_result = /href="([^"]*)"/.exec(tag); + if (link_result && !(link_result[1].startsWith("#"))) { // found a link which is not an internal anchor + var link = link_result[0]; + var url = link_result[1]; + if (! /target="([^"]*)"/.test(tag)) { // no target + subs.push([tag, '<a target="_blank"' + tag.slice(2, tag.length)]); + } + if (! /^\w+:\/\//.test(url)) { // no scheme + subs.push([link, 'href="http://' + url + '"']); + } + } + } + for (i in subs) { + xhtml = xhtml.replace(subs[i][0], subs[i][1]); + } + """) + return xhtml
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/browser/sat_browser/web_widget.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,89 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2011-2018 Jérôme Poisson <goffi@goffi.org> + +# This program 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. + +# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + +import pyjd # this is dummy in pyjs +from sat.core.log import getLogger +log = getLogger(__name__) + +from sat.core.i18n import D_ + +from pyjamas.ui.VerticalPanel import VerticalPanel +from pyjamas.ui.HorizontalPanel import HorizontalPanel +from pyjamas.ui.Button import Button +from pyjamas.ui.Frame import Frame +from pyjamas import DOM + + +import dialog +import libervia_widget +from constants import Const as C +from sat_frontends.quick_frontend import quick_widgets +from sat_frontends.tools import host_listener + + +class WebWidget(quick_widgets.QuickWidget, libervia_widget.LiberviaWidget): + """ (mini)browser like widget """ + + def __init__(self, host, target, show_url=True, profiles=None): + """ + @param host: SatWebFrontend instance + @param target: url to open + """ + quick_widgets.QuickWidget.__init__(self, host, target, C.PROF_KEY_NONE) + libervia_widget.LiberviaWidget.__init__(self, host) + self._vpanel = VerticalPanel() + self._vpanel.setSize('100%', '100%') + self._url = dialog.ExtTextBox(enter_cb=self.onUrlClick) + self._url.setText(target or "") + self._url.setWidth('100%') + if show_url: + hpanel = HorizontalPanel() + hpanel.add(self._url) + btn = Button("Go", self.onUrlClick) + hpanel.setCellWidth(self._url, "100%") + hpanel.add(btn) + self._vpanel.add(hpanel) + self._vpanel.setCellHeight(hpanel, '20px') + self._frame = Frame(target or "") + self._frame.setSize('100%', '100%') + DOM.setStyleAttribute(self._frame.getElement(), "position", "relative") + self._vpanel.add(self._frame) + self.setWidget(self._vpanel) + + def onUrlClick(self, sender): + url = self._url.getText() + scheme_end = url.find(':') + scheme = "" if scheme_end == -1 else url[:scheme_end] + if scheme not in C.WEB_PANEL_SCHEMES: + url = "http://" + url + self._frame.setUrl(url) + + +## Menu + +def hostReady(host): + def onWebWidget(): + web_widget = host.displayWidget(WebWidget, C.WEB_PANEL_DEFAULT_URL) + host.setSelected(web_widget) + + def gotMenus(): + host.menus.addMenu(C.MENU_GLOBAL, (D_(u"General"), D_(u"Web widget")), callback=onWebWidget) + host.addListener('gotMenus', gotMenus) + +host_listener.addListener(hostReady)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/browser/sat_browser/xmlui.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,506 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2011-2018 Jérôme Poisson <goffi@goffi.org> + +# This program 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. + +# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + +from sat.core.log import getLogger +log = getLogger(__name__) +from sat_frontends.tools import xmlui +from sat_browser import strings +from sat_frontends.tools import jid +from sat_browser.constants import Const as C +from sat_browser import dialog +from sat_browser import html_tools +from sat_browser import contact_panel + +from pyjamas.ui.VerticalPanel import VerticalPanel +from pyjamas.ui.HorizontalPanel import HorizontalPanel +from pyjamas.ui.TabPanel import TabPanel +from pyjamas.ui.Grid import Grid +from pyjamas.ui.Label import Label +from pyjamas.ui.TextBox import TextBox +from pyjamas.ui.PasswordTextBox import PasswordTextBox +from pyjamas.ui.TextArea import TextArea +from pyjamas.ui.CheckBox import CheckBox +from pyjamas.ui.ListBox import ListBox +from pyjamas.ui.Button import Button +from pyjamas.ui.HTML import HTML + +import nativedom + + +class EmptyWidget(xmlui.EmptyWidget, Label): + + def __init__(self, xmlui_main, xmlui_parent): + Label.__init__(self, '') + + +class TextWidget(xmlui.TextWidget, Label): + + def __init__(self, xmlui_main, xmlui_parent, value): + Label.__init__(self, value) + + +class LabelWidget(xmlui.LabelWidget, TextWidget): + + def __init__(self, xmlui_main, xmlui_parent, value): + TextWidget.__init__(self, xmlui_main, xmlui_parent, value + ": ") + + +class JidWidget(xmlui.JidWidget, TextWidget): + + def __init__(self, xmlui_main, xmlui_parent, value): + TextWidget.__init__(self, xmlui_main, xmlui_parent, value) + + +class DividerWidget(xmlui.DividerWidget, HTML): + + def __init__(self, xmlui_main, xmlui_parent, style='line'): + """Add a divider + + @param xmlui_parent + @param style (unicode): one of: + - line: a simple line + - dot: a line of dots + - dash: a line of dashes + - plain: a full thick line + - blank: a blank line/space + """ + HTML.__init__(self, "<hr/>") + self.addStyleName(style) + + +class StringWidget(xmlui.StringWidget, TextBox): + + def __init__(self, xmlui_main, xmlui_parent, value, read_only=False): + TextBox.__init__(self) + self.setText(value) + self.setReadonly(read_only) + + def _xmluiSetValue(self, value): + self.setText(value) + + def _xmluiGetValue(self): + return self.getText() + + def _xmluiOnChange(self, callback): + self.addChangeListener(callback) + + +class JidInputWidget(xmlui.JidInputWidget, StringWidget): + + def __init__(self, xmlui_main, xmlui_parent, value, read_only=False): + StringWidget.__init__(self, xmlui_main, xmlui_parent, value, read_only) + + +class PasswordWidget(xmlui.PasswordWidget, PasswordTextBox): + + def __init__(self, xmlui_main, xmlui_parent, value, read_only=False): + PasswordTextBox.__init__(self) + self.setText(value) + self.setReadonly(read_only) + + def _xmluiSetValue(self, value): + self.setText(value) + + def _xmluiGetValue(self): + return self.getText() + + def _xmluiOnChange(self, callback): + self.addChangeListener(callback) + + +class TextBoxWidget(xmlui.TextBoxWidget, TextArea): + + def __init__(self, xmlui_main, xmlui_parent, value, read_only=False): + TextArea.__init__(self) + self.setText(value) + self.setReadonly(read_only) + + def _xmluiSetValue(self, value): + self.setText(value) + + def _xmluiGetValue(self): + return self.getText() + + def _xmluiOnChange(self, callback): + self.addChangeListener(callback) + + +class BoolWidget(xmlui.BoolWidget, CheckBox): + + def __init__(self, xmlui_main, xmlui_parent, state, read_only=False): + CheckBox.__init__(self) + self.setChecked(state) + self.setReadonly(read_only) + + def _xmluiSetValue(self, value): + self.setChecked(value == "true") + + def _xmluiGetValue(self): + return "true" if self.isChecked() else "false" + + def _xmluiOnChange(self, callback): + self.addClickListener(callback) + + +class IntWidget(xmlui.IntWidget, TextBox): + + def __init__(self, xmlui_main, xmlui_parent, value, read_only=False): + TextBox.__init__(self) + self.setText(value) + self.setReadonly(read_only) + + def _xmluiSetValue(self, value): + self.setText(value) + + def _xmluiGetValue(self): + return self.getText() + + def _xmluiOnChange(self, callback): + self.addChangeListener(callback) + + +class ButtonWidget(xmlui.ButtonWidget, Button): + + def __init__(self, xmlui_main, xmlui_parent, value, click_callback): + Button.__init__(self, value, click_callback) + + def _xmluiOnClick(self, callback): + self.addClickListener(callback) + + +class ListWidget(xmlui.ListWidget, ListBox): + + def __init__(self, xmlui_main, xmlui_parent, options, selected, flags): + ListBox.__init__(self) + multi_selection = 'single' not in flags + self.setMultipleSelect(multi_selection) + if multi_selection: + self.setVisibleItemCount(5) + for option in options: + self.addItem(option[1]) + self._xmlui_attr_map = {label: value for value, label in options} + self._xmluiSelectValues(selected) + + def _xmluiSelectValue(self, value): + """Select a value checking its item""" + try: + label = [label for label, _value in self._xmlui_attr_map.items() if _value == value][0] + except IndexError: + log.warning(u"Can't find value [%s] to select" % value) + return + self.selectItem(label) + + def _xmluiSelectValues(self, values): + """Select multiple values, ignore the items""" + self.setValueSelection(values) + + def _xmluiGetSelectedValues(self): + ret = [] + for label in self.getSelectedItemText(): + ret.append(self._xmlui_attr_map[label]) + return ret + + def _xmluiOnChange(self, callback): + self.addChangeListener(callback) + + def _xmluiAddValues(self, values, select=True): + selected = self._xmluiGetSelectedValues() + for value in values: + if value not in self._xmlui_attr_map.values(): + self.addItem(value) + self._xmlui_attr_map[value] = value + if value not in selected: + selected.append(value) + self._xmluiSelectValues(selected) + + +class JidsListWidget(contact_panel.ContactsPanel, xmlui.JidsListWidget): + + def __init__(self, xmlui_main, xmlui_parent, jids, styles): + contact_panel.ContactsPanel.__init__(self, xmlui_main.host, merge_resources=False) + self.addStyleName("xmlui-JidsListWidget") + self.setList([jid.JID(jid_) for jid_ in jids]) + + def _xmluiGetSelectedValues(self): + # XXX: there is not selection in this list, so we return all non empty values + return self.getJids() + + + +class LiberviaContainer(object): + + def _xmluiAppend(self, widget): + self.append(widget) + + +class AdvancedListContainer(xmlui.AdvancedListContainer, Grid): + + def __init__(self, xmlui_main, xmlui_parent, columns, selectable='no'): + Grid.__init__(self, 0, columns) + self.columns = columns + self.row = -1 + self.col = 0 + self._xmlui_rows_idx = [] + self._xmlui_selectable = selectable != 'no' + self._xmlui_selected_row = None + self.addTableListener(self) + if self._xmlui_selectable: + self.addStyleName('AdvancedListSelectable') + + def onCellClicked(self, grid, row, col): + if not self._xmlui_selectable: + return + self._xmlui_selected_row = row + try: + self._xmlui_select_cb(self) + except AttributeError: + log.warning("no select callback set") + + def _xmluiAppend(self, widget): + self.setWidget(self.row, self.col, widget) + self.col += 1 + + def _xmluiAddRow(self, idx): + self.row += 1 + self.col = 0 + self._xmlui_rows_idx.insert(self.row, idx) + self.resizeRows(self.row + 1) + + def _xmluiGetSelectedWidgets(self): + return [self.getWidget(self._xmlui_selected_row, col) for col in range(self.columns)] + + def _xmluiGetSelectedIndex(self): + try: + return self._xmlui_rows_idx[self._xmlui_selected_row] + except TypeError: + return None + + def _xmluiOnSelect(self, callback): + self._xmlui_select_cb = callback + + +class PairsContainer(xmlui.PairsContainer, Grid): + + def __init__(self, xmlui_main, xmlui_parent): + Grid.__init__(self, 0, 0) + self.row = 0 + self.col = 0 + + def _xmluiAppend(self, widget): + if self.col == 0: + self.resize(self.row + 1, 2) + self.setWidget(self.row, self.col, widget) + self.col += 1 + if self.col == 2: + self.row += 1 + self.col = 0 + + +class LabelContainer(PairsContainer, xmlui.LabelContainer): + pass + + +class TabsContainer(LiberviaContainer, xmlui.TabsContainer, TabPanel): + + def __init__(self, xmlui_main, xmlui_parent): + TabPanel.__init__(self) + self.setStyleName('liberviaTabPanel') + + def _xmluiAddTab(self, label, selected): + tab_panel = VerticalContainer(self) + self.add(tab_panel, label) + count = len(self.getChildren()) + if count == 1 or selected: + self.selectTab(count - 1) + return tab_panel + + +class VerticalContainer(LiberviaContainer, xmlui.VerticalContainer, VerticalPanel): + __bases__ = (LiberviaContainer, xmlui.VerticalContainer, VerticalPanel) + + def __init__(self, xmlui_main, xmlui_parent): + VerticalPanel.__init__(self) + + +## Dialogs ## + + +class Dlg(object): + + def _xmluiShow(self): + self.show() + + def _xmluiClose(self): + pass + + +class MessageDialog(Dlg, xmlui.MessageDialog, dialog.InfoDialog): + + def __init__(self, xmlui_main, xmlui_parent, title, message, level): + #TODO: level is not managed + title = html_tools.html_sanitize(title) + message = strings.addURLToText(html_tools.XHTML2Text(message)) + Dlg.__init__(self) + xmlui.MessageDialog.__init__(self, xmlui_main, xmlui_parent) + dialog.InfoDialog.__init__(self, title, message, self._xmluiValidated()) + + +class NoteDialog(xmlui.NoteDialog, MessageDialog): + # TODO: separate NoteDialog + + def __init__(self, xmlui_main, xmlui_parent, title, message, level): + xmlui.NoteDialog.__init__(self, xmlui_main, xmlui_parent) + MessageDialog.__init__(self, xmlui_main, xmlui_parent, title, message, level) + + +class ConfirmDialog(xmlui.ConfirmDialog, Dlg, dialog.ConfirmDialog): + + def __init__(self, xmlui_main, xmlui_parent, title, message, level): + #TODO: level is not managed + title = html_tools.html_sanitize(title) + message = strings.addURLToText(html_tools.XHTML2Text(message)) + xmlui.ConfirmDialog.__init__(self, xmlui_main, xmlui_parent) + Dlg.__init__(self) + dialog.ConfirmDialog.__init__(self, self.answered, message, title) + + def answered(self, validated): + if validated: + self._xmluiValidated() + else: + self._xmluiCancelled() + + +class FileDialog(xmlui.FileDialog, Dlg): + #TODO: + + def __init__(self, xmlui_main, xmlui_parent, title, message, level, filetype): + raise NotImplementedError("FileDialog is not implemented in Libervia yet") + + +class GenericFactory(object): + # XXX: __getattr__ doens't work here with pyjamas for an unknown reason + # so an introspection workaround is used + + def __init__(self, xmlui_main): + self.xmlui_main = xmlui_main + for name, cls in globals().items(): + if name.endswith("Widget") or name.endswith("Container") or name.endswith("Dialog"): + log.info("registering: %s" % name) + def createCreater(cls): + return lambda *args, **kwargs: self._genericCreate(cls, *args, **kwargs) + setattr(self, "create%s" % name, createCreater(cls)) + + def _genericCreate(self, cls, *args, **kwargs): + instance = cls(self.xmlui_main, *args, **kwargs) + return instance + + # def __getattr__(self, attr): + # if attr.startswith("create"): + # cls = globals()[attr[6:]] + # cls._xmlui_main = self._xmlui_main + # return cls + + +class WidgetFactory(GenericFactory): + + def _genericCreate(self, cls, *args, **kwargs): + instance = GenericFactory._genericCreate(self, cls, *args, **kwargs) + return instance + +class LiberviaXMLUIBase(object): + + def _xmluiLaunchAction(self, action_id, data): + self.host.launchAction(action_id, data, callback=self._defaultCb) + + +class XMLUIPanel(LiberviaXMLUIBase, xmlui.XMLUIPanel, VerticalPanel): + + def __init__(self, host, parsed_xml, title=None, flags=None, callback=None, ignore=None, whitelist=None, profile=C.PROF_KEY_NONE): + self.widget_factory = WidgetFactory(self) + self.host = host + VerticalPanel.__init__(self) + self.setSize('100%', '100%') + xmlui.XMLUIPanel.__init__(self, + host, + parsed_xml, + title = title, + flags = flags, + callback = callback, + profile = profile) + + def setCloseCb(self, close_cb): + self.close_cb = close_cb + + def _xmluiClose(self): + if self.close_cb: + self.close_cb() + else: + log.warning("no close method defined") + + def _xmluiSetParam(self, name, value, category): + self.host.bridge.call('setParam', None, name, value, category) + + def constructUI(self, parsed_dom): + super(XMLUIPanel, self).constructUI(parsed_dom) + self.add(self.main_cont) + self.setCellHeight(self.main_cont, '100%') + if self.type == 'form': + hpanel = HorizontalPanel() + hpanel.setStyleName('marginAuto') + hpanel.add(Button('Submit', self.onFormSubmitted)) + if not 'NO_CANCEL' in self.flags: + hpanel.add(Button('Cancel', self.onFormCancelled)) + self.add(hpanel) + elif self.type == 'param': + assert(isinstance(self.children[0][0], TabPanel)) + hpanel = HorizontalPanel() + hpanel.setStyleName('marginAuto') + hpanel.add(Button('Save', self.onSaveParams)) + hpanel.add(Button('Cancel', lambda ignore: self._xmluiClose())) + self.add(hpanel) + + def show(self): + options = ['NO_CLOSE'] if self.type == C.XMLUI_FORM else [] + _dialog = dialog.GenericDialog(self.xmlui_title, self, options=options) + self.setCloseCb(_dialog.close) + _dialog.show() + + +class XMLUIDialog(LiberviaXMLUIBase, xmlui.XMLUIDialog): + dialog_factory = GenericFactory() + + def __init__(self, host, parsed_dom, title=None, flags=None, callback=None, ignore=None, whitelist=None, profile=C.PROF_KEY_NONE): + self.dialog_factory = GenericFactory(self) + xmlui.XMLUIDialog.__init__(self, + host, + parsed_dom, + title=title, + flags=flags, + callback=callback, + ignore=ignore, + profile=profile) + self.host = host + +xmlui.registerClass(xmlui.CLASS_PANEL, XMLUIPanel) +xmlui.registerClass(xmlui.CLASS_DIALOG, XMLUIDialog) + +def create(*args, **kwargs): + dom = nativedom.NativeDOM() + kwargs['dom_parse'] = lambda xml_data: dom.parseString(xml_data) + return xmlui.create(*args, **kwargs)
--- a/ez_setup.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,332 +0,0 @@ -#!/usr/bin/env python -"""Bootstrap setuptools installation - -To use setuptools in your package's setup.py, include this -file in the same directory and add this to the top of your setup.py:: - - from ez_setup import use_setuptools - use_setuptools() - -To require a specific version of setuptools, set a download -mirror, or use an alternate download directory, simply supply -the appropriate options to ``use_setuptools()``. - -This file can also be run as a script to install or upgrade setuptools. -""" -import os -import shutil -import sys -import tempfile -import zipfile -import optparse -import subprocess -import platform -import textwrap -import contextlib - -from distutils import log - -try: - from urllib.request import urlopen -except ImportError: - from urllib2 import urlopen - -try: - from site import USER_SITE -except ImportError: - USER_SITE = None - -DEFAULT_VERSION = "5.5" -DEFAULT_URL = "https://pypi.python.org/packages/source/s/setuptools/" - -def _python_cmd(*args): - """ - Return True if the command succeeded. - """ - args = (sys.executable,) + args - return subprocess.call(args) == 0 - - -def _install(archive_filename, install_args=()): - with archive_context(archive_filename): - # installing - log.warn('Installing Setuptools') - if not _python_cmd('setup.py', 'install', *install_args): - log.warn('Something went wrong during the installation.') - log.warn('See the error message above.') - # exitcode will be 2 - return 2 - - -def _build_egg(egg, archive_filename, to_dir): - with archive_context(archive_filename): - # building an egg - log.warn('Building a Setuptools egg in %s', to_dir) - _python_cmd('setup.py', '-q', 'bdist_egg', '--dist-dir', to_dir) - # returning the result - log.warn(egg) - if not os.path.exists(egg): - raise IOError('Could not build the egg.') - - -class ContextualZipFile(zipfile.ZipFile): - """ - Supplement ZipFile class to support context manager for Python 2.6 - """ - - def __enter__(self): - return self - - def __exit__(self, type, value, traceback): - self.close() - - def __new__(cls, *args, **kwargs): - """ - Construct a ZipFile or ContextualZipFile as appropriate - """ - if hasattr(zipfile.ZipFile, '__exit__'): - return zipfile.ZipFile(*args, **kwargs) - return super(ContextualZipFile, cls).__new__(cls) - - -@contextlib.contextmanager -def archive_context(filename): - # extracting the archive - tmpdir = tempfile.mkdtemp() - log.warn('Extracting in %s', tmpdir) - old_wd = os.getcwd() - try: - os.chdir(tmpdir) - with ContextualZipFile(filename) as archive: - archive.extractall() - - # going in the directory - subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0]) - os.chdir(subdir) - log.warn('Now working in %s', subdir) - yield - - finally: - os.chdir(old_wd) - shutil.rmtree(tmpdir) - - -def _do_download(version, download_base, to_dir, download_delay): - egg = os.path.join(to_dir, 'setuptools-%s-py%d.%d.egg' - % (version, sys.version_info[0], sys.version_info[1])) - if not os.path.exists(egg): - archive = download_setuptools(version, download_base, - to_dir, download_delay) - _build_egg(egg, archive, to_dir) - sys.path.insert(0, egg) - - # Remove previously-imported pkg_resources if present (see - # https://bitbucket.org/pypa/setuptools/pull-request/7/ for details). - if 'pkg_resources' in sys.modules: - del sys.modules['pkg_resources'] - - import setuptools - setuptools.bootstrap_install_from = egg - - -def use_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, - to_dir=os.curdir, download_delay=15): - to_dir = os.path.abspath(to_dir) - rep_modules = 'pkg_resources', 'setuptools' - imported = set(sys.modules).intersection(rep_modules) - try: - import pkg_resources - except ImportError: - return _do_download(version, download_base, to_dir, download_delay) - try: - pkg_resources.require("setuptools>=" + version) - return - except pkg_resources.DistributionNotFound: - return _do_download(version, download_base, to_dir, download_delay) - except pkg_resources.VersionConflict as VC_err: - if imported: - msg = textwrap.dedent(""" - The required version of setuptools (>={version}) is not available, - and can't be installed while this script is running. Please - install a more recent version first, using - 'easy_install -U setuptools'. - - (Currently using {VC_err.args[0]!r}) - """).format(VC_err=VC_err, version=version) - sys.stderr.write(msg) - sys.exit(2) - - # otherwise, reload ok - del pkg_resources, sys.modules['pkg_resources'] - return _do_download(version, download_base, to_dir, download_delay) - -def _clean_check(cmd, target): - """ - Run the command to download target. If the command fails, clean up before - re-raising the error. - """ - try: - subprocess.check_call(cmd) - except subprocess.CalledProcessError: - if os.access(target, os.F_OK): - os.unlink(target) - raise - -def download_file_powershell(url, target): - """ - Download the file at url to target using Powershell (which will validate - trust). Raise an exception if the command cannot complete. - """ - target = os.path.abspath(target) - ps_cmd = ( - "[System.Net.WebRequest]::DefaultWebProxy.Credentials = " - "[System.Net.CredentialCache]::DefaultCredentials; " - "(new-object System.Net.WebClient).DownloadFile(%(url)r, %(target)r)" - % vars() - ) - cmd = [ - 'powershell', - '-Command', - ps_cmd, - ] - _clean_check(cmd, target) - -def has_powershell(): - if platform.system() != 'Windows': - return False - cmd = ['powershell', '-Command', 'echo test'] - with open(os.path.devnull, 'wb') as devnull: - try: - subprocess.check_call(cmd, stdout=devnull, stderr=devnull) - except Exception: - return False - return True - -download_file_powershell.viable = has_powershell - -def download_file_curl(url, target): - cmd = ['curl', url, '--silent', '--output', target] - _clean_check(cmd, target) - -def has_curl(): - cmd = ['curl', '--version'] - with open(os.path.devnull, 'wb') as devnull: - try: - subprocess.check_call(cmd, stdout=devnull, stderr=devnull) - except Exception: - return False - return True - -download_file_curl.viable = has_curl - -def download_file_wget(url, target): - cmd = ['wget', url, '--quiet', '--output-document', target] - _clean_check(cmd, target) - -def has_wget(): - cmd = ['wget', '--version'] - with open(os.path.devnull, 'wb') as devnull: - try: - subprocess.check_call(cmd, stdout=devnull, stderr=devnull) - except Exception: - return False - return True - -download_file_wget.viable = has_wget - -def download_file_insecure(url, target): - """ - Use Python to download the file, even though it cannot authenticate the - connection. - """ - src = urlopen(url) - try: - # Read all the data in one block. - data = src.read() - finally: - src.close() - - # Write all the data in one block to avoid creating a partial file. - with open(target, "wb") as dst: - dst.write(data) - -download_file_insecure.viable = lambda: True - -def get_best_downloader(): - downloaders = ( - download_file_powershell, - download_file_curl, - download_file_wget, - download_file_insecure, - ) - viable_downloaders = (dl for dl in downloaders if dl.viable()) - return next(viable_downloaders, None) - -def download_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, - to_dir=os.curdir, delay=15, downloader_factory=get_best_downloader): - """ - Download setuptools from a specified location and return its filename - - `version` should be a valid setuptools version number that is available - as an sdist for download under the `download_base` URL (which should end - with a '/'). `to_dir` is the directory where the egg will be downloaded. - `delay` is the number of seconds to pause before an actual download - attempt. - - ``downloader_factory`` should be a function taking no arguments and - returning a function for downloading a URL to a target. - """ - # making sure we use the absolute path - to_dir = os.path.abspath(to_dir) - zip_name = "setuptools-%s.zip" % version - url = download_base + zip_name - saveto = os.path.join(to_dir, zip_name) - if not os.path.exists(saveto): # Avoid repeated downloads - log.warn("Downloading %s", url) - downloader = downloader_factory() - downloader(url, saveto) - return os.path.realpath(saveto) - -def _build_install_args(options): - """ - Build the arguments to 'python setup.py install' on the setuptools package - """ - return ['--user'] if options.user_install else [] - -def _parse_args(): - """ - Parse the command line for options - """ - parser = optparse.OptionParser() - parser.add_option( - '--user', dest='user_install', action='store_true', default=False, - help='install in user site package (requires Python 2.6 or later)') - parser.add_option( - '--download-base', dest='download_base', metavar="URL", - default=DEFAULT_URL, - help='alternative URL from where to download the setuptools package') - parser.add_option( - '--insecure', dest='downloader_factory', action='store_const', - const=lambda: download_file_insecure, default=get_best_downloader, - help='Use internal, non-validating downloader' - ) - parser.add_option( - '--version', help="Specify which version to download", - default=DEFAULT_VERSION, - ) - options, args = parser.parse_args() - # positional arguments are ignored - return options - -def main(): - """Install or upgrade setuptools and EasyInstall""" - options = _parse_args() - archive = download_setuptools( - version=options.version, - download_base=options.download_base, - downloader_factory=options.downloader_factory, - ) - return _install(archive, _build_install_args(options)) - -if __name__ == '__main__': - sys.exit(main())
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/VERSION Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,1 @@ +0.7.0D
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/__init__.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,9 @@ +import os.path + +version_file = os.path.join(os.path.dirname(__file__), "VERSION") +try: + with open(version_file) as f: + __version__ = f.read().strip() +except NotImplementedError: + # pyjamas workaround + __version__ = "0.7.0D"
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/common/constants.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,67 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a SAT frontend +# Copyright (C) 2009-2018 Jérôme Poisson (goffi@goffi.org) + +# This program 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. + +# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + +from sat_frontends.quick_frontend import constants +import os.path + + +class Const(constants.Const): + + # XXX: we don't want to use the APP_VERSION inherited from sat.core.constants version + # as we use this version to check that there is not a mismatch with backend + APP_VERSION = u"0.7.0D" # Please add 'D' at the end for dev versions + LIBERVIA_MAIN_PAGE = "libervia.html" + + # REGISTRATION + # XXX: for now libervia forces the creation to lower case + # XXX: Regex patterns must be compatible with both Python and JS + REG_LOGIN_RE = r"^[a-z0-9_-]+$" + REG_EMAIL_RE = r"^.+@.+\..+" + PASSWORD_MIN_LENGTH = 6 + + # HTTP REQUEST RESULT VALUES + PROFILE_AUTH_ERROR = "PROFILE AUTH ERROR" + XMPP_AUTH_ERROR = "XMPP AUTH ERROR" + ALREADY_WAITING = "ALREADY WAITING" + SESSION_ACTIVE = "SESSION ACTIVE" + NOT_CONNECTED = "NOT CONNECTED" + PROFILE_LOGGED = "LOGGED" + PROFILE_LOGGED_EXT_JID = "LOGGED (REGISTERED WITH EXTERNAL JID)" + ALREADY_EXISTS = "ALREADY EXISTS" + REGISTRATION_SUCCEED = "REGISTRATION" + INTERNAL_ERROR = "INTERNAL ERROR" + INVALID_INPUT = "INVALID INPUT" + BAD_REQUEST = "BAD REQUEST" + NO_REPLY = "NO REPLY" + NOT_ALLOWED = "NOT ALLOWED" + UPLOAD_OK = "UPLOAD OK" + UPLOAD_KO = "UPLOAD KO" + + # directories + MEDIA_DIR = "media/" + CACHE_DIR = "cache" + + # avatars + DEFAULT_AVATAR_FILE = "default_avatar.png" + DEFAULT_AVATAR_URL = os.path.join(MEDIA_DIR, "misc", DEFAULT_AVATAR_FILE) + EMPTY_AVATAR_FILE = "empty_avatar" + EMPTY_AVATAR_URL = os.path.join(MEDIA_DIR, "misc", EMPTY_AVATAR_FILE) + + # blog + MAM_FILTER_CATEGORY = "http://salut-a-toi.org/protocols/mam_filter_category"
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/pages/app/page_meta.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,5 @@ +#!/usr/bin/env python2.7 +# -*- coding: utf-8 -*- + +name = u"app" +template = u"app/app.html"
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/pages/blog/page_meta.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,56 @@ +#!/usr/bin/env python2.7 +# -*- coding: utf-8 -*- +from libervia.server.constants import Const as C +from twisted.words.protocols.jabber import jid +from twisted.internet import defer +from libervia.server import session_iface +from sat.core.log import getLogger + +log = getLogger("pages/blog") + +name = u"blog" +access = C.PAGES_ACCESS_PUBLIC +template = u"blog/discover.html" + + +@defer.inlineCallbacks +def prepare_render(self, request): + profile = self.getProfile(request) + template_data = request.template_data + if profile is not None: + __, entities_own, entities_roster = yield self.host.bridgeCall( + "discoFindByFeatures", + [], + [(u"pubsub", u"pep")], + True, + False, + True, + True, + True, + profile, + ) + entities = template_data[u"disco_entities"] = ( + entities_own.keys() + entities_roster.keys() + ) + entities_url = template_data[u"entities_url"] = {} + identities = template_data[u"identities"] = self.host.getSessionData( + request, session_iface.ISATSession + ).identities + for entity_jid_s in entities: + entities_url[entity_jid_s] = self.getPageByName("blog_view").getURL( + entity_jid_s + ) + if entity_jid_s not in identities: + identities[entity_jid_s] = yield self.host.bridgeCall( + u"identityGet", entity_jid_s, profile + ) + + +def on_data_post(self, request): + jid_str = self.getPostedData(request, u"jid") + try: + jid_ = jid.JID(jid_str) + except RuntimeError: + self.pageError(request, C.HTTP_BAD_REQUEST) + url = self.getPageByName(u"blog_view").getURL(jid_.full()) + self.HTTPRedirect(request, url)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/pages/blog/view/atom.xml/page_meta.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,41 @@ +#!/usr/bin/env python2.7 +# -*- coding: utf-8 -*- + +from libervia.server.constants import Const as C +from twisted.internet import defer +from sat.tools.common import uri +import time + +name = u"blog_feed_atom" +access = C.PAGES_ACCESS_PUBLIC +template = u"blog/atom.xml" + + +@defer.inlineCallbacks +def prepare_render(self, request): + request.setHeader("Content-Type", "application/atom+xml; charset=utf-8") + data = self.getRData(request) + service, node = data[u"service"], data.get(u"node") + self.checkCache( + request, C.CACHE_PUBSUB, service=service, node=node, short="microblog" + ) + data["show_comments"] = False + template_data = request.template_data + blog_page = self.getPageByName(u"blog_view") + yield blog_page.prepare_render(self, request) + items = data[u"items"] + + template_data[u"request_uri"] = self.host.getExtBaseURL( + request, request.path.decode("utf-8") + ) + template_data[u"xmpp_uri"] = uri.buildXMPPUri( + u"pubsub", subtype=u"microblog", path=service.full(), node=node + ) + blog_view = self.getPageByName(u"blog_view") + template_data[u"http_uri"] = self.host.getExtBaseURL( + request, blog_view.getURL(service.full(), node) + ) + if items: + template_data[u"updated"] = items[0].updated + else: + template_data[u"updated"] = time.time()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/pages/blog/view/page_meta.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,299 @@ +#!/usr/bin/env python2.7 +# -*- coding: utf-8 -*- +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 libervia.server import utils +import unicodedata +import re +import cgi +from sat.core.log import getLogger +log = getLogger('pages/blog/view') + +"""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: + data['node'] = self.nextPath(request) + except IndexError: + data['node'] = u'' + else: + if data['node'] == u'@': + data['node'] = u'' + + try: + filter_kw = data['filter_keyword'] = self.nextPath(request) + except IndexError: + pass + else: + if 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) + + +@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, + [], + {}, + 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 getBlogData(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 = ([], {}) + + items = data_objects.BlogItems(blog_data) + defer.returnValue((blog_data, items)) + +@defer.inlineCallbacks +def prepare_render(self, request): + data = self.getRData(request) + # 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 + + ## pagination/filtering parameters + params = self.getAllPostedData(request, multiple=False) + if item_id: + extra = {} + else: + extra = {u'rsm_max': u'10'} + if u'after' in params: + extra[u'rsm_after'] = params[u'after'] + elif u'before' in params: + extra[u'rsm_before'] = params[u'before'] + 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 + blog_data, items = yield getBlogData(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: + last_id = items[-1].id + template_data['older_url'] = self.getParamURL(request, after=last_id) + if u'before' in params or u'after' in params: + first_id = items[0].id + template_data['newer_url'] = self.getParamURL(request, before=first_id) + 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) + + # no items, we have requested items before last post, or blog is empty + extra = {u'rsm_max': u'10'} + blog_data, items = yield getBlogData(self, request, service, node, None, extra, profile) + if items: + last_id = items[-1].id + template_data['older_url'] = self.getParamURL(request, after=last_id) + + ## identities ## + # identities are use 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', False) + + # 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_UNAUTHORIZED) + 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, 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_))
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/pages/chat/page_meta.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,145 @@ +#!/usr/bin/env python2.7 +# -*- coding: utf-8 -*- + +from sat.core.i18n import _ +from twisted.internet import defer +from sat.core.log import getLogger + +log = getLogger("pages/chat") +from sat.tools.common import data_objects +from twisted.words.protocols.jabber import jid +from libervia.server.constants import Const as C +from libervia.server import session_iface + +name = u"chat" +access = C.PAGES_ACCESS_PROFILE +template = u"chat/chat.html" +dynamic = True + + +def parse_url(self, request): + rdata = self.getRData(request) + + try: + target_jid_s = self.nextPath(request) + except IndexError: + # not chat jid, we redirect to jid selection page + self.pageRedirect(u"chat_select", request) + + try: + target_jid = jid.JID(target_jid_s) + if not target_jid.user: + raise ValueError(_(u"invalid jid for chat (no local part)")) + except Exception as e: + log.warning( + _(u"bad chat jid entered: {jid} ({msg})").format(jid=target_jid, msg=e) + ) + self.pageError(request, C.HTTP_BAD_REQUEST) + else: + rdata["target"] = target_jid + + +@defer.inlineCallbacks +def prepare_render(self, request): + # FIXME: bug on room filtering (currently display messages from all rooms) + session = self.host.getSessionData(request, session_iface.ISATSession) + template_data = request.template_data + rdata = self.getRData(request) + target_jid = rdata["target"] + profile = session.profile + profile_jid = session.jid + + disco = yield self.host.bridgeCall(u"discoInfos", target_jid.host, u"", True, profile) + if "conference" in [i[0] for i in disco[1]]: + chat_type = C.CHAT_GROUP + join_ret = yield self.host.bridgeCall( + u"mucJoin", target_jid.userhost(), "", "", profile + ) + already_joined, room_jid_s, occupants, user_nick, room_subject, dummy = join_ret + template_data[u"subject"] = room_subject + own_jid = jid.JID(room_jid_s) + own_jid.resource = user_nick + else: + chat_type = C.CHAT_ONE2ONE + own_jid = profile_jid + rdata["chat_type"] = chat_type + template_data["own_jid"] = own_jid + + self.registerSignal(request, u"messageNew") + history = yield self.host.bridgeCall( + u"historyGet", + profile_jid.userhost(), + target_jid.userhost(), + 20, + True, + {}, + profile, + ) + authors = {m[2] for m in history} + identities = {} + for author in authors: + identities[author] = yield self.host.bridgeCall(u"identityGet", author, profile) + + template_data[u"messages"] = data_objects.Messages(history) + template_data[u"identities"] = identities + template_data[u"target_jid"] = target_jid + template_data[u"chat_type"] = chat_type + + +def on_data(self, request, data): + session = self.host.getSessionData(request, session_iface.ISATSession) + rdata = self.getRData(request) + target = rdata["target"] + data_type = data.get(u"type", "") + if data_type == "msg": + message = data[u"body"] + mess_type = ( + C.MESS_TYPE_GROUPCHAT + if rdata["chat_type"] == C.CHAT_GROUP + else C.MESS_TYPE_CHAT + ) + log.debug(u"message received: {}".format(message)) + self.host.bridgeCall( + u"messageSend", + target.full(), + {u"": message}, + {}, + mess_type, + {}, + session.profile, + ) + else: + log.warning(u"unknown message type: {type}".format(type=data_type)) + + +@defer.inlineCallbacks +def on_signal(self, request, signal, *args): + if signal == "messageNew": + rdata = self.getRData(request) + template_data = request.template_data + template_data_update = {u"msg": data_objects.Message((args))} + target_jid = rdata["target"] + identities = template_data["identities"] + uid, timestamp, from_jid_s, to_jid_s, message, subject, mess_type, extra, dummy = ( + args + ) + from_jid = jid.JID(from_jid_s) + to_jid = jid.JID(to_jid_s) + if ( + target_jid.userhostJID() != from_jid.userhostJID() + and target_jid.userhostJID() != to_jid.userhostJID() + ): + # the message is not linked with page's room/user + return + + if from_jid_s not in identities: + profile = self.getProfile(request) + identities[from_jid_s] = yield self.host.bridgeCall( + u"identityGet", from_jid_s, profile + ) + template_data_update["identities"] = identities + self.renderAndUpdate( + request, u"chat/message.html", "#messages", template_data_update + ) + else: + log.error(_(u"Unexpected signal: {signal}").format(signal=signal))
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/pages/chat/select/page_meta.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,45 @@ +#!/usr/bin/env python2.7 +# -*- coding: utf-8 -*- + +from sat.core.i18n import _ +from libervia.server.constants import Const as C +from twisted.internet import defer +from twisted.words.protocols.jabber import jid +from sat.tools.common import data_objects +from sat.core.log import getLogger + +log = getLogger("pages/chat_select") + +name = u"chat_select" +access = C.PAGES_ACCESS_PROFILE +template = u"chat/select.html" + + +@defer.inlineCallbacks +def prepare_render(self, request): + profile = self.getProfile(request) + template_data = request.template_data + rooms = template_data["rooms"] = [] + bookmarks = yield self.host.bridgeCall("bookmarksList", "muc", "all", profile) + for bm_values in bookmarks.values(): + for room_jid, room_data in bm_values.iteritems(): + url = self.getPageByName(u"chat").getURL(room_jid) + rooms.append(data_objects.Room(room_jid, name=room_data.get("name"), url=url)) + rooms.sort(key=lambda r: r.name) + + +@defer.inlineCallbacks +def on_data_post(self, request): + jid_ = self.getPostedData(request, u"jid") + if u"@" not in jid_: + profile = self.getProfile(request) + service = yield self.host.bridgeCall("mucGetService", "", profile) + if service: + muc_jid = jid.JID(service) + muc_jid.user = jid_ + jid_ = muc_jid.full() + else: + log.warning(_(u"Invalid jid received: {jid}".format(jid=jid_))) + defer.returnValue(C.POST_NO_CONFIRM) + url = self.getPageByName(u"chat").getURL(jid_) + self.HTTPRedirect(request, url)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/pages/events/admin/page_meta.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,173 @@ +#!/usr/bin/env python2.7 +# -*- coding: utf-8 -*- + +from libervia.server.constants import Const as C +from twisted.internet import defer +from twisted.words.protocols.jabber import jid +from sat.tools.common.template import safe +from sat.core.i18n import _ +from sat.core.log import getLogger +import time +import cgi +import math +import re + +name = u"event_admin" +access = C.PAGES_ACCESS_PROFILE +template = u"event/admin.html" +log = getLogger(u"pages/" + name) +REG_EMAIL_RE = re.compile(C.REG_EMAIL_RE, re.IGNORECASE) + + +def parse_url(self, request): + self.getPathArgs( + request, + ("event_service", "event_node", "event_id"), + min_args=2, + event_service="@jid", + event_id="", + ) + + +@defer.inlineCallbacks +def prepare_render(self, request): + data = self.getRData(request) + template_data = request.template_data + + ## Event ## + + event_service = template_data[u"event_service"] = data["event_service"] + event_node = template_data[u"event_node"] = data["event_node"] + event_id = template_data[u"event_id"] = data["event_id"] + profile = self.getProfile(request) + event_timestamp, event_data = yield self.host.bridgeCall( + u"eventGet", + event_service.userhost() if event_service else "", + event_node, + event_id, + profile, + ) + try: + background_image = event_data.pop("background-image") + except KeyError: + pass + else: + template_data["dynamic_style"] = safe( + u""" + html { + background-image: url("%s"); + background-size: 15em; + } + """ + % cgi.escape(background_image, True) + ) + template_data["event"] = event_data + invitees = yield self.host.bridgeCall( + u"eventInviteesList", + event_data["invitees_service"], + event_data["invitees_node"], + profile, + ) + template_data["invitees"] = invitees + invitees_guests = 0 + for invitee_data in invitees.itervalues(): + if invitee_data.get("attend", "no") == "no": + continue + try: + invitees_guests += int(invitee_data.get("guests", 0)) + except ValueError: + log.warning( + _(u"guests value is not valid: {invitee}").format(invitee=invitee_data) + ) + template_data["invitees_guests"] = invitees_guests + template_data["days_left"] = int( + math.ceil((event_timestamp - time.time()) / (60 * 60 * 24)) + ) + + ## Blog ## + + data[u"service"] = jid.JID(event_data[u"blog_service"]) + data[u"node"] = event_data[u"blog_node"] + data[u"allow_commenting"] = u"simple" + + # we now need blog items, using blog common page + # this will fill the "items" template data + blog_page = self.getPageByName(u"blog_view") + yield blog_page.prepare_render(self, request) + + +@defer.inlineCallbacks +def on_data_post(self, request): + profile = self.getProfile(request) + if not profile: + log.error(u"got post data without profile") + self.pageError(request, C.HTTP_INTERNAL_ERROR) + type_ = self.getPostedData(request, "type") + if type_ == "blog": + service, node, title, body, lang = self.getPostedData( + request, (u"service", u"node", u"title", u"body", u"language") + ) + + if not body.strip(): + self.pageError(request, C.HTTP_BAD_REQUEST) + data = {u"content": body} + if title: + data[u"title"] = title + if lang: + data[u"language"] = lang + try: + comments = bool(self.getPostedData(request, u"comments").strip()) + except KeyError: + pass + else: + if comments: + data[u"allow_comments"] = C.BOOL_TRUE + + try: + yield self.host.bridgeCall(u"mbSend", service, node, data, profile) + except Exception as e: + if u"forbidden" in unicode(e): + self.pageError(request, C.HTTP_UNAUTHORIZED) + else: + raise e + elif type_ == "event": + service, node, event_id, jids, emails = self.getPostedData( + request, (u"service", u"node", u"event_id", u"jids", u"emails") + ) + for invitee_jid_s in jids.split(): + try: + invitee_jid = jid.JID(invitee_jid_s) + except RuntimeError as e: + log.warning( + _(u"this is not a valid jid: {jid}").format(jid=invitee_jid_s) + ) + continue + yield self.host.bridgeCall( + "eventInvite", invitee_jid.userhost(), service, node, event_id, profile + ) + for email_addr in emails.split(): + if not REG_EMAIL_RE.match(email_addr): + log.warning( + _(u"this is not a valid email address: {email}").format( + email=email_addr + ) + ) + continue + yield self.host.bridgeCall( + "eventInviteByEmail", + service, + node, + event_id, + email_addr, + {}, + u"", + u"", + u"", + u"", + u"", + u"", + profile, + ) + + else: + log.warning(_(u"Unhandled data type: {}").format(type_))
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/pages/events/new/page_meta.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,39 @@ +#!/usr/bin/env python2.7 +# -*- coding: utf-8 -*- + +from libervia.server.constants import Const as C +from twisted.internet import defer +from sat.core.log import getLogger +from sat.tools.common import date_utils + +"""creation of new events""" + +name = u"event_new" +access = C.PAGES_ACCESS_PROFILE +template = u"event/create.html" +log = getLogger(u"pages/" + name) + + +@defer.inlineCallbacks +def on_data_post(self, request): + request_data = self.getRData(request) + profile = self.getProfile(request) + title, location, body, date, main_img, bg_img = self.getPostedData( + request, ("name", "location", "body", "date", "main_image", "bg_image") + ) + timestamp = date_utils.date_parse(date) + data = {"name": title, "description": body, "location": location} + + for value, var in ((main_img, "image"), (bg_img, "background-image")): + value = value.strip() + if not value: + continue + if not value.startswith("http"): + self.pageError(request, C.HTTP_BAD_REQUEST) + data[var] = value + data[u"register"] = C.BOOL_TRUE + node = yield self.host.bridgeCall("eventCreate", timestamp, data, "", "", "", profile) + log.info(u"Event node created at {node}".format(node=node)) + + request_data["post_redirect_page"] = (self.getPageByName(u"event_admin"), "@", node) + defer.returnValue(C.POST_NO_CONFIRM)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/pages/events/page_meta.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,50 @@ +#!/usr/bin/env python2.7 +# -*- coding: utf-8 -*- + +from libervia.server.constants import Const as C +from twisted.internet import defer +from sat.core.i18n import _ +from sat.core.log import getLogger + +log = getLogger("pages/ticket") +"""ticket handling pages""" + +name = u"events" +access = C.PAGES_ACCESS_PUBLIC +template = u"event/overview.html" + + +@defer.inlineCallbacks +def parse_url(self, request): + profile = self.getProfile(request) + template_data = request.template_data + template_data[u"url_event_new"] = self.getSubPageURL(request, "event_new") + if profile is not None: + try: + events = yield self.host.bridgeCall("eventsList", "", "", profile) + except Exception: + log.warning(_(u"Can't get events list for {profile}").format(profile=profile)) + else: + own_events = [] + other_events = [] + for event in events: + if C.bool(event.get("creator", C.BOOL_FALSE)): + own_events.append(event) + event["url"] = self.getSubPageURL( + request, + u"event_admin", + event.get("service", ""), + event.get("node", ""), + event.get("item"), + ) + else: + other_events.append(event) + event["url"] = self.getSubPageURL( + request, + u"event_rsvp", + event.get("service", ""), + event.get("node", ""), + event.get("item"), + ) + + template_data[u"events"] = own_events + other_events
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/pages/events/rsvp/page_meta.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,99 @@ +#!/usr/bin/env python2.7 +# -*- coding: utf-8 -*- + +from libervia.server.constants import Const as C +from sat.core.i18n import _ +from twisted.internet import defer +from twisted.words.protocols.jabber import jid +from sat.core.log import getLogger +from sat.tools.common.template import safe +import time +import cgi + +"""creation of new events""" + +name = u"event_rsvp" +access = C.PAGES_ACCESS_PROFILE +template = u"event/invitation.html" +log = getLogger(u"pages/" + name) + + +def parse_url(self, request): + self.getPathArgs( + request, + ("event_service", "event_node", "event_id"), + min_args=2, + event_service="@jid", + event_id="", + ) + + +@defer.inlineCallbacks +def prepare_render(self, request): + template_data = request.template_data + data = self.getRData(request) + profile = self.getProfile(request) + + ## Event ## + + event_service = data["event_service"] + event_node = data[u"event_node"] + event_id = data[u"event_id"] + event_timestamp, event_data = yield self.host.bridgeCall( + u"eventGet", + event_service.userhost() if event_service else "", + event_node, + event_id, + profile, + ) + try: + background_image = event_data.pop("background-image") + except KeyError: + pass + else: + template_data["dynamic_style"] = safe( + u""" + html { + background-image: url("%s"); + background-size: 15em; + } + """ + % cgi.escape(background_image, True) + ) + template_data["event"] = event_data + event_invitee_data = yield self.host.bridgeCall( + u"eventInviteeGet", + event_data["invitees_service"], + event_data["invitees_node"], + profile, + ) + template_data["invitee"] = event_invitee_data + template_data["days_left"] = int((event_timestamp - time.time()) / (60 * 60 * 24)) + + ## Blog ## + + data[u"service"] = jid.JID(event_data[u"blog_service"]) + data[u"node"] = event_data[u"blog_node"] + data[u"allow_commenting"] = u"simple" + + # we now need blog items, using blog common page + # this will fill the "items" template data + blog_page = self.getPageByName(u"blog_view") + yield blog_page.prepare_render(self, request) + + +@defer.inlineCallbacks +def on_data_post(self, request): + type_ = self.getPostedData(request, u"type") + if type_ == u"comment": + blog_page = self.getPageByName(u"blog_view") + yield blog_page.on_data_post(self, request) + elif type_ == u"attendance": + profile = self.getProfile(request) + service, node, attend, guests = self.getPostedData( + request, (u"service", u"node", u"attend", u"guests") + ) + data = {u"attend": attend, u"guests": guests} + yield self.host.bridgeCall(u"eventInviteeSet", service, node, data, profile) + else: + log.warning(_(u"Unhandled data type: {}").format(type_))
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/pages/events/view/page_meta.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,96 @@ +#!/usr/bin/env python2.7 +# -*- coding: utf-8 -*- + +from libervia.server.constants import Const as C +from sat.core.i18n import _ +from twisted.internet import defer +from twisted.words.protocols.jabber import jid +from libervia.server import session_iface +from sat.tools.common import uri +from sat.tools.common.template import safe +import time +import cgi +from sat.core.log import getLogger + +name = u"event_view" +access = C.PAGES_ACCESS_PROFILE +template = u"event/invitation.html" +log = getLogger(u"pages/" + name) + + +@defer.inlineCallbacks +def prepare_render(self, request): + template_data = request.template_data + guest_session = self.host.getSessionData(request, session_iface.ISATGuestSession) + try: + event_uri = guest_session.data["event_uri"] + except KeyError: + log.warning(_(u"event URI not found, can't render event page")) + self.pageError(request, C.HTTP_SERVICE_UNAVAILABLE) + + data = self.getRData(request) + + ## Event ## + + event_uri_data = uri.parseXMPPUri(event_uri) + if event_uri_data[u"type"] != u"pubsub": + self.pageError(request, C.HTTP_SERVICE_UNAVAILABLE) + + event_service = template_data[u"event_service"] = jid.JID(event_uri_data[u"path"]) + event_node = template_data[u"event_node"] = event_uri_data[u"node"] + event_id = template_data[u"event_id"] = event_uri_data.get(u"item", "") + profile = self.getProfile(request) + event_timestamp, event_data = yield self.host.bridgeCall( + u"eventGet", event_service.userhost(), event_node, event_id, profile + ) + try: + background_image = event_data.pop("background-image") + except KeyError: + pass + else: + template_data["dynamic_style"] = safe( + u""" + html { + background-image: url("%s"); + background-size: 15em; + } + """ + % cgi.escape(background_image, True) + ) + template_data["event"] = event_data + event_invitee_data = yield self.host.bridgeCall( + u"eventInviteeGet", + event_data["invitees_service"], + event_data["invitees_node"], + profile, + ) + template_data["invitee"] = event_invitee_data + template_data["days_left"] = int((event_timestamp - time.time()) / (60 * 60 * 24)) + + ## Blog ## + + data[u"service"] = jid.JID(event_data[u"blog_service"]) + data[u"node"] = event_data[u"blog_node"] + data[u"allow_commenting"] = u"simple" + + # we now need blog items, using blog common page + # this will fill the "items" template data + blog_page = self.getPageByName(u"blog_view") + yield blog_page.prepare_render(self, request) + + +@defer.inlineCallbacks +def on_data_post(self, request): + type_ = self.getPostedData(request, u"type") + if type_ == u"comment": + blog_page = self.getPageByName(u"blog_view") + yield blog_page.on_data_post(self, request) + elif type_ == u"attendance": + profile = self.getProfile(request) + service, node, attend, guests = self.getPostedData( + request, (u"service", u"node", u"attend", u"guests") + ) + data = {u"attend": attend, u"guests": guests} + yield self.host.bridgeCall(u"eventInviteeSet", service, node, data, profile) + else: + log.warning(_(u"Unhandled data type: {}").format(type_))
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/pages/files/list/page_meta.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,108 @@ +#!/usr/bin/env python2.7 +# -*- coding: utf-8 -*- + +from libervia.server.constants import Const as C +from sat.core.i18n import _ +from twisted.internet import defer +from libervia.server import session_iface +from libervia.server import pages_tools +from sat.core.log import getLogger +from sat.tools.common import uri +import json +import os + +log = getLogger("pages/files/list") +"""files handling pages""" + +name = u"files_list" +access = C.PAGES_ACCESS_PROFILE +template = u"file/overview.html" + + +def parse_url(self, request): + self.getPathArgs(request, ["service", "*path"], min_args=1, service="jid", path="") + + +@defer.inlineCallbacks +def prepare_render(self, request): + data = self.getRData(request) + thumb_limit = data.get("thumb_limit", 300) + template_data = request.template_data + service, path_elts = data[u"service"], data[u"path"] + path = u"/".join(path_elts) + profile = self.getProfile(request) or C.SERVICE_PROFILE + + files_data = yield self.host.bridgeCall("FISList", service.full(), path, {}, profile) + for file_data in files_data: + try: + extra_raw = file_data[u"extra"] + except KeyError: + pass + else: + file_data[u"extra"] = json.loads(extra_raw) if extra_raw else {} + dir_path = path_elts + [file_data["name"]] + if file_data[u"type"] == C.FILE_TYPE_DIRECTORY: + page = self + elif file_data[u"type"] == C.FILE_TYPE_FILE: + page = self.getPageByName("files_view") + + ## thumbnails ## + try: + thumbnails = file_data[u"extra"]["thumbnails"] + if not thumbnails: + raise KeyError + except KeyError: + pass + else: + thumbnails.sort(key=lambda t: t["size"]) + thumb = thumbnails[0] + for thumb_data in thumbnails: + if thumb_data["size"][0] > thumb_limit: + break + thumb = thumb_data + if u"url" in thumb: + file_data["thumb_url"] = thumb["url"] + elif u"id" in thumb: + try: + thumb_path = yield self.host.bridgeCall( + "bobGetFile", service.full(), thumb[u"id"], profile + ) + except Exception as e: + log.warning( + _(u"Can't retrieve thumbnail: {reason}").format(reason=e) + ) + else: + filename = os.path.basename(thumb_path) + session_data = self.host.getSessionData( + request, session_iface.ISATSession + ) + file_data["thumb_url"] = os.path.join( + session_data.cache_dir, filename + ) + else: + raise ValueError( + u"unexpected file type: {file_type}".format(file_type=file_data[u"type"]) + ) + file_data[u"url"] = page.getURL(service.full(), *dir_path) + + ## comments ## + comments_url = file_data.get(u"comments_url") + if comments_url: + parsed_url = uri.parseXMPPUri(comments_url) + comments_service = file_data[u"comments_service"] = parsed_url["path"] + comments_node = file_data[u"comments_node"] = parsed_url["node"] + try: + comments_count = file_data[u"comments_count"] = int( + file_data["comments_count"] + ) + except KeyError: + comments_count = None + if comments_count and data.get("retrieve_comments", False): + file_data[u"comments"] = yield pages_tools.retrieveComments( + self, comments_service, comments_node, profile=profile + ) + + template_data[u"files_data"] = files_data + template_data[u"path"] = path + if path_elts: + template_data[u"parent_url"] = self.getURL(service.full(), *path_elts[:-1])
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/pages/files/page_meta.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,54 @@ +#!/usr/bin/env python2.7 +# -*- coding: utf-8 -*- + +from libervia.server.constants import Const as C +from twisted.internet import defer +from twisted.words.protocols.jabber import jid +from sat.core.log import getLogger + +log = getLogger("pages/files") +"""files handling pages""" + +name = u"files" +access = C.PAGES_ACCESS_PROFILE +template = u"file/discover.html" + + +@defer.inlineCallbacks +def prepare_render(self, request): + profile = self.getProfile(request) + template_data = request.template_data + namespace = self.host.ns_map["fis"] + entities_services, entities_own, entities_roster = yield self.host.bridgeCall( + "discoFindByFeatures", [namespace], [], False, True, True, True, False, profile + ) + tpl_service_entities = template_data["disco_service_entities"] = {} + tpl_own_entities = template_data["disco_own_entities"] = {} + tpl_roster_entities = template_data["disco_roster_entities"] = {} + entities_url = template_data["entities_url"] = {} + + # we store identities in dict of dict using category and type as keys + # this way it's easier to test category in the template + for tpl_entities, entities_map in ( + (tpl_service_entities, entities_services), + (tpl_own_entities, entities_own), + (tpl_roster_entities, entities_roster), + ): + for entity_str, entity_ids in entities_map.iteritems(): + entity_jid = jid.JID(entity_str) + tpl_entities[entity_jid] = identities = {} + for cat, type_, name in entity_ids: + identities.setdefault(cat, {}).setdefault(type_, []).append(name) + entities_url[entity_jid] = self.getPageByName("files_list").getURL( + entity_jid.full() + ) + + +def on_data_post(self, request): + jid_str = self.getPostedData(request, u"jid") + try: + jid_ = jid.JID(jid_str) + except RuntimeError: + self.pageError(request, C.HTTP_BAD_REQUEST) + url = self.getPageByName(u"files_list").getURL(jid_.full()) + self.HTTPRedirect(request, url)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/pages/files/view/page_meta.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,60 @@ +#!/usr/bin/env python2.7 +# -*- coding: utf-8 -*- + +from libervia.server.constants import Const as C +from sat.core.i18n import _ +from twisted.internet import defer +from twisted.web import static +from libervia.server.utils import ProgressHandler +import tempfile +import os +import os.path +from sat.core.log import getLogger + +log = getLogger("pages/files/view") +"""files handling pages""" + +name = u"files_view" +access = C.PAGES_ACCESS_PROFILE + + +def parse_url(self, request): + self.getPathArgs(request, ["service", "*path"], min_args=2, service="jid", path="") + + +def cleanup(dummy, tmp_dir, dest_path): + try: + os.unlink(dest_path) + except OSError: + log.warning(_(u"Can't remove temporary file {path}").format(path=dest_path)) + try: + os.rmdir(tmp_dir) + except OSError: + log.warning(_(u"Can't remove temporary directory {path}").format(path=tmp_dir)) + + +@defer.inlineCallbacks +def render(self, request): + data = self.getRData(request) + profile = self.getProfile(request) + service, path_elts = data[u"service"], data[u"path"] + basename = path_elts[-1] + dir_elts = path_elts[:-1] + dir_path = u"/".join(dir_elts) + tmp_dir = tempfile.mkdtemp() + dest_path = os.path.join(tmp_dir, basename) + request.notifyFinish().addCallback(cleanup, tmp_dir, dest_path) + progress_id = yield self.host.bridgeCall( + "fileJingleRequest", + service.full(), + dest_path, + basename, + u"", + u"", + {u"path": dir_path}, + profile, + ) + log.debug(u"file requested") + yield ProgressHandler(self.host, progress_id, profile).register() + log.debug(u"file downloaded") + self.delegateToResource(request, static.File(dest_path))
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/pages/forums/list/page_meta.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,63 @@ +#!/usr/bin/env python2.7 +# -*- coding: utf-8 -*- + +from libervia.server.constants import Const as C +from twisted.internet import defer +from sat.core.log import getLogger +from sat.core.i18n import _ +from sat.tools.common import uri as xmpp_uri + +log = getLogger("pages/forum") +import json + +"""forum handling pages""" + +name = u"forums" +access = C.PAGES_ACCESS_PUBLIC +template = u"forum/overview.html" + + +def parse_url(self, request): + self.getPathArgs( + request, + ["service", "node", "forum_key"], + service=u"@jid", + node=u"@", + forum_key=u"", + ) + + +def getLinks(self, forums): + for forum in forums: + try: + uri = forum["uri"] + except KeyError: + pass + else: + uri = xmpp_uri.parseXMPPUri(uri) + service = uri[u"path"] + node = uri[u"node"] + forum["http_url"] = self.getPageByName(u"forum_topics").getURL(service, node) + if u"sub-forums" in forum: + getLinks(self, forum[u"sub-forums"]) + + +@defer.inlineCallbacks +def prepare_render(self, request): + data = self.getRData(request) + template_data = request.template_data + service, node, key = data[u"service"], data[u"node"], data[u"forum_key"] + profile = self.getProfile(request) or C.SERVICE_PROFILE + + try: + forums_raw = yield self.host.bridgeCall( + "forumsGet", service.full() if service else u"", node, key, profile + ) + except Exception as e: + log.warning(_(u"Can't retrieve forums: {msg}").format(msg=e)) + forums = [] + else: + forums = json.loads(forums_raw) + getLinks(self, forums) + + template_data[u"forums"] = forums
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/pages/forums/topics/page_meta.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,69 @@ +#!/usr/bin/env python2.7 +# -*- coding: utf-8 -*- + +from libervia.server.constants import Const as C +from twisted.internet import defer +from sat.core.i18n import _ +from sat.core.log import getLogger +from sat.tools.common import uri as xmpp_uri + +log = getLogger("pages/forums/topics") + +name = u"forum_topics" +access = C.PAGES_ACCESS_PUBLIC +template = u"forum/view_topics.html" + + +def parse_url(self, request): + self.getPathArgs(request, ["service", "node"], 2, service=u"jid") + + +@defer.inlineCallbacks +def prepare_render(self, request): + profile = self.getProfile(request) or C.SERVICE_PROFILE + data = self.getRData(request) + service, node = data[u"service"], data[u"node"] + request.template_data.update({u"service": service, u"node": node}) + template_data = request.template_data + topics, metadata = yield self.host.bridgeCall( + u"forumTopicsGet", service.full(), node, {}, profile + ) + template_data[u"identities"] = identities = {} + for topic in topics: + parsed_uri = xmpp_uri.parseXMPPUri(topic[u"uri"]) + author = topic[u"author"] + topic[u"http_uri"] = self.getPageByName(u"forum_view").getURL( + parsed_uri[u"path"], parsed_uri[u"node"] + ) + if author not in identities: + identities[topic[u"author"]] = yield self.host.bridgeCall( + u"identityGet", author, profile + ) + template_data[u"topics"] = topics + + +@defer.inlineCallbacks +def on_data_post(self, request): + profile = self.getProfile(request) + if profile is None: + self.pageError(request, C.HTTP_UNAUTHORIZED) + type_ = self.getPostedData(request, u"type") + if type_ == u"new_topic": + service, node, title, body = self.getPostedData( + request, (u"service", u"node", u"title", u"body") + ) + + if not title or not body: + self.pageError(request, C.HTTP_BAD_REQUEST) + topic_data = {u"title": title, u"content": body} + try: + yield self.host.bridgeCall( + u"forumTopicCreate", service, node, topic_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_))
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/pages/forums/view/page_meta.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,51 @@ +#!/usr/bin/env python2.7 +# -*- coding: utf-8 -*- + +from libervia.server.constants import Const as C +from twisted.internet import defer +from sat.core.i18n import _ +from sat.core.log import getLogger + +log = getLogger("pages/forums/view") + +name = u"forum_view" +access = C.PAGES_ACCESS_PUBLIC +template = u"forum/view.html" + + +def parse_url(self, request): + self.getPathArgs(request, ["service", "node"], 2, service=u"jid") + + +@defer.inlineCallbacks +def prepare_render(self, request): + data = self.getRData(request) + data["show_comments"] = False + blog_page = self.getPageByName(u"blog_view") + request.args["before"] = [""] + request.args["reverse"] = ["1"] + yield blog_page.prepare_render(self, request) + request.template_data[u"login_url"] = self.getPageRedirectURL(request) + + +@defer.inlineCallbacks +def on_data_post(self, request): + profile = self.getProfile(request) + if profile is None: + self.pageError(request, C.HTTP_UNAUTHORIZED) + 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) + mb_data = {u"content": body} + try: + yield self.host.bridgeCall(u"mbSend", service, node, mb_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_))
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/pages/g/e/page_meta.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,96 @@ +#!/usr/bin/env python2.7 +# -*- coding: utf-8 -*- + +from libervia.server.constants import Const as C +from sat.core.i18n import _ +from twisted.internet import defer +from twisted.words.protocols.jabber import jid +from libervia.server import session_iface +from sat.tools.common import uri +from sat.tools.common.template import safe +import time +import cgi +from sat.core.log import getLogger + +log = getLogger("pages/g/e") + +access = C.PAGES_ACCESS_PROFILE +template = u"event/invitation.html" + + +@defer.inlineCallbacks +def prepare_render(self, request): + template_data = request.template_data + guest_session = self.host.getSessionData(request, session_iface.ISATGuestSession) + try: + event_uri = guest_session.data["event_uri"] + except KeyError: + log.warning(_(u"event URI not found, can't render event page")) + self.pageError(request, C.HTTP_SERVICE_UNAVAILABLE) + + data = self.getRData(request) + + ## Event ## + + event_uri_data = uri.parseXMPPUri(event_uri) + if event_uri_data[u"type"] != u"pubsub": + self.pageError(request, C.HTTP_SERVICE_UNAVAILABLE) + + event_service = template_data[u"event_service"] = jid.JID(event_uri_data[u"path"]) + event_node = template_data[u"event_node"] = event_uri_data[u"node"] + event_id = template_data[u"event_id"] = event_uri_data.get(u"item", "") + profile = self.getProfile(request) + event_timestamp, event_data = yield self.host.bridgeCall( + u"eventGet", event_service.userhost(), event_node, event_id, profile + ) + try: + background_image = event_data.pop("background-image") + except KeyError: + pass + else: + template_data["dynamic_style"] = safe( + u""" + html { + background-image: url("%s"); + background-size: 15em; + } + """ + % cgi.escape(background_image, True) + ) + template_data["event"] = event_data + event_invitee_data = yield self.host.bridgeCall( + u"eventInviteeGet", + event_data["invitees_service"], + event_data["invitees_node"], + profile, + ) + template_data["invitee"] = event_invitee_data + template_data["days_left"] = int((event_timestamp - time.time()) / (60 * 60 * 24)) + + ## Blog ## + + data[u"service"] = jid.JID(event_data[u"blog_service"]) + data[u"node"] = event_data[u"blog_node"] + data[u"allow_commenting"] = u"simple" + + # we now need blog items, using blog common page + # this will fill the "items" template data + blog_page = self.getPageByName(u"blog_view") + yield blog_page.prepare_render(self, request) + + +@defer.inlineCallbacks +def on_data_post(self, request): + type_ = self.getPostedData(request, u"type") + if type_ == u"comment": + blog_page = self.getPageByName(u"blog_view") + yield blog_page.on_data_post(self, request) + elif type_ == u"attendance": + profile = self.getProfile(request) + service, node, attend, guests = self.getPostedData( + request, (u"service", u"node", u"attend", u"guests") + ) + data = {u"attend": attend, u"guests": guests} + yield self.host.bridgeCall(u"eventInviteeSet", service, node, data, profile) + else: + log.warning(_(u"Unhandled data type: {}").format(type_))
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/pages/g/page_meta.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,104 @@ +#!/usr/bin/env python2.7 +# -*- coding: utf-8 -*- + +from libervia.server.constants import Const as C +from sat.core.i18n import _ +from twisted.internet import defer +from libervia.server import session_iface +from sat.core.log import getLogger + +log = getLogger("pages/g") + +access = C.PAGES_ACCESS_PUBLIC +template = u"invitation/welcome.html" + + +@defer.inlineCallbacks +def parse_url(self, request): + """check invitation id in URL and start session if needed + + if a session already exists for an other guest/profile, it will be purged + """ + try: + invitation_id = self.nextPath(request) + except IndexError: + self.pageError(request) + + sat_session, guest_session = self.host.getSessionData( + request, session_iface.ISATSession, session_iface.ISATGuestSession + ) + current_id = guest_session.id + + if current_id is not None and current_id != invitation_id: + log.info( + _( + u"killing guest session [{old_id}] because it is connecting with an other ID [{new_id}]" + ).format(old_id=current_id, new_id=invitation_id) + ) + self.host.purgeSession(request) + sat_session, guest_session = self.host.getSessionData( + request, session_iface.ISATSession, session_iface.ISATGuestSession + ) + current_id = None # FIXME: id non mis à zéro ici + profile = None + + profile = sat_session.profile + if profile is not None and current_id is None: + log.info( + _( + u"killing current profile session [{profile}] because a guest id is used" + ).format(profile=profile) + ) + self.host.purgeSession(request) + sat_session, guest_session = self.host.getSessionData( + request, session_iface.ISATSession, session_iface.ISATGuestSession + ) + profile = None + + if current_id is None: + log.debug(_(u"checking invitation [{id}]").format(id=invitation_id)) + try: + data = yield self.host.bridgeCall("invitationGet", invitation_id) + except Exception: + self.pageError(request, C.HTTP_UNAUTHORIZED) + else: + guest_session.id = invitation_id + guest_session.data = data + else: + data = guest_session.data + + if profile is None: + log.debug(_(u"connecting profile [{}]").format(profile)) + # we need to connect the profile + profile = data["guest_profile"] + password = data["password"] + try: + yield self.host.connect(request, profile, password) + except Exception as e: + log.warning(_(u"Can't connect profile: {msg}").format(msg=e)) + # FIXME: no good error code correspond + # maybe use a custom one? + self.pageError(request, code=C.HTTP_SERVICE_UNAVAILABLE) + + log.info( + _( + u"guest session started, connected with profile [{profile}]".format( + profile=profile + ) + ) + ) + + # we copy data useful in templates + template_data = request.template_data + template_data["norobots"] = True + if u"name" in data: + template_data[u"name"] = data[u"name"] + if u"language" in data: + template_data[u"locale"] = data[u"language"] + + +def prepare_render(self, request): + template_data = request.template_data + guest_session = self.host.getSessionData(request, session_iface.ISATGuestSession) + main_uri = guest_session.data.get("main_uri") + template_data[u"include_url"] = self.getPagePathFromURI(main_uri)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/pages/login/logged/page_meta.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,18 @@ +#!/usr/bin/env python2.7 +# -*- coding: utf-8 -*- + +from libervia.server import session_iface +from sat.core.log import getLogger + +log = getLogger("pages/login") + +"""SàT log-in page, with link to create an account""" + +template = u"login/logged.html" + + +def prepare_render(self, request): + template_data = request.template_data + session_data = self.host.getSessionData(request, session_iface.ISATSession) + template_data["guest_session"] = session_data.guest + template_data["session_started"] = session_data.started
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/pages/login/page_meta.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,86 @@ +#!/usr/bin/env python2.7 +# -*- coding: utf-8 -*- + +from sat.core.i18n import _ +from sat.core import exceptions +from libervia.server.constants import Const as C +from libervia.server import session_iface +from twisted.internet import defer +from sat.core.log import getLogger + +log = getLogger("pages/login") + +"""SàT log-in page, with link to create an account""" + +name = u"login" +access = C.PAGES_ACCESS_PUBLIC +template = u"login/login.html" + + +def prepare_render(self, request): + template_data = request.template_data + + # we redirect to logged page if a session is active + profile = self.getProfile(request) + if profile is not None: + self.pageRedirect("/login/logged", request) + + # login error message + session_data = self.host.getSessionData(request, session_iface.ISATSession) + login_error = session_data.popPageData(self, "login_error") + if login_error is not None: + template_data["S_C"] = C # we need server constants in template + template_data["login_error"] = login_error + template_data["empty_password_allowed"] = bool( + self.host.options["empty_password_allowed_warning_dangerous_list"] + ) + + # register page url + template_data["register_url"] = self.getPageRedirectURL(request, "register") + + # if login is set, we put it in template to prefill field + template_data["login"] = session_data.popPageData(self, "login") + + +def login_error(self, request, error_const): + """set login_error in page data + + @param error_const(unicode): one of login error constant + @return C.POST_NO_CONFIRM: avoid confirm message + """ + session_data = self.host.getSessionData(request, session_iface.ISATSession) + session_data.setPageData(self, "login_error", error_const) + return C.POST_NO_CONFIRM + + +@defer.inlineCallbacks +def on_data_post(self, request): + profile = self.getProfile(request) + type_ = self.getPostedData(request, "type") + if type_ == "disconnect": + if profile is None: + log.warning(_(u"Disconnect called when no profile is logged")) + self.pageError(request, C.HTTP_BAD_REQUEST) + else: + self.host.purgeSession(request) + defer.returnValue(C.POST_NO_CONFIRM) + elif type_ == "login": + login, password = self.getPostedData(request, (u"login", u"password")) + try: + status = yield self.host.connect(request, login, password) + except ValueError as e: + if e.message in (C.XMPP_AUTH_ERROR, C.PROFILE_AUTH_ERROR): + defer.returnValue(login_error(self, request, e.message)) + else: + # this error was not expected! + raise e + except exceptions.TimeOutError: + defer.returnValue(login_error(self, request, C.NO_REPLY)) + else: + if status in (C.PROFILE_LOGGED, C.PROFILE_LOGGED_EXT_JID, C.SESSION_ACTIVE): + # Profile has been logged correctly + self.redirectOrContinue(request) + else: + log.error(_(u"Unhandled status: {status}".format(status=status))) + else: + self.pageError(request, C.HTTP_BAD_REQUEST)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/pages/merge-requests/disco/page_meta.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,41 @@ +#!/usr/bin/env python2.7 +# -*- coding: utf-8 -*- + +from libervia.server.constants import Const as C +from twisted.words.protocols.jabber import jid +from sat.core.log import getLogger + +log = getLogger("pages/ticket") +"""ticket handling pages""" + +name = u"merge-requests_disco" +access = C.PAGES_ACCESS_PUBLIC +template = u"merge-request/discover.html" + + +def prepare_render(self, request): + mr_handlers_config = self.host.options["mr_handlers_json"] + if mr_handlers_config: + handlers = request.template_data["mr_handlers"] = [] + try: + for handler_data in mr_handlers_config: + service = handler_data[u"service"] + node = handler_data[u"node"] + name = handler_data[u"name"] + url = self.getPageByName(u"merge-requests").getURL(service, node) + handlers.append({u"name": name, u"url": url}) + except KeyError as e: + log.warning(u"Missing field in mr_handlers_json: {msg}".format(msg=e)) + except Exception as e: + log.warning(u"Can't decode mr handlers: {msg}".format(msg=e)) + + +def on_data_post(self, request): + jid_str = self.getPostedData(request, u"jid") + try: + jid_ = jid.JID(jid_str) + except RuntimeError: + self.pageError(request, C.HTTP_BAD_REQUEST) + # for now we just use default node + url = self.getPageByName(u"merge-requests").getURL(jid_.full(), u"@") + self.HTTPRedirect(request, url)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/pages/merge-requests/edit/page_meta.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,105 @@ +#!/usr/bin/env python2.7 +# -*- coding: utf-8 -*- + +from libervia.server.constants import Const as C +from sat.core.i18n import _ +from twisted.internet import defer +from sat.tools.common import template_xmlui +from sat.core.log import getLogger + +"""merge-requests edition""" + +name = u"merge-requests_edit" +access = C.PAGES_ACCESS_PROFILE +template = u"merge-request/edit.html" +log = getLogger("pages/" + name) + + +def parse_url(self, request): + try: + item_id = self.nextPath(request) + except IndexError: + log.warning(_(u"no ticket id specified")) + self.pageError(request, C.HTTP_BAD_REQUEST) + + data = self.getRData(request) + data[u"ticket_id"] = item_id + + +@defer.inlineCallbacks +def prepare_render(self, request): + data = self.getRData(request) + template_data = request.template_data + service, node, ticket_id = ( + data.get(u"service", u""), + data.get(u"node", u""), + data[u"ticket_id"], + ) + profile = self.getProfile(request) + + ignore = ( + "publisher", + "author", + "author_jid", + "author_email", + "created", + "updated", + "comments_uri", + "request_data", + "type", + ) + tickets = yield self.host.bridgeCall( + "mergeRequestsGet", + service.full() if service else u"", + node, + C.NO_LIMIT, + [ticket_id], + "", + {}, + profile, + ) + ticket = [template_xmlui.create(self.host, x, ignore=ignore) for x in tickets[0]][0] + + try: + # small trick to get a one line text input instead of the big textarea + ticket.widgets[u"labels"].type = u"string" + ticket.widgets[u"labels"].value = ticket.widgets[u"labels"].value.replace( + u"\n", ", " + ) + except KeyError: + pass + template_data[u"new_ticket_xmlui"] = ticket + + +@defer.inlineCallbacks +def on_data_post(self, request): + data = self.getRData(request) + service = data["service"] + node = data["node"] + ticket_id = data["ticket_id"] + posted_data = self.getAllPostedData(request) + if not posted_data["title"] or not posted_data["body"]: + self.pageError(request, C.HTTP_BAD_REQUEST) + try: + posted_data["labels"] = [l.strip() for l in posted_data["labels"][0].split(",")] + except (KeyError, IndexError): + pass + profile = self.getProfile(request) + yield self.host.bridgeCall( + "mergeRequestSet", + service.full(), + node, + u"", + u"auto", + posted_data, + u"", + ticket_id, + {"update": C.BOOL_TRUE}, + profile, + ) + # we don't want to redirect to edit page on success, but to tickets list + data["post_redirect_page"] = ( + self.getPageByName(u"merge-requests"), + service.full(), + node or u"@", + )
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/pages/merge-requests/new/page_meta.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,12 @@ +#!/usr/bin/env python2.7 +# -*- coding: utf-8 -*- + +from libervia.server.constants import Const as C +from sat.core.log import getLogger + +log = getLogger("pages/ticket") +"""ticket handling pages""" + +name = u"merge-requests_new" +access = C.PAGES_ACCESS_PUBLIC +template = u"merge-request/create.html"
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/pages/merge-requests/page_meta.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,58 @@ +#!/usr/bin/env python2.7 +# -*- coding: utf-8 -*- + +from libervia.server.constants import Const as C +from twisted.internet import defer +from sat.tools.common import template_xmlui +from sat.tools.common import data_objects +from sat.core.log import getLogger + +log = getLogger("pages/ticket") +"""ticket handling pages""" + +name = u"merge-requests" +access = C.PAGES_ACCESS_PUBLIC +template = u"ticket/overview.html" + + +def parse_url(self, request): + self.getPathArgs(request, ["service", "node"], service="jid") + data = self.getRData(request) + service, node = data[u"service"], data[u"node"] + if node is None: + self.pageRedirect(u"merge-requests_disco", request) + if node == u"@": + node = data[u"node"] = u"" + self.checkCache( + request, C.CACHE_PUBSUB, service=service, node=node, short="merge-requests" + ) + template_data = request.template_data + template_data[u"url_tickets_list"] = self.getPageByName("merge-requests").getURL( + service.full(), node + ) + template_data[u"url_tickets_new"] = self.getSubPageURL(request, "merge-requests_new") + + +@defer.inlineCallbacks +def prepare_render(self, request): + data = self.getRData(request) + template_data = request.template_data + service, node = data[u"service"], data[u"node"] + profile = self.getProfile(request) or C.SERVICE_PROFILE + + merge_requests = yield self.host.bridgeCall( + "mergeRequestsGet", + service.full() if service else u"", + node, + C.NO_LIMIT, + [], + "", + {"labels_as_list": C.BOOL_TRUE}, + profile, + ) + template_data[u"tickets"] = [ + template_xmlui.create(self.host, x) for x in merge_requests[0] + ] + template_data[u"on_ticket_click"] = data_objects.OnClick( + url=self.getSubPageURL(request, u"merge-requests_view") + u"/{item.id}" + )
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/pages/merge-requests/view/page_meta.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,97 @@ +#!/usr/bin/env python2.7 +# -*- coding: utf-8 -*- + +from libervia.server.constants import Const as C +from sat.core.i18n import _ +from libervia.server.utils import SubPage +from libervia.server import session_iface +from twisted.internet import defer +from twisted.words.protocols.jabber import jid +from sat.tools.common import template_xmlui +from sat.tools.common import uri +from sat.tools.common import data_objects +from sat.core.log import getLogger + +name = u"merge-requests_view" +access = C.PAGES_ACCESS_PUBLIC +template = u"merge-request/item.html" +log = getLogger(u"pages/" + name) + + +def parse_url(self, request): + try: + item_id = self.nextPath(request) + except IndexError: + log.warning(_(u"no ticket id specified")) + self.pageError(request, C.HTTP_BAD_REQUEST) + + data = self.getRData(request) + data[u"ticket_id"] = item_id + + +@defer.inlineCallbacks +def prepare_render(self, request): + data = self.getRData(request) + template_data = request.template_data + session = self.host.getSessionData(request, session_iface.ISATSession) + service, node, ticket_id = ( + data.get(u"service", u""), + data.get(u"node", u""), + data[u"ticket_id"], + ) + profile = self.getProfile(request) + + if profile is None: + profile = C.SERVICE_PROFILE + + tickets, metadata, parsed_tickets = yield self.host.bridgeCall( + "mergeRequestsGet", + service.full() if service else u"", + node, + C.NO_LIMIT, + [ticket_id], + "", + {"parse": C.BOOL_TRUE, "labels_as_list": C.BOOL_TRUE}, + profile, + ) + ticket = template_xmlui.create(self.host, tickets[0], ignore=["request_data", "type"]) + template_data[u"item"] = ticket + template_data["patches"] = parsed_tickets[0] + comments_uri = ticket.widgets["comments_uri"].value + if comments_uri: + uri_data = uri.parseXMPPUri(comments_uri) + template_data["comments_node"] = comments_node = uri_data["node"] + template_data["comments_service"] = comments_service = uri_data["path"] + comments = yield self.host.bridgeCall( + "mbGet", comments_service, comments_node, C.NO_LIMIT, [], {}, profile + ) + + template_data[u"comments"] = data_objects.BlogItems(comments) + template_data[u"login_url"] = self.getPageRedirectURL(request) + + if session.connected: + # we set edition URL only if user is the publisher or the node owner + publisher = jid.JID(ticket.widgets["publisher"].value) + is_publisher = publisher.userhostJID() == session.jid.userhostJID() + affiliation = None + if not is_publisher: + node = node or self.host.ns_map["merge_requests"] + affiliation = yield self.host.getAffiliation(request, service, node) + if is_publisher or affiliation == "owner": + template_data[u"url_ticket_edit"] = self.getURLByPath( + SubPage("merge-requests"), + service.full(), + node or u"@", + SubPage("merge-requests_edit"), + ticket_id, + ) + + +@defer.inlineCallbacks +def on_data_post(self, request): + type_ = self.getPostedData(request, u"type") + if type_ == u"comment": + blog_page = self.getPageByName(u"blog_view") + yield blog_page.on_data_post(self, request) + else: + log.warning(_(u"Unhandled data type: {}").format(type_))
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/pages/photos/album/page_meta.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,28 @@ +#!/usr/bin/env python2.7 +# -*- coding: utf-8 -*- + +from libervia.server.constants import Const as C +from sat.core.log import getLogger + +log = getLogger("pages/photo/album") + +name = u"photos_album" +access = C.PAGES_ACCESS_PROFILE +template = u"photo/album.html" + + +def parse_url(self, request): + self.getPathArgs(request, ["service", "*path"], min_args=1, service="jid", path="") + + +def prepare_render(self, request): + data = self.getRData(request) + data["thumb_limit"] = 1200 + data["retrieve_comments"] = True + files_page = self.getPageByName(u"files_list") + return files_page.prepare_render(self, request) + + +def on_data_post(self, request): + blog_page = self.getPageByName(u"blog_view") + return blog_page.on_data_post(self, request)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/pages/photos/page_meta.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,16 @@ +#!/usr/bin/env python2.7 +# -*- coding: utf-8 -*- + +from libervia.server.constants import Const as C +from twisted.internet import defer + +name = u"photos" +access = C.PAGES_ACCESS_PROFILE +template = u"photo/discover.html" + + +@defer.inlineCallbacks +def on_data_post(self, request): + jid_ = self.getPostedData(request, u"jid") + url = self.getPageByName(u"photos_album").getURL(jid_) + self.HTTPRedirect(request, url)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/pages/register/page_meta.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,61 @@ +#!/usr/bin/env python2.7 +# -*- coding: utf-8 -*- + +from libervia.server.constants import Const as C +from libervia.server import session_iface +from twisted.internet import defer +from sat.core.log import getLogger + +log = getLogger("pages/register") + +"""SàT account registration page""" + +name = u"register" +access = C.PAGES_ACCESS_PUBLIC +template = u"login/register.html" + + +def prepare_render(self, request): + profile = self.getProfile(request) + if profile is not None: + self.pageRedirect("/login/logged", request) + template_data = request.template_data + template_data["login_url"] = self.getPageByName("login").url + template_data["S_C"] = C # we need server constants in template + + # login error message + session_data = self.host.getSessionData(request, session_iface.ISATSession) + login_error = session_data.popPageData(self, "login_error") + if login_error is not None: + template_data["login_error"] = login_error + + # if fields were already filled, we reuse them + for k in (u"login", u"email", u"password"): + template_data[k] = session_data.popPageData(self, k) + + +@defer.inlineCallbacks +def on_data_post(self, request): + type_ = self.getPostedData(request, u"type") + if type_ == u"register": + login, email, password = self.getPostedData( + request, (u"login", u"email", u"password") + ) + status = yield self.host.registerNewAccount(request, login, password, email) + session_data = self.host.getSessionData(request, session_iface.ISATSession) + if status == C.REGISTRATION_SUCCEED: + # we prefill login field for login page + session_data.setPageData(self.getPageByName(u"login"), u"login", login) + # if we have a redirect_url we follow it + self.redirectOrContinue(request) + # else we redirect to login page + self.HTTPRedirect(request, self.getPageByName(u"login").url) + else: + session_data.setPageData(self, u"login_error", status) + l = locals() + for k in (u"login", u"email", u"password"): + # we save fields so user doesn't have to enter them again + session_data.setPageData(self, k, l[k]) + defer.returnValue(C.POST_NO_CONFIRM) + else: + self.pageError(request, C.HTTP_BAD_REQUEST)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/pages/tickets/disco/page_meta.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,41 @@ +#!/usr/bin/env python2.7 +# -*- coding: utf-8 -*- + +from libervia.server.constants import Const as C +from twisted.words.protocols.jabber import jid +from sat.core.log import getLogger + +log = getLogger("pages/ticket") +"""ticket handling pages""" + +name = u"tickets_disco" +access = C.PAGES_ACCESS_PUBLIC +template = u"ticket/discover.html" + + +def prepare_render(self, request): + tickets_trackers_config = self.host.options["tickets_trackers_json"] + if tickets_trackers_config: + trackers = request.template_data["tickets_trackers"] = [] + try: + for tracker_data in tickets_trackers_config: + service = tracker_data[u"service"] + node = tracker_data[u"node"] + name = tracker_data[u"name"] + url = self.getPageByName(u"tickets").getURL(service, node) + trackers.append({u"name": name, u"url": url}) + except KeyError as e: + log.warning(u"Missing field in tickets_trackers_json: {msg}".format(msg=e)) + except Exception as e: + log.warning(u"Can't decode tickets trackers: {msg}".format(msg=e)) + + +def on_data_post(self, request): + jid_str = self.getPostedData(request, u"jid") + try: + jid_ = jid.JID(jid_str) + except RuntimeError: + self.pageError(request, C.HTTP_BAD_REQUEST) + # for now we just use default node + url = self.getPageByName(u"tickets").getURL(jid_.full(), u"@") + self.HTTPRedirect(request, url)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/pages/tickets/edit/page_meta.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,94 @@ +#!/usr/bin/env python2.7 +# -*- coding: utf-8 -*- + +from libervia.server.constants import Const as C +from sat.core.i18n import _ +from twisted.internet import defer +from sat.tools.common import template_xmlui +from sat.core.log import getLogger + +log = getLogger("pages/ticket") +"""ticket handling pages""" + +name = u"tickets_edit" +access = C.PAGES_ACCESS_PROFILE +template = u"ticket/edit.html" + + +def parse_url(self, request): + try: + item_id = self.nextPath(request) + except IndexError: + log.warning(_(u"no ticket id specified")) + self.pageError(request, C.HTTP_BAD_REQUEST) + + data = self.getRData(request) + data[u"ticket_id"] = item_id + + +@defer.inlineCallbacks +def prepare_render(self, request): + data = self.getRData(request) + template_data = request.template_data + service, node, ticket_id = ( + data.get(u"service", u""), + data.get(u"node", u""), + data[u"ticket_id"], + ) + profile = self.getProfile(request) + + ignore = ( + "publisher", + "author", + "author_jid", + "author_email", + "created", + "updated", + "comments_uri", + ) + tickets = yield self.host.bridgeCall( + "ticketsGet", + service.full() if service else u"", + node, + C.NO_LIMIT, + [ticket_id], + "", + {}, + profile, + ) + ticket = [template_xmlui.create(self.host, x, ignore=ignore) for x in tickets[0]][0] + + try: + # small trick to get a one line text input instead of the big textarea + ticket.widgets[u"labels"].type = u"string" + ticket.widgets[u"labels"].value = ticket.widgets[u"labels"].value.replace( + u"\n", ", " + ) + except KeyError: + pass + template_data[u"new_ticket_xmlui"] = ticket + + +@defer.inlineCallbacks +def on_data_post(self, request): + data = self.getRData(request) + service = data["service"] + node = data["node"] + ticket_id = data["ticket_id"] + posted_data = self.getAllPostedData(request) + if not posted_data["title"] or not posted_data["body"]: + self.pageError(request, C.HTTP_BAD_REQUEST) + try: + posted_data["labels"] = [l.strip() for l in posted_data["labels"][0].split(",")] + except (KeyError, IndexError): + pass + profile = self.getProfile(request) + yield self.host.bridgeCall( + "ticketSet", service.full(), node, posted_data, u"", ticket_id, {}, profile + ) + # we don't want to redirect to edit page on success, but to tickets list + data["post_redirect_page"] = ( + self.getPageByName(u"tickets"), + service.full(), + node or u"@", + )
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/pages/tickets/new/page_meta.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,67 @@ +#!/usr/bin/env python2.7 +# -*- coding: utf-8 -*- + +from libervia.server.constants import Const as C +from twisted.internet import defer +from sat.tools.common import template_xmlui +from sat.core.log import getLogger + +log = getLogger("pages/ticket") +"""ticket handling pages""" + +name = u"tickets_new" +access = C.PAGES_ACCESS_PROFILE +template = u"ticket/create.html" + + +@defer.inlineCallbacks +def prepare_render(self, request): + data = self.getRData(request) + template_data = request.template_data + service, node = data.get(u"service", u""), data.get(u"node", u"") + profile = self.getProfile(request) + schema = yield self.host.bridgeCall("ticketsSchemaGet", service.full(), node, profile) + data["schema"] = schema + # following fields are handled in backend + ignore = ( + "author", + "author_jid", + "author_email", + "created", + "updated", + "comments_uri", + "status", + "milestone", + "priority", + ) + xmlui_obj = template_xmlui.create(self.host, schema, ignore=ignore) + try: + # small trick to get a one line text input instead of the big textarea + xmlui_obj.widgets[u"labels"].type = u"string" + except KeyError: + pass + template_data[u"new_ticket_xmlui"] = xmlui_obj + + +@defer.inlineCallbacks +def on_data_post(self, request): + data = self.getRData(request) + service = data["service"] + node = data["node"] + posted_data = self.getAllPostedData(request) + if not posted_data["title"] or not posted_data["body"]: + self.pageError(request, C.HTTP_BAD_REQUEST) + try: + posted_data["labels"] = [l.strip() for l in posted_data["labels"][0].split(",")] + except (KeyError, IndexError): + pass + profile = self.getProfile(request) + yield self.host.bridgeCall( + "ticketSet", service.full(), node, posted_data, u"", u"", {}, profile + ) + # we don't want to redirect to creation page on success, but to tickets list + data["post_redirect_page"] = ( + self.getPageByName(u"tickets"), + service.full(), + node or u"@", + )
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/pages/tickets/page_meta.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,53 @@ +#!/usr/bin/env python2.7 +# -*- coding: utf-8 -*- + +from libervia.server.constants import Const as C +from twisted.internet import defer +from sat.tools.common import template_xmlui +from sat.tools.common import data_objects +from sat.core.log import getLogger + +log = getLogger("pages/ticket") +"""ticket handling pages""" + +name = u"tickets" +access = C.PAGES_ACCESS_PUBLIC +template = u"ticket/overview.html" + + +def parse_url(self, request): + self.getPathArgs(request, ["service", "node"], service="jid") + data = self.getRData(request) + service, node = data[u"service"], data[u"node"] + if node is None: + self.pageRedirect(u"tickets_disco", request) + if node == u"@": + node = data[u"node"] = u"" + template_data = request.template_data + template_data[u"url_tickets_list"] = self.getURL(service.full(), node or u"@") + template_data[u"url_tickets_new"] = self.getSubPageURL(request, "tickets_new") + + +@defer.inlineCallbacks +def prepare_render(self, request): + data = self.getRData(request) + template_data = request.template_data + service, node = data[u"service"], data[u"node"] + profile = self.getProfile(request) or C.SERVICE_PROFILE + + self.checkCache(request, C.CACHE_PUBSUB, service=service, node=node, short="tickets") + + tickets = yield self.host.bridgeCall( + "ticketsGet", + service.full() if service else u"", + node, + C.NO_LIMIT, + [], + "", + {"labels_as_list": C.BOOL_TRUE}, + profile, + ) + template_data[u"tickets"] = [template_xmlui.create(self.host, x) for x in tickets[0]] + template_data[u"on_ticket_click"] = data_objects.OnClick( + url=self.getSubPageURL(request, u"tickets_view") + u"/{item.id}" + )
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/pages/tickets/view/page_meta.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,98 @@ +#!/usr/bin/env python2.7 +# -*- coding: utf-8 -*- + +from libervia.server.constants import Const as C +from sat.core.i18n import _ +from libervia.server.utils import SubPage +from libervia.server import session_iface +from twisted.internet import defer +from twisted.words.protocols.jabber import jid +from sat.tools.common import template_xmlui +from sat.tools.common import uri +from sat.tools.common import data_objects +from sat.core.log import getLogger + +log = getLogger("pages/tickets/view") +"""ticket handling pages""" + +name = u"tickets_view" +access = C.PAGES_ACCESS_PUBLIC +template = u"ticket/item.html" + + +def parse_url(self, request): + try: + item_id = self.nextPath(request) + except IndexError: + log.warning(_(u"no ticket id specified")) + self.pageError(request, C.HTTP_BAD_REQUEST) + + data = self.getRData(request) + data[u"ticket_id"] = item_id + + +@defer.inlineCallbacks +def prepare_render(self, request): + data = self.getRData(request) + template_data = request.template_data + session = self.host.getSessionData(request, session_iface.ISATSession) + service, node, ticket_id = ( + data.get(u"service", u""), + data.get(u"node", u""), + data[u"ticket_id"], + ) + profile = self.getProfile(request) + + if profile is None: + profile = C.SERVICE_PROFILE + + tickets = yield self.host.bridgeCall( + "ticketsGet", + service.full() if service else u"", + node, + C.NO_LIMIT, + [ticket_id], + "", + {"labels_as_list": C.BOOL_TRUE}, + profile, + ) + ticket = [template_xmlui.create(self.host, x) for x in tickets[0]][0] + template_data[u"item"] = ticket + comments_uri = ticket.widgets["comments_uri"].value + if comments_uri: + uri_data = uri.parseXMPPUri(comments_uri) + template_data["comments_node"] = comments_node = uri_data["node"] + template_data["comments_service"] = comments_service = uri_data["path"] + comments = yield self.host.bridgeCall( + "mbGet", comments_service, comments_node, C.NO_LIMIT, [], {}, profile + ) + + template_data[u"comments"] = data_objects.BlogItems(comments) + template_data[u"login_url"] = self.getPageRedirectURL(request) + + if session.connected: + # we set edition URL only if user is the publisher or the node owner + publisher = jid.JID(ticket.widgets["publisher"].value) + is_publisher = publisher.userhostJID() == session.jid.userhostJID() + affiliation = None + if not is_publisher: + node = node or self.host.ns_map["tickets"] + affiliation = yield self.host.getAffiliation(request, service, node) + if is_publisher or affiliation == "owner": + template_data[u"url_ticket_edit"] = self.getURLByPath( + SubPage("tickets"), + service.full(), + node or u"@", + SubPage("tickets_edit"), + ticket_id, + ) + + +@defer.inlineCallbacks +def on_data_post(self, request): + type_ = self.getPostedData(request, u"type") + if type_ == u"comment": + blog_page = self.getPageByName(u"blog_view") + yield blog_page.on_data_post(self, request) + else: + log.warning(_(u"Unhandled data type: {}").format(type_))
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/pages/u/atom.xml/page_meta.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,3 @@ +#!/usr/bin/env python2.7 +# -*- coding: utf-8 -*- +redirect = u"blog_feed_atom"
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/pages/u/blog/page_meta.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,15 @@ +#!/usr/bin/env python2.7 +# -*- coding: utf-8 -*- + +name = u"user_blog" + + +def parse_url(self, request): + # in this subpage, we want path args and query args + # (i.e. what's remaining in URL: filters, id, etc.) + # to be used by blog's url parser, so we don't skip parse_url + data = self.getRData(request) + service = data[u"service"] + self.pageRedirect( + u"blog_view", request, skip_parse_url=False, path_args=[service.full(), u"@"] + )
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/pages/u/page_meta.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,40 @@ +#!/usr/bin/env python2.7 +# -*- coding: utf-8 -*- + +from libervia.server.constants import Const as C +from twisted.internet import defer +from twisted.words.protocols.jabber import jid + +"""page used to target a user profile, e.g. for public blog""" + +name = u"user" +access = C.PAGES_ACCESS_PUBLIC # can be a callable +template = u"blog/articles.html" +url_cache = True + + +@defer.inlineCallbacks +def parse_url(self, request): + try: + prof_requested = self.nextPath(request) + except IndexError: + self.pageError(request) + + data = self.getRData(request) + + target_profile = yield self.host.bridgeCall("profileNameGet", prof_requested) + request.template_data[u"target_profile"] = target_profile + target_jid = yield self.host.bridgeCall( + "asyncGetParamA", "JabberID", "Connection", "value", profile_key=target_profile + ) + target_jid = jid.JID(target_jid) + data[u"service"] = target_jid + + +@defer.inlineCallbacks +def prepare_render(self, request): + data = self.getRData(request) + self.checkCache( + request, C.CACHE_PUBSUB, service=data[u"service"], node=None, short="microblog" + ) + self.pageRedirect(u"blog_view", request)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/server/blog.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,894 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2011-2018 Jérôme Poisson <goffi@goffi.org> +# Copyright (C) 2013-2016 Adrien Cossa <souliane@mailoo.org> + +# This program 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. + +# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + +from sat.core.i18n import _, D_ +from sat_frontends.tools.strings import addURLToText, fixXHTMLLinks +from sat.core.log import getLogger + +log = getLogger(__name__) +from sat.tools.common import data_format +from sat.tools import xml_tools +from dbus.exceptions import DBusException +from twisted.internet import defer +from twisted.web import server +from twisted.web.resource import Resource +from twisted.words.protocols.jabber.jid import JID +from twisted.words.xish import domish +from jinja2 import Environment, PackageLoader +from datetime import datetime +import re +import os +import sys +import urllib + +from libervia.server.html_tools import sanitizeHtml, convertNewLinesToXHTML +from libervia.server.constants import Const as C + +NS_ATOM = "http://www.w3.org/2005/Atom" +PARAMS_TO_GET = ( + C.STATIC_BLOG_PARAM_TITLE, + C.STATIC_BLOG_PARAM_BANNER, + C.STATIC_BLOG_PARAM_KEYWORDS, + C.STATIC_BLOG_PARAM_DESCRIPTION, +) +re_strip_empty_div = re.compile(r"<div ?/>|<div> *?</div>") + +# TODO: check disco features and use max_items when RSM is not available +# FIXME: change navigation links handling, this is is fragile +# XXX: this page will disappear, LiberviaPage will be used instead +# TODO: delete this page and create a compatibility page for links + + +def getDefaultQueryData(request): + """Return query data which must be present in all links + + @param request(twisted.web.http.Request): request instance comming from render + @return (dict): a dict with values as expected by urllib.urlencode + """ + default_query_data = {} + try: + default_query_data["tag"] = request.extra_dict[ + "mam_filter_{}".format(C.MAM_FILTER_CATEGORY) + ].encode("utf-8") + except KeyError: + pass + return default_query_data + + +def _quote(value): + """Quote a value for use in url + + @param value(unicode): value to quote + @return (str): quoted value + """ + return urllib.quote(value.encode("utf-8"), "") + + +def _unquote(quoted_value): + """Unquote a value coming from url + + @param unquote_value(str): value to unquote + @return (unicode): unquoted value + """ + assert not isinstance(quoted_value, unicode) + return urllib.unquote(quoted_value).decode("utf-8") + + +def _urlencode(query): + """Same as urllib.urlencode, but use '&' instead of '&'""" + return "&".join( + [ + "{}={}".format(urllib.quote_plus(str(k)), urllib.quote_plus(str(v))) + for k, v in query.iteritems() + ] + ) + + +class TemplateProcessor(object): + + THEME = "default" + + def __init__(self, host): + self.host = host + + # add Libervia's themes directory to the python path + sys.path.append(os.path.dirname(os.path.normpath(self.host.themes_dir))) + themes = os.path.basename(os.path.normpath(self.host.themes_dir)) + self.env = Environment(loader=PackageLoader(themes, self.THEME)) + + def useTemplate(self, request, tpl, data=None): + theme_url = os.path.join("/", C.THEMES_URL, self.THEME) + + data_ = { + "images": os.path.join(theme_url, "images"), + "styles": os.path.join(theme_url, "styles"), + } + if data: + data_.update(data) + + template = self.env.get_template("{}.html".format(tpl)) + return template.render(**data_).encode("utf-8") + + +class MicroBlog(Resource, TemplateProcessor): + isLeaf = True + + def __init__(self, host): + self.host = host + Resource.__init__(self) + TemplateProcessor.__init__(self, host) + self.avatars_cache = {} + + def _avatarPathToUrl(self, avatar, request, bare_jid_s): + filename = os.path.basename(avatar) + avatar_url = os.path.join(self.host.service_cache_url, filename) + self.avatars_cache[bare_jid_s] = avatar_url + return avatar_url + + def getAvatarURL(self, pub_jid, request): + """Return avatar of a jid if in cache, else ask for it. + + @param pub_jid (JID): publisher JID + @return: deferred avatar URL (unicode) + """ + bare_jid_s = pub_jid.userhost() + try: + url = self.avatars_cache[bare_jid_s] + except KeyError: + self.avatars_cache[ + bare_jid_s + ] = "" # avoid to request the vcard several times + d = self.host.bridgeCall( + "avatarGet", bare_jid_s, False, False, C.SERVICE_PROFILE + ) + d.addCallback(self._avatarPathToUrl, request, bare_jid_s) + return d + return defer.succeed(url if url else C.DEFAULT_AVATAR_URL) + + def render_GET(self, request): + if not request.postpath or len(request.postpath) > 2: + return self.useTemplate( + request, "static_blog_error", {"message": "You must indicate a nickname"} + ) + + prof_requested = _unquote(request.postpath[0]) + + try: + prof_found = self.host.bridge.profileNameGet(prof_requested) + except DBusException: + prof_found = None + if not prof_found or prof_found == C.SERVICE_PROFILE: + return self.useTemplate( + request, "static_blog_error", {"message": "Invalid nickname"} + ) + + d = defer.Deferred() + # TODO: jid caching + self.host.bridge.asyncGetParamA( + "JabberID", + "Connection", + "value", + profile_key=prof_found, + callback=d.callback, + errback=d.errback, + ) + d.addCallback(self.render_gotJID, request, prof_found) + return server.NOT_DONE_YET + + def render_gotJID(self, pub_jid_s, request, profile): + pub_jid = JID(pub_jid_s) + + request.extra_dict = {} # will be used for RSM and MAM + self.parseURLParams(request) + if request.item_id: + # FIXME: this part seems useless + # we want a specific item + # item_ids = [request.item_id] + # max_items = 1 + max_items = C.NO_LIMIT # FIXME + else: + # max_items = int(request.extra_dict['rsm_max']) # FIXME + max_items = C.NO_LIMIT + # TODO: use max_items only when RSM is not available + + if request.atom: + request.extra_dict.update(request.mam_extra) + self.getAtom( + pub_jid, + max_items, + request.extra_dict, + request.extra_comments_dict, + request, + profile, + ) + + elif request.item_id: + # we can't merge mam_extra now because we'll use item_ids + self.getItemById( + pub_jid, + request.item_id, + request.extra_dict, + request.extra_comments_dict, + request, + profile, + ) + else: + request.extra_dict.update(request.mam_extra) + self.getItems( + pub_jid, + max_items, + request.extra_dict, + request.extra_comments_dict, + request, + profile, + ) + + ## URL parsing + + def parseURLParams(self, request): + """Parse the request URL parameters. + + @param request: HTTP request + """ + if len(request.postpath) > 1: + if request.postpath[1] == "atom.xml": # return the atom feed + request.atom = True + request.item_id = None + else: + request.atom = False + request.item_id = _unquote(request.postpath[1]) + else: + request.item_id = None + request.atom = False + + self.parseURLParamsRSM(request) + # XXX: request.display_single is True when only one blog post is visible + request.display_single = (request.item_id is not None) or int( + request.extra_dict["rsm_max"] + ) == 1 + self.parseURLParamsCommentsRSM(request) + self.parseURLParamsMAM(request) + + def parseURLParamsRSM(self, request): + """Parse RSM request data from the URL parameters for main items + + fill request.extra_dict accordingly + @param request: HTTP request + """ + if request.item_id: # XXX: item_id and RSM are not compatible + return + try: + rsm_max = int(request.args["max"][0]) + if rsm_max > C.STATIC_RSM_MAX_LIMIT: + log.warning(u"Request with rsm_max over limit ({})".format(rsm_max)) + rsm_max = C.STATIC_RSM_MAX_LIMIT + request.extra_dict["rsm_max"] = unicode(rsm_max) + except (ValueError, KeyError): + request.extra_dict["rsm_max"] = unicode(C.STATIC_RSM_MAX_DEFAULT) + try: + request.extra_dict["rsm_index"] = request.args["index"][0] + except (ValueError, KeyError): + try: + request.extra_dict["rsm_before"] = request.args["before"][0].decode( + "utf-8" + ) + except KeyError: + try: + request.extra_dict["rsm_after"] = request.args["after"][0].decode( + "utf-8" + ) + except KeyError: + pass + + def parseURLParamsCommentsRSM(self, request): + """Parse RSM request data from the URL parameters for comments + + fill request.extra_dict accordingly + @param request: HTTP request + """ + request.extra_comments_dict = {} + if request.display_single: + try: + rsm_max = int(request.args["comments_max"][0]) + if rsm_max > C.STATIC_RSM_MAX_LIMIT: + log.warning(u"Request with rsm_max over limit ({})".format(rsm_max)) + rsm_max = C.STATIC_RSM_MAX_LIMIT + request.extra_comments_dict["rsm_max"] = unicode(rsm_max) + except (ValueError, KeyError): + request.extra_comments_dict["rsm_max"] = unicode( + C.STATIC_RSM_MAX_COMMENTS_DEFAULT + ) + else: + request.extra_comments_dict["rsm_max"] = "0" + + def parseURLParamsMAM(self, request): + """Parse MAM request data from the URL parameters for main items + + fill request.extra_dict accordingly + @param request: HTTP request + """ + # XXX: we use a separate dict for MAM as the filters are not used + # when display_single is set (because it then use item_ids which + # can't be used with MAM), but it is still used in this case + # for navigation links. + request.mam_extra = {} + try: + request.mam_extra[ + "mam_filter_{}".format(C.MAM_FILTER_CATEGORY) + ] = request.args["tag"][0].decode("utf-8") + except KeyError: + pass + + ## Items retrieval + + def getItemById( + self, pub_jid, item_id, extra_dict, extra_comments_dict, request, profile + ): + """ + + @param pub_jid (jid.JID): publisher JID + @param item_id(unicode): ID of the item to retrieve + @param extra_dict (dict): extra configuration for initial items only + @param extra_comments_dict (dict): extra configuration for comments only + @param request: HTTP request + @param profile + """ + + def gotItems(items): + items, metadata = items + item = items[0] # assume there's only one item + + def gotMetadata(result): + dummy, rsm_metadata = result + try: + metadata["rsm_count"] = rsm_metadata["rsm_count"] + except KeyError: + pass + try: + metadata["rsm_index"] = unicode(int(rsm_metadata["rsm_index"]) - 1) + except KeyError: + pass + + metadata["rsm_first"] = metadata["rsm_last"] = item["id"] + + def gotComments(comments): + # at this point we can merge mam dict + request.extra_dict.update(request.mam_extra) + # build the items as self.getItems would do it (and as self.renderHTML expects them to be) + comments = [ + ( + item["comments_service"], + item["comments_node"], + "", + comments[0], + comments[1], + ) + ] + self.renderHTML( + [(item, comments)], metadata, request, pub_jid, profile + ) + + # get the comments + # max_comments = int(extra_comments_dict['rsm_max']) # FIXME + max_comments = C.NO_LIMIT + # TODO: use max_comments only when RSM is not available + self.host.bridge.mbGet( + item["comments_service"], + item["comments_node"], + max_comments, + [], + extra_comments_dict, + C.SERVICE_PROFILE, + callback=gotComments, + errback=lambda failure: self.renderError(failure, request, pub_jid), + ) + + # XXX: retrieve RSM information related to the main item. We can't do it while + # retrieving the item, because item_ids and rsm should not be used together. + self.host.bridge.mbGet( + pub_jid.userhost(), + "", + 0, + [], + {"rsm_max": "1", "rsm_after": item["id"]}, + C.SERVICE_PROFILE, + callback=gotMetadata, + errback=lambda failure: self.renderError(failure, request, pub_jid), + ) + + # get the main item + self.host.bridge.mbGet( + pub_jid.userhost(), + "", + 0, + [item_id], + extra_dict, + C.SERVICE_PROFILE, + callback=gotItems, + errback=lambda failure: self.renderError(failure, request, pub_jid), + ) + + def getItems( + self, pub_jid, max_items, extra_dict, extra_comments_dict, request, profile + ): + """ + + @param pub_jid (jid.JID): publisher JID + @param max_items(int): maximum number of item to get, C.NO_LIMIT for no limit + @param extra_dict (dict): extra configuration for initial items only + @param extra_comments_dict (dict): extra configuration for comments only + @param request: HTTP request + @param profile + """ + + def getResultCb(data, rt_session): + remaining, results = data + # we have requested one node only + assert remaining == 0 + assert len(results) == 1 + service, node, failure, items, metadata = results[0] + if failure: + self.renderError(failure, request, pub_jid) + else: + self.renderHTML(items, metadata, request, pub_jid, profile) + + def getResult(rt_session): + self.host.bridge.mbGetFromManyWithCommentsRTResult( + rt_session, + C.SERVICE_PROFILE, + callback=lambda data: getResultCb(data, rt_session), + errback=lambda failure: self.renderError(failure, request, pub_jid), + ) + + # max_comments = int(extra_comments_dict['rsm_max']) # FIXME + max_comments = 0 + # TODO: use max_comments only when RSM is not available + self.host.bridge.mbGetFromManyWithComments( + C.JID, + [pub_jid.userhost()], + max_items, + max_comments, + extra_dict, + extra_comments_dict, + C.SERVICE_PROFILE, + callback=getResult, + ) + + def getAtom( + self, pub_jid, max_items, extra_dict, extra_comments_dict, request, profile + ): + """ + + @param pub_jid (jid.JID): publisher JID + @param max_items(int): maximum number of item to get, C.NO_LIMIT for no limit + @param extra_dict (dict): extra configuration for initial items only + @param extra_comments_dict (dict): extra configuration for comments only + @param request: HTTP request + @param profile + """ + + def gotItems(data): + # Generate a clean atom feed with uri linking to this blog + # from microblog data + items, metadata = data + feed_elt = domish.Element((NS_ATOM, u"feed")) + title = _(u"{user}'s blog").format(user=profile) + feed_elt.addElement(u"title", content=title) + + base_blog_url = self.host.getExtBaseURL( + request, u"blog/{user}".format(user=profile) + ) + + # atom link + link_feed_elt = feed_elt.addElement("link") + link_feed_elt["href"] = u"{base}/atom.xml".format(base=base_blog_url) + link_feed_elt["type"] = u"application/atom+xml" + link_feed_elt["rel"] = u"self" + + # blog link + link_blog_elt = feed_elt.addElement("link") + link_blog_elt["rel"] = u"alternate" + link_blog_elt["type"] = u"text/html" + link_blog_elt["href"] = base_blog_url + + # blog link XMPP uri + blog_xmpp_uri = metadata["uri"] + link_blog_elt = feed_elt.addElement("link") + link_blog_elt["rel"] = u"alternate" + link_blog_elt["type"] = u"application/atom+xml" + link_blog_elt["href"] = blog_xmpp_uri + + feed_elt.addElement("id", content=_quote(blog_xmpp_uri)) + updated_unix = max([float(item["updated"]) for item in items]) + updated_dt = datetime.fromtimestamp(updated_unix) + feed_elt.addElement( + u"updated", content=u"{}Z".format(updated_dt.isoformat("T")) + ) + + for item in items: + entry_elt = feed_elt.addElement(u"entry") + + # Title + try: + title = item["title"] + except KeyError: + # for microblog (without title), we use an abstract of content as title + title = u"{}…".format(u" ".join(item["content"][:70].split())) + entry_elt.addElement(u"title", content=title) + + # HTTP link + http_link_elt = entry_elt.addElement(u"link") + http_link_elt["rel"] = u"alternate" + http_link_elt["type"] = u"text/html" + http_link_elt["href"] = u"{base}/{quoted_id}".format( + base=base_blog_url, quoted_id=_quote(item["id"]) + ) + # XMPP link + xmpp_link_elt = entry_elt.addElement(u"link") + xmpp_link_elt["rel"] = u"alternate" + xmpp_link_elt["type"] = u"application/atom+xml" + xmpp_link_elt["href"] = u"{blog_uri};item={item_id}".format( + blog_uri=blog_xmpp_uri, item_id=item["id"] + ) + + # date metadata + entry_elt.addElement(u"id", content=item["atom_id"]) + updated = datetime.fromtimestamp(float(item["updated"])) + entry_elt.addElement( + u"updated", content=u"{}Z".format(updated.isoformat("T")) + ) + published = datetime.fromtimestamp(float(item["published"])) + entry_elt.addElement( + u"published", content=u"{}Z".format(published.isoformat("T")) + ) + + # author metadata + author_elt = entry_elt.addElement(u"author") + author_elt.addElement("name", content=item.get("author", profile)) + try: + author_elt.addElement( + "uri", content=u"xmpp:{}".format(item["author_jid"]) + ) + except KeyError: + pass + try: + author_elt.addElement("email", content=item["author_email"]) + except KeyError: + pass + + # categories + for tag in data_format.dict2iter("tag", item): + category_elt = entry_elt.addElement(u"category") + category_elt["term"] = tag + + # content + try: + content_xhtml = item["content_xhtml"] + except KeyError: + content_elt = entry_elt.addElement("content", content="content") + content_elt["type"] = "text" + else: + content_elt = entry_elt.addElement("content") + content_elt["type"] = "xhtml" + content_elt.addChild( + xml_tools.ElementParser()(content_xhtml, namespace=C.NS_XHTML) + ) + + atom_feed = u'<?xml version="1.0" encoding="utf-8"?>\n{}'.format( + feed_elt.toXml() + ) + self.renderAtomFeed(atom_feed, request), + + self.host.bridge.mbGet( + pub_jid.userhost(), + "", + max_items, + [], + extra_dict, + C.SERVICE_PROFILE, + callback=gotItems, + ) + + ## rendering + + def _updateDict(self, value, dict_, key): + dict_[key] = value + + def _getImageParams(self, options, key, default, alt): + """regexp from http://answers.oreilly.com/topic/280-how-to-validate-urls-with-regular-expressions/""" + url = options[key] if key in options else "" + regexp = ( + r"^(https?|ftp)://[a-z0-9-]+(\.[a-z0-9-]+)+(/[\w-]+)*/[\w-]+\.(gif|png|jpg)$" + ) + if re.match(regexp, url): + url = url + else: + url = default + return BlogImage(url, alt) + + def renderError(self, failure, request, pub_jid): + request.setResponseCode(500) + request.write( + self.useTemplate( + request, "static_blog_error", {"message": "Can't access requested data"} + ) + ) + request.finish() + + def renderHTML(self, items, metadata, request, pub_jid, profile): + """Retrieve the user parameters before actually rendering the static blog + + @param items(list[tuple(dict, list)]): same as in self._renderHTML + @param metadata(dict): original node metadata + @param request: HTTP request + @param pub_jid (JID): publisher JID + @param profile (unicode): %(doc_profile)s + """ + d_list = [] + options = {} + + d = self.getAvatarURL(pub_jid, request) + d.addCallback(self._updateDict, options, "avatar") + d.addErrback(self.renderError, request, pub_jid) + d_list.append(d) + + for param_name in PARAMS_TO_GET: + d = defer.Deferred() + self.host.bridge.asyncGetParamA( + param_name, + C.STATIC_BLOG_KEY, + "value", + C.SERVER_SECURITY_LIMIT, + profile, + callback=d.callback, + errback=d.errback, + ) + d.addCallback(self._updateDict, options, param_name) + d.addErrback(self.renderError, request, pub_jid) + d_list.append(d) + + dlist_d = defer.DeferredList(d_list) + dlist_d.addCallback( + lambda dummy: self._renderHTML(items, metadata, options, request, pub_jid) + ) + + def _renderHTML(self, items, metadata, options, request, pub_jid): + """Actually render the static blog. + + If mblog_data is a list of dict, we are missing the comments items so we just + display the main items. If mblog_data is a list of couple, each couple is + associating a main item data with the list of its comments, so we render all. + @param items(list[tuple(dict, list)]): list of 2-tuple with + - item(dict): item microblog data + - comments_list(list[tuple]): list of 5-tuple with + - service (unicode): pubsub service where the comments node is + - node (unicode): comments node + - failure (unicode): empty in case of success, else error message + - comments(list[dict]): list of microblog data + - comments_metadata(dict): metadata of the comment node + @param metadata(dict): original node metadata + @param options: dict defining the blog's parameters + @param request: the HTTP request + @param pub_jid (JID): publisher JID + """ + if not isinstance(options, dict): + options = {} + user = sanitizeHtml(pub_jid.user) + base_url = os.path.join("/blog/", user) + + def getOption(key): + return sanitizeHtml(options[key]) if key in options else "" + + avatar = os.path.normpath("/{}".format(getOption("avatar"))) + title = getOption(C.STATIC_BLOG_PARAM_TITLE) or user + query_data = _urlencode(getDefaultQueryData(request)).decode("utf-8") + + xmpp_uri = metadata["uri"] + if len(items) == 1: + # FIXME: that's really not a good way to get item id + # this must be changed after static blog refactorisation + item_id = items[0][0]["id"] + xmpp_uri += u";item={}".format(_quote(item_id)) + + data = { + "url_base": base_url, + "xmpp_uri": xmpp_uri, + "url_query": u"?{}".format(query_data) if query_data else "", + "keywords": getOption(C.STATIC_BLOG_PARAM_KEYWORDS), + "description": getOption(C.STATIC_BLOG_PARAM_DESCRIPTION), + "title": title, + "favicon": avatar, + "banner_img": self._getImageParams( + options, C.STATIC_BLOG_PARAM_BANNER, avatar, title + ), + } + + data["navlinks"] = NavigationLinks(request, items, metadata, base_url) + data["messages"] = [] + for item in items: + item, comments_list = item + comments, comments_count = [], 0 + for node_comments in comments_list: + comments.extend(node_comments[3]) + try: + comments_count += int(node_comments[4]["rsm_count"]) + except KeyError: + pass + data["messages"].append( + BlogMessage(request, base_url, item, comments, comments_count) + ) + + request.write(self.useTemplate(request, "static_blog", data)) + request.finish() + + def renderAtomFeed(self, feed, request): + request.write(feed.encode("utf-8")) + request.finish() + + +class NavigationLinks(object): + def __init__(self, request, items, metadata, base_url): + """Build the navigation links. + + @param items (list): list of items + @param metadata (dict): rsm data + @param base_url (unicode): the base URL for this user's blog + @return: dict + """ + # FIXME: this code must be refactorized, it is fragile + # and difficult to maintain + + # query data which must be present in all links + default_query_data = getDefaultQueryData(request) + + # which links we need to display + if request.display_single: + links = ("later_message", "older_message") + # key must exist when using the template + self.later_messages = self.older_messages = "" + else: + links = ("later_messages", "older_messages") + self.later_message = self.older_message = "" + + # now we set the links according to RSM + for key in links: + query_data = default_query_data.copy() + + if key.startswith("later_message"): + try: + index = int(metadata["rsm_index"]) + except (KeyError, ValueError): + pass + else: + if index == 0: + # we don't show this link on first page + setattr(self, key, "") + continue + try: + query_data["before"] = metadata["rsm_first"].encode("utf-8") + except KeyError: + pass + else: + try: + index = int(metadata["rsm_index"]) + count = int(metadata.get("rsm_count")) + except (KeyError, ValueError): + # XXX: if we don't have index or count, we can't know if we + # are on the last page or not + pass + else: + # if we have index, we don't show the after link + # on the last page + if index + len(items) >= count: + setattr(self, key, "") + continue + try: + query_data["after"] = metadata["rsm_last"].encode("utf-8") + except KeyError: + pass + + if request.display_single: + query_data["max"] = 1 + + link = "{}?{}".format(base_url, _urlencode(query_data)) + setattr(self, key, BlogLink(link, key, key.replace("_", " "))) + + +class BlogImage(object): + def __init__(self, url_, alt): + self.url = url_ + self.alt = alt + + +class BlogLink(object): + def __init__(self, url_, style, text): + self.url = url_ + self.style = style + self.text = text + + +class BlogMessage(object): + def __init__(self, request, base_url, entry, comments=None, comments_count=0): + """ + + @param request: HTTP request + @param base_url (unicode): the base URL + @param entry(dict): item microblog data + @param comments(list[dict]): list of microblog data + @param comments_count (int): total number of comments + """ + if comments is None: + comments = [] + timestamp = float(entry.get("published", 0)) + + # FIXME: for now we assume that the comments' depth is only 1 + is_comment = not entry.get("comments", False) + + self.date = datetime.fromtimestamp(timestamp) + self.type = "comment" if is_comment else "main_item" + self.style = "mblog_comment" if is_comment else "" + self.content = self.getText(entry, "content") + + if is_comment: + self.author = _(u"from {}").format(entry["author"]) + else: + self.author = " " + self.url = "{}/{}".format(base_url, _quote(entry["id"])) + query_data = getDefaultQueryData(request) + if query_data: + self.url += "?{}".format(_urlencode(query_data)) + self.title = self.getText(entry, "title") + self.tags = [sanitizeHtml(tag) for tag in data_format.dict2iter("tag", entry)] + + count_text = lambda count: D_(u"comments") if count > 1 else D_(u"comment") + + self.comments_text = u"{} {}".format( + comments_count, count_text(comments_count) + ) + + delta = comments_count - len(comments) + if request.display_single and delta > 0: + prev_url = "{}?{}".format( + self.url, _urlencode({"comments_max": comments_count}) + ) + prev_text = D_(u"show {count} previous {comments}").format( + count=delta, comments=count_text(delta) + ) + self.all_comments_link = BlogLink(prev_url, "comments_link", prev_text) + + if comments: + self.comments = [ + BlogMessage(request, base_url, comment) for comment in comments + ] + + def getText(self, entry, key): + try: + xhtml = entry["{}_xhtml".format(key)] + except KeyError: + try: + processor = addURLToText if key.startswith("content") else sanitizeHtml + return convertNewLinesToXHTML(processor(entry[key])) + except KeyError: + return None + else: + # FIXME: empty <div /> elements provoke rendering issue + # this regex is a temporary workadound, need more investigation + xhtml = re_strip_empty_div.sub("", xhtml) + return fixXHTMLLinks(xhtml)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/server/constants.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,120 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a SAT frontend +# Copyright (C) 2009-2018 Jérôme Poisson (goffi@goffi.org) + +# This program 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. + +# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + +from ..common import constants + + +class Const(constants.Const): + + APP_NAME = "Libervia" + APP_NAME_FILE = "libervia" + SERVICE_PROFILE = "libervia" # the SàT profile that is used for exporting the service + + SESSION_TIMEOUT = 7200 # Session's timeout, after that the user will be disconnected + HTML_DIR = "html/" + THEMES_DIR = "themes/" + THEMES_URL = "themes" + MEDIA_DIR = "media/" + CARDS_DIR = "games/cards/tarot" + PAGES_DIR = u"pages" + + ERRNUM_BRIDGE_ERRBACK = 0 # FIXME + ERRNUM_LIBERVIA = 0 # FIXME + + # Security limit for Libervia (get/set params) + SECURITY_LIMIT = 5 + + # Security limit for Libervia server_side + SERVER_SECURITY_LIMIT = constants.Const.NO_SECURITY_LIMIT + + # keys for cache values we can get from browser + ALLOWED_ENTITY_DATA = {"avatar", "nick"} + + STATIC_RSM_MAX_LIMIT = 100 + STATIC_RSM_MAX_DEFAULT = 10 + STATIC_RSM_MAX_COMMENTS_DEFAULT = 10 + + ## Libervia pages ## + PAGES_META_FILE = u"page_meta.py" + PAGES_ACCESS_NONE = ( + u"none" + ) # no access to this page (using its path will return a 404 error) + PAGES_ACCESS_PUBLIC = u"public" + PAGES_ACCESS_PROFILE = ( + u"profile" + ) # a session with an existing profile must be started + PAGES_ACCESS_ADMIN = u"admin" # only profiles set in admins_list can access the page + PAGES_ACCESS_ALL = ( + PAGES_ACCESS_NONE, + PAGES_ACCESS_PUBLIC, + PAGES_ACCESS_PROFILE, + PAGES_ACCESS_ADMIN, + ) + # names of the page to use for menu + DEFAULT_MENU = [ + "login", + "chat", + "blog", + "forums", + "photos", + "files", + "events", + "tickets", + "merge-requests", + "app", + ] + + ## Session flags ## + FLAG_CONFIRM = u"CONFIRM" + + ## Data post ## + POST_NO_CONFIRM = u"POST_NO_CONFIRM" + + ## HTTP methods ## + HTTP_METHOD_GET = u"GET" + HTTP_METHOD_POST = u"POST" + + ## HTTP codes ## + HTTP_SEE_OTHER = 303 + HTTP_NOT_MODIFIED = 304 + HTTP_BAD_REQUEST = 400 + HTTP_UNAUTHORIZED = 401 + HTTP_NOT_FOUND = 404 + HTTP_INTERNAL_ERROR = 500 + HTTP_SERVICE_UNAVAILABLE = 503 + + ## Cache ## + CACHE_PUBSUB = 0 + + ## Date/Time ## + HTTP_DAYS = ("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun") + HTTP_MONTH = ( + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", + )
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/server/html_tools.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,36 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2011-2018 Jérôme Poisson <goffi@goffi.org> + +# This program 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. + +# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + + +def sanitizeHtml(text): + """Sanitize HTML by escaping everything""" + # this code comes from official python wiki: http://wiki.python.org/moin/EscapingHtml + html_escape_table = { + "&": "&", + '"': """, + "'": "'", + ">": ">", + "<": "<", + } + + return "".join(html_escape_table.get(c, c) for c in text) + + +def convertNewLinesToXHTML(text): + return text.replace("\n", "<br/>")
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/server/pages.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,1383 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2011-2018 Jérôme Poisson <goffi@goffi.org> + +# This program 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. + +# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. +from twisted.web import server +from twisted.web import resource as web_resource +from twisted.web import util as web_util +from twisted.internet import defer +from twisted.words.protocols.jabber import jid +from twisted.python import failure + +from sat.core.i18n import _ +from sat.core import exceptions +from sat.tools.common import uri as common_uri +from sat.tools.common import date_utils +from sat.core.log import getLogger + +log = getLogger(__name__) +from libervia.server.constants import Const as C +from libervia.server import session_iface +from libervia.server.utils import quote, SubPage +import libervia + +from collections import namedtuple +import uuid +import os.path +import urllib +import time +import hashlib + +WebsocketMeta = namedtuple("WebsocketMeta", ("url", "token", "debug")) + + +class CacheBase(object): + def __init__(self): + self._created = time.time() + self._last_access = self._created + + @property + def created(self): + return self._created + + @property + def last_access(self): + return self._last_access + + @last_access.setter + def last_access(self, timestamp): + self._last_access = timestamp + + +class CachePage(CacheBase): + def __init__(self, rendered): + super(CachePage, self).__init__() + self._created = time.time() + self._last_access = self._created + self._rendered = rendered + self._hash = hashlib.sha256(rendered).hexdigest() + + @property + def rendered(self): + return self._rendered + + @property + def hash(self): + return self._hash + + +class CacheURL(CacheBase): + def __init__(self, request): + super(CacheURL, self).__init__() + try: + self._data = request.data.copy() + except AttributeError: + self._data = {} + self._template_data = request.template_data.copy() + self._prepath = request.prepath[:] + self._postpath = request.postpath[:] + del self._template_data["csrf_token"] + + def use(self, request): + self.last_access = time.time() + request.data = self._data.copy() + request.template_data.update(self._template_data) + request.prepath = self._prepath[:] + request.postpath = self._postpath[:] + + +class LiberviaPage(web_resource.Resource): + isLeaf = True # we handle subpages ourself + named_pages = {} + uri_callbacks = {} + signals_handlers = {} + pages_redirects = {} + cache = {} + cached_urls = {} + # Set of tuples (service/node/sub_id) of nodes subscribed for caching + # sub_id can be empty string if not handled by service + cache_pubsub_sub = set() + main_menu = None + + def __init__( + self, + host, + root_dir, + url, + name=None, + redirect=None, + access=None, + dynamic=False, + parse_url=None, + prepare_render=None, + render=None, + template=None, + on_data_post=None, + on_data=None, + on_signal=None, + url_cache=False, + ): + """initiate LiberviaPages + + LiberviaPages are the main resources of Libervia, using easy to set python files + The arguments are the variables found in page_meta.py + @param host(Libervia): the running instance of Libervia + @param root_dir(unicode): aboslute file path of the page + @param url(unicode): relative URL to the page + this URL may not be valid, as pages may require path arguments + @param name(unicode, None): if not None, a unique name to identify the page + can then be used for e.g. redirection + "/" is not allowed in names (as it can be used to construct URL paths) + @param redirect(unicode, None): if not None, this page will be redirected. A redirected + parameter is used as in self.pageRedirect. parse_url will not be skipped + using this redirect parameter is called "full redirection" + using self.pageRedirect is called "partial redirection" (because some rendering method + can still be used, e.g. parse_url) + @param access(unicode, None): permission needed to access the page + None means public access. + Pages inherit from parent pages: e.g. if a "settings" page is restricted to admins, + and if "settings/blog" is public, it still can only be accessed by admins. + see C.PAGES_ACCESS_* for details + @param dynamic(bool): if True, activate websocket for bidirectional communication + @param parse_url(callable, None): if set it will be called to handle the URL path + after this method, the page will be rendered if noting is left in path (request.postpath) + else a the request will be transmitted to a subpage + @param prepare_render(callable, None): if set, will be used to prepare the rendering + that often means gathering data using the bridge + @param render(callable, None): if not template is set, this method will be called and + what it returns will be rendered. + This method is mutually exclusive with template and must return a unicode string. + @param template(unicode, None): path to the template to render. + This method is mutually exclusive with render + @param on_data_post(callable, None): method to call when data is posted + None if not post is handled + on_data_post can return a string with following value: + - C.POST_NO_CONFIRM: confirm flag will not be set + @param on_data(callable, None): method to call when dynamic data is sent + this method is used with Libervia's websocket mechanism + @param on_signal(callable, None): method to call when a registered signal is received + this method is used with Libervia's websocket mechanism + """ + + web_resource.Resource.__init__(self) + self.host = host + self.root_dir = root_dir + self.url = url + self.name = name + if name is not None: + if name in self.named_pages: + raise exceptions.ConflictError( + _(u'a Libervia page named "{}" already exists'.format(name)) + ) + if u"/" in name: + raise ValueError(_(u'"/" is not allowed in page names')) + if not name: + raise ValueError(_(u"a page name can't be empty")) + self.named_pages[name] = self + if access is None: + access = C.PAGES_ACCESS_PUBLIC + if access not in ( + C.PAGES_ACCESS_PUBLIC, + C.PAGES_ACCESS_PROFILE, + C.PAGES_ACCESS_NONE, + ): + raise NotImplementedError( + _(u"{} access is not implemented yet").format(access) + ) + self.access = access + self.dynamic = dynamic + if redirect is not None: + # only page access and name make sense in case of full redirection + # so we check that rendering methods/values are not set + if not all( + lambda x: x is not None + for x in (parse_url, prepare_render, render, template) + ): + raise ValueError( + _( + u"you can't use full page redirection with other rendering method," + u"check self.pageRedirect if you need to use them" + ) + ) + self.redirect = redirect + else: + self.redirect = None + self.parse_url = parse_url + self.prepare_render = prepare_render + self.template = template + self.render_method = render + self.on_data_post = on_data_post + self.on_data = on_data + self.on_signal = on_signal + self.url_cache = url_cache + if access == C.PAGES_ACCESS_NONE: + # none pages just return a 404, no further check is needed + return + if template is not None and render is not None: + log.error(_(u"render and template methods can't be used at the same time")) + if parse_url is not None and not callable(parse_url): + log.error(_(u"parse_url must be a callable")) + + # if not None, next rendering will be cached + # it must then contain a list of the the keys to use (without the page instance) + # e.g. [C.SERVICE_PROFILE, "pubsub", server@example.tld, pubsub_node] + self._do_cache = None + + def __unicode__(self): + return u"LiberviaPage {name} at {url}".format( + name=self.name or u"<anonymous>", url=self.url + ) + + def __str__(self): + return self.__unicode__().encode("utf-8") + + @classmethod + def importPages(cls, host, parent=None, path=None): + """Recursively import Libervia pages""" + if path is None: + path = [] + if parent is None: + root_dir = os.path.join(os.path.dirname(libervia.__file__), C.PAGES_DIR) + parent = host + else: + root_dir = parent.root_dir + for d in os.listdir(root_dir): + dir_path = os.path.join(root_dir, d) + if not os.path.isdir(dir_path): + continue + meta_path = os.path.join(dir_path, C.PAGES_META_FILE) + if os.path.isfile(meta_path): + page_data = {} + new_path = path + [d] + # we don't want to force the presence of __init__.py + # so we use execfile instead of import. + # TODO: when moved to Python 3, __init__.py is not mandatory anymore + # so we can switch to import + execfile(meta_path, page_data) + resource = LiberviaPage( + host, + dir_path, + u"/" + u"/".join(new_path), + name=page_data.get("name"), + redirect=page_data.get("redirect"), + access=page_data.get("access"), + dynamic=page_data.get("dynamic", False), + parse_url=page_data.get("parse_url"), + prepare_render=page_data.get("prepare_render"), + render=page_data.get("render"), + template=page_data.get("template"), + on_data_post=page_data.get("on_data_post"), + on_data=page_data.get("on_data"), + on_signal=page_data.get("on_signal"), + url_cache=page_data.get("url_cache", False), + ) + parent.putChild(d, resource) + log.info(u"Added /{path} page".format(path=u"[...]/".join(new_path))) + if "uri_handlers" in page_data: + if not isinstance(page_data, dict): + log.error(_(u"uri_handlers must be a dict")) + else: + for uri_tuple, cb_name in page_data["uri_handlers"].iteritems(): + if len(uri_tuple) != 2 or not isinstance(cb_name, basestring): + log.error(_(u"invalid uri_tuple")) + continue + log.info(_(u"setting {}/{} URIs handler").format(*uri_tuple)) + try: + cb = page_data[cb_name] + except KeyError: + log.error( + _(u"missing {name} method to handle {1}/{2}").format( + name=cb_name, *uri_tuple + ) + ) + continue + else: + resource.registerURI(uri_tuple, cb) + + LiberviaPage.importPages(host, resource, new_path) + + @classmethod + def setMenu(cls, menus): + main_menu = [] + for menu in menus: + if not menu: + msg = _(u"menu item can't be empty") + log.error(msg) + raise ValueError(msg) + elif isinstance(menu, list): + if len(menu) != 2: + msg = _( + u"menu item as list must be in the form [page_name, absolue URL]" + ) + log.error(msg) + raise ValueError(msg) + page_name, url = menu + else: + page_name = menu + try: + url = cls.getPageByName(page_name).url + except KeyError as e: + log.error( + _( + u"Can'find a named page ({msg}), please check menu_json in configuration." + ).format(msg=e) + ) + raise e + main_menu.append((page_name, url)) + cls.main_menu = main_menu + + def registerURI(self, uri_tuple, get_uri_cb): + """register a URI handler + + @param uri_tuple(tuple[unicode, unicode]): type or URIs handler + type/subtype as returned by tools/common/parseXMPPUri + or type/None to handle all subtypes + @param get_uri_cb(callable): method which take uri_data dict as only argument + and return absolute path with correct arguments or None if the page + can't handle this URL + """ + if uri_tuple in self.uri_callbacks: + log.info( + _(u"{}/{} URIs are already handled, replacing by the new handler").format( + *uri_tuple + ) + ) + self.uri_callbacks[uri_tuple] = (self, get_uri_cb) + + def registerSignal(self, request, signal, check_profile=True): + r"""register a signal handler + + the page must be dynamic + when signal is received, self.on_signal will be called with: + - request + - signal name + - signal arguments + signal handler will be removed when connection with dynamic page will be lost + @param signal(unicode): name of the signal + last arg of signal must be profile, as it will be checked to filter signals + @param check_profile(bool): if True, signal profile (which MUST be last arg) will be + checked against session profile. + /!\ if False, profile will not be checked/filtered, be sure to know what you are doing + if you unset this option /!\ + """ + # FIXME: add a timeout, if socket is not opened before it, signal handler must be removed + if not self.dynamic: + log.error(_(u"You can't register signal if page is not dynamic")) + return + LiberviaPage.signals_handlers.setdefault(signal, {})[id(request)] = ( + self, + request, + check_profile, + ) + request._signals_registered.append(signal) + + @classmethod + def getPagePathFromURI(cls, uri): + """Retrieve page URL from xmpp: URI + + @param uri(unicode): URI with a xmpp: scheme + @return (unicode,None): absolute path (starting from root "/") to page handling the URI + None is returned if no page has been registered for this URI + """ + uri_data = common_uri.parseXMPPUri(uri) + try: + page, cb = cls.uri_callbacks[uri_data["type"], uri_data["sub_type"]] + except KeyError: + url = None + else: + url = cb(page, uri_data) + if url is None: + # no handler found + # we try to find a more generic one + try: + page, cb = cls.uri_callbacks[uri_data["type"], None] + except KeyError: + pass + else: + url = cb(page, uri_data) + return url + + @classmethod + def getPageByName(cls, name): + """retrieve page instance from its name + + @param name(unicode): name of the page + @return (LiberviaPage): page instance + @raise KeyError: the page doesn't exist + """ + return cls.named_pages[name] + + def getPageRedirectURL(self, request, page_name=u"login", url=None): + """generate URL for a page with redirect_url parameter set + + mainly used for login page with redirection to current page + @param request(server.Request): current HTTP request + @param page_name(unicode): name of the page to go + @param url(None, unicode): url to redirect to + None to use request path (i.e. current page) + @return (unicode): URL to use + """ + return u"{root_url}?redirect_url={redirect_url}".format( + root_url=self.getPageByName(page_name).url, + redirect_url=urllib.quote_plus(request.uri) + if url is None + else url.encode("utf-8"), + ) + + def getURL(self, *args): + """retrieve URL of the page set arguments + + *args(list[unicode]): argument to add to the URL as path elements + empty or None arguments will be ignored + """ + url_args = [quote(a) for a in args if a] + + if self.name is not None and self.name in self.pages_redirects: + # we check for redirection + redirect_data = self.pages_redirects[self.name] + args_hash = tuple(args) + for limit in xrange(len(args) + 1): + current_hash = args_hash[:limit] + if current_hash in redirect_data: + url_base = redirect_data[current_hash] + remaining = args[limit:] + remaining_url = "/".join(remaining) + return os.path.join("/", url_base, remaining_url) + + return os.path.join(self.url, *url_args) + + def getCurrentURL(self, request): + """retrieve URL used to access this page + + @return(unicode): current URL + """ + # we get url in the following way (splitting request.path instead of using + # request.prepath) because request.prepath may have been modified by + # redirection (if redirection args have been specified), while path reflect + # the real request + + # we ignore empty path elements (i.e. double '/' or '/' at the end) + path_elts = [p for p in request.path.split("/") if p] + + if request.postpath: + if not request.postpath[-1]: + # we remove trailing slash + request.postpath = request.postpath[:-1] + if request.postpath: + # getSubPageURL must return subpage from the point where + # the it is called, so we have to remove remanining + # path elements + path_elts = path_elts[: -len(request.postpath)] + + return u"/" + "/".join(path_elts).decode("utf-8") + + def getParamURL(self, request, **kwargs): + """use URL of current request but modify the parameters in query part + + **kwargs(dict[str, unicode]): argument to use as query parameters + @return (unicode): constructed URL + """ + current_url = self.getCurrentURL(request) + if kwargs: + encoded = urllib.urlencode( + {k: v.encode("utf-8") for k, v in kwargs.iteritems()} + ).decode("utf-8") + current_url = current_url + u"?" + encoded + return current_url + + def getSubPageByName(self, subpage_name, parent=None): + """retrieve a subpage and its path using its name + + @param subpage_name(unicode): name of the sub page + it must be a direct children of parent page + @param parent(LiberviaPage, None): parent page + None to use current page + @return (tuple[str, LiberviaPage]): page subpath and instance + @raise exceptions.NotFound: no page has been found + """ + if parent is None: + parent = self + for path, child in parent.children.iteritems(): + try: + child_name = child.name + except AttributeError: + # LiberviaPages have a name, but maybe this is an other Resource + continue + if child_name == subpage_name: + return path, child + raise exceptions.NotFound(_(u"requested sub page has not been found")) + + def getSubPageURL(self, request, page_name, *args): + """retrieve a page in direct children and build its URL according to request + + request's current path is used as base (at current parsing point, + i.e. it's more prepath than path). + Requested page is checked in children and an absolute URL is then built + by the resulting combination. + This method is useful to construct absolute URLs for children instead of + using relative path, which may not work in subpages, and are linked to the + names of directories (i.e. relative URL will break if subdirectory is renamed + while getSubPageURL won't as long as page_name is consistent). + Also, request.path is used, keeping real path used by user, + and potential redirections. + @param request(server.Request): current HTTP request + @param page_name(unicode): name of the page to retrieve + it must be a direct children of current page + @param *args(list[unicode]): arguments to add as path elements + if an arg is None, it will be ignored + @return (unicode): absolute URL to the sub page + """ + current_url = self.getCurrentURL(request) + path, child = self.getSubPageByName(page_name) + return os.path.join( + u"/", current_url, path, *[quote(a) for a in args if a is not None] + ) + + def getURLByNames(self, named_path): + """retrieve URL from pages names and arguments + + @param named_path(list[tuple[unicode, list[unicode]]]): path to the page as a list + of tuples of 2 items: + - first item is page name + - second item is list of path arguments of this page + @return (unicode): URL to the requested page with given path arguments + @raise exceptions.NotFound: one of the page was not found + """ + current_page = None + path = [] + for page_name, page_args in named_path: + if current_page is None: + current_page = self.getPageByName(page_name) + path.append(current_page.getURL(*page_args)) + else: + sub_path, current_page = self.getSubPageByName( + page_name, parent=current_page + ) + path.append(sub_path) + if page_args: + path.extend([quote(a) for a in page_args]) + return self.host.checkRedirection(u"/".join(path)) + + def getURLByPath(self, *args): + """generate URL by path + + this method as a similar effect as getURLByNames, but it is more readable + by using SubPage to get pages instead of using tuples + @param *args: path element: + - if unicode, will be used as argument + - if util.SubPage instance, must be the name of a subpage + @return (unicode): generated path + """ + args = list(args) + if not args: + raise ValueError("You must specify path elements") + # root page is the one needed to construct the base of the URL + # if first arg is not a SubPage instance, we use current page + if not isinstance(args[0], SubPage): + root = self + else: + root = self.getPageByName(args.pop(0)) + # we keep track of current page to check subpage + current_page = root + url_elts = [] + arguments = [] + while True: + while args and not isinstance(args[0], SubPage): + arguments.append(quote(args.pop(0))) + if not url_elts: + url_elts.append(root.getURL(*arguments)) + else: + url_elts.extend(arguments) + if not args: + break + else: + path, current_page = current_page.getSubPageByName(args.pop(0)) + arguments = [path] + return self.host.checkRedirection(u"/".join(url_elts)) + + def getChildWithDefault(self, path, request): + # we handle children ourselves + raise exceptions.InternalError( + u"this method should not be used with LiberviaPage" + ) + + def nextPath(self, request): + """get next URL path segment, and update request accordingly + + will move first segment of postpath in prepath + @param request(server.Request): current HTTP request + @return (unicode): unquoted segment + @raise IndexError: there is no segment left + """ + pathElement = request.postpath.pop(0) + request.prepath.append(pathElement) + return urllib.unquote(pathElement).decode("utf-8") + + def _filterPathValue(self, value, handler, name, request): + """Modify a path value according to handler (see [getPathArgs])""" + if handler in (u"@", u"@jid") and value == u"@": + value = None + + if handler in (u"", u"@"): + if value is None: + return u"" + elif handler in (u"jid", u"@jid"): + if value: + try: + return jid.JID(value) + except RuntimeError: + log.warning(_(u"invalid jid argument: {value}").format(value=value)) + self.pageError(request, C.HTTP_BAD_REQUEST) + else: + return u"" + else: + return handler(self, value, name, request) + + return value + + def getPathArgs(self, request, names, min_args=0, **kwargs): + """get several path arguments at once + + Arguments will be put in request data. + Missing arguments will have None value + @param names(list[unicode]): list of arguments to get + @param min_args(int): if less than min_args are found, PageError is used with C.HTTP_BAD_REQUEST + use 0 to ignore + @param **kwargs: special value or optional callback to use for arguments + names of the arguments must correspond to those in names + special values may be: + - '': use empty string instead of None when no value is specified + - '@': if value of argument is empty or '@', empty string will be used + - 'jid': value must be converted to jid.JID if it exists, else empty string is used + - '@jid': if value of arguments is empty or '@', empty string will be used, else it will be converted to jid + """ + data = self.getRData(request) + + for idx, name in enumerate(names): + if name[0] == u"*": + value = data[name[1:]] = [] + while True: + try: + value.append(self.nextPath(request)) + except IndexError: + idx -= 1 + break + else: + idx += 1 + else: + try: + value = data[name] = self.nextPath(request) + except IndexError: + data[name] = None + idx -= 1 + break + + values_count = idx + 1 + if values_count < min_args: + log.warning( + _( + u"Missing arguments in URL (got {count}, expected at least {min_args})" + ).format(count=values_count, min_args=min_args) + ) + self.pageError(request, C.HTTP_BAD_REQUEST) + + for name in names[values_count:]: + data[name] = None + + for name, handler in kwargs.iteritems(): + if name[0] == "*": + data[name] = [ + self._filterPathValue(v, handler, name, request) for v in data[name] + ] + else: + data[name] = self._filterPathValue(data[name], handler, name, request) + + ## Cache handling ## + + def _setCacheHeaders(self, request, cache): + """Set ETag and Last-Modified HTTP headers, used for caching""" + request.setHeader("ETag", cache.hash) + last_modified = self.host.getHTTPDate(cache.created) + request.setHeader("Last-Modified", last_modified) + + def _checkCacheHeaders(self, request, cache): + """Check if a cache condition is set on the request + + if condition is valid, C.HTTP_NOT_MODIFIED is returned + """ + etag_match = request.getHeader("If-None-Match") + if etag_match is not None: + if cache.hash == etag_match: + self.pageError(request, C.HTTP_NOT_MODIFIED, no_body=True) + else: + modified_match = request.getHeader("If-Modified-Since") + if modified_match is not None: + modified = date_utils.date_parse(modified_match) + if modified >= int(cache.created): + self.pageError(request, C.HTTP_NOT_MODIFIED, no_body=True) + + def checkCacheSubscribeCb(self, sub_id, service, node): + self.cache_pubsub_sub.add((service, node, sub_id)) + + def checkCacheSubscribeEb(self, failure_, service, node): + log.warning(_(u"Can't subscribe to node: {msg}").format(msg=failure_)) + # FIXME: cache must be marked as unusable here + + def psNodeWatchAddEb(self, failure_, service, node): + log.warning(_(u"Can't add node watched: {msg}").format(msg=failure_)) + + def checkCache(self, request, cache_type, **kwargs): + """check if a page is in cache and return cached version if suitable + + this method may perform extra operation to handle cache (e.g. subscribing to a + pubsub node) + @param request(server.Request): current HTTP request + @param cache_type(int): on of C.CACHE_* const. + @param **kwargs: args according to cache_type: + C.CACHE_PUBSUB: + service: pubsub service + node: pubsub node + short: short name of feature (needed if node is empty to find namespace) + + """ + if request.postpath: + # we are not on the final page, no need to go further + return + + profile = self.getProfile(request) or C.SERVICE_PROFILE + + if cache_type == C.CACHE_PUBSUB: + service, node = kwargs["service"], kwargs["node"] + if not node: + try: + short = kwargs["short"] + node = self.host.ns_map[short] + except KeyError: + log.warning( + _( + u'Can\'t use cache for empty node without namespace set, please ensure to set "short" and that it is registered' + ) + ) + return + if profile != C.SERVICE_PROFILE: + # only service profile is cache for now + return + try: + cache = self.cache[profile][cache_type][service][node][request.uri][self] + except KeyError: + # no cache yet, let's subscribe to the pubsub node + d1 = self.host.bridgeCall( + "psSubscribe", service.full(), node, {}, profile + ) + d1.addCallback(self.checkCacheSubscribeCb, service, node) + d1.addErrback(self.checkCacheSubscribeEb, service, node) + d2 = self.host.bridgeCall("psNodeWatchAdd", service.full(), node, profile) + d2.addErrback(self.psNodeWatchAddEb, service, node) + self._do_cache = [self, profile, cache_type, service, node, request.uri] + # we don't return the Deferreds as it is not needed to wait for + # the subscription to continue with page rendering + return + + else: + raise exceptions.InternalError(u"Unknown cache_type") + log.debug(u"using cache for {page}".format(page=self)) + cache.last_access = time.time() + self._setCacheHeaders(request, cache) + self._checkCacheHeaders(request, cache) + request.write(cache.rendered) + request.finish() + raise failure.Failure(exceptions.CancelError(u"cache is used")) + + def _cacheURL(self, dummy, request, profile): + self.cached_urls.setdefault(profile, {})[request.uri] = CacheURL(request) + + @classmethod + def onNodeEvent(cls, host, service, node, event_type, items, profile): + """Invalidate cache for all pages linked to this node""" + try: + cache = cls.cache[profile][C.CACHE_PUBSUB][jid.JID(service)][node] + except KeyError: + log.info( + _( + u"Removing subscription for {service}/{node}: " + u"the page is not cached" + ).format(service=service, node=node) + ) + d1 = host.bridgeCall("psUnsubscribe", service, node, profile) + d1.addErrback( + lambda failure_: log.warning( + _(u"Can't unsubscribe from {service}/{node}: {msg}").format( + service=service, node=node, msg=failure_ + ) + ) + ) + d2 = host.bridgeCall("psNodeWatchAdd", service, node, profile) + # TODO: check why the page is not in cache, remove subscription? + d2.addErrback( + lambda failure_: log.warning( + _(u"Can't remove watch for {service}/{node}: {msg}").format( + service=service, node=node, msg=failure_ + ) + ) + ) + else: + cache.clear() + + @classmethod + def onSignal(cls, host, signal, *args): + """Generic method which receive registered signals + + if a callback is registered for this signal, call it + @param host: Libervia instance + @param signal(unicode): name of the signal + @param *args: args of the signals + """ + for page, request, check_profile in cls.signals_handlers.get( + signal, {} + ).itervalues(): + if check_profile: + signal_profile = args[-1] + request_profile = page.getProfile(request) + if not request_profile: + # if you want to use signal without session, unset check_profile + # (be sure to know what you are doing) + log.error(_(u"no session started, signal can't be checked")) + continue + if signal_profile != request_profile: + # we ignore the signal, it's not for our profile + continue + if request._signals_cache is not None: + # socket is not yet opened, we cache the signal + request._signals_cache.append((request, signal, args)) + log.debug( + u"signal [{signal}] cached: {args}".format(signal=signal, args=args) + ) + else: + page.on_signal(page, request, signal, *args) + + def onSocketOpen(self, request): + """Called for dynamic pages when socket has just been opened + + we send all cached signals + """ + assert request._signals_cache is not None + cache = request._signals_cache + request._signals_cache = None + for request, signal, args in cache: + self.on_signal(self, request, signal, *args) + + def onSocketClose(self, request): + """Called for dynamic pages when socket has just been closed + + we remove signal handler + """ + for signal in request._signals_registered: + try: + del LiberviaPage.signals_handlers[signal][id(request)] + except KeyError: + log.error( + _( + u"Can't find signal handler for [{signal}], this should not happen" + ).format(signal=signal) + ) + else: + log.debug(_(u"Removed signal handler")) + + def delegateToResource(self, request, resource): + """continue workflow with Twisted Resource""" + buf = resource.render(request) + if buf == server.NOT_DONE_YET: + pass + else: + request.write(buf) + request.finish() + raise failure.Failure(exceptions.CancelError(u"resource delegation")) + + def HTTPRedirect(self, request, url): + """redirect to an URL using HTTP redirection + + @param request(server.Request): current HTTP request + @param url(unicode): url to redirect to + """ + web_util.redirectTo(url.encode("utf-8"), request) + request.finish() + raise failure.Failure(exceptions.CancelError(u"HTTP redirection is used")) + + def redirectOrContinue(self, request, redirect_arg=u"redirect_url"): + """helper method to redirect a page to an url given as arg + + if the arg is not present, the page will continue normal workflow + @param request(server.Request): current HTTP request + @param redirect_arg(unicode): argument to use to get redirection URL + @interrupt: redirect the page to requested URL + @interrupt pageError(C.HTTP_BAD_REQUEST): empty or non local URL is used + """ + try: + url = request.args["redirect_url"][0] + except (KeyError, IndexError): + pass + else: + # a redirection is requested + if not url or url[0] != u"/": + # we only want local urls + self.pageError(request, C.HTTP_BAD_REQUEST) + else: + self.HTTPRedirect(request, url) + + def pageRedirect(self, page_path, request, skip_parse_url=True, path_args=None): + """redirect a page to a named page + + the workflow will continue with the workflow of the named page, + skipping named page's parse_url method if it exist. + If you want to do a HTTP redirection, use HTTPRedirect + @param page_path(unicode): path to page (elements are separated by "/"): + if path starts with a "/": + path is a full path starting from root + else: + - first element is name as registered in name variable + - following element are subpages path + e.g.: "blog" redirect to page named "blog" + "blog/atom.xml" redirect to atom.xml subpage of "blog" + "/common/blog/atom.xml" redirect to the page at the fiven full path + @param request(server.Request): current HTTP request + @param skip_parse_url(bool): if True, parse_url method on redirect page will be skipped + @param path_args(list[unicode], None): path arguments to use in redirected page + @raise KeyError: there is no known page with this name + """ + # FIXME: render non LiberviaPage resources + path = page_path.rstrip(u"/").split(u"/") + if not path[0]: + redirect_page = self.host.root + else: + redirect_page = self.named_pages[path[0]] + + for subpage in path[1:]: + if redirect_page is self.host.root: + redirect_page = redirect_page.children[subpage] + else: + redirect_page = redirect_page.original.children[subpage] + + if path_args is not None: + args = [quote(a) for a in path_args] + request.postpath = args + request.postpath + + if self._do_cache: + # if cache is needed, it will be handled by final page + redirect_page._do_cache = self._do_cache + self._do_cache = None + + redirect_page.renderPage(request, skip_parse_url=skip_parse_url) + raise failure.Failure(exceptions.CancelError(u"page redirection is used")) + + def pageError(self, request, code=C.HTTP_NOT_FOUND, no_body=False): + """generate an error page and terminate the request + + @param request(server.Request): HTTP request + @param core(int): error code to use + @param no_body: don't write body if True + """ + request.setResponseCode(code) + if no_body: + request.finish() + else: + template = u"error/" + unicode(code) + ".html" + + rendered = self.host.renderer.render( + template, + root_path="/templates/", + error_code=code, + **request.template_data + ) + + self.writeData(rendered, request) + raise failure.Failure(exceptions.CancelError(u"error page is used")) + + def writeData(self, data, request): + """write data to transport and finish the request""" + if data is None: + self.pageError(request) + data_encoded = data.encode("utf-8") + + if self._do_cache is not None: + redirected_page = self._do_cache.pop(0) + cache = reduce(lambda d, k: d.setdefault(k, {}), self._do_cache, self.cache) + page_cache = cache[redirected_page] = CachePage(data_encoded) + self._setCacheHeaders(request, page_cache) + log.debug( + _(u"{page} put in cache for [{profile}]").format( + page=self, profile=self._do_cache[0] + ) + ) + self._do_cache = None + self._checkCacheHeaders(request, page_cache) + + request.write(data_encoded) + request.finish() + + def _subpagesHandler(self, dummy, request): + """render subpage if suitable + + this method checks if there is still an unmanaged part of the path + and check if it corresponds to a subpage. If so, it render the subpage + else it render a NoResource. + If there is no unmanaged part of the segment, current page workflow is pursued + """ + if request.postpath: + subpage = self.nextPath(request) + try: + child = self.children[subpage] + except KeyError: + self.pageError(request) + else: + child.render(request) + raise failure.Failure(exceptions.CancelError(u"subpage page is used")) + + def _prepare_dynamic(self, dummy, request): + # we need to activate dynamic page + # we set data for template, and create/register token + socket_token = unicode(uuid.uuid4()) + socket_url = self.host.getWebsocketURL(request) + socket_debug = C.boolConst(self.host.debug) + request.template_data["websocket"] = WebsocketMeta( + socket_url, socket_token, socket_debug + ) + self.host.registerWSToken(socket_token, self, request) + # we will keep track of handlers to remove + request._signals_registered = [] + # we will cache registered signals until socket is opened + request._signals_cache = [] + + def _prepare_render(self, dummy, request): + return defer.maybeDeferred(self.prepare_render, self, request) + + def _render_method(self, dummy, request): + return defer.maybeDeferred(self.render_method, self, request) + + def _render_template(self, dummy, request): + template_data = request.template_data + + # if confirm variable is set in case of successfuly data post + session_data = self.host.getSessionData(request, session_iface.ISATSession) + if session_data.popPageFlag(self, C.FLAG_CONFIRM): + template_data[u"confirm"] = True + + return self.host.renderer.render( + self.template, + root_path="/templates/", + media_path="/" + C.MEDIA_DIR, + cache_path=session_data.cache_dir, + main_menu=LiberviaPage.main_menu, + **template_data + ) + + def _renderEb(self, failure_, request): + """don't raise error on CancelError""" + failure_.trap(exceptions.CancelError) + + def _internalError(self, failure_, request): + """called if an error is not catched""" + log.error( + _(u"Uncatched error for HTTP request on {url}: {msg}").format( + url=request.URLPath(), msg=failure_ + ) + ) + self.pageError(request, C.HTTP_INTERNAL_ERROR) + + def _on_data_post_redirect(self, ret, request): + """called when page's on_data_post has been done successfuly + + This will do a Post/Redirect/Get pattern. + this method redirect to the same page or to request.data['post_redirect_page'] + post_redirect_page can be either a page or a tuple with page as first item, then a list of unicode arguments to append to the url. + if post_redirect_page is not used, initial request.uri (i.e. the same page as where the data have been posted) will be used for redirection. + HTTP status code "See Other" (303) is used as it is the recommanded code in this case. + @param ret(None, unicode, iterable): on_data_post return value + see LiberviaPage.__init__ on_data_post docstring + """ + if ret is None: + ret = () + elif isinstance(ret, basestring): + ret = (ret,) + else: + ret = tuple(ret) + raise NotImplementedError( + _(u"iterable in on_data_post return value is not used yet") + ) + session_data = self.host.getSessionData(request, session_iface.ISATSession) + request_data = self.getRData(request) + if "post_redirect_page" in request_data: + redirect_page_data = request_data["post_redirect_page"] + if isinstance(redirect_page_data, tuple): + redirect_page = redirect_page_data[0] + redirect_page_args = redirect_page_data[1:] + redirect_uri = redirect_page.getURL(*redirect_page_args) + else: + redirect_page = redirect_page_data + redirect_uri = redirect_page.url + else: + redirect_page = self + redirect_uri = request.uri + + if not C.POST_NO_CONFIRM in ret: + session_data.setPageFlag(redirect_page, C.FLAG_CONFIRM) + request.setResponseCode(C.HTTP_SEE_OTHER) + request.setHeader("location", redirect_uri) + request.finish() + raise failure.Failure(exceptions.CancelError(u"Post/Redirect/Get is used")) + + def _on_data_post(self, dummy, request): + csrf_token = self.host.getSessionData( + request, session_iface.ISATSession + ).csrf_token + try: + given_csrf = self.getPostedData(request, u"csrf_token") + except KeyError: + given_csrf = None + if given_csrf is None or given_csrf != csrf_token: + log.warning( + _(u"invalid CSRF token, hack attempt? URL: {url}, IP: {ip}").format( + url=request.uri, ip=request.getClientIP() + ) + ) + self.pageError(request, C.HTTP_UNAUTHORIZED) + d = defer.maybeDeferred(self.on_data_post, self, request) + d.addCallback(self._on_data_post_redirect, request) + return d + + def getPostedData(self, request, keys, multiple=False): + """get data from a POST request or from URL's query part and decode it + + @param request(server.Request): request linked to the session + @param keys(unicode, iterable[unicode]): name of the value(s) to get + unicode to get one value + iterable to get more than one + @param multiple(bool): True if multiple values are possible/expected + if False, the first value is returned + @return (iterator[unicode], list[iterator[unicode], unicode, list[unicode]): values received for this(these) key(s) + @raise KeyError: one specific key has been requested, and it is missing + """ + # FIXME: request.args is already unquoting the value, it seems we are doing double unquote + if isinstance(keys, basestring): + keys = [keys] + get_first = True + else: + get_first = False + + ret = [] + for key in keys: + gen = (urllib.unquote(v).decode("utf-8") for v in request.args.get(key, [])) + if multiple: + ret.append(gen) + else: + try: + ret.append(next(gen)) + except StopIteration: + raise KeyError(key) + + return ret[0] if get_first else ret + + def getAllPostedData(self, request, except_=(), multiple=True): + """get all posted data + + @param request(server.Request): request linked to the session + @param except_(iterable[unicode]): key of values to ignore + csrf_token will always be ignored + @param multiple(bool): if False, only the first values are returned + @return (dict[unicode, list[unicode]]): post values + """ + except_ = tuple(except_) + (u"csrf_token",) + ret = {} + for key, values in request.args.iteritems(): + key = urllib.unquote(key).decode("utf-8") + if key in except_: + continue + if not multiple: + ret[key] = urllib.unquote(values[0]).decode("utf-8") + else: + ret[key] = [urllib.unquote(v).decode("utf-8") for v in values] + return ret + + def getProfile(self, request): + """helper method to easily get current profile + + @return (unicode, None): current profile + None if no profile session is started + """ + sat_session = self.host.getSessionData(request, session_iface.ISATSession) + return sat_session.profile + + def getRData(self, request): + """helper method to get request data dict + + this dictionnary if for the request only, it is not saved in session + It is mainly used to pass data between pages/methods called during request workflow + @return (dict): request data + """ + try: + return request.data + except AttributeError: + request.data = {} + return request.data + + def _checkAccess(self, data, request): + """Check access according to self.access + + if access is not granted, show a HTTP_UNAUTHORIZED pageError and stop request, + else return data (so it can be inserted in deferred chain + """ + if self.access == C.PAGES_ACCESS_PUBLIC: + pass + elif self.access == C.PAGES_ACCESS_PROFILE: + profile = self.getProfile(request) + if not profile: + # no session started + if not self.host.options["allow_registration"]: + # registration not allowed, access is not granted + self.pageError(request, C.HTTP_UNAUTHORIZED) + else: + # registration allowed, we redirect to login page + login_url = self.getPageRedirectURL(request) + self.HTTPRedirect(request, login_url) + + return data + + def renderPartial(self, request, template, template_data): + """Render a template to be inserted in dynamic page + + this is NOT the normal page rendering method, it is used only to update + dynamic pages + @param template(unicode): path of the template to render + @param template_data(dict): template_data to use + """ + if not self.dynamic: + raise exceptions.InternalError( + _(u"renderPartial must only be used with dynamic pages") + ) + session_data = self.host.getSessionData(request, session_iface.ISATSession) + + return self.host.renderer.render( + template, + root_path="/templates/", + media_path="/" + C.MEDIA_DIR, + cache_path=session_data.cache_dir, + main_menu=LiberviaPage.main_menu, + **template_data + ) + + def renderAndUpdate( + self, request, template, selectors, template_data_update, update_type="append" + ): + """Helper method to render a partial page element and update the page + + this is NOT the normal page rendering method, it is used only to update + dynamic pages + @param request(server.Request): current HTTP request + @param template: same as for [renderPartial] + @param selectors: CSS selectors to use + @param template_data_update: template data to use + template data cached in request will be copied then updated + with this data + @parap update_type(unicode): one of: + append: append rendered element to selected element + """ + template_data = request.template_data.copy() + template_data.update(template_data_update) + html = self.renderPartial(request, template, template_data) + request.sendData(u"dom", selectors=selectors, update_type=update_type, html=html) + + def renderPage(self, request, skip_parse_url=False): + """Main method to handle the workflow of a LiberviaPage""" + + # template_data are the variables passed to template + if not hasattr(request, "template_data"): + session_data = self.host.getSessionData(request, session_iface.ISATSession) + csrf_token = session_data.csrf_token + request.template_data = { + u"profile": session_data.profile, + u"csrf_token": csrf_token, + } + + # XXX: here is the code which need to be executed once + # at the beginning of the request hanling + if request.postpath and not request.postpath[-1]: + # we don't differenciate URLs finishing with '/' or not + del request.postpath[-1] + + d = defer.Deferred() + d.addCallback(self._checkAccess, request) + + if self.redirect is not None: + d.addCallback( + lambda dummy: self.pageRedirect( + self.redirect, request, skip_parse_url=False + ) + ) + + if self.parse_url is not None and not skip_parse_url: + if self.url_cache: + profile = self.getProfile(request) + try: + cache_url = self.cached_urls[profile][request.uri] + except KeyError: + # no cache for this URI yet + # we do normal URL parsing, and then the cache + d.addCallback(self.parse_url, request) + d.addCallback(self._cacheURL, request, profile) + else: + log.debug(_(u"using URI cache for {page}").format(page=self)) + cache_url.use(request) + else: + d.addCallback(self.parse_url, request) + + d.addCallback(self._subpagesHandler, request) + + if request.method not in (C.HTTP_METHOD_GET, C.HTTP_METHOD_POST): + # only HTTP GET and POST are handled so far + d.addCallback(lambda dummy: self.pageError(request, C.HTTP_BAD_REQUEST)) + + if request.method == C.HTTP_METHOD_POST: + if self.on_data_post is None: + # if we don't have on_data_post, the page was not expecting POST + # so we return an error + d.addCallback(lambda dummy: self.pageError(request, C.HTTP_BAD_REQUEST)) + else: + d.addCallback(self._on_data_post, request) + # by default, POST follow normal behaviour after on_data_post is called + # this can be changed by a redirection or other method call in on_data_post + + if self.dynamic: + d.addCallback(self._prepare_dynamic, request) + + if self.prepare_render: + d.addCallback(self._prepare_render, request) + + if self.template: + d.addCallback(self._render_template, request) + elif self.render_method: + d.addCallback(self._render_method, request) + + d.addCallback(self.writeData, request) + d.addErrback(self._renderEb, request) + d.addErrback(self._internalError, request) + d.callback(self) + return server.NOT_DONE_YET + + def render_GET(self, request): + return self.renderPage(request) + + def render_POST(self, request): + return self.renderPage(request)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/server/pages_tools.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,58 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2011-2018 Jérôme Poisson <goffi@goffi.org> + +# This program 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. + +# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + +"""Helper methods for common operations on pages""" + +from sat.core.i18n import _ +from libervia.server.constants import Const as C +from twisted.internet import defer +from sat.core.log import getLogger + +log = getLogger(__name__) +from sat.tools.common import data_objects + + +def commentsDataToObjects(comments_data): + return data_objects.BlogItems(comments_data) + + +def retrieveComments(self, service, node, profile, pass_exceptions=True): + """Retrieve comments from server and convert them to data objects + + @param service(unicode): service holding the comments + @param node(unicode): node to retrieve + @param profile(unicode): profile of the user willing to find comments + @param pass_exceptions(bool): if True bridge exceptions will be ignored but logged + else exception will be raised + """ + try: + d = self.host.bridgeCall(u"mbGet", service, node, C.NO_LIMIT, [], {}, profile) + except Exception as e: + if not pass_exceptions: + raise e + else: + log.warning( + _(u"Can't get comments at {node} (service: {service}): {msg}").format( + service=service, node=node, msg=e + ) + ) + return defer.succeed([]) + + d.addCallback(commentsDataToObjects) + return d
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/server/server.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,2525 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2011-2018 Jérôme Poisson <goffi@goffi.org> + +# This program 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. + +# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + +from twisted.application import service +from twisted.internet import reactor, defer +from twisted.web import server +from twisted.web import static +from twisted.web import resource as web_resource +from twisted.web import util as web_util +from twisted.web import http +from twisted.python.components import registerAdapter +from twisted.python import failure +from twisted.words.protocols.jabber import jid + +from txjsonrpc.web import jsonrpc +from txjsonrpc import jsonrpclib + +from sat.core.log import getLogger + +log = getLogger(__name__) +from sat_frontends.bridge.dbus_bridge import ( + Bridge, + BridgeExceptionNoService, + const_TIMEOUT as BRIDGE_TIMEOUT, +) +from sat.core.i18n import _, D_ +from sat.core import exceptions +from sat.tools import utils +from sat.tools.common import regex +from sat.tools.common import template + +import re +import glob +import os.path +import sys +import tempfile +import shutil +import uuid +import urlparse +import urllib +import time +from httplib import HTTPS_PORT +import libervia +from libervia.server import websockets +from libervia.server.pages import LiberviaPage +from libervia.server.utils import quote, ProgressHandler +from functools import partial + +try: + import OpenSSL + from twisted.internet import ssl +except ImportError: + ssl = None + +from libervia.server.constants import Const as C +from libervia.server.blog import MicroBlog +from libervia.server import session_iface + + +# following value are set from twisted.plugins.libervia_server initialise (see the comment there) +DATA_DIR_DEFAULT = OPT_PARAMETERS_BOTH = OPT_PARAMETERS_CFG = coerceDataDir = None + + +class LiberviaSession(server.Session): + sessionTimeout = C.SESSION_TIMEOUT + + def __init__(self, *args, **kwargs): + self.__lock = False + server.Session.__init__(self, *args, **kwargs) + + def lock(self): + """Prevent session from expiring""" + self.__lock = True + self._expireCall.reset(sys.maxint) + + def unlock(self): + """Allow session to expire again, and touch it""" + self.__lock = False + self.touch() + + def touch(self): + if not self.__lock: + server.Session.touch(self) + + +class ProtectedFile(static.File): + """A static.File class which doens't show directory listing""" + + def directoryListing(self): + return web_resource.NoResource() + + +class LiberviaRootResource(ProtectedFile): + """Specialized resource for Libervia root + + handle redirections declared in sat.conf + """ + + def _initRedirections(self, options): + ## redirections + self.redirections = {} + self.inv_redirections = {} # new URL to old URL map + + if options["url_redirections_dict"] and not options["url_redirections_profile"]: + # FIXME: url_redirections_profile should not be needed. It is currently used to + # redirect to an URL which associate the profile with the service, but this + # is not clean, and service should be explicitly specified + raise ValueError( + u"url_redirections_profile need to be filled if you want to use url_redirections_dict" + ) + + for old, new_data in options["url_redirections_dict"].iteritems(): + # new_data can be a dictionary or a unicode url + if isinstance(new_data, dict): + # new_data dict must contain either "url", "page" or "path" key (exclusive) + # if "path" is used, a file url is constructed with it + if len({"path", "url", "page"}.intersection(new_data.keys())) != 1: + raise ValueError( + u'You must have one and only one of "url", "page" or "path" key in your url_redirections_dict data' + ) + if "url" in new_data: + new = new_data["url"] + elif "page" in new_data: + new = new_data + new["type"] = "page" + new.setdefault("path_args", []) + if not isinstance(new["path_args"], list): + log.error( + _( + u'"path_args" in redirection of {old} must be a list. Ignoring the redirection'.format( + old=old + ) + ) + ) + continue + new.setdefault("query_args", {}) + if not isinstance(new["query_args"], dict): + log.error( + _( + u'"query_args" in redirection of {old} must be a dictionary. Ignoring the redirection'.format( + old=old + ) + ) + ) + continue + new["path_args"] = [quote(a) for a in new["path_args"]] + # we keep an inversed dict of page redirection (page/path_args => redirecting URL) + # so getURL can return the redirecting URL if the same arguments are used + # making the URL consistent + args_hash = tuple(new["path_args"]) + LiberviaPage.pages_redirects.setdefault(new_data["page"], {})[ + args_hash + ] = old + + # we need lists in query_args because it will be used + # as it in request.path_args + for k, v in new["query_args"].iteritems(): + if isinstance(v, basestring): + new["query_args"][k] = [v] + elif "path" in new_data: + new = "file:{}".format(urllib.quote(new_data["path"])) + elif isinstance(new_data, basestring): + new = new_data + new_data = {} + else: + log.error( + _(u"ignoring invalid redirection value: {new_data}").format( + new_data=new_data + ) + ) + continue + + # some normalization + if not old.strip(): + # root URL special case + old = "" + elif not old.startswith("/"): + log.error( + _( + u"redirected url must start with '/', got {value}. Ignoring" + ).format(value=old) + ) + continue + else: + old = self._normalizeURL(old) + + if isinstance(new, dict): + # dict are handled differently, they contain data + # which ared use dynamically when the request is done + self.redirections[old] = new + if not old: + if new[u"type"] == u"page": + log.info( + _(u"Root URL redirected to page {name}").format( + name=new[u"page"] + ) + ) + else: + if new[u"type"] == u"page": + page = LiberviaPage.getPageByName(new[u"page"]) + url = page.getURL(*new.get(u"path_args", [])) + self.inv_redirections[url] = old + continue + + # at this point we have a redirection URL in new, we can parse it + new_url = urlparse.urlsplit(new.encode("utf-8")) + + # we handle the known URL schemes + if new_url.scheme == "xmpp": + location = LiberviaPage.getPagePathFromURI(new) + if location is None: + log.warning( + _( + u"ignoring redirection, no page found to handle this URI: {uri}" + ).format(uri=new) + ) + continue + request_data = self._getRequestData(location) + if old: + self.inv_redirections[location] = old + + elif new_url.scheme in ("", "http", "https"): + # direct redirection + if new_url.netloc: + raise NotImplementedError( + u"netloc ({netloc}) is not implemented yet for url_redirections_dict, it is not possible to redirect to an external website".format( + netloc=new_url.netloc + ) + ) + location = urlparse.urlunsplit( + ("", "", new_url.path, new_url.query, new_url.fragment) + ).decode("utf-8") + request_data = self._getRequestData(location) + if old: + self.inv_redirections[location] = old + + elif new_url.scheme in ("file"): + # file or directory + if new_url.netloc: + raise NotImplementedError( + u"netloc ({netloc}) is not implemented for url redirection to file system, it is not possible to redirect to an external host".format( + netloc=new_url.netloc + ) + ) + path = urllib.unquote(new_url.path) + if not os.path.isabs(path): + raise ValueError( + u"file redirection must have an absolute path: e.g. file:/path/to/my/file" + ) + # for file redirection, we directly put child here + segments, dummy, last_segment = old.rpartition("/") + url_segments = segments.split("/") if segments else [] + current = self + for segment in url_segments: + resource = web_resource.NoResource() + current.putChild(segment, resource) + current = resource + resource_class = ( + ProtectedFile if new_data.get("protected", True) else static.File + ) + current.putChild(last_segment, resource_class(path)) + log.info( + u"Added redirection from /{old} to file system path {path}".format( + old=old.decode("utf-8"), path=path.decode("utf-8") + ) + ) + continue # we don't want to use redirection system, so we continue here + + else: + raise NotImplementedError( + u"{scheme}: scheme is not managed for url_redirections_dict".format( + scheme=new_url.scheme + ) + ) + + self.redirections[old] = request_data + if not old: + log.info( + _(u"Root URL redirected to {uri}").format( + uri=request_data[1].decode("utf-8") + ) + ) + + # no need to keep url_redirections*, they will not be used anymore + del options["url_redirections_dict"] + del options["url_redirections_profile"] + + # the default root URL, if not redirected + if not "" in self.redirections: + self.redirections[""] = self._getRequestData(C.LIBERVIA_MAIN_PAGE) + + def _normalizeURL(self, url, lower=True): + """Return URL normalized for self.redirections dict + + @param url(unicode): URL to normalize + @param lower(bool): lower case of url if True + @return (str): normalized URL + """ + if lower: + url = url.lower() + return "/".join((p for p in url.encode("utf-8").split("/") if p)) + + def _getRequestData(self, uri): + """Return data needed to redirect request + + @param url(unicode): destination url + @return (tuple(list[str], str, str, dict): tuple with + splitted path as in Request.postpath + uri as in Request.uri + path as in Request.path + args as in Request.args + """ + uri = uri.encode("utf-8") + # XXX: we reuse code from twisted.web.http.py here + # as we need to have the same behaviour + x = uri.split(b"?", 1) + + if len(x) == 1: + path = uri + args = {} + else: + path, argstring = x + args = http.parse_qs(argstring, 1) + + # XXX: splitted path case must not be changed, as it may be significant + # (e.g. for blog items) + return ( + self._normalizeURL(path.decode("utf-8"), lower=False).split("/"), + uri, + path, + args, + ) + + def _redirect(self, request, request_data): + """Redirect an URL by rewritting request + + this is *NOT* a HTTP redirection, but equivalent to URL rewritting + @param request(web.http.request): original request + @param request_data(tuple): data returned by self._getRequestData + @return (web_resource.Resource): resource to use + """ + # recursion check + try: + request._redirected + except AttributeError: + pass + else: + try: + dummy, uri, dummy, dummy = request_data + except ValueError: + uri = u"" + log.error( + D_( + u"recursive redirection, please fix this URL:\n{old} ==> {new}" + ).format(old=request.uri.decode("utf-8"), new=uri.decode("utf-8")) + ) + return web_resource.NoResource() + + request._redirected = True # here to avoid recursive redirections + + if isinstance(request_data, dict): + if request_data["type"] == "page": + try: + page = LiberviaPage.getPageByName(request_data["page"]) + except KeyError: + log.error( + _( + u'Can\'t find page named "{name}" requested in redirection' + ).format(name=request_data["page"]) + ) + return web_resource.NoResource() + request.postpath = request_data["path_args"][:] + request.postpath + + try: + request.args.update(request_data["query_args"]) + except (TypeError, ValueError): + log.error( + _(u"Invalid args in redirection: {query_args}").format( + query_args=request_data["query_args"] + ) + ) + return web_resource.NoResource() + return page + else: + raise exceptions.InternalError(u"unknown request_data type") + else: + path_list, uri, path, args = request_data + log.debug( + u"Redirecting URL {old} to {new}".format( + old=request.uri.decode("utf-8"), new=uri.decode("utf-8") + ) + ) + # we change the request to reflect the new url + request.postpath = path_list[1:] + request.postpath + request.args = args + + # we start again to look for a child with the new url + return self.getChildWithDefault(path_list[0], request) + + def getChildWithDefault(self, name, request): + # XXX: this method is overriden only for root url + # which is the only ones who need to be handled before other children + if name == "" and not request.postpath: + return self._redirect(request, self.redirections[""]) + return super(LiberviaRootResource, self).getChildWithDefault(name, request) + + def getChild(self, name, request): + resource = super(LiberviaRootResource, self).getChild(name, request) + + if isinstance(resource, web_resource.NoResource): + # if nothing was found, we try our luck with redirections + # XXX: we want redirections to happen only if everything else failed + path_elt = request.prepath + request.postpath + for idx in xrange(len(path_elt), 0, -1): + test_url = "/".join(path_elt[:idx]).lower() + if test_url in self.redirections: + request_data = self.redirections[test_url] + request.postpath = path_elt[idx:] + return self._redirect(request, request_data) + + return resource + + def createSimilarFile(self, path): + # XXX: this method need to be overriden to avoid recreating a LiberviaRootResource + + f = LiberviaRootResource.__base__( + path, self.defaultType, self.ignoredExts, self.registry + ) + # refactoring by steps, here - constructor should almost certainly take these + f.processors = self.processors + f.indexNames = self.indexNames[:] + f.childNotFound = self.childNotFound + return f + + +class JSONRPCMethodManager(jsonrpc.JSONRPC): + def __init__(self, sat_host): + jsonrpc.JSONRPC.__init__(self) + self.sat_host = sat_host + + def asyncBridgeCall(self, method_name, *args, **kwargs): + return self.sat_host.bridgeCall(method_name, *args, **kwargs) + + +class MethodHandler(JSONRPCMethodManager): + def __init__(self, sat_host): + JSONRPCMethodManager.__init__(self, sat_host) + + def render(self, request): + self.session = request.getSession() + profile = session_iface.ISATSession(self.session).profile + if not profile: + # user is not identified, we return a jsonrpc fault + parsed = jsonrpclib.loads(request.content.read()) + fault = jsonrpclib.Fault( + C.ERRNUM_LIBERVIA, C.NOT_ALLOWED + ) # FIXME: define some standard error codes for libervia + return jsonrpc.JSONRPC._cbRender( + self, fault, request, parsed.get("id"), parsed.get("jsonrpc") + ) # pylint: disable=E1103 + return jsonrpc.JSONRPC.render(self, request) + + @defer.inlineCallbacks + def jsonrpc_getVersion(self): + """Return SàT version""" + try: + defer.returnValue(self._version_cache) + except AttributeError: + self._version_cache = yield self.sat_host.bridgeCall("getVersion") + defer.returnValue(self._version_cache) + + def jsonrpc_getLiberviaVersion(self): + """Return Libervia version""" + return self.sat_host.full_version + + def jsonrpc_disconnect(self): + """Disconnect the profile""" + sat_session = session_iface.ISATSession(self.session) + profile = sat_session.profile + self.sat_host.bridgeCall("disconnect", profile) + + def jsonrpc_getContacts(self): + """Return all passed args.""" + profile = session_iface.ISATSession(self.session).profile + return self.sat_host.bridgeCall("getContacts", profile) + + @defer.inlineCallbacks + def jsonrpc_addContact(self, entity, name, groups): + """Subscribe to contact presence, and add it to the given groups""" + profile = session_iface.ISATSession(self.session).profile + yield self.sat_host.bridgeCall("addContact", entity, profile) + yield self.sat_host.bridgeCall("updateContact", entity, name, groups, profile) + + def jsonrpc_delContact(self, entity): + """Remove contact from contacts list""" + profile = session_iface.ISATSession(self.session).profile + return self.sat_host.bridgeCall("delContact", entity, profile) + + def jsonrpc_updateContact(self, entity, name, groups): + """Update contact's roster item""" + profile = session_iface.ISATSession(self.session).profile + return self.sat_host.bridgeCall("updateContact", entity, name, groups, profile) + + def jsonrpc_subscription(self, sub_type, entity): + """Confirm (or infirm) subscription, + and setup user roster in case of subscription""" + profile = session_iface.ISATSession(self.session).profile + return self.sat_host.bridgeCall("subscription", sub_type, entity, profile) + + def jsonrpc_getWaitingSub(self): + """Return list of room already joined by user""" + profile = session_iface.ISATSession(self.session).profile + return self.sat_host.bridgeCall("getWaitingSub", profile) + + def jsonrpc_setStatus(self, presence, status): + """Change the presence and/or status + @param presence: value from ("", "chat", "away", "dnd", "xa") + @param status: any string to describe your status + """ + profile = session_iface.ISATSession(self.session).profile + return self.sat_host.bridgeCall( + "setPresence", "", presence, {"": status}, profile + ) + + def jsonrpc_messageSend(self, to_jid, msg, subject, type_, extra={}): + """send message""" + profile = session_iface.ISATSession(self.session).profile + return self.asyncBridgeCall( + "messageSend", to_jid, msg, subject, type_, extra, profile + ) + + ## PubSub ## + + def jsonrpc_psNodeDelete(self, service, node): + """Delete a whole node + + @param service (unicode): service jid + @param node (unicode): node to delete + """ + profile = session_iface.ISATSession(self.session).profile + return self.asyncBridgeCall("psNodeDelete", service, node, profile) + + # def jsonrpc_psRetractItem(self, service, node, item, notify): + # """Delete a whole node + + # @param service (unicode): service jid + # @param node (unicode): node to delete + # @param items (iterable): id of item to retract + # @param notify (bool): True if notification is required + # """ + # profile = session_iface.ISATSession(self.session).profile + # return self.asyncBridgeCall("psRetractItem", service, node, item, notify, profile) + + # def jsonrpc_psRetractItems(self, service, node, items, notify): + # """Delete a whole node + + # @param service (unicode): service jid + # @param node (unicode): node to delete + # @param items (iterable): ids of items to retract + # @param notify (bool): True if notification is required + # """ + # profile = session_iface.ISATSession(self.session).profile + # return self.asyncBridgeCall("psRetractItems", service, node, items, notify, profile) + + ## microblogging ## + + def jsonrpc_mbSend(self, service, node, mb_data): + """Send microblog data + + @param service (unicode): service jid or empty string to use profile's microblog + @param node (unicode): publishing node, or empty string to use microblog node + @param mb_data(dict): microblog data + @return: a deferred + """ + profile = session_iface.ISATSession(self.session).profile + return self.asyncBridgeCall("mbSend", service, node, mb_data, profile) + + def jsonrpc_mbRetract(self, service, node, items): + """Delete a whole node + + @param service (unicode): service jid, empty string for PEP + @param node (unicode): node to delete, empty string for default node + @param items (iterable): ids of items to retract + """ + profile = session_iface.ISATSession(self.session).profile + return self.asyncBridgeCall("mbRetract", service, node, items, profile) + + def jsonrpc_mbGet(self, service_jid, node, max_items, item_ids, extra): + """Get last microblogs from publisher_jid + + @param service_jid (unicode): pubsub service, usually publisher jid + @param node(unicode): mblogs node, or empty string to get the defaut one + @param max_items (int): maximum number of item to get or C.NO_LIMIT to get everything + @param item_ids (list[unicode]): list of item IDs + @param rsm (dict): TODO + @return: a deferred couple with the list of items and metadatas. + """ + profile = session_iface.ISATSession(self.session).profile + return self.asyncBridgeCall( + "mbGet", service_jid, node, max_items, item_ids, extra, profile + ) + + def jsonrpc_mbGetFromMany(self, publishers_type, publishers, max_items, extra): + """Get many blog nodes at once + + @param publishers_type (unicode): one of "ALL", "GROUP", "JID" + @param publishers (tuple(unicode)): tuple of publishers (empty list for all, list of groups or list of jids) + @param max_items (int): maximum number of item to get or C.NO_LIMIT to get everything + @param extra (dict): TODO + @return (str): RT Deferred session id + """ + profile = session_iface.ISATSession(self.session).profile + return self.sat_host.bridgeCall( + "mbGetFromMany", publishers_type, publishers, max_items, extra, profile + ) + + def jsonrpc_mbGetFromManyRTResult(self, rt_session): + """Get results from RealTime mbGetFromMany session + + @param rt_session (str): RT Deferred session id + """ + profile = session_iface.ISATSession(self.session).profile + return self.asyncBridgeCall("mbGetFromManyRTResult", rt_session, profile) + + def jsonrpc_mbGetFromManyWithComments( + self, + publishers_type, + publishers, + max_items, + max_comments, + rsm_dict, + rsm_comments_dict, + ): + """Helper method to get the microblogs and their comments in one shot + + @param publishers_type (str): type of the list of publishers (one of "GROUP" or "JID" or "ALL") + @param publishers (list): list of publishers, according to publishers_type (list of groups or list of jids) + @param max_items (int): optional limit on the number of retrieved items. + @param max_comments (int): maximum number of comments to retrieve + @param rsm_dict (dict): RSM data for initial items only + @param rsm_comments_dict (dict): RSM data for comments only + @param profile_key: profile key + @return (str): RT Deferred session id + """ + profile = session_iface.ISATSession(self.session).profile + return self.sat_host.bridgeCall( + "mbGetFromManyWithComments", + publishers_type, + publishers, + max_items, + max_comments, + rsm_dict, + rsm_comments_dict, + profile, + ) + + def jsonrpc_mbGetFromManyWithCommentsRTResult(self, rt_session): + """Get results from RealTime mbGetFromManyWithComments session + + @param rt_session (str): RT Deferred session id + """ + profile = session_iface.ISATSession(self.session).profile + return self.asyncBridgeCall( + "mbGetFromManyWithCommentsRTResult", rt_session, profile + ) + + # def jsonrpc_sendMblog(self, type_, dest, text, extra={}): + # """ Send microblog message + # @param type_ (unicode): one of "PUBLIC", "GROUP" + # @param dest (tuple(unicode)): recipient groups (ignored for "PUBLIC") + # @param text (unicode): microblog's text + # """ + # profile = session_iface.ISATSession(self.session).profile + # extra['allow_comments'] = 'True' + + # if not type_: # auto-detect + # type_ = "PUBLIC" if dest == [] else "GROUP" + + # if type_ in ("PUBLIC", "GROUP") and text: + # if type_ == "PUBLIC": + # #This text if for the public microblog + # log.debug("sending public blog") + # return self.sat_host.bridge.sendGroupBlog("PUBLIC", (), text, extra, profile) + # else: + # log.debug("sending group blog") + # dest = dest if isinstance(dest, list) else [dest] + # return self.sat_host.bridge.sendGroupBlog("GROUP", dest, text, extra, profile) + # else: + # raise Exception("Invalid data") + + # def jsonrpc_deleteMblog(self, pub_data, comments): + # """Delete a microblog node + # @param pub_data: a tuple (service, comment node identifier, item identifier) + # @param comments: comments node identifier (for main item) or False + # """ + # profile = session_iface.ISATSession(self.session).profile + # return self.sat_host.bridge.deleteGroupBlog(pub_data, comments if comments else '', profile) + + # def jsonrpc_updateMblog(self, pub_data, comments, message, extra={}): + # """Modify a microblog node + # @param pub_data: a tuple (service, comment node identifier, item identifier) + # @param comments: comments node identifier (for main item) or False + # @param message: new message + # @param extra: dict which option name as key, which can be: + # - allow_comments: True to accept an other level of comments, False else (default: False) + # - rich: if present, contain rich text in currently selected syntax + # """ + # profile = session_iface.ISATSession(self.session).profile + # if comments: + # extra['allow_comments'] = 'True' + # return self.sat_host.bridge.updateGroupBlog(pub_data, comments if comments else '', message, extra, profile) + + # def jsonrpc_sendMblogComment(self, node, text, extra={}): + # """ Send microblog message + # @param node: url of the comments node + # @param text: comment + # """ + # profile = session_iface.ISATSession(self.session).profile + # if node and text: + # return self.sat_host.bridge.sendGroupBlogComment(node, text, extra, profile) + # else: + # raise Exception("Invalid data") + + # def jsonrpc_getMblogs(self, publisher_jid, item_ids, max_items=C.RSM_MAX_ITEMS): + # """Get specified microblogs posted by a contact + # @param publisher_jid: jid of the publisher + # @param item_ids: list of microblogs items IDs + # @return list of microblog data (dict)""" + # profile = session_iface.ISATSession(self.session).profile + # d = self.asyncBridgeCall("getGroupBlogs", publisher_jid, item_ids, {'max_': unicode(max_items)}, False, profile) + # return d + + # def jsonrpc_getMblogsWithComments(self, publisher_jid, item_ids, max_comments=C.RSM_MAX_COMMENTS): + # """Get specified microblogs posted by a contact and their comments + # @param publisher_jid: jid of the publisher + # @param item_ids: list of microblogs items IDs + # @return list of couple (microblog data, list of microblog data)""" + # profile = session_iface.ISATSession(self.session).profile + # d = self.asyncBridgeCall("getGroupBlogsWithComments", publisher_jid, item_ids, {}, max_comments, profile) + # return d + + # def jsonrpc_getMassiveMblogs(self, publishers_type, publishers, rsm=None): + # """Get lasts microblogs posted by several contacts at once + + # @param publishers_type (unicode): one of "ALL", "GROUP", "JID" + # @param publishers (tuple(unicode)): tuple of publishers (empty list for all, list of groups or list of jids) + # @param rsm (dict): TODO + # @return: dict{unicode: list[dict]) + # key: publisher's jid + # value: list of microblog data (dict) + # """ + # profile = session_iface.ISATSession(self.session).profile + # if rsm is None: + # rsm = {'max_': unicode(C.RSM_MAX_ITEMS)} + # d = self.asyncBridgeCall("getMassiveGroupBlogs", publishers_type, publishers, rsm, profile) + # self.sat_host.bridge.massiveSubscribeGroupBlogs(publishers_type, publishers, profile) + # return d + + # def jsonrpc_getMblogComments(self, service, node, rsm=None): + # """Get all comments of given node + # @param service: jid of the service hosting the node + # @param node: comments node + # """ + # profile = session_iface.ISATSession(self.session).profile + # if rsm is None: + # rsm = {'max_': unicode(C.RSM_MAX_COMMENTS)} + # d = self.asyncBridgeCall("getGroupBlogComments", service, node, rsm, profile) + # return d + + def jsonrpc_getPresenceStatuses(self): + """Get Presence information for connected contacts""" + profile = session_iface.ISATSession(self.session).profile + return self.sat_host.bridgeCall("getPresenceStatuses", profile) + + def jsonrpc_historyGet(self, from_jid, to_jid, size, between, search=""): + """Return history for the from_jid/to_jid couple""" + sat_session = session_iface.ISATSession(self.session) + profile = sat_session.profile + sat_jid = sat_session.jid + if not sat_jid: + raise exceptions.InternalError("session jid should be set") + if ( + jid.JID(from_jid).userhost() != sat_jid.userhost() + and jid.JID(to_jid).userhost() != sat_jid.userhost() + ): + log.error( + u"Trying to get history from a different jid (given (browser): {}, real (backend): {}), maybe a hack attempt ?".format( + from_jid, sat_jid + ) + ) + return {} + d = self.asyncBridgeCall( + "historyGet", from_jid, to_jid, size, between, search, profile + ) + + def show(result_dbus): + result = [] + for line in result_dbus: + # XXX: we have to do this stupid thing because Python D-Bus use its own types instead of standard types + # and txJsonRPC doesn't accept D-Bus types, resulting in a empty query + uuid, timestamp, from_jid, to_jid, message, subject, mess_type, extra = ( + line + ) + result.append( + ( + unicode(uuid), + float(timestamp), + unicode(from_jid), + unicode(to_jid), + dict(message), + dict(subject), + unicode(mess_type), + dict(extra), + ) + ) + return result + + d.addCallback(show) + return d + + def jsonrpc_mucJoin(self, room_jid, nick): + """Join a Multi-User Chat room + + @param room_jid (unicode): room JID or empty string to generate a unique name + @param nick (unicode): user nick + """ + profile = session_iface.ISATSession(self.session).profile + d = self.asyncBridgeCall("joinMUC", room_jid, nick, {}, profile) + return d + + def jsonrpc_inviteMUC(self, contact_jid, room_jid): + """Invite a user to a Multi-User Chat room + + @param contact_jid (unicode): contact to invite + @param room_jid (unicode): room JID or empty string to generate a unique name + """ + profile = session_iface.ISATSession(self.session).profile + room_id = room_jid.split("@")[0] + service = room_jid.split("@")[1] + return self.sat_host.bridgeCall( + "inviteMUC", contact_jid, service, room_id, {}, profile + ) + + def jsonrpc_mucLeave(self, room_jid): + """Quit a Multi-User Chat room""" + profile = session_iface.ISATSession(self.session).profile + try: + room_jid = jid.JID(room_jid) + except: + log.warning("Invalid room jid") + return + return self.sat_host.bridgeCall("mucLeave", room_jid.userhost(), profile) + + def jsonrpc_mucGetRoomsJoined(self): + """Return list of room already joined by user""" + profile = session_iface.ISATSession(self.session).profile + return self.sat_host.bridgeCall("mucGetRoomsJoined", profile) + + def jsonrpc_mucGetDefaultService(self): + """@return: the default MUC""" + d = self.asyncBridgeCall("mucGetDefaultService") + return d + + def jsonrpc_launchTarotGame(self, other_players, room_jid=""): + """Create a room, invite the other players and start a Tarot game. + + @param other_players (list[unicode]): JIDs of the players to play with + @param room_jid (unicode): room JID or empty string to generate a unique name + """ + profile = session_iface.ISATSession(self.session).profile + return self.sat_host.bridgeCall( + "tarotGameLaunch", other_players, room_jid, profile + ) + + def jsonrpc_getTarotCardsPaths(self): + """Give the path of all the tarot cards""" + _join = os.path.join + _media_dir = _join(self.sat_host.media_dir, "") + return map( + lambda x: _join(C.MEDIA_DIR, x[len(_media_dir) :]), + glob.glob(_join(_media_dir, C.CARDS_DIR, "*_*.png")), + ) + + def jsonrpc_tarotGameReady(self, player, referee): + """Tell to the server that we are ready to start the game""" + profile = session_iface.ISATSession(self.session).profile + return self.sat_host.bridgeCall("tarotGameReady", player, referee, profile) + + def jsonrpc_tarotGamePlayCards(self, player_nick, referee, cards): + """Tell to the server the cards we want to put on the table""" + profile = session_iface.ISATSession(self.session).profile + return self.sat_host.bridgeCall( + "tarotGamePlayCards", player_nick, referee, cards, profile + ) + + def jsonrpc_launchRadioCollective(self, invited, room_jid=""): + """Create a room, invite people, and start a radio collective. + + @param invited (list[unicode]): JIDs of the contacts to play with + @param room_jid (unicode): room JID or empty string to generate a unique name + """ + profile = session_iface.ISATSession(self.session).profile + return self.sat_host.bridgeCall("radiocolLaunch", invited, room_jid, profile) + + def jsonrpc_getEntitiesData(self, jids, keys): + """Get cached data for several entities at once + + @param jids: list jids from who we wants data, or empty list for all jids in cache + @param keys: name of data we want (list) + @return: requested data""" + if not C.ALLOWED_ENTITY_DATA.issuperset(keys): + raise exceptions.PermissionError( + "Trying to access unallowed data (hack attempt ?)" + ) + profile = session_iface.ISATSession(self.session).profile + try: + return self.sat_host.bridgeCall("getEntitiesData", jids, keys, profile) + except Exception as e: + raise failure.Failure(jsonrpclib.Fault(C.ERRNUM_BRIDGE_ERRBACK, unicode(e))) + + def jsonrpc_getEntityData(self, jid, keys): + """Get cached data for an entity + + @param jid: jid of contact from who we want data + @param keys: name of data we want (list) + @return: requested data""" + if not C.ALLOWED_ENTITY_DATA.issuperset(keys): + raise exceptions.PermissionError( + "Trying to access unallowed data (hack attempt ?)" + ) + profile = session_iface.ISATSession(self.session).profile + try: + return self.sat_host.bridgeCall("getEntityData", jid, keys, profile) + except Exception as e: + raise failure.Failure(jsonrpclib.Fault(C.ERRNUM_BRIDGE_ERRBACK, unicode(e))) + + def jsonrpc_getCard(self, jid_): + """Get VCard for entiry + @param jid_: jid of contact from who we want data + @return: id to retrieve the profile""" + profile = session_iface.ISATSession(self.session).profile + return self.sat_host.bridgeCall("getCard", jid_, profile) + + @defer.inlineCallbacks + def jsonrpc_avatarGet(self, entity, cache_only, hash_only): + session_data = session_iface.ISATSession(self.session) + profile = session_data.profile + # profile_uuid = session_data.uuid + avatar = yield self.asyncBridgeCall( + "avatarGet", entity, cache_only, hash_only, profile + ) + if hash_only: + defer.returnValue(avatar) + else: + filename = os.path.basename(avatar) + avatar_url = os.path.join(session_data.cache_dir, filename) + defer.returnValue(avatar_url) + + def jsonrpc_getAccountDialogUI(self): + """Get the dialog for managing user account + @return: XML string of the XMLUI""" + profile = session_iface.ISATSession(self.session).profile + return self.sat_host.bridgeCall("getAccountDialogUI", profile) + + def jsonrpc_getParamsUI(self): + """Return the parameters XML for profile""" + profile = session_iface.ISATSession(self.session).profile + return self.asyncBridgeCall("getParamsUI", C.SECURITY_LIMIT, C.APP_NAME, profile) + + def jsonrpc_asyncGetParamA(self, param, category, attribute="value"): + """Return the parameter value for profile""" + profile = session_iface.ISATSession(self.session).profile + if category == "Connection": + # we need to manage the followings params here, else SECURITY_LIMIT would block them + if param == "JabberID": + return self.asyncBridgeCall( + "asyncGetParamA", param, category, attribute, profile_key=profile + ) + elif param == "autoconnect": + return defer.succeed(C.BOOL_TRUE) + d = self.asyncBridgeCall( + "asyncGetParamA", + param, + category, + attribute, + C.SECURITY_LIMIT, + profile_key=profile, + ) + return d + + def jsonrpc_setParam(self, name, value, category): + profile = session_iface.ISATSession(self.session).profile + return self.sat_host.bridgeCall( + "setParam", name, value, category, C.SECURITY_LIMIT, profile + ) + + def jsonrpc_launchAction(self, callback_id, data): + # FIXME: any action can be launched, this can be a huge security issue if callback_id can be guessed + # a security system with authorised callback_id must be implemented, similar to the one for authorised params + profile = session_iface.ISATSession(self.session).profile + d = self.asyncBridgeCall("launchAction", callback_id, data, profile) + return d + + def jsonrpc_chatStateComposing(self, to_jid_s): + """Call the method to process a "composing" state. + @param to_jid_s: contact the user is composing to + """ + profile = session_iface.ISATSession(self.session).profile + return self.sat_host.bridgeCall("chatStateComposing", to_jid_s, profile) + + def jsonrpc_getNewAccountDomain(self): + """@return: the domain for new account creation""" + d = self.asyncBridgeCall("getNewAccountDomain") + return d + + def jsonrpc_syntaxConvert( + self, text, syntax_from=C.SYNTAX_XHTML, syntax_to=C.SYNTAX_CURRENT + ): + """ Convert a text between two syntaxes + @param text: text to convert + @param syntax_from: source syntax (e.g. "markdown") + @param syntax_to: dest syntax (e.g.: "XHTML") + @param safe: clean resulting XHTML to avoid malicious code if True (forced here) + @return: converted text """ + profile = session_iface.ISATSession(self.session).profile + return self.sat_host.bridgeCall( + "syntaxConvert", text, syntax_from, syntax_to, True, profile + ) + + def jsonrpc_getLastResource(self, jid_s): + """Get the last active resource of that contact.""" + profile = session_iface.ISATSession(self.session).profile + return self.sat_host.bridgeCall("getLastResource", jid_s, profile) + + def jsonrpc_getFeatures(self): + """Return the available features in the backend for profile""" + profile = session_iface.ISATSession(self.session).profile + return self.sat_host.bridgeCall("getFeatures", profile) + + def jsonrpc_skipOTR(self): + """Tell the backend to leave OTR handling to Libervia.""" + profile = session_iface.ISATSession(self.session).profile + return self.sat_host.bridgeCall("skipOTR", profile) + + def jsonrpc_namespacesGet(self): + return self.sat_host.bridgeCall("namespacesGet") + + +class WaitingRequests(dict): + def setRequest(self, request, profile, register_with_ext_jid=False): + """Add the given profile to the waiting list. + + @param request (server.Request): the connection request + @param profile (str): %(doc_profile)s + @param register_with_ext_jid (bool): True if we will try to register the profile with an external XMPP account credentials + """ + dc = reactor.callLater(BRIDGE_TIMEOUT, self.purgeRequest, profile) + self[profile] = (request, dc, register_with_ext_jid) + + def purgeRequest(self, profile): + """Remove the given profile from the waiting list. + + @param profile (str): %(doc_profile)s + """ + try: + dc = self[profile][1] + except KeyError: + return + if dc.active(): + dc.cancel() + del self[profile] + + def getRequest(self, profile): + """Get the waiting request for the given profile. + + @param profile (str): %(doc_profile)s + @return: the waiting request or None + """ + return self[profile][0] if profile in self else None + + def getRegisterWithExtJid(self, profile): + """Get the value of the register_with_ext_jid parameter. + + @param profile (str): %(doc_profile)s + @return: bool or None + """ + return self[profile][2] if profile in self else None + + +class Register(JSONRPCMethodManager): + """This class manage the registration procedure with SàT + It provide an api for the browser, check password and setup the web server""" + + def __init__(self, sat_host): + JSONRPCMethodManager.__init__(self, sat_host) + self.profiles_waiting = {} + self.request = None + + def render(self, request): + """ + Render method with some hacks: + - if login is requested, try to login with form data + - except login, every method is jsonrpc + - user doesn't need to be authentified for explicitely listed methods, but must be for all others + """ + if request.postpath == ["login"]: + return self.loginOrRegister(request) + _session = request.getSession() + parsed = jsonrpclib.loads(request.content.read()) + method = parsed.get("method") # pylint: disable=E1103 + if method not in ["getSessionMetadata", "registerParams", "menusGet"]: + # if we don't call these methods, we need to be identified + profile = session_iface.ISATSession(_session).profile + if not profile: + # user is not identified, we return a jsonrpc fault + fault = jsonrpclib.Fault( + C.ERRNUM_LIBERVIA, C.NOT_ALLOWED + ) # FIXME: define some standard error codes for libervia + return jsonrpc.JSONRPC._cbRender( + self, fault, request, parsed.get("id"), parsed.get("jsonrpc") + ) # pylint: disable=E1103 + self.request = request + return jsonrpc.JSONRPC.render(self, request) + + def loginOrRegister(self, request): + """This method is called with the POST information from the registering form. + + @param request: request of the register form + @return: a constant indicating the state: + - C.BAD_REQUEST: something is wrong in the request (bad arguments) + - a return value from self._loginAccount or self._registerNewAccount + """ + try: + submit_type = request.args["submit_type"][0] + except KeyError: + return C.BAD_REQUEST + + if submit_type == "register": + self._registerNewAccount(request) + return server.NOT_DONE_YET + elif submit_type == "login": + self._loginAccount(request) + return server.NOT_DONE_YET + return Exception("Unknown submit type") + + @defer.inlineCallbacks + def _registerNewAccount(self, request): + try: + login = request.args["register_login"][0] + password = request.args["register_password"][0] + email = request.args["email"][0] + except KeyError: + request.write(C.BAD_REQUEST) + request.finish() + return + status = yield self.sat_host.registerNewAccount(request, login, password, email) + request.write(status) + request.finish() + + @defer.inlineCallbacks + def _loginAccount(self, request): + """Try to authenticate the user with the request information. + + will write to request a constant indicating the state: + - C.PROFILE_LOGGED: profile is connected + - C.PROFILE_LOGGED_EXT_JID: profile is connected and an external jid has been used + - C.SESSION_ACTIVE: session was already active + - C.BAD_REQUEST: something is wrong in the request (bad arguments) + - C.PROFILE_AUTH_ERROR: either the profile (login) or the profile password is wrong + - C.XMPP_AUTH_ERROR: the profile is authenticated but the XMPP password is wrong + - C.ALREADY_WAITING: a request has already been submitted for this profil, C.PROFILE_LOGGED_EXT_JID)e + - C.NOT_CONNECTED: connection has not been established + the request will then be finished + @param request: request of the register form + """ + try: + login = request.args["login"][0] + password = request.args["login_password"][0] + except KeyError: + request.write(C.BAD_REQUEST) + request.finish() + return + + assert login + + try: + status = yield self.sat_host.connect(request, login, password) + except ( + exceptions.DataError, + exceptions.ProfileUnknownError, + exceptions.PermissionError, + ): + request.write(C.PROFILE_AUTH_ERROR) + request.finish() + return + except exceptions.NotReady: + request.write(C.ALREADY_WAITING) + request.finish() + return + except exceptions.TimeOutError: + request.write(C.NO_REPLY) + request.finish() + return + except exceptions.InternalError as e: + request.write(e.message) + request.finish() + return + except exceptions.ConflictError: + request.write(C.SESSION_ACTIVE) + request.finish() + return + except ValueError as e: + if e.message in (C.PROFILE_AUTH_ERROR, C.XMPP_AUTH_ERROR): + request.write(e.message) + request.finish() + return + else: + raise e + + assert status + request.write(status) + request.finish() + + def jsonrpc_isConnected(self): + _session = self.request.getSession() + profile = session_iface.ISATSession(_session).profile + return self.sat_host.bridgeCall("isConnected", profile) + + def jsonrpc_connect(self): + _session = self.request.getSession() + profile = session_iface.ISATSession(_session).profile + if self.waiting_profiles.getRequest(profile): + raise jsonrpclib.Fault( + 1, C.ALREADY_WAITING + ) # FIXME: define some standard error codes for libervia + self.waiting_profiles.setRequest(self.request, profile) + self.sat_host.bridgeCall("connect", profile) + return server.NOT_DONE_YET + + def jsonrpc_getSessionMetadata(self): + """Return metadata useful on session start + + @return (dict): metadata which can have the following keys: + "plugged" (bool): True if a profile is already plugged + "warning" (unicode): a security warning message if plugged is False and if it make sense + this key may not be present + "allow_registration" (bool): True if registration is allowed + this key is only present if profile is unplugged + @return: a couple (registered, message) with: + - registered: + - message: + """ + metadata = {} + _session = self.request.getSession() + profile = session_iface.ISATSession(_session).profile + if profile: + metadata["plugged"] = True + else: + metadata["plugged"] = False + metadata["warning"] = self._getSecurityWarning() + metadata["allow_registration"] = self.sat_host.options["allow_registration"] + return metadata + + def jsonrpc_registerParams(self): + """Register the frontend specific parameters""" + # params = """<params><individual>...</category></individual>""" + # self.sat_host.bridge.paramsRegisterApp(params, C.SECURITY_LIMIT, C.APP_NAME) + + def jsonrpc_menusGet(self): + """Return the parameters XML for profile""" + # XXX: we put this method in Register because we get menus before being logged + return self.sat_host.bridgeCall("menusGet", "", C.SECURITY_LIMIT) + + def _getSecurityWarning(self): + """@return: a security warning message, or None if the connection is secure""" + if ( + self.request.URLPath().scheme == "https" + or not self.sat_host.options["security_warning"] + ): + return None + text = ( + "<p>" + + D_("You are about to connect to an unsecure service.") + + "</p><p> </p><p>" + ) + + if self.sat_host.options["connection_type"] == "both": + new_port = ( + (":%s" % self.sat_host.options["port_https_ext"]) + if self.sat_host.options["port_https_ext"] != HTTPS_PORT + else "" + ) + url = "https://%s" % self.request.URLPath().netloc.replace( + ":%s" % self.sat_host.options["port"], new_port + ) + text += D_( + "Please read our %(faq_prefix)ssecurity notice%(faq_suffix)s regarding HTTPS" + ) % { + "faq_prefix": '<a href="http://salut-a-toi.org/faq.html#https" target="#">', + "faq_suffix": "</a>", + } + text += "</p><p>" + D_("and use the secure version of this website:") + text += '</p><p> </p><p align="center"><a href="%(url)s">%(url)s</a>' % { + "url": url + } + else: + text += D_("You should ask your administrator to turn on HTTPS.") + + return text + "</p><p> </p>" + + +class SignalHandler(jsonrpc.JSONRPC): + def __init__(self, sat_host): + web_resource.Resource.__init__(self) + self.register = None + self.sat_host = sat_host + self._last_service_prof_disconnect = time.time() + self.signalDeferred = {} # dict of deferred (key: profile, value: Deferred) + # which manages the long polling HTTP request with signals + self.queue = {} + + def plugRegister(self, register): + self.register = register + + def jsonrpc_getSignals(self): + """Keep the connection alive until a signal is received, then send it + @return: (signal, *signal_args)""" + _session = self.request.getSession() + profile = session_iface.ISATSession(_session).profile + if profile in self.queue: # if we have signals to send in queue + if self.queue[profile]: + return self.queue[profile].pop(0) + else: + # the queue is empty, we delete the profile from queue + del self.queue[profile] + _session.lock() # we don't want the session to expire as long as this connection is active + + def unlock(signal, profile): + _session.unlock() + try: + source_defer = self.signalDeferred[profile] + if source_defer.called and source_defer.result[0] == "disconnected": + log.info(u"[%s] disconnected" % (profile,)) + try: + _session.expire() + except KeyError: + # FIXME: happen if session is ended using login page + # when pyjamas page is also launched + log.warning(u"session is already expired") + except IndexError: + log.error("Deferred result should be a tuple with fonction name first") + + self.signalDeferred[profile] = defer.Deferred() + self.request.notifyFinish().addBoth(unlock, profile) + return self.signalDeferred[profile] + + def getGenericCb(self, function_name): + """Return a generic function which send all params to signalDeferred.callback + function must have profile as last argument""" + + def genericCb(*args): + profile = args[-1] + if not profile in self.sat_host.prof_connected: + return + signal_data = (function_name, args[:-1]) + try: + signal_callback = self.signalDeferred[profile].callback + except KeyError: + self.queue.setdefault(profile, []).append(signal_data) + else: + signal_callback(signal_data) + del self.signalDeferred[profile] + + return genericCb + + def actionNewHandler(self, action_data, action_id, security_limit, profile): + """actionNew handler + + XXX: We need need a dedicated handler has actionNew use a security_limit which must be managed + @param action_data(dict): see bridge documentation + @param action_id(unicode): identitifer of the action + @param security_limit(int): %(doc_security_limit)s + @param profile(unicode): %(doc_profile)s + """ + if not profile in self.sat_host.prof_connected: + return + # FIXME: manage security limit in a dedicated method + # raise an exception if it's not OK + # and read value in sat.conf + if security_limit >= C.SECURITY_LIMIT: + log.debug( + u"Ignoring action {action_id}, blocked by security limit".format( + action_id=action_id + ) + ) + return + signal_data = ("actionNew", (action_data, action_id, security_limit)) + try: + signal_callback = self.signalDeferred[profile].callback + except KeyError: + self.queue.setdefault(profile, []).append(signal_data) + else: + signal_callback(signal_data) + del self.signalDeferred[profile] + + def connected(self, profile, jid_s): + """Connection is done. + + @param profile (unicode): %(doc_profile)s + @param jid_s (unicode): the JID that we were assigned by the server, as the resource might differ from the JID we asked for. + """ + # FIXME: _logged should not be called from here, check this code + # FIXME: check if needed to connect with external jid + # jid_s is handled in QuickApp.connectionHandler already + # assert self.register # register must be plugged + # request = self.sat_host.waiting_profiles.getRequest(profile) + # if request: + # self.sat_host._logged(profile, request) + + def disconnected(self, profile): + if profile == C.SERVICE_PROFILE: + # if service profile has been disconnected, we try to reconnect it + # if we can't we show error message + # and if we have 2 disconnection in a short time, we don't try to reconnect + # and display an error message + disconnect_delta = time.time() - self._last_service_prof_disconnect + if disconnect_delta < 15: + log.error( + _( + u"Service profile disconnected twice in a short time, please check connection" + ) + ) + else: + log.info( + _( + u"Service profile has been disconnected, but we need it! Reconnecting it..." + ) + ) + d = self.sat_host.bridgeCall( + "connect", profile, self.sat_host.options["passphrase"], {} + ) + d.addErrback( + lambda failure_: log.error( + _( + u"Can't reconnect service profile, please check connection: {reason}" + ).format(reason=failure_) + ) + ) + self._last_service_prof_disconnect = time.time() + return + + if not profile in self.sat_host.prof_connected: + log.info( + _( + u"'disconnected' signal received for a not connected profile ({profile})" + ).format(profile=profile) + ) + return + self.sat_host.prof_connected.remove(profile) + if profile in self.signalDeferred: + self.signalDeferred[profile].callback(("disconnected",)) + del self.signalDeferred[profile] + else: + if profile not in self.queue: + self.queue[profile] = [] + self.queue[profile].append(("disconnected",)) + + def render(self, request): + """ + Render method wich reject access if user is not identified + """ + _session = request.getSession() + parsed = jsonrpclib.loads(request.content.read()) + profile = session_iface.ISATSession(_session).profile + if not profile: + # user is not identified, we return a jsonrpc fault + fault = jsonrpclib.Fault( + C.ERRNUM_LIBERVIA, C.NOT_ALLOWED + ) # FIXME: define some standard error codes for libervia + return jsonrpc.JSONRPC._cbRender( + self, fault, request, parsed.get("id"), parsed.get("jsonrpc") + ) # pylint: disable=E1103 + self.request = request + return jsonrpc.JSONRPC.render(self, request) + + +class UploadManager(web_resource.Resource): + """This class manage the upload of a file + It redirect the stream to SàT core backend""" + + isLeaf = True + NAME = "path" # name use by the FileUpload + + def __init__(self, sat_host): + self.sat_host = sat_host + self.upload_dir = tempfile.mkdtemp() + self.sat_host.addCleanup(shutil.rmtree, self.upload_dir) + + def getTmpDir(self): + return self.upload_dir + + def _getFileName(self, request): + """Generate unique filename for a file""" + raise NotImplementedError + + def _fileWritten(self, request, filepath): + """Called once the file is actually written on disk + @param request: HTTP request object + @param filepath: full filepath on the server + @return: a tuple with the name of the async bridge method + to be called followed by its arguments. + """ + raise NotImplementedError + + def render(self, request): + """ + Render method with some hacks: + - if login is requested, try to login with form data + - except login, every method is jsonrpc + - user doesn't need to be authentified for getSessionMetadata, but must be for all other methods + """ + filename = self._getFileName(request) + filepath = os.path.join(self.upload_dir, filename) + # FIXME: the uploaded file is fully loaded in memory at form parsing time so far + # (see twisted.web.http.Request.requestReceived). A custom requestReceived should + # be written in the futur. In addition, it is not yet possible to get progression informations + # (see http://twistedmatrix.com/trac/ticket/288) + + with open(filepath, "w") as f: + f.write(request.args[self.NAME][0]) + + def finish(d): + error = isinstance(d, Exception) or isinstance(d, failure.Failure) + request.write(C.UPLOAD_KO if error else C.UPLOAD_OK) + # TODO: would be great to re-use the original Exception class and message + # but it is lost in the middle of the backtrace and encapsulated within + # a DBusException instance --> extract the data from the backtrace? + request.finish() + + d = JSONRPCMethodManager(self.sat_host).asyncBridgeCall( + *self._fileWritten(request, filepath) + ) + d.addCallbacks(lambda d: finish(d), lambda failure: finish(failure)) + return server.NOT_DONE_YET + + +class UploadManagerRadioCol(UploadManager): + NAME = "song" + + def _getFileName(self, request): + extension = os.path.splitext(request.args["filename"][0])[1] + return "%s%s" % ( + str(uuid.uuid4()), + extension, + ) # XXX: chromium doesn't seem to play song without the .ogg extension, even with audio/ogg mime-type + + def _fileWritten(self, request, filepath): + """Called once the file is actually written on disk + @param request: HTTP request object + @param filepath: full filepath on the server + @return: a tuple with the name of the async bridge method + to be called followed by its arguments. + """ + profile = session_iface.ISATSession(request.getSession()).profile + return ("radiocolSongAdded", request.args["referee"][0], filepath, profile) + + +class UploadManagerAvatar(UploadManager): + NAME = "avatar_path" + + def _getFileName(self, request): + return str(uuid.uuid4()) + + def _fileWritten(self, request, filepath): + """Called once the file is actually written on disk + @param request: HTTP request object + @param filepath: full filepath on the server + @return: a tuple with the name of the async bridge method + to be called followed by its arguments. + """ + profile = session_iface.ISATSession(request.getSession()).profile + return ("setAvatar", filepath, profile) + + +class Libervia(service.Service): + debug = defer.Deferred.debug # True if twistd/Libervia is launched in debug mode + + def __init__(self, options): + self.options = options + self.initialised = defer.Deferred() + self.waiting_profiles = WaitingRequests() # FIXME: should be removed + + if self.options["base_url_ext"]: + self.base_url_ext = self.options.pop("base_url_ext") + if self.base_url_ext[-1] != "/": + self.base_url_ext += "/" + self.base_url_ext_data = urlparse.urlsplit(self.base_url_ext) + else: + self.base_url_ext = None + # we split empty string anyway so we can do things like + # scheme = self.base_url_ext_data.scheme or 'https' + self.base_url_ext_data = urlparse.urlsplit("") + + if not self.options["port_https_ext"]: + self.options["port_https_ext"] = self.options["port_https"] + if self.options["data_dir"] == DATA_DIR_DEFAULT: + coerceDataDir( + self.options["data_dir"] + ) # this is not done when using the default value + + self.html_dir = os.path.join(self.options["data_dir"], C.HTML_DIR) + self.themes_dir = os.path.join(self.options["data_dir"], C.THEMES_DIR) + + self._cleanup = [] + + self.signal_handler = SignalHandler(self) + self.sessions = {} # key = session value = user + self.prof_connected = set() # Profiles connected + self.ns_map = {} # map of short name to namespaces + + ## bridge ## + try: + self.bridge = Bridge() + except BridgeExceptionNoService: + print(u"Can't connect to SàT backend, are you sure it's launched ?") + sys.exit(1) + self.bridge.bridgeConnect(callback=self._bridgeCb, errback=self._bridgeEb) + + def _namespacesGetCb(self, ns_map): + self.ns_map = ns_map + + def _namespacesGetEb(self, failure_): + log.error(_(u"Can't get namespaces map: {msg}").format(msg=failure_)) + + def backendReady(self, dummy): + self.root = root = LiberviaRootResource(self.html_dir) + _register = Register(self) + _upload_radiocol = UploadManagerRadioCol(self) + _upload_avatar = UploadManagerAvatar(self) + d = self.bridgeCall("namespacesGet") + d.addCallback(self._namespacesGetCb) + d.addErrback(self._namespacesGetEb) + self.signal_handler.plugRegister(_register) + self.bridge.register_signal("connected", self.signal_handler.connected) + self.bridge.register_signal("disconnected", self.signal_handler.disconnected) + # core + for signal_name in [ + "presenceUpdate", + "messageNew", + "subscribe", + "contactDeleted", + "newContact", + "entityDataUpdated", + "paramUpdate", + ]: + self.bridge.register_signal( + signal_name, self.signal_handler.getGenericCb(signal_name) + ) + # XXX: actionNew is handled separately because the handler must manage security_limit + self.bridge.register_signal("actionNew", self.signal_handler.actionNewHandler) + # plugins + for signal_name in [ + "psEvent", + "mucRoomJoined", + "tarotGameStarted", + "tarotGameNew", + "tarotGameChooseContrat", + "tarotGameShowCards", + "tarotGameInvalidCards", + "tarotGameCardsPlayed", + "tarotGameYourTurn", + "tarotGameScore", + "tarotGamePlayers", + "radiocolStarted", + "radiocolPreload", + "radiocolPlay", + "radiocolNoUpload", + "radiocolUploadOk", + "radiocolSongRejected", + "radiocolPlayers", + "mucRoomLeft", + "mucRoomUserChangedNick", + "chatStateReceived", + ]: + self.bridge.register_signal( + signal_name, self.signal_handler.getGenericCb(signal_name), "plugin" + ) + self.media_dir = self.bridge.getConfig("", "media_dir") + self.local_dir = self.bridge.getConfig("", "local_dir") + self.cache_root_dir = os.path.join(self.local_dir, C.CACHE_DIR) + + # JSON APIs + self.putChild("json_signal_api", self.signal_handler) + self.putChild("json_api", MethodHandler(self)) + self.putChild("register_api", _register) + + # files upload + self.putChild("upload_radiocol", _upload_radiocol) + self.putChild("upload_avatar", _upload_avatar) + + # static pages + self.putChild("blog_legacy", MicroBlog(self)) + self.putChild(C.THEMES_URL, ProtectedFile(self.themes_dir)) + + # websocket + if self.options["connection_type"] in ("https", "both"): + wss = websockets.LiberviaPageWSProtocol.getResource(self, secure=True) + self.putChild("wss", wss) + if self.options["connection_type"] in ("http", "both"): + ws = websockets.LiberviaPageWSProtocol.getResource(self, secure=False) + self.putChild("ws", ws) + + # Libervia pages + LiberviaPage.importPages(self) + LiberviaPage.setMenu(self.options["menu_json"]) + ## following signal is needed for cache handling in Libervia pages + self.bridge.register_signal( + "psEventRaw", partial(LiberviaPage.onNodeEvent, self), "plugin" + ) + self.bridge.register_signal( + "messageNew", partial(LiberviaPage.onSignal, self, "messageNew") + ) + + # Progress handling + self.bridge.register_signal( + "progressStarted", partial(ProgressHandler._signal, "started") + ) + self.bridge.register_signal( + "progressFinished", partial(ProgressHandler._signal, "finished") + ) + self.bridge.register_signal( + "progressError", partial(ProgressHandler._signal, "error") + ) + + # media dirs + # FIXME: get rid of dirname and "/" in C.XXX_DIR + self.putChild(os.path.dirname(C.MEDIA_DIR), ProtectedFile(self.media_dir)) + self.cache_resource = web_resource.NoResource() + self.putChild(C.CACHE_DIR, self.cache_resource) + + # special + self.putChild( + "radiocol", + ProtectedFile(_upload_radiocol.getTmpDir(), defaultType="audio/ogg"), + ) # FIXME: We cheat for PoC because we know we are on the same host, so we use directly upload dir + # pyjamas tests, redirected only for dev versions + if self.version[-1] == "D": + self.putChild("test", web_util.Redirect("/libervia_test.html")) + + # redirections + root._initRedirections(self.options) + + server.Request.defaultContentType = "text/html; charset=utf-8" + wrapped = web_resource.EncodingResourceWrapper( + root, [server.GzipEncoderFactory()] + ) + self.site = server.Site(wrapped) + self.site.sessionFactory = LiberviaSession + self.renderer = template.Renderer(self) + self.putChild("templates", ProtectedFile(self.renderer.base_dir)) + + def initEb(self, failure): + log.error(_(u"Init error: {msg}").format(msg=failure)) + reactor.stop() + return failure + + def _bridgeCb(self): + self.bridge.getReady( + lambda: self.initialised.callback(None), + lambda failure: self.initialised.errback(Exception(failure)), + ) + self.initialised.addCallback(self.backendReady) + self.initialised.addErrback(self.initEb) + + def _bridgeEb(self, failure): + log.error(u"Can't connect to bridge: {}".format(failure)) + + @property + def version(self): + """Return the short version of Libervia""" + return C.APP_VERSION + + @property + def full_version(self): + """Return the full version of Libervia (with extra data when in development mode)""" + version = self.version + if version[-1] == "D": + # we are in debug version, we add extra data + try: + return self._version_cache + except AttributeError: + self._version_cache = u"{} ({})".format( + version, utils.getRepositoryData(libervia) + ) + return self._version_cache + else: + return version + + def bridgeCall(self, method_name, *args, **kwargs): + """Call an asynchronous bridge method and return a deferred + + @param method_name: name of the method as a unicode + @return: a deferred which trigger the result + + """ + d = defer.Deferred() + + def _callback(*args): + if not args: + d.callback(None) + else: + if len(args) != 1: + Exception("Multiple return arguments not supported") + d.callback(args[0]) + + def _errback(result): + d.errback( + failure.Failure( + jsonrpclib.Fault(C.ERRNUM_BRIDGE_ERRBACK, result.classname) + ) + ) + + kwargs["callback"] = _callback + kwargs["errback"] = _errback + getattr(self.bridge, method_name)(*args, **kwargs) + return d + + @defer.inlineCallbacks + def _logged(self, profile, request): + """Set everything when a user just logged in + + @param profile + @param request + @return: a constant indicating the state: + - C.PROFILE_LOGGED + - C.PROFILE_LOGGED_EXT_JID + @raise exceptions.ConflictError: session is already active + """ + register_with_ext_jid = self.waiting_profiles.getRegisterWithExtJid(profile) + self.waiting_profiles.purgeRequest(profile) + session = request.getSession() + sat_session = session_iface.ISATSession(session) + if sat_session.profile: + log.error(_(u"/!\\ Session has already a profile, this should NEVER happen!")) + raise failure.Failure(exceptions.ConflictError("Already active")) + + sat_session.profile = profile + self.prof_connected.add(profile) + cache_dir = os.path.join( + self.cache_root_dir, u"profiles", regex.pathEscape(profile) + ) + # FIXME: would be better to have a global /cache URL which redirect to profile's cache directory, without uuid + self.cache_resource.putChild(sat_session.uuid, ProtectedFile(cache_dir)) + log.debug( + _(u"profile cache resource added from {uuid} to {path}").format( + uuid=sat_session.uuid, path=cache_dir + ) + ) + + def onExpire(): + log.info(u"Session expired (profile={profile})".format(profile=profile)) + self.cache_resource.delEntity(sat_session.uuid) + log.debug( + _(u"profile cache resource {uuid} deleted").format(uuid=sat_session.uuid) + ) + try: + # We purge the queue + del self.signal_handler.queue[profile] + except KeyError: + pass + # and now we disconnect the profile + self.bridgeCall("disconnect", profile) + + session.notifyOnExpire(onExpire) + + # FIXME: those session infos should be returned by connect or isConnected + infos = yield self.bridgeCall("sessionInfosGet", profile) + sat_session.jid = jid.JID(infos["jid"]) + sat_session.backend_started = int(infos["started"]) + + state = C.PROFILE_LOGGED_EXT_JID if register_with_ext_jid else C.PROFILE_LOGGED + defer.returnValue(state) + + @defer.inlineCallbacks + def connect(self, request, login, password): + """log user in + + If an other user was already logged, it will be unlogged first + @param request(server.Request): request linked to the session + @param login(unicode): user login + can be profile name + can be profile@[libervia_domain.ext] + can be a jid (a new profile will be created with this jid if needed) + @param password(unicode): user password + @return (unicode, None): C.SESSION_ACTIVE: if session was aleady active else self._logged value + @raise exceptions.DataError: invalid login + @raise exceptions.ProfileUnknownError: this login doesn't exist + @raise exceptions.PermissionError: a login is not accepted (e.g. empty password not allowed) + @raise exceptions.NotReady: a profile connection is already waiting + @raise exceptions.TimeoutError: didn't received and answer from Bridge + @raise exceptions.InternalError: unknown error + @raise ValueError(C.PROFILE_AUTH_ERROR): invalid login and/or password + @raise ValueError(C.XMPP_AUTH_ERROR): invalid XMPP account password + """ + + # XXX: all security checks must be done here, even if present in javascript + if login.startswith("@"): + raise failure.Failure(exceptions.DataError("No profile_key allowed")) + + if login.startswith("guest@@") and login.count("@") == 2: + log.debug("logging a guest account") + elif "@" in login: + if login.count("@") != 1: + raise failure.Failure( + exceptions.DataError("Invalid login: {login}".format(login=login)) + ) + try: + login_jid = jid.JID(login) + except (RuntimeError, jid.InvalidFormat, AttributeError): + raise failure.Failure(exceptions.DataError("No profile_key allowed")) + + # FIXME: should it be cached? + new_account_domain = yield self.bridgeCall("getNewAccountDomain") + + if login_jid.host == new_account_domain: + # redirect "user@libervia.org" to the "user" profile + login = login_jid.user + login_jid = None + else: + login_jid = None + + try: + profile = yield self.bridgeCall("profileNameGet", login) + except Exception: # XXX: ProfileUnknownError wouldn't work, it's encapsulated + # FIXME: find a better way to handle bridge errors + if ( + login_jid is not None and login_jid.user + ): # try to create a new sat profile using the XMPP credentials + if not self.options["allow_registration"]: + log.warning( + u"Trying to register JID account while registration is not allowed" + ) + raise failure.Failure( + exceptions.DataError( + u"JID login while registration is not allowed" + ) + ) + profile = login # FIXME: what if there is a resource? + connect_method = "asyncConnectWithXMPPCredentials" + register_with_ext_jid = True + else: # non existing username + raise failure.Failure(exceptions.ProfileUnknownError()) + else: + if profile != login or ( + not password + and profile + not in self.options["empty_password_allowed_warning_dangerous_list"] + ): + # profiles with empty passwords are restricted to local frontends + raise exceptions.PermissionError + register_with_ext_jid = False + + connect_method = "connect" + + # we check if there is not already an active session + sat_session = session_iface.ISATSession(request.getSession()) + if sat_session.profile: + # yes, there is + if sat_session.profile != profile: + # it's a different profile, we need to disconnect it + log.warning( + _( + u"{new_profile} requested login, but {old_profile} was already connected, disconnecting {old_profile}" + ).format(old_profile=sat_session.profile, new_profile=profile) + ) + self.purgeSession(request) + + if self.waiting_profiles.getRequest(profile): + # FIXME: check if and when this can happen + raise failure.Failure(exceptions.NotReady("Already waiting")) + + self.waiting_profiles.setRequest(request, profile, register_with_ext_jid) + try: + connected = yield self.bridgeCall(connect_method, profile, password) + except Exception as failure_: + fault = failure_.faultString + self.waiting_profiles.purgeRequest(profile) + if fault in ("PasswordError", "ProfileUnknownError"): + log.info( + u"Profile {profile} doesn't exist or the submitted password is wrong".format( + profile=profile + ) + ) + raise failure.Failure(ValueError(C.PROFILE_AUTH_ERROR)) + elif fault == "SASLAuthError": + log.info( + u"The XMPP password of profile {profile} is wrong".format( + profile=profile + ) + ) + raise failure.Failure(ValueError(C.XMPP_AUTH_ERROR)) + elif fault == "NoReply": + log.info( + _( + "Did not receive a reply (the timeout expired or the connection is broken)" + ) + ) + raise exceptions.TimeOutError + else: + log.error( + u'Unmanaged fault string "{fault}" in errback for the connection of profile {profile}'.format( + fault=fault, profile=profile + ) + ) + raise failure.Failure(exceptions.InternalError(fault)) + + if connected: + # profile is already connected in backend + # do we have a corresponding session in Libervia? + sat_session = session_iface.ISATSession(request.getSession()) + if sat_session.profile: + # yes, session is active + if sat_session.profile != profile: + # existing session should have been ended above + # so this line should never be reached + log.error( + _( + u"session profile [{session_profile}] differs from login profile [{profile}], this should not happen!" + ).format(session_profile=sat_session.profile, profile=profile) + ) + raise exceptions.InternalError("profile mismatch") + defer.returnValue(C.SESSION_ACTIVE) + log.info( + _( + u"profile {profile} was already connected in backend".format( + profile=profile + ) + ) + ) + # no, we have to create it + + state = yield self._logged(profile, request) + defer.returnValue(state) + + def registerNewAccount(self, request, login, password, email): + """Create a new account, or return error + @param request(server.Request): request linked to the session + @param login(unicode): new account requested login + @param email(unicode): new account email + @param password(unicode): new account password + @return(unicode): a constant indicating the state: + - C.BAD_REQUEST: something is wrong in the request (bad arguments) + - C.INVALID_INPUT: one of the data is not valid + - C.REGISTRATION_SUCCEED: new account has been successfully registered + - C.ALREADY_EXISTS: the given profile already exists + - C.INTERNAL_ERROR or any unmanaged fault string + @raise PermissionError: registration is now allowed in server configuration + """ + if not self.options["allow_registration"]: + log.warning( + _(u"Registration received while it is not allowed, hack attempt?") + ) + raise failure.Failure( + exceptions.PermissionError(u"Registration is not allowed on this server") + ) + + if ( + not re.match(C.REG_LOGIN_RE, login) + or not re.match(C.REG_EMAIL_RE, email, re.IGNORECASE) + or len(password) < C.PASSWORD_MIN_LENGTH + ): + return C.INVALID_INPUT + + def registered(result): + return C.REGISTRATION_SUCCEED + + def registeringError(failure): + status = failure.value.faultString + if status == "ConflictError": + return C.ALREADY_EXISTS + elif status == "InternalError": + return C.INTERNAL_ERROR + else: + log.error( + _(u"Unknown registering error status: {status }").format( + status=status + ) + ) + return status + + d = self.bridgeCall("registerSatAccount", email, password, login) + d.addCallback(registered) + d.addErrback(registeringError) + return d + + def addCleanup(self, callback, *args, **kwargs): + """Add cleaning method to call when service is stopped + + cleaning method will be called in reverse order of they insertion + @param callback: callable to call on service stop + @param *args: list of arguments of the callback + @param **kwargs: list of keyword arguments of the callback""" + self._cleanup.insert(0, (callback, args, kwargs)) + + def startService(self): + """Connect the profile for Libervia and start the HTTP(S) server(s)""" + + def eb(e): + log.error(_(u"Connection failed: %s") % e) + self.stop() + + def initOk(dummy): + try: + connected = self.bridge.isConnected(C.SERVICE_PROFILE) + except Exception as e: + # we don't want the traceback + msg = [l for l in unicode(e).split("\n") if l][-1] + log.error( + u"Can't check service profile ({profile}), are you sure it exists ?\n{error}".format( + profile=C.SERVICE_PROFILE, error=msg + ) + ) + self.stop() + return + if not connected: + self.bridge.connect( + C.SERVICE_PROFILE, + self.options["passphrase"], + {}, + callback=self._startService, + errback=eb, + ) + else: + self._startService() + + self.initialised.addCallback(initOk) + + ## URLs ## + + def putChild(self, path, resource): + """Add a child to the root resource""" + # FIXME: check that no information is leaked (c.f. https://twistedmatrix.com/documents/current/web/howto/using-twistedweb.html#request-encoders) + self.root.putChild( + path, + web_resource.EncodingResourceWrapper(resource, [server.GzipEncoderFactory()]), + ) + + def getExtBaseURLData(self, request): + """Retrieve external base URL Data + + this method tried to retrieve the base URL found by external user + It does by checking in this order: + - base_url_ext option from configuration + - proxy x-forwarder-host headers + - URL of the request + @return (urlparse.SplitResult): SplitResult instance with only scheme and netloc filled + """ + ext_data = self.base_url_ext_data + url_path = request.URLPath() + if not ext_data.scheme or not ext_data.netloc: + # ext_data is not specified, we check headers + if request.requestHeaders.hasHeader("x-forwarded-host"): + # we are behing a proxy + # we fill proxy_scheme and proxy_netloc value + proxy_host = request.requestHeaders.getRawHeaders("x-forwarded-host")[0] + try: + proxy_server = request.requestHeaders.getRawHeaders( + "x-forwarded-server" + )[0] + except TypeError: + # no x-forwarded-server found, we use proxy_host + proxy_netloc = proxy_host + else: + # if the proxy host has a port, we use it with server name + proxy_port = urlparse.urlsplit(u"//{}".format(proxy_host)).port + proxy_netloc = ( + u"{}:{}".format(proxy_server, proxy_port) + if proxy_port is not None + else proxy_server + ) + proxy_netloc = proxy_netloc.decode("utf-8") + try: + proxy_scheme = request.requestHeaders.getRawHeaders( + "x-forwarded-proto" + )[0].decode("utf-8") + except TypeError: + proxy_scheme = None + else: + proxy_scheme, proxy_netloc = None, None + else: + proxy_scheme, proxy_netloc = None, None + + return urlparse.SplitResult( + ext_data.scheme or proxy_scheme or url_path.scheme.decode("utf-8"), + ext_data.netloc or proxy_netloc or url_path.netloc.decode("utf-8"), + ext_data.path or u"/", + "", + "", + ) + + def getExtBaseURL(self, request, path="", query="", fragment="", scheme=None): + """Get external URL according to given elements + + external URL is the URL seen by external user + @param path(unicode): same as for urlsplit.urlsplit + path will be prefixed to follow found external URL if suitable + @param params(unicode): same as for urlsplit.urlsplit + @param query(unicode): same as for urlsplit.urlsplit + @param fragment(unicode): same as for urlsplit.urlsplit + @param scheme(unicode, None): if not None, will override scheme from base URL + @return (unicode): external URL + """ + split_result = self.getExtBaseURLData(request) + return urlparse.urlunsplit( + ( + split_result.scheme.decode("utf-8") if scheme is None else scheme, + split_result.netloc.decode("utf-8"), + os.path.join(split_result.path, path), + query, + fragment, + ) + ) + + def checkRedirection(self, url): + """check is a part of the URL prefix is redirected then replace it + + @param url(unicode): url to check + @return (unicode): possibly redirected URL which should link to the same location + """ + inv_redirections = self.root.inv_redirections + url_parts = url.strip(u"/").split(u"/") + for idx in xrange(len(url), 0, -1): + test_url = u"/" + u"/".join(url_parts[:idx]) + if test_url in inv_redirections: + rem_url = url_parts[idx:] + return os.path.join( + u"/", u"/".join([inv_redirections[test_url]] + rem_url) + ) + return url + + ## Sessions ## + + def purgeSession(self, request): + """helper method to purge a session during request handling""" + session = request.session + if session is not None: + log.debug(_(u"session purge")) + session.expire() + # FIXME: not clean but it seems that it's the best way to reset + # session during request handling + request._secureSession = request._insecureSession = None + + def getSessionData(self, request, *args): + """helper method to retrieve session data + + @param request(server.Request): request linked to the session + @param *args(zope.interface.Interface): interface of the session to get + @return (iterator(data)): requested session data + """ + session = request.getSession() + if len(args) == 1: + return args[0](session) + else: + return (iface(session) for iface in args) + + @defer.inlineCallbacks + def getAffiliation(self, request, service, node): + """retrieve pubsub node affiliation for current user + + use cache first, and request pubsub service if not cache is found + @param request(server.Request): request linked to the session + @param service(jid.JID): pubsub service + @param node(unicode): pubsub node + @return (unicode): affiliation + """ + sat_session = self.getSessionData(request, session_iface.ISATSession) + if sat_session.profile is None: + raise exceptions.InternalError(u"profile must be set to use this method") + affiliation = sat_session.getAffiliation(service, node) + if affiliation is not None: + defer.returnValue(affiliation) + else: + try: + affiliations = yield self.bridgeCall( + "psAffiliationsGet", service.full(), node, sat_session.profile + ) + except Exception as e: + log.warning( + "Can't retrieve affiliation for {service}/{node}: {reason}".format( + service=service, node=node, reason=e + ) + ) + affiliation = u"" + else: + try: + affiliation = affiliations[node] + except KeyError: + affiliation = u"" + sat_session.setAffiliation(service, node, affiliation) + defer.returnValue(affiliation) + + ## Websocket (dynamic pages) ## + + def getWebsocketURL(self, request): + base_url_split = self.getExtBaseURLData(request) + if base_url_split.scheme.endswith("s"): + scheme = u"wss" + else: + scheme = u"ws" + + return self.getExtBaseURL(request, path=scheme, scheme=scheme) + + def registerWSToken(self, token, page, request): + websockets.LiberviaPageWSProtocol.registerToken(token, page, request) + + ## Various utils ## + + def getHTTPDate(self, timestamp=None): + now = time.gmtime(timestamp) + fmt_date = u"{day_name}, %d {month_name} %Y %H:%M:%S GMT".format( + day_name=C.HTTP_DAYS[now.tm_wday], month_name=C.HTTP_MONTH[now.tm_mon - 1] + ) + return time.strftime(fmt_date, now) + + ## TLS related methods ## + + def _TLSOptionsCheck(self): + """Check options coherence if TLS is activated, and update missing values + + Must be called only if TLS is activated + """ + if not self.options["tls_certificate"]: + log.error(u"a TLS certificate is needed to activate HTTPS connection") + self.quit(1) + if not self.options["tls_private_key"]: + self.options["tls_private_key"] = self.options["tls_certificate"] + + if not self.options["tls_private_key"]: + self.options["tls_private_key"] = self.options["tls_certificate"] + + def _loadCertificates(self, f): + """Read a .pem file with a list of certificates + + @param f (file): file obj (opened .pem file) + @return (list[OpenSSL.crypto.X509]): list of certificates + @raise OpenSSL.crypto.Error: error while parsing the file + """ + # XXX: didn't found any method to load a .pem file with several certificates + # so the certificates split is done here + certificates = [] + buf = [] + while True: + line = f.readline() + buf.append(line) + if "-----END CERTIFICATE-----" in line: + certificates.append( + OpenSSL.crypto.load_certificate( + OpenSSL.crypto.FILETYPE_PEM, "".join(buf) + ) + ) + buf = [] + elif not line: + log.debug(u"{} certificate(s) found".format(len(certificates))) + return certificates + + def _loadPKey(self, f): + """Read a private key from a .pem file + + @param f (file): file obj (opened .pem file) + @return (list[OpenSSL.crypto.PKey]): private key object + @raise OpenSSL.crypto.Error: error while parsing the file + """ + return OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, f.read()) + + def _loadCertificate(self, f): + """Read a public certificate from a .pem file + + @param f (file): file obj (opened .pem file) + @return (list[OpenSSL.crypto.X509]): public certificate + @raise OpenSSL.crypto.Error: error while parsing the file + """ + return OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, f.read()) + + def _getTLSContextFactory(self): + """Load TLS certificate and build the context factory needed for listenSSL""" + if ssl is None: + raise ImportError(u"Python module pyOpenSSL is not installed!") + + cert_options = {} + + for name, option, method in [ + ("privateKey", "tls_private_key", self._loadPKey), + ("certificate", "tls_certificate", self._loadCertificate), + ("extraCertChain", "tls_chain", self._loadCertificates), + ]: + path = self.options[option] + if not path: + assert option == "tls_chain" + continue + log.debug(u"loading {option} from {path}".format(option=option, path=path)) + try: + with open(path) as f: + cert_options[name] = method(f) + except IOError as e: + log.error( + u"Error while reading file {path} for option {option}: {error}".format( + path=path, option=option, error=e + ) + ) + self.quit(2) + except OpenSSL.crypto.Error: + log.error( + u"Error while parsing file {path} for option {option}, are you sure it is a valid .pem file?".format( + path=path, option=option + ) + ) + if ( + option == "tls_private_key" + and self.options["tls_certificate"] == path + ): + log.error( + u"You are using the same file for private key and public certificate, make sure that both a in {path} or use --tls_private_key option".format( + path=path + ) + ) + self.quit(2) + + return ssl.CertificateOptions(**cert_options) + + ## service management ## + + def _startService(self, dummy=None): + """Actually start the HTTP(S) server(s) after the profile for Libervia is connected. + + @raise ImportError: OpenSSL is not available + @raise IOError: the certificate file doesn't exist + @raise OpenSSL.crypto.Error: the certificate file is invalid + """ + # now that we have service profile connected, we add resource for its cache + service_path = regex.pathEscape(C.SERVICE_PROFILE) + cache_dir = os.path.join(self.cache_root_dir, u"profiles", service_path) + self.cache_resource.putChild(service_path, ProtectedFile(cache_dir)) + self.service_cache_url = u"/" + os.path.join(C.CACHE_DIR, service_path) + session_iface.SATSession.service_cache_url = self.service_cache_url + + if self.options["connection_type"] in ("https", "both"): + self._TLSOptionsCheck() + context_factory = self._getTLSContextFactory() + reactor.listenSSL(self.options["port_https"], self.site, context_factory) + if self.options["connection_type"] in ("http", "both"): + if ( + self.options["connection_type"] == "both" + and self.options["redirect_to_https"] + ): + reactor.listenTCP( + self.options["port"], + server.Site( + RedirectToHTTPS( + self.options["port"], self.options["port_https_ext"] + ) + ), + ) + else: + reactor.listenTCP(self.options["port"], self.site) + + @defer.inlineCallbacks + def stopService(self): + log.info(_("launching cleaning methods")) + for callback, args, kwargs in self._cleanup: + callback(*args, **kwargs) + try: + yield self.bridgeCall("disconnect", C.SERVICE_PROFILE) + except Exception: + log.warning(u"Can't disconnect service profile") + + def run(self): + reactor.run() + + def stop(self): + reactor.stop() + + def quit(self, exit_code=None): + """Exit app when reactor is running + + @param exit_code(None, int): exit code + """ + self.stop() + sys.exit(exit_code or 0) + + +class RedirectToHTTPS(web_resource.Resource): + def __init__(self, old_port, new_port): + web_resource.Resource.__init__(self) + self.isLeaf = True + self.old_port = old_port + self.new_port = new_port + + def render(self, request): + netloc = request.URLPath().netloc.replace( + ":%s" % self.old_port, ":%s" % self.new_port + ) + url = "https://" + netloc + request.uri + return web_util.redirectTo(url, request) + + +registerAdapter(session_iface.SATSession, server.Session, session_iface.ISATSession) +registerAdapter( + session_iface.SATGuestSession, server.Session, session_iface.ISATGuestSession +)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/server/session_iface.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,192 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a SAT frontend +# Copyright (C) 2009-2018 Jérôme Poisson (goffi@goffi.org) + +# This program 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. + +# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. +from zope.interface import Interface, Attribute, implements +from sat.tools.common import data_objects +from libervia.server.constants import Const as C +from collections import OrderedDict +import os.path +import shortuuid +import time + +FLAGS_KEY = "_flags" +MAX_CACHE_AFFILIATIONS = 100 # number of nodes to keep in cache + + +class ISATSession(Interface): + profile = Attribute("Sat profile") + jid = Attribute("JID associated with the profile") + uuid = Attribute("uuid associated with the profile session") + identities = Attribute("Identities of XMPP entities") + + +class SATSession(object): + implements(ISATSession) + + def __init__(self, session): + self.profile = None + self.jid = None + self.started = time.time() + # time when the backend session was started + self.backend_started = None + self.uuid = unicode(shortuuid.uuid()) + self.identities = data_objects.Identities() + self.csrf_token = unicode(shortuuid.uuid()) + self.pages_data = {} # used to keep data accross reloads (key is page instance) + self.affiliations = OrderedDict() # cache for node affiliations + + @property + def cache_dir(self): + if self.profile is None: + return self.service_cache_url + u"/" + return os.path.join(u"/", C.CACHE_DIR, self.uuid) + u"/" + + @property + def connected(self): + return self.profile is not None + + @property + def guest(self): + """True if this is a guest session""" + if self.profile is None: + return False + else: + return self.profile.startswith("guest@@") + + def getPageData(self, page, key): + """get session data for a page + + @param page(LiberviaPage): instance of the page + @param key(object): data key + return (None, object): value of the key + None if not found or page_data doesn't exist + """ + return self.pages_data.get(page, {}).get(key) + + def popPageData(self, page, key, default=None): + """like getPageData, but remove key once value is gotten + + @param page(LiberviaPage): instance of the page + @param key(object): data key + @param default(object): value to return if key is not found + @return (object): found value or default + """ + page_data = self.pages_data.get(page) + if page_data is None: + return default + value = page_data.pop(key, default) + if not page_data: + # no need to keep unused page_data + del self.pages_data[page] + return value + + def setPageData(self, page, key, value): + """set data to persist on reload + + @param page(LiberviaPage): instance of the page + @param key(object): data key + @param value(object): value to set + @return (object): set value + """ + page_data = self.pages_data.setdefault(page, {}) + page_data[key] = value + return value + + def setPageFlag(self, page, flag): + """set a flag for this page + + @param page(LiberviaPage): instance of the page + @param flag(unicode): flag to set + """ + flags = self.getPageData(page, FLAGS_KEY) + if flags is None: + flags = self.setPageData(page, FLAGS_KEY, set()) + flags.add(flag) + + def popPageFlag(self, page, flag): + """return True if flag is set + + flag is removed if it was set + @param page(LiberviaPage): instance of the page + @param flag(unicode): flag to set + @return (bool): True if flaag was set + """ + page_data = self.pages_data.get(page, {}) + flags = page_data.get(FLAGS_KEY) + if flags is None: + return False + if flag in flags: + flags.remove(flag) + # we remove data if they are not used anymore + if not flags: + del page_data[FLAGS_KEY] + if not page_data: + del self.pages_data[page] + return True + else: + return False + + def getAffiliation(self, service, node): + """retrieve affiliation for a pubsub node + + @param service(jid.JID): pubsub service + @param node(unicode): pubsub node + @return (unicode, None): affiliation, or None if it is not in cache + """ + if service.resource: + raise ValueError(u"Service must not have a resource") + if not node: + raise ValueError(u"node must be set") + try: + affiliation = self.affiliations.pop((service, node)) + except KeyError: + return None + else: + # we replace at the top to get the most recently used on top + # so less recently used will be removed if cache is full + self.affiliations[(service, node)] = affiliation + return affiliation + + def setAffiliation(self, service, node, affiliation): + """cache affiliation for a node + + will empty cache when it become too big + @param service(jid.JID): pubsub service + @param node(unicode): pubsub node + @param affiliation(unicode): affiliation to this node + """ + if service.resource: + raise ValueError(u"Service must not have a resource") + if not node: + raise ValueError(u"node must be set") + self.affiliations[(service, node)] = affiliation + while len(self.affiliations) > MAX_CACHE_AFFILIATIONS: + self.affiliations.popitem(last=False) + + +class ISATGuestSession(Interface): + id = Attribute("UUID of the guest") + data = Attribute("data associated with the guest") + + +class SATGuestSession(object): + implements(ISATGuestSession) + + def __init__(self, session): + self.id = None + self.data = None
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/server/utils.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,115 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2011-2018 Jérôme Poisson <goffi@goffi.org> + +# This program 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. + +# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. +from sat.core.i18n import _ +from twisted.internet import reactor +from twisted.internet import defer +from sat.core import exceptions +from sat.core.log import getLogger +import urllib + +log = getLogger(__name__) + + +def quote(value, safe="@"): + """shortcut to quote an unicode value for URL""" + return urllib.quote(value.encode("utf-8"), safe=safe) + + +class ProgressHandler(object): + """class to help the management of progressions""" + + handlers = {} + + def __init__(self, host, progress_id, profile): + self.host = host + self.progress_id = progress_id + self.profile = profile + + @classmethod + def _signal(cls, name, progress_id, data, profile): + handlers = cls.handlers + if profile in handlers and progress_id in handlers[profile]: + handler_data = handlers[profile][progress_id] + timeout = handler_data[u"timeout"] + if timeout.active(): + timeout.cancel() + cb = handler_data[name] + if cb is not None: + cb(data) + if name == u"started": + pass + elif name == u"finished": + handler_data[u"deferred"].callback(data) + handler_data[u"instance"].unregister_handler() + elif name == u"error": + handler_data[u"deferred"].errback(Exception(data)) + handler_data[u"instance"].unregister_handler() + else: + log.error(u"unexpected signal: {name}".format(name=name)) + + def _timeout(self): + log.warning( + _( + u"No progress received, cancelling handler: {progress_id} [{profile}]" + ).format(progress_id=self.progress_id, profile=self.profile) + ) + + def unregister_handler(self): + """remove a previously registered handler""" + try: + del self.handlers[self.profile][self.progress_id] + except KeyError: + log.warning( + _(u"Trying to remove unknown handler: {progress_id} [{profile}]").format( + progress_id=self.progress_id, profile=self.profile + ) + ) + else: + if not self.handlers[self.profile]: + self.handlers[self.profile] + + def register(self, started_cb=None, finished_cb=None, error_cb=None, timeout=30): + """register the signals to handle progression + + @param started_cb(callable, None): method to call when progressStarted signal is received + @param finished_cb(callable, None): method to call when progressFinished signal is received + @param error_cb(callable, None): method to call when progressError signal is received + @param timeout(int): progress time out + if nothing happen in this progression during this delay, + an exception is raised + @return (D(dict[unicode,unicode])): a deferred called when progression is finished + """ + handler_data = self.handlers.setdefault(self.profile, {}).setdefault( + self.progress_id, {} + ) + if handler_data: + raise exceptions.ConflictError( + u"There is already one handler for this progression" + ) + handler_data[u"instance"] = self + deferred = handler_data[u"deferred"] = defer.Deferred() + handler_data[u"started"] = started_cb + handler_data[u"finished"] = finished_cb + handler_data[u"error"] = error_cb + handler_data[u"timeout"] = reactor.callLater(timeout, self._timeout) + return deferred + + +class SubPage(unicode): + """use to mark subpages when generating a page path"""
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/server/websockets.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,148 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2011-2018 Jérôme Poisson <goffi@goffi.org> + +# This program 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. + +# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + +from sat.core.i18n import _ +from sat.core.log import getLogger + +log = getLogger(__name__) +from sat.core import exceptions + +from autobahn.twisted import websocket +from autobahn.twisted import resource as resource +from autobahn.websocket import types + +import json + +LIBERVIA_PROTOCOL = "libervia_page" + + +class LiberviaPageWSProtocol(websocket.WebSocketServerProtocol): + host = None + tokens_map = {} + + def onConnect(self, request): + prefix = LIBERVIA_PROTOCOL + u"_" + for protocol in request.protocols: + if protocol.startswith(prefix): + token = protocol[len(prefix) :].strip() + if token: + break + else: + raise types.ConnectionDeny( + types.ConnectionDeny.NOT_IMPLEMENTED, u"Can't use this subprotocol" + ) + + if token not in self.tokens_map: + log.warning(_(u"Can't activate page socket: unknown token")) + raise types.ConnectionDeny( + types.ConnectionDeny.FORBIDDEN, u"Bad token, please reload page" + ) + self.token = token + self.page = self.tokens_map[token]["page"] + self.request = self.tokens_map[token]["request"] + return protocol + + def onOpen(self): + log.debug( + _( + u"Websocket opened for {page} (token: {token})".format( + page=self.page, token=self.token + ) + ) + ) + self.request.sendData = self.sendJSONData + self.page.onSocketOpen(self.request) + + def onMessage(self, payload, isBinary): + try: + data_json = json.loads(payload.decode("utf8")) + except ValueError as e: + log.warning( + _(u"Not valid JSON, ignoring data: {msg}\n{data}").format( + msg=e, data=payload + ) + ) + return + # we request page first, to raise an AttributeError + # if it is not set (which should never happen) + page = self.page + try: + cb = page.on_data + except AttributeError: + log.warning( + _( + u'No "on_data" method set on dynamic page, ignoring data:\n{data}' + ).format(data=data_json) + ) + else: + cb(page, self.request, data_json) + + def onClose(self, wasClean, code, reason): + try: + token = self.token + except AttributeError: + log.warning(_(u"Websocket closed but no token is associated")) + return + + self.page.onSocketClose(self.request) + + try: + del self.tokens_map[token] + del self.request.sendData + except (KeyError, AttributeError): + raise exceptions.InternalError( + _(u"Token or sendData doesn't exist, this should never happen!") + ) + log.debug( + _( + u"Websocket closed for {page} (token: {token}). {reason}".format( + page=self.page, + token=self.token, + reason=u"" + if wasClean + else _(u"Reason: {reason}").format(reason=reason), + ) + ) + ) + + def sendJSONData(self, type_, **data): + assert "type" not in data + data["type"] = type_ + self.sendMessage(json.dumps(data, ensure_ascii=False).encode("utf8")) + + @classmethod + def getBaseURL(cls, host, secure): + return u"ws{sec}://localhost:{port}".format( + sec="s" if secure else "", + port=cls.host.options["port_https" if secure else "port"], + ) + + @classmethod + def getResource(cls, host, secure): + if cls.host is None: + cls.host = host + factory = websocket.WebSocketServerFactory(cls.getBaseURL(host, secure)) + factory.protocol = cls + return resource.WebSocketResource(factory) + + @classmethod + def registerToken(cls, token, page, request): + if token in cls.tokens_map: + raise exceptions.ConflictError(_(u"This token is already registered")) + cls.tokens_map[token] = {"page": page, "request": request}
--- a/setup.py Sat Aug 11 18:35:37 2018 +0200 +++ b/setup.py Sat Aug 25 17:59:48 2018 +0200 @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- # Libervia: a Salut à Toi frontend -# Copyright (C) 2011-2016 Jérôme Poisson (goffi@goffi.org) +# Copyright (C) 2011-2018 Jérôme Poisson (goffi@goffi.org) # Copyright (C) 2013-2016 Adrien Cossa (souliane@mailoo.org) # This program is free software: you can redistribute it and/or modify @@ -18,293 +18,74 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -from ez_setup import use_setuptools -use_setuptools() -from setuptools.command.install import install from setuptools import setup -from distutils.file_util import copy_file import os -import sys -import subprocess -from stat import ST_MODE -import shutil -from src.server.constants import Const as C -import tempfile -# seen here: http://stackoverflow.com/questions/7275295 -try: - from setuptools.command import egg_info - egg_info.write_toplevel_names -except (ImportError, AttributeError): - pass -else: - def _top_level_package(name): - return name.split('.', 1)[0] +NAME = "libervia" - def _hacked_write_toplevel_names(cmd, basename, filename): - pkgs = dict.fromkeys( - [_top_level_package(k) - for k in cmd.distribution.iter_distribution_names() - if _top_level_package(k) != "twisted" - ] - ) - cmd.write_file("top-level names", filename, '\n'.join(pkgs) + '\n') - - egg_info.write_toplevel_names = _hacked_write_toplevel_names - - -NAME = 'libervia' -LAUNCH_DAEMON_COMMAND = 'libervia' +install_requires = [ + "sat", + "sat-templates", + "twisted", + "txJSON-RPC==0.3.1", + "zope.interface", + "pyopenssl", + "jinja2>=2.9", + "shortuuid", + "autobahn", +] +long_description = u"""\ +Libervia is a web frontend for Salut à Toi (SàT), a multi-frontends and multi-purposes XMPP client. +It features chat, blog, forums, events, tickets, merge requests, file sharing, photo albums, etc. +It is also a decentralized, XMPP based web framework. +""" -ENV_LIBERVIA_INSTALL = "LIBERVIA_INSTALL" # environment variable to customise installation -JS_DEBUG = "jsdebug" # use debug mode with pyjsbuild -NO_PREINSTALL_OPT = 'nopreinstall' # skip all preinstallation checks -AUTO_DEB_OPT = 'autodeb' # automaticaly install debs -CLEAN_OPT = 'clean' # remove previous installation directories -PURGE_OPT = 'purge' # remove building and previous installation directories - - -class MercurialException(Exception): - pass - - -def module_installed(module_name): - """Try to import module_name, and return False if it failed - @param module_name: name of the module to test - @return: True if successful""" - try: - __import__(module_name) - except ImportError: - return False - return True +with open(os.path.join(NAME, "VERSION")) as v: + VERSION = v.read().strip() +is_dev_version = VERSION.endswith("D") -class CustomInstall(install): - - def custom_auto_options(self): - """Change options for twistd in the shell script - Mainly change the paths""" - sh_buffer = "" - with open(self.sh_script_path, 'r') as sh_file: - for ori_line in sh_file: - if ori_line.startswith('PLUGIN_OPTIONS='): - dest_line = 'PLUGIN_OPTIONS="-d %s"\n' % self.install_data_dir - elif ori_line.startswith('PYTHON='): - dest_line = 'PYTHON="%s"\n' % sys.executable - else: - dest_line = ori_line - sh_buffer += dest_line - - with open(self.sh_script_path, 'w') as sh_file: - sh_file.write(sh_buffer) - - def custom_create_links(self): - """Create symbolic links to executables""" - # the script which launch the daemon - for source, dest in self.sh_script_links: - if os.path.islink(dest) and os.readlink(dest) != source: - os.remove(dest) # copy_file doesn't force the link update - dest_name, copied = copy_file(source, dest, link='sym') - assert (copied) - # we change the perm in the same way as in the original install_scripts - mode = ((os.stat(dest_name)[ST_MODE]) | 0555) & 07777 - os.chmod(dest_name, mode) - - def pyjs_build(self): - """Build the browser side JS files from Python source.""" - cwd = os.getcwd() - os.chdir(os.path.join('src', 'browser')) - # we must have only certain package in the path, so we create a tmp dir to link only what we need - tmp_dir = tempfile.mkdtemp() - import sat, sat_frontends - os.symlink(os.path.dirname(sat.__file__), os.path.join(tmp_dir,"sat")) # FIXME: only work on unixes - os.symlink(os.path.dirname(sat_frontends.__file__), os.path.join(tmp_dir,"sat_frontends")) # FIXME: only work on unixes - libervia_files = os.path.abspath("../../src") - os.symlink(libervia_files, os.path.join(tmp_dir,"libervia")) # FIXME: only work on unixes - for module in ('libervia_main', 'libervia_test'): - build_args = ['pyjsbuild', module] + (['-d'] if JS_DEBUG in install_opt else []) + ['--no-compile-inplace', '-I', tmp_dir, '-o', self.pyjamas_output_dir] - result = subprocess.call(build_args) - if result != 0: - continue - shutil.rmtree(tmp_dir) - os.chdir(cwd) - return result - - def copy_data_files(self): - # XXX: To copy the JS files couldn't be done with the data_files parameter - # of setuptools.setup because all the files to be copied must exist before - # the call. Also, we need the value of self.install_lib to build the JS - # files (it's not easily predictable as it may vary from one system to - # another), so we can't call pyjsbuild before setuptools.setup. - - html = os.path.join(self.install_data_dir, C.HTML_DIR) - if os.path.isdir(html): - shutil.rmtree(html, ignore_errors=True) - shutil.copytree(self.pyjamas_output_dir, html) +def libervia_dev_version(): + """Use mercurial data to compute version""" - def run(self): - self.sh_script_path = os.path.join(self.install_lib, NAME, 'libervia.sh') - self.sh_script_links = [(self.sh_script_path, os.path.join(self.install_scripts, LAUNCH_DAEMON_COMMAND))] - self.install_data_dir = os.path.join(self.install_data, 'share', NAME) - self.pyjamas_output_dir = os.path.join(os.getcwd(), 'html') - sys.stdout.write('running pre installation stuff\n') - sys.stdout.flush() - if PURGE_OPT in install_opt: - self.purge() - elif CLEAN_OPT in install_opt: - self.clean() - install.run(self) - sys.stdout.write('running post installation stuff\n') - sys.stdout.flush() - try: - build_result = self.pyjs_build() # build after libervia.common is accessible - except OSError as e: - print "can't run pyjsbuild, are you sure pyjamas is installed?\nexception: {}".format(e) - return - if build_result == 127: # TODO: remove magic string # FIXME: seems useless as an OSError is raised if pyjsbuild is not accessible - print "pyjsbuild is not installed or not accessible from the PATH of user '%s'" % os.getenv('USERNAME') - return - if build_result != 0: - print "pyjsbuild failed to build libervia" - return - self.copy_data_files() - self.custom_auto_options() - if not "arch" in install_opt: - self.custom_create_links() + def version_scheme(version): + return VERSION.replace("D", ".dev0") - def confirm(self, message): - """Ask the user for a confirmation""" - message += 'Proceed' - while True: - res = raw_input("%s (y/n)? " % message) - if res not in ['y', 'Y', 'n', 'N']: - print "Your response ('%s') was not one of the expected responses: y, n" % res - message = 'Proceed' - continue - if res in ('y', 'Y'): - return True - return False - - def clean(self, message=None, to_remove=None): - """Clean previous installation directories + def local_scheme(version): + return "+{rev}.{distance}".format(rev=version.node[1:], distance=version.distance) - @param message (str): to use a non-default confirmation message - @param to_remove (str): extra files/directories to remove - """ - if message is None: - message = "Cleaning previous installation directories" - if to_remove is None: - to_remove = [] - for path in [os.path.join(self.install_lib, NAME), - self.install_data_dir, - os.path.join(self.install_data, 'share', 'doc', NAME), - os.path.join(self.install_lib, "%s.egg-info" % self.config_vars['dist_fullname']), - os.path.join(self.install_lib, "%s-py%s.egg-info" % (self.config_vars['dist_fullname'], self.config_vars['py_version_short'])), - ]: - if os.path.isdir(path): - to_remove.append(path) - for source, dest in self.sh_script_links: - if os.path.islink(dest): - to_remove.append(dest) - plugin_file = os.path.join(self.install_lib, 'twisted', 'plugins', NAME) - if os.path.isfile(plugin_file): - to_remove.append(plugin_file) - - message = "%s:\n%s\n" % (message, "\n".join([" %s" % path for path in to_remove])) - if not self.confirm(message): - return - sys.stdout.write('cleaning previous installation directories...\n') - sys.stdout.flush() - for path in to_remove: - if os.path.isdir(path): - shutil.rmtree(path, ignore_errors=True) - else: - os.remove(path) - - def purge(self): - """Clean building and previous installation directories""" - message = "Cleaning building and previous installation directories" - to_remove = [os.path.join(os.getcwd(), 'build'), self.pyjamas_output_dir] - self.clean(message, to_remove) + return {"version_scheme": version_scheme, "local_scheme": local_scheme} -def preinstall_check(install_opt): - """Check presence of problematic dependencies, and try to install them with package manager - This ugly stuff is necessary as distributions are not installed correctly with setuptools/distribute - Hope to remove this at some point""" - - modules_tocheck = [] # if empty this method is dummy - - package = {'twisted': 'python-twisted-core', - 'twisted.words': 'python-twisted-words', - 'twisted.web': 'python-twisted-web', - 'mercurial': 'mercurial'} # this dict map dependencies to packages names for debian distributions - - sys.stdout.write("Running pre-installation dependencies check\n") - - # which modules are not installed ? - modules_toinstall = [mod for mod in modules_tocheck if not module_installed(mod)] - """# is mercurial available ? - hg_installed = subprocess.call('which hg', stdout=open('/dev/null', 'w'), shell=True) == 0 - if not hg_installed: - modules_toinstall.append('mercurial')""" # hg can be installed from pypi - - if modules_toinstall: - if AUTO_DEB_OPT in install_opt: # auto debian installation is requested - # are we on a distribution using apt ? - apt_path = subprocess.Popen('which apt-get', stdout=subprocess.PIPE, shell=True).communicate()[0][:-1] - else: - apt_path = None - - not_installed = set() - if apt_path: - # we have apt, we'll try to use it - for module_name in modules_toinstall: - package_name = package[module_name] - sys.stdout.write("Installing %s\n" % package_name) - success = subprocess.call('%s -qy install %s' % (apt_path, package_name), shell=True) == 0 - if not success: - not_installed.add(module_name) - else: - not_installed = set(modules_toinstall) - - if not_installed: - # some packages can't be automatically installed, we print their name for manual installation - sys.stdout.write("You should install the following dependencies with your distribution recommanded tool before installing %s:\n" % NAME) - for module_name in not_installed: - sys.stdout.write("- %s (Debian name: %s)\n" % (module_name, package[module_name])) - sys.exit(2) - - -if sys.argv[1].lower() in ['egg_info', 'install']: - # we only check dependencies if egg_info or install is used - install_opt = os.environ.get(ENV_LIBERVIA_INSTALL, "").split() - if not NO_PREINSTALL_OPT in install_opt: # user can force preinstall skipping - preinstall_check(install_opt) - -setup(name=NAME, - version='0.7.0a2.post1', - description=u'Web frontend for Salut à Toi', - long_description=u'Libervia is a web frontend for Salut à Toi (SàT), a multi-frontends and multi-purposes XMPP client.', - author='Association « Salut à Toi »', - author_email='contact@goffi.org', - url='http://www.salut-a-toi.org', - classifiers=['Development Status :: 3 - Alpha', - 'Environment :: Web Environment', - 'Framework :: Twisted', - 'License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)', - 'Operating System :: POSIX :: Linux', - 'Topic :: Communications :: Chat'], - package_dir={'libervia': 'src', 'twisted.plugins': 'src/twisted/plugins'}, - packages=['libervia', 'libervia.common', 'libervia.server', 'twisted.plugins'], - package_data={'libervia': ['libervia.sh']}, - include_package_data=True, - data_files=[(os.path.join('share', 'doc', NAME), ['COPYING', 'README', 'INSTALL']), ] + - [(os.path.join('share', NAME, root), - [os.path.join(root, f) for f in files]) - for root, dirs, files in os.walk(C.THEMES_DIR)], - scripts=[], - zip_safe=False, - install_requires=['sat', 'twisted', 'txJSON-RPC==0.3.1', 'zope.interface', 'pyopenssl', 'jinja2>=2.9', 'shortuuid', 'autobahn'], - cmdclass={'install': CustomInstall}, - ) +setup( + name=NAME, + version=VERSION, + description=u"Web frontend for Salut à Toi", + long_description=long_description, + author="Association « Salut à Toi »", + author_email="contact@goffi.org", + url="https://www.salut-a-toi.org", + classifiers=[ + "Development Status :: 3 - Alpha", + "Environment :: Web Environment", + "Framework :: Twisted", + "License :: OSI Approved :: GNU Affero General Public License v3 " + "or later (AGPLv3+)", + "Operating System :: POSIX :: Linux", + "Topic :: Communications :: Chat", + ], + packages=["libervia", "libervia.common", "libervia.server", "twisted.plugins"], + include_package_data=True, + data_files=[(os.path.join("share", "doc", NAME), ["COPYING", "README", "INSTALL"])] + + [ + (os.path.join("share", NAME, root), [os.path.join(root, f) for f in files]) + for root, dirs, files in os.walk(u"themes") + ], + scripts=["bin/libervia"], + zip_safe=False, + setup_requires=["setuptools_scm"] if is_dev_version else [], + use_scm_version=libervia_dev_version if is_dev_version else False, + install_requires=install_requires, + package_data={"libervia": ["VERSION"]}, + python_requires="~=2.7", +)
--- a/src/__init__.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,6 +0,0 @@ -try: - from common.constants import Const as C -except ImportError: - pass # import doesn't work at this stage with pyjamas -else: - __version__ = C.APP_VERSION
--- a/src/browser/collections.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,149 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2014 Jérôme Poisson <goffi@goffi.org> - -# This program 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. - -# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. - -class OrderedDict(object): - """Naive implementation of OrderedDict which is compatible with pyjamas""" - - def __init__(self, *args, **kwargs): - self.__internal_dict = {} - self.__keys = [] # this list keep the keys in order - if args: - if len(args)>1: - raise TypeError("OrderedDict expected at most 1 arguments, got {}".format(len(args))) - if isinstance(args[0], (dict, OrderedDict)): - for key, value in args[0].iteritems(): - self[key] = value - for key, value in args[0]: - self[key] = value - - def __len__(self): - return len(self.__keys) - - def __setitem__(self, key, value): - if key not in self.__keys: - self.__keys.append(key) - self.__internal_dict[key] = value - - def __getitem__(self, key): - return self.__internal_dict[key] - - def __delitem__(self, key): - del self.__internal_dict[key] - self.__keys.remove(key) - - def __contains__(self, key): - return key in self.__keys - - def clear(self): - self.__internal_dict.clear() - del self.__keys[:] - - def copy(self): - return OrderedDict(self) - - @classmethod - def fromkeys(cls, seq, value=None): - ret = OrderedDict() - for key in seq: - ret[key] = value - return ret - - def get(self, key, default=None): - try: - return self.__internal_dict[key] - except KeyError: - return default - - def has_key(self, key): - return key in self.__keys - - def keys(self): - return self.__keys[:] - - def iterkeys(self): - for key in self.__keys: - yield key - - def items(self): - ret = [] - for key in self.__keys: - ret.append((key, self.__internal_dict[key])) - return ret - - def iteritems(self): - for key in self.__keys: - yield (key, self.__internal_dict[key]) - - def values(self): - ret = [] - for key in self.__keys: - ret.append(self.__internal_dict[key]) - return ret - - def itervalues(self): - for key in self.__keys: - yield (self.__internal_dict[key]) - - def popitem(self, last=True): - try: - key = self.__keys.pop(-1 if last else 0) - except IndexError: - raise KeyError('dictionnary is empty') - value = self.__internal_dict.pop(key) - return((key, value)) - - def setdefault(self, key, default=None): - try: - return self.__internal_dict[key] - except KeyError: - self[key] = default - return default - - def update(self, *args, **kwargs): - if len(args) > 1: - raise TypeError('udpate expected at most 1 argument, got {}'.format(len(args))) - if args: - if hasattr(args[0], 'keys'): - for k in args[0]: - self[k] = args[0][k] - else: - for (k, v) in args[0]: - self[k] = v - for k, v in kwargs.items(): - self[k] = v - - def pop(self, *args): - if not args: - raise TypeError('pop expected at least 1 argument, got 0') - try: - self.__internal_dict.pop(args[0]) - except KeyError: - if len(args) == 2: - return args[1] - raise KeyError(args[0]) - self.__keys.remove(args[0]) - - def viewitems(self): - raise NotImplementedError - - def viewkeys(self): - raise NotImplementedError - - def viewvalues(self): - raise NotImplementedError
--- a/src/browser/libervia_main.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,709 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2011-2018 Jérôme Poisson <goffi@goffi.org> - -# This program 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. - -# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. - - -### logging configuration ### -from sat_browser import logging -logging.configure() -from sat.core.log import getLogger -log = getLogger(__name__) -### - -from sat.core.i18n import D_ - -from sat_frontends.quick_frontend.quick_app import QuickApp -from sat_frontends.quick_frontend import quick_widgets -from sat_frontends.quick_frontend import quick_menus - -from sat_frontends.tools.misc import InputHistory -from sat_browser import strings -from sat_frontends.tools import jid -from sat_frontends.tools import host_listener -from sat.core.i18n import _ - -from pyjamas.ui.RootPanel import RootPanel -# from pyjamas.ui.HTML import HTML -from pyjamas.ui.KeyboardListener import KEY_ESCAPE -from pyjamas.Timer import Timer -from pyjamas import Window, DOM - -from sat_browser import json -from sat_browser import register -from sat_browser.contact_list import ContactList -from sat_browser import main_panel -# from sat_browser import chat -from sat_browser import blog -from sat_browser import xmlui -from sat_browser import dialog -from sat_browser import html_tools -from sat_browser import notification -from sat_browser import libervia_widget -from sat_browser import web_widget -assert web_widget # XXX: just here to avoid pyflakes warning - -from sat_browser.constants import Const as C - - -try: - # FIXME: import plugin dynamically - from sat_browser import plugin_sec_otr -except ImportError: - pass - - -unicode = str # FIXME: pyjamas workaround - - -# MAX_MBLOG_CACHE = 500 # Max microblog entries kept in memories # FIXME - - -class SatWebFrontend(InputHistory, QuickApp): - ENCRYPTION_HANDLERS = False # e2e encryption is handled directly by Libervia, - # not backend - - def onModuleLoad(self): - log.info("============ onModuleLoad ==============") - self.bridge_signals = json.BridgeSignals(self) - QuickApp.__init__(self, json.BridgeCall, xmlui=xmlui, connect_bridge=False) - self.connectBridge() - self._profile_plugged = False - self.signals_cache[C.PROF_KEY_NONE] = [] - self.panel = main_panel.MainPanel(self) - self.tab_panel = self.panel.tab_panel - self.tab_panel.addTabListener(self) - self._register_box = None - RootPanel().add(self.panel) - - self.alerts_counter = notification.FaviconCounter() - self.notification = notification.Notification(self.alerts_counter) - DOM.addEventPreview(self) - self.importPlugins() - self._register = json.RegisterCall() - self._register.call('menusGet', self.gotMenus) - self._register.call('registerParams', None) - self._register.call('getSessionMetadata', self._getSessionMetadataCB) - self.initialised = False - self.init_cache = [] # used to cache events until initialisation is done - self.cached_params = {} - self.next_rsm_index = 0 - - #FIXME: microblog cache should be managed directly in blog module - self.mblog_cache = [] # used to keep our own blog entries in memory, to show them in new mblog panel - - self._versions={} # SàT and Libervia versions cache - - @property - def whoami(self): - # XXX: works because Libervia is mono-profile - # if one day Libervia manage several profiles at once, this must be deleted - return self.profiles[C.PROF_KEY_NONE].whoami - - @property - def contact_list(self): - return self.contact_lists[C.PROF_KEY_NONE] - - @property - def visible_widgets(self): - widgets_panel = self.tab_panel.getCurrentPanel() - return [wid for wid in widgets_panel.widgets if isinstance(wid, quick_widgets.QuickWidget)] - - @property - def base_location(self): - """Return absolute base url of this Libervia instance""" - url = Window.getLocation().getHref() - if url.endswith(C.LIBERVIA_MAIN_PAGE): - url = url[:-len(C.LIBERVIA_MAIN_PAGE)] - if url.endswith("/"): - url = url[:-1] - return url - - @property - def sat_version(self): - return self._versions["sat"] - - @property - def libervia_version(self): - return self._versions["libervia"] - - def getVersions(self, callback=None): - """Ask libervia server for SàT and Libervia version and fill local cache - - @param callback: method to call when both versions have been received - """ - def gotVersion(): - if len(self._versions) == 2 and callback is not None: - callback() - - if len(self._versions) == 2: - # we already have versions in cache - gotVersion() - return - - def gotSat(version): - self._versions["sat"] = version - gotVersion() - - def gotLibervia(version): - self._versions["libervia"] = version - gotVersion() - - self.bridge.getVersion(callback=gotSat, profile=None) - self.bridge.getLiberviaVersion(callback=gotLibervia, profile=None) # XXX: bridge direct call expect a profile, even for method with no profile needed - - def registerSignal(self, functionName, handler=None, iface="core", with_profile=True): - if handler is None: - callback = getattr(self, "{}{}".format(functionName, "Handler")) - else: - callback = handler - - self.bridge_signals.register_signal(functionName, callback, with_profile=with_profile) - - def importPlugins(self): - self.plugins = {} - try: - self.plugins['otr'] = plugin_sec_otr.OTR(self) - except TypeError: # plugin_sec_otr has not been imported - pass - - def getSelected(self): - wid = self.tab_panel.getCurrentPanel() - if not isinstance(wid, libervia_widget.WidgetsPanel): - log.error("Tab widget is not a WidgetsPanel, can't get selected widget") - return None - return wid.selected - - def setSelected(self, widget): - """Define the selected widget""" - widgets_panel = self.tab_panel.getCurrentPanel() - if not isinstance(widgets_panel, libervia_widget.WidgetsPanel): - return - - selected = widgets_panel.selected - - if selected == widget: - return - - if selected: - selected.removeStyleName('selected_widget') - - # FIXME: check that widget is in the current WidgetsPanel - widgets_panel.selected = widget - self.selected_widget = widget - - if widget: - widgets_panel.selected.addStyleName('selected_widget') - - def resize(self): - """Resize elements""" - Window.onResize() - - def onBeforeTabSelected(self, sender, tab_index): - return True - - def onTabSelected(self, sender, tab_index): - pass - # def onTabSelected(self, sender, tab_index): - # for widget in self.tab_panel.getCurrentPanel().widgets: - # if isinstance(widget, chat.Chat): - # clist = self.contact_list - # clist.removeAlerts(widget.current_target, True) - - def onEventPreview(self, event): - if event.type in ["keydown", "keypress", "keyup"] and event.keyCode == KEY_ESCAPE: - #needed to prevent request cancellation in Firefox - event.preventDefault() - return True - - def getAvatarURL(self, jid_): - """Return avatar of a jid if in cache, else ask for it. - - @param jid_ (jid.JID): JID of the contact - @return: the URL to the avatar (unicode) - """ - return self.getAvatar(jid_) or self.getDefaultAvatar() - - def getDefaultAvatar(self): - return C.DEFAULT_AVATAR_URL - - def registerWidget(self, wid): - log.debug(u"Registering %s" % wid.getDebugName()) - self.libervia_widgets.add(wid) - - def unregisterWidget(self, wid): - try: - self.libervia_widgets.remove(wid) - except KeyError: - log.warning(u'trying to remove a non registered Widget: %s' % wid.getDebugName()) - - def refresh(self): - """Refresh the general display.""" - self.contact_list.refresh() - for lib_wid in self.libervia_widgets: - lib_wid.refresh() - self.resize() - - def addWidget(self, wid, tab_index=None): - """ Add a widget at the bottom of the current or specified tab - - @param wid: LiberviaWidget to add - @param tab_index: index of the tab to add the widget to - """ - if tab_index is None or tab_index < 0 or tab_index >= self.tab_panel.getWidgetCount(): - panel = self.tab_panel.getCurrentPanel() - else: - panel = self.tab_panel.deck.getWidget(tab_index) - panel.addWidget(wid) - - def gotMenus(self, backend_menus): - """Put the menus data in cache and build the main menu bar - - @param backend_menus (list[tuple]): menu data from backend - """ - main_menu = self.panel.menu # most of global menu callbacks are in main_menu - - # Categories (with icons) - self.menus.addCategory(C.MENU_GLOBAL, [D_(u"General")], extra={'icon': 'home'}) - self.menus.addCategory(C.MENU_GLOBAL, [D_(u"Contacts")], extra={'icon': 'social'}) - self.menus.addCategory(C.MENU_GLOBAL, [D_(u"Groups")], extra={'icon': 'social'}) - #self.menus.addCategory(C.MENU_GLOBAL, [D_(u"Games")], extra={'icon': 'games'}) - - # menus to have before backend menus - self.menus.addMenu(C.MENU_GLOBAL, (D_(u"Groups"), D_(u"Discussion")), callback=main_menu.onJoinRoom) - - # menus added by the backend/plugins (include other types than C.MENU_GLOBAL) - self.menus.addMenus(backend_menus, top_extra={'icon': 'plugins'}) - - # menus to have under backend menus - self.menus.addMenu(C.MENU_GLOBAL, (D_(u"Contacts"), D_(u"Manage contact groups")), callback=main_menu.onManageContactGroups) - - # separator and right hand menus - self.menus.addMenuItem(C.MENU_GLOBAL, [], quick_menus.MenuSeparator()) - - self.menus.addMenu(C.MENU_GLOBAL, (D_(u"Help"), D_("Official chat room")), top_extra={'icon': 'help'}, callback=main_menu.onOfficialChatRoom) - self.menus.addMenu(C.MENU_GLOBAL, (D_(u"Help"), D_("Social contract")), top_extra={'icon': 'help'}, callback=main_menu.onSocialContract) - self.menus.addMenu(C.MENU_GLOBAL, (D_(u"Help"), D_("About")), callback=main_menu.onAbout) - self.menus.addMenu(C.MENU_GLOBAL, (D_(u"Settings"), D_("Account")), top_extra={'icon': 'settings'}, callback=main_menu.onAccount) - self.menus.addMenu(C.MENU_GLOBAL, (D_(u"Settings"), D_("Parameters")), callback=main_menu.onParameters) - # XXX: temporary, will change when a full profile will be managed in SàT - self.menus.addMenu(C.MENU_GLOBAL, (D_(u"Settings"), D_("Upload avatar")), callback=main_menu.onAvatarUpload) - - # we call listener to have menu added by local classes/plugins - self.callListeners('gotMenus') # FIXME: to be done another way or moved to quick_app - - # and finally the menus which must appear at the bottom - self.menus.addMenu(C.MENU_GLOBAL, (D_(u"General"), D_(u"Disconnect")), callback=main_menu.onDisconnect) - - # we can now display all the menus - main_menu.update(C.MENU_GLOBAL) - - # XXX: temp, will be reworked in the backed static blog plugin - self.menus.addMenu(C.MENU_JID_CONTEXT, (D_(u"User"), D_("Public blog")), callback=main_menu.onPublicBlog) - - def removeListener(self, type_, callback): - """Remove a callback from listeners - - @param type_: same as for [addListener] - @param callback: callback to remove - """ - # FIXME: workaround for pyjamas - # check KeyError issue - assert type_ in C.LISTENERS - try: - self._listeners[type_].pop(callback) - except KeyError: - pass - - def _getSessionMetadataCB(self, metadata): - if not metadata['plugged']: - warning = metadata.get("warning") - self.panel.setStyleAttribute("opacity", "0.25") # set background transparency - self._register_box = register.RegisterBox(self.logged, metadata) - self._register_box.centerBox() - self._register_box.show() - if warning: - dialog.InfoDialog(_('Security warning'), warning).show() - self._tryAutoConnect(skip_validation=not not warning) - else: - self._register.call('isConnected', self._isConnectedCB) - - def _isConnectedCB(self, connected): - if not connected: - self._register.call('connect', lambda x: self.logged()) - else: - self.logged() - - def logged(self): - self.panel.setStyleAttribute("opacity", "1") # background becomes foreground - if self._register_box: - self._register_box.hide() - del self._register_box # don't work if self._register_box is None - - # display the presence status panel and tab bar - self.presence_status_panel = main_panel.PresenceStatusPanel(self) - self.panel.addPresenceStatusPanel(self.presence_status_panel) - self.panel.tab_panel.getTabBar().setVisible(True) - - self.bridge_signals.getSignals(callback=self.bridge_signals.signalHandler, profile=None) - - def domain_cb(value): - self._defaultDomain = value - log.info(u"new account domain: %s" % value) - - def domain_eb(value): - self._defaultDomain = "libervia.org" - - self.bridge.getNewAccountDomain(callback=domain_cb, errback=domain_eb) - self.plug_profiles([C.PROF_KEY_NONE]) # XXX: None was used intitially, but pyjamas bug when using variable arguments and None is the only arg. - - def profilePlugged(self, dummy): - self._profile_plugged = True - QuickApp.profilePlugged(self, C.PROF_KEY_NONE) - contact_list = self.widgets.getOrCreateWidget(ContactList, None, on_new_widget=None, profile=C.PROF_KEY_NONE) - self.contact_list_widget = contact_list - self.panel.addContactList(contact_list) - - # FIXME: the contact list height has to be set manually the first time - self.resize() - - # XXX: as contact_list.update() is slow and it's called a lot of time - # during profile plugging, we prevent it before it's plugged - # and do all at once now - contact_list.update() - - try: - self.mblog_available = C.bool(self.features['XEP-0277']['available']) - except KeyError: - self.mblog_available = False - - try: - self.groupblog_available = C.bool(self.features['GROUPBLOG']['available']) - except KeyError: - self.groupblog_available = False - - blog_widget = self.displayWidget(blog.Blog, ()) - self.setSelected(blog_widget) - - if self.mblog_available: - if not self.groupblog_available: - dialog.InfoDialog(_(u"Group blogging not available"), _(u"Your server can manage (micro)blogging, but not fine permissions.<br />You'll only be able to blog publicly.")).show() - - else: - dialog.InfoDialog(_(u"Blogging not available"), _(u"Your server can't handle (micro)blogging.<br />You'll be able to see your contacts (micro)blogs, but not to post yourself.")).show() - - # we fill the panels already here - # for wid in self.widgets.getWidgets(blog.MicroblogPanel): - # if wid.accept_all(): - # self.bridge.getMassiveMblogs('ALL', (), None, profile=C.PROF_KEY_NONE, callback=wid.massiveInsert) - # else: - # self.bridge.getMassiveMblogs('GROUP', list(wid.accepted_groups), None, profile=C.PROF_KEY_NONE, callback=wid.massiveInsert) - - #we ask for our own microblogs: - # self.loadOurMainEntries() - - def gotDefaultMUC(default_muc): - self.default_muc = default_muc - self.bridge.mucGetDefaultService(profile=None, callback=gotDefaultMUC) - - def newWidget(self, wid): - log.debug(u"newWidget: {}".format(wid)) - self.addWidget(wid) - - def newMessageHandler(self, from_jid_s, msg, type_, to_jid_s, extra, profile=C.PROF_KEY_NONE): - if type_ == C.MESS_TYPE_HEADLINE: - from_jid = jid.JID(from_jid_s) - if from_jid.domain == self._defaultDomain: - # we display announcement from the server in a dialog for better visibility - try: - title = extra['subject'] - except KeyError: - title = _('Announcement from %s') % from_jid - msg = strings.addURLToText(html_tools.XHTML2Text(msg)) - dialog.InfoDialog(title, msg).show() - return - QuickApp.newMessageHandler(self, from_jid_s, msg, type_, to_jid_s, extra, profile) - - def disconnectedHandler(self, profile): - QuickApp.disconnectedHandler(self, profile) - Window.getLocation().reload() - - def setPresenceStatus(self, show='', status=None, profile=C.PROF_KEY_NONE): - self.presence_status_panel.setPresence(show) - if status is not None: - self.presence_status_panel.setStatus(status) - - def _tryAutoConnect(self, skip_validation=False): - """This method retrieve the eventual URL parameters to auto-connect the user. - @param skip_validation: if True, set the form values but do not validate it - """ - params = strings.getURLParams(Window.getLocation().getSearch()) - if "login" in params: - self._register_box._form.right_side.showStack(0) - self._register_box._form.login_box.setText(params["login"]) - self._register_box._form.login_pass_box.setFocus(True) - if "passwd" in params: - # try to connect - self._register_box._form.login_pass_box.setText(params["passwd"]) - if not skip_validation: - self._register_box._form.onLogin(None) - return True - else: - # this would eventually set the browser saved password - Timer(5, lambda: self._register_box._form.login_pass_box.setFocus(True)) - - def _actionManagerUnknownError(self): - dialog.InfoDialog("Error", - "Unmanaged action result", Width="400px").center() - - # def _ownBlogsFills(self, mblogs, mblog_panel=None): - # """Put our own microblogs in cache, then fill the panels with them. - - # @param mblogs (dict): dictionary mapping a publisher JID to blogs data. - # @param mblog_panel (MicroblogPanel): the panel to fill, or all if None. - # """ - # cache = [] - # for publisher in mblogs: - # for mblog in mblogs[publisher][0]: - # if 'content' not in mblog: - # log.warning(u"No content found in microblog [%s]" % mblog) - # continue - # if 'groups' in mblog: - # _groups = set(mblog['groups'].split() if mblog['groups'] else []) - # else: - # _groups = None - # mblog_entry = blog.MicroblogItem(mblog) - # cache.append((_groups, mblog_entry)) - - # self.mblog_cache.extend(cache) - # if len(self.mblog_cache) > MAX_MBLOG_CACHE: - # del self.mblog_cache[0:len(self.mblog_cache - MAX_MBLOG_CACHE)] - - # widget_list = [mblog_panel] if mblog_panel else self.widgets.getWidgets(blog.MicroblogPanel) - - # for wid in widget_list: - # self.fillMicroblogPanel(wid, cache) - - # # FIXME - - # if self.initialised: - # return - # self.initialised = True # initialisation phase is finished here - # for event_data in self.init_cache: # so we have to send all the cached events - # self.personalEventHandler(*event_data) - # del self.init_cache - - # def loadOurMainEntries(self, index=0, mblog_panel=None): - # """Load a page of our own blogs from the cache or ask them to the - # backend. Then fill the panels with them. - - # @param index (int): starting index of the blog page to retrieve. - # @param mblog_panel (MicroblogPanel): the panel to fill, or all if None. - # """ - # delta = index - self.next_rsm_index - # if delta < 0: - # assert mblog_panel is not None - # self.fillMicroblogPanel(mblog_panel, self.mblog_cache[index:index + C.RSM_MAX_ITEMS]) - # return - - # def cb(result): - # self._ownBlogsFills(result, mblog_panel) - - # rsm = {'max_': str(delta + C.RSM_MAX_ITEMS), 'index': str(self.next_rsm_index)} - # self.bridge.getMassiveMblogs('JID', [unicode(self.whoami.bare)], rsm, callback=cb, profile=C.PROF_KEY_NONE) - # self.next_rsm_index = index + C.RSM_MAX_ITEMS - - ## Signals callbacks ## - - # def personalEventHandler(self, sender, event_type, data): - # elif event_type == 'MICROBLOG_DELETE': - # for wid in self.widgets.getWidgets(blog.MicroblogPanel): - # wid.removeEntry(data['type'], data['id']) - - # if sender == self.whoami.bare and data['type'] == 'main_item': - # for index in xrange(0, len(self.mblog_cache)): - # entry = self.mblog_cache[index] - # if entry[1].id == data['id']: - # self.mblog_cache.remove(entry) - # break - - # def fillMicroblogPanel(self, mblog_panel, mblogs): - # """Fill a microblog panel with entries in cache - - # @param mblog_panel: MicroblogPanel instance - # """ - # #XXX: only our own entries are cached - # for cache_entry in mblogs: - # _groups, mblog_entry = cache_entry - # mblog_panel.addEntryIfAccepted(self.whoami.bare, *cache_entry) - - # def getEntityMBlog(self, entity): - # # FIXME: call this after a contact has been added to roster - # log.info(u"geting mblog for entity [%s]" % (entity,)) - # for lib_wid in self.libervia_widgets: - # if isinstance(lib_wid, blog.MicroblogPanel): - # if lib_wid.isJidAccepted(entity): - # self.bridge.call('getMassiveMblogs', lib_wid.massiveInsert, 'JID', [unicode(entity)]) - - def displayWidget(self, class_, target, dropped=False, new_tab=None, *args, **kwargs): - """Get or create a LiberviaWidget and select it. When the user dropped - something, a new widget is always created, otherwise we look for an - existing widget and re-use it if it's in the current tab. - - @arg class_(class): see quick_widgets.getOrCreateWidget - @arg target: see quick_widgets.getOrCreateWidget - @arg dropped(bool): if True, assume the widget has been dropped - @arg new_tab(unicode): if not None, it holds the name of a new tab to - open for the widget. If None, use the default behavior. - @param args(list): optional args to create a new instance of class_ - @param kwargs(list): optional kwargs to create a new instance of class_ - @return: the widget - """ - kwargs['profile'] = C.PROF_KEY_NONE - - if dropped: - kwargs['on_new_widget'] = None - kwargs['on_existing_widget'] = C.WIDGET_RECREATE - wid = self.widgets.getOrCreateWidget(class_, target, *args, **kwargs) - self.setSelected(wid) - return wid - - if new_tab: - kwargs['on_new_widget'] = None - kwargs['on_existing_widget'] = C.WIDGET_RECREATE - wid = self.widgets.getOrCreateWidget(class_, target, *args, **kwargs) - self.tab_panel.addWidgetsTab(new_tab) - self.addWidget(wid, tab_index=self.tab_panel.getWidgetCount() - 1) - return wid - - kwargs['on_existing_widget'] = C.WIDGET_RAISE - try: - wid = self.widgets.getOrCreateWidget(class_, target, *args, **kwargs) - except quick_widgets.WidgetAlreadyExistsError: - kwargs['on_existing_widget'] = C.WIDGET_KEEP - wid = self.widgets.getOrCreateWidget(class_, target, *args, **kwargs) - widgets_panel = wid.getParent(libervia_widget.WidgetsPanel, expect=False) - if widgets_panel is None: - # The widget exists but is hidden - self.addWidget(wid) - elif widgets_panel != self.tab_panel.getCurrentPanel(): - # the widget is on an other tab, so we add a new one here - kwargs['on_existing_widget'] = C.WIDGET_RECREATE - wid = self.widgets.getOrCreateWidget(class_, target, *args, **kwargs) - self.addWidget(wid) - self.setSelected(wid) - return wid - - def isHidden(self): - """Tells if the frontend window is hidden. - - @return bool - """ - return self.notification.isHidden() - - def updateAlertsCounter(self, extra_inc=0): - """Update the over whole alerts counter - - @param extra_inc (int): extra counter - """ - extra = self.alerts_counter.extra + extra_inc - self.alerts_counter.update(self.alerts_count, extra=extra) - - def _paramUpdate(self, name, value, category, refresh=True): - """This is called when the paramUpdate signal is received, but also - during initialization when the UI parameters values are retrieved. - @param refresh: set to True to refresh the general UI - """ - for param_cat, param_name in C.CACHED_PARAMS: - if name == param_name and category == param_cat: - self.cached_params[(category, name)] = value - if refresh: - self.refresh() - break - - def getCachedParam(self, category, name): - """Return a parameter cached value (e.g for refreshing the UI) - - @param category (unicode): the parameter category - @pram name (unicode): the parameter name - """ - return self.cached_params[(category, name)] if (category, name) in self.cached_params else None - - def sendError(self, errorData): - dialog.InfoDialog("Error while sending message", - "Your message can't be sent", Width="400px").center() - log.error("sendError: %s" % unicode(errorData)) - - def showWarning(self, type_=None, msg=None): - """Display a popup information message, e.g. to notify the recipient of a message being composed. - If type_ is None, a popup being currently displayed will be hidden. - @type_: a type determining the CSS style to be applied (see WarningPopup.showWarning) - @msg: message to be displayed - """ - if not hasattr(self, "warning_popup"): - self.warning_popup = main_panel.WarningPopup() - self.warning_popup.showWarning(type_, msg) - - def showDialog(self, message, title="", type_="info", answer_cb=None, answer_data=None): - if type_ == 'info': - popup = dialog.InfoDialog(unicode(title), unicode(message), callback=answer_cb) - elif type_ == 'error': - popup = dialog.InfoDialog(unicode(title), unicode(message), callback=answer_cb) - elif type_ == 'yes/no': - popup = dialog.ConfirmDialog(lambda answer: answer_cb(answer, answer_data), - text=unicode(message), title=unicode(title)) - popup.cancel_button.setText(_("No")) - else: - popup = dialog.InfoDialog(unicode(title), unicode(message), callback=answer_cb) - log.error(_('unmanaged dialog type: %s'), type_) - popup.show() - - def dialogFailure(self, failure): - dialog.InfoDialog("Error", - unicode(failure), Width="400px").center() - - def showFailure(self, err_data, msg=''): - """Show a failure that has been returned by an asynchronous bridge method. - - @param failure (defer.Failure): Failure instance - @param msg (unicode): message to display - """ - # FIXME: message is lost by JSON, we hardcode it for now... remove msg argument when possible - err_code, err_obj = err_data - title = err_obj['message']['faultString'] if isinstance(err_obj['message'], dict) else err_obj['message'] - self.showDialog(msg, title, 'error') - - def onJoinMUCFailure(self, err_data): - """Show a failure that has been returned when trying to join a room. - - @param failure (defer.Failure): Failure instance - """ - # FIXME: remove asap, see self.showFailure - err_code, err_obj = err_data - if err_obj["data"] == "AlreadyJoinedRoom": - msg = _(u"The room has already been joined.") - err_obj["message"] = _(u"Information") - else: - msg = _(u"Invalid room identifier. Please give a room short or full identifier like 'room' or '%s'.") % self.default_muc - err_obj["message"] = _(u"Error") - self.showFailure(err_data, msg) - - -if __name__ == '__main__': - app = SatWebFrontend() - app.onModuleLoad() - host_listener.callListeners(app)
--- a/src/browser/libervia_test.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,78 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2011-2018 Jérôme Poisson <goffi@goffi.org> - -# This program 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. - -# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. - - -# Just visit <root_url>/test. If you don't get any AssertError pop-up, -# everything is fine. #TODO: nicely display the results in HTML output. - - -### logging configuration ### -from sat_browser import logging -logging.configure() -from sat.core.log import getLogger -log = getLogger(__name__) -### - -from sat_frontends.tools import jid -from sat_browser import contact_list - - -def test_JID(): - """Check that the JID class reproduces the Twisted behavior""" - j1 = jid.JID("t1@test.org") - j1b = jid.JID("t1@test.org") - t1 = "t1@test.org" - - assert j1 == j1b - assert j1 != t1 - assert t1 != j1 - assert hash(j1) == hash(j1b) - assert hash(j1) != hash(t1) - - -def test_JIDIterable(): - """Check that our iterables reproduce the Twisted behavior""" - - j1 = jid.JID("t1@test.org") - j1b = jid.JID("t1@test.org") - j2 = jid.JID("t2@test.org") - t1 = "t1@test.org" - t2 = "t2@test.org" - jid_set = set([j1, t2]) - jid_list = contact_list.JIDList([j1, t2]) - jid_dict = {j1: "dummy 1", t2: "dummy 2"} - for iterable in (jid_set, jid_list, jid_dict): - log.info("Testing %s" % type(iterable)) - assert j1 in iterable - assert j1b in iterable - assert j2 not in iterable - assert t1 not in iterable - assert t2 in iterable - - # Check that the extra JIDList class is still needed - log.info("Testing Pyjamas native list") - jid_native_list = ([j1, t2]) - assert j1 in jid_native_list - assert j1b not in jid_native_list # this is NOT Twisted's behavior - assert j2 in jid_native_list # this is NOT Twisted's behavior - assert t1 in jid_native_list # this is NOT Twisted's behavior - assert t2 in jid_native_list - -test_JID() -test_JIDIterable()
--- a/src/browser/otr.min.js Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,42 +0,0 @@ -/* - - This file contains minified versions of otr.js, Big Integer Library, CryptoJS, EventEmitter. - It is sublicensed under AGPL v3 (or any later version) as allowed by the original licenses. - - Below some information about the authors and the licences of the included libraries: - - Big Integer Library v. 5.5 - Created 2000, last modified 2013 - Leemon Baird - www.leemon.com - Original licence information: - // This file is public domain. You can use it for any purpose without restriction. - // I do not guarantee that it is correct, so use it at your own risk. If you use - // it for something interesting, I'd appreciate hearing about it. If you find - // any bugs or make any improvements, I'd appreciate hearing about those too. - // It would also be nice if my name and URL were left in the comments. But none - // of that is required. - - CryptoJS v3.1.2 - code.google.com/p/crypto-js - (c) 2009-2013 by Jeff Mott. All rights reserved. - code.google.com/p/crypto-js/wiki/License (MIT licence) - - EventEmitter v4.2.3 - git.io/ee - Oliver Caldwell - MIT license - - otr.js v0.2.12 - 2014-04-15 - (c) 2014 - Arlo Breault <arlolra@gmail.com> - Freely distributed under the MPL v2.0 license. - -*/ - -if (typeof crypto !== 'undefined' && (typeof crypto.randomBytes === 'function' || typeof crypto.getRandomValues === 'function')) { - -(function(a,b){if(typeof define==="function"&&define.amd){define(b.bind(a,a.crypto||a.msCrypto))}else{if(typeof module!=="undefined"&&module.exports){module.exports=b(require("crypto"))}else{a.BigInt=b(a.crypto||a.msCrypto)}}}(this,function(v){var x=26;var E=1<<x;var aw=E-1;var bd="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_=!@#$%^&*()[]{}|;:,.<>/?`~ \\'\"+-";var o=a0(1,1,1);var W=new Array(0);var aT=W;var n=W;var k=W;var j=W;var i=W;var h=W,g=W;var f=W;var e=W;var ar=W;var a2=W;var aR=W,F=W,R=W;var S=W,V=W,aG=W,aE=W,aC=W,aA=W;var ah=W,ag=W,af=W,aL=W,P=W,O=W,aH=W;var N=W,al=W,aa=W,aQ=W,ao=W,bf=W,U=W,a8=W;var ae=W,H=W,X=W,ad=W,ab=W,M=W,L=W,aW=W;var a7=W;function ba(bl){var T,bj,bk,t;bj=new Array(bl);for(T=0;T<bl;T++){bj[T]=0}bj[0]=2;bk=0;for(;bj[bk]<bl;){for(T=bj[bk]*bj[bk];T<bl;T+=bj[bk]){bj[T]=1}bk++;bj[bk]=bj[bk-1]+1;for(;bj[bk]<bl&&bj[bj[bk]];bj[bk]++){}}t=new Array(bk);for(T=0;T<bk;T++){t[T]=bj[T]}return t}function l(T,t){if(aR.length!=T.length){aR=K(T);F=K(T);R=K(T)}y(R,t);return aN(T,R)}function aN(T,t){var bl,bk,bj,bm;if(aR.length!=T.length){aR=K(T);F=K(T);R=K(T)}ax(R,t);ax(F,T);ax(aR,T);at(F,-1);at(aR,-1);if(aZ(F)){return 0}for(bj=0;F[bj]==0;bj++){}for(bl=1,bk=2;F[bj]%bk==0;bk*=2,bl++){}bm=bj*x+bl-1;if(bm){G(F,bm)}aS(R,F,T);if(!z(R,1)&&!Q(R,aR)){bk=1;while(bk<=bm-1&&!Q(R,aR)){aJ(R,T);if(z(R,1)){return 0}bk++}if(!Q(R,aR)){return 0}}return 1}function ay(t){var bj,bk,T;for(bj=t.length-1;(t[bj]==0)&&(bj>0);bj--){}for(bk=0,T=t[bj];T;(T>>=1),bk++){}bk+=x*bj;return bk}function aD(t,bj){var T=a0(0,(t.length>bj?t.length:bj)*x,0);ax(T,t);return T}function ak(T){var t=a0(0,T,0);aq(t,T);return a4(t,1)}function D(t){if(t>=600){return q(t,2)}if(t>=550){return q(t,4)}if(t>=500){return q(t,5)}if(t>=400){return q(t,6)}if(t>=350){return q(t,7)}if(t>=300){return q(t,9)}if(t>=250){return q(t,12)}if(t>=200){return q(t,15)}if(t>=150){return q(t,18)}if(t>=100){return q(t,27)}return q(t,40)}function q(bj,bm){var T,bk,t,bl;bl=30000;T=a0(0,bj,0);if(N.length==0){N=ba(30000)}if(a7.length!=T.length){a7=K(T)}for(;;){a1(T,bj,0);T[0]|=1;t=0;for(bk=0;(bk<N.length)&&(N[bk]<=bl);bk++){if(be(T,N[bk])==0&&!z(T,N[bk])){t=1;break}}for(bk=0;bk<bm&&!t;bk++){a1(a7,bj,0);while(!B(T,a7)){a1(a7,bj,0)}if(!aN(T,a7)){t=1}}if(!t){return T}}}function b(t,bj){var T=K(t);aB(T,bj);return a4(T,1)}function p(t,bj){var T=aD(t,t.length+1);at(T,bj);return a4(T,1)}function d(t,bj){var T=aD(t,t.length+bj.length);aY(T,bj);return a4(T,1)}function aP(t,bk,bj){var T=aD(t,bj.length);aS(T,a4(bk,2),a4(bj,2),0);return a4(T,1)}function a5(t,bj){var T=aD(t,(t.length>bj.length?t.length+1:bj.length+1));bc(T,bj);return a4(T,1)}function w(t,bj){var T=aD(t,(t.length>bj.length?t.length+1:bj.length+1));aI(T,bj);return a4(T,1)}function bg(t,bk){var T=aD(t,bk.length);var bj;bj=Z(T,bk);return bj?a4(T,1):null}function an(t,bk,bj){var T=aD(t,bj.length);aU(T,bk,bj);return a4(T,1)}function aq(bt,bm){var br,bu,bk,bl,bv,bn,t,T,bp,bq,bj,bo,bs;if(N.length==0){N=ba(30000)}if(al.length==0){al=new Array(512);for(bn=0;bn<512;bn++){al[bn]=Math.pow(2,bn/511-1)}}br=0.1;bk=20;bs=20;if(aQ.length!=bt.length){aQ=K(bt);ao=K(bt);a8=K(bt);H=K(bt);ab=K(bt);M=K(bt);L=K(bt);ad=K(bt);X=K(bt);aa=K(bt);bf=K(bt);U=K(bt);ae=K(bt);aW=K(bt)}if(bm<=bs){bl=(1<<((bm+2)>>1))-1;y(bt,0);for(bv=1;bv;){bv=0;bt[0]=1|(1<<(bm-1))|aV(bm);for(bn=1;(bn<N.length)&&((N[bn]&bl)==N[bn]);bn++){if(0==(bt[0]%N[bn])){bv=1;break}}}Y(bt);return}T=br*bm*bm;if(bm>2*bk){for(t=1;bm-bm*t<=bk;){t=al[aV(9)]}}else{t=0.5}bo=Math.floor(t*bm)+1;aq(U,bo);y(aQ,0);aQ[Math.floor((bm-2)/x)]|=(1<<((bm-2)%x));c(aQ,U,aa,bf);bq=ay(aa);for(;;){for(;;){a1(ao,bq,0);if(B(aa,ao)){break}}at(ao,1);aI(ao,aa);ax(X,U);aY(X,ao);m(X,2);at(X,1);ax(H,ao);m(H,2);for(bp=0,bn=0;(bn<N.length)&&(N[bn]<T);bn++){if(be(X,N[bn])==0&&!z(X,N[bn])){bp=1;break}}if(!bp){if(!l(X,2)){bp=1}}if(!bp){at(X,-3);for(bn=X.length-1;(X[bn]==0)&&(bn>0);bn--){}for(bj=0,bu=X[bn];bu;(bu>>=1),bj++){}bj+=x*bn;for(;;){a1(ae,bj,0);if(B(X,ae)){break}}at(X,3);at(ae,2);ax(ad,ae);ax(a8,X);at(a8,-1);aS(ad,a8,X);at(ad,-1);if(aZ(ad)){ax(ad,ae);aS(ad,H,X);at(ad,-1);ax(aW,X);ax(ab,ad);A(ab,X);if(z(ab,1)){ax(bt,aW);return}}}}}function aO(bk,bj){var T,t;T=Math.floor((bk-1)/x)+2;t=a0(0,0,T);a1(t,bk,bj);return t}function a1(t,bl,bk){var bj,T;for(bj=0;bj<t.length;bj++){t[bj]=0}T=Math.floor((bl-1)/x)+1;for(bj=0;bj<T;bj++){t[bj]=aV(x)}t[T-1]&=(2<<((bl-1)%x))-1;if(bk==1){t[T-1]|=(1<<((bl-1)%x))}}function ap(t,bk){var T,bj;T=K(t);bj=K(bk);A(T,bj);return T}function A(br,bq){var bp,bo,T,bn,bm,bl,t,bk,bj,bs;if(ar.length!=br.length){ar=K(br)}bj=1;while(bj){bj=0;for(bp=1;bp<bq.length;bp++){if(bq[bp]){bj=1;break}}if(!bj){break}for(bp=br.length;!br[bp]&&bp>=0;bp--){}bo=br[bp];T=bq[bp];bn=1;bm=0;bl=0;t=1;while((T+bl)&&(T+t)){bk=Math.floor((bo+bn)/(T+bl));bs=Math.floor((bo+bm)/(T+t));if(bk!=bs){break}W=bn-bk*bl;bn=bl;bl=W;W=bm-bk*t;bm=t;t=W;W=bo-bk*T;bo=T;T=W}if(bm){ax(ar,br);J(br,bq,bn,bm);J(bq,ar,t,bl)}else{aB(br,bq);ax(ar,br);ax(br,bq);ax(bq,ar)}}if(bq[0]==0){return}W=be(br,bq[0]);y(br,bq[0]);bq[0]=W;while(bq[0]){br[0]%=bq[0];W=br[0];br[0]=bq[0];bq[0]=W}}function Z(t,bj){var T=1+2*Math.max(t.length,bj.length);if(!(t[0]&1)&&!(bj[0]&1)){y(t,0);return 0}if(V.length!=T){V=new Array(T);S=new Array(T);aG=new Array(T);aE=new Array(T);aC=new Array(T);aA=new Array(T)}ax(V,t);ax(S,bj);y(aG,1);y(aE,0);y(aC,0);y(aA,1);for(;;){while(!(V[0]&1)){s(V);if(!(aG[0]&1)&&!(aE[0]&1)){s(aG);s(aE)}else{aI(aG,bj);s(aG);bc(aE,t);s(aE)}}while(!(S[0]&1)){s(S);if(!(aC[0]&1)&&!(aA[0]&1)){s(aC);s(aA)}else{aI(aC,bj);s(aC);bc(aA,t);s(aA)}}if(!B(S,V)){bc(V,S);bc(aG,aC);bc(aE,aA)}else{bc(S,V);bc(aC,aG);bc(aA,aE)}if(z(V,0)){while(r(aC)){aI(aC,bj)}ax(t,aC);if(!z(S,1)){y(t,0);return 0}return 1}}}function aX(bj,bm){var bk=1,T=0,bl;for(;;){if(bj==1){return bk}if(bj==0){return 0}T-=bk*Math.floor(bm/bj);bm%=bj;if(bm==1){return T}if(bm==0){return 0}bk-=T*Math.floor(bj/bm);bj%=bm}}function C(t,T){return aX(t,T)}function u(T,bn,bl,bj,t){var bm=0;var bk=Math.max(T.length,bn.length);if(V.length!=bk){V=new Array(bk);aG=new Array(bk);aE=new Array(bk);aC=new Array(bk);aA=new Array(bk)}while(!(T[0]&1)&&!(bn[0]&1)){s(T);s(bn);bm++}ax(V,T);ax(bl,bn);y(aG,1);y(aE,0);y(aC,0);y(aA,1);for(;;){while(!(V[0]&1)){s(V);if(!(aG[0]&1)&&!(aE[0]&1)){s(aG);s(aE)}else{aI(aG,bn);s(aG);bc(aE,T);s(aE)}}while(!(bl[0]&1)){s(bl);if(!(aC[0]&1)&&!(aA[0]&1)){s(aC);s(aA)}else{aI(aC,bn);s(aC);bc(aA,T);s(aA)}}if(!B(bl,V)){bc(V,bl);bc(aG,aC);bc(aE,aA)}else{bc(bl,V);bc(aC,aG);bc(aA,aE)}if(z(V,0)){while(r(aC)){aI(aC,bn);bc(aA,T)}m(aA,-1);ax(bj,aC);ax(t,aA);bi(bl,bm);return}}}function r(t){return((t[t.length-1]>>(x-1))&1)}function a(t,bn,T){var bl,bm=t.length,bk=bn.length;var bj=((bm+T)<bk)?(bm+T):bk;for(bl=bk-1-T;bl<bm&&bl>=0;bl++){if(t[bl]>0){return 1}}for(bl=bm-1+T;bl<bk;bl++){if(bn[bl]>0){return 0}}for(bl=bj-1;bl>=T;bl--){if(t[bl-T]>bn[bl]){return 1}else{if(t[bl-T]<bn[bl]){return 0}}}return 0}function B(t,bk){var bj;var T=(t.length<bk.length)?t.length:bk.length;for(bj=t.length;bj<bk.length;bj++){if(bk[bj]){return 0}}for(bj=bk.length;bj<t.length;bj++){if(t[bj]){return 1}}for(bj=T-1;bj>=0;bj--){if(t[bj]>bk[bj]){return 1}else{if(t[bj]<bk[bj]){return 0}}}return 0}function c(bt,bq,T,t){var bm,bl;var bk,bj,bs,bp,bn,br,bo;ax(t,bt);for(bl=bq.length;bq[bl-1]==0;bl--){}bo=bq[bl-1];for(br=0;bo;br++){bo>>=1}br=x-br;bi(bq,br);bi(t,br);for(bm=t.length;t[bm-1]==0&&bm>bl;bm--){}y(T,0);while(!a(bq,t,bm-bl)){az(t,bq,bm-bl);T[bm-bl]++}for(bk=bm-1;bk>=bl;bk--){if(t[bk]==bq[bl-1]){T[bk-bl]=aw}else{T[bk-bl]=Math.floor((t[bk]*E+t[bk-1])/bq[bl-1])}for(;;){bp=(bl>1?bq[bl-2]:0)*T[bk-bl];bn=bp;bp=bp&aw;bn=(bn-bp)/E;bs=bn+T[bk-bl]*bq[bl-1];bn=bs;bs=bs&aw;bn=(bn-bs)/E;if(bn==t[bk]?bs==t[bk-1]?bp>(bk>1?t[bk-2]:0):bs>t[bk-1]:bn>t[bk]){T[bk-bl]--}else{break}}av(t,bq,-T[bk-bl],bk-bl);if(r(t)){a9(t,bq,bk-bl);T[bk-bl]--}}G(bq,br);G(t,br)}function Y(T){var bk,bj,bl,t;bj=T.length;bl=0;for(bk=0;bk<bj;bk++){bl+=T[bk];t=0;if(bl<0){t=bl&aw;t=-((bl-t)/E);bl+=t*E}T[bk]=bl&aw;bl=((bl-T[bk])/E)-t}}function be(t,bk){var T,bj=0;for(T=t.length-1;T>=0;T--){bj=(bj*E+t[T])%bk}return bj}function a0(bk,bl,bm){var bj,T,bn;T=Math.ceil(bl/x)+1;T=bm>T?bm:T;bn=new Array(T);y(bn,bk);return bn}function a6(bt,bk,bj){var bp,bn,bm,bs,br,T;var bl=bt.length;if(bk==-1){bs=new Array(0);for(;;){br=new Array(bs.length+1);for(bn=0;bn<bs.length;bn++){br[bn+1]=bs[bn]}br[0]=parseInt(bt,10);bs=br;bp=bt.indexOf(",",0);if(bp<1){break}bt=bt.substring(bp+1);if(bt.length==0){break}}if(bs.length<bj){br=new Array(bj);ax(br,bs);return br}return bs}var bo=bk,t=0;var bq=bk==1?bl:0;while(bo>1){if(bo&1){t=1}bq+=bl;bo>>=1}bq+=t*bl;bs=a0(0,bq,0);for(bn=0;bn<bl;bn++){bp=bd.indexOf(bt.substring(bn,bn+1),0);if(bk<=36&&bp>=36){bp-=26}if(bp>=bk||bp<0){break}m(bs,bk);at(bs,bp)}for(bl=bs.length;bl>0&&!bs[bl-1];bl--){}bl=bj>bl+1?bj:bl+1;br=new Array(bl);T=bl<bs.length?bl:bs.length;for(bn=0;bn<T;bn++){br[bn]=bs[bn]}for(;bn<bl;bn++){br[bn]=0}return br}function z(t,bj){var T;if(t[0]!=bj){return 0}for(T=1;T<t.length;T++){if(t[T]){return 0}}return 1}function Q(t,bk){var bj;var T=t.length<bk.length?t.length:bk.length;for(bj=0;bj<T;bj++){if(t[bj]!=bk[bj]){return 0}}if(t.length>bk.length){for(;bj<t.length;bj++){if(t[bj]){return 0}}}else{for(;bj<bk.length;bj++){if(bk[bj]){return 0}}}return 1}function aZ(t){var T;for(T=0;T<t.length;T++){if(t[T]){return 0}}return 1}function aM(T,bm){var bk,bj,bl="";if(f.length!=T.length){f=K(T)}else{ax(f,T)}if(bm==-1){for(bk=T.length-1;bk>0;bk--){bl+=T[bk]+","}bl+=T[0]}else{while(!aZ(f)){bj=ac(f,bm);bl=bd.substring(bj,bj+1)+bl}}if(bl.length==0){bl="0"}return bl}function K(t){var T,bj;bj=new Array(t.length);ax(bj,t);return bj}function ax(t,bk){var bj;var T=t.length<bk.length?t.length:bk.length;for(bj=0;bj<T;bj++){t[bj]=bk[bj]}for(bj=T;bj<t.length;bj++){t[bj]=0}}function y(t,bk){var T,bj;for(bj=bk,T=0;T<t.length;T++){t[T]=bj&aw;bj>>=x}}function at(T,bm){var bk,bj,bl,t;T[0]+=bm;bj=T.length;bl=0;for(bk=0;bk<bj;bk++){bl+=T[bk];t=0;if(bl<0){t=bl&aw;t=-((bl-t)/E);bl+=t*E}T[bk]=bl&aw;bl=((bl-T[bk])/E)-t;if(!bl){return}}}function G(t,bk){var bj;var T=Math.floor(bk/x);if(T){for(bj=0;bj<t.length-T;bj++){t[bj]=t[bj+T]}for(;bj<t.length;bj++){t[bj]=0}bk%=x}for(bj=0;bj<t.length-1;bj++){t[bj]=aw&((t[bj+1]<<(x-bk))|(t[bj]>>bk))}t[bj]>>=bk}function s(t){var T;for(T=0;T<t.length-1;T++){t[T]=aw&((t[T+1]<<(x-1))|(t[T]>>1))}t[T]=(t[T]>>1)|(t[T]&(E>>1))}function bi(t,bk){var bj;var T=Math.floor(bk/x);if(T){for(bj=t.length;bj>=T;bj--){t[bj]=t[bj-T]}for(;bj>=0;bj--){t[bj]=0}bk%=x}if(!bk){return}for(bj=t.length-1;bj>0;bj--){t[bj]=aw&((t[bj]<<bk)|(t[bj-1]>>(x-bk)))}t[bj]=aw&(t[bj]<<bk)}function m(T,bm){var bk,bj,bl,t;if(!bm){return}bj=T.length;bl=0;for(bk=0;bk<bj;bk++){bl+=T[bk]*bm;t=0;if(bl<0){t=bl&aw;t=-((bl-t)/E);bl+=t*E}T[bk]=bl&aw;bl=((bl-T[bk])/E)-t}}function ac(t,bl){var T,bk=0,bj;for(T=t.length-1;T>=0;T--){bj=bk*E+t[T];t[T]=Math.floor(bj/bl);bk=bj%bl}return bk}function J(T,bo,bj,t){var bl,bn,bk,bm;bk=T.length<bo.length?T.length:bo.length;bm=T.length;for(bn=0,bl=0;bl<bk;bl++){bn+=bj*T[bl]+t*bo[bl];T[bl]=bn&aw;bn=(bn-T[bl])/E}for(bl=bk;bl<bm;bl++){bn+=bj*T[bl];T[bl]=bn&aw;bn=(bn-T[bl])/E}}function av(T,bo,t,bl){var bk,bn,bj,bm;bj=T.length<bl+bo.length?T.length:bl+bo.length;bm=T.length;for(bn=0,bk=bl;bk<bj;bk++){bn+=T[bk]+t*bo[bk-bl];T[bk]=bn&aw;bn=(bn-T[bk])/E}for(bk=bj;bn&&bk<bm;bk++){bn+=T[bk];T[bk]=bn&aw;bn=(bn-T[bk])/E}}function a9(t,bn,bk){var bj,bm,T,bl;T=t.length<bk+bn.length?t.length:bk+bn.length;bl=t.length;for(bm=0,bj=bk;bj<T;bj++){bm+=t[bj]+bn[bj-bk];t[bj]=bm&aw;bm=(bm-t[bj])/E}for(bj=T;bm&&bj<bl;bj++){bm+=t[bj];t[bj]=bm&aw;bm=(bm-t[bj])/E}}function az(t,bn,bk){var bj,bm,T,bl;T=t.length<bk+bn.length?t.length:bk+bn.length;bl=t.length;for(bm=0,bj=bk;bj<T;bj++){bm+=t[bj]-bn[bj-bk];t[bj]=bm&aw;bm=(bm-t[bj])/E}for(bj=T;bm&&bj<bl;bj++){bm+=t[bj];t[bj]=bm&aw;bm=(bm-t[bj])/E}}function bc(t,bm){var bj,bl,T,bk;T=t.length<bm.length?t.length:bm.length;for(bl=0,bj=0;bj<T;bj++){bl+=t[bj]-bm[bj];t[bj]=bl&aw;bl=(bl-t[bj])/E}for(bj=T;bl&&bj<t.length;bj++){bl+=t[bj];t[bj]=bl&aw;bl=(bl-t[bj])/E}}function aI(t,bm){var bj,bl,T,bk;T=t.length<bm.length?t.length:bm.length;for(bl=0,bj=0;bj<T;bj++){bl+=t[bj]+bm[bj];t[bj]=bl&aw;bl=(bl-t[bj])/E}for(bj=T;bl&&bj<t.length;bj++){bl+=t[bj];t[bj]=bl&aw;bl=(bl-t[bj])/E}}function aY(t,bj){var T;if(aT.length!=2*t.length){aT=new Array(2*t.length)}y(aT,0);for(T=0;T<bj.length;T++){if(bj[T]){av(aT,t,bj[T],T)}}ax(t,aT)}function aB(t,T){if(h.length!=t.length){h=K(t)}else{ax(h,t)}if(g.length!=t.length){g=K(t)}c(h,T,g,t)}function aU(t,bk,bj){var T;if(n.length!=2*t.length){n=new Array(2*t.length)}y(n,0);for(T=0;T<bk.length;T++){if(bk[T]){av(n,t,bk[T],T)}}aB(n,bj);ax(t,n)}function aJ(bo,t){var bk,bj,bm,bn,bl,bp,T;for(bl=bo.length;bl>0&&!bo[bl-1];bl--){}T=bl>t.length?2*bl:2*t.length;if(n.length!=T){n=new Array(T)}y(n,0);for(bk=0;bk<bl;bk++){bn=n[2*bk]+bo[bk]*bo[bk];n[2*bk]=bn&aw;bn=(bn-n[2*bk])/E;for(bj=bk+1;bj<bl;bj++){bn=n[bk+bj]+2*bo[bk]*bo[bj]+bn;n[bk+bj]=(bn&aw);bn=(bn-n[bk+bj])/E}n[bk+bl]=bn}aB(n,t);ax(bo,n)}function a4(t,T){var bj,bk;for(bj=t.length;bj>0&&!t[bj-1];bj--){}bk=new Array(bj+T);ax(bk,t);return bk}function aS(t,bn,bm){var bl,bk,T,bj;if(e.length!=bm.length){e=K(bm)}if((bm[0]&1)==0){ax(e,t);y(t,1);while(!z(bn,0)){if(bn[0]&1){aU(t,e,bm)}ac(bn,2);aJ(e,bm)}return}y(e,0);for(T=bm.length;T>0&&!bm[T-1];T--){}bj=E-aX(be(bm,E),E);e[T]=1;aU(t,e,bm);if(i.length!=t.length){i=K(t)}else{ax(i,t)}for(bl=bn.length-1;bl>0&!bn[bl];bl--){}if(bn[bl]==0){y(t,1);return}for(bk=1<<(x-1);bk&&!(bn[bl]&bk);bk>>=1){}for(;;){if(!(bk>>=1)){bl--;if(bl<0){au(t,o,bm,bj);return}bk=1<<(x-1)}au(t,t,bm,bj);if(bk&bn[bl]){au(t,i,bm,bj)}}}function au(br,bo,T,bs){var bk,bj,bn,bp,bt,bl,bq;var bu=T.length;var bm=bo.length;if(a2.length!=bu){a2=new Array(bu)}y(a2,0);for(;bu>0&&T[bu-1]==0;bu--){}for(;bm>0&&bo[bm-1]==0;bm--){}bq=a2.length-1;for(bk=0;bk<bu;bk++){bt=a2[0]+br[bk]*bo[0];bp=((bt&aw)*bs)&aw;bn=(bt+bp*T[0]);bn=(bn-(bn&aw))/E;bt=br[bk];bj=1;for(;bj<bm-4;){bn+=a2[bj]+bp*T[bj]+bt*bo[bj];bl=a2[bj-1]=bn&aw;bn=(bn-bl)/E;bj++;bn+=a2[bj]+bp*T[bj]+bt*bo[bj];bl=a2[bj-1]=bn&aw;bn=(bn-bl)/E;bj++;bn+=a2[bj]+bp*T[bj]+bt*bo[bj];bl=a2[bj-1]=bn&aw;bn=(bn-bl)/E;bj++;bn+=a2[bj]+bp*T[bj]+bt*bo[bj];bl=a2[bj-1]=bn&aw;bn=(bn-bl)/E;bj++;bn+=a2[bj]+bp*T[bj]+bt*bo[bj];bl=a2[bj-1]=bn&aw;bn=(bn-bl)/E;bj++}for(;bj<bm;){bn+=a2[bj]+bp*T[bj]+bt*bo[bj];bl=a2[bj-1]=bn&aw;bn=(bn-bl)/E;bj++}for(;bj<bu-4;){bn+=a2[bj]+bp*T[bj];bl=a2[bj-1]=bn&aw;bn=(bn-bl)/E;bj++;bn+=a2[bj]+bp*T[bj];bl=a2[bj-1]=bn&aw;bn=(bn-bl)/E;bj++;bn+=a2[bj]+bp*T[bj];bl=a2[bj-1]=bn&aw;bn=(bn-bl)/E;bj++;bn+=a2[bj]+bp*T[bj];bl=a2[bj-1]=bn&aw;bn=(bn-bl)/E;bj++;bn+=a2[bj]+bp*T[bj];bl=a2[bj-1]=bn&aw;bn=(bn-bl)/E;bj++}for(;bj<bu;){bn+=a2[bj]+bp*T[bj];bl=a2[bj-1]=bn&aw;bn=(bn-bl)/E;bj++}for(;bj<bq;){bn+=a2[bj];bl=a2[bj-1]=bn&aw;bn=(bn-bl)/E;bj++}a2[bj-1]=bn&aw}if(!B(T,a2)){bc(a2,T)}ax(br,a2)}function bb(t,bj,T){return an(t,bg(bj,T),T)}function aj(T,t,bj){T=b(T,bj);t=b(t,bj);if(B(t,T)){T=w(T,bj)}return a5(T,t)}function I(bj){var T=Math.floor(bj/x)+2;var bl=new Array(T);for(var bk=0;bk<T;bk++){bl[bk]=0}bl[T-2]=1<<(bj%x);return bl}var bh=(function(){var t=0,T={};for(;t<256;++t){T[t]=String.fromCharCode(t)}return T}());function ai(t,T){T||(T=0);t=K(t);var bj="";while(!aZ(t)){bj=bh[t[0]&255]+bj;G(t,8)}while(bj.length<T){bj="\x00"+bj}return bj}function aK(T){var t=a6("0",10,T.length);T.forEach(function(bk,bj){if(bj){bi(t,8)}t[0]|=bk});return t}var a3=(function(){if(typeof v!=="undefined"&&typeof v.randomBytes==="function"){return function(bj){try{var t=v.randomBytes(bj)}catch(T){throw T}return Array.prototype.slice.call(t,0)}}else{if(typeof v!=="undefined"&&typeof v.getRandomValues==="function"){return function(T){var t=new Uint8Array(T);v.getRandomValues(t);return Array.prototype.slice.call(t,0)}}else{throw new Error("Keys should not be generated without CSPRNG.")}}}());function aF(){return a3(40)}function am(){return a3(1)[0]}function aV(bj){if(bj>31){throw new Error("Too many bits.")}var bk=0,bl=0;var t=Math.floor(bj/8);var T=(1<<(bj%8))-1;if(T){bl=am()&T}for(;bk<t;bk++){bl=(256*bl)+am()}return bl}return{str2bigInt:a6,bigInt2str:aM,int2bigInt:a0,multMod:an,powMod:aP,inverseMod:bg,randBigInt:aO,randBigInt_:a1,equals:Q,equalsInt:z,sub:a5,mod:b,modInt:be,mult:d,divInt_:ac,rightShift_:G,dup:K,greater:B,add:w,isZero:aZ,bitSize:ay,millerRabin:aN,divide_:c,trim:a4,primes:N,findPrimes:ba,getSeed:aF,divMod:bb,subMod:aj,twoToThe:I,bigInt2bits:ai,ba2bigInt:aK}}));(function(a,b){if(typeof define==="function"&&define.amd){define(b)}else{if(typeof module!=="undefined"&&module.exports){module.exports=b()}else{a.CryptoJS=b()}}}(this,function(){var a=a||(function(f,h){var b={};var c=b.lib={};var k=c.Base=(function(){function o(){}return{extend:function(q){o.prototype=this;var p=new o();if(q){p.mixIn(q)}if(!p.hasOwnProperty("init")){p.init=function(){p.$super.init.apply(this,arguments)}}p.init.prototype=p;p.$super=this;return p},create:function(){var p=this.extend();p.init.apply(p,arguments);return p},init:function(){},mixIn:function(q){for(var p in q){if(q.hasOwnProperty(p)){this[p]=q[p]}}if(q.hasOwnProperty("toString")){this.toString=q.toString}},clone:function(){return this.init.prototype.extend(this)}}}());var m=c.WordArray=k.extend({init:function(p,o){p=this.words=p||[];if(o!=h){this.sigBytes=o}else{this.sigBytes=p.length*4}},toString:function(o){return(o||i).stringify(this)},concat:function(u){var r=this.words;var q=u.words;var o=this.sigBytes;var t=u.sigBytes;this.clamp();if(o%4){for(var s=0;s<t;s++){var p=(q[s>>>2]>>>(24-(s%4)*8))&255;r[(o+s)>>>2]|=p<<(24-((o+s)%4)*8)}}else{if(q.length>65535){for(var s=0;s<t;s+=4){r[(o+s)>>>2]=q[s>>>2]}}else{r.push.apply(r,q)}}this.sigBytes+=t;return this},clamp:function(){var p=this.words;var o=this.sigBytes;p[o>>>2]&=4294967295<<(32-(o%4)*8);p.length=f.ceil(o/4)},clone:function(){var o=k.clone.call(this);o.words=this.words.slice(0);return o},random:function(q){var p=[];for(var o=0;o<q;o+=4){p.push((f.random()*4294967296)|0)}return new m.init(p,q)}});var n=b.enc={};var i=n.Hex={stringify:function(q){var s=q.words;var p=q.sigBytes;var r=[];for(var o=0;o<p;o++){var t=(s[o>>>2]>>>(24-(o%4)*8))&255;r.push((t>>>4).toString(16));r.push((t&15).toString(16))}return r.join("")},parse:function(q){var o=q.length;var r=[];for(var p=0;p<o;p+=2){r[p>>>3]|=parseInt(q.substr(p,2),16)<<(24-(p%8)*4)}return new m.init(r,o/2)}};var e=n.Latin1={stringify:function(r){var s=r.words;var q=r.sigBytes;var o=[];for(var p=0;p<q;p++){var t=(s[p>>>2]>>>(24-(p%4)*8))&255;o.push(String.fromCharCode(t))}return o.join("")},parse:function(q){var o=q.length;var r=[];for(var p=0;p<o;p++){r[p>>>2]|=(q.charCodeAt(p)&255)<<(24-(p%4)*8)}return new m.init(r,o)}};var d=n.Utf8={stringify:function(o){try{return decodeURIComponent(escape(e.stringify(o)))}catch(p){throw new Error("Malformed UTF-8 data")}},parse:function(o){return e.parse(unescape(encodeURIComponent(o)))}};var j=c.BufferedBlockAlgorithm=k.extend({reset:function(){this._data=new m.init();this._nDataBytes=0},_append:function(o){if(typeof o=="string"){o=d.parse(o)}this._data.concat(o);this._nDataBytes+=o.sigBytes},_process:function(x){var r=this._data;var y=r.words;var o=r.sigBytes;var u=this.blockSize;var w=u*4;var v=o/w;if(x){v=f.ceil(v)}else{v=f.max((v|0)-this._minBufferSize,0)}var t=v*u;var s=f.min(t*4,o);if(t){for(var q=0;q<t;q+=u){this._doProcessBlock(y,q)}var p=y.splice(0,t);r.sigBytes-=s}return new m.init(p,s)},clone:function(){var o=k.clone.call(this);o._data=this._data.clone();return o},_minBufferSize:0});var g=c.Hasher=j.extend({cfg:k.extend(),init:function(o){this.cfg=this.cfg.extend(o);this.reset()},reset:function(){j.reset.call(this);this._doReset()},update:function(o){this._append(o);this._process();return this},finalize:function(o){if(o){this._append(o)}var p=this._doFinalize();return p},blockSize:512/32,_createHelper:function(o){return function(q,p){return new o.init(p).finalize(q)}},_createHmacHelper:function(o){return function(q,p){return new l.HMAC.init(o,p).finalize(q)}}});var l=b.algo={};return b}(Math));(function(){var f=a;var b=f.lib;var c=b.WordArray;var e=f.enc;var d=e.Base64={stringify:function(m){var o=m.words;var q=m.sigBytes;var h=this._map;m.clamp();var n=[];for(var l=0;l<q;l+=3){var t=(o[l>>>2]>>>(24-(l%4)*8))&255;var r=(o[(l+1)>>>2]>>>(24-((l+1)%4)*8))&255;var p=(o[(l+2)>>>2]>>>(24-((l+2)%4)*8))&255;var s=(t<<16)|(r<<8)|p;for(var k=0;(k<4)&&(l+k*0.75<q);k++){n.push(h.charAt((s>>>(6*(3-k)))&63))}}var g=h.charAt(64);if(g){while(n.length%4){n.push(g)}}return n.join("")},parse:function(p){var n=p.length;var h=this._map;var g=h.charAt(64);if(g){var q=p.indexOf(g);if(q!=-1){n=q}}var o=[];var m=0;for(var l=0;l<n;l++){if(l%4){var k=h.indexOf(p.charAt(l-1))<<((l%4)*2);var j=h.indexOf(p.charAt(l))>>>(6-(l%4)*2);o[m>>>2]|=(k|j)<<(24-(m%4)*8);m++}}return c.create(o,m)},_map:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="}}());a.lib.Cipher||(function(e){var n=a;var x=n.lib;var j=x.Base;var u=x.WordArray;var w=x.BufferedBlockAlgorithm;var s=n.enc;var g=s.Utf8;var m=s.Base64;var c=n.algo;var i=c.EvpKDF;var k=x.Cipher=w.extend({cfg:j.extend(),createEncryptor:function(C,B){return this.create(this._ENC_XFORM_MODE,C,B)},createDecryptor:function(C,B){return this.create(this._DEC_XFORM_MODE,C,B)},init:function(D,C,B){this.cfg=this.cfg.extend(B);this._xformMode=D;this._key=C;this.reset()},reset:function(){w.reset.call(this);this._doReset()},process:function(B){this._append(B);return this._process()},finalize:function(C){if(C){this._append(C)}var B=this._doFinalize();return B},keySize:128/32,ivSize:128/32,_ENC_XFORM_MODE:1,_DEC_XFORM_MODE:2,_createHelper:(function(){function B(C){if(typeof C=="string"){return h}else{return A}}return function(C){return{encrypt:function(F,E,D){return B(E).encrypt(C,F,E,D)},decrypt:function(F,E,D){return B(E).decrypt(C,F,E,D)}}}}())});var q=x.StreamCipher=k.extend({_doFinalize:function(){var B=this._process(!!"flush");return B},blockSize:1});var t=n.mode={};var z=x.BlockCipherMode=j.extend({createEncryptor:function(B,C){return this.Encryptor.create(B,C)},createDecryptor:function(B,C){return this.Decryptor.create(B,C)},init:function(B,C){this._cipher=B;this._iv=C}});var d=t.CBC=(function(){var B=z.extend();B.Encryptor=B.extend({processBlock:function(G,F){var D=this._cipher;var E=D.blockSize;C.call(this,G,F,E);D.encryptBlock(G,F);this._prevBlock=G.slice(F,F+E)}});B.Decryptor=B.extend({processBlock:function(H,G){var D=this._cipher;var F=D.blockSize;var E=H.slice(G,G+F);D.decryptBlock(H,G);C.call(this,H,G,F);this._prevBlock=E}});function C(I,H,F){var D=this._iv;if(D){var G=D;this._iv=e}else{var G=this._prevBlock}for(var E=0;E<F;E++){I[H+E]^=G[E]}}return B}());var f=n.pad={};var b=f.Pkcs7={pad:function(G,E){var F=E*4;var I=F-G.sigBytes%F;var B=(I<<24)|(I<<16)|(I<<8)|I;var D=[];for(var C=0;C<I;C+=4){D.push(B)}var H=u.create(D,I);G.concat(H)},unpad:function(B){var C=B.words[(B.sigBytes-1)>>>2]&255;B.sigBytes-=C}};var r=x.BlockCipher=k.extend({cfg:k.cfg.extend({mode:d,padding:b}),reset:function(){k.reset.call(this);var B=this.cfg;var C=B.iv;var E=B.mode;if(this._xformMode==this._ENC_XFORM_MODE){var D=E.createEncryptor}else{var D=E.createDecryptor;this._minBufferSize=1}this._mode=D.call(E,this,C&&C.words)},_doProcessBlock:function(C,B){this._mode.processBlock(C,B)},_doFinalize:function(){var C=this.cfg.padding;if(this._xformMode==this._ENC_XFORM_MODE){C.pad(this._data,this.blockSize);var B=this._process(!!"flush")}else{var B=this._process(!!"flush");C.unpad(B)}return B},blockSize:128/32});var p=x.CipherParams=j.extend({init:function(B){this.mixIn(B)},toString:function(B){return(B||this.formatter).stringify(this)}});var o=n.format={};var v=o.OpenSSL={stringify:function(B){var E=B.ciphertext;var C=B.salt;if(C){var D=u.create([1398893684,1701076831]).concat(C).concat(E)}else{var D=E}return D.toString(m)},parse:function(D){var C=m.parse(D);var E=C.words;if(E[0]==1398893684&&E[1]==1701076831){var B=u.create(E.slice(2,4));E.splice(0,4);C.sigBytes-=16}return p.create({ciphertext:C,salt:B})}};var A=x.SerializableCipher=j.extend({cfg:j.extend({format:v}),encrypt:function(B,G,E,C){C=this.cfg.extend(C);var D=B.createEncryptor(E,C);var H=D.finalize(G);var F=D.cfg;return p.create({ciphertext:H,key:E,iv:F.iv,algorithm:B,mode:F.mode,padding:F.padding,blockSize:B.blockSize,formatter:C.format})},decrypt:function(B,F,D,C){C=this.cfg.extend(C);F=this._parse(F,C.format);var E=B.createDecryptor(D,C).finalize(F.ciphertext);return E},_parse:function(B,C){if(typeof B=="string"){return C.parse(B,this)}else{return B}}});var l=n.kdf={};var y=l.OpenSSL={execute:function(D,G,B,F){if(!F){F=u.random(64/8)}var E=i.create({keySize:G+B}).compute(D,F);var C=u.create(E.words.slice(G),B*4);E.sigBytes=G*4;return p.create({key:E,iv:C,salt:F})}};var h=x.PasswordBasedCipher=A.extend({cfg:A.cfg.extend({kdf:y}),encrypt:function(B,E,D,C){C=this.cfg.extend(C);var G=C.kdf.execute(D,B.keySize,B.ivSize);C.iv=G.iv;var F=A.encrypt.call(this,B,E,G.key,C);F.mixIn(G);return F},decrypt:function(B,F,D,C){C=this.cfg.extend(C);F=this._parse(F,C.format);var G=C.kdf.execute(D,B.keySize,B.ivSize,F.salt);C.iv=G.iv;var E=A.decrypt.call(this,B,F,G.key,C);return E}})}());(function(){var b=a;var c=b.lib;var q=c.BlockCipher;var l=b.algo;var e=[];var m=[];var p=[];var o=[];var n=[];var k=[];var j=[];var i=[];var h=[];var g=[];(function(){var u=[];for(var s=0;s<256;s++){if(s<128){u[s]=s<<1}else{u[s]=(s<<1)^283}}var y=0;var v=0;for(var s=0;s<256;s++){var w=v^(v<<1)^(v<<2)^(v<<3)^(v<<4);w=(w>>>8)^(w&255)^99;e[y]=w;m[w]=y;var r=u[y];var B=u[r];var z=u[B];var A=(u[w]*257)^(w*16843008);p[y]=(A<<24)|(A>>>8);o[y]=(A<<16)|(A>>>16);n[y]=(A<<8)|(A>>>24);k[y]=A;var A=(z*16843009)^(B*65537)^(r*257)^(y*16843008);j[w]=(A<<24)|(A>>>8);i[w]=(A<<16)|(A>>>16);h[w]=(A<<8)|(A>>>24);g[w]=A;if(!y){y=v=1}else{y=r^u[u[u[z^r]]];v^=u[u[v]]}}}());var d=[0,1,2,4,8,16,32,64,128,27,54];var f=l.AES=q.extend({_doReset:function(){var A=this._key;var s=A.words;var z=A.sigBytes/4;var y=this._nRounds=z+6;var r=(y+1)*4;var u=this._keySchedule=[];for(var x=0;x<r;x++){if(x<z){u[x]=s[x]}else{var B=u[x-1];if(!(x%z)){B=(B<<8)|(B>>>24);B=(e[B>>>24]<<24)|(e[(B>>>16)&255]<<16)|(e[(B>>>8)&255]<<8)|e[B&255];B^=d[(x/z)|0]<<24}else{if(z>6&&x%z==4){B=(e[B>>>24]<<24)|(e[(B>>>16)&255]<<16)|(e[(B>>>8)&255]<<8)|e[B&255]}}u[x]=u[x-z]^B}}var v=this._invKeySchedule=[];for(var w=0;w<r;w++){var x=r-w;if(w%4){var B=u[x]}else{var B=u[x-4]}if(w<4||x<=4){v[w]=B}else{v[w]=j[e[B>>>24]]^i[e[(B>>>16)&255]]^h[e[(B>>>8)&255]]^g[e[B&255]]}}},encryptBlock:function(s,r){this._doCryptBlock(s,r,this._keySchedule,p,o,n,k,e)},decryptBlock:function(u,s){var r=u[s+1];u[s+1]=u[s+3];u[s+3]=r;this._doCryptBlock(u,s,this._invKeySchedule,j,i,h,g,m);var r=u[s+1];u[s+1]=u[s+3];u[s+3]=r},_doCryptBlock:function(A,z,I,w,u,s,r,H){var F=this._nRounds;var y=A[z]^I[0];var x=A[z+1]^I[1];var v=A[z+2]^I[2];var t=A[z+3]^I[3];var G=4;for(var J=1;J<F;J++){var E=w[y>>>24]^u[(x>>>16)&255]^s[(v>>>8)&255]^r[t&255]^I[G++];var D=w[x>>>24]^u[(v>>>16)&255]^s[(t>>>8)&255]^r[y&255]^I[G++];var C=w[v>>>24]^u[(t>>>16)&255]^s[(y>>>8)&255]^r[x&255]^I[G++];var B=w[t>>>24]^u[(y>>>16)&255]^s[(x>>>8)&255]^r[v&255]^I[G++];y=E;x=D;v=C;t=B}var E=((H[y>>>24]<<24)|(H[(x>>>16)&255]<<16)|(H[(v>>>8)&255]<<8)|H[t&255])^I[G++];var D=((H[x>>>24]<<24)|(H[(v>>>16)&255]<<16)|(H[(t>>>8)&255]<<8)|H[y&255])^I[G++];var C=((H[v>>>24]<<24)|(H[(t>>>16)&255]<<16)|(H[(y>>>8)&255]<<8)|H[x&255])^I[G++];var B=((H[t>>>24]<<24)|(H[(y>>>16)&255]<<16)|(H[(x>>>8)&255]<<8)|H[v&255])^I[G++];A[z]=E;A[z+1]=D;A[z+2]=C;A[z+3]=B},keySize:256/32});b.AES=q._createHelper(f)}());(function(){var h=a;var e=h.lib;var g=e.WordArray;var c=e.Hasher;var f=h.algo;var b=[];var d=f.SHA1=c.extend({_doReset:function(){this._hash=new g.init([1732584193,4023233417,2562383102,271733878,3285377520])},_doProcessBlock:function(o,k){var u=this._hash.words;var s=u[0];var r=u[1];var q=u[2];var p=u[3];var m=u[4];for(var l=0;l<80;l++){if(l<16){b[l]=o[k+l]|0}else{var j=b[l-3]^b[l-8]^b[l-14]^b[l-16];b[l]=(j<<1)|(j>>>31)}var v=((s<<5)|(s>>>27))+m+b[l];if(l<20){v+=((r&q)|(~r&p))+1518500249}else{if(l<40){v+=(r^q^p)+1859775393}else{if(l<60){v+=((r&q)|(r&p)|(q&p))-1894007588}else{v+=(r^q^p)-899497514}}}m=p;p=q;q=(r<<30)|(r>>>2);r=s;s=v}u[0]=(u[0]+s)|0;u[1]=(u[1]+r)|0;u[2]=(u[2]+q)|0;u[3]=(u[3]+p)|0;u[4]=(u[4]+m)|0},_doFinalize:function(){var k=this._data;var l=k.words;var i=this._nDataBytes*8;var j=k.sigBytes*8;l[j>>>5]|=128<<(24-j%32);l[(((j+64)>>>9)<<4)+14]=Math.floor(i/4294967296);l[(((j+64)>>>9)<<4)+15]=i;k.sigBytes=l.length*4;this._process();return this._hash},clone:function(){var i=c.clone.call(this);i._hash=this._hash.clone();return i}});h.SHA1=c._createHelper(d);h.HmacSHA1=c._createHmacHelper(d)}());(function(d){var b=a;var c=b.lib;var h=c.WordArray;var f=c.Hasher;var i=b.algo;var k=[];var j=[];(function(){function o(s){var r=d.sqrt(s);for(var q=2;q<=r;q++){if(!(s%q)){return false}}return true}function m(q){return((q-(q|0))*4294967296)|0}var p=2;var l=0;while(l<64){if(o(p)){if(l<8){k[l]=m(d.pow(p,1/2))}j[l]=m(d.pow(p,1/3));l++}p++}}());var e=[];var g=i.SHA256=f.extend({_doReset:function(){this._hash=new h.init(k.slice(0))},_doProcessBlock:function(o,n){var r=this._hash.words;var E=r[0];var D=r[1];var C=r[2];var B=r[3];var A=r[4];var z=r[5];var y=r[6];var x=r[7];for(var w=0;w<64;w++){if(w<16){e[w]=o[n+w]|0}else{var m=e[w-15];var G=((m<<25)|(m>>>7))^((m<<14)|(m>>>18))^(m>>>3);var s=e[w-2];var F=((s<<15)|(s>>>17))^((s<<13)|(s>>>19))^(s>>>10);e[w]=G+e[w-7]+F+e[w-16]}var t=(A&z)^(~A&y);var l=(E&D)^(E&C)^(D&C);var v=((E<<30)|(E>>>2))^((E<<19)|(E>>>13))^((E<<10)|(E>>>22));var u=((A<<26)|(A>>>6))^((A<<21)|(A>>>11))^((A<<7)|(A>>>25));var q=x+u+t+j[w]+e[w];var p=v+l;x=y;y=z;z=A;A=(B+q)|0;B=C;C=D;D=E;E=(q+p)|0}r[0]=(r[0]+E)|0;r[1]=(r[1]+D)|0;r[2]=(r[2]+C)|0;r[3]=(r[3]+B)|0;r[4]=(r[4]+A)|0;r[5]=(r[5]+z)|0;r[6]=(r[6]+y)|0;r[7]=(r[7]+x)|0},_doFinalize:function(){var n=this._data;var o=n.words;var l=this._nDataBytes*8;var m=n.sigBytes*8;o[m>>>5]|=128<<(24-m%32);o[(((m+64)>>>9)<<4)+14]=d.floor(l/4294967296);o[(((m+64)>>>9)<<4)+15]=l;n.sigBytes=o.length*4;this._process();return this._hash},clone:function(){var l=f.clone.call(this);l._hash=this._hash.clone();return l}});b.SHA256=f._createHelper(g);b.HmacSHA256=f._createHmacHelper(g)}(Math));(function(){var h=a;var e=h.lib;var d=e.Base;var g=h.enc;var c=g.Utf8;var f=h.algo;var b=f.HMAC=d.extend({init:function(r,o){r=this._hasher=new r.init();if(typeof o=="string"){o=c.parse(o)}var l=r.blockSize;var j=l*4;if(o.sigBytes>j){o=r.finalize(o)}o.clamp();var q=this._oKey=o.clone();var n=this._iKey=o.clone();var p=q.words;var k=n.words;for(var m=0;m<l;m++){p[m]^=1549556828;k[m]^=909522486}q.sigBytes=n.sigBytes=j;this.reset()},reset:function(){var i=this._hasher;i.reset();i.update(this._iKey)},update:function(i){this._hasher.update(i);return this},finalize:function(i){var j=this._hasher;var l=j.finalize(i);j.reset();var k=j.finalize(this._oKey.clone().concat(l));return k}})}());a.pad.NoPadding={pad:function(){},unpad:function(){}};a.mode.CTR=(function(){var c=a.lib.BlockCipherMode.extend();var b=c.Encryptor=c.extend({processBlock:function(l,k){var d=this._cipher;var h=d.blockSize;var f=this._iv;var e=this._counter;if(f){e=this._counter=f.slice(0);this._iv=undefined}var j=e.slice(0);d.encryptBlock(j,0);e[h-1]=(e[h-1]+1)|0;for(var g=0;g<h;g++){l[k+g]^=j[g]}}});c.Decryptor=b;return c}());return a})); -(function(){function c(){}var l=c.prototype;function h(w,x){var v=w.length;while(v--){if(w[v].listener===x){return v}}return -1}function j(v){return function w(){return this[v].apply(this,arguments)}}l.getListeners=function t(v){var y=this._getEvents();var w;var x;if(typeof v==="object"){w={};for(x in y){if(y.hasOwnProperty(x)&&v.test(x)){w[x]=y[x]}}}else{w=y[v]||(y[v]=[])}return w};l.flattenListeners=function r(x){var v=[];var w;for(w=0;w<x.length;w+=1){v.push(x[w].listener)}return v};l.getListenersAsObject=function e(v){var x=this.getListeners(v);var w;if(x instanceof Array){w={};w[v]=x}return w||x};l.addListener=function f(v,y){var x=this.getListenersAsObject(v);var z=typeof y==="object";var w;for(w in x){if(x.hasOwnProperty(w)&&h(x[w],y)===-1){x[w].push(z?y:{listener:y,once:false})}}return this};l.on=j("addListener");l.addOnceListener=function a(v,w){return this.addListener(v,{listener:w,once:true})};l.once=j("addOnceListener");l.defineEvent=function p(v){this.getListeners(v);return this};l.defineEvents=function q(v){for(var w=0;w<v.length;w+=1){this.defineEvent(v[w])}return this};l.removeListener=function b(v,z){var y=this.getListenersAsObject(v);var w;var x;for(x in y){if(y.hasOwnProperty(x)){w=h(y[x],z);if(w!==-1){y[x].splice(w,1)}}}return this};l.off=j("removeListener");l.addListeners=function m(v,w){return this.manipulateListeners(false,v,w)};l.removeListeners=function s(v,w){return this.manipulateListeners(true,v,w)};l.manipulateListeners=function g(w,x,z){var y;var A;var B=w?this.removeListener:this.addListener;var v=w?this.removeListeners:this.addListeners;if(typeof x==="object"&&!(x instanceof RegExp)){for(y in x){if(x.hasOwnProperty(y)&&(A=x[y])){if(typeof A==="function"){B.call(this,y,A)}else{v.call(this,y,A)}}}}else{y=z.length;while(y--){B.call(this,x,z[y])}}return this};l.removeEvent=function o(v){var y=typeof v;var x=this._getEvents();var w;if(y==="string"){delete x[v]}else{if(y==="object"){for(w in x){if(x.hasOwnProperty(w)&&v.test(w)){delete x[w]}}}else{delete this._events}}return this};l.emitEvent=function u(v,x){var A=this.getListenersAsObject(v);var B;var z;var y;var w;for(y in A){if(A.hasOwnProperty(y)){z=A[y].length;while(z--){B=A[y][z];if(B.once===true){this.removeListener(v,B.listener)}w=B.listener.apply(this,x||[]);if(w===this._getOnceReturnValue()){this.removeListener(v,B.listener)}}}}return this};l.trigger=j("emitEvent");l.emit=function k(v){var w=Array.prototype.slice.call(arguments,1);return this.emitEvent(v,w)};l.setOnceReturnValue=function i(v){this._onceReturnValue=v;return this};l._getOnceReturnValue=function n(){if(this.hasOwnProperty("_onceReturnValue")){return this._onceReturnValue}else{return true}};l._getEvents=function d(){return this._events||(this._events={})};if(typeof define==="function"&&define.amd){define(function(){return c})}else{if(typeof module==="object"&&module.exports){module.exports=c}else{this.EventEmitter=c}}}.call(this)); -!function(root,factory){"function"==typeof define&&define.amd?define(["bigint","crypto","eventemitter"],function(BigInt,CryptoJS,EventEmitter){var root={BigInt:BigInt,CryptoJS:CryptoJS,EventEmitter:EventEmitter,OTR:{},DSA:{}};return factory.call(root)}):(root.OTR={},root.DSA={},factory.call(root))}(this,function(){return function(){"use strict";var root=this,CONST={N:"FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA237327FFFFFFFFFFFFFFFF",G:"2",MSGSTATE_PLAINTEXT:0,MSGSTATE_ENCRYPTED:1,MSGSTATE_FINISHED:2,AUTHSTATE_NONE:0,AUTHSTATE_AWAITING_DHKEY:1,AUTHSTATE_AWAITING_REVEALSIG:2,AUTHSTATE_AWAITING_SIG:3,WHITESPACE_TAG:" ",WHITESPACE_TAG_V2:" ",WHITESPACE_TAG_V3:" ",OTR_TAG:"?OTR",OTR_VERSION_1:"\x00",OTR_VERSION_2:"\x00",OTR_VERSION_3:"\x00",SMPSTATE_EXPECT0:0,SMPSTATE_EXPECT1:1,SMPSTATE_EXPECT2:2,SMPSTATE_EXPECT3:3,SMPSTATE_EXPECT4:4,STATUS_SEND_QUERY:0,STATUS_AKE_INIT:1,STATUS_AKE_SUCCESS:2,STATUS_END_OTR:3};"undefined"!=typeof module&&module.exports?module.exports=CONST:root.OTR.CONST=CONST}.call(this),function(){"use strict";var CryptoJS,BigInt,root=this,HLP={};"undefined"!=typeof module&&module.exports?(module.exports=HLP={},CryptoJS=require("../vendor/crypto.js"),BigInt=require("../vendor/bigint.js")):(root.OTR&&(root.OTR.HLP=HLP),root.DSA&&(root.DSA.HLP=HLP),CryptoJS=root.CryptoJS,BigInt=root.BigInt);var DTS={BYTE:1,SHORT:2,INT:4,CTR:8,MAC:20,SIG:40},WRAPPER_BEGIN="?OTR",WRAPPER_END=".",TWO=BigInt.str2bigInt("2",10);HLP.debug=function(msg){this.debug&&"function"!=typeof this.debug&&"undefined"!=typeof console&&console.log(msg)},HLP.extend=function(child,parent){function Ctor(){this.constructor=child}for(var key in parent)Object.hasOwnProperty.call(parent,key)&&(child[key]=parent[key]);Ctor.prototype=parent.prototype,child.prototype=new Ctor,child.__super__=parent.prototype},HLP.compare=function(str1,str2){if(str1.length!==str2.length)return!1;for(var i=0,result=0;i<str1.length;i++)result|=str1[i].charCodeAt(0)^str2[i].charCodeAt(0);return 0===result},HLP.randomExponent=function(){return BigInt.randBigInt(1536)},HLP.smpHash=function(version,fmpi,smpi){var sha256=CryptoJS.algo.SHA256.create();sha256.update(CryptoJS.enc.Latin1.parse(HLP.packBytes(version,DTS.BYTE))),sha256.update(CryptoJS.enc.Latin1.parse(HLP.packMPI(fmpi))),smpi&&sha256.update(CryptoJS.enc.Latin1.parse(HLP.packMPI(smpi)));var hash=sha256.finalize();return HLP.bits2bigInt(hash.toString(CryptoJS.enc.Latin1))},HLP.makeMac=function(aesctr,m){var pass=CryptoJS.enc.Latin1.parse(m),mac=CryptoJS.HmacSHA256(CryptoJS.enc.Latin1.parse(aesctr),pass);return HLP.mask(mac.toString(CryptoJS.enc.Latin1),0,160)},HLP.make1Mac=function(aesctr,m){var pass=CryptoJS.enc.Latin1.parse(m),mac=CryptoJS.HmacSHA1(CryptoJS.enc.Latin1.parse(aesctr),pass);return mac.toString(CryptoJS.enc.Latin1)},HLP.encryptAes=function(msg,c,iv){var opts={mode:CryptoJS.mode.CTR,iv:CryptoJS.enc.Latin1.parse(iv),padding:CryptoJS.pad.NoPadding},aesctr=CryptoJS.AES.encrypt(msg,CryptoJS.enc.Latin1.parse(c),opts),aesctr_decoded=CryptoJS.enc.Base64.parse(aesctr.toString());return CryptoJS.enc.Latin1.stringify(aesctr_decoded)},HLP.decryptAes=function(msg,c,iv){msg=CryptoJS.enc.Latin1.parse(msg);var opts={mode:CryptoJS.mode.CTR,iv:CryptoJS.enc.Latin1.parse(iv),padding:CryptoJS.pad.NoPadding};return CryptoJS.AES.decrypt(CryptoJS.enc.Base64.stringify(msg),CryptoJS.enc.Latin1.parse(c),opts)},HLP.multPowMod=function(a,b,c,d,e){return BigInt.multMod(BigInt.powMod(a,b,e),BigInt.powMod(c,d,e),e)},HLP.ZKP=function(v,c,d,e){return BigInt.equals(c,HLP.smpHash(v,d,e))},HLP.GTOE=function(a,b){return BigInt.equals(a,b)||BigInt.greater(a,b)},HLP.between=function(x,a,b){return BigInt.greater(x,a)&&BigInt.greater(b,x)},HLP.checkGroup=function(g,N_MINUS_2){return HLP.GTOE(g,TWO)&&HLP.GTOE(N_MINUS_2,g)},HLP.h1=function(b,secbytes){var sha1=CryptoJS.algo.SHA1.create();return sha1.update(CryptoJS.enc.Latin1.parse(b)),sha1.update(CryptoJS.enc.Latin1.parse(secbytes)),sha1.finalize().toString(CryptoJS.enc.Latin1)},HLP.h2=function(b,secbytes){var sha256=CryptoJS.algo.SHA256.create();return sha256.update(CryptoJS.enc.Latin1.parse(b)),sha256.update(CryptoJS.enc.Latin1.parse(secbytes)),sha256.finalize().toString(CryptoJS.enc.Latin1)},HLP.mask=function(bytes,start,n){return bytes.substr(start/8,n/8)};var _toString=String.fromCharCode;HLP.packBytes=function(val,bytes){val=val.toString(16);for(var nex,res="";bytes>0;bytes--)nex=val.length?val.substr(-2,2):"0",val=val.substr(0,val.length-2),res=_toString(parseInt(nex,16))+res;return res},HLP.packINT=function(d){return HLP.packBytes(d,DTS.INT)},HLP.packCtr=function(d){return HLP.padCtr(HLP.packBytes(d,DTS.CTR))},HLP.padCtr=function(ctr){return ctr+"\x00\x00\x00\x00\x00\x00\x00\x00"},HLP.unpackCtr=function(d){return d=HLP.toByteArray(d.substring(0,8)),HLP.unpack(d)},HLP.unpack=function(arr){for(var val=0,i=0,len=arr.length;len>i;i++)val=256*val+arr[i];return val},HLP.packData=function(d){return HLP.packINT(d.length)+d},HLP.bits2bigInt=function(bits){return bits=HLP.toByteArray(bits),BigInt.ba2bigInt(bits)},HLP.packMPI=function(mpi){return HLP.packData(BigInt.bigInt2bits(BigInt.trim(mpi,0)))},HLP.packSHORT=function(short){return HLP.packBytes(short,DTS.SHORT)},HLP.unpackSHORT=function(short){return short=HLP.toByteArray(short),HLP.unpack(short)},HLP.packTLV=function(type,value){return HLP.packSHORT(type)+HLP.packSHORT(value.length)+value},HLP.readLen=function(msg){return msg=HLP.toByteArray(msg.substring(0,4)),HLP.unpack(msg)},HLP.readData=function(data){var n=HLP.unpack(data.splice(0,4));return[n,data]},HLP.readMPI=function(data){return data=HLP.toByteArray(data),data=HLP.readData(data),BigInt.ba2bigInt(data[1])},HLP.packMPIs=function(arr){return arr.reduce(function(prv,cur){return prv+HLP.packMPI(cur)},"")},HLP.unpackMPIs=function(num,mpis){for(var i=0,arr=[];num>i;i++)arr.push("MPI");return HLP.splitype(arr,mpis).map(function(m){return HLP.readMPI(m)})},HLP.wrapMsg=function(msg,fs,v3,our_it,their_it){msg=CryptoJS.enc.Base64.stringify(CryptoJS.enc.Latin1.parse(msg)),msg=WRAPPER_BEGIN+":"+msg+WRAPPER_END;var its;if(v3&&(its="|",its+=HLP.readLen(our_it).toString(16),its+="|",its+=HLP.readLen(their_it).toString(16)),!fs)return[null,msg];var n=Math.ceil(msg.length/fs);if(n>65535)return["Too many fragments"];if(1==n)return[null,msg];var k,bi,ei,frag,mf,mfs=[];for(k=1;n>=k;k++)bi=(k-1)*fs,ei=k*fs,frag=msg.slice(bi,ei),mf=WRAPPER_BEGIN,v3&&(mf+=its),mf+=","+k+",",mf+=n+",",mf+=frag+",",mfs.push(mf);return[null,mfs]},HLP.splitype=function splitype(arr,msg){var data=[];return arr.forEach(function(a){var str;switch(a){case"PUBKEY":str=splitype(["SHORT","MPI","MPI","MPI","MPI"],msg).join("");break;case"DATA":case"MPI":str=msg.substring(0,HLP.readLen(msg)+4);break;default:str=msg.substring(0,DTS[a])}data.push(str),msg=msg.substring(str.length)}),data};var _bin2num=function(){for(var i=0,_bin2num={};256>i;++i)_bin2num[String.fromCharCode(i)]=i;for(i=128;256>i;++i)_bin2num[String.fromCharCode(63232+i)]=i;return _bin2num}();HLP.toByteArray=function(data){for(var rv=[],ary=data.split(""),i=-1,iz=ary.length,remain=iz%8;remain--;)++i,rv[i]=_bin2num[ary[i]];for(remain=iz>>3;remain--;)rv.push(_bin2num[ary[++i]],_bin2num[ary[++i]],_bin2num[ary[++i]],_bin2num[ary[++i]],_bin2num[ary[++i]],_bin2num[ary[++i]],_bin2num[ary[++i]],_bin2num[ary[++i]]);return rv}}.call(this),function(){"use strict";function timer(){var start=(new Date).getTime();return function(s){if(DEBUG&&"undefined"!=typeof console){var t=(new Date).getTime();console.log(s+": "+(t-start)),start=t}}}function makeRandom(min,max){var c=BigInt.randBigInt(BigInt.bitSize(max));return HLP.between(c,min,max)?c:makeRandom(min,max)}function isProbPrime(k,n){var i,B=3e4,l=BigInt.bitSize(k),primes=BigInt.primes;for(0===primes.length&&(primes=BigInt.findPrimes(B)),rpprb.length!=k.length&&(rpprb=BigInt.dup(k)),i=0;i<primes.length&&primes[i]<=B;i++)if(0===BigInt.modInt(k,primes[i])&&!BigInt.equalsInt(k,primes[i]))return 0;for(i=0;n>i;i++){for(BigInt.randBigInt_(rpprb,l,0);!BigInt.greater(k,rpprb);)BigInt.randBigInt_(rpprb,l,0);if(!BigInt.millerRabin(k,rpprb))return 0}return 1}function generatePrimes(bit_length){for(var q,p,rem,counter,t=timer(),repeat=bit_lengths[bit_length].repeat,N=bit_lengths[bit_length].N,LM1=BigInt.twoToThe(bit_length-1),bl4=4*bit_length,brk=!1;;)if(q=BigInt.randBigInt(N,1),q[0]|=1,isProbPrime(q,repeat)){for(t("q"),counter=0;bl4>counter;counter++)if(p=BigInt.randBigInt(bit_length,1),p[0]|=1,rem=BigInt.mod(p,q),rem=BigInt.sub(rem,ONE),p=BigInt.sub(p,rem),!BigInt.greater(LM1,p)&&isProbPrime(p,repeat)){t("p"),primes[bit_length]={p:p,q:q},brk=!0;break}if(brk)break}for(var g,h=BigInt.dup(TWO),pm1=BigInt.sub(p,ONE),e=BigInt.multMod(pm1,BigInt.inverseMod(q,p),p);;){g=BigInt.powMod(h,e,p);{if(!BigInt.equals(g,ONE))return primes[bit_length].g=g,void t("g");h=BigInt.add(h,ONE)}}throw new Error("Unreachable!")}function DSA(obj,opts){if(!(this instanceof DSA))return new DSA(obj,opts);if(opts=opts||{},obj){var self=this;return["p","q","g","y","x"].forEach(function(prop){self[prop]=obj[prop]}),void(this.type=obj.type||KEY_TYPE)}var bit_length=parseInt(opts.bit_length?opts.bit_length:1024,10);if(!bit_lengths[bit_length])throw new Error("Unsupported bit length.");primes[bit_length]||generatePrimes(bit_length),this.p=primes[bit_length].p,this.q=primes[bit_length].q,this.g=primes[bit_length].g,this.type=KEY_TYPE,this.x=makeRandom(ZERO,this.q),this.y=BigInt.powMod(this.g,this.x,this.p),opts.nocache&&(primes[bit_length]=null)}function tokenizeStr(str){var start,end;if(start=str.indexOf("("),end=str.lastIndexOf(")"),0>start||0>end)throw new Error("Malformed S-Expression");str=str.substring(start+1,end);var splt=str.search(/\s/),obj={type:str.substring(0,splt),val:[]};if(str=str.substring(splt+1,end),start=str.indexOf("("),0>start)obj.val.push(str);else for(var i,len,ss,es;start>-1;){for(i=start+1,len=str.length,ss=1,es=0;len>i&&ss>es;i++)"("===str[i]&&ss++,")"===str[i]&&es++;obj.val.push(tokenizeStr(str.substring(start,++i))),str=str.substring(++i),start=str.indexOf("(")}return obj}function parseLibotr(obj){if(!obj.type)throw new Error("Parse error.");var o,val;return"privkeys"===obj.type?(o=[],obj.val.forEach(function(i){o.push(parseLibotr(i))}),o):(o={},obj.val.forEach(function(i){val=i.val[0],"string"==typeof val?0===val.indexOf("#")&&(val=val.substring(1,val.lastIndexOf("#")),val=BigInt.str2bigInt(val,16)):val=parseLibotr(i),o[i.type]=val}),o)}var CryptoJS,BigInt,Worker,WWPath,HLP,root=this;"undefined"!=typeof module&&module.exports?(module.exports=DSA,CryptoJS=require("../vendor/crypto.js"),BigInt=require("../vendor/bigint.js"),WWPath=require("path").join(__dirname,"/dsa-webworker.js"),HLP=require("./helpers.js")):(Object.keys(root.DSA).forEach(function(k){DSA[k]=root.DSA[k]}),root.DSA=DSA,CryptoJS=root.CryptoJS,BigInt=root.BigInt,Worker=root.Worker,WWPath="dsa-webworker.js",HLP=DSA.HLP);var ZERO=BigInt.str2bigInt("0",10),ONE=BigInt.str2bigInt("1",10),TWO=BigInt.str2bigInt("2",10),KEY_TYPE="\x00\x00",DEBUG=!1,rpprb=[],bit_lengths={1024:{N:160,repeat:40},2048:{N:224,repeat:56}},primes={};DSA.prototype={constructor:DSA,packPublic:function(){var str=this.type;return str+=HLP.packMPI(this.p),str+=HLP.packMPI(this.q),str+=HLP.packMPI(this.g),str+=HLP.packMPI(this.y)},packPrivate:function(){var str=this.packPublic()+HLP.packMPI(this.x);return str=CryptoJS.enc.Latin1.parse(str),str.toString(CryptoJS.enc.Base64)},generateNonce:function(m){var priv=BigInt.bigInt2bits(BigInt.trim(this.x,0)),rand=BigInt.bigInt2bits(BigInt.randBigInt(256)),sha256=CryptoJS.algo.SHA256.create();sha256.update(CryptoJS.enc.Latin1.parse(priv)),sha256.update(m),sha256.update(CryptoJS.enc.Latin1.parse(rand));var hash=sha256.finalize();return hash=HLP.bits2bigInt(hash.toString(CryptoJS.enc.Latin1)),BigInt.rightShift_(hash,256-BigInt.bitSize(this.q)),HLP.between(hash,ZERO,this.q)?hash:this.generateNonce(m)},sign:function(m){m=CryptoJS.enc.Latin1.parse(m);for(var k,b=BigInt.str2bigInt(m.toString(CryptoJS.enc.Hex),16),r=ZERO,s=ZERO;BigInt.isZero(s)||BigInt.isZero(r);)k=this.generateNonce(m),r=BigInt.mod(BigInt.powMod(this.g,k,this.p),this.q),BigInt.isZero(r)||(s=BigInt.inverseMod(k,this.q),s=BigInt.mult(s,BigInt.add(b,BigInt.mult(this.x,r))),s=BigInt.mod(s,this.q));return[r,s]},fingerprint:function(){var pk=this.packPublic();return this.type===KEY_TYPE&&(pk=pk.substring(2)),pk=CryptoJS.enc.Latin1.parse(pk),CryptoJS.SHA1(pk).toString(CryptoJS.enc.Hex)}},DSA.parsePublic=function(str,priv){var fields=["SHORT","MPI","MPI","MPI","MPI"];priv&&fields.push("MPI"),str=HLP.splitype(fields,str);var obj={type:str[0],p:HLP.readMPI(str[1]),q:HLP.readMPI(str[2]),g:HLP.readMPI(str[3]),y:HLP.readMPI(str[4])};return priv&&(obj.x=HLP.readMPI(str[5])),new DSA(obj)},DSA.parsePrivate=function(str,libotr){return libotr?parseLibotr(tokenizeStr(str))[0]["private-key"].dsa:(str=CryptoJS.enc.Base64.parse(str),str=str.toString(CryptoJS.enc.Latin1),DSA.parsePublic(str,!0))},DSA.verify=function(key,m,r,s){if(!HLP.between(r,ZERO,key.q)||!HLP.between(s,ZERO,key.q))return!1;var hm=CryptoJS.enc.Latin1.parse(m);hm=BigInt.str2bigInt(hm.toString(CryptoJS.enc.Hex),16);var w=BigInt.inverseMod(s,key.q),u1=BigInt.multMod(hm,w,key.q),u2=BigInt.multMod(r,w,key.q);u1=BigInt.powMod(key.g,u1,key.p),u2=BigInt.powMod(key.y,u2,key.p);var v=BigInt.mod(BigInt.multMod(u1,u2,key.p),key.q);return BigInt.equals(v,r)},DSA.createInWebWorker=function(options,cb){var opts={path:WWPath,seed:BigInt.getSeed};options&&"object"==typeof options&&Object.keys(options).forEach(function(k){opts[k]=options[k]}),"undefined"!=typeof module&&module.exports&&(Worker=require("webworker-threads").Worker);var worker=new Worker(opts.path);worker.onmessage=function(e){var data=e.data;switch(data.type){case"debug":if(!DEBUG||"undefined"==typeof console)return;console.log(data.val);break;case"data":worker.terminate(),cb(DSA.parsePrivate(data.val));break;default:throw new Error("Unrecognized type.")}},worker.postMessage({seed:opts.seed(),imports:opts.imports,debug:DEBUG})}}.call(this),function(){"use strict";var CryptoJS,CONST,HLP,root=this,Parse={};"undefined"!=typeof module&&module.exports?(module.exports=Parse,CryptoJS=require("../vendor/crypto.js"),CONST=require("./const.js"),HLP=require("./helpers.js")):(root.OTR.Parse=Parse,CryptoJS=root.CryptoJS,CONST=root.OTR.CONST,HLP=root.OTR.HLP);var tags={};tags[CONST.WHITESPACE_TAG_V2]=CONST.OTR_VERSION_2,tags[CONST.WHITESPACE_TAG_V3]=CONST.OTR_VERSION_3,Parse.parseMsg=function(otr,msg){var ver=[],start=msg.indexOf(CONST.OTR_TAG);if(!~start){if(this.initFragment(otr),ind=msg.indexOf(CONST.WHITESPACE_TAG),~ind){msg=msg.split(""),msg.splice(ind,16);for(var tag,len=msg.length;len>ind;)tag=msg.slice(ind,ind+8).join(""),Object.hasOwnProperty.call(tags,tag)?(msg.splice(ind,8),ver.push(tags[tag])):ind+=8;msg=msg.join("")}return{msg:msg,ver:ver}}var ind=start+CONST.OTR_TAG.length,com=msg[ind];if(","===com||"|"===com)return this.msgFragment(otr,msg.substring(ind+1),"|"===com);if(this.initFragment(otr),~["?","v"].indexOf(com)){"?"===msg[ind]&&(ver.push(CONST.OTR_VERSION_1),ind+=1);var vers={2:CONST.OTR_VERSION_2,3:CONST.OTR_VERSION_3},qs=msg.substring(ind+1),qi=qs.indexOf("?");return qi>=1&&(qs=qs.substring(0,qi).split(""),"v"===msg[ind]&&qs.forEach(function(q){Object.hasOwnProperty.call(vers,q)&&ver.push(vers[q])})),{cls:"query",ver:ver}}if(":"===com){ind+=1;var info=msg.substring(ind,ind+4);if(info.length<4)return{msg:msg};info=CryptoJS.enc.Base64.parse(info).toString(CryptoJS.enc.Latin1);var version=info.substring(0,2),type=info.substring(2);if(!otr["ALLOW_V"+HLP.unpackSHORT(version)])return{msg:msg};ind+=4;var end=msg.substring(ind).indexOf(".");if(!~end)return{msg:msg};msg=CryptoJS.enc.Base64.parse(msg.substring(ind,ind+end)),msg=CryptoJS.enc.Latin1.stringify(msg);var instance_tags;version===CONST.OTR_VERSION_3&&(instance_tags=msg.substring(0,8),msg=msg.substring(8));var cls;return~["","\n","",""].indexOf(type)?cls="ake":""===type&&(cls="data"),{version:version,type:type,msg:msg,cls:cls,instance_tags:instance_tags}}return" Error:"===msg.substring(ind,ind+7)?(otr.ERROR_START_AKE&&otr.sendQueryMsg(),{msg:msg.substring(ind+7),cls:"error"}):{msg:msg}},Parse.initFragment=function(otr){otr.fragment={s:"",j:0,k:0}},Parse.msgFragment=function(otr,msg,v3){if(msg=msg.split(","),v3){var its=msg.shift().split("|"),their_it=HLP.packINT(parseInt(its[0],16)),our_it=HLP.packINT(parseInt(its[1],16));if(otr.checkInstanceTags(their_it+our_it))return}if(!(msg.length<4||isNaN(parseInt(msg[0],10))||isNaN(parseInt(msg[1],10)))){var k=parseInt(msg[0],10),n=parseInt(msg[1],10);return msg=msg[2],k>n||0===n||0===k?void this.initFragment(otr):(1===k?(this.initFragment(otr),otr.fragment={k:1,n:n,s:msg}):n===otr.fragment.n&&k===otr.fragment.k+1?(otr.fragment.s+=msg,otr.fragment.k+=1):this.initFragment(otr),n===k?(msg=otr.fragment.s,this.initFragment(otr),this.parseMsg(otr,msg)):void 0)}}}.call(this),function(){"use strict";function hMac(gx,gy,pk,kid,m){var pass=CryptoJS.enc.Latin1.parse(m),hmac=CryptoJS.algo.HMAC.create(CryptoJS.algo.SHA256,pass);return hmac.update(CryptoJS.enc.Latin1.parse(HLP.packMPI(gx))),hmac.update(CryptoJS.enc.Latin1.parse(HLP.packMPI(gy))),hmac.update(CryptoJS.enc.Latin1.parse(pk)),hmac.update(CryptoJS.enc.Latin1.parse(kid)),hmac.finalize().toString(CryptoJS.enc.Latin1)}function AKE(otr){if(!(this instanceof AKE))return new AKE(otr);this.otr=otr,this.our_dh=otr.our_old_dh,this.our_keyid=otr.our_keyid-1,this.their_y=null,this.their_keyid=null,this.their_priv_pk=null,this.ssid=null,this.transmittedRS=!1,this.r=null;var self=this;["sendMsg"].forEach(function(meth){self[meth]=self[meth].bind(self)})}var CryptoJS,BigInt,CONST,HLP,DSA,root=this;"undefined"!=typeof module&&module.exports?(module.exports=AKE,CryptoJS=require("../vendor/crypto.js"),BigInt=require("../vendor/bigint.js"),CONST=require("./const.js"),HLP=require("./helpers.js"),DSA=require("./dsa.js")):(root.OTR.AKE=AKE,CryptoJS=root.CryptoJS,BigInt=root.BigInt,CONST=root.OTR.CONST,HLP=root.OTR.HLP,DSA=root.DSA);var N=BigInt.str2bigInt(CONST.N,16),N_MINUS_2=BigInt.sub(N,BigInt.str2bigInt("2",10));AKE.prototype={constructor:AKE,createKeys:function(g){var s=BigInt.powMod(g,this.our_dh.privateKey,N),secbytes=HLP.packMPI(s);this.ssid=HLP.mask(HLP.h2("\x00",secbytes),0,64);var tmp=HLP.h2("",secbytes);this.c=HLP.mask(tmp,0,128),this.c_prime=HLP.mask(tmp,128,128),this.m1=HLP.h2("",secbytes),this.m2=HLP.h2("",secbytes),this.m1_prime=HLP.h2("",secbytes),this.m2_prime=HLP.h2("",secbytes)},verifySignMac:function(mac,aesctr,m2,c,their_y,our_dh_pk,m1,ctr){var vmac=HLP.makeMac(aesctr,m2);if(!HLP.compare(mac,vmac))return["MACs do not match."];var x=HLP.decryptAes(aesctr.substring(4),c,ctr);x=HLP.splitype(["PUBKEY","INT","SIG"],x.toString(CryptoJS.enc.Latin1));var m=hMac(their_y,our_dh_pk,x[0],x[1],m1),pub=DSA.parsePublic(x[0]),r=HLP.bits2bigInt(x[2].substring(0,20)),s=HLP.bits2bigInt(x[2].substring(20));return DSA.verify(pub,m,r,s)?[null,HLP.readLen(x[1]),pub]:["Cannot verify signature of m."]},makeM:function(their_y,m1,c,m2){var pk=this.otr.priv.packPublic(),kid=HLP.packINT(this.our_keyid),m=hMac(this.our_dh.publicKey,their_y,pk,kid,m1);m=this.otr.priv.sign(m);var msg=pk+kid;msg+=BigInt.bigInt2bits(m[0],20),msg+=BigInt.bigInt2bits(m[1],20),msg=CryptoJS.enc.Latin1.parse(msg);var aesctr=HLP.packData(HLP.encryptAes(msg,c,HLP.packCtr(0))),mac=HLP.makeMac(aesctr,m2);return aesctr+mac},akeSuccess:function(version){return HLP.debug.call(this.otr,"success"),BigInt.equals(this.their_y,this.our_dh.publicKey)?this.otr.error("equal keys - we have a problem.",!0):(this.otr.our_old_dh=this.our_dh,this.otr.their_priv_pk=this.their_priv_pk,this.their_keyid===this.otr.their_keyid&&BigInt.equals(this.their_y,this.otr.their_y)||this.their_keyid===this.otr.their_keyid-1&&BigInt.equals(this.their_y,this.otr.their_old_y)||(this.otr.their_y=this.their_y,this.otr.their_old_y=null,this.otr.their_keyid=this.their_keyid,this.otr.sessKeys[0]=[new this.otr.DHSession(this.otr.our_dh,this.otr.their_y),null],this.otr.sessKeys[1]=[new this.otr.DHSession(this.otr.our_old_dh,this.otr.their_y),null]),this.otr.ssid=this.ssid,this.otr.transmittedRS=this.transmittedRS,this.otr_version=version,this.otr.authstate=CONST.AUTHSTATE_NONE,this.otr.msgstate=CONST.MSGSTATE_ENCRYPTED,this.r=null,this.myhashed=null,this.dhcommit=null,this.encrypted=null,this.hashed=null,this.otr.trigger("status",[CONST.STATUS_AKE_SUCCESS]),void this.otr.sendStored())},handleAKE:function(msg){var send,vsm,type,version=msg.version;switch(msg.type){case"":if(HLP.debug.call(this.otr,"d-h key message"),msg=HLP.splitype(["DATA","DATA"],msg.msg),this.otr.authstate===CONST.AUTHSTATE_AWAITING_DHKEY){var ourHash=HLP.readMPI(this.myhashed),theirHash=HLP.readMPI(msg[1]);if(BigInt.greater(ourHash,theirHash)){type="",send=this.dhcommit;break}this.our_dh=this.otr.dh(),this.otr.authstate=CONST.AUTHSTATE_NONE,this.r=null,this.myhashed=null}else this.otr.authstate===CONST.AUTHSTATE_AWAITING_SIG&&(this.our_dh=this.otr.dh());this.otr.authstate=CONST.AUTHSTATE_AWAITING_REVEALSIG,this.encrypted=msg[0].substring(4),this.hashed=msg[1].substring(4),type="\n",send=HLP.packMPI(this.our_dh.publicKey);break;case"\n":if(HLP.debug.call(this.otr,"reveal signature message"),msg=HLP.splitype(["MPI"],msg.msg),this.otr.authstate!==CONST.AUTHSTATE_AWAITING_DHKEY){if(this.otr.authstate!==CONST.AUTHSTATE_AWAITING_SIG)return;if(!BigInt.equals(this.their_y,HLP.readMPI(msg[0])))return}if(this.otr.authstate=CONST.AUTHSTATE_AWAITING_SIG,this.their_y=HLP.readMPI(msg[0]),!HLP.checkGroup(this.their_y,N_MINUS_2))return this.otr.error("Illegal g^y.",!0);this.createKeys(this.their_y),type="",send=HLP.packMPI(this.r),send+=this.makeM(this.their_y,this.m1,this.c,this.m2),this.m1=null,this.m2=null,this.c=null;break;case"":if(HLP.debug.call(this.otr,"signature message"),this.otr.authstate!==CONST.AUTHSTATE_AWAITING_REVEALSIG)return;msg=HLP.splitype(["DATA","DATA","MAC"],msg.msg),this.r=HLP.readMPI(msg[0]);var key=CryptoJS.enc.Hex.parse(BigInt.bigInt2str(this.r,16));key=CryptoJS.enc.Latin1.stringify(key);var gxmpi=HLP.decryptAes(this.encrypted,key,HLP.packCtr(0));gxmpi=gxmpi.toString(CryptoJS.enc.Latin1),this.their_y=HLP.readMPI(gxmpi);var hash=CryptoJS.SHA256(CryptoJS.enc.Latin1.parse(gxmpi));return HLP.compare(this.hashed,hash.toString(CryptoJS.enc.Latin1))?HLP.checkGroup(this.their_y,N_MINUS_2)?(this.createKeys(this.their_y),vsm=this.verifySignMac(msg[2],msg[1],this.m2,this.c,this.their_y,this.our_dh.publicKey,this.m1,HLP.packCtr(0)),vsm[0]?this.otr.error(vsm[0],!0):(this.their_keyid=vsm[1],this.their_priv_pk=vsm[2],send=this.makeM(this.their_y,this.m1_prime,this.c_prime,this.m2_prime),this.m1=null,this.m2=null,this.m1_prime=null,this.m2_prime=null,this.c=null,this.c_prime=null,this.sendMsg(version,"",send),void this.akeSuccess(version))):this.otr.error("Illegal g^x.",!0):this.otr.error("Hashed g^x does not match.",!0);case"":if(HLP.debug.call(this.otr,"data message"),this.otr.authstate!==CONST.AUTHSTATE_AWAITING_SIG)return;return msg=HLP.splitype(["DATA","MAC"],msg.msg),vsm=this.verifySignMac(msg[1],msg[0],this.m2_prime,this.c_prime,this.their_y,this.our_dh.publicKey,this.m1_prime,HLP.packCtr(0)),vsm[0]?this.otr.error(vsm[0],!0):(this.their_keyid=vsm[1],this.their_priv_pk=vsm[2],this.m1_prime=null,this.m2_prime=null,this.c_prime=null,this.transmittedRS=!0,void this.akeSuccess(version));default:return}this.sendMsg(version,type,send)},sendMsg:function(version,type,msg){var send=version+type,v3=version===CONST.OTR_VERSION_3;return v3&&(HLP.debug.call(this.otr,"instance tags"),send+=this.otr.our_instance_tag,send+=this.otr.their_instance_tag),send+=msg,send=HLP.wrapMsg(send,this.otr.fragment_size,v3,this.otr.our_instance_tag,this.otr.their_instance_tag),send[0]?this.otr.error(send[0]):void this.otr.io(send[1])},initiateAKE:function(version){HLP.debug.call(this.otr,"d-h commit message"),this.otr.trigger("status",[CONST.STATUS_AKE_INIT]),this.otr.authstate=CONST.AUTHSTATE_AWAITING_DHKEY;var gxmpi=HLP.packMPI(this.our_dh.publicKey);gxmpi=CryptoJS.enc.Latin1.parse(gxmpi),this.r=BigInt.randBigInt(128);var key=CryptoJS.enc.Hex.parse(BigInt.bigInt2str(this.r,16));key=CryptoJS.enc.Latin1.stringify(key),this.myhashed=CryptoJS.SHA256(gxmpi),this.myhashed=HLP.packData(this.myhashed.toString(CryptoJS.enc.Latin1)),this.dhcommit=HLP.packData(HLP.encryptAes(gxmpi,key,HLP.packCtr(0))),this.dhcommit+=this.myhashed,this.sendMsg(version,"",this.dhcommit)}}}.call(this),function(){"use strict";function SM(reqs){return this instanceof SM?(this.version=1,this.our_fp=reqs.our_fp,this.their_fp=reqs.their_fp,this.ssid=reqs.ssid,this.debug=!!reqs.debug,void this.init()):new SM(reqs)}var CryptoJS,BigInt,EventEmitter,CONST,HLP,root=this;"undefined"!=typeof module&&module.exports?(module.exports=SM,CryptoJS=require("../vendor/crypto.js"),BigInt=require("../vendor/bigint.js"),EventEmitter=require("../vendor/eventemitter.js"),CONST=require("./const.js"),HLP=require("./helpers.js")):(root.OTR.SM=SM,CryptoJS=root.CryptoJS,BigInt=root.BigInt,EventEmitter=root.EventEmitter,CONST=root.OTR.CONST,HLP=root.OTR.HLP);var G=BigInt.str2bigInt(CONST.G,10),N=BigInt.str2bigInt(CONST.N,16),N_MINUS_2=BigInt.sub(N,BigInt.str2bigInt("2",10)),Q=BigInt.sub(N,BigInt.str2bigInt("1",10));BigInt.divInt_(Q,2),HLP.extend(SM,EventEmitter),SM.prototype.init=function(){this.smpstate=CONST.SMPSTATE_EXPECT1,this.secret=null},SM.prototype.makeSecret=function(our,secret){var sha256=CryptoJS.algo.SHA256.create();sha256.update(CryptoJS.enc.Latin1.parse(HLP.packBytes(this.version,1))),sha256.update(CryptoJS.enc.Hex.parse(our?this.our_fp:this.their_fp)),sha256.update(CryptoJS.enc.Hex.parse(our?this.their_fp:this.our_fp)),sha256.update(CryptoJS.enc.Latin1.parse(this.ssid)),sha256.update(CryptoJS.enc.Latin1.parse(secret));var hash=sha256.finalize();this.secret=HLP.bits2bigInt(hash.toString(CryptoJS.enc.Latin1))},SM.prototype.makeG2s=function(){this.a2=HLP.randomExponent(),this.a3=HLP.randomExponent(),this.g2a=BigInt.powMod(G,this.a2,N),this.g3a=BigInt.powMod(G,this.a3,N),HLP.checkGroup(this.g2a,N_MINUS_2)&&HLP.checkGroup(this.g3a,N_MINUS_2)||this.makeG2s()},SM.prototype.computeGs=function(g2a,g3a){this.g2=BigInt.powMod(g2a,this.a2,N),this.g3=BigInt.powMod(g3a,this.a3,N)},SM.prototype.computePQ=function(r){this.p=BigInt.powMod(this.g3,r,N),this.q=HLP.multPowMod(G,r,this.g2,this.secret,N)},SM.prototype.computeR=function(){this.r=BigInt.powMod(this.QoQ,this.a3,N)},SM.prototype.computeRab=function(r){return BigInt.powMod(r,this.a3,N)},SM.prototype.computeC=function(v,r){return HLP.smpHash(v,BigInt.powMod(G,r,N))},SM.prototype.computeD=function(r,a,c){return BigInt.subMod(r,BigInt.multMod(a,c,Q),Q)},SM.prototype.handleSM=function(msg){var send,r2,r3,r7,t1,t2,t3,t4,rab,tmp2,cR,d7,ms,trust,expectStates={2:CONST.SMPSTATE_EXPECT1,3:CONST.SMPSTATE_EXPECT2,4:CONST.SMPSTATE_EXPECT3,5:CONST.SMPSTATE_EXPECT4,7:CONST.SMPSTATE_EXPECT1};if(6===msg.type)return this.init(),void this.trigger("abort");if(this.smpstate!==expectStates[msg.type])return this.abort();switch(this.smpstate){case CONST.SMPSTATE_EXPECT1:HLP.debug.call(this,"smp tlv 2");var ind,question;return 7===msg.type&&(ind=msg.msg.indexOf("\x00"),question=msg.msg.substring(0,ind),msg.msg=msg.msg.substring(ind+1)),ms=HLP.readLen(msg.msg.substr(0,4)),6!==ms?this.abort():(msg=HLP.unpackMPIs(6,msg.msg.substring(4)),HLP.checkGroup(msg[0],N_MINUS_2)&&HLP.checkGroup(msg[3],N_MINUS_2)&&HLP.ZKP(1,msg[1],HLP.multPowMod(G,msg[2],msg[0],msg[1],N))&&HLP.ZKP(2,msg[4],HLP.multPowMod(G,msg[5],msg[3],msg[4],N))?(this.g3ao=msg[3],this.makeG2s(),r2=HLP.randomExponent(),r3=HLP.randomExponent(),this.c2=this.computeC(3,r2),this.c3=this.computeC(4,r3),this.d2=this.computeD(r2,this.a2,this.c2),this.d3=this.computeD(r3,this.a3,this.c3),this.computeGs(msg[0],msg[3]),this.smpstate=CONST.SMPSTATE_EXPECT0,question=CryptoJS.enc.Latin1.parse(question).toString(CryptoJS.enc.Utf8),void this.trigger("question",[question])):this.abort());case CONST.SMPSTATE_EXPECT2:if(HLP.debug.call(this,"smp tlv 3"),ms=HLP.readLen(msg.msg.substr(0,4)),11!==ms)return this.abort();if(msg=HLP.unpackMPIs(11,msg.msg.substring(4)),!(HLP.checkGroup(msg[0],N_MINUS_2)&&HLP.checkGroup(msg[3],N_MINUS_2)&&HLP.checkGroup(msg[6],N_MINUS_2)&&HLP.checkGroup(msg[7],N_MINUS_2)))return this.abort();if(!HLP.ZKP(3,msg[1],HLP.multPowMod(G,msg[2],msg[0],msg[1],N)))return this.abort();if(!HLP.ZKP(4,msg[4],HLP.multPowMod(G,msg[5],msg[3],msg[4],N)))return this.abort();if(this.g3ao=msg[3],this.computeGs(msg[0],msg[3]),t1=HLP.multPowMod(this.g3,msg[9],msg[6],msg[8],N),t2=HLP.multPowMod(G,msg[9],this.g2,msg[10],N),t2=BigInt.multMod(t2,BigInt.powMod(msg[7],msg[8],N),N),!HLP.ZKP(5,msg[8],t1,t2))return this.abort();var r4=HLP.randomExponent();this.computePQ(r4);var r5=HLP.randomExponent(),r6=HLP.randomExponent(),tmp=HLP.multPowMod(G,r5,this.g2,r6,N),cP=HLP.smpHash(6,BigInt.powMod(this.g3,r5,N),tmp),d5=this.computeD(r5,r4,cP),d6=this.computeD(r6,this.secret,cP);this.QoQ=BigInt.divMod(this.q,msg[7],N),this.PoP=BigInt.divMod(this.p,msg[6],N),this.computeR(),r7=HLP.randomExponent(),tmp2=BigInt.powMod(this.QoQ,r7,N),cR=HLP.smpHash(7,BigInt.powMod(G,r7,N),tmp2),d7=this.computeD(r7,this.a3,cR),this.smpstate=CONST.SMPSTATE_EXPECT4,send=HLP.packINT(8)+HLP.packMPIs([this.p,this.q,cP,d5,d6,this.r,cR,d7]),send=HLP.packTLV(4,send);break;case CONST.SMPSTATE_EXPECT3:if(HLP.debug.call(this,"smp tlv 4"),ms=HLP.readLen(msg.msg.substr(0,4)),8!==ms)return this.abort();if(msg=HLP.unpackMPIs(8,msg.msg.substring(4)),!HLP.checkGroup(msg[0],N_MINUS_2)||!HLP.checkGroup(msg[1],N_MINUS_2)||!HLP.checkGroup(msg[5],N_MINUS_2))return this.abort();if(t1=HLP.multPowMod(this.g3,msg[3],msg[0],msg[2],N),t2=HLP.multPowMod(G,msg[3],this.g2,msg[4],N),t2=BigInt.multMod(t2,BigInt.powMod(msg[1],msg[2],N),N),!HLP.ZKP(6,msg[2],t1,t2))return this.abort();if(t3=HLP.multPowMod(G,msg[7],this.g3ao,msg[6],N),this.QoQ=BigInt.divMod(msg[1],this.q,N),t4=HLP.multPowMod(this.QoQ,msg[7],msg[5],msg[6],N),!HLP.ZKP(7,msg[6],t3,t4))return this.abort();this.computeR(),r7=HLP.randomExponent(),tmp2=BigInt.powMod(this.QoQ,r7,N),cR=HLP.smpHash(8,BigInt.powMod(G,r7,N),tmp2),d7=this.computeD(r7,this.a3,cR),send=HLP.packINT(3)+HLP.packMPIs([this.r,cR,d7]),send=HLP.packTLV(5,send),rab=this.computeRab(msg[5]),trust=!!BigInt.equals(rab,BigInt.divMod(msg[0],this.p,N)),this.trigger("trust",[trust,"answered"]),this.init();break;case CONST.SMPSTATE_EXPECT4:return HLP.debug.call(this,"smp tlv 5"),ms=HLP.readLen(msg.msg.substr(0,4)),3!==ms?this.abort():(msg=HLP.unpackMPIs(3,msg.msg.substring(4)),HLP.checkGroup(msg[0],N_MINUS_2)?(t3=HLP.multPowMod(G,msg[2],this.g3ao,msg[1],N),t4=HLP.multPowMod(this.QoQ,msg[2],msg[0],msg[1],N),HLP.ZKP(8,msg[1],t3,t4)?(rab=this.computeRab(msg[0]),trust=!!BigInt.equals(rab,this.PoP),this.trigger("trust",[trust,"asked"]),void this.init()):this.abort()):this.abort())}this.sendMsg(send)},SM.prototype.sendMsg=function(send){this.trigger("send",[this.ssid,"\x00"+send])},SM.prototype.rcvSecret=function(secret,question){HLP.debug.call(this,"receive secret");var fn,our=!1;this.smpstate===CONST.SMPSTATE_EXPECT0?fn=this.answer:(fn=this.initiate,our=!0),this.makeSecret(our,secret),fn.call(this,question)},SM.prototype.answer=function(){HLP.debug.call(this,"smp answer");var r4=HLP.randomExponent();this.computePQ(r4);var r5=HLP.randomExponent(),r6=HLP.randomExponent(),tmp=HLP.multPowMod(G,r5,this.g2,r6,N),cP=HLP.smpHash(5,BigInt.powMod(this.g3,r5,N),tmp),d5=this.computeD(r5,r4,cP),d6=this.computeD(r6,this.secret,cP);this.smpstate=CONST.SMPSTATE_EXPECT3;var send=HLP.packINT(11)+HLP.packMPIs([this.g2a,this.c2,this.d2,this.g3a,this.c3,this.d3,this.p,this.q,cP,d5,d6]);this.sendMsg(HLP.packTLV(3,send)) -},SM.prototype.initiate=function(question){HLP.debug.call(this,"smp initiate"),this.smpstate!==CONST.SMPSTATE_EXPECT1&&this.abort(),this.makeG2s();var r2=HLP.randomExponent(),r3=HLP.randomExponent();this.c2=this.computeC(1,r2),this.c3=this.computeC(2,r3),this.d2=this.computeD(r2,this.a2,this.c2),this.d3=this.computeD(r3,this.a3,this.c3),this.smpstate=CONST.SMPSTATE_EXPECT2;var send="",type=2;question&&(send+=question,send+="\x00",type=7),send+=HLP.packINT(6)+HLP.packMPIs([this.g2a,this.c2,this.d2,this.g3a,this.c3,this.d3]),this.sendMsg(HLP.packTLV(type,send))},SM.prototype.abort=function(){this.init(),this.sendMsg(HLP.packTLV(6,"")),this.trigger("abort")}}.call(this),function(){"use strict";function OTR(options){if(!(this instanceof OTR))return new OTR(options);if(options=options||{},options.priv&&!(options.priv instanceof DSA))throw new Error("Requires long-lived DSA key.");if(this.priv=options.priv?options.priv:new DSA,this.fragment_size=options.fragment_size||0,this.fragment_size<0)throw new Error("Fragment size must be a positive integer.");if(this.send_interval=options.send_interval||0,this.send_interval<0)throw new Error("Send interval must be a positive integer.");this.outgoing=[],this.our_instance_tag=options.instance_tag||OTR.makeInstanceTag(),this.debug=!!options.debug,this.smw=options.smw,this.init();var self=this;["sendMsg","receiveMsg"].forEach(function(meth){self[meth]=self[meth].bind(self)}),EventEmitter.call(this)}var CryptoJS,BigInt,EventEmitter,Worker,SMWPath,CONST,HLP,Parse,AKE,SM,DSA,root=this;"undefined"!=typeof module&&module.exports?(module.exports=OTR,CryptoJS=require("../vendor/crypto.js"),BigInt=require("../vendor/bigint.js"),EventEmitter=require("../vendor/eventemitter.js"),SMWPath=require("path").join(__dirname,"/sm-webworker.js"),CONST=require("./const.js"),HLP=require("./helpers.js"),Parse=require("./parse.js"),AKE=require("./ake.js"),SM=require("./sm.js"),DSA=require("./dsa.js"),OTR.CONST=CONST):(Object.keys(root.OTR).forEach(function(k){OTR[k]=root.OTR[k]}),root.OTR=OTR,CryptoJS=root.CryptoJS,BigInt=root.BigInt,EventEmitter=root.EventEmitter,Worker=root.Worker,SMWPath="sm-webworker.js",CONST=OTR.CONST,HLP=OTR.HLP,Parse=OTR.Parse,AKE=OTR.AKE,SM=OTR.SM,DSA=root.DSA);var G=BigInt.str2bigInt(CONST.G,10),N=BigInt.str2bigInt(CONST.N,16),MAX_INT=Math.pow(2,53)-1,MAX_UINT=Math.pow(2,31)-1;HLP.extend(OTR,EventEmitter),OTR.prototype.init=function(){this.msgstate=CONST.MSGSTATE_PLAINTEXT,this.authstate=CONST.AUTHSTATE_NONE,this.ALLOW_V2=!0,this.ALLOW_V3=!0,this.REQUIRE_ENCRYPTION=!1,this.SEND_WHITESPACE_TAG=!1,this.WHITESPACE_START_AKE=!1,this.ERROR_START_AKE=!1,Parse.initFragment(this),this.their_y=null,this.their_old_y=null,this.their_keyid=0,this.their_priv_pk=null,this.their_instance_tag="\x00\x00\x00\x00",this.our_dh=this.dh(),this.our_old_dh=this.dh(),this.our_keyid=2,this.sessKeys=[new Array(2),new Array(2)],this.storedMgs=[],this.oldMacKeys=[],this.sm=null,this._akeInit(),this.receivedPlaintext=!1},OTR.prototype._akeInit=function(){this.ake=new AKE(this),this.transmittedRS=!1,this.ssid=null},OTR.prototype._SMW=function(otr,reqs){this.otr=otr;var opts={path:SMWPath,seed:BigInt.getSeed};"object"==typeof otr.smw&&Object.keys(otr.smw).forEach(function(k){opts[k]=otr.smw[k]}),"undefined"!=typeof module&&module.exports&&(Worker=require("webworker-threads").Worker),this.worker=new Worker(opts.path);var self=this;this.worker.onmessage=function(e){var d=e.data;d&&self.trigger(d.method,d.args)},this.worker.postMessage({type:"seed",seed:opts.seed(),imports:opts.imports}),this.worker.postMessage({type:"init",reqs:reqs})},HLP.extend(OTR.prototype._SMW,EventEmitter),["handleSM","rcvSecret","abort"].forEach(function(m){OTR.prototype._SMW.prototype[m]=function(){this.worker.postMessage({type:"method",method:m,args:Array.prototype.slice.call(arguments,0)})}}),OTR.prototype._smInit=function(){var reqs={ssid:this.ssid,our_fp:this.priv.fingerprint(),their_fp:this.their_priv_pk.fingerprint(),debug:this.debug};this.smw?(this.sm&&this.sm.worker.terminate(),this.sm=new this._SMW(this,reqs)):this.sm=new SM(reqs);var self=this;["trust","abort","question"].forEach(function(e){self.sm.on(e,function(){self.trigger("smp",[e].concat(Array.prototype.slice.call(arguments)))})}),this.sm.on("send",function(ssid,send){self.ssid===ssid&&(send=self.prepareMsg(send),self.io(send))})},OTR.prototype.io=function(msg,meta){msg=[].concat(msg).map(function(m){return{msg:m,meta:meta}}),this.outgoing=this.outgoing.concat(msg);var self=this;!function send(first){if(!first){if(!self.outgoing.length)return;var elem=self.outgoing.shift();self.trigger("io",[elem.msg,elem.meta])}setTimeout(send,first?0:self.send_interval)}(!0)},OTR.prototype.dh=function(){var keys={privateKey:BigInt.randBigInt(320)};return keys.publicKey=BigInt.powMod(G,keys.privateKey,N),keys},OTR.prototype.DHSession=function DHSession(our_dh,their_y){if(!(this instanceof DHSession))return new DHSession(our_dh,their_y);var s=BigInt.powMod(their_y,our_dh.privateKey,N),secbytes=HLP.packMPI(s);this.id=HLP.mask(HLP.h2("\x00",secbytes),0,64);var sq=BigInt.greater(our_dh.publicKey,their_y),sendbyte=sq?"":"",rcvbyte=sq?"":"";this.sendenc=HLP.mask(HLP.h1(sendbyte,secbytes),0,128),this.sendmac=CryptoJS.SHA1(CryptoJS.enc.Latin1.parse(this.sendenc)),this.sendmac=this.sendmac.toString(CryptoJS.enc.Latin1),this.rcvenc=HLP.mask(HLP.h1(rcvbyte,secbytes),0,128),this.rcvmac=CryptoJS.SHA1(CryptoJS.enc.Latin1.parse(this.rcvenc)),this.rcvmac=this.rcvmac.toString(CryptoJS.enc.Latin1),this.rcvmacused=!1,this.extra_symkey=HLP.h2("ÿ",secbytes),this.send_counter=0,this.rcv_counter=0},OTR.prototype.rotateOurKeys=function(){var self=this;this.sessKeys[1].forEach(function(sk){sk&&sk.rcvmacused&&self.oldMacKeys.push(sk.rcvmac)}),this.our_old_dh=this.our_dh,this.our_dh=this.dh(),this.our_keyid+=1,this.sessKeys[1][0]=this.sessKeys[0][0],this.sessKeys[1][1]=this.sessKeys[0][1],this.sessKeys[0]=[this.their_y?new this.DHSession(this.our_dh,this.their_y):null,this.their_old_y?new this.DHSession(this.our_dh,this.their_old_y):null]},OTR.prototype.rotateTheirKeys=function(their_y){this.their_keyid+=1;var self=this;this.sessKeys.forEach(function(sk){sk[1]&&sk[1].rcvmacused&&self.oldMacKeys.push(sk[1].rcvmac)}),this.their_old_y=this.their_y,this.sessKeys[0][1]=this.sessKeys[0][0],this.sessKeys[1][1]=this.sessKeys[1][0],this.their_y=their_y,this.sessKeys[0][0]=new this.DHSession(this.our_dh,this.their_y),this.sessKeys[1][0]=new this.DHSession(this.our_old_dh,this.their_y)},OTR.prototype.prepareMsg=function(msg,esk){if(this.msgstate!==CONST.MSGSTATE_ENCRYPTED||0===this.their_keyid)return this.error("Not ready to encrypt.");var sessKeys=this.sessKeys[1][0];if(sessKeys.send_counter>=MAX_INT)return this.error("Should have rekeyed by now.");sessKeys.send_counter+=1;var ctr=HLP.packCtr(sessKeys.send_counter),send=this.ake.otr_version+"",v3=this.ake.otr_version===CONST.OTR_VERSION_3;if(v3&&(send+=this.our_instance_tag,send+=this.their_instance_tag),send+="\x00",send+=HLP.packINT(this.our_keyid-1),send+=HLP.packINT(this.their_keyid),send+=HLP.packMPI(this.our_dh.publicKey),send+=ctr.substring(0,8),Math.ceil(msg.length/8)>=MAX_UINT)return this.error("Message is too long.");var aes=HLP.encryptAes(CryptoJS.enc.Latin1.parse(msg),sessKeys.sendenc,ctr);return send+=HLP.packData(aes),send+=HLP.make1Mac(send,sessKeys.sendmac),send+=HLP.packData(this.oldMacKeys.splice(0).join("")),send=HLP.wrapMsg(send,this.fragment_size,v3,this.our_instance_tag,this.their_instance_tag),send[0]?this.error(send[0]):(esk&&this.trigger("file",["send",sessKeys.extra_symkey,esk]),send[1])},OTR.prototype.handleDataMsg=function(msg){var vt=msg.version+msg.type;this.ake.otr_version===CONST.OTR_VERSION_3&&(vt+=msg.instance_tags);var types=["BYTE","INT","INT","MPI","CTR","DATA","MAC","DATA"];msg=HLP.splitype(types,msg.msg);var ign=""===msg[0];if(this.msgstate!==CONST.MSGSTATE_ENCRYPTED||8!==msg.length)return void(ign||this.error("Received an unreadable encrypted message.",!0));var our_keyid=this.our_keyid-HLP.readLen(msg[2]),their_keyid=this.their_keyid-HLP.readLen(msg[1]);if(0>our_keyid||our_keyid>1)return void(ign||this.error("Not of our latest keys.",!0));if(0>their_keyid||their_keyid>1)return void(ign||this.error("Not of your latest keys.",!0));var their_y=their_keyid?this.their_old_y:this.their_y;if(1===their_keyid&&!their_y)return void(ign||this.error("Do not have that key."));var sessKeys=this.sessKeys[our_keyid][their_keyid],ctr=HLP.unpackCtr(msg[4]);if(ctr<=sessKeys.rcv_counter)return void(ign||this.error("Counter in message is not larger."));sessKeys.rcv_counter=ctr,vt+=msg.slice(0,6).join("");var vmac=HLP.make1Mac(vt,sessKeys.rcvmac);if(!HLP.compare(msg[6],vmac))return void(ign||this.error("MACs do not match."));sessKeys.rcvmacused=!0;var out=HLP.decryptAes(msg[5].substring(4),sessKeys.rcvenc,HLP.padCtr(msg[4]));out=out.toString(CryptoJS.enc.Latin1),our_keyid||this.rotateOurKeys(),their_keyid||this.rotateTheirKeys(HLP.readMPI(msg[3]));var ind=out.indexOf("\x00");return~ind&&(this.handleTLVs(out.substring(ind+1),sessKeys),out=out.substring(0,ind)),out=CryptoJS.enc.Latin1.parse(out),out.toString(CryptoJS.enc.Utf8)},OTR.prototype.handleTLVs=function(tlvs,sessKeys){for(var type,len,msg;tlvs.length&&(type=HLP.unpackSHORT(tlvs.substr(0,2)),len=HLP.unpackSHORT(tlvs.substr(2,2)),msg=tlvs.substr(4,len),!(msg.length<len));){switch(type){case 1:this.msgstate=CONST.MSGSTATE_FINISHED,this.trigger("status",[CONST.STATUS_END_OTR]);break;case 2:case 3:case 4:case 5:case 6:case 7:if(this.msgstate!==CONST.MSGSTATE_ENCRYPTED)return void(this.sm&&this.sm.abort());this.sm||this._smInit(),this.sm.handleSM({msg:msg,type:type});break;case 8:msg=msg.substring(4),msg=CryptoJS.enc.Latin1.parse(msg),msg=msg.toString(CryptoJS.enc.Utf8),this.trigger("file",["receive",sessKeys.extra_symkey,msg])}tlvs=tlvs.substring(4+len)}},OTR.prototype.smpSecret=function(secret,question){return this.msgstate!==CONST.MSGSTATE_ENCRYPTED?this.error("Must be encrypted for SMP."):"string"!=typeof secret||secret.length<1?this.error("Secret is required."):(this.sm||this._smInit(),secret=CryptoJS.enc.Utf8.parse(secret).toString(CryptoJS.enc.Latin1),question=CryptoJS.enc.Utf8.parse(question).toString(CryptoJS.enc.Latin1),void this.sm.rcvSecret(secret,question))},OTR.prototype.sendQueryMsg=function(){var versions={},msg=CONST.OTR_TAG;this.ALLOW_V2&&(versions[2]=!0),this.ALLOW_V3&&(versions[3]=!0);var vs=Object.keys(versions);vs.length&&(msg+="v",vs.forEach(function(v){"1"!==v&&(msg+=v)}),msg+="?"),this.io(msg),this.trigger("status",[CONST.STATUS_SEND_QUERY])},OTR.prototype.sendMsg=function(msg,meta){switch((this.REQUIRE_ENCRYPTION||this.msgstate!==CONST.MSGSTATE_PLAINTEXT)&&(msg=CryptoJS.enc.Utf8.parse(msg),msg=msg.toString(CryptoJS.enc.Latin1)),this.msgstate){case CONST.MSGSTATE_PLAINTEXT:if(this.REQUIRE_ENCRYPTION)return this.storedMgs.push({msg:msg,meta:meta}),void this.sendQueryMsg();this.SEND_WHITESPACE_TAG&&!this.receivedPlaintext&&(msg+=CONST.WHITESPACE_TAG,this.ALLOW_V3&&(msg+=CONST.WHITESPACE_TAG_V3),this.ALLOW_V2&&(msg+=CONST.WHITESPACE_TAG_V2));break;case CONST.MSGSTATE_FINISHED:return this.storedMgs.push({msg:msg,meta:meta}),void this.error("Message cannot be sent at this time.");case CONST.MSGSTATE_ENCRYPTED:msg=this.prepareMsg(msg);break;default:throw new Error("Unknown message state.")}msg&&this.io(msg,meta)},OTR.prototype.receiveMsg=function(msg){if(msg=Parse.parseMsg(this,msg)){switch(msg.cls){case"error":return void this.error(msg.msg);case"ake":if(msg.version===CONST.OTR_VERSION_3&&this.checkInstanceTags(msg.instance_tags))return;return void this.ake.handleAKE(msg);case"data":if(msg.version===CONST.OTR_VERSION_3&&this.checkInstanceTags(msg.instance_tags))return;msg.msg=this.handleDataMsg(msg),msg.encrypted=!0;break;case"query":this.msgstate===CONST.MSGSTATE_ENCRYPTED&&this._akeInit(),this.doAKE(msg);break;default:(this.REQUIRE_ENCRYPTION||this.msgstate!==CONST.MSGSTATE_PLAINTEXT)&&this.error("Received an unencrypted message."),this.receivedPlaintext=!0,this.WHITESPACE_START_AKE&&msg.ver.length>0&&this.doAKE(msg)}msg.msg&&this.trigger("ui",[msg.msg,!!msg.encrypted])}},OTR.prototype.checkInstanceTags=function(it){var their_it=HLP.readLen(it.substr(0,4)),our_it=HLP.readLen(it.substr(4,4));if(our_it&&our_it!==HLP.readLen(this.our_instance_tag))return!0;if(HLP.readLen(this.their_instance_tag)){if(HLP.readLen(this.their_instance_tag)!==their_it)return!0}else{if(100>their_it)return!0;this.their_instance_tag=HLP.packINT(their_it)}},OTR.prototype.doAKE=function(msg){this.ALLOW_V3&&~msg.ver.indexOf(CONST.OTR_VERSION_3)?this.ake.initiateAKE(CONST.OTR_VERSION_3):this.ALLOW_V2&&~msg.ver.indexOf(CONST.OTR_VERSION_2)?this.ake.initiateAKE(CONST.OTR_VERSION_2):this.error("OTR conversation requested, but no compatible protocol version found.")},OTR.prototype.error=function(err,send){return send?(this.debug||(err="An OTR error has occurred."),err="?OTR Error:"+err,void this.io(err)):void this.trigger("error",[err])},OTR.prototype.sendStored=function(){var self=this;this.storedMgs.splice(0).forEach(function(elem){var msg=self.prepareMsg(elem.msg);self.io(msg,elem.meta)})},OTR.prototype.sendFile=function(filename){if(this.msgstate!==CONST.MSGSTATE_ENCRYPTED)return this.error("Not ready to encrypt.");if(this.ake.otr_version!==CONST.OTR_VERSION_3)return this.error("Protocol v3 required.");if(!filename)return this.error("Please specify a filename.");var l1name=CryptoJS.enc.Utf8.parse(filename);if(l1name=l1name.toString(CryptoJS.enc.Latin1),l1name.length>=65532)return this.error("filename is too long.");var msg="\x00";msg+="\x00\b",msg+=HLP.packSHORT(4+l1name.length),msg+="\x00\x00\x00",msg+=l1name,msg=this.prepareMsg(msg,filename),this.io(msg)},OTR.prototype.endOtr=function(){this.msgstate===CONST.MSGSTATE_ENCRYPTED&&(this.sendMsg("\x00\x00\x00\x00"),this.sm&&(this.smw&&this.sm.worker.terminate(),this.sm=null)),this.msgstate=CONST.MSGSTATE_PLAINTEXT,this.receivedPlaintext=!1,this.trigger("status",[CONST.STATUS_END_OTR])},OTR.makeInstanceTag=function(){var num=BigInt.randBigInt(32);return BigInt.greater(BigInt.str2bigInt("100",16),num)?OTR.makeInstanceTag():HLP.packINT(parseInt(BigInt.bigInt2str(num,10),10))}}.call(this),{OTR:this.OTR,DSA:this.DSA}}); - -};
--- a/src/browser/otr.min.js_README Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,10 +0,0 @@ -The file otr.min.js includes: - - minified versions of the following otr.js dependencies: - otr/dep/bigint.js otr/dep/crypto.js otr/dep/eventemitter.js - The following minification tool has been used: yui compressor - - minified version of otr.js taken from the project homepage - -All original files can be retrieved from otr.js repository: - https://github.com/arlolra/otr/tree/master/build - -See the README file of Libervia, or inside otr.min.js itself for licence information.
--- a/src/browser/public/contrat_social.html Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,110 +0,0 @@ -<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> -<html><head> - - <meta content="text/html; charset=ISO-8859-1" http-equiv="content-type"> - <title>Salut Toi: Contrat Social</title> - - -</head><body> -Le projet Salut Toi est n d'un besoin de protection de nos -liberts, de notre vie prive et de notre indpendance. Il se veut -garant des droits et liberts qu'un utilisateur a vis vis de ses -propres informations, des informations numriques sur sa vie ou celles -de ses connaissances, des donnes qu'il manipule; et se veut galement -un point de contact humain, ne se substituant pas aux rapports rels, -mais au contraire les facilitant.<br> - -Salut Toi lutte et luttera toujours contre toute forme de main mise -sur les technologies par des intrts privs. Le rseau global doit -appartenir tous, et tre un point d'expression et de libert pour -l'Humanit.<br> - -<br> - - ce titre, Salut Toi et ceux qui y participent se basent sur un -contrat social, un engagement vis vis de ceux qui l'utilisent. Ce -contrat consiste en les points suivants:<br> - -<ul> - - <li>nous plaons la <span style="font-style: italic;">Libert</span> en tte de nos priorits: libert de -l'utilisateur, libert vis vis de ses donnes. Pour cela, Salut -Toi est un logiciel Libre - condition essentielle -, et son -infrastructure se base galement sur des logiciels Libres, c'est dire -des logiciels qui respectent ces 4 liberts fondamentales - <ul> - - <li>la libert d'excuter le programme, pour tous les usages,</li> - - </ul> - <ul> - - <li>la libert d'tudier le fonctionnement du programme et de -l'adapter ses besoins,</li> - - </ul> - <ul> - - <li>la libert de redistribuer des copies du programme,</li> - - </ul> - <ul> - - <li>la libert d'amliorer le programme et de distribuer ces -amliorations au public.<br> -</li> - - </ul> -</li> - - - - - -Vous avez ainsi la possibilit d'installer votre propre version de -Salut Toi sur votre propre machine, d'en vrifier - et de -comprendre - ainsi son fonctionnement, de l'adapter vos besoins, d'en -faire profiter vos amis. - - <li>Les informations vous concernant vous appartiennent, et nous -n'aurons pas la prtention - et l'indcence ! - de considrer le -contenu que vous produisez ou faites circuler via Salut Toi comme -nous appartenant. De mme, nous nous engageons ne jamais faire de -profit en revendant vos informations personnelles.</li> - <li>Nous incitons fortement la <span style="text-decoration: underline;">dcentralisation gnralise</span>. -Salut Toi tant bas sur un protocole dcentralis (XMPP), il l'est -lui-mme par nature. La dcentralisation est essentielle pour une -meilleure protection de vos informations, une meilleure rsistance la -censure ou aux pannes, et pour viter les drives autoritaires.</li> - <li>Luttant contre les tentatives de contrle priv et les abus -commerciaux du rseau global, et afin de garder notre indpendance, -nous nous refusons toute forme de publicit: vous ne verrez <span style="font-weight: bold;">jamais</span> -de forme de rclame commerciale de notre fait.</li> - <li>L'<span style="font-style: italic;">galit</span> des utilisateurs est essentielle pour nous, nous -refusons toute forme de discrimination, que ce soit pour une zone -gographique, une catgorie de la population, ou tout autre raison.</li> - <li>Nous ferons tout notre possible pour lutter contre toute -tentative de censure. Le rseau global doit tre un moyen d'expression -pour tous.</li> - <li>Nous refusons toute ide d'autorit absolue en ce qui concerne -les dcisions prises pour Salut Toi et son fonctionnement, et le -choix de la dcentralisation et l'utilisation de logiciel Libre permet -de lutter contre toute forme de hirarchie.</li> - - <li>L'ide de <span style="font-style: italic;">Fraternit</span> est essentielle, aussi: - <ul> - <li>nous ferons notre -possible pour aider les utilisateurs, quel que soit leur niveau</li> - <li>de mme, des efforts seront fait quant -l'accessibilit pour tous</li> - <li> Salut Toi , -XMPP, et les technologies utilises facilitent les changes -lectroniques, mais nous dsirons mettre l'accent sur les rencontres -relles et humaines: nous favoriserons toujours le rel sur le virtuel.</li> - </ul> -</li> - - -</ul> - -</body></html>
--- a/src/browser/public/favico.min.js Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,9 +0,0 @@ -/** - * This file is distributed with Libervia and is sublicensed under AGPL v3 (or any later version) as allowed by its original license. - * - * @license MIT - * @fileOverview Favico animations - * @author Miroslav Magda, http://blog.ejci.net - * @version 0.3.9 - */ -!function(){var e=function(e){"use strict";function t(e){if(e.paused||e.ended||g)return!1;try{f.clearRect(0,0,s,l),f.drawImage(e,0,0,s,l)}catch(o){}p=setTimeout(t,S.duration,e),O.setIcon(h)}function o(e){var t=/^#?([a-f\d])([a-f\d])([a-f\d])$/i;e=e.replace(t,function(e,t,o,n){return t+t+o+o+n+n});var o=/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(e);return o?{r:parseInt(o[1],16),g:parseInt(o[2],16),b:parseInt(o[3],16)}:!1}function n(e,t){var o,n={};for(o in e)n[o]=e[o];for(o in t)n[o]=t[o];return n}function r(){return b.hidden||b.msHidden||b.webkitHidden||b.mozHidden}e=e?e:{};var i,a,l,s,h,f,c,d,u,y,w,g,x,m,p,b,v={bgColor:"#d00",textColor:"#fff",fontFamily:"sans-serif",fontStyle:"bold",type:"circle",position:"down",animation:"slide",elementId:!1,dataUrl:!1,win:window};x={},x.ff="undefined"!=typeof InstallTrigger,x.chrome=!!window.chrome,x.opera=!!window.opera||navigator.userAgent.indexOf("Opera")>=0,x.ie=/*@cc_on!@*/!1,x.safari=Object.prototype.toString.call(window.HTMLElement).indexOf("Constructor")>0,x.supported=x.chrome||x.ff||x.opera;var C=[];w=function(){},d=g=!1;var E=function(){i=n(v,e),i.bgColor=o(i.bgColor),i.textColor=o(i.textColor),i.position=i.position.toLowerCase(),i.animation=S.types[""+i.animation]?i.animation:v.animation,b=i.win.document;var t=i.position.indexOf("up")>-1,r=i.position.indexOf("left")>-1;if(t||r)for(var d=0;d<S.types[""+i.animation].length;d++){var u=S.types[""+i.animation][d];t&&(u.y=u.y<.6?u.y-.4:u.y-2*u.y+(1-u.w)),r&&(u.x=u.x<.6?u.x-.4:u.x-2*u.x+(1-u.h)),S.types[""+i.animation][d]=u}i.type=A[""+i.type]?i.type:v.type,a=O.getIcon(),h=document.createElement("canvas"),c=document.createElement("img"),a.hasAttribute("href")?(c.setAttribute("crossOrigin","anonymous"),c.setAttribute("src",a.getAttribute("href")),c.onload=function(){l=c.height>0?c.height:32,s=c.width>0?c.width:32,h.height=l,h.width=s,f=h.getContext("2d"),M.ready()}):(c.setAttribute("src",""),l=32,s=32,c.height=l,c.width=s,h.height=l,h.width=s,f=h.getContext("2d"),M.ready())},M={};M.ready=function(){d=!0,M.reset(),w()},M.reset=function(){d&&(C=[],u=!1,y=!1,f.clearRect(0,0,s,l),f.drawImage(c,0,0,s,l),O.setIcon(h),window.clearTimeout(m),window.clearTimeout(p))},M.start=function(){if(d&&!y){var e=function(){u=C[0],y=!1,C.length>0&&(C.shift(),M.start())};if(C.length>0){y=!0;var t=function(){["type","animation","bgColor","textColor","fontFamily","fontStyle"].forEach(function(e){e in C[0].options&&(i[e]=C[0].options[e])}),S.run(C[0].options,function(){e()},!1)};u?S.run(u.options,function(){t()},!0):t()}}};var A={},I=function(e){return e.n="number"==typeof e.n?Math.abs(0|e.n):e.n,e.x=s*e.x,e.y=l*e.y,e.w=s*e.w,e.h=l*e.h,e.len=(""+e.n).length,e};A.circle=function(e){e=I(e);var t=!1;2===e.len?(e.x=e.x-.4*e.w,e.w=1.4*e.w,t=!0):e.len>=3&&(e.x=e.x-.65*e.w,e.w=1.65*e.w,t=!0),f.clearRect(0,0,s,l),f.drawImage(c,0,0,s,l),f.beginPath(),f.font=i.fontStyle+" "+Math.floor(e.h*(e.n>99?.85:1))+"px "+i.fontFamily,f.textAlign="center",t?(f.moveTo(e.x+e.w/2,e.y),f.lineTo(e.x+e.w-e.h/2,e.y),f.quadraticCurveTo(e.x+e.w,e.y,e.x+e.w,e.y+e.h/2),f.lineTo(e.x+e.w,e.y+e.h-e.h/2),f.quadraticCurveTo(e.x+e.w,e.y+e.h,e.x+e.w-e.h/2,e.y+e.h),f.lineTo(e.x+e.h/2,e.y+e.h),f.quadraticCurveTo(e.x,e.y+e.h,e.x,e.y+e.h-e.h/2),f.lineTo(e.x,e.y+e.h/2),f.quadraticCurveTo(e.x,e.y,e.x+e.h/2,e.y)):f.arc(e.x+e.w/2,e.y+e.h/2,e.h/2,0,2*Math.PI),f.fillStyle="rgba("+i.bgColor.r+","+i.bgColor.g+","+i.bgColor.b+","+e.o+")",f.fill(),f.closePath(),f.beginPath(),f.stroke(),f.fillStyle="rgba("+i.textColor.r+","+i.textColor.g+","+i.textColor.b+","+e.o+")","number"==typeof e.n&&e.n>999?f.fillText((e.n>9999?9:Math.floor(e.n/1e3))+"k+",Math.floor(e.x+e.w/2),Math.floor(e.y+e.h-.2*e.h)):f.fillText(e.n,Math.floor(e.x+e.w/2),Math.floor(e.y+e.h-.15*e.h)),f.closePath()},A.rectangle=function(e){e=I(e);var t=!1;2===e.len?(e.x=e.x-.4*e.w,e.w=1.4*e.w,t=!0):e.len>=3&&(e.x=e.x-.65*e.w,e.w=1.65*e.w,t=!0),f.clearRect(0,0,s,l),f.drawImage(c,0,0,s,l),f.beginPath(),f.font=i.fontStyle+" "+Math.floor(e.h*(e.n>99?.9:1))+"px "+i.fontFamily,f.textAlign="center",f.fillStyle="rgba("+i.bgColor.r+","+i.bgColor.g+","+i.bgColor.b+","+e.o+")",f.fillRect(e.x,e.y,e.w,e.h),f.fillStyle="rgba("+i.textColor.r+","+i.textColor.g+","+i.textColor.b+","+e.o+")","number"==typeof e.n&&e.n>999?f.fillText((e.n>9999?9:Math.floor(e.n/1e3))+"k+",Math.floor(e.x+e.w/2),Math.floor(e.y+e.h-.2*e.h)):f.fillText(e.n,Math.floor(e.x+e.w/2),Math.floor(e.y+e.h-.15*e.h)),f.closePath()};var T=function(e,t){t=("string"==typeof t?{animation:t}:t)||{},w=function(){try{if("number"==typeof e?e>0:""!==e){var n={type:"badge",options:{n:e}};if("animation"in t&&S.types[""+t.animation]&&(n.options.animation=""+t.animation),"type"in t&&A[""+t.type]&&(n.options.type=""+t.type),["bgColor","textColor"].forEach(function(e){e in t&&(n.options[e]=o(t[e]))}),["fontStyle","fontFamily"].forEach(function(e){e in t&&(n.options[e]=t[e])}),C.push(n),C.length>100)throw new Error("Too many badges requests in queue.");M.start()}else M.reset()}catch(r){throw new Error("Error setting badge. Message: "+r.message)}},d&&w()},U=function(e){w=function(){try{var t=e.width,o=e.height,n=document.createElement("img"),r=o/l>t/s?t/s:o/l;n.setAttribute("crossOrigin","anonymous"),n.setAttribute("src",e.getAttribute("src")),n.height=o/r,n.width=t/r,f.clearRect(0,0,s,l),f.drawImage(n,0,0,s,l),O.setIcon(h)}catch(i){throw new Error("Error setting image. Message: "+i.message)}},d&&w()},R=function(e){w=function(){try{if("stop"===e)return g=!0,M.reset(),void(g=!1);e.addEventListener("play",function(){t(this)},!1)}catch(o){throw new Error("Error setting video. Message: "+o.message)}},d&&w()},L=function(e){if(window.URL&&window.URL.createObjectURL||(window.URL=window.URL||{},window.URL.createObjectURL=function(e){return e}),x.supported){var o=!1;navigator.getUserMedia=navigator.getUserMedia||navigator.oGetUserMedia||navigator.msGetUserMedia||navigator.mozGetUserMedia||navigator.webkitGetUserMedia,w=function(){try{if("stop"===e)return g=!0,M.reset(),void(g=!1);o=document.createElement("video"),o.width=s,o.height=l,navigator.getUserMedia({video:!0,audio:!1},function(e){o.src=URL.createObjectURL(e),o.play(),t(o)},function(){})}catch(n){throw new Error("Error setting webcam. Message: "+n.message)}},d&&w()}},O={};O.getIcon=function(){var e=!1,t=function(){for(var e=b.getElementsByTagName("head")[0].getElementsByTagName("link"),t=e.length,o=t-1;o>=0;o--)if(/(^|\s)icon(\s|$)/i.test(e[o].getAttribute("rel")))return e[o];return!1};return i.element?e=i.element:i.elementId?(e=b.getElementById(i.elementId),e.setAttribute("href",e.getAttribute("src"))):(e=t(),e===!1&&(e=b.createElement("link"),e.setAttribute("rel","icon"),b.getElementsByTagName("head")[0].appendChild(e))),e.setAttribute("type","image/png"),e},O.setIcon=function(e){var t=e.toDataURL("image/png");if(i.dataUrl&&i.dataUrl(t),i.element)i.element.setAttribute("href",t),i.element.setAttribute("src",t);else if(i.elementId){var o=b.getElementById(i.elementId);o.setAttribute("href",t),o.setAttribute("src",t)}else if(x.ff||x.opera){var n=a;a=b.createElement("link"),x.opera&&a.setAttribute("rel","icon"),a.setAttribute("rel","icon"),a.setAttribute("type","image/png"),b.getElementsByTagName("head")[0].appendChild(a),a.setAttribute("href",t),n.parentNode&&n.parentNode.removeChild(n)}else a.setAttribute("href",t)};var S={};return S.duration=40,S.types={},S.types.fade=[{x:.4,y:.4,w:.6,h:.6,o:0},{x:.4,y:.4,w:.6,h:.6,o:.1},{x:.4,y:.4,w:.6,h:.6,o:.2},{x:.4,y:.4,w:.6,h:.6,o:.3},{x:.4,y:.4,w:.6,h:.6,o:.4},{x:.4,y:.4,w:.6,h:.6,o:.5},{x:.4,y:.4,w:.6,h:.6,o:.6},{x:.4,y:.4,w:.6,h:.6,o:.7},{x:.4,y:.4,w:.6,h:.6,o:.8},{x:.4,y:.4,w:.6,h:.6,o:.9},{x:.4,y:.4,w:.6,h:.6,o:1}],S.types.none=[{x:.4,y:.4,w:.6,h:.6,o:1}],S.types.pop=[{x:1,y:1,w:0,h:0,o:1},{x:.9,y:.9,w:.1,h:.1,o:1},{x:.8,y:.8,w:.2,h:.2,o:1},{x:.7,y:.7,w:.3,h:.3,o:1},{x:.6,y:.6,w:.4,h:.4,o:1},{x:.5,y:.5,w:.5,h:.5,o:1},{x:.4,y:.4,w:.6,h:.6,o:1}],S.types.popFade=[{x:.75,y:.75,w:0,h:0,o:0},{x:.65,y:.65,w:.1,h:.1,o:.2},{x:.6,y:.6,w:.2,h:.2,o:.4},{x:.55,y:.55,w:.3,h:.3,o:.6},{x:.5,y:.5,w:.4,h:.4,o:.8},{x:.45,y:.45,w:.5,h:.5,o:.9},{x:.4,y:.4,w:.6,h:.6,o:1}],S.types.slide=[{x:.4,y:1,w:.6,h:.6,o:1},{x:.4,y:.9,w:.6,h:.6,o:1},{x:.4,y:.9,w:.6,h:.6,o:1},{x:.4,y:.8,w:.6,h:.6,o:1},{x:.4,y:.7,w:.6,h:.6,o:1},{x:.4,y:.6,w:.6,h:.6,o:1},{x:.4,y:.5,w:.6,h:.6,o:1},{x:.4,y:.4,w:.6,h:.6,o:1}],S.run=function(e,t,o,a){var l=S.types[r()?"none":i.animation];return a=o===!0?"undefined"!=typeof a?a:l.length-1:"undefined"!=typeof a?a:0,t=t?t:function(){},a<l.length&&a>=0?(A[i.type](n(e,l[a])),m=setTimeout(function(){o?a-=1:a+=1,S.run(e,t,o,a)},S.duration),O.setIcon(h),void 0):void t()},E(),{badge:T,video:R,image:U,webcam:L,reset:M.reset,browser:{supported:x.supported}}};"undefined"!=typeof define&&define.amd?define([],function(){return e}):"undefined"!=typeof module&&module.exports?module.exports=e:this.Favico=e}();
--- a/src/browser/public/libervia.css Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1687 +0,0 @@ -/* -Libervia: a Salut à Toi frontend -Copyright (C) 2011-2016 Jérôme Poisson <goffi@goffi.org> -Copyright (C) 2011 Adrien Vigneron <adrienvigneron@mailoo.org> -Copyright (C) 2013-2016 Adrien Cossa <souliane@mailoo.org> - -This program 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. - -This program 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 this program. If not, see <http://www.gnu.org/licenses/>. -*/ - - -/* - * CSS Reset: see http://pyjs.org/wiki/csshellandhowtodealwithit/ - */ - -/* reset/default styles */ - -html, body, div, span, applet, object, iframe, -p, blockquote, pre, -a, abbr, acronym, address, big, cite, code, -del, dfn, em, font, img, ins, kbd, q, s, samp, -small, strike, strong, sub, sup, tt, var, -b, u, i, center, dl, dt, dd, li, -fieldset, form, label, legend, table, caption, -tbody, tfoot, thead, tr, th, td { - margin: 0; - padding: 0; - border: 0; - outline: 0; - font-size: 100%; - vertical-align: baseline; - background: transparent; - color: #444; -} - -/* styles for displaying rich text - START */ -h1, h2, h3, h4, h5, h6 { - margin: 0; - padding: 0; - border: 0; - outline: 0; - vertical-align: baseline; - background: transparent; - color: #444; - border-bottom: 1px solid rgb(170, 170, 170); - margin-bottom: 0.6em; -} -ol, ul { - margin: 0; - border: 0; - outline: 0; - font-size: 100%; - vertical-align: baseline; - background: transparent; - color: #444; -} -a:link { - color: blue; -} -.bubble p { - margin: 0.4em 0em; -} -.bubble img { - /* /!\ setting a max-width percentage value affects the toolbar icons */ - max-width: 600px; -} - -/* styles for displaying rich text - END */ - -blockquote, q { quotes: none; } - -blockquote:before, blockquote:after, -q:before, q:after { - content: ''; - content: none; -} - -:focus { outline: 0; } -ins { text-decoration: none; } -del { text-decoration: line-through; } - -table { - border-collapse: collapse; - border-spacing: 0; -} - -/* pyjamas iframe hide */ -iframe { position: absolute; } - - -html, body { - width: 100%; - height: 100%; - min-height: 100%; - -} - -body { - line-height: 1em; - font-size: 1em; - overflow: auto; - -} - -.scrollpanel { - margin-bottom: -10000px; - -} - -.iescrollpanelfix { - position: relative; - top: 100%; - margin-bottom: -10000px; - -} - -/* undo part of the above (non-IE) */ -html>body .iescrollpanelfix { position: static; } - -/* CSS Reset END */ - -body { - background-color: #fff; - font: normal 0.8em/1.5em Arial, Helvetica, sans-serif; -} - -.header { - background-color: #eee; - border-bottom: 1px solid #ddd; - width: 100%; - height: 64px; -} - -.mainPanel { - width: 100%; - height: 100%; -} - -.mainMenuBar { - background-color: #222; - background: -webkit-gradient(linear, left top, left bottom, from(#444444), to(#222222)); - background: -webkit-linear-gradient(top, #444444, #222222); - background: linear-gradient(to bottom, #444444, #222222); - height: 28px; - padding: 5px 5px 0 5px; - border: 1px solid #ddd; - border-radius: 0 0 1em 1em; - line-height: 100%; - -webkit-box-shadow: 0px 1px 4px #000; - box-shadow: 0px 1px 4px #000; - display: inline-block; - position: absolute; - left: 20px; - right: 20px; - width: auto; -} - -.mainMenuBar .gwt-MenuItem { - padding: 3px 15px; - text-decoration: none; - font-weight: bold; - height: 100%; - color: #e7e5e5; - border-radius: 1em 1em 1em 1em; - text-shadow: 0 1px 1px rgba(0, 0, 0, 0.4); - -webkit-transition: color 0.2s linear; - transition: color 0.2s linear; -} - -.mainMenuBar .gwt-MenuItem-selected { - background-color: #eee; - background: -webkit-gradient(linear, left top, left bottom, from(#eee), to(#aaa)); - background: -webkit-linear-gradient(top, #eee, #aaa); - background: linear-gradient(to bottom, #eee, #aaa); - color: #444; - text-shadow: 0 1px 0 rgba(255, 255, 255, 0.6); -} - -/* Menu bars and items */ - -.gwt-MenuBar { - /* Common to all menu bars */ - margin: 0; -} - -.gwt-MenuBar table { - /* Common to all tables within a menu bar */ - width: 100%; - display: inline-table; -} - -.gwt-MenuBar-horizontal { - /* Specific to horizontal menu bars*/ -} - -.gwt-MenuBar-vertical { - /* Specific to vertical menu bars*/ - background-color: #fff; - background: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#ccc)); - background: -webkit-linear-gradient(top, #fff, #ccc); - background: linear-gradient(to bottom, #fff, #ccc); - height: 100%; - min-width: 148px; - padding: 0; - border: solid 1px #aaa; - border-radius: 0 0 10px 10px; - -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, .3); - box-shadow: 0 1px 3px rgba(0, 0, 0, .3); -} - -.gwt-MenuItem img { - /* Common to all images within a menu item */ - padding-right: 2px; -} - -.gwt-MenuBar .gwt-MenuItem { - /* Common to items of all menu bars */ -} - -.gwt-MenuBar-horizontal .gwt-MenuItem { - /* Specific to items of horizontal menu bars*/ -} - -.gwt-MenuBar-vertical .gwt-MenuItem { - /* Specific to items of vertical menu bars*/ - padding: 8px 15px; -} - -.gwt-MenuBar .gwt-MenuItem-selected { - /* Common to all selected items */ - cursor: pointer; -} - -.gwt-MenuBar-horizontal .gwt-MenuItem-selected { - /* Specific to selected items of horizontal menu bars */ -} - -.gwt-MenuBar-vertical .gwt-MenuItem-selected { - /* Specific to selected items of vertical menu bars */ - background: #cf2828 !important; - background: -webkit-gradient(linear, left top, left bottom, from(#cf2828), to(#981a1a)) !important; - background: -webkit-linear-gradient(top, #cf2828, #981a1a) !important; - background: linear-gradient(to bottom, #cf2828, #981a1a) !important; - color: #fff !important; - border-radius: 0 0 0 0; - text-shadow: 0 1px 1px rgba(0, 0, 0, .1); - -webkit-transition: color 0.2s linear; - transition: color 0.2s linear; -} - -.gwt-MenuBar-vertical tr:last-child td { - /* Specific to last items of vertical menus */ - border-radius: 0 0 9px 9px !important; -} - -.menuLastPopup .gwt-MenuBar-vertical { - /* Specific to the last popup menu of the main menu bar */ - border-top-right-radius: 9px 9px; -} - -.menuLastPopup .gwt-MenuBar-vertical tr:first-child td { - /* Specific to the first item of the last popup menu of the main menu bar */ - border-radius: 0px 9px 0px 0px !important; -} - -.menuSeparator { - width: 100%; -} - -.menuSeparator.gwt-MenuItem-selected { - border: 0; - background: inherit; - cursor: default; -} - -.menuFlattenedCategory { - font-weight: bold; - font-style: italic; - padding: 8px 5px; - cursor: default; -} - -.menuFlattenedCategory.gwt-MenuItem-selected { - /* !important are needed for the style to not be overwritten when the item is selected */ - background-color: inherit !important; - background: inherit !important; - color: #444 !important; - cursor: default !important; -} - -/* Misc Pyjamas stuff */ - -.gwt-DialogBox { - padding: 10px; - border: 1px solid #aaa; - background-color: #fff; - background: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#ccc)); - background: -webkit-linear-gradient(top, #fff, #ccc); - background: linear-gradient(to bottom, #fff, #ccc); - border-radius: 9px 9px 9px 9px; - -webkit-box-shadow: 0px 1px 4px #000; - box-shadow: 0px 1px 4px #000; -} - -.gwt-DialogBox .Caption { - height: 20px; - font-size: 1.3em !important; - background-color: #cf2828; - background: #cf2828 !important; - background: -webkit-gradient(linear, left top, left bottom, from(#cf2828), to(#981a1a)) !important; - background: -webkit-linear-gradient(top, #cf2828, #981a1a) !important; - background: linear-gradient(to bottom, #cf2828, #981a1a) !important; - color: #fff; - padding: 3px 3px 4px 3px; - margin: -10px; - margin-bottom: 5px; - font-weight: bold; - cursor: default; - text-align: center; - border-radius: 7px 7px 0 0; -} - -/*DIALOG: button, listbox, textbox, label */ - -.gwt-DialogBox .gwt-button { - background-color: #ccc; - border-radius: 5px 5px 5px 5px; - -webkit-box-shadow: 0px 1px 4px #000; - box-shadow: 0px 1px 4px #000; - background: -webkit-gradient(linear, left top, left bottom, from(#444), to(#222)); - background: -webkit-linear-gradient(top, #444, #222); - background: linear-gradient(to bottom, #444, #222); - text-shadow: 1px 1px 1px rgba(0,0,0,0.2); - padding: 3px 5px 3px 5px; - margin: 10px 5px 10px 5px; - font-weight: bold; - font-size: 1em; - border: none; - -webkit-transition: color 0.2s linear; - transition: color 0.2s linear; -} - -.gwt-DialogBox .gwt-button:enabled { - cursor: pointer; - color: #fff; -} - -.gwt-DialogBox .gwt-button:enabled:hover { - background-color: #cf2828; - background: -webkit-gradient(linear, left top, left bottom, from(#cf2828), to(#981a1a)); - background: -webkit-linear-gradient(top, #cf2828, #981a1a); - background: linear-gradient(to bottom, #cf2828, #981a1a); - color: #fff; - text-shadow: 1px 1px 1px rgba(0,0,0,0.25); -} - -.gwt-DialogBox .gwt-TextBox, .gwt-DialogBox .gwt-PasswordTextBox { - background-color: #fff; - border-radius: 5px 5px 5px 5px; - -webkit-box-shadow:inset 0px 1px 4px #000; - box-shadow:inset 0px 1px 4px #000; - padding: 3px 5px 3px 5px; - margin: 10px 5px 10px 5px; - color: #444; - font-size: 1em; - border: none; -} - -.gwt-DialogBox .gwt-TextArea { - background-color: #fff; - border-radius: 5px 5px 5px 5px; - -webkit-box-shadow:inset 0px 1px 4px #000; - box-shadow:inset 0px 1px 4px #000; - padding: 3px 5px 3px 5px; - margin: 0px 5px 10px 5px; - color: #444; - border: none; - vertical-align: text-top; -} - -.gwt-DialogBox .gwt-ListBox { - overflow: auto; - width: 100%; - background-color: #fff; - border-radius: 5px 5px 5px 5px; - -webkit-box-shadow:inset 0px 1px 4px #000; - box-shadow:inset 0px 1px 4px #000; - padding: 3px 5px 3px 5px; - margin: 9px 5px 9px 5px; - color: #444; - font-size: 1em; - border: none; -} - -.gwt-DialogBox .gwt-Label { - margin-top: 13px; -} - -.gwt-DialogBox .gwt-CheckBox { - margin-top: 12px; - display: block; -} - -.gwt-DialogBox .gwt-RadioButton { - margin-top: 13px; - display: block; -} - -.gwt-DialogBox .gwt-RadioButton label { - vertical-align: bottom; -} - -.gwt-DialogBox tr td:first-child { - vertical-align: top !important; -} - -/* Custom Dialogs */ - -.formWarning { /* used when a form is not valid and must be corrected before submission */ - font-weight: bold; - color: lightcoral !important; - height: 34px; /* a higher value will screw up the display of registration tab, check before you modify */ - text-align: center; -} - -.formInfo { /* used when a form is being edited and we want to tell something to the user */ - color: lightcyan !important; -} - -.contactsChooser { - text-align: center; - margin:auto; - cursor: pointer; -} - -.infoDialogBody { - width: 100%; - height: 100% -} -/* Contact List */ - -div.contactList { - width: 100%; - margin-top: 9px; -} - -.contactTitle { - color: #cf2828; - font-size: 1.7em; - text-indent: 5px; - text-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); - width: 200px; - height: 30px; -} - -.contactsSwitch { - /* Button used to switch contacts panel */ - background: none; - border: 0; - padding: 0; - font-size: large; - margin-top: 9px; -} - -.groupPanel { - width: 100%; -} - -.groupPanel tr:first-child td { - padding-top: 10px; -} - -.group { - curser: pointer; - padding: 2px 15px; - margin: 5px; - display: inline-block; - text-decoration: none; - font-weight: bold; - color: #e7e5e5; - text-shadow: 0 1px 1px rgba(0, 0, 0, 0.4); - border-radius: 1em 1em 1em 1em; - background-color: #eee; - background: -webkit-gradient(linear, left top, left bottom, from(#eee), to(#aaa)); - background: -webkit-linear-gradient(top, #eee, #aaa); - background: linear-gradient(to bottom, #eee, #aaa); - color: #444; - text-shadow: 0 1px 0 rgba(255, 255, 255, 0.6); - -webkit-box-shadow: 0px 1px 1px #000; - box-shadow: 0px 1px 1px #000; -} - -div.group:hover { - color: #fff; - text-shadow: 0 1px 0 rgba(0, 0, 0, 0.6); - background-color: #cf2828; - background: -webkit-gradient(linear, left top, left bottom, from(#cf2828), to(#981a1a)); - background: -webkit-linear-gradient(top, #cf2828, #981a1a); - background: linear-gradient(to bottom, #cf2828, #981a1a); - -webkit-transition: color 0.1s linear; - transition: color 0.1s linear; -} - -.contactBox { - cursor: pointer; - width: 100%; - margin: 5px; - border-radius: 5px; - background: #EDEDED; -} - -.contactBox img, .muc_contact img { - width: 32px; - height: 32px; - border-radius: 5px; - margin: 5px 5px 0px 10px; -} - -.contactBox .widgetHeader_buttonGroup { - float: left; -} - -.contactBox .widgetHeader_buttonGroup img { - width: 32px; - height: 32px; - border-radius: 5px; - border: 1px solid #ededed; - padding: 0px 0px 0px 0px; - background: -webkit-gradient(linear, left top, left bottom, from(#eee), to(#aaa)); - background: -webkit-linear-gradient(top, #eee, #aaa); - background: linear-gradient(to bottom, #eee, #aaa); -} - -.contactBox .widgetHeader_buttonGroup img:hover { - border: 1px solid #cf2828; -} - -.contactBox table { - width: 100%; -} - -.contactLabel { - font-size: 1em; - margin-top: 3px; - padding: 3px 10px 3px 10px; -} - -.contact-menu-selected { - font-size: 1em; - margin-top: 3px; - padding: 3px 10px 3px 10px; - border-radius: 5px; - background-color: rgb(175, 175, 175); -} - -.gwt-ScrollPanel { - padding-right: 15px; /* avoid systematic horizontal scroll when only the vertical one is needed */ -} - -.xmlui-JidsListWidget { - padding-right: 20px; /* avoid systematic horizontal scroll when only the vertical one is needed */ - height: 300px; -} - -/* Contacts in MUC */ - -.muc_contact { - border-radius: 5px; - background: #EDEDED; - margin: 2px; - width: 100%; -} - -/* START - contact presence status */ -.contactLabel-connected { - color: #3c7e0c; - font-weight: bold; -} -.contactLabel-unavailable { -} -.contactLabel-chat { - color: #3c7e0c; - font-weight: bold; -} -.contactLabel-away { - color: brown; - font-weight: bold; -} -.contactLabel-dnd { - color: red; - font-weight: bold; -} -.contactLabel-xa { - color: red; - font-weight: bold; -} -/* END - contact presence status */ - -.selected { - color: #fff; - background-color: #cf2828; - background: -webkit-gradient(linear, left top, left bottom, from(#cf2828), to(#981a1a)); - background: -webkit-linear-gradient(top, #cf2828, #981a1a); - background: linear-gradient(to bottom, #cf2828, #981a1a); - border-radius: 1em 1em 1em 1em; - -webkit-transition: color 0.2s linear; - transition: color 0.2s linear; -} - -.messageBox { - width: 100%; - padding: 5px; - border: 1px solid #bbb; - color: #444; - background: #fff url('media/libervia/unibox_2.png') right bottom no-repeat; - -webkit-box-shadow:inset 0 0 10px #ddd; - box-shadow:inset 0 0 10px #ddd; - border-radius: 0px 0px 10px 10px; - height: 28px; - margin: 0px; -} - -.presenceStatusPanel { - margin: auto; - text-align: center; - padding: 5px 0px; - text-shadow: 0 -1px 1px rgba(255,255,255,0.25); - font-size: 1.2em; - background-color: #eee; - font-style: italic; - font-weight: bold; - color: #666; - cursor: pointer; -} - -.presence-button { - font-size: x-large; - padding-right: 5px; - cursor: pointer; -} - -/* RegisterBox */ - -.registerPanel_main button { - margin: 0; - padding: 0; - border: 0; -} - -.registerPanel_main div, .registerPanel_main button { - color: #fff; - text-decoration: none; -} - -.registerPanel_main{ - height: 100%; - border: 5px solid #222; - -webkit-box-shadow: 0px 1px 4px #000; - box-shadow: 0px 1px 4px #000; -} - -.registerPanel_right_side { - background: #111 url('media/libervia/register_right.png'); - height: 100%; - width: 100%; -} - -.registerPanel_right_side .gwt-StackPanelItem { - margin: 15px; - height: auto; - text-align: center; - cursor: pointer; - color: #fff; - display: block; - text-shadow: 1px 1px 0px rgba(255, 255, 255, 0.2); -} - -.registerPanel_right_side .gwt-StackPanelItem-selected { - display: none; -} - -.registerPanel_content { - margin: auto 50px; -} - -.registerPanel_content div { - font-size: 1em; - margin-left: 10px; - margin-top: 15px; - font-weight: bold; - color: #aaa; -} - -.registerPanel_content input { - height: 25px; - line-height: 25px; - width: 200px; - text-indent: 11px; - background: #000; - color: #aaa; - border: 1px solid #222; - border-radius: 15px 15px 15px 15px; -} - -.registerPanel_content input:focus { - border: 1px solid #444; -} - - -.registerPanel_content .button, .registerPanel_content .button:visited { - background: #222 url('media/libervia/gradient.png') repeat-x; - display: block; - text-decoration: none; - border-radius: 6px 6px 6px 6px; - border-bottom: 1px solid rgba(0,0,0,0.25); - cursor: pointer; - margin: 30px auto; -} - -/* Fix for Opera */ -.button, .button:visited { - border-radius: 6px 6px 6px 6px !important; -} - -.registerPanel_content .button:hover { background-color: #111; color: #fff; } -.registerPanel_content .button:active { top: 1px; } -.registerPanel_content .button, .registerPanel_content .button:visited { font-size: 1em; font-weight: bold; line-height: 1; text-shadow: 0 -1px 1px rgba(0,0,0,0.25); padding: 7px 10px 8px; } -.registerPanel_content .red.button, .registerPanel_content .red.button:visited { background-color: #000; } -.registerPanel_content .red.button:hover { background-color: #bc0000; } - -/* Widgets */ - -.widgetsPanel td { - vertical-align: top; -} - -.widgetsPanel > div > table { - border-collapse: separate !important; - border-spacing: 7px; -} - -.widgetHeader { - margin: auto; - height: 25px; - border-radius: 10px 10px 0 0; - background-color: #222; - background: -webkit-gradient(linear, left top, left bottom, from(#444), to(#222)); - background: -webkit-linear-gradient(top, #444, #222); - background: linear-gradient(to bottom, #444, #222); -} - -.widgetHeader_title { - color: #fff; - font-weight: bold; - text-align: left; - text-indent: 15px; - margin-top: 4px; -} - -.widgetHeader_info { - position: absolute; - right: 90px; # FIXME: temporary dirty setting to fit a header menu with 3 icon buttons - color: white; - background-color: white; - border-radius: 5px; - padding: 0px 4px; - top: 2px !important; -} - -.widgetHeader_info img { - padding: 2px; - height: 16px; -} - -.widgetHeader_buttonsWrapper { - position: absolute; - top: 0; - height: 100%; - width: 100%; -} - -.widgetHeader_buttonGroup { - float: right; -} - -.widgetHeader_buttonGroup img { - background-color: transparent; - width: 25px; - height: 20px; - padding: 2px 0px 3px 0px; - border-left: 1px solid #666; - border-top: 0; - border-radius: 0 0 0 0; - background: -webkit-gradient(linear, left top, left bottom, from(#555), to(#333)); - background: -webkit-linear-gradient(top, #555, #333); - background: linear-gradient(to bottom, #555, #333); - cursor: pointer; -} - -.widgetHeader_buttonGroup img:hover { - background-color: #cf2828; - background: -webkit-gradient(linear, left top, left bottom, from(#cf2828), to(#981a1a)); - background: -webkit-linear-gradient(top, #cf2828, #981a1a); - background: linear-gradient(to bottom, #cf2828, #981a1a); -} - -.widgetBody { - border-radius: 0 0 10px 10px; - background-color: #fff; - min-width: 200px; - min-height: 150px; - -webkit-box-shadow:inset 0px 0 1px #444; - box-shadow:inset 0px 0 1px #444; -} - -/* BorderWidgets */ - -.borderWidgetOnDrag { - background-color: lightgray; - border: 1px dashed #000; - border-radius: 1em; -} - -.bottomBorderWidget { - height: 10px !important; -} - -.leftBorderWidget, .rightBorderWidget { - width: 10px !important; -} - -.leftBorderWidget { - float: right; -} - -.rightBorderWidget { - float: left; -} - -/* Microblog */ - -.microblogPanel { - width: 100%; -} - -.microblogPanel_footer { - cursor: pointer; - text-align: center; - background-color: #ededed; - border-radius: 5px; - width: 85%; - margin: auto; - margin-top: 5px; - margin-bottom: 5px; -} - -.microblogPanel_footer a { - color: blue; -} - -.microblogNewButton { - width: 100%; - height: 35px; -} - -.subPanel { -} - -.subpanel .mb_entry { - padding-left: 65px; -} - -.mb_entry { - min-height: 64px; -} - -.mb_entry_header -{ - width: 100%; -} - -.mb_entry_header_info { - cursor: pointer; - padding: 0px 5px 0px 5px; -} - -.selected_widget .selected_entry .mb_entry_header_info -{ - background: #cf2828; - border-radius: 5px 5px 0px 0px; -} - -.mb_entry_comments { - float: right; - padding-right: 5px; -} - -.mb_entry_comments a { - color: blue; - cursor: pointer; -} - -.mb_entry_author { - font-weight: bold; -} - -.mb_entry_avatar { - float: left; -} - -.mb_entry_avatar img { - width: 48px; - height: 48px; - padding: 8px; - border-radius: 13px; /* padding value + 5px */ -} - -.mb_entry_dialog { - float: left; - min-height: 54px; - padding: 5px 20px 5px 20px; - border-collapse: separate; /* for the bubble queue since the entry dialog is now a HorizontalPanel */ -} - -.bubble { - position: relative; - padding: 15px; - margin: 2px; - border-radius:10px; - background: #EDEDED; - border-color: #C1C1C1; - border-width: 1px; - border-style: solid; - display: block; - border-collapse: separate; - min-height: 15px; /* for the bubble queue to be aligned when the bubble is empty */ -} - -.bubble:after { - background: transparent url('media/libervia/bubble_after.png') top right no-repeat; - border: none; - content: ""; - position: absolute; - bottom: auto; - left: -20px; - top: 16px; - display: block; - height: 20px; - width: 20px; -} - -.bubble textarea{ - width: 100%; - min-width: 350px; -} - -.mb_entry_timestamp { - font-style: italic; -} - -.mb_entry_actions { - float: right; - margin: 5px; - cursor: pointer; - font-size: large; -} - -.mb_entry_action_larger { - font-size: x-large; -} - -.mb_entry_toggle_syntax { - cursor: pointer; - float: right; - display: block; - position: relative; - top: -20px; - left: -20px; -} - -.mb_entry_publish_button { - cursor: pointer; - float: left; - display: block; - position: relative; - top: -20px; - left: 20px; -} - -/* START TAGS: styles are adapted from Dotclear */ -.mblog_tags { - background: #fbfbfb none repeat scroll 0% 0%; - padding: 5px; - margin: 8px 0px 5px 0px; - overflow: hidden; - border-radius: 5px; -} - -.mblog_tags li { - display: inline; - font-size: small; -} - -.mblog_tags li a { - float: left; - padding: 2px 8px 2px 18px; - white-space: nowrap; - color: #005D99; - text-decoration: none; - background: transparent url("../themes/default/images/flaticon/tag67.png") no-repeat scroll 0px 0px; -} -/* END TAGS */ - - -/* Chat & MUC Room */ - -.chatPanel { - height: 100%; - width: 100%; -} - -.chatPanel_body { - height: 100%; - width: 100%; -} - -.chatContent { - overflow: auto; - padding: 5px 15px 5px 15px; -} - -.chatText { - margin-top: 7px; -} - -.chatTextMe { - margin-top: 7px; - font-style: italic; -} - -.chatTextInfo { - margin-top: 7px; - font-weight: bold; - font-style: italic; -} - -.chatTextInfo-link { - font-weight: bold; - font-style: italic; - cursor: pointer; - display: inline; -} - -.chatArea { - height:100%; - width:100%; -} - -.chat_text_timestamp { - font-style: italic; - margin-right: -4px; - padding: 1px 3px 1px 3px; - border-radius: 15px 0 0 15px; - background-color: #eee; - color: #888; - border: 1px solid #ddd; - border-right: none; -} - -.chat_text_nick { - font-weight: bold; - padding: 1px 3px 1px 3px; - border-radius: 0 15px 15px 0; - background-color: #eee; - color: #b01e1e; - border: 1px solid #ddd; - border-left: none; -} - -.chat_text_msg { - white-space: pre-wrap; -} - -.chat_text_mymess { - color: #006600; -} - -.occupantsPanelCell { - border-right: 2px dotted #ddd; - padding-left: 5px; - height: 100%; -} - -/* Games */ - -.cardPanel { - background: #02FE03; - margin: 0 auto; -} - -.cardGamePlayerNick { - font-weight: bold; -} - -/* Radiocol */ - -.radiocolPanel { - -} - -.radiocol_metadata_lbl { - font-weight: bold; - padding-right: 5px; -} - -.radiocol_next_song { - margin-right: 5px; - font-style:italic; -} - -.radiocol_status { - margin-left: 10px; - margin-right: 10px; - font-weight: bold; - color: black; -} - -.radiocol_upload_status_ok { - margin-left: 10px; - margin-right: 10px; - font-weight: bold; - color: #28F215; -} - -.radiocol_upload_status_ko { - margin-left: 10px; - margin-right: 10px; - font-weight: bold; - color: #B80000; -} - -/* Drag and drop */ - -.dragover { - background: #cf2828 !important; - border-radius: 1em 1em 1em 1em !important; -} - -.dragover .widgetHeader, .dragover .widgetBody, .dragover .widgetBody span, .dragover .widgetHeader img { - background: #cf2828 !important; -} - -.dragover.widgetHeader { - border-radius: 1em 1em 0 0 !important; -} - -.dragover.widgetBody { - border-radius: 0 0 1em 1em !important; -} - -/* Warning message */ - -.warningPopup { - font-size: 1em; - width: 100%; - height: 26px; - text-align: center; - padding: 5px 0; - border-bottom: 1px solid #444; -} - -.warningTarget { - font-weight: bold; - -} - -.targetPublic { - background-color: red; -} - -.targetGroup { - background-color: #00FFFB; -} - -.targetOne2One { - background-color: #66FF00; -} - -.targetStatus { - background-color: #fff; -} - -.notifInfo { - background-color: #66FF00; -} - -.notifWarning { - background-color: #DB1616; -} - -/* Tab panel */ - -.gwt-TabPanel { -} - -.gwt-TabPanelBottom { - height: 100%; -} - -.gwt-TabBar { - font-weight: bold; - text-decoration: none; - border-bottom: 3px solid #a01c1c; -} - -.gwt-TabBar .gwt-TabBarFirst { - height: 100%; -} - -.gwt-TabBar .gwt-TabBarRest { -} - -.mainPanel .gwt-TabBar { - z-index: 10; -} - -.mainPanel .gwt-TabBar-oneTab { - position: fixed; - left: 0px; - bottom: 0px; - border: none; -} - -.mainPanel .gwt-TabBar-oneTab .gwt-TabBarItem-wrapper { - display: none; -} - -.mainPanel .gwt-TabBar-oneTab .gwt-TabBarItem-wrapper:nth-child(3) { - display: block; -} - -.liberviaTabPanel { - width: 100%; - height: 100%; -} - -.liberviaTabPanel .gwt-TabBarItem div { - color: #fff; -} - -.liberviaTabPanel .gwt-TabBarItem { - color: #444 !important; - background-color: #222; - background: -webkit-gradient(linear, left top, left bottom, from(#444), to(#222)); - background: -webkit-linear-gradient(top, #444, #222); - background: linear-gradient(to bottom, #444, #222); - -webkit-box-shadow: 0px 1px 4px #000; - box-shadow: 0px 1px 4px #000; - padding: 4px 15px 4px 15px; - border-radius: 1em 1em 0 0; - text-shadow: 0 1px 0 rgba(255, 255, 255, 0.2); - cursor: pointer; - margin-right: 5px; -} - -.liberviaTabPanel .gwt-TabBarItem-selected { - color: #fff; - background-color: #cf2828; - background: -webkit-gradient(linear, left top, left bottom, from(#cf2828), to(#981a1a)); - background: -webkit-linear-gradient(top, #cf2828, #981a1a); - background: linear-gradient(to bottom, #cf2828, #981a1a); - -webkit-box-shadow: 0px 1px 4px #000; - box-shadow: 0px 1px 4px #000; - padding: 4px 15px 4px 15px; - border-radius: 1em 1em 0 0; - text-shadow: 0 1px 0 rgba(0, 0, 0, 0.2); - cursor: default; -} - -.liberviaTabPanel div.gwt-TabBarItem:hover { - color: #fff; - background-color: #cf2828; - background: -webkit-gradient(linear, left top, left bottom, from(#cf2828), to(#981a1a)); - background: -webkit-linear-gradient(top, #cf2828, #981a1a); - background: linear-gradient(to bottom, #cf2828, #981a1a); - -webkit-box-shadow: 0px 1px 4px #000; - box-shadow: 0px 1px 4px #000; - padding: 4px 15px 4px 15px; - border-radius: 1em 1em 0 0; - text-shadow: 0 1px 0 rgba(0, 0, 0, 0.2); -} - -.globalLeftArea { - margin-top: 9px; -} - - -/* Misc */ - -.selected_widget .widgetHeader { - background-color: #cf2828; - background: -webkit-gradient(linear, left top, left bottom, from(#cf2828), to(#981a1a)); - background: -webkit-linear-gradient(top, #cf2828, #981a1a); - background: linear-gradient(to bottom, #cf2828, #981a1a); -} - -.infoFrame { - position: relative; - width: 100%; - height: 100%; -} - -.marginAuto { - margin: auto; -} - -.maxWidthLimit { - max-width: 500px; -} - -.transparent { - opacity: 0; -} - -/* URLs */ - -a.url { - color: blue; - text-decoration: none -} - -a:hover.url { - text-decoration: underline -} - -/* Rich Text/Message Editor */ - -.richTextEditor { -} - -.richTextEditor tbody { - width: 100%; - display: table; -} - -.richTextTitle { - margin-bottom: 5px; -} - -.richTextTitle textarea { - height: 22px; - width: 99%; - margin: auto; - padding: 4px; - display: block; - border: 0px; - border-radius: 5px; -} - -.richTextToolbar { - white-space: nowrap; - width: 100%; -} - -.richTextArea { - width: 100%; - height: 250px; -} - -.richTextWysiwyg { - min-height: 50px; - background-color: white; - border: 1px solid #a0a0a0; - border-radius: 5px; - display: block; - font-size: larger; - white-space: pre; -} - -.richTextSyntaxLabel { - text-align: right; - margin: 14px 0px 0px 14px; - font-size: 12px; -} - -.richTextToolButton { - cursor: pointer; - width:26px; - height:26px; - vertical-align: middle; - margin: 2px 1px; - border-radius: 5px 5px 5px 5px; - -webkit-box-shadow: 0px 1px 4px #000; - box-shadow: 0px 1px 4px #000; - border: none; - -webkit-transition: color 0.2s linear; - transition: color 0.2s linear; -} - -.richTextIcon { - width:16px; - height:16px; - vertical-align: middle; -} - -/* List panel */ - -.itemButtonCell { - width:55px; -} - -.itemKeyMenu { -} - -.itemKey { - cursor: pointer; - border-radius: 5px; - width: 50px; -} - -.listItem { -} - -.listItem-box { - cursor: pointer; - width: auto; - border: 1px solid #87B3FF; - border-radius: 5px 5px 5px 5px; - -webkit-box-shadow: inset 0px 1px 0px rgba(135, 179, 255, 0.6); - box-shadow: inset 0px 1px 2px rgba(135, 179, 255, 0.6); - padding: 2px 1px; -} - -.listItem-box-invalid { - border: 1px solid rgb(255, 0, 0); - -webkit-box-shadow: inset 0px 1px 0px rgba(255, 0, 0, 0.6); - box-shadow: inset 0px 1px 0px rgba(255, 0, 0, 0.6); -} - -.listItem-button { - cursor: pointer; - margin: 0px; - padding: 0px; - border: none; - background: transparent; -} - -.listItem-button span { - color: red; -} - -/* Popup (context) menu */ - -.popupMenuItem { - cursor: pointer; - border-radius: 5px; - width: 100%; -} - -/* Contact group manager */ - -.contactGroupEditor { - width: 680px !important; -} - -.contactGroupManager { - width: 400px !important; - height: 300px !important; - margin: 20px 0px; -} - -.contactGroupRoster { - width: 280px !important; - height: 300px !important; - margin: 20px 0px; -} - -.addContactGroupPanel { - -} - -.listPanel { - vertical-align:top; - padding: 10px 0px; -} - -.listPanel.dragover { - border-radius: 5px !important; - background: none repeat scroll 0% 0% rgb(135, 179, 255) !important; - border: 1px dashed rgb(35,79,255) !important; -} - -.toggleAssignedContacts { - white-space: nowrap; -} - -.listManager-button-cell { - vertical-align: top; - padding: 10px 0px; - width: 55px; - white-space: top; -} - -.listManager-button-cell .group { - border: 0px; - margin: 0px 5px; -} - -.tagsPanel-main { - margin-bottom: 10px; -} - -.tagsPanel-tags { - padding: 0px; - display: flex; - flex-wrap: wrap; -} - -/* Room and contacts chooser */ - -.room-contact-chooser { - width:380px; -} - -/* StackPanel */ - -.gwt-StackPanel { -} - -.gwt-StackPanel .gwt-StackPanelItem { - background-color: #222; - background: -webkit-gradient(linear, left top, left bottom, from(#888888), to(#666666)); - background: -webkit-linear-gradient(top, #888888, #666666); - background: linear-gradient(to bottom, #888888, #666666); - text-decoration: none; - font-weight: bold; - height: 100%; - color: #e7e5e5; - padding: 3px 15px; - border-radius: 1em 1em 1em 1em; - text-shadow: 0 1px 1px rgba(0, 0, 0, 0.4); - -webkit-transition: color 0.2s linear; - transition: color 0.2s linear; -} - -.gwt-StackPanel .gwt-StackPanelItem:hover { - background-color: #eee; - background: -webkit-gradient(linear, left top, left bottom, from(#eee), to(#aaa)); - background: -webkit-linear-gradient(top, #eee, #aaa); - background: linear-gradient(to bottom, #eee, #aaa); - color: #444; - text-shadow: 0 1px 0 rgba(255, 255, 255, 0.6); - cursor: pointer; -} - -.gwt-StackPanel .gwt-StackPanelItem-selected { - background-color: #eee; - background: -webkit-gradient(linear, left top, left bottom, from(#eee), to(#aaa)); - background: -webkit-linear-gradient(top, #eee, #aaa); - background: linear-gradient(to bottom, #eee,#aaa); - color: #444; - text-shadow: 0 1px 0 rgba(255, 255, 255, 0.6); - cursor: pointer; -} - -/* Caption Panel */ - -.gwt-CaptionPanel { - overflow: auto; - background-color: #fff; - border-radius: 5px 5px 5px 5px; - padding: 3px 5px 3px 5px; - margin: 10px 5px 10px 5px; - color: #444; - font-size: 1em; - border: solid 1px gray; -} - -/* Radio buttons */ - -.gwt-RadioButton { - white-space: nowrap; -} - -[contenteditable="true"] { -} - -/* XMLUI styles */ - -.AdvancedListSelectable tr{ - cursor: pointer; -} - -.AdvancedListSelectable tr:hover{ - background: none repeat scroll 0 0 #EE0000; -} - -.line hr { - -} - -.dot hr { - height: 0px; - border-top: 1px dotted; - border-bottom: 0px; -} - -.dash hr { - height: 0px; - border-top: 1px dashed; - border-bottom: 0px; -} - -.plain hr { - height: 10px; - color: black; - background-color: black; -} - -.blank hr { - border: 0px; -} - - -/* Some CSS to style the quote XHTML generated by Movim */ - -.mb_entry_dialog .bubble div.quote { - display: block; - border-radius: 2px; - border: 1px solid rgba(0, 0, 0, 0.12); - padding: 2rem; - box-sizing: border-box; -} - -.mb_entry_dialog .bubble div.quote:before, -.mb_entry_dialog .bubble div.quote:after { - content: ''; - display: none; -} - -.mb_entry_dialog .bubble div.quote ul { - display: flex; - flex-flow: row wrap; -} - -.mb_entry_dialog .bubble div.quote li { - flex: 1 25%; - list-style-type: none; - padding-left: 0; -} - -.mb_entry_dialog .bubble div.quote ul li > * { - margin-right: 1rem; -} - -.mb_entry_dialog .bubble div.quote li:first-child { - flex: 1 75%; -} - -@media screen and (max-width: 1024px) { - .mb_entry_dialog .bubble div.quote li { - flex: 1 100%; - } -} - -.mb_entry_dialog .bubble div.quote li img { - max-height: 10rem; - max-width: 100%; - float: right; -} - -.parameters { -} - -.parameters .xmlui-JidsListWidget { - height: auto; -}
--- a/src/browser/public/libervia.html Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,33 +0,0 @@ -<!-- -Libervia: a Salut à Toi frontend -Copyright (C) 2011 Jérôme Poisson (goffi@goffi.org) - -This program 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. - -This program 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 this program. If not, see <http://www.gnu.org/licenses/>. ---> - -<html> -<head profile="http://www.w3.org/2005/10/profile"> -<meta http-equiv="Content-Type" content="text/html; charset=utf-8"> -<meta name="pygwt:module" content="libervia_main"> -<link rel='stylesheet' href='libervia.css'> -<link rel="icon" type="image/png" href="sat_logo_16.png"> - -<title>Libervia</title> -</head> -<body bgcolor="white"> -<script language="javascript" src="bootstrap.js"></script> -<script language="javascript" src="favico.min.js"></script> -<iframe id='__pygwt_historyFrame' style='display:none;width:0;height:0;border:0'></iframe> -</body> -</html>
--- a/src/browser/public/robots.txt Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,10 +0,0 @@ -User-agent: * -Allow: /blog/ -Allow: /u/ -Allow: /b/ -Disallow: /libervia.html -Disallow: /mr -Disallow: /login - -User-agent: Mediapartners-* -Disallow: /
--- a/src/browser/sat_browser/base_menu.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,183 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2011-2018 Jérôme Poisson <goffi@goffi.org> - -# This program 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. - -# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. - - -"""Base classes for building a menu. - -These classes have been moved here from menu.py because they are also used -by base_widget.py, and the import sequence caused a JS runtime error.""" - - -from sat.core.log import getLogger -log = getLogger(__name__) - -from pyjamas.ui.MenuBar import MenuBar -from pyjamas.ui.MenuItem import MenuItem -from pyjamas import Window -from sat_frontends.quick_frontend import quick_menus -from sat_browser import html_tools - - -unicode = str # FIXME: pyjamas workaround - - -class MenuCmd(object): - """Return an object with an "execute" method that can be set to a menu item callback""" - - def __init__(self, menu_item, caller=None): - """ - @param menu_item(quick_menu.MenuItem): instance of a callbable MenuItem - @param caller: menu caller - """ - self.item = menu_item - self._caller = caller - - def execute(self): - self.item.call(self._caller) - - -class SimpleCmd(object): - """Return an object with an "executre" method that launch a callback""" - - def __init__(self, callback): - """ - @param callback: method to call when menu is selected - """ - self.callback = callback - - def execute(self): - self.callback() - - -class GenericMenuBar(MenuBar): - """A menu bar with sub-categories and items""" - - def __init__(self, host, vertical=False, styles=None, flat_level=0, **kwargs): - """ - @param host (SatWebFrontend): host instance - @param vertical (bool): True to display the popup menu vertically - @param styles (dict): specific styles to be applied: - - key: a value in ('moved_popup', 'menu_bar') - - value: a CSS class name - @param flat_level (int): sub-menus until that level see their items - displayed in the parent menu bar instead of in a callback popup. - """ - MenuBar.__init__(self, vertical, **kwargs) - self.host = host - self.styles = {} - if styles: - self.styles.update(styles) - try: - self.setStyleName(self.styles['menu_bar']) - except KeyError: - pass - self.menus_container = None - self.flat_level = flat_level - - def update(self, type_, caller=None): - """Method to call when menus have changed - - @param type_: menu type like in sat.core.sat_main.importMenu - @param caller: instance linked to the menus - """ - self.menus_container = self.host.menus.getMainContainer(type_) - self._caller=caller - self.createMenus() - - @classmethod - def getCategoryHTML(cls, category): - """Build the html to be used for displaying a category item. - - Inheriting classes may overwrite this method. - @param category (quick_menus.MenuCategory): category to add - @return unicode: HTML to display - """ - return html_tools.html_sanitize(category.name) - - def _buildMenus(self, container, flat_level, caller=None): - """Recursively build menus of the container - - @param container: a quick_menus.MenuContainer instance - @param caller: instance linked to the menus - """ - for child in container.getActiveMenus(): - if isinstance(child, quick_menus.MenuContainer): - item = self.addCategory(child, flat=bool(flat_level)) - submenu = item.getSubMenu() - if submenu is None: - submenu = self - submenu._buildMenus(child, flat_level-1 if flat_level else 0, caller) - elif isinstance(child, quick_menus.MenuSeparator): - item = MenuItem(text='', asHTML=None, StyleName="menuSeparator") - self.addItem(item) - elif isinstance(child, quick_menus.MenuItem): - self.addItem(child.name, False, MenuCmd(child, caller) if child.CALLABLE else None) - else: - log.error(u"Unknown child type: {}".format(child)) - - def createMenus(self): - self.clearItems() - if self.menus_container is None: - log.debug("Menu is empty") - return - self._buildMenus(self.menus_container, self.flat_level, self._caller) - - def doItemAction(self, item, fireCommand): - """Overwrites the default behavior for the popup menu to fit in the screen""" - MenuBar.doItemAction(self, item, fireCommand) - if not self.popup: - return - if self.vertical: - # move the popup if it would go over the screen's viewport - max_left = Window.getClientWidth() - self.getOffsetWidth() + 1 - self.popup.getOffsetWidth() - new_left = self.getAbsoluteLeft() - self.popup.getOffsetWidth() + 1 - top = item.getAbsoluteTop() - else: - # move the popup if it would go over the menu bar right extremity - max_left = self.getAbsoluteLeft() + self.getOffsetWidth() - self.popup.getOffsetWidth() - new_left = max_left - top = self.getAbsoluteTop() + self.getOffsetHeight() - 1 - if item.getAbsoluteLeft() > max_left: - self.popup.setPopupPosition(new_left, top) - # eventually smooth the popup edges to fit the menu own style - try: - self.popup.addStyleName(self.styles['moved_popup']) - except KeyError: - pass - - def addCategory(self, category, menu_bar=None, flat=False): - """Add a new category. - - @param category (quick_menus.MenuCategory): category to add - @param menu_bar (GenericMenuBar): instance to popup as the category sub-menu. - """ - html = self.getCategoryHTML(category) - - if menu_bar is not None: - assert not flat # can't have a menu_bar and be flat at the same time - sub_menu = menu_bar - elif not flat: - sub_menu = GenericMenuBar(self.host, vertical=True) - else: - sub_menu = None - - item = self.addItem(html, True, sub_menu) - if flat: - item.setStyleName("menuFlattenedCategory") - return item
--- a/src/browser/sat_browser/base_panel.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,236 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2011-2018 Jérôme Poisson <goffi@goffi.org> - -# This program 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. - -# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. - -from sat.core.log import getLogger -log = getLogger(__name__) -from sat.core.i18n import _ - -from pyjamas.ui.VerticalPanel import VerticalPanel -from pyjamas.ui.HorizontalPanel import HorizontalPanel -from pyjamas.ui.ScrollPanel import ScrollPanel -from pyjamas.ui.Button import Button -from pyjamas.ui.SimplePanel import SimplePanel -from pyjamas.ui.PopupPanel import PopupPanel -from pyjamas.ui.StackPanel import StackPanel -from pyjamas.ui.TextArea import TextArea -from pyjamas.ui.Event import BUTTON_LEFT, BUTTON_MIDDLE, BUTTON_RIGHT -from pyjamas import DOM - - -### Menus ### - - -class PopupMenuPanel(PopupPanel): - """Popup menu (contextual menu) with common callbacks for all the items. - - This implementation of a popup menu allow you to assign two special methods which - are common to all the items, in order to hide certain items and define their callbacks. - callbacks. The menu can be bound to any button of the mouse (left, middle, right). - """ - - def __init__(self, entries, hide=None, callback=None, vertical=True, style=None, **kwargs): - """ - @param entries (dict{unicode: dict{unicode: unicode}: - - menu item keys - - values: dict{unicode: unicode}: - - item data lile "title", "desc"... - - value - @param hide (callable): function of signature Widget, unicode: bool - which takes the sender and the item key, and returns True if that - item has to be hidden from the context menu. - @param callback (callbable): function of signature Widget, unicode: None - which takes the sender and the item key. - @param vertical (bool): set the direction vertical or horizontal - @param item_style (unicode): alternative CSS class for the menu items - @param menu_style (unicode): supplementary CSS class for the sender widget - """ - PopupPanel.__init__(self, autoHide=True, **kwargs) - self.entries = entries - self.hideMenu = hide - self.callback = callback - self.vertical = vertical - self.style = {"selected": None, "menu": "itemKeyMenu", "item": "popupMenuItem"} - if isinstance(style, dict): - self.style.update(style) - self.senders = {} - - def showMenu(self, sender): - """Popup the menu on the screen, where it fits to the sender's position. - - @param sender (Widget): the widget that has been clicked - """ - menu = VerticalPanel() if self.vertical is True else HorizontalPanel() - menu.setStyleName(self.style["menu"]) - - def button_cb(item): - # XXX: you can not put that method in the loop and rely on key - if self.callback is not None: - self.callback(sender=sender, key=item.key) - self.hide(autoClosed=True) - - for key, entry in self.entries.iteritems(): - if self.hideMenu is not None and self.hideMenu(sender=sender, key=key) is True: - continue - title = entry.get("title", key) - item = Button(title, button_cb, StyleName=self.style["item"]) - item.key = key # XXX: copy the key because we loop on it and it will change - item.setTitle(entry.get("desc", title)) - menu.add(item) - - if menu.getWidgetCount() == 0: - return # no item to display means no menu at all - - self.add(menu) - - if self.vertical is True: - x = sender.getAbsoluteLeft() + sender.getOffsetWidth() - y = sender.getAbsoluteTop() - else: - x = sender.getAbsoluteLeft() - y = sender.getAbsoluteTop() + sender.getOffsetHeight() - - self.setPopupPosition(x, y) - self.show() - - if self.style["selected"]: - sender.addStyleDependentName(self.style["selected"]) - - def onHide(popup): - if self.style["selected"]: - sender.removeStyleDependentName(self.style["selected"]) - return PopupPanel.onHideImpl(self, popup) - - self.onHideImpl = onHide - - def registerClickSender(self, sender, button=BUTTON_LEFT): - """Bind the menu to the specified sender. - - @param sender (Widget): bind the menu to this widget - @param (int): BUTTON_LEFT, BUTTON_MIDDLE or BUTTON_RIGHT - """ - self.senders.setdefault(sender, []) - self.senders[sender].append(button) - - if button == BUTTON_RIGHT: - # WARNING: to disable the context menu is a bit tricky... - # The following seems to work on Firefox 24.0, but: - # TODO: find a cleaner way to disable the context menu - sender.getElement().setAttribute("oncontextmenu", "return false") - - def onBrowserEvent(event): - button = DOM.eventGetButton(event) - if DOM.eventGetType(event) == "mousedown" and button in self.senders[sender]: - self.showMenu(sender) - return sender.__class__.onBrowserEvent(sender, event) - - sender.onBrowserEvent = onBrowserEvent - - def registerMiddleClickSender(self, sender): - self.registerClickSender(sender, BUTTON_MIDDLE) - - def registerRightClickSender(self, sender): - self.registerClickSender(sender, BUTTON_RIGHT) - - -### Generic panels ### - - -class ToggleStackPanel(StackPanel): - """This is a pyjamas.ui.StackPanel with modified behavior. All sub-panels ca be - visible at the same time, clicking a sub-panel header will not display it and hide - the others but only toggle its own visibility. The argument 'visibleStack' is ignored. - Note that the argument 'visible' has been added to listener's 'onStackChanged' method. - """ - - def __init__(self, **kwargs): - StackPanel.__init__(self, **kwargs) - - def onBrowserEvent(self, event): - if DOM.eventGetType(event) == "click": - index = self.getDividerIndex(DOM.eventGetTarget(event)) - if index != -1: - self.toggleStack(index) - - def add(self, widget, stackText="", asHTML=False, visible=False): - StackPanel.add(self, widget, stackText, asHTML) - self.setStackVisible(self.getWidgetCount() - 1, visible) - - def toggleStack(self, index): - if index >= self.getWidgetCount(): - return - visible = not self.getWidget(index).getVisible() - self.setStackVisible(index, visible) - for listener in self.stackListeners: - listener.onStackChanged(self, index, visible) - - -class TitlePanel(ToggleStackPanel): - """A toggle panel to set the message title""" - - TITLE = _("Title") - - def __init__(self, text=None): - ToggleStackPanel.__init__(self, Width="100%") - self.text_area = TextArea() - self.add(self.text_area, self.TITLE) - self.addStackChangeListener(self) - if text: - self.setText(text) - - def onStackChanged(self, sender, index, visible=None): - if visible is None: - visible = sender.getWidget(index).getVisible() - text = self.getText() - suffix = "" if (visible or not text) else (": %s" % text) - sender.setStackText(index, self.TITLE + suffix) - - def getText(self): - return self.text_area.getText() - - def setText(self, text): - self.text_area.setText(text) - - -class ScrollPanelWrapper(SimplePanel): - """Scroll Panel like component, wich use the full available space - to work around percent size issue, it use some of the ideas found - here: http://code.google.com/p/google-web-toolkit/issues/detail?id=316 - specially in code given at comment #46, thanks to Stefan Bachert""" - - def __init__(self, *args, **kwargs): - SimplePanel.__init__(self) - self.spanel = ScrollPanel(*args, **kwargs) - SimplePanel.setWidget(self, self.spanel) - DOM.setStyleAttribute(self.getElement(), "position", "relative") - DOM.setStyleAttribute(self.getElement(), "top", "0px") - DOM.setStyleAttribute(self.getElement(), "left", "0px") - DOM.setStyleAttribute(self.getElement(), "width", "100%") - DOM.setStyleAttribute(self.getElement(), "height", "100%") - DOM.setStyleAttribute(self.spanel.getElement(), "position", "absolute") - DOM.setStyleAttribute(self.spanel.getElement(), "width", "100%") - DOM.setStyleAttribute(self.spanel.getElement(), "height", "100%") - - def setWidget(self, widget): - self.spanel.setWidget(widget) - - def setScrollPosition(self, position): - self.spanel.setScrollPosition(position) - - def scrollToBottom(self): - self.setScrollPosition(self.spanel.getElement().scrollHeight)
--- a/src/browser/sat_browser/base_widget.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,71 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2011-2018 Jérôme Poisson <goffi@goffi.org> - -# This program 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. - -# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. - -from sat.core.log import getLogger -log = getLogger(__name__) -import base_menu -from sat_frontends.quick_frontend import quick_menus - - -### Exceptions ### - - -class NoLiberviaWidgetException(Exception): - """A Libervia widget was expected""" - pass - - -### Menus ### - - -class WidgetMenuBar(base_menu.GenericMenuBar): - - ITEM_TPL = "<img src='media/icons/misc/%s.png' />" - - def __init__(self, parent, host, vertical=False, styles=None): - """ - - @param parent (Widget): LiberviaWidget, or instance of another class - implementing the method addMenus - @param host (SatWebFrontend) - @param vertical (bool): if True, set the menu vertically - @param styles (dict): optional styles dict - """ - menu_styles = {'menu_bar': 'widgetHeader_buttonGroup'} - if styles: - menu_styles.update(styles) - base_menu.GenericMenuBar.__init__(self, host, vertical=vertical, styles=menu_styles) - - # regroup all the dynamic menu categories in a sub-menu - for menu_context in parent.plugin_menu_context: - main_cont = host.menus.getMainContainer(menu_context) - if len(main_cont)>0: # we don't add the icon if the menu is empty - sub_menu = base_menu.GenericMenuBar(host, vertical=True, flat_level=1) - sub_menu.update(menu_context, parent) - menu_category = quick_menus.MenuCategory("plugins", extra={'icon':'plugins'}) - self.addCategory(menu_category, sub_menu) - - @classmethod - def getCategoryHTML(cls, category): - """Build the html to be used for displaying a category item. - - @param category (quick_menus.MenuCategory): category to add - @return unicode: HTML to display - """ - return cls.ITEM_TPL % category.icon
--- a/src/browser/sat_browser/blog.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,560 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2011-2018 Jérôme Poisson <goffi@goffi.org> - -# This program 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. - -# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. - -import pyjd # this is dummy in pyjs -from sat.core.log import getLogger -log = getLogger(__name__) -from sat.tools.common import data_format -from sat.core.i18n import _ #, D_ - -from pyjamas.ui.SimplePanel import SimplePanel -from pyjamas.ui.VerticalPanel import VerticalPanel -from pyjamas.ui.ScrollPanel import ScrollPanel -from pyjamas.ui.HorizontalPanel import HorizontalPanel -from pyjamas.ui.Label import Label -from pyjamas.ui.HTML import HTML -from pyjamas.ui.Image import Image -from pyjamas.ui.ClickListener import ClickHandler -from pyjamas.ui.FlowPanel import FlowPanel -from pyjamas.ui import KeyboardListener as keyb -from pyjamas.ui.KeyboardListener import KeyboardHandler -from pyjamas.ui.FocusListener import FocusHandler -from pyjamas.ui.MouseListener import MouseHandler -from pyjamas.Timer import Timer - -from datetime import datetime - -import html_tools -import dialog -import richtext -import editor_widget -import libervia_widget -from constants import Const as C -from sat_frontends.quick_frontend import quick_widgets -from sat_frontends.quick_frontend import quick_blog - -unicode = str # XXX: pyjamas doesn't manage unicode -ENTRY_RICH = (C.ENTRY_MODE_RICH, C.ENTRY_MODE_XHTML) - - -class Entry(quick_blog.Entry, VerticalPanel, ClickHandler, FocusHandler, KeyboardHandler): - """Graphical representation of a quick_blog.Item""" - - def __init__(self, manager, item_data=None, comments_data=None, service=None, node=None): - quick_blog.Entry.__init__(self, manager, item_data, comments_data, service, node) - - VerticalPanel.__init__(self) - - self.panel = FlowPanel() - self.panel.setStyleName('mb_entry') - - self.header = HorizontalPanel(StyleName='mb_entry_header') - self.panel.add(self.header) - - self.entry_actions = VerticalPanel() - self.entry_actions.setStyleName('mb_entry_actions') - self.panel.add(self.entry_actions) - - entry_avatar = SimplePanel() - entry_avatar.setStyleName('mb_entry_avatar') - author_jid = self.author_jid - self.avatar = Image(self.blog.host.getAvatarURL(author_jid) if author_jid is not None else C.DEFAULT_AVATAR_URL) - # TODO: show a warning icon if author is not validated - entry_avatar.add(self.avatar) - self.panel.add(entry_avatar) - - self.entry_dialog = VerticalPanel() - self.entry_dialog.setStyleName('mb_entry_dialog') - self.panel.add(self.entry_dialog) - - self.comments_panel = None - self._current_comment = None - - self.add(self.panel) - ClickHandler.__init__(self) - self.addClickListener(self) - - self.refresh() - self.displayed = False # True when entry is added to parent - if comments_data: - self.addComments(comments_data) - - def refresh(self): - self.comment_label = None - self.update_label = None - self.delete_label = None - self.header.clear() - self.entry_dialog.clear() - self.entry_actions.clear() - self._setHeader() - self._setBubble() - self._setIcons() - - def _setHeader(self): - """Set the entry header.""" - if not self.new: - author = html_tools.html_sanitize(unicode(self.item.author)) - author_jid = html_tools.html_sanitize(unicode(self.item.author_jid)) - if author_jid and not self.item.author_verified: - author_jid += u' <span style="color:red; font-weight: bold;">⚠</span>' - if author: - author += " <%s>" % author_jid - elif author_jid: - author = author_jid - else: - author = _("<unknown author>") - - update_text = u" — ✍ " + "<span class='mb_entry_timestamp'>%s</span>" % datetime.fromtimestamp(self.item.updated) - self.header.add(HTML("""<span class='mb_entry_header_info'> - <span class='mb_entry_author'>%(author)s</span> on - <span class='mb_entry_timestamp'>%(published)s</span>%(updated)s - </span>""" % {'author': author, - 'published': datetime.fromtimestamp(self.item.published) if self.item.published is not None else '', - 'updated': update_text if self.item.published != self.item.updated else '' - })) - if self.item.comments: - self.show_comments_link = HTML('') - self.header.add(self.show_comments_link) - - def _setBubble(self): - """Set the bubble displaying the initial content.""" - content = {'text': self.item.content_xhtml if self.item.content_xhtml else self.item.content or '', - 'title': self.item.title_xhtml if self.item.title_xhtml else self.item.title or ''} - data_format.iter2dict('tag', self.item.tags, content) - - if self.mode == C.ENTRY_MODE_TEXT: - # assume raw text message have no title - self.bubble = editor_widget.LightTextEditor(content, modifiedCb=self._modifiedCb, afterEditCb=self._afterEditCb, options={'no_xhtml': True}) - elif self.mode in ENTRY_RICH: - content['syntax'] = C.SYNTAX_XHTML - if self.new: - options = [] - elif self.item.author_jid == self.blog.host.whoami.bare: - options = ['update_msg'] - else: - options = ['read_only'] - self.bubble = richtext.RichTextEditor(self.blog.host, content, modifiedCb=self._modifiedCb, afterEditCb=self._afterEditCb, options=options) - else: - log.error("Bad entry mode: %s" % self.mode) - self.bubble.addStyleName("bubble") - self.entry_dialog.add(self.bubble) - self.bubble.addEditListener(self._showWarning) # FIXME: remove edit listeners - self.setEditable(self.editable) - - def _setIcons(self): - """Set the entry icons (delete, update, comment)""" - if self.new: - return - - def addIcon(label, title): - label = Label(label) - label.setTitle(title) - label.addClickListener(self) - self.entry_actions.add(label) - return label - - if self.item.comments: - self.comment_label = addIcon(u"↶", "Comment this message") - self.comment_label.setStyleName('mb_entry_action_larger') - else: - self.comment_label = None - is_publisher = self.item.author_jid == self.blog.host.whoami.bare - if is_publisher: - self.update_label = addIcon(u"✍", "Edit this message") - # TODO: add delete button if we are the owner of the node - self.delete_label = addIcon(u"✗", "Delete this message") - else: - self.update_label = self.delete_label = None - - def _createCommentsPanel(self): - """Create the panel if it doesn't exists""" - if self.comments_panel is None: - self.comments_panel = VerticalPanel() - self.comments_panel.setStyleName('microblogPanel') - self.comments_panel.addStyleName('subPanel') - self.add(self.comments_panel) - - def setEditable(self, editable=True): - """Toggle the bubble between display and edit mode. - - @param editable (bool) - """ - self.editable = editable - self.bubble.edit(self.editable) - self.updateIconsAndButtons() - - def updateIconsAndButtons(self): - """Set the visibility of the icons and the button to switch between blog and microblog.""" - try: - self.bubble_commands.removeFromParent() - except (AttributeError, TypeError): - pass - if self.editable: - if self.mode == C.ENTRY_MODE_TEXT: - html = _(u'<a style="color: blue;">switch to blog</a>') - title = _(u'compose a rich text message with a title - suitable for writing articles') - else: - html = _(u'<a style="color: blue;">switch to microblog</a>') - title = _(u'compose a short message without title - suitable for sharing news') - toggle_syntax_button = HTML(html, Title=title) - toggle_syntax_button.addClickListener(self.toggleContentSyntax) - toggle_syntax_button.addStyleName('mb_entry_toggle_syntax') - toggle_syntax_button.setStyleAttribute('top', '-20px') # XXX: need to force CSS - toggle_syntax_button.setStyleAttribute('left', '-20px') - - self.bubble_commands = HorizontalPanel(Width="100%") - - if self.mode == C.ENTRY_MODE_TEXT: - publish_button = HTML(_(u'<a style="color: blue;">shift + enter to publish</a>'), Title=_(u"... or click here")) - publish_button.addStyleName('mb_entry_publish_button') - publish_button.addClickListener(lambda dummy: self.bubble.edit(False)) - publish_button.setStyleAttribute('top', '-20px') # XXX: need to force CSS - publish_button.setStyleAttribute('left', '20px') - self.bubble_commands.add(publish_button) - - self.bubble_commands.add(toggle_syntax_button) - self.entry_dialog.add(self.bubble_commands) - - # hide these icons while editing - try: - self.delete_label.setVisible(not self.editable) - except (TypeError, AttributeError): - pass - try: - self.update_label.setVisible(not self.editable) - except (TypeError, AttributeError): - pass - try: - self.comment_label.setVisible(not self.editable) - except (TypeError, AttributeError): - pass - - def onClick(self, sender): - - if sender == self: - self.blog.setSelectedEntry(self) - elif sender == self.delete_label: - self._onRetractClick() - elif sender == self.update_label: - self.setEditable(True) - elif sender == self.comment_label: - self._onCommentClick() - # elif sender == self.show_comments_link: - # self._blog_panel.loadAllCommentsForEntry(self) - - def _modifiedCb(self, content): - """Send the new content to the backend - - @return: False to restore the original content if a deletion has been cancelled - """ - if not content['text']: # previous content has been emptied - if not self.new: - self._onRetractClick() - return False - - self.item.content = self.item.content_rich = self.item.content_xhtml = None - self.item.title = self.item.title_rich = self.item.title_xhtml = None - - if self.mode in ENTRY_RICH: - # TODO: if the user change his parameters after the message edition started, - # the message syntax could be different then the current syntax: pass the - # message syntax in mb_data for the frontend to use it instead of current syntax. - self.item.content_rich = content['text'] # XXX: this also works if the syntax is XHTML - self.item.title = content['title'] - self.item.tags = list(data_format.dict2iter('tag', content)) - else: - self.item.content = content['text'] - - self.send() - - return True - - def _afterEditCb(self, content): - """Post edition treatments - - Remove the entry if it was an empty one (used for creating a new blog post). - Data for the actual new blog post will be received from the bridge - @param content(dict): edited content - """ - if self.new: - if self.level == 0: - # we have a main item, we keep the edit entry - self.reset(None) - # FIXME: would be better to reset bubble - # but bubble.setContent() doesn't seem to work - self.bubble.removeFromParent() - self._setBubble() - else: - # we don't keep edit entries for comments - self.delete() - else: - self.editable = False - self.updateIconsAndButtons() - - def _showWarning(self, sender, keycode, modifiers): - if keycode == keyb.KEY_ENTER & keyb.MODIFIER_SHIFT: # FIXME: fix edit_listeners, it's dirty (we have to check keycode/modifiers twice !) - self.blog.host.showWarning(None, None) - else: - # self.blog.host.showWarning(*self.blog.getWarningData(self.type == 'comment')) - self.blog.host.showWarning(*self.blog.getWarningData(False)) # FIXME: comments are not yet reimplemented - - def _onRetractClick(self): - """Ask confirmation then retract current entry.""" - assert not self.new - - def confirm_cb(answer): - if answer: - self.retract() - - entry_type = _("message") if self.level == 0 else _("comment") - and_comments = _(" All comments will be also deleted!") if self.item.comments else "" - text = _("Do you really want to delete this {entry_type}?{and_comments}").format( - entry_type=entry_type, and_comments=and_comments) - dialog.ConfirmDialog(confirm_cb, text=text).show() - - def _onCommentClick(self): - """Add an empty entry for a new comment""" - if self._current_comment is None: - if not self.item.comments_service or not self.item.comments_node: - log.warning("Invalid service and node for comments, can't create a comment") - self._current_comment = self.addEntry(editable=True, service=self.item.comments_service, node=self.item.comments_node, edit_entry=True) - self.blog.setSelectedEntry(self._current_comment, True) - self._current_comment.bubble.setFocus(True) # FIXME: should be done elsewhere (automatically)? - - def _changeMode(self, original_content, text): - self.mode = C.ENTRY_MODE_RICH if self.mode == C.ENTRY_MODE_TEXT else C.ENTRY_MODE_TEXT - if self.mode in ENTRY_RICH and not text: - text = ' ' # something different than empty string is needed to initialize the rich text editor - self.item.content = text - if self.mode in ENTRY_RICH: - self.item.content_rich = text # XXX: this also works if the syntax is XHTML - self.bubble.setDisplayContent() # needed in case the edition is aborted, to not end with an empty bubble - else: - self.item.content_xhtml = '' - self.bubble.removeFromParent() - self._setBubble() - self.bubble.setOriginalContent(original_content) - - def toggleContentSyntax(self): - """Toggle the editor between raw and rich text""" - original_content = self.bubble.getOriginalContent() - rich = self.mode in ENTRY_RICH - if rich: - original_content['syntax'] = C.SYNTAX_XHTML - - text = self.bubble.getContent()['text'] - - if not text.strip(): - self._changeMode(original_content,'') - else: - if rich: - def confirm_cb(answer): - if answer: - self.blog.host.bridge.syntaxConvert(text, C.SYNTAX_CURRENT, C.SYNTAX_TEXT, profile=None, - callback=lambda converted: self._changeMode(original_content, converted)) - dialog.ConfirmDialog(confirm_cb, text=_("Do you really want to lose the title and text formatting?")).show() - else: - self.blog.host.bridge.syntaxConvert(text, C.SYNTAX_TEXT, C.SYNTAX_XHTML, profile=None, - callback=lambda converted: self._changeMode(original_content, converted)) - - def update(self, entry=None): - """Update comments""" - self._createCommentsPanel() - self.entries.sort(key=lambda entry: entry.item.published) - # we put edit_entry at the end - edit_entry = [] if self.edit_entry is None else [self.edit_entry] - for idx, entry in enumerate(self.entries + edit_entry): - if not entry.displayed: - self.comments_panel.insert(entry, idx) - entry.displayed = True - - def delete(self): - quick_blog.Entry.delete(self) - - # _current comment is specific to libervia, we remove it - if isinstance(self.manager, Entry): - self.manager._current_comment = None - - # now we remove the pyjamas widgets - parent = self.parent - assert isinstance(parent, VerticalPanel) - self.removeFromParent() - if not parent.children: - # the vpanel is empty, we remove it - parent.removeFromParent() - try: - if self.manager.comments_panel == parent: - self.manager.comments_panel = None - except AttributeError: - assert isinstance(self.manager, quick_blog.QuickBlog) - - -class Blog(quick_blog.QuickBlog, libervia_widget.LiberviaWidget, MouseHandler): - """Panel used to show microblog""" - warning_msg_public = "This message will be <b>PUBLIC</b> and everybody will be able to see it, even people you don't know" - warning_msg_group = "This message will be published for all the people of the following groups: <span class='warningTarget'>%s</span>" - - def __init__(self, host, targets, profiles=None): - quick_blog.QuickBlog.__init__(self, host, targets, C.PROF_KEY_NONE) - title = ", ".join(targets) if targets else "Blog" - libervia_widget.LiberviaWidget.__init__(self, host, title, selectable=True) - MouseHandler.__init__(self) - self.vpanel = VerticalPanel() - self.vpanel.setStyleName('microblogPanel') - self.setWidget(self.vpanel) - if ((self._targets_type == C.ALL and self.host.mblog_available) or - (self._targets_type == C.GROUP and self.host.groupblog_available)): - self.addEntry(editable=True, edit_entry=True) - - self.getAll() - - # self.footer = HTML('', StyleName='microblogPanel_footer') - # self.footer.waiting = False - # self.footer.addClickListener(self) - # self.footer.addMouseListener(self) - # self.vpanel.add(self.footer) - # self.next_rsm_index = 0 - - def __str__(self): - return u"Blog Widget [targets: {}, profile: {}]".format(", ".join(self.targets) if self.targets else "meta blog", self.profile) - - def update(self): - self.entries.sort(key=lambda entry: entry.item.published, reverse=True) - - start_idx = 0 - if self.edit_entry is not None: - start_idx = 1 - if not self.edit_entry.displayed: - self.vpanel.insert(self.edit_entry, 0) - self.edit_entry.displayed = True - - # XXX: enumerate is buggued in pyjamas (start is not used) - # we have to use idx - idx = start_idx - for entry in self.entries: - if not entry.displayed: - self.vpanel.insert(entry, idx) - entry.displayed = True - idx += 1 - - # def onDelete(self): - # quick_widgets.QuickWidget.onDelete(self) - # self.host.removeListener('avatar', self.avatarListener) - - # def onAvatarUpdate(self, jid_, hash_, profile): - # """Called on avatar update events - - # @param jid_: jid of the entity with updated avatar - # @param hash_: hash of the avatar - # @param profile: %(doc_profile)s - # """ - # whoami = self.host.profiles[self.profile].whoami - # if self.isJidAccepted(jid_) or jid_.bare == whoami.bare: - # self.updateValue('avatar', jid_, hash_) - - @staticmethod - def onGroupDrop(host, targets): - """Create a microblog panel for one, several or all contact groups. - - @param host (SatWebFrontend): the SatWebFrontend instance - @param targets (tuple(unicode)): tuple of groups (empty for "all groups") - @return: the created MicroblogPanel - """ - # XXX: pyjamas doesn't support use of cls directly - widget = host.displayWidget(Blog, targets, dropped=True) - return widget - - # @property - # def accepted_groups(self): - # """Return a set of the accepted groups""" - # return set().union(*self.targets) - - def getWarningData(self, comment): - """ - @param comment: set to True if the composed message is a comment - @return: a couple (type, msg) for calling self.host.showWarning""" - if comment: - return ("PUBLIC", "This is a <span class='warningTarget'>comment</span> and keep the initial post visibility, so it is potentialy public") - elif self._targets_type == C.ALL: - # we have a meta MicroblogPanel, we publish publicly - return ("PUBLIC", self.warning_msg_public) - else: - # FIXME: manage several groups - return (self._targets_type, self.warning_msg_group % ' '.join(self.targets)) - - def ensureVisible(self, entry): - """Scroll to an entry to ensure its visibility - - @param entry (MicroblogEntry): the entry - """ - current = entry - while True: - parent = current.getParent() - if parent is None: - log.warning("Can't find any parent ScrollPanel") - return - elif isinstance(parent, ScrollPanel): - parent.ensureVisible(entry) - return - else: - current = parent - - def setSelectedEntry(self, entry, ensure_visible=False): - """Select an entry. - - @param entry (MicroblogEntry): the entry to select - @param ensure_visible (boolean): if True, also scroll to the entry - """ - if ensure_visible: - self.ensureVisible(entry) - - entry.addStyleName('selected_entry') # blink the clicked entry - clicked_entry = entry # entry may be None when the timer is done - Timer(500, lambda timer: clicked_entry.removeStyleName('selected_entry')) - - # def updateValue(self, type_, jid_, value): - # """Update a jid value in entries - - # @param type_: one of 'avatar', 'nick' - # @param jid_(jid.JID): jid concerned - # @param value: new value""" - # assert isinstance(jid_, jid.JID) # FIXME: temporary - # def updateVPanel(vpanel): - # avatar_url = self.host.getAvatarURL(jid_) - # for child in vpanel.children: - # if isinstance(child, MicroblogEntry) and child.author == jid_: - # child.updateAvatar(avatar_url) - # elif isinstance(child, VerticalPanel): - # updateVPanel(child) - # if type_ == 'avatar': - # updateVPanel(self.vpanel) - - # def onClick(self, sender): - # if sender == self.footer: - # self.loadMoreMainEntries() - - # def onMouseEnter(self, sender): - # if sender == self.footer: - # self.loadMoreMainEntries() - - -libervia_widget.LiberviaWidget.addDropKey("GROUP", lambda host, item: Blog.onGroupDrop(host, (item,))) -libervia_widget.LiberviaWidget.addDropKey("CONTACT_TITLE", lambda host, item: Blog.onGroupDrop(host, ())) -quick_blog.registerClass("ENTRY", Entry) -quick_widgets.register(quick_blog.QuickBlog, Blog)
--- a/src/browser/sat_browser/chat.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,345 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2011-2018 Jérôme Poisson <goffi@goffi.org> - -# This program 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. - -# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. - -from sat.core.log import getLogger -log = getLogger(__name__) - -# from sat_frontends.tools.games import SYMBOLS -from sat_browser import strings -from sat_frontends.tools import jid -from sat_frontends.quick_frontend import quick_widgets, quick_games, quick_menus -from sat_frontends.quick_frontend.quick_chat import QuickChat - -from pyjamas.ui.AbsolutePanel import AbsolutePanel -from pyjamas.ui.VerticalPanel import VerticalPanel -from pyjamas.ui.HorizontalPanel import HorizontalPanel -from pyjamas.ui.KeyboardListener import KEY_ENTER, KeyboardHandler -from pyjamas.ui.HTMLPanel import HTMLPanel -from pyjamas import DOM -from pyjamas import Window - -from datetime import datetime - -import html_tools -import libervia_widget -import base_panel -import contact_panel -import editor_widget -from constants import Const as C -import plugin_xep_0085 -import game_tarot -import game_radiocol - - -unicode = str # FIXME: pyjamas workaround - - -class MessageWidget(HTMLPanel): - - def __init__(self, mess_data): - """ - @param mess_data(quick_chat.Message, None): message data - None: used only for non text widgets (e.g.: focus separator) - """ - self.mess_data = mess_data - mess_data.widgets.add(self) - _msg_class = [] - if mess_data.type == C.MESS_TYPE_INFO: - markup = "<span class='{msg_class}'>{msg}</span>" - - if mess_data.extra.get('info_type') == 'me': - _msg_class.append('chatTextMe') - else: - _msg_class.append('chatTextInfo') - # FIXME: following code was in printInfo before refactoring - # seems to be used only in radiocol - # elif type_ == 'link': - # _wid = HTML(msg) - # _wid.setStyleName('chatTextInfo-link') - # if link_cb: - # _wid.addClickListener(link_cb) - else: - markup = "<span class='chat_text_timestamp'>{timestamp}</span> <span class='chat_text_nick'>{nick}</span> <span class='{msg_class}'>{msg}</span>" - _msg_class.append("chat_text_msg") - if mess_data.own_mess: - _msg_class.append("chat_text_mymess") - - xhtml = mess_data.main_message_xhtml - _date = datetime.fromtimestamp(float(mess_data.timestamp)) - HTMLPanel.__init__(self, markup.format( - timestamp = _date.strftime("%H:%M"), - nick = "[{}]".format(html_tools.html_sanitize(mess_data.nick)), - msg_class = ' '.join(_msg_class), - msg = strings.addURLToText(html_tools.html_sanitize(mess_data.main_message)) if not xhtml else html_tools.inlineRoot(xhtml) # FIXME: images and external links must be removed according to preferences - )) - if mess_data.type != C.MESS_TYPE_INFO: - self.setStyleName('chatText') - - -class Chat(QuickChat, libervia_widget.LiberviaWidget, KeyboardHandler): - - def __init__(self, host, target, type_=C.CHAT_ONE2ONE, nick=None, occupants=None, subject=None, profiles=None): - """Panel used for conversation (one 2 one or group chat) - - @param host: SatWebFrontend instance - @param target: entity (jid.JID) with who we have a conversation (contact's jid for one 2 one chat, or MUC room) - @param type: one2one for simple conversation, group for MUC - """ - QuickChat.__init__(self, host, target, type_, nick, occupants, subject, profiles=profiles) - self.vpanel = VerticalPanel() - self.vpanel.setSize('100%', '100%') - - # FIXME: temporary dirty initialization to display the OTR state - header_info = host.plugins['otr'].getInfoTextForUser(target) if (type_ == C.CHAT_ONE2ONE and 'otr' in host.plugins) else None - - libervia_widget.LiberviaWidget.__init__(self, host, title=unicode(target.bare), info=header_info, selectable=True) - self._body = AbsolutePanel() - self._body.setStyleName('chatPanel_body') - chat_area = HorizontalPanel() - chat_area.setStyleName('chatArea') - if type_ == C.CHAT_GROUP: - self.occupants_panel = contact_panel.ContactsPanel(host, merge_resources=False, - contacts_style="muc_contact", - contacts_menus=(C.MENU_JID_CONTEXT), - contacts_display=('resource',)) - chat_area.add(self.occupants_panel) - DOM.setAttribute(chat_area.getWidgetTd(self.occupants_panel), "className", "occupantsPanelCell") - # FIXME: workaround for a pyjamas issue: calling hash on a class method always return a different value if that method is defined directly within the class (with the "def" keyword) - self.presenceListener = self.onPresenceUpdate - self.host.addListener('presence', self.presenceListener, [C.PROF_KEY_NONE]) - self.avatarListener = self.onAvatarUpdate - host.addListener('avatar', self.avatarListener, [C.PROF_KEY_NONE]) - Window.addWindowResizeListener(self) - - else: - self.chat_state = None - - self._body.add(chat_area) - self.content = AbsolutePanel() - self.content.setStyleName('chatContent') - self.content_scroll = base_panel.ScrollPanelWrapper(self.content) - chat_area.add(self.content_scroll) - chat_area.setCellWidth(self.content_scroll, '100%') - self.vpanel.add(self._body) - self.vpanel.setCellHeight(self._body, '100%') - self.addStyleName('chatPanel') - self.setWidget(self.vpanel) - self.chat_state_machine = plugin_xep_0085.ChatStateMachine(self.host, unicode(self.target)) - - self.message_box = editor_widget.MessageBox(self.host) - self.message_box.onSelectedChange(self) - self.message_box.addKeyboardListener(self) - self.vpanel.add(self.message_box) - self.postInit() - - def onWindowResized(self, width=None, height=None): - if self.type == C.CHAT_GROUP: - ideal_height = self.content_scroll.getOffsetHeight() - self.occupants_panel.setHeight("%s%s" % (ideal_height, "px")) - - @property - def target(self): - # FIXME: for unknow reason, pyjamas doesn't use the method inherited from QuickChat - # FIXME: must remove this when either pyjamas is fixed, or we use an alternative - if self.type == C.CHAT_GROUP: - return self.current_target.bare - return self.current_target - - @property - def profile(self): - # FIXME: for unknow reason, pyjamas doesn't use the method inherited from QuickWidget - # FIXME: must remove this when either pyjamas is fixed, or we use an alternative - assert len(self.profiles) == 1 and not self.PROFILES_MULTIPLE and not self.PROFILES_ALLOW_NONE - return list(self.profiles)[0] - - @property - def plugin_menu_context(self): - return (C.MENU_ROOM,) if self.type == C.CHAT_GROUP else (C.MENU_SINGLE,) - - def onKeyDown(self, sender, keycode, modifiers): - if keycode == KEY_ENTER: - self.host.showWarning(None, None) - else: - self.host.showWarning(*self.getWarningData()) - - def getWarningData(self): - if self.type not in [C.CHAT_ONE2ONE, C.CHAT_GROUP]: - raise Exception("Unmanaged type !") - if self.type == C.CHAT_ONE2ONE: - msg = "This message will be sent to your contact <span class='warningTarget'>%s</span>" % self.target - elif self.type == C.CHAT_GROUP: - msg = "This message will be sent to all the participants of the multi-user room <span class='warningTarget'>%s</span>" % self.target - return ("ONE2ONE" if self.type == C.CHAT_ONE2ONE else "GROUP", msg) - - def onTextEntered(self, text): - self.host.messageSend(self.target, - {'': text}, - {}, - C.MESS_TYPE_GROUPCHAT if self.type == C.CHAT_GROUP else C.MESS_TYPE_CHAT, - {}, - errback=self.host.sendError, - profile_key=C.PROF_KEY_NONE - ) - self.chat_state_machine._onEvent("active") - - def onPresenceUpdate(self, entity, show, priority, statuses, profile): - """Update entity's presence status - - @param entity(jid.JID): entity updated - @param show: availability - @parap priority: resource's priority - @param statuses: dict of statuses - @param profile: %(doc_profile)s - """ - assert self.type == C.CHAT_GROUP - if entity.bare != self.target: - return - self.update(entity) - - def onAvatarUpdate(self, entity, hash_, profile): - """Called on avatar update events - - @param jid_: jid of the entity with updated avatar - @param hash_: hash of the avatar - @param profile: %(doc_profile)s - """ - assert self.type == C.CHAT_GROUP - if entity.bare != self.target: - return - self.update(entity) - - def onQuit(self): - libervia_widget.LiberviaWidget.onQuit(self) - if self.type == C.CHAT_GROUP: - self.host.removeListener('presence', self.presenceListener) - self.host.bridge.mucLeave(self.target.bare, profile=C.PROF_KEY_NONE) - - def newMessage(self, from_jid, target, msg, type_, extra, profile): - header_info = extra.pop('header_info', None) - if header_info: - self.setHeaderInfo(header_info) - QuickChat.newMessage(self, from_jid, target, msg, type_, extra, profile) - - def _onHistoryPrinted(self): - """Refresh or scroll down the focus after the history is printed""" - self.printMessages(clear=False) - super(Chat, self)._onHistoryPrinted() - - def printMessages(self, clear=True): - """generate message widgets - - @param clear(bool): clear message before printing if true - """ - if clear: - # FIXME: clear is not handler - pass - for message in self.messages.itervalues(): - self.appendMessage(message) - - def createMessage(self, message): - self.appendMessage(message) - - def appendMessage(self, message): - self.content.add(MessageWidget(message)) - self.content_scroll.scrollToBottom() - - def notify(self, contact="somebody", msg=""): - """Notify the user of a new message if primitivus doesn't have the focus. - - @param contact (unicode): contact who wrote to the users - @param msg (unicode): the message that has been received - """ - self.host.notification.notify(contact, msg) - - # def printDayChange(self, day): - # """Display the day on a new line. - - # @param day(unicode): day to display (or not if this method is not overwritten) - # """ - # self.printInfo("* " + day) - - def setTitle(self, title=None, extra=None): - """Refresh the title of this Chat dialog - - @param title (unicode): main title or None to use default - @param suffix (unicode): extra title (e.g. for chat states) or None - """ - if title is None: - title = unicode(self.target.bare) - if extra: - title += ' %s' % extra - libervia_widget.LiberviaWidget.setTitle(self, title) - - def onChatState(self, from_jid, state, profile): - super(Chat, self).onChatState(from_jid, state, profile) - if self.type == C.CHAT_ONE2ONE: - self.title_dynamic = C.CHAT_STATE_ICON[state] - - def update(self, entity=None): - """Update one or all entities. - - @param entity (jid.JID): entity to update - """ - if self.type == C.CHAT_ONE2ONE: # only update the chat title - if self.chat_state: - self.setTitle(extra='({})'.format(self.chat_state)) - else: - if entity is None: # rebuild all the occupants list - nicks = list(self.occupants) - nicks.sort() - self.occupants_panel.setList([jid.newResource(self.target, nick) for nick in nicks]) - else: # add, remove or update only one occupant - contact_list = self.host.contact_lists[self.profile] - show = contact_list.getCache(entity, C.PRESENCE_SHOW) - if show == C.PRESENCE_UNAVAILABLE or show is None: - self.occupants_panel.removeContactBox(entity) - else: - pass - # FIXME: legacy code, chat state must be checked - # box = self.occupants_panel.updateContactBox(entity) - # box.states.setHTML(u''.join(states.values())) - - # FIXME: legacy code, chat state must be checked - # if 'chat_state' in states.keys(): # start/stop sending "composing" state from now - # self.chat_state_machine.started = not not states['chat_state'] - - self.onWindowResized() # be sure to set the good height - - def addGamePanel(self, widget): - """Insert a game panel to this Chat dialog. - - @param widget (Widget): the game panel - """ - self.vpanel.insert(widget, 0) - self.vpanel.setCellHeight(widget, widget.getHeight()) - - def removeGamePanel(self, widget): - """Remove the game panel from this Chat dialog. - - @param widget (Widget): the game panel - """ - self.vpanel.remove(widget) - - -quick_widgets.register(QuickChat, Chat) -quick_widgets.register(quick_games.Tarot, game_tarot.TarotPanel) -quick_widgets.register(quick_games.Radiocol, game_radiocol.RadioColPanel) -libervia_widget.LiberviaWidget.addDropKey("CONTACT", lambda host, item: host.displayWidget(Chat, jid.JID(item), dropped=True)) -quick_menus.QuickMenusManager.addDataCollector(C.MENU_ROOM, {'room_jid': 'target'}) -quick_menus.QuickMenusManager.addDataCollector(C.MENU_SINGLE, {'jid': 'target'})
--- a/src/browser/sat_browser/constants.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,40 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a SAT frontend -# Copyright (C) 2009-2018 Jérôme Poisson (goffi@goffi.org) - -# This program 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. - -# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. - -from libervia.common.constants import Const as C - - -# Auxiliary functions -param_to_bool = lambda value: value == 'true' - - -class Const(C): - """Add here the constants that are only used by the browser side.""" - - # Cached parameters, e.g those that have an incidence on UI display/refresh: - # - they can be any parameter (not necessarily specific to Libervia) - # - list them as a couple (category, name) - CACHED_PARAMS = [('General', C.SHOW_OFFLINE_CONTACTS), - ('General', C.SHOW_EMPTY_GROUPS), - ] - - WEB_PANEL_DEFAULT_URL = "http://salut-a-toi.org" - WEB_PANEL_SCHEMES = {'http', 'https', 'ftp', 'file'} - - CONTACT_DEFAULT_DISPLAY=('bare', 'nick')
--- a/src/browser/sat_browser/contact_group.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,245 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2013-2016 Adrien Cossa <souliane@mailoo.org> - -# This program 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. - -# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. - -from pyjamas.ui.Button import Button -from pyjamas.ui.CheckBox import CheckBox -from pyjamas.ui.Label import Label -from pyjamas.ui.HorizontalPanel import HorizontalPanel -from pyjamas.ui.VerticalPanel import VerticalPanel -from pyjamas.ui.DialogBox import DialogBox -from pyjamas.ui.ScrollPanel import ScrollPanel -from pyjamas.ui import HasAlignment - -import dialog -import list_manager -import contact_panel -import contact_list -from sat_frontends.tools import jid - - -unicode = str # FIXME: pyjamas workaround - - -class ContactGroupManager(list_manager.ListManager): - - def __init__(self, editor, data, contacts, offsets): - """ - @param container (FlexTable): FlexTable parent widget - @param keys (dict{unicode: dict{unicode: unicode}}): dict binding items - keys to their display config data. - @param contacts (list): list of contacts - """ - self.editor = editor - list_manager.ListManager.__init__(self, data, contacts) - self.registerPopupMenuPanel(entries={"Remove group": {}}, - callback=lambda sender, key: self.removeGroup(sender)) - - def removeGroup(self, sender): - group = sender.getHTML() - - def confirm_cb(answer): - if answer: - list_manager.ListManager.removeList(self, group) - self.editor.add_group_panel.groups.remove(group) - - _dialog = dialog.ConfirmDialog(confirm_cb, text="Do you really want to delete the group '%s'?" % group) - _dialog.show() - - def tag(self, contacts): - list_manager.ListManager.tag(self, contacts) - self.editor.updateContactList(contacts) - - def untag(self, contacts, ignore_key=None): - list_manager.ListManager.untag(self, contacts, ignore_key) - self.editor.updateContactList(contacts) - - -class ContactGroupEditor(VerticalPanel): - """A big panel including a ContactGroupManager and other UI stuff.""" - - def __init__(self, host, container=None, onCloseCallback=None): - """ - - @param host (SatWebFrontend) - @param container (PanelBase): parent panel or None to display in a popup - @param onCloseCallback (callable) - """ - VerticalPanel.__init__(self, StyleName="contactGroupEditor") - self.host = host - - # eventually display in a popup - if container is None: - container = DialogBox(autoHide=False, centered=True) - container.setHTML("Manage contact groups") - self.container = container - self._on_close_callback = onCloseCallback - - self.all_contacts = contact_list.JIDList(self.host.contact_list.roster) - roster_entities_by_group = self.host.contact_list.roster_entities_by_group - del roster_entities_by_group[None] # remove the empty group - roster_groups = roster_entities_by_group.keys() - roster_groups.sort() - - # groups on the left - manager = self.initContactGroupManager(roster_entities_by_group) - self.add_group_panel = self.initAddGroupPanel(roster_groups) - left_container = VerticalPanel(Width="100%") - left_container.add(manager) - left_container.add(self.add_group_panel) - left_container.setCellHorizontalAlignment(self.add_group_panel, HasAlignment.ALIGN_CENTER) - left_panel = ScrollPanel(left_container, StyleName="contactGroupManager") - left_panel.setAlwaysShowScrollBars(True) - - # contact list on the right - east_panel = ScrollPanel(self.initContactList(), StyleName="contactGroupRoster") - east_panel.setAlwaysShowScrollBars(True) - - south_panel = self.initCloseSaveButtons() - - main_panel = HorizontalPanel() - main_panel.add(left_panel) - main_panel.add(east_panel) - self.add(Label("You get here an over whole view of your contact groups. There are two ways to assign your contacts to an existing group: write them into auto-completed textboxes or use the right panel to drag and drop them into the group.")) - self.add(main_panel) - self.add(south_panel) - - self.setCellHorizontalAlignment(south_panel, HasAlignment.ALIGN_CENTER) - - # need to be done after the contact list has been initialized - self.updateContactList() - - # Hide the contacts list from the main panel to not confuse the user - self.restore_contact_panel = False - clist = self.host.contact_list_widget - if clist.getVisible(): - self.restore_contact_panel = True - self.host.panel._contactsSwitch() - - container.add(self) - container.setVisible(True) - if isinstance(container, DialogBox): - container.center() - - def initContactGroupManager(self, data): - """Initialise the contact group manager. - - @param groups (list[unicode]): contact groups - """ - self.groups = ContactGroupManager(self, data, self.all_contacts) - return self.groups - - def initAddGroupPanel(self, groups): - """Initialise the 'Add group' panel. - - @param groups (list[unicode]): contact groups - """ - - def add_group_cb(key): - self.groups.addList(key) - self.add_group_panel.textbox.setFocus(True) - - add_group_panel = dialog.AddGroupPanel(groups, add_group_cb) - add_group_panel.addStyleName("addContactGroupPanel") - return add_group_panel - - def initCloseSaveButtons(self): - """Add the buttons to close the dialog and save the groups.""" - buttons = HorizontalPanel() - buttons.addStyleName("marginAuto") - buttons.add(Button("Cancel", listener=self.cancelWithoutSaving)) - buttons.add(Button("Save", listener=self.closeAndSave)) - return buttons - - def initContactList(self): - """Add the contact list to the DockPanel.""" - - self.toggle = CheckBox("Hide assigned contacts") - self.toggle.addClickListener(lambda dummy: self.updateContactList()) - self.toggle.addStyleName("toggleAssignedContacts") - self.contacts = contact_panel.ContactsPanel(self.host) - for contact in self.all_contacts: - self.contacts.updateContactBox(contact) - panel = VerticalPanel() - panel.add(self.toggle) - panel.add(self.contacts) - return panel - - def updateContactList(self, contacts=None): - """Update the contact list's items visibility, depending of the toggle - checkbox and the "contacts" attribute. - - @param contacts (list): contacts to be updated, or None to update all. - """ - if not hasattr(self, "toggle"): - return - if contacts is not None: - contacts = [jid.JID(contact) for contact in contacts] - contacts = set(contacts).intersection(self.all_contacts) - else: - contacts = self.all_contacts - - for contact in contacts: - if not self.toggle.getChecked(): # show all contacts - self.contacts.updateContactBox(contact).setVisible(True) - else: # show only non-assigned contacts - if contact in self.groups.untagged: - self.contacts.updateContactBox(contact).setVisible(True) - else: - self.contacts.updateContactBox(contact).setVisible(False) - - def __close(self): - """Remove the widget from parent or close the popup.""" - if isinstance(self.container, DialogBox): - self.container.hide() - self.container.remove(self) - if self._on_close_callback is not None: - self._on_close_callback() - if self.restore_contact_panel: - self.host.panel._contactsSwitch() - - def cancelWithoutSaving(self): - """Ask for confirmation before closing the dialog.""" - def confirm_cb(answer): - if answer: - self.__close() - - _dialog = dialog.ConfirmDialog(confirm_cb, text="Do you really want to cancel without saving?") - _dialog.show() - - def closeAndSave(self): - """Call bridge methods to save the changes and close the dialog""" - old_groups_by_entity = contact_list.JIDDict(self.host.contact_list.roster_groups_by_entity) - old_entities = old_groups_by_entity.keys() - result = {jid.JID(item): keys for item, keys in self.groups.getKeysByItem().iteritems()} - groups_by_entity = contact_list.JIDDict(result) - entities = groups_by_entity.keys() - - for invalid in entities.difference(self.all_contacts): - dialog.InfoDialog("Invalid contact(s)", - "The contact '%s' is not in your contact list but has been assigned to: '%s'." % (invalid, "', '".join(groups_by_entity[invalid])) + - "Your changes could not be saved: please check your assignments and save again.", Width="400px").center() - return - - for entity in old_entities.difference(entities): - self.host.bridge.call('updateContact', None, unicode(entity), '', []) - - for entity, groups in groups_by_entity.iteritems(): - if entity not in old_groups_by_entity or groups != old_groups_by_entity[entity]: - self.host.bridge.call('updateContact', None, unicode(entity), '', list(groups)) - self.__close()
--- a/src/browser/sat_browser/contact_list.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,319 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2011-2018 Jérôme Poisson <goffi@goffi.org> - -# This program 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. - -# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. - -import pyjd # this is dummy in pyjs -from sat.core.log import getLogger -log = getLogger(__name__) -from sat_frontends.quick_frontend.quick_contact_list import QuickContactList -from pyjamas.ui.SimplePanel import SimplePanel -from pyjamas.ui.ScrollPanel import ScrollPanel -from pyjamas.ui.VerticalPanel import VerticalPanel -from pyjamas.ui.ClickListener import ClickHandler -from pyjamas.ui.Label import Label -from pyjamas import Window -from pyjamas import DOM - -from constants import Const as C -from sat_frontends.tools import jid -import libervia_widget -import contact_panel -import blog -import chat - -unicode = str # XXX: pyjama doesn't manage unicode - - -class GroupLabel(libervia_widget.DragLabel, Label, ClickHandler): - def __init__(self, host, group): - """ - - @param host (SatWebFrontend) - @param group (unicode): group name - """ - self.group = group - Label.__init__(self, group) # , Element=DOM.createElement('div') - self.setStyleName('group') - libervia_widget.DragLabel.__init__(self, group, "GROUP", host) - ClickHandler.__init__(self) - self.addClickListener(self) - - def onClick(self, sender): - self.host.displayWidget(blog.Blog, (self.group,)) - - -class GroupPanel(VerticalPanel): - - def __init__(self, parent): - VerticalPanel.__init__(self) - self.setStyleName('groupPanel') - self._parent = parent - self._groups = set() - - def add(self, group): - if group in self._groups: - log.warning("trying to add an already existing group") - return - _item = GroupLabel(self._parent.host, group) - _item.addMouseListener(self._parent) - DOM.setStyleAttribute(_item.getElement(), "cursor", "pointer") - index = 0 - for group_ in [child.group for child in self.getChildren()]: - if group_ > group: - break - index += 1 - VerticalPanel.insert(self, _item, index) - self._groups.add(group) - - def remove(self, group): - for wid in self: - if isinstance(wid, GroupLabel) and wid.group == group: - VerticalPanel.remove(self, wid) - self._groups.remove(group) - return - log.warning("Trying to remove a non existent group") - - def getGroupBox(self, group): - """get the widget of a group - - @param group (unicode): the group - @return: GroupLabel instance if present, else None""" - for wid in self: - if isinstance(wid, GroupLabel) and wid.group == group: - return wid - return None - - def getGroups(self): - return self._groups - - -class ContactTitleLabel(libervia_widget.DragLabel, Label, ClickHandler): - - def __init__(self, host, text): - Label.__init__(self, text) # , Element=DOM.createElement('div') - self.setStyleName('contactTitle') - libervia_widget.DragLabel.__init__(self, text, "CONTACT_TITLE", host) - ClickHandler.__init__(self) - self.addClickListener(self) - - def onClick(self, sender): - self.host.displayWidget(blog.Blog, ()) - - -class ContactList(SimplePanel, QuickContactList): - """Manage the contacts and groups""" - - def __init__(self, host, target, on_click=None, on_change=None, user_data=None, profiles=None): - QuickContactList.__init__(self, host, C.PROF_KEY_NONE) - self.contact_list = self.host.contact_list - SimplePanel.__init__(self) - self.host = host - self.scroll_panel = ScrollPanel() - self.scroll_panel.addStyleName("gwt-ScrollPanel") # XXX: no class is set by Pyjamas - self.vPanel = VerticalPanel() - _title = ContactTitleLabel(host, 'Contacts') - DOM.setStyleAttribute(_title.getElement(), "cursor", "pointer") - - def on_click(contact_jid): - self.host.displayWidget(chat.Chat, contact_jid, type_=C.CHAT_ONE2ONE) - self.removeAlerts(contact_jid, True) - - self._contacts_panel = contact_panel.ContactsPanel(host, contacts_click=on_click, contacts_menus=(C.MENU_JID_CONTEXT, C.MENU_ROSTER_JID_CONTEXT)) - self._group_panel = GroupPanel(self) - - self.vPanel.add(_title) - self.vPanel.add(self._group_panel) - self.vPanel.add(self._contacts_panel) - self.scroll_panel.add(self.vPanel) - self.add(self.scroll_panel) - self.setStyleName('contactList') - Window.addWindowResizeListener(self) - - # FIXME: workaround for a pyjamas issue: calling hash on a class method always return a different value if that method is defined directly within the class (with the "def" keyword) - self.avatarListener = self.onAvatarUpdate - host.addListener('avatar', self.avatarListener, [C.PROF_KEY_NONE]) - self.postInit() - - @property - def profile(self): - return C.PROF_KEY_NONE - - def onDelete(self): - QuickContactList.onDelete(self) - self.host.removeListener('avatar', self.avatarListener) - - def update(self, entities=None, type_=None, profile=None): - # XXX: as update is slow, we avoid many updates on profile plugs - # and do them all at once at the end - if not self.host._profile_plugged: # FIXME: should not be necessary anymore (done in QuickFrontend) - return - ### GROUPS ### - _keys = self.contact_list._groups.keys() - try: - # XXX: Pyjamas doesn't do the set casting if None is present - _keys.remove(None) - except (KeyError, ValueError): # XXX: error raised depend on pyjama's compilation options - pass - current_groups = set(_keys) - shown_groups = self._group_panel.getGroups() - new_groups = current_groups.difference(shown_groups) - removed_groups = shown_groups.difference(current_groups) - for group in new_groups: - self._group_panel.add(group) - for group in removed_groups: - self._group_panel.remove(group) - - ### JIDS ### - to_show = [jid_ for jid_ in self.contact_list.roster if self.contact_list.entityVisible(jid_) and jid_ != self.contact_list.whoami.bare] - to_show.sort() - - self._contacts_panel.setList(to_show) - - def onWindowResized(self, width, height): - ideal_height = height - DOM.getAbsoluteTop(self.getElement()) - 5 - tab_panel = self.host.panel.tab_panel - if tab_panel.getWidgetCount() > 1: - ideal_height -= tab_panel.getTabBar().getOffsetHeight() - self.scroll_panel.setHeight("%s%s" % (ideal_height, "px")) - - def isContactInRoster(self, contact_jid): - """Test if the contact is in our roster list""" - for contact_box in self._contacts_panel: - if contact_jid == contact_box.jid: - return True - return False - - def getGroups(self): - return set([g for g in self._groups if g is not None]) - - def onMouseMove(self, sender, x, y): - pass - - def onMouseDown(self, sender, x, y): - pass - - def onMouseUp(self, sender, x, y): - pass - - def onMouseEnter(self, sender): - if isinstance(sender, GroupLabel): - jids = self.contact_list.getGroupData(sender.group, "jids") - for contact in self._contacts_panel.getBoxes(): - if contact.jid in jids: - contact.label.addStyleName("selected") - - def onMouseLeave(self, sender): - if isinstance(sender, GroupLabel): - jids = self.contact_list.getGroupData(sender.group, "jids") - for contact in self._contacts_panel.getBoxes(): - if contact.jid in jids: - contact.label.removeStyleName("selected") - - def onAvatarUpdate(self, jid_, hash_, profile): - """Called on avatar update events - - @param jid_: jid of the entity with updated avatar - @param hash_: hash of the avatar - @param profile: %(doc_profile)s - """ - box = self._contacts_panel.getContactBox(jid_) - if box: - box.update() - - def onNickUpdate(self, jid_, new_nick, profile): - box = self._contacts_panel.getContactBox(jid_) - if box: - box.update() - - def offlineContactsToShow(self): - """Tell if offline contacts should be visible according to the user settings - - @return: boolean - """ - return C.bool(self.host.getCachedParam('General', C.SHOW_OFFLINE_CONTACTS)) - - def emtyGroupsToShow(self): - """Tell if empty groups should be visible according to the user settings - - @return: boolean - """ - return C.bool(self.host.getCachedParam('General', C.SHOW_EMPTY_GROUPS)) - - def onPresenceUpdate(self, entity, show, priority, statuses, profile): - QuickContactList.onPresenceUpdate(self, entity, show, priority, statuses, profile) - box = self._contacts_panel.getContactBox(entity) - if box: # box doesn't exist for MUC bare entity, don't create it - box.update() - - -class JIDList(list): - """JID-friendly list implementation for Pyjamas""" - - def __contains__(self, item): - """Tells if the list contains the given item. - - @param item (object): element to check - @return: bool - """ - # Since our JID doesn't inherit from str/unicode, without this method - # the test would return True only when the objects references are the - # same. Tests have shown that the other iterable "set" and "dict" don't - # need this hack to reproduce the Twisted's behavior. - for other in self: - if other == item: - return True - return False - - def index(self, item): - i = 0 - for other in self: - if other == item: - return i - i += 1 - raise ValueError("JIDList.index(%(item)s): %(item)s not in list" % {"item": item}) - -class JIDSet(set): - """JID set implementation for Pyjamas""" - - def __contains__(self, item): - return __containsJID(self, item) - - -class JIDDict(dict): - """JID dict implementation for Pyjamas (a dict with JID keys)""" - - def __contains__(self, item): - return __containsJID(self, item) - - def keys(self): - return JIDSet(dict.keys(self)) - - -def __containsJID(iterable, item): - """Tells if the given item is in the iterable, works with JID. - - @param iterable(object): list, set or another iterable object - @param item (object): element - @return: bool - """ - # Pyjamas JID-friendly implementation of the "in" operator. Since our JID - # doesn't inherit from str, without this method the test would return True - # only when the objects references are the same. - if isinstance(item, jid.JID): - return hash(item) in [hash(other) for other in iterable if isinstance(other, jid.JID)] - return super(type(iterable), iterable).__contains__(iterable, item)
--- a/src/browser/sat_browser/contact_panel.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,157 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2011-2018 Jérôme Poisson <goffi@goffi.org> - -# This program 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. - -# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. - -""" Contacts / jids related panels """ - -import pyjd # this is dummy in pyjs -from sat.core.log import getLogger -log = getLogger(__name__) -from sat_frontends.tools import jid - -from pyjamas.ui.ScrollPanel import ScrollPanel -from pyjamas.ui.VerticalPanel import VerticalPanel - -import contact_widget -from constants import Const as C - - -class ContactsPanel(ScrollPanel): - """ContactList graphic representation - - Special features like popup menu panel or changing the contact states must be done in a sub-class. - """ - - def __init__(self, host, merge_resources=True, contacts_click=None, - contacts_style=None, contacts_menus=None, - contacts_display=C.CONTACT_DEFAULT_DISPLAY): - """ - - @param host (SatWebFrontend): host instance - @param merge_resources (bool): if True, the entities sharing the same - bare JID will also share the same contact box. - @param contacts_click (callable): click callback for the contact boxes - @param contacts_style (unicode): CSS style name for the contact boxes - @param contacts_menus (tuple): define the menu types that fit this - contact panel, with values from the menus type constants. - @param contacts_display (tuple): prioritize the display methods of the - contact's label with values in ("jid", "nick", "bare", "resource") - """ - self.panel = VerticalPanel() - ScrollPanel.__init__(self, self.panel) - self.addStyleName("gwt-ScrollPanel") # XXX: no class is set by Pyjamas - - self.host = host - self.merge_resources = merge_resources - self._contacts = {} # entity jid to ContactBox map - self.panel.click_listener = None - - if contacts_click is not None: - self.panel.onClick = contacts_click - - self.contacts_style = contacts_style - self.contacts_menus = contacts_menus - self.contacts_display = contacts_display - - def _key(self, contact_jid): - """Return internal key for this contact. - - @param contact_jid (jid.JID): contact JID - @return: jid.JID - """ - return contact_jid.bare if self.merge_resources else contact_jid - - def getJids(self): - """Return jids present in the panel - - @return (list[jid.JID]): full jids or bare jids if self.merge_resources is True - """ - return self._contacts.keys() - - def getBoxes(self): - """Return ContactBox present in the panel - - @return (list[ContactBox]): boxes of the contacts - """ - return self._contacts.itervalues() - - def clear(self): - """Clear all contacts.""" - self._contacts.clear() - VerticalPanel.clear(self.panel) - - def setList(self, jids): - """set all contacts in the list in one shot. - - @param jids (list[jid.JID]): jids to display (the order is kept) - @param name (unicode): optional name of the contact - """ - current_jids = [box.jid for box in self.panel.children if isinstance(box, contact_widget.ContactBox)] - if current_jids == jids: - # the display doesn't change - return - for contact_jid in set(current_jids).difference(jids): - self.removeContactBox(contact_jid) - count = 0 - for contact_jid in jids: - assert isinstance(contact_jid, jid.JID) - self.updateContactBox(contact_jid, count) - count += 1 - - def getContactBox(self, contact_jid): - """Get the contact box for the given contact. - - @param contact_jid (jid.JID): contact JID - @return: ContactBox or None - """ - try: - return self._contacts[self._key(contact_jid)] - except KeyError: - return None - - def updateContactBox(self, contact_jid, index=None): - """Add a contact or update it if it already exists. - - @param contact_jid (jid.JID): contact JID - @param index (int): insertion index if the contact is added - @return: ContactBox - """ - box = self.getContactBox(contact_jid) - if box: - box.update() - else: - entity = contact_jid.bare if self.merge_resources else contact_jid - box = contact_widget.ContactBox(self.host, entity, - style_name=self.contacts_style, - display=self.contacts_display, - plugin_menu_context=self.contacts_menus) - self._contacts[self._key(contact_jid)] = box - if index: - VerticalPanel.insert(self.panel, box, index) - else: - VerticalPanel.append(self.panel, box) - return box - - def removeContactBox(self, contact_jid): - """Remove a contact. - - @param contact_jid (jid.JID): contact JID - """ - box = self._contacts.pop(self._key(contact_jid), None) - if box: - VerticalPanel.remove(self.panel, box)
--- a/src/browser/sat_browser/contact_widget.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,160 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2011-2018 Jérôme Poisson <goffi@goffi.org> - -# This program 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. - -# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. - -import pyjd # this is dummy in pyjs -from sat.core.log import getLogger -log = getLogger(__name__) - -from sat.core import exceptions -from sat_frontends.quick_frontend import quick_menus -from pyjamas.ui.VerticalPanel import VerticalPanel -from pyjamas.ui.HTML import HTML -from pyjamas.ui.Image import Image -from pyjamas.ui.ClickListener import ClickHandler -from constants import Const as C -import html_tools -import base_widget -import libervia_widget - -unicode = str # XXX: pyjama doesn't manage unicode - - -class ContactLabel(HTML): - """Display a contact in HTML, selecting best display (jid/nick/etc)""" - - def __init__(self, host, jid_, display=C.CONTACT_DEFAULT_DISPLAY): - """ - - @param host (SatWebFrontend): host instance - @param jid_ (jid.JID): contact JID - @param display (tuple): prioritize the display methods of the contact's - label with values in ("jid", "nick", "bare", "resource"). - """ - # TODO: add a listener for nick changes - HTML.__init__(self) - self.host = host - self.jid = jid_ - self.display = display - self.alert = False - self.setStyleName('contactLabel') - - def update(self): - clist = self.host.contact_list - notifs = list(self.host.getNotifs(self.jid, exact_jid=False, profile=C.PROF_KEY_NONE)) - alerts_count = len(notifs) - alert_html = ("<strong>(%i)</strong> " % alerts_count) if alerts_count else "" - - contact_raw = None - for disp in self.display: - if disp == "jid": - contact_raw = unicode(self.jid) - elif disp == "nick": - clist = self.host.contact_list - contact_raw = html_tools.html_sanitize(clist.getCache(self.jid, "nick")) - elif disp == "bare": - contact_raw = unicode(self.jid.bare) - elif disp == "resource": - contact_raw = self.jid.resource - else: - raise exceptions.InternalError(u"Unknown display argument [{}]".format(disp)) - if contact_raw: - break - if not contact_raw: - log.error(u"Could not find a contact display for jid {jid} (display: {display})".format(jid=self.jid, display=self.display)) - contact_raw = "UNNAMED" - contact_html = html_tools.html_sanitize(contact_raw) - - html = "%(alert)s%(contact)s" % {'alert': alert_html, - 'contact': contact_html} - self.setHTML(html) - - -class ContactMenuBar(base_widget.WidgetMenuBar): - """WidgetMenuBar displaying the contact's avatar as category.""" - - def onBrowserEvent(self, event): - base_widget.WidgetMenuBar.onBrowserEvent(self, event) - event.stopPropagation() # prevent opening the chat dialog - - @classmethod - def getCategoryHTML(cls, category): - """Return the HTML code for displaying contact's avatar. - - @param category (quick_menus.MenuCategory): ignored - @return(unicode): HTML to display - """ - return '<img src="%s"/>' % C.DEFAULT_AVATAR_URL - - def setUrl(self, url): - """Set the URL of the contact avatar. - - @param url (unicode): avatar URL - """ - if not self.items: # the menu is empty but we've been asked to set an avatar - self.addCategory("dummy") - self.items[0].setHTML('<img src="%s" />' % url) - - -class ContactBox(VerticalPanel, ClickHandler, libervia_widget.DragLabel): - - def __init__(self, host, jid_, style_name=None, display=C.CONTACT_DEFAULT_DISPLAY, plugin_menu_context=None): - """ - @param host (SatWebFrontend): host instance - @param jid_ (jid.JID): contact JID - @param style_name (unicode): CSS style name - @param contacts_display (tuple): prioritize the display methods of the - contact's label with values in ("jid", "nick", "bare", "resource"). - @param plugin_menu_context (iterable): contexts of menus to have (list of C.MENU_* constant) - - """ - self.plugin_menu_context = [] if plugin_menu_context is None else plugin_menu_context - VerticalPanel.__init__(self, StyleName=style_name or 'contactBox', VerticalAlignment='middle') - ClickHandler.__init__(self) - libervia_widget.DragLabel.__init__(self, jid_, "CONTACT", host) - self.jid = jid_ - self.label = ContactLabel(host, self.jid, display=display) - self.avatar = ContactMenuBar(self, host) if plugin_menu_context else Image() - self.states = HTML() - self.add(self.avatar) - self.add(self.label) - self.add(self.states) - self.update() - self.addClickListener(self) - - def update(self): - """Update the display. - - @param with_bare (bool): if True, ignore the resource and update with bare information. - """ - self.avatar.setUrl(self.host.getAvatarURL(self.jid)) - - self.label.update() - clist = self.host.contact_list - show = clist.getCache(self.jid, C.PRESENCE_SHOW) - if show is None: - show = C.PRESENCE_UNAVAILABLE - html_tools.setPresenceStyle(self.label, show) - - def onClick(self, sender): - try: - self.parent.onClick(self.jid) - except (AttributeError, TypeError): - pass - -quick_menus.QuickMenusManager.addDataCollector(C.MENU_JID_CONTEXT, lambda caller, dummy: {'jid': unicode(caller.jid.bare)})
--- a/src/browser/sat_browser/dialog.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,616 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2011-2018 Jérôme Poisson <goffi@goffi.org> - -# This program 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. - -# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. - -from sat.core.log import getLogger -log = getLogger(__name__) - -from constants import Const as C -from sat_frontends.tools import jid - -from pyjamas.ui.VerticalPanel import VerticalPanel -from pyjamas.ui.Grid import Grid -from pyjamas.ui.HorizontalPanel import HorizontalPanel -from pyjamas.ui.PopupPanel import PopupPanel -from pyjamas.ui.DialogBox import DialogBox -from pyjamas.ui.ListBox import ListBox -from pyjamas.ui.Button import Button -from pyjamas.ui.TextBox import TextBox -from pyjamas.ui.Label import Label -from pyjamas.ui.HTML import HTML -from pyjamas.ui.RadioButton import RadioButton -from pyjamas.ui import HasAlignment -from pyjamas.ui.KeyboardListener import KEY_ESCAPE, KEY_ENTER -from pyjamas.ui.MouseListener import MouseWheelHandler -from pyjamas import Window - -import base_panel - - -# List here the patterns that are not allowed in contact group names -FORBIDDEN_PATTERNS_IN_GROUP = () - - -unicode = str # XXX: pyjama doesn't manage unicode - - -class RoomChooser(Grid): - """Select a room from the rooms you already joined, or create a new one""" - - GENERATE_MUC = "<use random name>" - - def __init__(self, host, room_jid_s=None): - """ - - @param host (SatWebFrontend) - @param room_jid_s (unicode): room JID - """ - Grid.__init__(self, 2, 2, Width='100%') - self.host = host - - self.new_radio = RadioButton("room", "Discussion room:") - self.new_radio.setChecked(True) - self.box = TextBox(Width='95%') - self.box.setText(room_jid_s if room_jid_s else self.GENERATE_MUC) - self.exist_radio = RadioButton("room", "Already joined:") - self.rooms_list = ListBox(Width='95%') - - self.add(self.new_radio, 0, 0) - self.add(self.box, 0, 1) - self.add(self.exist_radio, 1, 0) - self.add(self.rooms_list, 1, 1) - - self.box.addFocusListener(self) - self.rooms_list.addFocusListener(self) - - self.exist_radio.setVisible(False) - self.rooms_list.setVisible(False) - self.refreshOptions() - - @property - def room(self): - """Get the room that has been selected or entered by the user - - @return: jid.JID or None to let the backend generate a new name - """ - if self.exist_radio.getChecked(): - values = self.rooms_list.getSelectedValues() - return jid.JID(values[0]) if values else None - value = self.box.getText() - return None if value in ('', self.GENERATE_MUC) else jid.JID(value) - - def onFocus(self, sender): - if sender == self.rooms_list: - self.exist_radio.setChecked(True) - elif sender == self.box: - if self.box.getText() == self.GENERATE_MUC: - self.box.setText("") - self.new_radio.setChecked(True) - - def onLostFocus(self, sender): - if sender == self.box: - if self.box.getText() == "": - self.box.setText(self.GENERATE_MUC) - - def refreshOptions(self): - """Refresh the already joined room list""" - contact_list = self.host.contact_list - muc_rooms = contact_list.getSpecials(C.CONTACT_SPECIAL_GROUP) - for room in muc_rooms: - self.rooms_list.addItem(room.bare) - if len(muc_rooms) > 0: - self.exist_radio.setVisible(True) - self.rooms_list.setVisible(True) - self.exist_radio.setChecked(True) - - -class ContactsChooser(VerticalPanel): - """Select one or several connected contacts""" - - def __init__(self, host, nb_contact=None, ok_button=None): - """ - @param host: SatWebFrontend instance - @param nb_contact: number of contacts that have to be selected, None for no limit - If a tuple is given instead of an integer, nb_contact[0] is the minimal and - nb_contact[1] is the maximal number of contacts to be chosen. - """ - self.host = host - if isinstance(nb_contact, tuple): - if len(nb_contact) == 0: - nb_contact = None - elif len(nb_contact) == 1: - nb_contact = (nb_contact[0], nb_contact[0]) - elif nb_contact is not None: - nb_contact = (nb_contact, nb_contact) - if nb_contact is None: - log.debug("Need to select as many contacts as you want") - else: - log.debug("Need to select between %d and %d contacts" % nb_contact) - self.nb_contact = nb_contact - self.ok_button = ok_button - VerticalPanel.__init__(self, Width='100%') - self.contacts_list = ListBox() - self.contacts_list.setMultipleSelect(True) - self.contacts_list.setWidth("95%") - self.contacts_list.addStyleName('contactsChooser') - self.contacts_list.addChangeListener(self.onChange) - self.add(self.contacts_list) - self.refreshOptions() - self.onChange() - - @property - def contacts(self): - """Return the selected contacts. - - @return: list[jid.JID] - """ - return [jid.JID(contact) for contact in self.contacts_list.getSelectedValues(True)] - - def onChange(self, sender=None): - if self.ok_button is None: - return - if self.nb_contact: - selected = len(self.contacts_list.getSelectedValues(True)) - if selected >= self.nb_contact[0] and selected <= self.nb_contact[1]: - self.ok_button.setEnabled(True) - else: - self.ok_button.setEnabled(False) - - def refreshOptions(self, keep_selected=False): - """Fill the list with the connected contacts. - - @param keep_selected (boolean): if True, keep the current selection - """ - selection = self.contacts if keep_selected else [] - self.contacts_list.clear() - contacts = self.host.contact_list.roster_connected - self.contacts_list.setVisibleItemCount(10 if len(contacts) > 5 else 5) - self.contacts_list.addItem("") - for contact in contacts: - self.contacts_list.addItem(contact) - if selection: - self.contacts_list.setItemTextSelection([unicode(contact) for contact in selection]) - - -class RoomAndContactsChooser(DialogBox): - """Select a room and some users to invite in""" - - def __init__(self, host, callback, nb_contact=None, ok_button="OK", title="Discussion groups", - title_room="Join room", title_invite="Invite contacts", visible=(True, True)): - DialogBox.__init__(self, centered=True) - self.host = host - self.callback = callback - self.title_room = title_room - self.title_invite = title_invite - - button_panel = HorizontalPanel() - button_panel.addStyleName("marginAuto") - ok_button = Button("OK", self.onOK) - button_panel.add(ok_button) - button_panel.add(Button("Cancel", self.onCancel)) - - self.room_panel = RoomChooser(host, None if visible == (False, True) else host.default_muc) - self.contact_panel = ContactsChooser(host, nb_contact, ok_button) - - self.stack_panel = base_panel.ToggleStackPanel(Width="100%") - self.stack_panel.add(self.room_panel, visible=visible[0]) - self.stack_panel.add(self.contact_panel, visible=visible[1]) - self.stack_panel.addStackChangeListener(self) - self.onStackChanged(self.stack_panel, 0, visible[0]) - self.onStackChanged(self.stack_panel, 1, visible[1]) - - main_panel = VerticalPanel() - main_panel.setStyleName("room-contact-chooser") - main_panel.add(self.stack_panel) - main_panel.add(button_panel) - - self.setWidget(main_panel) - self.setHTML(title) - self.show() - - # FIXME: workaround for a pyjamas issue: calling hash on a class method always return a different value if that method is defined directly within the class (with the "def" keyword) - self.presenceListener = self.refreshContactList - # update the contacts list when someone logged in/out - self.host.addListener('presence', self.presenceListener, [C.PROF_KEY_NONE]) - - @property - def room(self): - """Get the room that has been selected or entered by the user - - @return: jid.JID or None - """ - return self.room_panel.room - - @property - def contacts(self): - """Return the selected contacts. - - @return: list[jid.JID] - """ - return self.contact_panel.contacts - - def onStackChanged(self, sender, index, visible=None): - if visible is None: - visible = sender.getWidget(index).getVisible() - if index == 0: - suffix = "" if (visible or not self.room) else ": %s" % self.room - sender.setStackText(0, self.title_room + suffix) - elif index == 1: - suffix = "" if (visible or not self.contacts) else ": %s" % ", ".join([unicode(contact) for contact in self.contacts]) - sender.setStackText(1, self.title_invite + suffix) - - def refreshContactList(self, *args, **kwargs): - """Called when someone log in/out to update the list. - - @param args: set by the event call but not used here - """ - self.contact_panel.refreshOptions(keep_selected=True) - - def onOK(self, sender): - room = self.room # pyjamas issue: you need to use an intermediate variable to access a property's method - self.hide() - self.callback(room, self.contacts) - - def onCancel(self, sender): - self.hide() - - def hide(self): - self.host.removeListener('presence', self.presenceListener) - DialogBox.hide(self, autoClosed=True) - - -class GenericConfirmDialog(DialogBox): - - def __init__(self, widgets, callback, title='Confirmation', prompt_widgets=None, **kwargs): - """ - Dialog to confirm an action - @param widgets (list[Widget]): widgets to attach - @param callback (callable): method to call when a button is pressed, - with the following arguments: - - result (bool): set to True if the dialog has been confirmed - - *args: a list of unicode (the values for the prompt_widgets) - @param title: title of the dialog - @param prompt_widgets (list[TextBox]): input widgets from which to retrieve - the string value(s) to be passed to the callback when OK button is pressed. - If None, OK button will return "True". Cancel button always returns "False". - """ - self.callback = callback - added_style = kwargs.pop('AddStyleName', None) - DialogBox.__init__(self, centered=True, **kwargs) - if added_style: - self.addStyleName(added_style) - - if prompt_widgets is None: - prompt_widgets = [] - - content = VerticalPanel() - content.setWidth('100%') - for wid in widgets: - content.add(wid) - if wid in prompt_widgets: - wid.setWidth('100%') - button_panel = HorizontalPanel() - button_panel.addStyleName("marginAuto") - self.confirm_button = Button("OK", self.onConfirm) - button_panel.add(self.confirm_button) - self.cancel_button = Button("Cancel", self.onCancel) - button_panel.add(self.cancel_button) - content.add(button_panel) - self.setHTML(title) - self.setWidget(content) - self.prompt_widgets = prompt_widgets - - def onConfirm(self, sender): - self.hide() - result = [True] - result.extend([box.getText() for box in self.prompt_widgets]) - self.callback(*result) - - def onCancel(self, sender): - self.hide() - self.callback(False) - - def show(self): - DialogBox.show(self) - if self.prompt_widgets: - self.prompt_widgets[0].setFocus(True) - - -class ConfirmDialog(GenericConfirmDialog): - - def __init__(self, callback, text='Are you sure ?', title='Confirmation', **kwargs): - GenericConfirmDialog.__init__(self, [HTML(text)], callback, title, **kwargs) - - -class GenericDialog(DialogBox): - """Dialog which just show a widget and a close button""" - - def __init__(self, title, main_widget, callback=None, options=None, **kwargs): - """Simple notice dialog box - @param title: HTML put in the header - @param main_widget: widget put in the body - @param callback: method to call on closing - @param options: one or more of the following options: - - NO_CLOSE: don't add a close button""" - added_style = kwargs.pop('AddStyleName', None) - DialogBox.__init__(self, centered=True, **kwargs) - if added_style: - self.addStyleName(added_style) - - self.callback = callback - if not options: - options = [] - _body = VerticalPanel() - _body.setSize('100%', '100%') - _body.setSpacing(4) - _body.add(main_widget) - _body.setCellWidth(main_widget, '100%') - _body.setCellHeight(main_widget, '100%') - if 'NO_CLOSE' not in options: - _close_button = Button("Close", self.onClose) - _body.add(_close_button) - _body.setCellHorizontalAlignment(_close_button, HasAlignment.ALIGN_CENTER) - self.setHTML(title) - self.setWidget(_body) - self.panel.setSize('100%', '100%') # Need this hack to have correct size in Gecko & Webkit - - def close(self): - """Same effect as clicking the close button""" - self.onClose(None) - - def onClose(self, sender): - self.hide() - if self.callback: - self.callback() - - -class InfoDialog(GenericDialog): - - def __init__(self, title, body, callback=None, options=None, **kwargs): - GenericDialog.__init__(self, title, HTML(body), callback, options, **kwargs) - - -class PromptDialog(GenericConfirmDialog): - - def __init__(self, callback, textes=None, values=None, title='User input', **kwargs): - """Prompt the user for one or more input(s). - - @param callback (callable): method to call when a button is pressed, - with the following arguments: - - result (bool): set to True if the dialog has been confirmed - - *args: a list of unicode (the values entered by the user) - @param textes (list[unicode]): HTML textes to display before the inputs - @param values (list[unicode]): default values for each input - @param title (unicode): dialog title - """ - if textes is None: - textes = [''] # display a single input without any description - if values is None: - values = [] - all_widgets = [] - prompt_widgets = [] - for count in xrange(len(textes)): - all_widgets.append(HTML(textes[count])) - prompt = TextBox() - if len(values) > count: - prompt.setText(values[count]) - all_widgets.append(prompt) - prompt_widgets.append(prompt) - - GenericConfirmDialog.__init__(self, all_widgets, callback, title, prompt_widgets, **kwargs) - - -class PopupPanelWrapper(PopupPanel): - """This wrapper catch Escape event to avoid request cancellation by Firefox""" - - def onEventPreview(self, event): - if event.type in ["keydown", "keypress", "keyup"] and event.keyCode == KEY_ESCAPE: - # needed to prevent request cancellation in Firefox - event.preventDefault() - return PopupPanel.onEventPreview(self, event) - - -class ExtTextBox(TextBox): - """Extended TextBox""" - - def __init__(self, *args, **kwargs): - if 'enter_cb' in kwargs: - self.enter_cb = kwargs['enter_cb'] - del kwargs['enter_cb'] - TextBox.__init__(self, *args, **kwargs) - self.addKeyboardListener(self) - - def onKeyUp(self, sender, keycode, modifiers): - pass - - def onKeyDown(self, sender, keycode, modifiers): - pass - - def onKeyPress(self, sender, keycode, modifiers): - if self.enter_cb and keycode == KEY_ENTER: - self.enter_cb(self) - - -class GroupSelector(DialogBox): - - def __init__(self, top_widgets, initial_groups, selected_groups, - ok_title="OK", ok_cb=None, cancel_cb=None): - DialogBox.__init__(self, centered=True) - main_panel = VerticalPanel() - self.ok_cb = ok_cb - self.cancel_cb = cancel_cb - - for wid in top_widgets: - main_panel.add(wid) - - main_panel.add(Label('Select in which groups your contact is:')) - self.list_box = ListBox() - self.list_box.setMultipleSelect(True) - self.list_box.setVisibleItemCount(5) - self.setAvailableGroups(initial_groups) - self.setGroupsSelected(selected_groups) - main_panel.add(self.list_box) - - def cb(text): - self.list_box.addItem(text) - self.list_box.setItemSelected(self.list_box.getItemCount() - 1, "selected") - - main_panel.add(AddGroupPanel(initial_groups, cb)) - - button_panel = HorizontalPanel() - button_panel.addStyleName("marginAuto") - button_panel.add(Button(ok_title, self.onOK)) - button_panel.add(Button("Cancel", self.onCancel)) - main_panel.add(button_panel) - - self.setWidget(main_panel) - - def getSelectedGroups(self): - """Return a list of selected groups""" - return self.list_box.getSelectedValues() - - def setAvailableGroups(self, groups): - _groups = list(set(groups)) - _groups.sort() - self.list_box.clear() - for group in _groups: - self.list_box.addItem(group) - - def setGroupsSelected(self, selected_groups): - self.list_box.setItemTextSelection(selected_groups) - - def onOK(self, sender): - self.hide() - if self.ok_cb: - self.ok_cb(self) - - def onCancel(self, sender): - self.hide() - if self.cancel_cb: - self.cancel_cb(self) - - -class AddGroupPanel(HorizontalPanel): - def __init__(self, groups, cb=None): - """ - @param groups: list of the already existing groups - """ - HorizontalPanel.__init__(self) - self.groups = groups - self.add(Label('New group:')) - self.textbox = ExtTextBox(enter_cb=self.onGroupInput) - self.add(self.textbox) - self.add(Button("Add", lambda sender: self.onGroupInput(self.textbox))) - self.cb = cb - - def onGroupInput(self, sender): - text = sender.getText() - if text == "": - return - for group in self.groups: - if text == group: - Window.alert("The group '%s' already exists." % text) - return - for pattern in FORBIDDEN_PATTERNS_IN_GROUP: - if pattern in text: - Window.alert("The pattern '%s' is not allowed in group names." % pattern) - return - sender.setText('') - self.groups.append(text) - if self.cb is not None: - self.cb(text) - - -class WheelTextBox(TextBox, MouseWheelHandler): - - def __init__(self, *args, **kwargs): - TextBox.__init__(self, *args, **kwargs) - MouseWheelHandler.__init__(self) - - -class IntSetter(HorizontalPanel): - """This class show a bar with button to set an int value""" - - def __init__(self, label, value=0, value_max=None, visible_len=3): - """initialize the intSetter - @param label: text shown in front of the setter - @param value: initial value - @param value_max: limit value, None or 0 for unlimited""" - HorizontalPanel.__init__(self) - self.value = value - self.value_max = value_max - _label = Label(label) - self.add(_label) - self.setCellWidth(_label, "100%") - minus_button = Button("-", self.onMinus) - self.box = WheelTextBox() - self.box.setVisibleLength(visible_len) - self.box.setText(unicode(value)) - self.box.addInputListener(self) - self.box.addMouseWheelListener(self) - plus_button = Button("+", self.onPlus) - self.add(minus_button) - self.add(self.box) - self.add(plus_button) - self.valueChangedListener = [] - - def addValueChangeListener(self, listener): - self.valueChangedListener.append(listener) - - def removeValueChangeListener(self, listener): - if listener in self.valueChangedListener: - self.valueChangedListener.remove(listener) - - def _callListeners(self): - for listener in self.valueChangedListener: - listener(self.value) - - def setValue(self, value): - """Change the value and fire valueChange listeners""" - self.value = value - self.box.setText(unicode(value)) - self._callListeners() - - def onMinus(self, sender, step=1): - self.value = max(0, self.value - step) - self.box.setText(unicode(self.value)) - self._callListeners() - - def onPlus(self, sender, step=1): - self.value += step - if self.value_max: - self.value = min(self.value, self.value_max) - self.box.setText(unicode(self.value)) - self._callListeners() - - def onInput(self, sender): - """Accept only valid integer && normalize print (no leading 0)""" - try: - self.value = int(self.box.getText()) if self.box.getText() else 0 - except ValueError: - pass - if self.value_max: - self.value = min(self.value, self.value_max) - self.box.setText(unicode(self.value)) - self._callListeners() - - def onMouseWheel(self, sender, velocity): - if velocity > 0: - self.onMinus(sender, 10) - else: - self.onPlus(sender, 10)
--- a/src/browser/sat_browser/editor_widget.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,380 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2011-2018 Jérôme Poisson <goffi@goffi.org> - -# This program 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. - -# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. - -from sat.core.log import getLogger -log = getLogger(__name__) -from sat_browser import strings - -from pyjamas.ui.HTML import HTML -from pyjamas.ui.SimplePanel import SimplePanel -from pyjamas.ui.TextArea import TextArea -from pyjamas.ui import KeyboardListener as keyb -from pyjamas.ui.FocusListener import FocusHandler -from pyjamas.ui.ClickListener import ClickHandler -from pyjamas.ui.MouseListener import MouseHandler -from pyjamas.Timer import Timer -from pyjamas import DOM - -import html_tools - - -class MessageBox(TextArea): - """A basic text area for entering messages""" - - def __init__(self, host): - TextArea.__init__(self) - self.host = host - self.size = (0, 0) - self.setStyleName('messageBox') - self.addKeyboardListener(self) - MouseHandler.__init__(self) - self.addMouseListener(self) - - def onBrowserEvent(self, event): - # XXX: woraroung a pyjamas bug: self.currentEvent is not set - # so the TextBox's cancelKey doens't work. This is a workaround - # FIXME: fix the bug upstream - self.currentEvent = event - TextArea.onBrowserEvent(self, event) - - def onKeyPress(self, sender, keycode, modifiers): - _txt = self.getText() - - def history_cb(text): - self.setText(text) - Timer(5, lambda timer: self.setCursorPos(len(text))) - - if keycode == keyb.KEY_ENTER: - if _txt: - self.host.selected_widget.onTextEntered(_txt) - self.host._updateInputHistory(_txt) # FIXME: why using a global variable ? - self.setText('') - sender.cancelKey() - elif keycode == keyb.KEY_UP: - self.host._updateInputHistory(_txt, -1, history_cb) - elif keycode == keyb.KEY_DOWN: - self.host._updateInputHistory(_txt, +1, history_cb) - else: - self._onComposing() - - def _onComposing(self): - """Callback when the user is composing a text.""" - self.host.selected_widget.chat_state_machine._onEvent("composing") - - def onMouseUp(self, sender, x, y): - size = (self.getOffsetWidth(), self.getOffsetHeight()) - if size != self.size: - self.size = size - self.host.resize() - - def onSelectedChange(self, selected): - self._selected_cache = selected - - -class BaseTextEditor(object): - """Basic definition of a text editor. The method edit gets a boolean parameter which - should be set to True when you want to edit the text and False to only display it.""" - - def __init__(self, content=None, strproc=None, modifiedCb=None, afterEditCb=None): - """ - Remark when inheriting this class: since the setContent method could be - overwritten by the child class, you should consider calling this __init__ - after all the parameters affecting this setContent method have been set. - @param content: dict with at least a 'text' key - @param strproc: method to be applied on strings to clean the content - @param modifiedCb: method to be called when the text has been modified. - This method can return: - - True: the modification will be saved and afterEditCb called; - - False: the modification won't be saved and afterEditCb called; - - None: the modification won't be saved and afterEditCb not called. - @param afterEditCb: method to be called when the edition is done - """ - if content is None: - content = {'text': ''} - assert 'text' in content - if strproc is None: - def strproc(text): - try: - return text.strip() - except (TypeError, AttributeError): - return text - self.strproc = strproc - self._modifiedCb = modifiedCb - self._afterEditCb = afterEditCb - self.initialized = False - self.edit_listeners = [] - self.setContent(content) - - def setContent(self, content=None): - """Set the editable content. - The displayed content, which is set from the child class, could differ. - - @param content (dict): content data, need at least a 'text' key - """ - if content is None: - content = {'text': ''} - elif not isinstance(content, dict): - content = {'text': content} - assert 'text' in content - self._original_content = {} - for key in content: - self._original_content[key] = self.strproc(content[key]) - - def getContent(self): - """Get the current edited or editable content. - @return: dict with at least a 'text' key - """ - raise NotImplementedError - - def setOriginalContent(self, content): - """Use this method with care! Content initialization should normally be - done with self.setContent. This method exists to let you trick the editor, - e.g. for self.modified to return True also when nothing has been modified. - @param content: dict - """ - self._original_content = content - - def getOriginalContent(self): - """ - @return (dict): the original content before modification (i.e. content given in __init__) - """ - return self._original_content - - def modified(self, content=None): - """Check if the content has been modified. - Remark: we don't use the direct comparison because we want to ignore empty elements - @content: content to be check against the original content or None to use the current content - @return: True if the content has been modified. - """ - if content is None: - content = self.getContent() - # the following method returns True if one non empty element exists in a but not in b - diff1 = lambda a, b: [a[key] for key in set(a.keys()).difference(b.keys()) if a[key]] != [] - # the following method returns True if the values for the common keys are not equals - diff2 = lambda a, b: [1 for key in set(a.keys()).intersection(b.keys()) if a[key] != b[key]] != [] - # finally the combination of both to return True if a difference is found - diff = lambda a, b: diff1(a, b) or diff1(b, a) or diff2(a, b) - - return diff(content, self._original_content) - - def edit(self, edit, abort=False): - """ - Remark: the editor must be visible before you call this method. - @param edit: set to True to edit the content or False to only display it - @param abort: set to True to cancel the edition and loose the changes. - If edit and abort are both True, self.abortEdition can be used to ask for a - confirmation. When edit is False and abort is True, abortion is actually done. - """ - if edit: - self.setFocus(True) - if abort: - content = self.getContent() - if not self.modified(content) or self.abortEdition(content): # e.g: ask for confirmation - self.edit(False, True) - return - else: - if not self.initialized: - return - content = self.getContent() - if abort: - self._afterEditCb(content) - return - if self._modifiedCb and self.modified(content): - result = self._modifiedCb(content) # e.g.: send a message or update something - if result is not None: - if self._afterEditCb: - self._afterEditCb(content) # e.g.: restore the display mode - if result is True: - self.setContent(content) - elif self._afterEditCb: - self._afterEditCb(content) - - self.initialized = True - - def setFocus(self, focus): - """ - @param focus: set to True to focus the editor - """ - raise NotImplementedError - - def abortEdition(self, content): - return True - - def addEditListener(self, listener): - """Add a method to be called whenever the text is edited. - @param listener: method taking two arguments: sender, keycode""" - self.edit_listeners.append(listener) - - -class SimpleTextEditor(BaseTextEditor, FocusHandler, keyb.KeyboardHandler, ClickHandler): - """Base class for manage a simple text editor.""" - - CONVERT_NEW_LINES = True - VALIDATE_WITH_SHIFT_ENTER = True - - def __init__(self, content=None, modifiedCb=None, afterEditCb=None, options=None): - """ - @param content - @param modifiedCb - @param afterEditCb - @param options (dict): can have the following value: - - no_xhtml: set to True to clean any xhtml content. - - enhance_display: if True, the display text will be enhanced with strings.addURLToText - - listen_keyboard: set to True to terminate the edition with <enter> or <escape>. - - listen_focus: set to True to terminate the edition when the focus is lost. - - listen_click: set to True to start the edition when you click on the widget. - """ - self.options = {'no_xhtml': False, - 'enhance_display': True, - 'listen_keyboard': True, - 'listen_focus': False, - 'listen_click': False - } - if options: - self.options.update(options) - if self.options['listen_focus']: - FocusHandler.__init__(self) - if self.options['listen_click']: - ClickHandler.__init__(self) - keyb.KeyboardHandler.__init__(self) - strproc = lambda text: html_tools.html_sanitize(html_tools.html_strip(text)) if self.options['no_xhtml'] else html_tools.html_strip(text) - BaseTextEditor.__init__(self, content, strproc, modifiedCb, afterEditCb) - self.textarea = self.display = None - - def setContent(self, content=None): - BaseTextEditor.setContent(self, content) - - def getContent(self): - raise NotImplementedError - - def edit(self, edit, abort=False): - BaseTextEditor.edit(self, edit) - if edit: - if self.options['listen_focus'] and self not in self.textarea._focusListeners: - self.textarea.addFocusListener(self) - if self.options['listen_click']: - self.display.clearClickListener() - if self not in self.textarea._keyboardListeners: - self.textarea.addKeyboardListener(self) - else: - self.setDisplayContent() - if self.options['listen_focus']: - try: - self.textarea.removeFocusListener(self) - except ValueError: - pass - if self.options['listen_click'] and self not in self.display._clickListeners: - self.display.addClickListener(self) - try: - self.textarea.removeKeyboardListener(self) - except ValueError: - pass - - def setDisplayContent(self): - text = self._original_content['text'] - if not self.options['no_xhtml']: - text = strings.addURLToImage(text) - if self.options['enhance_display']: - text = strings.addURLToText(text) - if self.CONVERT_NEW_LINES: - text = html_tools.convertNewLinesToXHTML(text) - text = strings.fixXHTMLLinks(text) - self.display.setHTML(text) - - def setFocus(self, focus): - raise NotImplementedError - - def onKeyDown(self, sender, keycode, modifiers): - for listener in self.edit_listeners: - listener(self.textarea, keycode, modifiers) # FIXME: edit_listeners must either be removed, or send an action instead of keycode/modifiers - if not self.options['listen_keyboard']: - return - if keycode == keyb.KEY_ENTER and (not self.VALIDATE_WITH_SHIFT_ENTER or modifiers & keyb.MODIFIER_SHIFT): - self.textarea.setFocus(False) - if not self.options['listen_focus']: - self.edit(False) - - def onLostFocus(self, sender): - """Finish the edition when focus is lost""" - if self.options['listen_focus']: - self.edit(False) - - def onClick(self, sender=None): - """Start the edition when the widget is clicked""" - if self.options['listen_click']: - self.edit(True) - - def onBrowserEvent(self, event): - if self.options['listen_focus']: - FocusHandler.onBrowserEvent(self, event) - if self.options['listen_click']: - ClickHandler.onBrowserEvent(self, event) - keyb.KeyboardHandler.onBrowserEvent(self, event) - - -class HTMLTextEditor(SimpleTextEditor, HTML, FocusHandler, keyb.KeyboardHandler): - """Manage a simple text editor with the HTML 5 "contenteditable" property.""" - - CONVERT_NEW_LINES = False # overwrite definition in SimpleTextEditor - - def __init__(self, content=None, modifiedCb=None, afterEditCb=None, options=None): - HTML.__init__(self) - SimpleTextEditor.__init__(self, content, modifiedCb, afterEditCb, options) - self.textarea = self.display = self - - def getContent(self): - text = DOM.getInnerHTML(self.getElement()) - return {'text': self.strproc(text) if text else ''} - - def edit(self, edit, abort=False): - if edit: - self.textarea.setHTML(self._original_content['text']) - self.getElement().setAttribute('contenteditable', 'true' if edit else 'false') - SimpleTextEditor.edit(self, edit, abort) - - def setFocus(self, focus): - if focus: - self.getElement().focus() - else: - self.getElement().blur() - - -class LightTextEditor(SimpleTextEditor, SimplePanel, FocusHandler, keyb.KeyboardHandler): - """Manage a simple text editor with a TextArea for editing, HTML for display.""" - - def __init__(self, content=None, modifiedCb=None, afterEditCb=None, options=None): - SimplePanel.__init__(self) - SimpleTextEditor.__init__(self, content, modifiedCb, afterEditCb, options) - self.textarea = TextArea() - self.display = HTML() - - def getContent(self): - text = self.textarea.getText() - return {'text': self.strproc(text) if text else ''} - - def edit(self, edit, abort=False): - if edit: - self.textarea.setText(self._original_content['text']) - self.setWidget(self.textarea if edit else self.display) - SimpleTextEditor.edit(self, edit, abort) - - def setFocus(self, focus): - if focus and self.isAttached(): - self.textarea.setCursorPos(len(self.textarea.getText())) - self.textarea.setFocus(focus)
--- a/src/browser/sat_browser/file_tools.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,163 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2011-2018 Jérôme Poisson <goffi@goffi.org> - -# This program 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. - -# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. - -from sat.core.log import getLogger -log = getLogger(__name__) -from constants import Const as C -from sat.core.i18n import _, D_ -from pyjamas.ui.FileUpload import FileUpload -from pyjamas.ui.FormPanel import FormPanel -from pyjamas import Window -from pyjamas import DOM -from pyjamas.ui.VerticalPanel import VerticalPanel -from pyjamas.ui.HTML import HTML -from pyjamas.ui.HorizontalPanel import HorizontalPanel -from pyjamas.ui.Button import Button -from pyjamas.ui.Label import Label - - -class FilterFileUpload(FileUpload): - - def __init__(self, name, max_size, types=None): - """ - @param name: the input element name and id - @param max_size: maximum file size in MB - @param types: allowed types as a list of couples (x, y, z): - - x: MIME content type e.g. "audio/ogg" - - y: file extension e.g. "*.ogg" - - z: description for the user e.g. "Ogg Vorbis Audio" - If types is None, all file format are accepted - """ - FileUpload.__init__(self) - self.setName(name) - while DOM.getElementById(name): - name = "%s_" % name - self.setID(name) - self._id = name - self.max_size = max_size - self.types = types - - def getFileInfo(self): - from __pyjamas__ import JS - JS("var file = top.document.getElementById(this._id).files[0]; return [file.size, file.type]") - - def check(self): - if self.getFilename() == "": - return False - (size, filetype) = self.getFileInfo() - if self.types and filetype not in [x for (x, y, z) in self.types]: - types = [] - for type_ in ["- %s (%s)" % (z, y) for (x, y, z) in self.types]: - if type_ not in types: - types.append(type_) - Window.alert('This file type is not accepted.\nAccepted file types are:\n\n%s' % "\n".join(types)) - return False - if size > self.max_size * pow(2, 20): - Window.alert('This file is too big!\nMaximum file size: %d MB.' % self.max_size) - return False - return True - - -class FileUploadPanel(FormPanel): - - def __init__(self, action_url, input_id, max_size, texts=None, close_cb=None): - """Build a form panel to upload a file. - @param action_url: the form action URL - @param input_id: the input element name and id - @param max_size: maximum file size in MB - @param texts: a dict to ovewrite the default textual values - @param close_cb: the close button callback method - """ - FormPanel.__init__(self) - self.texts = {'ok_button': D_('Upload file'), - 'cancel_button': D_('Cancel'), - 'body': D_('Please select a file.'), - 'submitting': D_('<strong>Submitting, please wait...</strong>'), - 'errback': D_("Your file has been rejected..."), - 'body_errback': D_('Please select another file.'), - 'callback': D_("Your file has been accepted!")} - if isinstance(texts, dict): - self.texts.update(texts) - self.close_cb = close_cb - self.setEncoding(FormPanel.ENCODING_MULTIPART) - self.setMethod(FormPanel.METHOD_POST) - self.setAction(action_url) - self.vPanel = VerticalPanel() - self.message = HTML(self.texts['body']) - self.vPanel.add(self.message) - - hPanel = HorizontalPanel() - hPanel.setSpacing(5) - hPanel.setStyleName('marginAuto') - self.file_upload = FilterFileUpload(input_id, max_size) - self.vPanel.add(self.file_upload) - - self.upload_btn = Button(self.texts['ok_button'], getattr(self, "onSubmitBtnClick")) - hPanel.add(self.upload_btn) - hPanel.add(Button(self.texts['cancel_button'], getattr(self, "onCloseBtnClick"))) - - self.status = Label() - hPanel.add(self.status) - - self.vPanel.add(hPanel) - - self.add(self.vPanel) - self.addFormHandler(self) - - def setCloseCb(self, close_cb): - self.close_cb = close_cb - - def onCloseBtnClick(self): - if self.close_cb: - self.close_cb() - else: - log.warning("no close method defined") - - def onSubmitBtnClick(self): - if not self.file_upload.check(): - return - self.message.setHTML(self.texts['submitting']) - self.upload_btn.setEnabled(False) - self.submit() - - def onSubmit(self, event): - pass - - def onSubmitComplete(self, event): - result = event.getResults() - if result == C.UPLOAD_KO: - Window.alert(self.texts['errback']) - self.message.setHTML(self.texts['body_errback']) - self.upload_btn.setEnabled(True) - elif result == C.UPLOAD_OK: - Window.alert(self.texts['callback']) - self.close_cb() - else: - Window.alert(_('Submit error: %s' % result)) - self.upload_btn.setEnabled(True) - - -class AvatarUpload(FileUploadPanel): - def __init__(self): - texts = {'ok_button': 'Upload avatar', - 'body': 'Please select an image to show as your avatar...<br>Your picture must be a square and will be resized to 64x64 pixels if necessary.', - 'errback': "Can't open image... did you actually submit an image?", - 'body_errback': 'Please select another image file.', - 'callback': "Your new profile picture has been set!"} - FileUploadPanel.__init__(self, 'upload_avatar', 'avatar_path', 2, texts)
--- a/src/browser/sat_browser/game_radiocol.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,347 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2011-2018 Jérôme Poisson <goffi@goffi.org> - -# This program 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. - -# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. - -import pyjd # this is dummy in pyjs -from sat.core.log import getLogger -log = getLogger(__name__) - -from sat.core.i18n import _, D_ -from sat_frontends.tools import host_listener -from constants import Const as C - -from pyjamas.ui.VerticalPanel import VerticalPanel -from pyjamas.ui.HorizontalPanel import HorizontalPanel -from pyjamas.ui.FlexTable import FlexTable -from pyjamas.ui.FormPanel import FormPanel -from pyjamas.ui.Label import Label -from pyjamas.ui.Button import Button -from pyjamas.ui.ClickListener import ClickHandler -from pyjamas.ui.Hidden import Hidden -from pyjamas.ui.CaptionPanel import CaptionPanel -from pyjamas.media.Audio import Audio -from pyjamas import Window -from pyjamas.Timer import Timer - -import html_tools -import file_tools -import dialog - - -unicode = str # XXX: pyjama doesn't manage unicode - - -class MetadataPanel(FlexTable): - - def __init__(self): - FlexTable.__init__(self) - title_lbl = Label("title:") - title_lbl.setStyleName('radiocol_metadata_lbl') - artist_lbl = Label("artist:") - artist_lbl.setStyleName('radiocol_metadata_lbl') - album_lbl = Label("album:") - album_lbl.setStyleName('radiocol_metadata_lbl') - self.title = Label("") - self.title.setStyleName('radiocol_metadata') - self.artist = Label("") - self.artist.setStyleName('radiocol_metadata') - self.album = Label("") - self.album.setStyleName('radiocol_metadata') - self.setWidget(0, 0, title_lbl) - self.setWidget(1, 0, artist_lbl) - self.setWidget(2, 0, album_lbl) - self.setWidget(0, 1, self.title) - self.setWidget(1, 1, self.artist) - self.setWidget(2, 1, self.album) - self.setStyleName("radiocol_metadata_pnl") - - def setTitle(self, title): - self.title.setText(title) - - def setArtist(self, artist): - self.artist.setText(artist) - - def setAlbum(self, album): - self.album.setText(album) - - -class ControlPanel(FormPanel): - """Panel used to show controls to add a song, or vote for the current one""" - - def __init__(self, parent): - FormPanel.__init__(self) - self.setEncoding(FormPanel.ENCODING_MULTIPART) - self.setMethod(FormPanel.METHOD_POST) - self.setAction("upload_radiocol") - self.timer_on = False - self._parent = parent - vPanel = VerticalPanel() - - types = [('audio/ogg', '*.ogg', 'Ogg Vorbis Audio'), - ('video/ogg', '*.ogv', 'Ogg Vorbis Video'), - ('application/ogg', '*.ogx', 'Ogg Vorbis Multiplex'), - ('audio/mpeg', '*.mp3', 'MPEG-Layer 3'), - ('audio/mp3', '*.mp3', 'MPEG-Layer 3'), - ] - self.file_upload = file_tools.FilterFileUpload("song", 10, types) - vPanel.add(self.file_upload) - - hPanel = HorizontalPanel() - self.upload_btn = Button("Upload song", getattr(self, "onBtnClick")) - hPanel.add(self.upload_btn) - self.status = Label() - self.updateStatus() - hPanel.add(self.status) - #We need to know the filename and the referee - self.filename_field = Hidden('filename', '') - hPanel.add(self.filename_field) - referee_field = Hidden('referee', self._parent.referee) - hPanel.add(self.filename_field) - hPanel.add(referee_field) - vPanel.add(hPanel) - - self.add(vPanel) - self.addFormHandler(self) - - def updateStatus(self): - if self.timer_on: - return - # TODO: the status should be different if a song is being played or not - queue = self._parent.getQueueSize() - queue_data = self._parent.queue_data - if queue < queue_data[0]: - left = queue_data[0] - queue - self.status.setText("[we need %d more song%s]" % (left, "s" if left > 1 else "")) - elif queue < queue_data[1]: - left = queue_data[1] - queue - self.status.setText("[%d available spot%s]" % (left, "s" if left > 1 else "")) - elif queue >= queue_data[1]: - self.status.setText("[The queue is currently full]") - self.status.setStyleName('radiocol_status') - - def onBtnClick(self): - if self.file_upload.check(): - self.status.setText('[Submitting, please wait...]') - self.filename_field.setValue(self.file_upload.getFilename()) - if self.file_upload.getFilename().lower().endswith('.mp3'): - self._parent._parent.host.showWarning('STATUS', 'For a better support, it is recommended to submit Ogg Vorbis file instead of MP3. You can convert your files easily, ask for help if needed!', 5000) - self.submit() - self.file_upload.setFilename("") - - def onSubmit(self, event): - pass - - def blockUpload(self): - self.file_upload.setVisible(False) - self.upload_btn.setEnabled(False) - - def unblockUpload(self): - self.file_upload.setVisible(True) - self.upload_btn.setEnabled(True) - - def setTemporaryStatus(self, text, style): - self.status.setText(text) - self.status.setStyleName('radiocol_upload_status_%s' % style) - self.timer_on = True - - def cb(timer): - self.timer_on = False - self.updateStatus() - - Timer(5000, cb) - - def onSubmitComplete(self, event): - result = event.getResults() - if result == C.UPLOAD_OK: - # the song can still be rejected (not readable, full queue...) - self.setTemporaryStatus('[Your song has been submitted to the radio]', "ok") - elif result == C.UPLOAD_KO: - self.setTemporaryStatus('[Something went wrong during your song upload]', "ko") - self._parent.radiocolSongRejectedHandler(_("The uploaded file has been rejected, only Ogg Vorbis and MP3 songs are accepted.")) - # TODO: would be great to re-use the original Exception class and message - # but it is lost in the middle of the traceback and encapsulated within - # a DBusException instance --> extract the data from the traceback? - else: - Window.alert(_('Submit error: %s' % result)) - self.status.setText('') - - -class Player(Audio): - - def __init__(self, player_id, metadata_panel): - Audio.__init__(self) - self._id = player_id - self.metadata = metadata_panel - self.timestamp = "" - self.title = "" - self.artist = "" - self.album = "" - self.filename = None - self.played = False # True when the song is playing/has played, becomes False on preload - self.setAutobuffer(True) - self.setAutoplay(False) - self.setVisible(False) - - def preload(self, timestamp, filename, title, artist, album): - """preload the song but doesn't play it""" - self.timestamp = timestamp - self.filename = filename - self.title = title - self.artist = artist - self.album = album - self.played = False - self.setSrc(u"radiocol/%s" % html_tools.html_sanitize(filename)) - log.debug(u"preloading %s in %s" % (title, self._id)) - - def play(self, play=True): - """Play or pause the song - @param play: set to True to play or to False to pause - """ - if play: - self.played = True - self.metadata.setTitle(self.title) - self.metadata.setArtist(self.artist) - self.metadata.setAlbum(self.album) - Audio.play(self) - else: - self.pause() - - -class RadioColPanel(HorizontalPanel, ClickHandler): - - def __init__(self, parent, referee, players, queue_data): - """ - @param parent - @param referee - @param players - @param queue_data: list of integers (queue to start, queue limit) - """ - # We need to set it here and not in the CSS :( - HorizontalPanel.__init__(self, Height="90px") - ClickHandler.__init__(self) - self._parent = parent - self.referee = referee - self.queue_data = queue_data - self.setStyleName("radiocolPanel") - - # Now we set up the layout - self.metadata_panel = MetadataPanel() - self.add(CaptionPanel("Now playing", self.metadata_panel)) - self.playlist_panel = VerticalPanel() - self.add(CaptionPanel("Songs queue", self.playlist_panel)) - self.control_panel = ControlPanel(self) - self.add(CaptionPanel("Controls", self.control_panel)) - - self.next_songs = [] - self.players = [Player("player_%d" % i, self.metadata_panel) for i in xrange(queue_data[1] + 1)] - self.current_player = None - for player in self.players: - self.add(player) - self.addClickListener(self) - - help_msg = """Accepted file formats: Ogg Vorbis (recommended), MP3.<br /> - Please do not submit files that are protected by copyright.<br /> - Click <a style="color: red;">here</a> if you need some support :)""" - link_cb = lambda: self._parent.host.bridge.joinMUC(self._parent.host.default_muc, self._parent.nick, profile=C.PROF_KEY_NONE, callback=lambda dummy: None, errback=self._parent.host.onJoinMUCFailure) - # FIXME: printInfo disabled after refactoring - # self._parent.printInfo(help_msg, type_='link', link_cb=link_cb) - - def pushNextSong(self, title): - """Add a song to the left panel's next songs queue""" - next_song = Label(title) - next_song.setStyleName("radiocol_next_song") - self.next_songs.append(next_song) - self.playlist_panel.append(next_song) - self.control_panel.updateStatus() - - def popNextSong(self): - """Remove the first song of next songs list - should be called when the song is played""" - #FIXME: should check that the song we remove is the one we play - next_song = self.next_songs.pop(0) - self.playlist_panel.remove(next_song) - self.control_panel.updateStatus() - - def getQueueSize(self): - return len(self.playlist_panel.getChildren()) - - def radiocolCheckPreload(self, timestamp): - for player in self.players: - if player.timestamp == timestamp: - return False - return True - - def radiocolPreloadHandler(self, timestamp, filename, title, artist, album, sender): - if not self.radiocolCheckPreload(timestamp): - return # song already preloaded - preloaded = False - for player in self.players: - if not player.filename or \ - (player.played and player != self.current_player): - #if player has no file loaded, or it has already played its song - #we use it to preload the next one - player.preload(timestamp, filename, title, artist, album) - preloaded = True - break - if not preloaded: - log.warning("Can't preload song, we are getting too many songs to preload, we shouldn't have more than %d at once" % self.queue_data[1]) - else: - self.pushNextSong(title) - # FIXME: printInfo disabled after refactoring - # self._parent.printInfo(_('%(user)s uploaded %(artist)s - %(title)s') % {'user': sender, 'artist': artist, 'title': title}) - - def radiocolPlayHandler(self, filename): - found = False - for player in self.players: - if not found and player.filename == filename: - player.play() - self.popNextSong() - self.current_player = player - found = True - else: - player.play(False) # in case the previous player was not sync - if not found: - log.error("Song not found in queue, can't play it. This should not happen") - - def radiocolNoUploadHandler(self): - self.control_panel.blockUpload() - - def radiocolUploadOkHandler(self): - self.control_panel.unblockUpload() - - def radiocolSongRejectedHandler(self, reason): - Window.alert("Song rejected: %s" % reason) - - -## Menu - -def hostReady(host): - def onCollectiveRadio(self): - def callback(room_jid, contacts): - contacts = [unicode(contact) for contact in contacts] - room_jid_s = unicode(room_jid) if room_jid else '' - host.bridge.launchRadioCollective(contacts, room_jid_s, profile=C.PROF_KEY_NONE, callback=lambda dummy: None, errback=host.onJoinMUCFailure) - dialog.RoomAndContactsChooser(host, callback, ok_button="Choose", title="Collective Radio", visible=(False, True)) - - - def gotMenus(): - host.menus.addMenu(C.MENU_GLOBAL, (D_(u"Groups"), D_(u"Collective radio")), callback=onCollectiveRadio) - - host.addListener('gotMenus', gotMenus) - -host_listener.addListener(hostReady)
--- a/src/browser/sat_browser/game_tarot.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,410 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2011-2018 Jérôme Poisson <goffi@goffi.org> - -# This program 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. - -# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. - -import pyjd # this is dummy in pyjs -from sat.core.log import getLogger -log = getLogger(__name__) - -from sat.core.i18n import _, D_ -from sat_frontends.tools.games import TarotCard -from sat_frontends.tools import host_listener - -from pyjamas.ui.AbsolutePanel import AbsolutePanel -from pyjamas.ui.DockPanel import DockPanel -from pyjamas.ui.SimplePanel import SimplePanel -from pyjamas.ui.Image import Image -from pyjamas.ui.Label import Label -from pyjamas.ui.ClickListener import ClickHandler -from pyjamas.ui.MouseListener import MouseHandler -from pyjamas.ui import HasAlignment -from pyjamas import Window -from pyjamas import DOM -from constants import Const as C - -import dialog -import xmlui - - -CARD_WIDTH = 74 -CARD_HEIGHT = 136 -CARD_DELTA_Y = 30 -MIN_WIDTH = 950 # Minimum size of the panel -MIN_HEIGHT = 500 - - -unicode = str # XXX: pyjama doesn't manage unicode - - -class CardWidget(TarotCard, Image, MouseHandler): - """This class is used to represent a card, graphically and logically""" - - def __init__(self, parent, file_): - """@param file: path of the PNG file""" - self._parent = parent - Image.__init__(self, file_) - root_name = file_[file_.rfind("/") + 1:-4] - suit, value = root_name.split('_') - TarotCard.__init__(self, (suit, value)) - MouseHandler.__init__(self) - self.addMouseListener(self) - - def onMouseEnter(self, sender): - if self._parent.state == "ecart" or self._parent.state == "play": - DOM.setStyleAttribute(self.getElement(), "top", "0px") - - def onMouseLeave(self, sender): - if not self in self._parent.hand: - return - if not self in list(self._parent.selected): # FIXME: Workaround pyjs bug, must report it - DOM.setStyleAttribute(self.getElement(), "top", "%dpx" % CARD_DELTA_Y) - - def onMouseUp(self, sender, x, y): - if self._parent.state == "ecart": - if self not in list(self._parent.selected): - self._parent.addToSelection(self) - else: - self._parent.removeFromSelection(self) - elif self._parent.state == "play": - self._parent.playCard(self) - - -class TarotPanel(DockPanel, ClickHandler): - - def __init__(self, parent, referee, players): - DockPanel.__init__(self) - ClickHandler.__init__(self) - self._parent = parent - self._autoplay = None # XXX: use 0 to activate fake play, None else - self.referee = referee - self.players = players - self.player_nick = parent.nick - self.bottom_nick = self.player_nick - idx = self.players.index(self.player_nick) - idx = (idx + 1) % len(self.players) - self.right_nick = self.players[idx] - idx = (idx + 1) % len(self.players) - self.top_nick = self.players[idx] - idx = (idx + 1) % len(self.players) - self.left_nick = self.players[idx] - self.bottom_nick = self.player_nick - self.selected = set() # Card choosed by the player (e.g. during ecart) - self.hand_size = 13 # number of cards in a hand - self.hand = [] - self.to_show = [] - self.state = None - self.setSize("%dpx" % MIN_WIDTH, "%dpx" % MIN_HEIGHT) - self.setStyleName("cardPanel") - - # Now we set up the layout - _label = Label(self.top_nick) - _label.setStyleName('cardGamePlayerNick') - self.add(_label, DockPanel.NORTH) - self.setCellWidth(_label, '100%') - self.setCellHorizontalAlignment(_label, HasAlignment.ALIGN_CENTER) - - self.hand_panel = AbsolutePanel() - self.add(self.hand_panel, DockPanel.SOUTH) - self.setCellWidth(self.hand_panel, '100%') - self.setCellHorizontalAlignment(self.hand_panel, HasAlignment.ALIGN_CENTER) - - _label = Label(self.left_nick) - _label.setStyleName('cardGamePlayerNick') - self.add(_label, DockPanel.WEST) - self.setCellHeight(_label, '100%') - self.setCellVerticalAlignment(_label, HasAlignment.ALIGN_MIDDLE) - - _label = Label(self.right_nick) - _label.setStyleName('cardGamePlayerNick') - self.add(_label, DockPanel.EAST) - self.setCellHeight(_label, '100%') - self.setCellHorizontalAlignment(_label, HasAlignment.ALIGN_RIGHT) - self.setCellVerticalAlignment(_label, HasAlignment.ALIGN_MIDDLE) - - self.center_panel = DockPanel() - self.inner_left = SimplePanel() - self.inner_left.setSize("%dpx" % CARD_WIDTH, "%dpx" % CARD_HEIGHT) - self.center_panel.add(self.inner_left, DockPanel.WEST) - self.center_panel.setCellHeight(self.inner_left, '100%') - self.center_panel.setCellHorizontalAlignment(self.inner_left, HasAlignment.ALIGN_RIGHT) - self.center_panel.setCellVerticalAlignment(self.inner_left, HasAlignment.ALIGN_MIDDLE) - - self.inner_right = SimplePanel() - self.inner_right.setSize("%dpx" % CARD_WIDTH, "%dpx" % CARD_HEIGHT) - self.center_panel.add(self.inner_right, DockPanel.EAST) - self.center_panel.setCellHeight(self.inner_right, '100%') - self.center_panel.setCellVerticalAlignment(self.inner_right, HasAlignment.ALIGN_MIDDLE) - - self.inner_top = SimplePanel() - self.inner_top.setSize("%dpx" % CARD_WIDTH, "%dpx" % CARD_HEIGHT) - self.center_panel.add(self.inner_top, DockPanel.NORTH) - self.center_panel.setCellHorizontalAlignment(self.inner_top, HasAlignment.ALIGN_CENTER) - self.center_panel.setCellVerticalAlignment(self.inner_top, HasAlignment.ALIGN_BOTTOM) - - self.inner_bottom = SimplePanel() - self.inner_bottom.setSize("%dpx" % CARD_WIDTH, "%dpx" % CARD_HEIGHT) - self.center_panel.add(self.inner_bottom, DockPanel.SOUTH) - self.center_panel.setCellHorizontalAlignment(self.inner_bottom, HasAlignment.ALIGN_CENTER) - self.center_panel.setCellVerticalAlignment(self.inner_bottom, HasAlignment.ALIGN_TOP) - - self.inner_center = SimplePanel() - self.center_panel.add(self.inner_center, DockPanel.CENTER) - self.center_panel.setCellHorizontalAlignment(self.inner_center, HasAlignment.ALIGN_CENTER) - self.center_panel.setCellVerticalAlignment(self.inner_center, HasAlignment.ALIGN_MIDDLE) - - self.add(self.center_panel, DockPanel.CENTER) - self.setCellWidth(self.center_panel, '100%') - self.setCellHeight(self.center_panel, '100%') - self.setCellVerticalAlignment(self.center_panel, HasAlignment.ALIGN_MIDDLE) - self.setCellHorizontalAlignment(self.center_panel, HasAlignment.ALIGN_CENTER) - - self.loadCards() - self.mouse_over_card = None # contain the card to highlight - self.visible_size = CARD_WIDTH / 2 # number of pixels visible for cards - self.addClickListener(self) - - def loadCards(self): - """Load all the cards in memory""" - def _getTarotCardsPathsCb(paths): - log.debug("_getTarotCardsPathsCb") - for file_ in paths: - log.debug(u"path: %s" % file_) - card = CardWidget(self, file_) - log.debug(u"card: %s" % card) - self.cards[(card.suit, card.value)] = card - self.deck.append(card) - self._parent.host.bridge.call('tarotGameReady', None, self.player_nick, self.referee) - self.cards = {} - self.deck = [] - self.cards["atout"] = {} # As Tarot is a french game, it's more handy & logical to keep french names - self.cards["pique"] = {} # spade - self.cards["coeur"] = {} # heart - self.cards["carreau"] = {} # diamond - self.cards["trefle"] = {} # club - self._parent.host.bridge.call('getTarotCardsPaths', _getTarotCardsPathsCb) - - def onClick(self, sender): - if self.state == "chien": - self.to_show = [] - self.state = "wait" - self.updateToShow() - elif self.state == "wait_for_ecart": - self.state = "ecart" - self.hand.extend(self.to_show) - self.hand.sort() - self.to_show = [] - self.updateToShow() - self.updateHand() - - def tarotGameNewHandler(self, hand): - """Start a new game, with given hand""" - if hand is []: # reset the display after the scores have been showed - self.selected.clear() - del self.hand[:] - del self.to_show[:] - self.state = None - #empty hand - self.updateHand() - #nothing on the table - self.updateToShow() - for pos in ['top', 'left', 'bottom', 'right']: - getattr(self, "inner_%s" % pos).setWidget(None) - self._parent.host.bridge.call('tarotGameReady', None, self.player_nick, self.referee) - return - for suit, value in hand: - self.hand.append(self.cards[(suit, value)]) - self.hand.sort() - self.state = "init" - self.updateHand() - - def updateHand(self): - """Show the cards in the hand in the hand_panel (SOUTH panel)""" - self.hand_panel.clear() - self.hand_panel.setSize("%dpx" % (self.visible_size * (len(self.hand) + 1)), "%dpx" % (CARD_HEIGHT + CARD_DELTA_Y + 10)) - x_pos = 0 - y_pos = CARD_DELTA_Y - for card in self.hand: - self.hand_panel.add(card, x_pos, y_pos) - x_pos += self.visible_size - - def updateToShow(self): - """Show cards in the center panel""" - if not self.to_show: - _widget = self.inner_center.getWidget() - if _widget: - self.inner_center.remove(_widget) - return - panel = AbsolutePanel() - panel.setSize("%dpx" % ((CARD_WIDTH + 5) * len(self.to_show) - 5), "%dpx" % (CARD_HEIGHT)) - x_pos = 0 - y_pos = 0 - for card in self.to_show: - panel.add(card, x_pos, y_pos) - x_pos += CARD_WIDTH + 5 - self.inner_center.setWidget(panel) - - def _ecartConfirm(self, confirm): - if not confirm: - return - ecart = [] - for card in self.selected: - ecart.append((card.suit, card.value)) - self.hand.remove(card) - self.selected.clear() - self._parent.host.bridge.call('tarotGamePlayCards', None, self.player_nick, self.referee, ecart) - self.state = "wait" - self.updateHand() - - def addToSelection(self, card): - self.selected.add(card) - if len(self.selected) == 6: - dialog.ConfirmDialog(self._ecartConfirm, "Put these cards into chien ?").show() - - def tarotGameInvalidCardsHandler(self, phase, played_cards, invalid_cards): - """Invalid cards have been played - @param phase: phase of the game - @param played_cards: all the cards played - @param invalid_cards: cards which are invalid""" - - if phase == "play": - self.state = "play" - elif phase == "ecart": - self.state = "ecart" - else: - log.error("INTERNAL ERROR: unmanaged game phase") # FIXME: raise an exception here - - for suit, value in played_cards: - self.hand.append(self.cards[(suit, value)]) - - self.hand.sort() - self.updateHand() - if self._autoplay == None: # No dialog if there is autoplay - Window.alert('Cards played are invalid !') - self.__fakePlay() - - def removeFromSelection(self, card): - self.selected.remove(card) - if len(self.selected) == 6: - dialog.ConfirmDialog(self._ecartConfirm, "Put these cards into chien ?").show() - - def tarotGameChooseContratHandler(self, xml_data): - """Called when the player has to select his contrat - @param xml_data: SàT xml representation of the form""" - body = xmlui.create(self._parent.host, xml_data, flags=['NO_CANCEL']) - _dialog = dialog.GenericDialog(_('Please choose your contrat'), body, options=['NO_CLOSE']) - body.setCloseCb(_dialog.close) - _dialog.show() - - def tarotGameShowCardsHandler(self, game_stage, cards, data): - """Display cards in the middle of the game (to show for e.g. chien ou poignée)""" - self.to_show = [] - for suit, value in cards: - self.to_show.append(self.cards[(suit, value)]) - self.updateToShow() - if game_stage == "chien" and data['attaquant'] == self.player_nick: - self.state = "wait_for_ecart" - else: - self.state = "chien" - - def getPlayerLocation(self, nick): - """return player location (top,bottom,left or right)""" - for location in ['top', 'left', 'bottom', 'right']: - if getattr(self, '%s_nick' % location) == nick: - return location - log.error("This line should not be reached") - - def tarotGameCardsPlayedHandler(self, player, cards): - """A card has been played by player""" - if not len(cards): - log.warning("cards should not be empty") - return - if len(cards) > 1: - log.error("can't manage several cards played") - if self.to_show: - self.to_show = [] - self.updateToShow() - suit, value = cards[0] - player_pos = self.getPlayerLocation(player) - player_panel = getattr(self, "inner_%s" % player_pos) - - if player_panel.getWidget() != None: - #We have already cards on the table, we remove them - for pos in ['top', 'left', 'bottom', 'right']: - getattr(self, "inner_%s" % pos).setWidget(None) - - card = self.cards[(suit, value)] - DOM.setElemAttribute(card.getElement(), "style", "") - player_panel.setWidget(card) - - def tarotGameYourTurnHandler(self): - """Called when we have to play :)""" - if self.state == "chien": - self.to_show = [] - self.updateToShow() - self.state = "play" - self.__fakePlay() - - def __fakePlay(self): - """Convenience method for stupid autoplay - /!\ don't forgot to comment any interactive dialog for invalid card""" - if self._autoplay == None: - return - if self._autoplay >= len(self.hand): - self._autoplay = 0 - card = self.hand[self._autoplay] - self._parent.host.bridge.call('tarotGamePlayCards', None, self.player_nick, self.referee, [(card.suit, card.value)]) - del self.hand[self._autoplay] - self.state = "wait" - self._autoplay += 1 - - def playCard(self, card): - self.hand.remove(card) - self._parent.host.bridge.call('tarotGamePlayCards', None, self.player_nick, self.referee, [(card.suit, card.value)]) - self.state = "wait" - self.updateHand() - - def tarotGameScoreHandler(self, xml_data, winners, loosers): - """Show score at the end of a round""" - if not winners and not loosers: - title = "Draw game" - else: - if self.player_nick in winners: - title = "You <b>win</b> !" - else: - title = "You <b>loose</b> :(" - body = xmlui.create(self._parent.host, xml_data, title=title, flags=['NO_CANCEL']) - _dialog = dialog.GenericDialog(title, body, options=['NO_CLOSE']) - body.setCloseCb(_dialog.close) - _dialog.show() - - -## Menu - -def hostReady(host): - def onTarotGame(): - def onPlayersSelected(room_jid, other_players): - other_players = [unicode(contact) for contact in other_players] - room_jid_s = unicode(room_jid) if room_jid else '' - host.bridge.launchTarotGame(other_players, room_jid_s, profile=C.PROF_KEY_NONE, callback=lambda dummy: None, errback=host.onJoinMUCFailure) - dialog.RoomAndContactsChooser(host, onPlayersSelected, 3, title="Tarot", title_invite=_(u"Please select 3 other players"), visible=(False, True)) - - def gotMenus(): - host.menus.addMenu(C.MENU_GLOBAL, (D_(u"Groups"), D_(u"Tarot")), callback=onTarotGame) - host.addListener('gotMenus', gotMenus) - -host_listener.addListener(hostReady)
--- a/src/browser/sat_browser/html_tools.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,81 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2011-2018 Jérôme Poisson <goffi@goffi.org> - -# This program 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. - -# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. - -from sat_frontends.tools import xmltools - -import nativedom -from __pyjamas__ import JS - -dom = nativedom.NativeDOM() - - -def html_sanitize(html): - """Naive sanitization of HTML""" - return html.replace('<', '<').replace('>', '>') - - -def html_strip(html): - """Strip leading/trailing white spaces, HTML line breaks and sequences.""" - JS("""return html.replace(/(^(<br\/?>| |\s)+)|((<br\/?>| |\s)+$)/g, "");""") - -def inlineRoot(xhtml): - """ make root element inline """ - doc = dom.parseString(xhtml) - return xmltools.inlineRoot(doc) - - -def convertNewLinesToXHTML(text): - """Replace all the \n with <br/>""" - return text.replace('\n', '<br/>') - - -def XHTML2Text(xhtml): - """Helper method to apply both html_sanitize and convertNewLinesToXHTML""" - return convertNewLinesToXHTML(html_sanitize(xhtml)) - - -def buildPresenceStyle(presence, base_style=None): - """Return the CSS classname to be used for displaying the given presence information. - - @param presence (unicode): presence is a value in ('', 'chat', 'away', 'dnd', 'xa') - @param base_style (unicode): base classname - @return: unicode - """ - if not base_style: - base_style = "contactLabel" - return '%s-%s' % (base_style, presence or 'connected') - - -def setPresenceStyle(widget, presence, base_style=None): - """ - Set the CSS style of a contact's element according to its presence. - - @param widget (Widget): the UI element of the contact - @param presence (unicode): a value in ("", "chat", "away", "dnd", "xa"). - @param base_style (unicode): the base name of the style to apply - """ - if not hasattr(widget, 'presence_style'): - widget.presence_style = None - style = buildPresenceStyle(presence, base_style) - if style == widget.presence_style: - return - if widget.presence_style is not None: - widget.removeStyleName(widget.presence_style) - widget.addStyleName(style) - widget.presence_style = style
--- a/src/browser/sat_browser/json.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,298 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2011-2018 Jérôme Poisson <goffi@goffi.org> - -# This program 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. - -# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. - - -### logging configuration ### -from sat.core.log import getLogger -log = getLogger(__name__) -### - -from pyjamas.Timer import Timer -from pyjamas import Window -from pyjamas import JSONService -import time -from sat_browser import main_panel - -from sat_browser.constants import Const as C -import random - - -class LiberviaMethodProxy(object): - """This class manage calling for one method""" - - def __init__(self, parent, method): - self._parent = parent - self._method = method - - def call(self, *args, **kwargs): - """Method called when self._method attribue is used in JSON_PROXY_PARENT - - This method manage callback/errback in kwargs, and profile(_key) removing - @param *args: positional arguments of self._method - @param **kwargs: keyword arguments of self._method - """ - callback=kwargs.pop('callback', None) - errback=kwargs.pop('errback', None) - - # as profile is linked to browser session and managed server side, we remove them - profile_removed = False - try: - kwargs['profile'] # FIXME: workaround for pyjamas bug: KeyError is not raised with del - del kwargs['profile'] - profile_removed = True - except KeyError: - pass - - try: - kwargs['profile_key'] # FIXME: workaround for pyjamas bug: KeyError is not raised iwith del - del kwargs['profile_key'] - profile_removed = True - except KeyError: - pass - - if not profile_removed and args: - # if profile was not in kwargs, there is most probably one in args - args = list(args) - assert isinstance(args[-1], basestring) # Detect when we want to remove a callback (or something else) instead of the profile - del args[-1] - - if kwargs: - # kwargs should be empty here, we don't manage keyword arguments on bridge calls - log.error(u"kwargs is not empty after treatment on method call: kwargs={}".format(kwargs)) - - id_ = self._parent.callMethod(self._method, args) - - # callback or errback are managed in parent LiberviaJsonProxy with call id - if callback is not None: - self._parent.cb[id_] = callback - if errback is not None: - self._parent.eb[id_] = errback - - -class LiberviaJsonProxy(JSONService.JSONService): - - def __init__(self, url, methods): - self._serviceURL = url - self.methods = methods - JSONService.JSONService.__init__(self, url, self) - self.cb = {} - self.eb = {} - self._registerMethods(methods) - - def _registerMethods(self, methods): - if methods: - for method in methods: - log.debug(u"Registering JSON method call [{}]".format(method)) - setattr(self, - method, - getattr(LiberviaMethodProxy(self, method), 'call') - ) - - def callMethod(self, method, params, handler = None): - ret = super(LiberviaJsonProxy, self).callMethod(method, params, handler) - return ret - - def call(self, method, cb, *args): - # FIXME: deprecated call method, must be removed once it's not used anymore - id_ = self.callMethod(method, args) - log.debug(u"call: method={} [id={}], args={}".format(method, id_, args)) - if cb: - if isinstance(cb, tuple): - if len(cb) != 2: - log.error("tuple syntax for bridge.call is (callback, errback), aborting") - return - if cb[0] is not None: - self.cb[id_] = cb[0] - self.eb[id_] = cb[1] - else: - self.cb[id_] = cb - - def onRemoteResponse(self, response, request_info): - try: - _cb = self.cb[request_info.id] - except KeyError: - pass - else: - _cb(response) - del self.cb[request_info.id] - - try: - del self.eb[request_info.id] - except KeyError: - pass - - def onRemoteError(self, code, errobj, request_info): - """def dump(obj): - print "\n\nDUMPING %s\n\n" % obj - for i in dir(obj): - print "%s: %s" % (i, getattr(obj,i))""" - try: - _eb = self.eb[request_info.id] - except KeyError: - if code != 0: - log.error("Internal server error") - """for o in code, error, request_info: - dump(o)""" - else: - if isinstance(errobj['message'], dict): - log.error(u"Error %s: %s" % (errobj['message']['faultCode'], errobj['message']['faultString'])) - else: - log.error(u"%s" % errobj['message']) - else: - _eb((code, errobj)) - del self.eb[request_info.id] - - try: - del self.cb[request_info.id] - except KeyError: - pass - - -class RegisterCall(LiberviaJsonProxy): - def __init__(self): - LiberviaJsonProxy.__init__(self, "/register_api", - ["getSessionMetadata", "isConnected", "connect", "registerParams", "menusGet"]) - - -class BridgeCall(LiberviaJsonProxy): - def __init__(self): - LiberviaJsonProxy.__init__(self, "/json_api", - ["getContacts", "addContact", "messageSend", - "psNodeDelete", "psRetractItem", "psRetractItems", - "mbSend", "mbRetract", "mbGet", "mbGetFromMany", "mbGetFromManyRTResult", - "mbGetFromManyWithComments", "mbGetFromManyWithCommentsRTResult", - "historyGet", "getPresenceStatuses", "joinMUC", "mucLeave", "mucGetRoomsJoined", - "inviteMUC", "launchTarotGame", "getTarotCardsPaths", "tarotGameReady", - "tarotGamePlayCards", "launchRadioCollective", - "getWaitingSub", "subscription", "delContact", "updateContact", "avatarGet", - "getEntityData", "getParamsUI", "asyncGetParamA", "setParam", "launchAction", - "disconnect", "chatStateComposing", "getNewAccountDomain", - "syntaxConvert", "getAccountDialogUI", "getMainResource", "getEntitiesData", - "getVersion", "getLiberviaVersion", "mucGetDefaultService", "getFeatures", - "namespacesGet", - ]) - - def __call__(self, *args, **kwargs): - return LiberviaJsonProxy.__call__(self, *args, **kwargs) - - def getConfig(self, dummy1, dummy2): # FIXME - log.warning("getConfig is not implemeted in Libervia yet") - return '' - - def isConnected(self, dummy, callback): # FIXME - log.warning("isConnected is not implemeted in Libervia as for now profile is connected if session is opened") - callback(True) - - def encryptionPluginsGet(self, callback, errback): - """e2e encryption have no sense if made on backend, so we ignore this call""" - callback([]) - - def bridgeConnect(self, callback, errback): - callback() - - -class BridgeSignals(LiberviaJsonProxy): - - def __init__(self, host): - self.host = host - self.retry_time = None - self.retry_nb = 0 - self.retry_warning = None - self.retry_timer = None - LiberviaJsonProxy.__init__(self, "/json_signal_api", - ["getSignals"]) - self._signals = {} # key: signal name, value: callback - - def onRemoteResponse(self, response, request_info): - if self.retry_time: - log.info("Connection with server restablished") - self.retry_nb = 0 - self.retry_time = None - LiberviaJsonProxy.onRemoteResponse(self, response, request_info) - - def onRemoteError(self, code, errobj, request_info): - if errobj['message'] == 'Empty Response': - log.warning(u"Empty reponse bridgeSignal\ncode={}\nrequest_info: id={} method={} handler={}".format(code, request_info.id, request_info.method, request_info.handler)) - # FIXME: to check/replace by a proper session end on disconnected signal - # Window.getLocation().reload() # XXX: reset page in case of session ended. - # FIXME: Should be done more properly without hard reload - LiberviaJsonProxy.onRemoteError(self, code, errobj, request_info) - #we now try to reconnect - if isinstance(errobj['message'], dict) and errobj['message']['faultCode'] == 0: - Window.alert('You are not allowed to connect to server') - else: - def _timerCb(dummy): - current = time.time() - if current > self.retry_time: - msg = "Trying to reconnect to server..." - log.info(msg) - self.retry_warning.showWarning("INFO", msg) - self.retry_timer.cancel() - self.retry_warning = self.retry_timer = None - self.getSignals(callback=self.signalHandler, profile=None) - else: - remaining = int(self.retry_time - current) - msg_html = u"Connection with server lost. Retrying in <strong>{}</strong> s".format(remaining) - self.retry_warning.showWarning("WARNING", msg_html, None) - - if self.retry_nb < 3: - retry_delay = 1 - elif self.retry_nb < 10: - retry_delay = random.randint(1,10) - else: - retry_delay = random.randint(1,60) - self.retry_nb += 1 - log.warning(u"Lost connection, trying to reconnect in {} s (try #{})".format(retry_delay, self.retry_nb)) - self.retry_time = time.time() + retry_delay - self.retry_warning = main_panel.WarningPopup() - self.retry_timer = Timer(notify=_timerCb) - self.retry_timer.scheduleRepeating(1000) - _timerCb(None) - - def register_signal(self, name, callback, with_profile=True): - """Register a signal - - @param: name of the signal to register - @param callback: method to call - @param with_profile: True if the original bridge method need a profile - """ - log.debug(u"Registering signal {}".format(name)) - if name in self._signals: - log.error(u"Trying to register and already registered signal ({})".format(name)) - else: - self._signals[name] = (callback, with_profile) - - def signalHandler(self, signal_data): - self.getSignals(callback=self.signalHandler, profile=None) - if len(signal_data) == 1: - signal_data.append([]) - log.debug(u"Got signal ==> name: %s, params: %s" % (signal_data[0], signal_data[1])) - name, args = signal_data - try: - callback, with_profile = self._signals[name] - except KeyError: - log.warning(u"Ignoring {} signal: no handler registered !".format(name)) - return - if with_profile: - args.append(C.PROF_KEY_NONE) - if not self.host._profile_plugged: - log.debug("Profile is not plugged, we cache the signal") - self.host.signals_cache[C.PROF_KEY_NONE].append((name, callback, args, {})) - else: - callback(*args)
--- a/src/browser/sat_browser/libervia_widget.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,811 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2011-2018 Jérôme Poisson <goffi@goffi.org> - -# This program 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. - -# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. -"""Libervia base widget""" - -import pyjd # this is dummy in pyjs -from sat.core.log import getLogger -log = getLogger(__name__) - -from sat.core.i18n import _ -from sat.core import exceptions -from sat_frontends.quick_frontend import quick_widgets - -from pyjamas.ui.FlexTable import FlexTable -from pyjamas.ui.TabPanel import TabPanel -from pyjamas.ui.SimplePanel import SimplePanel -from pyjamas.ui.AbsolutePanel import AbsolutePanel -from pyjamas.ui.VerticalPanel import VerticalPanel -from pyjamas.ui.HorizontalPanel import HorizontalPanel -from pyjamas.ui.HTMLPanel import HTMLPanel -from pyjamas.ui.Label import Label -from pyjamas.ui.HTML import HTML -from pyjamas.ui.Button import Button -from pyjamas.ui.Widget import Widget -from pyjamas.ui.ClickListener import ClickHandler -from pyjamas.ui import HasAlignment -from pyjamas.ui.DragWidget import DragWidget -from pyjamas.ui.DropWidget import DropWidget -from pyjamas import DOM -from pyjamas import Window - -import dialog -import base_menu -import base_widget -import base_panel - - -unicode = str # FIXME: pyjamas workaround - - -# FIXME: we need to group several unrelated panels/widgets in this module because of isinstance tests and other references to classes (e.g. if we separate Drag n Drop classes in a separate module, we'll have cyclic import because of the references to LiberviaWidget in DropCell). -# TODO: use a more generic method (either use duck typing, or register classes in a generic way, without hard references), then split classes in separate modules - - -### Drag n Drop ### - - -class DragLabel(DragWidget): - - def __init__(self, text, type_, host=None): - """Base of Drag n Drop mecanism in Libervia - - @param text: data embedded with in drag n drop operation - @param type_: type of data that we are dragging - @param host: if not None, the host will be use to highlight BorderWidgets - """ - DragWidget.__init__(self) - self.host = host - self._text = text - self.type_ = type_ - - def onDragStart(self, event): - dt = event.dataTransfer - dt.setData('text/plain', "%s\n%s" % (self._text, self.type_)) - dt.setDragImage(self.getElement(), 15, 15) - if self.host is not None: - current_panel = self.host.tab_panel.getCurrentPanel() - for widget in current_panel.widgets: - if isinstance(widget, BorderWidget): - widget.addStyleName('borderWidgetOnDrag') - - def onDragEnd(self, event): - if self.host is not None: - current_panel = self.host.tab_panel.getCurrentPanel() - for widget in current_panel.widgets: - if isinstance(widget, BorderWidget): - widget.removeStyleName('borderWidgetOnDrag') - - -class LiberviaDragWidget(DragLabel): - """ A DragLabel which keep the widget being dragged as class value """ - current = None # widget currently dragged - - def __init__(self, text, type_, widget): - DragLabel.__init__(self, text, type_, widget.host) - self.widget = widget - - def onDragStart(self, event): - LiberviaDragWidget.current = self.widget - DragLabel.onDragStart(self, event) - - def onDragEnd(self, event): - DragLabel.onDragEnd(self, event) - LiberviaDragWidget.current = None - - -class DropCell(DropWidget): - """Cell in the middle grid which replace itself with the dropped widget on DnD""" - drop_keys = {} - - def __init__(self, host): - DropWidget.__init__(self) - self.host = host - self.setStyleName('dropCell') - - @classmethod - def addDropKey(cls, key, cb): - """Add a association between a key and a class to create on drop. - - @param key: key to be associated (e.g. "CONTACT", "CHAT") - @param cb: a callable (either a class or method) returning a - LiberviaWidget instance - """ - DropCell.drop_keys[key] = cb - - def onDragEnter(self, event): - if self == LiberviaDragWidget.current: - return - self.addStyleName('dragover') - DOM.eventPreventDefault(event) - - def onDragLeave(self, event): - if event.clientX <= self.getAbsoluteLeft() or event.clientY <= self.getAbsoluteTop() or\ - event.clientX >= self.getAbsoluteLeft() + self.getOffsetWidth() - 1 or event.clientY >= self.getAbsoluteTop() + self.getOffsetHeight() - 1: - # We check that we are inside widget's box, and we don't remove the style in this case because - # if the mouse is over a widget inside the DropWidget, if will leave the DropWidget, and we - # don't want that - self.removeStyleName('dragover') - - def onDragOver(self, event): - DOM.eventPreventDefault(event) - - def _getCellAndRow(self, grid, event): - """Return cell and row index where the event is occuring""" - cell = grid.getEventTargetCell(event) - row = DOM.getParent(cell) - return (row.rowIndex, cell.cellIndex) - - def onDrop(self, event): - """ - @raise NoLiberviaWidgetException: something else than a LiberviaWidget - has been returned by the callback. - """ - self.removeStyleName('dragover') - DOM.eventPreventDefault(event) - item, item_type = eventGetData(event) - if item_type == "WIDGET": - if not LiberviaDragWidget.current: - log.error("No widget registered in LiberviaDragWidget !") - return - _new_panel = LiberviaDragWidget.current - if self == _new_panel: # We can't drop on ourself - return - # we need to remove the widget from the panel as it will be inserted elsewhere - widgets_panel = _new_panel.getParent(WidgetsPanel, expect=True) - wid_row = widgets_panel.getWidgetCoords(_new_panel)[0] - row_wids = widgets_panel.getLiberviaRowWidgets(wid_row) - if len(row_wids) == 1 and wid_row == widgets_panel.getWidgetCoords(self)[0]: - # the dropped widget is the only one in the same row - # as the target widget (self), we don't do anything - return - widgets_panel.removeWidget(_new_panel) - elif item_type in self.drop_keys: - _new_panel = self.drop_keys[item_type](self.host, item) - if not isinstance(_new_panel, LiberviaWidget): - raise base_widget.NoLiberviaWidgetException - else: - log.warning("unmanaged item type") - return - if isinstance(self, LiberviaWidget): - # self.host.unregisterWidget(self) # FIXME - self.onQuit() - if not isinstance(_new_panel, LiberviaWidget): - log.warning("droping an object which is not a class of LiberviaWidget") - _flextable = self.getParent() - _widgetspanel = _flextable.getParent().getParent() - row_idx, cell_idx = self._getCellAndRow(_flextable, event) - if self.host.getSelected() == self: - self.host.setSelected(None) - _widgetspanel.changeWidget(row_idx, cell_idx, _new_panel) - """_unempty_panels = filter(lambda wid:not isinstance(wid,EmptyWidget),list(_flextable)) - _width = 90/float(len(_unempty_panels) or 1) - #now we resize all the cell of the column - for panel in _unempty_panels: - td_elt = panel.getElement().parentNode - DOM.setStyleAttribute(td_elt, "width", "%s%%" % _width)""" - if isinstance(self, quick_widgets.QuickWidget): - self.host.widgets.deleteWidget(self) - - -class EmptyWidget(DropCell, SimplePanel): - """Empty dropable panel""" - - def __init__(self, host): - SimplePanel.__init__(self) - DropCell.__init__(self, host) - #self.setWidget(HTML('')) - self.setSize('100%', '100%') - - -class BorderWidget(EmptyWidget): - def __init__(self, host): - EmptyWidget.__init__(self, host) - self.addStyleName('borderPanel') - - -class LeftBorderWidget(BorderWidget): - def __init__(self, host): - BorderWidget.__init__(self, host) - self.addStyleName('leftBorderWidget') - - -class RightBorderWidget(BorderWidget): - def __init__(self, host): - BorderWidget.__init__(self, host) - self.addStyleName('rightBorderWidget') - - -class BottomBorderWidget(BorderWidget): - def __init__(self, host): - BorderWidget.__init__(self, host) - self.addStyleName('bottomBorderWidget') - - -class DropTab(Label, DropWidget): - - def __init__(self, tab_panel, text): - Label.__init__(self, text) - DropWidget.__init__(self, tab_panel) - self.tab_panel = tab_panel - self.setStyleName('dropCell') - self.setWordWrap(False) - - def _getIndex(self): - """ get current index of the DropTab """ - # XXX: awful hack, but seems the only way to get index - return self.tab_panel.tabBar.panel.getWidgetIndex(self.getParent().getParent()) - 1 - - def onDragEnter(self, event): - #if self == LiberviaDragWidget.current: - # return - self.parent.addStyleName('dragover') - DOM.eventPreventDefault(event) - - def onDragLeave(self, event): - self.parent.removeStyleName('dragover') - - def onDragOver(self, event): - DOM.eventPreventDefault(event) - - def onDrop(self, event): - DOM.eventPreventDefault(event) - self.parent.removeStyleName('dragover') - if self._getIndex() == self.tab_panel.tabBar.getSelectedTab(): - # the widget comes from the same tab, so nothing to do, we let it there - return - - item, item_type = eventGetData(event) - if item_type == "WIDGET": - if not LiberviaDragWidget.current: - log.error("No widget registered in LiberviaDragWidget !") - return - _new_panel = LiberviaDragWidget.current - elif item_type in DropCell.drop_keys: - pass # create the widget when we are sure there's a tab for it - else: - log.warning("unmanaged item type") - return - - # XXX: when needed, new tab creation must be done exactly here to not mess up with LiberviaDragWidget.onDragEnd - try: - widgets_panel = self.tab_panel.getWidget(self._getIndex()) - except IndexError: # widgets panel doesn't exist, e.g. user dropped in "+" tab - widgets_panel = self.tab_panel.addWidgetsTab(None) - if widgets_panel is None: # user cancelled - return - - if item_type == "WIDGET": - _new_panel.getParent(WidgetsPanel, expect=True).removeWidget(_new_panel) - else: - _new_panel = DropCell.drop_keys[item_type](self.tab_panel.host, item) - - widgets_panel.addWidget(_new_panel) - - -### Libervia Widget ### - - -class WidgetHeader(AbsolutePanel, LiberviaDragWidget): - - def __init__(self, parent, host, title, info=None): - """ - @param parent (LiberviaWidget): LiberWidget instance - @param host (SatWebFrontend): SatWebFrontend instance - @param title (Label, HTML): text widget instance - @param info (Widget): text widget instance - """ - AbsolutePanel.__init__(self) - self.add(title) - if info: - # FIXME: temporary design to display the info near the menu - button_group_wrapper = HorizontalPanel() - button_group_wrapper.add(info) - else: - button_group_wrapper = SimplePanel() - button_group_wrapper.setStyleName('widgetHeader_buttonsWrapper') - button_group = base_widget.WidgetMenuBar(parent, host) - button_group.addItem('<img src="media/icons/misc/settings.png"/>', True, base_menu.SimpleCmd(parent.onSetting)) - button_group.addItem('<img src="media/icons/misc/close.png"/>', True, base_menu.SimpleCmd(parent.onClose)) - button_group_wrapper.add(button_group) - self.add(button_group_wrapper) - self.addStyleName('widgetHeader') - LiberviaDragWidget.__init__(self, "", "WIDGET", parent) - - -class LiberviaWidget(DropCell, VerticalPanel, ClickHandler): - """Libervia's widget which can replace itself with a dropped widget on DnD""" - - def __init__(self, host, title='', info=None, selectable=False, plugin_menu_context=None): - """Init the widget - - @param host (SatWebFrontend): SatWebFrontend instance - @param title (unicode): title shown in the header of the widget - @param info (unicode): info shown in the header of the widget - @param selectable (bool): True is widget can be selected by user - @param plugin_menu_context (iterable): contexts of menus to have (list of C.MENU_* constant) - """ - VerticalPanel.__init__(self) - DropCell.__init__(self, host) - ClickHandler.__init__(self) - self._selectable = selectable - self._plugin_menu_context = [] if plugin_menu_context is None else plugin_menu_context - self._title_id = HTMLPanel.createUniqueId() - self._setting_button_id = HTMLPanel.createUniqueId() - self._close_button_id = HTMLPanel.createUniqueId() - self._title = Label(title) - self._title.setStyleName('widgetHeader_title') - if info is not None: - self._info = HTML(info) - self._info.setStyleName('widgetHeader_info') - else: - self._info = None - header = WidgetHeader(self, host, self._title, self._info) - self.add(header) - self.setSize('100%', '100%') - self.addStyleName('widget') - if self._selectable: - self.addClickListener(self) - - @property - def plugin_menu_context(self): - return self._plugin_menu_context - - def getDebugName(self): - return "%s (%s)" % (self, self._title.getText()) - - def getParent(self, class_=None, expect=True): - """Return the closest ancestor of the specified class. - - Note: this method overrides pyjamas.ui.Widget.getParent - - @param class_: class of the ancestor to look for or None to return the first parent - @param expect: set to True if the parent is expected (raise an error if not found) - @return: the parent/ancestor or None if it has not been found - @raise exceptions.InternalError: expect is True and no parent is found - """ - current = Widget.getParent(self) - if class_ is None: - return current # this is the default behavior - while current is not None and not isinstance(current, class_): - current = Widget.getParent(current) - if current is None and expect: - raise exceptions.InternalError("Can't find parent %s for %s" % (class_, self)) - return current - - def onClick(self, sender): - self.host.setSelected(self) - - def onClose(self, sender): - """ Called when the close button is pushed """ - widgets_panel = self.getParent(WidgetsPanel, expect=True) - widgets_panel.removeWidget(self) - self.onQuit() - self.host.widgets.deleteWidget(self) - - def onQuit(self): - """ Called when the widget is actually ending """ - pass - - def refresh(self): - """This can be overwritten by a child class to refresh the display when, - instead of creating a new one, an existing widget is found and reused. - """ - pass - - def onSetting(self, sender): - widpanel = self.getParent(WidgetsPanel, expect=True) - row, col = widpanel.getIndex(self) - body = VerticalPanel() - - # colspan & rowspan - colspan = widpanel.getColSpan(row, col) - rowspan = widpanel.getRowSpan(row, col) - - def onColSpanChange(value): - widpanel.setColSpan(row, col, value) - - def onRowSpanChange(value): - widpanel.setRowSpan(row, col, value) - colspan_setter = dialog.IntSetter("Columns span", colspan) - colspan_setter.addValueChangeListener(onColSpanChange) - colspan_setter.setWidth('100%') - rowspan_setter = dialog.IntSetter("Rows span", rowspan) - rowspan_setter.addValueChangeListener(onRowSpanChange) - rowspan_setter.setWidth('100%') - body.add(colspan_setter) - body.add(rowspan_setter) - - # size - width_str = self.getWidth() - if width_str.endswith('px'): - width = int(width_str[:-2]) - else: - width = 0 - height_str = self.getHeight() - if height_str.endswith('px'): - height = int(height_str[:-2]) - else: - height = 0 - - def onWidthChange(value): - if not value: - self.setWidth('100%') - else: - self.setWidth('%dpx' % value) - - def onHeightChange(value): - if not value: - self.setHeight('100%') - else: - self.setHeight('%dpx' % value) - width_setter = dialog.IntSetter("width (0=auto)", width) - width_setter.addValueChangeListener(onWidthChange) - width_setter.setWidth('100%') - height_setter = dialog.IntSetter("height (0=auto)", height) - height_setter.addValueChangeListener(onHeightChange) - height_setter.setHeight('100%') - body.add(width_setter) - body.add(height_setter) - - # reset - def onReset(sender): - colspan_setter.setValue(1) - rowspan_setter.setValue(1) - width_setter.setValue(0) - height_setter.setValue(0) - - reset_bt = Button("Reset", onReset) - body.add(reset_bt) - body.setCellHorizontalAlignment(reset_bt, HasAlignment.ALIGN_CENTER) - - _dialog = dialog.GenericDialog("Widget setting", body) - _dialog.show() - - def setTitle(self, text): - """change the title in the header of the widget - @param text: text of the new title""" - self._title.setText(text) - - def setHeaderInfo(self, text): - """change the info in the header of the widget - @param text: text of the new title""" - try: - self._info.setHTML(text) - except TypeError: - log.error("LiberviaWidget.setInfo: info widget has not been initialized!") - - def isSelectable(self): - return self._selectable - - def setSelectable(self, selectable): - if not self._selectable: - try: - self.removeClickListener(self) - except ValueError: - pass - if self.selectable and not self in self._clickListeners: - self.addClickListener(self) - self._selectable = selectable - - def getWarningData(self): - """ Return exposition warning level when this widget is selected and something is sent to it - This method should be overriden by children - @return: tuple (warning level type/HTML msg). Type can be one of: - - PUBLIC - - GROUP - - ONE2ONE - - MISC - - NONE - """ - if not self._selectable: - log.error("getWarningLevel must not be called for an unselectable widget") - raise Exception - # TODO: cleaner warning types (more general constants) - return ("NONE", None) - - def setWidget(self, widget, scrollable=True): - """Set the widget that will be in the body of the LiberviaWidget - @param widget: widget to put in the body - @param scrollable: if true, the widget will be in a ScrollPanelWrapper""" - if scrollable: - _scrollpanelwrapper = base_panel.ScrollPanelWrapper() - _scrollpanelwrapper.setStyleName('widgetBody') - _scrollpanelwrapper.setWidget(widget) - body_wid = _scrollpanelwrapper - else: - body_wid = widget - self.add(body_wid) - self.setCellHeight(body_wid, '100%') - - def doDetachChildren(self): - # We need to force the use of a panel subclass method here, - # for the same reason as doAttachChildren - VerticalPanel.doDetachChildren(self) - - def doAttachChildren(self): - # We need to force the use of a panel subclass method here, else - # the event will not propagate to children - VerticalPanel.doAttachChildren(self) - - -# XXX: WidgetsPanel and MainTabPanel are both here to avoir cyclic import - - -class WidgetsPanel(base_panel.ScrollPanelWrapper): - """The panel wanaging the widgets indide a tab""" - - def __init__(self, host, locked=False): - """ - - @param host (SatWebFrontend): host instance - @param locked (bool): If True, the tab containing self will not be - removed when there are no more widget inside self. If False, the - tab will be removed with self's last widget. - """ - base_panel.ScrollPanelWrapper.__init__(self) - self.setSize('100%', '100%') - self.host = host - self.locked = locked - self.selected = None - self.flextable = FlexTable() - self.flextable.setSize('100%', '100%') - self.setWidget(self.flextable) - self.setStyleName('widgetsPanel') - _bottom = BottomBorderWidget(self.host) - self.flextable.setWidget(0, 0, _bottom) # There will be always an Empty widget on the last row, - # dropping a widget there will add a new row - td_elt = _bottom.getElement().parentNode - DOM.setStyleAttribute(td_elt, "height", "1px") # needed so the cell adapt to the size of the border (specially in webkit) - self._max_cols = 1 # give the maximum number of columns in a raw - - @property - def widgets(self): - return iter(self.flextable) - - def isLocked(self): - return self.locked - - def changeWidget(self, row, col, wid): - """Change the widget in the given location, add row or columns when necessary""" - log.debug(u"changing widget: %s %s %s" % (wid.getDebugName(), row, col)) - last_row = max(0, self.flextable.getRowCount() - 1) - # try: # FIXME: except without exception specified ! - prev_wid = self.flextable.getWidget(row, col) - # except: - # log.error("Trying to change an unexisting widget !") - # return - - cellFormatter = self.flextable.getFlexCellFormatter() - - if isinstance(prev_wid, BorderWidget): - # We are on a border, we must create a row and/or columns - prev_wid.removeStyleName('dragover') - - if isinstance(prev_wid, BottomBorderWidget): - # We are on the bottom border, we create a new row - self.flextable.insertRow(last_row) - self.flextable.setWidget(last_row, 0, LeftBorderWidget(self.host)) - self.flextable.setWidget(last_row, 1, wid) - self.flextable.setWidget(last_row, 2, RightBorderWidget(self.host)) - cellFormatter.setHorizontalAlignment(last_row, 2, HasAlignment.ALIGN_RIGHT) - row = last_row - - elif isinstance(prev_wid, LeftBorderWidget): - if col != 0: - log.error("LeftBorderWidget must be on the first column !") - return - self.flextable.insertCell(row, col + 1) - self.flextable.setWidget(row, 1, wid) - - elif isinstance(prev_wid, RightBorderWidget): - if col != self.flextable.getCellCount(row) - 1: - log.error("RightBorderWidget must be on the last column !") - return - self.flextable.insertCell(row, col) - self.flextable.setWidget(row, col, wid) - - else: - prev_wid.removeFromParent() - self.flextable.setWidget(row, col, wid) - - _max_cols = max(self._max_cols, self.flextable.getCellCount(row)) - if _max_cols != self._max_cols: - self._max_cols = _max_cols - self._sizesAdjust() - - def _sizesAdjust(self): - cellFormatter = self.flextable.getFlexCellFormatter() - width = 100.0 / max(1, self._max_cols - 2) # we don't count the borders - - for row_idx in xrange(self.flextable.getRowCount()): - for col_idx in xrange(self.flextable.getCellCount(row_idx)): - _widget = self.flextable.getWidget(row_idx, col_idx) - if _widget and not isinstance(_widget, BorderWidget): - td_elt = _widget.getElement().parentNode - DOM.setStyleAttribute(td_elt, "width", "%.2f%%" % width) - - last_row = max(0, self.flextable.getRowCount() - 1) - cellFormatter.setColSpan(last_row, 0, self._max_cols) - - def addWidget(self, wid): - """Add a widget to a new cell on the next to last row""" - last_row = max(0, self.flextable.getRowCount() - 1) - log.debug(u"putting widget %s at %d, %d" % (wid.getDebugName(), last_row, 0)) - self.changeWidget(last_row, 0, wid) - - def removeWidget(self, wid): - """Remove a widget and the cell where it is""" - _row, _col = self.flextable.getIndex(wid) - self.flextable.remove(wid) - self.flextable.removeCell(_row, _col) - if not self.getLiberviaRowWidgets(_row): # we have no more widgets, we remove the row - self.flextable.removeRow(_row) - _max_cols = 1 - for row_idx in xrange(self.flextable.getRowCount()): - _max_cols = max(_max_cols, self.flextable.getCellCount(row_idx)) - if _max_cols != self._max_cols: - self._max_cols = _max_cols - self._sizesAdjust() - current = self - - blank_page = self.getLiberviaWidgetsCount() == 0 # do we still have widgets on the page ? - - if blank_page and not self.isLocked(): - # we now notice the MainTabPanel that the WidgetsPanel is empty and need to be removed - while current is not None: - if isinstance(current, MainTabPanel): - current.onWidgetPanelRemove(self) - return - current = current.getParent() - log.error("no MainTabPanel found !") - - def getWidgetCoords(self, wid): - return self.flextable.getIndex(wid) - - def getLiberviaRowWidgets(self, row): - """ Return all the LiberviaWidget in the row """ - return [wid for wid in self.getRowWidgets(row) if isinstance(wid, LiberviaWidget)] - - def getRowWidgets(self, row): - """ Return all the widgets in the row """ - widgets = [] - cols = self.flextable.getCellCount(row) - for col in xrange(cols): - widgets.append(self.flextable.getWidget(row, col)) - return widgets - - def getLiberviaWidgetsCount(self): - """ Get count of contained widgets """ - return len([wid for wid in self.flextable if isinstance(wid, LiberviaWidget)]) - - def getIndex(self, wid): - return self.flextable.getIndex(wid) - - def getColSpan(self, row, col): - cellFormatter = self.flextable.getFlexCellFormatter() - return cellFormatter.getColSpan(row, col) - - def setColSpan(self, row, col, value): - cellFormatter = self.flextable.getFlexCellFormatter() - return cellFormatter.setColSpan(row, col, value) - - def getRowSpan(self, row, col): - cellFormatter = self.flextable.getFlexCellFormatter() - return cellFormatter.getRowSpan(row, col) - - def setRowSpan(self, row, col, value): - cellFormatter = self.flextable.getFlexCellFormatter() - return cellFormatter.setRowSpan(row, col, value) - - -class MainTabPanel(TabPanel, ClickHandler): - """The panel managing the tabs""" - - def __init__(self, host): - TabPanel.__init__(self, FloatingTab=True) - ClickHandler.__init__(self) - self.host = host - self.setStyleName('liberviaTabPanel') - self.tabBar.addTab(DropTab(self, u'✚'), asHTML=False) - self.tabBar.setVisible(False) # set to True when profile is logged - self.tabBar.addStyleDependentName('oneTab') - - def onTabSelected(self, sender, tabIndex): - if tabIndex < self.getWidgetCount(): - TabPanel.onTabSelected(self, sender, tabIndex) - self.host.selected_widget = self.getCurrentPanel().selected - return - # user clicked the "+" tab - self.addWidgetsTab(None, select=True) - - def getCurrentPanel(self): - """ Get the panel of the currently selected tab - - @return: WidgetsPanel - """ - return self.deck.visibleWidget - - def addTab(self, widget, label, select=False): - """Create a new tab for the given widget. - - @param widget (Widget): widget to associate to the tab - @param label (unicode): label of the tab - @param select (bool): True to select the added tab - """ - TabPanel.add(self, widget, DropTab(self, label), False) - if self.getWidgetCount() > 1: - self.tabBar.removeStyleDependentName('oneTab') - self.host.resize() - if select: - self.selectTab(self.getWidgetCount() - 1) - - def addWidgetsTab(self, label, select=False, locked=False): - """Create a new tab for containing LiberviaWidgets. - - @param label (unicode): label of the tab (None or '' for user prompt) - @param select (bool): True to select the added tab - @param locked (bool): If True, the tab will not be removed when there - are no more widget inside. If False, the tab will be removed with - the last widget. - @return: WidgetsPanel - """ - widgets_panel = WidgetsPanel(self.host, locked=locked) - - if not label: - default_label = _(u'new tab') - try: - label = Window.prompt(_(u'Name of the new tab'), default_label) - if not label: # empty label or user pressed "cancel" - return None - except: # this happens when the user prevents the page to open the prompt dialog - label = default_label - - self.addTab(widgets_panel, label, select) - return widgets_panel - - def onWidgetPanelRemove(self, panel): - """ Called when a child WidgetsPanel is empty and need to be removed """ - widget_index = self.getWidgetIndex(panel) - self.remove(panel) - widgets_count = self.getWidgetCount() - if widgets_count == 1: - self.tabBar.addStyleDependentName('oneTab') - self.host.resize() - self.selectTab(widget_index if widget_index < widgets_count else widgets_count - 1) - - -def eventGetData(event): - """Retrieve the event data. - - @param event(EventObject) - @return tuple: (event_text, event_type) - """ - dt = event.dataTransfer - # 'text', 'text/plain', and 'Text' are equivalent. - try: - item, item_type = dt.getData("text/plain").split('\n') # Workaround for webkit, only text/plain seems to be managed - if item_type and item_type[-1] == '\0': # Workaround for what looks like a pyjamas bug: the \0 should not be there, and - item_type = item_type[:-1] # .strip('\0') and .replace('\0','') don't work. TODO: check this and fill a bug report - # item_type = dt.getData("type") - log.debug(u"event data: %s (type %s)" % (item, item_type)) - except: - log.debug("event data not found") - item = ' ' - item_type = None - return item, item_type
--- a/src/browser/sat_browser/list_manager.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,516 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2013-2016 Adrien Cossa <souliane@mailoo.org> - -# This program 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. - -# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. - -from sat.core.log import getLogger -log = getLogger(__name__) -from sat.core.i18n import _ - -from pyjamas.ui.ClickListener import ClickHandler -from pyjamas.ui.FocusListener import FocusHandler -from pyjamas.ui.ChangeListener import ChangeHandler -from pyjamas.ui.DragHandler import DragHandler -from pyjamas.ui.KeyboardListener import KeyboardHandler, KEY_ENTER -from pyjamas.ui.DragWidget import DragWidget -from pyjamas.ui.ListBox import ListBox -from pyjamas.ui.Button import Button -from pyjamas.ui.FlowPanel import FlowPanel -from pyjamas.ui.HorizontalPanel import HorizontalPanel -from pyjamas.ui.FlexTable import FlexTable -from pyjamas.ui.AutoComplete import AutoCompleteTextBox - -import base_panel -import base_widget -import libervia_widget - -from sat_frontends.quick_frontend import quick_list_manager - - -unicode = str # FIXME: pyjamas workaround - - -class ListItem(HorizontalPanel): - """This class implements a list item with auto-completion and a delete button.""" - - STYLE = {"listItem": "listItem", - "listItem-box": "listItem-box", - "listItem-box-invalid": "listItem-box-invalid", - "listItem-button": "listItem-button", - } - - VALID = 1 - INVALID = 2 - DUPLICATE = 3 - - def __init__(self, listener=None, taglist=None, validate=None): - """ - - @param listener (ListItemHandler): handler for the UI events - @param taglist (quick_list_manager.QuickTagList): list manager - @param validate (callable): method returning a bool to validate the entry - """ - HorizontalPanel.__init__(self) - self.addStyleName(self.STYLE["listItem"]) - - self.box = AutoCompleteTextBox(StyleName=self.STYLE["listItem-box"]) - self.remove_btn = Button('<span>x</span>', Visible=False) - self.remove_btn.setStyleName(self.STYLE["listItem-button"]) - self.add(self.box) - self.add(self.remove_btn) - - if listener: - self.box.addFocusListener(listener) - self.box.addChangeListener(listener) - self.box.addKeyboardListener(listener) - self.box.choices.addClickListener(listener) - self.remove_btn.addClickListener(listener) - - self.taglist = taglist - self.validate = validate - self.last_checked_value = "" - self.last_validity = self.VALID - - @property - def text(self): - return self.box.getText() - - def setText(self, text): - """ - Set the text and refresh the Widget. - - @param text (unicode): text to set - """ - self.box.setText(text) - self.refresh() - - def refresh(self): - if self.last_checked_value == self.text: - return - - if self.taglist and self.last_checked_value: - self.taglist.untag([self.last_checked_value]) - - if self.validate: # if None, the state is always valid - self.last_validity = self.validate(self.text) - - if self.last_validity == self.VALID: - self.box.removeStyleName(self.STYLE["listItem-box-invalid"]) - self.box.setVisibleLength(max(len(self.text), 10)) - elif self.last_validity == self.INVALID: - self.box.addStyleName(self.STYLE["listItem-box-invalid"]) - elif self.last_validity == self.DUPLICATE: - self.remove_btn.click() # this may do more stuff then self.remove() - return - - if self.taglist and self.text: - self.taglist.tag([self.text]) - self.last_checked_value = self.text - self.box.setSelectionRange(len(self.text), 0) - self.remove_btn.setVisible(len(self.text) > 0) - - def setFocus(self, focused): - self.box.setFocus(focused) - - def remove(self): - """Remove the list item from its parent.""" - self.removeFromParent() - - if self.taglist and self.text: # this must be done after the widget has been removed - self.taglist.untag([self.text]) - - -class DraggableListItem(ListItem, DragWidget): - """This class is like ListItem, but in addition it can be dragged.""" - - def __init__(self, listener=None, taglist=None, validate=None): - """ - - @param listener (ListItemHandler): handler for the UI events - @param taglist (quick_list_manager.QuickTagList): list manager - @param validate (callable): method returning a bool to validate the entry - """ - ListItem.__init__(self, listener, taglist, validate) - DragWidget.__init__(self) - self.addDragListener(listener) - - - def onDragStart(self, event): - """The user starts dragging the item.""" - dt = event.dataTransfer - dt.setData('text/plain', "%s\n%s" % (self.text, "CONTACT_TEXTBOX")) - dt.setDragImage(self.box.getElement(), 15, 15) - - -class ListItemHandler(ClickHandler, FocusHandler, KeyboardHandler, ChangeHandler): - """Implements basic handlers for the ListItem events.""" - - last_item = None # the last item is an empty text box for user input - - def __init__(self, taglist): - """ - - @param taglist (quick_list_manager.QuickTagList): list manager - """ - ClickHandler.__init__(self) - FocusHandler.__init__(self) - ChangeHandler.__init__(self) - KeyboardHandler.__init__(self) - self.taglist = taglist - - def addItem(self, item): - raise NotImplementedError - - def removeItem(self, item): - raise NotImplementedError - - def onClick(self, sender): - """The remove button or a suggested completion item has been clicked.""" - #log.debug("onClick sender type: %s" % type(sender)) - if isinstance(sender, Button): - item = sender.getParent() - self.removeItem(item) - elif isinstance(sender, ListBox): - # this is called after onChange when you click a suggested item, and now we get the final value - textbox = sender._clickListeners[0] - self.checkValue(textbox) - else: - raise AssertionError - - def onFocus(self, sender): - """The text box has the focus.""" - #log.debug("onFocus sender type: %s" % type(sender)) - assert isinstance(sender, AutoCompleteTextBox) - sender.setCompletionItems(self.taglist.untagged) - - def onKeyUp(self, sender, keycode, modifiers): - """The text box is being modified - or ENTER key has been pressed.""" - # this is called after onChange when you press ENTER, and now we get the final value - #log.debug("onKeyUp sender type: %s" % type(sender)) - assert isinstance(sender, AutoCompleteTextBox) - if keycode == KEY_ENTER: - self.checkValue(sender) - - def onChange(self, sender): - """The text box has been changed by the user.""" - # this is called before the completion when you press ENTER or click a suggest item - #log.debug("onChange sender type: %s" % type(sender)) - assert isinstance(sender, AutoCompleteTextBox) - self.checkValue(sender) - - def checkValue(self, textbox): - """Internal handler to call when a new value is submitted by the user.""" - item = textbox.getParent() - if item.text == item.last_checked_value: - # this method has already been called (by self.onChange) and there's nothing new - return - item.refresh() - if item == self.last_item and item.last_validity == ListItem.VALID and item.text: - self.addItem() - -class DraggableListItemHandler(ListItemHandler, DragHandler): - """Implements basic handlers for the DraggableListItem events.""" - - def __init__(self, manager): - """ - - @param manager (ListManager): list manager - """ - ListItemHandler.__init__(self, manager) - DragHandler.__init__(self) - - @property - def manager(self): - return self.taglist - - def onDragStart(self, event): - """The user starts dragging the item.""" - self.manager.drop_target = None - - def onDragEnd(self, event): - """The user dropped the list item.""" - text, dummy = libervia_widget.eventGetData(event) - target = self.manager.drop_target # self or another ListPanel - if text == "" or target is None: - return - if target != self: # move the item from self to target - target.addItem(text) - self.removeItem(self.getItem(text)) - - -class ListPanel(FlowPanel, DraggableListItemHandler, libervia_widget.DropCell): - """Implements a list of items.""" - # XXX: beware that pyjamas.ui.FlowPanel is not fully implemented: - # - it can not be used with pyjamas.ui.Label - # - FlowPanel.insert doesn't work - - STYLE = {"listPanel": "listPanel"} - ACCEPT_NEW_ENTRY = False - - def __init__(self, manager, items=None): - """Initialization with a button for the list name (key) and a DraggableListItem. - - @param manager (ListManager): list manager - @param items (list): items to be set - """ - FlowPanel.__init__(self) - DraggableListItemHandler.__init__(self, manager) - libervia_widget.DropCell.__init__(self, None) - self.addStyleName(self.STYLE["listPanel"]) - self.manager = manager - self.resetItems(items) - - # FIXME: dirty magic strings '@' and '@@' - self.drop_keys = {"GROUP": lambda host, item_s: self.addItem("@%s" % item_s), - "CONTACT": lambda host, item_s: self.addItem(item_s), - "CONTACT_TITLE": lambda host, item_s: self.addItem('@@'), - "CONTACT_TEXTBOX": lambda host, item_s: setattr(self.manager, "drop_target", self), - } - - def onDrop(self, event): - """Something has been dropped in this ListPanel""" - try: - libervia_widget.DropCell.onDrop(self, event) - except base_widget.NoLiberviaWidgetException: - pass - - def getItem(self, text): - """Get an item from its text. - - @param text(unicode): item text - """ - for child in self.getChildren(): - if child.text == text: - return child - return None - - def getItems(self): - """Get the non empty items. - - @return list(unicode) - """ - return [widget.text for widget in self.getChildren() if isinstance(widget, ListItem) and widget.text] - - def validateItem(self, text): - """Return validation code after the item has been changed. - - @param text (unicode): item text to check - @return: int value defined by one of these constants: - - VALID if the item is valid - - INVALID if the item is not valid but can be displayed - - DUPLICATE if the item is a duplicate - """ - def count(list_, item): # XXX: list.count in not implemented by pyjamas - return len([elt for elt in list_ if elt == item]) - - if count(self.getItems(), text) > 1: - return ListItem.DUPLICATE # item already exists in this list so we suggest its deletion - if self.ACCEPT_NEW_ENTRY: - return ListItem.VALID - return ListItem.VALID if text in self.manager.items or not text else ListItem.INVALID - - def addItem(self, text=""): - """Add an item. - - @param text (unicode): text to be set. - @return: True if the item has been really added or merged. - """ - if text in self.getItems(): # avoid duplicate in the same list - return - - item = DraggableListItem(self, self.manager, self.validateItem) - self.add(item) - - if self.last_item: - if self.last_item.last_validity == ListItem.INVALID: - # switch the two values so that the invalid one stays in last position - item.setText(self.last_item.text) - self.last_item.setText(text) - elif not self.last_item.text: - # copy the new value to previous empty item - self.last_item.setText(text) - else: # first item of the list, or previous last item has been deleted - item.setText(text) - - self.last_item = item - self.last_item.setFocus(True) - - def removeItem(self, item): - """Remove an item. - - @param item(DraggableListItem): item to remove - """ - if item == self.last_item: - self.addItem("") - item.remove() # this also updates the taglist - - def resetItems(self, items): - """Reset the items. - - @param items (list): items to be set - """ - for child in self.getChildren(): - child.remove() - - self.addItem() - if not items: - return - - items.sort() - for item in items: - self.addItem(unicode(item)) - - -class ListManager(FlexTable, quick_list_manager.QuickTagList): - """Implements a table to manage one or several lists of items.""" - - STYLE = {"listManager-button": "group", - "listManager-button-cell": "listManager-button-cell", - } - - def __init__(self, data=None, items=None): - """ - @param data (dict{unicode: list}): dict binding keys to tagged items. - @param items (list): full list of items (tagged and untagged) - """ - FlexTable.__init__(self, Width="100%") - quick_list_manager.QuickTagList.__init__(self, [unicode(item) for item in items]) - self.lists = {} - - if data: - for key, items in data.iteritems(): - self.addList(key, [unicode(item) for item in items]) - - def addList(self, key, items=None): - """Add a Button and ListPanel for a new list. - - @param key (unicode): list name - @param items (list): items to append to the new list - """ - if key in self.lists: - return - - if items is None: - items = [] - - self.lists[key] = {"button": Button(key, Title=key, StyleName=self.STYLE["listManager-button"]), - "panel": ListPanel(self, items)} - - y, x = len(self.lists), 0 - self.insertRow(y) - self.setWidget(y, x, self.lists[key]["button"]) - self.setWidget(y, x + 1, self.lists[key]["panel"]) - self.getCellFormatter().setStyleName(y, x, self.STYLE["listManager-button-cell"]) - - try: - self.popup_menu.registerClickSender(self.lists[key]["button"]) - except (AttributeError, TypeError): # self.registerPopupMenuPanel hasn't been called yet - pass - - def removeList(self, key): - """Remove a ListPanel from this manager. - - @param key (unicode): list name - """ - items = self.lists[key]["panel"].getItems() - (y, x) = self.getIndex(self.lists[key]["button"]) - self.removeRow(y) - del self.lists[key] - self.untag(items) - - def untag(self, items): - """Untag some items. - - Check first if the items are not used in any panel. - - @param items (list): items to be removed - """ - items_assigned = set() - for values in self.getItemsByKey().itervalues(): - items_assigned.update(values) - quick_list_manager.QuickTagList.untag(self, [item for item in items if item not in items_assigned]) - - def getItemsByKey(self): - """Get the items grouped by list name. - - @return dict{unicode: list} - """ - return {key: self.lists[key]["panel"].getItems() for key in self.lists} - - def getKeysByItem(self): - """Get the keys groups by item. - - @return dict{object: set(unicode)} - """ - result = {} - for key in self.lists: - for item in self.lists[key]["panel"].getItems(): - result.setdefault(item, set()).add(key) - return result - - def registerPopupMenuPanel(self, entries, callback): - """Register a popup menu panel for the list names' buttons. - - @param entries (dict{unicode: dict{unicode: unicode}}): menu entries - @param callback (callable): common callback for all menu items, arguments are: - - button widget - - list name (item key) - """ - self.popup_menu = base_panel.PopupMenuPanel(entries, callback=callback) - for key in self.lists: # register click sender for already existing lists - self.popup_menu.registerClickSender(self.lists[key]["button"]) - - -class TagsPanel(base_panel.ToggleStackPanel): - """A toggle panel to set the tags""" - - TAGS = _("Tags") - - STYLE = {"main": "tagsPanel-main", - "tags": "tagsPanel-tags"} - - def __init__(self, suggested_tags, tags=None): - """ - - @param suggested_tags (list[unicode]): list of all suggested tags - @param tags (list[unicode]): already assigned tags - """ - base_panel.ToggleStackPanel.__init__(self, Width="100%") - self.addStyleName(self.STYLE["main"]) - - if tags is None: - tags = [] - - self.tags = ListPanel(quick_list_manager.QuickTagList(suggested_tags), tags) - self.tags.addStyleName(self.STYLE["tags"]) - self.tags.ACCEPT_NEW_ENTRY = True - self.add(self.tags, self.TAGS) - self.addStackChangeListener(self) - - def onStackChanged(self, sender, index, visible=None): - if visible is None: - visible = sender.getWidget(index).getVisible() - text = ", ".join(self.getTags()) - suffix = "" if (visible or not text) else (": %s" % text) - sender.setStackText(index, self.TAGS + suffix) - - def getTags(self): - return self.tags.getItems() - - def setTags(self, items): - self.tags.resetItems(items) -
--- a/src/browser/sat_browser/logging.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,58 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2011-2018 Jérôme Poisson <goffi@goffi.org> - -# This program 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. - -# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. -"""This module configure logs for Libervia browser side""" - -from __pyjamas__ import console -from constants import Const as C -from sat.core import log # XXX: we don't use core.log_config here to avoid the impossible imports in pyjamas - - -class LiberviaLogger(log.Logger): - - def out(self, message, level=None): - try: - console - except: - # XXX: for older Firefox version, the displayed error is "libervia_main ReferenceError: console is not defined" - # but none of the following exception class is working: ReferenceError, TypeError, NameError, Exception... - # it works when you don't explicit a class, tested with Firefox 3.0.4 - print message - return - if level == C.LOG_LVL_DEBUG: - console.debug(message) - elif level == C.LOG_LVL_INFO: - console.info(message) - elif level == C.LOG_LVL_WARNING: - console.warn(message) - else: - console.error(message) - - -def configure(): - fmt = '[%(name)s] %(message)s' - log.configure(C.LOG_BACKEND_CUSTOM, - logger_class = LiberviaLogger, - level = C.LOG_LVL_DEBUG, - fmt = fmt, - output = None, - logger = None, - colors = False, - force_colors = False) - # FIXME: workaround for Pyjamas, need to be removed when Pyjamas is fixed - LiberviaLogger.fmt = fmt
--- a/src/browser/sat_browser/main_panel.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,314 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2011-2018 Jérôme Poisson <goffi@goffi.org> - -# This program 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. - -# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. - -"""Panels used as main basis""" - -import pyjd # this is dummy in pyjs -from sat.core.log import getLogger -log = getLogger(__name__) - -from sat.core.i18n import _ -from sat_browser import strings - -from pyjamas.ui.DockPanel import DockPanel -from pyjamas.ui.HorizontalPanel import HorizontalPanel -from pyjamas.ui.VerticalPanel import VerticalPanel -from pyjamas.ui.Button import Button -from pyjamas.ui.HTML import HTML -from pyjamas.ui.ClickListener import ClickHandler -from pyjamas.Timer import Timer -from pyjamas.ui import HasVerticalAlignment - - -import menu -import dialog -import base_widget -import base_menu -import libervia_widget -import editor_widget -import html_tools -from constants import Const as C - - -### Warning notification (visibility of message, and other warning data) ### - - -class WarningPopup(): - - def __init__(self): - self._popup = None - self._timer = Timer(notify=self._timeCb) - self.timeout = None - self._html = None - self._last_type = None - self._last_html = None - - def showWarning(self, type_=None, msg=None, duration=2000): - """Display a popup information message, e.g. to notify the recipient of a message being composed. - - If type_ is None, a popup being currently displayed will be hidden. - @type_: a type determining the CSS style to be applied (see _showWarning) - @msg: message to be displayed - @duration(int, None): time (in ms) to display the message - """ - if type_ is None: - self._removeWarning() - return - - self.timeout = duration - - if not self._popup or self._last_type != type_ or self._last_html != msg: - self._showWarning(type_, msg) - - def _showWarning(self, type_, msg): - """Display a popup information message, e.g. to notify the recipient of a message being composed. - - @type_: a type determining the CSS style to be applied. For now the defined styles are - "NONE" (will do nothing), "PUBLIC", "GROUP", "STATUS" and "ONE2ONE". - @msg: message to be displayed - """ - if type_ == "NONE": - return - if not msg: - log.warning("no msg set") - return - if type_ == "PUBLIC": - style = "targetPublic" - elif type_ == "GROUP": - style = "targetGroup" - elif type_ == "STATUS": - style = "targetStatus" - elif type_ == "ONE2ONE": - style = "targetOne2One" - elif type_ == "INFO": - style = "notifInfo" - elif type_ == "WARNING": - style = "notifWarning" - else: - log.error("unknown message type") - return - - self._last_html = msg - - if self._popup is None: - self._popup = dialog.PopupPanelWrapper(autoHide=False, modal=False) - self._html = HTML(msg) - self._popup.add(self._html) - - left = 0 - top = 0 # max(0, self.getAbsoluteTop() - contents.getOffsetHeight() - 2) - self._popup.setPopupPosition(left, top) - self._popup.show() - else: - self._html.setHTML(msg) - - if type_ != self._last_type: - self._last_type = type_ - self._popup.setStyleName("warningPopup") - self._popup.addStyleName(style) - - if self.timeout is not None: - self._timer.schedule(self.timeout) - - def _timeCb(self, timer): - if self._popup: - self._popup.hide() - self._popup = None - - def _removeWarning(self): - """Remove the popup""" - self._timer.cancel() - self._timeCb(None) - - -### Status ### - - -class StatusPanel(editor_widget.HTMLTextEditor): - - EMPTY_STATUS = '<click to set a status>' - VALIDATE_WITH_SHIFT_ENTER = False - - def __init__(self, host, status=''): - self.host = host - modifiedCb = lambda content: self.host.bridge.call('setStatus', None, self.host.presence_status_panel.presence, content['text']) or True - editor_widget.HTMLTextEditor.__init__(self, {'text': status}, modifiedCb, options={'no_xhtml': True, 'listen_focus': True, 'listen_click': True}) - self.edit(False) - self.setStyleName('marginAuto') - - @property - def status(self): - return self._original_content['text'] - - def __cleanContent(self, content): - status = content['text'] - if status == self.EMPTY_STATUS or status in C.PRESENCE.values(): - content['text'] = '' - return content - - def getContent(self): - return self.__cleanContent(editor_widget.HTMLTextEditor.getContent(self)) - - def setContent(self, content): - content = self.__cleanContent(content) - editor_widget.BaseTextEditor.setContent(self, content) - - def setDisplayContent(self): - status = self._original_content['text'] - try: - presence = self.host.presence_status_panel.presence - except AttributeError: # during initialization - presence = None - if not status: - if presence and presence in C.PRESENCE: - status = C.PRESENCE[presence] - else: - status = self.EMPTY_STATUS - self.display.setHTML(strings.addURLToText(status)) - - -class PresenceStatusMenuBar(base_widget.WidgetMenuBar): - def __init__(self, parent): - styles = {'menu_bar': 'presence-button'} - base_widget.WidgetMenuBar.__init__(self, parent, parent.host, styles=styles) - self.button = self.addCategory(u"◉") - presence_menu = self.button.getSubMenu() - for presence, presence_i18n in C.PRESENCE.items(): - html = u'<span class="%s">◉</span> %s' % (html_tools.buildPresenceStyle(presence), presence_i18n) - presence_menu.addItem(html, True, base_menu.SimpleCmd(lambda presence=presence: self.changePresenceCb(presence))) - self.parent_panel = parent - - def changePresenceCb(self, presence=''): - """Callback to notice the backend of a new presence set by the user. - @param presence (unicode): the new presence is a value in ('', 'chat', 'away', 'dnd', 'xa') - """ - self.host.bridge.call('setStatus', None, presence, self.parent_panel.status_panel.status) - - @classmethod - def getCategoryHTML(cls, category): - """Build the html to be used for displaying a category item. - - @param category (quick_menus.MenuCategory): category to add - @return unicode: HTML to display - """ - return category - - -class PresenceStatusPanel(HorizontalPanel, ClickHandler): - - def __init__(self, host, presence="", status=""): - self.host = host - self.plugin_menu_context = [] - HorizontalPanel.__init__(self, Width='100%') - self.presence_bar = PresenceStatusMenuBar(self) - self.status_panel = StatusPanel(host, status=status) - self.setPresence(presence) - - panel = HorizontalPanel() - panel.add(self.presence_bar) - panel.add(self.status_panel) - panel.setCellVerticalAlignment(self.presence_bar, 'baseline') - panel.setCellVerticalAlignment(self.status_panel, 'baseline') - panel.setStyleName("presenceStatusPanel") - self.add(panel) - - self.status_panel.edit(False) - - ClickHandler.__init__(self) - self.addClickListener(self) - - @property - def presence(self): - return self._presence - - @property - def status(self): - return self.status_panel._original_content['text'] - - def setPresence(self, presence): - self._presence = presence - html_tools.setPresenceStyle(self.presence_bar.button, self._presence) - - def setStatus(self, status): - self.status_panel.setContent({'text': status}) - self.status_panel.setDisplayContent() - - def onClick(self, sender): - # As status is the default target of uniBar, we don't want to select anything if click on it - self.host.setSelected(None) - - -### Panels managing the main area ### - - -class MainPanel(DockPanel): - """The panel which take the whole screen""" - - def __init__(self, host): - self.host = host - DockPanel.__init__(self, StyleName="mainPanel liberviaTabPanel") - - # menu and status panel - self.header = VerticalPanel(StyleName="header") - self.menu = menu.MainMenuBar(host) - self.header.add(self.menu) - - # contacts - self.contacts_switch = Button(u'«', self._contactsSwitch) - self.contacts_switch.addStyleName('contactsSwitch') - - # tab panel - self.tab_panel = libervia_widget.MainTabPanel(host) - self.tab_panel.addWidgetsTab(_(u"Discussions"), select=True, locked=True) - - # XXX: widget's addition order is important! - self.add(self.header, DockPanel.NORTH) - self.add(self.tab_panel, DockPanel.CENTER) - self.setCellWidth(self.tab_panel, '100%') - self.setCellHeight(self.tab_panel, '100%') - self.add(self.tab_panel.getTabBar(), DockPanel.SOUTH) - - def addContactList(self, contact_list): - self.add(self.contacts_switch, DockPanel.WEST) - self.add(contact_list, DockPanel.WEST) - - def addPresenceStatusPanel(self, panel): - self.header.add(panel) - self.header.setCellHeight(panel, '100%') - self.header.setCellVerticalAlignment(panel, HasVerticalAlignment.ALIGN_BOTTOM) - - def _contactsSwitch(self, btn=None): - """ (Un)hide contacts panel """ - if btn is None: - btn = self.contacts_switch - clist = self.host.contact_list_widget - clist.setVisible(not clist.getVisible()) - btn.setText(u"«" if clist.getVisible() else u"»") - self.host.resize() - - def _contactsMove(self, parent): - """Move the contacts container (containing the contact list and - the "hide/show" button) to another parent, but always as the - first child position (insert at index 0). - """ - if self._contacts.getParent(): - if self._contacts.getParent() == parent: - return - self._contacts.removeFromParent() - parent.insert(self._contacts, 0)
--- a/src/browser/sat_browser/menu.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,177 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2011-2018 Jérôme Poisson <goffi@goffi.org> - -# This program 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. - -# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. - -import pyjd # this is dummy in pyjs -from sat.core.log import getLogger -log = getLogger(__name__) - -from pyjamas.ui.HTML import HTML -from pyjamas.ui.Frame import Frame - -from constants import Const as C -import file_tools -import xmlui -import chat -import dialog -import contact_group -import base_menu -from sat_browser import html_tools -from sat_browser import web_widget - - -unicode = str # FIXME: pyjamas workaround - - -class MainMenuBar(base_menu.GenericMenuBar): - """The main menu bar which is displayed on top of the document""" - - ITEM_TPL = "<img src='media/icons/menu/%s_menu_red.png' />%s" - - def __init__(self, host): - styles = {'moved_popup': 'menuLastPopup', 'menu_bar': 'mainMenuBar'} - base_menu.GenericMenuBar.__init__(self, host, vertical=False, styles=styles) - - @classmethod - def getCategoryHTML(cls, category): - """Build the html to be used for displaying a category item. - - @param category (quick_menus.MenuCategory): category to add - @return unicode: HTML to display - """ - name = html_tools.html_sanitize(category.name) - return cls.ITEM_TPL % (category.icon, name) if category.icon is not None else name - - ## callbacks - - # General menu - - def onDisconnect(self): - def confirm_cb(answer): - if answer: - self.host.disconnect(C.PROF_KEY_NONE) - _dialog = dialog.ConfirmDialog(confirm_cb, text="Do you really want to disconnect ?") - _dialog.show() - - #Contact menu - - def onManageContactGroups(self): - """Open the contact groups manager.""" - - def onCloseCallback(): - pass - - contact_group.ContactGroupEditor(self.host, None, onCloseCallback) - - #Group menu - def onJoinRoom(self): - - def invite(room_jid, contacts): - for contact in contacts: - self.host.bridge.call('inviteMUC', None, unicode(contact), unicode(room_jid)) - - def join(room_jid, contacts): - if self.host.whoami: - nick = self.host.whoami.node - contact_list = self.host.contact_list - if room_jid is None or room_jid not in contact_list.getSpecials(C.CONTACT_SPECIAL_GROUP): - room_jid_s = unicode(room_jid) if room_jid else '' - self.host.bridge.joinMUC(room_jid_s, nick, profile=C.PROF_KEY_NONE, callback=lambda room_jid: invite(room_jid, contacts), errback=self.host.onJoinMUCFailure) - else: - self.host.displayWidget(chat.Chat, room_jid, type_="group") - invite(room_jid, contacts) - - dialog.RoomAndContactsChooser(self.host, join, ok_button="Join", visible=(True, False)) - - - # Help menu - - def onOfficialChatRoom(self): - nick = self.host.whoami.node - self.host.bridge.joinMUC(self.host.default_muc, nick, profile=C.PROF_KEY_NONE, callback=lambda dummy: None, errback=self.host.onJoinMUCFailure) - - def onSocialContract(self): - _frame = Frame('contrat_social.html') - _frame.setStyleName('infoFrame') - _dialog = dialog.GenericDialog("Contrat Social", _frame) - _dialog.setSize('80%', '80%') - _dialog.show() - - def onAbout(self): - def gotVersions(): - _about = HTML("""<b>Libervia</b>, a Salut à Toi project<br /> - <br /> - Libervia is a web frontend for Salut à Toi<br /> - <span style='font-style: italic;'>SàT version:</span> {sat_version}<br/> - <span style='font-style: italic;'>Libervia version:</span> {libervia_version}<br/> - <br /> - You can contact the authors at <a href="mailto:contact@salut-a-toi.org">contact@salut-a-toi.org</a><br /> - Blog available (mainly in french) at <a href="http://www.goffi.org" target="_blank">http://www.goffi.org</a><br /> - Project page: <a href="http://salut-a-toi.org"target="_blank">http://salut-a-toi.org</a><br /> - <br /> - Any help welcome :) - <p style='font-size:small;text-align:center'>This project is dedicated to Roger Poisson</p> - """.format(sat_version=self.host.sat_version, libervia_version=self.host.libervia_version)) - _dialog = dialog.GenericDialog("About", _about) - _dialog.show() - self.host.getVersions(gotVersions) - - #Settings menu - - def onAccount(self): - def gotUI(xml_ui): - if not xml_ui: - return - body = xmlui.create(self.host, xml_ui) - _dialog = dialog.GenericDialog("Manage your account", body, options=['NO_CLOSE']) - body.setCloseCb(_dialog.close) - _dialog.show() - self.host.bridge.call('getAccountDialogUI', gotUI) - - def onParameters(self): - def gotParams(xml_ui): - if not xml_ui: - return - body = xmlui.create(self.host, xml_ui) - _dialog = dialog.GenericDialog("Parameters", body, options=['NO_CLOSE']) - _dialog.addStyleName("parameters") - body.setCloseCb(_dialog.close) - _dialog.setSize('80%', '80%') - _dialog.show() - self.host.bridge.getParamsUI(profile=C.PROF_KEY_NONE, callback=gotParams) - - def removeItemParams(self): - """Remove the Parameters item from the Settings menu bar.""" - self.menu_settings.removeItem(self.item_params) - - def onAvatarUpload(self): - body = file_tools.AvatarUpload() - _dialog = dialog.GenericDialog("Avatar upload", body, options=['NO_CLOSE']) - body.setCloseCb(_dialog.close) - _dialog.setWidth('40%') - _dialog.show() - - def onPublicBlog(self, contact_box, data, profile): - # FIXME: Q&D way to check domain, need to be done in a cleaner way - if contact_box.jid.domain != self.host._defaultDomain: - self.host.showDialog(u"Public blogs from other domains are not managed yet", "Can't show public blog", "error") - return - - url = '{}/blog/{}'.format(self.host.base_location, contact_box.jid.node) - widget = self.host.displayWidget(web_widget.WebWidget, url, show_url=False) - self.host.setSelected(widget)
--- a/src/browser/sat_browser/nativedom.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,104 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2011-2018 Jérôme Poisson <goffi@goffi.org> - -# This program 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. - -# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. - -""" -This class provide basic DOM parsing based on native javascript parser -__init__ code comes from Tim Down at http://stackoverflow.com/a/8412989 -""" - -from __pyjamas__ import JS - - -class Node(object): - - def __init__(self, js_node): - self._node = js_node - - def _jsNodesList2List(self, js_nodes_list): - ret = [] - for i in range(len(js_nodes_list)): - #ret.append(Element(js_nodes_list.item(i))) - ret.append(self.__class__(js_nodes_list.item(i))) # XXX: Ugly, but used to work around a Pyjamas's bug - return ret - - def __getattr__(self, name): - if name in ('TEXT_NODE', 'ELEMENT_NODE', 'ATTRIBUTE_NODE', 'COMMENT_NODE', 'nodeName', 'nodeType', 'wholeText'): - return getattr(self._node, name) - return object.__getattribute__(self, name) - - @property # XXX: doesn't work in --strict mode in pyjs - def childNodes(self): - return self._jsNodesList2List(self._node.childNodes) - - def getAttribute(self, attr): - return self._node.getAttribute(attr) - - def setAttribute(self, attr, value): - return self._node.setAttribute(attr, value) - - def hasAttribute(self, attr): - return self._node.hasAttribute(attr) - - def toxml(self): - return JS("""this._node.outerHTML || new XMLSerializer().serializeToString(this._node);""") - - -class Element(Node): - - def __init__(self, js_node): - Node.__init__(self, js_node) - - def getElementsByTagName(self, tagName): - return self._jsNodesList2List(self._node.getElementsByTagName(tagName)) - - -class Document(Node): - - def __init__(self, js_document): - Node.__init__(self, js_document) - - @property - def documentElement(self): - return Element(self._node.documentElement) - - -class NativeDOM: - - def __init__(self): - JS(""" - - if (typeof window.DOMParser != "undefined") { - this.parseXml = function(xmlStr) { - return ( new window.DOMParser() ).parseFromString(xmlStr, "text/xml"); - }; - } else if (typeof window.ActiveXObject != "undefined" && - new window.ActiveXObject("Microsoft.XMLDOM")) { - this.parseXml = function(xmlStr) { - var xmlDoc = new window.ActiveXObject("Microsoft.XMLDOM"); - xmlDoc.async = "false"; - xmlDoc.loadXML(xmlStr); - return xmlDoc; - }; - } else { - throw new Error("No XML parser found"); - } - """) - - def parseString(self, xml): - return Document(self.parseXml(xml))
--- a/src/browser/sat_browser/notification.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,150 +0,0 @@ -from __pyjamas__ import JS, wnd -from sat.core.log import getLogger -log = getLogger(__name__) -from sat.core.i18n import _ - -from pyjamas import Window -from pyjamas.Timer import Timer -import favico.min.js - -import dialog - -TIMER_DELAY = 5000 - - -class Notification(object): - """ - If the browser supports it, the user allowed it to and the tab is in the - background, send desktop notifications on messages. - - Requires both Web Notifications and Page Visibility API. - """ - - def __init__(self, alerts_counter): - """ - - @param alerts_counter (FaviconCounter): counter instance - """ - self.alerts_counter = alerts_counter - self.enabled = False - user_agent = None - notif_permission = None - JS(""" - if (!('hidden' in document)) - document.hidden = false; - - user_agent = navigator.userAgent - - if (!('Notification' in window)) - return; - - notif_permission = Notification.permission - - if (Notification.permission === 'granted') - this.enabled = true; - - else if (Notification.permission === 'default') { - Notification.requestPermission(function(permission){ - if (permission !== 'granted') - return; - - self.enabled = true; //need to use self instead of this - }); - } - """) - - if "Chrome" in user_agent and notif_permission not in ['granted', 'denied']: - self.user_agent = user_agent - self._installChromiumWorkaround() - - wnd().onfocus = self.onFocus - # wnd().onblur = self.onBlur - - def _installChromiumWorkaround(self): - # XXX: Workaround for Chromium behaviour, it's doens't manage requestPermission on onLoad event - # see https://code.google.com/p/chromium/issues/detail?id=274284 - # FIXME: need to be removed if Chromium behaviour changes - try: - version_full = [s for s in self.user_agent.split() if "Chrome" in s][0].split('/')[1] - version = int(version_full.split('.')[0]) - except (IndexError, ValueError): - log.warning("Can't find Chromium version") - version = 0 - log.info("Chromium version: %d" % (version,)) - if version < 22: - log.info("Notification use the old prefixed version or are unmanaged") - return - if version < 32: - dialog.InfoDialog(_("Notifications activation for Chromium"), _('You need to activate notifications manually for your Chromium version.<br/>To activate notifications, click on the favicon on the left of the address bar')).show() - return - - log.info("==> Installing Chromium notifications request workaround <==") - self._old_click = wnd().onclick - wnd().onclick = self._chromiumWorkaround - - def _chromiumWorkaround(self): - log.info("Activating workaround") - JS(""" - Notification.requestPermission(function(permission){ - if (permission !== 'granted') - return; - self.enabled = true; //need to use self instead of this - }); - """) - wnd().onclick = self._old_click - - def onFocus(self, event=None): - self.alerts_counter.update(extra=0) - - # def onBlur(self, event=None): - # pass - - def isHidden(self): - JS("""return document.hidden;""") - - def _notify(self, title, body, icon): - if not self.enabled: - return - notification = None - # FIXME: icon has been removed because the notification can't display a HTTPS file - JS(""" - notification = new Notification(title, {body: body}); - // Probably won’t work, but it doesn’t hurt to try. - notification.addEventListener('click', function() { - window.focus(); - }); - """) - notification.onshow = lambda: Timer(TIMER_DELAY, lambda timer: notification.close()) - - def notify(self, title, body, icon='/media/icons/apps/48/sat.png'): - if self.isHidden(): - self._notify(title, body, icon) - - -class FaviconCounter(object): - """Display numbers over the favicon to signal e.g. waiting messages""" - - def __init__(self): - # XXX: the file favico.min.js is loaded from public/libervia.html because I get NS_ERROR_FAILURE when it's loaded with Pyjamas. It sounds like a context issue, with the favicon not being found. - - JS(""" - self.counter = new top.Favico({ - animation : 'slide', - bgColor: '#5CB85C', - }); - """) - - self.count = 0 # messages that are not displayed - self.extra = 0 # messages that are displayed but the window is hidden - - def update(self, count=None, extra=None): - """Update the favicon counter. - - @param count (int): primary counter - @param extra (int): extra counter - """ - if count is not None: - self.count = count - if extra is not None: - self.extra = extra - self.counter.badge(self.count + self.extra)
--- a/src/browser/sat_browser/otrjs_wrapper.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,273 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia wrapper for otr.js -# Copyright (C) 2009-2018 Jérôme Poisson (goffi@goffi.org) -# Copyright (C) 2013-2016 Adrien Cossa (souliane@mailoo.org) - -# This program 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. - -# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. - -"""This file is a wrapper for otr.js. It partially reproduces the usage -(modules, classes and attributes names) and behavior of potr, so you -can easily adapt some code based on potr to Pyjamas applications. - -potr is released under the GNU LESSER GENERAL PUBLIC LICENSE Version 3 - - https://github.com/python-otr/pure-python-otr/blob/master/LICENSE - -otr.js is released under the Mozilla Public Licence Version 2.0 - - https://github.com/arlolra/otr/blob/master/license -""" - -from __pyjamas__ import JS - -# should you re-use this class outside SàT, you can import __pyjamas__.console as log instead -from sat.core.log import getLogger -log = getLogger(__name__) - - -# XXX: pyjamas can't probably import more than one JS file, it messes the order. -# XXX: pyjamas needs the file to be in the compilation directory - no submodule. -# XXX: pyjamas needs the imported file to end with a empty line or semi-column. -# FIXME: fix these bugs upstream in Pyjamas -import otr.min.js - - -def isSupported(): - JS("""return (typeof OTR !== 'undefined');""") - - -if not isSupported(): - # see https://developer.mozilla.org/en-US/docs/Web/API/window.crypto.getRandomValues#Browser_Compatibility - log.error('Your browser is not implementing CSPRNG: OTR has been disabled.') - raise ImportError('CSPRNG is not supported by your browser') - - -class context(object): - - # Pre-declare these attributes to avoid the pylint "undefined variable" errors - STATUS_SEND_QUERY = None - STATUS_AKE_INIT = None - STATUS_AKE_SUCCESS = None - STATUS_END_OTR = None - STATE_PLAINTEXT = None - STATE_ENCRYPTED = None - STATE_FINISHED = None - OTR_TAG = None - OTR_VERSION_2 = None - OTR_VERSION_3 = None - WHITESPACE_TAG = None - WHITESPACE_TAG_V2 = None - WHITESPACE_TAG_V3 = None - - JS(""" - $cls_definition['STATUS_SEND_QUERY'] = OTR.CONST.STATUS_SEND_QUERY; - $cls_definition['STATUS_AKE_INIT'] = OTR.CONST.STATUS_AKE_INIT; - $cls_definition['STATUS_AKE_SUCCESS'] = OTR.CONST.STATUS_AKE_SUCCESS; - $cls_definition['STATUS_END_OTR'] = OTR.CONST.STATUS_END_OTR; - $cls_definition['STATE_PLAINTEXT'] = OTR.CONST.MSGSTATE_PLAINTEXT; - $cls_definition['STATE_ENCRYPTED'] = OTR.CONST.MSGSTATE_ENCRYPTED; - $cls_definition['STATE_FINISHED'] = OTR.CONST.MSGSTATE_FINISHED; - $cls_definition['OTR_TAG'] = OTR.CONST.OTR_TAG; - $cls_definition['OTR_VERSION_2'] = OTR.CONST.OTR_VERSION_2; - $cls_definition['OTR_VERSION_3'] = OTR.CONST.OTR_VERSION_3; - $cls_definition['WHITESPACE_TAG'] = OTR.CONST.WHITESPACE_TAG; - $cls_definition['WHITESPACE_TAG_V2'] = OTR.CONST.WHITESPACE_TAG_V2; - $cls_definition['WHITESPACE_TAG_V3'] = OTR.CONST.WHITESPACE_TAG_V3; - """) - - class UnencryptedMessage(Exception): - pass - - class Context(object): - - def __init__(self, account, peername): - self.user = account - self.peer = peername - self.trustName = self.peer - options = {'fragment_size': 140, - 'send_interval': 200, - 'priv': account.getPrivkey(), # this would generate the account key if it hasn't been done yet - 'debug': False, - } - JS("""self.otr = new OTR(options);""") - - for policy in ('ALLOW_V2', 'ALLOW_V3', 'REQUIRE_ENCRYPTION'): - setattr(self.otr, policy, self.getPolicy(policy)) - - self.otr.on('ui', self.receiveMessageCb) - self.otr.on('io', self.sendMessageCb) - self.otr.on('error', self.messageErrorCb) - self.otr.on('status', lambda status: self.setStateCb(self.otr.msgstate, status)) - self.otr.on('smp', self.smpAuthCb) - - @property - def state(self): - return self.otr.msgstate - - @state.setter - def state(self, state): - self.otr.msgstate = state - - def getCurrentKey(self): - return self.otr.their_priv_pk - - def setTrust(self, fingerprint, trustLevel): - self.user.setTrust(self.trustName, fingerprint, trustLevel) - - def setCurrentTrust(self, trustLevel): - self.setTrust(self.otr.their_priv_pk.fingerprint(), trustLevel) - - def getTrust(self, fingerprint, default=None): - return self.user.getTrust(self.trustName, fingerprint, default) - - def getCurrentTrust(self): - # XXX: the docstring of potr for the return value of this method is incorrect - if self.otr.their_priv_pk is None: - return None - return self.getTrust(self.otr.their_priv_pk.fingerprint(), None) - - def getUsedVersion(self): - """Return the otr version that is beeing used""" - # this method doesn't exist in potr, it has been added for convenience - try: - return self.otr.ake.otr_version - except AttributeError: - return None - - def disconnect(self): - self.otr.endOtr() - - def finish(self): - """Finish the session - avoid to send any message and the user has to manually disconnect""" - # it means TLV of type 1 (two first bytes), message length 0 (2 last bytes) - self.otr.handleTLVs('\x00\x01\x00\x00') - - def receiveMessage(self, msg): - """Received a message, ask otr.js to (try to) decrypt it""" - self.otr.receiveMsg(msg) - - def sendMessage(self, msg): - """Ask otr.js to encrypt a message for sending""" - self.otr.sendMsg(msg) - - def sendQueryMessage(self): - """Start or refresh an encryption communication""" - # otr.js offers this method, with potr you have to build the query message yourself - self.otr.sendQueryMsg() - - def inject(self, msg, appdata=None): - return self.sendMessageCb(msg, appdata) - - def getPolicy(self, key): - raise NotImplementedError - - def smpAuthSecret(self, secret, question=None): - return self.otr.smpSecret(secret, question) - - def smpAuthAbort(self, act=None): - # XXX: dirty hack to let the triggered method know who aborted the - # authentication. We need it to display the proper feedback and, - # if the correspondent aborted, set the conversation 'unverified'. - self.otr.sm.init() - JS("""self.otr.sm.sendMsg(OTR.HLP.packTLV(6, ''))""") - self.smpAuthCb('abort', '', act) - - def sendMessageCb(self, msg, meta): - """Actually send the message after it's been encrypted""" - raise NotImplementedError - - def receiveMessageCb(self, msg, encrypted): - """Display the message after it's been eventually decrypted""" - raise NotImplementedError - - def messageErrorCb(self, error): - """Message error callback""" - raise NotImplementedError - - def setStateCb(self, newstate): - raise NotImplementedError - - def smpAuthCb(self, newstate): - raise NotImplementedError - - class Account(object): - - def __init__(self, host): - self.host = host - self.privkey = None - self.trusts = {} - - def getPrivkey(self): - # the return value must have a method serializePrivateKey() - # if the key is not saved yet, call savePrivkey to generate it - if self.privkey is None: - self.privkey = self.loadPrivkey() - if self.privkey is None: - JS("""self.privkey = new DSA();""") - self.savePrivkey() - return self.privkey - - def setTrust(self, key, fingerprint, trustLevel): - if key not in self.trusts: - self.trusts[key] = {} - self.trusts[key][fingerprint] = trustLevel - self.saveTrusts() - - def getTrust(self, key, fingerprint, default=None): - if key not in self.trusts: - return default - return self.trusts[key].get(fingerprint, default) - - def loadPrivkey(self): - raise NotImplementedError - - def savePrivkey(self): - raise NotImplementedError - - def saveTrusts(self): - raise NotImplementedError - - -class crypt(object): - - class PK(object): - - def parsePrivateKey(self, key): - JS("""return DSA.parsePrivate(key);""") - - -class proto(object): - - @classmethod - def checkForOTR(cls, body): - """Helper method to check if the message contains OTR starting tag or whitespace - - @return: - - context.OTR_TAG if the message starts with it - - context.WHITESPACE_TAG if the message contains OTR whitespaces - - None otherwise - """ - if body.startswith(context.OTR_TAG): - return context.OTR_TAG - index = body.find(context.WHITESPACE_TAG) - if index < 0: - return False - tags = [body[i:i + 8] for i in range(index, len(body), 8)] - if [True for tag in tags if tag in (context.WHITESPACE_TAG_V2, context.WHITESPACE_TAG_V3)]: - return context.WHITESPACE_TAG - return None - - -# serialazePrivateKey is the method name in potr -JS("""DSA.serializePrivateKey = DSA.packPrivate;""")
--- a/src/browser/sat_browser/plugin_sec_otr.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,612 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia plugin for OTR encryption -# Copyright (C) 2009-2018 Jérôme Poisson (goffi@goffi.org) -# Copyright (C) 2013-2016 Adrien Cossa (souliane@mailoo.org) - -# This program 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. - -# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. - -""" -This file is adapted from sat.plugins.plugin.sec_otr. It offers browser-side OTR encryption using otr.js. -The text messages to display are mostly taken from the Pidgin OTR plugin (GPL 2.0, see http://otr.cypherpunks.ca). -""" - -from sat.core.log import getLogger -log = getLogger(__name__) - -from sat.core.i18n import _, D_ -from sat.core import exceptions -from sat.tools import trigger - -from constants import Const as C -from sat_frontends.tools import jid -import otrjs_wrapper as otr -import dialog -import chat -import uuid -import time - - -NS_OTR = "otr_plugin" -PRIVATE_KEY = "PRIVATE KEY" -MAIN_MENU = D_('OTR') # TODO: get this constant directly from backend's plugin -DIALOG_EOL = "<br />" - -AUTH_TRUSTED = D_("Verified") -AUTH_UNTRUSTED = D_("Unverified") -AUTH_OTHER_TITLE = D_("Authentication of {jid}") -AUTH_US_TITLE = D_("Authentication to {jid}") -AUTH_TRUST_NA_TITLE = D_("Authentication requirement") -AUTH_TRUST_NA_TXT = D_("You must start an OTR conversation before authenticating your correspondent.") -AUTH_INFO_TXT = D_("Authenticating a correspondent helps ensure that the person you are talking to is who he or she claims to be.{eol}{eol}") -AUTH_FINGERPRINT_YOURS = D_("Your fingerprint is:{eol}{fingerprint}{eol}{eol}Start an OTR conversation to have your correspondent one.") -AUTH_FINGERPRINT_TXT = D_("To verify the fingerprint, contact your correspondent via some other authenticated channel (i.e. not in this chat), such as the telephone or GPG-signed email. Each of you should tell your fingerprint to the other.{eol}{eol}") -AUTH_FINGERPRINT_VERIFY = D_("Fingerprint for you, {you}:{eol}{your_fp}{eol}{eol}Purported fingerprint for {other}:{eol}{other_fp}{eol}{eol}Did you verify that this is in fact the correct fingerprint for {other}?") -AUTH_QUEST_DEFINE_TXT = D_("To authenticate using a question, pick a question whose answer is known only to you and your correspondent. Enter this question and this answer, then wait for your correspondent to enter the answer too. If the answers don't match, then you may be talking to an imposter.{eol}{eol}") -AUTH_QUEST_DEFINE = D_("Enter question here:{eol}") -AUTH_QUEST_ANSWER_TXT = D_("Your correspondent is attempting to determine if he or she is really talking to you, or if it's someone pretending to be you. Your correspondent has asked a question, indicated below. To authenticate to your correspondent, enter the answer and click OK.{eol}{eol}") -AUTH_QUEST_ANSWER = D_("This is the question asked by your correspondent:{eol}{question}") -AUTH_SECRET_INPUT = D_("{eol}{eol}Enter secret answer here: (case sensitive){eol}") -AUTH_ABORTED_TXT = D_("Authentication aborted.") -AUTH_FAILED_TXT = D_("Authentication failed.") -AUTH_OTHER_OK = D_("Authentication successful.") -AUTH_US_OK = D_("Your correspondent has successfully authenticated you.") -AUTH_OTHER_TOO = D_("You may want to authenticate your correspondent as well by asking your own question.") -AUTH_STATUS = D_("The current conversation is now {state}.") - -FINISHED_CONTEXT_TITLE = D_('Finished OTR conversation with {jid}') -SEND_PLAIN_IN_FINISHED_CONTEXT = D_("Your message was not sent because your correspondent closed the OTR conversation on his/her side. Either close your own side, or refresh the session.") -RECEIVE_PLAIN_IN_ENCRYPTED_CONTEXT = D_("WARNING: received unencrypted data in a supposedly encrypted context!") - -QUERY_ENCRYPTED = D_('Attempting to refresh the OTR conversation with {jid}...') -QUERY_NOT_ENCRYPTED = D_('Attempting to start an OTR conversation with {jid}...') -AKE_ENCRYPTED = D_(" conversation with {jid} started. Your client is not logging this conversation.") -AKE_NOT_ENCRYPTED = D_("ERROR: successfully ake'd with {jid} but the conversation is not encrypted!") -END_ENCRYPTED = D_("ERROR: the OTR session ended but the context is still supposedly encrypted!") -END_PLAIN_NO_MORE = D_("Your conversation with {jid} is no more encrypted.") -END_PLAIN_HAS_NOT = D_("Your conversation with {jid} hasn't been encrypted.") -END_FINISHED = D_("{jid} has ended his or her private conversation with you; you should do the same.") - -KEY_TITLE = D_('Private key') -KEY_NA_TITLE = D_("No private key") -KEY_NA_TXT = D_("You don't have any private key yet.") -KEY_DROP_TITLE = D_('Drop your private key') -KEY_DROP_TXT = D_("You private key is used to encrypt messages for your correspondent, nobody except you must know it, if you are in doubt, you should drop it!{eol}{eol}Are you sure you want to drop your private key?") -KEY_DROPPED_TXT = D_("Your private key has been dropped.") - -QUERY_TITLE = D_("Going encrypted") -QUERY_RECEIVED = D_("{jid} is willing to start with you an OTR encrypted conversation.{eol}{eol}") -QUERY_SEND = D_("You are about to start an OTR encrypted conversation with {jid}.{eol}{eol}") -QUERY_SLOWDOWN = D_("This end-to-end encryption is computed by your web browser and you may experience slowdowns.{eol}{eol}") -QUERY_NO_KEY = D_("This will take up to 10 seconds to generate your single use private key and start the conversation. In a future version of Libervia, your private key will be safely and persistently stored, so you will have to generate it only once.{eol}{eol}") -QUERY_KEY = D_("You already have a private key, but to start the conversation will still require a couple of seconds.{eol}{eol}") -QUERY_CONFIRM = D_("Press OK to start now the encryption.") - -ACTION_NA_TITLE = D_("Impossible action") -ACTION_NA = D_("Your correspondent must be connected to start an OTR conversation with him.") - -DEFAULT_POLICY_FLAGS = { - 'ALLOW_V2': True, - 'ALLOW_V3': True, - 'REQUIRE_ENCRYPTION': False, - 'SEND_WHITESPACE_TAG': False, # FIXME: we need to complete sendMessageTg before turning this to True - 'WHITESPACE_START_AKE': False, # FIXME: we need to complete newMessageTg before turning this to True -} - -# list a couple of texts or htmls (untrusted, trusted) for each state -OTR_MSG_STATES = { - otr.context.STATE_PLAINTEXT: [ - '<img src="media/icons/silk/lock_open.png" /><img src="media/icons/silk/key_delete.png" />', - '<img src="media/icons/silk/lock_open.png" /><img src="media/icons/silk/key.png" />' - ], - otr.context.STATE_ENCRYPTED: [ - '<img src="media/icons/silk/lock.png" /><img src="media/icons/silk/key_delete.png" />', - '<img src="media/icons/silk/lock.png" /><img src="media/icons/silk/key.png" />' - ], - otr.context.STATE_FINISHED: [ - '<img src="media/icons/silk/lock_break.png" /><img src="media/icons/silk/key_delete.png" />', - '<img src="media/icons/silk/lock_break.png" /><img src="media/icons/silk/key.png" />' - ] -} - - -unicode = str # FIXME: pyjamas workaround - - -class NotConnectedEntity(Exception): - pass - - -class Context(otr.context.Context): - - def __init__(self, host, account, other_jid): - """ - - @param host (satWebFrontend) - @param account (Account) - @param other_jid (jid.JID): JID of the person your chat correspondent - """ - super(Context, self).__init__(account, other_jid) - self.host = host - - def getPolicy(self, key): - """Get the value of the specified policy - - @param key (unicode): a value in: - - ALLOW_V1 (apriori removed from otr.js) - - ALLOW_V2 - - ALLOW_V3 - - REQUIRE_ENCRYPTION - - SEND_WHITESPACE_TAG - - WHITESPACE_START_AKE - - ERROR_START_AKE - @return: unicode - """ - if key in DEFAULT_POLICY_FLAGS: - return DEFAULT_POLICY_FLAGS[key] - else: - return False - - def receiveMessageCb(self, msg, encrypted): - assert isinstance(self.peer, jid.JID) - if not encrypted: - log.warning(u"A plain-text message has been handled by otr.js") - log.debug(u"message received (was %s): %s" % ('encrypted' if encrypted else 'plain', msg)) - uuid_ = str(uuid.uuid4()) # FIXME - if not encrypted: - if self.state == otr.context.STATE_ENCRYPTED: - log.warning(u"Received unencrypted message in an encrypted context (from %(jid)s)" % {'jid': self.peer}) - self.host.newMessageHandler(uuid_, time.time(), unicode(self.peer), unicode(self.host.whoami), {'': RECEIVE_PLAIN_IN_ENCRYPTED_CONTEXT}, {}, C.MESS_TYPE_INFO, {}) - self.host.newMessageHandler(uuid_, time.time(), unicode(self.peer), unicode(self.host.whoami), {'': msg}, {}, C.MESS_TYPE_CHAT, {}) - - def sendMessageCb(self, msg, meta=None): - assert isinstance(self.peer, jid.JID) - log.debug(u"message to send%s: %s" % ((' (attached meta data: %s)' % meta) if meta else '', msg)) - self.host.bridge.call('sendMessage', (None, self.host.sendError), unicode(self.peer), msg, '', C.MESS_TYPE_CHAT, {'send_only': 'true'}) - - def messageErrorCb(self, error): - log.error(u'error occured: %s' % error) - - def setStateCb(self, msg_state, status): - if status == otr.context.STATUS_AKE_INIT: - return - - other_jid_s = self.peer - feedback = _(u"Error: the state of the conversation with %s is unknown!") - trust = self.getCurrentTrust() - - if status == otr.context.STATUS_SEND_QUERY: - feedback = QUERY_ENCRYPTED if msg_state == otr.context.STATE_ENCRYPTED else QUERY_NOT_ENCRYPTED - - elif status == otr.context.STATUS_AKE_SUCCESS: - trusted_str = AUTH_TRUSTED if trust else AUTH_UNTRUSTED - feedback = (trusted_str + AKE_ENCRYPTED) if msg_state == otr.context.STATE_ENCRYPTED else AKE_NOT_ENCRYPTED - - elif status == otr.context.STATUS_END_OTR: - if msg_state == otr.context.STATE_PLAINTEXT: - feedback = END_PLAIN_NO_MORE - elif msg_state == otr.context.STATE_ENCRYPTED: - log.error(END_ENCRYPTED) - elif msg_state == otr.context.STATE_FINISHED: - feedback = END_FINISHED - - uuid_ = str(uuid.uuid4()) # FIXME - self.host.newMessageHandler(uuid_, time.time(), unicode(self.peer), unicode(self.host.whoami), {'': feedback.format(jid=other_jid_s)}, {}, C.MESS_TYPE_INFO, {'header_info': OTR.getInfoText(msg_state, trust)}) - - def setCurrentTrust(self, new_trust='', act='asked', type_='trust'): - log.debug(u"setCurrentTrust: trust={trust}, act={act}, type={type}".format(type=type_, trust=new_trust, act=act)) - title = (AUTH_OTHER_TITLE if act == "asked" else AUTH_US_TITLE).format(jid=self.peer) - old_trust = self.getCurrentTrust() - if type_ == 'abort': - msg = AUTH_ABORTED_TXT - elif new_trust: - if act == "asked": - msg = AUTH_OTHER_OK - else: - msg = AUTH_US_OK - if not old_trust: - msg += " " + AUTH_OTHER_TOO - else: - msg = AUTH_FAILED_TXT - dialog.InfoDialog(title, msg, AddStyleName="maxWidthLimit").show() - if act != "asked": - return - otr.context.Context.setCurrentTrust(self, new_trust) - if old_trust != new_trust: - feedback = AUTH_STATUS.format(state=(AUTH_TRUSTED if new_trust else AUTH_UNTRUSTED).lower()) - uuid_ = str(uuid.uuid4()) # FIXME - self.host.newMessageHandler(uuid_, time.time(), unicode(self.peer), unicode(self.host.whoami), {'': feedback}, {}, C.MESS_TYPE_INFO, {'header_info': OTR.getInfoText(self.state, new_trust)}) - - def fingerprintAuthCb(self): - """OTR v2 authentication using manual fingerprint comparison""" - priv_key = self.user.privkey - - if priv_key is None: # OTR._authenticate should not let us arrive here - raise exceptions.InternalError - return - - other_key = self.getCurrentKey() - if other_key is None: - # we have a private key, but not the fingerprint of our correspondent - msg = (AUTH_INFO_TXT + AUTH_FINGERPRINT_YOURS).format(fingerprint=priv_key.fingerprint(), eol=DIALOG_EOL) - dialog.InfoDialog(_("Fingerprint"), msg, AddStyleName="maxWidthLimit").show() - return - - def setTrust(confirm): - self.setCurrentTrust('fingerprint' if confirm else '') - - text = (AUTH_INFO_TXT + "<i>" + AUTH_FINGERPRINT_TXT + "</i>" + AUTH_FINGERPRINT_VERIFY).format(you=self.host.whoami, your_fp=priv_key.fingerprint(), other=self.peer, other_fp=other_key.fingerprint(), eol=DIALOG_EOL) - title = AUTH_OTHER_TITLE.format(jid=self.peer) - dialog.ConfirmDialog(setTrust, text, title, AddStyleName="maxWidthLimit").show() - - def smpAuthCb(self, type_, data, act=None): - """OTR v3 authentication using the socialist millionaire protocol. - - @param type_ (unicode): a value in ('question', 'trust', 'abort') - @param data (unicode, bool): this could be: - - a string containing the question if type_ is 'question' - - a boolean value telling if the authentication succeed when type_ is 'trust' - @param act (unicode): a value in ('asked', 'answered') - """ - log.debug(u"smpAuthCb: type={type}, data={data}, act={act}".format(type=type_, data=data, act=act)) - if act is None: - if type_ == 'question': - act = 'answered' # OTR._authenticate calls this method with act="asked" - elif type_ == 'abort': - act = 'asked' # smpAuthAbort triggers this method with act='answered' when needed - - # FIXME upstream: if the correspondent uses Pidgin and authenticate us via - # fingerprints, we will reach this code... that's wrong, this method is for SMP! - # There's probably a bug to fix in otr.js. Do it together with the issue that - # make us need the dirty self.smpAuthAbort. - else: - log.error("FIXME: unmanaged ambiguous 'act' value in Context.smpAuthCb!") - title = (AUTH_OTHER_TITLE if act == "asked" else AUTH_US_TITLE).format(jid=self.peer) - if type_ == 'question': - if act == 'asked': - def cb(result, question, answer=None): - if not result or not answer: # dialog cancelled or the answer is empty - return - self.smpAuthSecret(answer, question) - text = (AUTH_INFO_TXT + "<i>" + AUTH_QUEST_DEFINE_TXT + "</i>" + AUTH_QUEST_DEFINE).format(eol=DIALOG_EOL) - dialog.PromptDialog(cb, [text, AUTH_SECRET_INPUT.format(eol=DIALOG_EOL)], title=title, AddStyleName="maxWidthLimit").show() - else: - def cb(result, answer): - if not result or not answer: # dialog cancelled or the answer is empty - self.smpAuthAbort('answered') - return - self.smpAuthSecret(answer) - text = (AUTH_INFO_TXT + "<i>" + AUTH_QUEST_ANSWER_TXT + "</i>" + AUTH_QUEST_ANSWER).format(eol=DIALOG_EOL, question=data) - dialog.PromptDialog(cb, [text + AUTH_SECRET_INPUT.format(eol=DIALOG_EOL)], title=title, AddStyleName="maxWidthLimit").show() - elif type_ == 'trust': - self.setCurrentTrust('smp' if data else '', act) - elif type_ == 'abort': - self.setCurrentTrust('', act, 'abort') - - def disconnect(self): - """Disconnect the session.""" - if self.state != otr.context.STATE_PLAINTEXT: - super(Context, self).disconnect() - - def finish(self): - """Finish the session - avoid to send any message but the user still has to end the session himself.""" - if self.state == otr.context.STATE_ENCRYPTED: - super(Context, self).finish() - - -class Account(otr.context.Account): - - def __init__(self, host): - log.debug(u"new account: %s" % host.whoami) - if not host.whoami.resource: - log.warning("Account created without resource") - super(Account, self).__init__(host.whoami) - self.host = host - - def loadPrivkey(self): - return self.privkey - - def savePrivkey(self): - # TODO: serialize and encrypt the private key and save it to a HTML5 persistent storage - # We need to ask the user before saving the key (e.g. if he's not on his private machine) - # self.privkey.serializePrivateKey() --> encrypt --> store - if self.privkey is None: - raise exceptions.InternalError(_("Save is called but privkey is None !")) - pass - - def saveTrusts(self): - # TODO save the trusts as it would be done for the private key - pass - - -class ContextManager(object): - - def __init__(self, host): - self.host = host - self.account = Account(host) - self.contexts = {} - - def startContext(self, other_jid): - assert isinstance(other_jid, jid.JID) # never start an OTR session with a bare JID - # FIXME upstream: apparently pyjamas doesn't implement setdefault well, it ignores JID.__hash__ redefinition - #context = self.contexts.setdefault(other_jid, Context(self.host, self.account, other_jid)) - if other_jid not in self.contexts: - self.contexts[other_jid] = Context(self.host, self.account, other_jid) - return self.contexts[other_jid] - - def getContextForUser(self, other_jid, start=True): - """Get the context for the given JID - - @param other_jid (jid.JID): your correspondent - @param start (bool): start non-existing context if True - @return: Context - """ - try: - other_jid = self.fixResource(other_jid) - except NotConnectedEntity: - log.debug(u"getContextForUser [%s]: not connected!" % other_jid) - return None - log.debug(u"getContextForUser [%s]" % other_jid) - if start: - return self.startContext(other_jid) - else: - return self.contexts.get(other_jid, None) - - def getContextsForBareUser(self, bare_jid): - """Get all the contexts for the users sharing the given bare JID. - - @param bare_jid (jid.JID): bare JID - @return: list[Context] - """ - return [context for other_jid, context in self.contexts.iteritems() if other_jid.bare == bare_jid] - - def fixResource(self, other_jid): - """Return the full JID in case the resource of the given JID is missing. - - @param other_jid (jid.JID): JID to check - @return jid.JID - """ - if other_jid.resource: - return other_jid - clist = self.host.contact_list - if clist.getCache(other_jid.bare, C.PRESENCE_SHOW) is None: - raise NotConnectedEntity - return clist.getFullJid(other_jid) - - -class OTR(object): - - def __init__(self, host): - log.info(_(u"OTR plugin initialization")) - self.host = host - self.context_manager = None - self.host.bridge._registerMethods(["skipOTR"]) - self.host.trigger.add("messageNewTrigger", self.newMessageTg, priority=trigger.TriggerManager.MAX_PRIORITY) - self.host.trigger.add("messageSendTrigger", self.sendMessageTg, priority=trigger.TriggerManager.MAX_PRIORITY) - - # FIXME: workaround for a pyjamas issue: calling hash on a class method always return a different value if that method is defined directly within the class (with the "def" keyword) - self._profilePluggedListener = self.profilePluggedListener - self._gotMenusListener = self.gotMenusListener - # FIXME: these listeners are never removed, can't be removed by themselves (it modifies the list while looping), maybe need a 'one_shot' argument - self.host.addListener('profilePlugged', self._profilePluggedListener) - self.host.addListener('gotMenus', self._gotMenusListener) - - @classmethod - def getInfoText(self, state=otr.context.STATE_PLAINTEXT, trust=''): - """Get the widget info text for a certain message state and trust. - - @param state (unicode): message state - @param trust (unicode): trust - @return: unicode - """ - if not state: - state = OTR_MSG_STATES.keys()[0] - return OTR_MSG_STATES[state][1 if trust else 0] - - def getInfoTextForUser(self, other_jid): - """Get the current info text for a conversation. - - @param other_jid (jid.JID): JID of the correspondant - """ - otrctx = self.context_manager.getContextForUser(other_jid, start=False) - if otrctx is None: - return OTR.getInfoText() - else: - return OTR.getInfoText(otrctx.state, otrctx.getCurrentTrust()) - - def gotMenusListener(self,): - # TODO: get menus paths to hook directly from backend's OTR plugin - self.host.menus.addMenuHook(C.MENU_SINGLE, (MAIN_MENU, D_(u"Start/Refresh")), callback=self._startRefresh) - self.host.menus.addMenuHook(C.MENU_SINGLE, (MAIN_MENU, D_(u"End session")), callback=self._endSession) - self.host.menus.addMenuHook(C.MENU_SINGLE, (MAIN_MENU, D_(u"Authenticate")), callback=self._authenticate) - self.host.menus.addMenuHook(C.MENU_SINGLE, (MAIN_MENU, D_(u"Drop private key")), callback=self._dropPrivkey) - - def profilePluggedListener(self, profile): - # FIXME: workaround for a pyjamas issue: calling hash on a class method always return a different value if that method is defined directly within the class (with the "def" keyword) - self._presenceListener = self.presenceListener - self._disconnectListener = self.disconnectListener - self.host.addListener('presence', self._presenceListener, [C.PROF_KEY_NONE]) - # FIXME: this listener is never removed, can't be removed by itself (it modifies the list while looping), maybe need a 'one_shot' argument - self.host.addListener('disconnect', self._disconnectListener, [C.PROF_KEY_NONE]) - - self.host.bridge.call('skipOTR', None) - self.context_manager = ContextManager(self.host) - # TODO: retrieve the encrypted private key from a HTML5 persistent storage, - # decrypt it, parse it with otr.crypt.PK.parsePrivateKey(privkey) and - # assign it to self.context_manager.account.privkey - - def disconnectListener(self, profile): - """Things to do just before the profile disconnection""" - self.host.removeListener('presence', self._presenceListener) - - for context in self.context_manager.contexts.values(): - context.disconnect() # FIXME: no time to send the message before the profile has been disconnected - - def presenceListener(self, entity, show, priority, statuses, profile): - if show == C.PRESENCE_UNAVAILABLE: - self.endSession(entity, disconnect=False) - - def newMessageTg(self, uid, timestamp, from_jid, to_jid, msg, subject, msg_type, extra, profile): - if msg_type != C.MESS_TYPE_CHAT: - return True - - try: - msg = msg.values()[0] # FIXME: Q&D fix for message refactoring, message is now a dict - except IndexError: - return True - tag = otr.proto.checkForOTR(msg) - if tag is None or (tag == otr.context.WHITESPACE_TAG and not DEFAULT_POLICY_FLAGS['WHITESPACE_START_AKE']): - return True - - other_jid = to_jid if from_jid.bare == self.host.whoami.bare else from_jid - otrctx = self.context_manager.getContextForUser(other_jid, start=False) - if otrctx is None: - def confirm(confirm): - if confirm: - self.host.displayWidget(chat.Chat, other_jid) - self.context_manager.startContext(other_jid).receiveMessage(msg) - else: - # FIXME: plain text messages with whitespaces would be lost here when WHITESPACE_START_AKE is True - pass - key = self.context_manager.account.privkey - question = QUERY_RECEIVED + QUERY_SLOWDOWN + (QUERY_KEY if key else QUERY_NO_KEY) + QUERY_CONFIRM - dialog.ConfirmDialog(confirm, question.format(jid=other_jid, eol=DIALOG_EOL), QUERY_TITLE, AddStyleName="maxWidthLimit").show() - else: # do not ask for user confirmation if the context exist - otrctx.receiveMessage(msg) - - return False # interrupt the main process - - def sendMessageTg(self, to_jid, message, subject, mess_type, extra, callback, errback, profile_key): - if mess_type != C.MESS_TYPE_CHAT: - return True - - otrctx = self.context_manager.getContextForUser(to_jid, start=False) - if otrctx is not None and otrctx.state != otr.context.STATE_PLAINTEXT: - if otrctx.state == otr.context.STATE_ENCRYPTED: - log.debug(u"encrypting message") - otrctx.sendMessage(message) - uuid_ = str(uuid.uuid4()) # FIXME - self.host.newMessageHandler(uuid_, time.time(), unicode(self.host.whoami), unicode(to_jid), {'': message}, {}, mess_type, extra) - else: - feedback = SEND_PLAIN_IN_FINISHED_CONTEXT - dialog.InfoDialog(FINISHED_CONTEXT_TITLE.format(jid=to_jid), feedback, AddStyleName="maxWidthLimit").show() - return False # interrupt the main process - - log.debug(u"sending message unencrypted") - return True - - def endSession(self, other_jid, disconnect=True): - """Finish or disconnect an OTR session - - @param other_jid (jid.JID): other JID - @param disconnect (bool): if False, finish the session but do not disconnect it - """ - # checking for private key existence is not needed, context checking is enough - if other_jid.resource: - contexts = [self.context_manager.getContextForUser(other_jid, start=False)] - else: # contact disconnected itself so we need to terminate the OTR session but the Chat panel lost its resource - contexts = self.context_manager.getContextsForBareUser(other_jid) - for otrctx in contexts: - if otrctx is None or otrctx.state == otr.context.STATE_PLAINTEXT: - if disconnect: - uuid_ = str(uuid.uuid4()) # FIXME - self.host.newMessageHandler(uuid_, time.time(), unicode(other_jid), unicode(self.host.whoami), {'': END_PLAIN_HAS_NOT.format(jid=other_jid)}, {}, C.MESS_TYPE_INFO, {}) - return - if disconnect: - otrctx.disconnect() - else: - otrctx.finish() - - # Menu callbacks - - def _startRefresh(self, caller, menu_data, profile): - """Start or refresh an OTR session - - @param menu_data: %(menu_data)s - """ - def query(other_jid): - otrctx = self.context_manager.getContextForUser(other_jid) - if otrctx: - otrctx.sendQueryMessage() - - other_jid = jid.JID(menu_data['jid']) - clist = self.host.contact_list - if clist.getCache(other_jid.bare, C.PRESENCE_SHOW) is None: - dialog.InfoDialog(ACTION_NA_TITLE, ACTION_NA, AddStyleName="maxWidthLimit").show() - return - - key = self.context_manager.account.privkey - if key is None: - def confirm(confirm): - if confirm: - query(other_jid) - msg = QUERY_SEND + QUERY_SLOWDOWN + QUERY_NO_KEY + QUERY_CONFIRM - dialog.ConfirmDialog(confirm, msg.format(jid=other_jid, eol=DIALOG_EOL), QUERY_TITLE, AddStyleName="maxWidthLimit").show() - else: # on query reception we ask always, if we initiate we just ask the first time - query(other_jid) - - def _endSession(self, caller, menu_data, profile): - """End an OTR session - - @param menu_data: %(menu_data)s - """ - self.endSession(jid.JID(menu_data['jid'])) - - def _authenticate(self, caller, menu_data, profile): - """Authenticate other user and see our own fingerprint - - @param menu_data: %(menu_data)s - @param profile: %(doc_profile)s - """ - def not_available(): - dialog.InfoDialog(AUTH_TRUST_NA_TITLE, AUTH_TRUST_NA_TXT, AddStyleName="maxWidthLimit").show() - - to_jid = jid.JID(menu_data['jid']) - - # checking for private key existence is not needed, context checking is enough - otrctx = self.context_manager.getContextForUser(to_jid, start=False) - if otrctx is None or otrctx.state != otr.context.STATE_ENCRYPTED: - not_available() - return - otr_version = otrctx.getUsedVersion() - if otr_version == otr.context.OTR_VERSION_2: - otrctx.fingerprintAuthCb() - elif otr_version == otr.context.OTR_VERSION_3: - otrctx.smpAuthCb('question', None, 'asked') - else: - not_available() - - def _dropPrivkey(self, caller, menu_data, profile): - """Drop our private Key - - @param menu_data: %(menu_data)s - @param profile: %(doc_profile)s - """ - priv_key = self.context_manager.account.privkey - if priv_key is None: - # we have no private key yet - dialog.InfoDialog(KEY_NA_TITLE, KEY_NA_TXT, AddStyleName="maxWidthLimit").show() - return - - def dropKey(confirm): - if confirm: - # we end all sessions - for context in self.context_manager.contexts.values(): - context.disconnect() - self.context_manager.contexts.clear() - self.context_manager.account.privkey = None - dialog.InfoDialog(KEY_TITLE, KEY_DROPPED_TXT, AddStyleName="maxWidthLimit").show() - - dialog.ConfirmDialog(dropKey, KEY_DROP_TXT.format(eol=DIALOG_EOL), KEY_DROP_TITLE, AddStyleName="maxWidthLimit").show()
--- a/src/browser/sat_browser/plugin_xep_0085.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,77 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# SAT plugin for Chat State Notifications Protocol (xep-0085) -# Copyright (C) 2013-2016 Adrien Cossa (souliane@mailoo.org) - -# This program 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. - -# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. - -from pyjamas.Timer import Timer - - -# Copy of the map from sat/src/plugins/plugin_xep_0085 -TRANSITIONS = {"active": {"next_state": "inactive", "delay": 120}, - "inactive": {"next_state": "gone", "delay": 480}, - "gone": {"next_state": "", "delay": 0}, - "composing": {"next_state": "paused", "delay": 30}, - "paused": {"next_state": "inactive", "delay": 450} - } - - -class ChatStateMachine: - """This is an adapted version of the ChatStateMachine from sat/src/plugins/plugin_xep_0085 - which manage a timer on the web browser and keep it synchronized with the timer that runs - on the backend. This is only needed to avoid calling the bridge method chatStateComposing - too often ; accuracy is not needed so we can ignore the delay of the communication between - the web browser and the backend (the timer on the web browser always starts a bit before). - /!\ Keep this file up to date if you modify the one in the sat plugins directory. - """ - def __init__(self, host, target_s): - - self.host = host - self.target_s = target_s - self.started = False - self.state = None - self.timer = None - - def _onEvent(self, state): - # Pyjamas callback takes no extra argument so we need this trick - - # Here we should check the value of the parameter "Send chat state notifications" - # but this costs two messages. It's even better to call chatStateComposing - # with a doubt, it will be checked by the back-end anyway before sending - # the actual notifications to the other client. - if state == "composing" and not self.started: - return - self.started = True - self.next_state = state - self.__onEvent(None) - - def __onEvent(self, timer): - state = self.next_state - - assert(state in TRANSITIONS) - transition = TRANSITIONS[state] - assert("next_state" in transition and "delay" in transition) - - if state != self.state and state == "composing": - self.host.bridge.call('chatStateComposing', None, self.target_s) - - self.state = state - if self.timer is not None: - self.timer.cancel() - - if transition["next_state"] and transition["delay"] > 0: - self.next_state = transition["next_state"] - self.timer = Timer(transition["delay"] * 1000, self.__onEvent) # pyjamas timer is in milliseconds
--- a/src/browser/sat_browser/register.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,285 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2011-2018 Jérôme Poisson <goffi@goffi.org> -# Copyright (C) 2011, 2012 Adrien Vigneron <adrienvigneron@mailoo.org> - -# This program 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. - -# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. - -#This page manage subscription and new account creation - -import pyjd # this is dummy in pyjs -from sat.core.i18n import _ - -from pyjamas.ui.SimplePanel import SimplePanel -from pyjamas.ui.VerticalPanel import VerticalPanel -from pyjamas.ui.HorizontalPanel import HorizontalPanel -from pyjamas.ui.StackPanel import StackPanel -from pyjamas.ui.PasswordTextBox import PasswordTextBox -from pyjamas.ui.TextBox import TextBox -from pyjamas.ui.FormPanel import FormPanel -from pyjamas.ui.Button import Button -from pyjamas.ui.Label import Label -from pyjamas.ui.HTML import HTML -from pyjamas.ui.PopupPanel import PopupPanel -from pyjamas.ui.Image import Image -from pyjamas.ui.Hidden import Hidden -from pyjamas import Window -from pyjamas.ui.KeyboardListener import KEY_ENTER -from pyjamas.Timer import Timer - -from __pyjamas__ import JS - -from constants import Const as C - - - -class RegisterPanel(FormPanel): - - def __init__(self, callback, session_data): - """ - @param callback(callable): method to call if login successful - @param session_data(dict): session metadata - """ - FormPanel.__init__(self) - self.setSize('600px', '350px') - self.callback = callback - self.setMethod(FormPanel.METHOD_POST) - main_panel = HorizontalPanel() - main_panel.setStyleName('registerPanel_main') - left_side = Image("media/libervia/register_left.png") - main_panel.add(left_side) - - ##StackPanel## - self.right_side = StackPanel(StyleName='registerPanel_right_side') - main_panel.add(self.right_side) - main_panel.setCellWidth(self.right_side, '100%') - - ##Login stack## - login_stack = SimplePanel() - login_stack.setStyleName('registerPanel_content') - login_vpanel = VerticalPanel() - login_stack.setWidget(login_vpanel) - - self.login_warning_msg = HTML('') - self.login_warning_msg.setStyleName('formWarning') - login_vpanel.add(self.login_warning_msg) - - login_label = Label('Login:') - self.login_box = TextBox() - self.login_box.setName("login") - self.login_box.addKeyboardListener(self) - login_pass_label = Label('Password:') - self.login_pass_box = PasswordTextBox() - self.login_pass_box.setName("login_password") - self.login_pass_box.addKeyboardListener(self) - - login_vpanel.add(login_label) - login_vpanel.add(self.login_box) - login_vpanel.add(login_pass_label) - login_vpanel.add(self.login_pass_box) - login_but = Button("Log in", getattr(self, "onLogin")) - login_but.setStyleName('button') - login_but.addStyleName('red') - login_vpanel.add(login_but) - self.right_side.add(login_stack, 'Return to the login screen') - - #The hidden submit_type field - self.submit_type = Hidden('submit_type') - login_vpanel.add(self.submit_type) - - ##Register stack## - if session_data["allow_registration"]: - register_stack = SimplePanel() - register_stack.setStyleName('registerPanel_content') - register_vpanel = VerticalPanel() - register_stack.setWidget(register_vpanel) - - self.register_warning_msg = HTML('') - self.register_warning_msg.setStyleName('formWarning') - register_vpanel.add(self.register_warning_msg) - - register_login_label = Label('Login:') - self.register_login_box = TextBox() - self.register_login_box.setName("register_login") - self.register_login_box.addKeyboardListener(self) - email_label = Label('E-mail:') - self.email_box = TextBox() - self.email_box.setName("email") - self.email_box.addKeyboardListener(self) - register_pass_label = Label('Password:') - self.register_pass_box = PasswordTextBox() - self.register_pass_box.setName("register_password") - self.register_pass_box.addKeyboardListener(self) - register_vpanel.add(register_login_label) - register_vpanel.add(self.register_login_box) - register_vpanel.add(email_label) - register_vpanel.add(self.email_box) - register_vpanel.add(register_pass_label) - register_vpanel.add(self.register_pass_box) - - register_but = Button("Register a new account", getattr(self, "onRegister")) - register_but.setStyleName('button') - register_but.addStyleName('red') - register_vpanel.add(register_but) - - self.right_side.add(register_stack, 'No account yet? Create a new one!') - self.right_side.addStackChangeListener(self) - register_stack.setWidth(None) - login_stack.setWidth(None) - - self.add(main_panel) - self.addFormHandler(self) - self.setAction('register_api/login') - - def onStackChanged(self, sender, index): - if index == 0: - self.login_box.setFocus(True) - elif index == 1: - self.register_login_box.setFocus(True) - - def onKeyPress(self, sender, keycode, modifiers): - # XXX: this is triggered before the textbox value has changed - if keycode == KEY_ENTER: - # Browsers offer an auto-completion feature to any - # text box, but the selected value is not set when - # the widget looses the focus. Using a timer with - # any delay value > 0 would do the trick. - if sender == self.login_box: - Timer(5, lambda timer: self.login_pass_box.setFocus(True)) - elif sender == self.login_pass_box: - self.onLogin(None) - elif sender == self.register_login_box: - Timer(5, lambda timer: self.email_box.setFocus(True)) - elif sender == self.email_box: - Timer(5, lambda timer: self.register_pass_box.setFocus(True)) - elif sender == self.register_pass_box: - self.onRegister(None) - - def onKeyUp(self, sender, keycode, modifiers): - # XXX: this is triggered after the textbox value has changed - if sender == self.login_box: - if "@" in self.login_box.getText(): - self.login_warning_msg.setHTML(_('<span class="formInfo">Entering a full JID is only needed to connect with an external XMPP account.</span>')) - else: - self.login_warning_msg.setHTML("") - - def onKeyDown(self, sender, keycode, modifiers): - pass - - def onLogin(self, button): - if not self.checkJID(self.login_box.getText()): - self.login_warning_msg.setHTML('Invalid login, valid characters<br>are a-z A-Z 0-9 _ - or a bare JID') - else: - self.submit_type.setValue('login') - self.submit(None) - - def onRegister(self, button): - # XXX: for now libervia forces the creation to lower case - self.register_login_box.setText(self.register_login_box.getText().lower()) - if not self.checkLogin(self.register_login_box.getText()): - self.register_warning_msg.setHTML(_('Invalid login, valid characters<br>are a-z A-Z 0-9 _ -')) - elif not self.checkEmail(self.email_box.getText()): - self.register_warning_msg.setHTML(_('Invalid email address<br>(or not accepted yet)')) - elif len(self.register_pass_box.getText()) < C.PASSWORD_MIN_LENGTH: - self.register_warning_msg.setHTML(_('Your password must contain<br>at least %d characters.') % C.PASSWORD_MIN_LENGTH) - else: - self.register_warning_msg.setHTML("") - self.submit_type.setValue('register') - self.submit(None) - - def onSubmit(self, event): - pass - - def onSubmitComplete(self, event): - result = event.getResults() - if result == C.PROFILE_AUTH_ERROR: - self.login_warning_msg.setHTML(_('Your login and/or password is incorrect. Please try again.')) - elif result == C.XMPP_AUTH_ERROR: - # TODO: call stdui action CHANGE_XMPP_PASSWD_ID as it's done in primitivus - Window.alert(_(u'Your XMPP account failed to connect. Did you enter the good password? If you have changed your XMPP password since your last connection on Libervia, please use another SàT frontend to update your profile.')) - elif result == C.PROFILE_LOGGED_EXT_JID: - self.callback() - Window.alert(_('A profile has been created on this Libervia service using your existing XMPP account. Since you are not using our XMPP server, we can not guaranty that all the extra features (blog, directory...) will fully work.')) - elif result == C.PROFILE_LOGGED: - self.callback() - elif result == C.SESSION_ACTIVE: - Window.alert(_('Session already active, this should not happen, please contact the author to fix it.')) - elif result == C.NO_REPLY: - Window.alert(_("Did not receive a reply (the timeout expired or the connection is broken).")) - elif result == C.ALREADY_EXISTS: - self.register_warning_msg.setHTML(_('This login already exists,<br>please choose another one.')) - elif result == C.INTERNAL_ERROR: - self.register_warning_msg.setHTML(_('An registration error occurred, please contact the server administrator.')) - elif result == C.REGISTRATION_SUCCEED: - self.login_warning_msg.setHTML("") - self.register_warning_msg.setHTML("") - self.login_box.setText(self.register_login_box.getText()) - self.login_pass_box.setText('') - self.register_login_box.setText('') - self.register_pass_box.setText('') - self.email_box.setText('') - self.right_side.showStack(0) - self.login_pass_box.setFocus(True) - Window.alert(_('An email has been sent to you with your login informations\nPlease remember that this is ONLY A TECHNICAL DEMO.')) - else: - Window.alert(_("An error occurred and we couldn't process your request. Please report the following error name to the administrators of your network: '%s'" % result)) - - def checkLogin(self, text): - """Check if the given text is a valid login - - @param text (unicode) - @return bool - """ - # FIXME: Pyjamas re module is not stable so we use pure JS instead - # FIXME: login is restricted to this regex until we fix the account creation - JS("""return /^(\w|-)+$/.test(text);""") - - def checkEmail(self, text): - """Check if the given text is a valid email address. - - @param text (unicode) - @return bool - """ - # FIXME: Pyjamas re module is not stable so we use pure JS instead - # FIXME: send a message to validate the email instead of using a bad regex - JS("""return /^(\w|-|\.|\+)+@(\w|-)+\.(\w|-)+$/.test(text);""") - - def checkJID(self, text): - """Check if the given text is a valid JID. - - @param text (unicode) - @return bool - """ - # FIXME: Pyjamas re module is not stable so we use pure JS instead - # FIXME: this regex is too restrictive for people using external XMPP account - JS("""return /^(\w|-|\.|\+)+(@(\w|-)+\.(\w|-)+)?$/.test(text);""") - - -class RegisterBox(PopupPanel): - - def __init__(self, callback, session_data, *args, **kwargs): - PopupPanel.__init__(self, *args, **kwargs) - self._form = RegisterPanel(callback, session_data) - self.setWidget(self._form) - - def onWindowResized(self, width, height): - super(RegisterBox, self).onWindowResized(width, height) - self.centerBox() - - def show(self): - super(RegisterBox, self).show() - self.centerBox() - self._form.login_box.setFocus(True)
--- a/src/browser/sat_browser/richtext.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,360 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2013-2016 Adrien Cossa <souliane@mailoo.org> - -# This program 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. - -# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. - -from sat_frontends.tools import composition -from sat.core.i18n import _ -from sat.core.log import getLogger -log = getLogger(__name__) -from sat.tools.common import data_format - -from pyjamas.ui.TextArea import TextArea -from pyjamas.ui.Button import Button -from pyjamas.ui.CheckBox import CheckBox -from pyjamas.ui.Label import Label -from pyjamas.ui.FlexTable import FlexTable -from pyjamas.ui.HorizontalPanel import HorizontalPanel -from pyjamas.ui.KeyboardListener import KeyboardHandler -from pyjamas import Window -from __pyjamas__ import doc - -from constants import Const as C -import dialog -import base_panel -import editor_widget -import html_tools -import list_manager - - -class RichTextEditor(editor_widget.BaseTextEditor, FlexTable): - """Panel for the rich text editor.""" - - STYLE = {'main': 'richTextEditor', - 'title': 'richTextTitle', - 'toolbar': 'richTextToolbar', - 'textarea': 'richTextArea' - } - - def __init__(self, host, content=None, modifiedCb=None, afterEditCb=None, options=None): - """ - - @param host (SatWebFrontend): host instance - @param content (dict): dict with at least a 'text' key - @param modifiedCb (callable): to be called when the text has been modified - @param afterEditCb (callable): to be called when the edition is done - @param options (list[unicode]): UI options ("read_only", "update_msg") - """ - FlexTable.__init__(self) # FIXME - self.host = host - self.wysiwyg = False - self.read_only = 'read_only' in options - self.update_msg = 'update_msg' in options - - indices = (-1, -1, 0, -1, -1) if self.read_only else (0, 1, 2, 3, 4) - self.title_offset, self.toolbar_offset, self.content_offset, self.tags_offset, self.command_offset = indices - self.addStyleName(self.STYLE['main']) - - editor_widget.BaseTextEditor.__init__(self, content, None, modifiedCb, afterEditCb) - - def addEditListener(self, listener): - """Add a method to be called whenever the text is edited. - - @param listener: method taking two arguments: sender, keycode - """ - editor_widget.BaseTextEditor.addEditListener(self, listener) - if hasattr(self, 'display'): - self.display.addEditListener(listener) - - def refresh(self, edit=None): - """Refresh the UI for edition/display mode. - - @param edit: set to True to display the edition mode - """ - if edit is None: - edit = hasattr(self, 'textarea') and self.textarea.getVisible() - - for widget in ['title_panel', 'tags_panel', 'command']: - if hasattr(self, widget): - getattr(self, widget).setVisible(edit) - - if hasattr(self, 'toolbar'): - self.toolbar.setVisible(False) - - if not hasattr(self, 'display'): - self.display = editor_widget.HTMLTextEditor(options={'enhance_display': False, 'listen_keyboard': False}) # for display mode - for listener in self.edit_listeners: - self.display.addEditListener(listener) - - if not self.read_only and not hasattr(self, 'textarea'): - self.textarea = EditTextArea(self) # for edition mode - self.textarea.addStyleName(self.STYLE['textarea']) - - self.getFlexCellFormatter().setColSpan(self.content_offset, 0, 2) - if edit and not self.wysiwyg: - self.textarea.setWidth('100%') # CSS width doesn't do it, don't know why - self.setWidget(self.content_offset, 0, self.textarea) - else: - self.setWidget(self.content_offset, 0, self.display) - if not edit: - return - - if not self.read_only and not hasattr(self, 'title_panel'): - self.title_panel = base_panel.TitlePanel() - self.title_panel.addStyleName(self.STYLE['title']) - self.getFlexCellFormatter().setColSpan(self.title_offset, 0, 2) - self.setWidget(self.title_offset, 0, self.title_panel) - - if not self.read_only and not hasattr(self, 'tags_panel'): - suggested_tags = [] # TODO: feed this list with tags suggestion - self.tags_panel = list_manager.TagsPanel(suggested_tags) - self.getFlexCellFormatter().setColSpan(self.tags_offset, 0, 2) - self.setWidget(self.tags_offset, 0, self.tags_panel) - - if not self.read_only and not hasattr(self, 'command'): - self.command = HorizontalPanel() - self.command.addStyleName("marginAuto") - self.command.add(Button("Cancel", lambda: self.edit(True, True))) - self.command.add(Button("Update" if self.update_msg else "Send message", lambda: self.edit(False))) - self.getFlexCellFormatter().setColSpan(self.command_offset, 0, 2) - self.setWidget(self.command_offset, 0, self.command) - - def setToolBar(self, syntax): - """This method is called asynchronously after the parameter - holding the rich text syntax is retrieved. It is called at - each call of self.edit(True) because the user may - have change his setting since the last time.""" - if syntax is None or syntax not in composition.RICH_SYNTAXES.keys(): - syntax = composition.RICH_SYNTAXES.keys()[0] - if hasattr(self, "toolbar") and self.toolbar.syntax == syntax: - self.toolbar.setVisible(True) - return - self.toolbar = HorizontalPanel() - self.toolbar.syntax = syntax - self.toolbar.addStyleName(self.STYLE['toolbar']) - for key in composition.RICH_SYNTAXES[syntax].keys(): - self.addToolbarButton(syntax, key) - self.wysiwyg_button = CheckBox(_('preview')) - wysiywgCb = lambda sender: self.setWysiwyg(sender.getChecked()) - self.wysiwyg_button.addClickListener(wysiywgCb) - self.toolbar.add(self.wysiwyg_button) - self.syntax_label = Label(_("Syntax: %s") % syntax) - self.syntax_label.addStyleName("richTextSyntaxLabel") - self.toolbar.add(self.syntax_label) - self.toolbar.setCellWidth(self.syntax_label, "100%") - self.getFlexCellFormatter().setColSpan(self.toolbar_offset, 0, 2) - self.setWidget(self.toolbar_offset, 0, self.toolbar) - - def setWysiwyg(self, wysiwyg, init=False): - """Toggle the edition mode between rich content syntax and wysiwyg. - @param wysiwyg: boolean value - @param init: set to True to re-init without switching the widgets.""" - def setWysiwyg(): - self.wysiwyg = wysiwyg - try: - self.wysiwyg_button.setChecked(wysiwyg) - except (AttributeError, TypeError): - pass - try: - if wysiwyg: - self.syntax_label.addStyleName('transparent') - else: - self.syntax_label.removeStyleName('transparent') - except (AttributeError, TypeError): - pass - if not wysiwyg: - self.display.removeStyleName('richTextWysiwyg') - - if init: - setWysiwyg() - return - - self.getFlexCellFormatter().setColSpan(self.content_offset, 0, 2) - if wysiwyg: - def syntaxConvertCb(text): - self.display.setContent({'text': text}) - self.textarea.removeFromParent() # XXX: force as it is not always done... - self.setWidget(self.content_offset, 0, self.display) - self.display.addStyleName('richTextWysiwyg') - self.display.edit(True) - content = self.getContent() - if content['text'] and content['syntax'] != C.SYNTAX_XHTML: - self.host.bridge.call('syntaxConvert', syntaxConvertCb, content['text'], content['syntax'], C.SYNTAX_XHTML) - else: - syntaxConvertCb(content['text']) - else: - syntaxConvertCb = lambda text: self.textarea.setText(text) - text = self.display.getContent()['text'] - if text and self.toolbar.syntax != C.SYNTAX_XHTML: - self.host.bridge.call('syntaxConvert', syntaxConvertCb, text) - else: - syntaxConvertCb(text) - self.setWidget(self.content_offset, 0, self.textarea) - self.textarea.setWidth('100%') # CSS width doesn't do it, don't know why - - setWysiwyg() # do it in the end because it affects self.getContent - - def addToolbarButton(self, syntax, key): - """Add a button with the defined parameters.""" - button = Button('<img src="%s" class="richTextIcon" />' % - composition.RICH_BUTTONS[key]["icon"]) - button.setTitle(composition.RICH_BUTTONS[key]["tip"]) - button.addStyleName('richTextToolButton') - self.toolbar.add(button) - - def buttonCb(): - """Generic callback for a toolbar button.""" - text = self.textarea.getText() - cursor_pos = self.textarea.getCursorPos() - selection_length = self.textarea.getSelectionLength() - data = composition.RICH_SYNTAXES[syntax][key] - if selection_length == 0: - middle_text = data[1] - else: - middle_text = text[cursor_pos:cursor_pos + selection_length] - self.textarea.setText(text[:cursor_pos] - + data[0] - + middle_text - + data[2] - + text[cursor_pos + selection_length:]) - self.textarea.setCursorPos(cursor_pos + len(data[0]) + len(middle_text)) - self.textarea.setFocus(True) - self.textarea.onKeyDown() - - def wysiwygCb(): - """Callback for a toolbar button while wysiwyg mode is enabled.""" - data = composition.COMMANDS[key] - - def execCommand(command, arg): - self.display.setFocus(True) - doc().execCommand(command, False, arg.strip() if arg else '') - # use Window.prompt instead of dialog.PromptDialog to not loose the focus - prompt = lambda command, text: execCommand(command, Window.prompt(text)) - if isinstance(data, tuple) or isinstance(data, list): - if data[1]: - prompt(data[0], data[1]) - else: - execCommand(data[0], data[2]) - else: - execCommand(data, False, '') - self.textarea.onKeyDown() - - button.addClickListener(lambda: wysiwygCb() if self.wysiwyg else buttonCb()) - - def getContent(self): - assert(hasattr(self, 'textarea')) - assert(hasattr(self, 'toolbar')) - if self.wysiwyg: - content = {'text': self.display.getContent()['text'], 'syntax': C.SYNTAX_XHTML} - else: - content = {'text': self.strproc(self.textarea.getText()), 'syntax': self.toolbar.syntax} - if hasattr(self, 'title_panel'): - content.update({'title': self.strproc(self.title_panel.getText())}) - if hasattr(self, 'tags_panel'): - data_format.iter2dict('tag', self.tags_panel.getTags(), content) - return content - - def edit(self, edit=False, abort=False, sync=False): - """ - Remark: the editor must be visible before you call this method. - @param edit: set to True to edit the content or False to only display it - @param abort: set to True to cancel the edition and loose the changes. - If edit and abort are both True, self.abortEdition can be used to ask for a - confirmation. When edit is False and abort is True, abortion is actually done. - @param sync: set to True to cancel the edition after the content has been saved somewhere else - """ - if not (edit and abort): - self.refresh(edit) # not when we are asking for a confirmation - editor_widget.BaseTextEditor.edit(self, edit, abort, sync) # after the UI has been refreshed - if (edit and abort): - return # self.abortEdition is called by editor_widget.BaseTextEditor.edit - self.setWysiwyg(False, init=True) # after editor_widget.BaseTextEditor (it affects self.getContent) - if sync: - return - # the following must NOT be done at each UI refresh! - content = self._original_content - if edit: - def getParamCb(syntax): - # set the editable text in the current user-selected syntax - def syntaxConvertCb(text=None): - if text is not None: - # Important: this also update self._original_content - content.update({'text': text}) - content.update({'syntax': syntax}) - self.textarea.setText(content['text']) - - if hasattr(self, 'title_panel') and 'title' in content: - self.title_panel.setText(content['title']) - self.title_panel.setStackVisible(0, content['title'] != '') - - if hasattr(self, 'tags_panel'): - tags = list(data_format.dict2iter('tag', content)) - self.tags_panel.setTags(tags) - self.tags_panel.setStackVisible(0, len(tags) > 0) - - self.setToolBar(syntax) - if content['text'] and content['syntax'] != syntax: - self.host.bridge.call('syntaxConvert', syntaxConvertCb, content['text'], content['syntax']) - else: - syntaxConvertCb() - self.host.bridge.call('asyncGetParamA', getParamCb, composition.PARAM_NAME_SYNTAX, composition.PARAM_KEY_COMPOSITION) - else: - if not self.initialized: - # set the display text in XHTML only during init because a new MicroblogEntry instance is created after each modification - self.setDisplayContent() - self.display.edit(False) - - def setDisplayContent(self): - """Set the content of the editor_widget.HTMLTextEditor which is used for display/wysiwyg""" - content = self._original_content - text = content['text'] - if 'title' in content and content['title']: - title = '<h2>%s</h2>' % html_tools.html_sanitize(content['title']) - else: - title = "" - - tags = "" - for tag in data_format.dict2iter('tag', content): - tags += "<li><a>%s</a></li>" % html_tools.html_sanitize(tag) - if tags: - tags = '<ul class="mblog_tags">%s</ul>' % tags - - self.display.setContent({'text': "%s%s%s" % (title, tags, text)}) - - def setFocus(self, focus): - self.textarea.setFocus(focus) - - def abortEdition(self, content): - """Ask for confirmation before closing the dialog.""" - def confirm_cb(answer): - if answer: - self.edit(False, True) - _dialog = dialog.ConfirmDialog(confirm_cb, text="Do you really want to %s?" % ("cancel your changes" if self.update_msg else "cancel this message")) - _dialog.cancel_button.setText(_("No")) - _dialog.show() - - -class EditTextArea(TextArea, KeyboardHandler): - def __init__(self, _parent): - TextArea.__init__(self) - self._parent = _parent - KeyboardHandler.__init__(self) - self.addKeyboardListener(self) - - def onKeyDown(self, sender=None, keycode=None, modifiers=None): - for listener in self._parent.edit_listeners: - listener(self, keycode, modifiers) # FIXME: edit_listeners must either be removed, or send an action instead of keycode/modifiers
--- a/src/browser/sat_browser/strings.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,101 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# SAT helpers methods for plugins -# Copyright (C) 2013-2016 Adrien Cossa (souliane@mailoo.org) - -# This program 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. - -# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. - -from __pyjamas__ import JS - - -def getURLParams(url): - """This comes from pyjamas.Location.makeUrlDict with a small change - to also parse full URLs, and parameters with no value specified - (in that case the default value "" is used). - @param url: any URL with or without parameters - @return: a dictionary of the parameters, if any was given, or {} - """ - dict_ = {} - if "/" in url: - # keep the part after the last "/" - url = url[url.rindex("/") + 1:] - if url.startswith("?"): - # remove the first "?" - url = url[1:] - pairs = url.split("&") - for pair in pairs: - if len(pair) < 3: - continue - kv = pair.split("=", 1) - dict_[kv[0]] = kv[1] if len(kv) > 1 else "" - return dict_ - - -def addURLToText(text, new_target=True): - """Check a text for what looks like an URL and make it clickable. - - @param string (unicode): text to process - @param new_target (bool): if True, make the link open in a new window - """ - # FIXME: Workaround for a pyjamas bug with regex, base method in sat.frontends.tools.strings - # In some case, Pyjamas' re module get crazy and freeze browsers (tested with Iceweasel and Chromium). - # we use javascript as a workaround - # This method is inspired from https://stackoverflow.com/questions/1500260/detect-urls-in-text-with-javascript - JS("""var urlRegex = /(https?:\/\/[^\s]+)/g; - var target = new_target ? ' target="_blank"' : ''; - return text.replace(urlRegex, function(url) { - return '<a href="' + url + '"' + target + ' class="url">' + url + '</a>'; - })""") - - -def addURLToImage(text): - """Check a XHTML text for what looks like an imageURL and make it clickable. - - @param text (unicode): text to process - """ - # FIXME: Pyjamas re module is not stable so we use pure JS instead, base method in sat.frontends.tools.strings - JS("""var imgRegex = /<img[^>]* src="([^"]+)"[^>]*>/g; - return text.replace(imgRegex, function(img, src) { - return '<a href="' + src + '" target="_blank">' + img + '</a>'; - })""") - -def fixXHTMLLinks(xhtml): - """Add http:// if the scheme is missing and force opening in a new window. - - @param string (unicode): XHTML Content - """ - # FIXME: Pyjamas re module is not stable so we use pure JS instead, base method in sat.frontends.tools.strings - JS("""var subs = []; - var tag_re = /<a(?: \w+="[^"]*")* ?\/?>/g; - var result; - while ((result = tag_re.exec(xhtml)) !== null) { - tag = result[0]; - var link_result = /href="([^"]*)"/.exec(tag); - if (link_result && !(link_result[1].startsWith("#"))) { // found a link which is not an internal anchor - var link = link_result[0]; - var url = link_result[1]; - if (! /target="([^"]*)"/.test(tag)) { // no target - subs.push([tag, '<a target="_blank"' + tag.slice(2, tag.length)]); - } - if (! /^\w+:\/\//.test(url)) { // no scheme - subs.push([link, 'href="http://' + url + '"']); - } - } - } - for (i in subs) { - xhtml = xhtml.replace(subs[i][0], subs[i][1]); - } - """) - return xhtml
--- a/src/browser/sat_browser/web_widget.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,89 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2011-2018 Jérôme Poisson <goffi@goffi.org> - -# This program 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. - -# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. - -import pyjd # this is dummy in pyjs -from sat.core.log import getLogger -log = getLogger(__name__) - -from sat.core.i18n import D_ - -from pyjamas.ui.VerticalPanel import VerticalPanel -from pyjamas.ui.HorizontalPanel import HorizontalPanel -from pyjamas.ui.Button import Button -from pyjamas.ui.Frame import Frame -from pyjamas import DOM - - -import dialog -import libervia_widget -from constants import Const as C -from sat_frontends.quick_frontend import quick_widgets -from sat_frontends.tools import host_listener - - -class WebWidget(quick_widgets.QuickWidget, libervia_widget.LiberviaWidget): - """ (mini)browser like widget """ - - def __init__(self, host, target, show_url=True, profiles=None): - """ - @param host: SatWebFrontend instance - @param target: url to open - """ - quick_widgets.QuickWidget.__init__(self, host, target, C.PROF_KEY_NONE) - libervia_widget.LiberviaWidget.__init__(self, host) - self._vpanel = VerticalPanel() - self._vpanel.setSize('100%', '100%') - self._url = dialog.ExtTextBox(enter_cb=self.onUrlClick) - self._url.setText(target or "") - self._url.setWidth('100%') - if show_url: - hpanel = HorizontalPanel() - hpanel.add(self._url) - btn = Button("Go", self.onUrlClick) - hpanel.setCellWidth(self._url, "100%") - hpanel.add(btn) - self._vpanel.add(hpanel) - self._vpanel.setCellHeight(hpanel, '20px') - self._frame = Frame(target or "") - self._frame.setSize('100%', '100%') - DOM.setStyleAttribute(self._frame.getElement(), "position", "relative") - self._vpanel.add(self._frame) - self.setWidget(self._vpanel) - - def onUrlClick(self, sender): - url = self._url.getText() - scheme_end = url.find(':') - scheme = "" if scheme_end == -1 else url[:scheme_end] - if scheme not in C.WEB_PANEL_SCHEMES: - url = "http://" + url - self._frame.setUrl(url) - - -## Menu - -def hostReady(host): - def onWebWidget(): - web_widget = host.displayWidget(WebWidget, C.WEB_PANEL_DEFAULT_URL) - host.setSelected(web_widget) - - def gotMenus(): - host.menus.addMenu(C.MENU_GLOBAL, (D_(u"General"), D_(u"Web widget")), callback=onWebWidget) - host.addListener('gotMenus', gotMenus) - -host_listener.addListener(hostReady)
--- a/src/browser/sat_browser/xmlui.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,506 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2011-2018 Jérôme Poisson <goffi@goffi.org> - -# This program 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. - -# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. - -from sat.core.log import getLogger -log = getLogger(__name__) -from sat_frontends.tools import xmlui -from sat_browser import strings -from sat_frontends.tools import jid -from sat_browser.constants import Const as C -from sat_browser import dialog -from sat_browser import html_tools -from sat_browser import contact_panel - -from pyjamas.ui.VerticalPanel import VerticalPanel -from pyjamas.ui.HorizontalPanel import HorizontalPanel -from pyjamas.ui.TabPanel import TabPanel -from pyjamas.ui.Grid import Grid -from pyjamas.ui.Label import Label -from pyjamas.ui.TextBox import TextBox -from pyjamas.ui.PasswordTextBox import PasswordTextBox -from pyjamas.ui.TextArea import TextArea -from pyjamas.ui.CheckBox import CheckBox -from pyjamas.ui.ListBox import ListBox -from pyjamas.ui.Button import Button -from pyjamas.ui.HTML import HTML - -import nativedom - - -class EmptyWidget(xmlui.EmptyWidget, Label): - - def __init__(self, xmlui_main, xmlui_parent): - Label.__init__(self, '') - - -class TextWidget(xmlui.TextWidget, Label): - - def __init__(self, xmlui_main, xmlui_parent, value): - Label.__init__(self, value) - - -class LabelWidget(xmlui.LabelWidget, TextWidget): - - def __init__(self, xmlui_main, xmlui_parent, value): - TextWidget.__init__(self, xmlui_main, xmlui_parent, value + ": ") - - -class JidWidget(xmlui.JidWidget, TextWidget): - - def __init__(self, xmlui_main, xmlui_parent, value): - TextWidget.__init__(self, xmlui_main, xmlui_parent, value) - - -class DividerWidget(xmlui.DividerWidget, HTML): - - def __init__(self, xmlui_main, xmlui_parent, style='line'): - """Add a divider - - @param xmlui_parent - @param style (unicode): one of: - - line: a simple line - - dot: a line of dots - - dash: a line of dashes - - plain: a full thick line - - blank: a blank line/space - """ - HTML.__init__(self, "<hr/>") - self.addStyleName(style) - - -class StringWidget(xmlui.StringWidget, TextBox): - - def __init__(self, xmlui_main, xmlui_parent, value, read_only=False): - TextBox.__init__(self) - self.setText(value) - self.setReadonly(read_only) - - def _xmluiSetValue(self, value): - self.setText(value) - - def _xmluiGetValue(self): - return self.getText() - - def _xmluiOnChange(self, callback): - self.addChangeListener(callback) - - -class JidInputWidget(xmlui.JidInputWidget, StringWidget): - - def __init__(self, xmlui_main, xmlui_parent, value, read_only=False): - StringWidget.__init__(self, xmlui_main, xmlui_parent, value, read_only) - - -class PasswordWidget(xmlui.PasswordWidget, PasswordTextBox): - - def __init__(self, xmlui_main, xmlui_parent, value, read_only=False): - PasswordTextBox.__init__(self) - self.setText(value) - self.setReadonly(read_only) - - def _xmluiSetValue(self, value): - self.setText(value) - - def _xmluiGetValue(self): - return self.getText() - - def _xmluiOnChange(self, callback): - self.addChangeListener(callback) - - -class TextBoxWidget(xmlui.TextBoxWidget, TextArea): - - def __init__(self, xmlui_main, xmlui_parent, value, read_only=False): - TextArea.__init__(self) - self.setText(value) - self.setReadonly(read_only) - - def _xmluiSetValue(self, value): - self.setText(value) - - def _xmluiGetValue(self): - return self.getText() - - def _xmluiOnChange(self, callback): - self.addChangeListener(callback) - - -class BoolWidget(xmlui.BoolWidget, CheckBox): - - def __init__(self, xmlui_main, xmlui_parent, state, read_only=False): - CheckBox.__init__(self) - self.setChecked(state) - self.setReadonly(read_only) - - def _xmluiSetValue(self, value): - self.setChecked(value == "true") - - def _xmluiGetValue(self): - return "true" if self.isChecked() else "false" - - def _xmluiOnChange(self, callback): - self.addClickListener(callback) - - -class IntWidget(xmlui.IntWidget, TextBox): - - def __init__(self, xmlui_main, xmlui_parent, value, read_only=False): - TextBox.__init__(self) - self.setText(value) - self.setReadonly(read_only) - - def _xmluiSetValue(self, value): - self.setText(value) - - def _xmluiGetValue(self): - return self.getText() - - def _xmluiOnChange(self, callback): - self.addChangeListener(callback) - - -class ButtonWidget(xmlui.ButtonWidget, Button): - - def __init__(self, xmlui_main, xmlui_parent, value, click_callback): - Button.__init__(self, value, click_callback) - - def _xmluiOnClick(self, callback): - self.addClickListener(callback) - - -class ListWidget(xmlui.ListWidget, ListBox): - - def __init__(self, xmlui_main, xmlui_parent, options, selected, flags): - ListBox.__init__(self) - multi_selection = 'single' not in flags - self.setMultipleSelect(multi_selection) - if multi_selection: - self.setVisibleItemCount(5) - for option in options: - self.addItem(option[1]) - self._xmlui_attr_map = {label: value for value, label in options} - self._xmluiSelectValues(selected) - - def _xmluiSelectValue(self, value): - """Select a value checking its item""" - try: - label = [label for label, _value in self._xmlui_attr_map.items() if _value == value][0] - except IndexError: - log.warning(u"Can't find value [%s] to select" % value) - return - self.selectItem(label) - - def _xmluiSelectValues(self, values): - """Select multiple values, ignore the items""" - self.setValueSelection(values) - - def _xmluiGetSelectedValues(self): - ret = [] - for label in self.getSelectedItemText(): - ret.append(self._xmlui_attr_map[label]) - return ret - - def _xmluiOnChange(self, callback): - self.addChangeListener(callback) - - def _xmluiAddValues(self, values, select=True): - selected = self._xmluiGetSelectedValues() - for value in values: - if value not in self._xmlui_attr_map.values(): - self.addItem(value) - self._xmlui_attr_map[value] = value - if value not in selected: - selected.append(value) - self._xmluiSelectValues(selected) - - -class JidsListWidget(contact_panel.ContactsPanel, xmlui.JidsListWidget): - - def __init__(self, xmlui_main, xmlui_parent, jids, styles): - contact_panel.ContactsPanel.__init__(self, xmlui_main.host, merge_resources=False) - self.addStyleName("xmlui-JidsListWidget") - self.setList([jid.JID(jid_) for jid_ in jids]) - - def _xmluiGetSelectedValues(self): - # XXX: there is not selection in this list, so we return all non empty values - return self.getJids() - - - -class LiberviaContainer(object): - - def _xmluiAppend(self, widget): - self.append(widget) - - -class AdvancedListContainer(xmlui.AdvancedListContainer, Grid): - - def __init__(self, xmlui_main, xmlui_parent, columns, selectable='no'): - Grid.__init__(self, 0, columns) - self.columns = columns - self.row = -1 - self.col = 0 - self._xmlui_rows_idx = [] - self._xmlui_selectable = selectable != 'no' - self._xmlui_selected_row = None - self.addTableListener(self) - if self._xmlui_selectable: - self.addStyleName('AdvancedListSelectable') - - def onCellClicked(self, grid, row, col): - if not self._xmlui_selectable: - return - self._xmlui_selected_row = row - try: - self._xmlui_select_cb(self) - except AttributeError: - log.warning("no select callback set") - - def _xmluiAppend(self, widget): - self.setWidget(self.row, self.col, widget) - self.col += 1 - - def _xmluiAddRow(self, idx): - self.row += 1 - self.col = 0 - self._xmlui_rows_idx.insert(self.row, idx) - self.resizeRows(self.row + 1) - - def _xmluiGetSelectedWidgets(self): - return [self.getWidget(self._xmlui_selected_row, col) for col in range(self.columns)] - - def _xmluiGetSelectedIndex(self): - try: - return self._xmlui_rows_idx[self._xmlui_selected_row] - except TypeError: - return None - - def _xmluiOnSelect(self, callback): - self._xmlui_select_cb = callback - - -class PairsContainer(xmlui.PairsContainer, Grid): - - def __init__(self, xmlui_main, xmlui_parent): - Grid.__init__(self, 0, 0) - self.row = 0 - self.col = 0 - - def _xmluiAppend(self, widget): - if self.col == 0: - self.resize(self.row + 1, 2) - self.setWidget(self.row, self.col, widget) - self.col += 1 - if self.col == 2: - self.row += 1 - self.col = 0 - - -class LabelContainer(PairsContainer, xmlui.LabelContainer): - pass - - -class TabsContainer(LiberviaContainer, xmlui.TabsContainer, TabPanel): - - def __init__(self, xmlui_main, xmlui_parent): - TabPanel.__init__(self) - self.setStyleName('liberviaTabPanel') - - def _xmluiAddTab(self, label, selected): - tab_panel = VerticalContainer(self) - self.add(tab_panel, label) - count = len(self.getChildren()) - if count == 1 or selected: - self.selectTab(count - 1) - return tab_panel - - -class VerticalContainer(LiberviaContainer, xmlui.VerticalContainer, VerticalPanel): - __bases__ = (LiberviaContainer, xmlui.VerticalContainer, VerticalPanel) - - def __init__(self, xmlui_main, xmlui_parent): - VerticalPanel.__init__(self) - - -## Dialogs ## - - -class Dlg(object): - - def _xmluiShow(self): - self.show() - - def _xmluiClose(self): - pass - - -class MessageDialog(Dlg, xmlui.MessageDialog, dialog.InfoDialog): - - def __init__(self, xmlui_main, xmlui_parent, title, message, level): - #TODO: level is not managed - title = html_tools.html_sanitize(title) - message = strings.addURLToText(html_tools.XHTML2Text(message)) - Dlg.__init__(self) - xmlui.MessageDialog.__init__(self, xmlui_main, xmlui_parent) - dialog.InfoDialog.__init__(self, title, message, self._xmluiValidated()) - - -class NoteDialog(xmlui.NoteDialog, MessageDialog): - # TODO: separate NoteDialog - - def __init__(self, xmlui_main, xmlui_parent, title, message, level): - xmlui.NoteDialog.__init__(self, xmlui_main, xmlui_parent) - MessageDialog.__init__(self, xmlui_main, xmlui_parent, title, message, level) - - -class ConfirmDialog(xmlui.ConfirmDialog, Dlg, dialog.ConfirmDialog): - - def __init__(self, xmlui_main, xmlui_parent, title, message, level): - #TODO: level is not managed - title = html_tools.html_sanitize(title) - message = strings.addURLToText(html_tools.XHTML2Text(message)) - xmlui.ConfirmDialog.__init__(self, xmlui_main, xmlui_parent) - Dlg.__init__(self) - dialog.ConfirmDialog.__init__(self, self.answered, message, title) - - def answered(self, validated): - if validated: - self._xmluiValidated() - else: - self._xmluiCancelled() - - -class FileDialog(xmlui.FileDialog, Dlg): - #TODO: - - def __init__(self, xmlui_main, xmlui_parent, title, message, level, filetype): - raise NotImplementedError("FileDialog is not implemented in Libervia yet") - - -class GenericFactory(object): - # XXX: __getattr__ doens't work here with pyjamas for an unknown reason - # so an introspection workaround is used - - def __init__(self, xmlui_main): - self.xmlui_main = xmlui_main - for name, cls in globals().items(): - if name.endswith("Widget") or name.endswith("Container") or name.endswith("Dialog"): - log.info("registering: %s" % name) - def createCreater(cls): - return lambda *args, **kwargs: self._genericCreate(cls, *args, **kwargs) - setattr(self, "create%s" % name, createCreater(cls)) - - def _genericCreate(self, cls, *args, **kwargs): - instance = cls(self.xmlui_main, *args, **kwargs) - return instance - - # def __getattr__(self, attr): - # if attr.startswith("create"): - # cls = globals()[attr[6:]] - # cls._xmlui_main = self._xmlui_main - # return cls - - -class WidgetFactory(GenericFactory): - - def _genericCreate(self, cls, *args, **kwargs): - instance = GenericFactory._genericCreate(self, cls, *args, **kwargs) - return instance - -class LiberviaXMLUIBase(object): - - def _xmluiLaunchAction(self, action_id, data): - self.host.launchAction(action_id, data, callback=self._defaultCb) - - -class XMLUIPanel(LiberviaXMLUIBase, xmlui.XMLUIPanel, VerticalPanel): - - def __init__(self, host, parsed_xml, title=None, flags=None, callback=None, ignore=None, whitelist=None, profile=C.PROF_KEY_NONE): - self.widget_factory = WidgetFactory(self) - self.host = host - VerticalPanel.__init__(self) - self.setSize('100%', '100%') - xmlui.XMLUIPanel.__init__(self, - host, - parsed_xml, - title = title, - flags = flags, - callback = callback, - profile = profile) - - def setCloseCb(self, close_cb): - self.close_cb = close_cb - - def _xmluiClose(self): - if self.close_cb: - self.close_cb() - else: - log.warning("no close method defined") - - def _xmluiSetParam(self, name, value, category): - self.host.bridge.call('setParam', None, name, value, category) - - def constructUI(self, parsed_dom): - super(XMLUIPanel, self).constructUI(parsed_dom) - self.add(self.main_cont) - self.setCellHeight(self.main_cont, '100%') - if self.type == 'form': - hpanel = HorizontalPanel() - hpanel.setStyleName('marginAuto') - hpanel.add(Button('Submit', self.onFormSubmitted)) - if not 'NO_CANCEL' in self.flags: - hpanel.add(Button('Cancel', self.onFormCancelled)) - self.add(hpanel) - elif self.type == 'param': - assert(isinstance(self.children[0][0], TabPanel)) - hpanel = HorizontalPanel() - hpanel.setStyleName('marginAuto') - hpanel.add(Button('Save', self.onSaveParams)) - hpanel.add(Button('Cancel', lambda ignore: self._xmluiClose())) - self.add(hpanel) - - def show(self): - options = ['NO_CLOSE'] if self.type == C.XMLUI_FORM else [] - _dialog = dialog.GenericDialog(self.xmlui_title, self, options=options) - self.setCloseCb(_dialog.close) - _dialog.show() - - -class XMLUIDialog(LiberviaXMLUIBase, xmlui.XMLUIDialog): - dialog_factory = GenericFactory() - - def __init__(self, host, parsed_dom, title=None, flags=None, callback=None, ignore=None, whitelist=None, profile=C.PROF_KEY_NONE): - self.dialog_factory = GenericFactory(self) - xmlui.XMLUIDialog.__init__(self, - host, - parsed_dom, - title=title, - flags=flags, - callback=callback, - ignore=ignore, - profile=profile) - self.host = host - -xmlui.registerClass(xmlui.CLASS_PANEL, XMLUIPanel) -xmlui.registerClass(xmlui.CLASS_DIALOG, XMLUIDialog) - -def create(*args, **kwargs): - dom = nativedom.NativeDOM() - kwargs['dom_parse'] = lambda xml_data: dom.parseString(xml_data) - return xmlui.create(*args, **kwargs)
--- a/src/common/constants.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,67 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a SAT frontend -# Copyright (C) 2009-2018 Jérôme Poisson (goffi@goffi.org) - -# This program 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. - -# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. - -from sat_frontends.quick_frontend import constants -import os.path - - -class Const(constants.Const): - - # XXX: we don't want to use the APP_VERSION inherited from sat.core.constants version - # as we use this version to check that there is not a mismatch with backend - APP_VERSION = u"0.7.0D" # Please add 'D' at the end for dev versions - LIBERVIA_MAIN_PAGE = "libervia.html" - - # REGISTRATION - # XXX: for now libervia forces the creation to lower case - # XXX: Regex patterns must be compatible with both Python and JS - REG_LOGIN_RE = r"^[a-z0-9_-]+$" - REG_EMAIL_RE = r"^.+@.+\..+" - PASSWORD_MIN_LENGTH = 6 - - # HTTP REQUEST RESULT VALUES - PROFILE_AUTH_ERROR = "PROFILE AUTH ERROR" - XMPP_AUTH_ERROR = "XMPP AUTH ERROR" - ALREADY_WAITING = "ALREADY WAITING" - SESSION_ACTIVE = "SESSION ACTIVE" - NOT_CONNECTED = "NOT CONNECTED" - PROFILE_LOGGED = "LOGGED" - PROFILE_LOGGED_EXT_JID = "LOGGED (REGISTERED WITH EXTERNAL JID)" - ALREADY_EXISTS = "ALREADY EXISTS" - REGISTRATION_SUCCEED = "REGISTRATION" - INTERNAL_ERROR = "INTERNAL ERROR" - INVALID_INPUT = "INVALID INPUT" - BAD_REQUEST = "BAD REQUEST" - NO_REPLY = "NO REPLY" - NOT_ALLOWED = "NOT ALLOWED" - UPLOAD_OK = "UPLOAD OK" - UPLOAD_KO = "UPLOAD KO" - - # directories - MEDIA_DIR = "media/" - CACHE_DIR = "cache" - - # avatars - DEFAULT_AVATAR_FILE = "default_avatar.png" - DEFAULT_AVATAR_URL = os.path.join(MEDIA_DIR, "misc", DEFAULT_AVATAR_FILE) - EMPTY_AVATAR_FILE = "empty_avatar" - EMPTY_AVATAR_URL = os.path.join(MEDIA_DIR, "misc", EMPTY_AVATAR_FILE) - - # blog - MAM_FILTER_CATEGORY = "http://salut-a-toi.org/protocols/mam_filter_category"
--- a/src/libervia.sh Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,107 +0,0 @@ -#!/bin/sh - -DEBUG="" -DAEMON="" -PYTHON="python2" -TWISTD="$(which twistd)" - -kill_process() { - # $1 is the file containing the PID to kill, $2 is the process name - if [ -f $1 ]; then - PID=`cat $1` - if ps -p $PID > /dev/null; then - echo "Terminating $2... " - kill -INT $PID - else - echo "No running process of ID $PID... removing PID file" - rm -f $1 - fi - else - echo "$2 is probably not running (PID file doesn't exist)" - fi -} - -#We use python to parse config files -eval `"$PYTHON" << PYTHONEND -from libervia.server.constants import Const as C -from sat.memory.memory import fixLocalDir -from ConfigParser import SafeConfigParser -from os.path import expanduser, join -import sys -import codecs -import locale - -sys.stdout = codecs.getwriter(locale.getpreferredencoding())(sys.stdout) - -fixLocalDir() # XXX: tmp update code, will be removed in the future - -config = SafeConfigParser(defaults=C.DEFAULT_CONFIG) -try: - config.read(C.CONFIG_FILES) -except: - print ("echo \"/!\\ Can't read main config ! Please check the syntax\";") - print ("exit 1") - sys.exit() - -env=[] -env.append("PID_DIR='%s'" % join(expanduser(config.get('DEFAULT', 'pid_dir')),'')) -env.append("LOG_DIR='%s'" % join(expanduser(config.get('DEFAULT', 'log_dir')),'')) -env.append("APP_NAME='%s'" % C.APP_NAME) -env.append("APP_NAME_FILE='%s'" % C.APP_NAME_FILE) -print ";".join(env) -PYTHONEND -` -APP_NAME="$APP_NAME" -PID_FILE="$PID_DIR$APP_NAME_FILE.pid" -LOG_FILE="$LOG_DIR$APP_NAME_FILE.log" -RUNNING_MSG="$APP_NAME is running" -NOT_RUNNING_MSG="$APP_NAME is *NOT* running" - -# if there is one argument which is "stop", then we kill Libervia -if [ $# -ge 1 ];then - if [ $1 = "stop" ];then - kill_process $PID_FILE "$APP_NAME" - exit 0 - elif [ $1 = "debug" ];then - echo "Launching $APP_NAME in debug mode" - DEBUG="--debug" - elif [ $1 = "fg" ];then - echo "Launching $APP_NAME in foreground mode" - DAEMON="n" - elif [ $1 = "status" ];then - if [ -f $PID_FILE ]; then - PID=`cat $PID_FILE` - ps -p$PID 2>&1 > /dev/null - if [ $? = 0 ];then - echo "$RUNNING_MSG (pid: $PID)" - exit 0 - else - echo "$NOT_RUNNING_MSG, but a pid file is present (bad exit ?): $PID_FILE" - exit 2 - fi - else - echo "$NOT_RUNNING_MSG" - exit 1 - fi - else - echo "bad argument, please use one of (stop, debug, fg, status) or no argument" - exit 1 - fi - shift -fi - - -#Don't change the next lines -PLUGIN_OPTIONS="" -AUTO_OPTIONS="" -ADDITIONAL_OPTIONS="--pidfile $PID_FILE --logfile $LOG_FILE $AUTO_OPTIONS $DEBUG" - - -MAIN_OPTIONS="-${DAEMON}o" - -log_dir=`dirname "$LOG_FILE"` -if [ ! -d $log_dir ] ; then - mkdir $log_dir -fi - -exec $PYTHON $TWISTD $MAIN_OPTIONS $ADDITIONAL_OPTIONS $APP_NAME_FILE $PLUGIN_OPTIONS $@
--- a/src/pages/app/page_meta.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,5 +0,0 @@ -#!/usr/bin/env python2.7 -# -*- coding: utf-8 -*- - -name = u"app" -template = u"app/app.html"
--- a/src/pages/blog/page_meta.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,56 +0,0 @@ -#!/usr/bin/env python2.7 -# -*- coding: utf-8 -*- -from libervia.server.constants import Const as C -from twisted.words.protocols.jabber import jid -from twisted.internet import defer -from libervia.server import session_iface -from sat.core.log import getLogger - -log = getLogger("pages/blog") - -name = u"blog" -access = C.PAGES_ACCESS_PUBLIC -template = u"blog/discover.html" - - -@defer.inlineCallbacks -def prepare_render(self, request): - profile = self.getProfile(request) - template_data = request.template_data - if profile is not None: - __, entities_own, entities_roster = yield self.host.bridgeCall( - "discoFindByFeatures", - [], - [(u"pubsub", u"pep")], - True, - False, - True, - True, - True, - profile, - ) - entities = template_data[u"disco_entities"] = ( - entities_own.keys() + entities_roster.keys() - ) - entities_url = template_data[u"entities_url"] = {} - identities = template_data[u"identities"] = self.host.getSessionData( - request, session_iface.ISATSession - ).identities - for entity_jid_s in entities: - entities_url[entity_jid_s] = self.getPageByName("blog_view").getURL( - entity_jid_s - ) - if entity_jid_s not in identities: - identities[entity_jid_s] = yield self.host.bridgeCall( - u"identityGet", entity_jid_s, profile - ) - - -def on_data_post(self, request): - jid_str = self.getPostedData(request, u"jid") - try: - jid_ = jid.JID(jid_str) - except RuntimeError: - self.pageError(request, C.HTTP_BAD_REQUEST) - url = self.getPageByName(u"blog_view").getURL(jid_.full()) - self.HTTPRedirect(request, url)
--- a/src/pages/blog/view/atom.xml/page_meta.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,41 +0,0 @@ -#!/usr/bin/env python2.7 -# -*- coding: utf-8 -*- - -from libervia.server.constants import Const as C -from twisted.internet import defer -from sat.tools.common import uri -import time - -name = u"blog_feed_atom" -access = C.PAGES_ACCESS_PUBLIC -template = u"blog/atom.xml" - - -@defer.inlineCallbacks -def prepare_render(self, request): - request.setHeader("Content-Type", "application/atom+xml; charset=utf-8") - data = self.getRData(request) - service, node = data[u"service"], data.get(u"node") - self.checkCache( - request, C.CACHE_PUBSUB, service=service, node=node, short="microblog" - ) - data["show_comments"] = False - template_data = request.template_data - blog_page = self.getPageByName(u"blog_view") - yield blog_page.prepare_render(self, request) - items = data[u"items"] - - template_data[u"request_uri"] = self.host.getExtBaseURL( - request, request.path.decode("utf-8") - ) - template_data[u"xmpp_uri"] = uri.buildXMPPUri( - u"pubsub", subtype=u"microblog", path=service.full(), node=node - ) - blog_view = self.getPageByName(u"blog_view") - template_data[u"http_uri"] = self.host.getExtBaseURL( - request, blog_view.getURL(service.full(), node) - ) - if items: - template_data[u"updated"] = items[0].updated - else: - template_data[u"updated"] = time.time()
--- a/src/pages/blog/view/page_meta.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,299 +0,0 @@ -#!/usr/bin/env python2.7 -# -*- coding: utf-8 -*- -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 libervia.server import utils -import unicodedata -import re -import cgi -from sat.core.log import getLogger -log = getLogger('pages/blog/view') - -"""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: - data['node'] = self.nextPath(request) - except IndexError: - data['node'] = u'' - else: - if data['node'] == u'@': - data['node'] = u'' - - try: - filter_kw = data['filter_keyword'] = self.nextPath(request) - except IndexError: - pass - else: - if 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) - - -@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, - [], - {}, - 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 getBlogData(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 = ([], {}) - - items = data_objects.BlogItems(blog_data) - defer.returnValue((blog_data, items)) - -@defer.inlineCallbacks -def prepare_render(self, request): - data = self.getRData(request) - # 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 - - ## pagination/filtering parameters - params = self.getAllPostedData(request, multiple=False) - if item_id: - extra = {} - else: - extra = {u'rsm_max': u'10'} - if u'after' in params: - extra[u'rsm_after'] = params[u'after'] - elif u'before' in params: - extra[u'rsm_before'] = params[u'before'] - 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 - blog_data, items = yield getBlogData(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: - last_id = items[-1].id - template_data['older_url'] = self.getParamURL(request, after=last_id) - if u'before' in params or u'after' in params: - first_id = items[0].id - template_data['newer_url'] = self.getParamURL(request, before=first_id) - 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) - - # no items, we have requested items before last post, or blog is empty - extra = {u'rsm_max': u'10'} - blog_data, items = yield getBlogData(self, request, service, node, None, extra, profile) - if items: - last_id = items[-1].id - template_data['older_url'] = self.getParamURL(request, after=last_id) - - ## identities ## - # identities are use 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', False) - - # 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_UNAUTHORIZED) - 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, 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_))
--- a/src/pages/chat/page_meta.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,145 +0,0 @@ -#!/usr/bin/env python2.7 -# -*- coding: utf-8 -*- - -from sat.core.i18n import _ -from twisted.internet import defer -from sat.core.log import getLogger - -log = getLogger("pages/chat") -from sat.tools.common import data_objects -from twisted.words.protocols.jabber import jid -from libervia.server.constants import Const as C -from libervia.server import session_iface - -name = u"chat" -access = C.PAGES_ACCESS_PROFILE -template = u"chat/chat.html" -dynamic = True - - -def parse_url(self, request): - rdata = self.getRData(request) - - try: - target_jid_s = self.nextPath(request) - except IndexError: - # not chat jid, we redirect to jid selection page - self.pageRedirect(u"chat_select", request) - - try: - target_jid = jid.JID(target_jid_s) - if not target_jid.user: - raise ValueError(_(u"invalid jid for chat (no local part)")) - except Exception as e: - log.warning( - _(u"bad chat jid entered: {jid} ({msg})").format(jid=target_jid, msg=e) - ) - self.pageError(request, C.HTTP_BAD_REQUEST) - else: - rdata["target"] = target_jid - - -@defer.inlineCallbacks -def prepare_render(self, request): - # FIXME: bug on room filtering (currently display messages from all rooms) - session = self.host.getSessionData(request, session_iface.ISATSession) - template_data = request.template_data - rdata = self.getRData(request) - target_jid = rdata["target"] - profile = session.profile - profile_jid = session.jid - - disco = yield self.host.bridgeCall(u"discoInfos", target_jid.host, u"", True, profile) - if "conference" in [i[0] for i in disco[1]]: - chat_type = C.CHAT_GROUP - join_ret = yield self.host.bridgeCall( - u"mucJoin", target_jid.userhost(), "", "", profile - ) - already_joined, room_jid_s, occupants, user_nick, room_subject, dummy = join_ret - template_data[u"subject"] = room_subject - own_jid = jid.JID(room_jid_s) - own_jid.resource = user_nick - else: - chat_type = C.CHAT_ONE2ONE - own_jid = profile_jid - rdata["chat_type"] = chat_type - template_data["own_jid"] = own_jid - - self.registerSignal(request, u"messageNew") - history = yield self.host.bridgeCall( - u"historyGet", - profile_jid.userhost(), - target_jid.userhost(), - 20, - True, - {}, - profile, - ) - authors = {m[2] for m in history} - identities = {} - for author in authors: - identities[author] = yield self.host.bridgeCall(u"identityGet", author, profile) - - template_data[u"messages"] = data_objects.Messages(history) - template_data[u"identities"] = identities - template_data[u"target_jid"] = target_jid - template_data[u"chat_type"] = chat_type - - -def on_data(self, request, data): - session = self.host.getSessionData(request, session_iface.ISATSession) - rdata = self.getRData(request) - target = rdata["target"] - data_type = data.get(u"type", "") - if data_type == "msg": - message = data[u"body"] - mess_type = ( - C.MESS_TYPE_GROUPCHAT - if rdata["chat_type"] == C.CHAT_GROUP - else C.MESS_TYPE_CHAT - ) - log.debug(u"message received: {}".format(message)) - self.host.bridgeCall( - u"messageSend", - target.full(), - {u"": message}, - {}, - mess_type, - {}, - session.profile, - ) - else: - log.warning(u"unknown message type: {type}".format(type=data_type)) - - -@defer.inlineCallbacks -def on_signal(self, request, signal, *args): - if signal == "messageNew": - rdata = self.getRData(request) - template_data = request.template_data - template_data_update = {u"msg": data_objects.Message((args))} - target_jid = rdata["target"] - identities = template_data["identities"] - uid, timestamp, from_jid_s, to_jid_s, message, subject, mess_type, extra, dummy = ( - args - ) - from_jid = jid.JID(from_jid_s) - to_jid = jid.JID(to_jid_s) - if ( - target_jid.userhostJID() != from_jid.userhostJID() - and target_jid.userhostJID() != to_jid.userhostJID() - ): - # the message is not linked with page's room/user - return - - if from_jid_s not in identities: - profile = self.getProfile(request) - identities[from_jid_s] = yield self.host.bridgeCall( - u"identityGet", from_jid_s, profile - ) - template_data_update["identities"] = identities - self.renderAndUpdate( - request, u"chat/message.html", "#messages", template_data_update - ) - else: - log.error(_(u"Unexpected signal: {signal}").format(signal=signal))
--- a/src/pages/chat/select/page_meta.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,45 +0,0 @@ -#!/usr/bin/env python2.7 -# -*- coding: utf-8 -*- - -from sat.core.i18n import _ -from libervia.server.constants import Const as C -from twisted.internet import defer -from twisted.words.protocols.jabber import jid -from sat.tools.common import data_objects -from sat.core.log import getLogger - -log = getLogger("pages/chat_select") - -name = u"chat_select" -access = C.PAGES_ACCESS_PROFILE -template = u"chat/select.html" - - -@defer.inlineCallbacks -def prepare_render(self, request): - profile = self.getProfile(request) - template_data = request.template_data - rooms = template_data["rooms"] = [] - bookmarks = yield self.host.bridgeCall("bookmarksList", "muc", "all", profile) - for bm_values in bookmarks.values(): - for room_jid, room_data in bm_values.iteritems(): - url = self.getPageByName(u"chat").getURL(room_jid) - rooms.append(data_objects.Room(room_jid, name=room_data.get("name"), url=url)) - rooms.sort(key=lambda r: r.name) - - -@defer.inlineCallbacks -def on_data_post(self, request): - jid_ = self.getPostedData(request, u"jid") - if u"@" not in jid_: - profile = self.getProfile(request) - service = yield self.host.bridgeCall("mucGetService", "", profile) - if service: - muc_jid = jid.JID(service) - muc_jid.user = jid_ - jid_ = muc_jid.full() - else: - log.warning(_(u"Invalid jid received: {jid}".format(jid=jid_))) - defer.returnValue(C.POST_NO_CONFIRM) - url = self.getPageByName(u"chat").getURL(jid_) - self.HTTPRedirect(request, url)
--- a/src/pages/events/admin/page_meta.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,173 +0,0 @@ -#!/usr/bin/env python2.7 -# -*- coding: utf-8 -*- - -from libervia.server.constants import Const as C -from twisted.internet import defer -from twisted.words.protocols.jabber import jid -from sat.tools.common.template import safe -from sat.core.i18n import _ -from sat.core.log import getLogger -import time -import cgi -import math -import re - -name = u"event_admin" -access = C.PAGES_ACCESS_PROFILE -template = u"event/admin.html" -log = getLogger(u"pages/" + name) -REG_EMAIL_RE = re.compile(C.REG_EMAIL_RE, re.IGNORECASE) - - -def parse_url(self, request): - self.getPathArgs( - request, - ("event_service", "event_node", "event_id"), - min_args=2, - event_service="@jid", - event_id="", - ) - - -@defer.inlineCallbacks -def prepare_render(self, request): - data = self.getRData(request) - template_data = request.template_data - - ## Event ## - - event_service = template_data[u"event_service"] = data["event_service"] - event_node = template_data[u"event_node"] = data["event_node"] - event_id = template_data[u"event_id"] = data["event_id"] - profile = self.getProfile(request) - event_timestamp, event_data = yield self.host.bridgeCall( - u"eventGet", - event_service.userhost() if event_service else "", - event_node, - event_id, - profile, - ) - try: - background_image = event_data.pop("background-image") - except KeyError: - pass - else: - template_data["dynamic_style"] = safe( - u""" - html { - background-image: url("%s"); - background-size: 15em; - } - """ - % cgi.escape(background_image, True) - ) - template_data["event"] = event_data - invitees = yield self.host.bridgeCall( - u"eventInviteesList", - event_data["invitees_service"], - event_data["invitees_node"], - profile, - ) - template_data["invitees"] = invitees - invitees_guests = 0 - for invitee_data in invitees.itervalues(): - if invitee_data.get("attend", "no") == "no": - continue - try: - invitees_guests += int(invitee_data.get("guests", 0)) - except ValueError: - log.warning( - _(u"guests value is not valid: {invitee}").format(invitee=invitee_data) - ) - template_data["invitees_guests"] = invitees_guests - template_data["days_left"] = int( - math.ceil((event_timestamp - time.time()) / (60 * 60 * 24)) - ) - - ## Blog ## - - data[u"service"] = jid.JID(event_data[u"blog_service"]) - data[u"node"] = event_data[u"blog_node"] - data[u"allow_commenting"] = u"simple" - - # we now need blog items, using blog common page - # this will fill the "items" template data - blog_page = self.getPageByName(u"blog_view") - yield blog_page.prepare_render(self, request) - - -@defer.inlineCallbacks -def on_data_post(self, request): - profile = self.getProfile(request) - if not profile: - log.error(u"got post data without profile") - self.pageError(request, C.HTTP_INTERNAL_ERROR) - type_ = self.getPostedData(request, "type") - if type_ == "blog": - service, node, title, body, lang = self.getPostedData( - request, (u"service", u"node", u"title", u"body", u"language") - ) - - if not body.strip(): - self.pageError(request, C.HTTP_BAD_REQUEST) - data = {u"content": body} - if title: - data[u"title"] = title - if lang: - data[u"language"] = lang - try: - comments = bool(self.getPostedData(request, u"comments").strip()) - except KeyError: - pass - else: - if comments: - data[u"allow_comments"] = C.BOOL_TRUE - - try: - yield self.host.bridgeCall(u"mbSend", service, node, data, profile) - except Exception as e: - if u"forbidden" in unicode(e): - self.pageError(request, C.HTTP_UNAUTHORIZED) - else: - raise e - elif type_ == "event": - service, node, event_id, jids, emails = self.getPostedData( - request, (u"service", u"node", u"event_id", u"jids", u"emails") - ) - for invitee_jid_s in jids.split(): - try: - invitee_jid = jid.JID(invitee_jid_s) - except RuntimeError as e: - log.warning( - _(u"this is not a valid jid: {jid}").format(jid=invitee_jid_s) - ) - continue - yield self.host.bridgeCall( - "eventInvite", invitee_jid.userhost(), service, node, event_id, profile - ) - for email_addr in emails.split(): - if not REG_EMAIL_RE.match(email_addr): - log.warning( - _(u"this is not a valid email address: {email}").format( - email=email_addr - ) - ) - continue - yield self.host.bridgeCall( - "eventInviteByEmail", - service, - node, - event_id, - email_addr, - {}, - u"", - u"", - u"", - u"", - u"", - u"", - profile, - ) - - else: - log.warning(_(u"Unhandled data type: {}").format(type_))
--- a/src/pages/events/new/page_meta.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,39 +0,0 @@ -#!/usr/bin/env python2.7 -# -*- coding: utf-8 -*- - -from libervia.server.constants import Const as C -from twisted.internet import defer -from sat.core.log import getLogger -from sat.tools.common import date_utils - -"""creation of new events""" - -name = u"event_new" -access = C.PAGES_ACCESS_PROFILE -template = u"event/create.html" -log = getLogger(u"pages/" + name) - - -@defer.inlineCallbacks -def on_data_post(self, request): - request_data = self.getRData(request) - profile = self.getProfile(request) - title, location, body, date, main_img, bg_img = self.getPostedData( - request, ("name", "location", "body", "date", "main_image", "bg_image") - ) - timestamp = date_utils.date_parse(date) - data = {"name": title, "description": body, "location": location} - - for value, var in ((main_img, "image"), (bg_img, "background-image")): - value = value.strip() - if not value: - continue - if not value.startswith("http"): - self.pageError(request, C.HTTP_BAD_REQUEST) - data[var] = value - data[u"register"] = C.BOOL_TRUE - node = yield self.host.bridgeCall("eventCreate", timestamp, data, "", "", "", profile) - log.info(u"Event node created at {node}".format(node=node)) - - request_data["post_redirect_page"] = (self.getPageByName(u"event_admin"), "@", node) - defer.returnValue(C.POST_NO_CONFIRM)
--- a/src/pages/events/page_meta.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,50 +0,0 @@ -#!/usr/bin/env python2.7 -# -*- coding: utf-8 -*- - -from libervia.server.constants import Const as C -from twisted.internet import defer -from sat.core.i18n import _ -from sat.core.log import getLogger - -log = getLogger("pages/ticket") -"""ticket handling pages""" - -name = u"events" -access = C.PAGES_ACCESS_PUBLIC -template = u"event/overview.html" - - -@defer.inlineCallbacks -def parse_url(self, request): - profile = self.getProfile(request) - template_data = request.template_data - template_data[u"url_event_new"] = self.getSubPageURL(request, "event_new") - if profile is not None: - try: - events = yield self.host.bridgeCall("eventsList", "", "", profile) - except Exception: - log.warning(_(u"Can't get events list for {profile}").format(profile=profile)) - else: - own_events = [] - other_events = [] - for event in events: - if C.bool(event.get("creator", C.BOOL_FALSE)): - own_events.append(event) - event["url"] = self.getSubPageURL( - request, - u"event_admin", - event.get("service", ""), - event.get("node", ""), - event.get("item"), - ) - else: - other_events.append(event) - event["url"] = self.getSubPageURL( - request, - u"event_rsvp", - event.get("service", ""), - event.get("node", ""), - event.get("item"), - ) - - template_data[u"events"] = own_events + other_events
--- a/src/pages/events/rsvp/page_meta.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,99 +0,0 @@ -#!/usr/bin/env python2.7 -# -*- coding: utf-8 -*- - -from libervia.server.constants import Const as C -from sat.core.i18n import _ -from twisted.internet import defer -from twisted.words.protocols.jabber import jid -from sat.core.log import getLogger -from sat.tools.common.template import safe -import time -import cgi - -"""creation of new events""" - -name = u"event_rsvp" -access = C.PAGES_ACCESS_PROFILE -template = u"event/invitation.html" -log = getLogger(u"pages/" + name) - - -def parse_url(self, request): - self.getPathArgs( - request, - ("event_service", "event_node", "event_id"), - min_args=2, - event_service="@jid", - event_id="", - ) - - -@defer.inlineCallbacks -def prepare_render(self, request): - template_data = request.template_data - data = self.getRData(request) - profile = self.getProfile(request) - - ## Event ## - - event_service = data["event_service"] - event_node = data[u"event_node"] - event_id = data[u"event_id"] - event_timestamp, event_data = yield self.host.bridgeCall( - u"eventGet", - event_service.userhost() if event_service else "", - event_node, - event_id, - profile, - ) - try: - background_image = event_data.pop("background-image") - except KeyError: - pass - else: - template_data["dynamic_style"] = safe( - u""" - html { - background-image: url("%s"); - background-size: 15em; - } - """ - % cgi.escape(background_image, True) - ) - template_data["event"] = event_data - event_invitee_data = yield self.host.bridgeCall( - u"eventInviteeGet", - event_data["invitees_service"], - event_data["invitees_node"], - profile, - ) - template_data["invitee"] = event_invitee_data - template_data["days_left"] = int((event_timestamp - time.time()) / (60 * 60 * 24)) - - ## Blog ## - - data[u"service"] = jid.JID(event_data[u"blog_service"]) - data[u"node"] = event_data[u"blog_node"] - data[u"allow_commenting"] = u"simple" - - # we now need blog items, using blog common page - # this will fill the "items" template data - blog_page = self.getPageByName(u"blog_view") - yield blog_page.prepare_render(self, request) - - -@defer.inlineCallbacks -def on_data_post(self, request): - type_ = self.getPostedData(request, u"type") - if type_ == u"comment": - blog_page = self.getPageByName(u"blog_view") - yield blog_page.on_data_post(self, request) - elif type_ == u"attendance": - profile = self.getProfile(request) - service, node, attend, guests = self.getPostedData( - request, (u"service", u"node", u"attend", u"guests") - ) - data = {u"attend": attend, u"guests": guests} - yield self.host.bridgeCall(u"eventInviteeSet", service, node, data, profile) - else: - log.warning(_(u"Unhandled data type: {}").format(type_))
--- a/src/pages/events/view/page_meta.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,96 +0,0 @@ -#!/usr/bin/env python2.7 -# -*- coding: utf-8 -*- - -from libervia.server.constants import Const as C -from sat.core.i18n import _ -from twisted.internet import defer -from twisted.words.protocols.jabber import jid -from libervia.server import session_iface -from sat.tools.common import uri -from sat.tools.common.template import safe -import time -import cgi -from sat.core.log import getLogger - -name = u"event_view" -access = C.PAGES_ACCESS_PROFILE -template = u"event/invitation.html" -log = getLogger(u"pages/" + name) - - -@defer.inlineCallbacks -def prepare_render(self, request): - template_data = request.template_data - guest_session = self.host.getSessionData(request, session_iface.ISATGuestSession) - try: - event_uri = guest_session.data["event_uri"] - except KeyError: - log.warning(_(u"event URI not found, can't render event page")) - self.pageError(request, C.HTTP_SERVICE_UNAVAILABLE) - - data = self.getRData(request) - - ## Event ## - - event_uri_data = uri.parseXMPPUri(event_uri) - if event_uri_data[u"type"] != u"pubsub": - self.pageError(request, C.HTTP_SERVICE_UNAVAILABLE) - - event_service = template_data[u"event_service"] = jid.JID(event_uri_data[u"path"]) - event_node = template_data[u"event_node"] = event_uri_data[u"node"] - event_id = template_data[u"event_id"] = event_uri_data.get(u"item", "") - profile = self.getProfile(request) - event_timestamp, event_data = yield self.host.bridgeCall( - u"eventGet", event_service.userhost(), event_node, event_id, profile - ) - try: - background_image = event_data.pop("background-image") - except KeyError: - pass - else: - template_data["dynamic_style"] = safe( - u""" - html { - background-image: url("%s"); - background-size: 15em; - } - """ - % cgi.escape(background_image, True) - ) - template_data["event"] = event_data - event_invitee_data = yield self.host.bridgeCall( - u"eventInviteeGet", - event_data["invitees_service"], - event_data["invitees_node"], - profile, - ) - template_data["invitee"] = event_invitee_data - template_data["days_left"] = int((event_timestamp - time.time()) / (60 * 60 * 24)) - - ## Blog ## - - data[u"service"] = jid.JID(event_data[u"blog_service"]) - data[u"node"] = event_data[u"blog_node"] - data[u"allow_commenting"] = u"simple" - - # we now need blog items, using blog common page - # this will fill the "items" template data - blog_page = self.getPageByName(u"blog_view") - yield blog_page.prepare_render(self, request) - - -@defer.inlineCallbacks -def on_data_post(self, request): - type_ = self.getPostedData(request, u"type") - if type_ == u"comment": - blog_page = self.getPageByName(u"blog_view") - yield blog_page.on_data_post(self, request) - elif type_ == u"attendance": - profile = self.getProfile(request) - service, node, attend, guests = self.getPostedData( - request, (u"service", u"node", u"attend", u"guests") - ) - data = {u"attend": attend, u"guests": guests} - yield self.host.bridgeCall(u"eventInviteeSet", service, node, data, profile) - else: - log.warning(_(u"Unhandled data type: {}").format(type_))
--- a/src/pages/files/list/page_meta.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,108 +0,0 @@ -#!/usr/bin/env python2.7 -# -*- coding: utf-8 -*- - -from libervia.server.constants import Const as C -from sat.core.i18n import _ -from twisted.internet import defer -from libervia.server import session_iface -from libervia.server import pages_tools -from sat.core.log import getLogger -from sat.tools.common import uri -import json -import os - -log = getLogger("pages/files/list") -"""files handling pages""" - -name = u"files_list" -access = C.PAGES_ACCESS_PROFILE -template = u"file/overview.html" - - -def parse_url(self, request): - self.getPathArgs(request, ["service", "*path"], min_args=1, service="jid", path="") - - -@defer.inlineCallbacks -def prepare_render(self, request): - data = self.getRData(request) - thumb_limit = data.get("thumb_limit", 300) - template_data = request.template_data - service, path_elts = data[u"service"], data[u"path"] - path = u"/".join(path_elts) - profile = self.getProfile(request) or C.SERVICE_PROFILE - - files_data = yield self.host.bridgeCall("FISList", service.full(), path, {}, profile) - for file_data in files_data: - try: - extra_raw = file_data[u"extra"] - except KeyError: - pass - else: - file_data[u"extra"] = json.loads(extra_raw) if extra_raw else {} - dir_path = path_elts + [file_data["name"]] - if file_data[u"type"] == C.FILE_TYPE_DIRECTORY: - page = self - elif file_data[u"type"] == C.FILE_TYPE_FILE: - page = self.getPageByName("files_view") - - ## thumbnails ## - try: - thumbnails = file_data[u"extra"]["thumbnails"] - if not thumbnails: - raise KeyError - except KeyError: - pass - else: - thumbnails.sort(key=lambda t: t["size"]) - thumb = thumbnails[0] - for thumb_data in thumbnails: - if thumb_data["size"][0] > thumb_limit: - break - thumb = thumb_data - if u"url" in thumb: - file_data["thumb_url"] = thumb["url"] - elif u"id" in thumb: - try: - thumb_path = yield self.host.bridgeCall( - "bobGetFile", service.full(), thumb[u"id"], profile - ) - except Exception as e: - log.warning( - _(u"Can't retrieve thumbnail: {reason}").format(reason=e) - ) - else: - filename = os.path.basename(thumb_path) - session_data = self.host.getSessionData( - request, session_iface.ISATSession - ) - file_data["thumb_url"] = os.path.join( - session_data.cache_dir, filename - ) - else: - raise ValueError( - u"unexpected file type: {file_type}".format(file_type=file_data[u"type"]) - ) - file_data[u"url"] = page.getURL(service.full(), *dir_path) - - ## comments ## - comments_url = file_data.get(u"comments_url") - if comments_url: - parsed_url = uri.parseXMPPUri(comments_url) - comments_service = file_data[u"comments_service"] = parsed_url["path"] - comments_node = file_data[u"comments_node"] = parsed_url["node"] - try: - comments_count = file_data[u"comments_count"] = int( - file_data["comments_count"] - ) - except KeyError: - comments_count = None - if comments_count and data.get("retrieve_comments", False): - file_data[u"comments"] = yield pages_tools.retrieveComments( - self, comments_service, comments_node, profile=profile - ) - - template_data[u"files_data"] = files_data - template_data[u"path"] = path - if path_elts: - template_data[u"parent_url"] = self.getURL(service.full(), *path_elts[:-1])
--- a/src/pages/files/page_meta.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,54 +0,0 @@ -#!/usr/bin/env python2.7 -# -*- coding: utf-8 -*- - -from libervia.server.constants import Const as C -from twisted.internet import defer -from twisted.words.protocols.jabber import jid -from sat.core.log import getLogger - -log = getLogger("pages/files") -"""files handling pages""" - -name = u"files" -access = C.PAGES_ACCESS_PROFILE -template = u"file/discover.html" - - -@defer.inlineCallbacks -def prepare_render(self, request): - profile = self.getProfile(request) - template_data = request.template_data - namespace = self.host.ns_map["fis"] - entities_services, entities_own, entities_roster = yield self.host.bridgeCall( - "discoFindByFeatures", [namespace], [], False, True, True, True, False, profile - ) - tpl_service_entities = template_data["disco_service_entities"] = {} - tpl_own_entities = template_data["disco_own_entities"] = {} - tpl_roster_entities = template_data["disco_roster_entities"] = {} - entities_url = template_data["entities_url"] = {} - - # we store identities in dict of dict using category and type as keys - # this way it's easier to test category in the template - for tpl_entities, entities_map in ( - (tpl_service_entities, entities_services), - (tpl_own_entities, entities_own), - (tpl_roster_entities, entities_roster), - ): - for entity_str, entity_ids in entities_map.iteritems(): - entity_jid = jid.JID(entity_str) - tpl_entities[entity_jid] = identities = {} - for cat, type_, name in entity_ids: - identities.setdefault(cat, {}).setdefault(type_, []).append(name) - entities_url[entity_jid] = self.getPageByName("files_list").getURL( - entity_jid.full() - ) - - -def on_data_post(self, request): - jid_str = self.getPostedData(request, u"jid") - try: - jid_ = jid.JID(jid_str) - except RuntimeError: - self.pageError(request, C.HTTP_BAD_REQUEST) - url = self.getPageByName(u"files_list").getURL(jid_.full()) - self.HTTPRedirect(request, url)
--- a/src/pages/files/view/page_meta.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,60 +0,0 @@ -#!/usr/bin/env python2.7 -# -*- coding: utf-8 -*- - -from libervia.server.constants import Const as C -from sat.core.i18n import _ -from twisted.internet import defer -from twisted.web import static -from libervia.server.utils import ProgressHandler -import tempfile -import os -import os.path -from sat.core.log import getLogger - -log = getLogger("pages/files/view") -"""files handling pages""" - -name = u"files_view" -access = C.PAGES_ACCESS_PROFILE - - -def parse_url(self, request): - self.getPathArgs(request, ["service", "*path"], min_args=2, service="jid", path="") - - -def cleanup(dummy, tmp_dir, dest_path): - try: - os.unlink(dest_path) - except OSError: - log.warning(_(u"Can't remove temporary file {path}").format(path=dest_path)) - try: - os.rmdir(tmp_dir) - except OSError: - log.warning(_(u"Can't remove temporary directory {path}").format(path=tmp_dir)) - - -@defer.inlineCallbacks -def render(self, request): - data = self.getRData(request) - profile = self.getProfile(request) - service, path_elts = data[u"service"], data[u"path"] - basename = path_elts[-1] - dir_elts = path_elts[:-1] - dir_path = u"/".join(dir_elts) - tmp_dir = tempfile.mkdtemp() - dest_path = os.path.join(tmp_dir, basename) - request.notifyFinish().addCallback(cleanup, tmp_dir, dest_path) - progress_id = yield self.host.bridgeCall( - "fileJingleRequest", - service.full(), - dest_path, - basename, - u"", - u"", - {u"path": dir_path}, - profile, - ) - log.debug(u"file requested") - yield ProgressHandler(self.host, progress_id, profile).register() - log.debug(u"file downloaded") - self.delegateToResource(request, static.File(dest_path))
--- a/src/pages/forums/list/page_meta.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,63 +0,0 @@ -#!/usr/bin/env python2.7 -# -*- coding: utf-8 -*- - -from libervia.server.constants import Const as C -from twisted.internet import defer -from sat.core.log import getLogger -from sat.core.i18n import _ -from sat.tools.common import uri as xmpp_uri - -log = getLogger("pages/forum") -import json - -"""forum handling pages""" - -name = u"forums" -access = C.PAGES_ACCESS_PUBLIC -template = u"forum/overview.html" - - -def parse_url(self, request): - self.getPathArgs( - request, - ["service", "node", "forum_key"], - service=u"@jid", - node=u"@", - forum_key=u"", - ) - - -def getLinks(self, forums): - for forum in forums: - try: - uri = forum["uri"] - except KeyError: - pass - else: - uri = xmpp_uri.parseXMPPUri(uri) - service = uri[u"path"] - node = uri[u"node"] - forum["http_url"] = self.getPageByName(u"forum_topics").getURL(service, node) - if u"sub-forums" in forum: - getLinks(self, forum[u"sub-forums"]) - - -@defer.inlineCallbacks -def prepare_render(self, request): - data = self.getRData(request) - template_data = request.template_data - service, node, key = data[u"service"], data[u"node"], data[u"forum_key"] - profile = self.getProfile(request) or C.SERVICE_PROFILE - - try: - forums_raw = yield self.host.bridgeCall( - "forumsGet", service.full() if service else u"", node, key, profile - ) - except Exception as e: - log.warning(_(u"Can't retrieve forums: {msg}").format(msg=e)) - forums = [] - else: - forums = json.loads(forums_raw) - getLinks(self, forums) - - template_data[u"forums"] = forums
--- a/src/pages/forums/topics/page_meta.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,69 +0,0 @@ -#!/usr/bin/env python2.7 -# -*- coding: utf-8 -*- - -from libervia.server.constants import Const as C -from twisted.internet import defer -from sat.core.i18n import _ -from sat.core.log import getLogger -from sat.tools.common import uri as xmpp_uri - -log = getLogger("pages/forums/topics") - -name = u"forum_topics" -access = C.PAGES_ACCESS_PUBLIC -template = u"forum/view_topics.html" - - -def parse_url(self, request): - self.getPathArgs(request, ["service", "node"], 2, service=u"jid") - - -@defer.inlineCallbacks -def prepare_render(self, request): - profile = self.getProfile(request) or C.SERVICE_PROFILE - data = self.getRData(request) - service, node = data[u"service"], data[u"node"] - request.template_data.update({u"service": service, u"node": node}) - template_data = request.template_data - topics, metadata = yield self.host.bridgeCall( - u"forumTopicsGet", service.full(), node, {}, profile - ) - template_data[u"identities"] = identities = {} - for topic in topics: - parsed_uri = xmpp_uri.parseXMPPUri(topic[u"uri"]) - author = topic[u"author"] - topic[u"http_uri"] = self.getPageByName(u"forum_view").getURL( - parsed_uri[u"path"], parsed_uri[u"node"] - ) - if author not in identities: - identities[topic[u"author"]] = yield self.host.bridgeCall( - u"identityGet", author, profile - ) - template_data[u"topics"] = topics - - -@defer.inlineCallbacks -def on_data_post(self, request): - profile = self.getProfile(request) - if profile is None: - self.pageError(request, C.HTTP_UNAUTHORIZED) - type_ = self.getPostedData(request, u"type") - if type_ == u"new_topic": - service, node, title, body = self.getPostedData( - request, (u"service", u"node", u"title", u"body") - ) - - if not title or not body: - self.pageError(request, C.HTTP_BAD_REQUEST) - topic_data = {u"title": title, u"content": body} - try: - yield self.host.bridgeCall( - u"forumTopicCreate", service, node, topic_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_))
--- a/src/pages/forums/view/page_meta.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,51 +0,0 @@ -#!/usr/bin/env python2.7 -# -*- coding: utf-8 -*- - -from libervia.server.constants import Const as C -from twisted.internet import defer -from sat.core.i18n import _ -from sat.core.log import getLogger - -log = getLogger("pages/forums/view") - -name = u"forum_view" -access = C.PAGES_ACCESS_PUBLIC -template = u"forum/view.html" - - -def parse_url(self, request): - self.getPathArgs(request, ["service", "node"], 2, service=u"jid") - - -@defer.inlineCallbacks -def prepare_render(self, request): - data = self.getRData(request) - data["show_comments"] = False - blog_page = self.getPageByName(u"blog_view") - request.args["before"] = [""] - request.args["reverse"] = ["1"] - yield blog_page.prepare_render(self, request) - request.template_data[u"login_url"] = self.getPageRedirectURL(request) - - -@defer.inlineCallbacks -def on_data_post(self, request): - profile = self.getProfile(request) - if profile is None: - self.pageError(request, C.HTTP_UNAUTHORIZED) - 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) - mb_data = {u"content": body} - try: - yield self.host.bridgeCall(u"mbSend", service, node, mb_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_))
--- a/src/pages/g/e/page_meta.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,96 +0,0 @@ -#!/usr/bin/env python2.7 -# -*- coding: utf-8 -*- - -from libervia.server.constants import Const as C -from sat.core.i18n import _ -from twisted.internet import defer -from twisted.words.protocols.jabber import jid -from libervia.server import session_iface -from sat.tools.common import uri -from sat.tools.common.template import safe -import time -import cgi -from sat.core.log import getLogger - -log = getLogger("pages/g/e") - -access = C.PAGES_ACCESS_PROFILE -template = u"event/invitation.html" - - -@defer.inlineCallbacks -def prepare_render(self, request): - template_data = request.template_data - guest_session = self.host.getSessionData(request, session_iface.ISATGuestSession) - try: - event_uri = guest_session.data["event_uri"] - except KeyError: - log.warning(_(u"event URI not found, can't render event page")) - self.pageError(request, C.HTTP_SERVICE_UNAVAILABLE) - - data = self.getRData(request) - - ## Event ## - - event_uri_data = uri.parseXMPPUri(event_uri) - if event_uri_data[u"type"] != u"pubsub": - self.pageError(request, C.HTTP_SERVICE_UNAVAILABLE) - - event_service = template_data[u"event_service"] = jid.JID(event_uri_data[u"path"]) - event_node = template_data[u"event_node"] = event_uri_data[u"node"] - event_id = template_data[u"event_id"] = event_uri_data.get(u"item", "") - profile = self.getProfile(request) - event_timestamp, event_data = yield self.host.bridgeCall( - u"eventGet", event_service.userhost(), event_node, event_id, profile - ) - try: - background_image = event_data.pop("background-image") - except KeyError: - pass - else: - template_data["dynamic_style"] = safe( - u""" - html { - background-image: url("%s"); - background-size: 15em; - } - """ - % cgi.escape(background_image, True) - ) - template_data["event"] = event_data - event_invitee_data = yield self.host.bridgeCall( - u"eventInviteeGet", - event_data["invitees_service"], - event_data["invitees_node"], - profile, - ) - template_data["invitee"] = event_invitee_data - template_data["days_left"] = int((event_timestamp - time.time()) / (60 * 60 * 24)) - - ## Blog ## - - data[u"service"] = jid.JID(event_data[u"blog_service"]) - data[u"node"] = event_data[u"blog_node"] - data[u"allow_commenting"] = u"simple" - - # we now need blog items, using blog common page - # this will fill the "items" template data - blog_page = self.getPageByName(u"blog_view") - yield blog_page.prepare_render(self, request) - - -@defer.inlineCallbacks -def on_data_post(self, request): - type_ = self.getPostedData(request, u"type") - if type_ == u"comment": - blog_page = self.getPageByName(u"blog_view") - yield blog_page.on_data_post(self, request) - elif type_ == u"attendance": - profile = self.getProfile(request) - service, node, attend, guests = self.getPostedData( - request, (u"service", u"node", u"attend", u"guests") - ) - data = {u"attend": attend, u"guests": guests} - yield self.host.bridgeCall(u"eventInviteeSet", service, node, data, profile) - else: - log.warning(_(u"Unhandled data type: {}").format(type_))
--- a/src/pages/g/page_meta.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,104 +0,0 @@ -#!/usr/bin/env python2.7 -# -*- coding: utf-8 -*- - -from libervia.server.constants import Const as C -from sat.core.i18n import _ -from twisted.internet import defer -from libervia.server import session_iface -from sat.core.log import getLogger - -log = getLogger("pages/g") - -access = C.PAGES_ACCESS_PUBLIC -template = u"invitation/welcome.html" - - -@defer.inlineCallbacks -def parse_url(self, request): - """check invitation id in URL and start session if needed - - if a session already exists for an other guest/profile, it will be purged - """ - try: - invitation_id = self.nextPath(request) - except IndexError: - self.pageError(request) - - sat_session, guest_session = self.host.getSessionData( - request, session_iface.ISATSession, session_iface.ISATGuestSession - ) - current_id = guest_session.id - - if current_id is not None and current_id != invitation_id: - log.info( - _( - u"killing guest session [{old_id}] because it is connecting with an other ID [{new_id}]" - ).format(old_id=current_id, new_id=invitation_id) - ) - self.host.purgeSession(request) - sat_session, guest_session = self.host.getSessionData( - request, session_iface.ISATSession, session_iface.ISATGuestSession - ) - current_id = None # FIXME: id non mis à zéro ici - profile = None - - profile = sat_session.profile - if profile is not None and current_id is None: - log.info( - _( - u"killing current profile session [{profile}] because a guest id is used" - ).format(profile=profile) - ) - self.host.purgeSession(request) - sat_session, guest_session = self.host.getSessionData( - request, session_iface.ISATSession, session_iface.ISATGuestSession - ) - profile = None - - if current_id is None: - log.debug(_(u"checking invitation [{id}]").format(id=invitation_id)) - try: - data = yield self.host.bridgeCall("invitationGet", invitation_id) - except Exception: - self.pageError(request, C.HTTP_UNAUTHORIZED) - else: - guest_session.id = invitation_id - guest_session.data = data - else: - data = guest_session.data - - if profile is None: - log.debug(_(u"connecting profile [{}]").format(profile)) - # we need to connect the profile - profile = data["guest_profile"] - password = data["password"] - try: - yield self.host.connect(request, profile, password) - except Exception as e: - log.warning(_(u"Can't connect profile: {msg}").format(msg=e)) - # FIXME: no good error code correspond - # maybe use a custom one? - self.pageError(request, code=C.HTTP_SERVICE_UNAVAILABLE) - - log.info( - _( - u"guest session started, connected with profile [{profile}]".format( - profile=profile - ) - ) - ) - - # we copy data useful in templates - template_data = request.template_data - template_data["norobots"] = True - if u"name" in data: - template_data[u"name"] = data[u"name"] - if u"language" in data: - template_data[u"locale"] = data[u"language"] - - -def prepare_render(self, request): - template_data = request.template_data - guest_session = self.host.getSessionData(request, session_iface.ISATGuestSession) - main_uri = guest_session.data.get("main_uri") - template_data[u"include_url"] = self.getPagePathFromURI(main_uri)
--- a/src/pages/login/logged/page_meta.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,18 +0,0 @@ -#!/usr/bin/env python2.7 -# -*- coding: utf-8 -*- - -from libervia.server import session_iface -from sat.core.log import getLogger - -log = getLogger("pages/login") - -"""SàT log-in page, with link to create an account""" - -template = u"login/logged.html" - - -def prepare_render(self, request): - template_data = request.template_data - session_data = self.host.getSessionData(request, session_iface.ISATSession) - template_data["guest_session"] = session_data.guest - template_data["session_started"] = session_data.started
--- a/src/pages/login/page_meta.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,86 +0,0 @@ -#!/usr/bin/env python2.7 -# -*- coding: utf-8 -*- - -from sat.core.i18n import _ -from sat.core import exceptions -from libervia.server.constants import Const as C -from libervia.server import session_iface -from twisted.internet import defer -from sat.core.log import getLogger - -log = getLogger("pages/login") - -"""SàT log-in page, with link to create an account""" - -name = u"login" -access = C.PAGES_ACCESS_PUBLIC -template = u"login/login.html" - - -def prepare_render(self, request): - template_data = request.template_data - - # we redirect to logged page if a session is active - profile = self.getProfile(request) - if profile is not None: - self.pageRedirect("/login/logged", request) - - # login error message - session_data = self.host.getSessionData(request, session_iface.ISATSession) - login_error = session_data.popPageData(self, "login_error") - if login_error is not None: - template_data["S_C"] = C # we need server constants in template - template_data["login_error"] = login_error - template_data["empty_password_allowed"] = bool( - self.host.options["empty_password_allowed_warning_dangerous_list"] - ) - - # register page url - template_data["register_url"] = self.getPageRedirectURL(request, "register") - - # if login is set, we put it in template to prefill field - template_data["login"] = session_data.popPageData(self, "login") - - -def login_error(self, request, error_const): - """set login_error in page data - - @param error_const(unicode): one of login error constant - @return C.POST_NO_CONFIRM: avoid confirm message - """ - session_data = self.host.getSessionData(request, session_iface.ISATSession) - session_data.setPageData(self, "login_error", error_const) - return C.POST_NO_CONFIRM - - -@defer.inlineCallbacks -def on_data_post(self, request): - profile = self.getProfile(request) - type_ = self.getPostedData(request, "type") - if type_ == "disconnect": - if profile is None: - log.warning(_(u"Disconnect called when no profile is logged")) - self.pageError(request, C.HTTP_BAD_REQUEST) - else: - self.host.purgeSession(request) - defer.returnValue(C.POST_NO_CONFIRM) - elif type_ == "login": - login, password = self.getPostedData(request, (u"login", u"password")) - try: - status = yield self.host.connect(request, login, password) - except ValueError as e: - if e.message in (C.XMPP_AUTH_ERROR, C.PROFILE_AUTH_ERROR): - defer.returnValue(login_error(self, request, e.message)) - else: - # this error was not expected! - raise e - except exceptions.TimeOutError: - defer.returnValue(login_error(self, request, C.NO_REPLY)) - else: - if status in (C.PROFILE_LOGGED, C.PROFILE_LOGGED_EXT_JID, C.SESSION_ACTIVE): - # Profile has been logged correctly - self.redirectOrContinue(request) - else: - log.error(_(u"Unhandled status: {status}".format(status=status))) - else: - self.pageError(request, C.HTTP_BAD_REQUEST)
--- a/src/pages/merge-requests/disco/page_meta.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,41 +0,0 @@ -#!/usr/bin/env python2.7 -# -*- coding: utf-8 -*- - -from libervia.server.constants import Const as C -from twisted.words.protocols.jabber import jid -from sat.core.log import getLogger - -log = getLogger("pages/ticket") -"""ticket handling pages""" - -name = u"merge-requests_disco" -access = C.PAGES_ACCESS_PUBLIC -template = u"merge-request/discover.html" - - -def prepare_render(self, request): - mr_handlers_config = self.host.options["mr_handlers_json"] - if mr_handlers_config: - handlers = request.template_data["mr_handlers"] = [] - try: - for handler_data in mr_handlers_config: - service = handler_data[u"service"] - node = handler_data[u"node"] - name = handler_data[u"name"] - url = self.getPageByName(u"merge-requests").getURL(service, node) - handlers.append({u"name": name, u"url": url}) - except KeyError as e: - log.warning(u"Missing field in mr_handlers_json: {msg}".format(msg=e)) - except Exception as e: - log.warning(u"Can't decode mr handlers: {msg}".format(msg=e)) - - -def on_data_post(self, request): - jid_str = self.getPostedData(request, u"jid") - try: - jid_ = jid.JID(jid_str) - except RuntimeError: - self.pageError(request, C.HTTP_BAD_REQUEST) - # for now we just use default node - url = self.getPageByName(u"merge-requests").getURL(jid_.full(), u"@") - self.HTTPRedirect(request, url)
--- a/src/pages/merge-requests/edit/page_meta.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,105 +0,0 @@ -#!/usr/bin/env python2.7 -# -*- coding: utf-8 -*- - -from libervia.server.constants import Const as C -from sat.core.i18n import _ -from twisted.internet import defer -from sat.tools.common import template_xmlui -from sat.core.log import getLogger - -"""merge-requests edition""" - -name = u"merge-requests_edit" -access = C.PAGES_ACCESS_PROFILE -template = u"merge-request/edit.html" -log = getLogger("pages/" + name) - - -def parse_url(self, request): - try: - item_id = self.nextPath(request) - except IndexError: - log.warning(_(u"no ticket id specified")) - self.pageError(request, C.HTTP_BAD_REQUEST) - - data = self.getRData(request) - data[u"ticket_id"] = item_id - - -@defer.inlineCallbacks -def prepare_render(self, request): - data = self.getRData(request) - template_data = request.template_data - service, node, ticket_id = ( - data.get(u"service", u""), - data.get(u"node", u""), - data[u"ticket_id"], - ) - profile = self.getProfile(request) - - ignore = ( - "publisher", - "author", - "author_jid", - "author_email", - "created", - "updated", - "comments_uri", - "request_data", - "type", - ) - tickets = yield self.host.bridgeCall( - "mergeRequestsGet", - service.full() if service else u"", - node, - C.NO_LIMIT, - [ticket_id], - "", - {}, - profile, - ) - ticket = [template_xmlui.create(self.host, x, ignore=ignore) for x in tickets[0]][0] - - try: - # small trick to get a one line text input instead of the big textarea - ticket.widgets[u"labels"].type = u"string" - ticket.widgets[u"labels"].value = ticket.widgets[u"labels"].value.replace( - u"\n", ", " - ) - except KeyError: - pass - template_data[u"new_ticket_xmlui"] = ticket - - -@defer.inlineCallbacks -def on_data_post(self, request): - data = self.getRData(request) - service = data["service"] - node = data["node"] - ticket_id = data["ticket_id"] - posted_data = self.getAllPostedData(request) - if not posted_data["title"] or not posted_data["body"]: - self.pageError(request, C.HTTP_BAD_REQUEST) - try: - posted_data["labels"] = [l.strip() for l in posted_data["labels"][0].split(",")] - except (KeyError, IndexError): - pass - profile = self.getProfile(request) - yield self.host.bridgeCall( - "mergeRequestSet", - service.full(), - node, - u"", - u"auto", - posted_data, - u"", - ticket_id, - {"update": C.BOOL_TRUE}, - profile, - ) - # we don't want to redirect to edit page on success, but to tickets list - data["post_redirect_page"] = ( - self.getPageByName(u"merge-requests"), - service.full(), - node or u"@", - )
--- a/src/pages/merge-requests/new/page_meta.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,12 +0,0 @@ -#!/usr/bin/env python2.7 -# -*- coding: utf-8 -*- - -from libervia.server.constants import Const as C -from sat.core.log import getLogger - -log = getLogger("pages/ticket") -"""ticket handling pages""" - -name = u"merge-requests_new" -access = C.PAGES_ACCESS_PUBLIC -template = u"merge-request/create.html"
--- a/src/pages/merge-requests/page_meta.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,58 +0,0 @@ -#!/usr/bin/env python2.7 -# -*- coding: utf-8 -*- - -from libervia.server.constants import Const as C -from twisted.internet import defer -from sat.tools.common import template_xmlui -from sat.tools.common import data_objects -from sat.core.log import getLogger - -log = getLogger("pages/ticket") -"""ticket handling pages""" - -name = u"merge-requests" -access = C.PAGES_ACCESS_PUBLIC -template = u"ticket/overview.html" - - -def parse_url(self, request): - self.getPathArgs(request, ["service", "node"], service="jid") - data = self.getRData(request) - service, node = data[u"service"], data[u"node"] - if node is None: - self.pageRedirect(u"merge-requests_disco", request) - if node == u"@": - node = data[u"node"] = u"" - self.checkCache( - request, C.CACHE_PUBSUB, service=service, node=node, short="merge-requests" - ) - template_data = request.template_data - template_data[u"url_tickets_list"] = self.getPageByName("merge-requests").getURL( - service.full(), node - ) - template_data[u"url_tickets_new"] = self.getSubPageURL(request, "merge-requests_new") - - -@defer.inlineCallbacks -def prepare_render(self, request): - data = self.getRData(request) - template_data = request.template_data - service, node = data[u"service"], data[u"node"] - profile = self.getProfile(request) or C.SERVICE_PROFILE - - merge_requests = yield self.host.bridgeCall( - "mergeRequestsGet", - service.full() if service else u"", - node, - C.NO_LIMIT, - [], - "", - {"labels_as_list": C.BOOL_TRUE}, - profile, - ) - template_data[u"tickets"] = [ - template_xmlui.create(self.host, x) for x in merge_requests[0] - ] - template_data[u"on_ticket_click"] = data_objects.OnClick( - url=self.getSubPageURL(request, u"merge-requests_view") + u"/{item.id}" - )
--- a/src/pages/merge-requests/view/page_meta.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,97 +0,0 @@ -#!/usr/bin/env python2.7 -# -*- coding: utf-8 -*- - -from libervia.server.constants import Const as C -from sat.core.i18n import _ -from libervia.server.utils import SubPage -from libervia.server import session_iface -from twisted.internet import defer -from twisted.words.protocols.jabber import jid -from sat.tools.common import template_xmlui -from sat.tools.common import uri -from sat.tools.common import data_objects -from sat.core.log import getLogger - -name = u"merge-requests_view" -access = C.PAGES_ACCESS_PUBLIC -template = u"merge-request/item.html" -log = getLogger(u"pages/" + name) - - -def parse_url(self, request): - try: - item_id = self.nextPath(request) - except IndexError: - log.warning(_(u"no ticket id specified")) - self.pageError(request, C.HTTP_BAD_REQUEST) - - data = self.getRData(request) - data[u"ticket_id"] = item_id - - -@defer.inlineCallbacks -def prepare_render(self, request): - data = self.getRData(request) - template_data = request.template_data - session = self.host.getSessionData(request, session_iface.ISATSession) - service, node, ticket_id = ( - data.get(u"service", u""), - data.get(u"node", u""), - data[u"ticket_id"], - ) - profile = self.getProfile(request) - - if profile is None: - profile = C.SERVICE_PROFILE - - tickets, metadata, parsed_tickets = yield self.host.bridgeCall( - "mergeRequestsGet", - service.full() if service else u"", - node, - C.NO_LIMIT, - [ticket_id], - "", - {"parse": C.BOOL_TRUE, "labels_as_list": C.BOOL_TRUE}, - profile, - ) - ticket = template_xmlui.create(self.host, tickets[0], ignore=["request_data", "type"]) - template_data[u"item"] = ticket - template_data["patches"] = parsed_tickets[0] - comments_uri = ticket.widgets["comments_uri"].value - if comments_uri: - uri_data = uri.parseXMPPUri(comments_uri) - template_data["comments_node"] = comments_node = uri_data["node"] - template_data["comments_service"] = comments_service = uri_data["path"] - comments = yield self.host.bridgeCall( - "mbGet", comments_service, comments_node, C.NO_LIMIT, [], {}, profile - ) - - template_data[u"comments"] = data_objects.BlogItems(comments) - template_data[u"login_url"] = self.getPageRedirectURL(request) - - if session.connected: - # we set edition URL only if user is the publisher or the node owner - publisher = jid.JID(ticket.widgets["publisher"].value) - is_publisher = publisher.userhostJID() == session.jid.userhostJID() - affiliation = None - if not is_publisher: - node = node or self.host.ns_map["merge_requests"] - affiliation = yield self.host.getAffiliation(request, service, node) - if is_publisher or affiliation == "owner": - template_data[u"url_ticket_edit"] = self.getURLByPath( - SubPage("merge-requests"), - service.full(), - node or u"@", - SubPage("merge-requests_edit"), - ticket_id, - ) - - -@defer.inlineCallbacks -def on_data_post(self, request): - type_ = self.getPostedData(request, u"type") - if type_ == u"comment": - blog_page = self.getPageByName(u"blog_view") - yield blog_page.on_data_post(self, request) - else: - log.warning(_(u"Unhandled data type: {}").format(type_))
--- a/src/pages/photos/album/page_meta.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,28 +0,0 @@ -#!/usr/bin/env python2.7 -# -*- coding: utf-8 -*- - -from libervia.server.constants import Const as C -from sat.core.log import getLogger - -log = getLogger("pages/photo/album") - -name = u"photos_album" -access = C.PAGES_ACCESS_PROFILE -template = u"photo/album.html" - - -def parse_url(self, request): - self.getPathArgs(request, ["service", "*path"], min_args=1, service="jid", path="") - - -def prepare_render(self, request): - data = self.getRData(request) - data["thumb_limit"] = 1200 - data["retrieve_comments"] = True - files_page = self.getPageByName(u"files_list") - return files_page.prepare_render(self, request) - - -def on_data_post(self, request): - blog_page = self.getPageByName(u"blog_view") - return blog_page.on_data_post(self, request)
--- a/src/pages/photos/page_meta.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,16 +0,0 @@ -#!/usr/bin/env python2.7 -# -*- coding: utf-8 -*- - -from libervia.server.constants import Const as C -from twisted.internet import defer - -name = u"photos" -access = C.PAGES_ACCESS_PROFILE -template = u"photo/discover.html" - - -@defer.inlineCallbacks -def on_data_post(self, request): - jid_ = self.getPostedData(request, u"jid") - url = self.getPageByName(u"photos_album").getURL(jid_) - self.HTTPRedirect(request, url)
--- a/src/pages/register/page_meta.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,61 +0,0 @@ -#!/usr/bin/env python2.7 -# -*- coding: utf-8 -*- - -from libervia.server.constants import Const as C -from libervia.server import session_iface -from twisted.internet import defer -from sat.core.log import getLogger - -log = getLogger("pages/register") - -"""SàT account registration page""" - -name = u"register" -access = C.PAGES_ACCESS_PUBLIC -template = u"login/register.html" - - -def prepare_render(self, request): - profile = self.getProfile(request) - if profile is not None: - self.pageRedirect("/login/logged", request) - template_data = request.template_data - template_data["login_url"] = self.getPageByName("login").url - template_data["S_C"] = C # we need server constants in template - - # login error message - session_data = self.host.getSessionData(request, session_iface.ISATSession) - login_error = session_data.popPageData(self, "login_error") - if login_error is not None: - template_data["login_error"] = login_error - - # if fields were already filled, we reuse them - for k in (u"login", u"email", u"password"): - template_data[k] = session_data.popPageData(self, k) - - -@defer.inlineCallbacks -def on_data_post(self, request): - type_ = self.getPostedData(request, u"type") - if type_ == u"register": - login, email, password = self.getPostedData( - request, (u"login", u"email", u"password") - ) - status = yield self.host.registerNewAccount(request, login, password, email) - session_data = self.host.getSessionData(request, session_iface.ISATSession) - if status == C.REGISTRATION_SUCCEED: - # we prefill login field for login page - session_data.setPageData(self.getPageByName(u"login"), u"login", login) - # if we have a redirect_url we follow it - self.redirectOrContinue(request) - # else we redirect to login page - self.HTTPRedirect(request, self.getPageByName(u"login").url) - else: - session_data.setPageData(self, u"login_error", status) - l = locals() - for k in (u"login", u"email", u"password"): - # we save fields so user doesn't have to enter them again - session_data.setPageData(self, k, l[k]) - defer.returnValue(C.POST_NO_CONFIRM) - else: - self.pageError(request, C.HTTP_BAD_REQUEST)
--- a/src/pages/tickets/disco/page_meta.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,41 +0,0 @@ -#!/usr/bin/env python2.7 -# -*- coding: utf-8 -*- - -from libervia.server.constants import Const as C -from twisted.words.protocols.jabber import jid -from sat.core.log import getLogger - -log = getLogger("pages/ticket") -"""ticket handling pages""" - -name = u"tickets_disco" -access = C.PAGES_ACCESS_PUBLIC -template = u"ticket/discover.html" - - -def prepare_render(self, request): - tickets_trackers_config = self.host.options["tickets_trackers_json"] - if tickets_trackers_config: - trackers = request.template_data["tickets_trackers"] = [] - try: - for tracker_data in tickets_trackers_config: - service = tracker_data[u"service"] - node = tracker_data[u"node"] - name = tracker_data[u"name"] - url = self.getPageByName(u"tickets").getURL(service, node) - trackers.append({u"name": name, u"url": url}) - except KeyError as e: - log.warning(u"Missing field in tickets_trackers_json: {msg}".format(msg=e)) - except Exception as e: - log.warning(u"Can't decode tickets trackers: {msg}".format(msg=e)) - - -def on_data_post(self, request): - jid_str = self.getPostedData(request, u"jid") - try: - jid_ = jid.JID(jid_str) - except RuntimeError: - self.pageError(request, C.HTTP_BAD_REQUEST) - # for now we just use default node - url = self.getPageByName(u"tickets").getURL(jid_.full(), u"@") - self.HTTPRedirect(request, url)
--- a/src/pages/tickets/edit/page_meta.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,94 +0,0 @@ -#!/usr/bin/env python2.7 -# -*- coding: utf-8 -*- - -from libervia.server.constants import Const as C -from sat.core.i18n import _ -from twisted.internet import defer -from sat.tools.common import template_xmlui -from sat.core.log import getLogger - -log = getLogger("pages/ticket") -"""ticket handling pages""" - -name = u"tickets_edit" -access = C.PAGES_ACCESS_PROFILE -template = u"ticket/edit.html" - - -def parse_url(self, request): - try: - item_id = self.nextPath(request) - except IndexError: - log.warning(_(u"no ticket id specified")) - self.pageError(request, C.HTTP_BAD_REQUEST) - - data = self.getRData(request) - data[u"ticket_id"] = item_id - - -@defer.inlineCallbacks -def prepare_render(self, request): - data = self.getRData(request) - template_data = request.template_data - service, node, ticket_id = ( - data.get(u"service", u""), - data.get(u"node", u""), - data[u"ticket_id"], - ) - profile = self.getProfile(request) - - ignore = ( - "publisher", - "author", - "author_jid", - "author_email", - "created", - "updated", - "comments_uri", - ) - tickets = yield self.host.bridgeCall( - "ticketsGet", - service.full() if service else u"", - node, - C.NO_LIMIT, - [ticket_id], - "", - {}, - profile, - ) - ticket = [template_xmlui.create(self.host, x, ignore=ignore) for x in tickets[0]][0] - - try: - # small trick to get a one line text input instead of the big textarea - ticket.widgets[u"labels"].type = u"string" - ticket.widgets[u"labels"].value = ticket.widgets[u"labels"].value.replace( - u"\n", ", " - ) - except KeyError: - pass - template_data[u"new_ticket_xmlui"] = ticket - - -@defer.inlineCallbacks -def on_data_post(self, request): - data = self.getRData(request) - service = data["service"] - node = data["node"] - ticket_id = data["ticket_id"] - posted_data = self.getAllPostedData(request) - if not posted_data["title"] or not posted_data["body"]: - self.pageError(request, C.HTTP_BAD_REQUEST) - try: - posted_data["labels"] = [l.strip() for l in posted_data["labels"][0].split(",")] - except (KeyError, IndexError): - pass - profile = self.getProfile(request) - yield self.host.bridgeCall( - "ticketSet", service.full(), node, posted_data, u"", ticket_id, {}, profile - ) - # we don't want to redirect to edit page on success, but to tickets list - data["post_redirect_page"] = ( - self.getPageByName(u"tickets"), - service.full(), - node or u"@", - )
--- a/src/pages/tickets/new/page_meta.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,67 +0,0 @@ -#!/usr/bin/env python2.7 -# -*- coding: utf-8 -*- - -from libervia.server.constants import Const as C -from twisted.internet import defer -from sat.tools.common import template_xmlui -from sat.core.log import getLogger - -log = getLogger("pages/ticket") -"""ticket handling pages""" - -name = u"tickets_new" -access = C.PAGES_ACCESS_PROFILE -template = u"ticket/create.html" - - -@defer.inlineCallbacks -def prepare_render(self, request): - data = self.getRData(request) - template_data = request.template_data - service, node = data.get(u"service", u""), data.get(u"node", u"") - profile = self.getProfile(request) - schema = yield self.host.bridgeCall("ticketsSchemaGet", service.full(), node, profile) - data["schema"] = schema - # following fields are handled in backend - ignore = ( - "author", - "author_jid", - "author_email", - "created", - "updated", - "comments_uri", - "status", - "milestone", - "priority", - ) - xmlui_obj = template_xmlui.create(self.host, schema, ignore=ignore) - try: - # small trick to get a one line text input instead of the big textarea - xmlui_obj.widgets[u"labels"].type = u"string" - except KeyError: - pass - template_data[u"new_ticket_xmlui"] = xmlui_obj - - -@defer.inlineCallbacks -def on_data_post(self, request): - data = self.getRData(request) - service = data["service"] - node = data["node"] - posted_data = self.getAllPostedData(request) - if not posted_data["title"] or not posted_data["body"]: - self.pageError(request, C.HTTP_BAD_REQUEST) - try: - posted_data["labels"] = [l.strip() for l in posted_data["labels"][0].split(",")] - except (KeyError, IndexError): - pass - profile = self.getProfile(request) - yield self.host.bridgeCall( - "ticketSet", service.full(), node, posted_data, u"", u"", {}, profile - ) - # we don't want to redirect to creation page on success, but to tickets list - data["post_redirect_page"] = ( - self.getPageByName(u"tickets"), - service.full(), - node or u"@", - )
--- a/src/pages/tickets/page_meta.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,53 +0,0 @@ -#!/usr/bin/env python2.7 -# -*- coding: utf-8 -*- - -from libervia.server.constants import Const as C -from twisted.internet import defer -from sat.tools.common import template_xmlui -from sat.tools.common import data_objects -from sat.core.log import getLogger - -log = getLogger("pages/ticket") -"""ticket handling pages""" - -name = u"tickets" -access = C.PAGES_ACCESS_PUBLIC -template = u"ticket/overview.html" - - -def parse_url(self, request): - self.getPathArgs(request, ["service", "node"], service="jid") - data = self.getRData(request) - service, node = data[u"service"], data[u"node"] - if node is None: - self.pageRedirect(u"tickets_disco", request) - if node == u"@": - node = data[u"node"] = u"" - template_data = request.template_data - template_data[u"url_tickets_list"] = self.getURL(service.full(), node or u"@") - template_data[u"url_tickets_new"] = self.getSubPageURL(request, "tickets_new") - - -@defer.inlineCallbacks -def prepare_render(self, request): - data = self.getRData(request) - template_data = request.template_data - service, node = data[u"service"], data[u"node"] - profile = self.getProfile(request) or C.SERVICE_PROFILE - - self.checkCache(request, C.CACHE_PUBSUB, service=service, node=node, short="tickets") - - tickets = yield self.host.bridgeCall( - "ticketsGet", - service.full() if service else u"", - node, - C.NO_LIMIT, - [], - "", - {"labels_as_list": C.BOOL_TRUE}, - profile, - ) - template_data[u"tickets"] = [template_xmlui.create(self.host, x) for x in tickets[0]] - template_data[u"on_ticket_click"] = data_objects.OnClick( - url=self.getSubPageURL(request, u"tickets_view") + u"/{item.id}" - )
--- a/src/pages/tickets/view/page_meta.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,98 +0,0 @@ -#!/usr/bin/env python2.7 -# -*- coding: utf-8 -*- - -from libervia.server.constants import Const as C -from sat.core.i18n import _ -from libervia.server.utils import SubPage -from libervia.server import session_iface -from twisted.internet import defer -from twisted.words.protocols.jabber import jid -from sat.tools.common import template_xmlui -from sat.tools.common import uri -from sat.tools.common import data_objects -from sat.core.log import getLogger - -log = getLogger("pages/tickets/view") -"""ticket handling pages""" - -name = u"tickets_view" -access = C.PAGES_ACCESS_PUBLIC -template = u"ticket/item.html" - - -def parse_url(self, request): - try: - item_id = self.nextPath(request) - except IndexError: - log.warning(_(u"no ticket id specified")) - self.pageError(request, C.HTTP_BAD_REQUEST) - - data = self.getRData(request) - data[u"ticket_id"] = item_id - - -@defer.inlineCallbacks -def prepare_render(self, request): - data = self.getRData(request) - template_data = request.template_data - session = self.host.getSessionData(request, session_iface.ISATSession) - service, node, ticket_id = ( - data.get(u"service", u""), - data.get(u"node", u""), - data[u"ticket_id"], - ) - profile = self.getProfile(request) - - if profile is None: - profile = C.SERVICE_PROFILE - - tickets = yield self.host.bridgeCall( - "ticketsGet", - service.full() if service else u"", - node, - C.NO_LIMIT, - [ticket_id], - "", - {"labels_as_list": C.BOOL_TRUE}, - profile, - ) - ticket = [template_xmlui.create(self.host, x) for x in tickets[0]][0] - template_data[u"item"] = ticket - comments_uri = ticket.widgets["comments_uri"].value - if comments_uri: - uri_data = uri.parseXMPPUri(comments_uri) - template_data["comments_node"] = comments_node = uri_data["node"] - template_data["comments_service"] = comments_service = uri_data["path"] - comments = yield self.host.bridgeCall( - "mbGet", comments_service, comments_node, C.NO_LIMIT, [], {}, profile - ) - - template_data[u"comments"] = data_objects.BlogItems(comments) - template_data[u"login_url"] = self.getPageRedirectURL(request) - - if session.connected: - # we set edition URL only if user is the publisher or the node owner - publisher = jid.JID(ticket.widgets["publisher"].value) - is_publisher = publisher.userhostJID() == session.jid.userhostJID() - affiliation = None - if not is_publisher: - node = node or self.host.ns_map["tickets"] - affiliation = yield self.host.getAffiliation(request, service, node) - if is_publisher or affiliation == "owner": - template_data[u"url_ticket_edit"] = self.getURLByPath( - SubPage("tickets"), - service.full(), - node or u"@", - SubPage("tickets_edit"), - ticket_id, - ) - - -@defer.inlineCallbacks -def on_data_post(self, request): - type_ = self.getPostedData(request, u"type") - if type_ == u"comment": - blog_page = self.getPageByName(u"blog_view") - yield blog_page.on_data_post(self, request) - else: - log.warning(_(u"Unhandled data type: {}").format(type_))
--- a/src/pages/u/atom.xml/page_meta.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,3 +0,0 @@ -#!/usr/bin/env python2.7 -# -*- coding: utf-8 -*- -redirect = u"blog_feed_atom"
--- a/src/pages/u/blog/page_meta.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,15 +0,0 @@ -#!/usr/bin/env python2.7 -# -*- coding: utf-8 -*- - -name = u"user_blog" - - -def parse_url(self, request): - # in this subpage, we want path args and query args - # (i.e. what's remaining in URL: filters, id, etc.) - # to be used by blog's url parser, so we don't skip parse_url - data = self.getRData(request) - service = data[u"service"] - self.pageRedirect( - u"blog_view", request, skip_parse_url=False, path_args=[service.full(), u"@"] - )
--- a/src/pages/u/page_meta.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,40 +0,0 @@ -#!/usr/bin/env python2.7 -# -*- coding: utf-8 -*- - -from libervia.server.constants import Const as C -from twisted.internet import defer -from twisted.words.protocols.jabber import jid - -"""page used to target a user profile, e.g. for public blog""" - -name = u"user" -access = C.PAGES_ACCESS_PUBLIC # can be a callable -template = u"blog/articles.html" -url_cache = True - - -@defer.inlineCallbacks -def parse_url(self, request): - try: - prof_requested = self.nextPath(request) - except IndexError: - self.pageError(request) - - data = self.getRData(request) - - target_profile = yield self.host.bridgeCall("profileNameGet", prof_requested) - request.template_data[u"target_profile"] = target_profile - target_jid = yield self.host.bridgeCall( - "asyncGetParamA", "JabberID", "Connection", "value", profile_key=target_profile - ) - target_jid = jid.JID(target_jid) - data[u"service"] = target_jid - - -@defer.inlineCallbacks -def prepare_render(self, request): - data = self.getRData(request) - self.checkCache( - request, C.CACHE_PUBSUB, service=data[u"service"], node=None, short="microblog" - ) - self.pageRedirect(u"blog_view", request)
--- a/src/server/blog.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,894 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2011-2018 Jérôme Poisson <goffi@goffi.org> -# Copyright (C) 2013-2016 Adrien Cossa <souliane@mailoo.org> - -# This program 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. - -# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. - -from sat.core.i18n import _, D_ -from sat_frontends.tools.strings import addURLToText, fixXHTMLLinks -from sat.core.log import getLogger - -log = getLogger(__name__) -from sat.tools.common import data_format -from sat.tools import xml_tools -from dbus.exceptions import DBusException -from twisted.internet import defer -from twisted.web import server -from twisted.web.resource import Resource -from twisted.words.protocols.jabber.jid import JID -from twisted.words.xish import domish -from jinja2 import Environment, PackageLoader -from datetime import datetime -import re -import os -import sys -import urllib - -from libervia.server.html_tools import sanitizeHtml, convertNewLinesToXHTML -from libervia.server.constants import Const as C - -NS_ATOM = "http://www.w3.org/2005/Atom" -PARAMS_TO_GET = ( - C.STATIC_BLOG_PARAM_TITLE, - C.STATIC_BLOG_PARAM_BANNER, - C.STATIC_BLOG_PARAM_KEYWORDS, - C.STATIC_BLOG_PARAM_DESCRIPTION, -) -re_strip_empty_div = re.compile(r"<div ?/>|<div> *?</div>") - -# TODO: check disco features and use max_items when RSM is not available -# FIXME: change navigation links handling, this is is fragile -# XXX: this page will disappear, LiberviaPage will be used instead -# TODO: delete this page and create a compatibility page for links - - -def getDefaultQueryData(request): - """Return query data which must be present in all links - - @param request(twisted.web.http.Request): request instance comming from render - @return (dict): a dict with values as expected by urllib.urlencode - """ - default_query_data = {} - try: - default_query_data["tag"] = request.extra_dict[ - "mam_filter_{}".format(C.MAM_FILTER_CATEGORY) - ].encode("utf-8") - except KeyError: - pass - return default_query_data - - -def _quote(value): - """Quote a value for use in url - - @param value(unicode): value to quote - @return (str): quoted value - """ - return urllib.quote(value.encode("utf-8"), "") - - -def _unquote(quoted_value): - """Unquote a value coming from url - - @param unquote_value(str): value to unquote - @return (unicode): unquoted value - """ - assert not isinstance(quoted_value, unicode) - return urllib.unquote(quoted_value).decode("utf-8") - - -def _urlencode(query): - """Same as urllib.urlencode, but use '&' instead of '&'""" - return "&".join( - [ - "{}={}".format(urllib.quote_plus(str(k)), urllib.quote_plus(str(v))) - for k, v in query.iteritems() - ] - ) - - -class TemplateProcessor(object): - - THEME = "default" - - def __init__(self, host): - self.host = host - - # add Libervia's themes directory to the python path - sys.path.append(os.path.dirname(os.path.normpath(self.host.themes_dir))) - themes = os.path.basename(os.path.normpath(self.host.themes_dir)) - self.env = Environment(loader=PackageLoader(themes, self.THEME)) - - def useTemplate(self, request, tpl, data=None): - theme_url = os.path.join("/", C.THEMES_URL, self.THEME) - - data_ = { - "images": os.path.join(theme_url, "images"), - "styles": os.path.join(theme_url, "styles"), - } - if data: - data_.update(data) - - template = self.env.get_template("{}.html".format(tpl)) - return template.render(**data_).encode("utf-8") - - -class MicroBlog(Resource, TemplateProcessor): - isLeaf = True - - def __init__(self, host): - self.host = host - Resource.__init__(self) - TemplateProcessor.__init__(self, host) - self.avatars_cache = {} - - def _avatarPathToUrl(self, avatar, request, bare_jid_s): - filename = os.path.basename(avatar) - avatar_url = os.path.join(self.host.service_cache_url, filename) - self.avatars_cache[bare_jid_s] = avatar_url - return avatar_url - - def getAvatarURL(self, pub_jid, request): - """Return avatar of a jid if in cache, else ask for it. - - @param pub_jid (JID): publisher JID - @return: deferred avatar URL (unicode) - """ - bare_jid_s = pub_jid.userhost() - try: - url = self.avatars_cache[bare_jid_s] - except KeyError: - self.avatars_cache[ - bare_jid_s - ] = "" # avoid to request the vcard several times - d = self.host.bridgeCall( - "avatarGet", bare_jid_s, False, False, C.SERVICE_PROFILE - ) - d.addCallback(self._avatarPathToUrl, request, bare_jid_s) - return d - return defer.succeed(url if url else C.DEFAULT_AVATAR_URL) - - def render_GET(self, request): - if not request.postpath or len(request.postpath) > 2: - return self.useTemplate( - request, "static_blog_error", {"message": "You must indicate a nickname"} - ) - - prof_requested = _unquote(request.postpath[0]) - - try: - prof_found = self.host.bridge.profileNameGet(prof_requested) - except DBusException: - prof_found = None - if not prof_found or prof_found == C.SERVICE_PROFILE: - return self.useTemplate( - request, "static_blog_error", {"message": "Invalid nickname"} - ) - - d = defer.Deferred() - # TODO: jid caching - self.host.bridge.asyncGetParamA( - "JabberID", - "Connection", - "value", - profile_key=prof_found, - callback=d.callback, - errback=d.errback, - ) - d.addCallback(self.render_gotJID, request, prof_found) - return server.NOT_DONE_YET - - def render_gotJID(self, pub_jid_s, request, profile): - pub_jid = JID(pub_jid_s) - - request.extra_dict = {} # will be used for RSM and MAM - self.parseURLParams(request) - if request.item_id: - # FIXME: this part seems useless - # we want a specific item - # item_ids = [request.item_id] - # max_items = 1 - max_items = C.NO_LIMIT # FIXME - else: - # max_items = int(request.extra_dict['rsm_max']) # FIXME - max_items = C.NO_LIMIT - # TODO: use max_items only when RSM is not available - - if request.atom: - request.extra_dict.update(request.mam_extra) - self.getAtom( - pub_jid, - max_items, - request.extra_dict, - request.extra_comments_dict, - request, - profile, - ) - - elif request.item_id: - # we can't merge mam_extra now because we'll use item_ids - self.getItemById( - pub_jid, - request.item_id, - request.extra_dict, - request.extra_comments_dict, - request, - profile, - ) - else: - request.extra_dict.update(request.mam_extra) - self.getItems( - pub_jid, - max_items, - request.extra_dict, - request.extra_comments_dict, - request, - profile, - ) - - ## URL parsing - - def parseURLParams(self, request): - """Parse the request URL parameters. - - @param request: HTTP request - """ - if len(request.postpath) > 1: - if request.postpath[1] == "atom.xml": # return the atom feed - request.atom = True - request.item_id = None - else: - request.atom = False - request.item_id = _unquote(request.postpath[1]) - else: - request.item_id = None - request.atom = False - - self.parseURLParamsRSM(request) - # XXX: request.display_single is True when only one blog post is visible - request.display_single = (request.item_id is not None) or int( - request.extra_dict["rsm_max"] - ) == 1 - self.parseURLParamsCommentsRSM(request) - self.parseURLParamsMAM(request) - - def parseURLParamsRSM(self, request): - """Parse RSM request data from the URL parameters for main items - - fill request.extra_dict accordingly - @param request: HTTP request - """ - if request.item_id: # XXX: item_id and RSM are not compatible - return - try: - rsm_max = int(request.args["max"][0]) - if rsm_max > C.STATIC_RSM_MAX_LIMIT: - log.warning(u"Request with rsm_max over limit ({})".format(rsm_max)) - rsm_max = C.STATIC_RSM_MAX_LIMIT - request.extra_dict["rsm_max"] = unicode(rsm_max) - except (ValueError, KeyError): - request.extra_dict["rsm_max"] = unicode(C.STATIC_RSM_MAX_DEFAULT) - try: - request.extra_dict["rsm_index"] = request.args["index"][0] - except (ValueError, KeyError): - try: - request.extra_dict["rsm_before"] = request.args["before"][0].decode( - "utf-8" - ) - except KeyError: - try: - request.extra_dict["rsm_after"] = request.args["after"][0].decode( - "utf-8" - ) - except KeyError: - pass - - def parseURLParamsCommentsRSM(self, request): - """Parse RSM request data from the URL parameters for comments - - fill request.extra_dict accordingly - @param request: HTTP request - """ - request.extra_comments_dict = {} - if request.display_single: - try: - rsm_max = int(request.args["comments_max"][0]) - if rsm_max > C.STATIC_RSM_MAX_LIMIT: - log.warning(u"Request with rsm_max over limit ({})".format(rsm_max)) - rsm_max = C.STATIC_RSM_MAX_LIMIT - request.extra_comments_dict["rsm_max"] = unicode(rsm_max) - except (ValueError, KeyError): - request.extra_comments_dict["rsm_max"] = unicode( - C.STATIC_RSM_MAX_COMMENTS_DEFAULT - ) - else: - request.extra_comments_dict["rsm_max"] = "0" - - def parseURLParamsMAM(self, request): - """Parse MAM request data from the URL parameters for main items - - fill request.extra_dict accordingly - @param request: HTTP request - """ - # XXX: we use a separate dict for MAM as the filters are not used - # when display_single is set (because it then use item_ids which - # can't be used with MAM), but it is still used in this case - # for navigation links. - request.mam_extra = {} - try: - request.mam_extra[ - "mam_filter_{}".format(C.MAM_FILTER_CATEGORY) - ] = request.args["tag"][0].decode("utf-8") - except KeyError: - pass - - ## Items retrieval - - def getItemById( - self, pub_jid, item_id, extra_dict, extra_comments_dict, request, profile - ): - """ - - @param pub_jid (jid.JID): publisher JID - @param item_id(unicode): ID of the item to retrieve - @param extra_dict (dict): extra configuration for initial items only - @param extra_comments_dict (dict): extra configuration for comments only - @param request: HTTP request - @param profile - """ - - def gotItems(items): - items, metadata = items - item = items[0] # assume there's only one item - - def gotMetadata(result): - dummy, rsm_metadata = result - try: - metadata["rsm_count"] = rsm_metadata["rsm_count"] - except KeyError: - pass - try: - metadata["rsm_index"] = unicode(int(rsm_metadata["rsm_index"]) - 1) - except KeyError: - pass - - metadata["rsm_first"] = metadata["rsm_last"] = item["id"] - - def gotComments(comments): - # at this point we can merge mam dict - request.extra_dict.update(request.mam_extra) - # build the items as self.getItems would do it (and as self.renderHTML expects them to be) - comments = [ - ( - item["comments_service"], - item["comments_node"], - "", - comments[0], - comments[1], - ) - ] - self.renderHTML( - [(item, comments)], metadata, request, pub_jid, profile - ) - - # get the comments - # max_comments = int(extra_comments_dict['rsm_max']) # FIXME - max_comments = C.NO_LIMIT - # TODO: use max_comments only when RSM is not available - self.host.bridge.mbGet( - item["comments_service"], - item["comments_node"], - max_comments, - [], - extra_comments_dict, - C.SERVICE_PROFILE, - callback=gotComments, - errback=lambda failure: self.renderError(failure, request, pub_jid), - ) - - # XXX: retrieve RSM information related to the main item. We can't do it while - # retrieving the item, because item_ids and rsm should not be used together. - self.host.bridge.mbGet( - pub_jid.userhost(), - "", - 0, - [], - {"rsm_max": "1", "rsm_after": item["id"]}, - C.SERVICE_PROFILE, - callback=gotMetadata, - errback=lambda failure: self.renderError(failure, request, pub_jid), - ) - - # get the main item - self.host.bridge.mbGet( - pub_jid.userhost(), - "", - 0, - [item_id], - extra_dict, - C.SERVICE_PROFILE, - callback=gotItems, - errback=lambda failure: self.renderError(failure, request, pub_jid), - ) - - def getItems( - self, pub_jid, max_items, extra_dict, extra_comments_dict, request, profile - ): - """ - - @param pub_jid (jid.JID): publisher JID - @param max_items(int): maximum number of item to get, C.NO_LIMIT for no limit - @param extra_dict (dict): extra configuration for initial items only - @param extra_comments_dict (dict): extra configuration for comments only - @param request: HTTP request - @param profile - """ - - def getResultCb(data, rt_session): - remaining, results = data - # we have requested one node only - assert remaining == 0 - assert len(results) == 1 - service, node, failure, items, metadata = results[0] - if failure: - self.renderError(failure, request, pub_jid) - else: - self.renderHTML(items, metadata, request, pub_jid, profile) - - def getResult(rt_session): - self.host.bridge.mbGetFromManyWithCommentsRTResult( - rt_session, - C.SERVICE_PROFILE, - callback=lambda data: getResultCb(data, rt_session), - errback=lambda failure: self.renderError(failure, request, pub_jid), - ) - - # max_comments = int(extra_comments_dict['rsm_max']) # FIXME - max_comments = 0 - # TODO: use max_comments only when RSM is not available - self.host.bridge.mbGetFromManyWithComments( - C.JID, - [pub_jid.userhost()], - max_items, - max_comments, - extra_dict, - extra_comments_dict, - C.SERVICE_PROFILE, - callback=getResult, - ) - - def getAtom( - self, pub_jid, max_items, extra_dict, extra_comments_dict, request, profile - ): - """ - - @param pub_jid (jid.JID): publisher JID - @param max_items(int): maximum number of item to get, C.NO_LIMIT for no limit - @param extra_dict (dict): extra configuration for initial items only - @param extra_comments_dict (dict): extra configuration for comments only - @param request: HTTP request - @param profile - """ - - def gotItems(data): - # Generate a clean atom feed with uri linking to this blog - # from microblog data - items, metadata = data - feed_elt = domish.Element((NS_ATOM, u"feed")) - title = _(u"{user}'s blog").format(user=profile) - feed_elt.addElement(u"title", content=title) - - base_blog_url = self.host.getExtBaseURL( - request, u"blog/{user}".format(user=profile) - ) - - # atom link - link_feed_elt = feed_elt.addElement("link") - link_feed_elt["href"] = u"{base}/atom.xml".format(base=base_blog_url) - link_feed_elt["type"] = u"application/atom+xml" - link_feed_elt["rel"] = u"self" - - # blog link - link_blog_elt = feed_elt.addElement("link") - link_blog_elt["rel"] = u"alternate" - link_blog_elt["type"] = u"text/html" - link_blog_elt["href"] = base_blog_url - - # blog link XMPP uri - blog_xmpp_uri = metadata["uri"] - link_blog_elt = feed_elt.addElement("link") - link_blog_elt["rel"] = u"alternate" - link_blog_elt["type"] = u"application/atom+xml" - link_blog_elt["href"] = blog_xmpp_uri - - feed_elt.addElement("id", content=_quote(blog_xmpp_uri)) - updated_unix = max([float(item["updated"]) for item in items]) - updated_dt = datetime.fromtimestamp(updated_unix) - feed_elt.addElement( - u"updated", content=u"{}Z".format(updated_dt.isoformat("T")) - ) - - for item in items: - entry_elt = feed_elt.addElement(u"entry") - - # Title - try: - title = item["title"] - except KeyError: - # for microblog (without title), we use an abstract of content as title - title = u"{}…".format(u" ".join(item["content"][:70].split())) - entry_elt.addElement(u"title", content=title) - - # HTTP link - http_link_elt = entry_elt.addElement(u"link") - http_link_elt["rel"] = u"alternate" - http_link_elt["type"] = u"text/html" - http_link_elt["href"] = u"{base}/{quoted_id}".format( - base=base_blog_url, quoted_id=_quote(item["id"]) - ) - # XMPP link - xmpp_link_elt = entry_elt.addElement(u"link") - xmpp_link_elt["rel"] = u"alternate" - xmpp_link_elt["type"] = u"application/atom+xml" - xmpp_link_elt["href"] = u"{blog_uri};item={item_id}".format( - blog_uri=blog_xmpp_uri, item_id=item["id"] - ) - - # date metadata - entry_elt.addElement(u"id", content=item["atom_id"]) - updated = datetime.fromtimestamp(float(item["updated"])) - entry_elt.addElement( - u"updated", content=u"{}Z".format(updated.isoformat("T")) - ) - published = datetime.fromtimestamp(float(item["published"])) - entry_elt.addElement( - u"published", content=u"{}Z".format(published.isoformat("T")) - ) - - # author metadata - author_elt = entry_elt.addElement(u"author") - author_elt.addElement("name", content=item.get("author", profile)) - try: - author_elt.addElement( - "uri", content=u"xmpp:{}".format(item["author_jid"]) - ) - except KeyError: - pass - try: - author_elt.addElement("email", content=item["author_email"]) - except KeyError: - pass - - # categories - for tag in data_format.dict2iter("tag", item): - category_elt = entry_elt.addElement(u"category") - category_elt["term"] = tag - - # content - try: - content_xhtml = item["content_xhtml"] - except KeyError: - content_elt = entry_elt.addElement("content", content="content") - content_elt["type"] = "text" - else: - content_elt = entry_elt.addElement("content") - content_elt["type"] = "xhtml" - content_elt.addChild( - xml_tools.ElementParser()(content_xhtml, namespace=C.NS_XHTML) - ) - - atom_feed = u'<?xml version="1.0" encoding="utf-8"?>\n{}'.format( - feed_elt.toXml() - ) - self.renderAtomFeed(atom_feed, request), - - self.host.bridge.mbGet( - pub_jid.userhost(), - "", - max_items, - [], - extra_dict, - C.SERVICE_PROFILE, - callback=gotItems, - ) - - ## rendering - - def _updateDict(self, value, dict_, key): - dict_[key] = value - - def _getImageParams(self, options, key, default, alt): - """regexp from http://answers.oreilly.com/topic/280-how-to-validate-urls-with-regular-expressions/""" - url = options[key] if key in options else "" - regexp = ( - r"^(https?|ftp)://[a-z0-9-]+(\.[a-z0-9-]+)+(/[\w-]+)*/[\w-]+\.(gif|png|jpg)$" - ) - if re.match(regexp, url): - url = url - else: - url = default - return BlogImage(url, alt) - - def renderError(self, failure, request, pub_jid): - request.setResponseCode(500) - request.write( - self.useTemplate( - request, "static_blog_error", {"message": "Can't access requested data"} - ) - ) - request.finish() - - def renderHTML(self, items, metadata, request, pub_jid, profile): - """Retrieve the user parameters before actually rendering the static blog - - @param items(list[tuple(dict, list)]): same as in self._renderHTML - @param metadata(dict): original node metadata - @param request: HTTP request - @param pub_jid (JID): publisher JID - @param profile (unicode): %(doc_profile)s - """ - d_list = [] - options = {} - - d = self.getAvatarURL(pub_jid, request) - d.addCallback(self._updateDict, options, "avatar") - d.addErrback(self.renderError, request, pub_jid) - d_list.append(d) - - for param_name in PARAMS_TO_GET: - d = defer.Deferred() - self.host.bridge.asyncGetParamA( - param_name, - C.STATIC_BLOG_KEY, - "value", - C.SERVER_SECURITY_LIMIT, - profile, - callback=d.callback, - errback=d.errback, - ) - d.addCallback(self._updateDict, options, param_name) - d.addErrback(self.renderError, request, pub_jid) - d_list.append(d) - - dlist_d = defer.DeferredList(d_list) - dlist_d.addCallback( - lambda dummy: self._renderHTML(items, metadata, options, request, pub_jid) - ) - - def _renderHTML(self, items, metadata, options, request, pub_jid): - """Actually render the static blog. - - If mblog_data is a list of dict, we are missing the comments items so we just - display the main items. If mblog_data is a list of couple, each couple is - associating a main item data with the list of its comments, so we render all. - @param items(list[tuple(dict, list)]): list of 2-tuple with - - item(dict): item microblog data - - comments_list(list[tuple]): list of 5-tuple with - - service (unicode): pubsub service where the comments node is - - node (unicode): comments node - - failure (unicode): empty in case of success, else error message - - comments(list[dict]): list of microblog data - - comments_metadata(dict): metadata of the comment node - @param metadata(dict): original node metadata - @param options: dict defining the blog's parameters - @param request: the HTTP request - @param pub_jid (JID): publisher JID - """ - if not isinstance(options, dict): - options = {} - user = sanitizeHtml(pub_jid.user) - base_url = os.path.join("/blog/", user) - - def getOption(key): - return sanitizeHtml(options[key]) if key in options else "" - - avatar = os.path.normpath("/{}".format(getOption("avatar"))) - title = getOption(C.STATIC_BLOG_PARAM_TITLE) or user - query_data = _urlencode(getDefaultQueryData(request)).decode("utf-8") - - xmpp_uri = metadata["uri"] - if len(items) == 1: - # FIXME: that's really not a good way to get item id - # this must be changed after static blog refactorisation - item_id = items[0][0]["id"] - xmpp_uri += u";item={}".format(_quote(item_id)) - - data = { - "url_base": base_url, - "xmpp_uri": xmpp_uri, - "url_query": u"?{}".format(query_data) if query_data else "", - "keywords": getOption(C.STATIC_BLOG_PARAM_KEYWORDS), - "description": getOption(C.STATIC_BLOG_PARAM_DESCRIPTION), - "title": title, - "favicon": avatar, - "banner_img": self._getImageParams( - options, C.STATIC_BLOG_PARAM_BANNER, avatar, title - ), - } - - data["navlinks"] = NavigationLinks(request, items, metadata, base_url) - data["messages"] = [] - for item in items: - item, comments_list = item - comments, comments_count = [], 0 - for node_comments in comments_list: - comments.extend(node_comments[3]) - try: - comments_count += int(node_comments[4]["rsm_count"]) - except KeyError: - pass - data["messages"].append( - BlogMessage(request, base_url, item, comments, comments_count) - ) - - request.write(self.useTemplate(request, "static_blog", data)) - request.finish() - - def renderAtomFeed(self, feed, request): - request.write(feed.encode("utf-8")) - request.finish() - - -class NavigationLinks(object): - def __init__(self, request, items, metadata, base_url): - """Build the navigation links. - - @param items (list): list of items - @param metadata (dict): rsm data - @param base_url (unicode): the base URL for this user's blog - @return: dict - """ - # FIXME: this code must be refactorized, it is fragile - # and difficult to maintain - - # query data which must be present in all links - default_query_data = getDefaultQueryData(request) - - # which links we need to display - if request.display_single: - links = ("later_message", "older_message") - # key must exist when using the template - self.later_messages = self.older_messages = "" - else: - links = ("later_messages", "older_messages") - self.later_message = self.older_message = "" - - # now we set the links according to RSM - for key in links: - query_data = default_query_data.copy() - - if key.startswith("later_message"): - try: - index = int(metadata["rsm_index"]) - except (KeyError, ValueError): - pass - else: - if index == 0: - # we don't show this link on first page - setattr(self, key, "") - continue - try: - query_data["before"] = metadata["rsm_first"].encode("utf-8") - except KeyError: - pass - else: - try: - index = int(metadata["rsm_index"]) - count = int(metadata.get("rsm_count")) - except (KeyError, ValueError): - # XXX: if we don't have index or count, we can't know if we - # are on the last page or not - pass - else: - # if we have index, we don't show the after link - # on the last page - if index + len(items) >= count: - setattr(self, key, "") - continue - try: - query_data["after"] = metadata["rsm_last"].encode("utf-8") - except KeyError: - pass - - if request.display_single: - query_data["max"] = 1 - - link = "{}?{}".format(base_url, _urlencode(query_data)) - setattr(self, key, BlogLink(link, key, key.replace("_", " "))) - - -class BlogImage(object): - def __init__(self, url_, alt): - self.url = url_ - self.alt = alt - - -class BlogLink(object): - def __init__(self, url_, style, text): - self.url = url_ - self.style = style - self.text = text - - -class BlogMessage(object): - def __init__(self, request, base_url, entry, comments=None, comments_count=0): - """ - - @param request: HTTP request - @param base_url (unicode): the base URL - @param entry(dict): item microblog data - @param comments(list[dict]): list of microblog data - @param comments_count (int): total number of comments - """ - if comments is None: - comments = [] - timestamp = float(entry.get("published", 0)) - - # FIXME: for now we assume that the comments' depth is only 1 - is_comment = not entry.get("comments", False) - - self.date = datetime.fromtimestamp(timestamp) - self.type = "comment" if is_comment else "main_item" - self.style = "mblog_comment" if is_comment else "" - self.content = self.getText(entry, "content") - - if is_comment: - self.author = _(u"from {}").format(entry["author"]) - else: - self.author = " " - self.url = "{}/{}".format(base_url, _quote(entry["id"])) - query_data = getDefaultQueryData(request) - if query_data: - self.url += "?{}".format(_urlencode(query_data)) - self.title = self.getText(entry, "title") - self.tags = [sanitizeHtml(tag) for tag in data_format.dict2iter("tag", entry)] - - count_text = lambda count: D_(u"comments") if count > 1 else D_(u"comment") - - self.comments_text = u"{} {}".format( - comments_count, count_text(comments_count) - ) - - delta = comments_count - len(comments) - if request.display_single and delta > 0: - prev_url = "{}?{}".format( - self.url, _urlencode({"comments_max": comments_count}) - ) - prev_text = D_(u"show {count} previous {comments}").format( - count=delta, comments=count_text(delta) - ) - self.all_comments_link = BlogLink(prev_url, "comments_link", prev_text) - - if comments: - self.comments = [ - BlogMessage(request, base_url, comment) for comment in comments - ] - - def getText(self, entry, key): - try: - xhtml = entry["{}_xhtml".format(key)] - except KeyError: - try: - processor = addURLToText if key.startswith("content") else sanitizeHtml - return convertNewLinesToXHTML(processor(entry[key])) - except KeyError: - return None - else: - # FIXME: empty <div /> elements provoke rendering issue - # this regex is a temporary workadound, need more investigation - xhtml = re_strip_empty_div.sub("", xhtml) - return fixXHTMLLinks(xhtml)
--- a/src/server/constants.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,120 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a SAT frontend -# Copyright (C) 2009-2018 Jérôme Poisson (goffi@goffi.org) - -# This program 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. - -# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. - -from ..common import constants - - -class Const(constants.Const): - - APP_NAME = "Libervia" - APP_NAME_FILE = "libervia" - SERVICE_PROFILE = "libervia" # the SàT profile that is used for exporting the service - - SESSION_TIMEOUT = 7200 # Session's timeout, after that the user will be disconnected - HTML_DIR = "html/" - THEMES_DIR = "themes/" - THEMES_URL = "themes" - MEDIA_DIR = "media/" - CARDS_DIR = "games/cards/tarot" - PAGES_DIR = u"pages" - - ERRNUM_BRIDGE_ERRBACK = 0 # FIXME - ERRNUM_LIBERVIA = 0 # FIXME - - # Security limit for Libervia (get/set params) - SECURITY_LIMIT = 5 - - # Security limit for Libervia server_side - SERVER_SECURITY_LIMIT = constants.Const.NO_SECURITY_LIMIT - - # keys for cache values we can get from browser - ALLOWED_ENTITY_DATA = {"avatar", "nick"} - - STATIC_RSM_MAX_LIMIT = 100 - STATIC_RSM_MAX_DEFAULT = 10 - STATIC_RSM_MAX_COMMENTS_DEFAULT = 10 - - ## Libervia pages ## - PAGES_META_FILE = u"page_meta.py" - PAGES_ACCESS_NONE = ( - u"none" - ) # no access to this page (using its path will return a 404 error) - PAGES_ACCESS_PUBLIC = u"public" - PAGES_ACCESS_PROFILE = ( - u"profile" - ) # a session with an existing profile must be started - PAGES_ACCESS_ADMIN = u"admin" # only profiles set in admins_list can access the page - PAGES_ACCESS_ALL = ( - PAGES_ACCESS_NONE, - PAGES_ACCESS_PUBLIC, - PAGES_ACCESS_PROFILE, - PAGES_ACCESS_ADMIN, - ) - # names of the page to use for menu - DEFAULT_MENU = [ - "login", - "chat", - "blog", - "forums", - "photos", - "files", - "events", - "tickets", - "merge-requests", - "app", - ] - - ## Session flags ## - FLAG_CONFIRM = u"CONFIRM" - - ## Data post ## - POST_NO_CONFIRM = u"POST_NO_CONFIRM" - - ## HTTP methods ## - HTTP_METHOD_GET = u"GET" - HTTP_METHOD_POST = u"POST" - - ## HTTP codes ## - HTTP_SEE_OTHER = 303 - HTTP_NOT_MODIFIED = 304 - HTTP_BAD_REQUEST = 400 - HTTP_UNAUTHORIZED = 401 - HTTP_NOT_FOUND = 404 - HTTP_INTERNAL_ERROR = 500 - HTTP_SERVICE_UNAVAILABLE = 503 - - ## Cache ## - CACHE_PUBSUB = 0 - - ## Date/Time ## - HTTP_DAYS = ("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun") - HTTP_MONTH = ( - "Jan", - "Feb", - "Mar", - "Apr", - "May", - "Jun", - "Jul", - "Aug", - "Sep", - "Oct", - "Nov", - "Dec", - )
--- a/src/server/html_tools.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,36 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2011-2018 Jérôme Poisson <goffi@goffi.org> - -# This program 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. - -# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. - - -def sanitizeHtml(text): - """Sanitize HTML by escaping everything""" - # this code comes from official python wiki: http://wiki.python.org/moin/EscapingHtml - html_escape_table = { - "&": "&", - '"': """, - "'": "'", - ">": ">", - "<": "<", - } - - return "".join(html_escape_table.get(c, c) for c in text) - - -def convertNewLinesToXHTML(text): - return text.replace("\n", "<br/>")
--- a/src/server/pages.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1383 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2011-2018 Jérôme Poisson <goffi@goffi.org> - -# This program 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. - -# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. -from twisted.web import server -from twisted.web import resource as web_resource -from twisted.web import util as web_util -from twisted.internet import defer -from twisted.words.protocols.jabber import jid -from twisted.python import failure - -from sat.core.i18n import _ -from sat.core import exceptions -from sat.tools.common import uri as common_uri -from sat.tools.common import date_utils -from sat.core.log import getLogger - -log = getLogger(__name__) -from libervia.server.constants import Const as C -from libervia.server import session_iface -from libervia.server.utils import quote, SubPage -import libervia - -from collections import namedtuple -import uuid -import os.path -import urllib -import time -import hashlib - -WebsocketMeta = namedtuple("WebsocketMeta", ("url", "token", "debug")) - - -class CacheBase(object): - def __init__(self): - self._created = time.time() - self._last_access = self._created - - @property - def created(self): - return self._created - - @property - def last_access(self): - return self._last_access - - @last_access.setter - def last_access(self, timestamp): - self._last_access = timestamp - - -class CachePage(CacheBase): - def __init__(self, rendered): - super(CachePage, self).__init__() - self._created = time.time() - self._last_access = self._created - self._rendered = rendered - self._hash = hashlib.sha256(rendered).hexdigest() - - @property - def rendered(self): - return self._rendered - - @property - def hash(self): - return self._hash - - -class CacheURL(CacheBase): - def __init__(self, request): - super(CacheURL, self).__init__() - try: - self._data = request.data.copy() - except AttributeError: - self._data = {} - self._template_data = request.template_data.copy() - self._prepath = request.prepath[:] - self._postpath = request.postpath[:] - del self._template_data["csrf_token"] - - def use(self, request): - self.last_access = time.time() - request.data = self._data.copy() - request.template_data.update(self._template_data) - request.prepath = self._prepath[:] - request.postpath = self._postpath[:] - - -class LiberviaPage(web_resource.Resource): - isLeaf = True # we handle subpages ourself - named_pages = {} - uri_callbacks = {} - signals_handlers = {} - pages_redirects = {} - cache = {} - cached_urls = {} - # Set of tuples (service/node/sub_id) of nodes subscribed for caching - # sub_id can be empty string if not handled by service - cache_pubsub_sub = set() - main_menu = None - - def __init__( - self, - host, - root_dir, - url, - name=None, - redirect=None, - access=None, - dynamic=False, - parse_url=None, - prepare_render=None, - render=None, - template=None, - on_data_post=None, - on_data=None, - on_signal=None, - url_cache=False, - ): - """initiate LiberviaPages - - LiberviaPages are the main resources of Libervia, using easy to set python files - The arguments are the variables found in page_meta.py - @param host(Libervia): the running instance of Libervia - @param root_dir(unicode): aboslute file path of the page - @param url(unicode): relative URL to the page - this URL may not be valid, as pages may require path arguments - @param name(unicode, None): if not None, a unique name to identify the page - can then be used for e.g. redirection - "/" is not allowed in names (as it can be used to construct URL paths) - @param redirect(unicode, None): if not None, this page will be redirected. A redirected - parameter is used as in self.pageRedirect. parse_url will not be skipped - using this redirect parameter is called "full redirection" - using self.pageRedirect is called "partial redirection" (because some rendering method - can still be used, e.g. parse_url) - @param access(unicode, None): permission needed to access the page - None means public access. - Pages inherit from parent pages: e.g. if a "settings" page is restricted to admins, - and if "settings/blog" is public, it still can only be accessed by admins. - see C.PAGES_ACCESS_* for details - @param dynamic(bool): if True, activate websocket for bidirectional communication - @param parse_url(callable, None): if set it will be called to handle the URL path - after this method, the page will be rendered if noting is left in path (request.postpath) - else a the request will be transmitted to a subpage - @param prepare_render(callable, None): if set, will be used to prepare the rendering - that often means gathering data using the bridge - @param render(callable, None): if not template is set, this method will be called and - what it returns will be rendered. - This method is mutually exclusive with template and must return a unicode string. - @param template(unicode, None): path to the template to render. - This method is mutually exclusive with render - @param on_data_post(callable, None): method to call when data is posted - None if not post is handled - on_data_post can return a string with following value: - - C.POST_NO_CONFIRM: confirm flag will not be set - @param on_data(callable, None): method to call when dynamic data is sent - this method is used with Libervia's websocket mechanism - @param on_signal(callable, None): method to call when a registered signal is received - this method is used with Libervia's websocket mechanism - """ - - web_resource.Resource.__init__(self) - self.host = host - self.root_dir = root_dir - self.url = url - self.name = name - if name is not None: - if name in self.named_pages: - raise exceptions.ConflictError( - _(u'a Libervia page named "{}" already exists'.format(name)) - ) - if u"/" in name: - raise ValueError(_(u'"/" is not allowed in page names')) - if not name: - raise ValueError(_(u"a page name can't be empty")) - self.named_pages[name] = self - if access is None: - access = C.PAGES_ACCESS_PUBLIC - if access not in ( - C.PAGES_ACCESS_PUBLIC, - C.PAGES_ACCESS_PROFILE, - C.PAGES_ACCESS_NONE, - ): - raise NotImplementedError( - _(u"{} access is not implemented yet").format(access) - ) - self.access = access - self.dynamic = dynamic - if redirect is not None: - # only page access and name make sense in case of full redirection - # so we check that rendering methods/values are not set - if not all( - lambda x: x is not None - for x in (parse_url, prepare_render, render, template) - ): - raise ValueError( - _( - u"you can't use full page redirection with other rendering method," - u"check self.pageRedirect if you need to use them" - ) - ) - self.redirect = redirect - else: - self.redirect = None - self.parse_url = parse_url - self.prepare_render = prepare_render - self.template = template - self.render_method = render - self.on_data_post = on_data_post - self.on_data = on_data - self.on_signal = on_signal - self.url_cache = url_cache - if access == C.PAGES_ACCESS_NONE: - # none pages just return a 404, no further check is needed - return - if template is not None and render is not None: - log.error(_(u"render and template methods can't be used at the same time")) - if parse_url is not None and not callable(parse_url): - log.error(_(u"parse_url must be a callable")) - - # if not None, next rendering will be cached - # it must then contain a list of the the keys to use (without the page instance) - # e.g. [C.SERVICE_PROFILE, "pubsub", server@example.tld, pubsub_node] - self._do_cache = None - - def __unicode__(self): - return u"LiberviaPage {name} at {url}".format( - name=self.name or u"<anonymous>", url=self.url - ) - - def __str__(self): - return self.__unicode__().encode("utf-8") - - @classmethod - def importPages(cls, host, parent=None, path=None): - """Recursively import Libervia pages""" - if path is None: - path = [] - if parent is None: - root_dir = os.path.join(os.path.dirname(libervia.__file__), C.PAGES_DIR) - parent = host - else: - root_dir = parent.root_dir - for d in os.listdir(root_dir): - dir_path = os.path.join(root_dir, d) - if not os.path.isdir(dir_path): - continue - meta_path = os.path.join(dir_path, C.PAGES_META_FILE) - if os.path.isfile(meta_path): - page_data = {} - new_path = path + [d] - # we don't want to force the presence of __init__.py - # so we use execfile instead of import. - # TODO: when moved to Python 3, __init__.py is not mandatory anymore - # so we can switch to import - execfile(meta_path, page_data) - resource = LiberviaPage( - host, - dir_path, - u"/" + u"/".join(new_path), - name=page_data.get("name"), - redirect=page_data.get("redirect"), - access=page_data.get("access"), - dynamic=page_data.get("dynamic", False), - parse_url=page_data.get("parse_url"), - prepare_render=page_data.get("prepare_render"), - render=page_data.get("render"), - template=page_data.get("template"), - on_data_post=page_data.get("on_data_post"), - on_data=page_data.get("on_data"), - on_signal=page_data.get("on_signal"), - url_cache=page_data.get("url_cache", False), - ) - parent.putChild(d, resource) - log.info(u"Added /{path} page".format(path=u"[...]/".join(new_path))) - if "uri_handlers" in page_data: - if not isinstance(page_data, dict): - log.error(_(u"uri_handlers must be a dict")) - else: - for uri_tuple, cb_name in page_data["uri_handlers"].iteritems(): - if len(uri_tuple) != 2 or not isinstance(cb_name, basestring): - log.error(_(u"invalid uri_tuple")) - continue - log.info(_(u"setting {}/{} URIs handler").format(*uri_tuple)) - try: - cb = page_data[cb_name] - except KeyError: - log.error( - _(u"missing {name} method to handle {1}/{2}").format( - name=cb_name, *uri_tuple - ) - ) - continue - else: - resource.registerURI(uri_tuple, cb) - - LiberviaPage.importPages(host, resource, new_path) - - @classmethod - def setMenu(cls, menus): - main_menu = [] - for menu in menus: - if not menu: - msg = _(u"menu item can't be empty") - log.error(msg) - raise ValueError(msg) - elif isinstance(menu, list): - if len(menu) != 2: - msg = _( - u"menu item as list must be in the form [page_name, absolue URL]" - ) - log.error(msg) - raise ValueError(msg) - page_name, url = menu - else: - page_name = menu - try: - url = cls.getPageByName(page_name).url - except KeyError as e: - log.error( - _( - u"Can'find a named page ({msg}), please check menu_json in configuration." - ).format(msg=e) - ) - raise e - main_menu.append((page_name, url)) - cls.main_menu = main_menu - - def registerURI(self, uri_tuple, get_uri_cb): - """register a URI handler - - @param uri_tuple(tuple[unicode, unicode]): type or URIs handler - type/subtype as returned by tools/common/parseXMPPUri - or type/None to handle all subtypes - @param get_uri_cb(callable): method which take uri_data dict as only argument - and return absolute path with correct arguments or None if the page - can't handle this URL - """ - if uri_tuple in self.uri_callbacks: - log.info( - _(u"{}/{} URIs are already handled, replacing by the new handler").format( - *uri_tuple - ) - ) - self.uri_callbacks[uri_tuple] = (self, get_uri_cb) - - def registerSignal(self, request, signal, check_profile=True): - r"""register a signal handler - - the page must be dynamic - when signal is received, self.on_signal will be called with: - - request - - signal name - - signal arguments - signal handler will be removed when connection with dynamic page will be lost - @param signal(unicode): name of the signal - last arg of signal must be profile, as it will be checked to filter signals - @param check_profile(bool): if True, signal profile (which MUST be last arg) will be - checked against session profile. - /!\ if False, profile will not be checked/filtered, be sure to know what you are doing - if you unset this option /!\ - """ - # FIXME: add a timeout, if socket is not opened before it, signal handler must be removed - if not self.dynamic: - log.error(_(u"You can't register signal if page is not dynamic")) - return - LiberviaPage.signals_handlers.setdefault(signal, {})[id(request)] = ( - self, - request, - check_profile, - ) - request._signals_registered.append(signal) - - @classmethod - def getPagePathFromURI(cls, uri): - """Retrieve page URL from xmpp: URI - - @param uri(unicode): URI with a xmpp: scheme - @return (unicode,None): absolute path (starting from root "/") to page handling the URI - None is returned if no page has been registered for this URI - """ - uri_data = common_uri.parseXMPPUri(uri) - try: - page, cb = cls.uri_callbacks[uri_data["type"], uri_data["sub_type"]] - except KeyError: - url = None - else: - url = cb(page, uri_data) - if url is None: - # no handler found - # we try to find a more generic one - try: - page, cb = cls.uri_callbacks[uri_data["type"], None] - except KeyError: - pass - else: - url = cb(page, uri_data) - return url - - @classmethod - def getPageByName(cls, name): - """retrieve page instance from its name - - @param name(unicode): name of the page - @return (LiberviaPage): page instance - @raise KeyError: the page doesn't exist - """ - return cls.named_pages[name] - - def getPageRedirectURL(self, request, page_name=u"login", url=None): - """generate URL for a page with redirect_url parameter set - - mainly used for login page with redirection to current page - @param request(server.Request): current HTTP request - @param page_name(unicode): name of the page to go - @param url(None, unicode): url to redirect to - None to use request path (i.e. current page) - @return (unicode): URL to use - """ - return u"{root_url}?redirect_url={redirect_url}".format( - root_url=self.getPageByName(page_name).url, - redirect_url=urllib.quote_plus(request.uri) - if url is None - else url.encode("utf-8"), - ) - - def getURL(self, *args): - """retrieve URL of the page set arguments - - *args(list[unicode]): argument to add to the URL as path elements - empty or None arguments will be ignored - """ - url_args = [quote(a) for a in args if a] - - if self.name is not None and self.name in self.pages_redirects: - # we check for redirection - redirect_data = self.pages_redirects[self.name] - args_hash = tuple(args) - for limit in xrange(len(args) + 1): - current_hash = args_hash[:limit] - if current_hash in redirect_data: - url_base = redirect_data[current_hash] - remaining = args[limit:] - remaining_url = "/".join(remaining) - return os.path.join("/", url_base, remaining_url) - - return os.path.join(self.url, *url_args) - - def getCurrentURL(self, request): - """retrieve URL used to access this page - - @return(unicode): current URL - """ - # we get url in the following way (splitting request.path instead of using - # request.prepath) because request.prepath may have been modified by - # redirection (if redirection args have been specified), while path reflect - # the real request - - # we ignore empty path elements (i.e. double '/' or '/' at the end) - path_elts = [p for p in request.path.split("/") if p] - - if request.postpath: - if not request.postpath[-1]: - # we remove trailing slash - request.postpath = request.postpath[:-1] - if request.postpath: - # getSubPageURL must return subpage from the point where - # the it is called, so we have to remove remanining - # path elements - path_elts = path_elts[: -len(request.postpath)] - - return u"/" + "/".join(path_elts).decode("utf-8") - - def getParamURL(self, request, **kwargs): - """use URL of current request but modify the parameters in query part - - **kwargs(dict[str, unicode]): argument to use as query parameters - @return (unicode): constructed URL - """ - current_url = self.getCurrentURL(request) - if kwargs: - encoded = urllib.urlencode( - {k: v.encode("utf-8") for k, v in kwargs.iteritems()} - ).decode("utf-8") - current_url = current_url + u"?" + encoded - return current_url - - def getSubPageByName(self, subpage_name, parent=None): - """retrieve a subpage and its path using its name - - @param subpage_name(unicode): name of the sub page - it must be a direct children of parent page - @param parent(LiberviaPage, None): parent page - None to use current page - @return (tuple[str, LiberviaPage]): page subpath and instance - @raise exceptions.NotFound: no page has been found - """ - if parent is None: - parent = self - for path, child in parent.children.iteritems(): - try: - child_name = child.name - except AttributeError: - # LiberviaPages have a name, but maybe this is an other Resource - continue - if child_name == subpage_name: - return path, child - raise exceptions.NotFound(_(u"requested sub page has not been found")) - - def getSubPageURL(self, request, page_name, *args): - """retrieve a page in direct children and build its URL according to request - - request's current path is used as base (at current parsing point, - i.e. it's more prepath than path). - Requested page is checked in children and an absolute URL is then built - by the resulting combination. - This method is useful to construct absolute URLs for children instead of - using relative path, which may not work in subpages, and are linked to the - names of directories (i.e. relative URL will break if subdirectory is renamed - while getSubPageURL won't as long as page_name is consistent). - Also, request.path is used, keeping real path used by user, - and potential redirections. - @param request(server.Request): current HTTP request - @param page_name(unicode): name of the page to retrieve - it must be a direct children of current page - @param *args(list[unicode]): arguments to add as path elements - if an arg is None, it will be ignored - @return (unicode): absolute URL to the sub page - """ - current_url = self.getCurrentURL(request) - path, child = self.getSubPageByName(page_name) - return os.path.join( - u"/", current_url, path, *[quote(a) for a in args if a is not None] - ) - - def getURLByNames(self, named_path): - """retrieve URL from pages names and arguments - - @param named_path(list[tuple[unicode, list[unicode]]]): path to the page as a list - of tuples of 2 items: - - first item is page name - - second item is list of path arguments of this page - @return (unicode): URL to the requested page with given path arguments - @raise exceptions.NotFound: one of the page was not found - """ - current_page = None - path = [] - for page_name, page_args in named_path: - if current_page is None: - current_page = self.getPageByName(page_name) - path.append(current_page.getURL(*page_args)) - else: - sub_path, current_page = self.getSubPageByName( - page_name, parent=current_page - ) - path.append(sub_path) - if page_args: - path.extend([quote(a) for a in page_args]) - return self.host.checkRedirection(u"/".join(path)) - - def getURLByPath(self, *args): - """generate URL by path - - this method as a similar effect as getURLByNames, but it is more readable - by using SubPage to get pages instead of using tuples - @param *args: path element: - - if unicode, will be used as argument - - if util.SubPage instance, must be the name of a subpage - @return (unicode): generated path - """ - args = list(args) - if not args: - raise ValueError("You must specify path elements") - # root page is the one needed to construct the base of the URL - # if first arg is not a SubPage instance, we use current page - if not isinstance(args[0], SubPage): - root = self - else: - root = self.getPageByName(args.pop(0)) - # we keep track of current page to check subpage - current_page = root - url_elts = [] - arguments = [] - while True: - while args and not isinstance(args[0], SubPage): - arguments.append(quote(args.pop(0))) - if not url_elts: - url_elts.append(root.getURL(*arguments)) - else: - url_elts.extend(arguments) - if not args: - break - else: - path, current_page = current_page.getSubPageByName(args.pop(0)) - arguments = [path] - return self.host.checkRedirection(u"/".join(url_elts)) - - def getChildWithDefault(self, path, request): - # we handle children ourselves - raise exceptions.InternalError( - u"this method should not be used with LiberviaPage" - ) - - def nextPath(self, request): - """get next URL path segment, and update request accordingly - - will move first segment of postpath in prepath - @param request(server.Request): current HTTP request - @return (unicode): unquoted segment - @raise IndexError: there is no segment left - """ - pathElement = request.postpath.pop(0) - request.prepath.append(pathElement) - return urllib.unquote(pathElement).decode("utf-8") - - def _filterPathValue(self, value, handler, name, request): - """Modify a path value according to handler (see [getPathArgs])""" - if handler in (u"@", u"@jid") and value == u"@": - value = None - - if handler in (u"", u"@"): - if value is None: - return u"" - elif handler in (u"jid", u"@jid"): - if value: - try: - return jid.JID(value) - except RuntimeError: - log.warning(_(u"invalid jid argument: {value}").format(value=value)) - self.pageError(request, C.HTTP_BAD_REQUEST) - else: - return u"" - else: - return handler(self, value, name, request) - - return value - - def getPathArgs(self, request, names, min_args=0, **kwargs): - """get several path arguments at once - - Arguments will be put in request data. - Missing arguments will have None value - @param names(list[unicode]): list of arguments to get - @param min_args(int): if less than min_args are found, PageError is used with C.HTTP_BAD_REQUEST - use 0 to ignore - @param **kwargs: special value or optional callback to use for arguments - names of the arguments must correspond to those in names - special values may be: - - '': use empty string instead of None when no value is specified - - '@': if value of argument is empty or '@', empty string will be used - - 'jid': value must be converted to jid.JID if it exists, else empty string is used - - '@jid': if value of arguments is empty or '@', empty string will be used, else it will be converted to jid - """ - data = self.getRData(request) - - for idx, name in enumerate(names): - if name[0] == u"*": - value = data[name[1:]] = [] - while True: - try: - value.append(self.nextPath(request)) - except IndexError: - idx -= 1 - break - else: - idx += 1 - else: - try: - value = data[name] = self.nextPath(request) - except IndexError: - data[name] = None - idx -= 1 - break - - values_count = idx + 1 - if values_count < min_args: - log.warning( - _( - u"Missing arguments in URL (got {count}, expected at least {min_args})" - ).format(count=values_count, min_args=min_args) - ) - self.pageError(request, C.HTTP_BAD_REQUEST) - - for name in names[values_count:]: - data[name] = None - - for name, handler in kwargs.iteritems(): - if name[0] == "*": - data[name] = [ - self._filterPathValue(v, handler, name, request) for v in data[name] - ] - else: - data[name] = self._filterPathValue(data[name], handler, name, request) - - ## Cache handling ## - - def _setCacheHeaders(self, request, cache): - """Set ETag and Last-Modified HTTP headers, used for caching""" - request.setHeader("ETag", cache.hash) - last_modified = self.host.getHTTPDate(cache.created) - request.setHeader("Last-Modified", last_modified) - - def _checkCacheHeaders(self, request, cache): - """Check if a cache condition is set on the request - - if condition is valid, C.HTTP_NOT_MODIFIED is returned - """ - etag_match = request.getHeader("If-None-Match") - if etag_match is not None: - if cache.hash == etag_match: - self.pageError(request, C.HTTP_NOT_MODIFIED, no_body=True) - else: - modified_match = request.getHeader("If-Modified-Since") - if modified_match is not None: - modified = date_utils.date_parse(modified_match) - if modified >= int(cache.created): - self.pageError(request, C.HTTP_NOT_MODIFIED, no_body=True) - - def checkCacheSubscribeCb(self, sub_id, service, node): - self.cache_pubsub_sub.add((service, node, sub_id)) - - def checkCacheSubscribeEb(self, failure_, service, node): - log.warning(_(u"Can't subscribe to node: {msg}").format(msg=failure_)) - # FIXME: cache must be marked as unusable here - - def psNodeWatchAddEb(self, failure_, service, node): - log.warning(_(u"Can't add node watched: {msg}").format(msg=failure_)) - - def checkCache(self, request, cache_type, **kwargs): - """check if a page is in cache and return cached version if suitable - - this method may perform extra operation to handle cache (e.g. subscribing to a - pubsub node) - @param request(server.Request): current HTTP request - @param cache_type(int): on of C.CACHE_* const. - @param **kwargs: args according to cache_type: - C.CACHE_PUBSUB: - service: pubsub service - node: pubsub node - short: short name of feature (needed if node is empty to find namespace) - - """ - if request.postpath: - # we are not on the final page, no need to go further - return - - profile = self.getProfile(request) or C.SERVICE_PROFILE - - if cache_type == C.CACHE_PUBSUB: - service, node = kwargs["service"], kwargs["node"] - if not node: - try: - short = kwargs["short"] - node = self.host.ns_map[short] - except KeyError: - log.warning( - _( - u'Can\'t use cache for empty node without namespace set, please ensure to set "short" and that it is registered' - ) - ) - return - if profile != C.SERVICE_PROFILE: - # only service profile is cache for now - return - try: - cache = self.cache[profile][cache_type][service][node][request.uri][self] - except KeyError: - # no cache yet, let's subscribe to the pubsub node - d1 = self.host.bridgeCall( - "psSubscribe", service.full(), node, {}, profile - ) - d1.addCallback(self.checkCacheSubscribeCb, service, node) - d1.addErrback(self.checkCacheSubscribeEb, service, node) - d2 = self.host.bridgeCall("psNodeWatchAdd", service.full(), node, profile) - d2.addErrback(self.psNodeWatchAddEb, service, node) - self._do_cache = [self, profile, cache_type, service, node, request.uri] - # we don't return the Deferreds as it is not needed to wait for - # the subscription to continue with page rendering - return - - else: - raise exceptions.InternalError(u"Unknown cache_type") - log.debug(u"using cache for {page}".format(page=self)) - cache.last_access = time.time() - self._setCacheHeaders(request, cache) - self._checkCacheHeaders(request, cache) - request.write(cache.rendered) - request.finish() - raise failure.Failure(exceptions.CancelError(u"cache is used")) - - def _cacheURL(self, dummy, request, profile): - self.cached_urls.setdefault(profile, {})[request.uri] = CacheURL(request) - - @classmethod - def onNodeEvent(cls, host, service, node, event_type, items, profile): - """Invalidate cache for all pages linked to this node""" - try: - cache = cls.cache[profile][C.CACHE_PUBSUB][jid.JID(service)][node] - except KeyError: - log.info( - _( - u"Removing subscription for {service}/{node}: " - u"the page is not cached" - ).format(service=service, node=node) - ) - d1 = host.bridgeCall("psUnsubscribe", service, node, profile) - d1.addErrback( - lambda failure_: log.warning( - _(u"Can't unsubscribe from {service}/{node}: {msg}").format( - service=service, node=node, msg=failure_ - ) - ) - ) - d2 = host.bridgeCall("psNodeWatchAdd", service, node, profile) - # TODO: check why the page is not in cache, remove subscription? - d2.addErrback( - lambda failure_: log.warning( - _(u"Can't remove watch for {service}/{node}: {msg}").format( - service=service, node=node, msg=failure_ - ) - ) - ) - else: - cache.clear() - - @classmethod - def onSignal(cls, host, signal, *args): - """Generic method which receive registered signals - - if a callback is registered for this signal, call it - @param host: Libervia instance - @param signal(unicode): name of the signal - @param *args: args of the signals - """ - for page, request, check_profile in cls.signals_handlers.get( - signal, {} - ).itervalues(): - if check_profile: - signal_profile = args[-1] - request_profile = page.getProfile(request) - if not request_profile: - # if you want to use signal without session, unset check_profile - # (be sure to know what you are doing) - log.error(_(u"no session started, signal can't be checked")) - continue - if signal_profile != request_profile: - # we ignore the signal, it's not for our profile - continue - if request._signals_cache is not None: - # socket is not yet opened, we cache the signal - request._signals_cache.append((request, signal, args)) - log.debug( - u"signal [{signal}] cached: {args}".format(signal=signal, args=args) - ) - else: - page.on_signal(page, request, signal, *args) - - def onSocketOpen(self, request): - """Called for dynamic pages when socket has just been opened - - we send all cached signals - """ - assert request._signals_cache is not None - cache = request._signals_cache - request._signals_cache = None - for request, signal, args in cache: - self.on_signal(self, request, signal, *args) - - def onSocketClose(self, request): - """Called for dynamic pages when socket has just been closed - - we remove signal handler - """ - for signal in request._signals_registered: - try: - del LiberviaPage.signals_handlers[signal][id(request)] - except KeyError: - log.error( - _( - u"Can't find signal handler for [{signal}], this should not happen" - ).format(signal=signal) - ) - else: - log.debug(_(u"Removed signal handler")) - - def delegateToResource(self, request, resource): - """continue workflow with Twisted Resource""" - buf = resource.render(request) - if buf == server.NOT_DONE_YET: - pass - else: - request.write(buf) - request.finish() - raise failure.Failure(exceptions.CancelError(u"resource delegation")) - - def HTTPRedirect(self, request, url): - """redirect to an URL using HTTP redirection - - @param request(server.Request): current HTTP request - @param url(unicode): url to redirect to - """ - web_util.redirectTo(url.encode("utf-8"), request) - request.finish() - raise failure.Failure(exceptions.CancelError(u"HTTP redirection is used")) - - def redirectOrContinue(self, request, redirect_arg=u"redirect_url"): - """helper method to redirect a page to an url given as arg - - if the arg is not present, the page will continue normal workflow - @param request(server.Request): current HTTP request - @param redirect_arg(unicode): argument to use to get redirection URL - @interrupt: redirect the page to requested URL - @interrupt pageError(C.HTTP_BAD_REQUEST): empty or non local URL is used - """ - try: - url = request.args["redirect_url"][0] - except (KeyError, IndexError): - pass - else: - # a redirection is requested - if not url or url[0] != u"/": - # we only want local urls - self.pageError(request, C.HTTP_BAD_REQUEST) - else: - self.HTTPRedirect(request, url) - - def pageRedirect(self, page_path, request, skip_parse_url=True, path_args=None): - """redirect a page to a named page - - the workflow will continue with the workflow of the named page, - skipping named page's parse_url method if it exist. - If you want to do a HTTP redirection, use HTTPRedirect - @param page_path(unicode): path to page (elements are separated by "/"): - if path starts with a "/": - path is a full path starting from root - else: - - first element is name as registered in name variable - - following element are subpages path - e.g.: "blog" redirect to page named "blog" - "blog/atom.xml" redirect to atom.xml subpage of "blog" - "/common/blog/atom.xml" redirect to the page at the fiven full path - @param request(server.Request): current HTTP request - @param skip_parse_url(bool): if True, parse_url method on redirect page will be skipped - @param path_args(list[unicode], None): path arguments to use in redirected page - @raise KeyError: there is no known page with this name - """ - # FIXME: render non LiberviaPage resources - path = page_path.rstrip(u"/").split(u"/") - if not path[0]: - redirect_page = self.host.root - else: - redirect_page = self.named_pages[path[0]] - - for subpage in path[1:]: - if redirect_page is self.host.root: - redirect_page = redirect_page.children[subpage] - else: - redirect_page = redirect_page.original.children[subpage] - - if path_args is not None: - args = [quote(a) for a in path_args] - request.postpath = args + request.postpath - - if self._do_cache: - # if cache is needed, it will be handled by final page - redirect_page._do_cache = self._do_cache - self._do_cache = None - - redirect_page.renderPage(request, skip_parse_url=skip_parse_url) - raise failure.Failure(exceptions.CancelError(u"page redirection is used")) - - def pageError(self, request, code=C.HTTP_NOT_FOUND, no_body=False): - """generate an error page and terminate the request - - @param request(server.Request): HTTP request - @param core(int): error code to use - @param no_body: don't write body if True - """ - request.setResponseCode(code) - if no_body: - request.finish() - else: - template = u"error/" + unicode(code) + ".html" - - rendered = self.host.renderer.render( - template, - root_path="/templates/", - error_code=code, - **request.template_data - ) - - self.writeData(rendered, request) - raise failure.Failure(exceptions.CancelError(u"error page is used")) - - def writeData(self, data, request): - """write data to transport and finish the request""" - if data is None: - self.pageError(request) - data_encoded = data.encode("utf-8") - - if self._do_cache is not None: - redirected_page = self._do_cache.pop(0) - cache = reduce(lambda d, k: d.setdefault(k, {}), self._do_cache, self.cache) - page_cache = cache[redirected_page] = CachePage(data_encoded) - self._setCacheHeaders(request, page_cache) - log.debug( - _(u"{page} put in cache for [{profile}]").format( - page=self, profile=self._do_cache[0] - ) - ) - self._do_cache = None - self._checkCacheHeaders(request, page_cache) - - request.write(data_encoded) - request.finish() - - def _subpagesHandler(self, dummy, request): - """render subpage if suitable - - this method checks if there is still an unmanaged part of the path - and check if it corresponds to a subpage. If so, it render the subpage - else it render a NoResource. - If there is no unmanaged part of the segment, current page workflow is pursued - """ - if request.postpath: - subpage = self.nextPath(request) - try: - child = self.children[subpage] - except KeyError: - self.pageError(request) - else: - child.render(request) - raise failure.Failure(exceptions.CancelError(u"subpage page is used")) - - def _prepare_dynamic(self, dummy, request): - # we need to activate dynamic page - # we set data for template, and create/register token - socket_token = unicode(uuid.uuid4()) - socket_url = self.host.getWebsocketURL(request) - socket_debug = C.boolConst(self.host.debug) - request.template_data["websocket"] = WebsocketMeta( - socket_url, socket_token, socket_debug - ) - self.host.registerWSToken(socket_token, self, request) - # we will keep track of handlers to remove - request._signals_registered = [] - # we will cache registered signals until socket is opened - request._signals_cache = [] - - def _prepare_render(self, dummy, request): - return defer.maybeDeferred(self.prepare_render, self, request) - - def _render_method(self, dummy, request): - return defer.maybeDeferred(self.render_method, self, request) - - def _render_template(self, dummy, request): - template_data = request.template_data - - # if confirm variable is set in case of successfuly data post - session_data = self.host.getSessionData(request, session_iface.ISATSession) - if session_data.popPageFlag(self, C.FLAG_CONFIRM): - template_data[u"confirm"] = True - - return self.host.renderer.render( - self.template, - root_path="/templates/", - media_path="/" + C.MEDIA_DIR, - cache_path=session_data.cache_dir, - main_menu=LiberviaPage.main_menu, - **template_data - ) - - def _renderEb(self, failure_, request): - """don't raise error on CancelError""" - failure_.trap(exceptions.CancelError) - - def _internalError(self, failure_, request): - """called if an error is not catched""" - log.error( - _(u"Uncatched error for HTTP request on {url}: {msg}").format( - url=request.URLPath(), msg=failure_ - ) - ) - self.pageError(request, C.HTTP_INTERNAL_ERROR) - - def _on_data_post_redirect(self, ret, request): - """called when page's on_data_post has been done successfuly - - This will do a Post/Redirect/Get pattern. - this method redirect to the same page or to request.data['post_redirect_page'] - post_redirect_page can be either a page or a tuple with page as first item, then a list of unicode arguments to append to the url. - if post_redirect_page is not used, initial request.uri (i.e. the same page as where the data have been posted) will be used for redirection. - HTTP status code "See Other" (303) is used as it is the recommanded code in this case. - @param ret(None, unicode, iterable): on_data_post return value - see LiberviaPage.__init__ on_data_post docstring - """ - if ret is None: - ret = () - elif isinstance(ret, basestring): - ret = (ret,) - else: - ret = tuple(ret) - raise NotImplementedError( - _(u"iterable in on_data_post return value is not used yet") - ) - session_data = self.host.getSessionData(request, session_iface.ISATSession) - request_data = self.getRData(request) - if "post_redirect_page" in request_data: - redirect_page_data = request_data["post_redirect_page"] - if isinstance(redirect_page_data, tuple): - redirect_page = redirect_page_data[0] - redirect_page_args = redirect_page_data[1:] - redirect_uri = redirect_page.getURL(*redirect_page_args) - else: - redirect_page = redirect_page_data - redirect_uri = redirect_page.url - else: - redirect_page = self - redirect_uri = request.uri - - if not C.POST_NO_CONFIRM in ret: - session_data.setPageFlag(redirect_page, C.FLAG_CONFIRM) - request.setResponseCode(C.HTTP_SEE_OTHER) - request.setHeader("location", redirect_uri) - request.finish() - raise failure.Failure(exceptions.CancelError(u"Post/Redirect/Get is used")) - - def _on_data_post(self, dummy, request): - csrf_token = self.host.getSessionData( - request, session_iface.ISATSession - ).csrf_token - try: - given_csrf = self.getPostedData(request, u"csrf_token") - except KeyError: - given_csrf = None - if given_csrf is None or given_csrf != csrf_token: - log.warning( - _(u"invalid CSRF token, hack attempt? URL: {url}, IP: {ip}").format( - url=request.uri, ip=request.getClientIP() - ) - ) - self.pageError(request, C.HTTP_UNAUTHORIZED) - d = defer.maybeDeferred(self.on_data_post, self, request) - d.addCallback(self._on_data_post_redirect, request) - return d - - def getPostedData(self, request, keys, multiple=False): - """get data from a POST request or from URL's query part and decode it - - @param request(server.Request): request linked to the session - @param keys(unicode, iterable[unicode]): name of the value(s) to get - unicode to get one value - iterable to get more than one - @param multiple(bool): True if multiple values are possible/expected - if False, the first value is returned - @return (iterator[unicode], list[iterator[unicode], unicode, list[unicode]): values received for this(these) key(s) - @raise KeyError: one specific key has been requested, and it is missing - """ - # FIXME: request.args is already unquoting the value, it seems we are doing double unquote - if isinstance(keys, basestring): - keys = [keys] - get_first = True - else: - get_first = False - - ret = [] - for key in keys: - gen = (urllib.unquote(v).decode("utf-8") for v in request.args.get(key, [])) - if multiple: - ret.append(gen) - else: - try: - ret.append(next(gen)) - except StopIteration: - raise KeyError(key) - - return ret[0] if get_first else ret - - def getAllPostedData(self, request, except_=(), multiple=True): - """get all posted data - - @param request(server.Request): request linked to the session - @param except_(iterable[unicode]): key of values to ignore - csrf_token will always be ignored - @param multiple(bool): if False, only the first values are returned - @return (dict[unicode, list[unicode]]): post values - """ - except_ = tuple(except_) + (u"csrf_token",) - ret = {} - for key, values in request.args.iteritems(): - key = urllib.unquote(key).decode("utf-8") - if key in except_: - continue - if not multiple: - ret[key] = urllib.unquote(values[0]).decode("utf-8") - else: - ret[key] = [urllib.unquote(v).decode("utf-8") for v in values] - return ret - - def getProfile(self, request): - """helper method to easily get current profile - - @return (unicode, None): current profile - None if no profile session is started - """ - sat_session = self.host.getSessionData(request, session_iface.ISATSession) - return sat_session.profile - - def getRData(self, request): - """helper method to get request data dict - - this dictionnary if for the request only, it is not saved in session - It is mainly used to pass data between pages/methods called during request workflow - @return (dict): request data - """ - try: - return request.data - except AttributeError: - request.data = {} - return request.data - - def _checkAccess(self, data, request): - """Check access according to self.access - - if access is not granted, show a HTTP_UNAUTHORIZED pageError and stop request, - else return data (so it can be inserted in deferred chain - """ - if self.access == C.PAGES_ACCESS_PUBLIC: - pass - elif self.access == C.PAGES_ACCESS_PROFILE: - profile = self.getProfile(request) - if not profile: - # no session started - if not self.host.options["allow_registration"]: - # registration not allowed, access is not granted - self.pageError(request, C.HTTP_UNAUTHORIZED) - else: - # registration allowed, we redirect to login page - login_url = self.getPageRedirectURL(request) - self.HTTPRedirect(request, login_url) - - return data - - def renderPartial(self, request, template, template_data): - """Render a template to be inserted in dynamic page - - this is NOT the normal page rendering method, it is used only to update - dynamic pages - @param template(unicode): path of the template to render - @param template_data(dict): template_data to use - """ - if not self.dynamic: - raise exceptions.InternalError( - _(u"renderPartial must only be used with dynamic pages") - ) - session_data = self.host.getSessionData(request, session_iface.ISATSession) - - return self.host.renderer.render( - template, - root_path="/templates/", - media_path="/" + C.MEDIA_DIR, - cache_path=session_data.cache_dir, - main_menu=LiberviaPage.main_menu, - **template_data - ) - - def renderAndUpdate( - self, request, template, selectors, template_data_update, update_type="append" - ): - """Helper method to render a partial page element and update the page - - this is NOT the normal page rendering method, it is used only to update - dynamic pages - @param request(server.Request): current HTTP request - @param template: same as for [renderPartial] - @param selectors: CSS selectors to use - @param template_data_update: template data to use - template data cached in request will be copied then updated - with this data - @parap update_type(unicode): one of: - append: append rendered element to selected element - """ - template_data = request.template_data.copy() - template_data.update(template_data_update) - html = self.renderPartial(request, template, template_data) - request.sendData(u"dom", selectors=selectors, update_type=update_type, html=html) - - def renderPage(self, request, skip_parse_url=False): - """Main method to handle the workflow of a LiberviaPage""" - - # template_data are the variables passed to template - if not hasattr(request, "template_data"): - session_data = self.host.getSessionData(request, session_iface.ISATSession) - csrf_token = session_data.csrf_token - request.template_data = { - u"profile": session_data.profile, - u"csrf_token": csrf_token, - } - - # XXX: here is the code which need to be executed once - # at the beginning of the request hanling - if request.postpath and not request.postpath[-1]: - # we don't differenciate URLs finishing with '/' or not - del request.postpath[-1] - - d = defer.Deferred() - d.addCallback(self._checkAccess, request) - - if self.redirect is not None: - d.addCallback( - lambda dummy: self.pageRedirect( - self.redirect, request, skip_parse_url=False - ) - ) - - if self.parse_url is not None and not skip_parse_url: - if self.url_cache: - profile = self.getProfile(request) - try: - cache_url = self.cached_urls[profile][request.uri] - except KeyError: - # no cache for this URI yet - # we do normal URL parsing, and then the cache - d.addCallback(self.parse_url, request) - d.addCallback(self._cacheURL, request, profile) - else: - log.debug(_(u"using URI cache for {page}").format(page=self)) - cache_url.use(request) - else: - d.addCallback(self.parse_url, request) - - d.addCallback(self._subpagesHandler, request) - - if request.method not in (C.HTTP_METHOD_GET, C.HTTP_METHOD_POST): - # only HTTP GET and POST are handled so far - d.addCallback(lambda dummy: self.pageError(request, C.HTTP_BAD_REQUEST)) - - if request.method == C.HTTP_METHOD_POST: - if self.on_data_post is None: - # if we don't have on_data_post, the page was not expecting POST - # so we return an error - d.addCallback(lambda dummy: self.pageError(request, C.HTTP_BAD_REQUEST)) - else: - d.addCallback(self._on_data_post, request) - # by default, POST follow normal behaviour after on_data_post is called - # this can be changed by a redirection or other method call in on_data_post - - if self.dynamic: - d.addCallback(self._prepare_dynamic, request) - - if self.prepare_render: - d.addCallback(self._prepare_render, request) - - if self.template: - d.addCallback(self._render_template, request) - elif self.render_method: - d.addCallback(self._render_method, request) - - d.addCallback(self.writeData, request) - d.addErrback(self._renderEb, request) - d.addErrback(self._internalError, request) - d.callback(self) - return server.NOT_DONE_YET - - def render_GET(self, request): - return self.renderPage(request) - - def render_POST(self, request): - return self.renderPage(request)
--- a/src/server/pages_tools.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,58 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2011-2018 Jérôme Poisson <goffi@goffi.org> - -# This program 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. - -# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. - -"""Helper methods for common operations on pages""" - -from sat.core.i18n import _ -from libervia.server.constants import Const as C -from twisted.internet import defer -from sat.core.log import getLogger - -log = getLogger(__name__) -from sat.tools.common import data_objects - - -def commentsDataToObjects(comments_data): - return data_objects.BlogItems(comments_data) - - -def retrieveComments(self, service, node, profile, pass_exceptions=True): - """Retrieve comments from server and convert them to data objects - - @param service(unicode): service holding the comments - @param node(unicode): node to retrieve - @param profile(unicode): profile of the user willing to find comments - @param pass_exceptions(bool): if True bridge exceptions will be ignored but logged - else exception will be raised - """ - try: - d = self.host.bridgeCall(u"mbGet", service, node, C.NO_LIMIT, [], {}, profile) - except Exception as e: - if not pass_exceptions: - raise e - else: - log.warning( - _(u"Can't get comments at {node} (service: {service}): {msg}").format( - service=service, node=node, msg=e - ) - ) - return defer.succeed([]) - - d.addCallback(commentsDataToObjects) - return d
--- a/src/server/server.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,2525 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2011-2018 Jérôme Poisson <goffi@goffi.org> - -# This program 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. - -# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. - -from twisted.application import service -from twisted.internet import reactor, defer -from twisted.web import server -from twisted.web import static -from twisted.web import resource as web_resource -from twisted.web import util as web_util -from twisted.web import http -from twisted.python.components import registerAdapter -from twisted.python import failure -from twisted.words.protocols.jabber import jid - -from txjsonrpc.web import jsonrpc -from txjsonrpc import jsonrpclib - -from sat.core.log import getLogger - -log = getLogger(__name__) -from sat_frontends.bridge.dbus_bridge import ( - Bridge, - BridgeExceptionNoService, - const_TIMEOUT as BRIDGE_TIMEOUT, -) -from sat.core.i18n import _, D_ -from sat.core import exceptions -from sat.tools import utils -from sat.tools.common import regex -from sat.tools.common import template - -import re -import glob -import os.path -import sys -import tempfile -import shutil -import uuid -import urlparse -import urllib -import time -from httplib import HTTPS_PORT -import libervia -from libervia.server import websockets -from libervia.server.pages import LiberviaPage -from libervia.server.utils import quote, ProgressHandler -from functools import partial - -try: - import OpenSSL - from twisted.internet import ssl -except ImportError: - ssl = None - -from libervia.server.constants import Const as C -from libervia.server.blog import MicroBlog -from libervia.server import session_iface - - -# following value are set from twisted.plugins.libervia_server initialise (see the comment there) -DATA_DIR_DEFAULT = OPT_PARAMETERS_BOTH = OPT_PARAMETERS_CFG = coerceDataDir = None - - -class LiberviaSession(server.Session): - sessionTimeout = C.SESSION_TIMEOUT - - def __init__(self, *args, **kwargs): - self.__lock = False - server.Session.__init__(self, *args, **kwargs) - - def lock(self): - """Prevent session from expiring""" - self.__lock = True - self._expireCall.reset(sys.maxint) - - def unlock(self): - """Allow session to expire again, and touch it""" - self.__lock = False - self.touch() - - def touch(self): - if not self.__lock: - server.Session.touch(self) - - -class ProtectedFile(static.File): - """A static.File class which doens't show directory listing""" - - def directoryListing(self): - return web_resource.NoResource() - - -class LiberviaRootResource(ProtectedFile): - """Specialized resource for Libervia root - - handle redirections declared in sat.conf - """ - - def _initRedirections(self, options): - ## redirections - self.redirections = {} - self.inv_redirections = {} # new URL to old URL map - - if options["url_redirections_dict"] and not options["url_redirections_profile"]: - # FIXME: url_redirections_profile should not be needed. It is currently used to - # redirect to an URL which associate the profile with the service, but this - # is not clean, and service should be explicitly specified - raise ValueError( - u"url_redirections_profile need to be filled if you want to use url_redirections_dict" - ) - - for old, new_data in options["url_redirections_dict"].iteritems(): - # new_data can be a dictionary or a unicode url - if isinstance(new_data, dict): - # new_data dict must contain either "url", "page" or "path" key (exclusive) - # if "path" is used, a file url is constructed with it - if len({"path", "url", "page"}.intersection(new_data.keys())) != 1: - raise ValueError( - u'You must have one and only one of "url", "page" or "path" key in your url_redirections_dict data' - ) - if "url" in new_data: - new = new_data["url"] - elif "page" in new_data: - new = new_data - new["type"] = "page" - new.setdefault("path_args", []) - if not isinstance(new["path_args"], list): - log.error( - _( - u'"path_args" in redirection of {old} must be a list. Ignoring the redirection'.format( - old=old - ) - ) - ) - continue - new.setdefault("query_args", {}) - if not isinstance(new["query_args"], dict): - log.error( - _( - u'"query_args" in redirection of {old} must be a dictionary. Ignoring the redirection'.format( - old=old - ) - ) - ) - continue - new["path_args"] = [quote(a) for a in new["path_args"]] - # we keep an inversed dict of page redirection (page/path_args => redirecting URL) - # so getURL can return the redirecting URL if the same arguments are used - # making the URL consistent - args_hash = tuple(new["path_args"]) - LiberviaPage.pages_redirects.setdefault(new_data["page"], {})[ - args_hash - ] = old - - # we need lists in query_args because it will be used - # as it in request.path_args - for k, v in new["query_args"].iteritems(): - if isinstance(v, basestring): - new["query_args"][k] = [v] - elif "path" in new_data: - new = "file:{}".format(urllib.quote(new_data["path"])) - elif isinstance(new_data, basestring): - new = new_data - new_data = {} - else: - log.error( - _(u"ignoring invalid redirection value: {new_data}").format( - new_data=new_data - ) - ) - continue - - # some normalization - if not old.strip(): - # root URL special case - old = "" - elif not old.startswith("/"): - log.error( - _( - u"redirected url must start with '/', got {value}. Ignoring" - ).format(value=old) - ) - continue - else: - old = self._normalizeURL(old) - - if isinstance(new, dict): - # dict are handled differently, they contain data - # which ared use dynamically when the request is done - self.redirections[old] = new - if not old: - if new[u"type"] == u"page": - log.info( - _(u"Root URL redirected to page {name}").format( - name=new[u"page"] - ) - ) - else: - if new[u"type"] == u"page": - page = LiberviaPage.getPageByName(new[u"page"]) - url = page.getURL(*new.get(u"path_args", [])) - self.inv_redirections[url] = old - continue - - # at this point we have a redirection URL in new, we can parse it - new_url = urlparse.urlsplit(new.encode("utf-8")) - - # we handle the known URL schemes - if new_url.scheme == "xmpp": - location = LiberviaPage.getPagePathFromURI(new) - if location is None: - log.warning( - _( - u"ignoring redirection, no page found to handle this URI: {uri}" - ).format(uri=new) - ) - continue - request_data = self._getRequestData(location) - if old: - self.inv_redirections[location] = old - - elif new_url.scheme in ("", "http", "https"): - # direct redirection - if new_url.netloc: - raise NotImplementedError( - u"netloc ({netloc}) is not implemented yet for url_redirections_dict, it is not possible to redirect to an external website".format( - netloc=new_url.netloc - ) - ) - location = urlparse.urlunsplit( - ("", "", new_url.path, new_url.query, new_url.fragment) - ).decode("utf-8") - request_data = self._getRequestData(location) - if old: - self.inv_redirections[location] = old - - elif new_url.scheme in ("file"): - # file or directory - if new_url.netloc: - raise NotImplementedError( - u"netloc ({netloc}) is not implemented for url redirection to file system, it is not possible to redirect to an external host".format( - netloc=new_url.netloc - ) - ) - path = urllib.unquote(new_url.path) - if not os.path.isabs(path): - raise ValueError( - u"file redirection must have an absolute path: e.g. file:/path/to/my/file" - ) - # for file redirection, we directly put child here - segments, dummy, last_segment = old.rpartition("/") - url_segments = segments.split("/") if segments else [] - current = self - for segment in url_segments: - resource = web_resource.NoResource() - current.putChild(segment, resource) - current = resource - resource_class = ( - ProtectedFile if new_data.get("protected", True) else static.File - ) - current.putChild(last_segment, resource_class(path)) - log.info( - u"Added redirection from /{old} to file system path {path}".format( - old=old.decode("utf-8"), path=path.decode("utf-8") - ) - ) - continue # we don't want to use redirection system, so we continue here - - else: - raise NotImplementedError( - u"{scheme}: scheme is not managed for url_redirections_dict".format( - scheme=new_url.scheme - ) - ) - - self.redirections[old] = request_data - if not old: - log.info( - _(u"Root URL redirected to {uri}").format( - uri=request_data[1].decode("utf-8") - ) - ) - - # no need to keep url_redirections*, they will not be used anymore - del options["url_redirections_dict"] - del options["url_redirections_profile"] - - # the default root URL, if not redirected - if not "" in self.redirections: - self.redirections[""] = self._getRequestData(C.LIBERVIA_MAIN_PAGE) - - def _normalizeURL(self, url, lower=True): - """Return URL normalized for self.redirections dict - - @param url(unicode): URL to normalize - @param lower(bool): lower case of url if True - @return (str): normalized URL - """ - if lower: - url = url.lower() - return "/".join((p for p in url.encode("utf-8").split("/") if p)) - - def _getRequestData(self, uri): - """Return data needed to redirect request - - @param url(unicode): destination url - @return (tuple(list[str], str, str, dict): tuple with - splitted path as in Request.postpath - uri as in Request.uri - path as in Request.path - args as in Request.args - """ - uri = uri.encode("utf-8") - # XXX: we reuse code from twisted.web.http.py here - # as we need to have the same behaviour - x = uri.split(b"?", 1) - - if len(x) == 1: - path = uri - args = {} - else: - path, argstring = x - args = http.parse_qs(argstring, 1) - - # XXX: splitted path case must not be changed, as it may be significant - # (e.g. for blog items) - return ( - self._normalizeURL(path.decode("utf-8"), lower=False).split("/"), - uri, - path, - args, - ) - - def _redirect(self, request, request_data): - """Redirect an URL by rewritting request - - this is *NOT* a HTTP redirection, but equivalent to URL rewritting - @param request(web.http.request): original request - @param request_data(tuple): data returned by self._getRequestData - @return (web_resource.Resource): resource to use - """ - # recursion check - try: - request._redirected - except AttributeError: - pass - else: - try: - dummy, uri, dummy, dummy = request_data - except ValueError: - uri = u"" - log.error( - D_( - u"recursive redirection, please fix this URL:\n{old} ==> {new}" - ).format(old=request.uri.decode("utf-8"), new=uri.decode("utf-8")) - ) - return web_resource.NoResource() - - request._redirected = True # here to avoid recursive redirections - - if isinstance(request_data, dict): - if request_data["type"] == "page": - try: - page = LiberviaPage.getPageByName(request_data["page"]) - except KeyError: - log.error( - _( - u'Can\'t find page named "{name}" requested in redirection' - ).format(name=request_data["page"]) - ) - return web_resource.NoResource() - request.postpath = request_data["path_args"][:] + request.postpath - - try: - request.args.update(request_data["query_args"]) - except (TypeError, ValueError): - log.error( - _(u"Invalid args in redirection: {query_args}").format( - query_args=request_data["query_args"] - ) - ) - return web_resource.NoResource() - return page - else: - raise exceptions.InternalError(u"unknown request_data type") - else: - path_list, uri, path, args = request_data - log.debug( - u"Redirecting URL {old} to {new}".format( - old=request.uri.decode("utf-8"), new=uri.decode("utf-8") - ) - ) - # we change the request to reflect the new url - request.postpath = path_list[1:] + request.postpath - request.args = args - - # we start again to look for a child with the new url - return self.getChildWithDefault(path_list[0], request) - - def getChildWithDefault(self, name, request): - # XXX: this method is overriden only for root url - # which is the only ones who need to be handled before other children - if name == "" and not request.postpath: - return self._redirect(request, self.redirections[""]) - return super(LiberviaRootResource, self).getChildWithDefault(name, request) - - def getChild(self, name, request): - resource = super(LiberviaRootResource, self).getChild(name, request) - - if isinstance(resource, web_resource.NoResource): - # if nothing was found, we try our luck with redirections - # XXX: we want redirections to happen only if everything else failed - path_elt = request.prepath + request.postpath - for idx in xrange(len(path_elt), 0, -1): - test_url = "/".join(path_elt[:idx]).lower() - if test_url in self.redirections: - request_data = self.redirections[test_url] - request.postpath = path_elt[idx:] - return self._redirect(request, request_data) - - return resource - - def createSimilarFile(self, path): - # XXX: this method need to be overriden to avoid recreating a LiberviaRootResource - - f = LiberviaRootResource.__base__( - path, self.defaultType, self.ignoredExts, self.registry - ) - # refactoring by steps, here - constructor should almost certainly take these - f.processors = self.processors - f.indexNames = self.indexNames[:] - f.childNotFound = self.childNotFound - return f - - -class JSONRPCMethodManager(jsonrpc.JSONRPC): - def __init__(self, sat_host): - jsonrpc.JSONRPC.__init__(self) - self.sat_host = sat_host - - def asyncBridgeCall(self, method_name, *args, **kwargs): - return self.sat_host.bridgeCall(method_name, *args, **kwargs) - - -class MethodHandler(JSONRPCMethodManager): - def __init__(self, sat_host): - JSONRPCMethodManager.__init__(self, sat_host) - - def render(self, request): - self.session = request.getSession() - profile = session_iface.ISATSession(self.session).profile - if not profile: - # user is not identified, we return a jsonrpc fault - parsed = jsonrpclib.loads(request.content.read()) - fault = jsonrpclib.Fault( - C.ERRNUM_LIBERVIA, C.NOT_ALLOWED - ) # FIXME: define some standard error codes for libervia - return jsonrpc.JSONRPC._cbRender( - self, fault, request, parsed.get("id"), parsed.get("jsonrpc") - ) # pylint: disable=E1103 - return jsonrpc.JSONRPC.render(self, request) - - @defer.inlineCallbacks - def jsonrpc_getVersion(self): - """Return SàT version""" - try: - defer.returnValue(self._version_cache) - except AttributeError: - self._version_cache = yield self.sat_host.bridgeCall("getVersion") - defer.returnValue(self._version_cache) - - def jsonrpc_getLiberviaVersion(self): - """Return Libervia version""" - return self.sat_host.full_version - - def jsonrpc_disconnect(self): - """Disconnect the profile""" - sat_session = session_iface.ISATSession(self.session) - profile = sat_session.profile - self.sat_host.bridgeCall("disconnect", profile) - - def jsonrpc_getContacts(self): - """Return all passed args.""" - profile = session_iface.ISATSession(self.session).profile - return self.sat_host.bridgeCall("getContacts", profile) - - @defer.inlineCallbacks - def jsonrpc_addContact(self, entity, name, groups): - """Subscribe to contact presence, and add it to the given groups""" - profile = session_iface.ISATSession(self.session).profile - yield self.sat_host.bridgeCall("addContact", entity, profile) - yield self.sat_host.bridgeCall("updateContact", entity, name, groups, profile) - - def jsonrpc_delContact(self, entity): - """Remove contact from contacts list""" - profile = session_iface.ISATSession(self.session).profile - return self.sat_host.bridgeCall("delContact", entity, profile) - - def jsonrpc_updateContact(self, entity, name, groups): - """Update contact's roster item""" - profile = session_iface.ISATSession(self.session).profile - return self.sat_host.bridgeCall("updateContact", entity, name, groups, profile) - - def jsonrpc_subscription(self, sub_type, entity): - """Confirm (or infirm) subscription, - and setup user roster in case of subscription""" - profile = session_iface.ISATSession(self.session).profile - return self.sat_host.bridgeCall("subscription", sub_type, entity, profile) - - def jsonrpc_getWaitingSub(self): - """Return list of room already joined by user""" - profile = session_iface.ISATSession(self.session).profile - return self.sat_host.bridgeCall("getWaitingSub", profile) - - def jsonrpc_setStatus(self, presence, status): - """Change the presence and/or status - @param presence: value from ("", "chat", "away", "dnd", "xa") - @param status: any string to describe your status - """ - profile = session_iface.ISATSession(self.session).profile - return self.sat_host.bridgeCall( - "setPresence", "", presence, {"": status}, profile - ) - - def jsonrpc_messageSend(self, to_jid, msg, subject, type_, extra={}): - """send message""" - profile = session_iface.ISATSession(self.session).profile - return self.asyncBridgeCall( - "messageSend", to_jid, msg, subject, type_, extra, profile - ) - - ## PubSub ## - - def jsonrpc_psNodeDelete(self, service, node): - """Delete a whole node - - @param service (unicode): service jid - @param node (unicode): node to delete - """ - profile = session_iface.ISATSession(self.session).profile - return self.asyncBridgeCall("psNodeDelete", service, node, profile) - - # def jsonrpc_psRetractItem(self, service, node, item, notify): - # """Delete a whole node - - # @param service (unicode): service jid - # @param node (unicode): node to delete - # @param items (iterable): id of item to retract - # @param notify (bool): True if notification is required - # """ - # profile = session_iface.ISATSession(self.session).profile - # return self.asyncBridgeCall("psRetractItem", service, node, item, notify, profile) - - # def jsonrpc_psRetractItems(self, service, node, items, notify): - # """Delete a whole node - - # @param service (unicode): service jid - # @param node (unicode): node to delete - # @param items (iterable): ids of items to retract - # @param notify (bool): True if notification is required - # """ - # profile = session_iface.ISATSession(self.session).profile - # return self.asyncBridgeCall("psRetractItems", service, node, items, notify, profile) - - ## microblogging ## - - def jsonrpc_mbSend(self, service, node, mb_data): - """Send microblog data - - @param service (unicode): service jid or empty string to use profile's microblog - @param node (unicode): publishing node, or empty string to use microblog node - @param mb_data(dict): microblog data - @return: a deferred - """ - profile = session_iface.ISATSession(self.session).profile - return self.asyncBridgeCall("mbSend", service, node, mb_data, profile) - - def jsonrpc_mbRetract(self, service, node, items): - """Delete a whole node - - @param service (unicode): service jid, empty string for PEP - @param node (unicode): node to delete, empty string for default node - @param items (iterable): ids of items to retract - """ - profile = session_iface.ISATSession(self.session).profile - return self.asyncBridgeCall("mbRetract", service, node, items, profile) - - def jsonrpc_mbGet(self, service_jid, node, max_items, item_ids, extra): - """Get last microblogs from publisher_jid - - @param service_jid (unicode): pubsub service, usually publisher jid - @param node(unicode): mblogs node, or empty string to get the defaut one - @param max_items (int): maximum number of item to get or C.NO_LIMIT to get everything - @param item_ids (list[unicode]): list of item IDs - @param rsm (dict): TODO - @return: a deferred couple with the list of items and metadatas. - """ - profile = session_iface.ISATSession(self.session).profile - return self.asyncBridgeCall( - "mbGet", service_jid, node, max_items, item_ids, extra, profile - ) - - def jsonrpc_mbGetFromMany(self, publishers_type, publishers, max_items, extra): - """Get many blog nodes at once - - @param publishers_type (unicode): one of "ALL", "GROUP", "JID" - @param publishers (tuple(unicode)): tuple of publishers (empty list for all, list of groups or list of jids) - @param max_items (int): maximum number of item to get or C.NO_LIMIT to get everything - @param extra (dict): TODO - @return (str): RT Deferred session id - """ - profile = session_iface.ISATSession(self.session).profile - return self.sat_host.bridgeCall( - "mbGetFromMany", publishers_type, publishers, max_items, extra, profile - ) - - def jsonrpc_mbGetFromManyRTResult(self, rt_session): - """Get results from RealTime mbGetFromMany session - - @param rt_session (str): RT Deferred session id - """ - profile = session_iface.ISATSession(self.session).profile - return self.asyncBridgeCall("mbGetFromManyRTResult", rt_session, profile) - - def jsonrpc_mbGetFromManyWithComments( - self, - publishers_type, - publishers, - max_items, - max_comments, - rsm_dict, - rsm_comments_dict, - ): - """Helper method to get the microblogs and their comments in one shot - - @param publishers_type (str): type of the list of publishers (one of "GROUP" or "JID" or "ALL") - @param publishers (list): list of publishers, according to publishers_type (list of groups or list of jids) - @param max_items (int): optional limit on the number of retrieved items. - @param max_comments (int): maximum number of comments to retrieve - @param rsm_dict (dict): RSM data for initial items only - @param rsm_comments_dict (dict): RSM data for comments only - @param profile_key: profile key - @return (str): RT Deferred session id - """ - profile = session_iface.ISATSession(self.session).profile - return self.sat_host.bridgeCall( - "mbGetFromManyWithComments", - publishers_type, - publishers, - max_items, - max_comments, - rsm_dict, - rsm_comments_dict, - profile, - ) - - def jsonrpc_mbGetFromManyWithCommentsRTResult(self, rt_session): - """Get results from RealTime mbGetFromManyWithComments session - - @param rt_session (str): RT Deferred session id - """ - profile = session_iface.ISATSession(self.session).profile - return self.asyncBridgeCall( - "mbGetFromManyWithCommentsRTResult", rt_session, profile - ) - - # def jsonrpc_sendMblog(self, type_, dest, text, extra={}): - # """ Send microblog message - # @param type_ (unicode): one of "PUBLIC", "GROUP" - # @param dest (tuple(unicode)): recipient groups (ignored for "PUBLIC") - # @param text (unicode): microblog's text - # """ - # profile = session_iface.ISATSession(self.session).profile - # extra['allow_comments'] = 'True' - - # if not type_: # auto-detect - # type_ = "PUBLIC" if dest == [] else "GROUP" - - # if type_ in ("PUBLIC", "GROUP") and text: - # if type_ == "PUBLIC": - # #This text if for the public microblog - # log.debug("sending public blog") - # return self.sat_host.bridge.sendGroupBlog("PUBLIC", (), text, extra, profile) - # else: - # log.debug("sending group blog") - # dest = dest if isinstance(dest, list) else [dest] - # return self.sat_host.bridge.sendGroupBlog("GROUP", dest, text, extra, profile) - # else: - # raise Exception("Invalid data") - - # def jsonrpc_deleteMblog(self, pub_data, comments): - # """Delete a microblog node - # @param pub_data: a tuple (service, comment node identifier, item identifier) - # @param comments: comments node identifier (for main item) or False - # """ - # profile = session_iface.ISATSession(self.session).profile - # return self.sat_host.bridge.deleteGroupBlog(pub_data, comments if comments else '', profile) - - # def jsonrpc_updateMblog(self, pub_data, comments, message, extra={}): - # """Modify a microblog node - # @param pub_data: a tuple (service, comment node identifier, item identifier) - # @param comments: comments node identifier (for main item) or False - # @param message: new message - # @param extra: dict which option name as key, which can be: - # - allow_comments: True to accept an other level of comments, False else (default: False) - # - rich: if present, contain rich text in currently selected syntax - # """ - # profile = session_iface.ISATSession(self.session).profile - # if comments: - # extra['allow_comments'] = 'True' - # return self.sat_host.bridge.updateGroupBlog(pub_data, comments if comments else '', message, extra, profile) - - # def jsonrpc_sendMblogComment(self, node, text, extra={}): - # """ Send microblog message - # @param node: url of the comments node - # @param text: comment - # """ - # profile = session_iface.ISATSession(self.session).profile - # if node and text: - # return self.sat_host.bridge.sendGroupBlogComment(node, text, extra, profile) - # else: - # raise Exception("Invalid data") - - # def jsonrpc_getMblogs(self, publisher_jid, item_ids, max_items=C.RSM_MAX_ITEMS): - # """Get specified microblogs posted by a contact - # @param publisher_jid: jid of the publisher - # @param item_ids: list of microblogs items IDs - # @return list of microblog data (dict)""" - # profile = session_iface.ISATSession(self.session).profile - # d = self.asyncBridgeCall("getGroupBlogs", publisher_jid, item_ids, {'max_': unicode(max_items)}, False, profile) - # return d - - # def jsonrpc_getMblogsWithComments(self, publisher_jid, item_ids, max_comments=C.RSM_MAX_COMMENTS): - # """Get specified microblogs posted by a contact and their comments - # @param publisher_jid: jid of the publisher - # @param item_ids: list of microblogs items IDs - # @return list of couple (microblog data, list of microblog data)""" - # profile = session_iface.ISATSession(self.session).profile - # d = self.asyncBridgeCall("getGroupBlogsWithComments", publisher_jid, item_ids, {}, max_comments, profile) - # return d - - # def jsonrpc_getMassiveMblogs(self, publishers_type, publishers, rsm=None): - # """Get lasts microblogs posted by several contacts at once - - # @param publishers_type (unicode): one of "ALL", "GROUP", "JID" - # @param publishers (tuple(unicode)): tuple of publishers (empty list for all, list of groups or list of jids) - # @param rsm (dict): TODO - # @return: dict{unicode: list[dict]) - # key: publisher's jid - # value: list of microblog data (dict) - # """ - # profile = session_iface.ISATSession(self.session).profile - # if rsm is None: - # rsm = {'max_': unicode(C.RSM_MAX_ITEMS)} - # d = self.asyncBridgeCall("getMassiveGroupBlogs", publishers_type, publishers, rsm, profile) - # self.sat_host.bridge.massiveSubscribeGroupBlogs(publishers_type, publishers, profile) - # return d - - # def jsonrpc_getMblogComments(self, service, node, rsm=None): - # """Get all comments of given node - # @param service: jid of the service hosting the node - # @param node: comments node - # """ - # profile = session_iface.ISATSession(self.session).profile - # if rsm is None: - # rsm = {'max_': unicode(C.RSM_MAX_COMMENTS)} - # d = self.asyncBridgeCall("getGroupBlogComments", service, node, rsm, profile) - # return d - - def jsonrpc_getPresenceStatuses(self): - """Get Presence information for connected contacts""" - profile = session_iface.ISATSession(self.session).profile - return self.sat_host.bridgeCall("getPresenceStatuses", profile) - - def jsonrpc_historyGet(self, from_jid, to_jid, size, between, search=""): - """Return history for the from_jid/to_jid couple""" - sat_session = session_iface.ISATSession(self.session) - profile = sat_session.profile - sat_jid = sat_session.jid - if not sat_jid: - raise exceptions.InternalError("session jid should be set") - if ( - jid.JID(from_jid).userhost() != sat_jid.userhost() - and jid.JID(to_jid).userhost() != sat_jid.userhost() - ): - log.error( - u"Trying to get history from a different jid (given (browser): {}, real (backend): {}), maybe a hack attempt ?".format( - from_jid, sat_jid - ) - ) - return {} - d = self.asyncBridgeCall( - "historyGet", from_jid, to_jid, size, between, search, profile - ) - - def show(result_dbus): - result = [] - for line in result_dbus: - # XXX: we have to do this stupid thing because Python D-Bus use its own types instead of standard types - # and txJsonRPC doesn't accept D-Bus types, resulting in a empty query - uuid, timestamp, from_jid, to_jid, message, subject, mess_type, extra = ( - line - ) - result.append( - ( - unicode(uuid), - float(timestamp), - unicode(from_jid), - unicode(to_jid), - dict(message), - dict(subject), - unicode(mess_type), - dict(extra), - ) - ) - return result - - d.addCallback(show) - return d - - def jsonrpc_mucJoin(self, room_jid, nick): - """Join a Multi-User Chat room - - @param room_jid (unicode): room JID or empty string to generate a unique name - @param nick (unicode): user nick - """ - profile = session_iface.ISATSession(self.session).profile - d = self.asyncBridgeCall("joinMUC", room_jid, nick, {}, profile) - return d - - def jsonrpc_inviteMUC(self, contact_jid, room_jid): - """Invite a user to a Multi-User Chat room - - @param contact_jid (unicode): contact to invite - @param room_jid (unicode): room JID or empty string to generate a unique name - """ - profile = session_iface.ISATSession(self.session).profile - room_id = room_jid.split("@")[0] - service = room_jid.split("@")[1] - return self.sat_host.bridgeCall( - "inviteMUC", contact_jid, service, room_id, {}, profile - ) - - def jsonrpc_mucLeave(self, room_jid): - """Quit a Multi-User Chat room""" - profile = session_iface.ISATSession(self.session).profile - try: - room_jid = jid.JID(room_jid) - except: - log.warning("Invalid room jid") - return - return self.sat_host.bridgeCall("mucLeave", room_jid.userhost(), profile) - - def jsonrpc_mucGetRoomsJoined(self): - """Return list of room already joined by user""" - profile = session_iface.ISATSession(self.session).profile - return self.sat_host.bridgeCall("mucGetRoomsJoined", profile) - - def jsonrpc_mucGetDefaultService(self): - """@return: the default MUC""" - d = self.asyncBridgeCall("mucGetDefaultService") - return d - - def jsonrpc_launchTarotGame(self, other_players, room_jid=""): - """Create a room, invite the other players and start a Tarot game. - - @param other_players (list[unicode]): JIDs of the players to play with - @param room_jid (unicode): room JID or empty string to generate a unique name - """ - profile = session_iface.ISATSession(self.session).profile - return self.sat_host.bridgeCall( - "tarotGameLaunch", other_players, room_jid, profile - ) - - def jsonrpc_getTarotCardsPaths(self): - """Give the path of all the tarot cards""" - _join = os.path.join - _media_dir = _join(self.sat_host.media_dir, "") - return map( - lambda x: _join(C.MEDIA_DIR, x[len(_media_dir) :]), - glob.glob(_join(_media_dir, C.CARDS_DIR, "*_*.png")), - ) - - def jsonrpc_tarotGameReady(self, player, referee): - """Tell to the server that we are ready to start the game""" - profile = session_iface.ISATSession(self.session).profile - return self.sat_host.bridgeCall("tarotGameReady", player, referee, profile) - - def jsonrpc_tarotGamePlayCards(self, player_nick, referee, cards): - """Tell to the server the cards we want to put on the table""" - profile = session_iface.ISATSession(self.session).profile - return self.sat_host.bridgeCall( - "tarotGamePlayCards", player_nick, referee, cards, profile - ) - - def jsonrpc_launchRadioCollective(self, invited, room_jid=""): - """Create a room, invite people, and start a radio collective. - - @param invited (list[unicode]): JIDs of the contacts to play with - @param room_jid (unicode): room JID or empty string to generate a unique name - """ - profile = session_iface.ISATSession(self.session).profile - return self.sat_host.bridgeCall("radiocolLaunch", invited, room_jid, profile) - - def jsonrpc_getEntitiesData(self, jids, keys): - """Get cached data for several entities at once - - @param jids: list jids from who we wants data, or empty list for all jids in cache - @param keys: name of data we want (list) - @return: requested data""" - if not C.ALLOWED_ENTITY_DATA.issuperset(keys): - raise exceptions.PermissionError( - "Trying to access unallowed data (hack attempt ?)" - ) - profile = session_iface.ISATSession(self.session).profile - try: - return self.sat_host.bridgeCall("getEntitiesData", jids, keys, profile) - except Exception as e: - raise failure.Failure(jsonrpclib.Fault(C.ERRNUM_BRIDGE_ERRBACK, unicode(e))) - - def jsonrpc_getEntityData(self, jid, keys): - """Get cached data for an entity - - @param jid: jid of contact from who we want data - @param keys: name of data we want (list) - @return: requested data""" - if not C.ALLOWED_ENTITY_DATA.issuperset(keys): - raise exceptions.PermissionError( - "Trying to access unallowed data (hack attempt ?)" - ) - profile = session_iface.ISATSession(self.session).profile - try: - return self.sat_host.bridgeCall("getEntityData", jid, keys, profile) - except Exception as e: - raise failure.Failure(jsonrpclib.Fault(C.ERRNUM_BRIDGE_ERRBACK, unicode(e))) - - def jsonrpc_getCard(self, jid_): - """Get VCard for entiry - @param jid_: jid of contact from who we want data - @return: id to retrieve the profile""" - profile = session_iface.ISATSession(self.session).profile - return self.sat_host.bridgeCall("getCard", jid_, profile) - - @defer.inlineCallbacks - def jsonrpc_avatarGet(self, entity, cache_only, hash_only): - session_data = session_iface.ISATSession(self.session) - profile = session_data.profile - # profile_uuid = session_data.uuid - avatar = yield self.asyncBridgeCall( - "avatarGet", entity, cache_only, hash_only, profile - ) - if hash_only: - defer.returnValue(avatar) - else: - filename = os.path.basename(avatar) - avatar_url = os.path.join(session_data.cache_dir, filename) - defer.returnValue(avatar_url) - - def jsonrpc_getAccountDialogUI(self): - """Get the dialog for managing user account - @return: XML string of the XMLUI""" - profile = session_iface.ISATSession(self.session).profile - return self.sat_host.bridgeCall("getAccountDialogUI", profile) - - def jsonrpc_getParamsUI(self): - """Return the parameters XML for profile""" - profile = session_iface.ISATSession(self.session).profile - return self.asyncBridgeCall("getParamsUI", C.SECURITY_LIMIT, C.APP_NAME, profile) - - def jsonrpc_asyncGetParamA(self, param, category, attribute="value"): - """Return the parameter value for profile""" - profile = session_iface.ISATSession(self.session).profile - if category == "Connection": - # we need to manage the followings params here, else SECURITY_LIMIT would block them - if param == "JabberID": - return self.asyncBridgeCall( - "asyncGetParamA", param, category, attribute, profile_key=profile - ) - elif param == "autoconnect": - return defer.succeed(C.BOOL_TRUE) - d = self.asyncBridgeCall( - "asyncGetParamA", - param, - category, - attribute, - C.SECURITY_LIMIT, - profile_key=profile, - ) - return d - - def jsonrpc_setParam(self, name, value, category): - profile = session_iface.ISATSession(self.session).profile - return self.sat_host.bridgeCall( - "setParam", name, value, category, C.SECURITY_LIMIT, profile - ) - - def jsonrpc_launchAction(self, callback_id, data): - # FIXME: any action can be launched, this can be a huge security issue if callback_id can be guessed - # a security system with authorised callback_id must be implemented, similar to the one for authorised params - profile = session_iface.ISATSession(self.session).profile - d = self.asyncBridgeCall("launchAction", callback_id, data, profile) - return d - - def jsonrpc_chatStateComposing(self, to_jid_s): - """Call the method to process a "composing" state. - @param to_jid_s: contact the user is composing to - """ - profile = session_iface.ISATSession(self.session).profile - return self.sat_host.bridgeCall("chatStateComposing", to_jid_s, profile) - - def jsonrpc_getNewAccountDomain(self): - """@return: the domain for new account creation""" - d = self.asyncBridgeCall("getNewAccountDomain") - return d - - def jsonrpc_syntaxConvert( - self, text, syntax_from=C.SYNTAX_XHTML, syntax_to=C.SYNTAX_CURRENT - ): - """ Convert a text between two syntaxes - @param text: text to convert - @param syntax_from: source syntax (e.g. "markdown") - @param syntax_to: dest syntax (e.g.: "XHTML") - @param safe: clean resulting XHTML to avoid malicious code if True (forced here) - @return: converted text """ - profile = session_iface.ISATSession(self.session).profile - return self.sat_host.bridgeCall( - "syntaxConvert", text, syntax_from, syntax_to, True, profile - ) - - def jsonrpc_getLastResource(self, jid_s): - """Get the last active resource of that contact.""" - profile = session_iface.ISATSession(self.session).profile - return self.sat_host.bridgeCall("getLastResource", jid_s, profile) - - def jsonrpc_getFeatures(self): - """Return the available features in the backend for profile""" - profile = session_iface.ISATSession(self.session).profile - return self.sat_host.bridgeCall("getFeatures", profile) - - def jsonrpc_skipOTR(self): - """Tell the backend to leave OTR handling to Libervia.""" - profile = session_iface.ISATSession(self.session).profile - return self.sat_host.bridgeCall("skipOTR", profile) - - def jsonrpc_namespacesGet(self): - return self.sat_host.bridgeCall("namespacesGet") - - -class WaitingRequests(dict): - def setRequest(self, request, profile, register_with_ext_jid=False): - """Add the given profile to the waiting list. - - @param request (server.Request): the connection request - @param profile (str): %(doc_profile)s - @param register_with_ext_jid (bool): True if we will try to register the profile with an external XMPP account credentials - """ - dc = reactor.callLater(BRIDGE_TIMEOUT, self.purgeRequest, profile) - self[profile] = (request, dc, register_with_ext_jid) - - def purgeRequest(self, profile): - """Remove the given profile from the waiting list. - - @param profile (str): %(doc_profile)s - """ - try: - dc = self[profile][1] - except KeyError: - return - if dc.active(): - dc.cancel() - del self[profile] - - def getRequest(self, profile): - """Get the waiting request for the given profile. - - @param profile (str): %(doc_profile)s - @return: the waiting request or None - """ - return self[profile][0] if profile in self else None - - def getRegisterWithExtJid(self, profile): - """Get the value of the register_with_ext_jid parameter. - - @param profile (str): %(doc_profile)s - @return: bool or None - """ - return self[profile][2] if profile in self else None - - -class Register(JSONRPCMethodManager): - """This class manage the registration procedure with SàT - It provide an api for the browser, check password and setup the web server""" - - def __init__(self, sat_host): - JSONRPCMethodManager.__init__(self, sat_host) - self.profiles_waiting = {} - self.request = None - - def render(self, request): - """ - Render method with some hacks: - - if login is requested, try to login with form data - - except login, every method is jsonrpc - - user doesn't need to be authentified for explicitely listed methods, but must be for all others - """ - if request.postpath == ["login"]: - return self.loginOrRegister(request) - _session = request.getSession() - parsed = jsonrpclib.loads(request.content.read()) - method = parsed.get("method") # pylint: disable=E1103 - if method not in ["getSessionMetadata", "registerParams", "menusGet"]: - # if we don't call these methods, we need to be identified - profile = session_iface.ISATSession(_session).profile - if not profile: - # user is not identified, we return a jsonrpc fault - fault = jsonrpclib.Fault( - C.ERRNUM_LIBERVIA, C.NOT_ALLOWED - ) # FIXME: define some standard error codes for libervia - return jsonrpc.JSONRPC._cbRender( - self, fault, request, parsed.get("id"), parsed.get("jsonrpc") - ) # pylint: disable=E1103 - self.request = request - return jsonrpc.JSONRPC.render(self, request) - - def loginOrRegister(self, request): - """This method is called with the POST information from the registering form. - - @param request: request of the register form - @return: a constant indicating the state: - - C.BAD_REQUEST: something is wrong in the request (bad arguments) - - a return value from self._loginAccount or self._registerNewAccount - """ - try: - submit_type = request.args["submit_type"][0] - except KeyError: - return C.BAD_REQUEST - - if submit_type == "register": - self._registerNewAccount(request) - return server.NOT_DONE_YET - elif submit_type == "login": - self._loginAccount(request) - return server.NOT_DONE_YET - return Exception("Unknown submit type") - - @defer.inlineCallbacks - def _registerNewAccount(self, request): - try: - login = request.args["register_login"][0] - password = request.args["register_password"][0] - email = request.args["email"][0] - except KeyError: - request.write(C.BAD_REQUEST) - request.finish() - return - status = yield self.sat_host.registerNewAccount(request, login, password, email) - request.write(status) - request.finish() - - @defer.inlineCallbacks - def _loginAccount(self, request): - """Try to authenticate the user with the request information. - - will write to request a constant indicating the state: - - C.PROFILE_LOGGED: profile is connected - - C.PROFILE_LOGGED_EXT_JID: profile is connected and an external jid has been used - - C.SESSION_ACTIVE: session was already active - - C.BAD_REQUEST: something is wrong in the request (bad arguments) - - C.PROFILE_AUTH_ERROR: either the profile (login) or the profile password is wrong - - C.XMPP_AUTH_ERROR: the profile is authenticated but the XMPP password is wrong - - C.ALREADY_WAITING: a request has already been submitted for this profil, C.PROFILE_LOGGED_EXT_JID)e - - C.NOT_CONNECTED: connection has not been established - the request will then be finished - @param request: request of the register form - """ - try: - login = request.args["login"][0] - password = request.args["login_password"][0] - except KeyError: - request.write(C.BAD_REQUEST) - request.finish() - return - - assert login - - try: - status = yield self.sat_host.connect(request, login, password) - except ( - exceptions.DataError, - exceptions.ProfileUnknownError, - exceptions.PermissionError, - ): - request.write(C.PROFILE_AUTH_ERROR) - request.finish() - return - except exceptions.NotReady: - request.write(C.ALREADY_WAITING) - request.finish() - return - except exceptions.TimeOutError: - request.write(C.NO_REPLY) - request.finish() - return - except exceptions.InternalError as e: - request.write(e.message) - request.finish() - return - except exceptions.ConflictError: - request.write(C.SESSION_ACTIVE) - request.finish() - return - except ValueError as e: - if e.message in (C.PROFILE_AUTH_ERROR, C.XMPP_AUTH_ERROR): - request.write(e.message) - request.finish() - return - else: - raise e - - assert status - request.write(status) - request.finish() - - def jsonrpc_isConnected(self): - _session = self.request.getSession() - profile = session_iface.ISATSession(_session).profile - return self.sat_host.bridgeCall("isConnected", profile) - - def jsonrpc_connect(self): - _session = self.request.getSession() - profile = session_iface.ISATSession(_session).profile - if self.waiting_profiles.getRequest(profile): - raise jsonrpclib.Fault( - 1, C.ALREADY_WAITING - ) # FIXME: define some standard error codes for libervia - self.waiting_profiles.setRequest(self.request, profile) - self.sat_host.bridgeCall("connect", profile) - return server.NOT_DONE_YET - - def jsonrpc_getSessionMetadata(self): - """Return metadata useful on session start - - @return (dict): metadata which can have the following keys: - "plugged" (bool): True if a profile is already plugged - "warning" (unicode): a security warning message if plugged is False and if it make sense - this key may not be present - "allow_registration" (bool): True if registration is allowed - this key is only present if profile is unplugged - @return: a couple (registered, message) with: - - registered: - - message: - """ - metadata = {} - _session = self.request.getSession() - profile = session_iface.ISATSession(_session).profile - if profile: - metadata["plugged"] = True - else: - metadata["plugged"] = False - metadata["warning"] = self._getSecurityWarning() - metadata["allow_registration"] = self.sat_host.options["allow_registration"] - return metadata - - def jsonrpc_registerParams(self): - """Register the frontend specific parameters""" - # params = """<params><individual>...</category></individual>""" - # self.sat_host.bridge.paramsRegisterApp(params, C.SECURITY_LIMIT, C.APP_NAME) - - def jsonrpc_menusGet(self): - """Return the parameters XML for profile""" - # XXX: we put this method in Register because we get menus before being logged - return self.sat_host.bridgeCall("menusGet", "", C.SECURITY_LIMIT) - - def _getSecurityWarning(self): - """@return: a security warning message, or None if the connection is secure""" - if ( - self.request.URLPath().scheme == "https" - or not self.sat_host.options["security_warning"] - ): - return None - text = ( - "<p>" - + D_("You are about to connect to an unsecure service.") - + "</p><p> </p><p>" - ) - - if self.sat_host.options["connection_type"] == "both": - new_port = ( - (":%s" % self.sat_host.options["port_https_ext"]) - if self.sat_host.options["port_https_ext"] != HTTPS_PORT - else "" - ) - url = "https://%s" % self.request.URLPath().netloc.replace( - ":%s" % self.sat_host.options["port"], new_port - ) - text += D_( - "Please read our %(faq_prefix)ssecurity notice%(faq_suffix)s regarding HTTPS" - ) % { - "faq_prefix": '<a href="http://salut-a-toi.org/faq.html#https" target="#">', - "faq_suffix": "</a>", - } - text += "</p><p>" + D_("and use the secure version of this website:") - text += '</p><p> </p><p align="center"><a href="%(url)s">%(url)s</a>' % { - "url": url - } - else: - text += D_("You should ask your administrator to turn on HTTPS.") - - return text + "</p><p> </p>" - - -class SignalHandler(jsonrpc.JSONRPC): - def __init__(self, sat_host): - web_resource.Resource.__init__(self) - self.register = None - self.sat_host = sat_host - self._last_service_prof_disconnect = time.time() - self.signalDeferred = {} # dict of deferred (key: profile, value: Deferred) - # which manages the long polling HTTP request with signals - self.queue = {} - - def plugRegister(self, register): - self.register = register - - def jsonrpc_getSignals(self): - """Keep the connection alive until a signal is received, then send it - @return: (signal, *signal_args)""" - _session = self.request.getSession() - profile = session_iface.ISATSession(_session).profile - if profile in self.queue: # if we have signals to send in queue - if self.queue[profile]: - return self.queue[profile].pop(0) - else: - # the queue is empty, we delete the profile from queue - del self.queue[profile] - _session.lock() # we don't want the session to expire as long as this connection is active - - def unlock(signal, profile): - _session.unlock() - try: - source_defer = self.signalDeferred[profile] - if source_defer.called and source_defer.result[0] == "disconnected": - log.info(u"[%s] disconnected" % (profile,)) - try: - _session.expire() - except KeyError: - # FIXME: happen if session is ended using login page - # when pyjamas page is also launched - log.warning(u"session is already expired") - except IndexError: - log.error("Deferred result should be a tuple with fonction name first") - - self.signalDeferred[profile] = defer.Deferred() - self.request.notifyFinish().addBoth(unlock, profile) - return self.signalDeferred[profile] - - def getGenericCb(self, function_name): - """Return a generic function which send all params to signalDeferred.callback - function must have profile as last argument""" - - def genericCb(*args): - profile = args[-1] - if not profile in self.sat_host.prof_connected: - return - signal_data = (function_name, args[:-1]) - try: - signal_callback = self.signalDeferred[profile].callback - except KeyError: - self.queue.setdefault(profile, []).append(signal_data) - else: - signal_callback(signal_data) - del self.signalDeferred[profile] - - return genericCb - - def actionNewHandler(self, action_data, action_id, security_limit, profile): - """actionNew handler - - XXX: We need need a dedicated handler has actionNew use a security_limit which must be managed - @param action_data(dict): see bridge documentation - @param action_id(unicode): identitifer of the action - @param security_limit(int): %(doc_security_limit)s - @param profile(unicode): %(doc_profile)s - """ - if not profile in self.sat_host.prof_connected: - return - # FIXME: manage security limit in a dedicated method - # raise an exception if it's not OK - # and read value in sat.conf - if security_limit >= C.SECURITY_LIMIT: - log.debug( - u"Ignoring action {action_id}, blocked by security limit".format( - action_id=action_id - ) - ) - return - signal_data = ("actionNew", (action_data, action_id, security_limit)) - try: - signal_callback = self.signalDeferred[profile].callback - except KeyError: - self.queue.setdefault(profile, []).append(signal_data) - else: - signal_callback(signal_data) - del self.signalDeferred[profile] - - def connected(self, profile, jid_s): - """Connection is done. - - @param profile (unicode): %(doc_profile)s - @param jid_s (unicode): the JID that we were assigned by the server, as the resource might differ from the JID we asked for. - """ - # FIXME: _logged should not be called from here, check this code - # FIXME: check if needed to connect with external jid - # jid_s is handled in QuickApp.connectionHandler already - # assert self.register # register must be plugged - # request = self.sat_host.waiting_profiles.getRequest(profile) - # if request: - # self.sat_host._logged(profile, request) - - def disconnected(self, profile): - if profile == C.SERVICE_PROFILE: - # if service profile has been disconnected, we try to reconnect it - # if we can't we show error message - # and if we have 2 disconnection in a short time, we don't try to reconnect - # and display an error message - disconnect_delta = time.time() - self._last_service_prof_disconnect - if disconnect_delta < 15: - log.error( - _( - u"Service profile disconnected twice in a short time, please check connection" - ) - ) - else: - log.info( - _( - u"Service profile has been disconnected, but we need it! Reconnecting it..." - ) - ) - d = self.sat_host.bridgeCall( - "connect", profile, self.sat_host.options["passphrase"], {} - ) - d.addErrback( - lambda failure_: log.error( - _( - u"Can't reconnect service profile, please check connection: {reason}" - ).format(reason=failure_) - ) - ) - self._last_service_prof_disconnect = time.time() - return - - if not profile in self.sat_host.prof_connected: - log.info( - _( - u"'disconnected' signal received for a not connected profile ({profile})" - ).format(profile=profile) - ) - return - self.sat_host.prof_connected.remove(profile) - if profile in self.signalDeferred: - self.signalDeferred[profile].callback(("disconnected",)) - del self.signalDeferred[profile] - else: - if profile not in self.queue: - self.queue[profile] = [] - self.queue[profile].append(("disconnected",)) - - def render(self, request): - """ - Render method wich reject access if user is not identified - """ - _session = request.getSession() - parsed = jsonrpclib.loads(request.content.read()) - profile = session_iface.ISATSession(_session).profile - if not profile: - # user is not identified, we return a jsonrpc fault - fault = jsonrpclib.Fault( - C.ERRNUM_LIBERVIA, C.NOT_ALLOWED - ) # FIXME: define some standard error codes for libervia - return jsonrpc.JSONRPC._cbRender( - self, fault, request, parsed.get("id"), parsed.get("jsonrpc") - ) # pylint: disable=E1103 - self.request = request - return jsonrpc.JSONRPC.render(self, request) - - -class UploadManager(web_resource.Resource): - """This class manage the upload of a file - It redirect the stream to SàT core backend""" - - isLeaf = True - NAME = "path" # name use by the FileUpload - - def __init__(self, sat_host): - self.sat_host = sat_host - self.upload_dir = tempfile.mkdtemp() - self.sat_host.addCleanup(shutil.rmtree, self.upload_dir) - - def getTmpDir(self): - return self.upload_dir - - def _getFileName(self, request): - """Generate unique filename for a file""" - raise NotImplementedError - - def _fileWritten(self, request, filepath): - """Called once the file is actually written on disk - @param request: HTTP request object - @param filepath: full filepath on the server - @return: a tuple with the name of the async bridge method - to be called followed by its arguments. - """ - raise NotImplementedError - - def render(self, request): - """ - Render method with some hacks: - - if login is requested, try to login with form data - - except login, every method is jsonrpc - - user doesn't need to be authentified for getSessionMetadata, but must be for all other methods - """ - filename = self._getFileName(request) - filepath = os.path.join(self.upload_dir, filename) - # FIXME: the uploaded file is fully loaded in memory at form parsing time so far - # (see twisted.web.http.Request.requestReceived). A custom requestReceived should - # be written in the futur. In addition, it is not yet possible to get progression informations - # (see http://twistedmatrix.com/trac/ticket/288) - - with open(filepath, "w") as f: - f.write(request.args[self.NAME][0]) - - def finish(d): - error = isinstance(d, Exception) or isinstance(d, failure.Failure) - request.write(C.UPLOAD_KO if error else C.UPLOAD_OK) - # TODO: would be great to re-use the original Exception class and message - # but it is lost in the middle of the backtrace and encapsulated within - # a DBusException instance --> extract the data from the backtrace? - request.finish() - - d = JSONRPCMethodManager(self.sat_host).asyncBridgeCall( - *self._fileWritten(request, filepath) - ) - d.addCallbacks(lambda d: finish(d), lambda failure: finish(failure)) - return server.NOT_DONE_YET - - -class UploadManagerRadioCol(UploadManager): - NAME = "song" - - def _getFileName(self, request): - extension = os.path.splitext(request.args["filename"][0])[1] - return "%s%s" % ( - str(uuid.uuid4()), - extension, - ) # XXX: chromium doesn't seem to play song without the .ogg extension, even with audio/ogg mime-type - - def _fileWritten(self, request, filepath): - """Called once the file is actually written on disk - @param request: HTTP request object - @param filepath: full filepath on the server - @return: a tuple with the name of the async bridge method - to be called followed by its arguments. - """ - profile = session_iface.ISATSession(request.getSession()).profile - return ("radiocolSongAdded", request.args["referee"][0], filepath, profile) - - -class UploadManagerAvatar(UploadManager): - NAME = "avatar_path" - - def _getFileName(self, request): - return str(uuid.uuid4()) - - def _fileWritten(self, request, filepath): - """Called once the file is actually written on disk - @param request: HTTP request object - @param filepath: full filepath on the server - @return: a tuple with the name of the async bridge method - to be called followed by its arguments. - """ - profile = session_iface.ISATSession(request.getSession()).profile - return ("setAvatar", filepath, profile) - - -class Libervia(service.Service): - debug = defer.Deferred.debug # True if twistd/Libervia is launched in debug mode - - def __init__(self, options): - self.options = options - self.initialised = defer.Deferred() - self.waiting_profiles = WaitingRequests() # FIXME: should be removed - - if self.options["base_url_ext"]: - self.base_url_ext = self.options.pop("base_url_ext") - if self.base_url_ext[-1] != "/": - self.base_url_ext += "/" - self.base_url_ext_data = urlparse.urlsplit(self.base_url_ext) - else: - self.base_url_ext = None - # we split empty string anyway so we can do things like - # scheme = self.base_url_ext_data.scheme or 'https' - self.base_url_ext_data = urlparse.urlsplit("") - - if not self.options["port_https_ext"]: - self.options["port_https_ext"] = self.options["port_https"] - if self.options["data_dir"] == DATA_DIR_DEFAULT: - coerceDataDir( - self.options["data_dir"] - ) # this is not done when using the default value - - self.html_dir = os.path.join(self.options["data_dir"], C.HTML_DIR) - self.themes_dir = os.path.join(self.options["data_dir"], C.THEMES_DIR) - - self._cleanup = [] - - self.signal_handler = SignalHandler(self) - self.sessions = {} # key = session value = user - self.prof_connected = set() # Profiles connected - self.ns_map = {} # map of short name to namespaces - - ## bridge ## - try: - self.bridge = Bridge() - except BridgeExceptionNoService: - print(u"Can't connect to SàT backend, are you sure it's launched ?") - sys.exit(1) - self.bridge.bridgeConnect(callback=self._bridgeCb, errback=self._bridgeEb) - - def _namespacesGetCb(self, ns_map): - self.ns_map = ns_map - - def _namespacesGetEb(self, failure_): - log.error(_(u"Can't get namespaces map: {msg}").format(msg=failure_)) - - def backendReady(self, dummy): - self.root = root = LiberviaRootResource(self.html_dir) - _register = Register(self) - _upload_radiocol = UploadManagerRadioCol(self) - _upload_avatar = UploadManagerAvatar(self) - d = self.bridgeCall("namespacesGet") - d.addCallback(self._namespacesGetCb) - d.addErrback(self._namespacesGetEb) - self.signal_handler.plugRegister(_register) - self.bridge.register_signal("connected", self.signal_handler.connected) - self.bridge.register_signal("disconnected", self.signal_handler.disconnected) - # core - for signal_name in [ - "presenceUpdate", - "messageNew", - "subscribe", - "contactDeleted", - "newContact", - "entityDataUpdated", - "paramUpdate", - ]: - self.bridge.register_signal( - signal_name, self.signal_handler.getGenericCb(signal_name) - ) - # XXX: actionNew is handled separately because the handler must manage security_limit - self.bridge.register_signal("actionNew", self.signal_handler.actionNewHandler) - # plugins - for signal_name in [ - "psEvent", - "mucRoomJoined", - "tarotGameStarted", - "tarotGameNew", - "tarotGameChooseContrat", - "tarotGameShowCards", - "tarotGameInvalidCards", - "tarotGameCardsPlayed", - "tarotGameYourTurn", - "tarotGameScore", - "tarotGamePlayers", - "radiocolStarted", - "radiocolPreload", - "radiocolPlay", - "radiocolNoUpload", - "radiocolUploadOk", - "radiocolSongRejected", - "radiocolPlayers", - "mucRoomLeft", - "mucRoomUserChangedNick", - "chatStateReceived", - ]: - self.bridge.register_signal( - signal_name, self.signal_handler.getGenericCb(signal_name), "plugin" - ) - self.media_dir = self.bridge.getConfig("", "media_dir") - self.local_dir = self.bridge.getConfig("", "local_dir") - self.cache_root_dir = os.path.join(self.local_dir, C.CACHE_DIR) - - # JSON APIs - self.putChild("json_signal_api", self.signal_handler) - self.putChild("json_api", MethodHandler(self)) - self.putChild("register_api", _register) - - # files upload - self.putChild("upload_radiocol", _upload_radiocol) - self.putChild("upload_avatar", _upload_avatar) - - # static pages - self.putChild("blog_legacy", MicroBlog(self)) - self.putChild(C.THEMES_URL, ProtectedFile(self.themes_dir)) - - # websocket - if self.options["connection_type"] in ("https", "both"): - wss = websockets.LiberviaPageWSProtocol.getResource(self, secure=True) - self.putChild("wss", wss) - if self.options["connection_type"] in ("http", "both"): - ws = websockets.LiberviaPageWSProtocol.getResource(self, secure=False) - self.putChild("ws", ws) - - # Libervia pages - LiberviaPage.importPages(self) - LiberviaPage.setMenu(self.options["menu_json"]) - ## following signal is needed for cache handling in Libervia pages - self.bridge.register_signal( - "psEventRaw", partial(LiberviaPage.onNodeEvent, self), "plugin" - ) - self.bridge.register_signal( - "messageNew", partial(LiberviaPage.onSignal, self, "messageNew") - ) - - # Progress handling - self.bridge.register_signal( - "progressStarted", partial(ProgressHandler._signal, "started") - ) - self.bridge.register_signal( - "progressFinished", partial(ProgressHandler._signal, "finished") - ) - self.bridge.register_signal( - "progressError", partial(ProgressHandler._signal, "error") - ) - - # media dirs - # FIXME: get rid of dirname and "/" in C.XXX_DIR - self.putChild(os.path.dirname(C.MEDIA_DIR), ProtectedFile(self.media_dir)) - self.cache_resource = web_resource.NoResource() - self.putChild(C.CACHE_DIR, self.cache_resource) - - # special - self.putChild( - "radiocol", - ProtectedFile(_upload_radiocol.getTmpDir(), defaultType="audio/ogg"), - ) # FIXME: We cheat for PoC because we know we are on the same host, so we use directly upload dir - # pyjamas tests, redirected only for dev versions - if self.version[-1] == "D": - self.putChild("test", web_util.Redirect("/libervia_test.html")) - - # redirections - root._initRedirections(self.options) - - server.Request.defaultContentType = "text/html; charset=utf-8" - wrapped = web_resource.EncodingResourceWrapper( - root, [server.GzipEncoderFactory()] - ) - self.site = server.Site(wrapped) - self.site.sessionFactory = LiberviaSession - self.renderer = template.Renderer(self) - self.putChild("templates", ProtectedFile(self.renderer.base_dir)) - - def initEb(self, failure): - log.error(_(u"Init error: {msg}").format(msg=failure)) - reactor.stop() - return failure - - def _bridgeCb(self): - self.bridge.getReady( - lambda: self.initialised.callback(None), - lambda failure: self.initialised.errback(Exception(failure)), - ) - self.initialised.addCallback(self.backendReady) - self.initialised.addErrback(self.initEb) - - def _bridgeEb(self, failure): - log.error(u"Can't connect to bridge: {}".format(failure)) - - @property - def version(self): - """Return the short version of Libervia""" - return C.APP_VERSION - - @property - def full_version(self): - """Return the full version of Libervia (with extra data when in development mode)""" - version = self.version - if version[-1] == "D": - # we are in debug version, we add extra data - try: - return self._version_cache - except AttributeError: - self._version_cache = u"{} ({})".format( - version, utils.getRepositoryData(libervia) - ) - return self._version_cache - else: - return version - - def bridgeCall(self, method_name, *args, **kwargs): - """Call an asynchronous bridge method and return a deferred - - @param method_name: name of the method as a unicode - @return: a deferred which trigger the result - - """ - d = defer.Deferred() - - def _callback(*args): - if not args: - d.callback(None) - else: - if len(args) != 1: - Exception("Multiple return arguments not supported") - d.callback(args[0]) - - def _errback(result): - d.errback( - failure.Failure( - jsonrpclib.Fault(C.ERRNUM_BRIDGE_ERRBACK, result.classname) - ) - ) - - kwargs["callback"] = _callback - kwargs["errback"] = _errback - getattr(self.bridge, method_name)(*args, **kwargs) - return d - - @defer.inlineCallbacks - def _logged(self, profile, request): - """Set everything when a user just logged in - - @param profile - @param request - @return: a constant indicating the state: - - C.PROFILE_LOGGED - - C.PROFILE_LOGGED_EXT_JID - @raise exceptions.ConflictError: session is already active - """ - register_with_ext_jid = self.waiting_profiles.getRegisterWithExtJid(profile) - self.waiting_profiles.purgeRequest(profile) - session = request.getSession() - sat_session = session_iface.ISATSession(session) - if sat_session.profile: - log.error(_(u"/!\\ Session has already a profile, this should NEVER happen!")) - raise failure.Failure(exceptions.ConflictError("Already active")) - - sat_session.profile = profile - self.prof_connected.add(profile) - cache_dir = os.path.join( - self.cache_root_dir, u"profiles", regex.pathEscape(profile) - ) - # FIXME: would be better to have a global /cache URL which redirect to profile's cache directory, without uuid - self.cache_resource.putChild(sat_session.uuid, ProtectedFile(cache_dir)) - log.debug( - _(u"profile cache resource added from {uuid} to {path}").format( - uuid=sat_session.uuid, path=cache_dir - ) - ) - - def onExpire(): - log.info(u"Session expired (profile={profile})".format(profile=profile)) - self.cache_resource.delEntity(sat_session.uuid) - log.debug( - _(u"profile cache resource {uuid} deleted").format(uuid=sat_session.uuid) - ) - try: - # We purge the queue - del self.signal_handler.queue[profile] - except KeyError: - pass - # and now we disconnect the profile - self.bridgeCall("disconnect", profile) - - session.notifyOnExpire(onExpire) - - # FIXME: those session infos should be returned by connect or isConnected - infos = yield self.bridgeCall("sessionInfosGet", profile) - sat_session.jid = jid.JID(infos["jid"]) - sat_session.backend_started = int(infos["started"]) - - state = C.PROFILE_LOGGED_EXT_JID if register_with_ext_jid else C.PROFILE_LOGGED - defer.returnValue(state) - - @defer.inlineCallbacks - def connect(self, request, login, password): - """log user in - - If an other user was already logged, it will be unlogged first - @param request(server.Request): request linked to the session - @param login(unicode): user login - can be profile name - can be profile@[libervia_domain.ext] - can be a jid (a new profile will be created with this jid if needed) - @param password(unicode): user password - @return (unicode, None): C.SESSION_ACTIVE: if session was aleady active else self._logged value - @raise exceptions.DataError: invalid login - @raise exceptions.ProfileUnknownError: this login doesn't exist - @raise exceptions.PermissionError: a login is not accepted (e.g. empty password not allowed) - @raise exceptions.NotReady: a profile connection is already waiting - @raise exceptions.TimeoutError: didn't received and answer from Bridge - @raise exceptions.InternalError: unknown error - @raise ValueError(C.PROFILE_AUTH_ERROR): invalid login and/or password - @raise ValueError(C.XMPP_AUTH_ERROR): invalid XMPP account password - """ - - # XXX: all security checks must be done here, even if present in javascript - if login.startswith("@"): - raise failure.Failure(exceptions.DataError("No profile_key allowed")) - - if login.startswith("guest@@") and login.count("@") == 2: - log.debug("logging a guest account") - elif "@" in login: - if login.count("@") != 1: - raise failure.Failure( - exceptions.DataError("Invalid login: {login}".format(login=login)) - ) - try: - login_jid = jid.JID(login) - except (RuntimeError, jid.InvalidFormat, AttributeError): - raise failure.Failure(exceptions.DataError("No profile_key allowed")) - - # FIXME: should it be cached? - new_account_domain = yield self.bridgeCall("getNewAccountDomain") - - if login_jid.host == new_account_domain: - # redirect "user@libervia.org" to the "user" profile - login = login_jid.user - login_jid = None - else: - login_jid = None - - try: - profile = yield self.bridgeCall("profileNameGet", login) - except Exception: # XXX: ProfileUnknownError wouldn't work, it's encapsulated - # FIXME: find a better way to handle bridge errors - if ( - login_jid is not None and login_jid.user - ): # try to create a new sat profile using the XMPP credentials - if not self.options["allow_registration"]: - log.warning( - u"Trying to register JID account while registration is not allowed" - ) - raise failure.Failure( - exceptions.DataError( - u"JID login while registration is not allowed" - ) - ) - profile = login # FIXME: what if there is a resource? - connect_method = "asyncConnectWithXMPPCredentials" - register_with_ext_jid = True - else: # non existing username - raise failure.Failure(exceptions.ProfileUnknownError()) - else: - if profile != login or ( - not password - and profile - not in self.options["empty_password_allowed_warning_dangerous_list"] - ): - # profiles with empty passwords are restricted to local frontends - raise exceptions.PermissionError - register_with_ext_jid = False - - connect_method = "connect" - - # we check if there is not already an active session - sat_session = session_iface.ISATSession(request.getSession()) - if sat_session.profile: - # yes, there is - if sat_session.profile != profile: - # it's a different profile, we need to disconnect it - log.warning( - _( - u"{new_profile} requested login, but {old_profile} was already connected, disconnecting {old_profile}" - ).format(old_profile=sat_session.profile, new_profile=profile) - ) - self.purgeSession(request) - - if self.waiting_profiles.getRequest(profile): - # FIXME: check if and when this can happen - raise failure.Failure(exceptions.NotReady("Already waiting")) - - self.waiting_profiles.setRequest(request, profile, register_with_ext_jid) - try: - connected = yield self.bridgeCall(connect_method, profile, password) - except Exception as failure_: - fault = failure_.faultString - self.waiting_profiles.purgeRequest(profile) - if fault in ("PasswordError", "ProfileUnknownError"): - log.info( - u"Profile {profile} doesn't exist or the submitted password is wrong".format( - profile=profile - ) - ) - raise failure.Failure(ValueError(C.PROFILE_AUTH_ERROR)) - elif fault == "SASLAuthError": - log.info( - u"The XMPP password of profile {profile} is wrong".format( - profile=profile - ) - ) - raise failure.Failure(ValueError(C.XMPP_AUTH_ERROR)) - elif fault == "NoReply": - log.info( - _( - "Did not receive a reply (the timeout expired or the connection is broken)" - ) - ) - raise exceptions.TimeOutError - else: - log.error( - u'Unmanaged fault string "{fault}" in errback for the connection of profile {profile}'.format( - fault=fault, profile=profile - ) - ) - raise failure.Failure(exceptions.InternalError(fault)) - - if connected: - # profile is already connected in backend - # do we have a corresponding session in Libervia? - sat_session = session_iface.ISATSession(request.getSession()) - if sat_session.profile: - # yes, session is active - if sat_session.profile != profile: - # existing session should have been ended above - # so this line should never be reached - log.error( - _( - u"session profile [{session_profile}] differs from login profile [{profile}], this should not happen!" - ).format(session_profile=sat_session.profile, profile=profile) - ) - raise exceptions.InternalError("profile mismatch") - defer.returnValue(C.SESSION_ACTIVE) - log.info( - _( - u"profile {profile} was already connected in backend".format( - profile=profile - ) - ) - ) - # no, we have to create it - - state = yield self._logged(profile, request) - defer.returnValue(state) - - def registerNewAccount(self, request, login, password, email): - """Create a new account, or return error - @param request(server.Request): request linked to the session - @param login(unicode): new account requested login - @param email(unicode): new account email - @param password(unicode): new account password - @return(unicode): a constant indicating the state: - - C.BAD_REQUEST: something is wrong in the request (bad arguments) - - C.INVALID_INPUT: one of the data is not valid - - C.REGISTRATION_SUCCEED: new account has been successfully registered - - C.ALREADY_EXISTS: the given profile already exists - - C.INTERNAL_ERROR or any unmanaged fault string - @raise PermissionError: registration is now allowed in server configuration - """ - if not self.options["allow_registration"]: - log.warning( - _(u"Registration received while it is not allowed, hack attempt?") - ) - raise failure.Failure( - exceptions.PermissionError(u"Registration is not allowed on this server") - ) - - if ( - not re.match(C.REG_LOGIN_RE, login) - or not re.match(C.REG_EMAIL_RE, email, re.IGNORECASE) - or len(password) < C.PASSWORD_MIN_LENGTH - ): - return C.INVALID_INPUT - - def registered(result): - return C.REGISTRATION_SUCCEED - - def registeringError(failure): - status = failure.value.faultString - if status == "ConflictError": - return C.ALREADY_EXISTS - elif status == "InternalError": - return C.INTERNAL_ERROR - else: - log.error( - _(u"Unknown registering error status: {status }").format( - status=status - ) - ) - return status - - d = self.bridgeCall("registerSatAccount", email, password, login) - d.addCallback(registered) - d.addErrback(registeringError) - return d - - def addCleanup(self, callback, *args, **kwargs): - """Add cleaning method to call when service is stopped - - cleaning method will be called in reverse order of they insertion - @param callback: callable to call on service stop - @param *args: list of arguments of the callback - @param **kwargs: list of keyword arguments of the callback""" - self._cleanup.insert(0, (callback, args, kwargs)) - - def startService(self): - """Connect the profile for Libervia and start the HTTP(S) server(s)""" - - def eb(e): - log.error(_(u"Connection failed: %s") % e) - self.stop() - - def initOk(dummy): - try: - connected = self.bridge.isConnected(C.SERVICE_PROFILE) - except Exception as e: - # we don't want the traceback - msg = [l for l in unicode(e).split("\n") if l][-1] - log.error( - u"Can't check service profile ({profile}), are you sure it exists ?\n{error}".format( - profile=C.SERVICE_PROFILE, error=msg - ) - ) - self.stop() - return - if not connected: - self.bridge.connect( - C.SERVICE_PROFILE, - self.options["passphrase"], - {}, - callback=self._startService, - errback=eb, - ) - else: - self._startService() - - self.initialised.addCallback(initOk) - - ## URLs ## - - def putChild(self, path, resource): - """Add a child to the root resource""" - # FIXME: check that no information is leaked (c.f. https://twistedmatrix.com/documents/current/web/howto/using-twistedweb.html#request-encoders) - self.root.putChild( - path, - web_resource.EncodingResourceWrapper(resource, [server.GzipEncoderFactory()]), - ) - - def getExtBaseURLData(self, request): - """Retrieve external base URL Data - - this method tried to retrieve the base URL found by external user - It does by checking in this order: - - base_url_ext option from configuration - - proxy x-forwarder-host headers - - URL of the request - @return (urlparse.SplitResult): SplitResult instance with only scheme and netloc filled - """ - ext_data = self.base_url_ext_data - url_path = request.URLPath() - if not ext_data.scheme or not ext_data.netloc: - # ext_data is not specified, we check headers - if request.requestHeaders.hasHeader("x-forwarded-host"): - # we are behing a proxy - # we fill proxy_scheme and proxy_netloc value - proxy_host = request.requestHeaders.getRawHeaders("x-forwarded-host")[0] - try: - proxy_server = request.requestHeaders.getRawHeaders( - "x-forwarded-server" - )[0] - except TypeError: - # no x-forwarded-server found, we use proxy_host - proxy_netloc = proxy_host - else: - # if the proxy host has a port, we use it with server name - proxy_port = urlparse.urlsplit(u"//{}".format(proxy_host)).port - proxy_netloc = ( - u"{}:{}".format(proxy_server, proxy_port) - if proxy_port is not None - else proxy_server - ) - proxy_netloc = proxy_netloc.decode("utf-8") - try: - proxy_scheme = request.requestHeaders.getRawHeaders( - "x-forwarded-proto" - )[0].decode("utf-8") - except TypeError: - proxy_scheme = None - else: - proxy_scheme, proxy_netloc = None, None - else: - proxy_scheme, proxy_netloc = None, None - - return urlparse.SplitResult( - ext_data.scheme or proxy_scheme or url_path.scheme.decode("utf-8"), - ext_data.netloc or proxy_netloc or url_path.netloc.decode("utf-8"), - ext_data.path or u"/", - "", - "", - ) - - def getExtBaseURL(self, request, path="", query="", fragment="", scheme=None): - """Get external URL according to given elements - - external URL is the URL seen by external user - @param path(unicode): same as for urlsplit.urlsplit - path will be prefixed to follow found external URL if suitable - @param params(unicode): same as for urlsplit.urlsplit - @param query(unicode): same as for urlsplit.urlsplit - @param fragment(unicode): same as for urlsplit.urlsplit - @param scheme(unicode, None): if not None, will override scheme from base URL - @return (unicode): external URL - """ - split_result = self.getExtBaseURLData(request) - return urlparse.urlunsplit( - ( - split_result.scheme.decode("utf-8") if scheme is None else scheme, - split_result.netloc.decode("utf-8"), - os.path.join(split_result.path, path), - query, - fragment, - ) - ) - - def checkRedirection(self, url): - """check is a part of the URL prefix is redirected then replace it - - @param url(unicode): url to check - @return (unicode): possibly redirected URL which should link to the same location - """ - inv_redirections = self.root.inv_redirections - url_parts = url.strip(u"/").split(u"/") - for idx in xrange(len(url), 0, -1): - test_url = u"/" + u"/".join(url_parts[:idx]) - if test_url in inv_redirections: - rem_url = url_parts[idx:] - return os.path.join( - u"/", u"/".join([inv_redirections[test_url]] + rem_url) - ) - return url - - ## Sessions ## - - def purgeSession(self, request): - """helper method to purge a session during request handling""" - session = request.session - if session is not None: - log.debug(_(u"session purge")) - session.expire() - # FIXME: not clean but it seems that it's the best way to reset - # session during request handling - request._secureSession = request._insecureSession = None - - def getSessionData(self, request, *args): - """helper method to retrieve session data - - @param request(server.Request): request linked to the session - @param *args(zope.interface.Interface): interface of the session to get - @return (iterator(data)): requested session data - """ - session = request.getSession() - if len(args) == 1: - return args[0](session) - else: - return (iface(session) for iface in args) - - @defer.inlineCallbacks - def getAffiliation(self, request, service, node): - """retrieve pubsub node affiliation for current user - - use cache first, and request pubsub service if not cache is found - @param request(server.Request): request linked to the session - @param service(jid.JID): pubsub service - @param node(unicode): pubsub node - @return (unicode): affiliation - """ - sat_session = self.getSessionData(request, session_iface.ISATSession) - if sat_session.profile is None: - raise exceptions.InternalError(u"profile must be set to use this method") - affiliation = sat_session.getAffiliation(service, node) - if affiliation is not None: - defer.returnValue(affiliation) - else: - try: - affiliations = yield self.bridgeCall( - "psAffiliationsGet", service.full(), node, sat_session.profile - ) - except Exception as e: - log.warning( - "Can't retrieve affiliation for {service}/{node}: {reason}".format( - service=service, node=node, reason=e - ) - ) - affiliation = u"" - else: - try: - affiliation = affiliations[node] - except KeyError: - affiliation = u"" - sat_session.setAffiliation(service, node, affiliation) - defer.returnValue(affiliation) - - ## Websocket (dynamic pages) ## - - def getWebsocketURL(self, request): - base_url_split = self.getExtBaseURLData(request) - if base_url_split.scheme.endswith("s"): - scheme = u"wss" - else: - scheme = u"ws" - - return self.getExtBaseURL(request, path=scheme, scheme=scheme) - - def registerWSToken(self, token, page, request): - websockets.LiberviaPageWSProtocol.registerToken(token, page, request) - - ## Various utils ## - - def getHTTPDate(self, timestamp=None): - now = time.gmtime(timestamp) - fmt_date = u"{day_name}, %d {month_name} %Y %H:%M:%S GMT".format( - day_name=C.HTTP_DAYS[now.tm_wday], month_name=C.HTTP_MONTH[now.tm_mon - 1] - ) - return time.strftime(fmt_date, now) - - ## TLS related methods ## - - def _TLSOptionsCheck(self): - """Check options coherence if TLS is activated, and update missing values - - Must be called only if TLS is activated - """ - if not self.options["tls_certificate"]: - log.error(u"a TLS certificate is needed to activate HTTPS connection") - self.quit(1) - if not self.options["tls_private_key"]: - self.options["tls_private_key"] = self.options["tls_certificate"] - - if not self.options["tls_private_key"]: - self.options["tls_private_key"] = self.options["tls_certificate"] - - def _loadCertificates(self, f): - """Read a .pem file with a list of certificates - - @param f (file): file obj (opened .pem file) - @return (list[OpenSSL.crypto.X509]): list of certificates - @raise OpenSSL.crypto.Error: error while parsing the file - """ - # XXX: didn't found any method to load a .pem file with several certificates - # so the certificates split is done here - certificates = [] - buf = [] - while True: - line = f.readline() - buf.append(line) - if "-----END CERTIFICATE-----" in line: - certificates.append( - OpenSSL.crypto.load_certificate( - OpenSSL.crypto.FILETYPE_PEM, "".join(buf) - ) - ) - buf = [] - elif not line: - log.debug(u"{} certificate(s) found".format(len(certificates))) - return certificates - - def _loadPKey(self, f): - """Read a private key from a .pem file - - @param f (file): file obj (opened .pem file) - @return (list[OpenSSL.crypto.PKey]): private key object - @raise OpenSSL.crypto.Error: error while parsing the file - """ - return OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, f.read()) - - def _loadCertificate(self, f): - """Read a public certificate from a .pem file - - @param f (file): file obj (opened .pem file) - @return (list[OpenSSL.crypto.X509]): public certificate - @raise OpenSSL.crypto.Error: error while parsing the file - """ - return OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, f.read()) - - def _getTLSContextFactory(self): - """Load TLS certificate and build the context factory needed for listenSSL""" - if ssl is None: - raise ImportError(u"Python module pyOpenSSL is not installed!") - - cert_options = {} - - for name, option, method in [ - ("privateKey", "tls_private_key", self._loadPKey), - ("certificate", "tls_certificate", self._loadCertificate), - ("extraCertChain", "tls_chain", self._loadCertificates), - ]: - path = self.options[option] - if not path: - assert option == "tls_chain" - continue - log.debug(u"loading {option} from {path}".format(option=option, path=path)) - try: - with open(path) as f: - cert_options[name] = method(f) - except IOError as e: - log.error( - u"Error while reading file {path} for option {option}: {error}".format( - path=path, option=option, error=e - ) - ) - self.quit(2) - except OpenSSL.crypto.Error: - log.error( - u"Error while parsing file {path} for option {option}, are you sure it is a valid .pem file?".format( - path=path, option=option - ) - ) - if ( - option == "tls_private_key" - and self.options["tls_certificate"] == path - ): - log.error( - u"You are using the same file for private key and public certificate, make sure that both a in {path} or use --tls_private_key option".format( - path=path - ) - ) - self.quit(2) - - return ssl.CertificateOptions(**cert_options) - - ## service management ## - - def _startService(self, dummy=None): - """Actually start the HTTP(S) server(s) after the profile for Libervia is connected. - - @raise ImportError: OpenSSL is not available - @raise IOError: the certificate file doesn't exist - @raise OpenSSL.crypto.Error: the certificate file is invalid - """ - # now that we have service profile connected, we add resource for its cache - service_path = regex.pathEscape(C.SERVICE_PROFILE) - cache_dir = os.path.join(self.cache_root_dir, u"profiles", service_path) - self.cache_resource.putChild(service_path, ProtectedFile(cache_dir)) - self.service_cache_url = u"/" + os.path.join(C.CACHE_DIR, service_path) - session_iface.SATSession.service_cache_url = self.service_cache_url - - if self.options["connection_type"] in ("https", "both"): - self._TLSOptionsCheck() - context_factory = self._getTLSContextFactory() - reactor.listenSSL(self.options["port_https"], self.site, context_factory) - if self.options["connection_type"] in ("http", "both"): - if ( - self.options["connection_type"] == "both" - and self.options["redirect_to_https"] - ): - reactor.listenTCP( - self.options["port"], - server.Site( - RedirectToHTTPS( - self.options["port"], self.options["port_https_ext"] - ) - ), - ) - else: - reactor.listenTCP(self.options["port"], self.site) - - @defer.inlineCallbacks - def stopService(self): - log.info(_("launching cleaning methods")) - for callback, args, kwargs in self._cleanup: - callback(*args, **kwargs) - try: - yield self.bridgeCall("disconnect", C.SERVICE_PROFILE) - except Exception: - log.warning(u"Can't disconnect service profile") - - def run(self): - reactor.run() - - def stop(self): - reactor.stop() - - def quit(self, exit_code=None): - """Exit app when reactor is running - - @param exit_code(None, int): exit code - """ - self.stop() - sys.exit(exit_code or 0) - - -class RedirectToHTTPS(web_resource.Resource): - def __init__(self, old_port, new_port): - web_resource.Resource.__init__(self) - self.isLeaf = True - self.old_port = old_port - self.new_port = new_port - - def render(self, request): - netloc = request.URLPath().netloc.replace( - ":%s" % self.old_port, ":%s" % self.new_port - ) - url = "https://" + netloc + request.uri - return web_util.redirectTo(url, request) - - -registerAdapter(session_iface.SATSession, server.Session, session_iface.ISATSession) -registerAdapter( - session_iface.SATGuestSession, server.Session, session_iface.ISATGuestSession -)
--- a/src/server/session_iface.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,192 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a SAT frontend -# Copyright (C) 2009-2018 Jérôme Poisson (goffi@goffi.org) - -# This program 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. - -# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. -from zope.interface import Interface, Attribute, implements -from sat.tools.common import data_objects -from libervia.server.constants import Const as C -from collections import OrderedDict -import os.path -import shortuuid -import time - -FLAGS_KEY = "_flags" -MAX_CACHE_AFFILIATIONS = 100 # number of nodes to keep in cache - - -class ISATSession(Interface): - profile = Attribute("Sat profile") - jid = Attribute("JID associated with the profile") - uuid = Attribute("uuid associated with the profile session") - identities = Attribute("Identities of XMPP entities") - - -class SATSession(object): - implements(ISATSession) - - def __init__(self, session): - self.profile = None - self.jid = None - self.started = time.time() - # time when the backend session was started - self.backend_started = None - self.uuid = unicode(shortuuid.uuid()) - self.identities = data_objects.Identities() - self.csrf_token = unicode(shortuuid.uuid()) - self.pages_data = {} # used to keep data accross reloads (key is page instance) - self.affiliations = OrderedDict() # cache for node affiliations - - @property - def cache_dir(self): - if self.profile is None: - return self.service_cache_url + u"/" - return os.path.join(u"/", C.CACHE_DIR, self.uuid) + u"/" - - @property - def connected(self): - return self.profile is not None - - @property - def guest(self): - """True if this is a guest session""" - if self.profile is None: - return False - else: - return self.profile.startswith("guest@@") - - def getPageData(self, page, key): - """get session data for a page - - @param page(LiberviaPage): instance of the page - @param key(object): data key - return (None, object): value of the key - None if not found or page_data doesn't exist - """ - return self.pages_data.get(page, {}).get(key) - - def popPageData(self, page, key, default=None): - """like getPageData, but remove key once value is gotten - - @param page(LiberviaPage): instance of the page - @param key(object): data key - @param default(object): value to return if key is not found - @return (object): found value or default - """ - page_data = self.pages_data.get(page) - if page_data is None: - return default - value = page_data.pop(key, default) - if not page_data: - # no need to keep unused page_data - del self.pages_data[page] - return value - - def setPageData(self, page, key, value): - """set data to persist on reload - - @param page(LiberviaPage): instance of the page - @param key(object): data key - @param value(object): value to set - @return (object): set value - """ - page_data = self.pages_data.setdefault(page, {}) - page_data[key] = value - return value - - def setPageFlag(self, page, flag): - """set a flag for this page - - @param page(LiberviaPage): instance of the page - @param flag(unicode): flag to set - """ - flags = self.getPageData(page, FLAGS_KEY) - if flags is None: - flags = self.setPageData(page, FLAGS_KEY, set()) - flags.add(flag) - - def popPageFlag(self, page, flag): - """return True if flag is set - - flag is removed if it was set - @param page(LiberviaPage): instance of the page - @param flag(unicode): flag to set - @return (bool): True if flaag was set - """ - page_data = self.pages_data.get(page, {}) - flags = page_data.get(FLAGS_KEY) - if flags is None: - return False - if flag in flags: - flags.remove(flag) - # we remove data if they are not used anymore - if not flags: - del page_data[FLAGS_KEY] - if not page_data: - del self.pages_data[page] - return True - else: - return False - - def getAffiliation(self, service, node): - """retrieve affiliation for a pubsub node - - @param service(jid.JID): pubsub service - @param node(unicode): pubsub node - @return (unicode, None): affiliation, or None if it is not in cache - """ - if service.resource: - raise ValueError(u"Service must not have a resource") - if not node: - raise ValueError(u"node must be set") - try: - affiliation = self.affiliations.pop((service, node)) - except KeyError: - return None - else: - # we replace at the top to get the most recently used on top - # so less recently used will be removed if cache is full - self.affiliations[(service, node)] = affiliation - return affiliation - - def setAffiliation(self, service, node, affiliation): - """cache affiliation for a node - - will empty cache when it become too big - @param service(jid.JID): pubsub service - @param node(unicode): pubsub node - @param affiliation(unicode): affiliation to this node - """ - if service.resource: - raise ValueError(u"Service must not have a resource") - if not node: - raise ValueError(u"node must be set") - self.affiliations[(service, node)] = affiliation - while len(self.affiliations) > MAX_CACHE_AFFILIATIONS: - self.affiliations.popitem(last=False) - - -class ISATGuestSession(Interface): - id = Attribute("UUID of the guest") - data = Attribute("data associated with the guest") - - -class SATGuestSession(object): - implements(ISATGuestSession) - - def __init__(self, session): - self.id = None - self.data = None
--- a/src/server/utils.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,115 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2011-2018 Jérôme Poisson <goffi@goffi.org> - -# This program 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. - -# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. -from sat.core.i18n import _ -from twisted.internet import reactor -from twisted.internet import defer -from sat.core import exceptions -from sat.core.log import getLogger -import urllib - -log = getLogger(__name__) - - -def quote(value, safe="@"): - """shortcut to quote an unicode value for URL""" - return urllib.quote(value.encode("utf-8"), safe=safe) - - -class ProgressHandler(object): - """class to help the management of progressions""" - - handlers = {} - - def __init__(self, host, progress_id, profile): - self.host = host - self.progress_id = progress_id - self.profile = profile - - @classmethod - def _signal(cls, name, progress_id, data, profile): - handlers = cls.handlers - if profile in handlers and progress_id in handlers[profile]: - handler_data = handlers[profile][progress_id] - timeout = handler_data[u"timeout"] - if timeout.active(): - timeout.cancel() - cb = handler_data[name] - if cb is not None: - cb(data) - if name == u"started": - pass - elif name == u"finished": - handler_data[u"deferred"].callback(data) - handler_data[u"instance"].unregister_handler() - elif name == u"error": - handler_data[u"deferred"].errback(Exception(data)) - handler_data[u"instance"].unregister_handler() - else: - log.error(u"unexpected signal: {name}".format(name=name)) - - def _timeout(self): - log.warning( - _( - u"No progress received, cancelling handler: {progress_id} [{profile}]" - ).format(progress_id=self.progress_id, profile=self.profile) - ) - - def unregister_handler(self): - """remove a previously registered handler""" - try: - del self.handlers[self.profile][self.progress_id] - except KeyError: - log.warning( - _(u"Trying to remove unknown handler: {progress_id} [{profile}]").format( - progress_id=self.progress_id, profile=self.profile - ) - ) - else: - if not self.handlers[self.profile]: - self.handlers[self.profile] - - def register(self, started_cb=None, finished_cb=None, error_cb=None, timeout=30): - """register the signals to handle progression - - @param started_cb(callable, None): method to call when progressStarted signal is received - @param finished_cb(callable, None): method to call when progressFinished signal is received - @param error_cb(callable, None): method to call when progressError signal is received - @param timeout(int): progress time out - if nothing happen in this progression during this delay, - an exception is raised - @return (D(dict[unicode,unicode])): a deferred called when progression is finished - """ - handler_data = self.handlers.setdefault(self.profile, {}).setdefault( - self.progress_id, {} - ) - if handler_data: - raise exceptions.ConflictError( - u"There is already one handler for this progression" - ) - handler_data[u"instance"] = self - deferred = handler_data[u"deferred"] = defer.Deferred() - handler_data[u"started"] = started_cb - handler_data[u"finished"] = finished_cb - handler_data[u"error"] = error_cb - handler_data[u"timeout"] = reactor.callLater(timeout, self._timeout) - return deferred - - -class SubPage(unicode): - """use to mark subpages when generating a page path"""
--- a/src/server/websockets.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,148 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2011-2018 Jérôme Poisson <goffi@goffi.org> - -# This program 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. - -# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. - -from sat.core.i18n import _ -from sat.core.log import getLogger - -log = getLogger(__name__) -from sat.core import exceptions - -from autobahn.twisted import websocket -from autobahn.twisted import resource as resource -from autobahn.websocket import types - -import json - -LIBERVIA_PROTOCOL = "libervia_page" - - -class LiberviaPageWSProtocol(websocket.WebSocketServerProtocol): - host = None - tokens_map = {} - - def onConnect(self, request): - prefix = LIBERVIA_PROTOCOL + u"_" - for protocol in request.protocols: - if protocol.startswith(prefix): - token = protocol[len(prefix) :].strip() - if token: - break - else: - raise types.ConnectionDeny( - types.ConnectionDeny.NOT_IMPLEMENTED, u"Can't use this subprotocol" - ) - - if token not in self.tokens_map: - log.warning(_(u"Can't activate page socket: unknown token")) - raise types.ConnectionDeny( - types.ConnectionDeny.FORBIDDEN, u"Bad token, please reload page" - ) - self.token = token - self.page = self.tokens_map[token]["page"] - self.request = self.tokens_map[token]["request"] - return protocol - - def onOpen(self): - log.debug( - _( - u"Websocket opened for {page} (token: {token})".format( - page=self.page, token=self.token - ) - ) - ) - self.request.sendData = self.sendJSONData - self.page.onSocketOpen(self.request) - - def onMessage(self, payload, isBinary): - try: - data_json = json.loads(payload.decode("utf8")) - except ValueError as e: - log.warning( - _(u"Not valid JSON, ignoring data: {msg}\n{data}").format( - msg=e, data=payload - ) - ) - return - # we request page first, to raise an AttributeError - # if it is not set (which should never happen) - page = self.page - try: - cb = page.on_data - except AttributeError: - log.warning( - _( - u'No "on_data" method set on dynamic page, ignoring data:\n{data}' - ).format(data=data_json) - ) - else: - cb(page, self.request, data_json) - - def onClose(self, wasClean, code, reason): - try: - token = self.token - except AttributeError: - log.warning(_(u"Websocket closed but no token is associated")) - return - - self.page.onSocketClose(self.request) - - try: - del self.tokens_map[token] - del self.request.sendData - except (KeyError, AttributeError): - raise exceptions.InternalError( - _(u"Token or sendData doesn't exist, this should never happen!") - ) - log.debug( - _( - u"Websocket closed for {page} (token: {token}). {reason}".format( - page=self.page, - token=self.token, - reason=u"" - if wasClean - else _(u"Reason: {reason}").format(reason=reason), - ) - ) - ) - - def sendJSONData(self, type_, **data): - assert "type" not in data - data["type"] = type_ - self.sendMessage(json.dumps(data, ensure_ascii=False).encode("utf8")) - - @classmethod - def getBaseURL(cls, host, secure): - return u"ws{sec}://localhost:{port}".format( - sec="s" if secure else "", - port=cls.host.options["port_https" if secure else "port"], - ) - - @classmethod - def getResource(cls, host, secure): - if cls.host is None: - cls.host = host - factory = websocket.WebSocketServerFactory(cls.getBaseURL(host, secure)) - factory.protocol = cls - return resource.WebSocketResource(factory) - - @classmethod - def registerToken(cls, token, page, request): - if token in cls.tokens_map: - raise exceptions.ConflictError(_(u"This token is already registered")) - cls.tokens_map[token] = {"page": page, "request": request}
--- a/src/twisted/plugins/libervia_server.py Sat Aug 11 18:35:37 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,264 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2013-2018 Jérôme Poisson <goffi@goffi.org> -# Copyright (C) 2013-2016 Adrien Cossa <souliane@mailoo.org> -# Copyright (C) 2013 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr> - -# This program 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. - -# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. - -from twisted.internet import defer - -if defer.Deferred.debug: - # if we are in debug mode, we want to use ipdb instead of pdb - try: - import ipdb - import pdb - - pdb.set_trace = ipdb.set_trace - pdb.post_mortem = ipdb.post_mortem - except ImportError: - pass - -import os.path -import libervia -import sat - -from libervia.server.constants import Const as C - -from sat.core.i18n import _ -from sat.tools import config - -from zope.interface import implements - -from twisted.python import usage -from twisted.plugin import IPlugin -from twisted.application.service import IServiceMaker -import ConfigParser - - -CONFIG_SECTION = C.APP_NAME.lower() -if libervia.__version__ != sat.__version__: - import sys - - sys.stderr.write( - u"""sat module version ({sat_version}) and {current_app} version ({current_version}) mismatch - -sat module is located at {sat_path} -libervia module is located at {libervia_path} - -Please be sure to have the same version running -""".format( - sat_version=sat.__version__, - current_app=C.APP_NAME, - current_version=libervia.__version__, - sat_path=os.path.dirname(sat.__file__), - libervia_path=os.path.dirname(libervia.__file__), - ).encode( - "utf-8" - ) - ) - sys.stderr.flush() - # we call os._exit to avoid help to be printed by twisted - import os - - os._exit(1) - - -def coerceConnectionType(value): # called from Libervia.OPT_PARAMETERS - allowed_values = ("http", "https", "both") - if value not in allowed_values: - raise ValueError( - "%(given)s not in %(expected)s" - % {"given": value, "expected": str(allowed_values)} - ) - return value - - -def coerceDataDir(value): # called from Libervia.OPT_PARAMETERS - if isinstance(value, unicode): - # XXX: if value comes from sat.conf, it's unicode, - # and we need byte str here (for twisted) - value = value.encode("utf-8") - value = value.encode("utf-8") - html = os.path.join(value, C.HTML_DIR) - if not os.path.isfile(os.path.join(html, C.LIBERVIA_MAIN_PAGE)): - raise ValueError( - "%s is not a Libervia's browser HTML directory" % os.path.realpath(html) - ) - themes_dir = os.path.join(value, C.THEMES_DIR) - if not os.path.isfile(os.path.join(themes_dir, "default/styles/blog.css")): - raise ValueError( - "%s is not a Libervia's server data directory" % os.path.realpath(themes_dir) - ) - return value - - -def coerceBool(value): - return C.bool(value) - - -def coerceUnicode(value): - # XXX: we use this method to check which value to convert to Unicode - # but we don't do the conversion here as Twisted expect str - return value - - -DATA_DIR_DEFAULT = '' -# options which are in sat.conf and on command line, -# see https://twistedmatrix.com/documents/current/api/twisted.python.usage.Options.html -OPT_PARAMETERS_BOTH = [['connection_type', 't', 'https', _(u"'http', 'https' or 'both' " - "(to launch both servers).").encode('utf-8'), - coerceConnectionType], - ['port', 'p', 8080, - _(u'The port number to listen HTTP on.').encode('utf-8'), int], - ['port_https', 's', 8443, - _(u'The port number to listen HTTPS on.').encode('utf-8'), int], - ['port_https_ext', 'e', 0, _(u'The external port number used for ' - u'HTTPS (0 means port_https value).').encode('utf-8'), int], - ['tls_private_key', '', '', _(u'TLS certificate private key (PEM ' - u'format)').encode('utf-8'), coerceUnicode], - ['tls_certificate', 'c', 'libervia.pem', _(u'TLS public ' - u'certificate or private key and public certificate combined ' - u'(PEM format)').encode('utf-8'), coerceUnicode], - ['tls_chain', '', '', _(u'TLS certificate intermediate chain (PEM ' - u'format)').encode('utf-8'), coerceUnicode], - ['redirect_to_https', 'r', True, _(u'Automatically redirect from ' - u'HTTP to HTTPS.').encode('utf-8'), coerceBool], - ['security_warning', 'w', True, _(u'Warn user that he is about to ' - u'connect on HTTP.').encode('utf-8'), coerceBool], - ['passphrase', 'k', '', (_(u"Passphrase for the SàT profile " - u"named '%s'") % C.SERVICE_PROFILE).encode('utf-8'), - coerceUnicode], - ['data_dir', 'd', DATA_DIR_DEFAULT, _(u'Data directory for ' - u'Libervia').encode('utf-8'), coerceDataDir], - ['allow_registration', '', True, _(u'Allow user to register new ' - u'account').encode('utf-8'), coerceBool], - ['base_url_ext', '', '', - _(u'The external URL to use as base URL').encode('utf-8'), - coerceUnicode], - ] -# Options which are in sat.conf only -OPT_PARAMETERS_CFG = [ - ["empty_password_allowed_warning_dangerous_list", None, "", None], - ["url_redirections_profile", None, "", None], - ["url_redirections_dict", None, {}, None], - ["menu_json", None, C.DEFAULT_MENU, None], - ["tickets_trackers_json", None, None, None], - ["mr_handlers_json", None, None, None], -] - - -def initialise(options): - """Method to initialise global modules""" - from twisted.internet import gireactor - gireactor.install() - - # XXX: We need to configure logs before any log method is used, - # so here is the best place. - from sat.core import log_config - - log_config.satConfigure(C.LOG_BACKEND_TWISTED, C, backend_data=options) - from libervia.server import server - - # we can't import this file from libervia.server.server because it's not a true module - # (there is no __init__.py file, as required by twistd plugin system), so we set the - # global values from here - server.DATA_DIR_DEFAULT = DATA_DIR_DEFAULT - server.OPT_PARAMETERS_BOTH = OPT_PARAMETERS_BOTH - server.OPT_PARAMETERS_CFG = OPT_PARAMETERS_CFG - server.coerceDataDir = coerceDataDir - - -class Options(usage.Options): - # optArgs is not really useful in our case, we need more than a flag - optParameters = OPT_PARAMETERS_BOTH - - def __init__(self): - """Read SàT configuration file in order to overwrite the hard-coded default values - - Priority for the usage of the values is (from lowest to highest): - - hard-coded default values - - values from SàT configuration files - - values passed on the command line - """ - # If we do it the reading later: after the command line options have been parsed, - # there's no good way to know - # if the options values are the hard-coded ones or if they have been passed - # on the command line. - - # FIXME: must be refactored + code can be factorised with backend - config_parser = ConfigParser.SafeConfigParser() - config_parser.read(C.CONFIG_FILES) - self.handleDeprecated(config_parser) - for param in self.optParameters + OPT_PARAMETERS_CFG: - name = param[0] - try: - value = config.getConfig(config_parser, CONFIG_SECTION, name, Exception) - if isinstance(value, unicode): - value = value.encode("utf-8") - try: - param[2] = param[4](value) - except IndexError: # the coerce method is optional - param[2] = value - except (ConfigParser.NoSectionError, ConfigParser.NoOptionError): - pass - usage.Options.__init__(self) - for opt_data in OPT_PARAMETERS_CFG: - self[opt_data[0]] = opt_data[2] - - def handleDeprecated(self, config_parser): - """display warning and/or change option when a deprecated option if found - - param config_parser(ConfigParser): read ConfigParser instance for sat.conf - """ - replacements = (("ssl_certificate", "tls_certificate"),) - for old, new in replacements: - try: - value = config.getConfig(config_parser, CONFIG_SECTION, old, Exception) - except (ConfigParser.NoSectionError, ConfigParser.NoOptionError): - pass - else: - print(u"\n/!\\ Use of {old} is deprecated, please use {new} instead\n" - .format(old=old, new=new)) - config_parser.set(CONFIG_SECTION, new, value) - - -class LiberviaMaker(object): - implements(IServiceMaker, IPlugin) - - tapname = C.APP_NAME_FILE - description = _(u"The web frontend of Salut à Toi") - options = Options - - def makeService(self, options): - for opt in OPT_PARAMETERS_BOTH: - # FIXME: that's a ugly way to get unicode in Libervia - # from command line or sat.conf - # we should move to argparse and handle options this properly - try: - coerce_cb = opt[4] - except IndexError: - continue - if coerce_cb == coerceUnicode: - options[opt[0]] = options[opt[0]].decode("utf-8") - initialise(options.parent) - from libervia.server import server - - return server.Libervia(options) - - -# affectation to some variable is necessary for twisted introspection to work -serviceMaker = LiberviaMaker()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/twisted/plugins/libervia_server.py Sat Aug 25 17:59:48 2018 +0200 @@ -0,0 +1,267 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2013-2018 Jérôme Poisson <goffi@goffi.org> +# Copyright (C) 2013-2016 Adrien Cossa <souliane@mailoo.org> +# Copyright (C) 2013 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr> + +# This program 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. + +# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + +from twisted.internet import defer + +if defer.Deferred.debug: + # if we are in debug mode, we want to use ipdb instead of pdb + try: + import ipdb + import pdb + + pdb.set_trace = ipdb.set_trace + pdb.post_mortem = ipdb.post_mortem + except ImportError: + pass + +import os.path +import libervia +import sat + +from libervia.server.constants import Const as C + +from sat.core.i18n import _ +from sat.tools import config + +from zope.interface import implements + +from twisted.python import usage +from twisted.plugin import IPlugin +from twisted.application.service import IServiceMaker +import ConfigParser + + +CONFIG_SECTION = C.APP_NAME.lower() +if libervia.__version__ != sat.__version__: + import sys + + sys.stderr.write( + u"""sat module version ({sat_version}) and {current_app} version ({current_version}) mismatch + +sat module is located at {sat_path} +libervia module is located at {libervia_path} + +Please be sure to have the same version running +""".format( + sat_version=sat.__version__, + current_app=C.APP_NAME, + current_version=libervia.__version__, + sat_path=os.path.dirname(sat.__file__), + libervia_path=os.path.dirname(libervia.__file__), + ).encode( + "utf-8" + ) + ) + sys.stderr.flush() + # we call os._exit to avoid help to be printed by twisted + import os + + os._exit(1) + + +def coerceConnectionType(value): # called from Libervia.OPT_PARAMETERS + allowed_values = ("http", "https", "both") + if value not in allowed_values: + raise ValueError( + "%(given)s not in %(expected)s" + % {"given": value, "expected": str(allowed_values)} + ) + return value + + +def coerceDataDir(value): # called from Libervia.OPT_PARAMETERS + if not value: + # we ignore missing values + return u'' + if isinstance(value, unicode): + # XXX: if value comes from sat.conf, it's unicode, + # and we need byte str here (for twisted) + value = value.encode("utf-8") + value = value.encode("utf-8") + html = os.path.join(value, C.HTML_DIR) + if not os.path.isfile(os.path.join(html, C.LIBERVIA_MAIN_PAGE)): + raise ValueError( + "%s is not a Libervia's browser HTML directory" % os.path.realpath(html) + ) + themes_dir = os.path.join(value, C.THEMES_DIR) + if not os.path.isfile(os.path.join(themes_dir, "default/styles/blog.css")): + raise ValueError( + "%s is not a Libervia's server data directory" % os.path.realpath(themes_dir) + ) + return value + + +def coerceBool(value): + return C.bool(value) + + +def coerceUnicode(value): + # XXX: we use this method to check which value to convert to Unicode + # but we don't do the conversion here as Twisted expect str + return value + + +DATA_DIR_DEFAULT = '' +# options which are in sat.conf and on command line, +# see https://twistedmatrix.com/documents/current/api/twisted.python.usage.Options.html +OPT_PARAMETERS_BOTH = [['connection_type', 't', 'https', _(u"'http', 'https' or 'both' " + "(to launch both servers).").encode('utf-8'), + coerceConnectionType], + ['port', 'p', 8080, + _(u'The port number to listen HTTP on.').encode('utf-8'), int], + ['port_https', 's', 8443, + _(u'The port number to listen HTTPS on.').encode('utf-8'), int], + ['port_https_ext', 'e', 0, _(u'The external port number used for ' + u'HTTPS (0 means port_https value).').encode('utf-8'), int], + ['tls_private_key', '', '', _(u'TLS certificate private key (PEM ' + u'format)').encode('utf-8'), coerceUnicode], + ['tls_certificate', 'c', 'libervia.pem', _(u'TLS public ' + u'certificate or private key and public certificate combined ' + u'(PEM format)').encode('utf-8'), coerceUnicode], + ['tls_chain', '', '', _(u'TLS certificate intermediate chain (PEM ' + u'format)').encode('utf-8'), coerceUnicode], + ['redirect_to_https', 'r', True, _(u'Automatically redirect from ' + u'HTTP to HTTPS.').encode('utf-8'), coerceBool], + ['security_warning', 'w', True, _(u'Warn user that he is about to ' + u'connect on HTTP.').encode('utf-8'), coerceBool], + ['passphrase', 'k', '', (_(u"Passphrase for the SàT profile " + u"named '%s'") % C.SERVICE_PROFILE).encode('utf-8'), + coerceUnicode], + ['data_dir', 'd', DATA_DIR_DEFAULT, _(u'Data directory for ' + u'Libervia legacy').encode('utf-8'), coerceDataDir], + ['allow_registration', '', True, _(u'Allow user to register new ' + u'account').encode('utf-8'), coerceBool], + ['base_url_ext', '', '', + _(u'The external URL to use as base URL').encode('utf-8'), + coerceUnicode], + ] +# Options which are in sat.conf only +OPT_PARAMETERS_CFG = [ + ["empty_password_allowed_warning_dangerous_list", None, "", None], + ["url_redirections_profile", None, "", None], + ["url_redirections_dict", None, {}, None], + ["menu_json", None, C.DEFAULT_MENU, None], + ["tickets_trackers_json", None, None, None], + ["mr_handlers_json", None, None, None], +] + + +def initialise(options): + """Method to initialise global modules""" + from twisted.internet import gireactor + gireactor.install() + + # XXX: We need to configure logs before any log method is used, + # so here is the best place. + from sat.core import log_config + + log_config.satConfigure(C.LOG_BACKEND_TWISTED, C, backend_data=options) + from libervia.server import server + + # we can't import this file from libervia.server.server because it's not a true module + # (there is no __init__.py file, as required by twistd plugin system), so we set the + # global values from here + server.DATA_DIR_DEFAULT = DATA_DIR_DEFAULT + server.OPT_PARAMETERS_BOTH = OPT_PARAMETERS_BOTH + server.OPT_PARAMETERS_CFG = OPT_PARAMETERS_CFG + server.coerceDataDir = coerceDataDir + + +class Options(usage.Options): + # optArgs is not really useful in our case, we need more than a flag + optParameters = OPT_PARAMETERS_BOTH + + def __init__(self): + """Read SàT configuration file in order to overwrite the hard-coded default values + + Priority for the usage of the values is (from lowest to highest): + - hard-coded default values + - values from SàT configuration files + - values passed on the command line + """ + # If we do it the reading later: after the command line options have been parsed, + # there's no good way to know + # if the options values are the hard-coded ones or if they have been passed + # on the command line. + + # FIXME: must be refactored + code can be factorised with backend + config_parser = ConfigParser.SafeConfigParser() + config_parser.read(C.CONFIG_FILES) + self.handleDeprecated(config_parser) + for param in self.optParameters + OPT_PARAMETERS_CFG: + name = param[0] + try: + value = config.getConfig(config_parser, CONFIG_SECTION, name, Exception) + if isinstance(value, unicode): + value = value.encode("utf-8") + try: + param[2] = param[4](value) + except IndexError: # the coerce method is optional + param[2] = value + except (ConfigParser.NoSectionError, ConfigParser.NoOptionError): + pass + usage.Options.__init__(self) + for opt_data in OPT_PARAMETERS_CFG: + self[opt_data[0]] = opt_data[2] + + def handleDeprecated(self, config_parser): + """display warning and/or change option when a deprecated option if found + + param config_parser(ConfigParser): read ConfigParser instance for sat.conf + """ + replacements = (("ssl_certificate", "tls_certificate"),) + for old, new in replacements: + try: + value = config.getConfig(config_parser, CONFIG_SECTION, old, Exception) + except (ConfigParser.NoSectionError, ConfigParser.NoOptionError): + pass + else: + print(u"\n/!\\ Use of {old} is deprecated, please use {new} instead\n" + .format(old=old, new=new)) + config_parser.set(CONFIG_SECTION, new, value) + + +class LiberviaMaker(object): + implements(IServiceMaker, IPlugin) + + tapname = C.APP_NAME_FILE + description = _(u"The web frontend of Salut à Toi") + options = Options + + def makeService(self, options): + for opt in OPT_PARAMETERS_BOTH: + # FIXME: that's a ugly way to get unicode in Libervia + # from command line or sat.conf + # we should move to argparse and handle options this properly + try: + coerce_cb = opt[4] + except IndexError: + continue + if coerce_cb == coerceUnicode: + options[opt[0]] = options[opt[0]].decode("utf-8") + initialise(options.parent) + from libervia.server import server + + return server.Libervia(options) + + +# affectation to some variable is necessary for twisted introspection to work +serviceMaker = LiberviaMaker()