feat: Implement reference picker for document sections

Signed-off-by: Julius Härtl <jus@bitgrid.net>
This commit is contained in:
Julius Härtl 2023-05-11 16:14:32 +02:00
parent 82e06d26f7
commit a903a1db75
No known key found for this signature in database
GPG key ID: 4C614C6ED2CDE6DF
18 changed files with 782 additions and 5 deletions

View file

@ -34,6 +34,7 @@ return [
['name' => 'document#publicPage', 'url' => '/public', 'verb' => 'GET'],
['name' => 'document#editOnline', 'url' => 'editonline', 'verb' => 'GET'],
['name' => 'document#editOnlineTarget', 'url' => 'editonline/{fileId}/{target}', 'verb' => 'GET'],
// external api access
['name' => 'document#extAppGetData', 'url' => '/ajax/extapp/data/{fileId}', 'verb' => 'POST'],
@ -87,5 +88,8 @@ return [
['name' => 'Federation#index', 'url' => '/api/v1/federation', 'verb' => 'GET'],
['name' => 'Federation#remoteWopiToken', 'url' => '/api/v1/federation', 'verb' => 'POST'],
['name' => 'Federation#initiatorUser', 'url' => '/api/v1/federation/user', 'verb' => 'POST'],
['name' => 'Target#getTargets', 'url' => '/api/v1/targets', 'verb' => 'GET'],
['name' => 'Target#getPreview', 'url' => '/api/v1/targets/preview', 'verb' => 'GET'],
],
];

View file

@ -8,6 +8,9 @@
"php": "8.0"
}
},
"require": {
"ext-json": "*"
},
"require-dev": {
"roave/security-advisories": "dev-master",
"jakub-onderka/php-parallel-lint": "^1.0.0",

View file

@ -23,6 +23,7 @@ return array(
'OCA\\Richdocuments\\Controller\\FederationController' => $baseDir . '/../lib/Controller/FederationController.php',
'OCA\\Richdocuments\\Controller\\OCSController' => $baseDir . '/../lib/Controller/OCSController.php',
'OCA\\Richdocuments\\Controller\\SettingsController' => $baseDir . '/../lib/Controller/SettingsController.php',
'OCA\\Richdocuments\\Controller\\TargetController' => $baseDir . '/../lib/Controller/TargetController.php',
'OCA\\Richdocuments\\Controller\\TemplatesController' => $baseDir . '/../lib/Controller/TemplatesController.php',
'OCA\\Richdocuments\\Controller\\WopiController' => $baseDir . '/../lib/Controller/WopiController.php',
'OCA\\Richdocuments\\Db\\Asset' => $baseDir . '/../lib/Db/Asset.php',
@ -40,6 +41,7 @@ return array(
'OCA\\Richdocuments\\Listener\\CSPListener' => $baseDir . '/../lib/Listener/CSPListener.php',
'OCA\\Richdocuments\\Listener\\FileCreatedFromTemplateListener' => $baseDir . '/../lib/Listener/FileCreatedFromTemplateListener.php',
'OCA\\Richdocuments\\Listener\\LoadViewerListener' => $baseDir . '/../lib/Listener/LoadViewerListener.php',
'OCA\\Richdocuments\\Listener\\ReferenceListener' => $baseDir . '/../lib/Listener/ReferenceListener.php',
'OCA\\Richdocuments\\Listener\\ShareLinkListener' => $baseDir . '/../lib/Listener/ShareLinkListener.php',
'OCA\\Richdocuments\\Middleware\\WOPIMiddleware' => $baseDir . '/../lib/Middleware/WOPIMiddleware.php',
'OCA\\Richdocuments\\Migration\\Version2060Date20200302131958' => $baseDir . '/../lib/Migration/Version2060Date20200302131958.php',
@ -55,11 +57,14 @@ return array(
'OCA\\Richdocuments\\Preview\\Office' => $baseDir . '/../lib/Preview/Office.php',
'OCA\\Richdocuments\\Preview\\OpenDocument' => $baseDir . '/../lib/Preview/OpenDocument.php',
'OCA\\Richdocuments\\Preview\\Pdf' => $baseDir . '/../lib/Preview/Pdf.php',
'OCA\\Richdocuments\\Reference\\OfficeTargetReferenceProvider' => $baseDir . '/../lib/Reference/OfficeTargetReferenceProvider.php',
'OCA\\Richdocuments\\Service\\CapabilitiesService' => $baseDir . '/../lib/Service/CapabilitiesService.php',
'OCA\\Richdocuments\\Service\\DemoService' => $baseDir . '/../lib/Service/DemoService.php',
'OCA\\Richdocuments\\Service\\FederationService' => $baseDir . '/../lib/Service/FederationService.php',
'OCA\\Richdocuments\\Service\\FileTargetService' => $baseDir . '/../lib/Service/FileTargetService.php',
'OCA\\Richdocuments\\Service\\FontService' => $baseDir . '/../lib/Service/FontService.php',
'OCA\\Richdocuments\\Service\\InitialStateService' => $baseDir . '/../lib/Service/InitialStateService.php',
'OCA\\Richdocuments\\Service\\RemoteService' => $baseDir . '/../lib/Service/RemoteService.php',
'OCA\\Richdocuments\\Service\\UserScopeService' => $baseDir . '/../lib/Service/UserScopeService.php',
'OCA\\Richdocuments\\Settings\\Admin' => $baseDir . '/../lib/Settings/Admin.php',
'OCA\\Richdocuments\\Settings\\Personal' => $baseDir . '/../lib/Settings/Personal.php',

View file

@ -38,6 +38,7 @@ class ComposerStaticInitRichdocuments
'OCA\\Richdocuments\\Controller\\FederationController' => __DIR__ . '/..' . '/../lib/Controller/FederationController.php',
'OCA\\Richdocuments\\Controller\\OCSController' => __DIR__ . '/..' . '/../lib/Controller/OCSController.php',
'OCA\\Richdocuments\\Controller\\SettingsController' => __DIR__ . '/..' . '/../lib/Controller/SettingsController.php',
'OCA\\Richdocuments\\Controller\\TargetController' => __DIR__ . '/..' . '/../lib/Controller/TargetController.php',
'OCA\\Richdocuments\\Controller\\TemplatesController' => __DIR__ . '/..' . '/../lib/Controller/TemplatesController.php',
'OCA\\Richdocuments\\Controller\\WopiController' => __DIR__ . '/..' . '/../lib/Controller/WopiController.php',
'OCA\\Richdocuments\\Db\\Asset' => __DIR__ . '/..' . '/../lib/Db/Asset.php',
@ -55,6 +56,7 @@ class ComposerStaticInitRichdocuments
'OCA\\Richdocuments\\Listener\\CSPListener' => __DIR__ . '/..' . '/../lib/Listener/CSPListener.php',
'OCA\\Richdocuments\\Listener\\FileCreatedFromTemplateListener' => __DIR__ . '/..' . '/../lib/Listener/FileCreatedFromTemplateListener.php',
'OCA\\Richdocuments\\Listener\\LoadViewerListener' => __DIR__ . '/..' . '/../lib/Listener/LoadViewerListener.php',
'OCA\\Richdocuments\\Listener\\ReferenceListener' => __DIR__ . '/..' . '/../lib/Listener/ReferenceListener.php',
'OCA\\Richdocuments\\Listener\\ShareLinkListener' => __DIR__ . '/..' . '/../lib/Listener/ShareLinkListener.php',
'OCA\\Richdocuments\\Middleware\\WOPIMiddleware' => __DIR__ . '/..' . '/../lib/Middleware/WOPIMiddleware.php',
'OCA\\Richdocuments\\Migration\\Version2060Date20200302131958' => __DIR__ . '/..' . '/../lib/Migration/Version2060Date20200302131958.php',
@ -70,11 +72,14 @@ class ComposerStaticInitRichdocuments
'OCA\\Richdocuments\\Preview\\Office' => __DIR__ . '/..' . '/../lib/Preview/Office.php',
'OCA\\Richdocuments\\Preview\\OpenDocument' => __DIR__ . '/..' . '/../lib/Preview/OpenDocument.php',
'OCA\\Richdocuments\\Preview\\Pdf' => __DIR__ . '/..' . '/../lib/Preview/Pdf.php',
'OCA\\Richdocuments\\Reference\\OfficeTargetReferenceProvider' => __DIR__ . '/..' . '/../lib/Reference/OfficeTargetReferenceProvider.php',
'OCA\\Richdocuments\\Service\\CapabilitiesService' => __DIR__ . '/..' . '/../lib/Service/CapabilitiesService.php',
'OCA\\Richdocuments\\Service\\DemoService' => __DIR__ . '/..' . '/../lib/Service/DemoService.php',
'OCA\\Richdocuments\\Service\\FederationService' => __DIR__ . '/..' . '/../lib/Service/FederationService.php',
'OCA\\Richdocuments\\Service\\FileTargetService' => __DIR__ . '/..' . '/../lib/Service/FileTargetService.php',
'OCA\\Richdocuments\\Service\\FontService' => __DIR__ . '/..' . '/../lib/Service/FontService.php',
'OCA\\Richdocuments\\Service\\InitialStateService' => __DIR__ . '/..' . '/../lib/Service/InitialStateService.php',
'OCA\\Richdocuments\\Service\\RemoteService' => __DIR__ . '/..' . '/../lib/Service/RemoteService.php',
'OCA\\Richdocuments\\Service\\UserScopeService' => __DIR__ . '/..' . '/../lib/Service/UserScopeService.php',
'OCA\\Richdocuments\\Settings\\Admin' => __DIR__ . '/..' . '/../lib/Settings/Admin.php',
'OCA\\Richdocuments\\Settings\\Personal' => __DIR__ . '/..' . '/../lib/Settings/Personal.php',

View file

@ -137,6 +137,10 @@ class AppConfig {
return $this->config->getAppValue(Application::APPNAME, self::WOPI_URL, '');
}
public function getDisableCertificateValidation(): bool {
return $this->config->getAppValue(Application::APPNAME, 'disable_certificate_verification', 'no') === 'yes';
}
public function getUseGroups(): ?array {
$groups = $this->config->getAppValue(Application::APPNAME, 'use_groups', '');
if ($groups === '') {

View file

@ -31,6 +31,7 @@ use OCA\Richdocuments\Listener\BeforeFetchPreviewListener;
use OCA\Richdocuments\Listener\CSPListener;
use OCA\Richdocuments\Listener\FileCreatedFromTemplateListener;
use OCA\Richdocuments\Listener\LoadViewerListener;
use OCA\Richdocuments\Listener\ReferenceListener;
use OCA\Richdocuments\Listener\ShareLinkListener;
use OCA\Richdocuments\Middleware\WOPIMiddleware;
use OCA\Richdocuments\PermissionManager;
@ -39,6 +40,7 @@ use OCA\Richdocuments\Preview\MSWord;
use OCA\Richdocuments\Preview\OOXML;
use OCA\Richdocuments\Preview\OpenDocument;
use OCA\Richdocuments\Preview\Pdf;
use OCA\Richdocuments\Reference\OfficeTargetReferenceProvider;
use OCA\Richdocuments\Service\CapabilitiesService;
use OCA\Richdocuments\Template\CollaboraTemplateProvider;
use OCA\Richdocuments\WOPI\DiscoveryManager;
@ -47,6 +49,7 @@ use OCP\AppFramework\App;
use OCP\AppFramework\Bootstrap\IBootContext;
use OCP\AppFramework\Bootstrap\IBootstrap;
use OCP\AppFramework\Bootstrap\IRegistrationContext;
use OCP\Collaboration\Reference\RenderReferenceEvent;
use OCP\Files\Template\FileCreatedFromTemplateEvent;
use OCP\Files\Template\ITemplateManager;
use OCP\Files\Template\TemplateFileCreator;
@ -73,6 +76,8 @@ class Application extends App implements IBootstrap {
$context->registerEventListener(LoadViewer::class, LoadViewerListener::class);
$context->registerEventListener(ShareLinkAccessedEvent::class, ShareLinkListener::class);
$context->registerEventListener(BeforePreviewFetchedEvent::class, BeforeFetchPreviewListener::class);
$context->registerEventListener(RenderReferenceEvent::class, ReferenceListener::class);
$context->registerReferenceProvider(OfficeTargetReferenceProvider::class);
}
public function boot(IBootContext $context): void {

View file

@ -39,6 +39,8 @@ use OCP\Share\IManager;
class DocumentController extends Controller {
use DocumentTrait;
public const SESSION_FILE_TARGET = 'richdocuments_openfile_target';
/** @var ?string */
private $uid;
/** @var IConfig */
@ -140,12 +142,13 @@ class DocumentController extends Controller {
/**
* @NoAdminRequired
* @UseSession
*
* @param string $fileId
* @param string|null $path
* @return RedirectResponse|TemplateResponse
*/
public function index($fileId, $path = null) {
public function index($fileId, ?string $path = null) {
try {
$folder = $this->rootFolder->getUserFolder($this->uid);
@ -188,6 +191,14 @@ class DocumentController extends Controller {
'path' => $folder->getRelativePath($item->getPath()),
];
$targetData = $this->session->get(self::SESSION_FILE_TARGET);
if ($targetData) {
$this->session->remove(self::SESSION_FILE_TARGET);
if ($targetData['fileId'] === $item->getId()) {
$params['target'] = $targetData['target'];
}
}
$encryptionManager = \OC::$server->getEncryptionManager();
if ($encryptionManager->isEnabled()) {
// Update the current file to be accessible with system public shared key
@ -403,8 +414,9 @@ class DocumentController extends Controller {
/**
* @NoCSRFRequired
* @NoAdminRequired
* @UseSession
*/
public function editOnline(string $path = null, string $userId = null) {
public function editOnline(string $path = null, ?string $userId = null, ?string $target = null) {
if ($path === null) {
return $this->renderErrorPage('No path provided');
}
@ -428,6 +440,46 @@ class DocumentController extends Controller {
try {
$userFolder = $this->rootFolder->getUserFolder($userId);
$file = $userFolder->get($path);
if ($target !== null) {
$this->session->set(self::SESSION_FILE_TARGET, [
'fileId' => $file->getId(),
'target' => $target,
]);
}
$redirectUrl = $this->urlGenerator->getAbsoluteURL('/index.php/f/' . $file->getId());
return new RedirectResponse($redirectUrl);
} catch (NotFoundException $e) {
} catch (NotPermittedException $e) {
} catch (NoUserException $e) {
}
return $this->renderErrorPage('File not found', Http::STATUS_NOT_FOUND);
}
/**
* @NoCSRFRequired
* @NoAdminRequired
* @UseSession
*/
public function editOnlineTarget(int $fileId, ?string $target = null) {
if (!$this->uid) {
return $this->renderErrorPage('File not found', Http::STATUS_NOT_FOUND);
}
try {
$userFolder = $this->rootFolder->getUserFolder($this->uid);
$files = $userFolder->getById($fileId);
$file = array_shift($files);
if (!$file) {
return $this->renderErrorPage('File not found', Http::STATUS_NOT_FOUND);
}
if ($target !== null) {
$this->session->set(self::SESSION_FILE_TARGET, [
'fileId' => $file->getId(),
'target' => $target,
]);
}
$redirectUrl = $this->urlGenerator->getAbsoluteURL('/index.php/f/' . $file->getId());
return new RedirectResponse($redirectUrl);
} catch (NotFoundException $e) {

View file

@ -0,0 +1,75 @@
<?php
namespace OCA\Richdocuments\Controller;
use OC\User\NoUserException;
use OCA\Richdocuments\Service\FileTargetService;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataDisplayResponse;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\Http\Response;
use OCP\Files\File;
use OCP\Files\IRootFolder;
use OCP\Files\NotFoundException;
use OCP\Files\NotPermittedException;
use OCP\IRequest;
class TargetController extends \OCP\AppFramework\OCSController {
public function __construct(
string $appName,
IRequest $request,
private FileTargetService $fileTargetService,
private IRootFolder $rootFolder,
private ?string $userId,
) {
parent::__construct($appName, $request);
}
/**
* @NoAdminRequired
*/
public function getTargets(string $path): DataResponse {
try {
$file = $this->getFile($path);
return new DataResponse($this->fileTargetService->getFileTargets($file));
} catch (NotFoundException $e) {
}
return new DataResponse('File not found', Http::STATUS_NOT_FOUND);
}
/**
* @NoAdminRequired
* @NoCSRFRequired
*/
public function getPreview(string $path, string $target): Response {
try {
$file = $this->getFile($path);
return new DataDisplayResponse(
$this->fileTargetService->getTargetPreview($file, $target),
Http::STATUS_OK,
['Content-Type' => 'image/png']
);
} catch (NotFoundException $e) {
return new DataResponse('File not found', Http::STATUS_NOT_FOUND);
}
}
/**
* @throws NotFoundException
*/
private function getFile(string $path): File {
try {
$file = $this->rootFolder->getUserFolder($this->userId)->get($path);
if (!$file instanceof File) {
throw new NotFoundException();
}
return $file;
} catch (NotFoundException|NotPermittedException|NoUserException $e) {
throw new NotFoundException();
}
}
}

View file

@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2023 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Richdocuments\Listener;
use OCP\Collaboration\Reference\RenderReferenceEvent;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\Util;
/** @template-implements IEventListener<Event|RenderReferenceEvent> */
class ReferenceListener implements IEventListener {
public function handle(Event $event): void {
if (!$event instanceof RenderReferenceEvent) {
return;
}
Util::addScript('richdocuments', 'richdocuments-reference');
}
}

View file

@ -0,0 +1,148 @@
<?php
namespace OCA\Richdocuments\Reference;
use Exception;
use OCA\Richdocuments\AppInfo\Application;
use OCA\Richdocuments\Service\FileTargetService;
use OCP\Collaboration\Reference\ADiscoverableReferenceProvider;
use OCP\Collaboration\Reference\IReference;
use OCP\Collaboration\Reference\Reference;
use OCP\Files\IMimeTypeDetector;
use OCP\Files\IRootFolder;
use OCP\IL10N;
use OCP\IPreview;
use OCP\IURLGenerator;
use Psr\Log\LoggerInterface;
class OfficeTargetReferenceProvider extends ADiscoverableReferenceProvider {
public function __construct(
private FileTargetService $fileTargetService,
private IURLGenerator $urlGenerator,
private IL10N $l10n,
private IPreview $previewManager,
private IMimeTypeDetector $mimeTypeDetector,
private IRootFolder $rootFolder,
private LoggerInterface $logger,
private ?string $userId,
) {
}
/**
* @inheritDoc
*/
public function matchReference(string $referenceText): bool {
$start = $this->urlGenerator->getAbsoluteURL('/apps/' . Application::APPNAME);
$startIndex = $this->urlGenerator->getAbsoluteURL('/index.php/apps/' . Application::APPNAME);
$noIndexMatch = preg_match('/^' . preg_quote($start, '/') . '\/editonline\/([0-9]+)\/(.*)$/', $referenceText) === 1;
$indexMatch = preg_match('/^' . preg_quote($startIndex, '/') . '\/editonline\/([0-9]+)\/(.*)$/', $referenceText) === 1;
return $noIndexMatch || $indexMatch;
}
/**
* @inheritDoc
*/
public function resolveReference(string $referenceText): ?IReference {
$start = $this->urlGenerator->getAbsoluteURL('/apps/' . Application::APPNAME);
$startIndex = $this->urlGenerator->getAbsoluteURL('/index.php/apps/' . Application::APPNAME);
$matched = preg_match('/^' . preg_quote($start, '/') . '\/editonline\/([0-9]+)\/(.*)$/', $referenceText, $matches) === 1;
if (!$matched) {
$matched = preg_match('/^' . preg_quote($startIndex, '/') . '\/editonline\/([0-9]+)\/(.*)$/', $referenceText, $matches) === 1;
}
if (!$matched) {
return null;
}
$fileId = (int)$matches[1];
$target = urldecode($matches[2]);
try {
$userFolder = $this->rootFolder->getUserFolder($this->userId);
$files = $userFolder->getById($fileId);
$file = array_shift($files);
} catch (Exception $e) {
$this->logger->info('Failed to get file for office target reference: ' . $fileId, ['exception' => $e]);
return null;
}
if ($file === null) {
return null;
}
$label = null;
if ($this->previewManager->isMimeSupported($file->getMimeType())) {
$preview = $this->urlGenerator->linkToRouteAbsolute('core.Preview.getPreviewByFileId', ['x' => 1600, 'y' => 630, 'fileId' => $fileId]);
} else {
$fileTypeIconUrl = $this->mimeTypeDetector->mimeTypeIcon($file->getMimeType());
$preview = $fileTypeIconUrl;
}
$targets = $this->fileTargetService->getFileTargets($file);
foreach ($targets as $value) {
$entries = $value['entries'];
foreach ($entries as $entry) {
if ($entry['id'] === $target) {
$label = $entry['name'];
$preview = $entry['preview'] ?? $preview;
break 2;
}
}
}
$reference = new Reference($referenceText);
$reference->setTitle($label ?? $file->getName());
$reference->setDescription($label ? $file->getName(): null);
$reference->setUrl($referenceText);
$reference->setImageUrl($preview);
/*
FIXME: THis requires a change in the file provider to properly open the target link still
$reference->setRichObject('file', [
'id' => $file->getId(),
'name' => $file->getName() . ' · ' . $label,
'size' => $file->getSize(),
'path' => $userFolder->getRelativePath($file->getPath()),
'link' => $reference->getUrl(),
'mimetype' => $file->getMimetype(),
'mtime' => $file->getMTime(),
'preview-available' => $this->previewManager->isAvailable($file)
]);
*/
return $reference;
}
/**
* @inheritDoc
*/
public function getCachePrefix(string $referenceId): string {
return $referenceId;
}
/**
* @inheritDoc
*/
public function getCacheKey(string $referenceId): ?string {
return $this->userId ?? '';
}
public function getId(): string {
return 'office-target';
}
public function getTitle(): string {
return $this->l10n->t('Link to office document section');
}
public function getOrder(): int {
return 90;
}
public function getIconUrl(): string {
return $this->urlGenerator->getAbsoluteURL($this->urlGenerator->imagePath(Application::APPNAME, 'app-dark.svg'));
}
}

View file

@ -0,0 +1,92 @@
<?php
namespace OCA\Richdocuments\Service;
use OCP\Files\File;
use OCP\Files\IRootFolder;
use OCP\ICacheFactory;
use OCP\IL10N;
use Psr\Log\LoggerInterface;
class FileTargetService {
public function __construct(
private RemoteService $remoteService,
private ICacheFactory $cacheFactory,
private IRootFolder $rootFolder,
private LoggerInterface $logger,
private IL10N $l10n,
private ?string $userId,
) {
}
public function getFileTargets(File $file): array {
$cache = $this->cacheFactory->createDistributed('richdocuments-filetarget');
$cacheKey = $file->getId() . '_' . $file->getMTime();
if ($cached = $cache->get($cacheKey)) {
return $cached;
}
$result = $this->remoteService->fetchTargets($file);
$categories = [];
$targets = $result['Targets'];
$filePath = $this->rootFolder->getUserFolder($this->userId)->getRelativePath($file->getPath());
// Limit what we support to display
if (isset($targets['Headings'])) {
$categories['headings'] = [
'label' => $this->l10n->t('Headings'),
'entries' => $this->mapTargets($filePath, $targets['Headings'])
];
}
if (isset($targets['Sections'])) {
$categories['sections'] = [
'label' => $this->l10n->t('Sections'),
'entries' => $this->mapTargets($filePath, $targets['Sections'])
];
}
if (isset($targets['Images'])) {
$categories['images'] = [
'label' => $this->l10n->t('Images'),
'entries' => $this->mapTargets($filePath, $targets['Images'])
];
}
if (isset($targets['Sheets'])) {
$categories['sheets'] = [
'label' => $this->l10n->t('Sheets'),
'entries' => $this->mapTargets($filePath, $targets['Sheets'])
];
}
$cache->set($cacheKey, $categories);
return $categories;
}
public function getTargetPreview($file, $target) {
return $this->remoteService->fetchTargetThumbnail($file, $target);
}
private function mapTargets(string $filePath, array $targets): array {
$result = [];
foreach ($targets as $name => $identifier) {
$result[] = [
'id' => $identifier,
'name' => $name,
// Disable previews for now as they may cause endless requests against Collabora
// 'preview' => $this->urlGenerator->linkToOCSRouteAbsolute('richdocuments.Target.getPreview', [
// 'path' => $filePath,
// 'target' => $identifier,
// ]),
];
}
return $result;
}
}

View file

@ -0,0 +1,88 @@
<?php
namespace OCA\Richdocuments\Service;
use Exception;
use OCA\Richdocuments\AppConfig;
use OCP\Files\File;
use OCP\Files\NotFoundException;
use OCP\Http\Client\IClientService;
use Psr\Log\LoggerInterface;
class RemoteService {
public const REMOTE_TIMEOUT_DEFAULT = 25;
public function __construct(
private AppConfig $appConfig,
private IClientService $clientService,
private LoggerInterface $logger,
) {
}
public function fetchTargets($file): array {
$client = $this->clientService->newClient();
try {
$response = $client->put(
$this->appConfig->getCollaboraUrlInternal(). '/cool/extract-link-targets',
$this->getRequestOptionsForFile($file)
);
} catch (Exception $e) {
$this->logger->warning('Failed to fetch extract-link-targets', ['exception' => $e]);
return [];
}
$json = trim($response->getBody());
$json = str_replace(['", }', "\r\n", "\t"], ['" }', '\r\n', '\t'], $json);
try {
$result = json_decode($json, true, 512, JSON_THROW_ON_ERROR);
} catch (\JsonException $e) {
$this->logger->warning('Failed to parse extract-link-targets response', ['exception' => $e]);
return [];
}
return $result;
}
public function fetchTargetThumbnail(File $file, string $target): ?string {
$client = $this->clientService->newClient();
try {
$response = $client->put($this->appConfig->getCollaboraUrlInternal(). '/cool/get-thumbnail', $this->getRequestOptionsForFile($file, $target));
return (string)$response->getBody();
} catch (Exception $e) {
$this->logger->info('Failed to fetch target thumbnail', ['exception' => $e]);
}
return null;
}
private function getRequestOptionsForFile(File $file, ?string $target = null): array {
$useTempFile = $file->isEncrypted() || !$file->getStorage()->isLocal();
if ($useTempFile) {
$localFile = $file->getStorage()->getLocalFile($file->getInternalPath());
if (!is_string($localFile)) {
throw new NotFoundException('Could not get local file');
}
$stream = fopen($localFile, 'rb');
} else {
$stream = $file->fopen('rb');
}
$options = [
'timeout' => self::REMOTE_TIMEOUT_DEFAULT,
'multipart' => [
['name' => $file->getName(), 'contents' => $stream],
['name' => 'target', 'contents' => $target]
]
];
if ($this->appConfig->getDisableCertificateValidation()) {
$options['verify'] = false;
}
$options['headers'] = [
'User-Agent' => 'Nextcloud Server / richdocuments',
'Accept' => 'application/json',
];
return $options;
}
}

View file

@ -247,7 +247,7 @@ const documentsMain = {
$(document.body).addClass('claro')
$(document.body).prepend(documentsMain.UI.container)
const urlsrc = getWopiUrl({ fileId, title, readOnly: false, closeButton: !documentsMain.hideCloseButton, revisionHistory: !documentsMain.isPublic })
const urlsrc = getWopiUrl({ fileId, title, readOnly: false, closeButton: !documentsMain.hideCloseButton, revisionHistory: !documentsMain.isPublic, target: Config.get('target') })
// access_token - must be passed via a form post
const accessToken = encodeURIComponent(documentsMain.token)

View file

@ -32,7 +32,7 @@ const getSearchParam = (name) => {
return decodeURI(results[1]) || ''
}
const getWopiUrl = ({ fileId, title, readOnly, closeButton, revisionHistory }) => {
const getWopiUrl = ({ fileId, title, readOnly, closeButton, revisionHistory, target = undefined }) => {
// Only set the revision history parameter if the versions app is enabled
revisionHistory = revisionHistory && window?.oc_appswebroots?.files_versions
@ -53,6 +53,7 @@ const getWopiUrl = ({ fileId, title, readOnly, closeButton, revisionHistory }) =
+ (closeButton ? '&closebutton=1' : '')
+ (revisionHistory ? '&revisionhistory=1' : '')
+ (readOnly ? '&permission=readonly' : '')
+ (target ? '&target=' + encodeURIComponent(target) : '')
}
const getDocumentUrlFromTemplate = (templateId, fileName, fileDir, fillWithTemplate) => {

45
src/reference.js Normal file
View file

@ -0,0 +1,45 @@
/*
* @copyright Copyright (c) 2023 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import Vue from 'vue'
import { translate as t } from '@nextcloud/l10n'
import { registerCustomPickerElement, NcCustomPickerRenderResult } from '@nextcloud/vue/dist/Components/NcRichText.js'
import DocumentTargetPicker from './view/DocumentTargetPicker.vue'
Vue.mixin({
methods: {
t,
},
})
registerCustomPickerElement('office-target', (el, { providerId, accessible }) => {
const Element = Vue.extend(DocumentTargetPicker)
const vueElement = new Element({
propsData: {
providerId,
accessible,
},
}).$mount(el)
return new NcCustomPickerRenderResult(vueElement.$el, vueElement)
}, (el, renderResult) => {
renderResult.object.$destroy()
})

View file

@ -0,0 +1,204 @@
<!--
- @copyright Copyright (c) 2023 Julius Härtl <jus@bitgrid.net>
-
- @author Julius Härtl <jus@bitgrid.net>
-
- @license GNU AGPL version 3 or any later version
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as
- published by the Free Software Foundation, either version 3 of the
- License, or (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<template>
<div v-if="filePath === null" class="office-target-picker">
<div ref="picker" class="reference-file-picker" />
</div>
<div v-else class="office-target-picker">
<h2>{{ t('richdocuments', 'Link to office document section') }}</h2>
<NcLoadingIcon v-if="!sections || !fileId" :size="44" />
<div v-else>
<NcEmptyContent v-if="sections.length === 0" :title="t('richdocuments', 'Could not find any section in the document')">
<template #icon>
<TableOfContentsIcon />
</template>
</NcEmptyContent>
<template v-else>
<div v-for="section in sections" :key="section.label">
<h3>{{ section.label }}</h3>
<ul>
<NcListItem v-for="entry in section.entries"
:key="entry.id"
:title="entry.name"
:class="{ 'list-item__wrapper--active': entry.id === target }"
@click="setTarget(entry)">
<template v-if="entry.preview" #icon>
<img :src="entry.preview">
</template>
</NcListItem>
</ul>
</div>
</template>
<div v-if="sections.length !== 0" class="office-target-picker__buttons">
<NcButton type="primary" :disabled="!target" @click="submit()">
{{ t('richdocuments', 'Link to office document section') }}
</NcButton>
</div>
</div>
</div>
</template>
<script>
import { FilePickerType } from '@nextcloud/dialogs'
import { generateUrl, generateOcsUrl } from '@nextcloud/router'
import axios from '@nextcloud/axios'
import { NcButton, NcEmptyContent, NcListItem, NcLoadingIcon } from '@nextcloud/vue'
import TableOfContentsIcon from 'vue-material-design-icons/TableOfContents.vue'
export default {
name: 'DocumentTargetPicker',
components: {
NcButton,
NcEmptyContent,
NcListItem,
NcLoadingIcon,
TableOfContentsIcon,
},
props: {
providerId: {
type: String,
required: true,
},
accessible: {
type: Boolean,
default: false,
},
},
data() {
return {
fileId: null,
filePath: null,
target: null,
sections: null,
}
},
mounted() {
this.openFilePicker()
window.addEventListener('click', this.onWindowClick)
},
beforeDestroy() {
window.removeEventListener('click', this.onWindowClick)
},
methods: {
onWindowClick(e) {
if (e.target.tagName === 'A' && e.target.classList.contains('oc-dialog-close')) {
this.$emit('cancel')
}
},
async openFilePicker() {
const self = this
OC.dialogs.filepicker(
t('files', 'Select file or folder to link to'),
function(file) {
const client = OC.Files.getClient()
client.getFileInfo(file).then((_status, fileInfo) => {
self.fileId = fileInfo.id
})
self.filePath = file
self.fetchReferences()
},
false, // multiselect
OC.getCapabilities().richdocuments.mimetypes, // mime filter
false, // modal
FilePickerType.Choose, // type
'',
{
target: this.$refs.picker,
},
)
},
setTarget(entry) {
this.target = entry.id
},
submit() {
const fileLink = window.location.protocol + '//' + window.location.host
+ generateUrl('/apps/richdocuments/editonline/{fileId}/{target}', { fileId: this.fileId, target: this.target })
this.$emit('submit', fileLink)
},
async fetchReferences() {
const response = await axios.get(generateOcsUrl('/apps/richdocuments/api/v1/targets'), {
params: {
path: this.filePath,
},
})
this.sections = response.data.ocs.data
},
},
}
</script>
<style scoped lang="scss">
.reference-file-picker {
flex-grow: 1;
&:deep(.oc-dialog) {
transform: none !important;
box-shadow: none !important;
flex-grow: 1 !important;
position: static !important;
width: 100% !important;
height: auto !important;
padding: 0 !important;
max-width: initial;
.oc-dialog-close {
display: none;
}
.oc-dialog-buttonrow.onebutton.aside {
position: absolute;
padding: 12px 32px;
}
.oc-dialog-content {
max-width: 100% !important;
}
}
}
.office-target-picker {
margin: calc(var(--default-grid-baseline) * 4);
flex-grow: 1;
height: 80vh;
&__buttons {
position: sticky;
bottom: 12px;
display: flex;
align-items: flex-end;
flex-direction: column;
}
}
h3 {
font-weight: bold;
color: var(--color-primary-element);
font-size: var(--default-font-size);
line-height: 44px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
opacity: .7;
box-shadow: none !important;
flex-shrink: 0;
}
</style>

View file

@ -334,7 +334,7 @@ export default {
z-index: 1;
top: 0;
left: 0;
background-color: #fff;
background-color: var(--color-main-background);
&.debug {
opacity: .5;
}

View file

@ -10,6 +10,7 @@ webpackConfig.entry = {
document: path.join(__dirname, 'src', 'document.js'),
admin: path.join(__dirname, 'src', 'admin.js'),
personal: path.join(__dirname, 'src', 'personal.js'),
reference: path.join(__dirname, 'src', 'reference.js'),
}
webpackRules.RULE_JS.test = /\.m?js$/