Signed-off-by: Julien Veyssier <eneiluj@posteo.net>
This commit is contained in:
Julien Veyssier 2022-02-09 13:38:40 +01:00
Родитель 25ae0782c8
Коммит df776ef81a
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4141FEE162030638
9 изменённых файлов: 550 добавлений и 1 удалений

Просмотреть файл

@ -52,6 +52,10 @@ return [
['name' => 'settings#updateWatermarkSettings', 'url' => 'settings/watermark', 'verb' => 'POST'],
['name' => 'settings#checkSettings', 'url' => 'settings/check', 'verb' => 'GET'],
['name' => 'settings#demoServers', 'url' => 'settings/demo', 'verb' => 'GET'],
['name' => 'settings#getFontNames', 'url' => 'settings/fonts', 'verb' => 'GET'],
['name' => 'settings#getFontFile', 'url' => 'settings/fonts/{name}', 'verb' => 'GET'],
['name' => 'settings#deleteFontFile', 'url' => 'settings/fonts/{name}', 'verb' => 'DELETE'],
['name' => 'settings#uploadFontFile', 'url' => 'settings/fonts', 'verb' => 'POST'],
//Mobile access
['name' => 'directView#show', 'url' => '/direct/{token}', 'verb' => 'GET'],

Просмотреть файл

@ -11,8 +11,11 @@
namespace OCA\Richdocuments\Controller;
use Exception;
use OCA\Richdocuments\Service\CapabilitiesService;
use OCA\Richdocuments\Service\DemoService;
use OCA\Richdocuments\Service\FontService;
use OCA\Richdocuments\UploadException;
use OCA\Richdocuments\WOPI\DiscoveryManager;
use OCA\Richdocuments\WOPI\Parser;
use \OCP\AppFramework\Controller;
@ -26,8 +29,13 @@ use \OCP\IL10N;
use OCA\Richdocuments\AppConfig;
use OCP\IConfig;
use OCP\PreConditionNotMetException;
use OCP\Util;
class SettingsController extends Controller{
public const FONT_MIME_TYPES = [
'font/ttf',
];
/** @var IL10N */
private $l10n;
/** @var AppConfig */
@ -46,6 +54,10 @@ class SettingsController extends Controller{
private $demoService;
/** @var ILogger */
private $logger;
/**
* @var FontService
*/
private $fontService;
public function __construct($appName,
IRequest $request,
@ -56,6 +68,7 @@ class SettingsController extends Controller{
Parser $wopiParser,
CapabilitiesService $capabilitiesService,
DemoService $demoService,
FontService $fontService,
ILogger $logger,
$userId
) {
@ -69,6 +82,7 @@ class SettingsController extends Controller{
$this->demoService = $demoService;
$this->logger = $logger;
$this->userId = $userId;
$this->fontService = $fontService;
}
/**
@ -284,4 +298,94 @@ class SettingsController extends Controller{
return new JSONResponse($response);
}
/**
* @NoAdminRequired
* @PublicPage
* @NoCSRFRequired
*
* @return JSONResponse
*/
public function getFontNames(): JSONResponse {
$response = $this->fontService->getFontFileNames();
return new JSONResponse($response);
}
/**
* @NoAdminRequired
* @PublicPage
* @NoCSRFRequired
*
* @param string $name
* @return DataResponse
*/
public function getFontFile(string $name): DataResponse {
$fontFileContent = $this->fontService->getFontFile($name);
return new DataResponse($fontFileContent);
}
/**
* @param string $name
* @return DataResponse
*/
public function deleteFontFile(string $name): DataResponse {
$this->fontService->deleteFontFile($name);
return new DataResponse();
}
/**
* @return JSONResponse
*/
public function uploadFontFile(): JSONResponse {
try {
$file = $this->getUploadedFile('fontfile');
if (isset($file['tmp_name'], $file['name'], $file['type'])) {
if (!in_array($file['type'], self::FONT_MIME_TYPES, true)) {
return new JSONResponse(['error' => 'Font type not supported'], Http::STATUS_BAD_REQUEST);
}
$newFileResource = fopen($file['tmp_name'], 'rb');
if ($newFileResource === false) {
throw new Exception('Could not read file');
}
$newFileName = $file['name'];
$uploadResult = $this->fontService->uploadFontFile($newFileName, $newFileResource);
return new JSONResponse($uploadResult);
}
return new JSONResponse(['error' => 'No uploaded file'], Http::STATUS_BAD_REQUEST);
} catch (Exception $e) {
$this->logger->error('Upload error', ['exception' => $e]);
return new JSONResponse(['error' => 'Upload error'], Http::STATUS_BAD_REQUEST);
}
}
/**
* @param string $key
* @return array
* @throws UploadException
*/
private function getUploadedFile(string $key): array {
$file = $this->request->getUploadedFile($key);
$error = null;
$phpFileUploadErrors = [
UPLOAD_ERR_OK => $this->l10n->t('The file was uploaded'),
UPLOAD_ERR_INI_SIZE => $this->l10n->t('The uploaded file exceeds the upload_max_filesize directive in php.ini'),
UPLOAD_ERR_FORM_SIZE => $this->l10n->t('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form'),
UPLOAD_ERR_PARTIAL => $this->l10n->t('The file was only partially uploaded'),
UPLOAD_ERR_NO_FILE => $this->l10n->t('No file was uploaded'),
UPLOAD_ERR_NO_TMP_DIR => $this->l10n->t('Missing a temporary folder'),
UPLOAD_ERR_CANT_WRITE => $this->l10n->t('Could not write file to disk'),
UPLOAD_ERR_EXTENSION => $this->l10n->t('A PHP extension stopped the file upload'),
];
if (empty($file)) {
$error = $this->l10n->t('No file uploaded or file size exceeds maximum of %s', [Util::humanFileSize(Util::uploadLimit())]);
}
if (!empty($file) && array_key_exists('error', $file) && $file['error'] !== UPLOAD_ERR_OK) {
$error = $phpFileUploadErrors[$file['error']];
}
if ($error !== null) {
throw new UploadException($error);
}
return $file;
}
}

Просмотреть файл

@ -0,0 +1,84 @@
<?php
/**
* @copyright Copyright (c) 2022 Julien Veyssier <eneiluj@posteo.net>
*
* @author Julien Veyssier <eneiluj@posteo.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\Service;
use OCP\Files\IAppData;
use OCP\Files\NotFoundException;
use OCP\Files\SimpleFS\ISimpleFile;
use OCP\Files\SimpleFS\ISimpleFolder;
class FontService {
/**
* @var IAppData
*/
private $appData;
public function __construct(IAppData $appData) {
$this->appData = $appData;
}
private function getFontAppDataDir(): ISimpleFolder {
try {
return $this->appData->getFolder('fonts');
} catch (NotFoundException $e) {
return $this->appData->newFolder('fonts');
}
}
/**
* Get the list of available font files
*
*/
public function getFontFileNames(): array {
$fontDir = $this->getFontAppDataDir();
return array_map(
function (ISimpleFile $f) use ($fontDir) {
return $f->getName();
},
$fontDir->getDirectoryListing()
);
}
public function uploadFontFile(string $fileName, $newFileResource): array {
$fontDir = $this->getFontAppDataDir();
$newFile = $fontDir->newFile($fileName, $newFileResource);
return [
'size' => $newFile->getSize(),
];
}
public function getFontFile(string $fileName): string {
$fontDir = $this->getFontAppDataDir();
return $fontDir->getFile($fileName)->getContent();
}
public function deleteFontFile(string $fileName): void {
$fontDir = $this->getFontAppDataDir();
if ($fontDir->fileExists($fileName)) {
$fontDir->getFile($fileName)->delete();
}
}
}

Просмотреть файл

@ -26,6 +26,7 @@ namespace OCA\Richdocuments\Settings;
use OCA\Richdocuments\AppConfig;
use OCA\Richdocuments\Service\CapabilitiesService;
use OCA\Richdocuments\Service\DemoService;
use OCA\Richdocuments\Service\FontService;
use OCA\Richdocuments\Service\InitialStateService;
use OCA\Richdocuments\TemplateManager;
use OCP\AppFramework\Http\TemplateResponse;
@ -51,6 +52,10 @@ class Admin implements ISettings {
/** @var InitialStateService */
private $initialState;
/**
* @var FontService
*/
private $fontService;
public function __construct(
IConfig $config,
@ -58,6 +63,7 @@ class Admin implements ISettings {
TemplateManager $manager,
CapabilitiesService $capabilitiesService,
DemoService $demoService,
FontService $fontService,
InitialStateService $initialStateService
) {
$this->config = $config;
@ -66,6 +72,7 @@ class Admin implements ISettings {
$this->capabilitiesService = $capabilitiesService;
$this->demoService = $demoService;
$this->initialState = $initialStateService;
$this->fontService = $fontService;
}
public function getForm() {
@ -89,7 +96,8 @@ class Admin implements ISettings {
'demo_servers' => $this->demoService->fetchDemoServers(),
'web_server' => strtolower($_SERVER['SERVER_SOFTWARE']),
'os_family' => PHP_VERSION_ID >= 70200 ? PHP_OS_FAMILY : PHP_OS,
'platform' => php_uname('m')
'platform' => php_uname('m'),
'fonts' => $this->fontService->getFontFileNames(),
],
],
'blank'

27
lib/UploadException.php Normal file
Просмотреть файл

@ -0,0 +1,27 @@
<?php
/**
* @copyright Copyright (c) 2022 Julien Veyssier <eneiluj@posteo.net>
*
* @author Julien Veyssier <eneiluj@posteo.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;
class UploadException extends \Exception {
}

Просмотреть файл

@ -273,6 +273,16 @@
:hint="t('richdocuments', 'List of IPV4 and IPV6 IP-addresses and subnets that are allowed to perform requests of the WOPI endpoints. If no allow list is specified all hosts will be allowed. E.g. 10.0.0.20,10.0.4.0/24')"
:disabled="updating"
@update="updateWopiAllowlist" />
<SettingsInputFile
:label="t('richdocuments', 'Upload extra font file')"
:uploading="uploadingFont"
:mimetypes="fontMimes"
@change="uploadFont" />
<SettingsFontList
:fonts="settings.fonts"
:label="t('richdocuments', 'Available fonts')"
@deleted="onFontDeleted" />
</div>
<div v-if="isSetup" id="secure-view-settings" class="section">
@ -355,11 +365,14 @@ import { showWarning } from '@nextcloud/dialogs'
import Multiselect from '@nextcloud/vue/dist/Components/Multiselect'
import Modal from '@nextcloud/vue/dist/Components/Modal'
import axios from '@nextcloud/axios'
import { showError } from '@nextcloud/dialogs'
import SettingsCheckbox from './SettingsCheckbox'
import SettingsInputText from './SettingsInputText'
import SettingsSelectTag from './SettingsSelectTag'
import SettingsSelectGroup from './SettingsSelectGroup'
import SettingsExternalApps from './SettingsExternalApps'
import SettingsInputFile from './SettingsInputFile'
import SettingsFontList from './SettingsFontList'
import '@nextcloud/dialogs/styles/toast.scss'
@ -367,6 +380,7 @@ const SERVER_STATE_OK = 0
const SERVER_STATE_LOADING = 1
const SERVER_STATE_CONNECTION_ERROR = 2
const PROTOCOL_MISMATCH = 3
const fontMimes = ['font/ttf']
export default {
name: 'AdminSettings',
@ -377,6 +391,8 @@ export default {
SettingsSelectGroup,
Multiselect,
SettingsExternalApps,
SettingsInputFile,
SettingsFontList,
Modal,
},
props: {
@ -401,6 +417,8 @@ export default {
appUrl: generateUrl('/settings/apps/app-bundles/richdocumentscode'),
approvedDemoModal: false,
updating: false,
uploadingFont: false,
fontMimes,
groups: [],
tags: [],
uiVisible: {
@ -425,6 +443,7 @@ export default {
allTagsList: [],
text: '',
},
fonts: [],
},
}
},
@ -472,6 +491,7 @@ export default {
}
Vue.set(this.settings, 'edit_groups', this.settings.edit_groups ? this.settings.edit_groups.split('|') : null)
Vue.set(this.settings, 'use_groups', this.settings.use_groups ? this.settings.use_groups.split('|') : null)
Vue.set(this.settings, 'fonts', this.initial.fonts ? this.initial.fonts : [])
this.uiVisible.canonical_webroot = !!(this.settings.canonical_webroot && this.settings.canonical_webroot !== '')
this.uiVisible.external_apps = !!(this.settings.external_apps && this.settings.external_apps !== '')
@ -639,6 +659,43 @@ export default {
return url.protocol
},
uploadFont(event) {
// TODO define font format list
const files = event.target.files
const file = files[0]
if (!fontMimes.includes(file.type)) {
showError(t('text', 'Font format not supported'))
return
}
this.uploadingFont = true
// Clear input to ensure that the change event will be emitted if
// the same file is picked again.
event.target.value = ''
const formData = new FormData()
formData.append('fontfile', file)
const url = generateUrl('/apps/richdocuments/settings/fonts')
axios.post(url, formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
}).then((response) => {
// TODO reload font list
this.settings.fonts.push(file.name)
}).catch((error) => {
console.error(error)
showError(error?.response?.data?.error)
}).then(() => {
this.uploadingFont = false
})
},
onFontDeleted(name) {
const index = this.settings.fonts.indexOf(name)
if (index !== -1) {
this.settings.fonts.splice(index, 1)
}
},
},
}
</script>

Просмотреть файл

@ -0,0 +1,84 @@
<!--
- @copyright Copyright (c) 2022 Julien Veyssier <eneiluj@posteo.net>
-
- @author Julien Veyssier <eneiluj@posteo.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 class="settings-font">
<label>{{ name }}</label>
<button
:class="{
'icon-delete': true,
svg: true,
'loading-small': disabled,
}"
:disabled="disabled"
@click="onDeleteClick" />
</div>
</template>
<script>
import axios from '@nextcloud/axios'
import { generateUrl } from '@nextcloud/router'
import { showError } from '@nextcloud/dialogs'
export default {
name: 'SettingsFont',
props: {
name: {
type: String,
required: true,
}
},
data() {
return {
disabled: false,
}
},
computed: {
},
methods: {
onDeleteClick() {
console.debug('DELETE', this.name)
this.disabled = true
const url = generateUrl('/apps/richdocuments/settings/fonts/') + encodeURIComponent(this.name)
axios.delete(url).then((response) => {
this.$emit('deleted')
}).catch((error) => {
console.error(error)
showError(error?.response?.data?.error)
}).then(() => {
this.disabled = false
})
},
},
}
</script>
<style scoped lang="scss">
.settings-font {
display: flex;
align-items: center;
button {
margin-left: 15px;
}
}
</style>

Просмотреть файл

@ -0,0 +1,77 @@
<!--
- @copyright Copyright (c) 2022 Julien Veyssier <eneiluj@posteo.net>
-
- @author Julien Veyssier <eneiluj@posteo.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 class="settings-entry">
<b>{{ label }}</b>
<SettingsFont v-for="name in sortedFonts"
:key="name"
:name="name"
@deleted="$emit('deleted', name)" />
</div>
</template>
<script>
import SettingsFont from './SettingsFont'
export default {
name: 'SettingsFontList',
components: {
SettingsFont,
},
props: {
label: {
type: String,
required: true,
},
fonts: {
type: Array,
default: () => [],
}
},
data() {
return {
}
},
computed: {
sortedFonts() {
return this.fonts.slice().sort((a, b) => {
const la = a.toLowerCase()
const lb = b.toLowerCase()
return la > lb
? 1
: la < lb
? -1
: 0
})
}
},
methods: {
},
}
</script>
<style scoped lang="scss">
label {
padding: 15px 0 15px 0;
}
</style>

Просмотреть файл

@ -0,0 +1,104 @@
<!--
- @copyright Copyright (c) 2022 Julien Veyssier <eneiluj@posteo.net>
-
- @author Julien Veyssier <eneiluj@posteo.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 class="settings-entry">
<label :for="id">{{ label }}</label><br>
<button
id="uploadlogo"
:class="{
'icon-upload': true,
svg: true,
'loading-small': uploading,
}"
:disabled="uploading"
@click="onUploadClick" />
<em v-if="hint !== ''">{{ hint }}</em>
<input :id="id"
ref="fileInput"
type="file"
aria-hidden="true"
class="hidden-visually"
:accept="acceptedMimeTypes"
@change="$emit('change', $event)">
</div>
</template>
<script>
let uuid = 0
export default {
name: 'SettingsInputFile',
props: {
label: {
type: String,
required: true,
},
hint: {
type: String,
default: '',
},
uploading: {
type: Boolean,
default: false,
},
mimetypes: {
type: Array,
default: () => ['*'],
}
},
data() {
return {
}
},
computed: {
id() {
return 'settings-file-' + this.uuid
},
acceptedMimeTypes() {
return this.mimetypes.join(',')
},
},
watch: {
},
beforeCreate() {
this.uuid = uuid.toString()
uuid += 1
},
methods: {
onUploadClick() {
this.$refs.fileInput.click()
},
},
}
</script>
<style scoped lang="scss">
.settings-entry {
padding: 15px 0 15px 0;
display: flex;
align-items: center;
button {
margin-left: 15px;
}
}
</style>