diff options
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 Binary files differnew file mode 100644 index 000000000..bdb6cdcc0 --- /dev/null +++ b/skins/default/images/contactpic.png 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()" /> <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" /> <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> |