summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--config/main.inc.php.dist36
-rw-r--r--program/include/html.php7
-rw-r--r--program/include/main.inc41
-rw-r--r--program/include/rcmail.php143
-rw-r--r--program/include/rcube_addressbook.php117
-rw-r--r--program/include/rcube_browser.php1
-rw-r--r--program/include/rcube_contacts.php140
-rw-r--r--program/include/rcube_ldap.php126
-rw-r--r--program/include/rcube_plugin.php15
-rw-r--r--program/include/rcube_plugin_api.php118
-rw-r--r--program/include/rcube_shared.inc19
-rwxr-xr-xprogram/include/rcube_template.php5
-rw-r--r--program/include/rcube_vcard.php172
-rw-r--r--program/js/app.js332
-rw-r--r--program/localization/en_US/labels.inc39
-rw-r--r--program/localization/en_US/messages.inc3
-rw-r--r--program/steps/addressbook/copy.inc2
-rw-r--r--program/steps/addressbook/delete.inc4
-rw-r--r--program/steps/addressbook/edit.inc118
-rw-r--r--program/steps/addressbook/export.inc25
-rw-r--r--program/steps/addressbook/func.inc345
-rw-r--r--program/steps/addressbook/groups.inc3
-rw-r--r--program/steps/addressbook/import.inc9
-rw-r--r--program/steps/addressbook/list.inc2
-rw-r--r--program/steps/addressbook/mailto.inc6
-rw-r--r--program/steps/addressbook/save.inc162
-rw-r--r--program/steps/addressbook/search.inc8
-rw-r--r--program/steps/addressbook/show.inc133
-rw-r--r--program/steps/mail/autocomplete.inc20
-rw-r--r--program/steps/mail/compose.inc2
-rw-r--r--skins/default/addressbook.css163
-rw-r--r--skins/default/common.css20
-rw-r--r--skins/default/functions.js21
-rw-r--r--skins/default/iehacks.css6
-rw-r--r--skins/default/images/contactpic.pngbin0 -> 375 bytes
-rw-r--r--skins/default/mail.css14
-rw-r--r--skins/default/templates/contact.html9
-rw-r--r--skins/default/templates/contactadd.html22
-rw-r--r--skins/default/templates/contactedit.html22
39 files changed, 2010 insertions, 420 deletions
diff --git a/config/main.inc.php.dist b/config/main.inc.php.dist
index 85f3a0609..150b70d71 100644
--- a/config/main.inc.php.dist
+++ b/config/main.inc.php.dist
@@ -308,6 +308,15 @@ $rcmail_config['identities_level'] = 0;
// mime magic database
$rcmail_config['mime_magic'] = '/usr/share/misc/magic';
+// path to imagemagick identify binary
+$rcmail_config['im_identify_path'] = null;
+
+// path to imagemagick convert binary
+$rcmail_config['im_convert_path'] = null;
+
+// maximum size of uploaded contact photos in pixel
+$rcmail_config['contact_photo_size'] = 160;
+
// Enable DNS checking for e-mail address validation
$rcmail_config['email_dns_check'] = false;
@@ -346,6 +355,9 @@ $rcmail_config['date_long'] = 'd.m.Y H:i';
// use this format for today's date display (date or strftime format)
$rcmail_config['date_today'] = 'H:i';
+// use this format for date display without time (date or strftime format)
+$rcmail_config['date_format'] = 'Y-m-d';
+
// store draft message is this mailbox
// leave blank if draft messages should not be stored
$rcmail_config['drafts_mbox'] = 'Drafts';
@@ -458,7 +470,6 @@ $rcmail_config['ldap_public']['Verisign'] = array(
// The login name is used to search for the DN to bind with
'search_base_dn' => '',
'search_filter' => '', // e.g. '(&(objectClass=posixAccount)(uid=%u))'
-
'writable' => false, // Indicates if we can write to the LDAP directory or not.
// If writable is true then these fields need to be populated:
// LDAP_Object_Classes, required_fields, LDAP_rdn
@@ -467,10 +478,21 @@ $rcmail_config['ldap_public']['Verisign'] = array(
'LDAP_rdn' => 'mail', // The RDN field that is used for new entries, this field needs to be one of the search_fields, the base of base_dn is appended to the RDN to insert into the LDAP directory.
'ldap_version' => 3, // using LDAPv3
'search_fields' => array('mail', 'cn'), // fields to search in
- 'name_field' => 'cn', // this field represents the contact's name
- 'email_field' => 'mail', // this field represents the contact's e-mail
- 'surname_field' => 'sn', // this field represents the contact's last name
- 'firstname_field' => 'gn', // this field represents the contact's first name
+ 'fieldmap' => array( // mapping of contact fields to directory attributes
+ // Roundcube => LDAP
+ 'name' => 'cn',
+ 'surname' => 'sn',
+ 'firstname' => 'givenName',
+ 'email' => 'mail',
+ 'phone:home' => 'homePhone',
+ 'phone:work' => 'telephoneNumber',
+ 'phone:mobile' => 'mobile',
+ 'street' => 'street',
+ 'zipcode' => 'postalCode',
+ 'locality' => 'l',
+ 'country' => 'c',
+ 'organization' => 'o',
+ ),
'sort' => 'cn', // The field to sort the listing by.
'scope' => 'sub', // search mode: sub|base|list
'filter' => '', // used for basic listing (if not empty) and will be &'d with search queries. example: status=act
@@ -489,6 +511,10 @@ $rcmail_config['autocomplete_addressbooks'] = array('sql');
// may need to do lengthy results building given overly-broad searches
$rcmail_config['autocomplete_min_length'] = 1;
+// show address fields in this order
+// available placeholders: {street}, {locality}, {zipcode}, {country}, {region}
+$rcmail_config['address_template'] = '{street}<br/>{locality} {zipcode}<br/>{country} {region}';
+
// ----------------------------------
// USER PREFERENCES
// ----------------------------------
diff --git a/program/include/html.php b/program/include/html.php
index a7599cd9f..ef7314e6f 100644
--- a/program/include/html.php
+++ b/program/include/html.php
@@ -71,6 +71,9 @@ class html
*/
public static function tag($tagname, $attrib = array(), $content = null, $allowed_attrib = null)
{
+ if (is_string($attrib))
+ $attrib = array('class' => $attrib);
+
$inline_tags = array('a','span','img');
$suffix = $attrib['nl'] || ($content && $attrib['nl'] !== false && !in_array($tagname, $inline_tags)) ? "\n" : '';
@@ -147,7 +150,7 @@ class html
$attr = array('href' => $attr);
}
return self::tag('a', $attr, $cont, array_merge(self::$common_attrib,
- array('href','target','name','onclick','onmouseover','onmouseout','onmousedown','onmouseup')));
+ array('href','target','name','rel','onclick','onmouseover','onmouseout','onmousedown','onmouseup')));
}
/**
@@ -501,7 +504,7 @@ class html_select extends html
protected $tagname = 'select';
protected $options = array();
protected $allowed = array('name','size','tabindex','autocomplete',
- 'multiple','onchange','disabled');
+ 'multiple','onchange','disabled','rel');
/**
* Add a new option to this drop-down
diff --git a/program/include/main.inc b/program/include/main.inc
index 1ddb5f9c4..0815c259f 100644
--- a/program/include/main.inc
+++ b/program/include/main.inc
@@ -799,7 +799,7 @@ function rcube_table_output($attrib, $table_data, $a_show_cols, $id_col)
// format each col
foreach ($a_show_cols as $col)
- $table->add($col, Q($row_data[$col]));
+ $table->add($col, Q(is_array($row_data[$col]) ? $row_data[$col][0] : $row_data[$col]));
$c++;
}
@@ -819,32 +819,43 @@ function rcube_table_output($attrib, $table_data, $a_show_cols, $id_col)
* @return string HTML field definition
*/
function rcmail_get_edit_field($col, $value, $attrib, $type='text')
- {
+{
+ static $colcounts = array();
+
$fname = '_'.$col;
- $attrib['name'] = $fname;
+ $attrib['name'] = $fname . ($attrib['array'] ? '[]' : '');
+ $attrib['class'] = trim($attrib['class'] . ' ff_' . $col);
- if ($type=='checkbox')
- {
+ if ($type == 'checkbox') {
$attrib['value'] = '1';
$input = new html_checkbox($attrib);
- }
- else if ($type=='textarea')
- {
+ }
+ else if ($type == 'textarea') {
$attrib['cols'] = $attrib['size'];
$input = new html_textarea($attrib);
- }
- else
+ }
+ else if ($type == 'select') {
+ $input = new html_select($attrib);
+ $input->add('---', '');
+ $input->add(array_values($attrib['options']), array_keys($attrib['options']));
+ }
+ else {
+ if ($attrib['type'] != 'text' && $attrib['type'] != 'hidden')
+ $attrib['type'] = 'text';
$input = new html_inputfield($attrib);
+ }
// use value from post
- if (!empty($_POST[$fname]))
- $value = get_input_value($fname, RCUBE_INPUT_POST,
- $type == 'textarea' && strpos($attrib['class'], 'mce_editor')!==false ? true : false);
+ if (isset($_POST[$fname])) {
+ $postvalue = get_input_value($fname, RCUBE_INPUT_POST,
+ $type == 'textarea' && strpos($attrib['class'], 'mce_editor')!==false ? true : false);
+ $value = $attrib['array'] ? $postvalue[intval($colcounts[$col]++)] : $postvalue;
+ }
$out = $input->show($value);
-
+
return $out;
- }
+}
/**
diff --git a/program/include/rcmail.php b/program/include/rcmail.php
index cdf959f2e..56181a733 100644
--- a/program/include/rcmail.php
+++ b/program/include/rcmail.php
@@ -114,7 +114,7 @@ class rcmail
public $comm_path = './';
private $texts;
- private $books = array();
+ private $address_books = array();
private $action_map = array();
@@ -331,6 +331,10 @@ class rcmail
if ($plugin['instance'] instanceof rcube_addressbook) {
$contacts = $plugin['instance'];
}
+ // use existing instance
+ else if (isset($this->address_books[$id]) && is_a($this->address_books[$id], 'rcube_addressbook') && (!$writeable || !$this->address_books[$id]->readonly)) {
+ $contacts = $this->address_books[$id];
+ }
else if ($id && $ldap_config[$id]) {
$contacts = new rcube_ldap($ldap_config[$id], $this->config->get('ldap_debug'), $this->config->mail_domain($_SESSION['imap_host']));
}
@@ -351,8 +355,8 @@ class rcmail
}
// add to the 'books' array for shutdown function
- if (!in_array($contacts, $this->books))
- $this->books[] = $contacts;
+ if (!isset($this->address_books[$id]))
+ $this->address_books[$id] = $contacts;
return $contacts;
}
@@ -373,11 +377,12 @@ class rcmail
// We are using the DB address book
if ($abook_type != 'ldap') {
- $contacts = new rcube_contacts($this->db, null);
+ if (!isset($this->address_books['0']))
+ $this->address_books['0'] = new rcube_contacts($this->db, $this->user->ID);
$list['0'] = array(
- 'id' => 0,
+ 'id' => '0',
'name' => rcube_label('personaladrbook'),
- 'groups' => $contacts->groups,
+ 'groups' => $this->address_books['0']->groups,
'readonly' => false,
'autocomplete' => in_array('sql', $autocomplete)
);
@@ -398,14 +403,15 @@ class rcmail
$plugin = $this->plugins->exec_hook('addressbooks_list', array('sources' => $list));
$list = $plugin['sources'];
- if ($writeable && !empty($list)) {
- foreach ($list as $idx => $item) {
- if ($item['readonly']) {
+ foreach ($list as $idx => $item) {
+ // register source for shutdown function
+ if (!is_object($this->address_books[$item['id']]))
+ $this->address_books[$item['id']] = $item;
+ // remove from list if not writeable as requested
+ if ($writeable && $item['readonly'])
unset($list[$idx]);
- }
- }
}
-
+
return $list;
}
@@ -1078,9 +1084,12 @@ class rcmail
if (is_object($this->smtp))
$this->smtp->disconnect();
- foreach ($this->books as $book)
- if (is_object($book))
+ foreach ($this->address_books as $book) {
+ if (!is_object($book)) // maybe an address book instance wasn't fetched using get_address_book() yet
+ $book = $this->get_address_book($book['id']);
+ if (is_a($book, 'rcube_addressbook'))
$book->close();
+ }
// before closing the database connection, write session data
if ($_SERVER['REMOTE_ADDR'])
@@ -1307,6 +1316,112 @@ class rcmail
/**
+ * Use imagemagick or GD lib to read image properties
+ *
+ * @param string Absolute file path
+ * @return mixed Hash array with image props like type, width, height or False on error
+ */
+ public static function imageprops($filepath)
+ {
+ $rcmail = rcmail::get_instance();
+ if ($cmd = $rcmail->config->get('im_identify_path', false)) {
+ list(, $type, $size) = explode(' ', strtolower(rcmail::exec($cmd. ' 2>/dev/null {in}', array('in' => $filepath))));
+ if ($size)
+ list($width, $height) = explode('x', $size);
+ }
+ else if (function_exists('getimagesize')) {
+ $imsize = @getimagesize($filepath);
+ $width = $imsize[0];
+ $height = $imsize[1];
+ $type = preg_replace('!image/!', '', $imsize['mime']);
+ }
+
+ return $type ? array('type' => $type, 'width' => $width, 'height' => $height) : false;
+ }
+
+
+ /**
+ * Convert an image to a given size and type using imagemagick (ensures input is an image)
+ *
+ * @param $p['in'] Input filename (mandatory)
+ * @param $p['out'] Output filename (mandatory)
+ * @param $p['size'] Width x height of resulting image, e.g. "160x60"
+ * @param $p['type'] Output file type, e.g. "jpg"
+ * @param $p['-opts'] Custom command line options to ImageMagick convert
+ * @return Success of convert as true/false
+ */
+ public static function imageconvert($p)
+ {
+ $result = false;
+ $rcmail = rcmail::get_instance();
+ $convert = $rcmail->config->get('im_convert_path', false);
+ $identify = $rcmail->config->get('im_identify_path', false);
+
+ // imagemagick is required for this
+ if (!$convert)
+ return false;
+
+ if (!(($imagetype = @exif_imagetype($p['in'])) && ($type = image_type_to_extension($imagetype, false))))
+ list(, $type) = explode(' ', strtolower(rcmail::exec($identify . ' 2>/dev/null {in}', $p))); # for things like eps
+
+ $type = strtr($type, array("jpeg" => "jpg", "tiff" => "tif", "ps" => "eps", "ept" => "eps"));
+ $p += array('type' => $type, 'types' => "bmp,eps,gif,jp2,jpg,png,svg,tif", 'quality' => 75);
+ $p['-opts'] = array('-resize' => $p['size'].'>') + (array)$p['-opts'];
+
+ if (in_array($type, explode(',', $p['types']))) # Valid type?
+ $result = rcmail::exec($convert . ' 2>&1 -flatten -auto-orient -colorspace RGB -quality {quality} {-opts} {in} {type}:{out}', $p) === "";
+
+ return $result;
+ }
+
+
+ /**
+ * Construct shell command, execute it and return output as string.
+ * Keywords {keyword} are replaced with arguments
+ *
+ * @param $cmd Format string with {keywords} to be replaced
+ * @param $values (zero, one or more arrays can be passed)
+ * @return output of command. shell errors not detectable
+ */
+ public static function exec(/* $cmd, $values1 = array(), ... */)
+ {
+ $args = func_get_args();
+ $cmd = array_shift($args);
+ $values = $replacements = array();
+
+ // merge values into one array
+ foreach ($args as $arg)
+ $values += (array)$arg;
+
+ preg_match_all('/({(-?)([a-z]\w*)})/', $cmd, $matches, PREG_SET_ORDER);
+ foreach ($matches as $tags) {
+ list(, $tag, $option, $key) = $tags;
+ $parts = array();
+
+ if ($option) {
+ foreach ((array)$values["-$key"] as $key => $value) {
+ if ($value === true || $value === false || $value === null)
+ $parts[] = $value ? $key : "";
+ else foreach ((array)$value as $val)
+ $parts[] = "$key " . escapeshellarg($val);
+ }
+ }
+ else {
+ foreach ((array)$values[$key] as $value)
+ $parts[] = escapeshellarg($value);
+ }
+
+ $replacements[$tag] = join(" ", $parts);
+ }
+
+ // use strtr behaviour of going through source string once
+ $cmd = strtr($cmd, $replacements);
+
+ return (string)shell_exec($cmd);
+ }
+
+
+ /**
* Helper method to set a cookie with the current path and host settings
*
* @param string Cookie name
diff --git a/program/include/rcube_addressbook.php b/program/include/rcube_addressbook.php
index ae2a286c3..b9a31cc74 100644
--- a/program/include/rcube_addressbook.php
+++ b/program/include/rcube_addressbook.php
@@ -5,7 +5,7 @@
| program/include/rcube_addressbook.php |
| |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2006-2009, The Roundcube Dev Team |
+ | Copyright (C) 2006-2011, The Roundcube Dev Team |
| Licensed under the GNU GPL |
| |
| PURPOSE: |
@@ -27,13 +27,22 @@
*/
abstract class rcube_addressbook
{
- /** public properties */
- var $primary_key;
- var $groups = false;
- var $readonly = true;
- var $ready = false;
- var $list_page = 1;
- var $page_size = 10;
+ /** constants for error reporting **/
+ const ERROR_READ_ONLY = 1;
+ const ERROR_NO_CONNECTION = 2;
+ const ERROR_INCOMPLETE = 3;
+ const ERROR_SAVING = 4;
+
+ /** public properties (mandatory) */
+ public $primary_key;
+ public $groups = false;
+ public $readonly = true;
+ public $ready = false;
+ public $list_page = 1;
+ public $page_size = 10;
+ public $coltypes = array('name' => array('limit'=>1), 'firstname' => array('limit'=>1), 'surname' => array('limit'=>1), 'email' => array('limit'=>1));
+
+ protected $error;
/**
* Save a search string for future listings
@@ -55,6 +64,16 @@ abstract class rcube_addressbook
abstract function reset();
/**
+ * Refresh saved search set after data has changed
+ *
+ * @return mixed New search set
+ */
+ function refresh_search()
+ {
+ return $this->get_search_set();
+ }
+
+ /**
* List the current set of contact records
*
* @param array List of cols to show
@@ -69,9 +88,11 @@ abstract class rcube_addressbook
* @param array List of fields to search in
* @param string Search value
* @param boolean True if results are requested, False if count only
- * @return Indexed list of contact records and 'count' value
+ * @param boolean True to skip the count query (select only)
+ * @param array List of fields that cannot be empty
+ * @return object rcube_result_set List of contact records and 'count' value
*/
- abstract function search($fields, $value, $strict=false, $select=true);
+ abstract function search($fields, $value, $strict=false, $select=true, $nocount=false, $required=array());
/**
* Count number of available contacts in database
@@ -98,6 +119,27 @@ abstract class rcube_addressbook
abstract function get_record($id, $assoc=false);
/**
+ * Returns the last error occured (e.g. when updating/inserting failed)
+ *
+ * @return array Hash array with the following fields: type, message
+ */
+ function get_error()
+ {
+ return $this->error;
+ }
+
+ /**
+ * Setter for errors for internal use
+ *
+ * @param int Error type (one of this class' error constants)
+ * @param string Error message (name of a text label)
+ */
+ protected function set_error($type, $message)
+ {
+ $this->error = array('type' => $type, 'message' => $message);
+ }
+
+ /**
* Close connection to source
* Called on script shutdown
*/
@@ -129,6 +171,8 @@ abstract class rcube_addressbook
* Create a new contact record
*
* @param array Assoziative array with save data
+ * Keys: Field name with optional section in the form FIELD:SECTION
+ * Values: Field value. Can be either a string or an array of strings for multiple values
* @param boolean True to check for duplicates first
* @return mixed The created record ID on success, False on error
*/
@@ -138,10 +182,31 @@ abstract class rcube_addressbook
}
/**
+ * Create new contact records for every item in the record set
+ *
+ * @param object rcube_result_set Recordset to insert
+ * @param boolean True to check for duplicates first
+ * @return array List of created record IDs
+ */
+ function insertMultiple($recset, $check=false)
+ {
+ $ids = array();
+ if (is_object($recset) && is_a($recset, rcube_result_set)) {
+ while ($row = $recset->next()) {
+ if ($insert = $this->insert($row, $check))
+ $ids[] = $insert;
+ }
+ }
+ return $ids;
+ }
+
+ /**
* Update a specific contact record
*
* @param mixed Record identifier
* @param array Assoziative array with save data
+ * Keys: Field name with optional section in the form FIELD:SECTION
+ * Values: Field value. Can be either a string or an array of strings for multiple values
* @return boolean True on success, False on error
*/
function update($id, $save_cols)
@@ -176,9 +241,10 @@ abstract class rcube_addressbook
/**
* List all active contact groups of this source
*
+ * @param string Optional search string to match group name
* @return array Indexed list of contact groups, each a hash array
*/
- function list_groups()
+ function list_groups($search = null)
{
/* empty for address books don't supporting groups */
return array();
@@ -260,5 +326,34 @@ abstract class rcube_addressbook
/* empty for address books don't supporting groups */
return array();
}
+
+
+ /**
+ * Utility function to return all values of a certain data column
+ * either as flat list or grouped by subtype
+ *
+ * @param string Col name
+ * @param array Record data array as used for saving
+ * @param boolean True to return one array with all values, False for hash array with values grouped by type
+ * @return array List of column values
+ */
+ function get_col_values($col, $data, $flat = false)
+ {
+ $out = array();
+ foreach ($data as $c => $values) {
+ if (strpos($c, $col) === 0) {
+ if ($flat) {
+ $out = array_merge($out, (array)$values);
+ }
+ else {
+ list($f, $type) = explode(':', $c);
+ $out[$type] = array_merge((array)$out[$type], (array)$values);
+ }
+ }
+ }
+
+ return $out;
+ }
+
}
diff --git a/program/include/rcube_browser.php b/program/include/rcube_browser.php
index 581284ab7..c5f1b8a7a 100644
--- a/program/include/rcube_browser.php
+++ b/program/include/rcube_browser.php
@@ -68,6 +68,7 @@ class rcube_browser
$this->dom = ($this->mz || $this->safari || ($this->ie && $this->ver>=5) || ($this->opera && $this->ver>=7));
$this->pngalpha = $this->mz || $this->safari || ($this->ie && $this->ver>=5.5) ||
($this->ie && $this->ver>=5 && $this->mac) || ($this->opera && $this->ver>=7) ? true : false;
+ $this->imgdata = !$this->ie;
}
}
diff --git a/program/include/rcube_contacts.php b/program/include/rcube_contacts.php
index 9761e687a..4a4c1e27e 100644
--- a/program/include/rcube_contacts.php
+++ b/program/include/rcube_contacts.php
@@ -47,13 +47,17 @@ class rcube_contacts extends rcube_addressbook
private $table_cols = array('name', 'email', 'firstname', 'surname', 'vcard');
// public properties
- var $primary_key = 'contact_id';
- var $readonly = false;
- var $groups = true;
- var $list_page = 1;
- var $page_size = 10;
- var $group_id = 0;
- var $ready = false;
+ public $primary_key = 'contact_id';
+ public $readonly = false;
+ public $groups = true;
+ public $list_page = 1;
+ public $page_size = 10;
+ public $group_id = 0;
+ public $ready = false;
+ public $coltypes = array('name', 'firstname', 'surname', 'middlename', 'prefix', 'suffix', 'nickname',
+ 'jobtitle', 'organization', 'department', 'assistant', 'manager',
+ 'gender', 'maidenname', 'spouse', 'email', 'phone', 'address',
+ 'birthday', 'anniversary', 'website', 'im', 'notes', 'photo');
/**
@@ -152,7 +156,7 @@ class rcube_contacts extends rcube_addressbook
/**
* List the current set of contact records
*
- * @param array List of cols to show
+ * @param array List of cols to show, Null means all
* @param int Only return this number of records, use negative values for tail
* @param boolean True to skip the count query (select only)
* @return array Indexed list of contact records, each a hash array
@@ -187,11 +191,21 @@ class rcube_contacts extends rcube_addressbook
$this->user_id,
$this->group_id);
+ // determine whether we have to parse the vcard or if only db cols are requested
+ $read_vcard = !$cols || count(array_intersect($cols, $this->table_cols)) < count($cols);
+
while ($sql_result && ($sql_arr = $this->db->fetch_assoc($sql_result))) {
$sql_arr['ID'] = $sql_arr[$this->primary_key];
+
+ if ($read_vcard)
+ $sql_arr = $this->convert_db_data($sql_arr);
+ else
+ $sql_arr['email'] = preg_split('/,\s*/', $sql_arr['email']);
+
// make sure we have a name to display
if (empty($sql_arr['name']))
- $sql_arr['name'] = $sql_arr['email'];
+ $sql_arr['name'] = $sql_arr['email'][0];
+
$this->result->add($sql_arr);
}
@@ -222,7 +236,7 @@ class rcube_contacts extends rcube_addressbook
* @param boolean True if results are requested, False if count only
* @param boolean True to skip the count query (select only)
* @param array List of fields that cannot be empty
- * @return Indexed list of contact records and 'count' value
+ * @return object rcube_result_set Contact records and 'count' value
*/
function search($fields, $value, $strict=false, $select=true, $nocount=false, $required=array())
{
@@ -345,12 +359,12 @@ class rcube_contacts extends rcube_addressbook
);
if ($sql_arr = $this->db->fetch_assoc()) {
- $sql_arr['ID'] = $sql_arr[$this->primary_key];
+ $record = $this->convert_db_data($sql_arr);
$this->result = new rcube_result_set(1);
- $this->result->add($sql_arr);
+ $this->result->add($record);
}
- return $assoc && $sql_arr ? $sql_arr : $this->result;
+ return $assoc && $record ? $record : $this->result;
}
@@ -389,21 +403,29 @@ class rcube_contacts extends rcube_addressbook
*/
function insert($save_data, $check=false)
{
- if (is_object($save_data) && is_a($save_data, rcube_result_set))
- return $this->insert_recset($save_data, $check);
+ if (!is_array($save_data))
+ return false;
$insert_id = $existing = false;
- if ($check)
- $existing = $this->search('email', $save_data['email'], true, false);
+ if ($check) {
+ foreach ($save_data as $col => $values) {
+ if (strpos($col, 'email') === 0) {
+ foreach ((array)$values as $email) {
+ if ($existing = $this->search('email', $email, true, false))
+ break 2;
+ }
+ }
+ }
+ }
+ $save_data = $this->convert_save_data($save_data);
$a_insert_cols = $a_insert_values = array();
- foreach ($this->table_cols as $col)
- if (isset($save_data[$col])) {
- $a_insert_cols[] = $this->db->quoteIdentifier($col);
- $a_insert_values[] = $this->db->quote($save_data[$col]);
- }
+ foreach ($save_data as $col => $value) {
+ $a_insert_cols[] = $this->db->quoteIdentifier($col);
+ $a_insert_values[] = $this->db->quote($value);
+ }
if (!$existing->count && !empty($a_insert_cols)) {
$this->db->query(
@@ -426,20 +448,6 @@ class rcube_contacts extends rcube_addressbook
/**
- * Insert new contacts for each row in set
- */
- function insert_recset($result, $check=false)
- {
- $ids = array();
- while ($row = $result->next()) {
- if ($insert = $this->insert($row, $check))
- $ids[] = $insert;
- }
- return $ids;
- }
-
-
- /**
* Update a specific contact record
*
* @param mixed Record identifier
@@ -450,11 +458,12 @@ class rcube_contacts extends rcube_addressbook
{
$updated = false;
$write_sql = array();
+ $record = $this->get_record($id, true);
+ $save_cols = $this->convert_save_data($save_cols, $record);
- foreach ($this->table_cols as $col)
- if (isset($save_cols[$col]))
- $write_sql[] = sprintf("%s=%s", $this->db->quoteIdentifier($col),
- $this->db->quote($save_cols[$col]));
+ foreach ($save_cols as $col => $value) {
+ $write_sql[] = sprintf("%s=%s", $this->db->quoteIdentifier($col), $this->db->quote($value));
+ }
if (!empty($write_sql)) {
$this->db->query(
@@ -468,10 +477,61 @@ class rcube_contacts extends rcube_addressbook
);
$updated = $this->db->affected_rows();
+ $this->result = null; // clear current result (from get_record())
}
return $updated;
}
+
+
+ private function convert_db_data($sql_arr)
+ {
+ $record = array();
+ $record['ID'] = $sql_arr[$this->primary_key];
+
+ if ($sql_arr['vcard']) {
+ unset($sql_arr['email']);
+ $vcard = new rcube_vcard($sql_arr['vcard']);
+ $record += $vcard->get_assoc() + $sql_arr;
+ }
+ else {
+ $record += $sql_arr;
+ $record['email'] = preg_split('/,\s*/', $record['email']);
+ }
+
+ return $record;
+ }
+
+
+ private function convert_save_data($save_data, $record = array())
+ {
+ $out = array();
+
+ // copy values into vcard object
+ $vcard = new rcube_vcard($record['vcard'] ? $record['vcard'] : $save_data['vcard']);
+ $vcard->reset();
+ foreach ($save_data as $key => $values) {
+ list($field, $section) = explode(':', $key);
+ foreach ((array)$values as $value) {
+ if (isset($value))
+ $vcard->set($field, $value, $section);
+ }
+ }
+ $out['vcard'] = $vcard->export();
+
+ foreach ($this->table_cols as $col) {
+ $key = $col;
+ if (!isset($save_data[$key]))
+ $key .= ':home';
+ if (isset($save_data[$key]))
+ $out[$col] = is_array($save_data[$key]) ? join(',', $save_data[$key]) : $save_data[$key];
+ }
+
+ // save all e-mails in database column
+ $out['email'] = join(", ", $vcard->email);
+
+ return $out;
+ }
/**
diff --git a/program/include/rcube_ldap.php b/program/include/rcube_ldap.php
index 17d049ef1..7ea22ebac 100644
--- a/program/include/rcube_ldap.php
+++ b/program/include/rcube_ldap.php
@@ -26,23 +26,24 @@
*/
class rcube_ldap extends rcube_addressbook
{
- var $conn;
- var $prop = array();
- var $fieldmap = array();
+ protected $conn;
+ protected $prop = array();
+ protected $fieldmap = array();
- var $filter = '';
- var $result = null;
- var $ldap_result = null;
- var $sort_col = '';
- var $mail_domain = '';
- var $debug = false;
+ protected $filter = '';
+ protected $result = null;
+ protected $ldap_result = null;
+ protected $sort_col = '';
+ protected $mail_domain = '';
+ protected $debug = false;
/** public properties */
- var $primary_key = 'ID';
- var $readonly = true;
- var $list_page = 1;
- var $page_size = 10;
- var $ready = false;
+ public $primary_key = 'ID';
+ public $readonly = true;
+ public $list_page = 1;
+ public $page_size = 10;
+ public $ready = false;
+ public $coltypes = array();
/**
@@ -57,9 +58,37 @@ class rcube_ldap extends rcube_addressbook
{
$this->prop = $p;
- foreach ($p as $prop => $value)
- if (preg_match('/^(.+)_field$/', $prop, $matches))
- $this->fieldmap[$matches[1]] = $this->_attr_name(strtolower($value));
+ // fieldmap property is given
+ if (is_array($p['fieldmap'])) {
+ foreach ($p['fieldmap'] as $rf => $lf)
+ $this->fieldmap[$rf] = $this->_attr_name(strtolower($lf));
+ }
+ else {
+ // read deprecated *_field properties to remain backwards compatible
+ foreach ($p as $prop => $value)
+ if (preg_match('/^(.+)_field$/', $prop, $matches))
+ $this->fieldmap[$matches[1]] = $this->_attr_name(strtolower($value));
+ }
+
+ // use fieldmap to advertise supported coltypes to the application
+ foreach ($this->fieldmap as $col => $lf) {
+ list($col, $type) = explode(':', $col);
+ if (!is_array($this->coltypes[$col])) {
+ $subtypes = $type ? array($type) : null;
+ $this->coltypes[$col] = array('limit' => 2, 'subtypes' => $subtypes);
+ }
+ else if ($type) {
+ $this->coltypes[$col]['subtypes'][] = $type;
+ $this->coltypes[$col]['limit']++;
+ }
+ if ($type && !$this->fieldmap[$col])
+ $this->fieldmap[$col] = $lf;
+ }
+
+ if ($this->fieldmap['street'] && $this->fieldmap['locality'])
+ $this->coltypes['address'] = array('limit' => 1);
+ else if ($this->coltypes['address'])
+ $this->coltypes['address'] = array('type' => 'textarea', 'childs' => null, 'limit' => 1, 'size' => 40);
// make sure 'required_fields' is an array
if (!is_array($this->prop['required_fields']))
@@ -455,7 +484,7 @@ class rcube_ldap extends rcube_addressbook
if ($entry && ($rec = ldap_get_attributes($this->conn, $entry)))
{
- $this->_debug("S: OK");
+ $this->_debug("S: OK"/* . print_r($rec, true)*/);
$rec = array_change_key_case($rec, CASE_LOWER);
@@ -482,8 +511,10 @@ class rcube_ldap extends rcube_addressbook
// Map out the column names to their LDAP ones to build the new entry.
$newentry = array();
$newentry['objectClass'] = $this->prop['LDAP_Object_Classes'];
- foreach ($save_cols as $col => $val) {
- $fld = $this->_map_field($col);
+ foreach ($this->fieldmap as $col => $fld) {
+ $val = $save_cols[$col];
+ if (is_array($val))
+ $val = array_filter($val); // remove empty entries
if ($fld && $val) {
// The field does exist, add it to the entry.
$newentry[$fld] = $val;
@@ -491,23 +522,29 @@ class rcube_ldap extends rcube_addressbook
} // end foreach
// Verify that the required fields are set.
- // We know that the email address is required as a default of rcube, so
- // we will default its value into any unfilled required fields.
foreach ($this->prop['required_fields'] as $fld) {
+ $missing = null;
if (!isset($newentry[$fld])) {
- $newentry[$fld] = $newentry[$this->_map_field('email')];
- } // end if
- } // end foreach
+ $missing[] = $fld;
+ }
+ }
+
+ // abort process if requiered fields are missing
+ // TODO: generate message saying which fields are missing
+ if ($missing) {
+ $this->set_error(self::ERROR_INCOMPLETE, 'formincomplete');
+ return false;
+ }
// Build the new entries DN.
- $dn = $this->prop['LDAP_rdn'].'='.rcube_ldap::quote_string($newentry[$this->prop['LDAP_rdn']], true)
- .','.$this->prop['base_dn'];
+ $dn = $this->prop['LDAP_rdn'].'='.rcube_ldap::quote_string($newentry[$this->prop['LDAP_rdn']], true).','.$this->prop['base_dn'];
$this->_debug("C: Add [dn: $dn]: ".print_r($newentry, true));
$res = ldap_add($this->conn, $dn, $newentry);
if ($res === FALSE) {
$this->_debug("S: ".ldap_error($this->conn));
+ $this->set_error(self::ERROR_SAVING, 'errorsaving');
return false;
} // end if
@@ -533,8 +570,8 @@ class rcube_ldap extends rcube_addressbook
$newdata = array();
$replacedata = array();
$deletedata = array();
- foreach ($save_cols as $col => $val) {
- $fld = $this->_map_field($col);
+ foreach ($this->fieldmap as $col => $fld) {
+ $val = $save_cols[$col];
if ($fld) {
// The field does exist compare it to the ldap record.
if ($record[$col] != $val) {
@@ -566,6 +603,7 @@ class rcube_ldap extends rcube_addressbook
$this->_debug("C: Delete [dn: $dn]: ".print_r($deletedata, true));
if (!ldap_mod_del($this->conn, $dn, $deletedata)) {
$this->_debug("S: ".ldap_error($this->conn));
+ $this->set_error(self::ERROR_SAVING, 'errorsaving');
return false;
}
$this->_debug("S: OK");
@@ -575,11 +613,11 @@ class rcube_ldap extends rcube_addressbook
// Handle RDN change
if ($replacedata[$this->prop['LDAP_rdn']]) {
$newdn = $this->prop['LDAP_rdn'].'='
- .rcube_ldap::quote_string($replacedata[$this->prop['LDAP_rdn']], true)
- .','.$this->prop['base_dn'];
+ .rcube_ldap::quote_string($replacedata[$this->prop['LDAP_rdn']], true)
+ .','.$this->prop['base_dn'];
if ($dn != $newdn) {
$newrdn = $this->prop['LDAP_rdn'].'='
- .rcube_ldap::quote_string($replacedata[$this->prop['LDAP_rdn']], true);
+ .rcube_ldap::quote_string($replacedata[$this->prop['LDAP_rdn']], true);
unset($replacedata[$this->prop['LDAP_rdn']]);
}
}
@@ -589,7 +627,7 @@ class rcube_ldap extends rcube_addressbook
if (!ldap_mod_replace($this->conn, $dn, $replacedata)) {
$this->_debug("S: ".ldap_error($this->conn));
return false;
- }
+ }
$this->_debug("S: OK");
} // end if
} // end if
@@ -599,6 +637,7 @@ class rcube_ldap extends rcube_addressbook
$this->_debug("C: Add [dn: $dn]: ".print_r($newdata, true));
if (!ldap_mod_add($this->conn, $dn, $newdata)) {
$this->_debug("S: ".ldap_error($this->conn));
+ $this->set_error(self::ERROR_SAVING, 'errorsaving');
return false;
}
$this->_debug("S: OK");
@@ -638,6 +677,7 @@ class rcube_ldap extends rcube_addressbook
$res = ldap_delete($this->conn, $dn);
if ($res === FALSE) {
$this->_debug("S: ".ldap_error($this->conn));
+ $this->set_error(self::ERROR_SAVING, 'errorsaving');
return false;
} // end if
$this->_debug("S: OK");
@@ -679,8 +719,6 @@ class rcube_ldap extends rcube_addressbook
*/
private function _ldap2result($rec)
{
- global $RCMAIL;
-
$out = array();
if ($rec['dn'])
@@ -688,11 +726,17 @@ class rcube_ldap extends rcube_addressbook
foreach ($this->fieldmap as $rf => $lf)
{
- if ($rec[$lf]['count']) {
- if ($rf == 'email' && $this->mail_domain && !strpos($rec[$lf][0], '@'))
- $out[$rf] = sprintf('%s@%s', $rec[$lf][0], $this->mail_domain);
+ for ($i=0; $i < $rec[$lf]['count']; $i++) {
+ if (!($value = $rec[$lf][$i]))
+ continue;
+ if ($rf == 'email' && $this->mail_domain && !strpos($value, '@'))
+ $out[$rf][] = sprintf('%s@%s', $value, $this->mail_domain);
+ else if (in_array($rf, array('street','zipcode','locality','country','region')))
+ $out['address'][$i][$rf] = $value;
+ else if ($rec[$lf]['count'] > 1)
+ $out[$rf][] = $value;
else
- $out[$rf] = $rec[$lf][0];
+ $out[$rf] = $value;
}
}
@@ -741,6 +785,10 @@ class rcube_ldap extends rcube_addressbook
*/
function quote_string($str, $dn=false)
{
+ // take firt entry if array given
+ if (is_array($str))
+ $str = reset($str);
+
if ($dn)
$replace = array(','=>'\2c', '='=>'\3d', '+'=>'\2b', '<'=>'\3c',
'>'=>'\3e', ';'=>'\3b', '\\'=>'\5c', '"'=>'\22', '#'=>'\23');
diff --git a/program/include/rcube_plugin.php b/program/include/rcube_plugin.php
index 85ed77492..09791374c 100644
--- a/program/include/rcube_plugin.php
+++ b/program/include/rcube_plugin.php
@@ -83,7 +83,20 @@ abstract class rcube_plugin
* Initialization method, needs to be implemented by the plugin itself
*/
abstract function init();
-
+
+
+ /**
+ * Attempt to load the given plugin which is required for the current plugin
+ *
+ * @param string Plugin name
+ * @return boolean True on success, false on failure
+ */
+ public function require_plugin($plugin_name)
+ {
+ return $this->api->load_plugin($plugin_name);
+ }
+
+
/**
* Load local config file from plugins directory.
* The loaded values are patched over the global configuration.
diff --git a/program/include/rcube_plugin_api.php b/program/include/rcube_plugin_api.php
index 0f7ab3f4b..54a9a8bee 100644
--- a/program/include/rcube_plugin_api.php
+++ b/program/include/rcube_plugin_api.php
@@ -109,42 +109,9 @@ class rcube_plugin_api
$this->output = $rcmail->output;
$this->config = $rcmail->config;
- $plugins_dir = dir($this->dir);
- $plugins_dir = unslashify($plugins_dir->path);
$plugins_enabled = (array)$rcmail->config->get('plugins', array());
-
foreach ($plugins_enabled as $plugin_name) {
- $fn = $plugins_dir . DIRECTORY_SEPARATOR . $plugin_name . DIRECTORY_SEPARATOR . $plugin_name . '.php';
-
- if (file_exists($fn)) {
- include($fn);
-
- // instantiate class if exists
- if (class_exists($plugin_name, false)) {
- $plugin = new $plugin_name($this);
- // check inheritance...
- if (is_subclass_of($plugin, 'rcube_plugin')) {
- // ... task, request type and framed mode
- if ((!$plugin->task || preg_match('/^('.$plugin->task.')$/i', $rcmail->task))
- && (!$plugin->noajax || is_a($this->output, 'rcube_template'))
- && (!$plugin->noframe || empty($_REQUEST['_framed']))
- ) {
- $plugin->init();
- $this->plugins[] = $plugin;
- }
- }
- }
- else {
- raise_error(array('code' => 520, 'type' => 'php',
- 'file' => __FILE__, 'line' => __LINE__,
- 'message' => "No plugin class $plugin_name found in $fn"), true, false);
- }
- }
- else {
- raise_error(array('code' => 520, 'type' => 'php',
- 'file' => __FILE__, 'line' => __LINE__,
- 'message' => "Failed to load plugin file $fn"), true, false);
- }
+ $this->load_plugin($plugin_name);
}
// check existance of all required core plugins
@@ -158,31 +125,14 @@ class rcube_plugin_api
}
// load required core plugin if no derivate was found
- if (!$loaded) {
- $fn = $plugins_dir . DIRECTORY_SEPARATOR . $plugin_name . DIRECTORY_SEPARATOR . $plugin_name . '.php';
+ if (!$loaded)
+ $loaded = $this->load_plugin($plugin_name);
- if (file_exists($fn)) {
- include_once($fn);
-
- if (class_exists($plugin_name, false)) {
- $plugin = new $plugin_name($this);
- // check inheritance
- if (is_subclass_of($plugin, 'rcube_plugin')) {
- if (!$plugin->task || preg_match('/('.$plugin->task.')/i', $rcmail->task)) {
- $plugin->init();
- $this->plugins[] = $plugin;
- }
- $loaded = true;
- }
- }
- }
- }
-
// trigger fatal error if still not loaded
if (!$loaded) {
raise_error(array('code' => 520, 'type' => 'php',
- 'file' => __FILE__, 'line' => __LINE__,
- 'message' => "Requried plugin $plugin_name was not loaded"), true, true);
+ 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "Requried plugin $plugin_name was not loaded"), true, true);
}
}
@@ -191,6 +141,64 @@ class rcube_plugin_api
// maybe also register a shudown function which triggers shutdown functions of all plugin objects
}
+
+
+ /**
+ * Load the specified plugin
+ *
+ * @param string Plugin name
+ * @return boolean True on success, false if not loaded or failure
+ */
+ public function load_plugin($plugin_name)
+ {
+ static $plugins_dir;
+
+ $rcmail = rcmail::get_instance();
+
+ if (!$plugins_dir) {
+ $dir = dir($this->dir);
+ $plugins_dir = unslashify($dir->path);
+ }
+
+ // plugin already loaded
+ if ($this->plugins[$plugin_name] || class_exists($plugin_name, false))
+ return true;
+
+ $fn = $plugins_dir . DIRECTORY_SEPARATOR . $plugin_name . DIRECTORY_SEPARATOR . $plugin_name . '.php';
+
+ if (file_exists($fn)) {
+ include($fn);
+
+ // instantiate class if exists
+ if (class_exists($plugin_name, false)) {
+ $plugin = new $plugin_name($this);
+ // check inheritance...
+ if (is_subclass_of($plugin, 'rcube_plugin')) {
+ // ... task, request type and framed mode
+ if ((!$plugin->task || preg_match('/^('.$plugin->task.')$/i', $rcmail->task)) /*
+ && (!$plugin->noajax || is_a($rcmail->output, 'rcube_template'))
+ && (!$plugin->noframe || empty($_REQUEST['_framed']))*/
+ ) {
+ $plugin->init();
+ $this->plugins[$plugin_name] = $plugin;
+ }
+ return true;
+ }
+ }
+ else {
+ raise_error(array('code' => 520, 'type' => 'php',
+ 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "No plugin class $plugin_name found in $fn"), true, false);
+ }
+ }
+ else {
+ raise_error(array('code' => 520, 'type' => 'php',
+ 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "Failed to load plugin file $fn"), true, false);
+ }
+
+ return false;
+ }
/**
diff --git a/program/include/rcube_shared.inc b/program/include/rcube_shared.inc
index 36c832e80..83eefd6da 100644
--- a/program/include/rcube_shared.inc
+++ b/program/include/rcube_shared.inc
@@ -485,6 +485,25 @@ function rc_mime_content_type($path, $name, $failover = 'application/octet-strea
return $mime_type;
}
+
+/**
+ * Detect image type of the given binary data by checking magic numbers
+ *
+ * @param string Binary file content
+ * @return string Detected mime-type or jpeg as fallback
+ */
+function rc_image_content_type($data)
+{
+ $type = 'jpeg';
+ if (preg_match('/^\x89\x50\x4E\x47/', $data)) $type = 'png';
+ else if (preg_match('/^\x47\x49\x46\x38/', $data)) $type = 'gif';
+ else if (preg_match('/^\x00\x00\x01\x00/', $data)) $type = 'ico';
+// else if (preg_match('/^\xFF\xD8\xFF\xE0/', $data)) $type = 'jpeg';
+
+ return 'image/' . $type;
+}
+
+
/**
* A method to guess encoding of a string.
*
diff --git a/program/include/rcube_template.php b/program/include/rcube_template.php
index 2aa16f098..1d1a95b90 100755
--- a/program/include/rcube_template.php
+++ b/program/include/rcube_template.php
@@ -996,8 +996,11 @@ class rcube_template extends rcube_html_page
$attrib['action'] = './';
// we already have a <form> tag
- if ($attrib['form'])
+ if ($attrib['form']) {
+ if ($this->framed || !empty($_REQUEST['_framed']))
+ $hidden->add(array('name' => '_framed', 'value' => '1'));
return $hidden->show() . $content;
+ }
else
return $this->form_tag($attrib, $hidden->show() . $content);
}
diff --git a/program/include/rcube_vcard.php b/program/include/rcube_vcard.php
index 70ea55c87..11872249c 100644
--- a/program/include/rcube_vcard.php
+++ b/program/include/rcube_vcard.php
@@ -5,7 +5,7 @@
| program/include/rcube_vcard.php |
| |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2008-2009, The Roundcube Dev Team |
+ | Copyright (C) 2008-2011, The Roundcube Dev Team |
| Licensed under the GNU GPL |
| |
| PURPOSE: |
@@ -33,6 +33,24 @@ class rcube_vcard
'FN' => array(),
'N' => array(array('','','','','')),
);
+ private $fieldmap = array(
+ 'phone' => 'TEL',
+ 'birthday' => 'BDAY',
+ 'website' => 'URL',
+ 'notes' => 'NOTE',
+ 'email' => 'EMAIL',
+ 'address' => 'ADR',
+ 'gender' => 'X-GENDER',
+ 'maidenname' => 'X-MAIDENNAME',
+ 'anniversary' => 'X-ANNIVERSARY',
+ 'assistant' => 'X-ASSISTANT',
+ 'manager' => 'X-MANAGER',
+ 'spouse' => 'X-SPOUSE',
+ );
+ private $typemap = array('iPhone' => 'mobile', 'CELL' => 'mobile');
+ private $phonetypemap = array('HOME1' => 'HOME', 'BUSINESS1' => 'WORK', 'BUSINESS2' => 'WORK2', 'WORKFAX' => 'BUSINESSFAX');
+ private $addresstypemap = array('BUSINESS' => 'WORK');
+ private $immap = array('X-JABBER' => 'jabber', 'X-ICQ' => 'icq', 'X-MSN' => 'msn', 'X-AIM' => 'aim', 'X-YAHOO' => 'yahoo', 'X-SKYPE' => 'skype', 'X-SKYPE-USERNAME' => 'skype');
public $business = false;
public $displayname;
@@ -107,12 +125,92 @@ class rcube_vcard
/**
+ * Return vCard data as associative array to be unsed in Roundcube address books
+ *
+ * @return array Hash array with key-value pairs
+ */
+ public function get_assoc()
+ {
+ $out = array('name' => $this->displayname);
+ $typemap = $this->typemap;
+
+ // copy name fields to output array
+ foreach (array('firstname','surname','middlename','nickname','organization') as $col)
+ $out[$col] = $this->$col;
+
+ $out['prefix'] = $this->raw['N'][0][3];
+ $out['suffix'] = $this->raw['N'][0][4];
+
+ // convert from raw vcard data into associative data for Roundcube
+ foreach (array_flip($this->fieldmap) as $tag => $col) {
+ foreach ((array)$this->raw[$tag] as $i => $raw) {
+ if (is_array($raw)) {
+ $k = -1;
+ $key = $col;
+ $subtype = $typemap[$raw['type'][++$k]] ? $typemap[$raw['type'][$k]] : strtolower($raw['type'][$k]);
+ while ($k < count($raw['type']) && ($subtype == 'internet' || $subtype == 'pref'))
+ $subtype = $typemap[$raw['type'][++$k]] ? $typemap[$raw['type'][$k]] : strtolower($raw['type'][$k]);
+ if ($subtype)
+ $key .= ':' . $subtype;
+
+ // split ADR values into assoc array
+ if ($tag == 'ADR') {
+ list(,, $value['street'], $value['locality'], $value['region'], $value['zipcode'], $value['country']) = $raw;
+ $out[$key][] = $value;
+ }
+ else
+ $out[$key][] = $raw[0];
+ }
+ else {
+ $out[$col][] = $raw;
+ }
+ }
+ }
+
+ // handle special IM fields as used by Apple
+ foreach ($this->immap as $tag => $type) {
+ foreach ((array)$this->raw[$tag] as $i => $raw) {
+ $out['im:'.$type][] = $raw[0];
+ }
+ }
+
+ // copy photo data
+ if ($this->raw['PHOTO'])
+ $out['photo'] = $this->raw['PHOTO'][0][0];
+
+ return $out;
+ }
+
+
+ /**
* Convert the data structure into a vcard 3.0 string
*/
public function export()
{
return self::rfc2425_fold(self::vcard_encode($this->raw));
}
+
+
+ /**
+ * Clear the given fields in the loaded vcard data
+ *
+ * @param array List of field names to be reset
+ */
+ public function reset($fields = null)
+ {
+ if (!$fields)
+ $fields = array_merge(array_values($this->fieldmap), array_keys($this->immap), array('FN','N','ORG','NICKNAME','EMAIL','ADR','BDAY'));
+
+ foreach ($fields as $f)
+ unset($this->raw[$f]);
+
+ if (!$this->raw['N'])
+ $this->raw['N'] = array(array('','','','',''));
+ if (!$this->raw['FN'])
+ $this->raw['FN'] = array();
+
+ $this->email = array();
+ }
/**
@@ -120,24 +218,40 @@ class rcube_vcard
*
* @param string Field name
* @param string Field value
- * @param string Section name
+ * @param string Type/section name
*/
- public function set($field, $value, $section = 'HOME')
+ public function set($field, $value, $type = 'HOME')
{
+ $field = strtolower($field);
+ $type = strtoupper($type);
+ $typemap = array_flip($this->typemap);
+
switch ($field) {
case 'name':
case 'displayname':
$this->raw['FN'][0][0] = $value;
break;
+ case 'surname':
+ $this->raw['N'][0][0] = $value;
+ break;
+
case 'firstname':
$this->raw['N'][0][1] = $value;
break;
- case 'surname':
- $this->raw['N'][0][0] = $value;
+ case 'middlename':
+ $this->raw['N'][0][2] = $value;
break;
-
+
+ case 'prefix':
+ $this->raw['N'][0][3] = $value;
+ break;
+
+ case 'suffix':
+ $this->raw['N'][0][4] = $value;
+ break;
+
case 'nickname':
$this->raw['NICKNAME'][0][0] = $value;
break;
@@ -146,13 +260,47 @@ class rcube_vcard
$this->raw['ORG'][0][0] = $value;
break;
+ case 'photo':
+ $encoded = !preg_match('![^a-z0-9/=+-]!i', $value);
+ $this->raw['PHOTO'][0] = array(0 => $encoded ? $value : base64_encode($value), 'BASE64' => true);
+ break;
+
case 'email':
- $index = $this->get_type_index('EMAIL', $section);
- if (!is_array($this->raw['EMAIL'][$index])) {
- $this->raw['EMAIL'][$index] = array(0 => $value, 'type' => array('INTERNET', $section, 'pref'));
- }
- else {
- $this->raw['EMAIL'][$index][0] = $value;
+ $this->raw['EMAIL'][] = array(0 => $value, 'type' => array_filter(array('INTERNET', $type)));
+ $this->email[] = $value;
+ break;
+
+ case 'im':
+ // save IM subtypes into extension fields
+ $typemap = array_flip($this->immap);
+ if ($field = $typemap[strtolower($type)])
+ $this->raw[$field][] = array(0 => $value);
+ break;
+
+ case 'birthday':
+ if ($val = @strtotime($value))
+ $this->raw['BDAY'][] = array(0 => date('Y-m-d', $val), 'value' => array('date'));
+ break;
+
+ case 'address':
+ if ($this->addresstypemap[$type])
+ $type = $this->addresstypemap[$type];
+
+ $value = $value[0] ? $value : array('', '', $value['street'], $value['locality'], $value['region'], $value['zipcode'], $value['country']);
+
+ // fall through if not empty
+ if (!strlen(join('', $value)))
+ break;
+
+ default:
+ if ($field == 'phone' && $this->phonetypemap[$type])
+ $type = $this->phonetypemap[$type];
+
+ if (($tag = $this->fieldmap[$field]) && (is_array($value) || strlen($value))) {
+ $index = count($this->raw[$tag]);
+ $this->raw[$tag][$index] = (array)$value;
+ if ($type)
+ $this->raw[$tag][$index]['type'] = array(($typemap[$type] ? $typemap[$type] : $type));
}
break;
}
diff --git a/program/js/app.js b/program/js/app.js
index b21435ef1..e7af013fc 100644
--- a/program/js/app.js
+++ b/program/js/app.js
@@ -319,7 +319,20 @@ function rcube_webmail()
if ((this.env.action=='add' || this.env.action=='edit') && this.gui_objects.editform) {
this.enable_command('save', true);
- $("input[type='text']").first().select();
+ this.enable_command('upload-photo', this.env.coltypes.photo ? true : false);
+ this.enable_command('delete-photo', this.env.coltypes.photo && this.env.action == 'edit');
+
+ for (var col in this.env.coltypes)
+ this.init_edit_field(col, null);
+
+ $('.contactfieldgroup .row a.deletebutton').click(function(){ ref.delete_edit_field(this); return false });
+
+ $('select.addfieldmenu').change(function(e){
+ ref.insert_edit_field($(this).val(), $(this).attr('rel'), this);
+ this.selectedIndex = 0;
+ });
+
+ $("input[type='text']").first().focus();
}
else if (this.gui_objects.qsearchbox) {
this.enable_command('search', 'reset-search', 'moveto', true);
@@ -639,6 +652,9 @@ function rcube_webmail()
input_email.focus();
break;
}
+
+ // clear empty input fields
+ $('input.placeholder').each(function(){ if (this.value == this._placeholder) this.value = ''; });
}
this.gui_objects.editform.submit();
@@ -996,13 +1012,17 @@ function rcube_webmail()
case 'export':
if (this.contact_list.rowcount > 0) {
- var add_url = (this.env.source ? '_source='+urlencode(this.env.source)+'&' : '');
- if (this.env.search_request)
- add_url += '_search='+this.env.search_request;
-
- this.goto_url('export', add_url);
+ this.goto_url('export', { _source:this.env.source, _gid:this.env.group, _search:this.env.search_request });
}
break;
+
+ case 'upload-photo':
+ this.upload_contact_photo(props);
+ break;
+
+ case 'delete-photo':
+ this.replace_contact_photo('-del-');
+ break;
// user settings commands
case 'preferences':
@@ -1158,7 +1178,7 @@ function rcube_webmail()
this.is_framed = function()
{
- return (this.env.framed && parent.rcmail);
+ return (this.env.framed && parent.rcmail && parent.rcmail != this && parent.rcmail.command);
};
@@ -3177,27 +3197,7 @@ function rcube_webmail()
// create hidden iframe and post upload form
if (send) {
- var ts = new Date().getTime();
- var frame_name = 'rcmupload'+ts;
-
- // have to do it this way for IE
- // otherwise the form will be posted to a new window
- if (document.all) {
- var html = '<iframe name="'+frame_name+'" src="program/blank.gif" style="width:0;height:0;visibility:hidden;"></iframe>';
- document.body.insertAdjacentHTML('BeforeEnd',html);
- }
- else { // for standards-compilant browsers
- var frame = document.createElement('iframe');
- frame.name = frame_name;
- frame.style.border = 'none';
- frame.style.width = 0;
- frame.style.height = 0;
- frame.style.visibility = 'hidden';
- document.body.appendChild(frame);
- }
-
- // handle upload errors, parsing iframe content in onload
- $(frame_name).bind('load', {ts:ts}, function(e) {
+ this.async_upload_form(form, 'upload', function(e) {
var d, content = '';
try {
if (this.contentDocument) {
@@ -3218,11 +3218,6 @@ function rcube_webmail()
rcmail.env.uploadframe = e.data.ts;
});
- form.target = frame_name;
- form.action = this.env.comm_path+'&_action=upload&_uploadid='+ts;
- form.setAttribute('enctype', 'multipart/form-data');
- form.submit();
-
// display upload indicator and cancel button
var content = this.get_label('uploading');
if (this.env.loadingicon)
@@ -3979,6 +3974,165 @@ function rcube_webmail()
};
+ this.init_edit_field = function(col, elem)
+ {
+ if (!elem)
+ elem = $('.ff_' + col);
+
+ elem.focus(function(){ ref.focus_textfield(this); })
+ .blur(function(){ ref.blur_textfield(this); })
+ .each(function(){ this._placeholder = ref.env.coltypes[col].label; ref.blur_textfield(this); });
+ };
+
+ this.insert_edit_field = function(col, section, menu)
+ {
+ // just make pre-defined input field visible
+ var elem = $('#ff_'+col);
+ if (elem.length) {
+ elem.show().focus();
+ $(menu).children('option[value="'+col+'"]').attr('disabled', true);
+ }
+ else {
+ var lastelem = $('.ff_'+col),
+ appendcontainer = $('#contactsection'+section+' .contactcontroller'+col);
+
+ if (!appendcontainer.length)
+ appendcontainer = $('<fieldset>').addClass('contactfieldgroup contactcontroller'+col).insertAfter($('#contactsection'+section+' .contactfieldgroup').last());
+
+ if (appendcontainer.length && appendcontainer.get(0).nodeName == 'FIELDSET') {
+ var input, colprop = this.env.coltypes[col],
+ row = $('<div>').addClass('row'),
+ cell = $('<div>').addClass('contactfieldcontent data'),
+ label = $('<div>').addClass('contactfieldlabel label');
+
+ if (colprop.subtypes_select)
+ label.html(colprop.subtypes_select);
+ else
+ label.html(colprop.label);
+
+ var name_suffix = colprop.limit != 1 ? '[]' : '';
+ if (colprop.type == 'text' || colprop.type == 'date') {
+ input = $('<input>')
+ .addClass('ff_'+col)
+ .attr('type', 'text')
+ .attr('name', '_'+col+name_suffix)
+ .attr('size', colprop.size)
+ .appendTo(cell);
+
+ this.init_edit_field(col, input);
+ }
+ else if (colprop.type == 'composite') {
+ var childcol, cp, first;
+ for (var childcol in colprop.childs) {
+ cp = colprop.childs[childcol];
+ input = $('<input>')
+ .addClass('ff_'+childcol)
+ .attr('type', 'text')
+ .attr('name', '_'+childcol+name_suffix)
+ .attr('size', cp.size)
+ .appendTo(cell);
+ cell.append(" ");
+ this.init_edit_field(childcol, input);
+ if (!first) first = input;
+ }
+ input = first; // set focus to the first of this composite fields
+ }
+ else if (colprop.type == 'select') {
+ input = $('<select>')
+ .addClass('ff_'+col)
+ .attr('name', '_'+col+name_suffix)
+ .appendTo(cell);
+
+ var options = input.attr('options');
+ options[options.length] = new Option('---', '');
+ if (colprop.options)
+ $.each(colprop.options, function(i, val){ options[options.length] = new Option(val, i); });
+ }
+
+ if (input) {
+ var delbutton = $('<a href="#del"></a>')
+ .addClass('contactfieldbutton deletebutton')
+ .attr('title', this.get_label('delete'))
+ .attr('rel', col)
+ .html(this.env.delbutton)
+ .click(function(){ ref.delete_edit_field(this); return false })
+ .appendTo(cell);
+
+ row.append(label).append(cell).appendTo(appendcontainer.show());
+ input.first().focus();
+
+ // disable option if limit reached
+ if (!colprop.count) colprop.count = 0;
+ if (++colprop.count == colprop.limit && colprop.limit)
+ $(menu).children('option[value="'+col+'"]').attr('disabled', true);
+ }
+ }
+ }
+ };
+
+ this.delete_edit_field = function(elem)
+ {
+ var col = $(elem).attr('rel'),
+ colprop = this.env.coltypes[col],
+ fieldset = $(elem).parents('fieldset.contactfieldgroup'),
+ addmenu = fieldset.parent().find('select.addfieldmenu');
+
+ // just clear input but don't hide the last field
+ if (--colprop.count <= 0 && colprop.visible)
+ $(elem).parent().children('input').val('').blur();
+ else {
+ $(elem).parents('div.row').remove();
+ // hide entire fieldset if no more rows
+ if (!fieldset.children('div.row').length)
+ fieldset.hide();
+ }
+
+ // enable option in add-field selector or insert it if necessary
+ if (addmenu.length) {
+ var option = addmenu.children('option[value="'+col+'"]');
+ if (option.length)
+ option.attr('disabled', false);
+ else
+ option = $('<option>').attr('value', col).html(colprop.label).appendTo(addmenu);
+ addmenu.show();
+ }
+ };
+
+
+ this.upload_contact_photo = function(form)
+ {
+ if (form && form.elements._photo.value) {
+ this.async_upload_form(form, 'upload-photo', function(e) {
+ rcmail.set_busy(false, null, rcmail.photo_upload_id);
+ });
+
+ // display upload indicator
+ this.photo_upload_id = this.set_busy(true, 'uploading');
+ }
+ };
+
+ this.replace_contact_photo = function(id)
+ {
+ $('#ff_photo').val(id);
+
+ var buttons = this.buttons['upload-photo'];
+ for (var n=0; n < buttons.length; n++)
+ $('#'+buttons[n].id).html(this.get_label(id == '-del-' ? 'addphoto' : 'replacephoto'));
+
+ var img_src = id == '-del-' ? this.env.photo_placeholder :
+ this.env.comm_path + '&_action=photo&_source=' + this.env.source + '&_cid=' + this.env.cid + '&_photo=' + id;
+ $(this.gui_objects.contactphoto).children('img').attr('src', img_src);
+
+ this.enable_command('delete-photo', id != '-del-');
+ };
+
+ this.photo_upload_end = function()
+ {
+ this.set_busy(false, null, this.photo_upload_id);
+ delete this.photo_upload_id;
+ };
+
+
/*********************************************************/
/********* user settings methods *********/
/*********************************************************/
@@ -4505,6 +4659,23 @@ function rcube_webmail()
}
};
+
+ this.focus_textfield = function(elem)
+ {
+ elem._hasfocus = true;
+ var $elem = $(elem);
+ if ($elem.hasClass('placeholder') || $elem.val() == elem._placeholder)
+ $elem.val('').removeClass('placeholder').attr('spellcheck', true);
+ };
+
+ this.blur_textfield = function(elem)
+ {
+ elem._hasfocus = false;
+ var $elem = $(elem);
+ if (elem._placeholder && (!$elem.val() || $elem.val() == elem._placeholder))
+ $elem.addClass('placeholder').attr('spellcheck', false).val(elem._placeholder);
+ };
+
// write to the document/window title
this.set_pagetitle = function(title)
{
@@ -4951,6 +5122,39 @@ function rcube_webmail()
/********************************************************/
/********* remote request methods *********/
/********************************************************/
+
+ // compose a valid url with the given parameters
+ this.url = function(action, query)
+ {
+ var querystring = typeof(query) == 'string' ? '&' + query : '';
+
+ if (typeof action != 'string')
+ query = action;
+ else if (!query || typeof(query) != 'object')
+ query = {};
+
+ if (action)
+ query._action = action;
+ else
+ query._action = this.env.action;
+
+ var base = this.env.comm_path;
+
+ // overwrite task name
+ if (query._action.match(/([a-z]+)\/([a-z-_]+)/)) {
+ query._action = RegExp.$2;
+ base = base.replace(/\_task=[a-z]+/, '_task='+RegExp.$1);
+ }
+
+ // remove undefined values
+ var param = {};
+ for (var k in query) {
+ if (typeof(query[k]) != 'undefined' && query[k] !== null)
+ param[k] = query[k];
+ }
+
+ return base + '&' + $.param(param) + querystring;
+ };
this.redirect = function(url, lock)
{
@@ -4965,28 +5169,13 @@ function rcube_webmail()
this.goto_url = function(action, query, lock)
{
- var url = this.env.comm_path,
- querystring = query ? '&'+query : '';
-
- // overwrite task name
- if (action.match(/([a-z]+)\/([a-z-_]+)/)) {
- action = RegExp.$2;
- url = url.replace(/\_task=[a-z]+/, '_task='+RegExp.$1);
- }
-
- this.redirect(url+'&_action='+action+querystring, lock);
+ this.redirect(this.url(action, query));
};
// send a http request to the server
this.http_request = function(action, query, lock)
{
- var url = this.env.comm_path;
-
- // overwrite task name
- if (action.match(/([a-z]+)\/([a-z-_]+)/)) {
- action = RegExp.$2;
- url = url.replace(/\_task=[a-z]+/, '_task='+RegExp.$1);
- }
+ var url = this.url(action, query);
// trigger plugin hook
var result = this.triggerEvent('request'+action, query);
@@ -4999,7 +5188,7 @@ function rcube_webmail()
query = result;
}
- url += '&_remote=1&_action=' + action + (query ? '&' : '') + query;
+ url += '&_remote=1';
// send request
console.log('HTTP GET: ' + url);
@@ -5013,15 +5202,7 @@ function rcube_webmail()
// send a http POST request to the server
this.http_post = function(action, postdata, lock)
{
- var url = this.env.comm_path;
-
- // overwrite task name
- if (action.match(/([a-z]+)\/([a-z-_]+)/)) {
- action = RegExp.$2;
- url = url.replace(/\_task=[a-z]+/, '_task='+RegExp.$1);
- }
-
- url += '&_action=' + action;
+ var url = this.url(action);
if (postdata && typeof(postdata) == 'object') {
postdata._remote = 1;
@@ -5168,6 +5349,37 @@ function rcube_webmail()
this.display_message(this.get_label('servererror') + ' (' + errmsg + ')', 'error');
};
+ // post the given form to a hidden iframe
+ this.async_upload_form = function(form, action, onload)
+ {
+ var ts = new Date().getTime();
+ var frame_name = 'rcmupload'+ts;
+
+ // have to do it this way for IE
+ // otherwise the form will be posted to a new window
+ if (document.all) {
+ var html = '<iframe name="'+frame_name+'" src="program/blank.gif" style="width:0;height:0;visibility:hidden;"></iframe>';
+ document.body.insertAdjacentHTML('BeforeEnd', html);
+ }
+ else { // for standards-compilant browsers
+ var frame = document.createElement('iframe');
+ frame.name = frame_name;
+ frame.style.border = 'none';
+ frame.style.width = 0;
+ frame.style.height = 0;
+ frame.style.visibility = 'hidden';
+ document.body.appendChild(frame);
+ }
+
+ // handle upload errors, parsing iframe content in onload
+ $(frame_name).bind('load', {ts:ts}, onload);
+
+ form.target = frame_name;
+ form.action = this.url(action, { _uploadid:ts });
+ form.setAttribute('enctype', 'multipart/form-data');
+ form.submit();
+ };
+
// starts interval for keep-alive/check-recent signal
this.start_keepalive = function()
{
diff --git a/program/localization/en_US/labels.inc b/program/localization/en_US/labels.inc
index e2e23c4a1..59e396f2b 100644
--- a/program/localization/en_US/labels.inc
+++ b/program/localization/en_US/labels.inc
@@ -243,11 +243,38 @@ $labels['yourmessage'] = 'This is a Return Receipt for your message';
$labels['receiptnote'] = 'Note: This receipt only acknowledges that the message was displayed on the recipient\'s computer. There is no guarantee that the recipient has read or understood the message contents.';
// address boook
-$labels['name'] = 'Display name';
-$labels['firstname'] = 'First name';
-$labels['surname'] = 'Last name';
-$labels['email'] = 'E-Mail';
-
+$labels['name'] = 'Display name';
+$labels['firstname'] = 'First name';
+$labels['surname'] = 'Last name';
+$labels['middlename'] = 'Middle name';
+$labels['nameprefix'] = 'Prefix';
+$labels['namesuffix'] = 'Suffix';
+$labels['nickname'] = 'Nickname';
+$labels['jobtitle'] = 'Job title';
+$labels['organization'] = 'Company';
+$labels['department'] = 'Department';
+$labels['gender'] = 'Gender';
+$labels['maidenname'] = 'Maiden name';
+$labels['email'] = 'E-Mail';
+$labels['phone'] = 'Phone';
+$labels['address'] = 'Address';
+$labels['street'] = 'Street';
+$labels['locality'] = 'City';
+$labels['zipcode'] = 'Zip code';
+$labels['region'] = 'Region';
+$labels['country'] = 'Country';
+$labels['birthday'] = 'Birthday';
+$labels['anniversary'] = 'Anniversary';
+$labels['website'] = 'Website';
+$labels['instantmessenger'] = 'IM';
+$labels['notes'] = 'Notes';
+$labels['male'] = 'male';
+$labels['female'] = 'female';
+$labels['manager'] = 'Manager';
+$labels['assistant'] = 'Assistant';
+$labels['spouse'] = 'Spouse';
+
+$labels['addfield'] = 'Add field...';
$labels['addcontact'] = 'Add new contact';
$labels['editcontact'] = 'Edit contact';
$labels['contacts'] = 'Contacts';
@@ -258,6 +285,8 @@ $labels['cancel'] = 'Cancel';
$labels['save'] = 'Save';
$labels['delete'] = 'Delete';
$labels['rename'] = 'Rename';
+$labels['addphoto'] = 'Add';
+$labels['replacephoto'] = 'Replace';
$labels['newcontact'] = 'Create new contact card';
$labels['deletecontact'] = 'Delete selected contacts';
diff --git a/program/localization/en_US/messages.inc b/program/localization/en_US/messages.inc
index cca6e916a..df347d1db 100644
--- a/program/localization/en_US/messages.inc
+++ b/program/localization/en_US/messages.inc
@@ -104,7 +104,7 @@ $messages['forbiddencharacter'] = 'Folder name contains a forbidden character';
$messages['selectimportfile'] = 'Please select a file to upload';
$messages['addresswriterror'] = 'The selected address book is not writeable';
$messages['contactaddedtogroup'] = 'Successfully added the contacts to this group';
-$messages['contactremovedfromgroup'] = 'Successfully remove contacts from this group';
+$messages['contactremovedfromgroup'] = 'Successfully removed contacts from this group';
$messages['importwait'] = 'Importing, please wait...';
$messages['importerror'] = 'Import failed! The uploaded file is not a valid vCard file.';
$messages['importconfirm'] = '<b>Successfully imported $inserted contacts, $skipped existing entries skipped</b>:<p><em>$names</em></p>';
@@ -137,5 +137,6 @@ $messages['namecannotbeempty'] = 'Name cannot be empty';
$messages['nametoolong'] = 'Name is too long';
$messages['folderupdated'] = 'Folder updated successfully';
$messages['foldercreated'] = 'Folder created successfully';
+$messages['invalidimageformat'] = 'Not a valid image format';
?>
diff --git a/program/steps/addressbook/copy.inc b/program/steps/addressbook/copy.inc
index 152add2e1..b891e012c 100644
--- a/program/steps/addressbook/copy.inc
+++ b/program/steps/addressbook/copy.inc
@@ -48,7 +48,7 @@ if ($cid && preg_match('/^[a-zA-Z0-9\+\/=_-]+(,[a-zA-Z0-9\+\/=_-]+)*$/', $cid) &
'record' => $a_record, 'source' => $target, 'group' => $target_group));
if (!$plugin['abort']) {
- if ($insert_id = $TARGET->insert($a_record, false)) {
+ if ($insert_id = $TARGET->insert($plugin['record'], false)) {
$ids[] = $insert_id;
$success++;
}
diff --git a/program/steps/addressbook/delete.inc b/program/steps/addressbook/delete.inc
index ea67746f4..1cd4f3550 100644
--- a/program/steps/addressbook/delete.inc
+++ b/program/steps/addressbook/delete.inc
@@ -38,6 +38,10 @@ if ($OUTPUT->ajax_call &&
// count contacts for this user
$result = $CONTACTS->count();
+ // update saved search after data changed
+ if (($search_request = $_REQUEST['_search']) && isset($_SESSION['search'][$search_request]))
+ $_SESSION['search'][$search_request] = $CONTACTS->refresh_search();
+
// update message count display
$OUTPUT->set_env('pagecount', ceil($result->count / $CONTACTS->page_size));
$OUTPUT->command('set_rowcount', rcmail_get_rowcount_text($result->count));
diff --git a/program/steps/addressbook/edit.inc b/program/steps/addressbook/edit.inc
index a65eafef1..747e12a3f 100644
--- a/program/steps/addressbook/edit.inc
+++ b/program/steps/addressbook/edit.inc
@@ -31,48 +31,143 @@ if ($CONTACTS->readonly) {
}
+function rcmail_contact_edithead($attrib)
+{
+ global $RCMAIL, $CONTACTS;
+
+ // check if we have a valid result
+ if ($RCMAIL->action != 'add'
+ && !(($result = $CONTACTS->get_result()) && ($record = $result->first()))
+ ) {
+ $RCMAIL->output->show_message('contactnotfound');
+ return false;
+ }
+
+ $i_size = !empty($attrib['size']) ? $attrib['size'] : 20;
+
+ $form = array(
+ 'head' => array(
+ 'content' => array(
+ 'prefix' => array('size' => $i_size),
+ 'firstname' => array('size' => $i_size, 'visible' => true),
+ 'middlename' => array('size' => $i_size),
+ 'surname' => array('size' => $i_size, 'visible' => true),
+ 'suffix' => array('size' => $i_size),
+ 'name' => array('size' => 2*$i_size),
+ 'nickname' => array('size' => 2*$i_size),
+ 'company' => array('size' => $i_size),
+ 'department' => array('size' => $i_size),
+ 'jobtitle' => array('size' => $i_size),
+ )
+ )
+ );
+
+ list($form_start, $form_end) = get_form_tags($attrib);
+ unset($attrib['form'], $attrib['name'], $attrib['size']);
+
+ // return the address edit form
+ $out = rcmail_contact_form($form, $record, $attrib);
+
+ return $form_start . $out . $form_end;
+}
+
+
function rcmail_contact_editform($attrib)
{
- global $RCMAIL, $CONTACTS, $OUTPUT;
+ global $RCMAIL, $CONTACTS, $CONTACT_COLTYPES;
// check if we have a valid result
if ($RCMAIL->action != 'add'
&& !(($result = $CONTACTS->get_result()) && ($record = $result->first()))
) {
- $OUTPUT->show_message('contactnotfound');
+ $RCMAIL->output->show_message('contactnotfound');
return false;
}
// add some labels to client
- $OUTPUT->add_label('noemailwarning', 'nonamewarning');
+ $RCMAIL->output->add_label('noemailwarning', 'nonamewarning');
$i_size = !empty($attrib['size']) ? $attrib['size'] : 40;
- $t_rows = !empty($attrib['textarearows']) ? $attrib['textarearows'] : 6;
+ $t_rows = !empty($attrib['textarearows']) ? $attrib['textarearows'] : 10;
$t_cols = !empty($attrib['textareacols']) ? $attrib['textareacols'] : 40;
$form = array(
'info' => array(
'name' => rcube_label('contactproperties'),
'content' => array(
- 'name' => array('type' => 'text', 'size' => $i_size),
- 'firstname' => array('type' => 'text', 'size' => $i_size),
- 'surname' => array('type' => 'text', 'size' => $i_size),
- 'email' => array('type' => 'text', 'size' => $i_size),
+ 'gender' => array('visible' => false),
+ 'maidenname' => array('size' => $i_size),
+ 'email' => array('size' => $i_size, 'visible' => true),
+ 'phone' => array('size' => $i_size, 'visible' => true),
+ 'address' => array('visible' => true),
+ 'birthday' => array('size' => 12),
+ 'anniversary' => array('size' => $i_size),
+ 'website' => array('size' => $i_size),
+ 'im' => array('size' => $i_size),
+ 'manager' => array('size' => $i_size),
+ 'assistant' => array('size' => $i_size),
+ 'spouse' => array('size' => $i_size),
),
),
);
-
+
+ if (isset($CONTACT_COLTYPES['notes'])) {
+ $form['notes'] = array(
+ 'name' => rcube_label('notes'),
+ 'content' => array(
+ 'notes' => array('size' => $t_cols, 'rows' => $t_rows, 'label' => false, 'visible' => true, 'limit' => 1),
+ ),
+ 'single' => true,
+ );
+ }
list($form_start, $form_end) = get_form_tags($attrib);
unset($attrib['form']);
// return the complete address edit form as table
- $out = rcmail_contact_form($form, $record);
+ $out = rcmail_contact_form($form, $record, $attrib);
return $form_start . $out . $form_end;
}
+function rcmail_upload_photo_form($attrib)
+{
+ global $OUTPUT;
+
+ // add ID if not given
+ if (!$attrib['id'])
+ $attrib['id'] = 'rcmUploadbox';
+
+ // find max filesize value
+ $max_filesize = parse_bytes(ini_get('upload_max_filesize'));
+ $max_postsize = parse_bytes(ini_get('post_max_size'));
+ if ($max_postsize && $max_postsize < $max_filesize)
+ $max_filesize = $max_postsize;
+ $max_filesize = show_bytes($max_filesize);
+
+ $hidden = new html_hiddenfield(array('name' => '_cid', 'value' => $GLOBALS['cid']));
+ $input = new html_inputfield(array('type' => 'file', 'name' => '_photo', 'size' => $attrib['size']));
+ $button = new html_inputfield(array('type' => 'button'));
+
+ $out = html::div($attrib,
+ $OUTPUT->form_tag(array('name' => 'uploadform', 'method' => 'post', 'enctype' => 'multipart/form-data'),
+ $hidden->show() .
+ html::div(null, $input->show()) .
+ html::div('hint', rcube_label(array('name' => 'maxuploadsize', 'vars' => array('size' => $max_filesize)))) .
+ html::div('buttons',
+ $button->show(rcube_label('close'), array('class' => 'button', 'onclick' => "$('#$attrib[id]').hide()")) . ' ' .
+ $button->show(rcube_label('upload'), array('class' => 'button mainaction', 'onclick' => JS_OBJECT_NAME . ".command('upload-photo', this.form)"))
+ )
+ )
+ );
+
+ $OUTPUT->add_label('addphoto','replacephoto');
+ $OUTPUT->add_gui_object('uploadbox', $attrib['id']);
+ return $out;
+}
+
+
// similar function as in /steps/settings/edit_identity.inc
function get_form_tags($attrib)
{
@@ -103,7 +198,10 @@ function get_form_tags($attrib)
}
+$OUTPUT->add_handler('contactedithead', 'rcmail_contact_edithead');
$OUTPUT->add_handler('contacteditform', 'rcmail_contact_editform');
+$OUTPUT->add_handler('contactphoto', 'rcmail_contact_photo');
+$OUTPUT->add_handler('photouploadform', 'rcmail_upload_photo_form');
if (!$CONTACTS->get_result() && $OUTPUT->template_exists('contactadd'))
$OUTPUT->send('contactadd');
diff --git a/program/steps/addressbook/export.inc b/program/steps/addressbook/export.inc
index 1b2e02944..509be596f 100644
--- a/program/steps/addressbook/export.inc
+++ b/program/steps/addressbook/export.inc
@@ -30,13 +30,24 @@ header('Content-Type: text/x-vcard; charset='.RCMAIL_CHARSET);
header('Content-Disposition: attachment; filename="rcube_contacts.vcf"');
while ($result && ($row = $result->next())) {
- $vcard = new rcube_vcard($row['vcard']);
- $vcard->set('displayname', $row['name']);
- $vcard->set('firstname', $row['firstname']);
- $vcard->set('surname', $row['surname']);
- $vcard->set('email', $row['email']);
-
- echo $vcard->export();
+ // we already have a vcard record
+ if ($row['vcard']) {
+ echo $row['vcard'];
+ }
+ // copy values into vcard object
+ else {
+ $vcard = new rcube_vcard($row['vcard']);
+ $vcard->reset();
+ foreach ($row as $key => $values) {
+ list($field, $section) = explode(':', $key);
+ foreach ((array)$values as $value) {
+ if (is_array($value) || strlen($value))
+ $vcard->set($field, $value, strtoupper($section));
+ }
+ }
+
+ echo $vcard->export();
+ }
}
exit;
diff --git a/program/steps/addressbook/func.inc b/program/steps/addressbook/func.inc
index e8b05edb4..e9b3dc8f2 100644
--- a/program/steps/addressbook/func.inc
+++ b/program/steps/addressbook/func.inc
@@ -56,6 +56,56 @@ if (!$OUTPUT->ajax_call) {
}
+// general definition of contact coltypes
+$CONTACT_COLTYPES = array(
+ 'name' => array('type' => 'text', 'size' => 40, 'limit' => 1, 'label' => rcube_label('name')),
+ 'firstname' => array('type' => 'text', 'size' => 19, 'limit' => 1, 'label' => rcube_label('firstname')),
+ 'surname' => array('type' => 'text', 'size' => 19, 'limit' => 1, 'label' => rcube_label('surname')),
+ 'middlename' => array('type' => 'text', 'size' => 19, 'limit' => 1, 'label' => rcube_label('middlename')),
+ 'prefix' => array('type' => 'text', 'size' => 8, 'limit' => 1, 'label' => rcube_label('nameprefix')),
+ 'suffix' => array('type' => 'text', 'size' => 8, 'limit' => 1, 'label' => rcube_label('namesuffix')),
+ 'nickname' => array('type' => 'text', 'size' => 40, 'limit' => 1, 'label' => rcube_label('nickname')),
+ 'jobtitle' => array('type' => 'text', 'size' => 40, 'limit' => 1, 'label' => rcube_label('jobtitle')),
+ 'organization' => array('type' => 'text', 'size' => 19, 'limit' => 1, 'label' => rcube_label('organization')),
+ 'department' => array('type' => 'text', 'size' => 19, 'limit' => 1, 'label' => rcube_label('department')),
+ 'gender' => array('type' => 'select', 'limit' => 1, 'label' => rcube_label('gender'), 'options' => array('male' => rcube_label('male'), 'female' => rcube_label('female'))),
+ 'maidenname' => array('type' => 'text', 'size' => 40, 'limit' => 1, 'label' => rcube_label('maidenname')),
+ 'email' => array('type' => 'text', 'size' => 40, 'label' => rcube_label('email'), 'subtypes' => array('home','work','other')),
+ 'phone' => array('type' => 'text', 'size' => 40, 'label' => rcube_label('phone'), 'subtypes' => array('home','home2','work','work2','mobile','main','homefax','workfax','car','pager','video','assistant','other')),
+ 'address' => array('type' => 'composite', 'label' => rcube_label('address'), 'subtypes' => array('home','work','other'), 'childs' => array(
+ 'street' => array('type' => 'text', 'size' => 40, 'label' => rcube_label('street')),
+ 'locality' => array('type' => 'text', 'size' => 28, 'label' => rcube_label('locality')),
+ 'zipcode' => array('type' => 'text', 'size' => 8, 'label' => rcube_label('zipcode')),
+ 'region' => array('type' => 'text', 'size' => 12, 'label' => rcube_label('region')),
+ 'country' => array('type' => 'text', 'size' => 40, 'label' => rcube_label('country')),
+ )),
+ 'birthday' => array('type' => 'date', 'size' => 12, 'label' => rcube_label('birthday'), 'limit' => 1, 'render_func' => 'rcmail_format_date_col'),
+ 'anniversary' => array('type' => 'date', 'size' => 12, 'label' => rcube_label('anniversary'), 'limit' => 1, 'render_func' => 'rcmail_format_date_col'),
+ 'website' => array('type' => 'text', 'size' => 40, 'label' => rcube_label('website'), 'subtypes' => array('homepage','work','blog','other')),
+ 'im' => array('type' => 'text', 'size' => 40, 'label' => rcube_label('instantmessenger'), 'subtypes' => array('aim','icq','msn','yahoo','jabber','skype','other')),
+ 'notes' => array('type' => 'textarea', 'size' => 40, 'rows' => 15, 'label' => rcube_label('notes'), 'limit' => 1),
+ 'photo' => array('type' => 'image', 'limit' => 1),
+ 'assistant' => array('type' => 'text', 'size' => 40, 'limit' => 1, 'label' => rcube_label('assistant')),
+ 'manager' => array('type' => 'text', 'size' => 40, 'limit' => 1, 'label' => rcube_label('manager')),
+ 'spouse' => array('type' => 'text', 'size' => 40, 'limit' => 1, 'label' => rcube_label('spouse')),
+ // TODO: define fields for vcards like GEO, KEY
+);
+
+// reduce/extend $CONTACT_COLTYPES with specification from the current $CONTACT object
+if (is_array($CONTACTS->coltypes)) {
+ // remove cols not listed by the backend class
+ $contact_cols = $CONTACTS->coltypes[0] ? array_flip($CONTACTS->coltypes) : $CONTACTS->coltypes;
+ $CONTACT_COLTYPES = array_intersect_key($CONTACT_COLTYPES, $contact_cols);
+ // add associative coltypes definition
+ if (!$CONTACTS->coltypes[0]) {
+ foreach ($CONTACTS->coltypes as $col => $colprop)
+ $CONTACT_COLTYPES[$col] = $CONTACT_COLTYPES[$col] ? array_merge($CONTACT_COLTYPES[$col], $colprop) : $colprop;
+ }
+}
+
+$OUTPUT->set_env('photocol', is_array($CONTACT_COLTYPES['photo']));
+
+
function rcmail_directory_list($attrib)
{
global $RCMAIL, $OUTPUT;
@@ -72,23 +122,21 @@ function rcmail_directory_list($attrib)
html::a(array('href' => '%s',
'onclick' => "return ".JS_OBJECT_NAME.".command('list','%s',this)"), '%s'));
- if (!$current && strtolower($RCMAIL->config->get('address_book_type', 'sql')) != 'ldap') {
- $current = '0';
- }
- else if (!$current) {
- // DB address book not used, see if a source is set, if not use the
- // first LDAP directory.
- $current = key((array)$RCMAIL->config->get('ldap_public', array()));
- }
+ // currently selected is the first address source in the list
+ if (!isset($current))
+ $current = strval(key((array)$OUTPUT->env['address_sources']));
foreach ((array)$OUTPUT->env['address_sources'] as $j => $source) {
- $id = $source['id'] ? $source['id'] : $j;
+ $id = strval($source['id'] ? $source['id'] : $j);
$js_id = JQ($id);
- $dom_id = preg_replace('/[^a-z0-9\-_]/i', '', $id);
- $out .= sprintf($line_templ, $dom_id, ($current == $id ? 'selected' : ''),
+ $dom_id = preg_replace('/[^a-z0-9\-_]/i', '_', $id);
+ $out .= sprintf($line_templ, $dom_id, ($current === $id ? 'selected' : ''),
Q(rcmail_url(null, array('_source' => $id))),
$js_id, (!empty($source['name']) ? Q($source['name']) : Q($id)));
- $groupdata = rcmail_contact_groups(array('out' => $out, 'jsdata' => $jsdata, 'source' => $id));
+
+ $groupdata = array('out' => $out, 'jsdata' => $jsdata, 'source' => $id);
+ if ($source['groups'])
+ $groupdata = rcmail_contact_groups($groupdata);
$jsdata = $groupdata['jsdata'];
$out = $groupdata['out'];
}
@@ -130,16 +178,16 @@ function rcmail_contacts_list($attrib)
{
global $CONTACTS, $OUTPUT;
+ // define list of cols to be displayed
+ $a_show_cols = array('name');
+
// count contacts for this user
- $result = $CONTACTS->list_records();
+ $result = $CONTACTS->list_records($a_show_cols);
// add id to message list table if not specified
if (!strlen($attrib['id']))
$attrib['id'] = 'rcmAddressList';
- // define list of cols to be displayed
- $a_show_cols = array('name');
-
// create XHTML table
$out = rcube_table_output($attrib, $result->records, $a_show_cols, $CONTACTS->primary_key);
@@ -233,9 +281,9 @@ function rcmail_get_rowcount_text()
}
-function rcmail_contact_form($form, $record)
+function rcmail_contact_form($form, $record, $attrib = null)
{
- global $RCMAIL;
+ global $RCMAIL, $CONFIG;
// Allow plugins to modify contact form content
$plugin = $RCMAIL->plugins->exec_hook('contact_form', array(
@@ -243,35 +291,222 @@ function rcmail_contact_form($form, $record)
$form = $plugin['form'];
$record = $plugin['record'];
+ $edit_mode = $RCMAIL->action != 'show';
+ $del_button = $attrib['deleteicon'] ? html::img(array('src' => $CONFIG['skin_path'] . $attrib['deleteicon'], 'alt' => rcube_label('delete'))) : rcube_label('delete');
+ unset($attrib['deleteicon']);
$out = '';
+
+ // get default coltypes
+ $coltypes = $GLOBALS['CONTACT_COLTYPES'];
+ $coltype_lables = array();
+
+ foreach ($coltypes as $col => $prop) {
+ if ($prop['subtypes']) {
+ $select_subtype = new html_select(array('name' => '_subtype_'.$col.'[]', 'class' => 'contactselectsubtype'));
+ $select_subtype->add($prop['subtypes']);
+ $coltypes[$col]['subtypes_select'] = $select_subtype->show();
+ }
+ if ($prop['childs']) {
+ foreach ($prop['childs'] as $childcol => $cp)
+ $coltype_lables[$childcol] = array('label' => $cp['label']);
+ }
+ }
- foreach ($form as $fieldset) {
+ foreach ($form as $section => $fieldset) {
+ // skip empty sections
if (empty($fieldset['content']))
continue;
+ $select_add = new html_select(array('class' => 'addfieldmenu', 'rel' => $section));
+ $select_add->add(rcube_label('addfield'), '');
+
+ // render head section with name fields (not a regular list of rows)
+ if ($section == 'head') {
+ $content = '';
+
+ $names_arr = array($record['prefix'], $record['firstname'], $record['middlename'], $record['surname'], $record['suffix']);
+ if ($record['name'] == join(' ', array_filter($names_arr)))
+ unset($record['name']);
+
+ // group fields
+ $field_blocks = array(
+ 'names' => array('prefix','firstname','middlename','surname','suffix'),
+ 'displayname' => array('name'),
+ 'nickname' => array('nickname'),
+ 'jobnames' => array('organization','department','jobtitle'),
+ );
+ foreach ($field_blocks as $blockname => $colnames) {
+ $fields = '';
+ foreach ($colnames as $col) {
+ // skip cols unknown to the backend
+ if (!$coltypes[$col])
+ continue;
+
+ if ($RCMAIL->action == 'show') {
+ if (!empty($record[$col]))
+ $fields .= html::span('namefield ' . $col, Q($record[$col])) . " ";
+ }
+ else {
+ $colprop = (array)$fieldset['content'][$col] + (array)$coltypes[$col];
+ $colprop['id'] = 'ff_'.$col;
+ if (empty($record[$col]) && !$colprop['visible']) {
+ $colprop['style'] = 'display:none';
+ $select_add->add($colprop['label'], $col);
+ }
+ $fields .= rcmail_get_edit_field($col, $record[$col], $colprop, $colprop['type']);
+ }
+ }
+ $content .= html::div($blockname, $fields);
+ }
+
+ if ($edit_mode)
+ $content .= html::p('addfield', $select_add->show(null));
+
+ $out .= html::tag('fieldset', $attrib, (!empty($fieldset['name']) ? html::tag('legend', null, Q($fieldset['name'])) : '') . $content) ."\n";
+ continue;
+ }
+
$content = '';
if (is_array($fieldset['content'])) {
- $table = new html_table(array('cols' => 2));
-
foreach ($fieldset['content'] as $col => $colprop) {
- $colprop['id'] = 'rcmfd_'.$col;
-
- $label = !empty($colprop['label']) ? $colprop['label'] : rcube_label($col);
+ // remove subtype part of col name
+ list($field, $subtype) = explode(':', $col);
+ if (!$subtype) $subtype = 'home';
+ $fullkey = $col.':'.$subtype;
+
+ // skip cols unknown to the backend
+ if (!$coltypes[$field])
+ continue;
+
+ // merge colprop with global coltype configuration
+ $colprop += $coltypes[$field];
+ $label = isset($colprop['label']) ? $colprop['label'] : rcube_label($col);
+
+ // prepare subtype selector in edit mode
+ if ($edit_mode && is_array($colprop['subtypes'])) {
+ $select_subtype = new html_select(array('name' => '_subtype_'.$col.'[]', 'class' => 'contactselectsubtype'));
+ $select_subtype->add($colprop['subtypes']);
+ }
+ else
+ $select_subtype = null;
if (!empty($colprop['value'])) {
- $value = $colprop['value'];
- }
- else if ($RCMAIL->action == 'show') {
- $value = $record[$col];
+ $values = (array)$colprop['value'];
}
else {
- $value = rcmail_get_edit_field($col, $record[$col], $colprop, $colprop['type']);
+ // iterate over possible subtypes and collect values with their subtype
+ if (is_array($colprop['subtypes'])) {
+ $values = $subtypes = array();
+ foreach ($colprop['subtypes'] as $i => $st) {
+ $newval = false;
+ if ($record[$field.':'.$st]) {
+ $subtypes[count($values)] = $st;
+ $newval = $record[$field.':'.$st];
+ }
+ else if ($i == 0 && $record[$field]) {
+ $subtypes[count($values)] = $st;
+ $newval = $record[$field];
+ }
+ if ($newval !== false) {
+ if (is_array($newval) && isset($newval[0]))
+ $values = array_merge($values, $newval);
+ else
+ $values[] = $newval;
+ }
+ }
+ }
+ else {
+ $values = $record[$fullkey] ? $record[$fullkey] : $record[$field];
+ $subtypes = null;
+ }
}
- $table->add('title', sprintf('<label for="%s">%s</label>', $colprop['id'], Q($label)));
- $table->add(null, $value);
+ // hack: create empty values array to force this field to be displayed
+ if (empty($values) && $colprop['visible'])
+ $values[] = '';
+
+ $rows = '';
+ foreach ((array)$values as $i => $val) {
+ if ($subtypes[$i])
+ $subtype = $subtypes[$i];
+
+ // render composite field
+ if ($colprop['type'] == 'composite') {
+ $composite = array(); $j = 0;
+ $template = $RCMAIL->config->get($col . '_template', '{'.join('} {', array_keys($colprop['childs'])).'}');
+ foreach ($colprop['childs'] as $childcol => $cp) {
+ $childvalue = $val[$childcol] ? $val[$childcol] : $val[$j];
+
+ if ($edit_mode) {
+ if ($colprop['subtypes'] || $colprop['limit'] != 1) $cp['array'] = true;
+ $composite['{'.$childcol.'}'] = rcmail_get_edit_field($childcol, $childvalue, $cp, $cp['type']) . " ";
+ }
+ else {
+ $childval = $cp['render_func'] ? call_user_func($cp['render_func'], $childvalue, $childcol) : Q($childvalue);
+ $composite['{'.$childcol.'}'] = html::span('data ' . $childcol, $childval) . " ";
+ }
+ $j++;
+ }
+
+ $coltypes[$field] += (array)$colprop;
+ $coltypes[$field]['count']++;
+ $val = strtr($template, $composite);
+ }
+ else if ($edit_mode) {
+ // call callback to render/format value
+ if ($colprop['render_func'])
+ $val = call_user_func($colprop['render_func'], $val, $col);
+
+ $coltypes[$field] = (array)$colprop + $coltypes[$field];
+
+ if ($colprop['subtypes'] || $colprop['limit'] != 1)
+ $colprop['array'] = true;
+
+ $val = rcmail_get_edit_field($col, $val, $colprop, $colprop['type']);
+ $coltypes[$field]['count']++;
+ }
+ else if ($colprop['render_func'])
+ $val = call_user_func($colprop['render_func'], $val, $col);
+ else if (is_array($colprop['options']) && isset($colprop['options'][$val]))
+ $val = $colprop['options'][$val];
+ else
+ $val = Q($val);
+
+ // use subtype as label
+ if ($colprop['subtypes'])
+ $label = $subtype;
+
+ // add delete button/link
+ if ($edit_mode && !($colprop['visible'] && $colprop['limit'] == 1))
+ $val .= html::a(array('href' => '#del', 'class' => 'contactfieldbutton deletebutton', 'title' => rcube_label('delete'), 'rel' => $col), $del_button);
+
+ // display row with label
+ if ($label) {
+ $rows .= html::div('row',
+ html::div('contactfieldlabel label', $select_subtype ? $select_subtype->show($subtype) : Q($label)) .
+ html::div('contactfieldcontent '.$colprop['type'], $val));
+ }
+ else // row without label
+ $rows .= html::div('row', html::div('contactfield', $val));
+ }
+
+ // add option to the add-field menu
+ if (!$colprop['limit'] || $coltypes[$field]['count'] < $colprop['limit']) {
+ $select_add->add($colprop['label'], $col);
+ $select_add->_count++;
+ }
+
+ // wrap rows in fieldgroup container
+ $content .= html::tag('fieldset', array('class' => 'contactfieldgroup contactcontroller' . $col, 'style' => ($rows ? null : 'display:none')),
+ ($colprop['subtypes'] ? html::tag('legend', null, Q($colprop['label'])) : ' ') .
+ $rows);
}
- $content = $table->show();
+
+ // also render add-field selector
+ if ($edit_mode)
+ $content .= html::p('addfield', $select_add->show(null, array('style' => $select_add->_count ? null : 'display:none')));
+
+ $content = html::div(array('id' => 'contactsection' . $section), $content);
}
else {
$content = $fieldset['content'];
@@ -279,11 +514,53 @@ function rcmail_contact_form($form, $record)
$out .= html::tag('fieldset', null, html::tag('legend', null, Q($fieldset['name'])) . $content) ."\n";
}
-
+
+ if ($edit_mode) {
+ $RCMAIL->output->set_env('coltypes', $coltypes + $coltype_lables);
+ $RCMAIL->output->set_env('delbutton', $del_button);
+ $RCMAIL->output->add_label('delete');
+ }
+
return $out;
}
+function rcmail_contact_photo($attrib)
+{
+ global $CONTACTS, $CONTACT_COLTYPES, $RCMAIL, $CONFIG;
+
+ if ($result = $CONTACTS->get_result())
+ $record = $result->first();
+
+ $photo_img = $attrib['placeholder'] ? $CONFIG['skin_path'] . $attrib['placeholder'] : 'program/blank.gif';
+ unset($attrib['placeholder']);
+
+ if ($CONTACT_COLTYPES['photo']) {
+ $RCMAIL->output->set_env('photo_placeholder', $photo_img);
+
+ if ($record['photo'])
+ $photo_img = $RCMAIL->url(array('_action' => 'photo', '_cid' => $record['ID'], '_source' => $_REQUEST['_source']));
+ $img = html::img(array('src' => $photo_img, 'border' => 1, 'alt' => ''));
+ $content = html::div($attrib, $img);
+
+ if ($RCMAIL->action == 'edit' || $RCMAIL->action == 'add') {
+ $RCMAIL->output->add_gui_object('contactphoto', $attrib['id']);
+ $hidden = new html_hiddenfield(array('name' => '_photo', 'id' => 'ff_photo'));
+ $content .= $hidden->show();
+ }
+ }
+
+ return $content;
+}
+
+
+function rcmail_format_date_col($val)
+{
+ global $RCMAIL;
+ return format_date($val, $RCMAIL->config->get('date_format', 'Y-m-d'));
+}
+
+
// register UI objects
$OUTPUT->add_handlers(array(
'directorylist' => 'rcmail_directory_list',
@@ -297,6 +574,8 @@ $OUTPUT->add_handlers(array(
// register action aliases
$RCMAIL->register_action_map(array(
'add' => 'edit.inc',
+ 'photo' => 'show.inc',
+ 'upload-photo' => 'save.inc',
'group-create' => 'groups.inc',
'group-rename' => 'groups.inc',
'group-delete' => 'groups.inc',
diff --git a/program/steps/addressbook/groups.inc b/program/steps/addressbook/groups.inc
index b7fdb2fff..b70bbf265 100644
--- a/program/steps/addressbook/groups.inc
+++ b/program/steps/addressbook/groups.inc
@@ -79,8 +79,7 @@ else if ($RCMAIL->action == 'group-create') {
if ($created && $OUTPUT->ajax_call) {
$OUTPUT->show_message('groupcreated', 'confirmation');
- $OUTPUT->command('insert_contact_group', array(
- 'source' => $source, 'id' => $created['id'], 'name' => $created['name']));
+ $OUTPUT->command('insert_contact_group', array('source' => $source) + $created);
}
else if (!$created) {
$OUTPUT->show_message($plugin['message'] ? $plugin['message'] : 'errorsaving', 'error');
diff --git a/program/steps/addressbook/import.inc b/program/steps/addressbook/import.inc
index 61b757f43..532afdbcd 100644
--- a/program/steps/addressbook/import.inc
+++ b/program/steps/addressbook/import.inc
@@ -150,13 +150,8 @@ if ($_FILES['_file']['tmp_name'] && is_uploaded_file($_FILES['_file']['tmp_name'
}
}
- $a_record = array(
- 'name' => $vcard->displayname,
- 'firstname' => $vcard->firstname,
- 'surname' => $vcard->surname,
- 'email' => $email,
- 'vcard' => $vcard->export(),
- );
+ $a_record = $vcard->get_assoc();
+ $a_record['vcard'] = $vcard->export();
$plugin = $RCMAIL->plugins->exec_hook('contact_create', array('record' => $a_record, 'source' => null));
$a_record = $plugin['record'];
diff --git a/program/steps/addressbook/list.inc b/program/steps/addressbook/list.inc
index 018f6e2a6..234e1a633 100644
--- a/program/steps/addressbook/list.inc
+++ b/program/steps/addressbook/list.inc
@@ -20,7 +20,7 @@
*/
// get contacts for this user
-$result = $CONTACTS->list_records();
+$result = $CONTACTS->list_records(array('name'));
// update message count display
$OUTPUT->set_env('pagecount', ceil($result->count / $CONTACTS->page_size));
diff --git a/program/steps/addressbook/mailto.inc b/program/steps/addressbook/mailto.inc
index d38ae9e62..702e1a61b 100644
--- a/program/steps/addressbook/mailto.inc
+++ b/program/steps/addressbook/mailto.inc
@@ -29,8 +29,10 @@ if ($cid && preg_match('/^[a-z0-9\+\/=_-]+(,[a-z0-9\+\/=_-]+)*$/i', $cid) && $CO
$CONTACTS->set_pagesize(100);
$recipients = $CONTACTS->search($CONTACTS->primary_key, $cid);
- while (is_object($recipients) && ($rec = $recipients->iterate()))
- $mailto[] = format_email_recipient($rec['email'], $rec['name']);
+ while (is_object($recipients) && ($rec = $recipients->iterate())) {
+ $emails = $CONTACTS->get_col_values('email', $rec, true);
+ $mailto[] = format_email_recipient($emails[0], $rec['name']);
+ }
}
if (!empty($mailto))
diff --git a/program/steps/addressbook/save.inc b/program/steps/addressbook/save.inc
index add8ef4e3..7d29b6fb2 100644
--- a/program/steps/addressbook/save.inc
+++ b/program/steps/addressbook/save.inc
@@ -5,7 +5,7 @@
| program/steps/addressbook/save.inc |
| |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2009, The Roundcube Dev Team |
+ | Copyright (C) 2005-2011, The Roundcube Dev Team |
| Licensed under the GNU GPL |
| |
| PURPOSE: |
@@ -29,33 +29,147 @@ if ($CONTACTS->readonly) {
return;
}
-// Basic input checks
-if ((!get_input_value('_name', RCUBE_INPUT_POST) || !get_input_value('_email', RCUBE_INPUT_POST))) {
- $OUTPUT->show_message('formincomplete', 'warning');
- rcmail_overwrite_action($return_action);
- return;
-}
+// handle photo upload for contacts
+if ($RCMAIL->action == 'upload-photo') {
+ // clear all stored output properties (like scripts and env vars)
+ $OUTPUT->reset();
+
+ if ($filepath = $_FILES['_photo']['tmp_name']) {
+ // check file type and resize image
+ $imageprop = rcmail::imageprops($_FILES['_photo']['tmp_name']);
+
+ if ($imageprop['width'] && $imageprop['height']) {
+ $maxsize = intval($RCMAIL->config->get('contact_photo_size', 160));
+ $tmpfname = tempnam($RCMAIL->config->get('temp_dir'), 'rcmImgConvert');
+ $save_hook = 'attachment_upload';
+
+ // scale image to a maximum size
+ if (($imageprop['width'] > $maxsize || $imageprop['height'] > $maxsize) &&
+ (rcmail::imageconvert(array('in' => $filepath, 'out' => $tmpfname, 'size' => $maxsize.'x'.$maxsize, 'type' => $imageprop['type'])) !== false)) {
+ $filepath = $tmpfname;
+ $save_hook = 'attachment_save';
+ }
+
+ // save uploaded file in storage backend
+ $attachment = $RCMAIL->plugins->exec_hook($save_hook, array(
+ 'path' => $filepath,
+ 'size' => $_FILES['_photo']['size'],
+ 'name' => $_FILES['_photo']['name'],
+ 'mimetype' => 'image/' . $imageprop['type'],
+ ));
+ }
+ else
+ $attachment['error'] = rcube_label('invalidimageformat');
+
+ if ($attachment['status'] && !$attachment['abort']) {
+ $file_id = $attachment['id'];
+ $_SESSION['contacts']['files'][$file_id] = $attachment;
+ $OUTPUT->command('replace_contact_photo', $file_id);
+ }
+ else { // upload failed
+ $err = $_FILES['_photo']['error'];
+ if ($err == UPLOAD_ERR_INI_SIZE || $err == UPLOAD_ERR_FORM_SIZE)
+ $msg = rcube_label(array('name' => 'filesizeerror', 'vars' => array('size' => show_bytes(parse_bytes(ini_get('upload_max_filesize'))))));
+ else if ($attachment['error'])
+ $msg = $attachment['error'];
+ else
+ $msg = rcube_label('fileuploaderror');
+
+ $OUTPUT->command('display_message', $msg, 'error');
+ }
+ }
+ else if ($_SERVER['REQUEST_METHOD'] == 'POST') {
+ // if filesize exceeds post_max_size then $_FILES array is empty,
+ // show filesizeerror instead of fileuploaderror
+ if ($maxsize = ini_get('post_max_size'))
+ $msg = rcube_label(array('name' => 'filesizeerror', 'vars' => array('size' => show_bytes(parse_bytes($maxsize)))));
+ else
+ $msg = rcube_label('fileuploaderror');
+
+ $OUTPUT->command('display_message', $msg, 'error');
+ }
+
+ $OUTPUT->command('photo_upload_end');
+ $OUTPUT->send('iframe');
+}
-// setup some vars we need
-$a_save_cols = array('name', 'firstname', 'surname', 'email');
-$a_record = array();
// read POST values into hash array
-foreach ($a_save_cols as $col) {
+$a_record = array();
+foreach ($GLOBALS['CONTACT_COLTYPES'] as $col => $colprop) {
$fname = '_'.$col;
- if (isset($_POST[$fname]))
+ if ($colprop['composite'])
+ continue;
+ // gather form data of composite fields
+ if ($colprop['childs']) {
+ $values = array();
+ foreach ($colprop['childs'] as $childcol => $cp) {
+ $vals = get_input_value('_'.$childcol, RCUBE_INPUT_POST);
+ foreach ((array)$vals as $i => $val)
+ $values[$i][$childcol] = $val;
+ }
+ $subtypes = get_input_value('_subtype_' . $col, RCUBE_INPUT_POST);
+ foreach ($subtypes as $i => $subtype)
+ if ($values[$i])
+ $a_record[$col.':'.$subtype][] = $values[$i];
+ }
+ // assign values and subtypes
+ else if (is_array($_POST[$fname])) {
+ $values = get_input_value($fname, RCUBE_INPUT_POST);
+ $subtypes = get_input_value('_subtype_' . $col, RCUBE_INPUT_POST);
+ foreach ($values as $i => $val) {
+ $subtype = $subtypes[$i] ? ':'.$subtypes[$i] : '';
+ $a_record[$col.$subtype][] = $val;
+ }
+ }
+ else if (isset($_POST[$fname])) {
$a_record[$col] = get_input_value($fname, RCUBE_INPUT_POST);
+ }
}
-// Validity checks
-$_email = idn_to_ascii($a_record['email']);
-if (!check_email($_email, false)) {
- $OUTPUT->show_message('emailformaterror', 'warning', array('email' => $_email));
+if (empty($a_record['name']))
+ $a_record['name'] = join(' ', array_filter(array($a_record['prefix'], $a_record['firstname'], $a_record['middlename'], $a_record['surname'], $a_record['suffix'],)));
+
+#var_dump($a_record);
+
+// Basic input checks (TODO: delegate to $CONTACTS instance)
+if (empty($a_record['name'])/* || empty($a_record['email'])*/) {
+ $OUTPUT->show_message('formincomplete', 'warning');
rcmail_overwrite_action($return_action);
return;
}
+// Validity checks
+foreach ($CONTACTS->get_col_values('email', $a_record, true) as $email) {
+ if (strlen($email)) {
+ $_email = idn_to_ascii($email);
+ if (!check_email($_email, false)) {
+ $OUTPUT->show_message('emailformaterror', 'warning', array('email' => $email));
+ rcmail_overwrite_action($return_action);
+ return;
+ }
+ }
+}
+
+// get raw photo data if changed
+if (isset($a_record['photo'])) {
+ if ($a_record['photo'] == '-del-') {
+ $a_record['photo'] = '';
+ }
+ else if ($tempfile = $_SESSION['contacts']['files'][$a_record['photo']]) {
+ $tempfile = $RCMAIL->plugins->exec_hook('attachment_get', $tempfile);
+ if ($tempfile['status'])
+ $a_record['photo'] = $tempfile['data'] ? $tempfile['data'] : @file_get_contents($tempfile['path']);
+ }
+ else
+ unset($a_record['photo']);
+
+ // cleanup session data
+ $RCMAIL->plugins->exec_hook('attachments_cleanup', array());
+ $RCMAIL->session->remove('contacts');
+}
+
// update an existing contact
if (!empty($cid))
{
@@ -92,7 +206,8 @@ if (!empty($cid))
}
else {
// show error message
- $OUTPUT->show_message($plugin['message'] ? $plugin['message'] : 'errorsaving', 'error', null, false);
+ $err = $CONTACTS->get_error();
+ $OUTPUT->show_message($plugin['message'] ? $plugin['message'] : ($err['message'] ? $err['message'] : 'errorsaving'), 'error', null, false);
rcmail_overwrite_action('show');
}
}
@@ -100,10 +215,16 @@ if (!empty($cid))
// insert a new contact
else {
// check for existing contacts
- $existing = $CONTACTS->search('email', $a_record['email'], true, false);
+ $existing = false;
+ foreach ($CONTACTS->get_col_values('email', $a_record, true) as $email) {
+ if (($res = $CONTACTS->search('email', $email, true, false)) && $res->count) {
+ $existing = true;
+ break;
+ }
+ }
// show warning message
- if ($existing->count) {
+ if ($existing) {
$OUTPUT->show_message('contactexists', 'warning', null, false);
rcmail_overwrite_action('add');
return;
@@ -138,7 +259,8 @@ else {
}
else {
// show error message
- $OUTPUT->show_message($plugin['message'] ? $plugin['message'] : 'errorsaving', 'error', null, false);
+ $err = $CONTACTS->get_error();
+ $OUTPUT->show_message($plugin['message'] ? $plugin['message'] : ($err['message'] ? $err['message'] : 'errorsaving'), 'error', null, false);
rcmail_overwrite_action('add');
}
}
diff --git a/program/steps/addressbook/search.inc b/program/steps/addressbook/search.inc
index 8cacbd9b1..9e40abad7 100644
--- a/program/steps/addressbook/search.inc
+++ b/program/steps/addressbook/search.inc
@@ -5,7 +5,7 @@
| program/steps/addressbook/search.inc |
| |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2007, The Roundcube Dev Team |
+ | Copyright (C) 2005-2011, The Roundcube Dev Team |
| Licensed under the GNU GPL |
| |
| PURPOSE: |
@@ -28,11 +28,11 @@ $search_request = md5('addr'.$search);
// get contacts for this user
$result = $CONTACTS->search(array('name','email'), $search);
+// save search settings in session
+$_SESSION['search'][$search_request] = $CONTACTS->get_search_set();
+
if ($result->count > 0)
{
- // save search settings in session
- $_SESSION['search'][$search_request] = $CONTACTS->get_search_set();
-
// create javascript list
rcmail_js_contacts_list($result);
}
diff --git a/program/steps/addressbook/show.inc b/program/steps/addressbook/show.inc
index 43ded2a6e..eb26450e6 100644
--- a/program/steps/addressbook/show.inc
+++ b/program/steps/addressbook/show.inc
@@ -25,8 +25,30 @@ if (($cid = get_input_value('_cid', RCUBE_INPUT_GPC)) && ($record = $CONTACTS->g
$OUTPUT->set_env('cid', $record['ID']);
}
+// return raw photo of the given contact
+if ($RCMAIL->action == 'photo') {
+ if (($file_id = get_input_value('_photo', RCUBE_INPUT_GPC)) && ($tempfile = $_SESSION['contacts']['files'][$file_id])) {
+ $tempfile = $RCMAIL->plugins->exec_hook('attachment_display', $tempfile);
+ if ($tempfile['status']) {
+ if ($tempfile['data'])
+ $data = $tempfile['data'];
+ else if ($tempfile['path'])
+ $data = file_get_contents($tempfile['path']);
+ }
+ }
+ else if ($record['photo']) {
+ $data = is_array($record['photo']) ? $record['photo'][0] : $record['photo'];
+ if (!preg_match('![^a-z0-9/=+-]!i', $data))
+ $data = base64_decode($data, true);
+ }
+
+ header('Content-Type: ' . rc_image_content_type($data));
+ echo $data ? $data : file_get_contents('program/blank.gif');
+ exit;
+}
-function rcmail_contact_details($attrib)
+
+function rcmail_contact_head($attrib)
{
global $CONTACTS, $RCMAIL;
@@ -36,54 +58,99 @@ function rcmail_contact_details($attrib)
return false;
}
- $i_size = !empty($attrib['size']) ? $attrib['size'] : 40;
- $t_rows = !empty($attrib['textarearows']) ? $attrib['textarearows'] : 6;
- $t_cols = !empty($attrib['textareacols']) ? $attrib['textareacols'] : 40;
-
$microformats = array('name' => 'fn', 'email' => 'email');
$form = array(
- 'info' => array(
- 'name' => rcube_label('contactproperties'),
+ 'head' => array( // section 'head' is magic!
'content' => array(
- 'name' => array('type' => 'text', 'size' => $i_size),
- 'firstname' => array('type' => 'text', 'size' => $i_size),
- 'surname' => array('type' => 'text', 'size' => $i_size),
- 'email' => array('type' => 'text', 'size' => $i_size),
+ 'prefix' => array('type' => 'text'),
+ 'firstname' => array('type' => 'text'),
+ 'middlename' => array('type' => 'text'),
+ 'surname' => array('type' => 'text'),
+ 'suffix' => array('type' => 'text'),
),
),
- 'groups' => array(
- 'name' => rcube_label('groups'),
- 'content' => '',
- ),
);
- // Get content of groups fieldset
- if ($groups = rcmail_contact_record_groups($record['ID'])) {
- $form['groups']['content'] = $groups;
- }
- else {
- unset($form['groups']);
+ unset($attrib['name']);
+ return rcmail_contact_form($form, $record, $attrib);
+}
+
+
+function rcmail_contact_details($attrib)
+{
+ global $CONTACTS, $RCMAIL, $CONTACT_COLTYPES;
+
+ // check if we have a valid result
+ if (!(($result = $CONTACTS->get_result()) && ($record = $result->first()))) {
+ //$RCMAIL->output->show_message('contactnotfound');
+ return false;
}
- if (!empty($record['email'])) {
- $form['info']['content']['email']['value'] = html::a(array(
- 'href' => 'mailto:' . $record['email'],
- 'onclick' => sprintf("return %s.command('compose','%s',this)", JS_OBJECT_NAME, JQ($record['email'])),
- 'title' => rcube_label('composeto'),
- 'class' => $microformats['email'],
- ), Q($record['email']));
+ $i_size = !empty($attrib['size']) ? $attrib['size'] : 40;
+
+ $form = array(
+ 'info' => array(
+ 'name' => rcube_label('contactproperties'),
+ 'content' => array(
+ 'gender' => array('size' => $i_size),
+ 'maidenname' => array('size' => $i_size),
+ 'email' => array('size' => $i_size, 'render_func' => 'rcmail_render_email_value'),
+ 'phone' => array('size' => $i_size),
+ 'address' => array(),
+ 'birthday' => array('size' => $i_size),
+ 'anniversary' => array('size' => $i_size),
+ 'website' => array('size' => $i_size, 'render_func' => 'rcmail_render_url_value'),
+ 'im' => array('size' => $i_size),
+ 'manager' => array('size' => $i_size),
+ 'assistant' => array('size' => $i_size),
+ 'spouse' => array('size' => $i_size),
+ ),
+ ),
+ );
+
+ if (isset($CONTACT_COLTYPES['notes'])) {
+ $form['notes'] = array(
+ 'name' => rcube_label('notes'),
+ 'content' => array(
+ 'notes' => array('type' => 'textarea', 'label' => false),
+ ),
+ );
}
- foreach (array('name', 'firstname', 'surname') as $col) {
- if ($record[$col]) {
- $form['info']['content'][$col]['value'] = html::span($microformats[$col], Q($record[$col]));
- }
+
+ if ($CONTACTS->groups) {
+ $form['groups'] = array(
+ 'name' => rcube_label('groups'),
+ 'content' => rcmail_contact_record_groups($record['ID']),
+ );
}
return rcmail_contact_form($form, $record);
}
+function rcmail_render_email_value($email, $col)
+{
+ return html::a(array(
+ 'href' => 'mailto:' . $email,
+ 'onclick' => sprintf("return %s.command('compose','%s',this)", JS_OBJECT_NAME, JQ($email)),
+ 'title' => rcube_label('composeto'),
+ 'class' => 'email',
+ ), Q($email));
+}
+
+
+function rcmail_render_url_value($url, $col)
+{
+ $prefix = preg_match('![htfps]+://!', $url) ? '' : 'http://';
+ return html::a(array(
+ 'href' => $prefix . $url,
+ 'target' => '_blank',
+ 'class' => 'url',
+ ), Q($url));
+}
+
+
function rcmail_contact_record_groups($contact_id)
{
global $RCMAIL, $CONTACTS, $GROUPS;
@@ -124,6 +191,8 @@ function rcmail_contact_record_groups($contact_id)
//$OUTPUT->framed = $_framed;
+$OUTPUT->add_handler('contacthead', 'rcmail_contact_head');
$OUTPUT->add_handler('contactdetails', 'rcmail_contact_details');
+$OUTPUT->add_handler('contactphoto', 'rcmail_contact_photo');
$OUTPUT->send('contact');
diff --git a/program/steps/mail/autocomplete.inc b/program/steps/mail/autocomplete.inc
index b42ebf87b..36542caec 100644
--- a/program/steps/mail/autocomplete.inc
+++ b/program/steps/mail/autocomplete.inc
@@ -29,8 +29,10 @@ if ($RCMAIL->action == 'group-expand') {
$abook->set_group($gid);
$abook->set_pagesize(1000); // TODO: limit number of group members by config
$result = $abook->list_records(array('email','name'));
- while ($result && ($sql_arr = $result->iterate()))
- $members[] = format_email_recipient($sql_arr['email'], $sql_arr['name']);
+ while ($result && ($sql_arr = $result->iterate())) {
+ foreach ((array)$sql_arr['email'] as $email)
+ $members[] = format_email_recipient($email, $sql_arr['name']);
+ }
$OUTPUT->command('replace_group_recipients', $gid, join(', ', $members));
}
@@ -45,12 +47,14 @@ else if ($book_types && $search = get_input_value('_search', RCUBE_INPUT_GPC, tr
if ($result = $abook->search(array('email','name'), $search, false, true, true, 'email')) {
while ($sql_arr = $result->iterate()) {
- $contact = format_email_recipient($sql_arr['email'], $sql_arr['name']);
- // when we've got more than one book, we need to skip duplicates
- if ($books_num == 1 || !in_array($contact, $contacts)) {
- $contacts[] = $contact;
- if (count($contacts) >= $MAXNUM)
- break 2;
+ foreach ((array)$abook->get_col_values('email', $sql_arr, true) as $email) {
+ $contact = format_email_recipient($email, $sql_arr['name']);
+ // when we've got more than one book, we need to skip duplicates
+ if ($books_num == 1 || !in_array($contact, $contacts)) {
+ $contacts[] = $contact;
+ if (count($contacts) >= $MAXNUM)
+ break 2;
+ }
}
}
}
diff --git a/program/steps/mail/compose.inc b/program/steps/mail/compose.inc
index 2b342a9f0..335945c5c 100644
--- a/program/steps/mail/compose.inc
+++ b/program/steps/mail/compose.inc
@@ -1133,7 +1133,7 @@ function rcmail_compose_attachment_form($attrib)
$out = html::div($attrib,
$OUTPUT->form_tag(array('name' => 'uploadform', 'method' => 'post', 'enctype' => 'multipart/form-data'),
- html::div(null, rcmail_compose_attachment_field(array('size' => $attrib[attachmentfieldsize]))) .
+ html::div(null, rcmail_compose_attachment_field(array('size' => $attrib['attachmentfieldsize']))) .
html::div('hint', rcube_label(array('name' => 'maxuploadsize', 'vars' => array('size' => $max_filesize)))) .
html::div('buttons',
$button->show(rcube_label('close'), array('class' => 'button', 'onclick' => "$('#$attrib[id]').hide()")) . ' ' .
diff --git a/skins/default/addressbook.css b/skins/default/addressbook.css
index a90dcf1bf..6e07281f7 100644
--- a/skins/default/addressbook.css
+++ b/skins/default/addressbook.css
@@ -214,3 +214,166 @@ body.iframe,
text-align: right;
}
+#contacttabs
+{
+ position: relative;
+ padding-bottom: 22px;
+}
+
+#contacttabs div.tabsbar {
+ top: 0;
+ left: 2px;
+}
+
+#contacttabs fieldset.tabbed {
+ position: relative;
+ top: 22px;
+ min-height: 5em;
+}
+
+#contacthead
+{
+ margin-bottom: 1em;
+ border: 0;
+ padding: 0;
+}
+
+#contacthead .names span.namefield,
+#contacthead .names input
+{
+ font-size: 140%;
+}
+
+#contacthead .displayname span.namefield
+{
+ font-size: 120%;
+}
+
+#contacthead span.nickname:before,
+#contacthead span.nickname:after,
+#contacthead input.ff_nickname:before,
+#contacthead input.ff_nickname:after
+{
+ content: '"';
+}
+
+#contacthead input
+{
+ margin-right: 6px;
+ margin-bottom: 0.2em;
+}
+
+#contacthead .names input,
+#contacthead .addnames input,
+#contacthead .jobnames input
+{
+ width: 180px;
+}
+
+#contacthead input.ff_prefix,
+#contacthead input.ff_suffix
+{
+ width: 90px;
+}
+
+#contacthead .addnames input.ff_name
+{
+ width: 374px;
+}
+
+#contactphoto
+{
+ float: right;
+ width: 60px;
+ margin-left: 3em;
+ margin-right: 4px;
+}
+
+#contactpic
+{
+ width: 60px;
+ min-height: 60px;
+ border: 1px solid #ccc;
+ background: white;
+}
+
+#contactpic img {
+ width: 60px;
+}
+
+#contactphoto .formlinks
+{
+ margin-top: 0.5em;
+ text-align: center;
+}
+
+fieldset.contactfieldgroup
+{
+ border: 0;
+ margin: 0.5em 0;
+ padding: 0.5em 2px;
+}
+
+fieldset.contactfieldgroup legend
+{
+ font-size: 0.9em;
+}
+
+.contactfieldgroup .row
+{
+ position: relative;
+ margin-bottom: 0.4em;
+}
+
+.contactfieldgroup .contactfieldlabel
+{
+ position: absolute;
+ top: 0;
+ left: 2px;
+ width: 90px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ color: #666;
+ font-weight: bold;
+}
+
+.contactfieldgroup .contactfieldlabel select
+{
+ width: 78px;
+ background: none;
+ border: 0;
+ color: #666;
+ font-weight: bold;
+ padding-left: 0;
+}
+
+.contactfieldgroup .contactfieldcontent
+{
+ padding-left: 100px;
+ min-height: 1em;
+ line-height: 1.3em;
+}
+
+.contactfieldgroup .contactfield {
+ line-height: 1.3em;
+}
+
+.contactcontrolleraddress .contactfieldcontent input {
+ margin-bottom: 0.1em;
+}
+
+.contactfieldcontent .contactfieldbutton {
+ vertical-align: middle;
+ margin-left: 0.5em;
+}
+
+#upload-form
+{
+ padding: 6px;
+}
+
+#upload-form div
+{
+ padding: 2px;
+}
diff --git a/skins/default/common.css b/skins/default/common.css
index e052552d0..0d9b30718 100644
--- a/skins/default/common.css
+++ b/skins/default/common.css
@@ -76,6 +76,12 @@ input, textarea
padding: 1px 3px;
}
+input.placeholder,
+textarea.placeholder
+{
+ color: #aaa;
+}
+
input.button
{
height: 20px;
@@ -114,6 +120,20 @@ img
font-size: 11px;
}
+.formlinks a,
+.formlinks a:visited
+{
+ color: #CC0000;
+ font-size: 11px;
+ text-decoration: none;
+}
+
+.formlinks a.disabled,
+.formlinks a.disabled:visited
+{
+ color: #999999;
+}
+
/** common user interface objects */
#mainscreen
diff --git a/skins/default/functions.js b/skins/default/functions.js
index 00e97fd1a..62e4783cb 100644
--- a/skins/default/functions.js
+++ b/skins/default/functions.js
@@ -25,9 +25,8 @@ function rcube_show_advanced(visible)
// Warning: don't place "caller" <script> inside page element (id)
function rcube_init_tabs(id, current)
{
- var content = document.getElementById(id),
- // get fieldsets of the higher-level (skip nested fieldsets)
- fs = $('fieldset', content).not('fieldset > fieldset');
+ var content = $('#'+id),
+ fs = content.children('fieldset');
if (!fs.length)
return;
@@ -42,9 +41,7 @@ function rcube_init_tabs(id, current)
// convert fildsets into tabs
fs.each(function(idx) {
- var tab, a, elm = $(this),
- // get first legend element
- legend = $(elm).children('legend');
+ var tab, a, elm = $(this), legend = elm.children('legend');
// create a tab
a = $('<a>').text(legend.text()).attr('href', '#');
@@ -66,8 +63,7 @@ function rcube_init_tabs(id, current)
function rcube_show_tab(id, index)
{
- var content = document.getElementById(id),
- fs = $('fieldset', content).not('fieldset > fieldset');
+ var fs = $('#'+id).children('fieldset');
fs.each(function(idx) {
// Show/hide fieldset (tab content)
@@ -94,7 +90,8 @@ function rcube_mail_ui()
mailboxmenu: {id:'mailboxoptionsmenu', above:1},
composemenu: {id:'composeoptionsmenu', editable:1},
// toggle: #1486823, #1486930
- uploadmenu: {id:'attachment-form', editable:1, above:1, toggle:!bw.ie&&!bw.linux }
+ uploadmenu: {id:'attachment-form', editable:1, above:1, toggle:!bw.ie&&!bw.linux },
+ uploadform: {id:'upload-form', editable:1, toggle:!bw.ie&&!bw.linux }
};
var obj;
@@ -136,6 +133,9 @@ show_popupmenu: function(popup, show)
if (!above && pos.top + ref.offsetHeight + obj.height() > window.innerHeight)
above = true;
+ if (pos.left + obj.width() > window.innerWidth)
+ pos.left = window.innerWidth - obj.width() - 30;
+
obj.css({ left:pos.left, top:(pos.top + (above ? -obj.height() : ref.offsetHeight)) });
}
@@ -500,6 +500,9 @@ function rcube_init_mail_ui()
if (rcmail.env.action == 'compose')
rcmail_ui.init_compose_form();
}
+ else if (rcmail.env.task == 'addressbook') {
+ rcmail.addEventListener('afterupload-photo', function(){ rcmail_ui.show_popup('uploadform', false); });
+ }
}
// Events handling in iframes (eg. preview pane)
diff --git a/skins/default/iehacks.css b/skins/default/iehacks.css
index 29ab8cb75..4c0816ad8 100644
--- a/skins/default/iehacks.css
+++ b/skins/default/iehacks.css
@@ -236,3 +236,9 @@ table.records-table thead tr td
{
margin-top: 2px;
}
+
+.contactfieldgroup legend
+{
+ padding: 0 0 0.5em 0;
+ margin-left: -4px;
+}
diff --git a/skins/default/images/contactpic.png b/skins/default/images/contactpic.png
new file mode 100644
index 000000000..bdb6cdcc0
--- /dev/null
+++ b/skins/default/images/contactpic.png
Binary files differ
diff --git a/skins/default/mail.css b/skins/default/mail.css
index 7bb308c06..30572299e 100644
--- a/skins/default/mail.css
+++ b/skins/default/mail.css
@@ -1342,20 +1342,6 @@ input.from_address
display: none;
}
-.formlinks a,
-.formlinks a:visited
-{
- color: #999999;
- font-size: 11px;
- text-decoration: none;
-}
-
-.formlinks a,
-.formlinks a:visited
-{
- color: #CC0000;
-}
-
#compose-editorfooter
{
position: absolute;
diff --git a/skins/default/templates/contact.html b/skins/default/templates/contact.html
index 06d0fbee5..084664e9d 100644
--- a/skins/default/templates/contact.html
+++ b/skins/default/templates/contact.html
@@ -9,12 +9,17 @@
<div id="contact-title" class="boxtitle"><roundcube:label name="contactproperties" /></div>
<div id="contact-details" class="boxcontent">
- <roundcube:object name="contactdetails" />
+ <div id="contactphoto"><roundcube:object name="contactphoto" id="contactpic" placeholder="/images/contactpic.png" /></div>
+ <roundcube:object name="contacthead" id="contacthead" />
+ <div style="clear:both"></div>
+ <div id="contacttabs">
+ <roundcube:object name="contactdetails" />
+ </div>
<p>
<roundcube:button command="edit" type="input" class="button" label="editcontact" condition="!ENV:readonly" />
</p>
</div>
-<script type="text/javascript">rcube_init_tabs('contact-details')</script>
+<script type="text/javascript">rcube_init_tabs('contacttabs')</script>
</body>
</html>
diff --git a/skins/default/templates/contactadd.html b/skins/default/templates/contactadd.html
index 1a10f10ac..b5fd05609 100644
--- a/skins/default/templates/contactadd.html
+++ b/skins/default/templates/contactadd.html
@@ -5,18 +5,34 @@
<roundcube:include file="/includes/links.html" />
<script type="text/javascript" src="/functions.js"></script>
</head>
-<body class="iframe">
+<body class="iframe" onload="rcube_init_mail_ui()">
<div id="contact-title" class="boxtitle"><roundcube:label name="addcontact" /></div>
<div id="contact-details" class="boxcontent">
- <roundcube:object name="contacteditform" size="40" />
+<form name="editform" method="post" action="./">
+ <div id="contactphoto">
+ <roundcube:object name="contactphoto" id="contactpic" placeholder="/images/contactpic.png" />
+ <div class="formlinks">
+ <roundcube:button command="upload-photo" id="uploadformlink" type="link" label="addphoto" class="disabled" classAct="active" onclick="rcmail_ui.show_popup('uploadform', true);return false" condition="env:photocol" /><br/>
+ <roundcube:button command="delete-photo" type="link" label="delete" class="disabled" classAct="active" condition="env:photocol" />
+ </div>
+ </div>
+ <roundcube:object name="contactedithead" id="contacthead" size="16" form="editform" />
+ <div style="clear:both"></div>
+
+ <div id="contacttabs">
+ <roundcube:object name="contacteditform" size="40" textareacols="60" deleteIcon="/images/icons/delete.png" form="editform" />
+ </div>
<p>
<input type="button" value="<roundcube:label name="cancel" />" class="button" onclick="history.back()" />&nbsp;
<roundcube:button command="save" type="input" class="button mainaction" label="save" />
</p>
</form>
</div>
-<script type="text/javascript">rcube_init_tabs('contact-details')</script>
+
+<roundcube:object name="photoUploadForm" id="upload-form" size="30" class="popupmenu" />
+
+<script type="text/javascript">rcube_init_tabs('contacttabs')</script>
</body>
</html>
diff --git a/skins/default/templates/contactedit.html b/skins/default/templates/contactedit.html
index a15aaf22a..681201caa 100644
--- a/skins/default/templates/contactedit.html
+++ b/skins/default/templates/contactedit.html
@@ -5,18 +5,34 @@
<roundcube:include file="/includes/links.html" />
<script type="text/javascript" src="/functions.js"></script>
</head>
-<body class="iframe">
+<body class="iframe" onload="rcube_init_mail_ui()">
<div id="contact-title" class="boxtitle"><roundcube:label name="editcontact" /></div>
<div id="contact-details" class="boxcontent">
- <roundcube:object name="contacteditform" size="40" />
+<form name="editform" method="post" action="./">
+ <div id="contactphoto">
+ <roundcube:object name="contactphoto" id="contactpic" placeholder="/images/contactpic.png" />
+ <div class="formlinks">
+ <roundcube:button command="upload-photo" id="uploadformlink" type="link" label="replacephoto" class="disabled" classAct="active" onclick="rcmail_ui.show_popup('uploadform', true);return false" condition="env:photocol" /><br/>
+ <roundcube:button command="delete-photo" type="link" label="delete" class="disabled" classAct="active" condition="env:photocol" />
+ </div>
+ </div>
+ <roundcube:object name="contactedithead" id="contacthead" size="16" form="editform" />
+ <div style="clear:both"></div>
+
+ <div id="contacttabs">
+ <roundcube:object name="contacteditform" size="40" textareacols="60" deleteIcon="/images/icons/delete.png" form="editform" />
+ </div>
<p>
<roundcube:button command="show" type="input" class="button" label="cancel" />&nbsp;
<roundcube:button command="save" type="input" class="button mainaction" label="save" />
</p>
</form>
</div>
-<script type="text/javascript">rcube_init_tabs('contact-details')</script>
+
+<roundcube:object name="photoUploadForm" id="upload-form" size="30" class="popupmenu" />
+
+<script type="text/javascript">rcube_init_tabs('contacttabs')</script>
</body>
</html>