From 0501b637a3177cce441166b5fcfe27c9bd9fbe0f Mon Sep 17 00:00:00 2001 From: thomascube Date: Tue, 18 Jan 2011 18:00:57 +0000 Subject: Merge branch devel-addressbook (r4193:4382) back into trunk --- config/main.inc.php.dist | 36 +++- program/include/html.php | 7 +- program/include/main.inc | 41 ++-- program/include/rcmail.php | 143 +++++++++++-- program/include/rcube_addressbook.php | 117 ++++++++++- program/include/rcube_browser.php | 1 + program/include/rcube_contacts.php | 140 +++++++++---- program/include/rcube_ldap.php | 126 +++++++---- program/include/rcube_plugin.php | 15 +- program/include/rcube_plugin_api.php | 118 ++++++----- program/include/rcube_shared.inc | 19 ++ program/include/rcube_template.php | 5 +- program/include/rcube_vcard.php | 172 +++++++++++++-- program/js/app.js | 332 +++++++++++++++++++++++------ program/localization/en_US/labels.inc | 39 +++- program/localization/en_US/messages.inc | 3 +- program/steps/addressbook/copy.inc | 2 +- program/steps/addressbook/delete.inc | 4 + program/steps/addressbook/edit.inc | 118 ++++++++++- program/steps/addressbook/export.inc | 25 ++- program/steps/addressbook/func.inc | 345 ++++++++++++++++++++++++++++--- program/steps/addressbook/groups.inc | 3 +- program/steps/addressbook/import.inc | 9 +- program/steps/addressbook/list.inc | 2 +- program/steps/addressbook/mailto.inc | 6 +- program/steps/addressbook/save.inc | 162 +++++++++++++-- program/steps/addressbook/search.inc | 8 +- program/steps/addressbook/show.inc | 133 +++++++++--- program/steps/mail/autocomplete.inc | 20 +- program/steps/mail/compose.inc | 2 +- skins/default/addressbook.css | 163 +++++++++++++++ skins/default/common.css | 20 ++ skins/default/functions.js | 21 +- skins/default/iehacks.css | 6 + skins/default/images/contactpic.png | Bin 0 -> 375 bytes skins/default/mail.css | 14 -- skins/default/templates/contact.html | 9 +- skins/default/templates/contactadd.html | 22 +- skins/default/templates/contactedit.html | 22 +- 39 files changed, 2010 insertions(+), 420 deletions(-) create mode 100644 skins/default/images/contactpic.png 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}
{locality} {zipcode}
{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']) @@ -1306,6 +1315,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 * 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 @@ -54,6 +63,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 * @@ -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 @@ -97,6 +118,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 */ @@ -137,11 +181,32 @@ abstract class rcube_addressbook /* empty for read-only address books */ } + /** + * 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( @@ -425,20 +447,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 * @@ -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
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; @@ -106,6 +124,64 @@ 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 */ @@ -113,6 +189,28 @@ class rcube_vcard { 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 = ''; - 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 = $('
').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 = $('
').addClass('row'), + cell = $('
').addClass('contactfieldcontent data'), + label = $('
').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 = $('') + .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 = $('') + .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 = $('" class="button" onclick="history.back()" /> 

- + + + + 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 @@ - +
- +
+
+ + +
+ +
+ +
+ +

 

- + + + + -- cgit v1.2.3