source: protocols/jabber/jabber_util.c @ e7dc02a

Last change on this file since e7dc02a was ae3dc99, checked in by Wilmer van der Gaast <wilmer@…>, at 2010-04-24T17:02:07Z

Merging stuff from mainline (1.2.6).

  • Property mode set to 100644
File size: 19.6 KB
Line 
1/***************************************************************************\
2*                                                                           *
3*  BitlBee - An IRC to IM gateway                                           *
4*  Jabber module - Misc. stuff                                              *
5*                                                                           *
6*  Copyright 2006-2010 Wilmer van der Gaast <wilmer@gaast.net>             
7*                                                                           *
8*  This program is free software; you can redistribute it and/or modify     *
9*  it under the terms of the GNU General Public License as published by     *
10*  the Free Software Foundation; either version 2 of the License, or        *
11*  (at your option) any later version.                                      *
12*                                                                           *
13*  This program is distributed in the hope that it will be useful,          *
14*  but WITHOUT ANY WARRANTY; without even the implied warranty of           *
15*  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the            *
16*  GNU General Public License for more details.                             *
17*                                                                           *
18*  You should have received a copy of the GNU General Public License along  *
19*  with this program; if not, write to the Free Software Foundation, Inc.,  *
20*  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.              *
21*                                                                           *
22\***************************************************************************/
23
24#include "jabber.h"
25#include "md5.h"
26#include "base64.h"
27
28static unsigned int next_id = 1;
29
30char *set_eval_priority( set_t *set, char *value )
31{
32        account_t *acc = set->data;
33        int i;
34       
35        if( sscanf( value, "%d", &i ) == 1 )
36        {
37                /* Priority is a signed 8-bit integer, according to RFC 3921. */
38                if( i < -128 || i > 127 )
39                        return SET_INVALID;
40        }
41        else
42                return SET_INVALID;
43       
44        /* Only run this stuff if the account is online ATM,
45           and if the setting seems to be acceptable. */
46        if( acc->ic )
47        {
48                /* Although set_eval functions usually are very nice and
49                   convenient, they have one disadvantage: If I would just
50                   call p_s_u() now to send the new prio setting, it would
51                   send the old setting because the set->value gets changed
52                   after the (this) eval returns a non-NULL value.
53                   
54                   So now I can choose between implementing post-set
55                   functions next to evals, or just do this little hack: */
56               
57                g_free( set->value );
58                set->value = g_strdup( value );
59               
60                /* (Yes, sorry, I prefer the hack. :-P) */
61               
62                presence_send_update( acc->ic );
63        }
64       
65        return value;
66}
67
68char *set_eval_tls( set_t *set, char *value )
69{
70        if( g_strcasecmp( value, "try" ) == 0 )
71                return value;
72        else
73                return set_eval_bool( set, value );
74}
75
76struct xt_node *jabber_make_packet( char *name, char *type, char *to, struct xt_node *children )
77{
78        struct xt_node *node;
79       
80        node = xt_new_node( name, NULL, children );
81       
82        if( type )
83                xt_add_attr( node, "type", type );
84        if( to )
85                xt_add_attr( node, "to", to );
86       
87        /* IQ packets should always have an ID, so let's generate one. It
88           might get overwritten by jabber_cache_add() if this packet has
89           to be saved until we receive a response. Cached packets get
90           slightly different IDs so we can recognize them. */
91        if( strcmp( name, "iq" ) == 0 )
92        {
93                char *id = g_strdup_printf( "%s%05x", JABBER_PACKET_ID, ( next_id++ ) & 0xfffff );
94                xt_add_attr( node, "id", id );
95                g_free( id );
96        }
97       
98        return node;
99}
100
101struct xt_node *jabber_make_error_packet( struct xt_node *orig, char *err_cond, char *err_type, char *err_code )
102{
103        struct xt_node *node, *c;
104        char *to;
105       
106        /* Create the "defined-condition" tag. */
107        c = xt_new_node( err_cond, NULL, NULL );
108        xt_add_attr( c, "xmlns", XMLNS_STANZA_ERROR );
109       
110        /* Put it in an <error> tag. */
111        c = xt_new_node( "error", NULL, c );
112        xt_add_attr( c, "type", err_type );
113       
114        /* Add the error code, if present */
115        if (err_code)
116                xt_add_attr( c, "code", err_code );
117       
118        /* To make the actual error packet, we copy the original packet and
119           add our <error>/type="error" tag. Including the original packet
120           is recommended, so let's just do it. */
121        node = xt_dup( orig );
122        xt_add_child( node, c );
123        xt_add_attr( node, "type", "error" );
124       
125        /* Return to sender. */
126        if( ( to = xt_find_attr( node, "from" ) ) )
127        {
128                xt_add_attr( node, "to", to );
129                xt_remove_attr( node, "from" );
130        }
131               
132        return node;
133}
134
135/* Cache a node/packet for later use. Mainly useful for IQ packets if you need
136   them when you receive the response. Use this BEFORE sending the packet so
137   it'll get a new id= tag, and do NOT free() the packet after sending it! */
138void jabber_cache_add( struct im_connection *ic, struct xt_node *node, jabber_cache_event func )
139{
140        struct jabber_data *jd = ic->proto_data;
141        struct jabber_cache_entry *entry = g_new0( struct jabber_cache_entry, 1 );
142        md5_state_t id_hash;
143        md5_byte_t id_sum[16];
144        char *id, *asc_hash;
145       
146        next_id ++;
147       
148        id_hash = jd->cached_id_prefix;
149        md5_append( &id_hash, (md5_byte_t*) &next_id, sizeof( next_id ) );
150        md5_finish( &id_hash, id_sum );
151        asc_hash = base64_encode( id_sum, 12 );
152       
153        id = g_strdup_printf( "%s%s", JABBER_CACHED_ID, asc_hash );
154        xt_add_attr( node, "id", id );
155        g_free( id );
156        g_free( asc_hash );
157       
158        entry->node = node;
159        entry->func = func;
160        entry->saved_at = time( NULL );
161        g_hash_table_insert( jd->node_cache, xt_find_attr( node, "id" ), entry );
162}
163
164void jabber_cache_entry_free( gpointer data )
165{
166        struct jabber_cache_entry *entry = data;
167       
168        xt_free_node( entry->node );
169        g_free( entry );
170}
171
172gboolean jabber_cache_clean_entry( gpointer key, gpointer entry, gpointer nullpointer );
173
174/* This one should be called from time to time (from keepalive, in this case)
175   to make sure things don't stay in the node cache forever. By marking nodes
176   during the first run and deleting marked nodes during a next run, every
177   node should be available in the cache for at least a minute (assuming the
178   function is indeed called every minute). */
179void jabber_cache_clean( struct im_connection *ic )
180{
181        struct jabber_data *jd = ic->proto_data;
182        time_t threshold = time( NULL ) - JABBER_CACHE_MAX_AGE;
183       
184        g_hash_table_foreach_remove( jd->node_cache, jabber_cache_clean_entry, &threshold );
185}
186
187gboolean jabber_cache_clean_entry( gpointer key, gpointer entry_, gpointer threshold_ )
188{
189        struct jabber_cache_entry *entry = entry_;
190        time_t *threshold = threshold_;
191       
192        return entry->saved_at < *threshold;
193}
194
195xt_status jabber_cache_handle_packet( struct im_connection *ic, struct xt_node *node )
196{
197        struct jabber_data *jd = ic->proto_data;
198        struct jabber_cache_entry *entry;
199        char *s;
200       
201        if( ( s = xt_find_attr( node, "id" ) ) == NULL ||
202            strncmp( s, JABBER_CACHED_ID, strlen( JABBER_CACHED_ID ) ) != 0 )
203        {
204                /* Silently ignore it, without an ID (or a non-cache
205                   ID) we don't know how to handle the packet and we
206                   probably don't have to. */
207                return XT_HANDLED;
208        }
209       
210        entry = g_hash_table_lookup( jd->node_cache, s );
211       
212        if( entry == NULL )
213        {
214                /*
215                There's no longer an easy way to see if we generated this
216                one or someone else, and there's a ten-minute timeout anyway,
217                so meh.
218               
219                imcb_log( ic, "Warning: Received %s-%s packet with unknown/expired ID %s!",
220                              node->name, xt_find_attr( node, "type" ) ? : "(no type)", s );
221                */
222        }
223        else if( entry->func )
224        {
225                return entry->func( ic, node, entry->node );
226        }
227       
228        return XT_HANDLED;
229}
230
231const struct jabber_away_state jabber_away_state_list[] =
232{
233        { "away",  "Away" },
234        { "chat",  "Free for Chat" },   /* WTF actually uses this? */
235        { "dnd",   "Do not Disturb" },
236        { "xa",    "Extended Away" },
237        { "",      NULL }
238};
239
240const struct jabber_away_state *jabber_away_state_by_code( char *code )
241{
242        int i;
243       
244        if( code == NULL )
245                return NULL;
246       
247        for( i = 0; jabber_away_state_list[i].full_name; i ++ )
248                if( g_strcasecmp( jabber_away_state_list[i].code, code ) == 0 )
249                        return jabber_away_state_list + i;
250       
251        return NULL;
252}
253
254const struct jabber_away_state *jabber_away_state_by_name( char *name )
255{
256        int i;
257       
258        if( name == NULL )
259                return NULL;
260       
261        for( i = 0; jabber_away_state_list[i].full_name; i ++ )
262                if( g_strcasecmp( jabber_away_state_list[i].full_name, name ) == 0 )
263                        return jabber_away_state_list + i;
264       
265        return NULL;
266}
267
268struct jabber_buddy_ask_data
269{
270        struct im_connection *ic;
271        char *handle;
272        char *realname;
273};
274
275static void jabber_buddy_ask_yes( void *data )
276{
277        struct jabber_buddy_ask_data *bla = data;
278       
279        presence_send_request( bla->ic, bla->handle, "subscribed" );
280       
281        if( imcb_find_buddy( bla->ic, bla->handle ) == NULL )
282                imcb_ask_add( bla->ic, bla->handle, NULL );
283       
284        g_free( bla->handle );
285        g_free( bla );
286}
287
288static void jabber_buddy_ask_no( void *data )
289{
290        struct jabber_buddy_ask_data *bla = data;
291       
292        presence_send_request( bla->ic, bla->handle, "subscribed" );
293       
294        g_free( bla->handle );
295        g_free( bla );
296}
297
298void jabber_buddy_ask( struct im_connection *ic, char *handle )
299{
300        struct jabber_buddy_ask_data *bla = g_new0( struct jabber_buddy_ask_data, 1 );
301        char *buf;
302       
303        bla->ic = ic;
304        bla->handle = g_strdup( handle );
305       
306        buf = g_strdup_printf( "The user %s wants to add you to his/her buddy list.", handle );
307        imcb_ask( ic, buf, bla, jabber_buddy_ask_yes, jabber_buddy_ask_no );
308        g_free( buf );
309}
310
311/* Returns a new string. Don't leak it! */
312char *jabber_normalize( const char *orig )
313{
314        int len, i;
315        char *new;
316       
317        len = strlen( orig );
318        new = g_new( char, len + 1 );
319       
320        /* So it turns out the /resource part is case sensitive. Yeah, and
321           it's Unicode but feck Unicode. :-P So stop once we see a slash. */
322        for( i = 0; i < len && orig[i] != '/' ; i ++ )
323                new[i] = tolower( orig[i] );
324        for( ; orig[i]; i ++ )
325                new[i] = orig[i];
326       
327        new[i] = 0;
328        return new;
329}
330
331/* Adds a buddy/resource to our list. Returns NULL if full_jid is not really a
332   FULL jid or if we already have this buddy/resource. XXX: No, great, actually
333   buddies from transports don't (usually) have resources. So we'll really have
334   to deal with that properly. Set their ->resource property to NULL. Do *NOT*
335   allow to mix this stuff, though... */
336struct jabber_buddy *jabber_buddy_add( struct im_connection *ic, char *full_jid_ )
337{
338        struct jabber_data *jd = ic->proto_data;
339        struct jabber_buddy *bud, *new, *bi;
340        char *s, *full_jid;
341       
342        full_jid = jabber_normalize( full_jid_ );
343       
344        if( ( s = strchr( full_jid, '/' ) ) )
345                *s = 0;
346       
347        new = g_new0( struct jabber_buddy, 1 );
348       
349        if( ( bud = g_hash_table_lookup( jd->buddies, full_jid ) ) )
350        {
351                /* The first entry is always a bare JID. If there are more, we
352                   should ignore the first one here. */
353                if( bud->next )
354                        bud = bud->next;
355               
356                /* If this is a transport buddy or whatever, it can't have more
357                   than one instance, so this is always wrong: */
358                if( s == NULL || bud->resource == NULL )
359                {
360                        if( s ) *s = '/';
361                        g_free( new );
362                        g_free( full_jid );
363                        return NULL;
364                }
365               
366                new->bare_jid = bud->bare_jid;
367               
368                /* We already have another resource for this buddy, add the
369                   new one to the list. */
370                for( bi = bud; bi; bi = bi->next )
371                {
372                        /* Check for dupes. */
373                        if( strcmp( bi->resource, s + 1 ) == 0 )
374                        {
375                                *s = '/';
376                                g_free( new );
377                                g_free( full_jid );
378                                return NULL;
379                        }
380                        /* Append the new item to the list. */
381                        else if( bi->next == NULL )
382                        {
383                                bi->next = new;
384                                break;
385                        }
386                }
387        }
388        else
389        {
390                new->full_jid = new->bare_jid = g_strdup( full_jid );
391                g_hash_table_insert( jd->buddies, new->bare_jid, new );
392               
393                if( s )
394                {
395                        new->next = g_new0( struct jabber_buddy, 1 );
396                        new->next->bare_jid = new->bare_jid;
397                        new = new->next;
398                }
399        }
400       
401        if( s )
402        {
403                *s = '/';
404                new->full_jid = full_jid;
405                new->resource = strchr( new->full_jid, '/' ) + 1;
406        }
407        else
408        {
409                /* Let's waste some more bytes of RAM instead of to make
410                   memory management a total disaster here. And it saves
411                   me one g_free() call in this function. :-P */
412                new->full_jid = full_jid;
413        }
414       
415        return new;
416}
417
418/* Finds a buddy from our structures. Can find both full- and bare JIDs. When
419   asked for a bare JID, it uses the "resource_select" setting to see which
420   resource to pick. */
421struct jabber_buddy *jabber_buddy_by_jid( struct im_connection *ic, char *jid_, get_buddy_flags_t flags )
422{
423        struct jabber_data *jd = ic->proto_data;
424        struct jabber_buddy *bud, *head;
425        char *s, *jid;
426       
427        jid = jabber_normalize( jid_ );
428       
429        if( ( s = strchr( jid, '/' ) ) )
430        {
431                int bare_exists = 0;
432               
433                *s = 0;
434                if( ( bud = g_hash_table_lookup( jd->buddies, jid ) ) )
435                {
436                        bare_exists = 1;
437                       
438                        if( bud->next )
439                                bud = bud->next;
440                       
441                        /* Just return the first one for this bare JID. */
442                        if( flags & GET_BUDDY_FIRST )
443                        {
444                                *s = '/';
445                                g_free( jid );
446                                return bud;
447                        }
448                       
449                        /* Is this one of those no-resource buddies? */
450                        if( bud->resource == NULL )
451                        {
452                                *s = '/';
453                                g_free( jid );
454                                return NULL;
455                        }
456                       
457                        /* See if there's an exact match. */
458                        for( ; bud; bud = bud->next )
459                                if( strcmp( bud->resource, s + 1 ) == 0 )
460                                        break;
461                }
462               
463                if( bud == NULL && ( flags & GET_BUDDY_CREAT ) &&
464                    ( bare_exists || imcb_find_buddy( ic, jid ) ) )
465                {
466                        *s = '/';
467                        bud = jabber_buddy_add( ic, jid );
468                }
469               
470                g_free( jid );
471                return bud;
472        }
473        else
474        {
475                struct jabber_buddy *best_prio, *best_time;
476                char *set;
477               
478                head = g_hash_table_lookup( jd->buddies, jid );
479                bud = ( head && head->next ) ? head->next : head;
480               
481                g_free( jid );
482               
483                if( bud == NULL )
484                        /* No match. Create it now? */
485                        return ( ( flags & GET_BUDDY_CREAT ) && imcb_find_buddy( ic, jid_ ) ) ?
486                                   jabber_buddy_add( ic, jid_ ) : NULL;
487                else if( bud->resource && ( flags & GET_BUDDY_EXACT ) )
488                        /* We want an exact match, so in thise case there shouldn't be a /resource. */
489                        return NULL;
490                else if( bud->resource == NULL || bud->next == NULL )
491                        /* No need for selection if there's only one option. */
492                        return bud;
493                else if( flags & GET_BUDDY_FIRST )
494                        /* Looks like the caller doesn't care about details. */
495                        return bud;
496                else if( flags & GET_BUDDY_BARE )
497                        return head;
498               
499                best_prio = best_time = bud;
500                for( ; bud; bud = bud->next )
501                {
502                        if( bud->priority > best_prio->priority )
503                                best_prio = bud;
504                        if( bud->last_msg > best_time->last_msg )
505                                best_time = bud;
506                }
507               
508                if( ( set = set_getstr( &ic->acc->set, "resource_select" ) ) == NULL )
509                        return NULL;
510                else if( strcmp( set, "priority" ) == 0 )
511                        return best_prio;
512                else if( flags & GET_BUDDY_BARE_OK ) /* && strcmp( set, "activity" ) == 0 */
513                {
514                        if( best_time->last_msg + set_getint( &ic->acc->set, "activity_timeout" ) >= time( NULL ) )
515                                return best_time;
516                        else
517                                return head;
518                }
519                else
520                        return best_time;
521        }
522}
523
524/* I'm keeping a separate ext_jid attribute to save a JID that makes sense
525   to export to BitlBee. This is mainly for groupchats right now. It's
526   a bit of a hack, but I just think having the user nickname in the hostname
527   part of the hostmask doesn't look nice on IRC. Normally you can convert
528   a normal JID to ext_jid by swapping the part before and after the / and
529   replacing the / with a =. But there should be some stripping (@s are
530   allowed in Jabber nicks...). */
531struct jabber_buddy *jabber_buddy_by_ext_jid( struct im_connection *ic, char *jid_, get_buddy_flags_t flags )
532{
533        struct jabber_buddy *bud;
534        char *s, *jid;
535       
536        jid = jabber_normalize( jid_ );
537       
538        if( ( s = strchr( jid, '=' ) ) == NULL )
539                return NULL;
540       
541        for( bud = jabber_buddy_by_jid( ic, s + 1, GET_BUDDY_FIRST ); bud; bud = bud->next )
542        {
543                /* Hmmm, could happen if not all people in the chat are anonymized? */
544                if( bud->ext_jid == NULL )
545                        continue;
546               
547                if( strcmp( bud->ext_jid, jid ) == 0 )
548                        break;
549        }
550       
551        g_free( jid );
552       
553        return bud;
554}
555
556/* Remove one specific full JID from our list. Use this when a buddy goes
557   off-line (because (s)he can still be online from a different location.
558   XXX: See above, we should accept bare JIDs too... */
559int jabber_buddy_remove( struct im_connection *ic, char *full_jid_ )
560{
561        struct jabber_data *jd = ic->proto_data;
562        struct jabber_buddy *bud, *prev = NULL, *bi;
563        char *s, *full_jid;
564       
565        full_jid = jabber_normalize( full_jid_ );
566       
567        if( ( s = strchr( full_jid, '/' ) ) )
568                *s = 0;
569       
570        if( ( bud = g_hash_table_lookup( jd->buddies, full_jid ) ) )
571        {
572                if( bud->next )
573                        bud = (prev=bud)->next;
574               
575                /* If there's only one item in the list (and if the resource
576                   matches), removing it is simple. (And the hash reference
577                   should be removed too!) */
578                if( bud->next == NULL &&
579                    ( ( s == NULL && bud->resource == NULL ) ||
580                      ( bud->resource && s && strcmp( bud->resource, s + 1 ) == 0 ) ) )
581                {
582                        return jabber_buddy_remove_bare( ic, full_jid );
583                }
584                else if( s == NULL || bud->resource == NULL )
585                {
586                        /* Tried to remove a bare JID while this JID does seem
587                           to have resources... (Or the opposite.) *sigh* */
588                        g_free( full_jid );
589                        return 0;
590                }
591                else
592                {
593                        for( bi = bud; bi; bi = (prev=bi)->next )
594                                if( strcmp( bi->resource, s + 1 ) == 0 )
595                                        break;
596                       
597                        g_free( full_jid );
598                       
599                        if( bi )
600                        {
601                                if( prev )
602                                        prev->next = bi->next;
603                                else
604                                        /* Don't think this should ever happen anymore. */
605                                        g_hash_table_replace( jd->buddies, bi->bare_jid, bi->next );
606                               
607                                g_free( bi->ext_jid );
608                                g_free( bi->full_jid );
609                                g_free( bi->away_message );
610                                g_free( bi );
611                               
612                                return 1;
613                        }
614                        else
615                        {
616                                return 0;
617                        }
618                }
619        }
620        else
621        {
622                g_free( full_jid );
623                return 0;
624        }
625}
626
627/* Remove a buddy completely; removes all resources that belong to the
628   specified bare JID. Use this when removing someone from the contact
629   list, for example. */
630int jabber_buddy_remove_bare( struct im_connection *ic, char *bare_jid )
631{
632        struct jabber_data *jd = ic->proto_data;
633        struct jabber_buddy *bud, *next;
634       
635        if( strchr( bare_jid, '/' ) )
636                return 0;
637       
638        if( ( bud = jabber_buddy_by_jid( ic, bare_jid, GET_BUDDY_FIRST ) ) )
639        {
640                /* Most important: Remove the hash reference. We don't know
641                   this buddy anymore. */
642                g_hash_table_remove( jd->buddies, bud->bare_jid );
643                g_free( bud->bare_jid );
644               
645                /* Deallocate the linked list of resources. */
646                while( bud )
647                {
648                        /* ext_jid && anonymous means that this buddy is
649                           specific to one groupchat (the one we're
650                           currently cleaning up) so it can be deleted
651                           completely. */
652                        if( bud->ext_jid && bud->flags & JBFLAG_IS_ANONYMOUS )
653                                imcb_remove_buddy( ic, bud->ext_jid, NULL );
654                       
655                        next = bud->next;
656                        g_free( bud->ext_jid );
657                        g_free( bud->full_jid );
658                        g_free( bud->away_message );
659                        g_free( bud );
660                        bud = next;
661                }
662               
663                return 1;
664        }
665        else
666        {
667                return 0;
668        }
669}
670
671time_t jabber_get_timestamp( struct xt_node *xt )
672{
673        struct xt_node *c;
674        char *s = NULL;
675        struct tm tp;
676       
677        for( c = xt->children; ( c = xt_find_node( c, "x" ) ); c = c->next )
678        {
679                if( ( s = xt_find_attr( c, "xmlns" ) ) && strcmp( s, XMLNS_DELAY ) == 0 )
680                        break;
681        }
682       
683        if( !c || !( s = xt_find_attr( c, "stamp" ) ) )
684                return 0;
685       
686        memset( &tp, 0, sizeof( tp ) );
687        if( sscanf( s, "%4d%2d%2dT%2d:%2d:%2d", &tp.tm_year, &tp.tm_mon, &tp.tm_mday,
688                                                &tp.tm_hour, &tp.tm_min, &tp.tm_sec ) != 6 )
689                return 0;
690       
691        tp.tm_year -= 1900;
692        tp.tm_mon --;
693       
694        return mktime_utc( &tp );
695}
696
697struct jabber_error *jabber_error_parse( struct xt_node *node, char *xmlns )
698{
699        struct jabber_error *err;
700        struct xt_node *c;
701        char *s;
702       
703        if( node == NULL )
704                return NULL;
705       
706        err = g_new0( struct jabber_error, 1 );
707        err->type = xt_find_attr( node, "type" );
708       
709        for( c = node->children; c; c = c->next )
710        {
711                if( !( s = xt_find_attr( c, "xmlns" ) ) ||
712                    strcmp( s, xmlns ) != 0 )
713                        continue;
714               
715                if( strcmp( c->name, "text" ) != 0 )
716                {
717                        err->code = c->name;
718                }
719                /* Only use the text if it doesn't have an xml:lang attribute,
720                   if it's empty or if it's set to something English. */
721                else if( !( s = xt_find_attr( c, "xml:lang" ) ) ||
722                         !*s || strncmp( s, "en", 2 ) == 0 )
723                {
724                        err->text = c->text;
725                }
726        }
727       
728        return err;
729}
730
731void jabber_error_free( struct jabber_error *err )
732{
733        g_free( err );
734}
Note: See TracBrowser for help on using the repository browser.