Changeset View
Changeset View
Standalone View
Standalone View
plugins/libcalendaring/libcalendaring.php
Show All 34 Lines | class libcalendaring extends rcube_plugin | ||||
public $timezone; | public $timezone; | ||||
public $gmt_offset; | public $gmt_offset; | ||||
public $dst_active; | public $dst_active; | ||||
public $timezone_offset; | public $timezone_offset; | ||||
public $ical_parts = array(); | public $ical_parts = array(); | ||||
public $ical_message; | public $ical_message; | ||||
public $defaults = array( | public $defaults = array( | ||||
'calendar_date_format' => "Y-m-d", | 'calendar_date_format' => "Y-m-d", | ||||
'calendar_date_short' => "M-j", | 'calendar_date_short' => "M-j", | ||||
'calendar_date_long' => "F j Y", | 'calendar_date_long' => "F j Y", | ||||
'calendar_date_agenda' => "l M-d", | 'calendar_date_agenda' => "l M-d", | ||||
'calendar_time_format' => "H:m", | 'calendar_time_format' => "H:m", | ||||
'calendar_first_day' => 1, | 'calendar_first_day' => 1, | ||||
'calendar_first_hour' => 6, | 'calendar_first_hour' => 6, | ||||
'calendar_date_format_sets' => array( | 'calendar_date_format_sets' => array( | ||||
'Y-m-d' => array('d M Y', 'm-d', 'l m-d'), | 'Y-m-d' => array('d M Y', 'm-d', 'l m-d'), | ||||
'Y/m/d' => array('d M Y', 'm/d', 'l m/d'), | 'Y/m/d' => array('d M Y', 'm/d', 'l m/d'), | ||||
'Y.m.d' => array('d M Y', 'm.d', 'l m.d'), | 'Y.m.d' => array('d M Y', 'm.d', 'l m.d'), | ||||
'd-m-Y' => array('d M Y', 'd-m', 'l d-m'), | 'd-m-Y' => array('d M Y', 'd-m', 'l d-m'), | ||||
'd/m/Y' => array('d M Y', 'd/m', 'l d/m'), | 'd/m/Y' => array('d M Y', 'd/m', 'l d/m'), | ||||
'd.m.Y' => array('d M Y', 'd.m', 'l d.m'), | 'd.m.Y' => array('d M Y', 'd.m', 'l d.m'), | ||||
'j.n.Y' => array('d M Y', 'd.m', 'l d.m'), | 'j.n.Y' => array('d M Y', 'd.m', 'l d.m'), | ||||
'm/d/Y' => array('M d Y', 'm/d', 'l m/d'), | 'm/d/Y' => array('M d Y', 'm/d', 'l m/d'), | ||||
), | ), | ||||
); | ); | ||||
private static $instance; | private static $instance; | ||||
private $mail_ical_parser; | private $mail_ical_parser; | ||||
/** | /** | ||||
* Singleton getter to allow direct access from other plugins | * Singleton getter to allow direct access from other plugins | ||||
▲ Show 20 Lines • Show All 114 Lines • ▼ Show 20 Lines | class libcalendaring extends rcube_plugin | ||||
/** | /** | ||||
* Shift dates into user's current timezone | * Shift dates into user's current timezone | ||||
* | * | ||||
* @param mixed Any kind of a date representation (DateTime object, string or unix timestamp) | * @param mixed Any kind of a date representation (DateTime object, string or unix timestamp) | ||||
* @return object DateTime object in user's timezone | * @return object DateTime object in user's timezone | ||||
*/ | */ | ||||
public function adjust_timezone($dt, $dateonly = false) | public function adjust_timezone($dt, $dateonly = false) | ||||
{ | { | ||||
if (is_numeric($dt)) | if (is_numeric($dt)) { | ||||
$dt = new DateTime('@'.$dt); | $dt = new DateTime('@'.$dt); | ||||
else if (is_string($dt)) | } | ||||
else if (is_string($dt)) { | |||||
$dt = rcube_utils::anytodatetime($dt); | $dt = rcube_utils::anytodatetime($dt); | ||||
} | |||||
if ($dt instanceof DateTime && !($dt->_dateonly || $dateonly)) { | if ($dt instanceof DateTime && empty($dt->_dateonly) && !$dateonly) { | ||||
$dt->setTimezone($this->timezone); | $dt->setTimezone($this->timezone); | ||||
} | } | ||||
return $dt; | return $dt; | ||||
} | } | ||||
/** | /** | ||||
* | * | ||||
*/ | */ | ||||
public function load_settings() | public function load_settings() | ||||
{ | { | ||||
$this->date_format_defaults(); | $this->date_format_defaults(); | ||||
$settings = array(); | $settings = array(); | ||||
▲ Show 20 Lines • Show All 73 Lines • ▼ Show 20 Lines | private function date_format_defaults() | ||||
} | } | ||||
} | } | ||||
/** | /** | ||||
* Compose a date string for the given event | * Compose a date string for the given event | ||||
*/ | */ | ||||
public function event_date_text($event, $tzinfo = false) | public function event_date_text($event, $tzinfo = false) | ||||
{ | { | ||||
$fromto = '--'; | $fromto = '--'; | ||||
$is_task = !empty($event['_type']) && $event['_type'] == 'task'; | |||||
// handle task objects | // handle task objects | ||||
if ($event['_type'] == 'task' && is_object($event['due'])) { | if ($is_task && !empty($event['due']) && is_object($event['due'])) { | ||||
$date_format = $event['due']->_dateonly ? self::to_php_date_format($this->rc->config->get('calendar_date_format', $this->defaults['calendar_date_format'])) : null; | $date_format = !empty($event['due']->_dateonly) ? self::to_php_date_format($this->rc->config->get('calendar_date_format', $this->defaults['calendar_date_format'])) : null; | ||||
$fromto = $this->rc->format_date($event['due'], $date_format, false); | $fromto = $this->rc->format_date($event['due'], $date_format, false); | ||||
// add timezone information | // add timezone information | ||||
if ($fromto && $tzinfo && ($tzname = $this->timezone->getName())) { | if ($fromto && $tzinfo && ($tzname = $this->timezone->getName())) { | ||||
$fromto .= ' (' . strtr($tzname, '_', ' ') . ')'; | $fromto .= ' (' . strtr($tzname, '_', ' ') . ')'; | ||||
} | } | ||||
return $fromto; | return $fromto; | ||||
▲ Show 20 Lines • Show All 41 Lines • ▼ Show 20 Lines | public function alarm_select($attrib, $alarm_types, $absolute_time = true) | ||||
unset($attrib['name']); | unset($attrib['name']); | ||||
$input_value = new html_inputfield(array('name' => 'alarmvalue[]', 'class' => 'edit-alarm-value form-control', 'size' => 3)); | $input_value = new html_inputfield(array('name' => 'alarmvalue[]', 'class' => 'edit-alarm-value form-control', 'size' => 3)); | ||||
$input_date = new html_inputfield(array('name' => 'alarmdate[]', 'class' => 'edit-alarm-date form-control', 'size' => 10)); | $input_date = new html_inputfield(array('name' => 'alarmdate[]', 'class' => 'edit-alarm-date form-control', 'size' => 10)); | ||||
$input_time = new html_inputfield(array('name' => 'alarmtime[]', 'class' => 'edit-alarm-time form-control', 'size' => 6)); | $input_time = new html_inputfield(array('name' => 'alarmtime[]', 'class' => 'edit-alarm-time form-control', 'size' => 6)); | ||||
$select_type = new html_select(array('name' => 'alarmtype[]', 'class' => 'edit-alarm-type form-control', 'id' => $attrib['id'])); | $select_type = new html_select(array('name' => 'alarmtype[]', 'class' => 'edit-alarm-type form-control', 'id' => $attrib['id'])); | ||||
$select_offset = new html_select(array('name' => 'alarmoffset[]', 'class' => 'edit-alarm-offset form-control')); | $select_offset = new html_select(array('name' => 'alarmoffset[]', 'class' => 'edit-alarm-offset form-control')); | ||||
$select_related = new html_select(array('name' => 'alarmrelated[]', 'class' => 'edit-alarm-related form-control')); | $select_related = new html_select(array('name' => 'alarmrelated[]', 'class' => 'edit-alarm-related form-control')); | ||||
$object_type = $attrib['_type'] ?: 'event'; | $object_type = !empty($attrib['_type']) ? $attrib['_type'] : 'event'; | ||||
$select_type->add($this->gettext('none'), ''); | $select_type->add($this->gettext('none'), ''); | ||||
foreach ($alarm_types as $type) | foreach ($alarm_types as $type) { | ||||
$select_type->add($this->gettext(strtolower("alarm{$type}option")), $type); | $select_type->add($this->gettext(strtolower("alarm{$type}option")), $type); | ||||
} | |||||
foreach (array('-M','-H','-D','+M','+H','+D') as $trigger) | foreach (array('-M','-H','-D','+M','+H','+D') as $trigger) { | ||||
$select_offset->add($this->gettext('trigger' . $trigger), $trigger); | $select_offset->add($this->gettext('trigger' . $trigger), $trigger); | ||||
} | |||||
$select_offset->add($this->gettext('trigger0'), '0'); | $select_offset->add($this->gettext('trigger0'), '0'); | ||||
if ($absolute_time) | if ($absolute_time) { | ||||
$select_offset->add($this->gettext('trigger@'), '@'); | $select_offset->add($this->gettext('trigger@'), '@'); | ||||
} | |||||
$select_related->add($this->gettext('relatedstart'), 'start'); | $select_related->add($this->gettext('relatedstart'), 'start'); | ||||
$select_related->add($this->gettext('relatedend' . $object_type), 'end'); | $select_related->add($this->gettext('relatedend' . $object_type), 'end'); | ||||
// pre-set with default values from user settings | // pre-set with default values from user settings | ||||
$preset = self::parse_alarm_value($this->rc->config->get('calendar_default_alarm_offset', '-15M')); | $preset = self::parse_alarm_value($this->rc->config->get('calendar_default_alarm_offset', '-15M')); | ||||
$hidden = array('style' => 'display:none'); | $hidden = array('style' => 'display:none'); | ||||
Show All 20 Lines | class libcalendaring extends rcube_plugin | ||||
{ | { | ||||
static $_emails = array(); | static $_emails = array(); | ||||
if (empty($user)) { | if (empty($user)) { | ||||
$user = $this->rc->user->get_username(); | $user = $this->rc->user->get_username(); | ||||
} | } | ||||
// return cached result | // return cached result | ||||
if (is_array($_emails[$user])) { | if (isset($_emails[$user])) { | ||||
return $_emails[$user]; | return $_emails[$user]; | ||||
} | } | ||||
$emails = array($user); | $emails = array($user); | ||||
$plugin = $this->rc->plugins->exec_hook('calendar_user_emails', array('emails' => $emails)); | $plugin = $this->rc->plugins->exec_hook('calendar_user_emails', array('emails' => $emails)); | ||||
$emails = array_map('strtolower', $plugin['emails']); | $emails = array_map('strtolower', $plugin['emails']); | ||||
// add all emails from the current user's identities | // add all emails from the current user's identities | ||||
▲ Show 20 Lines • Show All 386 Lines • ▼ Show 20 Lines | public function recurrence_text($rrule) | ||||
$limit = 10; | $limit = 10; | ||||
$exdates = array(); | $exdates = array(); | ||||
$format = $this->rc->config->get('calendar_date_format', $this->defaults['calendar_date_format']); | $format = $this->rc->config->get('calendar_date_format', $this->defaults['calendar_date_format']); | ||||
$format = self::to_php_date_format($format); | $format = self::to_php_date_format($format); | ||||
$format_fn = function($dt) use ($format) { | $format_fn = function($dt) use ($format) { | ||||
return rcmail::get_instance()->format_date($dt, $format); | return rcmail::get_instance()->format_date($dt, $format); | ||||
}; | }; | ||||
if (is_array($rrule['EXDATE']) && !empty($rrule['EXDATE'])) { | if (!empty($rrule['EXDATE']) && is_array($rrule['EXDATE'])) { | ||||
$exdates = array_map($format_fn, $rrule['EXDATE']); | $exdates = array_map($format_fn, $rrule['EXDATE']); | ||||
} | } | ||||
if (empty($rrule['FREQ']) && !empty($rrule['RDATE'])) { | if (empty($rrule['FREQ']) && !empty($rrule['RDATE'])) { | ||||
$rdates = array_map($format_fn, $rrule['RDATE']); | $rdates = array_map($format_fn, $rrule['RDATE']); | ||||
$more = false; | |||||
if (!empty($exdates)) { | if (!empty($exdates)) { | ||||
$rdates = array_diff($rdates, $exdates); | $rdates = array_diff($rdates, $exdates); | ||||
} | } | ||||
if (count($rdates) > $limit) { | if (count($rdates) > $limit) { | ||||
$rdates = array_slice($rdates, 0, $limit); | $rdates = array_slice($rdates, 0, $limit); | ||||
$more = true; | $more = true; | ||||
} | } | ||||
return $this->gettext('ondate') . ' ' . join(', ', $rdates) | return $this->gettext('ondate') . ' ' . join(', ', $rdates) . ($more ? '...' : ''); | ||||
. ($more ? '...' : ''); | |||||
} | } | ||||
$output = sprintf('%s %d ', $this->gettext('every'), $rrule['INTERVAL'] ?: 1); | $output = sprintf('%s %d ', $this->gettext('every'), $rrule['INTERVAL'] ?: 1); | ||||
switch ($rrule['FREQ']) { | switch ($rrule['FREQ']) { | ||||
case 'DAILY': | case 'DAILY': | ||||
$output .= $this->gettext('days'); | $output .= $this->gettext('days'); | ||||
break; | break; | ||||
case 'WEEKLY': | case 'WEEKLY': | ||||
$output .= $this->gettext('weeks'); | $output .= $this->gettext('weeks'); | ||||
break; | break; | ||||
case 'MONTHLY': | case 'MONTHLY': | ||||
$output .= $this->gettext('months'); | $output .= $this->gettext('months'); | ||||
break; | break; | ||||
case 'YEARLY': | case 'YEARLY': | ||||
$output .= $this->gettext('years'); | $output .= $this->gettext('years'); | ||||
break; | break; | ||||
} | } | ||||
if ($rrule['COUNT']) { | if (!empty($rrule['COUNT'])) { | ||||
$until = $this->gettext(array('name' => 'forntimes', 'vars' => array('nr' => $rrule['COUNT']))); | $until = $this->gettext(array('name' => 'forntimes', 'vars' => array('nr' => $rrule['COUNT']))); | ||||
} | } | ||||
else if ($rrule['UNTIL']) { | else if (!empty($rrule['UNTIL'])) { | ||||
$until = $this->gettext('recurrencend') . ' ' . $this->rc->format_date($rrule['UNTIL'], $format); | $until = $this->gettext('recurrencend') . ' ' . $this->rc->format_date($rrule['UNTIL'], $format); | ||||
} | } | ||||
else { | else { | ||||
$until = $this->gettext('forever'); | $until = $this->gettext('forever'); | ||||
} | } | ||||
$output .= ', ' . $until; | $output .= ', ' . $until; | ||||
if (!empty($exdates)) { | if (!empty($exdates)) { | ||||
$more = false; | |||||
if (count($exdates) > $limit) { | if (count($exdates) > $limit) { | ||||
$exdates = array_slice($exdates, 0, $limit); | $exdates = array_slice($exdates, 0, $limit); | ||||
$more = true; | $more = true; | ||||
} | } | ||||
$output .= '; ' . $this->gettext('except') . ' ' . join(', ', $exdates) | $output .= '; ' . $this->gettext('except') . ' ' . join(', ', $exdates) . ($more ? '...' : ''); | ||||
. ($more ? '...' : ''); | |||||
} | } | ||||
return $output; | return $output; | ||||
} | } | ||||
/** | /** | ||||
* Generate the form for recurrence settings | * Generate the form for recurrence settings | ||||
*/ | */ | ||||
▲ Show 20 Lines • Show All 181 Lines • ▼ Show 20 Lines | private function rrule_selectors($part, $noselect = null) | ||||
return $select_prefix->show() . ' ' . $select_wday->show(); | return $select_prefix->show() . ' ' . $select_wday->show(); | ||||
} | } | ||||
/** | /** | ||||
* Convert the recurrence settings to be processed on the client | * Convert the recurrence settings to be processed on the client | ||||
*/ | */ | ||||
public function to_client_recurrence($recurrence, $allday = false) | public function to_client_recurrence($recurrence, $allday = false) | ||||
{ | { | ||||
if ($recurrence['UNTIL']) { | if (!empty($recurrence['UNTIL'])) { | ||||
$recurrence['UNTIL'] = $this->adjust_timezone($recurrence['UNTIL'], $allday)->format('c'); | $recurrence['UNTIL'] = $this->adjust_timezone($recurrence['UNTIL'], $allday)->format('c'); | ||||
} | } | ||||
// format RDATE values | // format RDATE values | ||||
if (is_array($recurrence['RDATE'])) { | if (!empty($recurrence['RDATE'])) { | ||||
$libcal = $this; | $libcal = $this; | ||||
$recurrence['RDATE'] = array_map(function($rdate) use ($libcal) { | $recurrence['RDATE'] = array_map(function($rdate) use ($libcal) { | ||||
return $libcal->adjust_timezone($rdate, true)->format('c'); | return $libcal->adjust_timezone($rdate, true)->format('c'); | ||||
}, $recurrence['RDATE']); | }, (array) $recurrence['RDATE']); | ||||
} | } | ||||
unset($recurrence['EXCEPTIONS']); | unset($recurrence['EXCEPTIONS']); | ||||
return $recurrence; | return $recurrence; | ||||
} | } | ||||
/** | /** | ||||
* Process the alarms values submitted by the client | * Process the alarms values submitted by the client | ||||
*/ | */ | ||||
public function from_client_recurrence($recurrence, $start = null) | public function from_client_recurrence($recurrence, $start = null) | ||||
{ | { | ||||
if (is_array($recurrence) && !empty($recurrence['UNTIL'])) { | if (is_array($recurrence) && !empty($recurrence['UNTIL'])) { | ||||
$recurrence['UNTIL'] = new DateTime($recurrence['UNTIL'], $this->timezone); | $recurrence['UNTIL'] = new DateTime($recurrence['UNTIL'], $this->timezone); | ||||
} | } | ||||
if (is_array($recurrence) && is_array($recurrence['RDATE'])) { | if (is_array($recurrence) && !empty($recurrence['RDATE'])) { | ||||
$tz = $this->timezone; | $tz = $this->timezone; | ||||
$recurrence['RDATE'] = array_map(function($rdate) use ($tz, $start) { | $recurrence['RDATE'] = array_map(function($rdate) use ($tz, $start) { | ||||
try { | try { | ||||
$dt = new DateTime($rdate, $tz); | $dt = new DateTime($rdate, $tz); | ||||
if (is_a($start, 'DateTime')) | if (is_a($start, 'DateTime')) | ||||
$dt->setTime($start->format('G'), $start->format('i')); | $dt->setTime($start->format('G'), $start->format('i')); | ||||
return $dt; | return $dt; | ||||
} | } | ||||
▲ Show 20 Lines • Show All 96 Lines • ▼ Show 20 Lines | public function mail_get_itip_object($mbox, $uid, $mime_id, $type = null) | ||||
if ($uid && $mime_id) { | if ($uid && $mime_id) { | ||||
list($mime_id, $index) = explode(':', $mime_id); | list($mime_id, $index) = explode(':', $mime_id); | ||||
$part = $imap->get_message_part($uid, $mime_id); | $part = $imap->get_message_part($uid, $mime_id); | ||||
$headers = $imap->get_message_headers($uid); | $headers = $imap->get_message_headers($uid); | ||||
$parser = $this->get_ical(); | $parser = $this->get_ical(); | ||||
if ($part->ctype_parameters['charset']) { | if (!empty($part->ctype_parameters['charset'])) { | ||||
$charset = $part->ctype_parameters['charset']; | $charset = $part->ctype_parameters['charset']; | ||||
} | } | ||||
if ($part) { | if ($part) { | ||||
$objects = $parser->import($part, $charset); | $objects = $parser->import($part, $charset); | ||||
} | } | ||||
} | } | ||||
Show All 26 Lines | // $action = rcube_utils::get_input_value('action', rcube_utils::INPUT_GPC); | ||||
*/ | */ | ||||
public static function part_is_vcalendar($part, $message = null) | public static function part_is_vcalendar($part, $message = null) | ||||
{ | { | ||||
// First check if the message is "valid" (i.e. not multipart/report) | // First check if the message is "valid" (i.e. not multipart/report) | ||||
if ($message) { | if ($message) { | ||||
$level = explode('.', $part->mime_id); | $level = explode('.', $part->mime_id); | ||||
while (array_pop($level) !== null) { | while (array_pop($level) !== null) { | ||||
$parent = $message->mime_parts[join('.', $level) ?: 0]; | $id = join('.', $level) ?: 0; | ||||
if ($parent->mimetype == 'multipart/report') { | $parent = !empty($message->mime_parts[$id]) ? $message->mime_parts[$id] : null; | ||||
if ($parent && $parent->mimetype == 'multipart/report') { | |||||
return false; | return false; | ||||
} | } | ||||
} | } | ||||
} | } | ||||
return ( | return ( | ||||
in_array($part->mimetype, array('text/calendar', 'text/x-vcalendar', 'application/ics')) || | in_array($part->mimetype, array('text/calendar', 'text/x-vcalendar', 'application/ics')) || | ||||
// Apple sends files as application/x-any (!?) | // Apple sends files as application/x-any (!?) | ||||
($part->mimetype == 'application/x-any' && $part->filename && preg_match('/\.ics$/i', $part->filename)) | ($part->mimetype == 'application/x-any' && !empty($part->filename) && preg_match('/\.ics$/i', $part->filename)) | ||||
); | ); | ||||
} | } | ||||
/** | /** | ||||
* Single occourrences of recurring events are identified by their RECURRENCE-ID property | * Single occourrences of recurring events are identified by their RECURRENCE-ID property | ||||
* in iCal which is represented as 'recurrence_date' in our internal data structure. | * in iCal which is represented as 'recurrence_date' in our internal data structure. | ||||
* | * | ||||
* Check if such a property exists and derive the '_instance' identifier and '_savemode' | * Check if such a property exists and derive the '_instance' identifier and '_savemode' | ||||
Show All 23 Lines | // $action = rcube_utils::get_input_value('action', rcube_utils::INPUT_GPC); | ||||
/** | /** | ||||
* Return a date() format string to render identifiers for recurrence instances | * Return a date() format string to render identifiers for recurrence instances | ||||
* | * | ||||
* @param array Hash array with event properties | * @param array Hash array with event properties | ||||
* @return string Format string | * @return string Format string | ||||
*/ | */ | ||||
public static function recurrence_id_format($event) | public static function recurrence_id_format($event) | ||||
{ | { | ||||
return $event['allday'] ? 'Ymd' : 'Ymd\THis'; | return !empty($event['allday']) ? 'Ymd' : 'Ymd\THis'; | ||||
} | } | ||||
/** | /** | ||||
* Return the identifer for the given instance of a recurring event | * Return the identifer for the given instance of a recurring event | ||||
* | * | ||||
* @param array Hash array with event properties | * @param array Hash array with event properties | ||||
* @param bool All-day flag from the main event | * @param bool All-day flag from the main event | ||||
* | * | ||||
* @return mixed Format string or null if identifier cannot be generated | * @return mixed Format string or null if identifier cannot be generated | ||||
*/ | */ | ||||
public static function recurrence_instance_identifier($event, $allday = null) | public static function recurrence_instance_identifier($event, $allday = null) | ||||
{ | { | ||||
$instance_date = $event['recurrence_date'] ?: $event['start']; | $instance_date = !empty($event['recurrence_date']) ? $event['recurrence_date'] : $event['start']; | ||||
if ($instance_date && is_a($instance_date, 'DateTime')) { | if ($instance_date instanceof DateTime) { | ||||
// According to RFC5545 (3.8.4.4) RECURRENCE-ID format should | // According to RFC5545 (3.8.4.4) RECURRENCE-ID format should | ||||
// be date/date-time depending on the main event type, not the exception | // be date/date-time depending on the main event type, not the exception | ||||
if ($allday === null) { | if ($allday === null) { | ||||
$allday = $event['allday']; | $allday = !empty($event['allday']); | ||||
} | } | ||||
return $instance_date->format($allday ? 'Ymd' : 'Ymd\THis'); | return $instance_date->format($allday ? 'Ymd' : 'Ymd\THis'); | ||||
} | } | ||||
} | } | ||||
/********* Attendee handling functions *********/ | /********* Attendee handling functions *********/ | ||||
▲ Show 20 Lines • Show All 223 Lines • ▼ Show 20 Lines | public static function from_php_date_format($from) | ||||
'h' => 'hh', | 'h' => 'hh', | ||||
'G' => 'H', | 'G' => 'H', | ||||
'g' => 'h', | 'g' => 'h', | ||||
'i' => 'mm', | 'i' => 'mm', | ||||
's' => 'ss', | 's' => 'ss', | ||||
'c' => '', | 'c' => '', | ||||
)); | )); | ||||
} | } | ||||
} | } |