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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ SPDX-License-Identifier: AGPL-3.0-or-later

## [Unreleased]

## [0.4.0] - 2025-08-04

- Fix: internal and external shares
- Fix: "Open in Cryptpad" not showing
- Fix: german translations.

## [0.3.7] - 2025-02-17

- Fix: Now working with Nextcloud versions 30 and 31
Expand Down
2 changes: 1 addition & 1 deletion appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ interface in the "General" tab.
CryptPad". You can find it in the "Integration" category.
3. Configure "Open in CryptPad" in the administration settings of Nextcloud.
]]></description>
<version>0.3.7</version>
<version>0.4.0</version>
<licence>agpl</licence>
<author mail="contact@cryptpad.org" homepage="https://cryptpad.org">XWiki CryptPad Team and contributors</author>
<namespace>OpenInCryptPad</namespace>
Expand Down
4,116 changes: 1,301 additions & 2,815 deletions composer.lock

Large diffs are not rendered by default.

11 changes: 10 additions & 1 deletion lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
// SPDX-License-Identifier: AGPL-3.0-or-later

namespace OCA\OpenInCryptPad\AppInfo;
use OCA\OpenInCryptPad\Listener\PublicShareBeforeTemplateRenderedListener;
use OCA\OpenInCryptPad\Listener\FilesLoadAdditionalScriptsListener;

use OCP\AppFramework\App;
use OCP\AppFramework\Bootstrap\IBootContext;
Expand All @@ -20,12 +22,19 @@ public function __construct() {
}

public function register(IRegistrationContext $context): void {
// "This event is triggered when the files app is rendered. It can be used to add additional scripts to the files app."
// See: https://docs.nextcloud.com/server/latest/developer_manual/basics/events.html#oca-files-event-loadadditionalscriptsevent
$context->registerEventListener(\OCA\Files\Event\LoadAdditionalScriptsEvent::class, FilesLoadAdditionalScriptsListener::class);

// "Emitted before the rendering step of the public share page happens. The event holds a flag that specifies if it is the authentication page of a public share."
// See: https://docs.nextcloud.com/server/latest/developer_manual/basics/events.html#oca-files-sharing-event-beforetemplaterenderedevent
$context->registerEventListener(\OCA\Files_Sharing\Event\BeforeTemplateRenderedEvent::class, PublicShareBeforeTemplateRenderedListener::class);
}

public function boot(IBootContext $context): void {
/**
* Always add main script
*/
Util::addInitScript(self::APP_ID, 'openincryptpad-main', 'files');
// Util::addInitScript(self::APP_ID, 'openincryptpad-main', 'files');

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The LoadAdditionalScriptsEvent does work in older NC versions (we support) right? Than we can remove boot completely.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing boot gives an internal server error and breaks NC itself. I think we can keep it empty. (and maybe at some point we will need to implement a 'global' script again)

}
}
8 changes: 5 additions & 3 deletions lib/Controller/EditorController.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,12 @@ public function __construct(IRequest $request,
* @NoAdminRequired
* @NoCSRFRequired
*/
public function page($id, $path, $mimeType): TemplateResponse {
public function page($id, $path, $mimeType, $isShared, $fileName): TemplateResponse {
$app = SettingsService::APP_FOR_MIME_TYPE[$mimeType];
$fileType = SettingsService::FILE_TYPE_FOR_MIME_TYPE[$mimeType];
$cryptPadUrl = $this->settingsService->getCryptPadUrl($app);
$apiUrl = $cryptPadUrl . '/cryptpad-api.js';
$infoScript = $this->getInfoScript($id, $path, $mimeType, $fileType, $app, $cryptPadUrl);
$infoScript = $this->getInfoScript($id, $path, $mimeType, $fileType, $app, $cryptPadUrl, $isShared, $fileName);

$response = new TemplateResponse(
'openincryptpad',
Expand All @@ -54,14 +54,16 @@ public function page($id, $path, $mimeType): TemplateResponse {
return $response;
}

public function getInfoScript($id, $path, $mimeType, $fileType, $app, $cryptPadUrl): string {
public function getInfoScript($id, $path, $mimeType, $fileType, $app, $cryptPadUrl, $isShared, $fileName): string {
return 'window.OpenInCryptPadInfo = ' . json_encode([
'fileId' => $id,
'filePath' => $path,
'mimeType' => $mimeType,
'fileType' => $fileType,
'app' => $app,
'cryptPadUrl' => $cryptPadUrl,
'isShared' => $isShared,
'fileName' => $fileName
]);
}

Expand Down
22 changes: 22 additions & 0 deletions lib/Listener/FilesLoadAdditionalScriptsListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

declare(strict_types=1);

namespace OCA\OpenInCryptPad\Listener;

use OCA\OpenInCryptPad\AppInfo\Application;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\Util;


/** @template-implements IEventListener<\OCA\Files_Sharing\Event\BeforeTemplateRenderedEvent> */
class FilesLoadAdditionalScriptsListener implements IEventListener {

public function __construct() {
}

public function handle(Event $event): void {
Util::addInitScript(Application::APP_ID, 'openincryptpad-main');
}
}
22 changes: 22 additions & 0 deletions lib/Listener/PublicShareBeforeTemplateRenderedListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

declare(strict_types=1);

namespace OCA\OpenInCryptPad\Listener;

use OCA\OpenInCryptPad\AppInfo\Application;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\Util;


/** @template-implements IEventListener<\OCA\Files_Sharing\Event\BeforeTemplateRenderedEvent> */
class PublicShareBeforeTemplateRenderedListener implements IEventListener {

public function __construct() {
}

public function handle(Event $event): void {
Util::addScript(Application::APP_ID, 'openincryptpad-public');
}
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "openincryptpad",
"description": "Open files in CryptPad",
"version": "0.3.6",
"version": "0.4.0",
"author": "XWiki CryptPad Team <contact@cryptpad.org> and contributors",
"contributors": [],
"bugs": {
Expand Down
34 changes: 34 additions & 0 deletions psalm.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,40 @@
</projectFiles>
<extraFiles>
<directory name="vendor" />

<directory name="nextcloud-server/apps/admin_audit"/>
<directory name="nextcloud-server/apps/cloud_federation_api"/>
<directory name="nextcloud-server/apps/comments"/>
<directory name="nextcloud-server/apps/contactsinteraction"/>
<directory name="nextcloud-server/apps/dashboard"/>
<directory name="nextcloud-server/apps/dav"/>
<directory name="nextcloud-server/apps/encryption"/>
<directory name="nextcloud-server/apps/federatedfilesharing"/>
<directory name="nextcloud-server/apps/federation"/>
<directory name="nextcloud-server/apps/files"/>
<directory name="nextcloud-server/apps/files_external"/>
<directory name="nextcloud-server/apps/files_sharing"/>
<directory name="nextcloud-server/apps/files_trashbin"/>
<directory name="nextcloud-server/apps/files_versions"/>
<directory name="nextcloud-server/apps/lookup_server_connector"/>
<directory name="nextcloud-server/apps/oauth2"/>
<directory name="nextcloud-server/apps/provisioning_api"/>
<directory name="nextcloud-server/apps/settings"/>
<directory name="nextcloud-server/apps/sharebymail"/>
<directory name="nextcloud-server/apps/systemtags"/>
<directory name="nextcloud-server/apps/testing"/>
<directory name="nextcloud-server/apps/theming"/>
<directory name="nextcloud-server/apps/twofactor_backupcodes"/>
<directory name="nextcloud-server/apps/updatenotification"/>
<directory name="nextcloud-server/apps/user_ldap"/>
<directory name="nextcloud-server/apps/user_status"/>
<directory name="nextcloud-server/apps/weather_status"/>
<directory name="nextcloud-server/apps/webhook_listeners"/>
<directory name="nextcloud-server/apps/workflowengine"/>
<directory name="nextcloud-server/core"/>
<directory name="nextcloud-server/lib"/>
<directory name="nextcloud-server/ocs"/>
<directory name="nextcloud-server/ocs-provider"/>
<ignoreFiles>
<directory name="vendor/phpunit/php-code-coverage" />
<directory name="vendor/psalm" />
Expand Down
106 changes: 76 additions & 30 deletions src/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,36 +31,67 @@
fileType,
app,
cryptPadUrl,
isShared,
fileName,
} = window.OpenInCryptPadInfo
document.title = fileName(filePath) + ' - Nextcloud'

const sessionKey = await getSessionForFile(fileId)
let blob
let viewMode = ''
document.title = fileName + ' - Nextcloud'
// if opening file from a share link, we don't get access to the file path, but we can download it
if (isShared.startsWith('true')) {
viewMode = 'view'
}

if (isShared.includes('External')) {
blob = await loadFileContentShared(filePath, mimeType)
} else {
blob = await loadFileContent(filePath, mimeType)
}

const blob = await loadFileContent(filePath, mimeType)
let viewOnlyMode = false
let sessionKey
try {
sessionKey = await getSessionForFile(fileId)
} catch (e) {
viewOnlyMode = true
}

const docUrl = URL.createObjectURL(blob)

CryptPadAPI(cryptPadUrl, 'editor-content', {
document: {
url: docUrl,
key: sessionKey,
fileType,
},
documentType: app,
events: {
onSave: (data, cb) => onSave(filePath, data, cb),
const events = viewOnlyMode
? {
onSave: (data, cb) => null,
onNewKey: (data, cb) => cb(data.new), // Just accept and ignore any session key CryptPad wants to use
onHasUnsavedChanges: (unsavedChanges) => null,
onInsertImage,
}
: {
onSave: (data, cb) => onSave(filePath, data, cb, isShared),
onNewKey: (data, cb) => updateSessionForFile(fileId, data, cb),
onHasUnsavedChanges: (unsavedChanges) => {
const elem = document.querySelector('#unsaved-indicator')
elem.className = unsavedChanges ? 'visible' : ''
},
onInsertImage,
}

CryptPadAPI(cryptPadUrl, 'editor-content', {
document: {
url: docUrl,
key: sessionKey,
fileType,
},
documentType: app,
mode: viewMode,
events,
width: '100%',
height: '100%',
})

checkForPermissionChange(filePath, () => resetCryptPadSession(fileId))
if (isShared !== 'trueExternal') {
checkForPermissionChange(filePath, () => resetCryptPadSession(fileId))
}
initBackButton()

} catch (e) {
Expand Down Expand Up @@ -159,18 +190,6 @@
.join()
}

/**
* @param {string} filePath the file path
*/
function fileName(filePath) {
if (!filePath) {
return
}

const parts = filePath.split('/')
return parts[parts.length - 1]
}

/**
*
* @param {string} filePath the file path
Expand All @@ -190,16 +209,41 @@
}
}

/**
*
* @param {string} downloadPath the download path for the file
* @param {string} mimeType the mime type
*/
async function loadFileContentShared(downloadPath, mimeType) {
try {
const response = await fetch(downloadPath)
if (!response.ok) {
throw new Error(`Failed to fetch file: ${response.statusText}`)
}
const blob = await response.blob()

return blob
} catch (e) {
console.log('MASSIVE ERROR')

Check failure on line 227 in src/editor.js

View workflow job for this annotation

GitHub Actions / eslint

Unexpected console statement
console.log(e)

Check failure on line 228 in src/editor.js

View workflow job for this annotation

GitHub Actions / eslint

Unexpected console statement
throw e[1]
}
}

/**
*
* @param {string} filePath the file path
* @param {Blob} data the data to dave
* @param {Function} cb callback
* @param {string} isShared if file is shared
*/
function onSave(filePath, data, cb) {
saveFileContent(filePath, data)
.then(() => cb())
.catch(cb)
function onSave(filePath, data, cb, isShared) {
if (isShared === 'false') {
saveFileContent(filePath, data)
.then(() => cb())
.catch(cb)
}
// if it's through a share link, we shouldn't save (read only)
}

/**
Expand All @@ -218,8 +262,10 @@
if (response.ok) {
const body = await response.json()
return body.sessionKey
} else {
} else if (response.status === 404) {
return null
} else {
throw new Error('no write permission')
}
}

Expand Down
Loading
Loading