thomascube
2011-01-18 0501b637a3177cce441166b5fcfe27c9bd9fbe0f
Merge branch devel-addressbook (r4193:4382) back into trunk

1 files added
38 files modified
2438 ■■■■ changed files
config/main.inc.php.dist 36 ●●●● patch | view | raw | blame | history
program/include/html.php 7 ●●●● patch | view | raw | blame | history
program/include/main.inc 43 ●●●●● patch | view | raw | blame | history
program/include/rcmail.php 143 ●●●●● patch | view | raw | blame | history
program/include/rcube_addressbook.php 117 ●●●●● patch | view | raw | blame | history
program/include/rcube_browser.php 1 ●●●● patch | view | raw | blame | history
program/include/rcube_contacts.php 140 ●●●● patch | view | raw | blame | history
program/include/rcube_ldap.php 126 ●●●●● patch | view | raw | blame | history
program/include/rcube_plugin.php 15 ●●●●● patch | view | raw | blame | history
program/include/rcube_plugin_api.php 118 ●●●● patch | view | raw | blame | history
program/include/rcube_shared.inc 19 ●●●●● patch | view | raw | blame | history
program/include/rcube_template.php 5 ●●●● patch | view | raw | blame | history
program/include/rcube_vcard.php 172 ●●●●● patch | view | raw | blame | history
program/js/app.js 332 ●●●● patch | view | raw | blame | history
program/localization/en_US/labels.inc 37 ●●●● patch | view | raw | blame | history
program/localization/en_US/messages.inc 3 ●●●● patch | view | raw | blame | history
program/steps/addressbook/copy.inc 2 ●●● patch | view | raw | blame | history
program/steps/addressbook/delete.inc 4 ●●●● patch | view | raw | blame | history
program/steps/addressbook/edit.inc 120 ●●●●● patch | view | raw | blame | history
program/steps/addressbook/export.inc 25 ●●●● patch | view | raw | blame | history
program/steps/addressbook/func.inc 343 ●●●●● patch | view | raw | blame | history
program/steps/addressbook/groups.inc 3 ●●●● patch | view | raw | blame | history
program/steps/addressbook/import.inc 9 ●●●● patch | view | raw | blame | history
program/steps/addressbook/list.inc 2 ●●● patch | view | raw | blame | history
program/steps/addressbook/mailto.inc 6 ●●●●● patch | view | raw | blame | history
program/steps/addressbook/save.inc 168 ●●●● patch | view | raw | blame | history
program/steps/addressbook/search.inc 8 ●●●● patch | view | raw | blame | history
program/steps/addressbook/show.inc 135 ●●●● patch | view | raw | blame | history
program/steps/mail/autocomplete.inc 20 ●●●●● patch | view | raw | blame | history
program/steps/mail/compose.inc 2 ●●● patch | view | raw | blame | history
skins/default/addressbook.css 163 ●●●●● patch | view | raw | blame | history
skins/default/common.css 20 ●●●●● patch | view | raw | blame | history
skins/default/functions.js 21 ●●●●● patch | view | raw | blame | history
skins/default/iehacks.css 6 ●●●●● patch | view | raw | blame | history
skins/default/images/contactpic.png patch | view | raw | blame | history
skins/default/mail.css 14 ●●●●● patch | view | raw | blame | history
skins/default/templates/contact.html 9 ●●●● patch | view | raw | blame | history
skins/default/templates/contactadd.html 22 ●●●● patch | view | raw | blame | history
skins/default/templates/contactedit.html 22 ●●●● patch | view | raw | blame | history
config/main.inc.php.dist
@@ -308,6 +308,15 @@
// 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;
@@ -345,6 +354,9 @@
// 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
@@ -458,7 +470,6 @@
  // 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 @@
  '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 @@
// 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
// ----------------------------------
program/include/html.php
@@ -71,6 +71,9 @@
     */
    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 @@
            $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 @@
    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
program/include/main.inc
@@ -799,7 +799,7 @@
      // 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 @@
 * @return string HTML field definition
 */
function rcmail_get_edit_field($col, $value, $attrib, $type='text')
  {
  $fname = '_'.$col;
  $attrib['name'] = $fname;
{
  static $colcounts = array();
  
  if ($type=='checkbox')
    {
  $fname = '_'.$col;
  $attrib['name'] = $fname . ($attrib['array'] ? '[]' : '');
  $attrib['class'] = trim($attrib['class'] . ' ff_' . $col);
  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;
  }
}
/**
program/include/rcmail.php
@@ -114,7 +114,7 @@
  public $comm_path = './';
  private $texts;
  private $books = array();
  private $address_books = array();
  private $action_map = array();
@@ -331,6 +331,10 @@
    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 @@
    }
    // 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 @@
    // 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 @@
    $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 @@
    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 @@
  /**
   * 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
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 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 @@
     * @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
@@ -96,6 +117,27 @@
     * @return mixed Result object with all record fields or False if not found
     */
    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
@@ -129,6 +171,8 @@
     * 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 @@
    }
    /**
     * 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 @@
    /**
     * 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 @@
        /* 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;
    }
}
program/include/rcube_browser.php
@@ -68,6 +68,7 @@
        $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;
    }
}
program/include/rcube_contacts.php
@@ -47,13 +47,17 @@
    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 @@
    /**
     * 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 @@
            $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 @@
     * @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 @@
        );
        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 @@
     */
    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 @@
    /**
     * 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 @@
    {
        $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 @@
            );
            $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;
    }
    /**
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 @@
  {
    $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 @@
      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 @@
    // 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 @@
    } // 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 @@
    $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 @@
      $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 @@
      // 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 @@
        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 @@
      $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 @@
      $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 @@
   */
  private function _ldap2result($rec)
  {
    global $RCMAIL;
    $out = array();
    
    if ($rec['dn'])
@@ -688,11 +726,17 @@
    
    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 @@
   */
  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');
program/include/rcube_plugin.php
@@ -83,7 +83,20 @@
   * 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.
program/include/rcube_plugin_api.php
@@ -109,42 +109,9 @@
    $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 @@
      }
      
      // 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 @@
    
    // 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;
  }
  
  
  /**
program/include/rcube_shared.inc
@@ -485,6 +485,25 @@
    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.
 *
program/include/rcube_template.php
@@ -996,8 +996,11 @@
        $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);
    }
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 @@
    '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,11 +125,91 @@
  /**
   * 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 @@
   *
   * @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 @@
        $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;
    }
program/js/app.js
@@ -319,7 +319,20 @@
        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 @@
              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,12 +1012,16 @@
      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
@@ -1158,7 +1178,7 @@
  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 @@
    // 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) {
@@ -3217,11 +3217,6 @@
        if (bw.opera)
          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');
@@ -3979,6 +3974,165 @@
  };
  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 @@
    }
  };
  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 @@
  /********************************************************/
  /*********        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 @@
  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 @@
        query = result;
    }
    url += '&_remote=1&_action=' + action + (query ? '&' : '') + query;
    url += '&_remote=1';
    // send request
    console.log('HTTP GET: ' + url);
@@ -5013,15 +5202,7 @@
  // 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 @@
      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()
  {
program/localization/en_US/labels.inc
@@ -243,11 +243,38 @@
$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['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';
program/localization/en_US/messages.inc
@@ -104,7 +104,7 @@
$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['nametoolong'] = 'Name is too long';
$messages['folderupdated'] = 'Folder updated successfully';
$messages['foldercreated'] = 'Folder created successfully';
$messages['invalidimageformat'] = 'Not a valid image format';
?>
program/steps/addressbook/copy.inc
@@ -48,7 +48,7 @@
          '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++;
          }
program/steps/addressbook/delete.inc
@@ -38,6 +38,10 @@
        // 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));
program/steps/addressbook/edit.inc
@@ -31,45 +31,140 @@
}
function rcmail_contact_editform($attrib)
function rcmail_contact_edithead($attrib)
{
   global $RCMAIL, $CONTACTS, $OUTPUT;
   global $RCMAIL, $CONTACTS;
    // 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;
    }
    $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, $CONTACT_COLTYPES;
    // 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;
    }
    // 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;
}
@@ -103,7 +198,10 @@
}
$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');
program/steps/addressbook/export.inc
@@ -30,13 +30,24 @@
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;
program/steps/addressbook/func.inc
@@ -56,6 +56,56 @@
}
// 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 @@
        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,15 +178,15 @@
{
    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_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 @@
    $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;
                // remove subtype part of col name
                list($field, $subtype) = explode(':', $col);
                if (!$subtype) $subtype = 'home';
                $fullkey = $col.':'.$subtype;
                $label = !empty($colprop['label']) ? $colprop['label'] : rcube_label($col);
                // 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,8 +514,50 @@
        $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'));
}
@@ -297,6 +574,8 @@
// 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',
program/steps/addressbook/groups.inc
@@ -79,8 +79,7 @@
  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');
program/steps/addressbook/import.inc
@@ -150,13 +150,8 @@
        }
      }
      
      $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'];
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));
program/steps/addressbook/mailto.inc
@@ -29,8 +29,10 @@
  $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))
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,31 +29,145 @@
  return;
}
// Basic input checks
if ((!get_input_value('_name', RCUBE_INPUT_POST) || !get_input_value('_email', RCUBE_INPUT_POST))) {
// 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');
}
// read POST values into hash array
$a_record = array();
foreach ($GLOBALS['CONTACT_COLTYPES'] as $col => $colprop) {
  $fname = '_'.$col;
  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);
  }
}
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;
}
// 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) {
  $fname = '_'.$col;
  if (isset($_POST[$fname]))
    $a_record[$col] = get_input_value($fname, RCUBE_INPUT_POST);
// 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;
    }
  }
}
// Validity checks
$_email = idn_to_ascii($a_record['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
@@ -92,7 +206,8 @@
  }
  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 @@
// 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 {
    // 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');
  }
}
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 @@
// 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);
}
program/steps/addressbook/show.inc
@@ -25,8 +25,30 @@
    $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,51 +58,96 @@
        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(
        'head' => array(  // section 'head' is magic!
            'content' => array(
                'prefix' => array('type' => 'text'),
                'firstname' => array('type' => 'text'),
                'middlename' => array('type' => 'text'),
                'surname' => array('type' => 'text'),
                'suffix' => array('type' => 'text'),
            ),
        ),
    );
    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;
    }
    $i_size = !empty($attrib['size']) ? $attrib['size'] : 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('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),
            ),
        ),
        'groups' => array(
            'name'    => rcube_label('groups'),
            'content' => '',
        ),
    );
    // Get content of groups fieldset
    if ($groups = rcmail_contact_record_groups($record['ID'])) {
        $form['groups']['content'] = $groups;
    if (isset($CONTACT_COLTYPES['notes'])) {
        $form['notes'] = array(
            'name'    => rcube_label('notes'),
            'content' => array(
                'notes' => array('type' => 'textarea', 'label' => false),
            ),
        );
    }
    else {
        unset($form['groups']);
    }
    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']));
    }
    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));
}
@@ -124,6 +191,8 @@
//$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');
program/steps/mail/autocomplete.inc
@@ -29,8 +29,10 @@
    $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 @@
    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;
          }
        }
      }
    }
program/steps/mail/compose.inc
@@ -1133,7 +1133,7 @@
  
  $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()")) . ' ' .
skins/default/addressbook.css
@@ -214,3 +214,166 @@
  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;
}
skins/default/common.css
@@ -76,6 +76,12 @@
  padding: 1px 3px;
}
input.placeholder,
textarea.placeholder
{
  color: #aaa;
}
input.button
{
  height: 20px;
@@ -114,6 +120,20 @@
  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
skins/default/functions.js
@@ -25,9 +25,8 @@
// 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 @@
  // 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_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 @@
    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;
@@ -135,6 +132,9 @@
    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 @@
    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)
skins/default/iehacks.css
@@ -236,3 +236,9 @@
{
  margin-top: 2px;
}
.contactfieldgroup legend
{
    padding: 0 0 0.5em 0;
    margin-left: -4px;
}
skins/default/images/contactpic.png
skins/default/mail.css
@@ -1342,20 +1342,6 @@
  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;
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>
skins/default/templates/contactadd.html
@@ -5,18 +5,34 @@
<roundcube:include file="/includes/links.html" />
<script type="text/javascript" src="/functions.js"></script>
</head>
<body class="iframe">
<body class="iframe" onload="rcube_init_mail_ui()">
<div id="contact-title" class="boxtitle"><roundcube:label name="addcontact" /></div>
<div id="contact-details" class="boxcontent">
  <roundcube:object name="contacteditform" size="40" />
<form name="editform" method="post" action="./">
  <div id="contactphoto">
    <roundcube:object name="contactphoto" id="contactpic" placeholder="/images/contactpic.png" />
    <div class="formlinks">
      <roundcube:button command="upload-photo" id="uploadformlink" type="link" label="addphoto" class="disabled" classAct="active" onclick="rcmail_ui.show_popup('uploadform', true);return false" condition="env:photocol" /><br/>
      <roundcube:button command="delete-photo" type="link" label="delete" class="disabled" classAct="active" condition="env:photocol" />
    </div>
  </div>
  <roundcube:object name="contactedithead" id="contacthead" size="16" form="editform" />
  <div style="clear:both"></div>
  <div id="contacttabs">
    <roundcube:object name="contacteditform" size="40" textareacols="60" deleteIcon="/images/icons/delete.png" form="editform" />
  </div>
  <p>
    <input type="button" value="<roundcube:label name="cancel" />" class="button" onclick="history.back()" />&nbsp;
    <roundcube:button command="save" type="input" class="button mainaction" label="save" />
  </p>
</form>
</div>
<script type="text/javascript">rcube_init_tabs('contact-details')</script>
<roundcube:object name="photoUploadForm" id="upload-form" size="30" class="popupmenu" />
<script type="text/javascript">rcube_init_tabs('contacttabs')</script>
</body>
</html>
skins/default/templates/contactedit.html
@@ -5,18 +5,34 @@
<roundcube:include file="/includes/links.html" />
<script type="text/javascript" src="/functions.js"></script>
</head>
<body class="iframe">
<body class="iframe" onload="rcube_init_mail_ui()">
<div id="contact-title" class="boxtitle"><roundcube:label name="editcontact" /></div>
<div id="contact-details" class="boxcontent">
  <roundcube:object name="contacteditform" size="40" />
<form name="editform" method="post" action="./">
  <div id="contactphoto">
    <roundcube:object name="contactphoto" id="contactpic" placeholder="/images/contactpic.png" />
    <div class="formlinks">
      <roundcube:button command="upload-photo" id="uploadformlink" type="link" label="replacephoto" class="disabled" classAct="active" onclick="rcmail_ui.show_popup('uploadform', true);return false" condition="env:photocol" /><br/>
      <roundcube:button command="delete-photo" type="link" label="delete" class="disabled" classAct="active" condition="env:photocol" />
    </div>
  </div>
  <roundcube:object name="contactedithead" id="contacthead" size="16" form="editform" />
  <div style="clear:both"></div>
  <div id="contacttabs">
    <roundcube:object name="contacteditform" size="40" textareacols="60" deleteIcon="/images/icons/delete.png" form="editform" />
  </div>
  <p>
    <roundcube:button command="show" type="input" class="button" label="cancel" />&nbsp;
    <roundcube:button command="save" type="input" class="button mainaction" label="save" />
  </p>
</form>
</div>
<script type="text/javascript">rcube_init_tabs('contact-details')</script>
<roundcube:object name="photoUploadForm" id="upload-form" size="30" class="popupmenu" />
<script type="text/javascript">rcube_init_tabs('contacttabs')</script>
</body>
</html>