def _list_users(self, req): """ Process the ajax request for fetching users from database. Require at least two characters in the name string before allowing any values. Disallow % character and allow _ only as a normal character, that is part of the username. """ name = req.args.get('q', '') auth = safe_string(req.args.get('auth', '').lower()) perm = safe_string(req.args.get('perm', '').upper()) limit = safe_int(req.args.get('limit', '30')) status = safe_string(req.args.get('status', '').lower()) raw_fields = safe_string(req.args.get('field', 'username').lower()) fields = [field for field in raw_fields.split(',') if field.strip() in self.db_fields.keys()] # If no fields or % in query => not allowed if not fields or '%' in name: return send_json(req, '', status=403) # Allow underscore in names/query name = safe_string(name).replace('_', '\_') states = [stat.lower() for stat in status.split(',') if stat] # Do the query rows = self._query_users(req, query=name, fields=fields, states=states, auth=auth, perm=perm, limit=limit) # Construct response in JSON list format # Serialize datetime objects into iso format: 2012-05-15T09:43:14 return send_json(req, rows)
def _list_message_groups(self, req): """ Returns list of message groups: messages grouped by the sender """ # Check permission from home env home_env = HomeProject().get_env() perm = PermissionCache(home_env, req.authname) query = req.args.get('q') or None # If empty, return latest limit = req.args.get('limit', 5) # Convert types try: limit = int(limit) except ValueError: return send_json(req, {'result': 'Invalid request'}, status=403) # Check permission if 'MESSAGE_VIEW' not in perm: return send_json(req, {'result': 'Permission denied'}, status=403) msgsrv = self.env[MessageService] userstore = get_userstore() user = userstore.getUser(req.authname) # TODO: Permission checks? return send_json(req, msgsrv.get_messages_grouped_by(user.id, query=query, limit=limit))
def _list_notifications(self, req): """ Returns the list of missed notification and optionally reset them """ chname = None initiator = req.args.get('initiator', '') reset = req.args.get('reset', 'false').lower() in ('yes', 'true', 'on', '1') ntype = req.args.get('type', '') # Check permissions if req.authname == 'anonymous': return send_json(req, {'result': 'Permission denied'}, status=403) userstore = get_userstore() user = userstore.getUser(req.authname) ns = self.env[NotificationSystem] if not ns: return send_json(req, []) # Fetch notifications sent to user chname = ns.generate_channel_name(user_id=user.id) # Get notifications try: notifications = ns.get_notifications(chname) except TracError, e: self.log.error('Failed to retrieve notifications') return send_json(req, {'result': e.message}, status=500)
def _remove_permission(self, req, group_store, perm_sys): req.perm.require('PERMISSION_REVOKE') group = req.args['group'] permission = req.args['permission'] def group_perms(): res = [] for permission, enabled in perm_sys.get_user_permissions( group).iteritems(): if not enabled: continue res.append(permission) return res try: before = group_perms() group_store.revoke_permission_from_group(group, permission) removed = set(before) - set(group_perms()) remaining = self._perm_data(group_store, perm_sys)[group]['implicit_count'] send_json( req, { 'result': 'SUCCESS', 'removed': list(removed), 'remaining': remaining }) except InvalidPermissionsState, e: req.send(e.message, content_type='text/plain', status=403)
def _mark_read(self, req): """ Marks message / messages within a group read :param Request req: Trac requst with following arguments: - message_id: Message id - group_id: Message group id .. NOTE:: Either ``message_id`` or ``group_id`` needs to be provided :return: JSON response with result: 'OK' or error """ # NOTE: Depends on NotificationSystem. Moving method in NotificationRestAPI does not make sense either # because Notification system is generic and does not know about the MessageGroups if not self.env.is_component_enabled('multiproject.common.notifications.push.NotificationSystem'): return send_json(req, {'result': 'NotificationSystem is not enabled/available'}, status=500) group_id = req.args.get('group_id', None) message_id = req.args.get('message_id', None) # Load NotificationSystem from multiproject.common.notifications.push import NotificationSystem ns = self.env[NotificationSystem] # Load user (anon check already done at this point) user = get_userstore().getUser(req.authname) chname = ns.generate_channel_name(user_id=user.id) # Load specified message group if message_id: msg = Message.get(message_id) if not msg: return send_json(req, {'result': 'Message was not found'}, status=404) ns.reset_notification(chname, {'id': msg.id, 'type': 'message'}) # Reset all messages within a group elif group_id: mg = MessageGroup.get(group_id) if not mg: return send_json(req, {'result': 'Message group or user was not found'}, status=404) # Generate notification keys based on group messages notification_keys = ['message-%d' % msg.id for msg in mg.get_messages()] ns.reset_notifications(chname, keys=notification_keys) # Missing required arguments else: return send_json(req, {'result': 'Invalid request'}, status=400) return send_json(req, {'result': 'OK'})
def _show_user(self, req): """ Returns JSON presentation from the requested user """ # NOTE: Limiting down the username user_id = req.args.get('id', '')[:100] username = req.args.get('username', '')[:100] if not any((user_id, username)): return req.send('', status=404) userstore = get_userstore() user = userstore.getUserWhereId(user_id) if user_id else userstore.getUser(username) if not user: return send_json(req, '', status=404) return send_json(req, user)
def _remove_member(self, req, group_store): req.perm.require("PERMISSION_REVOKE") member = req.args.get("member") member_type = req.args.get("type") group = req.args.get("group") if not member or not group or member_type not in self.MEMBER_TYPES: raise TracError("Invalid arguments") if member_type in ("user", "login_status"): try: group_store.remove_user_from_group(member, group) send_json(req, {}) except InvalidPermissionsState, e: req.send(e.message, content_type="text/plain", status=403) except ValueError, e: req.send(e.message, content_type="text/plain", status=403)
def _remove_member(self, req, group_store): req.perm.require('PERMISSION_REVOKE') member = req.args.get('member') member_type = req.args.get('type') group = req.args.get('group') if not member or not group or member_type not in self.MEMBER_TYPES: raise TracError('Invalid arguments') if member_type in ('user', 'login_status'): try: group_store.remove_user_from_group(member, group) send_json(req, {}) except InvalidPermissionsState, e: req.send(e.message, content_type='text/plain', status=403) except ValueError, e: req.send(e.message, content_type='text/plain', status=403)
def _post_message(self, req): """ Handles the message post request :param Request req: Trac POST request, with following arguments: - content: Message content. Can contain wiki markup - group_id: Send message to specific user """ group_id = req.args.get('group_id') msgsrv = self.env[MessageService] mg = msgsrv.get_message_group(group_id) userstore = get_userstore() user = userstore.getUser(req.authname) content = req.args.get('content') # Ensure content and receiver is set if not content or not any((mg.recipients, group_id)): self.log.warning('Invalid rest request') return send_json(req, {'result': 'Invalid rest request'}, status=400) # Check permission from home env home_env = HomeProject().get_env() perm = PermissionCache(home_env, req.authname) # Check permission if 'MESSAGE_CREATE' not in perm or user.id not in mg.recipients: return send_json(req, {'result': 'Permission denied'}, status=403) # Send message using MessageService msgsrv.post( content, sender=user, group=mg, ) self.log.info('Handled REST API request (%s) successfully' % req.path_info) # Return JSON presentation of the created message return send_json(req, 'OK')
def _get_notification(self, req): """ Get and especially reset the specified notification """ reset = req.args.get('reset', 'false').lower() in ('yes', 'true', 'on', '1') ntype = req.args.get('type') nid = req.args.get('id') if not all((ntype, nid)): return send_json(req, {'result': 'Either or both type and id missing'}, status=400) userstore = get_userstore() user = userstore.getUser(req.authname) ns = self.env[NotificationSystem] chname = ns.generate_channel_name(user_id=user.id) notification = {'id': nid, 'type': ntype} if reset: ns.reset_notification(chname, notification) return send_json(req, notification)
def process_request(self, req): # Select handler based on last part of the request path action = req.path_info.rsplit('/', 1)[1] if action in self.handlers.keys(): return getattr(self, self.handlers[action])(req) # Single notification if 'id' in req.args: return self._get_notification(req) return send_json(req, {'result': 'Missing action'}, status=404)
def _remove_permission(self, req, group_store, perm_sys): req.perm.require("PERMISSION_REVOKE") group = req.args["group"] permission = req.args["permission"] def group_perms(): res = [] for permission, enabled in perm_sys.get_user_permissions(group).iteritems(): if not enabled: continue res.append(permission) return res try: before = group_perms() group_store.revoke_permission_from_group(group, permission) removed = set(before) - set(group_perms()) remaining = self._perm_data(group_store, perm_sys)[group]["implicit_count"] send_json(req, {"result": "SUCCESS", "removed": list(removed), "remaining": remaining}) except InvalidPermissionsState, e: req.send(e.message, content_type="text/plain", status=403)
def process_request(self, req): if req.authname == 'anonymous': raise TracError('Authentication required') # Select handler based on last part of the request path action = req.path_info.rsplit('/', 1)[1] if action in self.handlers.keys(): return getattr(self, self.handlers[action])(req) # Single message if 'message_id' in req.args: return self._get_message(req) return send_json(req, {'result': 'Missing action'}, status=404)
def _user_name_or_mail_exists(self, req): """ Returns JSON representation for requested username or mail """ query = req.args.get('q', '')[:100] if not query: self.log.exception("query string not given. %s" % query) return req.send('', status=404) userstore = get_userstore() recordExists = userstore.userNameOrMailExists(query) return send_json(req, recordExists)
def _remove_permission(self, req, group_store, perm_sys): req.perm.require('PERMISSION_REVOKE') group = req.args['group'] permission = req.args['permission'] def group_perms(): res = [] for permission, enabled in perm_sys.get_user_permissions(group).iteritems(): if not enabled: continue res.append(permission) return res try: before = group_perms() group_store.revoke_permission_from_group(group, permission) removed = set(before) - set(group_perms()) remaining = self._perm_data(group_store, perm_sys)[group]['implicit_count'] send_json(req, {'result': 'SUCCESS', 'removed': list(removed), 'remaining': remaining}) except InvalidPermissionsState, e: req.send(e.message, content_type='text/plain', status=403)
def _delete_message(self, req): """ Deletes one or multiple messages from the user by hiding them (others can still see them) Following Request parameters are expected: - message_id: Id of the message to delete - group_id: Id of the message group .. note:: Either ``message_id`` or ``group_id`` is required """ userstore = get_userstore() msgsrv = MessageService(self.env) user = userstore.getUser(req.authname) message_id = req.args.get('message_id') group_id = req.args.get('group_id') only_hide = True try: # Delete one message if message_id: message = Message.get(message_id) # Everyone can hide message, even the non-recipient msgsrv.delete_message(message.id, user.id, only_hide) # Delete all group messages elif group_id: # Everyone can hide message, even the non-recipient mg = MessageGroup.get(group_id) # NOTE: Should delete_messages take objects intead of ids? msgsrv.delete_messages([msg.id for msg in mg.get_messages()], user.id, only_hide) except Exception, e: self.log.warning('Deleting the message failed with id: {0}'.format(message_id)) return send_json(req, {'result': 'Failed to delete the message: {0}'.format(e)}, status=404)
class NotificationRestAPI(Component): """ Component implements simple REST API for messages """ implements(IJSONDataPublisherInterface, IRequestHandler) juggernaut_host = Option('multiproject-messages', 'juggernaut_host', None, 'Juggernaut server host name or ip (or proxy on front them). Defaults to current domain') juggernaut_port = IntOption('multiproject-messages', 'juggernaut_port', None, 'Juggernaut server port. Defaults to current port') juggernaut_secure = BoolOption('multiproject-messages', 'juggernaut_secure', False, 'Secure connection or not') juggernaut_transports = ListOption('multiproject-messages', 'juggernaut_transports', [], 'Set/limit the used tranportation methods. Defaults to automatic selection. ' 'Valid values: websocket, flashsocket, htmlfile, xhr-polling, jsonp-polling') redis_host = Option('multiproject-messages', 'redis_host', 'localhost', 'Redis server host name or ip') redis_port = IntOption('multiproject-messages', 'redis_port', 6379, 'Redis server port') handlers = { 'list':'_list_notifications', } # IJSONDataPublisherInterface def publish_json_data(self, req): return {'conf': { 'juggernaut_host': self.juggernaut_host, 'juggernaut_port': self.juggernaut_port, 'juggernaut_secure': self.juggernaut_secure, # FIXME: Using self.juggernaut_transports directly returns all items in a string 'juggernaut_transports': self.config.getlist('multiproject-messages', 'juggernaut_transports', []), 'redis_port': self.redis_port, 'redis_host': self.redis_host, }} # IRequestHandler def match_request(self, req): return req.path_info.startswith('/api/notification') def process_request(self, req): # Select handler based on last part of the request path action = req.path_info.rsplit('/', 1)[1] if action in self.handlers.keys(): return getattr(self, self.handlers[action])(req) # Single notification if 'id' in req.args: return self._get_notification(req) return send_json(req, {'result': 'Missing action'}, status=404) def _list_notifications(self, req): """ Returns the list of missed notification and optionally reset them """ chname = None initiator = req.args.get('initiator', '') reset = req.args.get('reset', 'false').lower() in ('yes', 'true', 'on', '1') ntype = req.args.get('type', '') # Check permissions if req.authname == 'anonymous': return send_json(req, {'result': 'Permission denied'}, status=403) userstore = get_userstore() user = userstore.getUser(req.authname) ns = self.env[NotificationSystem] if not ns: return send_json(req, []) # Fetch notifications sent to user chname = ns.generate_channel_name(user_id=user.id) # Get notifications try: notifications = ns.get_notifications(chname) except TracError, e: self.log.error('Failed to retrieve notifications') return send_json(req, {'result': e.message}, status=500) # Internal filtering and notification reset function def filter_and_reset(notification): if initiator and notification.get('initiator', '') != initiator: return False if ntype and notification.get('type', '') != ntype: return False if reset: ns.reset_notification(chname, notification) return True # Filter by sender if set notifications = filter(filter_and_reset, notifications) # If user want's to reset status, send empty notification so the listening clients can update their state if reset: ns.send_notification([chname], {'type': ntype}, store=False) return send_json(req, notifications)
def _get_message_group(self, req): """ Returns the group information based on receivers/group information provided in request:: /api/message/group?recipients=123,345,235&action=create => {id: 2, recipients: [123, 345, 235]} /api/message/group?group_id=2&action=view => {id: 2, recipients: [123, 345, 235]} /api/message/group?recipients=123,345&group_id=2&action=update => {id: 2, recipients: [123]} /api/message/group?group_id=2 => {id: 2, recipients: [123, 345]} /api/message/group?recipients=123,345,235&action=create => {id: 3, recipients: [123, 345, 235]} """ msgsrv = self.env[MessageService] # Check permission from home env home_env = HomeProject().get_env() perm = PermissionCache(home_env, req.authname) # Check permission if 'MESSAGE_VIEW' not in perm: return send_json(req, {'result': 'Permission denied'}, status=403) userstore = get_userstore() user = userstore.getUser(req.authname) recipients = [] action = 'view' group_id = None group_title = None mg = None # Read parameters try: recipients_raw = req.args.get('recipients') group_title = req.args.get('title', group_title) if recipients_raw: recipients += [long(repid) for repid in recipients_raw.split(',')] action = req.args.get('action', action) group_id = req.args.get('group_id', group_id) except ValueError: return send_json(req, {'result': 'Invalid request'}, status=500) try: # Create new group if action == 'create': mg = msgsrv.create_message_group(user.id, group_title, recipients) self.log.info('User %s created a message group: %s' % (user, mg.id)) # Fetch info from existing group elif action == 'view': if not group_id: raise Exception('Group id missing in request') mg = msgsrv.get_message_group(group_id) # Update exiting group elif action == 'update': if not group_id: raise Exception('Group id missing in request') # Only existing recipients (if any) can add/remove recipients mg = msgsrv.get_message_group(group_id) if mg.recipients and user.id not in mg.recipients: self.log.warning('User %s tried adding/removing recipients without being one of them' % user) return send_json(req, {'result': 'Permission denied'}, status=403) # Update the recipients mg = msgsrv.update_message_group(group_id, user.id, {'recipients': recipients, 'title': group_title}) self.log.info('User %s updated the message group: %s' % (user, mg.id)) # No action, no valid request else: raise Exception('Action missing in request') except Exception, ex: self.log.exception('Error in message group request') return send_json(req, {'result': 'Failed to retrieve group information: {0}'.format(ex)}, status=500)
self.log.warning('User %s tried adding/removing recipients without being one of them' % user) return send_json(req, {'result': 'Permission denied'}, status=403) # Update the recipients mg = msgsrv.update_message_group(group_id, user.id, {'recipients': recipients, 'title': group_title}) self.log.info('User %s updated the message group: %s' % (user, mg.id)) # No action, no valid request else: raise Exception('Action missing in request') except Exception, ex: self.log.exception('Error in message group request') return send_json(req, {'result': 'Failed to retrieve group information: {0}'.format(ex)}, status=500) return send_json(req, mg) def _mark_read(self, req): """ Marks message / messages within a group read :param Request req: Trac requst with following arguments: - message_id: Message id - group_id: Message group id .. NOTE:: Either ``message_id`` or ``group_id`` needs to be provided
def _get_message(self, req): """ Returns contents of the message, if user has permission to see it: - If user is marked as sender or receiver - If message is project wide, and user has MESSAGE_VIEW permission in the project :param Request req: Trac request, with following arguments: - message_id: Id of the message to load. Required. - format: Render to HTML or return as raw text. Possible values: html / raw :returns: Message in JSON format """ # Check permission from home env home_env = HomeProject().get_env() home_perm = PermissionCache(home_env, req.authname) # Get current user userstore = get_userstore() user = userstore.getUser(req.authname) this_env = self.env.project_identifier # Convert types message_id = req.args.get('message_id') try: message_id = int(message_id) except ValueError: return send_json(req, {'result': 'Invalid request'}, status=403) # Get message msgsrv = self.env[MessageService] message = msgsrv.get_message(user.id, message_id) # Run through wiki processor if req.args.get('format', 'html') != 'raw': env = self.env msg_env = message.env context = Context.from_request(req) # If message is sent from different env, load it for correct links if msg_env and msg_env != this_env: env = open_environment(os.path.join( self.config.get('multiproject', 'sys_projects_root'), msg_env), use_cache=True ) # Render to HTML using wiki formatter. Set href to context in order to get correct links context.href = Href('/%s' % env.path.rsplit('/', 1)[1]) message.content = format_to_html(self.env, context, message.content) if not message: return send_json(req, {'result': 'Message cannot be not found'}, status=404) # Permission check: if user is in recipients if user.id in message.recipients and 'MESSAGE_VIEW' in home_perm: return send_json(req, message) self.log.warning('Permission denied for %s to see message: (sender: %s, receivers: %s)' % (req.authname, message.sender_id, message.recipients)) return send_json(req, {'result': 'Permission denied'}, status=403)
def _list_messages(self, req): """ Returns multiple message in JSON format, based on given request arguments. :param Request req: Trac request, with following arguments: - limit: Maximum number of responses - query: String matching with the fields: - username - given name - last name - message content - format: Render to HTML or return as raw text. Possible values: html / raw :returns: Message in JSON format """ userstore = get_userstore() user = userstore.getUser(req.authname) msgsrv = MessageService(self.env) this_env = self.env.project_identifier messages = [] group_id = req.args.get('group_id') query = req.args.get('q', '') limit = req.args.get('limit', 5) # Convert types try: limit = int(limit) except ValueError: return send_json(req, {'result': 'Invalid request'}, status=403) if query: # Get messages from all groups messages = msgsrv.get_messages_with_query(user.id, query, limit) elif group_id: # Get messages from specified messages = msgsrv.get_messages(user.id, group_id, limit) else: return send_json(req, {'result': 'Invalid request'}, status=403) # Render to HTML unless opted out if req.args.get('format', 'html') != 'raw': context = Context.from_request(req) # Iterate message to render them for msg in messages: env = self.env msg_env = msg.env # If message is sent from different env, load it for correct links if msg_env and msg_env != this_env: env = open_environment(os.path.join( self.config.get('multiproject', 'sys_projects_root'), msg_env), use_cache=True ) # Override the URL because the env does not have abs_href set context.href = Href('/%s' % env.path.rsplit('/', 1)[1]) msg.content = format_to_html(env, context, msg.content) return send_json(req, messages)