CalDAV driver for Tasks
includes cache syncronization fixes and PHP8 fixes.
This commit is contained in:
parent
ca07e581dd
commit
a3ef1eedf1
19 changed files with 2007 additions and 128 deletions
|
@ -687,6 +687,6 @@ class caldav_driver extends kolab_driver
|
|||
],
|
||||
];
|
||||
|
||||
return kolab_utils::folder_form($form, $folder, 'calendar', [], true);
|
||||
return kolab_utils::folder_form($form, '', 'calendar', [], true);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,6 +30,9 @@ class calendar_ui
|
|||
private $ready = false;
|
||||
|
||||
public $screen;
|
||||
public $action;
|
||||
public $calendar;
|
||||
|
||||
|
||||
function __construct($cal)
|
||||
{
|
||||
|
|
|
@ -33,8 +33,8 @@ use \Sabre\VObject\DateTimeParser;
|
|||
class libcalendaring_vcalendar implements Iterator
|
||||
{
|
||||
private $timezone;
|
||||
private $attach_uri = null;
|
||||
private $prodid = '-//Roundcube libcalendaring//Sabre//Sabre VObject//EN';
|
||||
private $attach_uri;
|
||||
private $prodid;
|
||||
private $type_component_map = array('event' => 'VEVENT', 'task' => 'VTODO');
|
||||
private $attendee_keymap = array(
|
||||
'name' => 'CN',
|
||||
|
@ -73,7 +73,7 @@ class libcalendaring_vcalendar implements Iterator
|
|||
function __construct($tz = null)
|
||||
{
|
||||
$this->timezone = $tz;
|
||||
$this->prodid = '-//Roundcube libcalendaring ' . RCUBE_VERSION . '//Sabre//Sabre VObject ' . VObject\Version::VERSION . '//EN';
|
||||
$this->prodid = '-//Roundcube ' . RCUBE_VERSION . '//Sabre VObject ' . VObject\Version::VERSION . '//EN';
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -210,6 +210,24 @@ CREATE TABLE `kolab_cache_dav_event` (
|
|||
PRIMARY KEY(`folder_id`,`uid`)
|
||||
) ROW_FORMAT=DYNAMIC ENGINE=INNODB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
DROP TABLE IF EXISTS `kolab_cache_dav_task`;
|
||||
|
||||
CREATE TABLE `kolab_cache_dav_task` (
|
||||
`folder_id` BIGINT UNSIGNED NOT NULL,
|
||||
`uid` VARCHAR(512) NOT NULL,
|
||||
`etag` VARCHAR(128) DEFAULT NULL,
|
||||
`created` DATETIME DEFAULT NULL,
|
||||
`changed` DATETIME DEFAULT NULL,
|
||||
`data` LONGTEXT NOT NULL,
|
||||
`tags` TEXT NOT NULL,
|
||||
`words` TEXT NOT NULL,
|
||||
`dtstart` DATETIME,
|
||||
`dtend` DATETIME,
|
||||
CONSTRAINT `fk_kolab_cache_dav_task_folder` FOREIGN KEY (`folder_id`)
|
||||
REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
PRIMARY KEY(`folder_id`,`uid`)
|
||||
) ROW_FORMAT=DYNAMIC ENGINE=INNODB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
SET FOREIGN_KEY_CHECKS=1;
|
||||
|
||||
REPLACE INTO `system` (`name`, `value`) VALUES ('libkolab-version', '2022100500');
|
||||
REPLACE INTO `system` (`name`, `value`) VALUES ('libkolab-version', '2022122800');
|
||||
|
|
17
plugins/libkolab/SQL/mysql/2022122800.sql
Normal file
17
plugins/libkolab/SQL/mysql/2022122800.sql
Normal file
|
@ -0,0 +1,17 @@
|
|||
DROP TABLE IF EXISTS `kolab_cache_dav_task`;
|
||||
|
||||
CREATE TABLE `kolab_cache_dav_task` (
|
||||
`folder_id` BIGINT UNSIGNED NOT NULL,
|
||||
`uid` VARCHAR(512) NOT NULL,
|
||||
`etag` VARCHAR(128) DEFAULT NULL,
|
||||
`created` DATETIME DEFAULT NULL,
|
||||
`changed` DATETIME DEFAULT NULL,
|
||||
`data` LONGTEXT NOT NULL,
|
||||
`tags` TEXT NOT NULL,
|
||||
`words` TEXT NOT NULL,
|
||||
`dtstart` DATETIME,
|
||||
`dtend` DATETIME,
|
||||
CONSTRAINT `fk_kolab_cache_dav_task_folder` FOREIGN KEY (`folder_id`)
|
||||
REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
PRIMARY KEY(`folder_id`,`uid`)
|
||||
) ROW_FORMAT=DYNAMIC ENGINE=INNODB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
|
@ -264,9 +264,9 @@ class kolab_dav_client
|
|||
foreach ($response->getElementsByTagName('response') as $element) {
|
||||
$folder = $this->getFolderPropertiesFromResponse($element);
|
||||
|
||||
// Note: Addressbooks don't have 'type' specified
|
||||
// Note: Addressbooks don't have 'types' specified
|
||||
if (($component == 'VCARD' && in_array('addressbook', $folder['resource_type']))
|
||||
|| $folder['type'] === $component
|
||||
|| in_array($component, (array) $folder['types'])
|
||||
) {
|
||||
$folders[] = $folder;
|
||||
}
|
||||
|
@ -296,18 +296,7 @@ class kolab_dav_client
|
|||
|
||||
$response = $this->request($location, 'PUT', $content, $headers);
|
||||
|
||||
if ($response !== false) {
|
||||
// Note: ETag is not always returned, e.g. https://github.com/cyrusimap/cyrus-imapd/issues/2456
|
||||
$etag = isset($this->responseHeaders['etag']) ? $this->responseHeaders['etag'] : null;
|
||||
|
||||
if (is_string($etag) && preg_match('|^".*"$|', $etag)) {
|
||||
$etag = substr($etag, 1, -1);
|
||||
}
|
||||
|
||||
return $etag;
|
||||
}
|
||||
|
||||
return false;
|
||||
return $this->getETagFromResponse($response);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -338,6 +327,23 @@ class kolab_dav_client
|
|||
return $response !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move a DAV object
|
||||
*
|
||||
* @param string $source Source object location
|
||||
* @param string $target Target object content
|
||||
*
|
||||
* @return false|string|null ETag string (or NULL) on success, False on error
|
||||
*/
|
||||
public function move($source, $target)
|
||||
{
|
||||
$headers = ['Destination' => $target];
|
||||
|
||||
$response = $this->request($source, 'MOVE', '', $headers);
|
||||
|
||||
return $this->getETagFromResponse($response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get folder properties.
|
||||
*
|
||||
|
@ -665,10 +671,10 @@ class kolab_dav_client
|
|||
$ctag = $ctag->nodeValue;
|
||||
}
|
||||
|
||||
$component = null;
|
||||
$components = [];
|
||||
if ($set_element = $element->getElementsByTagName('supported-calendar-component-set')->item(0)) {
|
||||
if ($comp_element = $set_element->getElementsByTagName('comp')->item(0)) {
|
||||
$component = $comp_element->attributes->getNamedItem('name')->nodeValue;
|
||||
foreach ($set_element->getElementsByTagName('comp') as $comp_element) {
|
||||
$components[] = $comp_element->attributes->getNamedItem('name')->nodeValue;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -685,7 +691,7 @@ class kolab_dav_client
|
|||
'name' => $name,
|
||||
'ctag' => $ctag,
|
||||
'color' => $color,
|
||||
'type' => $component,
|
||||
'types' => $components,
|
||||
'resource_type' => $types,
|
||||
];
|
||||
|
||||
|
@ -741,6 +747,25 @@ class kolab_dav_client
|
|||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ETag from a response
|
||||
*/
|
||||
protected function getETagFromResponse($response)
|
||||
{
|
||||
if ($response !== false) {
|
||||
// Note: ETag is not always returned, e.g. https://github.com/cyrusimap/cyrus-imapd/issues/2456
|
||||
$etag = isset($this->responseHeaders['etag']) ? $this->responseHeaders['etag'] : null;
|
||||
|
||||
if (is_string($etag) && preg_match('|^".*"$|', $etag)) {
|
||||
$etag = substr($etag, 1, -1);
|
||||
}
|
||||
|
||||
return $etag;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize HTTP request object
|
||||
*/
|
||||
|
|
|
@ -751,7 +751,7 @@ abstract class kolab_format
|
|||
}
|
||||
|
||||
// in kolab_storage attachments are indexed by content-id
|
||||
foreach ((array) $object['attachments'] as $attachment) {
|
||||
foreach ((array) ($object['attachments'] ?? []) as $attachment) {
|
||||
$key = null;
|
||||
|
||||
// Roundcube ID has nothing to do with the storage ID, remove it
|
||||
|
|
|
@ -92,8 +92,9 @@ class kolab_storage_cache
|
|||
$rcmail->add_shutdown_function(array($this, '_sync_unlock'));
|
||||
}
|
||||
|
||||
if ($storage_folder)
|
||||
if ($storage_folder) {
|
||||
$this->set_folder($storage_folder);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1261,15 +1262,13 @@ class kolab_storage_cache
|
|||
|
||||
$read_query = "SELECT `synclock`, `ctag` FROM `{$this->folders_table}` WHERE `folder_id` = ?";
|
||||
$write_query = "UPDATE `{$this->folders_table}` SET `synclock` = ? WHERE `folder_id` = ? AND `synclock` = ?";
|
||||
|
||||
$max_lock_time = $this->_max_sync_lock_time();
|
||||
$sync_lock = intval($this->metadata['synclock'] ?? 0);
|
||||
|
||||
// wait if locked (expire locks after 10 minutes) ...
|
||||
// ... or if setting lock fails (another process meanwhile set it)
|
||||
while (
|
||||
($sync_lock + $max_lock_time > time()) ||
|
||||
(($res = $this->db->query($write_query, time(), $this->folder_id, $sync_lock))
|
||||
(intval($this->metadata['synclock'] ?? 0) + $max_lock_time > time()) ||
|
||||
(($res = $this->db->query($write_query, time(), $this->folder_id, intval($this->metadata['synclock'] ?? 0)))
|
||||
&& !($affected = $this->db->affected_rows($res))
|
||||
)
|
||||
) {
|
||||
|
|
|
@ -157,9 +157,10 @@ class kolab_storage_dav_cache extends kolab_storage_cache
|
|||
}
|
||||
}
|
||||
|
||||
$i = 0;
|
||||
|
||||
// Fetch new objects and store in DB
|
||||
if (!empty($new_index)) {
|
||||
$i = 0;
|
||||
foreach (array_chunk($new_index, $chunk_size, true) as $chunk) {
|
||||
$objects = $this->folder->dav->getData($this->folder->href, $this->folder->get_dav_type(), $chunk);
|
||||
|
||||
|
@ -707,4 +708,38 @@ class kolab_storage_dav_cache extends kolab_storage_cache
|
|||
|
||||
return $object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read this folder's ID and cache metadata
|
||||
*/
|
||||
protected function _read_folder_data()
|
||||
{
|
||||
// already done
|
||||
if (!empty($this->folder_id) || !$this->ready) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Different than in Kolab XML-based storage, in *DAV folders can
|
||||
// contain different types of data, e.g. Calendar can store events and tasks.
|
||||
// Therefore we both `resource` and `type` in WHERE.
|
||||
|
||||
$sql_arr = $this->db->fetch_assoc($this->db->query(
|
||||
"SELECT `folder_id`, `synclock`, `ctag`, `changed` FROM `{$this->folders_table}`"
|
||||
. " WHERE `resource` = ? AND `type` = ?",
|
||||
$this->resource_uri,
|
||||
$this->folder->type
|
||||
));
|
||||
|
||||
if ($sql_arr) {
|
||||
$this->folder_id = $sql_arr['folder_id'];
|
||||
$this->metadata = $sql_arr;
|
||||
}
|
||||
else {
|
||||
$this->db->query("INSERT INTO `{$this->folders_table}` (`resource`, `type`)"
|
||||
. " VALUES (?, ?)", $this->resource_uri, $this->folder->type);
|
||||
|
||||
$this->folder_id = $this->db->insert_id('kolab_folders');
|
||||
$this->metadata = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
112
plugins/libkolab/lib/kolab_storage_dav_cache_task.php
Normal file
112
plugins/libkolab/lib/kolab_storage_dav_cache_task.php
Normal file
|
@ -0,0 +1,112 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Kolab storage cache class for task objects
|
||||
*
|
||||
* @author Aleksander Machniak <machniak@apheleia-it.ch>
|
||||
*
|
||||
* Copyright (C) 2013-2022 Apheleia IT AG <contact@apheleia-it.ch>
|
||||
*
|
||||
* 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 kolab_storage_dav_cache_task extends kolab_storage_dav_cache
|
||||
{
|
||||
protected $extra_cols = ['dtstart','dtend'];
|
||||
protected $data_props = ['categories', 'status', 'complete', 'start', 'due'];
|
||||
protected $fulltext_cols = ['title', 'description', 'categories'];
|
||||
|
||||
/**
|
||||
* Helper method to convert the given Kolab object into a dataset to be written to cache
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
protected function _serialize($object)
|
||||
{
|
||||
$sql_data = parent::_serialize($object);
|
||||
|
||||
$sql_data['dtstart'] = !empty($object['start']) ? $this->_convert_datetime($object['start']) : null;
|
||||
$sql_data['dtend'] = !empty($object['due']) ? $this->_convert_datetime($object['due']) : null;
|
||||
|
||||
$sql_data['tags'] = ' ' . join(' ', $this->get_tags($object)) . ' '; // pad with spaces for strict/prefix search
|
||||
$sql_data['words'] = ' ' . join(' ', $this->get_words($object)) . ' ';
|
||||
|
||||
return $sql_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to get words to index for fulltext search
|
||||
*
|
||||
* @return array List of words to save in cache
|
||||
*/
|
||||
public function get_words($object = [])
|
||||
{
|
||||
$data = '';
|
||||
|
||||
foreach ($this->fulltext_cols as $colname) {
|
||||
list($col, $field) = strpos($colname, ':') ? explode(':', $colname) : [$colname, null];
|
||||
|
||||
if (empty($object[$col])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($field) {
|
||||
$a = [];
|
||||
foreach ((array) $object[$col] as $attr) {
|
||||
if (!empty($attr[$field])) {
|
||||
$a[] = $attr[$field];
|
||||
}
|
||||
}
|
||||
$val = join(' ', $a);
|
||||
}
|
||||
else {
|
||||
$val = is_array($object[$col]) ? join(' ', $object[$col]) : $object[$col];
|
||||
}
|
||||
|
||||
if (is_string($val) && strlen($val)) {
|
||||
$data .= $val . ' ';
|
||||
}
|
||||
}
|
||||
|
||||
$words = rcube_utils::normalize_string($data, true);
|
||||
|
||||
return array_unique($words);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to get object specific tags to cache
|
||||
*
|
||||
* @return array List of tags to save in cache
|
||||
*/
|
||||
public function get_tags($object)
|
||||
{
|
||||
$tags = [];
|
||||
|
||||
if ((isset($object['status']) && $object['status'] == 'COMPLETED')
|
||||
|| (isset($object['complete']) && $object['complete'] == 100 && empty($object['status']))
|
||||
) {
|
||||
$tags[] = 'x-complete';
|
||||
}
|
||||
|
||||
if (!empty($object['priority']) && $object['priority'] == 1) {
|
||||
$tags[] = 'x-flagged';
|
||||
}
|
||||
|
||||
if (!empty($object['parent_id'])) {
|
||||
$tags[] = 'x-parent:' . $object['parent_id'];
|
||||
}
|
||||
|
||||
return array_unique($tags);
|
||||
}
|
||||
}
|
|
@ -20,6 +20,8 @@
|
|||
* 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/>.
|
||||
*/
|
||||
|
||||
#[AllowDynamicProperties]
|
||||
class kolab_storage_dav_folder extends kolab_storage_folder
|
||||
{
|
||||
public $dav;
|
||||
|
@ -310,8 +312,8 @@ class kolab_storage_dav_folder extends kolab_storage_folder
|
|||
/**
|
||||
* Move a Kolab object message to another IMAP folder
|
||||
*
|
||||
* @param string Object UID
|
||||
* @param string IMAP folder to move object to
|
||||
* @param string Object UID
|
||||
* @param kolab_storage_dav_folder Target folder to move object into
|
||||
*
|
||||
* @return bool True on success, false on failure
|
||||
*/
|
||||
|
@ -321,9 +323,16 @@ class kolab_storage_dav_folder extends kolab_storage_folder
|
|||
return false;
|
||||
}
|
||||
|
||||
// TODO
|
||||
$source = $this->object_location($uid);
|
||||
$target = $target_folder->object_location($uid);
|
||||
|
||||
return false;
|
||||
$success = $this->dav->move($source, $target) !== false;
|
||||
|
||||
if ($success) {
|
||||
$this->cache->set($uid, false);
|
||||
}
|
||||
|
||||
return $success;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -466,15 +475,15 @@ class kolab_storage_dav_folder extends kolab_storage_folder
|
|||
return false;
|
||||
}
|
||||
|
||||
if ($this->type == 'event') {
|
||||
if ($this->type == 'event' || $this->type == 'task') {
|
||||
$ical = libcalendaring::get_ical();
|
||||
$events = $ical->import($object['data']);
|
||||
$objects = $ical->import($object['data']);
|
||||
|
||||
if (!count($events) || empty($events[0]['uid'])) {
|
||||
if (!count($objects) || empty($objects[0]['uid'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$result = $events[0];
|
||||
$result = $objects[0];
|
||||
|
||||
$result['_attachments'] = $result['attachments'] ?? [];
|
||||
unset($result['attachments']);
|
||||
|
@ -550,12 +559,15 @@ class kolab_storage_dav_folder extends kolab_storage_folder
|
|||
{
|
||||
$result = '';
|
||||
|
||||
if ($this->type == 'event') {
|
||||
if ($this->type == 'event' || $this->type == 'task') {
|
||||
$ical = libcalendaring::get_ical();
|
||||
|
||||
if (!empty($object['exceptions'])) {
|
||||
$object['recurrence']['EXCEPTIONS'] = $object['exceptions'];
|
||||
}
|
||||
|
||||
$object['_type'] = $this->type;
|
||||
|
||||
// pre-process attachments
|
||||
if (isset($object['_attachments']) && is_array($object['_attachments'])) {
|
||||
foreach ($object['_attachments'] as $key => $attachment) {
|
||||
|
@ -669,7 +681,7 @@ class kolab_storage_dav_folder extends kolab_storage_folder
|
|||
return $result;
|
||||
}
|
||||
|
||||
protected function object_location($uid)
|
||||
public function object_location($uid)
|
||||
{
|
||||
return unslashify($this->href) . '/' . urlencode($uid) . '.' . $this->get_dav_ext();
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
A task management module for Roundcube
|
||||
--------------------------------------
|
||||
|
||||
This plugin currently supports a local database as well as a Kolab groupware
|
||||
This plugin currently supports a local database, CalDAV server or a Kolab groupware
|
||||
server as backends for tasklists and todo items storage.
|
||||
|
||||
|
||||
|
@ -43,7 +43,7 @@ driver.
|
|||
$ cd ../../
|
||||
$ bin/initdb.sh --dir=plugins/tasklist/drivers/database/SQL
|
||||
|
||||
4. Build css styles for the Elastic skin
|
||||
4. Build css styles for the Elastic skin (if needed)
|
||||
|
||||
$ lessc --relative-urls -x plugins/libkolab/skins/elastic/libkolab.less > plugins/libkolab/skins/elastic/libkolab.min.css
|
||||
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
<?php
|
||||
|
||||
// backend type (database, kolab)
|
||||
// backend type (database, kolab, caldav)
|
||||
$config['tasklist_driver'] = 'kolab';
|
||||
|
||||
// CalDAV server location (required when tasklist_driver = caldav)
|
||||
$config['tasklist_caldav_server'] = "http://localhost";
|
||||
|
||||
// default sorting order of tasks listing (auto, datetime, startdatetime, flagged, complete, changed)
|
||||
$config['tasklist_sort_col'] = '';
|
||||
|
||||
|
|
1627
plugins/tasklist/drivers/caldav/tasklist_caldav_driver.php
Normal file
1627
plugins/tasklist/drivers/caldav/tasklist_caldav_driver.php
Normal file
File diff suppressed because it is too large
Load diff
|
@ -75,7 +75,7 @@ abstract class tasklist_driver
|
|||
public $attendees = false;
|
||||
public $undelete = false; // task undelete action
|
||||
public $sortable = false;
|
||||
public $alarm_types = array('DISPLAY');
|
||||
public $alarm_types = ['DISPLAY'];
|
||||
public $alarm_absolute = true;
|
||||
public $last_error;
|
||||
|
||||
|
@ -331,7 +331,7 @@ abstract class tasklist_driver
|
|||
public function get_message_related_tasks($headers, $folder)
|
||||
{
|
||||
// to be implemented by the derived classes
|
||||
return array();
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -342,7 +342,8 @@ abstract class tasklist_driver
|
|||
*/
|
||||
public function is_complete($task)
|
||||
{
|
||||
return ($task['complete'] >= 1.0 && empty($task['status'])) || $task['status'] === 'COMPLETED';
|
||||
return (isset($task['complete']) && $task['complete'] >= 1.0 && empty($task['status']))
|
||||
|| (!empty($task['status']) && $task['status'] === 'COMPLETED');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -428,7 +429,7 @@ abstract class tasklist_driver
|
|||
*/
|
||||
public function tasklist_edit_form($action, $list, $formfields)
|
||||
{
|
||||
$table = new html_table(array('cols' => 2, 'class' => 'propform'));
|
||||
$table = new html_table(['cols' => 2, 'class' => 'propform']);
|
||||
|
||||
foreach ($formfields as $col => $colprop) {
|
||||
$label = !empty($colprop['label']) ? $colprop['label'] : $rcmail->gettext("$domain.$col");
|
||||
|
@ -446,13 +447,13 @@ abstract class tasklist_driver
|
|||
public function tasklist_caldav_url($list)
|
||||
{
|
||||
$rcmail = rcube::get_instance();
|
||||
if (!empty($list['caldavuid']) && ($template = $rcmail->config->get('calendar_caldav_url', null))) {
|
||||
return strtr($template, array(
|
||||
if (!empty($list['caldavuid']) && ($template = $rcmail->config->get('calendar_caldav_url'))) {
|
||||
return strtr($template, [
|
||||
'%h' => $_SERVER['HTTP_HOST'],
|
||||
'%u' => urlencode($rcmail->get_user_name()),
|
||||
'%i' => urlencode($list['caldavuid']),
|
||||
'%n' => urlencode($list['editname']),
|
||||
));
|
||||
]);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
|
|
@ -99,7 +99,9 @@
|
|||
<legend><roundcube:label name="tasklist.tabassignments" /></legend>
|
||||
<div class="form-group row" id="taskedit-organizer">
|
||||
<label for="edit-identities-list" class="col-form-label col-sm-2"><roundcube:label name="tasklist.roleorganizer" /></label>
|
||||
<roundcube:object name="plugin.identity_select" id="edit-identities-list" class="col-sm-10 form-control" />
|
||||
<span class="col-sm-10">
|
||||
<roundcube:object name="plugin.identity_select" id="edit-identities-list" />
|
||||
</span>
|
||||
</div>
|
||||
<h3 id="aria-label-attendeestable" class="voice"><roundcube:label name="tasklist.arialabeleventassignments" /></h3>
|
||||
<roundcube:object name="plugin.attendees_list" id="edit-attendees-table" class="edit-attendees-table no-img table table-sm"
|
||||
|
|
|
@ -296,7 +296,7 @@ function rcube_tasklist_ui(settings)
|
|||
rcmail.addEventListener('plugin.update_tasklist', update_list);
|
||||
rcmail.addEventListener('plugin.destroy_tasklist', destroy_list);
|
||||
rcmail.addEventListener('plugin.unlock_saving', unlock_saving);
|
||||
rcmail.addEventListener('plugin.refresh_tagcloud', function() { update_tagcloud(); });
|
||||
rcmail.addEventListener('plugin.refresh_tagcloud', function() { update_taglist(); });
|
||||
rcmail.addEventListener('requestrefresh', before_refresh);
|
||||
rcmail.addEventListener('plugin.reload_data', function(){
|
||||
list_tasks(null, true);
|
||||
|
@ -2755,10 +2755,10 @@ function rcube_tasklist_ui(settings)
|
|||
*/
|
||||
function task_show_attachments(list, container, task, edit)
|
||||
{
|
||||
libkolab.list_attachments(list, container, edit, task,
|
||||
function(id) { remove_attachment(id); },
|
||||
function(data) { load_attachment(data); }
|
||||
);
|
||||
libkolab.list_attachments(list, container, edit, task,
|
||||
function(id) { remove_attachment(id); },
|
||||
function(data) { load_attachment(data); }
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#[AllowDynamicProperties]
|
||||
class tasklist extends rcube_plugin
|
||||
{
|
||||
const FILTER_MASK_TODAY = 1;
|
||||
|
@ -206,7 +207,8 @@ class tasklist extends rcube_plugin
|
|||
$action = rcube_utils::get_input_value('action', rcube_utils::INPUT_GPC);
|
||||
$rec = rcube_utils::get_input_value('t', rcube_utils::INPUT_POST, true);
|
||||
$oldrec = $rec;
|
||||
$success = $refresh = $got_msg = false;
|
||||
$success = $got_msg = false;
|
||||
$refresh = [];
|
||||
|
||||
// force notify if hidden + active
|
||||
$itip_send_option = (int)$this->rc->config->get('calendar_itip_send_option', 3);
|
||||
|
@ -284,8 +286,7 @@ class tasklist extends rcube_plugin
|
|||
foreach ((array)$rec['id'] as $id) {
|
||||
$r = $rec;
|
||||
$r['id'] = $id;
|
||||
if ($this->driver->move_task($r)) {
|
||||
$new_task = $this->driver->get_task($r);
|
||||
if ($this->driver->move_task($r) && ($new_task = $this->driver->get_task($r))) {
|
||||
$new_task['tempid'] = $id;
|
||||
$refresh[] = $new_task;
|
||||
$success = true;
|
||||
|
@ -330,7 +331,7 @@ class tasklist extends rcube_plugin
|
|||
// update parent task to adjust list of children
|
||||
if (!empty($oldrec['parent_id'])) {
|
||||
$parent = array('id' => $oldrec['parent_id'], 'list' => $rec['list']);
|
||||
if ($parent = $this->driver->get_task()) {
|
||||
if ($parent = $this->driver->get_task($parent)) {
|
||||
$refresh[] = $parent;
|
||||
}
|
||||
}
|
||||
|
@ -547,23 +548,26 @@ class tasklist extends rcube_plugin
|
|||
$itip = $this->load_itip();
|
||||
$itip->set_sender_email($sender['email']);
|
||||
|
||||
if ($itip->send_itip_message($this->to_libcal($task), 'REPLY', $task['organizer'], 'itipsubject' . $status, 'itipmailbody' . $status))
|
||||
if ($itip->send_itip_message($this->to_libcal($task), 'REPLY', $task['organizer'], 'itipsubject' . $status, 'itipmailbody' . $status)) {
|
||||
$this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $task['organizer']['name'] ?: $task['organizer']['email']))), 'confirmation');
|
||||
else
|
||||
}
|
||||
else {
|
||||
$this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// unlock client
|
||||
$this->rc->output->command('plugin.unlock_saving', $success);
|
||||
|
||||
if ($refresh) {
|
||||
if (!empty($refresh)) {
|
||||
if (!empty($refresh['id'])) {
|
||||
$this->encode_task($refresh);
|
||||
}
|
||||
else if (is_array($refresh)) {
|
||||
foreach ($refresh as $i => $r)
|
||||
foreach ($refresh as $i => $r) {
|
||||
$this->encode_task($refresh[$i]);
|
||||
}
|
||||
}
|
||||
$this->rc->output->command('plugin.update_task', $refresh);
|
||||
}
|
||||
|
@ -688,13 +692,13 @@ class tasklist extends rcube_plugin
|
|||
}
|
||||
|
||||
// convert the submitted recurrence settings
|
||||
if (is_array($rec['recurrence'])) {
|
||||
if (isset($rec['recurrence']) && is_array($rec['recurrence'])) {
|
||||
$refdate = null;
|
||||
if (!empty($rec['date'])) {
|
||||
$refdate = new DateTime($rec['date'] . ' ' . $rec['time'], $this->timezone);
|
||||
$refdate = new DateTime($rec['date'] . ' ' . ($rec['time'] ?? ''), $this->timezone);
|
||||
}
|
||||
else if (!empty($rec['startdate'])) {
|
||||
$refdate = new DateTime($rec['startdate'] . ' ' . $rec['starttime'], $this->timezone);
|
||||
$refdate = new DateTime($rec['startdate'] . ' ' . ($rec['starttime'] ?? ''), $this->timezone);
|
||||
}
|
||||
|
||||
if ($refdate) {
|
||||
|
@ -732,7 +736,7 @@ class tasklist extends rcube_plugin
|
|||
|
||||
if (!empty($rec['attendees'])) {
|
||||
foreach ((array) $rec['attendees'] as $i => $attendee) {
|
||||
if (is_string($attendee['rsvp'])) {
|
||||
if (isset($attendee['rsvp']) && is_string($attendee['rsvp'])) {
|
||||
$rec['attendees'][$i]['rsvp'] = $attendee['rsvp'] == 'true' || $attendee['rsvp'] == '1';
|
||||
}
|
||||
}
|
||||
|
@ -768,16 +772,17 @@ class tasklist extends rcube_plugin
|
|||
try {
|
||||
// parse date from user format (#2801)
|
||||
$date_format = $this->rc->config->get(empty($rec[$time_key]) ? 'date_format' : 'date_long', 'Y-m-d');
|
||||
$date = DateTime::createFromFormat($date_format, trim($rec[$date_key] . ' ' . $rec[$time_key]), $this->timezone);
|
||||
$date = DateTime::createFromFormat($date_format, trim(($rec[$date_key] ?? '') . ' ' . ($rec[$time_key] ?? '')), $this->timezone);
|
||||
|
||||
// fall back to default strtotime logic
|
||||
if (empty($date)) {
|
||||
$date = new DateTime($rec[$date_key] . ' ' . $rec[$time_key], $this->timezone);
|
||||
$date = new DateTime(($rec[$date_key] ?? '') . ' ' . ($rec[$time_key] ?? ''), $this->timezone);
|
||||
}
|
||||
|
||||
$rec[$date_key] = $date->format('Y-m-d');
|
||||
if (!empty($rec[$time_key]))
|
||||
if (!empty($rec[$time_key])) {
|
||||
$rec[$time_key] = $date->format('H:i');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -804,7 +809,7 @@ class tasklist extends rcube_plugin
|
|||
private function handle_recurrence(&$rec, $old)
|
||||
{
|
||||
$clone = null;
|
||||
if ($this->driver->is_complete($rec) && $old && !$this->driver->is_complete($old) && is_array($rec['recurrence'])) {
|
||||
if ($this->driver->is_complete($rec) && $old && !$this->driver->is_complete($old) && !empty($rec['recurrence'])) {
|
||||
$engine = libcalendaring::get_recurrence();
|
||||
$rrule = $rec['recurrence'];
|
||||
$updates = array();
|
||||
|
@ -814,12 +819,13 @@ class tasklist extends rcube_plugin
|
|||
if (empty($rec[$date_key]))
|
||||
continue;
|
||||
|
||||
$date = new DateTime($rec[$date_key] . ' ' . $rec[$time_key], $this->timezone);
|
||||
$date = new DateTime($rec[$date_key] . ' ' . ($rec[$time_key] ?? ''), $this->timezone);
|
||||
$engine->init($rrule, $date);
|
||||
if ($next = $engine->next_start()) {
|
||||
$updates[$date_key] = $next->format('Y-m-d');
|
||||
if (!empty($rec[$time_key]))
|
||||
if (!empty($rec[$time_key])) {
|
||||
$updates[$time_key] = $next->format('H:i');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1174,15 +1180,15 @@ class tasklist extends rcube_plugin
|
|||
*/
|
||||
private function encode_task(&$rec)
|
||||
{
|
||||
$rec['mask'] = $this->filter_mask($rec);
|
||||
$rec['flagged'] = intval($rec['flagged']);
|
||||
$rec['complete'] = floatval($rec['complete']);
|
||||
$rec['mask'] = $this->filter_mask($rec);
|
||||
$rec['flagged'] = intval($rec['flagged'] ?? 0);
|
||||
$rec['complete'] = floatval($rec['complete'] ?? 0);
|
||||
|
||||
if (is_object($rec['created'])) {
|
||||
if (!empty($rec['created']) && is_object($rec['created'])) {
|
||||
$rec['created_'] = $this->rc->format_date($rec['created']);
|
||||
$rec['created'] = $rec['created']->format('U');
|
||||
}
|
||||
if (is_object($rec['changed'])) {
|
||||
if (!empty($rec['changed']) && is_object($rec['changed'])) {
|
||||
$rec['changed_'] = $this->rc->format_date($rec['changed']);
|
||||
$rec['changed'] = $rec['changed']->format('U');
|
||||
}
|
||||
|
@ -1190,9 +1196,9 @@ class tasklist extends rcube_plugin
|
|||
$rec['changed'] = null;
|
||||
}
|
||||
|
||||
if ($rec['date']) {
|
||||
if (!empty($rec['date'])) {
|
||||
try {
|
||||
$date = new DateTime($rec['date'] . ' ' . $rec['time'], $this->timezone);
|
||||
$date = new DateTime($rec['date'] . ' ' . ($rec['time'] ?? ''), $this->timezone);
|
||||
$rec['datetime'] = intval($date->format('U'));
|
||||
$rec['date'] = $date->format($this->rc->config->get('date_format', 'Y-m-d'));
|
||||
$rec['_hasdate'] = 1;
|
||||
|
@ -1206,9 +1212,9 @@ class tasklist extends rcube_plugin
|
|||
$rec['_hasdate'] = 0;
|
||||
}
|
||||
|
||||
if ($rec['startdate']) {
|
||||
if (!empty($rec['startdate'])) {
|
||||
try {
|
||||
$date = new DateTime($rec['startdate'] . ' ' . $rec['starttime'], $this->timezone);
|
||||
$date = new DateTime($rec['startdate'] . ' ' . ($rec['starttime'] ?? ''), $this->timezone);
|
||||
$rec['startdatetime'] = intval($date->format('U'));
|
||||
$rec['startdate'] = $date->format($this->rc->config->get('date_format', 'Y-m-d'));
|
||||
}
|
||||
|
@ -1224,12 +1230,13 @@ class tasklist extends rcube_plugin
|
|||
|
||||
if (!empty($rec['recurrence'])) {
|
||||
$rec['recurrence_text'] = $this->lib->recurrence_text($rec['recurrence']);
|
||||
$rec['recurrence'] = $this->lib->to_client_recurrence($rec['recurrence'], $rec['time'] || $rec['starttime']);
|
||||
$rec['recurrence'] = $this->lib->to_client_recurrence($rec['recurrence'], !empty($rec['time']) || !empty($rec['starttime']));
|
||||
}
|
||||
|
||||
if (!empty($rec['attachments'])) {
|
||||
foreach ((array) $rec['attachments'] as $k => $attachment) {
|
||||
$rec['attachments'][$k]['classname'] = rcube_utils::file2class($attachment['mimetype'], $attachment['name']);
|
||||
unset($rec['attachments'][$k]['data']);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1248,17 +1255,21 @@ class tasklist extends rcube_plugin
|
|||
$rec['description'] = $h2t->get_text();
|
||||
}
|
||||
|
||||
if (!is_array($rec['tags']))
|
||||
$rec['tags'] = (array)$rec['tags'];
|
||||
if (!isset($rec['tags']) || !is_array($rec['tags'])) {
|
||||
$rec['tags'] = (array) ($rec['tags'] ?? '');
|
||||
}
|
||||
|
||||
sort($rec['tags'], SORT_LOCALE_STRING);
|
||||
|
||||
if (in_array($rec['id'], $this->collapsed_tasks))
|
||||
$rec['collapsed'] = true;
|
||||
if (in_array($rec['id'], $this->collapsed_tasks)) {
|
||||
$rec['collapsed'] = true;
|
||||
}
|
||||
|
||||
if (empty($rec['parent_id']))
|
||||
if (empty($rec['parent_id'])) {
|
||||
$rec['parent_id'] = null;
|
||||
}
|
||||
|
||||
$this->task_titles[$rec['id']] = $rec['title'];
|
||||
$this->task_titles[$rec['id']] = $rec['title'] ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1267,7 +1278,9 @@ class tasklist extends rcube_plugin
|
|||
private function is_html($task)
|
||||
{
|
||||
// check for opening and closing <html> or <body> tags
|
||||
return (preg_match('/<(html|body)(\s+[a-z]|>)/', $task['description'], $m) && strpos($task['description'], '</'.$m[1].'>') > 0);
|
||||
return isset($task['description'])
|
||||
&& preg_match('/<(html|body)(\s+[a-z]|>)/', $task['description'], $m)
|
||||
&& strpos($task['description'], '</' . $m[1] . '>') > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1303,9 +1316,9 @@ class tasklist extends rcube_plugin
|
|||
static $today, $today_date, $tomorrow, $weeklimit;
|
||||
|
||||
if (!$today) {
|
||||
$today_date = new DateTime('now', $this->timezone);
|
||||
$today_date = new libcalendaring_datetime('now', $this->timezone);
|
||||
$today = $today_date->format('Y-m-d');
|
||||
$tomorrow_date = new DateTime('now + 1 day', $this->timezone);
|
||||
$tomorrow_date = new libcalendaring_datetime('now + 1 day', $this->timezone);
|
||||
$tomorrow = $tomorrow_date->format('Y-m-d');
|
||||
|
||||
// In Kolab-mode we hide "Next 7 days" filter, which means
|
||||
|
@ -1320,21 +1333,25 @@ class tasklist extends rcube_plugin
|
|||
}
|
||||
|
||||
$mask = 0;
|
||||
$start = $rec['startdate'] ?: '1900-00-00';
|
||||
$duedate = $rec['date'] ?: '3000-00-00';
|
||||
$start = !empty($rec['startdate']) ? $rec['startdate'] : '1900-00-00';
|
||||
$duedate = !empty($rec['date']) ? $rec['date'] : '3000-00-00';
|
||||
|
||||
if ($rec['flagged'])
|
||||
if (!empty($rec['flagged'])) {
|
||||
$mask |= self::FILTER_MASK_FLAGGED;
|
||||
if ($this->driver->is_complete($rec))
|
||||
}
|
||||
if ($this->driver->is_complete($rec)) {
|
||||
$mask |= self::FILTER_MASK_COMPLETE;
|
||||
}
|
||||
|
||||
if (empty($rec['date']))
|
||||
if (empty($rec['date'])) {
|
||||
$mask |= self::FILTER_MASK_NODATE;
|
||||
else if ($rec['date'] < $today)
|
||||
}
|
||||
else if ($rec['date'] < $today) {
|
||||
$mask |= self::FILTER_MASK_OVERDUE;
|
||||
}
|
||||
|
||||
if (empty($rec['recurrence']) || $duedate < $today || $start > $weeklimit) {
|
||||
if ($duedate <= $today || ($rec['startdate'] && $start <= $today))
|
||||
if ($duedate <= $today || (!empty($rec['startdate']) && $start <= $today))
|
||||
$mask |= self::FILTER_MASK_TODAY;
|
||||
else if (($start > $today && $start <= $tomorrow) || ($duedate > $today && $duedate <= $tomorrow))
|
||||
$mask |= self::FILTER_MASK_TOMORROW;
|
||||
|
@ -1343,8 +1360,8 @@ class tasklist extends rcube_plugin
|
|||
else if ($start > $weeklimit || $duedate > $weeklimit)
|
||||
$mask |= self::FILTER_MASK_LATER;
|
||||
}
|
||||
else if ($rec['startdate'] || $rec['date']) {
|
||||
$date = new DateTime($rec['startdate'] ?: $rec['date'], $this->timezone);
|
||||
else if (!empty($rec['startdate']) || !empty($rec['date'])) {
|
||||
$date = new libcalendaring_datetime(!empty($rec['startdate']) ? $rec['startdate'] : $rec['date'], $this->timezone);
|
||||
|
||||
// set safe recurrence start
|
||||
while ($date->format('Y-m-d') >= $today) {
|
||||
|
@ -1392,10 +1409,12 @@ class tasklist extends rcube_plugin
|
|||
}
|
||||
|
||||
// add masks for assigned tasks
|
||||
if ($this->is_organizer($rec) && !empty($rec['attendees']) && $this->is_attendee($rec) === false)
|
||||
if ($this->is_organizer($rec) && !empty($rec['attendees']) && $this->is_attendee($rec) === false) {
|
||||
$mask |= self::FILTER_MASK_ASSIGNED;
|
||||
else if (/*empty($rec['attendees']) ||*/ $this->is_attendee($rec) !== false)
|
||||
}
|
||||
else if (/*empty($rec['attendees']) ||*/ $this->is_attendee($rec) !== false) {
|
||||
$mask |= self::FILTER_MASK_MYTASKS;
|
||||
}
|
||||
|
||||
return $mask;
|
||||
}
|
||||
|
@ -1406,7 +1425,7 @@ class tasklist extends rcube_plugin
|
|||
public function is_attendee($task)
|
||||
{
|
||||
$emails = $this->lib->get_user_emails();
|
||||
foreach ((array)$task['attendees'] as $i => $attendee) {
|
||||
foreach ((array) ($task['attendees'] ?? []) as $i => $attendee) {
|
||||
if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
|
||||
return $i;
|
||||
}
|
||||
|
@ -1732,7 +1751,7 @@ class tasklist extends rcube_plugin
|
|||
$id = rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC);
|
||||
$rev = rcube_utils::get_input_value('_rev', rcube_utils::INPUT_GPC);
|
||||
|
||||
$task = array('id' => $task, 'list' => $list, 'rev' => $rev);
|
||||
$task = ['id' => $task, 'list' => $list, 'rev' => $rev];
|
||||
$attachment = $this->driver->get_attachment($id, $task);
|
||||
|
||||
// show part page
|
||||
|
@ -1741,7 +1760,9 @@ class tasklist extends rcube_plugin
|
|||
}
|
||||
// deliver attachment content
|
||||
else if ($attachment) {
|
||||
$attachment['body'] = $this->driver->get_attachment_body($id, $task);
|
||||
if (empty($attachment['body'])) {
|
||||
$attachment['body'] = $this->driver->get_attachment_body($id, $task);
|
||||
}
|
||||
$handler->attachment_get($attachment);
|
||||
}
|
||||
|
||||
|
|
|
@ -21,13 +21,13 @@
|
|||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
||||
#[AllowDynamicProperties]
|
||||
class tasklist_ui
|
||||
{
|
||||
private $rc;
|
||||
private $plugin;
|
||||
private $ready = false;
|
||||
private $gui_objects = array();
|
||||
private $gui_objects = [];
|
||||
|
||||
function __construct($plugin)
|
||||
{
|
||||
|
@ -74,7 +74,7 @@ class tasklist_ui
|
|||
*/
|
||||
function load_settings()
|
||||
{
|
||||
$settings = array();
|
||||
$settings = [];
|
||||
|
||||
$settings['invite_shared'] = (int)$this->rc->config->get('calendar_allow_invite_shared', 0);
|
||||
$settings['itip_notify'] = (int)$this->rc->config->get('calendar_itip_send_option', 3);
|
||||
|
@ -120,7 +120,7 @@ class tasklist_ui
|
|||
/**
|
||||
* Render a HTML select box for user identity selection
|
||||
*/
|
||||
function identity_select($attrib = array())
|
||||
function identity_select($attrib = [])
|
||||
{
|
||||
$attrib['name'] = 'identity';
|
||||
$select = new html_select($attrib);
|
||||
|
@ -165,10 +165,10 @@ class tasklist_ui
|
|||
/**
|
||||
*
|
||||
*/
|
||||
public function tasklists($attrib = array())
|
||||
public function tasklists($attrib = [])
|
||||
{
|
||||
$tree = true;
|
||||
$jsenv = array();
|
||||
$tree = true;
|
||||
$jsenv = [];
|
||||
$lists = $this->plugin->driver->get_lists(0, $tree);
|
||||
|
||||
if (empty($attrib['id'])) {
|
||||
|
@ -181,18 +181,18 @@ class tasklist_ui
|
|||
}
|
||||
else {
|
||||
// fall-back to flat folder listing
|
||||
$attrib['class'] .= ' flat';
|
||||
|
||||
$attrib['class'] = ($attrib['class'] ?? '') . ' flat';
|
||||
$html = '';
|
||||
foreach ((array)$lists as $id => $prop) {
|
||||
|
||||
foreach ((array) $lists as $id => $prop) {
|
||||
if (!empty($attrib['activeonly']) && empty($prop['active'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$html .= html::tag('li', array(
|
||||
$html .= html::tag('li', [
|
||||
'id' => 'rcmlitasklist' . rcube_utils::html_identifier($id),
|
||||
'class' => isset($prop['group']) ? $prop['group'] : null,
|
||||
),
|
||||
'class' => $prop['group'] ?? null,
|
||||
],
|
||||
$this->tasklist_list_item($id, $prop, $jsenv, !empty($attrib['activeonly']))
|
||||
);
|
||||
}
|
||||
|
@ -288,15 +288,19 @@ class tasklist_ui
|
|||
'aria-labelledby' => $label_id
|
||||
));
|
||||
|
||||
$actions = '';
|
||||
if (!empty($prop['removable'])) {
|
||||
$actions .= html::a(['href' => '#', 'class' => 'remove', 'title' => $this->plugin->gettext('removelist')], ' ');
|
||||
}
|
||||
$actions .= html::a(['href' => '#', 'class' => 'quickview', 'title' => $this->plugin->gettext('focusview'), 'role' => 'checkbox', 'aria-checked' => 'false'], ' ');
|
||||
if (isset($prop['subscribed'])) {
|
||||
$action .= html::a(['href' => '#', 'class' => 'subscribed', 'title' => $this->plugin->gettext('tasklistsubscribe'), 'role' => 'checkbox', 'aria-checked' => $prop['subscribed'] ? 'true' : 'false'], ' ');
|
||||
}
|
||||
|
||||
return html::div(join(' ', $classes),
|
||||
html::a(array('class' => 'listname', 'title' => $title, 'href' => '#', 'id' => $label_id),
|
||||
!empty($prop['listname']) ? $prop['listname'] : $prop['name']) .
|
||||
(!empty($prop['virtual']) ? '' : $chbox . html::span('actions',
|
||||
(!empty($prop['removable']) ? html::a(array('href' => '#', 'class' => 'remove', 'title' => $this->plugin->gettext('removelist')), ' ') : '')
|
||||
. html::a(array('href' => '#', 'class' => 'quickview', 'title' => $this->plugin->gettext('focusview'), 'role' => 'checkbox', 'aria-checked' => 'false'), ' ')
|
||||
. (isset($prop['subscribed']) ? html::a(array('href' => '#', 'class' => 'subscribed', 'title' => $this->plugin->gettext('tasklistsubscribe'), 'role' => 'checkbox', 'aria-checked' => $prop['subscribed'] ? 'true' : 'false'), ' ') : '')
|
||||
)
|
||||
)
|
||||
html::a(['class' => 'listname', 'title' => $title, 'href' => '#', 'id' => $label_id],
|
||||
!empty($prop['listname']) ? $prop['listname'] : $prop['name'])
|
||||
. (!empty($prop['virtual']) ? '' : $chbox . html::span('actions', $actions))
|
||||
);
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue