diff --git a/dev/Admin/Contacts.js b/dev/Admin/Contacts.js index e0b440b6f..6d89d53d7 100644 --- a/dev/Admin/Contacts.js +++ b/dev/Admin/Contacts.js @@ -10,6 +10,7 @@ function AdminContacts() this.defautOptionsAfterRender = Utils.defautOptionsAfterRender; this.enableContacts = ko.observable(!!RL.settingsGet('ContactsEnable')); this.contactsSharing = ko.observable(!!RL.settingsGet('ContactsSharing')); + this.contactsSync = ko.observable(!!RL.settingsGet('ContactsSync')); var aTypes = ['sqlite', 'mysql', 'pgsql'], @@ -183,6 +184,12 @@ AdminContacts.prototype.onBuild = function () 'ContactsSharing': bValue ? '1' : '0' }); }); + + self.contactsSync.subscribe(function (bValue) { + RL.remote().saveAdminConfig(null, { + 'ContactsSync': bValue ? '1' : '0' + }); + }); self.contactsType.subscribe(function (sValue) { RL.remote().saveAdminConfig(f5, { diff --git a/dev/Models/ContactModel.js b/dev/Models/ContactModel.js index e190cbaa6..d72ee8eb2 100644 --- a/dev/Models/ContactModel.js +++ b/dev/Models/ContactModel.js @@ -6,6 +6,7 @@ function ContactModel() { this.idContact = 0; + this.idContactStr = ''; this.display = ''; this.properties = []; this.readOnly = false; @@ -57,6 +58,7 @@ ContactModel.prototype.parse = function (oItem) if (oItem && 'Object/Contact' === oItem['@Object']) { this.idContact = Utils.pInt(oItem['IdContact']); + this.idContactStr = Utils.pString(oItem['IdContactStr']); this.display = Utils.pString(oItem['Display']); this.readOnly = !!oItem['ReadOnly']; this.scopeType = Utils.pInt(oItem['ScopeType']); diff --git a/dev/Storages/WebMailAjaxRemote.js b/dev/Storages/WebMailAjaxRemote.js index ad0a2b4f3..ab5b450ea 100644 --- a/dev/Storages/WebMailAjaxRemote.js +++ b/dev/Storages/WebMailAjaxRemote.js @@ -570,11 +570,12 @@ WebMailAjaxRemoteStorage.prototype.contacts = function (fCallback, iOffset, iLim /** * @param {?Function} fCallback */ -WebMailAjaxRemoteStorage.prototype.contactSave = function (fCallback, sRequestUid, sUid, nScopeType, aProperties) +WebMailAjaxRemoteStorage.prototype.contactSave = function (fCallback, sRequestUid, sUid, sUidStr, nScopeType, aProperties) { this.defaultRequest(fCallback, 'ContactSave', { 'RequestUid': sRequestUid, 'Uid': Utils.trim(sUid), + 'UidStr': Utils.trim(sUidStr), 'ScopeType': nScopeType, 'Properties': aProperties }); diff --git a/dev/ViewModels/PopupsContactsViewModel.js b/dev/ViewModels/PopupsContactsViewModel.js index 60d9b8a40..34dea21ef 100644 --- a/dev/ViewModels/PopupsContactsViewModel.js +++ b/dev/ViewModels/PopupsContactsViewModel.js @@ -48,6 +48,7 @@ function PopupsContactsViewModel() this.viewClearSearch = ko.observable(false); this.viewID = ko.observable(''); + this.viewIDStr = ko.observable(''); this.viewReadOnly = ko.observable(false); this.viewScopeType = ko.observable(Enums.ContactScopeType.Default); this.viewProperties = ko.observableArray([]); @@ -251,13 +252,14 @@ function PopupsContactsViewModel() self.viewID(Utils.pInt(oData.Result.ResultID)); } + if ('' === self.viewIDStr()) + { + self.viewIDStr(Utils.pString(oData.Result.ResultIDStr)); + } + self.reloadContactList(); bRes = true; } -// else -// { -// // TODO -// } _.delay(function () { self.viewSaveTrigger(bRes ? Enums.SaveSettingsStep.TrueResult : Enums.SaveSettingsStep.FalseResult); @@ -270,7 +272,7 @@ function PopupsContactsViewModel() }, 1000); } - }, sRequestUid, this.viewID(), this.viewScopeType(), aProperties); + }, sRequestUid, this.viewID(), this.viewIDStr(), this.viewScopeType(), aProperties); }, function () { var @@ -414,6 +416,7 @@ PopupsContactsViewModel.prototype.populateViewContact = function (oContact) { var sId = '', + sIdStr = '', bHasName = false, aList = [] ; @@ -427,6 +430,7 @@ PopupsContactsViewModel.prototype.populateViewContact = function (oContact) if (oContact) { sId = oContact.idContact; + sIdStr = oContact.idContactStr; if (Utils.isNonEmptyArray(oContact.properties)) { @@ -454,6 +458,7 @@ PopupsContactsViewModel.prototype.populateViewContact = function (oContact) } this.viewID(sId); + this.viewIDStr(sIdStr); this.viewProperties([]); this.viewProperties(aList); diff --git a/rainloop/v/0.0.0/app/libraries/RainLoop/Actions.php b/rainloop/v/0.0.0/app/libraries/RainLoop/Actions.php index 9912bafbf..10456266e 100644 --- a/rainloop/v/0.0.0/app/libraries/RainLoop/Actions.php +++ b/rainloop/v/0.0.0/app/libraries/RainLoop/Actions.php @@ -995,6 +995,7 @@ class Actions $aResult['ContactsEnable'] = (bool) $oConfig->Get('contacts', 'enable', false); $aResult['ContactsSharing'] = (bool) $oConfig->Get('contacts', 'allow_sharing', false); + $aResult['ContactsSync'] = (bool) $oConfig->Get('contacts', 'allow_carddav_sync', false); $aResult['ContactsPdoType'] = $this->ValidateContactPdoType(\trim($this->Config()->Get('contacts', 'type', 'sqlite'))); $aResult['ContactsPdoDsn'] = (string) $oConfig->Get('contacts', 'pdo_dsn', ''); $aResult['ContactsPdoType'] = (string) $oConfig->Get('contacts', 'type', ''); @@ -1875,6 +1876,7 @@ class Actions $this->setConfigFromParams($oConfig, 'ContactsEnable', 'contacts', 'enable', 'bool'); $this->setConfigFromParams($oConfig, 'ContactsSharing', 'contacts', 'allow_sharing', 'bool'); + $this->setConfigFromParams($oConfig, 'ContactsSync', 'contacts', 'allow_carddav_sync', 'bool'); $this->setConfigFromParams($oConfig, 'ContactsPdoDsn', 'contacts', 'pdo_dsn', 'string'); $this->setConfigFromParams($oConfig, 'ContactsPdoUser', 'contacts', 'pdo_user', 'string'); $this->setConfigFromParams($oConfig, 'ContactsPdoPassword', 'contacts', 'pdo_password', 'dummy'); @@ -4207,7 +4209,7 @@ class Actions if ($this->PersonalAddressBookProvider($oAccount)->IsActive()) { $iCount = 0; - $mResult = $this->PersonalAddressBookProvider($oAccount)->GetContacts($oAccount, + $mResult = $this->PersonalAddressBookProvider($oAccount)->GetContacts($oAccount->ParentEmailHelper(), $iOffset, $iLimit, $sSearch, $iCount); } @@ -4236,7 +4238,7 @@ class Actions $bResult = false; if (0 < \count($aFilteredUids) && $this->PersonalAddressBookProvider($oAccount)->IsActive()) { - $bResult = $this->PersonalAddressBookProvider($oAccount)->DeleteContacts($oAccount, $aFilteredUids); + $bResult = $this->PersonalAddressBookProvider($oAccount)->DeleteContacts($oAccount->ParentEmailHelper(), $aFilteredUids); } return $this->DefaultResponse(__FUNCTION__, $bResult); @@ -4256,6 +4258,7 @@ class Actions if ($oPab && $oPab->IsActive() && 0 < \strlen($sRequestUid)) { $sUid = \trim($this->GetActionParam('Uid', '')); + $sUidStr = \trim($this->GetActionParam('UidStr', '')); $iScopeType = (int) $this->GetActionParam('ScopeType', null); if (!in_array($iScopeType, array( \RainLoop\Providers\PersonalAddressBook\Enumerations\ScopeType::DEFAULT_, @@ -4269,6 +4272,10 @@ class Actions { $oContact->IdContact = $sUid; } + if (0 < \strlen($sUidStr)) + { + $oContact->IdContactStr = $sUidStr; + } $oContact->ScopeType = $iScopeType; @@ -4290,12 +4297,13 @@ class Actions } } - $bResult = $oPab->ContactSave($oAccount, $oContact); + $bResult = $oPab->ContactSave($oAccount->ParentEmailHelper(), $oContact); } return $this->DefaultResponse(__FUNCTION__, array( 'RequestUid' => $sRequestUid, 'ResultID' => $bResult ? $oContact->IdContact : '', + 'ResultIDStr' => $bResult ? $oContact->IdContactStr : '', 'Result' => $bResult )); } @@ -4328,7 +4336,7 @@ class Actions $oPab = $this->PersonalAddressBookProvider($oAccount); if ($oPab && $oPab->IsActive()) { - $aSuggestions = $oPab->GetSuggestions($oAccount, $sQuery, $iLimit); + $aSuggestions = $oPab->GetSuggestions($oAccount->ParentEmailHelper(), $sQuery, $iLimit); if (0 === \count($aResult)) { $aResult = $aSuggestions; @@ -5938,6 +5946,7 @@ class Actions $mResult = \array_merge($this->objectData($mResponse, $sParent, $aParameters), array( /* @var $mResponse \RainLoop\Providers\PersonalAddressBook\Classes\Contact */ 'IdContact' => $mResponse->IdContact, + 'IdContactStr' => $mResponse->IdContactStr, 'Display' => \MailSo\Base\Utils::Utf8Clear($mResponse->Display), 'ReadOnly' => $mResponse->ReadOnly, 'ScopeType' => $mResponse->ScopeType, diff --git a/rainloop/v/0.0.0/app/libraries/RainLoop/Common/PdoAbstract.php b/rainloop/v/0.0.0/app/libraries/RainLoop/Common/PdoAbstract.php index a7d958cd8..120a62b31 100644 --- a/rainloop/v/0.0.0/app/libraries/RainLoop/Common/PdoAbstract.php +++ b/rainloop/v/0.0.0/app/libraries/RainLoop/Common/PdoAbstract.php @@ -229,12 +229,18 @@ abstract class PdoAbstract /** * @param string $sEmail * @param bool $bSkipInsert = false - * @param bool $bSkipUpdateTables = false + * @param bool $bCache = true * * @return int */ - protected function getUserId($sEmail, $bSkipInsert = false) + protected function getUserId($sEmail, $bSkipInsert = false, $bCache = true) { + static $aCache = array(); + if ($bCache && isset($aCache[$sEmail])) + { + return $aCache[$sEmail]; + } + $sEmail = \strtolower(\trim($sEmail)); if (empty($sEmail)) { @@ -242,20 +248,38 @@ abstract class PdoAbstract } $oStmt = $this->prepareAndExecute('SELECT id_user FROM rainloop_users WHERE rl_email = :rl_email', - array(':rl_email' => array($sEmail, \PDO::PARAM_STR))); + array( + ':rl_email' => array($sEmail, \PDO::PARAM_STR) + ) + ); $mRow = $oStmt->fetch(\PDO::FETCH_ASSOC); if ($mRow && isset($mRow['id_user']) && \is_numeric($mRow['id_user'])) { - return (int) $mRow['id_user']; + $iResult = (int) $mRow['id_user']; + if (0 >= $iResult) + { + throw new \Exception('id_user <= 0'); + } + + if ($bCache) + { + $aCache[$sEmail] = $iResult; + } + + return $iResult; } if (!$bSkipInsert) { $oStmt->closeCursor(); - $oStmt = $this->prepareAndExecute('INSERT INTO rainloop_users (rl_email) VALUES (:rl_email)', - array(':rl_email' => array($sEmail, \PDO::PARAM_STR))); + $oStmt = $this->prepareAndExecute('INSERT INTO rainloop_users (rl_email, rl_hash) VALUES (:rl_email, :rl_hash)', + array( + ':rl_email' => array($sEmail, \PDO::PARAM_STR), + ':rl_hash' => array(\md5($sEmail.\microtime(true)), \PDO::PARAM_STR) + ) + ); return $this->getUserId($sEmail, true); } @@ -374,6 +398,8 @@ abstract class PdoAbstract } /** + * @param bool $bWatchVersion = true + * * @throws \Exception */ protected function initSystemTables() @@ -384,55 +410,51 @@ abstract class PdoAbstract if ($oPdo) { $aQ = array(); - if ('mysql' === $this->sDbType) + switch ($this->sDbType) { - $aQ[] = 'CREATE TABLE IF NOT EXISTS rainloop_system ( - sys_name varchar(50) NOT NULL, - value_int int UNSIGNED NOT NULL DEFAULT 0, - value_str varchar(128) NOT NULL DEFAULT \'\', - INDEX sys_name_rainloop_system_index (sys_name) -) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;'; - - $aQ[] = 'CREATE TABLE IF NOT EXISTS rainloop_users ( - id_user int UNSIGNED NOT NULL AUTO_INCREMENT, - rl_email varchar(128) NOT NULL DEFAULT \'\', - PRIMARY KEY(id_user), - INDEX rl_email_rainloop_users_index (rl_email) + case 'mysql': + $aQ[] = 'CREATE TABLE IF NOT EXISTS rainloop_system ( +sys_name varchar(50) NOT NULL, +value_int int UNSIGNED NOT NULL DEFAULT 0, +value_str varchar(128) NOT NULL DEFAULT \'\', +INDEX sys_name_rainloop_system_index (sys_name) +) /*!40000 ENGINE=INNODB *//*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;'; + $aQ[] = 'CREATE TABLE IF NOT EXISTS rainloop_users ( +id_user int UNSIGNED NOT NULL AUTO_INCREMENT, +rl_email varchar(128) NOT NULL DEFAULT \'\', +PRIMARY KEY(id_user), +INDEX rl_email_rainloop_users_index (rl_email) ) /*!40000 ENGINE=INNODB */;'; - } - else if ('pgsql' === $this->sDbType) - { - $aQ[] = 'CREATE TABLE rainloop_system ( - sys_name varchar(50) NOT NULL, - value_int integer NOT NULL DEFAULT 0, - value_str varchar(128) NOT NULL DEFAULT \'\' -);'; + break; - $aQ[] = 'CREATE INDEX sys_name_rainloop_system_index ON rainloop_system (sys_name);'; - - $aQ[] = 'CREATE SEQUENCE id_user START WITH 1 INCREMENT BY 1 NO MAXVALUE NO MINVALUE CACHE 1;'; - - $aQ[] = 'CREATE TABLE rainloop_users ( - id_user integer DEFAULT nextval(\'id_user\'::text) PRIMARY KEY, - rl_email varchar(128) NOT NULL DEFAULT \'\' + case 'pgsql': + $aQ[] = 'CREATE TABLE rainloop_system ( +sys_name varchar(50) NOT NULL, +value_int integer NOT NULL DEFAULT 0, +value_str varchar(128) NOT NULL DEFAULT \'\' );'; - $aQ[] = 'CREATE INDEX rl_email_rainloop_users_index ON rainloop_users (rl_email);'; - } - else if ('sqlite' === $this->sDbType) - { - $aQ[] = 'CREATE TABLE rainloop_system ( - sys_name text NOT NULL, - value_int integer NOT NULL default 0, - value_str text NOT NULL default \'\' + $aQ[] = 'CREATE INDEX sys_name_rainloop_system_index ON rainloop_system (sys_name);'; + $aQ[] = 'CREATE SEQUENCE id_user START WITH 1 INCREMENT BY 1 NO MAXVALUE NO MINVALUE CACHE 1;'; + $aQ[] = 'CREATE TABLE rainloop_users ( +id_user integer DEFAULT nextval(\'id_user\'::text) PRIMARY KEY, +rl_email varchar(128) NOT NULL DEFAULT \'\' );'; + $aQ[] = 'CREATE INDEX rl_email_rainloop_users_index ON rainloop_users (rl_email);'; + break; - $aQ[] = 'CREATE INDEX sys_name_rainloop_system_index ON rainloop_system (sys_name);'; - - $aQ[] = 'CREATE TABLE rainloop_users ( - id_user integer NOT NULL PRIMARY KEY, - rl_email text NOT NULL default \'\' + case 'sqlite': + $aQ[] = 'CREATE TABLE rainloop_system ( +sys_name text NOT NULL, +value_int integer NOT NULL DEFAULT 0, +value_str text NOT NULL DEFAULT \'\' );'; - $aQ[] = 'CREATE INDEX rl_email_rainloop_users_index ON rainloop_users (rl_email);'; + $aQ[] = 'CREATE INDEX sys_name_rainloop_system_index ON rainloop_system (sys_name);'; + $aQ[] = 'CREATE TABLE rainloop_users ( +id_user integer NOT NULL PRIMARY KEY, +rl_email text NOT NULL DEFAULT \'\' +);'; + $aQ[] = 'CREATE INDEX rl_email_rainloop_users_index ON rainloop_users (rl_email);'; + break; } if (0 < \count($aQ)) diff --git a/rainloop/v/0.0.0/app/libraries/RainLoop/Config/Application.php b/rainloop/v/0.0.0/app/libraries/RainLoop/Config/Application.php index 390b89ce2..b9dd4e888 100644 --- a/rainloop/v/0.0.0/app/libraries/RainLoop/Config/Application.php +++ b/rainloop/v/0.0.0/app/libraries/RainLoop/Config/Application.php @@ -84,6 +84,7 @@ class Application extends \RainLoop\Config\AbstractConfig 'contacts' => array( 'enable' => array(false, 'Enable contacts'), 'allow_sharing' => array(true), + 'allow_carddav_sync' => array(false), 'suggestions_limit' => array(30), 'type' => array('sqlite', ''), 'pdo_dsn' => array('mysql:host=127.0.0.1;port=3306;dbname=rainloop', ''), diff --git a/rainloop/v/0.0.0/app/libraries/RainLoop/Providers/PersonalAddressBook.php b/rainloop/v/0.0.0/app/libraries/RainLoop/Providers/PersonalAddressBook.php index 35f5eeac0..3529667a9 100644 --- a/rainloop/v/0.0.0/app/libraries/RainLoop/Providers/PersonalAddressBook.php +++ b/rainloop/v/0.0.0/app/libraries/RainLoop/Providers/PersonalAddressBook.php @@ -67,8 +67,20 @@ class PersonalAddressBook extends \RainLoop\Providers\AbstractProvider */ public function GetUserUidByEmail($sEmail) { - return $this->oDriver instanceof \RainLoop\Providers\PersonalAddressBook\PersonalAddressBookInterface && - $this->oDriver->GetUserUidByEmail($sEmail); + return $this->oDriver instanceof \RainLoop\Providers\PersonalAddressBook\PersonalAddressBookInterface ? + $this->oDriver->GetUserUidByEmail($sEmail) : ''; + } + + /** + * @param string $sEmail + * @param bool $bCreate = false + * + * @return string + */ + public function GetUserHashByEmail($sEmail, $bCreate = false) + { + return $this->oDriver instanceof \RainLoop\Providers\PersonalAddressBook\PersonalAddressBookInterface ? + $this->oDriver->GetUserHashByEmail($sEmail, $bCreate) : ''; } /** @@ -121,6 +133,18 @@ class PersonalAddressBook extends \RainLoop\Providers\AbstractProvider $iOffset, $iLimit, $sSearch, $iResultCount) : array(); } + /** + * @param string $sEmail + * @param string $sID + * @param bool $bIsStrID = false + * + * @return \RainLoop\Providers\PersonalAddressBook\Classes\Contact|null + */ + public function GetContactByID($sEmail, $mID, $bIsStrID = false) + { + return $this->IsActive() ? $this->oDriver->GetContactByID($sEmail, $mID, $bIsStrID) : null; + } + /** * @param \RainLoop\Account $oAccount * @param string $sSearch diff --git a/rainloop/v/0.0.0/app/libraries/RainLoop/Providers/PersonalAddressBook/Classes/Contact.php b/rainloop/v/0.0.0/app/libraries/RainLoop/Providers/PersonalAddressBook/Classes/Contact.php index eb32d85cc..d7ddfcf4c 100644 --- a/rainloop/v/0.0.0/app/libraries/RainLoop/Providers/PersonalAddressBook/Classes/Contact.php +++ b/rainloop/v/0.0.0/app/libraries/RainLoop/Providers/PersonalAddressBook/Classes/Contact.php @@ -11,6 +11,11 @@ class Contact */ public $IdContact; + /** + * @var string + */ + public $IdContactStr; + /** * @var string */ @@ -59,6 +64,7 @@ class Contact public function Clear() { $this->IdContact = ''; + $this->IdContactStr = ''; $this->IdUser = 0; $this->Display = ''; $this->DisplayName = ''; @@ -95,6 +101,11 @@ class Contact } } + if (empty($this->IdContactStr)) + { + $this->IdContactStr = \Sabre\DAV\UUIDUtil::getUUID(); + } + $this->DisplayName = $sDisplayName; $this->DisplayEmail = $sDisplayEmail; @@ -123,6 +134,8 @@ class Contact */ public function ToVCardObject() { + $this->UpdateDependentValues(); + $oVCard = new \Sabre\VObject\Component\VCard('VCARD'); $oVCard->VERSION = '3.0'; @@ -141,6 +154,32 @@ class Contact case PropertyType::NICK: $oVCard->NICKNAME = $oProperty->Value; break; + case PropertyType::EMAIl_PERSONAL: + case PropertyType::EMAIl_OTHER: + $oVCard->add('EMAIL', $oProperty->Value, array('TYPE' => 'INTERNET', 'TYPE' => 'HOME')); + break; + case PropertyType::EMAIl_BUSSINES: + $oVCard->add('EMAIL', $oProperty->Value, array('TYPE' => 'INTERNET', 'TYPE' => 'WORK')); + break; + case PropertyType::PHONE_PERSONAL: + case PropertyType::PHONE_OTHER: + $oVCard->add('TEL', $oProperty->Value, array('TYPE' => 'VOICE', 'TYPE' => 'HOME')); + break; + case PropertyType::PHONE_BUSSINES: + $oVCard->add('TEL', $oProperty->Value, array('TYPE' => 'VOICE', 'TYPE' => 'WORK')); + break; + case PropertyType::MOBILE_PERSONAL: + case PropertyType::MOBILE_BUSSINES: + case PropertyType::MOBILE_OTHER: + $oVCard->add('TEL', $oProperty->Value, array('TYPE' => 'VOICE', 'TYPE' => 'CELL')); + break; + case PropertyType::FAX_PERSONAL: + case PropertyType::FAX_OTHER: + $oVCard->add('TEL', $oProperty->Value, array('TYPE' => 'FAX', 'TYPE' => 'HOME')); + break; + case PropertyType::FAX_BUSSINES: + $oVCard->add('TEL', $oProperty->Value, array('TYPE' => 'FAX', 'TYPE' => 'WORK')); + break; case PropertyType::FIRST_NAME: $sFirstName = $oProperty->Value; break; @@ -148,15 +187,10 @@ class Contact $sSurName = $oProperty->Value; break; } - - if (PropertyType::FULLNAME === $oProperty->Type) - { - $oVCard->FN = $oProperty->Value; - } } } - $oVCard->UID = ((string) $this->IdContact).'.vcf'; + $oVCard->UID = $this->VCardUID(); // $oVCard->N = array( // $sSurName, @@ -169,4 +203,14 @@ class Contact return $oVCard; } + + /** + * @return string + */ + public function VCardUID() + { + return $this->IdContactStr.'.vcf'; + } + + } diff --git a/rainloop/v/0.0.0/app/libraries/RainLoop/Providers/PersonalAddressBook/PdoPersonalAddressBook.php b/rainloop/v/0.0.0/app/libraries/RainLoop/Providers/PersonalAddressBook/PdoPersonalAddressBook.php index 51a17660b..1407c40fe 100644 --- a/rainloop/v/0.0.0/app/libraries/RainLoop/Providers/PersonalAddressBook/PdoPersonalAddressBook.php +++ b/rainloop/v/0.0.0/app/libraries/RainLoop/Providers/PersonalAddressBook/PdoPersonalAddressBook.php @@ -93,15 +93,54 @@ class PdoPersonalAddressBook } /** - * @param \RainLoop\Account $oAccount + * @param string $sEmail + * @param bool $bCreate = false + * + * @return string + */ + public function GetUserHashByEmail($sEmail, $bCreate = false) + { + $sHash = ''; + $iUserID = $this->getUserId($sEmail); + if (0 < $iUserID) + { + $oStmt = $this->prepareAndExecute('SELECT pass_hash FROM rainloop_pab_users_hashes WHERE id_user = :id_user LIMIT 1', + array(':id_user' => array($iUserID, \PDO::PARAM_INT))); + + if ($oStmt) + { + $aFetch = $oStmt->fetchAll(\PDO::FETCH_ASSOC); + if ($aFetch && !empty($aFetch[0]['pass_hash'])) + { + $sHash = \rtrim(\base_convert(\md5(\md5($sEmail.'-'.\trim($aFetch[0]['pass_hash']).'-rainloop')), 16, 32), '0'); + } + else if ($bCreate) + { + $this->prepareAndExecute('INSERT INTO rainloop_pab_users_hashes (id_user, pass_hash) VALUES (:id_user, :pass_hash);', + array( + ':id_user' => array($iUserID, \PDO::PARAM_INT), + ':pass_hash' => array(\md5($sEmail.\microtime(true)), \PDO::PARAM_STR) + ) + ); + + $this->GetUserHashByEmail($sEmail, false); + } + } + } + + return $sHash; + } + + /** + * @param string $sEmail * @param \RainLoop\Providers\PersonalAddressBook\Classes\Contact $oContact * * @return bool */ - public function ContactSave($oAccount, &$oContact) + public function ContactSave($sEmail, &$oContact) { $this->Sync(); - $iUserID = $this->getUserId($oAccount->ParentEmailHelper()); + $iUserID = $this->getUserId($sEmail); $iIdContact = \strlen($oContact->IdContact) && \is_numeric($oContact->IdContact) ? (int) $oContact->IdContact : 0; @@ -127,13 +166,14 @@ class PdoPersonalAddressBook { $aFreq = $this->getContactFreq($iUserID, $iIdContact); - $sSql = 'UPDATE rainloop_pab_contacts SET display = :display, display_name = :display_name, display_email = :display_email, '. + $sSql = 'UPDATE rainloop_pab_contacts SET id_contact_str = :id_contact_str, display = :display, display_name = :display_name, display_email = :display_email, '. 'scope_type = :scope_type, changed = :changed WHERE id_user = :id_user AND id_contact = :id_contact'; $this->prepareAndExecute($sSql, array( ':id_user' => array($iUserID, \PDO::PARAM_INT), ':id_contact' => array($iIdContact, \PDO::PARAM_INT), + ':id_contact_str' => array($oContact->IdContactStr, \PDO::PARAM_STR), ':display' => array($oContact->Display, \PDO::PARAM_STR), ':display_name' => array($oContact->DisplayName, \PDO::PARAM_STR), ':display_email' => array($oContact->DisplayEmail, \PDO::PARAM_STR), @@ -154,12 +194,13 @@ class PdoPersonalAddressBook else { $sSql = 'INSERT INTO rainloop_pab_contacts '. - '( id_user, display, display_name, display_email, scope_type, changed) VALUES '. - '(:id_user, :display, :display_name, :display_email, :scope_type, :changed)'; + '( id_user, id_contact_str, display, display_name, display_email, scope_type, changed) VALUES '. + '(:id_user, :id_contact_str, :display, :display_name, :display_email, :scope_type, :changed)'; $this->prepareAndExecute($sSql, array( ':id_user' => array($iUserID, \PDO::PARAM_INT), + ':id_contact_str' => array($oContact->IdContactStr, \PDO::PARAM_STR), ':display' => array($oContact->Display, \PDO::PARAM_STR), ':display_name' => array($oContact->DisplayName, \PDO::PARAM_STR), ':display_email' => array($oContact->DisplayEmail, \PDO::PARAM_STR), @@ -225,14 +266,14 @@ class PdoPersonalAddressBook } /** - * @param \RainLoop\Account $oAccount + * @param string $sEmail * @param array $aContactIds * * @return bool */ - public function DeleteContacts($oAccount, $aContactIds) + public function DeleteContacts($sEmail, $aContactIds) { - $iUserID = $this->getUserId($oAccount->ParentEmailHelper()); + $iUserID = $this->getUserId($sEmail); $aContactIds = \array_filter($aContactIds, function (&$mItem) { $mItem = (int) \trim($mItem); @@ -255,15 +296,15 @@ class PdoPersonalAddressBook } /** - * @param \RainLoop\Account $oAccount + * @param string $sEmail * @param array $aTagsIds * * @return bool */ - public function DeleteTags($oAccount, $aTagsIds) + public function DeleteTags($sEmail, $aTagsIds) { $this->Sync(); - $iUserID = $this->getUserId($oAccount->ParentEmailHelper()); + $iUserID = $this->getUserId($sEmail); $aTagsIds = \array_filter($aTagsIds, function (&$mItem) { $mItem = (int) \trim($mItem); @@ -285,7 +326,7 @@ class PdoPersonalAddressBook } /** - * @param \RainLoop\Account|mixed $mAccountOrId + * @param string $sEmail * @param int $iOffset = 0 * @param int $iLimit = 20 * @param string $sSearch = '' @@ -293,7 +334,7 @@ class PdoPersonalAddressBook * * @return array */ - public function GetContacts($mAccountOrId, $iOffset = 0, $iLimit = 20, $sSearch = '', &$iResultCount = 0) + public function GetContacts($sEmail, $iOffset = 0, $iLimit = 20, $sSearch = '', &$iResultCount = 0) { $this->Sync(); @@ -301,11 +342,7 @@ class PdoPersonalAddressBook $iLimit = 0 < $iLimit ? (int) $iLimit : 20; $sSearch = \trim($sSearch); - $iUserID = $mAccountOrId instanceof \RainLoop\Account ? $this->getUserId($mAccountOrId->ParentEmailHelper()) : (int) $mAccountOrId; - if (0 === $iUserID) - { - throw new \InvalidArgumentException('Invalid Accoutn ID'); - } + $iUserID = $this->getUserId($sEmail); $iCount = 0; $aSearchIds = array(); @@ -428,6 +465,7 @@ class PdoPersonalAddressBook $oContact = new \RainLoop\Providers\PersonalAddressBook\Classes\Contact(); $oContact->IdContact = (string) $iIdContact; + $oContact->IdContactStr = isset($aItem['id_contact_str']) ? (string) $aItem['id_contact_str'] : ''; $oContact->Display = isset($aItem['display']) ? (string) $aItem['display'] : ''; $oContact->DisplayName = isset($aItem['display_name']) ? (string) $aItem['display_name'] : ''; $oContact->DisplayEmail = isset($aItem['display_email']) ? (string) $aItem['display_email'] : ''; @@ -498,7 +536,124 @@ class PdoPersonalAddressBook } /** - * @param \RainLoop\Account $oAccount + * @param string $sEmail + * @param string $sID + * @param bool $bIsStrID = false + * + * @return \RainLoop\Providers\PersonalAddressBook\Classes\Contact|null + */ + public function GetContactByID($sEmail, $mID, $bIsStrID = false) + { + $this->Sync(); + + $mID = \trim($mID); + + $iUserID = $this->getUserId($sEmail); + + $sSql = 'SELECT * FROM rainloop_pab_contacts '. + 'WHERE id_user = :id_user'. + ($this->bConsiderShare ? ' OR scope_type = :scope_type_share_all' : '') + ; + + $aParams = array( + ':id_user' => array($iUserID, \PDO::PARAM_INT) + ); + + if ($this->bConsiderShare) + { + $aParams[':scope_type_share_all'] = array(\RainLoop\Providers\PersonalAddressBook\Enumerations\ScopeType::SHARE_ALL, \PDO::PARAM_INT); + } + + if ($bIsStrID) + { + $sSql .= ' AND id_contact_str = :id_contact_str'; + $aParams[':id_contact_str'] = array($mID, \PDO::PARAM_STR); + } + else + { + $sSql .= ' AND id_contact = :id_contact'; + $aParams[':id_contact'] = array($mID, \PDO::PARAM_INT); + } + + $sSql .= ' LIMIT 1'; + + $oContact = null; + $iIdContact = 0; + + $oStmt = $this->prepareAndExecute($sSql, $aParams); + if ($oStmt) + { + $aFetch = $oStmt->fetchAll(\PDO::FETCH_ASSOC); + if (\is_array($aFetch) && 0 < \count($aFetch)) + { + foreach ($aFetch as $aItem) + { + $iIdContact = $aItem && isset($aItem['id_contact']) ? (int) $aItem['id_contact'] : 0; + if (0 < $iIdContact) + { + $oContact = new \RainLoop\Providers\PersonalAddressBook\Classes\Contact(); + + $oContact->IdContact = (string) $iIdContact; + $oContact->IdContactStr = isset($aItem['id_contact_str']) ? (string) $aItem['id_contact_str'] : ''; + $oContact->Display = isset($aItem['display']) ? (string) $aItem['display'] : ''; + $oContact->DisplayName = isset($aItem['display_name']) ? (string) $aItem['display_name'] : ''; + $oContact->DisplayEmail = isset($aItem['display_email']) ? (string) $aItem['display_email'] : ''; + $oContact->ScopeType = isset($aItem['scope_type']) ? (int) $aItem['scope_type'] : + \RainLoop\Providers\PersonalAddressBook\Enumerations\ScopeType::DEFAULT_; + $oContact->Changed = isset($aItem['changed']) ? (int) $aItem['changed'] : 0; + $oContact->ReadOnly = $iUserID !== (isset($aItem['id_user']) ? (int) $aItem['id_user'] : 0); + } + } + } + + unset($aFetch); + + if (0 < $iIdContact && $oContact) + { + $oStmt->closeCursor(); + + $sSql = 'SELECT * FROM rainloop_pab_properties WHERE id_contact = '.$iIdContact; + $oStmt = $this->prepareAndExecute($sSql); + + if ($oStmt) + { + $aFetch = $oStmt->fetchAll(\PDO::FETCH_ASSOC); + if (\is_array($aFetch) && 0 < \count($aFetch)) + { + foreach ($aFetch as $aItem) + { + if ($aItem && isset($aItem['id_prop'], $aItem['id_contact'], $aItem['prop_type'], $aItem['prop_value'])) + { + if ((string) $oContact->IdContact === (string) $aItem['id_contact']) + { + $oProperty = new \RainLoop\Providers\PersonalAddressBook\Classes\Property(); + $oProperty->IdProperty = (int) $aItem['id_prop']; + $oProperty->ScopeType = isset($aItem['scope_type']) ? (int) $aItem['scope_type'] : + \RainLoop\Providers\PersonalAddressBook\Enumerations\ScopeType::DEFAULT_; + $oProperty->Type = (int) $aItem['prop_type']; + $oProperty->TypeCustom = isset($aItem['prop_type_custom']) ? (string) $aItem['prop_type_custom'] : ''; + $oProperty->Value = (string) $aItem['prop_value']; + $oProperty->ValueCustom = isset($aItem['prop_value_custom']) ? (string) $aItem['prop_value_custom'] : ''; + $oProperty->Frec = isset($aItem['prop_frec']) ? (int) $aItem['prop_frec'] : 0; + + $oContact->Properties[] = $oProperty; + } + } + } + } + + unset($aFetch); + + $oContact->UpdateDependentValues(); + } + } + } + + return $oContact; + } + + /** + * @param string $sEmail * @param string $sSearch * @param int $iLimit = 20 * @@ -506,7 +661,7 @@ class PdoPersonalAddressBook * * @throws \InvalidArgumentException */ - public function GetSuggestions($oAccount, $sSearch, $iLimit = 20) + public function GetSuggestions($sEmail, $sSearch, $iLimit = 20) { $sSearch = \trim($sSearch); if (0 === \strlen($sSearch)) @@ -516,7 +671,7 @@ class PdoPersonalAddressBook $this->Sync(); - $iUserID = $this->getUserId($oAccount->ParentEmailHelper()); + $iUserID = $this->getUserId($sEmail); $sTypes = implode(',', array( PropertyType::EMAIl_PERSONAL, PropertyType::EMAIl_BUSSINES, PropertyType::EMAIl_OTHER, PropertyType::FULLNAME @@ -675,13 +830,13 @@ class PdoPersonalAddressBook } /** - * @param \RainLoop\Account $oAccount + * @param string $sEmail * @param array $aEmails * @param bool $bCreateAuto = true * * @return bool */ - public function IncFrec($oAccount, $aEmails, $bCreateAuto = true) + public function IncFrec($sEmail, $aEmails, $bCreateAuto = true) { $self = $this; $aEmailsObjects = \array_map(function ($mItem) { @@ -700,7 +855,7 @@ class PdoPersonalAddressBook } $this->Sync(); - $iUserID = $this->getUserId($oAccount->ParentEmailHelper()); + $iUserID = $this->getUserId($sEmail); $sTypes = \implode(',', array( PropertyType::EMAIl_PERSONAL, PropertyType::EMAIl_BUSSINES, PropertyType::EMAIl_OTHER @@ -792,7 +947,7 @@ class PdoPersonalAddressBook if (0 < \count($oContact->Properties)) { - $this->ContactSave($oAccount, $oContact); + $this->ContactSave($sEmail, $oContact); } $oContact->Clear(); @@ -975,11 +1130,11 @@ POSTGRESINITIAL; CREATE TABLE rainloop_pab_contacts ( id_contact integer NOT NULL PRIMARY KEY, id_user integer NOT NULL, - scope_type integer NOT NULL default 0, - display_name text NOT NULL default '', - display_email text NOT NULL default '', - display text NOT NULL default '', - changed integer NOT NULL default 0 + scope_type integer NOT NULL DEFAULT 0, + display_name text NOT NULL DEFAULT '', + display_email text NOT NULL DEFAULT '', + display text NOT NULL DEFAULT '', + changed integer NOT NULL DEFAULT 0 ); CREATE INDEX id_user_scope_type_rainloop_pab_contacts_index ON rainloop_pab_contacts (id_user, scope_type); @@ -988,12 +1143,12 @@ CREATE TABLE rainloop_pab_properties ( id_prop integer NOT NULL PRIMARY KEY, id_contact integer NOT NULL, id_user integer NOT NULL, - scope_type integer NOT NULL default 0, + scope_type integer NOT NULL DEFAULT 0, prop_type integer NOT NULL, - prop_type_custom text NOT NULL default '', - prop_value text NOT NULL default '', - prop_value_custom text NOT NULL default '', - prop_frec integer NOT NULL default 0 + prop_type_custom text NOT NULL DEFAULT '', + prop_value text NOT NULL DEFAULT '', + prop_value_custom text NOT NULL DEFAULT '', + prop_frec integer NOT NULL DEFAULT 0 ); CREATE INDEX id_user_rainloop_pab_properties_index ON rainloop_pab_properties (id_user); @@ -1045,15 +1200,36 @@ SQLITEINITIAL; { case 'mysql': return $this->dataBaseUpgrade($this->sDsnType.'-pab-version', array( - 1 => $this->getInitialTablesArray($this->sDsnType) + 1 => $this->getInitialTablesArray($this->sDsnType), + 2 => array( +'ALTER TABLE rainloop_pab_contacts ADD id_contact_str varchar(128) NOT NULL DEFAULT \'\' AFTER id_contact;', +'CREATE TABLE IF NOT EXISTS rainloop_pab_users_hashes ( + id_user int UNSIGNED NOT NULL, + pass_hash varchar(128) NOT NULL +)/*!40000 ENGINE=INNODB */;' + ) )); case 'pgsql': return $this->dataBaseUpgrade($this->sDsnType.'-pab-version', array( - 1 => $this->getInitialTablesArray($this->sDsnType) + 1 => $this->getInitialTablesArray($this->sDsnType), + 2 => array( +'ALTER TABLE rainloop_pab_contacts ADD id_contact_str varchar(128) NOT NULL DEFAULT \'\';', +'CREATE TABLE rainloop_pab_users_hashes ( + id_user integer NOT NULL, + pass_hash varchar(128) NOT NULL +);' + ) )); case 'sqlite': return $this->dataBaseUpgrade($this->sDsnType.'-pab-version', array( - 1 => $this->getInitialTablesArray($this->sDsnType) + 1 => $this->getInitialTablesArray($this->sDsnType), + 2 => array( +'ALTER TABLE rainloop_pab_contacts ADD id_contact_str text NOT NULL DEFAULT \'\';', +'CREATE TABLE rainloop_pab_users_hashes ( + id_user integer NOT NULL, + pass_hash text NOT NULL +);' + ) )); } diff --git a/rainloop/v/0.0.0/app/libraries/RainLoop/Providers/PersonalAddressBook/PersonalAddressBookInterface.php b/rainloop/v/0.0.0/app/libraries/RainLoop/Providers/PersonalAddressBook/PersonalAddressBookInterface.php index 4ee95411c..74a1413e9 100644 --- a/rainloop/v/0.0.0/app/libraries/RainLoop/Providers/PersonalAddressBook/PersonalAddressBookInterface.php +++ b/rainloop/v/0.0.0/app/libraries/RainLoop/Providers/PersonalAddressBook/PersonalAddressBookInterface.php @@ -10,31 +10,31 @@ interface PersonalAddressBookInterface public function IsSupported(); /** - * @param \RainLoop\Account $oAccount + * @param string $sEmail * @param \RainLoop\Providers\PersonalAddressBook\Classes\Contact $oContact * * @return bool */ - public function ContactSave($oAccount, &$oContact); + public function ContactSave($sEmail, &$oContact); /** - * @param \RainLoop\Account $oAccount + * @param string $sEmail * @param array $aContactIds * * @return bool */ - public function DeleteContacts($oAccount, $aContactIds); + public function DeleteContacts($sEmail, $aContactIds); /** - * @param \RainLoop\Account $oAccount + * @param string $sEmail * @param array $aTagsIds * * @return bool */ - public function DeleteTags($oAccount, $aTagsIds); + public function DeleteTags($sEmail, $aTagsIds); /** - * @param \RainLoop\Account|mixed $mAccountOrId + * @param string $sEmail * @param int $iOffset = 0 * @param int $iLimit = 20 * @param string $sSearch = '' @@ -42,10 +42,10 @@ interface PersonalAddressBookInterface * * @return array */ - public function GetContacts($mAccountOrId, $iOffset = 0, $iLimit = 20, $sSearch = '', &$iResultCount = 0); + public function GetContacts($sEmail, $iOffset = 0, $iLimit = 20, $sSearch = '', &$iResultCount = 0); /** - * @param \RainLoop\Account $oAccount + * @param string $sEmail * @param string $sSearch * @param int $iLimit = 20 * @@ -53,13 +53,13 @@ interface PersonalAddressBookInterface * * @throws \InvalidArgumentException */ - public function GetSuggestions($oAccount, $sSearch, $iLimit = 20); + public function GetSuggestions($sEmail, $sSearch, $iLimit = 20); /** - * @param \RainLoop\Account $oAccount + * @param string $sEmail * @param array $aEmails * * @return bool */ - public function IncFrec($oAccount, $aEmails); + public function IncFrec($sEmail, $aEmails); } \ No newline at end of file diff --git a/rainloop/v/0.0.0/app/libraries/RainLoop/SabreDAV/AuthBasic.php b/rainloop/v/0.0.0/app/libraries/RainLoop/SabreDAV/AuthBasic.php index 10dd8adf6..2c5ffb750 100644 --- a/rainloop/v/0.0.0/app/libraries/RainLoop/SabreDAV/AuthBasic.php +++ b/rainloop/v/0.0.0/app/libraries/RainLoop/SabreDAV/AuthBasic.php @@ -1,31 +1,30 @@ -oPersonalAddressBook = $oPersonalAddressBook; - } - - /** - * @param string $sUserName - * @param string $sPassword - * - * @return bool - */ - protected function validateUserPass($sUserName, $sPassword) - { - $this->currentUser = $sUserName; - return true; // TODO - } -} +oPersonalAddressBook = $oPersonalAddressBook; + } + + /** + * @param string $sUserName + * @param string $sPassword + * + * @return bool + */ + protected function validateUserPass($sUserName, $sPassword) + { + return $sPassword === $this->oPersonalAddressBook->GetUserHashByEmail($sUserName, true); + } +} diff --git a/rainloop/v/0.0.0/app/libraries/RainLoop/SabreDAV/AuthDigest.php b/rainloop/v/0.0.0/app/libraries/RainLoop/SabreDAV/AuthDigest.php index e2afc799d..a27aaaf42 100644 --- a/rainloop/v/0.0.0/app/libraries/RainLoop/SabreDAV/AuthDigest.php +++ b/rainloop/v/0.0.0/app/libraries/RainLoop/SabreDAV/AuthDigest.php @@ -1,28 +1,37 @@ -oPersonalAddressBook = $oPersonalAddressBook; - } - - /** - * @return bool - */ - public function getDigestHash($sRealm, $sUserName) - { - $this->currentUser = $sUserName; - return true; // TODO - } -} +oPersonalAddressBook = $oPersonalAddressBook; + } + + /** + * @param string $sRealm + * @param string $sUserName + * + * @return string|null + */ + public function getDigestHash($sRealm, $sUserName) + { + $sHash = $this->oPersonalAddressBook->GetUserHashByEmail($sUserName, true); + if (!empty($sHash)) + { + $this->currentUser = $sUserName; + return \md5($sUserName.':'.$sRealm.':'.$sHash); + } + + return null; + } +} diff --git a/rainloop/v/0.0.0/app/libraries/RainLoop/SabreDAV/CardDAV.php b/rainloop/v/0.0.0/app/libraries/RainLoop/SabreDAV/CardDAV.php index 8b467b439..d723ee29d 100644 --- a/rainloop/v/0.0.0/app/libraries/RainLoop/SabreDAV/CardDAV.php +++ b/rainloop/v/0.0.0/app/libraries/RainLoop/SabreDAV/CardDAV.php @@ -1,278 +1,333 @@ -oPersonalAddressBook = $oPersonalAddressBook; - } - - /** - * @param string $sPrincipalUri - * - * @return string - */ - private function getEmailFromPrincipalUri($sPrincipalUri) - { - $sEmail = ''; - $aMatch = array(); - if (\preg_match('/\/?principals\/([^@\/]+@[^@\/]+)/i', $sPrincipalUri, $aMatch) && !empty($aMatch[1])) - { - $sEmail = \trim($aMatch[1]); - } - - return $sEmail; - } - - /** - * @param string $sPrincipalUri - * - * @return string - */ - private function getCalendarID($sPrincipalUri) - { - $mUserId = (string) $this->oPersonalAddressBook->GetUserUidByEmail( - $this->getEmailFromPrincipalUri($sPrincipalUri)); - - return !empty($mUserId) ? $mUserId : ''; - } - - /** - * Returns the list of addressbooks for a specific user. - * - * @param string $sPrincipalUri - * @return array - */ - public function getAddressBooksForUser($sPrincipalUri) - { - $mAddressBookID = $this->getCalendarID($sPrincipalUri); - - $aAddressBooks = array(); - if (!empty($mAddressBookID)) - { - $aAddressBooks[] = array( - 'id' => $mAddressBookID, - 'uri' => 'default', - 'principaluri' => $sPrincipalUri, - '{DAV:}displayname' => 'Personal Address Book', - '{'.\Sabre\CardDAV\Plugin::NS_CARDDAV.'}addressbook-description' => 'Personal Address Book', - '{http://calendarserver.org/ns/}getctag' => 1, - '{'.\Sabre\CardDAV\Plugin::NS_CARDDAV.'}supported-address-data' => new \Sabre\CardDAV\Property\SupportedAddressData() - ); - } - - return $aAddressBooks; - } - - /** - * Updates an addressbook's properties - * - * See Sabre\DAV\IProperties for a description of the mutations array, as - * well as the return value. - * - * @param mixed $mAddressBookId - * @param array $aMutations - * @see Sabre\DAV\IProperties::updateProperties - * @return bool|array - */ - public function updateAddressBook($mAddressBookId, array $aMutations) - { - return false; - } - - /** - * Creates a new address book - * - * @param string $sPrincipalUri - * @param string $sUrl Just the 'basename' of the url. - * @param array $aProperties - * - * @return void - */ - public function createAddressBook($sPrincipalUri, $sUrl, array $aProperties) - { - } - - /** - * Deletes an entire addressbook and all its contents - * - * @param mixed $mAddressBookId - * - * @return void - */ - public function deleteAddressBook($mAddressBookId) - { - } - - private function simpleGetCards($mAddressBookId, $sCardUri = '') - { - $aResult = array(); - if (!empty($mAddressBookId)) - { - $aList = $this->oPersonalAddressBook->GetContacts($mAddressBookId, 0, 20); - foreach ($aList as /* @var $oItem \RainLoop\Providers\PersonalAddressBook\Classes\Contact */ $oItem) - { - $oVCard = $oItem->ToVCardObject(); - if ('' === $sCardUri || $sCardUri === $oVCard->UID) - { - $aResult[] = array( - 'id' => $oItem->IdContact, - 'uri' => $oVCard->UID, - 'lastmodified' => $oItem->Changed, - 'carddata' => $oVCard->serialize() - ); - } - } - } - - return $aResult; - } - - /** - * Returns all cards for a specific addressbook id. - * - * This method should return the following properties for each card: - * * carddata - raw vcard data - * * uri - Some unique url - * * lastmodified - A unix timestamp - * - * It's recommended to also return the following properties: - * * etag - A unique etag. This must change every time the card changes. - * * size - The size of the card in bytes. - * - * If these last two properties are provided, less time will be spent - * calculating them. If they are specified, you can also ommit carddata. - * This may speed up certain requests, especially with large cards. - * - * @param mixed $mAddressbookId - * - * @return array - */ - public function getCards($mAddressBookId) - { - return $this->simpleGetCards($mAddressBookId); - } - - /** - * Returns a specfic card. - * - * The same set of properties must be returned as with getCards. The only - * exception is that 'carddata' is absolutely required. - * - * @param mixed $mAddressBookId - * @param string $sCardUri - * @return array - */ - public function getCard($mAddressBookId, $sCardUri) - { - $aList = $this->simpleGetCards($mAddressBookId, $sCardUri); - return 0 < count($aList) && isset($aList) ? $aList[0] : false; - } - - /** - * Creates a new card. - * - * The addressbook id will be passed as the first argument. This is the - * same id as it is returned from the getAddressbooksForUser method. - * - * The cardUri is a base uri, and doesn't include the full path. The - * cardData argument is the vcard body, and is passed as a string. - * - * It is possible to return an ETag from this method. This ETag is for the - * newly created resource, and must be enclosed with double quotes (that - * is, the string itself must contain the double quotes). - * - * You should only return the ETag if you store the carddata as-is. If a - * subsequent GET request on the same card does not have the same body, - * byte-by-byte and you did return an ETag here, clients tend to get - * confused. - * - * If you don't return an ETag, you can just return null. - * - * @param mixed $mAddressBookId - * @param string $sCardUri - * @param string $sCardData - * @return string|null - */ - public function createCard($mAddressBookId, $sCardUri, $sCardData) - { - return null; -// -// $stmt = $this->pdo->prepare('INSERT INTO ' . $this->cardsTableName . ' (carddata, uri, lastmodified, addressbookid) VALUES (?, ?, ?, ?)'); -// -// $result = $stmt->execute(array($cardData, $cardUri, time(), $addressBookId)); -// -// $stmt2 = $this->pdo->prepare('UPDATE ' . $this->addressBooksTableName . ' SET ctag = ctag + 1 WHERE id = ?'); -// $stmt2->execute(array($addressBookId)); -// -// return '"' . md5($cardData) . '"'; - } - - /** - * Updates a card. - * - * The addressbook id will be passed as the first argument. This is the - * same id as it is returned from the getAddressbooksForUser method. - * - * The cardUri is a base uri, and doesn't include the full path. The - * cardData argument is the vcard body, and is passed as a string. - * - * It is possible to return an ETag from this method. This ETag should - * match that of the updated resource, and must be enclosed with double - * quotes (that is: the string itself must contain the actual quotes). - * - * You should only return the ETag if you store the carddata as-is. If a - * subsequent GET request on the same card does not have the same body, - * byte-by-byte and you did return an ETag here, clients tend to get - * confused. - * - * If you don't return an ETag, you can just return null. - * - * @param mixed $mAddressBookId - * @param string $sCardUri - * @param string $sCardData - * @return string|null - */ - public function updateCard($mAddressBookId, $sCardUri, $sCardData) - { - return null; - -// $stmt = $this->pdo->prepare('UPDATE ' . $this->cardsTableName . ' SET carddata = ?, lastmodified = ? WHERE uri = ? AND addressbookid =?'); -// $stmt->execute(array($cardData, time(), $cardUri, $addressBookId)); -// -// $stmt2 = $this->pdo->prepare('UPDATE ' . $this->addressBooksTableName . ' SET ctag = ctag + 1 WHERE id = ?'); -// $stmt2->execute(array($addressBookId)); -// -// return '"' . md5($cardData) . '"'; - } - - /** - * Deletes a card - * - * @param mixed $mAddressBookId - * @param string $sCardUri - * - * @return bool - */ - public function deleteCard($mAddressBookId, $sCardUri) - { - return false; -// $stmt = $this->pdo->prepare('DELETE FROM ' . $this->cardsTableName . ' WHERE addressbookid = ? AND uri = ?'); -// $stmt->execute(array($addressBookId, $cardUri)); -// -// $stmt2 = $this->pdo->prepare('UPDATE ' . $this->addressBooksTableName . ' SET ctag = ctag + 1 WHERE id = ?'); -// $stmt2->execute(array($addressBookId)); -// -// return $stmt->rowCount()===1; - } -} +oPersonalAddressBook = $oPersonalAddressBook; + $this->oPersonalAddressBook->ConsiderShare(false); + $this->oAuthBackend = $oAuthBackend; + } + + /** + * @param string $sPrincipalUri + * + * @return string + */ + private function getEmailFromPrincipalUri($sPrincipalUri) + { + $sEmail = ''; + $aMatch = array(); + if (\preg_match('/\/?principals\/([^@\/]+@[^@\/]+)/i', $sPrincipalUri, $aMatch) && !empty($aMatch[1])) + { + $sEmail = \trim($aMatch[1]); + } + + return $sEmail; + } + + /** + * @param string $sPrincipalUri = '' + * @param string $mAddressBookID = '' + * + * @return string + */ + private function getAuthEmail($sPrincipalUri = '', $mAddressBookID = '') + { + $sGetCurrentUser = \trim($this->oAuthBackend->getCurrentUser()); + if (0 < \strlen($sPrincipalUri) && 0 < \strlen($sGetCurrentUser) && + $sGetCurrentUser !== $this->getEmailFromPrincipalUri($sPrincipalUri)) + { + $sGetCurrentUser = ''; + } + + if (0 < \strlen((string) $mAddressBookID) && 0 < \strlen($sGetCurrentUser) && + (string) $mAddressBookID !== (string) $this->oPersonalAddressBook->GetUserUidByEmail($sGetCurrentUser)) + { + $sGetCurrentUser = ''; + } + + return $sGetCurrentUser; + } + + /** + * Returns the list of addressbooks for a specific user. + * + * @param string $sPrincipalUri + * @return array + */ + public function getAddressBooksForUser($sPrincipalUri) + { + $aAddressBooks = array(); + + $sEmail = $this->getAuthEmail($sPrincipalUri); + if (0 < strlen($sEmail)) + { + $mAddressBookID = $this->oPersonalAddressBook->GetUserUidByEmail($sEmail); + if (!empty($mAddressBookID)) + { + $aAddressBooks[] = array( + 'id' => $mAddressBookID, + 'uri' => 'default', + 'principaluri' => $sPrincipalUri, + '{DAV:}displayname' => 'Personal Address Book', + '{'.\Sabre\CardDAV\Plugin::NS_CARDDAV.'}addressbook-description' => 'Personal Address Book', + '{http://calendarserver.org/ns/}getctag' => 1, + '{'.\Sabre\CardDAV\Plugin::NS_CARDDAV.'}supported-address-data' => new \Sabre\CardDAV\Property\SupportedAddressData() + ); + } + } + + return $aAddressBooks; + } + + /** + * Updates an addressbook's properties + * + * See Sabre\DAV\IProperties for a description of the mutations array, as + * well as the return value. + * + * @param mixed $mAddressBookId + * @param array $aMutations + * @see Sabre\DAV\IProperties::updateProperties + * @return bool|array + */ + public function updateAddressBook($mAddressBookID, array $aMutations) + { + return false; + } + + /** + * Creates a new address book + * + * @param string $sPrincipalUri + * @param string $sUrl Just the 'basename' of the url. + * @param array $aProperties + * + * @return void + */ + public function createAddressBook($sPrincipalUri, $sUrl, array $aProperties) + { + } + + /** + * Deletes an entire addressbook and all its contents + * + * @param mixed $mAddressBookID + * + * @return void + */ + public function deleteAddressBook($mAddressBookID) + { + } + + /** + * Returns all cards for a specific addressbook id. + * + * This method should return the following properties for each card: + * * carddata - raw vcard data + * * uri - Some unique url + * * lastmodified - A unix timestamp + * + * It's recommended to also return the following properties: + * * etag - A unique etag. This must change every time the card changes. + * * size - The size of the card in bytes. + * + * If these last two properties are provided, less time will be spent + * calculating them. If they are specified, you can also ommit carddata. + * This may speed up certain requests, especially with large cards. + * + * @param mixed $mAddressBookID + * + * @return array + */ + public function getCards($mAddressBookID) + { + $aResult = array(); + if (!empty($mAddressBookID)) + { + $sEmail = $this->getAuthEmail('', $mAddressBookID); + if (!empty($sEmail)) + { + $aList = $this->oPersonalAddressBook->GetContacts($sEmail, 0, 500); + foreach ($aList as /* @var $oItem \RainLoop\Providers\PersonalAddressBook\Classes\Contact */ $oItem) + { + if (!$oItem->ReadOnly) + { + $sCardData = $oItem->ToVCardObject()->serialize(); + $aResult[] = array( + 'uri' => $oItem->VCardUID(), + 'lastmodified' => $oItem->Changed, + 'etag' => \md5($sCardData), + 'size' => \strlen($sCardData) + ); + } + } + } + } + + return $aResult; + } + + /** + * Returns a specfic card. + * + * The same set of properties must be returned as with getCards. The only + * exception is that 'carddata' is absolutely required. + * + * @param mixed $mAddressBookID + * @param string $sCardUri + * @return array + */ + public function getCard($mAddressBookID, $sCardUri) + { + $oContact = null; + if (!empty($mAddressBookID) && !empty($sCardUri) && '.vcf' === \substr($sCardUri, -4)) + { + $sEmail = $this->getAuthEmail('', $mAddressBookID); + if (!empty($sEmail)) + { + $oContact = $this->oPersonalAddressBook->GetContactByID($sEmail, \substr($sCardUri, 0, -4), true); + } + } + + if ($oContact) + { + $sCardData = $oContact->ToVCardObject()->serialize(); + return array( + 'uri' => $oContact->VCardUID(), + 'lastmodified' => $oContact->Changed, + 'etag' => \md5($sCardData), + 'size' => \strlen($sCardData), + 'carddata' => $sCardData + ); + } + + return false; + } + + /** + * Creates a new card. + * + * The addressbook id will be passed as the first argument. This is the + * same id as it is returned from the getAddressbooksForUser method. + * + * The cardUri is a base uri, and doesn't include the full path. The + * cardData argument is the vcard body, and is passed as a string. + * + * It is possible to return an ETag from this method. This ETag is for the + * newly created resource, and must be enclosed with double quotes (that + * is, the string itself must contain the double quotes). + * + * You should only return the ETag if you store the carddata as-is. If a + * subsequent GET request on the same card does not have the same body, + * byte-by-byte and you did return an ETag here, clients tend to get + * confused. + * + * If you don't return an ETag, you can just return null. + * + * @param mixed $mAddressBookID + * @param string $sCardUri + * @param string $sCardData + * + * @return string|null + */ + public function createCard($mAddressBookID, $sCardUri, $sCardData) + { + if (!empty($mAddressBookID) && !empty($sCardUri) && '.vcf' === \substr($sCardUri, -4) && 0 < \strlen($sCardData)) + { + $sEmail = $this->getAuthEmail('', $mAddressBookID); + if (!empty($sEmail)) + { + // TODO + } + } + + return null; + } + + /** + * Updates a card. + * + * The addressbook id will be passed as the first argument. This is the + * same id as it is returned from the getAddressbooksForUser method. + * + * The cardUri is a base uri, and doesn't include the full path. The + * cardData argument is the vcard body, and is passed as a string. + * + * It is possible to return an ETag from this method. This ETag should + * match that of the updated resource, and must be enclosed with double + * quotes (that is: the string itself must contain the actual quotes). + * + * You should only return the ETag if you store the carddata as-is. If a + * subsequent GET request on the same card does not have the same body, + * byte-by-byte and you did return an ETag here, clients tend to get + * confused. + * + * If you don't return an ETag, you can just return null. + * + * @param mixed $mAddressBookID + * @param string $sCardUri + * @param string $sCardData + * + * @return string|null + */ + public function updateCard($mAddressBookID, $sCardUri, $sCardData) + { + if (!empty($mAddressBookID) && !empty($sCardUri) && '.vcf' === \substr($sCardUri, -4) && 0 < \strlen($sCardData)) + { + $sEmail = $this->getAuthEmail('', $mAddressBookID); + if (!empty($sEmail)) + { + // TODO + } + } + + return null; + } + + /** + * Deletes a card + * + * @param mixed $mAddressBookID + * @param string $sCardUri + * + * @return bool + */ + public function deleteCard($mAddressBookID, $sCardUri) + { + $bResult = false; + $oContact = null; + if (!empty($mAddressBookID) && !empty($sCardUri) && '.vcf' === \substr($sCardUri, -4)) + { + $sEmail = $this->getAuthEmail('', $mAddressBookID); + if (!empty($sEmail)) + { + $oContact = $this->oPersonalAddressBook->GetContactByID($sEmail, \substr($sCardUri, 0, -4), true); + } + } + + if ($oContact) + { + $bResult = $this->oPersonalAddressBook->DeleteContacts($sEmail, array($oContact->IdContact)); + } + + return $bResult; + } +} diff --git a/rainloop/v/0.0.0/app/libraries/RainLoop/SabreDAV/Principal.php b/rainloop/v/0.0.0/app/libraries/RainLoop/SabreDAV/Principal.php index 0622102c4..7aa19451d 100644 --- a/rainloop/v/0.0.0/app/libraries/RainLoop/SabreDAV/Principal.php +++ b/rainloop/v/0.0.0/app/libraries/RainLoop/SabreDAV/Principal.php @@ -72,7 +72,6 @@ class Principal extends \Sabre\DAVACL\PrincipalBackend\AbstractBackend $sGetCurrentUser = $this->oAuthBackend->getCurrentUser(); if ('principals/'.$sGetCurrentUser === $sPath) { -// var_dump($sPath); return array( 'uri' => 'principals/'.$sGetCurrentUser, '{DAV:}displayname' => $sGetCurrentUser, @@ -133,7 +132,7 @@ class Principal extends \Sabre\DAVACL\PrincipalBackend\AbstractBackend */ function updatePrincipal($path, $mutations) { - return array(); + return true; } /** diff --git a/rainloop/v/0.0.0/app/libraries/RainLoop/Service.php b/rainloop/v/0.0.0/app/libraries/RainLoop/Service.php index 3b9c745ec..04a264a88 100644 --- a/rainloop/v/0.0.0/app/libraries/RainLoop/Service.php +++ b/rainloop/v/0.0.0/app/libraries/RainLoop/Service.php @@ -103,6 +103,14 @@ class Service $bCached = false; $sResult = ''; $sPathInfo = \trim(\trim($this->oHttp->GetServer('PATH_INFO', '')), ' /'); + if (!empty($sPathInfo)) + { + if ('dav' !== \substr($sPathInfo, 0, 3)) + { + $sPathInfo = ''; + } + } + if (empty($sPathInfo)) { $sQuery = \trim(\trim($this->oHttp->GetServer('QUERY_STRING', '')), ' /'); diff --git a/rainloop/v/0.0.0/app/libraries/RainLoop/ServiceActions.php b/rainloop/v/0.0.0/app/libraries/RainLoop/ServiceActions.php index a86fdd965..896ff7d92 100644 --- a/rainloop/v/0.0.0/app/libraries/RainLoop/ServiceActions.php +++ b/rainloop/v/0.0.0/app/libraries/RainLoop/ServiceActions.php @@ -714,10 +714,13 @@ class ServiceActions /** * @return string */ - public function ServiceCardDav() + public function ServiceDav() { - return ''; - + if (!$this->Config()->Get('contacts', 'allow_carddav_sync', false)) + { + return ''; + } + try { \set_error_handler(function ($errno, $errstr, $errfile, $errline ) { @@ -726,9 +729,10 @@ class ServiceActions $oPersonalAddressBookProvider = $this->oActions->PersonalAddressBookProvider(); - $oAuthBackend = new \RainLoop\SabreDAV\AuthBasic($oPersonalAddressBookProvider); +// $oAuthBackend = new \RainLoop\SabreDAV\AuthBasic($oPersonalAddressBookProvider); + $oAuthBackend = new \RainLoop\SabreDAV\AuthDigest($oPersonalAddressBookProvider); - $oCarddavBackend = new \RainLoop\SabreDAV\CardDAV($oPersonalAddressBookProvider); + $oCarddavBackend = new \RainLoop\SabreDAV\CardDAV($oPersonalAddressBookProvider, $oAuthBackend); $oPrincipalBackend = new \RainLoop\SabreDAV\Principal($oPersonalAddressBookProvider, $oAuthBackend); @@ -740,7 +744,7 @@ class ServiceActions $oServer = new \Sabre\DAV\Server($aNodes); $aPath = \trim($this->oHttp->GetPath(), '/\\ '); - $oServer->setBaseUri((0 < \strlen($aPath) ? '/'.$aPath.'/' : '').'index.php/carddav/'); + $oServer->setBaseUri((0 < \strlen($aPath) ? '/'.$aPath : '').'/index.php/dav/'); // Plugins $oServer->addPlugin(new \Sabre\DAV\Auth\Plugin($oAuthBackend, 'SabreDAV')); diff --git a/rainloop/v/0.0.0/app/templates/Views/AdminSettingsContacts.html b/rainloop/v/0.0.0/app/templates/Views/AdminSettingsContacts.html index 038bdec5b..590c2091a 100644 --- a/rainloop/v/0.0.0/app/templates/Views/AdminSettingsContacts.html +++ b/rainloop/v/0.0.0/app/templates/Views/AdminSettingsContacts.html @@ -23,6 +23,10 @@ Allow contacts sharing +
diff --git a/rainloop/v/0.0.0/static/js/admin.js b/rainloop/v/0.0.0/static/js/admin.js index 8954505e2..b5973196a 100644 --- a/rainloop/v/0.0.0/static/js/admin.js +++ b/rainloop/v/0.0.0/static/js/admin.js @@ -5316,6 +5316,7 @@ function AdminContacts() this.defautOptionsAfterRender = Utils.defautOptionsAfterRender; this.enableContacts = ko.observable(!!RL.settingsGet('ContactsEnable')); this.contactsSharing = ko.observable(!!RL.settingsGet('ContactsSharing')); + this.contactsSync = ko.observable(!!RL.settingsGet('ContactsSync')); var aTypes = ['sqlite', 'mysql', 'pgsql'], @@ -5489,6 +5490,12 @@ AdminContacts.prototype.onBuild = function () 'ContactsSharing': bValue ? '1' : '0' }); }); + + self.contactsSync.subscribe(function (bValue) { + RL.remote().saveAdminConfig(null, { + 'ContactsSync': bValue ? '1' : '0' + }); + }); self.contactsType.subscribe(function (sValue) { RL.remote().saveAdminConfig(f5, { diff --git a/rainloop/v/0.0.0/static/js/app.js b/rainloop/v/0.0.0/static/js/app.js index c7f0aa66c..6248b9dbe 100644 --- a/rainloop/v/0.0.0/static/js/app.js +++ b/rainloop/v/0.0.0/static/js/app.js @@ -5784,6 +5784,7 @@ EmailModel.prototype.inputoTagLine = function () function ContactModel() { this.idContact = 0; + this.idContactStr = ''; this.display = ''; this.properties = []; this.readOnly = false; @@ -5835,6 +5836,7 @@ ContactModel.prototype.parse = function (oItem) if (oItem && 'Object/Contact' === oItem['@Object']) { this.idContact = Utils.pInt(oItem['IdContact']); + this.idContactStr = Utils.pString(oItem['IdContactStr']); this.display = Utils.pString(oItem['Display']); this.readOnly = !!oItem['ReadOnly']; this.scopeType = Utils.pInt(oItem['ScopeType']); @@ -9346,6 +9348,7 @@ function PopupsContactsViewModel() this.viewClearSearch = ko.observable(false); this.viewID = ko.observable(''); + this.viewIDStr = ko.observable(''); this.viewReadOnly = ko.observable(false); this.viewScopeType = ko.observable(Enums.ContactScopeType.Default); this.viewProperties = ko.observableArray([]); @@ -9549,13 +9552,14 @@ function PopupsContactsViewModel() self.viewID(Utils.pInt(oData.Result.ResultID)); } + if ('' === self.viewIDStr()) + { + self.viewIDStr(Utils.pString(oData.Result.ResultIDStr)); + } + self.reloadContactList(); bRes = true; } -// else -// { -// // TODO -// } _.delay(function () { self.viewSaveTrigger(bRes ? Enums.SaveSettingsStep.TrueResult : Enums.SaveSettingsStep.FalseResult); @@ -9568,7 +9572,7 @@ function PopupsContactsViewModel() }, 1000); } - }, sRequestUid, this.viewID(), this.viewScopeType(), aProperties); + }, sRequestUid, this.viewID(), this.viewIDStr(), this.viewScopeType(), aProperties); }, function () { var @@ -9712,6 +9716,7 @@ PopupsContactsViewModel.prototype.populateViewContact = function (oContact) { var sId = '', + sIdStr = '', bHasName = false, aList = [] ; @@ -9725,6 +9730,7 @@ PopupsContactsViewModel.prototype.populateViewContact = function (oContact) if (oContact) { sId = oContact.idContact; + sIdStr = oContact.idContactStr; if (Utils.isNonEmptyArray(oContact.properties)) { @@ -9752,6 +9758,7 @@ PopupsContactsViewModel.prototype.populateViewContact = function (oContact) } this.viewID(sId); + this.viewIDStr(sIdStr); this.viewProperties([]); this.viewProperties(aList); @@ -15021,11 +15028,12 @@ WebMailAjaxRemoteStorage.prototype.contacts = function (fCallback, iOffset, iLim /** * @param {?Function} fCallback */ -WebMailAjaxRemoteStorage.prototype.contactSave = function (fCallback, sRequestUid, sUid, nScopeType, aProperties) +WebMailAjaxRemoteStorage.prototype.contactSave = function (fCallback, sRequestUid, sUid, sUidStr, nScopeType, aProperties) { this.defaultRequest(fCallback, 'ContactSave', { 'RequestUid': sRequestUid, 'Uid': Utils.trim(sUid), + 'UidStr': Utils.trim(sUidStr), 'ScopeType': nScopeType, 'Properties': aProperties });