/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*
*/
package org.apache.vysper.xmpp.modules.extension.xep0045_muc.handler;
import java.util.Collection;
import java.util.List;
import org.apache.vysper.xml.fragment.XMLElement;
import org.apache.vysper.xml.fragment.XMLSemanticError;
import org.apache.vysper.xmpp.addressing.Entity;
import org.apache.vysper.xmpp.addressing.EntityFormatException;
import org.apache.vysper.xmpp.addressing.EntityImpl;
import org.apache.vysper.xmpp.delivery.failure.DeliveryException;
import org.apache.vysper.xmpp.delivery.failure.IgnoreFailureStrategy;
import org.apache.vysper.xmpp.modules.core.base.handler.DefaultIQHandler;
import org.apache.vysper.xmpp.modules.extension.xep0045_muc.MUCStanzaBuilder;
import org.apache.vysper.xmpp.modules.extension.xep0045_muc.model.Affiliation;
import org.apache.vysper.xmpp.modules.extension.xep0045_muc.model.Conference;
import org.apache.vysper.xmpp.modules.extension.xep0045_muc.model.Occupant;
import org.apache.vysper.xmpp.modules.extension.xep0045_muc.model.Role;
import org.apache.vysper.xmpp.modules.extension.xep0045_muc.model.Room;
import org.apache.vysper.xmpp.modules.extension.xep0045_muc.model.RoomType;
import org.apache.vysper.xmpp.modules.extension.xep0045_muc.stanzas.IqAdminItem;
import org.apache.vysper.xmpp.modules.extension.xep0045_muc.stanzas.MucUserItem;
import org.apache.vysper.xmpp.modules.extension.xep0045_muc.stanzas.Status;
import org.apache.vysper.xmpp.modules.extension.xep0045_muc.stanzas.Status.StatusCode;
import org.apache.vysper.xmpp.protocol.NamespaceURIs;
import org.apache.vysper.xmpp.server.ServerRuntimeContext;
import org.apache.vysper.xmpp.server.SessionContext;
import org.apache.vysper.xmpp.server.response.ServerErrorResponses;
import org.apache.vysper.xmpp.stanza.IQStanza;
import org.apache.vysper.xmpp.stanza.IQStanzaType;
import org.apache.vysper.xmpp.stanza.PresenceStanzaType;
import org.apache.vysper.xmpp.stanza.Stanza;
import org.apache.vysper.xmpp.stanza.StanzaBuilder;
import org.apache.vysper.xmpp.stanza.StanzaErrorCondition;
import org.apache.vysper.xmpp.stanza.StanzaErrorType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Implementation of <a href="http://xmpp.org/extensions/xep-0045.html">XEP-0045 Multi-user chat</a>.
*
*
* @author The Apache MINA Project (dev@mina.apache.org)
*/
public class MUCIqAdminHandler extends DefaultIQHandler {
final Logger logger = LoggerFactory.getLogger(MUCIqAdminHandler.class);
private Conference conference;
public MUCIqAdminHandler(Conference conference) {
this.conference = conference;
}
@Override
protected boolean verifyNamespace(Stanza stanza) {
return verifyInnerNamespace(stanza, NamespaceURIs.XEP0045_MUC_ADMIN);
}
private Entity roomAndNick(Room room, Occupant occupant) {
return new EntityImpl(room.getJID(), occupant.getNick());
}
@Override
protected Stanza handleGet(IQStanza stanza, ServerRuntimeContext serverRuntimeContext, SessionContext sessionContext) {
try {
Entity roomEntity = stanza.getTo();
Room room = conference.findRoom(roomEntity);
Affiliation fromAffiliation = room.getAffiliations().getAffiliation(stanza.getFrom());
XMLElement query = stanza.getSingleInnerElementsNamed("query", NamespaceURIs.XEP0045_MUC_ADMIN);
XMLElement itemElm = query.getSingleInnerElementsNamed("item", NamespaceURIs.XEP0045_MUC_ADMIN);
StanzaBuilder result = StanzaBuilder.createDirectReply(stanza, false, IQStanzaType.RESULT);
result.startInnerElement("query", NamespaceURIs.XEP0045_MUC_ADMIN);
IqAdminItem item = IqAdminItem.getWrapper(itemElm);
Affiliation affiliation = item.getAffiliation();
if(affiliation != null) {
// check access
if(affiliation == Affiliation.Outcast || affiliation == Affiliation.Member) {
if(fromAffiliation.compareTo(Affiliation.Admin) > 0) {
return MUCHandlerHelper.createErrorReply(stanza, StanzaErrorType.CANCEL, StanzaErrorCondition.NOT_ALLOWED);
}
} else if(affiliation == Affiliation.Owner || affiliation == Affiliation.Admin) {
if(fromAffiliation.compareTo(Affiliation.Owner) > 0) {
return MUCHandlerHelper.createErrorReply(stanza, StanzaErrorType.CANCEL, StanzaErrorCondition.NOT_ALLOWED);
}
}
// Retrieve ban list
Collection<Entity> users = room.getAffiliations().getByAffiliation(affiliation);
for(Entity user : users) {
IqAdminItem resultItem = new IqAdminItem(user, affiliation);
result.addPreparedElement(resultItem);
}
}
return result.build();
} catch (XMLSemanticError e) {
logger.debug("Invalid MUC admin stanza", e);
return createBadRequestError(stanza, serverRuntimeContext, sessionContext,
"Invalid IQ stanza");
} catch (EntityFormatException e) {
return createBadRequestError(stanza, serverRuntimeContext, sessionContext,
"Invalid JID");
}
}
@Override
protected Stanza handleSet(IQStanza stanza, ServerRuntimeContext serverRuntimeContext, SessionContext sessionContext) {
logger.debug("Received MUC admin stanza");
Room room = conference.findRoom(stanza.getTo());
Occupant moderator = room.findOccupantByJID(stanza.getFrom());
// check if moderator
if (moderator == null || moderator.getRole() != Role.Moderator) {
// only moderators are allowed to continue
logger.debug("Only moderators are allowed to issue admin stanzas");
return MUCHandlerHelper.createErrorReply(stanza, StanzaErrorType.AUTH, StanzaErrorCondition.FORBIDDEN);
}
try {
XMLElement query = stanza.getSingleInnerElementsNamed("query", NamespaceURIs.XEP0045_MUC_ADMIN);
List<XMLElement> itemElements = query.getInnerElementsNamed("item", NamespaceURIs.XEP0045_MUC_ADMIN);
Stanza result = StanzaBuilder.createDirectReply(stanza, false, IQStanzaType.RESULT).build();
for(XMLElement itemElement : itemElements) {
IqAdminItem item;
try {
item = IqAdminItem.getWrapper(itemElement);
} catch (EntityFormatException e) {
return createBadRequestError(stanza, serverRuntimeContext, sessionContext,
"Invalid JID");
}
if (item.getRole() != null) {
logger.debug("Changing role");
result = changeRole(stanza, serverRuntimeContext, sessionContext, item, room, moderator);
} else if(item.getAffiliation() != null) {
logger.debug("Changing affiliation");
result = changeAffiliation(stanza, serverRuntimeContext, sessionContext, item, room, moderator);
} else {
logger.debug("Invalid MUC admin stanza");
return createBadRequestError(stanza, serverRuntimeContext, sessionContext, "Unknown IQ stanza");
}
// give up on error
if("error".equals(result.getAttributeValue("type"))) {
return result;
}
}
return result;
} catch (XMLSemanticError e) {
logger.debug("Invalid MUC admin stanza", e);
return createBadRequestError(stanza, serverRuntimeContext, sessionContext,
"Invalid IQ stanza");
}
}
private Stanza createBadRequestError(IQStanza stanza, ServerRuntimeContext serverRuntimeContext,
SessionContext sessionContext, String message) {
return ServerErrorResponses.getStanzaError(StanzaErrorCondition.BAD_REQUEST, stanza,
StanzaErrorType.MODIFY, message,
getErrorLanguage(serverRuntimeContext, sessionContext), null);
}
private Stanza changeAffiliation(IQStanza stanza, ServerRuntimeContext serverRuntimeContext,
SessionContext sessionContext, IqAdminItem item, Room room, Occupant moderator) {
// only allowed by admins and owners
if(moderator.getAffiliation() != Affiliation.Admin && moderator.getAffiliation() != Affiliation.Owner) {
return MUCHandlerHelper.createErrorReply(stanza, StanzaErrorType.CANCEL,
StanzaErrorCondition.NOT_ALLOWED);
}
Entity target = null;
if (item.getNick() != null) {
target = room.findOccupantByNick(item.getNick()).getJid();
} else {
try {
if(item.getJid() != null) {
target = item.getJid();
} else {
return createBadRequestError(stanza, serverRuntimeContext, sessionContext, "Missing nick for item");
}
} catch (EntityFormatException e) {
return createBadRequestError(stanza, serverRuntimeContext, sessionContext, "Invalid JID");
}
}
Affiliation currentAffiliation = room.getAffiliations().getAffiliation(target);
Affiliation newAffiliation = item.getAffiliation();
// if the target is present in the room, we need to send presence updates
// otherwise we should send messages
Occupant targetOccupant = room.findOccupantByJID(target);
// notify remaining users that user got affiliation updated
PresenceStanzaType presenceType = null;
Status status = null;
Role newRole;
Entity from;
if(targetOccupant != null) {
newRole = targetOccupant.getRole();
from = roomAndNick(room, targetOccupant);
} else {
newRole = Role.None;
from = room.getJID();
}
// only owners can revoke ownership and admin
if((currentAffiliation == Affiliation.Owner || currentAffiliation == Affiliation.Admin) && moderator.getAffiliation() != Affiliation.Owner) {
return MUCHandlerHelper.createErrorReply(stanza, StanzaErrorType.CANCEL,
StanzaErrorCondition.NOT_ALLOWED);
}
// if the occupant is getting revoke as a member, and this is a members-only room, he also needs to leave the room
if((newAffiliation == Affiliation.None && room.isRoomType(RoomType.MembersOnly)) || newAffiliation == Affiliation.Outcast) {
if(newAffiliation == Affiliation.Outcast && currentAffiliation.compareTo(moderator.getAffiliation()) < 0) {
return MUCHandlerHelper.createErrorReply(stanza, StanzaErrorType.CANCEL,
StanzaErrorCondition.NOT_ALLOWED);
}
if(targetOccupant != null) {
room.removeOccupant(target);
}
presenceType = PresenceStanzaType.UNAVAILABLE;
if(newAffiliation == Affiliation.Outcast) {
status = new Status(StatusCode.BEEN_BANNED);
} else {
status = new Status(StatusCode.REMOVED_BY_AFFILIATION);
}
newRole = Role.None;
MucUserItem presenceItem = new MucUserItem(newAffiliation, newRole);
if(targetOccupant != null) {
Stanza presenceToFormerMember = MUCStanzaBuilder.createPresenceStanza(from, target,
presenceType, NamespaceURIs.XEP0045_MUC_USER, presenceItem, status);
relayStanza(target, presenceToFormerMember, serverRuntimeContext);
}
} else if(newAffiliation == Affiliation.Owner || newAffiliation == Affiliation.Admin) {
if(moderator.getAffiliation() != Affiliation.Owner) {
return MUCHandlerHelper.createErrorReply(stanza, StanzaErrorType.CANCEL,
StanzaErrorCondition.NOT_ALLOWED);
}
}
room.getAffiliations().add(target, newAffiliation);
if(targetOccupant != null) {
MucUserItem presenceItem = new MucUserItem(newAffiliation, newRole);
for (Occupant occupant : room.getOccupants()) {
Stanza presenceToRemaining = MUCStanzaBuilder.createPresenceStanza(from, occupant.getJid(),
presenceType, NamespaceURIs.XEP0045_MUC_USER, presenceItem, status);
relayStanza(occupant.getJid(), presenceToRemaining, serverRuntimeContext);
}
} else {
room.getAffiliations().add(target, newAffiliation);
MucUserItem presenceItem = new MucUserItem(target, null, newAffiliation, Role.None);
for (Occupant occupant : room.getOccupants()) {
StanzaBuilder builder = MUCStanzaBuilder.createMessageStanza(room.getJID(), occupant.getJid(), null, null);
builder.addPreparedElement(presenceItem);
relayStanza(occupant.getJid(), builder.build(), serverRuntimeContext);
}
}
return StanzaBuilder.createIQStanza(stanza.getTo(), stanza.getFrom(), IQStanzaType.RESULT, stanza.getID())
.build();
}
private Stanza changeRole(IQStanza stanza, ServerRuntimeContext serverRuntimeContext,
SessionContext sessionContext, IqAdminItem item, Room room, Occupant moderator) {
Occupant target = null;
if (item.getNick() != null) {
target = room.findOccupantByNick(item.getNick());
} else {
return createBadRequestError(stanza, serverRuntimeContext, sessionContext, "Missing nick for item");
}
Role newRole = item.getRole();
// you can not change yourself
if (moderator.getJid().equals(target.getJid())) {
return MUCHandlerHelper.createErrorReply(stanza, StanzaErrorType.CANCEL, StanzaErrorCondition.CONFLICT);
}
// verify change
if (newRole == Role.None) {
// a moderator can not kick someone with a higher affiliation
if (target.getAffiliation().compareTo(moderator.getAffiliation()) < 0) {
return MUCHandlerHelper.createErrorReply(stanza, StanzaErrorType.CANCEL,
StanzaErrorCondition.NOT_ALLOWED);
}
} else if (newRole == Role.Visitor) {
// moderator, admin and owner can not have their voice revoked
if (target.getAffiliation() == Affiliation.Admin || target.getAffiliation() == Affiliation.Owner) {
return MUCHandlerHelper.createErrorReply(stanza, StanzaErrorType.CANCEL,
StanzaErrorCondition.NOT_ALLOWED);
}
} else if (newRole == Role.Participant) {
if (target.getRole() == Role.Moderator) {
// only admin and owner might revoke moderator
if (moderator.getAffiliation() != Affiliation.Admin && moderator.getAffiliation() != Affiliation.Owner) {
return MUCHandlerHelper.createErrorReply(stanza, StanzaErrorType.CANCEL,
StanzaErrorCondition.NOT_ALLOWED);
}
// admin and owners can not be revoked
if (target.getAffiliation() == Affiliation.Admin || target.getAffiliation() == Affiliation.Owner) {
return MUCHandlerHelper.createErrorReply(stanza, StanzaErrorType.CANCEL,
StanzaErrorCondition.NOT_ALLOWED);
}
}
} else if (newRole == Role.Moderator) {
// only admin and owner might grant moderator
if (moderator.getAffiliation() != Affiliation.Admin && moderator.getAffiliation() != Affiliation.Owner) {
return MUCHandlerHelper.createErrorReply(stanza, StanzaErrorType.CANCEL,
StanzaErrorCondition.NOT_ALLOWED);
}
}
target.setRole(newRole);
if (newRole == Role.None) {
// remove user from room
room.removeOccupant(target.getJid());
}
Entity targetInRoom = roomAndNick(room, target);
Status status = null;
if (newRole == Role.None) {
status = new Status(StatusCode.BEEN_KICKED);
// notify user he got kicked
Stanza presenceToKicked = MUCStanzaBuilder.createPresenceStanza(targetInRoom, target.getJid(),
PresenceStanzaType.UNAVAILABLE, NamespaceURIs.XEP0045_MUC_USER, new MucUserItem(
Affiliation.None, Role.None),
// TODO handle <actor>
// TODO handle <reason>
status);
relayStanza(target.getJid(), presenceToKicked, serverRuntimeContext);
}
PresenceStanzaType availType = (newRole == Role.None) ? PresenceStanzaType.UNAVAILABLE : null;
// notify remaining users that user got role updated
MucUserItem presenceItem = new MucUserItem(target.getAffiliation(), newRole);
for (Occupant occupant : room.getOccupants()) {
Stanza presenceToRemaining = MUCStanzaBuilder.createPresenceStanza(targetInRoom, occupant.getJid(),
availType, NamespaceURIs.XEP0045_MUC_USER, presenceItem, status);
relayStanza(occupant.getJid(), presenceToRemaining, serverRuntimeContext);
}
return StanzaBuilder.createIQStanza(stanza.getTo(), stanza.getFrom(), IQStanzaType.RESULT, stanza.getID())
.build();
}
protected void relayStanza(Entity receiver, Stanza stanza, ServerRuntimeContext serverRuntimeContext) {
try {
serverRuntimeContext.getStanzaRelay().relay(receiver, stanza, new IgnoreFailureStrategy());
} catch (DeliveryException e) {
logger.warn("presence relaying failed ", e);
}
}
}