583 lines
12 KiB
C
583 lines
12 KiB
C
|
/*
|
||
|
*
|
||
|
* OBEX Server
|
||
|
*
|
||
|
* Copyright (C) 2009-2010 Intel Corporation
|
||
|
* Copyright (C) 2007-2010 Marcel Holtmann <marcel@holtmann.org>
|
||
|
*
|
||
|
*
|
||
|
* 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 St, Fifth Floor, Boston, MA 02110-1301 USA
|
||
|
*
|
||
|
*/
|
||
|
|
||
|
#ifdef HAVE_CONFIG_H
|
||
|
#include <config.h>
|
||
|
#endif
|
||
|
|
||
|
#include <dirent.h>
|
||
|
#include <errno.h>
|
||
|
#include <stdio.h>
|
||
|
#include <stdint.h>
|
||
|
#include <string.h>
|
||
|
#include <glib.h>
|
||
|
#include <stdlib.h>
|
||
|
#include <sys/types.h>
|
||
|
#include <sys/stat.h>
|
||
|
#include <fcntl.h>
|
||
|
#include <unistd.h>
|
||
|
#include <libical/ical.h>
|
||
|
#include <libical/vobject.h>
|
||
|
#include <libical/vcc.h>
|
||
|
|
||
|
#include "log.h"
|
||
|
#include "phonebook.h"
|
||
|
|
||
|
typedef void (*vcard_func_t) (const char *file, VObject *vo, void *user_data);
|
||
|
|
||
|
struct dummy_data {
|
||
|
phonebook_cb cb;
|
||
|
void *user_data;
|
||
|
const struct apparam_field *apparams;
|
||
|
char *folder;
|
||
|
int fd;
|
||
|
guint id;
|
||
|
};
|
||
|
|
||
|
struct cache_query {
|
||
|
phonebook_entry_cb entry_cb;
|
||
|
phonebook_cache_ready_cb ready_cb;
|
||
|
void *user_data;
|
||
|
DIR *dp;
|
||
|
};
|
||
|
|
||
|
static char *root_folder = NULL;
|
||
|
|
||
|
static void dummy_free(void *user_data)
|
||
|
{
|
||
|
struct dummy_data *dummy = user_data;
|
||
|
|
||
|
if (dummy->fd >= 0)
|
||
|
close(dummy->fd);
|
||
|
|
||
|
g_free(dummy->folder);
|
||
|
g_free(dummy);
|
||
|
}
|
||
|
|
||
|
static void query_free(void *user_data)
|
||
|
{
|
||
|
struct cache_query *query = user_data;
|
||
|
|
||
|
if (query->dp)
|
||
|
closedir(query->dp);
|
||
|
|
||
|
g_free(query);
|
||
|
}
|
||
|
|
||
|
int phonebook_init(void)
|
||
|
{
|
||
|
if (root_folder)
|
||
|
return 0;
|
||
|
|
||
|
/* FIXME: It should NOT be hard-coded */
|
||
|
root_folder = g_build_filename(getenv("HOME"), "phonebook", NULL);
|
||
|
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
void phonebook_exit(void)
|
||
|
{
|
||
|
g_free(root_folder);
|
||
|
root_folder = NULL;
|
||
|
}
|
||
|
|
||
|
static int handle_cmp(gconstpointer a, gconstpointer b)
|
||
|
{
|
||
|
const char *f1 = a;
|
||
|
const char *f2 = b;
|
||
|
unsigned int i1, i2;
|
||
|
|
||
|
if (sscanf(f1, "%u.vcf", &i1) != 1)
|
||
|
return -1;
|
||
|
|
||
|
if (sscanf(f2, "%u.vcf", &i2) != 1)
|
||
|
return -1;
|
||
|
|
||
|
return (i1 - i2);
|
||
|
}
|
||
|
|
||
|
static int foreach_vcard(DIR *dp, vcard_func_t func, uint16_t offset,
|
||
|
uint16_t maxlistcount, void *user_data, uint16_t *count)
|
||
|
{
|
||
|
struct dirent *ep;
|
||
|
GSList *sorted = NULL, *l;
|
||
|
VObject *v;
|
||
|
FILE *fp;
|
||
|
int err, fd, folderfd;
|
||
|
uint16_t n = 0;
|
||
|
|
||
|
folderfd = dirfd(dp);
|
||
|
if (folderfd < 0) {
|
||
|
err = errno;
|
||
|
error("dirfd(): %s(%d)", strerror(err), err);
|
||
|
return -err;
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* Sorting vcards by file name. versionsort is a GNU extension.
|
||
|
* The simple sorting function implemented on handle_cmp address
|
||
|
* vcards handle only(handle is always a number). This sort function
|
||
|
* doesn't address filename started by "0".
|
||
|
*/
|
||
|
while ((ep = readdir(dp))) {
|
||
|
char *filename;
|
||
|
|
||
|
if (ep->d_name[0] == '.')
|
||
|
continue;
|
||
|
|
||
|
filename = g_filename_to_utf8(ep->d_name, -1, NULL, NULL, NULL);
|
||
|
if (filename == NULL) {
|
||
|
error("g_filename_to_utf8: invalid filename");
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
if (!g_str_has_suffix(filename, ".vcf")) {
|
||
|
g_free(filename);
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
sorted = g_slist_insert_sorted(sorted, filename, handle_cmp);
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* Filtering only the requested vCards attributes. Offset
|
||
|
* shall be based on the first entry of the phonebook.
|
||
|
*/
|
||
|
for (l = g_slist_nth(sorted, offset);
|
||
|
l && n < maxlistcount; l = l->next) {
|
||
|
const char *filename = l->data;
|
||
|
|
||
|
fd = openat(folderfd, filename, O_RDONLY);
|
||
|
if (fd < 0) {
|
||
|
err = errno;
|
||
|
error("openat(%s): %s(%d)", filename, strerror(err), err);
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
fp = fdopen(fd, "r");
|
||
|
v = Parse_MIME_FromFile(fp);
|
||
|
if (v != NULL) {
|
||
|
func(filename, v, user_data);
|
||
|
deleteVObject(v);
|
||
|
n++;
|
||
|
}
|
||
|
|
||
|
close(fd);
|
||
|
}
|
||
|
|
||
|
g_slist_free_full(sorted, g_free);
|
||
|
|
||
|
if (count)
|
||
|
*count = n;
|
||
|
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
static void entry_concat(const char *filename, VObject *v, void *user_data)
|
||
|
{
|
||
|
GString *buffer = user_data;
|
||
|
char tmp[1024];
|
||
|
int len;
|
||
|
|
||
|
/*
|
||
|
* VObject API uses len for IN and OUT
|
||
|
* Written bytes is also returned in the len variable
|
||
|
*/
|
||
|
len = sizeof(tmp);
|
||
|
memset(tmp, 0, len);
|
||
|
|
||
|
writeMemVObject(tmp, &len, v);
|
||
|
|
||
|
/* FIXME: only the requested fields must be added */
|
||
|
g_string_append_len(buffer, tmp, len);
|
||
|
}
|
||
|
|
||
|
static gboolean read_dir(void *user_data)
|
||
|
{
|
||
|
struct dummy_data *dummy = user_data;
|
||
|
GString *buffer;
|
||
|
DIR *dp;
|
||
|
uint16_t count = 0, max, offset;
|
||
|
|
||
|
buffer = g_string_new("");
|
||
|
|
||
|
dp = opendir(dummy->folder);
|
||
|
if (dp == NULL) {
|
||
|
int err = errno;
|
||
|
DBG("opendir(): %s(%d)", strerror(err), err);
|
||
|
goto done;
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* For PullPhoneBook function, the decision of returning the size
|
||
|
* or contacts is made in the PBAP core. When MaxListCount is ZERO,
|
||
|
* PCE wants to know the size of a given folder, PSE shall ignore all
|
||
|
* other applicattion parameters that may be present in the request.
|
||
|
*/
|
||
|
if (dummy->apparams->maxlistcount == 0) {
|
||
|
max = 0xffff;
|
||
|
offset = 0;
|
||
|
} else {
|
||
|
max = dummy->apparams->maxlistcount;
|
||
|
offset = dummy->apparams->liststartoffset;
|
||
|
}
|
||
|
|
||
|
foreach_vcard(dp, entry_concat, offset, max, buffer, &count);
|
||
|
|
||
|
closedir(dp);
|
||
|
done:
|
||
|
/* FIXME: Missing vCards fields filtering */
|
||
|
dummy->cb(buffer->str, buffer->len, count, 0, TRUE, dummy->user_data);
|
||
|
|
||
|
g_string_free(buffer, TRUE);
|
||
|
|
||
|
return FALSE;
|
||
|
}
|
||
|
|
||
|
static void entry_notify(const char *filename, VObject *v, void *user_data)
|
||
|
{
|
||
|
struct cache_query *query = user_data;
|
||
|
VObject *property, *subproperty;
|
||
|
GString *name;
|
||
|
const char *tel;
|
||
|
long unsigned int handle;
|
||
|
|
||
|
property = isAPropertyOf(v, VCNameProp);
|
||
|
if (!property)
|
||
|
return;
|
||
|
|
||
|
if (sscanf(filename, "%lu.vcf", &handle) != 1)
|
||
|
return;
|
||
|
|
||
|
if (handle > UINT32_MAX)
|
||
|
return;
|
||
|
|
||
|
/* LastName; FirstName; MiddleName; Prefix; Suffix */
|
||
|
|
||
|
name = g_string_new("");
|
||
|
subproperty = isAPropertyOf(property, VCFamilyNameProp);
|
||
|
if (subproperty) {
|
||
|
g_string_append(name,
|
||
|
fakeCString(vObjectUStringZValue(subproperty)));
|
||
|
}
|
||
|
|
||
|
subproperty = isAPropertyOf(property, VCGivenNameProp);
|
||
|
if (subproperty)
|
||
|
g_string_append_printf(name, ";%s",
|
||
|
fakeCString(vObjectUStringZValue(subproperty)));
|
||
|
|
||
|
subproperty = isAPropertyOf(property, VCAdditionalNamesProp);
|
||
|
if (subproperty)
|
||
|
g_string_append_printf(name, ";%s",
|
||
|
fakeCString(vObjectUStringZValue(subproperty)));
|
||
|
|
||
|
subproperty = isAPropertyOf(property, VCNamePrefixesProp);
|
||
|
if (subproperty)
|
||
|
g_string_append_printf(name, ";%s",
|
||
|
fakeCString(vObjectUStringZValue(subproperty)));
|
||
|
|
||
|
|
||
|
subproperty = isAPropertyOf(property, VCNameSuffixesProp);
|
||
|
if (subproperty)
|
||
|
g_string_append_printf(name, ";%s",
|
||
|
fakeCString(vObjectUStringZValue(subproperty)));
|
||
|
|
||
|
property = isAPropertyOf(v, VCTelephoneProp);
|
||
|
|
||
|
tel = property ? fakeCString(vObjectUStringZValue(property)) : NULL;
|
||
|
|
||
|
query->entry_cb(filename, handle, name->str, NULL, tel,
|
||
|
query->user_data);
|
||
|
g_string_free(name, TRUE);
|
||
|
}
|
||
|
|
||
|
static gboolean create_cache(void *user_data)
|
||
|
{
|
||
|
struct cache_query *query = user_data;
|
||
|
|
||
|
/*
|
||
|
* MaxListCount and ListStartOffset shall not be used
|
||
|
* when creating the cache. All entries shall be fetched.
|
||
|
* PBAP core is responsible for consider these application
|
||
|
* parameters before reply the entries.
|
||
|
*/
|
||
|
foreach_vcard(query->dp, entry_notify, 0, 0xffff, query, NULL);
|
||
|
|
||
|
query->ready_cb(query->user_data);
|
||
|
|
||
|
return FALSE;
|
||
|
}
|
||
|
|
||
|
static gboolean read_entry(void *user_data)
|
||
|
{
|
||
|
struct dummy_data *dummy = user_data;
|
||
|
char buffer[1024];
|
||
|
ssize_t count;
|
||
|
|
||
|
memset(buffer, 0, sizeof(buffer));
|
||
|
count = read(dummy->fd, buffer, sizeof(buffer));
|
||
|
|
||
|
if (count < 0) {
|
||
|
int err = errno;
|
||
|
error("read(): %s(%d)", strerror(err), err);
|
||
|
count = 0;
|
||
|
}
|
||
|
|
||
|
/* FIXME: Missing vCards fields filtering */
|
||
|
|
||
|
dummy->cb(buffer, count, 1, 0, TRUE, dummy->user_data);
|
||
|
|
||
|
return FALSE;
|
||
|
}
|
||
|
|
||
|
static gboolean is_dir(const char *dir)
|
||
|
{
|
||
|
struct stat st;
|
||
|
|
||
|
if (stat(dir, &st) < 0) {
|
||
|
int err = errno;
|
||
|
error("stat(%s): %s (%d)", dir, strerror(err), err);
|
||
|
return FALSE;
|
||
|
}
|
||
|
|
||
|
return S_ISDIR(st.st_mode);
|
||
|
}
|
||
|
|
||
|
char *phonebook_set_folder(const char *current_folder,
|
||
|
const char *new_folder, uint8_t flags, int *err)
|
||
|
{
|
||
|
gboolean root, child;
|
||
|
char *tmp1, *tmp2, *base, *absolute, *relative = NULL;
|
||
|
int len, ret = 0;
|
||
|
|
||
|
root = (g_strcmp0("/", current_folder) == 0);
|
||
|
child = (new_folder && strlen(new_folder) != 0);
|
||
|
|
||
|
switch (flags) {
|
||
|
case 0x02:
|
||
|
/* Go back to root */
|
||
|
if (!child) {
|
||
|
relative = g_strdup("/");
|
||
|
goto done;
|
||
|
}
|
||
|
|
||
|
relative = g_build_filename(current_folder, new_folder, NULL);
|
||
|
break;
|
||
|
case 0x03:
|
||
|
/* Go up 1 level */
|
||
|
if (root) {
|
||
|
/* Already root */
|
||
|
ret = -EBADR;
|
||
|
goto done;
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* Removing one level of the current folder. Current folder
|
||
|
* contains AT LEAST one level since it is not at root folder.
|
||
|
* Use glib utility functions to handle invalid chars in the
|
||
|
* folder path properly.
|
||
|
*/
|
||
|
tmp1 = g_path_get_basename(current_folder);
|
||
|
tmp2 = g_strrstr(current_folder, tmp1);
|
||
|
len = tmp2 - (current_folder + 1);
|
||
|
|
||
|
g_free(tmp1);
|
||
|
|
||
|
if (len == 0)
|
||
|
base = g_strdup("/");
|
||
|
else
|
||
|
base = g_strndup(current_folder, len);
|
||
|
|
||
|
/* Return: one level only */
|
||
|
if (!child) {
|
||
|
relative = base;
|
||
|
goto done;
|
||
|
}
|
||
|
|
||
|
relative = g_build_filename(base, new_folder, NULL);
|
||
|
g_free(base);
|
||
|
|
||
|
break;
|
||
|
default:
|
||
|
ret = -EBADR;
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
done:
|
||
|
if (!relative) {
|
||
|
if (err)
|
||
|
*err = ret;
|
||
|
|
||
|
return NULL;
|
||
|
}
|
||
|
|
||
|
absolute = g_build_filename(root_folder, relative, NULL);
|
||
|
if (!is_dir(absolute)) {
|
||
|
g_free(relative);
|
||
|
relative = NULL;
|
||
|
ret = -ENOENT;
|
||
|
}
|
||
|
|
||
|
g_free(absolute);
|
||
|
|
||
|
if (err)
|
||
|
*err = ret;
|
||
|
|
||
|
return relative;
|
||
|
}
|
||
|
|
||
|
void phonebook_req_finalize(void *request)
|
||
|
{
|
||
|
struct dummy_data *dummy = request;
|
||
|
|
||
|
/* dummy_data will be cleaned when request will be finished via
|
||
|
* g_source_remove */
|
||
|
if (dummy && dummy->id)
|
||
|
g_source_remove(dummy->id);
|
||
|
}
|
||
|
|
||
|
void *phonebook_pull(const char *name, const struct apparam_field *params,
|
||
|
phonebook_cb cb, void *user_data, int *err)
|
||
|
{
|
||
|
struct dummy_data *dummy;
|
||
|
char *filename, *folder;
|
||
|
|
||
|
/*
|
||
|
* Main phonebook objects will be created dinamically based on the
|
||
|
* folder content. All vcards inside the given folder will be appended
|
||
|
* in the "virtual" main phonebook object.
|
||
|
*/
|
||
|
|
||
|
filename = g_build_filename(root_folder, name, NULL);
|
||
|
|
||
|
if (!g_str_has_suffix(filename, ".vcf")) {
|
||
|
g_free(filename);
|
||
|
if (err)
|
||
|
*err = -EBADR;
|
||
|
return NULL;
|
||
|
}
|
||
|
|
||
|
folder = g_strndup(filename, strlen(filename) - 4);
|
||
|
g_free(filename);
|
||
|
if (!is_dir(folder)) {
|
||
|
g_free(folder);
|
||
|
if (err)
|
||
|
*err = -ENOENT;
|
||
|
return NULL;
|
||
|
}
|
||
|
|
||
|
dummy = g_new0(struct dummy_data, 1);
|
||
|
dummy->cb = cb;
|
||
|
dummy->user_data = user_data;
|
||
|
dummy->apparams = params;
|
||
|
dummy->folder = folder;
|
||
|
dummy->fd = -1;
|
||
|
|
||
|
if (err)
|
||
|
*err = 0;
|
||
|
|
||
|
return dummy;
|
||
|
}
|
||
|
|
||
|
int phonebook_pull_read(void *request)
|
||
|
{
|
||
|
struct dummy_data *dummy = request;
|
||
|
|
||
|
if (!dummy)
|
||
|
return -ENOENT;
|
||
|
|
||
|
dummy->id = g_idle_add_full(G_PRIORITY_DEFAULT_IDLE, read_dir, dummy,
|
||
|
dummy_free);
|
||
|
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
void *phonebook_get_entry(const char *folder, const char *id,
|
||
|
const struct apparam_field *params, phonebook_cb cb,
|
||
|
void *user_data, int *err)
|
||
|
{
|
||
|
struct dummy_data *dummy;
|
||
|
char *filename;
|
||
|
int fd;
|
||
|
guint ret;
|
||
|
|
||
|
filename = g_build_filename(root_folder, folder, id, NULL);
|
||
|
|
||
|
fd = open(filename, O_RDONLY);
|
||
|
if (fd < 0) {
|
||
|
DBG("open(): %s(%d)", strerror(errno), errno);
|
||
|
if (err)
|
||
|
*err = -ENOENT;
|
||
|
return NULL;
|
||
|
}
|
||
|
|
||
|
dummy = g_new0(struct dummy_data, 1);
|
||
|
dummy->cb = cb;
|
||
|
dummy->user_data = user_data;
|
||
|
dummy->apparams = params;
|
||
|
dummy->fd = fd;
|
||
|
|
||
|
ret = g_idle_add_full(G_PRIORITY_DEFAULT_IDLE, read_entry, dummy,
|
||
|
dummy_free);
|
||
|
|
||
|
if (err)
|
||
|
*err = 0;
|
||
|
|
||
|
return GINT_TO_POINTER(ret);
|
||
|
}
|
||
|
|
||
|
void *phonebook_create_cache(const char *name, phonebook_entry_cb entry_cb,
|
||
|
phonebook_cache_ready_cb ready_cb, void *user_data, int *err)
|
||
|
{
|
||
|
struct cache_query *query;
|
||
|
char *foldername;
|
||
|
DIR *dp;
|
||
|
guint ret;
|
||
|
|
||
|
foldername = g_build_filename(root_folder, name, NULL);
|
||
|
dp = opendir(foldername);
|
||
|
g_free(foldername);
|
||
|
|
||
|
if (dp == NULL) {
|
||
|
DBG("opendir(): %s(%d)", strerror(errno), errno);
|
||
|
if (err)
|
||
|
*err = -ENOENT;
|
||
|
return NULL;
|
||
|
}
|
||
|
|
||
|
query = g_new0(struct cache_query, 1);
|
||
|
query->entry_cb = entry_cb;
|
||
|
query->ready_cb = ready_cb;
|
||
|
query->user_data = user_data;
|
||
|
query->dp = dp;
|
||
|
|
||
|
ret = g_idle_add_full(G_PRIORITY_DEFAULT_IDLE, create_cache, query,
|
||
|
query_free);
|
||
|
|
||
|
if (err)
|
||
|
*err = 0;
|
||
|
|
||
|
return GINT_TO_POINTER(ret);
|
||
|
}
|