Skip to content
Draft
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
14 changes: 13 additions & 1 deletion appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<name>Recommendations</name>
<summary>Shows recommended files</summary>
<description>Shows recommended files for quick access of files and folders with recent activity</description>
<version>5.0.0-dev.0</version>
<version>5.0.0-dev.1</version>
<licence>agpl</licence>
<author>Christoph Wurst</author>
<author>Jan-Christoph Borchardt</author>
Expand All @@ -19,7 +19,19 @@
<dependencies>
<nextcloud min-version="32" max-version="32" />
</dependencies>
<types>
<filesystem/>
<dav/>
</types>
<commands>
<command>OCA\Recommendations\Command\GetRecommendations</command>
</commands>
<sabre>
<collections>
<collection>OCA\Recommendations\Sabre\RootCollection</collection>
</collections>
<plugins>
<plugin>OCA\Recommendations\Sabre\PropFindPlugin</plugin>
</plugins>
</sabre>
</info>
2 changes: 2 additions & 0 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

namespace OCA\Recommendations\AppInfo;

use OCA\DAV\Connector\Sabre\Principal;
use OCA\Files\Event\LoadAdditionalScriptsEvent;
use OCA\Recommendations\Capabilities;
use OCA\Recommendations\Dashboard\RecommendationWidget;
Expand All @@ -29,6 +30,7 @@
$context->registerEventListener(LoadAdditionalScriptsEvent::class, FilesLoadAdditionalScriptsListener::class);
$context->registerDashboardWidget(RecommendationWidget::class);
$context->registerCapability(Capabilities::class);
$context->registerServiceAlias('principalBackend', Principal::class);

Check failure on line 33 in lib/AppInfo/Application.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-master

UndefinedClass

lib/AppInfo/Application.php:33:54: UndefinedClass: Class, interface or enum named OCA\DAV\Connector\Sabre\Principal does not exist (see https://psalm.dev/019)
}

public function boot(IBootContext $context): void {
Expand Down
4 changes: 2 additions & 2 deletions lib/Command/GetRecommendations.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,12 @@ public function execute(InputInterface $input, OutputInterface $output) {
);

if (is_null($user)) {
$output->writeln("user does not exist");
$output->writeln('user does not exist');
return 1;
}

if ($input->getArgument('max')) {
$recommendations = $this->recommendationService->getRecommendations($user, (int) $input->getArgument('max'));
$recommendations = $this->recommendationService->getRecommendations($user, (int)$input->getArgument('max'));
} else {
$recommendations = $this->recommendationService->getRecommendations($user);
}
Expand Down
4 changes: 2 additions & 2 deletions lib/Controller/RecommendationController.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public function __construct(IRequest $request,
public function index(): DataResponse {
$user = $this->userSession->getUser();
if (is_null($user)) {
throw new Exception("Not logged in");
throw new Exception('Not logged in');
}
$response = [];
$response['enabled'] = $this->config->getUserValue($user->getUID(), Application::APP_ID, 'enabled', 'true') === 'true';
Expand All @@ -73,7 +73,7 @@ public function index(): DataResponse {
public function always(): DataResponse {
$user = $this->userSession->getUser();
if (is_null($user)) {
throw new Exception("Not logged in");
throw new Exception('Not logged in');
}
$response = [
'enabled' => $this->config->getUserValue($user->getUID(), Application::APP_ID, 'enabled', 'true') === 'true',
Expand Down
4 changes: 2 additions & 2 deletions lib/Controller/SettingsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public function __construct($appName,
public function getSettings(): JSONResponse {
$user = $this->userSession->getUser();
if (!$user instanceof IUser) {
throw new Exception("Not logged in");
throw new Exception('Not logged in');
}
return new JSONResponse([
'enabled' => $this->config->getUserValue($user->getUID(), Application::APP_ID, 'enabled', 'true') === 'true',
Expand All @@ -55,7 +55,7 @@ public function getSettings(): JSONResponse {
public function setSetting(string $key, string $value): JSONResponse {
$user = $this->userSession->getUser();
if (!$user instanceof IUser) {
throw new Exception("Not logged in");
throw new Exception('Not logged in');
}
$availableSettings = ['enabled'];
if (!in_array($key, $availableSettings)) {
Expand Down
2 changes: 1 addition & 1 deletion lib/Dashboard/RecommendationWidget.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public function __construct(
IURLGenerator $urlGenerator,
IMimeTypeDetector $mimeTypeDetector,
RecommendationService $recommendationService,
IUserManager $userManager
IUserManager $userManager,
) {
$this->userSession = $userSession;
$this->l10n = $l10n;
Expand Down
1 change: 1 addition & 0 deletions lib/ResponseDefinitions.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
* mimeType: string,
* hasPreview: bool,
* reason: string,
* reasonLabel: string,
* }
*
* @psalm-suppress UnusedClass
Expand Down
14 changes: 14 additions & 0 deletions lib/Sabre/IRecommendationNode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Recommendations\Sabre;

interface IRecommendationNode {
public function getRecommendationReason(): string;
public function getRecommendationReasonLabel(): string;
}
51 changes: 51 additions & 0 deletions lib/Sabre/PropFindPlugin.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Recommendations\Sabre;

use OCP\Files\Folder;
use Sabre\DAV\INode;
use Sabre\DAV\PropFind;
use Sabre\DAV\Server;
use Sabre\DAV\ServerPlugin;

class PropFindPlugin extends ServerPlugin {

Check failure on line 17 in lib/Sabre/PropFindPlugin.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-master

UndefinedClass

lib/Sabre/PropFindPlugin.php:17:30: UndefinedClass: Class, interface or enum named Sabre\DAV\ServerPlugin does not exist (see https://psalm.dev/019)
private ?Folder $userFolder = null;

public const RECOMMENDATION_REASON = '{http://nextcloud.org/ns}recommendation-reason';
public const RECOMMENDATION_REASON_LABEL = '{http://nextcloud.org/ns}recommendation-reason-label';
public const RECOMMENDATION_ORIGINAL_LOCATION = '{http://nextcloud.org/ns}recommendation-original-location';

public function getPluginName(): string {
return 'recommendationsPropFindPlugin';
}

public function initialize(Server $server): void {
$server->on('propFind', $this->propFind(...));
}

public function propFind(PropFind $propFind, INode $node): void {
if ($node instanceof RecommendationDirectory || $node instanceof RecommendationFile) {
$propFind->handle(
self::RECOMMENDATION_REASON,
/** @psalm-suppress PossiblyNullReference Null already checked above */
fn () => $node->getRecommendationReason(),
);
$propFind->handle(
self::RECOMMENDATION_REASON_LABEL,
/** @psalm-suppress PossiblyNullReference Null already checked above */
fn () => $node->getRecommendationReasonLabel(),
);
$propFind->handle(
self::RECOMMENDATION_ORIGINAL_LOCATION,
/** @psalm-suppress PossiblyNullReference Null already checked above */
fn () => $node->getPath(),
);
}
}
}
37 changes: 37 additions & 0 deletions lib/Sabre/RecommendationDirectory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Recommendations\Sabre;

use OC\Files\View;
use OCA\DAV\Connector\Sabre\CachingTree;
use OCA\DAV\Connector\Sabre\Directory;
use OCP\Files\FileInfo;
use OCP\Share\IManager;

class RecommendationDirectory extends Directory implements IRecommendationNode {

Check failure on line 17 in lib/Sabre/RecommendationDirectory.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-master

UndefinedClass

lib/Sabre/RecommendationDirectory.php:17:39: UndefinedClass: Class, interface or enum named OCA\DAV\Connector\Sabre\Directory does not exist (see https://psalm.dev/019)
public function __construct(
private string $recommendationReason,
private string $recommendationReasonLabel,
View $view,
FileInfo $info,
?CachingTree $tree = null,
?IManager $shareManager = null,
) {
parent::__construct($view, $info, $tree, $shareManager);
}


public function getRecommendationReason(): string {
return $this->recommendationReason;
}

public function getRecommendationReasonLabel(): string {
return $this->recommendationReasonLabel;
}
}
38 changes: 38 additions & 0 deletions lib/Sabre/RecommendationFile.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Recommendations\Sabre;

use OC\Files\View;
use OCA\DAV\Connector\Sabre\File;
use OCP\Files\FileInfo;
use OCP\IL10N;
use OCP\IRequest;
use OCP\Share\IManager;

class RecommendationFile extends File implements IRecommendationNode {

Check failure on line 18 in lib/Sabre/RecommendationFile.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-master

UndefinedClass

lib/Sabre/RecommendationFile.php:18:34: UndefinedClass: Class, interface or enum named OCA\DAV\Connector\Sabre\File does not exist (see https://psalm.dev/019)
public function __construct(
private string $recommendationReason,
private string $recommendationReasonLabel,
View $view,
FileInfo $info,
?IManager $shareManager = null,
?IRequest $request = null,
?IL10N $l10n = null,
) {
parent::__construct($view, $info, $shareManager, $request, $l10n);
}

public function getRecommendationReason(): string {
return $this->recommendationReason;
}

public function getRecommendationReasonLabel(): string {
return $this->recommendationReasonLabel;
}
}
132 changes: 132 additions & 0 deletions lib/Sabre/RecommendationsHome.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Recommendations\Sabre;

use OC\Files\Filesystem;
use OC\Files\View;
use OCA\Recommendations\AppInfo\Application;
use OCA\Recommendations\Service\IRecommendation;
use OCA\Recommendations\Service\RecommendationService;
use OCP\Files\FileInfo;
use OCP\IConfig;
use OCP\IL10N;
use OCP\IRequest;
use OCP\IUser;
use OCP\Share\IManager;
use Sabre\DAV\Exception\Forbidden;
use Sabre\DAV\Exception\NotFound;
use Sabre\DAV\ICollection;

class RecommendationsHome implements ICollection {

Check failure on line 26 in lib/Sabre/RecommendationsHome.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-master

UndefinedClass

lib/Sabre/RecommendationsHome.php:26:38: UndefinedClass: Class, interface or enum named Sabre\DAV\ICollection does not exist (see https://psalm.dev/019)

/** @var ?list<IRecommendation> */
private ?array $cachedRecommendations = null;

private View $fileView;

public function __construct(
private array $principalInfo,
private IUser $user,
private IConfig $config,
private RecommendationService $recommendationService,
private IManager $shareManager,
private IRequest $request,
private IL10N $l10n,
) {
$this->fileView = Filesystem::getView();
}

public function delete() {
throw new Forbidden();
}

public function getName(): string {
[, $name] = \Sabre\Uri\split($this->principalInfo['uri']);
return $name;
}

public function setName($name) {
throw new Forbidden();
}

public function createFile($name, $data = null) {
throw new Forbidden();
}

public function createDirectory($name) {
throw new Forbidden();
}

public function getChild($name) {
if (!$this->isEnabled()) {
throw new NotFound('Recommendations are disabled');
}

$recommendations = $this->getChildren();
foreach ($recommendations as $child) {
if ($child->getName() === $name) {
return $child;
}
}

throw new NotFound("Child '$name' not found in recommendations");
}

public function getChildren(): array {
if (!$this->isEnabled()) {
return [];
}

if ($this->cachedRecommendations === null) {
$this->cachedRecommendations = $this->recommendationService->getRecommendations($this->user);
}

return array_map(
function (IRecommendation $recommendation) {
$fileInfo = $recommendation->getNode()->getFileInfo();
if ($recommendation->getNode()->getType() === FileInfo::TYPE_FOLDER) {
return new RecommendationDirectory(
$recommendation->getReason(),
$recommendation->getReasonLabel(),
$this->fileView,
$fileInfo,
null,
$this->shareManager,
);
}
return new RecommendationFile(
$recommendation->getReason(),
$recommendation->getReasonLabel(),
$this->fileView,
$fileInfo,
$this->shareManager,
$this->request,
$this->l10n
);
},
$this->cachedRecommendations
);
}

public function childExists($name): bool {
if (!$this->isEnabled()) {
return false;
}
// TODO: map the recommendations to a Sabre node type
return true;
}

public function getLastModified(): int {
return 0;
}

private function isEnabled(): bool {
return $this->config->getUserValue($this->user->getUID(), Application::APP_ID, 'enabled', 'true') === 'true';
}
}
Loading
Loading