Signed-off-by: Christoph Wurst <christoph@winzerhof-wurst.at>tags/v16.0.0alpha1
@@ -94,6 +94,11 @@ return [ | |||
'url' => '/api/v1/sharees', | |||
'verb' => 'GET', | |||
], | |||
[ | |||
'name' => 'ShareesAPI#findRecommended', | |||
'url' => '/api/v1/sharees_recommended', | |||
'verb' => 'GET', | |||
], | |||
/* | |||
* Remote Shares | |||
*/ |
@@ -29,17 +29,29 @@ declare(strict_types=1); | |||
*/ | |||
namespace OCA\Files_Sharing\Controller; | |||
use function array_filter; | |||
use function array_slice; | |||
use function array_values; | |||
use Generator; | |||
use OC\Collaboration\Collaborators\SearchResult; | |||
use OCP\AppFramework\Http\DataResponse; | |||
use OCP\AppFramework\OCS\OCSBadRequestException; | |||
use OCP\AppFramework\OCSController; | |||
use OCP\Collaboration\Collaborators\ISearch; | |||
use OCP\Collaboration\Collaborators\ISearchResult; | |||
use OCP\Collaboration\Collaborators\SearchResultType; | |||
use OCP\IRequest; | |||
use OCP\IConfig; | |||
use OCP\IURLGenerator; | |||
use OCP\Share; | |||
use OCP\Share\IManager; | |||
use function usort; | |||
class ShareesAPIController extends OCSController { | |||
/** @var userId */ | |||
protected $userId; | |||
/** @var IConfig */ | |||
protected $config; | |||
@@ -87,6 +99,7 @@ class ShareesAPIController extends OCSController { | |||
private $collaboratorSearch; | |||
/** | |||
* @param string $UserId | |||
* @param string $appName | |||
* @param IRequest $request | |||
* @param IConfig $config | |||
@@ -95,6 +108,7 @@ class ShareesAPIController extends OCSController { | |||
* @param ISearch $collaboratorSearch | |||
*/ | |||
public function __construct( | |||
$UserId, | |||
string $appName, | |||
IRequest $request, | |||
IConfig $config, | |||
@@ -103,7 +117,7 @@ class ShareesAPIController extends OCSController { | |||
ISearch $collaboratorSearch | |||
) { | |||
parent::__construct($appName, $request); | |||
$this->userId = $UserId; | |||
$this->config = $config; | |||
$this->urlGenerator = $urlGenerator; | |||
$this->shareManager = $shareManager; | |||
@@ -212,6 +226,148 @@ class ShareesAPIController extends OCSController { | |||
return $response; | |||
} | |||
/** | |||
* @param string $user | |||
* @param int $shareType | |||
* | |||
* @return Generator<array<string>> | |||
*/ | |||
private function getAllShareesByType(string $user, int $shareType): Generator { | |||
$offset = 0; | |||
$pageSize = 50; | |||
while (count($page = $this->shareManager->getSharesBy( | |||
$user, | |||
$shareType, | |||
null, | |||
false, | |||
$pageSize, | |||
$offset | |||
))) { | |||
foreach ($page as $share) { | |||
yield [$share->getSharedWith(), $share->getSharedWithDisplayName() ?? $share->getSharedWith()]; | |||
} | |||
$offset += $pageSize; | |||
} | |||
} | |||
private function sortShareesByFrequency(array $sharees): array { | |||
usort($sharees, function(array $s1, array $s2) { | |||
return $s2['count'] - $s1['count']; | |||
}); | |||
return $sharees; | |||
} | |||
private $searchResultTypeMap = [ | |||
Share::SHARE_TYPE_USER => 'users', | |||
Share::SHARE_TYPE_GROUP => 'groups', | |||
Share::SHARE_TYPE_REMOTE => 'remotes', | |||
Share::SHARE_TYPE_REMOTE_GROUP => 'remote_groups', | |||
Share::SHARE_TYPE_EMAIL => 'emails', | |||
]; | |||
private function getAllSharees(string $user, array $shareTypes): ISearchResult { | |||
$result = []; | |||
foreach ($shareTypes as $shareType) { | |||
$sharees = $this->getAllShareesByType($user, $shareType); | |||
$shareTypeResults = []; | |||
foreach ($sharees as list($sharee, $displayname)) { | |||
if (!isset($this->searchResultTypeMap[$shareType])) { | |||
continue; | |||
} | |||
if (!isset($shareTypeResults[$sharee])) { | |||
$shareTypeResults[$sharee] = [ | |||
'count' => 1, | |||
'label' => $displayname, | |||
'value' => [ | |||
'shareType' => $shareType, | |||
'shareWith' => $sharee, | |||
], | |||
]; | |||
} else { | |||
$shareTypeResults[$sharee]['count']++; | |||
} | |||
} | |||
$result = array_merge($result, array_values($shareTypeResults)); | |||
} | |||
$top5 = array_slice( | |||
$this->sortShareesByFrequency($result), | |||
0, | |||
5 | |||
); | |||
$searchResult = new SearchResult(); | |||
foreach ($this->searchResultTypeMap as $int => $str) { | |||
$searchResult->addResultSet(new SearchResultType($str), [], []); | |||
foreach ($top5 as $x) { | |||
if ($x['value']['shareType'] === $int) { | |||
$searchResult->addResultSet(new SearchResultType($str), [], [$x]); | |||
} | |||
} | |||
} | |||
return $searchResult; | |||
} | |||
/** | |||
* @NoAdminRequired | |||
* | |||
* @param string $itemType | |||
* @return DataResponse | |||
* @throws OCSBadRequestException | |||
*/ | |||
public function findRecommended(string $itemType = null, $shareType = null): DataResponse { | |||
$shareTypes = [ | |||
Share::SHARE_TYPE_USER, | |||
]; | |||
if ($itemType === null) { | |||
throw new OCSBadRequestException('Missing itemType'); | |||
} elseif ($itemType === 'file' || $itemType === 'folder') { | |||
if ($this->shareManager->allowGroupSharing()) { | |||
$shareTypes[] = Share::SHARE_TYPE_GROUP; | |||
} | |||
if ($this->isRemoteSharingAllowed($itemType)) { | |||
$shareTypes[] = Share::SHARE_TYPE_REMOTE; | |||
} | |||
if ($this->isRemoteGroupSharingAllowed($itemType)) { | |||
$shareTypes[] = Share::SHARE_TYPE_REMOTE_GROUP; | |||
} | |||
if ($this->shareManager->shareProviderExists(Share::SHARE_TYPE_EMAIL)) { | |||
$shareTypes[] = Share::SHARE_TYPE_EMAIL; | |||
} | |||
if ($this->shareManager->shareProviderExists(Share::SHARE_TYPE_ROOM)) { | |||
$shareTypes[] = Share::SHARE_TYPE_ROOM; | |||
} | |||
} else { | |||
$shareTypes[] = Share::SHARE_TYPE_GROUP; | |||
$shareTypes[] = Share::SHARE_TYPE_EMAIL; | |||
} | |||
// FIXME: DI | |||
if (\OC::$server->getAppManager()->isEnabledForUser('circles') && class_exists('\OCA\Circles\ShareByCircleProvider')) { | |||
$shareTypes[] = Share::SHARE_TYPE_CIRCLE; | |||
} | |||
if (isset($_GET['shareType']) && is_array($_GET['shareType'])) { | |||
$shareTypes = array_intersect($shareTypes, $_GET['shareType']); | |||
sort($shareTypes); | |||
} else if (is_numeric($shareType)) { | |||
$shareTypes = array_intersect($shareTypes, [(int) $shareType]); | |||
sort($shareTypes); | |||
} | |||
return new DataResponse( | |||
$this->getAllSharees($this->userId, $shareTypes)->asArray() | |||
); | |||
} | |||
/** | |||
* Method to get out the static call for better testing | |||
* |
@@ -50,6 +50,9 @@ class ShareesAPIControllerTest extends TestCase { | |||
/** @var ShareesAPIController */ | |||
protected $sharees; | |||
/** @var string */ | |||
protected $uid; | |||
/** @var IRequest|\PHPUnit_Framework_MockObject_MockObject */ | |||
protected $request; | |||
@@ -62,6 +65,7 @@ class ShareesAPIControllerTest extends TestCase { | |||
protected function setUp() { | |||
parent::setUp(); | |||
$this->uid = 'test123'; | |||
$this->request = $this->createMock(IRequest::class); | |||
$this->shareManager = $this->createMock(IManager::class); | |||
@@ -74,6 +78,7 @@ class ShareesAPIControllerTest extends TestCase { | |||
$this->collaboratorSearch = $this->createMock(ISearch::class); | |||
$this->sharees = new ShareesAPIController( | |||
$this->uid, | |||
'files_sharing', | |||
$this->request, | |||
$configMock, | |||
@@ -243,6 +248,8 @@ class ShareesAPIControllerTest extends TestCase { | |||
->method('allowGroupSharing') | |||
->willReturn($allowGroupSharing); | |||
/** @var string */ | |||
$uid = 'test123'; | |||
/** @var IRequest|\PHPUnit_Framework_MockObject_MockObject $request */ | |||
$request = $this->createMock(IRequest::class); | |||
/** @var IURLGenerator|\PHPUnit_Framework_MockObject_MockObject $urlGenerator */ | |||
@@ -251,6 +258,7 @@ class ShareesAPIControllerTest extends TestCase { | |||
/** @var \PHPUnit_Framework_MockObject_MockObject|\OCA\Files_Sharing\Controller\ShareesAPIController $sharees */ | |||
$sharees = $this->getMockBuilder('\OCA\Files_Sharing\Controller\ShareesAPIController') | |||
->setConstructorArgs([ | |||
$uid, | |||
'files_sharing', | |||
$request, | |||
$config, | |||
@@ -335,6 +343,8 @@ class ShareesAPIControllerTest extends TestCase { | |||
$config->expects($this->never()) | |||
->method('getAppValue'); | |||
/** @var string */ | |||
$uid = 'test123'; | |||
/** @var IRequest|\PHPUnit_Framework_MockObject_MockObject $request */ | |||
$request = $this->createMock(IRequest::class); | |||
/** @var IURLGenerator|\PHPUnit_Framework_MockObject_MockObject $urlGenerator */ | |||
@@ -343,6 +353,7 @@ class ShareesAPIControllerTest extends TestCase { | |||
/** @var \PHPUnit_Framework_MockObject_MockObject|\OCA\Files_Sharing\Controller\ShareesAPIController $sharees */ | |||
$sharees = $this->getMockBuilder('\OCA\Files_Sharing\Controller\ShareesAPIController') | |||
->setConstructorArgs([ | |||
$uid, | |||
'files_sharing', | |||
$request, | |||
$config, |
@@ -50,6 +50,9 @@ | |||
/** @type {object} **/ | |||
_lastSuggestions: undefined, | |||
/** @type {object} **/ | |||
_lastRecommendations: undefined, | |||
/** @type {int} **/ | |||
_pendingOperationsCount: 0, | |||
@@ -382,7 +385,299 @@ | |||
return this._lastSuggestions.promise; | |||
}, | |||
_getRecommendations: function(model) { | |||
if (this._lastRecommendations && | |||
this._lastRecommendations.model === model) { | |||
return this._lastRecommendations.promise; | |||
} | |||
var deferred = $.Deferred(); | |||
$.get( | |||
OC.linkToOCS('apps/files_sharing/api/v1') + 'sharees_recommended', | |||
{ | |||
format: 'json', | |||
itemType: model.get('itemType') | |||
}, | |||
function (result) { | |||
if (result.ocs.meta.statuscode === 100) { | |||
var filter = function(users, groups, remotes, remote_groups, emails, circles, rooms) { | |||
if (typeof(emails) === 'undefined') { | |||
emails = []; | |||
} | |||
if (typeof(circles) === 'undefined') { | |||
circles = []; | |||
} | |||
if (typeof(rooms) === 'undefined') { | |||
rooms = []; | |||
} | |||
var usersLength; | |||
var groupsLength; | |||
var remotesLength; | |||
var remoteGroupsLength; | |||
var emailsLength; | |||
var circlesLength; | |||
var roomsLength; | |||
var i, j; | |||
//Filter out the current user | |||
usersLength = users.length; | |||
for (i = 0; i < usersLength; i++) { | |||
if (users[i].value.shareWith === OC.currentUser) { | |||
users.splice(i, 1); | |||
break; | |||
} | |||
} | |||
// Filter out the owner of the share | |||
if (model.hasReshare()) { | |||
usersLength = users.length; | |||
for (i = 0 ; i < usersLength; i++) { | |||
if (users[i].value.shareWith === model.getReshareOwner()) { | |||
users.splice(i, 1); | |||
break; | |||
} | |||
} | |||
} | |||
var shares = model.get('shares'); | |||
var sharesLength = shares.length; | |||
// Now filter out all sharees that are already shared with | |||
for (i = 0; i < sharesLength; i++) { | |||
var share = shares[i]; | |||
if (share.share_type === OC.Share.SHARE_TYPE_USER) { | |||
usersLength = users.length; | |||
for (j = 0; j < usersLength; j++) { | |||
if (users[j].value.shareWith === share.share_with) { | |||
users.splice(j, 1); | |||
break; | |||
} | |||
} | |||
} else if (share.share_type === OC.Share.SHARE_TYPE_GROUP) { | |||
groupsLength = groups.length; | |||
for (j = 0; j < groupsLength; j++) { | |||
if (groups[j].value.shareWith === share.share_with) { | |||
groups.splice(j, 1); | |||
break; | |||
} | |||
} | |||
} else if (share.share_type === OC.Share.SHARE_TYPE_REMOTE) { | |||
remotesLength = remotes.length; | |||
for (j = 0; j < remotesLength; j++) { | |||
if (remotes[j].value.shareWith === share.share_with) { | |||
remotes.splice(j, 1); | |||
break; | |||
} | |||
} | |||
} else if (share.share_type === OC.Share.SHARE_TYPE_REMOTE_GROUP) { | |||
remoteGroupsLength = remote_groups.length; | |||
for (j = 0; j < remoteGroupsLength; j++) { | |||
if (remote_groups[j].value.shareWith === share.share_with) { | |||
remote_groups.splice(j, 1); | |||
break; | |||
} | |||
} | |||
} else if (share.share_type === OC.Share.SHARE_TYPE_EMAIL) { | |||
emailsLength = emails.length; | |||
for (j = 0; j < emailsLength; j++) { | |||
if (emails[j].value.shareWith === share.share_with) { | |||
emails.splice(j, 1); | |||
break; | |||
} | |||
} | |||
} else if (share.share_type === OC.Share.SHARE_TYPE_CIRCLE) { | |||
circlesLength = circles.length; | |||
for (j = 0; j < circlesLength; j++) { | |||
if (circles[j].value.shareWith === share.share_with) { | |||
circles.splice(j, 1); | |||
break; | |||
} | |||
} | |||
} else if (share.share_type === OC.Share.SHARE_TYPE_ROOM) { | |||
roomsLength = rooms.length; | |||
for (j = 0; j < roomsLength; j++) { | |||
if (rooms[j].value.shareWith === share.share_with) { | |||
rooms.splice(j, 1); | |||
break; | |||
} | |||
} | |||
} | |||
} | |||
}; | |||
filter( | |||
result.ocs.data.exact.users, | |||
result.ocs.data.exact.groups, | |||
result.ocs.data.exact.remotes, | |||
result.ocs.data.exact.remote_groups, | |||
result.ocs.data.exact.emails, | |||
result.ocs.data.exact.circles, | |||
result.ocs.data.exact.rooms | |||
); | |||
var exactUsers = result.ocs.data.exact.users; | |||
var exactGroups = result.ocs.data.exact.groups; | |||
var exactRemotes = result.ocs.data.exact.remotes || []; | |||
var exactRemoteGroups = result.ocs.data.exact.remote_groups || []; | |||
var exactEmails = []; | |||
if (typeof(result.ocs.data.emails) !== 'undefined') { | |||
exactEmails = result.ocs.data.exact.emails; | |||
} | |||
var exactCircles = []; | |||
if (typeof(result.ocs.data.circles) !== 'undefined') { | |||
exactCircles = result.ocs.data.exact.circles; | |||
} | |||
var exactRooms = []; | |||
if (typeof(result.ocs.data.rooms) !== 'undefined') { | |||
exactRooms = result.ocs.data.exact.rooms; | |||
} | |||
var exactMatches = exactUsers.concat(exactGroups).concat(exactRemotes).concat(exactRemoteGroups).concat(exactEmails).concat(exactCircles).concat(exactRooms); | |||
filter( | |||
result.ocs.data.users, | |||
result.ocs.data.groups, | |||
result.ocs.data.remotes, | |||
result.ocs.data.remote_groups, | |||
result.ocs.data.emails, | |||
result.ocs.data.circles, | |||
result.ocs.data.rooms | |||
); | |||
var users = result.ocs.data.users; | |||
var groups = result.ocs.data.groups; | |||
var remotes = result.ocs.data.remotes || []; | |||
var remoteGroups = result.ocs.data.remote_groups || []; | |||
var lookup = result.ocs.data.lookup || []; | |||
var emails = []; | |||
if (typeof(result.ocs.data.emails) !== 'undefined') { | |||
emails = result.ocs.data.emails; | |||
} | |||
var circles = []; | |||
if (typeof(result.ocs.data.circles) !== 'undefined') { | |||
circles = result.ocs.data.circles; | |||
} | |||
var rooms = []; | |||
if (typeof(result.ocs.data.rooms) !== 'undefined') { | |||
rooms = result.ocs.data.rooms; | |||
} | |||
var suggestions = exactMatches.concat(users).concat(groups).concat(remotes).concat(remoteGroups).concat(emails).concat(circles).concat(rooms).concat(lookup); | |||
function dynamicSort(property) { | |||
return function (a,b) { | |||
var aProperty = ''; | |||
var bProperty = ''; | |||
if (typeof a[property] !== 'undefined') { | |||
aProperty = a[property]; | |||
} | |||
if (typeof b[property] !== 'undefined') { | |||
bProperty = b[property]; | |||
} | |||
return (aProperty < bProperty) ? -1 : (aProperty > bProperty) ? 1 : 0; | |||
} | |||
} | |||
/** | |||
* Sort share entries by uuid to properly group them | |||
*/ | |||
var grouped = suggestions.sort(dynamicSort('uuid')); | |||
var previousUuid = null; | |||
var groupedLength = grouped.length; | |||
var result = []; | |||
/** | |||
* build the result array that only contains all contact entries from | |||
* merged contacts, if the search term matches its contact name | |||
*/ | |||
for (var i = 0; i < groupedLength; i++) { | |||
if (typeof grouped[i].uuid !== 'undefined' && grouped[i].uuid === previousUuid) { | |||
grouped[i].merged = true; | |||
} | |||
if (typeof grouped[i].merged === 'undefined') { | |||
result.push(grouped[i]); | |||
} | |||
previousUuid = grouped[i].uuid; | |||
} | |||
var moreResultsAvailable = | |||
( | |||
oc_config['sharing.maxAutocompleteResults'] > 0 | |||
&& Math.min(perPage, oc_config['sharing.maxAutocompleteResults']) | |||
<= Math.max( | |||
users.length + exactUsers.length, | |||
groups.length + exactGroups.length, | |||
remoteGroups.length + exactRemoteGroups.length, | |||
remotes.length + exactRemotes.length, | |||
emails.length + exactEmails.length, | |||
circles.length + exactCircles.length, | |||
rooms.length + exactRooms.length, | |||
lookup.length | |||
) | |||
); | |||
deferred.resolve(result, exactMatches, moreResultsAvailable); | |||
} else { | |||
deferred.reject(result.ocs.meta.message); | |||
} | |||
} | |||
).fail(function() { | |||
deferred.reject(); | |||
}); | |||
this._lastRecommendations = { | |||
model: model, | |||
promise: deferred.promise() | |||
}; | |||
return this._lastRecommendations.promise; | |||
}, | |||
recommendationHandler: function (response) { | |||
var view = this; | |||
var $shareWithField = $('.shareWithField'); | |||
this._getRecommendations( | |||
view.model | |||
).done(function(suggestions, exactMatches) { | |||
view._pendingOperationsCount--; | |||
if (view._pendingOperationsCount === 0) { | |||
$loading.addClass('hidden'); | |||
$loading.removeClass('inlineblock'); | |||
$confirm.removeClass('hidden'); | |||
} | |||
if (suggestions.length > 0) { | |||
$shareWithField | |||
.autocomplete("option", "autoFocus", true); | |||
response(suggestions); | |||
} else { | |||
console.info('no sharing recommendations found'); | |||
response(); | |||
} | |||
}).fail(function(message) { | |||
view._pendingOperationsCount--; | |||
if (view._pendingOperationsCount === 0) { | |||
$loading.addClass('hidden'); | |||
$loading.removeClass('inlineblock'); | |||
$confirm.removeClass('hidden'); | |||
} | |||
console.error('could not load recommendations', message) | |||
}); | |||
}, | |||
autocompleteHandler: function (search, response) { | |||
// If nothing is entered we show recommendations instead of search | |||
// results | |||
if (search.term.length === 0) { | |||
this.recommendationHandler(response); | |||
return; | |||
} | |||
var $shareWithField = $('.shareWithField'), | |||
view = this, | |||
$loading = this.$el.find('.shareWithLoading'), | |||
@@ -766,7 +1061,7 @@ | |||
}; | |||
$shareField.autocomplete({ | |||
minLength: 1, | |||
minLength: 0, | |||
delay: 750, | |||
focus: function(event) { | |||
event.preventDefault(); |