<?php

/**
 +-----------------------------------------------------------------------+
 | This file is part of the Roundcube Webmail client                     |
 |                                                                       |
 | Copyright (C) The Roundcube Dev Team                                  |
 |                                                                       |
 | Licensed under the GNU General Public License version 3 or            |
 | any later version with exceptions for skins & plugins.                |
 | See the README file for a full license statement.                     |
 |                                                                       |
 | PURPOSE:                                                              |
 |   CSV to vCard data conversion                                        |
 +-----------------------------------------------------------------------+
 | Author: Aleksander Machniak <alec@alec.pl>                            |
 +-----------------------------------------------------------------------+
*/

/**
 * CSV to vCard data converter
 *
 * @package    Framework
 * @subpackage Addressbook
 */
class rcube_csv2vcard
{
    /**
     * CSV to vCard fields mapping
     *
     * @var array
     */
    protected $csv2vcard_map = array(
        // MS Outlook 2010
        'anniversary'           => 'anniversary',
        'assistants_name'       => 'assistant',
        'assistants_phone'      => 'phone:assistant',
        'birthday'              => 'birthday',
        'business_city'         => 'locality:work',
        'business_countryregion' => 'country:work',
        'business_fax'          => 'phone:work,fax',
        'business_phone'        => 'phone:work',
        'business_phone_2'      => 'phone:work2',
        'business_postal_code'  => 'zipcode:work',
        'business_state'        => 'region:work',
        'business_street'       => 'street:work',
        //'business_street_2'     => '',
        //'business_street_3'     => '',
        'car_phone'             => 'phone:car',
        'categories'            => 'groups',
        //'children'              => '',
        'company'               => 'organization',
        //'company_main_phone'    => '',
        'department'            => 'department',
        'email_2_address'       => 'email:other',
        //'email_2_type'          => '',
        'email_3_address'       => 'email:other',
        //'email_3_type'          => '',
        'email_address'         => 'email:pref',
        //'email_type'            => '',
        'first_name'            => 'firstname',
        'gender'                => 'gender',
        'home_city'             => 'locality:home',
        'home_countryregion'    => 'country:home',
        'home_fax'              => 'phone:home,fax',
        'home_phone'            => 'phone:home',
        'home_phone_2'          => 'phone:home2',
        'home_postal_code'      => 'zipcode:home',
        'home_state'            => 'region:home',
        'home_street'           => 'street:home',
        //'home_street_2'         => '',
        //'home_street_3'         => '',
        //'initials'              => '',
        //'isdn'                  => '',
        'job_title'             => 'jobtitle',
        //'keywords'              => '',
        //'language'              => '',
        'last_name'             => 'surname',
        //'location'              => '',
        'managers_name'         => 'manager',
        'middle_name'           => 'middlename',
        //'mileage'               => '',
        'mobile_phone'          => 'phone:cell',
        'notes'                 => 'notes',
        //'office_location'       => '',
        'other_city'            => 'locality:other',
        'other_countryregion'   => 'country:other',
        'other_fax'             => 'phone:other,fax',
        'other_phone'           => 'phone:other',
        'other_postal_code'     => 'zipcode:other',
        'other_state'           => 'region:other',
        'other_street'          => 'street:other',
        //'other_street_2'        => '',
        //'other_street_3'        => '',
        'pager'                 => 'phone:pager',
        'primary_phone'         => 'phone:pref',
        //'profession'            => '',
        //'radio_phone'           => '',
        'spouse'                => 'spouse',
        'suffix'                => 'suffix',
        'title'                 => 'title',
        'web_page'              => 'website:homepage',

        // Thunderbird
        'birth_day'             => 'birthday-d',
        'birth_month'           => 'birthday-m',
        'birth_year'            => 'birthday-y',
        'display_name'          => 'displayname',
        'fax_number'            => 'phone:fax',
        'home_address'          => 'street:home',
        //'home_address_2'        => '',
        'home_country'          => 'country:home',
        'home_zipcode'          => 'zipcode:home',
        'mobile_number'         => 'phone:cell',
        'nickname'              => 'nickname',
        'organization'          => 'organization',
        'pager_number'          => 'phone:pager',
        'primary_email'         => 'email:pref',
        'secondary_email'       => 'email:other',
        'web_page_1'            => 'website:homepage',
        'web_page_2'            => 'website:other',
        'work_phone'            => 'phone:work',
        'work_address'          => 'street:work',
        //'work_address_2'        => '',
        'work_country'          => 'country:work',
        'work_zipcode'          => 'zipcode:work',
        'last'                  => 'surname',
        'first'                 => 'firstname',
        'work_city'             => 'locality:work',
        'work_state'            => 'region:work',
        'home_city_short'       => 'locality:home',
        'home_state_short'      => 'region:home',

        // Atmail
        'date_of_birth'         => 'birthday',
        'email'                 => 'email:pref',
        'home_mobile'           => 'phone:cell',
        'home_zip'              => 'zipcode:home',
        'info'                  => 'notes',
        'user_photo'            => 'photo',
        'url'                   => 'website:homepage',
        'work_company'          => 'organization',
        'work_dept'             => 'departament',
        'work_fax'              => 'phone:work,fax',
        'work_mobile'           => 'phone:work,cell',
        'work_title'            => 'jobtitle',
        'work_zip'              => 'zipcode:work',
        'group'                 => 'groups',

        // GMail
        'groups'                => 'groups',
        'group_membership'      => 'groups',
        'given_name'            => 'firstname',
        'additional_name'       => 'middlename',
        'family_name'           => 'surname',
        'name'                  => 'displayname',
        'name_prefix'           => 'prefix',
        'name_suffix'           => 'suffix',

        // Format of Letter Hub test files from
        // https://letterhub.com/sample-csv-file-with-contacts/
        'company_name'          => 'organization',
        'address'               => 'street:home',
        'city'                  => 'locality:home',
        //'county'                => '',
        'state'                 => 'region:home',
        'zip'                   => 'zipcode:home',
        'phone1'                => 'phone:home',
        'phone'                 => 'phone:work',
        'email'                 => 'email:home',
    );

    /**
     * CSV label to text mapping for English
     *
     * @var array
     */
    protected $label_map = array(
        // MS Outlook 2010
        'anniversary'       => "Anniversary",
        'assistants_name'   => "Assistant's Name",
        'assistants_phone'  => "Assistant's Phone",
        'birthday'          => "Birthday",
        'business_city'     => "Business City",
        'business_countryregion' => "Business Country/Region",
        'business_fax'      => "Business Fax",
        'business_phone'    => "Business Phone",
        'business_phone_2'  => "Business Phone 2",
        'business_postal_code' => "Business Postal Code",
        'business_state'    => "Business State",
        'business_street'   => "Business Street",
        //'business_street_2' => "Business Street 2",
        //'business_street_3' => "Business Street 3",
        'car_phone'         => "Car Phone",
        'categories'        => "Categories",
        //'children'          => "Children",
        'company'           => "Company",
        //'company_main_phone' => "Company Main Phone",
        'department'        => "Department",
        //'directory_server'  => "Directory Server",
        'email_2_address'   => "E-mail 2 Address",
        //'email_2_type'      => "E-mail 2 Type",
        'email_3_address'   => "E-mail 3 Address",
        //'email_3_type'      => "E-mail 3 Type",
        'email_address'     => "E-mail Address",
        //'email_type'        => "E-mail Type",
        'first_name'        => "First Name",
        'gender'            => "Gender",
        'home_city'         => "Home City",
        'home_countryregion' => "Home Country/Region",
        'home_fax'          => "Home Fax",
        'home_phone'        => "Home Phone",
        'home_phone_2'      => "Home Phone 2",
        'home_postal_code'  => "Home Postal Code",
        'home_state'        => "Home State",
        'home_street'       => "Home Street",
        //'home_street_2'     => "Home Street 2",
        //'home_street_3'     => "Home Street 3",
        //'initials'          => "Initials",
        //'isdn'              => "ISDN",
        'job_title'         => "Job Title",
        //'keywords'          => "Keywords",
        //'language'          => "Language",
        'last_name'         => "Last Name",
        //'location'          => "Location",
        'managers_name'     => "Manager's Name",
        'middle_name'       => "Middle Name",
        //'mileage'           => "Mileage",
        'mobile_phone'      => "Mobile Phone",
        'notes'             => "Notes",
        //'office_location'   => "Office Location",
        'other_city'        => "Other City",
        'other_countryregion' => "Other Country/Region",
        'other_fax'         => "Other Fax",
        'other_phone'       => "Other Phone",
        'other_postal_code' => "Other Postal Code",
        'other_state'       => "Other State",
        'other_street'      => "Other Street",
        //'other_street_2'    => "Other Street 2",
        //'other_street_3'    => "Other Street 3",
        'pager'             => "Pager",
        'primary_phone'     => "Primary Phone",
        //'profession'        => "Profession",
        //'radio_phone'       => "Radio Phone",
        'spouse'            => "Spouse",
        'suffix'            => "Suffix",
        'title'             => "Title",
        'web_page'          => "Web Page",

        // Thunderbird
        'birth_day'         => "Birth Day",
        'birth_month'       => "Birth Month",
        'birth_year'        => "Birth Year",
        'display_name'      => "Display Name",
        'fax_number'        => "Fax Number",
        'home_address'      => "Home Address",
        //'home_address_2'    => "Home Address 2",
        'home_country'      => "Home Country",
        'home_zipcode'      => "Home ZipCode",
        'mobile_number'     => "Mobile Number",
        'nickname'          => "Nickname",
        'organization'      => "Organization",
        'pager_number'      => "Pager Namber",
        'primary_email'     => "Primary Email",
        'secondary_email'   => "Secondary Email",
        'web_page_1'        => "Web Page 1",
        'web_page_2'        => "Web Page 2",
        'work_phone'        => "Work Phone",
        'work_address'      => "Work Address",
        //'work_address_2'    => "Work Address 2",
        'work_city'         => "Work City",
        'work_country'      => "Work Country",
        'work_state'        => "Work State",
        'work_zipcode'      => "Work ZipCode",

        // Atmail
        'date_of_birth'     => "Date of Birth",
        'email'             => "Email",
        //'email_2'         => "Email2",
        //'email_3'         => "Email3",
        //'email_4'         => "Email4",
        //'email_5'         => "Email5",
        'home_mobile'       => "Home Mobile",
        'home_zip'          => "Home Zip",
        'info'              => "Info",
        'user_photo'        => "User Photo",
        'url'               => "URL",
        'work_company'      => "Work Company",
        'work_dept'         => "Work Dept",
        'work_fax'          => "Work Fax",
        'work_mobile'       => "Work Mobile",
        'work_title'        => "Work Title",
        'work_zip'          => "Work Zip",
        'group'             => "Group",

        // GMail
        'groups'            => "Groups",
        'group_membership'  => "Group Membership",
        'given_name'        => "Given Name",
        'additional_name'   => "Additional Name",
        'family_name'       => "Family Name",
        'name'              => "Name",
        'name_prefix'       => "Name Prefix",
        'name_suffix'       => "Name Suffix",
    );

    /**
     * Special fields map for GMail format
     *
     * @var array
     */
    protected $gmail_label_map = array(
        'E-mail' => array(
            'Value' => array(
                'home' => 'email:home',
                'work' => 'email:work',
                '*'    => 'email:other',
            ),
        ),
        'Phone' => array(
            'Value' => array(
                'home'    => 'phone:home',
                'homefax' => 'phone:homefax',
                'main'    => 'phone:pref',
                'pager'   => 'phone:pager',
                'mobile'  => 'phone:cell',
                'work'    => 'phone:work',
                'workfax' => 'phone:workfax',
            ),
        ),
        'Relation' => array(
            'Value' => array(
                'spouse' => 'spouse',
            ),
        ),
        'Website' => array(
            'Value' => array(
                'profile'  => 'website:profile',
                'blog'     => 'website:blog',
                'homepage' => 'website:homepage',
                'work'     => 'website:work',
            ),
        ),
        'Address' => array(
            'Street' => array(
                'home' => 'street:home',
                'work' => 'street:work',
            ),
            'City' => array(
                'home' => 'locality:home',
                'work' => 'locality:work',
            ),
            'Region' => array(
                'home' => 'region:home',
                'work' => 'region:work',
            ),
            'Postal Code' => array(
                'home' => 'zipcode:home',
                'work' => 'zipcode:work',
            ),
            'Country' => array(
                'home' => 'country:home',
                'work' => 'country:work',
            ),
        ),
        'Organization' => array(
            'Name' => array(
                '' => 'organization',
            ),
            'Title' => array(
                '' => 'jobtitle',
            ),
            'Department' => array(
                '' => 'department',
            ),
        ),
    );


    protected $local_label_map = array();
    protected $vcards          = array();
    protected $map             = array();
    protected $gmail_map       = array();


    /**
     * Class constructor
     *
     * @param string $lang File language
     */
    public function __construct($lang = 'en_US')
    {
        // Localize fields map
        if ($lang && $lang != 'en_US') {
            if (file_exists(RCUBE_LOCALIZATION_DIR . "$lang/csv2vcard.inc")) {
                include RCUBE_LOCALIZATION_DIR . "$lang/csv2vcard.inc";
            }

            if (!empty($map)) {
                $this->local_label_map = array_merge($this->label_map, $map);
            }
        }

        $this->label_map       = array_flip($this->label_map);
        $this->local_label_map = array_flip($this->local_label_map);
    }

    /**
     * Import contacts from CSV file
     *
     * @param string $csv Content of the CSV file
     */
    public function import($csv)
    {
        // convert to UTF-8
        $head      = substr($csv, 0, 4096);
        $charset   = rcube_charset::detect($head, RCUBE_CHARSET);
        $csv       = rcube_charset::convert($csv, $charset);
        $csv       = preg_replace(array('/^[\xFE\xFF]{2}/', '/^\xEF\xBB\xBF/', '/^\x00+/'), '', $csv); // also remove BOM
        $head      = '';
        $prev_line = false;

        $this->map       = array();
        $this->gmail_map = array();

        // Parse file
        foreach (preg_split("/[\r\n]+/", $csv) as $line) {
            if (!empty($prev_line)) {
                $line = '"' . $line;
            }

            $elements = $this->parse_line($line);

            if (empty($elements)) {
                continue;
            }

            // Parse header
            if (empty($this->map)) {
                $this->parse_header($elements);
                if (empty($this->map)) {
                    break;
                }
            }
            // Parse data row
            else {
                // handle multiline elements (e.g. Gmail)
                if (!empty($prev_line)) {
                    $first = array_shift($elements);

                    if ($first[0] == '"') {
                        $prev_line[count($prev_line)-1] = '"' . $prev_line[count($prev_line)-1] . "\n" . substr($first, 1);
                    }
                    else {
                        $prev_line[count($prev_line)-1] .= "\n" . $first;
                    }

                    $elements = array_merge($prev_line, $elements);
                }

                $last_element = $elements[count($elements)-1];
                if ($last_element[0] == '"') {
                    $elements[count($elements)-1] = substr($last_element, 1);
                    $prev_line = $elements;
                    continue;
                }
                $this->csv_to_vcard($elements);
                $prev_line = false;
            }
        }
    }

    /**
     * Export vCards
     *
     * @return array rcube_vcard List of vcards
     */
    public function export()
    {
        return $this->vcards;
    }

    /**
     * Parse CSV file line
     *
     * @param string $line Line of text from CSV file
     *
     * @return array CSV data extracted from the line
     */
    protected function parse_line($line)
    {
        $line = trim($line);
        if (empty($line)) {
            return null;
        }

        $fields = rcube_utils::explode_quoted_string(',', $line);

        // remove quotes if needed
        if (!empty($fields)) {
            foreach ($fields as $idx => $value) {
                if (($len = strlen($value)) > 1 && $value[0] == '"' && $value[$len-1] == '"') {
                    // remove surrounding quotes
                    $value = substr($value, 1, -1);
                    // replace doubled quotes inside the string with single quote
                    $value = str_replace('""', '"', $value);

                    $fields[$idx] = $value;
                }
            }
        }

        return $fields;
    }

    /**
     * Parse CSV header line, detect fields mapping
     *
     * @param array $elements Array of field names from a first line in CSV file
     */
    protected function parse_header($elements)
    {
        $map1 = array();
        $map2 = array();
        $size = count($elements);

        // check English labels
        for ($i = 0; $i < $size; $i++) {
            $label = $this->label_map[$elements[$i]];
            if ($label && !empty($this->csv2vcard_map[$label])) {
                $map1[$i] = $this->csv2vcard_map[$label];
            }
        }

        // check localized labels
        if (!empty($this->local_label_map)) {
            for ($i = 0; $i < $size; $i++) {
                $label = $this->local_label_map[$elements[$i]];

                // special localization label
                if ($label && $label[0] == '_') {
                    $label = substr($label, 1);
                }

                if ($label && !empty($this->csv2vcard_map[$label])) {
                    $map2[$i] = $this->csv2vcard_map[$label];
                }
            }
        }

        // If nothing recognized fallback to simple non-localized labels
        if (empty($map1) && empty($map2)) {
            for ($i = 0; $i < $size; $i++) {
                $label = str_replace(' ', '_', strtolower($elements[$i]));
                if (!empty($this->csv2vcard_map[$label])) {
                    $map1[$i] = $this->csv2vcard_map[$label];
                }
            }
        }

        $this->map = count($map1) >= count($map2) ? $map1 : $map2;

        // support special Gmail format
        foreach ($this->gmail_label_map as $key => $items) {
            $num = 1;
            while (($_key = "$key $num - Type") && ($found = array_search($_key, $elements)) !== false) {
                $this->gmail_map["$key:$num"] = array('_key' => $key, '_idx' => $found);
                foreach (array_keys($items) as $item_key) {
                    $_key = "$key $num - $item_key";
                    if (($found = array_search($_key, $elements)) !== false) {
                        $this->gmail_map["$key:$num"][$item_key] = $found;
                    }
                }

                $num++;
            }
        }
    }

    /**
     * Convert CSV data row to vCard
     *
     * @param array $data CSV data array
     */
    protected function csv_to_vcard($data)
    {
        $contact = array();
        foreach ($this->map as $idx => $name) {
            $value = $data[$idx];
            if ($value !== null && $value !== '') {
                if (!empty($contact[$name])) {
                    $contact[$name]   = (array) $contact[$name];
                    $contact[$name][] = $value;
                }
                else {
                   $contact[$name] = $value;
                }
            }
        }

        // Gmail format support
        foreach ($this->gmail_map as $idx => $item) {
            $type = preg_replace('/[^a-z]/', '', strtolower($data[$item['_idx']]));
            $key  = $item['_key'];

            unset($item['_idx']);
            unset($item['_key']);

            foreach ($item as $item_key => $item_idx) {
                $value = $data[$item_idx];
                if ($value !== null && $value !== '') {
                    foreach (array($type, '*') as $_type) {
                        if ($data_idx = $this->gmail_label_map[$key][$item_key][$_type]) {
                            $value = explode(' ::: ', $value);

                            if (!empty($contact[$data_idx])) {
                                $contact[$data_idx]   = array_merge((array) $contact[$data_idx], $value);
                            }
                            else {
                                $contact[$data_idx] = $value;
                            }
                            break;
                        }
                    }
                }
            }
        }

        if (empty($contact)) {
            return;
        }

        // Handle special values
        if (!empty($contact['birthday-d']) && !empty($contact['birthday-m']) && !empty($contact['birthday-y'])) {
            $contact['birthday'] = $contact['birthday-y'] .'-' .$contact['birthday-m'] . '-' . $contact['birthday-d'];
        }

        if (!empty($contact['groups'])) {
            // categories/groups separator in vCard is ',' not ';'
            $contact['groups'] = str_replace(',', '', $contact['groups']);
            $contact['groups'] = str_replace(';', ',', $contact['groups']);

            if (!empty($this->gmail_map)) {
                // remove "* " added by GMail
                $contact['groups'] = str_replace('* ', '', $contact['groups']);
                // replace strange delimiter
                $contact['groups'] = str_replace(' ::: ', ',', $contact['groups']);
            }
        }

        // Empty dates, e.g. "0/0/00", "0000-00-00 00:00:00"
        foreach (array('birthday', 'anniversary') as $key) {
            if (!empty($contact[$key])) {
                $date = preg_replace('/[0[:^word:]]/', '', $contact[$key]);
                if (empty($date)) {
                    unset($contact[$key]);
                }
            }
        }

        if (!empty($contact['gender']) && ($gender = strtolower($contact['gender']))) {
            if (!in_array($gender, array('male', 'female'))) {
                unset($contact['gender']);
            }
        }

        // Convert address(es) to rcube_vcard data
        foreach ($contact as $idx => $value) {
            $name = explode(':', $idx);
            if (in_array($name[0], array('street', 'locality', 'region', 'zipcode', 'country'))) {
                $contact['address:'.$name[1]][$name[0]] = $value;
                unset($contact[$idx]);
            }
        }

        // Create vcard object
        $vcard = new rcube_vcard();
        foreach ($contact as $name => $value) {
            $name = explode(':', $name);
            if (is_array($value) && $name[0] != 'address') {
                foreach ((array) $value as $val) {
                    $vcard->set($name[0], $val, $name[1]);
                }
            }
            else {
                $vcard->set($name[0], $value, $name[1]);
            }
        }

        // add to the list
        $this->vcards[] = $vcard;
    }
}
