diff options
24 files changed, 675 insertions, 19 deletions
@@ -1,6 +1,11 @@ CHANGELOG RoundCube Webmail --------------------------- +2008/09/02 (thomasb) +---------- +- Add feature to import contacts from vcard files (#1326103) +- Mark form buttons that provide the most obvious operation (mainaction) + 2008/08/30 (alec) ---------- - Improved HTML to TXT conversion by html2text class update diff --git a/program/include/main.inc b/program/include/main.inc index e337c4eb8..83ed8c02e 100644 --- a/program/include/main.inc +++ b/program/include/main.inc @@ -792,19 +792,20 @@ function format_email_recipient($email, $name='') * * @param mixed Debug message or data */ -function console($msg) +function console() { - if (!is_string($msg)) - $msg = var_export($msg, true); + $msg = array(); + foreach (func_get_args() as $arg) + $msg[] = !is_string($arg) ? var_export($arg, true) : $arg; if (!($GLOBALS['CONFIG']['debug_level'] & 4)) - write_log('console', $msg); + write_log('console', join(";\n", $msg)); else if ($GLOBALS['OUTPUT']->ajax_call) - print "/*\n $msg \n*/\n"; + print "/*\n " . join(";\n", $msg) . " \n*/\n"; else { print '<div style="background:#eee; border:1px solid #ccc; margin-bottom:3px; padding:6px"><pre>'; - print $msg; + print join(";<br/>\n", $msg); print "</pre></div>\n"; } } diff --git a/program/include/rcube_contacts.php b/program/include/rcube_contacts.php index 811ec4cc3..7ef2af1df 100644 --- a/program/include/rcube_contacts.php +++ b/program/include/rcube_contacts.php @@ -34,7 +34,7 @@ class rcube_contacts var $result = null; var $search_fields; var $search_string; - var $table_cols = array('name', 'email', 'firstname', 'surname'); + var $table_cols = array('name', 'email', 'firstname', 'surname', 'vcard'); /** public properties */ var $primary_key = 'contact_id'; diff --git a/program/include/rcube_template.php b/program/include/rcube_template.php index ba7353972..db2275ed4 100755 --- a/program/include/rcube_template.php +++ b/program/include/rcube_template.php @@ -670,7 +670,7 @@ class rcube_template extends rcube_html_page * @todo Remove all inline JS calls and use jQuery instead. * @todo Remove all sprintf()'s - they are pretty, but also slow. */ - private function button($attrib) + public function button($attrib) { static $sa_buttons = array(); static $s_button_count = 100; diff --git a/program/include/rcube_vcard.php b/program/include/rcube_vcard.php new file mode 100644 index 000000000..560d37d17 --- /dev/null +++ b/program/include/rcube_vcard.php @@ -0,0 +1,386 @@ +<?php + +/* + +-----------------------------------------------------------------------+ + | program/include/rcube_vcard.php | + | | + | This file is part of the RoundCube Webmail client | + | Copyright (C) 2008, RoundCube Dev. - Switzerland | + | Licensed under the GNU GPL | + | | + | PURPOSE: | + | Logical representation of a vcard address record | + +-----------------------------------------------------------------------+ + | Author: Thomas Bruederli <roundcube@gmail.com> | + +-----------------------------------------------------------------------+ + + $Id: $ + +*/ + + +/** + * Logical representation of a vcard-based address record + * Provides functions to parse and export vCard data format + * + * @package Addressbook + * @author Thomas Bruederli <roundcube@gmail.com> + */ +class rcube_vcard +{ + private $raw = array(); + + public $business = false; + public $displayname; + public $surname; + public $firstname; + public $middlename; + public $nickname; + public $organization; + public $notes; + public $email = array(); + + + /** + * Constructor + */ + public function __construct($vcard = null) + { + if (!empty($vcard)) + $this->load($vcard); + } + + + /** + * Load record from (internal, unfolded) vcard 3.0 format + * + * @param string vCard string to parse + */ + public function load($vcard) + { + $this->raw = self::vcard_decode($vcard); + + // find well-known address fields + $this->displayname = $this->raw['FN'][0]; + $this->surname = $this->raw['N'][0][0]; + $this->firstname = $this->raw['N'][0][1]; + $this->middlename = $this->raw['N'][0][2]; + $this->nickname = $this->raw['NICKNAME'][0]; + $this->organization = $this->raw['ORG'][0]; + $this->business = ($this->raw['X-ABShowAs'][0] == 'COMPANY') || (join('', (array)$this->raw['N'][0]) == '' && !empty($this->organization)); + + foreach ((array)$this->raw['EMAIL'] as $i => $raw_email) + $this->email[$i] = $raw_email[0]; + + // make the pref e-mail address the first entry in $this->email + $pref_index = $this->get_type_index('EMAIL', 'pref'); + if ($pref_index > 0) { + $tmp = $this->email[0]; + $this->email[0] = $this->email[$pref_index]; + $this->email[$pref_index] = $tmp; + } + } + + + /** + * Convert the data structure into a vcard 3.0 string + */ + public function export() + { + return self::rfc2425_fold(self::vcard_encode($this->raw)); + } + + + /** + * Setter for address record fields + * + * @param string Field name + * @param string Field value + * @param string Section name + */ + public function set($field, $value, $section = 'home') + { + switch ($field) { + case 'name': + case 'displayname': + $this->raw['FN'][0] = $value; + break; + + case 'firstname': + $this->raw['N'][0][1] = $value; + break; + + case 'surname': + $this->raw['N'][0][0] = $value; + break; + + case 'nickname': + $this->raw['NICKNAME'][0] = $value; + break; + + case 'organization': + $this->raw['ORG'][0] = $value; + 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; + } + break; + } + } + + + /** + * Find index with the '$type' attribute + * + * @param string Field name + * @return int Field index having $type set + */ + private function get_type_index($field, $type = 'pref') + { + $result = 0; + if ($this->raw[$field]) { + foreach ($this->raw[$field] as $i => $data) { + if (is_array($data['type']) && in_array_nocase('pref', $data['type'])) + $result = $i; + } + } + + return $result; + } + + + /** + * Factory method to import a vcard file + * + * @param string vCard file content + * @return array List of rcube_vcard objects + */ + public static function import($data) + { + $out = array(); + + // detect charset and convert to utf-8 + $encoding = self::detect_encoding($data); + if ($encoding && $encoding != RCMAIL_CHARSET) { + $data = rcube_charset_convert($data, $encoding); + } + + $vcard_block = ''; + $in_vcard_block = false; + + foreach (preg_split("/[\r\n]+/", $data) as $i => $line) { + if ($in_vcard_block && !empty($line)) + $vcard_block .= $line . "\n"; + + if (trim($line) == 'END:VCARD') { + // parse vcard + $obj = new rcube_vcard(self::cleanup($vcard_block)); + if (!empty($obj->displayname)) + $out[] = $obj; + + $in_vcard_block = false; + } + else if (trim($line) == 'BEGIN:VCARD') { + $vcard_block = $line . "\n"; + $in_vcard_block = true; + } + } + + return $out; + } + + + /** + * Normalize vcard data for better parsing + * + * @param string vCard block + * @return string Cleaned vcard block + */ + private static function cleanup($vcard) + { + // Convert special types (like Skype) to normal type='skype' classes with this simple regex ;) + $vcard = preg_replace( + '/item(\d+)\.(TEL|URL)([^:]*?):(.*?)item\1.X-ABLabel:(?:_\$!<)?([\w-() ]*)(?:>!\$_)?./s', + '\2;type=\5\3:\4', + $vcard); + + // Remove cruft like item1.X-AB*, item1.ADR instead of ADR, and empty lines + $vcard = preg_replace(array('/^item\d*\.X-AB.*$/m', '/^item\d*\./m', "/\n+/"), array('', '', "\n"), $vcard); + + // remove vcard 2.1 charset definitions + $vcard = preg_replace('/;CHARSET=[^:]+/', '', $vcard); + + return $vcard; + } + + + private static function rfc2425_fold($val) + { + return preg_replace('/:([^\n]{72,})/e', '":\n ".rtrim(chunk_split("\\1", 72, "\n "))', $val) . "\n\n"; + } + + + /** + * Decodes a vcard block (vcard 3.0 format, unfolded) + * into an array structure + * + * @param string vCard block to parse + * @return array Raw data structure + */ + private static function vcard_decode($vcard) + { + // Perform RFC2425 line unfolding + $vcard = preg_replace(array("/\r/", "/\n\s+/"), '', $vcard); + + $data = array(); + if (preg_match_all('/^([^\\:]*):(.+)$/m', $vcard, $regs, PREG_SET_ORDER)) { + foreach($regs as $line) { + // convert 2.1-style "EMAIL;internet;home:" to 3.0-style "EMAIL;TYPE=internet,home:" + if(($data['VERSION'][0] == "2.1") && preg_match('/^([^;]+);([^:]+)/', $line[1], $regs2) && !preg_match('/^TYPE=/i', $regs2[2])) { + $line[1] = $regs2[1] . ";TYPE=" . strtr($regs2[2], array(";" => ",")); + } + + if (!preg_match('/^(BEGIN|END)$/', $line[1]) && preg_match_all('/([^\\;]+);?/', $line[1], $regs2)) { + $entry = array(self::vcard_unquote($line[2])); + + foreach($regs2[1] as $attrid => $attr) { + if ((list($key, $value) = explode('=', $attr)) && $value) + $entry[strtolower($key)] = array_merge((array)$entry[strtolower($key)], (array)self::vcard_unquote($value, ',')); + elseif ($attrid > 0) + $entry[$key] = true; # true means attr without =value + } + + $data[$regs2[1][0]][] = count($entry) > 1 ? $entry : $entry[0]; + } + } + + unset($data['VERSION']); + } + + return $data; + } + + + /** + * Split quoted string + * + * @param string vCard string to split + * @param string Separator char/string + * @return array List with splitted values + */ + private static function vcard_unquote($s, $sep = ';') + { + // break string into parts separated by $sep, but leave escaped $sep alone + if (count($parts = explode($sep, strtr($s, array("\\$sep" => "\007")))) > 1) { + foreach($parts as $s) { + $result[] = self::vcard_unquote(strtr($s, array("\007" => "\\$sep")), $sep); + } + return $result; + } + else { + return strtr($s, array("\r" => '', '\\\\' => '\\', '\n' => "\n", '\,' => ',', '\;' => ';', '\:' => ':')); + } + } + + + /** + * Encodes an entry for storage in our database (vcard 3.0 format, unfolded) + * + * @param array Raw data structure to encode + * @return string vCard encoded string + */ + static function vcard_encode($data) + { + foreach((array)$data as $type => $entries) { + /* valid N has 5 properties */ + while ($type == "N" && count($entries[0]) < 5) + $entries[0][] = ""; + + foreach((array)$entries as $entry) { + $attr = ''; + if (is_array($entry)) { + $value = array(); + foreach($entry as $attrname => $attrvalues) { + if (is_int($attrname)) + $value[] = $attrvalues; + elseif ($attrvalues === true) + $attr .= ";$attrname"; # true means just tag, not tag=value, as in PHOTO;BASE64:... + else { + foreach((array)$attrvalues as $attrvalue) + $attr .= ";$attrname=" . self::vcard_quote($attrvalue, ','); + } + } + } + else { + $value = $entry; + } + + $vcard .= self::vcard_quote($type) . $attr . ':' . self::vcard_quote($value) . "\n"; + } + } + + return "BEGIN:VCARD\nVERSION:3.0\n{$vcard}END:VCARD\n"; + } + + + /** + * Join indexed data array to a vcard quoted string + * + * @param array Field data + * @param string Separator + * @return string Joined and quoted string + */ + private static function vcard_quote($s, $sep = ';') + { + if (is_array($s)) { + foreach($s as $part) { + $r[] = self::vcard_quote($part, $sep); + } + return(implode($sep, (array)$r)); + } + else { + return strtr($s, array('\\' => '\\\\', "\r" => '', "\n" => '\n', ';' => '\;', ':' => '\:')); + } + } + + + /** + * Returns UNICODE type based on BOM (Byte Order Mark) + * + * @param string Input string to test + * @return string Detected encoding + */ + private static function detect_encoding($string) + { + if (substr($string, 0, 4) == "\0\0\xFE\xFF") return 'UTF-32BE'; // Big Endian + if (substr($string, 0, 4) == "\xFF\xFE\0\0") return 'UTF-32LE'; // Little Endian + if (substr($string, 0, 2) == "\xFE\xFF") return 'UTF-16BE'; // Big Endian + if (substr($string, 0, 2) == "\xFF\xFE") return 'UTF-16LE'; // Little Endian + if (substr($string, 0, 3) == "\xEF\xBB\xBF") return 'UTF-8'; + + // No match, check for UTF-8 + // from http://w3.org/International/questions/qa-forms-utf-8.html + if (preg_match('/\A( + [\x09\x0A\x0D\x20-\x7E] + | [\xC2-\xDF][\x80-\xBF] + | \xE0[\xA0-\xBF][\x80-\xBF] + | [\xE1-\xEC\xEE\xEF][\x80-\xBF]{2} + | \xED[\x80-\x9F][\x80-\xBF] + | \xF0[\x90-\xBF][\x80-\xBF]{2} + | [\xF1-\xF3][\x80-\xBF]{3} + | \xF4[\x80-\x8F][\x80-\xBF]{2} + )*\z/xs', substr($string, 0, 2048))) + return 'UTF-8'; + + return null; + } + +} + + diff --git a/program/js/app.js b/program/js/app.js index d64b58c50..ff6ff8cf3 100644 --- a/program/js/app.js +++ b/program/js/app.js @@ -281,7 +281,7 @@ function rcube_webmail() if ((this.env.action=='add' || this.env.action=='edit') && this.gui_objects.editform) this.enable_command('save', true); else - this.enable_command('search', 'reset-search', 'moveto', true); + this.enable_command('search', 'reset-search', 'moveto', 'import', true); this.enable_command('list', true); break; @@ -972,7 +972,7 @@ function rcube_webmail() break; } - // reset quicksearch + // reset quicksearch case 'reset-search': var s = this.env.search_request; this.reset_qsearch(); @@ -983,6 +983,21 @@ function rcube_webmail() this.list_contacts(this.env.source); break; + case 'import': + if (this.env.action == 'import' && this.gui_objects.importform) { + var file = document.getElementById('rcmimportfile'); + if (file && !file.value) { + alert(this.get_label('selectimportfile')); + break; + } + this.gui_objects.importform.submit(); + this.set_busy(true, 'importwait'); + this.lock_form(this.gui_objects.importform, true); + } + else + this.goto_url('import'); + break + // collapse/expand folder case 'collapse-folder': if (props) diff --git a/program/localization/de_CH/labels.inc b/program/localization/de_CH/labels.inc index 3b32459d6..a5909b8ed 100644 --- a/program/localization/de_CH/labels.inc +++ b/program/localization/de_CH/labels.inc @@ -184,6 +184,12 @@ $labels['nextpage'] = 'Nächste Seite'; $labels['lastpage'] = 'Letzte Seite'; $labels['groups'] = 'Gruppen'; $labels['personaladrbook'] = 'Persönliches Adressbuch'; +$labels['import'] = 'Importieren'; +$labels['importcontacts'] = 'Adressen importieren'; +$labels['importfromfile'] = 'Import aus Datei:'; +$labels['importreplace'] = 'Bestehendes Adressbuch komplett ersetzen'; +$labels['importtext'] = 'Sie können Kontakte aus einem bestehenden Adressbuch hochladen.<br/>Es können Adressbücher im <a href="http://de.wikipedia.org/wiki/VCard">vCard-Format</a> importiert werden.'; +$labels['done'] = 'Fertig'; $labels['settingsfor'] = 'Einstellungen für'; $labels['preferences'] = 'Einstellungen'; $labels['userpreferences'] = 'Benutzereinstellungen'; diff --git a/program/localization/de_CH/messages.inc b/program/localization/de_CH/messages.inc index 83817b5bb..201bf9469 100644 --- a/program/localization/de_CH/messages.inc +++ b/program/localization/de_CH/messages.inc @@ -80,5 +80,10 @@ $messages['errorsendingreceipt'] = 'Bestätigung konnte nicht gesendet werden'; $messages['nodeletelastidentity'] = 'Sie können diesen Absender nicht löschen'; $messages['addsubfolderhint'] = 'Wird als Unterdornder des aktuell selektieren Ordners erstellt'; $messages['forbiddencharacter'] = 'Der Ordnername enthält ein ungültiges Zeichen'; +$messages['selectimportfile'] = 'Bitte wählen Sie eine Datei zum Importieren aus'; +$messages['addresswriterror'] = 'Das gewählte Adressbuch kann nicht verändert werden'; +$messages['importwait'] = 'Daten werden importiert, bitte warten...'; +$messages['importerror'] = 'Import fehlgeschlagen! Die hochgeladene Datei ist nicht im vCard-Format.'; +$messages['importconfirm'] = '<b>Es wurden $inserted Adressen erfolgreich importiert und $skipped bestehende Einträge übersprungen</b>:<p><em>$names</em></p>'; ?> diff --git a/program/localization/en_US/labels.inc b/program/localization/en_US/labels.inc index 43d91f94b..b628799b2 100644 --- a/program/localization/en_US/labels.inc +++ b/program/localization/en_US/labels.inc @@ -232,6 +232,12 @@ $labels['lastpage'] = 'Show last set'; $labels['groups'] = 'Groups'; $labels['personaladrbook'] = 'Personal Addresses'; +$labels['import'] = 'Import'; +$labels['importcontacts'] = 'Import contacts'; +$labels['importfromfile'] = 'Import from file:'; +$labels['importreplace'] = 'Replace the entire address book'; +$labels['importtext'] = 'You can upload contacts from an existing address book.<br/>We currently support importing addresses from the <a href="http://en.wikipedia.org/wiki/VCard">vCard</a> data format.'; +$labels['done'] = 'Done'; // settings $labels['settingsfor'] = 'Settings for'; diff --git a/program/localization/en_US/messages.inc b/program/localization/en_US/messages.inc index 8777bb17a..a2a955936 100644 --- a/program/localization/en_US/messages.inc +++ b/program/localization/en_US/messages.inc @@ -80,5 +80,10 @@ $messages['errorsendingreceipt'] = 'Could not send the receipt'; $messages['nodeletelastidentity'] = 'You cannot delete this identity, it\'s your last one.'; $messages['addsubfolderhint'] = 'This folder will be created as subfolder of the currently selected one'; $messages['forbiddencharacter'] = 'Folder name contains a forbidden character'; +$messages['selectimportfile'] = 'Please select a file to upload'; +$messages['addresswriterror'] = 'The selected address book is not writeable'; +$messages['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>'; ?>
\ No newline at end of file diff --git a/program/steps/addressbook/import.inc b/program/steps/addressbook/import.inc new file mode 100644 index 000000000..22913493b --- /dev/null +++ b/program/steps/addressbook/import.inc @@ -0,0 +1,179 @@ +<?php + +/* + +-----------------------------------------------------------------------+ + | program/steps/addressbook/import.inc | + | | + | This file is part of the RoundCube Webmail client | + | Copyright (C) 2008, RoundCube Dev. - Switzerland | + | Licensed under the GNU GPL | + | | + | PURPOSE: | + | Import contacts from a vCard or CSV file | + | | + +-----------------------------------------------------------------------+ + | Author: Thomas Bruederli <roundcube@gmail.com> | + +-----------------------------------------------------------------------+ + + $Id: $ + +*/ + +/** + * Handler function to display the import/upload form + */ +function rcmail_import_form($attrib) +{ + global $RCMAIL, $OUTPUT; + + $attrib += array('id' => "rcmImportForm"); + + $upload = new html_inputfield(array('type' => 'file', 'name' => '_file', 'id' => 'rcmimportfile', 'size' => 40)); + $form = html::p(null, html::label('rcmimportfile', rcube_label('importfromfile')) . html::br() . $upload->show()); + + $check_replace = new html_checkbox(array('name' => '_replace', 'value' => 1, 'id' => 'rcmimportreplace')); + $form .= html::p(null, $check_replace->show(get_input_value('_replace', RCUBE_INPUT_GPC)) . + html::label('rcmimportreplace', rcube_label('importreplace'))); + + $OUTPUT->add_label('selectimportfile','importwait'); + $OUTPUT->add_gui_object('importform', $attrib['id']); + + $out = html::p(null, Q(rcube_label('importtext'), 'show')); + + $out .= $OUTPUT->form_tag(array( + 'action' => $RCMAIL->url('import'), + 'method' => 'post', + 'enctype' => 'multipart/form-data') + $attrib, + $form); + + return $out; +} + + +/** + * Render the confirmation page for the import process + */ +function rcmail_import_confirm($attrib) +{ + global $IMPORT_STATS; + + $vars = get_object_vars($IMPORT_STATS); + $vars['names'] = join(', ', $IMPORT_STATS->names); + + return html::p($attrib, Q(rcube_label(array( + 'name' => 'importconfirm', + 'nr' => $IMORT_STATS->inserted, + 'vars' => $vars, + )), 'show')); +} + + +/** + * Create navigation buttons for the current import step + */ +function rcmail_import_buttons($attrib) +{ + global $IMPORT_STATS, $OUTPUT; + + $attrib += array('type' => "input"); + unset($attrib['name']); + + if (is_object($IMPORT_STATS)) { + $attrib['class'] = trim($attrib['class'] . ' mainaction'); + $out = $OUTPUT->button(array('command' => "list", 'label' => "done") + $attrib); + } + else { + $out = $OUTPUT->button(array('command' => "list", 'label' => "cancel") + $attrib); + $out .= ' '; + $attrib['class'] = trim($attrib['class'] . ' mainaction'); + $out .= $OUTPUT->button(array('command' => "import", 'label' => "import") + $attrib); + } + + return $out; +} + + +/** The import process **/ + +$importstep = 'rcmail_import_form'; + +if ($_FILES['_file']['tmp_name'] && is_uploaded_file($_FILES['_file']['tmp_name'])) { + + $replace = (bool)get_input_value('_replace', RCUBE_INPUT_GPC); + $CONTACTS = $RCMAIL->get_address_book(null, true); + + // let rcube_vcard do the hard work :-) + $vcards = rcube_vcard::import(file_get_contents($_FILES['_file']['tmp_name'])); + + // no vcards detected + if (!count($vcards)) { + $OUTPUT->show_message('importerror', 'error'); + } + else if ($CONTACTS->readonly) { + $OUTPUT->show_message('addresswriterror', 'error'); + } + else { + $IMPORT_STATS = new stdClass; + $IMPORT_STATS->names = array(); + $IMPORT_STATS->count = count($vcards); + $IMPORT_STATS->inserted = $IMPORT_STATS->skipped = $IMPORT_STATS->errors = 0; + + if ($replace) + $CONTACTS->delete_all(); + + foreach ($vcards as $vcard) { + $email = $vcard->email[0]; + + if (!$replace) { + // compare e-mail address + $existing = $CONTACTS->search('email', $email, false, false); + if (!$existing->count) { // compare display name + $existing = $CONTACTS->search('name', $vcard->displayname, false, false); + } + if ($existing->count) { + $IMPORT_STATS->skipped++; + continue; + } + } + + $success = $CONTACTS->insert(array( + 'name' => $vcard->displayname, + 'firstname' => $vcard->firstname, + 'surname' => $vcard->surname, + 'email' => $email, + 'vcard' => $vcard->export(), + )); + + if ($success) { + $IMPORT_STATS->inserted++; + $IMPORT_STATS->names[] = $vcard->displayname; + } + else { + $IMPORT_STATS->errors++; + } + } + + $importstep = 'rcmail_import_confirm'; + } +} +else if ($err = $_FILES['_file']['error']) { + if ($err == UPLOAD_ERR_INI_SIZE || $err == UPLOAD_ERR_FORM_SIZE) { + $OUTPUT->show_message('filesizeerror', 'error', array('size' => show_bytes(parse_bytes(ini_get('upload_max_filesize'))))); + } + else { + $OUTPUT->show_message('fileuploaderror', 'error'); + } +} + + +$OUTPUT->set_pagetitle(rcube_label('importcontacts')); + +$OUTPUT->add_handlers(array( + 'importstep' => $importstep, + 'importnav' => 'rcmail_import_buttons', +)); + +// render page +$OUTPUT->send('importcontacts'); + +?>
\ No newline at end of file diff --git a/skins/default/addresses.css b/skins/default/addresses.css index 8209c1f56..27612c304 100644 --- a/skins/default/addresses.css +++ b/skins/default/addresses.css @@ -30,7 +30,7 @@ color: #333333; } -#directorylist, #addresslist +#directorylist, #addresslist, #importbox { position: absolute; top: 85px; @@ -54,6 +54,20 @@ width: 340px; } +#importbox +{ + left: 20px; + right: 40px; + height: auto; + bottom: auto; + padding-bottom: 4ex; +} + +#importbox a +{ + color: blue; +} + #directorylist ul { list-style: none; diff --git a/skins/default/common.css b/skins/default/common.css index 22c65420f..c9a641fa7 100644 --- a/skins/default/common.css +++ b/skins/default/common.css @@ -76,7 +76,8 @@ input, textarea } input[type="checkbox"], -input[type="radio"] +input[type="radio"], +input[type="file"] { border: 0; padding: 0; @@ -99,6 +100,12 @@ input.button:hover color: black; } +input.mainaction +{ + font-weight: bold; + border: 1px solid #999; +} + img { behavior: url('skins/default/pngbehavior.htc'); diff --git a/skins/default/images/buttons/adr_import_act.png b/skins/default/images/buttons/adr_import_act.png Binary files differnew file mode 100644 index 000000000..c600c7121 --- /dev/null +++ b/skins/default/images/buttons/adr_import_act.png diff --git a/skins/default/images/buttons/adr_import_pas.png b/skins/default/images/buttons/adr_import_pas.png Binary files differnew file mode 100644 index 000000000..265d1b4fd --- /dev/null +++ b/skins/default/images/buttons/adr_import_pas.png diff --git a/skins/default/images/buttons/adr_import_sel.png b/skins/default/images/buttons/adr_import_sel.png Binary files differnew file mode 100644 index 000000000..d78bbfdea --- /dev/null +++ b/skins/default/images/buttons/adr_import_sel.png diff --git a/skins/default/templates/addcontact.html b/skins/default/templates/addcontact.html index e52b8c737..70a796aa2 100644 --- a/skins/default/templates/addcontact.html +++ b/skins/default/templates/addcontact.html @@ -14,7 +14,7 @@ <p><br /> <input type="button" value="<roundcube:label name="cancel" />" class="button" onclick="history.back()" /> -<roundcube:button command="save" type="input" class="button" label="save" /> +<roundcube:button command="save" type="input" class="button" label="save mainaction" /> </p> </form> diff --git a/skins/default/templates/addressbook.html b/skins/default/templates/addressbook.html index a97f584de..6083ac66e 100644 --- a/skins/default/templates/addressbook.html +++ b/skins/default/templates/addressbook.html @@ -15,7 +15,7 @@ <roundcube:button command="add" imageSel="/images/buttons/add_contact_sel.png" imageAct="/images/buttons/add_contact_act.png" imagePas="/images/buttons/add_contact_pas.png" width="32" height="32" title="newcontact" /> <roundcube:button command="delete" imageSel="/images/buttons/delete_sel.png" imageAct="/images/buttons/delete_act.png" imagePas="/images/buttons/delete_pas.png" width="32" height="32" title="deletecontact" /> <roundcube:button command="compose" imageSel="/images/buttons/compose_sel.png" imageAct="/images/buttons/compose_act.png" imagePas="/images/buttons/compose_pas.png" width="32" height="32" title="composeto" /> -<roundcube:button command="print" imageSel="/images/buttons/print_sel.png" imageAct="/images/buttons/print_act.png" imagePas="/images/buttons/print_pas.png" width="32" height="32" title="print" /> +<roundcube:button command="import" imageSel="/images/buttons/download_sel.png" imageAct="/images/buttons/adr_import_act.png" imagePas="/images/buttons/adr_import_pas.png" width="32" height="32" title="importcontacts" /> <roundcube:button command="export" imageSel="/images/buttons/download_sel.png" imageAct="/images/buttons/download_act.png" imagePas="/images/buttons/download_pas.png" width="32" height="32" title="export" /> </div> diff --git a/skins/default/templates/compose.html b/skins/default/templates/compose.html index f3cc9279c..da8b45710 100644 --- a/skins/default/templates/compose.html +++ b/skins/default/templates/compose.html @@ -133,7 +133,7 @@ function rcmail_prev_sibling(elm) <table border="0" cellspacing="0" width="100%" summary=""><tbody> <tr> <td style="white-space: nowrap"> -<roundcube:button type="input" command="send" class="button" label="sendmessage" tabindex="8" /> +<roundcube:button type="input" command="send" class="button mainaction" label="sendmessage" tabindex="8" /> <roundcube:button type="input" command="list" class="button" label="cancel" tabindex="9" /> </td> <td style="text-align:center; white-space: nowrap"> diff --git a/skins/default/templates/editcontact.html b/skins/default/templates/editcontact.html index bff81df0f..b92f709d9 100644 --- a/skins/default/templates/editcontact.html +++ b/skins/default/templates/editcontact.html @@ -14,7 +14,7 @@ <p><br /> <roundcube:button command="show" type="input" class="button" label="cancel" /> -<roundcube:button command="save" type="input" class="button" label="save" /> +<roundcube:button command="save" type="input" class="button mainaction" label="save" /> </p> </form> diff --git a/skins/default/templates/editidentity.html b/skins/default/templates/editidentity.html index 04a3e8b39..94c70e615 100644 --- a/skins/default/templates/editidentity.html +++ b/skins/default/templates/editidentity.html @@ -25,7 +25,7 @@ <p><br /> <roundcube:button command="delete" type="input" class="button" label="delete" /> -<roundcube:button command="save" type="input" class="button" label="save" /> +<roundcube:button command="save" type="input" class="button mainaction" label="save" /> </p> </div> </div> diff --git a/skins/default/templates/importcontacts.html b/skins/default/templates/importcontacts.html new file mode 100644 index 000000000..35a8e41f8 --- /dev/null +++ b/skins/default/templates/importcontacts.html @@ -0,0 +1,27 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> +<title><roundcube:object name="pagetitle" /></title> +<roundcube:include file="/includes/links.html" /> +<link rel="stylesheet" type="text/css" href="/addresses.css" /> +</head> +<body> + +<roundcube:include file="/includes/taskbar.html" /> +<roundcube:include file="/includes/header.html" /> + +<div id="importbox"> +<div class="boxtitle"><roundcube:label name="importcontacts" /></div> + +<div style="padding-left:20px; width:48em"> +<roundcube:object name="importstep" /> + +<p><br /> +<roundcube:object name="importnav" class="button" /> +</p> + +</div> +</div> + +</body> +</html> diff --git a/skins/default/templates/login.html b/skins/default/templates/login.html index a3089d781..9e1cc4a00 100644 --- a/skins/default/templates/login.html +++ b/skins/default/templates/login.html @@ -39,7 +39,7 @@ <form name="form" action="./" method="post"> <roundcube:object name="loginform" form="form" autocomplete="off" /> -<p style="text-align:center;"><input type="submit" class="button" value="<roundcube:label name='login' />" /></p> +<p style="text-align:center;"><input type="submit" class="button mainaction" value="<roundcube:label name='login' />" /></p> </form> </div> diff --git a/skins/default/templates/settings.html b/skins/default/templates/settings.html index dfb67e05a..490d35de4 100644 --- a/skins/default/templates/settings.html +++ b/skins/default/templates/settings.html @@ -17,7 +17,7 @@ <div style="padding:15px"> <roundcube:object name="userprefs"> -<p><roundcube:button command="save" type="input" class="button" label="save" /></p> +<p><roundcube:button command="save" type="input" class="button mainaction" label="save" /></p> </div> </div> |