Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117883149
libcalendaring.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
58 KB
Referenced Files
None
Subscribers
None
libcalendaring.php
View Options
<?php
/**
* Library providing common functions for calendaring plugins
*
* Provides utility functions for calendar-related modules such as
* - alarms display and dismissal
* - attachment handling
* - recurrence computation and UI elements
* - ical parsing and exporting
* - itip scheduling protocol
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2012-2014, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class
libcalendaring
extends
rcube_plugin
{
public
$rc
;
public
$timezone
;
public
$gmt_offset
;
public
$dst_active
;
public
$timezone_offset
;
public
$ical_parts
=
array
();
public
$ical_message
;
public
$defaults
=
array
(
'calendar_date_format'
=>
"yyyy-MM-dd"
,
'calendar_date_short'
=>
"M-d"
,
'calendar_date_long'
=>
"MMM d yyyy"
,
'calendar_date_agenda'
=>
"ddd MM-dd"
,
'calendar_time_format'
=>
"HH:mm"
,
'calendar_first_day'
=>
1
,
'calendar_first_hour'
=>
6
,
'calendar_date_format_sets'
=>
array
(
'yyyy-MM-dd'
=>
array
(
'MMM d yyyy'
,
'M-d'
,
'ddd MM-dd'
),
'dd-MM-yyyy'
=>
array
(
'd MMM yyyy'
,
'd-M'
,
'ddd dd-MM'
),
'yyyy/MM/dd'
=>
array
(
'MMM d yyyy'
,
'M/d'
,
'ddd MM/dd'
),
'MM/dd/yyyy'
=>
array
(
'MMM d yyyy'
,
'M/d'
,
'ddd MM/dd'
),
'dd/MM/yyyy'
=>
array
(
'd MMM yyyy'
,
'd/M'
,
'ddd dd/MM'
),
'dd.MM.yyyy'
=>
array
(
'dd. MMM yyyy'
,
'd.M'
,
'ddd dd.MM.'
),
'd.M.yyyy'
=>
array
(
'd. MMM yyyy'
,
'd.M'
,
'ddd d.MM.'
),
),
);
private
static
$instance
;
private
static
$email_regex
=
'/([a-z0-9][a-z0-9
\-\.\+\_
]*@[^&@"
\'
.][^@&"
\'
]*
\\
.([^
\\
x00-
\\
x40
\\
x5b-
\\
x60
\\
x7b-
\\
x7f]{2,}|xn--[a-z0-9]{2,}))/'
;
private
$mail_ical_parser
;
/**
* Singleton getter to allow direct access from other plugins
*/
public
static
function
get_instance
()
{
return
self
::
$instance
;
}
/**
* Required plugin startup method
*/
public
function
init
()
{
self
::
$instance
=
$this
;
$this
->
rc
=
rcube
::
get_instance
();
// set user's timezone
try
{
$this
->
timezone
=
new
DateTimeZone
(
$this
->
rc
->
config
->
get
(
'timezone'
,
'GMT'
));
}
catch
(
Exception
$e
)
{
$this
->
timezone
=
new
DateTimeZone
(
'GMT'
);
}
$now
=
new
DateTime
(
'now'
,
$this
->
timezone
);
$this
->
gmt_offset
=
$now
->
getOffset
();
$this
->
dst_active
=
$now
->
format
(
'I'
);
$this
->
timezone_offset
=
$this
->
gmt_offset
/
3600
-
$this
->
dst_active
;
$this
->
add_texts
(
'localization/'
,
false
);
// include client scripts and styles
if
(
$this
->
rc
->
output
)
{
if
(
$this
->
rc
->
output
->
type
==
'html'
)
{
$this
->
rc
->
output
->
set_env
(
'libcal_settings'
,
$this
->
load_settings
());
$this
->
include_script
(
'libcalendaring.js'
);
$this
->
include_stylesheet
(
$this
->
local_skin_path
()
.
'/libcal.css'
);
}
// add hook to display alarms
$this
->
add_hook
(
'refresh'
,
array
(
$this
,
'refresh'
));
$this
->
register_action
(
'plugin.alarms'
,
array
(
$this
,
'alarms_action'
));
$this
->
register_action
(
'plugin.expand_attendee_group'
,
array
(
$this
,
'expand_attendee_group'
));
}
// proceed initialization in startup hook
$this
->
add_hook
(
'startup'
,
array
(
$this
,
'startup'
));
}
/**
* Startup hook
*/
public
function
startup
(
$args
)
{
if
(
$args
[
'task'
]
==
'mail'
)
{
if
(
$args
[
'action'
]
==
'show'
||
$args
[
'action'
]
==
'preview'
)
{
$this
->
add_hook
(
'message_load'
,
array
(
$this
,
'mail_message_load'
));
}
}
}
/**
* Load iCalendar functions
*/
public
static
function
get_ical
()
{
$self
=
self
::
get_instance
();
require_once
(
$self
->
home
.
'/libvcalendar.php'
);
return
new
libvcalendar
();
}
/**
* Load iTip functions
*/
public
static
function
get_itip
(
$domain
=
'libcalendaring'
)
{
$self
=
self
::
get_instance
();
require_once
(
$self
->
home
.
'/lib/libcalendaring_itip.php'
);
return
new
libcalendaring_itip
(
$self
,
$domain
);
}
/**
* Load recurrence computation engine
*/
public
static
function
get_recurrence
()
{
$self
=
self
::
get_instance
();
require_once
(
$self
->
home
.
'/lib/libcalendaring_recurrence.php'
);
return
new
libcalendaring_recurrence
(
$self
);
}
/**
* Shift dates into user's current timezone
*
* @param mixed Any kind of a date representation (DateTime object, string or unix timestamp)
* @return object DateTime object in user's timezone
*/
public
function
adjust_timezone
(
$dt
,
$dateonly
=
false
)
{
if
(
is_numeric
(
$dt
))
$dt
=
new
DateTime
(
'@'
.
$dt
);
else
if
(
is_string
(
$dt
))
$dt
=
rcube_utils
::
anytodatetime
(
$dt
);
if
(
$dt
instanceof
DateTime
&&
!(
$dt
->
_dateonly
||
$dateonly
))
{
$dt
->
setTimezone
(
$this
->
timezone
);
}
return
$dt
;
}
/**
*
*/
public
function
load_settings
()
{
$this
->
date_format_defaults
();
$settings
=
array
();
// configuration
$settings
[
'date_format'
]
=
(
string
)
$this
->
rc
->
config
->
get
(
'calendar_date_format'
,
$this
->
defaults
[
'calendar_date_format'
]);
$settings
[
'time_format'
]
=
(
string
)
$this
->
rc
->
config
->
get
(
'calendar_time_format'
,
$this
->
defaults
[
'calendar_time_format'
]);
$settings
[
'date_short'
]
=
(
string
)
$this
->
rc
->
config
->
get
(
'calendar_date_short'
,
$this
->
defaults
[
'calendar_date_short'
]);
$settings
[
'date_long'
]
=
(
string
)
$this
->
rc
->
config
->
get
(
'calendar_date_long'
,
$this
->
defaults
[
'calendar_date_long'
]);
$settings
[
'dates_long'
]
=
str_replace
(
' yyyy'
,
'[ yyyy]'
,
$settings
[
'date_long'
])
.
"{ '—' "
.
$settings
[
'date_long'
]
.
'}'
;
$settings
[
'first_day'
]
=
(
int
)
$this
->
rc
->
config
->
get
(
'calendar_first_day'
,
$this
->
defaults
[
'calendar_first_day'
]);
$settings
[
'timezone'
]
=
$this
->
timezone_offset
;
$settings
[
'dst'
]
=
$this
->
dst_active
;
// localization
$settings
[
'days'
]
=
array
(
$this
->
rc
->
gettext
(
'sunday'
),
$this
->
rc
->
gettext
(
'monday'
),
$this
->
rc
->
gettext
(
'tuesday'
),
$this
->
rc
->
gettext
(
'wednesday'
),
$this
->
rc
->
gettext
(
'thursday'
),
$this
->
rc
->
gettext
(
'friday'
),
$this
->
rc
->
gettext
(
'saturday'
)
);
$settings
[
'days_short'
]
=
array
(
$this
->
rc
->
gettext
(
'sun'
),
$this
->
rc
->
gettext
(
'mon'
),
$this
->
rc
->
gettext
(
'tue'
),
$this
->
rc
->
gettext
(
'wed'
),
$this
->
rc
->
gettext
(
'thu'
),
$this
->
rc
->
gettext
(
'fri'
),
$this
->
rc
->
gettext
(
'sat'
)
);
$settings
[
'months'
]
=
array
(
$this
->
rc
->
gettext
(
'longjan'
),
$this
->
rc
->
gettext
(
'longfeb'
),
$this
->
rc
->
gettext
(
'longmar'
),
$this
->
rc
->
gettext
(
'longapr'
),
$this
->
rc
->
gettext
(
'longmay'
),
$this
->
rc
->
gettext
(
'longjun'
),
$this
->
rc
->
gettext
(
'longjul'
),
$this
->
rc
->
gettext
(
'longaug'
),
$this
->
rc
->
gettext
(
'longsep'
),
$this
->
rc
->
gettext
(
'longoct'
),
$this
->
rc
->
gettext
(
'longnov'
),
$this
->
rc
->
gettext
(
'longdec'
)
);
$settings
[
'months_short'
]
=
array
(
$this
->
rc
->
gettext
(
'jan'
),
$this
->
rc
->
gettext
(
'feb'
),
$this
->
rc
->
gettext
(
'mar'
),
$this
->
rc
->
gettext
(
'apr'
),
$this
->
rc
->
gettext
(
'may'
),
$this
->
rc
->
gettext
(
'jun'
),
$this
->
rc
->
gettext
(
'jul'
),
$this
->
rc
->
gettext
(
'aug'
),
$this
->
rc
->
gettext
(
'sep'
),
$this
->
rc
->
gettext
(
'oct'
),
$this
->
rc
->
gettext
(
'nov'
),
$this
->
rc
->
gettext
(
'dec'
)
);
$settings
[
'today'
]
=
$this
->
rc
->
gettext
(
'today'
);
// define list of file types which can be displayed inline
// same as in program/steps/mail/show.inc
$settings
[
'mimetypes'
]
=
(
array
)
$this
->
rc
->
config
->
get
(
'client_mimetypes'
);
return
$settings
;
}
/**
* Helper function to set date/time format according to config and user preferences
*/
private
function
date_format_defaults
()
{
static
$defaults
=
array
();
// nothing to be done
if
(
isset
(
$defaults
[
'date_format'
]))
return
;
$defaults
[
'date_format'
]
=
$this
->
rc
->
config
->
get
(
'calendar_date_format'
,
self
::
from_php_date_format
(
$this
->
rc
->
config
->
get
(
'date_format'
)));
$defaults
[
'time_format'
]
=
$this
->
rc
->
config
->
get
(
'calendar_time_format'
,
self
::
from_php_date_format
(
$this
->
rc
->
config
->
get
(
'time_format'
)));
// override defaults
if
(
$defaults
[
'date_format'
])
$this
->
defaults
[
'calendar_date_format'
]
=
$defaults
[
'date_format'
];
if
(
$defaults
[
'time_format'
])
$this
->
defaults
[
'calendar_time_format'
]
=
$defaults
[
'time_format'
];
// derive format variants from basic date format
$format_sets
=
$this
->
rc
->
config
->
get
(
'calendar_date_format_sets'
,
$this
->
defaults
[
'calendar_date_format_sets'
]);
if
(
$format_set
=
$format_sets
[
$this
->
defaults
[
'calendar_date_format'
]])
{
$this
->
defaults
[
'calendar_date_long'
]
=
$format_set
[
0
];
$this
->
defaults
[
'calendar_date_short'
]
=
$format_set
[
1
];
$this
->
defaults
[
'calendar_date_agenda'
]
=
$format_set
[
2
];
}
}
/**
* Compose a date string for the given event
*/
public
function
event_date_text
(
$event
,
$tzinfo
=
false
)
{
$fromto
=
'--'
;
// handle task objects
if
(
$event
[
'_type'
]
==
'task'
&&
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
;
$fromto
=
$this
->
rc
->
format_date
(
$event
[
'due'
],
$date_format
,
false
);
// add timezone information
if
(
$fromto
&&
$tzinfo
&&
(
$tzname
=
$this
->
timezone
->
getName
()))
{
$fromto
.=
' ('
.
strtr
(
$tzname
,
'_'
,
' '
)
.
')'
;
}
return
$fromto
;
}
// abort if no valid event dates are given
if
(!
is_object
(
$event
[
'start'
])
||
!
is_a
(
$event
[
'start'
],
'DateTime'
)
||
!
is_object
(
$event
[
'end'
])
||
!
is_a
(
$event
[
'end'
],
'DateTime'
))
{
return
$fromto
;
}
$duration
=
$event
[
'start'
]->
diff
(
$event
[
'end'
])->
format
(
's'
);
$this
->
date_format_defaults
();
$date_format
=
self
::
to_php_date_format
(
$this
->
rc
->
config
->
get
(
'calendar_date_format'
,
$this
->
defaults
[
'calendar_date_format'
]));
$time_format
=
self
::
to_php_date_format
(
$this
->
rc
->
config
->
get
(
'calendar_time_format'
,
$this
->
defaults
[
'calendar_time_format'
]));
if
(
$event
[
'allday'
])
{
$fromto
=
format_date
(
$event
[
'start'
],
$date_format
);
if
((
$todate
=
format_date
(
$event
[
'end'
],
$date_format
))
!=
$fromto
)
$fromto
.=
' - '
.
$todate
;
}
else
if
(
$duration
<
86400
&&
$event
[
'start'
]->
format
(
'd'
)
==
$event
[
'end'
]->
format
(
'd'
))
{
$fromto
=
format_date
(
$event
[
'start'
],
$date_format
)
.
' '
.
format_date
(
$event
[
'start'
],
$time_format
)
.
' - '
.
format_date
(
$event
[
'end'
],
$time_format
);
}
else
{
$fromto
=
format_date
(
$event
[
'start'
],
$date_format
)
.
' '
.
format_date
(
$event
[
'start'
],
$time_format
)
.
' - '
.
format_date
(
$event
[
'end'
],
$date_format
)
.
' '
.
format_date
(
$event
[
'end'
],
$time_format
);
}
// add timezone information
if
(
$tzinfo
&&
(
$tzname
=
$this
->
timezone
->
getName
()))
{
$fromto
.=
' ('
.
strtr
(
$tzname
,
'_'
,
' '
)
.
')'
;
}
return
$fromto
;
}
/**
* Render HTML form for alarm configuration
*/
public
function
alarm_select
(
$attrib
,
$alarm_types
,
$absolute_time
=
true
)
{
unset
(
$attrib
[
'name'
]);
$select_type
=
new
html_select
(
array
(
'name'
=>
'alarmtype[]'
,
'class'
=>
'edit-alarm-type'
,
'id'
=>
$attrib
[
'id'
]));
$select_type
->
add
(
$this
->
gettext
(
'none'
),
''
);
foreach
(
$alarm_types
as
$type
)
$select_type
->
add
(
$this
->
gettext
(
strtolower
(
"alarm{$type}option"
)),
$type
);
$input_value
=
new
html_inputfield
(
array
(
'name'
=>
'alarmvalue[]'
,
'class'
=>
'edit-alarm-value'
,
'size'
=>
3
));
$input_date
=
new
html_inputfield
(
array
(
'name'
=>
'alarmdate[]'
,
'class'
=>
'edit-alarm-date'
,
'size'
=>
10
));
$input_time
=
new
html_inputfield
(
array
(
'name'
=>
'alarmtime[]'
,
'class'
=>
'edit-alarm-time'
,
'size'
=>
6
));
$select_offset
=
new
html_select
(
array
(
'name'
=>
'alarmoffset[]'
,
'class'
=>
'edit-alarm-offset'
));
foreach
(
array
(
'-M'
,
'-H'
,
'-D'
,
'+M'
,
'+H'
,
'+D'
)
as
$trigger
)
$select_offset
->
add
(
$this
->
gettext
(
'trigger'
.
$trigger
),
$trigger
);
if
(
$absolute_time
)
$select_offset
->
add
(
$this
->
gettext
(
'trigger@'
),
'@'
);
// pre-set with default values from user settings
$preset
=
self
::
parse_alaram_value
(
$this
->
rc
->
config
->
get
(
'calendar_default_alarm_offset'
,
'-15M'
));
$hidden
=
array
(
'style'
=>
'display:none'
);
$html
=
html
::
span
(
'edit-alarm-set'
,
$select_type
->
show
(
$this
->
rc
->
config
->
get
(
'calendar_default_alarm_type'
,
''
))
.
' '
.
html
::
span
(
array
(
'class'
=>
'edit-alarm-values'
,
'style'
=>
'display:none'
),
$input_value
->
show
(
$preset
[
0
])
.
' '
.
$select_offset
->
show
(
$preset
[
1
])
.
' '
.
$input_date
->
show
(
''
,
$hidden
)
.
' '
.
$input_time
->
show
(
''
,
$hidden
)
)
);
// TODO: support adding more alarms
#$html .= html::a(array('href' => '#', 'id' => 'edit-alam-add', 'title' => $this->gettext('addalarm')),
# $attrib['addicon'] ? html::img(array('src' => $attrib['addicon'], 'alt' => 'add')) : '(+)');
return
$html
;
}
/**
* Get a list of email addresses of the current user (from login and identities)
*/
public
function
get_user_emails
()
{
static
$emails
;
// return cached result
if
(
is_array
(
$emails
))
{
return
$emails
;
}
$emails
=
array
();
$plugin
=
$this
->
rc
->
plugins
->
exec_hook
(
'calendar_user_emails'
,
array
(
'emails'
=>
$emails
));
$emails
=
array_map
(
'strtolower'
,
$plugin
[
'emails'
]);
if
(
$plugin
[
'abort'
])
{
return
$emails
;
}
$emails
[]
=
$this
->
rc
->
user
->
get_username
();
foreach
(
$this
->
rc
->
user
->
list_identities
()
as
$identity
)
{
$emails
[]
=
strtolower
(
$identity
[
'email'
]);
}
$emails
=
array_unique
(
$emails
);
return
$emails
;
}
/**
* Set the given participant status to the attendee matching the current user's identities
*
* @param array Hash array with event struct
* @param string The PARTSTAT value to set
* @return mixed Email address of the updated attendee or False if none matching found
*/
public
function
set_partstat
(&
$event
,
$status
)
{
$emails
=
$this
->
get_user_emails
();
foreach
((
array
)
$event
[
'attendees'
]
as
$i
=>
$attendee
)
{
if
(
$attendee
[
'email'
]
&&
in_array
(
strtolower
(
$attendee
[
'email'
]),
$emails
))
{
$event
[
'attendees'
][
$i
][
'status'
]
=
strtoupper
(
$status
);
return
$attendee
[
'email'
];
}
}
return
false
;
}
/********* Alarms handling *********/
/**
* Helper function to convert alarm trigger strings
* into two-field values (e.g. "-45M" => 45, "-M")
*/
public
static
function
parse_alaram_value
(
$val
)
{
if
(
$val
[
0
]
==
'@'
)
{
return
array
(
new
DateTime
(
$val
));
}
else
if
(
preg_match
(
'/([+-]?)P?(T?
\d
+[HMSDW])+/'
,
$val
,
$m
)
&&
preg_match_all
(
'/T?(
\d
+)([HMSDW])/'
,
$val
,
$m2
,
PREG_SET_ORDER
))
{
if
(
$m
[
1
]
==
''
)
$m
[
1
]
=
'+'
;
foreach
(
$m2
as
$seg
)
{
$prefix
=
$seg
[
2
]
==
'D'
||
$seg
[
2
]
==
'W'
?
'P'
:
'PT'
;
if
(
$seg
[
1
]
>
0
)
{
// ignore zero values
return
array
(
$seg
[
1
],
$m
[
1
].
$seg
[
2
],
$m
[
1
].
$seg
[
1
].
$seg
[
2
],
$m
[
1
].
$prefix
.
$seg
[
1
].
$seg
[
2
]);
}
}
// return zero value nevertheless
return
array
(
$seg
[
1
],
$m
[
1
].
$seg
[
2
],
$m
[
1
].
$seg
[
1
].
$seg
[
2
],
$m
[
1
].
$prefix
.
$seg
[
1
].
$seg
[
2
]);
}
return
false
;
}
/**
* Convert the alarms list items to be processed on the client
*/
public
static
function
to_client_alarms
(
$valarms
)
{
return
array_map
(
function
(
$alarm
){
if
(
$alarm
[
'trigger'
]
instanceof
DateTime
)
{
$alarm
[
'trigger'
]
=
'@'
.
$alarm
[
'trigger'
]->
format
(
'U'
);
}
else
if
(
$trigger
=
libcalendaring
::
parse_alaram_value
(
$alarm
[
'trigger'
]))
{
$alarm
[
'trigger'
]
=
$trigger
[
2
];
}
return
$alarm
;
},
(
array
)
$valarms
);
}
/**
* Process the alarms values submitted by the client
*/
public
static
function
from_client_alarms
(
$valarms
)
{
return
array_map
(
function
(
$alarm
){
if
(
$alarm
[
'trigger'
][
0
]
==
'@'
)
{
try
{
$alarm
[
'trigger'
]
=
new
DateTime
(
$alarm
[
'trigger'
]);
$alarm
[
'trigger'
]->
setTimezone
(
new
DateTimeZone
(
'UTC'
));
}
catch
(
Exception
$e
)
{
/* handle this ? */
}
}
else
if
(
$trigger
=
libcalendaring
::
parse_alaram_value
(
$alarm
[
'trigger'
]))
{
$alarm
[
'trigger'
]
=
$trigger
[
3
];
}
return
$alarm
;
},
(
array
)
$valarms
);
}
/**
* Render localized text for alarm settings
*/
public
static
function
alarms_text
(
$alarms
)
{
if
(
is_array
(
$alarms
)
&&
is_array
(
$alarms
[
0
]))
{
$texts
=
array
();
foreach
(
$alarms
as
$alarm
)
{
if
(
$text
=
self
::
alarm_text
(
$alarm
))
$texts
[]
=
$text
;
}
return
join
(
', '
,
$texts
);
}
else
{
return
self
::
alarm_text
(
$alarms
);
}
}
/**
* Render localized text for a single alarm property
*/
public
static
function
alarm_text
(
$alarm
)
{
if
(
is_string
(
$alarm
))
{
list
(
$trigger
,
$action
)
=
explode
(
':'
,
$alarm
);
}
else
{
$trigger
=
$alarm
[
'trigger'
];
$action
=
$alarm
[
'action'
];
}
$text
=
''
;
$rcube
=
rcube
::
get_instance
();
switch
(
$action
)
{
case
'EMAIL'
:
$text
=
$rcube
->
gettext
(
'libcalendaring.alarmemail'
);
break
;
case
'DISPLAY'
:
$text
=
$rcube
->
gettext
(
'libcalendaring.alarmdisplay'
);
break
;
case
'AUDIO'
:
$text
=
$rcube
->
gettext
(
'libcalendaring.alarmaudio'
);
break
;
}
if
(
$trigger
instanceof
DateTime
)
{
$text
.=
' '
.
$rcube
->
gettext
(
array
(
'name'
=>
'libcalendaring.alarmat'
,
'vars'
=>
array
(
'datetime'
=>
$rcube
->
format_date
(
$trigger
))
));
}
else
if
(
preg_match
(
'/@(
\d
+)/'
,
$trigger
,
$m
))
{
$text
.=
' '
.
$rcube
->
gettext
(
array
(
'name'
=>
'libcalendaring.alarmat'
,
'vars'
=>
array
(
'datetime'
=>
$rcube
->
format_date
(
$m
[
1
]))
));
}
else
if
(
$val
=
self
::
parse_alaram_value
(
$trigger
))
{
// TODO: for all-day events say 'on date of event at XX' ?
if
(
$val
[
0
]
==
0
)
$text
.=
' '
.
$rcube
->
gettext
(
'libcalendaring.triggerattime'
);
else
$text
.=
' '
.
intval
(
$val
[
0
])
.
' '
.
$rcube
->
gettext
(
'libcalendaring.trigger'
.
$val
[
1
]);
}
else
{
return
false
;
}
return
$text
;
}
/**
* Get the next alarm (time & action) for the given event
*
* @param array Record data
* @return array Hash array with alarm time/type or null if no alarms are configured
*/
public
static
function
get_next_alarm
(
$rec
,
$type
=
'event'
)
{
if
(!(
$rec
[
'valarms'
]
||
$rec
[
'alarms'
])
||
$rec
[
'cancelled'
]
||
$rec
[
'status'
]
==
'CANCELLED'
)
return
null
;
if
(
$type
==
'task'
)
{
$timezone
=
self
::
get_instance
()->
timezone
;
if
(
$rec
[
'startdate'
])
$rec
[
'start'
]
=
new
DateTime
(
$rec
[
'startdate'
]
.
' '
.
(
$rec
[
'starttime'
]
?:
'12:00'
),
$timezone
);
if
(
$rec
[
'date'
])
$rec
[(
$rec
[
'start'
]
?
'end'
:
'start'
)]
=
new
DateTime
(
$rec
[
'date'
]
.
' '
.
(
$rec
[
'time'
]
?:
'12:00'
),
$timezone
);
}
if
(!
$rec
[
'end'
])
$rec
[
'end'
]
=
$rec
[
'start'
];
// support legacy format
if
(!
$rec
[
'valarms'
])
{
list
(
$trigger
,
$action
)
=
explode
(
':'
,
$rec
[
'alarms'
],
2
);
if
(
$alarm
=
self
::
parse_alaram_value
(
$trigger
))
{
$rec
[
'valarms'
]
=
array
(
array
(
'action'
=>
$action
,
'trigger'
=>
$alarm
[
3
]
?:
$alarm
[
0
]));
}
}
$expires
=
new
DateTime
(
'now - 12 hours'
);
$alarm_id
=
$rec
[
'id'
];
// alarm ID eq. record ID by default to keep backwards compatibility
// handle multiple alarms
$notify_at
=
null
;
foreach
(
$rec
[
'valarms'
]
as
$alarm
)
{
$notify_time
=
null
;
if
(
$alarm
[
'trigger'
]
instanceof
DateTime
)
{
$notify_time
=
$alarm
[
'trigger'
];
}
else
if
(
is_string
(
$alarm
[
'trigger'
]))
{
$refdate
=
$alarm
[
'trigger'
][
0
]
==
'+'
?
$rec
[
'end'
]
:
$rec
[
'start'
];
// abort if no reference date is available to compute notification time
if
(!
is_a
(
$refdate
,
'DateTime'
))
continue
;
// TODO: for all-day events, take start @ 00:00 as reference date ?
try
{
$interval
=
new
DateInterval
(
trim
(
$alarm
[
'trigger'
],
'+-'
));
$interval
->
invert
=
$alarm
[
'trigger'
][
0
]
!=
'+'
;
$notify_time
=
clone
$refdate
;
$notify_time
->
add
(
$interval
);
}
catch
(
Exception
$e
)
{
rcube
::
raise_error
(
$e
,
true
);
continue
;
}
}
if
(
$notify_time
&&
(!
$notify_at
||
(
$notify_time
<
$notify_at
&&
$notify_time
>
$expires
)))
{
$notify_at
=
$notify_time
;
$action
=
$alarm
[
'action'
];
$alarm_prop
=
$alarm
;
// generate a unique alarm ID if multiple alarms are set
if
(
count
(
$rec
[
'valarms'
])
>
1
)
{
$alarm_id
=
substr
(
md5
(
$rec
[
'id'
]),
0
,
16
)
.
'-'
.
$notify_at
->
format
(
'Ymd
\T
His'
);
}
}
}
return
!
$notify_at
?
null
:
array
(
'time'
=>
$notify_at
->
format
(
'U'
),
'action'
=>
$action
?
strtoupper
(
$action
)
:
'DISPLAY'
,
'id'
=>
$alarm_id
,
'prop'
=>
$alarm_prop
,
);
}
/**
* Handler for keep-alive requests
* This will check for pending notifications and pass them to the client
*/
public
function
refresh
(
$attr
)
{
// collect pending alarms from all providers (e.g. calendar, tasks)
$plugin
=
$this
->
rc
->
plugins
->
exec_hook
(
'pending_alarms'
,
array
(
'time'
=>
time
(),
'alarms'
=>
array
(),
));
if
(!
$plugin
[
'abort'
]
&&
!
empty
(
$plugin
[
'alarms'
]))
{
// make sure texts and env vars are available on client
$this
->
add_texts
(
'localization/'
,
true
);
$this
->
rc
->
output
->
add_label
(
'close'
);
$this
->
rc
->
output
->
set_env
(
'snooze_select'
,
$this
->
snooze_select
());
$this
->
rc
->
output
->
command
(
'plugin.display_alarms'
,
$this
->
_alarms_output
(
$plugin
[
'alarms'
]));
}
}
/**
* Handler for alarm dismiss/snooze requests
*/
public
function
alarms_action
()
{
// $action = rcube_utils::get_input_value('action', rcube_utils::INPUT_GPC);
$data
=
rcube_utils
::
get_input_value
(
'data'
,
rcube_utils
::
INPUT_POST
,
true
);
$data
[
'ids'
]
=
explode
(
','
,
$data
[
'id'
]);
$plugin
=
$this
->
rc
->
plugins
->
exec_hook
(
'dismiss_alarms'
,
$data
);
if
(
$plugin
[
'success'
])
$this
->
rc
->
output
->
show_message
(
'successfullysaved'
,
'confirmation'
);
else
$this
->
rc
->
output
->
show_message
(
'calendar.errorsaving'
,
'error'
);
}
/**
* Generate reduced and streamlined output for pending alarms
*/
private
function
_alarms_output
(
$alarms
)
{
$out
=
array
();
foreach
(
$alarms
as
$alarm
)
{
$out
[]
=
array
(
'id'
=>
$alarm
[
'id'
],
'start'
=>
$alarm
[
'start'
]
?
$this
->
adjust_timezone
(
$alarm
[
'start'
])->
format
(
'c'
)
:
''
,
'end'
=>
$alarm
[
'end'
]
?
$this
->
adjust_timezone
(
$alarm
[
'end'
])->
format
(
'c'
)
:
''
,
'allDay'
=>
(
$alarm
[
'allday'
]
==
1
)?
true
:
false
,
'title'
=>
$alarm
[
'title'
],
'location'
=>
$alarm
[
'location'
],
'calendar'
=>
$alarm
[
'calendar'
],
);
}
return
$out
;
}
/**
* Render a dropdown menu to choose snooze time
*/
private
function
snooze_select
(
$attrib
=
array
())
{
$steps
=
array
(
5
=>
'repeatinmin'
,
10
=>
'repeatinmin'
,
15
=>
'repeatinmin'
,
20
=>
'repeatinmin'
,
30
=>
'repeatinmin'
,
60
=>
'repeatinhr'
,
120
=>
'repeatinhrs'
,
1440
=>
'repeattomorrow'
,
10080
=>
'repeatinweek'
,
);
$items
=
array
();
foreach
(
$steps
as
$n
=>
$label
)
{
$items
[]
=
html
::
tag
(
'li'
,
null
,
html
::
a
(
array
(
'href'
=>
"#"
.
(
$n
*
60
),
'class'
=>
'active'
),
$this
->
gettext
(
array
(
'name'
=>
$label
,
'vars'
=>
array
(
'min'
=>
$n
%
60
,
'hrs'
=>
intval
(
$n
/
60
))))));
}
return
html
::
tag
(
'ul'
,
$attrib
+
array
(
'class'
=>
'toolbarmenu'
),
join
(
"
\n
"
,
$items
),
html
::
$common_attrib
);
}
/********* Recurrence rules handling ********/
/**
* Render localized text describing the recurrence rule of an event
*/
public
function
recurrence_text
(
$rrule
)
{
// derive missing FREQ and INTERVAL from RDATE list
if
(
empty
(
$rrule
[
'FREQ'
])
&&
!
empty
(
$rrule
[
'RDATE'
]))
{
$first
=
$rrule
[
'RDATE'
][
0
];
$second
=
$rrule
[
'RDATE'
][
1
];
$third
=
$rrule
[
'RDATE'
][
2
];
if
(
is_a
(
$first
,
'DateTime'
)
&&
is_a
(
$second
,
'DateTime'
))
{
$diff
=
$first
->
diff
(
$second
);
foreach
(
array
(
'y'
=>
'YEARLY'
,
'm'
=>
'MONTHLY'
,
'd'
=>
'DAILY'
)
as
$k
=>
$freq
)
{
if
(
$diff
->
$k
!=
0
)
{
$rrule
[
'FREQ'
]
=
$freq
;
$rrule
[
'INTERVAL'
]
=
$diff
->
$k
;
// verify interval with next item
if
(
is_a
(
$third
,
'DateTime'
))
{
$diff2
=
$second
->
diff
(
$third
);
if
(
$diff2
->
$k
!=
$diff
->
$k
)
{
unset
(
$rrule
[
'INTERVAL'
]);
}
}
break
;
}
}
}
if
(!
$rrule
[
'INTERVAL'
])
{
$rrule
[
'FREQ'
]
=
'RDATE'
;
}
$rrule
[
'UNTIL'
]
=
end
(
$rrule
[
'RDATE'
]);
}
$freq
=
sprintf
(
'%s %d '
,
$this
->
gettext
(
'every'
),
$rrule
[
'INTERVAL'
]);
$details
=
''
;
switch
(
$rrule
[
'FREQ'
])
{
case
'DAILY'
:
$freq
.=
$this
->
gettext
(
'days'
);
break
;
case
'WEEKLY'
:
$freq
.=
$this
->
gettext
(
'weeks'
);
break
;
case
'MONTHLY'
:
$freq
.=
$this
->
gettext
(
'months'
);
break
;
case
'YEARLY'
:
$freq
.=
$this
->
gettext
(
'years'
);
break
;
}
if
(
$rrule
[
'INTERVAL'
]
<=
1
)
{
$freq
=
$this
->
gettext
(
strtolower
(
$rrule
[
'FREQ'
]));
}
if
(
$rrule
[
'COUNT'
])
{
$until
=
$this
->
gettext
(
array
(
'name'
=>
'forntimes'
,
'vars'
=>
array
(
'nr'
=>
$rrule
[
'COUNT'
])));
}
else
if
(
$rrule
[
'UNTIL'
])
{
$until
=
$this
->
gettext
(
'recurrencend'
)
.
' '
.
format_date
(
$rrule
[
'UNTIL'
],
self
::
to_php_date_format
(
$this
->
rc
->
config
->
get
(
'calendar_date_format'
,
$this
->
defaults
[
'calendar_date_format'
])));
}
else
{
$until
=
$this
->
gettext
(
'forever'
);
}
$except
=
''
;
if
(
is_array
(
$rrule
[
'EXDATE'
])
&&
!
empty
(
$rrule
[
'EXDATE'
]))
{
$format
=
self
::
to_php_date_format
(
$this
->
rc
->
config
->
get
(
'calendar_date_format'
,
$this
->
defaults
[
'calendar_date_format'
]));
$exdates
=
array_map
(
function
(
$dt
)
use
(
$format
)
{
return
format_date
(
$dt
,
$format
);
},
array_slice
(
$rrule
[
'EXDATE'
],
0
,
10
)
);
$except
=
'; '
.
$this
->
gettext
(
'except'
)
.
' '
.
join
(
', '
,
$exdates
);
}
return
rtrim
(
$freq
.
$details
.
', '
.
$until
.
$except
);
}
/**
* Generate the form for recurrence settings
*/
public
function
recurrence_form
(
$attrib
=
array
())
{
switch
(
$attrib
[
'part'
])
{
// frequency selector
case
'frequency'
:
$select
=
new
html_select
(
array
(
'name'
=>
'frequency'
,
'id'
=>
'edit-recurrence-frequency'
));
$select
->
add
(
$this
->
gettext
(
'never'
),
''
);
$select
->
add
(
$this
->
gettext
(
'daily'
),
'DAILY'
);
$select
->
add
(
$this
->
gettext
(
'weekly'
),
'WEEKLY'
);
$select
->
add
(
$this
->
gettext
(
'monthly'
),
'MONTHLY'
);
$select
->
add
(
$this
->
gettext
(
'yearly'
),
'YEARLY'
);
$select
->
add
(
$this
->
gettext
(
'rdate'
),
'RDATE'
);
$html
=
html
::
label
(
'edit-recurrence-frequency'
,
$this
->
gettext
(
'frequency'
))
.
$select
->
show
(
''
);
break
;
// daily recurrence
case
'daily'
:
$select
=
$this
->
interval_selector
(
array
(
'name'
=>
'interval'
,
'class'
=>
'edit-recurrence-interval'
,
'id'
=>
'edit-recurrence-interval-daily'
));
$html
=
html
::
div
(
$attrib
,
html
::
label
(
'edit-recurrence-interval-daily'
,
$this
->
gettext
(
'every'
))
.
$select
->
show
(
1
)
.
html
::
span
(
'label-after'
,
$this
->
gettext
(
'days'
)));
break
;
// weekly recurrence form
case
'weekly'
:
$select
=
$this
->
interval_selector
(
array
(
'name'
=>
'interval'
,
'class'
=>
'edit-recurrence-interval'
,
'id'
=>
'edit-recurrence-interval-weekly'
));
$html
=
html
::
div
(
$attrib
,
html
::
label
(
'edit-recurrence-interval-weekly'
,
$this
->
gettext
(
'every'
))
.
$select
->
show
(
1
)
.
html
::
span
(
'label-after'
,
$this
->
gettext
(
'weeks'
)));
// weekday selection
$daymap
=
array
(
'sun'
,
'mon'
,
'tue'
,
'wed'
,
'thu'
,
'fri'
,
'sat'
);
$checkbox
=
new
html_checkbox
(
array
(
'name'
=>
'byday'
,
'class'
=>
'edit-recurrence-weekly-byday'
));
$first
=
$this
->
rc
->
config
->
get
(
'calendar_first_day'
,
1
);
for
(
$weekdays
=
''
,
$j
=
$first
;
$j
<=
$first
+
6
;
$j
++)
{
$d
=
$j
%
7
;
$weekdays
.=
html
::
label
(
array
(
'class'
=>
'weekday'
),
$checkbox
->
show
(
''
,
array
(
'value'
=>
strtoupper
(
substr
(
$daymap
[
$d
],
0
,
2
))))
.
$this
->
gettext
(
$daymap
[
$d
])
)
.
' '
;
}
$html
.=
html
::
div
(
$attrib
,
html
::
label
(
null
,
$this
->
gettext
(
'bydays'
))
.
$weekdays
);
break
;
// monthly recurrence form
case
'monthly'
:
$select
=
$this
->
interval_selector
(
array
(
'name'
=>
'interval'
,
'class'
=>
'edit-recurrence-interval'
,
'id'
=>
'edit-recurrence-interval-monthly'
));
$html
=
html
::
div
(
$attrib
,
html
::
label
(
'edit-recurrence-interval-monthly'
,
$this
->
gettext
(
'every'
))
.
$select
->
show
(
1
)
.
html
::
span
(
'label-after'
,
$this
->
gettext
(
'months'
)));
$checkbox
=
new
html_checkbox
(
array
(
'name'
=>
'bymonthday'
,
'class'
=>
'edit-recurrence-monthly-bymonthday'
));
for
(
$monthdays
=
''
,
$d
=
1
;
$d
<=
31
;
$d
++)
{
$monthdays
.=
html
::
label
(
array
(
'class'
=>
'monthday'
),
$checkbox
->
show
(
''
,
array
(
'value'
=>
$d
))
.
$d
);
$monthdays
.=
$d
%
7
?
' '
:
html
::
br
();
}
// rule selectors
$radio
=
new
html_radiobutton
(
array
(
'name'
=>
'repeatmode'
,
'class'
=>
'edit-recurrence-monthly-mode'
));
$table
=
new
html_table
(
array
(
'cols'
=>
2
,
'border'
=>
0
,
'cellpadding'
=>
0
,
'class'
=>
'formtable'
));
$table
->
add
(
'label'
,
html
::
label
(
null
,
$radio
->
show
(
'BYMONTHDAY'
,
array
(
'value'
=>
'BYMONTHDAY'
))
.
' '
.
$this
->
gettext
(
'each'
)));
$table
->
add
(
null
,
$monthdays
);
$table
->
add
(
'label'
,
html
::
label
(
null
,
$radio
->
show
(
''
,
array
(
'value'
=>
'BYDAY'
))
.
' '
.
$this
->
gettext
(
'onevery'
)));
$table
->
add
(
null
,
$this
->
rrule_selectors
(
$attrib
[
'part'
]));
$html
.=
html
::
div
(
$attrib
,
$table
->
show
());
break
;
// annually recurrence form
case
'yearly'
:
$select
=
$this
->
interval_selector
(
array
(
'name'
=>
'interval'
,
'class'
=>
'edit-recurrence-interval'
,
'id'
=>
'edit-recurrence-interval-yearly'
));
$html
=
html
::
div
(
$attrib
,
html
::
label
(
'edit-recurrence-interval-yearly'
,
$this
->
gettext
(
'every'
))
.
$select
->
show
(
1
)
.
html
::
span
(
'label-after'
,
$this
->
gettext
(
'years'
)));
// month selector
$monthmap
=
array
(
''
,
'jan'
,
'feb'
,
'mar'
,
'apr'
,
'may'
,
'jun'
,
'jul'
,
'aug'
,
'sep'
,
'oct'
,
'nov'
,
'dec'
);
$checkbox
=
new
html_checkbox
(
array
(
'name'
=>
'bymonth'
,
'class'
=>
'edit-recurrence-yearly-bymonth'
));
for
(
$months
=
''
,
$m
=
1
;
$m
<=
12
;
$m
++)
{
$months
.=
html
::
label
(
array
(
'class'
=>
'month'
),
$checkbox
->
show
(
null
,
array
(
'value'
=>
$m
))
.
$this
->
gettext
(
$monthmap
[
$m
]));
$months
.=
$m
%
4
?
' '
:
html
::
br
();
}
$html
.=
html
::
div
(
$attrib
+
array
(
'id'
=>
'edit-recurrence-yearly-bymonthblock'
),
$months
);
// day rule selection
$html
.=
html
::
div
(
$attrib
,
html
::
label
(
null
,
$this
->
gettext
(
'onevery'
))
.
$this
->
rrule_selectors
(
$attrib
[
'part'
],
'---'
));
break
;
// end of recurrence form
case
'until'
:
$radio
=
new
html_radiobutton
(
array
(
'name'
=>
'repeat'
,
'class'
=>
'edit-recurrence-until'
));
$select
=
$this
->
interval_selector
(
array
(
'name'
=>
'times'
,
'id'
=>
'edit-recurrence-repeat-times'
));
$input
=
new
html_inputfield
(
array
(
'name'
=>
'untildate'
,
'id'
=>
'edit-recurrence-enddate'
,
'size'
=>
"10"
));
$html
=
html
::
div
(
'line first'
,
html
::
label
(
null
,
$radio
->
show
(
''
,
array
(
'value'
=>
''
,
'id'
=>
'edit-recurrence-repeat-forever'
))
.
' '
.
$this
->
gettext
(
'forever'
))
);
$forntimes
=
$this
->
gettext
(
array
(
'name'
=>
'forntimes'
,
'vars'
=>
array
(
'nr'
=>
'%s'
))
);
$html
.=
html
::
div
(
'line'
,
$radio
->
show
(
''
,
array
(
'value'
=>
'count'
,
'id'
=>
'edit-recurrence-repeat-count'
,
'aria-label'
=>
sprintf
(
$forntimes
,
'N'
)))
.
' '
.
sprintf
(
$forntimes
,
$select
->
show
(
1
))
);
$html
.=
html
::
div
(
'line'
,
$radio
->
show
(
''
,
array
(
'value'
=>
'until'
,
'id'
=>
'edit-recurrence-repeat-until'
,
'aria-label'
=>
$this
->
gettext
(
'untilenddate'
)))
.
' '
.
$this
->
gettext
(
'untildate'
)
.
' '
.
$input
->
show
(
''
,
array
(
'aria-label'
=>
$this
->
gettext
(
'untilenddate'
)))
);
$html
=
html
::
div
(
$attrib
,
html
::
label
(
null
,
ucfirst
(
$this
->
gettext
(
'recurrencend'
)))
.
$html
);
break
;
case
'rdate'
:
$ul
=
html
::
tag
(
'ul'
,
array
(
'id'
=>
'edit-recurrence-rdates'
),
''
);
$input
=
new
html_inputfield
(
array
(
'name'
=>
'rdate'
,
'id'
=>
'edit-recurrence-rdate-input'
,
'size'
=>
"10"
));
$button
=
new
html_inputfield
(
array
(
'type'
=>
'button'
,
'class'
=>
'button add'
,
'value'
=>
$this
->
gettext
(
'addrdate'
)));
$html
.=
html
::
div
(
$attrib
,
$ul
.
html
::
div
(
'inputform'
,
$input
->
show
()
.
$button
->
show
()));
break
;
}
return
$html
;
}
/**
* Input field for interval selection
*/
private
function
interval_selector
(
$attrib
)
{
$select
=
new
html_select
(
$attrib
);
$select
->
add
(
range
(
1
,
30
),
range
(
1
,
30
));
return
$select
;
}
/**
* Drop-down menus for recurrence rules like "each last sunday of"
*/
private
function
rrule_selectors
(
$part
,
$noselect
=
null
)
{
// rule selectors
$select_prefix
=
new
html_select
(
array
(
'name'
=>
'bydayprefix'
,
'id'
=>
"edit-recurrence-$part-prefix"
));
if
(
$noselect
)
$select_prefix
->
add
(
$noselect
,
''
);
$select_prefix
->
add
(
array
(
$this
->
gettext
(
'first'
),
$this
->
gettext
(
'second'
),
$this
->
gettext
(
'third'
),
$this
->
gettext
(
'fourth'
),
$this
->
gettext
(
'last'
)
),
array
(
1
,
2
,
3
,
4
,
-
1
));
$select_wday
=
new
html_select
(
array
(
'name'
=>
'byday'
,
'id'
=>
"edit-recurrence-$part-byday"
));
if
(
$noselect
)
$select_wday
->
add
(
$noselect
,
''
);
$daymap
=
array
(
'sunday'
,
'monday'
,
'tuesday'
,
'wednesday'
,
'thursday'
,
'friday'
,
'saturday'
);
$first
=
$this
->
rc
->
config
->
get
(
'calendar_first_day'
,
1
);
for
(
$j
=
$first
;
$j
<=
$first
+
6
;
$j
++)
{
$d
=
$j
%
7
;
$select_wday
->
add
(
$this
->
gettext
(
$daymap
[
$d
]),
strtoupper
(
substr
(
$daymap
[
$d
],
0
,
2
)));
}
return
$select_prefix
->
show
()
.
' '
.
$select_wday
->
show
();
}
/**
* Convert the recurrence settings to be processed on the client
*/
public
function
to_client_recurrence
(
$recurrence
,
$allday
=
false
)
{
if
(
$recurrence
[
'UNTIL'
])
$recurrence
[
'UNTIL'
]
=
$this
->
adjust_timezone
(
$recurrence
[
'UNTIL'
],
$allday
)->
format
(
'c'
);
// format RDATE values
if
(
is_array
(
$recurrence
[
'RDATE'
]))
{
$libcal
=
$this
;
$recurrence
[
'RDATE'
]
=
array_map
(
function
(
$rdate
)
use
(
$libcal
)
{
return
$libcal
->
adjust_timezone
(
$rdate
,
true
)->
format
(
'c'
);
},
$recurrence
[
'RDATE'
]);
}
unset
(
$recurrence
[
'EXCEPTIONS'
]);
return
$recurrence
;
}
/**
* Process the alarms values submitted by the client
*/
public
function
from_client_recurrence
(
$recurrence
,
$start
=
null
)
{
if
(
is_array
(
$recurrence
)
&&
!
empty
(
$recurrence
[
'UNTIL'
]))
{
$recurrence
[
'UNTIL'
]
=
new
DateTime
(
$recurrence
[
'UNTIL'
],
$this
->
timezone
);
}
if
(
is_array
(
$recurrence
)
&&
is_array
(
$recurrence
[
'RDATE'
]))
{
$tz
=
$this
->
timezone
;
$recurrence
[
'RDATE'
]
=
array_map
(
function
(
$rdate
)
use
(
$tz
,
$start
)
{
try
{
$dt
=
new
DateTime
(
$rdate
,
$tz
);
if
(
is_a
(
$start
,
'DateTime'
))
$dt
->
setTime
(
$start
->
format
(
'G'
),
$start
->
format
(
'i'
));
return
$dt
;
}
catch
(
Exception
$e
)
{
return
null
;
}
},
$recurrence
[
'RDATE'
]);
}
return
$recurrence
;
}
/********* Attachments handling *********/
/**
* Handler for attachment uploads
*/
public
function
attachment_upload
(
$session_key
,
$id_prefix
=
''
)
{
// Upload progress update
if
(!
empty
(
$_GET
[
'_progress'
]))
{
$this
->
rc
->
upload_progress
();
}
$recid
=
$id_prefix
.
rcube_utils
::
get_input_value
(
'_id'
,
rcube_utils
::
INPUT_GPC
);
$uploadid
=
rcube_utils
::
get_input_value
(
'_uploadid'
,
rcube_utils
::
INPUT_GPC
);
if
(!
is_array
(
$_SESSION
[
$session_key
])
||
$_SESSION
[
$session_key
][
'id'
]
!=
$recid
)
{
$_SESSION
[
$session_key
]
=
array
();
$_SESSION
[
$session_key
][
'id'
]
=
$recid
;
$_SESSION
[
$session_key
][
'attachments'
]
=
array
();
}
// clear all stored output properties (like scripts and env vars)
$this
->
rc
->
output
->
reset
();
if
(
is_array
(
$_FILES
[
'_attachments'
][
'tmp_name'
]))
{
foreach
(
$_FILES
[
'_attachments'
][
'tmp_name'
]
as
$i
=>
$filepath
)
{
// Process uploaded attachment if there is no error
$err
=
$_FILES
[
'_attachments'
][
'error'
][
$i
];
if
(!
$err
)
{
$attachment
=
array
(
'path'
=>
$filepath
,
'size'
=>
$_FILES
[
'_attachments'
][
'size'
][
$i
],
'name'
=>
$_FILES
[
'_attachments'
][
'name'
][
$i
],
'mimetype'
=>
rcube_mime
::
file_content_type
(
$filepath
,
$_FILES
[
'_attachments'
][
'name'
][
$i
],
$_FILES
[
'_attachments'
][
'type'
][
$i
]),
'group'
=>
$recid
,
);
$attachment
=
$this
->
rc
->
plugins
->
exec_hook
(
'attachment_upload'
,
$attachment
);
}
if
(!
$err
&&
$attachment
[
'status'
]
&&
!
$attachment
[
'abort'
])
{
$id
=
$attachment
[
'id'
];
// store new attachment in session
unset
(
$attachment
[
'status'
],
$attachment
[
'abort'
]);
$_SESSION
[
$session_key
][
'attachments'
][
$id
]
=
$attachment
;
if
((
$icon
=
$_SESSION
[
$session_key
.
'_deleteicon'
])
&&
is_file
(
$icon
))
{
$button
=
html
::
img
(
array
(
'src'
=>
$icon
,
'alt'
=>
$this
->
rc
->
gettext
(
'delete'
)
));
}
else
{
$button
=
Q
(
$this
->
rc
->
gettext
(
'delete'
));
}
$content
=
html
::
a
(
array
(
'href'
=>
"#delete"
,
'class'
=>
'delete'
,
'onclick'
=>
sprintf
(
"return %s.remove_from_attachment_list('rcmfile%s')"
,
JS_OBJECT_NAME
,
$id
),
'title'
=>
$this
->
rc
->
gettext
(
'delete'
),
'aria-label'
=>
$this
->
rc
->
gettext
(
'delete'
)
.
' '
.
$attachment
[
'name'
],
),
$button
);
$content
.=
Q
(
$attachment
[
'name'
]);
$this
->
rc
->
output
->
command
(
'add2attachment_list'
,
"rcmfile$id"
,
array
(
'html'
=>
$content
,
'name'
=>
$attachment
[
'name'
],
'mimetype'
=>
$attachment
[
'mimetype'
],
'classname'
=>
rcube_utils
::
file2class
(
$attachment
[
'mimetype'
],
$attachment
[
'name'
]),
'complete'
=>
true
),
$uploadid
);
}
else
{
// upload failed
if
(
$err
==
UPLOAD_ERR_INI_SIZE
||
$err
==
UPLOAD_ERR_FORM_SIZE
)
{
$msg
=
$this
->
rc
->
gettext
(
array
(
'name'
=>
'filesizeerror'
,
'vars'
=>
array
(
'size'
=>
show_bytes
(
parse_bytes
(
ini_get
(
'upload_max_filesize'
))))));
}
else
if
(
$attachment
[
'error'
])
{
$msg
=
$attachment
[
'error'
];
}
else
{
$msg
=
$this
->
rc
->
gettext
(
'fileuploaderror'
);
}
$this
->
rc
->
output
->
command
(
'display_message'
,
$msg
,
'error'
);
$this
->
rc
->
output
->
command
(
'remove_from_attachment_list'
,
$uploadid
);
}
}
}
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
=
$this
->
rc
->
gettext
(
array
(
'name'
=>
'filesizeerror'
,
'vars'
=>
array
(
'size'
=>
show_bytes
(
parse_bytes
(
$maxsize
)))));
else
$msg
=
$this
->
rc
->
gettext
(
'fileuploaderror'
);
$this
->
rc
->
output
->
command
(
'display_message'
,
$msg
,
'error'
);
$this
->
rc
->
output
->
command
(
'remove_from_attachment_list'
,
$uploadid
);
}
$this
->
rc
->
output
->
send
(
'iframe'
);
}
/**
* Deliver an event/task attachment to the client
* (similar as in Roundcube core program/steps/mail/get.inc)
*/
public
function
attachment_get
(
$attachment
)
{
ob_end_clean
();
if
(
$attachment
&&
$attachment
[
'body'
])
{
// allow post-processing of the attachment body
$part
=
new
rcube_message_part
;
$part
->
filename
=
$attachment
[
'name'
];
$part
->
size
=
$attachment
[
'size'
];
$part
->
mimetype
=
$attachment
[
'mimetype'
];
$plugin
=
$this
->
rc
->
plugins
->
exec_hook
(
'message_part_get'
,
array
(
'body'
=>
$attachment
[
'body'
],
'mimetype'
=>
strtolower
(
$attachment
[
'mimetype'
]),
'download'
=>
!
empty
(
$_GET
[
'_download'
]),
'part'
=>
$part
,
));
if
(
$plugin
[
'abort'
])
exit
;
$mimetype
=
$plugin
[
'mimetype'
];
list
(
$ctype_primary
,
$ctype_secondary
)
=
explode
(
'/'
,
$mimetype
);
$browser
=
$this
->
rc
->
output
->
browser
;
// send download headers
if
(
$plugin
[
'download'
])
{
header
(
"Content-Type: application/octet-stream"
);
if
(
$browser
->
ie
)
header
(
"Content-Type: application/force-download"
);
}
else
if
(
$ctype_primary
==
'text'
)
{
header
(
"Content-Type: text/$ctype_secondary"
);
}
else
{
header
(
"Content-Type: $mimetype"
);
header
(
"Content-Transfer-Encoding: binary"
);
}
// display page, @TODO: support text/plain (and maybe some other text formats)
if
(
$mimetype
==
'text/html'
&&
empty
(
$_GET
[
'_download'
]))
{
$OUTPUT
=
new
rcube_html_page
();
// @TODO: use washtml on $body
$OUTPUT
->
write
(
$plugin
[
'body'
]);
}
else
{
// don't kill the connection if download takes more than 30 sec.
@
set_time_limit
(
0
);
$filename
=
$attachment
[
'name'
];
$filename
=
preg_replace
(
'[
\r\n
]'
,
''
,
$filename
);
if
(
$browser
->
ie
&&
$browser
->
ver
<
7
)
$filename
=
rawurlencode
(
abbreviate_string
(
$filename
,
55
));
else
if
(
$browser
->
ie
)
$filename
=
rawurlencode
(
$filename
);
else
$filename
=
addcslashes
(
$filename
,
'"'
);
$disposition
=
!
empty
(
$_GET
[
'_download'
])
?
'attachment'
:
'inline'
;
header
(
"Content-Disposition: $disposition; filename=
\"
$filename
\"
"
);
echo
$plugin
[
'body'
];
}
exit
;
}
// if we arrive here, the requested part was not found
header
(
'HTTP/1.1 404 Not Found'
);
exit
;
}
/**
* Show "loading..." page in attachment iframe
*/
public
function
attachment_loading_page
()
{
$url
=
str_replace
(
'&_preload=1'
,
''
,
$_SERVER
[
'REQUEST_URI'
]);
$message
=
$this
->
rc
->
gettext
(
'loadingdata'
);
header
(
'Content-Type: text/html; charset='
.
RCUBE_CHARSET
);
print
"<html>
\n
<head>
\n
"
.
'<meta http-equiv="refresh" content="0; url='
.
Q
(
$url
).
'">'
.
"
\n
"
.
'<meta http-equiv="content-type" content="text/html; charset='
.
RCUBE_CHARSET
.
'">'
.
"
\n
"
.
"</head>
\n
<body>
\n
$message
\n
</body>
\n
</html>"
;
exit
;
}
/**
* Template object for attachment display frame
*/
public
function
attachment_frame
(
$attrib
=
array
())
{
$mimetype
=
strtolower
(
$this
->
attachment
[
'mimetype'
]);
list
(
$ctype_primary
,
$ctype_secondary
)
=
explode
(
'/'
,
$mimetype
);
$attrib
[
'src'
]
=
'./?'
.
str_replace
(
'_frame='
,
(
$ctype_primary
==
'text'
?
'_show='
:
'_preload='
),
$_SERVER
[
'QUERY_STRING'
]);
return
html
::
iframe
(
$attrib
);
}
/**
*
*/
public
function
attachment_header
(
$attrib
=
array
())
{
$table
=
new
html_table
(
array
(
'cols'
=>
3
));
if
(!
empty
(
$this
->
attachment
[
'name'
]))
{
$table
->
add
(
'title'
,
Q
(
$this
->
rc
->
gettext
(
'filename'
)));
$table
->
add
(
'header'
,
Q
(
$this
->
attachment
[
'name'
]));
$table
->
add
(
'download-link'
,
html
::
a
(
'?'
.
str_replace
(
'_frame='
,
'_download='
,
$_SERVER
[
'QUERY_STRING'
]),
Q
(
$this
->
rc
->
gettext
(
'download'
))));
}
if
(!
empty
(
$this
->
attachment
[
'size'
]))
{
$table
->
add
(
'title'
,
Q
(
$this
->
rc
->
gettext
(
'filesize'
)));
$table
->
add
(
'header'
,
Q
(
show_bytes
(
$this
->
attachment
[
'size'
])));
}
return
$table
->
show
(
$attrib
);
}
/********* iTip message detection *********/
/**
* Check mail message structure of there are .ics files attached
*/
public
function
mail_message_load
(
$p
)
{
$this
->
ical_message
=
$p
[
'object'
];
$itip_part
=
null
;
// check all message parts for .ics files
foreach
((
array
)
$this
->
ical_message
->
mime_parts
as
$part
)
{
if
(
self
::
part_is_vcalendar
(
$part
))
{
if
(
$part
->
ctype_parameters
[
'method'
])
$itip_part
=
$part
->
mime_id
;
else
$this
->
ical_parts
[]
=
$part
->
mime_id
;
}
}
// priorize part with method parameter
if
(
$itip_part
)
{
$this
->
ical_parts
=
array
(
$itip_part
);
}
}
/**
* Getter for the parsed iCal objects attached to the current email message
*
* @return object libvcalendar parser instance with the parsed objects
*/
public
function
get_mail_ical_objects
()
{
// create parser and load ical objects
if
(!
$this
->
mail_ical_parser
)
{
$this
->
mail_ical_parser
=
$this
->
get_ical
();
foreach
(
$this
->
ical_parts
as
$mime_id
)
{
$part
=
$this
->
ical_message
->
mime_parts
[
$mime_id
];
$charset
=
$part
->
ctype_parameters
[
'charset'
]
?:
RCMAIL_CHARSET
;
$this
->
mail_ical_parser
->
import
(
$this
->
ical_message
->
get_part_body
(
$mime_id
,
true
),
$charset
);
// stop on the part that has an iTip method specified
if
(
count
(
$this
->
mail_ical_parser
->
objects
)
&&
$this
->
mail_ical_parser
->
method
)
{
$this
->
mail_ical_parser
->
message_date
=
$this
->
ical_message
->
headers
->
date
;
$this
->
mail_ical_parser
->
mime_id
=
$mime_id
;
// store the message's sender address for comparisons
$this
->
mail_ical_parser
->
sender
=
preg_match
(
self
::
$email_regex
,
$this
->
ical_message
->
headers
->
from
,
$m
)
?
$m
[
1
]
:
''
;
if
(!
empty
(
$this
->
mail_ical_parser
->
sender
))
{
foreach
(
$this
->
mail_ical_parser
->
objects
as
$i
=>
$object
)
{
$this
->
mail_ical_parser
->
objects
[
$i
][
'_sender'
]
=
$this
->
mail_ical_parser
->
sender
;
$this
->
mail_ical_parser
->
objects
[
$i
][
'_sender_utf'
]
=
rcube_utils
::
idn_to_utf8
(
$this
->
mail_ical_parser
->
sender
);
}
}
break
;
}
}
}
return
$this
->
mail_ical_parser
;
}
/**
* Read the given mime message from IMAP and parse ical data
*
* @param string Mailbox name
* @param string Message UID
* @param string Message part ID and object index (e.g. '1.2:0')
* @param string Object type filter (optional)
*
* @return array Hash array with the parsed iCal
*/
public
function
mail_get_itip_object
(
$mbox
,
$uid
,
$mime_id
,
$type
=
null
)
{
$charset
=
RCMAIL_CHARSET
;
// establish imap connection
$imap
=
$this
->
rc
->
get_storage
();
$imap
->
set_mailbox
(
$mbox
);
if
(
$uid
&&
$mime_id
)
{
list
(
$mime_id
,
$index
)
=
explode
(
':'
,
$mime_id
);
$part
=
$imap
->
get_message_part
(
$uid
,
$mime_id
);
$headers
=
$imap
->
get_message_headers
(
$uid
);
$parser
=
$this
->
get_ical
();
if
(
$part
->
ctype_parameters
[
'charset'
])
{
$charset
=
$part
->
ctype_parameters
[
'charset'
];
}
if
(
$part
)
{
$objects
=
$parser
->
import
(
$part
,
$charset
);
}
}
// successfully parsed events/tasks?
if
(!
empty
(
$objects
)
&&
(
$object
=
$objects
[
$index
])
&&
(!
$type
||
$object
[
'_type'
]
==
$type
))
{
if
(
$parser
->
method
)
$object
[
'_method'
]
=
$parser
->
method
;
// store the message's sender address for comparisons
$object
[
'_sender'
]
=
preg_match
(
self
::
$email_regex
,
$headers
->
from
,
$m
)
?
$m
[
1
]
:
''
;
$object
[
'_sender_utf'
]
=
rcube_utils
::
idn_to_utf8
(
$object
[
'_sender'
]);
return
$object
;
}
return
null
;
}
/**
* Checks if specified message part is a vcalendar data
*
* @param rcube_message_part Part object
* @return boolean True if part is of type vcard
*/
public
static
function
part_is_vcalendar
(
$part
)
{
return
(
in_array
(
$part
->
mimetype
,
array
(
'text/calendar'
,
'text/x-vcalendar'
,
'application/ics'
))
||
// Apple sends files as application/x-any (!?)
(
$part
->
mimetype
==
'application/x-any'
&&
$part
->
filename
&&
preg_match
(
'/
\.
ics$/i'
,
$part
->
filename
))
);
}
/********* Attendee handling functions *********/
/**
* Handler for attendee group expansion requests
*/
public
function
expand_attendee_group
()
{
$id
=
rcube_utils
::
get_input_value
(
'id'
,
rcube_utils
::
INPUT_POST
);
$data
=
rcube_utils
::
get_input_value
(
'data'
,
rcube_utils
::
INPUT_POST
,
true
);
$result
=
array
(
'id'
=>
$id
,
'members'
=>
array
());
$maxnum
=
500
;
// iterate over all autocomplete address books (we don't know the source of the group)
foreach
((
array
)
$this
->
rc
->
config
->
get
(
'autocomplete_addressbooks'
,
'sql'
)
as
$abook_id
)
{
if
((
$abook
=
$this
->
rc
->
get_address_book
(
$abook_id
))
&&
$abook
->
groups
)
{
foreach
(
$abook
->
list_groups
(
$data
[
'name'
],
1
)
as
$group
)
{
// this is the matching group to expand
if
(
in_array
(
$data
[
'email'
],
(
array
)
$group
[
'email'
]))
{
$abook
->
set_pagesize
(
$maxnum
);
$abook
->
set_group
(
$group
[
'ID'
]);
// get all members
$res
=
$abook
->
list_records
(
$this
->
rc
->
config
->
get
(
'contactlist_fields'
));
// handle errors (e.g. sizelimit, timelimit)
if
(
$abook
->
get_error
())
{
$result
[
'error'
]
=
$this
->
rc
->
gettext
(
'expandattendeegrouperror'
,
'libcalendaring'
);
$res
=
false
;
}
// check for maximum number of members (we don't wanna bloat the UI too much)
else
if
(
$res
->
count
>
$maxnum
)
{
$result
[
'error'
]
=
$this
->
rc
->
gettext
(
'expandattendeegroupsizelimit'
,
'libcalendaring'
);
$res
=
false
;
}
while
(
$res
&&
(
$member
=
$res
->
iterate
()))
{
$emails
=
(
array
)
$abook
->
get_col_values
(
'email'
,
$member
,
true
);
if
(!
empty
(
$emails
)
&&
(
$email
=
array_shift
(
$emails
)))
{
$result
[
'members'
][]
=
array
(
'email'
=>
$email
,
'name'
=>
rcube_addressbook
::
compose_list_name
(
$member
),
);
}
}
break
2
;
}
}
}
}
$this
->
rc
->
output
->
command
(
'plugin.expand_attendee_callback'
,
$result
);
}
/********* Static utility functions *********/
/**
* Convert the internal structured data into a vcalendar rrule 2.0 string
*/
public
static
function
to_rrule
(
$recurrence
)
{
if
(
is_string
(
$recurrence
))
return
$recurrence
;
$rrule
=
''
;
foreach
((
array
)
$recurrence
as
$k
=>
$val
)
{
$k
=
strtoupper
(
$k
);
switch
(
$k
)
{
case
'UNTIL'
:
$val
=
$val
->
format
(
'Ymd
\T
His'
);
break
;
case
'RDATE'
:
case
'EXDATE'
:
foreach
((
array
)
$val
as
$i
=>
$ex
)
$val
[
$i
]
=
$ex
->
format
(
'Ymd
\T
His'
);
$val
=
join
(
','
,
(
array
)
$val
);
break
;
case
'EXCEPTIONS'
:
continue
2
;
}
$rrule
.=
$k
.
'='
.
$val
.
';'
;
}
return
rtrim
(
$rrule
,
';'
);
}
/**
* Convert from fullcalendar date format to PHP date() format string
*/
public
static
function
to_php_date_format
(
$from
)
{
// "dd.MM.yyyy HH:mm:ss" => "d.m.Y H:i:s"
return
strtr
(
strtr
(
$from
,
array
(
'yyyy'
=>
'Y'
,
'yy'
=>
'y'
,
'MMMM'
=>
'F'
,
'MMM'
=>
'M'
,
'MM'
=>
'm'
,
'M'
=>
'n'
,
'dddd'
=>
'l'
,
'ddd'
=>
'D'
,
'dd'
=>
'd'
,
'd'
=>
'j'
,
'HH'
=>
'**'
,
'hh'
=>
'%%'
,
'H'
=>
'G'
,
'h'
=>
'g'
,
'mm'
=>
'i'
,
'ss'
=>
's'
,
'TT'
=>
'A'
,
'tt'
=>
'a'
,
'T'
=>
'A'
,
't'
=>
'a'
,
'u'
=>
'c'
,
)),
array
(
'**'
=>
'H'
,
'%%'
=>
'h'
,
));
}
/**
* Convert from PHP date() format to fullcalendar format string
*/
public
static
function
from_php_date_format
(
$from
)
{
// "d.m.Y H:i:s" => "dd.MM.yyyy HH:mm:ss"
return
strtr
(
$from
,
array
(
'y'
=>
'yy'
,
'Y'
=>
'yyyy'
,
'M'
=>
'MMM'
,
'F'
=>
'MMMM'
,
'm'
=>
'MM'
,
'n'
=>
'M'
,
'j'
=>
'd'
,
'd'
=>
'dd'
,
'D'
=>
'ddd'
,
'l'
=>
'dddd'
,
'H'
=>
'HH'
,
'h'
=>
'hh'
,
'G'
=>
'H'
,
'g'
=>
'h'
,
'i'
=>
'mm'
,
's'
=>
'ss'
,
'A'
=>
'TT'
,
'a'
=>
'tt'
,
'c'
=>
'u'
,
));
}
}
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Mon, Apr 6, 12:56 AM (5 d, 2 h ago)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
80/9c/1ec1f1dce80b2f8a7fe397951974
Default Alt Text
libcalendaring.php (58 KB)
Attached To
Mode
rRPK roundcubemail-plugins-kolab
Attached
Detach File
Event Timeline