propagate from branch 'im.pidgin.pidgin' (head dc77ee83fe8dd877daae0019926ee40dd6b9b0ac) soc.2008.masterpassword

Sat, 19 Jul 2008 17:45:27 +0000

author
Vivien Bernet-Rollande <scrouaf@soc.pidgin.im>
date
Sat, 19 Jul 2008 17:45:27 +0000
branch
soc.2008.masterpassword
changeset 33977
3da7eb538e04
parent 23884
dc77ee83fe8d (current diff)
parent 33976
ff88d1340abe (diff)
child 33978
2c649fae664e

propagate from branch 'im.pidgin.pidgin' (head dc77ee83fe8dd877daae0019926ee40dd6b9b0ac)
to branch 'im.pidgin.soc.2008.masterpassword' (head ff88d1340abeb30db8368db451aed2c3a9cb44a1)

COPYRIGHT file | annotate | diff | comparison | revisions
libpurple/plugin.h file | annotate | diff | comparison | revisions
libpurple/protocols/msn/soap.c file | annotate | diff | comparison | revisions
libpurple/protocols/msn/soap.h file | annotate | diff | comparison | revisions
libpurple/protocols/msn/soap2.c file | annotate | diff | comparison | revisions
libpurple/protocols/msn/soap2.h file | annotate | diff | comparison | revisions
--- a/COPYRIGHT	Sat Jul 19 03:10:10 2008 +0000
+++ b/COPYRIGHT	Sat Jul 19 17:45:27 2008 +0000
@@ -30,6 +30,7 @@
 Dave Bell
 Igor Belyi
 Brian Bernas
+Vivien Bernet-Rollande
 Paul Betts
 Jonas Birmé
 George-Cristian Bîrzan
--- a/libpurple/account.c	Sat Jul 19 03:10:10 2008 +0000
+++ b/libpurple/account.c	Sat Jul 19 17:45:27 2008 +0000
@@ -41,6 +41,14 @@
 #include "util.h"
 #include "xmlnode.h"
 
+/**
+ * TODO :
+ *  - grab Trannie's code for asynch connection
+ *  - re-write account parsing and syncing as async
+ *  - read password _after_ the rest of the account
+ */
+
+
 typedef struct
 {
 	PurpleConnectionErrorInfo *current_error;
@@ -371,7 +379,7 @@
 	child = xmlnode_new_child(node, "name");
 	xmlnode_insert_data(child, purple_account_get_username(account), -1);
 
-	if (purple_account_get_remember_password(account) &&
+	if (purple_account_get_remember_password(account) &&			//  FIXME : change this so it asks the plugin for the node
 		((tmp = purple_account_get_password(account)) != NULL))
 	{
 		child = xmlnode_new_child(node, "password");
@@ -794,7 +802,7 @@
 	g_free(protocol_id);
 
 	/* Read the password */
-	child = xmlnode_get_child(node, "password");
+	child = xmlnode_get_child(node, "password");					// FIXME : call plugin 
 	if ((child != NULL) && ((data = xmlnode_get_data(child)) != NULL))
 	{
 		purple_account_set_remember_password(ret, TRUE);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libpurple/keyring.c	Sat Jul 19 17:45:27 2008 +0000
@@ -0,0 +1,574 @@
+/**
+ * @file keyring.c Keyring plugin API
+ */
+
+/* purple
+ *
+ * Purple is the legal property of its developers, whose names are too numerous
+ * to list here.  Please refer to the COPYRIGHT file distributed with this
+ * source distribution.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02111-1301  USA
+ */
+
+
+/**
+ * Most functions in this files are :
+ *  - wrappers that will call the plugin
+ *  - keyring managment stuff
+ *  - accessors
+ *
+ * TODO :
+ *  - unregister
+ *  - use accessors
+ *  - purple_keyring_init()
+ *  - purple_keyring_set_inuse() needs to be async for error checking and reversability.
+ *
+ * Questions :
+ *  - cleanup
+ *  - warning
+ *  - breaking API
+ */
+
+#include <glib.h>
+#include "keyring.h"
+#include "account.h"
+
+/*******************************/
+/* opaque structures           */
+/*******************************/
+
+/* used to import and export password info */
+struct _PurpleKeyringPasswordNode
+{
+	PurpleAccount * account;
+	const char * encryption;
+	const char * mode;
+	const char * data;
+/**
+ * strings cannot be modified.
+ * all are static excpet data
+ */
+};
+
+
+/*******************************/
+/* globals                     */
+/*******************************/
+
+GList * purple_keyring_keyringlist = NULL;	/* list of available keyrings 	*/
+PurpleKeyring * purple_keyring_inuse = NULL;	/* keyring being used		*/
+
+
+/*******************************/
+/* functions                   */
+/*******************************/
+
+/* manipulate keyring list, used by config interface */
+
+const GList * 
+purple_keyring_get_keyringlist()
+{
+	return purple_keyring_keyringlist;
+}
+
+const PurpleKeyring * 
+purple_keyring_get_inuse()
+{
+	return purple_keyring_inuse;
+}
+
+
+/**
+ * If reading fails, export empty, issue warning, continue
+ * If writing fails, abort.
+ */
+struct _PurpleKeyringChangeTracker
+{
+	GError ** error;			// could probably be dropped
+	PurpleKeyringSetInUseCb cb;
+	gpointer data;
+	PurpleKeyring * new;
+	PurpleKeyring * old;		// we are done when : finished == TRUE && read_outstanding == 0
+	int read_outstanding;
+	gboolean finished;
+	gboolean abort;
+};
+
+void
+purple_keyring_set_inuse_check_error_cb(const PurpleAccount * account,
+					GError ** error,
+					gpointer data)
+{
+
+	const char * name;
+	PurpleKeyringClose close;
+	struct _PurpleKeyringChangeTracker * tracker;
+
+	tracker = (struct _PurpleKeyringChangeTracker *)data;
+
+	name = purple_account_get_username(account);
+
+	if ((error != NULL) && ((**error).domain == ERR_PIDGINKEYRING)) {
+
+		switch((**error).code) {
+
+			case ERR_NOPASSWD :
+				g_debug("No password found while changing keyring for account %s", name);
+				break;
+
+			case ERR_NOACCOUNT :
+				g_debug("No info on account %s found while changing keyring", name);
+				break;
+
+			case ERR_NOCHANNEL :
+				g_debug("Failed to communicate with backend while changing keyring for account %s, aborting change.", name);
+				tracker->abort == TRUE;
+				break;
+
+			default :
+				// FIXME : display error string
+				g_debug("Unknown error while changing keyring for account %s", name);
+				break;
+		}
+	}
+
+	/* if this was the last one */
+	if (tracker->finished == TRUE) && (tracker->read_outstanding == 0)) {
+	
+		if (tracker->abort == TRUE) {
+			
+			tracker->abort = TRUE;
+
+			close = purple_keyring_get_close_keyring(tracker->old);
+			close(error);			
+
+			g_free(tracker);
+			return;
+		} else {
+			close = purple_keyring_get_close_keyring(tracker->new);
+			close(error);
+
+			tracker->cb(TRUE, error, tracker->data);
+			g_free(tracker);
+			return;
+		}
+
+	}
+	return;
+}
+
+
+void
+purple_keyring_set_inuse_got_pw_cb(const PurpleAccount * account, 
+			 gchar * password, 
+			 GError ** error, 
+			 gpointer data)
+{
+	PurpleKeyring * new;
+	PurpleKeyringSave save;
+	struct _PurpleKeyringChangeTracker * tracker;
+
+	
+	tracker = (struct _PurpleKeyringChangeTracker *)data;
+	new = tracker->new;
+
+	/* XXX check for read error or just forward ? */
+
+	tracker->read_outstanding--;
+	
+	save = purple_keyring_get_save_password(new);
+	save(account, password, error, purple_keyring_set_inuse_check_error_cb, 
+		tracker);
+
+	return;
+}
+
+/* FIXME : needs to be async and cancelable */
+/* PurpleKeyringSetInUseCb */
+void 
+purple_keyring_set_inuse(PurpleKeyring * new,
+			 GError ** error,
+			 PurpleKeyringSetInUseCb cb,
+			 gpointer data)
+{
+
+	GList * cur;
+	const PurpleKeyring * old;
+	PurpleKeyringRead read;
+	struct _PurpleKeyringChangeTracker * tracker;
+
+
+	if (purple_keyring_inuse != NULL) {
+
+		tracker = g_malloc(sizeof(struct _PurpleKeyringChangeTracker));
+		old = purple_keyring_get_inuse();
+
+		tracker->error = error;
+		tracker->cb = cb;
+		tracker->data = data;
+		tracker->new = new;
+		tracker->old = old;
+		tracker->read_outstanding = 0;
+		tracker->finished = FALSE;
+		tracker->abort = FALSE;
+
+		for (cur = purple_accounts_get_all(); 
+		    (cur != NULL) && (tracker->abort != TRUE);
+		    cur = cur->next)
+		{
+			tracker->read_outstanding++;
+
+			if (cur->next == NULL) {
+				tracker->finished = TRUE;
+			}
+
+			read = purple_keyring_get_read_password(old);
+			read(cur->data, error, purple_keyring_set_inuse_got_pw_cb, tracker);
+		}
+
+	} else { /* no keyring was set before. */
+
+		purple_keyring_inuse = new;
+		cb(data);
+		return;
+	}
+}
+
+/* register a keyring plugin */
+/**
+ * XXX : function to unregister a keyring ?
+ *	 validate input ? add magix field ?
+ */
+void 
+purple_keyring_register(PurpleKeyring * info)
+{
+	// TODO : emit signal
+	purple_keyring_keyringlist = g_list_prepend(purple_keyring_keyringlist,
+		info);
+}
+
+purple_keyring_unregister(PurpleKeyring * info)
+{
+	
+}
+/**
+ * wrappers to import and export passwords
+ */
+
+
+/** 
+ * used by account.c while reading a password from xml
+ * might not really need to be async.
+ * TODO : rewrite InternalKeyring as async
+ */
+void
+purple_keyring_import_password(const PurpleKeyringPasswordNode * passwordnode,
+				GError ** error,
+				PurpleKeyringImportCallback cb,
+				gpointer data)
+{
+	PurpleKeyringImportPassword import;
+
+	if (purple_keyring_inuse == NULL) {
+
+		g_set_error(error, ERR_PIDGINKEYRING, ERR_NOKEYRING, 
+			"No Keyring configured.");
+		cb(error, data);
+
+	} else {
+		import = purple_keyring_get_import_password(purple_keyring_inuse);
+		import(passwordnode, error, cb, data);
+	}
+	return;
+}
+
+/* 
+ * used by account.c while syncing accounts
+ *  returned data must be g_free()'d
+ */
+void
+purple_keyring_export_password(PurpleAccount * account,
+			       GError ** error,
+			       PurpleKeyringExportCallback cb,
+			       gpointer data)
+{
+	PurpleKeyringExportPassword export;
+
+	if (purple_keyring_inuse == NULL) {
+
+		g_set_error(error, ERR_PIDGINKEYRING, ERR_NOKEYRING, 
+			"No Keyring configured.");
+		cb(NULL, error, data);
+
+	} else {
+		export = purple_keyring_get_export_password(purple_keyring_inuse);
+		export(account, error, cb, data);
+	}
+	return;
+}
+
+
+/** 
+ * functions called from the code to access passwords (account.h):
+ *	purple_account_get_password()	<- TODO : rewrite these functions as asynch, as well as functions calling them. Will most likely break API compatibility.
+ *	purple_account_set_password()
+ * so these functions will call : 
+ */
+void 
+purple_keyring_get_password(const PurpleAccount *account,
+			    GError ** error,
+			    PurpleKeyringReadCallback cb,
+			    gpointer data)
+{
+	PurpleKeyringRead read;
+
+	if (purple_keyring_inuse == NULL) {
+
+		g_set_error(error, ERR_PIDGINKEYRING, ERR_NOKEYRING, 
+			"No Keyring configured.");
+		cb(account, NULL, error, data);
+
+	} else {
+
+		read = purple_keyring_get_read_password(purple_keyring_inuse);
+		read(account, error, cb, data);
+
+	}
+	return;
+}
+
+void 
+purple_keyring_set_password(const PurpleAccount * account, 
+			    gchar * password, 
+			    GError ** error, 
+			    PurpleKeyringSaveCallback cb,
+			    gpointer data)
+{
+	PurpleKeyringSave save;
+
+	if (purple_keyring_inuse == NULL) {
+
+		g_set_error(error, ERR_PIDGINKEYRING, ERR_NOKEYRING, 
+				"No Keyring configured.");
+		cb(account, error, data);
+
+	} else {
+		save = purple_keyring_get_save_password(purple_keyring_inuse);
+		save(account, password, error, cb, data);
+	}
+	return;
+}
+
+/* accessors for data structure fields */
+	/* PurpleKeyring */
+const char *
+purple_keyring_get_name(const PurpleKeyring * info)
+{
+	return info->name;
+}
+
+PurpleKeyringRead 
+purple_keyring_get_read_password(const PurpleKeyring * info)
+{
+	return info->read_password;
+}
+
+PurpleKeyringSave
+purple_keyring_get_save_password(const PurpleKeyring * info)
+{
+	return info->save_password;
+}
+
+PurpleKeyringClose 
+purple_keyring_get_close_keyring(const PurpleKeyring * info)
+{
+	return info->close_keyring;
+}
+
+PurpleKeyringFree 
+purple_keyring_get_free_password(const PurpleKeyring * info)
+{
+	return info->free_password;
+}
+
+PurpleKeyringChangeMaster 
+purple_keyring_get_change_master(const PurpleKeyring * info)
+{
+	return info->change_master;
+}
+
+PurpleKeyringImportPassword
+purple_keyring_get_import_password(const PurpleKeyring * info)
+{
+	return info->import_password;
+}
+
+PurpleKeyringExportPassword 
+purple_keyring_get_export_password(const PurpleKeyring * info)
+{
+	return info->export_password;
+}
+
+
+void 
+purple_keyring_set_name(PurpleKeyring * info,
+			char * name)
+{
+	info->name = name;
+}
+
+void 
+purple_keyring_set_read_password(PurpleKeyring * info,
+				 PurpleKeyringRead read)
+{
+	info->read_password = read; /*  returned data must be g_free()'d */
+}
+
+void 
+purple_keyring_set_save_password(PurpleKeyring * info,
+				 PurpleKeyringSave save)
+{
+	info->save_password = save;
+}
+
+void 
+purple_keyring_set_close_keyring(PurpleKeyring * info,
+				 PurpleKeyringClose close)
+{
+	info->close_keyring = close;
+}
+
+void 
+purple_keyring_set_free_password(PurpleKeyring * info,
+				 PurpleKeyringFree free)
+{
+	info->free_password = free;
+}
+
+void 
+purple_keyring_set_change_master(PurpleKeyring * info,
+				 PurpleKeyringChangeMaster change_master)
+{
+	info->change_master = change_master;
+}
+
+void 
+purple_keyring_set_import_password(PurpleKeyring * info,
+				   PurpleKeyringImportPassword import_password)
+{
+	info->import_password = import_password;
+}
+void 
+purple_keyring_set_export_password(PurpleKeyring * info,
+				   PurpleKeyringExportPassword export_password)
+{
+	info->export_password = export_password;
+}
+
+
+	/* PurpleKeyringPasswordNode */
+
+PurpleKeyringPasswordNode * 
+purple_keyring_password_node_new()
+{
+	PurpleKeyringPasswordNode * ret;
+
+	ret = g_malloc(sizeof(PurpleKeyringPasswordNode));
+	return ret;
+}
+
+void 
+purple_keyring_password_node_free(PurpleKeyringPasswordNode * node)
+{
+	g_free(node);
+	return;
+}
+
+const PurpleAccount * 
+purple_keyring_password_node_get_account(const PurpleKeyringPasswordNode * info)
+{
+	return info->account;
+}
+
+const char * 
+purple_keyring_password_node_get_encryption(const PurpleKeyringPasswordNode * info)
+{
+	return info->encryption;
+}
+
+const char * 
+purple_keyring_password_node_get_mode(const PurpleKeyringPasswordNode * info)
+{
+	return info->mode;
+}
+
+const char * 
+purple_keyring_password_node_get_data(const PurpleKeyringPasswordNode * info)
+{
+	return info->data;
+}
+
+
+void 
+purple_keyring_password_node_set_account(PurpleKeyringPasswordNode * info, 
+					 PurpleAccount * account)
+{
+	info->account = account;
+	return;	
+}
+
+void 
+purple_keyring_password_node_set_encryption(PurpleKeyringPasswordNode * info,
+					    const char * encryption)
+{
+	info->encryption = encryption;
+	return;
+}
+
+void 
+purple_keyring_password_node_set_mode(PurpleKeyringPasswordNode * info,
+				      const char * mode)
+{
+	info->mode = mode;
+	return;
+}
+
+void 
+purple_keyring_password_node_set_data(PurpleKeyringPasswordNode * info,
+				      const char * data)
+{
+	info->data = data;
+	return;
+}
+
+GQuark
+purple_keyring_error_domain()
+{
+	return g_quark_from_static_string("Libpurple keyring");
+}
+
+/**
+ * prepare stuff (called at startup)
+ * TODO : see if we need to cleanup
+ */
+void purple_keyring_init()
+{//FIXME
+	/**
+	 * read safe to use in confing
+	 * make sure said safe is loaded
+	 * else fallback
+	 */
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libpurple/keyring.h	Sat Jul 19 17:45:27 2008 +0000
@@ -0,0 +1,427 @@
+/**
+ * @file keyring.h Keyring plugin API
+ * @ingroup core
+ *
+ * @todo 
+ *  - Offer a way to prompt the user for a password or for a password change.
+ */
+
+/* purple
+ *
+ * Purple is the legal property of its developers, whose names are too numerous
+ * to list here.  Please refer to the COPYRIGHT file distributed with this
+ * source distribution.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program ; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02111-1301  USA
+ */
+
+#ifndef _PURPLE_KEYRING_H_
+#define _PURPLE_KEYRING_H_
+
+#include <glib.h>
+#include "account.h"
+
+/*******************************************************/
+/** @name data structures and types                    */
+/*******************************************************/
+/*@{*/
+typedef struct _PurpleKeyring PurpleKeyring;				/* public (for now) */
+
+/*@}*/
+
+/**
+ * XXX maybe strip a couple GError* if they're not used,
+ * since they should only be interresting for the callback
+ *  --> ability to forward errors ?
+ *
+ */
+
+/********************************************************/
+/** @name Callbacks for basic keyring operation         */
+/********************************************************/
+/*@{*/
+
+/**
+ * Callback for once a password is read. If there was a problem, the password should
+ * be NULL, and the error set.
+ *
+ * @param account The account of which the password was asked.
+ * @param password The password that was read. This should be freed when the callback returns.
+ * @param error Error that could have occured. Must be freed if non NULL.
+ * @param data Data passed to the callback.
+ */
+typedef void (*PurpleKeyringReadCallback)(const PurpleAccount * account,
+					  gchar * password,
+					  GError ** error,
+					  gpointer data);
+
+/**
+ * Callback for once a password has been stored. If there was a problem, the error will be set.
+ *
+ * @param account The account of which the password was saved.
+ * @param error Error that could have occured. Must be freed if non NULL.
+ * @param data Data passed to the callback.
+ */
+typedef void (*PurpleKeyringSaveCallback)(const PurpleAccount * account, 
+					  GError ** error,
+					  gpointer data);
+
+/**
+ * Callback for once the master password for a keyring has been changed.
+ *
+ * @param result Will be TRUE if the password has been changed, false otherwise.
+ * @param error Error that has occured. Must be freed if non NULL.
+ * @param data Data passed to the callback.
+ */
+typedef void (*PurpleKeyringChangeMasterCallback)(gboolean result,
+						  GError ** error,
+						  gpointer data);
+
+/*@}*/
+
+/********************************************************/
+/** @name pointers to the keyring's functions           */
+/********************************************************/
+/*@{*/
+
+/**
+ * Read the password for an account
+ * @param account The account which's password we want.
+ * @param cb A callback to be used once the password is found.
+ * @param data Data to be passed to the callback.
+ */
+typedef void (*PurpleKeyringRead)(const PurpleAccount * account,
+				  PurpleKeyringReadCallback cb,
+				  gpointer data);
+
+/**
+ * Store a password in the keyring.
+ * @param account The account for which the password is to be stored.
+ * @param password The password to be stored. This password will be freed once 
+ * the function returns, so one must take care to make a copy if they call other 
+ * async functions later. If the password is NULL, this means that the keyring
+ * should forget about that password.
+ * @param destroypassword A function that will be called to free the password.
+ * @param cb A callback to be called once the password is saved.
+ * @param data A pointer to be passed to the callback
+ */
+typedef void (*PurpleKeyringSave)(const PurpleAccount * account,
+				  gchar * password,
+				  GDestroyNotify destroypassword,
+				  PurpleKeyringSaveCallback cb,
+				  gpointer data);
+
+/**
+ * Close the keyring.
+ * This will be called so the keyring can do any cleanup it wants.
+ * @param error An error that may occur.
+ */
+typedef void (*PurpleKeyringClose)(GError ** error);
+
+/**
+ * Change the master password for the keyring. If NULL, it means the Keyring
+ * has no master password capabilities.
+ * @param error An error that may occur.
+ * @param cb A callback to call once the master password has been changed.
+ * @param data A pointer to pass to the callback.
+ */
+typedef void (*PurpleKeyringChangeMaster)(GError ** error,
+					  PurpleKeyringChangeMasterCallback cb,
+					  gpointer data);
+
+
+/* */
+/**
+ * Import info on an XML node.
+ * This is not async because it is not meant to prompt for a master password and decrypt passwords.
+ * @param account The account.
+ * @param mode A keyring specific option that was stored. Can be NULL, will be freed when function returns.
+ * @param data Data that was stored, Can be NULL, will be freed when function returns (so copy it if you need it).
+ * @return TRUE on success, FALSE on failure.
+ */
+typedef gboolean (*PurpleKeyringImportPassword)(PurpleAccount * account, 
+					    char * mode,
+					    char * data,
+					    GError ** error);
+
+/**
+ * Export information that will be stored in an XML node.
+ * This is not async because it is not meant to prompt for a master password or encrypt passwords.
+ * @param account The account for which we want the info.
+ * @param mode An option field that can be used by the plugin. This is expected to be a static string.
+ * @param data The data to be stored in the XML node. This string will be freed using destroy() once not needed anymore.
+ * @param error Will be set if a problem occured.
+ * @param destroy A function to be called, if non NULL, to free data.
+ * @return TRUE on success, FALSE on failure.
+ */
+typedef gboolean (*PurpleKeyringExportPassword)(PurpleAccount * account,
+						char ** mode,
+						char ** data,
+						GError ** error,
+						GDestroyNotify ** destroy);
+
+/**
+ * information about a keyring
+ * @todo : move as hidden, so we can have hidden fields.
+ */
+struct _PurpleKeyring
+{
+	char *  name;
+	PurpleKeyringRead read_password;
+	PurpleKeyringSave save_password;
+	PurpleKeyringClose close_keyring;
+	PurpleKeyringChangeMaster change_master;
+	PurpleKeyringImportPassword import_password;
+	PurpleKeyringExportPassword export_password;
+	gpointer r1;	/* RESERVED */
+	gpointer r2;	/* RESERVED */
+	gpointer r3;	/* RESERVED */
+};
+
+
+/*@}*/
+
+
+/***************************************/
+/** @name Keyring API                  */
+/***************************************/
+/*@{*/
+
+/**
+ * Prepare stuff at startup
+ */
+void 
+purple_keyring_init();
+
+/**
+ * Get the keyring list. Used by the UI.
+ */
+const GList * 
+purple_keyring_get_keyringlist();
+
+/**
+ * Get the keyring being used.
+ */
+const PurpleKeyring * 
+purple_keyring_get_inuse();
+
+/**
+ * Set the keyring to use. This function will move all passwords from
+ * the old keyring to the new one. If it fails, it will cancel all changes,
+ * close the new keyring, and notify the callback. If it succeeds, it will
+ * remove all passwords from the old safe and close that safe.
+ * @param newkeyring The new keyring to use.
+ * @param error Error that may occur.
+ * @param cb The callback to call once the change is done.
+ * @param data A pointer that will be passed to the callback.
+ */
+void
+purple_keyring_set_inuse(const PurpleKeyring * newkeyring,
+			 PurpleKeyringSetInUseCallback cb,
+			 gpointer data);
+
+/**
+ * Register a keyring plugin.
+ * @param keyrint The keyring to register.
+ */
+void 
+purple_plugin_keyring_register(PurpleKeyring * keyring);
+
+/**
+ * Unregister a keyring plugin. In case the keyring is in use,
+ * passwords will be moved to a fallback safe, and the keyring
+ * to unregister will be properly closed.
+ * @param keyrint The keyring to unregister.
+ */
+void 
+purple_plugin_keyring_unregister(PurpleKeyring * keyring);
+
+/*@}*/
+
+
+/***************************************/
+/** @name Keyring plugin wrappers      */
+/***************************************/
+/*@{*/
+
+/**
+ * used by account.c while reading a password from xml.
+ * @param account The account.
+ * @param keyringid The plugin ID that was stored in the xml file. Can be NULL.
+ * @param mode A keyring specific option that was stored. Can be NULL.
+ * @param data Data that was stored, can be NULL.
+ */
+void purple_keyring_import_password(PurpleAccount * account, 
+				    char * keyringid,
+				    char * mode,
+				    char * data,
+				    GError ** error);
+
+/**
+ * used by account.c while syncing accounts
+ * @param account The account for which we want the info.
+ * @param keyringid The plugin id to be stored in the XML node. This will be NULL or a string that can be considered static.
+ * @param mode An option field that can be used by the plugin. This will be NULL or a string that can be considered static.
+ * @param data The data to be stored in the XML node. This string must be freed using destroy() once not needed anymore if it is not NULL.
+ * @param error Will be set if a problem occured.
+ * @param destroy A function to be called, if non NULL, to free data.
+ */
+void 
+purple_keyring_export_password(PurpleAccount * account,
+			       char ** keyringid,
+			       char ** mode,
+			       char ** data,
+			       GError ** error,
+			       GDestroyNotify ** destroy);
+
+
+
+/** 
+ * functions called from the code to access passwords (account.h):
+ *	purple_account_get_password()
+ *	purple_account_set_password()
+ * @todo :
+ *	- rewrite these functions to use the sync functions for compatibility,
+ *	- build an async way to access the async functions, and patch all libpurple 
+ *	code that calls the accessors to use new ones.
+ */
+
+/**
+ * Read a password from the active safe.
+ * This should be renamed purple_keyring_get_password() when getting
+ * to 3.0, while dropping purple_keyring_get_password_sync().
+ * @param account The account for which we want the password.
+ * @param cb The callback to be called.
+ * @param data A pointer passed to the callback.
+ */
+void 
+purple_keyring_get_password_async(const PurpleAccount * account,
+				  PurpleKeyringReadCallback cb,
+				  gpointer data);
+
+/**
+ * Set a password to be remembered.
+ * This should be renamed purple_keyring_set_password() when getting
+ * to 3.0, while dropping purple_keyring_set_password_sync().keyring
+ * @param account The account for which the password is to be saved.
+ * @param password The password to save.
+ * @param destroypassword A function called to free the password. Can be NULL.
+ * @param cb A callback for once the password is saved.
+ * @param data A pointer to be passed to the callback.
+ */
+void 
+purple_keyring_set_password_async(const PurpleAccount * account, 
+				  gchar * password,
+				  GDestroyNotify destroypassword,
+				  PurpleKeyringSaveCallback cb,
+				  gpointer data);
+
+/**
+ * Read a password in a synchronous way.
+ * This is here only for compatibility reasons. Keyrings are not
+ * expected to support this (and shouldn't), and new code should
+ * use purple_keyring_get_password_async() instead.
+ * @param account The account for which we want the password.
+ * @return A pointer to the the password.
+ */
+char * 
+purple_keyring_get_password_sync(const PurpleAccount * account);
+
+/**
+ * Save a password in a synchronous way.
+ * This is here only for compatibility reasons. Keyrings are not
+ * expected to support this (and shouldn't), and new code should
+ * use purple_keyring_set_password_async() instead.
+ * @param account The account for which we want the password.
+ * @param password The password to save.
+ */
+void 
+purple_keyring_set_password_sync(PurpleAccount * account,
+				 const char *password);
+
+/**
+ * Close a safe.
+ * @param keyring The safe to close.
+ * @param error Error that might occur.
+ */
+void
+purple_keyring_close(PurpleKeyring * keyring,
+		     GError ** error);
+
+/**
+ * Change the master password for a safe (if the safe supports it).
+ * @param cb A callback to notify once we're done.
+ * @param data A pointer to be passed to the callback.
+ */
+void 
+purple_keyring_change_master(PurpleKeyringChangeMasterCallback cb,
+			     gpointer data);
+
+/*@}*/
+
+/***************************************/
+/** @name PurpleKeyring Accessors      */
+/***************************************/
+/*@{*/
+
+PurpleKeyring * purple_keyring_new();
+void purple_keyring_free(PurpleKeyring * keyring);
+
+const char * purple_keyring_get_name(const PurpleKeyring * info);
+PurpleKeyringRead purple_keyring_get_read_password(const PurpleKeyring * info);
+PurpleKeyringSave purple_keyring_get_save_password(const PurpleKeyring * info);
+PurpleKeyringClose purple_keyring_get_close_keyring(const PurpleKeyring * info);
+PurpleKeyringChangeMaster purple_keyring_get_change_master(const PurpleKeyring * info);
+PurpleKeyringImportPassword purple_keyring_get_import_password(const PurpleKeyring * info);
+PurpleKeyringExportPassword purple_keyring_get_export_password(const PurpleKeyring * info);
+
+void purple_keyring_set_name(PurpleKeyring * info, char * name);		/* must be static, will not be auto-freed upon destruction */
+void purple_keyring_set_read_password(PurpleKeyring * info, PurpleKeyringRead read);
+void purple_keyring_set_save_password(PurpleKeyring * info, PurpleKeyringSave save);
+void purple_keyring_set_close_keyring(PurpleKeyring * info, PurpleKeyringClose close);
+void purple_keyring_set_change_master(PurpleKeyring * info, PurpleKeyringChangeMaster change_master);
+void purple_keyring_set_import_password(PurpleKeyring * info, PurpleKeyringImportPassword import_password);
+void purple_keyring_set_export_password(PurpleKeyring * info, PurpleKeyringExportPassword export_password);
+
+/*@}*/
+
+/***************************************/
+/** @name Error Codes                  */
+/***************************************/
+/*@{*/
+
+/**
+ * Error domain GQuark. 
+ * See @ref purple_keyring_error_domain .
+ */
+#define ERR_PIDGINKEYRING 	purple_keyring_error_domain()
+/** stuff here too */
+GQuark purple_keyring_error_domain();
+
+/** error codes for keyrings. */
+enum PurpleKeyringError
+{
+	ERR_OK = 0,		/**< no error. */
+	ERR_NOPASSWD = 1,	/**< no stored password. */
+	ERR_NOACCOUNT,		/**< account not found. */
+	ERR_WRONGPASS,		/**< user submitted wrong password when prompted. */
+	ERR_WRONGFORMAT,	/**< data passed is not in suitable format. */
+	ERR_NOKEYRING,		/**< no keyring configured. */
+	ERR_NOCHANNEL,		/**< failed to communicate with the backend */
+	ERR_UNKNOWN		/**< unknown error */
+};
+
+/*}@*/
+#endif /* _PURPLE_KEYRING_H_ */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libpurple/keyring_info.txt	Sat Jul 19 17:45:27 2008 +0000
@@ -0,0 +1,103 @@
+
+API
+What needs to be done :
+	Store a password with/without master password
+	Retrieve a password with/without master password
+	Forget/delete a password
+	Know if a master password can/must be used
+	Update a password
+	change master password
+	close the keyring (open is of course automatic)
+	free/clean memory allocated for a password
+
+Several solutions exist, but the one most likely to be used is the following :
+ - A NULL master password is passed if no master password is required.
+ - A NULL password is passed to remove the account from the keyring
+ - The first idea for the fift problem is for libpurple to update the master password, libpurple reads the password, erases it, and stores it back. But considering some keyrings might have a keyring-wide master password, i might be better to have one function to do it all. So this will have it's own function
+ - to update a password, a new password is simply written
+ - close is a simple function call
+ - now, for the delicate part. We need to know what the capabilities for our plugin are. There are several ways for that too, but the one I prefer is the following : a bitmask is stored allong with the structure that contains all the info on the plugin. Using a function, or even worse, return error codes for such a simple task would be pretty ugly imho.
+
+So, now we know what libpurple needs to know about each plugin :
+ - read a password
+ - store a password
+ - change master password
+ - capabilities bitmask
+ - close the safe
+ - free a password's memory	( no need to make that async )
+
+Also, we want to keep track of what plugin is currently being used, and how.
+In the preferences, I'll add information on what plugin is being used, as well as weather it's currently using a master password.
+This is because some plugins might not know wether a master password is being used or not. (I'm thinking simple crypto stuff that might decrypt as junk if used with the wrong key.)
+
+So, now we know what we need to register a plugin, let's write some defs.
+
+typedef void (*PurpleKeyringReadCallback)(const PurpleAccount * account, int result, gchar * password, gpointer data);
+typedef void (*PurpleKeyringSaveCallback)(const PurpleAccount * account, int result, gpointer data);
+typedef void (*PurpleKeyringCloseCallback)(int result, gpointer data);	/* async just so we can check for errors */
+typedef void (*PurpleKeyringChangeMasterCallback)(int result, gpointer data);
+
+typedef void (*PurpleKeyringRead)(const PurpleAccount * account, gchar * masterpassword, PurpleKeyringReadCallback cb, gpointer data);
+typedef void (*PurpleKeyringSave)(const PurpleAccount * account, gchar * masterpassword, PurpleKeyringSaveCallback cb, gpointer data);
+typedef void (*PurpleKeyringClose)(gpointer data);
+typedef void (*PurpleKeyringChangeMaster)(gchar * oldpassword, gchar * newpassword, PurpleKeyringChangeMasterCallback cb, gpointer data);
+typedef void (*PurpleKeyringFree)(gchar * password);
+
+Now, we can write the structure that will contain all this info
+
+typedef struct _PurpleKeyringInfo {
+	char * name;
+	PurpleKeyringRead read_password;
+	PurpleKeyringSave save_password;
+	PurpleKeyringClose close_keyring;
+	PurpleKeyringFree free_password;
+	PurpleKeyringChangeMaster change_master;
+	int capabilities;	
+	gpointer r1;	/* RESERVED */
+	gpointer r2;	/* RESERVED */
+	gpointer r3;	/* RESERVED */
+} PurpleKeyringInfo;
+
+void purple_plugin_keyring_register(PurpleKeyringInfo * info) /* do we need a callback ? */
+
+
+Ok, so there's something really cool, all accesses to the passwords are done through this function.
+
+const char *
+purple_account_get_password(const PurpleAccount *account)
+{
+	g_return_val_if_fail(account != NULL, NULL);
+
+	return account->password;
+}
+
+
+
+
+
+
+
+
+
+
+
+
+stuff to do
+	
+
+
+	/* Read the password */
+	child = xmlnode_get_child(node, "password");
+	if ((child != NULL) && ((data = xmlnode_get_data(child)) != NULL))
+	{
+		purple_account_set_remember_password(ret, TRUE);
+		purple_account_set_password(ret, data);
+		g_free(data);
+	}
+
+
+
+questions :
+
+ - includes (order and stuff)
+ - trannie's mtn is down ?
--- a/libpurple/plugin.h	Sat Jul 19 03:10:10 2008 +0000
+++ b/libpurple/plugin.h	Sat Jul 19 17:45:27 2008 +0000
@@ -62,6 +62,7 @@
 #define PURPLE_PRIORITY_LOWEST  -9999
 
 #define PURPLE_PLUGIN_FLAG_INVISIBLE 0x01
+#define PURPLE_PLUGIN_FLAG_AUTOLOAD  0x02
 
 #define PURPLE_PLUGIN_MAGIC 5 /* once we hit 6.0.0 I think we can remove this */
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libpurple/plugins/keyrings/internalkeyring.c	Sat Jul 19 17:45:27 2008 +0000
@@ -0,0 +1,390 @@
+/* TODO
+ - fix error reporting
+ - use hashtable instead of Glib
+ - plugin interface
+ - move keyring info struct upwards
+*/
+
+#include <glib.h>
+#include <string.h>
+#include "version.h"
+#include "keyring.h"
+#include "account.h"
+
+/******************************/
+/** Macros and constants      */
+/******************************/
+
+#define KEYRINGNAME			"internalkeyring"
+#define INTERNALKEYRING_VERSION		"0.2a"
+#define INTERNALKEYRING_ID		"core-scrouaf-internalkeyring"
+#define INTERNALKEYRING_AUTHOR		"Vivien Bernet-Rollande <vbernetr@etu.utc.fr>"
+#define INTERNALKEYRING_DESCRIPTION	\
+	"This keyring plugin offers a password storage backend compatible with the former storage system."
+
+
+/******************************/
+/** Data Structures           */
+/******************************/
+
+typedef struct _InternalKeyring_PasswordInfo InternalKeyring_PasswordInfo;
+
+struct _InternalKeyring_PasswordInfo {
+	const PurpleAccount * account;
+	gchar * password;
+};
+
+/******************************/
+/** Globals                   */
+/******************************/
+
+GList * InternalKeyring_passwordlist = NULL;		/* use hashtable ? */
+
+
+/******************************/
+/** Internal functions        */
+/******************************/
+
+/**
+ * retrieve the InternalKeyring_PasswordInfo structure for an account
+ * TODO : rewrite this to use hashtables rather than GList
+ */
+static InternalKeyring_PasswordInfo * 
+InternalKeyring_get_account_info(const PurpleAccount * account)
+{
+	GList * p;
+	InternalKeyring_PasswordInfo * i;
+
+	for (p = InternalKeyring_passwordlist; p != NULL; p = p->next)  {
+		i = (InternalKeyring_PasswordInfo*)(p->data);
+		if (i->account == account)
+			return i;
+	}
+	return NULL;
+}
+
+/**
+ * Free or create an InternalKeyring_PasswordInfo structure and all pointed data.
+ * XXX /!\ Update this when adding fields to InternalKeyring_PasswordInfo
+ * XXX : rewrite this to use hashtables rather than GList
+ *        (fix InternalKeyring_Close() as well)
+ */
+static void
+InternalKeyring_add_passwordinfo(InternalKeyring_PasswordInfo * info)
+{
+	InternalKeyring_passwordlist = g_list_prepend(InternalKeyring_passwordlist, info);
+	return;
+}
+
+static void
+InternalKeyring_free_passwordinfo(InternalKeyring_PasswordInfo * info)
+{
+	g_free(info->password);
+	InternalKeyring_passwordlist = g_list_remove(InternalKeyring_passwordlist, info);
+	g_free(info);
+	return;
+}
+
+/**
+ * wrapper so we can use it in close
+ * TODO : find a more elegant way
+ */
+static void
+InternalKeyring_free_passwordinfo_from_g_list(gpointer info, gpointer data)
+{
+	InternalKeyring_free_passwordinfo((InternalKeyring_PasswordInfo*)info);
+	return;
+}
+
+
+gboolean
+InternalKeyring_is_valid_cleartext(const PurpleKeyringPasswordNode * node)
+{
+	const char * enc;
+	const char * mode;
+	const char * data;
+	const PurpleAccount * account;	
+
+	enc = purple_keyring_password_node_get_encryption(node);
+	mode = purple_keyring_password_node_get_mode(node);
+	data = purple_keyring_password_node_get_data(node);
+	account = purple_keyring_password_node_get_account(node);
+
+	if ((enc == NULL || strcmp(enc, KEYRINGNAME) == 0)&&
+	    (mode == NULL || strcmp(mode, "cleartext") == 0)&&
+	    data != NULL &&
+	    account != NULL) {
+
+		return TRUE;
+
+	} else {
+
+		return FALSE;
+
+	}
+}
+
+/******************************/
+/** Keyring interface         */
+/******************************/
+
+/**
+ * returns the password if the password is known.
+ */
+void 
+InternalKeyring_read(const PurpleAccount * account,
+		     GError ** error, 
+		     PurpleKeyringReadCallback cb,
+		     gpointer data)
+{
+	InternalKeyring_PasswordInfo * info;
+	char * ret;
+	
+	info = InternalKeyring_get_account_info(account);
+
+	if ( info == NULL ) {				/* no info on account */
+
+		g_set_error(error, ERR_PIDGINKEYRING, ERR_NOACCOUNT, 
+			"No info for account.");
+		cb(account, NULL, error, data);
+		return;
+
+	} else if (info->password == NULL) {		/* unknown password */
+
+		g_set_error(error, ERR_PIDGINKEYRING, ERR_NOPASSWD, 
+			"No Password for this account.");
+		cb(account, NULL, error, data);
+		return;
+
+	} else {
+
+		ret = info->password;
+		cb(account, ret, error, data);
+	}
+}
+
+/*
+ * save a new password
+ */
+void
+InternalKeyring_save(const PurpleAccount * account, 
+		     gchar * password,
+		     GError ** error,
+		     PurpleKeyringSaveCallback cb,
+		     gpointer data)
+{
+	InternalKeyring_PasswordInfo * info;
+
+	info = InternalKeyring_get_account_info(account);
+
+	if (password == NULL) {
+		/* forget password */
+		if (info == NULL) {
+			g_set_error(error, ERR_PIDGINKEYRING, ERR_NOPASSWD, 
+				"No Password for this account.");
+			cb(account, error, data);
+			return;
+		}
+
+		InternalKeyring_free_passwordinfo(info);
+
+		if (cb != NULL)
+			cb(account, error, data);
+		return;
+
+	} else {	/* password != NULL */
+
+		if ( info == NULL ) {
+			info = g_malloc0(sizeof (InternalKeyring_PasswordInfo));
+			InternalKeyring_add_passwordinfo(info);
+		}
+
+		/* if we already had a password, forget about it */
+		if ( info->password != NULL )	
+			g_free(info->password);
+
+		info->password = g_malloc(strlen( password + 1 ));
+		strcpy(info->password, password);
+
+		if (cb != NULL)
+			cb(account, error, data);
+		return;
+	}
+}
+
+/*
+ * clears and frees all PasswordInfo structures.
+ * TODO : rewrite using Hashtable
+ */
+void 
+InternalKeyring_close(GError ** error)
+{
+	g_list_foreach(InternalKeyring_passwordlist,
+		InternalKeyring_free_passwordinfo_from_g_list, NULL);
+	return;
+}
+
+/*
+ * does nothing since we don't want to free the stored info
+ */
+void 
+InternalKeyring_free(gchar * password,
+		     GError ** error)
+{
+	return;		/* nothing to free or cleanup until we forget the password */
+}
+
+/**
+ * Imports password info from a PurpleKeyringPasswordNode structure
+ * (called for each account when accounts.xml is parsed)
+ * returns TRUE if sucessful, FALSE otherwise.
+ * TODO : add error reporting 
+ *	  use accessors for PurpleKeyringPasswordNode (FIXME)
+ *	  FIXME : REWRITE AS ASYNC
+ */
+void
+InternalKeyring_import_password(const PurpleKeyringPasswordNode * nodeinfo,
+				GError ** error,
+				PurpleKeyringImportCallback cb,
+				gpointer cbdata)
+{
+	InternalKeyring_PasswordInfo * pwinfo;
+	const char * data;
+
+	if (InternalKeyring_is_valid_cleartext(nodeinfo)) {
+
+		pwinfo = g_malloc0(sizeof(InternalKeyring_PasswordInfo));
+		InternalKeyring_add_passwordinfo(pwinfo);
+
+		data = purple_keyring_password_node_get_data(nodeinfo);		
+		pwinfo->password = g_malloc(strlen(data) + 1);
+		strcpy(pwinfo->password, data);
+
+		pwinfo->account = purple_keyring_password_node_get_account(nodeinfo);
+
+		//return TRUE;
+
+	} else {
+		/* invalid input */
+		//return FALSE;
+	}		
+}
+
+
+/**
+ * Exports password info to a PurpleKeyringPasswordNode structure
+ * (called for each account when accounts are synced)
+ * TODO : add proper error reporting 
+ */
+void
+InternalKeyring_export_password(const PurpleAccount * account,
+				GError ** error,
+				PurpleKeyringExportCallback cb,
+				gpointer data)
+{
+	PurpleKeyringPasswordNode * nodeinfo;
+	InternalKeyring_PasswordInfo * pwinfo;
+
+	nodeinfo = purple_keyring_password_node_new();
+	pwinfo = InternalKeyring_get_account_info(account);
+
+	if (pwinfo->password == NULL) {
+
+		// FIXME : error
+		cb(NULL, error, data);
+		return;
+
+	} else {
+
+		purple_keyring_password_node_set_encryption(nodeinfo, KEYRINGNAME);
+		purple_keyring_password_node_set_mode(nodeinfo, "cleartext");
+		purple_keyring_password_node_set_data(nodeinfo, pwinfo->password);
+
+		cb(nodeinfo, error, data);
+		return;
+	}
+}
+
+
+
+/******************************/
+/** Keyring plugin stuff      */
+/******************************/
+
+PurpleKeyring InternalKeyring_KeyringInfo =
+{
+	"internalkeyring",
+	InternalKeyring_read,
+	InternalKeyring_save,
+	InternalKeyring_close,
+	InternalKeyring_free,
+	NULL,				/* change_master */
+	InternalKeyring_import_password,
+	InternalKeyring_export_password,
+	NULL,				/* RESERVED */
+	NULL,				/* RESERVED */
+	NULL				/* RESERVED */
+};
+
+
+/******************************/
+/** Plugin interface          */
+/******************************/
+
+gboolean 
+InternalKeyring_load(PurplePlugin *plugin)
+{
+	purple_plugin_keyring_register(&InternalKeyring_KeyringInfo);	/* FIXME : structure should be hidden */
+	return TRUE;
+}
+
+/**
+ * TODO : handle error, maybe return FALSE on problem
+ * (no reason for it to fail unless data is corrupted though)
+ */
+gboolean 
+InternalKeyring_unload(PurplePlugin *plugin)
+{
+	InternalKeyring_close(NULL);
+	return TRUE;
+}
+
+void 
+InternalKeyring_destroy(PurplePlugin *plugin)
+{
+	InternalKeyring_close(NULL);
+	return;
+}
+
+
+PurplePluginInfo plugininfo =
+{
+	PURPLE_PLUGIN_MAGIC,						/* magic */
+	PURPLE_MAJOR_VERSION,						/* major_version */
+	PURPLE_MINOR_VERSION,						/* minor_version */
+	PURPLE_PLUGIN_STANDARD,						/* type */
+	NULL,								/* ui_requirement */
+	PURPLE_PLUGIN_FLAG_INVISIBLE|PURPLE_PLUGIN_FLAG_AUTOLOAD,	/* flags */
+	NULL,								/* dependencies */
+	PURPLE_PRIORITY_DEFAULT,					/* priority */
+	INTERNALKEYRING_ID,						/* id */
+	"internal-keyring-plugin",					/* name */
+	INTERNALKEYRING_VERSION,					/* version */
+	"Internal Keyring Plugin",					/* summary */
+	INTERNALKEYRING_DESCRIPTION,					/* description */
+	INTERNALKEYRING_AUTHOR,						/* author */
+	"N/A",								/* homepage */
+	InternalKeyring_load,						/* load */
+	InternalKeyring_unload,						/* unload */
+	InternalKeyring_destroy,					/* destroy */
+	NULL,								/* ui_info */
+	NULL,								/* extra_info */
+	NULL,								/* prefs_info */
+	NULL,								/* actions */
+	NULL,								/* padding... */
+	NULL,
+	NULL,
+	NULL,
+};
+
+
--- a/libpurple/xmlnode.h	Sat Jul 19 03:10:10 2008 +0000
+++ b/libpurple/xmlnode.h	Sat Jul 19 17:45:27 2008 +0000
@@ -160,7 +160,7 @@
  *
  * @param node   The node to set an attribute for.
  * @param attr   The name of the attribute to set
- * @param prefix The prefix of the attribute to ste
+ * @param prefix The prefix of the attribute to set
  * @param value  The value of the attribute
  */
 void xmlnode_set_attrib_with_prefix(xmlnode *node, const char *attr, const char *prefix, const char *value);

mercurial