# HG changeset patch # User Goffi # Date 1565716351 -7200 # Node ID b2d067339de3e08f6301c53d786929ff172bf61a # Parent f14ab8a25e8b594ba13daa4117fca3c1837e171f python 3 port: /!\ Python 3.6+ is now needed to use libervia /!\ instability may occur and features may not be working anymore, this will improve with time /!\ TxJSONRPC dependency has been removed The same procedure as in backend has been applied (check backend commit ab2696e34d29 logs for details). Removed now deprecated code (Pyjamas compiled browser part, legacy blog, JSON RPC related code). Adapted code to work without `html` and `themes` dirs. diff -r f14ab8a25e8b -r b2d067339de3 bin/libervia --- a/bin/libervia Tue Aug 13 09:39:33 2019 +0200 +++ b/bin/libervia Tue Aug 13 19:12:31 2019 +0200 @@ -2,7 +2,7 @@ DEBUG="" DAEMON="" -PYTHON="python2" +PYTHON="python3" TWISTD="$(which twistd)" kill_process() { @@ -25,17 +25,13 @@ eval `"$PYTHON" << PYTHONEND from libervia.server.constants import Const as C from sat.memory.memory import fixLocalDir -from ConfigParser import SafeConfigParser +from configparser import ConfigParser 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) +config = ConfigParser(defaults=C.DEFAULT_CONFIG) try: config.read(C.CONFIG_FILES) except: @@ -48,7 +44,7 @@ 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) +print (";".join(env)) PYTHONEND ` APP_NAME="$APP_NAME" diff -r f14ab8a25e8b -r b2d067339de3 browser/collections.py --- a/browser/collections.py Tue Aug 13 09:39:33 2019 +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 - -# 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 . - -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 diff -r f14ab8a25e8b -r b2d067339de3 browser/libervia_main.py --- a/browser/libervia_main.py Tue Aug 13 09:39:33 2019 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,714 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2011-2019 Jérôme Poisson - -# 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 . - - -### logging configuration ### -from sat_browser import logging -logging.configure() -from sat.core.log import getLogger -log = getLogger(__name__) -### - -from sat_browser import json -# XXX: workaround for incomplete json.dumps in pyjamas -import json as json_pyjs -dumps_old = json_pyjs.dumps -json_pyjs.dumps = lambda obj, *args, **kwargs: dumps_old(obj) - -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 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.
You'll only be able to blog publicly.")).show() - - else: - dialog.InfoDialog(_(u"Blogging not available"), _(u"Your server can't handle (micro)blogging.
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) diff -r f14ab8a25e8b -r b2d067339de3 browser/libervia_test.py --- a/browser/libervia_test.py Tue Aug 13 09:39:33 2019 +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-2019 Jérôme Poisson - -# 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 . - - -# Just visit /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() diff -r f14ab8a25e8b -r b2d067339de3 browser/otr.min.js --- a/browser/otr.min.js Tue Aug 13 09:39:33 2019 +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 - 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<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;(bkbj.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;(bn2*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;(bn0);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=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)=0;bl++){if(t[bl]>0){return 1}}for(bl=bm-1+T;bl0){return 0}}for(bl=bj-1;bl>=T;bl--){if(t[bl-T]>bn[bl]){return 1}else{if(t[bl-T]=0;bj--){if(t[bj]>bk[bj]){return 1}else{if(t[bj]>=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=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;bn1){if(bo&1){t=1}bq+=bl;bo>>=1}bq+=t*bl;bs=a0(0,bq,0);for(bn=0;bn=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=blbk.length){for(;bj0;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>=x}}function at(T,bm){var bk,bj,bl,t;T[0]+=bm;bj=T.length;bl=0;for(bk=0;bk>bk))}t[bj]>>=bk}function s(t){var T;for(T=0;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]<>(x-bk)))}t[bj]=aw&(t[bj]<=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.length0&&!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;bk0&&!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;bk31){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>>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>>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>>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>>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>>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>>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>>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>>(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>>(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>>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>>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>>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>>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;m0;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;ii;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.length0&&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}}); - -}; diff -r f14ab8a25e8b -r b2d067339de3 browser/otr.min.js_README --- a/browser/otr.min.js_README Tue Aug 13 09:39:33 2019 +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. diff -r f14ab8a25e8b -r b2d067339de3 browser/public/contrat_social.html --- a/browser/public/contrat_social.html Tue Aug 13 09:39:33 2019 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,110 +0,0 @@ - - - - - Salut Toi: Contrat Social - - - -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.
- -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.
- -
- - 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:
- -
    - -
  • nous plaons la Libert 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 -
      - -
    • la libert d'excuter le programme, pour tous les usages,
    • - -
    -
      - -
    • la libert d'tudier le fonctionnement du programme et de -l'adapter ses besoins,
    • - -
    -
      - -
    • la libert de redistribuer des copies du programme,
    • - -
    -
      - -
    • la libert d'amliorer le programme et de distribuer ces -amliorations au public.
      -
    • - -
    -
  • - - - - - -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. - -
  • 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.
  • -
  • Nous incitons fortement la dcentralisation gnralise. -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.
  • -
  • 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 jamais -de forme de rclame commerciale de notre fait.
  • -
  • L'galit 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.
  • -
  • Nous ferons tout notre possible pour lutter contre toute -tentative de censure. Le rseau global doit tre un moyen d'expression -pour tous.
  • -
  • 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.
  • - -
  • L'ide de Fraternit est essentielle, aussi: -
      -
    • nous ferons notre -possible pour aider les utilisateurs, quel que soit leur niveau
    • -
    • de mme, des efforts seront fait quant -l'accessibilit pour tous
    • -
    • 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.
    • -
    -
  • - - -
- - diff -r f14ab8a25e8b -r b2d067339de3 browser/public/favico.min.js --- a/browser/public/favico.min.js Tue Aug 13 09:39:33 2019 +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;d0?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=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}(); diff -r f14ab8a25e8b -r b2d067339de3 browser/public/libervia.css --- a/browser/public/libervia.css Tue Aug 13 09:39:33 2019 +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 -Copyright (C) 2011 Adrien Vigneron -Copyright (C) 2013-2016 Adrien Cossa - -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 . -*/ - - -/* - * 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; -} diff -r f14ab8a25e8b -r b2d067339de3 browser/public/libervia.html --- a/browser/public/libervia.html Tue Aug 13 09:39:33 2019 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,33 +0,0 @@ - - - - - - - - - -Libervia - - - - - - - diff -r f14ab8a25e8b -r b2d067339de3 browser/public/robots.txt --- a/browser/public/robots.txt Tue Aug 13 09:39:33 2019 +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: / diff -r f14ab8a25e8b -r b2d067339de3 browser/public/sat_logo_16.png Binary file browser/public/sat_logo_16.png has changed diff -r f14ab8a25e8b -r b2d067339de3 browser/sat_browser/__init__.py diff -r f14ab8a25e8b -r b2d067339de3 browser/sat_browser/base_menu.py --- a/browser/sat_browser/base_menu.py Tue Aug 13 09:39:33 2019 +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-2019 Jérôme Poisson - -# 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 . - - -"""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 diff -r f14ab8a25e8b -r b2d067339de3 browser/sat_browser/base_panel.py --- a/browser/sat_browser/base_panel.py Tue Aug 13 09:39:33 2019 +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-2019 Jérôme Poisson - -# 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 . - -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) diff -r f14ab8a25e8b -r b2d067339de3 browser/sat_browser/base_widget.py --- a/browser/sat_browser/base_widget.py Tue Aug 13 09:39:33 2019 +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-2019 Jérôme Poisson - -# 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 . - -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 = "" - - 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 diff -r f14ab8a25e8b -r b2d067339de3 browser/sat_browser/blog.py --- a/browser/sat_browser/blog.py Tue Aug 13 09:39:33 2019 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,559 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2011-2019 Jérôme Poisson - -# 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 . - -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.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' ' - if author: - author += " <%s>" % author_jid - elif author_jid: - author = author_jid - else: - author = _("") - - update_text = u" — ✍ " + "" % datetime.fromtimestamp(self.item.updated) - self.header.add(HTML(""" - on - %(updated)s - """ % {'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 ''} - content['tags'] = self.item.tags - - 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'switch to blog') - title = _(u'compose a rich text message with a title - suitable for writing articles') - else: - html = _(u'switch to microblog') - 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'shift + enter to publish'), 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 = content['tags'] - 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 PUBLIC 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: %s" - - 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 comment 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) diff -r f14ab8a25e8b -r b2d067339de3 browser/sat_browser/chat.py --- a/browser/sat_browser/chat.py Tue Aug 13 09:39:33 2019 +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-2019 Jérôme Poisson - -# 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 . - -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 = "{msg}" - - 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 = "{timestamp} {nick} {msg}" - _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 %s" % self.target - elif self.type == C.CHAT_GROUP: - msg = "This message will be sent to all the participants of the multi-user room %s" % 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'}) diff -r f14ab8a25e8b -r b2d067339de3 browser/sat_browser/constants.py --- a/browser/sat_browser/constants.py Tue Aug 13 09:39:33 2019 +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-2019 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 . - -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') diff -r f14ab8a25e8b -r b2d067339de3 browser/sat_browser/contact_group.py --- a/browser/sat_browser/contact_group.py Tue Aug 13 09:39:33 2019 +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 - -# 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 . - -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_entities) - 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() diff -r f14ab8a25e8b -r b2d067339de3 browser/sat_browser/contact_list.py --- a/browser/sat_browser/contact_list.py Tue Aug 13 09:39:33 2019 +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-2019 Jérôme Poisson - -# 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 . - -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) diff -r f14ab8a25e8b -r b2d067339de3 browser/sat_browser/contact_panel.py --- a/browser/sat_browser/contact_panel.py Tue Aug 13 09:39:33 2019 +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-2019 Jérôme Poisson - -# 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 . - -""" 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) diff -r f14ab8a25e8b -r b2d067339de3 browser/sat_browser/contact_widget.py --- a/browser/sat_browser/contact_widget.py Tue Aug 13 09:39:33 2019 +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-2019 Jérôme Poisson - -# 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 . - -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 = ("(%i) " % 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 '' % 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('' % 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)}) diff -r f14ab8a25e8b -r b2d067339de3 browser/sat_browser/dialog.py --- a/browser/sat_browser/dialog.py Tue Aug 13 09:39:33 2019 +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-2019 Jérôme Poisson - -# 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 . - -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 = "" - - 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) diff -r f14ab8a25e8b -r b2d067339de3 browser/sat_browser/editor_widget.py --- a/browser/sat_browser/editor_widget.py Tue Aug 13 09:39:33 2019 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,383 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2011-2019 Jérôme Poisson - -# 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 . - -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: - if isinstance(content[key], list): - self._original_content[key] = [self.strproc(s) for s in content[key]] - else: - 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 or . - - 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) diff -r f14ab8a25e8b -r b2d067339de3 browser/sat_browser/file_tools.py --- a/browser/sat_browser/file_tools.py Tue Aug 13 09:39:33 2019 +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-2019 Jérôme Poisson - -# 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 . - -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_('Submitting, please wait...'), - '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...
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) diff -r f14ab8a25e8b -r b2d067339de3 browser/sat_browser/game_radiocol.py --- a/browser/sat_browser/game_radiocol.py Tue Aug 13 09:39:33 2019 +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-2019 Jérôme Poisson - -# 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 . - -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.
- Please do not submit files that are protected by copyright.
- Click here 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) diff -r f14ab8a25e8b -r b2d067339de3 browser/sat_browser/game_tarot.py --- a/browser/sat_browser/game_tarot.py Tue Aug 13 09:39:33 2019 +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-2019 Jérôme Poisson - -# 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 . - -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 win !" - else: - title = "You loose :(" - 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) diff -r f14ab8a25e8b -r b2d067339de3 browser/sat_browser/html_tools.py --- a/browser/sat_browser/html_tools.py Tue Aug 13 09:39:33 2019 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,80 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2011-2019 Jérôme Poisson - -# 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 . - -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(/(^(| |\s)+)|((| |\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
""" - return text.replace('\n', '
') - - -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 diff -r f14ab8a25e8b -r b2d067339de3 browser/sat_browser/json.py --- a/browser/sat_browser/json.py Tue Aug 13 09:39:33 2019 +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-2019 Jérôme Poisson - -# 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 . - - -### 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 {} 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) diff -r f14ab8a25e8b -r b2d067339de3 browser/sat_browser/libervia_widget.py --- a/browser/sat_browser/libervia_widget.py Tue Aug 13 09:39:33 2019 +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-2019 Jérôme Poisson - -# 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 . -"""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('', True, base_menu.SimpleCmd(parent.onSetting)) - button_group.addItem('', 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 diff -r f14ab8a25e8b -r b2d067339de3 browser/sat_browser/list_manager.py --- a/browser/sat_browser/list_manager.py Tue Aug 13 09:39:33 2019 +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 - -# 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 . - -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('x', 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) - diff -r f14ab8a25e8b -r b2d067339de3 browser/sat_browser/logging.py --- a/browser/sat_browser/logging.py Tue Aug 13 09:39:33 2019 +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-2019 Jérôme Poisson - -# 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 . -"""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 diff -r f14ab8a25e8b -r b2d067339de3 browser/sat_browser/main_panel.py --- a/browser/sat_browser/main_panel.py Tue Aug 13 09:39:33 2019 +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-2019 Jérôme Poisson - -# 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 . - -"""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' %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) diff -r f14ab8a25e8b -r b2d067339de3 browser/sat_browser/menu.py --- a/browser/sat_browser/menu.py Tue Aug 13 09:39:33 2019 +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-2019 Jérôme Poisson - -# 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 . - -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 = "%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("""Libervia, a Salut à Toi project
-
- Libervia is a web frontend for Salut à Toi
- SàT version: {sat_version}
- Libervia version: {libervia_version}
-
- You can contact the authors at contact@salut-a-toi.org
- Blog available (mainly in french) at http://www.goffi.org
- Project page: http://salut-a-toi.org
-
- Any help welcome :) -

This project is dedicated to Roger Poisson

- """.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) diff -r f14ab8a25e8b -r b2d067339de3 browser/sat_browser/nativedom.py --- a/browser/sat_browser/nativedom.py Tue Aug 13 09:39:33 2019 +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-2019 Jérôme Poisson - -# 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 . - -""" -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)) diff -r f14ab8a25e8b -r b2d067339de3 browser/sat_browser/notification.py --- a/browser/sat_browser/notification.py Tue Aug 13 09:39:33 2019 +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.
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) diff -r f14ab8a25e8b -r b2d067339de3 browser/sat_browser/otrjs_wrapper.py --- a/browser/sat_browser/otrjs_wrapper.py Tue Aug 13 09:39:33 2019 +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-2019 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 . - -"""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;""") diff -r f14ab8a25e8b -r b2d067339de3 browser/sat_browser/plugin_sec_otr.py --- a/browser/sat_browser/plugin_sec_otr.py Tue Aug 13 09:39:33 2019 +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-2019 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 . - -""" -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 = "
" - -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: [ - '', - '' - ], - otr.context.STATE_ENCRYPTED: [ - '', - '' - ], - otr.context.STATE_FINISHED: [ - '', - '' - ] -} - - -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 + "" + AUTH_FINGERPRINT_TXT + "" + 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 + "" + AUTH_QUEST_DEFINE_TXT + "" + 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 + "" + AUTH_QUEST_ANSWER_TXT + "" + 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() diff -r f14ab8a25e8b -r b2d067339de3 browser/sat_browser/plugin_xep_0085.py --- a/browser/sat_browser/plugin_xep_0085.py Tue Aug 13 09:39:33 2019 +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 . - -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 diff -r f14ab8a25e8b -r b2d067339de3 browser/sat_browser/register.py --- a/browser/sat_browser/register.py Tue Aug 13 09:39:33 2019 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,287 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2011-2019 Jérôme Poisson -# Copyright (C) 2011, 2012 Adrien Vigneron - -# 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 . - -#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(_('Entering a full JID is only needed to connect with an external XMPP account.')) - 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
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
are a-z A-Z 0-9 _ -')) - elif not self.checkEmail(self.email_box.getText()): - self.register_warning_msg.setHTML(_('Invalid email address
(or not accepted yet)')) - elif len(self.register_pass_box.getText()) < C.PASSWORD_MIN_LENGTH: - self.register_warning_msg.setHTML(_('Your password must contain
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,
please choose another one.')) - elif result == C.INVALID_CERTIFICATE: - self.register_warning_msg.setHTML(_('The certificate of the server is invalid,
please contact your server administrator.')) - 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) diff -r f14ab8a25e8b -r b2d067339de3 browser/sat_browser/richtext.py --- a/browser/sat_browser/richtext.py Tue Aug 13 09:39:33 2019 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,359 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2013-2016 Adrien Cossa - -# 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 . - -from sat_frontends.tools import composition -from sat.core.i18n import _ -from sat.core.log import getLogger -log = getLogger(__name__) - -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('' % - 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'): - content['tags'] = self.tags_panel.getTags() - 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 = content['tags'] - 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 = '

%s

' % html_tools.html_sanitize(content['title']) - else: - title = "" - - tags = "" - for tag in content['tags']: - tags += "
  • %s
  • " % html_tools.html_sanitize(tag) - if tags: - tags = '
      %s
    ' % 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 diff -r f14ab8a25e8b -r b2d067339de3 browser/sat_browser/strings.py --- a/browser/sat_browser/strings.py Tue Aug 13 09:39:33 2019 +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 . - -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 '' + url + ''; - })""") - - -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 = /]* src="([^"]+)"[^>]*>/g; - return text.replace(imgRegex, function(img, src) { - return '' + img + ''; - })""") - -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 = //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, ' - -# 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 . - -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) diff -r f14ab8a25e8b -r b2d067339de3 browser/sat_browser/xmlui.py --- a/browser/sat_browser/xmlui.py Tue Aug 13 09:39:33 2019 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,505 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2011-2019 Jérôme Poisson - -# 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 . - -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, "
    ") - 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': - 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) diff -r f14ab8a25e8b -r b2d067339de3 libervia/common/constants.py --- a/libervia/common/constants.py Tue Aug 13 09:39:33 2019 +0200 +++ b/libervia/common/constants.py Tue Aug 13 19:12:31 2019 +0200 @@ -25,7 +25,7 @@ # 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.8.0D" # Please add 'D' at the end for dev versions + APP_VERSION = "0.8.0D" # Please add 'D' at the end for dev versions LIBERVIA_MAIN_PAGE = "libervia.html" LIBERVIA_PAGE_START = "/login" diff -r f14ab8a25e8b -r b2d067339de3 libervia/pages/app/page_meta.py --- a/libervia/pages/app/page_meta.py Tue Aug 13 09:39:33 2019 +0200 +++ b/libervia/pages/app/page_meta.py Tue Aug 13 19:12:31 2019 +0200 @@ -1,5 +1,5 @@ -#!/usr/bin/env python2.7 +#!/usr/bin/env python3 # -*- coding: utf-8 -*- -name = u"app" -template = u"app/app.html" +name = "app" +template = "app/app.html" diff -r f14ab8a25e8b -r b2d067339de3 libervia/pages/blog/page_meta.py --- a/libervia/pages/blog/page_meta.py Tue Aug 13 09:39:33 2019 +0200 +++ b/libervia/pages/blog/page_meta.py Tue Aug 13 19:12:31 2019 +0200 @@ -1,4 +1,4 @@ -#!/usr/bin/env python2.7 +#!/usr/bin/env python3 # -*- coding: utf-8 -*- from sat.core.i18n import _ from libervia.server.constants import Const as C @@ -9,9 +9,9 @@ log = getLogger(__name__) -name = u"blog" +name = "blog" access = C.PAGES_ACCESS_PUBLIC -template = u"blog/discover.html" +template = "blog/discover.html" @defer.inlineCallbacks @@ -22,7 +22,7 @@ __, entities_own, entities_roster = yield self.host.bridgeCall( "discoFindByFeatures", [], - [(u"pubsub", u"pep")], + [("pubsub", "pep")], True, False, True, @@ -30,11 +30,11 @@ True, profile, ) - entities = template_data[u"disco_entities"] = ( - entities_own.keys() + entities_roster.keys() + entities = template_data["disco_entities"] = ( + list(entities_own.keys()) + list(entities_roster.keys()) ) - entities_url = template_data[u"entities_url"] = {} - identities = template_data[u"identities"] = self.host.getSessionData( + entities_url = template_data["entities_url"] = {} + identities = template_data["identities"] = self.host.getSessionData( request, session_iface.ISATSession ).identities d_list = [] @@ -43,24 +43,24 @@ entity_jid_s ) if entity_jid_s not in identities: - d_list.append(self.host.bridgeCall(u"identityGet", + d_list.append(self.host.bridgeCall("identityGet", entity_jid_s, profile)) identities_data = yield defer.DeferredList(d_list) for idx, (success, identity) in enumerate(identities_data): entity_jid_s = entities[idx] if not success: - log.warning(_(u"Can't retrieve identity of {entity}") + log.warning(_("Can't retrieve identity of {entity}") .format(entity=entity_jid_s)) else: identities[entity_jid_s] = identity def on_data_post(self, request): - jid_str = self.getPostedData(request, u"jid") + jid_str = self.getPostedData(request, "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()) + url = self.getPageByName("blog_view").getURL(jid_.full()) self.HTTPRedirect(request, url) diff -r f14ab8a25e8b -r b2d067339de3 libervia/pages/blog/view/atom.xml/page_meta.py --- a/libervia/pages/blog/view/atom.xml/page_meta.py Tue Aug 13 09:39:33 2019 +0200 +++ b/libervia/pages/blog/view/atom.xml/page_meta.py Tue Aug 13 19:12:31 2019 +0200 @@ -1,4 +1,4 @@ -#!/usr/bin/env python2.7 +#!/usr/bin/env python3 # -*- coding: utf-8 -*- from libervia.server.constants import Const as C @@ -6,36 +6,36 @@ from sat.tools.common import uri import time -name = u"blog_feed_atom" +name = "blog_feed_atom" access = C.PAGES_ACCESS_PUBLIC -template = u"blog/atom.xml" +template = "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") + service, node = data["service"], data.get("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") + blog_page = self.getPageByName("blog_view") yield blog_page.prepare_render(self, request) - items = data[u"items"] + items = data["items"] - template_data[u"request_uri"] = self.host.getExtBaseURL( + template_data["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 + template_data["xmpp_uri"] = uri.buildXMPPUri( + "pubsub", subtype="microblog", path=service.full(), node=node ) - blog_view = self.getPageByName(u"blog_view") - template_data[u"http_uri"] = self.host.getExtBaseURL( + blog_view = self.getPageByName("blog_view") + template_data["http_uri"] = self.host.getExtBaseURL( request, blog_view.getURL(service.full(), node) ) if items: - template_data[u"updated"] = items[0].updated + template_data["updated"] = items[0].updated else: - template_data[u"updated"] = time.time() + template_data["updated"] = time.time() diff -r f14ab8a25e8b -r b2d067339de3 libervia/pages/blog/view/page_meta.py --- a/libervia/pages/blog/view/page_meta.py Tue Aug 13 09:39:33 2019 +0200 +++ b/libervia/pages/blog/view/page_meta.py Tue Aug 13 19:12:31 2019 +0200 @@ -1,4 +1,4 @@ -#!/usr/bin/env python2.7 +#!/usr/bin/env python3 # -*- coding: utf-8 -*- import unicodedata import re @@ -19,20 +19,20 @@ log = getLogger(__name__) """generic blog (with service/node provided)""" -name = u'blog_view' -template = u"blog/articles.html" -uri_handlers = {(u'pubsub', u'microblog'): 'microblog_uri'} +name = 'blog_view' +template = "blog/articles.html" +uri_handlers = {('pubsub', 'microblog'): 'microblog_uri'} -RE_TEXT_URL = re.compile(ur'[^a-zA-Z,_]+') +RE_TEXT_URL = re.compile(r'[^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']]) + args = [uri_data['path'], uri_data['node']] + if 'item' in uri_data: + args.extend(['id', uri_data['item']]) return self.getURL(*args) def parse_url(self, request): @@ -49,31 +49,31 @@ try: service = self.nextPath(request) except IndexError: - data['service'] = u'' + data['service'] = '' else: try: - data[u"service"] = jid.JID(service) + data["service"] = jid.JID(service) except Exception: - log.warning(_(u"bad service entered: {}").format(service)) + log.warning(_("bad service entered: {}").format(service)) self.pageError(request, C.HTTP_BAD_REQUEST) try: node = self.nextPath(request) except IndexError: - node = u'@' - data['node'] = u'' if node == u'@' else node + node = '@' + data['node'] = '' if node == '@' else node try: filter_kw = data['filter_keyword'] = self.nextPath(request) except IndexError: - filter_kw = u'@' + filter_kw = '@' else: - if filter_kw == u'@': + if filter_kw == '@': # No filter, this is used when a subpage is needed, notably Atom feed pass - elif filter_kw == u'id': + elif filter_kw == 'id': try: - data[u'item'] = self.nextPath(request) + data['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 @@ -81,32 +81,32 @@ self.nextPath(request) except IndexError: pass - elif filter_kw == u'tag': + elif filter_kw == 'tag': try: - data[u'tag'] = self.nextPath(request) + data['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( + log.warning(_("invalid filter keyword: {filter_kw}").format( filter_kw=filter_kw)) self.pageError(request, C.HTTP_BAD_REQUEST) # if URL is parsed here, we'll have atom.xml available and we need to # add the link to the page atom_url = self.getURLByPath( - SubPage(u'blog_view'), + SubPage('blog_view'), service, node, filter_kw, - SubPage(u'blog_feed_atom'), + SubPage('blog_feed_atom'), ) - request.template_data[u'atom_url'] = atom_url - request.template_data.setdefault(u'links', []).append({ - u"href": atom_url, - u"type": "application/atom+xml", - u"rel": "alternate", - u"title": "{service}'s blog".format(service=service)}) + request.template_data['atom_url'] = atom_url + request.template_data.setdefault('links', []).append({ + "href": atom_url, + "type": "application/atom+xml", + "rel": "alternate", + "title": "{service}'s blog".format(service=service)}) @defer.inlineCallbacks @@ -115,15 +115,15 @@ 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)) + log.warning(_("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) + identities[author] = yield self.host.bridgeCall('identityGet', author, profile) for comment_data in blog_item.comments: - service = comment_data[u'service'] - node = comment_data[u'node'] + service = comment_data['service'] + node = comment_data['node'] try: - comments_data = yield self.host.bridgeCall(u'mbGet', + comments_data = yield self.host.bridgeCall('mbGet', service, node, C.NO_LIMIT, @@ -131,7 +131,7 @@ {C.KEY_ORDER_BY: C.ORDER_BY_CREATION}, profile) except Exception as e: - log.warning(_(u"Can't get comments at {node} (service: {service}): {msg}").format( + log.warning(_("Can't get comments at {node} (service: {service}): {msg}").format( service=service, node=node, msg=e)) @@ -148,7 +148,7 @@ items_id = [item_id] else: items_id = [] - blog_data = yield self.host.bridgeCall(u'mbGet', + blog_data = yield self.host.bridgeCall('mbGet', service.userhost(), node, C.NO_LIMIT, @@ -157,10 +157,10 @@ profile) except Exception as e: # FIXME: need a better way to test errors in bridge errback - if u"forbidden" in unicode(e): + if "forbidden" in str(e): self.pageError(request, 401) else: - log.warning(_(u"can't retrieve blog for [{service}]: {msg}".format( + log.warning(_("can't retrieve blog for [{service}]: {msg}".format( service = service.userhost(), msg=e))) blog_data = ([], {}) @@ -169,9 +169,9 @@ @defer.inlineCallbacks def prepare_render(self, request): data = self.getRData(request) - page_max = data.get(u"page_max", 10) + page_max = data.get("page_max", 10) # if the comments are not explicitly hidden, we show them - service, node, item_id, show_comments = data.get(u'service', u''), data.get(u'node', u''), data.get(u'item'), data.get(u'show_comments', True) + service, node, item_id, show_comments = data.get('service', ''), data.get('node', ''), data.get('item'), data.get('show_comments', True) profile = self.getProfile(request) if profile is None: profile = C.SERVICE_PROFILE @@ -186,7 +186,7 @@ extra = self.getPubsubExtra(request, page_max=page_max) tag = data.get('tag') if tag: - extra[u'mam_filter_{}'.format(C.MAM_FILTER_CATEGORY)] = tag + extra['mam_filter_{}'.format(C.MAM_FILTER_CATEGORY)] = tag ## main data ## # we get data from backend/XMPP here @@ -195,11 +195,11 @@ ## 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 'service' not in template_data: + template_data['service'] = service + if 'node' not in template_data: + template_data['node'] = node + target_profile = template_data.get('target_profile') if items: if not item_id: @@ -212,7 +212,7 @@ ## identities ## # identities are used to show nice nickname or avatars - identities = template_data[u'identities'] = self.host.getSessionData(request, session_iface.ISATSession).identities + identities = template_data['identities'] = self.host.getSessionData(request, session_iface.ISATSession).identities ## Comments ## # if comments are requested, we need to take them @@ -223,29 +223,29 @@ # 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') + blog_base_url_item = self.getPageByName('blog_view').getURL(service.full(), node or '@', 'id') + blog_base_url_tag = self.getPageByName('blog_view').getURL(service.full(), node or '@', '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'])]) + blog_base_url_item = self.getURLByNames([('user', [target_profile]), ('user_blog', ['id'])]) + blog_base_url_tag = self.getURLByNames([('user', [target_profile]), ('user_blog', ['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']) + bg_img = yield self.host.bridgeCall('asyncGetParamA', 'Background', 'Blog page', 'value', -1, template_data['target_profile']) if bg_img: - template_data['dynamic_style'] = safe(u""" + template_data['dynamic_style'] = safe(""" :root { --bg-img: url("%s"); } """ % cgi.escape(bg_img, True)) - template_data[u'items'] = data[u'items'] = items + template_data['items'] = data['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 = {} + template_data['items'].items.reverse() + template_data['items_http_uri'] = items_http_uri = {} + template_data['tags_http_uri'] = tags_http_uri = {} for item in items: - blog_canonical_url = u'/'.join([blog_base_url_item, utils.quote(item.id)]) + blog_canonical_url = '/'.join([blog_base_url_item, utils.quote(item.id)]) if len(blog_canonical_url) > URL_LIMIT_MARK: blog_url = blog_canonical_url else: @@ -253,35 +253,35 @@ # 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]) + text = unicodedata.normalize('NFD', text).encode('ascii', 'ignore').decode('utf-8') + text = RE_TEXT_URL.sub(' ', text).lower() + text = '-'.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] + if '-' in text: + text = text.rsplit('-', 1)[0] else: text = text[:TEXT_MAX_LEN] if text: - blog_url = blog_canonical_url + u'/' + text + blog_url = blog_canonical_url + '/' + 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)]) + tag_url = '/'.join([blog_base_url_tag, utils.quote(tag)]) tags_http_uri[tag] = self.host.getExtBaseURL(request, tag_url) # if True, page should display a comment box - template_data[u'allow_commenting'] = data.get(u'allow_commenting', profile_connected) + template_data['allow_commenting'] = data.get('allow_commenting', profile_connected) # last but not least, we add a xmpp: link to the node - uri_args = {u'path': service.full()} + uri_args = {'path': service.full()} if node: - uri_args[u'node'] = node + uri_args['node'] = node if item_id: - uri_args[u'item'] = item_id - template_data[u'xmpp_uri'] = uri.buildXMPPUri(u'pubsub', subtype='microblog', **uri_args) + uri_args['item'] = item_id + template_data['xmpp_uri'] = uri.buildXMPPUri('pubsub', subtype='microblog', **uri_args) @defer.inlineCallbacks @@ -289,23 +289,23 @@ profile = self.getProfile(request) if profile is None: self.pageError(request, C.HTTP_FORBIDDEN) - type_ = self.getPostedData(request, u'type') - if type_ == u'comment': - service, node, body = self.getPostedData(request, (u'service', u'node', u'body')) + type_ = self.getPostedData(request, 'type') + if type_ == 'comment': + service, node, body = self.getPostedData(request, ('service', 'node', 'body')) if not body: self.pageError(request, C.HTTP_BAD_REQUEST) - comment_data = {u"content": body} + comment_data = {"content": body} try: - yield self.host.bridgeCall(u'mbSend', + yield self.host.bridgeCall('mbSend', service, node, data_format.serialise(comment_data), profile) except Exception as e: - if u"forbidden" in unicode(e): + if "forbidden" in str(e): self.pageError(request, 401) else: raise e else: - log.warning(_(u"Unhandled data type: {}").format(type_)) + log.warning(_("Unhandled data type: {}").format(type_)) diff -r f14ab8a25e8b -r b2d067339de3 libervia/pages/chat/page_meta.py --- a/libervia/pages/chat/page_meta.py Tue Aug 13 09:39:33 2019 +0200 +++ b/libervia/pages/chat/page_meta.py Tue Aug 13 19:12:31 2019 +0200 @@ -1,4 +1,4 @@ -#!/usr/bin/env python2.7 +#!/usr/bin/env python3 # -*- coding: utf-8 -*- from sat.core.i18n import _ @@ -11,9 +11,9 @@ from libervia.server.constants import Const as C from libervia.server import session_iface -name = u"chat" +name = "chat" access = C.PAGES_ACCESS_PROFILE -template = u"chat/chat.html" +template = "chat/chat.html" dynamic = True @@ -24,15 +24,15 @@ target_jid_s = self.nextPath(request) except IndexError: # not chat jid, we redirect to jid selection page - self.pageRedirect(u"chat_select", request) + self.pageRedirect("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)")) + raise ValueError(_("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) + _("bad chat jid entered: {jid} ({msg})").format(jid=target_jid, msg=e) ) self.pageError(request, C.HTTP_BAD_REQUEST) else: @@ -49,14 +49,14 @@ profile = session.profile profile_jid = session.jid - disco = yield self.host.bridgeCall(u"discoInfos", target_jid.host, u"", True, profile) + disco = yield self.host.bridgeCall("discoInfos", target_jid.host, "", 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 + "mucJoin", target_jid.userhost(), "", "", profile ) already_joined, room_jid_s, occupants, user_nick, room_subject, __ = join_ret - template_data[u"subject"] = room_subject + template_data["subject"] = room_subject own_jid = jid.JID(room_jid_s) own_jid.resource = user_nick else: @@ -65,9 +65,9 @@ rdata["chat_type"] = chat_type template_data["own_jid"] = own_jid - self.registerSignal(request, u"messageNew") + self.registerSignal(request, "messageNew") history = yield self.host.bridgeCall( - u"historyGet", + "historyGet", profile_jid.userhost(), target_jid.userhost(), 20, @@ -78,45 +78,45 @@ authors = {m[2] for m in history} identities = {} for author in authors: - identities[author] = yield self.host.bridgeCall(u"identityGet", author, profile) + identities[author] = yield self.host.bridgeCall("identityGet", author, profile) - template_data[u"messages"] = data_objects.Messages(history) - rdata[u'identities'] = template_data[u"identities"] = identities - template_data[u"target_jid"] = target_jid - template_data[u"chat_type"] = chat_type + template_data["messages"] = data_objects.Messages(history) + rdata['identities'] = template_data["identities"] = identities + template_data["target_jid"] = target_jid + template_data["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", "") + data_type = data.get("type", "") if data_type == "msg": - message = data[u"body"] + message = data["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)) + log.debug("message received: {}".format(message)) self.host.bridgeCall( - u"messageSend", + "messageSend", target.full(), - {u"": message}, + {"": message}, {}, mess_type, {}, session.profile, ) else: - log.warning(u"unknown message type: {type}".format(type=data_type)) + log.warning("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_update = {u"msg": data_objects.Message((args))} + template_data_update = {"msg": data_objects.Message((args))} target_jid = rdata["target"] identities = rdata["identities"] uid, timestamp, from_jid_s, to_jid_s, message, subject, mess_type, extra, __ = ( @@ -134,11 +134,11 @@ 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 + "identityGet", from_jid_s, profile ) template_data_update["identities"] = identities self.renderAndUpdate( - request, u"chat/message.html", "#messages", template_data_update + request, "chat/message.html", "#messages", template_data_update ) else: - log.error(_(u"Unexpected signal: {signal}").format(signal=signal)) + log.error(_("Unexpected signal: {signal}").format(signal=signal)) diff -r f14ab8a25e8b -r b2d067339de3 libervia/pages/chat/select/page_meta.py --- a/libervia/pages/chat/select/page_meta.py Tue Aug 13 09:39:33 2019 +0200 +++ b/libervia/pages/chat/select/page_meta.py Tue Aug 13 19:12:31 2019 +0200 @@ -1,4 +1,4 @@ -#!/usr/bin/env python2.7 +#!/usr/bin/env python3 # -*- coding: utf-8 -*- from sat.core.i18n import _ @@ -10,9 +10,9 @@ log = getLogger(__name__) -name = u"chat_select" +name = "chat_select" access = C.PAGES_ACCESS_PROFILE -template = u"chat/select.html" +template = "chat/select.html" @defer.inlineCallbacks @@ -21,17 +21,17 @@ 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) + for bm_values in list(bookmarks.values()): + for room_jid, room_data in bm_values.items(): + url = self.getPageByName("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_: + jid_ = self.getPostedData(request, "jid") + if "@" not in jid_: profile = self.getProfile(request) service = yield self.host.bridgeCall("mucGetService", "", profile) if service: @@ -39,7 +39,7 @@ muc_jid.user = jid_ jid_ = muc_jid.full() else: - log.warning(_(u"Invalid jid received: {jid}".format(jid=jid_))) + log.warning(_("Invalid jid received: {jid}".format(jid=jid_))) defer.returnValue(C.POST_NO_CONFIRM) - url = self.getPageByName(u"chat").getURL(jid_) + url = self.getPageByName("chat").getURL(jid_) self.HTTPRedirect(request, url) diff -r f14ab8a25e8b -r b2d067339de3 libervia/pages/events/admin/page_meta.py --- a/libervia/pages/events/admin/page_meta.py Tue Aug 13 09:39:33 2019 +0200 +++ b/libervia/pages/events/admin/page_meta.py Tue Aug 13 19:12:31 2019 +0200 @@ -1,4 +1,4 @@ -#!/usr/bin/env python2.7 +#!/usr/bin/env python3 # -*- coding: utf-8 -*- from libervia.server.constants import Const as C @@ -12,9 +12,9 @@ import math import re -name = u"event_admin" +name = "event_admin" access = C.PAGES_ACCESS_PROFILE -template = u"event/admin.html" +template = "event/admin.html" log = getLogger(__name__) REG_EMAIL_RE = re.compile(C.REG_EMAIL_RE, re.IGNORECASE) @@ -36,12 +36,12 @@ ## 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"] + event_service = template_data["event_service"] = data["event_service"] + event_node = template_data["event_node"] = data["event_node"] + event_id = template_data["event_id"] = data["event_id"] profile = self.getProfile(request) event_timestamp, event_data = yield self.host.bridgeCall( - u"eventGet", + "eventGet", event_service.userhost() if event_service else "", event_node, event_id, @@ -53,7 +53,7 @@ pass else: template_data["dynamic_style"] = safe( - u""" + """ html { background-image: url("%s"); background-size: 15em; @@ -63,21 +63,21 @@ ) template_data["event"] = event_data invitees = yield self.host.bridgeCall( - u"eventInviteesList", + "eventInviteesList", event_data["invitees_service"], event_data["invitees_node"], profile, ) template_data["invitees"] = invitees invitees_guests = 0 - for invitee_data in invitees.itervalues(): + for invitee_data in invitees.values(): 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) + _("guests value is not valid: {invitee}").format(invitee=invitee_data) ) template_data["invitees_guests"] = invitees_guests template_data["days_left"] = int( @@ -86,13 +86,13 @@ ## 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" + data["service"] = jid.JID(event_data["blog_service"]) + data["node"] = event_data["blog_node"] + data["allow_commenting"] = "simple" # we now need blog items, using blog common page # this will fill the "items" template data - blog_page = self.getPageByName(u"blog_view") + blog_page = self.getPageByName("blog_view") yield blog_page.prepare_render(self, request) @@ -100,46 +100,46 @@ def on_data_post(self, request): profile = self.getProfile(request) if not profile: - log.error(u"got post data without profile") + log.error("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") + request, ("service", "node", "title", "body", "language") ) if not body.strip(): self.pageError(request, C.HTTP_BAD_REQUEST) - data = {u"content": body} + data = {"content": body} if title: - data[u"title"] = title + data["title"] = title if lang: - data[u"language"] = lang + data["language"] = lang try: - comments = bool(self.getPostedData(request, u"comments").strip()) + comments = bool(self.getPostedData(request, "comments").strip()) except KeyError: pass else: if comments: - data[u"allow_comments"] = C.BOOL_TRUE + data["allow_comments"] = C.BOOL_TRUE try: - yield self.host.bridgeCall(u"mbSend", service, node, data, profile) + yield self.host.bridgeCall("mbSend", service, node, data, profile) except Exception as e: - if u"forbidden" in unicode(e): + if "forbidden" in str(e): self.pageError(request, C.HTTP_FORBIDDEN) 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") + request, ("service", "node", "event_id", "jids", "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) + _("this is not a valid jid: {jid}").format(jid=invitee_jid_s) ) continue yield self.host.bridgeCall( @@ -148,7 +148,7 @@ 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( + _("this is not a valid email address: {email}").format( email=email_addr ) ) @@ -160,14 +160,14 @@ event_id, email_addr, {}, - u"", - u"", - u"", - u"", - u"", - u"", + "", + "", + "", + "", + "", + "", profile, ) else: - log.warning(_(u"Unhandled data type: {}").format(type_)) + log.warning(_("Unhandled data type: {}").format(type_)) diff -r f14ab8a25e8b -r b2d067339de3 libervia/pages/events/new/page_meta.py --- a/libervia/pages/events/new/page_meta.py Tue Aug 13 09:39:33 2019 +0200 +++ b/libervia/pages/events/new/page_meta.py Tue Aug 13 19:12:31 2019 +0200 @@ -1,4 +1,4 @@ -#!/usr/bin/env python2.7 +#!/usr/bin/env python3 # -*- coding: utf-8 -*- from libervia.server.constants import Const as C @@ -8,9 +8,9 @@ """creation of new events""" -name = u"event_new" +name = "event_new" access = C.PAGES_ACCESS_PROFILE -template = u"event/create.html" +template = "event/create.html" log = getLogger(__name__) @@ -31,9 +31,9 @@ if not value.startswith("http"): self.pageError(request, C.HTTP_BAD_REQUEST) data[var] = value - data[u"register"] = C.BOOL_TRUE + data["register"] = C.BOOL_TRUE node = yield self.host.bridgeCall("eventCreate", timestamp, data, "", "", "", profile) - log.info(u"Event node created at {node}".format(node=node)) + log.info("Event node created at {node}".format(node=node)) - request_data["post_redirect_page"] = (self.getPageByName(u"event_admin"), "@", node) + request_data["post_redirect_page"] = (self.getPageByName("event_admin"), "@", node) defer.returnValue(C.POST_NO_CONFIRM) diff -r f14ab8a25e8b -r b2d067339de3 libervia/pages/events/page_meta.py --- a/libervia/pages/events/page_meta.py Tue Aug 13 09:39:33 2019 +0200 +++ b/libervia/pages/events/page_meta.py Tue Aug 13 19:12:31 2019 +0200 @@ -1,4 +1,4 @@ -#!/usr/bin/env python2.7 +#!/usr/bin/env python3 # -*- coding: utf-8 -*- from libervia.server.constants import Const as C @@ -9,21 +9,21 @@ log = getLogger(__name__) """ticket handling pages""" -name = u"events" +name = "events" access = C.PAGES_ACCESS_PUBLIC -template = u"event/overview.html" +template = "event/overview.html" @defer.inlineCallbacks def prepare_render(self, request): profile = self.getProfile(request) template_data = request.template_data - template_data[u"url_event_new"] = self.getSubPageURL(request, "event_new") + template_data["url_event_new"] = self.getSubPageURL(request, "event_new") if profile is not None: try: events = yield self.host.bridgeCall("eventsList", "", "", profile) except Exception as e: - log.warning(_(u"Can't get events list for {profile}: {reason}").format( + log.warning(_("Can't get events list for {profile}: {reason}").format( profile=profile, reason=e)) else: own_events = [] @@ -33,7 +33,7 @@ own_events.append(event) event["url"] = self.getSubPageURL( request, - u"event_admin", + "event_admin", event.get("service", ""), event.get("node", ""), event.get("item"), @@ -42,12 +42,12 @@ other_events.append(event) event["url"] = self.getSubPageURL( request, - u"event_rsvp", + "event_rsvp", event.get("service", ""), event.get("node", ""), event.get("item"), ) - if u"thumb_url" not in event and u"image" in event: - event[u"thumb_url"] = event[u"image"] + if "thumb_url" not in event and "image" in event: + event["thumb_url"] = event["image"] - template_data[u"events"] = own_events + other_events + template_data["events"] = own_events + other_events diff -r f14ab8a25e8b -r b2d067339de3 libervia/pages/events/rsvp/page_meta.py --- a/libervia/pages/events/rsvp/page_meta.py Tue Aug 13 09:39:33 2019 +0200 +++ b/libervia/pages/events/rsvp/page_meta.py Tue Aug 13 19:12:31 2019 +0200 @@ -1,4 +1,4 @@ -#!/usr/bin/env python2.7 +#!/usr/bin/env python3 # -*- coding: utf-8 -*- from libervia.server.constants import Const as C @@ -12,9 +12,9 @@ """creation of new events""" -name = u"event_rsvp" +name = "event_rsvp" access = C.PAGES_ACCESS_PROFILE -template = u"event/invitation.html" +template = "event/invitation.html" log = getLogger(__name__) @@ -37,10 +37,10 @@ ## Event ## event_service = data["event_service"] - event_node = data[u"event_node"] - event_id = data[u"event_id"] + event_node = data["event_node"] + event_id = data["event_id"] event_timestamp, event_data = yield self.host.bridgeCall( - u"eventGet", + "eventGet", event_service.userhost() if event_service else "", event_node, event_id, @@ -52,7 +52,7 @@ pass else: template_data["dynamic_style"] = safe( - u""" + """ html { background-image: url("%s"); background-size: 15em; @@ -62,7 +62,7 @@ ) template_data["event"] = event_data event_invitee_data = yield self.host.bridgeCall( - u"eventInviteeGet", + "eventInviteeGet", event_data["invitees_service"], event_data["invitees_node"], profile, @@ -72,28 +72,28 @@ ## 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" + data["service"] = jid.JID(event_data["blog_service"]) + data["node"] = event_data["blog_node"] + data["allow_commenting"] = "simple" # we now need blog items, using blog common page # this will fill the "items" template data - blog_page = self.getPageByName(u"blog_view") + blog_page = self.getPageByName("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") + type_ = self.getPostedData(request, "type") + if type_ == "comment": + blog_page = self.getPageByName("blog_view") yield blog_page.on_data_post(self, request) - elif type_ == u"attendance": + elif type_ == "attendance": profile = self.getProfile(request) service, node, attend, guests = self.getPostedData( - request, (u"service", u"node", u"attend", u"guests") + request, ("service", "node", "attend", "guests") ) - data = {u"attend": attend, u"guests": guests} - yield self.host.bridgeCall(u"eventInviteeSet", service, node, data, profile) + data = {"attend": attend, "guests": guests} + yield self.host.bridgeCall("eventInviteeSet", service, node, data, profile) else: - log.warning(_(u"Unhandled data type: {}").format(type_)) + log.warning(_("Unhandled data type: {}").format(type_)) diff -r f14ab8a25e8b -r b2d067339de3 libervia/pages/events/view/page_meta.py --- a/libervia/pages/events/view/page_meta.py Tue Aug 13 09:39:33 2019 +0200 +++ b/libervia/pages/events/view/page_meta.py Tue Aug 13 19:12:31 2019 +0200 @@ -1,4 +1,4 @@ -#!/usr/bin/env python2.7 +#!/usr/bin/env python3 # -*- coding: utf-8 -*- from libervia.server.constants import Const as C @@ -12,9 +12,9 @@ import cgi from sat.core.log import getLogger -name = u"event_view" +name = "event_view" access = C.PAGES_ACCESS_PROFILE -template = u"event/invitation.html" +template = "event/invitation.html" log = getLogger(__name__) @@ -25,7 +25,7 @@ try: event_uri = guest_session.data["event_uri"] except KeyError: - log.warning(_(u"event URI not found, can't render event page")) + log.warning(_("event URI not found, can't render event page")) self.pageError(request, C.HTTP_SERVICE_UNAVAILABLE) data = self.getRData(request) @@ -33,15 +33,15 @@ ## Event ## event_uri_data = uri.parseXMPPUri(event_uri) - if event_uri_data[u"type"] != u"pubsub": + if event_uri_data["type"] != "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", "") + event_service = template_data["event_service"] = jid.JID(event_uri_data["path"]) + event_node = template_data["event_node"] = event_uri_data["node"] + event_id = template_data["event_id"] = event_uri_data.get("item", "") profile = self.getProfile(request) event_timestamp, event_data = yield self.host.bridgeCall( - u"eventGet", event_service.userhost(), event_node, event_id, profile + "eventGet", event_service.userhost(), event_node, event_id, profile ) try: background_image = event_data.pop("background-image") @@ -49,7 +49,7 @@ pass else: template_data["dynamic_style"] = safe( - u""" + """ html { background-image: url("%s"); background-size: 15em; @@ -59,7 +59,7 @@ ) template_data["event"] = event_data event_invitee_data = yield self.host.bridgeCall( - u"eventInviteeGet", + "eventInviteeGet", event_data["invitees_service"], event_data["invitees_node"], profile, @@ -69,28 +69,28 @@ ## 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" + data["service"] = jid.JID(event_data["blog_service"]) + data["node"] = event_data["blog_node"] + data["allow_commenting"] = "simple" # we now need blog items, using blog common page # this will fill the "items" template data - blog_page = self.getPageByName(u"blog_view") + blog_page = self.getPageByName("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") + type_ = self.getPostedData(request, "type") + if type_ == "comment": + blog_page = self.getPageByName("blog_view") yield blog_page.on_data_post(self, request) - elif type_ == u"attendance": + elif type_ == "attendance": profile = self.getProfile(request) service, node, attend, guests = self.getPostedData( - request, (u"service", u"node", u"attend", u"guests") + request, ("service", "node", "attend", "guests") ) - data = {u"attend": attend, u"guests": guests} - yield self.host.bridgeCall(u"eventInviteeSet", service, node, data, profile) + data = {"attend": attend, "guests": guests} + yield self.host.bridgeCall("eventInviteeSet", service, node, data, profile) else: - log.warning(_(u"Unhandled data type: {}").format(type_)) + log.warning(_("Unhandled data type: {}").format(type_)) diff -r f14ab8a25e8b -r b2d067339de3 libervia/pages/files/list/page_meta.py --- a/libervia/pages/files/list/page_meta.py Tue Aug 13 09:39:33 2019 +0200 +++ b/libervia/pages/files/list/page_meta.py Tue Aug 13 19:12:31 2019 +0200 @@ -1,4 +1,4 @@ -#!/usr/bin/env python2.7 +#!/usr/bin/env python3 # -*- coding: utf-8 -*- from libervia.server.constants import Const as C @@ -14,9 +14,9 @@ log = getLogger(__name__) """files handling pages""" -name = u"files_list" +name = "files_list" access = C.PAGES_ACCESS_PROFILE -template = u"file/overview.html" +template = "file/overview.html" def parse_url(self, request): @@ -28,27 +28,27 @@ 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) + service, path_elts = data["service"], data["path"] + path = "/".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"] + extra_raw = file_data["extra"] except KeyError: pass else: - file_data[u"extra"] = json.loads(extra_raw) if extra_raw else {} + file_data["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: + if file_data["type"] == C.FILE_TYPE_DIRECTORY: page = self - elif file_data[u"type"] == C.FILE_TYPE_FILE: + elif file_data["type"] == C.FILE_TYPE_FILE: page = self.getPageByName("files_view") ## thumbnails ## try: - thumbnails = file_data[u"extra"]["thumbnails"] + thumbnails = file_data["extra"]["thumbnails"] if not thumbnails: raise KeyError except KeyError: @@ -60,16 +60,16 @@ if thumb_data["size"][0] > thumb_limit: break thumb = thumb_data - if u"url" in thumb: + if "url" in thumb: file_data["thumb_url"] = thumb["url"] - elif u"id" in thumb: + elif "id" in thumb: try: thumb_path = yield self.host.bridgeCall( - "bobGetFile", service.full(), thumb[u"id"], profile + "bobGetFile", service.full(), thumb["id"], profile ) except Exception as e: log.warning( - _(u"Can't retrieve thumbnail: {reason}").format(reason=e) + _("Can't retrieve thumbnail: {reason}").format(reason=e) ) else: filename = os.path.basename(thumb_path) @@ -81,28 +81,28 @@ ) else: raise ValueError( - u"unexpected file type: {file_type}".format(file_type=file_data[u"type"]) + "unexpected file type: {file_type}".format(file_type=file_data["type"]) ) - file_data[u"url"] = page.getURL(service.full(), *dir_path) + file_data["url"] = page.getURL(service.full(), *dir_path) ## comments ## - comments_url = file_data.get(u"comments_url") + comments_url = file_data.get("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"] + comments_service = file_data["comments_service"] = parsed_url["path"] + comments_node = file_data["comments_node"] = parsed_url["node"] try: - comments_count = file_data[u"comments_count"] = int( + comments_count = file_data["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( + file_data["comments"] = yield pages_tools.retrieveComments( self, comments_service, comments_node, profile=profile ) - template_data[u"files_data"] = files_data - template_data[u"path"] = path + template_data["files_data"] = files_data + template_data["path"] = path if path_elts: - template_data[u"parent_url"] = self.getURL(service.full(), *path_elts[:-1]) + template_data["parent_url"] = self.getURL(service.full(), *path_elts[:-1]) diff -r f14ab8a25e8b -r b2d067339de3 libervia/pages/files/page_meta.py --- a/libervia/pages/files/page_meta.py Tue Aug 13 09:39:33 2019 +0200 +++ b/libervia/pages/files/page_meta.py Tue Aug 13 19:12:31 2019 +0200 @@ -1,4 +1,4 @@ -#!/usr/bin/env python2.7 +#!/usr/bin/env python3 # -*- coding: utf-8 -*- from libervia.server.constants import Const as C @@ -9,9 +9,9 @@ log = getLogger(__name__) """files handling pages""" -name = u"files" +name = "files" access = C.PAGES_ACCESS_PROFILE -template = u"file/discover.html" +template = "file/discover.html" @defer.inlineCallbacks @@ -34,7 +34,7 @@ (tpl_own_entities, entities_own), (tpl_roster_entities, entities_roster), ): - for entity_str, entity_ids in entities_map.iteritems(): + for entity_str, entity_ids in entities_map.items(): entity_jid = jid.JID(entity_str) tpl_entities[entity_jid] = identities = {} for cat, type_, name in entity_ids: @@ -45,10 +45,10 @@ def on_data_post(self, request): - jid_str = self.getPostedData(request, u"jid") + jid_str = self.getPostedData(request, "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()) + url = self.getPageByName("files_list").getURL(jid_.full()) self.HTTPRedirect(request, url) diff -r f14ab8a25e8b -r b2d067339de3 libervia/pages/files/view/page_meta.py --- a/libervia/pages/files/view/page_meta.py Tue Aug 13 09:39:33 2019 +0200 +++ b/libervia/pages/files/view/page_meta.py Tue Aug 13 19:12:31 2019 +0200 @@ -1,4 +1,4 @@ -#!/usr/bin/env python2.7 +#!/usr/bin/env python3 # -*- coding: utf-8 -*- from libervia.server.constants import Const as C @@ -14,7 +14,7 @@ log = getLogger(__name__) """files handling pages""" -name = u"files_view" +name = "files_view" access = C.PAGES_ACCESS_PROFILE @@ -26,21 +26,21 @@ try: os.unlink(dest_path) except OSError: - log.warning(_(u"Can't remove temporary file {path}").format(path=dest_path)) + log.warning(_("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)) + log.warning(_("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"] + service, path_elts = data["service"], data["path"] basename = path_elts[-1] dir_elts = path_elts[:-1] - dir_path = u"/".join(dir_elts) + dir_path = "/".join(dir_elts) tmp_dir = tempfile.mkdtemp() dest_path = os.path.join(tmp_dir, basename) request.notifyFinish().addCallback(cleanup, tmp_dir, dest_path) @@ -49,12 +49,12 @@ service.full(), dest_path, basename, - u"", - u"", - {u"path": dir_path}, + "", + "", + {"path": dir_path}, profile, ) - log.debug(u"file requested") + log.debug("file requested") yield ProgressHandler(self.host, progress_id, profile).register() - log.debug(u"file downloaded") + log.debug("file downloaded") self.delegateToResource(request, static.File(dest_path)) diff -r f14ab8a25e8b -r b2d067339de3 libervia/pages/forums/list/page_meta.py --- a/libervia/pages/forums/list/page_meta.py Tue Aug 13 09:39:33 2019 +0200 +++ b/libervia/pages/forums/list/page_meta.py Tue Aug 13 19:12:31 2019 +0200 @@ -1,4 +1,4 @@ -#!/usr/bin/env python2.7 +#!/usr/bin/env python3 # -*- coding: utf-8 -*- from libervia.server.constants import Const as C @@ -12,18 +12,18 @@ """forum handling pages""" -name = u"forums" +name = "forums" access = C.PAGES_ACCESS_PUBLIC -template = u"forum/overview.html" +template = "forum/overview.html" def parse_url(self, request): self.getPathArgs( request, ["service", "node", "forum_key"], - service=u"@jid", - node=u"@", - forum_key=u"", + service="@jid", + node="@", + forum_key="", ) @@ -35,29 +35,29 @@ 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"]) + service = uri["path"] + node = uri["node"] + forum["http_url"] = self.getPageByName("forum_topics").getURL(service, node) + if "sub-forums" in forum: + getLinks(self, forum["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"] + service, node, key = data["service"], data["node"], data["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 + "forumsGet", service.full() if service else "", node, key, profile ) except Exception as e: - log.warning(_(u"Can't retrieve forums: {msg}").format(msg=e)) + log.warning(_("Can't retrieve forums: {msg}").format(msg=e)) forums = [] else: forums = json.loads(forums_raw) getLinks(self, forums) - template_data[u"forums"] = forums + template_data["forums"] = forums diff -r f14ab8a25e8b -r b2d067339de3 libervia/pages/forums/topics/page_meta.py --- a/libervia/pages/forums/topics/page_meta.py Tue Aug 13 09:39:33 2019 +0200 +++ b/libervia/pages/forums/topics/page_meta.py Tue Aug 13 19:12:31 2019 +0200 @@ -1,4 +1,4 @@ -#!/usr/bin/env python2.7 +#!/usr/bin/env python3 # -*- coding: utf-8 -*- from libervia.server.constants import Const as C @@ -9,37 +9,37 @@ log = getLogger(__name__) -name = u"forum_topics" +name = "forum_topics" access = C.PAGES_ACCESS_PUBLIC -template = u"forum/view_topics.html" +template = "forum/view_topics.html" def parse_url(self, request): - self.getPathArgs(request, ["service", "node"], 2, service=u"jid") + self.getPathArgs(request, ["service", "node"], 2, service="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}) + service, node = data["service"], data["node"] + request.template_data.update({"service": service, "node": node}) template_data = request.template_data topics, metadata = yield self.host.bridgeCall( - u"forumTopicsGet", service.full(), node, {}, profile + "forumTopicsGet", service.full(), node, {}, profile ) - template_data[u"identities"] = identities = {} + template_data["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"] + parsed_uri = xmpp_uri.parseXMPPUri(topic["uri"]) + author = topic["author"] + topic["http_uri"] = self.getPageByName("forum_view").getURL( + parsed_uri["path"], parsed_uri["node"] ) if author not in identities: - identities[topic[u"author"]] = yield self.host.bridgeCall( - u"identityGet", author, profile + identities[topic["author"]] = yield self.host.bridgeCall( + "identityGet", author, profile ) - template_data[u"topics"] = topics + template_data["topics"] = topics @defer.inlineCallbacks @@ -47,23 +47,23 @@ profile = self.getProfile(request) if profile is None: self.pageError(request, C.HTTP_FORBIDDEN) - type_ = self.getPostedData(request, u"type") - if type_ == u"new_topic": + type_ = self.getPostedData(request, "type") + if type_ == "new_topic": service, node, title, body = self.getPostedData( - request, (u"service", u"node", u"title", u"body") + request, ("service", "node", "title", "body") ) if not title or not body: self.pageError(request, C.HTTP_BAD_REQUEST) - topic_data = {u"title": title, u"content": body} + topic_data = {"title": title, "content": body} try: yield self.host.bridgeCall( - u"forumTopicCreate", service, node, topic_data, profile + "forumTopicCreate", service, node, topic_data, profile ) except Exception as e: - if u"forbidden" in unicode(e): + if "forbidden" in str(e): self.pageError(request, 401) else: raise e else: - log.warning(_(u"Unhandled data type: {}").format(type_)) + log.warning(_("Unhandled data type: {}").format(type_)) diff -r f14ab8a25e8b -r b2d067339de3 libervia/pages/forums/view/page_meta.py --- a/libervia/pages/forums/view/page_meta.py Tue Aug 13 09:39:33 2019 +0200 +++ b/libervia/pages/forums/view/page_meta.py Tue Aug 13 19:12:31 2019 +0200 @@ -1,4 +1,4 @@ -#!/usr/bin/env python2.7 +#!/usr/bin/env python3 # -*- coding: utf-8 -*- from libervia.server.constants import Const as C @@ -8,24 +8,24 @@ log = getLogger(__name__) -name = u"forum_view" +name = "forum_view" access = C.PAGES_ACCESS_PUBLIC -template = u"forum/view.html" +template = "forum/view.html" def parse_url(self, request): - self.getPathArgs(request, ["service", "node"], 2, service=u"jid") + self.getPathArgs(request, ["service", "node"], 2, service="jid") @defer.inlineCallbacks def prepare_render(self, request): data = self.getRData(request) data["show_comments"] = False - blog_page = self.getPageByName(u"blog_view") + blog_page = self.getPageByName("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) + request.template_data["login_url"] = self.getPageRedirectURL(request) @defer.inlineCallbacks @@ -33,19 +33,19 @@ profile = self.getProfile(request) if profile is None: self.pageError(request, C.HTTP_FORBIDDEN) - type_ = self.getPostedData(request, u"type") - if type_ == u"comment": - service, node, body = self.getPostedData(request, (u"service", u"node", u"body")) + type_ = self.getPostedData(request, "type") + if type_ == "comment": + service, node, body = self.getPostedData(request, ("service", "node", "body")) if not body: self.pageError(request, C.HTTP_BAD_REQUEST) - mb_data = {u"content": body} + mb_data = {"content": body} try: - yield self.host.bridgeCall(u"mbSend", service, node, mb_data, profile) + yield self.host.bridgeCall("mbSend", service, node, mb_data, profile) except Exception as e: - if u"forbidden" in unicode(e): + if "forbidden" in str(e): self.pageError(request, 401) else: raise e else: - log.warning(_(u"Unhandled data type: {}").format(type_)) + log.warning(_("Unhandled data type: {}").format(type_)) diff -r f14ab8a25e8b -r b2d067339de3 libervia/pages/g/e/page_meta.py --- a/libervia/pages/g/e/page_meta.py Tue Aug 13 09:39:33 2019 +0200 +++ b/libervia/pages/g/e/page_meta.py Tue Aug 13 19:12:31 2019 +0200 @@ -1,4 +1,4 @@ -#!/usr/bin/env python2.7 +#!/usr/bin/env python3 # -*- coding: utf-8 -*- -redirect = u"event_view" +redirect = "event_view" diff -r f14ab8a25e8b -r b2d067339de3 libervia/pages/g/page_meta.py --- a/libervia/pages/g/page_meta.py Tue Aug 13 09:39:33 2019 +0200 +++ b/libervia/pages/g/page_meta.py Tue Aug 13 19:12:31 2019 +0200 @@ -1,4 +1,4 @@ -#!/usr/bin/env python2.7 +#!/usr/bin/env python3 # -*- coding: utf-8 -*- from libervia.server.constants import Const as C @@ -10,7 +10,7 @@ log = getLogger(__name__) access = C.PAGES_ACCESS_PUBLIC -template = u"invitation/welcome.html" +template = "invitation/welcome.html" @defer.inlineCallbacks @@ -32,7 +32,7 @@ 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}]" + "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) @@ -46,7 +46,7 @@ if profile is not None and current_id is None: log.info( _( - u"killing current profile session [{profile}] because a guest id is used" + "killing current profile session [{profile}] because a guest id is used" ).format(profile=profile) ) self.host.purgeSession(request) @@ -56,7 +56,7 @@ profile = None if current_id is None: - log.debug(_(u"checking invitation [{id}]").format(id=invitation_id)) + log.debug(_("checking invitation [{id}]").format(id=invitation_id)) try: data = yield self.host.bridgeCall("invitationGet", invitation_id) except Exception: @@ -68,21 +68,21 @@ data = guest_session.data if profile is None: - log.debug(_(u"connecting profile [{}]").format(profile)) + log.debug(_("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)) + log.warning(_("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( + "guest session started, connected with profile [{profile}]".format( profile=profile ) ) @@ -91,16 +91,16 @@ # 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"] + if "name" in data: + template_data["name"] = data["name"] + if "language" in data: + template_data["locale"] = data["language"] def handleEventInterest(self, interest): if C.bool(interest.get("creator", C.BOOL_FALSE)): - page_name = u"event_admin" + page_name = "event_admin" else: - page_name = u"event_rsvp" + page_name = "event_rsvp" interest["url"] = self.getPageByName(page_name).getURL( interest.get("service", ""), @@ -108,24 +108,24 @@ interest.get("item"), ) - if u"thumb_url" not in interest and u"image" in interest: - interest[u"thumb_url"] = interest[u"image"] + if "thumb_url" not in interest and "image" in interest: + interest["thumb_url"] = interest["image"] def handleFISInterest(self, interest): - path = interest.get(u'path', u'') - path_args = [p for p in path.split(u'/') if p] - subtype = interest.get(u'subtype') + path = interest.get('path', '') + path_args = [p for p in path.split('/') if p] + subtype = interest.get('subtype') - if subtype == u'files': - page_name = u"files_view" - elif interest.get(u'subtype') == u'photos': - page_name = u"photos_album" + if subtype == 'files': + page_name = "files_view" + elif interest.get('subtype') == 'photos': + page_name = "photos_album" else: - log.warning(u"unknown interest subtype: {subtype}".format(subtype=subtype)) + log.warning("unknown interest subtype: {subtype}".format(subtype=subtype)) return False interest["url"] = self.getPageByName(page_name).getURL( - interest[u'service'], *path_args) + interest['service'], *path_args) @defer.inlineCallbacks def prepare_render(self, request): @@ -137,17 +137,17 @@ interests = yield self.host.bridgeCall( "interestsList", "", "", "", profile) except Exception: - log.warning(_(u"Can't get interests list for {profile}").format( + log.warning(_("Can't get interests list for {profile}").format( profile=profile)) else: # we only want known interests (photos and events for now) # this dict map namespaces of interest to a callback which can manipulate # the data. If it returns False, the interest is skipped ns_data = {} - template_data[u'interests_map'] = interests_map = {} + template_data['interests_map'] = interests_map = {} - for short_name, cb in ((u'event', handleEventInterest), - (u'fis', handleFISInterest), + for short_name, cb in (('event', handleEventInterest), + ('fis', handleFISInterest), ): try: namespace = self.host.ns_map[short_name] @@ -157,13 +157,13 @@ ns_data[namespace] = (cb, short_name) for interest in interests: - namespace = interest.get(u'namespace') + namespace = interest.get('namespace') if namespace not in ns_data: continue cb, short_name = ns_data[namespace] if cb(self, interest) == False: continue - key = interest.get(u'subtype', short_name) + key = interest.get('subtype', short_name) interests_map.setdefault(key, []).append(interest) # main URI @@ -172,4 +172,4 @@ if main_uri: include_url = self.getPagePathFromURI(main_uri) if include_url is not None: - template_data[u"include_url"] = include_url + template_data["include_url"] = include_url diff -r f14ab8a25e8b -r b2d067339de3 libervia/pages/login/logged/page_meta.py --- a/libervia/pages/login/logged/page_meta.py Tue Aug 13 09:39:33 2019 +0200 +++ b/libervia/pages/login/logged/page_meta.py Tue Aug 13 19:12:31 2019 +0200 @@ -1,4 +1,4 @@ -#!/usr/bin/env python2.7 +#!/usr/bin/env python3 # -*- coding: utf-8 -*- from libervia.server import session_iface @@ -8,7 +8,7 @@ """SàT log-in page, with link to create an account""" -template = u"login/logged.html" +template = "login/logged.html" def prepare_render(self, request): diff -r f14ab8a25e8b -r b2d067339de3 libervia/pages/login/page_meta.py --- a/libervia/pages/login/page_meta.py Tue Aug 13 09:39:33 2019 +0200 +++ b/libervia/pages/login/page_meta.py Tue Aug 13 19:12:31 2019 +0200 @@ -1,4 +1,4 @@ -#!/usr/bin/env python2.7 +#!/usr/bin/env python3 # -*- coding: utf-8 -*- from sat.core.i18n import _ @@ -12,9 +12,9 @@ """SàT log-in page, with link to create an account""" -name = u"login" +name = "login" access = C.PAGES_ACCESS_PUBLIC -template = u"login/login.html" +template = "login/login.html" def prepare_render(self, request): @@ -59,18 +59,18 @@ type_ = self.getPostedData(request, "type") if type_ == "disconnect": if profile is None: - log.warning(_(u"Disconnect called when no profile is logged")) + log.warning(_("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")) + login, password = self.getPostedData(request, ("login", "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)) + if str(e) in (C.XMPP_AUTH_ERROR, C.PROFILE_AUTH_ERROR): + defer.returnValue(login_error(self, request, str(e))) else: # this error was not expected! raise e @@ -81,6 +81,6 @@ # Profile has been logged correctly self.redirectOrContinue(request) else: - log.error(_(u"Unhandled status: {status}".format(status=status))) + log.error(_("Unhandled status: {status}".format(status=status))) else: self.pageError(request, C.HTTP_BAD_REQUEST) diff -r f14ab8a25e8b -r b2d067339de3 libervia/pages/merge-requests/disco/page_meta.py --- a/libervia/pages/merge-requests/disco/page_meta.py Tue Aug 13 09:39:33 2019 +0200 +++ b/libervia/pages/merge-requests/disco/page_meta.py Tue Aug 13 19:12:31 2019 +0200 @@ -1,4 +1,4 @@ -#!/usr/bin/env python2.7 +#!/usr/bin/env python3 # -*- coding: utf-8 -*- from libervia.server.constants import Const as C @@ -8,9 +8,9 @@ log = getLogger(__name__) """ticket handling pages""" -name = u"merge-requests_disco" +name = "merge-requests_disco" access = C.PAGES_ACCESS_PUBLIC -template = u"merge-request/discover.html" +template = "merge-request/discover.html" def prepare_render(self, request): @@ -19,23 +19,23 @@ 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}) + service = handler_data["service"] + node = handler_data["node"] + name = handler_data["name"] + url = self.getPageByName("merge-requests").getURL(service, node) + handlers.append({"name": name, "url": url}) except KeyError as e: - log.warning(u"Missing field in mr_handlers_json: {msg}".format(msg=e)) + log.warning("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)) + log.warning("Can't decode mr handlers: {msg}".format(msg=e)) def on_data_post(self, request): - jid_str = self.getPostedData(request, u"jid") + jid_str = self.getPostedData(request, "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"@") + url = self.getPageByName("merge-requests").getURL(jid_.full(), "@") self.HTTPRedirect(request, url) diff -r f14ab8a25e8b -r b2d067339de3 libervia/pages/merge-requests/edit/page_meta.py --- a/libervia/pages/merge-requests/edit/page_meta.py Tue Aug 13 09:39:33 2019 +0200 +++ b/libervia/pages/merge-requests/edit/page_meta.py Tue Aug 13 19:12:31 2019 +0200 @@ -1,4 +1,4 @@ -#!/usr/bin/env python2.7 +#!/usr/bin/env python3 # -*- coding: utf-8 -*- from libervia.server.constants import Const as C @@ -10,9 +10,9 @@ """merge-requests edition""" -name = u"merge-requests_edit" +name = "merge-requests_edit" access = C.PAGES_ACCESS_PROFILE -template = u"merge-request/edit.html" +template = "merge-request/edit.html" log = getLogger(__name__) @@ -20,11 +20,11 @@ try: item_id = self.nextPath(request) except IndexError: - log.warning(_(u"no ticket id specified")) + log.warning(_("no ticket id specified")) self.pageError(request, C.HTTP_BAD_REQUEST) data = self.getRData(request) - data[u"ticket_id"] = item_id + data["ticket_id"] = item_id @defer.inlineCallbacks @@ -32,9 +32,9 @@ 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"], + data.get("service", ""), + data.get("node", ""), + data["ticket_id"], ) profile = self.getProfile(request) @@ -51,7 +51,7 @@ ) tickets = yield self.host.bridgeCall( "mergeRequestsGet", - service.full() if service else u"", + service.full() if service else "", node, C.NO_LIMIT, [ticket_id], @@ -63,22 +63,22 @@ 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", ", " + ticket.widgets["labels"].type = "string" + ticket.widgets["labels"].value = ticket.widgets["labels"].value.replace( + "\n", ", " ) except KeyError: pass # same as tickets_edit - wid = ticket.widgets[u'body'] - if wid.type == u"xhtmlbox": - wid.type = u"textbox" + wid = ticket.widgets['body'] + if wid.type == "xhtmlbox": + wid.type = "textbox" wid.value = yield self.host.bridgeCall( - u"syntaxConvert", wid.value, C.SYNTAX_XHTML, u"markdown", + "syntaxConvert", wid.value, C.SYNTAX_XHTML, "markdown", False, profile) - template_data[u"new_ticket_xmlui"] = ticket + template_data["new_ticket_xmlui"] = ticket @defer.inlineCallbacks @@ -98,27 +98,27 @@ # we convert back body to XHTML body = yield self.host.bridgeCall( - u"syntaxConvert", posted_data[u'body'][0], u"markdown", C.SYNTAX_XHTML, + "syntaxConvert", posted_data['body'][0], "markdown", C.SYNTAX_XHTML, False, profile) - posted_data[u'body'] = [u'
    {body}
    '.format(ns=C.NS_XHTML, + posted_data['body'] = ['
    {body}
    '.format(ns=C.NS_XHTML, body=body)] - extra = {u'update': True} + extra = {'update': True} yield self.host.bridgeCall( "mergeRequestSet", service.full(), node, - u"", - u"auto", + "", + "auto", posted_data, - u"", + "", ticket_id, data_format.serialise(extra), 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"), + self.getPageByName("merge-requests"), service.full(), - node or u"@", + node or "@", ) diff -r f14ab8a25e8b -r b2d067339de3 libervia/pages/merge-requests/new/page_meta.py --- a/libervia/pages/merge-requests/new/page_meta.py Tue Aug 13 09:39:33 2019 +0200 +++ b/libervia/pages/merge-requests/new/page_meta.py Tue Aug 13 19:12:31 2019 +0200 @@ -1,4 +1,4 @@ -#!/usr/bin/env python2.7 +#!/usr/bin/env python3 # -*- coding: utf-8 -*- from libervia.server.constants import Const as C @@ -7,6 +7,6 @@ log = getLogger(__name__) """ticket handling pages""" -name = u"merge-requests_new" +name = "merge-requests_new" access = C.PAGES_ACCESS_PUBLIC -template = u"merge-request/create.html" +template = "merge-request/create.html" diff -r f14ab8a25e8b -r b2d067339de3 libervia/pages/merge-requests/page_meta.py --- a/libervia/pages/merge-requests/page_meta.py Tue Aug 13 09:39:33 2019 +0200 +++ b/libervia/pages/merge-requests/page_meta.py Tue Aug 13 19:12:31 2019 +0200 @@ -1,4 +1,4 @@ -#!/usr/bin/env python2.7 +#!/usr/bin/env python3 # -*- coding: utf-8 -*- from libervia.server.constants import Const as C @@ -10,39 +10,39 @@ log = getLogger(__name__) """ticket handling pages""" -name = u"merge-requests" +name = "merge-requests" access = C.PAGES_ACCESS_PUBLIC -template = u"ticket/overview.html" +template = "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"] + service, node = data["service"], data["node"] if node is None: - self.pageRedirect(u"merge-requests_disco", request) - if node == u"@": - node = data[u"node"] = u"" + self.pageRedirect("merge-requests_disco", request) + if node == "@": + node = data["node"] = "" 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( + template_data["url_tickets_list"] = self.getPageByName("merge-requests").getURL( service.full(), node ) - template_data[u"url_tickets_new"] = self.getSubPageURL(request, "merge-requests_new") + template_data["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"] + service, node = data["service"], data["node"] profile = self.getProfile(request) or C.SERVICE_PROFILE merge_requests = yield self.host.bridgeCall( "mergeRequestsGet", - service.full() if service else u"", + service.full() if service else "", node, C.NO_LIMIT, [], @@ -50,9 +50,9 @@ {"labels_as_list": C.BOOL_TRUE}, profile, ) - template_data[u"tickets"] = [ + template_data["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}" + template_data["on_ticket_click"] = data_objects.OnClick( + url=self.getSubPageURL(request, "merge-requests_view") + "/{item.id}" ) diff -r f14ab8a25e8b -r b2d067339de3 libervia/pages/merge-requests/view/page_meta.py --- a/libervia/pages/merge-requests/view/page_meta.py Tue Aug 13 09:39:33 2019 +0200 +++ b/libervia/pages/merge-requests/view/page_meta.py Tue Aug 13 19:12:31 2019 +0200 @@ -1,4 +1,4 @@ -#!/usr/bin/env python2.7 +#!/usr/bin/env python3 # -*- coding: utf-8 -*- from libervia.server.constants import Const as C @@ -12,9 +12,9 @@ from sat.tools.common import data_objects from sat.core.log import getLogger -name = u"merge-requests_view" +name = "merge-requests_view" access = C.PAGES_ACCESS_PUBLIC -template = u"merge-request/item.html" +template = "merge-request/item.html" log = getLogger(__name__) @@ -22,11 +22,11 @@ try: item_id = self.nextPath(request) except IndexError: - log.warning(_(u"no ticket id specified")) + log.warning(_("no ticket id specified")) self.pageError(request, C.HTTP_BAD_REQUEST) data = self.getRData(request) - data[u"ticket_id"] = item_id + data["ticket_id"] = item_id @defer.inlineCallbacks @@ -35,9 +35,9 @@ 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"], + data.get("service", ""), + data.get("node", ""), + data["ticket_id"], ) profile = self.getProfile(request) @@ -46,7 +46,7 @@ tickets, metadata, parsed_tickets = yield self.host.bridgeCall( "mergeRequestsGet", - service.full() if service else u"", + service.full() if service else "", node, C.NO_LIMIT, [ticket_id], @@ -55,7 +55,7 @@ profile, ) ticket = template_xmlui.create(self.host, tickets[0], ignore=["request_data", "type"]) - template_data[u"item"] = ticket + template_data["item"] = ticket template_data["patches"] = parsed_tickets[0] comments_uri = ticket.widgets["comments_uri"].value if comments_uri: @@ -66,8 +66,8 @@ "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) + template_data["comments"] = data_objects.BlogItems(comments) + template_data["login_url"] = self.getPageRedirectURL(request) if session.connected: # we set edition URL only if user is the publisher or the node owner @@ -78,10 +78,10 @@ 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( + template_data["url_ticket_edit"] = self.getURLByPath( SubPage("merge-requests"), service.full(), - node or u"@", + node or "@", SubPage("merge-requests_edit"), ticket_id, ) @@ -89,9 +89,9 @@ @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") + type_ = self.getPostedData(request, "type") + if type_ == "comment": + blog_page = self.getPageByName("blog_view") yield blog_page.on_data_post(self, request) else: - log.warning(_(u"Unhandled data type: {}").format(type_)) + log.warning(_("Unhandled data type: {}").format(type_)) diff -r f14ab8a25e8b -r b2d067339de3 libervia/pages/photos/album/page_meta.py --- a/libervia/pages/photos/album/page_meta.py Tue Aug 13 09:39:33 2019 +0200 +++ b/libervia/pages/photos/album/page_meta.py Tue Aug 13 19:12:31 2019 +0200 @@ -1,4 +1,4 @@ -#!/usr/bin/env python2.7 +#!/usr/bin/env python3 # -*- coding: utf-8 -*- from libervia.server.constants import Const as C @@ -6,9 +6,9 @@ log = getLogger(__name__) -name = u"photos_album" +name = "photos_album" access = C.PAGES_ACCESS_PROFILE -template = u"photo/album.html" +template = "photo/album.html" def parse_url(self, request): @@ -19,10 +19,10 @@ data = self.getRData(request) data["thumb_limit"] = 1200 data["retrieve_comments"] = True - files_page = self.getPageByName(u"files_list") + files_page = self.getPageByName("files_list") return files_page.prepare_render(self, request) def on_data_post(self, request): - blog_page = self.getPageByName(u"blog_view") + blog_page = self.getPageByName("blog_view") return blog_page.on_data_post(self, request) diff -r f14ab8a25e8b -r b2d067339de3 libervia/pages/photos/page_meta.py --- a/libervia/pages/photos/page_meta.py Tue Aug 13 09:39:33 2019 +0200 +++ b/libervia/pages/photos/page_meta.py Tue Aug 13 19:12:31 2019 +0200 @@ -1,4 +1,4 @@ -#!/usr/bin/env python2.7 +#!/usr/bin/env python3 # -*- coding: utf-8 -*- from libervia.server.constants import Const as C @@ -8,9 +8,9 @@ log = getLogger(__name__) -name = u"photos" +name = "photos" access = C.PAGES_ACCESS_PROFILE -template = u"photo/discover.html" +template = "photo/discover.html" @defer.inlineCallbacks @@ -23,29 +23,29 @@ interests = yield self.host.bridgeCall( "interestsList", "", "", namespace, profile) except Exception: - log.warning(_(u"Can't get interests list for {profile}").format( + log.warning(_("Can't get interests list for {profile}").format( profile=profile)) else: # we only want photo albums filtered_interests = [] for interest in interests: - if interest.get(u'subtype') != u'photos': + if interest.get('subtype') != 'photos': continue - path = interest.get(u'path', u'') - path_args = [p for p in path.split(u'/') if p] + path = interest.get('path', '') + path_args = [p for p in path.split('/') if p] interest["url"] = self.getSubPageURL( request, - u"photos_album", - interest[u'service'], + "photos_album", + interest['service'], *path_args ) filtered_interests.append(interest) - template_data[u'interests'] = filtered_interests + template_data['interests'] = filtered_interests @defer.inlineCallbacks def on_data_post(self, request): - jid_ = self.getPostedData(request, u"jid") - url = self.getPageByName(u"photos_album").getURL(jid_) + jid_ = self.getPostedData(request, "jid") + url = self.getPageByName("photos_album").getURL(jid_) self.HTTPRedirect(request, url) diff -r f14ab8a25e8b -r b2d067339de3 libervia/pages/register/page_meta.py --- a/libervia/pages/register/page_meta.py Tue Aug 13 09:39:33 2019 +0200 +++ b/libervia/pages/register/page_meta.py Tue Aug 13 19:12:31 2019 +0200 @@ -1,4 +1,4 @@ -#!/usr/bin/env python2.7 +#!/usr/bin/env python3 # -*- coding: utf-8 -*- from libervia.server.constants import Const as C @@ -10,9 +10,9 @@ """SàT account registration page""" -name = u"register" +name = "register" access = C.PAGES_ACCESS_PUBLIC -template = u"login/register.html" +template = "login/register.html" def prepare_render(self, request): @@ -30,30 +30,30 @@ template_data["login_error"] = login_error #  if fields were already filled, we reuse them - for k in (u"login", u"email", u"password"): + for k in ("login", "email", "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": + type_ = self.getPostedData(request, "type") + if type_ == "register": login, email, password = self.getPostedData( - request, (u"login", u"email", u"password") + request, ("login", "email", "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) + session_data.setPageData(self.getPageByName("login"), "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) + self.HTTPRedirect(request, self.getPageByName("login").url) else: - session_data.setPageData(self, u"login_error", status) + session_data.setPageData(self, "login_error", status) l = locals() - for k in (u"login", u"email", u"password"): + for k in ("login", "email", "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) diff -r f14ab8a25e8b -r b2d067339de3 libervia/pages/tickets/disco/page_meta.py --- a/libervia/pages/tickets/disco/page_meta.py Tue Aug 13 09:39:33 2019 +0200 +++ b/libervia/pages/tickets/disco/page_meta.py Tue Aug 13 19:12:31 2019 +0200 @@ -1,4 +1,4 @@ -#!/usr/bin/env python2.7 +#!/usr/bin/env python3 # -*- coding: utf-8 -*- from libervia.server.constants import Const as C @@ -8,9 +8,9 @@ log = getLogger(__name__) """ticket handling pages""" -name = u"tickets_disco" +name = "tickets_disco" access = C.PAGES_ACCESS_PUBLIC -template = u"ticket/discover.html" +template = "ticket/discover.html" def prepare_render(self, request): @@ -19,23 +19,23 @@ 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}) + service = tracker_data["service"] + node = tracker_data["node"] + name = tracker_data["name"] + url = self.getPageByName("tickets").getURL(service, node) + trackers.append({"name": name, "url": url}) except KeyError as e: - log.warning(u"Missing field in tickets_trackers_json: {msg}".format(msg=e)) + log.warning("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)) + log.warning("Can't decode tickets trackers: {msg}".format(msg=e)) def on_data_post(self, request): - jid_str = self.getPostedData(request, u"jid") + jid_str = self.getPostedData(request, "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"@") + url = self.getPageByName("tickets").getURL(jid_.full(), "@") self.HTTPRedirect(request, url) diff -r f14ab8a25e8b -r b2d067339de3 libervia/pages/tickets/edit/page_meta.py --- a/libervia/pages/tickets/edit/page_meta.py Tue Aug 13 09:39:33 2019 +0200 +++ b/libervia/pages/tickets/edit/page_meta.py Tue Aug 13 19:12:31 2019 +0200 @@ -1,4 +1,4 @@ -#!/usr/bin/env python2.7 +#!/usr/bin/env python3 # -*- coding: utf-8 -*- from libervia.server.constants import Const as C @@ -11,20 +11,20 @@ log = getLogger(__name__) """ticket handling pages""" -name = u"tickets_edit" +name = "tickets_edit" access = C.PAGES_ACCESS_PROFILE -template = u"ticket/edit.html" +template = "ticket/edit.html" def parse_url(self, request): try: item_id = self.nextPath(request) except IndexError: - log.warning(_(u"no ticket id specified")) + log.warning(_("no ticket id specified")) self.pageError(request, C.HTTP_BAD_REQUEST) data = self.getRData(request) - data[u"ticket_id"] = item_id + data["ticket_id"] = item_id @defer.inlineCallbacks @@ -32,9 +32,9 @@ 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"], + data.get("service", ""), + data.get("node", ""), + data["ticket_id"], ) profile = self.getProfile(request) @@ -51,7 +51,7 @@ ) tickets = yield self.host.bridgeCall( "ticketsGet", - service.full() if service else u"", + service.full() if service else "", node, C.NO_LIMIT, [ticket_id], @@ -63,23 +63,23 @@ 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", ", " + ticket.widgets["labels"].type = "string" + ticket.widgets["labels"].value = ticket.widgets["labels"].value.replace( + "\n", ", " ) except KeyError: pass # for now we don't have XHTML editor, so we'll go with a TextBox and a convertion # to a text friendly syntax using markdown - wid = ticket.widgets[u'body'] - if wid.type == u"xhtmlbox": - wid.type = u"textbox" + wid = ticket.widgets['body'] + if wid.type == "xhtmlbox": + wid.type = "textbox" wid.value = yield self.host.bridgeCall( - u"syntaxConvert", wid.value, C.SYNTAX_XHTML, u"markdown", + "syntaxConvert", wid.value, C.SYNTAX_XHTML, "markdown", False, profile) - template_data[u"new_ticket_xmlui"] = ticket + template_data["new_ticket_xmlui"] = ticket @defer.inlineCallbacks @@ -99,19 +99,19 @@ # we convert back body to XHTML body = yield self.host.bridgeCall( - u"syntaxConvert", posted_data[u'body'][0], u"markdown", C.SYNTAX_XHTML, + "syntaxConvert", posted_data['body'][0], "markdown", C.SYNTAX_XHTML, False, profile) - posted_data[u'body'] = [u'
    {body}
    '.format(ns=C.NS_XHTML, + posted_data['body'] = ['
    {body}
    '.format(ns=C.NS_XHTML, body=body)] - extra = {u'update': True} + extra = {'update': True} yield self.host.bridgeCall( - "ticketSet", service.full(), node, posted_data, u"", ticket_id, + "ticketSet", service.full(), node, posted_data, "", ticket_id, data_format.serialise(extra), profile ) # we don't want to redirect to edit page on success, but to tickets list data["post_redirect_page"] = ( - self.getPageByName(u"tickets"), + self.getPageByName("tickets"), service.full(), - node or u"@", + node or "@", ) diff -r f14ab8a25e8b -r b2d067339de3 libervia/pages/tickets/new/page_meta.py --- a/libervia/pages/tickets/new/page_meta.py Tue Aug 13 09:39:33 2019 +0200 +++ b/libervia/pages/tickets/new/page_meta.py Tue Aug 13 19:12:31 2019 +0200 @@ -1,4 +1,4 @@ -#!/usr/bin/env python2.7 +#!/usr/bin/env python3 # -*- coding: utf-8 -*- from libervia.server.constants import Const as C @@ -9,16 +9,16 @@ log = getLogger(__name__) """ticket handling pages""" -name = u"tickets_new" +name = "tickets_new" access = C.PAGES_ACCESS_PROFILE -template = u"ticket/create.html" +template = "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"") + service, node = data.get("service", ""), data.get("node", "") profile = self.getProfile(request) schema = yield self.host.bridgeCall("ticketsSchemaGet", service.full(), node, profile) data["schema"] = schema @@ -37,18 +37,18 @@ 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" + xmlui_obj.widgets["labels"].type = "string" except KeyError: pass # same as for tickets_edit, we have to convert for now - wid = xmlui_obj.widgets[u'body'] - if wid.type == u"xhtmlbox": - wid.type = u"textbox" + wid = xmlui_obj.widgets['body'] + if wid.type == "xhtmlbox": + wid.type = "textbox" wid.value = yield self.host.bridgeCall( - u"syntaxConvert", wid.value, C.SYNTAX_XHTML, u"markdown", + "syntaxConvert", wid.value, C.SYNTAX_XHTML, "markdown", False, profile) - template_data[u"new_ticket_xmlui"] = xmlui_obj + template_data["new_ticket_xmlui"] = xmlui_obj @defer.inlineCallbacks @@ -67,18 +67,18 @@ # we convert back body to XHTML body = yield self.host.bridgeCall( - u"syntaxConvert", posted_data[u'body'][0], u"markdown", C.SYNTAX_XHTML, + "syntaxConvert", posted_data['body'][0], "markdown", C.SYNTAX_XHTML, False, profile) - posted_data[u'body'] = [u'
    {body}
    '.format(ns=C.NS_XHTML, + posted_data['body'] = ['
    {body}
    '.format(ns=C.NS_XHTML, body=body)] yield self.host.bridgeCall( - "ticketSet", service.full(), node, posted_data, u"", u"", u"", profile + "ticketSet", service.full(), node, posted_data, "", "", "", profile ) # we don't want to redirect to creation page on success, but to tickets list data["post_redirect_page"] = ( - self.getPageByName(u"tickets"), + self.getPageByName("tickets"), service.full(), - node or u"@", + node or "@", ) diff -r f14ab8a25e8b -r b2d067339de3 libervia/pages/tickets/page_meta.py --- a/libervia/pages/tickets/page_meta.py Tue Aug 13 09:39:33 2019 +0200 +++ b/libervia/pages/tickets/page_meta.py Tue Aug 13 19:12:31 2019 +0200 @@ -1,4 +1,4 @@ -#!/usr/bin/env python2.7 +#!/usr/bin/env python3 # -*- coding: utf-8 -*- from libervia.server.constants import Const as C @@ -10,39 +10,39 @@ log = getLogger(__name__) """ticket handling pages""" -name = u"tickets" +name = "tickets" access = C.PAGES_ACCESS_PUBLIC -template = u"ticket/overview.html" +template = "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"] + service, node = data["service"], data["node"] if node is None: - self.pageRedirect(u"tickets_disco", request) - if node == u"@": - node = data[u"node"] = u"" + self.pageRedirect("tickets_disco", request) + if node == "@": + node = data["node"] = "" 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") + template_data["url_tickets_list"] = self.getURL(service.full(), node or "@") + template_data["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"] + service, node = data["service"], data["node"] profile = self.getProfile(request) or C.SERVICE_PROFILE self.checkCache(request, C.CACHE_PUBSUB, service=service, node=node, short="tickets") extra = self.getPubsubExtra(request) - extra[u"labels_as_list"] = C.BOOL_TRUE + extra["labels_as_list"] = C.BOOL_TRUE tickets, metadata = yield self.host.bridgeCall( "ticketsGet", - service.full() if service else u"", + service.full() if service else "", node, C.NO_LIMIT, [], @@ -50,9 +50,9 @@ extra, profile, ) - template_data[u"tickets"] = [template_xmlui.create(self.host, x) for x in tickets] - template_data[u"on_ticket_click"] = data_objects.OnClick( - url=self.getSubPageURL(request, u"tickets_view") + u"/{item.id}" + template_data["tickets"] = [template_xmlui.create(self.host, x) for x in tickets] + template_data["on_ticket_click"] = data_objects.OnClick( + url=self.getSubPageURL(request, "tickets_view") + "/{item.id}" ) metadata = data_objects.parsePubSubMetadata(metadata, tickets) self.setPagination(request, metadata) diff -r f14ab8a25e8b -r b2d067339de3 libervia/pages/tickets/view/page_meta.py --- a/libervia/pages/tickets/view/page_meta.py Tue Aug 13 09:39:33 2019 +0200 +++ b/libervia/pages/tickets/view/page_meta.py Tue Aug 13 19:12:31 2019 +0200 @@ -1,4 +1,4 @@ -#!/usr/bin/env python2.7 +#!/usr/bin/env python3 # -*- coding: utf-8 -*- from libervia.server.constants import Const as C @@ -15,20 +15,20 @@ log = getLogger(__name__) """ticket handling pages""" -name = u"tickets_view" +name = "tickets_view" access = C.PAGES_ACCESS_PUBLIC -template = u"ticket/item.html" +template = "ticket/item.html" def parse_url(self, request): try: item_id = self.nextPath(request) except IndexError: - log.warning(_(u"no ticket id specified")) + log.warning(_("no ticket id specified")) self.pageError(request, C.HTTP_BAD_REQUEST) data = self.getRData(request) - data[u"ticket_id"] = item_id + data["ticket_id"] = item_id @defer.inlineCallbacks @@ -37,9 +37,9 @@ 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"], + data.get("service", ""), + data.get("node", ""), + data["ticket_id"], ) profile = self.getProfile(request) @@ -48,7 +48,7 @@ tickets = yield self.host.bridgeCall( "ticketsGet", - service.full() if service else u"", + service.full() if service else "", node, C.NO_LIMIT, [ticket_id], @@ -57,7 +57,7 @@ profile, ) ticket = [template_xmlui.create(self.host, x) for x in tickets[0]][0] - template_data[u"item"] = ticket + template_data["item"] = ticket comments_uri = ticket.widgets["comments_uri"].value if comments_uri: uri_data = uri.parseXMPPUri(comments_uri) @@ -67,8 +67,8 @@ "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) + template_data["comments"] = data_objects.BlogItems(comments) + template_data["login_url"] = self.getPageRedirectURL(request) if session.connected: # we set edition URL only if user is the publisher or the node owner @@ -79,10 +79,10 @@ 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( + template_data["url_ticket_edit"] = self.getURLByPath( SubPage("tickets"), service.full(), - node or u"@", + node or "@", SubPage("tickets_edit"), ticket_id, ) @@ -90,9 +90,9 @@ @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") + type_ = self.getPostedData(request, "type") + if type_ == "comment": + blog_page = self.getPageByName("blog_view") yield blog_page.on_data_post(self, request) else: - log.warning(_(u"Unhandled data type: {}").format(type_)) + log.warning(_("Unhandled data type: {}").format(type_)) diff -r f14ab8a25e8b -r b2d067339de3 libervia/pages/u/atom.xml/page_meta.py --- a/libervia/pages/u/atom.xml/page_meta.py Tue Aug 13 09:39:33 2019 +0200 +++ b/libervia/pages/u/atom.xml/page_meta.py Tue Aug 13 19:12:31 2019 +0200 @@ -1,4 +1,4 @@ -#!/usr/bin/env python2.7 +#!/usr/bin/env python3 # -*- coding: utf-8 -*- -redirect = u"blog_feed_atom" -name = u"user_blog_feed_atom" +redirect = "blog_feed_atom" +name = "user_blog_feed_atom" diff -r f14ab8a25e8b -r b2d067339de3 libervia/pages/u/blog/page_meta.py --- a/libervia/pages/u/blog/page_meta.py Tue Aug 13 09:39:33 2019 +0200 +++ b/libervia/pages/u/blog/page_meta.py Tue Aug 13 19:12:31 2019 +0200 @@ -1,7 +1,7 @@ -#!/usr/bin/env python2.7 +#!/usr/bin/env python3 # -*- coding: utf-8 -*- -name = u"user_blog" +name = "user_blog" def parse_url(self, request): @@ -9,7 +9,7 @@ # (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"] + service = data["service"] self.pageRedirect( - u"blog_view", request, skip_parse_url=False, path_args=[service.full(), u"@"] + "blog_view", request, skip_parse_url=False, path_args=[service.full(), "@"] ) diff -r f14ab8a25e8b -r b2d067339de3 libervia/pages/u/page_meta.py --- a/libervia/pages/u/page_meta.py Tue Aug 13 09:39:33 2019 +0200 +++ b/libervia/pages/u/page_meta.py Tue Aug 13 19:12:31 2019 +0200 @@ -1,4 +1,4 @@ -#!/usr/bin/env python2.7 +#!/usr/bin/env python3 # -*- coding: utf-8 -*- from libervia.server.constants import Const as C @@ -7,9 +7,9 @@ """page used to target a user profile, e.g. for public blog""" -name = u"user" +name = "user" access = C.PAGES_ACCESS_PUBLIC # can be a callable -template = u"blog/articles.html" +template = "blog/articles.html" url_cache = True @@ -23,31 +23,31 @@ data = self.getRData(request) target_profile = yield self.host.bridgeCall("profileNameGet", prof_requested) - request.template_data[u"target_profile"] = target_profile + request.template_data["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 + data["service"] = target_jid # if URL is parsed here, we'll have atom.xml available and we need to # add the link to the page - atom_url = self.getSubPageURL(request, u'user_blog_feed_atom') - request.template_data[u'atom_url'] = atom_url - request.template_data.setdefault(u'links', []).append({ - u"href": atom_url, - u"type": "application/atom+xml", - u"rel": "alternate", - u"title": "{target_profile}'s blog".format(target_profile=target_profile)}) + atom_url = self.getSubPageURL(request, 'user_blog_feed_atom') + request.template_data['atom_url'] = atom_url + request.template_data.setdefault('links', []).append({ + "href": atom_url, + "type": "application/atom+xml", + "rel": "alternate", + "title": "{target_profile}'s blog".format(target_profile=target_profile)}) @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" + request, C.CACHE_PUBSUB, service=data["service"], node=None, short="microblog" ) - self.pageRedirect(u"blog_view", request) + self.pageRedirect("blog_view", request) def on_data_post(self, request): - return self.getPageByName(u"blog_view").on_data_post(self, request) + return self.getPageByName("blog_view").on_data_post(self, request) diff -r f14ab8a25e8b -r b2d067339de3 libervia/server/blog.py --- a/libervia/server/blog.py Tue Aug 13 09:39:33 2019 +0200 +++ b/libervia/server/blog.py Tue Aug 13 19:12:31 2019 +0200 @@ -36,7 +36,7 @@ import re import os import sys -import urllib +import urllib.request, urllib.parse, urllib.error from libervia.server.html_tools import sanitizeHtml, convertNewLinesToXHTML from libervia.server.constants import Const as C @@ -78,7 +78,7 @@ @param value(unicode): value to quote @return (str): quoted value """ - return urllib.quote(value.encode("utf-8"), "") + return urllib.parse.quote(value.encode("utf-8"), "") def _unquote(quoted_value): @@ -87,16 +87,16 @@ @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") + assert not isinstance(quoted_value, str) + return urllib.parse.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() + "{}={}".format(urllib.parse.quote_plus(str(k)), urllib.parse.quote_plus(str(v))) + for k, v in query.items() ] ) @@ -277,11 +277,11 @@ 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)) + log.warning("Request with rsm_max over limit ({})".format(rsm_max)) rsm_max = C.STATIC_RSM_MAX_LIMIT - request.extra_dict["rsm_max"] = unicode(rsm_max) + request.extra_dict["rsm_max"] = str(rsm_max) except (ValueError, KeyError): - request.extra_dict["rsm_max"] = unicode(C.STATIC_RSM_MAX_DEFAULT) + request.extra_dict["rsm_max"] = str(C.STATIC_RSM_MAX_DEFAULT) try: request.extra_dict["rsm_index"] = request.args["index"][0] except (ValueError, KeyError): @@ -308,11 +308,11 @@ 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)) + log.warning("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) + request.extra_comments_dict["rsm_max"] = str(rsm_max) except (ValueError, KeyError): - request.extra_comments_dict["rsm_max"] = unicode( + request.extra_comments_dict["rsm_max"] = str( C.STATIC_RSM_MAX_COMMENTS_DEFAULT ) else: @@ -363,7 +363,7 @@ except KeyError: pass try: - metadata["rsm_index"] = unicode(int(rsm_metadata["rsm_index"]) - 1) + metadata["rsm_index"] = str(int(rsm_metadata["rsm_index"]) - 1) except KeyError: pass @@ -491,83 +491,83 @@ # from microblog data items, metadata = data items = [data_format.deserialise(i) for i in items] - feed_elt = domish.Element((NS_ATOM, u"feed")) - title = _(u"{user}'s blog").format(user=profile) - feed_elt.addElement(u"title", content=title) + feed_elt = domish.Element((NS_ATOM, "feed")) + title = _("{user}'s blog").format(user=profile) + feed_elt.addElement("title", content=title) base_blog_url = self.host.getExtBaseURL( - request, u"blog/{user}".format(user=profile) + request, "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" + link_feed_elt["href"] = "{base}/atom.xml".format(base=base_blog_url) + link_feed_elt["type"] = "application/atom+xml" + link_feed_elt["rel"] = "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["rel"] = "alternate" + link_blog_elt["type"] = "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["rel"] = "alternate" + link_blog_elt["type"] = "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")) + "updated", content="{}Z".format(updated_dt.isoformat("T")) ) for item in items: - entry_elt = feed_elt.addElement(u"entry") + entry_elt = feed_elt.addElement("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) + title = "{}…".format(" ".join(item["content"][:70].split())) + entry_elt.addElement("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( + http_link_elt = entry_elt.addElement("link") + http_link_elt["rel"] = "alternate" + http_link_elt["type"] = "text/html" + http_link_elt["href"] = "{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( + xmpp_link_elt = entry_elt.addElement("link") + xmpp_link_elt["rel"] = "alternate" + xmpp_link_elt["type"] = "application/atom+xml" + xmpp_link_elt["href"] = "{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"]) + entry_elt.addElement("id", content=item["atom_id"]) updated = datetime.fromtimestamp(float(item["updated"])) entry_elt.addElement( - u"updated", content=u"{}Z".format(updated.isoformat("T")) + "updated", content="{}Z".format(updated.isoformat("T")) ) published = datetime.fromtimestamp(float(item["published"])) entry_elt.addElement( - u"published", content=u"{}Z".format(published.isoformat("T")) + "published", content="{}Z".format(published.isoformat("T")) ) # author metadata - author_elt = entry_elt.addElement(u"author") + author_elt = entry_elt.addElement("author") author_elt.addElement("name", content=item.get("author", profile)) try: author_elt.addElement( - "uri", content=u"xmpp:{}".format(item["author_jid"]) + "uri", content="xmpp:{}".format(item["author_jid"]) ) except KeyError: pass @@ -578,7 +578,7 @@ # categories for tag in item.get('tags', []): - category_elt = entry_elt.addElement(u"category") + category_elt = entry_elt.addElement("category") category_elt["term"] = tag # content @@ -594,7 +594,7 @@ xml_tools.ElementParser()(content_xhtml, namespace=C.NS_XHTML) ) - atom_feed = u'\n{}'.format( + atom_feed = '\n{}'.format( feed_elt.toXml() ) self.renderAtomFeed(atom_feed, request), @@ -708,12 +708,12 @@ # 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)) + xmpp_uri += ";item={}".format(_quote(item_id)) data = { "url_base": base_url, "xmpp_uri": xmpp_uri, - "url_query": u"?{}".format(query_data) if query_data else "", + "url_query": "?{}".format(query_data) if query_data else "", "keywords": getOption(C.STATIC_BLOG_PARAM_KEYWORDS), "description": getOption(C.STATIC_BLOG_PARAM_DESCRIPTION), "title": title, @@ -850,7 +850,7 @@ self.content = self.getText(entry, "content") if is_comment: - self.author = _(u"from {}").format(entry["author"]) + self.author = _("from {}").format(entry["author"]) else: self.author = " " self.url = "{}/{}".format(base_url, _quote(entry["id"])) @@ -860,9 +860,9 @@ self.title = self.getText(entry, "title") self.tags = [sanitizeHtml(tag) for tag in entry.get('tags', [])] - count_text = lambda count: D_(u"comments") if count > 1 else D_(u"comment") + count_text = lambda count: D_("comments") if count > 1 else D_("comment") - self.comments_text = u"{} {}".format( + self.comments_text = "{} {}".format( comments_count, count_text(comments_count) ) @@ -871,7 +871,7 @@ prev_url = "{}?{}".format( self.url, _urlencode({"comments_max": comments_count}) ) - prev_text = D_(u"show {count} previous {comments}").format( + prev_text = D_("show {count} previous {comments}").format( count=delta, comments=count_text(delta) ) self.all_comments_link = BlogLink(prev_url, "comments_link", prev_text) diff -r f14ab8a25e8b -r b2d067339de3 libervia/server/constants.py --- a/libervia/server/constants.py Tue Aug 13 09:39:33 2019 +0200 +++ b/libervia/server/constants.py Tue Aug 13 19:12:31 2019 +0200 @@ -32,12 +32,12 @@ THEMES_URL = "themes" MEDIA_DIR = "media/" CARDS_DIR = "games/cards/tarot" - PAGES_DIR = u"pages" - TASKS_DIR = u"tasks" - LIBERVIA_CACHE = u"libervia" - BUILD_DIR = u"__b" + PAGES_DIR = "pages" + TASKS_DIR = "tasks" + LIBERVIA_CACHE = "libervia" + BUILD_DIR = "__b" - TPL_RESOURCE = u'_t' + TPL_RESOURCE = '_t' ERRNUM_BRIDGE_ERRBACK = 0 # FIXME ERRNUM_LIBERVIA = 0 # FIXME @@ -56,15 +56,15 @@ STATIC_RSM_MAX_COMMENTS_DEFAULT = 10 ## Libervia pages ## - PAGES_META_FILE = u"page_meta.py" + PAGES_META_FILE = "page_meta.py" PAGES_ACCESS_NONE = ( - u"none" + "none" ) #  no access to this page (using its path will return a 404 error) - PAGES_ACCESS_PUBLIC = u"public" + PAGES_ACCESS_PUBLIC = "public" PAGES_ACCESS_PROFILE = ( - u"profile" + "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_ADMIN = "admin" #  only profiles set in admins_list can access the page PAGES_ACCESS_ALL = ( PAGES_ACCESS_NONE, PAGES_ACCESS_PUBLIC, @@ -86,14 +86,14 @@ ] ## Session flags ## - FLAG_CONFIRM = u"CONFIRM" + FLAG_CONFIRM = "CONFIRM" ## Data post ## - POST_NO_CONFIRM = u"POST_NO_CONFIRM" + POST_NO_CONFIRM = "POST_NO_CONFIRM" ## HTTP methods ## - HTTP_METHOD_GET = u"GET" - HTTP_METHOD_POST = u"POST" + HTTP_METHOD_GET = b"GET" + HTTP_METHOD_POST = b"POST" ## HTTP codes ## HTTP_SEE_OTHER = 303 diff -r f14ab8a25e8b -r b2d067339de3 libervia/server/pages.py --- a/libervia/server/pages.py Tue Aug 13 09:39:33 2019 +0200 +++ b/libervia/server/pages.py Tue Aug 13 19:12:31 2019 +0200 @@ -36,10 +36,11 @@ import uuid import os.path -import urllib +import urllib.request, urllib.parse, urllib.error import time import hashlib import copy +from functools import reduce log = getLogger(__name__) @@ -176,11 +177,11 @@ if (name in self.named_pages and not (replace_on_conflict and self.named_pages[name].url == url)): 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')) + _('a Libervia page named "{}" already exists'.format(name))) + if "/" in name: + raise ValueError(_('"/" is not allowed in page names')) if not name: - raise ValueError(_(u"a page name can't be empty")) + raise ValueError(_("a page name can't be empty")) self.named_pages[name] = self if access is None: access = C.PAGES_ACCESS_PUBLIC @@ -190,7 +191,7 @@ C.PAGES_ACCESS_NONE, ): raise NotImplementedError( - _(u"{} access is not implemented yet").format(access) + _("{} access is not implemented yet").format(access) ) self.access = access self.dynamic = dynamic @@ -202,8 +203,8 @@ for x in (parse_url, prepare_render, render, template) ): raise ValueError( - _(u"you can't use full page redirection with other rendering" - u"method, check self.pageRedirect if you need to use them")) + _("you can't use full page redirection with other rendering" + "method, check self.pageRedirect if you need to use them")) self.redirect = redirect else: self.redirect = None @@ -219,22 +220,18 @@ # 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")) + log.error(_("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")) + log.error(_("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} (vhost: {vhost_root})".format( - name=self.name or u"", url=self.url, vhost_root=self.vhost_root) - def __str__(self): - return self.__unicode__().encode("utf-8") - + return "LiberviaPage {name} at {url} (vhost: {vhost_root})".format( + name=self.name or "", url=self.url, vhost_root=self.vhost_root) @property def named_pages(self): @@ -269,29 +266,29 @@ - libervia_page: created resource """ dir_path = os.path.dirname(meta_path) - page_data = {"__name__": u".".join([u"page"] + url_elts)} + page_data = {"__name__": ".".join(["page"] + url_elts)} # 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) + exec(compile(open(meta_path, "rb").read(), meta_path, 'exec'), page_data) return page_data, LiberviaPage( host=host, vhost_root=vhost_root, root_dir=dir_path, - url=u"/" + u"/".join(url_elts), - name=page_data.get(u"name"), - redirect=page_data.get(u"redirect"), - access=page_data.get(u"access"), - dynamic=page_data.get(u"dynamic", False), - parse_url=page_data.get(u"parse_url"), - prepare_render=page_data.get(u"prepare_render"), - render=page_data.get(u"render"), - template=page_data.get(u"template"), - on_data_post=page_data.get(u"on_data_post"), - on_data=page_data.get(u"on_data"), - on_signal=page_data.get(u"on_signal"), - url_cache=page_data.get(u"url_cache", False), + url="/" + "/".join(url_elts), + 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), replace_on_conflict=replace_on_conflict ) @@ -327,8 +324,8 @@ if not os.path.isdir(dir_path): continue if _extra_pages and d in _parent.children: - log.debug(_(u"[{host_name}] {path} is already present, ignoring it") - .format(host_name=vhost_root.host_name, path=u'/'.join(_path+[d]))) + log.debug(_("[{host_name}] {path} is already present, ignoring it") + .format(host_name=vhost_root.host_name, path='/'.join(_path+[d]))) continue meta_path = os.path.join(dir_path, C.PAGES_META_FILE) if os.path.isfile(meta_path): @@ -341,29 +338,29 @@ continue else: raise e - _parent.putChild(d, resource) - log_msg = (u"[{host_name}] Added /{path} page".format( + _parent.putChild(d.encode('utf-8'), resource) + log_msg = ("[{host_name}] Added /{path} page".format( host_name=vhost_root.host_name, - path=u"[…]/".join(new_path))) + path="[…]/".join(new_path))) if _extra_pages: log.debug(log_msg) else: log.info(log_msg) if "uri_handlers" in page_data: if not isinstance(page_data, dict): - log.error(_(u"uri_handlers must be a dict")) + log.error(_("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")) + for uri_tuple, cb_name in page_data["uri_handlers"].items(): + if len(uri_tuple) != 2 or not isinstance(cb_name, str): + log.error(_("invalid uri_tuple")) continue if not _extra_pages: - log.info(_(u"setting {}/{} URIs handler") + log.info(_("setting {}/{} URIs handler") .format(*uri_tuple)) try: cb = page_data[cb_name] except KeyError: - log.error(_(u"missing {name} method to handle {1}/{2}") + log.error(_("missing {name} method to handle {1}/{2}") .format(name=cb_name, *uri_tuple)) continue else: @@ -388,16 +385,16 @@ return path = file_path.path.decode('utf-8') base_name = os.path.basename(path) - if base_name != u"page_meta.py": + if base_name != "page_meta.py": # we only handle libervia pages return - log.debug(u"{flags} event(s) received for {file_path}".format( - flags=u", ".join(flags), file_path=file_path)) + log.debug("{flags} event(s) received for {file_path}".format( + flags=", ".join(flags), file_path=file_path)) dir_path = os.path.dirname(path) if not dir_path.startswith(site_path): - raise exceptions.InternalError(u"watched file should start with site path") + raise exceptions.InternalError("watched file should start with site path") path_elts = [p for p in dir_path[len(site_path):].split('/') if p] if not path_elts: @@ -422,7 +419,7 @@ if idx != len(path_elts)-1: # a page has been created in a subdir when one or more # page_meta.py are missing on the way - log.warning(_(u"Can't create a page at {path}, missing parents") + log.warning(_("Can't create a page at {path}, missing parents") .format(path=path)) return new_page = True @@ -442,7 +439,7 @@ # EncodingResourceWrapper should probably be removed resource.children = page.children except Exception as e: - log.warning(_(u"Can't create page: {reason}").format(reason=e)) + log.warning(_("Can't create page: {reason}").format(reason=e)) else: url_elt = path_elts[-1] if not new_page: @@ -451,9 +448,9 @@ # we can now add the new page parent.putChild(url_elt, resource) if new_page: - log.info(_(u"{page} created").format(page=resource)) + log.info(_("{page} created").format(page=resource)) else: - log.info(_(u"{page} reloaded").format(page=resource)) + log.info(_("{page} reloaded").format(page=resource)) def registerURI(self, uri_tuple, get_uri_cb): """Register a URI handler @@ -466,7 +463,7 @@ can't handle this URL """ if uri_tuple in self.uri_callbacks: - log.info(_(u"{}/{} URIs are already handled, replacing by the new handler") + log.info(_("{}/{} URIs are already handled, replacing by the new handler") .format( *uri_tuple)) self.uri_callbacks[uri_tuple] = (self, get_uri_cb) @@ -499,7 +496,7 @@ # 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")) + log.error(_("You can't register signal if page is not dynamic")) return signal_id = self.getSignalId(request) LiberviaPage.signals_handlers.setdefault(signal, {})[signal_id] = [ @@ -522,7 +519,7 @@ def getPagePathFromURI(self, uri): return self.vhost_root.getPagePathFromURI(uri) - def getPageRedirectURL(self, request, page_name=u"login", url=None): + def getPageRedirectURL(self, request, page_name="login", url=None): """generate URL for a page with redirect_url parameter set mainly used for login page with redirection to current page @@ -532,9 +529,9 @@ None to use request path (i.e. current page) @return (unicode): URL to use """ - return u"{root_url}?redirect_url={redirect_url}".format( + return "{root_url}?redirect_url={redirect_url}".format( root_url=self.getPageByName(page_name).url, - redirect_url=urllib.quote_plus(request.uri) + redirect_url=urllib.parse.quote_plus(request.uri) if url is None else url.encode("utf-8"), ) @@ -551,7 +548,7 @@ #  we check for redirection redirect_data = self.pages_redirects[self.name] args_hash = tuple(args) - for limit in xrange(len(args) + 1): + for limit in range(len(args) + 1): current_hash = args_hash[:limit] if current_hash in redirect_data: url_base = redirect_data[current_hash] @@ -572,7 +569,7 @@ # 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] + path_elts = [p for p in request.path.decode('utf-8').split("/") if p] if request.postpath: if not request.postpath[-1]: @@ -584,7 +581,7 @@ # path elements path_elts = path_elts[: -len(request.postpath)] - return u"/" + "/".join(path_elts).decode("utf-8") + return "/" + "/".join(path_elts) def getParamURL(self, request, **kwargs): """use URL of current request but modify the parameters in query part @@ -594,10 +591,10 @@ """ 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 + encoded = urllib.parse.urlencode( + {k: v for k, v in kwargs.items()} + ) + current_url = current_url + "?" + encoded return current_url def getSubPageByName(self, subpage_name, parent=None): @@ -612,15 +609,15 @@ """ if parent is None: parent = self - for path, child in parent.children.iteritems(): + for path, child in parent.children.items(): 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")) + return path.decode('utf-8'), child + raise exceptions.NotFound(_("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 @@ -645,7 +642,7 @@ 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] + "/", current_url, path, *[quote(a) for a in args if a is not None] ) def getURLByNames(self, named_path): @@ -671,7 +668,7 @@ path.append(sub_path) if page_args: path.extend([quote(a) for a in page_args]) - return self.host.checkRedirection(self.vhost_root, u"/".join(path)) + return self.host.checkRedirection(self.vhost_root, "/".join(path)) def getURLByPath(self, *args): """Generate URL by path @@ -708,12 +705,12 @@ else: path, current_page = current_page.getSubPageByName(args.pop(0)) arguments = [path] - return self.host.checkRedirection(self.vhost_root, u"/".join(url_elts)) + return self.host.checkRedirection(self.vhost_root, "/".join(url_elts)) def getChildWithDefault(self, path, request): # we handle children ourselves raise exceptions.InternalError( - u"this method should not be used with LiberviaPage" + "this method should not be used with LiberviaPage" ) def nextPath(self, request): @@ -726,25 +723,25 @@ """ pathElement = request.postpath.pop(0) request.prepath.append(pathElement) - return urllib.unquote(pathElement).decode("utf-8") + return urllib.parse.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"@": + if handler in ("@", "@jid") and value == "@": value = None - if handler in (u"", u"@"): + if handler in ("", "@"): if value is None: - return u"" - elif handler in (u"jid", u"@jid"): + return "" + elif handler in ("jid", "@jid"): if value: try: return jid.JID(value) except RuntimeError: - log.warning(_(u"invalid jid argument: {value}").format(value=value)) + log.warning(_("invalid jid argument: {value}").format(value=value)) self.pageError(request, C.HTTP_BAD_REQUEST) else: - return u"" + return "" else: return handler(self, value, name, request) @@ -772,7 +769,7 @@ data = self.getRData(request) for idx, name in enumerate(names): - if name[0] == u"*": + if name[0] == "*": value = data[name[1:]] = [] while True: try: @@ -792,14 +789,14 @@ values_count = idx + 1 if values_count < min_args: - log.warning(_(u"Missing arguments in URL (got {count}, expected at least " - u"{min_args})").format(count=values_count, min_args=min_args)) + log.warning(_("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(): + for name, handler in kwargs.items(): if name[0] == "*": data[name] = [ self._filterPathValue(v, handler, name, request) for v in data[name] @@ -831,15 +828,15 @@ if extra is None: extra = {} else: - assert not {u"rsm_max", u"rsm_after", u"rsm_before", - C.KEY_ORDER_BY}.intersection(extra.keys()) - extra[u"rsm_max"] = unicode(page_max) + assert not {"rsm_max", "rsm_after", "rsm_before", + C.KEY_ORDER_BY}.intersection(list(extra.keys())) + extra["rsm_max"] = str(page_max) if order_by is not None: extra[C.KEY_ORDER_BY] = order_by - if u'after' in params: - extra[u'rsm_after'] = params[u'after'] - elif u'before' in params: - extra[u'rsm_before'] = params[u'before'] + if 'after' in params: + extra['rsm_after'] = params['after'] + elif 'before' in params: + extra['rsm_before'] = params['before'] return extra def setPagination(self, request, pubsub_data): @@ -853,7 +850,7 @@ """ template_data = request.template_data try: - last_id = pubsub_data[u"rsm_last"] + last_id = pubsub_data["rsm_last"] except KeyError: # no pagination available return @@ -862,10 +859,10 @@ # We only show previous button if it's not the first page already. # If we have no index, we default to display the button anyway # as we can't know if we are on the first page or not. - first_id = pubsub_data[u"rsm_first"] + first_id = pubsub_data["rsm_first"] template_data['previous_page_url'] = self.getParamURL(request, before=first_id) - if not pubsub_data[u"complete"]: + if not pubsub_data["complete"]: # we also show the page next button if complete is None because we # can't know where we are in the feed in this case. template_data['next_page_url'] = self.getParamURL(request, after=last_id) @@ -899,11 +896,11 @@ 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_)) + log.warning(_("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_)) + log.warning(_("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 @@ -932,9 +929,9 @@ short = kwargs["short"] node = self.host.ns_map[short] except KeyError: - log.warning(_(u'Can\'t use cache for empty node without namespace ' - u'set, please ensure to set "short" and that it is ' - u'registered')) + log.warning(_('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 @@ -963,14 +960,14 @@ return else: - raise exceptions.InternalError(u"Unknown cache_type") - log.debug(u"using cache for {page}".format(page=self)) + raise exceptions.InternalError("Unknown cache_type") + log.debug("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")) + raise failure.Failure(exceptions.CancelError("cache is used")) def _cacheURL(self, __, request, profile): self.cached_urls.setdefault(profile, {})[request.uri] = CacheURL(request) @@ -982,18 +979,18 @@ 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)) + "Removing subscription for {service}/{node}: " + "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( + _("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( + _("Can't remove watch for {service}/{node}: {msg}").format( service=service, node=node, msg=failure_))) else: cache.clear() @@ -1009,14 +1006,14 @@ """ for page, request, check_profile in cls.signals_handlers.get( signal, {} - ).itervalues(): + ).values(): 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")) + log.error(_("no session started, signal can't be checked")) continue if signal_profile != request_profile: #  we ignore the signal, it's not for our profile @@ -1025,7 +1022,7 @@ # 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) + "signal [{signal}] cached: {args}".format(signal=signal, args=args) ) else: page.on_signal(page, request, signal, *args) @@ -1039,7 +1036,7 @@ # we need to replace corresponding original requests by this websocket request # in signals_handlers signal_id = request.signal_id - for signal_handlers_map in self.__class__.signals_handlers.itervalues(): + for signal_handlers_map in self.__class__.signals_handlers.values(): if signal_id in signal_handlers_map: signal_handlers_map[signal_id][1] = request @@ -1058,10 +1055,10 @@ try: del LiberviaPage.signals_handlers[signal][signal_id] except KeyError: - log.error(_(u"Can't find signal handler for [{signal}], this should not " - u"happen").format(signal=signal)) + log.error(_("Can't find signal handler for [{signal}], this should not " + "happen").format(signal=signal)) else: - log.debug(_(u"Removed signal handler")) + log.debug(_("Removed signal handler")) def delegateToResource(self, request, resource): """continue workflow with Twisted Resource""" @@ -1071,7 +1068,7 @@ else: request.write(buf) request.finish() - raise failure.Failure(exceptions.CancelError(u"resource delegation")) + raise failure.Failure(exceptions.CancelError("resource delegation")) def HTTPRedirect(self, request, url): """redirect to an URL using HTTP redirection @@ -1081,9 +1078,9 @@ """ web_util.redirectTo(url.encode("utf-8"), request) request.finish() - raise failure.Failure(exceptions.CancelError(u"HTTP redirection is used")) + raise failure.Failure(exceptions.CancelError("HTTP redirection is used")) - def redirectOrContinue(self, request, redirect_arg=u"redirect_url"): + def redirectOrContinue(self, request, redirect_arg="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 @@ -1092,13 +1089,14 @@ @interrupt: redirect the page to requested URL @interrupt pageError(C.HTTP_BAD_REQUEST): empty or non local URL is used """ + redirect_arg = redirect_arg.encode('utf-8') try: - url = request.args["redirect_url"][0] + url = request.args[redirect_arg][0].decode('utf-8') except (KeyError, IndexError): pass else: #  a redirection is requested - if not url or url[0] != u"/": + if not url or url[0] != "/": # we only want local urls self.pageError(request, C.HTTP_BAD_REQUEST) else: @@ -1126,13 +1124,14 @@ @raise KeyError: there is no known page with this name """ # FIXME: render non LiberviaPage resources - path = page_path.rstrip(u"/").split(u"/") + path = page_path.rstrip("/").split("/") if not path[0]: redirect_page = self.vhost_root else: redirect_page = self.named_pages[path[0]] for subpage in path[1:]: + subpage = subpage.encode('utf-8') if redirect_page is self.vhost_root: redirect_page = redirect_page.children[subpage] else: @@ -1148,7 +1147,7 @@ self._do_cache = None redirect_page.renderPage(request, skip_parse_url=skip_parse_url) - raise failure.Failure(exceptions.CancelError(u"page redirection is used")) + raise failure.Failure(exceptions.CancelError("page redirection is used")) def pageError(self, request, code=C.HTTP_NOT_FOUND, no_body=False): """generate an error page and terminate the request @@ -1164,13 +1163,13 @@ if no_body: request.finish() else: - template = u"error/" + unicode(code) + ".html" + template = "error/" + str(code) + ".html" template_data = request.template_data session_data = self.host.getSessionData(request, session_iface.ISATSession) if session_data.locale is not None: - template_data[u'locale'] = session_data.locale + template_data['locale'] = session_data.locale if self.vhost_root.site_name: - template_data[u'site'] = self.vhost_root.site_name + template_data['site'] = self.vhost_root.site_name rendered = self.host.renderer.render( template, @@ -1179,7 +1178,7 @@ ) self.writeData(rendered, request) - raise failure.Failure(exceptions.CancelError(u"error page is used")) + raise failure.Failure(exceptions.CancelError("error page is used")) def writeData(self, data, request): """write data to transport and finish the request""" @@ -1192,7 +1191,7 @@ 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}]") + log.debug(_("{page} put in cache for [{profile}]") .format( page=self, profile=self._do_cache[0])) self._do_cache = None self._checkCacheHeaders(request, page_cache) @@ -1200,8 +1199,8 @@ try: request.write(data_encoded) except AttributeError: - log.warning(_(u"Can't write page, the request has probably been cancelled " - u"(browser tab closed or reloaded)")) + log.warning(_("Can't write page, the request has probably been cancelled " + "(browser tab closed or reloaded)")) return request.finish() @@ -1214,19 +1213,19 @@ If there is no unmanaged part of the segment, current page workflow is pursued """ if request.postpath: - subpage = self.nextPath(request) + subpage = self.nextPath(request).encode('utf-8') try: child = self.children[subpage] except KeyError: self.pageError(request) else: child.render(request) - raise failure.Failure(exceptions.CancelError(u"subpage page is used")) + raise failure.Failure(exceptions.CancelError("subpage page is used")) def _prepare_dynamic(self, __, request): # we need to activate dynamic page # we set data for template, and create/register token - socket_token = unicode(uuid.uuid4()) + socket_token = str(uuid.uuid4()) socket_url = self.host.getWebsocketURL(request) socket_debug = C.boolConst(self.host.debug) request.template_data["websocket"] = WebsocketMeta( @@ -1250,21 +1249,21 @@ # 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 + template_data["confirm"] = True notifs = session_data.popPageNotifications(self) if notifs: - template_data[u"notifications"] = notifs + template_data["notifications"] = notifs if session_data.locale is not None: - template_data[u'locale'] = session_data.locale + template_data['locale'] = session_data.locale if self.vhost_root.site_name: - template_data[u'site'] = self.vhost_root.site_name + template_data['site'] = self.vhost_root.site_name return self.host.renderer.render( self.template, page_url=self.getURL(), - media_path=u"/" + C.MEDIA_DIR, + media_path="/" + C.MEDIA_DIR, cache_path=session_data.cache_dir, - build_path=u"/" + C.BUILD_DIR + u"/", + build_path="/" + C.BUILD_DIR + "/", main_menu=self.main_menu, **template_data) @@ -1274,10 +1273,10 @@ def _internalError(self, failure_, request): """called if an error is not catched""" - if failure_.check(BridgeException) and failure_.value.condition == u'not-allowed': - log.warning(u"not allowed exception catched") + if failure_.check(BridgeException) and failure_.value.condition == 'not-allowed': + log.warning("not allowed exception catched") self.pageError(request, C.HTTP_FORBIDDEN) - log.error(_(u"Uncatched error for HTTP request on {url}: {msg}") + log.error(_("Uncatched error for HTTP request on {url}: {msg}") .format( url=request.URLPath(), msg=failure_)) self.pageError(request, C.HTTP_INTERNAL_ERROR) @@ -1290,7 +1289,7 @@ request.setResponseCode(C.HTTP_SEE_OTHER) request.setHeader("location", request.uri) request.finish() - raise failure.Failure(exceptions.CancelError(u"Post/Redirect/Get is used")) + raise failure.Failure(exceptions.CancelError("Post/Redirect/Get is used")) def _on_data_post_redirect(self, ret, request): """called when page's on_data_post has been done successfuly @@ -1308,12 +1307,12 @@ """ if ret is None: ret = () - elif isinstance(ret, basestring): + elif isinstance(ret, str): ret = (ret,) else: ret = tuple(ret) raise NotImplementedError( - _(u"iterable in on_data_post return value is not used yet") + _("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) @@ -1333,21 +1332,21 @@ 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.setHeader(b"location", redirect_uri) request.finish() - raise failure.Failure(exceptions.CancelError(u"Post/Redirect/Get is used")) + raise failure.Failure(exceptions.CancelError("Post/Redirect/Get is used")) def _on_data_post(self, __, request): csrf_token = self.host.getSessionData( request, session_iface.ISATSession ).csrf_token try: - given_csrf = self.getPostedData(request, u"csrf_token") + given_csrf = self.getPostedData(request, "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( + _("invalid CSRF token, hack attempt? URL: {url}, IP: {ip}").format( url=request.uri, ip=request.getClientIP() ) ) @@ -1374,15 +1373,18 @@ """ #  FIXME: request.args is already unquoting the value, it seems we are doing # double unquote - if isinstance(keys, basestring): + if isinstance(keys, str): keys = [keys] get_first = True else: get_first = False + keys = [k.encode('utf-8') for k in keys] + ret = [] for key in keys: - gen = (urllib.unquote(v).decode("utf-8") for v in request.args.get(key, [])) + gen = (urllib.parse.unquote(v.decode("utf-8")) + for v in request.args.get(key, [])) if multiple: ret.append(gen) else: @@ -1405,16 +1407,18 @@ @param multiple(bool): if False, only the first values are returned @return (dict[unicode, list[unicode]]): post values """ - except_ = tuple(except_) + (u"csrf_token",) + except_ = tuple(except_) + ("csrf_token",) ret = {} - for key, values in request.args.iteritems(): - key = urllib.unquote(key).decode("utf-8") + for key, values in request.args.items(): + key = key.decode('utf-8') + key = urllib.parse.unquote(key) if key in except_: continue + values = [v.decode('utf-8') for v in values] if not multiple: - ret[key] = urllib.unquote(values[0]).decode("utf-8") + ret[key] = urllib.parse.unquote(values[0]) else: - ret[key] = [urllib.unquote(v).decode("utf-8") for v in values] + ret[key] = [urllib.parse.unquote(v) for v in values] return ret def getProfile(self, request): @@ -1472,7 +1476,7 @@ if not accept_language: return accepted = {a.strip() for a in accept_language.split(',')} - available = [unicode(l) for l in self.host.renderer.translations] + available = [str(l) for l in self.host.renderer.translations] for lang in accepted: lang = lang.split(';')[0].strip().lower() if not lang: @@ -1494,20 +1498,20 @@ """ if not self.dynamic: raise exceptions.InternalError( - _(u"renderPartial must only be used with dynamic pages") + _("renderPartial must only be used with dynamic pages") ) session_data = self.host.getSessionData(request, session_iface.ISATSession) if session_data.locale is not None: - template_data[u'locale'] = session_data.locale + template_data['locale'] = session_data.locale if self.vhost_root.site_name: - template_data[u'site'] = self.vhost_root.site_name + template_data['site'] = self.vhost_root.site_name return self.host.renderer.render( template, page_url=self.getURL(), - media_path=u"/" + C.MEDIA_DIR, + media_path="/" + C.MEDIA_DIR, cache_path=session_data.cache_dir, - build_path=u"/" + C.BUILD_DIR + u"/", + build_path="/" + C.BUILD_DIR + "/", main_menu=self.main_menu, **template_data ) @@ -1533,9 +1537,9 @@ html = self.renderPartial(request, template, template_data) try: request.sendData( - u"dom", selectors=selectors, update_type=update_type, html=html) + "dom", selectors=selectors, update_type=update_type, html=html) except Exception as e: - log.error(u"Can't renderAndUpdate, html was: {html}".format(html=html)) + log.error("Can't renderAndUpdate, html was: {html}".format(html=html)) raise e def renderPage(self, request, skip_parse_url=False): @@ -1546,8 +1550,8 @@ 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, + "profile": session_data.profile, + "csrf_token": csrf_token, } # XXX: here is the code which need to be executed once @@ -1561,13 +1565,13 @@ try: locale = request.args.pop(C.KEY_LANG)[0] except IndexError: - log.warning(u"empty lang received") + log.warning("empty lang received") else: - if u"/" in locale: + if "/" in locale: # "/" is refused because locale may sometime be used to access # path, if localised documents are available for instance - log.warning(_(u'illegal char found in locale ("/"), hack ' - u'attempt? locale={locale}').format(locale=locale)) + log.warning(_('illegal char found in locale ("/"), hack ' + 'attempt? locale={locale}').format(locale=locale)) locale = None session_data.locale = locale @@ -1596,7 +1600,7 @@ d.addCallback(self.parse_url, request) d.addCallback(self._cacheURL, request, profile) else: - log.debug(_(u"using URI cache for {page}").format(page=self)) + log.debug(_("using URI cache for {page}").format(page=self)) cache_url.use(request) else: d.addCallback(self.parse_url, request) diff -r f14ab8a25e8b -r b2d067339de3 libervia/server/pages_tools.py --- a/libervia/server/pages_tools.py Tue Aug 13 09:39:33 2019 +0200 +++ b/libervia/server/pages_tools.py Tue Aug 13 19:12:31 2019 +0200 @@ -42,13 +42,13 @@ else exception will be raised """ try: - d = self.host.bridgeCall(u"mbGet", service, node, C.NO_LIMIT, [], {}, profile) + d = self.host.bridgeCall("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( + _("Can't get comments at {node} (service: {service}): {msg}").format( service=service, node=node, msg=e ) ) diff -r f14ab8a25e8b -r b2d067339de3 libervia/server/server.py --- a/libervia/server/server.py Tue Aug 13 09:39:33 2019 +0200 +++ b/libervia/server/server.py Tue Aug 13 19:12:31 2019 +0200 @@ -18,14 +18,10 @@ # along with this program. If not, see . import re -import glob import os.path import sys -import tempfile -import shutil -import uuid -import urlparse -import urllib +import urllib.parse +import urllib.request, urllib.error import time import copy from twisted.application import service @@ -34,16 +30,12 @@ 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.web import vhost from twisted.python.components import registerAdapter from twisted.python import failure from twisted.python import filepath from twisted.words.protocols.jabber import jid -from txjsonrpc.web import jsonrpc -from txjsonrpc import jsonrpclib - from sat.core.log import getLogger from sat_frontends.bridge.dbus_bridge import ( @@ -58,7 +50,6 @@ from sat.tools.common import regex from sat.tools.common import template from sat.tools.common import uri as common_uri -from httplib import HTTPS_PORT import libervia from libervia.server import websockets from libervia.server.pages import LiberviaPage @@ -73,7 +64,6 @@ ssl = None from libervia.server.constants import Const as C -from libervia.server.blog import MicroBlog from libervia.server import session_iface log = getLogger(__name__) @@ -81,7 +71,7 @@ # 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 +DATA_DIR_DEFAULT = OPT_PARAMETERS_BOTH = OPT_PARAMETERS_CFG = None DEFAULT_MASK = (inotify.IN_CREATE | inotify.IN_MODIFY | inotify.IN_MOVE_SELF | inotify.IN_MOVED_TO) @@ -102,7 +92,7 @@ def watchDir(self, dir_path, callback, mask=DEFAULT_MASK, auto_add=False, recursive=False, **kwargs): - log.info(_(u"Watching directory {dir_path}").format(dir_path=dir_path)) + log.info(_("Watching directory {dir_path}").format(dir_path=dir_path)) callbacks = [lambda __, filepath, mask: callback(self.host, filepath, inotify.humanReadableMask(mask), **kwargs)] self.notifier.watch( @@ -120,7 +110,7 @@ def lock(self): """Prevent session from expiring""" self.__lock = True - self._expireCall.reset(sys.maxint) + self._expireCall.reset(sys.maxsize) def unlock(self): """Allow session to expire again, and touch it""" @@ -147,6 +137,15 @@ return web_resource.NoResource() + def getChild(self, path, request): + return super().getChild(path, request) + + def getChildWithDefault(self, path, request): + return super().getChildWithDefault(path, request) + + def getChildForRequest(self, request): + return super().getChildForRequest(request) + class LiberviaRootResource(ProtectedFile): """Specialized resource for Libervia root @@ -165,15 +164,12 @@ self.cached_urls = {} self.main_menu = None - def __unicode__(self): - return (u"Root resource for {host_name} using {site_name} at {site_path} and " - u"deserving files at {path}".format( + def __str__(self): + return ("Root resource for {host_name} using {site_name} at {site_path} and " + "deserving files at {path}".format( host_name=self.host_name, site_name=self.site_name, site_path=self.site_path, path=self.path)) - def __str__(self): - return self.__unicode__.encode('utf-8') - def _initRedirections(self, options): url_redirections = options["url_redirections_dict"] @@ -183,16 +179,16 @@ self.redirections = {} self.inv_redirections = {} # new URL to old URL map - for old, new_data in url_redirections.iteritems(): + for old, new_data in url_redirections.items(): # 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: + if len({"path", "url", "page"}.intersection(list(new_data.keys()))) != 1: raise ValueError( - u'You must have one and only one of "url", "page" or "path" key ' - u'in your url_redirections_dict data') + '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: @@ -201,15 +197,15 @@ new.setdefault("path_args", []) if not isinstance(new["path_args"], list): log.error( - _(u'"path_args" in redirection of {old} must be a list. ' - u'Ignoring the redirection'.format(old=old))) + _('"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 ' - u'dictionary. Ignoring the redirection'.format(old=old))) + '"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 @@ -223,17 +219,17 @@ # 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): + for k, v in new["query_args"].items(): + if isinstance(v, str): new["query_args"][k] = [v] elif "path" in new_data: - new = "file:{}".format(urllib.quote(new_data["path"])) - elif isinstance(new_data, basestring): + new = "file:{}".format(urllib.parse.quote(new_data["path"])) + elif isinstance(new_data, str): new = new_data new_data = {} else: log.error( - _(u"ignoring invalid redirection value: {new_data}").format( + _("ignoring invalid redirection value: {new_data}").format( new_data=new_data ) ) @@ -244,7 +240,7 @@ # root URL special case old = "" elif not old.startswith("/"): - log.error(_(u"redirected url must start with '/', got {value}. Ignoring") + log.error(_("redirected url must start with '/', got {value}. Ignoring") .format(value=old)) continue else: @@ -255,29 +251,29 @@ # which ared use dynamically when the request is done self.redirections[old] = new if not old: - if new[u"type"] == u"page": + if new["type"] == "page": log.info( - _(u"Root URL redirected to page {name}").format( - name=new[u"page"] + _("Root URL redirected to page {name}").format( + name=new["page"] ) ) else: - if new[u"type"] == u"page": - page = self.getPageByName(new[u"page"]) - url = page.getURL(*new.get(u"path_args", [])) + if new["type"] == "page": + page = self.getPageByName(new["page"]) + url = page.getURL(*new.get("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")) + new_url = urllib.parse.urlsplit(new) # we handle the known URL schemes if new_url.scheme == "xmpp": location = self.getPagePathFromURI(new) if location is None: log.warning( - _(u"ignoring redirection, no page found to handle this URI: " - u"{uri}").format(uri=new)) + _("ignoring redirection, no page found to handle this URI: " + "{uri}").format(uri=new)) continue request_data = self._getRequestData(location) if old: @@ -287,29 +283,29 @@ # direct redirection if new_url.netloc: raise NotImplementedError( - u"netloc ({netloc}) is not implemented yet for " - u"url_redirections_dict, it is not possible to redirect to an " - u"external website".format(netloc=new_url.netloc)) - location = urlparse.urlunsplit( + "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 = urllib.parse.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"): + elif new_url.scheme == "file": # file or directory if new_url.netloc: raise NotImplementedError( - u"netloc ({netloc}) is not implemented for url redirection to " - u"file system, it is not possible to redirect to an external " + "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) + path = urllib.parse.unquote(new_url.path) if not os.path.isabs(path): raise ValueError( - u"file redirection must have an absolute path: e.g. " - u"file:/path/to/my/file") + "file redirection must have an absolute path: e.g. " + "file:/path/to/my/file") # for file redirection, we directly put child here segments, __, last_segment = old.rpartition("/") url_segments = segments.split("/") if segments else [] @@ -322,27 +318,27 @@ ProtectedFile if new_data.get("protected", True) else static.File ) current.putChild( - last_segment, + last_segment.encode('utf-8'), resource_class(path, defaultType="application/octet-stream") ) - log.info(u"[{host_name}] Added redirection from /{old} to file system " - u"path {path}".format(host_name=self.host_name, - old=old.decode("utf-8"), - path=path.decode("utf-8"))) + log.info("[{host_name}] Added redirection from /{old} to file system " + "path {path}".format(host_name=self.host_name, + old=old, + path=path)) 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}: scheme is not managed for url_redirections_dict".format( scheme=new_url.scheme ) ) self.redirections[old] = request_data if not old: - log.info(_(u"[{host_name}] Root URL redirected to {uri}") + log.info(_("[{host_name}] Root URL redirected to {uri}") .format(host_name=self.host_name, - uri=request_data[1].decode("utf-8"))) + uri=request_data[1])) # the default root URL, if not redirected if not "" in self.redirections: @@ -353,13 +349,13 @@ main_menu = [] for menu in menus: if not menu: - msg = _(u"menu item can't be empty") + msg = _("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]" + "menu item as list must be in the form [page_name, absolue URL]" ) log.error(msg) raise ValueError(msg) @@ -369,8 +365,8 @@ try: url = self.getPageByName(page_name).url except KeyError as e: - log_msg = _(u"Can'find a named page ({msg}), please check " - u"menu_json in configuration.").format(msg=e.args[0]) + log_msg = _("Can'find a named page ({msg}), please check " + "menu_json in configuration.").format(msg=e.args[0]) log.error(log_msg) raise exceptions.ConfigError(log_msg) main_menu.append((page_name, url)) @@ -385,7 +381,7 @@ """ if lower: url = url.lower() - return "/".join((p for p in url.encode("utf-8").split("/") if p)) + return "/".join((p for p in url.split("/") if p)) def _getRequestData(self, uri): """Return data needed to redirect request @@ -397,22 +393,22 @@ path as in Request.path args as in Request.args """ - uri = uri.encode("utf-8") + uri = uri # XXX: we reuse code from twisted.web.http.py here # as we need to have the same behaviour - x = uri.split(b"?", 1) + x = uri.split("?", 1) if len(x) == 1: path = uri args = {} else: path, argstring = x - args = http.parse_qs(argstring, 1) + args = urllib.parse.parse_qs(argstring, True) # 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("/"), + self._normalizeURL(path, lower=False).split("/"), uri, path, args, @@ -435,10 +431,10 @@ try: __, uri, __, __ = request_data except ValueError: - uri = u"" - log.error(D_( u"recursive redirection, please fix this URL:\n" - u"{old} ==> {new}").format( - old=request.uri.decode("utf-8"), new=uri.decode("utf-8"))) + uri = "" + log.error(D_( "recursive redirection, please fix this URL:\n" + "{old} ==> {new}").format( + old=request.uri.decode("utf-8"), new=uri)) return web_resource.NoResource() request._redirected = True # here to avoid recursive redirections @@ -450,29 +446,31 @@ except KeyError: log.error( _( - u'Can\'t find page named "{name}" requested in redirection' + '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 + path_args = [pa.encode('utf-8') for pa in request_data["path_args"]] + request.postpath = 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( + _("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") + raise exceptions.InternalError("unknown request_data type") else: path_list, uri, path, args = request_data + path_list = [p.encode('utf-8') for p in path_list] log.debug( - u"Redirecting URL {old} to {new}".format( - old=request.uri.decode("utf-8"), new=uri.decode("utf-8") + "Redirecting URL {old} to {new}".format( + old=request.uri.decode('utf-8'), new=uri ) ) # we change the request to reflect the new url @@ -520,7 +518,7 @@ 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: + if name == b"" and not request.postpath: return self._redirect(request, self.redirections[""]) return super(LiberviaRootResource, self).getChildWithDefault(name, request) @@ -531,8 +529,8 @@ # 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() + for idx in range(len(path_elt), 0, -1): + test_url = b"/".join(path_elt[:idx]).decode('utf-8').lower() if test_url in self.redirections: request_data = self.redirections[test_url] request.postpath = path_elt[idx:] @@ -542,6 +540,8 @@ def putChild(self, path, resource): """Add a child to the root resource""" + if not isinstance(path, bytes): + raise ValueError("path must be specified in bytes") if not isinstance(resource, web_resource.EncodingResourceWrapper): # FIXME: check that no information is leaked (c.f. https://twistedmatrix.com/documents/current/web/howto/using-twistedweb.html#request-encoders) resource = web_resource.EncodingResourceWrapper( @@ -562,639 +562,6 @@ return f -class JSONRPCMethodManager(jsonrpc.JSONRPC): - def __init__(self, sat_host): - jsonrpc.JSONRPC.__init__(self) - self.sat_host = sat_host - - def _bridgeCallEb(self, failure_): - """Raise a jsonrpclib failure for the frontend""" - return failure.Failure( - jsonrpclib.Fault(C.ERRNUM_BRIDGE_ERRBACK, failure_.value.classname) - ) - - def asyncBridgeCall(self, method_name, *args, **kwargs): - d = self.sat_host.bridgeCall(method_name, *args, **kwargs) - d.addErrback(self._bridgeCallEb) - return d - - -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 " - u"(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. @@ -1237,508 +604,6 @@ 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 profile, - C.PROFILE_LOGGED_EXT_JID) - - 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 = """...""" - # 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 = ( - "

    " - + D_("You are about to connect to an unsecure service.") - + "

     

    " - ) - - 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": '', - "faq_suffix": "", - } - text += "

    " + D_("and use the secure version of this website:") - text += '

     

    %(url)s' % { - "url": url - } - else: - text += D_("You should ask your administrator to turn on HTTPS.") - - return text + "

     

    " - - -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 " - u"check connection")) - else: - log.info( - _(u"Service profile has been disconnected, but we need it! " - u"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: " - u"{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 " - u"({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: - # FIXME: this method should not use _cbRender - # but all txJsonRPC code will be removed in 0.8 in favor of webRTC - # and it is currently used only with Libervia legacy app, - # so we do a is_jsonp workaround for now - self.is_jsonp = False - # 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") - ) - 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 @@ -1753,26 +618,18 @@ 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) + self.base_url_ext_data = urllib.parse.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("") + self.base_url_ext_data = urllib.parse.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 @@ -1820,15 +677,15 @@ section = site_root_res.site_name.lower().strip() value = config.getConfig(self.main_conf, section, key, default=default) if value_type is not None: - if value_type == u'path': + if value_type == 'path': v_filter = lambda v: os.path.abspath(os.path.expanduser(v)) else: - raise ValueError(u"unknown value type {value_type}".format( + raise ValueError("unknown value type {value_type}".format( value_type = value_type)) if isinstance(value, list): value = [v_filter(v) for v in value] elif isinstance(value, dict): - value = {k:v_filter(v) for k,v in value.items()} + value = {k:v_filter(v) for k,v in list(value.items())} elif value is not None: value = v_filter(v) return value @@ -1837,12 +694,12 @@ self.ns_map = ns_map def _namespacesGetEb(self, failure_): - log.error(_(u"Can't get namespaces map: {msg}").format(msg=failure_)) + log.error(_("Can't get namespaces map: {msg}").format(msg=failure_)) @template.contextfilter def _front_url_filter(self, ctx, relative_url): - template_data = ctx[u'template_data'] - return os.path.join(u'/', C.TPL_RESOURCE, template_data.site or u'sat', + template_data = ctx['template_data'] + return os.path.join('/', C.TPL_RESOURCE, template_data.site or 'sat', C.TEMPLATE_TPL_DIR, template_data.theme, relative_url) def _moveFirstLevelToDict(self, options, key, keys_to_keep): @@ -1860,41 +717,42 @@ except KeyError: return if not isinstance(conf, dict): - options[key] = {u'': conf} + options[key] = {'': conf} return - default_dict = conf.get(u'', {}) + default_dict = conf.get('', {}) to_delete = [] - for key, value in conf.iteritems(): + for key, value in conf.items(): if key not in keys_to_keep: default_dict[key] = value to_delete.append(key) for key in to_delete: del conf[key] if default_dict: - conf[u''] = default_dict + conf[''] = default_dict @defer.inlineCallbacks def backendReady(self, __): - if self.options[u'dev_mode']: - log.info(_(u"Developer mode activated")) + if self.options['dev_mode']: + log.info(_("Developer mode activated")) 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) self.renderer = template.Renderer(self, self._front_url_filter) - sites_names = self.renderer.sites_paths.keys() + sites_names = list(self.renderer.sites_paths.keys()) self._moveFirstLevelToDict(self.options, "url_redirections_dict", sites_names) self._moveFirstLevelToDict(self.options, "menu_json", sites_names) - if not u'' in self.options["menu_json"]: - self.options["menu_json"][u''] = C.DEFAULT_MENU + if not '' in self.options["menu_json"]: + self.options["menu_json"][''] = C.DEFAULT_MENU # we create virtual hosts and import Libervia pages into them self.vhost_root = vhost.NameVirtualHost() default_site_path = os.path.abspath(os.path.dirname(libervia.__file__)) # self.sat_root is official Libervia site + root_path = os.path.join(default_site_path, C.TEMPLATE_STATIC_DIR) self.sat_root = default_root = LiberviaRootResource( - host=self, host_name=u'', site_name=u'', site_path=default_site_path, - path=self.html_dir) + host=self, host_name='', site_name='', site_path=default_site_path, + path=root_path) if self.options['dev_mode']: self.files_watcher.watchDir( default_site_path, auto_add=True, recursive=True, @@ -1906,19 +764,20 @@ # FIXME: handle _setMenu in a more generic way, taking care of external sites self.sat_root._setMenu(self.options["menu_json"]) self.vhost_root.default = default_root - existing_vhosts = {u'': default_root} + existing_vhosts = {b'': default_root} - for host_name, site_name in self.options["vhosts_dict"].iteritems(): + for host_name, site_name in self.options["vhosts_dict"].items(): + encoded_site_name = site_name.encode('utf-8') try: site_path = self.renderer.sites_paths[site_name] except KeyError: log.warning(_( - u"host {host_name} link to non existing site {site_name}, ignoring " - u"it").format(host_name=host_name, site_name=site_name)) + "host {host_name} link to non existing site {site_name}, ignoring " + "it").format(host_name=host_name, site_name=site_name)) continue - if site_name in existing_vhosts: + if encoded_site_name in existing_vhosts: # we have an alias host, we re-use existing resource - res = existing_vhosts[site_name] + res = existing_vhosts[encoded_site_name] else: # for root path we first check if there is a global static dir # if not, we use default template's static dic @@ -1934,7 +793,7 @@ site_path=site_path, path=root_path) - existing_vhosts[site_name] = res + existing_vhosts[encoded_site_name] = res if self.options['dev_mode']: self.files_watcher.watchDir( @@ -1960,90 +819,22 @@ self.vhost_root.addHost(host_name.encode('utf-8'), res) templates_res = web_resource.Resource() - self.putChildAll(C.TPL_RESOURCE, templates_res) - for site_name, site_path in self.renderer.sites_paths.iteritems(): - templates_res.putChild(site_name or u'sat', ProtectedFile(site_path)) + self.putChildAll(C.TPL_RESOURCE.encode('utf-8'), templates_res) + for site_name, site_path in self.renderer.sites_paths.items(): + templates_res.putChild(site_name.encode('utf-8') or b'sat', + ProtectedFile(site_path)) - _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" - ) - - # JSON APIs - self.putChildSAT("json_signal_api", self.signal_handler) - self.putChildSAT("json_api", MethodHandler(self)) - self.putChildSAT("register_api", _register) - - # files upload - self.putChildSAT("upload_radiocol", _upload_radiocol) - self.putChildSAT("upload_avatar", _upload_avatar) - - # static pages - # FIXME: legacy blog must be removed entirely in 0.8 - try: - micro_blog = MicroBlog(self) - except Exception as e: - log.warning(u"Can't load legacy microblog, ignoring it: {reason}".format( - reason=e)) - else: - self.putChildSAT("blog_legacy", micro_blog) - self.putChildSAT(C.THEMES_URL, ProtectedFile(self.themes_dir)) # websocket if self.options["connection_type"] in ("https", "both"): wss = websockets.LiberviaPageWSProtocol.getResource(self, secure=True) - self.putChildAll("wss", wss) + self.putChildAll(b'wss', wss) if self.options["connection_type"] in ("http", "both"): ws = websockets.LiberviaPageWSProtocol.getResource(self, secure=False) - self.putChildAll("ws", ws) + self.putChildAll(b'ws', ws) ## following signal is needed for cache handling in Libervia pages self.bridge.register_signal( @@ -2066,19 +857,10 @@ # media dirs # FIXME: get rid of dirname and "/" in C.XXX_DIR - self.putChildAll(os.path.dirname(C.MEDIA_DIR), ProtectedFile(self.media_dir)) + self.putChildAll(os.path.dirname(C.MEDIA_DIR).encode('utf-8'), + ProtectedFile(self.media_dir)) self.cache_resource = web_resource.NoResource() - self.putChildAll(C.CACHE_DIR, self.cache_resource) - - # special - self.putChildSAT( - "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.putChildSAT("test", web_util.Redirect("/libervia_test.html")) + self.putChildAll(C.CACHE_DIR.encode('utf-8'), self.cache_resource) # redirections for root in self.roots: @@ -2095,7 +877,7 @@ self.site.sessionFactory = LiberviaSession def initEb(self, failure): - log.error(_(u"Init error: {msg}").format(msg=failure)) + log.error(_("Init error: {msg}").format(msg=failure)) reactor.stop() return failure @@ -2109,9 +891,9 @@ def _bridgeEb(self, failure_): if isinstance(failure_, BridgeExceptionNoService): - print(u"Can't connect to SàT backend, are you sure it's launched ?") + print("Can't connect to SàT backend, are you sure it's launched ?") else: - log.error(u"Can't connect to bridge: {}".format(failure)) + log.error("Can't connect to bridge: {}".format(failure)) sys.exit(1) @property @@ -2128,7 +910,7 @@ try: return self._version_cache except AttributeError: - self._version_cache = u"{} ({})".format( + self._version_cache = "{} ({})".format( version, utils.getRepositoryData(libervia) ) return self._version_cache @@ -2176,34 +958,30 @@ 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!")) + log.error(_("/!\\ 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) + self.cache_root_dir, "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)) + self.cache_resource.putChild(sat_session.uuid.encode('utf-8'), + ProtectedFile(cache_dir)) log.debug( - _(u"profile cache resource added from {uuid} to {path}").format( + _("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.info("Session expired (profile={profile})".format(profile=profile)) + self.cache_resource.delEntity(sat_session.uuid.encode('utf-8')) log.debug( - _(u"profile cache resource {uuid} deleted").format(uuid=sat_session.uuid) + _("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) @@ -2276,11 +1054,11 @@ ): # 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 " - u"allowed") + "Trying to register JID account while registration is not " + "allowed") raise failure.Failure( exceptions.DataError( - u"JID login while registration is not allowed" + "JID login while registration is not allowed" ) ) profile = login # FIXME: what if there is a resource? @@ -2307,8 +1085,8 @@ 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 " - u"connected, disconnecting {old_profile}").format( + "{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) @@ -2323,23 +1101,23 @@ fault = getattr(failure_, 'classname', None) self.waiting_profiles.purgeRequest(profile) if fault in ("PasswordError", "ProfileUnknownError"): - log.info(u"Profile {profile} doesn't exist or the submitted password is " - u"wrong".format( profile=profile)) + log.info("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" + log.info("The XMPP password of profile {profile} is wrong" .format(profile=profile)) raise failure.Failure(ValueError(C.XMPP_AUTH_ERROR)) elif fault == "NoReply": - log.info(_(u"Did not receive a reply (the timeout expired or the " - u"connection is broken)")) + log.info(_("Did not receive a reply (the timeout expired or the " + "connection is broken)")) raise exceptions.TimeOutError elif fault is None: - log.info(_(u"Unexepected failure: {failure_}").format(failure_=failure)) + log.info(_("Unexepected failure: {failure_}").format(failure_=failure)) raise failure_ else: - log.error(u'Unmanaged fault class "{fault}" in errback for the ' - u'connection of profile {profile}'.format( + log.error('Unmanaged fault class "{fault}" in errback for the ' + 'connection of profile {profile}'.format( fault=fault, profile=profile)) raise failure.Failure(exceptions.InternalError(fault)) @@ -2353,14 +1131,14 @@ # existing session should have been ended above # so this line should never be reached log.error(_( - u"session profile [{session_profile}] differs from login " - u"profile [{profile}], this should not happen!") + "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} was already connected in backend".format( profile=profile ) ) @@ -2386,10 +1164,10 @@ """ if not self.options["allow_registration"]: log.warning( - _(u"Registration received while it is not allowed, hack attempt?") + _("Registration received while it is not allowed, hack attempt?") ) raise failure.Failure( - exceptions.PermissionError(u"Registration is not allowed on this server") + exceptions.PermissionError("Registration is not allowed on this server") ) if ( @@ -2413,7 +1191,7 @@ return C.INTERNAL_ERROR else: log.error( - _(u"Unknown registering error status: {status}\n{traceback}").format( + _("Unknown registering error status: {status}\n{traceback}").format( status=status, traceback=failure_.value.message ) ) @@ -2437,7 +1215,7 @@ """Connect the profile for Libervia and start the HTTP(S) server(s)""" def eb(e): - log.error(_(u"Connection failed: %s") % e) + log.error(_("Connection failed: %s") % e) self.stop() def initOk(__): @@ -2445,10 +1223,10 @@ 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] + msg = [l for l in str(e).split("\n") if l][-1] log.error( - u"Can't check service profile ({profile}), are you sure it exists ?" - u"\n{error}".format(profile=C.SERVICE_PROFILE, error=msg)) + "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: @@ -2468,10 +1246,14 @@ def putChildSAT(self, path, resource): """Add a child to the sat resource""" + if not isinstance(path, bytes): + raise ValueError("path must be specified in bytes") self.sat_root.putChild(path, resource) def putChildAll(self, path, resource): """Add a child to all vhost root resources""" + if not isinstance(path, bytes): + raise ValueError("path must be specified in bytes") # we wrap before calling putChild, to avoid having useless multiple instances # of the resource # FIXME: check that no information is leaked (c.f. https://twistedmatrix.com/documents/current/web/howto/using-twistedweb.html#request-encoders) @@ -2491,7 +1273,7 @@ C.CACHE_DIR, C.LIBERVIA_CACHE, regex.pathEscape(site_name)] - build_path = u"/".join(build_path_elts) + build_path = "/".join(build_path_elts) return os.path.abspath(os.path.expanduser(build_path)) def getExtBaseURLData(self, request): @@ -2522,9 +1304,9 @@ 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_port = urllib.parse.urlsplit("//{}".format(proxy_host)).port proxy_netloc = ( - u"{}:{}".format(proxy_server, proxy_port) + "{}:{}".format(proxy_server, proxy_port) if proxy_port is not None else proxy_server ) @@ -2540,10 +1322,10 @@ else: proxy_scheme, proxy_netloc = None, None - return urlparse.SplitResult( + return urllib.parse.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"/", + ext_data.path or "/", "", "", ) @@ -2561,10 +1343,10 @@ @return (unicode): external URL """ split_result = self.getExtBaseURLData(request) - return urlparse.urlunsplit( + return urllib.parse.urlunsplit( ( - split_result.scheme.decode("utf-8") if scheme is None else scheme, - split_result.netloc.decode("utf-8"), + split_result.scheme if scheme is None else scheme, + split_result.netloc, os.path.join(split_result.path, path), query, fragment, @@ -2579,13 +1361,13 @@ @return (unicode): possibly redirected URL which should link to the same location """ inv_redirections = vhost_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]) + url_parts = url.strip("/").split("/") + for idx in range(len(url), 0, -1): + test_url = "/" + "/".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) + "/", "/".join([inv_redirections[test_url]] + rem_url) ) return url @@ -2595,7 +1377,7 @@ """helper method to purge a session during request handling""" session = request.session if session is not None: - log.debug(_(u"session purge")) + log.debug(_("session purge")) session.expire() # FIXME: not clean but it seems that it's the best way to reset # session during request handling @@ -2626,7 +1408,7 @@ """ 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") + raise exceptions.InternalError("profile must be set to use this method") affiliation = sat_session.getAffiliation(service, node) if affiliation is not None: defer.returnValue(affiliation) @@ -2641,12 +1423,12 @@ service=service, node=node, reason=e ) ) - affiliation = u"" + affiliation = "" else: try: affiliation = affiliations[node] except KeyError: - affiliation = u"" + affiliation = "" sat_session.setAffiliation(service, node, affiliation) defer.returnValue(affiliation) @@ -2655,9 +1437,9 @@ def getWebsocketURL(self, request): base_url_split = self.getExtBaseURLData(request) if base_url_split.scheme.endswith("s"): - scheme = u"wss" + scheme = "wss" else: - scheme = u"ws" + scheme = "ws" return self.getExtBaseURL(request, path=scheme, scheme=scheme) @@ -2672,7 +1454,7 @@ def getHTTPDate(self, timestamp=None): now = time.gmtime(timestamp) - fmt_date = u"{day_name}, %d {month_name} %Y %H:%M:%S GMT".format( + fmt_date = "{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) @@ -2685,7 +1467,7 @@ 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") + log.error("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"] @@ -2715,7 +1497,7 @@ ) buf = [] elif not line: - log.debug(u"{} certificate(s) found".format(len(certificates))) + log.debug("{} certificate(s) found".format(len(certificates))) return certificates def _loadPKey(self, f): @@ -2739,7 +1521,7 @@ 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!") + raise ImportError("Python module pyOpenSSL is not installed!") cert_options = {} @@ -2752,29 +1534,29 @@ if not path: assert option == "tls_chain" continue - log.debug(u"loading {option} from {path}".format(option=option, path=path)) + log.debug("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( + "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 " - u"it is a valid .pem file?".format( path=path, option=option)) + "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 " - u"certificate, make sure that both a in {path} or use " - u"--tls_private_key option".format(path=path)) + "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) @@ -2790,9 +1572,10 @@ """ # 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) + cache_dir = os.path.join(self.cache_root_dir, "profiles", service_path) + self.cache_resource.putChild(service_path.encode('utf-8'), + ProtectedFile(cache_dir)) + self.service_cache_url = "/" + 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"): @@ -2823,7 +1606,7 @@ try: yield self.bridgeCall("disconnect", C.SERVICE_PROFILE) except Exception: - log.warning(u"Can't disconnect service profile") + log.warning("Can't disconnect service profile") def run(self): reactor.run() diff -r f14ab8a25e8b -r b2d067339de3 libervia/server/session_iface.py --- a/libervia/server/session_iface.py Tue Aug 13 09:39:33 2019 +0200 +++ b/libervia/server/session_iface.py Tue Aug 13 19:12:31 2019 +0200 @@ -16,7 +16,8 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from zope.interface import Interface, Attribute, implements +from zope.interface import Interface, Attribute +from zope.interface import implementer from sat.tools.common import data_objects from libervia.server.constants import Const as C from libervia.server.classes import Notification @@ -37,8 +38,8 @@ identities = Attribute("Identities of XMPP entities") +@implementer(ISATSession) class SATSession(object): - implements(ISATSession) def __init__(self, session): self.profile = None @@ -46,9 +47,9 @@ self.started = time.time() # time when the backend session was started self.backend_started = None - self.uuid = unicode(shortuuid.uuid()) + self.uuid = str(shortuuid.uuid()) self.identities = data_objects.Identities() - self.csrf_token = unicode(shortuuid.uuid()) + self.csrf_token = str(shortuuid.uuid()) self.locale = None # i18n of the pages self.pages_data = {} # used to keep data accross reloads (key is page instance) self.affiliations = OrderedDict() # cache for node affiliations @@ -56,8 +57,8 @@ @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"/" + return self.service_cache_url + "/" + return os.path.join("/", C.CACHE_DIR, self.uuid) + "/" @property def connected(self): @@ -178,9 +179,9 @@ @return (unicode, None): affiliation, or None if it is not in cache """ if service.resource: - raise ValueError(u"Service must not have a resource") + raise ValueError("Service must not have a resource") if not node: - raise ValueError(u"node must be set") + raise ValueError("node must be set") try: affiliation = self.affiliations.pop((service, node)) except KeyError: @@ -200,9 +201,9 @@ @param affiliation(unicode): affiliation to this node """ if service.resource: - raise ValueError(u"Service must not have a resource") + raise ValueError("Service must not have a resource") if not node: - raise ValueError(u"node must be set") + raise ValueError("node must be set") self.affiliations[(service, node)] = affiliation while len(self.affiliations) > MAX_CACHE_AFFILIATIONS: self.affiliations.popitem(last=False) @@ -213,8 +214,8 @@ data = Attribute("data associated with the guest") +@implementer(ISATGuestSession) class SATGuestSession(object): - implements(ISATGuestSession) def __init__(self, session): self.id = None diff -r f14ab8a25e8b -r b2d067339de3 libervia/server/tasks.py --- a/libervia/server/tasks.py Tue Aug 13 09:39:33 2019 +0200 +++ b/libervia/server/tasks.py Tue Aug 13 19:12:31 2019 +0200 @@ -32,7 +32,7 @@ class TasksManager(object): """Handle tasks of a Libervia site""" - FILE_EXTS = {u'py'} + FILE_EXTS = {'py'} def __init__(self, host, site_resource): """ @@ -67,41 +67,41 @@ @property def task_data(self): - return self.tasks[self._current_task][u'data'] + return self.tasks[self._current_task]['data'] def validateData(self, data): """Check values in data""" - for var, default, allowed in ((u"ON_ERROR", u"stop", (u"continue", u"stop")), - (u"LOG_OUTPUT", True, bool), - (u"WATCH_DIRS", [], list)): + for var, default, allowed in (("ON_ERROR", "stop", ("continue", "stop")), + ("LOG_OUTPUT", True, bool), + ("WATCH_DIRS", [], list)): value = data.setdefault(var, default) if isinstance(allowed, type): if not isinstance(value, allowed): raise ValueError( - _(u"Unexpected value for {var}, {allowed} is expected.") + _("Unexpected value for {var}, {allowed} is expected.") .format(var = var, allowed = allowed)) else: if not value in allowed: - raise ValueError(_(u"Unexpected value for {var}: {value}").format( + raise ValueError(_("Unexpected value for {var}: {value}").format( var = var, value = value)) - for var, default, allowed in [[u"ON_ERROR", u"stop", (u"continue", u"stop")]]: + for var, default, allowed in [["ON_ERROR", "stop", ("continue", "stop")]]: value = data.setdefault(var, default) if not value in allowed: - raise ValueError(_(u"Unexpected value for {var}: {value}").format( + raise ValueError(_("Unexpected value for {var}: {value}").format( var = var, value = value)) def parseTasks(self): if not os.path.isdir(self.tasks_dir): - log.debug(_(u"{name} has no task to launch.").format( - name = self.resource.site_name or u"default site")) + log.debug(_("{name} has no task to launch.").format( + name = self.resource.site_name or "default site")) return filenames = os.listdir(self.tasks_dir) filenames.sort() for filename in filenames: filepath = os.path.join(self.tasks_dir, filename) - if not filename.startswith(u'task_') or not os.path.isfile(filepath): + if not filename.startswith('task_') or not os.path.isfile(filepath): continue task_name, ext = os.path.splitext(filename) task_name = task_name[5:].lower().strip() @@ -111,15 +111,15 @@ continue if task_name in self.tasks: raise exceptions.ConflictError( - u"A task with the name [{name}] already exists".format( + "A task with the name [{name}] already exists".format( name=task_name)) - task_data = {u"__name__": "{site_name}.task.{name}".format( + task_data = {"__name__": "{site_name}.task.{name}".format( site_name=self.site_name, name=task_name)} self.tasks[task_name] = { - u'path': filepath, - u'data': task_data, + 'path': filepath, + 'data': task_data, } - execfile(filepath, task_data) + exec(compile(open(filepath, "rb").read(), filepath, 'exec'), task_data) # we launch prepare, which is a method used to prepare # data at runtime (e.g. set WATCH_DIRS using config) try: @@ -150,28 +150,28 @@ """ task_value = self.tasks[task_name] self._current_task = task_name - log.info(_(u'== running task "{task_name}" for {site_name} =='.format( + log.info(_('== running task "{task_name}" for {site_name} =='.format( task_name=task_name, site_name=self.site_name))) - data = task_value[u'data'] + data = task_value['data'] os.chdir(self.site_path) try: yield data['start'](self) except Exception as e: - on_error = data[u'ON_ERROR'] - if on_error == u'stop': + on_error = data['ON_ERROR'] + if on_error == 'stop': raise e - elif on_error == u'continue': - log.warning(_(u'Task "{task_name}" failed for {site_name}: {reason}') + elif on_error == 'continue': + log.warning(_('Task "{task_name}" failed for {site_name}: {reason}') .format(task_name=task_name, site_name=self.site_name, reason=e)) else: - raise exceptions.InternalError(u"we should never reach this point") + raise exceptions.InternalError("we should never reach this point") self._current_task = None @defer.inlineCallbacks def runTasks(self): """Run all the tasks found""" old_path = os.getcwd() - for task_name, task_value in self.tasks.iteritems(): + for task_name, task_value in self.tasks.items(): yield self.runTask(task_name) os.chdir(old_path) @@ -186,14 +186,14 @@ names = (name,) + args for n in names: try: - cmd_path = which(n)[0].encode('utf-8') + cmd_path = which(n)[0] except IndexError: pass else: return cmd_path raise exceptions.NotFound(_( - u"Can't find {name} command, did you install it?").format(name=name)) + "Can't find {name} command, did you install it?").format(name=name)) def runCommand(self, command, *args, **kwargs): - kwargs['verbose'] = self.task_data[u"LOG_OUTPUT"] + kwargs['verbose'] = self.task_data["LOG_OUTPUT"] return async_process.CommandProtocol.run(command, *args, **kwargs) diff -r f14ab8a25e8b -r b2d067339de3 libervia/server/utils.py --- a/libervia/server/utils.py Tue Aug 13 09:39:33 2019 +0200 +++ b/libervia/server/utils.py Tue Aug 13 19:12:31 2019 +0200 @@ -21,14 +21,14 @@ from twisted.internet import defer from sat.core import exceptions from sat.core.log import getLogger -import urllib +import urllib.request, urllib.parse, urllib.error log = getLogger(__name__) def quote(value, safe="@"): """shortcut to quote an unicode value for URL""" - return urllib.quote(value.encode("utf-8"), safe=safe) + return urllib.parse.quote(value.encode("utf-8"), safe=safe) class ProgressHandler(object): @@ -46,27 +46,27 @@ handlers = cls.handlers if profile in handlers and progress_id in handlers[profile]: handler_data = handlers[profile][progress_id] - timeout = handler_data[u"timeout"] + timeout = handler_data["timeout"] if timeout.active(): timeout.cancel() cb = handler_data[name] if cb is not None: cb(data) - if name == u"started": + if name == "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() + elif name == "finished": + handler_data["deferred"].callback(data) + handler_data["instance"].unregister_handler() + elif name == "error": + handler_data["deferred"].errback(Exception(data)) + handler_data["instance"].unregister_handler() else: - log.error(u"unexpected signal: {name}".format(name=name)) + log.error("unexpected signal: {name}".format(name=name)) def _timeout(self): log.warning( _( - u"No progress received, cancelling handler: {progress_id} [{profile}]" + "No progress received, cancelling handler: {progress_id} [{profile}]" ).format(progress_id=self.progress_id, profile=self.profile) ) @@ -76,7 +76,7 @@ del self.handlers[self.profile][self.progress_id] except KeyError: log.warning( - _(u"Trying to remove unknown handler: {progress_id} [{profile}]").format( + _("Trying to remove unknown handler: {progress_id} [{profile}]").format( progress_id=self.progress_id, profile=self.profile ) ) @@ -100,16 +100,16 @@ ) if handler_data: raise exceptions.ConflictError( - u"There is already one handler for this progression" + "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) + handler_data["instance"] = self + deferred = handler_data["deferred"] = defer.Deferred() + handler_data["started"] = started_cb + handler_data["finished"] = finished_cb + handler_data["error"] = error_cb + handler_data["timeout"] = reactor.callLater(timeout, self._timeout) return deferred -class SubPage(unicode): +class SubPage(str): """use to mark subpages when generating a page path""" diff -r f14ab8a25e8b -r b2d067339de3 libervia/server/websockets.py --- a/libervia/server/websockets.py Tue Aug 13 09:39:33 2019 +0200 +++ b/libervia/server/websockets.py Tue Aug 13 19:12:31 2019 +0200 @@ -46,19 +46,19 @@ self.ws_protocol = ws_protocol self.ws_request = connection_request if self.isSecure(): - cookie_string = "TWISTED_SECURE_SESSION" + cookie_name = "TWISTED_SECURE_SESSION" else: - cookie_string = "TWISTED_SESSION" - cookie_value = server_request.getCookie(cookie_string) + cookie_name = "TWISTED_SESSION" + cookie_value = server_request.getCookie(cookie_name.encode('utf-8')) try: raw_cookies = ws_protocol.http_headers['cookie'] except KeyError: - raise ValueError(u"missing expected cookie header") + raise ValueError("missing expected cookie header") self.cookies = {k:v for k,v in (c.split('=') for c in raw_cookies.split(';'))} - if self.cookies[cookie_string] != cookie_value: + if self.cookies[cookie_name] != cookie_value.decode('utf-8'): raise exceptions.PermissionError( - u"Bad cookie value, this should never happen.\n" - u"headers: {headers}".format(headers=ws_protocol.http_headers)) + "Bad cookie value, this should never happen.\n" + "headers: {headers}".format(headers=ws_protocol.http_headers)) self.template_data = server_request.template_data self.data = server_request.data @@ -94,7 +94,7 @@ tokens_map = {} def onConnect(self, request): - prefix = LIBERVIA_PROTOCOL + u"_" + prefix = LIBERVIA_PROTOCOL + "_" for protocol in request.protocols: if protocol.startswith(prefix): token = protocol[len(prefix) :].strip() @@ -102,13 +102,13 @@ break else: raise types.ConnectionDeny( - types.ConnectionDeny.NOT_IMPLEMENTED, u"Can't use this subprotocol" + types.ConnectionDeny.NOT_IMPLEMENTED, "Can't use this subprotocol" ) if token not in self.tokens_map: - log.warning(_(u"Can't activate page socket: unknown token")) + log.warning(_("Can't activate page socket: unknown token")) raise types.ConnectionDeny( - types.ConnectionDeny.FORBIDDEN, u"Bad token, please reload page" + types.ConnectionDeny.FORBIDDEN, "Bad token, please reload page" ) self.token = token token_map = self.tokens_map.pop(token) @@ -119,7 +119,7 @@ def onOpen(self): log.debug( _( - u"Websocket opened for {page} (token: {token})".format( + "Websocket opened for {page} (token: {token})".format( page=self.page, token=self.token ) ) @@ -131,7 +131,7 @@ data_json = json.loads(payload.decode("utf8")) except ValueError as e: log.warning( - _(u"Not valid JSON, ignoring data: {msg}\n{data}").format( + _("Not valid JSON, ignoring data: {msg}\n{data}").format( msg=e, data=payload ) ) @@ -144,7 +144,7 @@ except AttributeError: log.warning( _( - u'No "on_data" method set on dynamic page, ignoring data:\n{data}' + 'No "on_data" method set on dynamic page, ignoring data:\n{data}' ).format(data=data_json) ) else: @@ -155,26 +155,26 @@ page = self.page except AttributeError: log.debug( - u"page is not available, the socket was probably not opened cleanly.\n" - u"reason: {reason}".format(reason=reason)) + "page is not available, the socket was probably not opened cleanly.\n" + "reason: {reason}".format(reason=reason)) return page.onSocketClose(self.request) log.debug( _( - u"Websocket closed for {page} (token: {token}). {reason}".format( + "Websocket closed for {page} (token: {token}). {reason}".format( page=self.page, token=self.token, - reason=u"" + reason="" if wasClean - else _(u"Reason: {reason}").format(reason=reason), + else _("Reason: {reason}").format(reason=reason), ) ) ) @classmethod def getBaseURL(cls, host, secure): - return u"ws{sec}://localhost:{port}".format( + return "ws{sec}://localhost:{port}".format( sec="s" if secure else "", port=cls.host.options["port_https" if secure else "port"], ) @@ -190,5 +190,5 @@ @classmethod def registerToken(cls, token, page, request): if token in cls.tokens_map: - raise exceptions.ConflictError(_(u"This token is already registered")) + raise exceptions.ConflictError(_("This token is already registered")) cls.tokens_map[token] = {"page": page, "request": request} diff -r f14ab8a25e8b -r b2d067339de3 setup.py --- a/setup.py Tue Aug 13 09:39:33 2019 +0200 +++ b/setup.py Tue Aug 13 19:12:31 2019 +0200 @@ -1,4 +1,4 @@ -#!/usr/bin/env python2 +#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Libervia: a Salut à Toi frontend @@ -24,17 +24,16 @@ NAME = "libervia" install_requires = [ - "sat>=0.7.0", + "sat>=0.8.0.dev0", "sat-templates", "twisted", - "txJSON-RPC==0.3.1", "zope.interface", "pyopenssl", "jinja2>=2.9", "shortuuid", "autobahn", ] -long_description = u"""\ +long_description = """\ 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. @@ -60,7 +59,7 @@ setup( name=NAME, version=VERSION, - description=u"Web frontend for Salut à Toi", + description="Web frontend for Salut à Toi", long_description=long_description, author="Association « Salut à Toi »", author_email="contact@goffi.org", @@ -79,7 +78,7 @@ 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") + for root, dirs, files in os.walk("themes") ], scripts=["bin/libervia"], zip_safe=False, diff -r f14ab8a25e8b -r b2d067339de3 themes/__init__.py diff -r f14ab8a25e8b -r b2d067339de3 themes/default/images/atom/Feed-icon.svg --- a/themes/default/images/atom/Feed-icon.svg Tue Aug 13 09:39:33 2019 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - diff -r f14ab8a25e8b -r b2d067339de3 themes/default/images/atom/README --- a/themes/default/images/atom/README Tue Aug 13 09:39:33 2019 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,7 +0,0 @@ -The work in this directory is licenced under a MPL/GPL/LGPL tri-license: - - MPL 1.1 - - GPL 2 or later - - LGPL 2 or later - -Originally distributed by the Mozilla Foundation -Source: http://www.feedicons.com \ No newline at end of file diff -r f14ab8a25e8b -r b2d067339de3 themes/default/images/flaticon/COPYING --- a/themes/default/images/flaticon/COPYING Tue Aug 13 09:39:33 2019 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,2 +0,0 @@ -The work in this directory is licensed under the Creative Commons BY 3.0 License. To view a copy of this license, visit http://creativecommons.org/licenses/by/3.0/ or send a letter to Creative Commons, 171 2nd Street, Suite 300, San Francisco, California, 94105, USA. - diff -r f14ab8a25e8b -r b2d067339de3 themes/default/images/flaticon/README --- a/themes/default/images/flaticon/README Tue Aug 13 09:39:33 2019 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,5 +0,0 @@ -The work in this directory is licensed under the Creative Commons BY 3.0 License and comes from http://www.flaticon.com. -List of files and their autorships: - - clock104.png from Icon Works - - comment33.png from Dave Gandy - - tag67.png from Katarina Stefanikova diff -r f14ab8a25e8b -r b2d067339de3 themes/default/images/flaticon/clock104.png Binary file themes/default/images/flaticon/clock104.png has changed diff -r f14ab8a25e8b -r b2d067339de3 themes/default/images/flaticon/comment33.png Binary file themes/default/images/flaticon/comment33.png has changed diff -r f14ab8a25e8b -r b2d067339de3 themes/default/images/flaticon/tag67.png Binary file themes/default/images/flaticon/tag67.png has changed diff -r f14ab8a25e8b -r b2d067339de3 themes/default/images/sat_logo_16.png Binary file themes/default/images/sat_logo_16.png has changed diff -r f14ab8a25e8b -r b2d067339de3 themes/default/images/tpl/COPYING --- a/themes/default/images/tpl/COPYING Tue Aug 13 09:39:33 2019 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1 +0,0 @@ -The work in this directory is licensed under the Creative Commons BY-SA License. To view a copy of this license, visit http://creativecommons.org/licenses/by-sa/3.0/ or send a letter to Creative Commons, 171 2nd Street, Suite 300, San Francisco, California, 94105, USA. diff -r f14ab8a25e8b -r b2d067339de3 themes/default/images/tpl/README --- a/themes/default/images/tpl/README Tue Aug 13 09:39:33 2019 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,3 +0,0 @@ -The work in this directory and subdirectories have been made by Andreas Gohr and the Dokuwiki community. -All the work is available under Creative Commons BY-SA. -Notice that the Creative Common BY-SA apply this directory and all its subdirectories. diff -r f14ab8a25e8b -r b2d067339de3 themes/default/images/tpl/page-gradient.png Binary file themes/default/images/tpl/page-gradient.png has changed diff -r f14ab8a25e8b -r b2d067339de3 themes/default/static_blog.html --- a/themes/default/static_blog.html Tue Aug 13 09:39:33 2019 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,137 +0,0 @@ -{# -Libervia: a Salut à Toi frontend -Copyright (C) 2011 - 2016 Jérôme Poisson -Copyright (C) 2013 - 2016 Adrien Cossa - - -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 . -#} - -{% macro message(entry) -%} - -
    - {% if entry.type == "comment" %} -
    - -
    - {% else %} - -
    - -
    -
    - {% endif %} - - {% if entry.title %} -

    {{entry.title}}

    - {% endif %} - {% if entry.tags %} -
      - {% for tag in entry.tags %} -
    • {{tag}}
    • - {% endfor %} -
    - {% endif %} - {{entry.content}} -
    - {% if entry.type == "main_item" %} - - - - {% endif %} -
    - {% if entry.all_comments_link %} - {{ link(entry.all_comments_link) }} - {% endif %} - - {% for comment in entry.comments %} - {{ message(comment) }} - {% endfor %} - -{%- endmacro %} - -{% macro link(entry) -%} - {{entry.text}} -{%- endmacro %} - -{% macro image(entry) -%} - {{entry.alt}} -{%- endmacro %} - - - - - - - - - - - - - - {{title}} - - - - - -
    -
    - {{ link(navlinks.later_message) }} - {{ link(navlinks.later_messages) }} - {{ link(navlinks.older_message) }} -
    -
    - - {% for entry in messages %} - {{ message(entry) }} - {% endfor %} - - - -
    - Powered by Salut à Toi -
    - -
    - Subscribe to this news feed - - Atom feed - -
    - - - diff -r f14ab8a25e8b -r b2d067339de3 themes/default/static_blog_error.html --- a/themes/default/static_blog_error.html Tue Aug 13 09:39:33 2019 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,32 +0,0 @@ -{# -Libervia: a Salut à Toi frontend -Copyright (C) 2011, 2012, 2013, 2014, 2015 Jérôme Poisson -Copyright (C) 2013, 2014, 2015 Adrien Cossa - -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 . -#} - - - - - - - - Blog error - - - -

    {{message}}

    - - diff -r f14ab8a25e8b -r b2d067339de3 themes/default/styles/blog.css --- a/themes/default/styles/blog.css Tue Aug 13 09:39:33 2019 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,259 +0,0 @@ -/* -Libervia: a Salut à Toi frontend -Copyright (C) 2011, 2012, 2013, 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 . -*/ - -html { - box-sizing: border-box; -} -*, *:before, *:after { - box-sizing: inherit; -} - -body { - background: url("../images/tpl/page-gradient.png") repeat-x scroll left top #FBFAF9; - font: normal 0.8em/1.5em Arial, Helvetica, sans-serif; -} - -h1.error { - text-align: center; - color: red; -} - -.mblog_title { - font-size: x-large; - font-weight: bold; - margin: 20px; - font-family: FreeSans, Liberation Sans, Arial, sans-serif; - color: rgb(51, 51, 51); -} - -.mblog_title a { - text-decoration: none; - color: rgb(51, 51, 51); -} - -.mblog_entry { - width: 750px; - text-align: justify; - border: 1px solid LightGray; - margin: 5px auto 20px auto; - padding: 5px 10px; - -moz-border-radius: 15px; - -webkit-border-radius: 15px; - border-radius: 15px; - box-shadow: 0px 0px 0.5em rgb(153, 153, 153); - font-family: FreeSans, Liberation Sans, Arial, sans-serif; - color: rgb(51, 51, 51); -} - -.mblog_comments{ - background: url("../images/flaticon/comment33.png") no-repeat scroll 0px 2px transparent; - padding: 0px 0px 0px 18px; -} - -.mblog_comment { - position: relative; - left: 20px; - width: 710px; -} - -.mblog_header { - font-size: small; - border-bottom: 1px dotted #ccc; - color: gray; - display: table; - width: 100%; -} - -.mblog_header_main:hover { - background-color: #f0f0f0; - -moz-border-radius: 5px; - -webkit-border-radius: 5px; - border-radius: 5px; -} - -.mblog_footer { - font-size: small; - border-top: 1px dotted #ccc; - color: gray; - display: table; - width: 100%; -} - -.mblog_footer_main:hover { - background-color: #f0f0f0; - -moz-border-radius: 5px; - -webkit-border-radius: 5px; - border-radius: 5px; -} - -.mblog_metadata { - display: table-row; - width: 100%; -} - -.mblog_author { - display: table-cell; -} - -.mblog_timestamp { - display: table-cell; - float: right; - background: url("../images/flaticon/clock104.png") no-repeat scroll 0px 1px transparent; - padding: 0px 0px 0px 18px; -} - -/* START TAGS: styles are adapted from Dotclear */ -.mblog_tags { - background: #fbfbfb none repeat scroll 0% 0%; - padding: 5px; - margin: 8px 0px 0px; - overflow: hidden; -} - -.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("../images/flaticon/tag67.png") no-repeat scroll 0px 0px; -} -/* END TAGS */ - -.mblog_content { - display: block; - padding-top: 5px; -} - -.item_link { - text-decoration: none; -} - -.comments_link { - text-decoration: none; - text-align: center; - color: #2B73B7; - font-size: smaller; - display: block; - left: 2%; - position: relative; -} - -.header, .footer { - margin: 20px; - height: 20px; - text-align: center; - border-top: 1px solid #CCC; - border-bottom: 1px solid #CCC; -} - -.header_content, .footer_content { - margin: 0px 20%; -} - -.later_message { - text-decoration: none; - float: left; - color: #2B73B7; - font-size: smaller; -} - -.older_message { - text-decoration: none; - float: right; - color: #2B73B7; - font-size: smaller; -} - -.later_messages, .older_messages { - text-decoration: none; - text-align:center; - color: #2B73B7; - font-size: smaller; -} - -.mblog_entry h1, h2, h3, h4, h5, h6 { - border-bottom: 1px solid rgb(170, 170, 170); -} - -.mblog_entry h1 a, h2 a, h3 a, h4 a, h5 a, h6 a { - text-decoration: none; - color: #666; -} - -img { - max-width: 100%; -} - -video { - max-width: 100%; -} - -.mblog_title img { - max-width: 800px; - max-height: 150px; - vertical-align: middle; - margin-right: 10px; - border-radius: 5px; -} - -.powered, .feed { - text-align: center; - font-family: FreeSans,Liberation Sans,Arial,sans-serif; - color: rgb(51, 51, 51); - font-size: small; -} - -.powered a, .feed a { - text-decoration: none; -} -.powered img, .feed img { - vertical-align: bottom; - width: 16px; - heigth: 16px; -} - -@media (min-width: 600px) and (max-width: 750px) { - .mblog_entry { - width: 600px; - } - .mblog_comment { - width: 560px; - } -} -@media (min-width: 480px) and (max-width: 600px) { - .mblog_entry { - width: 480px; - } - .mblog_comment { - width: 440px; - } -} -@media (max-width: 480px) { - .mblog_entry { - width: 360px; - } - .mblog_comment { - width: 320px; - } -} diff -r f14ab8a25e8b -r b2d067339de3 twisted/plugins/libervia_server.py --- a/twisted/plugins/libervia_server.py Tue Aug 13 09:39:33 2019 +0200 +++ b/twisted/plugins/libervia_server.py Tue Aug 13 19:12:31 2019 +0200 @@ -41,12 +41,12 @@ from sat.core.i18n import _ from sat.tools import config -from zope.interface import implements +from zope.interface import implementer from twisted.python import usage from twisted.plugin import IPlugin from twisted.application.service import IServiceMaker -import ConfigParser +import configparser CONFIG_SECTION = C.APP_NAME.lower() @@ -54,7 +54,7 @@ import sys sys.stderr.write( - u"""sat module version ({sat_version}) and {current_app} version ({current_version}) mismatch + """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} @@ -78,6 +78,7 @@ def coerceConnectionType(value): # called from Libervia.OPT_PARAMETERS + assert isinstance(value, str) allowed_values = ("http", "https", "both") if value not in allowed_values: raise ValueError( @@ -87,35 +88,12 @@ 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")): - # XXX: we just display a message, as themes_dir is only used by legacy blog - # which will be removed entirely in 0.8 - # TODO: remove entirely legacy blog and linked options - print "%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): + assert isinstance(value, str) # 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 @@ -124,45 +102,43 @@ 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' " +OPT_PARAMETERS_BOTH = [['connection_type', 't', 'https', _("'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], + _('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'), + _('The port number to listen HTTPS on.').encode('utf-8'), int], + ['port_https_ext', 'e', 0, _('The external port number used for ' + 'HTTPS (0 means port_https value).').encode('utf-8'), int], + ['tls_private_key', '', '', _('TLS certificate private key (PEM ' + 'format)').encode('utf-8'), coerceUnicode], + ['tls_certificate', 'c', 'libervia.pem', _('TLS public ' + 'certificate or private key and public certificate combined ' + '(PEM format)').encode('utf-8'), coerceUnicode], + ['tls_chain', '', '', _('TLS certificate intermediate chain (PEM ' + 'format)').encode('utf-8'), coerceUnicode], + ['redirect_to_https', 'r', True, _('Automatically redirect from ' + 'HTTP to HTTPS.').encode('utf-8'), coerceBool], + ['security_warning', 'w', True, _('Warn user that he is about to ' + 'connect on HTTP.').encode('utf-8'), coerceBool], + ['passphrase', 'k', '', (_("Passphrase for the SàT profile " + "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], + ['allow_registration', '', True, _('Allow user to register new ' + 'account').encode('utf-8'), coerceBool], ['base_url_ext', '', '', - _(u'The external URL to use as base URL').encode('utf-8'), + _('The external URL to use as base URL').encode('utf-8'), coerceUnicode], - ['dev_mode', 'D', False, _(u'Developer mode, automatically reload' - u'modified pages').encode('utf-8'), coerceBool], + ['dev_mode', 'D', False, _('Developer mode, automatically reload' + 'modified pages').encode('utf-8'), coerceBool], ] # Options which are in sat.conf only OPT_PARAMETERS_CFG = [ ["empty_password_allowed_warning_dangerous_list", None, "", None], ["vhosts_dict", None, {}, None], ["url_redirections_dict", None, {}, None], - ["menu_json", None, {u'': C.DEFAULT_MENU}, None], + ["menu_json", None, {'': C.DEFAULT_MENU}, None], ["tickets_trackers_json", None, None, None], ["mr_handlers_json", None, None, None], ] @@ -183,7 +159,6 @@ 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): @@ -204,20 +179,20 @@ # on the command line. # FIXME: must be refactored + code can be factorised with backend - config_parser = ConfigParser.SafeConfigParser() + 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") + # if isinstance(value, str): + # 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): + except (configparser.NoSectionError, configparser.NoOptionError): pass usage.Options.__init__(self) for opt_data in OPT_PARAMETERS_CFG: @@ -232,19 +207,19 @@ for old, new in replacements: try: value = config.getConfig(config_parser, CONFIG_SECTION, old, Exception) - except (ConfigParser.NoSectionError, ConfigParser.NoOptionError): + 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)) + print(("\n/!\\ Use of {old} is deprecated, please use {new} instead\n" + .format(old=old, new=new))) config_parser.set(CONFIG_SECTION, new, value) +@implementer(IServiceMaker, IPlugin) class LiberviaMaker(object): - implements(IServiceMaker, IPlugin) tapname = C.APP_NAME_FILE - description = _(u"The web frontend of Salut à Toi") + description = _("The web frontend of Salut à Toi") options = Options def makeService(self, options): @@ -259,7 +234,9 @@ except IndexError: continue if coerce_cb == coerceUnicode: - options[opt[0]] = options[opt[0]].decode("utf-8") + if not isinstance(options[opt[0]], str): + print(f"FIXME: {opt[0]} is not unicode") + options[opt[0]] = options[opt[0]].decode("utf-8") initialise(options.parent) from libervia.server import server