Merge pull request #25090 from nextcloud/enh/file-templates

File templates
This commit is contained in:
Roeland Jago Douma 2021-01-28 16:17:29 +01:00 committed by GitHub
commit 62fa85c7bf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 1782 additions and 16 deletions

View File

@ -138,6 +138,21 @@ $application->registerRoutes(
'url' => '/api/v1/directEditing/create',
'verb' => 'POST'
],
[
'name' => 'Template#list',
'url' => '/api/v1/templates',
'verb' => 'GET'
],
[
'name' => 'Template#create',
'url' => '/api/v1/templates/create',
'verb' => 'POST'
],
[
'name' => 'Template#path',
'url' => '/api/v1/templates/path',
'verb' => 'POST'
],
[
'name' => 'TransferOwnership#transfer',
'url' => '/api/v1/transferownership',

View File

@ -35,6 +35,7 @@ return array(
'OCA\\Files\\Controller\\ApiController' => $baseDir . '/../lib/Controller/ApiController.php',
'OCA\\Files\\Controller\\DirectEditingController' => $baseDir . '/../lib/Controller/DirectEditingController.php',
'OCA\\Files\\Controller\\DirectEditingViewController' => $baseDir . '/../lib/Controller/DirectEditingViewController.php',
'OCA\\Files\\Controller\\TemplateController' => $baseDir . '/../lib/Controller/TemplateController.php',
'OCA\\Files\\Controller\\TransferOwnershipController' => $baseDir . '/../lib/Controller/TransferOwnershipController.php',
'OCA\\Files\\Controller\\ViewController' => $baseDir . '/../lib/Controller/ViewController.php',
'OCA\\Files\\Db\\TransferOwnership' => $baseDir . '/../lib/Db/TransferOwnership.php',

View File

@ -50,6 +50,7 @@ class ComposerStaticInitFiles
'OCA\\Files\\Controller\\ApiController' => __DIR__ . '/..' . '/../lib/Controller/ApiController.php',
'OCA\\Files\\Controller\\DirectEditingController' => __DIR__ . '/..' . '/../lib/Controller/DirectEditingController.php',
'OCA\\Files\\Controller\\DirectEditingViewController' => __DIR__ . '/..' . '/../lib/Controller/DirectEditingViewController.php',
'OCA\\Files\\Controller\\TemplateController' => __DIR__ . '/..' . '/../lib/Controller/TemplateController.php',
'OCA\\Files\\Controller\\TransferOwnershipController' => __DIR__ . '/..' . '/../lib/Controller/TransferOwnershipController.php',
'OCA\\Files\\Controller\\ViewController' => __DIR__ . '/..' . '/../lib/Controller/ViewController.php',
'OCA\\Files\\Db\\TransferOwnership' => __DIR__ . '/..' . '/../lib/Db/TransferOwnership.php',

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

2
apps/files/js/dist/templates.js vendored Normal file

File diff suppressed because one or more lines are too long

1
apps/files/js/dist/templates.js.map vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -119,6 +119,11 @@
var lastPos;
var checkInput = function () {
// Special handling for the setup template directory
if ($target.attr('data-action') === 'template-init') {
return true;
}
var filename = $input.val();
try {
if (!Files.isFileNameValid(filename)) {
@ -198,7 +203,21 @@
iconClass: actionSpec.iconClass,
fileType: actionSpec.fileType,
actionHandler: actionSpec.actionHandler,
});
checkFilename: actionSpec.checkFilename
});
},
/**
* Remove a menu item from the "New" file menu
* @param {string} actionId
*/
removeMenuEntry: function(actionId) {
var index = this._menuItems.findIndex(function (actionSpec) {
return actionSpec.id === actionId;
});
if (index > -1) {
this._menuItems.splice(index, 1);
}
},
/**

View File

@ -0,0 +1,76 @@
<?php
/**
* @copyright Copyright (c) 2021 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/>.
*
*/
declare(strict_types=1);
namespace OCA\Files\Controller;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\OCS\OCSForbiddenException;
use OCP\AppFramework\OCSController;
use OCP\Files\GenericFileException;
use OCP\Files\Template\ITemplateManager;
use OCP\IRequest;
class TemplateController extends OCSController {
protected $templateManager;
public function __construct($appName, IRequest $request, ITemplateManager $templateManager) {
parent::__construct($appName, $request);
$this->templateManager = $templateManager;
}
/**
* @NoAdminRequired
*/
public function list(): DataResponse {
return new DataResponse($this->templateManager->listTemplates());
}
/**
* @NoAdminRequired
* @throws OCSForbiddenException
*/
public function create(string $filePath, string $templatePath = '', string $templateType = 'user'): DataResponse {
try {
return new DataResponse($this->templateManager->createFromTemplate($filePath, $templatePath, $templateType));
} catch (GenericFileException $e) {
throw new OCSForbiddenException($e->getMessage());
}
}
/**
* @NoAdminRequired
*/
public function path(string $templatePath = '', bool $copySystemTemplates = false) {
try {
$templatePath = $this->templateManager->initializeTemplateDirectory($templatePath, null, $copySystemTemplates);
return new DataResponse([
'template_path' => $templatePath,
'templates' => $this->templateManager->listCreators()
]);
} catch (\Exception $e) {
throw new OCSForbiddenException($e->getMessage());
}
}
}

View File

@ -44,10 +44,12 @@ use OCP\AppFramework\Http\ContentSecurityPolicy;
use OCP\AppFramework\Http\RedirectResponse;
use OCP\AppFramework\Http\Response;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Services\IInitialState;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\Folder;
use OCP\Files\IRootFolder;
use OCP\Files\NotFoundException;
use OCP\Files\Template\ITemplateManager;
use OCP\IConfig;
use OCP\IL10N;
use OCP\IRequest;
@ -80,6 +82,10 @@ class ViewController extends Controller {
protected $rootFolder;
/** @var Helper */
protected $activityHelper;
/** @var IInitialState */
private $initialState;
/** @var ITemplateManager */
private $templateManager;
public function __construct(string $appName,
IRequest $request,
@ -90,7 +96,9 @@ class ViewController extends Controller {
IUserSession $userSession,
IAppManager $appManager,
IRootFolder $rootFolder,
Helper $activityHelper
Helper $activityHelper,
IInitialState $initialState,
ITemplateManager $templateManager
) {
parent::__construct($appName, $request);
$this->appName = $appName;
@ -103,6 +111,8 @@ class ViewController extends Controller {
$this->appManager = $appManager;
$this->rootFolder = $rootFolder;
$this->activityHelper = $activityHelper;
$this->initialState = $initialState;
$this->templateManager = $templateManager;
}
/**
@ -180,6 +190,7 @@ class ViewController extends Controller {
// Load the files we need
\OCP\Util::addStyle('files', 'merged');
\OCP\Util::addScript('files', 'merged-index');
\OCP\Util::addScript('files', 'dist/templates');
// mostly for the home storage's free space
// FIXME: Make non static
@ -283,6 +294,8 @@ class ViewController extends Controller {
if (class_exists(LoadViewer::class)) {
$this->eventDispatcher->dispatchTyped(new LoadViewer());
}
$this->initialState->provideInitialState('templates_path', $this->templateManager->hasTemplateDirectory() ? $this->templateManager->getTemplatePath() : null);
$this->initialState->provideInitialState('templates', $this->templateManager->listCreators());
$params = [];
$params['usedSpacePercent'] = (int) $storageInfo['relative'];

View File

@ -30,7 +30,8 @@ namespace OCA\Files\Event;
use OCP\EventDispatcher\Event;
/**
* This event is triggered when the files app is rendered. It canb e used to add additional scripts to the files app.
* This event is triggered when the files app is rendered.
* It can be used to add additional scripts to the files app.
*
* @since 17.0.0
*/

View File

@ -0,0 +1,220 @@
<!--
- @copyright Copyright (c) 2020 John Molakvoæ <skjnldsv@protonmail.com>
-
- @author John Molakvoæ <skjnldsv@protonmail.com>
-
- @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>
<li class="template-picker__item">
<input :id="id"
:checked="checked"
type="radio"
class="radio"
name="template-picker"
@change="onCheck">
<label :for="id" class="template-picker__label">
<div class="template-picker__preview"
:class="failedPreview ? 'template-picker__preview--failed' : ''">
<img class="template-picker__image"
:src="realPreviewUrl"
alt=""
draggable="false"
@error="onFailure">
</div>
<span class="template-picker__title">
{{ nameWithoutExt }}
</span>
</label>
</li>
</template>
<script>
import { generateUrl } from '@nextcloud/router'
import { encodeFilePath } from '../utils/fileUtils'
import { getToken, isPublic } from '../utils/davUtils'
// preview width generation
const previewWidth = 256
export default {
name: 'TemplatePreview',
inheritAttrs: false,
props: {
basename: {
type: String,
required: true,
},
checked: {
type: Boolean,
default: false,
},
fileid: {
type: [String, Number],
required: true,
},
filename: {
type: String,
required: true,
},
previewUrl: {
type: String,
default: null,
},
hasPreview: {
type: Boolean,
default: true,
},
mime: {
type: String,
required: true,
},
ratio: {
type: Number,
default: null,
},
},
data() {
return {
failedPreview: false,
}
},
computed: {
/**
* Strip away extension from name
* @returns {string}
*/
nameWithoutExt() {
return this.basename.indexOf('.') > -1 ? this.basename.split('.').slice(0, -1).join('.') : this.basename
},
id() {
return `template-picker-${this.fileid}`
},
realPreviewUrl() {
// If original preview failed, fallback to mime icon
if (this.failedPreview && this.mimeIcon) {
return this.mimeIcon
}
if (this.previewUrl) {
return this.previewUrl
}
// TODO: find a nicer standard way of doing this?
if (isPublic()) {
return generateUrl(`/apps/files_sharing/publicpreview/${getToken()}?fileId=${this.fileid}&file=${encodeFilePath(this.filename)}&x=${previewWidth}&y=${previewWidth}&a=1`)
}
return generateUrl(`/core/preview?fileId=${this.fileid}&x=${previewWidth}&y=${previewWidth}&a=1`)
},
mimeIcon() {
return OC.MimeType.getIconUrl(this.mime)
},
},
methods: {
onCheck() {
this.$emit('check', this.fileid)
},
onFailure() {
this.failedPreview = true
},
},
}
</script>
<style lang="scss" scoped>
.template-picker {
&__item {
display: flex;
}
&__label {
display: flex;
// Align in the middle of the grid
align-items: center;
flex: 1 1;
flex-direction: column;
&, * {
cursor: pointer;
user-select: none;
}
&::before {
display: none !important;
}
}
&__preview {
display: block;
overflow: hidden;
// Stretch so all entries are the same width
flex: 1 1;
width: var(--width);
min-height: var(--height);
max-height: var(--height);
padding: 0;
border: var(--border) solid var(--color-border);
border-radius: var(--border-radius-large);
input:checked + label > & {
border-color: var(--color-primary);
}
&--failed {
// Make sure to properly center fallback icon
display: flex;
}
}
&__image {
max-width: 100%;
background-color: var(--color-main-background);
object-fit: cover;
}
// Failed preview, fallback to mime icon
&__preview--failed &__image {
width: calc(var(--margin) * 8);
// Center mime icon
margin: auto;
background-color: transparent !important;
object-fit: initial;
}
&__title {
overflow: hidden;
// also count preview border
max-width: calc(var(--width) + 2*2px);
padding: var(--margin);
white-space: nowrap;
text-overflow: ellipsis;
}
}
</style>

View File

@ -0,0 +1,29 @@
/**
* @copyright Copyright (c) 2021 John Molakvoæ <skjnldsv@protonmail.com>
*
* @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @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 { generateOcsUrl } from '@nextcloud/router'
import axios from '@nextcloud/axios'
export const getTemplates = async function() {
const response = await axios.get(generateOcsUrl('apps/files/api/v1', 2) + 'templates')
return response.data.ocs.data
}

144
apps/files/src/templates.js Normal file
View File

@ -0,0 +1,144 @@
/**
* @copyright Copyright (c) 2020 John Molakvoæ <skjnldsv@protonmail.com>
*
* @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @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 { getLoggerBuilder } from '@nextcloud/logger'
import { loadState } from '@nextcloud/initial-state'
import { translate as t, translatePlural as n } from '@nextcloud/l10n'
import { generateOcsUrl } from '@nextcloud/router'
import { getCurrentDirectory } from './utils/davUtils'
import axios from '@nextcloud/axios'
import Vue from 'vue'
import TemplatePickerView from './views/TemplatePicker'
import { getCurrentUser } from '@nextcloud/auth'
import { showError } from '@nextcloud/dialogs'
// Set up logger
const logger = getLoggerBuilder()
.setApp('files')
.detectUser()
.build()
// Add translates functions
Vue.mixin({
methods: {
t,
n,
},
})
// Create document root
const TemplatePickerRoot = document.createElement('div')
TemplatePickerRoot.id = 'template-picker'
document.body.appendChild(TemplatePickerRoot)
// Retrieve and init templates
let templates = loadState('files', 'templates', [])
let templatesPath = loadState('files', 'templates_path', false)
logger.debug('Templates providers', templates)
logger.debug('Templates folder', { templatesPath })
// Init vue app
const View = Vue.extend(TemplatePickerView)
const TemplatePicker = new View({
name: 'TemplatePicker',
propsData: {
logger,
},
})
TemplatePicker.$mount('#template-picker')
// Init template engine after load to make sure it's the last injected entry
window.addEventListener('DOMContentLoaded', function() {
if (!templatesPath) {
logger.debug('Templates folder not initialized')
const initTemplatesPlugin = {
attach(menu) {
// register the new menu entry
menu.addMenuEntry({
id: 'template-init',
displayName: t('files', 'Set up templates folder'),
templateName: t('files', 'Templates'),
iconClass: 'icon-template-add',
fileType: 'file',
actionHandler(name) {
initTemplatesFolder(name)
menu.removeMenuEntry('template-init')
},
})
},
}
OC.Plugins.register('OCA.Files.NewFileMenu', initTemplatesPlugin)
}
})
// Init template files menu
templates.forEach((provider, index) => {
const newTemplatePlugin = {
attach(menu) {
const fileList = menu.fileList
// only attach to main file list, public view is not supported yet
if (fileList.id !== 'files' && fileList.id !== 'files.public') {
return
}
// register the new menu entry
menu.addMenuEntry({
id: `template-new-${provider.app}-${index}`,
displayName: provider.label,
templateName: provider.label + provider.extension,
iconClass: provider.iconClass || 'icon-file',
fileType: 'file',
actionHandler(name) {
TemplatePicker.open(name, provider)
},
})
},
}
OC.Plugins.register('OCA.Files.NewFileMenu', newTemplatePlugin)
})
/**
* Init the template directory
*
* @param {string} name the templates folder name
*/
const initTemplatesFolder = async function(name) {
const templatePath = (getCurrentDirectory() + `/${name}`).replace('//', '/')
try {
logger.debug('Initializing the templates directory', { templatePath })
const response = await axios.post(generateOcsUrl('apps/files/api/v1/templates', 2) + 'path', {
templatePath,
copySystemTemplates: true,
})
// Go to template directory
OCA.Files.App.currentFileList.changeDirectory(templatePath, true, true)
templates = response.data.ocs.data.templates
templatesPath = response.data.ocs.data.template_path
} catch (error) {
logger.error('Unable to initialize the templates directory')
showError(t('files', 'Unable to initialize the templates directory'))
}
}

View File

@ -0,0 +1,52 @@
/**
* @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
*
* @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @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 { generateRemoteUrl } from '@nextcloud/router'
import { getCurrentUser } from '@nextcloud/auth'
export const getRootPath = function() {
if (getCurrentUser()) {
return generateRemoteUrl(`dav/files/${getCurrentUser().uid}`)
} else {
return generateRemoteUrl('webdav').replace('/remote.php', '/public.php')
}
}
export const isPublic = function() {
return !getCurrentUser()
}
export const getToken = function() {
return document.getElementById('sharingToken') && document.getElementById('sharingToken').value
}
/**
* Return the current directory, fallback to root
* @returns {string}
*/
export const getCurrentDirectory = function() {
const currentDirInfo = OCA?.Files?.App?.currentFileList?.dirInfo
|| { path: '/', name: '' }
// Make sure we don't have double slashes
return `${currentDirInfo.path}/${currentDirInfo.name}`.replace(/\/\//gi, '/')
}

View File

@ -0,0 +1,53 @@
/**
* @copyright Copyright (c) 2021 John Molakvoæ <skjnldsv@protonmail.com>
*
* @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @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/>.
*
*/
/**
* Get an url encoded path
*
* @param {String} path the full path
* @returns {string} url encoded file path
*/
const encodeFilePath = function(path) {
const pathSections = (path.startsWith('/') ? path : `/${path}`).split('/')
let relativePath = ''
pathSections.forEach((section) => {
if (section !== '') {
relativePath += '/' + encodeURIComponent(section)
}
})
return relativePath
}
/**
* Extract dir and name from file path
*
* @param {String} path the full path
* @returns {String[]} [dirPath, fileName]
*/
const extractFilePaths = function(path) {
const pathSections = path.split('/')
const fileName = pathSections[pathSections.length - 1]
const dirPath = pathSections.slice(0, pathSections.length - 1).join('/')
return [dirPath, fileName]
}
export { encodeFilePath, extractFilePaths }

View File

@ -0,0 +1,294 @@
<!--
- @copyright Copyright (c) 2020 John Molakvoæ <skjnldsv@protonmail.com>
-
- @author John Molakvoæ <skjnldsv@protonmail.com>
-
- @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>
<Modal v-if="opened"
:clear-view-delay="-1"
class="templates-picker"
size="large"
@close="close">
<form class="templates-picker__form"
:style="style"
@submit.prevent.stop="onSubmit">
<h2>{{ t('files', 'Pick a template for {name}', { name: nameWithoutExt }) }}</h2>
<!-- Templates list -->
<ul class="templates-picker__list">
<TemplatePreview
v-bind="emptyTemplate"
:checked="checked === emptyTemplate.fileid"
@check="onCheck" />
<TemplatePreview
v-for="template in provider.templates"
:key="template.fileid"
v-bind="template"
:checked="checked === template.fileid"
:ratio="provider.ratio"
@check="onCheck" />
</ul>
<!-- Cancel and submit -->
<div class="templates-picker__buttons">
<button @click="close">
{{ t('files', 'Cancel') }}
</button>
<input type="submit"
class="primary"
:value="t('files', 'Create')"
:aria-label="t('files', 'Create a new file with the selected template')">
</div>
</form>
<EmptyContent v-if="loading" class="templates-picker__loading" icon="icon-loading">
{{ t('files', 'Creating file') }}
</EmptyContent>
</Modal>
</template>
<script>
import { generateOcsUrl } from '@nextcloud/router'
import { showError } from '@nextcloud/dialogs'
import axios from '@nextcloud/axios'
import EmptyContent from '@nextcloud/vue/dist/Components/EmptyContent'
import Modal from '@nextcloud/vue/dist/Components/Modal'
import { getCurrentDirectory } from '../utils/davUtils'
import { getTemplates } from '../services/Templates'
import TemplatePreview from '../components/TemplatePreview'
const border = 2
const margin = 8
const width = margin * 20
export default {
name: 'TemplatePicker',
components: {
EmptyContent,
Modal,
TemplatePreview,
},
props: {
logger: {
type: Object,
required: true,
},
},
data() {
return {
// Check empty template by default
checked: -1,
loading: false,
name: null,
opened: false,
provider: null,
}
},
computed: {
/**
* Strip away extension from name
* @returns {string}
*/
nameWithoutExt() {
return this.name.indexOf('.') > -1 ? this.name.split('.').slice(0, -1).join('.') : this.name
},
emptyTemplate() {
return {
basename: t('files', 'Blank'),
fileid: -1,
filename: this.t('files', 'Blank'),
hasPreview: false,
mime: this.provider?.mimetypes[0] || this.provider?.mimetypes,
}
},
selectedTemplate() {
return this.provider.templates.find(template => template.fileid === this.checked)
},
/**
* Style css vars bin,d
* @returns {Object}
*/
style() {
return {
'--margin': margin + 'px',
'--width': width + 'px',
'--border': border + 'px',
'--fullwidth': width + 2 * margin + 2 * border + 'px',
'--height': this.provider.ratio ? Math.round(width / this.provider.ratio) + 'px' : null,
}
},
},
methods: {
/**
* Open the picker
* @param {string} name the file name to create
* @param {object} provider the template provider picked
*/
async open(name, provider) {
this.checked = this.emptyTemplate.fileid
this.name = name
this.provider = provider
const templates = await getTemplates()
const fetchedProvider = templates.find((fetchedProvider) => fetchedProvider.app === provider.app && fetchedProvider.label === provider.label)
if (fetchedProvider === null) {
throw new Error('Failed to match provider in results')
}
this.provider = fetchedProvider
// If there is no templates available, just create an empty file
if (fetchedProvider.templates.length === 0) {
this.onSubmit()
return
}
// Else, open the picker
this.opened = true
},
/**
* Close the picker and reset variables
*/
close() {
this.checked = this.emptyTemplate.fileid
this.loading = false
this.name = null
this.opened = false
this.provider = null
},
/**
* Manages the radio template picker change
* @param {number} fileid the selected template file id
*/
onCheck(fileid) {
this.checked = fileid
},
async onSubmit() {
this.loading = true
const currentDirectory = getCurrentDirectory()
const fileList = OCA?.Files?.App?.currentFileList
try {
const response = await axios.post(generateOcsUrl('apps/files/api/v1/templates', 2) + 'create', {
filePath: `${currentDirectory}/${this.name}`,
templatePath: this.selectedTemplate?.filename,
templateType: this.selectedTemplate?.templateType,
})
const fileInfo = response.data.ocs.data
this.logger.debug('Created new file', fileInfo)
// Run default action
const fileAction = OCA.Files.fileActions.getDefaultFileAction(fileInfo.mime, 'file', OC.PERMISSION_ALL)
fileAction.action(fileInfo.basename, {
$file: null,
dir: currentDirectory,
fileList,
fileActions: fileList?.fileActions,
})
// Reload files list
fileList?.reload?.() || window.location.reload()
this.close()
} catch (error) {
this.logger.error('Error while creating the new file from template')
console.error(error)
showError(this.t('files', 'Unable to create new file from template'))
} finally {
this.loading = false
}
},
},
}
</script>
<style lang="scss" scoped>
.templates-picker {
&__form {
padding: calc(var(--margin) * 2);
// Will be handled by the buttons
padding-bottom: 0;
h2 {
text-align: center;
font-weight: bold;
margin: var(--margin) 0 calc(var(--margin) * 2);
}
}
&__list {
display: grid;
grid-gap: calc(var(--margin) * 2);
grid-auto-columns: 1fr;
// We want maximum 5 columns. Putting 6 as we don't count the grid gap. So it will always be lower than 6
max-width: calc(var(--fullwidth) * 6);
grid-template-columns: repeat(auto-fit, var(--fullwidth));
// Make sure all rows are the same height
grid-auto-rows: 1fr;
// Center the columns set
justify-content: center;
}
&__buttons {
display: flex;
justify-content: space-between;
padding: calc(var(--margin) * 2) var(--margin);
position: sticky;
bottom: 0;
background-image: linear-gradient(0, var(--gradient-main-background));
button, input[type='submit'] {
height: 44px;
}
}
// Make sure we're relative for the loading emptycontent on top
/deep/ .modal-container {
position: relative;
overflow-y: auto !important;
}
&__loading {
position: absolute;
top: 0;
left: 0;
justify-content: center;
width: 100%;
height: 100%;
margin: 0;
background-color: var(--color-main-background-translucent);
}
}
</style>

View File

@ -36,10 +36,12 @@ use OCA\Files\Activity\Helper;
use OCA\Files\Controller\ViewController;
use OCP\App\IAppManager;
use OCP\AppFramework\Http;
use OCP\AppFramework\Services\IInitialState;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\File;
use OCP\Files\Folder;
use OCP\Files\IRootFolder;
use OCP\Files\Template\ITemplateManager;
use OCP\IConfig;
use OCP\IL10N;
use OCP\IRequest;
@ -78,6 +80,10 @@ class ViewControllerTest extends TestCase {
private $rootFolder;
/** @var Helper|\PHPUnit\Framework\MockObject\MockObject */
private $activityHelper;
/** @var IInitialState|\PHPUnit\Framework\MockObject\MockObject */
private $initialState;
/** @var ITemplateManager|\PHPUnit\Framework\MockObject\MockObject */
private $templateManager;
protected function setUp(): void {
parent::setUp();
@ -97,6 +103,8 @@ class ViewControllerTest extends TestCase {
->willReturn($this->user);
$this->rootFolder = $this->getMockBuilder('\OCP\Files\IRootFolder')->getMock();
$this->activityHelper = $this->createMock(Helper::class);
$this->initialState = $this->createMock(IInitialState::class);
$this->templateManager = $this->createMock(ITemplateManager::class);
$this->viewController = $this->getMockBuilder('\OCA\Files\Controller\ViewController')
->setConstructorArgs([
'files',
@ -109,6 +117,8 @@ class ViewControllerTest extends TestCase {
$this->appManager,
$this->rootFolder,
$this->activityHelper,
$this->initialState,
$this->templateManager,
])
->setMethods([
'getStorageInfo',

View File

@ -2,7 +2,8 @@ const path = require('path');
module.exports = {
entry: {
'sidebar': path.join(__dirname, 'src', 'sidebar.js'),
sidebar: path.join(__dirname, 'src', 'sidebar.js'),
templates: path.join(__dirname, 'src', 'templates.js'),
'files-app-settings': path.join(__dirname, 'src', 'files-app-settings.js'),
'personal-settings': path.join(__dirname, 'src', 'main-personal-settings.js'),
},

View File

@ -317,6 +317,21 @@ $CONFIG = [
*/
'skeletondirectory' => '/path/to/nextcloud/core/skeleton',
/**
* The directory where the template files are located. These files will be
* copied to the template directory of new users. Leave empty to not copy any
* template files.
* ``{lang}`` can be used as a placeholder for the language of the user.
* If the directory does not exist, it falls back to non dialect (from ``de_DE``
* to ``de``). If that does not exist either, it falls back to ``default``
*
* If this is not set creating a template directory will only happen if no custom
* ``skeletondirectory`` is defined, otherwise the shipped templates will be used
* to create a template directory for the user.
*/
'templatesdirectory' => '/path/to/nextcloud/templates',
/**
* If your user backend does not allow password resets (e.g. when it's a
* read-only user backend like LDAP), you can specify a custom link, where the

View File

@ -7,6 +7,9 @@
--color-main-background: #{$color-main-background};
--color-main-background-translucent: #{$color-main-background-translucent};
// To use like this: background-image: linear-gradient(0, var(--gradient-main-background));
--gradient-main-background: var(--color-main-background) 0%, var(--color-main-background-translucent) 85%, transparent 100%;
--color-background-hover: #{$color-background-hover};
--color-background-dark: #{$color-background-dark};
--color-background-darker: #{$color-background-darker};

View File

@ -229,6 +229,7 @@ audio, canvas, embed, iframe, img, input, object, video {
@include icon-black-white('quota', 'actions', 1, true);
@include icon-black-white('rename', 'actions', 1, true);
@include icon-black-white('screen', 'actions', 1, true);
@include icon-black-white('template-add', 'actions', 1, true);
.icon-screen-white {
filter: drop-shadow(1px 1px 4px var(--color-box-shadow));

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 21.33 21.33" width="16" height="16"><path fill="none" d="M0 0h24v24H0z"/><path d="M15.33 18h-12V6H10V4H3.33a2 2 0 00-2 2v12c0 1.1.9 2 2 2h12a2 2 0 002-2v-6.67h-2zM17.33 1.33h-2V4h-2.66v2h2.66v2.67h2V6H20V4h-2.67z"/><path d="M5.33 14.33h8v2h-8zm8-1.33v-2h-8v2zm-8-5.33h8v2h-8z"/></svg>

After

Width:  |  Height:  |  Size: 337 B

View File

@ -315,6 +315,11 @@ return array(
'OCP\\Files\\Storage\\IStorage' => $baseDir . '/lib/public/Files/Storage/IStorage.php',
'OCP\\Files\\Storage\\IStorageFactory' => $baseDir . '/lib/public/Files/Storage/IStorageFactory.php',
'OCP\\Files\\Storage\\IWriteStreamStorage' => $baseDir . '/lib/public/Files/Storage/IWriteStreamStorage.php',
'OCP\\Files\\Template\\FileCreatedFromTemplateEvent' => $baseDir . '/lib/public/Files/Template/FileCreatedFromTemplateEvent.php',
'OCP\\Files\\Template\\ICustomTemplateProvider' => $baseDir . '/lib/public/Files/Template/ICustomTemplateProvider.php',
'OCP\\Files\\Template\\ITemplateManager' => $baseDir . '/lib/public/Files/Template/ITemplateManager.php',
'OCP\\Files\\Template\\Template' => $baseDir . '/lib/public/Files/Template/Template.php',
'OCP\\Files\\Template\\TemplateFileCreator' => $baseDir . '/lib/public/Files/Template/TemplateFileCreator.php',
'OCP\\Files\\UnseekableException' => $baseDir . '/lib/public/Files/UnseekableException.php',
'OCP\\Files_FullTextSearch\\Model\\AFilesDocument' => $baseDir . '/lib/public/Files_FullTextSearch/Model/AFilesDocument.php',
'OCP\\FullTextSearch\\Exceptions\\FullTextSearchAppNotAvailableException' => $baseDir . '/lib/public/FullTextSearch/Exceptions/FullTextSearchAppNotAvailableException.php',
@ -1125,6 +1130,7 @@ return array(
'OC\\Files\\Stream\\HashWrapper' => $baseDir . '/lib/private/Files/Stream/HashWrapper.php',
'OC\\Files\\Stream\\Quota' => $baseDir . '/lib/private/Files/Stream/Quota.php',
'OC\\Files\\Stream\\SeekableHttpStream' => $baseDir . '/lib/private/Files/Stream/SeekableHttpStream.php',
'OC\\Files\\Template\\TemplateManager' => $baseDir . '/lib/private/Files/Template/TemplateManager.php',
'OC\\Files\\Type\\Detection' => $baseDir . '/lib/private/Files/Type/Detection.php',
'OC\\Files\\Type\\Loader' => $baseDir . '/lib/private/Files/Type/Loader.php',
'OC\\Files\\Type\\TemplateManager' => $baseDir . '/lib/private/Files/Type/TemplateManager.php',

View File

@ -344,6 +344,11 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c
'OCP\\Files\\Storage\\IStorage' => __DIR__ . '/../../..' . '/lib/public/Files/Storage/IStorage.php',
'OCP\\Files\\Storage\\IStorageFactory' => __DIR__ . '/../../..' . '/lib/public/Files/Storage/IStorageFactory.php',
'OCP\\Files\\Storage\\IWriteStreamStorage' => __DIR__ . '/../../..' . '/lib/public/Files/Storage/IWriteStreamStorage.php',
'OCP\\Files\\Template\\FileCreatedFromTemplateEvent' => __DIR__ . '/../../..' . '/lib/public/Files/Template/FileCreatedFromTemplateEvent.php',
'OCP\\Files\\Template\\ICustomTemplateProvider' => __DIR__ . '/../../..' . '/lib/public/Files/Template/ICustomTemplateProvider.php',
'OCP\\Files\\Template\\ITemplateManager' => __DIR__ . '/../../..' . '/lib/public/Files/Template/ITemplateManager.php',
'OCP\\Files\\Template\\Template' => __DIR__ . '/../../..' . '/lib/public/Files/Template/Template.php',
'OCP\\Files\\Template\\TemplateFileCreator' => __DIR__ . '/../../..' . '/lib/public/Files/Template/TemplateFileCreator.php',
'OCP\\Files\\UnseekableException' => __DIR__ . '/../../..' . '/lib/public/Files/UnseekableException.php',
'OCP\\Files_FullTextSearch\\Model\\AFilesDocument' => __DIR__ . '/../../..' . '/lib/public/Files_FullTextSearch/Model/AFilesDocument.php',
'OCP\\FullTextSearch\\Exceptions\\FullTextSearchAppNotAvailableException' => __DIR__ . '/../../..' . '/lib/public/FullTextSearch/Exceptions/FullTextSearchAppNotAvailableException.php',
@ -1154,6 +1159,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c
'OC\\Files\\Stream\\HashWrapper' => __DIR__ . '/../../..' . '/lib/private/Files/Stream/HashWrapper.php',
'OC\\Files\\Stream\\Quota' => __DIR__ . '/../../..' . '/lib/private/Files/Stream/Quota.php',
'OC\\Files\\Stream\\SeekableHttpStream' => __DIR__ . '/../../..' . '/lib/private/Files/Stream/SeekableHttpStream.php',
'OC\\Files\\Template\\TemplateManager' => __DIR__ . '/../../..' . '/lib/private/Files/Template/TemplateManager.php',
'OC\\Files\\Type\\Detection' => __DIR__ . '/../../..' . '/lib/private/Files/Type/Detection.php',
'OC\\Files\\Type\\Loader' => __DIR__ . '/../../..' . '/lib/private/Files/Type/Loader.php',
'OC\\Files\\Type\\TemplateManager' => __DIR__ . '/../../..' . '/lib/private/Files/Type/TemplateManager.php',

View File

@ -77,6 +77,9 @@ class RegistrationContext {
/** @var array[] */
private $wellKnownHandlers = [];
/** @var array[] */
private $templateProviders = [];
/** @var ILogger */
private $logger;
@ -186,6 +189,13 @@ class RegistrationContext {
$class
);
}
public function registerTemplateProvider(string $providerClass): void {
$this->context->registerTemplateProvider(
$this->appId,
$providerClass
);
}
};
}
@ -279,6 +289,13 @@ class RegistrationContext {
];
}
public function registerTemplateProvider(string $appId, string $class): void {
$this->templateProviders[] = [
'appId' => $appId,
'class' => $class,
];
}
/**
* @param App[] $apps
*/
@ -451,7 +468,7 @@ class RegistrationContext {
}
/**
* @erturn array[]
* @return array[]
*/
public function getInitialStates(): array {
return $this->initialStates;
@ -463,4 +480,8 @@ class RegistrationContext {
public function getWellKnownHandlers(): array {
return $this->wellKnownHandlers;
}
public function getTemplateProviders(): array {
return $this->templateProviders;
}
}

View File

@ -0,0 +1,346 @@
<?php
/**
* @copyright Copyright (c) 2021 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/>.
*
*/
declare(strict_types=1);
namespace OC\Files\Template;
use OC\AppFramework\Bootstrap\Coordinator;
use OC\Files\Cache\Scanner;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\Folder;
use OCP\Files\File;
use OCP\Files\GenericFileException;
use OCP\Files\IRootFolder;
use OCP\Files\Node;
use OCP\Files\NotFoundException;
use OCP\Files\NotPermittedException;
use OCP\Files\Template\FileCreatedFromTemplateEvent;
use OCP\Files\Template\ICustomTemplateProvider;
use OCP\Files\Template\ITemplateManager;
use OCP\Files\Template\Template;
use OCP\Files\Template\TemplateFileCreator;
use OCP\IConfig;
use OCP\IPreview;
use OCP\IServerContainer;
use OCP\IUserSession;
use OCP\L10N\IFactory;
use Psr\Log\LoggerInterface;
class TemplateManager implements ITemplateManager {
private $registeredTypes = [];
private $types = [];
/** @var array|null */
private $providers = null;
private $serverContainer;
private $eventDispatcher;
private $rootFolder;
private $previewManager;
private $config;
private $l10n;
private $logger;
private $userId;
private $l10nFactory;
/** @var Coordinator */
private $bootstrapCoordinator;
public function __construct(
IServerContainer $serverContainer,
IEventDispatcher $eventDispatcher,
Coordinator $coordinator,
IRootFolder $rootFolder,
IUserSession $userSession,
IPreview $previewManager,
IConfig $config,
IFactory $l10nFactory,
LoggerInterface $logger
) {
$this->serverContainer = $serverContainer;
$this->eventDispatcher = $eventDispatcher;
$this->bootstrapCoordinator = $coordinator;
$this->rootFolder = $rootFolder;
$this->previewManager = $previewManager;
$this->config = $config;
$this->l10nFactory = $l10nFactory;
$this->l10n = $l10nFactory->get('lib');
$this->logger = $logger;
$user = $userSession->getUser();
$this->userId = $user ? $user->getUID() : null;
}
public function registerTemplateFileCreator(callable $callback): void {
$this->registeredTypes[] = $callback;
}
public function getRegisteredProviders(): array {
if ($this->providers !== null) {
return $this->providers;
}
$context = $this->bootstrapCoordinator->getRegistrationContext();
$this->providers = [];
foreach ($context->getTemplateProviders() as $provider) {
$this->providers[$provider['class']] = $this->serverContainer->get($provider['class']);
}
return $this->providers;
}
public function getTypes(): array {
foreach ($this->registeredTypes as $registeredType) {
$this->types[] = $registeredType();
}
return $this->types;
}
public function listCreators(): array {
$types = $this->getTypes();
usort($types, function (TemplateFileCreator $a, TemplateFileCreator $b) {
return $a->getOrder() - $b->getOrder();
});
return $types;
}
public function listTemplates(): array {
return array_map(function (TemplateFileCreator $entry) {
return array_merge($entry->jsonSerialize(), [
'templates' => $this->getTemplateFiles($entry)
]);
}, $this->listCreators());
}
/**
* @param string $filePath
* @param string $templateId
* @return array
* @throws GenericFileException
*/
public function createFromTemplate(string $filePath, string $templateId = '', string $templateType = 'user'): array {
$userFolder = $this->rootFolder->getUserFolder($this->userId);
try {
$userFolder->get($filePath);
throw new GenericFileException($this->l10n->t('File already exists'));
} catch (NotFoundException $e) {
}
try {
$targetFile = $userFolder->newFile($filePath);
if ($templateType === 'user' && $templateId !== '') {
$template = $userFolder->get($templateId);
$template->copy($targetFile->getPath());
} else {
$matchingProvider = array_filter($this->getRegisteredProviders(), function (ICustomTemplateProvider $provider) use ($templateType) {
return $templateType === get_class($provider);
});
$provider = array_shift($matchingProvider);
if ($provider) {
$template = $provider->getCustomTemplate($templateId);
$template->copy($targetFile->getPath());
}
}
$this->eventDispatcher->dispatchTyped(new FileCreatedFromTemplateEvent($template, $targetFile));
return $this->formatFile($userFolder->get($filePath));
} catch (\Exception $e) {
$this->logger->error($e->getMessage(), ['exception' => $e]);
throw new GenericFileException($this->l10n->t('Failed to create file from template'));
}
}
/**
* @return Folder
* @throws \OCP\Files\NotFoundException
* @throws \OCP\Files\NotPermittedException
* @throws \OC\User\NoUserException
*/
private function getTemplateFolder(): Node {
if ($this->getTemplatePath() !== '') {
return $this->rootFolder->getUserFolder($this->userId)->get($this->getTemplatePath());
}
throw new NotFoundException();
}
private function getTemplateFiles(TemplateFileCreator $type): array {
$templates = [];
foreach ($this->getRegisteredProviders() as $provider) {
foreach ($type->getMimetypes() as $mimetype) {
foreach ($provider->getCustomTemplates($mimetype) as $template) {
$templates[] = $template;
}
}
}
try {
$userTemplateFolder = $this->getTemplateFolder();
} catch (\Exception $e) {
return $templates;
}
foreach ($type->getMimetypes() as $mimetype) {
foreach ($userTemplateFolder->searchByMime($mimetype) as $templateFile) {
$template = new Template(
'user',
$this->rootFolder->getUserFolder($this->userId)->getRelativePath($templateFile->getPath()),
$templateFile
);
$template->setHasPreview($this->previewManager->isAvailable($templateFile));
$templates[] = $template;
}
}
return $templates;
}
/**
* @param Node|File $file
* @return array
* @throws NotFoundException
* @throws \OCP\Files\InvalidPathException
*/
private function formatFile(Node $file): array {
return [
'basename' => $file->getName(),
'etag' => $file->getEtag(),
'fileid' => $file->getId(),
'filename' => $this->rootFolder->getUserFolder($this->userId)->getRelativePath($file->getPath()),
'lastmod' => $file->getMTime(),
'mime' => $file->getMimetype(),
'size' => $file->getSize(),
'type' => $file->getType(),
'hasPreview' => $this->previewManager->isAvailable($file)
];
}
public function hasTemplateDirectory(): bool {
try {
$this->getTemplateFolder();
return true;
} catch (\Exception $e) {
}
return false;
}
public function setTemplatePath(string $path): void {
$this->config->setUserValue($this->userId, 'core', 'templateDirectory', $path);
}
public function getTemplatePath(): string {
return $this->config->getUserValue($this->userId, 'core', 'templateDirectory', '');
}
public function initializeTemplateDirectory(string $path = null, string $userId = null, $copyTemplates = true): string {
if ($userId !== null) {
$this->userId = $userId;
}
$defaultSkeletonDirectory = \OC::$SERVERROOT . '/core/skeleton';
$defaultTemplateDirectory = \OC::$SERVERROOT . '/core/skeleton/Templates';
$skeletonPath = $this->config->getSystemValue('skeletondirectory', $defaultSkeletonDirectory);
$skeletonTemplatePath = $this->config->getSystemValue('templatedirectory', $defaultTemplateDirectory);
$isDefaultSkeleton = $skeletonPath === $defaultSkeletonDirectory;
$isDefaultTemplates = $skeletonTemplatePath === $defaultTemplateDirectory;
$userLang = $this->l10nFactory->getUserLanguage();
try {
$l10n = $this->l10nFactory->get('lib', $userLang);
$userFolder = $this->rootFolder->getUserFolder($this->userId);
$userTemplatePath = $path ?? $l10n->t('Templates') . '/';
// Initial user setup without a provided path
if ($path === null) {
// All locations are default so we just need to rename the directory to the users language
if ($isDefaultSkeleton && $isDefaultTemplates) {
if (!$userFolder->nodeExists('Templates')) {
return '';
}
$newPath = $userFolder->getPath() . '/' . $userTemplatePath;
if ($newPath !== $userFolder->get('Templates')->getPath()) {
$userFolder->get('Templates')->move($newPath);
}
$this->setTemplatePath($userTemplatePath);
return $userTemplatePath;
}
if ($isDefaultSkeleton && !empty($skeletonTemplatePath) && !$isDefaultTemplates && $userFolder->nodeExists('Templates')) {
$shippedSkeletonTemplates = $userFolder->get('Templates');
$shippedSkeletonTemplates->delete();
}
}
try {
$folder = $userFolder->newFolder($userTemplatePath);
} catch (NotPermittedException $e) {
$folder = $userFolder->get($userTemplatePath);
}
$folderIsEmpty = count($folder->getDirectoryListing()) === 0;
if (!$copyTemplates) {
$this->setTemplatePath($userTemplatePath);
return $userTemplatePath;
}
if (!$isDefaultTemplates && $folderIsEmpty) {
$localizedSkeletonTemplatePath = $this->getLocalizedTemplatePath($skeletonTemplatePath, $userLang);
if (!empty($localizedSkeletonTemplatePath) && file_exists($localizedSkeletonTemplatePath)) {
\OC_Util::copyr($localizedSkeletonTemplatePath, $folder);
$userFolder->getStorage()->getScanner()->scan($userTemplatePath, Scanner::SCAN_RECURSIVE);
$this->setTemplatePath($userTemplatePath);
return $userTemplatePath;
}
}
if ($path !== null && $isDefaultSkeleton && $isDefaultTemplates && $folderIsEmpty) {
$localizedSkeletonPath = $this->getLocalizedTemplatePath($skeletonPath . '/Templates', $userLang);
if (!empty($localizedSkeletonPath) && file_exists($localizedSkeletonPath)) {
\OC_Util::copyr($localizedSkeletonPath, $folder);
$userFolder->getStorage()->getScanner()->scan($userTemplatePath, Scanner::SCAN_RECURSIVE);
$this->setTemplatePath($userTemplatePath);
return $userTemplatePath;
}
}
$this->setTemplatePath($path ?? '');
return $this->getTemplatePath();
} catch (\Throwable $e) {
$this->logger->error('Failed to initialize templates directory to user language ' . $userLang . ' for ' . $userId, ['app' => 'files_templates', 'exception' => $e]);
}
$this->setTemplatePath('');
return $this->getTemplatePath();
}
private function getLocalizedTemplatePath(string $skeletonTemplatePath, string $userLang) {
$localizedSkeletonTemplatePath = str_replace('{lang}', $userLang, $skeletonTemplatePath);
if (!file_exists($localizedSkeletonTemplatePath)) {
$dialectStart = strpos($userLang, '_');
if ($dialectStart !== false) {
$localizedSkeletonTemplatePath = str_replace('{lang}', substr($userLang, 0, $dialectStart), $skeletonTemplatePath);
}
if ($dialectStart === false || !file_exists($localizedSkeletonTemplatePath)) {
$localizedSkeletonTemplatePath = str_replace('{lang}', 'default', $skeletonTemplatePath);
}
}
return $localizedSkeletonTemplatePath;
}
}

View File

@ -95,6 +95,7 @@ use OC\Files\Node\HookConnector;
use OC\Files\Node\LazyRoot;
use OC\Files\Node\Root;
use OC\Files\Storage\StorageFactory;
use OC\Files\Template\TemplateManager;
use OC\Files\Type\Loader;
use OC\Files\View;
use OC\FullTextSearch\FullTextSearchManager;
@ -166,6 +167,7 @@ use OCP\Files\IRootFolder;
use OCP\Files\Mount\IMountManager;
use OCP\Files\NotFoundException;
use OCP\Files\Storage\IStorageFactory;
use OCP\Files\Template\ITemplateManager;
use OCP\FullTextSearch\IFullTextSearchManager;
use OCP\GlobalScale\IConfig;
use OCP\Group\Events\BeforeGroupCreatedEvent;
@ -289,6 +291,7 @@ class Server extends ServerContainer implements IServerContainer {
$this->registerDeprecatedAlias('ContactsManager', \OCP\Contacts\IManager::class);
$this->registerAlias(\OCP\DirectEditing\IManager::class, \OC\DirectEditing\Manager::class);
$this->registerAlias(ITemplateManager::class, TemplateManager::class);
$this->registerAlias(IActionFactory::class, ActionFactory::class);

View File

@ -66,11 +66,13 @@
use bantu\IniGetWrapper\IniGetWrapper;
use OC\AppFramework\Http\Request;
use OC\Files\Storage\LocalRootStorage;
use OCP\Files\Template\ITemplateManager;
use OCP\IConfig;
use OCP\IGroupManager;
use OCP\ILogger;
use OCP\IUser;
use OCP\IUserSession;
use Psr\Log\LoggerInterface;
class OC_Util {
public static $scripts = [];
@ -411,6 +413,9 @@ class OC_Util {
* @suppress PhanDeprecatedFunction
*/
public static function copySkeleton($userId, \OCP\Files\Folder $userDirectory) {
/** @var LoggerInterface $logger */
$logger = \OC::$server->get(LoggerInterface::class);
$plainSkeletonDirectory = \OC::$server->getConfig()->getSystemValue('skeletondirectory', \OC::$SERVERROOT . '/core/skeleton');
$userLang = \OC::$server->getL10NFactory()->findLanguage();
$skeletonDirectory = str_replace('{lang}', $userLang, $plainSkeletonDirectory);
@ -439,14 +444,14 @@ class OC_Util {
}
if (!empty($skeletonDirectory)) {
\OCP\Util::writeLog(
'files_skeleton',
'copying skeleton for '.$userId.' from '.$skeletonDirectory.' to '.$userDirectory->getFullPath('/'),
ILogger::DEBUG
);
$logger->debug('copying skeleton for '.$userId.' from '.$skeletonDirectory.' to '.$userDirectory->getFullPath('/'), ['app' => 'files_skeleton']);
self::copyr($skeletonDirectory, $userDirectory);
// update the file cache
$userDirectory->getStorage()->getScanner()->scan('', \OC\Files\Cache\Scanner::SCAN_RECURSIVE);
/** @var ITemplateManager $templateManager */
$templateManager = \OC::$server->get(ITemplateManager::class);
$templateManager->initializeTemplateDirectory(null, $userId);
}
}

View File

@ -31,6 +31,7 @@ namespace OCP\AppFramework\Bootstrap;
use OCP\AppFramework\IAppContainer;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\Template\ICustomTemplateProvider;
use OCP\IContainer;
/**
@ -196,4 +197,14 @@ interface IRegistrationContext {
* @since 21.0.0
*/
public function registerWellKnownHandler(string $class): void;
/**
* Register a custom template provider class that is able to inject custom templates
* in addition to the user defined ones
*
* @param string $providerClass
* @psalm-param class-string<ICustomTemplateProvider> $providerClass
* @since 21.0.0
*/
public function registerTemplateProvider(string $providerClass): void;
}

View File

@ -0,0 +1,64 @@
<?php
/*
* @copyright Copyright (c) 2021 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/>.
*
*/
declare(strict_types=1);
namespace OCP\Files\Template;
use OCP\EventDispatcher\Event;
use OCP\Files\File;
/**
* @since 21.0.0
*/
class FileCreatedFromTemplateEvent extends Event {
private $template;
private $target;
/**
* @param File|null $template
* @param File $target
* @since 21.0.0
*/
public function __construct(?File $template, File $target) {
$this->template = $template;
$this->target = $target;
}
/**
* @return File|null
* @since 21.0.0
*/
public function getTemplate(): ?File {
return $this->template;
}
/**
* @return File
* @since 21.0.0
*/
public function getTarget(): File {
return $this->target;
}
}

View File

@ -0,0 +1,51 @@
<?php
/*
* @copyright Copyright (c) 2021 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/>.
*
*/
declare(strict_types=1);
namespace OCP\Files\Template;
use OCP\Files\File;
/**
* @since 21.0.0
*/
interface ICustomTemplateProvider {
/**
* Return a list of additional templates that the template provider is offering
*
* @return File[]
* @since 21.0.0
*/
public function getCustomTemplates(string $mimetype): array;
/**
* Return the file for a given template id
*
* @param string $template identifier of the template
* @return File
* @since 21.0.0
*/
public function getCustomTemplate(string $template): File;
}

View File

@ -0,0 +1,94 @@
<?php
/**
* @copyright Copyright (c) 2021 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/>.
*
*/
declare(strict_types=1);
namespace OCP\Files\Template;
use OCP\Files\GenericFileException;
/**
* @since 21.0.0
*/
interface ITemplateManager {
/**
* Register a template type support
*
* @param callable(): TemplateFileCreator $callback A callback which returns the TemplateFileCreator instance to register
* @since 21.0.0
*/
public function registerTemplateFileCreator(callable $callback): void;
/**
* Get a list of available file creators
*
* @return array
* @since 21.0.0
*/
public function listCreators(): array;
/**
* Get a list of available file creators and their offered templates
*
* @return array
* @since 21.0.0
*/
public function listTemplates(): array;
/**
* @return bool
* @since 21.0.0
*/
public function hasTemplateDirectory(): bool;
/**
* @param string $path
* @return void
* @since 21.0.0
*/
public function setTemplatePath(string $path): void;
/**
* @return string
* @since 21.0.0
*/
public function getTemplatePath(): string;
/**
* @param string|null $path
* @param string|null $userId
* @since 21.0.0
*/
public function initializeTemplateDirectory(string $path = null, string $userId = null, $copyTemplates = true): string;
/**
* @param string $filePath
* @param string $templateId
* @return array
* @throws GenericFileException
* @since 21.0.0
*/
public function createFromTemplate(string $filePath, string $templateId = '', string $templateType = 'user'): array;
}

View File

@ -0,0 +1,89 @@
<?php
/*
* @copyright Copyright (c) 2021 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/>.
*
*/
declare(strict_types=1);
namespace OCP\Files\Template;
use OCP\Files\File;
/**
* @since 21.0.0
*/
final class Template implements \JsonSerializable {
/** @var string */
private $templateType;
/** @var string */
private $templateId;
/** @var File */
private $file;
/** @var bool */
private $hasPreview = false;
/** @var string|null */
private $previewUrl = null;
/**
* @since 21.0.0
*/
public function __construct(string $templateType, string $templateId, File $file) {
$this->templateType = $templateType;
$this->templateId = $templateId;
$this->file = $file;
}
/**
* @since 21.0.0
*/
public function setCustomPreviewUrl(string $previewUrl): void {
$this->previewUrl = $previewUrl;
}
/**
* @since 21.0.0
*/
public function setHasPreview(bool $hasPreview): void {
$this->hasPreview = $hasPreview;
}
/**
* @since 21.0.0
*/
public function jsonSerialize() {
return [
'templateType' => $this->templateType,
'templateId' => $this->templateId,
'basename' => $this->file->getName(),
'etag' => $this->file->getEtag(),
'fileid' => $this->file->getId(),
'filename' => $this->templateId,
'lastmod' => $this->file->getMTime(),
'mime' => $this->file->getMimetype(),
'size' => $this->file->getSize(),
'type' => $this->file->getType(),
'hasPreview' => $this->hasPreview,
'previewUrl' => $this->previewUrl
];
}
}

View File

@ -0,0 +1,118 @@
<?php
/*
* @copyright Copyright (c) 2021 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/>.
*
*/
declare(strict_types=1);
namespace OCP\Files\Template;
/**
* @since 21.0.0
*/
final class TemplateFileCreator implements \JsonSerializable {
protected $appId;
protected $mimetypes = [];
protected $actionName;
protected $fileExtension;
protected $iconClass;
protected $ratio = null;
protected $order = 100;
/**
* @since 21.0.0
*/
public function __construct(
string $appId, string $actionName, string $fileExtension
) {
$this->appId = $appId;
$this->actionName = $actionName;
$this->fileExtension = $fileExtension;
}
/**
* @since 21.0.0
*/
public function getAppId(): string {
return $this->appId;
}
/**
* @since 21.0.0
*/
public function setIconClass(string $iconClass): TemplateFileCreator {
$this->iconClass = $iconClass;
return $this;
}
/**
* @since 21.0.0
*/
public function addMimetype(string $mimetype): TemplateFileCreator {
$this->mimetypes[] = $mimetype;
return $this;
}
/**
* @since 21.0.0
*/
public function getMimetypes(): array {
return $this->mimetypes;
}
/**
* @since 21.0.0
*/
public function setRatio(float $ratio): TemplateFileCreator {
$this->ratio = $ratio;
return $this;
}
/**
* @param int $order order in which the create action shall be listed
* @since 21.0.0
*/
public function setOrder(int $order): TemplateFileCreator {
$this->order = $order;
return $this;
}
/**
* @since 21.0.0
*/
public function getOrder(): int {
return $this->order;
}
/**
* @since 21.0.0
*/
public function jsonSerialize() {
return [
'app' => $this->appId,
'label' => $this->actionName,
'extension' => $this->fileExtension,
'iconClass' => $this->iconClass,
'mimetypes' => $this->mimetypes,
'ratio' => $this->ratio
];
}
}