summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGunnar Wrobel <wrobel@gentoo.org>2008-08-08 13:20:39 +0000
committerGunnar Wrobel <wrobel@gentoo.org>2008-08-08 13:20:39 +0000
commiteb785693bfff428c30f77672b1436a1e37980fc3 (patch)
treed6da09e39fc66b886098be73d58e39e889052bea /www-apps/horde-webmail/files
parentAdd longdescription to metadata.xml. (diff)
downloadgentoo-2-eb785693bfff428c30f77672b1436a1e37980fc3.tar.gz
gentoo-2-eb785693bfff428c30f77672b1436a1e37980fc3.tar.bz2
gentoo-2-eb785693bfff428c30f77672b1436a1e37980fc3.zip
Added a kolabified version of the horde-webmail package.
(Portage version: 2.1.4.4)
Diffstat (limited to 'www-apps/horde-webmail/files')
-rw-r--r--www-apps/horde-webmail/files/horde-webmail-1.1.1_kolab.patch3411
-rw-r--r--www-apps/horde-webmail/files/postinstall-en.txt.kolab15
-rw-r--r--www-apps/horde-webmail/files/reconfig.kolab15
3 files changed, 3441 insertions, 0 deletions
diff --git a/www-apps/horde-webmail/files/horde-webmail-1.1.1_kolab.patch b/www-apps/horde-webmail/files/horde-webmail-1.1.1_kolab.patch
new file mode 100644
index 000000000000..8c579bee51ea
--- /dev/null
+++ b/www-apps/horde-webmail/files/horde-webmail-1.1.1_kolab.patch
@@ -0,0 +1,3411 @@
+diff -r e16c159ea616 config/conf.php
+--- /dev/null Thu Jan 01 00:00:00 1970 +0000
++++ b/config/conf.php Fri Aug 08 14:19:56 2008 +0200
+@@ -0,0 +1,113 @@
++<?php
++/* CONFIG START. DO NOT CHANGE ANYTHING IN OR AFTER THIS LINE. */
++// $Horde: horde/config/conf.xml,v 1.230 2008/05/26 11:51:43 jan Exp $
++$conf['vhosts'] = false;
++$conf['debug_level'] = E_ALL;
++$conf['max_exec_time'] = 0;
++$conf['compress_pages'] = true;
++$conf['umask'] = 077;
++$conf['use_ssl'] = 2;
++$conf['server']['name'] = $_SERVER['SERVER_NAME'];
++$conf['server']['port'] = $_SERVER['SERVER_PORT'];
++$conf['urls']['pretty'] = false;
++$conf['safe_ips'] = array();
++$conf['session']['name'] = 'Horde';
++$conf['session']['use_only_cookies'] = false;
++$conf['session']['cache_limiter'] = 'nocache';
++$conf['session']['timeout'] = 0;
++$conf['cookie']['domain'] = $_SERVER['SERVER_NAME'];
++$conf['cookie']['path'] = '/';
++$conf['sql']['database'] = dirname(__FILE__) . '/../storage/horde.db';
++$conf['sql']['mode'] = '0640';
++$conf['sql']['charset'] = 'utf-8';
++$conf['sql']['phptype'] = 'sqlite';
++$conf['auth']['admins'] = array('manager');
++$conf['auth']['checkip'] = true;
++$conf['auth']['checkbrowser'] = true;
++$conf['auth']['alternate_login'] = false;
++$conf['auth']['redirect_on_logout'] = false;
++$conf['auth']['params']['login_block'] = true;
++$conf['auth']['params']['login_block_count'] = 3;
++$conf['auth']['params']['login_block_time'] = 5;
++$conf['auth']['driver'] = 'kolab';
++$conf['signup']['allow'] = false;
++$conf['log']['priority'] = PEAR_LOG_DEBUG;
++$conf['log']['ident'] = 'HORDE';
++$conf['log']['params'] = array();
++$conf['log']['name'] = dirname(__FILE__) . '/../log/horde.log';
++$conf['log']['params']['append'] = true;
++$conf['log']['type'] = 'file';
++$conf['log']['enabled'] = true;
++$conf['log_accesskeys'] = false;
++$conf['prefs']['driver'] = 'kolab_imap';
++$conf['alarms']['params']['driverconfig'] = 'horde';
++$conf['alarms']['params']['ttl'] = 300;
++$conf['alarms']['driver'] = 'sql';
++$conf['datatree']['params']['driverconfig'] = 'horde';
++$conf['datatree']['driver'] = 'sql';
++$conf['group']['driver'] = 'kolab';
++$conf['group']['cache'] = false;
++$conf['perms']['driverconfig'] = 'horde';
++$conf['perms']['driver'] = 'sql';
++$conf['share']['no_sharing'] = false;
++$conf['share']['any_group'] = false;
++$conf['share']['cache'] = true;
++$conf['share']['driver'] = 'kolab';
++$conf['cache']['default_lifetime'] = 1800;
++$conf['cache']['params']['dir'] = Horde::getTempDir();
++$conf['cache']['params']['sub'] = 0;
++$conf['cache']['driver'] = 'file';
++$conf['lock']['driver'] = 'none';
++$conf['token']['driver'] = 'none';
++$conf['mailer']['params']['auth'] = false;
++$conf['mailer']['type'] = 'smtp';
++$conf['mailformat']['brokenrfc2231'] = false;
++$conf['tmpdir'] = dirname(__FILE__) . '/../tmp/';
++$conf['vfs']['params']['vfsroot'] = dirname(__FILE__) . '/../storage';
++$conf['vfs']['type'] = 'file';
++$conf['sessionhandler']['type'] = 'none';
++$conf['sessionhandler']['memcache'] = false;
++$conf['image']['convert'] = '/usr/bin/convert';
++$conf['mime']['magic_db'] = '/etc/mime.types';
++$conf['problems']['email'] = 'webmaster@example.com';
++$conf['problems']['maildomain'] = 'example.com';
++$conf['problems']['tickets'] = false;
++$conf['problems']['attachments'] = true;
++$conf['menu']['apps'] = array();
++$conf['menu']['always'] = false;
++$conf['menu']['links']['help'] = 'all';
++$conf['menu']['links']['help_about'] = true;
++$conf['menu']['links']['options'] = 'authenticated';
++$conf['menu']['links']['problem'] = 'all';
++$conf['menu']['links']['login'] = 'all';
++$conf['menu']['links']['logout'] = 'authenticated';
++$conf['hooks']['permsdenied'] = false;
++$conf['hooks']['username'] = false;
++$conf['hooks']['preauthenticate'] = false;
++$conf['hooks']['postauthenticate'] = false;
++$conf['hooks']['authldap'] = false;
++$conf['hooks']['groupldap'] = false;
++$conf['portal']['fixed_blocks'] = array();
++$conf['accounts']['driver'] = 'null';
++$conf['user']['verify_from_addr'] = false;
++$conf['imsp']['enabled'] = false;
++$conf['kolab']['ldap']['server'] = 'localhost';
++$conf['kolab']['ldap']['port'] = 389;
++$conf['kolab']['ldap']['basedn'] = 'dc=example,dc=com';
++$conf['kolab']['ldap']['phpdn'] = 'cn=nobody,cn=internal,dc=example,dc=com';
++$conf['kolab']['ldap']['phppw'] = 'dummy';
++$conf['kolab']['imap']['server'] = 'localhost';
++$conf['kolab']['imap']['port'] = 143;
++$conf['kolab']['imap']['sieveport'] = 2000;
++$conf['kolab']['imap']['maildomain'] = 'example.com';
++$conf['kolab']['imap']['virtdomains'] = true;
++$conf['kolab']['smtp']['server'] = 'localhost';
++$conf['kolab']['smtp']['port'] = 25;
++$conf['kolab']['misc']['multidomain'] = false;
++$conf['kolab']['cache_folders'] = true;
++$conf['kolab']['enabled'] = true;
++$conf['memcache']['enabled'] = false;
++/* CONFIG END. DO NOT CHANGE ANYTHING IN OR BEFORE THIS LINE. */
++if (file_exists(dirname(__FILE__) . '/kolab.php')) {
++ require_once(dirname(__FILE__) . '/kolab.php');
++}
+diff -r e16c159ea616 config/kolab.php
+--- /dev/null Thu Jan 01 00:00:00 1970 +0000
++++ b/config/kolab.php Fri Aug 08 14:19:56 2008 +0200
+@@ -0,0 +1,34 @@
++<?php
++/* Add additional admins here */
++$conf['auth']['admins'] = array('manager');
++
++/* Add your horde vhost hostname here */
++$conf['cookie']['domain'] = 'webmail.example.com';
++
++/* Add the path to the vhost install here (the value of the -d switch when you used webapp-config) */
++$conf['cookie']['path'] = '/';
++
++/* The email address of your sys admin */
++$conf['problems']['email'] = 'hostmaster@example.com';
++
++/* Primary mail domain of your Kolab server */
++$conf['problems']['maildomain'] = 'pardus.de';
++
++/* The hostname of your LDAP server (usually the same as the hostname of the Kolab server) */
++$conf['kolab']['ldap']['server'] = 'localhost';
++
++/* Base DN of your LDAP server */
++$conf['kolab']['ldap']['basedn'] = 'dc=example,dc=com';
++
++/* php_dn from your /kolab/etc/kolab/kolab.conf */
++$conf['kolab']['ldap']['phpdn'] = 'cn=nobody,cn=internal,dc=example,dc=com';
++
++/* php_pw from your /kolab/etc/kolab/kolab.conf */
++$conf['kolab']['ldap']['phppw'] = 'MY_SECRET_PASSWORD';
++
++/* Hostname of your IMAP server (usually the same as the hostname of the Kolab server) */
++$conf['kolab']['imap']['server'] = 'localhost';
++
++/* Primary mail domain of your Kolab server */
++$conf['kolab']['imap']['maildomain'] = 'example.com';
++?>
+diff -r e16c159ea616 config/prefs.php
+--- a/config/prefs.php Fri Aug 08 13:11:07 2008 +0200
++++ b/config/prefs.php Fri Aug 08 14:19:56 2008 +0200
+@@ -357,7 +357,7 @@
+
+ // UI theme
+ $_prefs['theme'] = array(
+- 'value' => 'bluewhite',
++ 'value' => 'silver',
+ 'locked' => false,
+ 'shared' => true,
+ 'type' => 'select',
+diff -r e16c159ea616 imp/config/conf.php
+--- a/imp/config/conf.php Fri Aug 08 13:11:07 2008 +0200
++++ b/imp/config/conf.php Fri Aug 08 14:19:56 2008 +0200
+@@ -1,7 +1,8 @@
+ <?php
+ /* CONFIG START. DO NOT CHANGE ANYTHING IN OR AFTER THIS LINE. */
+ // $Horde: imp/config/conf.xml,v 1.53.2.33 2008/05/06 17:54:04 slusarz Exp $
+-$conf['spell']['driver'] = '';
++$conf['spell']['driver'] = 'aspell';
++$conf['utils']['gnupg'] = '/usr/bin/gpg';
+ $conf['utils']['gnupg_keyserver'] = array('pgp.mit.edu');
+ $conf['utils']['gnupg_timeout'] = 10;
+ $conf['menu']['apps'] = array();
+@@ -52,8 +53,8 @@
+ $conf['hooks']['signature'] = false;
+ $conf['hooks']['trailer'] = false;
+ $conf['hooks']['fetchmail_filter'] = false;
+-$conf['hooks']['mbox_redirect'] = false;
+-$conf['hooks']['mbox_icon'] = false;
++$conf['hooks']['mbox_redirect'] = true;
++$conf['hooks']['mbox_icon'] = true;
+ $conf['hooks']['spam_bounce'] = false;
+ $conf['hooks']['msglist_format'] = false;
+ $conf['maillog']['use_maillog'] = true;
+diff -r e16c159ea616 imp/config/hooks.php
+--- a/imp/config/hooks.php Fri Aug 08 13:11:07 2008 +0200
++++ b/imp/config/hooks.php Fri Aug 08 14:19:56 2008 +0200
+@@ -426,6 +426,9 @@
+ case 'contact':
+ return $GLOBALS['registry']->get('webroot', 'turba');
+
++ case 'prefs':
++ return $GLOBALS['registry']->get('webroot', 'horde') . '/services/prefs.php?app=horde';
++
+ default:
+ return '';
+ }
+@@ -476,6 +479,14 @@
+ 'alt' => _("Contacts")
+ );
+ break;
++
++ case 'prefs':
++ $icons[$name] = array(
++ 'icon' => 'prefs.png',
++ 'icondir' => $GLOBALS['registry']->getImageDir('horde'),
++ 'alt' => _("Preferences")
++ );
++ break;
+ }
+ }
+
+diff -r e16c159ea616 imp/config/servers.php
+--- a/imp/config/servers.php Fri Aug 08 13:11:07 2008 +0200
++++ b/imp/config/servers.php Fri Aug 08 14:19:56 2008 +0200
+@@ -215,10 +215,30 @@
+
+ /* Example configurations: */
+
+-$servers['imap'] = array(
+- 'name' => 'IMAP Server',
+- 'server' => 'localhost',
+- 'hordeauth' => false,
+- 'protocol' => 'imap/notls',
+- 'port' => 143,
+-);
++if ($GLOBALS['conf']['kolab']['enabled']) {
++ require_once 'Horde/Kolab.php';
++
++ if (!is_callable('Kolab', 'getServer')) {
++ $server = $GLOBALS['conf']['kolab']['imap']['server'];
++ } else {
++ $server = Kolab::getServer('imap');
++ }
++
++ $servers['kolab'] = array(
++ 'name' => 'Kolab Cyrus IMAP Server',
++ 'server' => $server,
++ 'hordeauth' => 'full',
++ 'protocol' => 'imap/notls/novalidate-cert',
++ 'port' => $GLOBALS['conf']['kolab']['imap']['port'],
++ 'maildomain' => $GLOBALS['conf']['kolab']['imap']['maildomain'],
++ 'realm' => '',
++ 'preferred' => '',
++ 'quota' => array(
++ 'driver' => 'imap',
++ 'params' => array('hide_quota_when_unlimited' => true),
++ ),
++ 'acl' => array(
++ 'driver' => 'rfc2086',
++ ),
++ );
++}
+diff -r e16c159ea616 kronolith/config/conf.php
+--- a/kronolith/config/conf.php Fri Aug 08 13:11:07 2008 +0200
++++ b/kronolith/config/conf.php Fri Aug 08 14:19:56 2008 +0200
+@@ -1,12 +1,11 @@
+ <?php
+ /* CONFIG START. DO NOT CHANGE ANYTHING IN OR AFTER THIS LINE. */
+ // $Horde: kronolith/config/conf.xml,v 1.14.10.5 2007/12/20 14:12:23 jan Exp $
+-$conf['calendar']['params']['table'] = 'kronolith_events';
+-$conf['calendar']['params']['driverconfig'] = 'horde';
+-$conf['calendar']['driver'] = 'sql';
+-$conf['storage']['params']['table'] = 'kronolith_storage';
+-$conf['storage']['params']['driverconfig'] = 'horde';
+-$conf['storage']['driver'] = 'sql';
++$conf['calendar']['driver'] = 'kolab';
++$conf['storage']['driver'] = 'kolab';
++$conf['storage']['default_domain'] = '';
++$conf['storage']['freebusy']['protocol'] = 'https';
++$conf['storage']['freebusy']['port'] = 443;
+ $conf['metadata']['keywords'] = false;
+ $conf['autoshare']['shareperms'] = 'none';
+ $conf['holidays']['enable'] = true;
+@@ -14,3 +13,6 @@
+ $conf['menu']['import_export'] = true;
+ $conf['menu']['apps'] = array();
+ /* CONFIG END. DO NOT CHANGE ANYTHING IN OR BEFORE THIS LINE. */
++if (file_exists(dirname(__FILE__) . '/kolab.php')) {
++ require_once(dirname(__FILE__) . '/kolab.php');
++}
+diff -r e16c159ea616 kronolith/config/kolab.php
+--- /dev/null Thu Jan 01 00:00:00 1970 +0000
++++ b/kronolith/config/kolab.php Fri Aug 08 14:19:56 2008 +0200
+@@ -0,0 +1,10 @@
++<?php
++/* Primary mail domain of your Kolab server */
++$conf['storage']['default_domain'] = 'example.com';
++
++/* Hostname of your IMAP server (usually the same as the hostname of the Kolab server) */
++$conf['reminder']['server_name'] = 'localhost';
++
++/* The email address of your sys admin */
++$conf['reminder']['from_addr'] = 'hostmaster@example.com';
++?>
+diff -r e16c159ea616 lib/Horde/Prefs/kolab.php
+--- a/lib/Horde/Prefs/kolab.php Fri Aug 08 13:11:07 2008 +0200
++++ b/lib/Horde/Prefs/kolab.php Fri Aug 08 14:19:56 2008 +0200
+@@ -1,5 +1,6 @@
+ <?php
+
++require_once 'Horde/Auth.php';
+ require_once 'Horde/Prefs/ldap.php';
+ require_once 'Horde/Kolab.php';
+
+@@ -42,7 +43,8 @@
+ 'searchpw' => $GLOBALS['conf']['kolab']['ldap']['phppw'],
+ 'uid' => 'mail');
+
+- parent::Prefs_ldap($user, $password, $scope, $params, $caching);
++ parent::Prefs_ldap(Auth::getAuth(), Auth::getCredential('password'),
++ $scope, $params, $caching);
+ }
+
+ }
+diff -r e16c159ea616 lib/Horde/iCalendar.php
+--- a/lib/Horde/iCalendar.php Fri Aug 08 13:11:07 2008 +0200
++++ b/lib/Horde/iCalendar.php Fri Aug 08 14:19:56 2008 +0200
+@@ -152,6 +152,41 @@
+ 'value' => $value,
+ 'values' => $values
+ );
++ }
++ }
++
++ /**
++ * Sets an attribute empty.
++ *
++ * @param string $name The name of the attribute.
++ * @param array $params Array containing any addition parameters for
++ * this attribute.
++ * @param boolean $append True to append the attribute, False to replace
++ * the first matching attribute found.
++ */
++ function setAttributeEmpty($name, $params = array(), $append = false)
++ {
++ switch ($name) {
++ case 'EXDATE':
++ case 'RDATE':
++ $this->setAttribute($name, array(), $params, $append, true);
++ break;
++ case 'COMPLETED':
++ case 'CREATED':
++ case 'DCREATED':
++ case 'LAST-MODIFIED':
++ case 'DTEND':
++ case 'DTSTART':
++ case 'DTSTAMP':
++ case 'DUE':
++ case 'AALARM':
++ case 'RECURRENCE-ID':
++ /* These values expect a date and there is no sensible
++ default so we better leave them alone for now. */
++ break;
++ default:
++ $this->setAttribute($name, '', $params, $append);
++ break;
+ }
+ }
+
+diff -r e16c159ea616 lib/SyncML/Backend.php
+--- a/lib/SyncML/Backend.php Fri Aug 08 13:11:07 2008 +0200
++++ b/lib/SyncML/Backend.php Fri Aug 08 14:19:56 2008 +0200
+@@ -574,6 +574,24 @@
+ }
+
+ /**
++ * Checks if the entry specified by $cuid has been modified on the server.
++ *
++ * @param string $databaseURI URI of Database to sync. Like
++ * calendar, tasks, contacts or notes.
++ * May include optional parameters:
++ * tasks?options=ignorecompleted.
++ * @param string $cuid Client ID of this entry
++ * @param integer $from_ts Start timestamp.
++ *
++ * @return boolean True if the client entry has been modified on
++ * the server. False otherwise.
++ */
++ function unchanged($databaseURI, $cuid, $from_ts)
++ {
++ die ("Not implemented!");
++ }
++
++ /**
+ * Authenticates the user at the backend.
+ *
+ * For some types of authentications (notably auth:basic) the username
+diff -r e16c159ea616 lib/SyncML/Backend/Horde.php
+--- a/lib/SyncML/Backend/Horde.php Fri Aug 08 13:11:07 2008 +0200
++++ b/lib/SyncML/Backend/Horde.php Fri Aug 08 14:19:56 2008 +0200
+@@ -202,6 +202,11 @@
+ __FILE__, __LINE__, PEAR_LOG_DEBUG);
+ continue;
+ }
++ if ($suid_ts > $to_ts) {
++ // Add delayed to the next sync operation
++ $this->logMessage("Add ignored, lies in sync future: $suid", __FILE__, __LINE__, PEAR_LOG_DEBUG);
++ continue;
++ }
+ $this->logMessage(
+ "Adding to client from db $database, server id $suid",
+ __FILE__, __LINE__, PEAR_LOG_DEBUG);
+@@ -250,6 +255,11 @@
+ // mirror that back to the client.
+ $this->logMessage("Changed on server after sent from client: $suid ignored",
+ __FILE__, __LINE__, PEAR_LOG_DEBUG);
++ continue;
++ }
++ if ($suid_ts > $to_ts) {
++ // Change delayed to the next sync operation
++ $this->logMessage("Change ignored, lies in sync future: $suid", __FILE__, __LINE__, PEAR_LOG_DEBUG);
+ continue;
+ }
+ $cuid = $this->_getCuid($database, $suid);
+@@ -311,6 +321,11 @@
+ $this->logMessage("Deleted on server after request from client: $suid ignored",
+ __FILE__, __LINE__, PEAR_LOG_DEBUG);
+ continue;
++ }
++ if ($suid_ts > $to_ts) {
++ // Delete delayed to the next sync operation
++ $this->logMessage("Delete ignored, lies in sync future: $suid", __FILE__, __LINE__, PEAR_LOG_DEBUG);
++ continue;
+ }
+ $cuid = $this->_getCuid($database, $suid);
+ if (!$cuid) {
+@@ -509,6 +524,71 @@
+ $this->createUidMap($database, $cuid, $suid, $ts);
+ }
+
++ return true;
++ }
++
++ /**
++ * Checks if the entry specified by $cuid has been modified on the server.
++ *
++ * @param string $databaseURI URI of Database to sync. Like
++ * calendar, tasks, contacts or notes.
++ * May include optional parameters:
++ * tasks?options=ignorecompleted.
++ * @param string $cuid Client ID of this entry
++ * @param integer $from_ts Last server sync time.
++ *
++ * @return boolean True if the client entry has been modified on
++ * the server. False otherwise.
++ */
++ function getConflict($databaseURI, $cuid, $server_ts)
++ {
++ global $registry;
++
++ $database = $this->_normalize($databaseURI);
++
++ // Only server needs to do a cuid<->suid map
++ if ($this->_backendMode == SYNCML_BACKENDMODE_SERVER) {
++ $suid = $this->_getSuid($database, $cuid);
++ } else {
++ $suid = $cuid;
++ }
++ $this->logMessage("checking modification date of entry suid $suid in $database", __FILE__, __LINE__, PEAR_LOG_DEBUG);
++
++ if ($suid) {
++ $mod_ts = $registry->call($database . '/getActionTimestamp', array($suid, 'modify', SyncML_Backend::getParameter($databaseURI,'source')));
++ $client_ts = $this->_getChangeTS($database, $suid);
++ // Check that the last server anchor is smaller than the
++ // last modification stamp (this means that a change
++ // occurred after the last sync started). This might still
++ // be a client change so check that the last client update
++ // also happened before the last modification)
++ if ($server_ts < $mod_ts && $client_ts < $mod_ts) {
++ // Duplicate the server entry
++ $this->logMessage("Conflict detected. Returning conflicting server entry $suid.", __FILE__, __LINE__, PEAR_LOG_DEBUG);
++ $device = &$_SESSION['SyncML.state']->getDevice();
++ $contentType = $device->getPreferredContentType($databaseURI);
++ return $this->retrieveEntry($databaseURI, $suid, $contentType);
++ }
++ }
++ return false;
++ }
++
++ function duplicateConflict($databaseURI, $content)
++ {
++ global $registry;
++
++ $database = $this->_normalize($databaseURI);
++ $device = &$_SESSION['SyncML.state']->getDevice();
++ $contentType = $device->getPreferredContentType($databaseURI);
++ $content = preg_replace('/(\r\n|\r|\n)UID:.*?(\r\n|\r|\n)/', '\1', $content, 1);
++ $duid = $registry->call($database. '/import',
++ array($content, $contentType,
++ SyncML_Backend::getParameter($databaseURI,'source')));
++ if (is_a($duid, 'PEAR_Error')) {
++ $this->logMessage("duplicating entry failed.", __FILE__, __LINE__, PEAR_LOG_DEBUG);
++ return false;
++ }
++ $this->logMessage("duplicated server entry to $duid.", __FILE__, __LINE__, PEAR_LOG_DEBUG);
+ return true;
+ }
+
+diff -r e16c159ea616 lib/SyncML/Constants.php
+--- a/lib/SyncML/Constants.php Fri Aug 08 13:11:07 2008 +0200
++++ b/lib/SyncML/Constants.php Fri Aug 08 14:19:56 2008 +0200
+@@ -64,7 +64,7 @@
+ define('RESPONSE_PARTIAL_CONTENT', 206);
+ define('RESPONSE_CONFLICT_RESOLVED_WITH_MERGE', 207);
+ define('RESPONSE_CONFLICT_RESOLVED_WITH_CLIENT_WINNING', 208);
+-define('RESPONSE_CONFILCT_RESOLVED_WITH_DUPLICATE', 209);
++define('RESPONSE_CONFLICT_RESOLVED_WITH_DUPLICATE', 209);
+ define('RESPONSE_DELETE_WITHOUT_ARCHIVE', 210);
+ define('RESPONSE_ITEM_NO_DELETED', 211);
+ define('RESPONSE_AUTHENTICATION_ACCEPTED', 212);
+diff -r e16c159ea616 lib/SyncML/Device.php
+--- a/lib/SyncML/Device.php Fri Aug 08 13:11:07 2008 +0200
++++ b/lib/SyncML/Device.php Fri Aug 08 14:19:56 2008 +0200
+@@ -161,12 +161,81 @@
+ SYNCML_LOGFILE_DATA,
+ "\nInput received from client ($contentType):\n$content\n");
+
++ $content = $this->_completeEmptyAttributes($content, $contentType);
++
+ // Always remove client UID. UID will be seperately passed in XML.
+ $content = preg_replace('/(\r\n|\r|\n)UID:.*?(\r\n|\r|\n)/',
+ '\1', $content, 1);
+
+ return array($content, $contentType);
+ }
++
++ /**
++ * Complete any attributes that the client supports but did not
++ * provide. In case the user did delete an attribute on the client
++ * this action is required to indicate to the server that the
++ * attribute needs to be deleted. A completely missing attribute
++ * would be considered as "no change" by the server.
++ *
++ * @param string $content The content to convert
++ * @param string $contentType The contentType of the content
++ *
++ * @return string The converted content
++ */
++ function _completeEmptyAttributes($content, $contentType)
++ {
++
++ $di = $_SESSION['SyncML.state']->deviceInfo;
++
++ if (empty($di) || !isset($di->_CTCap) || !isset($di->_CTCap[$contentType])) {
++ return $content;
++ }
++
++ require_once 'Horde/iCalendar.php';
++ $iCal = new Horde_iCalendar();
++ if (!$iCal->parsevCalendar($content)) {
++ // We cant parse the content, return it unchanged
++ return $content;
++ }
++ $components = $iCal->getComponents();
++ $attributes = $di->_CTCap[$contentType];
++
++ foreach ($components as $component) {
++ foreach ($attributes as $name => $properties) {
++ if ($name == 'BEGIN' || $name == 'END') {
++ continue;
++ }
++ $values = $component->getAllAttributes($name);
++ if (!isset($properties->_params)) {
++ if (empty($values)) {
++ // Undefined attribute -> replace it with
++ // the correct empty value
++ $component->setAttributeEmpty($name);
++ }
++ } else {
++ foreach ($properties->_params as $key => $property) {
++ if (!empty($values)) {
++ $present = true;
++ } else {
++ $present = false;
++ foreach ($values as $value) {
++ if (in_array($key, array_keys($value['params']))) {
++ $present = true;
++ break;
++ }
++ }
++ }
++ if (!$present) {
++ // Undefined attribute -> replace it with
++ // the correct empty value
++ $component->setAttributeEmpty($name, array($key => null), true);
++ }
++ }
++ }
++ }
++ }
++ return $iCal->exportvCalendar();
++ }
+
+ /**
+ * Converts the content from the backend to a format suitable for the
+diff -r e16c159ea616 lib/SyncML/Sync.php
+--- a/lib/SyncML/Sync.php Fri Aug 08 13:11:07 2008 +0200
++++ b/lib/SyncML/Sync.php Fri Aug 08 14:19:56 2008 +0200
+@@ -214,7 +214,13 @@
+ }
+ } elseif ($item->elementType =='Delete') {
+ /* Handle client delete requests. */
++ $duplicate = $backend->getConflict($hordedatabase, $cuid, $this->_serverAnchorLast);
+ $ok = $backend->deleteEntry($database, $cuid);
++ $duplicated = false;
++ if ($duplicate) {
++ $duplicated = $backend->duplicateConflict($hordedatabase, $duplicate);
++ }
++
+ if (!$ok && $tasksincalendar) {
+ $backend->logMessage(
+ 'Task ' . $cuid . ' deletion sent with calendar request',
+@@ -224,8 +230,14 @@
+
+ if ($ok) {
+ $this->_client_delete_count++;
+- $item->responseCode = RESPONSE_OK;
+ $backend->logMessage('Deleted entry ' . $suid . ' due to client request', __FILE__, __LINE__, PEAR_LOG_DEBUG);
++ if (!$duplicated) {
++ $item->responseCode = RESPONSE_OK;
++ } else {
++ $this->_client_add_count++;
++ $backend->logMessage('Duplicated entry ' . $suid . ' due to client/server conflict', __FILE__, __LINE__, PEAR_LOG_INFO);
++ $item->responseCode = RESPONSE_CONFLICT_RESOLVED_WITH_DUPLICATE;
++ }
+ } else {
+ $this->_errors++;
+ $item->responseCode = RESPONSE_ITEM_NO_DELETED;
+@@ -234,13 +246,24 @@
+
+ } elseif ($item->elementType == 'Replace') {
+ /* Handle client replace requests. */
++ $duplicate = $backend->getConflict($hordedatabase, $cuid, $this->_serverAnchorLast);
+ $suid = $backend->replaceEntry($hordedatabase, $content,
+ $contentType, $cuid);
++ $duplicated = false;
++ if ($duplicate) {
++ $duplicated = $backend->duplicateConflict($hordedatabase, $duplicate);
++ }
+
+ if (!is_a($suid, 'PEAR_Error')) {
+ $this->_client_replace_count++;
+- $item->responseCode = RESPONSE_OK;
+ $backend->logMessage('Replaced entry ' . $suid . ' due to client request', __FILE__, __LINE__, PEAR_LOG_DEBUG);
++ if (!$duplicated) {
++ $item->responseCode = RESPONSE_OK;
++ } else {
++ $this->_client_add_count++;
++ $backend->logMessage('Duplicated entry ' . $suid . ' due to client/server conflict', __FILE__, __LINE__, PEAR_LOG_INFO);
++ $item->responseCode = RESPONSE_CONFLICT_RESOLVED_WITH_DUPLICATE;
++ }
+ } else {
+ $backend->logMessage($suid->message, __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+diff -r e16c159ea616 lib/core.php
+--- a/lib/core.php Fri Aug 08 13:11:07 2008 +0200
++++ b/lib/core.php Fri Aug 08 14:19:56 2008 +0200
+@@ -31,6 +31,12 @@
+ * include_path, you must add an ini_set() call here to add their location to
+ * the include_path. */
+ ini_set('include_path', dirname(__FILE__) . PATH_SEPARATOR . dirname(__FILE__) . '/../pear');
++ini_set('log_errors', true);
++ini_set('display_errors', '0');
++ini_set('error_log', dirname(__FILE__) . '/../log/php-errors.log');
++session_save_path(dirname(__FILE__) . '/../tmp/');
++ini_set('upload_tmp_dir', dirname(__FILE__) . '/../tmp/');
++putenv('TMPDIR=' . dirname(__FILE__) . '/../tmp/');
+
+ /* PEAR base class. */
+ include_once 'PEAR.php';
+diff -r e16c159ea616 log/.htaccess
+--- /dev/null Thu Jan 01 00:00:00 1970 +0000
++++ b/log/.htaccess Fri Aug 08 14:19:56 2008 +0200
+@@ -0,0 +1,1 @@
++Deny from All
+\ No newline at end of file
+diff -r e16c159ea616 mnemo/config/conf.php
+--- a/mnemo/config/conf.php Fri Aug 08 13:11:07 2008 +0200
++++ b/mnemo/config/conf.php Fri Aug 08 14:19:56 2008 +0200
+@@ -1,9 +1,8 @@
+ <?php
+ /* CONFIG START. DO NOT CHANGE ANYTHING IN OR AFTER THIS LINE. */
+ // $Horde: mnemo/config/conf.xml,v 1.17.10.1 2007/12/20 14:17:38 jan Exp $
+-$conf['storage']['params']['table'] = 'mnemo_memos';
+-$conf['storage']['params']['driverconfig'] = 'horde';
+-$conf['storage']['driver'] = 'sql';
++$conf['storage']['driver'] = 'kolab';
++$conf['utils']['gnupg'] = '/usr/bin/gpg';
+ $conf['menu']['print'] = true;
+ $conf['menu']['import_export'] = true;
+ $conf['menu']['apps'] = array();
+diff -r e16c159ea616 nag/config/conf.php
+--- a/nag/config/conf.php Fri Aug 08 13:11:07 2008 +0200
++++ b/nag/config/conf.php Fri Aug 08 14:19:56 2008 +0200
+@@ -1,9 +1,7 @@
+ <?php
+ /* CONFIG START. DO NOT CHANGE ANYTHING IN OR AFTER THIS LINE. */
+ // $Horde: nag/config/conf.xml,v 1.25.10.2 2007/12/20 14:23:06 jan Exp $
+-$conf['storage']['params']['table'] = 'nag_tasks';
+-$conf['storage']['params']['driverconfig'] = 'horde';
+-$conf['storage']['driver'] = 'sql';
++$conf['storage']['driver'] = 'kolab';
+ $conf['menu']['print'] = true;
+ $conf['menu']['import_export'] = true;
+ $conf['menu']['apps'] = array();
+diff -r e16c159ea616 nag/lib/Block/tree_menu.php
+--- a/nag/lib/Block/tree_menu.php Fri Aug 08 13:11:07 2008 +0200
++++ b/nag/lib/Block/tree_menu.php Fri Aug 08 14:19:56 2008 +0200
+@@ -39,6 +39,15 @@
+ array('icon' => 'add.png',
+ 'icondir' => $icondir,
+ 'url' => Util::addParameter($add, array('tasklist_id' => $name))));
++
++ $tree->addNode($parent . $name,
++ $parent,
++ $tasklist->get('name'),
++ $indent + 1,
++ false,
++ array('icon' => 'nag.png',
++ 'icondir' => $icondir,
++ 'url' => Util::addParameter(Horde::applicationUrl('list.php'), array('tasklist_id' => $name))));
+ }
+
+ $tree->addNode($parent . '__search',
+diff -r e16c159ea616 nag/lib/Driver.php
+--- a/nag/lib/Driver.php Fri Aug 08 13:11:07 2008 +0200
++++ b/nag/lib/Driver.php Fri Aug 08 14:19:56 2008 +0200
+@@ -208,13 +208,14 @@
+ * @param boolean $private Whether the task is private.
+ * @param string $owner The owner of the event.
+ * @param string $assignee The assignee of the event.
++ * @param Nag_Recurrence $recurrence Task recurrence.
+ *
+ * @return array array(ID,UID) of new task
+ */
+ function add($name, $desc, $start = 0, $due = 0, $priority = 0,
+ $estimate = 0.0, $completed = 0, $category = '', $alarm = 0,
+ $uid = null, $parent = '', $private = false, $owner = null,
+- $assignee = null)
++ $assignee = null, $recurrence = null)
+ {
+ if (is_null($uid)) {
+ $uid = $this->generateUID();
+@@ -225,7 +226,7 @@
+
+ $taskId = $this->_add($name, $desc, $start, $due, $priority, $estimate,
+ $completed, $category, $alarm, $uid, $parent,
+- $private, $owner, $assignee);
++ $private, $owner, $assignee, $recurrence);
+ if (is_a($taskId, 'PEAR_Error')) {
+ return $taskId;
+ }
+@@ -273,11 +274,13 @@
+ * @param string $owner The owner of the event.
+ * @param string $assignee The assignee of the event.
+ * @param integer $completed_date The task's completion date.
++ * @param Nag_Recurrence $recurrence Task recurrence.
+ */
+ function modify($taskId, $name, $desc, $start = 0, $due = 0, $priority = 0,
+ $estimate = 0.0, $completed = 0, $category = '',
+ $alarm = 0, $parent = '', $private = false,
+- $owner = null, $assignee = null, $completed_date = null)
++ $owner = null, $assignee = null, $completed_date = null,
++ $recurrence = null)
+ {
+ /* Retrieve unmodified task. */
+ $task = $this->get($taskId);
+@@ -293,7 +296,7 @@
+ $modify = $this->_modify($taskId, $name, $desc, $start, $due,
+ $priority, $estimate, $completed, $category,
+ $alarm, $parent, $private, $owner, $assignee,
+- $completed_date);
++ $completed_date, $recurrence);
+ if (is_a($modify, 'PEAR_Error')) {
+ return $modify;
+ }
+@@ -546,6 +549,13 @@
+ var $private;
+
+ /**
++ * Recurrence information for a periodic task
++ *
++ * @var Nag_Recurrence
++ */
++ var $recurrence;
++
++ /**
+ * URL to view the task.
+ *
+ * @var string
+@@ -659,6 +669,11 @@
+ $key = 'id';
+ } elseif ($key == 'parent') {
+ $key = 'parent_id';
++ } elseif ($key == 'recurrence' && is_array($val)) {
++ require_once NAG_BASE . '/lib/Recurrence.php';
++ $recurrence = new Nag_Recurrence($task['due']);
++ $recurrence->fromHash($val);
++ $val = $recurrence;
+ }
+ $this->$key = $val;
+ }
+@@ -684,7 +699,8 @@
+ $this->private,
+ $this->owner,
+ $this->assignee,
+- $this->completed_date);
++ $this->completed_date,
++ $this->recurrence);
+ }
+
+ /**
+@@ -1012,6 +1028,12 @@
+ */
+ function toHash()
+ {
++ if ($this->recurs()) {
++ $recurrence = $this->recurrence->toHash();
++ } else {
++ $recurrence = null;
++ }
++
+ return array('tasklist_id' => $this->tasklist,
+ 'task_id' => $this->id,
+ 'uid' => $this->uid,
+@@ -1028,7 +1050,8 @@
+ 'completed' => $this->completed,
+ 'completed_date' => $this->completed_date,
+ 'alarm' => $this->alarm,
+- 'private' => $this->private);
++ 'private' => $this->private,
++ 'recurrence' => $recurrence);
+ }
+
+ /**
+@@ -1299,4 +1322,54 @@
+ }
+ }
+
++ /**
++ * Returns whether this task is a recurring task.
++ *
++ * @return boolean True if this is a recurring task.
++ */
++ function recurs()
++ {
++ return isset($this->recurrence) &&
++ !$this->recurrence->hasRecurType(NAG_RECUR_NONE);
++ }
++
++ /**
++ * Toggles completion status of this task. Moves a recurring task
++ * to the next occurence on completion.
++ */
++ function toggleComplete()
++ {
++ if ($this->completed) {
++ $this->completed_date = null;
++ $this->completed = false;
++ return;
++ }
++
++ if ($this->recurs()) {
++ /* Get current occurrence (task due date) */
++ $current = new Horde_Date($this->due);
++ /* Advance this occurence by a day to indicate that we want the
++ * following occurence (Recurrence uses days as minimal time
++ * duration between occurrences). */
++ $current->mday++;
++ /* Get the next occurence. */
++ $next = $this->recurrence->nextActiveRecurrence($current);
++ if ($next) {
++ if (!empty($this->start)) {
++ /* The task has a delayed start that should be moved forward
++ * by the same timespan.
++ */
++ $this->start += $next->timestamp() - $this->due;
++ }
++ $this->due = $next->timestamp();
++ $this->completed = false;
++ $this->recurrence->addCompletion($next->year, $next->month,
++ $next->mday);
++ return;
++ }
++ }
++
++ $this->completed_date = time();
++ $this->completed = true;
++ }
+ }
+diff -r e16c159ea616 nag/lib/Driver/kolab.php
+--- a/nag/lib/Driver/kolab.php Fri Aug 08 13:11:07 2008 +0200
++++ b/nag/lib/Driver/kolab.php Fri Aug 08 14:19:56 2008 +0200
+@@ -114,17 +114,19 @@
+ * @param boolean $private Whether the task is private.
+ * @param string $owner The owner of the event.
+ * @param string $assignee The assignee of the event.
++ * @param Nag_Recurrence $recurrence Task recurrence.
+ *
+ * @return string The Nag ID of the new task.
+ */
+ function _add($name, $desc, $start = 0, $due = 0, $priority = 0,
+ $completed = 0, $estimate = 0.0, $category = '', $alarm = 0,
+ $uid = null, $parent = null, $private = false, $owner = null,
+- $assignee = null)
++ $assignee = null, $recurrence = null)
+ {
+ return $this->_wrapper->add($name, $desc, $start, $due, $priority,
+ $completed, $estimate, $category, $alarm,
+- $uid, $parent, $private, $owner, $assignee);
++ $uid, $parent, $private, $owner, $assignee,
++ $recurrence);
+ }
+
+ /**
+@@ -145,18 +147,21 @@
+ * @param string $owner The owner of the event.
+ * @param string $assignee The assignee of the event.
+ * @param integer $completed_date The task's completion date.
++ * @param Nag_Recurrence $recurrence Task recurrence.
+ *
+ * @return boolean Indicates if the modification was successfull.
+ */
+ function _modify($taskId, $name, $desc, $start = 0, $due = 0, $priority = 0,
+ $estimate = 0.0, $completed = 0, $category = '',
+ $alarm = 0, $parent = null, $private = false,
+- $owner = null, $assignee = null, $completed_date = null)
++ $owner = null, $assignee = null, $completed_date = null,
++ $recurrence = null)
+ {
+ return $this->_wrapper->modify($taskId, $name, $desc, $start, $due,
+ $priority, $estimate, $completed,
+ $category, $alarm, $parent, $private,
+- $owner, $assignee, $completed_date);
++ $owner, $assignee, $completed_date,
++ $recurrence);
+ }
+
+ /**
+@@ -379,13 +384,15 @@
+ * @param string $owner The owner of the event.
+ * @param string $assignee The assignee of the event.
+ * @param integer $completed_date The task's completion date.
++ * @param Nag_Recurrence $recurrence Task recurrence.
+ *
+ * @return string The ID of the task.
+ */
+ function _setObject($name, $desc, $start = 0, $due = 0, $priority = 0,
+ $estimate = 0.0, $completed = 0, $category = '',
+ $alarm = 0, $parent = null, $private = false,
+- $owner = null, $assignee = null, $completed_date = null)
++ $owner = null, $assignee = null, $completed_date = null,
++ $recurrence = null)
+ {
+ if ($due == 0) {
+ $alarm = 0;
+@@ -432,13 +439,14 @@
+ * @param boolean $private Whether the task is private.
+ * @param string $owner The owner of the event.
+ * @param string $assignee The assignee of the event.
++ * @param Nag_Recurrence $recurrence Task recurrence.
+ *
+ * @return string The Nag ID of the new task.
+ */
+ function add($name, $desc, $start = 0, $due = 0, $priority = 0,
+ $completed = 0, $estimate = 0.0, $category = '', $alarm = 0,
+ $uid = null, $parent = null, $private = false, $owner = null,
+- $assignee = null)
++ $assignee = null, $recurrence = null)
+ {
+ // Usually provided by the generic Driver class
+ if ($uid !== null) {
+@@ -453,7 +461,8 @@
+
+ return $this->_setObject($name, $desc, $start, $due, $priority,
+ $completed, $estimate, $category, $alarm,
+- $parent, $private, $owner, $assignee);
++ $parent, $private, $owner, $assignee,
++ $recurrence);
+ }
+
+ /**
+@@ -474,6 +483,7 @@
+ * @param string $owner The owner of the event.
+ * @param string $assignee The assignee of the event.
+ * @param integer $completed_date The task's completion date.
++ * @param Nag_Recurrence $recurrence Task recurrence.
+ *
+ * @return boolean Indicates if the modification was successfull.
+ */
+@@ -481,7 +491,7 @@
+ $priority = 0, $estimate = 0.0, $completed = 0,
+ $category = '', $alarm = 0, $parent = null,
+ $private = false, $owner = null, $assignee = null,
+- $completed_date = null)
++ $completed_date = null, $recurrence = null)
+ {
+ // Load the object into the kolab driver
+ $result = $this->_kolab->loadObject($taskId);
+@@ -492,7 +502,7 @@
+ $result = $this->_setObject($name, $desc, $start, $due, $priority,
+ $estimate, $completed, $category, $alarm,
+ $parent, $private, $owner, $assignee,
+- $completed_date);
++ $completed_date, $recurrence);
+ if (is_a($result, 'PEAR_Error')) {
+ return $result;
+ }
+@@ -780,6 +790,7 @@
+ * @param string $owner The owner of the event.
+ * @param string $assignee The assignee of the event.
+ * @param integer $completed_date The task's completion date.
++ * @param Nag_Recurrence $recurrence Task recurrence.
+ *
+ * @return mixed The id of the task if successful, a PEAR error
+ * otherwise
+@@ -788,7 +799,7 @@
+ $estimate = 0.0, $completed = 0, $category = '',
+ $alarm = 0, $uid = null, $parent = null,
+ $private = false, $owner = null, $assignee = null,
+- $completed_date = null)
++ $completed_date = null, $recurrence = null)
+ {
+ if (empty($uid)) {
+ $task_uid = $this->_store->generateUID();
+@@ -808,6 +819,10 @@
+ $sensitivity = 'public';
+ }
+
++ if ($recurrence) {
++ $recurrence = $recurrence->toHash();
++ }
++
+ $result = $this->_store->save(array(
+ 'uid' => $task_uid,
+ 'name' => $name,
+@@ -817,6 +832,7 @@
+ 'priority' => $priority,
+ 'completed' => $completed,
+ 'categories' => $category,
++ 'recurrence' => $recurrence,
+ 'alarm' => $alarm,
+ 'parent' => $parent,
+ 'sensitivity' => $sensitivity,
+@@ -854,6 +870,7 @@
+ * @param boolean $private Whether the task is private.
+ * @param string $owner The owner of the event.
+ * @param string $assignee The assignee of the event.
++ * @param Nag_Recurrence $recurrence Task recurrence.
+ *
+ * @return mixed The id of the task if successful, a PEAR error
+ * otherwise
+@@ -861,11 +878,12 @@
+ function add($name, $desc, $start = 0, $due = 0, $priority = 0,
+ $estimate = 0.0, $completed = 0, $category = '', $alarm = 0,
+ $uid = null, $parent = null, $private = false, $owner = null,
+- $assignee = null)
++ $assignee = null, $recurrence = null)
+ {
+ return $this->_setObject($name, $desc, $start, $due, $priority,
+ $estimate, $completed, $category, $alarm,
+- null, $parent, $private, $owner, $assignee);
++ null, $parent, $private, $owner, $assignee,
++ null, $recurrence);
+ }
+
+ /**
+@@ -886,6 +904,7 @@
+ * @param string $owner The owner of the event.
+ * @param string $assignee The assignee of the event.
+ * @param integer $completed_date The task's completion date.
++ * @param Nag_Recurrence $recurrence Task recurrence.
+ *
+ * @return mixed The id of the task if successful, a PEAR error
+ * otherwise
+@@ -893,12 +912,13 @@
+ function modify($taskId, $name, $desc, $start = 0, $due = 0, $priority = 0,
+ $estimate = 0.0, $completed = 0, $category = '',
+ $alarm = 0, $parent = null, $private = false,
+- $owner = null, $assignee = null, $completed_date = null)
++ $owner = null, $assignee = null, $completed_date = null,
++ $recurrence = null)
+ {
+ $result = $this->_setObject($name, $desc, $start, $due, $priority,
+ $estimate, $completed, $category, $alarm,
+ $taskId, $parent, $private, $owner, $assignee,
+- $completed_date);
++ $completed_date, $recurrence);
+ if (is_a($result, 'PEAR_Error')) {
+ return $result;
+ }
+@@ -1033,6 +1053,13 @@
+ }
+ unset($task['sensitivity']);
+
++ if (isset($task['recurrence']) && isset($task['due'])) {
++ require_once NAG_BASE . '/lib/Recurrence.php';
++ $recurrence = new Nag_Recurrence($task['due']);
++ $recurrence->fromHash($task['recurrence']);
++ $task['recurrence'] = $recurrence;
++ }
++
+ $share = &$GLOBALS['nag_shares']->getShare($this->_tasklist);
+ $task['owner'] = $share->get('owner');
+
+diff -r e16c159ea616 nag/lib/Driver/sql.php
+--- a/nag/lib/Driver/sql.php Fri Aug 08 13:11:07 2008 +0200
++++ b/nag/lib/Driver/sql.php Fri Aug 08 14:19:56 2008 +0200
+@@ -158,13 +158,14 @@
+ * @param boolean $private Whether the task is private.
+ * @param string $owner The owner of the event.
+ * @param string $assignee The assignee of the event.
++ * @param Nag_Recurrence $recurrence Task recurrence.
+ *
+ * @return string The Nag ID of the new task.
+ */
+ function _add($name, $desc, $start = 0, $due = 0, $priority = 0,
+ $estimate = 0.0, $completed = 0, $category = '', $alarm = 0,
+ $uid = null, $parent = '', $private = false, $owner = null,
+- $assignee = null)
++ $assignee = null, $recurrence = null)
+ {
+ $taskId = md5(uniqid(mt_rand(), true));
+ if ($uid === null) {
+@@ -229,12 +230,13 @@
+ * @param string $owner The owner of the event.
+ * @param string $assignee The assignee of the event.
+ * @param integer $completed_date The task's completion date.
++ * @param Nag_Recurrence $recurrence Task recurrence.
+ */
+ function _modify($taskId, $name, $desc, $start = 0, $due = 0,
+ $priority = 0, $estimate = 0.0, $completed = 0,
+ $category = '', $alarm = 0, $parent = '',
+ $private = false, $owner = null, $assignee = null,
+- $completed_date = null)
++ $completed_date = null, $recurrence = null)
+ {
+ $query = sprintf('UPDATE %s SET' .
+ ' task_creator = ?, ' .
+diff -r e16c159ea616 nag/lib/Forms/task.php
+--- a/nag/lib/Forms/task.php Fri Aug 08 13:11:07 2008 +0200
++++ b/nag/lib/Forms/task.php Fri Aug 08 14:19:56 2008 +0200
+@@ -87,6 +87,7 @@
+
+ $this->addVariable(_("Private?"), 'private', 'boolean', false);
+ $this->addVariable(_("Due By"), 'due', 'nag_due', false);
++
+ $this->addVariable(_("Delay Start Until"), 'start', 'nag_start', false);
+ $this->addVariable(_("Alarm"), 'alarm', 'nag_alarm', false);
+
+@@ -96,6 +97,9 @@
+ $this->addVariable(_("Estimated Time"), 'estimate', 'number', false);
+ $this->addVariable(_("Completed?"), 'completed', 'boolean', false);
+ $this->addVariable(_("Description"), 'desc', 'longtext', false);
++
++ $this->addVariable(_("Recurrence"), 'recurrence', 'nag_recurrence', false, false, false);
++ $this->addVariable(_("Recur Until"), 'recur_end', 'nag_recur_end', false, false, false);
+
+ $buttons = array(_("Save"));
+ if ($delete) {
+@@ -214,6 +218,150 @@
+ }
+
+ /**
++ * The Horde_Form_Type_nag_recurrence class provides a form field for editing
++ * task recurrences.
++ *
++ * @author Gunnar Wrobel <p@rdus.de>
++ * @since Nag 2.2
++ * @package Nag
++ */
++class Horde_Form_Type_nag_recurrence extends Horde_Form_Type {
++
++ function getInfo(&$vars, &$var, &$info)
++ {
++ $recurrence = $vars->get('recurrence');
++ if ($recurrence) {
++ $info = $recurrence;
++ return;
++ }
++
++ // Recurrence.
++ $recur = $vars->get('recur');
++ if ($recur !== null && $recur !== '') {
++ require_once NAG_BASE . '/lib/Recurrence.php';
++ $recurrence = new Nag_Recurrence(time());
++
++ $recurrence->setRecurType($recur);
++ switch ($recur) {
++ case NAG_RECUR_DAILY:
++ $recurrence->setRecurInterval($vars->get('recur_daily_interval', 1));
++ break;
++
++ case NAG_RECUR_WEEKLY:
++ $weekly = $vars->get('weekly');
++ $weekdays = 0;
++ if (is_array($weekly)) {
++ foreach ($weekly as $day) {
++ $weekdays |= $day;
++ }
++ }
++
++ if ($weekdays == 0) {
++ // Sunday starts at 0.
++ switch ($this->start->dayOfWeek()) {
++ case 0: $weekdays |= HORDE_DATE_MASK_SUNDAY; break;
++ case 1: $weekdays |= HORDE_DATE_MASK_MONDAY; break;
++ case 2: $weekdays |= HORDE_DATE_MASK_TUESDAY; break;
++ case 3: $weekdays |= HORDE_DATE_MASK_WEDNESDAY; break;
++ case 4: $weekdays |= HORDE_DATE_MASK_THURSDAY; break;
++ case 5: $weekdays |= HORDE_DATE_MASK_FRIDAY; break;
++ case 6: $weekdays |= HORDE_DATE_MASK_SATURDAY; break;
++ }
++ }
++
++ $recurrence->setRecurInterval($vars->get('recur_weekly_interval', 1));
++ $recurrence->setRecurOnDay($weekdays);
++ break;
++
++ case NAG_RECUR_MONTHLY_DATE:
++ $recurrence->setRecurInterval($vars->get('recur_day_of_month_interval', 1));
++ break;
++
++ case NAG_RECUR_MONTHLY_WEEKDAY:
++ $recurrence->setRecurInterval($vars->get('recur_week_of_month_interval', 1));
++ break;
++
++ case NAG_RECUR_YEARLY_DATE:
++ $recurrence->setRecurInterval($vars->get('recur_yearly_interval', 1));
++ break;
++
++ case NAG_RECUR_YEARLY_DAY:
++ $recurrence->setRecurInterval($vars->get('recur_yearly_day_interval', 1));
++ break;
++
++ case NAG_RECUR_YEARLY_WEEKDAY:
++ $recurrence->setRecurInterval($vars->get('recur_yearly_weekday_interval', 1));
++ break;
++ }
++ $info = $recurrence->toHash();
++ } else {
++ $info = array();
++ }
++ }
++
++ function isValid(&$var, &$vars, $value, &$message)
++ {
++ return true;
++ }
++
++}
++
++/**
++ * The Horde_Form_Type_nag_recur_end class provides a form field for editing
++ * task recurrence ends.
++ *
++ * @author Gunnar Wrobel <p@rdus.de>
++ * @since Nag 2.2
++ * @package Nag
++ */
++class Horde_Form_Type_nag_recur_end extends Horde_Form_Type {
++
++ function getInfo(&$vars, &$var, &$info)
++ {
++ $r = $vars->get('recurrence');
++ if ($r) {
++ $info = array('range' => $r['range'],
++ 'range-type' => $r['range-type']);
++ return;
++ }
++
++ // Recurrence.
++ $recur = $vars->get('recur');
++ if ($recur !== null && $recur !== '') {
++ require_once NAG_BASE . '/lib/Recurrence.php';
++ $recurrence = new Nag_Recurrence(time());
++ $recurrence->setRecurType(NAG_RECUR_DAILY);
++ if ($vars->get('recur_enddate_type') == 'date') {
++ $recur_enddate = $vars->get('recur_enddate');
++ $recurrence->setRecurEnd(new Horde_Date(
++ array('hour' => 1,
++ 'min' => 1,
++ 'sec' => 1,
++ 'month' => $recur_enddate['month'],
++ 'mday' => $recur_enddate['day'],
++ 'year' => $recur_enddate['year'])));
++ } elseif ($vars->get('recur_enddate_type') == 'count') {
++ $recurrence->setRecurCount($vars->get('recur_count'));
++ } elseif ($vars->get('recur_enddate_type') == 'none') {
++ $recurrence->setRecurCount(0);
++ $recurrence->setRecurEnd(null);
++ }
++ $r = $recurrence->toHash();
++ $info = array('range' => $r['range'],
++ 'range-type' => $r['range-type']);
++ } else {
++ $info = array();
++ }
++ }
++
++ function isValid(&$var, &$vars, $value, &$message)
++ {
++ return true;
++ }
++
++}
++
++/**
+ * The Horde_Form_Type_nag_start class provides a form field for editing
+ * task delayed start dates.
+ *
+diff -r e16c159ea616 nag/lib/Recurrence.php
+--- /dev/null Thu Jan 01 00:00:00 1970 +0000
++++ b/nag/lib/Recurrence.php Fri Aug 08 14:19:56 2008 +0200
+@@ -0,0 +1,1469 @@
++<?php
++/**
++ * This file contains the Nag_Recurrence class and according constants.
++ *
++ * $Horde: nag/lib/Recurrence.php,v 1.1 2008/07/13 12:36:03 jan Exp $
++ *
++ * Copyright 2007-2008 The Horde Project (http://www.horde.org/)
++ *
++ * See the enclosed file COPYING for license information (LGPL). If you
++ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
++ *
++ * @since Horde 3.2
++ * @package Horde_Date
++ */
++
++/** Horde_Date */
++require_once 'Horde/Date.php';
++
++/** Date_Calc */
++require_once 'Date/Calc.php';
++
++/** No recurrence. */
++define('NAG_RECUR_NONE', 0);
++/** Recurs daily. */
++define('NAG_RECUR_DAILY', 1);
++/** Recurs weekly. */
++define('NAG_RECUR_WEEKLY', 2);
++/** Recurs monthly on the same date. */
++define('NAG_RECUR_MONTHLY_DATE', 3);
++/** Recurs monthly on the same week day. */
++define('NAG_RECUR_MONTHLY_WEEKDAY', 4);
++/** Recurs yearly on the same date. */
++define('NAG_RECUR_YEARLY_DATE', 5);
++/** Recurs yearly on the same day of the year. */
++define('NAG_RECUR_YEARLY_DAY', 6);
++/** Recurs yearly on the same week day. */
++define('NAG_RECUR_YEARLY_WEEKDAY', 7);
++
++/**
++ * The Nag_Recurrence class implements algorithms for calculating
++ * recurrences of events, including several recurrence types, intervals,
++ * exceptions, and conversion from and to vCalendar and iCalendar recurrence
++ * rules.
++ *
++ * All methods expecting dates as parameters accept all values that the
++ * Horde_Date constructor accepts, i.e. a timestamp, another Horde_Date
++ * object, an ISO time string or a hash.
++ *
++ * @author Jan Schneider <jan@horde.org>
++ * @since Horde 3.2
++ * @package Horde_Date
++ */
++class Nag_Recurrence {
++
++ /**
++ * The start time of the event.
++ *
++ * @var Horde_Date
++ */
++ var $start;
++
++ /**
++ * The end date of the recurrence interval.
++ *
++ * @var Horde_Date
++ */
++ var $recurEnd = null;
++
++ /**
++ * The number of recurrences.
++ *
++ * @var integer
++ */
++ var $recurCount = null;
++
++ /**
++ * The type of recurrence this event follows. NAG_RECUR_* constant.
++ *
++ * @var integer
++ */
++ var $recurType = NAG_RECUR_NONE;
++
++ /**
++ * The length of time between recurrences. The time unit depends on the
++ * recurrence type.
++ *
++ * @var integer
++ */
++ var $recurInterval = 1;
++
++ /**
++ * Any additional recurrence data.
++ *
++ * @var integer
++ */
++ var $recurData = null;
++
++ /**
++ * All the exceptions from recurrence for this event.
++ *
++ * @var array
++ */
++ var $exceptions = array();
++
++ /**
++ * All the dates this recurrence has been marked as completed.
++ *
++ * @var array
++ */
++ var $completions = array();
++
++ /**
++ * Constructor.
++ *
++ * @param Horde_Date $start Start of the recurring event.
++ */
++ function Nag_Recurrence($start)
++ {
++ $this->start = new Horde_Date($start);
++ }
++
++ /**
++ * Checks if this event recurs on a given day of the week.
++ *
++ * @param integer $dayMask A mask consisting of HORDE_DATE_MASK_*
++ * constants specifying the day(s) to check.
++ *
++ * @return boolean True if this event recurs on the given day(s).
++ */
++ function recurOnDay($dayMask)
++ {
++ return ($this->recurData & $dayMask);
++ }
++
++ /**
++ * Specifies the days this event recurs on.
++ *
++ * @param integer $dayMask A mask consisting of HORDE_DATE_MASK_*
++ * constants specifying the day(s) to recur on.
++ */
++ function setRecurOnDay($dayMask)
++ {
++ $this->recurData = $dayMask;
++ }
++
++ /**
++ * Returns the days this event recurs on.
++ *
++ * @return integer A mask consisting of HORDE_DATE_MASK_* constants
++ * specifying the day(s) this event recurs on.
++ */
++ function getRecurOnDays()
++ {
++ return $this->recurData;
++ }
++
++ /**
++ * Returns whether this event has a specific recurrence type.
++ *
++ * @param integer $recurrence NAG_RECUR_* constant of the
++ * recurrence type to check for.
++ *
++ * @return boolean True if the event has the specified recurrence type.
++ */
++ function hasRecurType($recurrence)
++ {
++ return ($recurrence == $this->recurType);
++ }
++
++ /**
++ * Sets a recurrence type for this event.
++ *
++ * @param integer $recurrence A NAG_RECUR_* constant.
++ */
++ function setRecurType($recurrence)
++ {
++ $this->recurType = $recurrence;
++ }
++
++ /**
++ * Returns recurrence type of this event.
++ *
++ * @return integer A NAG_RECUR_* constant.
++ */
++ function getRecurType()
++ {
++ return $this->recurType;
++ }
++
++ /**
++ * Returns a description of this event's recurring type.
++ *
++ * @return string Human readable recurring type.
++ */
++ function getRecurName()
++ {
++ switch ($this->getRecurType()) {
++ case NAG_RECUR_NONE: return _("No recurrence");
++ case NAG_RECUR_DAILY: return _("Daily");
++ case NAG_RECUR_WEEKLY: return _("Weekly");
++ case NAG_RECUR_MONTHLY_DATE:
++ case NAG_RECUR_MONTHLY_WEEKDAY: return _("Monthly");
++ case NAG_RECUR_YEARLY_DATE:
++ case NAG_RECUR_YEARLY_DAY:
++ case NAG_RECUR_YEARLY_WEEKDAY: return _("Yearly");
++ }
++ }
++
++ /**
++ * Sets the length of time between recurrences of this event.
++ *
++ * @param integer $interval The time between recurrences.
++ */
++ function setRecurInterval($interval)
++ {
++ if ($interval > 0) {
++ $this->recurInterval = $interval;
++ }
++ }
++
++ /**
++ * Retrieves the length of time between recurrences of this event.
++ *
++ * @return integer The number of seconds between recurrences.
++ */
++ function getRecurInterval()
++ {
++ return $this->recurInterval;
++ }
++
++ /**
++ * Sets the number of recurrences of this event.
++ *
++ * @param integer $count The number of recurrences.
++ */
++ function setRecurCount($count)
++ {
++ if ($count > 0) {
++ $this->recurCount = (int)$count;
++ // Recurrence counts and end dates are mutually exclusive.
++ $this->recurEnd = null;
++ } else {
++ $this->recurCount = null;
++ }
++ }
++
++ /**
++ * Retrieves the number of recurrences of this event.
++ *
++ * @return integer The number recurrences.
++ */
++ function getRecurCount()
++ {
++ return $this->recurCount;
++ }
++
++ /**
++ * Returns whether this event has a recurrence with a fixed count.
++ *
++ * @return boolean True if this recurrence has a fixed count.
++ */
++ function hasRecurCount()
++ {
++ return isset($this->recurCount);
++ }
++
++ /**
++ * Sets the start date of the recurrence interval.
++ *
++ * @param Horde_Date $start The recurrence start.
++ */
++ function setRecurStart($start)
++ {
++ $this->start = new Horde_Date($start);
++ }
++
++ /**
++ * Retrieves the start date of the recurrence interval.
++ *
++ * @return Horde_Date The recurrence start.
++ */
++ function getRecurStart()
++ {
++ return $this->start;
++ }
++
++ /**
++ * Sets the end date of the recurrence interval.
++ *
++ * @param Horde_Date $end The recurrence end.
++ */
++ function setRecurEnd($end)
++ {
++ if (!empty($end)) {
++ // Recurrence counts and end dates are mutually exclusive.
++ $this->recurCount = null;
++ }
++ $this->recurEnd = new Horde_Date($end);
++ }
++
++ /**
++ * Retrieves the end date of the recurrence interval.
++ *
++ * @return Horde_Date The recurrence end.
++ */
++ function getRecurEnd()
++ {
++ return $this->recurEnd;
++ }
++
++ /**
++ * Returns whether this event has a recurrence end.
++ *
++ * @return boolean True if this recurrence ends.
++ */
++ function hasRecurEnd()
++ {
++ return isset($this->recurEnd) && isset($this->recurEnd->year) &&
++ $this->recurEnd->year != 9999;
++ }
++
++ /**
++ * Finds the next recurrence of this event that's after $afterDate.
++ *
++ * @param Horde_Date $afterDate Return events after this date.
++ *
++ * @return Horde_Date|boolean The date of the next recurrence or false
++ * if the event does not recur after
++ * $afterDate.
++ */
++ function nextRecurrence($afterDate)
++ {
++ $after = new Horde_Date($afterDate);
++ $after->correct();
++
++ if ($this->start->compareDateTime($after) >= 0) {
++ return new Horde_Date($this->start);
++ }
++
++ if ($this->recurInterval == 0) {
++ return false;
++ }
++
++ switch ($this->getRecurType()) {
++ case NAG_RECUR_DAILY:
++ $diff = Date_Calc::dateDiff($this->start->mday, $this->start->month, $this->start->year, $after->mday, $after->month, $after->year);
++ $recur = ceil($diff / $this->recurInterval);
++ if ($this->recurCount && $recur >= $this->recurCount) {
++ return false;
++ }
++ $recur *= $this->recurInterval;
++ $next = new Horde_Date($this->start);
++ list($next->mday, $next->month, $next->year) = explode('/', Date_Calc::daysToDate(Date_Calc::dateToDays($next->mday, $next->month, $next->year) + $recur, '%e/%m/%Y'));
++ if ((!$this->hasRecurEnd() ||
++ $next->compareDateTime($this->recurEnd) <= 0) &&
++ $next->compareDateTime($after) >= 0) {
++ return new Horde_Date($next);
++ }
++ break;
++
++ case NAG_RECUR_WEEKLY:
++ if (empty($this->recurData)) {
++ return false;
++ }
++
++ list($start_week->mday, $start_week->month, $start_week->year) = explode('/', Date_Calc::beginOfWeek($this->start->mday, $this->start->month, $this->start->year, '%e/%m/%Y'));
++ $start_week->hour = $this->start->hour;
++ $start_week->min = $this->start->min;
++ $start_week->sec = $this->start->sec;
++ list($after_week->mday, $after_week->month, $after_week->year) = explode('/', Date_Calc::beginOfWeek($after->mday, $after->month, $after->year, '%e/%m/%Y'));
++ $after_week_end = new Horde_Date($after_week);
++ $after_week_end->mday += 7;
++ $after_week_end->correct();
++
++ $diff = Date_Calc::dateDiff($start_week->mday, $start_week->month, $start_week->year,
++ $after_week->mday, $after_week->month, $after_week->year);
++ $recur = $diff + ($diff % ($this->recurInterval * 7));
++ if ($this->recurCount &&
++ ceil($recur / 7) / $this->recurInterval >= $this->recurCount) {
++ return false;
++ }
++ $next = $start_week;
++ list($next->mday, $next->month, $next->year) = explode('/', Date_Calc::daysToDate(Date_Calc::dateToDays($next->mday, $next->month, $next->year) + $recur, '%e/%m/%Y'));
++ $next = new Horde_Date($next);
++ while ($next->compareDateTime($after) < 0 &&
++ $next->compareDateTime($after_week_end) < 0) {
++ ++$next->mday;
++ $next->correct();
++ }
++ if (!$this->hasRecurEnd() ||
++ $next->compareDateTime($this->recurEnd) <= 0) {
++ if ($next->compareDateTime($after_week_end) >= 0) {
++ return $this->nextRecurrence($after_week_end);
++ }
++ while (!$this->recurOnDay((int)pow(2, $next->dayOfWeek())) &&
++ $next->compareDateTime($after_week_end) < 0) {
++ ++$next->mday;
++ $next->correct();
++ }
++ if (!$this->hasRecurEnd() ||
++ $next->compareDateTime($this->recurEnd) <= 0) {
++ if ($next->compareDateTime($after_week_end) >= 0) {
++ return $this->nextRecurrence($after_week_end);
++ } else {
++ return $next;
++ }
++ }
++ }
++ break;
++
++ case NAG_RECUR_MONTHLY_DATE:
++ $start = new Horde_Date($this->start);
++ if ($after->compareDateTime($start) < 0) {
++ $after = $start;
++ }
++
++ // If we're starting past this month's recurrence of the event,
++ // look in the next month on the day the event recurs.
++ if ($after->mday > $start->mday) {
++ ++$after->month;
++ $after->mday = $start->mday;
++ $after->correct();
++ }
++
++ // Adjust $start to be the first match.
++ $offset = ($after->month - $start->month) + ($after->year - $start->year) * 12;
++ $offset = floor(($offset + $this->recurInterval - 1) / $this->recurInterval) * $this->recurInterval;
++
++ if ($this->recurCount &&
++ ($offset / $this->recurInterval) >= $this->recurCount) {
++ return false;
++ }
++ $start->month += $offset;
++ $count = $offset / $this->recurInterval;
++
++ do {
++ if ($this->recurCount &&
++ $count++ >= $this->recurCount) {
++ return false;
++ }
++
++ // Don't correct for day overflow; we just skip February 30th,
++ // for example.
++ $start->correct(HORDE_DATE_MASK_MONTH);
++
++ // Bail if we've gone past the end of recurrence.
++ if ($this->hasRecurEnd() &&
++ $this->recurEnd->compareDateTime($start) < 0) {
++ return false;
++ }
++ if ($start->isValid()) {
++ return $start;
++ }
++
++ // If the interval is 12, and the date isn't valid, then we
++ // need to see if February 29th is an option. If not, then the
++ // event will _never_ recur, and we need to stop checking to
++ // avoid an infinite loop.
++ if ($this->recurInterval == 12 && ($start->month != 2 || $start->mday > 29)) {
++ return false;
++ }
++
++ // Add the recurrence interval.
++ $start->month += $this->recurInterval;
++ } while (true);
++
++ break;
++
++ case NAG_RECUR_MONTHLY_WEEKDAY:
++ // Start with the start date of the event.
++ $estart = new Horde_Date($this->start);
++
++ // What day of the week, and week of the month, do we recur on?
++ $nth = ceil($this->start->mday / 7);
++ $weekday = $estart->dayOfWeek();
++
++ // Adjust $estart to be the first candidate.
++ $offset = ($after->month - $estart->month) + ($after->year - $estart->year) * 12;
++ $offset = floor(($offset + $this->recurInterval - 1) / $this->recurInterval) * $this->recurInterval;
++
++ // Adjust our working date until it's after $after.
++ $estart->month += $offset - $this->recurInterval;
++
++ $count = $offset / $this->recurInterval;
++ do {
++ if ($this->recurCount &&
++ $count++ >= $this->recurCount) {
++ return false;
++ }
++
++ $estart->month += $this->recurInterval;
++ $estart->correct();
++
++ $next = new Horde_Date($estart);
++ $next->setNthWeekday($weekday, $nth);
++
++ if ($next->compareDateTime($after) < 0) {
++ // We haven't made it past $after yet, try again.
++ continue;
++ }
++ if ($this->hasRecurEnd() &&
++ $next->compareDateTime($this->recurEnd) > 0) {
++ // We've gone past the end of recurrence; we can give up
++ // now.
++ return false;
++ }
++
++ // We have a candidate to return.
++ break;
++ } while (true);
++
++ return $next;
++
++ case NAG_RECUR_YEARLY_DATE:
++ // Start with the start date of the event.
++ $estart = new Horde_Date($this->start);
++
++ if ($after->month > $estart->month ||
++ ($after->month == $estart->month && $after->mday > $estart->mday)) {
++ ++$after->year;
++ $after->month = $estart->month;
++ $after->mday = $estart->mday;
++ }
++
++ // Seperate case here for February 29th
++ if ($estart->month == 2 && $estart->mday == 29) {
++ while (!Horde_Date::isLeapYear($after->year)) {
++ ++$after->year;
++ }
++ }
++
++ // Adjust $estart to be the first candidate.
++ $offset = $after->year - $estart->year;
++ if ($offset > 0) {
++ $offset = floor(($offset + $this->recurInterval - 1) / $this->recurInterval) * $this->recurInterval;
++ $estart->year += $offset;
++ }
++
++ // We've gone past the end of recurrence; give up.
++ if ($this->recurCount &&
++ $offset >= $this->recurCount) {
++ return false;
++ }
++ if ($this->hasRecurEnd() &&
++ $this->recurEnd->compareDateTime($estart) < 0) {
++ return false;
++ }
++
++ return $estart;
++
++ case NAG_RECUR_YEARLY_DAY:
++ // Check count first.
++ $dayofyear = $this->start->dayOfYear();
++ $count = ($after->year - $this->start->year) / $this->recurInterval + 1;
++ if ($this->recurCount &&
++ ($count > $this->recurCount ||
++ ($count == $this->recurCount &&
++ $after->dayOfYear() > $dayofyear))) {
++ return false;
++ }
++
++ // Start with a rough interval.
++ $estart = new Horde_Date($this->start);
++ $estart->year += floor($count - 1) * $this->recurInterval;
++
++ // Now add the difference to the required day of year.
++ $estart->mday += $dayofyear - $estart->dayOfYear();
++ $estart->correct();
++
++ // Add an interval if the estimation was wrong.
++ if ($estart->compareDate($after) < 0) {
++ $estart->year += $this->recurInterval;
++ $estart->mday += $dayofyear - $estart->dayOfYear();
++ $estart->correct();
++ }
++
++ // We've gone past the end of recurrence; give up.
++ if ($this->hasRecurEnd() &&
++ $this->recurEnd->compareDateTime($estart) < 0) {
++ return false;
++ }
++
++ return $estart;
++
++ case NAG_RECUR_YEARLY_WEEKDAY:
++ // Start with the start date of the event.
++ $estart = new Horde_Date($this->start);
++
++ // What day of the week, and week of the month, do we recur on?
++ $nth = ceil($this->start->mday / 7);
++ $weekday = $estart->dayOfWeek();
++
++ // Adjust $estart to be the first candidate.
++ $offset = floor(($after->year - $estart->year + $this->recurInterval - 1) / $this->recurInterval) * $this->recurInterval;
++
++ // Adjust our working date until it's after $after.
++ $estart->year += $offset - $this->recurInterval;
++
++ $count = $offset / $this->recurInterval;
++ do {
++ if ($this->recurCount &&
++ $count++ >= $this->recurCount) {
++ return false;
++ }
++
++ $estart->year += $this->recurInterval;
++ $estart->correct();
++
++ $next = new Horde_Date($estart);
++ $next->setNthWeekday($weekday, $nth);
++
++ if ($next->compareDateTime($after) < 0) {
++ // We haven't made it past $after yet, try again.
++ continue;
++ }
++ if ($this->hasRecurEnd() &&
++ $next->compareDateTime($this->recurEnd) > 0) {
++ // We've gone past the end of recurrence; we can give up
++ // now.
++ return false;
++ }
++
++ // We have a candidate to return.
++ break;
++ } while (true);
++
++ return $next;
++ }
++
++ // We didn't find anything, the recurType was bad, or something else
++ // went wrong - return false.
++ return false;
++ }
++
++ /**
++ * Returns whether this event has any date that matches the recurrence
++ * rules and is not an exception.
++ *
++ * @return boolean True if an active recurrence exists.
++ */
++ function hasActiveRecurrence()
++ {
++ if (!$this->hasRecurEnd()) {
++ return true;
++ }
++
++ $next = $this->nextRecurrence(new Horde_Date($this->start));
++ while (is_object($next)) {
++ if (!$this->hasException($next->year, $next->month, $next->mday) &&
++ !$this->hasCompletion($next->year, $next->month, $next->mday)) {
++ return true;
++ }
++
++ $next = $this->nextRecurrence(array('year' => $next->year,
++ 'month' => $next->month,
++ 'mday' => $next->mday + 1,
++ 'hour' => $next->hour,
++ 'min' => $next->min,
++ 'sec' => $next->sec));
++ }
++
++ return false;
++ }
++
++ /**
++ * Returns the next active recurrence.
++ *
++ * @param Horde_Date $afterDate Return events after this date.
++ *
++ * @return Horde_Date|boolean The date of the next active
++ * recurrence or false if the event
++ * has no active recurrence after
++ * $afterDate.
++ */
++ function nextActiveRecurrence($afterDate)
++ {
++ $next = $this->nextRecurrence($afterDate);
++ while (is_object($next)) {
++ if (!$this->hasException($next->year, $next->month, $next->mday) &&
++ !$this->hasCompletion($next->year, $next->month, $next->mday)) {
++ return $next;
++ }
++ $next->mday++;
++ $next = $this->nextRecurrence($next);
++ }
++
++ return false;
++ }
++
++ /**
++ * Adds an exception to a recurring event.
++ *
++ * @param integer $year The year of the execption.
++ * @param integer $month The month of the execption.
++ * @param integer $mday The day of the month of the exception.
++ */
++ function addException($year, $month, $mday)
++ {
++ $this->exceptions[] = sprintf('%04d%02d%02d', $year, $month, $mday);
++ }
++
++ /**
++ * Deletes an exception from a recurring event.
++ *
++ * @param integer $year The year of the execption.
++ * @param integer $month The month of the execption.
++ * @param integer $mday The day of the month of the exception.
++ */
++ function deleteException($year, $month, $mday)
++ {
++ $key = array_search(sprintf('%04d%02d%02d', $year, $month, $mday), $this->exceptions);
++ if ($key !== false) {
++ unset($this->exceptions[$key]);
++ }
++ }
++
++ /**
++ * Checks if an exception exists for a given reccurence of an event.
++ *
++ * @param integer $year The year of the reucrance.
++ * @param integer $month The month of the reucrance.
++ * @param integer $mday The day of the month of the reucrance.
++ *
++ * @return boolean True if an exception exists for the given date.
++ */
++ function hasException($year, $month, $mday)
++ {
++ return in_array(sprintf('%04d%02d%02d', $year, $month, $mday),
++ $this->getExceptions());
++ }
++
++ /**
++ * Retrieves all the exceptions for this event.
++ *
++ * @return array Array containing the dates of all the exceptions in
++ * YYYYMMDD form.
++ */
++ function getExceptions()
++ {
++ return $this->exceptions;
++ }
++
++ /**
++ * Adds an completion to a recurring event.
++ *
++ * @param integer $year The year of the execption.
++ * @param integer $month The month of the execption.
++ * @param integer $mday The day of the month of the completion.
++ */
++ function addCompletion($year, $month, $mday)
++ {
++ $this->completions[] = sprintf('%04d%02d%02d', $year, $month, $mday);
++ }
++
++ /**
++ * Deletes an completion from a recurring event.
++ *
++ * @param integer $year The year of the execption.
++ * @param integer $month The month of the execption.
++ * @param integer $mday The day of the month of the completion.
++ */
++ function deleteCompletion($year, $month, $mday)
++ {
++ $key = array_search(sprintf('%04d%02d%02d', $year, $month, $mday), $this->completions);
++ if ($key !== false) {
++ unset($this->completions[$key]);
++ }
++ }
++
++ /**
++ * Checks if a completion exists for a given reccurence of an event.
++ *
++ * @param integer $year The year of the reucrance.
++ * @param integer $month The month of the recurrance.
++ * @param integer $mday The day of the month of the recurrance.
++ *
++ * @return boolean True if an completion exists for the given date.
++ */
++ function hasCompletion($year, $month, $mday)
++ {
++ return in_array(sprintf('%04d%02d%02d', $year, $month, $mday),
++ $this->getCompletions());
++ }
++
++ /**
++ * Retrieves all the completions for this event.
++ *
++ * @return array Array containing the dates of all the completions in
++ * YYYYMMDD form.
++ */
++ function getCompletions()
++ {
++ return $this->completions;
++ }
++
++ /**
++ * Parses a vCalendar 1.0 recurrence rule.
++ *
++ * @link http://www.imc.org/pdi/vcal-10.txt
++ * @link http://www.shuchow.com/vCalAddendum.html
++ *
++ * @param string $rrule A vCalendar 1.0 conform RRULE value.
++ */
++ function fromRRule10($rrule)
++ {
++ if (!$rrule) {
++ return;
++ }
++
++ if (!preg_match('/([A-Z]+)(\d+)?(.*)/', $rrule, $matches)) {
++ // No recurrence data - event does not recur.
++ $this->setRecurType(NAG_RECUR_NONE);
++ }
++
++ // Always default the recurInterval to 1.
++ $this->setRecurInterval(!empty($matches[2]) ? $matches[2] : 1);
++
++ $remainder = trim($matches[3]);
++
++ switch ($matches[1]) {
++ case 'D':
++ $this->setRecurType(NAG_RECUR_DAILY);
++ break;
++
++ case 'W':
++ $this->setRecurType(NAG_RECUR_WEEKLY);
++ if (!empty($remainder)) {
++ $maskdays = array('SU' => HORDE_DATE_MASK_SUNDAY,
++ 'MO' => HORDE_DATE_MASK_MONDAY,
++ 'TU' => HORDE_DATE_MASK_TUESDAY,
++ 'WE' => HORDE_DATE_MASK_WEDNESDAY,
++ 'TH' => HORDE_DATE_MASK_THURSDAY,
++ 'FR' => HORDE_DATE_MASK_FRIDAY,
++ 'SA' => HORDE_DATE_MASK_SATURDAY);
++ $mask = 0;
++ while (preg_match('/^ ?[A-Z]{2} ?/', $remainder, $matches)) {
++ $day = trim($matches[0]);
++ $remainder = substr($remainder, strlen($matches[0]));
++ $mask |= $maskdays[$day];
++ }
++ $this->setRecurOnDay($mask);
++ } else {
++ // Recur on the day of the week of the original recurrence.
++ $maskdays = array(HORDE_DATE_SUNDAY => HORDE_DATE_MASK_SUNDAY,
++ HORDE_DATE_MONDAY => HORDE_DATE_MASK_MONDAY,
++ HORDE_DATE_TUESDAY => HORDE_DATE_MASK_TUESDAY,
++ HORDE_DATE_WEDNESDAY => HORDE_DATE_MASK_WEDNESDAY,
++ HORDE_DATE_THURSDAY => HORDE_DATE_MASK_THURSDAY,
++ HORDE_DATE_FRIDAY => HORDE_DATE_MASK_FRIDAY,
++ HORDE_DATE_SATURDAY => HORDE_DATE_MASK_SATURDAY);
++ $this->setRecurOnDay($maskdays[$this->start->dayOfWeek()]);
++ }
++ break;
++
++ case 'MP':
++ $this->setRecurType(NAG_RECUR_MONTHLY_WEEKDAY);
++ break;
++
++ case 'MD':
++ $this->setRecurType(NAG_RECUR_MONTHLY_DATE);
++ break;
++
++ case 'YM':
++ $this->setRecurType(NAG_RECUR_YEARLY_DATE);
++ break;
++
++ case 'YD':
++ $this->setRecurType(NAG_RECUR_YEARLY_DAY);
++ break;
++ }
++
++ // We don't support modifiers at the moment, strip them.
++ while ($remainder && !preg_match('/^(#\d+|\d{8})($| |T\d{6})/', $remainder)) {
++ $remainder = substr($remainder, 1);
++ }
++ if (!empty($remainder)) {
++ if (strpos($remainder, '#') !== false) {
++ $this->setRecurCount(substr($remainder, 1));
++ } else {
++ list($year, $month, $mday) = sscanf($remainder, '%04d%02d%02d');
++ $this->setRecurEnd(new Horde_Date(array('year' => $year,
++ 'month' => $month,
++ 'mday' => $mday)));
++ }
++ }
++ }
++
++ /**
++ * Creates a vCalendar 1.0 recurrence rule.
++ *
++ * @link http://www.imc.org/pdi/vcal-10.txt
++ * @link http://www.shuchow.com/vCalAddendum.html
++ *
++ * @param Horde_iCalendar $calendar A Horde_iCalendar object instance.
++ *
++ * @return string A vCalendar 1.0 conform RRULE value.
++ */
++ function toRRule10($calendar)
++ {
++ switch ($this->recurType) {
++ case NAG_RECUR_NONE:
++ return '';
++
++ case NAG_RECUR_DAILY:
++ $rrule = 'D' . $this->recurInterval;
++ break;
++
++ case NAG_RECUR_WEEKLY:
++ $rrule = 'W' . $this->recurInterval;
++ $vcaldays = array('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA');
++
++ for ($i = 0; $i <= 7 ; ++$i) {
++ if ($this->recurOnDay(pow(2, $i))) {
++ $rrule .= ' ' . $vcaldays[$i];
++ }
++ }
++ break;
++
++ case NAG_RECUR_MONTHLY_DATE:
++ $rrule = 'MD' . $this->recurInterval . ' ' . trim($this->start->mday);
++ break;
++
++ case NAG_RECUR_MONTHLY_WEEKDAY:
++ $next_week = new Horde_Date($this->start);
++ $next_week->mday += 7;
++ $next_week->correct();
++
++ if ($this->start->month != $next_week->month) {
++ $p = 5;
++ } else {
++ $p = (int)($this->start->mday / 7);
++ if (($this->start->mday % 7) > 0) {
++ $p++;
++ }
++ }
++
++ $vcaldays = array('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA');
++ $rrule = 'MP' . $this->recurInterval . ' ' . $p . '+ ' . $vcaldays[$this->start->dayOfWeek()];
++ break;
++
++ case NAG_RECUR_YEARLY_DATE:
++ $rrule = 'YM' . $this->recurInterval . ' ' . trim($this->start->month);
++ break;
++
++ case NAG_RECUR_YEARLY_DAY:
++ $rrule = 'YD' . $this->recurInterval . ' ' . $this->start->dayOfYear();
++ break;
++
++ default:
++ return '';
++ }
++
++ return $this->hasRecurEnd() ?
++ $rrule . ' ' . $calendar->_exportDate($this->recurEnd) :
++ $rrule . ' #' . (int)$this->getRecurCount();
++ }
++
++ /**
++ * Parses an iCalendar 2.0 recurrence rule.
++ *
++ * @link http://rfc.net/rfc2445.html#s4.8.5
++ * @link http://www.shuchow.com/vCalAddendum.html
++ *
++ * @param string $rrule An iCalendar 2.0 conform RRULE value.
++ */
++ function fromRRule20($rrule)
++ {
++ // Parse the recurrence rule into keys and values.
++ $rdata = array();
++ $parts = explode(';', $rrule);
++ foreach ($parts as $part) {
++ list($key, $value) = explode('=', $part, 2);
++ $rdata[String::upper($key)] = $value;
++ }
++
++ if (isset($rdata['FREQ'])) {
++ // Always default the recurInterval to 1.
++ $this->setRecurInterval(isset($rdata['INTERVAL']) ? $rdata['INTERVAL'] : 1);
++
++ switch (String::upper($rdata['FREQ'])) {
++ case 'DAILY':
++ $this->setRecurType(NAG_RECUR_DAILY);
++ break;
++
++ case 'WEEKLY':
++ $this->setRecurType(NAG_RECUR_WEEKLY);
++ if (isset($rdata['BYDAY'])) {
++ $maskdays = array('SU' => HORDE_DATE_MASK_SUNDAY,
++ 'MO' => HORDE_DATE_MASK_MONDAY,
++ 'TU' => HORDE_DATE_MASK_TUESDAY,
++ 'WE' => HORDE_DATE_MASK_WEDNESDAY,
++ 'TH' => HORDE_DATE_MASK_THURSDAY,
++ 'FR' => HORDE_DATE_MASK_FRIDAY,
++ 'SA' => HORDE_DATE_MASK_SATURDAY);
++ $days = explode(',', $rdata['BYDAY']);
++ $mask = 0;
++ foreach ($days as $day) {
++ $mask |= $maskdays[$day];
++ }
++ $this->setRecurOnDay($mask);
++ } else {
++ // Recur on the day of the week of the original
++ // recurrence.
++ $maskdays = array(
++ HORDE_DATE_SUNDAY => HORDE_DATE_MASK_SUNDAY,
++ HORDE_DATE_MONDAY => HORDE_DATE_MASK_MONDAY,
++ HORDE_DATE_TUESDAY => HORDE_DATE_MASK_TUESDAY,
++ HORDE_DATE_WEDNESDAY => HORDE_DATE_MASK_WEDNESDAY,
++ HORDE_DATE_THURSDAY => HORDE_DATE_MASK_THURSDAY,
++ HORDE_DATE_FRIDAY => HORDE_DATE_MASK_FRIDAY,
++ HORDE_DATE_SATURDAY => HORDE_DATE_MASK_SATURDAY);
++ $this->setRecurOnDay($maskdays[$this->start->dayOfWeek()]);
++ }
++ break;
++
++ case 'MONTHLY':
++ if (isset($rdata['BYDAY'])) {
++ $this->setRecurType(NAG_RECUR_MONTHLY_WEEKDAY);
++ } else {
++ $this->setRecurType(NAG_RECUR_MONTHLY_DATE);
++ }
++ break;
++
++ case 'YEARLY':
++ if (isset($rdata['BYYEARDAY'])) {
++ $this->setRecurType(NAG_RECUR_YEARLY_DAY);
++ } elseif (isset($rdata['BYDAY'])) {
++ $this->setRecurType(NAG_RECUR_YEARLY_WEEKDAY);
++ } else {
++ $this->setRecurType(NAG_RECUR_YEARLY_DATE);
++ }
++ break;
++ }
++
++ if (isset($rdata['UNTIL'])) {
++ list($year, $month, $mday) = sscanf($rdata['UNTIL'],
++ '%04d%02d%02d');
++ $this->setRecurEnd(new Horde_Date(array('year' => $year,
++ 'month' => $month,
++ 'mday' => $mday)));
++ }
++ if (isset($rdata['COUNT'])) {
++ $this->setRecurCount($rdata['COUNT']);
++ }
++ } else {
++ // No recurrence data - event does not recur.
++ $this->setRecurType(NAG_RECUR_NONE);
++ }
++ }
++
++ /**
++ * Creates an iCalendar 2.0 recurrence rule.
++ *
++ * @link http://rfc.net/rfc2445.html#s4.8.5
++ * @link http://www.shuchow.com/vCalAddendum.html
++ *
++ * @param Horde_iCalendar $calendar A Horde_iCalendar object instance.
++ *
++ * @return string An iCalendar 2.0 conform RRULE value.
++ */
++ function toRRule20($calendar)
++ {
++ switch ($this->recurType) {
++ case NAG_RECUR_NONE:
++ return '';
++
++ case NAG_RECUR_DAILY:
++ $rrule = 'FREQ=DAILY;INTERVAL=' . $this->recurInterval;
++ break;
++
++ case NAG_RECUR_WEEKLY:
++ $rrule = 'FREQ=WEEKLY;INTERVAL=' . $this->recurInterval . ';BYDAY=';
++ $vcaldays = array('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA');
++
++ for ($i = $flag = 0; $i <= 7 ; ++$i) {
++ if ($this->recurOnDay(pow(2, $i))) {
++ if ($flag) {
++ $rrule .= ',';
++ }
++ $rrule .= $vcaldays[$i];
++ $flag = true;
++ }
++ }
++ break;
++
++ case NAG_RECUR_MONTHLY_DATE:
++ $rrule = 'FREQ=MONTHLY;INTERVAL=' . $this->recurInterval;
++ break;
++
++ case NAG_RECUR_MONTHLY_WEEKDAY:
++ $next_week = new Horde_Date($this->start);
++ $next_week->mday += 7;
++ $next_week->correct();
++ if ($this->start->month != $next_week->month) {
++ $p = 5;
++ } else {
++ $p = (int)($this->start->mday / 7);
++ if (($this->start->mday % 7) > 0) {
++ $p++;
++ }
++ }
++ $vcaldays = array('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA');
++ $rrule = 'FREQ=MONTHLY;INTERVAL=' . $this->recurInterval
++ . ';BYDAY=' . $p . $vcaldays[$this->start->dayOfWeek()];
++ break;
++
++ case NAG_RECUR_YEARLY_DATE:
++ $rrule = 'FREQ=YEARLY;INTERVAL=' . $this->recurInterval;
++ break;
++
++ case NAG_RECUR_YEARLY_DAY:
++ $rrule = 'FREQ=YEARLY;INTERVAL=' . $this->recurInterval
++ . ';BYYEARDAY=' . $this->start->dayOfYear();
++ break;
++
++ case NAG_RECUR_YEARLY_WEEKDAY:
++ $vcaldays = array('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA');
++ $weekday = date('W', mktime(0,
++ 0,
++ 0,
++ $this->start->month,
++ 1,
++ $this->start->year));
++ $rrule = 'FREQ=YEARLY;INTERVAL=' . $this->recurInterval
++ . ';BYDAY=' . ($this->start->weekOfYear() - $weekday + 1)
++ . $vcaldays[$this->start->dayOfWeek()]
++ . ';BYMONTH=' . $this->start->month;
++ break;
++ }
++
++ if ($this->hasRecurEnd()) {
++ $rrule .= ';UNTIL=' . $calendar->_exportDate($this->recurEnd);
++ }
++ if ($count = $this->getRecurCount()) {
++ $rrule .= ';COUNT=' . $count;
++ }
++ return $rrule;
++ }
++
++ /**
++ * Parses the recurrence data from a hash.
++ *
++ * @param array $hash The hash to convert.
++ *
++ * @return boolean True if the hash seemed valid, false otherwise.
++ */
++ function fromHash($hash)
++ {
++ if (!isset($hash['interval']) || !isset($hash['interval']) ||
++ !isset($hash['range-type'])) {
++ $this->setRecurType(NAG_RECUR_NONE);
++ return false;
++ }
++
++ $this->setRecurInterval((int) $hash['interval']);
++
++ $parse_day = false;
++ $set_daymask = false;
++ $update_month = false;
++ $update_daynumber = false;
++ $update_weekday = false;
++ $nth_weekday = -1;
++
++ switch ($hash['cycle']) {
++ case 'daily':
++ $this->setRecurType(NAG_RECUR_DAILY);
++ break;
++
++ case 'weekly':
++ $this->setRecurType(NAG_RECUR_WEEKLY);
++ $parse_day = true;
++ $set_daymask = true;
++ break;
++
++ case 'monthly':
++ if (!isset($hash['daynumber'])) {
++ $this->setRecurType(NAG_RECUR_NONE);
++ return false;
++ }
++
++ switch ($hash['type']) {
++ case 'daynumber':
++ $this->setRecurType(NAG_RECUR_MONTHLY_DATE);
++ $update_daynumber = true;
++ break;
++
++ case 'weekday':
++ $this->setRecurType(NAG_RECUR_MONTHLY_WEEKDAY);
++ $nth_weekday = (int) $hash['daynumber'];
++ $hash['daynumber'] = 1;
++ $parse_day = true;
++ $update_daynumber = true;
++ $update_weekday = true;
++ break;
++ }
++ break;
++
++ case 'yearly':
++ if (!isset($hash['type'])) {
++ $this->setRecurType(NAG_RECUR_NONE);
++ return false;
++ }
++
++ switch ($hash['type']) {
++ case 'monthday':
++ $this->setRecurType(NAG_RECUR_YEARLY_DATE);
++ $update_month = true;
++ $update_daynumber = true;
++ break;
++
++ case 'yearday':
++ if (!isset($hash['month'])) {
++ $this->setRecurType(NAG_RECUR_NONE);
++ return false;
++ }
++
++ $this->setRecurType(NAG_RECUR_YEARLY_DAY);
++ // Start counting days in January.
++ $hash['month'] = 'january';
++ $update_month = true;
++ $update_daynumber = true;
++ break;
++
++ case 'weekday':
++ if (!isset($hash['daynumber'])) {
++ $this->setRecurType(NAG_RECUR_NONE);
++ return false;
++ }
++
++ $this->setRecurType(NAG_RECUR_YEARLY_WEEKDAY);
++ $nth_weekday = (int) $hash['daynumber'];
++ $hash['daynumber'] = 1;
++ $parse_day = true;
++ $update_month = true;
++ $update_daynumber = true;
++ $update_weekday = true;
++ break;
++ }
++ }
++
++ switch ($hash['range-type']) {
++ case 'number':
++ if (!isset($hash['range'])) {
++ $this->setRecurType(NAG_RECUR_NONE);
++ return false;
++ }
++
++ $this->setRecurCount((int) $hash['range']);
++ break;
++
++ case 'date':
++ $recur_end = new Horde_Date($hash['range']);
++ $recur_end->hour = 23;
++ $recur_end->min = 59;
++ $recur_end->sec = 59;
++ $this->setRecurEnd($recur_end);
++ break;
++ }
++
++ // Need to parse <day>?
++ $last_found_day = -1;
++ if ($parse_day) {
++ if (!isset($hash['day'])) {
++ $this->setRecurType(NAG_RECUR_NONE);
++ return false;
++ }
++
++ $mask = 0;
++ $bits = array(
++ 'monday' => HORDE_DATE_MASK_MONDAY,
++ 'tuesday' => HORDE_DATE_MASK_TUESDAY,
++ 'wednesday' => HORDE_DATE_MASK_WEDNESDAY,
++ 'thursday' => HORDE_DATE_MASK_THURSDAY,
++ 'friday' => HORDE_DATE_MASK_FRIDAY,
++ 'saturday' => HORDE_DATE_MASK_SATURDAY,
++ 'sunday' => HORDE_DATE_MASK_SUNDAY,
++ );
++ $days = array(
++ 'monday' => HORDE_DATE_MONDAY,
++ 'tuesday' => HORDE_DATE_TUESDAY,
++ 'wednesday' => HORDE_DATE_WEDNESDAY,
++ 'thursday' => HORDE_DATE_THURSDAY,
++ 'friday' => HORDE_DATE_FRIDAY,
++ 'saturday' => HORDE_DATE_SATURDAY,
++ 'sunday' => HORDE_DATE_SUNDAY,
++ );
++
++ foreach ($hash['day'] as $day) {
++ // Validity check.
++ if (empty($day) || !isset($bits[$day])) {
++ continue;
++ }
++
++ $mask |= $bits[$day];
++ $last_found_day = $days[$day];
++ }
++
++ if ($set_daymask) {
++ $this->setRecurOnDay($mask);
++ }
++ }
++
++ if ($update_month || $update_daynumber || $update_weekday) {
++ if ($update_month) {
++ $month2number = array(
++ 'january' => 1,
++ 'february' => 2,
++ 'march' => 3,
++ 'april' => 4,
++ 'may' => 5,
++ 'june' => 6,
++ 'july' => 7,
++ 'august' => 8,
++ 'september' => 9,
++ 'october' => 10,
++ 'november' => 11,
++ 'december' => 12,
++ );
++
++ if (isset($month2number[$hash['month']])) {
++ $this->start->month = $month2number[$hash['month']];
++ }
++ }
++
++ if ($update_daynumber) {
++ if (!isset($hash['daynumber'])) {
++ $this->setRecurType(NAG_RECUR_NONE);
++ return false;
++ }
++
++ $this->start->mday = $hash['daynumber'];
++ }
++
++ if ($update_weekday) {
++ $this->start->setNthWeekday($last_found_day, $nth_weekday);
++ }
++
++ $this->start->correct();
++ }
++
++ // Exceptions.
++ if (isset($hash['exceptions'])) {
++ $this->exceptions = $hash['exceptions'];
++ }
++
++ if (isset($hash['completions'])) {
++ $this->completions = $hash['completions'];
++ }
++
++ return true;
++ }
++
++ /**
++ * Export this object into a hash.
++ *
++ * @return array The recurrence hash.
++ */
++ function toHash()
++ {
++ if ($this->getRecurType() == NAG_RECUR_NONE) {
++ return array();
++ }
++
++ $day2number = array(
++ 0 => 'sunday',
++ 1 => 'monday',
++ 2 => 'tuesday',
++ 3 => 'wednesday',
++ 4 => 'thursday',
++ 5 => 'friday',
++ 6 => 'saturday'
++ );
++ $month2number = array(
++ 1 => 'january',
++ 2 => 'february',
++ 3 => 'march',
++ 4 => 'april',
++ 5 => 'may',
++ 6 => 'june',
++ 7 => 'july',
++ 8 => 'august',
++ 9 => 'september',
++ 10 => 'october',
++ 11 => 'november',
++ 12 => 'december'
++ );
++
++ $hash = array('interval' => $this->getRecurInterval());
++ $start = $this->getRecurStart();
++
++ switch ($this->getRecurType()) {
++ case NAG_RECUR_DAILY:
++ $hash['cycle'] = 'daily';
++ break;
++
++ case NAG_RECUR_WEEKLY:
++ $hash['cycle'] = 'weekly';
++ $bits = array(
++ 'monday' => HORDE_DATE_MASK_MONDAY,
++ 'tuesday' => HORDE_DATE_MASK_TUESDAY,
++ 'wednesday' => HORDE_DATE_MASK_WEDNESDAY,
++ 'thursday' => HORDE_DATE_MASK_THURSDAY,
++ 'friday' => HORDE_DATE_MASK_FRIDAY,
++ 'saturday' => HORDE_DATE_MASK_SATURDAY,
++ 'sunday' => HORDE_DATE_MASK_SUNDAY,
++ );
++ $days = array();
++ foreach($bits as $name => $bit) {
++ if ($this->recurOnDay($bit)) {
++ $days[] = $name;
++ }
++ }
++ $hash['day'] = $days;
++ break;
++
++ case NAG_RECUR_MONTHLY_DATE:
++ $hash['cycle'] = 'monthly';
++ $hash['type'] = 'daynumber';
++ $hash['daynumber'] = $start->mday;
++ break;
++
++ case NAG_RECUR_MONTHLY_WEEKDAY:
++ $hash['cycle'] = 'monthly';
++ $hash['type'] = 'weekday';
++ $hash['daynumber'] = $start->weekOfMonth();
++ $hash['day'] = array ($day2number[$start->dayOfWeek()]);
++ break;
++
++ case NAG_RECUR_YEARLY_DATE:
++ $hash['cycle'] = 'yearly';
++ $hash['type'] = 'monthday';
++ $hash['daynumber'] = $start->mday;
++ $hash['month'] = $month2number[$start->month];
++ break;
++
++ case NAG_RECUR_YEARLY_DAY:
++ $hash['cycle'] = 'yearly';
++ $hash['type'] = 'yearday';
++ $hash['daynumber'] = $start->dayOfYear();
++ break;
++
++ case NAG_RECUR_YEARLY_WEEKDAY:
++ $hash['cycle'] = 'yearly';
++ $hash['type'] = 'weekday';
++ $hash['daynumber'] = $start->weekOfMonth();
++ $hash['day'] = array ($day2number[$start->dayOfWeek()]);
++ $hash['month'] = $month2number[$start->month];
++ }
++
++ if ($this->hasRecurCount()) {
++ $hash['range-type'] = 'number';
++ $hash['range'] = $this->getRecurCount();
++ } elseif ($this->hasRecurEnd()) {
++ $date = $this->getRecurEnd();
++ $hash['range-type'] = 'date';
++ $hash['range'] = $date->datestamp();
++ } else {
++ $hash['range-type'] = 'none';
++ $hash['range'] = '';
++ }
++
++ // Recurrence exceptions
++ $hash['exceptions'] = $this->exceptions;
++ $hash['completions'] = $this->completions;
++
++ return $hash;
++ }
++
++}
+diff -r e16c159ea616 nag/lib/UI/VarRenderer/nag.php
+--- a/nag/lib/UI/VarRenderer/nag.php Fri Aug 08 13:11:07 2008 +0200
++++ b/nag/lib/UI/VarRenderer/nag.php Fri Aug 08 14:19:56 2008 +0200
+@@ -26,6 +26,147 @@
+ * @package Nag
+ */
+ class Horde_UI_VarRenderer_nag extends Horde_UI_VarRenderer_html {
++
++ function _renderVarInput_nag_recurrence($form, &$var, &$vars)
++ {
++ $var->type->getInfo($vars, $var, $info);
++
++ require_once NAG_BASE . '/lib/Recurrence.php';
++
++ $r = &new Nag_Recurrence(time());
++ if (!empty($info)) {
++ $r->fromHash($info);
++ }
++?>
++<table cellspacing="0">
++<tr>
++ <td valign="top" colspan="4">
++ <table cellspacing="0" width="100%">
++ <tr>
++ <td class="nowrap">
++ <input id="recurnone" type="radio" class="checkbox" name="recur" onclick="clearFields(0);" value="<?php echo NAG_RECUR_NONE ?>"<?php if ($r->hasRecurType(NAG_RECUR_NONE)) echo ' checked="checked"' ?> /><label for="recurnone"> <?php echo _("No recurrence") ?></label>
++ </td>
++ </tr>
++ <tr>
++ <td class="nowrap">
++ <input id="recurdaily" type="radio" class="checkbox" name="recur" onclick="setInterval('recur_daily_interval');" value="<?php echo NAG_RECUR_DAILY ?>"<?php if ($r->hasRecurType(NAG_RECUR_DAILY)) echo ' checked="checked"' ?> /><label for="recurdaily"> <?php echo _("Daily: Recurs every") ?>&nbsp;</label>
++ <input type="text" id="recur_daily_interval" name="recur_daily_interval" size="2" onkeypress="setRecur(1);" onchange="setRecur(1);" value="<?php echo $r->hasRecurType(NAG_RECUR_DAILY) ? $r->getRecurInterval() : '' ?>" />&nbsp;<?php echo Horde::label('recur_daily_interval', _("day(s)")) ?>
++ </td>
++ </tr>
++ <tr>
++ <td class="nowrap">
++ <input id="recurweekly" type="radio" class="checkbox" name="recur" onclick="setInterval('recur_weekly_interval');" value="<?php echo NAG_RECUR_WEEKLY ?>"<?php if ($r->hasRecurType(NAG_RECUR_WEEKLY)) echo ' checked="checked"' ?> /><label for="recurweekly"> <?php echo _("Weekly: Recurs every") ?>&nbsp;</label>
++ <input type="text" id="recur_weekly_interval" name="recur_weekly_interval" size="2" onkeypress="setRecur(2);" onchange="setRecur(2);" value="<?php echo $r->hasRecurType(NAG_RECUR_WEEKLY) ? $r->getRecurInterval() : '' ?>" />&nbsp;
++ <?php echo Horde::label('recur_weekly_interval', _("week(s) on:")) ?><br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
++ <label for="mo"><?php echo _("Mo") ?></label><input id="mo" type="checkbox" class="checkbox" name="weekly[]" onclick="setInterval('recur_weekly_interval');setRecur(2);" value="<?php echo HORDE_DATE_MASK_MONDAY ?>"<?php if ($r->recurOnDay(HORDE_DATE_MASK_MONDAY)) echo ' checked="checked"' ?> />&nbsp;
++ <label for="tu"><?php echo _("Tu") ?></label><input id="tu" type="checkbox" class="checkbox" name="weekly[]" onclick="setInterval('recur_weekly_interval');setRecur(2);" value="<?php echo HORDE_DATE_MASK_TUESDAY ?>"<?php if ($r->recurOnDay(HORDE_DATE_MASK_TUESDAY)) echo ' checked="checked"' ?> />&nbsp;
++ <label for="we"><?php echo _("We") ?></label><input id="we" type="checkbox" class="checkbox" name="weekly[]" onclick="setInterval('recur_weekly_interval');setRecur(2);" value="<?php echo HORDE_DATE_MASK_WEDNESDAY ?>"<?php if ($r->recurOnDay(HORDE_DATE_MASK_WEDNESDAY)) echo ' checked="checked"' ?> />&nbsp;
++ <label for="th"><?php echo _("Th") ?></label><input id="th" type="checkbox" class="checkbox" name="weekly[]" onclick="setInterval('recur_weekly_interval');setRecur(2);" value="<?php echo HORDE_DATE_MASK_THURSDAY ?>"<?php if ($r->recurOnDay(HORDE_DATE_MASK_THURSDAY)) echo ' checked="checked"' ?> />&nbsp;
++ <label for="fr"><?php echo _("Fr") ?></label><input id="fr" type="checkbox" class="checkbox" name="weekly[]" onclick="setInterval('recur_weekly_interval');setRecur(2);" value="<?php echo HORDE_DATE_MASK_FRIDAY ?>"<?php if ($r->recurOnDay(HORDE_DATE_MASK_FRIDAY)) echo ' checked="checked"' ?> />&nbsp;
++ <label for="sa"><?php echo _("Sa") ?></label><input id="sa" type="checkbox" class="checkbox" name="weekly[]" onclick="setInterval('recur_weekly_interval');setRecur(2);" value="<?php echo HORDE_DATE_MASK_SATURDAY ?>"<?php if ($r->recurOnDay(HORDE_DATE_MASK_SATURDAY)) echo ' checked="checked"' ?> />&nbsp;
++ <label for="su"><?php echo _("Su") ?></label><input id="su" type="checkbox" class="checkbox" name="weekly[]" onclick="setInterval('recur_weekly_interval');setRecur(2);" value="<?php echo HORDE_DATE_MASK_SUNDAY ?>"<?php if ($r->recurOnDay(HORDE_DATE_MASK_SUNDAY)) echo ' checked="checked"' ?> />&nbsp;
++ </td>
++ </tr>
++ <tr>
++ <td class="nowrap">
++ <input id="recurmonthday" type="radio" class="checkbox" name="recur" onclick="setInterval('recur_day_of_month_interval');" value="<?php echo NAG_RECUR_MONTHLY_DATE ?>"<?php if ($r->hasRecurType(NAG_RECUR_MONTHLY_DATE)) echo ' checked="checked"' ?> /><label for="recurmonthday"> <?php echo _("Monthly: Recurs every") ?>&nbsp;</label>
++ <input type="text" id="recur_day_of_month_interval" name="recur_day_of_month_interval" size="2" onkeypress="setRecur(3);" onchange="setRecur(3);" value="<?php echo $r->hasRecurType(NAG_RECUR_MONTHLY_DATE) ? $r->getRecurInterval() : '' ?>" />&nbsp;<?php echo Horde::label('recur_day_of_month_interval', _("month(s)") . ' ' . _("on the same date")) ?>
++ </td>
++ </tr>
++ <tr>
++ <td class="nowrap">
++ <input id="recurmonthweek" type="radio" class="checkbox" name="recur" onclick="setInterval('recur_week_of_month_interval');" value="<?php echo NAG_RECUR_MONTHLY_WEEKDAY ?>"<?php if ($r->hasRecurType(NAG_RECUR_MONTHLY_WEEKDAY)) echo ' checked="checked"' ?> /><label for="recurmonthweek"> <?php echo _("Monthly: Recurs every") ?>&nbsp;</label>
++ <input type="text" id="recur_week_of_month_interval" name="recur_week_of_month_interval" size="2" onkeypress="setRecur(4);" onchange="setRecur(4);" value="<?php echo $r->hasRecurType(NAG_RECUR_MONTHLY_WEEKDAY) ? $r->getRecurInterval() : '' ?>" />&nbsp;<?php echo Horde::label('recur_week_of_month_interval', _("month(s)") . ' ' . _("on the same weekday")) ?>
++ </td>
++ </tr>
++ <tr>
++ <td class="nowrap">
++ <input id="recuryear" type="radio" class="checkbox" name="recur" onclick="setInterval('recur_yearly_interval');" value="<?php echo NAG_RECUR_YEARLY_DATE ?>"<?php if ($r->hasRecurType(NAG_RECUR_YEARLY_DATE)) echo ' checked="checked"' ?> /><label for="recuryear"> <?php echo _("Yearly: Recurs every") ?>&nbsp;</label>
++ <input type="text" id="recur_yearly_interval" name="recur_yearly_interval" size="2" onkeypress="setRecur(5);" onchange="setRecur(5);" value="<?php echo $r->hasRecurType(NAG_RECUR_YEARLY_DATE) ? $r->getRecurInterval() : '' ?>" />&nbsp;<?php echo Horde::label('recur_yearly_interval', _("year(s) on the same date")) ?>
++ </td>
++ </tr>
++ <tr>
++ <td class="nowrap">
++ <input id="recuryearday" type="radio" class="checkbox" name="recur" onclick="setInterval('recur_yearly_day_interval');" value="<?php echo NAG_RECUR_YEARLY_DAY ?>"<?php if ($r->hasRecurType(NAG_RECUR_YEARLY_DAY)) echo ' checked="checked"' ?> /><label for="recuryearday"> <?php echo _("Yearly: Recurs every") ?>&nbsp;</label>
++ <input type="text" id="recur_yearly_day_interval" name="recur_yearly_day_interval" size="2" onkeypress="setRecur(6);" onchange="setRecur(6);" value="<?php echo $r->hasRecurType(NAG_RECUR_YEARLY_DAY) ? $r->getRecurInterval() : '' ?>" />&nbsp;<?php echo Horde::label('recur_yearly_day_interval', _("year(s) on the same day of the year")) ?>
++ </td>
++ </tr>
++ <tr>
++ <td class="nowrap">
++ <input id="recuryearweekday" type="radio" class="checkbox" name="recur" onclick="setInterval('recur_yearly_weekday_interval');" value="<?php echo NAG_RECUR_YEARLY_WEEKDAY ?>"<?php if ($r->hasRecurType(NAG_RECUR_YEARLY_WEEKDAY)) echo ' checked="checked"' ?> /><label for="recuryearweekday"> <?php echo _("Yearly: Recurs every") ?>&nbsp;</label>
++ <input type="text" id="recur_yearly_weekday_interval" name="recur_yearly_weekday_interval" size="2" onkeypress="setRecur(7);" onchange="setRecur(7);" value="<?php echo $r->hasRecurType(NAG_RECUR_YEARLY_WEEKDAY) ? $r->getRecurInterval() : '' ?>" />&nbsp;<?php echo Horde::label('recur_yearly_weekday_interval', _("year(s) on the same weekday and month of the year")) ?>
++ </td>
++ </tr>
++ </table>
++ </td>
++</tr>
++</table>
++<?php
++ }
++
++ function _renderVarInput_nag_recur_end($form, &$var, &$vars)
++ {
++ $var->type->getInfo($vars, $var, $recur_end);
++
++ require_once NAG_BASE . '/lib/Recurrence.php';
++
++ $r = &new Nag_Recurrence(time());
++ $recurrence = $vars->get('recurrence');
++ if ($recurrence && $recur_end) {
++ $recurrence = array_merge($recurrence, $recur_end);
++ }
++ $r->fromHash($recurrence);
++ if ($r->hasRecurEnd()) {
++ $end = $r->recurEnd;
++ } else {
++ if ($vars->exists('task_due')) {
++ $end = new Horde_Date($vars->get('task_due'));
++ } else {
++ $end = new Horde_Date(time());
++ }
++ }
++?>
++<table cellspacing="0">
++<tr>
++ <td colspan="3">
++ <input id="recurnoend" type="radio" class="checkbox" name="recur_enddate_type" value="none"<?php echo (($r->hasRecurEnd() || $r->hasRecurCount()) ? '' : ' checked="checked"') ?> /><label for="recurnoend"> <?php echo _("No end date") ?></label>
++ </td>
++<?php if ($GLOBALS['browser']->hasFeature('dom')): ?>
++ <td>&nbsp;</td>
++<?php endif; ?>
++</tr>
++<tr>
++ <td colspan="3">
++ <input type="radio" class="checkbox" id="recur_enddate_type" name="recur_enddate_type" value="date"<?php echo ($r->hasRecurEnd() ? ' checked="checked"' : '') ?> />
++ <label for="recur_enddate_year" class="hidden"><?php echo _("Recurrence End Year") ?></label>
++ <input name="recur_enddate[year]" value="<?php echo $end->year ?>" type="text" id="recur_enddate_year" size="4" maxlength="4" onkeypress="document.<?php echo $form->getName() ?>.recur_enddate_type[1].checked = true;" onchange="document.<?php echo $form->getName() ?>.recur_enddate_type[1].checked = true;" /> -
++ <label for="recur_enddate_month" class="hidden"><?php echo _("Recurrence End Month") ?></label>
++ <input name="recur_enddate[month]" value="<?php echo $end->month ?>" type="text" id="recur_enddate_month" size="4" maxlength="4" onkeypress="document.<?php echo $form->getName() ?>.recur_enddate_type[1].checked = true;" onchange="document.<?php echo $form->getName() ?>.recur_enddate_type[1].checked = true;" /> -
++ <label for="recur_enddate_day" class="hidden"><?php echo _("Recurrence End Day") ?></label>
++ <input name="recur_enddate[day]" value="<?php echo $end->mday ?>" type="text" id="recur_enddate_day" size="4" maxlength="4" onkeypress="document.<?php echo $form->getName() ?>.recur_enddate_type[1].checked = true;" onchange="document.<?php echo $form->getName() ?>.recur_enddate_type[1].checked = true;" />
++<?php if ($GLOBALS['browser']->hasFeature('dom')): ?>
++ </td>
++ <td>
++<?php
++ Horde::addScriptFile('open_calendar.js', 'horde');
++ echo Horde::link('#', _("Select a date"), '', '', 'openCalendar(\'recur_enddateimg\', \'recur_enddate\'); return false;') . Horde::img('calendar.png', _("Set recurrence end date"), 'id="recur_enddateimg"', $GLOBALS['registry']->getImageDir('horde')) . '</a>';
++endif;
++?>
++ </td>
++</tr>
++<tr>
++ <td colspan="3">
++ <input type="radio" class="checkbox" name="recur_enddate_type" value="count"<?php echo ($r->getRecurCount() ? ' checked="checked"' : '') ?> />
++ <input type="text" id="recur_count" name="recur_count" size="2" onkeypress="document.<?php echo $form->getName() ?>.recur_enddate_type[2].checked = true;" onchange="document.<?php echo $form->getName() ?>.recur_enddate_type[2].checked = true;" value="<?php if ($r->getRecurCount()) echo $r->getRecurCount() ?>" />
++ <label for="recur_count"><?php echo _("recurrences") ?></label>
++ </td>
++<?php if ($GLOBALS['browser']->hasFeature('dom')): ?>
++ <td>&nbsp;</td>
++<?php endif; ?>
++</tr>
++</table>
++<?php
++ }
+
+ function _renderVarInput_nag_start($form, &$var, &$vars)
+ {
+diff -r e16c159ea616 nag/list.php
+--- a/nag/list.php Fri Aug 08 13:11:07 2008 +0200
++++ b/nag/list.php Fri Aug 08 14:19:56 2008 +0200
+@@ -75,10 +75,24 @@
+ break;
+
+ default:
++ $tasklist = Util::getFormData('tasklist_id');
++ if ($tasklist) {
++ $share = $GLOBALS['nag_shares']->getShare($tasklist);
++ if (!is_a($share, 'PEAR_Error')) {
++ $notification->push(sprintf(_("Displaying all tasks from tasklist %s."), $share->get('name')));
++ $completed = 1;
++ } else {
++ $tasklist = null;
++ }
++ } else {
++ $completed = null;
++ }
+ /* Get the full, sorted task list. */
+ $tasks = Nag::listTasks($prefs->getValue('sortby'),
+ $prefs->getValue('sortdir'),
+- $prefs->getValue('altsortby'));
++ $prefs->getValue('altsortby'),
++ $tasklist,
++ $completed);
+ if (is_a($tasks, 'PEAR_Error')) {
+ $notification->push($tasks, 'horde.error');
+ $tasks = new Nag_Task();
+diff -r e16c159ea616 nag/task.php
+--- a/nag/task.php Fri Aug 08 13:11:07 2008 +0200
++++ b/nag/task.php Fri Aug 08 14:19:56 2008 +0200
+@@ -141,6 +141,19 @@
+ $cManager->add($info['category']['value']);
+ }
+
++ /* Handle recurrence. */
++ if ($info['recurrence'] && $info['due']) {
++ require_once NAG_BASE . '/lib/Recurrence.php';
++ $recurrence = &new Nag_Recurrence($info['due']);
++ $r = $info['recurrence'];
++ if ($info['recur_end']) {
++ $r = array_merge($r, $info['recur_end']);
++ }
++ $recurrence->fromHash($r);
++ } else {
++ $recurrence = null;
++ }
++
+ /* If a task id is set, we're modifying an existing task.
+ * Otherwise, we're adding a new task with the provided
+ * attributes. */
+@@ -154,7 +167,8 @@
+ $info['category']['value'],
+ $info['alarm'], $info['parent'],
+ (int)$info['private'],
+- Auth::getAuth());
++ Auth::getAuth(), null, null,
++ $recurrence);
+
+ if (!is_a($result, 'PEAR_Error') &&
+ $info['old_tasklist'] != $info['tasklist_id']) {
+@@ -191,7 +205,8 @@
+ $info['category']['value'],
+ $info['alarm'], null, $info['parent'],
+ (int)$info['private'],
+- Auth::getAuth());
++ Auth::getAuth(), null,
++ $recurrence);
+ }
+
+ /* Check our results. */
+@@ -221,12 +236,7 @@
+ if (is_a($share, 'PEAR_Error') || !$share->hasPermission(Auth::getAuth(), PERMS_EDIT)) {
+ $notification->push(sprintf(_("Access denied completing task %s."), $task->name), 'horde.error');
+ } else {
+- $task->completed = !$task->completed;
+- if ($task->completed) {
+- $task->completed_date = time();
+- } else {
+- $task->completed_date = null;
+- }
++ $task->toggleComplete();
+ $result = $task->save();
+ if (is_a($result, 'PEAR_Error')) {
+ $notification->push(sprintf(_("There was a problem completing %s: %s"),
+@@ -234,6 +244,13 @@
+ } else {
+ if ($task->completed) {
+ $notification->push(sprintf(_("Completed %s."), $task->name), 'horde.success');
++ } else if ($task->recurs()) {
++ if (!empty($task->start)) {
++ $delay = sprintf(_(" (Delayed until %s)"), Nag::formatDate($task->start, false));
++ } else {
++ $delay = '';
++ }
++ $notification->push(sprintf(_("Next task recurrence due at %s" . $delay . "."), Nag::formatDate($task->due)), 'horde.success');
+ } else {
+ $notification->push(sprintf(_("%s is now incomplete."), $task->name), 'horde.success');
+ }
+diff -r e16c159ea616 nag/templates/list/task_summaries.inc
+--- a/nag/templates/list/task_summaries.inc Fri Aug 08 13:11:07 2008 +0200
++++ b/nag/templates/list/task_summaries.inc Fri Aug 08 14:19:56 2008 +0200
+@@ -4,7 +4,7 @@
+ if (!is_a($share, 'PEAR_Error') &&
+ $share->hasPermission(Auth::getAuth(), PERMS_EDIT)) {
+ if (!$task->completed) {
+- if (!$task->childrenCompleted()) {
++ if (!$task->childrenCompleted() && !$task->recurs()) {
+ $label = _("Incomplete sub tasks, complete them first");
+ echo Horde::img('unchecked.png', $label, array('title' => $label));
+ } else {
+diff -r e16c159ea616 nag/templates/view/task.inc
+--- a/nag/templates/view/task.inc Fri Aug 08 13:11:07 2008 +0200
++++ b/nag/templates/view/task.inc Fri Aug 08 14:19:56 2008 +0200
+@@ -66,6 +66,54 @@
+ </tr>
+ <?php endif; ?>
+
++<?php if ($task->recurs()): ?>
++<!-- recurrence -->
++<tr>
++ <td colspan="2" class="control"><strong><?php echo _("Recurrence") ?></strong></td>
++</tr>
++<tr>
++ <td class="rightAlign" valign="top"><strong><?php echo _("Pattern") ?>&nbsp;&nbsp;</strong></td>
++ <td valign="top">
++<?php if ($task->recurrence->hasRecurType(NAG_RECUR_DAILY)): ?>
++ <?php echo _("Daily: Recurs every") . ' ' . $task->recurrence->getRecurInterval() . ' ' . _("day(s)") ?>
++<?php elseif ($task->recurrence->hasRecurType(NAG_RECUR_WEEKLY)):
++ $weekdays = array();
++ if ($task->recurrence->recurOnDay(HORDE_DATE_MASK_MONDAY)) $weekdays[] = _("Monday");
++ if ($task->recurrence->recurOnDay(HORDE_DATE_MASK_TUESDAY)) $weekdays[] = _("Tuesday");
++ if ($task->recurrence->recurOnDay(HORDE_DATE_MASK_WEDNESDAY)) $weekdays[] = _("Wednesday");
++ if ($task->recurrence->recurOnDay(HORDE_DATE_MASK_THURSDAY)) $weekdays[] = _("Thursday");
++ if ($task->recurrence->recurOnDay(HORDE_DATE_MASK_FRIDAY)) $weekdays[] = _("Friday");
++ if ($task->recurrence->recurOnDay(HORDE_DATE_MASK_SATURDAY)) $weekdays[] = _("Saturday");
++ if ($task->recurrence->recurOnDay(HORDE_DATE_MASK_SUNDAY)) $weekdays[] = _("Sunday");
++ echo _("Weekly: Recurs every") . ' ' . $task->recurrence->getRecurInterval() . ' ' . _("week(s) on:") . ' ' . implode(', ', $weekdays) ?>
++<?php elseif ($task->recurrence->hasRecurType(NAG_RECUR_MONTHLY_DATE)): ?>
++ <?php echo _("Monthly: Recurs every") . ' ' . $task->recurrence->getRecurInterval() . ' ' . _("month(s)") . ' ' . _("on the same date") ?>
++<?php elseif ($task->recurrence->hasRecurType(NAG_RECUR_MONTHLY_WEEKDAY)): ?>
++ <?php echo _("Monthly: Recurs every") . ' ' . $task->recurrence->getRecurInterval() . ' ' . _("month(s)") . ' ' . _("on the same weekday") ?>
++<?php elseif ($task->recurrence->hasRecurType(NAG_RECUR_YEARLY_DATE)): ?>
++ <?php echo _("Yearly: Recurs every") . ' ' . $task->recurrence->getRecurInterval() . ' ' . _("year(s) on the same date") ?>
++<?php elseif ($task->recurrence->hasRecurType(NAG_RECUR_YEARLY_DAY)): ?>
++ <?php echo _("Yearly: Recurs every") . ' ' . $task->recurrence->getRecurInterval() . ' ' . _("year(s) on the same day of the year") ?>
++<?php elseif ($task->recurrence->hasRecurType(NAG_RECUR_YEARLY_WEEKDAY)): ?>
++ <?php echo _("Yearly: Recurs every") . ' ' . $task->recurrence->getRecurInterval() . ' ' . _("year(s) on the same weekday and month of the year") ?>
++<?php endif; ?>
++ </td>
++</tr>
++
++<!-- recur end date -->
++<tr>
++ <td class="rightAlign"><strong><?php echo _("Recur Until") ?>&nbsp;&nbsp;</strong></td>
++ <td><?php echo $task->recurrence->hasRecurEnd() ? strftime($prefs->getValue('date_format'), $task->recurrence->recurEnd->timestamp()) . date($prefs->getValue('twentyFour') ? ' G:i' : ' g:i a', $task->recurrence->recurEnd->timestamp()) : ($task->recurrence->getRecurCount() ? sprintf(_("%d times"), $task->recurrence->getRecurCount()) : _("No end date")) ?></td>
++</tr>
++
++<?php if ($task->recurrence->getExceptions()): ?>
++<!-- exceptions -->
++<tr>
++ <td class="rightAlign"><strong><?php echo _("Exceptions") ?>&nbsp;&nbsp;</strong></td>
++ <td><?php echo $task->exceptionsList(); ?></td>
++</tr>
++<?php endif; endif; ?>
++
+ <?php if (strlen($task->desc)): ?>
+ <tr>
+ <td colspan="2" class="taskBody">
+diff -r e16c159ea616 storage/.htaccess
+--- /dev/null Thu Jan 01 00:00:00 1970 +0000
++++ b/storage/.htaccess Fri Aug 08 14:19:56 2008 +0200
+@@ -0,0 +1,1 @@
++Deny from All
+\ No newline at end of file
+diff -r e16c159ea616 tmp/.htaccess
+--- /dev/null Thu Jan 01 00:00:00 1970 +0000
++++ b/tmp/.htaccess Fri Aug 08 14:19:56 2008 +0200
+@@ -0,0 +1,1 @@
++Deny from All
+\ No newline at end of file
+diff -r e16c159ea616 turba/config/conf.php
+--- a/turba/config/conf.php Fri Aug 08 13:11:07 2008 +0200
++++ b/turba/config/conf.php Fri Aug 08 14:19:56 2008 +0200
+@@ -3,8 +3,8 @@
+ // $Horde: turba/config/conf.xml,v 1.6.2.5 2008/05/06 21:26:59 bklang Exp $
+ $conf['menu']['import_export'] = true;
+ $conf['menu']['apps'] = array();
+-$conf['client']['addressbook'] = 'localsql';
+-$conf['shares']['source'] = 'localsql';
++$conf['client']['addressbook'] = 'INBOX%2FClients';
++$conf['shares']['source'] = 'kolab';
+ $conf['comments']['allow'] = true;
+-$conf['documents']['type'] = 'none';
++$conf['documents']['type'] = 'horde';
+ /* CONFIG END. DO NOT CHANGE ANYTHING IN OR BEFORE THIS LINE. */
+diff -r e16c159ea616 turba/config/sources.php
+--- a/turba/config/sources.php Fri Aug 08 13:11:07 2008 +0200
++++ b/turba/config/sources.php Fri Aug 08 14:19:56 2008 +0200
+@@ -149,6 +149,8 @@
+ * Here are some example configurations:
+ */
+
++if (empty($GLOBALS['conf']['kolab']['enabled'])) {
++
+ /**
+ * A local address book in an SQL database. This implements a private
+ * per-user address book. Sharing of this source with other users may be
+@@ -260,3 +262,233 @@
+ 'use_shares' => true,
+ 'list_name_field' => 'lastname',
+ );
++
++}
++
++/* Begin Kolab sources. */
++if (!empty($GLOBALS['conf']['kolab']['enabled'])) {
++
++ /* Only use LDAP if we have that extension in PHP */
++ if (function_exists('ldap_connect')) {
++ require_once 'Horde/Kolab.php';
++
++ if (!is_callable('Kolab', 'getServer')) {
++ $_kolab_server = $GLOBALS['conf']['kolab']['ldap']['server'];
++ } else {
++ $_kolab_server = Kolab::getServer('ldap');
++ }
++
++ /* A global address book for a Kolab Server. This is typically a
++ * read-only public directory, stored in the default Kolab LDAP server.
++ * The user accessing this should have read permissions to the shared
++ * directory in LDAP. */
++ $cfgSources['kolab_global'] = array(
++ 'title' => _("Global Address Book"),
++ 'type' => 'ldap',
++ 'params' => array(
++ 'server' => $_kolab_server,
++ 'port' => $GLOBALS['conf']['kolab']['ldap']['port'],
++ 'tls' => false,
++ 'root' => $GLOBALS['conf']['kolab']['ldap']['basedn'],
++ 'sizelimit' => 200,
++ 'dn' => array('cn'),
++ 'objectclass' => array(
++ 'inetOrgPerson'
++ ),
++ 'scope' => 'sub',
++ 'charset' => 'utf-8',
++ 'version' => 3,
++ 'bind_dn' => '',
++ 'bind_password' => '',
++ ),
++ 'map' => array(
++ '__key' => 'dn',
++ 'name' => 'cn',
++ 'firstname' => 'givenName',
++ 'lastname' => 'sn',
++ 'email' => 'mail',
++ 'alias' => 'alias',
++ 'title' => 'title',
++ 'company' => 'o',
++ 'workStreet' => 'street',
++ 'workCity' => 'l',
++ 'workProvince' => 'st',
++ 'workPostalCode' => 'postalCode',
++ 'workCountry' => 'c',
++ 'homePhone' => 'homePhone',
++ 'workPhone' => 'telephoneNumber',
++ 'cellPhone' => 'mobile',
++ 'fax' => 'fax',
++ 'notes' => 'description',
++ 'freebusyUrl' => 'kolabHomeServer',
++ ),
++ 'search' => array(
++ 'name',
++ 'firstname',
++ 'lastname',
++ 'email',
++ 'title',
++ 'company',
++ 'workAddress',
++ 'workCity',
++ 'workProvince',
++ 'workPostalCode',
++ 'workCountry',
++ 'homePhone',
++ 'workPhone',
++ 'cellPhone',
++ 'fax',
++ 'notes',
++ ),
++ 'strict' => array(
++ 'dn',
++ ),
++ 'export' => true,
++ 'browse' => true,
++ );
++ }
++
++ /**
++ * The local address books for a Kolab user. These are stored in specially
++ * flagged contact folder within the users Cyrus IMAP mailbox.
++ *
++ * Still missing attributes are:
++ *
++ * picture, sensitivity
++ */
++
++ $cfgSources['kolab'] = array(
++ 'title' => _("Contacts"),
++ 'type' => 'kolab',
++ 'params' => array(
++ 'charset' => 'utf-8',
++ ),
++ 'list_name_field' => 'lastname',
++ 'map' => array(
++ '__key' => 'uid',
++ '__uid' => 'uid',
++ '__type' => '__type',
++ '__members' => '__members',
++ /* Personal */
++ 'name' => array('fields' => array('firstname', 'middlenames', 'lastname'),
++ 'format' => '%s %s %s',
++ 'attribute' => 'full-name'),
++ 'firstname' => 'given-name',
++ 'lastname' => 'last-name',
++ 'middlenames' => 'middle-names',
++ 'namePrefix' => 'prefix',
++ 'nameSuffix' => 'suffix',
++ 'initials' => 'initials',
++ 'nickname' => 'nick-name',
++ 'gender' => 'gender',
++ 'birthday' => 'birthday',
++ 'spouse' => 'spouse-name',
++ 'anniversary' => 'anniversary',
++ 'children' => 'children',
++ /* Location */
++ 'workStreet' => 'addr-business-street',
++ 'workCity' => 'addr-business-locality',
++ 'workProvince' => 'addr-business-region',
++ 'workPostalCode' => 'addr-business-postal-code',
++ 'workCountry' => 'addr-business-country',
++ 'homeStreet' => 'addr-home-street',
++ 'homeCity' => 'addr-home-locality',
++ 'homeProvince' => 'addr-home-region',
++ 'homePostalCode' => 'addr-home-postal-code',
++ 'homeCountry' => 'addr-home-country',
++ /* Communications */
++ 'emails' => 'emails',
++ 'homePhone' => 'phone-home1',
++ 'workPhone' => 'phone-business1',
++ 'cellPhone' => 'phone-mobile',
++ 'fax' => 'phone-businessfax',
++ 'instantMessenger' => 'im-address',
++ /* Organization */
++ 'title' => 'job-title',
++ 'role' => 'profession',
++ 'company' => 'organization',
++ 'department' => 'department',
++ 'office' => 'office-location',
++ 'manager' => 'manager-name',
++ 'assistant' => 'assistant',
++ /* Other */
++ 'category' => 'categories',
++ 'notes' => 'body',
++ 'website' => 'web-page',
++ 'freebusyUrl' => 'free-busy-url',
++ 'language' => 'language',
++ 'latitude' => 'latitude',
++ 'longitude' => 'longitude',
++ ),
++ 'tabs' => array(
++ _("Personal") => array('name', 'firstname', 'lastname', 'middlenames',
++ 'namePrefix', 'nameSuffix', 'initials', 'nickname',
++ 'gender', 'birthday', 'spouse', 'anniversary',
++ 'children'),
++ _("Location") => array('homeStreet', 'homeCity', 'homeProvince',
++ 'homePostalCode', 'homeCountry', 'workStreet',
++ 'workCity', 'workProvince', 'workPostalCode',
++ 'workCountry'),
++ _("Communications") => array('emails', 'homePhone', 'workPhone',
++ 'cellPhone', 'fax', 'instantMessenger'),
++ _("Organization") => array('title', 'role', 'company', 'department',
++ 'office', 'manager', 'assistant'),
++ _("Other") => array('category', 'notes', 'website', 'freebusyUrl',
++ 'language', 'latitude', 'longitude'),
++ ),
++ 'search' => array(
++ /* Personal */
++ 'firstname',
++ 'lastname',
++ 'middlenames',
++ 'namePrefix',
++ 'nameSuffix',
++ 'initials',
++ 'nickname',
++ 'gender',
++ 'birthday',
++ 'spouse',
++ 'anniversary',
++ 'children',
++ /* Location */
++ 'workStreet',
++ 'workCity',
++ 'workProvince',
++ 'workPostalCode',
++ 'workCountry',
++ 'homeStreet',
++ 'homeCity',
++ 'homeProvince',
++ 'homePostalCode',
++ 'homeCountry',
++ /* Communications */
++ 'emails',
++ 'homePhone',
++ 'workPhone',
++ 'cellPhone',
++ 'fax',
++ 'instantMessenger',
++ /* Organization */
++ 'title',
++ 'role',
++ 'company',
++ 'department',
++ 'office',
++ 'manager',
++ 'assistant',
++ /* Other */
++ 'category',
++ 'notes',
++ 'website',
++ 'language',
++ ),
++ 'strict' => array(
++ 'uid',
++ ),
++ 'export' => true,
++ 'browse' => true,
++ 'use_shares' => true,
++ 'shares_only' => true,
++ );
++}
++/* End Kolab sources. */
+diff -r e16c159ea616 turba/lib/Driver.php
+--- a/turba/lib/Driver.php Fri Aug 08 13:11:07 2008 +0200
++++ b/turba/lib/Driver.php Fri Aug 08 14:19:56 2008 +0200
+@@ -1368,9 +1368,6 @@
+ $hash = array();
+ $attr = $vcard->getAllAttributes();
+ foreach ($attr as $item) {
+- if (empty($item['value'])) {
+- continue;
+- }
+
+ switch ($item['name']) {
+ case 'FN':
+diff -r e16c159ea616 turba/lib/Driver/share.php
+--- a/turba/lib/Driver/share.php Fri Aug 08 13:11:07 2008 +0200
++++ b/turba/lib/Driver/share.php Fri Aug 08 14:19:56 2008 +0200
+@@ -145,7 +145,7 @@
+ function _deleteAll($sourceName = null)
+ {
+ if (is_null($sourceName)) {
+- $sourceName = $this->getContactOwner();
++ $sourceName = $this->getName();
+ }
+ return $this->_driver->_deleteAll($sourceName);
+ }
diff --git a/www-apps/horde-webmail/files/postinstall-en.txt.kolab b/www-apps/horde-webmail/files/postinstall-en.txt.kolab
new file mode 100644
index 000000000000..95bff6b4b650
--- /dev/null
+++ b/www-apps/horde-webmail/files/postinstall-en.txt.kolab
@@ -0,0 +1,15 @@
+Congratulations, you nearly finished installing the Kolab web client.
+
+All you need to do in order to finish the setup of the groupware web
+client is to enter the details of your Kolab server in the two files
+
+ ${MY_INSTALLDIR}/config/kolab.php
+ ${MY_INSTALLDIR}/kronolith/config/kolab.php
+
+Just follow the comments in these files.
+
+Once you are finished you should be able to log into the new site at
+
+ http://${VHOST_HOSTNAME}/${VHOST_APPDIR}/
+
+Enjoy!
diff --git a/www-apps/horde-webmail/files/reconfig.kolab b/www-apps/horde-webmail/files/reconfig.kolab
new file mode 100644
index 000000000000..dbf053428f61
--- /dev/null
+++ b/www-apps/horde-webmail/files/reconfig.kolab
@@ -0,0 +1,15 @@
+#!/bin/bash
+
+die() {
+ echo "#####"
+ echo $1
+ echo "#####"
+ exit 1
+}
+
+if [ $1 = "install" ]; then
+ sqlite ${MY_INSTALLDIR}/storage/horde.db < ${MY_INSTALLDIR}/scripts/sql/groupware.sql
+ chown ${VHOST_SERVER_UID}\:${VHOST_SERVER_GID} ${MY_INSTALLDIR}/storage/horde.db ${MY_INSTALLDIR}/{log,tmp,storage}
+elif [ $1 = "clean" ]; then
+ echo $1
+fi