Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions lib/Controller/ContactIntegrationController.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public function __construct(string $appName,
*/
#[TrapError]
public function match(string $mail): JSONResponse {
return (new JSONResponse($this->service->findMatches($mail)))->cacheFor(60 * 60, false, true);
return (new JSONResponse($this->service->findMatches($this->uid, $mail)))->cacheFor(60 * 60, false, true);
}

/**
Expand Down Expand Up @@ -92,7 +92,7 @@ public function autoComplete(string $term): JSONResponse {
return new JSONResponse($decoded);
}
}
$res = $this->service->autoComplete($term);
$res = $this->service->autoComplete($this->uid, $term);
$this->cache->set("{$this->uid}:$term", json_encode($res), 24 * 3600);
return new JSONResponse($res);
}
Expand Down
2 changes: 1 addition & 1 deletion lib/IMAP/ImapMessageFetcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -549,7 +549,7 @@ private function parseHeaders(Horde_Imap_Client_Data_Fetch $fetch): void {
$this->hasDkimSignature = $dkimSignatureHeader !== null;

if ($this->runPhishingCheck) {
$this->phishingDetails = $this->phishingDetectionService->checkHeadersForPhishing($parsedHeaders, $fetch->getFlags(), $this->hasHtmlMessage, $this->htmlMessage);
$this->phishingDetails = $this->phishingDetectionService->checkHeadersForPhishing($this->userId, $parsedHeaders, $fetch->getFlags(), $this->hasHtmlMessage, $this->htmlMessage);
}

$listUnsubscribeHeader = $parsedHeaders->getHeader('list-unsubscribe');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ public function __construct(ContactsIntegration $ci) {
$this->contactsIntegration = $ci;
}

public function findMatches(string $mail): array {
$matches = $this->contactsIntegration->getContactsWithMail($mail);
public function findMatches(string $uid, string $mail): array {
$matches = $this->contactsIntegration->getContactsWithMail($uid, $mail);
return $matches;
}

Expand All @@ -32,7 +32,7 @@ public function newContact(string $name, string $mail): ?array {
return $this->contactsIntegration->newContact($name, $mail);
}

public function autoComplete(string $term): array {
return $this->contactsIntegration->getContactsWithName($term);
public function autoComplete(string $uid, string $term): array {
return $this->contactsIntegration->getContactsWithName($uid, $term);
}
}
172 changes: 94 additions & 78 deletions lib/Service/ContactsIntegration.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use OCP\IConfig;
use OCP\IGroupManager;
use OCP\IUserManager;
use function is_array;

class ContactsIntegration {
/** @var IManager */
Expand Down Expand Up @@ -46,55 +47,9 @@ public function __construct(IManager $contactsManager,
* @return array
*/
public function getMatchingRecipient(string $userId, string $term): array {
if (!$this->contactsManager->isEnabled()) {
return [];
}

// If 'Allow username autocompletion in share dialog' is disabled in the admin sharing settings, then we must not
// auto-complete system users
$shareeEnumeration = $this->config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes') === 'yes';
$shareeEnumerationInGroupOnly = $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_group', 'no') === 'yes';
$shareeEnumerationFullMatch = $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_full_match', 'yes') === 'yes';
$shareeEnumerationFullMatchUserId = $shareeEnumerationFullMatch && $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_full_match_userid', 'yes') === 'yes';
$shareeEnumerationFullMatchEmail = $shareeEnumerationFullMatch && $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_full_match_email', 'yes') === 'yes';

$result = $this->contactsManager->search(
$term,
['UID', 'FN', 'EMAIL'],
[
'enumeration' => $shareeEnumeration,
'fullmatch' => $shareeEnumerationFullMatch,
'limit' => 20,
],
);
if (empty($result)) {
return [];
}
$result = $this->search($userId, $term, ['UID', 'FN', 'EMAIL']);
$receivers = [];

if ($shareeEnumeration && $shareeEnumerationInGroupOnly) {
$user = $this->userManager->get($userId);
if ($user === null) {
return [];
}
$userGroups = $this->groupManager->getUserGroupIds($user);
}

foreach ($result as $r) {
$isSystemUser = isset($r['isLocalSystemBook']) && $r['isLocalSystemBook'];
$isInSameGroup = false;
if ($isSystemUser && $shareeEnumerationInGroupOnly) {
foreach ($userGroups as $userGroup) {
if ($this->groupManager->isInGroup($r['UID'], $userGroup)) {
$isInSameGroup = true;
break;
}
}
if (!$shareeEnumerationFullMatch && !$isInSameGroup) {
continue;
}
}

$id = $r['UID'];
$fn = $r['FN'] ?? null;
if (!isset($r['EMAIL'])) {
Expand All @@ -111,23 +66,10 @@ public function getMatchingRecipient(string $userId, string $term): array {
if ($e === '') {
continue;
}
$lowerTerm = strtolower($term);

if ($isSystemUser && $shareeEnumerationInGroupOnly && !$isInSameGroup) {
// Check for full match. If full match is disabled, matching results already filtered out
if (!($lowerTerm !== '' && (
($shareeEnumerationFullMatch && !empty($fn) && $lowerTerm === strtolower($fn))
|| ($shareeEnumerationFullMatchUserId && $lowerTerm === strtolower($id))
|| ($shareeEnumerationFullMatchEmail && $lowerTerm === strtolower($e))))) {
// Not a full Match
continue;
}
}

$receivers[] = [
'id' => $id,
// Show full name if possible or fall back to email
'label' => $fn,
'label' => $fn ?? $e,
'email' => $e,
'photo' => $photo,
'source' => 'contacts',
Expand Down Expand Up @@ -229,21 +171,98 @@ public function newContact(string $name, string $mailAddr, string $type = 'HOME'
return $createdContact;
}

private function search(string $userId, string $term, array $fields, ?bool $strictSearch = null): array {
if (!$this->contactsManager->isEnabled()) {
return [];
}

// If 'Allow username autocompletion in share dialog' is disabled in the admin sharing settings, then we must not
// auto-complete system users
$shareeEnumeration = $this->config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes') === 'yes';
$shareeEnumerationInGroupOnly = $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_group', 'no') === 'yes';
$shareeEnumerationFullMatch = $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_full_match', 'yes') === 'yes';
$shareeEnumerationFullMatchDisplayName = $shareeEnumerationFullMatch && $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_full_match_displayname', 'yes') === 'yes';
$shareeEnumerationFullMatchUserId = $shareeEnumerationFullMatch && $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_full_match_userid', 'yes') === 'yes';
$shareeEnumerationFullMatchEmail = $shareeEnumerationFullMatch && $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_full_match_email', 'yes') === 'yes';

$options = [
'enumeration' => $shareeEnumeration,
'fullmatch' => $shareeEnumerationFullMatch,
'limit' => 20,
];
if ($strictSearch !== null) {
$options['strict_search'] = $strictSearch;
}

$result = $this->contactsManager->search(
$term,
$fields,
$options,
);

$userGroups = [];
if ($shareeEnumeration && $shareeEnumerationInGroupOnly) {
$user = $this->userManager->get($userId);
if ($user === null) {
return [];
}
$userGroups = $this->groupManager->getUserGroupIds($user);
}

$filteredResults = [];
foreach ($result as $r) {
$isSystemUser = isset($r['isLocalSystemBook']) && $r['isLocalSystemBook'];
$isInSameGroup = false;
if ($isSystemUser && $shareeEnumerationInGroupOnly) {
foreach ($userGroups as $userGroup) {
if ($this->groupManager->isInGroup($r['UID'], $userGroup)) {
$isInSameGroup = true;
break;
}
}
if (!$shareeEnumerationFullMatch && !$isInSameGroup) {
continue;
}
}

if ($isSystemUser && $shareeEnumerationInGroupOnly && !$isInSameGroup && $shareeEnumerationFullMatch) {
// Check for full match. If full match is disabled, non-matching results already filtered out above.
$id = $r['UID'];
$fn = $r['FN'] ?? null;
$lowerTerm = strtolower($term);
$isMatch = ($lowerTerm !== '' && (
($shareeEnumerationFullMatchDisplayName && !empty($fn) && $lowerTerm === strtolower($fn))
|| ($shareeEnumerationFullMatchUserId && $lowerTerm === strtolower($id)))) ;
if ($shareeEnumerationFullMatchEmail && !$isMatch) {
$email = $r['EMAIL'] ?? null;
if ($email === null) {
continue;
}
$emails = is_array($email) ? $email : [$email];
foreach ($emails as $e) {
if ($lowerTerm === strtolower($e)) {
$isMatch = true;
break;
}
}
}
if (!$isMatch) {
continue;
}
}

$filteredResults[] = $r;
}
return $filteredResults;
}

/**
* @param string[] $fields
*/
private function doSearch(string $term, array $fields, bool $strictSearch) : array {
$allowSystemUsers = $this->config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes') === 'yes';

$result = $this->contactsManager->search($term, $fields, [
'strict_search' => $strictSearch,
'limit' => 20,
]);
private function doSearch(string $userId, string $term, array $fields, bool $strictSearch) : array {
$result = $this->search($userId, $term, $fields, $strictSearch);
$matches = [];
foreach ($result as $r) {
if (!$allowSystemUsers && isset($r['isLocalSystemBook']) && $r['isLocalSystemBook']) {
continue;
}
$id = $r['UID'];
$fn = $r['FN'];
$email = $r['EMAIL'] ?? null;
Expand All @@ -258,18 +277,15 @@ private function doSearch(string $term, array $fields, bool $strictSearch) : arr

/**
* Extracts all Contacts with the specified mail address
*
* @param string $mailAddr
* @return array
*/
public function getContactsWithMail(string $mailAddr) {
return $this->doSearch($mailAddr, ['EMAIL'], true);
public function getContactsWithMail(string $userId, string $mailAddr): array {
return $this->doSearch($userId, $mailAddr, ['EMAIL'], true);
}

/**
* Extracts all Contacts with the specified name
*/
public function getContactsWithName(string $name): array {
return $this->doSearch($name, ['FN'], false);
public function getContactsWithName(string $userId, string $name): array {
return $this->doSearch($userId, $name, ['FN'], false);
}
}
4 changes: 2 additions & 2 deletions lib/Service/PhishingDetection/ContactCheck.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ public function __construct(
$this->contactIntegration = $contactIntegration;
}

public function run(string $fn, string $email): PhishingDetectionResult {
public function run(string $userId, string $fn, string $email): PhishingDetectionResult {
$emails = [];
$contacts = $this->contactIntegration->getContactsWithName($fn);
$contacts = $this->contactIntegration->getContactsWithName($userId, $fn);
foreach ($contacts as $contact) {
if (!isset($contact['email'])) {
continue;
Expand Down
4 changes: 2 additions & 2 deletions lib/Service/PhishingDetection/PhishingDetectionService.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public function __construct(
* @return array
* @throws \Exception
*/
public function checkHeadersForPhishing(Horde_Mime_Headers $headers, array $flags, bool $hasHtmlMessage, string $htmlMessage = ''): array {
public function checkHeadersForPhishing(string $uid, Horde_Mime_Headers $headers, array $flags, bool $hasHtmlMessage, string $htmlMessage = ''): array {
/** @var string|null $fromFN */
$fromFN = null;
/** @var string|null $fromEmail */
Expand Down Expand Up @@ -66,7 +66,7 @@ public function checkHeadersForPhishing(Horde_Mime_Headers $headers, array $flag
$list->addCheck($this->replyToCheck->run($fromEmail, $replyToEmail));
}
if ($fromFN !== null) {
$list->addCheck($this->contactCheck->run($fromFN, $fromEmail));
$list->addCheck($this->contactCheck->run($uid, $fromFN, $fromEmail));
}
if ($customEmail !== null) {
$list->addCheck($this->customEmailCheck->run($fromEmail, $customEmail));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,10 @@ protected function setUp(): void {
public function testContactCheck(): void {
$this->contactsIntegration->expects(self::once())
->method('getContactsWithName')
->with('John Doe')
->with('batman', 'John Doe')
->willReturn([['id' => 1, 'fn' => 'John Doe', 'email' => ['jhon@example.org','Doe@example.org']]]);

$result = $this->contactCheck->run('John Doe', 'jhon.doe@example.org');
$result = $this->contactCheck->run('batman', 'John Doe', 'jhon.doe@example.org');
$this->assertTrue($result->isPhishing());
}

Expand All @@ -73,7 +73,7 @@ public function testCheckHeadersForPhishing(): void {
$headerStream = fopen(__DIR__ . '/../../../data/phishing-mail-headers.txt', 'r');
$parsedHeaders = Horde_Mime_Headers::parseHeaders($headerStream);
fclose($headerStream);
$result = $this->service->checkHeadersForPhishing($parsedHeaders, [], false);
$result = $this->service->checkHeadersForPhishing('batman', $parsedHeaders, [], false);
$this->assertTrue($result['warning']);
}
}
10 changes: 5 additions & 5 deletions tests/Unit/Controller/ContactIntegrationControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ public function testMatch(): void {

$this->service->expects($this->once())
->method('findMatches')
->with($mail)
->with($this->userId, $mail)
->willReturn($expected);

$response = $this->controller->match($mail);
Expand Down Expand Up @@ -170,7 +170,7 @@ public function testAutoCompleteCacheMissCallsService(): void {

$this->service->expects($this->once())
->method('autoComplete')
->with($term)
->with($this->userId, $term)
->willReturn($serviceResult);

$this->cache->expects($this->once())
Expand Down Expand Up @@ -201,7 +201,7 @@ public function testAutoCompleteCacheInvalidJsonFallsBackToService(): void {

$this->service->expects($this->once())
->method('autoComplete')
->with($term)
->with($this->userId, $term)
->willReturn($serviceResult);

$this->cache->expects($this->once())
Expand All @@ -228,7 +228,7 @@ public function testAutoCompleteEmptyResult(): void {

$this->service->expects($this->once())
->method('autoComplete')
->with($term)
->with($this->userId, $term)
->willReturn([]);

$this->cache->expects($this->once())
Expand Down Expand Up @@ -256,7 +256,7 @@ public function testAutoCompleteCacheKeyIsUserSpecific(): void {

$this->service->expects($this->once())
->method('autoComplete')
->with($term)
->with($this->userId, $term)
->willReturn([]);

$this->cache->expects($this->once())
Expand Down
Loading
Loading