summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndre Moreira Magalhaes (andrunko) <andre.magalhaes@collabora.co.uk>2010-04-14 13:24:02 -0300
committerAndre Moreira Magalhaes (andrunko) <andre.magalhaes@collabora.co.uk>2010-04-14 13:24:11 -0300
commit52679ccc631a6b8544cf5ce373fca6b22501fb4b (patch)
treedc9d5dbea07a8a4a29acce93163caef038ec6313
parent32c575e530251420d69c24c2768517ce5e07b889 (diff)
parentac516315a046e0e086822c7aba97df63b890eea3 (diff)
Merge branch 'contact-info'
Reviewed-by: Simon McVittie <simon.mcvittie@collabora.co.uk>
-rw-r--r--extensions/Connection_Interface_Contact_Info.xml136
-rw-r--r--src/Makefile.am2
-rw-r--r--src/conn-aliasing.c12
-rw-r--r--src/conn-avatars.c31
-rw-r--r--src/conn-contact-info.c1152
-rw-r--r--src/conn-contact-info.h40
-rw-r--r--src/connection.c27
-rw-r--r--src/connection.h6
-rw-r--r--src/util.c7
-rw-r--r--src/util.h2
-rw-r--r--src/vcard-manager.c592
-rw-r--r--src/vcard-manager.h45
-rw-r--r--tests/twisted/Makefile.am4
-rw-r--r--tests/twisted/constants.py5
-rw-r--r--tests/twisted/servicetest.py1
-rw-r--r--tests/twisted/vcard/get-contact-info.py65
-rw-r--r--tests/twisted/vcard/overlapping-sets.py96
-rw-r--r--tests/twisted/vcard/redundant-set.py13
-rw-r--r--tests/twisted/vcard/refresh-contact-info.py62
-rw-r--r--tests/twisted/vcard/set-contact-info.py302
-rw-r--r--tests/twisted/vcard/supported-fields.py107
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)