Compare commits

...

3 Commits

39 changed files with 768 additions and 214 deletions

View File

@ -501,6 +501,10 @@ form {
}
}
.modal {
z-index: 3000;
}
.modal-dialog-large {
max-width: 80%;
}

View File

@ -2,7 +2,7 @@
<div>
<div class="row">
<div class="col">
<Files />
<Files v-bind:context="context" />
</div>
</div>
</div>
@ -16,6 +16,13 @@ import Files from './Files'
export default {
name: 'FileManager',
props: {
context: {
type: String,
required: true,
default: 'crud',
}
},
components: {
Files
}

View File

@ -1,9 +1,6 @@
<template>
<div>
<nav aria-label="breadcrumb bg-light">
<div class="float-right">
</div>
<ol class="breadcrumb mb-0 float-right file-manager-views">
<li class="breadcrumb-item">
<span class="fa fa-grip-horizontal" v-on:click="setView('grid')"></span>
@ -13,7 +10,7 @@
</li>
</ol>
<ol class="breadcrumb mb-0 float-right file-manager-actions">
<ol v-if="['crud'].indexOf(context) > -1" class="breadcrumb mb-0 float-right file-manager-actions">
<li class="breadcrumb-item">
<span class="fa fa-upload text-primary" v-bind:data-modal="generateUploadLink(directory)"></span>
<span class="fa fa-folder-plus text-primary" v-bind:data-modal="generateNewDirectoryLink(directory)"></span>
@ -44,7 +41,7 @@
<div v-for="item in directories" class="card mt-3 ml-3 mb-3 border-0">
<div class="card-body p-2">
<div class="card-text" v-on:dblclick="setDirectory(item.path)" v-bind:data-modal="generateInfoLink(item, true)">
<div class="card-text" v-on:dblclick="setDirectory(item.path)" v-bind:data-modal="generateInfoLink(item, true, context)">
<div class="text-center">
<div class="display-4 text-warning">
<span class="fa fa-folder"></span>
@ -63,7 +60,7 @@
</div>
</div>
</div>
<div v-for="item in files" class="card mt-3 ml-3 mb-3 border-0" v-bind:data-modal="generateInfoLink(item)">
<div v-for="item in files" class="card mt-3 ml-3 mb-3 border-0" v-bind:data-modal="generateInfoLink(item, null, context)">
<div class="card-body p-2">
<div class="card-text">
<div class="text-center">
@ -97,7 +94,7 @@
</td>
</tr>
<tr v-for="item in directories" v-on:dblclick="setDirectory(item.path)" v-bind:data-modal="generateInfoLink(item, true)">
<tr v-for="item in directories" v-on:dblclick="setDirectory(item.path)" v-bind:data-modal="generateInfoLink(item, true, context)">
<td width="10">
<span class="fa fa-folder text-warning"></span>
</td>
@ -115,7 +112,7 @@
<td width="10">
<FileIcon v-bind:mime="item.mime" />
</td>
<td v-bind:data-modal="generateInfoLink(item)">
<td v-bind:data-modal="generateInfoLink(item, null, context)">
<div v-if="item.locked" class="float-right">
<span class="btn btn-sm btn-light">
<span class="fa fa-lock"></span>
@ -178,6 +175,12 @@ export default {
components: {
FileIcon
},
props: {
context: {
type: String,
required: false
}
},
data () {
return {
view: 'list',
@ -197,14 +200,16 @@ export default {
localStorage.setItem('file-manager.view', view)
},
generateInfoLink (item, directory) {
generateInfoLink (item, directory, context) {
if (directory) {
return Routing.generate('admin_file_manager_info', {
file: item.path
file: item.path,
context: context
})
} else {
return Routing.generate('admin_file_manager_info', {
file: item.path + '/' + item.basename
file: item.path + '/' + item.basename,
context: context
})
}
},
@ -259,7 +264,8 @@ export default {
watch: {
directory (directory) {
axios.get(Routing.generate('admin_file_manager_api_directory', {
directory: this.directory
directory: this.directory,
context: this.context
}))
.then((response) => {
this.buildBreadcrum(response.data.breadcrumb)

View File

@ -1,4 +1,53 @@
const $ = require('jquery')
const Vue = require('vue').default
const FileManager = require('../components/file-manager/FileManager').default
const createModal = function (url) {
let container = $('#file-manager-modal-container')
const body = $('body')
if (!container.length) {
container = $('<div id="file-manager-modal-container" class="modal">')
body.append(container)
}
container.html(`
<div class="modal-dialog modal-dialog-large">
<div class="modal-content">
<div class="modal-body">
<div id="file-manager-modal-content">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
</div>
</div>
</div>
`)
$(container).modal('show')
return $(container)
}
const fileManagerBrowser = function (callback) {
const container = createModal()
$('body').on('click', '#file-manager-insert', (e) => {
callback($(e.target).attr('data-value'), {})
$('#modal-container').modal('hide')
container.modal('hide')
})
new Vue({
el: '#file-manager-modal-content',
template: '<FileManager context="tinymce" />',
components: {
FileManager
}
})
}
if (typeof tinymce !== 'undefined') {
tinymce.murph = tinymce.murph || {}
@ -13,6 +62,8 @@ if (typeof tinymce !== 'undefined') {
spellchecker_dialog: true,
tinycomments_mode: 'embedded',
convert_urls: false,
file_picker_callback: fileManagerBrowser,
file_picker_types: 'image',
init_instance_callback: function (editor) {
editor.on('SetContent', () => {
tinymce.triggerSave(false, true)
@ -29,7 +80,7 @@ if (typeof tinymce !== 'undefined') {
tinymce.murph.modes.default = tinymce.murph.modes.default || {
plugins: 'print preview importcss searchreplace visualblocks visualchars fullscreen template table charmap hr pagebreak nonbreaking toc insertdatetime advlist lists wordcount textpattern noneditable help charmap quickbars link image code autoresize',
menubar: 'file edit view insert format tools table tc help',
toolbar: 'undo redo | bold italic underline strikethrough | link image | fontselect fontsizeselect formatselect | alignleft aligncenter alignright alignjustify | outdent indent | numlist bullist checklist | forecolor backcolor casechange permanentpen formatpainter removeformat | pagebreak | charmap | fullscreen preview',
toolbar: 'undo redo | bold italic underline strikethrough | link image | fontselect fontsizeselect formatselect | alignleft aligncenter alignright alignjustify | outdent indent | numlist bullist checklist | forecolor backcolor casechange permanentpen formatpainter removeformat | pagebreak | charmap | fullscreen preview',
quickbars_selection_toolbar: 'bold italic | quicklink h2 h3 blockquote quickimage quicktable',
contextmenu: 'link image imagetools table configurepermanentpen'
}

View File

@ -1,4 +1,3 @@
// file-manager
const Vue = require('vue').default
const FileManager = require('../components/file-manager/FileManager').default

View File

@ -1,5 +1,28 @@
const $ = require('jquery')
const openModal = function (url) {
let container = $('#modal-container')
const body = $('body')
if (!container.length) {
container = $('<div id="modal-container" class="modal">')
body.append(container)
}
const loader = $('<div style="position: absolute; top: 25vh; left: 50vw; z-index: 2000">')
loader.html('<div class="spinner-border text-primary" role="status"><span class="sr-only">Loading...</span></div>')
body.append(loader)
container.html('')
$(container).modal('show')
container.load(url, function () {
loader.remove()
})
}
module.exports = function () {
let click = 0
@ -18,32 +41,13 @@ module.exports = function () {
click = 0
let container = $('#modal-container')
const body = $('body')
if (!container.length) {
container = $('<div id="modal-container" class="modal">')
body.append(container)
}
const loader = $('<div style="position: absolute; top: 25vh; left: 50vw; z-index: 2000">')
loader.html('<div class="spinner-border text-primary" role="status"><span class="sr-only">Loading...</span></div>')
body.append(loader)
container.html('')
let url = $(e.target).attr('data-modal')
if (!url) {
url = $(e.target).parents('*[data-modal]').first().attr('data-modal')
}
$(container).modal('show')
container.load(url, function () {
loader.remove()
})
openModal(url)
}, 250)
})
@ -51,6 +55,6 @@ module.exports = function () {
const dataModal = urlParams.get('data-modal')
if (dataModal) {
$('*[data-modal="' + dataModal + '"]').first().click()
openModal(dataModal)
}
}

View File

@ -10,25 +10,33 @@ core:
templates:
- {name: "Default", file: "page/simple/default.html.twig"}
file_manager:
# mimes:
# - image/png
# - image/jpg
# - image/jpeg
# - image/gif
# - application/pdf
# - application/ogg
# - video/mp4
# - application/zip
# - multipart/x-zip
# - application/rar
# - application/x-rar-compressed
# - application/x-zip-compressed
# - application/tar
# - application/x-tar
# - text/plain
# - text/x-asm
# - application/octet-stream
# path: "%kernel.project_dir%/public/uploads"
# path_uri: "/uploads"
# path_locked:
# - "%kernel.project_dir%/public/uploads"
# mimes:
# - image/png
# - image/jpg
# - image/jpeg
# - image/gif
# - image/svg+xml
# - video/mp4
# - audio/mpeg3
# - audio/x-mpeg-3
# - multipart/x-zip
# - multipart/x-gzip
# - application/pdf
# - application/ogg
# - application/zip
# - application/rar
# - application/x-rar-compressed
# - application/x-zip-compressed
# - application/tar
# - application/x-tar
# - application/x-bzip
# - application/x-bzip2
# - application/x-gzip
# - application/octet-stream
# - application/msword
# - text/plain
# - text/css
# path: "%kernel.project_dir%/public/uploads"
# path_uri: "/uploads"
# path_locked:
# - "%kernel.project_dir%/public/uploads"

View File

@ -16,6 +16,14 @@ abstract class AdminController extends AbstractController
$this->coreParameters = $parameters->get('core');
}
/**
* @Route("/_ping", name="_ping")
*/
public function ping()
{
return $this->json(true);
}
/**
* {@inheritdoc}
*/
@ -29,12 +37,4 @@ abstract class AdminController extends AbstractController
}
abstract protected function getSection(): string;
/**
* @Route("/_ping", name="_ping")
*/
public function ping()
{
return $this->json(true);
}
}

View File

@ -61,7 +61,7 @@ abstract class CrudController extends AdminController
$form->handleRequest($request);
if ($form->isValid()) {
if ($beforeCreate !== null) {
if (null !== $beforeCreate) {
call_user_func_array($beforeCreate, [$entity, $form, $request]);
}
@ -104,7 +104,7 @@ abstract class CrudController extends AdminController
$form->handleRequest($request);
if ($form->isValid()) {
if ($beforeUpdate !== null) {
if (null !== $beforeUpdate) {
call_user_func_array($beforeUpdate, [$entity, $form, $request]);
}
@ -149,7 +149,7 @@ abstract class CrudController extends AdminController
foreach ($pager as $key => $entity) {
if (isset($items[$key + 1])) {
$entity->$setter($items[$key + 1] + $orderStart);
$entity->{$setter}($items[$key + 1] + $orderStart);
$entityManager->update($entity);
}
@ -192,7 +192,7 @@ abstract class CrudController extends AdminController
$query->useFilters($this->filters);
if ($target === 'selection') {
if ('selection' === $target) {
$isSelection = true;
$pager = $query->paginate($page, $configuration->getMaxPerPage($context));
} else {
@ -216,7 +216,7 @@ abstract class CrudController extends AdminController
$configuration = $this->getConfiguration();
if ($this->isCsrfTokenValid('delete'.$entity->getId(), $request->request->get('_token'))) {
if ($beforeDelete !== null) {
if (null !== $beforeDelete) {
call_user_func($beforeDelete, $entity);
}

View File

@ -6,7 +6,9 @@ use App\Core\Controller\Admin\AdminController;
use App\Core\FileManager\FsFileManager;
use App\Core\Form\FileManager\DirectoryCreateType;
use App\Core\Form\FileManager\DirectoryRenameType;
use App\Core\Form\FileManager\FileInformationType;
use App\Core\Form\FileManager\FileUploadType;
use App\Core\Manager\EntityManager;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
@ -35,22 +37,53 @@ class FileManagerAdminController extends AdminController
}
/**
* @Route("/info", name="admin_file_manager_info", options={"expose"=true})
* @Route("/info/{tab}/{context}", name="admin_file_manager_info", options={"expose"=true})
*/
public function info(FsFileManager $manager, Request $request): Response
{
$info = $manager->info($request->query->get('file'));
public function info(
FsFileManager $manager,
Request $request,
EntityManager $entityManager,
string $context = 'crud',
string $tab = 'information'
): Response {
$splInfo = $manager->getSplInfo($request->query->get('file'));
if (!$info) {
if (!$splInfo) {
throw $this->createNotFoundException();
}
$path = $manager->getPathUri().'/'.$info->getRelativePathname();
$fileInfo = $manager->getFileInformation($request->query->get('file'));
$path = $manager->getPathUri().'/'.$splInfo->getRelativePathname();
$form = $this->createForm(FileInformationType::class, $fileInfo);
if ($request->isMethod('POST')) {
$form->handleRequest($request);
if ($form->isValid()) {
$entityManager->update($fileInfo);
$this->addFlash('success', 'The data has been saved.');
} else {
$this->addFlash('warning', 'The form is not valid.');
}
return $this->redirectToRoute('admin_file_manager_index', [
'data-modal' => $this->generateUrl('admin_file_manager_info', [
'file' => $request->query->get('file'),
'tab' => 'attributes',
]),
'path' => $splInfo->getRelativePath(),
]);
}
return $this->render('@Core/file_manager/info.html.twig', [
'info' => $info,
'splInfo' => $splInfo,
'path' => $path,
'isLocked' => $manager->isLocked($info->getRelativePathname()),
'isLocked' => $manager->isLocked($splInfo->getRelativePathname()),
'tab' => $tab,
'form' => $form->createView(),
'context' => $context,
]);
}
@ -59,13 +92,13 @@ class FileManagerAdminController extends AdminController
*/
public function directoryNew(FsFileManager $manager, Request $request): Response
{
$info = $manager->info($request->query->get('file'));
$splInfo = $manager->getSplInfo($request->query->get('file'));
if (!$info) {
if (!$splInfo) {
throw $this->createNotFoundException();
}
if (!$info->isDir()) {
if (!$splInfo->isDir()) {
throw $this->createNotFoundException();
}
@ -92,7 +125,7 @@ class FileManagerAdminController extends AdminController
}
return $this->redirectToRoute('admin_file_manager_index', [
'path' => $info->getRelativePath(),
'path' => $splInfo->getRelativePath(),
]);
}
@ -108,13 +141,13 @@ class FileManagerAdminController extends AdminController
*/
public function directoryRename(FsFileManager $manager, Request $request): Response
{
$info = $manager->info($request->query->get('file'));
$splInfo = $manager->getSplInfo($request->query->get('file'));
if (!$info) {
if (!$splInfo) {
throw $this->createNotFoundException();
}
if (!$info->isDir()) {
if (!$splInfo->isDir()) {
throw $this->createNotFoundException();
}
@ -142,7 +175,7 @@ class FileManagerAdminController extends AdminController
}
return $this->redirectToRoute('admin_file_manager_index', [
'path' => $info->getRelativePath(),
'path' => $splInfo->getRelativePath(),
]);
}
@ -158,13 +191,13 @@ class FileManagerAdminController extends AdminController
*/
public function upload(FsFileManager $manager, Request $request): Response
{
$info = $manager->info($request->query->get('file'));
$splInfo = $manager->getSplInfo($request->query->get('file'));
if (!$info) {
if (!$splInfo) {
throw $this->createNotFoundException();
}
if (!$info->isDir()) {
if (!$splInfo->isDir()) {
throw $this->createAccessDeniedException();
}
@ -189,7 +222,7 @@ class FileManagerAdminController extends AdminController
}
return $this->redirectToRoute('admin_file_manager_index', [
'path' => $info->getRelativePathname(),
'path' => $splInfo->getRelativePathname(),
]);
}
@ -206,9 +239,9 @@ class FileManagerAdminController extends AdminController
public function delete(FsFileManager $manager, Request $request): Response
{
$path = $request->request->get('file');
$info = $manager->info($request->request->get('file'));
$splInfo = $manager->getSplInfo($request->request->get('file'));
if (!$info) {
if (!$splInfo) {
throw $this->createNotFoundException();
}
@ -221,7 +254,7 @@ class FileManagerAdminController extends AdminController
}
return $this->redirectToRoute('admin_file_manager_index', [
'path' => $info->getRelativePath(),
'path' => $splInfo->getRelativePath(),
]);
}

View File

@ -13,6 +13,7 @@ use App\Core\Form\Site\NodeMoveType;
use App\Core\Form\Site\NodeType as EntityType;
use App\Core\Manager\EntityManager;
use App\Core\Repository\Site\NodeRepository;
use App\Core\Site\ControllerLocator;
use App\Core\Site\PageLocator;
use App\Core\Sitemap\SitemapBuilder;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
@ -20,7 +21,6 @@ use Symfony\Component\Form\FormError;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use App\Core\Site\ControllerLocator;
/**
* @Route("/admin/site/node")
@ -278,7 +278,8 @@ class NodeAdminController extends AdminController
$entity
->setPage($page)
->setAliasNode(null);
->setAliasNode(null)
;
} elseif ('existing' === $pageAction) {
if ($pageEntity) {
$entity->setPage($pageEntity);
@ -291,7 +292,8 @@ class NodeAdminController extends AdminController
} elseif ('none' === $pageAction) {
$entity
->setPage(null)
->setAliasNode(null);
->setAliasNode(null)
;
}
}
}

View File

@ -5,18 +5,16 @@ namespace App\Core\Controller\Site;
use App\Core\Controller\Admin\Crud\CrudController;
use App\Core\Crud\CrudConfiguration;
use App\Core\Crud\Field;
use App\Core\Entity\EntityInterface;
use App\Core\Manager\EntityManager;
use App\Core\Entity\Site\Page\Page as Entity;
use App\Core\Form\Site\Page\PageType as Type;
use App\Core\Form\Site\Page\Filter\PageFilterType as FilterType;
use App\Core\Form\Site\Page\PageType as Type;
use App\Core\Manager\EntityManager;
use App\Core\Repository\Site\Page\PageRepositoryQuery as RepositoryQuery;
use App\Core\Site\PageLocator;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Routing\Annotation\Route;
use App\Core\Site\PageLocator;
use App\Core\Repository\Site\Page\PageRepositoryQuery;
class PageAdminController extends CrudController
{
@ -53,8 +51,7 @@ class PageAdminController extends CrudController
RepositoryQuery $repositoryQuery,
PageLocator $pageLocator,
Request $request
): Response
{
): Response {
$entity = $repositoryQuery->filterById($entity)->findOne();
$this->getConfiguration()->setFormOptions('edit', [
@ -98,12 +95,13 @@ class PageAdminController extends CrudController
])
->setField('index', 'Elements', Field\TextField::class, [
'view' => '@Core/site/page_admin/fields/nodes.html.twig',
'sort' => ['navigation', function(RepositoryQuery $query, $direction) {
'sort' => ['navigation', function (RepositoryQuery $query, $direction) {
$query
->leftJoin('.nodes', 'node')
->leftJoin('node.menu', 'menu')
->leftJoin('menu.navigation', 'navigation')
->orderBy('navigation.label', $direction);
->orderBy('navigation.label', $direction)
;
}],
])
;

View File

@ -31,7 +31,7 @@ class PageController extends AbstractController
{
$parameters = array_merge($this->getDefaultRenderParameters(), $parameters);
if ($response === null) {
if (null === $response) {
$contentType = $this->siteRequest->getNode()->getContentType();
$response = new Response(null, 200, [

View File

@ -33,7 +33,8 @@ class TreeAdminController extends AdminController
if (null === $navigation) {
$navigation = $navigationQuery->create()
->orderBy('.sortOrder')
->findOne();
->findOne()
;
}
if (null === $navigation) {

View File

@ -5,13 +5,13 @@ namespace App\Core\Controller\Task;
use App\Core\Controller\Admin\AdminController;
use App\Core\Event\Task\TaskInitEvent;
use App\Core\Event\Task\TaskRunRequestedEvent;
use SensioLabs\AnsiConverter\AnsiToHtmlConverter;
use SensioLabs\AnsiConverter\Theme\SolarizedTheme;
use Symfony\Component\Console\Output\BufferedOutput;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use SensioLabs\AnsiConverter\AnsiToHtmlConverter;
use SensioLabs\AnsiConverter\Theme\SolarizedTheme;
/**
* @Route("/admin/task")

View File

@ -14,19 +14,27 @@ class Configuration implements ConfigurationInterface
'image/jpg',
'image/jpeg',
'image/gif',
'image/svg+xml',
'video/mp4',
'audio/mpeg3',
'audio/x-mpeg-3',
'multipart/x-zip',
'multipart/x-gzip',
'application/pdf',
'application/ogg',
'video/mp4',
'application/zip',
'multipart/x-zip',
'application/rar',
'application/x-rar-compressed',
'application/x-zip-compressed',
'application/tar',
'application/x-tar',
'application/x-bzip',
'application/x-bzip2',
'application/x-gzip',
'application/octet-stream',
'application/msword',
'text/plain',
'text/x-asm',
'application/octet-stream'
'text/css',
];
$defaultLocked = [

View File

@ -0,0 +1,48 @@
<?php
namespace App\Core\Entity;
use App\Repository\Entity\FileInformationRepository;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity(repositoryClass=FileInformationRepository::class)
*/
class FileInformation implements EntityInterface
{
/**
* @ORM\Id
* @ORM\GeneratedValue(strategy="NONE")
* @ORM\Column(type="string", length=255, unique=true)
*/
protected $id;
/**
* @ORM\Column(type="text", nullable=true)
*/
protected $attributes;
public function getId(): ?string
{
return $this->id;
}
public function setId(string $id): self
{
$this->id = $id;
return $this;
}
public function getAttributes()
{
return json_decode($this->attributes, true);
}
public function setAttributes($attributes): self
{
$this->attributes = json_encode($attributes);
return $this;
}
}

View File

@ -3,7 +3,6 @@
namespace App\Core\Entity\Site\Page;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\HttpFoundation\File\File;
/**
* @ORM\Entity

View File

@ -242,7 +242,7 @@ class Page implements EntityInterface
public function setOgImage($ogImage): self
{
if ($this->ogImage !== null && $ogImage === null) {
if (null !== $this->ogImage && null === $ogImage) {
return $this;
}

View File

@ -0,0 +1,16 @@
<?php
namespace App\Core\Factory;
use App\Core\Entity\FileInformation as Entity;
class FileInformationFactory implements FactoryInterface
{
public function create(string $id): Entity
{
$entity = new Entity();
$entity->setId($id);
return $entity;
}
}

View File

@ -18,7 +18,8 @@ class NavigationSettingFactory implements FactoryInterface
$entity
->setNavigation($navigation)
->setCode($code);
->setCode($code)
;
return $entity;
}

View File

@ -2,7 +2,10 @@
namespace App\Core\FileManager;
use App\Core\Entity\FileInformation;
use App\Core\Factory\FileInformationFactory;
use App\Core\Form\FileUploadHandler;
use App\Core\Repository\FileInformationRepositoryQuery;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Finder\Finder;
@ -21,12 +24,21 @@ class FsFileManager
protected string $pathUri;
protected array $pathLocked;
protected FileUploadHandler $uploadHandler;
protected FileInformationFactory $fileInformationFactory;
protected FileInformationRepositoryQuery $fileInformationRepositoryQuery;
public function __construct(ParameterBagInterface $params, FileUploadHandler $uploadHandler)
{
public function __construct(
ParameterBagInterface $params,
FileUploadHandler $uploadHandler,
FileInformationFactory $fileInformationFactory,
FileInformationRepositoryQuery $fileInformationRepositoryQuery
) {
$config = $params->get('core')['file_manager'];
$this->uploadHandler = $uploadHandler;
$this->fileInformationFactory = $fileInformationFactory;
$this->fileInformationRepositoryQuery = $fileInformationRepositoryQuery;
$this->mimes = $config['mimes'];
$this->path = $config['path'];
$this->pathUri = $this->normalizePath($config['path_uri']);
@ -85,7 +97,7 @@ class FsFileManager
return $data;
}
public function info(string $path): ?SplFileInfo
public function getSplInfo(string $path): ?SplFileInfo
{
$path = $this->normalizePath($path);
@ -95,6 +107,7 @@ class FsFileManager
$finder = new Finder();
$finder->in($this->path)
->depth('== '.substr_count($path, '/'))
->name(basename($path))
;
@ -113,9 +126,36 @@ class FsFileManager
return null;
}
public function getFileInformation(string $path): ?FileInformation
{
$file = $this->getSplInfo($path);
if (!$file) {
return null;
}
if ($file->isDir()) {
return null;
}
$hash = hash_file('sha384', $file->getPathName());
$info = $this->fileInformationRepositoryQuery
->where('.id = :hash')
->setParameter(':hash', $hash)
->findOne()
;
if (!$info) {
$info = $this->fileInformationFactory->create($hash);
}
return $info;
}
public function createDirectory(string $name, string $path): bool
{
$file = $this->info($path);
$file = $this->getSplInfo($path);
if (!$file || $this->isLocked($path)) {
return false;
@ -135,7 +175,7 @@ class FsFileManager
public function renameDirectory(string $name, string $path): bool
{
$file = $this->info($path);
$file = $this->getSplInfo($path);
if (!$file || $this->isLocked($path)) {
return false;
@ -166,7 +206,7 @@ class FsFileManager
public function delete(string $path): bool
{
$file = $this->info($path);
$file = $this->getSplInfo($path);
if ($this->isLocked($file)) {
return false;
@ -182,7 +222,7 @@ class FsFileManager
public function isLocked($path): bool
{
$file = $this->info($path);
$file = $this->getSplInfo($path);
if (!$file) {
return false;

View File

@ -3,14 +3,11 @@
namespace App\Core\Form\FileManager;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Validator\Constraints\File;
use Symfony\Component\Validator\Constraints\All;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Validator\Constraints\Regex;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\Regex;
class DirectoryCreateType extends AbstractType
{

View File

@ -3,14 +3,11 @@
namespace App\Core\Form\FileManager;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Validator\Constraints\File;
use Symfony\Component\Validator\Constraints\All;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Validator\Constraints\Regex;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\Regex;
class DirectoryRenameType extends AbstractType
{

View File

@ -0,0 +1,49 @@
<?php
namespace App\Core\Form\FileManager;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\NotBlank;
class FileInformationAttributeType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add(
'label',
TextType::class,
[
'label' => 'Label',
'required' => true,
'attr' => [
],
'constraints' => [
new NotBlank(),
],
]
);
$builder->add(
'value',
TextType::class,
[
'label' => 'Value',
'required' => false,
'attr' => [
],
'constraints' => [
],
]
);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => null,
]);
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace App\Core\Form\FileManager;
use App\Core\Entity\FileInformation;
use App\Core\Form\Type\CollectionType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class FileInformationType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add(
'attributes',
CollectionType::class,
[
'entry_type' => FileInformationAttributeType::class,
'by_reference' => false,
'allow_add' => true,
'allow_delete' => true,
'prototype' => true,
]
);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => FileInformation::class,
]);
}
}

View File

@ -3,11 +3,11 @@
namespace App\Core\Form\FileManager;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Validator\Constraints\File;
use Symfony\Component\Validator\Constraints\All;
use Symfony\Component\Validator\Constraints\File;
class FileUploadType extends AbstractType
{

View File

@ -3,11 +3,11 @@
namespace App\Core\Form\Site;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
class NavigationAdditionalDomainType extends AbstractType
{

View File

@ -4,14 +4,13 @@ namespace App\Core\Form\Site;
use App\Core\Entity\Site\Navigation;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\Length;
use Symfony\Component\Validator\Constraints\NotBlank;
use App\Core\Form\Site\NavigationAdditionalDomainType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
class NavigationType extends AbstractType
{

View File

@ -7,13 +7,13 @@ use App\Core\Entity\Site\Page\Page;
use Doctrine\ORM\EntityRepository;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
class NodeType extends AbstractType
{
@ -181,7 +181,7 @@ class NodeType extends AbstractType
'required' => false,
'class' => Node::class,
'choice_label' => 'label',
'choices' => call_user_func(function() use ($options, $builder) {
'choices' => call_user_func(function () use ($options, $builder) {
$nodes = [];
foreach ($options['navigation']->getMenus() as $menu) {

View File

@ -4,13 +4,11 @@ namespace App\Core\Form\Site\Page;
use App\Core\Entity\Site\Page\Block;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\CallbackTransformer;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;
class CollectionBlockType extends AbstractType
{

View File

@ -48,9 +48,9 @@ class MakeRepositoryQuery extends AbstractMaker
);
$queryDetails = $generator->createClassNameDetails(
$repositoryClass,
$repositoryClass.'Query',
'Repository\\',
'Query'
''
);
$id = u($queryDetails->getShortName())

View File

@ -0,0 +1,21 @@
<?php
namespace App\Core\Repository;
use App\Core\Entity\FileInformation;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @method FileInformation|null find($id, $lockMode = null, $lockVersion = null)
* @method FileInformation|null findOneBy(array $criteria, array $orderBy = null)
* @method FileInformation[] findAll()
* @method FileInformation[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class FileInformationRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, FileInformation::class);
}
}

View File

@ -0,0 +1,14 @@
<?php
namespace App\Core\Repository;
use App\Core\Repository\FileInformationRepository as Repository;
use Knp\Component\Pager\PaginatorInterface;
class FileInformationRepositoryQuery extends RepositoryQuery
{
public function __construct(Repository $repository, PaginatorInterface $paginator)
{
parent::__construct($repository, 'f', $paginator);
}
}

View File

@ -186,3 +186,5 @@
"Rename": "Renommer"
"Rename directory": "Renommer le répertoire"
"New directory": "Nouveau répertoire"
"Preview": "Aperçu"
"Insert": "Insérer"

View File

@ -1,3 +1,5 @@
{% set tab = tab ?? 'information' %}
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
@ -9,85 +11,216 @@
</button>
</div>
<div class="modal-body">
{% if info.type == 'file' %}
<div class="form-group">
<label for="file-manager-url">{{ 'Absolute URL'|trans }}</label><br>
<input class="form-control" type="text" readonly value="{{ absolute_url(asset(path)) }}" id="file-manager-url" />
</div>
<ul class="nav nav-pills" role="tablist">
<li class="nav-item" role="presentation">
<a class="nav-link {% if tab == 'information' %}active{% endif %}" data-toggle="tab" href="#tab-fm-information">
{{ 'Information'|trans }}
</a>
</li>
<p>
<label for="file-manager-url2">{{ 'Relative URL'|trans }}</label><br>
<input class="form-control" type="text" readonly value="{{ asset(path) }}" id="file-manager-url2" />
</p>
{% endif %}
<li class="nav-item" role="presentation">
<a class="nav-link {% if tab == 'attributes' %}active{% endif %}" data-toggle="tab" href="#tab-fm-attributes">
{{ 'Attributes'|trans }}
</a>
</li>
<ul class="list-group mb-3">
{% if info.type == 'file' %}
<li class="list-group-item d-flex justify-content-between align-items-center">
{{ 'File size'|trans }}
<button class="btn btn-sm btn-light">{{ info.size|readable_filesize }}</button>
{% if splInfo.extension in ['jpeg', 'jpg', 'gif', 'png', 'svg'] %}
<li class="nav-item" role="presentation">
<a class="nav-link {% if tab == 'preview' %}active{% endif %}" data-toggle="tab" href="#tab-fm-preview">
{{ 'Preview'|trans }}
</a>
</li>
{% endif %}
<li class="list-group-item d-flex justify-content-between align-items-center">
{{ 'Creation date'|trans }}
<button class="btn btn-sm btn-light">{{ info.mTime|date('Y-m-d H:i') }}</button>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
{{ 'Modification date'|trans }}
<button class="btn btn-sm btn-light">{{ info.cTime|date('Y-m-d H:i') }}</button>
</li>
</ul>
{% if info.extension in ['jpeg', 'jpg', 'gif', 'png', 'svg'] %}
<div class="card">
<div class="card-img-top bg-tiles text-center">
<a href="{{ asset(path) }}" target="_blank">
<img src="{{ asset(path) }}" class="img-fluid">
</a>
</div>
</div>
{% endif %}
</div>
<div class="modal-footer justify-content-between">
{% if not isLocked %}
<div>
<button type="submit" form="form-file-delete" class="btn btn-danger" form="form-file-delete">
{{ 'Delete'|trans }}
</button>
<div class="tab-content pt-4">
<div class="tab-pane {% if tab == 'information' %}show active{% endif %}" id="tab-fm-information">
{% if splInfo.type == 'file' %}
<div class="form-group">
<label for="file-manager-url">{{ 'Absolute URL'|trans }}</label><br>
<input class="form-control" type="text" readonly value="{{ absolute_url(asset(path)) }}" id="file-manager-url" />
</div>
{% if info.isDir %}
<button form="form-file-delete" class="btn btn-primary" data-modal="{{ path('admin_file_manager_directory_rename', {file: info.relativePathname}) }}">
{{ 'Rename'|trans }}
</button>
<p>
<label for="file-manager-url2">{{ 'Relative URL'|trans }}</label><br>
<input class="form-control" type="text" readonly value="{{ asset(path) }}" id="file-manager-url2" />
</p>
{% endif %}
<ul class="list-group mb-3">
{% if splInfo.type == 'file' %}
<li class="list-group-item d-flex justify-content-between align-items-center">
{{ 'File size'|trans }}
<button class="btn btn-sm btn-light">{{ splInfo.size|readable_filesize }}</button>
</li>
{% endif %}
<li class="list-group-item d-flex justify-content-between align-items-center">
{{ 'Creation date'|trans }}
<button class="btn btn-sm btn-light">{{ splInfo.mTime|date('Y-m-d H:i') }}</button>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
{{ 'Modification date'|trans }}
<button class="btn btn-sm btn-light">{{ splInfo.cTime|date('Y-m-d H:i') }}</button>
</li>
</ul>
</div>
<div class="tab-pane {% if tab == 'attributes' %}show active{% endif %}" id="tab-fm-attributes">
{% if context == 'tinymce' %}
<div class="accordion mb-3" id="fm-attributes">
{% for item in form.attributes %}
<div class="card">
<div class="card-header p-0">
<span class="btn btn-link btn-block text-left" data-toggle="collapse" data-target="#fm-attribute-{{ loop.index }}">
{{ item.vars.data.label }}
</span>
</div>
<div class="collapse" data-parent="#fm-attributes" id="fm-attribute-{{ loop.index }}">
<div class="card-body">
<div>{{ 'Value'|trans }}</div>
<code>{{ item.vars.data.value }}</code>
{% set code = 'fattr://' ~ form.vars.data.id ~ '/' ~ item.vars.data.label %}
{% set code = '{{' ~ code ~ '}}' %}
<div class="mt-1">{{ 'Tag to insert in content'|trans }}</div>
<code>{{ code }}</code>
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<form method="post" action="{{ path('admin_file_manager_info', {tab: 'attributes', file: splInfo.relativePathname}) }}" id="form-fm-attributes">
<div class="accordion mb-3" data-collection="collection-fm-attributes" id="form-fm-attributes-collection">
{% for item in form.attributes %}
<div class="card" data-collection-item="{{ loop.index }}">
<div class="card-header p-0">
<span class="btn btn-link btn-block text-left" data-toggle="collapse" data-target="#form-fm-attribute-{{ loop.index }}">
{{ item.vars.data.label }}
</span>
</div>
<div class="collapse" data-parent="#form-fm-attributes-collection" id="form-fm-attribute-{{ loop.index }}">
<div class="card-body">
{{ form_row(item.label) }}
{{ form_row(item.value) }}
{% set code = 'fattr://' ~ form.vars.data.id ~ '/' ~ item.vars.data.label %}
{% set code = '{{' ~ code ~ '}}' %}
<div>{{ 'Tag to insert in content'|trans }}</div>
<code>{{ code }}</code>
<div class="text-right">
<span data-collection-delete-container class="btn btn-sm btn-danger">
<span data-collection-delete="{{ loop.index }}" class="fa fa-trash"></span>
</span>
</div>
{{ form_rest(item) }}
</div>
</div>
</div>
{% endfor %}
</div>
<div data-collection-add="collection-fm-attributes" class="collection-add">
<span class="btn btn-primary" data-collection-add="collection-fm-attributes">
<span class="fa fa-plus"></span>
{{ 'New attribut'|trans }}
</span>
</div>
{{ form_row(form._token) }}
</form>
{% endif %}
</div>
{% else %}
<span class="btn btn-light">
<span class="fa fa-lock"></span>
</span>
{% endif %}
<div class="div">
{% if info.type == 'file' %}
<a class="btn btn-primary" href="{{ asset(path) }}" target="_blank">
{{ 'Download'|trans }}
</a>
{% if splInfo.extension in ['jpeg', 'jpg', 'gif', 'png', 'svg'] %}
<div class="tab-pane {% if tab == 'preview' %}show active{% endif %}" id="tab-fm-preview">
<div class="card">
<div class="card-img-top bg-tiles text-center">
<a href="{{ asset(path) }}" target="_blank">
<img src="{{ asset(path) }}" class="img-fluid">
</a>
</div>
</div>
</div>
{% endif %}
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{ 'Close'|trans }}</button>
</div>
</div>
<div class="modal-footer {% if context != 'tinymce' %}justify-content-between{% endif %}">
{% if context == 'tinymce' %}
<div>
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{ 'Close'|trans }}</button>
<button type="button" class="btn btn-primary" id="file-manager-insert" data-value="{{ asset(path) }}">{{ 'Insert'|trans }}</button>
</div>
{% else %}
<div>
{% if not isLocked %}
<button type="submit" form="form-fm-delete" class="btn btn-danger">
{{ 'Delete'|trans }}
</button>
{% if splInfo.isDir %}
<button form="form-file-delete" class="btn btn-primary" data-modal="{{ path('admin_file_manager_directory_rename', {file: splInfo.relativePathname}) }}">
{{ 'Rename'|trans }}
</button>
{% endif %}
{% else %}
<span class="btn btn-light">
<span class="fa fa-lock"></span>
</span>
{% endif %}
{% if splInfo.type == 'file' %}
<a class="btn btn-success" href="{{ asset(path) }}" target="_blank">
{{ 'Download'|trans }}
</a>
{% endif %}
</div>
<div>
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{ 'Close'|trans }}</button>
<button type="submit" class="btn btn-primary" form="form-fm-attributes">{{ 'Save'|trans }}</button>
</div>
{% endif %}
</div>
</div>
</div>
{% if not isLocked %}
<form method="post" action="{{ path('admin_file_manager_delete') }}" id="form-file-delete" data-form-confirm>
<input type="hidden" name="file" value="{{ info.relativePathname }}">
<form method="post" action="{{ path('admin_file_manager_delete') }}" id="form-fm-delete" data-form-confirm>
<input type="hidden" name="file" value="{{ splInfo.relativePathname }}">
<input type="hidden" name="_token" value="{{ csrf_token('delete') }}">
<input type="hidden" name="_method" value="DELETE">
</form>
{% endif %}
<template type="text/template" id="collection-fm-attributes">
<div class="card" data-collection-item="__name__">
<div class="card-header p-0">
<span class="btn btn-link btn-block text-left" data-toggle="collapse" data-target="#form-fm-attribute-__name__">
{{ 'New attribut'|trans }}
</span>
</div>
<div class="collapse show" id="form-fm-__name__" data-parent="#form-fm-attributes-collection">
<div class="card-body">
{{ form_row(form.attributes.vars.prototype.label) }}
{{ form_row(form.attributes.vars.prototype.value) }}
<div class="text-right">
<span data-collection-delete-container class="btn btn-sm btn-danger"></span>
</div>
{{ form_rest(form.attributes.vars.prototype) }}
</div>
</div>
</div>
</template>

View File

@ -1,4 +1,4 @@
<ul class="nav nav-pills" id="myTab" role="tablist">
<ul class="nav nav-pills" role="tablist">
<li class="nav-item" role="presentation">
<a class="nav-link {% if tab == 'content' %}active{% endif %}" data-toggle="tab" href="#form-node-edit-content">
{{ 'Content'|trans }}

View File

@ -0,0 +1,85 @@
<?php
namespace App\Core\Twig\Extension;
use App\Core\FileManager\FsFileManager;
use App\Core\Repository\FileInformationRepositoryQuery;
use function Symfony\Component\String\u;
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
class FileInformationExtension extends AbstractExtension
{
protected FsFileManager $fsManage²r;
protected FileInformationRepositoryQuery $query;
public function __construct(FsFileManager $fsManager, FileInformationRepositoryQuery $query)
{
$this->fsManager = $fsManager;
$this->query = $query;
}
/**
* {@inheritdoc}
*/
public function getFilters()
{
return [
new TwigFilter('file_attribute', [$this, 'fileAttribute']),
new TwigFilter('file_attributes', [$this, 'fileAttributes']),
];
}
public function fileAttribute(string $file, string $label): ?string
{
$file = u($file);
$pathUri = $this->fsManager->getPathUri();
$pathUri2 = '/'.$pathUri;
if ($file->startsWith($pathUri) || $file->startsWith($pathUri2)) {
$file = $file->replaceMatches('#^'.preg_quote($pathUri).'#', '');
$file = $file->replaceMatches('#^'.preg_quote($pathUri2).'#', '');
}
$fileInfo = $this->fsManager->getFileInformation((string) $file);
if ($fileInfo) {
foreach ($fileInfo->getAttributes() as $attribute) {
if ($attribute['label'] === $label) {
return $attribute['value'];
}
}
}
return null;
}
public function fileAttributes(?string $content): ?string
{
preg_match_all('#\{\{\s*fattr://(?P<hash>[a-z0-9]+)\/(?P<label>.+)\s*\}\}#isU', $content, $match, PREG_SET_ORDER);
foreach ($match as $block) {
$hash = $block['hash'];
$label = $block['label'];
$value = null;
$fileInfo = $this->query->create()
->where('.id LIKE :hash')
->setParameter(':hash', $hash.'%')
->findOne()
;
if ($fileInfo) {
foreach ($fileInfo->getAttributes() as $attribute) {
if ($attribute['label'] === $label) {
$value = $attribute['value'];
}
}
}
$content = str_replace($block[0], $value, $content);
}
return $content;
}
}

View File

@ -6,7 +6,7 @@ use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
class BlockExtension extends AbstractExtension
class UrlExtension extends AbstractExtension
{
protected UrlGeneratorInterface $urlGenerator;
@ -25,7 +25,7 @@ class BlockExtension extends AbstractExtension
];
}
public function replaceUrl($content)
public function replaceUrl(?string $content)
{
preg_match_all('#\{\{\s*url://(?P<route>[a-z_]+)(\?(?P<params>.*))?\s*\}\}#isU', $content, $match, PREG_SET_ORDER);