1: <?php
2: /**
3: * This code is licensed under AGPLv3 license or Afterlogic Software License
4: * if commercial version of the product was purchased.
5: * For full statements of the licenses see LICENSE-AFTERLOGIC and LICENSE-AGPL3 files.
6: */
7:
8: namespace Aurora\Modules\TeamContacts;
9:
10: use Afterlogic\DAV\Backend;
11: use Afterlogic\DAV\Constants;
12: use Aurora\Api;
13: use Aurora\Modules\Contacts\Enums\StorageType;
14: use Aurora\Modules\Contacts\Models\ContactCard;
15: use Aurora\Modules\Contacts\Module as ContactsModule;
16: use Aurora\System\Enums\UserRole;
17: use Sabre\VObject\UUIDUtil;
18: use Illuminate\Database\Capsule\Manager as Capsule;
19: use Aurora\System\Exceptions\ApiException;
20: use Aurora\Modules\Contacts\Enums\Access;
21:
22: /**
23: * @license https://www.gnu.org/licenses/agpl-3.0.html AGPL-3.0
24: * @license https://afterlogic.com/products/common-licensing Afterlogic Software License
25: * @copyright Copyright (c) 2023, Afterlogic Corp.
26: *
27: * @property Settings $oModuleSettings
28: *
29: * @package Modules
30: */
31: class Module extends \Aurora\System\Module\AbstractModule
32: {
33: protected static $iStorageOrder = 20;
34:
35: protected $userPublicIdToDelete = null;
36:
37: protected $teamAddressBook = null;
38:
39: protected $storagesMapToAddressbooks = [
40: StorageType::Team => Constants::ADDRESSBOOK_TEAM_NAME
41: ];
42:
43: public function init()
44: {
45: $this->subscribeEvent('Contacts::GetAddressBooks::after', array($this, 'onAfterGetAddressBooks'));
46: $this->subscribeEvent('Core::CreateUser::after', array($this, 'onAfterCreateUser'));
47: $this->subscribeEvent('Contacts::PrepareFiltersFromStorage', array($this, 'onPrepareFiltersFromStorage'));
48: $this->subscribeEvent('Contacts::GetContacts::after', array($this, 'onAfterGetContacts'));
49: $this->subscribeEvent('Contacts::GetContact::after', array($this, 'onAfterGetContact'));
50: $this->subscribeEvent('Core::DoServerInitializations::after', array($this, 'onAfterDoServerInitializations'));
51: $this->subscribeEvent('Contacts::CheckAccessToObject::after', array($this, 'onAfterCheckAccessToObject'));
52: $this->subscribeEvent('Contacts::GetContactSuggestions', array($this, 'onGetContactSuggestions'));
53: $this->subscribeEvent('Contacts::CheckAccessToAddressBook::after', array($this, 'onAfterCheckAccessToAddressBook'));
54: $this->subscribeEvent('Contacts::UpdateAddressBook::before', array($this, 'onBeforeUpdateAddressBook'));
55:
56: $this->subscribeEvent('Contacts::PopulateContactArguments', array($this, 'populateContactArguments'));
57: $this->subscribeEvent('Contacts::CreateContact::before', array($this, 'populateContactArguments'));
58: $this->subscribeEvent('Contacts::ContactQueryBuilder', array($this, 'onContactQueryBuilder'));
59:
60: $this->subscribeEvent('Core::DeleteUser::before', array($this, 'onBeforeDeleteUser'));
61: $this->subscribeEvent('Core::DeleteUser::after', array($this, 'onAfterDeleteUser'));
62:
63: $this->subscribeEvent('Contacts::UpdateContactObject::before', array($this, 'onBeforeUpdateContactObject'));
64: $this->subscribeEvent('Contacts::GetStoragesMapToAddressbooks::after', array($this, 'onAfterGetStoragesMapToAddressbooks'));
65:
66: $this->subscribeEvent('Core::GetGroupContactsEmails', array($this, 'onGetGroupContactsEmails'));
67: }
68:
69: /**
70: * @return Module
71: */
72: public static function getInstance()
73: {
74: return parent::getInstance();
75: }
76:
77: /**
78: * @return Module
79: */
80: public static function Decorator()
81: {
82: return parent::Decorator();
83: }
84:
85: /**
86: * @return Settings
87: */
88: public function getModuleSettings()
89: {
90: return $this->oModuleSettings;
91: }
92:
93: public function onAfterGetAddressBooks(&$aArgs, &$mResult)
94: {
95: if (!is_array($mResult)) {
96: $mResult = [];
97: }
98:
99: $addressbook = $this->GetTeamAddressbook($aArgs['UserId']);
100: if ($addressbook) {
101: /**
102: * @var array $addressbook
103: */
104: $mResult[] = [
105: 'Id' => 'team',
106: 'EntityId' => (int) $addressbook['id'],
107: 'CTag' => (int) $addressbook['{http://sabredav.org/ns}sync-token'],
108: 'Display' => true,
109: 'Order' => 1,
110: 'DisplayName' => $addressbook['{DAV:}displayname'],
111: 'Uri' => $addressbook['uri'],
112: 'Url' => $addressbook['uri'],
113: ];
114: }
115: }
116:
117: public function GetTeamAddressbook($UserId)
118: {
119: Api::CheckAccess($UserId);
120:
121: $addressbook = false;
122:
123: $oUser = Api::getUserById($UserId);
124: if ($oUser) {
125: $sPrincipalUri = Constants::PRINCIPALS_PREFIX . $oUser->IdTenant . '_' . Constants::DAV_TENANT_PRINCIPAL;
126: $addressbook = Backend::Carddav()->getAddressBookForUser($sPrincipalUri, 'gab');
127: if (!$addressbook) {
128: if (Backend::Carddav()->createAddressBook($sPrincipalUri, 'gab', ['{DAV:}displayname' => Constants::ADDRESSBOOK_TEAM_DISPLAY_NAME])) {
129: $addressbook = Backend::Carddav()->getAddressBookForUser($sPrincipalUri, 'gab');
130: }
131: }
132: if ($addressbook) {
133: $addressbook['id_tenant'] = $oUser->IdTenant;
134: }
135: }
136:
137: return $addressbook;
138: }
139:
140: private function createContactForUser($iUserId, $sEmail)
141: {
142: $mResult = false;
143: if (0 < $iUserId) {
144: $addressbook = $this->GetTeamAddressbook($iUserId);
145: if ($addressbook) {
146: $uid = UUIDUtil::getUUID();
147: $vcard = new \Sabre\VObject\Component\VCard(['UID' => $uid]);
148: $vcard->add(
149: 'EMAIL',
150: $sEmail,
151: [
152: 'type' => ['work'],
153: 'pref' => 1,
154: ]
155: );
156:
157: $mResult = !!Backend::Carddav()->createCard($addressbook['id'], $uid . '.vcf', $vcard->serialize());
158: }
159: }
160: return $mResult;
161: }
162:
163: public function onAfterCreateUser($aArgs, &$mResult)
164: {
165: $iUserId = isset($mResult) && (int) $mResult > 0 ? $mResult : 0;
166:
167: if ((int) $iUserId > 0) {
168: $this->createContactForUser($iUserId, $aArgs['PublicId']);
169: }
170: }
171:
172: public function onPrepareFiltersFromStorage(&$aArgs, &$mResult)
173: {
174: if (isset($aArgs['Storage']) && ($aArgs['Storage'] === StorageType::Team || $aArgs['Storage'] === StorageType::All)) {
175: $aArgs['IsValid'] = true;
176:
177: $oUser = \Aurora\System\Api::getAuthenticatedUser();
178:
179: $addressbook = $this->GetTeamAddressbook($oUser->Id);
180: if ($addressbook) {
181: if (isset($aArgs['Query'])) {
182: $aArgs['Query']->addSelect(Capsule::connection()->raw(
183: 'CASE
184: WHEN ' . Capsule::connection()->getTablePrefix() . 'adav_cards.addressbookid = ' . $addressbook['id'] . ' THEN true
185: ELSE false
186: END as IsTeam'
187: ));
188: }
189: $mResult = $mResult->orWhere('adav_cards.addressbookid', $addressbook['id']);
190: }
191: }
192: }
193:
194: public function onAfterGetContacts($aArgs, &$mResult)
195: {
196: if (\is_array($mResult) && \is_array($mResult['List'])) {
197: $user = Api::getUserById($aArgs['UserId']);
198: $teamAddressbook = $this->GetTeamAddressbook($aArgs['UserId']);
199:
200: if ($user && $teamAddressbook) {
201: $authenticatedUser = \Aurora\System\Api::getAuthenticatedUser();
202: foreach ($mResult['List'] as $iIndex => $aContact) {
203: $allowEditTeamContactsByTenantAdmins = ContactsModule::getInstance()->oModuleSettings->AllowEditTeamContactsByTenantAdmins;
204: $isUserTenantAdmin = $authenticatedUser->Role === UserRole::TenantAdmin && $user->IdTenant === $authenticatedUser->IdTenant;
205:
206: if (isset($aContact['AddressBookId']) && $aContact['AddressBookId'] == $teamAddressbook['id']) {
207: $aContact['ReadOnly'] = false;
208: if ($aContact['ViewEmail'] === $user->PublicId) {
209: $aContact['ItsMe'] = true;
210: } elseif (!(($allowEditTeamContactsByTenantAdmins && $isUserTenantAdmin) || $authenticatedUser->isAdmin())) {
211: $aContact['ReadOnly'] = true;
212: }
213: $mResult['List'][$iIndex] = $aContact;
214: }
215: }
216: }
217: }
218: }
219:
220: public function onAfterGetContact($aArgs, &$mResult)
221: {
222: $authenticatedUser = \Aurora\System\Api::getAuthenticatedUser();
223: $teamAddressbook = $this->GetTeamAddressbook($authenticatedUser->Id);
224: if ($teamAddressbook) {
225: if ($mResult && $authenticatedUser && $mResult->AddressBookId == $teamAddressbook['id']) {
226: $mResult->IdTenant = $teamAddressbook['id_tenant'] ?? 0;
227: $allowEditTeamContactsByTenantAdmins = ContactsModule::getInstance()->oModuleSettings->AllowEditTeamContactsByTenantAdmins;
228: $isUserTenantAdmin = $authenticatedUser->Role === UserRole::TenantAdmin;
229: $isContactInTenant = $mResult->IdTenant === $authenticatedUser->IdTenant;
230: if ($mResult->BusinessEmail === $authenticatedUser->PublicId) {
231: $mResult->ExtendedInformation['ItsMe'] = true;
232: } elseif (!(($allowEditTeamContactsByTenantAdmins && $isUserTenantAdmin && $isContactInTenant) || $authenticatedUser->isAdmin())) {
233: $mResult->ExtendedInformation['ReadOnly'] = true;
234: }
235: }
236: }
237: }
238:
239: /**
240: * Creates team contacts if they are missing within current tenant.
241: */
242: public function onAfterDoServerInitializations($aArgs, &$mResult)
243: {
244: $oUser = \Aurora\System\Api::getAuthenticatedUser();
245: if ($oUser && $oUser->Role === UserRole::NormalUser) {
246: $teamAddressBook = $this->GetTeamAddressbook($oUser->Id);
247: if ($teamAddressBook) {
248: $contact = Capsule::connection()->table('contacts_cards')
249: ->where('AddressBookId', $teamAddressBook['id'])
250: ->where('ViewEmail', $oUser->PublicId)
251: ->first();
252: if (!$contact) {
253: $this->createContactForUser($oUser->Id, $oUser->PublicId);
254: }
255: }
256: }
257: }
258:
259: public function onGetContactSuggestions(&$aArgs, &$mResult)
260: {
261: if ($aArgs['Storage'] === 'all' || $aArgs['Storage'] === StorageType::Team) {
262: $mResult[StorageType::Team] = \Aurora\Modules\Contacts\Module::Decorator()->GetContacts(
263: $aArgs['UserId'],
264: StorageType::Team,
265: 0,
266: $aArgs['Limit'],
267: $aArgs['SortField'],
268: $aArgs['SortOrder'],
269: $aArgs['Search']
270: );
271: }
272: }
273:
274: /**
275: *
276: */
277: public function populateContactArguments(&$aArgs, &$mResult)
278: {
279: if (isset($aArgs['Storage'], $aArgs['UserId'])) {
280: $aStorageParts = \explode('-', $aArgs['Storage']);
281: if (isset($aStorageParts[0]) && $aStorageParts[0] === StorageType::Team) {
282:
283: $addressbook = $this->GetTeamAddressbook($aArgs['UserId']);
284: if ($addressbook) {
285: $aArgs['Storage'] = StorageType::Team;
286: $aArgs['AddressBookId'] = $addressbook['id'];
287:
288: $mResult = true;
289: }
290: }
291: }
292: }
293:
294: public function onContactQueryBuilder(&$aArgs, &$query)
295: {
296: $addressbook = $this->GetTeamAddressbook($aArgs['UserId']);
297: $query->orWhere(function ($q) use ($addressbook, $aArgs) {
298: $q->where('adav_addressbooks.id', $addressbook['id']);
299: if (is_array($aArgs['UUID'])) {
300: $ids = $aArgs['UUID'];
301: if (count($aArgs['UUID']) === 0) {
302: $ids = [null];
303: }
304: $q->whereIn('adav_cards.id', $ids);
305: } else {
306: $q->where('adav_cards.id', $aArgs['UUID']);
307: }
308: });
309: }
310:
311: public function onBeforeDeleteUser(&$aArgs, &$mResult)
312: {
313: if (isset($aArgs['UserId'])) {
314: $this->userPublicIdToDelete = Api::getUserPublicIdById($aArgs['UserId']);
315: $this->teamAddressBook = $this->GetTeamAddressbook($aArgs['UserId']);
316: }
317: }
318:
319: public function onAfterDeleteUser($aArgs, &$mResult)
320: {
321: if ($mResult && $this->userPublicIdToDelete && $this->teamAddressBook) {
322: $card = Capsule::connection()->table('contacts_cards')
323: ->join('adav_cards', 'contacts_cards.CardId', '=', 'adav_cards.id')
324: ->join('adav_addressbooks', 'adav_cards.addressbookid', '=', 'adav_addressbooks.id')
325: ->where('adav_addressbooks.id', $this->teamAddressBook['id'])
326: ->where('ViewEmail', $this->userPublicIdToDelete)
327: ->select('adav_cards.uri as card_uri', 'adav_addressbooks.id as addressbook_id')
328: ->first();
329:
330: if ($card) {
331: Backend::Carddav()->deleteCard($card->addressbook_id, $card->card_uri);
332: }
333: }
334: }
335:
336: public function onAfterCheckAccessToAddressBook(&$aArgs, &$mResult)
337: {
338: if (isset($aArgs['User'], $aArgs['AddressBookId'])) {
339: $oUser = $aArgs['User'];
340: $addressbook = $this->GetTeamAddressbook($oUser->Id);
341: if ($addressbook && $addressbook['id'] == $aArgs['AddressBookId']) {
342: if ($aArgs['Access'] === Access::Write && $oUser->UserRole !== UserRole::SuperAdmin) {
343: if ($oUser->UserRole === UserRole::TenantAdmin && ContactsModule::getInstance()->oModuleSettings->AllowEditTeamContactsByTenantAdmins) {
344: $mResult = true;
345: } else {
346: $mResult = false;
347: }
348: } else {
349: $mResult = true;
350: }
351: return true;
352: }
353: }
354: }
355:
356: public function onAfterCheckAccessToObject(&$aArgs, &$mResult)
357: {
358: $oUser = $aArgs['User'] ?? null;
359: $oContact = $aArgs['Contact'] ?? null;
360:
361: if ($oUser) {
362: $teamAddressBook = $this->GetTeamAddressbook($oUser->Id);
363: if ($oContact instanceof \Aurora\Modules\Contacts\Classes\Contact && (int) $oContact->AddressBookId === (int) $teamAddressBook['id']) {
364: if ($aArgs['Access'] === Access::Write && $aArgs['User']->UserRole !== UserRole::SuperAdmin) {
365: if ((isset($oContact->ExtendedInformation['ItsMe']) && $oContact->ExtendedInformation['ItsMe']) || // ItsMe
366: ($oUser->Role === UserRole::TenantAdmin && ContactsModule::getInstance()->oModuleSettings->AllowEditTeamContactsByTenantAdmins)) {
367: $mResult = true;
368: } else {
369: $mResult = false;
370: }
371: } else { // is SuperAdmin
372: $mResult = true;
373: }
374: return true;
375: }
376: }
377: }
378:
379: public function onBeforeUpdateContactObject(&$aArgs, &$mResult)
380: {
381: $user = Api::getAuthenticatedUser();
382: $oContact = $aArgs['Contact'] ?? null;
383:
384: if ($user && $oContact) {
385: $addressbook = Backend::Carddav()->getAddressBookById($oContact->AddressBookId);
386: if ($addressbook['uri'] === 'gab') {
387: $teamAddressbook = $this->GetTeamAddressbook($user->Id);
388:
389: $isSuperAdmin = $user->Role === UserRole::SuperAdmin;
390: $isTenant = $user->Role === UserRole::TenantAdmin;
391: $isCorrectTeamAddressbook = $teamAddressbook['id'] == $oContact->AddressBookId;
392: $isItsMe = isset($oContact->ExtendedInformation['ItsMe']) && $oContact->ExtendedInformation['ItsMe'];
393: $isReadOnly = isset($oContact->ExtendedInformation['ReadOnly']) && $oContact->ExtendedInformation['ReadOnly'];
394:
395: if (!($isSuperAdmin || ($isTenant && !$isReadOnly && $isCorrectTeamAddressbook) || $isItsMe)) {
396: throw new ApiException(\Aurora\System\Notifications::AccessDenied, null, 'AccessDenied');
397: }
398: }
399: }
400: }
401:
402: public function onBeforeUpdateAddressBook(&$aArgs, &$mResult)
403: {
404: $addressbook = Backend::Carddav()->getAddressBookById($aArgs['EntityId']);
405: if ($addressbook && $addressbook['uri'] === 'gab') {
406: throw new ApiException(\Aurora\System\Notifications::AccessDenied, null, 'AccessDenied');
407: }
408: }
409:
410: public function onAfterGetStoragesMapToAddressbooks(&$aArgs, &$mResult)
411: {
412: $mResult = array_merge($mResult, $this->storagesMapToAddressbooks);
413: }
414:
415: public function onGetGroupContactsEmails(&$aArgs, &$mResult)
416: {
417: $oUser = $aArgs['User'];
418: $oGroup = $aArgs['Group'];
419: if ($oUser && $oGroup) {
420: $abook = $this->GetTeamAddressbook($oUser->Id);
421: if ($abook) {
422: if ($oGroup->IsAll) {
423: $mResult = ContactCard::where('AddressBookId', $abook['id'])->get()->map(
424: function (ContactCard $oContact) {
425: if (!empty($oContact->FullName)) {
426: return '"' . $oContact->FullName . '"' . '<' . $oContact->ViewEmail . '>';
427: } else {
428: return $oContact->ViewEmail;
429: }
430: }
431: )->toArray();
432: } else {
433: $mResult = $oGroup->Users->map(function ($oUser) use ($abook) {
434: $oContact = ContactCard::where('IdUser', $oUser->Id)->where('AddressBookId', $abook['id'])->first();
435: if ($oContact && !empty($oContact->FullName)) {
436: return '"' . $oContact->FullName . '"' . '<' . $oUser->PublicId . '>';
437: } else {
438: return $oUser->PublicId;
439: }
440: })->toArray();
441: }
442: }
443: }
444: }
445: }
446: