mod_roster.c   [plain text]


/* --------------------------------------------------------------------------
 *
 * License
 *
 * The contents of this file are subject to the Jabber Open Source License
 * Version 1.0 (the "JOSL").  You may not copy or use this file, in either
 * source code or executable form, except in compliance with the JOSL. You
 * may obtain a copy of the JOSL at http://www.jabber.org/ or at
 * http://www.opensource.org/.  
 *
 * Software distributed under the JOSL is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied.  See the JOSL
 * for the specific language governing rights and limitations under the
 * JOSL.
 *
 * Copyrights
 * 
 * Portions created by or assigned to Jabber.com, Inc. are 
 * Copyright (c) 1999-2002 Jabber.com, Inc.  All Rights Reserved.  Contact
 * information for Jabber.com, Inc. is available at http://www.jabber.com/.
 *
 * Portions Copyright (c) 1998-1999 Jeremie Miller.
 * 
 * Acknowledgements
 * 
 * Special thanks to the Jabber Open Source Contributors for their
 * suggestions and support of Jabber.
 * 
 * Alternatively, the contents of this file may be used under the terms of the
 * GNU General Public License Version 2 or later (the "GPL"), in which case
 * the provisions of the GPL are applicable instead of those above.  If you
 * wish to allow use of your version of this file only under the terms of the
 * GPL and not to allow others to use your version of this file under the JOSL,
 * indicate your decision by deleting the provisions above and replace them
 * with the notice and other provisions required by the GPL.  If you do not
 * delete the provisions above, a recipient may use your version of this file
 * under either the JOSL or the GPL. 
 * 
 * 
 * --------------------------------------------------------------------------*/
#include "jsm.h"

xmlnode mod_roster_get(udata u)
{
    xmlnode ret;

    log_debug("mod_roster","getting %s's roster",u->user);

    /* get the existing roster */
    ret = xdb_get(u->si->xc, u->id, NS_ROSTER);
    if(ret == NULL)
    { /* there isn't one, sucky, create a container node and let xdb manage it */
        log_debug("mod_roster","creating");
        ret = xmlnode_new_tag("query");
        xmlnode_put_attrib(ret,"xmlns",NS_ROSTER);
    }

    return ret;
}

xmlnode mod_roster_get_item(xmlnode roster, jid id, char *name, int *newflag)
{
    xmlnode ret;

    log_debug("mod_roster","getting item %s",jid_full(id));

    ret = jid_nodescan(id,roster);

    if(ret == NULL)
    { /* there isn't one, brew one up */
        log_debug("mod_roster","creating");
        ret = xmlnode_insert_tag(roster,"item");
        xmlnode_put_attrib(ret,"jid",jid_full(id));
        if(name != NULL)
            xmlnode_put_attrib(ret,"name",name);
        xmlnode_put_attrib(ret,"subscription","none");
        *newflag = 1;
    }

    return ret;
}

void mod_roster_push(udata user, xmlnode item)
{ /* push the item to all session */
    session cur;
    xmlnode packet, query;

    log_debug("mod_roster","pushing %s",xmlnode2str(item));

    if(xmlnode_get_attrib(item,"hidden") != NULL) return;

    /* create a jpacket roster item push */
    packet = xmlnode_new_tag("iq");
    xmlnode_put_attrib(packet, "type", "set");
    query = xmlnode_insert_tag(packet, "query");
    xmlnode_put_attrib(query,"xmlns",NS_ROSTER);
    xmlnode_insert_tag_node(query,item);
    xmlnode_hide_attrib(xmlnode_get_firstchild(query),"subscribe"); /* hide the server tirds */

    /* send a copy to all session that have a roster */
    for(cur = user->sessions; cur != NULL; cur = cur->next)
        if(cur->roster)
            js_session_to(cur, jpacket_new(xmlnode_dup(packet)));

    xmlnode_free(packet);
}

#define S10N_ADD_FROM 1
#define S10N_ADD_TO 2
#define S10N_REM_FROM 3
#define S10N_REM_TO 4

void mod_roster_set_s10n(int set, xmlnode item)
{
    switch(set)
    { /* LAZY ALERT, yeah, redundant code, gak! */
    case S10N_ADD_FROM:
        if(j_strcmp(xmlnode_get_attrib(item,"subscription"),"to") == 0 || j_strcmp(xmlnode_get_attrib(item,"subscription"),"both") == 0)
            xmlnode_put_attrib(item,"subscription","both");
        else
            xmlnode_put_attrib(item,"subscription","from");
        break;
    case S10N_ADD_TO:
        if(j_strcmp(xmlnode_get_attrib(item,"subscription"),"from") == 0 || j_strcmp(xmlnode_get_attrib(item,"subscription"),"both") == 0)
            xmlnode_put_attrib(item,"subscription","both");
        else
            xmlnode_put_attrib(item,"subscription","to");
        break;
    case S10N_REM_FROM:
        if(j_strcmp(xmlnode_get_attrib(item,"subscription"),"both") == 0 || j_strcmp(xmlnode_get_attrib(item,"subscription"),"to") == 0)
            xmlnode_put_attrib(item,"subscription","to");
        else
            xmlnode_put_attrib(item,"subscription","none");
        break;
    case S10N_REM_TO:
        if(j_strcmp(xmlnode_get_attrib(item,"subscription"),"both") == 0 || j_strcmp(xmlnode_get_attrib(item,"subscription"),"from") == 0)
            xmlnode_put_attrib(item,"subscription","from");
        else
            xmlnode_put_attrib(item,"subscription","none");
        break;
    }
}

/* force presence updates through all sessions, thanks to 'smarter' presence logic */
void mod_roster_pforce(udata u, jid to, int uflag)
{
    session s;
    xmlnode x;

    log_debug(ZONE,"brute forcing presence updates");

    /* loop through all the sessions */
    for(s = u->sessions; s != NULL; s = s->next)
    {
        if(uflag)
            x = jutil_presnew(JPACKET__UNAVAILABLE,NULL,NULL);
        else
            x = xmlnode_dup(s->presence);
        xmlnode_put_attrib(x,"to",jid_full(to));
        js_session_from(s,jpacket_new(x));
    }
}

mreturn mod_roster_out_s10n(mapi m)
{
    xmlnode roster, item;
    int newflag, to, from;
    jid curr;

    if(m->packet->to == NULL) return M_PASS;
    if(jid_cmpx(jid_user(m->s->id),m->packet->to,JID_USER|JID_SERVER) == 0) return M_PASS; /* vanity complex */

    log_debug("mod_roster","handling outgoing s10n");

    newflag = to = from = 0;
    roster = mod_roster_get(m->user);
    item = mod_roster_get_item(roster,m->packet->to,NULL,&newflag);

    /* vars */
    if(j_strcmp(xmlnode_get_attrib(item,"subscription"),"to") == 0)
        to = 1;
    if(j_strcmp(xmlnode_get_attrib(item,"subscription"),"from") == 0)
        from = 1;
    if(j_strcmp(xmlnode_get_attrib(item,"subscription"),"both") == 0)
        to = from = 1;

    switch(jpacket_subtype(m->packet))
    {
    case JPACKET__SUBSCRIBE:
        if(!to)
        {
            xmlnode_put_attrib(item,"ask","subscribe");
            mod_roster_push(m->user, item);
        }
        break;
    case JPACKET__SUBSCRIBED:
        mod_roster_set_s10n(S10N_ADD_FROM,item); /* update subscription */
        jid_append(js_trustees(m->user),m->packet->to); /* make them trusted now */
        xmlnode_hide_attrib(item,"subscribe"); /* cancel any pending requests */
        xmlnode_hide_attrib(item,"hidden"); /* don't hide it anymore */
        mod_roster_pforce(m->user, m->packet->to, 0); /* they are now subscribed to us, send them our presence */
        mod_roster_push(m->user, item);
        break;
    case JPACKET__UNSUBSCRIBE:
        if(to)
        {
            xmlnode_put_attrib(item,"ask","unsubscribe");
            mod_roster_push(m->user, item);
        }else if(newflag){
            xmlnode_hide(item);
        }
        break;
    case JPACKET__UNSUBSCRIBED:
        if(from)
        {
            mod_roster_set_s10n(S10N_REM_FROM,item); /* update subscription */
            /* remove them from the user trusted list */
            for(curr = js_trustees(m->user);curr != NULL && jid_cmp(curr->next,m->packet->to) != 0;curr = curr->next);
            if(curr != NULL && curr->next != NULL)
                curr->next = curr->next->next;
            mod_roster_pforce(m->user, m->packet->to, 1); /* make us offline */
            mod_roster_push(m->user, item);
        }else if(newflag){
            xmlnode_hide(item);
        }else{
            if(xmlnode_get_attrib(item,"hidden") != NULL)
                xmlnode_hide(item); /* remove it for good */
            else
                xmlnode_hide_attrib(item,"subscribe"); /* just cancel any pending requests */
        }
        break;
    }

    /* save the roster */
    /* XXX what do we do if the set fails?  hrmf... */
    xdb_set(m->si->xc, m->user->id, NS_ROSTER, roster);

    /* make sure it's sent from the *user*, not the resource */
    xmlnode_put_attrib(m->packet->x,"from",jid_full(jid_user(m->s->id)));
    jpacket_reset(m->packet);

    xmlnode_free(roster);

    return M_PASS;
}

mreturn mod_roster_out_iq(mapi m)
{
    xmlnode roster, cur, pres, item;
    int newflag;
    jid id;

    if(!NSCHECK(m->packet->iq,NS_ROSTER)) return M_PASS;

    roster = mod_roster_get(m->user);

    switch(jpacket_subtype(m->packet))
    {
    case JPACKET__GET:
        log_debug("mod_roster","handling get request");
        xmlnode_put_attrib(m->packet->x,"type","result");
        m->s->roster = 1;

        /* insert the roster into the result */
        xmlnode_hide(m->packet->iq);
        xmlnode_insert_tag_node(m->packet->x, roster);
        jpacket_reset(m->packet);

        /* filter out pending subscribes */
        for(cur = xmlnode_get_firstchild(m->packet->iq); cur != NULL; cur = xmlnode_get_nextsibling(cur))
        {
            if(xmlnode_get_attrib(cur,"subscribe") != NULL)
                xmlnode_hide_attrib(cur,"subscribe");
            if(xmlnode_get_attrib(cur,"hidden") != NULL)
                xmlnode_hide(cur);
        }

        /* send to the user */
        js_session_to(m->s,m->packet);

        /* redeliver those subscribes */
        for(cur = xmlnode_get_firstchild(roster); cur != NULL; cur = xmlnode_get_nextsibling(cur))
            if(xmlnode_get_attrib(cur,"subscribe") != NULL)
            {
                pres = xmlnode_new_tag("presence");
                xmlnode_put_attrib(pres,"type","subscribe");
                xmlnode_put_attrib(pres,"from",xmlnode_get_attrib(cur,"jid"));
                if(strlen(xmlnode_get_attrib(cur,"subscribe")) > 0)
                    xmlnode_insert_cdata(xmlnode_insert_tag(pres,"status"),xmlnode_get_attrib(cur,"subscribe"),-1);
                js_session_to(m->s,jpacket_new(pres));
            }

        break;
    case JPACKET__SET:
        log_debug("mod_roster","handling set request");

        /* loop through the incoming items updating or creating */
        for(cur = xmlnode_get_firstchild(m->packet->iq); cur != NULL; cur = xmlnode_get_nextsibling(cur))
        {
            if(xmlnode_get_type(cur) != NTYPE_TAG || xmlnode_get_attrib(cur,"jid") == NULL)
                continue;

            id = jid_new(m->packet->p,xmlnode_get_attrib(cur,"jid"));
            if(id == NULL || jid_cmpx(jid_user(m->s->id),id,JID_USER|JID_SERVER) == 0) continue;

            /* zoom to find the existing item in the current roster, and hide it */
            item = mod_roster_get_item(roster, id, NULL, &newflag);
            xmlnode_hide(item);

            /* drop you sukkah */
            if(j_strcmp(xmlnode_get_attrib(cur,"subscription"),"remove") == 0)
            {
                /* cancel our subscription to them */
                if(j_strcmp(xmlnode_get_attrib(item,"subscription"),"both") == 0 || j_strcmp(xmlnode_get_attrib(item,"subscription"),"to") == 0 || j_strcmp(xmlnode_get_attrib(item,"ask"),"subscribe") == 0)
                    js_session_from(m->s,jpacket_new(jutil_presnew(JPACKET__UNSUBSCRIBE,xmlnode_get_attrib(cur,"jid"),NULL)));

                /* tell them their subscription to us is toast */
                if(j_strcmp(xmlnode_get_attrib(item,"subscription"),"both") == 0 || j_strcmp(xmlnode_get_attrib(item,"subscription"),"from") == 0)
                    js_session_from(m->s,jpacket_new(jutil_presnew(JPACKET__UNSUBSCRIBED,xmlnode_get_attrib(cur,"jid"),NULL)));

                /* push this remove out */
                mod_roster_push(m->user,cur);
                continue;
            }

            /* copy the old stuff into the new one and insert it into the roster */
            xmlnode_put_attrib(cur,"subscription",xmlnode_get_attrib(item,"subscription"));
            xmlnode_put_attrib(cur,"ask",xmlnode_get_attrib(item,"ask")); /* prolly not here, but just in case */
            xmlnode_insert_tag_node(roster,cur);

            /* push the new item */
            mod_roster_push(m->user,cur);
        }

        /* send to the user */
        jutil_iqresult(m->packet->x);
        jpacket_reset(m->packet);
        js_session_to(m->s,m->packet);

        /* save the changes */
        log_debug(ZONE,"SROSTER: %s",xmlnode2str(roster));
        /* XXX what do we do if the set fails?  hrmf... */
        xdb_set(m->si->xc, m->user->id, NS_ROSTER, roster);

        break;
    default:
        /* JPACKET__RESULT: result from a roster push to the client */
        xmlnode_free(m->packet->x);
        break;
    }

    xmlnode_free(roster);
    return M_HANDLED;
}

mreturn mod_roster_out(mapi m, void *arg)
{
    if(m->packet->type == JPACKET_IQ) return mod_roster_out_iq(m);
    if(m->packet->type == JPACKET_S10N) return mod_roster_out_s10n(m);

    return M_IGNORE;
}

mreturn mod_roster_session(mapi m, void *arg)
{
    js_mapi_session(es_OUT,m->s,mod_roster_out,NULL);
    return M_PASS;
}

mreturn mod_roster_s10n(mapi m, void *arg)
{
    xmlnode roster, item, reply, reply2;
    char *status;
    session top;
    int newflag, drop, to, from, push;

    push = newflag = drop = to = from = 0;

    /* check for incoming s10n (subscription) requests */
    if(m->packet->type != JPACKET_S10N) return M_IGNORE;

    if(m->user == NULL) return M_PASS;
    if(jid_cmpx(m->packet->from,m->packet->to,JID_USER|JID_SERVER) == 0) return M_PASS; /* vanity complex */

    /* now we can get to work and handle this user's incoming subscription crap */
    roster = mod_roster_get(m->user);
    item = mod_roster_get_item(roster,m->packet->from,xmlnode_get_attrib(m->packet->x,"name"),&newflag);
    reply2 = reply = NULL;
    jid_set(m->packet->to,NULL,JID_RESOURCE); /* make sure we're only dealing w/ the user id */

    log_debug("mod_roster","s10n %s request from %s with existing item %s",xmlnode_get_attrib(m->packet->x,"type"),jid_full(m->packet->from),xmlnode2str(item));

    /* vars */
    if(j_strcmp(xmlnode_get_attrib(item,"subscription"),"to") == 0)
        to = 1;
    if(j_strcmp(xmlnode_get_attrib(item,"subscription"),"from") == 0)
        from = 1;
    if(j_strcmp(xmlnode_get_attrib(item,"subscription"),"both") == 0)
        to = from = 1;

    switch(jpacket_subtype(m->packet))
    {
    case JPACKET__SUBSCRIBE:
        if(from)
        {
            /* already subscribed, respond automatically */
            reply = jutil_presnew(JPACKET__SUBSCRIBED,jid_full(m->packet->from),"Already Subscribed");
            jid_set(m->packet->to,NULL,JID_RESOURCE);
            xmlnode_put_attrib(reply,"from",jid_full(m->packet->to));
            drop = 1;

            /* the other person obviously is re-adding them to their roster, and should be told of the current presence */
            reply2 = jutil_presnew(JPACKET__PROBE,jid_full(m->packet->to),NULL);
            xmlnode_put_attrib(reply2,"from",jid_full(m->packet->from));

        }else{
            /* tuck request in the roster */
            status = xmlnode_get_tag_data(m->packet->x,"status");
            if(status == NULL)
                xmlnode_put_attrib(item,"subscribe","");
            else
                xmlnode_put_attrib(item,"subscribe",status);
            if(newflag) /* SPECIAL CASE: special flag so that we can hide these incoming subscribe requests */
                xmlnode_put_attrib(item,"hidden","");
        }
        break;
    case JPACKET__SUBSCRIBED:
        if(to)
        { /* already subscribed, drop */
            drop = 1;
        }else{
            /* cancel any ask, s10n=to */
            xmlnode_hide_attrib(item,"ask");
            mod_roster_set_s10n(S10N_ADD_TO,item);
            push = 1;
        }
        break;
    case JPACKET__UNSUBSCRIBE:
        if(from)
        {
            /* remove s10n=from */
            xmlnode_hide_attrib(item,"subscribe");
            mod_roster_set_s10n(S10N_REM_FROM,item);
            if(xmlnode_get_attrib(item,"hidden") != NULL)
                xmlnode_hide(item);
            else
                push = 1;
        }else{
            if(newflag)
                xmlnode_hide(item);
            drop = 1;
        }
        /* respond automatically */
        reply = jutil_presnew(JPACKET__UNSUBSCRIBED,jid_full(m->packet->from),"Autoreply");
        jid_set(m->packet->to,NULL,JID_RESOURCE);
        xmlnode_put_attrib(reply,"from",jid_full(m->packet->to));
        break;
    case JPACKET__UNSUBSCRIBED:
        if(to || xmlnode_get_attrib(item,"ask") != NULL)
        {
            /* cancel any ask, remove s10n=to */
            xmlnode_hide_attrib(item,"ask");
            mod_roster_set_s10n(S10N_REM_TO,item);
            push = 1;
        }else{
            if(newflag)
                xmlnode_hide(item);
            drop = 1;
        }
    }

    /* XXX what do we do if the set fails?  hrmf... */
    xdb_set(m->si->xc, m->user->id, NS_ROSTER, roster);

    /* these are delayed until after we check the roster back in, avoid rancid race conditions */
    if(reply != NULL)
        js_deliver(m->si,jpacket_new(reply));
    if(reply2 != NULL)
        js_deliver(m->si,jpacket_new(reply2));

    /* find primary session */
    top = js_session_primary(m->user);

    /* if we can, deliver this to that session */
    if(!drop && top != NULL && top->roster)
        js_session_to(top,m->packet);
    else
        xmlnode_free(m->packet->x);

    if(push)
        mod_roster_push(m->user,item);

    xmlnode_free(roster);
    return M_HANDLED;
}

void mod_roster(jsmi si)
{
    /* we just register for new sessions */
    js_mapi_register(si,e_SESSION,mod_roster_session,NULL);
    js_mapi_register(si,e_DELIVER,mod_roster_s10n,NULL);
}