Show sharing recommendations

Signed-off-by: Christoph Wurst <christoph@winzerhof-wurst.at>
This commit is contained in:
Christoph Wurst 2019-02-13 17:16:01 +01:00 committed by Roeland Jago Douma
parent 5df6400e28
commit f3023aaa85
No known key found for this signature in database
GPG Key ID: F941078878347C0C
25 changed files with 1102 additions and 225 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -94,6 +94,11 @@ return [
'url' => '/api/v1/sharees',
'verb' => 'GET',
],
[
'name' => 'ShareesAPI#findRecommended',
'url' => '/api/v1/sharees_recommended',
'verb' => 'GET',
],
/*
* Remote Shares
*/

View File

@ -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
*

View File

@ -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,

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -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();

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long