diff options
author | Andre Moreira Magalhaes (andrunko) <andre.magalhaes@collabora.co.uk> | 2010-04-14 13:24:02 -0300 |
---|---|---|
committer | Andre Moreira Magalhaes (andrunko) <andre.magalhaes@collabora.co.uk> | 2010-04-14 13:24:11 -0300 |
commit | 52679ccc631a6b8544cf5ce373fca6b22501fb4b (patch) | |
tree | dc9d5dbea07a8a4a29acce93163caef038ec6313 | |
parent | 32c575e530251420d69c24c2768517ce5e07b889 (diff) | |
parent | ac516315a046e0e086822c7aba97df63b890eea3 (diff) |
Merge branch 'contact-info'
Reviewed-by: Simon McVittie <simon.mcvittie@collabora.co.uk>
-rw-r--r-- | extensions/Connection_Interface_Contact_Info.xml | 136 | ||||
-rw-r--r-- | src/Makefile.am | 2 | ||||
-rw-r--r-- | src/conn-aliasing.c | 12 | ||||
-rw-r--r-- | src/conn-avatars.c | 31 | ||||
-rw-r--r-- | src/conn-contact-info.c | 1152 | ||||
-rw-r--r-- | src/conn-contact-info.h | 40 | ||||
-rw-r--r-- | src/connection.c | 27 | ||||
-rw-r--r-- | src/connection.h | 6 | ||||
-rw-r--r-- | src/util.c | 7 | ||||
-rw-r--r-- | src/util.h | 2 | ||||
-rw-r--r-- | src/vcard-manager.c | 592 | ||||
-rw-r--r-- | src/vcard-manager.h | 45 | ||||
-rw-r--r-- | tests/twisted/Makefile.am | 4 | ||||
-rw-r--r-- | tests/twisted/constants.py | 5 | ||||
-rw-r--r-- | tests/twisted/servicetest.py | 1 | ||||
-rw-r--r-- | tests/twisted/vcard/get-contact-info.py | 65 | ||||
-rw-r--r-- | tests/twisted/vcard/overlapping-sets.py | 96 | ||||
-rw-r--r-- | tests/twisted/vcard/redundant-set.py | 13 | ||||
-rw-r--r-- | tests/twisted/vcard/refresh-contact-info.py | 62 | ||||
-rw-r--r-- | tests/twisted/vcard/set-contact-info.py | 302 | ||||
-rw-r--r-- | tests/twisted/vcard/supported-fields.py | 107 |
21 files changed, 2506 insertions, 201 deletions
diff --git a/extensions/Connection_Interface_Contact_Info.xml b/extensions/Connection_Interface_Contact_Info.xml index d08545466..e8681c939 100644 --- a/extensions/Connection_Interface_Contact_Info.xml +++ b/extensions/Connection_Interface_Contact_Info.xml @@ -110,6 +110,11 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.</ city), region (e.g., state or province), the postal code, and the country name.</dd> + <dt>label</dt> + <dd>A free-form street address for the contact, formatted as a + single value (with embedded newlines where necessary) suitable for + printing on an address label</dd> + <dt>tel</dt> <dd>A telephone number for the contact.</dd> @@ -124,9 +129,10 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.</ VERSION:3.0 FN:Wee Ninja N;LANGUAGE=ja:Ninja;Wee;;;-san - ORG:Collabora, Ltd.;Human Resources\; Company Policy Enforcement + ORG:Collabora, Ltd.;Management Division;Human Resources\; Company Policy Enforcement ADR;TYPE=WORK,POSTAL,PARCEL:;;11 Kings Parade;Cambridge;Cambridgeshire ;CB2 1SJ;UK + LABEL;TYPE=WORK,POSTAL,PARCEL:11 Kings Parade\nCambridge\nCambridgeshire\nUK\nCB2 1SJ TEL;TYPE=VOICE,WORK:+44 1223 362967, +44 7700 900753 EMAIL;TYPE=INTERNET,PREF:wee.ninja@collabora.co.uk EMAIL;TYPE=INTERNET:wee.ninja@example.com @@ -140,9 +146,16 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.</ [ ('fn', [], ['Wee Ninja']), ('n', ['language=ja'], ['Ninja', 'Wee', '', '', '-san']), - ('org', [], ['Collabora, Ltd.', 'Human Resources; Company Policy Enforcement']), + ('org', [], ['Collabora, Ltd.', 'Management Division', + 'Human Resources; Company Policy Enforcement']), ('adr', ['type=work','type=postal','type=parcel'], ['','','11 Kings Parade','Cambridge', 'Cambridgeshire','CB2 1SJ','UK']), + ('label', ['type=work','type=postal','type=parcel'], + ['''11 Kings Parade + Cambridge + Cambridgeshire + UK + CB2 1SJ''']), ('tel', ['type=voice','type=work'], ['+44 1223 362967']), ('tel', ['type=voice','type=work'], ['+44 7700 900753']), ('email', ['type=internet','type=pref'], ['wee.ninja@collabora.co.uk']), @@ -162,7 +175,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.</ name="Contact_Info"/> </tp:mapping> - <signal name="ContactInfoChanged" tp:name-for-bindings="ContactInfoChanged"> + <signal name="ContactInfoChanged" tp:name-for-bindings="Contact_Info_Changed"> <arg name="Contact" type="u" tp:type="Contact_Handle"> <tp:docstring> An integer handle for the contact whose info has changed. @@ -197,10 +210,34 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.</ <tp:docstring> Request information on several contacts at once. This SHOULD only return cached information, omitting handles for which no information is - cached from the returned map. For contacts without cached information, - the information SHOULD be requested from the network, with the result - signalled later by <tp:member-ref>ContactInfoChanged</tp:member-ref>. + cached from the returned map. + </tp:docstring> + </method> + + <method name="RefreshContactInfo" + tp:name-for-bindings="Refresh_Contact_Info"> + <arg direction="in" name="Contacts" type="au" tp:type="Contact_Handle[]"> + <tp:docstring> + Integer handles for contacts. + </tp:docstring> + </arg> + <tp:added version="0.19.UNRELEASED"/> + <tp:docstring> + Retrieve information for the given contact, requesting it from the + network if an up-to-date version is not cached locally. This method + SHOULD return immediately, emitting + <tp:member-ref>ContactInfoChanged</tp:member-ref> when the contacts' + updated contact information is returned. + + <tp:rationale> + This method allows a client with cached contact information to + update its cache after a number of days. + </tp:rationale> </tp:docstring> + <tp:possible-errors> + <tp:error name="org.freedesktop.Telepathy.Error.Disconnected"/> + <tp:error name="org.freedesktop.Telepathy.Error.InvalidHandle"/> + </tp:possible-errors> </method> <method name="RequestContactInfo" @@ -219,8 +256,17 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.</ <tp:docstring> Retrieve information for a contact, requesting it from the network if it is not cached locally. + + <tp:rationale> + This method is appropriate for an explicit user request to show + a contact's information; it allows a UI to wait for the contact + info to be returned. + </tp:rationale> </tp:docstring> <tp:possible-errors> + <tp:error name="org.freedesktop.Telepathy.Error.Disconnected"/> + <tp:error name="org.freedesktop.Telepathy.Error.NetworkError"/> + <tp:error name="org.freedesktop.Telepathy.Error.InvalidHandle"/> <tp:error name="org.freedesktop.Telepathy.Error.NotAvailable"> <tp:docstring> The contact's information could not be retrieved. @@ -262,7 +308,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.</ </tp:possible-errors> </method> - <tp:enum name="Contact_Info_Flag" value-prefix="Contact_Info_Flag" + <tp:enum name="Contact_Info_Flags" value-prefix="Contact_Info_Flag" type="u"> <tp:docstring> Flags defining the behaviour of contact information on this protocol. @@ -282,8 +328,6 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.</ <tp:docstring> Indicates that the protocol pushes all contacts' information to the connection manager without prompting. If set, - <tp:member-ref>RequestContactInfo</tp:member-ref> will not cause a - network roundtrip and <tp:member-ref>ContactInfoChanged</tp:member-ref> will be emitted whenever contacts' information changes. </tp:docstring> @@ -309,10 +353,22 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.</ </tp:simple-type> <property name="ContactInfoFlags" type="u" access="read" - tp:type="Contact_Info_Flag" tp:name-for-bindings="Contact_Info_Flags"> - <tp:docstring> - An integer representing the bitwise-OR of flags on this connection. - This property should be constant over the lifetime of a connection. + tp:type="Contact_Info_Flags" tp:name-for-bindings="Contact_Info_Flags"> + <tp:docstring xmlns="http://www.w3.org/1999/xhtml"> + <p>An integer representing the bitwise-OR of flags on this + connection.</p> + + <p>This property MAY change, without change notification, at any time + before the connection moves to status Connection_Status_Connected. + It MUST NOT change after that point.</p> + + <tp:rationale> + <p>Some XMPP servers, like Facebook Chat, do not allow the vCard to + be changed (and so would not have the Can_Set flag). Whether the + user's server is one of these cannot necessarily be detected until + quite late in the connection process.</p> + </tp:rationale> + </tp:docstring> </property> @@ -328,8 +384,8 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.</ <tp:member type="as" name="Parameters" tp:type="VCard_Type_Parameter[]"> <tp:docstring>The set of vCard type parameters which may be set on this field. If this list is empty and the - Contact_Info_Field_Flag_Parameters_Mandatory - flag is unset, any vCard type parameters may be used.</tp:docstring> + Contact_Info_Field_Flag_Parameters_Exact flag is not set, any vCard type + parameters may be used.</tp:docstring> </tp:member> <tp:member type="u" name="Flags" tp:type="Contact_Info_Field_Flags"> @@ -352,7 +408,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.</ list indicates that arbitrary vCard fields are permitted. This property SHOULD be the empty list, and be ignored by clients, if <tp:member-ref>ContactInfoFlags</tp:member-ref> does not contain the - Can_Set <tp:type>Contact_Info_Flag</tp:type>.</p> + Can_Set flag.</p> <p>For example, an implementation of XEP-0054, which defines a mapping of vCards to XML for use over XMPP, would set this property to the @@ -364,11 +420,11 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.</ <pre> [ - ('tel', ['home'], Parameters_Mandatory, 1), - ('tel', ['cell'], Parameters_Mandatory, 1), - ('adr', [], Parameters_Mandatory, 1), - ('bday', [], Parameters_Mandatory, 1), - ('email', ['internet'], Parameters_Mandatory, 1), + ('tel', ['home'], Parameters_Exact, 1), + ('tel', ['cell'], Parameters_Exact, 1), + ('adr', [], Parameters_Exact, 1), + ('bday', [], Parameters_Exact, 1), + ('email', ['internet'], Parameters_Exact, 1), ]</pre> <p>A protocol which allows users to specify up to four phone numbers, @@ -381,6 +437,17 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.</ seems to correspond roughly to the largest example above) or something mapping 1-1 to vCard (such as XMPP).</p> </tp:rationale> + + <p>This property MAY change, without change notification, at any time + before the connection moves to status Connection_Status_Connected. + It MUST NOT change after that point.</p> + + <tp:rationale> + <p>Some XMPP servers, like Google Talk, only allow a small subset of + the "vcard-temp" protocol. Whether the user's server is one of + these cannot be detected until quite late in the connection + process.</p> + </tp:rationale> </tp:docstring> </property> @@ -389,7 +456,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.</ <tp:docstring xmlns="http://www.w3.org/1999/xhtml"> Flags describing the behaviour of a vCard field. </tp:docstring> - <tp:flag suffix="Parameters_Mandatory" value="1"> + <tp:flag suffix="Parameters_Exact" value="1"> <tp:docstring xmlns="http://www.w3.org/1999/xhtml"> <p>If present, exactly the parameters indicated must be set on this field; in the case of an empty list of parameters, this implies that @@ -411,6 +478,31 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.</ <tp:type>Contact_Info_Field</tp:type>s forming a structured representation of a vCard (as defined by RFC 2426), using field names and semantics defined therein.</p> + + <p>On some protocols, information about your contacts is pushed to you, + with change notification; on others, like XMPP, the client must + explicitly request the avatar, and has no way to tell whether it has + changed without retrieving it in its entirety. This distinction is + exposed by <tp:member-ref>ContactInfoFlags</tp:member-ref> containing + the Push flag.</p> + + <p>On protocols with the Push flag set, UIs can connect to + <tp:member-ref>ContactInfoChanged</tp:member-ref>, call + <tp:member-ref>GetContactInfo</tp:member-ref> once at login for the set + of contacts they are interested in, and then be sure they will receive + the latest contact info. On protocols like XMPP, clients can do the + same, but will receive (at most) opportunistic updates if the info is + retrieved for other reasons. Clients may call + <tp:member-ref>RequestContactInfo</tp:member-ref> or + <tp:member-ref>RefreshContactInfo</tp:member-ref> to force a contact's + info to be updated, but MUST NOT do so unless this is either in + response to direct user action, or to refresh their own cache after a + number of days.</p> + + <tp:rationale> + <p>We don't want clients to accidentally cause a ridiculous amount of + network traffic.</p> + </tp:rationale> </tp:docstring> </interface> </node> diff --git a/src/Makefile.am b/src/Makefile.am index 02ef616c9..d8ef643e8 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -45,6 +45,8 @@ libgabble_convenience_la_SOURCES = \ conn-aliasing.c \ conn-avatars.h \ conn-avatars.c \ + conn-contact-info.h \ + conn-contact-info.c \ conn-location.h \ conn-location.c \ conn-olpc.h \ diff --git a/src/conn-aliasing.c b/src/conn-aliasing.c index b153f8215..f0a30d2c3 100644 --- a/src/conn-aliasing.c +++ b/src/conn-aliasing.c @@ -1,7 +1,7 @@ /* * conn-aliasing.c - Gabble connection aliasing interface - * Copyright (C) 2005-2007 Collabora Ltd. - * Copyright (C) 2005-2007 Nokia Corporation + * Copyright (C) 2005-2010 Collabora Ltd. + * Copyright (C) 2005-2010 Nokia Corporation * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -525,6 +525,8 @@ setaliases_foreach (gpointer key, gpointer value, gpointer user_data) if (base->self_handle == handle) { + GList *edits = NULL; + /* User has called SetAliases on themselves - patch their vCard. * FIXME: because SetAliases is currently synchronous, we ignore errors * here, and just let the request happen in the background. @@ -547,8 +549,10 @@ setaliases_foreach (gpointer key, gpointer value, gpointer user_data) lm_message_unref (msg); } - gabble_vcard_manager_edit (data->conn->vcard_manager, 0, NULL, NULL, - G_OBJECT(data->conn), 1, "NICKNAME", alias); + edits = g_list_append (edits, gabble_vcard_manager_edit_info_new ( + NULL, alias, GABBLE_VCARD_EDIT_SET_ALIAS, NULL)); + gabble_vcard_manager_edit (data->conn->vcard_manager, 0, NULL, + NULL, G_OBJECT (data->conn), edits); } if (NULL != error) diff --git a/src/conn-avatars.c b/src/conn-avatars.c index b3dc9493b..3ba74f345 100644 --- a/src/conn-avatars.c +++ b/src/conn-avatars.c @@ -1,7 +1,7 @@ /* * conn-avatars.c - Gabble connection avatar interface - * Copyright (C) 2005-2007 Collabora Ltd. - * Copyright (C) 2005-2007 Nokia Corporation + * Copyright (C) 2005-2010 Collabora Ltd. + * Copyright (C) 2005-2010 Nokia Corporation * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -819,8 +819,9 @@ gabble_connection_set_avatar (TpSvcConnectionInterfaceAvatars *iface, { GabbleConnection *self = GABBLE_CONNECTION (iface); TpBaseConnection *base = (TpBaseConnection *) self; + GabbleVCardManagerEditInfo *edit_info; + GList *edits = NULL; struct _set_avatar_ctx *ctx; - gchar *value = NULL; gchar *base64; TP_BASE_CONNECTION_ERROR_IF_NOT_CONNECTED (base, context); @@ -828,20 +829,34 @@ gabble_connection_set_avatar (TpSvcConnectionInterfaceAvatars *iface, ctx = g_new0 (struct _set_avatar_ctx, 1); ctx->conn = self; ctx->invocation = context; - if (avatar) + + if (avatar != NULL && avatar->len > 0) { ctx->avatar = g_string_new_len (avatar->data, avatar->len); base64 = base64_encode (avatar->len, avatar->data, TRUE); - value = g_strdup_printf ("%s %s", mime_type, base64); + + DEBUG ("Replacing avatar"); + + edit_info = gabble_vcard_manager_edit_info_new ("PHOTO", + NULL, GABBLE_VCARD_EDIT_REPLACE, + "TYPE", mime_type, + "BINVAL", base64, + NULL); + g_free (base64); } + else + { + DEBUG ("Removing avatar"); + edit_info = gabble_vcard_manager_edit_info_new ("PHOTO", + NULL, GABBLE_VCARD_EDIT_DELETE, NULL); + } - DEBUG ("called"); + edits = g_list_append (edits, edit_info); gabble_vcard_manager_edit (self->vcard_manager, 0, _set_avatar_cb2, ctx, (GObject *) self, - 1, "PHOTO", value); - g_free (value); + edits); } diff --git a/src/conn-contact-info.c b/src/conn-contact-info.c new file mode 100644 index 000000000..5c3d8d076 --- /dev/null +++ b/src/conn-contact-info.c @@ -0,0 +1,1152 @@ +/* + * conn-contact-info.c - Gabble connection ContactInfo interface + * Copyright (C) 2009-2010 Collabora Ltd. + * Copyright (C) 2009-2010 Nokia Corporation + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "config.h" + +#include "conn-contact-info.h" + +#include <string.h> + +#include <telepathy-glib/svc-connection.h> +#include <telepathy-glib/interfaces.h> + +#include "extensions/extensions.h" + +#include "vcard-manager.h" + +#define DEBUG_FLAG GABBLE_DEBUG_CONNECTION + +#include "debug.h" +#include "util.h" + +/* Arbitrary lengths for supported fields' types, increase as necessary when + * adding new fields */ +#define MAX_TYPES 14 +#define MAX_ELEMENTS 8 +#define MAX_TYPE_PARAM_LEN 8 /* strlen ("internet") in "type=internet" */ + +typedef enum { + /* in Telepathy: one value per field; in XMPP: one value per field */ + FIELD_SIMPLE, + /* same as FIELD_SIMPLE but may not be repeated */ + FIELD_SIMPLE_ONCE, + /* in Telepathy: exactly n_elements values; in XMPP: a child element for + * each entry in elements, in that order */ + FIELD_STRUCTURED, + /* same as FIELD_STRUCTURED but may not be repeated */ + FIELD_STRUCTURED_ONCE, + + /* Special cases: */ + + /* in Telepathy, one multi-line value; in XMPP, a sequence of <LINE>s */ + FIELD_LABEL, + /* same as FIELD_STRUCTURED except the last element may repeat n times */ + FIELD_ORG +} FieldBehaviour; + +typedef struct { + /* Name in XEP-0054, vcard-temp (upper-case as per the DTD) */ + const gchar *xmpp_name; + /* Name in Telepathy's vCard representation (lower-case), or NULL + * to lower-case the XEP-0054 name automatically */ + const gchar *vcard_name; + /* General type of field */ + FieldBehaviour behaviour; + /* Telepathy flags for this field (none are applicable to XMPP yet) */ + GabbleContactInfoFieldFlags tp_flags; + /* Valid values for the TYPE type-parameter, in upper case */ + const gchar * const types[MAX_TYPES]; + /* Child elements for structured/repeating fields, in upper case */ + const gchar * const elements[MAX_ELEMENTS]; +} VCardField; + +static VCardField known_fields[] = { + /* Simple fields */ + { "FN", NULL, FIELD_SIMPLE_ONCE, 0, { NULL }, { NULL } }, + { "BDAY", NULL, FIELD_SIMPLE, 0, { NULL }, { NULL } }, + { "MAILER", NULL, FIELD_SIMPLE, 0, { NULL }, { NULL } }, + { "TZ", NULL, FIELD_SIMPLE, 0, { NULL }, { NULL } }, + { "TITLE", NULL, FIELD_SIMPLE, 0, { NULL }, { NULL } }, + { "ROLE", NULL, FIELD_SIMPLE, 0, { NULL }, { NULL } }, + { "NOTE", NULL, FIELD_SIMPLE, 0, { NULL }, { NULL } }, + { "PRODID", NULL, FIELD_SIMPLE, 0, { NULL }, { NULL } }, + { "REV", NULL, FIELD_SIMPLE, 0, { NULL }, { NULL } }, + { "SORT-STRING", NULL, FIELD_SIMPLE, 0, { NULL }, { NULL } }, + { "UID", NULL, FIELD_SIMPLE, 0, { NULL }, { NULL } }, + { "URL", NULL, FIELD_SIMPLE, 0, { NULL }, { NULL } }, + { "NICKNAME", NULL, FIELD_SIMPLE, 0, { NULL }, { NULL } }, + + /* Simple fields which are Jabber-specific */ + { "JABBERID", "x-jabber", FIELD_SIMPLE, 0, { NULL }, { NULL } }, + { "DESC", "x-desc", FIELD_SIMPLE, 0, { NULL }, { NULL } }, + + /* Structured fields */ + { "N", NULL, FIELD_STRUCTURED_ONCE, 0, { NULL }, + { "FAMILY", "GIVEN", "MIDDLE", "PREFIX", "SUFFIX", NULL } }, + { "ADR", NULL, FIELD_STRUCTURED, 0, + { "type=home", "type=work", "type=postal", "type=parcel", + "type=dom", "type=intl", "type=pref", NULL }, + { "POBOX", "EXTADD", "STREET", "LOCALITY", "REGION", "PCODE", "CTRY", + NULL } }, + { "GEO", NULL, FIELD_STRUCTURED_ONCE, 0, + { NULL }, + { "LAT", "LON", NULL } }, + /* TEL and EMAIL are like structured fields: they have exactly one child + * per occurrence */ + { "TEL", NULL, FIELD_STRUCTURED, 0, + { "type=home", "type=work", "type=voice", "type=fax", "type=pager", + "type=msg", "type=cell", "type=video", "type=bbs", "type=modem", + "type=isdn", "type=pcs", "type=pref", NULL }, + { "NUMBER", NULL } }, + { "EMAIL", NULL, FIELD_STRUCTURED, 0, + { "type=home", "type=work", "type=internet", "type=pref", + "type=x400", NULL }, + { "USERID", NULL } }, + + /* Special cases with their own semantics */ + { "LABEL", NULL, FIELD_LABEL, 0, + { "type=home", "type=work", "type=postal", "type=parcel", + "type=dom", "type=intl", "type=pref", NULL }, + { NULL } }, + { "ORG", NULL, FIELD_ORG, 0, { NULL }, { NULL } }, + + /* Things we don't handle: */ + + /* PHOTO: we treat it as the avatar instead */ + + /* KEY: is Base64 (perhaps? hard to tell from the XEP) */ + /* LOGO: can be base64 or a URL */ + /* SOUND: can be base64, URL, or phonetic (!) */ + /* AGENT: is an embedded vCard (!) */ + /* CATEGORIES: same vCard encoding as NICKNAME, but split into KEYWORDs + * in XMPP; nobody is likely to use it on XMPP */ + /* CLASS: if you're putting non-PUBLIC vCards on your XMPP account, + * you're probably Doing It Wrong */ + + { NULL } +}; +/* static XML element name => static VCardField */ +static GHashTable *known_fields_xmpp = NULL; +/* g_strdup'd Telepathy pseudo-vCard element name => static VCardField */ +static GHashTable *known_fields_vcard = NULL; + +/* one-per-process GABBLE_ARRAY_TYPE_FIELD_SPECS */ +static GPtrArray *supported_fields = NULL; + +/* + * _insert_contact_field: + * @contact_info: an array of Contact_Info_Field structures + * @field_name: a vCard field name in any case combination + * @field_params: a list of vCard type-parameters, typically of the form + * type=xxx; must be in lower-case if case-insensitive + * @field_values: for unstructured fields, an array containing one element; + * for structured fields, the elements of the field in order + */ +static void +_insert_contact_field (GPtrArray *contact_info, + const gchar *field_name, + const gchar * const *field_params, + const gchar * const *field_values) +{ + gchar *field_name_down = g_ascii_strdown (field_name, -1); + + g_ptr_array_add (contact_info, tp_value_array_build (3, + G_TYPE_STRING, field_name_down, + G_TYPE_STRV, field_params, + G_TYPE_STRV, field_values, + G_TYPE_INVALID)); + + g_free (field_name_down); +} + +static void +_create_contact_field_extended (GPtrArray *contact_info, + WockyXmppNode *node, + const gchar * const *supported_types, + const gchar * const *mandatory_fields) +{ + guint i; + WockyXmppNode *child_node; + GPtrArray *field_params = NULL; + gchar **field_values = NULL; + guint supported_types_size = 0; + guint mandatory_fields_size = 0; + + if (supported_types != NULL) + supported_types_size = g_strv_length ((gchar **) supported_types); + + field_params = g_ptr_array_new (); + + /* we can simply omit a type if not found */ + for (i = 0; i < supported_types_size; ++i) + { + guint j; + gchar child_name[MAX_TYPE_PARAM_LEN + 1] = { '\0' }; + + /* the +5 is to skip over "type=" - all type-parameters we support have + * type=, which is verified in conn_contact_info_build_supported_fields + */ + for (j = 0; + j < MAX_TYPE_PARAM_LEN && supported_types[i][j + 5] != '\0'; + j++) + { + child_name[j] = g_ascii_toupper (supported_types[i][j + 5]); + } + + child_node = wocky_xmpp_node_get_child (node, child_name); + + if (child_node != NULL) + g_ptr_array_add (field_params, (gchar *) supported_types[i]); + } + + g_ptr_array_add (field_params, NULL); + + if (mandatory_fields != NULL) + { + mandatory_fields_size = g_strv_length ((gchar **) mandatory_fields); + + /* the mandatory field values need to be ordered properly */ + field_values = g_new0 (gchar *, mandatory_fields_size + 1); + + for (i = 0; i < mandatory_fields_size; ++i) + { + child_node = wocky_xmpp_node_get_child (node, mandatory_fields[i]); + + if (child_node != NULL) + field_values[i] = child_node->content; + else + field_values[i] = ""; + } + } + + _insert_contact_field (contact_info, node->name, + (const gchar * const *) field_params->pdata, + (const gchar * const *) field_values); + + /* The strings in both arrays are borrowed, so we just need to free the + * arrays themselves. */ + g_ptr_array_free (field_params, TRUE); + g_free (field_values); +} + +static GPtrArray * +_parse_vcard (WockyXmppNode *vcard_node, + GError **error) +{ + GPtrArray *contact_info = dbus_g_type_specialized_construct ( + GABBLE_ARRAY_TYPE_CONTACT_INFO_FIELD_LIST); + NodeIter i; + + for (i = node_iter (vcard_node); i; i = node_iter_next (i)) + { + WockyXmppNode *node = node_iter_data (i); + const VCardField *field; + + if (node->name == NULL || !tp_strdiff (node->name, "")) + continue; + + field = g_hash_table_lookup (known_fields_xmpp, node->name); + + if (field == NULL) + { + DEBUG ("unknown vCard node in XML: %s", node->name); + continue; + } + + switch (field->behaviour) + { + case FIELD_SIMPLE: + case FIELD_SIMPLE_ONCE: + { + const gchar * const field_values[2] = { node->content, NULL }; + + _insert_contact_field (contact_info, node->name, NULL, + field_values); + } + break; + + case FIELD_STRUCTURED: + case FIELD_STRUCTURED_ONCE: + { + _create_contact_field_extended (contact_info, node, + field->types, field->elements); + } + break; + + case FIELD_ORG: + { + WockyXmppNode *orgname = wocky_xmpp_node_get_child (node, + "ORGNAME"); + NodeIter orgunit_iter; + GPtrArray *field_values; + const gchar *value; + + if (orgname == NULL) + { + DEBUG ("ignoring <ORG> with no <ORGNAME>"); + break; + } + + field_values = g_ptr_array_new (); + + value = orgname->content; + + if (value == NULL) + value = ""; + + g_ptr_array_add (field_values, (gpointer) value); + + for (orgunit_iter = node_iter (node); + orgunit_iter != NULL; + orgunit_iter = node_iter_next (orgunit_iter)) + { + WockyXmppNode *orgunit = node_iter_data (orgunit_iter); + + if (tp_strdiff (orgunit->name, "ORGUNIT")) + continue; + + value = orgunit->content; + + if (value == NULL) + value = ""; + + g_ptr_array_add (field_values, (gpointer) value); + } + + g_ptr_array_add (field_values, NULL); + + _insert_contact_field (contact_info, "org", NULL, + (const gchar * const *) field_values->pdata); + + g_ptr_array_free (field_values, TRUE); + } + break; + + case FIELD_LABEL: + { + NodeIter line_iter; + gchar *field_values[2] = { NULL, NULL }; + GString *text = g_string_new (""); + + for (line_iter = node_iter (node); + line_iter != NULL; + line_iter = node_iter_next (line_iter)) + { + const gchar *line; + WockyXmppNode *line_node = node_iter_data (line_iter); + + if (tp_strdiff (line_node->name, "LINE")) + continue; + + line = line_node->content; + + if (line != NULL) + { + g_string_append (text, line); + } + + if (line == NULL || ! g_str_has_suffix (line, "\n")) + { + g_string_append_c (text, '\n'); + } + } + + field_values[0] = g_string_free (text, FALSE); + _insert_contact_field (contact_info, "label", NULL, + (const gchar * const *) field_values); + g_free (field_values[0]); + } + break; + + default: + g_assert_not_reached (); + } + } + + return contact_info; +} + +static void +_emit_contact_info_changed (GabbleSvcConnectionInterfaceContactInfo *iface, + TpHandle contact, + WockyXmppNode *vcard_node) +{ + GPtrArray *contact_info; + + contact_info = _parse_vcard (vcard_node, NULL); + + if (contact_info == NULL) + return; + + gabble_svc_connection_interface_contact_info_emit_contact_info_changed ( + iface, contact, contact_info); + + g_boxed_free (GABBLE_ARRAY_TYPE_CONTACT_INFO_FIELD_LIST, contact_info); +} + +static void +_request_vcards_cb (GabbleVCardManager *manager, + GabbleVCardManagerRequest *request, + TpHandle handle, + WockyXmppNode *vcard_node, + GError *vcard_error, + gpointer user_data) +{ + GabbleConnection *conn = GABBLE_CONNECTION (user_data); + GabbleSvcConnectionInterfaceContactInfo *iface = + (GabbleSvcConnectionInterfaceContactInfo *) conn; + + g_assert (g_hash_table_lookup (conn->vcard_requests, + GUINT_TO_POINTER (handle))); + + g_hash_table_remove (conn->vcard_requests, + GUINT_TO_POINTER (handle)); + + if (vcard_error == NULL) + _emit_contact_info_changed (iface, handle, vcard_node); +} + +/** + * gabble_connection_get_contact_info + * + * Implements D-Bus method GetContactInfo + * on interface org.freedesktop.Telepathy.Connection.Interface.ContactInfo + * + * @context: The D-Bus invocation context to use to return values + * or throw an error. + */ +static void +gabble_connection_get_contact_info ( + GabbleSvcConnectionInterfaceContactInfo *iface, + const GArray *contacts, + DBusGMethodInvocation *context) +{ + GabbleConnection *self = GABBLE_CONNECTION (iface); + TpBaseConnection *base = (TpBaseConnection *) self; + TpHandleRepoIface *contacts_repo = + tp_base_connection_get_handles (base, TP_HANDLE_TYPE_CONTACT); + GError *error = NULL; + guint i; + GHashTable *ret; + + TP_BASE_CONNECTION_ERROR_IF_NOT_CONNECTED (TP_BASE_CONNECTION (iface), + context); + + if (!tp_handles_are_valid (contacts_repo, contacts, FALSE, &error)) + { + dbus_g_method_return_error (context, error); + g_error_free (error); + return; + } + + ret = dbus_g_type_specialized_construct (GABBLE_HASH_TYPE_CONTACT_INFO_MAP); + + for (i = 0; i < contacts->len; i++) + { + WockyXmppNode *vcard_node; + TpHandle contact = g_array_index (contacts, TpHandle, i); + + if (gabble_vcard_manager_get_cached (self->vcard_manager, + contact, &vcard_node)) + { + GPtrArray *contact_info = _parse_vcard (vcard_node, NULL); + + /* we have the cached vcard but it cannot be parsed, skipping */ + if (contact_info == NULL) + { + DEBUG ("contact %d vcard is cached but cannot be parsed, " + "skipping.", contact); + continue; + } + + g_hash_table_insert (ret, GUINT_TO_POINTER (contact), + contact_info); + } + } + + gabble_svc_connection_interface_contact_info_return_from_get_contact_info ( + context, ret); + + g_boxed_free (GABBLE_HASH_TYPE_CONTACT_INFO_MAP, ret); +} + +static void +_return_from_request_contact_info (WockyXmppNode *vcard_node, + GError *vcard_error, + DBusGMethodInvocation *context) +{ + GError *error = NULL; + GPtrArray *contact_info; + + if (NULL == vcard_node) + { + GError tp_error = { TP_ERRORS, TP_ERROR_NOT_AVAILABLE, + vcard_error->message }; + + if (vcard_error->domain == GABBLE_XMPP_ERROR) + { + switch (vcard_error->code) + { + case XMPP_ERROR_NOT_AUTHORIZED: + case XMPP_ERROR_FORBIDDEN: + tp_error.code = TP_ERROR_PERMISSION_DENIED; + break; + + case XMPP_ERROR_ITEM_NOT_FOUND: + tp_error.code = TP_ERROR_DOES_NOT_EXIST; + break; + } + /* what other mappings make sense here? */ + } + + dbus_g_method_return_error (context, &tp_error); + return; + } + + contact_info = _parse_vcard (vcard_node, &error); + + if (contact_info == NULL) + { + dbus_g_method_return_error (context, error); + g_error_free (error); + return; + } + + gabble_svc_connection_interface_contact_info_return_from_request_contact_info ( + context, contact_info); + + g_boxed_free (GABBLE_ARRAY_TYPE_CONTACT_INFO_FIELD_LIST, contact_info); +} + +static void +_request_vcard_cb (GabbleVCardManager *self, + GabbleVCardManagerRequest *request, + TpHandle handle, + WockyXmppNode *vcard_node, + GError *vcard_error, + gpointer user_data) +{ + DBusGMethodInvocation *context = user_data; + + _return_from_request_contact_info (vcard_node, vcard_error, context); +} + +/** + * gabble_connection_refresh_contact_info + * + * Implements D-Bus method RefreshContactInfo + * on interface org.freedesktop.Telepathy.Connection.Interface.ContactInfo + * + * @context: The D-Bus invocation context to use to return values + * or throw an error. + */ +static void +gabble_connection_refresh_contact_info (GabbleSvcConnectionInterfaceContactInfo *iface, + const GArray *contacts, + DBusGMethodInvocation *context) +{ + GabbleConnection *self = GABBLE_CONNECTION (iface); + TpBaseConnection *base = (TpBaseConnection *) self; + TpHandleRepoIface *contacts_repo = + tp_base_connection_get_handles (base, TP_HANDLE_TYPE_CONTACT); + GError *error = NULL; + guint i; + + TP_BASE_CONNECTION_ERROR_IF_NOT_CONNECTED (TP_BASE_CONNECTION (iface), + context); + + if (!tp_handles_are_valid (contacts_repo, contacts, FALSE, &error)) + { + dbus_g_method_return_error (context, error); + g_error_free (error); + return; + } + + for (i = 0; i < contacts->len; i++) + { + TpHandle contact = g_array_index (contacts, TpHandle, i); + + if (g_hash_table_lookup (self->vcard_requests, + GUINT_TO_POINTER (contact)) == NULL) + { + GabbleVCardManagerRequest *request; + + request = gabble_vcard_manager_request (self->vcard_manager, + contact, 0, _request_vcards_cb, self, NULL); + + g_hash_table_insert (self->vcard_requests, + GUINT_TO_POINTER (contact), request); + } + } + + gabble_svc_connection_interface_contact_info_return_from_refresh_contact_info ( + context); +} + +/** + * gabble_connection_request_contact_info + * + * Implements D-Bus method RequestContactInfo + * on interface org.freedesktop.Telepathy.Connection.Interface.ContactInfo + * + * @context: The D-Bus invocation context to use to return values + * or throw an error. + */ +static void +gabble_connection_request_contact_info (GabbleSvcConnectionInterfaceContactInfo *iface, + guint contact, + DBusGMethodInvocation *context) +{ + GabbleConnection *self = GABBLE_CONNECTION (iface); + TpBaseConnection *base = (TpBaseConnection *) self; + TpHandleRepoIface *contact_handles = tp_base_connection_get_handles (base, + TP_HANDLE_TYPE_CONTACT); + GError *err = NULL; + WockyXmppNode *vcard_node; + + TP_BASE_CONNECTION_ERROR_IF_NOT_CONNECTED (base, context); + + if (!tp_handle_is_valid (contact_handles, contact, &err)) + { + dbus_g_method_return_error (context, err); + g_error_free (err); + return; + } + + if (gabble_vcard_manager_get_cached (self->vcard_manager, + contact, &vcard_node)) + _return_from_request_contact_info (vcard_node, NULL, context); + else + gabble_vcard_manager_request (self->vcard_manager, contact, 0, + _request_vcard_cb, context, NULL); +} + +static GabbleVCardManagerEditInfo * +conn_contact_info_new_edit (const VCardField *field, + const gchar *value, + const gchar * const *field_params, + GError **error) +{ + GabbleVCardManagerEditInfo *edit_info; + GabbleVCardEditType edit_type = GABBLE_VCARD_EDIT_APPEND; + const gchar * const *p; + + if (field->behaviour == FIELD_STRUCTURED_ONCE || + field->behaviour == FIELD_SIMPLE_ONCE) + edit_type = GABBLE_VCARD_EDIT_REPLACE; + + edit_info = gabble_vcard_manager_edit_info_new (field->xmpp_name, value, + edit_type, NULL); + + if (field_params == NULL) + return edit_info; + + if (field->types[0] == NULL && field_params[0] != NULL) + { + g_set_error (error, TP_ERRORS, TP_ERROR_INVALID_ARGUMENT, + "%s vCard field expects no type-parameters", field->xmpp_name); + gabble_vcard_manager_edit_info_free (edit_info); + return NULL; + } + + for (p = field_params; *p != NULL; ++p) + { + guint i; + gboolean used = FALSE; + + for (i = 0; field->types[i] != NULL; i++) + { + if (!tp_strdiff (field->types[i], *p)) + { + /* the +5 is to skip over "type=" - all type-parameters we + * support have type=, which is verified in + * conn_contact_info_build_supported_fields */ + gchar *tmp = g_ascii_strup (field->types[i] + 5, -1); + + gabble_vcard_manager_edit_info_add_child (edit_info, + tmp, NULL); + g_free (tmp); + + used = TRUE; + break; + } + } + + if (!used) + { + g_set_error (error, TP_ERRORS, TP_ERROR_INVALID_ARGUMENT, + "%s vCard field does not support type-parameter %s", + field->xmpp_name, *p); + gabble_vcard_manager_edit_info_free (edit_info); + return NULL; + } + } + + return edit_info; +} + +static void +_set_contact_info_cb (GabbleVCardManager *vcard_manager, + GabbleVCardManagerEditRequest *request, + WockyXmppNode *vcard_node, + GError *vcard_error, + gpointer user_data) +{ + DBusGMethodInvocation *context = user_data; + + if (vcard_node == NULL) + { + GError tp_error = { TP_ERRORS, TP_ERROR_NOT_AVAILABLE, + vcard_error->message }; + + if (vcard_error->domain == GABBLE_XMPP_ERROR) + if (vcard_error->code == XMPP_ERROR_BAD_REQUEST || + vcard_error->code == XMPP_ERROR_NOT_ACCEPTABLE) + tp_error.code = TP_ERROR_INVALID_ARGUMENT; + + dbus_g_method_return_error (context, &tp_error); + } + else + { + gabble_svc_connection_interface_contact_info_return_from_set_contact_info ( + context); + } +} + +/** + * gabble_connection_set_contact_info + * + * Implements D-Bus method SetContactInfo + * on interface org.freedesktop.Telepathy.Connection.Interface.ContactInfo + * + * @context: The D-Bus invocation context to use to return values + * or throw an error. + */ +static void +gabble_connection_set_contact_info (GabbleSvcConnectionInterfaceContactInfo *iface, + const GPtrArray *contact_info, + DBusGMethodInvocation *context) +{ + GabbleConnection *self = GABBLE_CONNECTION (iface); + TpBaseConnection *base = (TpBaseConnection *) self; + GList *edits = NULL; + guint i; + GError *error = NULL; + + TP_BASE_CONNECTION_ERROR_IF_NOT_CONNECTED (base, context); + + for (i = 0; i < contact_info->len; i++) + { + GValueArray *structure = g_ptr_array_index (contact_info, i); + guint n_field_values = 0; + VCardField *field; + const gchar *field_name; + const gchar * const *field_params; + const gchar * const *field_values; + GabbleVCardManagerEditInfo *edit_info; + + field_name = g_value_get_string (structure->values + 0); + field_params = g_value_get_boxed (structure->values + 1); + field_values = g_value_get_boxed (structure->values + 2); + + if (field_values != NULL) + n_field_values = g_strv_length ((gchar **) field_values); + + field = g_hash_table_lookup (known_fields_vcard, field_name); + + if (field == NULL) + { + g_set_error (&error, TP_ERRORS, TP_ERROR_INVALID_ARGUMENT, + "unknown vCard field from D-Bus: %s", field_name); + goto finally; + } + + if (!gabble_vcard_manager_can_use_vcard_field (self->vcard_manager, + field->xmpp_name)) + { + g_set_error (&error, TP_ERRORS, TP_ERROR_INVALID_ARGUMENT, + "%s vCard field is not supported by this server", + field->xmpp_name); + goto finally; + } + + switch (field->behaviour) + { + case FIELD_SIMPLE: + case FIELD_SIMPLE_ONCE: + { + if (n_field_values != 1) + { + g_set_error (&error, TP_ERRORS, TP_ERROR_INVALID_ARGUMENT, + "%s vCard field expects one value but got %u", + field->xmpp_name, n_field_values); + goto finally; + } + + edit_info = conn_contact_info_new_edit (field, field_values[0], + field_params, &error); + + if (edit_info == NULL) + { + goto finally; + } + } + break; + + case FIELD_STRUCTURED: + case FIELD_STRUCTURED_ONCE: + { + guint n_elements = g_strv_length ((gchar **) field->elements); + guint j; + + if (n_field_values != n_elements) + { + g_set_error (&error, TP_ERRORS, TP_ERROR_INVALID_ARGUMENT, + "%s vCard field expects %u values but got %u", + field->xmpp_name, n_elements, n_field_values); + goto finally; + } + + edit_info = conn_contact_info_new_edit (field, NULL, + field_params, &error); + + if (edit_info == NULL) + { + goto finally; + } + + for (j = 0; j < n_elements; ++j) + gabble_vcard_manager_edit_info_add_child (edit_info, + field->elements[j], field_values[j]); + } + break; + + case FIELD_ORG: + { + guint j; + + if (n_field_values == 0) + { + g_set_error (&error, TP_ERRORS, TP_ERROR_INVALID_ARGUMENT, + "ORG vCard field expects at least one value but got 0"); + goto finally; + } + + edit_info = conn_contact_info_new_edit (field, NULL, + field_params, &error); + + if (edit_info == NULL) + { + goto finally; + } + + gabble_vcard_manager_edit_info_add_child (edit_info, + "ORGNAME", field_values[0]); + + for (j = 1; field_values[j] != NULL; j++) + { + gabble_vcard_manager_edit_info_add_child (edit_info, + "ORGUNIT", field_values[j]); + } + } + break; + + case FIELD_LABEL: + { + gchar **lines; + guint j; + + if (n_field_values != 1) + { + g_set_error (&error, TP_ERRORS, TP_ERROR_INVALID_ARGUMENT, + "%s vCard field expects one value but got %u", + field->xmpp_name, n_field_values); + goto finally; + } + + edit_info = conn_contact_info_new_edit (field, NULL, + field_params, &error); + + if (edit_info == NULL) + { + goto finally; + } + + lines = g_strsplit (field_values[0], "\n", 0); + + for (j = 0; lines[j] != NULL; j++) + { + /* don't emit a trailing empty line if the label ended + * with \n */ + if (lines[j][0] == '\0' && lines[j + 1] == NULL) + continue; + + gabble_vcard_manager_edit_info_add_child (edit_info, + "LINE", lines[j]); + } + + g_strfreev (lines); + } + break; + + default: + g_assert_not_reached (); + } + + g_assert (edit_info != NULL); + edits = g_list_append (edits, edit_info); + } + +finally: + if (error != NULL) + { + DEBUG ("%s", error->message); + g_list_foreach (edits, (GFunc) gabble_vcard_manager_edit_info_free, + NULL); + dbus_g_method_return_error (context, error); + g_error_free (error); + } + else + { + edits = g_list_prepend (edits, + gabble_vcard_manager_edit_info_new (NULL, NULL, + GABBLE_VCARD_EDIT_CLEAR, NULL)); + + /* fix the alias (if missing) afterwards */ + edits = g_list_append (edits, + gabble_vcard_manager_edit_info_new (NULL, NULL, + GABBLE_VCARD_EDIT_SET_ALIAS, NULL)); + + gabble_vcard_manager_edit (self->vcard_manager, 0, + _set_contact_info_cb, context, + G_OBJECT (self), edits); + } +} + +static void +_vcard_updated (GObject *object, + TpHandle contact, + gpointer user_data) +{ + GabbleConnection *conn = GABBLE_CONNECTION (user_data); + WockyXmppNode *vcard_node; + + if (gabble_vcard_manager_get_cached (conn->vcard_manager, + contact, &vcard_node)) + { + _emit_contact_info_changed ( + GABBLE_SVC_CONNECTION_INTERFACE_CONTACT_INFO (conn), + contact, vcard_node); + } +} + +/* vcard_manager may be NULL. */ +static GPtrArray * +conn_contact_info_build_supported_fields (GabbleVCardManager *vcard_manager) +{ + GPtrArray *fields = dbus_g_type_specialized_construct ( + GABBLE_ARRAY_TYPE_FIELD_SPECS); + VCardField *field; + + for (field = known_fields; field->xmpp_name != NULL; field++) + { + GValueArray *va; + gchar *vcard_name; + guint max_times; + guint i; + + /* Shorthand to avoid having to put it in the struct initialization: + * on XMPP, there is no field that supports arbitrary type-parameters. + * Setting Parameters_Mandatory eliminates the special case that an + * empty list means arbitrary parameters. */ + if (field->types[0] == NULL) + { + field->tp_flags |= + GABBLE_CONTACT_INFO_FIELD_FLAG_PARAMETERS_EXACT; + } + +#ifndef G_DISABLE_ASSERT + for (i = 0; field->types[i] != NULL; i++) + { + /* All type-parameters XMPP currently supports are of the form type=, + * which is assumed in _create_contact_field_extended and + * conn_contact_info_edit_add_type_params */ + g_assert (g_str_has_prefix (field->types[i], "type=")); + + g_assert_cmpuint ((guint) strlen (field->types[i]), <=, + MAX_TYPE_PARAM_LEN + 5); + } +#endif + + if (vcard_manager != NULL && + !gabble_vcard_manager_can_use_vcard_field (vcard_manager, + field->xmpp_name)) + { + continue; + } + + if (field->vcard_name != NULL) + vcard_name = g_strdup (field->vcard_name); + else + vcard_name = g_ascii_strdown (field->xmpp_name, -1); + + switch (field->behaviour) + { + case FIELD_SIMPLE_ONCE: + case FIELD_STRUCTURED_ONCE: + max_times = 1; + break; + + default: + max_times = G_MAXUINT32; + } + + va = tp_value_array_build (4, + G_TYPE_STRING, vcard_name, + G_TYPE_STRV, field->types, + G_TYPE_UINT, field->tp_flags, + G_TYPE_UINT, max_times, + G_TYPE_INVALID); + + g_free (vcard_name); + + g_ptr_array_add (fields, va); + } + + return fields; +} + +void +conn_contact_info_class_init (GabbleConnectionClass *klass) +{ + VCardField *field; + + /* These are never freed; they're only allocated once per run of Gabble. + * The destructor in the latter is only set for completeness */ + known_fields_xmpp = g_hash_table_new (g_str_hash, g_str_equal); + known_fields_vcard = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, NULL); + + supported_fields = conn_contact_info_build_supported_fields (NULL); + + for (field = known_fields; field->xmpp_name != NULL; field++) + { + gchar *vcard_name; + + if (field->vcard_name != NULL) + vcard_name = g_strdup (field->vcard_name); + else + vcard_name = g_ascii_strdown (field->xmpp_name, -1); + + g_hash_table_insert (known_fields_xmpp, + (gchar *) field->xmpp_name, field); + g_hash_table_insert (known_fields_vcard, vcard_name, field); + } +} + +static void +conn_contact_info_status_changed_cb (GabbleConnection *conn, + guint status, + guint reason, + gpointer user_data G_GNUC_UNUSED) +{ + if (status != TP_CONNECTION_STATUS_CONNECTED) + return; + + g_assert (conn->contact_info_fields == NULL); + + if (gabble_vcard_manager_has_limited_vcard_fields (conn->vcard_manager)) + { + conn->contact_info_fields = conn_contact_info_build_supported_fields ( + conn->vcard_manager); + } +} + +void +conn_contact_info_init (GabbleConnection *conn) +{ + conn->contact_info_fields = NULL; + + g_signal_connect (conn->vcard_manager, "vcard-update", + G_CALLBACK (_vcard_updated), conn); + + g_signal_connect (conn, "status-changed", + G_CALLBACK (conn_contact_info_status_changed_cb), NULL); +} + +void +conn_contact_info_finalize (GabbleConnection *conn) +{ + if (conn->contact_info_fields != NULL) + { + g_boxed_free (GABBLE_ARRAY_TYPE_FIELD_SPECS, conn->contact_info_fields); + conn->contact_info_fields = NULL; + } +} + +void +conn_contact_info_iface_init (gpointer g_iface, gpointer iface_data) +{ + GabbleSvcConnectionInterfaceContactInfoClass *klass = g_iface; + +#define IMPLEMENT(x) gabble_svc_connection_interface_contact_info_implement_##x (\ + klass, gabble_connection_##x) + IMPLEMENT(get_contact_info); + IMPLEMENT(refresh_contact_info); + IMPLEMENT(request_contact_info); + IMPLEMENT(set_contact_info); +#undef IMPLEMENT +} + +static TpDBusPropertiesMixinPropImpl props[] = { + { "ContactInfoFlags", GUINT_TO_POINTER (GABBLE_CONTACT_INFO_FLAG_CAN_SET), + NULL }, + { "SupportedFields", NULL, NULL }, + { NULL } +}; +TpDBusPropertiesMixinPropImpl *conn_contact_info_properties = props; + +void +conn_contact_info_properties_getter (GObject *object, + GQuark interface, + GQuark name, + GValue *value, + gpointer getter_data) +{ + GabbleConnection *conn = GABBLE_CONNECTION (object); + GQuark q_supported_fields = g_quark_from_static_string ( + "SupportedFields"); + + if (name == q_supported_fields) + { + if (conn->contact_info_fields != NULL) + { + g_value_set_boxed (value, conn->contact_info_fields); + } + else + { + g_value_set_static_boxed (value, supported_fields); + } + } + else + { + g_value_set_uint (value, GPOINTER_TO_UINT (getter_data)); + } +} diff --git a/src/conn-contact-info.h b/src/conn-contact-info.h new file mode 100644 index 000000000..a075daaa5 --- /dev/null +++ b/src/conn-contact-info.h @@ -0,0 +1,40 @@ +/* + * conn-contact-info.h - Header for Gabble connection ContactInfo interface + * Copyright (C) 2009-2010 Collabora Ltd. + * Copyright (C) 2009-2010 Nokia Corporation + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef __CONN_CONTACT_INFO_H__ +#define __CONN_CONTACT_INFO_H__ + +#include "connection.h" + +G_BEGIN_DECLS + +void conn_contact_info_class_init (GabbleConnectionClass *klass); +void conn_contact_info_init (GabbleConnection *conn); +void conn_contact_info_finalize (GabbleConnection *conn); +void conn_contact_info_iface_init (gpointer g_iface, gpointer iface_data); + +extern TpDBusPropertiesMixinPropImpl *conn_contact_info_properties; +void conn_contact_info_properties_getter (GObject *object, GQuark interface, + GQuark name, GValue *value, gpointer getter_data); + +G_END_DECLS + +#endif /* __CONN_CONTACT_INFO_H__ */ + diff --git a/src/connection.c b/src/connection.c index 06ebebc45..1ceae4f63 100644 --- a/src/connection.c +++ b/src/connection.c @@ -1,7 +1,7 @@ /* * gabble-connection.c - Source for GabbleConnection - * Copyright (C) 2005, 2006, 2008 Collabora Ltd. - * Copyright (C) 2005, 2006, 2008 Nokia Corporation + * Copyright (C) 2005-2010 Collabora Ltd. + * Copyright (C) 2005-2010 Nokia Corporation * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -52,6 +52,7 @@ #include "caps-hash.h" #include "conn-aliasing.h" #include "conn-avatars.h" +#include "conn-contact-info.h" #include "conn-location.h" #include "conn-presence.h" #include "conn-sidecars.h" @@ -94,6 +95,8 @@ G_DEFINE_TYPE_WITH_CODE(GabbleConnection, conn_aliasing_iface_init); G_IMPLEMENT_INTERFACE (TP_TYPE_SVC_CONNECTION_INTERFACE_AVATARS, conn_avatars_iface_init); + G_IMPLEMENT_INTERFACE (GABBLE_TYPE_SVC_CONNECTION_INTERFACE_CONTACT_INFO, + conn_contact_info_iface_init); G_IMPLEMENT_INTERFACE (TP_TYPE_SVC_CONNECTION_INTERFACE_CAPABILITIES, capabilities_service_iface_init); G_IMPLEMENT_INTERFACE (TP_TYPE_SVC_DBUS_PROPERTIES, @@ -332,6 +335,7 @@ gabble_connection_constructor (GType type, conn_aliasing_init (self); conn_avatars_init (self); + conn_contact_info_init (self); conn_presence_init (self); conn_olpc_activity_properties_init (self); conn_location_init (self); @@ -349,6 +353,7 @@ gabble_connection_constructor (GType type, self->bytestream_factory = gabble_bytestream_factory_new (self); self->avatar_requests = g_hash_table_new (NULL, NULL); + self->vcard_requests = g_hash_table_new (NULL, NULL); if (priv->fallback_socks5_proxies == NULL) { @@ -716,6 +721,7 @@ gabble_connection_class_init (GabbleConnectionClass *gabble_connection_class) TP_IFACE_CONNECTION_INTERFACE_SIMPLE_PRESENCE, TP_IFACE_CONNECTION_INTERFACE_PRESENCE, TP_IFACE_CONNECTION_INTERFACE_AVATARS, + GABBLE_IFACE_CONNECTION_INTERFACE_CONTACT_INFO, TP_IFACE_CONNECTION_INTERFACE_CONTACTS, TP_IFACE_CONNECTION_INTERFACE_REQUESTS, GABBLE_IFACE_OLPC_GADGET, @@ -744,22 +750,27 @@ gabble_connection_class_init (GabbleConnectionClass *gabble_connection_class) { NULL } }; static TpDBusPropertiesMixinIfaceImpl prop_interfaces[] = { - { GABBLE_IFACE_OLPC_GADGET, + /* 0 */ { GABBLE_IFACE_OLPC_GADGET, conn_olpc_gadget_properties_getter, NULL, olpc_gadget_props, }, - { TP_IFACE_CONNECTION_INTERFACE_LOCATION, + /* 1 */ { TP_IFACE_CONNECTION_INTERFACE_LOCATION, conn_location_properties_getter, conn_location_properties_setter, location_props, }, - { TP_IFACE_CONNECTION_INTERFACE_AVATARS, + /* 2 */ { TP_IFACE_CONNECTION_INTERFACE_AVATARS, conn_avatars_properties_getter, NULL, NULL, }, - { GABBLE_IFACE_CONNECTION_INTERFACE_GABBLE_DECLOAK, + /* 3 */ { GABBLE_IFACE_CONNECTION_INTERFACE_CONTACT_INFO, + conn_contact_info_properties_getter, + NULL, + NULL, + }, + /* 4 */ { GABBLE_IFACE_CONNECTION_INTERFACE_GABBLE_DECLOAK, tp_dbus_properties_mixin_getter_gobject_properties, tp_dbus_properties_mixin_setter_gobject_properties, decloak_props, @@ -773,6 +784,7 @@ gabble_connection_class_init (GabbleConnectionClass *gabble_connection_class) }; prop_interfaces[2].props = conn_avatars_properties; + prop_interfaces[3].props = conn_contact_info_properties; DEBUG("Initializing (GabbleConnectionClass *)%p", gabble_connection_class); @@ -979,6 +991,7 @@ gabble_connection_class_init (GabbleConnectionClass *gabble_connection_class) conn_presence_class_init (gabble_connection_class); + conn_contact_info_class_init (gabble_connection_class); } static void @@ -1029,6 +1042,7 @@ gabble_connection_dispose (GObject *object) conn_olpc_activity_properties_dispose (self); g_hash_table_destroy (self->avatar_requests); + g_hash_table_destroy (self->vcard_requests); conn_mail_notif_dispose (self); @@ -1141,6 +1155,7 @@ gabble_connection_finalize (GObject *object) tp_contacts_mixin_finalize (G_OBJECT(self)); conn_presence_finalize (self); + conn_contact_info_finalize (self); gabble_capabilities_finalize (self); diff --git a/src/connection.h b/src/connection.h index 9b64693bc..36874f2b9 100644 --- a/src/connection.h +++ b/src/connection.h @@ -171,6 +171,9 @@ struct _GabbleConnection { /* outstanding avatar requests */ GHashTable *avatar_requests; + /* outstanding vcard requests */ + GHashTable *vcard_requests; + /* jingle factory */ GabbleJingleFactory *jingle_factory; @@ -199,6 +202,9 @@ struct _GabbleConnection { guint unread_mails_count; guint new_mail_handler_id; + /* ContactInfo.SupportedFields, or NULL to use the generic one */ + GPtrArray *contact_info_fields; + GabbleConnectionPrivate *priv; }; diff --git a/src/util.c b/src/util.c index ab501f68e..f37b993bb 100644 --- a/src/util.c +++ b/src/util.c @@ -131,13 +131,6 @@ lm_message_node_add_own_nick (LmMessageNode *node, } void -lm_message_node_unlink (LmMessageNode *orphan, - LmMessageNode *parent) -{ - parent->children = g_slist_remove (parent->children, orphan); -} - -void lm_message_node_steal_children (LmMessageNode *snatcher, LmMessageNode *mum) { diff --git a/src/util.h b/src/util.h index cd4de7b48..71207f4aa 100644 --- a/src/util.h +++ b/src/util.h @@ -53,8 +53,6 @@ gchar *gabble_generate_id (void); void lm_message_node_add_own_nick (LmMessageNode *node, GabbleConnection *conn); -void lm_message_node_unlink (LmMessageNode *orphan, - LmMessageNode *parent); void lm_message_node_steal_children (LmMessageNode *snatcher, LmMessageNode *mum); gboolean lm_message_node_has_namespace (LmMessageNode *node, const gchar *ns, diff --git a/src/vcard-manager.c b/src/vcard-manager.c index 02877e040..e33fd5290 100644 --- a/src/vcard-manager.c +++ b/src/vcard-manager.c @@ -1,8 +1,8 @@ /* * vcard-manager.c - Source for Gabble vCard lookup helper * - * Copyright (C) 2007 Collabora Ltd. - * Copyright (C) 2006 Nokia Corporation + * Copyright (C) 2007-2010 Collabora Ltd. + * Copyright (C) 2006-2010 Nokia Corporation * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -31,6 +31,7 @@ #include "base64.h" #include "conn-aliasing.h" +#include "conn-contact-info.h" #include "connection.h" #include "debug.h" #include "namespaces.h" @@ -47,10 +48,60 @@ static guint request_wait_delay = 5 * 60; static const gchar *NO_ALIAS = "none"; +typedef struct { + gchar *key; + gchar *value; +} GabbleVCardChild; + +static GabbleVCardChild * +gabble_vcard_child_new (const gchar *key, + const gchar *value) +{ + GabbleVCardChild *child = g_slice_new (GabbleVCardChild); + + child->key = g_strdup (key); + child->value = g_strdup (value); + return child; +} + +static void +gabble_vcard_child_free (GabbleVCardChild *child) +{ + g_free (child->key); + g_free (child->value); + g_slice_free (GabbleVCardChild, child); +} + +struct _GabbleVCardManagerEditInfo { + /* name of element to edit */ + gchar *element_name; + + /* value of element to edit or NULL if no value should be used */ + gchar *element_value; + + /* list of GabbleVCardChild */ + GList *children; + + /* If REPLACE, the first element with this name (if any) will be updated; + * if APPEND, an element with this name will be added; + * if DELETE, all elements with this name will be removed; + * if CLEAR, everything except PHOTO and NICKNAME will be deleted, in + * preparation for a SetContactInfo operation + * if SET_ALIAS and element_value is NULL, set the best alias we have + * as the NICKNAME or FN (as appropriate) if that field doesn't already + * have a value + * if SET_ALIAS and element_value is non-NULL, set that + * as the NICKNAME or FN (as appropriate), overriding anything already + * there + */ + GabbleVCardEditType edit_type; +}; + /* signal enum */ enum { NICKNAME_UPDATE, + VCARD_UPDATE, GOT_SELF_INITIAL_AVATAR, LAST_SIGNAL }; @@ -89,9 +140,8 @@ struct _GabbleVCardManagerPrivate gboolean have_self_avatar; - /* Contains all the vCard fields that should be changed, using field - * names as keys. (Maps gchar* -> gchar *). */ - GHashTable *edits; + /* list of pending edits (GabbleVCardManagerEditInfo structures) */ + GList *edits; /* Contains RequestPipelineItem for our SET vCard request, or NULL if we * don't have SET request in the pipeline already. At most one SET request @@ -248,6 +298,11 @@ gabble_vcard_manager_class_init (GabbleVCardManagerClass *cls) 0, NULL, NULL, g_cclosure_marshal_VOID__UINT, G_TYPE_NONE, 1, G_TYPE_UINT); + signals[VCARD_UPDATE] = g_signal_new ("vcard-update", + G_TYPE_FROM_CLASS (cls), G_SIGNAL_RUN_LAST, + 0, NULL, NULL, g_cclosure_marshal_VOID__UINT, + G_TYPE_NONE, 1, G_TYPE_UINT); + signals[GOT_SELF_INITIAL_AVATAR] = g_signal_new ("got-self-initial-avatar", G_TYPE_FROM_CLASS (cls), G_SIGNAL_RUN_LAST, 0, NULL, NULL, g_cclosure_marshal_VOID__STRING, @@ -526,7 +581,12 @@ gabble_vcard_manager_dispose (GObject *object) DEBUG ("%p", object); if (priv->edits != NULL) - g_hash_table_destroy (priv->edits); + { + g_list_foreach (priv->edits, + (GFunc) gabble_vcard_manager_edit_info_free, NULL); + g_list_free (priv->edits); + } + priv->edits = NULL; if (priv->cache_timer) @@ -648,20 +708,15 @@ status_changed_cb (GObject *object, alias_src = _gabble_connection_get_cached_alias (conn, base->self_handle, &alias); - if (alias_src < GABBLE_CONNECTION_ALIAS_FROM_VCARD) + + if (alias_src >= GABBLE_CONNECTION_ALIAS_FROM_VCARD) { - /* this alias isn't reliable enough to want to patch it in */ - g_free (alias); - alias = NULL; + priv->edits = g_list_append (priv->edits, + gabble_vcard_manager_edit_info_new (NULL, alias, + GABBLE_VCARD_EDIT_SET_ALIAS, NULL)); } - else - { - if (priv->edits == NULL) - priv->edits = g_hash_table_new_full (g_str_hash, g_str_equal, - g_free, g_free); - g_hash_table_insert (priv->edits, g_strdup ("NICKNAME"), alias); - } + g_free (alias); /* FIXME: we happen to know that synchronous errors can't happen */ gabble_vcard_manager_request (self, base->self_handle, 0, @@ -764,8 +819,6 @@ extract_nickname (LmMessageNode *vcard_node) { LmMessageNode *node; const gchar *nick; - gchar **bits; - gchar *ret; node = lm_message_node_get_child (vcard_node, "NICKNAME"); @@ -774,18 +827,7 @@ extract_nickname (LmMessageNode *vcard_node) nick = lm_message_node_get_value (node); - /* nick is comma-separated, we want the first one. rule out corner cases of - * the entire string or the first value being empty before we g_strsplit */ - if (nick == NULL || *nick == '\0' || *nick == ',') - return NULL; - - bits = g_strsplit (nick, ",", 2); - - ret = g_strdup (bits[0]); - - g_strfreev (bits); - - return ret; + return g_strdup (nick); } static void @@ -818,6 +860,8 @@ observe_vcard (GabbleConnection *conn, } } + g_signal_emit (G_OBJECT (manager), signals[VCARD_UPDATE], 0, handle); + old_alias = gabble_vcard_manager_get_cached_alias (manager, handle); if (old_alias != NULL && !tp_strdiff (old_alias, alias)) @@ -920,7 +964,9 @@ replace_reply_cb (GabbleConnection *conn, if (priv->edits != NULL) { /* All the requests for these edits have just been cancelled. */ - g_hash_table_destroy (priv->edits); + g_list_foreach (priv->edits, + (GFunc) gabble_vcard_manager_edit_info_free, NULL); + g_list_free (priv->edits); priv->edits = NULL; } } @@ -932,50 +978,232 @@ replace_reply_cb (GabbleConnection *conn, } } -static void -patch_vcard_foreach (gpointer k, gpointer v, gpointer user_data) +/* This function must return TRUE for any significant change, but may also + * return TRUE for insignificant changes, as long as they aren't commonly done + * (NICKNAME, PHOTO and in future FN are the problematic ones). */ +static gboolean +gabble_vcard_manager_replace_is_significant (GabbleVCardManagerEditInfo *info, + LmMessageNode *old_vcard) { - gchar *key = k; - gchar *value = v; - LmMessageNode *vcard_node = user_data; + gboolean seen = FALSE; + NodeIter i; + + for (i = node_iter (old_vcard); i != NULL; i = node_iter_next (i)) + { + LmMessageNode *node = node_iter_data (i); + const gchar *value; + const gchar *new_value; + + /* skip over nodes that aren't the one we want to edit */ + if (tp_strdiff (info->element_name, node->name)) + continue; + + /* if there are >= 2 copies of this field, we're going to reduce that + * to 1 */ + if (seen) + return TRUE; + + /* consider NULL and "" to be different representations for the + * same thing */ + value = lm_message_node_get_value (node); + new_value = info->element_value; + + if (value == NULL) + value = ""; + + if (new_value == NULL) + new_value = ""; + + if (tp_strdiff (value, new_value)) + return TRUE; + + /* we assume that a change to child nodes is always significant, + * unless it's the <PHOTO/> */ + if (!tp_strdiff (node->name, "PHOTO")) + { + /* For the special case of PHOTO, we know that the child nodes + * are only meant to appear once, so we can be more aggressive + * about avoiding unnecessary edits: assume that the PHOTO on + * the server doesn't have extra children, and that one matching + * child is enough. */ + GList *child_iter; + + for (child_iter = info->children; + child_iter != NULL; + child_iter = child_iter->next) + { + GabbleVCardChild *child = child_iter->data; + LmMessageNode *child_node = lm_message_node_get_child (node, + child->key); + + if (child_node == NULL || + tp_strdiff (lm_message_node_get_value (child_node), + child->value)) + { + return TRUE; + } + } + } + else + { + if (info->children != NULL) + return TRUE; + } + } + + /* if there are no copies of this field, we're going to add one; otherwise, + * seen == TRUE implies we've seen exactly one copy, and it matched what + * we want */ + return !seen; +} + +static LmMessageNode *vcard_copy (LmMessageNode *parent, LmMessageNode *src, + const gchar *exclude, gboolean *exclude_mattered); + +static LmMessage * +gabble_vcard_manager_edit_info_apply (GabbleVCardManagerEditInfo *info, + LmMessageNode *old_vcard, + GabbleVCardManager *vcard_manager) +{ + LmMessage *msg; + LmMessageNode *vcard_node; LmMessageNode *node; + GList *iter; + gboolean maybe_changed = FALSE; + GabbleConnection *conn = vcard_manager->priv->connection; + TpBaseConnection *base = (TpBaseConnection *) conn; - /* For PHOTO the value is special-cased to be "image/jpeg base64base64" */ - if (!tp_strdiff (key, "PHOTO")) + if (info->edit_type == GABBLE_VCARD_EDIT_SET_ALIAS) { - node = lm_message_node_get_child (vcard_node, "PHOTO"); - if (node != NULL) + /* SET_ALIAS is shorthand for a REPLACE operation or nothing */ + + g_assert (info->element_name == NULL); + + if (gabble_vcard_manager_can_use_vcard_field (vcard_manager, "NICKNAME")) + { + info->element_name = g_strdup ("NICKNAME"); + } + else { - lm_message_node_unlink (node, vcard_node); - lm_message_node_unref (node); + /* Google Talk servers won't let us set a NICKNAME; recover by + * setting the FN */ + info->element_name = g_strdup ("FN"); } - node = lm_message_node_add_child (vcard_node, "PHOTO", ""); - if (value != NULL) + if (info->element_value == NULL) { - gchar **tokens = g_strsplit (value, " ", 2); + /* We're just trying to fix a possibly-incomplete SetContactInfo() - + * */ + gchar *alias; + + node = lm_message_node_get_child (old_vcard, info->element_name); - DEBUG ("Setting PHOTO of type %s, BINVAL length %ld starting %.30s", - tokens[0], (long) strlen (tokens[1]), tokens[1]); - lm_message_node_add_child (node, "TYPE", tokens[0]); - lm_message_node_add_child (node, "BINVAL", tokens[1]); + /* If the user has set this field explicitly via SetContactInfo(), + * that takes precedence */ + if (node != NULL) + return NULL; - g_strfreev (tokens); + if (_gabble_connection_get_cached_alias (conn, base->self_handle, + &alias) < GABBLE_CONNECTION_ALIAS_FROM_VCARD) + { + /* not good enough to want to put it in the vCard */ + g_free (alias); + return NULL; + } + + info->element_value = alias; } + + info->edit_type = GABBLE_VCARD_EDIT_REPLACE; } - else + + if (info->edit_type == GABBLE_VCARD_EDIT_APPEND || + info->edit_type == GABBLE_VCARD_EDIT_REPLACE) { - node = lm_message_node_get_child (vcard_node, key); + if (!gabble_vcard_manager_can_use_vcard_field (vcard_manager, + info->element_name)) + { + DEBUG ("ignoring vcard node %s because this server doesn't " + "support it", info->element_name); + return NULL; + } + } - if (node) + /* A special case for replacing one field with another: we detect no-op + * changes more actively, because we make changes of this type quite + * frequently (on every login), and as well as wasting bandwidth, setting + * the vCard too often can cause a memory leak in OpenFire (see fd.o#25341). + */ + if (info->edit_type == GABBLE_VCARD_EDIT_REPLACE && + ! gabble_vcard_manager_replace_is_significant (info, old_vcard)) + { + DEBUG ("ignoring no-op vCard %s replacement", info->element_name); + return NULL; + } + + msg = lm_message_new_with_sub_type (NULL, LM_MESSAGE_TYPE_IQ, + LM_MESSAGE_SUB_TYPE_SET); + + if (info->edit_type == GABBLE_VCARD_EDIT_CLEAR) + { + /* start from a clean slate... */ + vcard_node = lm_message_node_add_child (msg->node, "vCard", ""); + lm_message_node_set_attribute (vcard_node, "xmlns", "vcard-temp"); + + /* ... but as a special case, the photo gets copied in from the old + * vCard, because SetContactInfo doesn't touch photos */ + node = lm_message_node_get_child (old_vcard, "PHOTO"); + + if (node != NULL) + vcard_copy (vcard_node, node, NULL, NULL); + + /* Yes, we can do this: "LmMessageNode" is really a WockyXmppNode */ + if (wocky_xmpp_node_equal (old_vcard, vcard_node)) { - lm_message_node_set_value (node, value); + /* nothing actually happened, forget it */ + lm_message_unref (msg); + return NULL; } - else + + return msg; + } + + if (info->edit_type == GABBLE_VCARD_EDIT_APPEND) + { + /* appending: keep all child nodes */ + vcard_node = vcard_copy (msg->node, old_vcard, NULL, NULL); + } + else + { + /* replacing or deleting: exclude all matching child nodes from + * copying */ + vcard_node = vcard_copy (msg->node, old_vcard, info->element_name, + &maybe_changed); + } + + if (info->edit_type != GABBLE_VCARD_EDIT_DELETE) + { + maybe_changed = TRUE; + + node = lm_message_node_add_child (vcard_node, + info->element_name, info->element_value); + + for (iter = info->children; iter != NULL; iter = iter->next) { - lm_message_node_add_child (vcard_node, key, value); + GabbleVCardChild *child = iter->data; + + lm_message_node_add_child (node, child->key, child->value); } } + + if ((!maybe_changed) || wocky_xmpp_node_equal (old_vcard, vcard_node)) + { + /* nothing actually happened, forget it */ + lm_message_unref (msg); + return NULL; + } + + return msg; } /* Loudmouth hates me. The feelings are mutual. @@ -983,7 +1211,10 @@ patch_vcard_foreach (gpointer k, gpointer v, gpointer user_data) * Note that this function doesn't copy any attributes other than * xmlns, because LM provides no way to iterate over attributes. Thanks, LM. */ static LmMessageNode * -vcard_copy (LmMessageNode *parent, LmMessageNode *src) +vcard_copy (LmMessageNode *parent, + LmMessageNode *src, + const gchar *exclude, + gboolean *exclude_mattered) { LmMessageNode *new = lm_message_node_add_child (parent, src->name, lm_message_node_get_value (src)); @@ -995,50 +1226,30 @@ vcard_copy (LmMessageNode *parent, LmMessageNode *src) lm_message_node_set_attribute (new, "xmlns", xmlns); for (i = node_iter (src); i; i = node_iter_next (i)) - vcard_copy (new, node_iter_data (i)); + { + LmMessageNode *child = node_iter_data (i); + + if (tp_strdiff (child->name, exclude)) + { + vcard_copy (new, child, NULL, NULL); + } + else + { + if (exclude_mattered != NULL) + *exclude_mattered = TRUE; + } + } return new; } -static gboolean -vcard_node_changed (GabbleConnection *conn, - const gchar *key, - const gchar *value, - LmMessageNode *vcard_node) -{ - LmMessageNode *node; - - if (conn->features & GABBLE_CONNECTION_FEATURES_GOOGLE_ROSTER && - strcmp (key, "N") != 0 && strcmp (key, "FN") != 0 && - strcmp (key, "PHOTO") != 0) - { - return FALSE; - } - - node = lm_message_node_get_child (vcard_node, key); - if (node != NULL) - { - const gchar *node_value = lm_message_node_get_value (node); - - if (!tp_strdiff (node_value, value)) - return FALSE; - } - - DEBUG ("vcard node %s changed, vcard needs update", key); - return TRUE; -} - static void manager_patch_vcard (GabbleVCardManager *self, LmMessageNode *vcard_node) { GabbleVCardManagerPrivate *priv = self->priv; - LmMessage *msg; - LmMessageNode *patched_vcard; + LmMessage *msg = NULL; GList *li; - GHashTableIter iter; - gpointer key, value; - gboolean vcard_changed = FALSE; /* Bail out if we don't have outstanding edits to make, or if we already * have a set request in progress. @@ -1046,36 +1257,38 @@ manager_patch_vcard (GabbleVCardManager *self, if (priv->edits == NULL || priv->edit_pipeline_item != NULL) return; - g_hash_table_iter_init (&iter, priv->edits); - while (g_hash_table_iter_next (&iter, &key, &value)) + /* Apply any unsent edits to the patched vCard */ + for (li = priv->edits; li != NULL; li = li->next) { - if (vcard_node_changed (priv->connection, key, value, vcard_node)) - { - vcard_changed = TRUE; - break; - } + LmMessage *new_msg = gabble_vcard_manager_edit_info_apply ( + li->data, vcard_node, self); + + /* edit_info_apply returns NULL if nothing happened */ + if (new_msg == NULL) + continue; + + if (msg != NULL) + lm_message_unref (msg); + + msg = new_msg; + /* gabble_vcard_manager_edit_info_apply always returns an IQ message + * with one vCard child */ + vcard_node = lm_message_node_get_child (msg->node, "vCard"); + g_assert (vcard_node != NULL); } - if (!vcard_changed) + if (msg == NULL) { - DEBUG ("nothing changed, not updating vcard"); + DEBUG ("nothing really changed, not updating vCard"); goto out; } DEBUG("patching vcard"); - msg = lm_message_new_with_sub_type (NULL, LM_MESSAGE_TYPE_IQ, - LM_MESSAGE_SUB_TYPE_SET); - - patched_vcard = vcard_copy (msg->node, vcard_node); - - /* Apply any unsent edits to the patched vCard */ - g_hash_table_foreach (priv->edits, patch_vcard_foreach, patched_vcard); - /* We'll save the patched vcard, and if the server says * we're ok, put it into the cache. But we want to leave the * original vcard in the cache until that happens. */ - priv->patched_vcard = lm_message_node_ref (patched_vcard); + priv->patched_vcard = lm_message_node_ref (vcard_node); priv->edit_pipeline_item = gabble_request_pipeline_enqueue ( priv->connection->req_pipeline, msg, default_request_timeout, @@ -1085,7 +1298,9 @@ manager_patch_vcard (GabbleVCardManager *self, out: /* We've applied those, forget about them */ - g_hash_table_destroy (priv->edits); + g_list_foreach (priv->edits, (GFunc) gabble_vcard_manager_edit_info_free, + NULL); + g_list_free (priv->edits); priv->edits = NULL; /* Current edit requests are in the pipeline, remember it so we @@ -1183,7 +1398,9 @@ pipeline_reply_cb (GabbleConnection *conn, if (entry->handle == base->self_handle && priv->edits != NULL) { /* We won't have a chance to apply those, might as well forget them */ - g_hash_table_destroy (priv->edits); + g_list_foreach (priv->edits, + (GFunc) gabble_vcard_manager_edit_info_free, NULL); + g_list_free (priv->edits); priv->edits = NULL; replace_reply_cb (conn, reply_msg, self, error); @@ -1351,16 +1568,46 @@ gabble_vcard_manager_request (GabbleVCardManager *self, } GabbleVCardManagerEditRequest * +gabble_vcard_manager_edit_one (GabbleVCardManager *self, + guint timeout, + GabbleVCardManagerEditCb callback, + gpointer user_data, + GObject *object, + const gchar *element_name, + const gchar *element_value) +{ + GList *edits = NULL; + GabbleVCardManagerEditInfo *info; + + info = gabble_vcard_manager_edit_info_new ( + element_name, element_value, GABBLE_VCARD_EDIT_REPLACE, NULL); + + if (info->element_value) + DEBUG ("%s => value of length %ld starting %.30s", info->element_name, + (long) strlen (info->element_value), info->element_value); + else + DEBUG ("%s => null value", info->element_name); + + edits = g_list_append (edits, info); + + return gabble_vcard_manager_edit (self, timeout, callback, + user_data, object, edits); +} + +/* Add a pending request to edit the vCard. When it finishes, call the given + * callback. The callback may be NULL. + * + * The method takes over the ownership of the callers reference to \a edits and + * its contents. + */ +GabbleVCardManagerEditRequest * gabble_vcard_manager_edit (GabbleVCardManager *self, guint timeout, GabbleVCardManagerEditCb callback, gpointer user_data, GObject *object, - size_t n_pairs, - ...) + GList *edits) { - va_list ap; - size_t i; GabbleVCardManagerPrivate *priv = self->priv; TpBaseConnection *base = (TpBaseConnection *) priv->connection; GabbleVCardManagerEditRequest *req; @@ -1380,27 +1627,7 @@ gabble_vcard_manager_edit (GabbleVCardManager *self, NULL, NULL); } - if (priv->edits == NULL) - priv->edits = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); - - va_start (ap, n_pairs); - for (i = 0; i < n_pairs; i++) - { - gchar *key = g_strdup (va_arg (ap, const gchar *)); - gchar *value = g_strdup (va_arg (ap, const gchar *)); - - if (value) - { - DEBUG ("%s => value of length %ld starting %.30s", key, - (long) strlen (value), value); - } - else - { - DEBUG ("%s => null value", key); - } - g_hash_table_insert (priv->edits, key, value); - } - va_end (ap); + priv->edits = g_list_concat (priv->edits, edits); req = g_slice_new (GabbleVCardManagerEditRequest); req->manager = self; @@ -1571,3 +1798,94 @@ gabble_vcard_manager_set_default_request_timeout (guint timeout) { default_request_timeout = timeout; } + +GabbleVCardManagerEditInfo * +gabble_vcard_manager_edit_info_new (const gchar *element_name, + const gchar *element_value, + GabbleVCardEditType edit_type, + ...) +{ + GabbleVCardManagerEditInfo *info; + va_list ap; + const gchar *key; + const gchar *value; + + if (edit_type == GABBLE_VCARD_EDIT_DELETE) + { + const gchar *first_edit = NULL; + + g_return_val_if_fail (element_value == NULL, NULL); + + va_start (ap, edit_type); + first_edit = va_arg (ap, const gchar *); + va_end (ap); + g_return_val_if_fail (first_edit == NULL, NULL); + } + + info = g_slice_new (GabbleVCardManagerEditInfo); + info->element_name = g_strdup (element_name); + info->element_value = g_strdup (element_value); + info->edit_type = edit_type; + info->children = NULL; + + va_start (ap, edit_type); + + while ((key = va_arg (ap, const gchar *))) + { + value = va_arg (ap, const gchar *); + gabble_vcard_manager_edit_info_add_child (info, key, value); + } + + va_end (ap); + + return info; +} + +void +gabble_vcard_manager_edit_info_add_child ( + GabbleVCardManagerEditInfo *edit_info, + const gchar *key, + const gchar *value) +{ + edit_info->children = g_list_append (edit_info->children, + gabble_vcard_child_new (key, value)); +} + +void +gabble_vcard_manager_edit_info_free (GabbleVCardManagerEditInfo *info) +{ + g_free (info->element_name); + g_free (info->element_value); + g_list_foreach (info->children, (GFunc) gabble_vcard_child_free, NULL); + g_list_free (info->children); + g_slice_free (GabbleVCardManagerEditInfo, info); +} + +gboolean +gabble_vcard_manager_has_limited_vcard_fields (GabbleVCardManager *self) +{ + if (self->priv->connection->features & + GABBLE_CONNECTION_FEATURES_GOOGLE_ROSTER) + return TRUE; + + return FALSE; +} + +gboolean +gabble_vcard_manager_can_use_vcard_field (GabbleVCardManager *self, + const gchar *field_name) +{ + if (self->priv->connection->features & + GABBLE_CONNECTION_FEATURES_GOOGLE_ROSTER) + { + /* Google's server only allows N, FN and PHOTO */ + if (tp_strdiff (field_name, "N") && + tp_strdiff (field_name, "FN") && + tp_strdiff (field_name, "PHOTO")) + { + return FALSE; + } + } + + return TRUE; +} diff --git a/src/vcard-manager.h b/src/vcard-manager.h index 65a42cbda..b11b6b781 100644 --- a/src/vcard-manager.h +++ b/src/vcard-manager.h @@ -1,8 +1,8 @@ /* * vcard-manager.h - vCard lookup helper for Gabble connections * - * Copyright (C) 2006 Collabora Ltd. - * Copyright (C) 2006 Nokia Corporation + * Copyright (C) 2006-2010 Collabora Ltd. + * Copyright (C) 2006-2010 Nokia Corporation * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -33,6 +33,7 @@ typedef struct _GabbleVCardManagerPrivate GabbleVCardManagerPrivate; typedef struct _GabbleVCardManagerClass GabbleVCardManagerClass; typedef struct _GabbleVCardManagerRequest GabbleVCardManagerRequest; typedef struct _GabbleVCardManagerEditRequest GabbleVCardManagerEditRequest; +typedef struct _GabbleVCardManagerEditInfo GabbleVCardManagerEditInfo; /** * GabbleVCardManagerError: @@ -78,6 +79,14 @@ struct _GabbleVCardManager { GabbleVCardManagerPrivate *priv; }; +typedef enum { + GABBLE_VCARD_EDIT_REPLACE, + GABBLE_VCARD_EDIT_APPEND, + GABBLE_VCARD_EDIT_DELETE, + GABBLE_VCARD_EDIT_CLEAR, + GABBLE_VCARD_EDIT_SET_ALIAS +} GabbleVCardEditType; + typedef void (*GabbleVCardManagerCb)(GabbleVCardManager *self, GabbleVCardManagerRequest *request, TpHandle handle, @@ -115,19 +124,45 @@ typedef void (*GabbleVCardManagerEditCb)(GabbleVCardManager *self, GError *error, gpointer user_data); +GabbleVCardManagerEditRequest *gabble_vcard_manager_edit_one (GabbleVCardManager *, + guint timeout, + GabbleVCardManagerEditCb, + gpointer user_data, + GObject *object, + const gchar *element_name, + const gchar *element_value); GabbleVCardManagerEditRequest *gabble_vcard_manager_edit (GabbleVCardManager *, guint timeout, GabbleVCardManagerEditCb, gpointer user_data, GObject *object, - size_t n_pairs, - ...); - + GList *edits); void gabble_vcard_manager_remove_edit_request (GabbleVCardManagerEditRequest *); gchar *vcard_get_avatar_sha1 (LmMessageNode *vcard); +GabbleVCardManagerEditInfo *gabble_vcard_manager_edit_info_new ( + const gchar *element_name, + const gchar *element_value, + GabbleVCardEditType edit_type, + ...) G_GNUC_NULL_TERMINATED; + +void gabble_vcard_manager_edit_info_add_child ( + GabbleVCardManagerEditInfo *edit_info, const gchar *key, + const gchar *value); + +void gabble_vcard_manager_edit_info_free (GabbleVCardManagerEditInfo *info); + +gboolean gabble_vcard_manager_has_limited_vcard_fields ( + GabbleVCardManager *self); +gboolean gabble_vcard_manager_can_use_vcard_field (GabbleVCardManager *self, + const gchar *field_name); + +GabbleVCardManagerEditRequest *gabble_vcard_manager_edit_alias ( + GabbleVCardManager *self, guint timeout, GabbleVCardManagerEditCb callback, + gpointer user_data, GObject *object, const gchar *new_alias); + /* For unit tests only */ void gabble_vcard_manager_set_suspend_reply_timeout (guint timeout); void gabble_vcard_manager_set_default_request_timeout (guint timeout); diff --git a/tests/twisted/Makefile.am b/tests/twisted/Makefile.am index 6f8447825..86d2ed910 100644 --- a/tests/twisted/Makefile.am +++ b/tests/twisted/Makefile.am @@ -84,10 +84,14 @@ TWISTED_TESTS = \ tubes/request-invalid-dbus-tube.py \ tubes/test-get-available-tubes.py \ tubes/test-socks5-muc.py \ + vcard/get-contact-info.py \ vcard/item-not-found.py \ vcard/overlapping-sets.py \ vcard/redundant-set.py \ + vcard/refresh-contact-info.py \ + vcard/set-contact-info.py \ vcard/set-set-disconnect.py \ + vcard/supported-fields.py \ vcard/test-alias-empty-vcard.py \ vcard/test-alias-pep.py \ vcard/test-alias.py \ diff --git a/tests/twisted/constants.py b/tests/twisted/constants.py index 263e779f2..00540b51f 100644 --- a/tests/twisted/constants.py +++ b/tests/twisted/constants.py @@ -97,6 +97,7 @@ CONN_IFACE_AVATARS = CONN + '.Interface.Avatars' CONN_IFACE_CAPS = CONN + '.Interface.Capabilities' CONN_IFACE_CONTACTS = CONN + '.Interface.Contacts' CONN_IFACE_CONTACT_CAPS = CONN + '.Interface.ContactCapabilities' +CONN_IFACE_CONTACT_INFO = CONN + ".Interface.ContactInfo.DRAFT" CONN_IFACE_SIMPLE_PRESENCE = CONN + '.Interface.SimplePresence' CONN_IFACE_REQUESTS = CONN + '.Interface.Requests' CONN_IFACE_LOCATION = CONN + '.Interface.Location' @@ -305,3 +306,7 @@ PRESENCE_HIDDEN = 5 PRESENCE_BUSY = 6 PRESENCE_UNKNOWN = 7 PRESENCE_ERROR = 8 + +CONTACT_INFO_FLAG_CAN_SET = 1 +CONTACT_INFO_FLAG_PUSH = 2 +CONTACT_INFO_FIELD_FLAG_PARAMETERS_MANDATORY = 1 diff --git a/tests/twisted/servicetest.py b/tests/twisted/servicetest.py index 34b8ce668..476c2e171 100644 --- a/tests/twisted/servicetest.py +++ b/tests/twisted/servicetest.py @@ -337,6 +337,7 @@ def wrap_connection(conn): 'Presence', 'SimplePresence', 'Requests']] + [('Peer', 'org.freedesktop.DBus.Peer'), ('ContactCapabilities', cs.CONN_IFACE_CONTACT_CAPS), + ('ContactInfo', cs.CONN_IFACE_CONTACT_INFO), ('Location', cs.CONN_IFACE_LOCATION), ('Future', tp_name_prefix + '.Connection.FUTURE'), ('MailNotification', cs.CONN_IFACE_MAIL_NOTIFICATION), diff --git a/tests/twisted/vcard/get-contact-info.py b/tests/twisted/vcard/get-contact-info.py new file mode 100644 index 000000000..eddf6fc15 --- /dev/null +++ b/tests/twisted/vcard/get-contact-info.py @@ -0,0 +1,65 @@ + +""" +Test ContactInfo support. +""" + +from servicetest import call_async, EventPattern, assertEquals +from gabbletest import exec_test, acknowledge_iq, make_result_iq +import constants as cs +import dbus + + +def test(q, bus, conn, stream): + conn.Connect() + _, event = q.expect_many( + EventPattern('dbus-signal', signal='StatusChanged', + args=[cs.CONN_STATUS_CONNECTED, cs.CSR_REQUESTED]), + EventPattern('stream-iq', to=None, query_ns='vcard-temp', + query_name='vCard')) + + acknowledge_iq(stream, event.stanza) + + handle = conn.RequestHandles(1, ['bob@foo.com'])[0] + call_async(q, conn.ContactInfo, 'RefreshContactInfo', [handle]) + + event = q.expect('stream-iq', to='bob@foo.com', query_ns='vcard-temp', + query_name='vCard') + result = make_result_iq(stream, event.stanza) + result.firstChildElement().addElement('FN', content='Bob') + n = result.firstChildElement().addElement('N') + n.addElement('GIVEN', content='Bob') + result.firstChildElement().addElement('NICKNAME', + content=r'bob,bob1\,,bob2,bob3\,bob4') + label = result.firstChildElement().addElement('LABEL') + label.addElement('LINE', content='42 West Wallaby Street') + label.addElement('LINE', content="Bishop's Stortford\n") + label.addElement('LINE', content='Huntingdon') + org = result.firstChildElement().addElement('ORG') + # ORG is a sequence of decreasingly large org.units, starting + # with the organisation name itself (but here we've moved the org name + # to the end, to make sure that works.) + org.addElement('ORGUNIT', content='Dept. of Examples') + org.addElement('ORGUNIT', content='Exemplary Team') + org.addElement('ORGNAME', content='Collabora Ltd.') + stream.send(result) + + q.expect('dbus-signal', signal='ContactInfoChanged') + + # The request should be satisfied from the cache. + assertEquals( + {handle: [(u'fn', [], [u'Bob']), + (u'n', [], [u'', u'Bob', u'', u'', u'']), + (u'nickname', [], [r'bob,bob1\,,bob2,bob3\,bob4']), + # LABEL comes out as a single blob of text + (u'label', [], ['42 West Wallaby Street\n' + "Bishop's Stortford\n" + 'Huntingdon\n']), + # ORG is a sequence of decreasingly large org.units, starting + # with the organisation + (u'org', [], [u'Collabora Ltd.', u'Dept. of Examples', + u'Exemplary Team']), + ]}, conn.ContactInfo.GetContactInfo([handle])) + + +if __name__ == '__main__': + exec_test(test) diff --git a/tests/twisted/vcard/overlapping-sets.py b/tests/twisted/vcard/overlapping-sets.py index 79a8e793b..7e6639fab 100644 --- a/tests/twisted/vcard/overlapping-sets.py +++ b/tests/twisted/vcard/overlapping-sets.py @@ -4,7 +4,8 @@ import base64 from twisted.words.xish import xpath import constants as cs -from servicetest import EventPattern, call_async, sync_dbus +from servicetest import (EventPattern, call_async, sync_dbus, assertEquals, + assertLength) from gabbletest import ( acknowledge_iq, exec_test, expect_and_handle_get_vcard, make_result_iq, sync_stream) @@ -19,27 +20,112 @@ def test(q, bus, conn, stream): sync_stream(q, stream) handle = conn.GetSelfHandle() - call_async(q, conn.Aliasing, 'SetAliases', {handle: 'Some Guy'}) + call_async(q, conn.Aliasing, 'SetAliases', {handle: 'Robert the Bruce'}) sync_dbus(bus, q, conn) acknowledge_iq(stream, vcard_get_event.stanza) # Gabble sets a new vCard with our nickname. vcard_set_event = q.expect('stream-iq', iq_type='set', query_ns=ns.VCARD_TEMP, query_name='vCard') + assertEquals('Robert the Bruce', xpath.queryForString('/iq/vCard/NICKNAME', + vcard_set_event.stanza)) + assertEquals(None, xpath.queryForNodes('/iq/vCard/PHOTO', + vcard_set_event.stanza)) + assertEquals(None, xpath.queryForNodes('/iq/vCard/FN', + vcard_set_event.stanza)) + assertEquals(None, xpath.queryForNodes('/iq/vCard/N', + vcard_set_event.stanza)) - # Before the server replies, the user sets their avatar. + # Before the server replies, the user sets their avatar call_async(q, conn.Avatars, 'SetAvatar', 'hello', 'image/png') sync_dbus(bus, q, conn) + # This acknowledgement is for the nickname acknowledge_iq(stream, vcard_set_event.stanza) + hello_binval = base64.b64encode('hello') + + # This sets the avatar vcard_set_event = q.expect('stream-iq', iq_type='set', query_ns=ns.VCARD_TEMP, query_name='vCard') + assertEquals('Robert the Bruce', xpath.queryForString('/iq/vCard/NICKNAME', + vcard_set_event.stanza)) + assertLength(1, xpath.queryForNodes('/iq/vCard/PHOTO', + vcard_set_event.stanza)) + assertEquals('image/png', xpath.queryForString('/iq/vCard/PHOTO/TYPE', + vcard_set_event.stanza)) + assertEquals(hello_binval, xpath.queryForString('/iq/vCard/PHOTO/BINVAL', + vcard_set_event.stanza)) + assertEquals(None, xpath.queryForNodes('/iq/vCard/FN', + vcard_set_event.stanza)) + assertEquals(None, xpath.queryForNodes('/iq/vCard/N', + vcard_set_event.stanza)) + + # Before the server replies, the user sets their ContactInfo + call_async(q, conn.ContactInfo, 'SetContactInfo', + [(u'fn', [], [u'King Robert I']), + (u'n', [], [u'de Brus', u'Robert', u'', u'King', u'']), + (u'nickname', [], [u'Bob'])]) + sync_dbus(bus, q, conn) + # This acknowledgement is for the avatar; SetAvatar won't happen + # until this has acknowledge_iq(stream, vcard_set_event.stanza) q.expect('dbus-return', method='SetAvatar') - # And then crashes. - sync_stream(q, stream) + # This sets the ContactInfo + vcard_set_event = q.expect('stream-iq', iq_type='set', + query_ns=ns.VCARD_TEMP, query_name='vCard') + assertEquals('Bob', xpath.queryForString('/iq/vCard/NICKNAME', + vcard_set_event.stanza)) + assertLength(1, xpath.queryForNodes('/iq/vCard/PHOTO', + vcard_set_event.stanza)) + assertEquals('image/png', xpath.queryForString('/iq/vCard/PHOTO/TYPE', + vcard_set_event.stanza)) + assertEquals(hello_binval, xpath.queryForString('/iq/vCard/PHOTO/BINVAL', + vcard_set_event.stanza)) + assertLength(1, xpath.queryForNodes('/iq/vCard/N', + vcard_set_event.stanza)) + assertEquals('Robert', xpath.queryForString('/iq/vCard/N/GIVEN', + vcard_set_event.stanza)) + assertEquals('de Brus', xpath.queryForString('/iq/vCard/N/FAMILY', + vcard_set_event.stanza)) + assertEquals('King', xpath.queryForString('/iq/vCard/N/PREFIX', + vcard_set_event.stanza)) + assertEquals('King Robert I', xpath.queryForString('/iq/vCard/FN', + vcard_set_event.stanza)) + + # Before the server replies, the user unsets their avatar + call_async(q, conn.Avatars, 'SetAvatar', '', '') + sync_dbus(bus, q, conn) + # This acknowledgement is for the ContactInfo; SetContactInfo won't happen + # until this has + acknowledge_iq(stream, vcard_set_event.stanza) + q.expect('dbus-return', method='SetContactInfo') + + vcard_set_event = q.expect('stream-iq', iq_type='set', + query_ns=ns.VCARD_TEMP, query_name='vCard') + assertEquals('Bob', xpath.queryForString('/iq/vCard/NICKNAME', + vcard_set_event.stanza)) + assertEquals(None, xpath.queryForNodes('/iq/vCard/PHOTO', + vcard_set_event.stanza)) + assertLength(1, xpath.queryForNodes('/iq/vCard/N', + vcard_set_event.stanza)) + assertEquals('Robert', xpath.queryForString('/iq/vCard/N/GIVEN', + vcard_set_event.stanza)) + assertEquals('de Brus', xpath.queryForString('/iq/vCard/N/FAMILY', + vcard_set_event.stanza)) + assertEquals('King', xpath.queryForString('/iq/vCard/N/PREFIX', + vcard_set_event.stanza)) + assertEquals('King Robert I', xpath.queryForString('/iq/vCard/FN', + vcard_set_event.stanza)) + + # This acknowledgement is for the avatar; SetAvatar won't finish + # until this is received + acknowledge_iq(stream, vcard_set_event.stanza) + q.expect('dbus-return', method='SetAvatar') + + # Now Gabble gets disconnected. + sync_stream(q, stream) conn.Disconnect() q.expect('dbus-signal', signal='StatusChanged', args=[2, 1]) diff --git a/tests/twisted/vcard/redundant-set.py b/tests/twisted/vcard/redundant-set.py index 05a73f111..d3694348e 100644 --- a/tests/twisted/vcard/redundant-set.py +++ b/tests/twisted/vcard/redundant-set.py @@ -24,11 +24,14 @@ def test(q, bus, conn, stream, is_google): result = make_result_iq(stream, iq_event.stanza) # Testing reveals that Google's vCard server does not actually support - # NICKNAME (or indeed any fields beside FN and PHOTO): if you set a vCard - # including it, it accepts the request but strips out the unsupported - # fields. So if the server looks like Google, we don't bother re-setting - # the NICKNAME. - if not is_google: + # NICKNAME (or indeed any fields beside FN, N and PHOTO): if you set a + # vCard including it, it accepts the request but strips out the unsupported + # fields. So if the server looks like Google, it's a redundant set + # operation on FN that we want to avoid. + if is_google: + vcard = result.firstChildElement() + vcard.addElement('FN', content='oh hello there') + else: vcard = result.firstChildElement() vcard.addElement('NICKNAME', content='oh hello there') diff --git a/tests/twisted/vcard/refresh-contact-info.py b/tests/twisted/vcard/refresh-contact-info.py new file mode 100644 index 000000000..7a64006e3 --- /dev/null +++ b/tests/twisted/vcard/refresh-contact-info.py @@ -0,0 +1,62 @@ + +""" +Test ContactInfo support. +""" + +from servicetest import call_async, EventPattern, assertEquals +from gabbletest import exec_test, acknowledge_iq, make_result_iq +import constants as cs +import dbus + + +def test(q, bus, conn, stream): + conn.Connect() + _, event = q.expect_many( + EventPattern('dbus-signal', signal='StatusChanged', + args=[cs.CONN_STATUS_CONNECTED, cs.CSR_REQUESTED]), + EventPattern('stream-iq', to=None, query_ns='vcard-temp', + query_name='vCard')) + + acknowledge_iq(stream, event.stanza) + + handle = conn.RequestHandles(1, ['bob@foo.com'])[0] + call_async(q, conn.ContactInfo, 'RefreshContactInfo', [handle]) + + event = q.expect('stream-iq', to='bob@foo.com', query_ns='vcard-temp', + query_name='vCard') + result = make_result_iq(stream, event.stanza) + result.firstChildElement().addElement('FN', content='Bob') + n = result.firstChildElement().addElement('N') + n.addElement('GIVEN', content='Bob') + result.firstChildElement().addElement('NICKNAME', + content=r'bob,bob1\,,bob2,bob3\,bob4') + label = result.firstChildElement().addElement('LABEL') + label.addElement('LINE', content='42 West Wallaby Street') + label.addElement('LINE', content="Bishop's Stortford\n") + label.addElement('LINE', content='Huntingdon') + org = result.firstChildElement().addElement('ORG') + # ORG is a sequence of decreasingly large org.units, starting + # with the organisation name itself (but here we've moved the org name + # to the end, to make sure that works.) + org.addElement('ORGUNIT', content='Dept. of Examples') + org.addElement('ORGUNIT', content='Exemplary Team') + org.addElement('ORGNAME', content='Collabora Ltd.') + stream.send(result) + + q.expect('dbus-signal', signal='ContactInfoChanged', + args=[handle, [(u'fn', [], [u'Bob']), + (u'n', [], [u'', u'Bob', u'', u'', u'']), + (u'nickname', [], [r'bob,bob1\,,bob2,bob3\,bob4']), + # LABEL comes out as a single blob of text + (u'label', [], ['42 West Wallaby Street\n' + "Bishop's Stortford\n" + 'Huntingdon\n']), + # ORG is a sequence of decreasingly large org.units, starting + # with the organisation + (u'org', [], [u'Collabora Ltd.', u'Dept. of Examples', + u'Exemplary Team']), + ]]) + + +if __name__ == '__main__': + exec_test(test) diff --git a/tests/twisted/vcard/set-contact-info.py b/tests/twisted/vcard/set-contact-info.py new file mode 100644 index 000000000..080a24686 --- /dev/null +++ b/tests/twisted/vcard/set-contact-info.py @@ -0,0 +1,302 @@ + +""" +Test ContactInfo setting support. +""" + +from servicetest import (EventPattern, call_async, assertEquals, assertLength, + assertContains, sync_dbus) +from gabbletest import exec_test, acknowledge_iq, sync_stream +import constants as cs + +from twisted.words.xish import xpath +from twisted.words.protocols.jabber.client import IQ + +def repeat_previous_vcard(stream, iq, previous): + result = IQ(stream, 'result') + result['id'] = iq['id'] + to = iq.getAttribute('to') + + if to is not None: + result["from"] = to + + result.addRawXml(previous.firstChildElement().toXml()) + stream.send(result) + +def test(q, bus, conn, stream): + conn.Connect() + _, event = q.expect_many( + EventPattern('dbus-signal', signal='StatusChanged', + args=[cs.CONN_STATUS_CONNECTED, cs.CSR_REQUESTED]), + EventPattern('stream-iq', to=None, query_ns='vcard-temp', + query_name='vCard'), + ) + + sync_stream(q, stream) + sync_dbus(bus, q, conn) + + acknowledge_iq(stream, event.stanza) + + call_async(q, conn.ContactInfo, 'SetContactInfo', + [(u'fn', [], [u'Wee Ninja']), + (u'n', [], [u'Ninja', u'Wee', u'', u'', u'-san']), + (u'org', [], ['Collabora, Ltd.']), + (u'adr', ['type=work','type=postal','type=parcel'], + ['', '', '11 Kings Parade', 'Cambridge', 'Cambridgeshire', + 'CB2 1SJ', 'UK']), + (u'label', ['type=work'], [ + '11 Kings Parade\n' + 'Cambridge\n' + 'Cambridgeshire\n' + 'CB2 1SJ\n' + 'UK\n']), + (u'tel', ['type=voice','type=work'], ['+44 1223 362967']), + (u'tel', ['type=voice','type=work'], ['+44 7700 900753']), + (u'email', ['type=internet','type=pref'], + ['wee.ninja@collabora.co.uk']), + (u'email', ['type=internet'], ['wee.ninja@example.com']), + (u'x-jabber', [], ['wee.ninja@collabora.co.uk']), + (u'x-jabber', [], ['wee.ninja@example.com']), + (u'url', [], ['http://www.thinkgeek.com/geektoys/plush/8823/']), + (u'nickname', [], [u'HR Ninja']), + (u'nickname', [], [u'Enforcement Ninja'])]) + + vcard_set_event = q.expect('stream-iq', iq_type='set', + query_ns='vcard-temp', query_name='vCard') + + assertLength(2, xpath.queryForNodes('/iq/vCard/NICKNAME', + vcard_set_event.stanza)) + nicknames = [] + for nickname in xpath.queryForNodes('/iq/vCard/NICKNAME', + vcard_set_event.stanza): + nicknames.append(str(nickname)) + assertEquals(['HR Ninja', 'Enforcement Ninja'], nicknames) + + assertEquals(None, xpath.queryForNodes('/iq/vCard/PHOTO', + vcard_set_event.stanza)) + assertLength(1, xpath.queryForNodes('/iq/vCard/N', + vcard_set_event.stanza)) + assertEquals('Wee', xpath.queryForString('/iq/vCard/N/GIVEN', + vcard_set_event.stanza)) + assertEquals('Ninja', xpath.queryForString('/iq/vCard/N/FAMILY', + vcard_set_event.stanza)) + assertEquals('-san', xpath.queryForString('/iq/vCard/N/SUFFIX', + vcard_set_event.stanza)) + assertEquals('Wee Ninja', xpath.queryForString('/iq/vCard/FN', + vcard_set_event.stanza)) + + assertLength(1, xpath.queryForNodes('/iq/vCard/ORG', + vcard_set_event.stanza)) + assertEquals('Collabora, Ltd.', + xpath.queryForString('/iq/vCard/ORG/ORGNAME', + vcard_set_event.stanza)) + assertEquals(None, xpath.queryForNodes('/iq/vCard/ORG/ORGUNIT', + vcard_set_event.stanza)) + + assertLength(1, xpath.queryForNodes('/iq/vCard/LABEL', + vcard_set_event.stanza)) + lines = xpath.queryForNodes('/iq/vCard/LABEL/LINE', vcard_set_event.stanza) + assertLength(5, lines) + for i, exp_line in enumerate(['11 Kings Parade', 'Cambridge', + 'Cambridgeshire', 'CB2 1SJ', 'UK']): + assertEquals(exp_line, str(lines[i])) + + assertLength(2, xpath.queryForNodes('/iq/vCard/TEL', + vcard_set_event.stanza)) + for tel in xpath.queryForNodes('/iq/vCard/TEL', vcard_set_event.stanza): + assertLength(1, xpath.queryForNodes('/TEL/NUMBER', tel)) + assertContains(xpath.queryForString('/TEL/NUMBER', tel), + ('+44 1223 362967', '+44 7700 900753')) + assertLength(1, xpath.queryForNodes('/TEL/VOICE', tel)) + assertLength(1, xpath.queryForNodes('/TEL/WORK', tel)) + + assertLength(2, xpath.queryForNodes('/iq/vCard/EMAIL', + vcard_set_event.stanza)) + for email in xpath.queryForNodes('/iq/vCard/EMAIL', + vcard_set_event.stanza): + assertContains(xpath.queryForString('/EMAIL/USERID', email), + ('wee.ninja@example.com', 'wee.ninja@collabora.co.uk')) + assertLength(1, xpath.queryForNodes('/EMAIL/INTERNET', email)) + if 'collabora' in xpath.queryForString('/EMAIL/USERID', email): + assertLength(1, xpath.queryForNodes('/EMAIL/PREF', email)) + else: + assertEquals(None, xpath.queryForNodes('/EMAIL/PREF', email)) + + assertLength(2, xpath.queryForNodes('/iq/vCard/JABBERID', + vcard_set_event.stanza)) + for jid in xpath.queryForNodes('/iq/vCard/JABBERID', + vcard_set_event.stanza): + assertContains(xpath.queryForString('/JABBERID', jid), + ('wee.ninja@example.com', 'wee.ninja@collabora.co.uk')) + + acknowledge_iq(stream, vcard_set_event.stanza) + q.expect_many( + EventPattern('dbus-return', method='SetContactInfo'), + EventPattern('dbus-signal', signal='AliasesChanged', + predicate=lambda e: e.args[0][0][1] == 'HR Ninja'), + EventPattern('dbus-signal', signal='ContactInfoChanged'), + ) + + # Exercise various invalid SetContactInfo operations + sync_stream(q, stream) + sync_dbus(bus, q, conn) + + forbidden = [EventPattern('stream-iq', query_ns='vcard-temp')] + q.forbid_events(forbidden) + + # unknown field + call_async(q, conn.ContactInfo, 'SetContactInfo', + [('x-salary', [], ['547 espressos per month'])]) + q.expect('dbus-error', method='SetContactInfo', name=cs.INVALID_ARGUMENT) + + # not enough values for a simple field + call_async(q, conn.ContactInfo, 'SetContactInfo', + [('fn', [], [])]) + q.expect('dbus-error', method='SetContactInfo', name=cs.INVALID_ARGUMENT) + + # too many values for a simple field + call_async(q, conn.ContactInfo, 'SetContactInfo', + [('fn', [], ['Wee', 'Ninja'])]) + q.expect('dbus-error', method='SetContactInfo', name=cs.INVALID_ARGUMENT) + + # unsupported type-parameter for a simple field + call_async(q, conn.ContactInfo, 'SetContactInfo', + [('fn', ['language=ja'], ['Wee Ninja'])]) + q.expect('dbus-error', method='SetContactInfo', name=cs.INVALID_ARGUMENT) + + # unsupported type-parameter for a structured field + call_async(q, conn.ContactInfo, 'SetContactInfo', + [(u'n', ['language=ja'], [u'Ninja', u'Wee', u'', u'', u'-san'])]) + q.expect('dbus-error', method='SetContactInfo', name=cs.INVALID_ARGUMENT) + + # unsupported type-parameter for LABEL + call_async(q, conn.ContactInfo, 'SetContactInfo', + [('label', ['language=en'], ['Collabora Ltd.\n11 Kings Parade'])]) + q.expect('dbus-error', method='SetContactInfo', name=cs.INVALID_ARGUMENT) + + # not enough values for LABEL + call_async(q, conn.ContactInfo, 'SetContactInfo', + [('label', [], [])]) + q.expect('dbus-error', method='SetContactInfo', name=cs.INVALID_ARGUMENT) + + # too many values for LABEL + call_async(q, conn.ContactInfo, 'SetContactInfo', + [('label', [], ['11 Kings Parade', 'Cambridge'])]) + q.expect('dbus-error', method='SetContactInfo', name=cs.INVALID_ARGUMENT) + + # unsupported type-parameter for ORG + call_async(q, conn.ContactInfo, 'SetContactInfo', + [('org', ['language=en'], ['Collabora Ltd.'])]) + q.expect('dbus-error', method='SetContactInfo', name=cs.INVALID_ARGUMENT) + + # empty ORG + call_async(q, conn.ContactInfo, 'SetContactInfo', + [('org', [], [])]) + q.expect('dbus-error', method='SetContactInfo', name=cs.INVALID_ARGUMENT) + + # not enough values for N + call_async(q, conn.ContactInfo, 'SetContactInfo', + [('n', [], ['Ninja'])]) + q.expect('dbus-error', method='SetContactInfo', name=cs.INVALID_ARGUMENT) + + # too many values for N + call_async(q, conn.ContactInfo, 'SetContactInfo', + [('n', [], + 'what could it mean if you have too many field values?'.split())]) + q.expect('dbus-error', method='SetContactInfo', name=cs.INVALID_ARGUMENT) + + q.unforbid_events(forbidden) + + # Following a reshuffle, Company Policy Enforcement is declared to be + # a sub-department within Human Resources, and the ninja no longer + # qualifies for a company phone + + vcard_in = [(u'fn', [], [u'Wee Ninja']), + (u'n', [], [u'Ninja', u'Wee', u'', u'', u'-san']), + (u'org', [], ['Collabora, Ltd.', + 'Human Resources', 'Company Policy Enforcement']), + (u'adr', ['type=work','type=postal','type=parcel'], + ['', '', '11 Kings Parade', 'Cambridge', 'Cambridgeshire', + 'CB2 1SJ', 'UK']), + (u'tel', ['type=voice','type=work'], ['+44 1223 362967']), + (u'email', ['type=internet','type=pref'], + ['wee.ninja@collabora.co.uk']), + (u'email', ['type=internet'], ['wee.ninja@example.com']), + (u'url', [], ['http://www.thinkgeek.com/geektoys/plush/8823/']), + (u'nickname', [], [u'HR Ninja']), + (u'nickname', [], [u'Enforcement Ninja'])] + + call_async(q, conn.ContactInfo, 'SetContactInfo', vcard_in) + + event = q.expect('stream-iq', iq_type='get', query_ns='vcard-temp', + query_name='vCard') + repeat_previous_vcard(stream, event.stanza, vcard_set_event.stanza) + + _, vcard_set_event = q.expect_many( + EventPattern('dbus-signal', signal='ContactInfoChanged'), + EventPattern('stream-iq', iq_type='set', query_ns='vcard-temp', + query_name='vCard'), + ) + + assertLength(1, xpath.queryForNodes('/iq/vCard/ORG', + vcard_set_event.stanza)) + assertEquals('Collabora, Ltd.', + xpath.queryForString('/iq/vCard/ORG/ORGNAME', + vcard_set_event.stanza)) + units = xpath.queryForNodes('/iq/vCard/ORG/ORGUNIT', + vcard_set_event.stanza) + assertLength(2, units) + for i, exp_unit in enumerate(['Human Resources', + 'Company Policy Enforcement']): + assertEquals(exp_unit, str(units[i])) + + assertLength(1, xpath.queryForNodes('/iq/vCard/TEL', + vcard_set_event.stanza)) + for tel in xpath.queryForNodes('/iq/vCard/TEL', vcard_set_event.stanza): + assertLength(1, xpath.queryForNodes('/TEL/NUMBER', tel)) + assertEquals('+44 1223 362967', + xpath.queryForString('/TEL/NUMBER', tel)) + assertLength(1, xpath.queryForNodes('/TEL/VOICE', tel)) + assertLength(1, xpath.queryForNodes('/TEL/WORK', tel)) + + acknowledge_iq(stream, vcard_set_event.stanza) + _, event = q.expect_many( + EventPattern('dbus-return', method='SetContactInfo'), + EventPattern('dbus-signal', signal='ContactInfoChanged'), + ) + + vcard_out = event.args[1][:] + + # the only change we expect to see is that perhaps the fields are + # re-ordered, and perhaps the types on the 'tel' are re-ordered + + assertEquals(vcard_in[4][0], 'tel') + vcard_in[4][1].sort() + assertEquals(vcard_out[4][0], 'tel') + vcard_out[4][1].sort() + assertEquals(vcard_in, vcard_out) + + # Finally, the ninja decides that publishing his contact details is not + # very ninja-like, and decides to be anonymous. The first (most important) + # of his nicknames from the old vCard is kept, due to nickname's dual role + # as ContactInfo and the alias. + call_async(q, conn.ContactInfo, 'SetContactInfo', []) + + event = q.expect('stream-iq', iq_type='get', query_ns='vcard-temp', + query_name='vCard') + repeat_previous_vcard(stream, event.stanza, vcard_set_event.stanza) + + vcard_set_event = q.expect('stream-iq', iq_type='set', + query_ns='vcard-temp', query_name='vCard') + assertLength(1, xpath.queryForNodes('/iq/vCard/*', + vcard_set_event.stanza)) + assertEquals('HR Ninja', xpath.queryForString('/iq/vCard/NICKNAME', + vcard_set_event.stanza)) + + acknowledge_iq(stream, vcard_set_event.stanza) + q.expect_many( + EventPattern('dbus-return', method='SetContactInfo'), + EventPattern('dbus-signal', signal='ContactInfoChanged'), + ) + +if __name__ == '__main__': + exec_test(test) diff --git a/tests/twisted/vcard/supported-fields.py b/tests/twisted/vcard/supported-fields.py new file mode 100644 index 000000000..13a92eb8b --- /dev/null +++ b/tests/twisted/vcard/supported-fields.py @@ -0,0 +1,107 @@ +# -*- coding: utf-8 -*- + +"""Feature test for ContactInfo.SupportedFields +""" + +# Copyright © 2010 Collabora Ltd. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +from servicetest import (EventPattern, assertEquals, call_async) +from gabbletest import (exec_test, GoogleXmlStream) +import constants as cs + +PARAMS_MANDATORY = cs.CONTACT_INFO_FIELD_FLAG_PARAMETERS_MANDATORY +UNLIMITED = 0xffffffffL + +def types(s): + ret = ['type=%s' % t for t in s.split()] + ret.sort() + return ret + +def check_google_props(props): + assertEquals(cs.CONTACT_INFO_FLAG_CAN_SET, props['ContactInfoFlags']) + sf = props['SupportedFields'] + sf.sort() + for f in sf: + f[1].sort() # type-parameters + assertEquals([ + ('fn', [], PARAMS_MANDATORY, 1), + ('n', [], PARAMS_MANDATORY, 1), + ], sf) + +def check_normal_props(props): + assertEquals(cs.CONTACT_INFO_FLAG_CAN_SET, props['ContactInfoFlags']) + sf = props['SupportedFields'] + sf.sort() + for f in sf: + f[1].sort() # type-parameters + assertEquals([ + ('adr', types('home work postal parcel dom intl pref'), 0, UNLIMITED), + ('bday', [], PARAMS_MANDATORY, UNLIMITED), + ('email', types('home work internet pref x400'), 0, UNLIMITED), + ('fn', [], PARAMS_MANDATORY, 1), + ('geo', [], PARAMS_MANDATORY, 1), + ('label', types('home work postal parcel dom intl pref'), 0, + UNLIMITED), + ('mailer', [], PARAMS_MANDATORY, UNLIMITED), + ('n', [], PARAMS_MANDATORY, 1), + ('nickname', [], PARAMS_MANDATORY, UNLIMITED), + ('note', [], PARAMS_MANDATORY, UNLIMITED), + ('org', [], PARAMS_MANDATORY, UNLIMITED), + ('prodid', [], PARAMS_MANDATORY, UNLIMITED), + ('rev', [], PARAMS_MANDATORY, UNLIMITED), + ('role', [], PARAMS_MANDATORY, UNLIMITED), + ('sort-string', [], PARAMS_MANDATORY, UNLIMITED), + ('tel', types('home work voice fax pager msg cell video bbs ' + 'modem isdn pcs pref'), 0, UNLIMITED), + ('title', [], PARAMS_MANDATORY, UNLIMITED), + ('tz', [], PARAMS_MANDATORY, UNLIMITED), + ('uid', [], PARAMS_MANDATORY, UNLIMITED), + ('url', [], PARAMS_MANDATORY, UNLIMITED), + ('x-desc', [], PARAMS_MANDATORY, UNLIMITED), + ('x-jabber', [], PARAMS_MANDATORY, UNLIMITED), + ], sf) + +def test_google(q, bus, conn, stream): + test(q, bus, conn, stream, is_google=True) + +def test(q, bus, conn, stream, is_google=False): + props = conn.GetAll(cs.CONN_IFACE_CONTACT_INFO, + dbus_interface=cs.PROPERTIES_IFACE) + check_normal_props(props) + + conn.Connect() + + q.expect('dbus-signal', signal='StatusChanged', + args=[cs.CONN_STATUS_CONNECTED, cs.CSR_REQUESTED]) + + props = conn.GetAll(cs.CONN_IFACE_CONTACT_INFO, + dbus_interface=cs.PROPERTIES_IFACE) + + if is_google: + check_google_props(props) + + # on a Google server, we can't use most vCard fields + call_async(q, conn.ContactInfo, 'SetContactInfo', + [('x-jabber', [], ['wee.ninja@collabora.co.uk'])]) + q.expect('dbus-error', method='SetContactInfo', + name=cs.INVALID_ARGUMENT) + else: + check_normal_props(props) + +if __name__ == '__main__': + exec_test(test) + exec_test(test_google, protocol=GoogleXmlStream) |