(Grav GitSync) Automatic Commit from smokephil

This commit is contained in:
smokephil 2024-03-22 22:42:22 +01:00 committed by GitSync
commit 4267db646d
2765 changed files with 462171 additions and 0 deletions

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,361 @@
<?php
declare(strict_types=1);
namespace Grav\Plugin\FlexObjects\Controllers;
use Grav\Common\Config\Config;
use Grav\Common\Grav;
use Grav\Common\Inflector;
use Grav\Common\Language\Language;
use Grav\Common\Session;
use Grav\Common\Uri;
use Grav\Common\User\Interfaces\UserInterface;
use Grav\Common\Utils;
use Grav\Framework\Controller\Traits\ControllerResponseTrait;
use Grav\Framework\Flex\FlexDirectory;
use Grav\Framework\Flex\FlexForm;
use Grav\Framework\Flex\FlexFormFlash;
use Grav\Framework\Flex\Interfaces\FlexFormInterface;
use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
use Grav\Framework\Psr7\Response;
use Grav\Framework\RequestHandler\Exception\NotFoundException;
use Grav\Framework\RequestHandler\Exception\PageExpiredException;
use Grav\Framework\Route\Route;
use Grav\Plugin\FlexObjects\Flex;
use Grav\Plugin\Form\Forms;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use RocketTheme\Toolbox\Event\Event;
use RocketTheme\Toolbox\Session\Message;
use function in_array;
use function is_callable;
/**
* Class AbstractController
* @package Grav\Plugin\FlexObjects\Controllers
*/
abstract class AbstractController implements RequestHandlerInterface
{
use ControllerResponseTrait;
/** @var string */
protected $nonce_action = 'flex-object';
/** @var string */
protected $nonce_name = 'nonce';
/** @var ServerRequestInterface */
protected $request;
/** @var Grav */
protected $grav;
/** @var UserInterface|null */
protected $user;
/** @var string */
protected $type;
/** @var string */
protected $key;
/** @var FlexDirectory */
protected $directory;
/** @var FlexObjectInterface */
protected $object;
/**
* Handle request.
*
* Fires event: flex.[directory].[task|action].[command]
*
* @param ServerRequestInterface $request
* @return Response
*/
public function handle(ServerRequestInterface $request): ResponseInterface
{
$attributes = $request->getAttributes();
$this->request = $request;
$this->grav = $attributes['grav'] ?? Grav::instance();
$this->type = $attributes['type'] ?? null;
$this->key = $attributes['key'] ?? null;
if ($this->type) {
$this->directory = $this->getFlex()->getDirectory($this->type);
$this->object = $attributes['object'] ?? null;
if (!$this->object && $this->key && $this->directory) {
$this->object = $this->directory->getObject($this->key) ?? $this->directory->createObject([], $this->key ?? '');
if (is_callable([$this->object, 'refresh'])) {
$this->object->refresh();
}
}
}
/** @var Route $route */
$route = $attributes['route'];
$post = $this->getPost();
if ($this->isFormSubmit()) {
$form = $this->getForm();
$this->nonce_name = $attributes['nonce_name'] ?? $form->getNonceName();
$this->nonce_action = $attributes['nonce_action'] ?? $form->getNonceAction();
}
try {
$task = $request->getAttribute('task') ?? $post['task'] ?? $route->getParam('task');
if ($task) {
if (empty($attributes['forwarded'])) {
$this->checkNonce($task);
}
$type = 'task';
$command = $task;
} else {
$type = 'action';
$command = $request->getAttribute('action') ?? $post['action'] ?? $route->getParam('action') ?? 'display';
}
$command = strtolower($command);
$event = new Event(
[
'controller' => $this,
'response' => null
]
);
$this->grav->fireEvent("flex.{$this->type}.{$type}.{$command}", $event);
$response = $event['response'];
if (!$response) {
/** @var Inflector $inflector */
$inflector = $this->grav['inflector'];
$method = $type . $inflector::camelize($command);
if ($method && method_exists($this, $method)) {
$response = $this->{$method}($request);
} else {
throw new NotFoundException($request);
}
}
} catch (\Exception $e) {
$response = $this->createErrorResponse($e);
}
if ($response instanceof Response) {
return $response;
}
return $this->createJsonResponse($response);
}
/**
* @return ServerRequestInterface
*/
public function getRequest(): ServerRequestInterface
{
return $this->request;
}
/**
* @param string|null $name
* @param mixed $default
* @return mixed
*/
public function getPost(string $name = null, $default = null)
{
$body = $this->request->getParsedBody();
if ($name) {
return $body[$name] ?? $default;
}
return $body;
}
/**
* @return bool
*/
public function isFormSubmit(): bool
{
return (bool)$this->getPost('__form-name__');
}
/**
* @param string|null $type
* @return FlexForm
*/
public function getForm(string $type = null): FlexFormInterface
{
$object = $this->getObject();
if (!$object) {
throw new \RuntimeException('Not Found', 404);
}
$formName = $this->getPost('__form-name__');
if ($formName) {
/** @var Forms $forms */
$forms = $this->getGrav()['forms'];
$form = $forms->getActiveForm();
if ($form instanceof FlexForm && $form->getName() === $formName && $form->getObject()->getFlexKey() === $object->getFlexKey()) {
return $form;
}
}
return $object->getForm($type ?? 'edit');
}
/**
* @param FlexObjectInterface $object
* @param string $type
* @return FlexFormFlash
*/
protected function getFormFlash(FlexObjectInterface $object, string $type = '')
{
/** @var Uri $uri */
$uri = $this->grav['uri'];
$url = $uri->url;
$formName = $this->getPost('__form-name__');
if (!$formName) {
$form = $object->getForm($type);
$formName = $form->getName();
$uniqueId = $form->getUniqueId();
} else {
$uniqueId = $this->getPost('__unique_form_id__') ?: $formName ?: sha1($url);
}
/** @var Session $session */
$session = $this->grav['session'];
$config = [
'session_id' => $session->getId(),
'unique_id' => $uniqueId,
'form_name' => $formName,
];
$flash = new FlexFormFlash($config);
if (!$flash->exists()) {
$flash->setUrl($url)->setUser($this->grav['user']);
}
return $flash;
}
/**
* @return Grav
*/
public function getGrav(): Grav
{
return $this->grav;
}
/**
* @return Session
*/
public function getSession(): Session
{
return $this->grav['session'];
}
/**
* @return Flex
*/
public function getFlex(): Flex
{
return $this->grav['flex_objects'];
}
/**
* @return string
*/
public function getDirectoryType(): string
{
return $this->type;
}
/**
* @return string
*/
public function getObjectKey(): string
{
return $this->key;
}
/**
* @return FlexDirectory|null
*/
public function getDirectory(): ?FlexDirectory
{
return $this->directory;
}
/**
* @return FlexObjectInterface|null
*/
public function getObject(): ?FlexObjectInterface
{
return $this->object;
}
/**
* @param string $string
* @param array $args
* @return string
*/
public function translate(string $string, ...$args): string
{
/** @var Language $language */
$language = $this->grav['language'];
array_unshift($args, $string);
return $language->translate($args);
}
/**
* @param string $message
* @param string $type
* @return $this
*/
public function setMessage(string $message, string $type = 'info'): self
{
/** @var Message $messages */
$messages = $this->grav['messages'];
$messages->add($message, $type);
return $this;
}
/**
* @param UserInterface $user
* @return void
*/
public function setUser(UserInterface $user): void
{
$this->user = $user;
}
/**
* @return Config
*/
protected function getConfig(): Config
{
return $this->grav['config'];
}
/**
* @param string $task
* @return void
* @throws PageExpiredException
*/
protected function checkNonce(string $task): void
{
$nonce = null;
if (in_array(strtoupper($this->request->getMethod()), ['POST', 'PUT', 'PATCH', 'DELETE'])) {
$nonce = $this->getPost($this->nonce_name);
}
if (!$nonce) {
$nonce = $this->grav['uri']->param($this->nonce_name);
}
if (!$nonce) {
$nonce = $this->grav['uri']->query($this->nonce_name);
}
if (!$nonce || !Utils::verifyNonce($nonce, $this->nonce_action)) {
throw new PageExpiredException($this->request);
}
}
}

View file

@ -0,0 +1,675 @@
<?php
declare(strict_types=1);
namespace Grav\Plugin\FlexObjects\Controllers;
use Exception;
use Grav\Common\Debugger;
use Grav\Common\Page\Interfaces\PageInterface;
use Grav\Common\Page\Medium\Medium;
use Grav\Common\Page\Medium\MediumFactory;
use Grav\Common\Utils;
use Grav\Framework\Flex\FlexObject;
use Grav\Framework\Flex\Interfaces\FlexAuthorizeInterface;
use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
use Grav\Framework\Media\Interfaces\MediaInterface;
use LogicException;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\UploadedFileInterface;
use RocketTheme\Toolbox\Event\Event;
use RuntimeException;
use function is_array;
use function is_string;
/**
* Class MediaController
* @package Grav\Plugin\FlexObjects\Controllers
*/
class MediaController extends AbstractController
{
/**
* @return ResponseInterface
*/
public function taskMediaUpload(): ResponseInterface
{
$this->checkAuthorization('media.create');
$object = $this->getObject();
if (null === $object) {
throw new RuntimeException('Not Found', 404);
}
if (!method_exists($object, 'checkUploadedMediaFile')) {
throw new RuntimeException('Not Found', 404);
}
// Get updated object from Form Flash.
$flash = $this->getFormFlash($object);
if ($flash->exists()) {
$object = $flash->getObject() ?? $object;
$object->update([], $flash->getFilesByFields());
}
// Get field for the uploaded media.
$field = $this->getPost('name', 'undefined');
if ($field === 'undefined') {
$field = null;
}
$request = $this->getRequest();
$files = $request->getUploadedFiles();
if ($field && isset($files['data'])) {
$files = $files['data'];
$parts = explode('.', $field);
$last = array_pop($parts);
foreach ($parts as $name) {
if (!is_array($files[$name])) {
throw new RuntimeException($this->translate('PLUGIN_ADMIN.INVALID_PARAMETERS'), 400);
}
$files = $files[$name];
}
$file = $files[$last] ?? null;
} else {
// Legacy call with name being the filename instead of field name.
$file = $files['file'] ?? null;
$field = null;
}
/** @var UploadedFileInterface $file */
if (is_array($file)) {
$file = reset($file);
}
if (!$file instanceof UploadedFileInterface) {
throw new RuntimeException($this->translate('PLUGIN_ADMIN.INVALID_PARAMETERS'), 400);
}
$filename = $file->getClientFilename();
$object->checkUploadedMediaFile($file, $filename, $field);
try {
// TODO: This only merges main level data, but is good for ordering (for now).
$data = $flash->getData() ?? [];
$data = array_replace($data, (array)$this->getPost('data'));
$crop = $this->getPost('crop');
if (is_string($crop)) {
$crop = json_decode($crop, true, 512, JSON_THROW_ON_ERROR);
}
$flash->setData($data);
$flash->addUploadedFile($file, $field, $crop);
$flash->save();
} catch (Exception $e) {
throw new RuntimeException($e->getMessage(), $e->getCode(), $e);
}
// Include exif metadata into the response if configured to do so
$metadata = [];
$include_metadata = $this->grav['config']->get('system.media.auto_metadata_exif', false);
if ($include_metadata) {
$medium = MediumFactory::fromUploadedFile($file);
$media = $object->getMedia();
$media->add($filename, $medium);
$basename = str_replace(['@3x', '@2x'], '', Utils::pathinfo($filename, PATHINFO_BASENAME));
if (isset($media[$basename])) {
$metadata = $media[$basename]->metadata() ?: [];
}
}
$response = [
'code' => 200,
'status' => 'success',
'message' => $this->translate('PLUGIN_ADMIN.FILE_UPLOADED_SUCCESSFULLY'),
'filename' => htmlspecialchars($filename, ENT_QUOTES | ENT_HTML5, 'UTF-8'),
'metadata' => $metadata
];
return $this->createJsonResponse($response);
}
/**
* @return ResponseInterface
*/
public function taskMediaUploadMeta(): ResponseInterface
{
try {
$this->checkAuthorization('media.create');
$object = $this->getObject();
if (null === $object) {
throw new RuntimeException('Not Found', 404);
}
if (!method_exists($object, 'getMediaField')) {
throw new RuntimeException('Not Found', 404);
}
$object->refresh();
// Get updated object from Form Flash.
$flash = $this->getFormFlash($object);
if ($flash->exists()) {
$object = $flash->getObject() ?? $object;
$object->update([], $flash->getFilesByFields());
}
// Get field and data for the uploaded media.
$field = (string)$this->getPost('field');
$media = $object->getMediaField($field);
if (!$media) {
throw new RuntimeException('Media field not found: ' . $field, 404);
}
$data = $this->getPost('data');
if (is_string($data)) {
$data = json_decode($data, true);
}
$filename = Utils::basename($data['name'] ?? '');
// Update field.
$files = $object->getNestedProperty($field, []);
// FIXME: Do we want to save something into the field as well?
$files[$filename] = [];
$object->setNestedProperty($field, $files);
$info = [
'modified' => $data['modified'] ?? null,
'size' => $data['size'] ?? null,
'mime' => $data['mime'] ?? null,
'width' => $data['width'] ?? null,
'height' => $data['height'] ?? null,
'duration' => $data['duration'] ?? null,
'orientation' => $data['orientation'] ?? null,
'meta' => array_filter($data, static function ($val) { return $val !== null; })
];
$info = array_filter($info, static function ($val) { return $val !== null; });
// As the file may not be saved locally, we need to update the index.
$media->updateIndex([$filename => $info]);
$object->save();
$flash->save();
$response = [
'code' => 200,
'status' => 'success',
'message' => $this->translate('PLUGIN_ADMIN.FILE_UPLOADED_SUCCESSFULLY'),
'field' => $field,
'filename' => $filename,
'metadata' => $data
];
} catch (\Exception $e) {
/** @var Debugger $debugger */
$debugger = $this->grav['debugger'];
$debugger->addException($e);
return $this->createJsonErrorResponse($e);
}
return $this->createJsonResponse($response);
}
/**
* @return ResponseInterface
*/
public function taskMediaReorder(): ResponseInterface
{
try {
$this->checkAuthorization('media.update');
$object = $this->getObject();
if (null === $object) {
throw new RuntimeException('Not Found', 404);
}
if (!method_exists($object, 'getMediaField')) {
throw new RuntimeException('Not Found', 404);
}
$object->refresh();
// Get updated object from Form Flash.
$flash = $this->getFormFlash($object);
if ($flash->exists()) {
$object = $flash->getObject() ?? $object;
$object->update([], $flash->getFilesByFields());
}
// Get field and data for the uploaded media.
$field = (string)$this->getPost('field');
$media = $object->getMediaField($field);
if (!$media) {
throw new RuntimeException('Media field not found: ' . $field, 404);
}
// Create id => filename map from all files in the media.
$map = [];
foreach ($media as $name => $medium) {
$id = $medium->get('meta.id');
if ($id) {
$map[$id] = $name;
}
}
// Get reorder list and reorder the map.
$data = $this->getPost('data');
if (is_string($data)) {
$data = json_decode($data, true);
}
$data = array_fill_keys($data, null);
$map = array_filter(array_merge($data, $map), static function($val) { return $val !== null; });
// Reorder the files.
$files = $object->getNestedProperty($field, []);
$map = array_fill_keys($map, null);
$files = array_filter(array_merge($map, $files), static function($val) { return $val !== null; });
// Update field.
$object->setNestedProperty($field, $files);
$object->save();
$flash->save();
$response = [
'code' => 200,
'status' => 'success',
'message' => $this->translate('PLUGIN_ADMIN.FIELD_REORDER_SUCCESSFUL'),
'field' => $field,
'ordering' => array_keys($files)
];
} catch (\Exception $e) {
/** @var Debugger $debugger */
$debugger = $this->grav['debugger'];
$debugger->addException($e);
$ex = new RuntimeException($this->translate('PLUGIN_ADMIN.FIELD_REORDER_FAILED', $field), $e->getCode(), $e);
return $this->createJsonErrorResponse($ex);
}
return $this->createJsonResponse($response);
}
/**
* @return ResponseInterface
*/
public function taskMediaDelete(): ResponseInterface
{
$this->checkAuthorization('media.delete');
/** @var FlexObjectInterface|null $object */
$object = $this->getObject();
if (!$object) {
throw new RuntimeException('Not Found', 404);
}
$filename = $this->getPost('filename');
// Handle bad filenames.
if (!Utils::checkFilename($filename)) {
throw new RuntimeException($this->translate('PLUGIN_ADMIN.NO_FILE_FOUND'), 400);
}
try {
$field = $this->getPost('name');
$flash = $this->getFormFlash($object);
$flash->removeFile($filename, $field);
$flash->save();
} catch (Exception $e) {
throw new RuntimeException($e->getMessage(), $e->getCode(), $e);
}
$response = [
'code' => 200,
'status' => 'success',
'message' => $this->translate('PLUGIN_ADMIN.FILE_DELETED') . ': ' . htmlspecialchars($filename, ENT_QUOTES | ENT_HTML5, 'UTF-8')
];
return $this->createJsonResponse($response);
}
/**
* Used in pagemedia field.
*
* @return ResponseInterface
*/
public function taskMediaCopy(): ResponseInterface
{
$this->checkAuthorization('media.create');
/** @var FlexObjectInterface|null $object */
$object = $this->getObject();
if (!$object) {
throw new RuntimeException('Not Found', 404);
}
if (!method_exists($object, 'uploadMediaFile')) {
throw new RuntimeException('Not Found', 404);
}
$request = $this->getRequest();
$files = $request->getUploadedFiles();
$file = $files['file'] ?? null;
if (!$file instanceof UploadedFileInterface) {
throw new RuntimeException($this->translate('PLUGIN_ADMIN.INVALID_PARAMETERS'), 400);
}
$post = $request->getParsedBody();
$filename = $post['name'] ?? $file->getClientFilename();
// Upload media right away.
$object->uploadMediaFile($file, $filename);
// Include exif metadata into the response if configured to do so
$metadata = [];
$include_metadata = $this->grav['config']->get('system.media.auto_metadata_exif', false);
if ($include_metadata) {
$basename = str_replace(['@3x', '@2x'], '', Utils::pathinfo($filename, PATHINFO_BASENAME));
$media = $object->getMedia();
if (isset($media[$basename])) {
$metadata = $media[$basename]->metadata() ?: [];
}
}
if ($object instanceof PageInterface) {
// Backwards compatibility to existing plugins.
// DEPRECATED: page
$this->grav->fireEvent('onAdminAfterAddMedia', new Event(['object' => $object, 'page' => $object]));
}
$response = [
'code' => 200,
'status' => 'success',
'message' => $this->translate('PLUGIN_ADMIN.FILE_UPLOADED_SUCCESSFULLY'),
'filename' => htmlspecialchars($filename, ENT_QUOTES | ENT_HTML5, 'UTF-8'),
'metadata' => $metadata
];
return $this->createJsonResponse($response);
}
/**
* Used in pagemedia field.
*
* @return ResponseInterface
*/
public function taskMediaRemove(): ResponseInterface
{
$this->checkAuthorization('media.delete');
/** @var FlexObjectInterface|null $object */
$object = $this->getObject();
if (!$object) {
throw new RuntimeException('Not Found', 404);
}
if (!method_exists($object, 'deleteMediaFile')) {
throw new RuntimeException('Not Found', 404);
}
$field = $this->getPost('field');
$filename = $this->getPost('filename');
// Handle bad filenames.
if (!Utils::checkFilename($filename)) {
throw new RuntimeException($this->translate('PLUGIN_ADMIN.NO_FILE_FOUND'), 400);
}
$object->deleteMediaFile($filename, $field);
if ($field) {
$order = $object->getNestedProperty($field);
unset($order[$filename]);
$object->setNestedProperty($field, $order);
$object->save();
}
if ($object instanceof PageInterface) {
// Backwards compatibility to existing plugins.
// DEPRECATED: page
$this->grav->fireEvent('onAdminAfterDelMedia', new Event(['object' => $object, 'page' => $object, 'media' => $object->getMedia(), 'filename' => $filename]));
}
$response = [
'code' => 200,
'status' => 'success',
'message' => $this->translate('PLUGIN_ADMIN.FILE_DELETED') . ': ' . htmlspecialchars($filename, ENT_QUOTES | ENT_HTML5, 'UTF-8')
];
return $this->createJsonResponse($response);
}
/**
* @return ResponseInterface
*/
public function actionMediaList(): ResponseInterface
{
$this->checkAuthorization('media.list');
/** @var MediaInterface|FlexObjectInterface $object */
$object = $this->getObject();
if (!$object) {
throw new RuntimeException('Not Found', 404);
}
// Get updated object from Form Flash.
$flash = $this->getFormFlash($object);
if ($flash->exists()) {
$object = $flash->getObject() ?? $object;
$object->update([], $flash->getFilesByFields());
}
$media = $object->getMedia();
$media_list = [];
/**
* @var string $name
* @var Medium $medium
*/
foreach ($media->all() as $name => $medium) {
$media_list[$name] = [
'url' => $medium->display($medium->get('extension') === 'svg' ? 'source' : 'thumbnail')->cropZoom(400, 300)->url(),
'size' => $medium->get('size'),
'metadata' => $medium->metadata() ?: [],
'original' => $medium->higherQualityAlternative()->get('filename')
];
}
$response = [
'code' => 200,
'status' => 'success',
'results' => $media_list
];
return $this->createJsonResponse($response);
}
/**
* Used by the filepicker field to get a list of files in a folder.
*
* @return ResponseInterface
*/
protected function actionMediaPicker(): ResponseInterface
{
$this->checkAuthorization('media.list');
/** @var FlexObject $object */
$object = $this->getObject();
if (!$object || !\is_callable([$object, 'getFieldSettings'])) {
throw new RuntimeException('Not Found', 404);
}
// Get updated object from Form Flash.
$flash = $this->getFormFlash($object);
if ($flash->exists()) {
$object = $flash->getObject() ?? $object;
$object->update([], $flash->getFilesByFields());
}
$name = $this->getPost('name');
$settings = $name ? $object->getFieldSettings($name) : null;
if (empty($settings['media_picker_field'])) {
throw new RuntimeException('Not Found', 404);
}
$media = $object->getMediaField($name);
$available_files = [];
$metadata = [];
$thumbs = [];
/**
* @var string $name
* @var Medium $medium
*/
foreach ($media->all() as $name => $medium) {
$available_files[] = $name;
if (isset($settings['include_metadata'])) {
$img_metadata = $medium->metadata();
if ($img_metadata) {
$metadata[$name] = $img_metadata;
}
}
}
// Peak in the flashObject for optimistic filepicker updates
$pending_files = [];
$sessionField = base64_encode($this->grav['uri']->url());
$flash = $this->getSession()->getFlashObject('files-upload');
$folder = $media->getPath() ?: null;
if ($flash && isset($flash[$sessionField])) {
foreach ($flash[$sessionField] as $field => $data) {
foreach ($data as $file) {
$test = \dirname($file['path']);
if ($test === $folder) {
$pending_files[] = $file['name'];
}
}
}
}
$this->getSession()->setFlashObject('files-upload', $flash);
// Handle Accepted file types
// Accept can only be file extensions (.pdf|.jpg)
if (isset($settings['accept'])) {
$available_files = array_filter($available_files, function ($file) use ($settings) {
return $this->filterAcceptedFiles($file, $settings);
});
$pending_files = array_filter($pending_files, function ($file) use ($settings) {
return $this->filterAcceptedFiles($file, $settings);
});
}
if (isset($settings['deny'])) {
$available_files = array_filter($available_files, function ($file) use ($settings) {
return $this->filterDeniedFiles($file, $settings);
});
$pending_files = array_filter($pending_files, function ($file) use ($settings) {
return $this->filterDeniedFiles($file, $settings);
});
}
// Generate thumbs if needed
if (isset($settings['preview_images']) && $settings['preview_images'] === true) {
foreach ($available_files as $filename) {
$thumbs[$filename] = $media[$filename]->zoomCrop(100,100)->url();
}
}
$response = [
'code' => 200,
'status' => 'success',
'files' => array_values($available_files),
'pending' => array_values($pending_files),
'folder' => $folder,
'metadata' => $metadata,
'thumbs' => $thumbs
];
return $this->createJsonResponse($response);
}
/**
* @param string $file
* @param array $settings
* @return false|int
*/
protected function filterAcceptedFiles(string $file, array $settings)
{
$valid = false;
foreach ((array)$settings['accept'] as $type) {
$find = str_replace('*', '.*', $type);
$valid |= preg_match('#' . $find . '$#i', $file);
}
return $valid;
}
/**
* @param string $file
* @param array $settings
* @return false|int
*/
protected function filterDeniedFiles(string $file, array $settings)
{
$valid = true;
foreach ((array)$settings['deny'] as $type) {
$find = str_replace('*', '.*', $type);
$valid = !preg_match('#' . $find . '$#i', $file);
}
return $valid;
}
/**
* @param string $action
* @return void
* @throws LogicException
* @throws RuntimeException
*/
protected function checkAuthorization(string $action): void
{
$object = $this->getObject();
if (!$object) {
throw new RuntimeException('Not Found', 404);
}
// If object does not have ACL support ignore ACL checks.
if (!$object instanceof FlexAuthorizeInterface) {
return;
}
switch ($action) {
case 'media.list':
$action = 'read';
break;
case 'media.create':
case 'media.update':
case 'media.delete':
$action = $object->exists() ? 'update' : 'create';
break;
default:
throw new LogicException(sprintf('Unsupported authorize action %s', $action), 500);
}
if (!$object->isAuthorized($action, null, $this->user)) {
throw new RuntimeException('Forbidden', 403);
}
}
}

View file

@ -0,0 +1,543 @@
<?php
declare(strict_types=1);
namespace Grav\Plugin\FlexObjects\Controllers;
use Grav\Common\Grav;
use Grav\Framework\Flex\FlexForm;
use Grav\Framework\Flex\FlexObject;
use Grav\Framework\Flex\Interfaces\FlexAuthorizeInterface;
use Grav\Framework\Route\Route;
use Grav\Plugin\FlexObjects\Events\FlexTaskEvent;
use Nyholm\Psr7\ServerRequest;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use RocketTheme\Toolbox\Event\Event;
use RuntimeException;
/**
* Object controller is for the frontend.
*
* Currently following tasks are supported:
*
* - save (create or update)
* - create
* - update
* - delete
* - reset
* - preview
*/
class ObjectController extends AbstractController
{
/**
* Save object.
*
* Forwards call to either create or update task.
*
* @param ServerRequestInterface $request
* @return ResponseInterface
*/
public function taskSave(ServerRequestInterface $request): ResponseInterface
{
$form = $this->getForm();
$object = $form->getObject();
return $object->exists() ? $this->taskUpdate($request) : $this->taskCreate($request);
}
/**
* Create object.
*
* Task fails if object exists.
*
* @param ServerRequestInterface $request
* @return ResponseInterface
*/
public function taskCreate(ServerRequestInterface $request): ResponseInterface
{
$this->checkAuthorization('create');
$form = $this->getForm();
$callable = function (array $data, array $files, FlexObject $object) {
if (method_exists($object, 'storeOriginal')) {
$object->storeOriginal();
}
$object->update($data, $files);
if (\is_callable([$object, 'check'])) {
$object->check($this->user);
}
$event = new FlexTaskEvent($this, $object, 'create');
$this->grav->dispatchEvent($event);
$object->save();
};
$form->setSubmitMethod($callable);
$form->handleRequest($request);
if (!$form->isValid()) {
$error = $form->getError();
if ($error) {
$this->setMessage($error, 'error');
}
$errors = $form->getErrors();
foreach ($errors as $field) {
foreach ($field as $error) {
$this->setMessage($error, 'error');
}
}
$data = $form->getData();
if (null !== $data) {
$object = $form->getObject();
$flash = $form->getFlash();
$flash->setObject($object);
$flash->setData($data->toArray());
$flash->save();
}
return $this->createDisplayResponse();
}
// FIXME: make it conditional
$grav = $this->grav;
$grav->fireEvent('gitsync');
$this->object = $form->getObject();
$event = new Event(
[
'task' => 'create',
'controller' => $this,
'object' => $this->object,
'response' => null,
'message' => null,
]
);
$this->grav->fireEvent("flex.{$this->type}.task.create.after", $event);
$this->setMessage($event['message'] ?? $this->translate('PLUGIN_FLEX_OBJECTS.STATE.CREATED_SUCCESSFULLY'), 'info');
if ($event['response']) {
return $event['response'];
}
$redirect = $request->getAttribute('redirect', (string)$request->getUri());
return $this->createRedirectResponse($redirect, 303);
}
/**
* Update object.
*
* Task fails if object does not exist.
*
* @param ServerRequestInterface $request
* @return ResponseInterface
*/
public function taskUpdate(ServerRequestInterface $request): ResponseInterface
{
$this->checkAuthorization('update');
$form = $this->getForm();
$callable = function (array $data, array $files, FlexObject $object) {
if (method_exists($object, 'storeOriginal')) {
$object->storeOriginal();
}
$object->update($data, $files);
if (\is_callable([$object, 'check'])) {
$object->check($this->user);
}
$event = new FlexTaskEvent($this, $object, 'update');
$this->grav->dispatchEvent($event);
$object->save();
};
$form->setSubmitMethod($callable);
$form->handleRequest($request);
if (!$form->isValid()) {
$error = $form->getError();
if ($error) {
$this->setMessage($error, 'error');
}
$errors = $form->getErrors();
foreach ($errors as $field) {
foreach ($field as $error) {
$this->setMessage($error, 'error');
}
}
$data = $form->getData();
if (null !== $data) {
$object = $form->getObject();
$flash = $form->getFlash();
$flash->setObject($object);
$flash->setData($data->toArray());
$flash->save();
}
return $this->createDisplayResponse();
}
// FIXME: make it conditional
$grav = $this->grav;
$grav->fireEvent('gitsync');
$this->object = $form->getObject();
$event = new Event(
[
'task' => 'update',
'controller' => $this,
'object' => $this->object,
'response' => null,
'message' => null,
]
);
$this->grav->fireEvent("flex.{$this->type}.task.update.after", $event);
$this->setMessage($event['message'] ?? $this->translate('PLUGIN_FLEX_OBJECTS.STATE.UPDATED_SUCCESSFULLY'), 'info');
if ($event['response']) {
return $event['response'];
}
$redirect = $request->getAttribute('redirect', (string)$request->getUri()->getPath());
return $this->createRedirectResponse($redirect, 303);
}
/**
* Delete object.
*
* @param ServerRequestInterface $request
* @return ResponseInterface
*/
public function taskDelete(ServerRequestInterface $request): ResponseInterface
{
$this->checkAuthorization('delete');
$object = $this->getObject();
if (!$object) {
throw new RuntimeException('Not Found', 404);
}
$event = new FlexTaskEvent($this, $object, 'delete');
$this->grav->dispatchEvent($event);
$object->delete();
// FIXME: make it conditional
$grav = $this->grav;
$grav->fireEvent('gitsync');
$event = new Event(
[
'task' => 'delete',
'controller' => $this,
'object' => $object,
'response' => null,
'message' => null,
]
);
$this->grav->fireEvent("flex.{$this->type}.task.delete.after", $event);
$this->setMessage($this->translate($event['message'] ?? 'PLUGIN_FLEX_OBJECTS.STATE.DELETED_SUCCESSFULLY'), 'info');
if ($event['response']) {
return $event['response'];
}
$redirect = $request->getAttribute('redirect', (string)$request->getUri()->getPath());
return $this->createRedirectResponse($redirect, 303);
}
/**
* Reset form to original values.
*
* @param ServerRequestInterface $request
* @return ResponseInterface
*/
public function taskReset(ServerRequestInterface $request): ResponseInterface
{
$this->checkAuthorization('save');
$flash = $this->getForm()->getFlash();
$flash->delete();
$redirect = $request->getAttribute('redirect', (string)$request->getUri()->getPath());
return $this->createRedirectResponse($redirect, 303);
}
/**
* Preview object.
*
* Takes a form input and converts it to visible presentation of the object.
*
* @param ServerRequestInterface $request
* @return ResponseInterface
*/
public function taskPreview(ServerRequestInterface $request): ResponseInterface
{
$this->checkAuthorization('save');
/** @var FlexForm $form */
$form = $this->getForm('edit');
$form->setRequest($request);
if (!$form->validate()) {
$error = $form->getError();
if ($error) {
$this->setMessage($error, 'error');
}
$errors = $form->getErrors();
foreach ($errors as $field) {
foreach ($field as $error) {
$this->setMessage($error, 'error');
}
}
return $this->createRedirectResponse((string)$request->getUri(), 303);
}
$this->object = $form->updateObject();
return $this->actionDisplayPreview();
}
/**
* @param ServerRequestInterface $request
* @return ResponseInterface
*/
public function taskMediaList(ServerRequestInterface $request): ResponseInterface
{
$directory = $this->getDirectory();
if (!$directory) {
throw new RuntimeException('Not Found', 404);
}
return $this->forwardMediaTask('action', 'media.list');
}
/**
* @param ServerRequestInterface $request
* @return ResponseInterface
*/
public function taskMediaUpload(ServerRequestInterface $request): ResponseInterface
{
$directory = $this->getDirectory();
if (!$directory) {
throw new RuntimeException('Not Found', 404);
}
return $this->forwardMediaTask('task', 'media.upload');
}
/**
* @param ServerRequestInterface $request
* @return ResponseInterface
*/
public function taskMediaUploadMeta(ServerRequestInterface $request): ResponseInterface
{
$directory = $this->getDirectory();
if (!$directory) {
throw new RuntimeException('Not Found', 404);
}
return $this->forwardMediaTask('task', 'media.upload.meta');
}
/**
* @param ServerRequestInterface $request
* @return ResponseInterface
*/
public function taskMediaReorder(ServerRequestInterface $request): ResponseInterface
{
$directory = $this->getDirectory();
if (!$directory) {
throw new RuntimeException('Not Found', 404);
}
return $this->forwardMediaTask('task', 'media.reorder');
}
/**
* @param ServerRequestInterface $request
* @return ResponseInterface
*/
public function taskMediaDelete(ServerRequestInterface $request): ResponseInterface
{
$directory = $this->getDirectory();
if (!$directory) {
throw new RuntimeException('Not Found', 404);
}
return $this->forwardMediaTask('task', 'media.delete');
}
/**
* @param ServerRequestInterface $request
* @return ResponseInterface
*/
public function taskGetFilesInFolder(ServerRequestInterface $request): ResponseInterface
{
$directory = $this->getDirectory();
if (!$directory) {
throw new RuntimeException('Not Found', 404);
}
return $this->forwardMediaTask('action', 'media.picker');
}
/**
* @param ServerRequestInterface $request
* @return ResponseInterface
* @deprecated Do not use
*/
public function taskFilesUpload(ServerRequestInterface $request): ResponseInterface
{
/** @var Route $route */
$route = $this->grav['route'];
if ($route->getParam('task') === 'media.upload') {
return $this->taskMediaUpload($request);
}
throw new RuntimeException('Task filesUpload should not be called, please update form plugin!', 400);
}
/**
* @param ServerRequestInterface $request
* @return ResponseInterface
* @deprecated Do not use
*/
public function taskRemoveMedia(ServerRequestInterface $request): ResponseInterface
{
/** @var Route $route */
$route = $this->grav['route'];
if ($route->getParam('task') === 'media.delete') {
return $this->taskMediaDelete($request);
}
throw new RuntimeException('Task removeMedia should not be called, please update form plugin!', 400);
}
/**
* Display object preview.
*
* @return ResponseInterface
*/
public function actionDisplayPreview(): ResponseInterface
{
$this->checkAuthorization('save');
$this->checkAuthorization('read');
$object = $this->getObject();
if (!$object) {
throw new RuntimeException('No object found!', 404);
}
$grav = Grav::instance();
$grav['twig']->init();
$grav['theme'];
$content = [
'code' => 200,
'id' => $object->getKey(),
'exists' => $object->exists(),
'html' => (string)$object->render('preview', ['nocache' => []])
];
$accept = $this->getAccept(['application/json', 'text/html']);
if ($accept === 'text/html') {
return $this->createHtmlResponse($content['html']);
}
if ($accept === 'application/json') {
return $this->createJsonResponse($content);
}
throw new RuntimeException('Not found', 404);
}
/**
* @param string $action
* @param string|null $scope
* @return void
* @throws RuntimeException
*/
public function checkAuthorization(string $action, string $scope = null): void
{
$object = $this->getObject();
if (!$object) {
throw new RuntimeException('Not Found', 404);
}
if ($object instanceof FlexAuthorizeInterface) {
if (!$object->isAuthorized($action, $scope, $this->user)) {
throw new RuntimeException('Forbidden', 403);
}
}
}
/**
* @param string[] $actions
* @return void
* @throws RuntimeException
*/
public function checkAuthorizations(array $actions): void
{
$object = $this->getObject();
if (!$object) {
throw new RuntimeException('Not Found', 404);
}
if ($object instanceof FlexAuthorizeInterface) {
$test = false;
foreach ($actions as $action) {
$test |= $object->isAuthorized($action, null, $this->user);
}
if (!$test) {
throw new RuntimeException('Forbidden', 403);
}
}
}
/**
* @param string $type
* @param string $name
* @return ResponseInterface
*/
protected function forwardMediaTask(string $type, string $name): ResponseInterface
{
/** @var Route $route */
$route = $this->grav['route']->withGravParam('task', null)->withGravParam($type, $name);
$object = $this->getObject();
/** @var ServerRequest $request */
$request = $this->grav['request'];
$request = $request
->withAttribute($type, $name)
->withAttribute('type', $this->type)
->withAttribute('key', $this->key)
->withAttribute('storage_key', $object && $object->exists() ? $object->getStorageKey() : null)
->withAttribute('route', $route)
->withAttribute('forwarded', true)
->withAttribute('object', $object);
$controller = new MediaController();
if ($this->user) {
$controller->setUser($this->user);
}
return $controller->handle($request);
}
}

View file

@ -0,0 +1,84 @@
<?php
namespace Grav\Plugin\FlexObjects\Events;
use Grav\Framework\Flex\Interfaces\FlexCollectionInterface;
use Grav\Framework\Flex\Interfaces\FlexDirectoryInterface;
use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
use Grav\Framework\Object\Interfaces\ObjectInterface;
use Grav\Plugin\FlexObjects\Controllers\AbstractController;
/**
* @template T as FlexObjectInterface
* @template C as FlexCollectionInterface
*/
class FlexTaskEvent
{
/** @var string */
public $task;
/** @var string */
public $type;
/** @var string */
public $key;
/** @var ObjectInterface */
private $object;
/** @var AbstractController */
private $controller;
/**
* @param AbstractController $controller
* @param string $task
*/
public function __construct(AbstractController $controller, ObjectInterface $object, string $task)
{
$this->task = $task;
$this->type = $controller->getDirectoryType();
$this->key = $controller->getObjectKey();
$this->object = $object;
$this->controller = $controller;
}
/**
* @return AbstractController
*/
public function getController(): AbstractController
{
return $this->controller;
}
/**
* @return FlexDirectoryInterface
*/
public function getDirectory(): FlexDirectoryInterface
{
return $this->getController()->getDirectory();
}
/**
* @return FlexObjectInterface
* @phpstan-return T
*/
public function getModifiedObject(): FlexObjectInterface
{
return $this->object;
}
/**
* @return FlexObjectInterface
* @phpstan-return T
*/
public function getOriginalObject(): FlexObjectInterface
{
return $this->controller->getObject();
}
/**
* @return FlexCollectionInterface
* @phpstan-return C
*/
public function getCollection(): FlexCollectionInterface
{
return $this->getController()->getDirectory()->getCollection();
}
}

View file

@ -0,0 +1,444 @@
<?php
declare(strict_types=1);
namespace Grav\Plugin\FlexObjects;
use Grav\Common\Config\Config;
use Grav\Common\Filesystem\Folder;
use Grav\Common\Grav;
use Grav\Common\Page\Interfaces\PageInterface;
use Grav\Common\Utils;
use Grav\Framework\Flex\FlexDirectory;
use Grav\Framework\Flex\FlexObject;
use Grav\Framework\Flex\Interfaces\FlexCollectionInterface;
use Grav\Framework\Flex\Interfaces\FlexCommonInterface;
use Grav\Framework\Flex\Interfaces\FlexDirectoryInterface;
use Grav\Framework\Flex\Interfaces\FlexInterface;
use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
use Grav\Plugin\FlexObjects\Admin\AdminController;
use Grav\Plugin\FlexObjects\Table\DataTable;
/**
* Class Flex
* @package Grav\Plugin\FlexObjects
*/
class Flex implements FlexInterface
{
/** @var FlexInterface */
protected $flex;
/** @var array */
protected $adminRoutes;
/** @var array */
protected $adminMenu;
/** @var array */
protected $managed;
/**
* @param bool $newToOld
* @return array
* @internal
*/
public static function getLegacyBlueprintMap(bool $newToOld = true): array
{
$map = [
'blueprints://flex-objects/pages.yaml' => 'blueprints://flex-objects/grav-pages.yaml',
'blueprints://flex-objects/user-accounts.yaml' => 'blueprints://flex-objects/grav-accounts.yaml',
'blueprints://flex-objects/user-groups.yaml' => 'blueprints://flex-objects/grav-user-groups.yaml'
];
return $newToOld ? $map : array_flip($map);
}
/**
* Flex constructor.
* @param FlexInterface $flex
* @param array $types
*/
public function __construct(FlexInterface $flex, array $types)
{
$this->flex = $flex;
$this->managed = [];
$legacy = static::getLegacyBlueprintMap(false);
foreach ($types as $blueprint) {
// Backwards compatibility to v1.0.0-rc.3
$blueprint = $legacy[$blueprint] ?? $blueprint;
$type = Utils::basename((string)$blueprint, '.yaml');
if ($type) {
$this->managed[] = $type;
}
}
}
/**
* @param string $type
* @param string $blueprint
* @param array $config
* @return $this
*/
public function addDirectoryType(string $type, string $blueprint, array $config = [])
{
$this->flex->addDirectoryType($type, $blueprint, $config);
return $this;
}
/**
* @param FlexDirectory $directory
* @return $this
*/
public function addDirectory(FlexDirectory $directory)
{
$this->flex->addDirectory($directory);
return $this;
}
/**
* @param string $type
* @return bool
*/
public function hasDirectory(string $type): bool
{
return $this->flex->hasDirectory($type);
}
/**
* @param string[]|null $types
* @param bool $keepMissing
* @return array<FlexDirectoryInterface|null>
*/
public function getDirectories(array $types = null, bool $keepMissing = false): array
{
return $this->flex->getDirectories($types, $keepMissing);
}
/**
* Get directories which are not hidden in the site.
*
* @return array
*/
public function getDefaultDirectories(): array
{
$list = $this->getDirectories();
foreach ($list as $type => $directory) {
if ($directory->getConfig('site.hidden', false)) {
unset($list[$type]);
}
}
return $list;
}
/**
* @param string $type
* @return FlexDirectory|null
*/
public function getDirectory(string $type): ?FlexDirectory
{
return $this->flex->getDirectory($type);
}
/**
* @param string $type
* @param array|null $keys
* @param string|null $keyField
* @return FlexCollectionInterface|null
*/
public function getCollection(string $type, array $keys = null, string $keyField = null): ?FlexCollectionInterface
{
return $this->flex->getCollection($type, $keys, $keyField);
}
/**
* @param array $keys
* @param array $options In addition to the options in getObjects(), following options can be passed:
* collection_class: Class to be used to create the collection. Defaults to ObjectCollection.
* @return FlexCollectionInterface
* @throws \RuntimeException
*/
public function getMixedCollection(array $keys, array $options = []): FlexCollectionInterface
{
return $this->flex->getMixedCollection($keys, $options);
}
/**
* @param array $keys
* @param array $options Following optional options can be passed:
* types: List of allowed types.
* type: Allowed type if types isn't defined, otherwise acts as default_type.
* default_type: Set default type for objects given without type (only used if key_field isn't set).
* keep_missing: Set to true if you want to return missing objects as null.
* key_field: Key field which is used to match the objects.
* @return array
*/
public function getObjects(array $keys, array $options = []): array
{
return $this->flex->getObjects($keys, $options);
}
/**
* @param string $key
* @param string|null $type
* @param string|null $keyField
* @return FlexObjectInterface|null
*/
public function getObject(string $key, string $type = null, string $keyField = null): ?FlexObjectInterface
{
return $this->flex->getObject($key, $type, $keyField);
}
/**
* @return int
*/
public function count(): int
{
return $this->flex->count();
}
public function isManaged(string $type): bool
{
return \in_array($type, $this->managed, true);
}
/**
* @return array
*/
public function getAll(): array
{
$directories = $this->getDirectories($this->managed);
$all = $this->getBlueprints();
/** @var FlexDirectory $directory */
foreach ($all as $type => $directory) {
if (!isset($directories[$type])) {
$directories[$type] = $directory;
}
}
ksort($directories);
return $directories;
}
/**
* @return array
*/
public function getBlueprints(): array
{
$params = [
'pattern' => '|\.yaml|',
'value' => 'Url',
'recursive' => false,
'folders' => false
];
$directories = [];
$all = Folder::all('blueprints://flex-objects', $params);
foreach ($all as $url) {
$type = Utils::basename($url, '.yaml');
$directory = new FlexDirectory($type, $url);
if ($directory->getConfig('hidden') !== true) {
$directories[$type] = $directory;
}
}
// Order blueprints by title.
usort($directories, static function (FlexDirectory $a, FlexDirectory $b) {
return $a->getTitle() <=> $b->getTitle();
});
return $directories;
}
/**
* @param string|FlexDirectory $type
* @param array $options
* @return DataTable
*/
public function getDataTable($type, array $options = []): DataTable
{
$directory = $type instanceof FlexDirectory ? $type : $this->getDirectory($type);
if (!$directory) {
throw new \RuntimeException('Not Found', 404);
}
$collection = $options['collection'] ?? $directory->getCollection();
if (isset($options['filters']) && is_array($options['filters'])) {
$collection = $collection->filterBy($options['filters']);
}
$table = new DataTable($options);
$table->setCollection($collection);
return $table;
}
/**
* @param string|object|null $type
* @param array $params
* @param string $extension
* @return string
*/
public function adminRoute($type = null, array $params = [], string $extension = ''): string
{
if (\is_object($type)) {
$object = $type;
if ($object instanceof FlexCommonInterface || $object instanceof FlexDirectory) {
$type = $type->getFlexType();
} else {
return '';
}
} else {
$object = null;
}
$routes = $this->getAdminRoutes();
$grav = Grav::instance();
/** @var Config $config */
$config = $grav['config'];
if (!Utils::isAdminPlugin()) {
$parts = [
trim($grav['base_url'], '/'),
trim($config->get('plugins.admin.route'), '/')
];
}
if ($type && isset($routes[$type])) {
if (!$routes[$type]) {
// Directory has empty route.
return '';
}
// Directory has it's own menu item.
$parts[] = trim($routes[$type], '/');
} else {
if (empty($routes[''])) {
// Default route has been disabled.
return '';
}
// Use default route.
$parts[] = trim($routes[''], '/');
if ($type) {
$parts[] = $type;
}
}
// Append object key if available.
if ($object instanceof FlexObject) {
if ($object->exists()) {
$parts[] = trim($object->getKey(), '/');
} else {
if ($object->hasKey()) {
$parts[] = trim($object->getKey(), '/');
}
$params = ['' => 'add'] + $params;
}
}
$p = [];
$separator = $config->get('system.param_sep');
foreach ($params as $key => $val) {
$p[] = $key . $separator . $val;
}
$parts = array_filter($parts, static function ($val) { return $val !== ''; });
$route = '/' . implode('/', $parts);
$extension = $extension ? '.' . $extension : '';
return $route . $extension . ($p ? '/' . implode('/', $p) : '');
}
public function getAdminController(): ?AdminController
{
$grav = Grav::instance();
if (!isset($grav['admin'])) {
return null;
}
/** @var PageInterface $page */
$page = $grav['page'];
$header = $page->header();
$callable = $header->controller['controller']['instance'] ?? null;
if (null !== $callable && \is_callable($callable)) {
return $callable();
}
return null;
}
/**
* @return array
*/
public function getAdminRoutes(): array
{
if (null === $this->adminRoutes) {
$routes = [];
/** @var FlexDirectory $directory */
foreach ($this->getDirectories() as $directory) {
$config = $directory->getConfig('admin');
if (!$directory->isEnabled() || !empty($config['disabled'])) {
continue;
}
// Resolve route.
$route = $config['router']['path']
?? $config['menu']['list']['route']
?? "/flex-objects/{$directory->getFlexType()}";
$routes[$directory->getFlexType()] = $route;
}
$this->adminRoutes = $routes;
}
return $this->adminRoutes;
}
/**
* @return array
*/
public function getAdminMenuItems(): array
{
if (null === $this->adminMenu) {
$routes = [];
$count = 0;
$directories = $this->getDirectories();
/** @var FlexDirectory $directory */
foreach ($directories as $directory) {
$config = $directory->getConfig('admin');
if (!$directory->isEnabled() || !empty($config['disabled'])) {
continue;
}
$type = $directory->getFlexType();
$items = $directory->getConfig('admin.menu') ?? [];
if ($items) {
foreach ($items as $view => $item) {
$item += [
'route' => '/' . $type,
'title' => $directory->getTitle(),
'icon' => 'fa fa-file',
'directory' => $type
];
$routes[$type] = $item;
}
} else {
$count++;
}
}
if ($count && !isset($routes[''])) {
$routes[''] = ['route' => '/flex-objects'];
}
$this->adminMenu = $routes;
}
return $this->adminMenu;
}
}

View file

@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
namespace Grav\Plugin\FlexObjects;
use Grav\Common\Grav;
use Grav\Common\Page\Interfaces\PageInterface;
use Grav\Common\Page\Page;
use Grav\Framework\Form\Interfaces\FormFactoryInterface;
use Grav\Framework\Form\Interfaces\FormInterface;
use RocketTheme\Toolbox\Event\Event;
use function is_callable;
use function is_string;
/**
* Class FlexFormFactory
* @package Grav\Plugin\FlexObjects
*/
class FlexFormFactory implements FormFactoryInterface
{
/**
* @param Page $page
* @param string $name
* @param array $form
* @return FormInterface|null
*/
public function createPageForm(Page $page, string $name, array $form): ?FormInterface
{
return $this->createFormForPage($page, $name, $form);
}
/**
* @param PageInterface $page
* @param string $name
* @param array $form
* @return FormInterface|null
*/
public function createFormForPage(PageInterface $page, string $name, array $form): ?FormInterface
{
// Fire event
$grav = Grav::instance();
$grav->fireEvent('onBeforeFlexFormInitialize', new Event(['page' => $page, 'name' => $name, 'form' => &$form]));
$page->addForms([$form], true);
$formFlex = $form['flex'] ?? [];
$type = $formFlex['type'] ?? null;
$key = $formFlex['key'] ?? null;
if (null !== $key && !is_string($key)) {
$key = (string)$key;
}
$layout = $formFlex['layout'] ?? $name;
/** @var Flex $flex */
$flex = Grav::instance()['flex_objects'];
if (is_string($type)) {
$directory = $flex->getDirectory($type);
if (!$directory) {
return null;
}
$create = $form['actions']['create'] ?? true;
$edit = $form['actions']['edit'] ?? true;
$object = $edit && null !== $key ? $directory->getObject($key) : null;
if ($object) {
if (is_callable([$object, 'refresh'])) {
$object->refresh();
}
} elseif ($create) {
$object = $directory->createObject([], $key ?? '');
}
} else {
$object = $flex->getObject($key);
}
return $object ? $object->getForm($layout, ['form' => $form]) : null;
}
}

View file

@ -0,0 +1,64 @@
<?php
namespace Grav\Plugin\FlexObjects;
use Grav\Framework\Route\Route;
use Grav\Plugin\FlexObjects\Controllers\MediaController;
use Grav\Plugin\FlexObjects\Controllers\ObjectController;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
/**
* Class FlexRouter
* @package Grav\Plugin\FlexObjects
*/
class FlexRouter implements MiddlewareInterface
{
/**
* @param ServerRequestInterface $request
* @param RequestHandlerInterface $handler
* @return ResponseInterface
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$context = $request->getAttributes();
/** @var Route $route */
$route = $context['route'];
$post = $request->getParsedBody();
$task = $post['task'] ?? $route->getParam('task');
if (\in_array($task, ['cropupload', 'filesupload'])) {
$task = 'media.upload';
}
switch ($task) {
case 'media.upload':
case 'media.delete':
case 'media.copy':
case 'media.remove':
case 'media.list':
case 'media.add':
case 'listmedia':
case 'addmedia':
case 'delmedia':
return (new MediaController())->handle($request);
case 'save':
case 'create':
case 'update':
case 'delete':
case 'reset':
case 'preview':
case 'move':
return (new ObjectController())->handle($request);
}
// No handler found.
return $handler->handle($request);
}
}

View file

@ -0,0 +1,410 @@
<?php
declare(strict_types=1);
namespace Grav\Plugin\FlexObjects\Table;
use Grav\Common\Debugger;
use Grav\Common\Grav;
use Grav\Framework\Collection\CollectionInterface;
use Grav\Framework\Flex\Interfaces\FlexAuthorizeInterface;
use Grav\Framework\Flex\Interfaces\FlexCollectionInterface;
use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
use JsonSerializable;
use Throwable;
use Twig\Environment;
use Twig\Error\LoaderError;
use Twig\Error\RuntimeError;
use Twig\Error\SyntaxError;
use function is_array;
use function is_string;
/**
* Class DataTable
* @package Grav\Plugin\Gitea
*
* https://github.com/ratiw/vuetable-2/wiki/Data-Format-(JSON)
* https://github.com/ratiw/vuetable-2/wiki/Sorting
*/
class DataTable implements JsonSerializable
{
/** @var string */
private $url;
/** @var int */
private $limit;
/** @var int */
private $page;
/** @var array */
private $sort;
/** @var string */
private $search;
/** @var FlexCollectionInterface */
private $collection;
/** @var FlexCollectionInterface */
private $filteredCollection;
/** @var array */
private $columns;
/** @var Environment */
private $twig;
/** @var array */
private $twig_context;
/**
* DataTable constructor.
* @param array $params
*/
public function __construct(array $params)
{
$this->setUrl($params['url'] ?? '');
$this->setLimit((int)($params['limit'] ?? 10));
$this->setPage((int)($params['page'] ?? 1));
$this->setSort($params['sort'] ?? ['id' => 'asc']);
$this->setSearch($params['search'] ?? '');
}
/**
* @param string $url
* @return void
*/
public function setUrl(string $url): void
{
$this->url = $url;
}
/**
* @param int $limit
* @return void
*/
public function setLimit(int $limit): void
{
$this->limit = max(1, $limit);
}
/**
* @param int $page
* @return void
*/
public function setPage(int $page): void
{
$this->page = max(1, $page);
}
/**
* @param string|string[] $sort
* @return void
*/
public function setSort($sort): void
{
if (is_string($sort)) {
$sort = $this->decodeSort($sort);
} elseif (!is_array($sort)) {
$sort = [];
}
$this->sort = $sort;
}
/**
* @param string $search
* @return void
*/
public function setSearch(string $search): void
{
$this->search = $search;
}
/**
* @param CollectionInterface $collection
* @return void
*/
public function setCollection(CollectionInterface $collection): void
{
$this->collection = $collection;
$this->filteredCollection = null;
}
/**
* @return int
*/
public function getLimit(): int
{
return $this->limit;
}
/**
* @return int
*/
public function getPage(): int
{
return $this->page;
}
/**
* @return int
*/
public function getLastPage(): int
{
return 1 + (int)floor(max(0, $this->getTotal()-1) / $this->getLimit());
}
/**
* @return int
*/
public function getTotal(): int
{
$collection = $this->filteredCollection ?? $this->getCollection();
return $collection ? $collection->count() : 0;
}
/**
* @return array
*/
public function getSort(): array
{
return $this->sort;
}
/**
* @return FlexCollectionInterface|null
*/
public function getCollection(): ?FlexCollectionInterface
{
return $this->collection;
}
/**
* @param int $page
* @return string|null
*/
public function getUrl(int $page): ?string
{
if ($page < 1 || $page > $this->getLastPage()) {
return null;
}
return "{$this->url}.json?page={$page}&per_page={$this->getLimit()}&sort={$this->encodeSort()}";
}
/**
* @return array
*/
public function getColumns(): array
{
if (null === $this->columns) {
$collection = $this->getCollection();
if (!$collection) {
return [];
}
$blueprint = $collection->getFlexDirectory()->getBlueprint();
$schema = $blueprint->schema();
$columns = $blueprint->get('config/admin/views/list/fields') ?? $blueprint->get('config/admin/list/fields', []);
$list = [];
foreach ($columns as $key => $options) {
if (!isset($options['field'])) {
$options['field'] = $schema->get($options['alias'] ?? $key);
}
if (!$options['field'] || !empty($options['field']['ignore'])) {
continue;
}
$list[$key] = $options;
}
$this->columns = $list;
}
return $this->columns;
}
/**
* @return array
*/
public function getData(): array
{
$grav = Grav::instance();
/** @var Debugger $debugger */
$debugger = $grav['debugger'];
$debugger->startTimer('datatable', 'Data Table');
$collection = $this->getCollection();
if (!$collection) {
return [];
}
if ($this->search !== '') {
$collection = $collection->search($this->search);
}
$columns = $this->getColumns();
$collection = $collection->sort($this->getSort());
$this->filteredCollection = $collection;
$limit = $this->getLimit();
$page = $this->getPage();
$to = $page * $limit;
$from = $to - $limit + 1;
if ($from < 1 || $from > $this->getTotal()) {
$debugger->stopTimer('datatable');
return [];
}
$array = $collection->slice($from-1, $limit);
$twig = $grav['twig'];
$grav->fireEvent('onTwigSiteVariables');
$this->twig = $twig->twig;
$this->twig_context = $twig->twig_vars;
$list = [];
/** @var FlexObjectInterface $object */
foreach ($array as $object) {
$item = [
'id' => $object->getKey(),
'timestamp' => $object->getTimestamp()
];
foreach ($columns as $name => $column) {
$item[str_replace('.', '_', $name)] = $this->renderColumn($name, $column, $object);
}
$item['_actions_'] = $this->renderActions($object);
$list[] = $item;
}
$debugger->stopTimer('datatable');
return $list;
}
/**
* @return array
*/
public function jsonSerialize(): array
{
$data = $this->getData();
$total = $this->getTotal();
$limit = $this->getLimit();
$page = $this->getPage();
$to = $page * $limit;
$from = $to - $limit + 1;
$empty = empty($data);
return [
'links' => [
'pagination' => [
'total' => $total,
'per_page' => $limit,
'current_page' => $page,
'last_page' => $this->getLastPage(),
'next_page_url' => $this->getUrl($page+1),
'prev_page_url' => $this->getUrl($page-1),
'from' => $empty ? null : $from,
'to' => $empty ? null : min($to, $total),
]
],
'data' => $data
];
}
/**
* @param string $name
* @param array $column
* @param FlexObjectInterface $object
* @return false|string
* @throws Throwable
* @throws LoaderError
* @throws RuntimeError
* @throws SyntaxError
*/
protected function renderColumn(string $name, array $column, FlexObjectInterface $object)
{
$grav = Grav::instance();
$flex = $grav['flex_objects'];
$value = $object->getFormValue($name) ?? $object->getNestedProperty($name, $column['field']['default'] ?? null);
$type = $column['field']['type'] ?? 'text';
$hasLink = $column['link'] ?? null;
$link = null;
$authorized = $object instanceof FlexAuthorizeInterface
? ($object->isAuthorized('read') || $object->isAuthorized('update')) : true;
if ($hasLink && $authorized) {
$route = $grav['route']->withExtension('');
$link = $route->withAddedPath($object->getKey())->withoutParams()->getUri();
}
$template = $this->twig->resolveTemplate(["forms/fields/{$type}/edit_list.html.twig", 'forms/fields/text/edit_list.html.twig']);
return $this->twig->load($template)->render([
'value' => $value,
'link' => $link,
'field' => $column['field'],
'object' => $object,
'flex' => $flex,
'route' => $grav['route']->withExtension('')
] + $this->twig_context);
}
/**
* @param FlexObjectInterface $object
* @return false|string
* @throws Throwable
* @throws LoaderError
* @throws RuntimeError
* @throws SyntaxError
*/
protected function renderActions(FlexObjectInterface $object)
{
$grav = Grav::instance();
$type = $object->getFlexType();
$template = $this->twig->resolveTemplate(["flex-objects/types/{$type}/list/list_actions.html.twig", 'flex-objects/types/default/list/list_actions.html.twig']);
return $this->twig->load($template)->render([
'object' => $object,
'flex' => $grav['flex_objects'],
'route' => $grav['route']->withExtension('')
] + $this->twig_context);
}
/**
* @param string $sort
* @param string $fieldSeparator
* @param string $orderSeparator
* @return array
*/
protected function decodeSort(string $sort, string $fieldSeparator = ',', string $orderSeparator = '|'): array
{
$strings = explode($fieldSeparator, $sort);
$list = [];
foreach ($strings as $string) {
$item = explode($orderSeparator, $string, 2);
$key = array_shift($item);
$order = array_shift($item) === 'desc' ? 'desc' : 'asc';
$list[$key] = $order;
}
return $list;
}
/**
* @param string $fieldSeparator
* @param string $orderSeparator
* @return string
*/
protected function encodeSort(string $fieldSeparator = ',', string $orderSeparator = '|'): string
{
$list = [];
foreach ($this->getSort() as $key => $order) {
$list[] = $key . $orderSeparator . ($order ?: 'asc');
}
return implode($fieldSeparator, $list);
}
}