1
0
mirror of https://github.com/typemill/typemill.git synced 2025-07-26 00:31:38 +02:00

demo frontend events, plugin api and web-routes, filesystem security

This commit is contained in:
trendschau
2023-08-09 20:56:45 +02:00
parent 8dee533d84
commit 2274ab42f7
16 changed files with 322 additions and 480 deletions

View File

@@ -9,6 +9,8 @@
* DONE: License feature
* ToDo: Enhance with plugins
----
## Visual Editor
* DONE: Refactor and redesign

View File

@@ -0,0 +1,2 @@
meta:
navtitle: folder

File diff suppressed because one or more lines are too long

View File

@@ -70,3 +70,9 @@
noindex: false
path: /01-cyanine-theme/03-content-elements.md
keyPath: '1.3'
/folder:
navtitle: folder
hide: false
noindex: false
path: /02-folder
keyPath: 2

File diff suppressed because one or more lines are too long

View File

@@ -54,7 +54,15 @@ class ControllerApiAuthorArticle extends Controller
# publish content
$content = new Content($urlinfo['baseurl']);
$draftMarkdown = $content->getDraftMarkdown($item);
$content->publishMarkdown($item, $draftMarkdown);
$publish = $content->publishMarkdown($item, $draftMarkdown);
if($publish !== true)
{
$response->getBody()->write(json_encode([
'message' => $publish,
]));
return $response->withHeader('Content-Type', 'application/json')->withStatus(404);
}
# refresh navigation and item
$navigation->clearNavigation();
@@ -848,12 +856,12 @@ class ControllerApiAuthorArticle extends Controller
}
# check if it is a folder and if the folder has published pages.
if($item->elementType == 'folder' && isset($item->folderContent))
if($item->elementType == 'folder')
{
if(count($item->folderContent > 0))
if($this->folderHasPublishedPages($item))
{
$response->getBody()->write(json_encode([
'message' => 'This folder contains pages, please delete the pages first.',
'message' => 'The folder contains published pages. Please unpublish or delete the pages first.',
]));
return $response->withHeader('Content-Type', 'application/json')->withStatus(422);
@@ -889,4 +897,21 @@ class ControllerApiAuthorArticle extends Controller
return $response->withHeader('Content-Type', 'application/json');
}
private function folderHasPublishedPages($folder)
{
if(isset($folder->folderContent) && is_array($folder->folderContent) && count($folder->folderContent) > 0 )
{
foreach($folder->folderContent as $page)
{
if($page->status == 'published')
{
return true;
}
}
}
return false;
}
}

View File

@@ -360,385 +360,4 @@ class ControllerWebFrontend extends Controller
return $restrictionNotice;
}
/*
# Initiate Variables
$contentHTML = false;
$item = false;
$home = false;
$breadcrumb = false;
$currentpage = false;
$this->pathToContent = $this->settings['rootPath'] . $this->settings['contentFolder'];
$this->uri = $request->getUri()->withUserInfo('');
$this->base_url = $this->uri->getBaseUrl();
# if there is no structure at all, the content folder is probably empty
if(!$this->setStructureLive())
{
return $this->render($response, '/index.twig', array( 'content' => '<h1>No Content</h1><p>Your content folder is empty.</p>' ));
}
# we can create an initial sitemap here, but makes no sense for every pagecall. Sitemap will be created on first author interaction (publish/delete/channge page).
# $this->checkSitemap();
# if the admin activated to refresh the cache automatically each 10 minutes (e.g. use without admin area)
if(isset($this->settings['refreshcache']) && $this->settings['refreshcache'] && !$this->writeCache->validate('cache', 'lastCache.txt', 600))
{
# delete the cache
$dir = $this->settings['basePath'] . 'cache';
$this->writeCache->deleteCacheFiles($dir);
# update the internal structure
$this->setFreshStructureDraft();
# update the public structure
$this->setFreshStructureLive();
# update the navigation
$this->setFreshNavigation();
# update the sitemap
$this->updateSitemap();
}
# dispatch event and let others manipulate the structure
$this->structureLive = $this->c->dispatcher->dispatch('onPagetreeLoaded', new OnPagetreeLoaded($this->structureLive))->getData();
# check if there is a custom theme css
$theme = $this->settings['theme'];
$customcss = $this->writeCache->checkFile('cache', $theme . '-custom.css');
if($customcss)
{
$this->c->assets->addCSS($this->base_url . '/cache/' . $theme . '-custom.css');
}
$logo = false;
if(isset($this->settings['logo']) && $this->settings['logo'] != '')
{
# check if logo exists
if(file_exists($this->settings['rootPath'] . 'media/live/' . $this->settings['logo']))
{
$logo = 'media/live/' . $this->settings['logo'];
}
elseif(file_exists($this->settings['rootPath'] . 'media/files/' . $this->settings['logo']))
{
$logo = 'media/files/' . $this->settings['logo'];
}
}
$favicon = false;
if(isset($this->settings['favicon']) && $this->settings['favicon'] != '')
{
$favicon = true;
$this->c->assets->addMeta('tilecolor','<meta name="msapplication-TileColor" content="#F9F8F6" />');
$this->c->assets->addMeta('tileimage','<meta name="msapplication-TileImage" content="' . $this->base_url . '/media/files/favicon-144.png" />');
$this->c->assets->addMeta('icon16','<link rel="icon" type="image/png" href="' . $this->base_url . '/media/files/favicon-16.png" sizes="16x16" />');
$this->c->assets->addMeta('icon32','<link rel="icon" type="image/png" href="' . $this->base_url . '/media/files/favicon-32.png" sizes="32x32" />');
$this->c->assets->addMeta('icon72','<link rel="apple-touch-icon" sizes="72x72" href="' . $this->base_url . '/media/files/favicon-72.png" />');
$this->c->assets->addMeta('icon114','<link rel="apple-touch-icon" sizes="114x114" href="' . $this->base_url . '/media/files/favicon-114.png" />');
$this->c->assets->addMeta('icon144','<link rel="apple-touch-icon" sizes="144x144" href="' . $this->base_url . '/media/files/favicon-144.png" />');
$this->c->assets->addMeta('icon180','<link rel="apple-touch-icon" sizes="180x180" href="' . $this->base_url . '/media/files/favicon-180.png" />');
}
# the navigation is a copy of the structure without the hidden pages
# hint: if the navigation has been deleted from the cache, then we do not recreate it here to save performace.
# Instead you have to recreate cache in admin or change a page (publish/unpublish/delete/move)
$navigation = $this->writeCache->getCache('cache', 'navigation.txt');
if(!$navigation)
{
# use the structure if there is no cached navigation
$navigation = $this->structureLive;
}
# start pagination
if(isset($args['params']))
{
$argSegments = explode("/", $args['params']);
# check if the last url segment is a number
$pageNumber = array_pop($argSegments);
if(is_numeric($pageNumber) && $pageNumber < 10000)
{
# then check if the segment before the page is a "p" that indicates a paginator
$pageIndicator = array_pop($argSegments);
if($pageIndicator == "p")
{
# use page number as current page variable
$currentpage = $pageNumber;
# set empty args for startpage
$args = [];
# if there are still params
if(!empty($argSegments))
{
# add them to the args again
$args['params'] = implode("/", $argSegments);
}
}
}
}
# if the user is on startpage
$home = false;
if(empty($args))
{
$home = true;
$item = Folder::getItemForUrl($navigation, $this->uri->getBasePath(), $this->uri->getBaseUrl(), NULL, $home);
$urlRel = $this->uri->getBasePath();
}
else
{
# get the request url, trim args so physical folders have no trailing slash
$urlRel = $this->uri->getBasePath() . '/' . trim($args['params'], "/");
# find the url in the content-item-tree and return the item-object for the file
# important to use the structure here so it is found, even if the item is hidden.
$item = Folder::getItemForUrl($this->structureLive, $urlRel, $this->uri->getBasePath());
# if the item is a folder and if that folder is not hidden
if($item && $item->elementType == 'folder' && isset($item->hide) && !$item->hide)
{
# use the navigation instead of the structure so that hidden elements are erased
$item = Folder::getItemForUrl($navigation, $urlRel, $this->uri->getBaseUrl(), NULL, $home);
}
# if there is still no item, return a 404-page
if(!$item)
{
return $this->render404($response, array(
'navigation' => $navigation,
'settings' => $this->settings,
'base_url' => $this->base_url,
'title' => false,
'content' => false,
'item' => false,
'breadcrumb' => false,
'metatabs' => false,
'image' => false,
'logo' => $logo,
'favicon' => $favicon
));
}
}
###
if(isset($item->hide))
{
# if it is a hidden page
if($item->hide)
{
# get breadcrumb for page and set pages active
# use structure here because the hidden item is not part of the navigation
$breadcrumb = Folder::getBreadcrumb($this->structureLive, $item->keyPathArray);
$breadcrumb = $this->c->dispatcher->dispatch('onBreadcrumbLoaded', new OnBreadcrumbLoaded($breadcrumb))->getData();
# add the paging to the item
$item = Folder::getPagingForItem($this->structureLive, $item);
}
else
{
# get breadcrumb for page and set pages active
# use navigation, because it is used for frontend
$breadcrumb = Folder::getBreadcrumb($navigation, $item->keyPathArray);
$breadcrumb = $this->c->dispatcher->dispatch('onBreadcrumbLoaded', new OnBreadcrumbLoaded($breadcrumb))->getData();
# add the paging to the item
$item = Folder::getPagingForItem($navigation, $item);
}
}
###
# dispatch the item
$item = $this->c->dispatcher->dispatch('onItemLoaded', new OnItemLoaded($item))->getData();
# set the filepath
$filePath = $this->pathToContent . $item->path;
# check if url is a folder and add index.md
if($item->elementType == 'folder')
{
$filePath = $filePath . DIRECTORY_SEPARATOR . 'index.md';
}
# read the content of the file
$contentMD = file_exists($filePath) ? file_get_contents($filePath) : false;
# dispatch the original content without plugin-manipulations for case anyone wants to use it
$this->c->dispatcher->dispatch('onOriginalLoaded', new OnOriginalLoaded($contentMD));
# initiate object for metadata
$writeMeta = new WriteMeta();
# makes sure that you always have the full meta with title, description and all the rest.
$metatabs = $writeMeta->completePageMeta($contentMD, $this->settings, $item);
# write meta
if(isset($metatabs['meta']['noindex']) && $metatabs['meta']['noindex'])
{
$this->c->assets->addMeta('noindex','<meta name="robots" content="noindex">');
}
$this->c->assets->addMeta('og_site_name','<meta property="og:site_name" content="' . $this->settings['title'] . '">');
$this->c->assets->addMeta('og_title','<meta property="og:title" content="' . $metatabs['meta']['title'] . '">');
$this->c->assets->addMeta('og_description','<meta property="og:description" content="' . $metatabs['meta']['description'] . '">');
$this->c->assets->addMeta('og_type','<meta property="og:type" content="article">');
$this->c->assets->addMeta('og_url','<meta property="og:url" content="' . $item->urlAbs . '">');
# dispatch meta
$metatabs = $this->c->dispatcher->dispatch('onMetaLoaded', new OnMetaLoaded($metatabs))->getData();
# dispatch content
$contentMD = $this->c->dispatcher->dispatch('onMarkdownLoaded', new OnMarkdownLoaded($contentMD))->getData();
$itemUrl = isset($item->urlRel) ? $item->urlRel : false;
/* initialize parsedown
$parsedown = new ParsedownExtension($this->base_url, $this->settings, $this->c->dispatcher);
/* set safe mode to escape javascript and html in markdown
$parsedown->setSafeMode(true);
####
# check access restriction here
$restricted = $this->checkRestrictions($metatabs['meta']);
if($restricted)
{
# convert markdown into array of markdown block-elements
$markdownBlocks = $parsedown->markdownToArrayBlocks($contentMD);
# infos that plugins need to add restriction content
$restrictions = [
'restricted' => $restricted,
'defaultContent' => true,
'markdownBlocks' => $markdownBlocks,
];
# dispatch the data
$restrictions = $this->c->dispatcher->dispatch('onRestrictionsLoaded', new OnRestrictionsLoaded( $restrictions ))->getData();
# use the returned markdown
$markdownBlocks = $restrictions['markdownBlocks'];
# if no plugin has disabled the default behavior
if($restrictions['defaultContent'])
{
# cut the restricted content
$shortenedPage = $this->cutRestrictedContent($markdownBlocks);
# check if there is customized content
$restrictionnotice = $this->prepareRestrictionNotice();
# add notice to shortened content
$shortenedPage[] = $restrictionnotice;
# Use the shortened page
$markdownBlocks = $shortenedPage;
}
# finally transform the markdown blocks back to pure markdown text
$contentMD = $parsedown->arrayBlocksToMarkdown($markdownBlocks);
}
###
/* parse markdown-file to content-array
$contentArray = $parsedown->text($contentMD);
$contentArray = $this->c->dispatcher->dispatch('onContentArrayLoaded', new OnContentArrayLoaded($contentArray))->getData();
/* parse markdown-content-array to content-string
$contentHTML = $parsedown->markup($contentArray);
$contentHTML = $this->c->dispatcher->dispatch('onHtmlLoaded', new OnHtmlLoaded($contentHTML))->getData();
/* extract the h1 headline
$contentParts = explode("</h1>", $contentHTML, 2);
$title = isset($contentParts[0]) ? strip_tags($contentParts[0]) : $this->settings['title'];
$contentHTML = isset($contentParts[1]) ? $contentParts[1] : $contentHTML;
# get the first image from content array
$img_url = isset($metatabs['meta']['heroimage']) ? $metatabs['meta']['heroimage'] : false;
$img_alt = isset($metatabs['meta']['heroimagealt']) ? $metatabs['meta']['heroimagealt'] : false;
# get url and alt-tag for first image, if exists
if(!$img_url OR $img_url == '')
{
# extract first image from content
$firstImageMD = $this->getFirstImage($contentArray);
if($firstImageMD)
{
preg_match('#\((.*?)\)#', $firstImageMD, $img_url_result);
$img_url = isset($img_url_result[1]) ? $img_url_result[1] : false;
if($img_url)
{
preg_match('#\[(.*?)\]#', $firstImageMD, $img_alt_result);
$img_alt = isset($img_alt_result[1]) ? $img_alt_result[1] : false;
}
}
elseif($logo)
{
$img_url = $logo;
$pathinfo = pathinfo($this->settings['logo']);
$img_alt = $pathinfo['filename'];
}
}
$firstImage = false;
if($img_url)
{
$firstImage = array('img_url' => $this->base_url . '/' . $img_url, 'img_alt' => $img_alt);
$this->c->assets->addMeta('og_image','<meta property="og:image" content="' . $this->base_url . '/' . $img_url . '">');
$this->c->assets->addMeta('twitter_image_alt','<meta name="twitter:image:alt" content="' . $img_alt . '">');
$this->c->assets->addMeta('twitter_card','<meta name="twitter:card" content="summary_large_image">');
}
$route = empty($args) && isset($this->settings['themes'][$theme]['cover']) ? '/cover.twig' : '/index.twig';
return $this->render($response, $route, [
'home' => $home,
'navigation' => $navigation,
'title' => $title,
'content' => $contentHTML,
'item' => $item,
'breadcrumb' => $breadcrumb,
'settings' => $this->settings,
'base_url' => $this->base_url,
'metatabs' => $metatabs,
'image' => $firstImage,
'logo' => $logo,
'favicon' => $favicon,
'currentpage' => $currentpage
]);
}
*/
}

View File

@@ -2,6 +2,8 @@
namespace Typemill\Controllers;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use Typemill\Models\Navigation;
use Typemill\Models\Extension;
use Typemill\Models\User;
@@ -10,7 +12,7 @@ use Typemill\Models\Settings;
class ControllerWebSystem extends Controller
{
public function showSettings($request, $response, $args)
public function showSettings(Request $request, Response $response, $args)
{
$navigation = new Navigation();
$mainNavigation = $navigation->getMainNavigation(
@@ -49,7 +51,7 @@ class ControllerWebSystem extends Controller
]);
}
public function showThemes($request, $response, $args)
public function showThemes(Request $request, Response $response, $args)
{
$navigation = new Navigation();
$mainNavigation = $navigation->getMainNavigation(
@@ -101,7 +103,7 @@ class ControllerWebSystem extends Controller
]);
}
public function showPlugins($request, $response, $args)
public function showPlugins(Request $request, Response $response, $args)
{
$navigation = new Navigation();
$mainNavigation = $navigation->getMainNavigation(
@@ -152,7 +154,7 @@ class ControllerWebSystem extends Controller
]);
}
public function showLicense($request, $response, $args)
public function showLicense(Request $request, Response $response, $args)
{
$navigation = new Navigation();
$mainNavigation = $navigation->getMainNavigation(
@@ -192,7 +194,7 @@ class ControllerWebSystem extends Controller
]);
}
public function showAccount($request, $response, $args)
public function showAccount(Request $request, Response $response, $args)
{
$navigation = new Navigation();
$mainNavigation = $navigation->getMainNavigation(
@@ -230,7 +232,7 @@ class ControllerWebSystem extends Controller
]);
}
public function showUsers($request, $response, $args)
public function showUsers(Request $request, Response $response, $args)
{
$navigation = new Navigation();
$mainNavigation = $navigation->getMainNavigation(
@@ -274,7 +276,7 @@ class ControllerWebSystem extends Controller
]);
}
public function showUser($request, $response, $args)
public function showUser(Request $request, Response $response, $args)
{
$navigation = new Navigation();
$mainNavigation = $navigation->getMainNavigation(
@@ -316,7 +318,7 @@ class ControllerWebSystem extends Controller
]);
}
public function newUser($request, $response, $args)
public function newUser(Request $request, Response $response, $args)
{
$navigation = new Navigation();
$mainNavigation = $navigation->getMainNavigation(
@@ -345,24 +347,37 @@ class ControllerWebSystem extends Controller
]);
}
/*
public function showBlank($request, $response, $args)
public function blankSystemPage(Request $request, Response $response, $args)
{
$user = new User();
$settings = $this->c->get('settings');
$route = $request->getAttribute('route');
$navigation = $this->getMainNavigation();
$navigation = new Navigation();
$mainNavigation = $navigation->getMainNavigation(
$userrole = $request->getAttribute('c_userrole'),
$acl = $this->c->get('acl'),
$urlinfo = $this->c->get('urlinfo'),
$editor = $this->settings['editor']
);
$userrole = $request->getAttribute('c_userrole');
$acl = $this->c->get('acl');
$urlinfo = $this->c->get('urlinfo');
$editor = $this->settings['editor'];
$content = '<h1>Hello</h1><p>I am the showBlank method from the settings controller</p><p>In most cases I have been called from a plugin. But if you see this content, then the plugin does not work or has forgotten to inject its own content.</p>';
$systemNavigation = $navigation->getSystemNavigation(
$userrole = $request->getAttribute('c_userrole'),
$acl = $this->c->get('acl'),
$urlinfo = $this->c->get('urlinfo'),
$dispatcher = $this->c->get('dispatcher')
);
return $this->render($response, 'settings/blank.twig', array(
'settings' => $settings,
'acl' => $this->c->acl,
'navigation' => $navigation,
'content' => $content,
'route' => $route->getName()
));
}
*/
return $this->c->get('view')->render($response, 'layouts/layoutSystemBlank.twig', [
'settings' => $this->settings,
'mainnavi' => $mainNavigation,
'systemnavi' => $systemNavigation,
'jsdata' => [
'settings' => $this->settings,
'labels' => $this->c->get('translations'),
'urlinfo' => $this->c->get('urlinfo'),
'acl' => $this->c->get('acl'),
]
]);
}
}

View File

@@ -113,22 +113,31 @@ class Content
return $this->storage->getError();
}
public function deletePage($item)
public function deletePage($item, $result = NULL)
{
$extensions = ['.md', '.txt', '.yaml'];
$result = true;
if($item->elementType == 'folder' && isset($item->folderContent) && is_array($item->folderContent))
{
foreach($item->folderContent as $content)
{
$result = $this->deletePage($content);
if($result !== true){ break; }
}
}
foreach($extensions as $extension)
{
$result = $this->storage->deleteFile('contentFolder', '', $item->pathWithoutType . $extension);
}
if($result)
if($result !== true)
{
return true;
return $this->storage->getError();
}
return $this->storage->getError();
return true;
}
public function addDraftHtml($markdownArray)

View File

@@ -37,6 +37,10 @@ class Storage
protected $translationFolder = false;
protected $systemSettings = false;
protected $isReadable = [];
protected $isWrtiable = [];
public function __construct()
{
@@ -69,6 +73,19 @@ class Storage
$this->translationFolder = $this->basepath . 'system' . DIRECTORY_SEPARATOR . 'typemill' . DIRECTORY_SEPARATOR . 'author' . DIRECTORY_SEPARATOR . 'translations' . DIRECTORY_SEPARATOR;
$this->systemSettings = $this->basepath . 'system' . DIRECTORY_SEPARATOR . 'typemill' . DIRECTORY_SEPARATOR . 'settings';
$this->isWritable = [
'tmpFolder' => true,
'originalFolder' => true,
'liveFolder' => true,
'thumbsFolder' => true,
'customFolder' => true,
'fileFolder' => true,
'contentFolder' => true,
'dataFolder' => true,
'cacheFolder' => true,
'settingsFolder' => true
];
}
public function getError()
@@ -78,26 +95,31 @@ class Storage
public function getFolderPath($location, $folder = NULL)
{
# security: remove ../ from location
# security: make sure user does not write into basepath
# security: write only into certain folders
if(isset($this->$location))
{
$path = rtrim($this->$location, DIRECTORY_SEPARATOR);
$path .= DIRECTORY_SEPARATOR;
if($folder && $folder != '')
# check if folder is no hack like "../"
if($folder && $folder != '' && preg_match('/^(?:[\/\\a-z0-9_-]|\.(?!\.))+$/iD', $folder))
{
$folder = trim($folder, DIRECTORY_SEPARATOR);
$path .= $folder . DIRECTORY_SEPARATOR;
}
# echo '<pre>';
# echo $path;
elseif($location == 'basepath')
{
# do not allow direct access to basepath files
$this->error = "Access to basepath is not allowed.";
return false;
}
return $path;
}
$this->error = "We could not find a folderPath for $location";
return false;
}
@@ -152,7 +174,7 @@ class Storage
{
if($this->checkFile($location, $folder, $filename))
{
$filepath = $this->getFolderPath($location) . $folder . DIRECTORY_SEPARATOR . $filename;
$filepath = $this->getFolderPath($location, $folder) . $filename;
$fileContent = file_get_contents($filepath);
@@ -184,6 +206,13 @@ class Storage
public function writeFile($location, $folder, $filename, $data, $method = NULL)
{
if(!isset($this->isWritable[$location]))
{
$this->error = "It is not allowed to write into $location";
return false;
}
# CLEAN EVERYTHING UP FUNCTION
$folder = trim($folder, DIRECTORY_SEPARATOR);
$folder = ($folder == '') ? '' : $folder . DIRECTORY_SEPARATOR;
@@ -197,8 +226,8 @@ class Storage
}
}
$filepath = $this->getFolderPath($location) . $folder . $filename;
$filepath = $this->getFolderPath($location, $folder) . $filename;
$openfile = @fopen($filepath, "w");
if(!$openfile)
{
@@ -228,6 +257,13 @@ class Storage
public function renameFile($location, $folder, $oldname, $newname)
{
if(!isset($this->isWritable[$location]))
{
$this->error = "It is not allowed to write into $location";
return false;
}
$folder = trim($folder, DIRECTORY_SEPARATOR);
$oldFilePath = $this->getFolderPath($location) . $folder . DIRECTORY_SEPARATOR . $oldname;
@@ -249,12 +285,47 @@ class Storage
return true;
}
public function deleteFolder($location, $folder, $filename)
{
if(!isset($this->isWritable[$location]))
{
$this->error = "It is not allowed to write into $location";
return false;
}
$filepath = $this->getFolderPath($location, $folder) . $filename;
if(is_dir($filepath))
{
if(rmdir($dir))
{
return true;
}
$this->error = "We found the folder but could not delete $filepath";
return false;
}
$this->error = "The path $filepath is not a folder.";
return false;
}
public function deleteFile($location, $folder, $filename)
{
if(!isset($this->isWritable[$location]))
{
$this->error = "It is not allowed to write into $location";
return false;
}
if($this->checkFile($location, $folder, $filename))
{
$filepath = $this->getFolderPath($location) . $folder . DIRECTORY_SEPARATOR . $filename;
if(unlink($filepath))
{
return true;
@@ -329,6 +400,13 @@ class Storage
public function updateYaml($location, $folder, $filename, $contentArray)
{
if(!isset($this->isWritable[$location]))
{
$this->error = "It is not allowed to write into $location";
return false;
}
$yaml = \Symfony\Component\Yaml\Yaml::dump($contentArray,6);
if($this->writeFile($location, $folder, $filename, $yaml))
{

View File

@@ -45,28 +45,16 @@ class Plugins
{
# add the routes
$pluginRoutes = $className::addNewRoutes();
# multi-dimensional or simple array of routes
if(isset($pluginRoutes[0]))
foreach($pluginRoutes as $pluginRoute)
{
# if they are properly formatted, add them to routes array
foreach($pluginRoutes as $pluginRoute)
if(self::checkRouteArray($routes,$pluginRoute))
{
if(self::checkRouteArray($routes,$pluginRoute))
{
$pluginRoute['route'] = strtolower($pluginRoute['route']);
$routes[] = $pluginRoute;
}
$routeType = (substr($pluginRoute['route'], 0,5) == '/api/') ? 'api' : 'web';
$pluginRoute['route'] = strtolower($pluginRoute['route']);
$routes[$routeType][] = $pluginRoute;
}
}
elseif(is_array($routes))
{
if(self::checkRouteArray($routes,$pluginRoutes))
{
$pluginRoutes['route'] = strtolower($pluginRoutes['route']);
$routes[] = $pluginRoutes;
}
}
}
}
return $routes;
@@ -113,7 +101,9 @@ class Plugins
if(
isset($route['httpMethod']) AND in_array($route['httpMethod'], array('get','post','put','delete','head','patch','options'))
AND isset($route['route']) AND is_string($route['route'])
AND isset($route['class']) AND is_string($route['class']))
AND isset($route['class']) AND is_string($route['class'])
AND isset($route['name']) AND is_string($route['name'])
)
{
return true;
}

View File

@@ -265,7 +265,8 @@ const publisher = Vue.createApp({
{
if(error.response)
{
alert(error.response);
self.message = error.response.data.message;
self.messageClass = "bg-rose-500";
}
});
},
@@ -301,7 +302,8 @@ const publisher = Vue.createApp({
{
if(error.response)
{
alert(error.response);
self.message = error.response.data.message;
self.messageClass = "bg-rose-500";
}
});
},
@@ -326,7 +328,8 @@ const publisher = Vue.createApp({
{
if(error.response)
{
alert(error.response);
self.message = error.response.data.message;
self.messageClass = "bg-rose-500";
}
});
},
@@ -356,8 +359,10 @@ const publisher = Vue.createApp({
{
if(error.response)
{
alert(error.response);
}
self.showModal = false;
self.message = error.response.data.message;
self.messageClass = "bg-rose-500";
}
});
},
},

View File

@@ -43,10 +43,6 @@
</article>
</div>
<footer class="max-w-6xl m-auto bg-stone-50">
Typemill version xyz
</footer>
<!-- < csrf() | raw > -->
<script>

View File

@@ -0,0 +1,82 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{% block title %}{% endblock %}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="TYPEMILL system extension"/>
<meta name="msapplication-TileColor" content="#F9F8F6" />
<meta name="msapplication-TileImage" content="{{ base_url() }}/system/author/img/favicon-144.png" />
<link rel="icon" type="image/png" href="{{ base_url() }}/system/author/img/favicon-16.png" sizes="16x16" />
<link rel="icon" type="image/png" href="{{ base_url() }}/system/author/img/favicon-32.png" sizes="32x32" />
<link rel="apple-touch-icon" sizes="72x72" href="{{ base_url() }}/system/author/img/favicon-72.png" />
<link rel="apple-touch-icon" sizes="114x114" href="{{ base_url() }}/system/author/img/favicon-114.png" />
<link rel="apple-touch-icon" sizes="144x144" href="{{ base_url() }}/system/author/img/favicon-144.png" />
<link rel="apple-touch-icon" sizes="180x180" href="{{ base_url() }}/system/author/img/favicon-180.png" />
<link rel="stylesheet" href="{{ base_url() }}/system/typemill/author/css/output.css?v={{ settings.version }}" />
<link rel="stylesheet" href="{{ base_url() }}/system/typemill/author/css/custom.css?v={{ settings.version }}" />
{% block stylesheet %}{% endblock %}
{{ assets.renderCSS() }}
</head>
<body class="bg-stone-100">
{% include 'partials/symbols.twig' %}
<header class="border-b-2 border-stone-200">
{% include 'partials/mainNavi.twig' %}
</header>
<div class="max-w-6xl m-auto mt-7 flex justify-between" id="main" data-url="{{ base_url() }}">
<aside class="w-1/4">
{% include 'partials/systemNavi.twig' %}
</aside>
<article class="w-3/4 bg-stone-50 shadow-md p-8">
<h1 class="text-3xl font-bold mb-4">{{ translate('System') }} </h1>
<div id="system" v-cloak></div>
</article>
</div>
<script>
const data = {{ jsdata | json_encode() | raw }};
</script>
<script src="{{ base_url() }}/system/typemill/author/js/axios.min.js?v={{ settings.version }}"></script>
<script>
const tmaxios = axios.create();
tmaxios.defaults.baseURL = "{{ base_url() }}";
tmaxios.defaults.headers.common['X-Session-Auth'] = "true";
</script>
<script src="{{ base_url() }}/system/typemill/author/js/highlight.min.js?v={{ settings.version }}"></script>
<script src="{{ base_url() }}/system/typemill/author/js/vue.js?v={{ settings.version }}"></script>
<script src="{{ base_url() }}/system/typemill/author/js/vue-eventbus.js?v={{ settings.version }}"></script>
<script src="{{ base_url() }}/system/typemill/author/js/vue-shared.js?v={{ settings.version }}"></script>
<script src="{{ base_url() }}/system/typemill/author/js/vue-medialib.js?v={{ settings.version }}"></script>
<script src="{{ base_url() }}/system/typemill/author/js/vue-forms.js?v={{ settings.version }}"></script>
{% block javascript %}{% endblock %}
{{ assets.renderJS() }}
<script>
app.config.globalProperties.$filters = translatefilter;
app.mount('#system');
/* kixote.mount('#kixote'); */
</script>
</body>
</html>

View File

@@ -82,4 +82,26 @@ $app->group('/api/v1', function (RouteCollectorProxy $group) use ($acl) {
$group->get('/meta', ControllerApiAuthorMeta::class . ':getMeta')->setName('api.meta.get')->add(new ApiAuthorization($acl, 'mycontent', 'view'));
$group->post('/meta', ControllerApiAuthorMeta::class . ':updateMeta')->setName('api.metadata.update')->add(new ApiAuthorization($acl, 'mycontent', 'update'));
})->add(new ApiAuthentication());
})->add(new ApiAuthentication());
# api-routes from plugins
foreach($routes['api'] as $pluginRoute)
{
$method = $pluginRoute['httpMethod'] ?? false;
$route = $pluginRoute['route'] ?? false;
$class = $pluginRoute['class'] ?? false;
$name = $pluginRoute['name'] ?? false;
$resource = $pluginRoute['resource'] ?? false;
$privilege = $pluginRoute['privilege'] ?? false;
if($resources && $privilege)
{
# protected api requires authentication and authorization
$app->{$method}($route, $class)->setName($name)->add(new ApiAuthorization($acl, $resource, $privilege))->add(new ApiAuthentication());
}
else
{
# public api routes
$app->{$method}($route, $class)->setName($name);
}
}

View File

@@ -54,34 +54,24 @@ $app->redirect('/tm/', $routeParser->urlFor('auth.show'), 302);
# downloads
$app->get('/media/files[/{params:.*}]', ControllerWebDownload::class . ':download')->setName('download.file');
foreach($routes as $pluginRoute)
# web-routes from plugins
foreach($routes['web'] as $pluginRoute)
{
$method = $pluginRoute['httpMethod'];
$route = $pluginRoute['route'];
$class = $pluginRoute['class'];
# $resource = isset($pluginRoute['resource']) ? $pluginRoute['resource'] : NULL;
# $privilege = isset($pluginRoute['privilege']) ? $pluginRoute['privilege'] : NULL;
$method = $pluginRoute['httpMethod'] ?? false;
$route = $pluginRoute['route'] ?? false;
$class = $pluginRoute['class'] ?? false;
$name = $pluginRoute['name'] ?? false;
$resource = $pluginRoute['resource'] ?? false;
$privilege = $pluginRoute['privilege'] ?? false;
# echo '<br>';
# echo 'method: ' . $method . ' -> route: ' . $route . ' -> class: ' . $class;
if(isset($pluginRoute['name']))
if($resources && $privilege)
{
# $app->{$method}($route, $class)->setName($pluginRoute['name'])->add(new accessMiddleware($container['router'], $container['acl'], $resource, $privilege));
$app->{$method}($route, $class)->setName($pluginRoute['name']);
$app->{$method}($route, $class)->setName($name)->add(new WebAuthorization($routeParser, $acl, $resource, $privilege))->add(new WebRedirectIfUnauthenticated($routeParser));
}
else
{
# $app->{$method}($route, $class)->add(new accessMiddleware($container['router'], $container['acl'], $resource, $privilege));
$app->{$method}($route, $class);
$app->{$method}($route, $class)->setName($name);
}
# if api and if authorization
# ->add(new ApiAuthorization($acl, 'account', 'view'));
# if web and if authorization
# ->add(new WebAuthorization($acl, 'account', 'view'));
}
# die();
# website
$app->get('/[{route:.*}]', ControllerWebFrontend::class . ':index')->setName('home');